Skip to content

Session viewer

kaid viewer is a small embedded HTTP server that renders past review sessions in a browser-friendly UI. No external dependencies — sessions are read directly from the JSONL files kaid writes to disk during every review.

Launching

kaid viewer                       # start and open the browser
kaid viewer --addr :3000          # bind to all interfaces on port 3000
kaid viewer --addr 0.0.0.0:8080   # bind on all interfaces
kaid viewer --open=never          # just print the URL
kaid viewer --open=always         # force it when auto declines (piped output, WSL)

The default address is localhost:5483. The server holds the foreground - press Ctrl+C to stop it. Sessions are scanned lazily from ~/.kaid/sessions/ on each request, so a review running in another terminal shows up the moment its JSONL file appears.

DNS-rebinding protection

The viewer checks the Host header against a loopback allowlist (localhost, 127.0.0.1, ::1). A concrete bind host (e.g. --addr 192.168.1.10:5483) is added automatically, but wildcard binds (:3000, 0.0.0.0, ::) are not - reaching the UI from a LAN IP or hostname then returns forbidden host. To expose a wildcard bind, set KAID_VIEWER_ALLOWED_HOSTS to a comma-separated list of allowed hostnames (e.g. KAID_VIEWER_ALLOWED_HOSTS=box.local,192.168.1.10).

Opening the browser

kaid viewer opens the URL in your default browser as soon as the server is listening. --open controls this and takes the same three values as the global --color:

Value Behavior
auto (default) Open only where it is likely to work — see below.
always Open unconditionally. Use this where auto declines but a browser is in fact reachable — piped output, or WSL with no display.
never Print the URL and do nothing else.

In auto mode the browser is not opened when:

  • stdout is not a terminal — output is piped or redirected;
  • SSH_CONNECTION is set and no display is forwarded — a remote host with nothing to open into. ssh -X and ssh -Y set DISPLAY, so they are not suppressed;
  • on Linux, DISPLAY and WAYLAND_DISPLAY are both empty — no display server.

The reason is appended to the ready line, so a deliberately suppressed auto-open never looks like a broken one:

Viewer ready: http://localhost:5483 (browser not opened: no DISPLAY or WAYLAND_DISPLAY)

On Unix, $BROWSER is tried first: a colon-separated list of commands, each either containing a %s placeholder for the URL or receiving it as a trailing argument. Otherwise the platform default runs — open on macOS, xdg-open on Linux and the BSDs, rundll32 on Windows. Failing to open a browser is a warning on stderr and never fatal; the server keeps serving either way.

Four pages

The viewer has four URLs:

URL What you see
/ List of all repositories that have sessions on disk.
/r/{repo} List of sessions for one repository, newest first.
/r/{repo}/{sessionID} Full detail for a single session.
/r/{repo}/compare Two sessions of one repository, compared.

{repo} is a path-encoded string (separators / and \ replaced with -, colons replaced with _ — the same encoding used to name the on-disk directories). You don't usually type this — you click through.

/ — Repository list

For each repo with at least one session you see the repo path, the total session count, the most recent activity timestamp, and a Check link to its sessions. The search box filters the list by repo path, and ten repositories fit on a page; the pager at the bottom right moves between pages.

/r/{repo} — Session list for one repo

For each session: ID (a UUID), branch name (when kaid was able to detect it), review mode, model, file count, duration, and a started-at timestamp, and a Check link to the next-older session. Ten sessions fit on a page; the pager at the bottom right moves between pages.

/r/{repo}/{sessionID} — Session detail

The detail page is the interesting one. It shows:

  1. Header — diff range, model, branch, total tokens, run duration.
  2. File group — one block per reviewed group. Files are grouped semantically before review, so a block may cover several related files; its title is the group's file paths. Inside each group, five "task type" lanes:
Task type When it appears
plan_task The plan phase ran (largest file ≥ PLAN_MODE_LINE_THRESHOLD, or 2+ files totalling ≥ PLAN_MODE_GROUP_LINE_THRESHOLD).
main_task Every group. The main review loop — one pass per review round.
review_filter_task The post-review comment-filtering pass ran for this group.
memory_compression_task The active+compress zone exceeded 60 % / 80 % budget.
re_location_task A code_comment couldn't be anchored, fallback re-location ran.

Each lane is a horizontal strip of task cards — one per LLM round trip. Cards are coloured by task type so you can see at a glance which phases dominated the run.

Longer lists on this page — File breakdown, Files Reviewed, Session Tasks, and Conversations — show twenty items per page. The pager stays hidden while a section still fits on one page.

/r/{repo}/compare — Compare two sessions

The same four buckets kaid session compare prints, rendered as a page. The session list's Action column carries a Check link: each row opens a comparison against the next-older session, so the newest row shows what changed since the run before it. The oldest row shows -, having no older run to compare against.

Findings are sorted into four buckets:

Bucket Meaning
New Only the later run reported it.
Persisting Both runs reported it.
Resolved Only the earlier run reported it, and the later run did review that file.
Not reviewed Only the earlier run reported it, and the later run never looked at that file. Nobody re-checked it, so it is not resolved.

One thing the page does differently: the CLI omits a bucket that came out empty, the page always prints all four. Resolved (0) is an answer, and a section that silently vanished would read as a broken page.

Each bucket shows twenty findings per page; the pager stays hidden when the bucket fits on one page.

