# Show HN: Viewport – A clean local agent monitor

> Source: <https://github.com/Brumbelow/viewport>
> Published: 2026-06-03 18:33:35+00:00

Viewport is a local browser monitor for CLI coding agents.

With 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.

`viewport`

starts a local server on`127.0.0.1`

and prints the URL.`viewport run -- <command>`

starts 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.
- Run timelines are persisted in a local SQLite database and reloaded when the server starts again.
- 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.

The server is plain Node.js with static browser assets. Execution prefers
node-pty so wrapped agent CLIs get terminal behavior, with a child_process
fallback if PTY loading fails. No Vite or frontend build step is involved —
the browser frontend is plain ES modules under `public/js/`

(loaded with
`<script type="module">`

) and per-feature CSS modules under `public/css/`

imported by `public/styles.css`

.

Viewport auto-discovers the following agent CLIs from your `PATH`

:

`claude`

`codex`

`gemini`

`grok`

Each one is listed in `/api/agents`

with an `available`

flag derived at request
time. To point at a non-PATH install, set the matching env var:

`VIEWPORT_CLAUDE_BIN`

`VIEWPORT_CODEX_BIN`

`VIEWPORT_GEMINI_BIN`

`VIEWPORT_GROK_BIN`

For Codex, `CODEX_HOME`

defaults to `$HOME/.codex`

so nested sessions share the
same sign-in, model, approval, sandbox, and project trust settings as the
normal CLI. Override with `VIEWPORT_CODEX_HOME`

. No user-specific paths are
baked into the defaults.

Viewport 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:

```
npm install -g @brumbelow/viewport
viewport init        # writes ~/.viewport/{config.json,presets.json}
viewport             # start the server in the background
viewport doctor      # diagnose Node, node-pty, sqlite, agent discovery
viewport version
viewport status      # show running daemon state (exit 1 if not running)
viewport stop        # ask the daemon to exit cleanly
viewport open        # open the running daemon's URL in your browser
```

Until the package is on a registry, the same commands work after a local
`npm install -g .`

from a clone.

Outside the repository, Viewport stores local data in `~/.viewport/`

.
Set `VIEWPORT_DATA_DIR=/some/path`

to override, or run from inside the
cloned repo to keep data in the repo's `.viewport/`

.

If you start the daemon with a non-default `PORT`

, set the same `PORT`

env
when running `viewport status`

/ `stop`

/ `open`

— they probe that port.

Viewport uses Node's built-in SQLite bindings and `node-pty`

. Running on a
recent Node release (current LTS or newer) is recommended; `viewport doctor`

reports whether each piece loaded.

```
npm install
npm start
```

This starts Viewport in the background and prints the local URL.

To wrap another command:

```
node server/index.js run -- npm test
```

Or start the server without immediately launching a command:

```
node server/index.js --foreground
```

Viewport stores run timelines in `.viewport/viewport.sqlite`

, using Node's
built-in SQLite bindings. Each run gets a metadata row and ordered event rows.
If you pass env overrides, those values are stored in the local database so
restored runs retain their launch metadata. The directory is ignored by git and
can be deleted when old local history is no longer useful.

Launch presets can be created and edited from the Preferences dialog
(Settings → Presets tab). The file at `.viewport/presets.json`

(overridable
with `VIEWPORT_PRESETS_FILE`

) remains the source of truth and can also be
edited by hand:

```
{
  "presets": [
    {
      "id": "claude-viewport-print",
      "name": "Claude: viewport print",
      "agent": "claude",
      "cwd": "/path/to/project",
      "args": ["--print"],
      "env": {}
    }
  ]
}
```

Preset 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.

Codex launches resolve the `codex`

binary from `PATH`

(or `VIEWPORT_CODEX_BIN`

)
and set `CODEX_HOME`

to `$HOME/.codex`

so nested sessions share the same
sign-in, model, approval, sandbox, and project trust settings as the normal
CLI. Override the data directory with `VIEWPORT_CODEX_HOME`

