@oh-my-pi/pi-tui 18.13.2 → 18.14.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 (245) hide show
  1. package/CHANGELOG.md +40 -54
  2. package/README.md +34 -34
  3. package/dist/types/app-keybindings.d.ts +15 -0
  4. package/dist/types/apps/session-picker.d.ts +5 -0
  5. package/dist/types/charts/chart-plan.d.ts +9 -7
  6. package/dist/types/chat/assistant-message.d.ts +15 -3
  7. package/dist/types/chat/bash-execution.d.ts +2 -0
  8. package/dist/types/chat/display-preferences.d.ts +2 -0
  9. package/dist/types/chat/extension-types.d.ts +31 -0
  10. package/dist/types/chat/reply-write.d.ts +48 -0
  11. package/dist/types/chat/table-chart.d.ts +4 -3
  12. package/dist/types/chat/tool-execution.d.ts +1 -2
  13. package/dist/types/chat/transcript-outline.d.ts +7 -0
  14. package/dist/types/chat/transcript-render-helpers.d.ts +2 -0
  15. package/dist/types/chrome/index.d.ts +1 -0
  16. package/dist/types/chrome/transcript-notice.d.ts +35 -0
  17. package/dist/types/chrome/transcript-status.d.ts +20 -8
  18. package/dist/types/components/editor.d.ts +8 -0
  19. package/dist/types/components/markdown.d.ts +7 -0
  20. package/dist/types/native/blobs.d.ts +2 -1
  21. package/dist/types/native/picker.d.ts +3 -3
  22. package/dist/types/overlays/annotation-overlay.d.ts +7 -1
  23. package/dist/types/overlays/annotation-types.d.ts +28 -0
  24. package/dist/types/overlays/login-dialog.d.ts +5 -1
  25. package/dist/types/overlays/model-browser.d.ts +10 -1
  26. package/dist/types/overlays/oauth-selector.d.ts +23 -0
  27. package/dist/types/overlays/plan-review-overlay.d.ts +2 -0
  28. package/dist/types/overlays/rewind-selector.d.ts +1 -1
  29. package/dist/types/overlays/session-selector.d.ts +22 -2
  30. package/dist/types/prompt/composer.d.ts +41 -4
  31. package/dist/types/prompt/interactive-context-helpers.d.ts +10 -3
  32. package/dist/types/prompt/welcome.d.ts +6 -2
  33. package/dist/types/render/extension-links.d.ts +65 -0
  34. package/dist/types/render/hyperlink.d.ts +25 -7
  35. package/dist/types/render/index.d.ts +1 -0
  36. package/dist/types/setup/scenes/model.d.ts +1 -1
  37. package/dist/types/setup/scenes/outro.d.ts +5 -4
  38. package/dist/types/setup/scenes/sign-in.d.ts +13 -8
  39. package/dist/types/setup/scenes/splash.d.ts +4 -4
  40. package/dist/types/setup/scenes/types.d.ts +16 -1
  41. package/dist/types/setup/startup-splash.d.ts +1 -1
  42. package/dist/types/setup/wizard-overlay.d.ts +11 -7
  43. package/dist/types/setup/wizard.d.ts +5 -1
  44. package/dist/types/status-line/component.d.ts +1 -1
  45. package/dist/types/status-line/context-usage.d.ts +28 -6
  46. package/dist/types/status-line/host.d.ts +6 -2
  47. package/dist/types/theme/theme.d.ts +0 -23
  48. package/dist/types/tools/bash-interactive.d.ts +6 -0
  49. package/dist/types/tools/bash.d.ts +2 -0
  50. package/dist/types/tools/default-renderer.d.ts +6 -4
  51. package/dist/types/tools/irc.d.ts +6 -0
  52. package/dist/types/tools/json-tree.d.ts +23 -4
  53. package/dist/types/tools/lsp.d.ts +2 -0
  54. package/dist/types/tools/mcp.d.ts +6 -5
  55. package/dist/types/tools/native-view.d.ts +1 -1
  56. package/dist/types/tools/renderer.d.ts +2 -0
  57. package/dist/types/tools/streaming-output.d.ts +11 -0
  58. package/dist/types/tools/task.d.ts +9 -1
  59. package/dist/types/tools/url-scheme-host.d.ts +17 -0
  60. package/dist/types/tools/yield.d.ts +1 -1
  61. package/dist/types/tui.d.ts +7 -2
  62. package/package.json +10 -15
  63. package/src/app-keybindings.ts +15 -0
  64. package/src/apps/session-picker.ts +7 -0
  65. package/src/charts/chart-plan.ts +20 -50
  66. package/src/chat/advisor-message.ts +2 -0
  67. package/src/chat/assistant-message.ts +137 -19
  68. package/src/chat/background-tan-message.ts +4 -0
  69. package/src/chat/bash-execution.ts +6 -0
  70. package/src/chat/chat-transcript-builder.ts +58 -28
  71. package/src/chat/compaction-summary-message.ts +2 -1
  72. package/src/chat/display-preferences.ts +3 -0
  73. package/src/chat/extension-types.ts +34 -0
  74. package/src/chat/reply-write.ts +127 -0
  75. package/src/chat/table-chart.ts +10 -4
  76. package/src/chat/tool-execution.ts +20 -13
  77. package/src/chat/transcript-outline.ts +27 -0
  78. package/src/chat/transcript-render-helpers.ts +66 -24
  79. package/src/chrome/index.ts +1 -0
  80. package/src/chrome/transcript-notice.ts +83 -0
  81. package/src/chrome/transcript-status.ts +29 -29
  82. package/src/components/editor.ts +10 -1
  83. package/src/components/markdown.ts +116 -6
  84. package/src/hotkeys-markdown.ts +1 -0
  85. package/src/native/backend.ts +5 -66
  86. package/src/native/blobs.ts +3 -2
  87. package/src/native/picker.ts +3 -3
  88. package/src/overlays/advisor-config.ts +1 -10
  89. package/src/overlays/annotation-overlay.ts +125 -37
  90. package/src/overlays/annotation-types.ts +24 -0
  91. package/src/overlays/hook-selector.ts +2 -2
  92. package/src/overlays/login-dialog.ts +31 -5
  93. package/src/overlays/model-browser.ts +37 -0
  94. package/src/overlays/model-hub.ts +1 -7
  95. package/src/overlays/model-picker.ts +1 -10
  96. package/src/overlays/oauth-selector.ts +44 -6
  97. package/src/overlays/plan-review-overlay.ts +13 -2
  98. package/src/overlays/rewind-selector.ts +29 -12
  99. package/src/overlays/session-selector.ts +305 -39
  100. package/src/overlays/usage-dashboard.ts +5 -12
  101. package/src/prompt/composer.ts +213 -5
  102. package/src/prompt/custom-editor.ts +6 -1
  103. package/src/prompt/emoji-autocomplete.ts +0 -2
  104. package/src/prompt/interactive-context-helpers.ts +29 -4
  105. package/src/prompt/welcome.ts +31 -45
  106. package/src/render/extension-links.ts +214 -0
  107. package/src/render/hyperlink.ts +65 -9
  108. package/src/render/index.ts +1 -0
  109. package/src/setup/scenes/composer.ts +2 -0
  110. package/src/setup/scenes/glyph.ts +2 -0
  111. package/src/setup/scenes/model.ts +192 -29
  112. package/src/setup/scenes/outro.ts +30 -17
  113. package/src/setup/scenes/sign-in.ts +196 -400
  114. package/src/setup/scenes/splash.ts +25 -21
  115. package/src/setup/scenes/theme.ts +2 -0
  116. package/src/setup/scenes/types.ts +18 -1
  117. package/src/setup/startup-splash.ts +8 -7
  118. package/src/setup/wizard-overlay.ts +164 -33
  119. package/src/setup/wizard.ts +11 -2
  120. package/src/status-line/component.ts +4 -10
  121. package/src/status-line/context-usage.ts +72 -21
  122. package/src/status-line/host.ts +6 -2
  123. package/src/terminal.ts +24 -2
  124. package/src/theme/defaults.json +1 -0
  125. package/src/theme/loader.ts +1 -1
  126. package/src/theme/theme.ts +2 -75
  127. package/src/tools/bash-interactive.ts +6 -1
  128. package/src/tools/bash.ts +2 -0
  129. package/src/tools/default-renderer.ts +9 -6
  130. package/src/tools/eval.ts +14 -4
  131. package/src/tools/irc.ts +6 -0
  132. package/src/tools/job-widget.ts +13 -1
  133. package/src/tools/json-tree.ts +132 -42
  134. package/src/tools/lsp.ts +36 -15
  135. package/src/tools/mcp.ts +20 -12
  136. package/src/tools/native-view.ts +1 -1
  137. package/src/tools/renderer.ts +2 -0
  138. package/src/tools/streaming-output.ts +41 -0
  139. package/src/tools/task.ts +59 -17
  140. package/src/tools/url-scheme-host.ts +21 -0
  141. package/src/tools/yield.ts +18 -9
  142. package/src/tui.ts +43 -15
  143. package/THIRD-PARTY-NOTICES.txt +0 -22901
  144. package/dist/types/theme/defaults/index.d.ts +0 -10289
  145. package/src/theme/defaults/alabaster.json +0 -103
  146. package/src/theme/defaults/amethyst.json +0 -106
  147. package/src/theme/defaults/anthracite.json +0 -103
  148. package/src/theme/defaults/basalt.json +0 -101
  149. package/src/theme/defaults/birch.json +0 -105
  150. package/src/theme/defaults/dark-abyss.json +0 -101
  151. package/src/theme/defaults/dark-arctic.json +0 -114
  152. package/src/theme/defaults/dark-aurora.json +0 -105
  153. package/src/theme/defaults/dark-catppuccin.json +0 -117
  154. package/src/theme/defaults/dark-cavern.json +0 -101
  155. package/src/theme/defaults/dark-celestial.json +0 -116
  156. package/src/theme/defaults/dark-copper.json +0 -105
  157. package/src/theme/defaults/dark-cosmos.json +0 -100
  158. package/src/theme/defaults/dark-cyberpunk.json +0 -112
  159. package/src/theme/defaults/dark-dracula.json +0 -108
  160. package/src/theme/defaults/dark-eclipse.json +0 -101
  161. package/src/theme/defaults/dark-ember.json +0 -105
  162. package/src/theme/defaults/dark-equinox.json +0 -100
  163. package/src/theme/defaults/dark-forest.json +0 -106
  164. package/src/theme/defaults/dark-github.json +0 -115
  165. package/src/theme/defaults/dark-gruvbox.json +0 -122
  166. package/src/theme/defaults/dark-lavender.json +0 -105
  167. package/src/theme/defaults/dark-lunar.json +0 -99
  168. package/src/theme/defaults/dark-midnight.json +0 -105
  169. package/src/theme/defaults/dark-monochrome.json +0 -104
  170. package/src/theme/defaults/dark-monokai.json +0 -108
  171. package/src/theme/defaults/dark-nebula.json +0 -100
  172. package/src/theme/defaults/dark-neon-noir.json +0 -110
  173. package/src/theme/defaults/dark-nord.json +0 -108
  174. package/src/theme/defaults/dark-ocean.json +0 -111
  175. package/src/theme/defaults/dark-one.json +0 -110
  176. package/src/theme/defaults/dark-poimandres.json +0 -125
  177. package/src/theme/defaults/dark-rainforest.json +0 -101
  178. package/src/theme/defaults/dark-reef.json +0 -101
  179. package/src/theme/defaults/dark-retro.json +0 -102
  180. package/src/theme/defaults/dark-rose-pine.json +0 -106
  181. package/src/theme/defaults/dark-sakura.json +0 -105
  182. package/src/theme/defaults/dark-slate.json +0 -105
  183. package/src/theme/defaults/dark-solarized.json +0 -107
  184. package/src/theme/defaults/dark-solstice.json +0 -100
  185. package/src/theme/defaults/dark-starfall.json +0 -101
  186. package/src/theme/defaults/dark-sunset.json +0 -109
  187. package/src/theme/defaults/dark-swamp.json +0 -100
  188. package/src/theme/defaults/dark-synthwave.json +0 -113
  189. package/src/theme/defaults/dark-taiga.json +0 -101
  190. package/src/theme/defaults/dark-terminal.json +0 -105
  191. package/src/theme/defaults/dark-tokyo-night.json +0 -111
  192. package/src/theme/defaults/dark-tundra.json +0 -101
  193. package/src/theme/defaults/dark-twilight.json +0 -101
  194. package/src/theme/defaults/dark-volcanic.json +0 -101
  195. package/src/theme/defaults/graphite.json +0 -102
  196. package/src/theme/defaults/index.ts +0 -203
  197. package/src/theme/defaults/light-arctic.json +0 -117
  198. package/src/theme/defaults/light-aurora-day.json +0 -101
  199. package/src/theme/defaults/light-canyon.json +0 -101
  200. package/src/theme/defaults/light-catppuccin.json +0 -116
  201. package/src/theme/defaults/light-cirrus.json +0 -100
  202. package/src/theme/defaults/light-coral.json +0 -105
  203. package/src/theme/defaults/light-cyberpunk.json +0 -106
  204. package/src/theme/defaults/light-dawn.json +0 -100
  205. package/src/theme/defaults/light-dunes.json +0 -101
  206. package/src/theme/defaults/light-eucalyptus.json +0 -105
  207. package/src/theme/defaults/light-forest.json +0 -110
  208. package/src/theme/defaults/light-frost.json +0 -105
  209. package/src/theme/defaults/light-github.json +0 -125
  210. package/src/theme/defaults/light-glacier.json +0 -101
  211. package/src/theme/defaults/light-gruvbox.json +0 -118
  212. package/src/theme/defaults/light-haze.json +0 -100
  213. package/src/theme/defaults/light-honeycomb.json +0 -105
  214. package/src/theme/defaults/light-lagoon.json +0 -101
  215. package/src/theme/defaults/light-lavender.json +0 -105
  216. package/src/theme/defaults/light-meadow.json +0 -101
  217. package/src/theme/defaults/light-mint.json +0 -105
  218. package/src/theme/defaults/light-monochrome.json +0 -111
  219. package/src/theme/defaults/light-ocean.json +0 -109
  220. package/src/theme/defaults/light-one.json +0 -109
  221. package/src/theme/defaults/light-opal.json +0 -101
  222. package/src/theme/defaults/light-orchard.json +0 -101
  223. package/src/theme/defaults/light-paper.json +0 -105
  224. package/src/theme/defaults/light-poimandres.json +0 -125
  225. package/src/theme/defaults/light-prism.json +0 -100
  226. package/src/theme/defaults/light-retro.json +0 -108
  227. package/src/theme/defaults/light-sand.json +0 -105
  228. package/src/theme/defaults/light-savanna.json +0 -101
  229. package/src/theme/defaults/light-solarized.json +0 -112
  230. package/src/theme/defaults/light-soleil.json +0 -100
  231. package/src/theme/defaults/light-sunset.json +0 -109
  232. package/src/theme/defaults/light-synthwave.json +0 -108
  233. package/src/theme/defaults/light-tokyo-night.json +0 -121
  234. package/src/theme/defaults/light-wetland.json +0 -101
  235. package/src/theme/defaults/light-zenith.json +0 -99
  236. package/src/theme/defaults/limestone.json +0 -104
  237. package/src/theme/defaults/mahogany.json +0 -107
  238. package/src/theme/defaults/marble.json +0 -103
  239. package/src/theme/defaults/obsidian.json +0 -101
  240. package/src/theme/defaults/onyx.json +0 -101
  241. package/src/theme/defaults/pearl.json +0 -103
  242. package/src/theme/defaults/porcelain.json +0 -101
  243. package/src/theme/defaults/quartz.json +0 -106
  244. package/src/theme/defaults/sandstone.json +0 -105
  245. package/src/theme/defaults/titanium.json +0 -100
