Fix cold workspace URL sidebar navigation

Directly opening a workspace URL created a different React Navigation state shape than entering from /. Expo Router parsed the URL as a workspace layout route with a nested index child whose route.path and params pointed at the original workspace. Later sidebar navigation used router.dismissTo for another workspace. React Navigation's POP_TO reused the existing workspace route by name and updated the parent params, but preserved the nested child state. The result was a hybrid state: parent workspace params for B, child index path for A. Expo Router serialized the focused child path, so the address bar stayed on A and sidebar clicks looked like no-ops.

The fix is to stop using workspace/[workspaceId]/_layout.tsx as the actual screen. That layout did not render a Slot, so it only added an extra navigation level for Expo Router to preserve. Move the workspace UI, bootstrap boundary, open-intent consumption, and retained WorkspaceDeck into the leaf index route, making /h/:serverId/workspace/:workspaceId one real rendered route.

Keep workspace switches on navigateToWorkspace/router.dismissTo. This is not another replace workaround: replace previously created duplicate mounted workspace shells. The route-shape fix removes the stale nested child state that made dismissTo fail after cold deep links.

Also route archive/new-agent and notification flows through the centralized workspace navigation path so they do not bypass the same route contract.

Regression coverage: cold-load workspace A, click workspace B in the sidebar, and assert the URL changes to B. Verified with workspace-navigation-regression.spec.ts, settings-navigation.spec.ts, npm run typecheck, and npm run lint.
This commit is contained in:
Mohamed Boudra
2026-05-06 15:13:53 +07:00
parent 868479c493
commit 5e5fc97798
13 changed files with 342 additions and 296 deletions
@@ -9,7 +9,6 @@ import {
type ReactNode,
type RefObject,
} from "react";
import { useRouter } from "expo-router";
import { DiffStat } from "@/components/diff-stat";
import {
View,
@@ -87,7 +86,8 @@ import { GitHubIcon } from "@/components/icons/github-icon";
import { buildGitActions, type GitActions } from "@/components/git-actions-policy";
import { lineNumberGutterWidth } from "@/components/code-insets";
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
import { resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
import { openExternalUrl } from "@/utils/open-external-url";
import { GitActionsSplitButton } from "@/components/git-actions-split-button";
import { usePanelStore } from "@/stores/panel-store";
@@ -1874,7 +1874,6 @@ export function GitDiffPane({
const isMobile = useIsCompactFormFactor();
const showDesktopWebScrollbar = isWeb && !isMobile;
const canUseSplitLayout = isWeb && !isMobile;
const router = useRouter();
const [diffModeOverride, setDiffModeOverride] = useState<ReviewDraftMode | null>(null);
const [postShipArchiveSuggested, setPostShipArchiveSuggested] = useState(false);
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge");
@@ -2360,9 +2359,9 @@ export function GitDiffPane({
const handleArchiveSuccess = useCallback(
(targetWorkingDir: string) => {
router.replace(buildNewAgentRoute(serverId, targetWorkingDir));
navigateToWorkspace(serverId, targetWorkingDir);
},
[router, serverId],
[serverId],
);
const toastError = useCallback(