The mirror looked broken because a backgrounded tab on one device still pinned the shared PTY to its (default 80x24) size — so the device actively viewing got a cramped terminal. Fix: only an ACTIVELY-VIEWING client votes on PTY size. - attachWs no longer seeds a default size vote (a join may be a hidden mirror) - new 'blur' client message + clearClientDims(): a tab going hidden withdraws its size vote (still mirrors output); show() re-casts it - PTY size = min cols/rows across clients that have actually reported dims - session/protocol tests cover hidden-mirror-doesn't-clamp + blur Session manager (separate page, for the 'too many sessions' problem): - GET stays; add DELETE /live-sessions/:id and DELETE /live-sessions[?detached=1] - manager.killById(); LiveSessionInfo gains cols/rows - public/manage.html + manage.ts: list/open/kill sessions, kill-all / kill-detached, auto-refresh; 🗂 toolbar button; bundled as a 2nd esbuild entry Verified: two concurrent clients mirror output + shared input; manage page lists/kills (3→2→0). 225 tests green, tsc clean.
92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
/**
|
|
* 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'
|
|
|
|
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=<id>) 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.
|
|
mountKeybar((data) => app.sendToActive(data))
|
|
|
|
// 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())
|
|
|
|
// Session manager (standalone page) — manage/kill the host's running sessions.
|
|
const manageBtn = document.createElement('button')
|
|
manageBtn.className = 'toolbtn'
|
|
manageBtn.textContent = '🗂'
|
|
manageBtn.title = 'Manage sessions (open / kill)'
|
|
manageBtn.setAttribute('aria-label', 'Manage sessions')
|
|
manageBtn.addEventListener('click', () => {
|
|
location.href = '/manage.html'
|
|
})
|
|
toolbar.appendChild(manageBtn)
|
|
|
|
mountQrConnect(toolbar)
|
|
|
|
// 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.
|
|
})
|
|
})
|
|
}
|