From f350c716b9fe671233b7466935e64449cb7875de Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 13 Jul 2026 15:38:09 +0200 Subject: [PATCH] Keep forked chats focused in new tabs (#2038) * fix(app): focus explicitly opened workspace tabs Explicit tab navigation could be overwritten by attention-aware workspace navigation when the visible agent had just finished. Make workspace navigation own both normal attention selection and explicit targets so callers cannot sequence them inconsistently. * test(app): strengthen forked tab focus assertion --- docs/expo-router.md | 12 +++- packages/app/e2e/assistant-fork-menu.spec.ts | 23 +++++-- packages/app/src/agent-stream/view.tsx | 6 +- .../src/components/sidebar-workspace-list.tsx | 2 +- .../sidebar/sidebar-status-list.tsx | 2 +- .../src/components/workspace-setup-dialog.tsx | 4 +- .../app/src/hooks/use-keyboard-shortcuts.ts | 2 +- .../app/src/screens/new-workspace-screen.tsx | 11 ++-- .../index.ts | 13 ++-- .../navigation.test.ts | 63 ++++++++++++++++--- .../navigation.ts | 58 ++++++++++------- .../app/src/utils/navigate-to-agent/index.ts | 4 +- .../utils/navigate-to-agent/resolve.test.ts | 6 +- .../src/utils/navigate-to-agent/resolve.ts | 6 +- .../restore-archived-workspace.test.ts | 4 +- .../app/src/utils/prepare-workspace-tab.ts | 20 +----- .../src/utils/workspace-navigation.test.ts | 40 +----------- .../app/src/utils/workspace-navigation.ts | 19 +----- 18 files changed, 156 insertions(+), 139 deletions(-) diff --git a/docs/expo-router.md b/docs/expo-router.md index 14344a3f1..df3aa2c5b 100644 --- a/docs/expo-router.md +++ b/docs/expo-router.md @@ -48,8 +48,13 @@ dynamic params exist before any nested workspace leaf is selected. ## App-Wide Route Hops When app-wide routes such as `/new`, `/settings`, or `/sessions` navigate back -into a host workspace, express only the destination with `navigateToWorkspace()`. -Do not make the caller branch on its current route. +into a host workspace, use `navigateToWorkspace()`. Do not make the caller +branch on its current route. + +Pass only `serverId` and `workspaceId` for normal attention-aware navigation. +When the action names a specific tab, pass it as `target`; that explicit choice +is authoritative. Callers should not choose between separate route and tab +navigation APIs. The root stack owns `h/[serverId]`; the host stack owns `workspace/[workspaceId]/index`. Repeated global-route hops must `POP_TO` the @@ -133,7 +138,8 @@ Before landing route changes: - [ ] Did you change `packages/app/src/app`? Re-read this file. - [ ] Did you touch remembered workspace restore? Keep root on `/h/[serverId]`. -- [ ] Did any route return to a workspace? Use `navigateToWorkspace()`. +- [ ] Did a route return to a workspace? Use `navigateToWorkspace()` and pass a + `target` when the action names a specific tab. - [ ] Did you add a route? Register it in the layout that directly owns it. - [ ] Did `useLocalSearchParams()` lose a required param? Fix the route tree. - [ ] Did native show a blank screen without a crash? Suspect route ownership diff --git a/packages/app/e2e/assistant-fork-menu.spec.ts b/packages/app/e2e/assistant-fork-menu.spec.ts index c09a90921..47a26f531 100644 --- a/packages/app/e2e/assistant-fork-menu.spec.ts +++ b/packages/app/e2e/assistant-fork-menu.spec.ts @@ -54,14 +54,14 @@ async function expectChatHistoryPill(page: Page): Promise { test.describe("Assistant fork menu", () => { test.describe.configure({ timeout: 180_000 }); - test("forks an assistant turn into a new workspace draft tab", async ({ + test("focuses a forked assistant turn in a new workspace draft tab", async ({ page, seedForkWorkspace, }) => { const session = await seedForkWorkspace({ - repoPrefix: "assistant-fork-tab-", - title: "Assistant fork tab", - initialPrompt: "emit 1 coalesced agent stream updates for assistant fork tab.", + repoPrefix: "assistant-fork-focused-tab-", + title: "Assistant fork focused tab", + initialPrompt: "emit 1 coalesced agent stream updates for initial assistant fork turn.", model: "ten-second-stream", }); @@ -70,9 +70,24 @@ test.describe("Assistant fork menu", () => { await awaitAssistantMessage(page); await session.client.waitForFinish(session.agentId, 45_000); + await submitMessage(page, "emit 1 coalesced agent stream updates while this tab is visible."); + await session.client.waitForFinish(session.agentId, 45_000); + await awaitAssistantMessage(page); + + const agentTab = page.getByTestId(`workspace-tab-agent_${session.agentId}`); + await expect(agentTab).toHaveAttribute("aria-selected", "true"); + await openAssistantForkMenu(page); await page.getByTestId("assistant-fork-menu-new-tab").click(); + const selectedTab = page + .getByTestId("workspace-tabs-row") + .getByRole("button") + .and(page.locator('[aria-selected="true"]')); + await expect(selectedTab).toHaveAttribute("data-testid", /^workspace-tab-draft_/, { + timeout: 30_000, + }); + await expect(agentTab).toHaveAttribute("aria-selected", "false"); await expectChatHistoryPill(page); }); diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index b5edb8a2d..446ddee5c 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -81,7 +81,7 @@ import { type OpenFileDisposition, type WorkspaceFileOpenRequest, } from "@/workspace/file-open"; -import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation"; +import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; import { buildNewWorkspaceRoute } from "@/utils/host-routes"; import { useStableEvent } from "@/hooks/use-stable-event"; import { isWeb } from "@/constants/platform"; @@ -423,7 +423,7 @@ const AgentStreamViewComponent = forwardRef { if (!workspace.serverId) return; onWorkspacePress?.(); - navigateToWorkspace(workspace.serverId, workspace.workspaceId); + navigateToWorkspace({ serverId: workspace.serverId, workspaceId: workspace.workspaceId }); }, [onWorkspacePress, workspace.serverId, workspace.workspaceId]); return ( diff --git a/packages/app/src/components/workspace-setup-dialog.tsx b/packages/app/src/components/workspace-setup-dialog.tsx index 90eef6906..0a539a439 100644 --- a/packages/app/src/components/workspace-setup-dialog.tsx +++ b/packages/app/src/components/workspace-setup-dialog.tsx @@ -26,7 +26,7 @@ import type { import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name"; import { requireWorkspaceDirectory } from "@/utils/workspace-directory"; import { navigateToAgent } from "@/utils/navigate-to-agent"; -import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation"; +import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; import type { MessagePayload } from "@/composer/types"; function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null { @@ -218,7 +218,7 @@ export function WorkspaceSetupDialog() { return; } - navigateToPreparedWorkspaceTab({ + navigateToWorkspace({ serverId: pendingWorkspaceSetup.serverId, workspaceId, target, diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index 0f427c573..67c81bc54 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -120,7 +120,7 @@ export function useKeyboardShortcuts({ serverId: action.serverId, workspaceId: action.workspaceId, }; - navigateToWorkspace(action.serverId, action.workspaceId); + navigateToWorkspace({ serverId: action.serverId, workspaceId: action.workspaceId }); return true; case "navigate-last-workspace": return navigateToLastWorkspace(); diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index 93eaa9496..b4034e81b 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -46,8 +46,10 @@ import { } from "@/runtime/host-runtime"; import { useHostFeature, useHostFeatureMap } from "@/runtime/host-features"; import type { HostProfile } from "@/types/host-connection"; -import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; -import { useLastWorkspaceSelection } from "@/stores/navigation-active-workspace-store"; +import { + navigateToWorkspace, + useLastWorkspaceSelection, +} from "@/stores/navigation-active-workspace-store"; import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store"; import { useWorkspace } from "@/stores/session-store-hooks"; import { buildNewWorkspaceDraftKey, generateDraftId } from "@/stores/draft-keys"; @@ -65,7 +67,6 @@ import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state"; import { generateMessageId } from "@/types/stream"; import { toErrorMessage } from "@/utils/error-messages"; import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name"; -import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation"; import { getHostProjectSourceDirectory, hostProjectFromRoute, @@ -1172,7 +1173,7 @@ function submitWorkspaceDraft(input: SubmitDraftInput): void { ...(submission.featureValues ? { featureValues: submission.featureValues } : {}), allowEmptyText: true, }); - navigateToPreparedWorkspaceTab({ + navigateToWorkspace({ serverId, workspaceId, target: submission.target, @@ -2044,7 +2045,7 @@ export function NewWorkspaceScreen({ ensureWorkspace, serverId: selectedServerId, navigate: (targetServerId, workspaceId) => - navigateToWorkspace(targetServerId, workspaceId), + navigateToWorkspace({ serverId: targetServerId, workspaceId }), }); return; } diff --git a/packages/app/src/stores/navigation-active-workspace-store/index.ts b/packages/app/src/stores/navigation-active-workspace-store/index.ts index dc4ebedb4..b3358b3a1 100644 --- a/packages/app/src/stores/navigation-active-workspace-store/index.ts +++ b/packages/app/src/stores/navigation-active-workspace-store/index.ts @@ -11,6 +11,7 @@ import { navigateToLastWorkspace as navigateToLastWorkspacePure, navigateToWorkspace as navigateToWorkspacePure, parseActiveWorkspaceSelection, + type NavigateToWorkspaceInput, type NavigateToWorkspaceDeps, } from "./navigation"; import { useSessionStore } from "@/stores/session-store"; @@ -19,6 +20,7 @@ import { stripHostWorkspaceRouteEchoSearchFromBrowserUrlAfterCommit } from "@/ut import { navigateToHostWorkspaceRoute } from "@/navigation/workspace-route-navigation"; export type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection"; +export type { NavigateToWorkspaceInput } from "./navigation"; const lastWorkspaceSelectionStorage: LastWorkspaceSelectionStorage = { read: () => AsyncStorage.getItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY), @@ -34,9 +36,10 @@ function navigateDeps(): NavigateToWorkspaceDeps { getSessionWorkspaces: (serverId) => useSessionStore.getState().sessions[serverId]?.workspaces, getSessionAgents: (serverId) => useSessionStore.getState().sessions[serverId]?.agents.values() ?? [], - openWorkspaceAgentTab: (workspaceKey, agentId) => { - useWorkspaceLayoutStore.getState().openTabFocused(workspaceKey, { kind: "agent", agentId }); - }, + openTabFocused: (workspaceKey, target) => + useWorkspaceLayoutStore.getState().openTabFocused(workspaceKey, target), + pinAgent: (workspaceKey, agentId) => + useWorkspaceLayoutStore.getState().pinAgent(workspaceKey, agentId), rememberLastWorkspace: (selection) => lastWorkspaceSelectionStore.remember(selection), navigateToRoute: (route) => { navigateToHostWorkspaceRoute(route); @@ -57,8 +60,8 @@ export function getIsLastWorkspaceSelectionHydrated(): boolean { return lastWorkspaceSelectionStore.isHydrated(); } -export function navigateToWorkspace(serverId: string, workspaceId: string) { - navigateToWorkspacePure(serverId, workspaceId, navigateDeps()); +export function navigateToWorkspace(input: NavigateToWorkspaceInput): string { + return navigateToWorkspacePure(input, navigateDeps()); } export function navigateToLastWorkspace(): boolean { diff --git a/packages/app/src/stores/navigation-active-workspace-store/navigation.test.ts b/packages/app/src/stores/navigation-active-workspace-store/navigation.test.ts index e2f8aab93..719948ee9 100644 --- a/packages/app/src/stores/navigation-active-workspace-store/navigation.test.ts +++ b/packages/app/src/stores/navigation-active-workspace-store/navigation.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection"; +import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store"; import { navigateToLastWorkspace, navigateToWorkspace, @@ -9,25 +10,28 @@ import { } from "./navigation"; import type { Agent, WorkspaceDescriptor } from "@/stores/session-store"; -interface RecordedAgentTab { +interface RecordedTab { workspaceKey: string; - agentId: string; + target: WorkspaceTabTarget; } function createFakeDeps(overrides: Partial = {}) { const navigations: string[] = []; const remembered: ActiveWorkspaceSelection[] = []; - const openedAgentTabs: RecordedAgentTab[] = []; + const openedTabs: RecordedTab[] = []; const deps: NavigateToWorkspaceDeps = { getSessionWorkspaces: () => null, getSessionAgents: () => [] as Agent[], - openWorkspaceAgentTab: (workspaceKey, agentId) => - openedAgentTabs.push({ workspaceKey, agentId }), + openTabFocused: (workspaceKey, target) => { + openedTabs.push({ workspaceKey, target }); + return target.kind === "agent" ? target.agentId : null; + }, + pinAgent: () => undefined, rememberLastWorkspace: (selection) => remembered.push(selection), navigateToRoute: (route) => navigations.push(route), ...overrides, }; - return { deps, navigations, remembered, openedAgentTabs }; + return { deps, navigations, remembered, openedTabs }; } function createLastSelectionDeps( @@ -63,7 +67,7 @@ describe("workspace navigation", () => { it("navigates to a workspace route and remembers the selection", () => { const { deps, navigations, remembered } = createFakeDeps(); - navigateToWorkspace("server-1", "workspace-a", deps); + navigateToWorkspace({ serverId: "server-1", workspaceId: "workspace-a" }, deps); expect(navigations).toEqual(["/h/server-1/workspace/workspace-a"]); expect(remembered).toEqual([{ serverId: "server-1", workspaceId: "workspace-a" }]); @@ -81,14 +85,53 @@ describe("workspace navigation", () => { requiresAttention: true, attentionReason: "permission", } as unknown as Agent; - const { deps, openedAgentTabs } = createFakeDeps({ + const { deps, openedTabs } = createFakeDeps({ getSessionWorkspaces: () => new Map([[workspace.id, workspace]]), getSessionAgents: () => [agent], }); - navigateToWorkspace("server-1", "workspace-a", deps); + navigateToWorkspace({ serverId: "server-1", workspaceId: "workspace-a" }, deps); - expect(openedAgentTabs).toEqual([{ workspaceKey: "server-1:workspace-a", agentId: "agent-1" }]); + expect(openedTabs).toEqual([ + { + workspaceKey: "server-1:workspace-a", + target: { kind: "agent", agentId: "agent-1" }, + }, + ]); + }); + + it("keeps an explicit tab authoritative over an attention agent", () => { + const workspace = { + id: "workspace-a", + workspaceDirectory: "/repo/workspace-a", + } as WorkspaceDescriptor; + const agent = { + id: "agent-1", + cwd: "/repo/workspace-a", + workspaceId: "workspace-a", + requiresAttention: true, + attentionReason: "permission", + } as unknown as Agent; + const { deps, openedTabs } = createFakeDeps({ + getSessionWorkspaces: () => new Map([[workspace.id, workspace]]), + getSessionAgents: () => [agent], + }); + + navigateToWorkspace( + { + serverId: "server-1", + workspaceId: "workspace-a", + target: { kind: "draft", draftId: "draft-1" }, + }, + deps, + ); + + expect(openedTabs).toEqual([ + { + workspaceKey: "server-1:workspace-a", + target: { kind: "draft", draftId: "draft-1" }, + }, + ]); }); it("reads the active workspace from the current route", () => { diff --git a/packages/app/src/stores/navigation-active-workspace-store/navigation.ts b/packages/app/src/stores/navigation-active-workspace-store/navigation.ts index 17c7c96b3..4465e67e8 100644 --- a/packages/app/src/stores/navigation-active-workspace-store/navigation.ts +++ b/packages/app/src/stores/navigation-active-workspace-store/navigation.ts @@ -10,6 +10,8 @@ import { resolveWorkspaceMapKeyByIdentity, } from "@/utils/workspace-identity"; import type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection"; +import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store"; +import { prepareWorkspaceTab, type PrepareWorkspaceTabDeps } from "@/utils/prepare-workspace-tab"; export interface RouteSelectionInput { pathname: string; @@ -19,10 +21,16 @@ export interface RouteSelectionInput { }; } -export interface NavigateToWorkspaceDeps { +export interface NavigateToWorkspaceInput { + serverId: string; + workspaceId: string; + target?: WorkspaceTabTarget; + pin?: boolean; +} + +export interface NavigateToWorkspaceDeps extends PrepareWorkspaceTabDeps { getSessionWorkspaces: (serverId: string) => Map | null | undefined; getSessionAgents: (serverId: string) => Iterable; - openWorkspaceAgentTab: (workspaceKey: string, agentId: string) => void; rememberLastWorkspace: (selection: ActiveWorkspaceSelection) => void; navigateToRoute: (route: string) => void; } @@ -71,27 +79,35 @@ export function parseActiveWorkspaceSelection( } export function navigateToWorkspace( - serverId: string, - workspaceId: string, + input: NavigateToWorkspaceInput, deps: NavigateToWorkspaceDeps, -): void { - const workspaces = deps.getSessionWorkspaces(serverId); - const resolvedWorkspaceId = resolveWorkspaceMapKeyByIdentity({ - workspaces, - workspaceId, - }); - const workspaceAgents = resolvedWorkspaceId - ? Array.from(deps.getSessionAgents(serverId)).filter( - (agent) => normalizeWorkspaceOpaqueId(agent.workspaceId) === resolvedWorkspaceId, - ) - : []; - const attentionAgentId = pickAttentionAgent(workspaceAgents); - if (attentionAgentId && resolvedWorkspaceId) { - deps.openWorkspaceAgentTab(`${serverId}:${resolvedWorkspaceId}`, attentionAgentId); +): string { + if (input.target) { + prepareWorkspaceTab({ ...input, target: input.target }, deps); + } else { + const workspaces = deps.getSessionWorkspaces(input.serverId); + const resolvedWorkspaceId = resolveWorkspaceMapKeyByIdentity({ + workspaces, + workspaceId: input.workspaceId, + }); + const workspaceAgents = resolvedWorkspaceId + ? Array.from(deps.getSessionAgents(input.serverId)).filter( + (agent) => normalizeWorkspaceOpaqueId(agent.workspaceId) === resolvedWorkspaceId, + ) + : []; + const attentionAgentId = pickAttentionAgent(workspaceAgents); + if (attentionAgentId && resolvedWorkspaceId) { + deps.openTabFocused(`${input.serverId}:${resolvedWorkspaceId}`, { + kind: "agent", + agentId: attentionAgentId, + }); + } } - deps.rememberLastWorkspace({ serverId, workspaceId }); - deps.navigateToRoute(buildHostWorkspaceRoute(serverId, workspaceId)); + const route = buildHostWorkspaceRoute(input.serverId, input.workspaceId); + deps.rememberLastWorkspace({ serverId: input.serverId, workspaceId: input.workspaceId }); + deps.navigateToRoute(route); + return route; } export function navigateToLastWorkspace(deps: NavigateToLastWorkspaceDeps): boolean { @@ -99,6 +115,6 @@ export function navigateToLastWorkspace(deps: NavigateToLastWorkspaceDeps): bool if (!selection) { return false; } - navigateToWorkspace(selection.serverId, selection.workspaceId, deps); + navigateToWorkspace(selection, deps); return true; } diff --git a/packages/app/src/utils/navigate-to-agent/index.ts b/packages/app/src/utils/navigate-to-agent/index.ts index 175e0ae8e..292c9bbed 100644 --- a/packages/app/src/utils/navigate-to-agent/index.ts +++ b/packages/app/src/utils/navigate-to-agent/index.ts @@ -2,7 +2,7 @@ import { router, type Href } from "expo-router"; import { useSessionStore } from "@/stores/session-store"; import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime"; import { resolveNavigateToAgent, type NavigateToAgentInput } from "./resolve"; -import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation"; +import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; export type { NavigateToAgentInput } from "./resolve"; @@ -69,7 +69,7 @@ export function navigateToAgent(input: NavigateToAgentInput): string { navigateToHostAgent: (route) => { router.navigate(route as Href); }, - navigateToPreparedWorkspaceTab, + navigateToWorkspace, restoreArchivedWorkspace: ({ serverId, agentId, workspaceId }) => { restoreArchivedWorkspace(serverId, agentId, workspaceId); }, diff --git a/packages/app/src/utils/navigate-to-agent/resolve.test.ts b/packages/app/src/utils/navigate-to-agent/resolve.test.ts index 4f0a31af2..8347f1a0f 100644 --- a/packages/app/src/utils/navigate-to-agent/resolve.test.ts +++ b/packages/app/src/utils/navigate-to-agent/resolve.test.ts @@ -4,7 +4,7 @@ import { type AgentNavTarget, type NavigateToAgentDeps, } from "@/utils/navigate-to-agent/resolve"; -import type { NavigateToPreparedWorkspaceTabInput } from "@/utils/prepare-workspace-tab"; +import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store"; const SERVER_ID = "server-1"; const WORKSPACE_ID = "workspace-1"; @@ -14,7 +14,7 @@ interface RecordedHostNav { route: string; } -interface RecordedTabNav extends NavigateToPreparedWorkspaceTabInput {} +interface RecordedTabNav extends NavigateToWorkspaceInput {} interface RecordedRestore { serverId: string; @@ -40,7 +40,7 @@ function createFakeNavigators(target: AgentNavTarget): { navigateToHostAgent: (route) => { hostNavigations.push({ route }); }, - navigateToPreparedWorkspaceTab: (input) => { + navigateToWorkspace: (input) => { tabNavigations.push(input); return `/h/${input.serverId}/workspace/${input.workspaceId}`; }, diff --git a/packages/app/src/utils/navigate-to-agent/resolve.ts b/packages/app/src/utils/navigate-to-agent/resolve.ts index 6298608ef..95a65229f 100644 --- a/packages/app/src/utils/navigate-to-agent/resolve.ts +++ b/packages/app/src/utils/navigate-to-agent/resolve.ts @@ -1,6 +1,6 @@ import { buildHostAgentDetailRoute } from "@/utils/host-routes"; import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity"; -import type { NavigateToPreparedWorkspaceTabInput } from "@/utils/prepare-workspace-tab"; +import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store"; export interface NavigateToAgentInput { serverId: string; @@ -18,7 +18,7 @@ export interface AgentNavTarget { export interface NavigateToAgentDeps { readAgentNavTarget: (input: { serverId: string; agentId: string }) => AgentNavTarget; navigateToHostAgent: (route: string) => void; - navigateToPreparedWorkspaceTab: (input: NavigateToPreparedWorkspaceTabInput) => string; + navigateToWorkspace: (input: NavigateToWorkspaceInput) => string; restoreArchivedWorkspace: (input: { serverId: string; agentId: string; @@ -49,7 +49,7 @@ export function resolveNavigateToAgent( workspaceId, }); - return deps.navigateToPreparedWorkspaceTab({ + return deps.navigateToWorkspace({ serverId: input.serverId, workspaceId, target: { kind: "agent", agentId: input.agentId }, diff --git a/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts b/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts index ab0c01b31..87d3558bf 100644 --- a/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts +++ b/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts @@ -9,8 +9,8 @@ vi.mock("expo-router", () => ({ router: { navigate: vi.fn() }, })); -vi.mock("@/utils/workspace-navigation", () => ({ - navigateToPreparedWorkspaceTab: vi.fn(() => ""), +vi.mock("@/stores/navigation-active-workspace-store", () => ({ + navigateToWorkspace: vi.fn(() => ""), })); vi.mock("@/runtime/host-runtime", () => ({ diff --git a/packages/app/src/utils/prepare-workspace-tab.ts b/packages/app/src/utils/prepare-workspace-tab.ts index ae76e96b5..213b107f1 100644 --- a/packages/app/src/utils/prepare-workspace-tab.ts +++ b/packages/app/src/utils/prepare-workspace-tab.ts @@ -3,7 +3,6 @@ import { buildWorkspaceTabPersistenceKey, type WorkspaceTabTarget, } from "@/stores/workspace-tabs-store"; -import { buildHostWorkspaceRoute } from "@/utils/host-routes"; export interface PrepareWorkspaceTabInput { serverId: string; @@ -12,17 +11,11 @@ export interface PrepareWorkspaceTabInput { pin?: boolean; } -export type NavigateToPreparedWorkspaceTabInput = PrepareWorkspaceTabInput; - export interface PrepareWorkspaceTabDeps { openTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; pinAgent: (workspaceKey: string, agentId: string) => void; } -export interface NavigateToPreparedWorkspaceTabDeps extends PrepareWorkspaceTabDeps { - navigateToWorkspace: (serverId: string, workspaceId: string) => void; -} - function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget { if (target.kind !== "draft" || target.draftId.trim() !== "new") { return target; @@ -33,7 +26,7 @@ function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget { export function prepareWorkspaceTab( input: PrepareWorkspaceTabInput, deps: PrepareWorkspaceTabDeps, -): string { +): void { const target = getPreparedTarget(input.target); const key = buildWorkspaceTabPersistenceKey({ @@ -46,15 +39,4 @@ export function prepareWorkspaceTab( if (input.pin && target.kind === "agent") { deps.pinAgent(key, target.agentId); } - - return buildHostWorkspaceRoute(input.serverId, input.workspaceId); -} - -export function navigateToPreparedWorkspaceTab( - input: NavigateToPreparedWorkspaceTabInput, - deps: NavigateToPreparedWorkspaceTabDeps, -): string { - const route = prepareWorkspaceTab(input, deps); - deps.navigateToWorkspace(input.serverId, input.workspaceId); - return route; } diff --git a/packages/app/src/utils/workspace-navigation.test.ts b/packages/app/src/utils/workspace-navigation.test.ts index 2b3c60a7c..0f96b5f22 100644 --- a/packages/app/src/utils/workspace-navigation.test.ts +++ b/packages/app/src/utils/workspace-navigation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store"; -import { navigateToPreparedWorkspaceTab, prepareWorkspaceTab } from "@/utils/prepare-workspace-tab"; +import { prepareWorkspaceTab } from "@/utils/prepare-workspace-tab"; const SERVER_ID = "server-1"; const WORKSPACE_ID = "/repo/worktree"; @@ -16,11 +16,6 @@ interface RecordedPin { agentId: string; } -interface RecordedNavigation { - serverId: string; - workspaceId: string; -} - function createFakeLayout() { const openedTabs: RecordedOpenedTab[] = []; const pinnedAgents: RecordedPin[] = []; @@ -37,21 +32,11 @@ function createFakeLayout() { }; } -function createFakeNavigator() { - const navigations: RecordedNavigation[] = []; - return { - navigations, - navigateToWorkspace: (serverId: string, workspaceId: string) => { - navigations.push({ serverId, workspaceId }); - }, - }; -} - describe("prepareWorkspaceTab", () => { it("opens and focuses an agent tab", () => { const layout = createFakeLayout(); - const route = prepareWorkspaceTab( + prepareWorkspaceTab( { serverId: SERVER_ID, workspaceId: WORKSPACE_ID, @@ -60,30 +45,9 @@ describe("prepareWorkspaceTab", () => { layout, ); - expect(route).toBe("/h/server-1/workspace/b64_L3JlcG8vd29ya3RyZWU"); expect(layout.openedTabs).toEqual([ { key: "server-1:/repo/worktree", target: { kind: "agent", agentId: AGENT_ID } }, ]); expect(layout.pinnedAgents).toEqual([]); }); - - it("prepares a tab and navigates through the workspace navigation helper", () => { - const layout = createFakeLayout(); - const navigator = createFakeNavigator(); - - const route = navigateToPreparedWorkspaceTab( - { - serverId: SERVER_ID, - workspaceId: WORKSPACE_ID, - target: { kind: "agent", agentId: AGENT_ID }, - }, - { ...layout, navigateToWorkspace: navigator.navigateToWorkspace }, - ); - - expect(route).toBe("/h/server-1/workspace/b64_L3JlcG8vd29ya3RyZWU"); - expect(layout.openedTabs).toEqual([ - { key: "server-1:/repo/worktree", target: { kind: "agent", agentId: AGENT_ID } }, - ]); - expect(navigator.navigations).toEqual([{ serverId: SERVER_ID, workspaceId: WORKSPACE_ID }]); - }); }); diff --git a/packages/app/src/utils/workspace-navigation.ts b/packages/app/src/utils/workspace-navigation.ts index 514929c6d..57c0ff6ab 100644 --- a/packages/app/src/utils/workspace-navigation.ts +++ b/packages/app/src/utils/workspace-navigation.ts @@ -1,16 +1,10 @@ -import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store"; import { prepareWorkspaceTab as prepareWorkspaceTabPure, - navigateToPreparedWorkspaceTab as navigateToPreparedWorkspaceTabPure, type PrepareWorkspaceTabInput, - type NavigateToPreparedWorkspaceTabInput, } from "./prepare-workspace-tab"; -export type { - PrepareWorkspaceTabInput, - NavigateToPreparedWorkspaceTabInput, -} from "./prepare-workspace-tab"; +export type { PrepareWorkspaceTabInput } from "./prepare-workspace-tab"; function layoutStoreDeps() { const store = useWorkspaceLayoutStore.getState(); @@ -20,13 +14,6 @@ function layoutStoreDeps() { }; } -export function prepareWorkspaceTab(input: PrepareWorkspaceTabInput): string { - return prepareWorkspaceTabPure(input, layoutStoreDeps()); -} - -export function navigateToPreparedWorkspaceTab(input: NavigateToPreparedWorkspaceTabInput): string { - return navigateToPreparedWorkspaceTabPure(input, { - ...layoutStoreDeps(), - navigateToWorkspace, - }); +export function prepareWorkspaceTab(input: PrepareWorkspaceTabInput): void { + prepareWorkspaceTabPure(input, layoutStoreDeps()); }