Add service worker that enables full offline launch and persistent
caching of large Moonshine voice model assets (136MB .data file).
- sw.js: stale-while-revalidate for app shell (HTML, JS, CSS, fonts);
cache-first for sherpa model files and ghostty WASM; old shell caches
deleted on SW activate to reclaim storage after updates
- server.go: /sw.js route injects current staticAssetCacheBust version
into SW content so browser detects new SW on each deploy; new SW
pre-caches versioned terminal.js during install so update is ready
before next launch; SW registration added to dashboard HTML
- terminal.ts: register /sw.js on load; detect navigator.onLine at
startup; listen for online/offline events; pause reconnect loop when
offline instead of exhausting attempts; resume with reset attempts
when network returns; show "Offline. Will reconnect..." instead of
misleading WebSocket error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The mobile keybar was a small floating overlay that obscured terminal
content. Now it's a full-width bar docked to the bottom using flexbox
layout, so the terminal shrinks to accommodate it. Buttons use 44px
height (Apple HIG recommended touch target) and evenly fill the width.
Also uses 100dvh for correct mobile viewport sizing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two issues prevented the bell emoji from clearing reliably:
1. Race on tab return: clearBellState() runs on visibilitychange/focus,
but the WebSocket has buffered output from while the tab was hidden.
The terminal processes it immediately, onBell fires, and the bell is
right back. Fixed with a 1-second suppression window after clearing —
bells from buffered output are ignored.
2. Bell stays when clicking without typing: the bell only cleared on
onData (keyboard input). If you clicked the terminal or just switched
back without typing, the emoji persisted. Added a mousedown listener
on the terminal element to clear it on any mouse interaction.
The orange bell outline on tiles could get stuck because:
1. The terminal tab clears its bell localStorage entry on focus/keypress,
but the 'storage' event only fires across tabs — the dashboard in the
same browser never sees the removal.
2. The only way to clear the bell class was to click the tile to open it
(openTile → clearBellState). Simply switching back to the dashboard
tab never re-checked localStorage.
Now onDashboardFocusChanged() calls syncAllBellStates() which re-reads
localStorage for every tile and toggles the bell class accordingly,
clearing stale orange outlines as soon as the dashboard becomes visible.
Two issues caused stale thumbnails on the dashboard:
1. Returning to the dashboard tab only processed the (empty) refresh
queue — it never triggered a full refresh. Now, if the dashboard was
hidden for more than 3 seconds, all ETags are cleared and refreshAll()
is called so every tile fetches a fresh screenshot.
2. If SSE activity events were missed (e.g. during reconnect), tiles
would never update until the next terminal activity. Added a 10s
periodic fallback that calls refreshAll() when the dashboard is
visible, ensuring tiles stay reasonably current even without SSE.
Input tarpitting fixes:
- terminal.ts: batch stdin writes with 10ms coalescing window (flushes
immediately for large payloads like paste), replacing per-keystroke
WebSocket messages with fewer, larger frames
- server.go: replace per-message time.After() with a reusable timer to
eliminate GC pressure from repeated key input
- server.go: coalesce queued stdin writes (up to 4KB) into a single PTY
write to reduce syscall overhead
Screenshot/rendering pipeline optimizations:
- tracker.go: stop forcing empty cells to space; let exporters decide
what to render, drastically reducing work for mostly-blank terminals
- tracker.go: use clear() for dirty map instead of delete-in-loop
- svg_exporter.go: skip visually empty cells (blank glyph, default BG,
no reverse/underline); still render background rects for colored or
reverse-video cells
- png_exporter.go: add color parsing cache to avoid redundant hex
parsing per cell; add empty cell fast-path; short-circuit blend math
for coverage 0 and 255
Dashboard thumbnail concurrency:
- server.go: replace single-flight screenshot fetching with limited
parallelism (2-4 concurrent requests based on hardwareConcurrency)
so large dashboards with many tiles update faster
Also fixes typo in dashboard JS (tetagBySlug -> etagBySlug) that
silently broke ETag caching for screenshot refreshes.
Bumps version to 1.3.32.
PNG screenshots are now the default for dashboard previews,
with SVG available by setting WEBTERM_SCREENSHOT_MODE=svg.
Documentation and tests updated accordingly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dashboard now always downloads SVG on right-click even when
PNG thumbnails are enabled, while keeping SVG as the default mode
in the docs and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PNG screenshots are now gated by WEBTERM_SCREENSHOT_MODE.
The dashboard selects SVG by default and switches to PNG when enabled,
with ETag caching and eviction for both formats.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a tab is hidden, buffer inbound output and stop the heartbeat.
On visibility restore, drop the hidden buffer and reconnect to replay a clean
server state, while guarding against stale socket events.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two bugs introduced in the idle tracker pause commit:
1. handleOutput() short-circuited entirely when idle, skipping
connector.OnData() — so terminal output never reached clients
even after reconnecting. Fix: still call connector.OnData()
during idle; only skip the expensive tracker.Feed().
2. When a WebSocket client reconnects to an existing session,
handleWebSocket never called UpdateConnector(), so the idle
flag was never cleared and the tracker stayed paused. Fix:
create a fresh connector and call session.UpdateConnector()
on reconnect, which clears idle and rebuilds the tracker.
When no WebSocket client is connected, the session's readLoop still
processes every byte of terminal output through the go-te VT parser
(tracker.Feed), Screen.Draw grapheme segmentation, and string
allocations — even though nobody is consuming the screen state.
For programs like btop inside tmux that produce continuous full-screen
redraws, this causes sustained CPU usage and GC pressure over hours.
Fix: after a 10-second idle threshold (no client connected), skip
tracker.Feed() and only maintain the replay buffer. When a client
reconnects (UpdateConnector) or a screenshot is requested
(GetScreenSnapshot), rebuild the tracker by replaying the buffer
through a fresh VT parser instance.
Changes:
- Add idleSince atomic timestamp + MarkIdle() to Session interface
- handleOutput() skips tracker.Feed when idle > threshold
- UpdateConnector() clears idle flag and rebuilds tracker from replay
- GetScreenSnapshot() rebuilds stale tracker on-demand for screenshots
- Wire MarkIdle() call into handleWebSocket cleanup (client disconnect)
- Add TestIdleTrackerPauseAndRebuild covering the full lifecycle
- Add ?v=VERSION cache-busting query params to terminal page static URLs
(monospace.css, terminal.js) so Safari serves fresh assets after upgrades
- Add Cache-Control: no-cache header to terminal HTML page response
- Fix WebTerminal.setTheme() to assign through terminal.options proxy,
triggering handleOptionChange which updates renderer AND re-renders
(previously bypassed the proxy and only called renderer.setTheme)
- Remove 40+ debug console.log statements from investigation phase,
keeping only warnings/errors and essential lifecycle messages