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 , kill, steering prompts, evidence pinning, plus after-action summaries.
viewport
starts a local server on127.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, /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 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'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_*_BIN
env var with the absolute path. - For Codex,
CODEX_HOME
defaults to$HOME/.codex
. Override withVIEWPORT_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, /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 inscripts/smoke.mjs
.npm run test:unit
runs the per-module frontend unit tests intest/
(node:test
- jsdom; the browser modules exercised in isolation).
npm run test:load
runs the large-history regression suite inscripts/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 sharedterminal.js
, on thestate.js
/dom.js
/util.js
foundation.public/styles.css
mirrors that split: a small@import
manifest over per-feature CSS modules underpublic/css/
(runs
,launcher
,terminal
,steering
,pins
,search
,reports
,artifacts
,settings
) on abase
/layout
/dialogs
foundation, with aresponsive
media-query layer and atheme
token layer loaded last. No build step.- Every browser module — including the
app.js
orchestrator — has isolated unit tests undertest/
(run withnpm 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 matchingVIEWPORT_*_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 optionalargs
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 optionalenv
is 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 compacttext
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/ sends SIGSTOP and marks a running POSIX run d.
- POST /api/runs/:id/resume sends SIGCONT and returns a d 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.