plans: brief for the Settings reads and snapshot follow-up to row 155 (Darkwing's draft, Mos's mos-0816 notes, Dewey's F1)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
jason.woltjeandClaude Opus 5.5 committed 2026-10-18 16:19:11 -05:00
1 parent 53f3f29a34
commit 68fc31a944
1 file changed
+87
@@ -0,0 +1,87 @@
# Brief: Settings page reads and snapshots after Q169
Follow-up to row 155 (Q169, #1649), from Mos's two `public/bus.js` notes (mos-0816), which
Mos and Filbert (#1649 comment 29010) ruled non-blocking, a third instance Darkwing found while
checking them, and Dewey's F1 (#1649 comment 29012, withdrawn in 29014). Darkwing drafted it
(agents/darkwing/work/queue-155/followup-brief.md); Sage folded it in here unchanged in
substance. Line numbers below are `packages/webui/src/public/bus.js` and
`packages/webui/src/serve.mjs` as landed by row 155; Sage checked each one on the landing tree.
Owner Darkwing, reviewer Filbert, gate owner Sage. Starts after row 155 lands and builds on
that landing. Done when Filbert approves the candidate manifest, Mos gives the decision 123 go
on the `public/` diff, and the webui suite and every `scripts/test-*.sh` are green on Sage's
gate. Nothing live.
Neither item loses data today. (a) shows an empty name field where it should show the sign-in
state. (b) makes the next save fail with `revision-conflict`, which keeps the draft and tells
the human to reread. The fix makes the page match the other reads and stop sending a revision
it already knows is old.
## Items
1. (a) The names read rethrows like the others. `settingsView` (:674) reads `usage` and
`limits` with a catch that rethrows a 403 or `signed-out` and keeps any other failure local.
The `names` read at :678 catches everything, so a signed-out or forbidden answer becomes "the
name could not be read". Give it the same catch: rethrow 403 and `signed-out`, and keep the
empty field and its text for any other failure. Test (webui `settings-form` or
`assistant`): a fixture whose `names` read answers 403 gets the same page as a 403 on
`usage`, and a fixture whose `names` read fails another way still renders the Assistant
section with "could not be read".
2. (b) One settings snapshot. The page holds the launch settings twice: `settingsSnapshot`
(:559) for the launch form and the token-limit reset, and `assistantSnapshot` (:627) for the
Assistant section. Each save refreshes only its own:
- the launch form save (:587) sets `settingsSnapshot`, not `assistantSnapshot`;
- the assistant save (:666) sets `assistantSnapshot`, not `settingsSnapshot`;
- row 125's reset (:609) sets `settingsSnapshot` when no launch draft is open, never
`assistantSnapshot`.
So a save in one form while the other has an open draft leaves the other with the old
revision, and its next save is refused. The render paths re-snapshot only when their own
form has no draft (:563, :631), which is what keeps the stale copy alive.
Fix: one snapshot for both forms. Every successful save or reset sets it from the answer,
and drafts stay per form (`settingsInputs`, `assistantDrafts`, `nameDrafts`), so an open
draft keeps its typed values over the new snapshot. The launch form payload (:577) already
builds from the snapshot plus its inputs; the assistant payload (:642-653) does the same with
only the changed fields. The reload button (:584) clears the one snapshot. Test, one per
path, in webui `settings-form` (the launch and assistant saves) and `limits-browser` (the
reset):
- with a limits draft open, save the assistant, then submit the limits draft: its posted
revision is the one the assistant save returned, and the typed limit is still in the field;
- with an assistant draft open, save the launch form, then submit the assistant draft: its
posted revision is the one the launch save returned;
- with an assistant draft open, reset a token limit, then submit the assistant draft: its
posted revision is the one the reset returned.
Each test fails on row 155's candidate, where the second post carries the old revision.
3. (F1) Null bodies on the Console write routes. Dewey's F1: `Object.keys(value)` throws on a
JSON `null` in `seatSend` (`packages/webui/src/serve.mjs:241`, the new `/api/name` and
`/api/assistant-stop` routes), `settingsSend` (:203) and `projectFileSend` (:276), and an
async handler throw with no `unhandledRejection` handler ends the Console.
I checked it against the candidate, and that throw can't happen there. Each handler reads
its body through `body()` (:35, from ea00ec66d) inside a `try`, and `body()` parses the JSON
and throws `body must be a JSON object` for `null`, an array or any non-object (:45-46). The
`catch` answers 400 `invalid-request` before the `Object.keys` line runs. Two of the four
already have passing tests for it: `settings-write.test.mjs:17` posts `null` to
`/api/settings` and expects 400, and `project-file.test.mjs:76` ("a body that is JSON but not
an object is refused 400, not thrown on") posts `null`, `[]`, `5`, `"text"` and `true` and
expects 400 with "JSON object". The two `seatSend` routes have no such test.
Dewey withdrew F1 in #1649 comment 29014 (rev 1856) and agreed the item is tests only, with
no local check beyond `body()`. Test, one per `seatSend` route in `assistant.test.mjs`:
posting `null` to `/api/name` and to `/api/assistant-stop` gets 400 `invalid-request`, no
write call is made, and a following valid request still gets an answer. The settings and
project file routes keep their existing tests. Both new tests pass on row 155's candidate;
they pin `body()`'s check on the two routes that lack one.
The shell write-boundary test (`shell.test.mjs`) pins every POST `fetch` and every
`assistantPost(` route in `bus.js`. None of the three adds or changes a write, so its list and
count stay as row 155 leaves them; if the fix touches a `fetch(` line, that test changes with it.
## Out of scope
Server-side settings handling, the routes, and any change to what a save sends beyond the
revision.