Rewrite the Agent Note's stale 'TUI has no web arm' claim to match the
web fallback branch this PR added to transcript.ts. Replace the hardcoded
line-number comment with a symbolic reference, and mark the web arm's
unreachable optional-chain undefined side with a reasoned v8 ignore for
the per-file 100% branch gate.
Route a `web` result card's raw-content fallback through the TUI's dim
Markdown path (render() only recognized `card: 'generic'` as markdown
content, so web fallback rendered as bare undimmed text).
Memoize renderFetchOutput per (result, maxOutputChars) so the registry's
twin output.render / output.presentationMeta calls on the same frozen
result run one HTML->markdown conversion instead of two.
Remove the trailing `</content>`/`</invoke>` protocol residue from both
sides of the web-result-card Agent Note and re-record the pairing.
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
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.
The Web composer now renders a request that declares the `plan-review` intent
as its own surface: the waiting-approval card language — amber "Plan review"
strip, the plan as the scrolling markdown body, the question as the card's
accessible name — over one decision row of Chat about it / Refuse / Approve.
Gone from that surface are the 1/1 pager, the numbered radio rows, the custom
answer row, and Skip/Submit, which made approving a plan read as sitting an
exam.
Approve and Refuse answer with the asker's own option labels and keep its
descriptions as tooltips; Chat about it cancels the request so the composer
returns and the user can simply say what they want. Copy is bilingual under the
existing `question` namespace.
The shape choice lives inside the single composer entry rather than a second
chain registration, so the two surfaces cannot race the same carrier, and
`planReviewOf` falls back to the generic flow for any request it cannot render
as a card — the client sits downstream of a wire boundary and every request
must stay answerable.
web_search and web_fetch returned only model-facing text, whose markdown source
list is lossy (title-or-hostname label, snippet and date concatenated), so a
client could not recover the structured sources. Add a card:'web' result view
with a kind discriminant ('search' carrying structured sources + answer +
truncated, 'fetch' carrying url + statusCode + truncated), projected through
each tool's output.presentationMeta and read back in presentResult. A UI
without the web card falls back to content; the TUI is unchanged. The web
consumer is a follow-up.