From 2217c7d09125a5af8923379336e1beca9b6a2485 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Thu, 22 Oct 2026 09:31:05 +0800 Subject: [PATCH 1/6] fix(linux): preserve undo banner container lifetime --- linux/src/main.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index c864335..3f11a3f 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -2666,18 +2666,29 @@ void rebuild_task_list() { // --------------------------------------------------------------------------- guint banner_timeout() { + if (g_state.banner_box == nullptr) return 0; return GPOINTER_TO_UINT( g_object_get_data(G_OBJECT(g_state.banner_box), "banner-timeout")); } void hide_delete_banner() { + // banner_box is the persistent child of the revealer. Remove only the + // transient card; replacing the revealer's child would destroy banner_box + // while g_state.banner_box still points at it. + if (g_state.banner_box == nullptr) return; auto const timeout = banner_timeout(); if (timeout != 0) g_source_remove(timeout); g_object_set_data(G_OBJECT(g_state.banner_box), "banner-timeout", GUINT_TO_POINTER(0)); - gtk_revealer_set_reveal_child( - GTK_REVEALER(g_object_get_data(G_OBJECT(g_state.banner_box), - "banner-revealer")), FALSE); + auto* revealer = GTK_REVEALER( + g_object_get_data(G_OBJECT(g_state.banner_box), "banner-revealer")); + if (revealer != nullptr) { + gtk_revealer_set_reveal_child(revealer, FALSE); + } + while (auto* child = + gtk_widget_get_first_child(GTK_WIDGET(g_state.banner_box))) { + gtk_box_remove(g_state.banner_box, child); + } } void refresh_after_staged_delete() { @@ -2763,7 +2774,8 @@ void stage_delete_tasks(std::vector const& ids) { commit_staged_delete(); }), nullptr); gtk_box_append(GTK_BOX(card), close); - gtk_revealer_set_child(revealer, card); + // Keep the persistent banner_box alive; only the card is transient. + gtk_box_append(GTK_BOX(g_state.banner_box), card); gtk_revealer_set_reveal_child(revealer, TRUE); auto const timeout = g_timeout_add_seconds(6, on_undo_timeout, nullptr); g_object_set_data(G_OBJECT(g_state.banner_box), "banner-timeout", From c937dfff297b8b508f120ba817abe25be5a76b58 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Thu, 22 Oct 2026 09:31:09 +0800 Subject: [PATCH 2/6] test(linux): keep undo acceptance inside six-second window --- scripts/e2e/linux-atspi.py | 59 +++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index fd0b4f6..c5edc53 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -38,6 +38,14 @@ def role(node): return "unknown" +def name(node): + try: + return node.name or "" + except Exception: + # A row can disappear between two AT-SPI calls while GTK rebuilds it. + return "" + + def text(node): try: value = node.queryText() @@ -60,21 +68,21 @@ def find_application(): desktop = pyatspi.Registry.getDesktop(0) for application in children(desktop): for node in children(application): - if role(node) == "frame" and "taskly" in (node.name or "").casefold(): + if role(node) == "frame" and "taskly" in name(node).casefold(): return application return None -def find_named(root, name, expected_role=None): +def find_named(root, expected_name, expected_role=None): for node in walk(root): - if (node.name or "") == name and (expected_role is None or role(node) == expected_role): + if name(node) == expected_name and (expected_role is None or role(node) == expected_role): return node return None def find_editable_named(root, name): for node in walk(root): - if (node.name or "") != name: + if name(node) != name: continue try: node.queryEditableText() @@ -85,7 +93,35 @@ def find_editable_named(root, name): def contains_text(root, expected): - return any((node.name or "") == expected or text(node) == expected for node in walk(root)) + return any(name(node) == expected or text(node) == expected for node in walk(root)) + + +def find_staged_delete_ready(root): + """Find the Undo button and verify both staged task rows are already hidden in one tree walk.""" + undo = None + found_a = False + found_b = False + for node in walk(root): + node_name = name(node) + node_text = "" + if node_name == "Undo" and role(node) == "push button": + undo = node + if node_name == "64-bit task A": + found_a = True + elif not found_a: + node_text = text(node) + if node_text == "64-bit task A": + found_a = True + if node_name == "64-bit task B": + found_b = True + elif not found_b: + if not node_text: + node_text = text(node) + if node_text == "64-bit task B": + found_b = True + if undo is not None and found_a and found_b: + return None + return undo if undo is not None and not found_a and not found_b else None def matching_child_indexes(container, expected): @@ -142,7 +178,7 @@ def select_named_children(root, expected_names): def snapshot(node, depth=0): - result = {"name": node.name or "", "role": role(node)} + result = {"name": name(node), "role": role(node)} value = text(node) if value: result["text"] = value @@ -210,12 +246,13 @@ def main(): lambda: select_named_children(window, ["64-bit task A", "64-bit task B"]), ) press(delete) - wait_for( - "staged tasks hidden", - lambda: not contains_text(window, "64-bit task A") - and not contains_text(window, "64-bit task B"), + # The product's undo window is six seconds. Find the banner and confirm + # both rows are hidden in one AT-SPI traversal, then undo immediately. + undo = wait_for( + "staged deletion hidden with undo banner", + lambda: find_staged_delete_ready(window), + timeout=4.0, ) - undo = wait_for("undo banner", lambda: find_named(window, "Undo", "push button")) press(undo) wait_for( "identity-preserving undo", From 37a13ef28306eb21feccc3081baeb9e88e9de1cd Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Thu, 22 Oct 2026 09:31:25 +0800 Subject: [PATCH 3/6] test(linux): guard accessible names during row rebuilds --- scripts/e2e/linux-atspi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index c5edc53..6741ab7 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -80,9 +80,9 @@ def find_named(root, expected_name, expected_role=None): return None -def find_editable_named(root, name): +def find_editable_named(root, expected_name): for node in walk(root): - if name(node) != name: + if name(node) != expected_name: continue try: node.queryEditableText() From c59be132d2160c40b3ea1b0022a1a363549ba470 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Thu, 22 Oct 2026 09:31:58 +0800 Subject: [PATCH 4/6] fix(linux): use GtkSearchEntry placeholder API --- linux/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 3f11a3f..e298069 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -3027,8 +3027,8 @@ void apply_language() { build_menubar(); gtk_label_set_text(g_state.my_lists_caption, tr("sectionMyLists").c_str()); - gtk_entry_set_placeholder_text(GTK_ENTRY(g_state.search_box), - tr("searchHint").c_str()); + gtk_search_entry_set_placeholder_text(g_state.search_box, + tr("searchHint").c_str()); gtk_entry_set_placeholder_text(g_state.quick_add, tr("taskListInputHint").c_str()); gtk_accessible_update_property( From 514adcbc1e8ff7af7e0de13dbad353e9b8d9dbd7 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Thu, 22 Oct 2026 09:33:09 +0800 Subject: [PATCH 5/6] test(linux): tolerate transient AT-SPI container removal --- scripts/e2e/linux-atspi.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 6741ab7..46490f0 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -126,7 +126,11 @@ def find_staged_delete_ready(root): def matching_child_indexes(container, expected): result = [] - for index in range(container.childCount): + try: + count = container.childCount + except Exception: + return result + for index in range(count): try: child = container.getChildAtIndex(index) except Exception: From e49351b77629eb3c15dad326a89040425a1e9579 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Thu, 22 Oct 2026 10:11:10 +0800 Subject: [PATCH 6/6] ci(macos): ensure boot smoke always reaps host process --- .github/workflows/ci.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7835a05..406369b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,14 +136,34 @@ jobs: - name: Build host run: raco rivet build - name: Boot smoke + shell: bash run: | + set -euo pipefail smoke_home="$RUNNER_TEMP/taskly-smoke-home" + smoke_log="$RUNNER_TEMP/taskly-smoke.log" mkdir -p "$smoke_home" - HOME="$smoke_home" ./.rivet/stage/RivetHost & + HOME="$smoke_home" ./.rivet/stage/RivetHost >"$smoke_log" 2>&1 & APP_PID=$! + cleanup() { + if kill -0 "$APP_PID" 2>/dev/null; then + kill -TERM "$APP_PID" 2>/dev/null || true + for _ in 1 2 3 4 5; do + kill -0 "$APP_PID" 2>/dev/null || break + sleep 1 + done + kill -KILL "$APP_PID" 2>/dev/null || true + fi + wait "$APP_PID" 2>/dev/null || true + } + trap cleanup EXIT sleep 8 - kill -0 $APP_PID - kill $APP_PID + if ! kill -0 "$APP_PID" 2>/dev/null; then + cat "$smoke_log" || true + echo "::error::RivetHost exited before the 8-second smoke window" + exit 1 + fi + cleanup + trap - EXIT windows-host: name: Windows host (WinRT build)