- built-boot smoke asserts [data-variant=web][data-tool=web_search/fetch]
(the keyed WebRow) instead of [data-web] (which WebBlock draws even on the
GenericToolCard fallback, so a silent keyed-registration failure passed).
- WebBlock renders an explicit empty-state note when a search returns no
answer and no sources, instead of a blank <ol>; the chat row does not
surface the raw result content, so the backend's 'No results found.' was
otherwise invisible.
- README (ui-conversation, ui-primitives) and the frontend Agent Note record
the unknown-web-kind null arm, the details-panel flattened body, and the
empty-search copy; pairings re-recorded.
The coverage lane caught a regression in the prior commit: FileMutationRow now
passes the tool's raw path to the injected openFile (which resolves against cwd
in apply.ts), but diff-card.spec still asserted the row pre-resolved. Assert on
the raw path instead. Also fix the built-boot diff assertion to match on the
line body and footer text — the `+ ` prefix is a CSS ::before, absent from
textContent.
Bring the TUI diff footer onto the same terminator rule and distinct-path
count the Web DiffBlock uses (a trailing newline terminates its line; two
hunks in one file read as 1 file), so the two front ends' `+A -R · N file(s)`
footers agree. Reserve space in the diff path header for the floating copy
button so a long path no longer scrolls under it. Pass the tool's raw path to
the injected openFile (which already resolves against cwd) instead of resolving
twice. Rename the shared block-body CSS class to a card-neutral cardBody so a
terminal-spacing tweak cannot silently move the diff card. Add a same-file
two-hunk TUI unit test and an assembled built-boot assertion that the write
turn renders +1 -0 · 1 file end to end.
The base (feat/web-presenter) removed the content field from
WebSearchResultView/WebFetchResultView: the web card carries no content
copy and a capability-less UI falls back to the raw tool/result content
(web-result-card note). The fixture still constructed both web result
views with a content field, failing the e2e build with TS2353. Drop the
content spread and the stale Omit key; the fixture already emits the same
text as the tool/result content the fallback path renders.
Every expandable tool row shares one interaction (whole-row toggle,
icon-to-chevron hover preview) and one expanded body: an IN/OUT
gutter-labeled card with per-section 150px scroll caps and sticky labels.
toolRowModel derives result output and the error first line, terminalFailed
surfaces a failing exit as the collapsed row's red dot, a hover Inspect
pill jumps to the call's trajectory record through a one-shot store
handoff, and the chat view keeps its scroll offset across view switches.
- webCardModel returns null for an unknown web `kind` (wire from a newer host)
instead of drawing it as a malformed fetch, matching the unknown-`card` and
terminal-model wire-boundary default.
- Fixture WEB_SEARCH_RESULT/WEB_FETCH_RESULT and the source type derive from the
contract's ToolResultView via Extract, so a new contract field fails at the
type level rather than drifting silently.
- built-boot smoke asserts the web_search/web_fetch turns render their keyed
WebRow cards, giving the registration and wire projection an assembled check.
- DetailsPanel comment no longer claims the card omits content for search.
- ui-primitives README inline-Chinese limitation now lists WebBlock's controls.
The per-file coverage gate caught three uncovered paths in the error handling
this round added: the page banner for a failed row removal, the editor card's
transport-rejection catch, and `store.fail` itself.
Two of them are one click each — Remove with a rejecting write, Apply with a
rejecting write — so they are covered through the UI rather than by calling
the helpers directly. The third was a duplicated `error instanceof Error ?
error.message : String(error)` in two files; it becomes one exported
`messageOf`, which removes the branch from both call sites and gives the
fallback arm a home a direct unit test can reach (the lint rule forbids
rejecting a promise with a non-Error, so a rejection cannot exercise it).
The copyText switch's default arm calls assertNever, the closed-union
backstop that the per-file 100% coverage gate cannot reach without a
forged row kind. The assertNever function itself already carries the
v8 ignore; mark the switch arm that reaches it the same way, matching
TodoPanel's StatusGlyph default arm.
Review follow-ups on the presentation intent.
The card claimed any single-question request declaring the intent, then sent one
of two labels — so a producer offering a third option, or a multi-select batch,
lost answers the generic flow would have shown. That contradicts the intent's
own contract, so `planReviewOf` now claims only a binary single choice and
leaves everything else to the flow that can express it.
`ask()` also rejects a plan-review intent on a question with no `detail`: the
intent declares detail IS the plan, and without one a honouring UI asks the
user to approve something invisible. The client keeps its own fallback — it sits
downstream of a wire boundary — but the misconfiguration now fails at the asker.
`planReviewOf` stops being a value export of the client contract face (client
export discipline: pure helpers stay internal; the tests already import it
relatively), and the ui-question README fallback list, both languages, now
states every condition the code enforces.
- DetailsPanel: render the flattened result content below the web card, so a
web_fetch's fetched body (and a search's answer/source markdown) stays
visible on the panel's single-call reading surface — the card is a summary.
- WebBlock: the collapsed source tail keeps each source's original citation
number via <li value>, and the expand control is a marker-less <li> so the
<ol> is valid HTML; an empty-hostname URL (file:/data:) falls back to the raw
URL so a label is never blank.
- web-row / GenericToolCard: both spread WebBlock uniformly with maxSources
(fetch ignores it, like TerminalBlock's maxLines), dropping the duplicated
per-kind conditional.
- Docs: WebBlock added to the ui-primitives README (both languages) with a Web
retrieval section; the ui-conversation README's "inline licensed for this
intent alone" claim de-absolutized and a web-card paragraph added; the Agent
Note's safe-link description corrected to the http(s) subset of MarkdownText's
allowlist (mailto excluded). Fixture source comment aligned with its data.
- Tests: ol numbering + marker-less expander, empty-hostname label fallback,
the fetched body visible in the panel.