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_CONNECTIONis set and no display is forwarded — a remote host with nothing to open into.ssh -Xandssh -YsetDISPLAY, so they are not suppressed;- on Linux,
DISPLAYandWAYLAND_DISPLAYare 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:
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:
- Header — diff range, model, branch, total tokens, run duration.
- 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, plusCR:/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 /
thinkingblocks; - 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:
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 jsonoutput to a transient pipe in CI and run with a temporaryHOMEso 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
jqon 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_taskcards.