fix(web): preserve chat focus when restoring task layout (#1923)
This commit is contained in:
@@ -30,6 +30,22 @@ type MoveToCall = {
|
||||
options: MoveToOptions;
|
||||
};
|
||||
|
||||
type ReorderingPanel = {
|
||||
id: string;
|
||||
group: ReorderingGroup;
|
||||
api: {
|
||||
close: ReturnType<typeof vi.fn<() => void>>;
|
||||
component: string;
|
||||
moveTo: ReturnType<typeof vi.fn<(options: MoveToOptions) => void>>;
|
||||
setActive: ReturnType<typeof vi.fn<() => void>>;
|
||||
};
|
||||
};
|
||||
|
||||
type ReorderingGroup = {
|
||||
id: string;
|
||||
panels: ReorderingPanel[];
|
||||
};
|
||||
|
||||
const KEEP = "keep";
|
||||
const KEEP_PANEL = `session:${KEEP}`;
|
||||
const LEAKED_PANEL = "session:leaked";
|
||||
@@ -165,6 +181,72 @@ function expectedMove(panelId: string, index: number): MoveToCall {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Models Dockview 4.13.1's same-group move behavior: moving the active panel
|
||||
* removes it first, which activates a remaining sibling, then reinserts the
|
||||
* moved panel inactive when skipSetActive is true.
|
||||
*/
|
||||
function makeReorderingAutoSessionApi(): {
|
||||
api: DockviewApi;
|
||||
activePanelId: () => string | null;
|
||||
} {
|
||||
const panels: ReorderingPanel[] = [];
|
||||
const group: ReorderingGroup = { id: CENTER_GROUP, panels: [] };
|
||||
let activePanel: ReorderingPanel | null = null;
|
||||
|
||||
const removePanel = (panel: ReorderingPanel) => {
|
||||
const groupIndex = group.panels.indexOf(panel);
|
||||
if (groupIndex !== -1) group.panels.splice(groupIndex, 1);
|
||||
const panelIndex = panels.indexOf(panel);
|
||||
if (panelIndex !== -1) panels.splice(panelIndex, 1);
|
||||
if (activePanel === panel) activePanel = group.panels[0] ?? null;
|
||||
};
|
||||
|
||||
const createPanel = (id: string, component: string): ReorderingPanel => {
|
||||
const panel: ReorderingPanel = {
|
||||
id,
|
||||
group,
|
||||
api: {
|
||||
close: vi.fn(() => removePanel(panel)),
|
||||
component,
|
||||
moveTo: vi.fn((options: MoveToOptions) => {
|
||||
const sourceIndex = group.panels.indexOf(panel);
|
||||
if (sourceIndex !== -1) group.panels.splice(sourceIndex, 1);
|
||||
if (activePanel === panel) activePanel = group.panels[0] ?? null;
|
||||
group.panels.splice(options.index, 0, panel);
|
||||
if (!options.skipSetActive) activePanel = panel;
|
||||
}),
|
||||
setActive: vi.fn(() => {
|
||||
activePanel = panel;
|
||||
}),
|
||||
},
|
||||
};
|
||||
panels.push(panel);
|
||||
group.panels.push(panel);
|
||||
return panel;
|
||||
};
|
||||
|
||||
activePanel = createPanel("chat", "chat");
|
||||
createPanel("plan", "plan");
|
||||
|
||||
const api = {
|
||||
get activePanel() {
|
||||
return activePanel;
|
||||
},
|
||||
panels,
|
||||
groups: [group],
|
||||
getPanel: (id: string) => panels.find((panel) => panel.id === id) ?? null,
|
||||
addPanel: (options: { id: string; component: string; inactive?: boolean }): ReorderingPanel => {
|
||||
const panel = createPanel(options.id, options.component);
|
||||
if (!options.inactive) activePanel = panel;
|
||||
return panel;
|
||||
},
|
||||
removePanel,
|
||||
} as unknown as DockviewApi;
|
||||
|
||||
return { api, activePanelId: () => activePanel?.id ?? null };
|
||||
}
|
||||
|
||||
describe("reconcileRemovedSessionPanels", () => {
|
||||
it("closes a stale tracked panel that's still live in dockview", () => {
|
||||
// createdSet has session-A; A's panel is live; A is no longer in the task's
|
||||
@@ -554,6 +636,19 @@ describe("resolveSessionTabSyncTarget", () => {
|
||||
});
|
||||
|
||||
describe("runAutoSessionTabEffect", () => {
|
||||
it("keeps chat active when replacing its placeholder beside a Plan tab", () => {
|
||||
const sessionId = "session-current";
|
||||
const { api, activePanelId } = makeReorderingAutoSessionApi();
|
||||
const appStore = makeAutoSessionAppStore(AUTO_TASK_ID, [sessionId]);
|
||||
const refs = makeAutoSessionRefs();
|
||||
|
||||
withDockviewState({ api, preMaximizeLayout: null }, () => {
|
||||
runAutoSessionTabEffect(sessionId, appStore as never, refs as never);
|
||||
});
|
||||
|
||||
expect(activePanelId()).toBe(`session:${sessionId}`);
|
||||
});
|
||||
|
||||
it("updates previous refs when panel ensure is skipped for an unhydrated session", () => {
|
||||
const api = {
|
||||
panels: [{ id: "chat" }],
|
||||
|
||||
@@ -533,31 +533,35 @@ type AutoSessionTabRefs = {
|
||||
function activateSessionPanel(
|
||||
api: DockviewApi,
|
||||
effectiveSessionId: string,
|
||||
sessionPanelExistedBefore: boolean,
|
||||
activation: {
|
||||
sessionPanelExistedBefore: boolean;
|
||||
activePanelIdBeforeEnsure: string | null;
|
||||
},
|
||||
refs: AutoSessionTabRefs,
|
||||
tid: string | null,
|
||||
): ReturnType<DockviewApi["getPanel"]> {
|
||||
const activePanel = api.getPanel(`session:${effectiveSessionId}`);
|
||||
if (!activePanel) return activePanel;
|
||||
|
||||
const currentActivePanelId = api.activePanel?.id ?? null;
|
||||
const activePanelIdAfterReorder = api.activePanel?.id ?? null;
|
||||
const shouldActivate = shouldActivateSessionPanel({
|
||||
sessionPanelExistedBefore,
|
||||
sessionPanelExistedBefore: activation.sessionPanelExistedBefore,
|
||||
prevTaskId: refs.prevTaskIdRef.current,
|
||||
prevSessionId: refs.prevSessionIdRef.current,
|
||||
currentTaskId: tid,
|
||||
currentSessionId: effectiveSessionId,
|
||||
currentActivePanelId,
|
||||
currentActivePanelId: activation.activePanelIdBeforeEnsure,
|
||||
});
|
||||
if (isDebug()) {
|
||||
debug("useAutoSessionTab: activation decision", {
|
||||
effectiveSessionId,
|
||||
shouldActivate,
|
||||
sessionPanelExistedBefore,
|
||||
sessionPanelExistedBefore: activation.sessionPanelExistedBefore,
|
||||
prevTaskId: refs.prevTaskIdRef.current,
|
||||
prevSessionId: refs.prevSessionIdRef.current,
|
||||
currentTaskId: tid,
|
||||
currentActivePanelId,
|
||||
activePanelIdBeforeEnsure: activation.activePanelIdBeforeEnsure,
|
||||
activePanelIdAfterReorder,
|
||||
activeGroupId: activePanel.group.id,
|
||||
});
|
||||
}
|
||||
@@ -763,6 +767,9 @@ export function runAutoSessionTabEffect(
|
||||
|
||||
const initialPosition = resolveInitialPosition(api);
|
||||
const sessionPanelExistedBefore = !!api.getPanel(`session:${effectiveSessionId}`);
|
||||
// Preserve the restored/user-selected panel before adding and reordering the
|
||||
// session tab. Dockview's same-group move briefly activates a sibling even
|
||||
// with skipSetActive, so reading api.activePanel afterward loses this intent.
|
||||
const activePanelIdBeforeEnsure = api.activePanel?.id ?? null;
|
||||
const preserveActivePanel = shouldPreserveActivePanel(
|
||||
sessionPanelExistedBefore,
|
||||
@@ -788,7 +795,7 @@ export function runAutoSessionTabEffect(
|
||||
const activePanel = activateSessionPanel(
|
||||
api,
|
||||
effectiveSessionId,
|
||||
sessionPanelExistedBefore,
|
||||
{ sessionPanelExistedBefore, activePanelIdBeforeEnsure },
|
||||
refs,
|
||||
tid,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user