@oh-my-pi/pi-tui 18.4.2 → 18.4.4

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 (463) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/types/apps/cleanse-board.d.ts +17 -0
  3. package/dist/types/apps/git/help.d.ts +16 -0
  4. package/dist/types/apps/git/sidebar.d.ts +18 -0
  5. package/dist/types/apps/live-visualizer.d.ts +10 -0
  6. package/dist/types/apps/ps-top.d.ts +15 -0
  7. package/dist/types/apps/standalone-picker.d.ts +3 -0
  8. package/dist/types/autocomplete.d.ts +8 -0
  9. package/dist/types/chat/advisor-message.d.ts +12 -2
  10. package/dist/types/chat/assistant-message.d.ts +20 -0
  11. package/dist/types/chat/bash-execution.d.ts +9 -0
  12. package/dist/types/chat/chat-transcript-builder.d.ts +6 -1
  13. package/dist/types/chat/collab-prompt-message.d.ts +4 -0
  14. package/dist/types/chat/compaction-summary-message.d.ts +15 -0
  15. package/dist/types/chat/eval-execution.d.ts +8 -0
  16. package/dist/types/chat/execution-shared.d.ts +58 -0
  17. package/dist/types/chat/late-diagnostics-message.d.ts +15 -1
  18. package/dist/types/chat/read-tool-group.d.ts +15 -0
  19. package/dist/types/chat/skill-message.d.ts +9 -0
  20. package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +3 -0
  21. package/dist/types/chat/thinking-display.d.ts +2 -0
  22. package/dist/types/chat/todo-reminder.d.ts +4 -1
  23. package/dist/types/chat/tool-execution.d.ts +25 -1
  24. package/dist/types/chat/transcript-actions.d.ts +37 -0
  25. package/dist/types/chat/transcript-entry.d.ts +3 -1
  26. package/dist/types/chat/user-message.d.ts +25 -0
  27. package/dist/types/chrome/collab-qrcode.d.ts +7 -0
  28. package/dist/types/chrome/context-thresholds.d.ts +11 -0
  29. package/dist/types/chrome/countdown-timer.d.ts +9 -3
  30. package/dist/types/chrome/diff.d.ts +12 -0
  31. package/dist/types/chrome/dynamic-border.d.ts +3 -0
  32. package/dist/types/chrome/format.d.ts +6 -0
  33. package/dist/types/chrome/message-divider.d.ts +13 -0
  34. package/dist/types/chrome/message-frame.d.ts +10 -0
  35. package/dist/types/chrome/message-notice.d.ts +23 -0
  36. package/dist/types/chrome/overlay-box.d.ts +11 -2
  37. package/dist/types/chrome/qrcode.d.ts +7 -0
  38. package/dist/types/chrome/segment-track.d.ts +7 -0
  39. package/dist/types/chrome/selector-helpers.d.ts +0 -6
  40. package/dist/types/chrome/status-notice.d.ts +14 -0
  41. package/dist/types/chrome/streaming-panel.d.ts +3 -0
  42. package/dist/types/chrome/tool-activity.d.ts +3 -0
  43. package/dist/types/chrome/transcript-container.d.ts +13 -0
  44. package/dist/types/chrome/transcript-status.d.ts +15 -3
  45. package/dist/types/components/box.d.ts +7 -0
  46. package/dist/types/components/disclosure.d.ts +9 -0
  47. package/dist/types/components/editor.d.ts +42 -0
  48. package/dist/types/components/form.d.ts +14 -0
  49. package/dist/types/components/image.d.ts +7 -0
  50. package/dist/types/components/input.d.ts +20 -1
  51. package/dist/types/components/key-value-list.d.ts +3 -0
  52. package/dist/types/components/layout/geometry.d.ts +16 -0
  53. package/dist/types/components/layout/row.d.ts +7 -0
  54. package/dist/types/components/layout/split-pane.d.ts +8 -0
  55. package/dist/types/components/layout/stack.d.ts +6 -0
  56. package/dist/types/components/loader.d.ts +48 -0
  57. package/dist/types/components/markdown.d.ts +8 -0
  58. package/dist/types/components/metric.d.ts +7 -0
  59. package/dist/types/components/progress-bar.d.ts +3 -0
  60. package/dist/types/components/scroll-view.d.ts +6 -0
  61. package/dist/types/components/section.d.ts +3 -0
  62. package/dist/types/components/select-list.d.ts +33 -0
  63. package/dist/types/components/settings-list.d.ts +68 -0
  64. package/dist/types/components/spacer.d.ts +4 -0
  65. package/dist/types/components/tab-bar.d.ts +9 -0
  66. package/dist/types/components/table.d.ts +7 -0
  67. package/dist/types/components/text.d.ts +6 -0
  68. package/dist/types/components/tree-view.d.ts +9 -0
  69. package/dist/types/components/truncated-text.d.ts +3 -0
  70. package/dist/types/components/wizard-step.d.ts +6 -0
  71. package/dist/types/hotkeys-markdown.d.ts +24 -0
  72. package/dist/types/index.d.ts +5 -0
  73. package/dist/types/latex-block.d.ts +7 -0
  74. package/dist/types/loop-watchdog.d.ts +9 -0
  75. package/dist/types/native/apply.d.ts +39 -0
  76. package/dist/types/native/backend.d.ts +94 -0
  77. package/dist/types/native/blobs.d.ts +29 -0
  78. package/dist/types/native/describe.d.ts +56 -0
  79. package/dist/types/native/encode.d.ts +50 -0
  80. package/dist/types/native/icons.d.ts +18 -0
  81. package/dist/types/native/memo.d.ts +27 -0
  82. package/dist/types/native/node.d.ts +107 -0
  83. package/dist/types/native/overlay.d.ts +64 -0
  84. package/dist/types/native/picker.d.ts +124 -0
  85. package/dist/types/native/reconcile.d.ts +41 -0
  86. package/dist/types/native/settle.d.ts +11 -0
  87. package/dist/types/native/spans.d.ts +21 -0
  88. package/dist/types/native/state.d.ts +6 -0
  89. package/dist/types/native/tone.d.ts +24 -0
  90. package/dist/types/overlays/advisor-config.d.ts +20 -0
  91. package/dist/types/overlays/agent-hub-renderer.d.ts +27 -11
  92. package/dist/types/overlays/agent-hub.d.ts +10 -0
  93. package/dist/types/overlays/agent-transcript-viewer.d.ts +7 -0
  94. package/dist/types/overlays/agents-hub.d.ts +8 -0
  95. package/dist/types/overlays/annotation-overlay.d.ts +3 -0
  96. package/dist/types/overlays/ask-dialog.d.ts +23 -0
  97. package/dist/types/overlays/bordered-loader.d.ts +3 -0
  98. package/dist/types/overlays/btw-history-panel.d.ts +9 -0
  99. package/dist/types/overlays/btw-panel.d.ts +3 -0
  100. package/dist/types/overlays/cleanse-panel.d.ts +5 -2
  101. package/dist/types/overlays/composer-shape-preview.d.ts +7 -1
  102. package/dist/types/overlays/copy-selector.d.ts +38 -0
  103. package/dist/types/overlays/error-banner.d.ts +9 -0
  104. package/dist/types/overlays/extensions/extension-list.d.ts +1 -0
  105. package/dist/types/overlays/history-search.d.ts +18 -0
  106. package/dist/types/overlays/hook-editor.d.ts +28 -1
  107. package/dist/types/overlays/hook-input.d.ts +3 -0
  108. package/dist/types/overlays/hook-selector.d.ts +10 -0
  109. package/dist/types/overlays/hub-frame.d.ts +18 -0
  110. package/dist/types/overlays/jobs-panel.d.ts +24 -0
  111. package/dist/types/overlays/login-dialog.d.ts +10 -0
  112. package/dist/types/overlays/logout-account-selector.d.ts +4 -0
  113. package/dist/types/overlays/mcp-add-wizard.d.ts +4 -0
  114. package/dist/types/overlays/model-browser.d.ts +69 -0
  115. package/dist/types/overlays/model-hub.d.ts +7 -0
  116. package/dist/types/overlays/model-picker.d.ts +9 -1
  117. package/dist/types/overlays/move-overlay.d.ts +3 -0
  118. package/dist/types/overlays/oauth-selector.d.ts +4 -0
  119. package/dist/types/overlays/omfg-panel.d.ts +3 -0
  120. package/dist/types/overlays/pause-screen.d.ts +12 -0
  121. package/dist/types/overlays/plan-review-overlay.d.ts +5 -0
  122. package/dist/types/overlays/plan-save-overlay.d.ts +10 -0
  123. package/dist/types/overlays/plugin-selector.d.ts +4 -0
  124. package/dist/types/overlays/plugin-settings.d.ts +30 -0
  125. package/dist/types/overlays/queue-mode-selector.d.ts +4 -0
  126. package/dist/types/overlays/reset-usage-selector.d.ts +8 -0
  127. package/dist/types/overlays/rewind-selector.d.ts +5 -0
  128. package/dist/types/overlays/session-info-overlay.d.ts +19 -1
  129. package/dist/types/overlays/session-selector.d.ts +64 -1
  130. package/dist/types/overlays/settings-defs.d.ts +14 -0
  131. package/dist/types/overlays/settings-selector.d.ts +21 -0
  132. package/dist/types/overlays/show-images-selector.d.ts +4 -0
  133. package/dist/types/overlays/snapcompact-shape-preview.d.ts +2 -0
  134. package/dist/types/overlays/stats-notice.d.ts +8 -0
  135. package/dist/types/overlays/theme-selector.d.ts +11 -0
  136. package/dist/types/overlays/thinking-selector.d.ts +4 -0
  137. package/dist/types/overlays/tree-selector.d.ts +60 -2
  138. package/dist/types/overlays/usage-dashboard.d.ts +26 -3
  139. package/dist/types/overlays/usage-row.d.ts +36 -2
  140. package/dist/types/prompt/attachment-chips.d.ts +8 -0
  141. package/dist/types/prompt/composer-attachments.d.ts +15 -3
  142. package/dist/types/prompt/composer.d.ts +18 -1
  143. package/dist/types/prompt/custom-editor.d.ts +48 -2
  144. package/dist/types/prompt/editor-top-gap.d.ts +2 -0
  145. package/dist/types/prompt/macos-spelling.d.ts +6 -0
  146. package/dist/types/prompt/magic-keywords.d.ts +9 -0
  147. package/dist/types/prompt/queued-messages.d.ts +23 -0
  148. package/dist/types/prompt/skill-tokens.d.ts +4 -2
  149. package/dist/types/prompt/welcome.d.ts +16 -0
  150. package/dist/types/render/code-cell.d.ts +10 -0
  151. package/dist/types/render/file-list.d.ts +8 -0
  152. package/dist/types/render/hyperlink.d.ts +14 -0
  153. package/dist/types/render/output-block.d.ts +43 -0
  154. package/dist/types/render/output-pane.d.ts +17 -0
  155. package/dist/types/render/tool-card.d.ts +8 -0
  156. package/dist/types/render/tree-list.d.ts +8 -0
  157. package/dist/types/render/width-aware-text.d.ts +7 -0
  158. package/dist/types/setup/scenes/outro.d.ts +7 -0
  159. package/dist/types/setup/scenes/sign-in.d.ts +16 -9
  160. package/dist/types/setup/scenes/splash.d.ts +8 -0
  161. package/dist/types/setup/scenes/types.d.ts +6 -27
  162. package/dist/types/setup/wizard-overlay.d.ts +9 -0
  163. package/dist/types/status-line/component.d.ts +49 -5
  164. package/dist/types/status-line/context-usage.d.ts +20 -0
  165. package/dist/types/status-line/footer.d.ts +8 -0
  166. package/dist/types/status-line/metrics.d.ts +1 -2
  167. package/dist/types/status-line/segments.d.ts +10 -1
  168. package/dist/types/status-line/types.d.ts +40 -1
  169. package/dist/types/stdin-buffer.d.ts +10 -7
  170. package/dist/types/terminal-capabilities.d.ts +1 -1
  171. package/dist/types/terminal.d.ts +36 -0
  172. package/dist/types/theme/defaults/index.d.ts +100 -0
  173. package/dist/types/theme/schema.d.ts +2 -0
  174. package/dist/types/theme/shimmer.d.ts +8 -0
  175. package/dist/types/theme/theme-class.d.ts +2 -0
  176. package/dist/types/theme/theme.d.ts +33 -1
  177. package/dist/types/tools/ask.d.ts +3 -0
  178. package/dist/types/tools/ast-edit.d.ts +10 -1
  179. package/dist/types/tools/ast-grep.d.ts +10 -1
  180. package/dist/types/tools/autoresearch.d.ts +21 -4
  181. package/dist/types/tools/bash.d.ts +30 -1
  182. package/dist/types/tools/cfg-render.d.ts +5 -1
  183. package/dist/types/tools/debug.d.ts +3 -1
  184. package/dist/types/tools/default-renderer.d.ts +6 -1
  185. package/dist/types/tools/edit.d.ts +14 -1
  186. package/dist/types/tools/eval.d.ts +12 -1
  187. package/dist/types/tools/fetch.d.ts +16 -1
  188. package/dist/types/tools/find.d.ts +15 -1
  189. package/dist/types/tools/github.d.ts +3 -0
  190. package/dist/types/tools/glob.d.ts +14 -1
  191. package/dist/types/tools/goal.d.ts +3 -1
  192. package/dist/types/tools/grep.d.ts +14 -1
  193. package/dist/types/tools/grouped-file-output.d.ts +58 -0
  194. package/dist/types/tools/json-tree.d.ts +20 -0
  195. package/dist/types/tools/lsp.d.ts +3 -10
  196. package/dist/types/tools/mcp.d.ts +16 -1
  197. package/dist/types/tools/memory.d.ts +26 -0
  198. package/dist/types/tools/native-view.d.ts +90 -0
  199. package/dist/types/tools/proc-render.d.ts +7 -1
  200. package/dist/types/tools/read.d.ts +23 -1
  201. package/dist/types/tools/renderer.d.ts +57 -0
  202. package/dist/types/tools/report-tool-issue.d.ts +3 -0
  203. package/dist/types/tools/resolve.d.ts +14 -1
  204. package/dist/types/tools/task.d.ts +3 -0
  205. package/dist/types/tools/think.d.ts +3 -0
  206. package/dist/types/tools/todo.d.ts +12 -0
  207. package/dist/types/tools/vibe.d.ts +3 -1
  208. package/dist/types/tools/wait.d.ts +3 -0
  209. package/dist/types/tools/web-search-types.d.ts +90 -0
  210. package/dist/types/tools/web-search.d.ts +9 -168
  211. package/dist/types/tools/write.d.ts +14 -1
  212. package/dist/types/tools/xdev.d.ts +13 -0
  213. package/dist/types/tui.d.ts +49 -0
  214. package/package.json +9 -9
  215. package/src/apps/autoresearch-dashboard.ts +362 -4
  216. package/src/apps/cleanse-board.ts +219 -1
  217. package/src/apps/cleanse-picker.ts +9 -1
  218. package/src/apps/debug/log-viewer.ts +15 -15
  219. package/src/apps/debug/protocol-probe.ts +2 -20
  220. package/src/apps/git/avatar.ts +2 -22
  221. package/src/apps/git/git-tui.ts +406 -59
  222. package/src/apps/git/help.ts +130 -0
  223. package/src/apps/git/sidebar.ts +491 -0
  224. package/src/apps/live-visualizer.ts +65 -0
  225. package/src/apps/ps-top.ts +448 -17
  226. package/src/apps/session-picker.ts +1 -0
  227. package/src/apps/setup-model-picker.ts +5 -1
  228. package/src/apps/standalone-picker.ts +55 -1
  229. package/src/autocomplete.ts +44 -1
  230. package/src/chat/advisor-message.ts +72 -3
  231. package/src/chat/assistant-message.ts +538 -51
  232. package/src/chat/background-tan-message.ts +4 -4
  233. package/src/chat/bash-execution.ts +81 -0
  234. package/src/chat/cache-invalidation-marker.ts +8 -0
  235. package/src/chat/chat-transcript-builder.ts +37 -6
  236. package/src/chat/collab-prompt-message.ts +15 -3
  237. package/src/chat/compaction-summary-message.ts +74 -1
  238. package/src/chat/custom-message.ts +3 -1
  239. package/src/chat/eval-execution.ts +57 -0
  240. package/src/chat/execution-shared.ts +153 -1
  241. package/src/chat/hook-message.ts +1 -0
  242. package/src/chat/late-diagnostics-message.ts +85 -2
  243. package/src/chat/read-tool-group.ts +341 -1
  244. package/src/chat/served-model-marker.ts +8 -0
  245. package/src/chat/skill-message.ts +63 -0
  246. package/src/chat/stripped-tool-calls-placeholder.ts +18 -10
  247. package/src/chat/thinking-display.ts +73 -11
  248. package/src/chat/todo-reminder.ts +34 -4
  249. package/src/chat/tool-execution.ts +385 -10
  250. package/src/chat/transcript-actions.ts +38 -0
  251. package/src/chat/transcript-browser.ts +2 -9
  252. package/src/chat/transcript-entry.ts +10 -1
  253. package/src/chat/transcript-render-helpers.ts +16 -15
  254. package/src/chat/ttsr-notification.ts +38 -1
  255. package/src/chat/user-message.ts +167 -4
  256. package/src/chrome/collab-qrcode.ts +27 -2
  257. package/src/chrome/context-thresholds.ts +34 -0
  258. package/src/chrome/countdown-timer.ts +24 -1
  259. package/src/chrome/diff.ts +60 -1
  260. package/src/chrome/dynamic-border.ts +7 -0
  261. package/src/chrome/format.ts +12 -0
  262. package/src/chrome/message-divider.ts +33 -1
  263. package/src/chrome/message-frame.ts +53 -10
  264. package/src/chrome/message-notice.ts +71 -0
  265. package/src/chrome/overlay-box.ts +45 -2
  266. package/src/chrome/qrcode.ts +28 -0
  267. package/src/chrome/segment-track.ts +26 -0
  268. package/src/chrome/selector-helpers.ts +1 -17
  269. package/src/chrome/status-notice.ts +46 -0
  270. package/src/chrome/streaming-panel.ts +25 -1
  271. package/src/chrome/tool-activity.ts +20 -0
  272. package/src/chrome/transcript-container.ts +139 -17
  273. package/src/chrome/transcript-status.ts +40 -11
  274. package/src/components/box.ts +51 -0
  275. package/src/components/disclosure.ts +33 -0
  276. package/src/components/editor.ts +239 -36
  277. package/src/components/form.ts +93 -9
  278. package/src/components/image.ts +30 -0
  279. package/src/components/input.ts +77 -26
  280. package/src/components/key-value-list.ts +18 -0
  281. package/src/components/layout/geometry.ts +44 -0
  282. package/src/components/layout/row.ts +30 -0
  283. package/src/components/layout/split-pane.ts +65 -0
  284. package/src/components/layout/stack.ts +27 -0
  285. package/src/components/loader.ts +183 -1
  286. package/src/components/markdown.ts +131 -56
  287. package/src/components/metric.ts +39 -0
  288. package/src/components/progress-bar.ts +28 -0
  289. package/src/components/scroll-view.ts +23 -0
  290. package/src/components/section.ts +16 -0
  291. package/src/components/select-list.ts +174 -25
  292. package/src/components/settings-list.ts +264 -7
  293. package/src/components/spacer.ts +17 -0
  294. package/src/components/tab-bar.ts +42 -0
  295. package/src/components/table.ts +34 -0
  296. package/src/components/text.ts +23 -0
  297. package/src/components/tree-view.ts +65 -0
  298. package/src/components/truncated-text.ts +14 -0
  299. package/src/components/wizard-step.ts +18 -9
  300. package/src/debug-server.ts +14 -7
  301. package/src/hotkeys-markdown.ts +251 -69
  302. package/src/index.ts +6 -0
  303. package/src/latex-block.ts +11 -0
  304. package/src/loop-watchdog.ts +38 -10
  305. package/src/native/apply.ts +275 -0
  306. package/src/native/backend.ts +682 -0
  307. package/src/native/blobs.ts +70 -0
  308. package/src/native/describe.ts +124 -0
  309. package/src/native/encode.ts +233 -0
  310. package/src/native/icons.ts +184 -0
  311. package/src/native/memo.ts +64 -0
  312. package/src/native/node.ts +112 -0
  313. package/src/native/overlay.ts +131 -0
  314. package/src/native/picker.ts +333 -0
  315. package/src/native/reconcile.ts +1063 -0
  316. package/src/native/settle.ts +23 -0
  317. package/src/native/spans.ts +308 -0
  318. package/src/native/state.ts +28 -0
  319. package/src/native/tone.ts +79 -0
  320. package/src/overlays/advisor-config.ts +420 -0
  321. package/src/overlays/agent-hub-renderer.ts +155 -18
  322. package/src/overlays/agent-hub.ts +1037 -44
  323. package/src/overlays/agent-transcript-viewer.ts +129 -0
  324. package/src/overlays/agents-hub.ts +688 -53
  325. package/src/overlays/annotation-overlay.ts +391 -52
  326. package/src/overlays/ask-dialog.ts +435 -24
  327. package/src/overlays/bordered-loader.ts +17 -0
  328. package/src/overlays/btw-history-panel.ts +182 -0
  329. package/src/overlays/btw-panel.ts +68 -2
  330. package/src/overlays/cleanse-panel.ts +105 -15
  331. package/src/overlays/codex-reset-fireworks.ts +65 -5
  332. package/src/overlays/composer-shape-preview.ts +26 -1
  333. package/src/overlays/copy-selector.ts +545 -23
  334. package/src/overlays/error-banner.ts +44 -0
  335. package/src/overlays/extensions/extension-list.ts +23 -17
  336. package/src/overlays/history-search.ts +175 -33
  337. package/src/overlays/hook-editor.ts +113 -6
  338. package/src/overlays/hook-input.ts +22 -1
  339. package/src/overlays/hook-selector.ts +311 -22
  340. package/src/overlays/hub-frame.ts +80 -0
  341. package/src/overlays/jobs-panel.ts +112 -0
  342. package/src/overlays/login-dialog.ts +183 -1
  343. package/src/overlays/logout-account-selector.ts +102 -4
  344. package/src/overlays/mcp-add-wizard.ts +232 -53
  345. package/src/overlays/model-browser.ts +681 -22
  346. package/src/overlays/model-hub.ts +1402 -78
  347. package/src/overlays/model-picker.ts +184 -7
  348. package/src/overlays/move-overlay.ts +45 -0
  349. package/src/overlays/oauth-selector.ts +227 -23
  350. package/src/overlays/omfg-panel.ts +86 -2
  351. package/src/overlays/pause-screen.ts +55 -4
  352. package/src/overlays/plan-review-overlay.ts +352 -0
  353. package/src/overlays/plan-save-overlay.ts +45 -0
  354. package/src/overlays/plugin-selector.ts +21 -1
  355. package/src/overlays/plugin-settings.ts +195 -5
  356. package/src/overlays/queue-mode-selector.ts +19 -1
  357. package/src/overlays/reset-usage-selector.ts +110 -40
  358. package/src/overlays/rewind-selector.ts +458 -44
  359. package/src/overlays/session-account-selector.ts +1 -1
  360. package/src/overlays/session-info-overlay.ts +137 -1
  361. package/src/overlays/session-selector.ts +807 -46
  362. package/src/overlays/settings-defs.ts +41 -0
  363. package/src/overlays/settings-selector.ts +585 -33
  364. package/src/overlays/show-images-selector.ts +18 -1
  365. package/src/overlays/snapcompact-shape-preview.ts +48 -3
  366. package/src/overlays/stats-notice.ts +31 -0
  367. package/src/overlays/theme-selector.ts +31 -1
  368. package/src/overlays/thinking-selector.ts +22 -1
  369. package/src/overlays/tree-selector.ts +570 -110
  370. package/src/overlays/usage-dashboard.ts +587 -12
  371. package/src/overlays/usage-row.ts +161 -25
  372. package/src/prompt/attachment-chips.ts +60 -0
  373. package/src/prompt/composer-attachments.ts +31 -9
  374. package/src/prompt/composer.ts +55 -2
  375. package/src/prompt/custom-editor.ts +302 -4
  376. package/src/prompt/editor-top-gap.ts +8 -0
  377. package/src/prompt/macos-spelling.ts +24 -9
  378. package/src/prompt/magic-keywords.ts +22 -1
  379. package/src/prompt/model-mention-autocomplete.ts +1 -0
  380. package/src/prompt/queued-messages.ts +101 -0
  381. package/src/prompt/skill-tokens.ts +5 -3
  382. package/src/prompt/welcome.ts +213 -6
  383. package/src/render/code-cell.ts +103 -2
  384. package/src/render/file-list.ts +29 -0
  385. package/src/render/hyperlink.ts +32 -0
  386. package/src/render/output-block.ts +110 -0
  387. package/src/render/output-pane.ts +37 -0
  388. package/src/render/tool-card.ts +137 -0
  389. package/src/render/tree-list.ts +39 -0
  390. package/src/render/width-aware-text.ts +20 -0
  391. package/src/setup/scenes/composer.ts +29 -0
  392. package/src/setup/scenes/glyph.ts +15 -0
  393. package/src/setup/scenes/model.ts +36 -8
  394. package/src/setup/scenes/outro.ts +28 -1
  395. package/src/setup/scenes/sign-in.ts +138 -24
  396. package/src/setup/scenes/splash.ts +28 -1
  397. package/src/setup/scenes/theme.ts +88 -3
  398. package/src/setup/scenes/types.ts +7 -28
  399. package/src/setup/startup-splash.ts +46 -9
  400. package/src/setup/wizard-overlay.ts +140 -23
  401. package/src/setup/wizard.ts +1 -1
  402. package/src/status-line/component.ts +414 -18
  403. package/src/status-line/context-usage.ts +279 -50
  404. package/src/status-line/footer.ts +94 -1
  405. package/src/status-line/metrics.ts +3 -6
  406. package/src/status-line/segments.ts +463 -97
  407. package/src/status-line/separators.ts +3 -16
  408. package/src/status-line/types.ts +43 -1
  409. package/src/stdin-buffer.ts +27 -21
  410. package/src/terminal-capabilities.ts +7 -0
  411. package/src/terminal.ts +153 -11
  412. package/src/theme/defaults/dark-neon-noir.json +100 -0
  413. package/src/theme/defaults/index.ts +2 -0
  414. package/src/theme/defaults/light-prism.json +5 -5
  415. package/src/theme/defaults/onyx.json +62 -62
  416. package/src/theme/mermaid-cache.ts +10 -1
  417. package/src/theme/schema.ts +15 -0
  418. package/src/theme/shimmer.ts +33 -0
  419. package/src/theme/theme-class.ts +24 -19
  420. package/src/theme/theme.ts +111 -9
  421. package/src/theme/tui-adapters.ts +14 -1
  422. package/src/tools/ask.ts +157 -1
  423. package/src/tools/ast-edit.ts +174 -1
  424. package/src/tools/ast-grep.ts +82 -2
  425. package/src/tools/autoresearch.ts +149 -1
  426. package/src/tools/bash.ts +233 -62
  427. package/src/tools/cfg-render.ts +66 -1
  428. package/src/tools/debug.ts +78 -1
  429. package/src/tools/default-renderer.ts +35 -1
  430. package/src/tools/edit.ts +357 -31
  431. package/src/tools/eval.ts +257 -1
  432. package/src/tools/fetch.ts +102 -3
  433. package/src/tools/find.ts +127 -1
  434. package/src/tools/github.ts +119 -10
  435. package/src/tools/glob.ts +116 -1
  436. package/src/tools/goal.ts +56 -1
  437. package/src/tools/grep.ts +83 -2
  438. package/src/tools/grouped-file-output.ts +205 -0
  439. package/src/tools/json-tree.ts +116 -1
  440. package/src/tools/lsp.ts +318 -1
  441. package/src/tools/mcp.ts +80 -1
  442. package/src/tools/memory.ts +93 -11
  443. package/src/tools/native-view.ts +292 -0
  444. package/src/tools/proc-render.ts +221 -1
  445. package/src/tools/read.ts +199 -4
  446. package/src/tools/renderer.ts +61 -0
  447. package/src/tools/report-tool-issue.ts +7 -1
  448. package/src/tools/resolve.ts +63 -1
  449. package/src/tools/streaming-output.ts +79 -16
  450. package/src/tools/task.ts +406 -4
  451. package/src/tools/think.ts +19 -1
  452. package/src/tools/todo.ts +89 -0
  453. package/src/tools/vibe.ts +230 -2
  454. package/src/tools/wait.ts +181 -1
  455. package/src/tools/web-search-types.ts +99 -0
  456. package/src/tools/web-search.ts +111 -170
  457. package/src/tools/write.ts +144 -5
  458. package/src/tools/xdev.ts +64 -1
  459. package/src/tui.ts +295 -4
  460. package/dist/types/setup/scenes/providers.d.ts +0 -3
  461. package/dist/types/setup/scenes/web-search.d.ts +0 -22
  462. package/src/setup/scenes/providers.ts +0 -110
  463. package/src/setup/scenes/web-search.ts +0 -165
