{"slug": "show-hn-viewport-a-clean-local-agent-monitor", "title": "Show HN: Viewport – A clean local agent monitor", "summary": "A developer released Viewport, an open-source local browser monitor that provides a clean interface for observing and controlling CLI coding agents. The tool runs as a lightweight Node.js wrapper, streaming agent activity to a local browser view with controls for pausing, killing, and steering prompts, while persisting run timelines in a SQLite database. Viewport auto-discovers common agent CLIs like Claude, Codex, Gemini, and Grok from the user's PATH, offering developers a way to monitor and manage coding agent sessions without external dependencies or build steps.", "body_md": "Viewport is a local browser monitor for CLI coding agents.\n\nWith viewport, you can run a coding agent through a lightweight wrapper, stream its activity into a clean local browser view, and give the operator fast controls for pause, kill, steering prompts, evidence pinning, plus after-action summaries.\n\n`viewport`\n\nstarts a local server on`127.0.0.1`\n\nand prints the URL.`viewport run -- <command>`\n\nstarts the server and immediately launches a run.- Browser clients list runs, receive live output over Server-Sent Events, and can reconnect without losing the buffered event stream.\n- Run timelines are persisted in a local SQLite database and reloaded when the server starts again.\n- The UI exposes a run list, an xterm.js terminal view, status/runner/workspace metadata, launch presets for common agent/workspace combinations, a custom agent launcher with optional args, a literal start-folder field, pause/resume and kill buttons, a multiline steering input with send modes and prompt history, and quick buttons for Ctrl+C / Ctrl+D / Esc / arrow keys. Selected runs are deep-linkable.\n\nThe server is plain Node.js with static browser assets. Execution prefers\nnode-pty so wrapped agent CLIs get terminal behavior, with a child_process\nfallback if PTY loading fails. No Vite or frontend build step is involved —\nthe browser frontend is plain ES modules under `public/js/`\n\n(loaded with\n`<script type=\"module\">`\n\n) and per-feature CSS modules under `public/css/`\n\nimported by `public/styles.css`\n\n.\n\nViewport auto-discovers the following agent CLIs from your `PATH`\n\n:\n\n`claude`\n\n`codex`\n\n`gemini`\n\n`grok`\n\nEach one is listed in `/api/agents`\n\nwith an `available`\n\nflag derived at request\ntime. To point at a non-PATH install, set the matching env var:\n\n`VIEWPORT_CLAUDE_BIN`\n\n`VIEWPORT_CODEX_BIN`\n\n`VIEWPORT_GEMINI_BIN`\n\n`VIEWPORT_GROK_BIN`\n\nFor Codex, `CODEX_HOME`\n\ndefaults to `$HOME/.codex`\n\nso nested sessions share the\nsame sign-in, model, approval, sandbox, and project trust settings as the\nnormal CLI. Override with `VIEWPORT_CODEX_HOME`\n\n. No user-specific paths are\nbaked into the defaults.\n\nViewport ships as a single Node.js package and a small static frontend. Once published, a global install works the same way locally and on a workstation:\n\n```\nnpm install -g @brumbelow/viewport\nviewport init        # writes ~/.viewport/{config.json,presets.json}\nviewport             # start the server in the background\nviewport doctor      # diagnose Node, node-pty, sqlite, agent discovery\nviewport version\nviewport status      # show running daemon state (exit 1 if not running)\nviewport stop        # ask the daemon to exit cleanly\nviewport open        # open the running daemon's URL in your browser\n```\n\nUntil the package is on a registry, the same commands work after a local\n`npm install -g .`\n\nfrom a clone.\n\nOutside the repository, Viewport stores local data in `~/.viewport/`\n\n.\nSet `VIEWPORT_DATA_DIR=/some/path`\n\nto override, or run from inside the\ncloned repo to keep data in the repo's `.viewport/`\n\n.\n\nIf you start the daemon with a non-default `PORT`\n\n, set the same `PORT`\n\nenv\nwhen running `viewport status`\n\n/ `stop`\n\n/ `open`\n\n— they probe that port.\n\nViewport uses Node's built-in SQLite bindings and `node-pty`\n\n. Running on a\nrecent Node release (current LTS or newer) is recommended; `viewport doctor`\n\nreports whether each piece loaded.\n\n```\nnpm install\nnpm start\n```\n\nThis starts Viewport in the background and prints the local URL.\n\nTo wrap another command:\n\n```\nnode server/index.js run -- npm test\n```\n\nOr start the server without immediately launching a command:\n\n```\nnode server/index.js --foreground\n```\n\nViewport stores run timelines in `.viewport/viewport.sqlite`\n\n, using Node's\nbuilt-in SQLite bindings. Each run gets a metadata row and ordered event rows.\nIf you pass env overrides, those values are stored in the local database so\nrestored runs retain their launch metadata. The directory is ignored by git and\ncan be deleted when old local history is no longer useful.\n\nLaunch presets can be created and edited from the Preferences dialog\n(Settings → Presets tab). The file at `.viewport/presets.json`\n\n(overridable\nwith `VIEWPORT_PRESETS_FILE`\n\n) remains the source of truth and can also be\nedited by hand:\n\n```\n{\n  \"presets\": [\n    {\n      \"id\": \"claude-viewport-print\",\n      \"name\": \"Claude: viewport print\",\n      \"agent\": \"claude\",\n      \"cwd\": \"/path/to/project\",\n      \"args\": [\"--print\"],\n      \"env\": {}\n    }\n  ]\n}\n```\n\nPreset env values are merged into the launched process and stored in the local SQLite run metadata. Keep secrets out of preset env unless you are comfortable with them living in local Viewport history.\n\nCodex launches resolve the `codex`\n\nbinary from `PATH`\n\n(or `VIEWPORT_CODEX_BIN`\n\n)\nand set `CODEX_HOME`\n\nto `$HOME/.codex`\n\nso nested sessions share the same\nsign-in, model, approval, sandbox, and project trust settings as the normal\nCLI. Override the data directory with `VIEWPORT_CODEX_HOME`\n\n.\n\nBy default the server binds to `127.0.0.1`\n\n. Override the bind address with\n`VIEWPORT_HOST`\n\n. Whenever the bind address is loopback (`127.0.0.1`\n\n,\n`localhost`\n\n, or `::1`\n\n) no auth token is required, and the server still\nrejects any request whose `Host`\n\nheader or `Origin`\n\nis not loopback (the\nformer with `421`\n\n, the latter with `403`\n\n).\n\nIf `VIEWPORT_HOST`\n\nis set to a non-loopback address, an auth token is\nrequired on every request. Provide one explicitly with the `VIEWPORT_TOKEN`\n\nenvironment variable; if you leave it unset, Viewport generates a random\ntoken at startup and prints it. Send the token with the Authorization\nBearer scheme, the `X-Viewport-Token`\n\nheader, or the `?token=<value>`\n\nquery parameter. Requests missing or with an unrecognized token receive\n`401`\n\n.\n\nThe local SQLite database at `.viewport/viewport.sqlite`\n\n(or\n`$VIEWPORT_DATA_DIR/viewport.sqlite`\n\nif overridden) is the single source of\ntruth for run history, pins, and artifact metadata. Artifact blobs live\nalongside it under `.viewport/artifacts/`\n\n.\n\nTo back up, stop the daemon (`viewport stop`\n\n) and copy the entire\n`.viewport/`\n\ndirectory. To restore, drop the directory back in place and\nstart the daemon again — runs and their events replay from the database on\nstartup.\n\nIf SQLite refuses to open the file (for example after an unclean shutdown\non a flaky filesystem), Viewport renames the bad file to\n`viewport.sqlite.corrupt-<timestamp>`\n\nand starts a fresh database. The\ncorrupt file is left in place; you can attempt manual recovery with\n`sqlite3 viewport.sqlite.corrupt-… .recover`\n\nor delete it once you no\nlonger need the history.\n\nThe schema is keyed on SQLite's `PRAGMA user_version`\n\nand migrated\nforward at startup. A database created by an older Viewport release is\nupgraded in place; the migrations run in a single transaction per version\nand roll back on failure.\n\nViewport resolves `claude`\n\n, `codex`\n\n, `gemini`\n\n, and `grok`\n\nfrom `PATH`\n\nat\nrequest time. If `viewport doctor`\n\nor the `/api/agents`\n\npayload shows an\nagent as unavailable:\n\n- Confirm the binary is in\n`PATH`\n\nfor the shell that launched the daemon (`which claude`\n\n,`command -v codex`\n\n). The daemon inherits the launching shell's`PATH`\n\n; restart it after editing your shell profile. - If the binary lives outside\n`PATH`\n\n(for example a homebrew prefix not exported in your profile), set the matching`VIEWPORT_*_BIN`\n\nenv var with the absolute path. - For Codex,\n`CODEX_HOME`\n\ndefaults to`$HOME/.codex`\n\n. Override with`VIEWPORT_CODEX_HOME`\n\nif your sessions live elsewhere. - Symlinks resolve through the host shell, so a stale symlink will look\navailable but fail at spawn time — run the agent's own\n`--version`\n\nflag to confirm it executes outside Viewport first.\n\n```\nnpm run check   # node --check on the server and smoke test\nnpm test        # boots the server on a free port and exercises the API\n```\n\nThe smoke test boots the server with a temporary `PATH`\n\nof stub agent binaries\n(no real Claude / Codex / Gemini / Grok install required) and exercises:\nhealth, agent/workspace/preset discovery for all supported agents,\nauto-discovery + availability flags via PATH, run creation, cwd/env launch\nmetadata, xterm static assets, launch/steering/copy/export/summary,\nreport-preview and evidence-context UI controls, transcript export,\nafter-action summary endpoint (status, duration, output counts, last line,\n404), event-context endpoint, SSE delivery, pause/resume, input/resize/signal\nendpoints, Last-Event-ID resume, delete + active-run guard, presets CRUD,\nconcurrent-run cap, retention pruning, DB corruption recovery, evidence-bundle\nZIP export, pin-attachment lifecycle, and SQLite replay after server restart.\n\nAgents that opt in can emit small marker tags in their normal stdout to populate the Signals tab of a run's summary:\n\n`<viewport:milestone name=\"...\"/>`\n\n— a named milestone reached during the run.`<viewport:result status=\"ok|fail\"/>`\n\n— a final or intermediate result.`<viewport:note text=\"...\"/>`\n\n— a free-form note.\n\nMarkers are tolerant of extra whitespace inside the angle brackets and are extracted alongside the heuristic detectors (errors, file paths, idle gaps) that run on every captured run.\n\n`npm test`\n\nruns the integration smoke suite in`scripts/smoke.mjs`\n\n.`npm run test:unit`\n\nruns the per-module frontend unit tests in`test/`\n\n(`node:test`\n\n+ jsdom; the browser modules exercised in isolation).`npm run test:load`\n\nruns the large-history regression suite in`scripts/load.mjs`\n\n(~20 s; seeds ~500 runs / ~100k events).`docs/QA.md`\n\nis the manual browser checklist to walk before cutting a release.\n\n`public/js/app.js`\n\nis a thin orchestrator; feature logic lives in per-feature modules (`launcher`\n\n,`runs`\n\n,`streaming`\n\n,`steering`\n\n,`pins`\n\n,`artifacts`\n\n,`reports`\n\n,`evidence`\n\n,`search`\n\n,`settings`\n\n) plus a shared`terminal.js`\n\n, on the`state.js`\n\n/`dom.js`\n\n/`util.js`\n\nfoundation.`public/styles.css`\n\nmirrors that split: a small`@import`\n\nmanifest over per-feature CSS modules under`public/css/`\n\n(`runs`\n\n,`launcher`\n\n,`terminal`\n\n,`steering`\n\n,`pins`\n\n,`search`\n\n,`reports`\n\n,`artifacts`\n\n,`settings`\n\n) on a`base`\n\n/`layout`\n\n/`dialogs`\n\nfoundation, with a`responsive`\n\nmedia-query layer and a`theme`\n\ntoken layer loaded last. No build step.- Every browser module — including the\n`app.js`\n\norchestrator — has isolated unit tests under`test/`\n\n(run with`npm run test:unit`\n\n); the smoke suite continues to cover the server and asset wiring.\n\n- GET /api/health returns runner type, server start time, server cwd, and run counts.\n- GET /api/agents lists the supported agents (claude, codex, gemini, grok),\neach with an\n`available`\n\nflag based on PATH lookup or the matching`VIEWPORT_*_BIN`\n\noverride. - POST /api/agents/:id/runs starts a new interactive agent run for one of the\nsupported agent ids. Optional\n`cwd`\n\nchooses the workspace, and optional`args`\n\nappends CLI arguments such as permission flags. Returns 404 if the id is not a supported agent and 409 if the agent binary cannot be found. - GET /api/launch-presets lists built-in and configured launch presets.\n- POST /api/launch-presets/:id/runs starts a named preset. Presets carry agent,\ncwd, args, and env overrides; API responses expose\n`envKeys`\n\n, not env values. - GET /api/workspaces lists local workspace/project path suggestions. Override roots\nwith\n`VIEWPORT_WORKSPACE_ROOTS=/path/a:/path/b`\n\n. - GET /api/runs lists known runs.\n- POST /api/runs starts a shell command from a command string. Optional\n`cwd`\n\nmust point at an existing directory, and optional`env`\n\nis merged into the child process environment. Responses expose`envKeys`\n\n, not env values. - GET /api/runs/:id returns a run snapshot and buffered events.\n- GET /api/runs/:id/transcript returns a plain-text terminal transcript with run metadata, status events, and output chunks.\n- GET /api/runs/:id/summary returns a deterministic after-action summary\nderived locally from the run's stored events. The JSON includes status,\nexit info, command, cwd, runner, timestamps,\n`durationMs`\n\n, event counts (output/input/status/resize), output`bytes`\n\n/`lines`\n\n/`lastLine`\n\n, and a compact`text`\n\ndigest suitable for copy. - GET /api/runs/:id/report?format=html|md|json returns a local run report with summary signals, pins, artifacts, and a clipped transcript.\n- GET /api/runs/:id/context?eventId=N&toEventId=M returns a bounded JSON\nevidence window and copyable plain-text transcript context around one event\nor event span. Optional\n`before`\n\n/`after`\n\ncontrol surrounding event count. - DELETE /api/runs/:id removes a terminal run and its persisted timeline.\n- GET /api/runs/:id/events streams live events with SSE. Honors the\n`Last-Event-ID`\n\nheader so reconnecting clients do not see duplicates, and sends keep-alive comments every 15 seconds. - POST /api/runs/:id/kill requests termination (SIGTERM, escalating to SIGKILL after 1.5s).\n- POST /api/runs/:id/pause sends SIGSTOP and marks a running POSIX run paused.\n- POST /api/runs/:id/resume sends SIGCONT and returns a paused POSIX run to running.\n- POST /api/runs/:id/input writes the JSON\n`data`\n\nstring to the run's PTY (or stdin under the spawn fallback). - POST /api/runs/:id/resize sets the PTY size from\n`{cols, rows}`\n\n. - POST /api/runs/:id/signal delivers a named POSIX signal (\n`SIGINT`\n\n,`SIGTERM`\n\n,`SIGHUP`\n\n,`SIGQUIT`\n\n,`SIGKILL`\n\n,`SIGUSR1`\n\n,`SIGUSR2`\n\n). - POST /api/shutdown asks the server to exit cleanly. Used by\n`viewport stop`\n\nand equivalent to SIGTERM. Returns 202 immediately, then drains active runs and exits.", "url": "https://wpnews.pro/news/show-hn-viewport-a-clean-local-agent-monitor", "canonical_source": "https://github.com/Brumbelow/viewport", "published_at": "2026-06-03 18:33:35+00:00", "updated_at": "2026-06-03 18:51:27.880996+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-products", "ai-infrastructure"], "entities": ["Viewport", "CLI coding agents", "xterm.js", "SQLite", "Node.js", "node-pty"], "alternates": {"html": "https://wpnews.pro/news/show-hn-viewport-a-clean-local-agent-monitor", "markdown": "https://wpnews.pro/news/show-hn-viewport-a-clean-local-agent-monitor.md", "text": "https://wpnews.pro/news/show-hn-viewport-a-clean-local-agent-monitor.txt", "jsonld": "https://wpnews.pro/news/show-hn-viewport-a-clean-local-agent-monitor.jsonld"}}