// Per-device persistence of the multi-view layout (issue #34). The layout tree // is plain data, so it serialises straight to localStorage. On load we rebuild // with fresh node ids (the id counter resets each boot) and defensively drop // anything malformed — a corrupt or stale blob falls back to a single empty tab. // // Sessions that no longer exist when a layout is restored are not pruned here // (the session list isn't known yet at boot); their panes attach, the daemon // closes the dead socket, and the pane auto-closes (see ConnectionPool.onClosed). import { freshId, type Layout, type Node, type Tab } from "./layout.js"; const KEY = "maestro-layout-v1"; const VERSION = 1; export function saveLayout(layout: Layout): void { try { localStorage.setItem(KEY, serializeLayout(layout)); } catch { // Storage full or unavailable (private mode) — persistence is best-effort. } } export function serializeLayout(layout: Layout): string { return JSON.stringify({ v: VERSION, layout }); } export function loadLayout(): Layout | null { let raw: string | null; try { raw = localStorage.getItem(KEY); } catch { return null; } if (!raw) return null; try { const parsed = JSON.parse(raw) as { v?: number; layout?: unknown }; if (parsed.v !== VERSION) return null; return hydrate(parsed.layout); } catch { return null; } } export function hydrate(raw: unknown): Layout | null { if (!raw || typeof raw !== "object") return null; const r = raw as { tabs?: unknown; activeTabId?: unknown }; if (!Array.isArray(r.tabs) || r.tabs.length === 0) return null; const tabs: Tab[] = []; let activeTabId = ""; for (const rt of r.tabs as Array>) { const id = freshId("t"); let focusedLeafId: string | null = null; const root = hydrateNode(rt.root, rt.focusedLeafId, (fid) => { focusedLeafId = fid; }); tabs.push({ id, title: typeof rt.title === "string" ? rt.title : `Tab ${tabs.length + 1}`, root, focusedLeafId, }); if (rt.id === r.activeTabId) activeTabId = id; } if (!activeTabId) activeTabId = tabs[0].id; return { tabs, activeTabId }; } function hydrateNode( raw: unknown, oldFocused: unknown, onFocus: (newId: string) => void, ): Node | null { if (!raw || typeof raw !== "object") return null; const n = raw as Record; if (n.kind === "leaf") { const id = freshId("l"); if (n.id === oldFocused) onFocus(id); return { kind: "leaf", id, sessionId: typeof n.sessionId === "string" ? n.sessionId : null }; } if (n.kind === "split" && (n.dir === "row" || n.dir === "col")) { const a = hydrateNode(n.a, oldFocused, onFocus); const b = hydrateNode(n.b, oldFocused, onFocus); if (!a) return b; if (!b) return a; const ratio = typeof n.ratio === "number" && n.ratio > 0.05 && n.ratio < 0.95 ? n.ratio : 0.5; return { kind: "split", dir: n.dir, a, b, ratio }; } return null; }