@linxiraos/pi-tui 1.1.27 → 1.2.1

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 (376) hide show
  1. package/CHANGELOG.md +50 -461
  2. package/README.md +7 -5
  3. package/dist/types/app-keybindings.d.ts +1 -1
  4. package/dist/types/apps/cleanse-board.d.ts +1 -1
  5. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  6. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  7. package/dist/types/apps/git/git-tui.d.ts +2 -2
  8. package/dist/types/apps/git/help.d.ts +3 -1
  9. package/dist/types/apps/if-bench-board.d.ts +2 -2
  10. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  11. package/dist/types/autocomplete.d.ts +5 -2
  12. package/dist/types/charts/chart-plan.d.ts +674 -0
  13. package/dist/types/charts/chart-svg.d.ts +59 -0
  14. package/dist/types/charts/table-data.d.ts +175 -0
  15. package/dist/types/chat/assistant-message.d.ts +5 -0
  16. package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
  17. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  18. package/dist/types/chat/execution-shared.d.ts +6 -5
  19. package/dist/types/chat/fence-figure.d.ts +42 -0
  20. package/dist/types/chat/figure-markdown.d.ts +42 -0
  21. package/dist/types/chat/messages.d.ts +1 -1
  22. package/dist/types/chat/svg-figure.d.ts +33 -0
  23. package/dist/types/chat/svg-source.d.ts +49 -0
  24. package/dist/types/chat/table-chart.d.ts +89 -0
  25. package/dist/types/chat/tool-execution.d.ts +2 -0
  26. package/dist/types/chat/user-message.d.ts +15 -2
  27. package/dist/types/chrome/shared.d.ts +9 -0
  28. package/dist/types/chrome/transcript-container.d.ts +6 -0
  29. package/dist/types/components/box.d.ts +1 -0
  30. package/dist/types/components/disclosure.d.ts +1 -0
  31. package/dist/types/components/editor.d.ts +15 -4
  32. package/dist/types/components/form.d.ts +1 -0
  33. package/dist/types/components/image.d.ts +12 -44
  34. package/dist/types/components/key-value-list.d.ts +1 -0
  35. package/dist/types/components/layout/row.d.ts +1 -0
  36. package/dist/types/components/layout/stack.d.ts +1 -0
  37. package/dist/types/components/markdown.d.ts +36 -3
  38. package/dist/types/components/scroll-view.d.ts +1 -0
  39. package/dist/types/components/section.d.ts +1 -0
  40. package/dist/types/components/table.d.ts +1 -0
  41. package/dist/types/components/text.d.ts +1 -0
  42. package/dist/types/components/tree-view.d.ts +7 -0
  43. package/dist/types/components/truncated-text.d.ts +1 -0
  44. package/dist/types/fuzzy.d.ts +29 -1
  45. package/dist/types/key-hint-format.d.ts +1 -1
  46. package/dist/types/keybinding-matchers.d.ts +3 -0
  47. package/dist/types/keybindings.d.ts +1 -1
  48. package/dist/types/kitty-graphics.d.ts +2 -2
  49. package/dist/types/loop-watchdog.d.ts +8 -7
  50. package/dist/types/multiplexers/cmux.d.ts +16 -0
  51. package/dist/types/multiplexers/herdr.d.ts +26 -0
  52. package/dist/types/multiplexers/notify.d.ts +13 -0
  53. package/dist/types/multiplexers/orca.d.ts +12 -0
  54. package/dist/types/multiplexers/screen.d.ts +9 -0
  55. package/dist/types/multiplexers/tmux.d.ts +29 -0
  56. package/dist/types/multiplexers/types.d.ts +100 -0
  57. package/dist/types/multiplexers/wmux.d.ts +12 -0
  58. package/dist/types/multiplexers/zellij.d.ts +12 -0
  59. package/dist/types/native/backend.d.ts +3 -2
  60. package/dist/types/native/blobs.d.ts +7 -3
  61. package/dist/types/native/encode.d.ts +3 -4
  62. package/dist/types/native/overlay.d.ts +5 -0
  63. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  64. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  65. package/dist/types/overlays/agent-hub.d.ts +2 -0
  66. package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
  67. package/dist/types/overlays/agents-hub.d.ts +9 -0
  68. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  69. package/dist/types/overlays/annotation-types.d.ts +2 -0
  70. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  71. package/dist/types/overlays/history-search.d.ts +10 -0
  72. package/dist/types/overlays/hook-selector.d.ts +9 -4
  73. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  74. package/dist/types/overlays/model-browser.d.ts +54 -7
  75. package/dist/types/overlays/model-hub.d.ts +23 -0
  76. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  77. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  78. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  79. package/dist/types/overlays/session-selector.d.ts +6 -2
  80. package/dist/types/overlays/settings-defs.d.ts +1 -0
  81. package/dist/types/overlays/usage-dashboard.d.ts +8 -4
  82. package/dist/types/overlays/usage-display.d.ts +3 -1
  83. package/dist/types/prompt/composer.d.ts +2 -7
  84. package/dist/types/prompt/custom-editor.d.ts +11 -8
  85. package/dist/types/prompt/image-source.d.ts +2 -1
  86. package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
  87. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  88. package/dist/types/prompt/welcome.d.ts +19 -42
  89. package/dist/types/render/composite.d.ts +2 -0
  90. package/dist/types/render/output-pane.d.ts +1 -0
  91. package/dist/types/render/render-utils.d.ts +1 -0
  92. package/dist/types/render/tool-card.d.ts +6 -0
  93. package/dist/types/render/width-aware-text.d.ts +2 -0
  94. package/dist/types/setup/scenes/splash.d.ts +1 -1
  95. package/dist/types/setup/scenes/types.d.ts +1 -0
  96. package/dist/types/status-line/component.d.ts +3 -0
  97. package/dist/types/status-line/host.d.ts +4 -1
  98. package/dist/types/status-line/segments.d.ts +2 -0
  99. package/dist/types/status-line/types.d.ts +14 -3
  100. package/dist/types/terminal-capabilities.d.ts +27 -35
  101. package/dist/types/terminal-multiplexer.d.ts +127 -14
  102. package/dist/types/terminal.d.ts +22 -6
  103. package/dist/types/theme/active-symbols.d.ts +1 -1
  104. package/dist/types/theme/schema.d.ts +11 -2
  105. package/dist/types/theme/symbols.d.ts +1 -1
  106. package/dist/types/theme/theme-class.d.ts +7 -0
  107. package/dist/types/theme/theme.d.ts +1 -1
  108. package/dist/types/theme/tui-adapters.d.ts +3 -0
  109. package/dist/types/tmux.d.ts +11 -4
  110. package/dist/types/tools/daemon.d.ts +1 -1
  111. package/dist/types/tools/eval.d.ts +22 -3
  112. package/dist/types/tools/glob.d.ts +4 -0
  113. package/dist/types/tools/native-view.d.ts +47 -0
  114. package/dist/types/tools/output-meta.d.ts +11 -1
  115. package/dist/types/tools/renderer.d.ts +37 -2
  116. package/dist/types/tools/streaming-output.d.ts +20 -1
  117. package/dist/types/tools/task.d.ts +0 -4
  118. package/dist/types/tools/write.d.ts +9 -2
  119. package/dist/types/tools/yield.d.ts +22 -0
  120. package/dist/types/tui.d.ts +31 -0
  121. package/dist/types/utils.d.ts +6 -0
  122. package/dist/types/vim.d.ts +19 -5
  123. package/package.json +13 -9
  124. package/src/app-keybindings.d.ts +1 -1
  125. package/src/app-keybindings.ts +1 -1
  126. package/src/apps/autoresearch-dashboard.ts +7 -3
  127. package/src/apps/cleanse-board.ts +2 -2
  128. package/src/apps/cleanse-picker.ts +1 -1
  129. package/src/apps/debug/log-viewer.ts +188 -72
  130. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  131. package/src/apps/debug/raw-sse.ts +39 -5
  132. package/src/apps/git/diff-pane.ts +26 -5
  133. package/src/apps/git/git-tui.ts +2 -2
  134. package/src/apps/git/help.ts +29 -20
  135. package/src/apps/if-bench-board.ts +2 -2
  136. package/src/apps/ps-top.ts +2 -2
  137. package/src/apps/setup-model-picker.ts +1 -1
  138. package/src/autocomplete.d.ts +5 -2
  139. package/src/autocomplete.ts +67 -43
  140. package/src/charts/chart-plan.d.ts +674 -0
  141. package/src/charts/chart-plan.ts +5081 -0
  142. package/src/charts/chart-svg.d.ts +59 -0
  143. package/src/charts/chart-svg.ts +4657 -0
  144. package/src/charts/table-data.d.ts +175 -0
  145. package/src/charts/table-data.ts +1478 -0
  146. package/src/chat/advisor-message.ts +11 -0
  147. package/src/chat/assistant-message.d.ts +5 -0
  148. package/src/chat/assistant-message.ts +219 -50
  149. package/src/chat/chat-transcript-builder.d.ts +2 -0
  150. package/src/chat/chat-transcript-builder.ts +5 -3
  151. package/src/chat/compaction-summary-message.d.ts +1 -0
  152. package/src/chat/compaction-summary-message.ts +8 -0
  153. package/src/chat/execution-shared.d.ts +6 -5
  154. package/src/chat/execution-shared.ts +42 -25
  155. package/src/chat/fence-figure.d.ts +42 -0
  156. package/src/chat/fence-figure.ts +80 -0
  157. package/src/chat/figure-markdown.d.ts +42 -0
  158. package/src/chat/figure-markdown.ts +174 -0
  159. package/src/chat/messages.d.ts +1 -1
  160. package/src/chat/messages.ts +1 -1
  161. package/src/chat/read-tool-group.ts +6 -4
  162. package/src/chat/skill-message.ts +6 -0
  163. package/src/chat/svg-figure.d.ts +33 -0
  164. package/src/chat/svg-figure.ts +399 -0
  165. package/src/chat/svg-source.d.ts +49 -0
  166. package/src/chat/svg-source.ts +188 -0
  167. package/src/chat/table-chart.d.ts +89 -0
  168. package/src/chat/table-chart.ts +294 -0
  169. package/src/chat/tool-execution.d.ts +2 -0
  170. package/src/chat/tool-execution.ts +80 -6
  171. package/src/chat/user-message.d.ts +15 -2
  172. package/src/chat/user-message.ts +55 -3
  173. package/src/chrome/shared.ts +53 -0
  174. package/src/chrome/transcript-container.d.ts +6 -0
  175. package/src/chrome/transcript-container.ts +93 -13
  176. package/src/components/box.d.ts +1 -0
  177. package/src/components/box.ts +5 -0
  178. package/src/components/disclosure.d.ts +1 -0
  179. package/src/components/disclosure.ts +8 -0
  180. package/src/components/editor.d.ts +15 -4
  181. package/src/components/editor.ts +294 -46
  182. package/src/components/form.d.ts +1 -0
  183. package/src/components/form.ts +8 -0
  184. package/src/components/image.d.ts +12 -44
  185. package/src/components/image.ts +47 -151
  186. package/src/components/key-value-list.d.ts +1 -0
  187. package/src/components/key-value-list.ts +4 -0
  188. package/src/components/layout/row.d.ts +1 -0
  189. package/src/components/layout/row.ts +7 -0
  190. package/src/components/layout/stack.d.ts +1 -0
  191. package/src/components/layout/stack.ts +7 -0
  192. package/src/components/markdown.d.ts +36 -3
  193. package/src/components/markdown.ts +433 -318
  194. package/src/components/scroll-view.d.ts +1 -0
  195. package/src/components/scroll-view.ts +5 -0
  196. package/src/components/section.d.ts +1 -0
  197. package/src/components/section.ts +5 -0
  198. package/src/components/settings-list.ts +2 -0
  199. package/src/components/table.d.ts +1 -0
  200. package/src/components/table.ts +4 -0
  201. package/src/components/text.d.ts +1 -0
  202. package/src/components/text.ts +7 -0
  203. package/src/components/tree-view.d.ts +7 -0
  204. package/src/components/tree-view.ts +38 -8
  205. package/src/components/truncated-text.d.ts +1 -0
  206. package/src/components/truncated-text.ts +5 -0
  207. package/src/desktop-notify.ts +1 -1
  208. package/src/fuzzy.d.ts +29 -1
  209. package/src/fuzzy.ts +115 -30
  210. package/src/key-hint-format.d.ts +1 -1
  211. package/src/key-hint-format.ts +1 -1
  212. package/src/keybinding-matchers.d.ts +3 -0
  213. package/src/keybinding-matchers.ts +6 -2
  214. package/src/keybindings.d.ts +1 -1
  215. package/src/keybindings.ts +1 -1
  216. package/src/kitty-graphics.d.ts +2 -2
  217. package/src/kitty-graphics.ts +4 -4
  218. package/src/loop-watchdog.d.ts +8 -7
  219. package/src/loop-watchdog.ts +18 -13
  220. package/src/multiplexers/cmux.d.ts +16 -0
  221. package/src/multiplexers/cmux.ts +29 -0
  222. package/src/multiplexers/herdr.d.ts +26 -0
  223. package/src/multiplexers/herdr.ts +64 -0
  224. package/src/multiplexers/notify.d.ts +13 -0
  225. package/src/multiplexers/notify.ts +24 -0
  226. package/src/multiplexers/orca.d.ts +12 -0
  227. package/src/multiplexers/orca.ts +16 -0
  228. package/src/multiplexers/screen.d.ts +9 -0
  229. package/src/multiplexers/screen.ts +15 -0
  230. package/src/multiplexers/tmux.d.ts +29 -0
  231. package/src/multiplexers/tmux.ts +40 -0
  232. package/src/multiplexers/types.d.ts +100 -0
  233. package/src/multiplexers/types.ts +110 -0
  234. package/src/multiplexers/wmux.d.ts +12 -0
  235. package/src/multiplexers/wmux.ts +16 -0
  236. package/src/multiplexers/zellij.d.ts +12 -0
  237. package/src/multiplexers/zellij.ts +24 -0
  238. package/src/native/backend.d.ts +3 -2
  239. package/src/native/backend.ts +174 -18
  240. package/src/native/blobs.d.ts +7 -3
  241. package/src/native/blobs.ts +47 -12
  242. package/src/native/encode.d.ts +3 -4
  243. package/src/native/encode.ts +58 -31
  244. package/src/native/overlay.d.ts +5 -0
  245. package/src/native/overlay.ts +13 -0
  246. package/src/native/reconcile.ts +4 -0
  247. package/src/overlays/agent-hub-projection.d.ts +6 -5
  248. package/src/overlays/agent-hub-projection.ts +124 -13
  249. package/src/overlays/agent-hub-types.d.ts +9 -0
  250. package/src/overlays/agent-hub-types.ts +6 -0
  251. package/src/overlays/agent-hub.ts +46 -31
  252. package/src/overlays/agent-transcript-viewer.ts +91 -23
  253. package/src/overlays/agents-hub.ts +28 -8
  254. package/src/overlays/annotation-overlay.ts +293 -90
  255. package/src/overlays/annotation-types.ts +2 -0
  256. package/src/overlays/ask-dialog.ts +7 -10
  257. package/src/overlays/btw-history-panel.ts +12 -1
  258. package/src/overlays/copy-selector.ts +30 -8
  259. package/src/overlays/extensions/state-manager.ts +52 -12
  260. package/src/overlays/history-search.ts +100 -7
  261. package/src/overlays/hook-selector.ts +12 -5
  262. package/src/overlays/jobs-panel.ts +28 -46
  263. package/src/overlays/model-browser.ts +341 -110
  264. package/src/overlays/model-hub.ts +300 -49
  265. package/src/overlays/move-overlay.ts +28 -14
  266. package/src/overlays/oauth-selector.ts +54 -70
  267. package/src/overlays/plan-review-overlay.ts +194 -70
  268. package/src/overlays/plugin-settings.ts +11 -5
  269. package/src/overlays/report-panel.ts +2 -2
  270. package/src/overlays/rewind-selector.ts +86 -17
  271. package/src/overlays/session-observer-registry.d.ts +2 -0
  272. package/src/overlays/session-observer-registry.ts +10 -2
  273. package/src/overlays/session-selector.ts +64 -26
  274. package/src/overlays/settings-defs.ts +4 -3
  275. package/src/overlays/settings-selector.ts +33 -8
  276. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  277. package/src/overlays/stats-notice.ts +1 -1
  278. package/src/overlays/tree-selector.ts +41 -20
  279. package/src/overlays/usage-dashboard.ts +100 -50
  280. package/src/overlays/usage-display.ts +10 -4
  281. package/src/prompt/attachment-chips.ts +15 -11
  282. package/src/prompt/composer-attachments.ts +6 -4
  283. package/src/prompt/composer-cache.ts +4 -6
  284. package/src/prompt/composer.ts +6 -37
  285. package/src/prompt/custom-editor.ts +54 -26
  286. package/src/prompt/image-source.ts +2 -1
  287. package/src/prompt/interactive-context-helpers.ts +30 -18
  288. package/src/prompt/macos-spelling.ts +7 -0
  289. package/src/prompt/magic-keywords.ts +11 -2
  290. package/src/prompt/model-mention-autocomplete.ts +14 -14
  291. package/src/prompt/tips.txt +6 -5
  292. package/src/prompt/welcome.ts +235 -411
  293. package/src/prompt/word-completion.ts +38 -5
  294. package/src/render/composite.d.ts +2 -0
  295. package/src/render/composite.ts +64 -0
  296. package/src/render/output-pane.d.ts +1 -0
  297. package/src/render/output-pane.ts +6 -0
  298. package/src/render/render-utils.d.ts +1 -0
  299. package/src/render/render-utils.ts +11 -0
  300. package/src/render/tool-card.d.ts +6 -0
  301. package/src/render/tool-card.ts +15 -0
  302. package/src/render/width-aware-text.d.ts +2 -0
  303. package/src/render/width-aware-text.ts +5 -0
  304. package/src/setup/scenes/model.ts +2 -1
  305. package/src/setup/scenes/splash.ts +116 -53
  306. package/src/setup/scenes/types.ts +1 -0
  307. package/src/status-line/component.ts +292 -78
  308. package/src/status-line/host.ts +4 -1
  309. package/src/status-line/segments.ts +40 -22
  310. package/src/status-line/types.ts +15 -3
  311. package/src/terminal-capabilities.d.ts +27 -35
  312. package/src/terminal-capabilities.ts +75 -189
  313. package/src/terminal-multiplexer.d.ts +127 -14
  314. package/src/terminal-multiplexer.ts +112 -33
  315. package/src/terminal.d.ts +22 -6
  316. package/src/terminal.ts +139 -37
  317. package/src/theme/active-symbols.d.ts +1 -1
  318. package/src/theme/active-symbols.ts +1 -1
  319. package/src/theme/schema-validation.ts +1 -0
  320. package/src/theme/schema.d.ts +11 -2
  321. package/src/theme/schema.ts +14 -1
  322. package/src/theme/session-color.ts +1 -7
  323. package/src/theme/symbols.d.ts +1 -1
  324. package/src/theme/symbols.ts +6 -1
  325. package/src/theme/theme-class.d.ts +7 -0
  326. package/src/theme/theme-class.ts +26 -1
  327. package/src/theme/theme-schema.json +5 -1
  328. package/src/theme/theme.d.ts +1 -1
  329. package/src/theme/theme.ts +8 -4
  330. package/src/theme/tui-adapters.d.ts +3 -0
  331. package/src/theme/tui-adapters.ts +68 -0
  332. package/src/tmux.d.ts +11 -4
  333. package/src/tmux.ts +47 -11
  334. package/src/tools/ast-edit.ts +59 -35
  335. package/src/tools/ast-grep.ts +35 -16
  336. package/src/tools/bash.ts +92 -42
  337. package/src/tools/daemon.d.ts +1 -1
  338. package/src/tools/daemon.ts +1 -1
  339. package/src/tools/default-renderer.ts +35 -19
  340. package/src/tools/edit.ts +142 -120
  341. package/src/tools/eval-format/index.ts +28 -2
  342. package/src/tools/eval-format/javascript.ts +7 -5
  343. package/src/tools/eval.d.ts +22 -3
  344. package/src/tools/eval.ts +364 -208
  345. package/src/tools/fetch.ts +5 -6
  346. package/src/tools/glob.d.ts +4 -0
  347. package/src/tools/glob.ts +33 -65
  348. package/src/tools/grep.ts +81 -48
  349. package/src/tools/index.ts +2 -0
  350. package/src/tools/lsp.ts +81 -59
  351. package/src/tools/mcp.ts +111 -68
  352. package/src/tools/native-view.d.ts +47 -0
  353. package/src/tools/native-view.ts +102 -1
  354. package/src/tools/output-meta.d.ts +11 -1
  355. package/src/tools/output-meta.ts +23 -3
  356. package/src/tools/read.ts +7 -5
  357. package/src/tools/renderer.d.ts +37 -2
  358. package/src/tools/renderer.ts +42 -2
  359. package/src/tools/streaming-output.d.ts +20 -1
  360. package/src/tools/streaming-output.ts +149 -50
  361. package/src/tools/task.d.ts +0 -4
  362. package/src/tools/task.ts +112 -59
  363. package/src/tools/todo.ts +119 -30
  364. package/src/tools/wait.ts +34 -24
  365. package/src/tools/web-search.ts +1 -1
  366. package/src/tools/write.d.ts +9 -2
  367. package/src/tools/write.ts +147 -95
  368. package/src/tools/yield.d.ts +22 -0
  369. package/src/tools/yield.ts +225 -0
  370. package/src/tui.d.ts +31 -0
  371. package/src/tui.ts +309 -160
  372. package/src/utils.d.ts +6 -0
  373. package/src/utils.ts +19 -0
  374. package/src/vim.d.ts +19 -5
  375. package/src/vim.ts +333 -18
  376. package/src/windows-input-mode.ts +8 -0