.

By default the server binds to `127.0.0.1`

. Override the bind address with
`VIEWPORT_HOST`

. Whenever the bind address is loopback (`127.0.0.1`

,
`localhost`

, or `::1`

) no auth token is required, and the server still
rejects any request whose `Host`

header or `Origin`

is not loopback (the
former with `421`

, the latter with `403`

).

If `VIEWPORT_HOST`

is set to a non-loopback address, an auth token is
required on every request. Provide one explicitly with the `VIEWPORT_TOKEN`

environment variable; if you leave it unset, Viewport generates a random
token at startup and prints it. Send the token with the Authorization
Bearer scheme, the `X-Viewport-Token`

header, or the `?token=<value>`

query parameter. Requests missing or with an unrecognized token receive
`401`

.

The local SQLite database at `.viewport/viewport.sqlite`

(or
`$VIEWPORT_DATA_DIR/viewport.sqlite`

if overridden) is the single source of
truth for run history, pins, and artifact metadata. Artifact blobs live
alongside it under `.viewport/artifacts/`

.

To back up, stop the daemon (`viewport stop`

) and copy the entire
`.viewport/`

directory. To restore, drop the directory back in place and
start the daemon again — runs and their events replay from the database on
startup.

If SQLite refuses to open the file (for example after an unclean shutdown
on a flaky filesystem), Viewport renames the bad file to
`viewport.sqlite.corrupt-<timestamp>`

and starts a fresh database. The
corrupt file is left in place; you can attempt manual recovery with
`sqlite3 viewport.sqlite.corrupt-… .recover`

or delete it once you no
longer need the history.

The schema is keyed on SQLite's `PRAGMA user_version`

and migrated
forward at startup. A database created by an older Viewport release is
upgraded in place; the migrations run in a single transaction per version
and roll back on failure.

Viewport resolves `claude`

, `codex`

, `gemini`

, and `grok`

from `PATH`

at
request time. If `viewport doctor`

or the `/api/agents`

payload shows an
agent as unavailable:

- Confirm the binary is in
`PATH`

for the shell that launched the daemon (`which claude`

,`command -v codex`

). The daemon inherits the launching shell's`PATH`

; restart it after editing your shell profile. - If the binary lives outside
`PATH`

(for example a homebrew prefix not exported in your profile), set the matching`VIEWPORT_*_BIN`

env var with the absolute path. - For Codex,
`CODEX_HOME`

defaults to`$HOME/.codex`

. Override with`VIEWPORT_CODEX_HOME`

if your sessions live elsewhere. - Symlinks resolve through the host shell, so a stale symlink will look
available but fail at spawn time — run the agent's own
`--version`

flag to confirm it executes outside Viewport first.

```
npm run check   # node --check on the server and smoke test
npm test        # boots the server on a free port and exercises the API
```

The smoke test boots the server with a temporary `PATH`

of stub agent binaries
(no real Claude / Codex / Gemini / Grok install required) and exercises:
health, agent/workspace/preset discovery for all supported agents,
auto-discovery + availability flags via PATH, run creation, cwd/env launch
metadata, xterm static assets, launch/steering/copy/export/summary,
report-preview and evidence-context UI controls, transcript export,
after-action summary endpoint (status, duration, output counts, last line,
404), event-context endpoint, SSE delivery, pause/resume, input/resize/signal
endpoints, Last-Event-ID resume, delete + active-run guard, presets CRUD,
concurrent-run cap, retention pruning, DB corruption recovery, evidence-bundle
ZIP export, pin-attachment lifecycle, and SQLite replay after server restart.

Agents that opt in can emit small marker tags in their normal stdout to populate the Signals tab of a run's summary:

`<viewport:milestone name="..."/>`

— a named milestone reached during the run.`<viewport:result status="ok|fail"/>`

— a final or intermediate result.`<viewport:note text="..."/>`

— a free-form note.

Markers 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.

`npm test`

runs the integration smoke suite in`scripts/smoke.mjs`

