From 43bf4fdc74112c5d6ad80c8edd60f61ec2bd82ba Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Tue, 20 Oct 2026 18:58:34 +0800 Subject: [PATCH 01/11] fix(windows): restore quick-add draft when parsing or saving fails --- windows/MainWindow.xaml.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/windows/MainWindow.xaml.cpp b/windows/MainWindow.xaml.cpp index f95a0d03..91130452 100644 --- a/windows/MainWindow.xaml.cpp +++ b/windows/MainWindow.xaml.cpp @@ -1432,7 +1432,7 @@ winrt::fire_and_forget MainWindow::AddTaskAsync(std::wstring const& text) { // due (parse_quick_add); this host only forwards the result. (void)api.parse_quick_add_async( winrt::to_string(text), - [dispatcher, weak, backend](rivet_app::Result parsed) { + [dispatcher, weak, backend, text](rivet_app::Result parsed) { try { auto const parse = parsed.get(); rivet_app::API api(*backend); @@ -1441,13 +1441,17 @@ winrt::fire_and_forget MainWindow::AddTaskAsync(std::wstring const& text) { (void)api.add_task_async( parse.text, std::nullopt, parse.due_date, parse.due_time, std::nullopt, - [dispatcher, weak](rivet_app::Result result) { + [dispatcher, weak, text](rivet_app::Result result) { try { result.get(); } catch (std::exception const& e) { auto message = std::string(e.what()); - dispatcher.TryEnqueue([weak, message = std::move(message)] { + dispatcher.TryEnqueue([weak, text, message = std::move(message)] { if (auto window = weak.get()) { + // Never replace a newer draft typed while the RPC ran. + if (window->NewTaskBox().Text().empty()) { + window->NewTaskBox().Text(text); + } window->SetError(message); } }); @@ -1455,14 +1459,20 @@ winrt::fire_and_forget MainWindow::AddTaskAsync(std::wstring const& text) { }); } catch (std::exception const& e) { auto message = std::string(e.what()); - dispatcher.TryEnqueue([weak, message = std::move(message)] { + dispatcher.TryEnqueue([weak, text, message = std::move(message)] { if (auto window = weak.get()) { + if (window->NewTaskBox().Text().empty()) { + window->NewTaskBox().Text(text); + } window->SetError(message); } }); } }); } catch (std::exception const& e) { + if (NewTaskBox().Text().empty()) { + NewTaskBox().Text(text); + } SetError(e.what()); } } @@ -2046,6 +2056,12 @@ void MainWindow::OnNewTaskKeyDown( void MainWindow::CommitQuickAdd() { auto const text = std::wstring(NewTaskBox().Text()); + // Keep unsent input when the backend is unavailable; otherwise the + // user loses their draft before AddTaskAsync can reject the request. + if (text.find_first_not_of(L" \t\r\n") == std::wstring::npos || + backend_ == nullptr || !backend_->running()) { + return; + } NewTaskBox().Text(L""); AddTaskAsync(text); } From a9043d0fe456b39b8d6d846a6b0edcd2f798f788 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Tue, 20 Oct 2026 19:28:49 +0800 Subject: [PATCH 02/11] fix(windows): require an explicit target list for every quick-add task --- windows/MainWindow.xaml.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/MainWindow.xaml.h b/windows/MainWindow.xaml.h index 25acf31d..80cef090 100644 --- a/windows/MainWindow.xaml.h +++ b/windows/MainWindow.xaml.h @@ -117,7 +117,8 @@ struct MainWindow : MainWindowT { winrt::fire_and_forget ReloadTasksAsync(); winrt::fire_and_forget OpenDefaultDatabaseAsync(); winrt::fire_and_forget OpenDatabaseAsync(std::wstring const& path); - winrt::fire_and_forget AddTaskAsync(std::wstring const& text); + winrt::fire_and_forget AddTaskAsync(std::wstring const& text, + std::int64_t target_list_id); winrt::fire_and_forget ToggleTaskAsync(std::int64_t id, bool completed); winrt::fire_and_forget RenameTaskAsync(std::int64_t id, std::wstring const& text); winrt::fire_and_forget SetTaskDueAsync(std::int64_t id, std::wstring const& due_date); From 4c3d8408a65450cb734c024a24adab164a23b738 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Tue, 20 Oct 2026 19:30:00 +0800 Subject: [PATCH 03/11] fix(windows): route quick add to selected list and align task checkbox with title --- windows/MainWindow.xaml.cpp | 76 +++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/windows/MainWindow.xaml.cpp b/windows/MainWindow.xaml.cpp index 91130452..a911c4fc 100644 --- a/windows/MainWindow.xaml.cpp +++ b/windows/MainWindow.xaml.cpp @@ -867,6 +867,8 @@ void MainWindow::RenderSidebar() { row.ContextFlyout(menu); row.Tag(winrt::box_value(list.id)); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + row, L"list-" + std::to_wstring(list.id)); UserList().Items().Append(row); if (view_ == ViewKind::List && view_list_id_ == list.id) { UserList().SelectedIndex(UserList().Items().Size() - 1); @@ -914,36 +916,48 @@ mxc::Grid MainWindow::MakeTaskRow(rivet_app::Task const& task) { row.ColumnDefinitions().GetAt(0).Width(mx::GridLength{0, mx::GridUnitType::Auto}); row.ColumnDefinitions().GetAt(1).Width( mx::GridLength{1, mx::GridUnitType::Star}); + // The checkbox and title must be centered in the SAME first row. Metadata + // and notes (when present) occupy a second row under the title only. + row.RowDefinitions().Append(mxc::RowDefinition{}); + row.RowDefinitions().Append(mxc::RowDefinition{}); row.Padding(mx::ThicknessHelper::FromLengths(4, 4, 4, 4)); row.Tag(winrt::box_value(task.id)); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + row, L"task-row-" + std::to_wstring(task.id)); auto check = mxc::CheckBox{}; winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( check, L"task-complete-" + std::to_wstring(task.id)); check.IsChecked(task.completed); check.MinWidth(0); + check.VerticalAlignment(mx::VerticalAlignment::Center); check.Checked({this, &MainWindow::OnTaskCheckChanged}); check.Unchecked({this, &MainWindow::OnTaskCheckChanged}); check.Tag(winrt::box_value(task.id)); mxc::Grid::SetColumn(check, 0); + mxc::Grid::SetRow(check, 0); row.Children().Append(check); - auto content = mxc::StackPanel{}; - content.Margin(mx::ThicknessHelper::FromLengths(8, 0, 0, 0)); - content.Spacing(2); - mxc::Grid::SetColumn(content, 1); - row.Children().Append(content); - auto text = mxc::TextBlock{}; text.Text(wide(task.text)); text.FontSize(14); text.TextTrimming(mx::TextTrimming::CharacterEllipsis); + text.Margin(mx::ThicknessHelper::FromLengths(8, 0, 0, 0)); + text.VerticalAlignment(mx::VerticalAlignment::Center); + mxc::Grid::SetColumn(text, 1); + mxc::Grid::SetRow(text, 0); if (task.completed) { text.Foreground(ThemeBrush(L"TasklyMutedTextBrush")); text.TextDecorations( winrt::Windows::UI::Text::TextDecorations::Strikethrough); } - content.Children().Append(text); + row.Children().Append(text); + + auto details = mxc::StackPanel{}; + details.Margin(mx::ThicknessHelper::FromLengths(8, 0, 0, 0)); + details.Spacing(2); + mxc::Grid::SetColumn(details, 1); + mxc::Grid::SetRow(details, 1); // Meta line (PRODUCT-SPEC §5, DESIGN-TOKENS task row): owning list name // with its color dot in the smart views, then the due chip; the notes @@ -1000,7 +1014,7 @@ mxc::Grid MainWindow::MakeTaskRow(rivet_app::Task const& task) { meta.Children().Append(chip_text); } if (meta.Children().Size() > 0) { - content.Children().Append(meta); + details.Children().Append(meta); } // Notes preview: single line, ellipsized, muted (PRODUCT-SPEC §5). @@ -1010,7 +1024,11 @@ mxc::Grid MainWindow::MakeTaskRow(rivet_app::Task const& task) { notes_line.FontSize(12); notes_line.TextTrimming(mx::TextTrimming::CharacterEllipsis); notes_line.Foreground(ThemeBrush(L"TasklyMutedTextBrush")); - content.Children().Append(notes_line); + details.Children().Append(notes_line); + } + + if (details.Children().Size() > 0) { + row.Children().Append(details); } auto menu = mxc::MenuFlyout{}; @@ -1301,7 +1319,8 @@ winrt::fire_and_forget MainWindow::ReloadTasksAsync() { ? std::optional(list_id) : std::nullopt, include_completed, - [dispatcher, weak](rivet_app::Result result) { + [dispatcher, weak, view_kind, list_id, include_completed]( + rivet_app::Result result) { // Release the reload gate on every terminal path so later // polls and `changed` events are not swallowed. dispatcher.TryEnqueue([weak] { @@ -1312,8 +1331,17 @@ winrt::fire_and_forget MainWindow::ReloadTasksAsync() { }); try { auto const snapshot = result.get(); - dispatcher.TryEnqueue([weak, snapshot] { + dispatcher.TryEnqueue( + [weak, snapshot, view_kind, list_id, include_completed] { if (auto window = weak.get()) { + // The user may switch lists while this RPC is in flight: + // never paint a Work snapshot under the Life heading. + if (window->view_ != view_kind || + window->view_list_id_ != list_id || + window->show_completed_ != include_completed) { + window->ReloadTasksAsync(); + return; + } window->ApplySnapshot(snapshot, /*force=*/false); // An in-flight download owns the status line. if (!window->update_downloading_) { @@ -1419,7 +1447,8 @@ winrt::fire_and_forget MainWindow::OpenDatabaseAsync(std::wstring const& path) { } } -winrt::fire_and_forget MainWindow::AddTaskAsync(std::wstring const& text) { +winrt::fire_and_forget MainWindow::AddTaskAsync(std::wstring const& text, + std::int64_t target_list_id) { if (backend_ == nullptr || !backend_->running() || text.empty()) { co_return; } @@ -1432,15 +1461,15 @@ winrt::fire_and_forget MainWindow::AddTaskAsync(std::wstring const& text) { // due (parse_quick_add); this host only forwards the result. (void)api.parse_quick_add_async( winrt::to_string(text), - [dispatcher, weak, backend, text](rivet_app::Result parsed) { + [dispatcher, weak, backend, text, target_list_id](rivet_app::Result parsed) { try { auto const parse = parsed.get(); rivet_app::API api(*backend); // `changed` triggers the authoritative reload; the completion // only surfaces failures. (void)api.add_task_async( - parse.text, std::nullopt, parse.due_date, parse.due_time, - std::nullopt, + parse.text, std::optional(target_list_id), + parse.due_date, parse.due_time, std::nullopt, [dispatcher, weak, text](rivet_app::Result result) { try { result.get(); @@ -2062,8 +2091,23 @@ void MainWindow::CommitQuickAdd() { backend_ == nullptr || !backend_->running()) { return; } + // Match macOS/Linux: a named list owns new tasks; smart views use the + // first list only as an explicit fallback. Capture the target BEFORE the + // async parser runs, so switching views cannot redirect an in-flight add. + std::optional target_list_id; + if (view_ == ViewKind::List) { + if (find_by_id(lists_, view_list_id_) != nullptr) { + target_list_id = view_list_id_; + } + } else if (!lists_.empty()) { + target_list_id = lists_.front().id; + } + if (!target_list_id.has_value()) { + SetStatus(t("taskCreateListFirst")); + return; // Keep the user's draft until a valid destination exists. + } NewTaskBox().Text(L""); - AddTaskAsync(text); + AddTaskAsync(text, *target_list_id); } void MainWindow::OnQuickAddCommit( From 1f25649bc3a8f2a6d451f873f993b1e64861c55f Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Tue, 20 Oct 2026 19:31:58 +0800 Subject: [PATCH 04/11] test(windows): give new-list action a stable accessibility ID --- windows/MainWindow.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/windows/MainWindow.xaml b/windows/MainWindow.xaml index eca5d316..cd4262ed 100644 --- a/windows/MainWindow.xaml +++ b/windows/MainWindow.xaml @@ -238,6 +238,7 @@ Foreground="{ThemeResource TasklySecondaryTextBrush}" />