@linxiraos/pi-tui 1.1.26 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (423) hide show
  1. package/CHANGELOG.md +51 -491
  2. package/README.md +13 -7
  3. package/dist/types/active-terminal.d.ts +30 -0
  4. package/dist/types/app-keybindings.d.ts +7 -2
  5. package/dist/types/apps/cleanse-board.d.ts +1 -1
  6. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  7. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  8. package/dist/types/apps/git/git-tui.d.ts +5 -4
  9. package/dist/types/apps/git/help.d.ts +3 -1
  10. package/dist/types/apps/if-bench-board.d.ts +2 -2
  11. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  12. package/dist/types/autocomplete.d.ts +5 -2
  13. package/dist/types/charts/chart-plan.d.ts +674 -0
  14. package/dist/types/charts/chart-svg.d.ts +59 -0
  15. package/dist/types/charts/table-data.d.ts +175 -0
  16. package/dist/types/chat/assistant-message.d.ts +11 -1
  17. package/dist/types/chat/chat-transcript-builder.d.ts +3 -0
  18. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  19. package/dist/types/chat/execution-shared.d.ts +6 -5
  20. package/dist/types/chat/fence-figure.d.ts +42 -0
  21. package/dist/types/chat/figure-markdown.d.ts +42 -0
  22. package/dist/types/chat/messages.d.ts +1 -1
  23. package/dist/types/chat/svg-figure.d.ts +33 -0
  24. package/dist/types/chat/svg-source.d.ts +49 -0
  25. package/dist/types/chat/table-chart.d.ts +89 -0
  26. package/dist/types/chat/tool-execution.d.ts +2 -0
  27. package/dist/types/chat/user-message.d.ts +17 -4
  28. package/dist/types/chrome/shared.d.ts +9 -0
  29. package/dist/types/chrome/status-notice.d.ts +15 -3
  30. package/dist/types/chrome/transcript-container.d.ts +6 -0
  31. package/dist/types/components/box.d.ts +1 -0
  32. package/dist/types/components/disclosure.d.ts +1 -0
  33. package/dist/types/components/editor.d.ts +23 -5
  34. package/dist/types/components/form.d.ts +1 -0
  35. package/dist/types/components/image.d.ts +26 -44
  36. package/dist/types/components/input.d.ts +2 -1
  37. package/dist/types/components/key-value-list.d.ts +1 -0
  38. package/dist/types/components/layout/row.d.ts +1 -0
  39. package/dist/types/components/layout/stack.d.ts +1 -0
  40. package/dist/types/components/loader.d.ts +5 -6
  41. package/dist/types/components/markdown.d.ts +47 -5
  42. package/dist/types/components/scroll-view.d.ts +1 -0
  43. package/dist/types/components/section.d.ts +1 -0
  44. package/dist/types/components/table.d.ts +1 -0
  45. package/dist/types/components/text.d.ts +1 -0
  46. package/dist/types/components/tree-view.d.ts +7 -0
  47. package/dist/types/components/truncated-text.d.ts +1 -0
  48. package/dist/types/fuzzy.d.ts +29 -1
  49. package/dist/types/key-hint-format.d.ts +9 -1
  50. package/dist/types/keybinding-matchers.d.ts +3 -0
  51. package/dist/types/keybindings.d.ts +1 -1
  52. package/dist/types/kitty-graphics.d.ts +2 -2
  53. package/dist/types/loop-watchdog.d.ts +8 -7
  54. package/dist/types/multiplexers/cmux.d.ts +16 -0
  55. package/dist/types/multiplexers/herdr.d.ts +26 -0
  56. package/dist/types/multiplexers/notify.d.ts +13 -0
  57. package/dist/types/multiplexers/orca.d.ts +12 -0
  58. package/dist/types/multiplexers/screen.d.ts +9 -0
  59. package/dist/types/multiplexers/tmux.d.ts +29 -0
  60. package/dist/types/multiplexers/types.d.ts +100 -0
  61. package/dist/types/multiplexers/wmux.d.ts +12 -0
  62. package/dist/types/multiplexers/zellij.d.ts +12 -0
  63. package/dist/types/native/apply.d.ts +4 -3
  64. package/dist/types/native/backend.d.ts +5 -4
  65. package/dist/types/native/blobs.d.ts +7 -3
  66. package/dist/types/native/encode.d.ts +8 -6
  67. package/dist/types/native/node.d.ts +36 -5
  68. package/dist/types/native/overlay.d.ts +8 -3
  69. package/dist/types/native/settle.d.ts +4 -3
  70. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  71. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  72. package/dist/types/overlays/agent-hub.d.ts +3 -0
  73. package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -2
  74. package/dist/types/overlays/agents-hub.d.ts +9 -0
  75. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  76. package/dist/types/overlays/annotation-types.d.ts +2 -0
  77. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  78. package/dist/types/overlays/btw-history-panel.d.ts +13 -1
  79. package/dist/types/overlays/copy-selector.d.ts +1 -0
  80. package/dist/types/overlays/history-search.d.ts +10 -0
  81. package/dist/types/overlays/hook-selector.d.ts +9 -4
  82. package/dist/types/overlays/hub-frame.d.ts +4 -1
  83. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  84. package/dist/types/overlays/model-browser.d.ts +54 -7
  85. package/dist/types/overlays/model-hub.d.ts +37 -0
  86. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  87. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  88. package/dist/types/overlays/report-panel.d.ts +68 -0
  89. package/dist/types/overlays/rewind-selector.d.ts +1 -0
  90. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  91. package/dist/types/overlays/session-selector.d.ts +6 -2
  92. package/dist/types/overlays/settings-defs.d.ts +1 -0
  93. package/dist/types/overlays/thinking-selector.d.ts +2 -2
  94. package/dist/types/overlays/usage-dashboard.d.ts +12 -8
  95. package/dist/types/overlays/usage-display.d.ts +3 -1
  96. package/dist/types/prompt/composer-cache.d.ts +0 -16
  97. package/dist/types/prompt/composer.d.ts +18 -3
  98. package/dist/types/prompt/custom-editor.d.ts +25 -13
  99. package/dist/types/prompt/image-source.d.ts +2 -1
  100. package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
  101. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  102. package/dist/types/prompt/welcome.d.ts +7 -2
  103. package/dist/types/render/composite.d.ts +2 -0
  104. package/dist/types/render/output-pane.d.ts +1 -0
  105. package/dist/types/render/render-utils.d.ts +1 -0
  106. package/dist/types/render/tool-card.d.ts +6 -0
  107. package/dist/types/render/width-aware-text.d.ts +2 -0
  108. package/dist/types/setup/scenes/types.d.ts +1 -0
  109. package/dist/types/space-hold.d.ts +40 -25
  110. package/dist/types/status-line/component.d.ts +3 -0
  111. package/dist/types/status-line/context-usage.d.ts +7 -5
  112. package/dist/types/status-line/host.d.ts +4 -1
  113. package/dist/types/status-line/segments.d.ts +2 -0
  114. package/dist/types/status-line/types.d.ts +14 -3
  115. package/dist/types/terminal-capabilities.d.ts +41 -36
  116. package/dist/types/terminal-multiplexer.d.ts +127 -14
  117. package/dist/types/terminal.d.ts +23 -16
  118. package/dist/types/theme/active-symbols.d.ts +1 -1
  119. package/dist/types/theme/schema.d.ts +11 -2
  120. package/dist/types/theme/symbols.d.ts +1 -1
  121. package/dist/types/theme/theme-class.d.ts +7 -0
  122. package/dist/types/theme/theme.d.ts +1 -1
  123. package/dist/types/theme/tui-adapters.d.ts +3 -0
  124. package/dist/types/thinking.d.ts +1 -1
  125. package/dist/types/tmux.d.ts +11 -4
  126. package/dist/types/tools/bash.d.ts +7 -2
  127. package/dist/types/tools/daemon.d.ts +1 -1
  128. package/dist/types/tools/eval.d.ts +22 -3
  129. package/dist/types/tools/glob.d.ts +4 -0
  130. package/dist/types/tools/native-view.d.ts +47 -0
  131. package/dist/types/tools/output-meta.d.ts +11 -1
  132. package/dist/types/tools/renderer.d.ts +37 -2
  133. package/dist/types/tools/streaming-output.d.ts +20 -1
  134. package/dist/types/tools/task.d.ts +2 -3
  135. package/dist/types/tools/write.d.ts +9 -2
  136. package/dist/types/tools/yield.d.ts +22 -0
  137. package/dist/types/tui.d.ts +36 -0
  138. package/dist/types/utils.d.ts +6 -0
  139. package/dist/types/vim.d.ts +19 -5
  140. package/dist/types/windows-input-mode.d.ts +33 -1
  141. package/package.json +13 -9
  142. package/src/active-terminal.d.ts +30 -0
  143. package/src/active-terminal.ts +80 -0
  144. package/src/app-keybindings.d.ts +7 -2
  145. package/src/app-keybindings.ts +7 -2
  146. package/src/apps/autoresearch-dashboard.ts +7 -3
  147. package/src/apps/cleanse-board.ts +2 -2
  148. package/src/apps/cleanse-picker.ts +1 -1
  149. package/src/apps/debug/log-viewer.ts +188 -72
  150. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  151. package/src/apps/debug/raw-sse.ts +39 -5
  152. package/src/apps/git/diff-pane.ts +96 -23
  153. package/src/apps/git/git-tui.ts +23 -16
  154. package/src/apps/git/help.ts +29 -20
  155. package/src/apps/git/sidebar.ts +8 -3
  156. package/src/apps/if-bench-board.ts +2 -2
  157. package/src/apps/ps-top.ts +2 -2
  158. package/src/apps/setup-model-picker.ts +1 -1
  159. package/src/autocomplete.d.ts +5 -2
  160. package/src/autocomplete.ts +67 -43
  161. package/src/bracketed-paste.ts +10 -3
  162. package/src/charts/chart-plan.d.ts +674 -0
  163. package/src/charts/chart-plan.ts +5081 -0
  164. package/src/charts/chart-svg.d.ts +59 -0
  165. package/src/charts/chart-svg.ts +4657 -0
  166. package/src/charts/table-data.d.ts +175 -0
  167. package/src/charts/table-data.ts +1478 -0
  168. package/src/chat/advisor-message.ts +11 -0
  169. package/src/chat/assistant-message.d.ts +11 -1
  170. package/src/chat/assistant-message.ts +245 -54
  171. package/src/chat/chat-transcript-builder.d.ts +3 -0
  172. package/src/chat/chat-transcript-builder.ts +9 -3
  173. package/src/chat/compaction-summary-message.d.ts +1 -0
  174. package/src/chat/compaction-summary-message.ts +8 -0
  175. package/src/chat/execution-shared.d.ts +6 -5
  176. package/src/chat/execution-shared.ts +42 -25
  177. package/src/chat/fence-figure.d.ts +42 -0
  178. package/src/chat/fence-figure.ts +80 -0
  179. package/src/chat/figure-markdown.d.ts +42 -0
  180. package/src/chat/figure-markdown.ts +174 -0
  181. package/src/chat/messages.d.ts +1 -1
  182. package/src/chat/messages.ts +1 -1
  183. package/src/chat/read-tool-group.ts +6 -4
  184. package/src/chat/skill-message.ts +6 -0
  185. package/src/chat/svg-figure.d.ts +33 -0
  186. package/src/chat/svg-figure.ts +399 -0
  187. package/src/chat/svg-source.d.ts +49 -0
  188. package/src/chat/svg-source.ts +188 -0
  189. package/src/chat/table-chart.d.ts +89 -0
  190. package/src/chat/table-chart.ts +294 -0
  191. package/src/chat/tool-execution.d.ts +2 -0
  192. package/src/chat/tool-execution.ts +86 -7
  193. package/src/chat/user-message.d.ts +17 -4
  194. package/src/chat/user-message.ts +71 -8
  195. package/src/chrome/shared.ts +53 -0
  196. package/src/chrome/status-notice.ts +25 -7
  197. package/src/chrome/transcript-container.d.ts +6 -0
  198. package/src/chrome/transcript-container.ts +162 -19
  199. package/src/components/box.d.ts +1 -0
  200. package/src/components/box.ts +5 -0
  201. package/src/components/disclosure.d.ts +1 -0
  202. package/src/components/disclosure.ts +8 -0
  203. package/src/components/editor.d.ts +23 -5
  204. package/src/components/editor.ts +316 -47
  205. package/src/components/form.d.ts +1 -0
  206. package/src/components/form.ts +8 -0
  207. package/src/components/image.d.ts +26 -44
  208. package/src/components/image.ts +115 -152
  209. package/src/components/input.d.ts +2 -1
  210. package/src/components/input.ts +17 -7
  211. package/src/components/key-value-list.d.ts +1 -0
  212. package/src/components/key-value-list.ts +4 -0
  213. package/src/components/layout/row.d.ts +1 -0
  214. package/src/components/layout/row.ts +7 -0
  215. package/src/components/layout/stack.d.ts +1 -0
  216. package/src/components/layout/stack.ts +7 -0
  217. package/src/components/loader.d.ts +5 -6
  218. package/src/components/loader.ts +10 -17
  219. package/src/components/markdown.d.ts +47 -5
  220. package/src/components/markdown.ts +786 -303
  221. package/src/components/scroll-view.d.ts +1 -0
  222. package/src/components/scroll-view.ts +5 -0
  223. package/src/components/section.d.ts +1 -0
  224. package/src/components/section.ts +5 -0
  225. package/src/components/settings-list.ts +2 -0
  226. package/src/components/table.d.ts +1 -0
  227. package/src/components/table.ts +4 -0
  228. package/src/components/text.d.ts +1 -0
  229. package/src/components/text.ts +7 -0
  230. package/src/components/tree-view.d.ts +7 -0
  231. package/src/components/tree-view.ts +38 -8
  232. package/src/components/truncated-text.d.ts +1 -0
  233. package/src/components/truncated-text.ts +5 -0
  234. package/src/desktop-notify.ts +3 -2
  235. package/src/fuzzy.d.ts +29 -1
  236. package/src/fuzzy.ts +115 -30
  237. package/src/hotkeys-markdown.ts +9 -2
  238. package/src/key-hint-format.d.ts +9 -1
  239. package/src/key-hint-format.ts +26 -11
  240. package/src/keybinding-matchers.d.ts +3 -0
  241. package/src/keybinding-matchers.ts +6 -2
  242. package/src/keybindings.d.ts +1 -1
  243. package/src/keybindings.ts +1 -1
  244. package/src/kitty-graphics.d.ts +2 -2
  245. package/src/kitty-graphics.ts +11 -5
  246. package/src/loop-watchdog.d.ts +8 -7
  247. package/src/loop-watchdog.ts +18 -13
  248. package/src/multiplexers/cmux.d.ts +16 -0
  249. package/src/multiplexers/cmux.ts +29 -0
  250. package/src/multiplexers/herdr.d.ts +26 -0
  251. package/src/multiplexers/herdr.ts +64 -0
  252. package/src/multiplexers/notify.d.ts +13 -0
  253. package/src/multiplexers/notify.ts +24 -0
  254. package/src/multiplexers/orca.d.ts +12 -0
  255. package/src/multiplexers/orca.ts +16 -0
  256. package/src/multiplexers/screen.d.ts +9 -0
  257. package/src/multiplexers/screen.ts +15 -0
  258. package/src/multiplexers/tmux.d.ts +29 -0
  259. package/src/multiplexers/tmux.ts +40 -0
  260. package/src/multiplexers/types.d.ts +100 -0
  261. package/src/multiplexers/types.ts +110 -0
  262. package/src/multiplexers/wmux.d.ts +12 -0
  263. package/src/multiplexers/wmux.ts +16 -0
  264. package/src/multiplexers/zellij.d.ts +12 -0
  265. package/src/multiplexers/zellij.ts +24 -0
  266. package/src/native/apply.d.ts +4 -3
  267. package/src/native/apply.ts +4 -3
  268. package/src/native/backend.d.ts +5 -4
  269. package/src/native/backend.ts +199 -22
  270. package/src/native/blobs.d.ts +7 -3
  271. package/src/native/blobs.ts +47 -12
  272. package/src/native/encode.d.ts +8 -6
  273. package/src/native/encode.ts +76 -34
  274. package/src/native/node.d.ts +36 -5
  275. package/src/native/node.ts +31 -5
  276. package/src/native/overlay.d.ts +8 -3
  277. package/src/native/overlay.ts +17 -4
  278. package/src/native/reconcile.ts +16 -4
  279. package/src/native/settle.d.ts +4 -3
  280. package/src/native/settle.ts +4 -3
  281. package/src/overlays/agent-hub-projection.d.ts +6 -5
  282. package/src/overlays/agent-hub-projection.ts +124 -13
  283. package/src/overlays/agent-hub-types.d.ts +9 -0
  284. package/src/overlays/agent-hub-types.ts +6 -0
  285. package/src/overlays/agent-hub.ts +48 -29
  286. package/src/overlays/agent-transcript-viewer.ts +93 -23
  287. package/src/overlays/agents-hub.ts +28 -8
  288. package/src/overlays/annotation-overlay.ts +293 -90
  289. package/src/overlays/annotation-types.ts +2 -0
  290. package/src/overlays/ask-dialog.ts +10 -17
  291. package/src/overlays/btw-history-panel.ts +136 -39
  292. package/src/overlays/copy-selector.d.ts +1 -0
  293. package/src/overlays/copy-selector.ts +32 -8
  294. package/src/overlays/extensions/extension-dashboard.ts +4 -4
  295. package/src/overlays/extensions/state-manager.ts +52 -12
  296. package/src/overlays/history-search.ts +100 -7
  297. package/src/overlays/hook-selector.ts +12 -5
  298. package/src/overlays/hub-frame.ts +19 -1
  299. package/src/overlays/jobs-panel.ts +33 -52
  300. package/src/overlays/model-browser.ts +367 -122
  301. package/src/overlays/model-hub.ts +435 -59
  302. package/src/overlays/move-overlay.ts +28 -14
  303. package/src/overlays/oauth-selector.ts +54 -70
  304. package/src/overlays/pause-screen.ts +2 -2
  305. package/src/overlays/plan-review-overlay.ts +208 -70
  306. package/src/overlays/plugin-settings.ts +3 -3
  307. package/src/overlays/report-panel.ts +191 -0
  308. package/src/overlays/rewind-selector.ts +88 -17
  309. package/src/overlays/session-observer-registry.d.ts +2 -0
  310. package/src/overlays/session-observer-registry.ts +10 -2
  311. package/src/overlays/session-selector.ts +64 -26
  312. package/src/overlays/settings-defs.ts +5 -4
  313. package/src/overlays/settings-selector.ts +33 -8
  314. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  315. package/src/overlays/stats-notice.ts +1 -1
  316. package/src/overlays/thinking-selector.ts +11 -10
  317. package/src/overlays/tree-selector.ts +41 -20
  318. package/src/overlays/usage-dashboard.ts +117 -58
  319. package/src/overlays/usage-display.ts +10 -4
  320. package/src/overlays/usage-row.ts +22 -8
  321. package/src/prompt/attachment-chips.ts +15 -11
  322. package/src/prompt/composer-attachments.ts +6 -4
  323. package/src/prompt/composer-cache.ts +7 -86
  324. package/src/prompt/composer.ts +37 -26
  325. package/src/prompt/custom-editor.ts +282 -105
  326. package/src/prompt/image-source.ts +2 -1
  327. package/src/prompt/interactive-context-helpers.ts +32 -18
  328. package/src/prompt/macos-spelling.ts +7 -0
  329. package/src/prompt/magic-keywords.ts +11 -2
  330. package/src/prompt/model-mention-autocomplete.ts +14 -14
  331. package/src/prompt/queued-messages.ts +6 -4
  332. package/src/prompt/tips.txt +13 -9
  333. package/src/prompt/welcome.ts +64 -46
  334. package/src/prompt/word-completion.ts +38 -5
  335. package/src/render/composite.d.ts +2 -0
  336. package/src/render/composite.ts +64 -0
  337. package/src/render/output-pane.d.ts +1 -0
  338. package/src/render/output-pane.ts +6 -0
  339. package/src/render/render-utils.d.ts +1 -0
  340. package/src/render/render-utils.ts +16 -1
  341. package/src/render/tool-card.d.ts +6 -0
  342. package/src/render/tool-card.ts +15 -0
  343. package/src/render/width-aware-text.d.ts +2 -0
  344. package/src/render/width-aware-text.ts +5 -0
  345. package/src/setup/scenes/model.ts +2 -1
  346. package/src/setup/scenes/types.ts +1 -0
  347. package/src/space-hold.d.ts +40 -25
  348. package/src/space-hold.ts +156 -52
  349. package/src/status-line/component.ts +292 -78
  350. package/src/status-line/context-usage.ts +21 -46
  351. package/src/status-line/host.ts +4 -1
  352. package/src/status-line/segments.ts +40 -22
  353. package/src/status-line/types.ts +15 -3
  354. package/src/terminal-capabilities.d.ts +41 -36
  355. package/src/terminal-capabilities.ts +137 -213
  356. package/src/terminal-multiplexer.d.ts +127 -14
  357. package/src/terminal-multiplexer.ts +112 -33
  358. package/src/terminal.d.ts +23 -16
  359. package/src/terminal.ts +203 -99
  360. package/src/theme/active-symbols.d.ts +1 -1
  361. package/src/theme/active-symbols.ts +1 -1
  362. package/src/theme/schema-validation.ts +1 -0
  363. package/src/theme/schema.d.ts +11 -2
  364. package/src/theme/schema.ts +14 -1
  365. package/src/theme/session-color.ts +1 -7
  366. package/src/theme/symbols.d.ts +1 -1
  367. package/src/theme/symbols.ts +11 -4
  368. package/src/theme/theme-class.d.ts +7 -0
  369. package/src/theme/theme-class.ts +26 -1
  370. package/src/theme/theme-schema.json +5 -1
  371. package/src/theme/theme.d.ts +1 -1
  372. package/src/theme/theme.ts +8 -4
  373. package/src/theme/tui-adapters.d.ts +3 -0
  374. package/src/theme/tui-adapters.ts +68 -0
  375. package/src/thinking.d.ts +1 -1
  376. package/src/thinking.ts +1 -1
  377. package/src/tmux.d.ts +11 -4
  378. package/src/tmux.ts +47 -11
  379. package/src/tools/ast-edit.ts +59 -35
  380. package/src/tools/ast-grep.ts +35 -16
  381. package/src/tools/bash.d.ts +7 -2
  382. package/src/tools/bash.ts +116 -47
  383. package/src/tools/daemon.d.ts +1 -1
  384. package/src/tools/daemon.ts +1 -1
  385. package/src/tools/default-renderer.ts +35 -19
  386. package/src/tools/edit.ts +142 -120
  387. package/src/tools/eval-format/index.ts +28 -2
  388. package/src/tools/eval-format/javascript.ts +7 -5
  389. package/src/tools/eval.d.ts +22 -3
  390. package/src/tools/eval.ts +364 -208
  391. package/src/tools/fetch.ts +5 -6
  392. package/src/tools/glob.d.ts +4 -0
  393. package/src/tools/glob.ts +33 -65
  394. package/src/tools/grep.ts +81 -48
  395. package/src/tools/index.ts +2 -0
  396. package/src/tools/lsp.ts +81 -59
  397. package/src/tools/mcp.ts +111 -68
  398. package/src/tools/native-view.d.ts +47 -0
  399. package/src/tools/native-view.ts +102 -1
  400. package/src/tools/output-meta.d.ts +11 -1
  401. package/src/tools/output-meta.ts +23 -3
  402. package/src/tools/read.ts +7 -5
  403. package/src/tools/renderer.d.ts +37 -2
  404. package/src/tools/renderer.ts +42 -2
  405. package/src/tools/streaming-output.d.ts +20 -1
  406. package/src/tools/streaming-output.ts +154 -50
  407. package/src/tools/task.d.ts +2 -3
  408. package/src/tools/task.ts +115 -62
  409. package/src/tools/todo.ts +119 -30
  410. package/src/tools/wait.ts +34 -24
  411. package/src/tools/web-search.ts +1 -1
  412. package/src/tools/write.d.ts +9 -2
  413. package/src/tools/write.ts +147 -95
  414. package/src/tools/yield.d.ts +22 -0
  415. package/src/tools/yield.ts +225 -0
  416. package/src/tui.d.ts +36 -0
  417. package/src/tui.ts +666 -217
  418. package/src/utils.d.ts +6 -0
  419. package/src/utils.ts +19 -0
  420. package/src/vim.d.ts +19 -5
  421. package/src/vim.ts +333 -18
  422. package/src/windows-input-mode.d.ts +33 -1
  423. package/src/windows-input-mode.ts +212 -6
