nastech-tui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/.prettierrc +11 -0
  2. package/README.md +346 -0
  3. package/eslint.config.mjs +111 -0
  4. package/package.json +51 -0
  5. package/packages/nastech-ink/ambient.d.ts +83 -0
  6. package/packages/nastech-ink/index.d.ts +40 -0
  7. package/packages/nastech-ink/index.js +1 -0
  8. package/packages/nastech-ink/nastech-ink/ambient.d.ts +83 -0
  9. package/packages/nastech-ink/nastech-ink/index.d.ts +40 -0
  10. package/packages/nastech-ink/nastech-ink/index.js +1 -0
  11. package/packages/nastech-ink/nastech-ink/package.json +54 -0
  12. package/packages/nastech-ink/nastech-ink/src/bootstrap/state.ts +9 -0
  13. package/packages/nastech-ink/nastech-ink/src/entry-exports.ts +32 -0
  14. package/packages/nastech-ink/nastech-ink/src/hooks/use-stderr.ts +15 -0
  15. package/packages/nastech-ink/nastech-ink/src/hooks/use-stdout.ts +15 -0
  16. package/packages/nastech-ink/nastech-ink/src/ink/Ansi.tsx +435 -0
  17. package/packages/nastech-ink/nastech-ink/src/ink/app-mouse.test.ts +123 -0
  18. package/packages/nastech-ink/nastech-ink/src/ink/bidi.ts +145 -0
  19. package/packages/nastech-ink/nastech-ink/src/ink/cache-eviction.ts +45 -0
  20. package/packages/nastech-ink/nastech-ink/src/ink/clearTerminal.ts +68 -0
  21. package/packages/nastech-ink/nastech-ink/src/ink/colorize.test.ts +60 -0
  22. package/packages/nastech-ink/nastech-ink/src/ink/colorize.ts +277 -0
  23. package/packages/nastech-ink/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
  24. package/packages/nastech-ink/nastech-ink/src/ink/components/App.tsx +830 -0
  25. package/packages/nastech-ink/nastech-ink/src/ink/components/AppContext.ts +20 -0
  26. package/packages/nastech-ink/nastech-ink/src/ink/components/Box.tsx +294 -0
  27. package/packages/nastech-ink/nastech-ink/src/ink/components/Button.tsx +236 -0
  28. package/packages/nastech-ink/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
  29. package/packages/nastech-ink/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
  30. package/packages/nastech-ink/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
  31. package/packages/nastech-ink/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
  32. package/packages/nastech-ink/nastech-ink/src/ink/components/Link.tsx +38 -0
  33. package/packages/nastech-ink/nastech-ink/src/ink/components/Newline.tsx +43 -0
  34. package/packages/nastech-ink/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
  35. package/packages/nastech-ink/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
  36. package/packages/nastech-ink/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
  37. package/packages/nastech-ink/nastech-ink/src/ink/components/Spacer.tsx +23 -0
  38. package/packages/nastech-ink/nastech-ink/src/ink/components/StdinContext.ts +25 -0
  39. package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
  40. package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
  41. package/packages/nastech-ink/nastech-ink/src/ink/components/Text.test.ts +38 -0
  42. package/packages/nastech-ink/nastech-ink/src/ink/components/Text.tsx +336 -0
  43. package/packages/nastech-ink/nastech-ink/src/ink/constants.ts +6 -0
  44. package/packages/nastech-ink/nastech-ink/src/ink/cursor.ts +5 -0
  45. package/packages/nastech-ink/nastech-ink/src/ink/devtools.ts +2 -0
  46. package/packages/nastech-ink/nastech-ink/src/ink/dom.ts +495 -0
  47. package/packages/nastech-ink/nastech-ink/src/ink/events/click-event.ts +38 -0
  48. package/packages/nastech-ink/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
  49. package/packages/nastech-ink/nastech-ink/src/ink/events/dispatcher.ts +242 -0
  50. package/packages/nastech-ink/nastech-ink/src/ink/events/emitter.ts +40 -0
  51. package/packages/nastech-ink/nastech-ink/src/ink/events/event-handlers.ts +84 -0
  52. package/packages/nastech-ink/nastech-ink/src/ink/events/event.ts +11 -0
  53. package/packages/nastech-ink/nastech-ink/src/ink/events/focus-event.ts +18 -0
  54. package/packages/nastech-ink/nastech-ink/src/ink/events/input-event.ts +176 -0
  55. package/packages/nastech-ink/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
  56. package/packages/nastech-ink/nastech-ink/src/ink/events/mouse-event.ts +18 -0
  57. package/packages/nastech-ink/nastech-ink/src/ink/events/paste-event.ts +10 -0
  58. package/packages/nastech-ink/nastech-ink/src/ink/events/resize-event.ts +12 -0
  59. package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-event.ts +107 -0
  60. package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
  61. package/packages/nastech-ink/nastech-ink/src/ink/focus.ts +219 -0
  62. package/packages/nastech-ink/nastech-ink/src/ink/frame.ts +124 -0
  63. package/packages/nastech-ink/nastech-ink/src/ink/get-max-width.ts +27 -0
  64. package/packages/nastech-ink/nastech-ink/src/ink/global.d.ts +1 -0
  65. package/packages/nastech-ink/nastech-ink/src/ink/hit-test.test.ts +38 -0
  66. package/packages/nastech-ink/nastech-ink/src/ink/hit-test.ts +224 -0
  67. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
  68. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-app.ts +9 -0
  69. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
  70. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
  71. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
  72. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-input.ts +95 -0
  73. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
  74. package/packages/nastech-ink/package.json +57 -0
  75. package/packages/nastech-ink/src/bootstrap/state.ts +9 -0
  76. package/packages/nastech-ink/src/entry-exports.ts +32 -0
  77. package/packages/nastech-ink/src/hooks/use-stderr.ts +15 -0
  78. package/packages/nastech-ink/src/hooks/use-stdout.ts +15 -0
  79. package/packages/nastech-ink/src/ink/Ansi.tsx +435 -0
  80. package/packages/nastech-ink/src/ink/app-mouse.test.ts +123 -0
  81. package/packages/nastech-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  82. package/packages/nastech-ink/src/ink/bidi.ts +145 -0
  83. package/packages/nastech-ink/src/ink/cache-eviction.ts +45 -0
  84. package/packages/nastech-ink/src/ink/clearTerminal.ts +68 -0
  85. package/packages/nastech-ink/src/ink/colorize.test.ts +60 -0
  86. package/packages/nastech-ink/src/ink/colorize.ts +277 -0
  87. package/packages/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
  88. package/packages/nastech-ink/src/ink/components/App.tsx +855 -0
  89. package/packages/nastech-ink/src/ink/components/AppContext.ts +20 -0
  90. package/packages/nastech-ink/src/ink/components/Box.tsx +294 -0
  91. package/packages/nastech-ink/src/ink/components/Button.tsx +236 -0
  92. package/packages/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
  93. package/packages/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
  94. package/packages/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
  95. package/packages/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
  96. package/packages/nastech-ink/src/ink/components/Link.tsx +38 -0
  97. package/packages/nastech-ink/src/ink/components/Newline.tsx +43 -0
  98. package/packages/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
  99. package/packages/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
  100. package/packages/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
  101. package/packages/nastech-ink/src/ink/components/Spacer.tsx +23 -0
  102. package/packages/nastech-ink/src/ink/components/StdinContext.ts +25 -0
  103. package/packages/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
  104. package/packages/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
  105. package/packages/nastech-ink/src/ink/components/Text.test.ts +38 -0
  106. package/packages/nastech-ink/src/ink/components/Text.tsx +336 -0
  107. package/packages/nastech-ink/src/ink/constants.ts +6 -0
  108. package/packages/nastech-ink/src/ink/cursor.ts +5 -0
  109. package/packages/nastech-ink/src/ink/devtools.ts +2 -0
  110. package/packages/nastech-ink/src/ink/dom.ts +495 -0
  111. package/packages/nastech-ink/src/ink/events/click-event.ts +38 -0
  112. package/packages/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
  113. package/packages/nastech-ink/src/ink/events/dispatcher.ts +242 -0
  114. package/packages/nastech-ink/src/ink/events/emitter.ts +40 -0
  115. package/packages/nastech-ink/src/ink/events/event-handlers.ts +84 -0
  116. package/packages/nastech-ink/src/ink/events/event.ts +11 -0
  117. package/packages/nastech-ink/src/ink/events/focus-event.ts +18 -0
  118. package/packages/nastech-ink/src/ink/events/input-event.ts +176 -0
  119. package/packages/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
  120. package/packages/nastech-ink/src/ink/events/mouse-event.ts +18 -0
  121. package/packages/nastech-ink/src/ink/events/paste-event.ts +10 -0
  122. package/packages/nastech-ink/src/ink/events/resize-event.ts +12 -0
  123. package/packages/nastech-ink/src/ink/events/terminal-event.ts +107 -0
  124. package/packages/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
  125. package/packages/nastech-ink/src/ink/focus.ts +219 -0
  126. package/packages/nastech-ink/src/ink/frame.ts +124 -0
  127. package/packages/nastech-ink/src/ink/get-max-width.ts +27 -0
  128. package/packages/nastech-ink/src/ink/global.d.ts +1 -0
  129. package/packages/nastech-ink/src/ink/hit-test.test.ts +38 -0
  130. package/packages/nastech-ink/src/ink/hit-test.ts +224 -0
  131. package/packages/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
  132. package/packages/nastech-ink/src/ink/hooks/use-app.ts +9 -0
  133. package/packages/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
  134. package/packages/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
  135. package/packages/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
  136. package/packages/nastech-ink/src/ink/hooks/use-input.ts +95 -0
  137. package/packages/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
  138. package/packages/nastech-ink/src/ink/hooks/use-search-highlight.ts +56 -0
  139. package/packages/nastech-ink/src/ink/hooks/use-selection.ts +101 -0
  140. package/packages/nastech-ink/src/ink/hooks/use-stdin.ts +9 -0
  141. package/packages/nastech-ink/src/ink/hooks/use-tab-status.ts +71 -0
  142. package/packages/nastech-ink/src/ink/hooks/use-terminal-focus.ts +18 -0
  143. package/packages/nastech-ink/src/ink/hooks/use-terminal-title.ts +34 -0
  144. package/packages/nastech-ink/src/ink/hooks/use-terminal-viewport.ts +100 -0
  145. package/packages/nastech-ink/src/ink/hyperlinkHover.ts +52 -0
  146. package/packages/nastech-ink/src/ink/ink-cursor-advance.test.ts +234 -0
  147. package/packages/nastech-ink/src/ink/ink-resize.test.ts +50 -0
  148. package/packages/nastech-ink/src/ink/ink.tsx +2705 -0
  149. package/packages/nastech-ink/src/ink/instances.ts +10 -0
  150. package/packages/nastech-ink/src/ink/layout/engine.ts +6 -0
  151. package/packages/nastech-ink/src/ink/layout/geometry.ts +98 -0
  152. package/packages/nastech-ink/src/ink/layout/node.ts +145 -0
  153. package/packages/nastech-ink/src/ink/layout/yoga.ts +313 -0
  154. package/packages/nastech-ink/src/ink/line-width-cache.ts +38 -0
  155. package/packages/nastech-ink/src/ink/log-update.test.ts +223 -0
  156. package/packages/nastech-ink/src/ink/log-update.ts +752 -0
  157. package/packages/nastech-ink/src/ink/lru.ts +14 -0
  158. package/packages/nastech-ink/src/ink/measure-element.ts +23 -0
  159. package/packages/nastech-ink/src/ink/measure-text.ts +50 -0
  160. package/packages/nastech-ink/src/ink/node-cache.ts +53 -0
  161. package/packages/nastech-ink/src/ink/optimizer.ts +99 -0
  162. package/packages/nastech-ink/src/ink/output.ts +845 -0
  163. package/packages/nastech-ink/src/ink/parse-keypress.test.ts +133 -0
  164. package/packages/nastech-ink/src/ink/parse-keypress.ts +848 -0
  165. package/packages/nastech-ink/src/ink/reconciler.ts +382 -0
  166. package/packages/nastech-ink/src/ink/render-border.ts +206 -0
  167. package/packages/nastech-ink/src/ink/render-node-to-output.ts +1582 -0
  168. package/packages/nastech-ink/src/ink/render-to-screen.ts +236 -0
  169. package/packages/nastech-ink/src/ink/renderer.ts +169 -0
  170. package/packages/nastech-ink/src/ink/root.ts +204 -0
  171. package/packages/nastech-ink/src/ink/screen.ts +1590 -0
  172. package/packages/nastech-ink/src/ink/searchHighlight.ts +91 -0
  173. package/packages/nastech-ink/src/ink/selection.test.ts +82 -0
  174. package/packages/nastech-ink/src/ink/selection.ts +1143 -0
  175. package/packages/nastech-ink/src/ink/squash-text-nodes.ts +74 -0
  176. package/packages/nastech-ink/src/ink/stringWidth.ts +341 -0
  177. package/packages/nastech-ink/src/ink/styles.ts +750 -0
  178. package/packages/nastech-ink/src/ink/supports-hyperlinks.ts +51 -0
  179. package/packages/nastech-ink/src/ink/tabstops.ts +44 -0
  180. package/packages/nastech-ink/src/ink/terminal-focus-state.ts +52 -0
  181. package/packages/nastech-ink/src/ink/terminal-querier.ts +222 -0
  182. package/packages/nastech-ink/src/ink/terminal.test.ts +15 -0
  183. package/packages/nastech-ink/src/ink/terminal.ts +299 -0
  184. package/packages/nastech-ink/src/ink/termio/ansi.ts +75 -0
  185. package/packages/nastech-ink/src/ink/termio/csi.ts +334 -0
  186. package/packages/nastech-ink/src/ink/termio/dec.ts +99 -0
  187. package/packages/nastech-ink/src/ink/termio/esc.ts +69 -0
  188. package/packages/nastech-ink/src/ink/termio/osc.test.ts +191 -0
  189. package/packages/nastech-ink/src/ink/termio/osc.ts +724 -0
  190. package/packages/nastech-ink/src/ink/termio/parser.ts +467 -0
  191. package/packages/nastech-ink/src/ink/termio/sgr.ts +362 -0
  192. package/packages/nastech-ink/src/ink/termio/tokenize.test.ts +185 -0
  193. package/packages/nastech-ink/src/ink/termio/tokenize.ts +350 -0
  194. package/packages/nastech-ink/src/ink/termio/types.ts +230 -0
  195. package/packages/nastech-ink/src/ink/termio.ts +42 -0
  196. package/packages/nastech-ink/src/ink/useTerminalNotification.ts +110 -0
  197. package/packages/nastech-ink/src/ink/warn.ts +15 -0
  198. package/packages/nastech-ink/src/ink/widest-line.ts +22 -0
  199. package/packages/nastech-ink/src/ink/wrap-text.test.ts +17 -0
  200. package/packages/nastech-ink/src/ink/wrap-text.ts +144 -0
  201. package/packages/nastech-ink/src/ink/wrapAnsi.ts +13 -0
  202. package/packages/nastech-ink/src/native-ts/yoga-layout/enums.ts +112 -0
  203. package/packages/nastech-ink/src/native-ts/yoga-layout/index.ts +2326 -0
  204. package/packages/nastech-ink/src/utils/debug.ts +6 -0
  205. package/packages/nastech-ink/src/utils/earlyInput.ts +131 -0
  206. package/packages/nastech-ink/src/utils/env.ts +66 -0
  207. package/packages/nastech-ink/src/utils/envUtils.ts +13 -0
  208. package/packages/nastech-ink/src/utils/execFileNoThrow.test.ts +146 -0
  209. package/packages/nastech-ink/src/utils/execFileNoThrow.ts +115 -0
  210. package/packages/nastech-ink/src/utils/fullscreen.ts +3 -0
  211. package/packages/nastech-ink/src/utils/intl.ts +87 -0
  212. package/packages/nastech-ink/src/utils/log.ts +7 -0
  213. package/packages/nastech-ink/src/utils/semver.ts +57 -0
  214. package/packages/nastech-ink/src/utils/sliceAnsi.ts +106 -0
  215. package/packages/nastech-ink/text-input.d.ts +2 -0
  216. package/packages/nastech-ink/text-input.js +1 -0
  217. package/scripts/build.mjs +61 -0
  218. package/scripts/profile-tui.mjs +121 -0
  219. package/src/__tests__/activeSessionSwitcher.test.ts +157 -0
  220. package/src/__tests__/appChromeStatusRule.test.tsx +84 -0
  221. package/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  222. package/src/__tests__/approvalAction.test.ts +50 -0
  223. package/src/__tests__/asCommandDispatch.test.ts +27 -0
  224. package/src/__tests__/blockLayout.test.ts +122 -0
  225. package/src/__tests__/clipboard.test.ts +369 -0
  226. package/src/__tests__/constants.test.ts +53 -0
  227. package/src/__tests__/createGatewayEventHandler.test.ts +1091 -0
  228. package/src/__tests__/createSlashHandler.test.ts +822 -0
  229. package/src/__tests__/creditsCommand.test.ts +144 -0
  230. package/src/__tests__/cursorDriftRegression.test.ts +114 -0
  231. package/src/__tests__/details.test.ts +115 -0
  232. package/src/__tests__/emoji.test.ts +64 -0
  233. package/src/__tests__/externalLink.test.ts +144 -0
  234. package/src/__tests__/forceTruecolor.test.ts +191 -0
  235. package/src/__tests__/gatewayClient.test.ts +394 -0
  236. package/src/__tests__/gatewayRecovery.test.ts +47 -0
  237. package/src/__tests__/markdown.test.ts +331 -0
  238. package/src/__tests__/mathUnicode.test.ts +293 -0
  239. package/src/__tests__/memoryMonitor.test.ts +102 -0
  240. package/src/__tests__/messageLine.test.ts +19 -0
  241. package/src/__tests__/messages.test.ts +92 -0
  242. package/src/__tests__/orchestratorPromptSession.test.ts +64 -0
  243. package/src/__tests__/osc52.test.ts +67 -0
  244. package/src/__tests__/parentLog.test.ts +75 -0
  245. package/src/__tests__/paths.test.ts +70 -0
  246. package/src/__tests__/platform.test.ts +556 -0
  247. package/src/__tests__/precisionWheel.test.ts +44 -0
  248. package/src/__tests__/prompt.test.ts +31 -0
  249. package/src/__tests__/providers.test.ts +65 -0
  250. package/src/__tests__/reasoning.test.ts +76 -0
  251. package/src/__tests__/rpc.test.ts +27 -0
  252. package/src/__tests__/scroll.test.ts +99 -0
  253. package/src/__tests__/slashParity.test.ts +123 -0
  254. package/src/__tests__/spawnHistoryStore.test.ts +46 -0
  255. package/src/__tests__/stateIsolation.test.ts +46 -0
  256. package/src/__tests__/statusBarTicker.test.ts +18 -0
  257. package/src/__tests__/statusRule.test.ts +32 -0
  258. package/src/__tests__/streamingMarkdown.test.ts +121 -0
  259. package/src/__tests__/subagentTree.test.ts +407 -0
  260. package/src/__tests__/syntax.test.ts +45 -0
  261. package/src/__tests__/terminalModes.test.ts +39 -0
  262. package/src/__tests__/terminalParity.test.ts +77 -0
  263. package/src/__tests__/terminalSetup.test.ts +386 -0
  264. package/src/__tests__/termux.test.ts +35 -0
  265. package/src/__tests__/termuxComposerLayout.test.ts +40 -0
  266. package/src/__tests__/text.test.ts +233 -0
  267. package/src/__tests__/textInputBurstInput.test.ts +40 -0
  268. package/src/__tests__/textInputCursorSourceOfTruth.test.ts +50 -0
  269. package/src/__tests__/textInputFastEcho.test.ts +200 -0
  270. package/src/__tests__/textInputLineNav.test.ts +55 -0
  271. package/src/__tests__/textInputPassThrough.test.ts +59 -0
  272. package/src/__tests__/textInputRightClick.test.ts +48 -0
  273. package/src/__tests__/textInputWrap.test.ts +151 -0
  274. package/src/__tests__/theme.test.ts +311 -0
  275. package/src/__tests__/turnControllerNotice.test.ts +43 -0
  276. package/src/__tests__/turnStore.test.ts +66 -0
  277. package/src/__tests__/useCompletion.test.ts +35 -0
  278. package/src/__tests__/useComposerState.test.ts +59 -0
  279. package/src/__tests__/useConfigSync.test.ts +460 -0
  280. package/src/__tests__/useInputHandlers.test.ts +77 -0
  281. package/src/__tests__/useQueue.test.ts +28 -0
  282. package/src/__tests__/useSessionLifecycle.test.ts +60 -0
  283. package/src/__tests__/useVirtualHistoryHeights.test.ts +39 -0
  284. package/src/__tests__/viewport.test.ts +58 -0
  285. package/src/__tests__/viewportStore.test.ts +85 -0
  286. package/src/__tests__/virtualHeights.test.ts +96 -0
  287. package/src/__tests__/virtualHistoryClamp.test.ts +19 -0
  288. package/src/__tests__/virtualHistoryOffsetCache.test.ts +282 -0
  289. package/src/__tests__/wheelAccel.test.ts +138 -0
  290. package/src/app/createGatewayEventHandler.ts +833 -0
  291. package/src/app/createSlashHandler.ts +130 -0
  292. package/src/app/delegationStore.ts +77 -0
  293. package/src/app/gatewayContext.tsx +19 -0
  294. package/src/app/gatewayRecovery.ts +35 -0
  295. package/src/app/inputSelectionStore.ts +15 -0
  296. package/src/app/interfaces.ts +394 -0
  297. package/src/app/overlayStore.ts +53 -0
  298. package/src/app/scroll.ts +71 -0
  299. package/src/app/setupHandoff.ts +54 -0
  300. package/src/app/slash/commands/core.ts +648 -0
  301. package/src/app/slash/commands/credits.ts +57 -0
  302. package/src/app/slash/commands/debug.ts +48 -0
  303. package/src/app/slash/commands/ops.ts +717 -0
  304. package/src/app/slash/commands/session.ts +554 -0
  305. package/src/app/slash/commands/setup.ts +20 -0
  306. package/src/app/slash/registry.ts +20 -0
  307. package/src/app/slash/types.ts +21 -0
  308. package/src/app/spawnHistoryStore.ts +159 -0
  309. package/src/app/turnController.ts +866 -0
  310. package/src/app/turnStore.ts +85 -0
  311. package/src/app/uiStore.ts +44 -0
  312. package/src/app/useComposerState.ts +367 -0
  313. package/src/app/useConfigSync.ts +288 -0
  314. package/src/app/useInputHandlers.ts +576 -0
  315. package/src/app/useLongRunToolCharms.ts +69 -0
  316. package/src/app/useMainApp.ts +1039 -0
  317. package/src/app/useSessionLifecycle.ts +366 -0
  318. package/src/app/useSubmission.ts +429 -0
  319. package/src/app.tsx +25 -0
  320. package/src/banner.ts +93 -0
  321. package/src/components/activeSessionSwitcher.tsx +635 -0
  322. package/src/components/agentsOverlay.tsx +1073 -0
  323. package/src/components/appChrome.tsx +554 -0
  324. package/src/components/appLayout.tsx +444 -0
  325. package/src/components/appOverlays.tsx +254 -0
  326. package/src/components/branding.tsx +466 -0
  327. package/src/components/fpsOverlay.tsx +30 -0
  328. package/src/components/helpHint.tsx +73 -0
  329. package/src/components/markdown.tsx +1119 -0
  330. package/src/components/maskedPrompt.tsx +34 -0
  331. package/src/components/messageLine.tsx +237 -0
  332. package/src/components/modelPicker.tsx +527 -0
  333. package/src/components/overlayControls.tsx +50 -0
  334. package/src/components/pluginsHub.tsx +238 -0
  335. package/src/components/prompts.tsx +276 -0
  336. package/src/components/queuedMessages.tsx +64 -0
  337. package/src/components/sessionPicker.tsx +227 -0
  338. package/src/components/skillsHub.tsx +308 -0
  339. package/src/components/streamingAssistant.tsx +110 -0
  340. package/src/components/streamingMarkdown.tsx +174 -0
  341. package/src/components/textInput.tsx +1340 -0
  342. package/src/components/themed.tsx +30 -0
  343. package/src/components/thinking.tsx +1224 -0
  344. package/src/components/todoPanel.tsx +93 -0
  345. package/src/config/env.ts +64 -0
  346. package/src/config/limits.ts +13 -0
  347. package/src/config/timing.ts +6 -0
  348. package/src/content/charms.ts +1 -0
  349. package/src/content/faces.ts +17 -0
  350. package/src/content/fortunes.ts +30 -0
  351. package/src/content/hotkeys.ts +37 -0
  352. package/src/content/placeholders.ts +13 -0
  353. package/src/content/setup.ts +17 -0
  354. package/src/content/verbs.ts +38 -0
  355. package/src/domain/blockLayout.ts +146 -0
  356. package/src/domain/details.ts +76 -0
  357. package/src/domain/messages.ts +91 -0
  358. package/src/domain/paths.ts +16 -0
  359. package/src/domain/providers.ts +11 -0
  360. package/src/domain/roles.ts +9 -0
  361. package/src/domain/slash.ts +10 -0
  362. package/src/domain/usage.ts +3 -0
  363. package/src/domain/viewport.ts +51 -0
  364. package/src/entry.tsx +104 -0
  365. package/src/gatewayClient.ts +730 -0
  366. package/src/gatewayTypes.ts +568 -0
  367. package/src/hooks/useCompletion.ts +112 -0
  368. package/src/hooks/useGitBranch.ts +72 -0
  369. package/src/hooks/useInputHistory.ts +11 -0
  370. package/src/hooks/useQueue.ts +76 -0
  371. package/src/hooks/useVirtualHistory.ts +554 -0
  372. package/src/lib/circularBuffer.ts +48 -0
  373. package/src/lib/clipboard.ts +182 -0
  374. package/src/lib/editor.test.ts +74 -0
  375. package/src/lib/editor.ts +47 -0
  376. package/src/lib/emoji.ts +55 -0
  377. package/src/lib/externalCli.ts +16 -0
  378. package/src/lib/externalLink.ts +435 -0
  379. package/src/lib/forceTruecolor.ts +60 -0
  380. package/src/lib/fpsStore.ts +51 -0
  381. package/src/lib/fuzzy.test.ts +109 -0
  382. package/src/lib/fuzzy.ts +177 -0
  383. package/src/lib/gracefulExit.ts +47 -0
  384. package/src/lib/history.ts +82 -0
  385. package/src/lib/inputMetrics.ts +203 -0
  386. package/src/lib/liveProgress.test.ts +116 -0
  387. package/src/lib/liveProgress.ts +79 -0
  388. package/src/lib/mathUnicode.ts +770 -0
  389. package/src/lib/memory.test.ts +155 -0
  390. package/src/lib/memory.ts +188 -0
  391. package/src/lib/memoryMonitor.ts +109 -0
  392. package/src/lib/messages.test.ts +29 -0
  393. package/src/lib/messages.ts +8 -0
  394. package/src/lib/openExternalUrl.test.ts +217 -0
  395. package/src/lib/openExternalUrl.ts +158 -0
  396. package/src/lib/osc52.ts +73 -0
  397. package/src/lib/parentLog.ts +57 -0
  398. package/src/lib/perfPane.tsx +107 -0
  399. package/src/lib/platform.ts +409 -0
  400. package/src/lib/precisionWheel.ts +48 -0
  401. package/src/lib/prompt.ts +35 -0
  402. package/src/lib/reasoning.ts +55 -0
  403. package/src/lib/rpc.ts +41 -0
  404. package/src/lib/subagentTree.ts +355 -0
  405. package/src/lib/syntax.ts +117 -0
  406. package/src/lib/terminalModes.ts +51 -0
  407. package/src/lib/terminalParity.ts +78 -0
  408. package/src/lib/terminalSetup.ts +444 -0
  409. package/src/lib/termux.ts +29 -0
  410. package/src/lib/text.test.ts +18 -0
  411. package/src/lib/text.ts +339 -0
  412. package/src/lib/todo.test.ts +21 -0
  413. package/src/lib/todo.ts +9 -0
  414. package/src/lib/viewportStore.ts +124 -0
  415. package/src/lib/virtualHeights.ts +145 -0
  416. package/src/lib/wheelAccel.ts +190 -0
  417. package/src/protocol/interpolation.ts +3 -0
  418. package/src/protocol/paste.ts +1 -0
  419. package/src/theme.ts +589 -0
  420. package/src/types/nastech-ink.d.ts +176 -0
  421. package/src/types.ts +212 -0
  422. package/tsconfig.build.json +9 -0
  423. package/tsconfig.json +19 -0
  424. package/vitest.config.ts +7 -0
