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
Update ghostty-web to b0aa99e which wraps the requestAnimationFrame
render loop in try/catch. Previously, any exception in renderer.render()
or wasmTerm.getCursor() would silently kill the loop, permanently
freezing the terminal canvas while input continued to flow normally.
The fix ensures requestAnimationFrame is always re-scheduled and logs
errors to console for diagnosis. This addresses intermittent terminal
stalls that were not correlated with resize, focus, or user interaction.
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>
Address issue #3 by hardening the terminal WebSocket client against silent receive stalls seen in Firefox.
Changes:
- Added a client heartbeat watchdog that sends periodic ping messages.
- Detects stale inbound streams and forces reconnect when no inbound data/pong is seen within the timeout window.
- Added Blob WebSocket message handling for improved cross-browser receive compatibility.
- Ensures heartbeat timers are started/stopped with socket lifecycle and terminal disposal.
Validation:
- Rebuilt frontend bundle via make build-fast.
- Ran make check successfully.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the README <video> source to use the GitHub raw media URL for docs/demo.mp4 so the demo renders inline instead of resolving to a blob page.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the static screenshot with an embedded demo.mp4 player in the README and add docs/demo.mp4 to the repository so the preview is available in-repo.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve GitHub issue #2 by aligning the Go module identity with the repository path so works.
Changes made:
- Updated go.mod module path from github.com/rcarmo/webterm-go-port to github.com/rcarmo/webterm.
- Updated all internal import references to the new module path.
- Updated version ldflags in Makefile and Dockerfile to use github.com/rcarmo/webterm/webterm.Version.
- Added README quick-install section documenting the command.
Validation:
- Ran make check successfully after the rename/import updates.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>