Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
43bf4fd
fix(windows): restore quick-add draft when parsing or saving fails
turinglambdaai Oct 20, 2026
a9043d0
fix(windows): require an explicit target list for every quick-add task
turinglambdaai Oct 20, 2026
4c3d840
fix(windows): route quick add to selected list and align task checkbo…
turinglambdaai Oct 20, 2026
1f25649
test(windows): give new-list action a stable accessibility ID
turinglambdaai Oct 20, 2026
b159866
test(windows): expose stable automation selector for list-name prompt
turinglambdaai Oct 20, 2026
b0a044f
test(windows): expose active view title for list-routing regression
turinglambdaai Oct 20, 2026
4801779
test(windows): reproduce wrong-list Enter save and plain-row checkbox…
turinglambdaai Oct 20, 2026
7ed7b5d
docs: require correct list ownership and aligned plain task rows on e…
turinglambdaai Oct 20, 2026
93734e1
feat(windows): declare task move-to-list operation
turinglambdaai Oct 20, 2026
61f9e56
feat(windows): allow moving existing tasks between lists from context…
turinglambdaai Oct 20, 2026
2aa3ae2
docs: require explicit recovery of legacy misfiled Windows tasks
turinglambdaai Oct 20, 2026
73307d4
fix(windows): avoid showing All tasks under restored list heading
turinglambdaai Oct 20, 2026
108aec1
test(windows): find visible list text instead of inaccessible Grid IDs
turinglambdaai Oct 20, 2026
42e4924
fix(windows): expose list names through accessible TextBlock peers
turinglambdaai Oct 20, 2026
d8d6a33
fix(windows): expose unified task lookup for search actions
turinglambdaai Oct 20, 2026
ba1d5ec
fix(windows): allow editing searched tasks and show owning list
turinglambdaai Oct 20, 2026
ede8a04
fix(windows): refresh task search after edits
turinglambdaai Oct 20, 2026
f2837ed
fix(windows): keep search results actionable and fresh
turinglambdaai Oct 20, 2026
fd2144c
test(windows): expose task search automation selector
turinglambdaai Oct 20, 2026
c6932cd
test(windows): verify global search displays task ownership
turinglambdaai Oct 20, 2026
3c4863a
feat(windows): use native Extended task selection
turinglambdaai Oct 20, 2026
683f745
feat(windows): wire task-list keyboard selection handler
turinglambdaai Oct 20, 2026
b5c20e6
feat(windows): preserve task selections and support Enter/Escape navi…
turinglambdaai Oct 20, 2026
93feb1e
test(windows): expose task lists to accessibility automation
turinglambdaai Oct 20, 2026
4e84ad7
test(windows): verify keyboard multi-selection support in packaged host
turinglambdaai Oct 20, 2026
7dc4be1
docs: track Windows native task selection and remaining bulk action work
turinglambdaai Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/UX-BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ Scheduled(有日期的)。

- ✅ macOS 已落地:⌘/⇧ 多选 + 批量完成/排期/移动/删除,删除走单条撤销横幅
(PRODUCT-SPEC §5b 已收录批量语义)。
- 🔜 短期:Windows/Linux 跟进同一套多选与批量语义(见 README 平台状态)。
- ✅ Windows 列表选择基础:WinUI Extended 模式支持 Ctrl/Shift
多选、方向键与范围选择;Enter 打开当前选中任务的编辑对话框,Esc
清除选择。按任务 ID 在列表刷新后恢复选择(普通列表与搜索结果)。
- 🔜 **尚未完成**:Windows 的批量完成/排期/移动/删除、行内详情展开和撤销;
Linux 的批量操作;以及三平台按真实快捷键进行实机回归。

### 2.5 Quick Find

Expand Down Expand Up @@ -113,3 +117,15 @@ Scheduled(有日期的)。
https://digitoolbook.com/en/blog/things3-minimalist-task-management-gtd
- 2Do 官方功能页:https://www.2doapp.com/page-section/main-features
- 2Do Focus Lists:https://www.2doapp.com/docs/ios/focus-lists

### Windows task ownership recovery

- ✅ Task rows expose a right-click **Move to List / 移动到列表** submenu for
other lists. It calls `update_task` with the target list ID, preserving
title, due date/time, completion state and notes.
- This is the **explicit** recovery path for tasks already saved into the
wrong list by older Windows versions. Do not auto-migrate existing rows:
the original intended list cannot be inferred reliably.
- Manual acceptance: move a task from 工作 to 生活, confirm it disappears from
工作, appears in 生活 and keeps all other fields; then restart and verify
ownership persists. Exercise both languages and all smart-list views.
120 changes: 119 additions & 1 deletion scripts/e2e/e2e-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,119 @@ try {
return $false
})