@@ -0,0 +1,1073 @@
1
+ import { Box, NoSelect, ScrollBox, type ScrollBoxHandle, Text, useInput, useStdout } from '@nastechai/ink'
2
+ import { useStore } from '@nanostores/react'
3
+ import { type ReactNode, type RefObject, useEffect, useMemo, useRef, useState } from 'react'
4
+
5
+ import {
6
+ $delegationState,
7
+ $overlaySectionsOpen,
8
+ applyDelegationStatus,
9
+ toggleOverlaySection
10
+ } from '../app/delegationStore.js'
11
+ import { patchOverlayState } from '../app/overlayStore.js'
12
+ import { $spawnDiff, $spawnHistory, clearDiffPair, type SpawnSnapshot } from '../app/spawnHistoryStore.js'
13
+ import { useTurnSelector } from '../app/turnStore.js'
14
+ import type { GatewayClient } from '../gatewayClient.js'
15
+ import type { DelegationPauseResponse, DelegationStatusResponse, SubagentInterruptResponse } from '../gatewayTypes.js'
16
+ import { asRpcResult } from '../lib/rpc.js'
17
+ import {
18
+ buildSubagentTree,
19
+ descendantIds,
20
+ flattenTree,
21
+ fmtCost,
22
+ fmtDuration,
23
+ fmtTokens,
24
+ formatSummary,
25
+ hotnessBucket,
26
+ peakHotness,
27
+ sparkline,
28
+ topLevelSubagents,
29
+ treeTotals,
30
+ widthByDepth
31
+ } from '../lib/subagentTree.js'
32
+ import { compactPreview } from '../lib/text.js'
33
+ import type { Theme } from '../theme.js'
34
+ import type { SubagentNode, SubagentProgress } from '../types.js'
35
+
36
+ // ── Types + lookup tables ────────────────────────────────────────────
37
+
38
+ type SortMode = 'depth-first' | 'duration-desc' | 'status' | 'tools-desc'
39
+ type FilterMode = 'all' | 'failed' | 'leaf' | 'running'
40
+ type Status = SubagentProgress['status']
41
+
42
+ const SORT_ORDER: readonly SortMode[] = ['depth-first', 'tools-desc', 'duration-desc', 'status']
43
+ const FILTER_ORDER: readonly FilterMode[] = ['all', 'running', 'failed', 'leaf']
44
+
45
+ const SORT_LABEL: Record<SortMode, string> = {
46
+ 'depth-first': 'spawn order',
47
+ 'duration-desc': 'slowest',
48
+ status: 'status',
49
+ 'tools-desc': 'busiest'
50
+ }
51
+
52
+ const FILTER_LABEL: Record<FilterMode, string> = {
53
+ all: 'all',
54
+ failed: 'failed',
55
+ leaf: 'leaves',
56
+ running: 'running'
57
+ }
58
+
59
+ const STATUS_RANK: Record<Status, number> = {
60
+ error: 0,
61
+ failed: 0,
62
+ interrupted: 1,
63
+ timeout: 1,
64
+ running: 2,
65
+ queued: 3,
66
+ completed: 4
67
+ }
68
+
69
+ const statusRank = (status: string): number => STATUS_RANK[status as Status] ?? STATUS_RANK.error
70
+
71
+ const SORT_COMPARATORS: Record<SortMode, (a: SubagentNode, b: SubagentNode) => number> = {
72
+ 'depth-first': (a, b) => a.item.depth - b.item.depth || a.item.index - b.item.index,
73
+ 'tools-desc': (a, b) => b.aggregate.totalTools - a.aggregate.totalTools,
74
+ 'duration-desc': (a, b) => b.aggregate.totalDuration - a.aggregate.totalDuration,
75
+ status: (a, b) => statusRank(a.item.status) - statusRank(b.item.status)
76
+ }
77
+
78
+ const FILTER_PREDICATES: Record<FilterMode, (n: SubagentNode) => boolean> = {
79
+ all: () => true,
80
+ leaf: n => n.children.length === 0,
81
+ running: n => n.item.status === 'running' || n.item.status === 'queued',
82
+ failed: n =>
83
+ n.item.status === 'error' ||
84
+ n.item.status === 'failed' ||
85
+ n.item.status === 'interrupted' ||
86
+ n.item.status === 'timeout'
87
+ }
88
+
89
+ const STATUS_GLYPH: Record<Status, { color: (t: Theme) => string; glyph: string }> = {
90
+ running: { color: t => t.color.accent, glyph: '●' },
91
+ queued: { color: t => t.color.muted, glyph: '○' },
92
+ completed: { color: t => t.color.statusGood, glyph: '✓' },
93
+ interrupted: { color: t => t.color.warn, glyph: '■' },
94
+ failed: { color: t => t.color.error, glyph: '✗' },
95
+ timeout: { color: t => t.color.warn, glyph: '⌛' },
96
+ error: { color: t => t.color.error, glyph: '⚠' }
97
+ }
98
+
99
+ // Heatmap palette — cold → hot, resolved against the active theme.
100
+ const heatPalette = (t: Theme) => [t.color.border, t.color.accent, t.color.primary, t.color.warn, t.color.error]
101
+
102
+ // ── Pure helpers ─────────────────────────────────────────────────────
103
+
104
+ const fmtDur = (seconds?: number) => (seconds == null || seconds <= 0 ? '' : fmtDuration(seconds))
105
+ const fmtElapsedLabel = (seconds: number) => (seconds < 0 ? '' : fmtDuration(seconds))
106
+
107
+ const displayElapsedSeconds = (item: SubagentProgress, nowMs: number): number | null => {
108
+ if (item.durationSeconds != null) {
109
+ return item.durationSeconds
110
+ }
111
+
112
+ if (item.startedAt != null && (item.status === 'running' || item.status === 'queued')) {
113
+ return Math.max(0, (nowMs - item.startedAt) / 1000)
114
+ }
115
+
116
+ return null
117
+ }
118
+
119
+ const indentFor = (depth: number): string => ' '.repeat(Math.max(0, depth))
120
+ const formatRowId = (n: number): string => String(n + 1).padStart(2, ' ')
121
+ const cycle = <T,>(order: readonly T[], current: T): T => order[(order.indexOf(current) + 1) % order.length]!
122
+
123
+ const statusGlyph = (item: SubagentProgress, t: Theme) => {
124
+ // Defensive fallback for cross-version snapshots with unknown statuses.
125
+ const g = STATUS_GLYPH[item.status] ?? STATUS_GLYPH.error
126
+
127
+ return { color: g.color(t), glyph: g.glyph }
128
+ }
129
+
130
+ const prepareRows = (tree: SubagentNode[], sort: SortMode, filter: FilterMode): SubagentNode[] =>
131
+ tree.length === 0 ? [] : flattenTree([...tree].sort(SORT_COMPARATORS[sort])).filter(FILTER_PREDICATES[filter])
132
+
133
+ const diffMetricLine = (name: string, a: number, b: number, fmt: (n: number) => string) => {
134
+ const d = b - a
135
+ const sign = d === 0 ? '' : d > 0 ? '+' : '-'
136
+
137
+ return `${name}: ${fmt(a)} → ${fmt(b)} (${sign}${fmt(Math.abs(d)) || '0'})`
138
+ }
139
+
140
+ // ── Sub-components ───────────────────────────────────────────────────
141
+
142
+ /** Polled on parent `tick` so accordions can resize the thumb without a scroll event. */
143
+ function OverlayScrollbar({
144
+ scrollRef,
145
+ t,
146
+ tick
147
+ }: {
148
+ scrollRef: RefObject<null | ScrollBoxHandle>
149
+ t: Theme
150
+ tick: number
151
+ }) {
152
+ void tick // ensures re-render when the parent clock advances
153
+
154
+ const [hover, setHover] = useState(false)
155
+ const [grab, setGrab] = useState<null | number>(null)
156
+
157
+ const s = scrollRef.current
158
+ const vp = Math.max(0, s?.getViewportHeight() ?? 0)
159
+
160
+ if (!vp) {
161
+ return <Box width={1} />
162
+ }
163
+
164
+ const total = Math.max(vp, s?.getScrollHeight() ?? vp)
165
+ const scrollable = total > vp
166
+ const thumb = scrollable ? Math.max(1, Math.round((vp * vp) / total)) : vp
167
+ const travel = Math.max(1, vp - thumb)
168
+ const pos = Math.max(0, (s?.getScrollTop() ?? 0) + (s?.getPendingDelta() ?? 0))
169
+ const thumbTop = scrollable ? Math.round((pos / Math.max(1, total - vp)) * travel) : 0
170
+ const below = Math.max(0, vp - thumbTop - thumb)
171
+
172
+ const vBar = (n: number) => (n > 0 ? `${'│\n'.repeat(n - 1)}│` : '')
173
+ const thumbBody = `${'┃\n'.repeat(Math.max(0, thumb - 1))}┃`
174
+ const thumbColor = grab !== null ? t.color.primary : t.color.accent
175
+ const trackColor = hover ? t.color.border : t.color.muted
176
+
177
+ const jump = (row: number, offset: number) => {
178
+ if (!s || !scrollable) {
179
+ return
180
+ }
181
+
182
+ s.scrollTo(Math.round((Math.max(0, Math.min(travel, row - offset)) / travel) * Math.max(0, total - vp)))
183
+ }
184
+
185
+ return (
186
+ <Box
187
+ flexDirection="column"
188
+ onMouseDown={(e: { localRow?: number }) => {
189
+ const row = Math.max(0, Math.min(vp - 1, e.localRow ?? 0))
190
+ const off = row >= thumbTop && row < thumbTop + thumb ? row - thumbTop : Math.floor(thumb / 2)
191
+ setGrab(off)
192
+ jump(row, off)
193
+ }}
194
+ onMouseDrag={(e: { localRow?: number }) =>
195
+ jump(Math.max(0, Math.min(vp - 1, e.localRow ?? 0)), grab ?? Math.floor(thumb / 2))
196
+ }
197
+ onMouseEnter={() => setHover(true)}
198
+ onMouseLeave={() => setHover(false)}
199
+ onMouseUp={() => setGrab(null)}
200
+ width={1}
201
+ >
202
+ {!scrollable ? (
203
+ <Text color={trackColor} dim>
204
+ {vBar(vp)}
205
+ </Text>
206
+ ) : (
207
+ <>
208
+ {thumbTop > 0 ? (
209
+ <Text color={trackColor} dim={!hover}>
210
+ {vBar(thumbTop)}
211
+ </Text>
212
+ ) : null}
213
+
214
+ <Text color={thumbColor}>{thumbBody}</Text>
215
+
216
+ {below > 0 ? (
217
+ <Text color={trackColor} dim={!hover}>
218
+ {vBar(below)}
219
+ </Text>
220
+ ) : null}
221
+ </>
222
+ )}
223
+ </Box>
224
+ )
225
+ }
226
+
227
+ function GanttStrip({
228
+ cols,
229
+ cursor,
230
+ flatNodes,
231
+ maxRows,
232
+ now,
233
+ t
234
+ }: {
235
+ cols: number
236
+ cursor: number
237
+ flatNodes: SubagentNode[]
238
+ maxRows: number
239
+ now: number
240
+ t: Theme
241
+ }) {
242
+ const spans = flatNodes
243
+ .map((node, idx) => {
244
+ const started = node.item.startedAt ?? now
245
+
246
+ const ended =
247
+ node.item.durationSeconds != null && node.item.startedAt != null
248
+ ? node.item.startedAt + node.item.durationSeconds * 1000
249
+ : now
250
+
251
+ return { endAt: ended, idx, node, startAt: started }
252
+ })
253
+ .filter(s => s.endAt >= s.startAt)
254
+
255
+ if (!spans.length) {
256
+ return null
257
+ }
258
+
259
+ const globalStart = Math.min(...spans.map(s => s.startAt))
260
+ const globalEnd = Math.max(...spans.map(s => s.endAt))
261
+ const totalSpan = Math.max(1, globalEnd - globalStart)
262
+ const totalSeconds = (globalEnd - globalStart) / 1000
263
+
264
+ // 5-col id gutter (" 12 ") so the bar doesn't press against the id.
265
+ // 10-col right reserve: pad + up to `12m 30s`-style label without
266
+ // truncate-end against a full-width bar.
267
+ const idGutter = 5
268
+ const labelReserve = 10
269
+ const barWidth = Math.max(10, cols - idGutter - labelReserve)
270
+ const startIdx = Math.max(0, Math.min(Math.max(0, spans.length - maxRows), cursor - Math.floor(maxRows / 2)))
271
+ const shown = spans.slice(startIdx, startIdx + maxRows)
272
+
273
+ const bar = (startAt: number, endAt: number) => {
274
+ const s = Math.floor(((startAt - globalStart) / totalSpan) * barWidth)
275
+ const e = Math.min(barWidth, Math.ceil(((endAt - globalStart) / totalSpan) * barWidth))
276
+ const fill = Math.max(1, e - s)
277
+
278
+ return ' '.repeat(s) + '█'.repeat(fill) + ' '.repeat(Math.max(0, barWidth - s - fill))
279
+ }
280
+
281
+ const charStep = totalSeconds < 20 && barWidth > 20 ? 5 : 10
282
+
283
+ const ruler = Array.from({ length: barWidth }, (_, i) => {
284
+ if (i > 0 && i % 10 === 0) {
285
+ return '┼'
286
+ }
287
+
288
+ if (i > 0 && i % 5 === 0) {
289
+ return '·'
290
+ }
291
+
292
+ return '─'
293
+ }).join('')
294
+
295
+ const rulerLabels = (() => {
296
+ const chars = new Array(barWidth).fill(' ')
297
+
298
+ for (let pos = 0; pos < barWidth; pos += charStep) {
299
+ const secs = (pos / barWidth) * totalSeconds
300
+ const label = pos === 0 ? '0' : secs >= 1 ? `${Math.round(secs)}s` : `${secs.toFixed(1)}s`
301
+
302
+ for (let j = 0; j < label.length && pos + j < barWidth; j++) {
303
+ chars[pos + j] = label[j]!
304
+ }
305
+ }
306
+
307
+ return chars.join('')
308
+ })()
309
+
310
+ const windowLabel =
311
+ spans.length > maxRows ? ` (${startIdx + 1}-${Math.min(spans.length, startIdx + maxRows)}/${spans.length})` : ''
312
+
313
+ return (
314
+ <Box flexDirection="column" marginBottom={1}>
315
+ <Text color={t.color.muted}>
316
+ Timeline · {fmtElapsedLabel(Math.max(0, totalSeconds))}
317
+ {windowLabel}
318
+ </Text>
319
+
320
+ {shown.map(({ endAt, idx, node, startAt }) => {
321
+ const active = idx === cursor
322
+ const { color } = statusGlyph(node.item, t)
323
+ const accent = active ? t.color.accent : t.color.muted
324
+
325
+ const elSec = displayElapsedSeconds(node.item, now)
326
+ const elLabel = elSec != null ? fmtElapsedLabel(elSec) : ''
327
+
328
+ return (
329
+ <Text key={node.item.id} wrap="truncate-end">
330
+ <Text bold={active} color={accent}>
331
+ {formatRowId(idx)}
332
+ {' '}
333
+ </Text>
334
+
335
+ <Text color={active ? t.color.accent : color}>{bar(startAt, endAt)}</Text>
336
+
337
+ {elLabel ? (
338
+ <Text color={accent}>
339
+ {' '}
340
+ {elLabel}
341
+ </Text>
342
+ ) : null}
343
+ </Text>
344
+ )
345
+ })}
346
+
347
+ <Text color={t.color.muted} dim>
348
+ {' '}
349
+ {ruler}
350
+ </Text>
351
+
352
+ {totalSeconds > 0 ? (
353
+ <Text color={t.color.muted} dim>
354
+ {' '}
355
+ {rulerLabels}
356
+ </Text>
357
+ ) : null}
358
+ </Box>
359
+ )
360
+ }
361
+
362
+ function OverlaySection({
363
+ children,
364
+ count,
365
+ defaultOpen = false,
366
+ title,
367
+ t
368
+ }: {
369
+ children: ReactNode
370
+ count?: number
371
+ defaultOpen?: boolean
372
+ title: string
373
+ t: Theme
374
+ }) {
375
+ const openMap = useStore($overlaySectionsOpen)
376
+ const open = title in openMap ? openMap[title]! : defaultOpen
377
+
378
+ return (
379
+ <Box flexDirection="column" marginTop={1}>
380
+ <Box onClick={() => toggleOverlaySection(title, defaultOpen)}>
381
+ <Text color={t.color.label}>
382
+ <Text color={t.color.accent}>{open ? '▾ ' : '▸ '}</Text>
383
+ {title}
384
+ {typeof count === 'number' ? ` (${count})` : ''}
385
+ </Text>
386
+ </Box>
387
+
388
+ {open ? <Box flexDirection="column">{children}</Box> : null}
389
+ </Box>
390
+ )
391
+ }
392
+
393
+ function Field({ name, t, value }: { name: string; t: Theme; value: ReactNode }) {
394
+ return (
395
+ <Text wrap="truncate-end">
396
+ <Text color={t.color.label}>{name} · </Text>
397
+ <Text color={t.color.text}>{value}</Text>
398
+ </Text>
399
+ )
400
+ }
401
+
402
+ function Detail({ id, node, t }: { id?: string; node: SubagentNode; t: Theme }) {
403
+ const { aggregate: agg, item } = node
404
+ const { color, glyph } = statusGlyph(item, t)
405
+
406
+ const inputTokens = item.inputTokens ?? 0
407
+ const outputTokens = item.outputTokens ?? 0
408
+ const localTokens = inputTokens + outputTokens
409
+ const subtreeTokens = agg.inputTokens + agg.outputTokens - localTokens
410
+ const localCost = item.costUsd ?? 0
411
+ const subtreeCost = agg.costUsd - localCost
412
+
413
+ const filesRead = item.filesRead ?? []
414
+ const filesWritten = item.filesWritten ?? []
415
+ const outputTail = item.outputTail ?? []
416
+ // Tool calls: prefer the live stream; for archived / post-turn views
417
+ // that stream is often empty even when tool_count > 0, so fall back to
418
+ // the tool names captured in outputTail at subagent.complete time.
419
+ const toolLines = item.tools.length > 0 ? item.tools : outputTail.map(e => e.tool).filter(Boolean)
420
+
421
+ const filesOverflow = Math.max(0, filesRead.length - 8) + Math.max(0, filesWritten.length - 8)
422
+
423
+ return (
424
+ <Box flexDirection="column">
425
+ <Text bold color={t.color.text} wrap="wrap">
426
+ {id ? <Text color={t.color.accent}>#{id} </Text> : null}
427
+ <Text color={color}>{glyph}</Text> {item.goal}
428
+ </Text>
429
+
430
+ <Box flexDirection="column" marginTop={1}>
431
+ <Field name="depth" t={t} value={`${item.depth} · ${item.status}`} />
432
+ {item.model ? <Field name="model" t={t} value={item.model} /> : null}
433
+ {item.toolsets?.length ? <Field name="toolsets" t={t} value={item.toolsets.join(', ')} /> : null}
434
+ <Field name="tools" t={t} value={`${item.toolCount ?? 0} (subtree ${agg.totalTools})`} />
435
+ <Field
436
+ name="subtree"
437
+ t={t}
438
+ value={`${agg.descendantCount} agent${agg.descendantCount === 1 ? '' : 's'} · d${agg.maxDepthFromHere} · ⚡${agg.activeCount}`}
439
+ />
440
+ {item.durationSeconds ? <Field name="elapsed" t={t} value={fmtDur(item.durationSeconds)} /> : null}
441
+ {item.iteration != null ? <Field name="iteration" t={t} value={String(item.iteration)} /> : null}
442
+ {item.apiCalls ? <Field name="api calls" t={t} value={String(item.apiCalls)} /> : null}
443
+ </Box>
444
+
445
+ {localTokens > 0 || localCost > 0 ? (
446
+ <OverlaySection defaultOpen t={t} title="Budget">
447
+ {localTokens > 0 ? (
448
+ <Field
449
+ name="tokens"
450
+ t={t}
451
+ value={
452
+ <>
453
+ {fmtTokens(inputTokens)} in · {fmtTokens(outputTokens)} out
454
+ {item.reasoningTokens ? ` · ${fmtTokens(item.reasoningTokens)} reasoning` : ''}
455
+ </>
456
+ }
457
+ />
458
+ ) : null}
459
+
460
+ {localCost > 0 ? (
461
+ <Field
462
+ name="cost"
463
+ t={t}
464
+ value={
465
+ <>
466
+ {fmtCost(localCost)}
467
+ {subtreeCost >= 0.01 ? ` · subtree +${fmtCost(subtreeCost)}` : ''}
468
+ </>
469
+ }
470
+ />
471
+ ) : null}
472
+
473
+ {subtreeTokens > 0 ? <Field name="subtree tokens" t={t} value={`+${fmtTokens(subtreeTokens)}`} /> : null}
474
+ </OverlaySection>
475
+ ) : null}
476
+
477
+ {filesRead.length > 0 || filesWritten.length > 0 ? (
478
+ <OverlaySection count={filesRead.length + filesWritten.length} t={t} title="Files">
479
+ {filesWritten.slice(0, 8).map((p, i) => (
480
+ <Text color={t.color.statusGood} key={`w-${i}`} wrap="truncate-end">
481
+ +{p}
482
+ </Text>
483
+ ))}
484
+
485
+ {filesRead.slice(0, 8).map((p, i) => (
486
+ <Text color={t.color.text} key={`r-${i}`} wrap="truncate-end">
487
+ <Text color={t.color.muted}>·</Text> {p}
488
+ </Text>
489
+ ))}
490
+
491
+ {filesOverflow > 0 ? <Text color={t.color.muted}>…+{filesOverflow} more</Text> : null}
492
+ </OverlaySection>
493
+ ) : null}
494
+
495
+ {toolLines.length > 0 ? (
496
+ <OverlaySection count={toolLines.length} defaultOpen t={t} title="Tool calls">
497
+ {toolLines.map((line, i) => (
498
+ <Text color={t.color.text} key={i} wrap="wrap">
499
+ <Text color={t.color.muted}>·</Text> {line}
500
+ </Text>
501
+ ))}
502
+ </OverlaySection>
503
+ ) : null}
504
+
505
+ {outputTail.length > 0 ? (
506
+ <OverlaySection count={outputTail.length} defaultOpen t={t} title="Output">
507
+ {outputTail.map((entry, i) => (
508
+ <Text color={entry.isError ? t.color.error : t.color.text} key={i} wrap="wrap">
509
+ <Text bold color={entry.isError ? t.color.error : t.color.accent}>
510
+ {entry.tool}
511
+ </Text>{' '}
512
+ {entry.preview}
513
+ </Text>
514
+ ))}
515
+ </OverlaySection>
516
+ ) : null}
517
+
518
+ {item.notes.length ? (
519
+ <OverlaySection count={item.notes.length} t={t} title="Progress">
520
+ {item.notes.slice(-6).map((line, i) => (
521
+ <Text color={t.color.text} key={i} wrap="wrap">
522
+ <Text color={t.color.label}>·</Text> {line}
523
+ </Text>
524
+ ))}
525
+ </OverlaySection>
526
+ ) : null}
527
+
528
+ {item.summary ? (
529
+ <OverlaySection defaultOpen t={t} title="Summary">
530
+ <Text color={t.color.text} wrap="wrap">
531
+ {item.summary}
532
+ </Text>
533
+ </OverlaySection>
534
+ ) : null}
535
+ </Box>
536
+ )
537
+ }
538
+
539
+ function ListRow({
540
+ active,
541
+ index,
542
+ node,
543
+ peak,
544
+ t,
545
+ width
546
+ }: {
547
+ active: boolean
548
+ index: number
549
+ node: SubagentNode
550
+ peak: number
551
+ t: Theme
552
+ width: number
553
+ }) {
554
+ const { color, glyph } = statusGlyph(node.item, t)
555
+ const palette = heatPalette(t)
556
+ const heatIdx = hotnessBucket(node.aggregate.hotness, peak, palette.length)
557
+ const heatMarker = heatIdx >= 2 ? palette[heatIdx]! : null
558
+
559
+ const goal = compactPreview(node.item.goal || 'subagent', width - 28 - node.item.depth * 2)
560
+ const toolsCount = node.aggregate.totalTools > 0 ? ` ·${node.aggregate.totalTools}t` : ''
561
+ const kids = node.children.length ? ` ·${node.children.length}↓` : ''
562
+ const line = node.item.status === 'running' ? node.item.tools.at(-1) : undefined
563
+ const paren = line ? line.indexOf('(') : -1
564
+ const toolShort = line ? (paren > 0 ? line.slice(0, paren) : line).trim() : ''
565
+ const trailing = toolShort ? ` · ${compactPreview(toolShort, 14)}` : ''
566
+ const fg = active ? t.color.accent : t.color.text
567
+
568
+ return (
569
+ <Text bold={active} color={fg} inverse={active} wrap="truncate-end">
570
+ {' '}
571
+ <Text color={active ? fg : t.color.muted}>{formatRowId(index)} </Text>
572
+ {indentFor(node.item.depth)}
573
+ {heatMarker ? <Text color={heatMarker}>▍</Text> : null}
574
+ <Text color={active ? fg : color}>{glyph}</Text> {goal}
575
+ <Text color={active ? fg : t.color.muted}>
576
+ {toolsCount}
577
+ {kids}
578
+ {trailing}
579
+ </Text>
580
+ </Text>
581
+ )
582
+ }
583
+
584
+ function DiffPane({
585
+ label,
586
+ snapshot,
587
+ t,
588
+ totals,
589
+ width
590
+ }: {
591
+ label: string
592
+ snapshot: SpawnSnapshot
593
+ t: Theme
594
+ totals: ReturnType<typeof treeTotals>
595
+ width: number
596
+ }) {
597
+ return (
598
+ <Box flexDirection="column" width={width}>
599
+ <Text bold color={t.color.text}>
600
+ {label}
601
+ </Text>
602
+
603
+ <Text color={t.color.muted} wrap="truncate-end">
604
+ {snapshot.label}
605
+ </Text>
606
+
607
+ <Box marginTop={1}>
608
+ <Text color={t.color.muted} wrap="truncate-end">
609
+ {formatSummary(totals)}
610
+ </Text>
611
+ </Box>
612
+
613
+ <Box flexDirection="column" marginTop={1}>
614
+ {topLevelSubagents(snapshot.subagents)
615
+ .slice(0, 8)
616
+ .map(s => {
617
+ const { color, glyph } = statusGlyph(s, t)
618
+
619
+ return (
620
+ <Text color={t.color.muted} key={s.id} wrap="truncate-end">
621
+ <Text color={color}>{glyph}</Text> {s.goal || 'subagent'}
622
+ </Text>
623
+ )
624
+ })}
625
+ </Box>
626
+ </Box>
627
+ )
628
+ }
629
+
630
+ function DiffView({
631
+ cols,
632
+ onClose,
633
+ pair,
634
+ t
635
+ }: {
636
+ cols: number
637
+ onClose: () => void
638
+ pair: { baseline: SpawnSnapshot; candidate: SpawnSnapshot }
639
+ t: Theme
640
+ }) {
641
+ const aTotals = useMemo(() => treeTotals(buildSubagentTree(pair.baseline.subagents)), [pair.baseline])
642
+ const bTotals = useMemo(() => treeTotals(buildSubagentTree(pair.candidate.subagents)), [pair.candidate])
643
+ const paneWidth = Math.floor((cols - 4) / 2)
644
+
645
+ useInput((ch, key) => {
646
+ if (key.escape || ch === 'q') {
647
+ onClose()
648
+ }
649
+ })
650
+
651
+ const round = (n: number) => String(Math.round(n))
652
+ const sumTokens = (x: typeof aTotals) => x.inputTokens + x.outputTokens
653
+ const dollars = (n: number) => fmtCost(n) || '$0.00'
654
+
655
+ return (
656
+ <Box flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
657
+ <Box flexDirection="column" marginBottom={1}>
658
+ <Text bold color={t.color.border}>
659
+ Replay diff
660
+ </Text>
661
+ <Text color={t.color.muted}>baseline vs candidate · esc/q close</Text>
662
+ </Box>
663
+
664
+ <Box flexDirection="row" marginBottom={1}>
665
+ <DiffPane label="A · baseline" snapshot={pair.baseline} t={t} totals={aTotals} width={paneWidth} />
666
+ <Box width={2} />
667
+ <DiffPane label="B · candidate" snapshot={pair.candidate} t={t} totals={bTotals} width={paneWidth} />
668
+ </Box>
669
+
670
+ <Box flexDirection="column" marginTop={1}>
671
+ <Text bold color={t.color.accent}>
672
+ Δ
673
+ </Text>
674
+
675
+ <Text color={t.color.text}>
676
+ {diffMetricLine('agents', aTotals.descendantCount, bTotals.descendantCount, round)}
677
+ </Text>
678
+ <Text color={t.color.text}>{diffMetricLine('tools', aTotals.totalTools, bTotals.totalTools, round)}</Text>
679
+ <Text color={t.color.text}>
680
+ {diffMetricLine('depth', aTotals.maxDepthFromHere, bTotals.maxDepthFromHere, round)}
681
+ </Text>
682
+ <Text color={t.color.text}>
683
+ {diffMetricLine('duration', aTotals.totalDuration, bTotals.totalDuration, n => `${n.toFixed(1)}s`)}
684
+ </Text>
685
+ <Text color={t.color.text}>{diffMetricLine('tokens', sumTokens(aTotals), sumTokens(bTotals), fmtTokens)}</Text>
686
+ <Text color={t.color.text}>{diffMetricLine('cost', aTotals.costUsd, bTotals.costUsd, dollars)}</Text>
687
+ </Box>
688
+ </Box>
689
+ )
690
+ }
691
+
692
+ // ── Main overlay ─────────────────────────────────────────────────────
693
+
694
+ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: AgentsOverlayProps) {
695
+ const liveSubagents = useTurnSelector(state => state.subagents)
696
+ const delegation = useStore($delegationState)
697
+ const history = useStore($spawnHistory)
698
+ const diffPair = useStore($spawnDiff)
699
+ const { stdout } = useStdout()
700
+
701
+ // historyIndex === 0: live turn. 1..N pulls the Nth-most-recent archived
702
+ // snapshot. /replay passes N on open.
703
+ const [historyIndex, setHistoryIndex] = useState(() =>
704
+ Math.max(0, Math.min(history.length, Math.floor(initialHistoryIndex)))
705
+ )
706
+
707
+ const [sort, setSort] = useState<SortMode>('depth-first')
708
+ const [filter, setFilter] = useState<FilterMode>('all')
709
+ const [cursor, setCursor] = useState(0)
710
+ const [flash, setFlash] = useState<string>('')
711
+ const [now, setNow] = useState(() => Date.now())
712
+ // cc-style view switching: list = full-width row picker, detail = full-width
713
+ // scrollable pane. Two panes side-by-side in Ink fought Yoga flex.
714
+ const [mode, setMode] = useState<'detail' | 'list'>('list')
715
+
716
+ const detailScrollRef = useRef<null | ScrollBoxHandle>(null)
717
+ const prevLiveCountRef = useRef(liveSubagents.length)
718
+
719
+ // ── Derived state ──────────────────────────────────────────────────
720
+
721
+ const activeSnapshot = historyIndex > 0 ? history[historyIndex - 1] : null
722
+ // Instant fallback to history[0] the moment the live list clears — avoids
723
+ // a one-frame "no subagents" flash while the auto-follow effect fires.
724
+ const justFinishedSnapshot = historyIndex === 0 && liveSubagents.length === 0 ? (history[0] ?? null) : null
725
+ const effectiveSnapshot = activeSnapshot ?? justFinishedSnapshot
726
+ const replayMode = effectiveSnapshot != null
727
+ const subagents = replayMode ? effectiveSnapshot.subagents : liveSubagents
728
+
729
+ const tree = useMemo(() => buildSubagentTree(subagents), [subagents])
730
+ const totals = useMemo(() => treeTotals(tree), [tree])
731
+ const widths = useMemo(() => widthByDepth(tree), [tree])
732
+ const spark = useMemo(() => sparkline(widths), [widths])
733
+ const peak = useMemo(() => peakHotness(tree), [tree])
734
+ const rows = useMemo(() => prepareRows(tree, sort, filter), [tree, sort, filter])
735
+
736
+ const selected = rows[cursor] ?? null
737
+
738
+ const cols = stdout?.columns ?? 80
739
+ const rowsH = Math.max(8, (stdout?.rows ?? 24) - 10)
740
+ const listWindowStart = Math.max(0, cursor - Math.floor(rowsH / 2))
741
+
742
+ // ── Effects ────────────────────────────────────────────────────────
743
+
744
+ useEffect(() => {
745
+ // Ticker drives both the live gantt and OverlayScrollbar content-reflow
746
+ // detection. Slower in replay (nothing's growing) but not stopped
747
+ // because accordions still expand.
748
+ const id = setInterval(() => setNow(Date.now()), replayMode ? 300 : 500)
749
+
750
+ return () => clearInterval(id)
751
+ }, [replayMode])
752
+
753
+ useEffect(() => {
754
+ // Clamp stale index when history grows/shrinks beneath us.
755
+ if (historyIndex > history.length) {
756
+ setHistoryIndex(history.length)
757
+ }
758
+ }, [history.length, historyIndex])
759
+
760
+ useEffect(() => {
761
+ // Auto-follow the just-finished turn onto history[1] so the user isn't
762
+ // dropped into an empty live view. Fires only when transitioning from
763
+ // "had live subagents" → "live empty" while in live mode.
764
+ const prev = prevLiveCountRef.current
765
+ prevLiveCountRef.current = liveSubagents.length
766
+
767
+ if (historyIndex === 0 && prev > 0 && liveSubagents.length === 0 && history.length > 0) {
768
+ setHistoryIndex(1)
769
+ setCursor(0)
770
+ setFlash('turn finished · inspect freely · q to close')
771
+ }
772
+ }, [history.length, historyIndex, liveSubagents.length])
773
+
774
+ useEffect(() => {
775
+ // Reset detail scroll on navigation so the top of the new node shows.
776
+ detailScrollRef.current?.scrollTo(0)
777
+ }, [cursor, historyIndex, mode])
778
+
779
+ useEffect(() => {
780
+ // Warm caps + paused flag on open.
781
+ gw.request<DelegationStatusResponse>('delegation.status', {})
782
+ .then(r => applyDelegationStatus(asRpcResult<DelegationStatusResponse>(r)))
783
+ .catch(() => {})
784
+ }, [gw])
785
+
786
+ useEffect(() => {
787
+ if (cursor >= rows.length) {
788
+ setCursor(Math.max(0, rows.length - 1))
789
+ }
790
+ }, [cursor, rows.length])
791
+
792
+ // ── Actions ────────────────────────────────────────────────────────
793
+
794
+ const guardLive = (action: () => void) => {
795
+ if (replayMode) {
796
+ setFlash('replay mode — controls disabled')
797
+ } else {
798
+ action()
799
+ }
800
+ }
801
+
802
+ const interrupt = (id: string) => gw.request<SubagentInterruptResponse>('subagent.interrupt', { subagent_id: id })
803
+
804
+ const killOne = (id: string) =>
805
+ guardLive(() => {
806
+ interrupt(id)
807
+ .then(raw => {
808
+ const r = asRpcResult<SubagentInterruptResponse>(raw)
809
+ setFlash(r?.found ? `killing ${id}` : `not found: ${id}`)
810
+ })
811
+ .catch(() => setFlash(`kill failed: ${id}`))
812
+ })
813
+
814
+ const killSubtree = (node: SubagentNode) =>
815
+ guardLive(() => {
816
+ const ids = [node.item.id, ...descendantIds(node)]
817
+ ids.forEach(id => interrupt(id).catch(() => {}))
818
+ setFlash(`killing subtree · ${ids.length} node${ids.length === 1 ? '' : 's'}`)
819
+ })
820
+
821
+ const togglePause = () =>
822
+ guardLive(() => {
823
+ gw.request<DelegationPauseResponse>('delegation.pause', { paused: !delegation.paused })
824
+ .then(raw => {
825
+ const r = asRpcResult<DelegationPauseResponse>(raw)
826
+ applyDelegationStatus({ paused: r?.paused })
827
+ setFlash(r?.paused ? 'spawning paused' : 'spawning resumed')
828
+ })
829
+ .catch(() => setFlash('pause failed'))
830
+ })
831
+
832
+ const stepHistory = (delta: -1 | 1) =>
833
+ setHistoryIndex(idx => {
834
+ const next = Math.max(0, Math.min(history.length, idx + delta))
835
+
836
+ if (next !== idx) {
837
+ setCursor(0)
838
+ setFlash(next === 0 ? 'live turn' : `replay · ${next}/${history.length}`)
839
+ }
840
+
841
+ return next
842
+ })
843
+
844
+ const closeWithCleanup = () => {
845
+ clearDiffPair()
846
+ onClose()
847
+ }
848
+
849
+ // ── Input ──────────────────────────────────────────────────────────
850
+
851
+ const detailPageSize = Math.max(4, rowsH - 2)
852
+ const wheelDetailDy = 3
853
+ const scrollDetail = (dy: number) => detailScrollRef.current?.scrollBy(dy)
854
+
855
+ useInput((ch, key) => {
856
+ if (ch === 'q') {
857
+ return closeWithCleanup()
858
+ }
859
+
860
+ if (key.escape) {
861
+ return mode === 'detail' ? setMode('list') : closeWithCleanup()
862
+ }
863
+
864
+ // Shared actions (both modes).
865
+ if (ch === '<' || ch === '[') {
866
+ return stepHistory(1)
867
+ }
868
+
869
+ if (ch === '>' || ch === ']') {
870
+ return stepHistory(-1)
871
+ }
872
+
873
+ if (ch === 'p') {
874
+ return togglePause()
875
+ }
876
+
877
+ if (ch === 'x' && selected) {
878
+ return killOne(selected.item.id)
879
+ }
880
+
881
+ if (ch === 'X' && selected) {
882
+ return killSubtree(selected)
883
+ }
884
+
885
+ if (mode === 'detail') {
886
+ if (key.leftArrow || ch === 'h') {
887
+ return setMode('list')
888
+ }
889
+
890
+ if (key.pageUp || (key.ctrl && ch === 'u')) {
891
+ return scrollDetail(-detailPageSize)
892
+ }
893
+
894
+ if (key.pageDown || (key.ctrl && ch === 'd')) {
895
+ return scrollDetail(detailPageSize)
896
+ }
897
+
898
+ if (key.wheelUp) {
899
+ return scrollDetail(-wheelDetailDy)
900
+ }
901
+
902
+ if (key.wheelDown) {
903
+ return scrollDetail(wheelDetailDy)
904
+ }
905
+
906
+ if (key.upArrow || ch === 'k') {
907
+ return scrollDetail(-2)
908
+ }
909
+
910
+ if (key.downArrow || ch === 'j') {
911
+ return scrollDetail(2)
912
+ }
913
+
914
+ if (ch === 'g') {
915
+ return detailScrollRef.current?.scrollTo(0)
916
+ }
917
+
918
+ if (ch === 'G') {
919
+ return detailScrollRef.current?.scrollToBottom?.()
920
+ }
921
+
922
+ return
923
+ }
924
+
925
+ // List mode.
926
+ if ((key.return || key.rightArrow || ch === 'l') && selected) {
927
+ return setMode('detail')
928
+ }
929
+
930
+ if (key.upArrow || ch === 'k' || key.wheelUp) {
931
+ return setCursor(c => Math.max(0, c - 1))
932
+ }
933
+
934
+ if (key.downArrow || ch === 'j' || key.wheelDown) {
935
+ return setCursor(c => Math.min(Math.max(0, rows.length - 1), c + 1))
936
+ }
937
+
938
+ if (ch === 'g') {
939
+ return setCursor(0)
940
+ }
941
+
942
+ if (ch === 'G') {
943
+ return setCursor(Math.max(0, rows.length - 1))
944
+ }
945
+
946
+ if (ch === 's') {
947
+ return setSort(m => cycle(SORT_ORDER, m))
948
+ }
949
+
950
+ if (ch === 'f') {
951
+ return setFilter(m => cycle(FILTER_ORDER, m))
952
+ }
953
+ })
954
+
955
+ // ── Header assembly ────────────────────────────────────────────────
956
+
957
+ const mix = Object.entries(
958
+ subagents.reduce<Record<string, number>>((acc, it) => {
959
+ const key = it.model ? it.model.split('/').pop()! : 'inherit'
960
+ acc[key] = (acc[key] ?? 0) + 1
961
+
962
+ return acc
963
+ }, {})
964
+ )
965
+ .sort((a, b) => b[1] - a[1])
966
+ .slice(0, 4)
967
+ .map(([k, v]) => `${k}×${v}`)
968
+ .join(' · ')
969
+
970
+ const capsLabel = delegation.maxSpawnDepth
971
+ ? `caps d${delegation.maxSpawnDepth}/${delegation.maxConcurrentChildren ?? '?'}`
972
+ : ''
973
+
974
+ const title =
975
+ replayMode && effectiveSnapshot
976
+ ? `${historyIndex > 0 ? `Replay ${historyIndex}/${history.length}` : 'Last turn'} · finished ${new Date(
977
+ effectiveSnapshot.finishedAt
978
+ ).toLocaleTimeString()}`
979
+ : `Spawn tree${delegation.paused ? ' · ⏸ paused' : ''}`
980
+
981
+ const metaLine = [formatSummary(totals), spark, capsLabel, mix ? `· ${mix}` : ''].filter(Boolean).join(' ')
982
+
983
+ const controlsHint = replayMode
984
+ ? ' · controls locked'
985
+ : ` · x kill · X subtree · p ${delegation.paused ? 'resume' : 'pause'}`
986
+
987
+ // ── Rendering ──────────────────────────────────────────────────────
988
+
989
+ if (diffPair) {
990
+ return <DiffView cols={cols} onClose={closeWithCleanup} pair={diffPair} t={t} />
991
+ }
992
+
993
+ return (
994
+ <Box alignItems="stretch" flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
995
+ <Box flexDirection="column" marginBottom={1}>
996
+ <Text wrap="truncate-end">
997
+ <Text bold color={replayMode ? t.color.border : t.color.primary}>
998
+ {title}
999
+ </Text>
1000
+ {metaLine ? (
1001
+ <Text color={t.color.muted}>
1002
+ {' '}
1003
+ {metaLine}
1004
+ </Text>
1005
+ ) : null}
1006
+ </Text>
1007
+ </Box>
1008
+
1009
+ {rows.length === 0 ? (
1010
+ <Box flexDirection="column" flexGrow={1}>
1011
+ <Text color={t.color.muted}>No subagents this turn. Trigger delegate_task to populate the tree.</Text>
1012
+ </Box>
1013
+ ) : mode === 'list' ? (
1014
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} minHeight={0}>
1015
+ <GanttStrip cols={cols} cursor={cursor} flatNodes={rows} maxRows={6} now={now} t={t} />
1016
+
1017
+ <Box flexDirection="column" flexGrow={0} flexShrink={0} overflow="hidden">
1018
+ {rows.slice(listWindowStart, listWindowStart + rowsH).map((node, i) => (
1019
+ <ListRow
1020
+ active={listWindowStart + i === cursor}
1021
+ index={listWindowStart + i}
1022
+ key={node.item.id}
1023
+ node={node}
1024
+ peak={peak}
1025
+ t={t}
1026
+ width={cols}
1027
+ />
1028
+ ))}
1029
+ </Box>
1030
+ </Box>
1031
+ ) : (
1032
+ <Box flexDirection="row" flexGrow={1} flexShrink={1} minHeight={0}>
1033
+ <ScrollBox flexDirection="column" flexGrow={1} flexShrink={1} ref={detailScrollRef}>
1034
+ <Box flexDirection="column" paddingBottom={4} paddingRight={1}>
1035
+ {selected ? <Detail id={formatRowId(cursor).trim()} node={selected} t={t} /> : null}
1036
+ </Box>
1037
+ </ScrollBox>
1038
+
1039
+ <NoSelect flexShrink={0} marginLeft={1}>
1040
+ <OverlayScrollbar scrollRef={detailScrollRef} t={t} tick={now} />
1041
+ </NoSelect>
1042
+ </Box>
1043
+ )}
1044
+
1045
+ <Box flexDirection="column" marginTop={1}>
1046
+ {flash ? <Text color={t.color.accent}>{flash}</Text> : null}
1047
+
1048
+ {mode === 'list' ? (
1049
+ <Text color={t.color.muted}>
1050
+ ↑↓/jk move · g/G top/bottom · Enter/→ open detail{controlsHint} · s sort:{SORT_LABEL[sort]} · f filter:
1051
+ {FILTER_LABEL[filter]}
1052
+ {history.length > 0 ? ` · [ / ] history ${historyIndex}/${history.length}` : ''}
1053
+ {' · q close'}
1054
+ </Text>
1055
+ ) : (
1056
+ <Text color={t.color.muted}>
1057
+ ↑↓/jk scroll · PgUp/PgDn page · g/G top/bottom · Esc/← back to list{controlsHint} · q close
1058
+ </Text>
1059
+ )}
1060
+ </Box>
1061
+ </Box>
1062
+ )
1063
+ }
1064
+
1065
+ interface AgentsOverlayProps {
1066
+ gw: GatewayClient
1067
+ initialHistoryIndex?: number
1068
+ onClose: () => void
1069
+ t: Theme
1070
+ }
1071
+
1072
+ export const closeAgentsOverlay = () => patchOverlayState({ agents: false })
1073
+ export const openAgentsOverlay = () => patchOverlayState({ agents: true })