@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
@@ -15,11 +15,12 @@ interface AgentTreeProjection<TRecord extends AgentRecordLike> {
15
15
  export declare const STATUS_ORDER: Record<AgentStatus, number>;
16
16
  /** Exact observer usage for one roster entry. */
17
17
  export declare function progressMetrics(observed: ObservableSession | undefined): AgentMetrics | undefined;
18
- /** Live session whose own messages back a row's metrics when no observer progress exists. */
18
+ /** Live session used to cache a row's direct assistant usage, including with observer progress. */
19
19
  export declare function hubFallbackStatsSession<TRecord extends AgentRecordLike>(ref: TRecord, observed: ObservableSession | undefined): NonNullable<TRecord["session"]> | undefined;
20
20
  /**
21
- * One roster row's usage: live observer progress, then persisted history, then
22
- * the cached fallback read of a live session (populated by {@link aggregateMetrics}).
21
+ * Preserve progress/history for non-cost metrics, overlaying cumulative direct-session cost.
22
+ * Persisted `history.metrics.cost` is a leaf-scoped snapshot, so it never overrides live
23
+ * progress; only the cumulative direct-cost sources do.
23
24
  */
24
25
  export declare function hubRowMetrics<TRecord extends AgentRecordLike>(ref: TRecord, observed: ObservableSession | undefined, sessionMetrics: WeakMap<object, {
25
26
  metrics: AgentMetrics | undefined;
@@ -37,6 +38,6 @@ export declare function aggregateMetrics<TRecord extends AgentRecordLike>(args:
37
38
  metrics: AggregateMetrics;
38
39
  hasFallbackLiveSessions: boolean;
39
40
  };
40
- /** Parent-before-child projection preserving the roster's stable sibling order. */
41
- export declare function projectAgentTree<TRecord extends AgentRecordLike>(refs: readonly TRecord[]): AgentTreeProjection<TRecord>;
41
+ /** Parent-before-child projection preserving initial subtree rank and prepending new siblings. */
42
+ export declare function projectAgentTree<TRecord extends AgentRecordLike>(refs: readonly TRecord[], rosterRank: ReadonlyMap<TRecord, number> | undefined): AgentTreeProjection<TRecord>;
42
43
  export {};
@@ -23,6 +23,68 @@ function finiteMetric(value: number | undefined): number {
23
23
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
24
24
  }
25
25
 
26
+ /** Message list a fallback metrics read was taken from; unchanged list ⇒ unchanged metrics. */
27
+ interface FallbackReadStamp {
28
+ messages: readonly unknown[];
29
+ length: number;
30
+ last: unknown;
31
+ /** Usage and block count of the last message, which a streaming turn mutates in place. */
32
+ lastShape: string;
33
+ /** Session model, whose context window the context gauge reports. */
34
+ model: unknown;
35
+ contextWindow: number | null | undefined;
36
+ }
37
+
38
+ /** Stamp of each cached fallback read, keyed by the cache entry so it cannot outlive that read. */
39
+ const fallbackReadStamps = new WeakMap<object, FallbackReadStamp>();
40
+
41
+ function lastMessageShape(last: unknown): string {
42
+ if (!last || typeof last !== "object") return "";
43
+ const { usage, content } = last as {
44
+ usage?: { input?: number; output?: number; cacheWrite?: number; cost?: { total?: number } };
45
+ content?: unknown;
46
+ };
47
+ const blocks = Array.isArray(content) ? content.length : -1;
48
+ return `${usage?.input}:${usage?.output}:${usage?.cacheWrite}:${usage?.cost?.total}:${blocks}`;
49
+ }
50
+
51
+ function fallbackReadStamp(session: NonNullable<AgentRecordLike["session"]>): FallbackReadStamp | undefined {
52
+ try {
53
+ const messages = session.agent?.state?.messages;
54
+ if (!Array.isArray(messages)) return undefined;
55
+ const last = messages[messages.length - 1];
56
+ return {
57
+ messages,
58
+ length: messages.length,
59
+ last,
60
+ lastShape: lastMessageShape(last),
61
+ model: session.model,
62
+ contextWindow: session.model?.contextWindow,
63
+ };
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Whether a cached fallback read still matches the session's message list
71
+ * (append, compaction, rewrite, streaming tail) and model (context window).
72
+ */
73
+ function fallbackReadCurrent(entry: object, session: NonNullable<AgentRecordLike["session"]>): boolean {
74
+ const stamp = fallbackReadStamps.get(entry);
75
+ if (!stamp) return false;
76
+ const current = fallbackReadStamp(session);
77
+ return (
78
+ current !== undefined &&
79
+ current.messages === stamp.messages &&
80
+ current.length === stamp.length &&
81
+ current.last === stamp.last &&
82
+ current.lastShape === stamp.lastShape &&
83
+ current.model === stamp.model &&
84
+ current.contextWindow === stamp.contextWindow
85
+ );
86
+ }
87
+
26
88
  /** Exact observer usage for one roster entry. */
27
89
  export function progressMetrics(observed: ObservableSession | undefined): AgentMetrics | undefined {
28
90
  const progress = observed?.progress;
@@ -60,12 +122,33 @@ export function progressMetrics(observed: ObservableSession | undefined): AgentM
60
122
  };
61
123
  }
62
124
 
125
+ /** Lifetime cost from the authoritative session index, excluding task-result child billing. */
126
+ function readSessionDirectCost(session: NonNullable<AgentRecordLike["session"]>): number | undefined {
127
+ try {
128
+ const usage = session.sessionManager?.getUsageStatistics();
129
+ if (
130
+ usage &&
131
+ Number.isFinite(usage.cost) &&
132
+ Number.isFinite(usage.subagentCost) &&
133
+ usage.cost >= usage.subagentCost
134
+ ) {
135
+ return usage.cost - usage.subagentCost;
136
+ }
137
+ } catch {
138
+ // A session stopping during render may have already closed its stats host.
139
+ }
140
+ return undefined;
141
+ }
142
+
63
143
  /**
64
144
  * Read direct assistant usage from a live session. SessionStats also includes
65
145
  * usage embedded in completed `task` tool results, so using it for a parent
66
146
  * row would double-count child rows in the aggregate.
67
147
  */
68
- function readSessionMetrics(session: NonNullable<AgentRecordLike["session"]>): AgentMetrics | undefined {
148
+ function readSessionMetrics(
149
+ session: NonNullable<AgentRecordLike["session"]>,
150
+ directCost: number | undefined,
151
+ ): AgentMetrics | undefined {
69
152
  try {
70
153
  const stats = session.getSessionStats();
71
154
  const messages = session.agent?.state?.messages;
@@ -74,7 +157,7 @@ function readSessionMetrics(session: NonNullable<AgentRecordLike["session"]>): A
74
157
  tokens: stats.tokens.input + stats.tokens.output + stats.tokens.cacheWrite,
75
158
  requests: stats.assistantMessages,
76
159
  tools: stats.toolCalls,
77
- cost: stats.cost,
160
+ cost: directCost ?? stats.cost,
78
161
  durationMs: 0,
79
162
  durationKind: "unknown",
80
163
  contextTokens: stats.contextUsage?.tokens,
@@ -97,7 +180,7 @@ function readSessionMetrics(session: NonNullable<AgentRecordLike["session"]>): A
97
180
  tokens,
98
181
  requests,
99
182
  tools,
100
- cost,
183
+ cost: directCost ?? cost,
101
184
  durationMs: 0,
102
185
  durationKind: "unknown",
103
186
  contextTokens: stats.contextUsage?.tokens,
@@ -110,29 +193,39 @@ function readSessionMetrics(session: NonNullable<AgentRecordLike["session"]>): A
110
193
  }
111
194
  }
112
195
 
113
- /** Live session whose own messages back a row's metrics when no observer progress exists. */
196
+ /** Live session used to cache a row's direct assistant usage, including with observer progress. */
114
197
  export function hubFallbackStatsSession<TRecord extends AgentRecordLike>(
115
198
  ref: TRecord,
116
199
  observed: ObservableSession | undefined,
117
200
  ): NonNullable<TRecord["session"]> | undefined {
118
- if (observed?.progress) return undefined;
201
+ void observed;
119
202
  const session = ref.session;
120
203
  return session && typeof session.getSessionStats === "function" ? session : undefined;
121
204
  }
122
205
 
206
+ function withDirectCost(metrics: AgentMetrics | undefined, cost: number | undefined): AgentMetrics | undefined {
207
+ return metrics && cost !== undefined ? { ...metrics, cost } : metrics;
208
+ }
209
+
123
210
  /**
124
- * One roster row's usage: live observer progress, then persisted history, then
125
- * the cached fallback read of a live session (populated by {@link aggregateMetrics}).
211
+ * Preserve progress/history for non-cost metrics, overlaying cumulative direct-session cost.
212
+ * Persisted `history.metrics.cost` is a leaf-scoped snapshot, so it never overrides live
213
+ * progress; only the cumulative direct-cost sources do.
126
214
  */
127
215
  export function hubRowMetrics<TRecord extends AgentRecordLike>(
128
216
  ref: TRecord,
129
217
  observed: ObservableSession | undefined,
130
218
  sessionMetrics: WeakMap<object, { metrics: AgentMetrics | undefined }>,
131
219
  ): AgentMetrics | undefined {
132
- if (observed?.progress) return progressMetrics(observed);
133
- if (ref.history?.metrics) return ref.history.metrics;
134
220
  const session = hubFallbackStatsSession(ref, observed);
135
- return session ? sessionMetrics.get(session)?.metrics : undefined;
221
+ const liveEntry = session ? sessionMetrics.get(session) : undefined;
222
+ const retainedEntry = sessionMetrics.get(ref);
223
+ const historyMetrics = ref.history?.metrics;
224
+ const cost = liveEntry?.metrics?.cost ?? ref.history?.directCost ?? retainedEntry?.metrics?.cost;
225
+ if (observed?.progress) return withDirectCost(progressMetrics(observed), cost);
226
+ if (historyMetrics) return withDirectCost(historyMetrics, cost);
227
+ if (!session && retainedEntry?.metrics) return withDirectCost(retainedEntry.metrics, cost);
228
+ return withDirectCost(liveEntry?.metrics, cost);
136
229
  }
137
230
 
138
231
  export function aggregateMetrics<TRecord extends AgentRecordLike>(args: {
@@ -163,9 +256,21 @@ export function aggregateMetrics<TRecord extends AgentRecordLike>(args: {
163
256
  const fallbackSession = args.fallbackStatsSession(ref, observed);
164
257
  if (fallbackSession) {
165
258
  hasFallbackLiveSessions = true;
166
- if (args.refreshFallback || !args.sessionMetrics.has(fallbackSession)) {
167
- args.sessionMetrics.set(fallbackSession, { metrics: readSessionMetrics(fallbackSession) });
259
+ let cached = args.sessionMetrics.get(fallbackSession);
260
+ const directCost = readSessionDirectCost(fallbackSession);
261
+ // Refresh cost independently from the message scan: model_usage entries
262
+ // are cumulative but do not alter the agent's live message list.
263
+ if (!cached || (args.refreshFallback && !fallbackReadCurrent(cached, fallbackSession))) {
264
+ const stamp = fallbackReadStamp(fallbackSession);
265
+ const entry = { metrics: readSessionMetrics(fallbackSession, directCost) };
266
+ if (stamp) fallbackReadStamps.set(entry, stamp);
267
+ args.sessionMetrics.set(fallbackSession, entry);
268
+ cached = entry;
269
+ } else if (cached.metrics && directCost !== undefined) {
270
+ cached.metrics.cost = directCost;
168
271
  }
272
+ // Preserve valid direct-session snapshots through parking or a failed teardown read.
273
+ if (cached.metrics) args.sessionMetrics.set(ref, cached);
169
274
  }
170
275
  const metrics = args.metricsFor(ref, observed);
171
276
  if (!metrics || (fallbackSession && countedFallbackSessions.has(fallbackSession))) continue;
@@ -183,9 +288,10 @@ export function aggregateMetrics<TRecord extends AgentRecordLike>(args: {
183
288
  return { metrics: total, hasFallbackLiveSessions };
184
289
  }
185
290
 
186
- /** Parent-before-child projection preserving the roster's stable sibling order. */
291
+ /** Parent-before-child projection preserving initial subtree rank and prepending new siblings. */
187
292
  export function projectAgentTree<TRecord extends AgentRecordLike>(
188
293
  refs: readonly TRecord[],
294
+ rosterRank: ReadonlyMap<TRecord, number> | undefined,
189
295
  ): AgentTreeProjection<TRecord> {
190
296
  const ids = new Set<string>();
191
297
  const operationalIndex = new Map<string, number>();
@@ -218,7 +324,12 @@ export function projectAgentTree<TRecord extends AgentRecordLike>(
218
324
  if (!current) continue;
219
325
  if (current.expanded) {
220
326
  let order = operationalIndex.get(current.ref.id) ?? Number.MAX_SAFE_INTEGER;
327
+ const parentRank = rosterRank?.get(current.ref);
221
328
  for (const child of children.get(current.ref.id) ?? []) {
329
+ const childRank = rosterRank?.get(child);
330
+ // A child spawned later cannot move an existing parent's group.
331
+ if (parentRank !== undefined && childRank !== undefined && childRank < 0 && childRank < parentRank)
332
+ continue;
222
333
  order = Math.min(order, subtreeOrder.get(child.id) ?? Number.MAX_SAFE_INTEGER);
223
334
  }
224
335
  subtreeOrder.set(current.ref.id, order);
@@ -28,6 +28,13 @@ export interface AgentHubSession {
28
28
  messages: AgentMessage[];
29
29
  };
30
30
  };
31
+ /** Existing session ledger includes off-transcript model usage and nested-task attribution. */
32
+ readonly sessionManager?: {
33
+ getUsageStatistics(): {
34
+ cost: number;
35
+ subagentCost: number;
36
+ };
37
+ };
31
38
  getSessionStats(): {
32
39
  tokens: {
33
40
  input: number;
@@ -64,6 +71,8 @@ export interface AgentRecordLike {
64
71
  resolvedModel?: string;
65
72
  resolvedModelIsFallback?: boolean;
66
73
  metrics?: AgentMetricsSummary;
74
+ /** Cumulative own-session assistant and model_usage cost, excluding nested task results. */
75
+ directCost?: number;
67
76
  readOnly?: boolean;
68
77
  outputPath?: string;
69
78
  patchPath?: string;
@@ -22,6 +22,10 @@ export interface AgentHubSession {
22
22
  readonly model: Model | undefined;
23
23
  readonly servingModel?: { selector: string; isFallback: boolean };
24
24
  readonly agent?: { state: { messages: AgentMessage[] } };
25
+ /** Existing session ledger includes off-transcript model usage and nested-task attribution. */
26
+ readonly sessionManager?: {
27
+ getUsageStatistics(): { cost: number; subagentCost: number };
28
+ };
25
29
  getSessionStats(): {
26
30
  tokens: { input: number; output: number; cacheWrite: number };
27
31
  assistantMessages: number;
@@ -49,6 +53,8 @@ export interface AgentRecordLike {
49
53
  resolvedModel?: string;
50
54
  resolvedModelIsFallback?: boolean;
51
55
  metrics?: AgentMetricsSummary;
56
+ /** Cumulative own-session assistant and model_usage cost, excluding nested task results. */
57
+ directCost?: number;
52
58
  readOnly?: boolean;
53
59
  outputPath?: string;
54
60
  patchPath?: string;
@@ -200,6 +200,12 @@ const DATA_CHANGE_RENDER_COALESCE_MS = 100;
200
200
  /** Double-tap window for the table's left-left "close hub" gesture. */
201
201
  const LEFT_TAP_WINDOW_MS = 500;
202
202
 
203
+ function compareRosterAgents(a: AgentRecordLike, b: AgentRecordLike): number {
204
+ return (
205
+ STATUS_ORDER[a.status] - STATUS_ORDER[b.status] || b.lastActivity - a.lastActivity || a.id.localeCompare(b.id)
206
+ );
207
+ }
208
+
203
209
  function activityGlyph(row: AgentActivityRow): string {
204
210
  if (row.status === "error") return theme.fg("error", theme.status.error);
205
211
  if (row.status === "aborted") return theme.fg("warning", theme.status.aborted);
@@ -358,11 +364,10 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
358
364
  #rows: TRecord[] = [];
359
365
  #statusCounts: Record<AgentStatus, number> = { running: 0, idle: 0, parked: 0, aborted: 0 };
360
366
  #selectedRow = 0;
361
- /** Stable roster order captured on first refresh: keyboard navigation must
362
- * not jump as agents heartbeat. Existing agent generations keep their rank
363
- * while the hub is open; newly appearing generations append at the end. */
367
+ /** Initial status/recency rank stays fixed while open; new generations
368
+ * prepend without changing the relative order of existing rows. */
364
369
  #rowOrder: Map<TRecord, number> | undefined;
365
- #nextRowOrder = 0;
370
+ #nextNewRowOrder = 0;
366
371
  #hoveredRow: number | null = null;
367
372
  /** Per-render screen-line to agent-row map, shared by click and hover routing. */
368
373
  #hitRows: Array<number | undefined> = [];
@@ -616,6 +621,11 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
616
621
  this.#lastLeftTap = Date.now();
617
622
  }
618
623
 
624
+ /** Show `section`, as a slash-command deep link into an already-open hub does. */
625
+ showSection(section: AgentHubSection): void {
626
+ this.#switchSection(section);
627
+ }
628
+
619
629
  /**
620
630
  * Open the fullscreen transcript viewer for an agent id (public for table Enter
621
631
  * and tests). Mounts {@link AgentTranscriptViewer} as a `fullscreen` overlay so it
@@ -941,12 +951,16 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
941
951
  return { ...item, depth, open: this.#childrenByParent.has(ref.id) ? true : undefined };
942
952
  }
943
953
  if (!metrics) return item;
954
+ // A running agent's active time ticks terminal-side from the age at send; a settled one
955
+ // is frozen text. Progress `durationMs` was current when its frame arrived, so age it to now
956
+ // — re-sending the stale value on every repaint rewinds Tern's clock. A first frame can
957
+ // report 0 ms, so a progress timestamp alone makes the row clockable.
958
+ const progressAt = observed?.progress ? observed.progressAt : undefined;
944
959
  const facts: Record<string, string | number> = {
945
960
  cost: formatCost(metrics.cost),
946
- // A running agent's active time ticks terminal-side; a settled one is frozen text.
947
961
  time:
948
- ref.status === "running" && metrics.durationMs > 0
949
- ? metrics.durationMs
962
+ ref.status === "running" && (metrics.durationMs > 0 || progressAt !== undefined)
963
+ ? metrics.durationMs + (progressAt === undefined ? 0 : Math.max(0, Date.now() - progressAt))
950
964
  : formatDuration(metrics.durationMs),
951
965
  req: metrics.requests,
952
966
  tools: metrics.tools,
@@ -1519,35 +1533,35 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
1519
1533
  const refs = this.#registry.list().filter(ref => ref.id !== MAIN_AGENT_ID);
1520
1534
  this.#observedById = new Map();
1521
1535
  for (const session of this.#observers.getSessions()) this.#observedById.set(session.id, session);
1522
- // Stable roster order: capture the status+recency ranking once so keyboard
1523
- // navigation is not disrupted by heartbeats (issue #10524). Existing rows
1524
- // keep their rank while the hub is open; new agents append at the end.
1525
- // Defer the capture until persisted-subagent discovery settles so a
1526
- // mid-scan refresh cannot freeze a partial roster (the remaining agents
1527
- // would otherwise append in readdir order instead of being ranked).
1536
+ // Capture the status+recency ranking once so keyboard navigation does
1537
+ // not jump on heartbeats. Defer until persisted-subagent discovery
1538
+ // settles so a partial roster is not frozen in readdir order.
1528
1539
  const rowOrder = this.#rowOrder;
1529
1540
  let ordered: TRecord[];
1530
1541
  if (!rowOrder) {
1531
- ordered = refs.sort(
1532
- (a, b) =>
1533
- STATUS_ORDER[a.status] - STATUS_ORDER[b.status] ||
1534
- b.lastActivity - a.lastActivity ||
1535
- a.id.localeCompare(b.id),
1536
- );
1542
+ ordered = refs.sort(compareRosterAgents);
1537
1543
  if (!this.#loadingPersistedSubagents && ordered.length > 0) {
1538
- this.#rowOrder = new Map();
1539
- for (const ref of ordered) this.#rowOrder.set(ref, this.#nextRowOrder++);
1544
+ const rowOrder = new Map<TRecord, number>();
1545
+ for (const [index, ref] of ordered.entries()) rowOrder.set(ref, index);
1546
+ this.#rowOrder = rowOrder;
1540
1547
  }
1541
1548
  } else {
1542
1549
  for (const rankedRef of rowOrder.keys()) {
1543
1550
  if (!refs.includes(rankedRef)) rowOrder.delete(rankedRef);
1544
1551
  }
1545
- ordered = refs.sort(
1546
- (a, b) => (rowOrder.get(a) ?? Number.MAX_SAFE_INTEGER) - (rowOrder.get(b) ?? Number.MAX_SAFE_INTEGER),
1547
- );
1548
- for (const ref of ordered) {
1549
- if (!rowOrder.has(ref)) rowOrder.set(ref, this.#nextRowOrder++);
1552
+ // Each batch of newly appearing generations uses the initial sort,
1553
+ // then takes ranks above every agent already on screen.
1554
+ let newcomers: TRecord[] | undefined;
1555
+ for (const ref of refs) {
1556
+ if (!rowOrder.has(ref)) (newcomers ??= []).push(ref);
1557
+ }
1558
+ if (newcomers) {
1559
+ newcomers.sort(compareRosterAgents);
1560
+ for (let i = newcomers.length - 1; i >= 0; i--) {
1561
+ rowOrder.set(newcomers[i]!, --this.#nextNewRowOrder);
1562
+ }
1550
1563
  }
1564
+ ordered = refs.sort((a, b) => rowOrder.get(a)! - rowOrder.get(b)!);
1551
1565
  }
1552
1566
  const query = this.#agentFilter.getValue().trim();
1553
1567
  const rosterRows =
@@ -1556,7 +1570,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
1556
1570
  : ordered;
1557
1571
 
1558
1572
  if (this.#viewMode === "tree") {
1559
- const tree = projectAgentTree(rosterRows);
1573
+ const tree = projectAgentTree(rosterRows, rowOrder);
1560
1574
  this.#rows = tree.rows;
1561
1575
  this.#treeDepthById = tree.depthById;
1562
1576
  this.#treeParentById = tree.parentById;
@@ -1589,7 +1603,8 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
1589
1603
  for (const ref of rosterRows) this.#statusCounts[ref.status]++;
1590
1604
  this.#refreshAggregate();
1591
1605
  this.#refreshActivityData(rosterRows);
1592
- this.#refreshActivityRows();
1606
+ // The 2,000-row activity query only feeds the Activity tab; switching to it refreshes.
1607
+ if (this.#section === "activity") this.#refreshActivityRows();
1593
1608
  }
1594
1609
 
1595
1610
  #refreshActivityData(refs: readonly TRecord[]): void {
@@ -1620,7 +1635,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
1620
1635
  void Promise.all(pending)
1621
1636
  .then(() => {
1622
1637
  if (this.#disposed || generation !== this.#activitySyncGeneration) return;
1623
- this.#refreshActivityRows();
1638
+ if (this.#section === "activity") this.#refreshActivityRows();
1624
1639
  this.#requestRender();
1625
1640
  })
1626
1641
  .catch(() => {
@@ -41,15 +41,22 @@ import { sanitizeErrorLine } from "../chrome/error-block";
41
41
  import type { ScrollRangeAnchor } from "../components/scroll-view";
42
42
  import { formatContextUsage } from "../chrome/context-thresholds";
43
43
  import { node, span, text } from "../native/describe";
44
- import type { DescribeContext, NativeChild, NativeNode } from "../native/node";
45
- import { actionHint, hintsRow, overlayCard } from "../native/overlay";
44
+ import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
45
+ import { actionHint, escCloseButton, hintsRow, overlayCard } from "../native/overlay";
46
46
 
47
47
  /** Parsed message and model metadata relevant to a transcript viewer. */
48
48
  export type AgentTranscriptEntry = SessionMessageEntryLike | { type: "model_change"; model: string };
49
49
 
50
50
  /** Local filesystem and session parsing capabilities supplied by the host. */
51
51
  export interface AgentTranscriptSource {
52
- fs: Pick<typeof fs, "openSync" | "closeSync" | "readSync" | "readFileSync" | "statSync">;
52
+ /**
53
+ * Sync primitives drive the first paint and the incremental tail. When the
54
+ * host also supplies `promises.readFile` (e.g. passes `node:fs` itself), full
55
+ * reloads after rotation/rewrite read the file off the UI thread.
56
+ */
57
+ fs: Pick<typeof fs, "openSync" | "closeSync" | "readSync" | "readFileSync" | "statSync"> & {
58
+ promises?: Pick<typeof fs.promises, "readFile">;
59
+ };
53
60
  parseEntries(text: string): AgentTranscriptEntry[];
54
61
  }
55
62
 
@@ -86,6 +93,8 @@ export interface AgentTranscriptViewerDeps {
86
93
 
87
94
  /** How often to re-stat a file-backed transcript for growth (advisor/live tail). */
88
95
  const POLL_MS = 250;
96
+ /** Every Nth idle poll re-verifies sentinels, catching same-size/mtime rewrites. */
97
+ const IDLE_SENTINEL_CHECK_EVERY = 5;
89
98
 
90
99
  const SENTINEL_BYTES = 4096;
91
100
 
@@ -183,6 +192,10 @@ export class AgentTranscriptViewer implements Component {
183
192
  #model: string | undefined;
184
193
  #pollTimer: NodeJS.Timeout | undefined;
185
194
  #disposed = false;
195
+ /** Async full reload in flight; polls are skipped until it lands. */
196
+ #localLoadInFlight = false;
197
+ /** Idle polls since the sentinels were last verified. */
198
+ #idlePolls = 0;
186
199
  #initialEntryId: string | undefined;
187
200
  /** Last described node and the visible inputs it was built from. */
188
201
  #nativeCache: { signature: string; node: NativeNode } | undefined;
@@ -201,6 +214,8 @@ export class AgentTranscriptViewer implements Component {
201
214
  hideThinkingBlock: deps.hideThinkingBlock,
202
215
  proseOnlyThinking: deps.proseOnlyThinking,
203
216
  expandThinkingBlocks: deps.expandThinkingBlocks,
217
+ // Charts are for the main session's answers, not parked subagent, advisor, or guest transcripts.
218
+ tableCharts: false,
204
219
  requestRender: deps.requestRender,
205
220
  });
206
221
  this.#browser = new TranscriptBrowser({
@@ -213,8 +228,10 @@ export class AgentTranscriptViewer implements Component {
213
228
  this.#editor.setMaxHeight(4);
214
229
  this.#editor.onSubmit = text => this.#submit(text);
215
230
  }
216
- this.#refresh();
217
- this.#pollTimer = setInterval(() => this.#refresh(), POLL_MS);
231
+ // First paint loads synchronously so the initial entry can be revealed
232
+ // immediately; later full reloads from the poll may go async.
233
+ this.#refresh(false);
234
+ this.#pollTimer = setInterval(() => this.#refresh(true), POLL_MS);
218
235
  this.#pollTimer.unref?.();
219
236
  }
220
237
 
@@ -243,12 +260,13 @@ export class AgentTranscriptViewer implements Component {
243
260
  // ========================================================================
244
261
 
245
262
  /** Refresh the transcript from a local file or remote host. */
246
- #refresh(): void {
263
+ #refresh(allowAsync: boolean): void {
247
264
  if (this.#disposed) return;
248
265
  if (this.#deps.remote) {
249
266
  this.#fetchRemote();
250
267
  return;
251
268
  }
269
+ if (this.#localLoadInFlight) return;
252
270
  const sessionFile = this.#deps.registry.get(this.#deps.agentId)?.sessionFile;
253
271
  if (!sessionFile) {
254
272
  this.#clearLocal("none");
@@ -262,14 +280,29 @@ export class AgentTranscriptViewer implements Component {
262
280
  return;
263
281
  }
264
282
  const state = this.#localState;
265
- if (state && this.#canAppendLocal(sessionFile, stat, state)) {
266
- if (stat.size === state.size && stat.mtimeMs === state.mtimeMs) return;
267
- if (stat.size > state.size) {
268
- this.#appendLocal(sessionFile, stat, state);
283
+ if (state) {
284
+ // Idle fast path: an unchanged identity/size/mtime costs one stat;
285
+ // sentinels are re-read only every Nth idle poll so a rewrite that
286
+ // keeps size and mtime is still noticed.
287
+ if (
288
+ state.path === sessionFile &&
289
+ state.dev === stat.dev &&
290
+ state.ino === stat.ino &&
291
+ stat.size === state.size &&
292
+ stat.mtimeMs === state.mtimeMs
293
+ ) {
294
+ if (++this.#idlePolls < IDLE_SENTINEL_CHECK_EVERY) return;
295
+ this.#idlePolls = 0;
296
+ if (this.#canAppendLocal(sessionFile, stat, state)) return;
297
+ this.#loadLocalFull(sessionFile, stat, allowAsync);
298
+ return;
299
+ }
300
+ if (stat.size > state.size && this.#canAppendLocal(sessionFile, stat, state)) {
301
+ this.#appendLocal(sessionFile, stat, state, allowAsync);
269
302
  return;
270
303
  }
271
304
  }
272
- this.#loadLocalFull(sessionFile, stat);
305
+ this.#loadLocalFull(sessionFile, stat, allowAsync);
273
306
  }
274
307
 
275
308
  #clearLocal(reason: string): void {
@@ -303,7 +336,23 @@ export class AgentTranscriptViewer implements Component {
303
336
  return true;
304
337
  }
305
338
 
306
- #loadLocalFull(sessionFile: string, stat: fs.Stats): void {
339
+ #loadLocalFull(sessionFile: string, stat: fs.Stats, allowAsync: boolean): void {
340
+ const promises = allowAsync ? this.#deps.transcript.fs.promises : undefined;
341
+ if (promises) {
342
+ this.#localLoadInFlight = true;
343
+ void promises
344
+ .readFile(sessionFile)
345
+ .then(data => {
346
+ this.#localLoadInFlight = false;
347
+ if (!this.#disposed) this.#applyLocalFull(sessionFile, stat, data);
348
+ })
349
+ .catch((err: unknown) => {
350
+ // Leave #localState unchanged so a transient read error retries next poll.
351
+ this.#localLoadInFlight = false;
352
+ logger.debug("transcript viewer: read failed", { err: String(err) });
353
+ });
354
+ return;
355
+ }
307
356
  let data: Buffer;
308
357
  try {
309
358
  data = this.#deps.transcript.fs.readFileSync(sessionFile);
@@ -312,6 +361,10 @@ export class AgentTranscriptViewer implements Component {
312
361
  logger.debug("transcript viewer: read failed", { err: String(err) });
313
362
  return;
314
363
  }
364
+ this.#applyLocalFull(sessionFile, stat, data);
365
+ }
366
+
367
+ #applyLocalFull(sessionFile: string, stat: fs.Stats, data: Buffer): void {
315
368
  // The file may have grown between the earlier `statSync` and this read.
316
369
  // Anchor the tail cursor to what we actually consumed so the next poll's
317
370
  // `#appendLocal` never re-renders bytes already in the rebuilt transcript;
@@ -345,7 +398,7 @@ export class AgentTranscriptViewer implements Component {
345
398
  this.#rebuild(this.#extractMessages(this.#deps.transcript.parseEntries(complete)));
346
399
  }
347
400
 
348
- #appendLocal(sessionFile: string, stat: fs.Stats, state: LocalTranscriptState): void {
401
+ #appendLocal(sessionFile: string, stat: fs.Stats, state: LocalTranscriptState, allowAsync: boolean): void {
349
402
  let chunk: string;
350
403
  try {
351
404
  chunk = readFileRangeSync(
@@ -356,7 +409,7 @@ export class AgentTranscriptViewer implements Component {
356
409
  ).toString("utf-8");
357
410
  } catch (err) {
358
411
  logger.debug("transcript viewer: tail read failed", { err: String(err) });
359
- this.#loadLocalFull(sessionFile, stat);
412
+ this.#loadLocalFull(sessionFile, stat, allowAsync);
360
413
  return;
361
414
  }
362
415
  const combined = state.pending + chunk;
@@ -371,7 +424,7 @@ export class AgentTranscriptViewer implements Component {
371
424
  // File unlinked/rotated mid-poll: fall back to a guarded full reload
372
425
  // instead of letting the open escape the poll timer.
373
426
  logger.debug("transcript viewer: sentinel recompute failed", { err: String(err) });
374
- this.#loadLocalFull(sessionFile, stat);
427
+ this.#loadLocalFull(sessionFile, stat, allowAsync);
375
428
  return;
376
429
  }
377
430
  this.#localState = {
@@ -506,12 +559,7 @@ export class AgentTranscriptViewer implements Component {
506
559
  }
507
560
 
508
561
  if (matchesKey(data, "escape")) {
509
- if (this.#editor && this.#editor.getText().trim() !== "") {
510
- this.#editor.setText("");
511
- this.#deps.requestRender();
512
- return;
513
- }
514
- this.#deps.onClose();
562
+ this.#escape();
515
563
  return;
516
564
  }
517
565
 
@@ -594,6 +642,21 @@ export class AgentTranscriptViewer implements Component {
594
642
  return lines;
595
643
  }
596
644
 
645
+ /** The top-right `esc` runs Esc. */
646
+ handleNativeEvent(event: NativeUiEvent): void {
647
+ if (event.type === "action" && event.act === "close") this.#escape();
648
+ }
649
+
650
+ /** Esc: clear a non-empty draft first, else close the viewer. */
651
+ #escape(): void {
652
+ if (this.#editor && this.#editor.getText().trim() !== "") {
653
+ this.#editor.setText("");
654
+ this.#deps.requestRender();
655
+ return;
656
+ }
657
+ this.#deps.onClose();
658
+ }
659
+
597
660
  /**
598
661
  * Header, transcript body (the builder's container, which describes its own
599
662
  * blocks), notice, message editor, stats and key hints. Scrolling is the
@@ -622,6 +685,8 @@ export class AgentTranscriptViewer implements Component {
622
685
 
623
686
  const id = this.#deps.agentId;
624
687
  const children: NativeChild[] = [];
688
+ // Top row: the agent's meta on the left, a clickable `esc` (close) on the right.
689
+ const top: NativeChild[] = [];
625
690
  if (ref) {
626
691
  const kindTag = ref.parentId ? `${ref.kind} ${theme.sep.dot} of ${ref.parentId}` : ref.kind;
627
692
  const meta: NativeChild[] = [
@@ -630,8 +695,12 @@ export class AgentTranscriptViewer implements Component {
630
695
  text([span(kindTag, "dim")], { truncate: "end" }),
631
696
  ];
632
697
  if (this.#model) meta.push(text([span(this.#model, "muted")], { truncate: "end" }));
633
- children.push(node("row", { gap: "sm", align: "center" }, meta, "meta"));
698
+ top.push(node("row", { gap: "sm", align: "center", grow: 1, min: { w: 0 } }, meta, "meta"));
699
+ } else {
700
+ top.push(node("spacer", { grow: 1 }));
634
701
  }
702
+ top.push(escCloseButton());
703
+ children.push(node("row", { gap: "md", align: "center" }, top, "top"));
635
704
  children.push(
636
705
  placeholder === undefined
637
706
  ? node("col", { grow: 1 }, [this.#builder.container], "transcript")
@@ -694,7 +763,6 @@ export class AgentTranscriptViewer implements Component {
694
763
  children.push(
695
764
  hintsRow([
696
765
  this.#editor ? actionHint("tui.input.submit", "send") : undefined,
697
- { keys: ["escape"], label: "close" },
698
766
  { keys: [this.#deps.expandKeys[0] ?? "ctrl+o"], label: "expand" },
699
767
  ]),
700
768
  );