@@ -1,4 +1,5 @@
1
- import { applyBackgroundToLine, padding, visibleWidth } from "../utils";
1
+ import { applyBackgroundToLine, padding, stripTerminalSequences, visibleWidth } from "../utils";
2
+ import type { AnimationFrame, TranscriptPresentationTarget } from "../chrome/transcript-container";
2
3
  import { type Component, Container } from "../tui";
3
4
  import { Disclosure } from "../components/disclosure";
4
5
  import { Markdown } from "../components/markdown";
@@ -24,7 +25,13 @@ import type { ReactionTarget } from "./reaction";
24
25
  import { card, md, node, row, span, text } from "../native/describe";
25
26
  import { base64ImageNode } from "../native/blobs";
26
27
  import { hasTranscriptActions, runTranscriptAction } from "./transcript-actions";
27
- import { type NativeChild, type NativeNode, type NativeUiEvent, rootToggleExpanded } from "../native/node";
28
+ import {
29
+ type DescribeContext,
30
+ type NativeChild,
31
+ type NativeNode,
32
+ type NativeUiEvent,
33
+ rootToggleExpanded,
34
+ } from "../native/node";
28
35
  import { Memo } from "../native/memo";
29
36
 
30
37
  // OSC 133 shell integration: marks prompt zones for terminal multiplexers.
