@linxiraos/pi-tui 1.2.1 → 1.3.0

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 (274) hide show
  1. package/CHANGELOG.md +188 -125
  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/transcript-container.d.ts +2 -0
  21. package/dist/types/components/image.d.ts +2 -0
  22. package/dist/types/components/markdown.d.ts +49 -3
  23. package/dist/types/components/text.d.ts +2 -0
  24. package/dist/types/debug-server.d.ts +2 -0
  25. package/dist/types/keybinding-matchers.d.ts +6 -0
  26. package/dist/types/multiplexers/tmux.d.ts +1 -0
  27. package/dist/types/multiplexers/types.d.ts +6 -0
  28. package/dist/types/native/backend.d.ts +5 -4
  29. package/dist/types/native/blobs.d.ts +24 -12
  30. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  31. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  32. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  33. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  34. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  35. package/dist/types/overlays/hook-editor.d.ts +7 -2
  36. package/dist/types/overlays/hook-input.d.ts +12 -4
  37. package/dist/types/overlays/job-tree.d.ts +50 -0
  38. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  39. package/dist/types/overlays/login-dialog.d.ts +8 -3
  40. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  41. package/dist/types/overlays/model-browser.d.ts +6 -1
  42. package/dist/types/overlays/model-picker.d.ts +2 -0
  43. package/dist/types/overlays/pause-screen.d.ts +5 -0
  44. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  45. package/dist/types/overlays/report-panel.d.ts +5 -1
  46. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  47. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  48. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  49. package/dist/types/overlays/session-selector.d.ts +3 -1
  50. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  51. package/dist/types/prompt/composer.d.ts +21 -20
  52. package/dist/types/prompt/custom-editor.d.ts +20 -10
  53. package/dist/types/prompt/image-references.d.ts +22 -0
  54. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  55. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  56. package/dist/types/prompt/prose-gate.d.ts +3 -0
  57. package/dist/types/prompt/queued-messages.d.ts +1 -1
  58. package/dist/types/prompt/typing-word.d.ts +25 -0
  59. package/dist/types/prompt/welcome.d.ts +26 -6
  60. package/dist/types/render/hanging-wrap.d.ts +7 -0
  61. package/dist/types/render/hyperlink.d.ts +2 -0
  62. package/dist/types/render/render-utils.d.ts +9 -1
  63. package/dist/types/render/utils.d.ts +1 -1
  64. package/dist/types/setup/scenes/splash.d.ts +6 -5
  65. package/dist/types/status-line/context-usage.d.ts +4 -1
  66. package/dist/types/status-line/host.d.ts +6 -4
  67. package/dist/types/status-line/startup.d.ts +6 -6
  68. package/dist/types/status-line/types.d.ts +3 -0
  69. package/dist/types/stdin-buffer.d.ts +7 -0
  70. package/dist/types/terminal-capabilities.d.ts +16 -1
  71. package/dist/types/terminal-multiplexer.d.ts +7 -0
  72. package/dist/types/terminal.d.ts +8 -4
  73. package/dist/types/theme/symbols.d.ts +1 -1
  74. package/dist/types/theme/theme.d.ts +11 -0
  75. package/dist/types/thinking.d.ts +5 -0
  76. package/dist/types/tmux.d.ts +39 -4
  77. package/dist/types/tools/ast-edit.d.ts +2 -2
  78. package/dist/types/tools/ast-grep.d.ts +2 -2
  79. package/dist/types/tools/bash.d.ts +6 -1
  80. package/dist/types/tools/edit.d.ts +2 -0
  81. package/dist/types/tools/eval.d.ts +53 -4
  82. package/dist/types/tools/find.d.ts +14 -4
  83. package/dist/types/tools/glob.d.ts +3 -3
  84. package/dist/types/tools/grep.d.ts +2 -2
  85. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  86. package/dist/types/tools/native-view.d.ts +1 -1
  87. package/dist/types/tools/read.d.ts +32 -10
  88. package/dist/types/tools/renderer.d.ts +2 -0
  89. package/dist/types/tools/task.d.ts +31 -0
  90. package/dist/types/tools/write.d.ts +2 -0
  91. package/dist/types/tui.d.ts +20 -3
  92. package/package.json +10 -10
  93. package/src/app-keybindings.d.ts +18 -8
  94. package/src/app-keybindings.ts +20 -8
  95. package/src/apps/autoresearch-dashboard.ts +444 -27
  96. package/src/apps/debug/raw-sse.ts +2 -2
  97. package/src/apps/git/diff-pane.ts +39 -41
  98. package/src/apps/git/git-tui.ts +80 -14
  99. package/src/apps/git/sidebar.ts +10 -1
  100. package/src/apps/git/state.ts +12 -0
  101. package/src/autocomplete.d.ts +2 -0
  102. package/src/autocomplete.ts +21 -19
  103. package/src/bracketed-paste.ts +9 -2
  104. package/src/charts/chart-svg.d.ts +7 -13
  105. package/src/charts/chart-svg.ts +19 -22
  106. package/src/chat/advisor-message.d.ts +4 -1
  107. package/src/chat/advisor-message.ts +77 -24
  108. package/src/chat/assistant-message.d.ts +9 -4
  109. package/src/chat/assistant-message.ts +127 -31
  110. package/src/chat/bash-execution.ts +7 -2
  111. package/src/chat/figure-markdown.d.ts +7 -2
  112. package/src/chat/figure-markdown.ts +10 -3
  113. package/src/chat/image-loading.ts +9 -6
  114. package/src/chat/messages.d.ts +3 -1
  115. package/src/chat/messages.ts +4 -1
  116. package/src/chat/read-tool-group.d.ts +2 -2
  117. package/src/chat/read-tool-group.ts +97 -42
  118. package/src/chat/svg-figure.ts +3 -1
  119. package/src/chat/svg-source.ts +6 -3
  120. package/src/chat/table-chart.d.ts +11 -15
  121. package/src/chat/table-chart.ts +24 -57
  122. package/src/chat/tool-execution.d.ts +32 -20
  123. package/src/chat/tool-execution.ts +103 -36
  124. package/src/chat/user-message.d.ts +3 -2
  125. package/src/chat/user-message.ts +54 -34
  126. package/src/chrome/countdown-timer.ts +3 -3
  127. package/src/chrome/diff.d.ts +2 -3
  128. package/src/chrome/diff.ts +4 -9
  129. package/src/chrome/transcript-container.d.ts +2 -0
  130. package/src/chrome/transcript-container.ts +212 -28
  131. package/src/components/editor.ts +12 -12
  132. package/src/components/image.d.ts +2 -0
  133. package/src/components/image.ts +54 -20
  134. package/src/components/markdown.d.ts +49 -3
  135. package/src/components/markdown.ts +461 -11
  136. package/src/components/settings-list.ts +2 -4
  137. package/src/components/text.d.ts +2 -0
  138. package/src/components/text.ts +11 -1
  139. package/src/debug-server.d.ts +2 -0
  140. package/src/debug-server.ts +51 -69
  141. package/src/hotkeys-markdown.ts +2 -0
  142. package/src/keybinding-matchers.d.ts +6 -0
  143. package/src/keybinding-matchers.ts +19 -0
  144. package/src/multiplexers/tmux.d.ts +1 -0
  145. package/src/multiplexers/tmux.ts +3 -0
  146. package/src/multiplexers/types.d.ts +6 -0
  147. package/src/multiplexers/types.ts +6 -0
  148. package/src/native/backend.d.ts +5 -4
  149. package/src/native/backend.ts +13 -5
  150. package/src/native/blobs.d.ts +24 -12
  151. package/src/native/blobs.ts +56 -7
  152. package/src/native/reconcile.ts +43 -21
  153. package/src/overlays/advisor-config.ts +259 -8
  154. package/src/overlays/agent-hub-projection.d.ts +10 -5
  155. package/src/overlays/agent-hub-projection.ts +63 -12
  156. package/src/overlays/annotation-overlay.ts +422 -168
  157. package/src/overlays/approval-dialog.ts +389 -0
  158. package/src/overlays/ask-dialog.ts +244 -105
  159. package/src/overlays/btw-history-panel.ts +208 -147
  160. package/src/overlays/extensions/extension-list.ts +4 -6
  161. package/src/overlays/hook-editor.ts +90 -32
  162. package/src/overlays/hook-input.ts +105 -19
  163. package/src/overlays/job-tree.ts +177 -0
  164. package/src/overlays/jobs-panel.ts +222 -142
  165. package/src/overlays/login-dialog.ts +201 -91
  166. package/src/overlays/mcp-add-wizard.ts +562 -279
  167. package/src/overlays/model-browser.ts +32 -7
  168. package/src/overlays/model-hub.ts +4 -10
  169. package/src/overlays/model-picker.ts +58 -21
  170. package/src/overlays/pause-screen.ts +81 -23
  171. package/src/overlays/plan-review-overlay.ts +16 -0
  172. package/src/overlays/plugin-selector.ts +17 -1
  173. package/src/overlays/queue-mode-selector.ts +10 -0
  174. package/src/overlays/report-panel.ts +6 -2
  175. package/src/overlays/reset-usage-selector.ts +130 -21
  176. package/src/overlays/session-account-selector.ts +36 -0
  177. package/src/overlays/session-info-overlay.ts +199 -76
  178. package/src/overlays/session-selector.ts +15 -10
  179. package/src/overlays/settings-selector.ts +3 -0
  180. package/src/overlays/show-images-selector.ts +1 -0
  181. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  182. package/src/overlays/theme-selector.ts +6 -1
  183. package/src/overlays/thinking-selector.ts +8 -4
  184. package/src/overlays/tree-selector.ts +117 -2
  185. package/src/overlays/usage-dashboard.ts +498 -144
  186. package/src/prompt/composer-cache.ts +1 -1
  187. package/src/prompt/composer.ts +194 -74
  188. package/src/prompt/custom-editor.ts +168 -65
  189. package/src/prompt/image-references.d.ts +22 -0
  190. package/src/prompt/image-references.ts +79 -2
  191. package/src/prompt/internal-url-autocomplete.ts +1 -0
  192. package/src/prompt/macos-spelling.ts +28 -5
  193. package/src/prompt/prose-gate.ts +3 -0
  194. package/src/prompt/queued-messages.ts +9 -2
  195. package/src/prompt/tips.txt +1 -2
  196. package/src/prompt/typing-word.ts +59 -0
  197. package/src/prompt/welcome.ts +97 -45
  198. package/src/render/code-cell.ts +18 -5
  199. package/src/render/hanging-wrap.d.ts +7 -0
  200. package/src/render/hanging-wrap.ts +58 -0
  201. package/src/render/hyperlink.d.ts +2 -0
  202. package/src/render/hyperlink.ts +5 -0
  203. package/src/render/render-utils.d.ts +9 -1
  204. package/src/render/render-utils.ts +16 -2
  205. package/src/render/utils.d.ts +1 -1
  206. package/src/render/utils.ts +1 -1
  207. package/src/render/width-aware-text.ts +6 -5
  208. package/src/setup/scenes/splash.ts +22 -8
  209. package/src/status-line/component.ts +23 -10
  210. package/src/status-line/context-usage.ts +176 -73
  211. package/src/status-line/footer.ts +18 -35
  212. package/src/status-line/host.ts +6 -1
  213. package/src/status-line/segments.ts +16 -5
  214. package/src/status-line/startup.ts +6 -6
  215. package/src/status-line/types.ts +1 -0
  216. package/src/stdin-buffer.d.ts +7 -0
  217. package/src/stdin-buffer.ts +11 -1
  218. package/src/terminal-capabilities.d.ts +16 -1
  219. package/src/terminal-capabilities.ts +66 -28
  220. package/src/terminal-multiplexer.d.ts +7 -0
  221. package/src/terminal-multiplexer.ts +14 -0
  222. package/src/terminal.d.ts +8 -4
  223. package/src/terminal.ts +160 -46
  224. package/src/theme/symbols.d.ts +1 -1
  225. package/src/theme/symbols.ts +1 -0
  226. package/src/theme/theme-class.ts +1 -0
  227. package/src/theme/theme.d.ts +11 -0
  228. package/src/theme/theme.ts +46 -14
  229. package/src/thinking.d.ts +5 -0
  230. package/src/thinking.ts +9 -1
  231. package/src/tmux.d.ts +39 -4
  232. package/src/tmux.ts +110 -16
  233. package/src/tools/ask.ts +20 -9
  234. package/src/tools/ast-edit.d.ts +2 -2
  235. package/src/tools/ast-edit.ts +30 -7
  236. package/src/tools/ast-grep.d.ts +2 -2
  237. package/src/tools/ast-grep.ts +39 -35
  238. package/src/tools/bash.d.ts +6 -1
  239. package/src/tools/bash.ts +15 -9
  240. package/src/tools/debug.ts +2 -1
  241. package/src/tools/default-renderer.ts +3 -2
  242. package/src/tools/edit.d.ts +2 -0
  243. package/src/tools/edit.ts +100 -21
  244. package/src/tools/eval.d.ts +53 -4
  245. package/src/tools/eval.ts +173 -137
  246. package/src/tools/find.d.ts +14 -4
  247. package/src/tools/find.ts +71 -18
  248. package/src/tools/glob.d.ts +3 -3
  249. package/src/tools/glob.ts +51 -17
  250. package/src/tools/grep.d.ts +2 -2
  251. package/src/tools/grep.ts +99 -22
  252. package/src/tools/grouped-file-output.d.ts +9 -4
  253. package/src/tools/grouped-file-output.ts +21 -6
  254. package/src/tools/lsp.ts +30 -18
  255. package/src/tools/mcp.ts +2 -2
  256. package/src/tools/native-view.d.ts +1 -1
  257. package/src/tools/native-view.ts +2 -2
  258. package/src/tools/read.d.ts +32 -10
  259. package/src/tools/read.ts +89 -43
  260. package/src/tools/renderer.d.ts +2 -0
  261. package/src/tools/renderer.ts +2 -0
  262. package/src/tools/task.d.ts +31 -0
  263. package/src/tools/task.ts +40 -7
  264. package/src/tools/todo.ts +3 -2
  265. package/src/tools/wait.ts +16 -30
  266. package/src/tools/web-search.ts +2 -1
  267. package/src/tools/write.d.ts +2 -0
  268. package/src/tools/write.ts +36 -12
  269. package/src/tools/yield.ts +2 -2
  270. package/src/tui.d.ts +20 -3
  271. package/src/tui.ts +140 -36
  272. package/src/utils.ts +5 -3
  273. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  274. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -8,7 +8,7 @@ import { Text } from "../components/text";
