Port preview — click the URL your agent just printed
Auto-detected dev-server ports on any bound host become clickable previews served through kolu's own origin — TCP bytes riding the existing padi wire, so the two-hop topology needs zero new listeners.
The feature, in plain words. Agents start dev servers constantly and print the URL (localhost:3000). Today that URL is dead text if the terminal lives on a remote host. After this: padi notices the port, a chip appears on the terminal, and clicking it opens the running app — served through the same kolu origin you’re already on, no matter which machine the server actually runs on. (Codespaces/VS Code prove the demand — they auto-detect and forward console URLs; the agent era makes it sharper, because agents print URLs far more often than humans type ssh -L. See the future-work survey.)
The two-hop question, answered
The hard-looking case: kolu-server runs on a headless box you reach remotely, and it binds a padi on a third machine over ssh. The dev server listens on localhost:3000 on that third machine — your browser is two networks away. Doesn’t forwarding take two hops? Yes — and both hops already exist as open pipes. That’s the whole design:
- HOP A (browser → kolu-server) is the normal kolu origin — the HTTPS/WS connection the tab already has. The preview URL is a path on kolu’s own origin (
/preview-port/<host>/<port>/…), so auth is the origin gate that guards everything else, and there is no mixed-content or third-party-origin problem. - HOP B (kolu-server → the dev server) rides the existing padi wire: a new byte-stream member on the padi surface (“dial
localhost:<port>on your host and pipe bytes”) — the exact machinery classterminalAttachalready uses for PTY bytes, and the same read-through-the-bound-session shape the file-preview route shipped in W4. padi is on the target host, so its dial is loopback.
Why this shape beats the alternatives
- A second ssh channel (
ssh -L-style) — rejected. It re-enters ssh config/auth per forward, needs port allocation management on the middle box, silently breaks if the padi transport is ever not-ssh (the local arm already isn’t), and opens a listener on the headless box that the origin gate doesn’t guard. - Public tunnel services (cloudflared/ngrok class) — rejected. An external dependency that publishes your dev server to the internet to show it to yourself. Kolu’s topology already has a private path.
- The chosen shape needs no new listeners, no new auth surface, no per-forward setup, and works uniformly for the local arm (padi dials loopback directly — the degenerate one-hop case falls out for free).
The three pieces
- Detection (padi). A PTY-output sensor for URL/port patterns (padi already runs output sensors for agent state), publishing a per-terminal
portscell —{ port, url, firstSeenAt, terminalId }, value-typed per the forwarding policy. Agents print URLs; humans get the same chip when they start servers by hand. - Transport (padi surface + kolu-server). The
dialPortbyte-stream member (fail-through on link drop, per the byte-stream policy — a dropped preview reconnects end-to-end, never replays stale bytes), and kolu-server’s reverse-proxy route streaming request/response bodies over it. WebSocket upgrades tunnel the same way (the proxy speaks the upgrade; the bytes don’t care). - UI (client). A port chip on the terminal tile (and the dock row), click = open
/preview-port/<host>/<port>/in a tab or the preview panel. The chip carries the host name — the same per-host honesty rules as everything post-switch.
Honest limits, named up front
- Path-rewriting is out of scope. Apps that emit absolute URLs or hardcode their origin may misbehave under a sub-path proxy — the same limit Codespaces has; the escape hatch is opening the preview in its own tab where relative apps just work. No HTML rewriting middleware (that way lies a proxy engine).
- This is a dev preview, not ingress. Byte-stream framing over the surface wire costs more than a raw socket; fine for previews, not a production tunnel — and deliberately so (no silent graduation into infrastructure).
- W4’s per-subresource caveat applies (the preview route’s
?hostfallback): the proxy path embeds the host in the path, not the query, precisely so relative subresources inherit it. This fixes the H4 class rather than inheriting it.
Sequencing: single-host (local arm) works standing alone and is the natural first PR; the remote leg is the same members over the existing wire. Constraint ledger of the future-work note applies (notably: the wire stays per-feature-bounded — a preview stream is opened on click, never held warm per host).