# Regression: a task entered in 生活 must stay in 生活, not the
# database's first/default list 工作. These are the shipped default
# list names, from racket/taskly/model.rkt. The disposable acceptance
# profile guarantees default-list id 1 and the new 生活 list id 2.
$createList = Wait-Until 'create-list control' { Find-ById $window 'create-list' }
$createList.GetCurrentPattern(
[System.Windows.Automation.InvokePattern]::Pattern).Invoke()
$nameInput = Wait-Until 'new-list input' { Find-ById $window 'prompt-input' }
$nameInput.GetCurrentPattern(
[System.Windows.Automation.ValuePattern]::Pattern).SetValue('生活')
$confirm = Wait-Until 'new-list confirmation' { Find-ByName $window 'OK' }
$confirm.GetCurrentPattern(
[System.Windows.Automation.InvokePattern]::Pattern).Invoke()

function Select-TaskList {
param([int]$ListId, [string]$Name)
# WinUI's inner Grid is not a ControlView automation peer (verified
# against the failing packaged Windows screenshot). Resolve the
# displayed TextBlock instead, then select its ListViewItem parent.
$label = Wait-Until ("visible list: {0}" -f $Name) {
$found = Find-ById $window ("list-name-{0}" -f $ListId)
if ($null -eq $found) { $found = Find-ByName $window $Name }
return $found
}
$item = $label
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
while ($null -ne $item -and
$item.Current.ControlType -ne [System.Windows.Automation.ControlType]::ListItem) {
$item = $walker.GetParent($item)
}
if ($null -eq $item) { throw ("no selectable list item for {0}" -f $Name) }
$item.GetCurrentPattern(
[System.Windows.Automation.SelectionItemPattern]::Pattern).Select()
}

$paneTitle = Wait-Until 'active view label' {
Find-ById $window 'current-view-title'
}
Select-TaskList 2 '生活'
[void](Wait-Until '生活 view selected' {
if ($paneTitle.Current.Name -eq '生活') { return $true }
return $false
})
$plainTask = 'Life-only task without due date or notes'
$quickAdd.GetCurrentPattern(
[System.Windows.Automation.ValuePattern]::Pattern).SetValue($plainTask)
$quickAdd.SetFocus()
# Exercise the actual Enter key, not only the plus-button click.
[System.Windows.Forms.SendKeys]::SendWait('{ENTER}')

$titleText = Wait-Until 'task saved under 生活' {
Find-ByName $window $plainTask
}
# Layout assertions use the text and native checkbox peers: a Grid
# itself is not reliably exposed in WinUI UIA's ControlView.
$plainCheck = Wait-Until 'new task checkbox' {
Find-ById $window 'task-complete-2'
}
# Both elements share the row's first Grid row; the title must
# visually align with the native checkbox for a metadata-free task.
$checkBoxBounds = $plainCheck.Current.BoundingRectangle
$titleBounds = $titleText.Current.BoundingRectangle
$centerDiff = [Math]::Abs(
($checkBoxBounds.Top + $checkBoxBounds.Height / 2) -
($titleBounds.Top + $titleBounds.Height / 2))
$limit = [Math]::Max(8.0, $checkBoxBounds.Height / 4)
if ($centerDiff -gt $limit) {
throw ("plain task title and checkbox are misaligned by {0:N1}px" -f $centerDiff)
}

$taskList = Wait-Until 'extended task list' {
Find-ById $window 'task-list'
}
$selection = $taskList.GetCurrentPattern(
[System.Windows.Automation.SelectionPattern]::Pattern)
if (-not $selection.Current.CanSelectMultiple) {
throw 'Windows task list must support Ctrl/Shift multi-selection'
}

Select-TaskList 1 '工作'
[void](Wait-Until '工作 view selected' {
if ($paneTitle.Current.Name -eq '工作') { return $true }
return $false
})
[void](Wait-Until '生活 task absent from 工作' {
if ($null -eq (Find-ByName $window $plainTask)) { return $true }
return $false
})
Select-TaskList 2 '生活'
[void](Wait-Until '生活 task remains in 生活' {
if ($paneTitle.Current.Name -eq '生活' -and
$null -ne (Find-ByName $window $plainTask)) { return $true }
return $false
})

