@oh-my-pi/pi-tui 18.2.3 → 18.2.5

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 (750) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +43 -0
  3. package/dist/types/app-keybindings.d.ts +395 -0
  4. package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
  5. package/dist/types/apps/autoresearch-data.d.ts +15 -0
  6. package/dist/types/apps/cleanse-board.d.ts +66 -0
  7. package/dist/types/apps/cleanse-picker.d.ts +23 -0
  8. package/dist/types/apps/debug/log-formatting.d.ts +6 -0
  9. package/dist/types/apps/debug/log-viewer.d.ts +81 -0
  10. package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
  11. package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
  12. package/dist/types/apps/debug/raw-sse.d.ts +26 -0
  13. package/dist/types/apps/debug/terminal-info.d.ts +34 -0
  14. package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
  15. package/dist/types/apps/git/avatar.d.ts +9 -0
  16. package/dist/types/apps/git/colors.d.ts +49 -0
  17. package/dist/types/apps/git/diff-pane.d.ts +188 -0
  18. package/dist/types/apps/git/git-tui.d.ts +66 -0
  19. package/dist/types/apps/git/sidebar.d.ts +149 -0
  20. package/dist/types/apps/git/state.d.ts +117 -0
  21. package/dist/types/apps/if-bench-board.d.ts +75 -0
  22. package/dist/types/apps/live-visualizer.d.ts +33 -0
  23. package/dist/types/apps/ps-data.d.ts +51 -0
  24. package/dist/types/apps/ps-top.d.ts +33 -0
  25. package/dist/types/apps/session-picker.d.ts +26 -0
  26. package/dist/types/apps/setup-model-picker.d.ts +13 -0
  27. package/dist/types/apps/standalone-picker.d.ts +42 -0
  28. package/dist/types/autocomplete.d.ts +4 -0
  29. package/dist/types/chat/advisor-message.d.ts +11 -0
  30. package/dist/types/chat/assistant-message.d.ts +106 -0
  31. package/dist/types/chat/background-tan-message.d.ts +9 -0
  32. package/dist/types/chat/bash-execution.d.ts +47 -0
  33. package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
  34. package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
  35. package/dist/types/chat/collab-prompt-message.d.ts +10 -0
  36. package/dist/types/chat/compaction-summary-message.d.ts +54 -0
  37. package/dist/types/chat/custom-message.d.ts +10 -0
  38. package/dist/types/chat/display-preferences.d.ts +13 -0
  39. package/dist/types/chat/eval-execution.d.ts +29 -0
  40. package/dist/types/chat/execution-shared.d.ts +51 -0
  41. package/dist/types/chat/extension-types.d.ts +28 -0
  42. package/dist/types/chat/hook-message.d.ts +10 -0
  43. package/dist/types/chat/image-loading.d.ts +46 -0
  44. package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
  45. package/dist/types/chat/messages.d.ts +189 -0
  46. package/dist/types/chat/reaction.d.ts +30 -0
  47. package/dist/types/chat/read-target.d.ts +4 -0
  48. package/dist/types/chat/read-tool-group.d.ts +67 -0
  49. package/dist/types/chat/served-model-marker.d.ts +47 -0
  50. package/dist/types/chat/skill-message.d.ts +33 -0
  51. package/dist/types/chat/skill-title-input.d.ts +14 -0
  52. package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
  53. package/dist/types/chat/thinking-display.d.ts +12 -0
  54. package/dist/types/chat/todo-reminder.d.ts +13 -0
  55. package/dist/types/chat/tool-execution.d.ts +138 -0
  56. package/dist/types/chat/transcript-browser.d.ts +93 -0
  57. package/dist/types/chat/transcript-entry.d.ts +45 -0
  58. package/dist/types/chat/transcript-outline.d.ts +86 -0
  59. package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
  60. package/dist/types/chat/ttsr-notification.d.ts +23 -0
  61. package/dist/types/chat/user-message.d.ts +52 -0
  62. package/dist/types/chrome/chat-block.d.ts +64 -0
  63. package/dist/types/chrome/collab-qrcode.d.ts +28 -0
  64. package/dist/types/chrome/context-thresholds.d.ts +10 -0
  65. package/dist/types/chrome/countdown-timer.d.ts +14 -0
  66. package/dist/types/chrome/diff.d.ts +14 -0
  67. package/dist/types/chrome/dynamic-border.d.ts +16 -0
  68. package/dist/types/chrome/error-block.d.ts +17 -0
  69. package/dist/types/chrome/form-theme.d.ts +3 -0
  70. package/dist/types/chrome/format.d.ts +12 -0
  71. package/dist/types/chrome/index.d.ts +20 -0
  72. package/dist/types/chrome/keybinding-hints.d.ts +40 -0
  73. package/dist/types/chrome/live-board.d.ts +23 -0
  74. package/dist/types/chrome/local-date.d.ts +4 -0
  75. package/dist/types/chrome/message-divider.d.ts +22 -0
  76. package/dist/types/chrome/message-frame.d.ts +52 -0
  77. package/dist/types/chrome/message-notice.d.ts +35 -0
  78. package/dist/types/chrome/overlay-box.d.ts +74 -0
  79. package/dist/types/chrome/qrcode.d.ts +48 -0
  80. package/dist/types/chrome/segment-track.d.ts +27 -0
  81. package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
  82. package/dist/types/chrome/selector-helpers.d.ts +52 -0
  83. package/dist/types/chrome/shared.d.ts +5 -0
  84. package/dist/types/chrome/streaming-panel.d.ts +19 -0
  85. package/dist/types/chrome/tool-activity.d.ts +17 -0
  86. package/dist/types/chrome/transcript-container.d.ts +129 -0
  87. package/dist/types/chrome/transcript-status.d.ts +12 -0
  88. package/dist/types/chrome/visual-truncate.d.ts +19 -0
  89. package/dist/types/components/disclosure.d.ts +42 -0
  90. package/dist/types/components/form.d.ts +130 -0
  91. package/dist/types/components/image.d.ts +18 -16
  92. package/dist/types/components/key-value-list.d.ts +24 -0
  93. package/dist/types/components/layout/geometry.d.ts +73 -0
  94. package/dist/types/components/layout/row.d.ts +55 -0
  95. package/dist/types/components/layout/split-pane.d.ts +80 -0
  96. package/dist/types/components/layout/stack.d.ts +50 -0
  97. package/dist/types/components/menu-selection.d.ts +71 -0
  98. package/dist/types/components/metric.d.ts +44 -0
  99. package/dist/types/components/progress-bar.d.ts +36 -0
  100. package/dist/types/components/scroll-view.d.ts +53 -19
  101. package/dist/types/components/scroll-viewport.d.ts +46 -0
  102. package/dist/types/components/section.d.ts +19 -0
  103. package/dist/types/components/select-list.d.ts +46 -2
  104. package/dist/types/components/settings-list.d.ts +20 -0
  105. package/dist/types/components/table.d.ts +32 -0
  106. package/dist/types/components/tree-view.d.ts +130 -0
  107. package/dist/types/components/wizard-step.d.ts +60 -0
  108. package/dist/types/hotkeys-markdown.d.ts +7 -0
  109. package/dist/types/index.d.ts +17 -0
  110. package/dist/types/keybinding-matchers.d.ts +35 -0
  111. package/dist/types/keys.d.ts +2 -2
  112. package/dist/types/lang-from-path.d.ts +9 -0
  113. package/dist/types/overlays/advisor-config.d.ts +114 -0
  114. package/dist/types/overlays/agent-activity.d.ts +41 -0
  115. package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
  116. package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
  117. package/dist/types/overlays/agent-hub-types.d.ts +99 -0
  118. package/dist/types/overlays/agent-hub.d.ts +126 -0
  119. package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
  120. package/dist/types/overlays/agents-hub.d.ts +72 -0
  121. package/dist/types/overlays/ask-dialog.d.ts +88 -0
  122. package/dist/types/overlays/bordered-loader.d.ts +11 -0
  123. package/dist/types/overlays/btw-history-panel.d.ts +29 -0
  124. package/dist/types/overlays/btw-history.d.ts +17 -0
  125. package/dist/types/overlays/btw-panel.d.ts +26 -0
  126. package/dist/types/overlays/cleanse-panel.d.ts +36 -0
  127. package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
  128. package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
  129. package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
  130. package/dist/types/overlays/copy-selector.d.ts +48 -0
  131. package/dist/types/overlays/copy-targets.d.ts +63 -0
  132. package/dist/types/overlays/error-banner.d.ts +13 -0
  133. package/dist/types/overlays/extensions/display-text.d.ts +10 -0
  134. package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
  135. package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
  136. package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
  137. package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
  138. package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
  139. package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
  140. package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
  141. package/dist/types/overlays/extensions/types.d.ts +165 -0
  142. package/dist/types/overlays/history-search.d.ts +16 -0
  143. package/dist/types/overlays/hook-editor.d.ts +44 -0
  144. package/dist/types/overlays/hook-input.d.ts +20 -0
  145. package/dist/types/overlays/hook-selector.d.ts +71 -0
  146. package/dist/types/overlays/hub-frame.d.ts +77 -0
  147. package/dist/types/overlays/login-dialog.d.ts +43 -0
  148. package/dist/types/overlays/logout-account-selector.d.ts +15 -0
  149. package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
  150. package/dist/types/overlays/model-browser.d.ts +213 -0
  151. package/dist/types/overlays/model-hub.d.ts +72 -0
  152. package/dist/types/overlays/model-picker.d.ts +74 -0
  153. package/dist/types/overlays/model-selector.d.ts +52 -0
  154. package/dist/types/overlays/move-overlay.d.ts +39 -0
  155. package/dist/types/overlays/oauth-selector.d.ts +41 -0
  156. package/dist/types/overlays/omfg-panel.d.ts +20 -0
  157. package/dist/types/overlays/pause-screen.d.ts +43 -0
  158. package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
  159. package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
  160. package/dist/types/overlays/plan-toc.d.ts +41 -0
  161. package/dist/types/overlays/plugin-selector.d.ts +28 -0
  162. package/dist/types/overlays/plugin-settings.d.ts +180 -0
  163. package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
  164. package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
  165. package/dist/types/overlays/rewind-selector.d.ts +61 -0
  166. package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
  167. package/dist/types/overlays/session-account-selector.d.ts +24 -0
  168. package/dist/types/overlays/session-info-overlay.d.ts +17 -0
  169. package/dist/types/overlays/session-observer-registry.d.ts +77 -0
  170. package/dist/types/overlays/session-selector.d.ts +131 -0
  171. package/dist/types/overlays/settings-defs.d.ts +119 -0
  172. package/dist/types/overlays/settings-selector.d.ts +73 -0
  173. package/dist/types/overlays/show-images-selector.d.ts +11 -0
  174. package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
  175. package/dist/types/overlays/theme-selector.d.ts +12 -0
  176. package/dist/types/overlays/thinking-selector.d.ts +12 -0
  177. package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
  178. package/dist/types/overlays/tree-selector.d.ts +92 -0
  179. package/dist/types/overlays/usage-dashboard.d.ts +94 -0
  180. package/dist/types/overlays/usage-display.d.ts +5 -0
  181. package/dist/types/overlays/usage-row.d.ts +17 -0
  182. package/dist/types/prompt/attachment-chips.d.ts +19 -0
  183. package/dist/types/prompt/composer-attachments.d.ts +71 -0
  184. package/dist/types/prompt/composer-cache.d.ts +37 -0
  185. package/dist/types/prompt/composer.d.ts +159 -0
  186. package/dist/types/prompt/custom-editor.d.ts +261 -0
  187. package/dist/types/prompt/editor-top-gap.d.ts +18 -0
  188. package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
  189. package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
  190. package/dist/types/prompt/gradient-highlight.d.ts +32 -0
  191. package/dist/types/prompt/image-format.d.ts +4 -0
  192. package/dist/types/prompt/image-references.d.ts +26 -0
  193. package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
  194. package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
  195. package/dist/types/prompt/macos-spelling.d.ts +44 -0
  196. package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
  197. package/dist/types/prompt/magic-keywords.d.ts +26 -0
  198. package/dist/types/prompt/markdown-prose.d.ts +15 -0
  199. package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
  200. package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
  201. package/dist/types/prompt/orchestrate.d.ts +13 -0
  202. package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
  203. package/dist/types/prompt/queue-input.d.ts +8 -0
  204. package/dist/types/prompt/skill-tokens.d.ts +13 -0
  205. package/dist/types/prompt/tools-markdown.d.ts +12 -0
  206. package/dist/types/prompt/ultrathink.d.ts +13 -0
  207. package/dist/types/prompt/usage-amounts.d.ts +26 -0
  208. package/dist/types/prompt/video.d.ts +18 -0
  209. package/dist/types/prompt/welcome.d.ts +83 -0
  210. package/dist/types/prompt/workflow.d.ts +13 -0
  211. package/dist/types/render/code-cell.d.ts +51 -0
  212. package/dist/types/render/file-list.d.ts +22 -0
  213. package/dist/types/render/hyperlink.d.ts +80 -0
  214. package/dist/types/render/index.d.ts +16 -0
  215. package/dist/types/render/output-block.d.ts +54 -0
  216. package/dist/types/render/output-pane.d.ts +80 -0
  217. package/dist/types/render/render-utils.d.ts +312 -0
  218. package/dist/types/render/sixel.d.ts +21 -0
  219. package/dist/types/render/status-line.d.ts +23 -0
  220. package/dist/types/render/tool-card.d.ts +71 -0
  221. package/dist/types/render/tree-list.d.ts +29 -0
  222. package/dist/types/render/types.d.ts +17 -0
  223. package/dist/types/render/utils.d.ts +43 -0
  224. package/dist/types/render/width-aware-text.d.ts +23 -0
  225. package/dist/types/setup/lazy.d.ts +3 -0
  226. package/dist/types/setup/scenes/composer.d.ts +3 -0
  227. package/dist/types/setup/scenes/glyph.d.ts +3 -0
  228. package/dist/types/setup/scenes/model.d.ts +3 -0
  229. package/dist/types/setup/scenes/outro.d.ts +2 -0
  230. package/dist/types/setup/scenes/providers.d.ts +3 -0
  231. package/dist/types/setup/scenes/sign-in.d.ts +21 -0
  232. package/dist/types/setup/scenes/splash.d.ts +11 -0
  233. package/dist/types/setup/scenes/theme.d.ts +3 -0
  234. package/dist/types/setup/scenes/types.d.ts +110 -0
  235. package/dist/types/setup/scenes/web-search.d.ts +22 -0
  236. package/dist/types/setup/setup-version.d.ts +11 -0
  237. package/dist/types/setup/startup-splash.d.ts +9 -0
  238. package/dist/types/setup/wizard-overlay.d.ts +16 -0
  239. package/dist/types/setup/wizard.d.ts +25 -0
  240. package/dist/types/status-line/component.d.ts +182 -0
  241. package/dist/types/status-line/context-usage.d.ts +156 -0
  242. package/dist/types/status-line/footer.d.ts +30 -0
  243. package/dist/types/status-line/git-utils.d.ts +22 -0
  244. package/dist/types/status-line/host.d.ts +129 -0
  245. package/dist/types/status-line/index.d.ts +6 -0
  246. package/dist/types/status-line/loop.d.ts +18 -0
  247. package/dist/types/status-line/metrics.d.ts +21 -0
  248. package/dist/types/status-line/presets.d.ts +3 -0
  249. package/dist/types/status-line/schema.d.ts +15 -0
  250. package/dist/types/status-line/segments.d.ts +5 -0
  251. package/dist/types/status-line/separators.d.ts +3 -0
  252. package/dist/types/status-line/types.d.ts +236 -0
  253. package/dist/types/stdin-buffer.d.ts +4 -0
  254. package/dist/types/theme/color.d.ts +24 -0
  255. package/dist/types/theme/defaults/index.d.ts +9489 -0
  256. package/dist/types/theme/index.d.ts +4 -0
  257. package/dist/types/theme/loader.d.ts +23 -0
  258. package/dist/types/theme/mermaid-cache.d.ts +26 -0
  259. package/dist/types/theme/schema-validation.d.ts +3 -0
  260. package/dist/types/theme/schema.d.ts +25 -0
  261. package/dist/types/theme/session-color.d.ts +43 -0
  262. package/dist/types/theme/shimmer.d.ts +44 -0
  263. package/dist/types/theme/symbols.d.ts +34 -0
  264. package/dist/types/theme/theme-class.d.ts +299 -0
  265. package/dist/types/theme/theme.d.ts +111 -0
  266. package/dist/types/theme/tui-adapters.d.ts +30 -0
  267. package/dist/types/thinking.d.ts +165 -0
  268. package/dist/types/tools/agent-tree.d.ts +36 -0
  269. package/dist/types/tools/ask.d.ts +67 -0
  270. package/dist/types/tools/ast-edit.d.ts +53 -0
  271. package/dist/types/tools/ast-grep.d.ts +52 -0
  272. package/dist/types/tools/autoresearch.d.ts +156 -0
  273. package/dist/types/tools/bash-interactive.d.ts +28 -0
  274. package/dist/types/tools/bash.d.ts +103 -0
  275. package/dist/types/tools/conflict-detect.d.ts +43 -0
  276. package/dist/types/tools/debug.d.ts +60 -0
  277. package/dist/types/tools/default-renderer.d.ts +24 -0
  278. package/dist/types/tools/edit.d.ts +158 -0
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
  280. package/dist/types/tools/eval-format/index.d.ts +5 -0
  281. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  282. package/dist/types/tools/eval-format/python.d.ts +2 -0
  283. package/dist/types/tools/eval.d.ts +94 -0
  284. package/dist/types/tools/fetch.d.ts +42 -0
  285. package/dist/types/tools/gh-format.d.ts +8 -0
  286. package/dist/types/tools/github.d.ts +144 -0
  287. package/dist/types/tools/glob.d.ts +44 -0
  288. package/dist/types/tools/goal.d.ts +42 -0
  289. package/dist/types/tools/grep.d.ts +64 -0
  290. package/dist/types/tools/grouped-file-output.d.ts +71 -0
  291. package/dist/types/tools/hashline-format.d.ts +28 -0
  292. package/dist/types/tools/hub.d.ts +340 -0
  293. package/dist/types/tools/index.d.ts +4 -0
  294. package/dist/types/tools/json-tree.d.ts +59 -0
  295. package/dist/types/tools/line-ranges.d.ts +29 -0
  296. package/dist/types/tools/list-limit.d.ts +15 -0
  297. package/dist/types/tools/lsp.d.ts +72 -0
  298. package/dist/types/tools/match-line-format.d.ts +11 -0
  299. package/dist/types/tools/mcp.d.ts +81 -0
  300. package/dist/types/tools/memory.d.ts +67 -0
  301. package/dist/types/tools/output-meta.d.ts +151 -0
  302. package/dist/types/tools/read.d.ts +139 -0
  303. package/dist/types/tools/renderer.d.ts +98 -0
  304. package/dist/types/tools/report-tool-issue.d.ts +8 -0
  305. package/dist/types/tools/resolve.d.ts +47 -0
  306. package/dist/types/tools/streaming-output.d.ts +282 -0
  307. package/dist/types/tools/subprocess.d.ts +24 -0
  308. package/dist/types/tools/task-repair-args.d.ts +53 -0
  309. package/dist/types/tools/task-yield-assembly.d.ts +20 -0
  310. package/dist/types/tools/task.d.ts +425 -0
  311. package/dist/types/tools/terminal-output.d.ts +8 -0
  312. package/dist/types/tools/think.d.ts +13 -0
  313. package/dist/types/tools/todo.d.ts +118 -0
  314. package/dist/types/tools/tool-errors.d.ts +7 -0
  315. package/dist/types/tools/vibe.d.ts +120 -0
  316. package/dist/types/tools/web-search.d.ts +195 -0
  317. package/dist/types/tools/write.d.ts +76 -0
  318. package/dist/types/tools/xd-url.d.ts +13 -0
  319. package/dist/types/tools/xdev.d.ts +52 -0
  320. package/dist/types/tui.d.ts +18 -0
  321. package/dist/types/utils.d.ts +2 -0
  322. package/package.json +37 -3
  323. package/src/app-keybindings.ts +760 -0
  324. package/src/apps/autoresearch-dashboard.ts +500 -0
  325. package/src/apps/autoresearch-data.ts +64 -0
  326. package/src/apps/cleanse-board.ts +341 -0
  327. package/src/apps/cleanse-picker.ts +57 -0
  328. package/src/apps/debug/log-formatting.ts +53 -0
  329. package/src/apps/debug/log-viewer.ts +868 -0
  330. package/src/apps/debug/protocol-probe.ts +267 -0
  331. package/src/apps/debug/raw-sse-buffer.ts +449 -0
  332. package/src/apps/debug/raw-sse.ts +282 -0
  333. package/src/apps/debug/terminal-info.ts +115 -0
  334. package/src/apps/debug/viewer-frame.ts +174 -0
  335. package/src/apps/git/avatar.ts +52 -0
  336. package/src/apps/git/colors.ts +109 -0
  337. package/src/apps/git/diff-pane.ts +1512 -0
  338. package/src/apps/git/git-tui.ts +889 -0
  339. package/src/apps/git/sidebar.ts +1204 -0
  340. package/src/apps/git/state.ts +113 -0
  341. package/src/apps/if-bench-board.ts +324 -0
  342. package/src/apps/live-visualizer.ts +238 -0
  343. package/src/apps/ps-data.ts +116 -0
  344. package/src/apps/ps-top.ts +422 -0
  345. package/src/apps/session-picker.ts +95 -0
  346. package/src/apps/setup-model-picker.ts +21 -0
  347. package/src/apps/standalone-picker.ts +111 -0
  348. package/src/autocomplete.ts +9 -13
  349. package/src/chat/advisor-message.ts +175 -0
  350. package/src/chat/assistant-message.ts +1214 -0
  351. package/src/chat/background-tan-message.ts +35 -0
  352. package/src/chat/bash-execution.ts +394 -0
  353. package/src/chat/cache-invalidation-marker.ts +94 -0
  354. package/src/chat/chat-transcript-builder.ts +593 -0
  355. package/src/chat/collab-prompt-message.ts +34 -0
  356. package/src/chat/compaction-summary-message.ts +261 -0
  357. package/src/chat/custom-message.ts +23 -0
  358. package/src/chat/display-preferences.ts +24 -0
  359. package/src/chat/eval-execution.ts +172 -0
  360. package/src/chat/execution-shared.ts +113 -0
  361. package/src/chat/extension-types.ts +46 -0
  362. package/src/chat/hook-message.ts +20 -0
  363. package/src/chat/image-loading.ts +122 -0
  364. package/src/chat/late-diagnostics-message.ts +92 -0
  365. package/src/chat/messages.ts +305 -0
  366. package/src/chat/reaction.ts +65 -0
  367. package/src/chat/read-target.ts +7 -0
  368. package/src/chat/read-tool-group.ts +873 -0
  369. package/src/chat/served-model-marker.ts +91 -0
  370. package/src/chat/skill-message.ts +220 -0
  371. package/src/chat/skill-title-input.ts +41 -0
  372. package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
  373. package/src/chat/thinking-display.ts +312 -0
  374. package/src/chat/todo-reminder.ts +33 -0
  375. package/src/chat/tool-execution.ts +1395 -0
  376. package/src/chat/transcript-browser.ts +268 -0
  377. package/src/chat/transcript-entry.ts +96 -0
  378. package/src/chat/transcript-outline.ts +260 -0
  379. package/src/chat/transcript-render-helpers.ts +315 -0
  380. package/src/chat/ttsr-notification.ts +123 -0
  381. package/src/chat/user-message.ts +287 -0
  382. package/src/chrome/chat-block.ts +110 -0
  383. package/src/chrome/collab-qrcode.ts +74 -0
  384. package/src/chrome/context-thresholds.ts +85 -0
  385. package/src/chrome/countdown-timer.ts +74 -0
  386. package/src/chrome/diff.ts +266 -0
  387. package/src/chrome/dynamic-border.ts +36 -0
  388. package/src/chrome/error-block.ts +63 -0
  389. package/src/chrome/form-theme.ts +10 -0
  390. package/src/chrome/format.ts +66 -0
  391. package/src/chrome/index.ts +21 -0
  392. package/src/chrome/keybinding-hints.ts +55 -0
  393. package/src/chrome/live-board.ts +123 -0
  394. package/src/chrome/local-date.ts +20 -0
  395. package/src/chrome/message-divider.ts +51 -0
  396. package/src/chrome/message-frame.ts +164 -0
  397. package/src/chrome/message-notice.ts +103 -0
  398. package/src/chrome/overlay-box.ts +301 -0
  399. package/src/chrome/qrcode.ts +539 -0
  400. package/src/chrome/segment-track.ts +86 -0
  401. package/src/chrome/select-list-mouse-routing.ts +11 -0
  402. package/src/chrome/selector-helpers.ts +119 -0
  403. package/src/chrome/shared.ts +27 -0
  404. package/src/chrome/streaming-panel.ts +72 -0
  405. package/src/chrome/tool-activity.ts +44 -0
  406. package/src/chrome/transcript-container.ts +902 -0
  407. package/src/chrome/transcript-status.ts +27 -0
  408. package/src/chrome/visual-truncate.ts +70 -0
  409. package/src/components/disclosure.ts +235 -0
  410. package/src/components/editor.ts +10 -12
  411. package/src/components/form.ts +566 -0
  412. package/src/components/image.ts +80 -53
  413. package/src/components/input.ts +6 -27
  414. package/src/components/key-value-list.ts +88 -0
  415. package/src/components/layout/geometry.ts +202 -0
  416. package/src/components/layout/row.ts +283 -0
  417. package/src/components/layout/split-pane.ts +349 -0
  418. package/src/components/layout/stack.ts +261 -0
  419. package/src/components/markdown.ts +16 -12
  420. package/src/components/menu-selection.ts +248 -0
  421. package/src/components/metric.ts +132 -0
  422. package/src/components/progress-bar.ts +140 -0
  423. package/src/components/scroll-view.ts +372 -70
  424. package/src/components/scroll-viewport.ts +154 -0
  425. package/src/components/section.ts +77 -0
  426. package/src/components/select-list.ts +238 -159
  427. package/src/components/settings-list.ts +103 -80
  428. package/src/components/table.ts +127 -0
  429. package/src/components/tree-view.ts +481 -0
  430. package/src/components/wizard-step.ts +229 -0
  431. package/src/deccara.ts +1 -3
  432. package/src/hotkeys-markdown.ts +100 -0
  433. package/src/index.ts +18 -0
  434. package/src/keybinding-matchers.ts +99 -0
  435. package/src/keys.ts +2 -2
  436. package/src/lang-from-path.ts +252 -0
  437. package/src/overlays/advisor-config.ts +761 -0
  438. package/src/overlays/agent-activity.ts +113 -0
  439. package/src/overlays/agent-hub-projection.ts +250 -0
  440. package/src/overlays/agent-hub-renderer.ts +281 -0
  441. package/src/overlays/agent-hub-types.ts +74 -0
  442. package/src/overlays/agent-hub.ts +1562 -0
  443. package/src/overlays/agent-transcript-viewer.ts +669 -0
  444. package/src/overlays/agents-hub.ts +1203 -0
  445. package/src/overlays/ask-dialog.ts +1202 -0
  446. package/src/overlays/bordered-loader.ts +41 -0
  447. package/src/overlays/btw-history-panel.ts +591 -0
  448. package/src/overlays/btw-history.ts +33 -0
  449. package/src/overlays/btw-panel.ts +167 -0
  450. package/src/overlays/cleanse-panel.ts +167 -0
  451. package/src/overlays/codex-reset-fireworks.ts +375 -0
  452. package/src/overlays/composer-shape-preview.ts +137 -0
  453. package/src/overlays/composer-shape-registry.ts +82 -0
  454. package/src/overlays/copy-selector.ts +618 -0
  455. package/src/overlays/copy-targets.ts +210 -0
  456. package/src/overlays/error-banner.ts +39 -0
  457. package/src/overlays/extensions/display-text.ts +39 -0
  458. package/src/overlays/extensions/extension-dashboard.ts +645 -0
  459. package/src/overlays/extensions/extension-list.ts +667 -0
  460. package/src/overlays/extensions/inspector-model.ts +484 -0
  461. package/src/overlays/extensions/inspector-panel.ts +669 -0
  462. package/src/overlays/extensions/live-tool-session.ts +67 -0
  463. package/src/overlays/extensions/mcp-runtime.ts +289 -0
  464. package/src/overlays/extensions/state-manager.ts +337 -0
  465. package/src/overlays/extensions/types.ts +208 -0
  466. package/src/overlays/history-search.ts +268 -0
  467. package/src/overlays/hook-editor.ts +272 -0
  468. package/src/overlays/hook-input.ts +84 -0
  469. package/src/overlays/hook-selector.ts +635 -0
  470. package/src/overlays/hub-frame.ts +262 -0
  471. package/src/overlays/login-dialog.ts +231 -0
  472. package/src/overlays/logout-account-selector.ts +115 -0
  473. package/src/overlays/mcp-add-wizard.ts +1436 -0
  474. package/src/overlays/model-browser.ts +1176 -0
  475. package/src/overlays/model-hub.ts +2279 -0
  476. package/src/overlays/model-picker.ts +285 -0
  477. package/src/overlays/model-selector.ts +107 -0
  478. package/src/overlays/move-overlay.ts +197 -0
  479. package/src/overlays/oauth-selector.ts +446 -0
  480. package/src/overlays/omfg-panel.ts +136 -0
  481. package/src/overlays/pause-screen.ts +198 -0
  482. package/src/overlays/plan-review-overlay.ts +1227 -0
  483. package/src/overlays/plan-save-overlay.ts +74 -0
  484. package/src/overlays/plan-toc.ts +139 -0
  485. package/src/overlays/plugin-selector.ts +97 -0
  486. package/src/overlays/plugin-settings.ts +839 -0
  487. package/src/overlays/queue-mode-selector.ts +55 -0
  488. package/src/overlays/reset-usage-selector.ts +170 -0
  489. package/src/overlays/rewind-selector.ts +499 -0
  490. package/src/overlays/running-subagent-badge.ts +19 -0
  491. package/src/overlays/session-account-selector.ts +68 -0
  492. package/src/overlays/session-info-overlay.ts +96 -0
  493. package/src/overlays/session-observer-registry.ts +289 -0
  494. package/src/overlays/session-selector.ts +1137 -0
  495. package/src/overlays/settings-defs.ts +315 -0
  496. package/src/overlays/settings-selector.ts +1415 -0
  497. package/src/overlays/show-images-selector.ts +44 -0
  498. package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
  499. package/src/overlays/snapcompact-shape-preview.ts +192 -0
  500. package/src/overlays/theme-selector.ts +62 -0
  501. package/src/overlays/thinking-selector.ts +51 -0
  502. package/src/overlays/tiny-title-download-progress.ts +102 -0
  503. package/src/overlays/tree-selector.ts +1121 -0
  504. package/src/overlays/usage-dashboard.ts +629 -0
  505. package/src/overlays/usage-display.ts +45 -0
  506. package/src/overlays/usage-row.ts +118 -0
  507. package/src/prompt/attachment-chips.ts +201 -0
  508. package/src/prompt/composer-attachments.ts +280 -0
  509. package/src/prompt/composer-cache.ts +304 -0
  510. package/src/prompt/composer.ts +934 -0
  511. package/src/prompt/custom-editor.ts +1392 -0
  512. package/src/prompt/data/emojis.json +1 -0
  513. package/src/prompt/editor-top-gap.ts +28 -0
  514. package/src/prompt/emoji-autocomplete.ts +285 -0
  515. package/src/prompt/github-ref-autocomplete.ts +75 -0
  516. package/src/prompt/gradient-highlight.ts +101 -0
  517. package/src/prompt/image-format.ts +28 -0
  518. package/src/prompt/image-references.ts +102 -0
  519. package/src/prompt/interactive-context-helpers.ts +126 -0
  520. package/src/prompt/internal-url-autocomplete.ts +155 -0
  521. package/src/prompt/macos-spelling.ts +456 -0
  522. package/src/prompt/magic-keyword-boundary.ts +23 -0
  523. package/src/prompt/magic-keywords.ts +42 -0
  524. package/src/prompt/markdown-prose.ts +245 -0
  525. package/src/prompt/model-mention-autocomplete.ts +94 -0
  526. package/src/prompt/model-mention-syntax.ts +41 -0
  527. package/src/prompt/orchestrate.ts +26 -0
  528. package/src/prompt/prompt-action-autocomplete.ts +337 -0
  529. package/src/prompt/queue-input.ts +132 -0
  530. package/src/prompt/skill-tokens.ts +36 -0
  531. package/src/prompt/tips.txt +27 -0
  532. package/src/prompt/tools-markdown.ts +29 -0
  533. package/src/prompt/ultrathink.ts +26 -0
  534. package/src/prompt/usage-amounts.ts +99 -0
  535. package/src/prompt/video.ts +47 -0
  536. package/src/prompt/welcome.ts +605 -0
  537. package/src/prompt/workflow.ts +26 -0
  538. package/src/render/code-cell.ts +244 -0
  539. package/src/render/file-list.ts +58 -0
  540. package/src/render/hyperlink.ts +192 -0
  541. package/src/render/index.ts +17 -0
  542. package/src/render/output-block.ts +267 -0
  543. package/src/render/output-pane.ts +299 -0
  544. package/src/render/render-utils.ts +1182 -0
  545. package/src/render/sixel.ts +69 -0
  546. package/src/render/status-line.ts +55 -0
  547. package/src/render/tool-card.ts +272 -0
  548. package/src/render/tree-list.ts +188 -0
  549. package/src/render/types.ts +19 -0
  550. package/src/render/utils.ts +109 -0
  551. package/src/render/width-aware-text.ts +60 -0
  552. package/src/setup/lazy.ts +17 -0
  553. package/src/setup/scenes/composer.ts +127 -0
  554. package/src/setup/scenes/glyph.ts +120 -0
  555. package/src/setup/scenes/model.ts +138 -0
  556. package/src/setup/scenes/outro.ts +24 -0
  557. package/src/setup/scenes/providers.ts +107 -0
  558. package/src/setup/scenes/sign-in.ts +373 -0
  559. package/src/setup/scenes/splash.ts +192 -0
  560. package/src/setup/scenes/theme.ts +343 -0
  561. package/src/setup/scenes/types.ts +111 -0
  562. package/src/setup/scenes/web-search.ts +165 -0
  563. package/src/setup/setup-version.ts +11 -0
  564. package/src/setup/startup-splash.ts +107 -0
  565. package/src/setup/wizard-overlay.ts +319 -0
  566. package/src/setup/wizard.ts +101 -0
  567. package/src/status-line/component.ts +3003 -0
  568. package/src/status-line/context-usage.ts +688 -0
  569. package/src/status-line/footer.ts +323 -0
  570. package/src/status-line/git-utils.ts +42 -0
  571. package/src/status-line/host.ts +97 -0
  572. package/src/status-line/index.ts +6 -0
  573. package/src/status-line/loop.ts +28 -0
  574. package/src/status-line/metrics.ts +94 -0
  575. package/src/status-line/presets.ts +107 -0
  576. package/src/status-line/schema.ts +57 -0
  577. package/src/status-line/segments.ts +934 -0
  578. package/src/status-line/separators.ts +55 -0
  579. package/src/status-line/types.ts +227 -0
  580. package/src/stdin-buffer.ts +4 -2
  581. package/src/theme/color.ts +95 -0
  582. package/src/theme/dark.json +95 -0
  583. package/src/theme/defaults/alabaster.json +93 -0
  584. package/src/theme/defaults/amethyst.json +96 -0
  585. package/src/theme/defaults/anthracite.json +93 -0
  586. package/src/theme/defaults/basalt.json +91 -0
  587. package/src/theme/defaults/birch.json +95 -0
  588. package/src/theme/defaults/dark-abyss.json +91 -0
  589. package/src/theme/defaults/dark-arctic.json +104 -0
  590. package/src/theme/defaults/dark-aurora.json +95 -0
  591. package/src/theme/defaults/dark-catppuccin.json +107 -0
  592. package/src/theme/defaults/dark-cavern.json +91 -0
  593. package/src/theme/defaults/dark-celestial.json +106 -0
  594. package/src/theme/defaults/dark-copper.json +95 -0
  595. package/src/theme/defaults/dark-cosmos.json +90 -0
  596. package/src/theme/defaults/dark-cyberpunk.json +102 -0
  597. package/src/theme/defaults/dark-dracula.json +98 -0
  598. package/src/theme/defaults/dark-eclipse.json +91 -0
  599. package/src/theme/defaults/dark-ember.json +95 -0
  600. package/src/theme/defaults/dark-equinox.json +90 -0
  601. package/src/theme/defaults/dark-forest.json +96 -0
  602. package/src/theme/defaults/dark-github.json +105 -0
  603. package/src/theme/defaults/dark-gruvbox.json +112 -0
  604. package/src/theme/defaults/dark-lavender.json +95 -0
  605. package/src/theme/defaults/dark-lunar.json +89 -0
  606. package/src/theme/defaults/dark-midnight.json +95 -0
  607. package/src/theme/defaults/dark-monochrome.json +94 -0
  608. package/src/theme/defaults/dark-monokai.json +98 -0
  609. package/src/theme/defaults/dark-nebula.json +90 -0
  610. package/src/theme/defaults/dark-nord.json +98 -0
  611. package/src/theme/defaults/dark-ocean.json +101 -0
  612. package/src/theme/defaults/dark-one.json +100 -0
  613. package/src/theme/defaults/dark-poimandres.json +115 -0
  614. package/src/theme/defaults/dark-rainforest.json +91 -0
  615. package/src/theme/defaults/dark-reef.json +91 -0
  616. package/src/theme/defaults/dark-retro.json +92 -0
  617. package/src/theme/defaults/dark-rose-pine.json +96 -0
  618. package/src/theme/defaults/dark-sakura.json +95 -0
  619. package/src/theme/defaults/dark-slate.json +95 -0
  620. package/src/theme/defaults/dark-solarized.json +97 -0
  621. package/src/theme/defaults/dark-solstice.json +90 -0
  622. package/src/theme/defaults/dark-starfall.json +91 -0
  623. package/src/theme/defaults/dark-sunset.json +99 -0
  624. package/src/theme/defaults/dark-swamp.json +90 -0
  625. package/src/theme/defaults/dark-synthwave.json +103 -0
  626. package/src/theme/defaults/dark-taiga.json +91 -0
  627. package/src/theme/defaults/dark-terminal.json +95 -0
  628. package/src/theme/defaults/dark-tokyo-night.json +101 -0
  629. package/src/theme/defaults/dark-tundra.json +91 -0
  630. package/src/theme/defaults/dark-twilight.json +91 -0
  631. package/src/theme/defaults/dark-volcanic.json +91 -0
  632. package/src/theme/defaults/graphite.json +92 -0
  633. package/src/theme/defaults/index.ts +201 -0
  634. package/src/theme/defaults/light-arctic.json +107 -0
  635. package/src/theme/defaults/light-aurora-day.json +91 -0
  636. package/src/theme/defaults/light-canyon.json +91 -0
  637. package/src/theme/defaults/light-catppuccin.json +106 -0
  638. package/src/theme/defaults/light-cirrus.json +90 -0
  639. package/src/theme/defaults/light-coral.json +95 -0
  640. package/src/theme/defaults/light-cyberpunk.json +96 -0
  641. package/src/theme/defaults/light-dawn.json +90 -0
  642. package/src/theme/defaults/light-dunes.json +91 -0
  643. package/src/theme/defaults/light-eucalyptus.json +95 -0
  644. package/src/theme/defaults/light-forest.json +100 -0
  645. package/src/theme/defaults/light-frost.json +95 -0
  646. package/src/theme/defaults/light-github.json +115 -0
  647. package/src/theme/defaults/light-glacier.json +91 -0
  648. package/src/theme/defaults/light-gruvbox.json +108 -0
  649. package/src/theme/defaults/light-haze.json +90 -0
  650. package/src/theme/defaults/light-honeycomb.json +95 -0
  651. package/src/theme/defaults/light-lagoon.json +91 -0
  652. package/src/theme/defaults/light-lavender.json +95 -0
  653. package/src/theme/defaults/light-meadow.json +91 -0
  654. package/src/theme/defaults/light-mint.json +95 -0
  655. package/src/theme/defaults/light-monochrome.json +101 -0
  656. package/src/theme/defaults/light-ocean.json +99 -0
  657. package/src/theme/defaults/light-one.json +99 -0
  658. package/src/theme/defaults/light-opal.json +91 -0
  659. package/src/theme/defaults/light-orchard.json +91 -0
  660. package/src/theme/defaults/light-paper.json +95 -0
  661. package/src/theme/defaults/light-poimandres.json +115 -0
  662. package/src/theme/defaults/light-prism.json +90 -0
  663. package/src/theme/defaults/light-retro.json +98 -0
  664. package/src/theme/defaults/light-sand.json +95 -0
  665. package/src/theme/defaults/light-savanna.json +91 -0
  666. package/src/theme/defaults/light-solarized.json +102 -0
  667. package/src/theme/defaults/light-soleil.json +90 -0
  668. package/src/theme/defaults/light-sunset.json +99 -0
  669. package/src/theme/defaults/light-synthwave.json +98 -0
  670. package/src/theme/defaults/light-tokyo-night.json +111 -0
  671. package/src/theme/defaults/light-wetland.json +91 -0
  672. package/src/theme/defaults/light-zenith.json +89 -0
  673. package/src/theme/defaults/limestone.json +94 -0
  674. package/src/theme/defaults/mahogany.json +97 -0
  675. package/src/theme/defaults/marble.json +93 -0
  676. package/src/theme/defaults/obsidian.json +91 -0
  677. package/src/theme/defaults/onyx.json +91 -0
  678. package/src/theme/defaults/pearl.json +93 -0
  679. package/src/theme/defaults/porcelain.json +91 -0
  680. package/src/theme/defaults/quartz.json +96 -0
  681. package/src/theme/defaults/sandstone.json +95 -0
  682. package/src/theme/defaults/titanium.json +90 -0
  683. package/src/theme/index.ts +5 -0
  684. package/src/theme/light.json +93 -0
  685. package/src/theme/loader.ts +200 -0
  686. package/src/theme/mermaid-cache.ts +92 -0
  687. package/src/theme/schema-validation.ts +119 -0
  688. package/src/theme/schema.ts +168 -0
  689. package/src/theme/session-color.ts +253 -0
  690. package/src/theme/shimmer.ts +307 -0
  691. package/src/theme/symbols.ts +1433 -0
  692. package/src/theme/theme-class.ts +787 -0
  693. package/src/theme/theme-schema.json +463 -0
  694. package/src/theme/theme.ts +810 -0
  695. package/src/theme/tui-adapters.ts +339 -0
  696. package/src/thinking.ts +380 -0
  697. package/src/tools/agent-tree.ts +128 -0
  698. package/src/tools/ask.ts +482 -0
  699. package/src/tools/ast-edit.ts +210 -0
  700. package/src/tools/ast-grep.ts +171 -0
  701. package/src/tools/autoresearch.ts +287 -0
  702. package/src/tools/bash-interactive.ts +295 -0
  703. package/src/tools/bash.ts +468 -0
  704. package/src/tools/conflict-detect.ts +77 -0
  705. package/src/tools/debug.ts +167 -0
  706. package/src/tools/default-renderer.ts +172 -0
  707. package/src/tools/edit.ts +1258 -0
  708. package/src/tools/eval-format/code-mode-declarations.ts +61 -0
  709. package/src/tools/eval-format/index.ts +16 -0
  710. package/src/tools/eval-format/javascript.ts +952 -0
  711. package/src/tools/eval-format/python.ts +544 -0
  712. package/src/tools/eval.ts +835 -0
  713. package/src/tools/fetch.ts +226 -0
  714. package/src/tools/gh-format.ts +18 -0
  715. package/src/tools/github.ts +686 -0
  716. package/src/tools/glob.ts +213 -0
  717. package/src/tools/goal.ts +153 -0
  718. package/src/tools/grep.ts +385 -0
  719. package/src/tools/grouped-file-output.ts +211 -0
  720. package/src/tools/hashline-format.ts +59 -0
  721. package/src/tools/hub.ts +1488 -0
  722. package/src/tools/index.ts +71 -0
  723. package/src/tools/json-tree.ts +388 -0
  724. package/src/tools/line-ranges.ts +98 -0
  725. package/src/tools/list-limit.ts +43 -0
  726. package/src/tools/lsp.ts +702 -0
  727. package/src/tools/match-line-format.ts +20 -0
  728. package/src/tools/mcp.ts +309 -0
  729. package/src/tools/memory.ts +220 -0
  730. package/src/tools/output-meta.ts +367 -0
  731. package/src/tools/read.ts +628 -0
  732. package/src/tools/renderer.ts +107 -0
  733. package/src/tools/report-tool-issue.ts +13 -0
  734. package/src/tools/resolve.ts +139 -0
  735. package/src/tools/streaming-output.ts +1576 -0
  736. package/src/tools/subprocess.ts +39 -0
  737. package/src/tools/task-repair-args.ts +118 -0
  738. package/src/tools/task-yield-assembly.ts +147 -0
  739. package/src/tools/task.ts +2110 -0
  740. package/src/tools/terminal-output.ts +153 -0
  741. package/src/tools/think.ts +28 -0
  742. package/src/tools/todo.ts +580 -0
  743. package/src/tools/tool-errors.ts +15 -0
  744. package/src/tools/vibe.ts +420 -0
  745. package/src/tools/web-search.ts +409 -0
  746. package/src/tools/write.ts +511 -0
  747. package/src/tools/xd-url.ts +28 -0
  748. package/src/tools/xdev.ts +201 -0
  749. package/src/tui.ts +70 -8
  750. package/src/utils.ts +8 -0
