cd /news/ai-agents/show-hn-viewport-a-clean-local-agent… · home topics ai-agents article
[ARTICLE · art-20714] src=github.com pub= topic=ai-agents verified=true sentiment=↑ positive

Show HN: Viewport – A clean local agent monitor

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.

read10 min publishedJun 3, 2026

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.

── more in #ai-agents 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-viewport-a-c…] indexed:0 read:10min 2026-06-03 ·