A run old enough to predate run manifests recorded no coverage, so every unmatched finding from it falls into Resolved rather than Not reviewed.

To compare any other pair, edit the query string: /r/{repo}/compare?before=<older session id>&after=<newer session id>. Both ids must belong to the repository in the URL.

If the two runs used different review modes, the page shows the same warning kaid session compare prints: they may not have looked at the same files, so the buckets are not directly comparable.

What's in a task card

Click a task card to expand. Each card has:

  • a header row — request number, model badge, a token badge (P: prompt / C: completion, plus CR: / CW: cache read/write when present), a duration badge, and an error badge when the round failed;
  • Response — the raw assistant response, including any reasoning / thinking blocks;
  • Tool calls — each tool invocation with arguments + the result that was returned (collapsible).

The full message list sent to the model and the in-scope tool definitions are not rendered in the card UI; if you need them, inspect the JSONL transcript directly (the messages field on each llm_request record).

Review comments

Below the task lanes, the session page lists every finding the review produced as comment cards, grouped by file, showing the comment text, its existing/suggested code where present, and severity/category badges. Chips on the filter bar narrow the list by severity or category. Twenty comments fit on a page; the pager stays hidden when there is only one. Changing a severity or category filter returns to page 1; marking a finding, toggling Hide marked, or clearing marks keeps the current page where it still exists.

Marking findings as you fix them

Each card carries three buttons — Fixed / Ignored / Clear — that set a per-comment mark:

  • Marks are mutually exclusive: setting one replaces another, and Clear removes it. The current state shows as a colored chip on the card.
  • Hide marked (on by default, remembered per browser) keeps marked cards out of the way while you work through what is left. The toolbar counts how many are marked and hidden; switch the toggle off any time to see everything again.
  • Clear all marks resets the whole session at once.

Marks are viewer state, not review data — the viewer itself stays read-only:

  • They are stored in your browser's localStorage, scoped to the session page. Nothing is ever written next to the session JSONL, and the viewer exposes no write API at all.
  • Marks belong to one session and one browser: another browser or machine sees the session unmarked, and clearing the browser's storage for the site starts it over.
  • Re-running a review of the same change produces a new session, which starts unmarked.

Use cases

The viewer is designed around three workflows:

"Why did the model say that?"

Open a comment in your terminal output, locate the group containing the file in the viewer, and walk down its main_task lane. The card whose tool calls include the code_comment you care about is the round that produced it. The card's Response shows the model's reasoning; for the exact prompt + context the model was sent, open the llm_request record for that request number in the JSONL transcript (its messages field).

"Why was this file silent?"

A file with no comments is a successful review only if the model deliberately called task_done. If the lane shows tool calls but no code_comment, that's an intentional clean review. If the lane ends in an error card, it's a failure dressed up as silence — surface it as a warning.

"What did compression keep / drop?"

The memory_compression_task lane shows every compression round. Inside, the Response pane has the resulting summary; the rendered XML of the compress zone that was fed in lives in the round's llm_request messages in the JSONL transcript. Useful when debugging a "the model forgot earlier context" complaint — you can see whether compression dropped the relevant detail.

Storage layout on disk

The viewer reads from:

~/.kaid/sessions/
└── <path-encoded-repo-path>/
    └── <session-id>.jsonl

Each line in the JSONL file is one event:

{"type": "llm_request", "filePath": "src/foo.go", "taskType": "main_task", "request_no": 1, "messages": [{"role": "user", "content": "Review this diff…"}], "timestamp": "2026-06-02T10:15:23Z"}
{"type": "llm_response", "filePath": "src/foo.go", "taskType": "main_task", "model": "claude-sonnet-4-6", "content": "Found 2 issues…", "duration_ms": 8421, "usage": {"prompt_tokens": 12450, "completion_tokens": 320}}
{"type": "tool_call", "filePath": "src/foo.go", "tool_name": "file_read", "arguments": "{\"file_path\":\"src/foo.go\",\"start_line\":1,\"end_line\":50}", "result": "File: src/foo.go (Total lines: 220)\nIS_TRUNCATED: false\nLINE_RANGE: 1-50\n1|package foo…", "ok": true, "duration_ms": 14}

filePath holds the group key: a single path for a one-file group, or the group's paths sorted and comma-joined when several files were reviewed together.

Lines are append-only — a partial JSONL means a session was killed mid-run, and the viewer renders what it has.

To free disk space, delete entire session files; the viewer regenerates its index on the next request.

Privacy

The JSONL transcripts contain everything sent to and received from the LLM, including any code that was in the diff. They live entirely on your machine inside ~/.kaid/. kaid does not upload them anywhere.

If your reviews include code you wouldn't want stored long-term, either:

  • delete the session files periodically, or
  • redirect --audience agent --format json output to a transient pipe in CI and run with a temporary HOME so the JSONL never persists.

The OpenTelemetry exporter is a separate concern — see Telemetry for how to keep prompt content out of exported traces.

When the viewer is not the right tool

  • For programmatic post-processing (CI, dashboards), use kaid review --format json --audience agent. The viewer renders for humans, not machines.
  • For grepping across many sessions, use jq on the JSONL files directly. There's no search box in the UI yet.

See Also

  • Architecture - what those five task types actually do under the hood.
  • Tools - the tool calls you'll see in main_task cards.