@@ -5,11 +5,17 @@ import { ImageProtocol, TERMINAL } from "../terminal-capabilities";
5
5
  import { Markdown, type MarkdownTheme } from "../components/markdown";
6
6
  import { Spacer } from "../components/spacer";
7
7
  import { Text } from "../components/text";
8
- import { formatNumber } from "@oh-my-pi/pi-utils";
8
+ import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
9
9
  import chalk from "@oh-my-pi/pi-utils/chalk";
10
10
  import { LRUCache } from "@oh-my-pi/pi-utils/lru";
11
11
  import type { AssistantThinkingRenderer } from "./extension-types";
12
- import { ensureThemeSync, getMarkdownTheme, theme } from "../theme";
12
+ import { ensureThemeSync, getMarkdownTheme, getThemeEpoch, theme } from "../theme";
13
+ import { card, col, elapsed, node, span, text } from "../native/describe";
14
+ import { hasTranscriptActions, runTranscriptAction } from "./transcript-actions";
15
+ import type { NativeChild, NativeNode, NativeUiEvent } from "../native/node";
16
+ import { isNativeRendering } from "../native/state";
17
+ import { NativeImageCache } from "../native/blobs";
18
+ import { Memo } from "../native/memo";
13
19
  import { EMPTY_LINK_TARGETS, resolveImageOptions } from "../render/render-utils";