@@ -65,8 +65,9 @@ import {
65
65
  routeLateDiagnostics,
66
66
  } from "./late-diagnostics-message";
67
67
  import { groupedReadUsageCallIds, ReadToolGroupComponent, readArgsCollapseIntoGroup } from "./read-tool-group";
68
+ import { isReplyWrite, ReplyWriteComponent, replyDisplayMessage, toolResultReplies } from "./reply-write";
68
69
  import { SkillMessageComponent } from "./skill-message";
69
- import { ToolExecutionComponent } from "./tool-execution";
70
+ import { ToolExecutionComponent, type ToolExecutionHandle } from "./tool-execution";
70
71
  import { TranscriptContainer } from "../chrome/transcript-container";
71
72
  import { createUsageRowBlock, TurnUsageTally, turnElapsedMs } from "../overlays/usage-row";
72
73
  import { CollapsedSyntheticMessageComponent, UserMessageComponent } from "./user-message";
@@ -90,7 +91,7 @@ export interface ChatTranscriptBuilderDeps {
90
91
 
91
92
  export class ChatTranscriptBuilder {
92
93
  readonly container = new TranscriptContainer();
93
- #pendingTools = new Map<string, ToolExecutionComponent | ReadToolGroupComponent>();
94
+ #pendingTools = new Map<string, ToolExecutionHandle>();
94
95
  #readArgs = new Map<string, Record<string, unknown>>();
95
96
  #readGroup: ReadToolGroupComponent | null = null;
96
97
  #pendingUsage: Usage | undefined;
@@ -203,6 +204,15 @@ export class ChatTranscriptBuilder {
203
204
  this.#expandables.push(component);
204
205
  }
205
206
 
207
+ /** Append assistant prose (a reply, a post-tool segment, or an `out://` reply write) with the transcript's display preferences. */
208
+ #mountProse(component: AssistantMessageComponent): void {
209
+ component.setImagesVisible(displayPreferences.showImages);
210
+ component.setToolResultImagesVisible(!displayPreferences.hideToolActivity);
211
+ component.setTableChartsVisible(this.#deps.tableCharts !== false);
212
+ this.#trackExpandable(component);
213
+ this.container.addChild(component);
214
+ }
215
+
206
216
  /** A `wait` showing all-running is displaced by the next `wait` call. */
207
217
  #resolveWaitingPoll(nextToolName?: string): void {
208
218
  const previous = this.#waitingPoll;
@@ -417,12 +427,8 @@ export class ChatTranscriptBuilder {
417
427
  this.#deps.linkTargets,
418
428
  expandThinkingBlocks,
419
429
  );
420
- assistantComponent.setImagesVisible(displayPreferences.showImages);
421
- assistantComponent.setToolResultImagesVisible(!displayPreferences.hideToolActivity);
422
- assistantComponent.setTableChartsVisible(this.#deps.tableCharts !== false);
423
- this.#trackExpandable(assistantComponent);
424
430
  assistantComponent.pickReactionTarget(this.container.children);
425
- this.container.addChild(assistantComponent);
431
+ this.#mountProse(assistantComponent);
426
432
  let lastAssistantComponent = assistantComponent;
427
433
 
428
434
  if (displayPreferences.cacheMissMarker) {
@@ -456,11 +462,7 @@ export class ChatTranscriptBuilder {
456
462
  this.#deps.linkTargets,
457
463
  expandThinkingBlocks,
458
464
  );
459
- component.setImagesVisible(displayPreferences.showImages);
460
- component.setToolResultImagesVisible(!displayPreferences.hideToolActivity);
461
- component.setTableChartsVisible(this.#deps.tableCharts !== false);
462
- this.#trackExpandable(component);
463
- this.container.addChild(component);
465
+ this.#mountProse(component);
464
466
  lastAssistantComponent = component;
465
467
  };
466
468
 
@@ -492,22 +494,37 @@ export class ChatTranscriptBuilder {
492
494
 
493
495
  this.#readGroup?.seal();
494
496
  this.#readGroup = null;
495
- const component = new ToolExecutionComponent(
496
- content.name,
497
- content.arguments,
498
- {
499
- useBuiltInRenderer: this.#deps.isBuiltInTool?.(content.name) ?? true,
500
- // Stable ids and Kitty placeholder cells keep images anchored
501
- // while the transcript viewport scrolls and reflows.
502
- showImages: displayPreferences.showImages,
503
- },
504
- this.#deps.getTool?.(content.name),
505
- this.#deps.ui,
506
- this.#deps.cwd,
507
- content.id,
508
- );
509
- this.#trackExpandable(component);
510
- this.container.addChild(component);
497
+ const builtIn = this.#deps.isBuiltInTool?.(content.name) ?? true;
498
+ let component: ToolExecutionHandle;
499
+ if (builtIn && isReplyWrite(content.name, content.arguments) === true) {
500
+ const reply = new ReplyWriteComponent({
501
+ requestRender: () => this.#deps.requestRender(),
502
+ imageBudget: this.#deps.ui.imageBudget,
503
+ linkTargets: this.#deps.linkTargets,
504
+ });
505
+ reply.updateArgs(content.arguments);
506
+ reply.setArgsComplete();
507
+ this.#mountProse(reply);
508
+ component = reply;
509
+ } else {
510
+ const card = new ToolExecutionComponent(
511
+ content.name,
512
+ content.arguments,
513
+ {
514
+ useBuiltInRenderer: builtIn,
515
+ // Stable ids and Kitty placeholder cells keep images anchored
516
+ // while the transcript viewport scrolls and reflows.
517
+ showImages: displayPreferences.showImages,
518
+ },
519
+ this.#deps.getTool?.(content.name),
520
+ this.#deps.ui,
521
+ this.#deps.cwd,
522
+ content.id,
523
+ );
524
+ this.#trackExpandable(card);
525
+ this.container.addChild(card);
526
+ component = card;
527
+ }
511
528
 
512
529
  if (hasErrorStop && errorMessage) {
513
530
  component.updateResult(
@@ -549,6 +566,19 @@ export class ChatTranscriptBuilder {
549
566
  this.#readArgs.delete(message.toolCallId);
550
567
  return;
551
568
  }
569
+ for (const reply of toolResultReplies(message.details)) {
570
+ this.#mountProse(
571
+ new AssistantMessageComponent(
572
+ replyDisplayMessage(reply),
573
+ false,
574
+ () => this.#deps.requestRender(),
575
+ [],
576
+ this.#deps.ui.imageBudget,
577
+ true,
578
+ this.#deps.linkTargets,
579
+ ),
580
+ );
581
+ }
552
582
  if (!pending) return;
553
583
  pending.updateResult(message, false, message.toolCallId);
554
584
  this.#pendingTools.delete(message.toolCallId);
@@ -10,6 +10,7 @@ import type { TspSpan } from "@oh-my-pi/pi-wire";
10
10
  import { md, node, span, text } from "../native/describe";
11
11
  import { type NativeChild, type NativeNode, type NativeUiEvent, rootToggleExpanded } from "../native/node";
12
12
  import { Memo } from "../native/memo";
13
+ import { noticeRow } from "../chrome/transcript-notice";
13
14
 
14
15
  /** Divider labels per compaction method; unknown/legacy methods fall back to "compacted". */
15
16
  const COMPACTION_METHOD_LABELS: Record<string, string> = {
@@ -237,7 +238,7 @@ function compactionNativeDetail(message: CompactionSummaryMessage): NativeChild[
237
238
  ? `Summary · ${frames} snapcompact frame${frames === 1 ? "" : "s"} attached`
238
239
  : "Summary of the earlier conversation";
239
240
  const detail: NativeChild[] = [text([span(caption, "dim")], { role: "omp.compaction.caption", key: "caption" })];
240
- if (message.warning) detail.push(text([span(message.warning, "warning")], { wrap: "word", key: "warning" }));
241
+ if (message.warning) detail.push(noticeRow("warning", message.warning, { key: "warning" }));
241
242
  detail.push(md(message.summary, { key: "summary" }));
242
243
  return detail;
243
244
  }
@@ -6,6 +6,8 @@ export interface ChatTranscriptDisplayPreferences {
6
6
  cacheMissMarker: boolean;
7
7
  showTokenUsage: boolean;
8
8
  showTurnTime: boolean;
9
+ /** Max rows a collapsed advisor card shows below its header; 0 = no cap. */
10
+ advisorCollapsedLines: number;
9
11
  }
10
12
 
11
13
  /** Current transcript display preferences. */
@@ -16,6 +18,7 @@ export const chatTranscriptDisplayPreferences: ChatTranscriptDisplayPreferences
16
18
  cacheMissMarker: false,
17
19
  showTokenUsage: false,
18
20
  showTurnTime: false,
21
+ advisorCollapsedLines: 0,
19
22
  };
20
23
 
21
24
  /** Apply host display preferences without pulling settings into the renderer. */
@@ -1,3 +1,4 @@
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
1
2
  import type { Component, TUI } from "../tui";
2
3
  import type { Theme } from "../theme/theme";
3
4
  import type { CustomMessage, HookMessage } from "./messages";
@@ -30,6 +31,39 @@ export type AssistantThinkingRenderer = (
30
31
  theme: Theme,
31
32
  ) => Component | undefined;
32
33
 
34
+ export interface AssistantTextRenderContext {
35
+ /**
36
+ * The message being drawn; `message.timestamp` identifies the reply for `ctx.ui.rerenderMessage()`.
37
+ * After a tool call this is only the segment drawn there, so its `content` does not hold the whole reply.
38
+ * A deep copy: editing it, at any depth, cannot change the stored reply.
39
+ */
40
+ message: AssistantMessage;
41
+ /**
42
+ * The text block's index in the full reply's `content`, also when the reply is drawn in segments
43
+ * around tool calls; not an index into `message.content` for a segment. It counts the content as
44
+ * currently stored: a transcript rebuild that drops tool calls left without a result shifts later
45
+ * blocks down, so do not persist it as a permanent key.
46
+ */
47
+ contentIndex: number;
48
+ /** The block's markdown as drawn so far, after any earlier renderer. */
49
+ text: string;
50
+ /** True while the block may still grow; false for its final render, a block sealed at a tool call, and later redraws. */
51
+ streaming: boolean;
52
+ }
53
+
54
+ /**
55
+ * Display-only transform of one assistant text block: returns the markdown to draw, or `undefined`
56
+ * to draw `context.text` unchanged. The session, exports and model context keep the original text.
57
+ */
58
+ export type AssistantTextRenderer = (context: AssistantTextRenderContext) => string | undefined;
59
+
60
+ /** Extension text renderers for one assistant component, and where its content starts in the full reply. */
61
+ export interface AssistantTextRendering {
62
+ renderers: readonly AssistantTextRenderer[];
63
+ /** Index in the full reply's `content` of this component's first block; a segment after a tool call starts past it. */
64
+ contentOffset: number;
65
+ }
66
+
33
67
  export interface HookMessageRenderOptions {
34
68
  /** Whether the view is expanded */
35
69
  expanded: boolean;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Replies: text the agent sends to a scheme whose writes are `reply`-scoped (`out://`)
3
+ * is its mid-turn message to the user, drawn as assistant prose so it can answer
4
+ * without ending its turn.
5
+ * - A `write` call to a reply URL draws as {@link ReplyWriteComponent}, streaming as the
6
+ * call's arguments arrive, instead of a tool card.
7
+ * - Text a tool's commands write there (`printf … > out://` in bash) arrives in the
8
+ * tool's result `details.replies` ({@link toolResultReplies}); transcripts draw each
9
+ * one as a prose block after the call ({@link replyDisplayMessage}).
10
+ */
11
+ import type { AssistantMessage, Usage } from "@oh-my-pi/pi-ai";
12
+ import { isRecord } from "@oh-my-pi/pi-utils";
13
+ import type { ImageBudget } from "../components/image";
14
+ import { internalUrlSchemeSpec, internalUrlSchemeSpecs, splitUrlScheme } from "../tools/url-scheme-host";
15
+ import { AssistantMessageComponent } from "./assistant-message";
16
+ import type { ToolExecutionHandle, ToolResultFrame } from "./tool-execution";
17
+
18
+ /**
19
+ * Whether a `toolName` call writes to a reply URL. `undefined` while streamed args cannot tell yet:
20
+ * no path so far, or a path that could still grow into a reply URL (`ou`, `out:/`). Callers holding
21
+ * final args treat `undefined` as `false`.
22
+ */
23
+ export function isReplyWrite(toolName: string, args: unknown): boolean | undefined {
24
+ if (toolName !== "write") return false;
25
+ if (!isRecord(args)) return undefined;
26
+ const target = typeof args.path === "string" ? args.path : args.file_path;
27
+ if (typeof target !== "string") return undefined;
28
+ const url = splitUrlScheme(target);
29
+ if (url) return internalUrlSchemeSpec(url.scheme)?.write?.scope === "reply";
30
+ const lower = target.toLowerCase();
31
+ for (const [scheme, spec] of internalUrlSchemeSpecs()) {
32
+ if (spec.write?.scope === "reply" && `${scheme}://`.startsWith(lower)) return undefined;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ /** Display options of a reply block; the rest of its presentation follows the assistant prose defaults. */
38
+ export interface ReplyWriteOptions {
39
+ /** Repaint request for figures and images that finish loading after a render. */
40
+ readonly requestRender?: () => void;
41
+ readonly imageBudget?: ImageBudget;
42
+ /** Resolved local destinations of Markdown links, as for assistant text. */
43
+ readonly linkTargets?: ReadonlyMap<string, string>;
44
+ }
45
+
46
+ const NO_USAGE: Usage = {
47
+ input: 0,
48
+ output: 0,
49
+ cacheRead: 0,
50
+ cacheWrite: 0,
51
+ totalTokens: 0,
52
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
53
+ };
54
+
55
+ /** Replies a tool result carries in `details.replies` (text its commands wrote to a reply URL), oldest first. */
56
+ export function toolResultReplies(details: unknown): readonly string[] {
57
+ if (!isRecord(details) || !Array.isArray(details.replies)) return [];
58
+ return details.replies.filter((reply): reply is string => typeof reply === "string");
59
+ }
60
+
61
+ /** Assistant message drawing reply text. No provider turn backs it, so its identity fields stay empty. */
62
+ export function replyDisplayMessage(text: string, errorMessage?: string): AssistantMessage {
63
+ return {
64
+ role: "assistant",
65
+ content: [{ type: "text", text }],
66
+ api: "",
67
+ provider: "",
68
+ model: "",
69
+ usage: NO_USAGE,
70
+ stopReason: errorMessage === undefined ? "stop" : "error",
71
+ errorMessage,
72
+ timestamp: 0,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * A reply write ({@link isReplyWrite}) drawn as assistant prose. The call's lifecycle arrives
78
+ * through {@link ToolExecutionHandle}: streamed args grow the text, final args settle it, and
79
+ * the result seals the block, a failure showing its error under the text. It is a reply, not
80
+ * tool activity, so hiding tool activity leaves it on screen.
81
+ */
82
+ export class ReplyWriteComponent extends AssistantMessageComponent implements ToolExecutionHandle {
83
+ #text = "";
84
+ #argsComplete = false;
85
+
86
+ constructor(options: ReplyWriteOptions = {}) {
87
+ super(undefined, false, options.requestRender, [], options.imageBudget, true, options.linkTargets);
88
+ }
89
+
90
+ updateArgs(args: unknown): void {
91
+ const text = isRecord(args) && typeof args.content === "string" ? args.content : "";
92
+ if (text === this.#text || this.isTranscriptBlockFinalized()) return;
93
+ this.#text = text;
94
+ this.#draw();
95
+ }
96
+
97
+ setArgsComplete(): void {
98
+ if (this.#argsComplete || this.isTranscriptBlockFinalized()) return;
99
+ this.#argsComplete = true;
100
+ this.#draw();
101
+ }
102
+
103
+ setExecutionStarted(): void {}
104
+
105
+ updateResult(result: ToolResultFrame, isPartial = false): void {
106
+ if (isPartial || this.isTranscriptBlockFinalized()) return;
107
+ this.#argsComplete = true;
108
+ const error = result.isError
109
+ ? result.content
110
+ .filter(block => block.type === "text")
111
+ .map(block => block.text ?? "")
112
+ .join("\n")
113
+ : undefined;
114
+ this.updateContent(replyDisplayMessage(this.#text, error));
115
+ this.markTranscriptBlockFinalized();
116
+ }
117
+
118
+ parkAsBackground(): void {}
119
+
120
+ seal(): void {
121
+ this.markTranscriptBlockFinalized();
122
+ }
123
+
124
+ #draw(): void {
125
+ this.updateContent(replyDisplayMessage(this.#text), { transient: !this.#argsComplete });
126
+ }
127
+ }
@@ -37,10 +37,11 @@ export interface TableChartRequest {
37
37
  }
38
38
 
39
39
  /**
40
- * Picks the chart for a multi-series table under `smart`: a plan, or `null`
41
- * for no chart. A rejection falls back to the local guess.
40
+ * Picks the chart for a table under `smart`: a plan, or `null` for no chart.
41
+ * A rejection falls back to the local guess; `undefined` (no judge to ask
42
+ * right now) keeps the guess at once, exactly as `always` would draw it.
42
43
  */
43
- export type TableChartPlanner = (request: TableChartRequest) => Promise<ChartPlan | null>;
44
+ export type TableChartPlanner = (request: TableChartRequest) => Promise<ChartPlan | null> | undefined;
44
45
 
45
46
  /**
46
47
  * The chart under a table: its SVG, colored with figure tokens (`var(--accent)`, …)
@@ -126,7 +127,12 @@ export function lookupTableChart(table: Tokens.Table): ChartLookup {
126
127
  if (entry.chart !== undefined) return entry.chart;
127
128
  if (entry.pending) return entry.pending;
128
129
  const { analysis, guess } = entry;
129
- entry.pending = chartPlanner!({ markdown: table.raw, table: analysis, guess: guess! }).then(
130
+ const pick = chartPlanner!({ markdown: table.raw, table: analysis, guess: guess! });
131
+ if (!pick) {
132
+ entry.chart = drawChart(analysis, guess!, true);
133
+ return entry.chart;
134
+ }
135
+ entry.pending = pick.then(
130
136
  plan => {
131
137
  entry.chart = plan ? drawChart(analysis, plan) : null;
132
138
  },
@@ -126,6 +126,9 @@ function cardPreview(preview: NativeToolView["preview"]): TspPreview | undefined
126
126
  /** The describe hooks of a registry renderer or of a custom tool object. */
127
127
  type ToolDescriber = Pick<ToolRenderer, "describeCall" | "describeResult" | "mergeCallAndResult">;
128
128
 
129
+ /** Optional kinds of the host's vocabulary the describe hooks branch on. */
130
+ type NativeHostKinds = Required<Pick<RenderResultContextOptions, "media" | "json">>;
131
+
129
132
  class SafeToolRendererComponent implements Component {
130
133
  #toolName: string;
131
134
  #stage: ToolRendererStage;
@@ -233,7 +236,6 @@ export interface ToolExecutionHandle extends Component {
233
236
  setArgsComplete(toolCallId?: string): void;
234
237
  setExecutionStarted(toolCallId?: string): void;
235
238
  setExpanded(expanded: boolean): void;
236
- setToolActivityVisible(visible: boolean): void;
237
239
  /** Mark the call parked: it returned, but stays tracked for async job frames. */
238
240
  parkAsBackground(): void;
239
241
  /** Seal the block as final history and stop its animations. */
@@ -711,14 +713,15 @@ export class ToolExecutionComponent extends Container {
711
713
  // Convert async - catch errors from processing
712
714
  convertImageToPngShared(image)
713
715
  .then(converted => {
716
+ this.#kittyConversionsAwaited.delete(key);
714
717
  this.#kittyConverted.set(key, converted);
715
718
  this.#displayInputVersion++;
716
719
  this.#updateDisplay();
717
720
  this.#ui.requestRender();
718
721
  })
719
722
  .catch(() => {
720
- // Ignore conversion failures - display will use original image format
721
723
  this.#kittyConversionsAwaited.delete(key);
724
+ this.#ui.requestRender();
722
725
  });
723
726
  }
724
727
  }
@@ -859,9 +862,12 @@ export class ToolExecutionComponent extends Container {
859
862
  }
860
863
  }
861
864
 
862
- /** Whether the result's figure still waits for its raster; the transcript holds retirement meanwhile. */
865
+ /** Hold visible results until their figures and Kitty images are ready for immutable scrollback. */
863
866
  isTranscriptBlockPending(): boolean {
864
- return this.#toolActivityVisible && this.#figure?.pending === true;
867
+ return (
868
+ this.#toolActivityVisible &&
869
+ ((this.#showImages && this.#kittyConversionsAwaited.size > 0) || this.#figure?.pending === true)
870
+ );
865
871
  }
866
872
 
867
873
  /**
@@ -1018,7 +1024,7 @@ export class ToolExecutionComponent extends Container {
1018
1024
  override describe(cx?: DescribeContext): NativeNode {
1019
1025
  if (this.#toolName === "wait" && this.#isBenignSkip()) return EMPTY_NODE;
1020
1026
  const dataFirst = cx?.supports("tool") === true;
1021
- const media = cx?.supports("media") === true;
1027
+ const host: NativeHostKinds = { media: cx?.supports("media") === true, json: cx?.supports("json") === true };
1022
1028
  const key = [
1023
1029
  dataFirst,
1024
1030
  this.#resultVersion,
@@ -1031,11 +1037,12 @@ export class ToolExecutionComponent extends Container {
1031
1037
  this.#showImages,
1032
1038
  this.#displayInputVersion,
1033
1039
  this.#toolActivityVisible,
1034
- media,
1040
+ host.media,
1041
+ host.json,
1035
1042
  getThemeEpoch(),
1036
1043
  ];
1037
1044
  return this.#native.get(key, () =>
1038
- withHidden(dataFirst ? this.#describeTool(media) : this.#describeCard(media), !this.#toolActivityVisible),
1045
+ withHidden(dataFirst ? this.#describeTool(host) : this.#describeCard(host), !this.#toolActivityVisible),
1039
1046
  );
1040
1047
  }
1041
1048
 
@@ -1081,8 +1088,8 @@ export class ToolExecutionComponent extends Container {
1081
1088
  * The data-first `tool` node (§7.2): the renderer's head data, status,
1082
1089
  * timer, exit and intent as props; its body as borderless children.
1083
1090
  */
1084
- #describeTool(media: boolean): NativeNode {
1085
- const view = this.#nativeView(media);
1091
+ #describeTool(host: NativeHostKinds): NativeNode {
1092
+ const view = this.#nativeView(host);
1086
1093
  const status = this.#nativeStatus();
1087
1094
  const head = view.tool;
1088
1095
  const late = this.#lateDiagnosticsParts();
@@ -1151,8 +1158,8 @@ export class ToolExecutionComponent extends Container {
1151
1158
  return spans;
1152
1159
  }
1153
1160
 
1154
- #describeCard(media: boolean): NativeNode {
1155
- const view = this.#nativeView(media);
1161
+ #describeCard(host: NativeHostKinds): NativeNode {
1162
+ const view = this.#nativeView(host);
1156
1163
  const status = this.#nativeStatus();
1157
1164
  const late = this.#lateDiagnosticsParts();
1158
1165
  const fallbackHead = this.#fallbackHead(view);
@@ -1206,10 +1213,10 @@ export class ToolExecutionComponent extends Container {
1206
1213
  * the result view (merged renderers) or the call head over both bodies.
1207
1214
  * A renderer that throws falls back to the generic card.
1208
1215
  */
1209
- #nativeView(media: boolean): NativeToolView {
1216
+ #nativeView(host: NativeHostKinds): NativeToolView {
1210
1217
  const status = this.#nativeStatus();
1211
1218
  const options: RenderResultContextOptions = {
1212
- media,
1219
+ ...host,
1213
1220
  expanded: this.#expanded,
1214
1221
  isPartial: this.#isPartial,
1215
1222
  argsComplete: this.#argsComplete,
@@ -17,6 +17,7 @@ import { type ThemeColor, theme } from "../theme";
17
17
  import type { ChatTranscriptBuilder } from "./chat-transcript-builder";
18
18
  import { padToWidth } from "../render/utils";
19
19
  import { isUsageRowBlock } from "../overlays/usage-row";
20
+ import type { DescribeContext, NativeChild } from "../native/node";
20
21
 
21
22
  /** One selectable transcript item: a message entry plus its rendered block range. */
22
23
  export interface OutlineTarget {
@@ -148,6 +149,32 @@ export function outlineVisibility(
148
149
  });
149
150
  }
150
151
 
152
+ /**
153
+ * Per-target "describes something that shows" flags: {@link outlineVisibility}
154
+ * for a TSP page, where blocks describe nodes instead of rendering rows. An
155
+ * empty `col`/`row` (an interrupted turn with no output) shows nothing.
156
+ */
157
+ export function describedVisibility(
158
+ blocks: readonly Component[],
159
+ targets: readonly OutlineTarget[],
160
+ cx: DescribeContext,
161
+ ): boolean[] {
162
+ return targets.map(target => {
163
+ for (let index = target.start; index < target.end; index++) {
164
+ if (shows(blocks[index]!, cx)) return true;
165
+ }
166
+ return false;
167
+ });
168
+ }
169
+
170
+ /** Whether `child` describes a node that shows: a layout `col`/`row` only through a child that does. */
171
+ function shows(child: NativeChild, cx: DescribeContext): boolean {
172
+ const node = "k" in child ? child : child.describe?.(cx);
173
+ if (!node) return false;
174
+ if (node.k !== "col" && node.k !== "row") return true;
175
+ return (node.c ?? []).some(grandchild => shows(grandchild, cx));
176
+ }
177
+
151
178
  /** Dotted horizontal rule with rounded corners, spanning the outline width. */
152
179
  export function outlineRule(
153
180
  left: string,