From a042fbbe435d336b0480fcc92c5a838dbbe15435 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 21 Mar 2026 13:01:23 +0700 Subject: [PATCH] Keep explicitly reopened archived agent tabs open --- .../screens/workspace/workspace-screen.tsx | 98 ++++++++++++++++--- .../src/stores/workspace-layout-store.test.ts | 42 +++++++- .../app/src/stores/workspace-layout-store.ts | 64 ++++++++++++ 3 files changed, 188 insertions(+), 16 deletions(-) diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index df1ef8540..f747d62a3 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -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(); 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()); const pendingCloseTabIdsRef = useRef(new Set()); @@ -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, diff --git a/packages/app/src/stores/workspace-layout-store.test.ts b/packages/app/src/stores/workspace-layout-store.test.ts index dc30504d4..858949f3c 100644 --- a/packages/app/src/stores/workspace-layout-store.test.ts +++ b/packages/app/src/stores/workspace-layout-store.test.ts @@ -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: {}, + }); + }); }); diff --git a/packages/app/src/stores/workspace-layout-store.ts b/packages/app/src/stores/workspace-layout-store.ts index 3f2524dec..ec1d950d7 100644 --- a/packages/app/src/stores/workspace-layout-store.ts +++ b/packages/app/src/stores/workspace-layout-store.ts @@ -53,6 +53,7 @@ export type { SplitGroup, SplitNode, SplitPane, WorkspaceLayout }; interface WorkspaceLayoutStore { layoutByWorkspace: Record; splitSizesByWorkspace: Record>; + pinnedAgentIdsByWorkspace: Record>; 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()( (set, get) => ({ layoutByWorkspace: {}, splitSizesByWorkspace: {}, + pinnedAgentIdsByWorkspace: {}, openTab: (workspaceKey, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTarget = normalizeWorkspaceTabTarget(target); @@ -385,6 +389,66 @@ export const useWorkspaceLayoutStore = create()( }; }); }, + 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",