Restore archived agents from History (#2316)

* fix(app): restore archived agents from History

History navigation lost the explicit archived-agent intent during tab reconciliation, and workspace recovery stopped after restoring the workspace. Preserve the selected tab and recover its provider session as one action while serializing provider resume with timeline hydration.

* fix(app): preserve archived agent recovery invariants

* fix(server): preserve timeline hydration call shape

* fix(app): retain agent tabs through lookup

* fix(server): upgrade in-flight timeline broadcast
This commit is contained in:
Mohamed Boudra
2026-07-22 15:03:09 +02:00
committed by GitHub
parent 68993b7ab3
commit 42ee5a5949
22 changed files with 629 additions and 61 deletions
@@ -194,6 +194,7 @@ interface ReorderPaneTabsInLayoutInput {
export interface WorkspaceTabReconcileState {
layout: WorkspaceLayout;
pinnedAgentIds?: ReadonlySet<string> | null;
pendingAgentIds?: ReadonlySet<string> | null;
hiddenAgentIds?: ReadonlySet<string> | null;
}
@@ -1653,13 +1654,14 @@ interface EntityTabGroup {
function applyPinnedAndHidden(input: {
baseAgentIds: Set<string>;
pinnedAgentIds: Set<string>;
pendingAgentIds: Set<string>;
hiddenAgentIds: Set<string>;
knownAgentIds: Set<string>;
}): Set<string> {
const { baseAgentIds, pinnedAgentIds, hiddenAgentIds, knownAgentIds } = input;
const { baseAgentIds, pinnedAgentIds, pendingAgentIds, hiddenAgentIds, knownAgentIds } = input;
const result = new Set(baseAgentIds);
for (const agentId of pinnedAgentIds) {
if (knownAgentIds.has(agentId)) {
if (knownAgentIds.has(agentId) || pendingAgentIds.has(agentId)) {
result.add(agentId);
}
}
@@ -1784,6 +1786,7 @@ export function reconcileWorkspaceTabs(
findPaneById(nextLayout.root, nextLayout.focusedPaneId)?.focusedTabId ?? null;
let reconciledFocusedTabId = originalFocusedTabId;
const pinnedAgentIds = new Set(state.pinnedAgentIds ?? []);
const pendingAgentIds = new Set(state.pendingAgentIds ?? []);
const hiddenAgentIds = new Set(state.hiddenAgentIds ?? []);
const activeAgentIds = normalizeStringSet(snapshot.activeAgentIds);
const autoOpenAgentIds = normalizeStringSet(snapshot.autoOpenAgentIds);
@@ -1795,12 +1798,14 @@ export function reconcileWorkspaceTabs(
const visibleAgentIds = applyPinnedAndHidden({
baseAgentIds: activeAgentIds,
pinnedAgentIds,
pendingAgentIds,
hiddenAgentIds,
knownAgentIds,
});
const autoOpenSet = applyPinnedAndHidden({
baseAgentIds: autoOpenAgentIds,
pinnedAgentIds,
pendingAgentIds,
hiddenAgentIds,
knownAgentIds,
});
@@ -1591,6 +1591,66 @@ describe("workspace-layout-store actions", () => {
expect(Array.from(state.pinnedAgentIdsByWorkspace[workspaceKey] ?? [])).toEqual(["agent-1"]);
});
it("keeps an explicitly pinned archived agent before its detail is hydrated", () => {
const workspaceKey = createWorkspaceKey();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "archived-agent" });
store.pinAgent(workspaceKey, "archived-agent");
store.reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: [],
autoOpenAgentIds: [],
knownAgentIds: [],
standaloneTerminalIds: [],
});
expect(store.getWorkspaceTabs(workspaceKey).map((tab) => tab.tabId)).toEqual([
"agent_archived-agent",
]);
store.reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: [],
autoOpenAgentIds: [],
knownAgentIds: [],
standaloneTerminalIds: [],
});
expect(store.getWorkspaceTabs(workspaceKey).map((tab) => tab.tabId)).toEqual([
"agent_archived-agent",
]);
store.resolvePendingAgent(workspaceKey, "archived-agent");
store.reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: [],
autoOpenAgentIds: [],
knownAgentIds: [],
standaloneTerminalIds: [],
});
expect(store.getWorkspaceTabs(workspaceKey).map((tab) => tab.tabId)).toEqual([]);
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "archived-agent" });
store.pinAgent(workspaceKey, "archived-agent");
store.reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: [],
autoOpenAgentIds: [],
knownAgentIds: [],
standaloneTerminalIds: [],
});
expect(store.getWorkspaceTabs(workspaceKey).map((tab) => tab.tabId)).toEqual([
"agent_archived-agent",
]);
});
it("retargeting a tab to an agent clears hidden intent", () => {
const workspaceKey = createWorkspaceKey();
const store = workspaceLayoutStore.getState();
@@ -75,6 +75,7 @@ interface WorkspaceLayoutStore {
layoutByWorkspace: Record<string, WorkspaceLayout>;
splitSizesByWorkspace: Record<string, Record<string, number[]>>;
pinnedAgentIdsByWorkspace: Record<string, Set<string>>;
pendingAgentIdsByWorkspace: Record<string, Set<string>>;
hiddenAgentIdsByWorkspace: Record<string, Set<string>>;
focusRestorationByWorkspace: Record<string, WorkspaceFocusRestorationState>;
openTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
@@ -89,6 +90,7 @@ interface WorkspaceLayoutStore {
retargetTab: (workspaceKey: string, tabId: string, target: WorkspaceTabTarget) => string | null;
convertDraftToAgent: (workspaceKey: string, tabId: string, agentId: string) => string | null;
reconcileTabs: (workspaceKey: string, snapshot: WorkspaceTabSnapshot) => void;
resolvePendingAgent: (workspaceKey: string, agentId: string) => void;
reorderTabs: (workspaceKey: string, tabIds: string[]) => void;
getWorkspaceTabs: (workspaceKey: string) => WorkspaceTab[];
splitPane: (
@@ -229,6 +231,7 @@ export function createWorkspaceLayoutStore(
layoutByWorkspace: {},
splitSizesByWorkspace: {},
pinnedAgentIdsByWorkspace: {},
pendingAgentIdsByWorkspace: {},
hiddenAgentIdsByWorkspace: {},
focusRestorationByWorkspace: {},
openTabFocused: (workspaceKey, target) => {
@@ -467,6 +470,7 @@ export function createWorkspaceLayoutStore(
{
layout: currentLayout,
pinnedAgentIds: state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
pendingAgentIds: state.pendingAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
hiddenAgentIds: state.hiddenAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
},
snapshot,
@@ -483,6 +487,25 @@ export function createWorkspaceLayoutStore(
};
});
},
resolvePendingAgent: (workspaceKey, agentId) => {
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
const normalizedAgentId = trimNonEmpty(agentId);
if (!normalizedWorkspaceKey || !normalizedAgentId) {
return;
}
set((state) => {
const pendingAgentIdsByWorkspace = removeAgentIdFromWorkspaceSet(
state.pendingAgentIdsByWorkspace,
normalizedWorkspaceKey,
normalizedAgentId,
);
if (pendingAgentIdsByWorkspace === state.pendingAgentIdsByWorkspace) {
return state;
}
return { pendingAgentIdsByWorkspace };
});
},
reorderTabs: (workspaceKey, tabIds) => {
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
if (!normalizedWorkspaceKey) {
@@ -762,7 +785,12 @@ export function createWorkspaceLayoutStore(
set((state) => {
const currentPinnedAgentIds =
state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null;
if (currentPinnedAgentIds?.has(normalizedAgentId)) {
const currentPendingAgentIds =
state.pendingAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null;
if (
currentPinnedAgentIds?.has(normalizedAgentId) &&
currentPendingAgentIds?.has(normalizedAgentId)
) {
return state;
}
@@ -779,6 +807,11 @@ export function createWorkspaceLayoutStore(
...state.pinnedAgentIdsByWorkspace,
[normalizedWorkspaceKey]: nextPinnedAgentIds,
},
pendingAgentIdsByWorkspace: addAgentIdToWorkspaceSet(
state.pendingAgentIdsByWorkspace,
normalizedWorkspaceKey,
normalizedAgentId,
),
};
});
},
@@ -803,6 +836,11 @@ export function createWorkspaceLayoutStore(
delete nextPinnedAgentIdsByWorkspace[normalizedWorkspaceKey];
return {
pinnedAgentIdsByWorkspace: nextPinnedAgentIdsByWorkspace,
pendingAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet(
state.pendingAgentIdsByWorkspace,
normalizedWorkspaceKey,
normalizedAgentId,
),
};
}
@@ -814,6 +852,11 @@ export function createWorkspaceLayoutStore(
...state.pinnedAgentIdsByWorkspace,
[normalizedWorkspaceKey]: nextPinnedAgentIds,
},
pendingAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet(
state.pendingAgentIdsByWorkspace,
normalizedWorkspaceKey,
normalizedAgentId,
),
};
});
},
@@ -872,6 +915,7 @@ export function createWorkspaceLayoutStore(
normalizedWorkspaceKey in state.layoutByWorkspace ||
normalizedWorkspaceKey in state.splitSizesByWorkspace ||
normalizedWorkspaceKey in state.pinnedAgentIdsByWorkspace ||
normalizedWorkspaceKey in state.pendingAgentIdsByWorkspace ||
normalizedWorkspaceKey in state.hiddenAgentIdsByWorkspace ||
normalizedWorkspaceKey in state.focusRestorationByWorkspace;
if (!hasAny) {
@@ -883,6 +927,8 @@ export function createWorkspaceLayoutStore(
state.splitSizesByWorkspace;
const { [normalizedWorkspaceKey]: _pinned, ...pinnedAgentIdsByWorkspace } =
state.pinnedAgentIdsByWorkspace;
const { [normalizedWorkspaceKey]: _pending, ...pendingAgentIdsByWorkspace } =
state.pendingAgentIdsByWorkspace;
const { [normalizedWorkspaceKey]: _hidden, ...hiddenAgentIdsByWorkspace } =
state.hiddenAgentIdsByWorkspace;
const { [normalizedWorkspaceKey]: _restoration, ...focusRestorationByWorkspace } =
@@ -891,6 +937,7 @@ export function createWorkspaceLayoutStore(
layoutByWorkspace,
splitSizesByWorkspace,
pinnedAgentIdsByWorkspace,
pendingAgentIdsByWorkspace,
hiddenAgentIdsByWorkspace,
focusRestorationByWorkspace,
};