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
This commit is contained in:
Mohamed Boudra
2026-07-13 15:38:09 +02:00
committed by GitHub
parent fe9a486ac1
commit f350c716b9
18 changed files with 156 additions and 139 deletions

View File

@@ -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

View File

@@ -54,14 +54,14 @@ async function expectChatHistoryPill(page: Page): Promise<void> {
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);
});

View File

@@ -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<AgentStreamViewHandle, AgentStreamVi
}
if (context.workspaceId) {
navigateToPreparedWorkspaceTab({
navigateToWorkspace({
serverId: resolvedServerId,
workspaceId: context.workspaceId,
target: createWorkspaceFileTabTarget(location),
@@ -491,7 +491,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
throw new Error(t("message.actions.forkMissingWorkspace"));
}
const draftId = await prepareForkDraft();
navigateToPreparedWorkspaceTab({
navigateToWorkspace({
serverId: resolvedServerId,
workspaceId,
target: buildForkDraftTabTarget(draftSetup, draftId),

View File

@@ -1677,7 +1677,7 @@ function WorkspaceRowItem({
return;
}
onWorkspacePress?.();
navigateToWorkspace(workspace.serverId, workspace.workspaceId);
navigateToWorkspace({ serverId: workspace.serverId, workspaceId: workspace.workspaceId });
}, [onWorkspacePress, workspace.serverId, workspace.workspaceId]);
return (

View File

@@ -345,7 +345,7 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
const handlePress = useCallback(() => {
if (!workspace.serverId) return;
onWorkspacePress?.();
navigateToWorkspace(workspace.serverId, workspace.workspaceId);
navigateToWorkspace({ serverId: workspace.serverId, workspaceId: workspace.workspaceId });
}, [onWorkspacePress, workspace.serverId, workspace.workspaceId]);
return (

View File

@@ -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,

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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<NavigateToWorkspaceDeps> = {}) {
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", () => {

View File

@@ -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<string, WorkspaceDescriptor> | null | undefined;
getSessionAgents: (serverId: string) => Iterable<Agent>;
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;
}

View File

@@ -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);
},

View File

@@ -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}`;
},

View File

@@ -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 },

View File

@@ -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", () => ({

View File

@@ -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;
}

View File

@@ -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 }]);
});
});

View File

@@ -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());
}