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:
+9
-3
@@ -48,8 +48,13 @@ dynamic params exist before any nested workspace leaf is selected.
|
|||||||
## App-Wide Route Hops
|
## App-Wide Route Hops
|
||||||
|
|
||||||
When app-wide routes such as `/new`, `/settings`, or `/sessions` navigate back
|
When app-wide routes such as `/new`, `/settings`, or `/sessions` navigate back
|
||||||
into a host workspace, express only the destination with `navigateToWorkspace()`.
|
into a host workspace, use `navigateToWorkspace()`. Do not make the caller
|
||||||
Do not make the caller branch on its current route.
|
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
|
The root stack owns `h/[serverId]`; the host stack owns
|
||||||
`workspace/[workspaceId]/index`. Repeated global-route hops must `POP_TO` the
|
`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 change `packages/app/src/app`? Re-read this file.
|
||||||
- [ ] Did you touch remembered workspace restore? Keep root on `/h/[serverId]`.
|
- [ ] 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 you add a route? Register it in the layout that directly owns it.
|
||||||
- [ ] Did `useLocalSearchParams()` lose a required param? Fix the route tree.
|
- [ ] Did `useLocalSearchParams()` lose a required param? Fix the route tree.
|
||||||
- [ ] Did native show a blank screen without a crash? Suspect route ownership
|
- [ ] 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("Assistant fork menu", () => {
|
||||||
test.describe.configure({ timeout: 180_000 });
|
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,
|
page,
|
||||||
seedForkWorkspace,
|
seedForkWorkspace,
|
||||||
}) => {
|
}) => {
|
||||||
const session = await seedForkWorkspace({
|
const session = await seedForkWorkspace({
|
||||||
repoPrefix: "assistant-fork-tab-",
|
repoPrefix: "assistant-fork-focused-tab-",
|
||||||
title: "Assistant fork tab",
|
title: "Assistant fork focused tab",
|
||||||
initialPrompt: "emit 1 coalesced agent stream updates for assistant fork tab.",
|
initialPrompt: "emit 1 coalesced agent stream updates for initial assistant fork turn.",
|
||||||
model: "ten-second-stream",
|
model: "ten-second-stream",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,9 +70,24 @@ test.describe("Assistant fork menu", () => {
|
|||||||
await awaitAssistantMessage(page);
|
await awaitAssistantMessage(page);
|
||||||
await session.client.waitForFinish(session.agentId, 45_000);
|
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 openAssistantForkMenu(page);
|
||||||
await page.getByTestId("assistant-fork-menu-new-tab").click();
|
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);
|
await expectChatHistoryPill(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ import {
|
|||||||
type OpenFileDisposition,
|
type OpenFileDisposition,
|
||||||
type WorkspaceFileOpenRequest,
|
type WorkspaceFileOpenRequest,
|
||||||
} from "@/workspace/file-open";
|
} 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 { buildNewWorkspaceRoute } from "@/utils/host-routes";
|
||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
@@ -423,7 +423,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context.workspaceId) {
|
if (context.workspaceId) {
|
||||||
navigateToPreparedWorkspaceTab({
|
navigateToWorkspace({
|
||||||
serverId: resolvedServerId,
|
serverId: resolvedServerId,
|
||||||
workspaceId: context.workspaceId,
|
workspaceId: context.workspaceId,
|
||||||
target: createWorkspaceFileTabTarget(location),
|
target: createWorkspaceFileTabTarget(location),
|
||||||
@@ -491,7 +491,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
throw new Error(t("message.actions.forkMissingWorkspace"));
|
throw new Error(t("message.actions.forkMissingWorkspace"));
|
||||||
}
|
}
|
||||||
const draftId = await prepareForkDraft();
|
const draftId = await prepareForkDraft();
|
||||||
navigateToPreparedWorkspaceTab({
|
navigateToWorkspace({
|
||||||
serverId: resolvedServerId,
|
serverId: resolvedServerId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
target: buildForkDraftTabTarget(draftSetup, draftId),
|
target: buildForkDraftTabTarget(draftSetup, draftId),
|
||||||
|
|||||||
@@ -1677,7 +1677,7 @@ function WorkspaceRowItem({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
navigateToWorkspace(workspace.serverId, workspace.workspaceId);
|
navigateToWorkspace({ serverId: workspace.serverId, workspaceId: workspace.workspaceId });
|
||||||
}, [onWorkspacePress, workspace.serverId, workspace.workspaceId]);
|
}, [onWorkspacePress, workspace.serverId, workspace.workspaceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
|||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
if (!workspace.serverId) return;
|
if (!workspace.serverId) return;
|
||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
navigateToWorkspace(workspace.serverId, workspace.workspaceId);
|
navigateToWorkspace({ serverId: workspace.serverId, workspaceId: workspace.workspaceId });
|
||||||
}, [onWorkspacePress, workspace.serverId, workspace.workspaceId]);
|
}, [onWorkspacePress, workspace.serverId, workspace.workspaceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import type {
|
|||||||
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
||||||
import { requireWorkspaceDirectory } from "@/utils/workspace-directory";
|
import { requireWorkspaceDirectory } from "@/utils/workspace-directory";
|
||||||
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
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";
|
import type { MessagePayload } from "@/composer/types";
|
||||||
|
|
||||||
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
||||||
@@ -218,7 +218,7 @@ export function WorkspaceSetupDialog() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToPreparedWorkspaceTab({
|
navigateToWorkspace({
|
||||||
serverId: pendingWorkspaceSetup.serverId,
|
serverId: pendingWorkspaceSetup.serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
target,
|
target,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function useKeyboardShortcuts({
|
|||||||
serverId: action.serverId,
|
serverId: action.serverId,
|
||||||
workspaceId: action.workspaceId,
|
workspaceId: action.workspaceId,
|
||||||
};
|
};
|
||||||
navigateToWorkspace(action.serverId, action.workspaceId);
|
navigateToWorkspace({ serverId: action.serverId, workspaceId: action.workspaceId });
|
||||||
return true;
|
return true;
|
||||||
case "navigate-last-workspace":
|
case "navigate-last-workspace":
|
||||||
return navigateToLastWorkspace();
|
return navigateToLastWorkspace();
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ import {
|
|||||||
} from "@/runtime/host-runtime";
|
} from "@/runtime/host-runtime";
|
||||||
import { useHostFeature, useHostFeatureMap } from "@/runtime/host-features";
|
import { useHostFeature, useHostFeatureMap } from "@/runtime/host-features";
|
||||||
import type { HostProfile } from "@/types/host-connection";
|
import type { HostProfile } from "@/types/host-connection";
|
||||||
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
import {
|
||||||
import { useLastWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
navigateToWorkspace,
|
||||||
|
useLastWorkspaceSelection,
|
||||||
|
} from "@/stores/navigation-active-workspace-store";
|
||||||
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
|
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
|
||||||
import { useWorkspace } from "@/stores/session-store-hooks";
|
import { useWorkspace } from "@/stores/session-store-hooks";
|
||||||
import { buildNewWorkspaceDraftKey, generateDraftId } from "@/stores/draft-keys";
|
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 { generateMessageId } from "@/types/stream";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
||||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
|
||||||
import {
|
import {
|
||||||
getHostProjectSourceDirectory,
|
getHostProjectSourceDirectory,
|
||||||
hostProjectFromRoute,
|
hostProjectFromRoute,
|
||||||
@@ -1172,7 +1173,7 @@ function submitWorkspaceDraft(input: SubmitDraftInput): void {
|
|||||||
...(submission.featureValues ? { featureValues: submission.featureValues } : {}),
|
...(submission.featureValues ? { featureValues: submission.featureValues } : {}),
|
||||||
allowEmptyText: true,
|
allowEmptyText: true,
|
||||||
});
|
});
|
||||||
navigateToPreparedWorkspaceTab({
|
navigateToWorkspace({
|
||||||
serverId,
|
serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
target: submission.target,
|
target: submission.target,
|
||||||
@@ -2044,7 +2045,7 @@ export function NewWorkspaceScreen({
|
|||||||
ensureWorkspace,
|
ensureWorkspace,
|
||||||
serverId: selectedServerId,
|
serverId: selectedServerId,
|
||||||
navigate: (targetServerId, workspaceId) =>
|
navigate: (targetServerId, workspaceId) =>
|
||||||
navigateToWorkspace(targetServerId, workspaceId),
|
navigateToWorkspace({ serverId: targetServerId, workspaceId }),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
navigateToLastWorkspace as navigateToLastWorkspacePure,
|
navigateToLastWorkspace as navigateToLastWorkspacePure,
|
||||||
navigateToWorkspace as navigateToWorkspacePure,
|
navigateToWorkspace as navigateToWorkspacePure,
|
||||||
parseActiveWorkspaceSelection,
|
parseActiveWorkspaceSelection,
|
||||||
|
type NavigateToWorkspaceInput,
|
||||||
type NavigateToWorkspaceDeps,
|
type NavigateToWorkspaceDeps,
|
||||||
} from "./navigation";
|
} from "./navigation";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
@@ -19,6 +20,7 @@ import { stripHostWorkspaceRouteEchoSearchFromBrowserUrlAfterCommit } from "@/ut
|
|||||||
import { navigateToHostWorkspaceRoute } from "@/navigation/workspace-route-navigation";
|
import { navigateToHostWorkspaceRoute } from "@/navigation/workspace-route-navigation";
|
||||||
|
|
||||||
export type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection";
|
export type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection";
|
||||||
|
export type { NavigateToWorkspaceInput } from "./navigation";
|
||||||
|
|
||||||
const lastWorkspaceSelectionStorage: LastWorkspaceSelectionStorage = {
|
const lastWorkspaceSelectionStorage: LastWorkspaceSelectionStorage = {
|
||||||
read: () => AsyncStorage.getItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY),
|
read: () => AsyncStorage.getItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY),
|
||||||
@@ -34,9 +36,10 @@ function navigateDeps(): NavigateToWorkspaceDeps {
|
|||||||
getSessionWorkspaces: (serverId) => useSessionStore.getState().sessions[serverId]?.workspaces,
|
getSessionWorkspaces: (serverId) => useSessionStore.getState().sessions[serverId]?.workspaces,
|
||||||
getSessionAgents: (serverId) =>
|
getSessionAgents: (serverId) =>
|
||||||
useSessionStore.getState().sessions[serverId]?.agents.values() ?? [],
|
useSessionStore.getState().sessions[serverId]?.agents.values() ?? [],
|
||||||
openWorkspaceAgentTab: (workspaceKey, agentId) => {
|
openTabFocused: (workspaceKey, target) =>
|
||||||
useWorkspaceLayoutStore.getState().openTabFocused(workspaceKey, { kind: "agent", agentId });
|
useWorkspaceLayoutStore.getState().openTabFocused(workspaceKey, target),
|
||||||
},
|
pinAgent: (workspaceKey, agentId) =>
|
||||||
|
useWorkspaceLayoutStore.getState().pinAgent(workspaceKey, agentId),
|
||||||
rememberLastWorkspace: (selection) => lastWorkspaceSelectionStore.remember(selection),
|
rememberLastWorkspace: (selection) => lastWorkspaceSelectionStore.remember(selection),
|
||||||
navigateToRoute: (route) => {
|
navigateToRoute: (route) => {
|
||||||
navigateToHostWorkspaceRoute(route);
|
navigateToHostWorkspaceRoute(route);
|
||||||
@@ -57,8 +60,8 @@ export function getIsLastWorkspaceSelectionHydrated(): boolean {
|
|||||||
return lastWorkspaceSelectionStore.isHydrated();
|
return lastWorkspaceSelectionStore.isHydrated();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function navigateToWorkspace(serverId: string, workspaceId: string) {
|
export function navigateToWorkspace(input: NavigateToWorkspaceInput): string {
|
||||||
navigateToWorkspacePure(serverId, workspaceId, navigateDeps());
|
return navigateToWorkspacePure(input, navigateDeps());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function navigateToLastWorkspace(): boolean {
|
export function navigateToLastWorkspace(): boolean {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection";
|
import type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection";
|
||||||
|
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||||
import {
|
import {
|
||||||
navigateToLastWorkspace,
|
navigateToLastWorkspace,
|
||||||
navigateToWorkspace,
|
navigateToWorkspace,
|
||||||
@@ -9,25 +10,28 @@ import {
|
|||||||
} from "./navigation";
|
} from "./navigation";
|
||||||
import type { Agent, WorkspaceDescriptor } from "@/stores/session-store";
|
import type { Agent, WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
|
|
||||||
interface RecordedAgentTab {
|
interface RecordedTab {
|
||||||
workspaceKey: string;
|
workspaceKey: string;
|
||||||
agentId: string;
|
target: WorkspaceTabTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFakeDeps(overrides: Partial<NavigateToWorkspaceDeps> = {}) {
|
function createFakeDeps(overrides: Partial<NavigateToWorkspaceDeps> = {}) {
|
||||||
const navigations: string[] = [];
|
const navigations: string[] = [];
|
||||||
const remembered: ActiveWorkspaceSelection[] = [];
|
const remembered: ActiveWorkspaceSelection[] = [];
|
||||||
const openedAgentTabs: RecordedAgentTab[] = [];
|
const openedTabs: RecordedTab[] = [];
|
||||||
const deps: NavigateToWorkspaceDeps = {
|
const deps: NavigateToWorkspaceDeps = {
|
||||||
getSessionWorkspaces: () => null,
|
getSessionWorkspaces: () => null,
|
||||||
getSessionAgents: () => [] as Agent[],
|
getSessionAgents: () => [] as Agent[],
|
||||||
openWorkspaceAgentTab: (workspaceKey, agentId) =>
|
openTabFocused: (workspaceKey, target) => {
|
||||||
openedAgentTabs.push({ workspaceKey, agentId }),
|
openedTabs.push({ workspaceKey, target });
|
||||||
|
return target.kind === "agent" ? target.agentId : null;
|
||||||
|
},
|
||||||
|
pinAgent: () => undefined,
|
||||||
rememberLastWorkspace: (selection) => remembered.push(selection),
|
rememberLastWorkspace: (selection) => remembered.push(selection),
|
||||||
navigateToRoute: (route) => navigations.push(route),
|
navigateToRoute: (route) => navigations.push(route),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
return { deps, navigations, remembered, openedAgentTabs };
|
return { deps, navigations, remembered, openedTabs };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLastSelectionDeps(
|
function createLastSelectionDeps(
|
||||||
@@ -63,7 +67,7 @@ describe("workspace navigation", () => {
|
|||||||
it("navigates to a workspace route and remembers the selection", () => {
|
it("navigates to a workspace route and remembers the selection", () => {
|
||||||
const { deps, navigations, remembered } = createFakeDeps();
|
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(navigations).toEqual(["/h/server-1/workspace/workspace-a"]);
|
||||||
expect(remembered).toEqual([{ serverId: "server-1", workspaceId: "workspace-a" }]);
|
expect(remembered).toEqual([{ serverId: "server-1", workspaceId: "workspace-a" }]);
|
||||||
@@ -81,14 +85,53 @@ describe("workspace navigation", () => {
|
|||||||
requiresAttention: true,
|
requiresAttention: true,
|
||||||
attentionReason: "permission",
|
attentionReason: "permission",
|
||||||
} as unknown as Agent;
|
} as unknown as Agent;
|
||||||
const { deps, openedAgentTabs } = createFakeDeps({
|
const { deps, openedTabs } = createFakeDeps({
|
||||||
getSessionWorkspaces: () => new Map([[workspace.id, workspace]]),
|
getSessionWorkspaces: () => new Map([[workspace.id, workspace]]),
|
||||||
getSessionAgents: () => [agent],
|
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", () => {
|
it("reads the active workspace from the current route", () => {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
resolveWorkspaceMapKeyByIdentity,
|
resolveWorkspaceMapKeyByIdentity,
|
||||||
} from "@/utils/workspace-identity";
|
} from "@/utils/workspace-identity";
|
||||||
import type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection";
|
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 {
|
export interface RouteSelectionInput {
|
||||||
pathname: string;
|
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;
|
getSessionWorkspaces: (serverId: string) => Map<string, WorkspaceDescriptor> | null | undefined;
|
||||||
getSessionAgents: (serverId: string) => Iterable<Agent>;
|
getSessionAgents: (serverId: string) => Iterable<Agent>;
|
||||||
openWorkspaceAgentTab: (workspaceKey: string, agentId: string) => void;
|
|
||||||
rememberLastWorkspace: (selection: ActiveWorkspaceSelection) => void;
|
rememberLastWorkspace: (selection: ActiveWorkspaceSelection) => void;
|
||||||
navigateToRoute: (route: string) => void;
|
navigateToRoute: (route: string) => void;
|
||||||
}
|
}
|
||||||
@@ -71,27 +79,35 @@ export function parseActiveWorkspaceSelection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function navigateToWorkspace(
|
export function navigateToWorkspace(
|
||||||
serverId: string,
|
input: NavigateToWorkspaceInput,
|
||||||
workspaceId: string,
|
|
||||||
deps: NavigateToWorkspaceDeps,
|
deps: NavigateToWorkspaceDeps,
|
||||||
): void {
|
): string {
|
||||||
const workspaces = deps.getSessionWorkspaces(serverId);
|
if (input.target) {
|
||||||
const resolvedWorkspaceId = resolveWorkspaceMapKeyByIdentity({
|
prepareWorkspaceTab({ ...input, target: input.target }, deps);
|
||||||
workspaces,
|
} else {
|
||||||
workspaceId,
|
const workspaces = deps.getSessionWorkspaces(input.serverId);
|
||||||
});
|
const resolvedWorkspaceId = resolveWorkspaceMapKeyByIdentity({
|
||||||
const workspaceAgents = resolvedWorkspaceId
|
workspaces,
|
||||||
? Array.from(deps.getSessionAgents(serverId)).filter(
|
workspaceId: input.workspaceId,
|
||||||
(agent) => normalizeWorkspaceOpaqueId(agent.workspaceId) === resolvedWorkspaceId,
|
});
|
||||||
)
|
const workspaceAgents = resolvedWorkspaceId
|
||||||
: [];
|
? Array.from(deps.getSessionAgents(input.serverId)).filter(
|
||||||
const attentionAgentId = pickAttentionAgent(workspaceAgents);
|
(agent) => normalizeWorkspaceOpaqueId(agent.workspaceId) === resolvedWorkspaceId,
|
||||||
if (attentionAgentId && resolvedWorkspaceId) {
|
)
|
||||||
deps.openWorkspaceAgentTab(`${serverId}:${resolvedWorkspaceId}`, attentionAgentId);
|
: [];
|
||||||
|
const attentionAgentId = pickAttentionAgent(workspaceAgents);
|
||||||
|
if (attentionAgentId && resolvedWorkspaceId) {
|
||||||
|
deps.openTabFocused(`${input.serverId}:${resolvedWorkspaceId}`, {
|
||||||
|
kind: "agent",
|
||||||
|
agentId: attentionAgentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.rememberLastWorkspace({ serverId, workspaceId });
|
const route = buildHostWorkspaceRoute(input.serverId, input.workspaceId);
|
||||||
deps.navigateToRoute(buildHostWorkspaceRoute(serverId, workspaceId));
|
deps.rememberLastWorkspace({ serverId: input.serverId, workspaceId: input.workspaceId });
|
||||||
|
deps.navigateToRoute(route);
|
||||||
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function navigateToLastWorkspace(deps: NavigateToLastWorkspaceDeps): boolean {
|
export function navigateToLastWorkspace(deps: NavigateToLastWorkspaceDeps): boolean {
|
||||||
@@ -99,6 +115,6 @@ export function navigateToLastWorkspace(deps: NavigateToLastWorkspaceDeps): bool
|
|||||||
if (!selection) {
|
if (!selection) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
navigateToWorkspace(selection.serverId, selection.workspaceId, deps);
|
navigateToWorkspace(selection, deps);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { router, type Href } from "expo-router";
|
|||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
|
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
|
||||||
import { resolveNavigateToAgent, type NavigateToAgentInput } from "./resolve";
|
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";
|
export type { NavigateToAgentInput } from "./resolve";
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export function navigateToAgent(input: NavigateToAgentInput): string {
|
|||||||
navigateToHostAgent: (route) => {
|
navigateToHostAgent: (route) => {
|
||||||
router.navigate(route as Href);
|
router.navigate(route as Href);
|
||||||
},
|
},
|
||||||
navigateToPreparedWorkspaceTab,
|
navigateToWorkspace,
|
||||||
restoreArchivedWorkspace: ({ serverId, agentId, workspaceId }) => {
|
restoreArchivedWorkspace: ({ serverId, agentId, workspaceId }) => {
|
||||||
restoreArchivedWorkspace(serverId, agentId, workspaceId);
|
restoreArchivedWorkspace(serverId, agentId, workspaceId);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
type AgentNavTarget,
|
type AgentNavTarget,
|
||||||
type NavigateToAgentDeps,
|
type NavigateToAgentDeps,
|
||||||
} from "@/utils/navigate-to-agent/resolve";
|
} 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 SERVER_ID = "server-1";
|
||||||
const WORKSPACE_ID = "workspace-1";
|
const WORKSPACE_ID = "workspace-1";
|
||||||
@@ -14,7 +14,7 @@ interface RecordedHostNav {
|
|||||||
route: string;
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecordedTabNav extends NavigateToPreparedWorkspaceTabInput {}
|
interface RecordedTabNav extends NavigateToWorkspaceInput {}
|
||||||
|
|
||||||
interface RecordedRestore {
|
interface RecordedRestore {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -40,7 +40,7 @@ function createFakeNavigators(target: AgentNavTarget): {
|
|||||||
navigateToHostAgent: (route) => {
|
navigateToHostAgent: (route) => {
|
||||||
hostNavigations.push({ route });
|
hostNavigations.push({ route });
|
||||||
},
|
},
|
||||||
navigateToPreparedWorkspaceTab: (input) => {
|
navigateToWorkspace: (input) => {
|
||||||
tabNavigations.push(input);
|
tabNavigations.push(input);
|
||||||
return `/h/${input.serverId}/workspace/${input.workspaceId}`;
|
return `/h/${input.serverId}/workspace/${input.workspaceId}`;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
||||||
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
|
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 {
|
export interface NavigateToAgentInput {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -18,7 +18,7 @@ export interface AgentNavTarget {
|
|||||||
export interface NavigateToAgentDeps {
|
export interface NavigateToAgentDeps {
|
||||||
readAgentNavTarget: (input: { serverId: string; agentId: string }) => AgentNavTarget;
|
readAgentNavTarget: (input: { serverId: string; agentId: string }) => AgentNavTarget;
|
||||||
navigateToHostAgent: (route: string) => void;
|
navigateToHostAgent: (route: string) => void;
|
||||||
navigateToPreparedWorkspaceTab: (input: NavigateToPreparedWorkspaceTabInput) => string;
|
navigateToWorkspace: (input: NavigateToWorkspaceInput) => string;
|
||||||
restoreArchivedWorkspace: (input: {
|
restoreArchivedWorkspace: (input: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
@@ -49,7 +49,7 @@ export function resolveNavigateToAgent(
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return deps.navigateToPreparedWorkspaceTab({
|
return deps.navigateToWorkspace({
|
||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
target: { kind: "agent", agentId: input.agentId },
|
target: { kind: "agent", agentId: input.agentId },
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ vi.mock("expo-router", () => ({
|
|||||||
router: { navigate: vi.fn() },
|
router: { navigate: vi.fn() },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/utils/workspace-navigation", () => ({
|
vi.mock("@/stores/navigation-active-workspace-store", () => ({
|
||||||
navigateToPreparedWorkspaceTab: vi.fn(() => ""),
|
navigateToWorkspace: vi.fn(() => ""),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/runtime/host-runtime", () => ({
|
vi.mock("@/runtime/host-runtime", () => ({
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
buildWorkspaceTabPersistenceKey,
|
buildWorkspaceTabPersistenceKey,
|
||||||
type WorkspaceTabTarget,
|
type WorkspaceTabTarget,
|
||||||
} from "@/stores/workspace-tabs-store";
|
} from "@/stores/workspace-tabs-store";
|
||||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
|
||||||
|
|
||||||
export interface PrepareWorkspaceTabInput {
|
export interface PrepareWorkspaceTabInput {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -12,17 +11,11 @@ export interface PrepareWorkspaceTabInput {
|
|||||||
pin?: boolean;
|
pin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NavigateToPreparedWorkspaceTabInput = PrepareWorkspaceTabInput;
|
|
||||||
|
|
||||||
export interface PrepareWorkspaceTabDeps {
|
export interface PrepareWorkspaceTabDeps {
|
||||||
openTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
openTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
||||||
pinAgent: (workspaceKey: string, agentId: string) => void;
|
pinAgent: (workspaceKey: string, agentId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateToPreparedWorkspaceTabDeps extends PrepareWorkspaceTabDeps {
|
|
||||||
navigateToWorkspace: (serverId: string, workspaceId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
|
function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
|
||||||
if (target.kind !== "draft" || target.draftId.trim() !== "new") {
|
if (target.kind !== "draft" || target.draftId.trim() !== "new") {
|
||||||
return target;
|
return target;
|
||||||
@@ -33,7 +26,7 @@ function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
|
|||||||
export function prepareWorkspaceTab(
|
export function prepareWorkspaceTab(
|
||||||
input: PrepareWorkspaceTabInput,
|
input: PrepareWorkspaceTabInput,
|
||||||
deps: PrepareWorkspaceTabDeps,
|
deps: PrepareWorkspaceTabDeps,
|
||||||
): string {
|
): void {
|
||||||
const target = getPreparedTarget(input.target);
|
const target = getPreparedTarget(input.target);
|
||||||
const key =
|
const key =
|
||||||
buildWorkspaceTabPersistenceKey({
|
buildWorkspaceTabPersistenceKey({
|
||||||
@@ -46,15 +39,4 @@ export function prepareWorkspaceTab(
|
|||||||
if (input.pin && target.kind === "agent") {
|
if (input.pin && target.kind === "agent") {
|
||||||
deps.pinAgent(key, target.agentId);
|
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 { describe, expect, it } from "vitest";
|
||||||
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
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 SERVER_ID = "server-1";
|
||||||
const WORKSPACE_ID = "/repo/worktree";
|
const WORKSPACE_ID = "/repo/worktree";
|
||||||
@@ -16,11 +16,6 @@ interface RecordedPin {
|
|||||||
agentId: string;
|
agentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecordedNavigation {
|
|
||||||
serverId: string;
|
|
||||||
workspaceId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFakeLayout() {
|
function createFakeLayout() {
|
||||||
const openedTabs: RecordedOpenedTab[] = [];
|
const openedTabs: RecordedOpenedTab[] = [];
|
||||||
const pinnedAgents: RecordedPin[] = [];
|
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", () => {
|
describe("prepareWorkspaceTab", () => {
|
||||||
it("opens and focuses an agent tab", () => {
|
it("opens and focuses an agent tab", () => {
|
||||||
const layout = createFakeLayout();
|
const layout = createFakeLayout();
|
||||||
|
|
||||||
const route = prepareWorkspaceTab(
|
prepareWorkspaceTab(
|
||||||
{
|
{
|
||||||
serverId: SERVER_ID,
|
serverId: SERVER_ID,
|
||||||
workspaceId: WORKSPACE_ID,
|
workspaceId: WORKSPACE_ID,
|
||||||
@@ -60,30 +45,9 @@ describe("prepareWorkspaceTab", () => {
|
|||||||
layout,
|
layout,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(route).toBe("/h/server-1/workspace/b64_L3JlcG8vd29ya3RyZWU");
|
|
||||||
expect(layout.openedTabs).toEqual([
|
expect(layout.openedTabs).toEqual([
|
||||||
{ key: "server-1:/repo/worktree", target: { kind: "agent", agentId: AGENT_ID } },
|
{ key: "server-1:/repo/worktree", target: { kind: "agent", agentId: AGENT_ID } },
|
||||||
]);
|
]);
|
||||||
expect(layout.pinnedAgents).toEqual([]);
|
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 { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||||
import {
|
import {
|
||||||
prepareWorkspaceTab as prepareWorkspaceTabPure,
|
prepareWorkspaceTab as prepareWorkspaceTabPure,
|
||||||
navigateToPreparedWorkspaceTab as navigateToPreparedWorkspaceTabPure,
|
|
||||||
type PrepareWorkspaceTabInput,
|
type PrepareWorkspaceTabInput,
|
||||||
type NavigateToPreparedWorkspaceTabInput,
|
|
||||||
} from "./prepare-workspace-tab";
|
} from "./prepare-workspace-tab";
|
||||||
|
|
||||||
export type {
|
export type { PrepareWorkspaceTabInput } from "./prepare-workspace-tab";
|
||||||
PrepareWorkspaceTabInput,
|
|
||||||
NavigateToPreparedWorkspaceTabInput,
|
|
||||||
} from "./prepare-workspace-tab";
|
|
||||||
|
|
||||||
function layoutStoreDeps() {
|
function layoutStoreDeps() {
|
||||||
const store = useWorkspaceLayoutStore.getState();
|
const store = useWorkspaceLayoutStore.getState();
|
||||||
@@ -20,13 +14,6 @@ function layoutStoreDeps() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareWorkspaceTab(input: PrepareWorkspaceTabInput): string {
|
export function prepareWorkspaceTab(input: PrepareWorkspaceTabInput): void {
|
||||||
return prepareWorkspaceTabPure(input, layoutStoreDeps());
|
prepareWorkspaceTabPure(input, layoutStoreDeps());
|
||||||
}
|
|
||||||
|
|
||||||
export function navigateToPreparedWorkspaceTab(input: NavigateToPreparedWorkspaceTabInput): string {
|
|
||||||
return navigateToPreparedWorkspaceTabPure(input, {
|
|
||||||
...layoutStoreDeps(),
|
|
||||||
navigateToWorkspace,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user