@linxiraos/pi-tui 1.1.27 → 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 (373) hide show
  1. package/CHANGELOG.md +47 -463
  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 +1 -1
  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 +6 -1
  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/types.d.ts +1 -0
  95. package/dist/types/status-line/component.d.ts +3 -0
  96. package/dist/types/status-line/host.d.ts +4 -1
  97. package/dist/types/status-line/segments.d.ts +2 -0
  98. package/dist/types/status-line/types.d.ts +14 -3
  99. package/dist/types/terminal-capabilities.d.ts +27 -35
  100. package/dist/types/terminal-multiplexer.d.ts +127 -14
  101. package/dist/types/terminal.d.ts +22 -6
  102. package/dist/types/theme/active-symbols.d.ts +1 -1
  103. package/dist/types/theme/schema.d.ts +11 -2
  104. package/dist/types/theme/symbols.d.ts +1 -1
  105. package/dist/types/theme/theme-class.d.ts +7 -0
  106. package/dist/types/theme/theme.d.ts +1 -1
  107. package/dist/types/theme/tui-adapters.d.ts +3 -0
  108. package/dist/types/tmux.d.ts +11 -4
  109. package/dist/types/tools/daemon.d.ts +1 -1
  110. package/dist/types/tools/eval.d.ts +22 -3
  111. package/dist/types/tools/glob.d.ts +4 -0
  112. package/dist/types/tools/native-view.d.ts +47 -0
  113. package/dist/types/tools/output-meta.d.ts +11 -1
  114. package/dist/types/tools/renderer.d.ts +37 -2
  115. package/dist/types/tools/streaming-output.d.ts +20 -1
  116. package/dist/types/tools/task.d.ts +0 -4
  117. package/dist/types/tools/write.d.ts +9 -2
  118. package/dist/types/tools/yield.d.ts +22 -0
  119. package/dist/types/tui.d.ts +31 -0
  120. package/dist/types/utils.d.ts +6 -0
  121. package/dist/types/vim.d.ts +19 -5
  122. package/package.json +13 -9
  123. package/src/app-keybindings.d.ts +1 -1
  124. package/src/app-keybindings.ts +1 -1
  125. package/src/apps/autoresearch-dashboard.ts +7 -3
  126. package/src/apps/cleanse-board.ts +2 -2
  127. package/src/apps/cleanse-picker.ts +1 -1
  128. package/src/apps/debug/log-viewer.ts +188 -72
  129. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  130. package/src/apps/debug/raw-sse.ts +39 -5
  131. package/src/apps/git/diff-pane.ts +26 -5
  132. package/src/apps/git/git-tui.ts +2 -2
  133. package/src/apps/git/help.ts +29 -20
  134. package/src/apps/if-bench-board.ts +2 -2
  135. package/src/apps/ps-top.ts +2 -2
  136. package/src/apps/setup-model-picker.ts +1 -1
  137. package/src/autocomplete.d.ts +5 -2
  138. package/src/autocomplete.ts +67 -43
  139. package/src/charts/chart-plan.d.ts +674 -0
  140. package/src/charts/chart-plan.ts +5081 -0
  141. package/src/charts/chart-svg.d.ts +59 -0
  142. package/src/charts/chart-svg.ts +4657 -0
  143. package/src/charts/table-data.d.ts +175 -0
  144. package/src/charts/table-data.ts +1478 -0
  145. package/src/chat/advisor-message.ts +11 -0
  146. package/src/chat/assistant-message.d.ts +5 -0
  147. package/src/chat/assistant-message.ts +219 -50
  148. package/src/chat/chat-transcript-builder.d.ts +2 -0
  149. package/src/chat/chat-transcript-builder.ts +5 -3
  150. package/src/chat/compaction-summary-message.d.ts +1 -0
  151. package/src/chat/compaction-summary-message.ts +8 -0
  152. package/src/chat/execution-shared.d.ts +6 -5
  153. package/src/chat/execution-shared.ts +42 -25
  154. package/src/chat/fence-figure.d.ts +42 -0
  155. package/src/chat/fence-figure.ts +80 -0
  156. package/src/chat/figure-markdown.d.ts +42 -0
  157. package/src/chat/figure-markdown.ts +174 -0
  158. package/src/chat/messages.d.ts +1 -1
  159. package/src/chat/messages.ts +1 -1
  160. package/src/chat/read-tool-group.ts +6 -4
  161. package/src/chat/skill-message.ts +6 -0
  162. package/src/chat/svg-figure.d.ts +33 -0
  163. package/src/chat/svg-figure.ts +399 -0
  164. package/src/chat/svg-source.d.ts +49 -0
  165. package/src/chat/svg-source.ts +188 -0
  166. package/src/chat/table-chart.d.ts +89 -0
  167. package/src/chat/table-chart.ts +294 -0
  168. package/src/chat/tool-execution.d.ts +2 -0
  169. package/src/chat/tool-execution.ts +80 -6
  170. package/src/chat/user-message.d.ts +15 -2
  171. package/src/chat/user-message.ts +55 -3
  172. package/src/chrome/shared.ts +53 -0
  173. package/src/chrome/transcript-container.d.ts +6 -0
  174. package/src/chrome/transcript-container.ts +93 -13
  175. package/src/components/box.d.ts +1 -0
  176. package/src/components/box.ts +5 -0
  177. package/src/components/disclosure.d.ts +1 -0
  178. package/src/components/disclosure.ts +8 -0
  179. package/src/components/editor.d.ts +15 -4
  180. package/src/components/editor.ts +294 -46
  181. package/src/components/form.d.ts +1 -0
  182. package/src/components/form.ts +8 -0
  183. package/src/components/image.d.ts +12 -44
  184. package/src/components/image.ts +47 -151
  185. package/src/components/key-value-list.d.ts +1 -0
  186. package/src/components/key-value-list.ts +4 -0
  187. package/src/components/layout/row.d.ts +1 -0
  188. package/src/components/layout/row.ts +7 -0
  189. package/src/components/layout/stack.d.ts +1 -0
  190. package/src/components/layout/stack.ts +7 -0
  191. package/src/components/markdown.d.ts +36 -3
  192. package/src/components/markdown.ts +433 -318
  193. package/src/components/scroll-view.d.ts +1 -0
  194. package/src/components/scroll-view.ts +5 -0
  195. package/src/components/section.d.ts +1 -0
  196. package/src/components/section.ts +5 -0
  197. package/src/components/settings-list.ts +2 -0
  198. package/src/components/table.d.ts +1 -0
  199. package/src/components/table.ts +4 -0
  200. package/src/components/text.d.ts +1 -0
  201. package/src/components/text.ts +7 -0
  202. package/src/components/tree-view.d.ts +7 -0
  203. package/src/components/tree-view.ts +38 -8
  204. package/src/components/truncated-text.d.ts +1 -0
  205. package/src/components/truncated-text.ts +5 -0
  206. package/src/desktop-notify.ts +1 -1
  207. package/src/fuzzy.d.ts +29 -1
  208. package/src/fuzzy.ts +115 -30
  209. package/src/key-hint-format.d.ts +1 -1
  210. package/src/key-hint-format.ts +1 -1
  211. package/src/keybinding-matchers.d.ts +3 -0
  212. package/src/keybinding-matchers.ts +6 -2
  213. package/src/keybindings.d.ts +1 -1
  214. package/src/keybindings.ts +1 -1
  215. package/src/kitty-graphics.d.ts +2 -2
  216. package/src/kitty-graphics.ts +4 -4
  217. package/src/loop-watchdog.d.ts +8 -7
  218. package/src/loop-watchdog.ts +18 -13
  219. package/src/multiplexers/cmux.d.ts +16 -0
  220. package/src/multiplexers/cmux.ts +29 -0
  221. package/src/multiplexers/herdr.d.ts +26 -0
  222. package/src/multiplexers/herdr.ts +64 -0
  223. package/src/multiplexers/notify.d.ts +13 -0
  224. package/src/multiplexers/notify.ts +24 -0
  225. package/src/multiplexers/orca.d.ts +12 -0
  226. package/src/multiplexers/orca.ts +16 -0
  227. package/src/multiplexers/screen.d.ts +9 -0
  228. package/src/multiplexers/screen.ts +15 -0
  229. package/src/multiplexers/tmux.d.ts +29 -0
  230. package/src/multiplexers/tmux.ts +40 -0
  231. package/src/multiplexers/types.d.ts +100 -0
  232. package/src/multiplexers/types.ts +110 -0
  233. package/src/multiplexers/wmux.d.ts +12 -0
  234. package/src/multiplexers/wmux.ts +16 -0
  235. package/src/multiplexers/zellij.d.ts +12 -0
  236. package/src/multiplexers/zellij.ts +24 -0
  237. package/src/native/backend.d.ts +3 -2
  238. package/src/native/backend.ts +174 -18
  239. package/src/native/blobs.d.ts +7 -3
  240. package/src/native/blobs.ts +47 -12
  241. package/src/native/encode.d.ts +3 -4
  242. package/src/native/encode.ts +58 -31
  243. package/src/native/overlay.d.ts +5 -0
  244. package/src/native/overlay.ts +13 -0
  245. package/src/native/reconcile.ts +4 -0
  246. package/src/overlays/agent-hub-projection.d.ts +6 -5
  247. package/src/overlays/agent-hub-projection.ts +124 -13
  248. package/src/overlays/agent-hub-types.d.ts +9 -0
  249. package/src/overlays/agent-hub-types.ts +6 -0
  250. package/src/overlays/agent-hub.ts +44 -29
  251. package/src/overlays/agent-transcript-viewer.ts +91 -23
  252. package/src/overlays/agents-hub.ts +28 -8
  253. package/src/overlays/annotation-overlay.ts +293 -90
  254. package/src/overlays/annotation-types.ts +2 -0
  255. package/src/overlays/ask-dialog.ts +7 -10
  256. package/src/overlays/btw-history-panel.ts +12 -1
  257. package/src/overlays/copy-selector.ts +30 -8
  258. package/src/overlays/extensions/state-manager.ts +52 -12
  259. package/src/overlays/history-search.ts +100 -7
  260. package/src/overlays/hook-selector.ts +12 -5
  261. package/src/overlays/jobs-panel.ts +28 -46
  262. package/src/overlays/model-browser.ts +341 -110
  263. package/src/overlays/model-hub.ts +300 -49
  264. package/src/overlays/move-overlay.ts +28 -14
  265. package/src/overlays/oauth-selector.ts +54 -70
  266. package/src/overlays/plan-review-overlay.ts +194 -70
  267. package/src/overlays/plugin-settings.ts +3 -3
  268. package/src/overlays/report-panel.ts +2 -2
  269. package/src/overlays/rewind-selector.ts +86 -17
  270. package/src/overlays/session-observer-registry.d.ts +2 -0
  271. package/src/overlays/session-observer-registry.ts +10 -2
  272. package/src/overlays/session-selector.ts +64 -26
  273. package/src/overlays/settings-defs.ts +4 -3
  274. package/src/overlays/settings-selector.ts +33 -8
  275. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  276. package/src/overlays/stats-notice.ts +1 -1
  277. package/src/overlays/tree-selector.ts +41 -20
  278. package/src/overlays/usage-dashboard.ts +100 -50
  279. package/src/overlays/usage-display.ts +10 -4
  280. package/src/prompt/attachment-chips.ts +15 -11
  281. package/src/prompt/composer-attachments.ts +6 -4
  282. package/src/prompt/composer.ts +4 -15
  283. package/src/prompt/custom-editor.ts +54 -26
  284. package/src/prompt/image-source.ts +2 -1
  285. package/src/prompt/interactive-context-helpers.ts +30 -18
  286. package/src/prompt/macos-spelling.ts +7 -0
  287. package/src/prompt/magic-keywords.ts +11 -2
  288. package/src/prompt/model-mention-autocomplete.ts +14 -14
  289. package/src/prompt/tips.txt +6 -5
  290. package/src/prompt/welcome.ts +55 -12
  291. package/src/prompt/word-completion.ts +38 -5
  292. package/src/render/composite.d.ts +2 -0
  293. package/src/render/composite.ts +64 -0
  294. package/src/render/output-pane.d.ts +1 -0
  295. package/src/render/output-pane.ts +6 -0
  296. package/src/render/render-utils.d.ts +1 -0
  297. package/src/render/render-utils.ts +11 -0
  298. package/src/render/tool-card.d.ts +6 -0
  299. package/src/render/tool-card.ts +15 -0
  300. package/src/render/width-aware-text.d.ts +2 -0
  301. package/src/render/width-aware-text.ts +5 -0
  302. package/src/setup/scenes/model.ts +2 -1
  303. package/src/setup/scenes/types.ts +1 -0
  304. package/src/status-line/component.ts +292 -78
  305. package/src/status-line/host.ts +4 -1
  306. package/src/status-line/segments.ts +40 -22
  307. package/src/status-line/types.ts +15 -3
  308. package/src/terminal-capabilities.d.ts +27 -35
  309. package/src/terminal-capabilities.ts +75 -189
  310. package/src/terminal-multiplexer.d.ts +127 -14
  311. package/src/terminal-multiplexer.ts +112 -33
  312. package/src/terminal.d.ts +22 -6
  313. package/src/terminal.ts +139 -37
  314. package/src/theme/active-symbols.d.ts +1 -1
  315. package/src/theme/active-symbols.ts +1 -1
  316. package/src/theme/schema-validation.ts +1 -0
  317. package/src/theme/schema.d.ts +11 -2
  318. package/src/theme/schema.ts +14 -1
  319. package/src/theme/session-color.ts +1 -7
  320. package/src/theme/symbols.d.ts +1 -1
  321. package/src/theme/symbols.ts +6 -1
  322. package/src/theme/theme-class.d.ts +7 -0
  323. package/src/theme/theme-class.ts +26 -1
  324. package/src/theme/theme-schema.json +5 -1
  325. package/src/theme/theme.d.ts +1 -1
  326. package/src/theme/theme.ts +8 -4
  327. package/src/theme/tui-adapters.d.ts +3 -0
  328. package/src/theme/tui-adapters.ts +68 -0
  329. package/src/tmux.d.ts +11 -4
  330. package/src/tmux.ts +47 -11
  331. package/src/tools/ast-edit.ts +59 -35
  332. package/src/tools/ast-grep.ts +35 -16
  333. package/src/tools/bash.ts +92 -42
  334. package/src/tools/daemon.d.ts +1 -1
  335. package/src/tools/daemon.ts +1 -1
  336. package/src/tools/default-renderer.ts +35 -19
  337. package/src/tools/edit.ts +142 -120
  338. package/src/tools/eval-format/index.ts +28 -2
  339. package/src/tools/eval-format/javascript.ts +7 -5
  340. package/src/tools/eval.d.ts +22 -3
  341. package/src/tools/eval.ts +364 -208
  342. package/src/tools/fetch.ts +5 -6
  343. package/src/tools/glob.d.ts +4 -0
  344. package/src/tools/glob.ts +33 -65
  345. package/src/tools/grep.ts +81 -48
  346. package/src/tools/index.ts +2 -0
  347. package/src/tools/lsp.ts +81 -59
  348. package/src/tools/mcp.ts +111 -68
  349. package/src/tools/native-view.d.ts +47 -0
  350. package/src/tools/native-view.ts +102 -1
  351. package/src/tools/output-meta.d.ts +11 -1
  352. package/src/tools/output-meta.ts +23 -3
  353. package/src/tools/read.ts +7 -5
  354. package/src/tools/renderer.d.ts +37 -2
  355. package/src/tools/renderer.ts +42 -2
  356. package/src/tools/streaming-output.d.ts +20 -1
  357. package/src/tools/streaming-output.ts +149 -50
  358. package/src/tools/task.d.ts +0 -4
  359. package/src/tools/task.ts +112 -59
  360. package/src/tools/todo.ts +119 -30
  361. package/src/tools/wait.ts +34 -24
  362. package/src/tools/web-search.ts +1 -1
  363. package/src/tools/write.d.ts +9 -2
  364. package/src/tools/write.ts +147 -95
  365. package/src/tools/yield.d.ts +22 -0
  366. package/src/tools/yield.ts +225 -0
  367. package/src/tui.d.ts +31 -0
  368. package/src/tui.ts +309 -160
  369. package/src/utils.d.ts +6 -0
  370. package/src/utils.ts +19 -0
  371. package/src/vim.d.ts +19 -5
  372. package/src/vim.ts +333 -18
  373. package/src/windows-input-mode.ts +8 -0
