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]>
162 lines
10 KiB
Markdown
162 lines
10 KiB
Markdown
# CHAT-02 Console: review packet (#1507, row 5)
|
||
|
||
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.
|
||
|
||
## 1. Candidate hashes
|
||
|
||
```
|
||
77039b18845c913dfcbfaf4cd6854ed97865a14eb762773af6f6d19cb704fc11 packages/webui/README.md
|
||
68a054ee61f033f200a5d53b16b3274cbfb89d8bd065c8a049d5f804d51fe0a7 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
|
||
9ed68e39904181ba37961d22eea200b024344ef5a7de06bd7c2e8a41ebb1b0e0 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 (`␛`, `␇`, `␡`); bidi overrides and isolates show as `[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.** Three markers, each with 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."
|
||
- `reconcile`: a refusal with `reconcile: true`. The view keeps what it
|
||
showed, stops polling and offers "Reload conversation".
|
||
- **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 13 and seat 19, 185 of 185 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 185/185.
|
||
|
||
### 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 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 or U+202E 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; the reopen then shows the fork |
|
||
| No history, non-reply seats | test 3 |
|
||
| 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`): 10 of 10 caught, run against
|
||
both browser test files. The 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.
|
||
- 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 (`mutate-console.txt`) catches 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.
|