14
20
  import { WidthAwareText } from "../render";
15
21
  import { cachedPngConversion, convertImageToPngShared, imagePayloadKey } from "./image-loading";
@@ -20,6 +26,7 @@ import { formatErrorBlock } from "../chrome/error-block";
20
26
  import { type ServedModelMismatch, ServedModelMarkerComponent } from "./served-model-marker";
21
27
  import { isReactionTarget, type ReactionSplit, type ReactionTarget, splitReaction } from "./reaction";
22
28
  import { isRowPrefix, type TranscriptStableRow, trimBlankEdges } from "../chrome/transcript-container";
29
+ import { formatTurnUsage, type TurnUsageSummary } from "../overlays/usage-row";
23
30
 
24
31
  /**
25
32
  * Max wrapped rows of a turn-ending provider error rendered inline in the
@@ -31,9 +38,17 @@ import { isRowPrefix, type TranscriptStableRow, trimBlankEdges } from "../chrome
31
38
  const MAX_TRANSCRIPT_ERROR_ROWS = 8;
32
39
  const EMPTY_STABLE_RENDER: readonly string[] = [];
33
40
 
41
+ /** The native head of a finished thinking block: "Thought for 12s", or "Thought" when it was never seen streaming. */
42
+ function thoughtLabel(clock: { start: number; end?: number } | undefined): string {
43
+ if (clock?.end === undefined) return "Thought";
44
+ const ms = clock.end - clock.start;
45
+ return `Thought for ${ms < 60_000 ? `${Math.max(1, Math.round(ms / 1000))}s` : formatDuration(ms)}`;
46
+ }
47
+
34
48
  type ThinkingContentBlock = Extract<AssistantMessage["content"][number], { type: "thinking" }>;
