import { listSessions, saveSession, deleteSession, updateSessionTitle, touchSession, saveLayout, loadLayout, updateSessionGroup, type PersistedSession, } from '../adapters/session-bridge'; import { handleInfraError } from '../utils/handle-error'; export type LayoutPreset = '1-col' | '2-col' | '3-col' | '2x2' | 'master-stack'; export type PaneType = 'terminal' | 'agent' | 'markdown' | 'ssh' | 'context' | 'empty'; export interface Pane { id: string; type: PaneType; title: string; shell?: string; cwd?: string; args?: string[]; group?: string; focused: boolean; remoteMachineId?: string; } let panes = $state([]); let activePreset = $state('1-col'); let focusedPaneId = $state(null); let initialized = false; // --- Persistence helpers (fire-and-forget with error logging) --- function persistSession(pane: Pane): void { const now = Math.floor(Date.now() / 1000); const session: PersistedSession = { id: pane.id, type: pane.type, title: pane.title, shell: pane.shell, cwd: pane.cwd, args: pane.args, group_name: pane.group ?? '', created_at: now, last_used_at: now, }; saveSession(session).catch(e => handleInfraError(e, 'layout.persistSession')); } function persistLayout(): void { saveLayout({ preset: activePreset, pane_ids: panes.map(p => p.id), }).catch(e => handleInfraError(e, 'layout.persistLayout')); } // --- Public API --- export function getPanes(): Pane[] { return panes; } export function getActivePreset(): LayoutPreset { return activePreset; } export function getFocusedPaneId(): string | null { return focusedPaneId; } export function addPane(pane: Omit): void { panes.push({ ...pane, focused: false }); focusPane(pane.id); autoPreset(); persistSession({ ...pane, focused: false }); persistLayout(); } export function removePane(id: string): void { panes = panes.filter(p => p.id !== id); if (focusedPaneId === id) { focusedPaneId = panes.length > 0 ? panes[0].id : null; } autoPreset(); deleteSession(id).catch(e => handleInfraError(e, 'layout.deleteSession')); persistLayout(); } export function focusPane(id: string): void { focusedPaneId = id; panes = panes.map(p => ({ ...p, focused: p.id === id })); touchSession(id).catch(e => handleInfraError(e, 'layout.touchSession')); } export function focusPaneByIndex(index: number): void { if (index >= 0 && index < panes.length) { focusPane(panes[index].id); } } export function setPreset(preset: LayoutPreset): void { activePreset = preset; persistLayout(); } export function renamePaneTitle(id: string, title: string): void { const pane = panes.find(p => p.id === id); if (pane) { pane.title = title; updateSessionTitle(id, title).catch(e => handleInfraError(e, 'layout.updateTitle')); } } export function setPaneGroup(id: string, group: string): void { const pane = panes.find(p => p.id === id); if (pane) { pane.group = group || undefined; updateSessionGroup(id, group).catch(e => handleInfraError(e, 'layout.updateGroup')); } } /** Restore panes and layout from SQLite on app startup */ export async function restoreFromDb(): Promise { if (initialized) return; initialized = true; try { const [sessions, layout] = await Promise.all([listSessions(), loadLayout()]); if (layout.preset) { activePreset = layout.preset as LayoutPreset; } // Restore panes in layout order, falling back to DB order const sessionMap = new Map(sessions.map(s => [s.id, s])); const orderedIds = layout.pane_ids.length > 0 ? layout.pane_ids : sessions.map(s => s.id); for (const id of orderedIds) { const s = sessionMap.get(id); if (!s) continue; panes.push({ id: s.id, type: s.type as PaneType, title: s.title, shell: s.shell ?? undefined, cwd: s.cwd ?? undefined, args: s.args ?? undefined, group: s.group_name || undefined, focused: false, }); } if (panes.length > 0) { focusPane(panes[0].id); } } catch (e) { handleInfraError(e, 'layout.restoreFromDb'); } } function autoPreset(): void { const count = panes.length; if (count <= 1) activePreset = '1-col'; else if (count === 2) activePreset = '2-col'; else if (count === 3) activePreset = 'master-stack'; else activePreset = '2x2'; } /** CSS grid-template for current preset */ export function getGridTemplate(): { columns: string; rows: string } { switch (activePreset) { case '1-col': return { columns: '1fr', rows: '1fr' }; case '2-col': return { columns: '1fr 1fr', rows: '1fr' }; case '3-col': return { columns: '1fr 1fr 1fr', rows: '1fr' }; case '2x2': return { columns: '1fr 1fr', rows: '1fr 1fr' }; case 'master-stack': return { columns: '2fr 1fr', rows: '1fr 1fr' }; } } /** For master-stack: first pane spans full height */ export function getPaneGridArea(index: number): string | undefined { if (activePreset === 'master-stack' && index === 0) { return '1 / 1 / 3 / 2'; } return undefined; }