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.
38 lines
1.1 KiB
TypeScript
38 lines
1.1 KiB
TypeScript
import type { Href } from "expo-router";
|
|
import { buildHostAgentDetailRoute, buildHostRootRoute } from "@/utils/host-routes";
|
|
|
|
type NotificationData = Record<string, unknown> | null | undefined;
|
|
type NotificationRoute = Extract<Href, string>;
|
|
|
|
function readNonEmptyString(data: NotificationData, key: string): string | null {
|
|
const value = data?.[key];
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : null;
|
|
}
|
|
|
|
export function resolveNotificationTarget(data: NotificationData): {
|
|
serverId: string | null;
|
|
agentId: string | null;
|
|
workspaceId: string | null;
|
|
} {
|
|
return {
|
|
serverId: readNonEmptyString(data, "serverId"),
|
|
agentId: readNonEmptyString(data, "agentId"),
|
|
workspaceId: readNonEmptyString(data, "workspaceId"),
|
|
};
|
|
}
|
|
|
|
export function buildNotificationRoute(data: NotificationData): NotificationRoute {
|
|
const { serverId, agentId } = resolveNotificationTarget(data);
|
|
if (serverId && agentId) {
|
|
return buildHostAgentDetailRoute(serverId, agentId);
|
|
}
|
|
if (serverId) {
|
|
return buildHostRootRoute(serverId);
|
|
}
|
|
return "/" as const;
|
|
}
|