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(); });