Keep explicitly reopened archived agent tabs open
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user