mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-22 01:02:41 +00:00
Keep restored workspaces out of reconciliation loops (#3987)
* fix(app): stop workspace tab reconciliation loop Duplicate startup tabs could move workspace focus into Explorer. Once hydration removed the stale agent, empty-workspace reconciliation kept seeding drafts into Explorer and retriggering itself. Preserve ordinary-pane focus at the reconciliation boundary. * fix(app): normalize workspace focus before reconciliation Persisted Explorer focus must not steer newly reconciled tabs into the sidebar before the output invariant is applied.
This commit is contained in:
@@ -3391,6 +3391,99 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe(mainTab?.tabId);
|
||||
});
|
||||
|
||||
it("reconcileTabs stays stable when startup removes a duplicate agent kept in Explorer", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
workspaceLayoutStore.setState((state) => ({
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[workspaceKey]: {
|
||||
root: {
|
||||
kind: "group",
|
||||
group: {
|
||||
id: "root",
|
||||
direction: "horizontal",
|
||||
sizes: [0.78, 0.22],
|
||||
children: [
|
||||
createPane({
|
||||
id: "main",
|
||||
tabIds: ["new", "draft-origin-agent"],
|
||||
focusedTabId: "draft-origin-agent",
|
||||
targetsByTabId: {
|
||||
new: { kind: "new_tab" },
|
||||
"draft-origin-agent": { kind: "agent", agentId: "agent-1" },
|
||||
},
|
||||
}),
|
||||
createPane({
|
||||
id: "explorer",
|
||||
tabIds: ["files", "changes_tree", "agent_agent-1"],
|
||||
focusedTabId: "agent_agent-1",
|
||||
targetsByTabId: {
|
||||
files: { kind: "files" },
|
||||
changes_tree: { kind: "changes_tree" },
|
||||
"agent_agent-1": { kind: "agent", agentId: "agent-1" },
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
focusedPaneId: "main",
|
||||
},
|
||||
},
|
||||
explorerSidebarPaneIdByWorkspace: {
|
||||
...state.explorerSidebarPaneIdByWorkspace,
|
||||
[workspaceKey]: "explorer",
|
||||
},
|
||||
}));
|
||||
|
||||
store.reconcileTabs(workspaceKey, {
|
||||
agentsHydrated: false,
|
||||
terminalsHydrated: false,
|
||||
activeAgentIds: [],
|
||||
autoOpenAgentIds: [],
|
||||
knownAgentIds: [],
|
||||
knownTerminalIds: [],
|
||||
standaloneTerminalIds: [],
|
||||
});
|
||||
expect(workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].focusedPaneId).toBe(
|
||||
"main",
|
||||
);
|
||||
|
||||
workspaceLayoutStore.setState((state) => ({
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[workspaceKey]: {
|
||||
...state.layoutByWorkspace[workspaceKey],
|
||||
focusedPaneId: "explorer",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const emptySnapshot = {
|
||||
agentsHydrated: true,
|
||||
terminalsHydrated: true,
|
||||
activeAgentIds: [],
|
||||
autoOpenAgentIds: [],
|
||||
knownAgentIds: [],
|
||||
knownTerminalIds: [],
|
||||
standaloneTerminalIds: [],
|
||||
hasActivePendingDraftCreate: false,
|
||||
};
|
||||
store.reconcileTabs(workspaceKey, emptySnapshot);
|
||||
store.reconcileTabs(workspaceKey, emptySnapshot);
|
||||
const afterSecondReconcile = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||
store.reconcileTabs(workspaceKey, emptySnapshot);
|
||||
const afterThirdReconcile = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||
|
||||
expect(afterThirdReconcile).toBe(afterSecondReconcile);
|
||||
const mainTabs = collectAllTabs(afterThirdReconcile.root).filter(
|
||||
(tab) => findPaneContainingTab(afterThirdReconcile.root, tab.tabId)?.id === "main",
|
||||
);
|
||||
expect(mainTabs).toEqual([
|
||||
expect.objectContaining({ target: expect.objectContaining({ kind: "draft" }) }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reconcileTabs does not auto-open subagents omitted from autoOpenAgentIds", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
|
||||
|
||||
@@ -1182,9 +1182,15 @@ export function createWorkspaceLayoutStore(
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const currentLayout = getWorkspaceLayout(
|
||||
state.layoutByWorkspace,
|
||||
normalizedWorkspaceKey,
|
||||
const rawLayout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
||||
const explorerSidebarPaneId = resolveExplorerSidebarPaneId(
|
||||
rawLayout,
|
||||
state.explorerSidebarPaneIdByWorkspace[normalizedWorkspaceKey],
|
||||
);
|
||||
const currentLayout = keepWorkspaceFocusOutOfExplorerSidebar(
|
||||
rawLayout,
|
||||
explorerSidebarPaneId,
|
||||
rawLayout.focusedPaneId,
|
||||
);
|
||||
const nextState = reconcileWorkspaceTabs(
|
||||
{
|
||||
@@ -1192,21 +1198,23 @@ export function createWorkspaceLayoutStore(
|
||||
pinnedAgentIds: state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
|
||||
pendingAgentIds: state.pendingAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
|
||||
hiddenAgentIds: state.hiddenAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null,
|
||||
explorerSidebarPaneId: resolveExplorerSidebarPaneId(
|
||||
currentLayout,
|
||||
state.explorerSidebarPaneIdByWorkspace[normalizedWorkspaceKey],
|
||||
),
|
||||
explorerSidebarPaneId,
|
||||
},
|
||||
snapshot,
|
||||
);
|
||||
if (nextState.layout === currentLayout) {
|
||||
const nextLayout = keepWorkspaceFocusOutOfExplorerSidebar(
|
||||
nextState.layout,
|
||||
explorerSidebarPaneId,
|
||||
currentLayout.focusedPaneId,
|
||||
);
|
||||
if (nextLayout === rawLayout) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[normalizedWorkspaceKey]: nextState.layout,
|
||||
[normalizedWorkspaceKey]: nextLayout,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user