fix: lock workspace navigation tab sync

This commit is contained in:
Mohamed Boudra
2026-04-14 15:35:16 +07:00
parent 1e0f62976d
commit d0be7c3ee7
8 changed files with 291 additions and 67 deletions
+11 -1
View File
@@ -811,7 +811,17 @@ function RootStack() {
getId={({ params }) => {
const serverId = getRouteParamValue(params?.serverId);
const workspaceId = getRouteParamValue(params?.workspaceId);
return serverId && workspaceId ? `${serverId}:${workspaceId}` : undefined;
const openIntent = getRouteParamValue(
params && typeof params === "object" && "open" in params
? (params as { open?: string | string[] }).open
: undefined,
);
if (!serverId || !workspaceId) {
return undefined;
}
return openIntent
? `${serverId}:${workspaceId}:open:${openIntent}`
: `${serverId}:${workspaceId}`;
}}
/>
<Stack.Screen name="h/[serverId]/agent/[agentId]" options={{ gestureEnabled: false }} />
@@ -1,14 +1,17 @@
import { useEffect, useRef, useState } from "react";
import { useIsFocused, useNavigation } from "@react-navigation/native";
import {
useGlobalSearchParams,
useLocalSearchParams,
usePathname,
useRouter,
useRootNavigationState,
} from "expo-router";
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
import {
buildHostWorkspaceRoute,
decodeWorkspaceIdFromPathSegment,
parseHostWorkspaceRouteFromPathname,
parseWorkspaceOpenIntent,
@@ -44,6 +47,29 @@ function getOpenIntentTarget(openIntent: WorkspaceOpenIntent): WorkspaceTabTarge
return { kind: "draft", draftId: openIntent.draftId };
}
function stripOpenSearchParamFromBrowserUrl() {
if (!isWeb || typeof window === "undefined") {
return;
}
const url = new URL(window.location.href);
if (!url.searchParams.has("open")) {
return;
}
url.searchParams.delete("open");
window.history.replaceState(null, "", url.toString());
}
function clearConsumedOpenIntent(input: {
navigation: { setParams: (...args: any[]) => void };
router: { replace: (...args: any[]) => void };
serverId: string;
workspaceId: string;
}) {
input.router.replace(buildHostWorkspaceRoute(input.serverId, input.workspaceId));
input.navigation.setParams({ open: undefined });
stripOpenSearchParamFromBrowserUrl();
}
export default function HostWorkspaceLayout() {
return (
<HostRouteBootstrapBoundary>
@@ -53,6 +79,9 @@ export default function HostWorkspaceLayout() {
}
function HostWorkspaceLayoutContent() {
const isFocused = useIsFocused();
const navigation = useNavigation();
const router = useRouter();
const rootNavigationState = useRootNavigationState();
const consumedIntentRef = useRef<string | null>(null);
const [intentConsumed, setIntentConsumed] = useState(false);
@@ -73,12 +102,22 @@ function HostWorkspaceLayoutContent() {
if (!openValue) {
return;
}
if (!isFocused) {
return;
}
if (!rootNavigationState?.key) {
return;
}
const consumptionKey = `${serverId}:${workspaceId}:${openValue}`;
if (consumedIntentRef.current === consumptionKey) {
clearConsumedOpenIntent({
navigation,
router,
serverId,
workspaceId,
});
setIntentConsumed(true);
return;
}
consumedIntentRef.current = consumptionKey;
@@ -96,16 +135,15 @@ function HostWorkspaceLayoutContent() {
// Expo Router's replace ignores query-param-only changes (findDivergentState
// skips search params). Strip ?open from the browser URL directly so the
// address bar reflects the clean workspace route.
if (isWeb && typeof window !== "undefined") {
const url = new URL(window.location.href);
if (url.searchParams.has("open")) {
url.searchParams.delete("open");
window.history.replaceState(null, "", url.toString());
}
}
clearConsumedOpenIntent({
navigation,
router,
serverId,
workspaceId,
});
setIntentConsumed(true);
}, [openValue, rootNavigationState?.key, serverId, workspaceId]);
}, [isFocused, navigation, openValue, rootNavigationState?.key, router, serverId, workspaceId]);
if (openValue && !intentConsumed) {
return null;
@@ -1009,6 +1009,9 @@ function WorkspaceRowInner({
>
<Pressable
disabled={isArchiving}
aria-selected={selected}
accessibilityRole="button"
accessibilityState={{ selected }}
style={({ pressed }) => [
styles.workspaceRow,
isDragging && styles.workspaceRowDragging,
@@ -99,7 +99,6 @@ import {
} from "@/screens/workspace/workspace-header-source";
import {
deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
} from "@/screens/workspace/workspace-agent-visibility";
import { deriveWorkspacePaneState } from "@/screens/workspace/workspace-pane-state";
@@ -1010,7 +1009,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
return;
}
const terminalIds = new Set(terminals.map((terminal) => terminal.id));
const hasActivePendingDraftCreateInWorkspace = uiTabs.some((tab) => {
if (tab.target.kind !== "draft") {
return false;
@@ -1019,56 +1017,16 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
return pending?.serverId === normalizedServerId && pending.lifecycle === "active";
});
for (const agentId of workspaceAgentVisibility.activeAgentIds) {
if (hiddenAgentIds.has(agentId)) {
continue;
}
const representedByTarget = uiTabs.some(
(tab) => tab.target.kind === "agent" && tab.target.agentId === agentId,
);
const representedByDeterministicTabId = uiTabs.some(
(tab) => tab.tabId === `agent_${agentId}`,
);
if (
hasActivePendingDraftCreateInWorkspace &&
!representedByTarget &&
!representedByDeterministicTabId
) {
continue;
}
ensureWorkspaceTab({ kind: "agent", agentId });
}
for (const terminal of terminals) {
ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id });
}
const canPruneAgentTabs = hasHydratedAgents;
const canPruneTerminalTabs = terminalsQuery.isSuccess;
for (const tab of uiTabs) {
if (
canPruneAgentTabs &&
tab.target.kind === "agent" &&
!pinnedAgentIds.has(tab.target.agentId) &&
shouldPruneWorkspaceAgentTab({
agentId: tab.target.agentId,
agentsHydrated: hasHydratedAgents,
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
activeAgentIds: workspaceAgentVisibility.activeAgentIds,
})
) {
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
}
if (
canPruneTerminalTabs &&
tab.target.kind === "terminal" &&
!terminalIds.has(tab.target.terminalId)
) {
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
}
}
reconcileWorkspaceTabs(persistenceKey, {
agentsHydrated: hasHydratedAgents,
terminalsHydrated: terminalsQuery.isSuccess,
activeAgentIds: Array.from(workspaceAgentVisibility.activeAgentIds),
knownAgentIds: Array.from(workspaceAgentVisibility.knownAgentIds),
standaloneTerminalIds: terminals.map((terminal) => terminal.id),
hasActivePendingDraftCreate: hasActivePendingDraftCreateInWorkspace,
});
}, [
hasHydratedAgents,
hiddenAgentIds,
pendingByDraftId,
persistenceKey,
reconcileWorkspaceTabs,