import type { Component, TUI } from "@earendil-works/pi-tui"; // Store on the terminal, not a module singleton: extension loaders may isolate // modules, while Pi keeps this terminal across regular/fullscreen transitions. const STATE = Symbol.for("gentle-pi.experimental-sidebar.state"); /** In-process notification after a visual preference is persisted. */ export const VISUAL_SETTINGS_CHANGED = "gentle-pi.visual-settings-changed"; export interface SidebarState { active: boolean; visibility?: { todo?: boolean }; ownsHost?: () => boolean; /** * Columns the fullscreen rail reserves at the right edge (rail plus gap) * while `active && ownsHost()`. Overlays read it to stay inside the editor * column (the prompt-history picker's right margin) without importing * gentle-shell. */ railColumns?: number; /** True while Status placement is "hidden": the bottom Status bar paints nothing at any width or mode. */ statusHidden?: () => boolean; /** True while a painting top header is the only status row of a narrow fullscreen terminal: the bottom Status bar steps aside. */ headerOwnsStatus?: () => boolean; parts: Map; } /** A rail slot in the fullscreen sidebar. */ export interface SidebarRail extends Component { /** * Cheap digest of the live state this rail paints. The fullscreen layout memo * re-renders a part only when its digest changes, so a rail that reads session * data (model, thinking level, context, cost, extension statuses) must declare * one; explicit invalidateSidebar() stays for discrete state changes. */ digest?(): string; } export function sidebarState(tui: TUI): SidebarState { const terminal = tui.terminal as unknown as Record; return terminal[STATE] ??= { active: false, parts: new Map() }; } /** Keep the original bottom component mounted, suppressing only its paint. */ export function sidebarPart(tui: TUI, key: string, bottom: T, rail: SidebarRail = bottom): T { // Minimal extension hosts cannot share terminal-owned layout state. if (!tui.terminal) return bottom; const state = sidebarState(tui); state.parts.set(key, rail); return { ...bottom, render: (width: number) => (key === "todo" && state.visibility?.todo === false) || (key === "footer" && (state.statusHidden?.() || state.headerOwnsStatus?.())) || (state.active && state.ownsHost?.()) ? [] : bottom.render(width), dispose() { if (state.parts.get(key) === rail) state.parts.delete(key); bottom.dispose?.(); }, }; } /** * Register the fullscreen header rail: the one row above the hstack that * carries the brand, session identity, and the per-frame counters. There is * no narrow-mode bottom counterpart — the compact bar already carries this * data when the sidebar is inactive — so this only ever writes the "header" * slot in sidebarState(tui).parts, and returns its own disposer. */ export function sidebarHeader(tui: TUI, rail: SidebarRail): () => void { if (!tui.terminal) return () => {}; const state = sidebarState(tui); state.parts.set("header", rail); return () => { if (state.parts.get("header") === rail) state.parts.delete("header"); }; }