The features here were all merged to airc and extended further.
Swyd mirrors a tmux session, read-only, into a web page. The target viewer is the Tesla browser; the terminal session runs on the laptop sitting in the car (driven by voice via dais). There is deliberately no browser-to-terminal input path.
Tesla browser -> ngrok HTTPS dev domain -> laptop localhost server -> tmux
The server follows the active pane of one tmux session (a specific pane can be
pinned from the viewer), converts tmux capture-pane -e output to HTML
server-side, and the page polls it with ETag/304 suppression and an adaptive
interval. The server also spawns and supervises the ngrok agent as a child
process, so one systemd unit is the whole system.
tools/swyd on # start unit, wait for tunnel, print the bookmark URL
tools/swyd off # stop everything (server + ngrok child); nothing stays running
tools/swyd status # one-line status; --json for machines (dais)
tools/swyd url # print the bookmark URL without starting anythingOne-time install of the user unit:
systemctl --user link /home/k/swyd/tools/swyd.service
systemctl --user daemon-reloadLogs: journalctl --user -u swyd -f.
The Tesla bookmark is the URL printed by swyd on/swyd url:
https://<domain>/?k=<token>. The token persists as a cookie after the first
visit; the bookmark re-authenticates with zero typing even when the cookie is
gone. Everything except fonts/static assets answers plain 404 without the
token. /healthz is open to local loopback only (for dais-top), and requires
the token through the tunnel.
config.json (gitignored; see config.example.json for all keys with their
defaults). The auth token is generated and written there on first run.
{
"session": "swyd-test",
"port": 8080,
"resizeToViewport": false,
"ngrok": { "enabled": true, "domain": "carrousel-value-recipient.ngrok-free.dev" }
}CLI flags override the config per-invocation:
node src/server.js --session app --no-ngrok --port 8090 # local dev
node src/server.js --print-url # bookmark URL, then exit
yarn dev # server without tunnelresizeToViewport (opt-in): the viewer reports its cols/rows and the server
runs tmux resize-window to match the car screen exactly. Off by default
because it reshapes the same session shown on the laptop. The default sizing
is client-side: the page scales the font so the captured pane fits the
viewport (Fit), with manual A−/A+ override.
/viewer: terminal + status strip (live/idle/stale dot, pane label → pane picker overlay, font controls, day/night theme, pause)./probebrowser capability probe (kept for in-car diagnostics)./api/tmux/frameone frame as JSON (?pane=%5pins a pane); supportsIf-None-Match/304./api/tmux/paneslists panes./api/configviewer settings./healthzrich status JSON (tunnel URL, last-capture age, viewers in the last minute).
Note lastCaptureAgeMs in /healthz grows when no viewer is polling -
capture is request-driven, so "old but ngrok up" means no viewer, not
broken.
Node >= 20, dependency-free (built-in modules only). tmux >= 3.x, ngrok agent
on PATH (~/bin/ngrok).
yarn check # node --check over all server/client JS + inline scripts in public/*.htmlLayout: src/server.js (routes, lifecycle), src/tmux.js (capture, pane
resolution - note tmux exits 0 with empty fields for unknown targets, the
empty pane id is the "not found" signal), src/ansi.js (SGR -> HTML:
16-color palette as themable CSS classes, 256/truecolor inline, bold/dim/
italic/underline/reverse), src/auth.js, src/ngrok.js (child supervision,
URL discovery via the agent API on :4040), src/config.js. Viewer in
public/ (index.html, app.js, app.css).
More complete context for humans and fresh-context coding agents lives in
docs/: project brief, architecture, operations, Tesla
browser findings, implementation notes, and future work.
A color-cycling test session for poking at the viewer:
tmux new -d -s swyd-test -x 100 -y 30 \
'while true; do printf "\033[32mgreen\033[0m \033[38;5;208m256\033[0m \033[38;2;255;105;180mrgb\033[0m\n"; date; sleep 2; done'Network:
- Direct hotspot LAN access did not work from Tesla (
10.204.192.28:8080); the same URL worked from laptop and phone. Tesla appears to block hotspot-local/private addresses, at least10.0.0.0/8. - Cloudflare TryCloudflare worked; ngrok preferred because the assigned free dev domain is a stable bookmark.
- ngrok free tier: interstitial on first visit (cookie expires after ~1 week in Firefox), and only one agent session - a stray manually started ngrok blocks the supervised child (its error is logged verbatim).
Browser (Chrome 140-based, X11 UA):
- Viewport (fixed, confirmed in car): 1180x919 css px,
devicePixelRatio: 1(physical == css, no scaling). Fullscreen-stretched in Park is 1920x1089 (available 1920x1200) but still shows a location bar — not true fullscreen. fetch, WebSocket-through-ngrok, EventSource, cookies, and localStorage all work.navigator.language= fi.
Rendering:
- Tesla's generic
monospaceandCourier Newstacks do NOT render fixed-width; the bundled Fira Code (public/fonts/) renders aligned. The terminal must use the web font.
Confirmed in car (2026-06-12):
- Stays live while driving, not just in Park — minor latency, works well.
- Blinking cursor is visible and correctly positioned.
- Pane switching from the top pane-picker works well.
Still open — text sizing / pane fit:
- A quarter-size Konsole window fit the car screen well; a full half window was too much (too many cols/rows → client auto-fit shrinks the font too small). Raising the Konsole font (fewer cols/rows) helped.
- The lever is the source pane's cols/rows, not just font scaling. Likely
direction: the opt-in
resizeToViewportor a target cols/rows cap. Being tested before committing to an approach.
Polling at 1 Hz (stretching to pollIdleMaxMs after unchanged frames, 304s
in between) is simple and tunnel-friendly for a passive viewer. WebSocket
verified working through ngrok and can replace polling later as an
optimization, keeping polling as the fallback.
