diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index ee8dfb381..f0a0119b1 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -863,6 +863,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab); const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab); const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent); + const hideWorkspaceAgent = useWorkspaceLayoutStore((state) => state.hideAgent); const retargetWorkspaceTab = useWorkspaceLayoutStore((state) => state.retargetTab); const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane); const splitWorkspacePaneEmpty = useWorkspaceLayoutStore((state) => state.splitPaneEmpty); @@ -876,6 +877,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) ? (state.pinnedAgentIdsByWorkspace[persistenceKey] ?? EMPTY_PINNED_AGENT_IDS) : EMPTY_PINNED_AGENT_IDS, ); + const hiddenAgentIds = useWorkspaceLayoutStore((state) => + persistenceKey ? (state.hiddenAgentIdsByWorkspace[persistenceKey] ?? EMPTY_SET) : EMPTY_SET, + ); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); const { closingTabIds, closeTab } = useCloseTabs(); const closeWorkspaceTabWithCleanup = useCallback( @@ -890,10 +894,11 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) if (input.target?.kind === "agent") { unpinWorkspaceAgent(persistenceKey, input.target.agentId); + hideWorkspaceAgent(persistenceKey, input.target.agentId); } closeWorkspaceTab(persistenceKey, normalizedTabId); }, - [closeWorkspaceTab, persistenceKey, unpinWorkspaceAgent], + [closeWorkspaceTab, hideWorkspaceAgent, persistenceKey, unpinWorkspaceAgent], ); const focusedPaneTabState = useMemo( @@ -989,6 +994,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) }); for (const agentId of workspaceAgentVisibility.activeAgentIds) { + if (hiddenAgentIds.has(agentId)) { + continue; + } const representedByTarget = uiTabs.some( (tab) => tab.target.kind === "agent" && tab.target.agentId === agentId, ); @@ -1036,6 +1044,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) closeWorkspaceTabWithCleanup, ensureWorkspaceTab, hasHydratedAgents, + hiddenAgentIds, pendingByDraftId, pinnedAgentIds, persistenceKey, diff --git a/packages/app/src/stores/workspace-layout-store.test.ts b/packages/app/src/stores/workspace-layout-store.test.ts index 78f0aa446..d7305adb1 100644 --- a/packages/app/src/stores/workspace-layout-store.test.ts +++ b/packages/app/src/stores/workspace-layout-store.test.ts @@ -224,6 +224,7 @@ describe("workspace-layout-store actions", () => { layoutByWorkspace: {}, splitSizesByWorkspace: {}, pinnedAgentIdsByWorkspace: {}, + hiddenAgentIdsByWorkspace: {}, }); vi.restoreAllMocks(); }); @@ -702,4 +703,80 @@ describe("workspace-layout-store actions", () => { splitSizesByWorkspace: {}, }); }); + + it("keeps hidden agent intents 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.hideAgent(workspaceKey, "agent-1"); + store.hideAgent(workspaceKey, "agent-1"); + store.hideAgent(otherWorkspaceKey as string, "agent-2"); + + let state = useWorkspaceLayoutStore.getState(); + expect(Array.from(state.hiddenAgentIdsByWorkspace[workspaceKey] ?? [])).toEqual(["agent-1"]); + expect(Array.from(state.hiddenAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([ + "agent-2", + ]); + + store.unhideAgent(workspaceKey, "agent-1"); + + state = useWorkspaceLayoutStore.getState(); + expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined(); + expect(Array.from(state.hiddenAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([ + "agent-2", + ]); + + const partialize = useWorkspaceLayoutStore.persist.getOptions().partialize; + expect(partialize).toBeTypeOf("function"); + expect(partialize?.(state)).toEqual({ + layoutByWorkspace: {}, + splitSizesByWorkspace: {}, + }); + }); + + it("explicitly opening an agent tab clears hidden intent", () => { + const workspaceKey = createWorkspaceKey(); + const store = useWorkspaceLayoutStore.getState(); + + store.hideAgent(workspaceKey, "agent-1"); + store.openTab(workspaceKey, { kind: "agent", agentId: "agent-1" }); + + const state = useWorkspaceLayoutStore.getState(); + expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined(); + expect(state.getWorkspaceTabs(workspaceKey).map((tab) => tab.tabId)).toEqual(["agent_agent-1"]); + }); + + it("pinning an agent clears hidden intent", () => { + const workspaceKey = createWorkspaceKey(); + const store = useWorkspaceLayoutStore.getState(); + + store.hideAgent(workspaceKey, "agent-1"); + expect( + useWorkspaceLayoutStore.getState().hiddenAgentIdsByWorkspace[workspaceKey], + ).toBeDefined(); + + store.pinAgent(workspaceKey, "agent-1"); + + const state = useWorkspaceLayoutStore.getState(); + expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined(); + expect(Array.from(state.pinnedAgentIdsByWorkspace[workspaceKey] ?? [])).toEqual(["agent-1"]); + }); + + it("retargeting a tab to an agent clears hidden intent", () => { + const workspaceKey = createWorkspaceKey(); + const store = useWorkspaceLayoutStore.getState(); + + store.hideAgent(workspaceKey, "agent-1"); + const tabId = store.openTab(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" }); + store.retargetTab(workspaceKey, tabId!, { kind: "agent", agentId: "agent-1" }); + + const state = useWorkspaceLayoutStore.getState(); + expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined(); + }); }); diff --git a/packages/app/src/stores/workspace-layout-store.ts b/packages/app/src/stores/workspace-layout-store.ts index e942dae51..8e7155d8b 100644 --- a/packages/app/src/stores/workspace-layout-store.ts +++ b/packages/app/src/stores/workspace-layout-store.ts @@ -54,6 +54,7 @@ interface WorkspaceLayoutStore { layoutByWorkspace: Record; splitSizesByWorkspace: Record>; pinnedAgentIdsByWorkspace: Record>; + hiddenAgentIdsByWorkspace: Record>; openTab: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; closeTab: (workspaceKey: string, tabId: string) => void; focusTab: (workspaceKey: string, tabId: string) => void; @@ -81,6 +82,8 @@ interface WorkspaceLayoutStore { reorderTabsInPane: (workspaceKey: string, paneId: string, tabIds: string[]) => void; pinAgent: (workspaceKey: string, agentId: string) => void; unpinAgent: (workspaceKey: string, agentId: string) => void; + hideAgent: (workspaceKey: string, agentId: string) => void; + unhideAgent: (workspaceKey: string, agentId: string) => void; } const MAX_TREE_DEPTH = 4; @@ -93,6 +96,48 @@ function trimNonEmpty(value: string | null | undefined): string | null { return trimmed.length > 0 ? trimmed : null; } +function addAgentIdToWorkspaceSet( + state: Record>, + workspaceKey: string, + agentId: string, +): Record> { + const currentAgentIds = state[workspaceKey] ?? null; + if (currentAgentIds?.has(agentId)) { + return state; + } + + const nextAgentIds = new Set(currentAgentIds ?? []); + nextAgentIds.add(agentId); + return { + ...state, + [workspaceKey]: nextAgentIds, + }; +} + +function removeAgentIdFromWorkspaceSet( + state: Record>, + workspaceKey: string, + agentId: string, +): Record> { + const currentAgentIds = state[workspaceKey] ?? null; + if (!currentAgentIds?.has(agentId)) { + return state; + } + + if (currentAgentIds.size === 1) { + const nextState = { ...state }; + delete nextState[workspaceKey]; + return nextState; + } + + const nextAgentIds = new Set(currentAgentIds); + nextAgentIds.delete(agentId); + return { + ...state, + [workspaceKey]: nextAgentIds, + }; +} + function getWorkspaceLayout( state: Record, workspaceKey: string, @@ -106,6 +151,7 @@ export const useWorkspaceLayoutStore = create()( layoutByWorkspace: {}, splitSizesByWorkspace: {}, pinnedAgentIdsByWorkspace: {}, + hiddenAgentIdsByWorkspace: {}, openTab: (workspaceKey, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTarget = normalizeWorkspaceTabTarget(target); @@ -120,6 +166,14 @@ export const useWorkspaceLayoutStore = create()( }); set((state) => ({ + hiddenAgentIdsByWorkspace: + normalizedTarget.kind !== "agent" + ? state.hiddenAgentIdsByWorkspace + : removeAgentIdFromWorkspaceSet( + state.hiddenAgentIdsByWorkspace, + normalizedWorkspaceKey, + normalizedTarget.agentId, + ), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, @@ -194,6 +248,14 @@ export const useWorkspaceLayoutStore = create()( } set((state) => ({ + hiddenAgentIdsByWorkspace: + normalizedTarget.kind !== "agent" + ? state.hiddenAgentIdsByWorkspace + : removeAgentIdFromWorkspaceSet( + state.hiddenAgentIdsByWorkspace, + normalizedWorkspaceKey, + normalizedTarget.agentId, + ), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, @@ -412,6 +474,11 @@ export const useWorkspaceLayoutStore = create()( nextPinnedAgentIds.add(normalizedAgentId); return { + hiddenAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet( + state.hiddenAgentIdsByWorkspace, + normalizedWorkspaceKey, + normalizedAgentId, + ), pinnedAgentIdsByWorkspace: { ...state.pinnedAgentIdsByWorkspace, [normalizedWorkspaceKey]: nextPinnedAgentIds, @@ -454,6 +521,50 @@ export const useWorkspaceLayoutStore = create()( }; }); }, + hideAgent: (workspaceKey, agentId) => { + const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); + const normalizedAgentId = trimNonEmpty(agentId); + if (!normalizedWorkspaceKey || !normalizedAgentId) { + return; + } + + set((state) => { + const nextHiddenAgentIdsByWorkspace = addAgentIdToWorkspaceSet( + state.hiddenAgentIdsByWorkspace, + normalizedWorkspaceKey, + normalizedAgentId, + ); + if (nextHiddenAgentIdsByWorkspace === state.hiddenAgentIdsByWorkspace) { + return state; + } + + return { + hiddenAgentIdsByWorkspace: nextHiddenAgentIdsByWorkspace, + }; + }); + }, + unhideAgent: (workspaceKey, agentId) => { + const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); + const normalizedAgentId = trimNonEmpty(agentId); + if (!normalizedWorkspaceKey || !normalizedAgentId) { + return; + } + + set((state) => { + const nextHiddenAgentIdsByWorkspace = removeAgentIdFromWorkspaceSet( + state.hiddenAgentIdsByWorkspace, + normalizedWorkspaceKey, + normalizedAgentId, + ); + if (nextHiddenAgentIdsByWorkspace === state.hiddenAgentIdsByWorkspace) { + return state; + } + + return { + hiddenAgentIdsByWorkspace: nextHiddenAgentIdsByWorkspace, + }; + }); + }, }), { name: "workspace-layout-state",