package/src/tools/eval.ts CHANGED
@@ -9,14 +9,19 @@ import type {
9
9
  ToolRenderer,
10
10
  } from "./renderer";
11
11
  import type { TspSpan } from "@linxiraos/pi-wire";
12
- import { ansi, code as codeNode, compact, keyed, md, node, span, text } from "../native/describe";
12
+ import { compact, keyed, md, node, span, text } from "../native/describe";
13
13
  import type { NativeChild, NativeNode } from "../native/node";
14
14
  import { plainText } from "../native/spans";
15
- import { footnoteText, resultText } from "./native-view";
15
+ import { resultText, runBox, runFoot, type RunFootInput, runInput, runOutput, type RunState } from "./native-view";
16
16
  import { renderAgentTreeRow } from "./agent-tree";
17
17
  import { truncateToVisualLines } from "../chrome/visual-truncate";
18
18
  import { getMarkdownTheme, type Theme } from "../theme/theme";
19
- import { markFramedBlockComponent, outputBlockContentWidth, renderCodeCell } from "../render/index";
19
+ import {
20
+ type CodeCellOptions,
21
+ markFramedBlockComponent,
22
+ outputBlockContentWidth,
23
+ renderCodeCell,
24
+ } from "../render/index";
20
25
  import { formatOutputPaneLines } from "../render/output-pane";
