From bafedade96d3e76ffcfcc5e88597423a709b1d53 Mon Sep 17 00:00:00 2001 From: AKolenda Date: Tue, 20 Oct 2026 21:49:17 +0000 Subject: [PATCH 1/2] fix(web): keep Windows thread notifications clickable until handled Windows moves a timed-out toast into Notification Center, and Electron drops clicks on those entries because the notification object is already gone. Keep thread alerts on screen with requireInteraction on Windows so the click still opens the thread. --- ...adNotificationCoordinator.browser.test.tsx | 20 +++++++++++++++++++ .../ThreadNotificationCoordinator.tsx | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx b/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx index 2046a3fc5aa0..af1c7917c1cd 100644 --- a/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx +++ b/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx @@ -11,6 +11,7 @@ const state = vi.hoisted(() => ({ active: { environmentId: "env-1", threadId: "other-thread" }, focused: true, visible: "visible", + platform: "MacIntel", live: true, completedAt: null as string | null, archivedAt: null as string | null, @@ -160,6 +161,7 @@ function setup() { active: { environmentId: "env-1", threadId: "other-thread" }, focused: true, visible: "visible", + platform: "MacIntel", live: true, completedAt: null, archivedAt: null, @@ -180,6 +182,9 @@ function setup() { .mockImplementation(() => state.visible as DocumentVisibilityState), ); stack.use(vi.spyOn(document, "hasFocus").mockImplementation(() => state.focused)); + stack.use( + vi.spyOn(Navigator.prototype, "platform", "get").mockImplementation(() => state.platform), + ); stack.use( stubGlobal("Notification", Object.assign(state.notification, { permission: "granted" })), ); @@ -290,6 +295,7 @@ describe("thread notifications", () => { body: "Fix the login form", tag: expect.any(String), silent: true, + requireInteraction: false, }); }); @@ -370,9 +376,23 @@ describe("thread notifications", () => { body: "Fix the login form", tag: expect.any(String), silent: true, + requireInteraction: false, }); }); + it("keeps Windows system alerts on screen so a click still reaches the app", async () => { + await using app = setup(); + state.mode = "notifications"; + state.focused = false; + state.platform = "Win32"; + await app.render(); + await app.complete(); + expect(state.notification).toHaveBeenCalledWith( + "Thread completed", + expect.objectContaining({ requireInteraction: true }), + ); + }); + it("reveals the desktop window and opens the thread when a system alert is clicked", async () => { await using app = setup(); const revealWindow = vi.fn(() => Promise.resolve()); diff --git a/apps/web/src/components/ThreadNotificationCoordinator.tsx b/apps/web/src/components/ThreadNotificationCoordinator.tsx index 9e4e7d79150c..0165ba2bb392 100644 --- a/apps/web/src/components/ThreadNotificationCoordinator.tsx +++ b/apps/web/src/components/ThreadNotificationCoordinator.tsx @@ -16,6 +16,7 @@ import { import { useEffect, useRef } from "react"; import { getClientSettings, useClientSettings } from "../hooks/useSettings"; +import { isWindowsPlatform } from "../lib/utils"; import { useEnvironmentIds } from "../state/environments"; import { environmentShell } from "../state/shell"; import { @@ -221,6 +222,9 @@ function EnvironmentNotifications({ body: thread.title, tag: threadNotificationTag(environmentId, thread.id), silent: true, + // Windows drops clicks once a toast times out into Notification Center, so keep it + // on screen until it is clicked or the window regains focus and clears it. + requireInteraction: isWindowsPlatform(navigator.platform), }); onNotification(environmentId, notification); notification.addEventListener("click", () => { From 7da6f5bd5fdacb7a295fd312033e20ed63200561 Mon Sep 17 00:00:00 2001 From: AKolenda Date: Tue, 20 Oct 2026 21:59:04 +0000 Subject: [PATCH 2/2] fix(web): let Windows attention alerts time out Approval and input alerts can be resolved from another client while this window stays unfocused, so only completions stay on screen. --- ...hreadNotificationCoordinator.browser.test.tsx | 16 +++++++++++++++- .../components/ThreadNotificationCoordinator.tsx | 7 ++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx b/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx index af1c7917c1cd..12e79d4a26dc 100644 --- a/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx +++ b/apps/web/src/components/ThreadNotificationCoordinator.browser.test.tsx @@ -380,7 +380,7 @@ describe("thread notifications", () => { }); }); - it("keeps Windows system alerts on screen so a click still reaches the app", async () => { + it("keeps Windows completion alerts on screen so a click still reaches the app", async () => { await using app = setup(); state.mode = "notifications"; state.focused = false; @@ -393,6 +393,20 @@ describe("thread notifications", () => { ); }); + it("lets Windows attention alerts time out since another client may resolve them", async () => { + await using app = setup(); + state.mode = "notifications"; + state.focused = false; + state.platform = "Win32"; + await app.render(); + state.input = true; + await app.render(); + expect(state.notification).toHaveBeenCalledWith( + "Input needed", + expect.objectContaining({ requireInteraction: false }), + ); + }); + it("reveals the desktop window and opens the thread when a system alert is clicked", async () => { await using app = setup(); const revealWindow = vi.fn(() => Promise.resolve()); diff --git a/apps/web/src/components/ThreadNotificationCoordinator.tsx b/apps/web/src/components/ThreadNotificationCoordinator.tsx index 0165ba2bb392..0a09d7362d29 100644 --- a/apps/web/src/components/ThreadNotificationCoordinator.tsx +++ b/apps/web/src/components/ThreadNotificationCoordinator.tsx @@ -222,9 +222,10 @@ function EnvironmentNotifications({ body: thread.title, tag: threadNotificationTag(environmentId, thread.id), silent: true, - // Windows drops clicks once a toast times out into Notification Center, so keep it - // on screen until it is clicked or the window regains focus and clears it. - requireInteraction: isWindowsPlatform(navigator.platform), + // Windows drops clicks once a toast times out into Notification Center, so keep + // completions on screen until clicked or cleared on focus. Attention alerts still time + // out because another client can resolve them while this window stays unfocused. + requireInteraction: kind === "completion" && isWindowsPlatform(navigator.platform), }); onNotification(environmentId, notification); notification.addEventListener("click", () => {