Files
time/ventures/preview-sidecar/README.md
T

110 lines
5.1 KiB
Markdown

# preview-sidecar
A dependency-free localhost port watcher: it notices when you start a dev
server, opens a preview pane pointed at it, and can stamp a timestamped
screenshot receipt of what was actually on screen.
Inspired by [anthropics/claude-code#53997](https://github.com/anthropics/claude-code/issues/53997)
("Bring back local preview for agent-built apps"), closed as not planned.
This does not modify Claude Code or any agent CLI. It is a standalone
sidecar: a Python script you run next to whatever started the dev server.
## Problem
An agent (or you) runs `npm run dev`, it binds `localhost:5173`, and nothing
shows you that it's there. You tab-hunt for the port, open it by hand, and if
you want proof of what rendered, you take a screenshot yourself. This watches
a bounded list of common dev ports, opens a preview the moment one answers,
and gives you a one-command way to capture dated evidence of what it showed.
## Install
Requires Python 3.9+. No dependencies, no install step — copy `sidecar.py`
anywhere. The `screenshot` command additionally needs a Chromium-family
browser (Chrome, Chromium, or Edge) already installed on the machine; it is
not bundled.
```sh
# From the notplanned checkout:
python3 ventures/preview-sidecar/demo.py
python3 -m unittest discover -s ventures/preview-sidecar -p 'test_*.py' -v
```
For your own project, copy `sidecar.py` to a trusted location and run it
from there, e.g. alongside `npm run dev`.
## Usage
```sh
# Watch the default common dev ports (3000, 5173, 8080, ...) and auto-open a
# preview pane the moment one starts answering.
python3 sidecar.py watch
# Watch specific ports or a range instead.
python3 sidecar.py watch --ports 3000,5173
python3 sidecar.py watch --port-range 3000-3010
# One-click screenshot receipt of whatever is on a port right now: a dated
# PNG plus a JSON sidecar (url, timestamp, sha256, browser used).
python3 sidecar.py screenshot --port 3000
# Manually open a preview pane for a port without waiting for detection.
python3 sidecar.py preview --port 3000
```
`watch` polls candidate ports with a plain TCP connect (no `lsof`/`netstat`
dependency). The first time a port starts answering, it writes a small local
HTML file with an iframe pointed at `http://127.0.0.1:<port>` and opens it in
your default browser (`--no-open` to just log the event instead), plus
appends a JSON line to `events.jsonl` in `--out` so detections are
inspectable after the fact.
`screenshot` launches the browser headless, saves `receipt-<port>-<time>.png`
and a matching `.json` receipt, and prints the receipt. See
[`demo-receipt.png`](demo-receipt.png) / [`demo-receipt.json`](demo-receipt.json)
for a real example produced by `demo.py`.
## Recorded dogfood
`demo.py` starts a throwaway one-page site, runs `sidecar.py watch` against
it (no browser opened, so it's safe to run unattended), confirms the
detection event and preview file, then runs `sidecar.py screenshot` with a
real headless Chrome and verifies the PNG and its sha256. No mocks.
[Recorded output](demo.txt).
The demo's throwaway server runs as an in-process thread rather than a
`python -m http.server` subprocess. This repo is itself built and tested
inside Claude Code's own sandboxed Bash tool, which puts a freshly spawned
subprocess's listening socket in a network namespace its own parent can't
reach back into — a property of *this* build environment, not of normal
terminals. A thread-based listener sidesteps that quirk so the demo can still
exercise the real detect → preview → screenshot path end to end, including a
real TCP probe from a separate process and a real headless-Chrome screenshot.
On an ordinary dev machine, `npm run dev` / `vite` / etc. are plain reachable
subprocesses and this workaround doesn't apply.
## Limits and trust
- Detection is TCP-connect probing of a bounded candidate port list (default
15 common dev ports, or an explicit `--ports`/`--port-range` up to 200
ports), not a system-wide socket enumeration. It cannot see which process
owns a port — "something answered a connect" is the whole signal. A
non-HTTP service on a watched port will still trigger a preview attempt.
- Polling has a detection delay (`--interval`, default 2s) and will miss a
port that opens and closes between polls.
- The preview iframe can be blocked by a dev server's own
`X-Frame-Options`/CSP; the generated preview page always includes a plain
"open in new tab" link as a fallback.
- `screenshot` requires a Chromium-family browser found via `--chrome`,
`CHROME_PATH`, or a short list of common install paths. No browser means a
clear error, not a silent skip.
- Loopback (`127.0.0.1`) only by default. No remote tunneling, no ngrok-style
exposure, no orchestration of multiple dev servers — one detected port
gets one preview and, on request, one screenshot.
- `watch` is a foreground poll loop; background it yourself (`&`, tmux,
`nohup`, a launchd/systemd unit) if you want it to persist. There is no
daemon manager here.
- Nothing is uploaded anywhere. Screenshots and events stay in `--out`
(default a `preview-sidecar` directory under the OS temp dir) until you
move or delete them.