@oh-my-pi/pi-tui 18.2.4 → 18.2.6

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 (748) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +43 -0
  3. package/dist/types/app-keybindings.d.ts +395 -0
  4. package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
  5. package/dist/types/apps/autoresearch-data.d.ts +15 -0
  6. package/dist/types/apps/cleanse-board.d.ts +66 -0
  7. package/dist/types/apps/cleanse-picker.d.ts +23 -0
  8. package/dist/types/apps/debug/log-formatting.d.ts +6 -0
  9. package/dist/types/apps/debug/log-viewer.d.ts +81 -0
  10. package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
  11. package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
  12. package/dist/types/apps/debug/raw-sse.d.ts +26 -0
  13. package/dist/types/apps/debug/terminal-info.d.ts +34 -0
  14. package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
  15. package/dist/types/apps/git/avatar.d.ts +9 -0
  16. package/dist/types/apps/git/colors.d.ts +49 -0
  17. package/dist/types/apps/git/diff-pane.d.ts +188 -0
  18. package/dist/types/apps/git/git-tui.d.ts +66 -0
  19. package/dist/types/apps/git/sidebar.d.ts +149 -0
  20. package/dist/types/apps/git/state.d.ts +117 -0
  21. package/dist/types/apps/if-bench-board.d.ts +75 -0
  22. package/dist/types/apps/live-visualizer.d.ts +33 -0
  23. package/dist/types/apps/ps-data.d.ts +51 -0
  24. package/dist/types/apps/ps-top.d.ts +33 -0
  25. package/dist/types/apps/session-picker.d.ts +26 -0
  26. package/dist/types/apps/setup-model-picker.d.ts +13 -0
  27. package/dist/types/apps/standalone-picker.d.ts +42 -0
  28. package/dist/types/autocomplete.d.ts +4 -0
  29. package/dist/types/chat/advisor-message.d.ts +11 -0
  30. package/dist/types/chat/assistant-message.d.ts +106 -0
  31. package/dist/types/chat/background-tan-message.d.ts +9 -0
  32. package/dist/types/chat/bash-execution.d.ts +47 -0
  33. package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
  34. package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
  35. package/dist/types/chat/collab-prompt-message.d.ts +10 -0
  36. package/dist/types/chat/compaction-summary-message.d.ts +54 -0
  37. package/dist/types/chat/custom-message.d.ts +10 -0
  38. package/dist/types/chat/display-preferences.d.ts +13 -0
  39. package/dist/types/chat/eval-execution.d.ts +29 -0
  40. package/dist/types/chat/execution-shared.d.ts +51 -0
  41. package/dist/types/chat/extension-types.d.ts +28 -0
  42. package/dist/types/chat/hook-message.d.ts +10 -0
  43. package/dist/types/chat/image-loading.d.ts +46 -0
  44. package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
  45. package/dist/types/chat/messages.d.ts +189 -0
  46. package/dist/types/chat/reaction.d.ts +30 -0
  47. package/dist/types/chat/read-target.d.ts +4 -0
  48. package/dist/types/chat/read-tool-group.d.ts +67 -0
  49. package/dist/types/chat/served-model-marker.d.ts +47 -0
  50. package/dist/types/chat/skill-message.d.ts +33 -0
  51. package/dist/types/chat/skill-title-input.d.ts +14 -0
  52. package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
  53. package/dist/types/chat/thinking-display.d.ts +12 -0
  54. package/dist/types/chat/todo-reminder.d.ts +13 -0
  55. package/dist/types/chat/tool-execution.d.ts +138 -0
  56. package/dist/types/chat/transcript-browser.d.ts +93 -0
  57. package/dist/types/chat/transcript-entry.d.ts +45 -0
  58. package/dist/types/chat/transcript-outline.d.ts +86 -0
  59. package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
  60. package/dist/types/chat/ttsr-notification.d.ts +23 -0
  61. package/dist/types/chat/user-message.d.ts +52 -0
  62. package/dist/types/chrome/chat-block.d.ts +64 -0
  63. package/dist/types/chrome/collab-qrcode.d.ts +28 -0
  64. package/dist/types/chrome/context-thresholds.d.ts +10 -0
  65. package/dist/types/chrome/countdown-timer.d.ts +14 -0
  66. package/dist/types/chrome/diff.d.ts +14 -0
  67. package/dist/types/chrome/dynamic-border.d.ts +16 -0
  68. package/dist/types/chrome/error-block.d.ts +17 -0
  69. package/dist/types/chrome/form-theme.d.ts +3 -0
  70. package/dist/types/chrome/format.d.ts +12 -0
  71. package/dist/types/chrome/index.d.ts +20 -0
  72. package/dist/types/chrome/keybinding-hints.d.ts +40 -0
  73. package/dist/types/chrome/live-board.d.ts +23 -0
  74. package/dist/types/chrome/local-date.d.ts +4 -0
  75. package/dist/types/chrome/message-divider.d.ts +22 -0
  76. package/dist/types/chrome/message-frame.d.ts +52 -0
  77. package/dist/types/chrome/message-notice.d.ts +35 -0
  78. package/dist/types/chrome/overlay-box.d.ts +74 -0
  79. package/dist/types/chrome/qrcode.d.ts +48 -0
  80. package/dist/types/chrome/segment-track.d.ts +27 -0
  81. package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
  82. package/dist/types/chrome/selector-helpers.d.ts +52 -0
  83. package/dist/types/chrome/shared.d.ts +5 -0
  84. package/dist/types/chrome/streaming-panel.d.ts +19 -0
  85. package/dist/types/chrome/tool-activity.d.ts +17 -0
  86. package/dist/types/chrome/transcript-container.d.ts +129 -0
  87. package/dist/types/chrome/transcript-status.d.ts +12 -0
  88. package/dist/types/chrome/visual-truncate.d.ts +19 -0
  89. package/dist/types/components/disclosure.d.ts +42 -0
  90. package/dist/types/components/form.d.ts +130 -0
  91. package/dist/types/components/key-value-list.d.ts +24 -0
  92. package/dist/types/components/layout/geometry.d.ts +73 -0
  93. package/dist/types/components/layout/row.d.ts +55 -0
  94. package/dist/types/components/layout/split-pane.d.ts +80 -0
  95. package/dist/types/components/layout/stack.d.ts +50 -0
  96. package/dist/types/components/menu-selection.d.ts +71 -0
  97. package/dist/types/components/metric.d.ts +44 -0
  98. package/dist/types/components/progress-bar.d.ts +36 -0
  99. package/dist/types/components/scroll-view.d.ts +53 -19
  100. package/dist/types/components/scroll-viewport.d.ts +46 -0
  101. package/dist/types/components/section.d.ts +19 -0
  102. package/dist/types/components/select-list.d.ts +46 -2
  103. package/dist/types/components/settings-list.d.ts +20 -0
  104. package/dist/types/components/table.d.ts +32 -0
  105. package/dist/types/components/tree-view.d.ts +130 -0
  106. package/dist/types/components/wizard-step.d.ts +60 -0
  107. package/dist/types/hotkeys-markdown.d.ts +7 -0
  108. package/dist/types/index.d.ts +17 -0
  109. package/dist/types/keybinding-matchers.d.ts +35 -0
  110. package/dist/types/keys.d.ts +2 -2
  111. package/dist/types/lang-from-path.d.ts +9 -0
  112. package/dist/types/overlays/advisor-config.d.ts +114 -0
  113. package/dist/types/overlays/agent-activity.d.ts +41 -0
  114. package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
  115. package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
  116. package/dist/types/overlays/agent-hub-types.d.ts +99 -0
  117. package/dist/types/overlays/agent-hub.d.ts +126 -0
  118. package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
  119. package/dist/types/overlays/agents-hub.d.ts +72 -0
  120. package/dist/types/overlays/ask-dialog.d.ts +88 -0
  121. package/dist/types/overlays/bordered-loader.d.ts +11 -0
  122. package/dist/types/overlays/btw-history-panel.d.ts +29 -0
  123. package/dist/types/overlays/btw-history.d.ts +17 -0
  124. package/dist/types/overlays/btw-panel.d.ts +26 -0
  125. package/dist/types/overlays/cleanse-panel.d.ts +36 -0
  126. package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
  127. package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
  128. package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
  129. package/dist/types/overlays/copy-selector.d.ts +48 -0
  130. package/dist/types/overlays/copy-targets.d.ts +63 -0
  131. package/dist/types/overlays/error-banner.d.ts +13 -0
  132. package/dist/types/overlays/extensions/display-text.d.ts +10 -0
  133. package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
  134. package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
  135. package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
  136. package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
  137. package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
  138. package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
  139. package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
  140. package/dist/types/overlays/extensions/types.d.ts +165 -0
  141. package/dist/types/overlays/history-search.d.ts +16 -0
  142. package/dist/types/overlays/hook-editor.d.ts +44 -0
  143. package/dist/types/overlays/hook-input.d.ts +20 -0
  144. package/dist/types/overlays/hook-selector.d.ts +71 -0
  145. package/dist/types/overlays/hub-frame.d.ts +77 -0
  146. package/dist/types/overlays/login-dialog.d.ts +43 -0
  147. package/dist/types/overlays/logout-account-selector.d.ts +15 -0
  148. package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
  149. package/dist/types/overlays/model-browser.d.ts +213 -0
  150. package/dist/types/overlays/model-hub.d.ts +72 -0
  151. package/dist/types/overlays/model-picker.d.ts +74 -0
  152. package/dist/types/overlays/model-selector.d.ts +52 -0
  153. package/dist/types/overlays/move-overlay.d.ts +39 -0
  154. package/dist/types/overlays/oauth-selector.d.ts +41 -0
  155. package/dist/types/overlays/omfg-panel.d.ts +20 -0
  156. package/dist/types/overlays/pause-screen.d.ts +43 -0
  157. package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
  158. package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
  159. package/dist/types/overlays/plan-toc.d.ts +41 -0
  160. package/dist/types/overlays/plugin-selector.d.ts +28 -0
  161. package/dist/types/overlays/plugin-settings.d.ts +180 -0
  162. package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
  163. package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
  164. package/dist/types/overlays/rewind-selector.d.ts +61 -0
  165. package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
  166. package/dist/types/overlays/session-account-selector.d.ts +24 -0
  167. package/dist/types/overlays/session-info-overlay.d.ts +17 -0
  168. package/dist/types/overlays/session-observer-registry.d.ts +77 -0
  169. package/dist/types/overlays/session-selector.d.ts +131 -0
  170. package/dist/types/overlays/settings-defs.d.ts +119 -0
  171. package/dist/types/overlays/settings-selector.d.ts +73 -0
  172. package/dist/types/overlays/show-images-selector.d.ts +11 -0
  173. package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
  174. package/dist/types/overlays/theme-selector.d.ts +12 -0
  175. package/dist/types/overlays/thinking-selector.d.ts +12 -0
  176. package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
  177. package/dist/types/overlays/tree-selector.d.ts +92 -0
  178. package/dist/types/overlays/usage-dashboard.d.ts +94 -0
  179. package/dist/types/overlays/usage-display.d.ts +5 -0
  180. package/dist/types/overlays/usage-row.d.ts +17 -0
  181. package/dist/types/prompt/attachment-chips.d.ts +19 -0
  182. package/dist/types/prompt/composer-attachments.d.ts +71 -0
  183. package/dist/types/prompt/composer-cache.d.ts +37 -0
  184. package/dist/types/prompt/composer.d.ts +159 -0
  185. package/dist/types/prompt/custom-editor.d.ts +261 -0
  186. package/dist/types/prompt/editor-top-gap.d.ts +18 -0
  187. package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
  188. package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
  189. package/dist/types/prompt/gradient-highlight.d.ts +32 -0
  190. package/dist/types/prompt/image-format.d.ts +4 -0
  191. package/dist/types/prompt/image-references.d.ts +26 -0
  192. package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
  193. package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
  194. package/dist/types/prompt/macos-spelling.d.ts +44 -0
  195. package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
  196. package/dist/types/prompt/magic-keywords.d.ts +26 -0
  197. package/dist/types/prompt/markdown-prose.d.ts +15 -0
  198. package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
  199. package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
  200. package/dist/types/prompt/orchestrate.d.ts +13 -0
  201. package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
  202. package/dist/types/prompt/queue-input.d.ts +8 -0
  203. package/dist/types/prompt/skill-tokens.d.ts +13 -0
  204. package/dist/types/prompt/tools-markdown.d.ts +12 -0
  205. package/dist/types/prompt/ultrathink.d.ts +13 -0
  206. package/dist/types/prompt/usage-amounts.d.ts +26 -0
  207. package/dist/types/prompt/video.d.ts +18 -0
  208. package/dist/types/prompt/welcome.d.ts +83 -0
  209. package/dist/types/prompt/workflow.d.ts +13 -0
  210. package/dist/types/render/code-cell.d.ts +51 -0
  211. package/dist/types/render/file-list.d.ts +22 -0
  212. package/dist/types/render/hyperlink.d.ts +80 -0
  213. package/dist/types/render/index.d.ts +16 -0
  214. package/dist/types/render/output-block.d.ts +54 -0
  215. package/dist/types/render/output-pane.d.ts +80 -0
  216. package/dist/types/render/render-utils.d.ts +312 -0
  217. package/dist/types/render/sixel.d.ts +21 -0
  218. package/dist/types/render/status-line.d.ts +23 -0
  219. package/dist/types/render/tool-card.d.ts +71 -0
  220. package/dist/types/render/tree-list.d.ts +29 -0
  221. package/dist/types/render/types.d.ts +17 -0
  222. package/dist/types/render/utils.d.ts +43 -0
  223. package/dist/types/render/width-aware-text.d.ts +23 -0
  224. package/dist/types/setup/lazy.d.ts +3 -0
  225. package/dist/types/setup/scenes/composer.d.ts +3 -0
  226. package/dist/types/setup/scenes/glyph.d.ts +3 -0
  227. package/dist/types/setup/scenes/model.d.ts +3 -0
  228. package/dist/types/setup/scenes/outro.d.ts +2 -0
  229. package/dist/types/setup/scenes/providers.d.ts +3 -0
  230. package/dist/types/setup/scenes/sign-in.d.ts +21 -0
  231. package/dist/types/setup/scenes/splash.d.ts +11 -0
  232. package/dist/types/setup/scenes/theme.d.ts +3 -0
  233. package/dist/types/setup/scenes/types.d.ts +110 -0
  234. package/dist/types/setup/scenes/web-search.d.ts +22 -0
  235. package/dist/types/setup/setup-version.d.ts +11 -0
  236. package/dist/types/setup/startup-splash.d.ts +9 -0
  237. package/dist/types/setup/wizard-overlay.d.ts +16 -0
  238. package/dist/types/setup/wizard.d.ts +25 -0
  239. package/dist/types/status-line/component.d.ts +182 -0
  240. package/dist/types/status-line/context-usage.d.ts +156 -0
  241. package/dist/types/status-line/footer.d.ts +30 -0
  242. package/dist/types/status-line/git-utils.d.ts +22 -0
  243. package/dist/types/status-line/host.d.ts +129 -0
  244. package/dist/types/status-line/index.d.ts +6 -0
  245. package/dist/types/status-line/loop.d.ts +18 -0
  246. package/dist/types/status-line/metrics.d.ts +21 -0
  247. package/dist/types/status-line/presets.d.ts +3 -0
  248. package/dist/types/status-line/schema.d.ts +15 -0
  249. package/dist/types/status-line/segments.d.ts +5 -0
  250. package/dist/types/status-line/separators.d.ts +3 -0
  251. package/dist/types/status-line/types.d.ts +236 -0
  252. package/dist/types/stdin-buffer.d.ts +4 -0
  253. package/dist/types/theme/color.d.ts +24 -0
  254. package/dist/types/theme/defaults/index.d.ts +9489 -0
  255. package/dist/types/theme/index.d.ts +4 -0
  256. package/dist/types/theme/loader.d.ts +23 -0
  257. package/dist/types/theme/mermaid-cache.d.ts +26 -0
  258. package/dist/types/theme/schema-validation.d.ts +3 -0
  259. package/dist/types/theme/schema.d.ts +25 -0
  260. package/dist/types/theme/session-color.d.ts +43 -0
  261. package/dist/types/theme/shimmer.d.ts +44 -0
  262. package/dist/types/theme/symbols.d.ts +34 -0
  263. package/dist/types/theme/theme-class.d.ts +299 -0
  264. package/dist/types/theme/theme.d.ts +111 -0
  265. package/dist/types/theme/tui-adapters.d.ts +30 -0
  266. package/dist/types/thinking.d.ts +165 -0
  267. package/dist/types/tools/agent-tree.d.ts +36 -0
  268. package/dist/types/tools/ask.d.ts +67 -0
  269. package/dist/types/tools/ast-edit.d.ts +53 -0
  270. package/dist/types/tools/ast-grep.d.ts +52 -0
  271. package/dist/types/tools/autoresearch.d.ts +156 -0
  272. package/dist/types/tools/bash-interactive.d.ts +28 -0
  273. package/dist/types/tools/bash.d.ts +103 -0
  274. package/dist/types/tools/conflict-detect.d.ts +43 -0
  275. package/dist/types/tools/debug.d.ts +60 -0
  276. package/dist/types/tools/default-renderer.d.ts +24 -0
  277. package/dist/types/tools/edit.d.ts +158 -0
  278. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
  279. package/dist/types/tools/eval-format/index.d.ts +5 -0
  280. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  281. package/dist/types/tools/eval-format/python.d.ts +2 -0
  282. package/dist/types/tools/eval.d.ts +94 -0
  283. package/dist/types/tools/fetch.d.ts +42 -0
  284. package/dist/types/tools/gh-format.d.ts +8 -0
  285. package/dist/types/tools/github.d.ts +144 -0
  286. package/dist/types/tools/glob.d.ts +44 -0
  287. package/dist/types/tools/goal.d.ts +42 -0
  288. package/dist/types/tools/grep.d.ts +64 -0
  289. package/dist/types/tools/grouped-file-output.d.ts +71 -0
  290. package/dist/types/tools/hashline-format.d.ts +28 -0
  291. package/dist/types/tools/hub.d.ts +340 -0
  292. package/dist/types/tools/index.d.ts +4 -0
  293. package/dist/types/tools/json-tree.d.ts +59 -0
  294. package/dist/types/tools/line-ranges.d.ts +29 -0
  295. package/dist/types/tools/list-limit.d.ts +15 -0
  296. package/dist/types/tools/lsp.d.ts +72 -0
  297. package/dist/types/tools/match-line-format.d.ts +11 -0
  298. package/dist/types/tools/mcp.d.ts +81 -0
  299. package/dist/types/tools/memory.d.ts +67 -0
  300. package/dist/types/tools/output-meta.d.ts +151 -0
  301. package/dist/types/tools/read.d.ts +139 -0
  302. package/dist/types/tools/renderer.d.ts +98 -0
  303. package/dist/types/tools/report-tool-issue.d.ts +8 -0
  304. package/dist/types/tools/resolve.d.ts +47 -0
  305. package/dist/types/tools/streaming-output.d.ts +282 -0
  306. package/dist/types/tools/subprocess.d.ts +24 -0
  307. package/dist/types/tools/task-repair-args.d.ts +53 -0
  308. package/dist/types/tools/task-yield-assembly.d.ts +20 -0
  309. package/dist/types/tools/task.d.ts +425 -0
  310. package/dist/types/tools/terminal-output.d.ts +8 -0
  311. package/dist/types/tools/think.d.ts +13 -0
  312. package/dist/types/tools/todo.d.ts +118 -0
  313. package/dist/types/tools/tool-errors.d.ts +7 -0
  314. package/dist/types/tools/vibe.d.ts +120 -0
  315. package/dist/types/tools/web-search.d.ts +195 -0
  316. package/dist/types/tools/write.d.ts +76 -0
  317. package/dist/types/tools/xd-url.d.ts +13 -0
  318. package/dist/types/tools/xdev.d.ts +52 -0
  319. package/dist/types/tui.d.ts +18 -0
  320. package/dist/types/utils.d.ts +2 -0
  321. package/package.json +37 -3
  322. package/src/app-keybindings.ts +760 -0
  323. package/src/apps/autoresearch-dashboard.ts +500 -0
  324. package/src/apps/autoresearch-data.ts +64 -0
  325. package/src/apps/cleanse-board.ts +341 -0
  326. package/src/apps/cleanse-picker.ts +57 -0
  327. package/src/apps/debug/log-formatting.ts +53 -0
  328. package/src/apps/debug/log-viewer.ts +868 -0
  329. package/src/apps/debug/protocol-probe.ts +267 -0
  330. package/src/apps/debug/raw-sse-buffer.ts +449 -0
  331. package/src/apps/debug/raw-sse.ts +282 -0
  332. package/src/apps/debug/terminal-info.ts +115 -0
  333. package/src/apps/debug/viewer-frame.ts +174 -0
  334. package/src/apps/git/avatar.ts +52 -0
  335. package/src/apps/git/colors.ts +109 -0
  336. package/src/apps/git/diff-pane.ts +1512 -0
  337. package/src/apps/git/git-tui.ts +889 -0
  338. package/src/apps/git/sidebar.ts +1204 -0
  339. package/src/apps/git/state.ts +113 -0
  340. package/src/apps/if-bench-board.ts +324 -0
  341. package/src/apps/live-visualizer.ts +238 -0
  342. package/src/apps/ps-data.ts +116 -0
  343. package/src/apps/ps-top.ts +422 -0
  344. package/src/apps/session-picker.ts +95 -0
  345. package/src/apps/setup-model-picker.ts +21 -0
  346. package/src/apps/standalone-picker.ts +111 -0
  347. package/src/autocomplete.ts +9 -13
  348. package/src/chat/advisor-message.ts +175 -0
  349. package/src/chat/assistant-message.ts +1214 -0
  350. package/src/chat/background-tan-message.ts +35 -0
  351. package/src/chat/bash-execution.ts +394 -0
  352. package/src/chat/cache-invalidation-marker.ts +94 -0
  353. package/src/chat/chat-transcript-builder.ts +593 -0
  354. package/src/chat/collab-prompt-message.ts +34 -0
  355. package/src/chat/compaction-summary-message.ts +261 -0
  356. package/src/chat/custom-message.ts +23 -0
  357. package/src/chat/display-preferences.ts +24 -0
  358. package/src/chat/eval-execution.ts +172 -0
  359. package/src/chat/execution-shared.ts +113 -0
  360. package/src/chat/extension-types.ts +46 -0
  361. package/src/chat/hook-message.ts +20 -0
  362. package/src/chat/image-loading.ts +122 -0
  363. package/src/chat/late-diagnostics-message.ts +92 -0
  364. package/src/chat/messages.ts +305 -0
  365. package/src/chat/reaction.ts +65 -0
  366. package/src/chat/read-target.ts +7 -0
  367. package/src/chat/read-tool-group.ts +873 -0
  368. package/src/chat/served-model-marker.ts +91 -0
  369. package/src/chat/skill-message.ts +220 -0
  370. package/src/chat/skill-title-input.ts +41 -0
  371. package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
  372. package/src/chat/thinking-display.ts +312 -0
  373. package/src/chat/todo-reminder.ts +33 -0
  374. package/src/chat/tool-execution.ts +1395 -0
  375. package/src/chat/transcript-browser.ts +268 -0
  376. package/src/chat/transcript-entry.ts +96 -0
  377. package/src/chat/transcript-outline.ts +260 -0
  378. package/src/chat/transcript-render-helpers.ts +315 -0
  379. package/src/chat/ttsr-notification.ts +123 -0
  380. package/src/chat/user-message.ts +287 -0
  381. package/src/chrome/chat-block.ts +110 -0
  382. package/src/chrome/collab-qrcode.ts +74 -0
  383. package/src/chrome/context-thresholds.ts +85 -0
  384. package/src/chrome/countdown-timer.ts +74 -0
  385. package/src/chrome/diff.ts +266 -0
  386. package/src/chrome/dynamic-border.ts +36 -0
  387. package/src/chrome/error-block.ts +63 -0
  388. package/src/chrome/form-theme.ts +10 -0
  389. package/src/chrome/format.ts +66 -0
  390. package/src/chrome/index.ts +21 -0
  391. package/src/chrome/keybinding-hints.ts +55 -0
  392. package/src/chrome/live-board.ts +123 -0
  393. package/src/chrome/local-date.ts +20 -0
  394. package/src/chrome/message-divider.ts +51 -0
  395. package/src/chrome/message-frame.ts +164 -0
  396. package/src/chrome/message-notice.ts +103 -0
  397. package/src/chrome/overlay-box.ts +301 -0
  398. package/src/chrome/qrcode.ts +539 -0
  399. package/src/chrome/segment-track.ts +86 -0
  400. package/src/chrome/select-list-mouse-routing.ts +11 -0
  401. package/src/chrome/selector-helpers.ts +119 -0
  402. package/src/chrome/shared.ts +27 -0
  403. package/src/chrome/streaming-panel.ts +72 -0
  404. package/src/chrome/tool-activity.ts +44 -0
  405. package/src/chrome/transcript-container.ts +902 -0
  406. package/src/chrome/transcript-status.ts +27 -0
  407. package/src/chrome/visual-truncate.ts +70 -0
  408. package/src/components/disclosure.ts +235 -0
  409. package/src/components/editor.ts +10 -12
  410. package/src/components/form.ts +566 -0
  411. package/src/components/input.ts +6 -27
  412. package/src/components/key-value-list.ts +88 -0
  413. package/src/components/layout/geometry.ts +202 -0
  414. package/src/components/layout/row.ts +283 -0
  415. package/src/components/layout/split-pane.ts +349 -0
  416. package/src/components/layout/stack.ts +261 -0
  417. package/src/components/markdown.ts +16 -12
  418. package/src/components/menu-selection.ts +248 -0
  419. package/src/components/metric.ts +132 -0
  420. package/src/components/progress-bar.ts +140 -0
  421. package/src/components/scroll-view.ts +372 -70
  422. package/src/components/scroll-viewport.ts +154 -0
  423. package/src/components/section.ts +77 -0
  424. package/src/components/select-list.ts +238 -159
  425. package/src/components/settings-list.ts +103 -80
  426. package/src/components/table.ts +127 -0
  427. package/src/components/tree-view.ts +481 -0
  428. package/src/components/wizard-step.ts +229 -0
  429. package/src/deccara.ts +1 -3
  430. package/src/hotkeys-markdown.ts +100 -0
  431. package/src/index.ts +18 -0
  432. package/src/keybinding-matchers.ts +99 -0
  433. package/src/keys.ts +2 -2
  434. package/src/lang-from-path.ts +252 -0
  435. package/src/overlays/advisor-config.ts +761 -0
  436. package/src/overlays/agent-activity.ts +113 -0
  437. package/src/overlays/agent-hub-projection.ts +250 -0
  438. package/src/overlays/agent-hub-renderer.ts +281 -0
  439. package/src/overlays/agent-hub-types.ts +74 -0
  440. package/src/overlays/agent-hub.ts +1562 -0
  441. package/src/overlays/agent-transcript-viewer.ts +669 -0
  442. package/src/overlays/agents-hub.ts +1203 -0
  443. package/src/overlays/ask-dialog.ts +1202 -0
  444. package/src/overlays/bordered-loader.ts +41 -0
  445. package/src/overlays/btw-history-panel.ts +591 -0
  446. package/src/overlays/btw-history.ts +33 -0
  447. package/src/overlays/btw-panel.ts +167 -0
  448. package/src/overlays/cleanse-panel.ts +167 -0
  449. package/src/overlays/codex-reset-fireworks.ts +375 -0
  450. package/src/overlays/composer-shape-preview.ts +137 -0
  451. package/src/overlays/composer-shape-registry.ts +82 -0
  452. package/src/overlays/copy-selector.ts +618 -0
  453. package/src/overlays/copy-targets.ts +210 -0
  454. package/src/overlays/error-banner.ts +39 -0
  455. package/src/overlays/extensions/display-text.ts +39 -0
  456. package/src/overlays/extensions/extension-dashboard.ts +645 -0
  457. package/src/overlays/extensions/extension-list.ts +667 -0
  458. package/src/overlays/extensions/inspector-model.ts +484 -0
  459. package/src/overlays/extensions/inspector-panel.ts +669 -0
  460. package/src/overlays/extensions/live-tool-session.ts +67 -0
  461. package/src/overlays/extensions/mcp-runtime.ts +289 -0
  462. package/src/overlays/extensions/state-manager.ts +337 -0
  463. package/src/overlays/extensions/types.ts +208 -0
  464. package/src/overlays/history-search.ts +268 -0
  465. package/src/overlays/hook-editor.ts +272 -0
  466. package/src/overlays/hook-input.ts +84 -0
  467. package/src/overlays/hook-selector.ts +635 -0
  468. package/src/overlays/hub-frame.ts +262 -0
  469. package/src/overlays/login-dialog.ts +231 -0
  470. package/src/overlays/logout-account-selector.ts +115 -0
  471. package/src/overlays/mcp-add-wizard.ts +1436 -0
  472. package/src/overlays/model-browser.ts +1176 -0
  473. package/src/overlays/model-hub.ts +2279 -0
  474. package/src/overlays/model-picker.ts +285 -0
  475. package/src/overlays/model-selector.ts +107 -0
  476. package/src/overlays/move-overlay.ts +197 -0
  477. package/src/overlays/oauth-selector.ts +446 -0
  478. package/src/overlays/omfg-panel.ts +136 -0
  479. package/src/overlays/pause-screen.ts +198 -0
  480. package/src/overlays/plan-review-overlay.ts +1227 -0
  481. package/src/overlays/plan-save-overlay.ts +74 -0
  482. package/src/overlays/plan-toc.ts +139 -0
  483. package/src/overlays/plugin-selector.ts +97 -0
  484. package/src/overlays/plugin-settings.ts +839 -0
  485. package/src/overlays/queue-mode-selector.ts +55 -0
  486. package/src/overlays/reset-usage-selector.ts +170 -0
  487. package/src/overlays/rewind-selector.ts +499 -0
  488. package/src/overlays/running-subagent-badge.ts +19 -0
  489. package/src/overlays/session-account-selector.ts +68 -0
  490. package/src/overlays/session-info-overlay.ts +96 -0
  491. package/src/overlays/session-observer-registry.ts +289 -0
  492. package/src/overlays/session-selector.ts +1137 -0
  493. package/src/overlays/settings-defs.ts +315 -0
  494. package/src/overlays/settings-selector.ts +1415 -0
  495. package/src/overlays/show-images-selector.ts +44 -0
  496. package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
  497. package/src/overlays/snapcompact-shape-preview.ts +192 -0
  498. package/src/overlays/theme-selector.ts +62 -0
  499. package/src/overlays/thinking-selector.ts +51 -0
  500. package/src/overlays/tiny-title-download-progress.ts +102 -0
  501. package/src/overlays/tree-selector.ts +1121 -0
  502. package/src/overlays/usage-dashboard.ts +629 -0
  503. package/src/overlays/usage-display.ts +45 -0
  504. package/src/overlays/usage-row.ts +118 -0
  505. package/src/prompt/attachment-chips.ts +201 -0
  506. package/src/prompt/composer-attachments.ts +280 -0
  507. package/src/prompt/composer-cache.ts +304 -0
  508. package/src/prompt/composer.ts +934 -0
  509. package/src/prompt/custom-editor.ts +1392 -0
  510. package/src/prompt/data/emojis.json +1 -0
  511. package/src/prompt/editor-top-gap.ts +28 -0
  512. package/src/prompt/emoji-autocomplete.ts +285 -0
  513. package/src/prompt/github-ref-autocomplete.ts +75 -0
  514. package/src/prompt/gradient-highlight.ts +101 -0
  515. package/src/prompt/image-format.ts +28 -0
  516. package/src/prompt/image-references.ts +102 -0
  517. package/src/prompt/interactive-context-helpers.ts +126 -0
  518. package/src/prompt/internal-url-autocomplete.ts +155 -0
  519. package/src/prompt/macos-spelling.ts +456 -0
  520. package/src/prompt/magic-keyword-boundary.ts +23 -0
  521. package/src/prompt/magic-keywords.ts +42 -0
  522. package/src/prompt/markdown-prose.ts +245 -0
  523. package/src/prompt/model-mention-autocomplete.ts +94 -0
  524. package/src/prompt/model-mention-syntax.ts +41 -0
  525. package/src/prompt/orchestrate.ts +26 -0
  526. package/src/prompt/prompt-action-autocomplete.ts +337 -0
  527. package/src/prompt/queue-input.ts +132 -0
  528. package/src/prompt/skill-tokens.ts +36 -0
  529. package/src/prompt/tips.txt +27 -0
  530. package/src/prompt/tools-markdown.ts +29 -0
  531. package/src/prompt/ultrathink.ts +26 -0
  532. package/src/prompt/usage-amounts.ts +99 -0
  533. package/src/prompt/video.ts +47 -0
  534. package/src/prompt/welcome.ts +605 -0
  535. package/src/prompt/workflow.ts +26 -0
  536. package/src/render/code-cell.ts +244 -0
  537. package/src/render/file-list.ts +58 -0
  538. package/src/render/hyperlink.ts +192 -0
  539. package/src/render/index.ts +17 -0
  540. package/src/render/output-block.ts +267 -0
  541. package/src/render/output-pane.ts +299 -0
  542. package/src/render/render-utils.ts +1182 -0
  543. package/src/render/sixel.ts +69 -0
  544. package/src/render/status-line.ts +55 -0
  545. package/src/render/tool-card.ts +272 -0
  546. package/src/render/tree-list.ts +188 -0
  547. package/src/render/types.ts +19 -0
  548. package/src/render/utils.ts +109 -0
  549. package/src/render/width-aware-text.ts +60 -0
  550. package/src/setup/lazy.ts +17 -0
  551. package/src/setup/scenes/composer.ts +127 -0
  552. package/src/setup/scenes/glyph.ts +120 -0
  553. package/src/setup/scenes/model.ts +138 -0
  554. package/src/setup/scenes/outro.ts +24 -0
  555. package/src/setup/scenes/providers.ts +107 -0
  556. package/src/setup/scenes/sign-in.ts +373 -0
  557. package/src/setup/scenes/splash.ts +192 -0
  558. package/src/setup/scenes/theme.ts +343 -0
  559. package/src/setup/scenes/types.ts +111 -0
  560. package/src/setup/scenes/web-search.ts +165 -0
  561. package/src/setup/setup-version.ts +11 -0
  562. package/src/setup/startup-splash.ts +107 -0
  563. package/src/setup/wizard-overlay.ts +319 -0
  564. package/src/setup/wizard.ts +101 -0
  565. package/src/status-line/component.ts +3003 -0
  566. package/src/status-line/context-usage.ts +688 -0
  567. package/src/status-line/footer.ts +323 -0
  568. package/src/status-line/git-utils.ts +42 -0
  569. package/src/status-line/host.ts +97 -0
  570. package/src/status-line/index.ts +6 -0
  571. package/src/status-line/loop.ts +28 -0
  572. package/src/status-line/metrics.ts +94 -0
  573. package/src/status-line/presets.ts +107 -0
  574. package/src/status-line/schema.ts +57 -0
  575. package/src/status-line/segments.ts +934 -0
  576. package/src/status-line/separators.ts +55 -0
  577. package/src/status-line/types.ts +227 -0
  578. package/src/stdin-buffer.ts +4 -2
  579. package/src/theme/color.ts +95 -0
  580. package/src/theme/dark.json +95 -0
  581. package/src/theme/defaults/alabaster.json +93 -0
  582. package/src/theme/defaults/amethyst.json +96 -0
  583. package/src/theme/defaults/anthracite.json +93 -0
  584. package/src/theme/defaults/basalt.json +91 -0
  585. package/src/theme/defaults/birch.json +95 -0
  586. package/src/theme/defaults/dark-abyss.json +91 -0
  587. package/src/theme/defaults/dark-arctic.json +104 -0
  588. package/src/theme/defaults/dark-aurora.json +95 -0
  589. package/src/theme/defaults/dark-catppuccin.json +107 -0
  590. package/src/theme/defaults/dark-cavern.json +91 -0
  591. package/src/theme/defaults/dark-celestial.json +106 -0
  592. package/src/theme/defaults/dark-copper.json +95 -0
  593. package/src/theme/defaults/dark-cosmos.json +90 -0
  594. package/src/theme/defaults/dark-cyberpunk.json +102 -0
  595. package/src/theme/defaults/dark-dracula.json +98 -0
  596. package/src/theme/defaults/dark-eclipse.json +91 -0
  597. package/src/theme/defaults/dark-ember.json +95 -0
  598. package/src/theme/defaults/dark-equinox.json +90 -0
  599. package/src/theme/defaults/dark-forest.json +96 -0
  600. package/src/theme/defaults/dark-github.json +105 -0
  601. package/src/theme/defaults/dark-gruvbox.json +112 -0
  602. package/src/theme/defaults/dark-lavender.json +95 -0
  603. package/src/theme/defaults/dark-lunar.json +89 -0
  604. package/src/theme/defaults/dark-midnight.json +95 -0
  605. package/src/theme/defaults/dark-monochrome.json +94 -0
  606. package/src/theme/defaults/dark-monokai.json +98 -0
  607. package/src/theme/defaults/dark-nebula.json +90 -0
  608. package/src/theme/defaults/dark-nord.json +98 -0
  609. package/src/theme/defaults/dark-ocean.json +101 -0
  610. package/src/theme/defaults/dark-one.json +100 -0
  611. package/src/theme/defaults/dark-poimandres.json +115 -0
  612. package/src/theme/defaults/dark-rainforest.json +91 -0
  613. package/src/theme/defaults/dark-reef.json +91 -0
  614. package/src/theme/defaults/dark-retro.json +92 -0
  615. package/src/theme/defaults/dark-rose-pine.json +96 -0
  616. package/src/theme/defaults/dark-sakura.json +95 -0
  617. package/src/theme/defaults/dark-slate.json +95 -0
  618. package/src/theme/defaults/dark-solarized.json +97 -0
  619. package/src/theme/defaults/dark-solstice.json +90 -0
  620. package/src/theme/defaults/dark-starfall.json +91 -0
  621. package/src/theme/defaults/dark-sunset.json +99 -0
  622. package/src/theme/defaults/dark-swamp.json +90 -0
  623. package/src/theme/defaults/dark-synthwave.json +103 -0
  624. package/src/theme/defaults/dark-taiga.json +91 -0
  625. package/src/theme/defaults/dark-terminal.json +95 -0
  626. package/src/theme/defaults/dark-tokyo-night.json +101 -0
  627. package/src/theme/defaults/dark-tundra.json +91 -0
  628. package/src/theme/defaults/dark-twilight.json +91 -0
  629. package/src/theme/defaults/dark-volcanic.json +91 -0
  630. package/src/theme/defaults/graphite.json +92 -0
  631. package/src/theme/defaults/index.ts +201 -0
  632. package/src/theme/defaults/light-arctic.json +107 -0
  633. package/src/theme/defaults/light-aurora-day.json +91 -0
  634. package/src/theme/defaults/light-canyon.json +91 -0
  635. package/src/theme/defaults/light-catppuccin.json +106 -0
  636. package/src/theme/defaults/light-cirrus.json +90 -0
  637. package/src/theme/defaults/light-coral.json +95 -0
  638. package/src/theme/defaults/light-cyberpunk.json +96 -0
  639. package/src/theme/defaults/light-dawn.json +90 -0
  640. package/src/theme/defaults/light-dunes.json +91 -0
  641. package/src/theme/defaults/light-eucalyptus.json +95 -0
  642. package/src/theme/defaults/light-forest.json +100 -0
  643. package/src/theme/defaults/light-frost.json +95 -0
  644. package/src/theme/defaults/light-github.json +115 -0
  645. package/src/theme/defaults/light-glacier.json +91 -0
  646. package/src/theme/defaults/light-gruvbox.json +108 -0
  647. package/src/theme/defaults/light-haze.json +90 -0
  648. package/src/theme/defaults/light-honeycomb.json +95 -0
  649. package/src/theme/defaults/light-lagoon.json +91 -0
  650. package/src/theme/defaults/light-lavender.json +95 -0
  651. package/src/theme/defaults/light-meadow.json +91 -0
  652. package/src/theme/defaults/light-mint.json +95 -0
  653. package/src/theme/defaults/light-monochrome.json +101 -0
  654. package/src/theme/defaults/light-ocean.json +99 -0
  655. package/src/theme/defaults/light-one.json +99 -0
  656. package/src/theme/defaults/light-opal.json +91 -0
  657. package/src/theme/defaults/light-orchard.json +91 -0
  658. package/src/theme/defaults/light-paper.json +95 -0
  659. package/src/theme/defaults/light-poimandres.json +115 -0
  660. package/src/theme/defaults/light-prism.json +90 -0
  661. package/src/theme/defaults/light-retro.json +98 -0
  662. package/src/theme/defaults/light-sand.json +95 -0
  663. package/src/theme/defaults/light-savanna.json +91 -0
  664. package/src/theme/defaults/light-solarized.json +102 -0
  665. package/src/theme/defaults/light-soleil.json +90 -0
  666. package/src/theme/defaults/light-sunset.json +99 -0
  667. package/src/theme/defaults/light-synthwave.json +98 -0
  668. package/src/theme/defaults/light-tokyo-night.json +111 -0
  669. package/src/theme/defaults/light-wetland.json +91 -0
  670. package/src/theme/defaults/light-zenith.json +89 -0
  671. package/src/theme/defaults/limestone.json +94 -0
  672. package/src/theme/defaults/mahogany.json +97 -0
  673. package/src/theme/defaults/marble.json +93 -0
  674. package/src/theme/defaults/obsidian.json +91 -0
  675. package/src/theme/defaults/onyx.json +91 -0
  676. package/src/theme/defaults/pearl.json +93 -0
  677. package/src/theme/defaults/porcelain.json +91 -0
  678. package/src/theme/defaults/quartz.json +96 -0
  679. package/src/theme/defaults/sandstone.json +95 -0
  680. package/src/theme/defaults/titanium.json +90 -0
  681. package/src/theme/index.ts +5 -0
  682. package/src/theme/light.json +93 -0
  683. package/src/theme/loader.ts +200 -0
  684. package/src/theme/mermaid-cache.ts +92 -0
  685. package/src/theme/schema-validation.ts +119 -0
  686. package/src/theme/schema.ts +168 -0
  687. package/src/theme/session-color.ts +253 -0
  688. package/src/theme/shimmer.ts +307 -0
  689. package/src/theme/symbols.ts +1433 -0
  690. package/src/theme/theme-class.ts +787 -0
  691. package/src/theme/theme-schema.json +463 -0
  692. package/src/theme/theme.ts +810 -0
  693. package/src/theme/tui-adapters.ts +339 -0
  694. package/src/thinking.ts +380 -0
  695. package/src/tools/agent-tree.ts +128 -0
  696. package/src/tools/ask.ts +482 -0
  697. package/src/tools/ast-edit.ts +210 -0
  698. package/src/tools/ast-grep.ts +171 -0
  699. package/src/tools/autoresearch.ts +287 -0
  700. package/src/tools/bash-interactive.ts +295 -0
  701. package/src/tools/bash.ts +468 -0
  702. package/src/tools/conflict-detect.ts +77 -0
  703. package/src/tools/debug.ts +167 -0
  704. package/src/tools/default-renderer.ts +172 -0
  705. package/src/tools/edit.ts +1258 -0
  706. package/src/tools/eval-format/code-mode-declarations.ts +61 -0
  707. package/src/tools/eval-format/index.ts +16 -0
  708. package/src/tools/eval-format/javascript.ts +952 -0
  709. package/src/tools/eval-format/python.ts +544 -0
  710. package/src/tools/eval.ts +835 -0
  711. package/src/tools/fetch.ts +226 -0
  712. package/src/tools/gh-format.ts +18 -0
  713. package/src/tools/github.ts +686 -0
  714. package/src/tools/glob.ts +213 -0
  715. package/src/tools/goal.ts +153 -0
  716. package/src/tools/grep.ts +385 -0
  717. package/src/tools/grouped-file-output.ts +211 -0
  718. package/src/tools/hashline-format.ts +59 -0
  719. package/src/tools/hub.ts +1488 -0
  720. package/src/tools/index.ts +71 -0
  721. package/src/tools/json-tree.ts +388 -0
  722. package/src/tools/line-ranges.ts +98 -0
  723. package/src/tools/list-limit.ts +43 -0
  724. package/src/tools/lsp.ts +702 -0
  725. package/src/tools/match-line-format.ts +20 -0
  726. package/src/tools/mcp.ts +309 -0
  727. package/src/tools/memory.ts +220 -0
  728. package/src/tools/output-meta.ts +367 -0
  729. package/src/tools/read.ts +628 -0
  730. package/src/tools/renderer.ts +107 -0
  731. package/src/tools/report-tool-issue.ts +13 -0
  732. package/src/tools/resolve.ts +139 -0
  733. package/src/tools/streaming-output.ts +1576 -0
  734. package/src/tools/subprocess.ts +39 -0
  735. package/src/tools/task-repair-args.ts +118 -0
  736. package/src/tools/task-yield-assembly.ts +147 -0
  737. package/src/tools/task.ts +2110 -0
  738. package/src/tools/terminal-output.ts +153 -0
  739. package/src/tools/think.ts +28 -0
  740. package/src/tools/todo.ts +580 -0
  741. package/src/tools/tool-errors.ts +15 -0
  742. package/src/tools/vibe.ts +420 -0
  743. package/src/tools/web-search.ts +409 -0
  744. package/src/tools/write.ts +511 -0
  745. package/src/tools/xd-url.ts +28 -0
  746. package/src/tools/xdev.ts +201 -0
  747. package/src/tui.ts +57 -4
  748. package/src/utils.ts +8 -0
