From 06592061e209f2e153c129572e02888514aa54f1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 11 Jul 2026 13:55:09 +0200 Subject: [PATCH] fix(app): reopen archived History agents outside active directory History entries are not merged into the active session maps. Carry the row archive state through explicit restore intent so live workspaces still reopen without broadening ordinary navigation. --- packages/app/src/components/agent-list.tsx | 2 +- .../app/src/utils/navigate-to-agent/index.ts | 34 ++++++++++--------- .../utils/navigate-to-agent/resolve.test.ts | 10 ++++-- .../src/utils/navigate-to-agent/resolve.ts | 8 +++-- .../restore-archived-workspace.test.ts | 31 +++++++++++++---- 5 files changed, 57 insertions(+), 28 deletions(-) diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index 1f25c37f8b..62ac3c4448 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -394,7 +394,7 @@ export function AgentList({ serverId, agentId, workspaceId: agent.workspaceId, - restoreWorkspace: true, + restoreWorkspace: { agentArchived: Boolean(agent.archivedAt) }, pin: false, }); }, diff --git a/packages/app/src/utils/navigate-to-agent/index.ts b/packages/app/src/utils/navigate-to-agent/index.ts index c9a280ee90..b6b16f81d9 100644 --- a/packages/app/src/utils/navigate-to-agent/index.ts +++ b/packages/app/src/utils/navigate-to-agent/index.ts @@ -14,17 +14,21 @@ export type { NavigateToAgentInput } from "./resolve"; const RESTORE_TIMEOUT_MS = 30000; interface PendingWorkspaceRestore { - agentId: string; + target: HistoryWorkspaceRestore; unsubscribe: () => void; } +interface HistoryWorkspaceRestore { + serverId: string; + agentId: string; + workspaceId: string; + agentArchived: boolean; +} + const pendingWorkspaceRestores = new Map(); -function restoreWhenWorkspacesHydrate( - serverId: string, - agentId: string, - workspaceId: string, -): void { +function restoreWhenWorkspacesHydrate(target: HistoryWorkspaceRestore): void { + const { serverId, workspaceId } = target; const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId }); if (!key) { return; @@ -32,12 +36,12 @@ function restoreWhenWorkspacesHydrate( const pending = pendingWorkspaceRestores.get(key); if (pending) { - pending.agentId = agentId; + pending.target = target; return; } const restore: PendingWorkspaceRestore = { - agentId, + target, unsubscribe: () => {}, }; restore.unsubscribe = useSessionStore.subscribe((state) => { @@ -49,13 +53,14 @@ function restoreWhenWorkspacesHydrate( restore.unsubscribe(); pendingWorkspaceRestores.delete(key); if (session) { - restoreArchivedWorkspace(serverId, restore.agentId, workspaceId); + restoreArchivedWorkspace(restore.target); } }); pendingWorkspaceRestores.set(key, restore); } -function restoreArchivedWorkspace(serverId: string, agentId: string, workspaceId: string): void { +function restoreArchivedWorkspace(target: HistoryWorkspaceRestore): void { + const { serverId, agentId, workspaceId, agentArchived } = target; const snapshot = getHostRuntimeStore().getSnapshot(serverId); const client = snapshot?.client ?? null; if (!client || !isHostRuntimeConnected(snapshot)) { @@ -74,7 +79,7 @@ function restoreArchivedWorkspace(serverId: string, agentId: string, workspaceId // its existing loading state, then decide whether restoration is needed once the // daemon's workspace snapshot has landed. if (!session.hasHydratedWorkspaces) { - restoreWhenWorkspacesHydrate(serverId, agentId, workspaceId); + restoreWhenWorkspacesHydrate(target); return; } @@ -82,8 +87,7 @@ function restoreArchivedWorkspace(serverId: string, agentId: string, workspaceId // must restore even when its agent survived unarchived; an archived agent must // still reopen when its workspace survived. if (session.workspaces.has(workspaceId)) { - const agent = session.agents.get(agentId) ?? session.agentDetails.get(agentId); - if (agent?.archivedAt) { + if (agentArchived) { client.refreshAgent(agentId).catch((error) => { console.error("[HistoryRestore] Failed to reopen archived agent", { serverId, @@ -130,8 +134,6 @@ export function navigateToAgent(input: NavigateToAgentInput): string { router.navigate(route as Href); }, navigateToWorkspace, - restoreArchivedWorkspace: ({ serverId, agentId, workspaceId }) => { - restoreArchivedWorkspace(serverId, agentId, workspaceId); - }, + restoreArchivedWorkspace, }); } diff --git a/packages/app/src/utils/navigate-to-agent/resolve.test.ts b/packages/app/src/utils/navigate-to-agent/resolve.test.ts index 4961baa836..be99654cb8 100644 --- a/packages/app/src/utils/navigate-to-agent/resolve.test.ts +++ b/packages/app/src/utils/navigate-to-agent/resolve.test.ts @@ -20,6 +20,7 @@ interface RecordedRestore { serverId: string; agentId: string; workspaceId: string; + agentArchived: boolean; } function createFakeNavigators(target: AgentNavTarget): { @@ -83,14 +84,19 @@ describe("resolveNavigateToAgent", () => { { serverId: SERVER_ID, agentId: AGENT_ID, - restoreWorkspace: true, + restoreWorkspace: { agentArchived: true }, pin: true, }, deps, ); expect(restores).toEqual([ - { serverId: SERVER_ID, agentId: AGENT_ID, workspaceId: WORKSPACE_ID }, + { + serverId: SERVER_ID, + agentId: AGENT_ID, + workspaceId: WORKSPACE_ID, + agentArchived: true, + }, ]); expect(tabNavigations).toHaveLength(1); }); diff --git a/packages/app/src/utils/navigate-to-agent/resolve.ts b/packages/app/src/utils/navigate-to-agent/resolve.ts index 5548506717..efd5799b6c 100644 --- a/packages/app/src/utils/navigate-to-agent/resolve.ts +++ b/packages/app/src/utils/navigate-to-agent/resolve.ts @@ -11,7 +11,9 @@ export interface NavigateToAgentInput { // History can point at an agent whose owning workspace has been archived even // when the agent itself was not. Other navigation paths must not refresh an // agent just because their workspace descriptor is temporarily unavailable. - restoreWorkspace?: boolean; + restoreWorkspace?: { + agentArchived: boolean; + }; pin?: boolean; } @@ -27,6 +29,7 @@ export interface NavigateToAgentDeps { serverId: string; agentId: string; workspaceId: string; + agentArchived: boolean; }) => void; } @@ -45,11 +48,12 @@ export function resolveNavigateToAgent( return route; } - if (input.restoreWorkspace === true) { + if (input.restoreWorkspace) { deps.restoreArchivedWorkspace({ serverId: input.serverId, agentId: input.agentId, workspaceId, + agentArchived: input.restoreWorkspace.agentArchived, }); } diff --git a/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts b/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts index 5b6659a154..64134e8362 100644 --- a/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts +++ b/packages/app/src/utils/navigate-to-agent/restore-archived-workspace.test.ts @@ -116,12 +116,12 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => { useSessionStore.getState().clearSession(SERVER_ID); }); - function openFromHistory(): void { + function openFromHistory(entry = { agentArchived: true }): void { const input = { serverId: SERVER_ID, agentId: AGENT_ID, workspaceId: WORKSPACE_ID, - restoreWorkspace: true, + restoreWorkspace: entry, }; navigateToAgent(input); } @@ -159,7 +159,7 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => { }); refreshAgent.mockImplementation(() => new Promise(() => {})); - openFromHistory(); + openFromHistory({ agentArchived: false }); expect(refreshAgent).toHaveBeenCalledTimes(1); expect(refreshAgent).toHaveBeenCalledWith(AGENT_ID); @@ -191,7 +191,7 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => { }); refreshAgent.mockImplementation(() => new Promise(() => {})); - openFromHistory(); + openFromHistory({ agentArchived: false }); expect(refreshAgent).not.toHaveBeenCalled(); expect(status()).toBeNull(); @@ -213,7 +213,7 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => { }); refreshAgent.mockImplementation(() => new Promise(() => {})); - openFromHistory(); + openFromHistory({ agentArchived: false }); expect(refreshAgent).not.toHaveBeenCalled(); expect(status()).toBeNull(); @@ -229,7 +229,7 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => { connected = false; refreshAgent.mockImplementation(() => new Promise(() => {})); - openFromHistory(); + openFromHistory({ agentArchived: false }); expect(refreshAgent).not.toHaveBeenCalled(); expect(status()).toBeNull(); @@ -280,6 +280,23 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => { expect(status()).toBeNull(); }); + it("reopens an archived History agent omitted from the active session directory", () => { + const store = useSessionStore.getState(); + store.setAgents(SERVER_ID, (prev) => { + const next = new Map(prev); + next.delete(AGENT_ID); + return next; + }); + store.mergeWorkspaces(SERVER_ID, [workspace()]); + refreshAgent.mockImplementation(() => new Promise(() => {})); + + openFromHistory(); + + expect(refreshAgent).toHaveBeenCalledTimes(1); + expect(refreshAgent).toHaveBeenCalledWith(AGENT_ID); + expect(status()).toBeNull(); + }); + it("is a no-op when a non-archived workspace descriptor is already present", () => { const store = useSessionStore.getState(); store.setAgents(SERVER_ID, (prev) => { @@ -290,7 +307,7 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => { refreshAgent.mockImplementation(() => new Promise(() => {})); store.mergeWorkspaces(SERVER_ID, [workspace()]); - openFromHistory(); + openFromHistory({ agentArchived: false }); expect(refreshAgent).not.toHaveBeenCalled(); });