.`npm run test:unit`

runs the per-module frontend unit tests in`test/`

(`node:test`

+ jsdom; the browser modules exercised in isolation).`npm run test:load`

runs the large-history regression suite in`scripts/load.mjs`

(~20 s; seeds ~500 runs / ~100k events).`docs/QA.md`

is the manual browser checklist to walk before cutting a release.

`public/js/app.js`

is a thin orchestrator; feature logic lives in per-feature modules (`launcher`

,`runs`

,`streaming`

,`steering`

,`pins`

,`artifacts`

,`reports`

,`evidence`

,`search`

,`settings`

) plus a shared`terminal.js`

, on the`state.js`

/`dom.js`

/`util.js`

foundation.`public/styles.css`

mirrors that split: a small`@import`

manifest over per-feature CSS modules under`public/css/`

(`runs`

,`launcher`

,`terminal`

,`steering`

,`pins`

,`search`

,`reports`

,`artifacts`

,`settings`

) on a`base`

/`layout`

/`dialogs`

foundation, with a`responsive`

media-query layer and a`theme`

token layer loaded last. No build step.- Every browser module — including the
`app.js`

orchestrator — has isolated unit tests under`test/`

(run with`npm run test:unit`

); the smoke suite continues to cover the server and asset wiring.

- GET /api/health returns runner type, server start time, server cwd, and run counts.
- GET /api/agents lists the supported agents (claude, codex, gemini, grok),
each with an
`available`

flag based on PATH lookup or the matching`VIEWPORT_*_BIN`

override. - POST /api/agents/:id/runs starts a new interactive agent run for one of the
supported agent ids. Optional
`cwd`

chooses the workspace, and optional`args`

appends 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.
- POST /api/launch-presets/:id/runs starts a named preset. Presets carry agent,
cwd, args, and env overrides; API responses expose
`envKeys`

, not env values. - GET /api/workspaces lists local workspace/project path suggestions. Override roots
with
`VIEWPORT_WORKSPACE_ROOTS=/path/a:/path/b`

. - GET /api/runs lists known runs.
- POST /api/runs starts a shell command from a command string. Optional
`cwd`

must point at an existing directory, and optional`env`

is merged into the child process environment. Responses expose`envKeys`

, not env values. - GET /api/runs/:id returns a run snapshot and buffered events.
- GET /api/runs/:id/transcript returns a plain-text terminal transcript with run metadata, status events, and output chunks.
- GET /api/runs/:id/summary returns a deterministic after-action summary
derived locally from the run's stored events. The JSON includes status,
exit info, command, cwd, runner, timestamps,
`durationMs`

, event counts (output/input/status/resize), output`bytes`

/`lines`

/`lastLine`

, and a compact`text`

digest 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.
- GET /api/runs/:id/context?eventId=N&toEventId=M returns a bounded JSON
evidence window and copyable plain-text transcript context around one event
or event span. Optional
`before`

/`after`

control surrounding event count. - DELETE /api/runs/:id removes a terminal run and its persisted timeline.
- GET /api/runs/:id/events streams live events with SSE. Honors the
`Last-Event-ID`

header 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).
- POST /api/runs/:id/pause sends SIGSTOP and marks a running POSIX run paused.
- POST /api/runs/:id/resume sends SIGCONT and returns a paused POSIX run to running.
- POST /api/runs/:id/input writes the JSON
`data`

string to the run's PTY (or stdin under the spawn fallback). - POST /api/runs/:id/resize sets the PTY size from
`{cols, rows}`

. - POST /api/runs/:id/signal delivers a named POSIX signal (
`SIGINT`

,`SIGTERM`

,`SIGHUP`

,`SIGQUIT`

,`SIGKILL`

,`SIGUSR1`

,`SIGUSR2`

). - POST /api/shutdown asks the server to exit cleanly. Used by
`viewport stop`

and equivalent to SIGTERM. Returns 202 immediately, then drains active runs and exits.
