feat(tool-fs): editor-facing presentation for read/write/edit
The fs tools rendered as generic cards (title = tool name, raw file content) in
an ACP editor. Give them tool-owned presentation like bash/subagent have:
- read → title "Read <path>", kind read, offset/limit as rawInput
- write → title "Write <path>", kind edit
- edit → title "Edit <path>", kind edit, a clipped old→new rawInput summary
Add a provider-neutral `locations: { path, line? }[]` to ToolCallPresentation —
the files a call reads/modifies — so a capable editor can follow along / jump to
the file (read carries its offset as the line). The ACP bridge forwards it onto
the wire `tool_call` (ResolvedCallPresentation + call() + the tool_call build in
streamSessionEventUpdate). This flips the `locations` cell in the ACP feature
matrix to supported. The SDK already carries `tool_call.locations`
(ToolCallLocation `{ path, line? }`), so no ACP types leak into dsh-tools.
presentResult is intentionally omitted: it only receives `{ content, isError }`,
not the write/edit outcome, so titling by create-vs-overwrite or replacement
count would mean parsing the model-facing text — the static title stays.
Tests: pure presentCall assertions for all three tools incl. locations and the
edit rawInput clip; a bridge test drives the REAL fs tools through ToolPresenter
and asserts locations reaches the wire tool_call (proven to fail without the
forwarding line). New withFs harness option + dsh-fs devDeps on dsh-acp.
This commit is contained in:
@@ -83,5 +83,18 @@ export function applyEditTool(ctx: Context): void {
|
||||
ctx.emit('fs/observed', target, outcome.version, exec)
|
||||
return [{ type: 'text', text: formatEditOutput(target.displayPath, outcome) }]
|
||||
},
|
||||
// Pure display: `edit` kind, a location for editor follow-along, and a short
|
||||
// old→new summary as rawInput (truncated so a large replacement stays a
|
||||
// readable card). The replacement COUNT is not available here — presentResult
|
||||
// only sees `{ content, isError }`, not the outcome — so the title is static.
|
||||
presentCall(args) {
|
||||
const clip = (s: string): string => (s.length > 40 ? `${s.slice(0, 40)}…` : s)
|
||||
return {
|
||||
title: `Edit ${args.file_path}`,
|
||||
kind: 'edit',
|
||||
rawInput: `${JSON.stringify(clip(args.old_string))} → ${JSON.stringify(clip(args.new_string))}`,
|
||||
locations: [{ path: args.file_path }],
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -101,5 +101,20 @@ export function applyReadTool(ctx: Context): void {
|
||||
ctx.emit('fs/observed', target, info.version, exec)
|
||||
return [{ type: 'text', text: formatReadOutput(target.displayPath, outcome) }]
|
||||
},
|
||||
// Pure display: a UI card titled by the file, `read` kind (icon), and a
|
||||
// location so an editor can follow along to the file (and the read's offset
|
||||
// line). `rawInput` surfaces offset/limit when the model narrowed the read.
|
||||
presentCall(args) {
|
||||
const detail = [
|
||||
...args.offset !== undefined ? [`offset ${args.offset}`] : [],
|
||||
...args.limit !== undefined ? [`limit ${args.limit}`] : [],
|
||||
].join(', ')
|
||||
return {
|
||||
title: `Read ${args.file_path}`,
|
||||
kind: 'read',
|
||||
locations: [{ path: args.file_path, ...args.offset !== undefined ? { line: args.offset } : {} }],
|
||||
...detail.length > 0 ? { rawInput: detail } : {},
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -62,5 +62,12 @@ export function applyWriteTool(ctx: Context): void {
|
||||
ctx.emit('fs/observed', target, outcome.version, exec)
|
||||
return [{ type: 'text', text: formatWriteOutput(target.displayPath, outcome) }]
|
||||
},
|
||||
// Pure display: `edit` kind (an editor treats create/replace as an edit) and
|
||||
// a location so the UI can follow along to the written file. The create-vs-
|
||||
// overwrite fact lives in the model-facing result text; `presentResult` only
|
||||
// sees `{ content, isError }` (not the outcome), so the title stays static.
|
||||
presentCall(args) {
|
||||
return { title: `Write ${args.file_path}`, kind: 'edit', locations: [{ path: args.file_path }] }
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user