@@ -0,0 +1,1214 @@
1
+ import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
2
+ import { type Component, Container } from "../tui";
3
+ import { Image, type ImageBudget } from "../components/image";
4
+ import { ImageProtocol, TERMINAL } from "../terminal-capabilities";
5
+ import { Markdown, type MarkdownTheme } from "../components/markdown";
6
+ import { Spacer } from "../components/spacer";
7
+ import { Text } from "../components/text";
8
+ import { formatNumber } from "@oh-my-pi/pi-utils";
9
+ import chalk from "@oh-my-pi/pi-utils/chalk";
10
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
11
+ import type { AssistantThinkingRenderer } from "./extension-types";
12
+ import { ensureThemeSync, getMarkdownTheme, theme } from "../theme";
13
+ import { EMPTY_LINK_TARGETS, resolveImageOptions } from "../render/render-utils";
14
+ import { WidthAwareText } from "../render";
15
+ import { convertImageToPng } from "./image-loading";
16
+ import { canonicalizeMessage, formatThinkingForDisplay, hasDisplayableThinking } from "./thinking-display";
17
+ import { resolveAssistantErrorPresentation } from "./transcript-render-helpers";
18
+ import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cache-invalidation-marker";
19
+ import { formatErrorBlock } from "../chrome/error-block";
20
+ import { type ServedModelMismatch, ServedModelMarkerComponent } from "./served-model-marker";
21
+ import { isReactionTarget, type ReactionSplit, type ReactionTarget, splitReaction } from "./reaction";
22
+ import { isRowPrefix, type TranscriptStableRow, trimBlankEdges } from "../chrome/transcript-container";
23
+
24
+ /**
25
+ * Max wrapped rows of a turn-ending provider error rendered inline in the
26
+ * transcript. Bounds pathological error bodies — e.g. a proxy 502 whose body
27
+ * is a full HTML page — so they can't flood the scrollback, while a long
28
+ * single-line body wraps to the width instead of being cut at a fixed column.
29
+ * Full text is still kept in the persisted session.
30
+ */
31
+ const MAX_TRANSCRIPT_ERROR_ROWS = 8;
32
+ const EMPTY_STABLE_RENDER: readonly string[] = [];
33
+
34
+ type ThinkingContentBlock = Extract<AssistantMessage["content"][number], { type: "thinking" }>;
35
+ type DisplayThinkingContentBlock = ThinkingContentBlock & { rawThinking?: string };
36
+ type StablePart = { kind: "thinking" | "text"; text: string } | { kind: "spacer" };
37
+
38
+ /**
39
+ * One published prefix of the block's finished content. Later snapshots extend
40
+ * earlier ones part-wise (only the final part may grow), so rendered stable
41
+ * rows only ever gain a suffix — the append-only transcript contract that lets
42
+ * them retire into native scrollback mid-stream.
43
+ */
44
+ interface StableSnapshot {
45
+ // Earlier parts are immutable; only the final part needs a historical offset.
46
+ readonly partCount: number;
47
+ readonly lastTextLength: number;
48
+ }
49
+
50
+ function isSnapshotExtension(previous: readonly StablePart[], current: readonly StablePart[]): boolean {
51
+ if (previous.length > current.length) return false;
52
+ for (let index = 0; index < previous.length; index++) {
53
+ const before = previous[index]!;
54
+ const after = current[index]!;
55
+ if (before.kind !== after.kind) return false;
56
+ if (before.kind === "spacer" || after.kind === "spacer") continue;
57
+ const isLast = index === previous.length - 1;
58
+ if (isLast ? !after.text.startsWith(before.text) : after.text !== before.text) return false;
59
+ }
60
+ return true;
61
+ }
62
+
63
+ function resolveThinkingDisplay(block: ThinkingContentBlock, proseOnly: boolean): { text: string; visible: boolean } {
64
+ const rawThinking = (block as DisplayThinkingContentBlock).rawThinking;
65
+ // When rawThinking is set, `block.thinking` is already the formatted display
66
+ // text that buildDisplayMessage produced (then revealed/sliced by the
67
+ // streaming controller) — re-running the formatter would double-process it,
68
+ // and the growing revealed slice would never hit the per-tick memo. Only
69
+ // format raw (non-display) thinking blocks.
70
+ const formatted = rawThinking !== undefined ? block.thinking : formatThinkingForDisplay(block.thinking, proseOnly);
71
+ return {
72
+ text: formatted.trim(),
73
+ visible: hasDisplayableThinking(rawThinking ?? block.thinking, formatted),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Frames for the streaming "thinking" pulse rendered in place of a hidden
79
+ * thinking block while the model is still producing it. A single fixed-width
80
+ * starburst cycles through facets (✻ ✼ ❉ ❊ ✺ ✹ ✸ ✶) so the indicator animates
81
+ * in place without shifting the line or the trailing speed badge. The dwell per
82
+ * frame eases between {@link THINKING_DOTS_FRAME_MS_MIN} and
83
+ * {@link THINKING_DOTS_FRAME_MS_MAX} across each revolution (see
84
+ * {@link AssistantMessageComponent.thinkingDotsFrameDelay}).
85
+ */
86
+ const THINKING_DOTS_FRAMES = ["✻", "✼", "❉", "❊", "✺", "✹", "✸", "✶"] as const;
87
+ /**
88
+ * Pulse cadence bounds (ms). Each frame's dwell eases between these on a
89
+ * raised-cosine "breath" — quickest at the cycle start, slowest at its midpoint —
90
+ * so the starburst accelerates and slows instead of ticking at one fixed rate.
91
+ * Mean ≈ 150ms, snappier than the previous flat 320ms.
92
+ */
93
+ const THINKING_DOTS_FRAME_MS_MIN = 70;
94
+ const THINKING_DOTS_FRAME_MS_MAX = 230;
95
+
96
+ /** Rolling window (ms) over which streaming-rate observations are averaged. */
97
+ const SPEED_WINDOW_MS = 3000;
98
+ /** Color/clamp ceiling: a rate at or above this maps to the full accent color. */
99
+ const SPEED_MAX = 200;
100
+
101
+ /**
102
+ * Session-wide streaming-speed gauge. Only one thinking indicator animates at a
103
+ * time, so a single shared instance accumulates instantaneous tok/s observations
104
+ * and reports their windowed average — smoothing the jumpy per-delta numbers.
105
+ * Each thinking block resets the gauge on its first live sample (see
106
+ * {@link AssistantMessageComponent.updateContent}) so the average reflects only
107
+ * the active block, never a previous turn's trailing rate. Components feed it
108
+ * deltas (not cumulative totals), so a fresh turn restarting its token count at
109
+ * zero never produces a spike.
110
+ */
111
+ class SpeedTracker {
112
+ #observations: Array<{ time: number; rate: number }> = [];
113
+
114
+ #prune(now: number): void {
115
+ const threshold = now - SPEED_WINDOW_MS;
116
+ while (this.#observations.length > 0 && this.#observations[0]!.time < threshold) {
117
+ this.#observations.shift();
118
+ }
119
+ }
120
+
121
+ /** Record one instantaneous tok/s reading, clamped to {@link SPEED_MAX} so a
122
+ * single oversized delta (e.g. a buffered reflow tick) can't poison the
123
+ * windowed average. Non-finite/negative rates ignored. */
124
+ observe(rate: number, now = performance.now()): void {
125
+ if (!Number.isFinite(rate) || rate < 0) return;
126
+ this.#observations.push({ time: now, rate: Math.min(rate, SPEED_MAX) });
127
+ this.#prune(now);
128
+ }
129
+
130
+ /** Windowed-average tok/s; 0 once observations age out of the window. */
131
+ getSpeed(now = performance.now()): number {
132
+ this.#prune(now);
133
+ if (this.#observations.length === 0) return 0;
134
+ let sum = 0;
135
+ for (const o of this.#observations) sum += o.rate;
136
+ return sum / this.#observations.length;
137
+ }
138
+
139
+ reset(): void {
140
+ this.#observations = [];
141
+ }
142
+ }
143
+
144
+ /** One gauge for the whole session — see {@link SpeedTracker}. */
145
+ const sharedSpeedTracker = new SpeedTracker();
146
+
147
+ /** Test-only: clear the shared gauge so observations don't leak across cases. */
148
+ export function resetThinkingSpeedTracker(): void {
149
+ sharedSpeedTracker.reset();
150
+ }
151
+
152
+ /**
153
+ * Linear-interpolate two `#rrggbb` colors in sRGB space. `t` clamps to [0,1]:
154
+ * `t = 0` → `from`, `t = 1` → `to`. Drives the streaming speed badge, fading
155
+ * from a dim gray toward the theme accent as tok/s rises.
156
+ */
157
+ function lerpHex(from: string, to: string, t: number): string {
158
+ const k = t < 0 ? 0 : t > 1 ? 1 : t;
159
+ const fr = Number.parseInt(from.slice(1, 3), 16);
160
+ const fg = Number.parseInt(from.slice(3, 5), 16);
161
+ const fb = Number.parseInt(from.slice(5, 7), 16);
162
+ const tr = Number.parseInt(to.slice(1, 3), 16);
163
+ const tg = Number.parseInt(to.slice(3, 5), 16);
164
+ const tb = Number.parseInt(to.slice(5, 7), 16);
165
+ const r = Math.round(fr + (tr - fr) * k);
166
+ const g = Math.round(fg + (tg - fg) * k);
167
+ const b = Math.round(fb + (tb - fb) * k);
168
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
169
+ }
170
+
171
+ /**
172
+ * Renders an assistant message; streaming content remains mutable until the
173
+ * provider finalizes it because later deltas can revise earlier Markdown.
174
+ * The exception is the leading run of visible thinking blocks: raw thinking
175
+ * only ever appends, so its frozen Markdown prefix publishes as append-only
176
+ * stable rows ({@link AppendOnlyTranscriptBlock}) and can retire into native
177
+ * scrollback while the block still streams — a long reasoning trace is no
178
+ * longer clipped to the mutable viewport.
179
+ */
180
+ export class AssistantMessageComponent extends Container {
181
+ readonly transcriptBlockMode = "appendOnly" as const;
182
+ #contentContainer: Container;
183
+ #markerSlot: Container;
184
+ #cacheMarker?: CacheInvalidationMarkerComponent;
185
+ #servedModelMarker?: ServedModelMarkerComponent;
186
+ #lastMessage?: AssistantMessage;
187
+ #emergencyText?: Markdown;
188
+ #toolImagesByCallId = new Map<string, ImageContent[]>();
189
+ #convertedKittyImages = new Map<string, ImageContent>();
190
+ #showImages = true;
191
+ #showToolResultImages = true;
192
+ #kittyConversionsInFlight = new Set<string>();
193
+ #transcriptBlockFinalized: boolean;
194
+ /** See {@link setMidStreamPublication}; the wire's `stream-revision` axis decides it. */
195
+ #midStreamPublication = true;
196
+ /**
197
+ * When true, the turn-ending `Error: …` line for `stopReason === "error"` is
198
+ * suppressed because the same error is currently shown in the pinned banner
199
+ * above the editor (see `EventController` + `ErrorBannerComponent`). Avoids
200
+ * rendering the identical error twice (inline + banner) at the error moment.
201
+ * Restored to `false` when the banner is cleared at the next turn so the
202
+ * transcript keeps the error in history.
203
+ */
204
+ #errorPinned = false;
205
+ /**
206
+ * Whether the inline turn-ending error block renders its full body instead of
207
+ * the {@link MAX_TRANSCRIPT_ERROR_ROWS}-capped preview. Toggled by
208
+ * {@link setExpanded} so Ctrl+O (tool-output expansion) reveals a long
209
+ * provider error whose tail would otherwise be unreachable in the live TUI.
210
+ */
211
+ #errorExpanded = false;
212
+ /**
213
+ * True when the current {@link updateContent} message carries a truncatable
214
+ * inline provider error (the `#appendErrorBlock` path) — set whether or not
215
+ * the inline block was actually drawn, so it stays true even while the error
216
+ * is suppressed under a pinned banner. Gates {@link setExpanded} so toggling
217
+ * expansion only re-renders assistant turns that carry such an error, not
218
+ * every message in the transcript.
219
+ */
220
+ #hasTruncatableError = false;
221
+ /**
222
+ * Monotonic content version reported to the transcript container via
223
+ * {@link getTranscriptBlockVersion}. Bumped by {@link updateContent} — the
224
+ * choke point every mutator funnels through, including post-finalize changes
225
+ * such as `setErrorPinned(false)` restoring the inline error at the next
226
+ * turn's `agent_start`, late tool-result images, and async Kitty conversions.
227
+ */
228
+ #blockVersion = 0;
229
+ /** Whether the last updateContent carried an in-flight streaming partial; such
230
+ * renders bypass the markdown module LRU (see Markdown.transientRenderCache). */
231
+ #lastUpdateTransient = false;
232
+ // Fast-path state: reuse Markdown children when message shape is stable during streaming.
233
+ #fastPathKey: string | undefined;
234
+ #fastPathItems:
235
+ | Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
236
+ | undefined;
237
+ /** Live "thinking" pulse shown in place of a hidden thinking block while it
238
+ * streams; undefined when not animating. Driven by {@link #thinkingDotsTimer}. */
239
+ #thinkingDots: Text | undefined;
240
+ #thinkingDotsTimer: NodeJS.Timeout | undefined;
241
+ #thinkingDotsFrame = 0;
242
+ /** Previous cumulative provider token count + timestamp, for deriving this
243
+ * block's instantaneous streaming rate fed into {@link sharedSpeedTracker}.
244
+ * Undefined until the first thinking update of this block. */
245
+ #lastTokenCount: number | undefined;
246
+ #lastTokenTime = 0;
247
+ /** Published width-independent stable prefixes; grows only, never retracts. */
248
+ #stableSnapshots: StableSnapshot[] = [];
249
+ #stableParts: readonly StablePart[] = [];
250
+ #nextStableRowId = 0;
251
+ #transcriptStableRows: TranscriptStableRow[] = [];
252
+ /**
253
+ * Rendered rows per published snapshot index and width. The container asks
254
+ * for several different counts in one frame (emitted, offered end,
255
+ * projected, pressure-loop +1s); a 2-entry LRU thrashes across those and
256
+ * re-renders the whole prefix per miss. Sized generously so the live prefix
257
+ * stays cached; cleared on reset/finalize like before.
258
+ */
259
+ #stableRenderCache = new LRUCache<string, readonly string[]>({ max: 64 });
260
+ /** Provider-reported tokens in the live thinking block — reasoning tokens when
261
+ * the provider streams them, else total output — shown dimmed beside the
262
+ * speed badge. 0 when no thinking is streaming. */
263
+ #thinkingTokens = 0;
264
+ /** Whether this block has observed a positive provider-token delta — i.e. it is
265
+ * genuinely streaming tokens right now. Gates the numeric speed badge so the
266
+ * session-wide {@link sharedSpeedTracker} can't surface a previous turn's rate
267
+ * on a fresh block that has no live token throughput of its own. */
268
+ #thinkingRateLive = false;
269
+
270
+ #textColorTransform?: (text: string) => string;
271
+ #linkTargets: ReadonlyMap<string, string> = EMPTY_LINK_TARGETS;
272
+ #markdownTheme: MarkdownTheme | undefined;
273
+ /** Block this reply reacts to; undefined when the preceding block takes no reactions. */
274
+ #reactionTarget: ReactionTarget | undefined;
275
+ /** Reaction lifted from the reply's opening emoji, once resolved. */
276
+ #reaction: string | undefined;
277
+
278
+ setTextColorTransform(transform?: (text: string) => string): void {
279
+ this.#textColorTransform = transform;
280
+ }
281
+
282
+ #getProseTheme(): MarkdownTheme {
283
+ if (this.#markdownTheme) return this.#markdownTheme;
284
+ const base = getMarkdownTheme();
285
+ const snapshot = this.#linkTargets;
286
+ const markdownTheme = snapshot.size > 0 ? { ...base, resolveLink: (href: string) => snapshot.get(href) } : base;
287
+ this.#markdownTheme = markdownTheme;
288
+ return markdownTheme;
289
+ }
290
+
291
+ /**
292
+ * Install resolved destinations for model-authored prose links. A fresh
293
+ * theme object is required whenever the map changes because Markdown's
294
+ * render cache keys themes by object identity.
295
+ */
296
+ setLinkTargets(targets: ReadonlyMap<string, string>): void {
297
+ if (
298
+ targets === this.#linkTargets ||
299
+ (targets.size === this.#linkTargets.size &&
300
+ [...targets].every(([href, target]) => this.#linkTargets.get(href) === target))
301
+ ) {
302
+ return;
303
+ }
304
+ this.#linkTargets = targets;
305
+ this.#markdownTheme = undefined;
306
+ this.#fastPathKey = undefined;
307
+ this.#fastPathItems = undefined;
308
+ if (this.#lastMessage) {
309
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Choose the block this reply reacts to from the transcript it is about to
315
+ * join: the nearest preceding reaction-capable block (the user's bubble),
316
+ * looking past turn attachments such as file mentions or injected notices
317
+ * but never past an earlier reply — a continuation after tool calls has
318
+ * nothing to react to. Call before adding this component. An
319
+ * already-resolved reaction is re-applied, and a message rendered verbatim
320
+ * for lack of a target is re-rendered with its reaction stripped.
321
+ */
322
+ pickReactionTarget(transcript: readonly Component[]): void {
323
+ this.#reactionTarget = undefined;
324
+ for (let index = transcript.length - 1; index >= 0; index--) {
325
+ const block = transcript[index]!;
326
+ if (isReactionTarget(block)) {
327
+ this.#reactionTarget = block;
328
+ break;
329
+ }
330
+ if (block instanceof AssistantMessageComponent) break;
331
+ }
332
+ if (this.#reaction !== undefined) this.#reactionTarget?.setReaction(this.#reaction);
333
+ if (this.#lastMessage && this.#openingText(this.#lastMessage)?.split.emoji !== undefined) {
334
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
335
+ }
336
+ }
337
+
338
+ /** The reply's first non-empty text block with its reaction split, if any. */
339
+ #openingText(message: AssistantMessage): { index: number; block: TextContent; split: ReactionSplit } | undefined {
340
+ const index = message.content.findIndex(content => content.type === "text" && content.text.length > 0);
341
+ const block = message.content[index];
342
+ if (block?.type !== "text") return undefined;
343
+ return { index, block, split: splitReaction(block.text) };
344
+ }
345
+
346
+ /**
347
+ * Display form of `message` with the reaction handled: stripped and
348
+ * forwarded to the target once resolved, withheld entirely while a streaming
349
+ * prefix could still become one, and left verbatim when there is no target.
350
+ */
351
+ #displayMessage(message: AssistantMessage, transient: boolean): AssistantMessage {
352
+ const opening = this.#openingText(message);
353
+ if (!opening) return message;
354
+ const { index, block, split } = opening;
355
+ let text: string;
356
+ if (split.emoji !== undefined) {
357
+ if (this.#reaction !== split.emoji) {
358
+ this.#reaction = split.emoji;
359
+ this.#reactionTarget?.setReaction(split.emoji);
360
+ }
361
+ if (!this.#reactionTarget) return message;
362
+ text = split.body;
363
+ } else if (split.pending && transient) {
364
+ text = "";
365
+ } else {
366
+ return message;
367
+ }
368
+ const content = message.content.slice();
369
+ content[index] = { ...block, text };
370
+ return { ...message, content };
371
+ }
372
+ #hideThinkingBlock: boolean;
373
+ readonly #onImageUpdate?: () => void;
374
+ readonly #thinkingRenderers: readonly AssistantThinkingRenderer[];
375
+ readonly #imageBudget?: ImageBudget;
376
+ #proseOnlyThinking: boolean;
377
+
378
+ constructor(
379
+ message?: AssistantMessage,
380
+ hideThinkingBlock = false,
381
+ onImageUpdate?: () => void,
382
+ thinkingRenderers: readonly AssistantThinkingRenderer[] = [],
383
+ imageBudget?: ImageBudget,
384
+ proseOnlyThinking = true,
385
+ linkTargets?: ReadonlyMap<string, string>,
386
+ ) {
387
+ super();
388
+ this.#hideThinkingBlock = hideThinkingBlock;
389
+ this.#onImageUpdate = onImageUpdate;
390
+ this.#thinkingRenderers = thinkingRenderers;
391
+ this.#imageBudget = imageBudget;
392
+ this.#proseOnlyThinking = proseOnlyThinking;
393
+
394
+ ensureThemeSync();
395
+ this.#transcriptBlockFinalized = message !== undefined;
396
+ if (linkTargets?.size) this.#linkTargets = linkTargets;
397
+
398
+ // Container for text/thinking content.
399
+ this.#contentContainer = new Container();
400
+ this.addChild(this.#contentContainer);
401
+
402
+ // Cache-miss usage arrives only at message end. Keep its divider after
403
+ // streamed content so rows already emitted to native history remain a
404
+ // prefix of this append-only block.
405
+ this.#markerSlot = new Container();
406
+ this.addChild(this.#markerSlot);
407
+
408
+ if (message) {
409
+ this.updateContent(message);
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Show or clear the trailing cache-invalidation divider. Set at `message_end`
415
+ * (live) or during rebuild, once the turn's usage is known and compared
416
+ * against the previous turn's cache footprint.
417
+ */
418
+ setCacheInvalidation(info: CacheInvalidation | undefined): void {
419
+ this.#cacheMarker = info ? new CacheInvalidationMarkerComponent(info) : undefined;
420
+ this.#refreshMarkers();
421
+ }
422
+
423
+ /**
424
+ * Show or clear the trailing served-model divider. Set once the turn's
425
+ * signed thinking block (or router report) has named the model that actually
426
+ * answered, when it differs from the one requested.
427
+ */
428
+ setServedModelMismatch(info: ServedModelMismatch | undefined): void {
429
+ this.#servedModelMarker = info ? new ServedModelMarkerComponent(info) : undefined;
430
+ this.#refreshMarkers();
431
+ }
432
+
433
+ #refreshMarkers(): void {
434
+ this.#markerSlot.clear();
435
+ if (this.#servedModelMarker) this.#markerSlot.addChild(this.#servedModelMarker);
436
+ if (this.#cacheMarker) this.#markerSlot.addChild(this.#cacheMarker);
437
+ this.#blockVersion++;
438
+ }
439
+
440
+ override invalidate(): void {
441
+ super.invalidate();
442
+ // Theme/symbol changes arrive via invalidate(). Fast-path children captured
443
+ // their theme at construction, so drop them and force the teardown path to
444
+ // rebuild with the current theme. Streaming updates call updateContent()
445
+ // directly and keep the fast path.
446
+ this.#markdownTheme = undefined;
447
+ this.#fastPathKey = undefined;
448
+ this.#fastPathItems = undefined;
449
+ if (this.#lastMessage) {
450
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
451
+ }
452
+ }
453
+
454
+ setHideThinkingBlock(hide: boolean): void {
455
+ this.#hideThinkingBlock = hide;
456
+ }
457
+
458
+ /**
459
+ * Allow or withhold retiring finished lines into native scrollback while the
460
+ * turn is still streaming. Wires the `stream-revision` axis marks as able to
461
+ * revise already-streamed text must withhold it: published bytes go to
462
+ * terminal history once and cannot be retracted, so a later revision would
463
+ * leave the reader with a stale copy. Withholding costs only reachability —
464
+ * the block still retires whole when the turn ends.
465
+ */
466
+ setMidStreamPublication(allowed: boolean): void {
467
+ this.#midStreamPublication = allowed;
468
+ }
469
+
470
+ setProseOnlyThinking(proseOnly: boolean): void {
471
+ this.#proseOnlyThinking = proseOnly;
472
+ }
473
+
474
+ override dispose(): void {
475
+ this.#stopThinkingAnimation();
476
+ super.dispose();
477
+ }
478
+
479
+ /**
480
+ * Whether to render the animated "thinking" pulse in place of the suppressed
481
+ * reasoning: only while this block is still streaming (not yet finalized — the
482
+ * in-flight message always carries `stopReason: "stop"`, so finalization is the
483
+ * only reliable live signal), thinking is hidden, no tool call has started, and
484
+ * the active tail block is a thinking block (the model is reasoning right now).
485
+ * Once text starts, a tool call streams, or the block is sealed, the pulse ends.
486
+ */
487
+ #shouldAnimateThinking(message: AssistantMessage): boolean {
488
+ if (!this.#hideThinkingBlock || this.#transcriptBlockFinalized) return false;
489
+ let tail: "text" | "thinking" | undefined;
490
+ for (const content of message.content) {
491
+ if (content.type === "toolCall") return false;
492
+ if (content.type === "text" && canonicalizeMessage(content.text)) tail = "text";
493
+ else if (content.type === "thinking" && canonicalizeMessage(content.thinking)) tail = "thinking";
494
+ }
495
+ return tail === "thinking";
496
+ }
497
+
498
+ #thinkingDotsLabel(): string {
499
+ const glyph = THINKING_DOTS_FRAMES[this.#thinkingDotsFrame % THINKING_DOTS_FRAMES.length] ?? "…";
500
+ const coloredGlyph = theme.fg("thinkingText", glyph);
501
+ const thinkingLabel = theme.fg("muted", " Thinking");
502
+ const rate = Math.min(SPEED_MAX, sharedSpeedTracker.getSpeed());
503
+ // The numeric badge ("<total> · <rate> toks/s") only renders while this block
504
+ // is genuinely streaming provider tokens. A block that has observed no token
505
+ // delta (e.g. a provider that reports usage only at turn end) or whose rate
506
+ // has decayed to zero (a streaming lull) drops it entirely — the persistent
507
+ // text label keeps the pulse descriptive for terminals and screen readers.
508
+ // The liveness flag also stops the session-wide gauge from leaking a previous
509
+ // turn's rate onto a fresh token-less block.
510
+ if (!this.#thinkingRateLive || rate < 0.05) return coloredGlyph + thinkingLabel;
511
+ // Total provider tokens, dimmed, sit next to the pulse.
512
+ const totalSpan = this.#thinkingTokens > 0 ? theme.fg("dim", ` · ${formatNumber(this.#thinkingTokens)}`) : "";
513
+ // Speed badge color: dim gray at rest, brightening toward the theme accent as
514
+ // streaming speed climbs (gray → bright accent). Ease (sqrt) so typical
515
+ // mid-stream rates already read as clearly accent-tinted instead of staying
516
+ // gray until the rarely-hit SPEED_MAX ceiling.
517
+ const ratio = Math.sqrt(rate / SPEED_MAX);
518
+ const hex = lerpHex(theme.getColorHex("dim"), theme.getAccentColorHex(), ratio);
519
+ const rateText = ` · ${rate.toFixed(1)} toks/s`;
520
+ const rateSpan = theme.getColorMode() === "truecolor" ? chalk.hex(hex)(rateText) : theme.fg("muted", rateText);
521
+ return coloredGlyph + thinkingLabel + totalSpan + rateSpan;
522
+ }
523
+
524
+ #startThinkingAnimation(): void {
525
+ if (this.#thinkingDotsTimer) return;
526
+ this.#scheduleThinkingFrame();
527
+ }
528
+
529
+ /** Eased dwell (ms) for the current pulse frame: a raised cosine over the
530
+ * 8-frame cycle, continuous across the wrap, so the rotation breathes rather
531
+ * than advancing at a fixed interval. */
532
+ #thinkingDotsFrameDelay(): number {
533
+ const phase = (1 - Math.cos((2 * Math.PI * this.#thinkingDotsFrame) / THINKING_DOTS_FRAMES.length)) / 2;
534
+ return THINKING_DOTS_FRAME_MS_MIN + (THINKING_DOTS_FRAME_MS_MAX - THINKING_DOTS_FRAME_MS_MIN) * phase;
535
+ }
536
+
537
+ /** Self-rescheduling timeout (not a fixed interval) so each frame can pick its
538
+ * own eased dwell. */
539
+ #scheduleThinkingFrame(): void {
540
+ this.#thinkingDotsTimer = setTimeout(() => this.#advanceThinkingDots(), this.#thinkingDotsFrameDelay());
541
+ this.#thinkingDotsTimer.unref?.();
542
+ }
543
+
544
+ #advanceThinkingDots(): void {
545
+ this.#thinkingDotsTimer = undefined;
546
+ if (!this.#thinkingDots) {
547
+ this.#stopThinkingAnimation();
548
+ return;
549
+ }
550
+ this.#thinkingDotsFrame = (this.#thinkingDotsFrame + 1) % THINKING_DOTS_FRAMES.length;
551
+ if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
552
+ this.#onImageUpdate?.();
553
+ }
554
+ this.#scheduleThinkingFrame();
555
+ }
556
+
557
+ #stopThinkingAnimation(): void {
558
+ if (this.#thinkingDotsTimer) {
559
+ clearTimeout(this.#thinkingDotsTimer);
560
+ this.#thinkingDotsTimer = undefined;
561
+ }
562
+ this.#thinkingDotsFrame = 0;
563
+ }
564
+
565
+ /**
566
+ * Toggle suppression of the inline `Error: …` line while the same error is
567
+ * pinned in the banner above the editor. Re-renders so the change is visible.
568
+ */
569
+ setErrorPinned(pinned: boolean): void {
570
+ if (this.#errorPinned === pinned) return;
571
+ this.#errorPinned = pinned;
572
+ if (this.#lastMessage) {
573
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Expand or collapse the inline turn-ending error block so Ctrl+O
579
+ * (tool-output expansion) can reveal a long provider error's hidden tail.
580
+ * Only re-renders when the current message carries a truncatable error, so
581
+ * toggling expansion across the transcript skips ordinary turns. Works even
582
+ * while the error is pinned in the banner: the inline block is drawn (in full)
583
+ * when expanded so the complete body is reachable without sending a message.
584
+ */
585
+ setExpanded(expanded: boolean): void {
586
+ if (this.#errorExpanded === expanded) return;
587
+ this.#errorExpanded = expanded;
588
+ if (this.#hasTruncatableError && this.#lastMessage) {
589
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
590
+ }
591
+ }
592
+
593
+ isTranscriptBlockFinalized(): boolean {
594
+ return this.#transcriptBlockFinalized;
595
+ }
596
+
597
+ override render(width: number): readonly string[] {
598
+ const rows = super.render(width);
599
+ this.#publishStableSnapshot(rows, width);
600
+ return rows;
601
+ }
602
+
603
+ /** Width-independent stable identities for the streamed leading thinking run. */
604
+ getTranscriptStableRows(): readonly TranscriptStableRow[] {
605
+ return this.#transcriptStableRows;
606
+ }
607
+
608
+ /**
609
+ * Drop every published thinking stable row. Called by the transcript
610
+ * container during a visibility-driven destructive replay so the head no
611
+ * longer re-emits reasoning captured while thinking was visible. Safe only
612
+ * because the paired display reset clears the scrollback those rows occupied
613
+ * — see {@link AppendOnlyTranscriptBlock.resetTranscriptStableRows}.
614
+ */
615
+ resetTranscriptStableRows(): void {
616
+ this.#stableSnapshots = [];
617
+ this.#stableParts = [];
618
+ this.#transcriptStableRows = [];
619
+ this.#stableRenderCache.clear();
620
+ }
621
+
622
+ renderTranscriptStableRows(count: number, width: number): readonly string[] {
623
+ const index = Math.min(Math.trunc(count), this.#stableSnapshots.length);
624
+ if (index <= 0) return EMPTY_STABLE_RENDER;
625
+ const key = `${index}:${width}`;
626
+ const cached = this.#stableRenderCache.get(key);
627
+ if (cached) return cached;
628
+ const snapshot = this.#stableSnapshots[index - 1]!;
629
+ const parts = this.#stableParts.slice(0, snapshot.partCount);
630
+ const last = parts.at(-1);
631
+ if (last && last.kind !== "spacer") {
632
+ parts[parts.length - 1] = { kind: last.kind, text: last.text.slice(0, snapshot.lastTextLength) };
633
+ }
634
+ const rows = this.#renderStableSnapshot(parts, width);
635
+ this.#stableRenderCache.set(key, rows);
636
+ return rows;
637
+ }
638
+
639
+ /**
640
+ * Publish the block's finished prefix as stable transcript rows so a long
641
+ * stream can retire into native scrollback mid-turn instead of being clipped
642
+ * to the live viewport until the turn ends. Finished means bytes that can no
643
+ * longer change: closed child blocks, plus the streaming child's frozen
644
+ * Markdown prefix. Published bytes may already sit in terminal history, so
645
+ * every guard skips publication and nothing ever retracts it.
646
+ */
647
+ #publishStableSnapshot(rendered: readonly string[], width: number): void {
648
+ if (!this.#midStreamPublication) return;
649
+ const parts = this.#currentStableSnapshot();
650
+ if (!parts) return;
651
+ const last = parts.at(-1);
652
+ if (!last || last.kind === "spacer") return;
653
+ const snapshot = { partCount: parts.length, lastTextLength: last.text.length };
654
+ const previous = this.#stableSnapshots.at(-1);
655
+ if (previous && !isSnapshotExtension(this.#stableParts, parts)) return;
656
+ if (previous?.partCount === snapshot.partCount && previous.lastTextLength === snapshot.lastTextLength) return;
657
+ const currentRows = this.#renderStableSnapshot(parts, width);
658
+ // The container verifies stable rows against the blank-trimmed render.
659
+ if (!isRowPrefix(currentRows, trimBlankEdges(rendered))) return;
660
+ const previousRows = previous
661
+ ? this.renderTranscriptStableRows(this.#stableSnapshots.length, width)
662
+ : EMPTY_STABLE_RENDER;
663
+ if (!isRowPrefix(previousRows, currentRows)) return;
664
+ // Each stable row must add at least one physical row at every width.
665
+ if (currentRows.length === previousRows.length) return;
666
+ this.#stableParts = parts;
667
+ this.#stableSnapshots.push(snapshot);
668
+ this.#transcriptStableRows.push({ key: `thinking:${this.#nextStableRowId++}` });
669
+ this.#stableRenderCache.set(`${this.#stableSnapshots.length}:${width}`, currentRows);
670
+ }
671
+
672
+ /**
673
+ * Width-independent parts eligible for publication right now: the leading
674
+ * run of finished blocks, ending inside the streaming block at Markdown's
675
+ * frozen boundary. Undefined whenever any prefix byte could still change
676
+ * (finalized or non-transient renders, marker rows, extension components,
677
+ * hidden thinking, or no frozen prefix yet).
678
+ */
679
+ #currentStableSnapshot(): readonly StablePart[] | undefined {
680
+ if (this.#transcriptBlockFinalized || !this.#lastUpdateTransient) return undefined;
681
+ if (this.#markerSlot.children.length > 0) return undefined;
682
+ const items = this.#fastPathItems;
683
+ if (!items || items.length === 0) return undefined;
684
+ const parts: StablePart[] = [];
685
+ let itemIndex = 0;
686
+ for (const child of this.#contentContainer.children) {
687
+ const item = items[itemIndex];
688
+ if (item?.md === child) {
689
+ if (itemIndex === items.length - 1) {
690
+ // Streaming child: publish Markdown's frozen prefix, and only
691
+ // once non-blank content exists past it — the block's last
692
+ // non-blank line is still being written (thinking's prose fold
693
+ // may rewrite it) and must stay out of published bytes.
694
+ const raw = item.md.getLastRenderStableText();
695
+ const frozen = raw.trim();
696
+ if (frozen.length > 0 && /\S/.test(item.lastText.slice(raw.length))) {
697
+ parts.push({ kind: item.blockType, text: frozen });
698
+ }
699
+ break;
700
+ }
701
+ // Closed child: only the streaming tail may mutate in place, so
702
+ // everything before it is final.
703
+ parts.push({ kind: item.blockType, text: item.lastText });
704
+ itemIndex++;
705
+ continue;
706
+ }
707
+ if (child instanceof Spacer) {
708
+ parts.push({ kind: "spacer" });
709
+ continue;
710
+ }
711
+ // Unknown child (thinking extension, pulse, image, error row): stop.
712
+ break;
713
+ }
714
+ while (parts.at(-1)?.kind === "spacer") parts.pop();
715
+ if (parts.length === 0) return undefined;
716
+ return parts;
717
+ }
718
+
719
+ #renderStableSnapshot(parts: readonly StablePart[], width: number): readonly string[] {
720
+ const rows: string[] = [];
721
+ for (const part of parts) {
722
+ if (part.kind === "spacer") {
723
+ rows.push("");
724
+ continue;
725
+ }
726
+ // Constructor args mirror the live child Markdown exactly so these
727
+ // rows are byte-identical to the block render's prefix — including the
728
+ // trim the live children apply, which drops the trailing blank line a
729
+ // frozen prefix still carries.
730
+ const text = part.text.trim();
731
+ const markdown =
732
+ part.kind === "text"
733
+ ? new Markdown(
734
+ text,
735
+ 1,
736
+ 0,
737
+ this.#getProseTheme(),
738
+ this.#textColorTransform ? { color: this.#textColorTransform } : undefined,
739
+ 0,
740
+ )
741
+ : new Markdown(text, 1, 0, getMarkdownTheme(), {
742
+ color: (value: string) => theme.fg("thinkingText", value),
743
+ italic: true,
744
+ });
745
+ rows.push(...markdown.render(width));
746
+ }
747
+ return rows;
748
+ }
749
+
750
+ /** Render completed prose rather than an earlier thinking row under emergency viewport pressure. */
751
+ renderTranscriptBlockEmergencyRow(width: number): string | undefined {
752
+ if (!this.#transcriptBlockFinalized) return undefined;
753
+ return this.#emergencyText?.render(width)[0];
754
+ }
755
+
756
+ getTranscriptBlockVersion(): number {
757
+ return this.#blockVersion;
758
+ }
759
+
760
+ markTranscriptBlockFinalized(): void {
761
+ this.#transcriptBlockFinalized = true;
762
+ this.#stableRenderCache.clear();
763
+ this.#stopThinkingAnimation();
764
+ // If the live pulse was on screen when the block sealed, drop the fast path
765
+ // and rebuild so the placeholder is removed — finalized blocks never animate.
766
+ if (this.#thinkingDots) {
767
+ this.#fastPathKey = undefined;
768
+ this.#fastPathItems = undefined;
769
+ if (this.#lastMessage) this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
770
+ }
771
+ }
772
+
773
+ applyRetryRecovery(retryRecovery: AssistantMessage["retryRecovery"]): void {
774
+ if (!this.#lastMessage || !retryRecovery) return;
775
+ this.setErrorPinned(false);
776
+ this.updateContent({ ...this.#lastMessage, retryRecovery });
777
+ }
778
+
779
+ messagePersistenceKey(): string | undefined {
780
+ if (!this.#lastMessage) return undefined;
781
+ return [
782
+ "assistant",
783
+ this.#lastMessage.timestamp,
784
+ this.#lastMessage.provider,
785
+ this.#lastMessage.model,
786
+ this.#lastMessage.responseId ?? "",
787
+ this.#lastMessage.stopReason,
788
+ ].join(":");
789
+ }
790
+
791
+ /**
792
+ * Render a turn-ending provider error inline, wrapped to the render width.
793
+ * Collapsed (default), the block keeps {@link MAX_TRANSCRIPT_ERROR_ROWS}
794
+ * wrapped rows and ends with a dim `ctrl+o`/expand hint when rows were cut,
795
+ * so a pathological body — e.g. the HTML page a proxy returns on a 502 —
796
+ * can't flood the transcript. Expanded (via {@link setExpanded}), every row
797
+ * is rendered so the complete message is reachable. Mirrors
798
+ * {@link ErrorBannerComponent}. The caller owns the separating Spacer.
799
+ */
800
+ #appendErrorBlock(message: string): void {
801
+ const maxRows = this.#errorExpanded ? Number.POSITIVE_INFINITY : MAX_TRANSCRIPT_ERROR_ROWS;
802
+ this.#contentContainer.addChild(
803
+ new WidthAwareText(
804
+ contentWidth =>
805
+ formatErrorBlock(message, contentWidth, maxRows, (line, index) =>
806
+ theme.fg("error", index === 0 ? `Error: ${line}` : line),
807
+ ),
808
+ 1,
809
+ 0,
810
+ ),
811
+ );
812
+ }
813
+
814
+ /** Toggle rendering for assistant-native and tool-result images. */
815
+ setImagesVisible(visible: boolean): void {
816
+ if (this.#showImages === visible) return;
817
+ this.#showImages = visible;
818
+ if (this.#lastMessage) {
819
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
820
+ }
821
+ }
822
+
823
+ /** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
824
+ setToolResultImagesVisible(visible: boolean): void {
825
+ if (this.#showToolResultImages === visible) return;
826
+ this.#showToolResultImages = visible;
827
+ if (this.#lastMessage) {
828
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
829
+ }
830
+ }
831
+
832
+ setToolResultImages(toolCallId: string, images: ImageContent[]): void {
833
+ if (!toolCallId) return;
834
+ const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
835
+ for (const key of Array.from(this.#convertedKittyImages.keys())) {
836
+ if (key.startsWith(`${toolCallId}:`)) {
837
+ this.#convertedKittyImages.delete(key);
838
+ }
839
+ }
840
+ for (const key of Array.from(this.#kittyConversionsInFlight)) {
841
+ if (key.startsWith(`${toolCallId}:`)) {
842
+ this.#kittyConversionsInFlight.delete(key);
843
+ }
844
+ }
845
+ if (validImages.length === 0) {
846
+ this.#toolImagesByCallId.delete(toolCallId);
847
+ } else {
848
+ this.#toolImagesByCallId.set(toolCallId, validImages);
849
+ this.#convertImagesForKitty(validImages.map((image, index) => ({ image, key: `${toolCallId}:${index}` })));
850
+ }
851
+ if (this.#lastMessage) {
852
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
853
+ }
854
+ }
855
+
856
+ #convertImagesForKitty(entries: Array<{ image: ImageContent; key: string }>): void {
857
+ if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
858
+ for (const { image, key } of entries) {
859
+ if (image.mimeType === "image/png") continue;
860
+ if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
861
+ this.#kittyConversionsInFlight.add(key);
862
+ convertImageToPng(image)
863
+ .then(converted => {
864
+ this.#kittyConversionsInFlight.delete(key);
865
+ this.#convertedKittyImages.set(key, converted);
866
+ if (this.#lastMessage) {
867
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
868
+ }
869
+ this.#onImageUpdate?.();
870
+ })
871
+ .catch(() => {
872
+ this.#kittyConversionsInFlight.delete(key);
873
+ });
874
+ }
875
+ }
876
+
877
+ #renderImageEntries(entries: Array<{ image: ImageContent; key: string }>, withLeadingSpacer: boolean): void {
878
+ if (!this.#showImages || entries.length === 0) return;
879
+ this.#convertImagesForKitty(entries);
880
+
881
+ if (withLeadingSpacer) this.#contentContainer.addChild(new Spacer(1));
882
+ for (const { image, key } of entries) {
883
+ const displayImage =
884
+ TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png"
885
+ ? this.#convertedKittyImages.get(key)
886
+ : image;
887
+ if (TERMINAL.imageProtocol && displayImage) {
888
+ this.#contentContainer.addChild(
889
+ new Image(
890
+ displayImage.data,
891
+ displayImage.mimeType,
892
+ { fallbackColor: (text: string) => theme.fg("toolOutput", text) },
893
+ { ...resolveImageOptions(), budget: this.#imageBudget, imageKey: key },
894
+ ),
895
+ );
896
+ continue;
897
+ }
898
+ this.#contentContainer.addChild(new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0));
899
+ }
900
+ }
901
+
902
+ #renderToolImages(): void {
903
+ if (!this.#showToolResultImages) return;
904
+ const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
905
+ images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
906
+ );
907
+ this.#renderImageEntries(entries, true);
908
+ }
909
+
910
+ #appendThinkingExtensions(contentIndex: number, thinkingIndex: number, text: string): void {
911
+ for (const renderer of this.#thinkingRenderers) {
912
+ try {
913
+ const component = renderer(
914
+ {
915
+ contentIndex,
916
+ thinkingIndex,
917
+ text,
918
+ requestRender: () => this.#onImageUpdate?.(),
919
+ },
920
+ theme,
921
+ );
922
+ if (component) {
923
+ this.#contentContainer.addChild(component);
924
+ }
925
+ } catch {
926
+ // Ignore extension renderer failures and keep the original thinking block visible.
927
+ }
928
+ }
929
+ }
930
+
931
+ #computeShapeKey(message: AssistantMessage): string {
932
+ const parts: string[] = [`htb:${this.#hideThinkingBlock ? 1 : 0}|pot:${this.#proseOnlyThinking ? 1 : 0}`];
933
+ for (const content of message.content) {
934
+ if (content.type === "text") {
935
+ parts.push(canonicalizeMessage(content.text) ? "T1" : "T0");
936
+ } else if (content.type === "thinking") {
937
+ if (this.#hideThinkingBlock) {
938
+ // Match the pulse's empty/nonempty transition without formatting hidden text.
939
+ parts.push(canonicalizeMessage(content.thinking) ? "KH" : "K0");
940
+ } else {
941
+ parts.push(resolveThinkingDisplay(content, this.#proseOnlyThinking).visible ? "KV" : "K0");
942
+ }
943
+ } else {
944
+ // Non-rendered blocks (toolCall, redactedThinking, …) still occupy a
945
+ // content index. Encode their position so an inserted/removed one shifts
946
+ // the key and forces the teardown path instead of mis-indexing children.
947
+ parts.push(`O:${content.type}`);
948
+ }
949
+ }
950
+ return parts.join("|");
951
+ }
952
+
953
+ #canFastPath(message: AssistantMessage): boolean {
954
+ for (const content of message.content) {
955
+ if (content.type === "toolCall" || content.type === "image") return false;
956
+ }
957
+ if (this.#toolImagesByCallId.size > 0) return false;
958
+ const errorPresentation = resolveAssistantErrorPresentation(message);
959
+ if (errorPresentation.kind === "compact-recovered") return false;
960
+ if (
961
+ errorPresentation.kind === "full" &&
962
+ !(message.stopReason === "error" && this.#errorPinned && !this.#errorExpanded)
963
+ ) {
964
+ return false;
965
+ }
966
+ // Extension stability: if thinking renderers exist and any tracked thinking
967
+ // block's text changed, extensions may produce a different child count.
968
+ if (!this.#hideThinkingBlock && this.#thinkingRenderers.length > 0 && this.#fastPathItems) {
969
+ for (const item of this.#fastPathItems) {
970
+ if (item.blockType === "thinking") {
971
+ const content = message.content[item.contentIndex];
972
+ if (content?.type === "thinking") {
973
+ const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
974
+ if (display.text !== item.lastText) return false;
975
+ }
976
+ }
977
+ }
978
+ }
979
+ return true;
980
+ }
981
+
982
+ #tryFastPathUpdate(message: AssistantMessage, opts?: { transient?: boolean }): boolean {
983
+ if (!this.#fastPathKey || !this.#fastPathItems) return false;
984
+ if (!this.#canFastPath(message)) {
985
+ this.#fastPathKey = undefined;
986
+ this.#fastPathItems = undefined;
987
+ return false;
988
+ }
989
+ if (this.#computeShapeKey(message) !== this.#fastPathKey) {
990
+ this.#fastPathKey = undefined;
991
+ this.#fastPathItems = undefined;
992
+ return false;
993
+ }
994
+ const transient = opts?.transient === true;
995
+ // Shape is identical — setText only on Markdown children whose source changed.
996
+ this.#applyItemTransience(transient);
997
+ for (let i = 0; i < this.#fastPathItems.length; i++) {
998
+ const item = this.#fastPathItems[i]!;
999
+ const content = message.content[item.contentIndex];
1000
+ if (!content) {
1001
+ this.#fastPathKey = undefined;
1002
+ this.#fastPathItems = undefined;
1003
+ return false;
1004
+ }
1005
+ let newText: string;
1006
+ if (item.blockType === "text" && content.type === "text") {
1007
+ newText = content.text.trim();
1008
+ } else if (item.blockType === "thinking" && content.type === "thinking") {
1009
+ newText = resolveThinkingDisplay(content, this.#proseOnlyThinking).text;
1010
+ } else {
1011
+ this.#fastPathKey = undefined;
1012
+ this.#fastPathItems = undefined;
1013
+ return false;
1014
+ }
1015
+ if (newText !== item.lastText) {
1016
+ // Only the last (actively streaming) block may mutate in place: a
1017
+ // delta into an earlier block would invalidate rows the settled
1018
+ // walk already declared final, so tear down and rebuild instead.
1019
+ if (i < this.#fastPathItems.length - 1) {
1020
+ this.#fastPathKey = undefined;
1021
+ this.#fastPathItems = undefined;
1022
+ return false;
1023
+ }
1024
+ item.md.setText(newText);
1025
+ item.lastText = newText;
1026
+ }
1027
+ }
1028
+ if (this.#thinkingDots) {
1029
+ if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
1030
+ this.#onImageUpdate?.();
1031
+ }
1032
+ }
1033
+ return true;
1034
+ }
1035
+
1036
+ updateContent(message: AssistantMessage, opts?: { transient?: boolean }): void {
1037
+ this.#blockVersion++;
1038
+ this.#lastMessage = message;
1039
+ this.#lastUpdateTransient = opts?.transient === true;
1040
+ // Everything below renders the display form; #lastMessage keeps the
1041
+ // verbatim message so re-renders re-derive the reaction deterministically.
1042
+ message = this.#displayMessage(message, this.#lastUpdateTransient);
1043
+
1044
+ // Streaming-speed gauge: only a live, in-flight render of the single
1045
+ // animating hidden-thinking block feeds the shared session tracker. The
1046
+ // token count is the provider's own cumulative output — reasoning tokens when
1047
+ // reported (Gemini's thoughtsTokenCount, OpenAI's reasoning_tokens), else
1048
+ // total output tokens — never a character estimate, which undercounts when
1049
+ // the provider streams a summarized reasoning trace. An instantaneous tok/s
1050
+ // is derived from this block's delta and handed to the windowed averager.
1051
+ // Only transient renders count: the final non-transient render at
1052
+ // message_end carries the turn's end-of-stream usage, whose jump would spike
1053
+ // the gauge and pollute the next block. Providers that report usage only at
1054
+ // turn end leave the live count flat, so the rate stays 0 and the badge
1055
+ // self-suppresses (see #thinkingDotsLabel).
1056
+ const isThinkingNow = this.#lastUpdateTransient && this.#shouldAnimateThinking(message);
1057
+ if (isThinkingNow) {
1058
+ const currentTokens = message.usage.reasoningTokens ?? message.usage.output;
1059
+ this.#thinkingTokens = currentTokens;
1060
+ const now = performance.now();
1061
+ if (this.#lastTokenCount !== undefined) {
1062
+ const tokenDelta = currentTokens - this.#lastTokenCount;
1063
+ const elapsedMs = now - this.#lastTokenTime;
1064
+ if (tokenDelta > 0 && elapsedMs > 0) {
1065
+ // First live sample of this block: drop the session gauge's prior-turn
1066
+ // observations so the windowed average reflects only this block.
1067
+ if (!this.#thinkingRateLive) sharedSpeedTracker.reset();
1068
+ sharedSpeedTracker.observe((tokenDelta / elapsedMs) * 1000, now);
1069
+ this.#thinkingRateLive = true;
1070
+ }
1071
+ }
1072
+ this.#lastTokenCount = currentTokens;
1073
+ this.#lastTokenTime = now;
1074
+ } else {
1075
+ this.#lastTokenCount = undefined;
1076
+ this.#thinkingTokens = 0;
1077
+ this.#thinkingRateLive = false;
1078
+ }
1079
+
1080
+ // Fast path: reuse Markdown children when shape is stable during streaming
1081
+ if (this.#tryFastPathUpdate(message, opts)) return;
1082
+
1083
+ // Clear content container
1084
+ this.#contentContainer.clear();
1085
+ this.#emergencyText = undefined;
1086
+ this.#thinkingDots = undefined;
1087
+ this.#hasTruncatableError = false;
1088
+
1089
+ // Determine if we should capture Markdown instances for next fast path
1090
+ const shouldCapture = this.#canFastPath(message);
1091
+ const captureItems:
1092
+ | Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
1093
+ | undefined = shouldCapture ? [] : undefined;
1094
+
1095
+ const hasVisibleContent = message.content.some(
1096
+ c =>
1097
+ (c.type === "text" && canonicalizeMessage(c.text)) ||
1098
+ (c.type === "image" && c.data && c.mimeType) ||
1099
+ (!this.#hideThinkingBlock &&
1100
+ c.type === "thinking" &&
1101
+ resolveThinkingDisplay(c, this.#proseOnlyThinking).visible),
1102
+ );
1103
+
1104
+ // Render content in order
1105
+ let thinkingIndex = 0;
1106
+ let hasRenderedContent = false;
1107
+ for (let i = 0; i < message.content.length; i++) {
1108
+ const content = message.content[i];
1109
+ if (content.type === "text" && canonicalizeMessage(content.text)) {
1110
+ // Set paddingY=0 to avoid extra spacing before tool executions
1111
+ const trimmed = content.text.trim();
1112
+ const mdOptions = this.#textColorTransform ? { color: this.#textColorTransform } : undefined;
1113
+ const md = new Markdown(trimmed, 1, 0, this.#getProseTheme(), mdOptions, 0);
1114
+ this.#contentContainer.addChild(md);
1115
+ this.#emergencyText = md;
1116
+ captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
1117
+ hasRenderedContent = true;
1118
+ } else if (content.type === "thinking") {
1119
+ if (this.#hideThinkingBlock) {
1120
+ thinkingIndex += 1;
1121
+ continue;
1122
+ }
1123
+ const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
1124
+ if (!display.visible) continue;
1125
+ const thinkingText = display.text;
1126
+ // Add spacing only when another visible assistant content block follows.
1127
+ // This avoids a superfluous blank line before separately-rendered tool execution blocks.
1128
+ const hasVisibleContentAfter = message.content
1129
+ .slice(i + 1)
1130
+ .some(
1131
+ c =>
1132
+ (c.type === "text" && canonicalizeMessage(c.text)) ||
1133
+ (c.type === "image" && c.data && c.mimeType) ||
1134
+ (c.type === "thinking" && resolveThinkingDisplay(c, this.#proseOnlyThinking).visible),
1135
+ );
1136
+
1137
+ // Thinking traces in thinkingText color, italic
1138
+ const md = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
1139
+ color: (text: string) => theme.fg("thinkingText", text),
1140
+ italic: true,
1141
+ });
1142
+ md.transientRenderCache = this.#lastUpdateTransient;
1143
+ this.#contentContainer.addChild(md);
1144
+ captureItems?.push({ md, contentIndex: i, blockType: "thinking", lastText: thinkingText });
1145
+ this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
1146
+ hasRenderedContent = true;
1147
+ thinkingIndex += 1;
1148
+ if (hasVisibleContentAfter) {
1149
+ this.#contentContainer.addChild(new Spacer(1));
1150
+ }
1151
+ } else if (content.type === "image" && content.data && content.mimeType) {
1152
+ this.#renderImageEntries([{ image: content, key: `native:${i}` }], hasRenderedContent);
1153
+ hasRenderedContent ||= this.#showImages;
1154
+ }
1155
+ }
1156
+
1157
+ if (this.#shouldAnimateThinking(message)) {
1158
+ if (hasVisibleContent) this.#contentContainer.addChild(new Spacer(1));
1159
+ this.#thinkingDots = new Text(this.#thinkingDotsLabel(), 1, 0);
1160
+ this.#contentContainer.addChild(this.#thinkingDots);
1161
+ this.#startThinkingAnimation();
1162
+ } else {
1163
+ this.#stopThinkingAnimation();
1164
+ }
1165
+
1166
+ this.#renderToolImages();
1167
+ const errorPresentation = resolveAssistantErrorPresentation(message);
1168
+ const hasToolCalls = message.content.some(c => c.type === "toolCall");
1169
+ if (errorPresentation.kind === "compact-recovered") {
1170
+ this.#contentContainer.addChild(new Spacer(1));
1171
+ this.#contentContainer.addChild(new Text(theme.fg("dim", errorPresentation.text), 1, 0));
1172
+ } else if (!hasToolCalls && errorPresentation.kind === "full") {
1173
+ if (message.stopReason === "aborted") {
1174
+ this.#contentContainer.addChild(new Spacer(1));
1175
+ this.#contentContainer.addChild(new Text(theme.fg("error", errorPresentation.text), 1, 0));
1176
+ } else {
1177
+ // Non-aborted provider error: a truncatable inline block. Mark it so
1178
+ // setExpanded re-renders even while the same error is pinned above.
1179
+ this.#hasTruncatableError = true;
1180
+ // Suppress the inline block only while pinned AND collapsed — the
1181
+ // banner already shows the capped error there. When expanded, draw
1182
+ // the inline block in full so the complete body is reachable without
1183
+ // sending a message; the pinned banner stays a short reminder.
1184
+ if (!(message.stopReason === "error" && this.#errorPinned) || this.#errorExpanded) {
1185
+ this.#contentContainer.addChild(new Spacer(1));
1186
+ this.#appendErrorBlock(errorPresentation.text);
1187
+ }
1188
+ }
1189
+ }
1190
+ // Store fast-path state for next call
1191
+ if (shouldCapture) {
1192
+ this.#fastPathItems = captureItems;
1193
+ this.#fastPathKey = this.#computeShapeKey(message);
1194
+ this.#applyItemTransience(this.#lastUpdateTransient);
1195
+ } else {
1196
+ this.#fastPathKey = undefined;
1197
+ this.#fastPathItems = undefined;
1198
+ }
1199
+ }
1200
+
1201
+ /**
1202
+ * Only the actively streaming (last) markdown renders in transient mode;
1203
+ * completed blocks render final — syntax-highlighted, module-LRU-cached,
1204
+ * byte-stable — so their rows can settle into native scrollback mid-turn
1205
+ * and are byte-identical to the finalize render.
1206
+ */
1207
+ #applyItemTransience(transient: boolean): void {
1208
+ const items = this.#fastPathItems;
1209
+ if (!items) return;
1210
+ for (let i = 0; i < items.length; i++) {
1211
+ items[i]!.md.transientRenderCache = transient && i === items.length - 1;
1212
+ }
1213
+ }
1214
+ }