Add hidden agent tab intent to prevent reconciliation from re-adding closed tabs
When bulk-closing tabs (e.g. "Close to the left"), the reconciliation useEffect would re-add tabs for still-active agents before the server archived them, causing tabs to flicker back then disappear one by one. Adds hiddenAgentIdsByWorkspace as the inverse of pinnedAgentIds: a local, in-memory-only override that prevents reconciliation from re-opening tabs for agents the user just closed. Hidden intent is automatically cleared when explicitly opening, retargeting, or pinning an agent tab.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ interface WorkspaceLayoutStore {
|
||||
layoutByWorkspace: Record<string, WorkspaceLayout>;
|
||||
splitSizesByWorkspace: Record<string, Record<string, number[]>>;
|
||||
pinnedAgentIdsByWorkspace: Record<string, Set<string>>;
|
||||
hiddenAgentIdsByWorkspace: Record<string, Set<string>>;
|
||||
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<string, Set<string>>,
|
||||
workspaceKey: string,
|
||||
agentId: string,
|
||||
): Record<string, Set<string>> {
|
||||
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<string, Set<string>>,
|
||||
workspaceKey: string,
|
||||
agentId: string,
|
||||
): Record<string, Set<string>> {
|
||||
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<string, WorkspaceLayout>,
|
||||
workspaceKey: string,
|
||||
@@ -106,6 +151,7 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
||||
layoutByWorkspace: {},
|
||||
splitSizesByWorkspace: {},
|
||||
pinnedAgentIdsByWorkspace: {},
|
||||
hiddenAgentIdsByWorkspace: {},
|
||||
openTab: (workspaceKey, target) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedTarget = normalizeWorkspaceTabTarget(target);
|
||||
@@ -120,6 +166,14 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
||||
});
|
||||
|
||||
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<WorkspaceLayoutStore>()(
|
||||
}
|
||||
|
||||
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<WorkspaceLayoutStore>()(
|
||||
nextPinnedAgentIds.add(normalizedAgentId);
|
||||
|
||||
return {
|
||||
hiddenAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet(
|
||||
state.hiddenAgentIdsByWorkspace,
|
||||
normalizedWorkspaceKey,
|
||||
normalizedAgentId,
|
||||
),
|
||||
pinnedAgentIdsByWorkspace: {
|
||||
...state.pinnedAgentIdsByWorkspace,
|
||||
[normalizedWorkspaceKey]: nextPinnedAgentIds,
|
||||
@@ -454,6 +521,50 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
||||
};
|
||||
});
|
||||
},
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user