35
49
  type DisplayThinkingContentBlock = ThinkingContentBlock & { rawThinking?: string };
36
- type StablePart = { kind: "thinking" | "text"; text: string } | { kind: "spacer" };
50
+ type StablePartKind = "thinking" | "text";
51
+ type StablePart = { kind: StablePartKind; text: string } | { kind: "spacer" };
37
52
 
38
53
  /**
39
54
  * One published prefix of the block's finished content. Later snapshots extend
@@ -47,6 +62,37 @@ interface StableSnapshot {
47
62
  readonly lastTextLength: number;
48
63
  }
49
64
 
65
+ /**
66
+ * Stable-row renders at one width. `rows` renders snapshot `newest`; every
67
+ * count in `ends` renders byte-identically to `rows.slice(0, end)` — checked
68
+ * against `rows` when recorded — so an earlier prefix is a slice of the newest
69
+ * render instead of a re-render, and one row array stays resident per width.
70
+ */
71
+ interface StableRowLedger {
72
+ newest: number;
73
+ rows: readonly string[];
74
+ readonly ends: Map<number, number>;
75
+ /** Rendered rows of parts a snapshot has closed (full text final), by part index. */
76
+ readonly parts: (
77
+ | { readonly kind: StablePartKind; readonly text: string; readonly rows: readonly string[] }
78
+ | undefined
79
+ )[];
80
+ }
81
+
82
+ /** One Markdown instance reused while a stable part's text grows between renders. */
83
+ interface StablePartRenderer {
84
+ readonly index: number;
85
+ readonly kind: StablePartKind;
86
+ readonly md: Markdown;
87
+ }
88
+
89
+ /** Theme inputs cached stable renders were produced with; any change drops them. */
90
+ interface StableRenderInputs {
91
+ readonly prose: MarkdownTheme;
92
+ readonly markdown: MarkdownTheme;
93
+ readonly color: ((text: string) => string) | undefined;
94
+ }
95
+
50
96
  function isSnapshotExtension(previous: readonly StablePart[], current: readonly StablePart[]): boolean {
51
97
  if (previous.length > current.length) return false;
52
98
  for (let index = 0; index < previous.length; index++) {
@@ -219,6 +265,12 @@ export class AssistantMessageComponent extends Container {
219
265
  * provider error whose tail would otherwise be unreachable in the live TUI.
220
266
  */
221
267
  #errorExpanded = false;
268
+ /** The deduplicated message the native error frame shows (its "Copy error" copies it). */
269
+ #errorText: string | undefined;
270
+ /** A TTSR rule aborted this text; the turn re-streams below it. */
271
+ #rewound = false;
272
+ /** The turn's totals when this answer ends it; a TSP terminal shows them under the answer. */
273
+ #turnUsage: TurnUsageSummary | undefined;
222
274
  /**
223
275
  * True when the current {@link updateContent} message carries a truncatable
224
276
  * inline provider error (the `#appendErrorBlock` path) — set whether or not
@@ -260,13 +312,20 @@ export class AssistantMessageComponent extends Container {
260
312
  #nextStableRowId = 0;
261
313
  #transcriptStableRows: TranscriptStableRow[] = [];
262
314
  /**
263
- * Rendered rows per published snapshot index and width. The container asks
264
- * for several different counts in one frame (emitted, offered end,
265
- * projected, pressure-loop +1s); a 2-entry LRU thrashes across those and
266
- * re-renders the whole prefix per miss. Sized generously so the live prefix
267
- * stays cached; cleared on reset/finalize like before.
315
+ * Verified stable-row renders per width ({@link StableRowLedger}). The
316
+ * container asks for several counts per frame (emitted, offered end,
317
+ * projected) and each publication re-checks the previous prefix; the ledger
318
+ * answers all of them from the newest render. A few widths cover resizes.
319
+ * Cleared on reset, finalize, and theme change.
268
320
  */
269
- #stableRenderCache = new LRUCache<string, readonly string[]>({ max: 64 });
321
+ #stableLedgers = new LRUCache<number, StableRowLedger>({ max: 4 });
322
+ /** Prefixes handed out as ledger slices or rendered off-ledger, by `${count}:${width}`. */
323
+ #stableRenderCache = new LRUCache<string, readonly string[]>({ max: 8 });
324
+ /** Reused for the growing final part of each publication candidate. */
325
+ #stableHeadRenderer: StablePartRenderer | undefined;
326
+ /** Reused for final parts of published snapshots rendered off-ledger (reflow, replay). */
327
+ #stableReplayRenderer: StablePartRenderer | undefined;
328
+ #stableRenderInputs: StableRenderInputs | undefined;
270
329
  /** Provider-reported tokens in the live thinking block — reasoning tokens when
271
330
  * the provider streams them, else total output — shown dimmed beside the
272
331
  * speed badge. 0 when no thinking is streaming. */
@@ -284,6 +343,19 @@ export class AssistantMessageComponent extends Container {
284
343
  #reactionTarget: ReactionTarget | undefined;
285
344
  /** Reaction lifted from the reply's opening emoji, once resolved. */
286
345
  #reaction: string | undefined;
346
+ /** Display form of {@link #lastMessage} (reaction handled) the native description is built from. */
347
+ #displayedMessage: AssistantMessage | undefined;
348
+ /** Thinking-extension components per content index, recorded when the slow path mounts them. */
349
+ #thinkingExtensions = new Map<number, Component[]>();
350
+ /** Collapse state of thinking sections toggled in the terminal, by content index; cleared by {@link setHideThinkingBlock}. */
351
+ #thinkingCollapsed = new Map<number, boolean>();
352
+ /** When each thinking block was seen streaming and when it stopped (native "Thought for 12s"), by content index. */
353
+ #thinkingClock = new Map<number, { start: number; end?: number; tokens: number }>();
354
+ #nativeViewVersion = 0;
355
+ readonly #native = new Memo();
356
+ /** Markdown nodes by key, reused while their text and streaming flag are unchanged. */
357
+ #nativeParts = new Map<string, { text: string; stream: boolean; node: NativeNode }>();
358
+ readonly #nativeImages = new NativeImageCache();
287
359
 
288
360
  setTextColorTransform(transform?: (text: string) => string): void {
289
361
  this.#textColorTransform = transform;
@@ -430,6 +502,12 @@ export class AssistantMessageComponent extends Container {
430
502
  this.#refreshMarkers();
431
503
  }
432
504
 
505
+ /** Mark this answer as the end of a turn with the turn's totals (native only: ANSI keeps them in the status line). */
506
+ setTurnUsage(summary: TurnUsageSummary | undefined): void {
507
+ this.#turnUsage = summary;
508
+ this.#nativeViewVersion++;
509
+ }
510
+
433
511
  /**
434
512
  * Show or clear the trailing served-model divider. Set once the turn's
435
513
  * signed thinking block (or router report) has named the model that actually
@@ -463,6 +541,7 @@ export class AssistantMessageComponent extends Container {
463
541
 
464
542
  setHideThinkingBlock(hide: boolean): void {
465
543
  this.#hideThinkingBlock = hide;
544
+ this.#thinkingCollapsed.clear();
466
545
  }
467
546
 
468
547
  /**
@@ -495,14 +574,30 @@ export class AssistantMessageComponent extends Container {
495
574
  * Once text starts, a tool call streams, or the block is sealed, the pulse ends.
496
575
  */
497
576
  #shouldAnimateThinking(message: AssistantMessage): boolean {
498
- if (!this.#hideThinkingBlock || this.#transcriptBlockFinalized) return false;
577
+ return this.#hideThinkingBlock && this.#thinkingTailIndex(message) !== undefined;
578
+ }
579
+
580
+ /**
581
+ * Content index of the thinking block the model is producing right now:
582
+ * the block is still streaming (not finalized), no tool call has started,
583
+ * and the tail visible block is thinking. Undefined otherwise.
584
+ */
585
+ #thinkingTailIndex(message: AssistantMessage): number | undefined {
586
+ if (this.#transcriptBlockFinalized) return undefined;
499
587
  let tail: "text" | "thinking" | undefined;
500
- for (const content of message.content) {
501
- if (content.type === "toolCall") return false;
502
- if (content.type === "text" && canonicalizeMessage(content.text)) tail = "text";
503
- else if (content.type === "thinking" && canonicalizeMessage(content.thinking)) tail = "thinking";
588
+ let tailIndex = -1;
589
+ for (let index = 0; index < message.content.length; index++) {
590
+ const content = message.content[index]!;
591
+ if (content.type === "toolCall") return undefined;
592
+ if (content.type === "text" && canonicalizeMessage(content.text)) {
593
+ tail = "text";
594
+ tailIndex = index;
595
+ } else if (content.type === "thinking" && canonicalizeMessage(content.thinking)) {
596
+ tail = "thinking";
597
+ tailIndex = index;
598
+ }
504
599
  }
505
- return tail === "thinking";
600
+ return tail === "thinking" ? tailIndex : undefined;
506
601
  }
507
602
 
508
603
  #thinkingDotsLabel(): string {
@@ -532,7 +627,8 @@ export class AssistantMessageComponent extends Container {
532
627
  }
533
628
 
534
629
  #startThinkingAnimation(): void {
535
- if (this.#thinkingDotsTimer) return;
630
+ // A native terminal clocks the described starburst itself.
631
+ if (this.#thinkingDotsTimer || isNativeRendering()) return;
536
632
  this.#scheduleThinkingFrame();
537
633
  }
538
634
 
@@ -553,7 +649,7 @@ export class AssistantMessageComponent extends Container {
553
649
 
554
650
  #advanceThinkingDots(): void {
555
651
  this.#thinkingDotsTimer = undefined;
556
- if (!this.#thinkingDots) {
652
+ if (!this.#thinkingDots || isNativeRendering()) {
557
653
  this.#stopThinkingAnimation();
558
654
  return;
559
655
  }
@@ -610,6 +706,276 @@ export class AssistantMessageComponent extends Container {
610
706
  return rows;
611
707
  }
612
708
 
709
+ /**
710
+ * A `col` (role `omp.assistant`) of `md` nodes keyed by content index, so
711
+ * streamed deltas reach the terminal as `text append` on the same node;
712
+ * the tail block carries `stream: true` until the message finalizes.
713
+ * Thinking blocks are quiet collapsible `section`s (collapsed per
714
+ * `hideThinkingBlock`): a muted "Thinking…" shimmer while live, then
715
+ * "Thought for 12s"; tokens and rate ride in the head's `title`.
716
+ */
717
+ override describe(): NativeNode {
718
+ const tail = this.#displayedMessage ? this.#thinkingTailIndex(this.#displayedMessage) : undefined;
719
+ const rate =
720
+ tail !== undefined && this.#lastUpdateTransient && this.#thinkingRateLive
721
+ ? Math.round(Math.min(SPEED_MAX, sharedSpeedTracker.getSpeed()) * 10) / 10
722
+ : 0;
723
+ const key = [
724
+ this.#blockVersion,
725
+ this.#transcriptBlockFinalized,
726
+ this.#hideThinkingBlock,
727
+ this.#showImages,
728
+ this.#showToolResultImages,
729
+ this.#errorExpanded,
730
+ this.#nativeViewVersion,
731
+ rate,
732
+ getThemeEpoch(),
733
+ ];
734
+ return this.#native.get(key, () => this.#describeMessage(tail, rate));
735
+ }
736
+
737
+ /**
738
+ * A TTSR rule rewound this partial answer: a native terminal dims it and
739
+ * tags it `↺ rewound` (the ANSI render leaves it as it stopped).
740
+ */
741
+ markRewound(): void {
742
+ if (this.#rewound) return;
743
+ this.#rewound = true;
744
+ this.#nativeViewVersion++;
745
+ }
746
+
747
+ /** Mirrors a thinking section collapsed or expanded in the terminal. */
748
+ handleNativeEvent(event: NativeUiEvent): void {
749
+ if (event.type === "action") {
750
+ this.#handleErrorAction(event.act);
751
+ return;
752
+ }
753
+ if (event.type !== "toggle" || !event.key.startsWith("k")) return;
754
+ const index = Number(event.key.slice(1));
755
+ if (!Number.isInteger(index)) return;
756
+ this.#thinkingCollapsed.set(index, event.collapsed);
757
+ this.#nativeViewVersion++;
758
+ }
759
+
760
+ #describeMessage(thinkingTail: number | undefined, rate: number): NativeNode {
761
+ const message = this.#displayedMessage;
762
+ const previousParts = this.#nativeParts;
763
+ const parts = new Map<string, { text: string; stream: boolean; node: NativeNode }>();
764
+ // `slot` identifies the node across rebuilds; `key` is its sibling key inside its parent.
765
+ const markdown = (slot: string, key: string, source: string, stream: boolean): NativeNode => {
766
+ const cached = previousParts.get(slot);
767
+ const entry =
768
+ cached?.text === source && cached.stream === stream
769
+ ? cached
770
+ : {
771
+ text: source,
772
+ stream,
773
+ node: node("md", stream ? { text: source, stream: true } : { text: source }, undefined, key),
774
+ };
775
+ parts.set(slot, entry);
776
+ return entry.node;
777
+ };
778
+ const children: NativeChild[] = [];
779
+ if (message) {
780
+ const live = !this.#transcriptBlockFinalized && this.#lastUpdateTransient;
781
+ let tailIndex = -1;
782
+ for (let index = 0; index < message.content.length; index++) {
783
+ const content = message.content[index]!;
784
+ if (
785
+ (content.type === "text" && canonicalizeMessage(content.text)) ||
786
+ (content.type === "thinking" && canonicalizeMessage(content.thinking))
787
+ ) {
788
+ tailIndex = index;
789
+ }
790
+ }
791
+ for (let index = 0; index < message.content.length; index++) {
792
+ const content = message.content[index]!;
793
+ const streaming = live && index === tailIndex;
794
+ if (content.type === "text" && canonicalizeMessage(content.text)) {
795
+ children.push(markdown(`t${index}`, `t${index}`, content.text.trim(), streaming));
796
+ } else if (content.type === "thinking") {
797
+ const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
798
+ if (!display.visible) continue;
799
+ const thinkingLive = streaming && thinkingTail === index;
800
+ const clock = this.#thinkingClock.get(index);
801
+ if (thinkingLive) {
802
+ if (clock) clock.tokens = this.#thinkingTokens || clock.tokens;
803
+ else this.#thinkingClock.set(index, { start: performance.now(), tokens: this.#thinkingTokens });
804
+ } else if (clock && clock.end === undefined) {
805
+ clock.end = performance.now();
806
+ }
807
+ const tokens = thinkingLive ? this.#thinkingTokens : (clock?.tokens ?? 0);
808
+ const title = tokens > 0 ? `${formatNumber(tokens)} tokens` : undefined;
809
+ // Live: starburst · "Thinking…" · ticking timer · tok/s. Done: "Thought for 12s".
810
+ const head = thinkingLive
811
+ ? node(
812
+ "row",
813
+ { gap: "sm", title },
814
+ [
815
+ node("spinner", { style: "starburst", role: "omp.thinking.spin" }),
816
+ text([span("Thinking…", "muted")]),
817
+ elapsed(performance.now() - (this.#thinkingClock.get(index)?.start ?? performance.now())),
818
+ ...(rate >= 0.05 ? [node("rate", { value: rate, unit: "tok/s" })] : []),
819
+ ],
820
+ "head",
821
+ )
822
+ : node("text", { spans: [span(thoughtLabel(clock), "muted")], title }, undefined, "head");
823
+ const body = markdown(`k${index}`, "body", display.text, streaming);
824
+ children.push(
825
+ node(
826
+ "section",
827
+ {
828
+ // `.live` while streaming: the body clamps to its tail under a fade.
829
+ // `.ghost` while thinking is hidden (Ctrl+T): only a faint "Thought for 12s" stays.
830
+ role: thinkingLive
831
+ ? "omp.thinking.live"
832
+ : this.#hideThinkingBlock
833
+ ? "omp.thinking.ghost"
834
+ : "omp.thinking",
835
+ collapsible: true,
836
+ // Open while it streams; a finished thought folds to its "Thought for 12s" line.
837
+ collapsed: this.#thinkingCollapsed.get(index) ?? (this.#hideThinkingBlock || !thinkingLive),
838
+ // Tern's fold head sums it into "Worked for 12s".
839
+ took: clock?.end === undefined ? undefined : Math.max(0, Math.round(clock.end - clock.start)),
840
+ },
841
+ [head, body],
842
+ `k${index}`,
843
+ ),
844
+ );
845
+ if (!this.#hideThinkingBlock) children.push(...(this.#thinkingExtensions.get(index) ?? []));
846
+ } else if (content.type === "image" && content.data && content.mimeType && this.#showImages) {
847
+ children.push(this.#nativeImages.get(`i${index}`, content.data, content.mimeType));
848
+ }
849
+ }
850
+ if (this.#showImages && this.#showToolResultImages) {
851
+ for (const [toolCallId, images] of this.#toolImagesByCallId) {
852
+ images.forEach((image, index) => {
853
+ children.push(this.#nativeImages.get(`r${toolCallId}:${index}`, image.data, image.mimeType));
854
+ });
855
+ }
856
+ }
857
+ const errorNode = this.#describeError(message);
858
+ if (errorNode) children.push(errorNode);
859
+ }
860
+ this.#nativeParts = parts;
861
+ children.push(...this.#markerSlot.children);
862
+ if (this.#rewound) {
863
+ children.push(
864
+ node(
865
+ "badge",
866
+ { text: "↺ rewound", tone: "muted", role: "omp.assistant.rewound-tag" },
867
+ undefined,
868
+ "rewound",
869
+ ),
870
+ );
871
+ }
872
+ if (this.#turnUsage) {
873
+ const usage = formatTurnUsage(this.#turnUsage);
874
+ children.push(
875
+ node(
876
+ "row",
877
+ { role: "omp.turn.usage", gap: "xs", align: "center", title: usage.title },
878
+ [
879
+ node("icon", { name: "time" }, undefined, "icon"),
880
+ node("text", { text: usage.text }, undefined, "text"),
881
+ ],
882
+ "turn-usage",
883
+ ),
884
+ );
885
+ }
886
+ return col(children, { role: this.#rewound ? "omp.assistant.rewound" : "omp.assistant" });
887
+ }
888
+
889
+ /**
890
+ * Turn-ending error, recovered-retry note or abort label. A failed request
891
+ * is one error frame (head: "Request failed" + the HTTP status chip; body:
892
+ * the message once; then Retry / Copy error / Switch model), and stays in
893
+ * the transcript: the pinned banner is ANSI-only. A recovered attempt is an
894
+ * inline row that discloses the original error.
895
+ */
896
+ #describeError(message: AssistantMessage): NativeNode | undefined {
897
+ const presentation = resolveAssistantErrorPresentation(message);
898
+ if (presentation.kind === "compact-recovered") {
899
+ const attempt = message.retryRecovery?.attempt ?? 1;
900
+ const body = [message.errorMessage?.trim(), presentation.text].filter(
901
+ (line, index, all): line is string => !!line && all.indexOf(line) === index,
902
+ );
903
+ return node(
904
+ "section",
905
+ {
906
+ role: "omp.assistant.recovered",
907
+ head: [span(`↻ Recovered after ${attempt} ${attempt === 1 ? "retry" : "retries"}`, "muted")],
908
+ collapsible: true,
909
+ collapsed: true,
910
+ },
911
+ [text([span(body.join("\n"), "mono")], { wrap: "word" })],
912
+ "error",
913
+ );
914
+ }
915
+ if (presentation.kind !== "full" || message.content.some(content => content.type === "toolCall"))
916
+ return undefined;
917
+ if (message.stopReason === "aborted") {
918
+ return text([span(presentation.text, "error")], { wrap: "word", key: "error", role: "omp.assistant.abort" });
919
+ }
920
+ const lines = presentation.text
921
+ .split("\n")
922
+ .map(line => line.trim())
923
+ .filter(line => line.length > 0);
924
+ // "500 upstream overloaded" → chip "500", then the message once: a
925
+ // line the next one repeats with more detail is dropped.
926
+ const code = /^(\d{3})\s+/.exec(lines[0] ?? "");
927
+ if (code) lines[0] = lines[0]!.slice(code[0].length);
928
+ const detail = lines.filter((line, index) => !lines.slice(index + 1).some(next => next.startsWith(line)));
929
+ const errorText = detail.join("\n") || "Unknown error";
930
+ const children: NativeChild[] = [
931
+ node(
932
+ "row",
933
+ { gap: "sm" },
934
+ [
935
+ text([span("Request failed", "error strong")]),
936
+ ...(code ? [node("badge", { text: code[1]!, tone: "error", role: "omp.error.code" })] : []),
937
+ ],
938
+ "head",
939
+ ),
940
+ text([span(errorText, "mono")], {
941
+ wrap: "word",
942
+ lines: this.#errorExpanded ? undefined : MAX_TRANSCRIPT_ERROR_ROWS,
943
+ role: "omp.error.message",
944
+ key: "message",
945
+ }),
946
+ ];
947
+ if (message.stopReason === "error" && hasTranscriptActions()) {
948
+ const button = (act: string, label: string, keys: readonly string[], title: string): NativeNode =>
949
+ node(
950
+ "row",
951
+ { gap: "xs", role: "omp.error.action", actions: { click: act }, title },
952
+ keys.length > 0 ? [text(label), node("kbd", { keys })] : [text(label)],
953
+ act,
954
+ );
955
+ children.push(
956
+ node(
957
+ "row",
958
+ { gap: "sm", role: "omp.error.actions" },
959
+ [
960
+ button("retry", "Retry", ["F5"], "Retry the failed turn"),
961
+ button("copy-error", "Copy error", [], "Copy the error message"),
962
+ button("switch-model", "Switch model", ["⌥P"], "Pick another model for this session"),
963
+ ],
964
+ "actions",
965
+ ),
966
+ );
967
+ }
968
+ this.#errorText = errorText;
969
+ return card({ role: "omp.error", tone: "error", key: "error" }, children);
970
+ }
971
+
972
+ /** Error frame action clicks: omp's own retry, clipboard and model-picker paths. */
973
+ #handleErrorAction(act: string): void {
974
+ if (act === "retry") runTranscriptAction({ act: "retry" });
975
+ else if (act === "switch-model") runTranscriptAction({ act: "switch-model" });
976
+ else if (act === "copy-error" && this.#errorText) runTranscriptAction({ act: "copy", text: this.#errorText });
977
+ }
978
+
613
979
  /** Width-independent stable identities for the streamed leading thinking run. */
614
980
  getTranscriptStableRows(): readonly TranscriptStableRow[] {
615
981
  return this.#transcriptStableRows;
@@ -626,22 +992,33 @@ export class AssistantMessageComponent extends Container {
626
992
  this.#stableSnapshots = [];
627
993
  this.#stableParts = [];
628
994
  this.#transcriptStableRows = [];
629
- this.#stableRenderCache.clear();
995
+ this.#dropStableRenders();
630
996
  }
631
997
 
632
998
  renderTranscriptStableRows(count: number, width: number): readonly string[] {
633
999
  const index = Math.min(Math.trunc(count), this.#stableSnapshots.length);
634
1000
  if (index <= 0) return EMPTY_STABLE_RENDER;
1001
+ this.#syncStableRenderInputs();
1002
+ const ledger = this.#stableLedgers.get(width);
1003
+ if (ledger?.newest === index) return ledger.rows;
635
1004
  const key = `${index}:${width}`;
636
1005
  const cached = this.#stableRenderCache.get(key);
637
1006
  if (cached) return cached;
638
- const snapshot = this.#stableSnapshots[index - 1]!;
639
- const parts = this.#stableParts.slice(0, snapshot.partCount);
640
- const last = parts.at(-1);
641
- if (last && last.kind !== "spacer") {
642
- parts[parts.length - 1] = { kind: last.kind, text: last.text.slice(0, snapshot.lastTextLength) };
1007
+ const end = ledger?.ends.get(index);
1008
+ let rows: readonly string[];
1009
+ if (ledger !== undefined && end !== undefined) {
1010
+ rows = ledger.rows.slice(0, end);
1011
+ } else {
1012
+ const snapshot = this.#stableSnapshots[index - 1]!;
1013
+ rows = this.#renderStableParts(
1014
+ this.#stableParts,
1015
+ snapshot.partCount,
1016
+ snapshot.lastTextLength,
1017
+ width,
1018
+ "replay",
1019
+ );
1020
+ if (this.#recordStableRows(index, width, rows)) return rows;
643
1021
  }
644
- const rows = this.#renderStableSnapshot(parts, width);
645
1022
  this.#stableRenderCache.set(key, rows);
646
1023
  return rows;
647
1024
  }
@@ -662,9 +1039,12 @@ export class AssistantMessageComponent extends Container {
662
1039
  if (!last || last.kind === "spacer") return;
663
1040
  const snapshot = { partCount: parts.length, lastTextLength: last.text.length };
664
1041
  const previous = this.#stableSnapshots.at(-1);
665
- if (previous && !isSnapshotExtension(this.#stableParts, parts)) return;
1042
+ // An unmoved boundary publishes nothing whether or not it still extends
1043
+ // the last snapshot, so most frames skip the whole-document comparison.
666
1044
  if (previous?.partCount === snapshot.partCount && previous.lastTextLength === snapshot.lastTextLength) return;
667
- const currentRows = this.#renderStableSnapshot(parts, width);
1045
+ if (previous && !isSnapshotExtension(this.#stableParts, parts)) return;
1046
+ this.#syncStableRenderInputs();
1047
+ const currentRows = this.#renderStableParts(parts, parts.length, last.text.length, width, "head");
668
1048
  // The container verifies stable rows against the blank-trimmed render.
669
1049
  if (!isRowPrefix(currentRows, trimBlankEdges(rendered))) return;
670
1050
  const previousRows = previous
@@ -676,7 +1056,7 @@ export class AssistantMessageComponent extends Container {
676
1056
  this.#stableParts = parts;
677
1057
  this.#stableSnapshots.push(snapshot);
678
1058
  this.#transcriptStableRows.push({ key: `thinking:${this.#nextStableRowId++}` });
679
- this.#stableRenderCache.set(`${this.#stableSnapshots.length}:${width}`, currentRows);
1059
+ this.#recordStableRows(this.#stableSnapshots.length, width, currentRows);
680
1060
  }
681
1061
 
682
1062
  /**
@@ -726,37 +1106,136 @@ export class AssistantMessageComponent extends Container {
726
1106
  return parts;
727
1107
  }
728
1108
 
729
- #renderStableSnapshot(parts: readonly StablePart[], width: number): readonly string[] {
1109
+ /**
1110
+ * Render the first `partCount` stable parts, the final one cut to
1111
+ * `lastLength`. Rows match fresh Markdown renders of each part byte for
1112
+ * byte — {@link #createStableMarkdown} mirrors the live children — but a
1113
+ * closed part renders once per width, and the growing final part reuses one
1114
+ * Markdown instance so its already-frozen blocks are not re-lexed.
1115
+ */
1116
+ #renderStableParts(
1117
+ parts: readonly StablePart[],
1118
+ partCount: number,
1119
+ lastLength: number,
1120
+ width: number,
1121
+ role: "head" | "replay",
1122
+ ): readonly string[] {
1123
+ const ledger = this.#stableLedger(width);
730
1124
  const rows: string[] = [];
731
- for (const part of parts) {
1125
+ const lastIndex = partCount - 1;
1126
+ for (let index = 0; index < partCount; index++) {
1127
+ const part = parts[index]!;
732
1128
  if (part.kind === "spacer") {
733
1129
  rows.push("");
734
1130
  continue;
735
1131
  }
736
- // Constructor args mirror the live child Markdown exactly so these
737
- // rows are byte-identical to the block render's prefix — including the
738
- // trim the live children apply, which drops the trailing blank line a
739
- // frozen prefix still carries.
740
- const text = part.text.trim();
741
- const markdown =
742
- part.kind === "text"
743
- ? new Markdown(
744
- text,
745
- 1,
746
- 0,
747
- this.#getProseTheme(),
748
- this.#textColorTransform ? { color: this.#textColorTransform } : undefined,
749
- 0,
750
- )
751
- : new Markdown(text, 1, 0, getMarkdownTheme(), {
752
- color: (value: string) => theme.fg("thinkingText", value),
753
- italic: true,
754
- });
755
- rows.push(...markdown.render(width));
1132
+ const closed = index < lastIndex;
1133
+ const text = closed || lastLength === part.text.length ? part.text : part.text.slice(0, lastLength);
1134
+ const cached = ledger.parts[index];
1135
+ let partRows: readonly string[];
1136
+ if (cached?.kind === part.kind && cached.text === text) {
1137
+ partRows = cached.rows;
1138
+ } else {
1139
+ partRows = this.#renderStablePart(index, part.kind, text, width, closed ? undefined : role);
1140
+ if (closed) ledger.parts[index] = { kind: part.kind, text, rows: partRows };
1141
+ }
1142
+ for (const row of partRows) rows.push(row);
756
1143
  }
757
1144
  return rows;
758
1145
  }
759
1146
 
1147
+ /**
1148
+ * Render one part's Markdown. `role` names the instance reused for a final
1149
+ * part as its text grows; a closed part continues whichever instance was
1150
+ * already growing it, else renders once. Finalized blocks keep no instance.
1151
+ */
1152
+ #renderStablePart(
1153
+ index: number,
1154
+ kind: StablePartKind,
1155
+ text: string,
1156
+ width: number,
1157
+ role: "head" | "replay" | undefined,
1158
+ ): readonly string[] {
1159
+ // Trim like the live children, dropping the trailing blank line a frozen
1160
+ // prefix still carries.
1161
+ const trimmed = text.trim();
1162
+ if (this.#transcriptBlockFinalized) return this.#createStableMarkdown(kind, trimmed).render(width);
1163
+ const head = this.#stableHeadRenderer;
1164
+ const replay = this.#stableReplayRenderer;
1165
+ const renderer = role === "head" ? head : role === "replay" ? replay : head?.index === index ? head : replay;
1166
+ if (renderer?.index === index && renderer.kind === kind) {
1167
+ renderer.md.setText(trimmed);
1168
+ return renderer.md.render(width);
1169
+ }
1170
+ const md = this.#createStableMarkdown(kind, trimmed);
1171
+ if (role === "head") this.#stableHeadRenderer = { index, kind, md };
1172
+ else if (role === "replay") this.#stableReplayRenderer = { index, kind, md };
1173
+ return md.render(width);
1174
+ }
1175
+
1176
+ /** Constructor args mirror the live child Markdown so stable rows prefix the block render. */
1177
+ #createStableMarkdown(kind: StablePartKind, text: string): Markdown {
1178
+ return kind === "text"
1179
+ ? new Markdown(
1180
+ text,
1181
+ 1,
1182
+ 0,
1183
+ this.#getProseTheme(),
1184
+ this.#textColorTransform ? { color: this.#textColorTransform } : undefined,
1185
+ 0,
1186
+ )
1187
+ : new Markdown(text, 1, 0, getMarkdownTheme(), {
1188
+ color: (value: string) => theme.fg("thinkingText", value),
1189
+ italic: true,
1190
+ });
1191
+ }
1192
+
1193
+ #stableLedger(width: number): StableRowLedger {
1194
+ let ledger = this.#stableLedgers.get(width);
1195
+ if (ledger === undefined) {
1196
+ ledger = { newest: 0, rows: EMPTY_STABLE_RENDER, ends: new Map(), parts: [] };
1197
+ this.#stableLedgers.set(width, ledger);
1198
+ }
1199
+ return ledger;
1200
+ }
1201
+
1202
+ /**
1203
+ * Remember `rows` as snapshot `index`'s render at `width`. A newer snapshot
1204
+ * becomes the ledger's newest render — keeping earlier counts only when it
1205
+ * extends their rows; an older one that prefixes the newest keeps just its
1206
+ * row count. Returns whether `rows` is now the newest render.
1207
+ */
1208
+ #recordStableRows(index: number, width: number, rows: readonly string[]): boolean {
1209
+ const ledger = this.#stableLedger(width);
1210
+ if (index > ledger.newest) {
1211
+ if (!isRowPrefix(ledger.rows, rows)) ledger.ends.clear();
1212
+ ledger.newest = index;
1213
+ ledger.rows = rows;
1214
+ ledger.ends.set(index, rows.length);
1215
+ return true;
1216
+ }
1217
+ if (index < ledger.newest && isRowPrefix(rows, ledger.rows)) ledger.ends.set(index, rows.length);
1218
+ return false;
1219
+ }
1220
+
1221
+ /** Drop cached stable renders once the themes they were rendered with change. */
1222
+ #syncStableRenderInputs(): void {
1223
+ const prose = this.#getProseTheme();
1224
+ const markdown = getMarkdownTheme();
1225
+ const color = this.#textColorTransform;
1226
+ const inputs = this.#stableRenderInputs;
1227
+ if (inputs?.prose === prose && inputs.markdown === markdown && inputs.color === color) return;
1228
+ this.#dropStableRenders();
1229
+ this.#stableRenderInputs = { prose, markdown, color };
1230
+ }
1231
+
1232
+ #dropStableRenders(): void {
1233
+ this.#stableLedgers.clear();
1234
+ this.#stableRenderCache.clear();
1235
+ this.#stableHeadRenderer = undefined;
1236
+ this.#stableReplayRenderer = undefined;
1237
+ }
1238
+
760
1239
  /** Render completed prose rather than an earlier thinking row under emergency viewport pressure. */
761
1240
  renderTranscriptBlockEmergencyRow(width: number): string | undefined {
762
1241
  if (!this.#transcriptBlockFinalized) return undefined;
@@ -769,7 +1248,7 @@ export class AssistantMessageComponent extends Container {
769
1248
 
770
1249
  markTranscriptBlockFinalized(): void {
771
1250
  this.#transcriptBlockFinalized = true;
772
- this.#stableRenderCache.clear();
1251
+ this.#dropStableRenders();
773
1252
  this.#stopThinkingAnimation();
774
1253
  // If the live pulse was on screen when the block sealed, drop the fast path
775
1254
  // and rebuild so the placeholder is removed — finalized blocks never animate.
@@ -914,6 +1393,8 @@ export class AssistantMessageComponent extends Container {
914
1393
  }
915
1394
 
916
1395
  #appendThinkingExtensions(contentIndex: number, thinkingIndex: number, text: string): void {
1396
+ const mounted: Component[] = [];
1397
+ this.#thinkingExtensions.set(contentIndex, mounted);
917
1398
  for (const renderer of this.#thinkingRenderers) {
918
1399
  try {
919
1400
  const component = renderer(
@@ -927,6 +1408,7 @@ export class AssistantMessageComponent extends Container {
927
1408
  );
928
1409
  if (component) {
929
1410
  this.#contentContainer.addChild(component);
1411
+ mounted.push(component);
930
1412
  }
931
1413
  } catch {
932
1414
  // Ignore extension renderer failures and keep the original thinking block visible.
@@ -1046,6 +1528,7 @@ export class AssistantMessageComponent extends Container {
1046
1528
  // Everything below renders the display form; #lastMessage keeps the
1047
1529
  // verbatim message so re-renders re-derive the reaction deterministically.
1048
1530
  message = this.#displayMessage(message, this.#lastUpdateTransient);
1531
+ this.#displayedMessage = message;
1049
1532
 
1050
1533
  // Streaming-speed gauge: only a live, in-flight render of the single
1051
1534
  // animating hidden-thinking block feeds the shared session tracker. The
@@ -1059,7 +1542,10 @@ export class AssistantMessageComponent extends Container {
1059
1542
  // the gauge and pollute the next block. Providers that report usage only at
1060
1543
  // turn end leave the live count flat, so the rate stays 0 and the badge
1061
1544
  // self-suppresses (see #thinkingDotsLabel).
1062
- const isThinkingNow = this.#lastUpdateTransient && this.#shouldAnimateThinking(message);
1545
+ // Native terminals show the live rate on visible thinking too, not only the hidden pulse.
1546
+ const isThinkingNow =
1547
+ this.#lastUpdateTransient &&
1548
+ (isNativeRendering() ? this.#thinkingTailIndex(message) !== undefined : this.#shouldAnimateThinking(message));
1063
1549
  if (isThinkingNow) {
1064
1550
  const currentTokens = message.usage.reasoningTokens ?? message.usage.output;
1065
1551
  this.#thinkingTokens = currentTokens;
@@ -1088,6 +1574,7 @@ export class AssistantMessageComponent extends Container {
1088
1574
 
1089
1575
  // Clear content container
1090
1576
  this.#contentContainer.clear();
1577
+ this.#thinkingExtensions.clear();
1091
1578
  this.#emergencyText = undefined;
1092
1579
  this.#thinkingDots = undefined;
1093
1580
  this.#hasTruncatableError = false;