mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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}`;
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user