feat(fs): add a read render-intent card for the read tool result

The read tool's result carries structured numbered lines, but only the
model-facing envelope text reached the client. Add a card:'read' result view
(ReadResultView) projecting {path, lines, totalLines, lang} through the tool's
output.presentationMeta so presentResult reproduces it on live and replay
paths; the pending call stays a generic read card. A UI without the read
capability falls back to the envelope-stripped content, so the TUI is
unchanged. The web consumer that renders the line-numbered view is a follow-up.
This commit is contained in:
Chinesezjc
2026-07-30 17:03:05 +08:00
parent 69214b4708
commit eb4cc8efc5
9 changed files with 389 additions and 11 deletions

View File

@@ -74,6 +74,7 @@ export type {
ToolCallKind,
FileLocation,
FileDiff,
ReadFileLine,
ToolCallView,
GenericCallView,
TerminalCallView,
@@ -82,6 +83,7 @@ export type {
GenericResultView,
TerminalResultView,
DiffResultView,
ReadResultView,
} from './presentation.ts'
declare module 'cordis' {

View File

@@ -117,6 +117,18 @@ export interface DiffCallView {
locations?: FileLocation[]
}
/**
* One numbered line of a file, the unit a {@link ReadResultView} carries so a
* capable UI can render a syntax-highlighted, line-numbered code view. `number`
* is the 1-based line number in the file (a window past `offset` keeps the file's
* own numbering, not a 1-based re-count); `text` is the line without its trailing
* newline, already truncated to the read tool's per-line cap.
*/
export interface ReadFileLine {
number: number
text: string
}
/**
* How a tool wants the COMPLETED call shown — the *result* state, after `execute`
* returns. A `card`-tagged union mirroring {@link ToolCallView}: a UI switches on
@@ -125,7 +137,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 | ReadResultView
/**
* The default completed card: an optional replacement title and reformatted
@@ -176,3 +188,38 @@ 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[]
}
/**
* A completed file read rendered as a line-numbered, optionally syntax-highlighted
* code view by a capable UI. Set by a tool whose call reads file text (e.g.
* `read`); the pending state stays a {@link GenericCallView} (`kind: 'read'`)
* because a call carries no content until `execute` returns. The structured
* `lines`/`path`/`lang`/`totalLines` fields cannot be reconstructed from the
* model-facing result text alone, so the read tool projects them through its
* `output.presentationMeta` (persisted with the session log) and `presentResult`
* narrows that metadata back into this view on live and replay paths alike. A UI
* without the read capability falls back to `content` (the model-facing text with
* its envelope stripped), so this view degrades to the generic text card.
*/
export interface ReadResultView {
card: 'read'
/** Replacement title for the completed call. Omit to keep the pending-state title. */
title?: string
/** The read file's path (the model-facing path; the bridge relativizes it). */
path: string
/** The returned window's lines, in file order, each keeping its file line number. */
lines: ReadFileLine[]
/** Exact total line count in the file, so a UI can show a "showing N of M" affordance. */
totalLines: number
/**
* A syntax-highlighting language hint derived from the file extension (e.g.
* `ts`, `py`), or omitted when the extension maps to no known language so a UI
* renders the lines as plain text.
*/
lang?: string
/**
* The model-facing result content with its envelope stripped, for a UI without
* the read capability. Omit to let such a UI render the raw result content.
*/
content?: ContentBlock[]
}