The parenthetical listed 14/16 and this branch added 20, but the set never
held to it: IconRightUpOutline14 defaults to 8, IconTreeCorner8x10 to 10,
and IconWarningOutline16 to 14. Drop the list rather than maintain one that
drifts, and retitle the icons test that carried the same generalization.
The session row menu's Archive action carried ic_ds_download_outline_16, a
download tray that reads as "save to disk" rather than "put away". Swap it
for the design's archive glyph — a lidded box with a label slot — added to
the ic_ds_* set as IconArchiveOutline20.
The glyph is native 20, the first of that size in the set, so the menu's
16px icon slot asks for it explicitly. The figma export's 0.11px stroke ring
around the box contour is dropped: it restates the same contour in the same
ink, which currentColor already carries.
The card is a React child of the wrapper, so capture-phase presses on it
reached the wrapper's dismissal handler: the first pointerdown of a text
selection closed the card, contradicting its JSDoc contract. Restrict the
immediate close to presses outside the card, keeping it mounted under a
held press (and the browser click with it), and align onPointerLeave's
grace arming with Menu (only while open).
Pin both new behaviors in hover-card.spec and update the bilingual Agent
Note.
Address the ds-review-bot findings on the search card:
- searchCardModel dropped the result view's `content`, so a capped search's
`Full … stored at: <locator>` recovery footer vanished from the UI (the card
replaces the raw text). Thread it through as `SearchCardModel.recovery` and
render it below the card at all three sites, only when truncated.
- SearchRow's fallback body was gated on `state === 'error'`, so a settled
non-error call with no card (a successful nested run_code sub-dispatch, a
legacy generic result) showed only its summary with content lost. Widen it to
any settled call with `search === null`.
- searchCardModel trusted the `files`/`paths` shape the host wire schema only
string-checks; a malformed known-kind frame would crash SearchBlock. Validate
the full shape and fall to the generic path on mismatch.
- SearchBlock's restored tail file header added a row without consuming a tail
slot, exceeding maxLines by one and overstating the hidden count. Make it
consume a slot so the visible count holds at maxLines and `hidden` stays exact.
Correct the fixture JSDoc (now genuinely exceeds the row cap) and the Agent Note
recovery-text claim, sync the ui-conversation bilingual README with the search
row, and add an assembled keyless snapshot (apps/web/tests/search-card.snapshot.ts)
that pins the grep card's shape from the built bundles.
- 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.
Only TypeScript, shell, and JSON grammars load at Web boot; the read card's
wider langFromPath extension set loads through dynamic imports on first use,
so a session that never opens a read card in one of those languages avoids
~1.6 MB of grammar modules and their synchronous init. ReadBlock/CodeBlock
re-render on grammar-load via useSyncExternalStore, picking up highlighting
once the grammar registers. ReadBlock hides the copy control on an empty
window (a successful read of an empty file settles to lines: [] with
card:'read'), matching TerminalBlock so it cannot wipe the clipboard.
Complete the stalled review pass: drop the unused useMemo import (rows flatten
inline), add SearchBlock to the ui-primitives README (both languages) with a
Search results section, and re-record the doc pairings. Search card behavior
and tests unchanged (313 pass).
The workspace browser's two hover-raised popups both died on the way to
them. HoverCard closed on the first pointerleave and rendered its card
pointer-events:none, but the card sits 8px off the anchor, so every path
to it crossed ground belonging to neither. The row action menus put
closeOnPointerLeave's handler on the portaled list, so aiming back at the
... trigger that opened it, or overshooting a list edge, closed it with no
window to come back.
usePointerGrace owns one cancelable delayed close (200ms) shared by both
atoms: leaving arms it, returning cancels it. The hover card becomes
hit-testable so resting on it holds it open, and Menu moves pointer-leave
dismissal to the wrapper span, where React's enter/leave traversal makes
trigger and portaled list one region.
Both gestures are pinned in the real browser lane; each fails without the
corresponding fix.
- 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.
- DiffBlock: an empty side contributes zero lines and a trailing newline is a
terminator, so a create ending in a newline draws one added line (not a
phantom empty one) and a full deletion draws no phantom + line.
- diffCardModel: narrow the wire diffs payload (card is the only validated
field) so a malformed diff card falls back to the generic path instead of
throwing inside DiffBlock.
- FileMutationRow: surface the result text when an errored mutation has no diff
card, so a failed edit/write is more than a red dot.
- copyText ends its closed union on assertNever.
- Docs: drop the "bridge relativizes" claim, record the file-count divergence
from the TUI footer, correct the built-boot overclaim, note why the row title
outranks the view title, and make fixture turn 67 args self-consistent.
- Tests: terminator/empty-side/interior-blank rows, wire-narrowing null arms,
the error-text arm and its name/code fallback, stopped state, no-path summary,
and the registration/disposal shape.
Consume the card:'read' result view (path, numbered lines, totalLines, lang)
the read backend PR added. ReadBlock (ui-primitives) draws a per-line gutter
with each line's own file number, shiki highlighting via a new highlightLines
returning per-line token arrays, a 显示 X / Y 行 window note, a height cap
matching TerminalBlock, and a copy control. read-card-model is the single
resultView derivation; a keyed ReadRow registers under read with the card
resident under its path-link summary. The generic fallback and the details
panel are read-aware. Fixture gains a windowed read turn for the built-boot
snapshot.
Replace the separate '已截断 · 共 N' pill with '显示 X / 共 N 处匹配 · K 个文件'
(and '显示 X / 共 N 个路径' for glob), mirroring the read card's '显示 X / Y 行',
so the retained count and the pre-cap total read as one clause instead of two
numbers that appear to disagree.
Consume the card:'web' result view (structured sources + answer for search, the
URL and HTTP status for fetch) the web backend PR added. WebBlock
(ui-primitives) draws both kinds via the kind discriminant: a citation list of
safe external links (the MarkdownText protocol allowlist, title-or-hostname
label), a truncation indicator, a height cap matching TerminalBlock; a fetch
summary for the other kind. web-card-model is the single resultView derivation;
a keyed WebRow registers under web_search and web_fetch with the card resident
under its summary. The generic fallback and the details panel are web-aware.
Fixture gains web_search and web_fetch turns for the built-boot snapshot.
Consume the card:'search' result view (matches grouped by file for grep, a
path list for glob) the search backend PR added. SearchBlock (ui-primitives)
draws both kinds via the kind discriminant with a per-file collapse, a
truncation pill, a height cap matching TerminalBlock, and a copy control;
search-card-model is the single resultView derivation; a keyed SearchRow
registers under grep and glob with the card resident under its summary. The
generic fallback and the details panel are search-aware. Fixture gains grep and
glob turns for the built-boot snapshot.
The write/edit tools already declare card:'diff' with applied hunks on
callView/resultView, but the Web client discarded it: a mutation landed on
GenericToolCard and the details panel flattened the result to a <pre>. Add
DiffBlock (ui-primitives), diff-card-model (the single callView/resultView
derivation), and FileMutationRow (keyed under write and edit), and make the
generic fallback row and the details panel diff-aware. The +/- block form,
per-file path header, same-file gap, and footer mirror the TUI diff card;
the chat row caps at CHAT_DIFF_MAX_LINES against the panel's full height.
Unify the /plan claimed hint with the plan placeholder through a locale
namespace, localize slash menu group titles, replace the PermissionSelect
native select with the Menu primitive, add a goal pause verb chain, clamp
anchored popups to the viewport with scroll-into-view and outside-dismiss,
and fix onPasteUpgrade insertedRange to account for the chip trailing gap.