Files
stack/agents/dewey/work/chat-02/CONSOLE.md
T
jason.woltjeandClaude Opus 5.5 c9e771cf59 feat(webui): CHAT-02 Console, read-only conversation view (#1507)
History opens a seat's conversation from the Waiting card, table row
and inspector. It pages the whole branch through the CHAT-02 board
routes, renders untrusted text inert, polls with the follow cursor, and
marks every switch (branch, newer, reconcile, gone). The WebUI proxy
passes only the two conversation routes' queries upstream.

Dewey authored it. Filbert asked for changes on r1 (24b046af) and
approved r2 (d06de6a7) in review 160dd68d. A relaunch shows 'newer',
not 'reconcile', a deviation from brief 2.3 item 6 that Filbert
accepted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 18:05:11 -05:00

13 KiB
Raw Blame History

CHAT-02 Console: review packet (#1507, row 5), revision 2

Author: Dewey, 2026-09-26. Brief: BRIEF.md R4 (636b0fac…), §2.2, §2.3 and §4. Base: 3a209eea on refactor, which includes the backend commit a5beb6d9. The commits after a4d38a3d touch only packages/ledger and records. Nothing here is committed; the candidate is the working tree, pinned by the hashes below.

Reviewer (Sage's order): Filbert, all ten files. Revision 1 (CONSOLE-r1-24b046af.md, frozen) drew changes requested: one blocker, B1, and three nonblocking notes (agents/filbert/work/chat-02-console-review-2026-09-26.md, 91214fad…02bf8). §0 is the delta.

0. Revision 2 delta

Three files changed: app.js, conversation.test.mjs and the README. The delta against the revision 1 pins is evidence/console/r2-delta.patch (c1d65628…5866). Its removed HOSTILE line still carries revision 1's raw U+202E.

  • B1, Reload switched to the default branch without saying so. The reconcile marker's button is now reload and reopens with the branch the view was on (convURL({ id, branch })). The reopened view raises the branch marker again if the default has moved. If the board answers unknown-branch, the view opens the default branch and shows a gone marker: "The branch this view was on is no longer in the session. This view shows the latest branch." "Open the latest branch" has its own id, latest, and still reopens without a branch.
  • B1 test. The fork test continues in probe A's shape: the fork becomes the default, a same-inode rewrite refuses the next check, and Reload must show the rewritten main (QUESTION, MAIN_ANSWER, MAIN_MORE) with the branch marker. "Open the latest branch" then shows the fork. A second rewrite removes the fork; Reload must show main with the gone marker and no reconcile marker.
  • N1 taken. When "Open the newest session" lands on the file already open, the view keeps the old session id, so newer stays, and its text says the newer session's history is not readable yet. New test 3: a header-only relaunch file, the button keeps the marker and the old file; after the new file gets an entry, the same button opens it.
  • N2 taken. No raw bidi characters remain in app.js or the tests; the regex and HOSTILE use \u escapes.
  • N3 taken. inert() also shows C1 controls (U+0080–U+009F) and LRM, RLM and ALM as [U+XXXX], padded to four digits. The hostile fixture adds a single-byte CSI (U+009B) and an LRM, and the page check rejects raw ESC, CSI, LRM and RLO.

1. Candidate hashes

5a1a4de713a27404af4b049963ffcaac83371a1e2bd0ce3812d214d9a33dacc4  packages/webui/README.md
3c7f2f4c6a6e7867fef3f85b853a7538d98b03b2e303fdb0abcfa9b09eb80d6e  packages/webui/src/public/app.js
b972e2f7f22dafbbf7425773c0715875f9bbcf795af1e5f66a2d9dc394c77047  packages/webui/src/public/index.html
8747b83d16d93d880cf1502267aefc3e48f02e7b5b364177c6fb492320f63024  packages/webui/src/public/live.css
1187a98f52e937f30dc0fbbb3d83445feff7c0a38d3535252af1c7e7e704b7b3  packages/webui/src/serve.mjs
0477f66d5caf9d8f76ba3fb14d3f122f124d6701259f3d7447da639f69982525  packages/webui/tests/serve.test.mjs
52c2c6632713f1d48bdb4975b73d2dd920769401f6a01269d1a09ad1cac5a4a5  packages/webui/tests/conversation.test.mjs
b312c8a192a7a92f0642ee977b136a2ef5909b55632a4d0f669f4ca06e30eee0  packages/webui/tests/history-fixture.mjs
0918aeeaac89cac3d6977ad2e106d299c623e8f2ac6af631ab116ae7f14d3013  packages/webui/tests/history-return-flow.test.mjs
105d87ec3394589afb5c5a43230dd0a43752fcac05d6558b6205013a611a69b2  packages/control-board/tests/serve.test.mjs

The last three webui tests are new files; the rest are diffs against the base (git diff 3a209eea -- packages/webui packages/control-board/tests). The packages/ledger edits in the shared tree belong to another seat and are not part of this candidate.

2. What it does

  • Entry points. A History button on each seat's Waiting card, table row and inspector opens a read-only conversation view in place of the board. Back returns focus to the button that opened it.
  • Rendering. The view reads the whole branch through /api/conversation, page by page, and joins fragments and continuation parts by block. Nothing is clipped. Tool calls, tool results and thinking are collapsed details; redacted thinking says "not available".
  • Untrusted text. Session content is set only with textContent. Markdown stays as source. C0 controls and DEL show as Unicode control pictures (␛, ␇, ␡). C1 controls, bidi overrides and isolates, and LRM, RLM and ALM show as [U+009B], [U+202E] and similar. Newlines and tabs stay.
  • Polling. On the last page the view keeps the follow cursor and reads from it on each board refresh (every 10 s; Pause stops it). It scrolls only when the reader was already near the end.
  • Nothing switches silently. Four markers; three have a button:
    • branch: view.defaultBranch differs from the open branch. "Open the latest branch" reopens without a branch parameter.
    • newer: the board row's session id changed after the view opened, which is a relaunch. "Open the newest session." If that lands on the open file, the marker stays and says the newer history is not readable yet.
    • reconcile: a refusal with reconcile: true. The view keeps what it showed, stops polling and offers "Reload conversation", which reopens the same branch.
    • gone: Reload found the branch removed and opened the latest one.
  • Session picker. Lists every catalogue row for the seat, newest first; unavailable ones say why. Non-Pi harnesses and seats without files say so and show no reply form.
  • Reply. The view's form uses the same /api/reply path, draft map, pending-send lock and receipts as the inspector. A delivered send clears the box only if its text is unchanged.
  • Proxy. webui/src/serve.mjs adds GET /api/conversations and /api/conversation. Only those two carry their query string upstream; the board validates it. Upstream status and body pass through unchanged.
  • Age is unchanged from 42c08d52 (no Age lines in the diff).

3. Choices to review

  1. The relaunch marker is newer, not reconcile. Brief §2.3 item 6 says "shows the reconcile marker". A new session file does not change the open file, so the reader has nothing to refuse and the open view is still accurate. The view shows a separate newer marker instead, and the test asserts it, the kept file, the absence of the new file's text and the draft. reconcile is kept for refusals (tested in conversation.test.mjs with a same-inode rewrite).
  2. Relaunch detection uses the board's sessionId for the newest readable conversation only. An older session opened from the picker never shows newer, because it was never the board's current session.
  3. The conversation form has its own conv-form and conv-receipt classes. My first draft reused reply-form and receipt. The hidden view sits earlier in the DOM, so querySelector(".reply-form") in four existing browser tests found it first and failed; those are the four failures Filbert saw in the shared tree during the backend review. The inspector's classes are unchanged. CSS rules list both classes, and the submit handler matches .reply-form, #conv-form.
  4. Heading focus ring. Opening the view focuses its heading (tabindex="-1"). The shared :focus-visible rule draws its ring, as it does for the inspector title. A synthetic click() counts as keyboard focus, so the screenshots show the ring; a real mouse click does not. I kept it for keyboard users.
  5. Darkwing's two R2 notes on the backend routes are taken here, as Sage offered: the refusal-status test now scans every .mjs in packages/conversation/src and pins the whole REFUSAL_STATUS object. Test-only; serve.mjs is unchanged from a5beb6d9.

4. Evidence

4.1 Suites and contract checks

  • In /tmp/dewey-chat02/overlay-console, a git archive of 3a209eea with only the ten files overlaid: conversation 29, control-board 124, webui 14 and seat 19, 186 of 186 pass.
  • node docs/plans/chat-00/check.mjs, chat-01/check.mjs and chat-01c/check.mjs all exit 0 there.
  • The shared tree gives the same 186/186.

4.2 Acceptance map (brief §4)

Item Test
§2.2 hostile-render fixture conversation.test.mjs test 1: <script>, <img onerror>, a Markdown and an HTML javascript: link, ANSI and OSC escapes, a C1 CSI, an LRM and a bidi override, in assistant text and tool output. It asserts the exact visible text; no script, img, a, iframe, object, embed, svg, style or link element in the view; no on* attribute anywhere; no raw ESC, CSI, LRM or RLO in the page; window.injected undefined after a click; and an unchanged URL.
Full history, collapsed tools, hidden thinking test 1: roles in order, the long answer equal to its source, three closed details whose contents are not visible
Malformed-line and reconcile markers test 1
Branch marker, no silent switch, open on request test 2 (new): after the fork, the open branch grows and the next poll still follows it; Reload after a rewrite keeps that branch with the marker; "Open the latest branch" shows the fork; a vanished branch reloads to the default with the gone marker
Newer session not yet readable (N1) test 3 (new)
No history, non-reply seats test 4
Entry points card (history-return-flow), table and inspector (test 1)
§2.3 items 1–7 history-return-flow.test.mjs: send from the view; toolCall, then a draft with its caret at 4, then a delayed toolResult on a later poll; a peer message; an answer with MID_SENTINEL after character 250 and END_SENTINEL at the end, shown once with no "…"; a 4.5-million-character answer checked by SHA-256 in the page and split into at least two ordered continuation parts over the real route; a relaunch with the newer marker, the kept file and the draft; then "Open the newest session" with the draft kept. Refresh is never pressed.
Proxy webui/tests/serve.test.mjs: exact query forwarding for both routes, upstream 404 and body preserved, 405 on POST, a cross-origin 403 before the upstream is touched, and no query forwarded for /api/board
Browser evidence evidence/console/screens/conversation-{320,1440}-{light,dark}.png, from WEBUI_EVIDENCE in test 1: long answer, tool call and result, inert hostile text, malformed-line notice, reconcile marker. Test 1 asserts no horizontal overflow at 320 and 1440 in both modes.

4.3 Mutation testing

In scratch copies under /tmp/dewey-chat02/, never the served tree. Scripts and output in evidence/console/.

  • Proxy (mutate-proxy.py): 4 of 4 caught. Query dropped, query sent on every route, routes not proxied, routes accept POST.
  • Refusal-status test (mutate-status.py): 2 of 2 caught. unavailable changed to 422, and a new refusal raised from parts.mjs.
  • Conversation view (mutate-console.py), revision 2: 17 of 17 caught, run against both browser test files (mutate-console.txt). The revision 1 mutants: text set as HTML; controls not made visible; fragments replaced instead of joined; details open by default; no branch marker; no newer-session marker; no reconcile marker; the follow poll drops its branch; the follow poll takes view.defaultBranch instead of page.branch; and the two forms sharing the reply-form class. Added in revision 2: Reload drops its branch; "Open the latest branch" keeps the open branch; a gone branch is not reopened; a gone branch is reopened without its marker; "newest" on the same file clears the marker; C1 controls left raw; bidi marks left raw.
  • Revision 1's 10 of 10 is kept as mutate-console-r1.txt.
  • The first pass (mutate-console-pass1.txt) caught 9. It missed the defaultBranch mutant because the fork test reopened the view before the next poll ran. The fork test now adds an entry to the open branch after the fork and requires the next poll to show it with no reconcile marker. Under the mutant, that poll sends main's cursor with the fork's branch and is refused. The second pass caught it.

5. Known limits

  • Polling, not streaming. New entries arrive on the 10 s refresh. Streaming is CHAT-03.
  • A very long branch loads fully. The view reads every page on open; the largest real conversation is 18.6 MB. Windowing is CHAT-05.
  • Collapsed state survives redraws of a message, but a full reopen (picker, reload or branch change) starts collapsed again.
  • Pi only. Claude seats show "not available yet" (D2, B1).

6. After review

  • Pushing goes through Sage with Jason's word.
  • The live WebUI (PID 1266267, running since 2026-09-13 from this checkout) reads its static files from the working tree on every request, so it already serves this candidate's page. Its proxy is the 09-13 code in memory, which has no conversation routes, so History there answers "History unavailable: not found." It must restart after the commit.
  • Brief §4 live check: one board send with a long answer, shown in full in the view. It needs the commit and the WebUI restart, then Jason or Sage.