Keep explicitly reopened archived agent tabs open

This commit is contained in:
Mohamed Boudra
2026-03-21 13:01:23 +07:00
parent ee5577c2da
commit a042fbbe43
3 changed files with 188 additions and 16 deletions
@@ -120,6 +120,7 @@ const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__";
const NEW_TAB_TERMINAL_OPTION_ID = "__new_tab_terminal__";
type NewTabOptionId = typeof NEW_TAB_AGENT_OPTION_ID | typeof NEW_TAB_TERMINAL_OPTION_ID;
const EMPTY_UI_TABS: WorkspaceTab[] = [];
const EMPTY_PINNED_AGENT_IDS = new Set<string>();
type WorkspaceScreenProps = {
serverId: string;
@@ -810,6 +811,8 @@ function WorkspaceScreenContent({
const openWorkspaceTab = useWorkspaceLayoutStore((state) => state.openTab);
const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab);
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
const pinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.pinAgent);
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
const retargetWorkspaceTab = useWorkspaceLayoutStore((state) => state.retargetTab);
const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane);
const splitWorkspacePaneEmpty = useWorkspaceLayoutStore((state) => state.splitPaneEmpty);
@@ -817,6 +820,11 @@ function WorkspaceScreenContent({
const focusWorkspacePane = useWorkspaceLayoutStore((state) => state.focusPane);
const resizeWorkspaceSplit = useWorkspaceLayoutStore((state) => state.resizeSplit);
const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane);
const pinnedAgentIds = useWorkspaceLayoutStore((state) =>
persistenceKey
? state.pinnedAgentIdsByWorkspace[persistenceKey] ?? EMPTY_PINNED_AGENT_IDS
: EMPTY_PINNED_AGENT_IDS
);
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
const consumedOpenIntentsRef = useRef(new Set<string>());
const pendingCloseTabIdsRef = useRef(new Set<string>());
@@ -830,6 +838,38 @@ function WorkspaceScreenContent({
}),
[normalizedServerId, normalizedWorkspaceId, openIntent]
);
const currentOpenIntentPinnedAgentId = useMemo(
function getCurrentOpenIntentPinnedAgentId() {
if (!openIntent || openIntent.kind !== "agent" || !hasHydratedAgents) {
return null;
}
if (!workspaceAgentVisibility.knownAgentIds.has(openIntent.agentId)) {
return null;
}
if (workspaceAgentVisibility.activeAgentIds.has(openIntent.agentId)) {
return null;
}
return openIntent.agentId;
},
[hasHydratedAgents, openIntent, workspaceAgentVisibility]
);
const closeWorkspaceTabWithCleanup = useCallback(
function closeWorkspaceTabWithCleanup(input: {
tabId: string;
target?: WorkspaceTabTarget | null;
}) {
const normalizedTabId = trimNonEmpty(input.tabId);
if (!normalizedTabId || !persistenceKey) {
return;
}
if (input.target?.kind === "agent") {
unpinWorkspaceAgent(persistenceKey, input.target.agentId);
}
closeWorkspaceTab(persistenceKey, normalizedTabId);
},
[closeWorkspaceTab, persistenceKey, unpinWorkspaceAgent]
);
const focusedPaneTabState = useMemo(
() =>
@@ -930,6 +970,11 @@ function WorkspaceScreenContent({
if (!currentOpenIntentKey) {
return;
}
if (currentOpenIntentPinnedAgentId) {
pinWorkspaceAgent(persistenceKey, currentOpenIntentPinnedAgentId);
}
const intentKey = currentOpenIntentKey;
if (consumedOpenIntentsRef.current.has(intentKey)) {
if (resolvedOpenIntentKey !== intentKey) {
@@ -959,11 +1004,13 @@ function WorkspaceScreenContent({
focusWorkspaceTab(persistenceKey, tabId);
}
}, [
currentOpenIntentPinnedAgentId,
currentOpenIntentKey,
focusWorkspaceTab,
openIntent,
openWorkspaceDraftTab,
openWorkspaceTab,
pinWorkspaceAgent,
persistenceKey,
]);
@@ -1029,27 +1076,32 @@ function WorkspaceScreenContent({
if (
canPruneAgentTabs &&
tab.target.kind === "agent" &&
!pinnedAgentIds.has(tab.target.agentId) &&
tab.target.agentId !== currentOpenIntentPinnedAgentId &&
shouldPruneWorkspaceAgentTab({
agentId: tab.target.agentId,
agentsHydrated: hasHydratedAgents,
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
activeAgentIds: workspaceAgentVisibility.activeAgentIds,
})
) {
closeWorkspaceTab(persistenceKey, tab.tabId);
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
}
if (
canPruneTerminalTabs &&
tab.target.kind === "terminal" &&
!terminalIds.has(tab.target.terminalId)
) {
closeWorkspaceTab(persistenceKey, tab.tabId);
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
}
}
}, [
closeWorkspaceTab,
closeWorkspaceTabWithCleanup,
currentOpenIntentPinnedAgentId,
ensureWorkspaceTab,
hasHydratedAgents,
pendingByDraftId,
pinnedAgentIds,
persistenceKey,
terminals,
terminalsQuery.isSuccess,
@@ -1311,13 +1363,16 @@ function WorkspaceScreenContent({
);
if (persistenceKey) {
closeWorkspaceTab(persistenceKey, tabId);
closeWorkspaceTabWithCleanup({
tabId,
target: { kind: "terminal", terminalId },
});
}
},
});
},
[
closeWorkspaceTab,
closeWorkspaceTabWithCleanup,
killTerminalMutation,
persistenceKey,
queryClient,
@@ -1354,14 +1409,17 @@ function WorkspaceScreenContent({
setHoveredTabKey((current) => (current === tabId ? null : current));
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
if (persistenceKey) {
closeWorkspaceTab(persistenceKey, tabId);
closeWorkspaceTabWithCleanup({
tabId,
target: { kind: "agent", agentId },
});
}
},
});
},
[
archiveAgent,
closeWorkspaceTab,
closeWorkspaceTabWithCleanup,
isArchivingAgent,
normalizedServerId,
persistenceKey,
@@ -1374,10 +1432,10 @@ function WorkspaceScreenContent({
setHoveredTabKey((current) => (current === tabId ? null : current));
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
if (persistenceKey) {
closeWorkspaceTab(persistenceKey, tabId);
closeWorkspaceTabWithCleanup({ tabId });
}
},
[closeWorkspaceTab, persistenceKey]
[closeWorkspaceTabWithCleanup, persistenceKey]
);
const handleCloseTabById = useCallback(
@@ -1503,7 +1561,10 @@ function WorkspaceScreenContent({
};
});
if (persistenceKey) {
closeWorkspaceTab(persistenceKey, tabId);
closeWorkspaceTabWithCleanup({
tabId,
target: { kind: "terminal", terminalId },
});
}
} catch (error) {
console.warn(`[WorkspaceScreen] Failed to close terminal tab ${logLabel}`, { terminalId, error });
@@ -1517,7 +1578,10 @@ function WorkspaceScreenContent({
try {
await archiveAgent({ serverId: normalizedServerId, agentId });
if (persistenceKey) {
closeWorkspaceTab(persistenceKey, tabId);
closeWorkspaceTabWithCleanup({
tabId,
target: { kind: "agent", agentId },
});
}
} catch (error) {
console.warn(`[WorkspaceScreen] Failed to archive agent tab ${logLabel}`, { agentId, error });
@@ -1526,7 +1590,7 @@ function WorkspaceScreenContent({
for (const { tabId } of groups.otherTabs) {
if (persistenceKey) {
closeWorkspaceTab(persistenceKey, tabId);
closeWorkspaceTabWithCleanup({ tabId });
}
}
@@ -1536,7 +1600,7 @@ function WorkspaceScreenContent({
},
[
archiveAgent,
closeWorkspaceTab,
closeWorkspaceTabWithCleanup,
killTerminalMutation,
normalizedServerId,
persistenceKey,
@@ -1717,7 +1781,10 @@ function WorkspaceScreenContent({
if (action.id === "workspace.pane.close") {
for (const tabId of focusedPane.tabIds) {
closeWorkspaceTab(persistenceKey, tabId);
closeWorkspaceTabWithCleanup({
tabId,
target: allTabDescriptorsById.get(tabId)?.target ?? null,
});
}
return true;
}
@@ -1725,7 +1792,8 @@ function WorkspaceScreenContent({
return false;
},
[
closeWorkspaceTab,
allTabDescriptorsById,
closeWorkspaceTabWithCleanup,
focusWorkspacePane,
handleCreateDraftSplit,
moveWorkspaceTabToPane,
@@ -218,7 +218,11 @@ describe("workspace-layout-store tree transforms", () => {
describe("workspace-layout-store actions", () => {
beforeEach(() => {
useWorkspaceLayoutStore.setState({ layoutByWorkspace: {} });
useWorkspaceLayoutStore.setState({
layoutByWorkspace: {},
splitSizesByWorkspace: {},
pinnedAgentIdsByWorkspace: {},
});
vi.restoreAllMocks();
});
@@ -634,4 +638,40 @@ describe("workspace-layout-store actions", () => {
expect(layout).toEqual(createDefaultLayout());
});
it("keeps pinned archived agents in memory per workspace without persisting them", () => {
const workspaceKey = createWorkspaceKey();
const otherWorkspaceKey = buildWorkspaceTabPersistenceKey({
serverId: SERVER_ID,
workspaceId: "/repo/other-worktree",
});
expect(otherWorkspaceKey).toBeTruthy();
const store = useWorkspaceLayoutStore.getState();
store.pinAgent(workspaceKey, "agent-1");
store.pinAgent(workspaceKey, "agent-1");
store.pinAgent(otherWorkspaceKey as string, "agent-2");
let state = useWorkspaceLayoutStore.getState();
expect(Array.from(state.pinnedAgentIdsByWorkspace[workspaceKey] ?? [])).toEqual(["agent-1"]);
expect(Array.from(state.pinnedAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([
"agent-2",
]);
store.unpinAgent(workspaceKey, "agent-1");
state = useWorkspaceLayoutStore.getState();
expect(state.pinnedAgentIdsByWorkspace[workspaceKey]).toBeUndefined();
expect(Array.from(state.pinnedAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([
"agent-2",
]);
const partialize = useWorkspaceLayoutStore.persist.getOptions().partialize;
expect(partialize).toBeTypeOf("function");
expect(partialize?.(state)).toEqual({
layoutByWorkspace: {},
splitSizesByWorkspace: {},
});
});
});
@@ -53,6 +53,7 @@ export type { SplitGroup, SplitNode, SplitPane, WorkspaceLayout };
interface WorkspaceLayoutStore {
layoutByWorkspace: Record<string, WorkspaceLayout>;
splitSizesByWorkspace: Record<string, Record<string, number[]>>;
pinnedAgentIdsByWorkspace: Record<string, Set<string>>;
openTab: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
closeTab: (workspaceKey: string, tabId: string) => void;
focusTab: (workspaceKey: string, tabId: string) => void;
@@ -78,6 +79,8 @@ interface WorkspaceLayoutStore {
focusPane: (workspaceKey: string, paneId: string) => void;
resizeSplit: (workspaceKey: string, groupId: string, sizes: number[]) => void;
reorderTabsInPane: (workspaceKey: string, paneId: string, tabIds: string[]) => void;
pinAgent: (workspaceKey: string, agentId: string) => void;
unpinAgent: (workspaceKey: string, agentId: string) => void;
}
const MAX_TREE_DEPTH = 4;
@@ -99,6 +102,7 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
(set, get) => ({
layoutByWorkspace: {},
splitSizesByWorkspace: {},
pinnedAgentIdsByWorkspace: {},
openTab: (workspaceKey, target) => {
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
const normalizedTarget = normalizeWorkspaceTabTarget(target);
@@ -385,6 +389,66 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
};
});
},
pinAgent: (workspaceKey, agentId) => {
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
const normalizedAgentId = trimNonEmpty(agentId);
if (!normalizedWorkspaceKey || !normalizedAgentId) {
return;
}
set((state) => {
const currentPinnedAgentIds =
state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null;
if (currentPinnedAgentIds?.has(normalizedAgentId)) {
return state;
}
const nextPinnedAgentIds = new Set(currentPinnedAgentIds ?? []);
nextPinnedAgentIds.add(normalizedAgentId);
return {
pinnedAgentIdsByWorkspace: {
...state.pinnedAgentIdsByWorkspace,
[normalizedWorkspaceKey]: nextPinnedAgentIds,
},
};
});
},
unpinAgent: (workspaceKey, agentId) => {
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
const normalizedAgentId = trimNonEmpty(agentId);
if (!normalizedWorkspaceKey || !normalizedAgentId) {
return;
}
set((state) => {
const currentPinnedAgentIds =
state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null;
if (!currentPinnedAgentIds?.has(normalizedAgentId)) {
return state;
}
if (currentPinnedAgentIds.size === 1) {
const nextPinnedAgentIdsByWorkspace = {
...state.pinnedAgentIdsByWorkspace,
};
delete nextPinnedAgentIdsByWorkspace[normalizedWorkspaceKey];
return {
pinnedAgentIdsByWorkspace: nextPinnedAgentIdsByWorkspace,
};
}
const nextPinnedAgentIds = new Set(currentPinnedAgentIds);
nextPinnedAgentIds.delete(normalizedAgentId);
return {
pinnedAgentIdsByWorkspace: {
...state.pinnedAgentIdsByWorkspace,
[normalizedWorkspaceKey]: nextPinnedAgentIds,
},
};
});
},
}),
{
name: "workspace-layout-state",