@brumbelow/viewport
v1.0.0
Published
Local browser monitor and control surface for CLI coding-agent runs.
Maintainers
Readme
Viewport
Viewport is a local browser monitor for CLI coding agents.
The idea: run a coding agent through a lightweight wrapper, stream its activity into a cleaner local browser view, and give the operator fast controls for pause, kill, steering prompts, evidence pinning, and after-action summaries.
What it does today
viewportstarts a local server on127.0.0.1and 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. Transcripts can be exported as plain text during or after a run, an after-action summary dialog (Summary button) shows a local digest of the selected run with copy-to-clipboard, and a report dialog shows the Markdown run report in-app and can open the HTML or JSON versions. Pins, spans, and search results open a bounded evidence dialog around the relevant event range so useful transcript context can be copied without scrolling, and pinned moments can carry uploaded artifact attachments. The whole evidence set (report, transcript, pins, artifacts, manifest) can be exported as a ZIP bundle for offline sharing.
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.
Supported agents
Viewport auto-discovers the following agent CLIs from your PATH:
claudecodexgeminigrok
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_BINVIEWPORT_CODEX_BINVIEWPORT_GEMINI_BINVIEWPORT_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.
Install
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 browserUntil 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.
Run from source
npm install
npm startThis starts Viewport in the background and prints the local URL.
To wrap another command:
node server/index.js run -- npm testOr start the server without immediately launching a command:
node server/index.js --foregroundAPI shape
- 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
availableflag based on PATH lookup or the matchingVIEWPORT_*_BINoverride. - POST /api/agents/:id/runs starts a new interactive agent run for one of the
supported agent ids. Optional
cwdchooses the workspace, and optionalargsappends 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
cwdmust point at an existing directory, and optionalenvis merged into the child process environment. Responses exposeenvKeys, 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), outputbytes/lines/lastLine, and a compacttextdigest 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/aftercontrol 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-IDheader 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
datastring 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 stopand equivalent to SIGTERM. Returns 202 immediately, then drains active runs and exits.
Local data
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.
Network binding and auth
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.
Backup and restore
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.
Corruption recovery
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.
Troubleshooting agent discovery
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
PATHfor the shell that launched the daemon (which claude,command -v codex). The daemon inherits the launching shell'sPATH; 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 matchingVIEWPORT_*_BINenv var with the absolute path. - For Codex,
CODEX_HOMEdefaults to$HOME/.codex. Override withVIEWPORT_CODEX_HOMEif 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
--versionflag to confirm it executes outside Viewport first.
Verification
npm run check # node --check on the server and smoke test
npm test # boots the server on a free port and exercises the APIThe 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.
Summary markers
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.
Verification
npm testruns the integration smoke suite inscripts/smoke.mjs.npm run test:unitruns the per-module frontend unit tests intest/(node:test+ jsdom; the browser modules exercised in isolation).npm run test:loadruns the large-history regression suite inscripts/load.mjs(~20 s; seeds ~500 runs / ~100k events).docs/QA.mdis the manual browser checklist to walk before cutting a release.
Frontend layout
public/js/app.jsis a thin orchestrator; feature logic lives in per-feature modules (launcher,runs,streaming,steering,pins,artifacts,reports,evidence,search,settings) plus a sharedterminal.js, on thestate.js/dom.js/util.jsfoundation.public/styles.cssmirrors that split: a small@importmanifest over per-feature CSS modules underpublic/css/(runs,launcher,terminal,steering,pins,search,reports,artifacts,settings) on abase/layout/dialogsfoundation, with aresponsivemedia-query layer and athemetoken layer loaded last. No build step.- Every browser module — including the
app.jsorchestrator — has isolated unit tests undertest/(run withnpm run test:unit); the smoke suite continues to cover the server and asset wiring.