@@ -51,7 +58,7 @@ const OSC133_ZONE_CLOSE = OSC133_ZONE_END + OSC133_COMMAND_START + OSC133_COMMAN
51
58
 
52
59
  /** How a user bubble styles its prose and chips (see {@link userBubbleColor}). */
53
60
  export interface UserBubbleOptions {
54
- /** Materialized `file://` targets per attached image, indexed by chip number. */
61
+ /** Filesystem paths for attached image chips, indexed by chip number. */
55
62
  imageLinks?: readonly (string | undefined)[];
56
63
  /** The message's attached images in chip order (`#1` first); a native bubble shows them. */
57
64
  images?: readonly ImageContent[];
@@ -118,7 +125,8 @@ export function userBubbleColor(
118
125
  * (see {@link ReactionTarget}) drawn right-aligned in the bubble's top padding row;
119
126
  * a live-steered message carries a `*` marker left-aligned in the same row.
120
127
  */
121
- export class UserMessageComponent extends Container implements ReactionTarget {
128
+ export class UserMessageComponent extends Container implements ReactionTarget, TranscriptPresentationTarget {
129
+ #allocation = Number.POSITIVE_INFINITY;
122
130
  // Memoized OSC 133 zone wrapping keyed on the underlying container render
123
131
  // (same source ref ⇒ identical rows ⇒ reuse the wrapped copy). Keeps this
124
132
  // component reference-stable for the transcript's incremental assembly and
@@ -137,6 +145,8 @@ export class UserMessageComponent extends Container implements ReactionTarget {
137
145
  readonly #tokens: RegExp;
138
146
  #reaction: string | undefined;
139
147
  #native: NativeNode | undefined;
148
+ /** The terminal's clock {@link #native} was described with. */
149
+ #nativeHour12: boolean | undefined;
140
150
 
141
151
  constructor(text: string, options: UserBubbleOptions = {}) {
142
152
  super();
@@ -169,6 +179,12 @@ export class UserMessageComponent extends Container implements ReactionTarget {
169
179
  this.addChild(markdown);
170
180
  }
171
181
 
182
+ override releaseRenderCaches(): void {
183
+ this.#zoneSource = undefined;
184
+ this.#zoneLines = undefined;
185
+ super.releaseRenderCaches();
186
+ }
187
+
172
188
  setReaction(emoji: string): void {
173
189
  if (this.#reaction === emoji) return;
174
190
  this.#reaction = emoji;
@@ -184,17 +200,20 @@ export class UserMessageComponent extends Container implements ReactionTarget {
184
200
  * agent's reaction are chips at the bottom-right, so a reaction landing
185
201
  * later updates the frame in place, even deep in scrollback.
186
202
  */
187
- override describe(): NativeNode {
188
- if (this.#native) return this.#native;
203
+ override describe(cx?: DescribeContext): NativeNode {
204
+ // The terminal's clock: Bun's own default locale ignores the user's.
205
+ const hour12 = cx?.hour12;
206
+ if (this.#native && this.#nativeHour12 === hour12) return this.#native;
207
+ this.#nativeHour12 = hour12;
189
208
  const children: NativeChild[] = [];
190
209
  if (!this.#synthetic && hasTranscriptActions()) {
191
210
  const tools: NativeChild[] = [];
192
211
  if (this.#timestamp !== undefined) {
193
212
  const at = new Date(this.#timestamp);
194
213
  tools.push(
195
- text([span(at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), "dim mono")], {
214
+ text([span(at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12 }), "dim mono")], {
196
215
  role: "zeta.user.time",
197
- title: at.toLocaleString(),
216
+ title: at.toLocaleString([], { hour12 }),
198
217
  }),
199
218
  );
200
219
  }
@@ -270,11 +289,47 @@ export class UserMessageComponent extends Container implements ReactionTarget {
270
289
  return applyBackgroundToLine(` ${marker}${padding(gap)}${emoji}`, width, this.#bgColor);
271
290
  }
272
291
 
292
+ /** Apply the transcript allocator's current viewport reservation. */
293
+ setTranscriptAllocation(rows: number, _frame?: AnimationFrame): void {
294
+ this.#allocation = Math.max(0, Math.trunc(rows));
295
+ }
296
+
297
+ isTranscriptBlockFinalized(): boolean {
298
+ return true;
299
+ }
300
+
301
+ /**
302
+ * Single-row emergency representation for the overflow transcript layout:
303
+ * renders the first content line instead of the blank top padding row, and
304
+ * omits OSC 133 shell integration markers so moving live blocks across the
305
+ * viewport does not leave stale prompt marks on lines it no longer occupies.
306
+ */
307
+ renderTranscriptBlockEmergencyRow(width: number): string | undefined {
308
+ return this.#selectEmergencyRow(super.render(width));
309
+ }
310
+
311
+ #selectEmergencyRow(lines: readonly string[]): string | undefined {
312
+ if (lines.length === 0) return undefined;
313
+ if (lines.length >= 3) {
314
+ for (let i = 1; i < lines.length - 1; i++) {
315
+ if (stripTerminalSequences(lines[i]!).trim().length > 0) {
316
+ return lines[i];
317
+ }
318
+ }
319
+ return lines[1];
320
+ }
321
+ return lines[0];
322
+ }
323
+
273
324
  override render(width: number): readonly string[] {
274
325
  const lines = super.render(width);
275
326
  if (lines.length === 0) {
276
327
  return lines;
277
328
  }
329
+ if (this.#allocation === 1) {
330
+ const emergency = this.#selectEmergencyRow(lines);
331
+ if (emergency !== undefined) return [emergency];
332
+ }
278
333
  if (this.#zoneSource === lines && this.#zoneLines !== undefined) {
279
334
  return this.#zoneLines;
280
335
  }
@@ -326,6 +381,10 @@ class SyntheticSummary implements Component {
326
381
  this.#summary = summary;
327
382
  }
328
383
 
384
+ releaseRenderCaches(): void {
385
+ this.#cache = undefined;
386
+ }
387
+
329
388
  invalidate(): void {
330
389
  this.#cache = undefined;
331
390
  }
@@ -414,6 +473,10 @@ export class CollapsedSyntheticMessageComponent implements Component {
414
473
  this.#disclosure.invalidate();
415
474
  }
416
475
 
476
+ releaseRenderCaches(): void {
477
+ this.#disclosure.releaseRenderCaches();
478
+ }
479
+
417
480
  dispose(): void {
418
481
  this.#disclosure.dispose();
419
482
  }
@@ -10,6 +10,59 @@ export function sanitizeStatusText(text: string): string {
10
10
  return sanitizeDisplaySingleLine(text).replace(/ +/g, " ").trim();
11
11
  }
12
12
 
13
+ /** 7-bit SGR (`ESC [ <params> m`) — the one escape family a hook status may keep. */
14
+ const SGR_RE = /\x1b\[[0-9;:]*m/g;
15
+ const SGR_RESET = "\x1b[0m";
16
+ const WHITESPACE_RE = /\s/;
17
+
18
+ /**
19
+ * Like {@link sanitizeStatusText}, but keeps 7-bit SGR so extensions can colour
20
+ * `ctx.ui.setStatus` text. Every other escape (OSC links/titles, DCS/PM/APC,
21
+ * 7- and 8-bit CSI), C0/C1, lone surrogates, newlines and tabs are sanitized at
22
+ * least as strictly as in `sanitizeStatusText`; space runs collapse and edges
23
+ * trim (`String#trim` whitespace) by visible text, regardless of SGR placement.
24
+ * A styled result always ends with `\x1b[0m`.
25
+ */
26
+ export function sanitizeHookStatusText(input: string): string {
27
+ // Drop lone surrogates before splitting, as `sanitizeText` does, so stripped escapes
28
+ // cannot fuse surrogate halves from neighbouring chunks into a new character.
29
+ const wellFormed = input.toWellFormed();
30
+ const text = wellFormed === input ? input : wellFormed.replaceAll("\ufffd", "");
31
+ SGR_RE.lastIndex = 0;
32
+ if (!SGR_RE.test(text)) return sanitizeStatusText(text); // byte-identical for unstyled input
33
+ let out = "";
34
+ // SGR codes and whitespace seen since the last visible char, in source order.
35
+ let pending = "";
36
+ let lastWasSpace = false;
37
+ let hasVisible = false;
38
+ let cursor = 0;
39
+ SGR_RE.lastIndex = 0;
40
+ for (;;) {
41
+ const match = SGR_RE.exec(text);
42
+ // `sanitizeText` runs `Bun.stripANSI` only when an ESC is present; a chunk between SGR
43
+ // codes often has none, so strip explicitly to drop 8-bit (C1) sequence payloads.
44
+ const chunk = sanitizeDisplaySingleLine(Bun.stripANSI(text.slice(cursor, match ? match.index : text.length)));
45
+ for (const ch of chunk) {
46
+ if (WHITESPACE_RE.test(ch)) {
47
+ // Leading whitespace drops; ASCII space runs collapse even across SGR codes.
48
+ if (hasVisible && !(ch === " " && lastWasSpace)) pending += ch;
49
+ lastWasSpace = ch === " ";
50
+ continue;
51
+ }
52
+ out += pending + ch;
53
+ pending = "";
54
+ lastWasSpace = false;
55
+ hasVisible = true;
56
+ }
57
+ if (!match) break;
58
+ pending += match[0];
59
+ cursor = match.index + match[0].length;
60
+ }
61
+ if (!hasVisible) return "";
62
+ // Trailing whitespace trims; trailing SGR codes stay (they never contain whitespace).
63
+ return out + pending.replace(/\s+/g, "") + SGR_RESET;
64
+ }
65
+
13
66
  // ═══════════════════════════════════════════════════════════════════════════
14
67
  // Tab Bar Theme
15
68
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1,6 +1,6 @@
1
1
  import { Spacer } from "../components/spacer";
2
2
  import { Text } from "../components/text";
3
- import { node } from "../native/describe";
3
+ import { EMPTY_NODE, node } from "../native/describe";
4
4
  import type { NativeNode } from "../native/node";
5
5
  import { styledSpans } from "../native/spans";
6
6
  import { Container } from "../tui";
@@ -8,33 +8,51 @@ import { Container } from "../tui";
8
8
  /** How long a native status toast stays up (ms). */
9
9
  const TOAST_TTL = 2400;
10
10
 
11
+ /** How a {@link StatusNotice} shows. */
12
+ export interface StatusNoticeOptions {
13
+ /** Styles the ANSI line; resolved at render so a theme change re-shapes it. */
14
+ styleFn?: (text: string) => string;
15
+ /**
16
+ * Whether a native terminal toasts it (default). False keeps the notice to
17
+ * the ANSI transcript: background chatter such as MCP connection progress,
18
+ * which would otherwise re-toast on every server that connects or fails.
19
+ */
20
+ toast?: boolean;
21
+ }
22
+
11
23
  /**
12
24
  * A transient status notice (`showStatus`: "Thinking blocks: hidden", "Copied
13
25
  * to clipboard"). The ANSI render appends a blank row and the dim line to the
14
26
  * transcript; a native terminal shows it as a toast instead, so notices never
15
- * pile up in the history. Updating the text re-shows the toast.
27
+ * pile up in the history. Updating the text re-shows the toast; a notice
28
+ * with `toast: false` describes nothing natively.
16
29
  */
17
30
  export class StatusNotice extends Container {
18
31
  readonly #text: Text;
19
32
  #message: string;
33
+ #toast: boolean;
20
34
  #native: NativeNode | undefined;
21
35
 
22
- constructor(message: string, styleFn?: (text: string) => string) {
36
+ constructor(message: string, options: StatusNoticeOptions = {}) {
23
37
  super();
24
38
  this.#message = message;
25
- this.#text = new Text(message, 1, 0).setStyleFn(styleFn);
39
+ this.#toast = options.toast ?? true;
40
+ this.#text = new Text(message, 1, 0).setStyleFn(options.styleFn);
26
41
  this.addChild(new Spacer(1));
27
42
  this.addChild(this.#text);
28
43
  }
29
44
 
30
- setMessage(message: string, styleFn?: (text: string) => string): void {
31
- this.#text.setStyleFn(styleFn);
45
+ setMessage(message: string, options: StatusNoticeOptions = {}): void {
46
+ const toast = options.toast ?? true;
47
+ this.#text.setStyleFn(options.styleFn);
32
48
  this.#text.setText(message);
33
- if (message !== this.#message) this.#native = undefined;
49
+ if (message !== this.#message || toast !== this.#toast) this.#native = undefined;
34
50
  this.#message = message;
51
+ this.#toast = toast;
35
52
  }
36
53
 
37
54
  override describe(): NativeNode {
55
+ if (!this.#toast) return EMPTY_NODE;
38
56
  if (this.#native) return this.#native;
39
57
  // Callers may pass pre-styled text; a toast takes plain text.
40
58
  const plain = styledSpans(this.#message)
@@ -63,6 +63,12 @@ export interface TranscriptViewportSpan {
63
63
  export declare class TranscriptContainer extends Container {
64
64
  #private;
65
65
  addChild(component: Component): void;
66
+ /**
67
+ * `#entries` mirrors `children`; move both together or the next `#syncEntries`
68
+ * pays a full O(n) rebuild.
69
+ */
70
+ insertChildAt(component: Component, index: number): void;
71
+ /** Removes `component` if {@link canRemoveBlock} allows it; otherwise does nothing. */
66
72
  removeChild(component: Component): void;
67
73
  clear(): void;
68
74
  setToolActivityVisible(visible: boolean): void;
@@ -1,9 +1,11 @@
1
1
  import { type Component, Container, type HistoryBatch } from "../tui";
2
2
  import * as logger from "@linxiraos/pi-utils/logger";
3
3
  import { popLoopPhase, pushLoopPhase } from "@linxiraos/pi-utils";
4
+ import { renderForScrollback } from "../components/image";
4
5
  import { col } from "../native/describe";
5
6
  import type { NativeNode } from "../native/node";
6
7
  import { isNativeSettled, settleNative } from "../native/settle";
8
+ import { TERMINAL } from "../terminal-capabilities";
7
9
  import { isUsageRowBlock } from "../overlays/usage-row";
8
10
  import { isToolActivityComponent } from "./tool-activity";
9
11
 
@@ -56,6 +58,12 @@ export interface AppendOnlyTranscriptBlock {
56
58
 
57
59
  interface FinalizableBlock {
58
60
  isTranscriptBlockFinalized?(): boolean;
61
+ /**
62
+ * A finalized block whose rows still wait on async work (an SVG figure's
63
+ * raster). Retirement holds until it lands: committed rows can never be
64
+ * repainted, so retiring early would freeze a placeholder into scrollback.
65
+ */
66
+ isTranscriptBlockPending?(): boolean;
59
67
  /** Render the row that must remain represented under emergency viewport pressure. */
60
68
  renderTranscriptBlockEmergencyRow?(width: number): string | undefined;
61
69
  }
@@ -89,6 +97,13 @@ interface TranscriptEntry {
89
97
  * state for emitted-row slicing but never emits another mid-stream row.
90
98
  */
91
99
  stableFrozen: boolean;
100
+ /**
101
+ * Cached "renders at least one image line" result for the settled-image
102
+ * flush, keyed by width and image protocol (the probe upgrade re-encodes
103
+ * text-fallback blocks into graphics). Rows of a settled block are frozen,
104
+ * so a hit is exact.
105
+ */
106
+ imageScan: { width: number; protocol: string | null; hasImageRows: boolean } | undefined;
92
107
  }
93
108
 
94
109
  type RetirementPolicy = "pressure" | "flush";
@@ -120,7 +135,7 @@ const EMPTY_STABLE_ROWS: readonly TranscriptStableRow[] = [];
120
135
 
121
136
  function isFinalized(component: Component): boolean {
122
137
  const block = component as Component & FinalizableBlock;
123
- return block.isTranscriptBlockFinalized?.() ?? true;
138
+ return (block.isTranscriptBlockFinalized?.() ?? true) && block.isTranscriptBlockPending?.() !== true;
124
139
  }
125
140
 
126
141
  function blockMode(component: Component): TranscriptBlockMode {
@@ -199,6 +214,7 @@ export class TranscriptContainer extends Container {
199
214
  */
200
215
  #frameRows = new Map<TranscriptEntry, readonly string[]>();
201
216
  #frameRowsWidth = 0;
217
+ #releaseFailures = new WeakSet<Component>();
202
218
  /** The `children` array `#entries` last mirrored; see {@link #syncEntries}. */
203
219
  #syncedChildren: Component[] | undefined;
204
220
  /** Forces the next {@link #syncEntries} to compare every entry, not just the live tail. */
@@ -218,16 +234,42 @@ export class TranscriptContainer extends Container {
218
234
  stableRowCountByWidth: new Map(),
219
235
  emitted: 0,
220
236
  stableFrozen: false,
237
+ imageScan: undefined,
221
238
  });
222
- // Callers may splice a just-added block into place (insert after an
223
- // anchor); re-check the whole list once instead of trusting positions.
239
+ // `children` is public and may be edited externally; re-check the whole
240
+ // list once instead of trusting positions. Insert-after-anchor callers
241
+ // use insertChildAt, which keeps both arrays in lockstep.
224
242
  this.#entriesUnverified = true;
225
243
  }
226
244
 
245
+ /**
246
+ * `#entries` mirrors `children`; move both together or the next `#syncEntries`
247
+ * pays a full O(n) rebuild.
248
+ */
249
+ insertChildAt(component: Component, index: number): void {
250
+ this.addChild(component);
251
+ const from = this.children.length - 1;
252
+ if (index >= from) {
253
+ this.#entriesUnverified = false;
254
+ return;
255
+ }
256
+ const [moved] = this.children.splice(from, 1);
257
+ if (moved) this.children.splice(index, 0, moved);
258
+ const [entry] = this.#entries.splice(this.#entries.length - 1, 1);
259
+ if (entry) this.#entries.splice(index, 0, entry);
260
+ // Both arrays moved in lockstep, so the live-tail check applies again.
261
+ this.#entriesUnverified = false;
262
+ }
263
+
264
+ /** Removes `component` if {@link canRemoveBlock} allows it; otherwise does nothing. */
227
265
  override removeChild(component: Component): void {
228
- if (this.children.indexOf(component) < 0 || !this.canRemoveBlock(component)) return;
229
- super.removeChild(component);
230
- this.#entries = this.#entries.filter(candidate => candidate.component !== component);
266
+ const index = this.#removableIndex(component);
267
+ if (index < 0) return;
268
+ // #removableIndex synced entries, which now mirror children index for
269
+ // index. This class renders through its own entries, never Container's
270
+ // memoized render, so splicing children directly is the whole removal.
271
+ this.children.splice(index, 1);
272
+ this.#entries.splice(index, 1);
231
273
  this.#frontier = Math.min(this.#frontier, this.#entries.length);
232
274
  this.#childStartRows.delete(component);
233
275
  }
@@ -282,14 +324,24 @@ export class TranscriptContainer extends Container {
282
324
 
283
325
  /** Whether a transient block may be discarded without leaving tape history. */
284
326
  canRemoveBlock(component: Component): boolean {
327
+ return this.#removableIndex(component) >= 0;
328
+ }
329
+
330
+ /**
331
+ * Syncs entries, then returns `component`'s index if it can be discarded
332
+ * without leaving tape history (not committed, not emitted, not in an
333
+ * offered batch), else -1.
334
+ */
335
+ #removableIndex(component: Component): number {
285
336
  this.#syncEntries();
286
- const index = this.#entries.findIndex(entry => entry.component === component);
287
- if (index < 0) return false;
337
+ // Removable blocks are transient ones near the live tail, so search from there.
338
+ const index = this.#entries.findLastIndex(entry => entry.component === component);
339
+ if (index < 0) return -1;
288
340
  const entry = this.#entries[index]!;
289
- if (entry.state === "committed" || entry.emitted > 0) return false;
290
- if (this.#offered?.kind === "commit" && index < this.#offered.end) return false;
291
- if (this.#offered?.kind === "append" && index === this.#offered.entry) return false;
292
- return true;
341
+ if (entry.state === "committed" || entry.emitted > 0) return -1;
342
+ if (this.#offered?.kind === "commit" && index < this.#offered.end) return -1;
343
+ if (this.#offered?.kind === "append" && index === this.#offered.entry) return -1;
344
+ return index;
293
345
  }
294
346
 
295
347
  /**
@@ -576,7 +628,10 @@ export class TranscriptContainer extends Container {
576
628
  popLoopPhase();
577
629
  }
578
630
  this.#replayPending = false;
579
- if (rows.length === 0) return undefined;
631
+ if (rows.length === 0) {
632
+ this.#releaseCommittedRenderCaches();
633
+ return undefined;
634
+ }
580
635
  const batch: HistoryBatch = { id: this.#nextBatchId++, rows, kind: "replay" };
581
636
  this.#offered = { batch, kind: "replay" };
582
637
  return batch;
@@ -659,9 +714,20 @@ export class TranscriptContainer extends Container {
659
714
  if (!fits && appendHead === undefined) break;
660
715
  }
661
716
  const overflowing = keep > 0 || this.#liveCount() >= MAX_LIVE_BLOCKS;
717
+ // Graphics rows must reach native scrollback as soon as their block is
718
+ // finalized, not when the screen next overflows: live placements are
719
+ // repainted into the reserved-row block every frame, and on terminals
720
+ // whose live-region sixel does not stick (Windows Terminal) the image
721
+ // stays invisible until the exit flush commits the block.
722
+ // Retire the settled prefix through its last image-bearing block; the
723
+ // flush-at-stop path remains the fallback for what is still live then.
724
+ let imageFlushLimit = -1;
662
725
  if (policy === "pressure" && !overflowing) {
663
- this.#pinnedFrontier = undefined;
664
- return undefined;
726
+ imageFlushLimit = this.#settledImageFlushLimit(width);
727
+ if (imageFlushLimit < 0) {
728
+ this.#pinnedFrontier = undefined;
729
+ return undefined;
730
+ }
665
731
  }
666
732
 
667
733
  if (appendHead !== undefined && keep > 0) {
@@ -687,8 +753,11 @@ export class TranscriptContainer extends Container {
687
753
  }
688
754
 
689
755
  // Shutdown retires everything eligible; pressure retires exactly the
690
- // blocks that no longer fit, plus whatever the live-block cap demands.
756
+ // blocks that no longer fit, plus whatever the live-block cap demands
757
+ // — or, without overflow, the settled prefix through its last
758
+ // image-bearing block (graphics must land in scrollback promptly).
691
759
  let limit = this.#frontier + keep;
760
+ if (imageFlushLimit >= 0) limit = Math.max(limit, imageFlushLimit);
692
761
  if (this.#liveCount() >= MAX_LIVE_BLOCKS) {
693
762
  limit = Math.max(limit, this.#frontier + (this.#liveCount() - (MAX_LIVE_BLOCKS - 1)));
694
763
  }
@@ -740,6 +809,8 @@ export class TranscriptContainer extends Container {
740
809
  this.#retireEntry(this.#entries[index]!);
741
810
  }
742
811
  this.#frontier = offered.end;
812
+ } else {
813
+ this.#releaseCommittedRenderCaches();
743
814
  }
744
815
  this.#offered = undefined;
745
816
  if (this.#replayRequested) this.#startReplay();
@@ -759,6 +830,7 @@ export class TranscriptContainer extends Container {
759
830
  const entry = this.#entries[index]!;
760
831
  this.#setAllocation(entry.component, Number.MAX_SAFE_INTEGER, this.#lastFrame);
761
832
  const block = trimBlankEdges(entry.component.render(width));
833
+ if (entry.state === "committed") this.#releaseRenderCaches(entry);
762
834
  if (block.length === 0) continue;
763
835
  if (rows.length > 0) rows.unshift("");
764
836
  rows.unshift(...block);
@@ -806,6 +878,7 @@ export class TranscriptContainer extends Container {
806
878
  for (const entry of this.#entries) {
807
879
  this.#setAllocation(entry.component, Number.MAX_SAFE_INTEGER, this.#lastFrame);
808
880
  const block = this.#renderEntry(entry, width);
881
+ if (entry.state === "committed") this.#releaseRenderCaches(entry);
809
882
  if (block.length === 0) continue;
810
883
  if (rows.length > 0) rows.push("");
811
884
  this.#childStartRows.set(entry.component, rows.length);
@@ -1000,9 +1073,11 @@ export class TranscriptContainer extends Container {
1000
1073
  // Only the range head is sliced by its emitted stable prefix; every other
1001
1074
  // entry renders whole, so the append-only verification pass (a second
1002
1075
  // full render of the block's stable prefix) is skipped for them. This
1003
- // keeps a complete-ledger replay at one render per block.
1004
- const rendered =
1005
- index === start ? this.#renderEntry(entry, width) : trimBlankEdges(entry.component.render(width));
1076
+ // keeps a complete-ledger replay at one render per block. These rows go
1077
+ // to native scrollback, where nothing can repaint them.
1078
+ const rendered = renderForScrollback(() =>
1079
+ index === start ? this.#renderEntry(entry, width) : trimBlankEdges(entry.component.render(width)),
1080
+ );
1006
1081
  const emittedRows = index === start ? this.#renderStablePrefix(entry, entry.emitted, width).length : 0;
1007
1082
  const block = rendered.slice(emittedRows);
1008
1083
  reached = index + 1;
@@ -1045,6 +1120,29 @@ export class TranscriptContainer extends Container {
1045
1120
  entry.stableRows = EMPTY_STABLE_ROWS;
1046
1121
  entry.renderedStableByWidth = new Map();
1047
1122
  entry.stableRowCountByWidth = new Map();
1123
+ this.#frameRows.delete(entry);
1124
+ this.#releaseRenderCaches(entry);
1125
+ }
1126
+
1127
+ #releaseCommittedRenderCaches(): void {
1128
+ for (let index = 0; index < this.#frontier; index++) {
1129
+ this.#releaseRenderCaches(this.#entries[index]!);
1130
+ }
1131
+ }
1132
+
1133
+ #releaseRenderCaches(entry: TranscriptEntry): void {
1134
+ const release = entry.component.releaseRenderCaches;
1135
+ if (release === undefined) return;
1136
+ try {
1137
+ release.call(entry.component);
1138
+ } catch (err) {
1139
+ if (this.#releaseFailures.has(entry.component)) return;
1140
+ this.#releaseFailures.add(entry.component);
1141
+ logger.warn("Transcript block failed to release render caches", {
1142
+ component: entry.component.constructor.name,
1143
+ error: String(err),
1144
+ });
1145
+ }
1048
1146
  }
1049
1147
 
1050
1148
  #startReplay(): void {
@@ -1146,6 +1244,14 @@ export class TranscriptContainer extends Container {
1146
1244
  continue;
1147
1245
  }
1148
1246
  this.#setAllocation(candidate.entry.component, 1, frame);
1247
+ const emergency = (
1248
+ candidate.entry.component as Component & FinalizableBlock
1249
+ ).renderTranscriptBlockEmergencyRow?.(width);
1250
+ if (emergency !== undefined) {
1251
+ output.push(emergency);
1252
+ owners.push(candidate.entry.component);
1253
+ continue;
1254
+ }
1149
1255
  const rendered = this.#renderEntry(candidate.entry, width).slice(
1150
1256
  this.#projectedEmittedRowCount(candidate.entry, candidate.index, width),
1151
1257
  );
@@ -1168,6 +1274,42 @@ export class TranscriptContainer extends Container {
1168
1274
  }
1169
1275
  }
1170
1276
 
1277
+ /**
1278
+ * End (exclusive) of the settled prefix that must retire for its graphics
1279
+ * rows to land in native scrollback now: one past the last image-bearing
1280
+ * settled entry, or -1 when the prefix holds no image block. Entries after
1281
+ * a non-settled block cannot retire yet, so the walk stops at the first
1282
+ * one — an unfinalized image keeps waiting, exactly like at stop.
1283
+ */
1284
+ #settledImageFlushLimit(width: number): number {
1285
+ let last = -1;
1286
+ for (let index = this.#frontier; index < this.#entries.length; index++) {
1287
+ const entry = this.#entries[index]!;
1288
+ if (entry.state !== "settled") break;
1289
+ if (this.#hasImageRows(entry, width)) last = index;
1290
+ }
1291
+ return last < 0 ? -1 : last + 1;
1292
+ }
1293
+
1294
+ /** Whether the block's measured rows carry an image sequence (cached per width and protocol). */
1295
+ #hasImageRows(entry: TranscriptEntry, width: number): boolean {
1296
+ const protocol = TERMINAL.imageProtocol;
1297
+ const cached = entry.imageScan;
1298
+ if (cached !== undefined && cached.width === width && cached.protocol === protocol) return cached.hasImageRows;
1299
+ let hasImageRows = false;
1300
+ if (protocol !== null) {
1301
+ const rows = this.#measuredRows(entry, width);
1302
+ for (let index = 0; index < rows.length; index++) {
1303
+ if (TERMINAL.isImageLine(rows[index]!)) {
1304
+ hasImageRows = true;
1305
+ break;
1306
+ }
1307
+ }
1308
+ }
1309
+ entry.imageScan = { width, protocol, hasImageRows };
1310
+ return hasImageRows;
1311
+ }
1312
+
1171
1313
  #liveEntries(): Array<{ entry: TranscriptEntry; index: number }> {
1172
1314
  const start = this.#offered?.kind === "commit" ? this.#offered.end : this.#frontier;
1173
1315
  const live: Array<{ entry: TranscriptEntry; index: number }> = [];
@@ -1212,6 +1354,7 @@ export class TranscriptContainer extends Container {
1212
1354
  stableRowCountByWidth: new Map(),
1213
1355
  emitted: 0,
1214
1356
  stableFrozen: false,
1357
+ imageScan: undefined,
1215
1358
  },
1216
1359
  );
1217
1360
  this.#frontier = this.#entries.findIndex(entry => entry.state !== "committed");
@@ -30,6 +30,7 @@ export declare class Box implements Component {
30
30
  setBgFn(bgFn?: (text: string) => string): void;
31
31
  setBorder(border?: BoxBorder): void;
32
32
  invalidate(): void;
33
+ releaseRenderCaches(): void;
33
34
  /**
34
35
  * A `card` when the box has a border or background: the background token
35
36
  * picks tone/role (user message, tool state, selection), a border colour
@@ -135,6 +135,11 @@ export class Box implements Component {
135
135
  }
136
136
  }
137
137
 
138
+ releaseRenderCaches(): void {
139
+ this.#invalidateCache();
140
+ for (const child of this.children) child.releaseRenderCaches?.();
141
+ }
142
+
138
143
  /**
139
144
  * A `card` when the box has a border or background: the background token
140
145
  * picks tone/role (user message, tool state, selection), a border colour
@@ -36,6 +36,7 @@ export declare class Disclosure implements Component {
36
36
  setExpanded(expanded: boolean): void;
37
37
  setIgnoreTight(ignore: boolean): this;
38
38
  invalidate(): void;
39
+ releaseRenderCaches(): void;
39
40
  dispose(): void;
40
41
  debugState(): Record<string, unknown>;
41
42
  get debugChildren(): readonly Component[];
@@ -100,6 +100,14 @@ export class Disclosure implements Component {
100
100
  this.#body?.invalidate?.();
101
101
  }
102
102
 
103
+ releaseRenderCaches(): void {
104
+ this.#cache = undefined;
105
+ this.#boundedCache = undefined;
106
+ this.#summary?.releaseRenderCaches?.();
107
+ this.#collapsedBody?.releaseRenderCaches?.();
108
+ this.#body?.releaseRenderCaches?.();
109
+ }
110
+
103
111
  dispose(): void {
104
112
  if (this.#disposed) return;
105
113
  this.#disposed = true;