@@ -0,0 +1,1562 @@
1
+ /**
2
+ * Agent Hub overlay component.
3
+ *
4
+ * One overlay, two views:
5
+ * - Table view: every registered agent except Main (Main IS the ambient
6
+ * chat), live from the global AgentHubRegistry — status, unread irc count,
7
+ * current/last task, last activity. Navigate with keys, wheel, hover, and
8
+ * click; `r` revives a parked agent, `x` aborts + releases one.
9
+ * - Chat view: per-agent transcript (incremental session-file tail, absorbed
10
+ * from the old session observer overlay) plus an input line. Submitting
11
+ * revives a parked agent, then prompts/steers it; the message lands in the
12
+ * agent's persisted history via the normal prompt path.
13
+ *
14
+ * Replaces the old SessionObserverOverlayComponent (ctrl+s observer).
15
+ */
16
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
17
+ import { Container, type OverlayHandle, type TUI } from "../tui";
18
+ import { matchesKey } from "../keys";
19
+ import { routeSelectListMouse, routeSgrMouseInput, type SelectListMouseTarget } from "../mouse";
20
+ import { padding, visibleWidth, wrapTextWithAnsi } from "../utils";
21
+ import { formatAge, formatNumber, getProjectDir, logger } from "@oh-my-pi/pi-utils";
22
+ import {
23
+ type AgentActivitySource,
24
+ type AgentActivityKind,
25
+ type AgentActivityRow,
26
+ activityRowsFromProgress,
27
+ } from "./agent-activity";
28
+ import type { KeyId } from "../app-keybindings";
29
+ import type { MessageRenderer } from "../chat/extension-types";
30
+ import type { AgentLifecycleLike, IrcBusLike } from "./agent-hub-types";
31
+ import { type AgentRecordLike, type AgentHubRegistry, type AgentStatus, MAIN_AGENT_ID } from "./agent-hub-types";
32
+ import { USER_INTERRUPT_LABEL } from "../chat/messages";
33
+ import { shortenPath, truncateToWidth } from "../render/render-utils";
34
+ import { formatLocalDateTimeWithOffset } from "../chrome/local-date";
35
+ import type { ObservableSession, SessionObserverRegistry } from "./session-observer-registry";
36
+ import { theme } from "../theme/theme";
37
+ import { matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
38
+ import {
39
+ type AgentMetrics,
40
+ type AggregateMetrics,
41
+ aggregateMetrics,
42
+ progressMetrics,
43
+ projectAgentTree,
44
+ STATUS_ORDER,
45
+ } from "./agent-hub-projection";
46
+ import {
47
+ clampHubLine,
48
+ contextGauge,
49
+ formatChildIds,
50
+ formatCost,
51
+ formatMetricColumns,
52
+ formatMetricDuration,
53
+ formatMetrics,
54
+ formatRoleBadge,
55
+ modelBadge,
56
+ type RosterRender,
57
+ sanitizeLine,
58
+ statusGlyph,
59
+ statusText,
60
+ treeBranch,
61
+ treeContinuation,
62
+ treeMetadataIndent,
63
+ } from "./agent-hub-renderer";
64
+ import { sanitizeDisplaySingleLine } from "./extensions/display-text";
65
+ import { AgentTranscriptViewer, type AgentTranscriptSource } from "./agent-transcript-viewer";
66
+ import type { AgentRoleDisplay } from "./agent-hub-renderer";
67
+ import { fuzzyMatch } from "../fuzzy";
68
+ import { bottomBorder, divider, dividerSplit, PanelRows, row, topBorder, topBorderSplit } from "../chrome/overlay-box";
69
+ import { SplitPane } from "../components/layout/split-pane";
70
+ import { Stack } from "../components/layout/stack";
71
+
72
+ /** Two-pane mode needs a useful roster and a readable inspector. */
73
+ const SPLIT_MIN_WIDTH = 96;
74
+ const DETAIL_MIN_WIDTH = 34;
75
+ const ROSTER_MIN_WIDTH = 48;
76
+
77
+ export type AgentHubSection = "agents" | "activity";
78
+ type ActivityFilter = "all" | "errors" | "responses" | "tools";
79
+ type ActivityScope = "all" | "agent" | "subtree";
80
+
81
+ type HubViewMode = "roster" | "tree";
82
+
83
+ /** Refresh cadence for the relative-time column. */
84
+ const AGE_TICK_MS = 5_000;
85
+ const DATA_CHANGE_RENDER_COALESCE_MS = 100;
86
+ /** Double-tap window for the table's left-left "close hub" gesture. */
87
+ const LEFT_TAP_WINDOW_MS = 500;
88
+
89
+ function activityGlyph(row: AgentActivityRow): string {
90
+ if (row.status === "error") return theme.fg("error", theme.status.error);
91
+ if (row.status === "aborted") return theme.fg("warning", theme.status.aborted);
92
+ if (row.status === "pending") return theme.fg("accent", theme.status.running);
93
+ switch (row.kind) {
94
+ case "response":
95
+ return theme.fg("success", "◆");
96
+ case "tool":
97
+ return theme.fg("success", theme.status.success);
98
+ case "irc":
99
+ return theme.fg("accent", "→");
100
+ case "lifecycle":
101
+ return theme.fg("muted", "○");
102
+ }
103
+ }
104
+
105
+ function activityClock(timestamp: number): string {
106
+ return new Date(timestamp).toLocaleTimeString(undefined, {
107
+ hour: "2-digit",
108
+ minute: "2-digit",
109
+ second: "2-digit",
110
+ hour12: false,
111
+ });
112
+ }
113
+ /** Result of one host-backed transcript read for the Agent Hub viewer. */
114
+ export interface AgentHubRemoteTranscript {
115
+ text: string;
116
+ newSize: number;
117
+ /** Terminal read failure reported by the host; guests should surface it instead of retrying hot. */
118
+ error?: string;
119
+ }
120
+
121
+ /** Guest-side proxy for hub actions executed on the collab host. */
122
+ export interface AgentHubRemote {
123
+ chat(id: string, text: string): void;
124
+ kill(id: string): void;
125
+ revive(id: string): void;
126
+ /** Mirrors readFileIncremental: text from fromByte (complete JSONL lines), newSize = next fromByte base; null = temporarily unavailable. */
127
+ readTranscript(id: string, fromByte: number): Promise<AgentHubRemoteTranscript | null>;
128
+ }
129
+
130
+ export interface AgentHubDeps<TRecord extends AgentRecordLike = AgentRecordLike> {
131
+ /** Progress/status snapshot source (task lifecycle + progress channels). */
132
+ observers: SessionObserverRegistry;
133
+ /** Resolve the current display metadata for a model role. */
134
+ getRoleInfo?: (role: string) => AgentRoleDisplay;
135
+ /** Host-backed transcript parsing and local reads. */
136
+ transcript: AgentTranscriptSource;
137
+ /** Register persisted roster entries while the overlay remains alive. */
138
+ loadPersisted: (shouldContinue: () => boolean) => Promise<void>;
139
+ /** Keys that toggle the hub closed from inside (app.agents.hub + app.session.observe). */
140
+ hubKeys: KeyId[];
141
+ onDone: () => void;
142
+ requestRender: () => void;
143
+ /** Registry supplying the roster. */
144
+ registry: AgentHubRegistry<TRecord>;
145
+ /** Resolve lifecycle actions lazily when a local action needs them. */
146
+ lifecycle: () => AgentLifecycleLike<TRecord>;
147
+ /** Host message bus supplying unread counts. */
148
+ irc: IrcBusLike;
149
+ /** TUI handle for transcript components; tests omit it and get a render-only stub. */
150
+ ui?: TUI;
151
+ /** Tool lookup for transcript renderers (labels, custom render functions). */
152
+ getTool?: (name: string) => AgentTool | undefined;
153
+ /** Whether the active registry entry came from a built-in factory. */
154
+ isBuiltInTool?: (name: string) => boolean;
155
+ /** Extension message renderers for custom messages in the transcript. */
156
+ getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
157
+ /** Cwd used by tool renderers for path shortening; defaults to the project dir. */
158
+ cwd?: string;
159
+ /** Mirrors the main transcript's thinking-block visibility. */
160
+ hideThinkingBlock?: () => boolean;
161
+ proseOnlyThinking?: () => boolean;
162
+ /** Keys toggling tool output expansion (app.tools.expand). */
163
+ expandKeys?: KeyId[];
164
+ /** Focus the main view on this agent's live session (ctx.focusAgentSession). When absent (collab guest, tests), Enter opens the in-hub chat view instead. */
165
+ focusAgent?: (id: string) => Promise<void>;
166
+ /** Current main session file; used to seed parked historical subagents after restart. */
167
+ sessionFile?: string | null;
168
+ /** Initial top-level projection; slash commands deep-link into this surface. */
169
+ initialSection?: AgentHubSection;
170
+ /** Unified local or remote activity source. */
171
+ activity: AgentActivitySource;
172
+ /** Whether observer progress should update live activity rows. */
173
+ manageActivityLive?: boolean;
174
+
175
+ /** Collab guest: route actions/transcripts to the host instead of local sessions. */
176
+ remote?: AgentHubRemote;
177
+ }
178
+
179
+ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRecordLike>
180
+ extends Container
181
+ implements SelectListMouseTarget
182
+ {
183
+ #registry: AgentHubRegistry<TRecord>;
184
+ #observers: SessionObserverRegistry;
185
+ #getRoleInfo: ((role: string) => AgentRoleDisplay) | undefined;
186
+ #transcript: AgentTranscriptSource;
187
+ #irc: IrcBusLike;
188
+ #lifecycle: () => AgentLifecycleLike<TRecord>;
189
+ #onDone: () => void;
190
+ #requestRender: () => void;
191
+ #hubKeys: KeyId[];
192
+ #unsubscribers: Array<() => void> = [];
193
+ #ageTimer: NodeJS.Timeout | undefined;
194
+ #dataChangeTimer?: NodeJS.Timeout;
195
+ #remote: AgentHubRemote | undefined;
196
+ #disposed = false;
197
+ /** Resolves after persisted historical subagents have been registered and rows refreshed. */
198
+ readonly persistedSubagentsReady: Promise<void>;
199
+ /** Prevent the async persisted-session scan from flashing a false empty state. */
200
+ #loadingPersistedSubagents = false;
201
+ #section: AgentHubSection;
202
+ #activity: AgentActivitySource;
203
+ #manageActivityLive: boolean;
204
+ #activityRows: AgentActivityRow[] = [];
205
+ #selectedActivityRow = 0;
206
+ #activityFilter: ActivityFilter = "all";
207
+ #activityScope: ActivityScope = "all";
208
+ #activitySearch = "";
209
+ #activitySearchEditing = false;
210
+ #activityFollow = true;
211
+ #activitySyncGeneration = 0;
212
+ #activitySyncStamp = new Map<string, string>();
213
+
214
+ // Table state
215
+ #rows: TRecord[] = [];
216
+ #statusCounts: Record<AgentStatus, number> = { running: 0, idle: 0, parked: 0, aborted: 0 };
217
+ #selectedRow = 0;
218
+ /** Stable roster order captured on first refresh: keyboard navigation must
219
+ * not jump as agents heartbeat. Existing agent generations keep their rank
220
+ * while the hub is open; newly appearing generations append at the end. */
221
+ #rowOrder: Map<TRecord, number> | undefined;
222
+ #nextRowOrder = 0;
223
+ #hoveredRow: number | null = null;
224
+ /** Per-render screen-line to agent-row map, shared by click and hover routing. */
225
+ #hitRows: Array<number | undefined> = [];
226
+ #notice: string | undefined;
227
+ /** Double-tap window state for the table's left-left "close hub" gesture. */
228
+ #lastLeftTap = 0;
229
+ /** Operational ordering by default; tree mode groups descendants under their spawner. */
230
+ #viewMode: HubViewMode = "roster";
231
+ #treeDepthById = new Map<string, number>();
232
+ #treeParentById = new Map<string, string>();
233
+ #treeLastSiblingById = new Map<string, boolean>();
234
+ #treeMaxDepth = 0;
235
+ /** Fuzzy agent filter (`/`), applied to id and display name. */
236
+ #agentFilter = "";
237
+ #agentFilterEditing = false;
238
+ /** Current observer index and summary data, rebuilt on source changes rather than every paint. */
239
+ #observedById = new Map<string, ObservableSession>();
240
+ #aggregate: AggregateMetrics = {
241
+ tokens: 0,
242
+ requests: 0,
243
+ tools: 0,
244
+ cost: 0,
245
+ durationMs: 0,
246
+ durationKind: "active",
247
+ reportedAgents: 0,
248
+ activeDurationAgents: 0,
249
+ };
250
+ #childrenByParent = new Map<string, TRecord[]>();
251
+ /** Transcript-derived fallback stats are sampled only on the bounded age cadence. */
252
+ #sessionMetrics = new WeakMap<object, { metrics: AgentMetrics | undefined }>();
253
+ /** Avoid a cadence-time row scan for the common persisted-only roster. */
254
+ #hasFallbackLiveSessions = false;
255
+ /** On narrow terminals Tab replaces the roster with the selected-agent inspector. */
256
+ #narrowDetailsOpen = false;
257
+ /** Pane-local roster hits from the last body render, translated to frame lines after layout. */
258
+ #paneHitRows: Array<number | undefined> = [];
259
+ #contentRowsLast = 1;
260
+ #renderRosterPane = (width: number, height: number | undefined): readonly string[] => {
261
+ const rows = Math.max(1, Math.floor(height ?? this.#contentRowsLast));
262
+ const roster = this.#renderRosterPanel(width, rows, this.#observedById);
263
+ this.#paneHitRows = roster.hitRows;
264
+ return roster.lines;
265
+ };
266
+ #renderDetailPane = (width: number, height: number | undefined): readonly string[] => {
267
+ const rows = Math.max(1, Math.floor(height ?? this.#contentRowsLast));
268
+ return this.#renderDetailPanel(this.#rows[this.#selectedRow], width, rows, this.#observedById);
269
+ };
270
+ readonly #split = new SplitPane({
271
+ left: this.#renderRosterPane,
272
+ right: this.#renderDetailPane,
273
+ leftSize: { ratio: 0.58, min: ROSTER_MIN_WIDTH },
274
+ rightMinWidth: DETAIL_MIN_WIDTH,
275
+ splitAt: SPLIT_MIN_WIDTH,
276
+ narrowPane: "left",
277
+ prefix: () => `${theme.fg("border", theme.boxRound.vertical)} `,
278
+ divider: () => ` ${theme.fg("border", theme.boxRound.vertical)} `,
279
+ suffix: () => ` ${theme.fg("border", theme.boxRound.vertical)}`,
280
+ });
281
+ readonly #frameTop = new PanelRows();
282
+ readonly #frameDivider = new PanelRows();
283
+ readonly #frameFooter = new PanelRows();
284
+ readonly #frameBottom = new PanelRows();
285
+ readonly #frame = new Stack({
286
+ children: [
287
+ { content: this.#frameTop, height: 1 },
288
+ { content: this.#split, grow: 1 },
289
+ { content: this.#frameDivider, height: 1 },
290
+ { content: this.#frameFooter, height: 1 },
291
+ { content: this.#frameBottom, height: 1 },
292
+ ],
293
+ });
294
+ /** Scroll offset for the selected-agent inspector when its content overflows. */
295
+ #detailScrollOffset = 0;
296
+ #detailAgentId: string | undefined;
297
+
298
+ // Transcript-viewer launch deps (passed through to AgentTranscriptViewer).
299
+ #ui: TUI;
300
+ #getTool: ((name: string) => AgentTool | undefined) | undefined;
301
+ #isBuiltInTool: ((name: string) => boolean) | undefined;
302
+ #getMessageRenderer: ((customType: string) => MessageRenderer | undefined) | undefined;
303
+ #cwd: string;
304
+ #hideThinkingBlock: (() => boolean) | undefined;
305
+ #proseOnlyThinking: (() => boolean) | undefined;
306
+ #expandKeys: KeyId[];
307
+ #focusAgent: ((id: string) => Promise<void>) | undefined;
308
+
309
+ // Fullscreen transcript overlay opened by openChat(), if any.
310
+ #transcriptOverlay: OverlayHandle | undefined;
311
+ #transcriptViewer: AgentTranscriptViewer | undefined;
312
+
313
+ constructor(deps: AgentHubDeps<TRecord>) {
314
+ super();
315
+ this.#section = deps.initialSection ?? "agents";
316
+ this.#activity = deps.activity;
317
+ this.#manageActivityLive = deps.manageActivityLive ?? true;
318
+ this.#registry = deps.registry;
319
+ this.#observers = deps.observers;
320
+ this.#getRoleInfo = deps.getRoleInfo;
321
+ this.#transcript = deps.transcript;
322
+ this.#irc = deps.irc;
323
+ this.#lifecycle = deps.lifecycle;
324
+ this.#onDone = deps.onDone;
325
+ this.#requestRender = deps.requestRender;
326
+ this.#hubKeys = deps.hubKeys;
327
+ this.#remote = deps.remote;
328
+ this.#loadingPersistedSubagents = !this.#remote && Boolean(deps.sessionFile?.endsWith(".jsonl"));
329
+ this.#ui =
330
+ deps.ui ??
331
+ ({
332
+ requestRender: () => deps.requestRender(),
333
+ requestComponentRender: () => deps.requestRender(),
334
+ } as unknown as TUI);
335
+ this.#getTool = deps.getTool;
336
+ this.#isBuiltInTool = deps.isBuiltInTool;
337
+ this.#getMessageRenderer = deps.getMessageRenderer;
338
+ this.#cwd = deps.cwd ?? getProjectDir();
339
+ this.#hideThinkingBlock = deps.hideThinkingBlock;
340
+ this.#proseOnlyThinking = deps.proseOnlyThinking;
341
+ this.#expandKeys = deps.expandKeys ?? ["ctrl+o"];
342
+ this.#focusAgent = deps.focusAgent;
343
+
344
+ this.#unsubscribers.push(this.#registry.onChange(() => this.#scheduleDataChange()));
345
+ this.#unsubscribers.push(this.#observers.onChange(() => this.#scheduleDataChange()));
346
+ this.#ageTimer = setInterval(() => {
347
+ if (this.#hasFallbackLiveSessions) {
348
+ this.#refreshAggregate(true);
349
+ }
350
+ this.#requestRender();
351
+ }, AGE_TICK_MS);
352
+ this.#ageTimer.unref?.();
353
+
354
+ this.persistedSubagentsReady = this.#remote
355
+ ? Promise.resolve()
356
+ : deps
357
+ .loadPersisted(() => !this.#disposed)
358
+ .catch((error: unknown) => {
359
+ logger.warn("Failed to register persisted subagents", { error });
360
+ })
361
+ .finally(() => {
362
+ // Clear the loading flag first so this refresh captures the
363
+ // full status/recency ranking rather than a partial roster.
364
+ this.#loadingPersistedSubagents = false;
365
+ if (!this.#disposed) {
366
+ this.#refreshRows();
367
+ this.#requestRender();
368
+ }
369
+ });
370
+ this.#refreshRows();
371
+ }
372
+
373
+ /**
374
+ * Whether the current table view has no agents to show (every registered agent
375
+ * except Main). Persisted historical rows may arrive later; callers that need
376
+ * those included must wait for {@link persistedSubagentsReady} first.
377
+ */
378
+ get isEmpty(): boolean {
379
+ return this.#rows.length === 0;
380
+ }
381
+
382
+ /** Tear down every subscription and timer. Called by the overlay owner on close. */
383
+ override dispose(): void {
384
+ if (this.#disposed) return;
385
+ this.#disposed = true;
386
+ for (const unsubscribe of this.#unsubscribers.splice(0)) unsubscribe();
387
+ if (this.#ageTimer) {
388
+ clearInterval(this.#ageTimer);
389
+ this.#ageTimer = undefined;
390
+ }
391
+ if (this.#dataChangeTimer) {
392
+ clearTimeout(this.#dataChangeTimer);
393
+ this.#dataChangeTimer = undefined;
394
+ }
395
+ this.#closeTranscriptOverlay();
396
+ }
397
+
398
+ override render(width: number): readonly string[] {
399
+ const termHeight = this.#ui.terminal?.rows || process.stdout.rows || 40;
400
+ const frame = (
401
+ this.#section === "activity"
402
+ ? this.#renderActivityTable(width, termHeight)
403
+ : this.#renderTable(width, termHeight)
404
+ ).map(line => clampHubLine(line, width));
405
+ if (frame.length <= termHeight) return frame;
406
+
407
+ // A tiny terminal can leave less room than the fixed chrome needs. Keep
408
+ // the title and footer visible instead of spilling into scrollback.
409
+ const footerLines = Math.min(3, frame.length);
410
+ const bodyEnd = Math.max(0, termHeight - footerLines);
411
+ return [...frame.slice(0, bodyEnd), ...frame.slice(-footerLines)].slice(0, termHeight);
412
+ }
413
+
414
+ handleInput(keyData: string): void {
415
+ if (
416
+ routeSgrMouseInput(keyData, event => {
417
+ if (event.wheel === null && this.#section === "agents" && this.#split.mode === "split") {
418
+ const frameHit = this.#frame.locate(event.row, event.col);
419
+ const pane = frameHit?.index === 1 ? this.#split.locate(frameHit.line, frameHit.col) : undefined;
420
+ if (frameHit?.index === 1 && pane?.pane !== "left") return false;
421
+ }
422
+ return routeSelectListMouse(this, event, event.row);
423
+ })
424
+ ) {
425
+ return;
426
+ }
427
+
428
+ // The hub/observe keys always close the overlay (toggle semantics)
429
+ for (const key of this.#hubKeys) {
430
+ if (matchesKey(keyData, key)) {
431
+ this.#onDone();
432
+ return;
433
+ }
434
+ }
435
+ if (this.#section === "activity" && this.#activitySearchEditing) {
436
+ this.#handleActivitySearchInput(keyData);
437
+ return;
438
+ }
439
+ if (keyData === "1") {
440
+ this.#switchSection("agents");
441
+ return;
442
+ }
443
+ if (keyData === "2") {
444
+ this.#switchSection("activity");
445
+ return;
446
+ }
447
+ if (this.#section === "activity") this.#handleActivityInput(keyData);
448
+ else this.#handleTableInput(keyData);
449
+ }
450
+
451
+ /**
452
+ * Seed the table's left-left close detector with the current time so a single
453
+ * subsequent `←` (within {@link LEFT_TAP_WINDOW_MS}) dismisses the hub.
454
+ *
455
+ * The editor's own double-tap detector consumes the `←←` that opens the hub,
456
+ * leaving this detector at its fresh `0` — without this handoff the user would
457
+ * have to press `←←` a second time to escape. Called by the opener when the hub
458
+ * was raised by that gesture.
459
+ */
460
+ armCloseTap(): void {
461
+ this.#lastLeftTap = Date.now();
462
+ }
463
+
464
+ /**
465
+ * Open the fullscreen transcript viewer for an agent id (public for table Enter
466
+ * and tests). Mounts {@link AgentTranscriptViewer} as a `fullscreen` overlay so it
467
+ * owns the alternate screen; the hub table stays mounted underneath and is
468
+ * restored when the viewer closes. No-op without a real TUI (render-only test stub).
469
+ */
470
+ openChat(id: string, entryId?: string): void {
471
+ if (this.#disposed || !this.#registry.get(id)) return;
472
+ if (typeof this.#ui.showOverlay !== "function") return;
473
+ this.#closeTranscriptOverlay();
474
+ this.#notice = undefined;
475
+ const viewer = new AgentTranscriptViewer({
476
+ agentId: id,
477
+ transcript: this.#transcript,
478
+ initialEntryId: entryId,
479
+ registry: this.#registry,
480
+ remote: this.#remote,
481
+ observers: this.#observers,
482
+ lifecycle: this.#remote ? undefined : this.#lifecycle,
483
+ ui: this.#ui,
484
+ getTool: this.#getTool,
485
+ isBuiltInTool: this.#isBuiltInTool,
486
+ getMessageRenderer: this.#getMessageRenderer,
487
+ cwd: this.#cwd,
488
+ hideThinkingBlock: this.#hideThinkingBlock,
489
+ proseOnlyThinking: this.#proseOnlyThinking,
490
+ expandKeys: this.#expandKeys,
491
+ hubKeys: this.#hubKeys,
492
+ requestRender: this.#requestRender,
493
+ onClose: () => this.#closeTranscriptOverlay(viewer),
494
+ onHubClose: () => {
495
+ if (this.#disposed) return;
496
+ this.#closeTranscriptOverlay(viewer);
497
+ if (!this.#disposed) this.#onDone();
498
+ },
499
+ });
500
+ this.#transcriptViewer = viewer;
501
+ this.#transcriptOverlay = this.#ui.showOverlay(viewer, { width: "100%", margin: 0, fullscreen: true });
502
+ this.#ui.setFocus(viewer);
503
+ this.#requestRender();
504
+ }
505
+
506
+ /** Close and dispose the transcript overlay, restoring focus to the hub table. */
507
+ #closeTranscriptOverlay(expectedViewer?: AgentTranscriptViewer): void {
508
+ if (expectedViewer && this.#transcriptViewer !== expectedViewer) return;
509
+ const overlay = this.#transcriptOverlay;
510
+ const viewer = this.#transcriptViewer;
511
+ if (!overlay && !viewer) return;
512
+ overlay?.hide();
513
+ this.#transcriptOverlay = undefined;
514
+ viewer?.dispose();
515
+ this.#transcriptViewer = undefined;
516
+ if (!this.#disposed) {
517
+ if (typeof this.#ui.setFocus === "function") this.#ui.setFocus(this);
518
+ this.#requestRender();
519
+ }
520
+ }
521
+
522
+ // ========================================================================
523
+ // Live data plumbing
524
+ // ========================================================================
525
+
526
+ #scheduleDataChange(): void {
527
+ if (this.#dataChangeTimer) return;
528
+ this.#dataChangeTimer = setTimeout(() => {
529
+ this.#dataChangeTimer = undefined;
530
+ this.#onDataChange();
531
+ }, DATA_CHANGE_RENDER_COALESCE_MS);
532
+ this.#dataChangeTimer.unref?.();
533
+ }
534
+
535
+ #onDataChange(): void {
536
+ this.#refreshRows();
537
+ this.#requestRender();
538
+ }
539
+
540
+ #refreshRows(): void {
541
+ const selectedId = this.#rows[this.#selectedRow]?.id;
542
+ const refs = this.#registry.list().filter(ref => ref.id !== MAIN_AGENT_ID);
543
+ this.#observedById = new Map();
544
+ for (const session of this.#observers.getSessions()) this.#observedById.set(session.id, session);
545
+ // Stable roster order: capture the status+recency ranking once so keyboard
546
+ // navigation is not disrupted by heartbeats (issue #10524). Existing rows
547
+ // keep their rank while the hub is open; new agents append at the end.
548
+ // Defer the capture until persisted-subagent discovery settles so a
549
+ // mid-scan refresh cannot freeze a partial roster (the remaining agents
550
+ // would otherwise append in readdir order instead of being ranked).
551
+ const rowOrder = this.#rowOrder;
552
+ let ordered: TRecord[];
553
+ if (!rowOrder) {
554
+ ordered = refs.sort(
555
+ (a, b) =>
556
+ STATUS_ORDER[a.status] - STATUS_ORDER[b.status] ||
557
+ b.lastActivity - a.lastActivity ||
558
+ a.id.localeCompare(b.id),
559
+ );
560
+ if (!this.#loadingPersistedSubagents && ordered.length > 0) {
561
+ this.#rowOrder = new Map();
562
+ for (const ref of ordered) this.#rowOrder.set(ref, this.#nextRowOrder++);
563
+ }
564
+ } else {
565
+ for (const rankedRef of rowOrder.keys()) {
566
+ if (!refs.includes(rankedRef)) rowOrder.delete(rankedRef);
567
+ }
568
+ ordered = refs.sort(
569
+ (a, b) => (rowOrder.get(a) ?? Number.MAX_SAFE_INTEGER) - (rowOrder.get(b) ?? Number.MAX_SAFE_INTEGER),
570
+ );
571
+ for (const ref of ordered) {
572
+ if (!rowOrder.has(ref)) rowOrder.set(ref, this.#nextRowOrder++);
573
+ }
574
+ }
575
+ const query = this.#agentFilter.trim();
576
+ const rosterRows =
577
+ query.length > 0
578
+ ? ordered.filter(ref => fuzzyMatch(query, `${ref.id} ${ref.displayName ?? ""}`).matches)
579
+ : ordered;
580
+
581
+ if (this.#viewMode === "tree") {
582
+ const tree = projectAgentTree(rosterRows);
583
+ this.#rows = tree.rows;
584
+ this.#treeDepthById = tree.depthById;
585
+ this.#treeParentById = tree.parentById;
586
+ this.#treeLastSiblingById = tree.lastSiblingById;
587
+ this.#treeMaxDepth = 0;
588
+ for (const depth of tree.depthById.values()) this.#treeMaxDepth = Math.max(this.#treeMaxDepth, depth);
589
+ } else {
590
+ this.#rows = rosterRows;
591
+ this.#treeDepthById.clear();
592
+ this.#treeParentById.clear();
593
+ this.#treeLastSiblingById.clear();
594
+ this.#treeMaxDepth = 0;
595
+ }
596
+ const keptIndex = selectedId ? this.#rows.findIndex(ref => ref.id === selectedId) : -1;
597
+ this.#selectedRow = keptIndex >= 0 ? keptIndex : Math.min(this.#selectedRow, Math.max(0, this.#rows.length - 1));
598
+ const detailAgentId = this.#rows[this.#selectedRow]?.id;
599
+ if (detailAgentId !== this.#detailAgentId) {
600
+ this.#detailAgentId = detailAgentId;
601
+ this.#detailScrollOffset = 0;
602
+ }
603
+
604
+ this.#childrenByParent.clear();
605
+ for (const ref of rosterRows) {
606
+ const parent = ref.parentId ?? MAIN_AGENT_ID;
607
+ const children = this.#childrenByParent.get(parent);
608
+ if (children) children.push(ref);
609
+ else this.#childrenByParent.set(parent, [ref]);
610
+ }
611
+ this.#statusCounts = { running: 0, idle: 0, parked: 0, aborted: 0 };
612
+ for (const ref of rosterRows) this.#statusCounts[ref.status]++;
613
+ this.#refreshAggregate();
614
+ this.#refreshActivityData(rosterRows);
615
+ this.#refreshActivityRows();
616
+ }
617
+
618
+ #refreshActivityData(refs: readonly TRecord[]): void {
619
+ if (this.#manageActivityLive) {
620
+ const liveIds = new Set<string>();
621
+ for (const ref of refs) {
622
+ const observed = this.#observedById.get(ref.id);
623
+ if (observed?.progress) {
624
+ liveIds.add(ref.id);
625
+ this.#activity.setLive(ref.id, activityRowsFromProgress(observed.progress, observed.lastUpdate));
626
+ }
627
+ }
628
+ for (const ref of refs) {
629
+ if (!liveIds.has(ref.id)) this.#activity.setLive(ref.id, []);
630
+ }
631
+ }
632
+
633
+ const generation = ++this.#activitySyncGeneration;
634
+ const pending: Promise<void>[] = [];
635
+ for (const ref of refs) {
636
+ if (!this.#remote && !ref.sessionFile) continue;
637
+ const stamp = `${ref.sessionFile ?? ""}:${ref.lastActivity}`;
638
+ if (this.#activitySyncStamp.get(ref.id) === stamp) continue;
639
+ this.#activitySyncStamp.set(ref.id, stamp);
640
+ pending.push(this.#activity.sync(ref.id, ref.sessionFile));
641
+ }
642
+ if (pending.length === 0) return;
643
+ void Promise.all(pending)
644
+ .then(() => {
645
+ if (this.#disposed || generation !== this.#activitySyncGeneration) return;
646
+ this.#refreshActivityRows();
647
+ this.#requestRender();
648
+ })
649
+ .catch(() => {
650
+ // Individual sync paths already guard I/O failures; keep the hub render loop alive.
651
+ });
652
+ }
653
+
654
+ #activityAgentIds(): ReadonlySet<string> | undefined {
655
+ if (this.#activityScope === "all") return undefined;
656
+ const selected = this.#rows[this.#selectedRow]?.id;
657
+ if (!selected) return new Set();
658
+ const ids = new Set([selected]);
659
+ if (this.#activityScope === "agent") return ids;
660
+ const queue = [selected];
661
+ for (let index = 0; index < queue.length; index++) {
662
+ for (const child of this.#childrenByParent.get(queue[index]!) ?? []) {
663
+ if (ids.has(child.id)) continue;
664
+ ids.add(child.id);
665
+ queue.push(child.id);
666
+ }
667
+ }
668
+ return ids;
669
+ }
670
+
671
+ #refreshActivityRows(): void {
672
+ const kinds: ReadonlySet<AgentActivityKind> | undefined =
673
+ this.#activityFilter === "responses"
674
+ ? new Set(["response"])
675
+ : this.#activityFilter === "tools"
676
+ ? new Set(["tool"])
677
+ : undefined;
678
+ let rows = this.#activity.query({
679
+ agentIds: this.#activityAgentIds(),
680
+ kinds,
681
+ search: this.#activitySearch,
682
+ limit: 2_000,
683
+ });
684
+ if (this.#activityFilter === "errors") rows = rows.filter(row => row.status === "error");
685
+ this.#activityRows = rows;
686
+ if (rows.length === 0) this.#selectedActivityRow = 0;
687
+ else if (this.#activityFollow) this.#selectedActivityRow = rows.length - 1;
688
+ else this.#selectedActivityRow = Math.min(this.#selectedActivityRow, rows.length - 1);
689
+ }
690
+
691
+ #metricsFor(ref: TRecord, observed: ObservableSession | undefined): AgentMetrics | undefined {
692
+ if (observed?.progress) return progressMetrics(observed);
693
+ if (ref.history?.metrics) return ref.history.metrics;
694
+ const session = this.#fallbackStatsSession(ref, observed);
695
+ return session ? this.#sessionMetrics.get(session)?.metrics : undefined;
696
+ }
697
+
698
+ #fallbackStatsSession(
699
+ ref: TRecord,
700
+ observed: ObservableSession | undefined,
701
+ ): NonNullable<TRecord["session"]> | undefined {
702
+ if (observed?.progress) return undefined;
703
+ const session = ref.session;
704
+ return session && typeof session.getSessionStats === "function" ? session : undefined;
705
+ }
706
+
707
+ // ========================================================================
708
+ // Table view
709
+ // ========================================================================
710
+
711
+ #sectionTabs(): string {
712
+ const tab = (section: AgentHubSection, label: string): string =>
713
+ this.#section === section
714
+ ? theme.bg("selectedBg", theme.bold(theme.fg("accent", ` ${label} `)))
715
+ : theme.fg("muted", ` ${label} `);
716
+ return `${tab("agents", "1 Agents")}${theme.fg("dim", theme.sep.dot)}${tab("activity", "2 Activity")}`;
717
+ }
718
+
719
+ #renderActivityTable(width: number, termHeight: number): string[] {
720
+ this.#hitRows.length = 0;
721
+ const innerWidth = Math.max(1, width - 4);
722
+ const contentRows = Math.max(1, termHeight - 4);
723
+ const body: string[] = [this.#sectionTabs()];
724
+ const selectedAgent = this.#rows[this.#selectedRow]?.id;
725
+ const scope =
726
+ this.#activityScope === "all"
727
+ ? "all agents"
728
+ : this.#activityScope === "agent"
729
+ ? (selectedAgent ?? "selected agent")
730
+ : `${selectedAgent ?? "selected"} subtree`;
731
+ const search = this.#activitySearchEditing
732
+ ? theme.fg("accent", `search: ${this.#activitySearch}▌`)
733
+ : this.#activitySearch
734
+ ? `search: ${this.#activitySearch}`
735
+ : "search: —";
736
+ body.push(
737
+ theme.fg(
738
+ "dim",
739
+ `${scope}${theme.sep.dot}${this.#activityFilter}${theme.sep.dot}${this.#activityFollow ? "following" : "paused"}${theme.sep.dot}${search}`,
740
+ ),
741
+ );
742
+ if (contentRows >= 8) body.push("");
743
+
744
+ const budget = Math.max(0, contentRows - body.length);
745
+ if (this.#activityRows.length === 0 && budget > 0) {
746
+ body.push(theme.fg("muted", this.#activitySearch ? "No matching activity" : "No agent activity recorded yet"));
747
+ } else if (budget > 0) {
748
+ const selected = Math.min(this.#selectedActivityRow, this.#activityRows.length - 1);
749
+ const start = this.#activityFollow
750
+ ? Math.max(0, this.#activityRows.length - budget)
751
+ : Math.max(0, Math.min(selected - Math.floor(budget / 2), this.#activityRows.length - budget));
752
+ const end = Math.min(this.#activityRows.length, start + budget);
753
+ if (start > 0) {
754
+ body.push(theme.fg("dim", `… ${start} earlier`));
755
+ }
756
+ for (let index = start + Number(start > 0); index < end; index++) {
757
+ this.#hitRows[1 + body.length] = index;
758
+ body.push(this.#formatActivityRow(this.#activityRows[index]!, index === selected, innerWidth));
759
+ }
760
+ }
761
+ while (body.length < contentRows) body.push("");
762
+
763
+ const lines = [topBorder(width, "Agent Hub")];
764
+ for (const line of body.slice(0, contentRows)) lines.push(row(line, width));
765
+ lines.push(divider(width));
766
+ lines.push(
767
+ row(
768
+ theme.fg(
769
+ "dim",
770
+ "1:agents j/k:select Enter:transcript Space:follow f:filter s:scope /:search Esc:close",
771
+ ),
772
+ width,
773
+ ),
774
+ );
775
+ lines.push(bottomBorder(width));
776
+ return lines;
777
+ }
778
+
779
+ #formatActivityRow(activity: AgentActivityRow, selected: boolean, width: number): string {
780
+ const cursor = selected ? theme.fg("accent", theme.nav.cursor) : " ";
781
+ const ref = this.#registry.get(activity.agentId);
782
+ const observed = this.#observedById.get(activity.agentId);
783
+ const role = observed?.progress?.modelRole ?? ref?.history?.modelRole;
784
+ const roleBadge = role && this.#getRoleInfo ? `${formatRoleBadge(role, this.#getRoleInfo(role))} ` : "";
785
+ const agent = sanitizeLine(activity.agentId, Math.max(8, Math.min(18, Math.floor(width * 0.18))));
786
+ const title = sanitizeLine(
787
+ activity.kind === "tool" ? (activity.toolName ?? activity.title) : activity.title,
788
+ width,
789
+ );
790
+ const prefix =
791
+ `${cursor} ${theme.fg("dim", activityClock(activity.timestamp))} ${activityGlyph(activity)} ` +
792
+ `${roleBadge}${theme.bold(agent)} ${theme.fg(activity.kind === "response" ? "success" : "muted", title)}`;
793
+ const available = Math.max(1, width - visibleWidth(prefix) - visibleWidth(theme.sep.dot));
794
+ return `${prefix}${theme.fg("dim", theme.sep.dot)}${sanitizeLine(activity.summary, available)}`;
795
+ }
796
+ #renderTable(width: number, termHeight: number): string[] {
797
+ this.#hitRows.length = 0;
798
+ const contentRows = Math.max(1, termHeight - 4);
799
+ this.#contentRowsLast = contentRows;
800
+ this.#paneHitRows = [];
801
+ const selected = this.#rows[this.#selectedRow];
802
+ this.#split.setNarrowPane(this.#narrowDetailsOpen ? "right" : "left");
803
+ this.#split.setHeight(contentRows);
804
+ const geometry = this.#split.measure(width);
805
+ const isSplit = geometry.mode === "split";
806
+ const innerWidth = Math.max(1, width - 4);
807
+ let topLine: string;
808
+ if (isSplit) {
809
+ topLine = topBorderSplit(width, "Agent Hub", geometry.left?.width ?? 0);
810
+ } else if (this.#narrowDetailsOpen && selected) {
811
+ topLine = topBorder(width, `Agent Hub · ${selected.id}`);
812
+ } else {
813
+ topLine = topBorder(width, "Agent Hub");
814
+ }
815
+ const dividerLine = isSplit && geometry.left ? dividerSplit(width, geometry.left.width) : divider(width);
816
+ this.#frameTop.setLines([topLine]);
817
+ this.#frameDivider.setLines([dividerLine]);
818
+ this.#frameFooter.setLines([row(this.#footer(isSplit ? false : this.#narrowDetailsOpen, innerWidth), width)]);
819
+ this.#frameBottom.setLines([bottomBorder(width)]);
820
+ this.#frame.setHeight(contentRows + 4);
821
+ const lines = [...this.#frame.render(width)];
822
+ const bodyRow = this.#frame.childRect(1)?.row ?? 1;
823
+ for (let i = 0; i < this.#paneHitRows.length; i++) {
824
+ const hit = this.#paneHitRows[i];
825
+ if (hit !== undefined) this.#hitRows[bodyRow + i] = hit;
826
+ }
827
+ return lines;
828
+ }
829
+
830
+ #footer(showingNarrowDetails: boolean, availableWidth: number): string {
831
+ const nextView = this.#viewMode === "roster" ? "by parent" : "flat";
832
+ const filter =
833
+ this.#agentFilter.length > 0 ? `/${this.#agentFilter}${this.#agentFilterEditing ? "▌" : ""} · ` : "";
834
+ if (showingNarrowDetails) {
835
+ return theme.fg(
836
+ "dim",
837
+ `${filter}1:agents 2:activity Tab:roster PgUp/PgDn:scroll Enter:open t:${nextView} Esc:roster`,
838
+ );
839
+ }
840
+ if (availableWidth < 96) {
841
+ return theme.fg("dim", `${filter}j/k:select Enter:open t:${nextView} Tab:details r/x:manage Esc:close`);
842
+ }
843
+ return theme.fg(
844
+ "dim",
845
+ `${filter}1:agents 2:activity j/k/wheel:select PgUp/PgDn:details Enter/click:open t:${nextView} r:revive x:kill Esc:close`,
846
+ );
847
+ }
848
+
849
+ #renderRosterPanel(width: number, rows: number, observedById: ReadonlyMap<string, ObservableSession>): RosterRender {
850
+ const lines = this.#summaryLines(width);
851
+ const hitRows: Array<number | undefined> = Array.from({ length: lines.length });
852
+ if (rows >= 8) {
853
+ lines.push("");
854
+ hitRows.push(undefined);
855
+ }
856
+
857
+ const noticeLines = this.#notice ? [theme.fg("error", sanitizeLine(this.#notice, Math.max(10, width)))] : [];
858
+ const budget = Math.max(0, rows - lines.length - noticeLines.length);
859
+ if (this.#rows.length === 0) {
860
+ if (this.#loadingPersistedSubagents) {
861
+ if (budget > 0) {
862
+ lines.push(`${statusGlyph("running")} ${theme.fg("accent", "Loading saved agents…")}`);
863
+ hitRows.push(undefined);
864
+ }
865
+ } else {
866
+ const emptyState = [
867
+ `${theme.fg("muted", theme.status.shadowed)} ${theme.bold("No agents in this session")}`,
868
+ theme.fg("dim", "Finished, parked, and killed subagents remain with the session that created them."),
869
+ theme.fg("dim", "Resume that session with omp-dev --continue, or spawn a task here."),
870
+ ];
871
+ for (const line of emptyState.slice(0, budget)) {
872
+ lines.push(line);
873
+ hitRows.push(undefined);
874
+ }
875
+ }
876
+ } else if (budget > 0) {
877
+ const window = this.#renderRosterWindow(width, budget, observedById);
878
+ lines.push(...window.lines);
879
+ hitRows.push(...window.hitRows);
880
+ }
881
+ for (const notice of noticeLines) {
882
+ lines.push(notice);
883
+ hitRows.push(undefined);
884
+ }
885
+ while (lines.length < rows) {
886
+ lines.push("");
887
+ hitRows.push(undefined);
888
+ }
889
+ return { lines: lines.slice(0, rows), hitRows: hitRows.slice(0, rows) };
890
+ }
891
+
892
+ #renderRosterWindow(
893
+ width: number,
894
+ budget: number,
895
+ _observedById: ReadonlyMap<string, ObservableSession>,
896
+ ): RosterRender {
897
+ const lines: string[] = [];
898
+ const hitRows: Array<number | undefined> = [];
899
+ const rendered = new Map<number, string[]>();
900
+ const entryAt = (index: number): string[] => {
901
+ const cached = rendered.get(index);
902
+ if (cached) return cached;
903
+ const entry = this.#renderEntry(
904
+ this.#rows[index],
905
+ index === this.#selectedRow,
906
+ width,
907
+ this.#observableFor(this.#rows[index].id),
908
+ index === this.#hoveredRow,
909
+ );
910
+ rendered.set(index, entry);
911
+ return entry;
912
+ };
913
+ const appendEntry = (index: number, entry = entryAt(index)): void => {
914
+ for (const line of entry) {
915
+ lines.push(line);
916
+ hitRows.push(index);
917
+ }
918
+ };
919
+
920
+ let start = this.#selectedRow;
921
+ let end = this.#selectedRow + 1;
922
+ let used = entryAt(this.#selectedRow).length;
923
+ if (used > budget) {
924
+ appendEntry(this.#selectedRow, entryAt(this.#selectedRow).slice(0, budget));
925
+ return { lines, hitRows };
926
+ }
927
+
928
+ // Grow a window around the selection. Only visible entries are rendered,
929
+ // so the 5,000-agent Hub retains bounded paint cost.
930
+ for (let grew = true; grew;) {
931
+ grew = false;
932
+ if (end < this.#rows.length) {
933
+ const next = entryAt(end);
934
+ if (used + next.length <= budget) {
935
+ used += next.length;
936
+ end++;
937
+ grew = true;
938
+ }
939
+ }
940
+ if (start > 0) {
941
+ const previous = entryAt(start - 1);
942
+ if (used + previous.length <= budget) {
943
+ start--;
944
+ used += previous.length;
945
+ grew = true;
946
+ }
947
+ }
948
+ }
949
+ // Overflow labels consume real rows. Trim the farthest visible neighbors
950
+ // before painting them so the selected entry and both labels fit.
951
+ for (
952
+ let markerRows = Number(start > 0) + Number(end < this.#rows.length);
953
+ used + markerRows > budget && start < end;
954
+ markerRows = Number(start > 0) + Number(end < this.#rows.length)
955
+ ) {
956
+ if (end - 1 > this.#selectedRow) {
957
+ end--;
958
+ used -= entryAt(end).length;
959
+ } else if (start < this.#selectedRow) {
960
+ used -= entryAt(start).length;
961
+ start++;
962
+ } else {
963
+ break;
964
+ }
965
+ }
966
+ const showTopOverflow = start > 0 && used < budget;
967
+ const showBottomOverflow = end < this.#rows.length && used + Number(showTopOverflow) < budget;
968
+ if (showTopOverflow) {
969
+ lines.push(theme.fg("dim", `… ${start} more`));
970
+ hitRows.push(undefined);
971
+ }
972
+ for (let i = start; i < end; i++) appendEntry(i);
973
+ if (showBottomOverflow) {
974
+ lines.push(theme.fg("dim", `… ${this.#rows.length - end} more`));
975
+ hitRows.push(undefined);
976
+ }
977
+ return { lines, hitRows };
978
+ }
979
+
980
+ #summaryLines(width: number): string[] {
981
+ const active = (label: string): string => theme.bg("selectedBg", theme.bold(theme.fg("accent", ` ${label} `)));
982
+ const inactive = (label: string): string => theme.fg("muted", ` ${label} `);
983
+ const projection =
984
+ this.#viewMode === "roster"
985
+ ? `${active("Flat")}${theme.fg("dim", "/")}${inactive("By parent")}`
986
+ : `${inactive("Flat")}${theme.fg("dim", "/")}${active("By parent")}`;
987
+ const counts = this.#statusSummary();
988
+ const header = `${theme.bold("Roster")}${theme.fg("dim", theme.sep.dot)}${projection}${counts ? theme.fg("dim", theme.sep.dot) + counts : ""}`;
989
+ const lines = wrapTextWithAnsi(header, Math.max(1, width));
990
+
991
+ const metrics = this.#aggregate;
992
+ if (metrics.reportedAgents === 0) {
993
+ lines.push(
994
+ ...wrapTextWithAnsi(
995
+ theme.fg("dim", `Usage —${theme.sep.dot}0/${this.#rows.length} measured`),
996
+ Math.max(1, width),
997
+ ),
998
+ );
999
+ return lines;
1000
+ }
1001
+ const activeTime = formatMetricDuration(metrics);
1002
+ const usage = [
1003
+ theme.fg("statusLineCost", formatCost(metrics.cost)),
1004
+ theme.fg("dim", activeTime ? `${activeTime} agent time` : "agent time —"),
1005
+ theme.fg("dim", `${formatNumber(metrics.requests)} req`),
1006
+ theme.fg("dim", `${formatNumber(metrics.tools)} tools`),
1007
+ theme.fg("dim", `${formatNumber(metrics.tokens)} tok`),
1008
+ theme.fg("dim", `${metrics.activeDurationAgents}/${metrics.reportedAgents} timed`),
1009
+ theme.fg("dim", `${metrics.reportedAgents}/${this.#rows.length} measured`),
1010
+ ].join(theme.fg("dim", theme.sep.dot));
1011
+ lines.push(...wrapTextWithAnsi(usage, Math.max(1, width)));
1012
+ return lines;
1013
+ }
1014
+
1015
+ #statusSummary(): string {
1016
+ const parts: string[] = [];
1017
+ for (const status of ["running", "idle", "parked", "aborted"] as const) {
1018
+ const count = this.#statusCounts[status];
1019
+ if (count > 0) parts.push(`${statusGlyph(status)} ${statusText(status, `${count} ${status}`)}`);
1020
+ }
1021
+ return parts.join(theme.sep.dot);
1022
+ }
1023
+
1024
+ #refreshAggregate(refreshFallback = false): void {
1025
+ const result = aggregateMetrics({
1026
+ rows: this.#rows,
1027
+ observedById: this.#observedById,
1028
+ metricsFor: (ref, observed) => this.#metricsFor(ref, observed),
1029
+ fallbackStatsSession: (ref, observed) => this.#fallbackStatsSession(ref, observed),
1030
+ sessionMetrics: this.#sessionMetrics,
1031
+ refreshFallback,
1032
+ });
1033
+ this.#aggregate = result.metrics;
1034
+ this.#hasFallbackLiveSessions = result.hasFallbackLiveSessions;
1035
+ }
1036
+
1037
+ #observableFor(id: string): ObservableSession | undefined {
1038
+ return this.#observedById.get(id) ?? this.#observers.getSession(id);
1039
+ }
1040
+
1041
+ #renderDetailPanel(
1042
+ ref: TRecord | undefined,
1043
+ width: number,
1044
+ rows: number,
1045
+ _observedById: ReadonlyMap<string, ObservableSession>,
1046
+ ): string[] {
1047
+ if (!ref) return [theme.fg("dim", "Select an agent to inspect"), ...Array.from({ length: rows - 1 }, () => "")];
1048
+ const observed = this.#observableFor(ref.id);
1049
+ const progress = observed?.progress;
1050
+ const metrics = this.#metricsFor(ref, observed);
1051
+ const children = this.#childrenByParent.get(ref.id) ?? [];
1052
+ const lines: string[] = [];
1053
+ const add = (line = ""): void => {
1054
+ lines.push(truncateToWidth(line, width));
1055
+ };
1056
+ const addWrapped = (text: string, maxRows = 2): void => {
1057
+ for (const wrapped of wrapTextWithAnsi(sanitizeLine(text), Math.max(1, width)).slice(0, maxRows)) add(wrapped);
1058
+ };
1059
+ const section = (label: string, contentRows = 0): void => {
1060
+ if (lines.length > 0 && lines.length + 1 + contentRows < rows) add();
1061
+ add(theme.bold(theme.fg("accent", label)));
1062
+ };
1063
+
1064
+ add(`${statusGlyph(ref.status)} ${theme.bold(sanitizeDisplaySingleLine(ref.displayName || ref.id))}`);
1065
+ if (ref.displayName && ref.displayName !== ref.id) add(theme.fg("dim", sanitizeDisplaySingleLine(ref.id)));
1066
+ const lifecycleDetails = [
1067
+ metrics ? formatMetricDuration(metrics) : undefined,
1068
+ `active ${formatAge(Math.max(1, Math.round((Date.now() - ref.lastActivity) / 1000)))}`,
1069
+ ].filter(Boolean);
1070
+ add(
1071
+ `${statusText(ref.status, ref.status)}${theme.fg("dim", `${theme.sep.dot}${lifecycleDetails.join(theme.sep.dot)}`)}`,
1072
+ );
1073
+ const modelDetails: string[] = [];
1074
+ const modelRole = progress?.modelRole ?? ref.history?.modelRole;
1075
+ if (modelRole && this.#getRoleInfo) modelDetails.push(formatRoleBadge(modelRole, this.#getRoleInfo(modelRole)));
1076
+ const badge = modelBadge(ref, observed);
1077
+ if (badge) modelDetails.push(badge);
1078
+ if (modelDetails.length > 0) add(modelDetails.join(theme.sep.dot));
1079
+
1080
+ const task = observed?.description ?? progress?.task ?? ref.activity;
1081
+ if (task) {
1082
+ section("Task");
1083
+ addWrapped(task);
1084
+ }
1085
+
1086
+ const current = progress?.currentTool
1087
+ ? `${progress.currentTool}${progress.currentToolArgs ? ` · ${progress.currentToolArgs}` : ""}`
1088
+ : (progress?.lastIntent ?? ref.activity);
1089
+ if (current) {
1090
+ section("Current");
1091
+ addWrapped(current);
1092
+ if (progress?.retryState) {
1093
+ add(theme.fg("warning", `retry ${progress.retryState.attempt}/${progress.retryState.maxAttempts}`));
1094
+ }
1095
+ }
1096
+
1097
+ section("Usage", 1);
1098
+ if (metrics) {
1099
+ addWrapped(formatMetrics(metrics), 3);
1100
+ if (metrics.contextTokens !== undefined && metrics.contextWindow) {
1101
+ add(contextGauge(metrics.contextTokens, metrics.contextWindow));
1102
+ }
1103
+ } else {
1104
+ add(theme.fg("dim", "usage —"));
1105
+ }
1106
+
1107
+ section("Lineage");
1108
+ add(
1109
+ `Spawned by ${sanitizeDisplaySingleLine(ref.parentId ?? MAIN_AGENT_ID)}${children.length > 0 ? ` · ${children.length} children` : ""}`,
1110
+ );
1111
+ if (children.length > 0) add(theme.fg("dim", formatChildIds(children, width)));
1112
+ add(theme.fg("dim", `Registered ${formatLocalDateTimeWithOffset(new Date(ref.createdAt))}`));
1113
+
1114
+ section("Changes");
1115
+ add(
1116
+ theme.fg(
1117
+ "dim",
1118
+ ref.kind === "advisor" || ref.history?.readOnly
1119
+ ? "Read-only · 0 LoC"
1120
+ : "Shared workspace · per-agent LoC not attributable",
1121
+ ),
1122
+ );
1123
+ const artifacts = ref.history;
1124
+ if (artifacts?.outputPath) addWrapped(`Output ${shortenPath(artifacts.outputPath)}`);
1125
+ if (artifacts?.patchPath) addWrapped(`Patch ${shortenPath(artifacts.patchPath)}`);
1126
+ for (const nestedPath of artifacts?.nestedPatchPaths ?? []) addWrapped(`Nested patch ${shortenPath(nestedPath)}`);
1127
+ if (artifacts?.branchName) addWrapped(`Worktree branch ${artifacts.branchName}`);
1128
+
1129
+ if (lines.length < rows) add();
1130
+ if (lines.length < rows) add(theme.bold(theme.fg("accent", "Recent activity")));
1131
+ const activityBudget = Math.max(0, rows - lines.length);
1132
+ const activity = this.#activity.recent(ref.id, activityBudget);
1133
+ if (activity.length === 0 && activityBudget > 0) add(theme.fg("muted", "No response or tool activity yet"));
1134
+ else {
1135
+ for (const event of activity) {
1136
+ const title = sanitizeLine(event.kind === "tool" ? (event.toolName ?? event.title) : event.title, width);
1137
+ const prefix = `${theme.fg("dim", activityClock(event.timestamp))} ${activityGlyph(event)} ${theme.fg("muted", title)} `;
1138
+ add(`${prefix}${sanitizeLine(event.summary, Math.max(1, width - visibleWidth(prefix)))}`);
1139
+ }
1140
+ }
1141
+ const maxScroll = Math.max(0, lines.length - rows);
1142
+ this.#detailScrollOffset = Math.min(this.#detailScrollOffset, maxScroll);
1143
+ const visible = lines.slice(this.#detailScrollOffset, this.#detailScrollOffset + rows);
1144
+ while (visible.length < rows) visible.push("");
1145
+ return visible;
1146
+ }
1147
+
1148
+ /**
1149
+ * One agent entry keeps identity/model metadata on one line when it fits,
1150
+ * then packs task and all five usage metrics together below. Narrow rows wrap
1151
+ * only those dense secondary fields.
1152
+ */
1153
+ #renderEntry(
1154
+ ref: TRecord,
1155
+ selected: boolean,
1156
+ width: number,
1157
+ observed: ObservableSession | undefined,
1158
+ hovered = false,
1159
+ ): string[] {
1160
+ const max = Math.max(1, width);
1161
+ const cursor = selected ? theme.fg("accent", theme.nav.cursor) : " ";
1162
+ const treeMode = this.#viewMode === "tree";
1163
+ // Tree rails descend from the status dot: the connector sits between the
1164
+ // cursor and the glyph, so child rows hang under their parent's dot.
1165
+ const branch = treeMode
1166
+ ? treeBranch(ref, max, this.#treeDepthById, this.#treeParentById, this.#treeLastSiblingById)
1167
+ : "";
1168
+ const id = sanitizeDisplaySingleLine(ref.id);
1169
+ const styledId = selected ? theme.bold(theme.fg("accent", id)) : theme.bold(id);
1170
+ const fields: string[] = [`${cursor} ${branch}${statusGlyph(ref.status)} ${styledId}`];
1171
+ if (this.#viewMode === "roster" && ref.parentId && ref.parentId !== MAIN_AGENT_ID) {
1172
+ fields.push(theme.fg("dim", `↳ ${sanitizeDisplaySingleLine(ref.parentId)}`));
1173
+ }
1174
+ if (ref.kind === "advisor") {
1175
+ fields.push(theme.fg("warning", "read-only"));
1176
+ }
1177
+ const unread = this.#irc.unreadCount(ref.id);
1178
+ if (unread > 0) {
1179
+ fields.push(theme.fg("warning", `⧉ ${unread}`));
1180
+ }
1181
+ const left = fields.join(" ");
1182
+
1183
+ // Line 1 right side carries variable-width badges only. Cost/turns/time
1184
+ // get their own always-present metadata line below so wrapping task text
1185
+ // can never mix with them.
1186
+ const metrics = this.#metricsFor(ref, observed);
1187
+ const meta: string[] = [];
1188
+ const modelRole = observed?.progress?.modelRole ?? ref.history?.modelRole;
1189
+ if (modelRole && this.#getRoleInfo) {
1190
+ meta.push(formatRoleBadge(modelRole, this.#getRoleInfo(modelRole)));
1191
+ }
1192
+ const badge = modelBadge(ref, observed);
1193
+ if (badge) meta.push(badge);
1194
+ const right = meta.join(theme.sep.dot);
1195
+
1196
+ const entry: string[] = [];
1197
+ const detailIndent = Math.min(max - 1, 4 + visibleWidth(branch));
1198
+ const leftWidth = visibleWidth(left);
1199
+ const rightWidth = visibleWidth(right);
1200
+ if (rightWidth > 0 && leftWidth + 2 + rightWidth <= max) {
1201
+ entry.push(left + padding(max - leftWidth - rightWidth) + right);
1202
+ } else {
1203
+ entry.push(truncateToWidth(left.replace(/[\r\n]+/g, " "), max));
1204
+ }
1205
+
1206
+ const ownChildRail = this.#childrenByParent.has(ref.id) ? theme.fg("dim", "│ ") : " ";
1207
+ const continuation = treeMode
1208
+ ? ` ${treeContinuation(ref, max, this.#treeDepthById, this.#treeParentById, this.#treeLastSiblingById)}${ownChildRail}`
1209
+ : "";
1210
+ const indent = treeMode && visibleWidth(continuation) === detailIndent ? continuation : padding(detailIndent);
1211
+ const metadataIndent = treeMode ? treeMetadataIndent(max, this.#treeMaxDepth) : detailIndent;
1212
+ const metadataPrefix =
1213
+ treeMode && visibleWidth(continuation) === detailIndent
1214
+ ? continuation + padding(metadataIndent - detailIndent)
1215
+ : padding(metadataIndent);
1216
+ const detailWidth = Math.max(1, max - detailIndent);
1217
+ const task = observed?.description ?? observed?.progress?.task ?? ref.activity;
1218
+ if (task) {
1219
+ entry.push(`${indent}${theme.fg("muted", truncateToWidth(sanitizeLine(task, detailWidth), detailWidth))}`);
1220
+ }
1221
+ const age = formatAge(Math.max(1, Math.round((Date.now() - ref.lastActivity) / 1000)));
1222
+ const metadata = metrics ? formatMetricColumns(metrics, age) : `usage ${theme.sep.dot} ${age}`;
1223
+ entry.push(`${metadataPrefix}${theme.fg("dim", metadata)}`);
1224
+ if (!hovered) return entry;
1225
+ return entry.map(lineRow => {
1226
+ const rowWidth = visibleWidth(lineRow);
1227
+ return theme.bg("selectedBg", rowWidth < max ? lineRow + padding(max - rowWidth) : lineRow);
1228
+ });
1229
+ }
1230
+
1231
+ #scrollDetails(direction: -1 | 1): void {
1232
+ this.#detailScrollOffset = Math.max(0, this.#detailScrollOffset + direction * 5);
1233
+ this.#requestRender();
1234
+ }
1235
+
1236
+ #selectRow(index: number): void {
1237
+ if (index !== this.#selectedRow) {
1238
+ this.#detailScrollOffset = 0;
1239
+ this.#detailAgentId = this.#rows[index]?.id;
1240
+ }
1241
+ this.#selectedRow = index;
1242
+ }
1243
+
1244
+ handleWheel(delta: -1 | 1): void {
1245
+ this.#hoveredRow = null;
1246
+ if (this.#section === "activity") {
1247
+ if (this.#activityRows.length > 0) {
1248
+ this.#activityFollow = false;
1249
+ this.#selectedActivityRow = Math.max(
1250
+ 0,
1251
+ Math.min(this.#selectedActivityRow + delta, this.#activityRows.length - 1),
1252
+ );
1253
+ }
1254
+ } else if (this.#rows.length > 0) {
1255
+ this.#selectRow(Math.max(0, Math.min(this.#selectedRow + delta, this.#rows.length - 1)));
1256
+ }
1257
+ this.#requestRender();
1258
+ }
1259
+
1260
+ hitTest(line: number): number | undefined {
1261
+ return this.#hitRows[line];
1262
+ }
1263
+
1264
+ setHoverIndex(index: number | null): void {
1265
+ if (index === this.#hoveredRow) return;
1266
+ this.#hoveredRow = index;
1267
+ this.#requestRender();
1268
+ }
1269
+
1270
+ clickItem(index: number): void {
1271
+ if (this.#section === "activity") {
1272
+ if (index === this.#selectedActivityRow) {
1273
+ const activity = this.#activityRows[index];
1274
+ if (activity) this.openChat(activity.agentId, activity.entryId);
1275
+ return;
1276
+ }
1277
+ this.#activityFollow = false;
1278
+ this.#selectedActivityRow = index;
1279
+ this.#requestRender();
1280
+ return;
1281
+ }
1282
+ const selected = this.#rows[index];
1283
+ if (!selected) return;
1284
+ this.#hoveredRow = index;
1285
+ this.#selectRow(index);
1286
+ this.#requestRender();
1287
+ this.#activateAgent(selected);
1288
+ }
1289
+
1290
+ #switchSection(section: AgentHubSection): void {
1291
+ if (this.#section === section) return;
1292
+ this.#section = section;
1293
+ this.#hoveredRow = null;
1294
+ this.#narrowDetailsOpen = false;
1295
+ if (section === "activity") this.#refreshActivityRows();
1296
+ this.#requestRender();
1297
+ }
1298
+
1299
+ #handleActivitySearchInput(keyData: string): void {
1300
+ if (matchesKey(keyData, "escape") || matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
1301
+ this.#activitySearchEditing = false;
1302
+ } else if (matchesKey(keyData, "backspace")) {
1303
+ this.#activitySearch = this.#activitySearch.slice(0, -1);
1304
+ } else if (keyData.length === 1 && keyData >= " " && keyData !== "\u007f") {
1305
+ this.#activitySearch += keyData;
1306
+ } else {
1307
+ return;
1308
+ }
1309
+ this.#refreshActivityRows();
1310
+ this.#requestRender();
1311
+ }
1312
+
1313
+ #handleActivityInput(keyData: string): void {
1314
+ if (matchesKey(keyData, "escape")) {
1315
+ if (this.#activitySearch) {
1316
+ this.#activitySearch = "";
1317
+ this.#refreshActivityRows();
1318
+ this.#requestRender();
1319
+ } else {
1320
+ this.#onDone();
1321
+ }
1322
+ return;
1323
+ }
1324
+ if (matchesKey(keyData, "left")) {
1325
+ this.#switchSection("agents");
1326
+ return;
1327
+ }
1328
+ if (keyData === "/") {
1329
+ this.#activitySearchEditing = true;
1330
+ this.#requestRender();
1331
+ return;
1332
+ }
1333
+ if (keyData === " ") {
1334
+ this.#activityFollow = !this.#activityFollow;
1335
+ if (this.#activityFollow && this.#activityRows.length > 0) {
1336
+ this.#selectedActivityRow = this.#activityRows.length - 1;
1337
+ }
1338
+ this.#requestRender();
1339
+ return;
1340
+ }
1341
+ if (keyData === "f") {
1342
+ const filters: ActivityFilter[] = ["all", "errors", "responses", "tools"];
1343
+ this.#activityFilter = filters[(filters.indexOf(this.#activityFilter) + 1) % filters.length]!;
1344
+ this.#refreshActivityRows();
1345
+ this.#requestRender();
1346
+ return;
1347
+ }
1348
+ if (keyData === "s") {
1349
+ const scopes: ActivityScope[] = ["all", "agent", "subtree"];
1350
+ this.#activityScope = scopes[(scopes.indexOf(this.#activityScope) + 1) % scopes.length]!;
1351
+ this.#refreshActivityRows();
1352
+ this.#requestRender();
1353
+ return;
1354
+ }
1355
+ if (matchesKey(keyData, "j") || matchesSelectDown(keyData)) {
1356
+ if (this.#activityRows.length > 0) {
1357
+ this.#activityFollow = false;
1358
+ this.#selectedActivityRow = Math.min(this.#selectedActivityRow + 1, this.#activityRows.length - 1);
1359
+ }
1360
+ this.#requestRender();
1361
+ return;
1362
+ }
1363
+ if (matchesKey(keyData, "k") || matchesSelectUp(keyData)) {
1364
+ if (this.#activityRows.length > 0) {
1365
+ this.#activityFollow = false;
1366
+ this.#selectedActivityRow = Math.max(this.#selectedActivityRow - 1, 0);
1367
+ }
1368
+ this.#requestRender();
1369
+ return;
1370
+ }
1371
+ if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
1372
+ const activity = this.#activityRows[this.#selectedActivityRow];
1373
+ if (activity) this.openChat(activity.agentId, activity.entryId);
1374
+ }
1375
+ }
1376
+
1377
+ #handleTableInput(keyData: string): void {
1378
+ if (this.#agentFilterEditing) {
1379
+ if (matchesKey(keyData, "escape") || matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
1380
+ this.#agentFilterEditing = false;
1381
+ if (matchesKey(keyData, "escape")) this.#agentFilter = "";
1382
+ } else if (matchesKey(keyData, "backspace")) {
1383
+ this.#agentFilter = this.#agentFilter.slice(0, -1);
1384
+ } else if (keyData.length === 1 && keyData >= " " && keyData !== "\u007f") {
1385
+ this.#agentFilter += keyData;
1386
+ } else {
1387
+ return;
1388
+ }
1389
+ this.#refreshRows();
1390
+ this.#requestRender();
1391
+ return;
1392
+ }
1393
+ if (matchesKey(keyData, "escape")) {
1394
+ if (this.#agentFilter) {
1395
+ this.#agentFilter = "";
1396
+ this.#refreshRows();
1397
+ this.#requestRender();
1398
+ } else if (this.#narrowDetailsOpen && this.#split.mode !== "split") {
1399
+ this.#narrowDetailsOpen = false;
1400
+ this.#requestRender();
1401
+ } else {
1402
+ this.#onDone();
1403
+ }
1404
+ return;
1405
+ }
1406
+ if (keyData === "/") {
1407
+ this.#agentFilterEditing = true;
1408
+ this.#requestRender();
1409
+ return;
1410
+ }
1411
+ if ((matchesKey(keyData, "tab") || keyData === "\t") && this.#split.mode !== "split") {
1412
+ if (this.#rows.length > 0) this.#narrowDetailsOpen = !this.#narrowDetailsOpen;
1413
+ this.#requestRender();
1414
+ return;
1415
+ }
1416
+ if (this.#split.mode === "split" || this.#narrowDetailsOpen) {
1417
+ if (matchesKey(keyData, "pageUp")) {
1418
+ this.#scrollDetails(-1);
1419
+ return;
1420
+ }
1421
+ if (matchesKey(keyData, "pageDown")) {
1422
+ this.#scrollDetails(1);
1423
+ return;
1424
+ }
1425
+ }
1426
+ if (keyData === "t") {
1427
+ this.#hoveredRow = null;
1428
+ this.#viewMode = this.#viewMode === "roster" ? "tree" : "roster";
1429
+ this.#refreshRows();
1430
+ this.#requestRender();
1431
+ return;
1432
+ }
1433
+ if (matchesKey(keyData, "left")) {
1434
+ if (this.#narrowDetailsOpen && this.#split.mode !== "split") {
1435
+ this.#narrowDetailsOpen = false;
1436
+ this.#requestRender();
1437
+ return;
1438
+ }
1439
+ const now = Date.now();
1440
+ if (now - this.#lastLeftTap < LEFT_TAP_WINDOW_MS) {
1441
+ this.#lastLeftTap = 0;
1442
+ this.#onDone();
1443
+ } else {
1444
+ this.#lastLeftTap = now;
1445
+ }
1446
+ return;
1447
+ }
1448
+ this.#hoveredRow = null;
1449
+ if (matchesKey(keyData, "j") || matchesSelectDown(keyData)) {
1450
+ if (this.#rows.length > 0) {
1451
+ this.#selectRow(Math.min(this.#selectedRow + 1, this.#rows.length - 1));
1452
+ }
1453
+ this.#requestRender();
1454
+ return;
1455
+ }
1456
+ if (matchesKey(keyData, "k") || matchesSelectUp(keyData)) {
1457
+ if (this.#rows.length > 0) {
1458
+ this.#selectRow(Math.max(this.#selectedRow - 1, 0));
1459
+ }
1460
+ this.#requestRender();
1461
+ return;
1462
+ }
1463
+ if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
1464
+ const selected = this.#rows[this.#selectedRow];
1465
+ if (selected) this.#activateAgent(selected);
1466
+ return;
1467
+ }
1468
+ if (keyData === "r") {
1469
+ this.#reviveSelected();
1470
+ return;
1471
+ }
1472
+ if (keyData === "x") {
1473
+ this.#killSelected();
1474
+ return;
1475
+ }
1476
+ }
1477
+
1478
+ /**
1479
+ * Enter on a row: focus the main view on the agent's live session and close
1480
+ * the hub. The transcript then renders through the regular session pipeline —
1481
+ * exact parity by construction. Collab guests (no local sessions) keep the
1482
+ * in-hub chat view.
1483
+ */
1484
+ #activateAgent(ref: TRecord): void {
1485
+ this.#notice = undefined;
1486
+ const focusAgent = this.#focusAgent;
1487
+ // Aborted agents and advisor refs are read-only transcripts with no
1488
+ // revivable session; open the in-hub viewer instead of failing ensureLive.
1489
+ if (ref.kind === "advisor" || ref.status === "aborted" || this.#remote || !focusAgent) {
1490
+ this.openChat(ref.id);
1491
+ return;
1492
+ }
1493
+ void (async () => {
1494
+ try {
1495
+ await focusAgent(ref.id); // ensureLive inside revives parked agents; no parking, no session files
1496
+ this.#onDone();
1497
+ } catch (error) {
1498
+ this.#notice = error instanceof Error ? error.message : String(error);
1499
+ this.#requestRender();
1500
+ }
1501
+ })();
1502
+ }
1503
+
1504
+ #reviveSelected(): void {
1505
+ const ref = this.#rows[this.#selectedRow];
1506
+ if (!ref) return;
1507
+ if (ref.kind === "advisor") {
1508
+ this.#notice = `"${ref.id}" is a read-only advisor transcript — nothing to revive.`;
1509
+ this.#requestRender();
1510
+ return;
1511
+ }
1512
+ if (ref.status !== "parked") {
1513
+ this.#notice = `Agent "${ref.id}" is ${ref.status} — only parked agents can be revived.`;
1514
+ this.#requestRender();
1515
+ return;
1516
+ }
1517
+ this.#notice = undefined;
1518
+ if (this.#remote) {
1519
+ this.#remote.revive(ref.id);
1520
+ this.#requestRender();
1521
+ return;
1522
+ }
1523
+ // Fire-and-forget; failures surface as an inline notice
1524
+ this.#lifecycle()
1525
+ .ensureLive(ref.id)
1526
+ .catch((error: unknown) => {
1527
+ this.#notice = error instanceof Error ? error.message : String(error);
1528
+ this.#requestRender();
1529
+ });
1530
+ this.#requestRender();
1531
+ }
1532
+
1533
+ #killSelected(): void {
1534
+ const ref = this.#rows[this.#selectedRow];
1535
+ if (!ref) return;
1536
+ if (ref.kind === "advisor") {
1537
+ this.#notice = `"${ref.id}" is a read-only advisor transcript — cannot be killed.`;
1538
+ this.#requestRender();
1539
+ return;
1540
+ }
1541
+ this.#notice = undefined;
1542
+ if (this.#remote) {
1543
+ this.#remote.kill(ref.id);
1544
+ this.#refreshRows();
1545
+ this.#requestRender();
1546
+ return;
1547
+ }
1548
+ void (async () => {
1549
+ try {
1550
+ if (ref.status === "running" && ref.session) {
1551
+ await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
1552
+ }
1553
+ await this.#lifecycle().release(ref.id, ref, { tombstone: true });
1554
+ } catch (error) {
1555
+ logger.warn("Agent hub: kill failed", { id: ref.id, error: String(error) });
1556
+ this.#notice = error instanceof Error ? error.message : String(error);
1557
+ }
1558
+ this.#refreshRows();
1559
+ this.#requestRender();
1560
+ })();
1561
+ }
1562
+ }