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>
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
Close retired websocket connections in stopWSClient so clients reconnect promptly instead of remaining in a stdin-only state with no returning output. Add regression coverage to verify stopWSClient actively disconnects the websocket.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace silent output frame dropping with fail-fast slow-client disconnects when websocket send queues saturate, and replace unbounded stdin write goroutine spawning with a bounded queue + worker and timeout-driven disconnect under input backlog. Also add targeted regression tests for queue saturation and stdin backlog disconnect behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Guard route client cleanup against stale websocket/session close paths so newer connections keep receiving output after focus/reconnect transitions. Also refresh websocket read deadlines on every inbound message and add regression tests covering both reconnect and stale-session-close race scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce a dashboard workflow to export screenshot SVGs without external font URL references.
Server changes:
- Added screenshot query flags: sanitize_font_urls=1 and download=1.
- Added SVG sanitization that strips @font-face src:url(...) for the vendored font path.
- Added safe filename normalization for download responses.
- Added Content-Disposition attachment support for downloadable screenshot exports.
- Preserved existing cache behavior while computing ETags from the actual response variant.
Dashboard changes:
- Added tile contextmenu handler (right-click) to trigger sanitized SVG download per tile.
- Download URL includes cache-busting timestamp to avoid stale browser downloads.
Tests:
- Added coverage for sanitized download response (attachment header + font URL removal).
- Added coverage asserting dashboard HTML includes right-click sanitized download wiring.
- Validated with make format && make check.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ensure dashboard screenshot SVGs consistently use the vendored Fira Nerd font stack used by the terminal.
Changes:
- Added dashboard inline CSS declarations for @font-face (FiraCode Nerd Font/FiraMono Nerd Font) and --webterm-mono in the dashboard HTML template.
- Updated RenderTerminalSVG styling to use font-family: var(--webterm-mono, ...) with the full Nerd-font-aware fallback stack.
- Embedded matching @font-face declarations inside generated SVG style blocks so externally loaded SVG images can resolve the vendored font without relying on parent page CSS inheritance.
Result:
- Dashboard screenshot tiles and search thumbnails retain full glyph/icon coverage with consistent typography across environments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address intermittent stalls seen after many hours by improving liveness detection and failure handling for both streaming channels.
WebSocket changes:
- Added periodic server ping frames and a read deadline refreshed by pong replies.
- On sender write/ping failure, explicitly close the underlying connection so clients promptly observe disconnect and reconnect instead of remaining half-open.
SSE changes:
- Excluded /events from gzip middleware and added X-Accel-Buffering: no to reduce proxy buffering risk.
- Stop the SSE loop on write errors for activity/keepalive frames so dead subscribers are cleaned up immediately.
Tests:
- Added regression coverage for gzip bypass on /events.
- Added regression coverage ensuring SSE handler exits and unsubscribes on write failure.
- Verified with make format && make check.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>