refactor(fs): minimize and cap search card meta; keep TUI byte-identical

Address the review of the search render card:

- The search result view carries no `content`: it was a no-op for every
  consumer and serialized the whole search text twice. A UI without a search
  card falls back to the raw tool/result content; the TUI stays byte-identical
  to the pre-search-card generic fallback.
- Bound the serialized presentationMeta with a configurable searchMetaMaxBytes
  (default 64 KiB): the inline item cap does not bound bytes, and spill-policy
  only shrinks content, never meta. capMetaBytes drops trailing groups/paths.
- Share one retention pass (retainGrepMatches/retainGlobPaths in search-core)
  between the model-facing render and the meta projection; remove the second
  cap/preview implementation and the presentation<->grep module cycle by
  moving GrepMatch/previewLine to search-core.
- Rename the result-view discriminant kind -> shape so it no longer collides
  with GenericCallView.kind (ToolCallKind, whose values include 'search').
- Narrow the entry export surface to consumed symbols.
- Sync the three bilingual ToolResultView doc pairs and the Agent Note pair;
  document the deliberate empty-card acceptance vs diffsFromMeta.
- Regenerate config/tool/cordis catalogs for the new config field.
This commit is contained in:
Chinesezjc
2026-07-30 21:57:49 +08:00
parent 74060dfb86
commit 7b6f33f872
23 changed files with 403 additions and 228 deletions
+14 -24
View File
@@ -196,12 +196,14 @@ export interface SearchFileMatches {
/**
* A completed content search (`grep`) rendered as a search card whose matches are
* grouped by file, so a capable UI can list each file as an expandable group of
* its matched lines. `kind: 'matches'` discriminates this shape from the path
* shape ({@link SearchPathsResultView}) within {@link SearchResultView}.
* its matched lines. `shape: 'matches'` discriminates this variant from the path
* variant ({@link SearchPathsResultView}) within {@link SearchResultView}. The
* discriminant is `shape`, not `kind`, so it never collides with the
* {@link ToolCallKind} `kind` an icon-picking bridge reads off a call view.
*/
export interface SearchMatchesResultView {
card: 'search'
kind: 'matches'
shape: 'matches'
/** Replacement title for the completed call. Omit to keep the pending-state title. */
title?: string
/** Matched lines grouped by file, in first-seen file order. */
@@ -214,22 +216,16 @@ export interface SearchMatchesResultView {
truncated: boolean
/** Total matches the search found before capping (equals the retained count when not `truncated`). */
total: number
/**
* UI-facing content blocks reproducing the model-facing result text, so a UI
* without a dedicated search card renders it as text. Omit to let the UI render
* the raw result content.
*/
content?: ContentBlock[]
}
/**
* A completed path search (`glob`) rendered as a search card whose result is a flat
* path list. `kind: 'paths'` discriminates this shape from the grouped-matches
* shape ({@link SearchMatchesResultView}) within {@link SearchResultView}.
* path list. `shape: 'paths'` discriminates this variant from the grouped-matches
* variant ({@link SearchMatchesResultView}) within {@link SearchResultView}.
*/
export interface SearchPathsResultView {
card: 'search'
kind: 'paths'
shape: 'paths'
/** Replacement title for the completed call. Omit to keep the pending-state title. */
title?: string
/** The discovered paths, in the tool's result order (the retained page when `truncated`). */
@@ -242,24 +238,18 @@ export interface SearchPathsResultView {
truncated: boolean
/** Total paths the search found before capping (equals `paths.length` when not `truncated`). */
total: number
/**
* UI-facing content blocks reproducing the model-facing result text, so a UI
* without a dedicated search card renders it as text. Omit to let the UI render
* the raw result content.
*/
content?: ContentBlock[]
}
/**
* A completed search rendered as a search card, the result-time view a discovery
* tool (`grep`, `glob`) returns from `presentResult`. One `card: 'search'` view
* with two `kind`-discriminated shapes: grouped-by-file content matches
* with two `shape`-discriminated variants: grouped-by-file content matches
* ({@link SearchMatchesResultView}) and a flat path list
* ({@link SearchPathsResultView}). Both carry a `truncated`/`total` signal so a UI
* never presents a capped result as complete, and an optional `content` a UI
* without a search card renders as text. There is no call-time analogue: a search
* call stays a {@link GenericCallView} (`kind: 'search'`) because the pending
* state has no matches or paths to show — the structured shape exists only after
* `execute`.
* never presents a capped result as complete. The view carries no result text: a
* UI without a search card falls back to the raw `tool/result` content. There is
* no call-time analogue: a search call stays a {@link GenericCallView}
* (`kind: 'search'`) because the pending state has no matches or paths to show —
* the structured shape exists only after `execute`.
*/
export type SearchResultView = SearchMatchesResultView | SearchPathsResultView