21
26
  import { formatEvalCodeForDisplay } from "./eval-format/index";
22
27
  import {
@@ -66,6 +71,8 @@ export interface EvalCellResult {
66
71
  durationMs?: number;
67
72
  exitCode?: number;
68
73
  statusEvents?: EvalStatusEvent[];
74
+ /** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
75
+ statusEventsElided?: number;
69
76
  hasMarkdown?: boolean;
70
77
  }
71
78
 
@@ -75,6 +82,8 @@ export interface EvalToolDetails {
75
82
  jsonOutputs?: unknown[];
76
83
  images?: ImageContent[];
77
84
  statusEvents?: EvalStatusEvent[];
85
+ /** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
86
+ statusEventsElided?: number;
78
87
  isError?: boolean;
79
88
  meta?: OutputMeta;
80
89
  /** First backend that produced cells. Kept for transcript compatibility. */
@@ -144,6 +153,37 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
144
153
  return out;
145
154
  }
146
155
 
156
+ /**
157
+ * A rendered {@link renderCodeCell} box. Its first row is the header bar, the
158
+ * only row that depends on `spinnerFrame`; the code and output rows below it
159
+ * are reused across spinner ticks.
160
+ */
161
+ interface RenderedCodeCell {
162
+ readonly options: CodeCellOptions;
163
+ readonly lines: readonly string[];
164
+ }
165
+
166
+ /**
167
+ * Append `cell` for `spinnerFrame`. Only a running header animates, so only it
168
+ * is redrawn for a new frame: `renderOutputBlock` lays the header bar out from
169
+ * the header fields and width alone, so rendering an empty cell with the same
170
+ * header yields the identical bar row.
171
+ */
172
+ function pushCodeCellLines(
173
+ out: string[],
174
+ cell: RenderedCodeCell,
175
+ spinnerFrame: number | undefined,
176
+ theme: Theme,
177
+ ): void {
178
+ const { options, lines } = cell;
179
+ const header =
180
+ options.status === "running" && options.spinnerFrame !== spinnerFrame
181
+ ? renderCodeCell({ ...options, code: "", output: undefined, spinnerFrame }, theme)[0]!
182
+ : lines[0]!;
183
+ out.push(header);
184
+ for (let i = 1; i < lines.length; i++) out.push(lines[i]!);
185
+ }
186
+
147
187
  type AgentEventStatus = "pending" | "running" | "completed" | "failed" | "aborted";
148
188
 
149
189
  /**
@@ -159,11 +199,20 @@ export function statusEventKey(event: { op: string; [key: string]: unknown }): s
159
199
  return undefined;
160
200
  }
161
201
 
202
+ /**
203
+ * Discrete status events a log keeps. A cell polling a helper in a loop emits
204
+ * one event per call (hundreds of thousands), and every live update and the
205
+ * persisted tool result would otherwise carry them all.
206
+ */
207
+ export const MAX_STATUS_EVENTS = 200;
208
+
209
+ /** A status event list plus the count of discrete events dropped from its front. */
210
+ export type StatusEventLog = Pick<EvalCellResult, "statusEvents" | "statusEventsElided">;
211
+
162
212
  /**
163
213
  * Append or replace a status event. Progress snapshots (see
164
214
  * {@link statusEventKey}) coalesce in place, preserving first-seen order; every
165
- * other op is a discrete action and simply appends. Keeps the persisted event
166
- * list bounded even when a subagent or batch emits hundreds of progress ticks.
215
+ * other op is a discrete action and simply appends.
167
216
  */
168
217
  export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void {
169
218
  const key = statusEventKey(event);
@@ -177,6 +226,24 @@ export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEv
177
226
  events.push(event);
178
227
  }
179
228
 
229
+ /**
230
+ * {@link upsertStatusEvent} into a bounded log. Past {@link MAX_STATUS_EVENTS},
231
+ * the oldest discrete event is dropped and counted. Snapshots stay, since agent
232
+ * cards read them, and so does the newest committed `todo` result, which the
233
+ * session todo panel refreshes from. Renderers already show only the newest
234
+ * events behind an "… N earlier" row.
235
+ */
236
+ export function recordStatusEvent(log: StatusEventLog, event: EvalStatusEvent): void {
237
+ const events = (log.statusEvents ??= []);
238
+ upsertStatusEvent(events, event);
239
+ if (events.length <= MAX_STATUS_EVENTS) return;
240
+ const keep = events.findLastIndex(e => e.op === "todo" && e.committed === true);
241
+ const oldest = events.findIndex((e, i) => i !== keep && statusEventKey(e) === undefined);
242
+ if (oldest < 0) return;
243
+ events.splice(oldest, 1);
244
+ log.statusEventsElided = (log.statusEventsElided ?? 0) + 1;
245
+ }
246
+
180
247
  function eventString(value: unknown): string | undefined {
181
248
  return typeof value === "string" && value.length > 0 ? value : undefined;
182
249
  }
@@ -475,17 +542,20 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
475
542
  * the live edge for `log()` progress loops) behind an "… N earlier" marker,
