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
Frontend terminal:
- Share single TextDecoder across all instances (was one per terminal)
- Write binary WS frames as Uint8Array directly to ghostty-web,
avoiding intermediate string allocation from TextDecoder.decode()
- Reduce default scrollback to 200 on mobile (was 1000; Safari mobile
has ~300MB budget across all tabs)
- Pause heartbeat watchdog when tab is hidden to reduce WS traffic
and message queue growth for background terminals
Dashboard screenshots:
- Revoke old object URL before creating new one (reduces peak memory
by not having two decoded bitmaps alive simultaneously)
- Re-lookup card from cardsBySlug in fetch callback to avoid orphaned
blob URLs when renderTiles() runs during in-flight requests
- Remove thumbnailCache writes (was redundant with activeObjectURLBySlug)
Server:
- Halve replay buffer from 256KB to 128KB per session
Browser-side:
- Sparkline images now use blob object URLs with explicit revocation
instead of cache-busted img.src URLs that accumulate in Safari's
internal image cache
- renderTiles() now clears thumbnailCache, sparkline object URLs,
pending refresh timers, and etag cache on re-render
- Prevents unbounded growth of dashboard state objects
Server-side:
- Add periodic eviction of stale screenshotCache entries (every 60s,
removes entries older than maxScreenshotCacheTTL) to prevent
unbounded server memory growth from cached SVG strings
Two issues prevented themes from being applied:
1. Three server-side themes (miasma, github, gotham) were missing from
the frontend THEMES map in terminal.ts, causing the browser to
silently ignore them when set via data-theme attribute.
2. Landing YAML only recognized the 'theme' key, but users writing
'webterm-theme' (matching the Docker label convention) got no theme.
Now LoadLandingYAML accepts both 'theme' and 'webterm-theme' keys.
All 16 themes are now consistent across ThemeBackgrounds, ThemePalettes,
and the frontend THEMES map.
Shells (e.g. via Tera Term conventions) emit CSI ?7727h/l to toggle
Application Escape Key mode, which ghostty-web does not implement.
This produces noisy console warnings in the browser.
Strip these sequences server-side in the output pipeline (both
TerminalSession and DockerExecSession) before they reach the client,
using the same pattern as the existing DA response filter.
- Share single Ghostty WASM instance across all terminals instead of
loading a new one per tab (major memory savings on Safari)
- Track all window/document event listeners and remove them on dispose()
- Store and disconnect ResizeObserver on dispose()
- Clear resize debounce timer on dispose()
- Remove injected mobile keybar <style> element on dispose()
- Null out socket and clear message queue on dispose()
- Use addTrackedListener() helper for automatic cleanup registration
- Cap message queue at 1000 entries; trim oldest on overflow in send()
- Add per-instance 30s cleanup timer that trims stale queued messages
- Add module-level 30s sweep that disposes terminal instances whose
DOM containers have been removed (el.isConnected === false)
- Wire cleanup timer start/stop into initialize() and dispose()
- Add full 16-color palettes for tango and ristretto (previously background-only)
- Fix ristretto background from #2d2525 to #2c2525 per Monokai Pro Ristretto source
- Rename monokai to monokai-pro to reflect it is the Monokai Pro variant
- Add classic monokai theme (bg #272822) from Monokai Classic
- Import three new themes: miasma, github (dark dimmed), gotham
- All 16 themes now have complete entries in both ThemeBackgrounds and ThemePalettes
- Update terminal.ts frontend theme map to match