/** * public/main.ts — esbuild entry point for the browser frontend. * * Bootstraps the multi-tab app: a tab bar (#tabs) + a utility toolbar (#toolbar) * over a stack of terminal panes (#term), each tab an independent TerminalSession. * The mobile key bar routes input to the active tab. * * Per-session terminal/WS/reconnect logic lives in terminal-session.ts; * tab management + persistence in tabs.ts. */ // CSS side-effect import (esbuild → public/build/main.css; typed via css.d.ts). import '@xterm/xterm/css/xterm.css' import { mountKeybar } from './keybar.js' import { TabApp } from './tabs.js' import { mountSearch } from './search.js' import { mountQrConnect } from './qr.js' import { mountSettings, loadSettings, type Settings } from './settings.js' import { mountDashboard } from './dashboard.js' import { mountHistory } from './history.js' import { mountShortcuts } from './shortcuts.js' import { mountShareSession } from './share.js' import { mountGridToggle, matchFocusCycleKey } from './grid-layout.js' import { mountGridPresets } from './grid-presets.js' import { mountDigest } from './digest.js' const paneHost = document.getElementById('term') const tabs = document.getElementById('tabs') const toolbar = document.getElementById('toolbar') if (!paneHost) throw new Error('#term element not found in DOM') if (!tabs) throw new Error('#tabs element not found in DOM') if (!toolbar) throw new Error('#toolbar element not found in DOM') const app = new TabApp(paneHost, tabs) // v0.4: a share link (?join=) opens/focuses that shared session, then we // strip the param so a refresh doesn't keep re-opening it. const joinId = new URLSearchParams(location.search).get('join') if (joinId) { app.openSession(joinId) history.replaceState(null, '', location.pathname) } // Apply saved theme/font (M3) to the restored tabs. let settings: Settings = loadSettings() app.applySettings(settings) // Key bar (mobile + desktop) sends to whichever tab is active. onVoiceTrigger // activates the full voice path (dictation + context-gated confirm/reject, VC) // and un-dormants the 🎤 button (createVoiceInput / matchCommand are otherwise // unreachable dead code without this wiring). mountKeybar((data) => app.sendToActive(data), { onVoiceTrigger: (action) => app.handleVoiceTrigger(action), }) // Multi-device: when this device regains focus, re-assert the size of every // VISIBLE pane so a shared session snaps to THIS screen (latest-writer-wins) — // full-screen on whichever device you're using (all quadrants in a split grid). window.addEventListener('focus', () => app.refitVisible()) document.addEventListener('visibilitychange', () => { if (!document.hidden) app.refitVisible() }) // v2: Ctrl+` cycles the focused quadrant in a split grid (Ctrl+Shift+` reverses). // Capture phase so xterm doesn't swallow it; left untouched (no preventDefault) // in single mode so the terminal keeps the keystroke. document.addEventListener( 'keydown', (e) => { const dir = matchFocusCycleKey(e) if (dir === null) return if (app.getGridLayout() === 'single') return // let the terminal keep the key e.preventDefault() app.cycleFocus(dir) }, { capture: true }, ) // Toolbar utilities. mountSearch(toolbar, { find: (query, dir) => app.findInActive(query, dir), clear: () => app.clearActiveSearch(), }) mountSettings( toolbar, () => settings, (s) => { settings = s app.applySettings(s) }, ) mountDashboard(toolbar, { snapshot: () => app.snapshot(), focus: (idx) => app.focusTab(idx), }) mountHistory(toolbar, { resume: (cwd, id) => app.newTabForResume(cwd, id), }) mountShortcuts(toolbar) mountShareSession(toolbar, () => app.activeSessionId()) // Split-grid layout toggle (desktop-only; hidden below GRID_MIN_WIDTH). The // control drives app.setGridLayout and is registered back so a programmatic // layout change (e.g. auto-fallback on window narrow) re-syncs its pressed state. const gridToggle = mountGridToggle(toolbar, { getLayout: () => app.getGridLayout(), setLayout: (layout) => app.setGridLayout(layout), }) app.setGridToggle(gridToggle) // Saved layout presets (desktop-only, next to the layout toggle). mountGridPresets(toolbar, { current: () => app.gridArrangement(), apply: (preset) => app.applyGridPreset(preset), }) // Session management lives on the home Sessions chooser now (open / kill / // new), so the standalone /manage.html page was removed. mountQrConnect(toolbar) // W3(c): "while you were away" reconnect digest — one compact dismissible banner // summarising what finished / needs input / got stuck since this device's last // visit. Best-effort (no banner on fetch failure); advances its own last-seen. void mountDigest(document.body) // PWA: register the service worker (installable + offline shell, M4). if ('serviceWorker' in navigator) { window.addEventListener('load', () => { void navigator.serviceWorker.register('./sw.js').catch(() => { // SW registration is best-effort; the app works without it. }) }) }