@linxiraos/pi-tui 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/CHANGELOG.md +245 -224
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/status-notice.d.ts +2 -0
  21. package/dist/types/chrome/transcript-container.d.ts +2 -0
  22. package/dist/types/components/image.d.ts +2 -0
  23. package/dist/types/components/markdown.d.ts +49 -3
  24. package/dist/types/components/text.d.ts +2 -0
  25. package/dist/types/debug-server.d.ts +2 -0
  26. package/dist/types/keybinding-matchers.d.ts +6 -0
  27. package/dist/types/multiplexers/tmux.d.ts +1 -0
  28. package/dist/types/multiplexers/types.d.ts +6 -0
  29. package/dist/types/native/backend.d.ts +5 -4
  30. package/dist/types/native/blobs.d.ts +24 -12
  31. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  32. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  33. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  34. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  35. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  36. package/dist/types/overlays/hook-editor.d.ts +7 -2
  37. package/dist/types/overlays/hook-input.d.ts +12 -4
  38. package/dist/types/overlays/job-tree.d.ts +50 -0
  39. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  40. package/dist/types/overlays/login-dialog.d.ts +8 -3
  41. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  42. package/dist/types/overlays/model-browser.d.ts +6 -1
  43. package/dist/types/overlays/model-picker.d.ts +2 -0
  44. package/dist/types/overlays/pause-screen.d.ts +5 -0
  45. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  46. package/dist/types/overlays/report-panel.d.ts +5 -1
  47. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  48. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  49. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  50. package/dist/types/overlays/session-selector.d.ts +3 -1
  51. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  52. package/dist/types/prompt/composer.d.ts +23 -20
  53. package/dist/types/prompt/custom-editor.d.ts +20 -10
  54. package/dist/types/prompt/image-references.d.ts +22 -0
  55. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  56. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  57. package/dist/types/prompt/prose-gate.d.ts +3 -0
  58. package/dist/types/prompt/queued-messages.d.ts +1 -1
  59. package/dist/types/prompt/typing-word.d.ts +25 -0
  60. package/dist/types/prompt/welcome.d.ts +26 -6
  61. package/dist/types/render/hanging-wrap.d.ts +7 -0
  62. package/dist/types/render/hyperlink.d.ts +2 -0
  63. package/dist/types/render/render-utils.d.ts +9 -1
  64. package/dist/types/render/utils.d.ts +1 -1
  65. package/dist/types/setup/scenes/splash.d.ts +6 -5
  66. package/dist/types/status-line/context-usage.d.ts +4 -1
  67. package/dist/types/status-line/host.d.ts +6 -4
  68. package/dist/types/status-line/startup.d.ts +6 -6
  69. package/dist/types/status-line/types.d.ts +3 -0
  70. package/dist/types/stdin-buffer.d.ts +7 -0
  71. package/dist/types/terminal-capabilities.d.ts +16 -1
  72. package/dist/types/terminal-multiplexer.d.ts +7 -0
  73. package/dist/types/terminal.d.ts +8 -4
  74. package/dist/types/theme/symbols.d.ts +1 -1
  75. package/dist/types/theme/theme.d.ts +11 -0
  76. package/dist/types/thinking.d.ts +5 -0
  77. package/dist/types/tmux.d.ts +39 -4
  78. package/dist/types/tools/ast-edit.d.ts +2 -2
  79. package/dist/types/tools/ast-grep.d.ts +2 -2
  80. package/dist/types/tools/bash.d.ts +6 -1
  81. package/dist/types/tools/edit.d.ts +2 -0
  82. package/dist/types/tools/eval.d.ts +53 -4
  83. package/dist/types/tools/find.d.ts +14 -4
  84. package/dist/types/tools/glob.d.ts +3 -3
  85. package/dist/types/tools/grep.d.ts +2 -2
  86. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  87. package/dist/types/tools/native-view.d.ts +1 -1
  88. package/dist/types/tools/read.d.ts +32 -10
  89. package/dist/types/tools/renderer.d.ts +2 -0
  90. package/dist/types/tools/task.d.ts +31 -0
  91. package/dist/types/tools/web-search-types.d.ts +1 -0
  92. package/dist/types/tools/write.d.ts +2 -0
  93. package/dist/types/tui.d.ts +20 -3
  94. package/package.json +10 -10
  95. package/src/app-keybindings.d.ts +18 -8
  96. package/src/app-keybindings.ts +20 -8
  97. package/src/apps/autoresearch-dashboard.ts +444 -27
  98. package/src/apps/debug/raw-sse.ts +2 -2
  99. package/src/apps/git/diff-pane.ts +39 -41
  100. package/src/apps/git/git-tui.ts +80 -14
  101. package/src/apps/git/sidebar.ts +10 -1
  102. package/src/apps/git/state.ts +12 -0
  103. package/src/autocomplete.d.ts +2 -0
  104. package/src/autocomplete.ts +21 -19
  105. package/src/bracketed-paste.ts +9 -2
  106. package/src/charts/chart-svg.d.ts +7 -13
  107. package/src/charts/chart-svg.ts +19 -22
  108. package/src/charts/table-data.ts +17 -6
  109. package/src/chat/advisor-message.d.ts +4 -1
  110. package/src/chat/advisor-message.ts +77 -24
  111. package/src/chat/assistant-message.d.ts +9 -4
  112. package/src/chat/assistant-message.ts +127 -31
  113. package/src/chat/bash-execution.ts +7 -2
  114. package/src/chat/figure-markdown.d.ts +7 -2
  115. package/src/chat/figure-markdown.ts +10 -3
  116. package/src/chat/image-loading.ts +9 -6
  117. package/src/chat/messages.d.ts +3 -1
  118. package/src/chat/messages.ts +4 -1
  119. package/src/chat/read-tool-group.d.ts +2 -2
  120. package/src/chat/read-tool-group.ts +97 -42
  121. package/src/chat/svg-figure.ts +3 -1
  122. package/src/chat/svg-source.ts +6 -3
  123. package/src/chat/table-chart.d.ts +11 -15
  124. package/src/chat/table-chart.ts +24 -57
  125. package/src/chat/tool-execution.d.ts +32 -20
  126. package/src/chat/tool-execution.ts +103 -36
  127. package/src/chat/user-message.d.ts +3 -2
  128. package/src/chat/user-message.ts +54 -34
  129. package/src/chrome/countdown-timer.ts +3 -3
  130. package/src/chrome/diff.d.ts +2 -3
  131. package/src/chrome/diff.ts +4 -9
  132. package/src/chrome/status-notice.ts +5 -0
  133. package/src/chrome/transcript-container.d.ts +2 -0
  134. package/src/chrome/transcript-container.ts +212 -28
  135. package/src/components/editor.ts +12 -12
  136. package/src/components/image.d.ts +2 -0
  137. package/src/components/image.ts +54 -20
  138. package/src/components/markdown.d.ts +49 -3
  139. package/src/components/markdown.ts +482 -12
  140. package/src/components/settings-list.ts +2 -4
  141. package/src/components/text.d.ts +2 -0
  142. package/src/components/text.ts +11 -1
  143. package/src/debug-server.d.ts +2 -0
  144. package/src/debug-server.ts +51 -69
  145. package/src/hotkeys-markdown.ts +2 -0
  146. package/src/keybinding-matchers.d.ts +6 -0
  147. package/src/keybinding-matchers.ts +19 -0
  148. package/src/multiplexers/tmux.d.ts +1 -0
  149. package/src/multiplexers/tmux.ts +3 -0
  150. package/src/multiplexers/types.d.ts +6 -0
  151. package/src/multiplexers/types.ts +6 -0
  152. package/src/native/backend.d.ts +5 -4
  153. package/src/native/backend.ts +13 -5
  154. package/src/native/blobs.d.ts +24 -12
  155. package/src/native/blobs.ts +56 -7
  156. package/src/native/reconcile.ts +43 -21
  157. package/src/overlays/advisor-config.ts +259 -8
  158. package/src/overlays/agent-hub-projection.d.ts +10 -5
  159. package/src/overlays/agent-hub-projection.ts +63 -12
  160. package/src/overlays/annotation-overlay.ts +422 -168
  161. package/src/overlays/approval-dialog.ts +389 -0
  162. package/src/overlays/ask-dialog.ts +244 -105
  163. package/src/overlays/btw-history-panel.ts +208 -147
  164. package/src/overlays/extensions/extension-list.ts +4 -6
  165. package/src/overlays/hook-editor.ts +90 -32
  166. package/src/overlays/hook-input.ts +105 -19
  167. package/src/overlays/job-tree.ts +177 -0
  168. package/src/overlays/jobs-panel.ts +222 -142
  169. package/src/overlays/login-dialog.ts +201 -91
  170. package/src/overlays/mcp-add-wizard.ts +562 -279
  171. package/src/overlays/model-browser.ts +32 -7
  172. package/src/overlays/model-hub.ts +4 -10
  173. package/src/overlays/model-picker.ts +58 -21
  174. package/src/overlays/pause-screen.ts +81 -23
  175. package/src/overlays/plan-review-overlay.ts +16 -0
  176. package/src/overlays/plugin-selector.ts +17 -1
  177. package/src/overlays/queue-mode-selector.ts +10 -0
  178. package/src/overlays/report-panel.ts +6 -2
  179. package/src/overlays/reset-usage-selector.ts +130 -21
  180. package/src/overlays/session-account-selector.ts +36 -0
  181. package/src/overlays/session-info-overlay.ts +199 -76
  182. package/src/overlays/session-selector.ts +15 -10
  183. package/src/overlays/settings-selector.ts +3 -0
  184. package/src/overlays/show-images-selector.ts +1 -0
  185. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  186. package/src/overlays/theme-selector.ts +6 -1
  187. package/src/overlays/thinking-selector.ts +8 -4
  188. package/src/overlays/tree-selector.ts +117 -2
  189. package/src/overlays/usage-dashboard.ts +498 -144
  190. package/src/prompt/composer-cache.ts +4 -1
  191. package/src/prompt/composer.ts +200 -74
  192. package/src/prompt/custom-editor.ts +168 -65
  193. package/src/prompt/image-references.d.ts +22 -0
  194. package/src/prompt/image-references.ts +79 -2
  195. package/src/prompt/internal-url-autocomplete.ts +1 -0
  196. package/src/prompt/macos-spelling.ts +28 -5
  197. package/src/prompt/prose-gate.ts +3 -0
  198. package/src/prompt/queued-messages.ts +9 -2
  199. package/src/prompt/tips.txt +1 -2
  200. package/src/prompt/typing-word.ts +59 -0
  201. package/src/prompt/welcome.ts +97 -45
  202. package/src/render/code-cell.ts +18 -5
  203. package/src/render/hanging-wrap.d.ts +7 -0
  204. package/src/render/hanging-wrap.ts +58 -0
  205. package/src/render/hyperlink.d.ts +2 -0
  206. package/src/render/hyperlink.ts +5 -0
  207. package/src/render/render-utils.d.ts +9 -1
  208. package/src/render/render-utils.ts +16 -2
  209. package/src/render/utils.d.ts +1 -1
  210. package/src/render/utils.ts +1 -1
  211. package/src/render/width-aware-text.ts +6 -5
  212. package/src/setup/scenes/splash.ts +22 -8
  213. package/src/status-line/component.ts +79 -11
  214. package/src/status-line/context-usage.ts +176 -73
  215. package/src/status-line/footer.ts +18 -35
  216. package/src/status-line/host.ts +6 -1
  217. package/src/status-line/segments.ts +16 -5
  218. package/src/status-line/startup.ts +6 -6
  219. package/src/status-line/types.ts +1 -0
  220. package/src/stdin-buffer.d.ts +7 -0
  221. package/src/stdin-buffer.ts +300 -62
  222. package/src/terminal-capabilities.d.ts +16 -1
  223. package/src/terminal-capabilities.ts +66 -28
  224. package/src/terminal-multiplexer.d.ts +7 -0
  225. package/src/terminal-multiplexer.ts +14 -0
  226. package/src/terminal.d.ts +8 -4
  227. package/src/terminal.ts +190 -63
  228. package/src/theme/symbols.d.ts +1 -1
  229. package/src/theme/symbols.ts +1 -0
  230. package/src/theme/theme-class.ts +1 -0
  231. package/src/theme/theme.d.ts +11 -0
  232. package/src/theme/theme.ts +46 -14
  233. package/src/thinking.d.ts +5 -0
  234. package/src/thinking.ts +9 -1
  235. package/src/tmux.d.ts +39 -4
  236. package/src/tmux.ts +110 -16
  237. package/src/tools/ask.ts +20 -9
  238. package/src/tools/ast-edit.d.ts +2 -2
  239. package/src/tools/ast-edit.ts +30 -7
  240. package/src/tools/ast-grep.d.ts +2 -2
  241. package/src/tools/ast-grep.ts +39 -35
  242. package/src/tools/bash.d.ts +6 -1
  243. package/src/tools/bash.ts +15 -9
  244. package/src/tools/debug.ts +2 -1
  245. package/src/tools/default-renderer.ts +3 -2
  246. package/src/tools/edit.d.ts +2 -0
  247. package/src/tools/edit.ts +100 -21
  248. package/src/tools/eval.d.ts +53 -4
  249. package/src/tools/eval.ts +173 -137
  250. package/src/tools/find.d.ts +14 -4
  251. package/src/tools/find.ts +71 -18
  252. package/src/tools/glob.d.ts +3 -3
  253. package/src/tools/glob.ts +51 -17
  254. package/src/tools/grep.d.ts +2 -2
  255. package/src/tools/grep.ts +99 -22
  256. package/src/tools/grouped-file-output.d.ts +9 -4
  257. package/src/tools/grouped-file-output.ts +21 -6
  258. package/src/tools/lsp.ts +30 -18
  259. package/src/tools/mcp.ts +2 -2
  260. package/src/tools/native-view.d.ts +1 -1
  261. package/src/tools/native-view.ts +2 -2
  262. package/src/tools/read.d.ts +32 -10
  263. package/src/tools/read.ts +89 -43
  264. package/src/tools/renderer.d.ts +2 -0
  265. package/src/tools/renderer.ts +2 -0
  266. package/src/tools/task.d.ts +31 -0
  267. package/src/tools/task.ts +40 -7
  268. package/src/tools/todo.ts +3 -2
  269. package/src/tools/wait.ts +16 -30
  270. package/src/tools/web-search-types.d.ts +1 -0
  271. package/src/tools/web-search-types.ts +1 -0
  272. package/src/tools/web-search.ts +2 -1
  273. package/src/tools/write.d.ts +2 -0
  274. package/src/tools/write.ts +36 -12
  275. package/src/tools/yield.ts +2 -2
  276. package/src/tui.d.ts +20 -3
  277. package/src/tui.ts +160 -43
  278. package/src/utils.ts +5 -3
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  280. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
package/src/tools/lsp.ts CHANGED
@@ -28,6 +28,8 @@ import {
28
28
  } from "../render/render-utils";
