From 4b399d8a9139b17bb93cc814eef127f5342731d0 Mon Sep 17 00:00:00 2001 From: Wes Date: Sat, 15 Aug 2026 17:56:18 -0600 Subject: [PATCH] Cancel deferred sidebar navigation on newer intent Subscribe the sidebar deferral to the shared navigation-intent signal so back, forward, deep links, and every other navigation source supersede stale channel work. Keep optimistic selection until the committed route catches up, and cover immediate competing intents plus traversal races. Co-authored-by: Carl Signed-off-by: Wes --- desktop/src/app/AppShell.tsx | 25 ++------ .../useDeferredSidebarNavigation.ts | 19 +++++- .../sidebar/ui/AppSidebarPinnedHeader.tsx | 1 + desktop/tests/e2e/messaging.spec.ts | 64 ++++++++++++++++++- 4 files changed, 86 insertions(+), 23 deletions(-) diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 3fc65f504..91f17cf18 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -155,7 +155,6 @@ export function AppShell() { [location.pathname], ); const { - cancel: cancelPendingSidebarNavigation, pendingChannelId: pendingSidebarChannelId, selectDeferred: handleSidebarChannelSelect, } = useDeferredSidebarNavigation({ @@ -858,10 +857,7 @@ export function AppShell() { }); await goChannel(directMessage.id); }} - onSelectAgents={() => { - cancelPendingSidebarNavigation(); - void goAgents(); - }} + onSelectAgents={() => void goAgents()} onSelectChannel={handleSidebarChannelSelect} onOpenSearchResult={handleOpenSearchResult} searchChannels={channels} @@ -869,18 +865,9 @@ export function AppShell() { searchFocusRequest, scopeSearchFocusRequest, ]} - onSelectHome={() => { - cancelPendingSidebarNavigation(); - void goHome(); - }} - onSelectProjects={() => { - cancelPendingSidebarNavigation(); - void goProjects(); - }} - onSelectPulse={() => { - cancelPendingSidebarNavigation(); - void goPulse(); - }} + onSelectHome={() => void goHome()} + onSelectProjects={() => void goProjects()} + onSelectPulse={() => void goPulse()} onSelectSettings={handleOpenSettings} onSelectWorkflows={() => void goWorkflows()} onSetPresenceStatus={(status) => @@ -904,7 +891,9 @@ export function AppShell() { : undefined } selectedChannelId={sidebarSelectedChannelId} - selectedView={selectedView} + selectedView={ + pendingSidebarChannelId ? "channel" : selectedView + } unreadChannelIds={unreadChannelIds} previewActivityChannelIds={unreadThreadChannelIds} unreadChannelCounts={unreadChannelCounts} diff --git a/desktop/src/app/navigation/useDeferredSidebarNavigation.ts b/desktop/src/app/navigation/useDeferredSidebarNavigation.ts index 28110d210..b2800280d 100644 --- a/desktop/src/app/navigation/useDeferredSidebarNavigation.ts +++ b/desktop/src/app/navigation/useDeferredSidebarNavigation.ts @@ -22,7 +22,7 @@ export function useDeferredSidebarNavigation({ const pathnameRef = React.useRef(pathname); pathnameRef.current = pathname; - const cancel = React.useCallback(() => { + const cancelDeferred = React.useCallback(() => { generationRef.current += 1; if (frameRef.current !== null) { window.cancelAnimationFrame(frameRef.current); @@ -32,10 +32,23 @@ export function useDeferredSidebarNavigation({ window.clearTimeout(timerRef.current); timerRef.current = null; } - setPendingChannelId(null); }, []); + const cancel = React.useCallback(() => { + cancelDeferred(); + setPendingChannelId(null); + }, [cancelDeferred]); + React.useEffect(() => cancel, [cancel]); + React.useEffect(() => { + const handleNavigationIntent = () => cancelDeferred(); + window.addEventListener("buzz:navigation-intent", handleNavigationIntent); + return () => + window.removeEventListener( + "buzz:navigation-intent", + handleNavigationIntent, + ); + }, [cancelDeferred]); React.useEffect(() => { // A committed route change supersedes any deferred sidebar intent. void pathname; @@ -84,5 +97,5 @@ export function useDeferredSidebarNavigation({ [cancel, selectChannel], ); - return { cancel, pendingChannelId, selectDeferred }; + return { pendingChannelId, selectDeferred }; } diff --git a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx index 4a618fcf0..775c7387d 100644 --- a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx @@ -105,6 +105,7 @@ export function AppSidebarPrimaryMenu({ { + document + .querySelector('[data-testid="channel-random"]') + ?.click(); + const inboxButton = document.querySelector( + '[data-testid="open-home-view"]', + ); + if (!inboxButton) throw new Error("Expected Inbox button"); + inboxButton.click(); + }); await expect(page.getByTestId("home-inbox")).toBeVisible(); await page.waitForTimeout(100); @@ -2847,6 +2855,58 @@ test("superseded sidebar feedback cannot override newer navigation", async ({ ); }); +test("back navigation supersedes pending sidebar navigation", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + const historyLength = await page.evaluate(() => window.history.length); + await page.evaluate(() => { + document + .querySelector('[data-testid="channel-random"]') + ?.click(); + document + .querySelector('[data-testid="global-back"]') + ?.click(); + }); + + await expect(page).toHaveURL(/\/$/); + await expect(page.getByTestId("home-inbox")).toBeVisible(); + await page.waitForTimeout(100); + await expect(page.getByTestId("home-inbox")).toBeVisible(); + expect(await page.evaluate(() => window.history.length)).toBe(historyLength); +}); + +test("forward navigation supersedes pending sidebar navigation", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + await page.getByTestId("channel-random").click(); + await expect(page.getByTestId("chat-title")).toHaveText("random"); + await page.getByTestId("global-back").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + const historyLength = await page.evaluate(() => window.history.length); + await page.evaluate(() => { + document + .querySelector('[data-testid="channel-watercooler"]') + ?.click(); + document + .querySelector('[data-testid="global-forward"]') + ?.click(); + }); + + await expect(page.getByTestId("chat-title")).toHaveText("random"); + await expect(page).toHaveURL(/#\/channels\/[^?]+$/); + await page.waitForTimeout(100); + await expect(page.getByTestId("chat-title")).toHaveText("random"); + expect(await page.evaluate(() => window.history.length)).toBe(historyLength); +}); + test("thread open paints immediate feedback before deferred panel work", async ({ page, }) => {