From 73b28a7f6049142494a73bb2377c75c5aabf0835 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 21 Apr 2026 21:57:15 +0700 Subject: [PATCH] Fix notification clicks preserving workspace deck --- .../workspace-navigation-regression.spec.ts | 29 ++++++++++++ packages/app/src/app/_layout.tsx | 44 ++++++++++++++++--- .../app/src/utils/notification-routing.ts | 4 +- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/app/e2e/workspace-navigation-regression.spec.ts b/packages/app/e2e/workspace-navigation-regression.spec.ts index eb0b63db1..d83895236 100644 --- a/packages/app/e2e/workspace-navigation-regression.spec.ts +++ b/packages/app/e2e/workspace-navigation-regression.spec.ts @@ -139,6 +139,35 @@ test.describe("Workspace navigation regression", () => { await expect(secondDeckEntry).toBeVisible({ timeout: 30_000 }); await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(2); + await page.evaluate( + ({ agentId, serverId }) => { + globalThis.dispatchEvent( + new CustomEvent("paseo:web-notification-click", { + detail: { + data: { + serverId, + agentId, + reason: "finished", + }, + }, + cancelable: true, + }), + ); + }, + { agentId: secondAgent.id, serverId }, + ); + await waitForWorkspaceTabsVisible(page); + await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, secondWorkspace.workspaceId), { + timeout: 30_000, + }); + await expect(secondDeckEntry).toBeVisible({ timeout: 30_000 }); + await expectWorkspaceTabVisible(page, secondAgent.id); + await expectWorkspaceTabHidden(page, firstAgent.id); + await expectOnlyWorkspaceAgentTabsVisible(page, [secondAgent.id]); + await expect(firstDeckEntry).toBeAttached(); + await expect(firstDeckEntry).toBeHidden(); + await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(2); + await switchWorkspaceViaSidebar({ page, serverId, diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 141572169..cc202fe1b 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -28,6 +28,7 @@ import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; import { loadSettingsFromStorage } from "@/hooks/use-settings"; import { useColorScheme } from "@/hooks/use-color-scheme"; import { useOpenProject } from "@/hooks/use-open-project"; +import { useStableEvent } from "@/hooks/use-stable-event"; import { SessionProvider } from "@/contexts/session-context"; import type { HostProfile } from "@/types/host-connection"; import { @@ -77,7 +78,7 @@ import { import { listenToDesktopEvent } from "@/desktop/electron/events"; import { getDesktopHost } from "@/desktop/host"; import { updateDesktopWindowControls } from "@/desktop/electron/window"; -import { buildNotificationRoute } from "@/utils/notification-routing"; +import { buildNotificationRoute, resolveNotificationTarget } from "@/utils/notification-routing"; import { buildHostRootRoute, mapPathnameToServer, @@ -86,6 +87,10 @@ import { parseWorkspaceOpenIntent, decodeWorkspaceIdFromPathSegment, } from "@/utils/host-routes"; +import { useSessionStore } from "@/stores/session-store"; +import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution"; +import { navigateToWorkspace } from "@/hooks/use-workspace-navigation"; +import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; import { addBrowserActiveWorkspaceLocationListener, syncNavigationActiveWorkspace, @@ -124,7 +129,36 @@ const HostRuntimeBootstrapContext = createContext({ function PushNotificationRouter() { const router = useRouter(); + const pathname = usePathname(); const lastHandledIdRef = useRef(null); + const openNotification = useStableEvent((data: Record | undefined) => { + const target = resolveNotificationTarget(data); + const serverId = target.serverId; + const agentId = target.agentId; + if (serverId && agentId) { + const session = useSessionStore.getState().sessions[serverId]; + const agent = session?.agents.get(agentId); + const workspaceId = + target.workspaceId ?? + resolveWorkspaceIdByExecutionDirectory({ + workspaces: session?.workspaces.values(), + workspaceDirectory: agent?.cwd, + }); + + if (workspaceId) { + prepareWorkspaceTab({ + serverId, + workspaceId, + target: { kind: "agent", agentId }, + pin: true, + }); + navigateToWorkspace(serverId, workspaceId, { currentPathname: pathname }); + return; + } + } + + router.navigate(buildNotificationRoute(data)); + }); useEffect(() => { if (isWeb) { @@ -145,7 +179,7 @@ function PushNotificationRouter() { (payload as { data?: unknown }).data !== null ? (payload as { data: Record }).data : undefined; - router.push(buildNotificationRoute(data) as any); + openNotification(data); }, ); @@ -165,7 +199,7 @@ function PushNotificationRouter() { const openFromWebClick = (event: Event) => { const customEvent = event as CustomEvent; event.preventDefault(); - router.push(buildNotificationRoute(customEvent.detail?.data) as any); + openNotification(customEvent.detail?.data); }; target.addEventListener(WEB_NOTIFICATION_CLICK_EVENT, openFromWebClick as EventListener); @@ -198,7 +232,7 @@ function PushNotificationRouter() { const data = response.notification.request.content.data as | Record | undefined; - router.push(buildNotificationRoute(data) as any); + openNotification(data); }; const subscription = Notifications.addNotificationResponseReceivedListener(openFromResponse); @@ -212,7 +246,7 @@ function PushNotificationRouter() { return () => { subscription.remove(); }; - }, [router]); + }, [openNotification]); return null; } diff --git a/packages/app/src/utils/notification-routing.ts b/packages/app/src/utils/notification-routing.ts index 1d4b0772b..ff5c90e02 100644 --- a/packages/app/src/utils/notification-routing.ts +++ b/packages/app/src/utils/notification-routing.ts @@ -1,3 +1,4 @@ +import type { Href } from "expo-router"; import { buildHostAgentDetailRoute, buildHostRootRoute, @@ -5,6 +6,7 @@ import { } from "@/utils/host-routes"; type NotificationData = Record | null | undefined; +type NotificationRoute = Extract; function readNonEmptyString(data: NotificationData, key: string): string | null { const value = data?.[key]; @@ -27,7 +29,7 @@ export function resolveNotificationTarget(data: NotificationData): { }; } -export function buildNotificationRoute(data: NotificationData) { +export function buildNotificationRoute(data: NotificationData): NotificationRoute { const { serverId, agentId, workspaceId } = resolveNotificationTarget(data); if (serverId && agentId) { if (workspaceId) {