fix(web-search-card): surface truncation recovery, widen cardless fallback, validate wire shape, fix tail-cap
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.
This commit is contained in:
@@ -209,17 +209,23 @@ export function SearchBlock(props: SearchBlockProps) {
|
||||
const headLines = Math.ceil(maxLines / 2)
|
||||
const tailLines = maxLines - headLines
|
||||
const head = capped ? rows.slice(0, headLines) : rows
|
||||
const tail = capped ? rows.slice(rows.length - tailLines) : []
|
||||
const naturalTail = capped ? rows.slice(rows.length - tailLines) : []
|
||||
// When the tail slice begins inside a file's matches, its own header sits
|
||||
// above the cut and is not shown, so those rows could not be attributed to a
|
||||
// file. Restore the owning header at the top of the tail — unless the head
|
||||
// slice already carries it (a single large file), where it would duplicate.
|
||||
const tailLead = tail[0]
|
||||
const tailLead = naturalTail[0]
|
||||
const tailHeader = tailLead?.type === 'match'
|
||||
&& !head.some(row => row.type === 'file' && row.index === tailLead.fileIndex)
|
||||
? rows.find((row): row is Extract<SearchRow, { type: 'file' }> =>
|
||||
row.type === 'file' && row.index === tailLead.fileIndex)
|
||||
: undefined
|
||||
// The restored header is itself a row. Left extra it would push the card to
|
||||
// maxLines + 1 and overstate `hidden` by one, so it consumes a tail slot: drop
|
||||
// the tail's first row (the match whose header this is) for it. Visible rows
|
||||
// hold at maxLines and `hidden` stays exact; the dropped match joins the
|
||||
// hidden middle.
|
||||
const tail = tailHeader === undefined ? naturalTail : naturalTail.slice(1)
|
||||
|
||||
const renderRow = (row: SearchRow): ReactNode => {
|
||||
if (row.type === 'path') return <div className={css.line}>{row.path}</div>
|
||||
|
||||
Reference in New Issue
Block a user