Add hidden agent tab intent for workspace reconcile
This commit is contained in:
@@ -876,6 +876,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
const convertWorkspaceDraftToAgent = useWorkspaceLayoutStore((state) => state.convertDraftToAgent);
|
const convertWorkspaceDraftToAgent = useWorkspaceLayoutStore((state) => state.convertDraftToAgent);
|
||||||
const reconcileWorkspaceTabs = useWorkspaceLayoutStore((state) => state.reconcileTabs);
|
const reconcileWorkspaceTabs = useWorkspaceLayoutStore((state) => state.reconcileTabs);
|
||||||
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
|
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
|
||||||
|
const hideWorkspaceAgent = useWorkspaceLayoutStore((state) => state.hideAgent);
|
||||||
const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane);
|
const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane);
|
||||||
const splitWorkspacePaneEmpty = useWorkspaceLayoutStore((state) => state.splitPaneEmpty);
|
const splitWorkspacePaneEmpty = useWorkspaceLayoutStore((state) => state.splitPaneEmpty);
|
||||||
const moveWorkspaceTabToPane = useWorkspaceLayoutStore((state) => state.moveTabToPane);
|
const moveWorkspaceTabToPane = useWorkspaceLayoutStore((state) => state.moveTabToPane);
|
||||||
@@ -897,10 +898,11 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
|
|
||||||
if (input.target?.kind === "agent") {
|
if (input.target?.kind === "agent") {
|
||||||
unpinWorkspaceAgent(persistenceKey, input.target.agentId);
|
unpinWorkspaceAgent(persistenceKey, input.target.agentId);
|
||||||
|
hideWorkspaceAgent(persistenceKey, input.target.agentId);
|
||||||
}
|
}
|
||||||
closeWorkspaceTab(persistenceKey, normalizedTabId);
|
closeWorkspaceTab(persistenceKey, normalizedTabId);
|
||||||
},
|
},
|
||||||
[closeWorkspaceTab, persistenceKey, unpinWorkspaceAgent],
|
[closeWorkspaceTab, hideWorkspaceAgent, persistenceKey, unpinWorkspaceAgent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const focusedPaneTabState = useMemo(
|
const focusedPaneTabState = useMemo(
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ interface ReorderPaneTabsInLayoutInput {
|
|||||||
export interface WorkspaceTabReconcileState {
|
export interface WorkspaceTabReconcileState {
|
||||||
layout: WorkspaceLayout;
|
layout: WorkspaceLayout;
|
||||||
pinnedAgentIds?: ReadonlySet<string> | null;
|
pinnedAgentIds?: ReadonlySet<string> | null;
|
||||||
|
hiddenAgentIds?: ReadonlySet<string> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceTabSnapshot {
|
export interface WorkspaceTabSnapshot {
|
||||||
@@ -1429,6 +1430,7 @@ export function reconcileWorkspaceTabs(
|
|||||||
findPaneById(nextLayout.root, nextLayout.focusedPaneId)?.focusedTabId ?? null;
|
findPaneById(nextLayout.root, nextLayout.focusedPaneId)?.focusedTabId ?? null;
|
||||||
let reconciledFocusedTabId = originalFocusedTabId;
|
let reconciledFocusedTabId = originalFocusedTabId;
|
||||||
const pinnedAgentIds = new Set(state.pinnedAgentIds ?? []);
|
const pinnedAgentIds = new Set(state.pinnedAgentIds ?? []);
|
||||||
|
const hiddenAgentIds = new Set(state.hiddenAgentIds ?? []);
|
||||||
const activeAgentIds = normalizeStringSet(snapshot.activeAgentIds);
|
const activeAgentIds = normalizeStringSet(snapshot.activeAgentIds);
|
||||||
const knownAgentIds = normalizeStringSet(snapshot.knownAgentIds);
|
const knownAgentIds = normalizeStringSet(snapshot.knownAgentIds);
|
||||||
const standaloneTerminalIds = normalizeStringSet(snapshot.standaloneTerminalIds);
|
const standaloneTerminalIds = normalizeStringSet(snapshot.standaloneTerminalIds);
|
||||||
@@ -1438,6 +1440,9 @@ export function reconcileWorkspaceTabs(
|
|||||||
visibleAgentIds.add(agentId);
|
visibleAgentIds.add(agentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const agentId of hiddenAgentIds) {
|
||||||
|
visibleAgentIds.delete(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
const initialTabs = collectAllTabs(nextLayout.root);
|
const initialTabs = collectAllTabs(nextLayout.root);
|
||||||
const representedAgentIds = new Set(initialTabs.filter(isAgentTab).map((tab) => tab.target.agentId));
|
const representedAgentIds = new Set(initialTabs.filter(isAgentTab).map((tab) => tab.target.agentId));
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
layoutByWorkspace: {},
|
layoutByWorkspace: {},
|
||||||
splitSizesByWorkspace: {},
|
splitSizesByWorkspace: {},
|
||||||
pinnedAgentIdsByWorkspace: {},
|
pinnedAgentIdsByWorkspace: {},
|
||||||
|
hiddenAgentIdsByWorkspace: {},
|
||||||
});
|
});
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
@@ -837,6 +838,42 @@ describe("workspace-layout-store actions", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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("convertDraftToAgent removes the draft and focuses the existing canonical agent tab", () => {
|
it("convertDraftToAgent removes the draft and focuses the existing canonical agent tab", () => {
|
||||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||||
"67676767-6767-6767-6767-676767676767",
|
"67676767-6767-6767-6767-676767676767",
|
||||||
@@ -934,4 +971,38 @@ describe("workspace-layout-store actions", () => {
|
|||||||
expect(layout.focusedPaneId).toBe("main");
|
expect(layout.focusedPaneId).toBe("main");
|
||||||
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe("agent_agent-1");
|
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe("agent_agent-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reconcileTabs does not re-add locally hidden agent tabs", () => {
|
||||||
|
const workspaceKey = createWorkspaceKey();
|
||||||
|
|
||||||
|
useWorkspaceLayoutStore.setState((state) => ({
|
||||||
|
...state,
|
||||||
|
hiddenAgentIdsByWorkspace: {
|
||||||
|
[workspaceKey]: new Set<string>(["agent-1"]),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
|
||||||
|
agentsHydrated: true,
|
||||||
|
terminalsHydrated: true,
|
||||||
|
activeAgentIds: ["agent-1"],
|
||||||
|
knownAgentIds: ["agent-1"],
|
||||||
|
standaloneTerminalIds: [],
|
||||||
|
hasActivePendingDraftCreate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ interface WorkspaceLayoutStore {
|
|||||||
layoutByWorkspace: Record<string, WorkspaceLayout>;
|
layoutByWorkspace: Record<string, WorkspaceLayout>;
|
||||||
splitSizesByWorkspace: Record<string, Record<string, number[]>>;
|
splitSizesByWorkspace: Record<string, Record<string, number[]>>;
|
||||||
pinnedAgentIdsByWorkspace: Record<string, Set<string>>;
|
pinnedAgentIdsByWorkspace: Record<string, Set<string>>;
|
||||||
|
hiddenAgentIdsByWorkspace: Record<string, Set<string>>;
|
||||||
openTab: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
openTab: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
||||||
closeTab: (workspaceKey: string, tabId: string) => void;
|
closeTab: (workspaceKey: string, tabId: string) => void;
|
||||||
focusTab: (workspaceKey: string, tabId: string) => void;
|
focusTab: (workspaceKey: string, tabId: string) => void;
|
||||||
@@ -94,6 +95,8 @@ interface WorkspaceLayoutStore {
|
|||||||
reorderTabsInPane: (workspaceKey: string, paneId: string, tabIds: string[]) => void;
|
reorderTabsInPane: (workspaceKey: string, paneId: string, tabIds: string[]) => void;
|
||||||
pinAgent: (workspaceKey: string, agentId: string) => void;
|
pinAgent: (workspaceKey: string, agentId: string) => void;
|
||||||
unpinAgent: (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;
|
const MAX_TREE_DEPTH = 4;
|
||||||
@@ -106,6 +109,48 @@ function trimNonEmpty(value: string | null | undefined): string | null {
|
|||||||
return trimmed.length > 0 ? trimmed : 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(
|
function getWorkspaceLayout(
|
||||||
state: Record<string, WorkspaceLayout>,
|
state: Record<string, WorkspaceLayout>,
|
||||||
workspaceKey: string,
|
workspaceKey: string,
|
||||||
@@ -119,6 +164,7 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
|||||||
layoutByWorkspace: {},
|
layoutByWorkspace: {},
|
||||||
splitSizesByWorkspace: {},
|
splitSizesByWorkspace: {},
|
||||||
pinnedAgentIdsByWorkspace: {},
|
pinnedAgentIdsByWorkspace: {},
|
||||||
|
hiddenAgentIdsByWorkspace: {},
|
||||||
openTab: (workspaceKey, target) => {
|
openTab: (workspaceKey, target) => {
|
||||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||||
const normalizedTarget = normalizeWorkspaceTabTarget(target);
|
const normalizedTarget = normalizeWorkspaceTabTarget(target);
|
||||||
@@ -133,6 +179,14 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
|||||||
});
|
});
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
hiddenAgentIdsByWorkspace:
|
||||||
|
normalizedTarget.kind !== "agent"
|
||||||
|
? state.hiddenAgentIdsByWorkspace
|
||||||
|
: removeAgentIdFromWorkspaceSet(
|
||||||
|
state.hiddenAgentIdsByWorkspace,
|
||||||
|
normalizedWorkspaceKey,
|
||||||
|
normalizedTarget.agentId,
|
||||||
|
),
|
||||||
layoutByWorkspace: {
|
layoutByWorkspace: {
|
||||||
...state.layoutByWorkspace,
|
...state.layoutByWorkspace,
|
||||||
[normalizedWorkspaceKey]: result.layout,
|
[normalizedWorkspaceKey]: result.layout,
|
||||||
@@ -207,6 +261,14 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
hiddenAgentIdsByWorkspace:
|
||||||
|
normalizedTarget.kind !== "agent"
|
||||||
|
? state.hiddenAgentIdsByWorkspace
|
||||||
|
: removeAgentIdFromWorkspaceSet(
|
||||||
|
state.hiddenAgentIdsByWorkspace,
|
||||||
|
normalizedWorkspaceKey,
|
||||||
|
normalizedTarget.agentId,
|
||||||
|
),
|
||||||
layoutByWorkspace: {
|
layoutByWorkspace: {
|
||||||
...state.layoutByWorkspace,
|
...state.layoutByWorkspace,
|
||||||
[normalizedWorkspaceKey]: result.layout,
|
[normalizedWorkspaceKey]: result.layout,
|
||||||
@@ -233,6 +295,11 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
hiddenAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet(
|
||||||
|
state.hiddenAgentIdsByWorkspace,
|
||||||
|
normalizedWorkspaceKey,
|
||||||
|
normalizedAgentId,
|
||||||
|
),
|
||||||
layoutByWorkspace: {
|
layoutByWorkspace: {
|
||||||
...state.layoutByWorkspace,
|
...state.layoutByWorkspace,
|
||||||
[normalizedWorkspaceKey]: result.layout,
|
[normalizedWorkspaceKey]: result.layout,
|
||||||
@@ -253,6 +320,7 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
|||||||
{
|
{
|
||||||
layout: currentLayout,
|
layout: currentLayout,
|
||||||
pinnedAgentIds: state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
|
pinnedAgentIds: state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
|
||||||
|
hiddenAgentIds: state.hiddenAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
|
||||||
},
|
},
|
||||||
snapshot,
|
snapshot,
|
||||||
);
|
);
|
||||||
@@ -478,6 +546,11 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
|||||||
nextPinnedAgentIds.add(normalizedAgentId);
|
nextPinnedAgentIds.add(normalizedAgentId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
hiddenAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet(
|
||||||
|
state.hiddenAgentIdsByWorkspace,
|
||||||
|
normalizedWorkspaceKey,
|
||||||
|
normalizedAgentId,
|
||||||
|
),
|
||||||
pinnedAgentIdsByWorkspace: {
|
pinnedAgentIdsByWorkspace: {
|
||||||
...state.pinnedAgentIdsByWorkspace,
|
...state.pinnedAgentIdsByWorkspace,
|
||||||
[normalizedWorkspaceKey]: nextPinnedAgentIds,
|
[normalizedWorkspaceKey]: nextPinnedAgentIds,
|
||||||
@@ -520,6 +593,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",
|
name: "workspace-layout-state",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("prepareWorkspaceTab", () => {
|
|||||||
layoutByWorkspace: {},
|
layoutByWorkspace: {},
|
||||||
splitSizesByWorkspace: {},
|
splitSizesByWorkspace: {},
|
||||||
pinnedAgentIdsByWorkspace: {},
|
pinnedAgentIdsByWorkspace: {},
|
||||||
|
hiddenAgentIdsByWorkspace: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user