29
29
  import { renderStatusLine } from "../render";
30
30
  import { framedToolCard } from "../render/tool-card";
31
+ import { wrapGuidedLine, wrapPrefixedLine } from "../render/hanging-wrap";
32
+ import { visibleWidth } from "../utils";
31
33
  import { ansi, code, col, compact, keyed, md, node, row, span, text } from "../native/describe";
32
34
  import type { NativeNode } from "../native/node";
33
35
  import { OwnerMemo } from "../native/memo";
@@ -245,7 +247,7 @@ export function renderResult(
245
247
  return { label: "Response", state: "success", bodyLines: renderGeneric(text, lines, expanded, theme) };
246
248
  };
247
249
 
248
- return framedToolCard(theme, () => {
250
+ return framedToolCard(theme, ({ contentWidth }) => {
249
251
  // Read mutable state at render time
250
252
  const { expanded, isPartial, spinnerFrame } = options;
251
253
  const slot = expanded ? 1 : 0;
@@ -263,12 +265,22 @@ export function renderResult(
263
265
  : formatStatusIcon(isPartial ? "running" : "error", theme, spinnerFrame);
264
266
  const header = `${icon} LSP ${actionLabel}`;
265
267
 
268
+ // Hover code hangs under its rail; expanded rows hang under their tree
269
+ // connector or guide rails so Ctrl+O reveals full text instead of clipping.
270
+ const rail = ` ${theme.fg("mdCodeBlockBorder", theme.boxRound.vertical)} `;
271
+ const contentLines = codeBlockMatch
272
+ ? bodyLines.flatMap(line =>
273
+ line.startsWith(rail) ? wrapPrefixedLine(rail, rail, line.slice(rail.length), contentWidth) : [line],
274
+ )
275
+ : expanded
276
+ ? bodyLines.flatMap(line => wrapGuidedLine(line, contentWidth, theme))
277
+ : bodyLines;
266
278
  return {
267
279
  header,
268
280
  phase: isPartial ? "partial" : state,
269
281
  sections: [
270
282
  ...(requestLines.length > 0 ? [{ content: requestLines }] : []),
271
- { label: theme.fg("toolTitle", "Response"), content: bodyLines },
283
+ { label: theme.fg("toolTitle", "Response"), content: contentLines },
272
284
  ],
273
285
  applyBg: false,
274
286
  };
@@ -413,10 +425,7 @@ function renderDiagnostics(
413
425
  )}`;
414
426
  const message = formatDiagnosticMessage(item);
415
427
  if (message) {
416
- output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg(
417
- "muted",
418
- truncateToWidth(message, TRUNCATE_LENGTHS.LINE),
419
- )}`;
428
+ output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg("muted", message)}`;
420
429
  }
421
430
  }
422
431
  return output.split("\n");
@@ -427,7 +436,10 @@ function renderDiagnostics(
427
436
  parsedDiagnostics.length > 0 ? parsedDiagnostics.slice(0, 3) : fallbackDiagnostics.slice(0, 3);
428
437
  const remaining =
429
438
  (parsedDiagnostics.length > 0 ? parsedDiagnostics.length : fallbackDiagnostics.length) - previewItems.length;
430
- const expandHint = formatExpandHint(theme, expanded, remaining > 0);
439
+ const clipped = previewItems.some(
440
+ item => !("raw" in item) && visibleWidth(formatDiagnosticMessage(item)) > TRUNCATE_LENGTHS.CONTENT,
441
+ );
442
+ const expandHint = formatExpandHint(theme, expanded, remaining > 0 || clipped);
431
443
  let output = `${icon} ${theme.fg("dim", meta.join(theme.sep.dot))}${expandHint}`;
432
444
  for (let i = 0; i < previewItems.length; i++) {
433
445
  const item = previewItems[i];
@@ -479,8 +491,9 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
479
491
 
480
492
  const files = Array.from(byFile.keys());
481
493
 
482
- const renderGrouped = (maxFiles: number, maxLocsPerFile: number, showHint: boolean): string => {
483
- const expandHint = formatExpandHint(theme, undefined, showHint);
494
+ const renderGrouped = (maxFiles: number, maxLocsPerFile: number): string => {
495
+ const hidden = files.length > maxFiles || files.some(file => byFile.get(file)!.length > maxLocsPerFile);
496
+ const expandHint = formatExpandHint(theme, expanded, hidden);
484
497
  let output = `${icon} ${theme.fg("dim", `${refCount} found`)}${expandHint}`;
485
498
 
486
499
  const filesToShow = files.slice(0, maxFiles);
@@ -506,10 +519,9 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
506
519
  `line ${line}, col ${col}`,
507
520
  )}`;
508
521
  if (expanded) {
509
- const context = `at ${file}:${line}:${col}`;
510
522
  output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locCont)}${theme.fg(
511
523
  "muted",
512
- truncateToWidth(context, TRUNCATE_LENGTHS.LINE),
524
+ `at ${file}:${line}:${col}`,
513
525
  )}`;
514
526
  }
515
527
  }
@@ -532,11 +544,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
532
544
  return output;
533
545
  };
534
546
 
535
- if (expanded) {
536
- return renderGrouped(files.length, 3, false).split("\n");
537
- }
538
-
539
- return renderGrouped(3, 1, true).split("\n");
547
+ return (expanded ? renderGrouped(files.length, Number.POSITIVE_INFINITY) : renderGrouped(3, 1)).split("\n");
540
548
  }
541
549
 
542
550
  // =============================================================================
@@ -658,13 +666,17 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
658
666
  for (let i = 0; i < lines.length; i++) {
659
667
  const isLast = i === lines.length - 1;
660
668
  const branch = isLast ? theme.tree.last : theme.tree.branch;
661
- output += `\n ${theme.fg("dim", branch)} ${truncateToWidth(replaceTabs(lines[i]), TRUNCATE_LENGTHS.CONTENT)}`;
669
+ output += `\n ${theme.fg("dim", branch)} ${replaceTabs(lines[i])}`;
662
670
  }
663
671
  return output.split("\n");
664
672
  }
665
673
 
666
674
  const firstLine = lines[0] || "No output";
667
- const expandHint = formatExpandHint(theme, expanded, lines.length > 1);
675
+ const expandHint = formatExpandHint(
676
+ theme,
677
+ expanded,
678
+ lines.length > 1 || visibleWidth(firstLine) > TRUNCATE_LENGTHS.TITLE,
679
+ );
668
680
  let output = `${icon} ${theme.fg("dim", truncateToWidth(firstLine, TRUNCATE_LENGTHS.TITLE))}${expandHint}`;
669
681
 
670
682
  if (lines.length > 1) {
package/src/tools/mcp.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  renderJsonTreeLines,
27
27
  } from "./json-tree";
28
28
  import { formatStyledTruncationWarning, stripOutputNotice } from "./output-meta";
29
- import { formatExpandHint, truncateToWidth } from "../render/render-utils";
29
+ import { clipCollapsed, formatExpandHint } from "../render/render-utils";
30
30
  import { formatOutputPaneLines, styleToolOutputLine } from "../render/output-pane";
31
31
  import type { StatusLineOptions } from "../render/status-line";
32
32
  import { plainToolCard, type ToolCardPhase } from "../render/tool-card";
@@ -267,7 +267,7 @@ export function renderMCPResult(
267
267
  expanded,
268
268
  collapsedMaxLines: 4,
269
269
  expandedMaxLines: 12,
270
- styleLine: line => truncateToWidth(styleToolOutputLine(line, theme), contentWidth),
270
+ styleLine: line => clipCollapsed(styleToolOutputLine(line, theme), contentWidth, expanded),
271
271
  showExpandHintWhenUncapped: true,
272
272
  },
273
273
  theme,
@@ -85,7 +85,7 @@ export interface RunFootInput {
85
85
  readonly state: RunState;
86
86
  /** Exit code of a `failed` run; a non-zero code names the state `exit N`. */
87
87
  readonly exitCode?: number;
88
- /** Milliseconds run so far (`running`) or in total; the time is omitted when undefined. */
88
+ /** Milliseconds run so far (`running`, a live `background` job) or in total; omitted when undefined. */
89
89
  readonly elapsedMs?: number;
90
90
  /** Quiet facts (job id, service state, artifact), muted and ` · `-joined. */
91
91
  readonly facts?: readonly string[];
@@ -216,7 +216,7 @@ export interface RunFootInput {
216
216
  readonly state: RunState;
217
217
  /** Exit code of a `failed` run; a non-zero code names the state `exit N`. */
218
218
  readonly exitCode?: number;
219
- /** Milliseconds run so far (`running`) or in total; the time is omitted when undefined. */
219
+ /** Milliseconds run so far (`running`, a live `background` job) or in total; omitted when undefined. */
220
220
  readonly elapsedMs?: number;
221
221
  /** Quiet facts (job id, service state, artifact), muted and ` · `-joined. */
222
222
  readonly facts?: readonly string[];
@@ -250,7 +250,7 @@ export function runFoot(input: RunFootInput): NativeNode {
250
250
  ? undefined
251
251
  : node(
252
252
  "elapsed",
253
- input.state === "running"
253
+ input.state === "running" || input.state === "background"
254
254
  ? { role: "zeta.run.time", age: ms, format: "short" }
255
255
  : { role: "zeta.run.time", age: ms, stopped: ms, format: "short" },
256
256
  undefined,
@@ -1,6 +1,6 @@
1
1
  import type { SummaryResult } from "@linxiraos/pi-natives";
2
2
  import type { Component } from "../tui";
3
- import type { NativeToolView, RenderResultOptions, ToolActivityContext, ToolActivitySummary } from "./renderer";
3
+ import type { RenderResultContextOptions, NativeToolView, RenderResultOptions, ToolActivityContext, ToolActivitySummary } from "./renderer";
4
4
  import type { NativeNode } from "../native/node";
5
5
  import type { Theme } from "../theme/theme";
6
6
  import type { OutputMeta } from "./output-meta";
@@ -20,6 +20,12 @@ export interface ReadToolDetails {
20
20
  truncation?: ReadTruncationStats;
21
21
  isDirectory?: boolean;
22
22
  resolvedPath?: string;
23
+ /**
24
+ * Absolute path of the disk file whose image this read returned. A native TSP host that
25
+ * supports `media` shows the file by path instead of the uploaded image bytes. Unset for
26
+ * screenshots, URL reads and other images not backed by a file. Display-only.
27
+ */
28
+ mediaPath?: string;
23
29
  suffixResolution?: {
24
30
  from: string;
25
31
  to: string;
@@ -34,6 +40,11 @@ export interface ReadToolDetails {
34
40
  fileSize?: number;
35
41
  /** Full source line count when the read reached EOF and the count is exact. */
36
42
  totalLines?: number;
43
+ /**
44
+ * Whether the text's rows carry line-number prefixes (`N:` hashline, `N|` line numbers). Set where a
45
+ * file read is formatted; a raw read sets `false`. Snapcompact reads it to tell a `12:` prefix from content.
46
+ */
47
+ rowPrefixes?: boolean;
37
48
  /** Raw text + start line for user-visible TUI rendering, set when content is text-like.
38
49
  * Mirrors the same lines the model receives but without hashline/line-number prefixes,
39
50
  * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
@@ -49,7 +60,10 @@ export interface ReadToolDetails {
49
60
  };
50
61
  /** Number of unresolved git conflicts surfaced by this read (TUI uses for inline `⚠ N` badge). */
51
62
  conflictCount?: number;
52
- /** Paths recovered from a delimited read argument; used only by the TUI to render one call as multiple read rows. */
63
+ /**
64
+ * Paths recovered from a delimited read argument. The TUI renders one call as multiple read rows; snapcompact
65
+ * keeps such a composite result as text.
66
+ */
53
67
  displayReadTargets?: string[];
54
68
  /**
55
69
  * Resolved filesystem link target for each {@link displayReadTargets} entry, aligned by index; `null` when a
@@ -107,14 +121,22 @@ export declare function pendingFileLinkPath(inputPath: string): string;
107
121
  * target when the structured `resolvedPath` isn't set (the common plain-file and
108
122
  * image reads only record the path in `meta.source`). URL/internal sources are
109
123
  * not fs paths, so only `type: "path"` qualifies. */
110
- export declare function readSourceFsPath(details: ReadToolDetails | undefined): string | undefined;
124
+ export declare function readSourceFsPath(details: unknown): string | undefined;
125
+ /**
126
+ * Whether a read result renders as a Markdown preview rather than numbered source:
127
+ * the read tool tagged it `text/markdown` (`read.renderMarkdown`, or a protocol
128
+ * resource) and the call did not ask for `:raw` source. Shared by the standalone
129
+ * read card and the read group's content previews so both honor the setting.
130
+ */
131
+ export declare function readResultIsMarkdown(details: ReadToolDetails | undefined, args: ReadRenderArgs | undefined): boolean;
111
132
  /**
112
- * Read content as numbered `code` (no path header: the head names the file).
113
- * Numbers come from `lineNumbers` (`displayContent`'s per-line numbers,
114
- * `null` for an elided block; runs split with `…` between) when they cover
115
- * every line, else count up from `start`.
133
+ * Read content as numbered, wrapped `code` (no path header: the head names
134
+ * the file), matching the ANSI preview's hanging wrap. Numbers come from
135
+ * `lineNumbers` (`displayContent`'s per-line numbers, `null` for an elided
136
+ * block; runs split with `…` between) when they cover every line, else count
137
+ * up from `start`.
116
138
  */
117
- export declare function readContentCode(content: string, start: number, numbers: readonly (number | null)[] | undefined, lang: string | undefined): NativeNode[];
139
+ export declare function readContentCode(content: string, start: number, numbers: readonly (number | null)[] | undefined, path: string | undefined): NativeNode[];
118
140
  /** Render file, image, and URL reads in the transcript. */
119
141
  export declare const readToolRenderer: {
120
142
  activitySummary(args: unknown, _context: ToolActivityContext): ToolActivitySummary;
@@ -126,7 +148,7 @@ export declare const readToolRenderer: {
126
148
  }>;
127
149
  details?: ReadToolDetails;
128
150
  isError?: boolean;
129
- }, options: RenderResultOptions, uiTheme: Theme, args?: ReadRenderArgs): Component;
151
+ }, options: RenderResultContextOptions, uiTheme: Theme, args?: ReadRenderArgs): Component;
130
152
  describeCall(args: ReadRenderArgs): NativeToolView;
131
153
  /**
132
154
  * Inline (§7.3 read): the head names the file and range; the content
@@ -140,7 +162,7 @@ export declare const readToolRenderer: {
140
162
  }>;
141
163
  details?: ReadToolDetails;
142
164
  isError?: boolean;
143
- }, _options: RenderResultOptions, args?: ReadRenderArgs): NativeToolView;
165
+ }, options: RenderResultContextOptions, args?: ReadRenderArgs): NativeToolView;
144
166
  mergeCallAndResult: true;
145
167
  };
146
168
  /** Inclusive line range describing one elided span in a structural summary. */
package/src/tools/read.ts CHANGED
@@ -4,10 +4,12 @@ import { LINE_RANGE_CHUNK_SOURCE, parseLineRanges } from "./line-ranges";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { parseArchivePathCandidates } from "@linxiraos/pi-utils/ar";
7
+ import { isRecord, sanitizeText } from "@linxiraos/pi-utils";
7
8
  import type { Component } from "../tui";
8
9
  import { Text } from "../components/text";
9
10
  import type {
10
11
  NativeToolHead,
12
+ RenderResultContextOptions,
11
13
  NativeToolView,
12
14
  RenderResultOptions,
13
15
  ToolActivityContext,
@@ -15,7 +17,7 @@ import type {
15
17
  ToolRenderer,
16
18
  } from "./renderer";
17
19
  import { ansi, compact, md } from "../native/describe";
18
- import { base64ImageNode } from "../native/blobs";
20
+ import { base64ImageNode, nativeMediaNode, readMediaPath } from "../native/blobs";
19
21
  import type { NativeChild, NativeNode } from "../native/node";
20
22
  import { fileHref, footnoteText, inlineErrorView, resultText } from "./native-view";
21
23
  import { numberedCode } from "./grouped-file-output";
@@ -54,6 +56,12 @@ export interface ReadToolDetails {
54
56
  truncation?: ReadTruncationStats;
55
57
  isDirectory?: boolean;
56
58
  resolvedPath?: string;
59
+ /**
60
+ * Absolute path of the disk file whose image this read returned. A native TSP host that
61
+ * supports `media` shows the file by path instead of the uploaded image bytes. Unset for
62
+ * screenshots, URL reads and other images not backed by a file. Display-only.
63
+ */
64
+ mediaPath?: string;
57
65
  suffixResolution?: { from: string; to: string };
58
66
  url?: string;
59
67
  finalUrl?: string;
@@ -65,6 +73,11 @@ export interface ReadToolDetails {
65
73
  fileSize?: number;
66
74
  /** Full source line count when the read reached EOF and the count is exact. */
67
75
  totalLines?: number;
76
+ /**
77
+ * Whether the text's rows carry line-number prefixes (`N:` hashline, `N|` line numbers). Set where a
78
+ * file read is formatted; a raw read sets `false`. Snapcompact reads it to tell a `12:` prefix from content.
79
+ */
80
+ rowPrefixes?: boolean;
68
81
  /** Raw text + start line for user-visible TUI rendering, set when content is text-like.
69
82
  * Mirrors the same lines the model receives but without hashline/line-number prefixes,
70
83
  * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
@@ -76,7 +89,10 @@ export interface ReadToolDetails {
76
89
  summary?: { lines: number; elidedSpans: number; elidedLines: number };
77
90
  /** Number of unresolved git conflicts surfaced by this read (TUI uses for inline `⚠ N` badge). */
78
91
  conflictCount?: number;
79
- /** Paths recovered from a delimited read argument; used only by the TUI to render one call as multiple read rows. */
92
+ /**
93
+ * Paths recovered from a delimited read argument. The TUI renders one call as multiple read rows; snapcompact
94
+ * keeps such a composite result as text.
95
+ */
80
96
  displayReadTargets?: string[];
81
97
  /**
82
98
  * Resolved filesystem link target for each {@link displayReadTargets} entry, aligned by index; `null` when a
@@ -296,9 +312,17 @@ function firstReadSelectorLine(sel: string | undefined): number | undefined {
296
312
  * target when the structured `resolvedPath` isn't set (the common plain-file and
297
313
  * image reads only record the path in `meta.source`). URL/internal sources are
298
314
  * not fs paths, so only `type: "path"` qualifies. */
299
- export function readSourceFsPath(details: ReadToolDetails | undefined): string | undefined {
300
- const source = details?.meta?.source;
301
- return source?.type === "path" && typeof source.value === "string" ? source.value : undefined;
315
+ export function readSourceFsPath(details: unknown): string | undefined {
316
+ if (!isRecord(details) || !isRecord(details.meta) || !isRecord(details.meta.source)) return undefined;
317
+ const source = details.meta.source;
318
+ return source.type === "path" && typeof source.value === "string" ? source.value : undefined;
319
+ }
320
+
321
+ function readLinkTarget(details: unknown): string | undefined {
322
+ if (!isRecord(details)) return undefined;
323
+ if (typeof details.resolvedPath === "string") return details.resolvedPath;
324
+ if (typeof details.displayTarget === "string") return details.displayTarget;
325
+ return readSourceFsPath(details);
302
326
  }
303
327
 
304
328
  function formatReadPathLink(
@@ -314,9 +338,11 @@ function formatReadPathLink(
314
338
  const split = splitReadRenderPath(rawPath);
315
339
  const basePath = split.path || rawPath;
316
340
  const selectorSuffix = split.sel ? `:${split.sel}` : "";
317
- const plainDisplayPath = options.suffixResolution
318
- ? shortenPath(options.suffixResolution.to)
319
- : shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
341
+ const plainDisplayPath = sanitizeText(
342
+ options.suffixResolution
343
+ ? shortenPath(options.suffixResolution.to)
344
+ : shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath),
345
+ );
320
346
  // Calls render before the tool has resolved a filesystem target. Preserve
321
347
  // protocol resources as plain text, but resolve direct relative file paths
322
348
  // so terminals receive an explicit file: link rather than guessing HTTPS.
@@ -335,12 +361,29 @@ function readRawPath(args: ReadRenderArgs | undefined): string {
335
361
  return typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
336
362
  }
337
363
 
364
+ /**
365
+ * Whether a read result renders as a Markdown preview rather than numbered source:
366
+ * the read tool tagged it `text/markdown` (`read.renderMarkdown`, or a protocol
367
+ * resource) and the call did not ask for `:raw` source. Shared by the standalone
368
+ * read card and the read group's content previews so both honor the setting.
369
+ */
370
+ export function readResultIsMarkdown(details: ReadToolDetails | undefined, args: ReadRenderArgs | undefined): boolean {
371
+ if (details?.contentType !== "text/markdown" || args?.raw === true) return false;
372
+ const sel = splitReadRenderPath(readRawPath(args)).sel;
373
+ return sel?.split(":").some(chunk => chunk.toLowerCase() === "raw") !== true;
374
+ }
375
+
338
376
  /**
339
377
  * Native read head (§7.3 read): `Read path:13-36` with the path as the
340
378
  * target (shortened, plus the selector / legacy offset range), a `file://`
341
379
  * link when the fs path is known, and a suffix correction as meta.
342
380
  */
343
- function readNativeHead(rawPath: string, args: ReadRenderArgs | undefined, details?: ReadToolDetails): NativeToolHead {
381
+ function readNativeHead(
382
+ rawPath: string,
383
+ args: ReadRenderArgs | undefined,
384
+ details?: ReadToolDetails,
385
+ linkPath?: string,
386
+ ): NativeToolHead {
344
387
  const split = splitReadRenderPath(rawPath);
345
388
  const suffix = details?.suffixResolution;
346
389
  let target = suffix ? shortenPath(suffix.to) : shortenPath(split.path || rawPath);
@@ -359,9 +402,9 @@ function readNativeHead(rawPath: string, args: ReadRenderArgs | undefined, detai
359
402
  const conflicts = details?.conflictCount ?? 0;
360
403
  return {
361
404
  title: "Read",
362
- target: target || "…",
405
+ target: sanitizeText(target) || "…",
363
406
  targetKind: "path",
364
- href: fileHref(details?.resolvedPath ?? details?.displayTarget ?? readSourceFsPath(details)),
407
+ href: fileHref(linkPath ?? details?.resolvedPath ?? details?.displayTarget ?? readSourceFsPath(details)),
365
408
  meta,
366
409
  badges:
367
410
  conflicts > 0 ? [{ text: `${conflicts} conflict${conflicts === 1 ? "" : "s"}`, tone: "warning" }] : undefined,
@@ -378,21 +421,22 @@ function readRangeStart(rawPath: string, args: ReadRenderArgs | undefined): numb
378
421
  }
379
422
 
380
423
  /**
381
- * Read content as numbered `code` (no path header: the head names the file).
382
- * Numbers come from `lineNumbers` (`displayContent`'s per-line numbers,
383
- * `null` for an elided block; runs split with `…` between) when they cover
384
- * every line, else count up from `start`.
424
+ * Read content as numbered, wrapped `code` (no path header: the head names
425
+ * the file), matching the ANSI preview's hanging wrap. Numbers come from
426
+ * `lineNumbers` (`displayContent`'s per-line numbers, `null` for an elided
427
+ * block; runs split with `…` between) when they cover every line, else count
428
+ * up from `start`.
385
429
  */
386
430
  export function readContentCode(
387
431
  content: string,
388
432
  start: number,
389
433
  numbers: readonly (number | null)[] | undefined,
390
- lang: string | undefined,
434
+ path: string | undefined,
391
435
  ): NativeNode[] {
392
436
  const lines = content.split("\n");
393
437
  return numberedCode(
394
438
  lines.map((text, i) => ({ n: numbers && numbers.length === lines.length ? numbers[i]! : start + i, text })),
395
- { lang },
439
+ { path, wrap: true },
396
440
  );
397
441
  }
398
442
 
@@ -435,7 +479,7 @@ export const readToolRenderer = {
435
479
  typeof input?.file_path === "string" ? input.file_path : typeof input?.path === "string" ? input.path : "";
436
480
  const routed = readUrlCard(rawPath);
437
481
  if (routed) return { label: routed.card.label, detail: routed.target || routed.card.rootDetail };
438
- return { label: "Read", detail: shortenPath(rawPath) };
482
+ return { label: "Read", detail: sanitizeText(shortenPath(rawPath)) };
439
483
  },
440
484
  renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
441
485
  const rawPath =
@@ -462,10 +506,11 @@ export const readToolRenderer = {
462
506
 
463
507
  renderResult(
464
508
  result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails; isError?: boolean },
465
- options: RenderResultOptions,
509
+ options: RenderResultContextOptions,
466
510
  uiTheme: Theme,
467
511
  args?: ReadRenderArgs,
468
512
  ): Component {
513
+ const linkPath = readLinkTarget(options.renderContext?.rawDetails);
469
514
  const urlDetails = result.details as ReadUrlToolDetails | undefined;
470
515
  const baseRawPathForKind =
471
516
  typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
@@ -492,8 +537,8 @@ export const readToolRenderer = {
492
537
  const filePath =
493
538
  formatReadPathLink(rawPath, {
494
539
  offset: args?.offset,
495
- sourcePath: result.details?.displayTarget ?? readSourceFsPath(result.details),
496
- }) || shortenPath(rawPath);
540
+ sourcePath: linkPath ?? result.details?.displayTarget ?? readSourceFsPath(result.details),
541
+ }) || sanitizeText(shortenPath(rawPath));
497
542
  let title = filePath ? `Read ${filePath}` : "Read";
498
543
  if (args?.offset !== undefined || args?.limit !== undefined) {
499
544
  const startLine = args.offset ?? 1;
@@ -525,7 +570,9 @@ export const readToolRenderer = {
525
570
  const truncation = details?.meta?.truncation;
526
571
  const fallback = details?.truncation;
527
572
  if (details?.resolvedPath) {
528
- warningLines.push(uiTheme.fg("dim", wrapBrackets(`Resolved path: ${details.resolvedPath}`, uiTheme)));
573
+ warningLines.push(
574
+ uiTheme.fg("dim", wrapBrackets(`Resolved path: ${shortenPath(details.resolvedPath)}`, uiTheme)),
575
+ );
529
576
  }
530
577
  if (truncation) {
531
578
  if (fallback?.firstLineExceedsLimit) {
@@ -543,7 +590,7 @@ export const readToolRenderer = {
543
590
  if (imageContent) {
544
591
  const suffix = details?.suffixResolution;
545
592
  const displayPath = formatReadPathLink(rawPath, {
546
- resolvedPath: details?.resolvedPath,
593
+ resolvedPath: linkPath ?? details?.resolvedPath,
547
594
  sourcePath: details?.displayTarget ?? readSourceFsPath(details),
548
595
  suffixResolution: suffix,
549
596
  fallbackLabel: "image",
@@ -575,7 +622,7 @@ export const readToolRenderer = {
575
622
  // reads only record the absolute path in meta.source, so fall back to that
576
623
  // to keep the title clickable.
577
624
  const displayPath = formatReadPathLink(rawPath, {
578
- resolvedPath: details?.resolvedPath,
625
+ resolvedPath: linkPath ?? details?.resolvedPath,
579
626
  sourcePath: details?.displayTarget ?? readSourceFsPath(details),
580
627
  suffixResolution: suffix,
581
628
  offset: args?.offset,
@@ -594,9 +641,7 @@ export const readToolRenderer = {
594
641
  const n = details.conflictCount;
595
642
  title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
596
643
  }
597
- const rawRequested =
598
- args?.raw === true || renderPath.sel?.split(":").some(chunk => chunk.toLowerCase() === "raw") === true;
599
- const isMarkdown = details?.contentType === "text/markdown" && !rawRequested;
644
+ const isMarkdown = readResultIsMarkdown(details, args);
600
645
  let cachedWidth: number | undefined;
601
646
  let cachedExpanded: boolean | undefined;
602
647
  let cachedLines: string[] | undefined;
@@ -659,9 +704,10 @@ export const readToolRenderer = {
659
704
  */
660
705
  describeResult(
661
706
  result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails; isError?: boolean },
662
- _options: RenderResultOptions,
707
+ options: RenderResultContextOptions,
663
708
  args?: ReadRenderArgs,
664
709
  ): NativeToolView {
710
+ const linkPath = readLinkTarget(options.renderContext?.rawDetails);
665
711
  const details = result.details;
666
712
  const rawPath = readRawPath(args);
667
713
  const routed = readUrlCard(rawPath, details);
@@ -677,11 +723,11 @@ export const readToolRenderer = {
677
723
 
678
724
  if (result.isError) {
679
725
  const message = (resultText(result) || "Unknown error").replace(/^Error:\s*/, "");
680
- return inlineErrorView(readNativeHead(rawPath, args, details), message);
726
+ return inlineErrorView(readNativeHead(rawPath, args, details, linkPath), message);
681
727
  }
682
728
 
683
729
  const suffix = details?.suffixResolution;
684
- const head = readNativeHead(rawPath, args, details);
730
+ const head = readNativeHead(rawPath, args, details, linkPath);
685
731
  const truncation = details?.meta?.truncation;
686
732
  const fallback = details?.truncation;
687
733
  const firstLineTooLong = truncation !== undefined && fallback?.firstLineExceedsLimit === true;
@@ -695,6 +741,9 @@ export const readToolRenderer = {
695
741
  );
696
742
 
697
743
  const contentText = details?.displayContent?.text ?? stripOutputNotice(resultText(result), details?.meta);
744
+ // A file Tern's media viewer shows (image, model, PDF, video, audio): the host loads it by path.
745
+ const mediaPath = readMediaPath(options.renderContext?.rawDetails) ?? details?.mediaPath;
746
+ const media = options.media && mediaPath ? nativeMediaNode(mediaPath, "media") : undefined;
698
747
  const imageBlock = result.content?.find(isImageBlock);
699
748
  if (imageBlock) {
700
749
  const detailText = contentText.trim();
@@ -703,7 +752,7 @@ export const readToolRenderer = {
703
752
  tone: suffix ? "warning" : undefined,
704
753
  inline: true,
705
754
  body: compact<NativeChild>([
706
- describeReadImage(imageBlock, shortenPath(suffix?.to ?? splitReadRenderPath(rawPath).path)),
755
+ media ?? describeReadImage(imageBlock, shortenPath(suffix?.to ?? splitReadRenderPath(rawPath).path)),
707
756
  detailText.length > 0 && ansi(detailText),
708
757
  foot,
709
758
  ]),
@@ -711,18 +760,15 @@ export const readToolRenderer = {
711
760
  }
712
761
 
713
762
  const renderPath = splitReadRenderPath(rawPath);
714
- const rawRequested =
715
- args?.raw === true || renderPath.sel?.split(":").some(chunk => chunk.toLowerCase() === "raw") === true;
716
- const content: NativeChild[] =
717
- details?.contentType === "text/markdown" && !rawRequested
718
- ? [md(contentText)]
719
- : readContentCode(
720
- contentText,
721
- details?.displayContent?.startLine ?? readRangeStart(rawPath, args),
722
- details?.displayContent?.lineNumbers,
723
- getLanguageFromPath(renderPath.path),
724
- );
725
- return { tool: head, inline: true, body: compact<NativeChild>([...content, foot]) };
763
+ const content: NativeChild[] = readResultIsMarkdown(details, args)
764
+ ? [md(contentText)]
765
+ : readContentCode(
766
+ contentText,
767
+ details?.displayContent?.startLine ?? readRangeStart(rawPath, args),
768
+ details?.displayContent?.lineNumbers,
769
+ renderPath.path,
770
+ );
771
+ return { tool: head, inline: true, body: compact<NativeChild>([media, ...content, foot]) };
726
772
  },
727
773
  mergeCallAndResult: true,
728
774
  } satisfies ToolRenderer<ReadRenderArgs, ReadToolDetails>;
@@ -33,6 +33,8 @@ export interface RenderResultOptions {
33
33
  elapsedMs?: number;
34
34
  /** The call was abandoned without a final result (turn aborted, sealed). Native describe hooks only. */
35
35
  cancelled?: boolean;
36
+ /** The native host shows files by path (`cx.supports("media")`). Native describe hooks only. */
37
+ media?: boolean;
36
38
  }
37
39
  /** Render options for a result, plus the tool-specific context the transcript threads through. */
38
40
  export type RenderResultContextOptions = RenderResultOptions & {
@@ -34,6 +34,8 @@ export interface RenderResultOptions {
34
34
  elapsedMs?: number;
35
35
  /** The call was abandoned without a final result (turn aborted, sealed). Native describe hooks only. */
36
36
  cancelled?: boolean;
37
+ /** The native host shows files by path (`cx.supports("media")`). Native describe hooks only. */
38
+ media?: boolean;
37
39
  }
38
40
 
39
41
  /** Render options for a result, plus the tool-specific context the transcript threads through. */
@@ -101,6 +101,10 @@ export interface TaskItem {
101
101
  name?: string;
102
102
  /** Agent type to run this item (e.g. "scout"). Defaults to the spawn policy's default agent. */
103
103
  agent?: string;
104
+ /** Historical source session selected by the bundled consult agent. */
105
+ sourceSession?: string;
106
+ /** Optional model selector override for the historical source session. */
107
+ model?: string | string[];
104
108
  /** The work; required by the schema. */
105
109
  task?: string;
106
110
  /** How open-ended the work is; required by the schema and the child's sole `auto` thinking classification input. */
@@ -127,6 +131,10 @@ export interface TaskParams {
127
131
  name?: string;
128
132
  /** Agent type to spawn (flat form); omitted values resolve from the session spawn policy. */
129
133
  agent?: string;
134
+ /** Historical source session selected by the bundled consult agent. */
135
+ sourceSession?: string;
136
+ /** Optional model selector override for the historical source session. */
137
+ model?: string | string[];
130
138
  /** The work (flat form). */
131
139
  task?: string;
132
140
  /** How open-ended the work is (flat form); see {@link TaskItem.solutionSpace}. */
@@ -383,6 +391,29 @@ export interface SingleResult {
383
391
  attempt: number;
384
392
  errorMessage: string;
385
393
  };
394
+ /**
395
+ * Coarse category of a non-zero exit — `usage-limit`, `auth`,
396
+ * `account-policy`, `context-overflow`, `transient`, `aborted`, or
397
+ * `unknown`. A parent agent reads this to tell "swap the model" (the first
398
+ * three) from "fix the request" instead of parsing provider prose.
399
+ */
400
+ failureClass?: "usage-limit" | "auth" | "account-policy" | "context-overflow" | "transient" | "aborted" | "unknown";
401
+ /**
402
+ * Other models this session could run instead (selectors accepted by the
403
+ * task `model` parameter, including `@role` aliases). Empty when nothing
404
+ * else is configured or every alternative is already in cooldown.
405
+ */
406
+ fallbackCandidates?: string[];
407
+ /**
408
+ * Set when a caller-supplied `model` replaced the model this run would
409
+ * otherwise have used — the audit trail of an agent-driven switch.
410
+ */
411
+ modelChange?: {
412
+ source: "spawn-param" | "wake-param";
413
+ /** The model the run would have used without the override, when resolvable. */
414
+ from?: string;
415
+ to: string;
416
+ };
386
417
  /** Output metadata for agent:// URL integration */
387
418
  outputMeta?: {
388
419
  lineCount: number;