From dde8a9ea742cf592435805893369ad2f0d7986ad Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sun, 23 Aug 2026 23:25:46 -0700 Subject: [PATCH] fix(desktop): preserve activity window navigation context Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho --- desktop/playwright.config.ts | 1 + .../src/app/navigation/useAppNavigation.ts | 23 +++++- .../src/app/routes/channels.$channelId.tsx | 3 + .../tests/e2e/agent-activity-window.spec.ts | 75 +++++++++++++++++++ 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 desktop/tests/e2e/agent-activity-window.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index ff8a0e7703..86c195de00 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -51,6 +51,7 @@ export default defineConfig({ "**/observer-feed-screenshots.spec.ts", "**/core-memory-screenshots.spec.ts", "**/activity-scope-label-screenshots.spec.ts", + "**/agent-activity-window.spec.ts", "**/welcome-agent-modal-screenshots.spec.ts", "**/local-archive-screenshots.spec.ts", "**/voice-settings.spec.ts", diff --git a/desktop/src/app/navigation/useAppNavigation.ts b/desktop/src/app/navigation/useAppNavigation.ts index c4776564e3..c290bb67f4 100644 --- a/desktop/src/app/navigation/useAppNavigation.ts +++ b/desktop/src/app/navigation/useAppNavigation.ts @@ -7,6 +7,7 @@ import { } from "@tanstack/react-router"; import { openSearchHitWithNavigation } from "@/app/navigation/searchHitNavigation"; +import { currentCompanionWindowKind } from "@/app/companionWindow"; import type { SearchHit } from "@/shared/api/types"; type NavigationBehavior = { @@ -20,6 +21,16 @@ export function useAppNavigation() { const navigate = useNavigate(); const location = useLocation(); const canGoBack = useCanGoBack(); + const companionSearch = location.search as Record; + const companionCoordinates = + currentCompanionWindowKind() === "agent-activity" && + typeof companionSearch.community === "string" && + typeof companionSearch.agentSession === "string" + ? { + community: companionSearch.community, + agentSession: companionSearch.agentSession, + } + : undefined; const commitNavigation = React.useCallback( async ( @@ -31,20 +42,26 @@ export function useAppNavigation() { }, behavior: NavigationBehavior = {}, ) => { - const nextLocation = router.buildLocation(next as never); + const destination = companionCoordinates + ? { + ...next, + search: { ...next.search, ...companionCoordinates }, + } + : next; + const nextLocation = router.buildLocation(destination as never); if (location.href === nextLocation.href && !behavior.force) { return false; } await navigate({ - ...next, + ...destination, replace: behavior.replace, resetScroll: behavior.resetScroll, } as never); return true; }, - [location.href, navigate, router], + [companionCoordinates, location.href, navigate, router], ); const goHome = React.useCallback( diff --git a/desktop/src/app/routes/channels.$channelId.tsx b/desktop/src/app/routes/channels.$channelId.tsx index 892eef6a56..fa04107f49 100644 --- a/desktop/src/app/routes/channels.$channelId.tsx +++ b/desktop/src/app/routes/channels.$channelId.tsx @@ -13,6 +13,8 @@ import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; type ChannelRouteSearch = { agentSession?: string; + /** Immutable origin community carried by dedicated activity windows. */ + community?: string; /** * When set, the composer on mount will auto-submit its loaded draft once, * then clear this param. Value is the draft key that was loaded so the @@ -36,6 +38,7 @@ function validateChannelSearch( ): ChannelRouteSearch { return { agentSession: nonEmptyString(search.agentSession), + community: nonEmptyString(search.community), autoSend: nonEmptyString(search.autoSend), messageId: nonEmptyString(search.messageId), profile: nonEmptyString(search.profile), diff --git a/desktop/tests/e2e/agent-activity-window.spec.ts b/desktop/tests/e2e/agent-activity-window.spec.ts new file mode 100644 index 0000000000..6679dba2dd --- /dev/null +++ b/desktop/tests/e2e/agent-activity-window.spec.ts @@ -0,0 +1,75 @@ +import { expect, test } from "@playwright/test"; + +import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; + +const AGENT_PUBKEY = TEST_IDENTITIES.tyler.pubkey; +const AGENTS_CHANNEL_ID = "94a444a4-c0a3-5966-ab05-530c6ddc2301"; +const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"; +const COMMUNITY_ID = "e2e-default-community"; + +test("keeps dedicated activity windows connected when opening a channel reference", async ({ + page, +}) => { + await installMockBridge(page, { + windowLabel: `agent-activity-${AGENT_PUBKEY}-${AGENTS_CHANNEL_ID}`, + managedAgents: [ + { + pubkey: AGENT_PUBKEY, + name: "Observer Agent", + status: "running", + channelNames: ["agents"], + }, + ], + }); + + await page.goto( + `/#/channels/${AGENTS_CHANNEL_ID}?community=${COMMUNITY_ID}&agentSession=${AGENT_PUBKEY}`, + ); + const panel = page.getByTestId("agent-session-thread-panel"); + await expect(panel).toBeVisible({ timeout: 10_000 }); + await page.waitForFunction( + () => typeof window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__ === "function", + ); + await page.evaluate( + ({ agentPubkey, channelId }) => { + window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__?.({ + agentPubkey, + events: [ + { + seq: 1, + timestamp: new Date().toISOString(), + kind: "acp_read", + agentIndex: 0, + channelId, + sessionId: "session-activity-window", + turnId: "turn-activity-window", + payload: { + method: "session/update", + params: { + update: { + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: "Continue in #general" }, + }, + }, + }, + }, + ], + }); + }, + { agentPubkey: AGENT_PUBKEY, channelId: AGENTS_CHANNEL_ID }, + ); + + const channelReference = panel.getByRole("button", { + name: "Open channel general", + }); + await expect(channelReference).toBeVisible(); + await channelReference.click(); + + await expect(page).toHaveURL( + new RegExp( + `#/channels/${GENERAL_CHANNEL_ID}\\?.*community=${COMMUNITY_ID}`, + ), + ); + await expect(panel).toBeVisible(); + await expect(page.locator("body")).not.toBeEmpty(); +});