Fix archived workspace session routing\n\nCloses #128

This commit is contained in:
Mohamed Boudra
2026-03-20 12:17:50 +07:00
parent c9f2e01131
commit 26c07b671f
7 changed files with 152 additions and 29 deletions
+1 -6
View File
@@ -608,12 +608,7 @@ export default function RootLayout() {
>
<Stack.Screen name="index" />
<Stack.Screen name="settings" />
<Stack.Screen
name="h/[serverId]/workspace/[workspaceId]"
getId={({ params }) =>
`${params?.serverId}:${params?.workspaceId}`
}
/>
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
<Stack.Screen
name="h/[serverId]/agent/[agentId]"
options={{ gestureEnabled: false }}
@@ -1,4 +1,4 @@
import { useLocalSearchParams } from 'expo-router'
import { useGlobalSearchParams, useLocalSearchParams } from 'expo-router'
import { WorkspaceScreen } from '@/screens/workspace/workspace-screen'
import {
decodeWorkspaceIdFromPathSegment,
@@ -9,6 +9,8 @@ export default function HostWorkspaceLayout() {
const params = useLocalSearchParams<{
serverId?: string | string[]
workspaceId?: string | string[]
}>()
const globalParams = useGlobalSearchParams<{
open?: string | string[]
}>()
const serverValue = Array.isArray(params.serverId) ? params.serverId[0] : params.serverId
@@ -17,7 +19,7 @@ export default function HostWorkspaceLayout() {
: params.workspaceId
const serverId = serverValue?.trim() ?? ''
const workspaceId = workspaceValue ? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? '') : ''
const openValue = Array.isArray(params.open) ? params.open[0] : params.open
const openValue = Array.isArray(globalParams.open) ? globalParams.open[0] : globalParams.open
const openIntent = parseWorkspaceOpenIntent(openValue)
return (
+3 -6
View File
@@ -9,7 +9,7 @@ import {
} from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useCallback, useMemo, useState, type ReactElement } from 'react'
import { router, usePathname, type Href } from 'expo-router'
import { router, type Href } from 'expo-router'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { formatTimeAgo } from '@/utils/time'
import { shortenPath } from '@/utils/shorten-path'
@@ -250,7 +250,6 @@ export function AgentList({
showAttentionIndicator = true,
}: AgentListProps) {
const { theme } = useUnistyles()
const pathname = usePathname()
const insets = useSafeAreaInsets()
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null)
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
@@ -270,15 +269,13 @@ export function AgentList({
const serverId = agent.serverId
const agentId = agent.id
const shouldReplace = pathname.startsWith('/h/')
const navigate = shouldReplace ? router.replace : router.push
onAgentSelect?.()
const route: Href = buildHostWorkspaceAgentRoute(serverId, agent.cwd, agentId) as Href
navigate(route)
router.navigate(route)
},
[isActionSheetVisible, pathname, onAgentSelect]
[isActionSheetVisible, onAgentSelect]
)
const handleAgentLongPress = useCallback((agent: AggregatedAgent) => {
+2 -5
View File
@@ -14,7 +14,6 @@ import {
import {
buildHostWorkspaceAgentRoute,
buildHostSettingsRoute,
parseHostAgentRouteFromPathname,
parseServerIdFromPathname,
} from "@/utils/host-routes";
import type { ShortcutKey } from "@/utils/format-shortcut";
@@ -196,8 +195,6 @@ export function useCommandCenter() {
const handleSelectAgent = useCallback(
(agent: AggregatedAgent) => {
didNavigateRef.current = true;
const shouldReplace = Boolean(parseHostAgentRouteFromPathname(pathname));
const navigate = shouldReplace ? router.replace : router.push;
// Don't restore focus back to the prior element after we navigate.
clearCommandCenterFocusRestoreElement();
@@ -207,9 +204,9 @@ export function useCommandCenter() {
agent.cwd,
agent.id
) as Href;
navigate(route);
router.navigate(route);
},
[pathname, setOpen]
[setOpen]
);
const openProjectPicker = useOpenProjectPicker(activeServerId);
@@ -3,6 +3,7 @@ import type { Agent } from "@/stores/session-store";
import {
canOpenAgentTabFromRoute,
deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab,
} from "@/screens/workspace/workspace-agent-visibility";
function makeAgent(input: {
@@ -104,6 +105,33 @@ describe("workspace agent visibility", () => {
).toBe(true);
});
it("does not prune archived agent tabs when the workspace lookup still contains the agent", () => {
const archivedAgent = makeAgent({
id: "archived-agent",
cwd: "/repo/worktree",
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
});
const lookup = new Map<string, Agent>([[archivedAgent.id, archivedAgent]]);
expect(
shouldPruneWorkspaceAgentTab({
agentId: "archived-agent",
agentsHydrated: true,
workspaceAgentLookup: lookup,
})
).toBe(false);
});
it("prunes agent tabs once agents are hydrated and the agent is missing from the workspace lookup", () => {
expect(
shouldPruneWorkspaceAgentTab({
agentId: "missing-agent",
agentsHydrated: true,
workspaceAgentLookup: new Map<string, Agent>(),
})
).toBe(true);
});
it("sorts same-createdAt agents by lastActivityAt descending", () => {
const workspaceId = "/repo/worktree";
const createdAt = new Date("2026-03-04T00:00:00.000Z");
@@ -72,3 +72,17 @@ export function canOpenAgentTabFromRoute(input: {
}
return input.workspaceAgentLookup.has(input.agentId);
}
export function shouldPruneWorkspaceAgentTab(input: {
agentId: string;
agentsHydrated: boolean;
workspaceAgentLookup: Map<string, Agent>;
}): boolean {
if (!input.agentId.trim()) {
return false;
}
if (!input.agentsHydrated) {
return false;
}
return !input.workspaceAgentLookup.has(input.agentId);
}
@@ -95,8 +95,12 @@ import {
} from "@/screens/workspace/workspace-header-source";
import {
deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab,
} from "@/screens/workspace/workspace-agent-visibility";
import { deriveWorkspacePaneState } from "@/screens/workspace/workspace-pane-state";
import {
deriveWorkspacePaneState,
type WorkspaceDerivedTab,
} from "@/screens/workspace/workspace-pane-state";
import {
buildWorkspacePaneContentModel,
WorkspacePaneContent,
@@ -148,6 +152,34 @@ function buildOpenIntentKey(input: {
return `${input.serverId}:${input.workspaceId}:${openParam}`;
}
function openIntentMatchesActiveTab(input: {
openIntent?: WorkspaceOpenIntent | null;
activeTab: WorkspaceDerivedTab | null;
}): boolean {
const { openIntent, activeTab } = input;
if (!openIntent || !activeTab) {
return false;
}
const target = activeTab.descriptor.target;
if (openIntent.kind !== target.kind) {
return false;
}
if (openIntent.kind === "agent" && target.kind === "agent") {
return target.agentId === openIntent.agentId;
}
if (openIntent.kind === "terminal" && target.kind === "terminal") {
return target.terminalId === openIntent.terminalId;
}
if (openIntent.kind === "file" && target.kind === "file") {
return target.path === openIntent.path;
}
if (openIntent.kind === "draft" && target.kind === "draft") {
return openIntent.draftId === "new" || target.draftId === openIntent.draftId;
}
return false;
}
function getFallbackTabOptionLabel(tab: WorkspaceTabDescriptor): string {
if (tab.target.kind === "draft") {
return "New Agent";
@@ -796,6 +828,7 @@ function WorkspaceScreenContent({
const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane);
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
const consumedOpenIntentsRef = useRef(new Set<string>());
const reopenedArchivedOpenIntentsRef = useRef(new Set<string>());
const pendingCloseTabIdsRef = useRef(new Set<string>());
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
const currentOpenIntentKey = useMemo(
@@ -881,6 +914,43 @@ function WorkspaceScreenContent({
}
}, [currentOpenIntentKey, resolvedOpenIntentKey]);
useEffect(() => {
if (!client || !isConnected || openIntent?.kind !== "agent" || !currentOpenIntentKey) {
return;
}
const agentId = openIntent.agentId.trim();
if (!agentId) {
return;
}
if (reopenedArchivedOpenIntentsRef.current.has(currentOpenIntentKey)) {
return;
}
const agent = agentsById.get(agentId) ?? null;
if (!agent?.archivedAt) {
return;
}
reopenedArchivedOpenIntentsRef.current.add(currentOpenIntentKey);
void client.refreshAgent(agentId).catch((error) => {
console.warn("[WorkspaceScreen] Failed to reopen archived agent", {
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
agentId,
error,
});
});
}, [
agentsById,
client,
currentOpenIntentKey,
isConnected,
normalizedServerId,
normalizedWorkspaceId,
openIntent,
]);
useEffect(() => {
if (!openIntent || !persistenceKey) {
return;
@@ -904,9 +974,6 @@ function WorkspaceScreenContent({
...(draftId === "new" ? {} : { draftId }),
focus: true,
});
if (tabId) {
setResolvedOpenIntentKey(intentKey);
}
return;
}
@@ -918,15 +985,15 @@ function WorkspaceScreenContent({
: { kind: "file", path: openIntent.path };
const tabId = openWorkspaceTab(persistenceKey, target);
if (tabId) {
setResolvedOpenIntentKey(intentKey);
focusWorkspaceTab(persistenceKey, tabId);
}
}, [
currentOpenIntentKey,
focusWorkspaceTab,
openIntent,
openWorkspaceDraftTab,
openWorkspaceTab,
persistenceKey,
resolvedOpenIntentKey,
]);
const unresolvedOpenIntent = currentOpenIntentKey && resolvedOpenIntentKey !== currentOpenIntentKey
@@ -956,7 +1023,6 @@ function WorkspaceScreenContent({
return;
}
const agentIds = new Set(workspaceAgents.map((agent) => agent.id));
const terminalIds = new Set(terminals.map((terminal) => terminal.id));
const hasActivePendingDraftCreateInWorkspace = uiTabs.some((tab) => {
if (tab.target.kind !== "draft") {
@@ -989,7 +1055,15 @@ function WorkspaceScreenContent({
const canPruneAgentTabs = hasHydratedAgents;
const canPruneTerminalTabs = terminalsQuery.isSuccess;
for (const tab of uiTabs) {
if (canPruneAgentTabs && tab.target.kind === "agent" && !agentIds.has(tab.target.agentId)) {
if (
canPruneAgentTabs &&
tab.target.kind === "agent" &&
shouldPruneWorkspaceAgentTab({
agentId: tab.target.agentId,
agentsHydrated: hasHydratedAgents,
workspaceAgentLookup: agentsById,
})
) {
closeWorkspaceTab(persistenceKey, tab.tabId);
}
if (
@@ -1013,6 +1087,24 @@ function WorkspaceScreenContent({
]);
const activeTabId = resolvedPaneTabState.activeTabId;
const activeTab = resolvedPaneTabState.activeTab;
useEffect(() => {
if (!openIntent || !currentOpenIntentKey) {
return;
}
if (resolvedOpenIntentKey === currentOpenIntentKey) {
return;
}
if (
openIntentMatchesActiveTab({
openIntent,
activeTab,
})
) {
setResolvedOpenIntentKey(currentOpenIntentKey);
}
}, [activeTab, currentOpenIntentKey, openIntent, resolvedOpenIntentKey]);
useEffect(() => {
if (!activeTabId || !persistenceKey) {
@@ -1021,8 +1113,6 @@ function WorkspaceScreenContent({
focusWorkspaceTab(persistenceKey, activeTabId);
}, [activeTabId, focusWorkspaceTab, persistenceKey]);
const activeTab = resolvedPaneTabState.activeTab;
const tabs = useMemo<WorkspaceTabDescriptor[]>(
() => resolvedPaneTabState.tabs.map((tab) => tab.descriptor),
[resolvedPaneTabState.tabs]