presentation.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. /**
  2. * Tool render-intent vocabulary: the provider-neutral types a tool declares via
  3. * `ToolDefinition.presentCall`/`ToolDefinition.presentResult` to say how one of its calls
  4. * renders in a UI (an editor's tool-call card, a CLI log line).
  5. * @module @deepseek-ai/dsh-tools/src/presentation
  6. */
  7. import type { ContentBlock } from '@deepseek-ai/dsh-llm'
  8. /**
  9. * Category of a tool call, used by a UI to pick an icon or treatment. The
  10. * provider-neutral vocabulary lets tools describe themselves without depending
  11. * on a particular client; `other` is the default.
  12. */
  13. export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'
  14. /**
  15. * A file location a tool reads or modifies, so a capable UI can "follow along" —
  16. * highlight or jump to the file (and line) as the tool runs. `path` is what the
  17. * tool operated on (the model-facing path); `line` is an optional 1-based line
  18. * to focus (e.g. a read's offset).
  19. */
  20. export interface FileLocation {
  21. path: string
  22. line?: number
  23. }
  24. /**
  25. * A single-file change a tool is about to make, for a UI that renders inline
  26. * diffs. `oldText` is `null` for a new-file create (nothing to diff against);
  27. * an overwrite also uses `null`, because a call-time presenter has no access to
  28. * the file's prior content.
  29. */
  30. export interface FileDiff {
  31. path: string
  32. /** Prior content, or `null` for a new file / an overwrite (no prior content available at call time). */
  33. oldText: string | null
  34. /** Content after the change. */
  35. newText: string
  36. }
  37. /**
  38. * Provider-neutral pending-call presentation. Tools declare one tagged intent;
  39. * UI bridges map it without special-casing tool names.
  40. */
  41. export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView
  42. /**
  43. * The default card: a titled tool-call row with an optional category icon, a
  44. * salient raw input, extra content blocks, and follow-along file locations. Any
  45. * tool whose call is not a terminal or a diff uses this.
  46. */
  47. export interface GenericCallView {
  48. card: 'generic'
  49. /**
  50. * Human-readable, always-visible label describing what THIS call does. Keep it
  51. * short — a UI shows it as a card header / log line.
  52. */
  53. title: string
  54. /** Category for icon/treatment; defaults to `other` when omitted. */
  55. kind?: ToolCallKind
  56. /**
  57. * The salient input to surface in a detail/expanded view (e.g. a background
  58. * task id). Omit to show nothing; a string renders as-is, an object as pretty
  59. * JSON. NOT the full raw args object unless that is genuinely what a reader wants.
  60. */
  61. rawInput?: unknown
  62. /**
  63. * UI-facing content blocks to show on the pending call alongside the title.
  64. * Omit to show none. A UI maps these to its own content blocks.
  65. */
  66. content?: ContentBlock[]
  67. /** Files this call reads/modifies, for editor follow-along. Omit for a call that touches no file. */
  68. locations?: FileLocation[]
  69. }
  70. /**
  71. * A call that IS a shell command running in a working directory: a capable UI
  72. * renders it as a terminal card (cwd-headed, with the command as the title and
  73. * live/afterward output from the {@link TerminalResultView}); an incapable UI
  74. * falls back to a generic card whose body is the fenced command output. Set by a
  75. * tool whose call is a foreground command (e.g. `bash`).
  76. */
  77. export interface TerminalCallView {
  78. card: 'terminal'
  79. /** The command, shown as the terminal card's title / header line. */
  80. title: string
  81. /**
  82. * A human-readable one-line summary of what the command does, rendered ABOVE
  83. * the terminal card (the card itself has no description slot). Omit for none.
  84. */
  85. description?: string
  86. /**
  87. * Working directory the command runs in, shown as the terminal header. An
  88. * ABSOLUTE path is used as-is; a RELATIVE path is resolved by the UI bridge
  89. * against the session workspace (the pure presenter can't see the session cwd).
  90. * Omit entirely to let the bridge use the session workspace.
  91. */
  92. cwd?: string
  93. }
  94. /**
  95. * A call that creates or modifies files, rendered as an inline diff card by a
  96. * capable UI. Set by a tool whose call writes/edits a file (e.g. `write`,
  97. * `edit`). The diffs are derived from the call ARGUMENTS (a create's `oldText` is
  98. * `null`); the tool emits a separate {@link DiffResultView} after `execute` — the
  99. * applied change (an edit/overwrite hunk with context, or a whole-file diff for a
  100. * create).
  101. */
  102. export interface DiffCallView {
  103. card: 'diff'
  104. /** Card header (e.g. `Write foo.txt`). */
  105. title: string
  106. /** One entry per file the call changes. */
  107. diffs: FileDiff[]
  108. /** Files this call modifies, for editor follow-along (usually the diffs' paths). */
  109. locations?: FileLocation[]
  110. }
  111. /**
  112. * One numbered line of a file, the unit a {@link ReadResultView} carries so a
  113. * capable UI can render a syntax-highlighted, line-numbered code view. `number`
  114. * is the 1-based line number in the file (a window past `offset` keeps the file's
  115. * own numbering, not a 1-based re-count); `text` is the line without its trailing
  116. * newline, already truncated to the read tool's per-line cap.
  117. */
  118. export interface ReadFileLine {
  119. number: number
  120. text: string
  121. }
  122. /**
  123. * How a tool wants the COMPLETED call shown — the *result* state, after `execute`
  124. * returns. A `card`-tagged union mirroring {@link ToolCallView}: a UI switches on
  125. * `card`. Lets the tool reformat its result for a UI distinctly from the
  126. * model-facing text it returned from `execute`. Returned by
  127. * `ToolDefinition.presentResult`; omitting the method keeps the pending
  128. * title and renders the raw result content.
  129. */
  130. export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | ReadResultView | WebResultView
  131. /**
  132. * The default completed card: an optional replacement title and reformatted
  133. * content. Omit a field to keep the pending title / render the raw result content.
  134. */
  135. export interface GenericResultView {
  136. card: 'generic'
  137. /** Replacement title for the completed call. Omit to keep the pending-state title. */
  138. title?: string
  139. /**
  140. * UI-facing result content (harness {@link ContentBlock}s), reformatted from
  141. * the model-facing result. Omit to let the UI render the raw result content.
  142. */
  143. content?: ContentBlock[]
  144. }
  145. /**
  146. * The completed state of a {@link TerminalCallView}: the captured output and exit
  147. * status. A capable UI renders `output` in the terminal card and shows an
  148. * exit-status pill; an incapable UI gets a fenced ```console fallback the BRIDGE
  149. * derives from `output` (the tool does not double-encode it).
  150. */
  151. export interface TerminalResultView {
  152. card: 'terminal'
  153. /** Replacement title for the completed call. Omit to keep the pending-state title. */
  154. title?: string
  155. /** Captured command output (stdout+stderr as the tool chooses to combine them). */
  156. output?: string
  157. /**
  158. * Process exit code, when the run ended by exiting (not a signal). Lets a
  159. * capable UI show an exit-status pill. Omit when killed by a signal or unknown.
  160. */
  161. exitCode?: number
  162. /** Signal name that killed the process (e.g. `SIGTERM`). Mutually exclusive with `exitCode`. */
  163. signal?: string
  164. }
  165. /**
  166. * A completed file mutation rendered as an inline diff card, the result-time
  167. * analogue of {@link DiffCallView}. Because a completed UI update replaces the
  168. * pending card content, mutation tools return this even when it repeats the
  169. * call-time diff; otherwise raw result text would replace the diff.
  170. */
  171. export interface DiffResultView {
  172. card: 'diff'
  173. /** Replacement title for the completed call. Omit to keep the pending-state title. */
  174. title?: string
  175. /** The change to show, in file order — applied contextual hunks, or a whole-file diff when there is no before-image. */
  176. diffs: FileDiff[]
  177. }
  178. /**
  179. * A completed file read rendered as a line-numbered, optionally syntax-highlighted
  180. * code view by a capable UI. Set by a tool whose call reads file text (e.g.
  181. * `read`); the pending state stays a {@link GenericCallView} (`kind: 'read'`)
  182. * because a call carries no content until `execute` returns. The structured
  183. * `lines`/`path`/`lang`/`totalLines` fields cannot be reconstructed from the
  184. * model-facing result text alone, so the read tool projects them through its
  185. * `output.presentationMeta` (persisted with the session log) and `presentResult`
  186. * narrows that metadata back into this view on live and replay paths alike. A UI
  187. * without the read capability falls back to `content` (the model-facing text with
  188. * its envelope stripped), so this view degrades to the generic text card.
  189. */
  190. export interface ReadResultView {
  191. card: 'read'
  192. /** Replacement title for the completed call. Omit to keep the pending-state title. */
  193. title?: string
  194. /** The read file's path (the model-facing path; the bridge relativizes it). */
  195. path: string
  196. /**
  197. * The 1-based first line the window requested, preserved even when `lines` is
  198. * empty (a byte cap below the first selected line yields an empty window) so a
  199. * UI knows where the window starts and where a continuation resumes.
  200. */
  201. offset: number
  202. /** The returned window's lines, in file order, each keeping its file line number. */
  203. lines: ReadFileLine[]
  204. /** Exact total line count in the file, so a UI can show a "showing N of M" affordance. */
  205. totalLines: number
  206. /**
  207. * A syntax-highlighting language hint derived from the file extension (e.g.
  208. * `ts`, `py`), or omitted when the extension maps to no known language so a UI
  209. * renders the lines as plain text.
  210. */
  211. lang?: string
  212. /**
  213. * The model-facing result content with its envelope stripped, for a UI without
  214. * the read capability. Omit to let such a UI render the raw result content.
  215. */
  216. content?: ContentBlock[]
  217. }
  218. /**
  219. * One citeable source in a completed {@link WebSearchResultView}, the faithful
  220. * projection of one web-search source. The presentation projection of `dsh-web`'s
  221. * `WebSearchSource`: that seam type is the authoritative shape (core cannot depend
  222. * on the web seam, so the two are declared separately and MUST evolve together).
  223. * A web tool projects this shape through `output.presentationMeta` because the
  224. * render text cannot losslessly carry it (see the web-result-card Agent Note); its
  225. * `presentResult` reads it back.
  226. */
  227. export interface WebSource {
  228. /** The source URL. */
  229. url: string
  230. /** The source title, when the provider returned one. */
  231. title?: string
  232. /** A short excerpt or summary, when the provider returned one. */
  233. snippet?: string
  234. /** Publication/crawl timestamp as a provider-supplied ISO-8601 string, when present. */
  235. publishedAt?: string
  236. }
  237. /**
  238. * A completed web retrieval rendered as a structured card by a capable UI. Set
  239. * by a web tool whose call retrieves from the web (`web_search`, `web_fetch`).
  240. * One `kind`-tagged union carries both shapes because both are web retrieval and
  241. * a UI renders them with one component family; a UI switches on `kind`. An
  242. * incapable UI falls back to the raw `tool/result` content (this view carries no
  243. * `content` copy — see the web-result-card Agent Note). This is the result-time
  244. * analogue of the `web_search`/`web_fetch` calls' generic call views
  245. * (`kind: 'search'`/`'fetch'`); those tools keep their generic pending card and
  246. * add only this completed card.
  247. *
  248. * The `kind` field here is this union's own discriminant, NOT a
  249. * {@link ToolCallKind}: the two values deliberately match the tools' pending
  250. * `ToolCallKind` (`'search'`/`'fetch'`) so a call and its result read as one
  251. * category, but a new arm is a union edit plus a consumer branch, not any
  252. * arbitrary `ToolCallKind` value.
  253. */
  254. export type WebResultView = WebSearchResultView | WebFetchResultView
  255. /**
  256. * The completed state of a `web_search` call: the structured sources the model
  257. * cited, an optional provider answer, and whether the source list was cut to the
  258. * result cap. A capable UI renders the sources as a citation list; a UI without
  259. * the `web` capability falls back to the raw `tool/result` content.
  260. */
  261. export interface WebSearchResultView {
  262. card: 'web'
  263. kind: 'search'
  264. /** Replacement title for the completed call. Omit to keep the pending-state title. */
  265. title?: string
  266. /** The faithful, structured sources — the field render text cannot losslessly carry. */
  267. sources: WebSource[]
  268. /** The provider-generated answer text, when any. */
  269. answer?: string
  270. /** True when the seam cut the source list to honor the result cap. */
  271. truncated: boolean
  272. }
  273. /**
  274. * The completed state of a `web_fetch` call: the fetched URL, its HTTP status,
  275. * and whether the content was cut. The body itself is already markdown in the
  276. * raw `tool/result` content, so this card carries only the retrieval summary and
  277. * a UI without the `web` capability falls back to that content.
  278. */
  279. export interface WebFetchResultView {
  280. card: 'web'
  281. kind: 'fetch'
  282. /** Replacement title for the completed call. Omit to keep the pending-state title. */
  283. title?: string
  284. /** The final URL after allowed redirects. */
  285. url: string
  286. /** HTTP status code of the fetched response. */
  287. statusCode: number
  288. /**
  289. * True when the provider capped the decoded body, or the output cap or a
  290. * pre-conversion source cut trimmed the rendered text (the effective
  291. * truncation the model-facing text also reflects).
  292. */
  293. truncated: boolean
  294. }