476
543
  * matching the code/output tail-window convention. Collapsed keeps a small
477
544
  * fixed window; expanded widens to the viewport-sized preview window.
545
+ * `elided` counts events already dropped from the log's front.
478
546
  */
479
- function renderStatusEvents(events: EvalStatusEvent[], theme: Theme, expanded: boolean): string[] {
480
- if (events.length === 0) return [];
547
+ function renderStatusEvents(events: EvalStatusEvent[], theme: Theme, expanded: boolean, elided = 0): string[] {
548
+ if (events.length === 0 && elided === 0) return [];
481
549
 
482
550
  const max = expanded ? Math.max(10, previewWindowRows()) : 3;
483
- const hidden = Math.max(0, events.length - max);
484
- const visible = hidden > 0 ? events.slice(hidden) : events;
551
+ const shownFrom = Math.max(0, events.length - max);
552
+ const hidden = shownFrom + elided;
553
+ const visible = shownFrom > 0 ? events.slice(shownFrom) : events;
485
554
 
486
555
  const lines: string[] = [];
487
556
  if (hidden > 0) {
488
- lines.push(`${theme.fg("dim", theme.tree.branch)} ${theme.fg("dim", `… ${hidden} earlier`)}`);
557
+ const glyph = visible.length > 0 ? theme.tree.branch : theme.tree.last;
558
+ lines.push(`${theme.fg("dim", glyph)} ${theme.fg("dim", `… ${hidden} earlier`)}`);
489
559
  }
490
560
  for (let i = 0; i < visible.length; i++) {
491
561
  const isLast = i === visible.length - 1;
@@ -658,89 +728,112 @@ function splitConsoleTables(output: string): EvalOutputPart[] {
658
728
  return parts;
659
729
  }
660
730
 
661
- /** One cell's output: markdown, else terminal text with any `console.table` blocks as tables. */
662
- function evalOutputNodes(output: string, markdown: boolean, running: boolean, error: boolean): NativeNode[] {
731
+ /**
732
+ * One cell's outputs: markdown, else terminal text (following its tail,
733
+ * clamped to the last `previewLines` while folded) with any `console.table`
734
+ * blocks as tables.
735
+ */
736
+ function evalOutputNodes(output: string, markdown: boolean, previewLines: number): NativeNode[] {
663
737
  if (output.trim().length === 0) return [];
664
- if (markdown && !error) return [md(output)];
665
- const tone = error ? "error" : undefined;
666
- return splitConsoleTables(output).map(part =>
738
+ if (markdown) return [keyed(md(output), "output")];
739
+ return splitConsoleTables(output).map((part, i) =>
667
740
  part.kind === "text"
668
- ? ansi(part.text, { follow: running, tone, role: "zeta.tool.eval.output" })
669
- : node("table", {
670
- // Node's index column header is noise in a real table.
671
- cols: part.head.map((head, i) => ({ id: `c${i}`, head: head === "(index)" ? "" : head })),
672
- rows: part.rows.map((cells, r) => ({
673
- id: `r${r}`,
674
- cells: Object.fromEntries(cells.map((cell, i) => [`c${i}`, cell])),
675
- })),
676
- role: "zeta.tool.eval.table",
677
- }),
741
+ ? runOutput(part.text, {
742
+ role: "zeta.tool.eval.output",
743
+ previewLines,
744
+ key: i === 0 ? "output" : `output-${i}`,
745
+ })
746
+ : node(
747
+ "table",
748
+ {
749
+ // Node's index column header is noise in a real table.
750
+ cols: part.head.map((head, c) => ({ id: `c${c}`, head: head === "(index)" ? "" : head })),
751
+ rows: part.rows.map((cells, r) => ({
752
+ id: `r${r}`,
753
+ cells: Object.fromEntries(cells.map((cell, c) => [`c${c}`, cell])),
754
+ })),
755
+ role: "zeta.tool.eval.table",
756
+ },
757
+ undefined,
758
+ `table-${i}`,
759
+ ),
678
760
  );
679
761
  }
680
762
 
681
- /** Inputs of one eval cell section. */
682
- interface EvalCellSection {
763
+ /** Status lines a folded cell keeps: the newest ones. */
764
+ const RUN_STATUS_TAIL = 8;
765
+
766
+ /**
767
+ * A status log's events (`zeta.run.status`): all kept ones when expanded, else
768
+ * the newest {@link RUN_STATUS_TAIL}, after a muted `N earlier` line that also
769
+ * counts events already dropped from the log's front.
770
+ */
771
+ function evalStatusSection(log: StatusEventLog, expanded: boolean): NativeNode | undefined {
772
+ const events = log.statusEvents ?? [];
773
+ const elided = log.statusEventsElided ?? 0;
774
+ if (events.length === 0 && elided === 0) return undefined;
775
+ const shownFrom = expanded ? 0 : Math.max(0, events.length - RUN_STATUS_TAIL);
776
+ const hidden = shownFrom + elided;
777
+ const lines = events.slice(shownFrom).map((event, i) => keyed(describeStatusEvent(event), `s${hidden + i}`));
778
+ return node(
779
+ "col",
780
+ { role: "zeta.run.status", gap: "none" },
781
+ compact<NativeNode>([hidden > 0 && keyed(text([span(`${hidden} earlier`, "muted")]), "earlier"), ...lines]),
782
+ "status",
783
+ );
784
+ }
785
+
786
+ /** The foot state of a cell, or undefined while it has not started. */
787
+ function evalCellState(status: EvalCellResult["status"], cancelled: boolean): RunState | undefined {
788
+ switch (status) {
789
+ case "pending":
790
+ return undefined;
791
+ case "running":
792
+ return cancelled ? "cancelled" : "running";
793
+ case "complete":
794
+ return "done";
795
+ case "error":
796
+ return "failed";
797
+ }
798
+ }
799
+
800
+ /** Inputs of one eval cell box. */
801
+ interface EvalCellRun {
683
802
  readonly language: EvalLanguage;
684
803
  readonly code: string;
685
804
  readonly title?: string;
686
- readonly status?: EvalCellResult["status"];
687
- readonly durationMs?: number;
688
- readonly output?: readonly NativeChild[];
805
+ readonly outputs?: readonly NativeChild[];
806
+ readonly status?: NativeNode;
807
+ /** Omitted while the cell has not started. */
808
+ readonly foot?: RunFootInput;
689
809
  }
690
810
 
691
811
  /**
692
- * A notebook cell: a gutter mark beside the input (←, muted until the cell
693
- * runs, zeta's thinking starburst while it does) and beside its output (→).
694
- * Only multi-cell calls repeat titles; marks never invent execution counts.
812
+ * A notebook cell as one run box: the caption (multi-cell calls only), the
813
+ * source, its outputs, status lines and foot. A failed cell tints its box.
695
814
  */
696
- function evalCellSection(cell: EvalCellSection, index: number, total: number): NativeNode {
697
- let head: TspSpan[] | undefined;
815
+ function evalCellBox(cell: EvalCellRun, index: number, total: number): NativeNode {
816
+ let caption: NativeNode | undefined;
698
817
  if (total > 1) {
699
- head = [span(`${index + 1}/${total}`, "muted")];
818
+ const spans: TspSpan[] = [span(`${index + 1}/${total}`, "muted")];
700
819
  const title = plainText(cell.title ?? "").trim();
701
- if (title) head.push(span(` ${title}`, "toolTitle"));
702
- if (cell.status === "error") head.push(span(" · failed", "error"));
703
- if (cell.durationMs !== undefined) head.push(span(` · ${(cell.durationMs / 1000).toFixed(2)}s`, "muted"));
820
+ if (title) spans.push(span(` ${title}`, "toolTitle"));
821
+ caption = keyed(text(spans, { role: "zeta.run.caption" }), "caption");
704
822
  }
705
- const inputMark =
706
- cell.status === "running"
707
- ? node("spinner", { style: "starburst", role: "zeta.tool.eval.prompt", aria: "Running" })
708
- : node("icon", {
709
- name: "arrow-left",
710
- role: "zeta.tool.eval.prompt",
711
- aria: "Input",
712
- tone: !cell.status || cell.status === "pending" ? "muted" : undefined,
713
- });
714
- return node(
715
- "col",
716
- {
717
- role: "zeta.tool.eval.cell",
718
- tone: cell.status === "error" ? "error" : cell.status === "running" ? "pending" : undefined,
719
- },
720
- compact<NativeChild>([
721
- head ? text(head, { role: "zeta.tool.eval.caption" }) : undefined,
722
- node(
723
- "row",
724
- { role: "zeta.tool.eval.input", align: "start" },
725
- [
726
- inputMark,
727
- keyed(codeNode(cell.code, { lang: languageForHighlighter(cell.language), numbers: false }), "code"),
728
- ],
729
- "input",
730
- ),
731
- cell.output?.length
732
- ? node(
733
- "row",
734
- { role: "zeta.tool.eval.result", align: "start" },
735
- [
736
- node("icon", { name: "arrow-right", role: "zeta.tool.eval.prompt", aria: "Output" }),
737
- node("col", { role: "zeta.tool.eval.outputs" }, cell.output),
738
- ],
739
- "output",
740
- )
741
- : undefined,
742
- ]),
743
- `cell-${index}`,
823
+ return runBox(
824
+ [
825
+ caption,
826
+ cell.code.trim().length > 0 &&
827
+ runInput(cell.code, {
828
+ role: "zeta.tool.eval.input",
829
+ lang: languageForHighlighter(cell.language),
830
+ wrap: false,
831
+ }),
832
+ ...(cell.outputs ?? []),
833
+ cell.status,
834
+ cell.foot && runFoot(cell.foot),
835
+ ],
836
+ { key: `cell-${index}`, tone: cell.foot?.state === "failed" ? "error" : undefined },
744
837
  );
745
838
  }
746
839
 
@@ -757,47 +850,59 @@ export const evalToolRenderer = {
757
850
  return new Text(text, 0, 0);
758
851
  }
759
852
 
760
- let cached: { key: string; width: number; result: string[] } | undefined;
853
+ // Cell bodies depend on width, expansion, the viewport window and whether
854
+ // the call is live; a spinner tick only redraws the running header rows.
855
+ let bodies: { key: string; cells: RenderedCodeCell[] } | undefined;
856
+ let cached: { key: string; result: string[] } | undefined;
761
857
 
762
858
  return markFramedBlockComponent({
763
859
  render: (width: number): readonly string[] => {
764
- const key = `${options.expanded ? 1 : 0}|${options.spinnerFrame ?? "-"}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
765
- if (cached && cached.key === key && cached.width === width) {
860
+ const spinnerFrame = options.spinnerFrame;
861
+ const windowRows = previewWindowRows();
862
+ const bodyKey = `${width}|${options.expanded ? 1 : 0}|${windowRows}|${spinnerFrame === undefined ? 0 : 1}`;
863
+ const key = `${bodyKey}|${spinnerFrame ?? "-"}`;
864
+ if (cached?.key === key) {
766
865
  return cached.result;
767
866
  }
768
867
 
868
+ if (bodies?.key !== bodyKey) {
869
+ bodies = {
870
+ key: bodyKey,
871
+ cells: cells.map((cell, i) => {
872
+ const cellOptions: CodeCellOptions = {
873
+ code: cell.code,
874
+ language: languageForHighlighter(cell.language),
875
+ showLanguage: true,
876
+ index: i,
877
+ total: cells.length,
878
+ title: cell.title,
879
+ status: spinnerFrame !== undefined ? "running" : "pending",
880
+ spinnerFrame,
881
+ width,
882
+ // Viewport-sized tail window following the newest streamed code
883
+ // line; renderResult keeps the same cap so the cell never snaps
884
+ // open on completion. Only ctrl+o uncaps.
885
+ codeTail: true,
886
+ codeMaxLines: windowRows,
887
+ expanded: options.expanded,
888
+ };
889
+ return { options: cellOptions, lines: renderCodeCell(cellOptions, uiTheme) };
890
+ }),
891
+ };
892
+ }
893
+
769
894
  const lines: string[] = [];
770
- for (let i = 0; i < cells.length; i++) {
771
- const cell = cells[i];
772
- const cellLines = renderCodeCell(
773
- {
774
- code: cell.code,
775
- language: languageForHighlighter(cell.language),
776
- showLanguage: true,
777
- index: i,
778
- total: cells.length,
779
- title: cell.title,
780
- status: options.spinnerFrame !== undefined ? "running" : "pending",
781
- spinnerFrame: options.spinnerFrame,
782
- width,
783
- // Viewport-sized tail window following the newest streamed code
784
- // line; renderResult keeps the same cap so the cell never snaps
785
- // open on completion. Only ctrl+o uncaps.
786
- codeTail: true,
787
- codeMaxLines: previewWindowRows(),
788
- expanded: options.expanded,
789
- },
790
- uiTheme,
791
- );
792
- lines.push(...cellLines);
895
+ for (let i = 0; i < bodies.cells.length; i++) {
896
+ pushCodeCellLines(lines, bodies.cells[i]!, spinnerFrame, uiTheme);
793
897
  if (i < cells.length - 1) {
794
898
  lines.push("");
795
899
  }
796
900
  }
797
- cached = { key, width, result: lines };
901
+ cached = { key, result: lines };
798
902
  return lines;
799
903
  },
800
904
  invalidate: () => {
905
+ bodies = undefined;
801
906
  cached = undefined;
802
907
  },
803
908
  });
@@ -848,69 +953,106 @@ export const evalToolRenderer = {
848
953
  if (cellResults && cellResults.length > 0) {
849
954
  const displayCells = cellResults.map(cell => {
850
955
  const language = cell.language ?? details?.language ?? "python";
851
- return { cell, code: formatEvalCodeForDisplay(cell.code, language), language };
956
+ const allEvents = cell.statusEvents ?? [];
957
+ const agentEvents = allEvents.filter(e => e.op === "agent");
958
+ return {
959
+ cell,
960
+ code: formatEvalCodeForDisplay(cell.code, language),
961
+ language,
962
+ agentEvents,
963
+ otherEvents: agentEvents.length > 0 ? allEvents.filter(e => e.op !== "agent") : allEvents,
964
+ // Running agent rows carry the spinner; settled ones are frame-independent.
965
+ agentLive: agentEvents.some(e => agentEventStatus(e.status) === "running"),
966
+ };
852
967
  });
853
- let cached: { key: string; width: number; result: string[] } | undefined;
968
+ // Per-cell rows keyed by everything but the spinner frame: a tick only
969
+ // redraws running headers and running agent rows.
970
+ let bodies:
971
+ | {
972
+ key: string;
973
+ cells: { codeCell: RenderedCodeCell; agentLines: string[]; agentFrame: number | undefined }[];
974
+ }
975
+ | undefined;
976
+ let cached: { key: string; result: string[] } | undefined;
854
977
 
855
978
  return markFramedBlockComponent({
856
979
  render: (width: number): readonly string[] => {
980
+ const spinnerFrame = options.spinnerFrame;
857
981
  const expanded = options.renderContext?.expanded ?? options.expanded;
982
+ const windowRows = previewWindowRows();
858
983
  const previewLines = Math.min(
859
984
  options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES,
860
- previewWindowRows(),
985
+ windowRows,
861
986
  );
862
- const key = `${expanded}|${previewLines}|${options.spinnerFrame}|${previewWindowRows()}`;
863
- if (cached && cached.key === key && cached.width === width) {
987
+ const bodyKey = `${width}|${expanded}|${previewLines}|${windowRows}`;
988
+ const key = `${bodyKey}|${spinnerFrame}`;
989
+ if (cached?.key === key) {
864
990
  return cached.result;
865
991
  }
866
992
 
993
+ if (bodies?.key !== bodyKey) {
994
+ bodies = {
995
+ key: bodyKey,
996
+ cells: displayCells.map(({ cell, code, language, agentEvents, otherEvents }, i) => {
997
+ const statusLines = renderStatusEvents(otherEvents, uiTheme, expanded, cell.statusEventsElided);
998
+ const outputContent = formatCellOutputLines(cell, expanded, previewLines, uiTheme, width);
999
+ const outputLines = [...outputContent.lines];
1000
+ if (!expanded && outputContent.hiddenCount > 0) {
1001
+ outputLines.push(
1002
+ uiTheme.fg(
1003
+ "dim",
1004
+ `… ${outputContent.hiddenCount} more lines (${expandKeyHint()} to expand)`,
1005
+ ),
1006
+ );
1007
+ }
1008
+ if (statusLines.length > 0) {
1009
+ if (outputLines.length > 0) {
1010
+ outputLines.push(uiTheme.fg("dim", "Status"));
1011
+ }
1012
+ outputLines.push(...statusLines);
1013
+ }
1014
+ const cellOptions: CodeCellOptions = {
1015
+ code,
1016
+ language: languageForHighlighter(language),
1017
+ showLanguage: true,
1018
+ index: i,
1019
+ total: cellResults.length,
1020
+ title: cell.title,
1021
+ status: cell.status,
1022
+ spinnerFrame,
1023
+ duration: cell.durationMs,
1024
+ output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
1025
+ outputMaxLines: outputLines.length,
1026
+ // Same viewport-sized tail window as the pending preview so the
1027
+ // cell never snaps open on completion; only ctrl+o uncaps.
1028
+ // `output` keeps its own preview cap from above.
1029
+ codeTail: true,
1030
+ codeMaxLines: windowRows,
1031
+ expanded,
1032
+ width,
1033
+ };
1034
+ return {
1035
+ codeCell: { options: cellOptions, lines: renderCodeCell(cellOptions, uiTheme) },
1036
+ agentLines:
1037
+ agentEvents.length > 0
1038
+ ? renderAgentProgressEvents(agentEvents, uiTheme, width, spinnerFrame)
1039
+ : [],
1040
+ agentFrame: spinnerFrame,
1041
+ };
1042
+ }),
1043
+ };
1044
+ }
1045
+
867
1046
  const lines: string[] = [];
868
1047
  for (let i = 0; i < displayCells.length; i++) {
869
- const { cell, code, language } = displayCells[i];
870
- const allEvents = cell.statusEvents ?? [];
871
- const agentEvents = allEvents.filter(e => e.op === "agent");
872
- const otherEvents = agentEvents.length > 0 ? allEvents.filter(e => e.op !== "agent") : allEvents;
873
- const statusLines = renderStatusEvents(otherEvents, uiTheme, expanded);
874
- const outputContent = formatCellOutputLines(cell, expanded, previewLines, uiTheme, width);
875
- const outputLines = [...outputContent.lines];
876
- if (!expanded && outputContent.hiddenCount > 0) {
877
- outputLines.push(
878
- uiTheme.fg("dim", `… ${outputContent.hiddenCount} more lines (${expandKeyHint()} to expand)`),
879
- );
880
- }
881
- if (statusLines.length > 0) {
882
- if (outputLines.length > 0) {
883
- outputLines.push(uiTheme.fg("dim", "Status"));
884
- }
885
- outputLines.push(...statusLines);
886
- }
887
- const cellLines = renderCodeCell(
888
- {
889
- code,
890
- language: languageForHighlighter(language),
891
- showLanguage: true,
892
- index: i,
893
- total: cellResults.length,
894
- title: cell.title,
895
- status: cell.status,
896
- spinnerFrame: options.spinnerFrame,
897
- duration: cell.durationMs,
898
- output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
899
- outputMaxLines: outputLines.length,
900
- // Same viewport-sized tail window as the pending preview so the
901
- // cell never snaps open on completion; only ctrl+o uncaps.
902
- // `output` keeps its own preview cap from above.
903
- codeTail: true,
904
- codeMaxLines: previewWindowRows(),
905
- expanded,
906
- width,
907
- },
908
- uiTheme,
909
- );
910
- lines.push(...cellLines);
911
- if (agentEvents.length > 0) {
912
- lines.push(...renderAgentProgressEvents(agentEvents, uiTheme, width, options.spinnerFrame));
1048
+ const { agentEvents, agentLive } = displayCells[i]!;
1049
+ const body = bodies.cells[i]!;
1050
+ pushCodeCellLines(lines, body.codeCell, spinnerFrame, uiTheme);
1051
+ if (agentLive && body.agentFrame !== spinnerFrame) {
1052
+ body.agentLines = renderAgentProgressEvents(agentEvents, uiTheme, width, spinnerFrame);
1053
+ body.agentFrame = spinnerFrame;
913
1054
  }
1055
+ lines.push(...body.agentLines);
914
1056
  if (i < cellResults.length - 1) {
915
1057
  lines.push("");
916
1058
  }
@@ -928,10 +1070,11 @@ export const evalToolRenderer = {
928
1070
  if (warningLine) {
929
1071
  lines.push(warningLine);
930
1072
  }
931
- cached = { key, width, result: lines };
1073
+ cached = { key, result: lines };
932
1074
  return lines;
933
1075
  },
934
1076
  invalidate: () => {
1077
+ bodies = undefined;
935
1078
  cached = undefined;
936
1079
  },
937
1080
  });
@@ -945,6 +1088,7 @@ export const evalToolRenderer = {
945
1088
  statusEvents,
946
1089
  uiTheme,
947
1090
  options.renderContext?.expanded ?? options.expanded,
1091
+ details?.statusEventsElided,
948
1092
  );
949
1093
 
950
1094
  if (!combinedOutput && statusLines.length === 0) {
@@ -1043,16 +1187,20 @@ export const evalToolRenderer = {
1043
1187
  };
1044
1188
  },
1045
1189
 
1046
- describeCall(args: EvalRenderArgs, _options: RenderResultOptions): NativeToolView {
1190
+ describeCall(args: EvalRenderArgs, options: RenderResultOptions): NativeToolView {
1047
1191
  const cells = getRenderCells(args);
1192
+ // No foot until the call starts; then the first cell runs (before any result names it).
1193
+ const startFoot: RunFootInput | undefined = options.executionStarted
1194
+ ? { state: options.cancelled ? "cancelled" : "running", elapsedMs: options.elapsedMs }
1195
+ : undefined;
1048
1196
  return {
1049
1197
  tool: evalToolHead(
1050
1198
  cells[0]?.title,
1051
1199
  cells.map(cell => cell.language),
1052
1200
  cells.length,
1053
1201
  ),
1054
- body: cells.map((cell, i) => evalCellSection(cell, i, cells.length)),
1055
- preview: { lines: EVAL_DEFAULT_PREVIEW_LINES },
1202
+ body: cells.map((cell, i) => evalCellBox({ ...cell, foot: i === 0 ? startFoot : undefined }, i, cells.length)),
1203
+ preview: "children",
1056
1204
  };
1057
1205
  },
1058
1206
 
@@ -1063,82 +1211,90 @@ export const evalToolRenderer = {
1063
1211
  ): NativeToolView {
1064
1212
  const details = result.details;
1065
1213
  const isPartial = options.isPartial === true;
1214
+ const cancelled = options.cancelled === true;
1215
+ const expanded = options.renderContext?.expanded ?? options.expanded;
1066
1216
  const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
1067
1217
  const jsonNodes = (details?.jsonOutputs ?? []).map((value, index) =>
1068
1218
  keyed(describeJsonTree(value, { hiddenRootKeys: [] }), `display-${index}`),
1069
1219
  );
1070
- // A notice and truncation go to one quiet final line (a background job is a head badge).
1071
- const footer = footnoteText(compact([details?.notice]), details?.meta);
1220
+ // The call's notice and truncation join the last run's foot facts (a background job is a head badge).
1221
+ const callFacts = { facts: compact([details?.notice]), meta: details?.meta };
1072
1222
  const cellResults = details?.cells;
1073
- let cells: NativeNode[];
1074
- let head: NativeToolHead;
1075
1223
  if (cellResults && cellResults.length > 0) {
1076
1224
  const languages = cellResults.map(cell => cell.language ?? details?.language ?? "python");
1077
- head = evalToolHead(cellResults[0]!.title, languages, cellResults.length, details);
1078
- cells = cellResults.map((cell, i) => {
1225
+ const states = cellResults.map(cell => evalCellState(cell.status, cancelled));
1226
+ // A cell that never started has no foot to carry the call's facts: the last one that ran does.
1227
+ const factsIndex = states.findLastIndex(state => state !== undefined);
1228
+ let earlierMs = 0;
1229
+ const cells = cellResults.map((cell, i) => {
1079
1230
  const language = languages[i]!;
1080
- return evalCellSection(
1231
+ const state = states[i];
1232
+ // A running cell's clock is the call's minus the cells that finished before it.
1233
+ const elapsedMs =
1234
+ state === "running" || state === "cancelled"
1235
+ ? options.elapsedMs === undefined
1236
+ ? undefined
1237
+ : Math.max(0, options.elapsedMs - earlierMs)
1238
+ : cell.durationMs;
1239
+ earlierMs += cell.durationMs ?? 0;
1240
+ return evalCellBox(
1081
1241
  {
1082
1242
  language,
1083
1243
  code: formatEvalCodeForDisplay(cell.code, language),
1084
1244
  title: cell.title,
1085
- status: cell.status,
1086
- durationMs: cell.durationMs,
1087
- output: [
1245
+ outputs: [
1088
1246
  ...evalOutputNodes(
1089
1247
  cell.output,
1090
- cell.hasMarkdown === true,
1091
- cell.status === "running",
1092
- cell.status === "error",
1248
+ cell.hasMarkdown === true && cell.status !== "error",
1249
+ previewLines,
1093
1250
  ),
1094
- ...(cell.statusEvents ?? []).map(describeStatusEvent),
1251
+
1095
1252
  ...(i === cellResults.length - 1 ? jsonNodes : []),
1096
1253
  ],
1254
+ status: evalStatusSection(cell, expanded),
1255
+ foot: state && {
1256
+ state,
1257
+ exitCode: cell.exitCode,
1258
+ elapsedMs,
1259
+ ...(i === factsIndex ? callFacts : {}),
1260
+ },
1097
1261
  },
1098
1262
  i,
1099
1263
  cellResults.length,
1100
1264
  );
1101
1265
  });
1102
- } else {
1103
- // No per-cell results (older details): the call's cells, with the whole output under the last.
1104
- const argCells = getRenderCells(args);
1105
- const languages = details?.languages ?? (details?.language ? [details.language] : []);
1106
- head = evalToolHead(
1266
+ return {
1267
+ tool: evalToolHead(cellResults[0]!.title, languages, cellResults.length, details),
1268
+ body: cells,
1269
+ preview: "children",
1270
+ };
1271
+ }
1272
+ // No per-cell results (older details): the call's cells, the whole output and foot under the last.
1273
+ const argCells = getRenderCells(args);
1274
+ const languages = details?.languages ?? (details?.language ? [details.language] : []);
1275
+ const rawOutput = options.renderContext?.output ?? resultText(result).trimEnd();
1276
+ const state: RunState = cancelled ? "cancelled" : isPartial ? "running" : result.isError ? "failed" : "done";
1277
+ const last: Omit<EvalCellRun, "language" | "code" | "title"> = {
1278
+ outputs: [
1279
+ ...evalOutputNodes(stripOutputNotice(rawOutput, details?.meta).trimEnd(), false, previewLines),
1280
+ ...jsonNodes,
1281
+ ],
1282
+ status: evalStatusSection(details ?? {}, expanded),
1283
+ foot: { state, elapsedMs: options.elapsedMs, ...callFacts },
1284
+ };
1285
+ const runs: EvalCellRun[] =
1286
+ argCells.length > 0
1287
+ ? argCells.map((cell, i) => (i === argCells.length - 1 ? { ...cell, ...last } : cell))
1288
+ : [{ language: languages[0] ?? "python", code: "", ...last }];
1289
+ return {
1290
+ tool: evalToolHead(
1107
1291
  argCells[0]?.title,
1108
1292
  argCells.length > 0 ? argCells.map(cell => cell.language) : languages,
1109
1293
  argCells.length,
1110
1294
  details,
1111
- );
1112
- const rawOutput = options.renderContext?.output ?? resultText(result).trimEnd();
1113
- const output = [
1114
- ...evalOutputNodes(
1115
- stripOutputNotice(rawOutput, details?.meta).trimEnd(),
1116
- false,
1117
- isPartial,
1118
- result.isError === true,
1119
- ),
1120
- ...(details?.statusEvents ?? []).map(describeStatusEvent),
1121
- ...jsonNodes,
1122
- ];
1123
- cells =
1124
- argCells.length > 0
1125
- ? argCells.map((cell, i) =>
1126
- evalCellSection(
1127
- {
1128
- ...cell,
1129
- status: isPartial ? "running" : result.isError ? "error" : "complete",
1130
- output: i === argCells.length - 1 ? output : undefined,
1131
- },
1132
- i,
1133
- argCells.length,
1134
- ),
1135
- )
1136
- : output;
1137
- }
1138
- return {
1139
- tool: head,
1140
- body: compact<NativeChild>([...cells, footer]),
1141
- preview: { lines: previewLines },
1295
+ ),
1296
+ body: runs.map((run, i) => evalCellBox(run, i, runs.length)),
1297
+ preview: "children",
1142
1298
  };
1143
1299
  },
1144
1300