Fix duplicate workspace shell navigation

This commit is contained in:
Mohamed Boudra
2026-05-05 21:55:57 +07:00
parent 60531b5e1b
commit 02e5b56408
12 changed files with 96 additions and 62 deletions
+5 -5
View File
@@ -19,7 +19,7 @@ import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-over
import { getShortcutOs } from "@/utils/shortcut-platform";
import { getIsElectronRuntime } from "@/constants/layout";
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
import { focusWithRetries } from "@/utils/web-focus";
import { useActiveServerId } from "@/hooks/use-active-server-id";
@@ -123,7 +123,7 @@ function resolveActionShortcutKeys(
}
export function useCommandCenter() {
const _pathname = usePathname();
const pathname = usePathname();
const routeActiveServerId = useActiveServerId();
const { overrides } = useKeyboardShortcutOverrides();
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
@@ -212,14 +212,14 @@ export function useCommandCenter() {
router.navigate(buildHostAgentDetailRoute(agent.serverId, agent.id) as Href);
return;
}
const route = prepareWorkspaceTab({
navigateToPreparedWorkspaceTab({
serverId: agent.serverId,
workspaceId,
target: { kind: "agent", agentId: agent.id },
currentPathname: pathname,
});
router.navigate(route);
},
[setOpen],
[pathname, setOpen],
);
const openProjectPicker = useOpenProjectPicker(activeServerId);
@@ -1,5 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.hoisted(() => {
(globalThis as typeof globalThis & { __DEV__?: boolean }).__DEV__ = false;
});
vi.mock("@react-native-async-storage/async-storage", () => {
const storage = new Map<string, string>();
return {
@@ -19,10 +23,8 @@ const { replaceRoute } = vi.hoisted(() => ({
replaceRoute: vi.fn(),
}));
vi.mock("expo-router", () => ({
router: {
replace: replaceRoute,
},
vi.mock("@/hooks/use-workspace-navigation", () => ({
navigateToWorkspace: replaceRoute,
}));
import { openProjectDirectly } from "@/hooks/use-open-project";
@@ -89,7 +91,7 @@ describe("openProjectDirectly", () => {
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
openDraftTab: createOpenDraftTab(),
replaceRoute,
navigateToWorkspace: replaceRoute,
});
expect(result).toBe(true);
@@ -115,7 +117,7 @@ describe("openProjectDirectly", () => {
const tabs = collectAllTabs(layout.root);
expect(tabs).toHaveLength(1);
expect(tabs[0]?.target.kind).toBe("draft");
expect(replaceRoute).toHaveBeenCalledWith("/h/server-1/workspace/1");
expect(replaceRoute).toHaveBeenCalledWith("server-1", "1");
});
it("does not navigate or seed tabs when openProject fails", async () => {
@@ -133,7 +135,7 @@ describe("openProjectDirectly", () => {
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
openDraftTab: createOpenDraftTab(),
replaceRoute,
navigateToWorkspace: replaceRoute,
});
expect(result).toBe(false);
+4 -7
View File
@@ -1,5 +1,4 @@
import { useCallback } from "react";
import { router, type Href } from "expo-router";
import type { DaemonClient } from "@server/client/daemon-client";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import {
@@ -12,7 +11,7 @@ import {
useWorkspaceLayoutStore,
} from "@/stores/workspace-layout-store";
import { generateDraftId } from "@/stores/draft-keys";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
interface OpenProjectDirectlyInput {
serverId: string;
@@ -22,7 +21,7 @@ interface OpenProjectDirectlyInput {
mergeWorkspaces: (serverId: string, workspaces: Iterable<WorkspaceDescriptor>) => void;
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
openDraftTab: (workspaceKey: string) => string | null;
replaceRoute: (route: string) => void;
navigateToWorkspace: (serverId: string, workspaceId: string) => void;
}
export async function openProjectDirectly(input: OpenProjectDirectlyInput): Promise<boolean> {
@@ -50,7 +49,7 @@ export async function openProjectDirectly(input: OpenProjectDirectlyInput): Prom
}
input.openDraftTab(workspaceKey);
input.replaceRoute(buildHostWorkspaceRoute(normalizedServerId, workspace.id));
input.navigateToWorkspace(normalizedServerId, workspace.id);
return true;
}
@@ -75,9 +74,7 @@ export function useOpenProject(serverId: string | null): (path: string) => Promi
kind: "draft",
draftId: generateDraftId(),
}),
replaceRoute: (route) => {
router.replace(route as Href);
},
navigateToWorkspace,
});
},
[client, isConnected, mergeWorkspaces, normalizedServerId, setHasHydratedWorkspaces],
@@ -1,12 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { navigateMock } = vi.hoisted(() => ({
navigateMock: vi.fn(),
const { dismissToMock } = vi.hoisted(() => ({
dismissToMock: vi.fn(),
}));
vi.mock("expo-router", () => ({
router: {
navigate: navigateMock,
dismissTo: dismissToMock,
},
}));
@@ -19,11 +19,11 @@ import {
describe("navigateToWorkspace", () => {
beforeEach(() => {
navigateMock.mockReset();
dismissToMock.mockReset();
syncNavigationActiveWorkspace({ current: null });
});
it("uses router navigation from a non-workspace route even when active selection is stale", () => {
it("dismisses to the workspace route from a non-workspace route even when active selection is stale", () => {
activateNavigationWorkspaceSelection({
serverId: "server-1",
workspaceId: "workspace-a",
@@ -33,7 +33,7 @@ describe("navigateToWorkspace", () => {
currentPathname: "/h/server-1/sessions",
});
expect(navigateMock).toHaveBeenCalledWith("/h/server-1/workspace/workspace-b");
expect(dismissToMock).toHaveBeenCalledWith("/h/server-1/workspace/workspace-b");
expect(getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-a",
@@ -50,7 +50,7 @@ describe("navigateToWorkspace", () => {
currentPathname: "/h/server-1/workspace/workspace-a",
});
expect(navigateMock).not.toHaveBeenCalled();
expect(dismissToMock).not.toHaveBeenCalled();
expect(getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-b",
@@ -19,7 +19,7 @@ function shouldUseRetainedWorkspaceSwitch(input: {
}
if (input.currentPathname == null) {
return true;
return false;
}
return parseHostWorkspaceRouteFromPathname(input.currentPathname) !== null;
@@ -49,7 +49,7 @@ export function navigateToWorkspace(
}
const href = buildHostWorkspaceRoute(serverId, workspaceId);
router.navigate(href);
router.dismissTo(href);
}
export function useWorkspaceNavigation() {