@@ -31,6 +31,15 @@ export interface OutputSummary {
31
31
  columnDroppedBytes?: number;
32
32
  /** Number of distinct lines that hit the per-line column cap. */
33
33
  columnTruncatedLines?: number;
34
+ /**
35
+ * 1-indexed first and last lines the column cap cut, numbered as in the
36
+ * artifact. Absent when the numbering cannot match the artifact (an inline
37
+ * substitute was capped, or `replace()` rewrote the buffer).
38
+ */
39
+ columnTruncatedRange?: {
40
+ first: number;
41
+ last: number;
42
+ };
34
43
  /** Configured per-line column cap in effect (UTF-8 bytes), when > 0. */
35
44
  columnMax?: number;
36
45
  /** Artifact ID for internal URL access (artifact://<id>) when truncated */
@@ -236,7 +245,8 @@ export declare class OutputSink {
236
245
  constructor(options?: OutputSinkOptions);
237
246
  /**
238
247
  * Push a chunk of output. The buffer management and onChunk callback run
239
- * synchronously. File sink writes are deferred and serialized internally.
248
+ * synchronously; onChunk fires after the chunk is stored, so {@link preview}
249
+ * already includes it. File sink writes are deferred and serialized internally.
240
250
  *
241
251
  * `inline` substitutes a bounded representation for the in-memory buffer and
242
252
  * live preview while the complete chunk is mirrored to the artifact.
@@ -260,6 +270,15 @@ export declare class OutputSink {
260
270
  * branch in `dump()` against stale totals.
261
271
  */
262
272
  replace(text: string): void;
273
+ /**
274
+ * Current inline view without finalizing: the body {@link dump} would
275
+ * return right now (head + elision marker + rolling tail), minus any notice
276
+ * and minus the newline `dump()` appends after output that ends in a bare
277
+ * carriage return. Each call trims the rolling tail to its budget (a copy
278
+ * of the tail window once it overflowed), so call it from a throttled
279
+ * onChunk (`chunkThrottleMs`), not once per raw chunk.
280
+ */
281
+ preview(): string;
263
282
  dump(notice?: string): Promise<OutputSummary>;
264
283
  /**
265
284
  * Release the artifact spill descriptor on an exit path that skips
@@ -52,6 +52,12 @@ export interface OutputSummary {
52
52
  columnDroppedBytes?: number;
53
53
  /** Number of distinct lines that hit the per-line column cap. */
54
54
  columnTruncatedLines?: number;
55
+ /**
56
+ * 1-indexed first and last lines the column cap cut, numbered as in the
57
+ * artifact. Absent when the numbering cannot match the artifact (an inline
58
+ * substitute was capped, or `replace()` rewrote the buffer).
59
+ */
60
+ columnTruncatedRange?: { first: number; last: number };
55
61
  /** Configured per-line column cap in effect (UTF-8 bytes), when > 0. */
56
62
  columnMax?: number;
57
63
  /** Artifact ID for internal URL access (artifact://<id>) when truncated */
@@ -233,6 +239,10 @@ function truncateBytesWindowed(
233
239
  ? data.substring(0, Math.min(data.length, maxBytes))
234
240
  : data.substring(Math.max(0, data.length - maxBytes));
235
241
 
242
+ // All-ASCII window: one byte per char, so it is exactly `maxBytes` bytes on a boundary.
243
+ const windowBytes = Buffer.byteLength(window, "utf-8");
244
+ if (windowBytes === window.length) return { text: window, bytes: windowBytes };
245
+
236
246
  const buf = Buffer.from(window, "utf-8");
237
247
 
238
248
  if (mode === "head") {
@@ -876,6 +886,8 @@ export class TailBuffer {
876
886
  export class OutputSink {
877
887
  #buffer = "";
878
888
  #bufferBytes = 0;
889
+ /** The overflowed tail window may exceed its budget until {@link #trimTail} runs. */
890
+ #tailUntrimmed = false;
879
891
  #head = "";
880
892
  #headBytes = 0;
881
893
  #headLines = 0; // newline count inside #head
@@ -895,6 +907,10 @@ export class OutputSink {
895
907
  #columnEllipsisAdded = false;
896
908
  #columnDroppedBytes = 0;
897
909
  #columnTruncatedLines = 0;
910
+ #columnFirstCutLine = 0;
911
+ #columnLastCutLine = 0;
912
+ /** Set once a cut line's number may not match the artifact; the range is then withheld. */
913
+ #columnCutLinesUnmapped = false;
898
914
  #file?: {
899
915
  path: string;
900
916
  artifactId?: string;
@@ -1002,7 +1018,8 @@ export class OutputSink {
1002
1018
 
1003
1019
  /**
1004
1020
  * Push a chunk of output. The buffer management and onChunk callback run
1005
- * synchronously. File sink writes are deferred and serialized internally.
1021
+ * synchronously; onChunk fires after the chunk is stored, so {@link preview}
1022
+ * already includes it. File sink writes are deferred and serialized internally.
1006
1023
  *
1007
1024
  * `inline` substitutes a bounded representation for the in-memory buffer and
1008
1025
  * live preview while the complete chunk is mirrored to the artifact.
@@ -1015,6 +1032,8 @@ export class OutputSink {
1015
1032
  const substituted = inline !== undefined;
1016
1033
  const inlineChunk = inline === undefined ? chunk : sanitizeText(inline);
1017
1034
 
1035
+ this.#store(chunk, inlineChunk, substituted);
1036
+
1018
1037
  // Throttled onChunk: coalesce chunks arriving inside the throttle window.
1019
1038
  // A timer flushes quiet tails at the throttle boundary; dump() catches a
1020
1039
  // final pending chunk when the process exits before that timer fires.
@@ -1029,9 +1048,15 @@ export class OutputSink {
1029
1048
  this.#schedulePendingChunkFlush();
1030
1049
  }
1031
1050
  }
1051
+ }
1032
1052
 
1053
+ /** Count, cap, mirror, and retain one sanitized chunk. */
1054
+ #store(chunk: string, inlineChunk: string, substituted: boolean): void {
1033
1055
  const rawBytes = Buffer.byteLength(chunk, "utf-8");
1034
1056
  this.#totalBytes += rawBytes;
1057
+ // Line the chunk starts on, as numbered in the artifact; 0 when the capped
1058
+ // inline text is a substitute whose lines do not map onto the artifact.
1059
+ const firstLine = substituted ? 0 : this.#totalLines + 1;
1035
1060
 
1036
1061
  if (chunk.length > 0) {
1037
1062
  this.#sawData = true;
@@ -1042,7 +1067,7 @@ export class OutputSink {
1042
1067
  // Per-line column cap. State persists across chunks so a mid-line split
1043
1068
  // still respects the budget. Operates on the inline chunk; the complete
1044
1069
  // chunk is mirrored to the artifact before any cap is applied.
1045
- const capped = this.#maxColumns > 0 ? this.#applyColumnCap(inlineChunk) : inlineChunk;
1070
+ const capped = this.#maxColumns > 0 ? this.#applyColumnCap(inlineChunk, firstLine) : inlineChunk;
1046
1071
  const cappedBytes = capped === inlineChunk ? inlineBytes : Buffer.byteLength(capped, "utf-8");
1047
1072
  const cappedThisChunk = cappedBytes < inlineBytes;
1048
1073
  if (substituted) this.#truncated = true;
@@ -1090,11 +1115,13 @@ export class OutputSink {
1090
1115
  * cap; subsequent bytes are skipped until the next `\n`. State persists
1091
1116
  * across calls so a long line split across chunks still produces one marker.
1092
1117
  */
1093
- #applyColumnCap(chunk: string): string {
1118
+ #applyColumnCap(chunk: string, firstLine: number): string {
1094
1119
  if (chunk.length === 0) return chunk;
1120
+ if (!this.#columnEllipsisAdded && this.#fitsColumnCap(chunk)) return chunk;
1095
1121
  const max = this.#maxColumns;
1096
1122
  const parts: string[] = [];
1097
1123
  let cursor = 0;
1124
+ let line = firstLine;
1098
1125
  while (cursor < chunk.length) {
1099
1126
  const nlIdx = chunk.indexOf(NL, cursor);
1100
1127
  const segEnd = nlIdx === -1 ? chunk.length : nlIdx;
@@ -1125,6 +1152,12 @@ export class OutputSink {
1125
1152
  parts.push(ELLIPSIS);
1126
1153
  this.#columnDroppedBytes += segBytes - keptBytes;
1127
1154
  this.#columnTruncatedLines++;
1155
+ if (line > 0) {
1156
+ if (this.#columnFirstCutLine === 0) this.#columnFirstCutLine = line;
1157
+ this.#columnLastCutLine = line;
1158
+ } else {
1159
+ this.#columnCutLinesUnmapped = true;
1160
+ }
1128
1161
  this.#currentLineBytes += keptBytes + ellipsisBytes;
1129
1162
  this.#columnEllipsisAdded = true;
1130
1163
  }
@@ -1135,10 +1168,37 @@ export class OutputSink {
1135
1168
  this.#currentLineBytes = 0;
1136
1169
  this.#columnEllipsisAdded = false;
1137
1170
  cursor = nlIdx + 1;
1171
+ if (line > 0) line++;
1138
1172
  }
1139
1173
  return parts.join("");
1140
1174
  }
1141
1175
 
1176
+ /**
1177
+ * Fast path for {@link #applyColumnCap}: when every line segment fits the
1178
+ * remaining cap even at the 3-bytes-per-code-unit UTF-8 worst case, the
1179
+ * chunk passes through unchanged and only the trailing line width is
1180
+ * measured. Returns false (state untouched) when any line might trip the cap.
1181
+ */
1182
+ #fitsColumnCap(chunk: string): boolean {
1183
+ const max = this.#maxColumns;
1184
+ let room = max - this.#currentLineBytes;
1185
+ let cursor = 0;
1186
+ for (;;) {
1187
+ const nlIdx = chunk.indexOf(NL, cursor);
1188
+ const segEnd = nlIdx === -1 ? chunk.length : nlIdx;
1189
+ if ((segEnd - cursor) * 3 > room) return false;
1190
+ if (nlIdx === -1) break;
1191
+ cursor = nlIdx + 1;
1192
+ room = max;
1193
+ }
1194
+ if (cursor === 0) {
1195
+ this.#currentLineBytes += Buffer.byteLength(chunk, "utf-8");
1196
+ } else {
1197
+ this.#currentLineBytes = cursor === chunk.length ? 0 : Buffer.byteLength(chunk.substring(cursor), "utf-8");
1198
+ }
1199
+ return true;
1200
+ }
1201
+
1142
1202
  // The rolling tail budget is whatever the head window has not consumed of
1143
1203
  // the inline budget: `spillThreshold - #headBytes`. While the head window
1144
1204
  // fills it shrinks toward `spillThreshold - headLimit`; after `replace()`
@@ -1172,15 +1232,27 @@ export class OutputSink {
1172
1232
  const { text, bytes } = truncateTailBytes(chunk, threshold);
1173
1233
  this.#buffer = text;
1174
1234
  this.#bufferBytes = bytes;
1175
- } else {
1176
- // Intermediate size is bounded (<= threshold + dataBytes), safe to concat.
1177
- this.#buffer += chunk;
1178
- this.#bufferBytes += dataBytes;
1179
-
1180
- const { text, bytes } = truncateTailBytes(this.#buffer, threshold);
1181
- this.#buffer = text;
1182
- this.#bufferBytes = bytes;
1235
+ this.#tailUntrimmed = false;
1236
+ return;
1183
1237
  }
1238
+ // Amortize the trim: let the window grow to 2× the budget before cutting it
1239
+ // back, so steady-state chunks cost an append instead of a window copy.
1240
+ // `#trimTail()` restores the exact budget before the buffer is read.
1241
+ this.#buffer += chunk;
1242
+ this.#bufferBytes += dataBytes;
1243
+ this.#tailUntrimmed = true;
1244
+ if (this.#bufferBytes > threshold * 2) this.#trimTail();
1245
+ }
1246
+
1247
+ /** Cut an overflowed tail window back to the current tail budget. */
1248
+ #trimTail(): void {
1249
+ if (!this.#tailUntrimmed) return;
1250
+ this.#tailUntrimmed = false;
1251
+ const threshold = Math.max(0, this.#spillThreshold - this.#headBytes);
1252
+ if (this.#bufferBytes <= threshold) return;
1253
+ const { text, bytes } = truncateTailBytes(this.#buffer, threshold);
1254
+ this.#buffer = text;
1255
+ this.#bufferBytes = bytes;
1184
1256
  }
1185
1257
 
1186
1258
  /**
@@ -1372,6 +1444,7 @@ export class OutputSink {
1372
1444
  this.#clearPendingChunkTimer();
1373
1445
  this.#buffer = text;
1374
1446
  this.#bufferBytes = Buffer.byteLength(text, "utf-8");
1447
+ this.#tailUntrimmed = false;
1375
1448
  this.#head = "";
1376
1449
  this.#headBytes = 0;
1377
1450
  this.#headLines = 0;
@@ -1384,6 +1457,8 @@ export class OutputSink {
1384
1457
  this.#columnEllipsisAdded = false;
1385
1458
  this.#columnDroppedBytes = 0;
1386
1459
  this.#columnTruncatedLines = 0;
1460
+ // The artifact keeps the replaced stream, so later line numbers no longer match it.
1461
+ this.#columnCutLinesUnmapped = true;
1387
1462
  this.#pendingChunk = "";
1388
1463
  this.#pendingCarriageReturn = false;
1389
1464
  }
@@ -1458,22 +1533,31 @@ export class OutputSink {
1458
1533
  }
1459
1534
  }
1460
1535
 
1461
- async dump(notice?: string): Promise<OutputSummary> {
1462
- if (this.#pendingCarriageReturn) {
1463
- this.#pendingCarriageReturn = false;
1464
- this.push(NL);
1465
- }
1466
- const noticeLine = notice ? `[${notice}]\n` : "";
1536
+ /**
1537
+ * Current inline view without finalizing: the body {@link dump} would
1538
+ * return right now (head + elision marker + rolling tail), minus any notice
1539
+ * and minus the newline `dump()` appends after output that ends in a bare
1540
+ * carriage return. Each call trims the rolling tail to its budget (a copy
1541
+ * of the tail window once it overflowed), so call it from a throttled
1542
+ * onChunk (`chunkThrottleMs`), not once per raw chunk.
1543
+ */
1544
+ preview(): string {
1545
+ return this.#composeBody().body;
1546
+ }
1467
1547
 
1468
- // Flush any chunk still held back by the throttle so the live preview
1469
- // ends with the complete stream.
1470
- this.#flushPendingChunk();
1548
+ /**
1549
+ * Compose the visible output. With head retention, splice head + marker +
1550
+ * tail when content was elided. Otherwise return the rolling buffer.
1551
+ */
1552
+ #composeBody(): {
1553
+ body: string;
1554
+ outputBytes: number;
1555
+ outputLines: number;
1556
+ elidedBytes?: number;
1557
+ elidedLines?: number;
1558
+ } {
1559
+ this.#trimTail();
1471
1560
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1472
-
1473
- await this.#finalizeFile();
1474
-
1475
- // Compose the visible output. With head retention, splice head + marker
1476
- // + tail when content was elided. Otherwise return the rolling buffer.
1477
1561
  const headBytes = this.#headBytes;
1478
1562
  const tailBuf = this.#buffer;
1479
1563
  const tailBytes = this.#bufferBytes;
@@ -1484,39 +1568,50 @@ export class OutputSink {
1484
1568
  // so column-dropped bytes don't inflate the "elided from middle" count.
1485
1569
  const effectiveTotalBytes = Math.max(0, this.#totalBytes - this.#columnDroppedBytes);
1486
1570
 
1487
- let body: string;
1488
- let outputBytes: number;
1489
- let outputLines: number;
1490
- let elidedBytes: number | undefined;
1491
- let elidedLines: number | undefined;
1492
-
1493
1571
  if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
1494
1572
  // Middle was elided. Emit head + marker + tail.
1495
- elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
1496
- elidedLines = Math.max(0, totalLines - headLines - tailLines);
1573
+ const elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
1574
+ const elidedLines = Math.max(0, totalLines - headLines - tailLines);
1497
1575
  const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
1498
1576
  const markerBytes = Buffer.byteLength(marker, "utf-8");
1499
1577
  const headSep = this.#head.endsWith("\n") ? "" : "\n";
1500
1578
  const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
1501
- body = `${this.#head}${headSep}${marker}${tailSep}${tailBuf}`;
1502
- outputBytes =
1503
- headBytes +
1504
- markerBytes +
1505
- tailBytes +
1506
- Buffer.byteLength(headSep, "utf-8") +
1507
- Buffer.byteLength(tailSep, "utf-8");
1508
- outputLines = headLines + 1 + tailLines;
1509
- this.#truncated = true;
1510
- } else if (headBytes > 0) {
1579
+ return {
1580
+ body: `${this.#head}${headSep}${marker}${tailSep}${tailBuf}`,
1581
+ outputBytes: headBytes + markerBytes + tailBytes + headSep.length + tailSep.length,
1582
+ outputLines: headLines + 1 + tailLines,
1583
+ elidedBytes,
1584
+ elidedLines,
1585
+ };
1586
+ }
1587
+ if (headBytes > 0) {
1511
1588
  // Head + tail combine into the full buffered output (no overlap or elision).
1512
- body = `${this.#head}${tailBuf}`;
1513
- outputBytes = headBytes + tailBytes;
1514
- outputLines = body.length > 0 ? countNewlines(body) + 1 : 0;
1515
- } else {
1516
- body = tailBuf;
1517
- outputBytes = tailBytes;
1518
- outputLines = tailLines;
1589
+ const body = `${this.#head}${tailBuf}`;
1590
+ return {
1591
+ body,
1592
+ outputBytes: headBytes + tailBytes,
1593
+ outputLines: body.length > 0 ? countNewlines(body) + 1 : 0,
1594
+ };
1519
1595
  }
1596
+ return { body: tailBuf, outputBytes: tailBytes, outputLines: tailLines };
1597
+ }
1598
+
1599
+ async dump(notice?: string): Promise<OutputSummary> {
1600
+ if (this.#pendingCarriageReturn) {
1601
+ this.#pendingCarriageReturn = false;
1602
+ this.push(NL);
1603
+ }
1604
+ const noticeLine = notice ? `[${notice}]\n` : "";
1605
+
1606
+ // Flush any chunk still held back by the throttle so the live preview
1607
+ // ends with the complete stream.
1608
+ this.#flushPendingChunk();
1609
+ const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1610
+
1611
+ await this.#finalizeFile();
1612
+
1613
+ const { body, outputBytes, outputLines, elidedBytes, elidedLines } = this.#composeBody();
1614
+ if (elidedBytes !== undefined) this.#truncated = true;
1520
1615
 
1521
1616
  return {
1522
1617
  output: `${noticeLine}${body}`,
@@ -1530,6 +1625,10 @@ export class OutputSink {
1530
1625
  columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
1531
1626
  columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
1532
1627
  columnMax: this.#columnTruncatedLines > 0 ? this.#maxColumns : undefined,
1628
+ columnTruncatedRange:
1629
+ this.#columnFirstCutLine > 0 && !this.#columnCutLinesUnmapped
1630
+ ? { first: this.#columnFirstCutLine, last: this.#columnLastCutLine }
1631
+ : undefined,
1533
1632
  artifactId: this.#artifactError ? undefined : this.#file?.artifactId,
1534
1633
  artifactElidedBytes:
1535
1634
  this.#artifactError || !this.#file?.artifactId || this.#artifactElidedBytes === 0
@@ -107,8 +107,6 @@ export interface TaskItem {
107
107
  solutionSpace?: string;
108
108
  /** Per-spawn thinking effort: lowest/middle/highest level the resolved model supports. Overrides the agent's default selector (e.g. `auto`). */
109
109
  effort?: "lo" | "med" | "hi";
110
- /** Per-spawn model selector or ordered selector array; overrides agent and settings preferences. */
111
- model?: string | string[];
112
110
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
113
111
  outputSchema?: unknown;
114
112
  /** Validation behavior for a caller-provided or inherited output schema. */
@@ -135,8 +133,6 @@ export interface TaskParams {
135
133
  solutionSpace?: string;
136
134
  /** Per-spawn thinking effort (flat form): lowest/middle/highest level the resolved model supports. */
137
135
  effort?: "lo" | "med" | "hi";
138
- /** Per-spawn model selector or ordered selector array; overrides agent and settings preferences. */
139
- model?: string | string[];
140
136
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
141
137
  outputSchema?: unknown;
142
138
  /** Validation behavior for a caller-provided or inherited output schema. */
package/src/tools/task.ts CHANGED
@@ -47,7 +47,7 @@ import { assembleYieldResult, type YieldSectionShapes } from "./task-yield-assem
47
47
  import type { TspAgentProps, TspTone } from "@linxiraos/pi-wire";
48
48
  import { compact, kv, md, node, span, text } from "../native/describe";
49
49
  import type { NativeNode } from "../native/node";
50
- import { OwnerMemo } from "../native/memo";
50
+ import { OwnerMemo, sameItems } from "../native/memo";
51
51
  import { plainText } from "../native/spans";
52
52
  import { errorText, noteText, resultText } from "./native-view";
53
53
  import { taskSummary } from "../overlays/agent-hub-renderer";
@@ -242,6 +242,39 @@ function renderTypedYieldSections(value: unknown, continuePrefix: string, expand
242
242
  return lines;
243
243
  }
244
244
 
245
+ /**
246
+ * Per-frame memo for derived agent rows. Live task cards re-render at spinner
247
+ * cadence, while yield/recent-output arrays only change when the subagent
248
+ * reports. Entries are keyed by the source array (or object) and validated
249
+ * against a shallow snapshot of its elements plus the render deps, so in-place
250
+ * pushes/shifts invalidate correctly.
251
+ */
252
+ interface SnapshotMemoEntry<T> {
253
+ items: unknown[];
254
+ deps: unknown[];
255
+ value: T;
256
+ }
257
+
258
+ function memoBySnapshot<T>(
259
+ cache: WeakMap<object, SnapshotMemoEntry<T>>,
260
+ source: unknown,
261
+ deps: unknown[],
262
+ compute: () => T,
263
+ ): T {
264
+ if (source === null || typeof source !== "object") return compute();
265
+ const items: unknown[] = Array.isArray(source) ? source.slice() : [source];
266
+ const entry = cache.get(source);
267
+ if (entry && sameItems(entry.items, items) && sameItems(entry.deps, deps)) return entry.value;
268
+ const value = compute();
269
+ cache.set(source, { items, deps, value });
270
+ return value;
271
+ }
272
+
273
+ const yieldSectionsMemo = new WeakMap<object, SnapshotMemoEntry<string[]>>();
274
+ const completedReviewMemo = new WeakMap<object, SnapshotMemoEntry<string[] | null>>();
275
+ const recentOutputMemo = new WeakMap<object, SnapshotMemoEntry<string[]>>();
276
+ const agentResultMemo = new WeakMap<object, SnapshotMemoEntry<string[]>>();
277
+
245
278
  /** Formats sanitized task identifiers as hierarchy breadcrumbs. */
246
279
  export function formatTaskId(id: string): string {
247
280
  // Ids are name-based (e.g. "Anna", "Anna-2"); a "." separates nesting levels
@@ -776,27 +809,32 @@ function renderAgentProgress(
776
809
  // For completed tasks, render review verdicts assembled from incremental
777
810
  // yield sections.
778
811
  if (progress.status === "completed") {
779
- const completeData = normalizeYieldData(progress.extractedToolData.yield);
780
- const incrementalReview = extractIncrementalReviewResult(completeData);
781
- if (incrementalReview) {
782
- lines.push(
783
- ...renderReviewResult(
784
- incrementalReview.summary,
785
- incrementalReview.findings,
786
- continuePrefix,
787
- expanded,
788
- theme,
789
- ),
790
- );
791
- return lines; // Review result handles its own rendering
792
- }
793
- const reviewData = completeData
794
- .map(c => c.data as SubmitReviewDetails)
795
- .filter(d => d && typeof d === "object" && "overall_correctness" in d);
796
- if (reviewData.length > 0) {
797
- const summary = reviewData[reviewData.length - 1];
798
- const findings: FindingDetails[] = [];
799
- lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
812
+ const yieldValue = progress.extractedToolData.yield;
813
+ const reviewLines = memoBySnapshot(
814
+ completedReviewMemo,
815
+ yieldValue,
816
+ [continuePrefix, expanded, theme],
817
+ (): string[] | null => {
818
+ const completeData = normalizeYieldData(yieldValue);
819
+ const incrementalReview = extractIncrementalReviewResult(completeData);
820
+ if (incrementalReview) {
821
+ return renderReviewResult(
822
+ incrementalReview.summary,
823
+ incrementalReview.findings,
824
+ continuePrefix,
825
+ expanded,
826
+ theme,
827
+ );
828
+ }
829
+ const reviewData = completeData
830
+ .map(c => c.data as SubmitReviewDetails)
831
+ .filter(d => d && typeof d === "object" && "overall_correctness" in d);
832
+ if (reviewData.length === 0) return null;
833
+ return renderReviewResult(reviewData[reviewData.length - 1], [], continuePrefix, expanded, theme);
834
+ },
835
+ );
836
+ if (reviewLines) {
837
+ lines.push(...reviewLines);
800
838
  return lines; // Review result handles its own rendering
801
839
  }
802
840
  }
@@ -804,7 +842,11 @@ function renderAgentProgress(
804
842
  for (const toolName in progress.extractedToolData) {
805
843
  const dataArray = progress.extractedToolData[toolName];
806
844
  if (toolName === "yield") {
807
- lines.push(...renderTypedYieldSections(dataArray, continuePrefix, expanded, theme));
845
+ lines.push(
846
+ ...memoBySnapshot(yieldSectionsMemo, dataArray, [continuePrefix, expanded, theme], () =>
847
+ renderTypedYieldSections(dataArray, continuePrefix, expanded, theme),
848
+ ),
849
+ );
808
850
  continue;
809
851
  }
810
852
 
@@ -862,15 +904,19 @@ function renderAgentProgress(
862
904
  // Expanded view: recent output and tools
863
905
  if (expanded && progress.status === "running") {
864
906
  const previewRows = previewWindowRows();
865
- const output = capPreviewLines(
866
- sanitizeRecentOutput([...progress.recentOutput].reverse().join("\n")).split("\n"),
867
- theme,
868
- {
869
- max: previewRows,
870
- expandHint: false,
871
- },
872
- ).join("\n");
873
- lines.push(...renderOutputSection(output, continuePrefix, expanded, theme, 2, previewRows));
907
+ lines.push(
908
+ ...memoBySnapshot(recentOutputMemo, progress.recentOutput, [continuePrefix, theme, previewRows], () => {
909
+ const output = capPreviewLines(
910
+ sanitizeRecentOutput([...progress.recentOutput].reverse().join("\n")).split("\n"),
911
+ theme,
912
+ {
913
+ max: previewRows,
914
+ expandHint: false,
915
+ },
916
+ ).join("\n");
917
+ return renderOutputSection(output, continuePrefix, expanded, theme, 2, previewRows);
918
+ }),
919
+ );
874
920
  }
875
921
 
876
922
  return lines;
@@ -1347,7 +1393,7 @@ export function renderResult(
1347
1393
  const aborted = abortedCount > 0;
1348
1394
  const failed = failCount > 0;
1349
1395
  const mergeFailed = mergeFailedCount > 0;
1350
- const isError = result.isError === true || aborted || failed;
1396
+ const isError = aborted || failed;
1351
1397
  const agentCount = hasResults ? details.results.length : (details.progress?.length ?? 0);
1352
1398
  const icon: ToolUIStatus = options.isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
1353
1399
  // Header meta is the spawn count only; each row carries its own ⟨agent⟩
@@ -1373,6 +1419,25 @@ export function renderResult(
1373
1419
  theme,
1374
1420
  );
1375
1421
 
1422
+ // Fallback text is fixed for this snapshot; derive its trailing notice rows
1423
+ // once instead of re-splitting on every spinner frame.
1424
+ const fallbackExtraLines: string[] = [];
1425
+ if (fallbackText.trim()) {
1426
+ const summaryLines = fallbackText.split("\n");
1427
+ const markerIndex = summaryLines.findIndex(
1428
+ line =>
1429
+ line.includes("<system-notification>") ||
1430
+ line.startsWith("Applied patches:") ||
1431
+ line.startsWith("No changes to apply."),
1432
+ );
1433
+ if (markerIndex >= 0) {
1434
+ for (let i = markerIndex; i < summaryLines.length; i++) {
1435
+ const line = summaryLines[i];
1436
+ if (line.trim()) fallbackExtraLines.push(theme.fg("dim", line));
1437
+ }
1438
+ }
1439
+ }
1440
+
1376
1441
  return framedToolCard(theme, ({ width, contentWidth }) => {
1377
1442
  const { expanded, isPartial, spinnerFrame } = options;
1378
1443
  const frozen = options.renderContext?.frozen === true;
@@ -1414,7 +1479,11 @@ export function renderResult(
1414
1479
  const ordered = orderResultsForDisplay(details.results);
1415
1480
  const visible = expanded ? ordered : selectCollapsedResults(ordered);
1416
1481
  for (const res of visible) {
1417
- lines.push(...renderAgentResult(res, "", " ", expanded, theme, undefined, 0, contentWidth));
1482
+ lines.push(
1483
+ ...memoBySnapshot(agentResultMemo, res, [expanded, theme, contentWidth, isFeedModelBadgeEnabled()], () =>
1484
+ renderAgentResult(res, "", " ", expanded, theme, undefined, 0, contentWidth),
1485
+ ),
1486
+ );
1418
1487
  }
1419
1488
  if (visible.length < ordered.length) {
1420
1489
  const hint = formatExpandHint(theme, false, true);
@@ -1427,11 +1496,14 @@ export function renderResult(
1427
1496
  // (their payloads deliver through jobs) — keep their rows visible
1428
1497
  // beside the finalized inline results, live while running and
1429
1498
  // settled once their jobs finish.
1430
- const supplementalProgress = details.progress
1431
- ? orderProgressForDisplay(
1432
- details.progress.filter(progress => !details.results.some(res => res.id === progress.id)),
1433
- )
1434
- : [];
1499
+ let supplementalProgress: AgentProgress[] = [];
1500
+ if (details.progress) {
1501
+ const resultIds = new Set<string>();
1502
+ for (const res of details.results) resultIds.add(res.id);
1503
+ supplementalProgress = orderProgressForDisplay(
1504
+ details.progress.filter(progress => !resultIds.has(progress.id)),
1505
+ );
1506
+ }
1435
1507
  for (const progress of supplementalProgress) {
1436
1508
  lines.push(
1437
1509
  ...renderAgentProgress(
@@ -1484,22 +1556,7 @@ export function renderResult(
1484
1556
  };
1485
1557
  }
1486
1558
 
1487
- if (fallbackText.trim()) {
1488
- const summaryLines = fallbackText.split("\n");
1489
- const markerIndex = summaryLines.findIndex(
1490
- line =>
1491
- line.includes("<system-notification>") ||
1492
- line.startsWith("Applied patches:") ||
1493
- line.startsWith("No changes to apply."),
1494
- );
1495
- if (markerIndex >= 0) {
1496
- const extra = summaryLines.slice(markerIndex);
1497
- for (const line of extra) {
1498
- if (!line.trim()) continue;
1499
- lines.push(theme.fg("dim", line));
1500
- }
1501
- }
1502
- }
1559
+ for (const line of fallbackExtraLines) lines.push(line);
1503
1560
 
1504
1561
  while (lines.length > 0 && lines[0].trim() === "") lines.shift();
1505
1562
  return {
@@ -2149,8 +2206,6 @@ export interface TaskItem {
2149
2206
  solutionSpace?: string;
2150
2207
  /** Per-spawn thinking effort: lowest/middle/highest level the resolved model supports. Overrides the agent's default selector (e.g. `auto`). */
2151
2208
  effort?: "lo" | "med" | "hi";
2152
- /** Per-spawn model selector or ordered selector array; overrides agent and settings preferences. */
2153
- model?: string | string[];
2154
2209
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
2155
2210
  outputSchema?: unknown;
2156
2211
  /** Validation behavior for a caller-provided or inherited output schema. */
@@ -2178,8 +2233,6 @@ export interface TaskParams {
2178
2233
  solutionSpace?: string;
2179
2234
  /** Per-spawn thinking effort (flat form): lowest/middle/highest level the resolved model supports. */
2180
2235
  effort?: "lo" | "med" | "hi";
2181
- /** Per-spawn model selector or ordered selector array; overrides agent and settings preferences. */
2182
- model?: string | string[];
2183
2236
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
2184
2237
  outputSchema?: unknown;
2185
2238
  /** Validation behavior for a caller-provided or inherited output schema. */