import type { DockviewReadyEvent } from "dockview-react"; import type { StoreApi } from "zustand"; import type { AppState } from "@/lib/state/store"; import { useDockviewStore } from "@/lib/state/dockview-store"; import { getRootSplitview } from "@/lib/state/dockview-layout-builders"; import { setSessionLayout } from "@/lib/local-storage"; import { panelPortalManager } from "@/lib/layout/panel-portal-manager"; import { stopVscode } from "@/lib/api/domains/vscode-api"; import { stopUserShell } from "@/lib/api/domains/user-shell-api"; const LAYOUT_STORAGE_KEY = "dockview-layout-v1"; function trackPinnedWidths(api: DockviewReadyEvent["api"]): void { if (useDockviewStore.getState().isRestoringLayout) return; if (api.hasMaximizedGroup() || useDockviewStore.getState().preMaximizeLayout !== null) return; const sv = getRootSplitview(api); if (!sv || sv.length < 2) return; try { const sidebarW = sv.getViewSize(0); if (sidebarW > 50) { const current = useDockviewStore.getState().pinnedWidths.get("sidebar"); if (current !== sidebarW) { useDockviewStore.getState().setPinnedWidth("sidebar", sidebarW); } } if (sv.length >= 3) { const rightIdx = sv.length - 1; const rightW = sv.getViewSize(rightIdx); if (rightW > 50) { const current = useDockviewStore.getState().pinnedWidths.get("right"); if (current !== rightW) { useDockviewStore.getState().setPinnedWidth("right", rightW); } } } } catch { /* noop */ } } export function setupGroupTracking(api: DockviewReadyEvent["api"]): () => void { const d1 = api.onDidActiveGroupChange((group) => { useDockviewStore.setState({ activeGroupId: group?.id ?? null }); }); useDockviewStore.setState({ activeGroupId: api.activeGroup?.id ?? null }); const d2 = api.onDidLayoutChange(() => trackPinnedWidths(api)); trackPinnedWidths(api); return () => { d1.dispose(); d2.dispose(); }; } export function setupLayoutPersistence( api: DockviewReadyEvent["api"], saveTimerRef: React.MutableRefObject | null>, sessionIdRef: React.MutableRefObject, ): void { api.onDidLayoutChange(() => { if (useDockviewStore.getState().isRestoringLayout) return; if (saveTimerRef.current) clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(() => { try { const json = api.toJSON(); localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(json)); const sid = sessionIdRef.current; if (sid) { setSessionLayout(sid, json); } } catch { // Ignore serialization errors } }, 300); }); } export function setupPortalCleanup( api: DockviewReadyEvent["api"], appStore: StoreApi, ): void { api.onDidRemovePanel((panel) => { if (useDockviewStore.getState().isRestoringLayout) return; const isMax = useDockviewStore.getState().preMaximizeLayout !== null; const remaining = api.panels.filter((p) => p.id !== panel.id); const nonSidebar = remaining.filter((p) => p.api.component !== "sidebar"); // If we're in maximize mode and the last non-sidebar panel was just closed, // exit maximize to restore the pre-maximize layout (avoids empty view). // Then remove the closed panel from the restored layout so it doesn't reappear. if (isMax && nonSidebar.length === 0) { const removedId = panel.id; requestAnimationFrame(() => { useDockviewStore.getState().exitMaximizedLayout(); // exitMaximizedLayout schedules a rAF to finalize — wait for that, then // remove the panel that was closed (it was re-created from preMaximizeLayout). requestAnimationFrame(() => { const restoredPanel = api.getPanel(removedId); if (restoredPanel) { restoredPanel.api.close(); } }); }); } const entry = panelPortalManager.get(panel.id); // vscode is session-scoped so entry.sessionId is always set by PortalSlot. if (entry?.component === "vscode" && entry.sessionId) stopVscode(entry.sessionId); // terminal is global (no entry.sessionId) — fall back to the active session. if (entry?.component === "terminal") { const terminalSessionId = entry.sessionId ?? appStore.getState().tasks.activeSessionId; const terminalId = entry.params.terminalId as string | undefined; if (terminalId && terminalSessionId) stopUserShell(terminalSessionId, terminalId); } panelPortalManager.release(panel.id); }); }