8
8
  import { getImageDimensions, ImageProtocol, imageFallback, TERMINAL } from "../terminal-capabilities";
9
9
  import { type Component, Container, type TUI } from "../tui";
10
10
  import { truncateToWidth } from "../utils";
11
- import { getProjectDir, isRecord, logger, sanitizeText } from "@linxiraos/pi-utils";
11
+ import { getProjectDir, isRecord, logger, sanitizeText, sanitizeTextDeep } from "@linxiraos/pi-utils";
12
12
  import type { Theme } from "../theme/theme";
13
13
  import { ensureThemeSync, getThemeEpoch, theme } from "../theme/theme";
14
14
  import {
@@ -32,7 +32,8 @@ import {
32
32
  import { plainText } from "../native/spans";
33
33
  import { diagnosticsSection, displayPath, toolHead } from "../tools/native-view";
34
34
  import type { FileDiagnosticsResult } from "../tools/lsp";
35
- import { NativeImageCache } from "../native/blobs";
35
+ import { NativeImageCache, readMediaPath, shownMedia } from "../native/blobs";
36
+ import { imageLinksEnabled, linkedImageText } from "../prompt/image-references";
36
37
  import { Memo } from "../native/memo";
37
38
  import { type EditMode, type PerFileDiffPreview, renderStreamingFallback } from "../tools/edit";
38
39
  import { taskCardAgentIds } from "../tools/task";
@@ -218,18 +219,17 @@ export interface ToolExecutionOptions {
218
219
  useBuiltInRenderer?: boolean;
219
220
  }
220
221
 
222
+ /** A tool result or progress frame as a card shows it. */
223
+ export interface ToolResultFrame {
224
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
225
+ details?: unknown;
226
+ isError?: boolean;
227
+ }
228
+
221
229
  export interface ToolExecutionHandle extends Component {
222
230
  updateArgs(args: unknown, toolCallId?: string): void;
223
231
  updateStreamPreview?(update: unknown): void;
224
- updateResult(
225
- result: {
226
- content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
227
- details?: unknown;
228
- isError?: boolean;
229
- },
230
- isPartial?: boolean,
231
- toolCallId?: string,
232
- ): void;
232
+ updateResult(result: ToolResultFrame, isPartial?: boolean, toolCallId?: string): void;
233
233
  setArgsComplete(toolCallId?: string): void;
234
234
  setExecutionStarted(toolCallId?: string): void;
235
235
  setExpanded(expanded: boolean): void;
@@ -240,6 +240,50 @@ export interface ToolExecutionHandle extends Component {
240
240
  seal(): void;
241
241
  }
242
242
 
243
+ /** Tools whose returned call hands its card to the background job it started (see {@link parksAsBackground}). */
244
+ const JOB_CARD_TOOLS: Record<string, "always" | "native"> = { task: "always", bash: "native", eval: "native" };
245
+
246
+ /**
247
+ * Whether a call that returned with `details.async.state === "running"` stays
248
+ * parked ({@link ToolExecutionHandle.parkAsBackground}) so its job's later
249
+ * frames keep filling the card until a `completed`/`failed` frame settles it.
250
+ * Detached `task` spawns always do; backgrounded `bash`/`eval` jobs only under
251
+ * native rendering, where the terminal transcript keeps them frozen instead.
252
+ * Shared by the live event controller and the transcript rebuild.
253
+ */
254
+ export function parksAsBackground(toolName: string, details: unknown): boolean {
255
+ const mode = JOB_CARD_TOOLS[toolName];
256
+ if (mode === undefined || (mode === "native" && !isNativeRendering())) return false;
257
+ return asyncJobState(details) === "running";
258
+ }
259
+
260
+ /** The background job state a tool result reports (`details.async.state`: running, completed, failed, cancelled). */
261
+ export function asyncJobState(details: unknown): string | undefined {
262
+ if (!isRecord(details) || !isRecord(details.async)) return undefined;
263
+ return typeof details.async.state === "string" ? details.async.state : undefined;
264
+ }
265
+
266
+ /** Whether a frame is a background job's last one: it completed, failed or was cancelled. */
267
+ export function isSettledJobFrame(details: unknown): boolean {
268
+ const state = asyncJobState(details);
269
+ return state === "completed" || state === "failed" || state === "cancelled";
270
+ }
271
+
272
+ /**
273
+ * Settle a parked card ({@link parksAsBackground}) with its job's last frame
274
+ * ({@link isSettledJobFrame}): a completed or failed job is the call's result;
275
+ * a cancelled one keeps the output it got to and seals the card as cancelled.
276
+ */
277
+ export function settleWithJobFrame(component: ToolExecutionHandle, frame: ToolResultFrame, toolCallId: string): void {
278
+ const state = asyncJobState(frame.details);
279
+ if (state === "cancelled") {
280
+ component.updateResult(frame, true, toolCallId);
281
+ component.seal();
282
+ return;
283
+ }
284
+ component.updateResult({ ...frame, isError: state === "failed" }, false, toolCallId);
285
+ }
286
+
243
287
  /** Card tone per status when the renderer doesn't choose one; settled cards stay neutral. */
244
288
  const NATIVE_STATUS_TONE: Record<TspCardStatus, TspTone | undefined> = {
245
289
  pending: "pending",
@@ -376,6 +420,8 @@ export class ToolExecutionComponent extends Container {
376
420
  isError?: boolean;
377
421
  details?: unknown;
378
422
  };
423
+ // Machine paths remain verbatim; only #result.details is handed to display renderers.
424
+ #rawDetails: unknown;
379
425
  // Edit preview state
380
426
  #editMode?: EditMode;
381
427
  #editDiffPreview?: PerFileDiffPreview[];
@@ -581,22 +627,18 @@ export class ToolExecutionComponent extends Container {
581
627
  return result !== undefined && !this.#isPartial && (result.isError === true || result.details != null);
582
628
  }
583
629
 
584
- updateResult(
585
- result: {
586
- content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
587
- details?: unknown;
588
- isError?: boolean;
589
- },
590
- isPartial = false,
591
- _toolCallId?: string,
592
- ): void {
630
+ updateResult(result: ToolResultFrame, isPartial = false, _toolCallId?: string): void {
593
631
  const hadNoResult = this.#result === undefined;
594
632
  const wasPartialResult = this.#result !== undefined && this.#isPartial;
595
633
  const firstResultRepaintShapePainted = this.#firstResultViewportRepaintShapePainted;
596
634
  const partialResultPainted = this.#partialResultShapePainted;
597
635
  this.#firstResultViewportRepaintShapePainted = false;
598
636
  this.#partialResultShapePainted = false;
599
- this.#result = result;
637
+ this.#rawDetails = result.details;
638
+ // Renderers receive sanitized details for display; raw paths are available
639
+ // separately for links and media through the built-in render context.
640
+ const details = sanitizeTextDeep(result.details);
641
+ this.#result = details === result.details ? result : { ...result, details };
600
642
  this.#resultVersion++;
601
643
  this.#blockVersion++;
602
644
  this.#isPartial = isPartial;
@@ -941,6 +983,7 @@ export class ToolExecutionComponent extends Container {
941
983
  override describe(cx?: DescribeContext): NativeNode {
942
984
  if (this.#toolName === "wait" && this.#isBenignSkip()) return EMPTY_NODE;
943
985
  const dataFirst = cx?.supports("tool") === true;
986
+ const media = cx?.supports("media") === true;
944
987
  const key = [
945
988
  dataFirst,
946
989
  this.#resultVersion,
@@ -953,10 +996,11 @@ export class ToolExecutionComponent extends Container {
953
996
  this.#showImages,
954
997
  this.#displayInputVersion,
955
998
  this.#toolActivityVisible,
999
+ media,
956
1000
  getThemeEpoch(),
957
1001
  ];
958
1002
  return this.#native.get(key, () =>
959
- withHidden(dataFirst ? this.#describeTool() : this.#describeCard(), !this.#toolActivityVisible),
1003
+ withHidden(dataFirst ? this.#describeTool(media) : this.#describeCard(media), !this.#toolActivityVisible),
960
1004
  );
961
1005
  }
962
1006
 
@@ -1002,12 +1046,12 @@ export class ToolExecutionComponent extends Container {
1002
1046
  * The data-first `tool` node (§7.2): the renderer's head data, status,
1003
1047
  * timer, exit and intent as props; its body as borderless children.
1004
1048
  */
1005
- #describeTool(): NativeNode {
1006
- const view = this.#nativeView();
1049
+ #describeTool(media: boolean): NativeNode {
1050
+ const view = this.#nativeView(media);
1007
1051
  const status = this.#nativeStatus();
1008
1052
  const head = view.tool;
1009
1053
  const late = this.#lateDiagnosticsParts();
1010
- const body: NativeChild[] = [...(view.body ?? []), ...this.#nativeResultImages()];
1054
+ const body: NativeChild[] = [...(view.body ?? []), ...this.#nativeResultImages(view.body)];
1011
1055
  if (late.section) body.push(late.section);
1012
1056
  const inline = view.inline === true;
1013
1057
  const hasBody = body.length > 0;
@@ -1072,8 +1116,8 @@ export class ToolExecutionComponent extends Container {
1072
1116
  return spans;
1073
1117
  }
1074
1118
 
1075
- #describeCard(): NativeNode {
1076
- const view = this.#nativeView();
1119
+ #describeCard(media: boolean): NativeNode {
1120
+ const view = this.#nativeView(media);
1077
1121
  const status = this.#nativeStatus();
1078
1122
  const late = this.#lateDiagnosticsParts();
1079
1123
  const fallbackHead = this.#fallbackHead(view);
@@ -1095,7 +1139,7 @@ export class ToolExecutionComponent extends Container {
1095
1139
  const children: NativeChild[] = [
1096
1140
  node("row", { gap: "sm", align: "baseline" }, headChildren, "head"),
1097
1141
  ...(view.body ?? []),
1098
- ...this.#nativeResultImages(),
1142
+ ...this.#nativeResultImages(view.body),
1099
1143
  ...(late.section ? [late.section] : []),
1100
1144
  ];
1101
1145
  const role = `zeta.tool.${this.#toolName}`;
@@ -1127,9 +1171,10 @@ export class ToolExecutionComponent extends Container {
1127
1171
  * the result view (merged renderers) or the call head over both bodies.
1128
1172
  * A renderer that throws falls back to the generic card.
1129
1173
  */
1130
- #nativeView(): NativeToolView {
1174
+ #nativeView(media: boolean): NativeToolView {
1131
1175
  const status = this.#nativeStatus();
1132
1176
  const options: RenderResultContextOptions = {
1177
+ media,
1133
1178
  expanded: this.#expanded,
1134
1179
  isPartial: this.#isPartial,
1135
1180
  argsComplete: this.#argsComplete,
@@ -1208,12 +1253,17 @@ export class ToolExecutionComponent extends Container {
1208
1253
  return indicators ? (output ? `${output}\n${indicators}` : indicators) : output;
1209
1254
  }
1210
1255
 
1211
- #nativeResultImages(): NativeNode[] {
1256
+ /** Result images under the renderer's body, minus those the body already shows (as an `image` blob or a `media` file). */
1257
+ #nativeResultImages(body: readonly NativeChild[] | undefined): NativeNode[] {
1212
1258
  if (!this.#result || !this.#showImages) return [];
1259
+ const shown = shownMedia(body);
1260
+ const mediaPath = readMediaPath(this.#rawDetails);
1261
+ if (mediaPath && shown.paths.has(mediaPath)) return [];
1213
1262
  const images: NativeNode[] = [];
1214
1263
  this.#getAllImageBlocks().forEach((image, index) => {
1215
- if (image.data && image.mimeType)
1216
- images.push(this.#nativeImages.get(`img${index}`, image.data, image.mimeType));
1264
+ if (!image.data || !image.mimeType) return;
1265
+ const described = this.#nativeImages.get(`img${index}`, image.data, image.mimeType, mediaPath);
1266
+ if (!shown.blobs.has((described.p as { blob?: string } | undefined)?.blob ?? "")) images.push(described);
1217
1267
  });
1218
1268
  return images;
1219
1269
  }
@@ -1244,7 +1294,7 @@ export class ToolExecutionComponent extends Container {
1244
1294
  // TUI startup, so a result rendered before it lands must re-shape once it
1245
1295
  // does (it gates Image children vs text fallback in #rebuildDisplay); keyed
1246
1296
  // here for the same reason markdown.ts keys its render cache on it.
1247
- const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#argsComplete ? "1" : "0"}|${this.#executionStarted ? "1" : "0"}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}`;
1297
+ const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#argsComplete ? "1" : "0"}|${this.#executionStarted ? "1" : "0"}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}|${imageLinksEnabled()}`;
1248
1298
  if (key === this.#lastDisplayKey && this.#displayBuilt) return;
1249
1299
  this.#lastDisplayKey = key;
1250
1300
 
@@ -1514,6 +1564,10 @@ export class ToolExecutionComponent extends Container {
1514
1564
  this.#contentBox.clear();
1515
1565
 
1516
1566
  const renderContext = this.#buildRenderContext();
1567
+ const rawPerFileResults =
1568
+ isRecord(this.#rawDetails) && Array.isArray(this.#rawDetails.perFileResults)
1569
+ ? this.#rawDetails.perFileResults
1570
+ : undefined;
1517
1571
  this.#renderState.renderContext = renderContext;
1518
1572
 
1519
1573
  for (let i = 0; i < perFileResults.length; i++) {
@@ -1525,9 +1579,13 @@ export class ToolExecutionComponent extends Container {
1525
1579
  }
1526
1580
  const fileBox = new Box(0, 0);
1527
1581
  try {
1582
+ const fileOptions = {
1583
+ ...this.#renderState,
1584
+ renderContext: { ...renderContext, rawDetails: rawPerFileResults?.[i] },
1585
+ };
1528
1586
  const resultComponent = renderer.renderResult(
1529
1587
  { content: [], details: fileResult, isError: fileResult.isError },
1530
- this.#renderState,
1588
+ fileOptions,
1531
1589
  theme,
1532
1590
  );
1533
1591
  if (resultComponent) {
@@ -1651,6 +1709,7 @@ export class ToolExecutionComponent extends Container {
1651
1709
 
1652
1710
  if (this.#result) {
1653
1711
  const imageBlocks = this.#getAllImageBlocks();
1712
+ const sourcePath = readMediaPath(this.#rawDetails);
1654
1713
 
1655
1714
  for (let i = 0; i < imageBlocks.length; i++) {
1656
1715
  const img = imageBlocks[i];
@@ -1678,6 +1737,7 @@ export class ToolExecutionComponent extends Container {
1678
1737
  { fallbackColor: (s: string) => theme.fg("toolOutput", s) },
1679
1738
  {
1680
1739
  ...resolveImageOptions(),
1740
+ sourcePath,
1681
1741
  budget: this.#ui.imageBudget,
1682
1742
  imageKey: `te${this.#instanceId}:${i}`,
1683
1743
  requestRender: () => this.#ui.requestRender(),
@@ -1756,6 +1816,9 @@ export class ToolExecutionComponent extends Container {
1756
1816
  return Math.max(1, Math.min(maxSeconds, value));
1757
1817
  };
1758
1818
 
1819
+ if (this.#renderer && !this.#tool?.renderResult && this.#rawDetails !== undefined) {
1820
+ context.rawDetails = this.#rawDetails;
1821
+ }
1759
1822
  if (this.#toolName === "bash") {
1760
1823
  // Bash needs render context even before a result exists. The renderer uses the pending-call args
1761
1824
  // plus this context to keep the inline command preview visible while tool-call JSON is still streaming.
@@ -1816,7 +1879,8 @@ export class ToolExecutionComponent extends Container {
1816
1879
 
1817
1880
  #getTextOutput(): string {
1818
1881
  if (!this.#result) return "";
1819
- const key = `${this.#resultVersion}|${this.#showImages}|${TERMINAL.imageProtocol ?? "-"}`;
1882
+ // Image placeholders embed a link only under an enabled hyperlink policy.
1883
+ const key = `${this.#resultVersion}|${this.#showImages}|${TERMINAL.imageProtocol ?? "-"}|${imageLinksEnabled()}`;
1820
1884
  if (key === this.#textOutputKey) return this.#textOutput;
1821
1885
 
1822
1886
  const textBlocks = this.#result.content.filter(c => c.type === "text");
@@ -1833,7 +1897,10 @@ export class ToolExecutionComponent extends Container {
1833
1897
  .map(img => {
1834
1898
  const mimeType = String(img.mimeType);
1835
1899
  const dims = img.data ? (getImageDimensions(img.data, mimeType) ?? undefined) : undefined;
1836
- return imageFallback(mimeType, dims);
1900
+ const placeholder = imageFallback(mimeType, dims);
1901
+ return img.data
1902
+ ? linkedImageText(placeholder, { data: img.data, mimeType }, readMediaPath(this.#rawDetails))
1903
+ : placeholder;
1837
1904
  })
1838
1905
  .join("\n");
1839
1906
  output = output ? `${output}\n${imageIndicators}` : imageIndicators;
@@ -39,8 +39,9 @@ export declare class UserMessageComponent extends Container implements ReactionT
39
39
  /**
40
40
  * A head-less user-toned frame with the prompt as markdown under its
41
41
  * attached images (titled `#N` like their chips, opening the materialized
42
- * file on click): the fill says "you". A quiet toolbar (time, Copy, Rewind) fades in on hover when the
43
- * host handles transcript actions. The live-steering marker and the
42
+ * file on click): the fill says "you". A quiet footer like a turn's usage line
43
+ * (`⧉ ⏪ · 09:45`: Copy, Rewind, the time sent) fades in at its bottom-left on hover when
44
+ * the host handles transcript actions. The live-steering marker and the
44
45
  * agent's reaction are chips at the bottom-right, so a reaction landing
45
46
  * later updates the frame in place, even deep in scrollback.
46
47
  */
@@ -18,12 +18,12 @@ import {
18
18
  skillChipStyle,
19
19
  } from "../prompt/composer-attachments";
20
20
  import { MODEL_MENTION_TAG_RE } from "../prompt/model-mention-syntax";
21
- import { expandKeyHint, fileHyperlink } from "../render";
22
- import { imageReferenceHyperlink } from "../prompt/image-references";
21
+ import { expandKeyHint, fileHyperlink, fileLinkSpan } from "../render";
22
+ import { imageLinksEnabled, imageReferenceHyperlink } from "../prompt/image-references";
23
23
  import { highlightMagicKeywords } from "../prompt/magic-keywords";
24
24
  import type { ReactionTarget } from "./reaction";
25
25
  import { card, md, node, row, span, text } from "../native/describe";
26
- import { base64ImageNode } from "../native/blobs";
26
+ import { base64ImageNode, openableImageProps } from "../native/blobs";
27
27
  import { hasTranscriptActions, runTranscriptAction } from "./transcript-actions";
28
28
  import {
29
29
  type DescribeContext,
@@ -145,8 +145,9 @@ export class UserMessageComponent extends Container implements ReactionTarget, T
145
145
  readonly #tokens: RegExp;
146
146
  #reaction: string | undefined;
147
147
  #native: NativeNode | undefined;
148
- /** The terminal's clock {@link #native} was described with. */
148
+ /** The terminal's clock and the image link policy {@link #native} was described with. */
149
149
  #nativeHour12: boolean | undefined;
150
+ #nativeLinked = false;
150
151
 
151
152
  constructor(text: string, options: UserBubbleOptions = {}) {
152
153
  super();
@@ -195,44 +196,60 @@ export class UserMessageComponent extends Container implements ReactionTarget, T
195
196
  /**
196
197
  * A head-less user-toned frame with the prompt as markdown under its
197
198
  * attached images (titled `#N` like their chips, opening the materialized
198
- * file on click): the fill says "you". A quiet toolbar (time, Copy, Rewind) fades in on hover when the
199
- * host handles transcript actions. The live-steering marker and the
199
+ * file on click): the fill says "you". A quiet footer like a turn's usage line
200
+ * (`⧉ ⏪ · 09:45`: Copy, Rewind, the time sent) fades in at its bottom-left on hover when
201
+ * the host handles transcript actions. The live-steering marker and the
200
202
  * agent's reaction are chips at the bottom-right, so a reaction landing
201
203
  * later updates the frame in place, even deep in scrollback.
202
204
  */
203
205
  override describe(cx?: DescribeContext): NativeNode {
204
206
  // The terminal's clock: Bun's own default locale ignores the user's.
205
207
  const hour12 = cx?.hour12;
206
- if (this.#native && this.#nativeHour12 === hour12) return this.#native;
208
+ // Chip marks and thumbnails carry links only under an enabled policy.
209
+ const linked = imageLinksEnabled();
210
+ if (this.#native && this.#nativeHour12 === hour12 && this.#nativeLinked === linked) return this.#native;
207
211
  this.#nativeHour12 = hour12;
212
+ this.#nativeLinked = linked;
208
213
  const children: NativeChild[] = [];
209
214
  if (!this.#synthetic && hasTranscriptActions()) {
210
- const tools: NativeChild[] = [];
215
+ const tools: NativeChild[] = [
216
+ // `copy-message`, not Tern's local `copy` (that would copy the icon's empty label).
217
+ node(
218
+ "icon",
219
+ {
220
+ name: "copy",
221
+ role: "zeta.user.tool",
222
+ title: "Copy message",
223
+ aria: "Copy message",
224
+ actions: { click: "copy-message" },
225
+ },
226
+ undefined,
227
+ "copy",
228
+ ),
229
+ node(
230
+ "icon",
231
+ {
232
+ name: "rewind",
233
+ role: "zeta.user.tool",
234
+ title: "Rewind the conversation to an earlier message",
235
+ aria: "Rewind",
236
+ actions: { click: "rewind" },
237
+ },
238
+ undefined,
239
+ "rewind",
240
+ ),
241
+ ];
211
242
  if (this.#timestamp !== undefined) {
212
243
  const at = new Date(this.#timestamp);
213
244
  tools.push(
245
+ node("text", { text: "·" }, undefined, "dot"),
214
246
  text([span(at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12 }), "dim mono")], {
215
247
  role: "zeta.user.time",
216
248
  title: at.toLocaleString([], { hour12 }),
217
249
  }),
218
250
  );
219
251
  }
220
- tools.push(
221
- // `copy-message`, not Tern's local `copy` (that would copy the label).
222
- text("Copy", {
223
- role: "zeta.user.tool",
224
- actions: { click: "copy-message" },
225
- title: "Copy message",
226
- key: "copy",
227
- }),
228
- text("Rewind", {
229
- role: "zeta.user.tool",
230
- actions: { click: "rewind" },
231
- title: "Rewind the conversation to an earlier message",
232
- key: "rewind",
233
- }),
234
- );
235
- children.push(node("row", { gap: "xs", role: "zeta.user.tools" }, tools, "tools"));
252
+ children.push(node("row", { gap: "xs", align: "center", role: "zeta.user.tools" }, tools, "tools"));
236
253
  }
237
254
  // Videos keep their chip only: the native image node decodes stills.
238
255
  const thumbs: NativeNode[] = [];
@@ -240,16 +257,14 @@ export class UserMessageComponent extends Container implements ReactionTarget, T
240
257
  this.#images.forEach((image, i) => {
241
258
  if (!image.mimeType.startsWith("image/")) return;
242
259
  const label = `#${i + 1}`;
243
- const link = this.#imageLinks?.[i];
244
- // A click zooms the image in the terminal; the file opens from its context menu.
245
- const open = link ? { href: link, actions: { menu: ["open"] } } : {};
260
+ const open = openableImageProps(linked ? this.#imageLinks?.[i] : undefined);
246
261
  thumbs.push(base64ImageNode(image.data, image.mimeType, { alt: label, title: label, ...open }, label));
247
262
  });
248
263
  }
249
264
  if (thumbs.length > 0) {
250
265
  children.push(row(thumbs, { gap: "sm", wrap: true, align: "start", role: "zeta.user.images" }));
251
266
  }
252
- const marks = this.#synthetic ? [] : tokenMarks(this.#text, this.#tokens);
267
+ const marks = this.#synthetic ? [] : tokenMarks(this.#text, this.#tokens, this.#imageLinks);
253
268
  children.push(md(this.#text, marks.length > 0 ? { marks } : undefined));
254
269
  const badges: NativeChild[] = [];
255
270
  if (this.#liveSteered) {
@@ -346,13 +361,13 @@ export class UserMessageComponent extends Container implements ReactionTarget, T
346
361
  /**
347
362
  * The composer tokens in `text` as `md` marks, styled as the composer
348
363
  * decorates them (`CustomEditor.describeDecorations`), so a sent prompt keeps
349
- * its chips highlighted.
364
+ * its chips highlighted. Image and video chips link to their file, as in text mode.
350
365
  */
351
- function tokenMarks(text: string, tokens: RegExp): TspSpan[] {
366
+ function tokenMarks(text: string, tokens: RegExp, imageLinks: readonly (string | undefined)[] | undefined): TspSpan[] {
352
367
  // One mark per distinct token: Tern styles every occurrence of its text.
353
- const styles = new Map<string, string>();
368
+ const marks = new Map<string, TspSpan>();
354
369
  const mark = (s: string) => (label: string) => {
355
- styles.set(label, s);
370
+ marks.set(label, { t: label, s });
356
371
  return "";
357
372
  };
358
373
  renderPlaceholders(
@@ -361,11 +376,16 @@ function tokenMarks(text: string, tokens: RegExp): TspSpan[] {
361
376
  renderText: () => "",
362
377
  renderSkill: mark("customMessageLabel strong"),
363
378
  renderMention: mark("statusLineModel strong"),
364
- renderReference: mark("accent strong"),
379
+ renderReference: (label, kind, index) => {
380
+ const s = "accent strong";
381
+ const link = kind === "image" || kind === "video" ? imageLinks?.[index - 1] : undefined;
382
+ marks.set(label, link ? fileLinkSpan(link, label, s) : { t: label, s });
383
+ return "";
384
+ },
365
385
  },
366
386
  tokens,
367
387
  );
368
- return [...styles].map(([t, s]) => ({ t, s }));
388
+ return [...marks.values()];
369
389
  }
370
390
 
371
391
  /**
@@ -49,15 +49,15 @@ export class CountdownTimer {
49
49
 
50
50
  /**
51
51
  * The remaining time as a terminal-clocked `elapsed` node: its age is the
52
- * negative remaining time (the start lies in the future), so it counts up
53
- * toward zero and freezes there at expiry. The age is taken on the first
52
+ * negative remaining time (the start lies in the future), so it counts down
53
+ * to zero and holds there at expiry (no `stopped`: that would freeze it at
54
+ * zero from the start). The age is taken on the first
54
55
  * call after a (re)start, when the node is first written, and the node is
55
56
  * reused until {@link reset}.
56
57
  */
57
58
  describe(): NativeNode {
58
59
  this.#node ??= node("elapsed", {
59
60
  age: Math.min(0, Date.now() - this.#deadlineMs),
60
- stopped: 0,
61
61
  format: "short",
62
62
  });
63
63
  return this.#node;
@@ -7,10 +7,9 @@ import type { NativeNode } from "../native/node";
7
7
  * becomes one hunk; the terminal draws gutters, word emphasis and highlighting.
8
8
  * Removed rows carry old line numbers, added rows new ones and context rows
9
9
  * old ones, so the other side is derived from the running line delta.
10
+ * Tern picks the grammar from `path`.
10
11
  */
11
- export declare function nativeDiff(diffText: string, options?: {
12
- filePath?: string;
13
- } & TspProps<"diff">): NativeNode;
12
+ export declare function nativeDiff(diffText: string, props?: TspProps<"diff">): NativeNode;
14
13
  export interface RenderDiffOptions {
15
14
  /** File path used to resolve indentation (.editorconfig + defaults) */
16
15
  filePath?: string;
@@ -2,7 +2,7 @@ import { diffWords } from "@linxiraos/pi-natives";
2
2
  import { DEFAULT_TAB_WIDTH, sanitizeText } from "@linxiraos/pi-utils";
3
3
  import type { TspDiffHunk, TspProps } from "@linxiraos/pi-wire";
4
4
  import { theme as activeTheme, getLanguageFromPath, highlightCode, type Theme } from "../theme/index";
5
- import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs, shortenPath } from "../render/render-utils";
5
+ import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../render/render-utils";
6
6
  import { node } from "../native/describe";
7
7
  import type { NativeNode } from "../native/node";
8
8
  /** SGR dim on / normal intensity — additive, preserves fg/bg colors. */
@@ -106,9 +106,9 @@ function renderIntraLineDiff(
106
106
  * becomes one hunk; the terminal draws gutters, word emphasis and highlighting.
107
107
  * Removed rows carry old line numbers, added rows new ones and context rows
108
108
  * old ones, so the other side is derived from the running line delta.
109
+ * Tern picks the grammar from `path`.
109
110
  */
110
- export function nativeDiff(diffText: string, options: { filePath?: string } & TspProps<"diff"> = {}): NativeNode {
111
- const { filePath, ...props } = options;
111
+ export function nativeDiff(diffText: string, props: TspProps<"diff"> = {}): NativeNode {
112
112
  const hunks: TspDiffHunk[] = [];
113
113
  let lines: string[] = [];
114
114
  let oldStart = 0;
@@ -148,12 +148,7 @@ export function nativeDiff(diffText: string, options: { filePath?: string } & Ts
148
148
  lines.push(`${parsed.prefix}${parsed.content}`);
149
149
  }
150
150
  close();
151
- return node("diff", {
152
- ...props,
153
- hunks,
154
- path: props.path ?? (filePath ? shortenPath(filePath) : undefined),
155
- lang: props.lang ?? (filePath ? getLanguageFromPath(filePath) : undefined),
156
- });
151
+ return node("diff", { ...props, hunks });
157
152
  }
158
153
 
159
154
  export interface RenderDiffOptions {
@@ -100,6 +100,8 @@ export declare class TranscriptContainer extends Container {
100
100
  blockModes(): readonly TranscriptBlockMode[];
101
101
  /** Emitted stable semantic-row counts in transcript order. */
102
102
  emittedStableRows(): readonly number[];
103
+ /** Whether any block is still streaming or otherwise unfinalized. */
104
+ hasActiveBlock(): boolean;
103
105
  /** Whether visible active capacity and live-block memory permit another admission. */
104
106
  canAdmit(rows: number): boolean;
105
107
  /** Prepares one atomic replay of the committed ledger and an emitted active-head prefix. */