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.
This commit is contained in:
Mohamed Boudra
2026-07-16 15:10:38 +02:00
parent 81218ba7a4
commit 06592061e2
5 changed files with 57 additions and 28 deletions
+1 -1
View File
@@ -394,7 +394,7 @@ export function AgentList({
serverId,
agentId,
workspaceId: agent.workspaceId,
restoreWorkspace: true,
restoreWorkspace: { agentArchived: Boolean(agent.archivedAt) },
pin: false,
});
},
@@ -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<string, PendingWorkspaceRestore>();
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,
});
}
@@ -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);
});
@@ -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,
});
}
@@ -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();
});