feat(fs): add a search render-intent card for grep and glob results

grep and glob returned only model-facing text; the structured matches/paths
never reached the client. Add a card:'search' result view with a kind
discriminant ('matches' grouped by file for grep, 'paths' for glob), projected
through each tool's output.presentationMeta and read back in presentResult. The
projections re-apply the same inline cap and per-line budget as the render text
and report total + truncated, so a UI never presents a capped page as complete.
A UI without the search card falls back to content; the TUI is unchanged. The
web consumer is a follow-up.
This commit is contained in:
Chinesezjc
2026-07-30 17:03:54 +08:00
parent 69214b4708
commit 3e22adab28
14 changed files with 628 additions and 12 deletions

View File

@@ -82,6 +82,11 @@ export type {
GenericResultView,
TerminalResultView,
DiffResultView,
SearchResultView,
SearchMatchesResultView,
SearchPathsResultView,
SearchFileMatches,
SearchLineMatch,
} from './presentation.ts'
declare module 'cordis' {

View File

@@ -125,7 +125,7 @@ export interface DiffCallView {
* `ToolDefinition.presentResult`; omitting the method keeps the pending
* title and renders the raw result content.
*/
export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView
export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | SearchResultView
/**
* The default completed card: an optional replacement title and reformatted
@@ -176,3 +176,90 @@ export interface DiffResultView {
/** The change to show, in file order — applied contextual hunks, or a whole-file diff when there is no before-image. */
diffs: FileDiff[]
}
/** One matched line inside a {@link SearchFileMatches} group: its 1-based line number and text. */
export interface SearchLineMatch {
/** 1-based line number of the match within its file. */
lineNumber: number
/** The matched line text, as the tool surfaced it (the per-line preview budget already applied). */
line: string
}
/** One file's grouped content matches for a {@link SearchMatchesResultView}, in first-seen file order. */
export interface SearchFileMatches {
/** The file the matches belong to (the model-facing display path). */
path: string
/** The file's matched lines, in output order. */
matches: SearchLineMatch[]
}
/**
* 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}.
*/
export interface SearchMatchesResultView {
card: 'search'
kind: '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. */
files: SearchFileMatches[]
/**
* Whether the tool capped the inline result: `files` carries only the retained
* matches, not every match the search found. A UI shows a capped indicator so it
* never presents a partial group as complete.
*/
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}.
*/
export interface SearchPathsResultView {
card: 'search'
kind: '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`). */
paths: string[]
/**
* Whether the tool capped the inline result: `paths` carries only the retained
* page, not every path the search found. A UI shows a capped indicator so it
* never presents a partial list as complete.
*/
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
* ({@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`.
*/
export type SearchResultView = SearchMatchesResultView | SearchPathsResultView