Files
kandev/apps/web/lib/state/dockview-session-switch.ts
T

120 lines
4.3 KiB
TypeScript

/**
* Session switch logic for dockview layout management.
*
* Handles both a "fast path" (skip fromJSON when layout structure matches)
* and a "slow path" (full layout rebuild via fromJSON).
*/
import type { DockviewApi, SerializedDockview } from "dockview-react";
import { getSessionLayout } from "@/lib/local-storage";
import { applyLayoutFixups } from "./dockview-layout-builders";
import { fromDockviewApi, savedLayoutMatchesLive, layoutStructuresMatch } from "./layout-manager";
import type { LayoutState, LayoutGroupIds } from "./layout-manager";
export type SessionSwitchParams = {
api: DockviewApi;
oldSessionId: string | null;
newSessionId: string;
safeWidth: number;
safeHeight: number;
buildDefault: (api: DockviewApi) => void;
getDefaultLayout: () => LayoutState;
};
/**
* Remove ephemeral panels (file-editors, diffs, commit-details) from the
* live layout. These are session-specific panels that shouldn't carry over.
*
* When `keepSessionId` is provided, session chat panels whose ID does not
* match `session:{keepSessionId}` are also removed. This handles cross-task
* switches where the fast path is taken: without this, session tabs from the
* old task remain visible alongside the new task's tab.
*/
function removeEphemeralPanels(api: DockviewApi, keepSessionId?: string): void {
const toRemove = api.panels.filter((p) => {
const comp = p.api.component;
if (comp === "file-editor" || comp === "diff-viewer" || comp === "commit-detail") {
return true;
}
// Remove session chat tabs that belong to a different session.
// This covers cross-task switches where the layout structures match and
// the fast path is taken — old task's session tabs must not bleed through.
if (
keepSessionId !== undefined &&
comp === "chat" &&
p.id.startsWith("session:") &&
p.id !== `session:${keepSessionId}`
) {
return true;
}
return false;
});
for (const p of toRemove) {
try {
p.api.close();
} catch {
/* panel may already be gone */
}
}
}
/**
* Fast path: check if we can skip `api.fromJSON()` because the layout
* structure hasn't changed. Returns group IDs if the fast path was taken,
* or null if a full rebuild is needed.
*/
function tryFastSessionSwitch(params: SessionSwitchParams): LayoutGroupIds | null {
const { api, newSessionId, getDefaultLayout } = params;
const currentLayout = fromDockviewApi(api);
const saved = getSessionLayout(newSessionId);
let structuresMatch = false;
if (saved) {
// Compare live layout against saved layout by structural component set
structuresMatch = savedLayoutMatchesLive(currentLayout, saved as SerializedDockview);
} else {
// No saved layout — target is the default; compare LayoutState structures
structuresMatch = layoutStructuresMatch(currentLayout, getDefaultLayout());
}
if (!structuresMatch) return null;
// Fast path: keep the grid structure, clean up ephemeral panels and any
// session chat tabs that belong to a different session (cross-task switch).
// Session-scoped portals (terminal, browser, etc.) will be re-acquired
// via usePortalSlot's sessionId dependency change.
removeEphemeralPanels(api, newSessionId);
return applyLayoutFixups(api);
}
/**
* Switch the dockview layout between sessions.
*
* Uses a fast path when layouts are structurally identical (common case),
* falling back to a full `api.fromJSON()` rebuild when they differ.
*
* The caller is responsible for saving the old session layout and releasing
* session-scoped portals before calling this function.
*/
export function performSessionSwitch(params: SessionSwitchParams): LayoutGroupIds {
const { api, newSessionId, safeWidth, safeHeight, buildDefault } = params;
// Try fast path: skip fromJSON when layout structure hasn't changed
const fastResult = tryFastSessionSwitch(params);
if (fastResult) return fastResult;
// Slow path: full layout rebuild via fromJSON
const saved = getSessionLayout(newSessionId);
if (saved) {
try {
api.fromJSON(saved as SerializedDockview);
api.layout(safeWidth, safeHeight);
return applyLayoutFixups(api);
} catch {
/* fall through */
}
}
buildDefault(api);
api.layout(safeWidth, safeHeight);
return applyLayoutFixups(api);
}