# Search is global, even while 生活 is selected. A result must
# display its owning list, never appear as an unlabeled local row.
$searchBox = Wait-Until 'search box' { Find-ById $window 'task-search' }
$searchBox.GetCurrentPattern(
[System.Windows.Automation.ValuePattern]::Pattern).SetValue($plainTask)
$owner = Wait-Until 'search result list owner' {
Find-ById $window 'task-owner-2'
}
if ($owner.Current.Name -ne '生活') {
throw ("search result claims wrong owner: {0}" -f $owner.Current.Name)
}
$searchBox.GetCurrentPattern(
[System.Windows.Automation.ValuePattern]::Pattern).SetValue('')
[void](Wait-Until 'search cleared' {
if ($null -eq (Find-ById $window 'task-owner-2')) { return $true }
return $false
})

$nodes = $window.FindAll(
[System.Windows.Automation.TreeScope]::Descendants,
[System.Windows.Automation.Condition]::TrueCondition)
Expand All @@ -127,12 +240,17 @@ try {
[ordered]@{
platform = 'windows'
artifact = $executable.FullName
actions = @('set:quick-add', 'invoke:quick-add-submit', 'toggle:task-complete-1')
actions = @('set:quick-add', 'invoke:quick-add-submit', 'toggle:task-complete-1', 'create-list:生活', 'select-list:2', 'enter:quick-add', 'select-list:1', 'select-list:2')
assertions = @(
'packaged WinUI window activated',
'stable UI Automation identifiers exposed',
'RPC-created task appeared',
'changed Event removed the completed task from the active view',
'new task submitted by Enter is owned by 生活, not 工作',
'task checkbox and plain-text title are vertically aligned',
'Windows task list exposes native multiple selection',
'view switches preserve task list ownership',
'global search shows the real owning list for every task',
'native window close stopped the process'
)
} | ConvertTo-Json -Depth 5 |
Expand Down
11 changes: 11 additions & 0 deletions shared/spec/PRODUCT-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ README's honest gaps).
text first runs through ExtractTimeCommand (trailing `@10am`, `+1d`…),
target list = selected list else first list (none → error
`taskCreateListFirst`); new task lands at top; counts refresh.
The host captures the **concrete list ID at submission**, before any
asynchronous parsing or persistence. Switching lists while the request
is running must never change the destination. No list / invalid selected
list must keep the unsent text rather than silently using another list.
The Windows packaged acceptance must create a second list, submit a
task via Enter, verify that it remains in the selected list, and verify
that it never appears in the first/default list.
- Empty states: not connected → 📂 + `taskListEmptyHint`; connected and
empty → ✓ + `taskListEmpty`.
- Row order per view (SQL in DATA-FORMAT/CLI-SPEC §list): All & list views
Expand All @@ -64,6 +71,10 @@ README's honest gaps).

- 20px circular checkbox: incomplete = hollow ring; complete = ring +
check in accent; completed text gets strikethrough + tertiary color.
The checkbox and task title share the **same first-row vertical center**
on every platform. Due date, time and notes (only when actually present)
sit beneath the title, not beside or vertically above the checkbox.
A task with no metadata renders as one clean centered checkbox/title row.
- Meta line under the text when present: the localized due date
(today/tomorrow/yesterday/`Aug 31`/`8月31日`, plus the time when set) in
its semantic color (overdue incomplete `#FF3B30`, due today accent,
Expand Down
12 changes: 10 additions & 2 deletions windows/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@
Foreground="{ThemeResource TasklySecondaryTextBrush}" />
<Button
x:Name="NewListButton"
AutomationProperties.AutomationId="create-list"
Grid.Column="1"
Padding="6,2"
Background="Transparent"
Expand Down Expand Up @@ -303,6 +304,7 @@
Margin="10,0,12,0">
<TextBlock
x:Name="PaneTitle"
AutomationProperties.AutomationId="current-view-title"
FontSize="22"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
Expand All @@ -315,6 +317,7 @@
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
<TextBox
x:Name="SearchBox"
AutomationProperties.AutomationId="task-search"
Width="200"
Height="28"
CornerRadius="10"
Expand Down Expand Up @@ -401,8 +404,10 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ListView
x:Name="TaskList"
AutomationProperties.AutomationId="task-list"
Margin="16,0,16,16"
SelectionMode="Single">
SelectionMode="Extended"
KeyDown="OnTaskListKeyDown">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
Expand All @@ -421,7 +426,10 @@
VerticalScrollBarVisibility="Auto">
<ListView
x:Name="SearchResults"
Margin="24,8,24,16">
AutomationProperties.AutomationId="search-task-list"
Margin="24,8,24,16"
SelectionMode="Extended"
KeyDown="OnTaskListKeyDown">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
Expand Down
Loading
Loading