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,1224 @@
1
+ import { Box, NoSelect, Text } from '@nastechai/ink'
2
+ import { memo, type ReactNode, useEffect, useMemo, useState } from 'react'
3
+ import spinners, { type BrailleSpinnerName } from 'unicode-animations'
4
+
5
+ import { THINKING_COT_MAX } from '../config/limits.js'
6
+ import { sectionMode } from '../domain/details.js'
7
+ import {
8
+ buildSubagentTree,
9
+ fmtCost,
10
+ fmtTokens,
11
+ formatSummary as formatSpawnSummary,
12
+ hotnessBucket,
13
+ peakHotness,
14
+ sparkline,
15
+ treeTotals,
16
+ widthByDepth
17
+ } from '../lib/subagentTree.js'
18
+ import {
19
+ boundedLiveRenderText,
20
+ compactPreview,
21
+ estimateTokensRough,
22
+ fmtK,
23
+ formatToolCall,
24
+ parseToolTrailResultLine,
25
+ pick,
26
+ splitToolDuration,
27
+ thinkingPreview,
28
+ toolTrailLabel
29
+ } from '../lib/text.js'
30
+ import type { Theme } from '../theme.js'
31
+ import type {
32
+ ActiveTool,
33
+ ActivityItem,
34
+ DetailsMode,
35
+ SectionVisibility,
36
+ SubagentNode,
37
+ SubagentProgress,
38
+ ThinkingMode
39
+ } from '../types.js'
40
+
41
+ const THINK: BrailleSpinnerName[] = ['helix', 'breathe', 'orbit', 'dna', 'waverows', 'snake', 'pulse']
42
+ const TOOL: BrailleSpinnerName[] = ['cascade', 'scan', 'diagswipe', 'fillsweep', 'rain', 'columns', 'sparkle']
43
+
44
+ const fmtElapsed = (ms: number) => {
45
+ const sec = Math.max(0, ms) / 1000
46
+
47
+ return sec < 10 ? `${sec.toFixed(1)}s` : `${Math.round(sec)}s`
48
+ }
49
+
50
+ type TreeBranch = 'mid' | 'last'
51
+ type TreeRails = readonly boolean[]
52
+
53
+ const nextTreeRails = (rails: TreeRails, branch: TreeBranch) => [...rails, branch === 'mid']
54
+
55
+ const treeLead = (rails: TreeRails, branch: TreeBranch) =>
56
+ `${rails.map(on => (on ? '│ ' : ' ')).join('')}${branch === 'mid' ? '├─ ' : '└─ '}`
57
+
58
+ // ── Primitives ───────────────────────────────────────────────────────
59
+
60
+ function TreeRow({
61
+ branch,
62
+ children,
63
+ rails = [],
64
+ stemColor,
65
+ stemDim = true,
66
+ t
67
+ }: {
68
+ branch: TreeBranch
69
+ children: ReactNode
70
+ rails?: TreeRails
71
+ stemColor?: string
72
+ stemDim?: boolean
73
+ t: Theme
74
+ }) {
75
+ const lead = treeLead(rails, branch)
76
+
77
+ return (
78
+ <Box>
79
+ <NoSelect flexShrink={0} fromLeftEdge width={lead.length}>
80
+ <Text color={stemColor ?? t.color.muted} dim={stemDim}>
81
+ {lead}
82
+ </Text>
83
+ </NoSelect>
84
+ <Box flexDirection="column" flexGrow={1}>
85
+ {children}
86
+ </Box>
87
+ </Box>
88
+ )
89
+ }
90
+
91
+ function TreeTextRow({
92
+ branch,
93
+ color,
94
+ content,
95
+ dimColor,
96
+ rails = [],
97
+ t,
98
+ wrap = 'wrap-trim'
99
+ }: {
100
+ branch: TreeBranch
101
+ color: string
102
+ content: ReactNode
103
+ dimColor?: boolean
104
+ rails?: TreeRails
105
+ t: Theme
106
+ wrap?: 'truncate-end' | 'wrap' | 'wrap-trim'
107
+ }) {
108
+ const text = dimColor ? (
109
+ <Text color={color} dim wrap={wrap}>
110
+ {content}
111
+ </Text>
112
+ ) : (
113
+ <Text color={color} wrap={wrap}>
114
+ {content}
115
+ </Text>
116
+ )
117
+
118
+ return (
119
+ <TreeRow branch={branch} rails={rails} t={t}>
120
+ {text}
121
+ </TreeRow>
122
+ )
123
+ }
124
+
125
+ function TreeNode({
126
+ branch,
127
+ children,
128
+ header,
129
+ open,
130
+ rails = [],
131
+ stemColor,
132
+ stemDim,
133
+ t
134
+ }: {
135
+ branch: TreeBranch
136
+ children?: (rails: boolean[]) => ReactNode
137
+ header: ReactNode
138
+ open: boolean
139
+ rails?: TreeRails
140
+ stemColor?: string
141
+ stemDim?: boolean
142
+ t: Theme
143
+ }) {
144
+ return (
145
+ <Box flexDirection="column">
146
+ <TreeRow branch={branch} rails={rails} stemColor={stemColor} stemDim={stemDim} t={t}>
147
+ {header}
148
+ </TreeRow>
149
+ {open ? children?.(nextTreeRails(rails, branch)) : null}
150
+ </Box>
151
+ )
152
+ }
153
+
154
+ export function Spinner({ color, variant = 'think' }: { color: string; variant?: 'think' | 'tool' }) {
155
+ const spin = useMemo(() => {
156
+ const raw = spinners[pick(variant === 'tool' ? TOOL : THINK)]
157
+
158
+ return { ...raw, frames: raw.frames.map(f => [...f][0] ?? '⠀') }
159
+ }, [variant])
160
+
161
+ const [frame, setFrame] = useState(0)
162
+
163
+ useEffect(() => {
164
+ setFrame(0)
165
+ }, [spin])
166
+
167
+ useEffect(() => {
168
+ const id = setInterval(() => setFrame(f => (f + 1) % spin.frames.length), spin.interval)
169
+
170
+ return () => clearInterval(id)
171
+ }, [spin])
172
+
173
+ return <Text color={color}>{spin.frames[frame]}</Text>
174
+ }
175
+
176
+ interface DetailRow {
177
+ color: string
178
+ content: ReactNode
179
+ dimColor?: boolean
180
+ key: string
181
+ }
182
+
183
+ function Detail({
184
+ branch = 'last',
185
+ color,
186
+ content,
187
+ dimColor,
188
+ rails = [],
189
+ t
190
+ }: DetailRow & { branch?: TreeBranch; rails?: TreeRails; t: Theme }) {
191
+ return <TreeTextRow branch={branch} color={color} content={content} dimColor={dimColor} rails={rails} t={t} />
192
+ }
193
+
194
+ function StreamCursor({
195
+ color,
196
+ dimColor,
197
+ streaming = false,
198
+ visible = false
199
+ }: {
200
+ color: string
201
+ dimColor?: boolean
202
+ streaming?: boolean
203
+ visible?: boolean
204
+ }) {
205
+ const [on, setOn] = useState(true)
206
+
207
+ useEffect(() => {
208
+ if (!visible || !streaming) {
209
+ setOn(true)
210
+
211
+ return
212
+ }
213
+
214
+ const id = setInterval(() => setOn(v => !v), 420)
215
+
216
+ return () => clearInterval(id)
217
+ }, [streaming, visible])
218
+
219
+ if (!visible) {
220
+ return null
221
+ }
222
+
223
+ return dimColor ? (
224
+ <Text color={color} dim>
225
+ {streaming && on ? '▍' : ' '}
226
+ </Text>
227
+ ) : (
228
+ <Text color={color}>{streaming && on ? '▍' : ' '}</Text>
229
+ )
230
+ }
231
+
232
+ function Chevron({
233
+ count,
234
+ onClick,
235
+ open,
236
+ suffix,
237
+ t,
238
+ title,
239
+ tone = 'dim'
240
+ }: {
241
+ count?: number
242
+ onClick: (deep?: boolean) => void
243
+ open: boolean
244
+ suffix?: string
245
+ t: Theme
246
+ title: string
247
+ tone?: 'dim' | 'error' | 'warn'
248
+ }) {
249
+ const color = tone === 'error' ? t.color.error : tone === 'warn' ? t.color.warn : t.color.muted
250
+
251
+ return (
252
+ <Box onClick={(e: any) => onClick(!!e?.shiftKey || !!e?.ctrlKey)}>
253
+ <Text color={color} dim={tone === 'dim'}>
254
+ <Text color={t.color.accent}>{open ? '▾ ' : '▸ '}</Text>
255
+ {title}
256
+ {typeof count === 'number' ? ` (${count})` : ''}
257
+ {suffix ? (
258
+ <Text color={t.color.statusFg} dim>
259
+ {' '}
260
+ {suffix}
261
+ </Text>
262
+ ) : null}
263
+ </Text>
264
+ </Box>
265
+ )
266
+ }
267
+
268
+ function heatColor(node: SubagentNode, peak: number, theme: Theme): string | undefined {
269
+ const palette = [theme.color.border, theme.color.accent, theme.color.primary, theme.color.warn, theme.color.error]
270
+ const idx = hotnessBucket(node.aggregate.hotness, peak, palette.length)
271
+
272
+ // Below the median bucket we keep the default dim stem so cool branches
273
+ // fade into the chrome — only "hot" branches draw the eye.
274
+ if (idx < 2) {
275
+ return undefined
276
+ }
277
+
278
+ return palette[idx]
279
+ }
280
+
281
+ function SubagentAccordion({
282
+ branch,
283
+ expanded,
284
+ node,
285
+ peak,
286
+ rails = [],
287
+ t
288
+ }: {
289
+ branch: TreeBranch
290
+ expanded: boolean
291
+ node: SubagentNode
292
+ peak: number
293
+ rails?: TreeRails
294
+ t: Theme
295
+ }) {
296
+ const [open, setOpen] = useState(expanded)
297
+ const [deep, setDeep] = useState(expanded)
298
+ const [openThinking, setOpenThinking] = useState(expanded)
299
+ const [openTools, setOpenTools] = useState(expanded)
300
+ const [openNotes, setOpenNotes] = useState(expanded)
301
+ const [openKids, setOpenKids] = useState(expanded)
302
+
303
+ useEffect(() => {
304
+ if (!expanded) {
305
+ return
306
+ }
307
+
308
+ setOpen(true)
309
+ setDeep(true)
310
+ setOpenThinking(true)
311
+ setOpenTools(true)
312
+ setOpenNotes(true)
313
+ setOpenKids(true)
314
+ }, [expanded])
315
+
316
+ const expandAll = () => {
317
+ setOpen(true)
318
+ setDeep(true)
319
+ setOpenThinking(true)
320
+ setOpenTools(true)
321
+ setOpenNotes(true)
322
+ setOpenKids(true)
323
+ }
324
+
325
+ const item = node.item
326
+ const children = node.children
327
+ const aggregate = node.aggregate
328
+
329
+ const statusTone: 'dim' | 'error' | 'warn' =
330
+ item.status === 'error' || item.status === 'failed'
331
+ ? 'error'
332
+ : item.status === 'interrupted' || item.status === 'timeout'
333
+ ? 'warn'
334
+ : 'dim'
335
+
336
+ const prefix = item.taskCount > 1 ? `[${item.index + 1}/${item.taskCount}] ` : ''
337
+ const goalLabel = item.goal || `Subagent ${item.index + 1}`
338
+ const title = `${prefix}${open ? goalLabel : compactPreview(goalLabel, 60)}`
339
+ const summary = compactPreview((item.summary || '').replace(/\s+/g, ' ').trim(), 72)
340
+
341
+ // Suffix packs branch rollup: status · elapsed · per-branch tool/agent/token/cost.
342
+ // Emphasises the numbers the user can't easily eyeball from a flat list.
343
+ const statusLabel = item.status === 'queued' ? 'queued' : item.status === 'running' ? 'running' : String(item.status)
344
+
345
+ const rollupBits: string[] = [statusLabel]
346
+
347
+ if (item.durationSeconds) {
348
+ rollupBits.push(fmtElapsed(item.durationSeconds * 1000))
349
+ }
350
+
351
+ const localTools = item.toolCount ?? 0
352
+ const subtreeTools = aggregate.totalTools - localTools
353
+
354
+ if (localTools > 0) {
355
+ rollupBits.push(`${localTools} tool${localTools === 1 ? '' : 's'}`)
356
+ }
357
+
358
+ const localTokens = (item.inputTokens ?? 0) + (item.outputTokens ?? 0)
359
+
360
+ if (localTokens > 0) {
361
+ rollupBits.push(`${fmtTokens(localTokens)} tok`)
362
+ }
363
+
364
+ const localCost = item.costUsd ?? 0
365
+
366
+ if (localCost > 0) {
367
+ rollupBits.push(fmtCost(localCost))
368
+ }
369
+
370
+ const filesLocal = (item.filesWritten?.length ?? 0) + (item.filesRead?.length ?? 0)
371
+
372
+ if (filesLocal > 0) {
373
+ rollupBits.push(`⎘${filesLocal}`)
374
+ }
375
+
376
+ if (children.length > 0) {
377
+ rollupBits.push(`${aggregate.descendantCount}↓`)
378
+
379
+ if (subtreeTools > 0) {
380
+ rollupBits.push(`+${subtreeTools}t sub`)
381
+ }
382
+
383
+ const subCost = aggregate.costUsd - localCost
384
+
385
+ if (subCost >= 0.01) {
386
+ rollupBits.push(`+${fmtCost(subCost)} sub`)
387
+ }
388
+
389
+ if (aggregate.activeCount > 0 && item.status !== 'running') {
390
+ rollupBits.push(`⚡${aggregate.activeCount}`)
391
+ }
392
+ }
393
+
394
+ const suffix = rollupBits.join(' · ')
395
+
396
+ const thinkingText = item.thinking.join('\n')
397
+ const hasThinking = Boolean(thinkingText)
398
+ const hasTools = item.tools.length > 0
399
+ const noteRows = [...(summary ? [summary] : []), ...item.notes]
400
+ const hasNotes = noteRows.length > 0
401
+ const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.muted
402
+
403
+ const sections: {
404
+ header: ReactNode
405
+ key: string
406
+ open: boolean
407
+ render: (rails: boolean[]) => ReactNode
408
+ }[] = []
409
+
410
+ if (hasThinking) {
411
+ sections.push({
412
+ header: (
413
+ <Chevron
414
+ count={item.thinking.length}
415
+ onClick={shift => {
416
+ if (shift) {
417
+ expandAll()
418
+ } else {
419
+ setOpenThinking(v => !v)
420
+ }
421
+ }}
422
+ open={openThinking}
423
+ t={t}
424
+ title="Thinking"
425
+ />
426
+ ),
427
+ key: 'thinking',
428
+ open: openThinking,
429
+ render: childRails => (
430
+ <Thinking
431
+ active={item.status === 'running'}
432
+ branch="last"
433
+ mode="full"
434
+ rails={childRails}
435
+ reasoning={thinkingText}
436
+ streaming={item.status === 'running'}
437
+ t={t}
438
+ />
439
+ )
440
+ })
441
+ }
442
+
443
+ if (hasTools) {
444
+ sections.push({
445
+ header: (
446
+ <Chevron
447
+ count={item.tools.length}
448
+ onClick={shift => {
449
+ if (shift) {
450
+ expandAll()
451
+ } else {
452
+ setOpenTools(v => !v)
453
+ }
454
+ }}
455
+ open={openTools}
456
+ t={t}
457
+ title="Tool calls"
458
+ />
459
+ ),
460
+ key: 'tools',
461
+ open: openTools,
462
+ render: childRails => (
463
+ <Box flexDirection="column">
464
+ {item.tools.map((line, index) => (
465
+ <TreeTextRow
466
+ branch={index === item.tools.length - 1 ? 'last' : 'mid'}
467
+ color={t.color.text}
468
+ content={
469
+ <>
470
+ <Text color={t.color.accent}>● </Text>
471
+ {line}
472
+ </>
473
+ }
474
+ key={`${item.id}-tool-${index}`}
475
+ rails={childRails}
476
+ t={t}
477
+ />
478
+ ))}
479
+ </Box>
480
+ )
481
+ })
482
+ }
483
+
484
+ if (hasNotes) {
485
+ sections.push({
486
+ header: (
487
+ <Chevron
488
+ count={noteRows.length}
489
+ onClick={shift => {
490
+ if (shift) {
491
+ expandAll()
492
+ } else {
493
+ setOpenNotes(v => !v)
494
+ }
495
+ }}
496
+ open={openNotes}
497
+ t={t}
498
+ title="Progress"
499
+ tone={statusTone}
500
+ />
501
+ ),
502
+ key: 'notes',
503
+ open: openNotes,
504
+ render: childRails => (
505
+ <Box flexDirection="column">
506
+ {noteRows.map((line, index) => (
507
+ <TreeTextRow
508
+ branch={index === noteRows.length - 1 ? 'last' : 'mid'}
509
+ color={noteColor}
510
+ content={line}
511
+ dimColor={statusTone === 'dim'}
512
+ key={`${item.id}-note-${index}`}
513
+ rails={childRails}
514
+ t={t}
515
+ />
516
+ ))}
517
+ </Box>
518
+ )
519
+ })
520
+ }
521
+
522
+ if (children.length > 0) {
523
+ // Nested grandchildren — rendered recursively via SubagentAccordion,
524
+ // sharing the same keybindings / expand semantics as top-level nodes.
525
+ sections.push({
526
+ header: (
527
+ <Chevron
528
+ count={children.length}
529
+ onClick={shift => {
530
+ if (shift) {
531
+ expandAll()
532
+ } else {
533
+ setOpenKids(v => !v)
534
+ }
535
+ }}
536
+ open={openKids}
537
+ suffix={`d${item.depth + 1} · ${aggregate.descendantCount} total`}
538
+ t={t}
539
+ title="Spawned"
540
+ />
541
+ ),
542
+ key: 'subagents',
543
+ open: openKids,
544
+ render: childRails => (
545
+ <Box flexDirection="column">
546
+ {children.map((child, i) => (
547
+ <SubagentAccordion
548
+ branch={i === children.length - 1 ? 'last' : 'mid'}
549
+ expanded={expanded || deep}
550
+ key={child.item.id}
551
+ node={child}
552
+ peak={peak}
553
+ rails={childRails}
554
+ t={t}
555
+ />
556
+ ))}
557
+ </Box>
558
+ )
559
+ })
560
+ }
561
+
562
+ // Heatmap: amber→error gradient on the stem when this branch is "hot"
563
+ // (high tools/sec) relative to the whole tree's peak.
564
+ const stem = heatColor(node, peak, t)
565
+
566
+ return (
567
+ <TreeNode
568
+ branch={branch}
569
+ header={
570
+ <Chevron
571
+ onClick={shift => {
572
+ if (shift) {
573
+ expandAll()
574
+
575
+ return
576
+ }
577
+
578
+ setOpen(v => {
579
+ if (!v) {
580
+ setDeep(false)
581
+ }
582
+
583
+ return !v
584
+ })
585
+ }}
586
+ open={open}
587
+ suffix={suffix}
588
+ t={t}
589
+ title={title}
590
+ tone={statusTone}
591
+ />
592
+ }
593
+ open={open}
594
+ rails={rails}
595
+ stemColor={stem}
596
+ stemDim={stem == null}
597
+ t={t}
598
+ >
599
+ {childRails => (
600
+ <Box flexDirection="column">
601
+ {sections.map((section, index) => (
602
+ <TreeNode
603
+ branch={index === sections.length - 1 ? 'last' : 'mid'}
604
+ header={section.header}
605
+ key={`${item.id}-${section.key}`}
606
+ open={section.open}
607
+ rails={childRails}
608
+ t={t}
609
+ >
610
+ {section.render}
611
+ </TreeNode>
612
+ ))}
613
+ </Box>
614
+ )}
615
+ </TreeNode>
616
+ )
617
+ }
618
+
619
+ // ── Thinking ─────────────────────────────────────────────────────────
620
+
621
+ export const Thinking = memo(function Thinking({
622
+ active = false,
623
+ branch = 'last',
624
+ mode = 'truncated',
625
+ rails = [],
626
+ reasoning,
627
+ streaming = false,
628
+ t
629
+ }: {
630
+ active?: boolean
631
+ branch?: TreeBranch
632
+ mode?: ThinkingMode
633
+ rails?: TreeRails
634
+ reasoning: string
635
+ streaming?: boolean
636
+ t: Theme
637
+ }) {
638
+ const preview = useMemo(() => {
639
+ const raw = thinkingPreview(reasoning, mode, THINKING_COT_MAX)
640
+
641
+ return mode === 'full' ? boundedLiveRenderText(raw) : raw
642
+ }, [mode, reasoning])
643
+
644
+ const lines = useMemo(() => preview.split('\n').map(line => line.replace(/\t/g, ' ')), [preview])
645
+
646
+ if (!preview && !active) {
647
+ return null
648
+ }
649
+
650
+ return (
651
+ <TreeRow branch={branch} rails={rails} t={t}>
652
+ <Box flexDirection="column" flexGrow={1}>
653
+ {preview ? (
654
+ mode === 'full' ? (
655
+ lines.map((line, index) => (
656
+ <Text color={t.color.muted} key={index} wrap="wrap-trim">
657
+ {line || ' '}
658
+ {index === lines.length - 1 ? (
659
+ <StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
660
+ ) : null}
661
+ </Text>
662
+ ))
663
+ ) : (
664
+ <Text color={t.color.muted} wrap="truncate-end">
665
+ {preview}
666
+ <StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
667
+ </Text>
668
+ )
669
+ ) : (
670
+ <Text color={t.color.muted}>
671
+ <StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
672
+ </Text>
673
+ )}
674
+ </Box>
675
+ </TreeRow>
676
+ )
677
+ })
678
+
679
+ // ── ToolTrail ────────────────────────────────────────────────────────
680
+
681
+ interface Group {
682
+ color: string
683
+ content: ReactNode
684
+ details: DetailRow[]
685
+ key: string
686
+ label: string
687
+ }
688
+
689
+ export const ToolTrail = memo(function ToolTrail({
690
+ busy = false,
691
+ commandOverride = false,
692
+ detailsMode = 'collapsed',
693
+ outcome = '',
694
+ reasoningActive = false,
695
+ reasoning = '',
696
+ reasoningTokens,
697
+ reasoningStreaming = false,
698
+ sections,
699
+ subagents = [],
700
+ t,
701
+ tools = [],
702
+ toolTokens,
703
+ trail = [],
704
+ activity = []
705
+ }: {
706
+ busy?: boolean
707
+ commandOverride?: boolean
708
+ detailsMode?: DetailsMode
709
+ outcome?: string
710
+ reasoningActive?: boolean
711
+ reasoning?: string
712
+ reasoningTokens?: number
713
+ reasoningStreaming?: boolean
714
+ sections?: SectionVisibility
715
+ subagents?: SubagentProgress[]
716
+ t: Theme
717
+ tools?: ActiveTool[]
718
+ toolTokens?: number
719
+ trail?: string[]
720
+ activity?: ActivityItem[]
721
+ }) {
722
+ const visible = useMemo(
723
+ () => ({
724
+ thinking: sectionMode('thinking', detailsMode, sections, commandOverride),
725
+ tools: sectionMode('tools', detailsMode, sections, commandOverride),
726
+ subagents: sectionMode('subagents', detailsMode, sections, commandOverride),
727
+ activity: sectionMode('activity', detailsMode, sections, commandOverride)
728
+ }),
729
+ [commandOverride, detailsMode, sections]
730
+ )
731
+
732
+ const [now, setNow] = useState(() => Date.now())
733
+ // Local toggles own the open state once mounted. Init from the resolved
734
+ // section visibility so default-expanded sections (thinking/tools) render
735
+ // open on first paint; the useEffect below re-syncs when the user mutates
736
+ // visibility at runtime via /details. NEVER OR these against
737
+ // `visible.X === 'expanded'` at render time — that locks the panel open
738
+ // and silently breaks manual chevron clicks for default-expanded
739
+ // sections (regression caught after #14968).
740
+ const [openThinking, setOpenThinking] = useState(visible.thinking === 'expanded')
741
+ const [openTools, setOpenTools] = useState(visible.tools === 'expanded')
742
+ const [openSubagents, setOpenSubagents] = useState(visible.subagents === 'expanded')
743
+ const [deepSubagents, setDeepSubagents] = useState(visible.subagents === 'expanded')
744
+ const [openMeta, setOpenMeta] = useState(visible.activity === 'expanded')
745
+
746
+ useEffect(() => {
747
+ if (!tools.length || (visible.tools !== 'expanded' && !openTools)) {
748
+ return
749
+ }
750
+
751
+ const id = setInterval(() => setNow(Date.now()), 500)
752
+
753
+ return () => clearInterval(id)
754
+ }, [openTools, tools.length, visible.tools])
755
+
756
+ useEffect(() => {
757
+ setOpenThinking(visible.thinking === 'expanded')
758
+ setOpenTools(visible.tools === 'expanded')
759
+ setOpenSubagents(visible.subagents === 'expanded')
760
+ setOpenMeta(visible.activity === 'expanded')
761
+ }, [visible])
762
+
763
+ const cot = useMemo(() => thinkingPreview(reasoning, 'full', THINKING_COT_MAX), [reasoning])
764
+
765
+ // Spawn-tree derivations must live above any early return so React's
766
+ // rules-of-hooks sees a stable call order. Cheap O(N) builds memoised
767
+ // by subagent-list identity.
768
+ const spawnTree = useMemo(() => buildSubagentTree(subagents), [subagents])
769
+ const spawnPeak = useMemo(() => peakHotness(spawnTree), [spawnTree])
770
+ const spawnTotals = useMemo(() => treeTotals(spawnTree), [spawnTree])
771
+ const spawnWidths = useMemo(() => widthByDepth(spawnTree), [spawnTree])
772
+ const spawnSpark = useMemo(() => sparkline(spawnWidths), [spawnWidths])
773
+ const spawnSummaryLabel = useMemo(() => formatSpawnSummary(spawnTotals), [spawnTotals])
774
+
775
+ if (
776
+ !busy &&
777
+ !trail.length &&
778
+ !tools.length &&
779
+ !subagents.length &&
780
+ !activity.length &&
781
+ !cot &&
782
+ !reasoningActive &&
783
+ !outcome
784
+ ) {
785
+ return null
786
+ }
787
+
788
+ // ── Build groups + meta ────────────────────────────────────────
789
+
790
+ const groups: Group[] = []
791
+ const meta: DetailRow[] = []
792
+ const pushDetail = (row: DetailRow) => (groups.at(-1)?.details ?? meta).push(row)
793
+
794
+ for (const [i, line] of trail.entries()) {
795
+ const parsed = parseToolTrailResultLine(line)
796
+
797
+ if (parsed) {
798
+ groups.push({
799
+ color: parsed.mark === '✗' ? t.color.error : t.color.text,
800
+ content: parsed.call,
801
+ details: [],
802
+ key: `tr-${i}`,
803
+ label: parsed.call
804
+ })
805
+
806
+ if (parsed.detail) {
807
+ pushDetail({
808
+ color: parsed.mark === '✗' ? t.color.error : t.color.muted,
809
+ content: parsed.detail,
810
+ dimColor: parsed.mark !== '✗',
811
+ key: `tr-${i}-d`
812
+ })
813
+ }
814
+
815
+ continue
816
+ }
817
+
818
+ if (line.startsWith('drafting ')) {
819
+ const label = toolTrailLabel(line.slice(9).replace(/…$/, '').trim())
820
+
821
+ groups.push({
822
+ color: t.color.text,
823
+ content: label,
824
+ details: [{ color: t.color.muted, content: 'drafting...', dimColor: true, key: `tr-${i}-d` }],
825
+ key: `tr-${i}`,
826
+ label
827
+ })
828
+
829
+ continue
830
+ }
831
+
832
+ if (line === 'analyzing tool output…') {
833
+ pushDetail({
834
+ color: t.color.muted,
835
+ dimColor: true,
836
+ key: `tr-${i}`,
837
+ content: groups.length ? (
838
+ <>
839
+ <Spinner color={t.color.accent} variant="think" /> {line}
840
+ </>
841
+ ) : (
842
+ line
843
+ )
844
+ })
845
+
846
+ continue
847
+ }
848
+
849
+ meta.push({ color: t.color.muted, content: line, dimColor: true, key: `tr-${i}` })
850
+ }
851
+
852
+ for (const tool of tools) {
853
+ const label = formatToolCall(tool.name, tool.context || '')
854
+
855
+ groups.push({
856
+ color: t.color.text,
857
+ key: tool.id,
858
+ label,
859
+ details: tool.verboseArgs
860
+ ? [
861
+ {
862
+ color: t.color.muted,
863
+ content: `Args:\n${boundedLiveRenderText(tool.verboseArgs)}`,
864
+ dimColor: true,
865
+ key: `${tool.id}-args`
866
+ }
867
+ ]
868
+ : [],
869
+ content: (
870
+ <>
871
+ <Spinner color={t.color.accent} variant="tool" /> {label}
872
+ {tool.startedAt ? ` (${fmtElapsed(now - tool.startedAt)})` : ''}
873
+ </>
874
+ )
875
+ })
876
+ }
877
+
878
+ for (const item of activity.slice(-4)) {
879
+ const glyph = item.tone === 'error' ? '✗' : item.tone === 'warn' ? '!' : '·'
880
+ const color = item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.muted
881
+ meta.push({ color, content: `${glyph} ${item.text}`, dimColor: item.tone === 'info', key: `a-${item.id}` })
882
+ }
883
+
884
+ // ── Derived ────────────────────────────────────────────────────
885
+
886
+ const hasTools = groups.length > 0
887
+ const hasSubagents = subagents.length > 0
888
+ const hasMeta = meta.length > 0
889
+ const hasThinking = !!cot || reasoningActive || reasoningStreaming
890
+ const thinkingLive = reasoningActive || reasoningStreaming
891
+
892
+ const tokenCount =
893
+ reasoningTokens && reasoningTokens > 0 ? reasoningTokens : reasoning ? estimateTokensRough(reasoning) : 0
894
+
895
+ const toolTokenCount = toolTokens ?? 0
896
+ const totalTokenCount = tokenCount + toolTokenCount
897
+ const thinkingTokensLabel = tokenCount > 0 ? `~${fmtK(tokenCount)} tokens` : null
898
+
899
+ const toolTokensLabel = toolTokens !== undefined && toolTokens > 0 ? `~${fmtK(toolTokens)} tokens` : undefined
900
+
901
+ const totalTokensLabel = tokenCount > 0 && toolTokenCount > 0 ? `~${fmtK(totalTokenCount)} total` : null
902
+ const delegateGroups = groups.filter(g => g.label.startsWith('Delegate Task'))
903
+ const inlineDelegateKey = hasSubagents && delegateGroups.length === 1 ? delegateGroups[0]!.key : null
904
+
905
+ const toolLabel = (group: Group) => {
906
+ const { duration, label } = splitToolDuration(String(group.content))
907
+
908
+ return duration ? (
909
+ <>
910
+ {label}
911
+ <Text color={t.color.statusFg} dim>
912
+ {duration}
913
+ </Text>
914
+ </>
915
+ ) : (
916
+ group.content
917
+ )
918
+ }
919
+
920
+ // ── Backstop: floating alerts when every panel is hidden ─────────
921
+ //
922
+ // Per-section overrides win over the global details_mode (they're computed
923
+ // by sectionMode), so we only collapse to nothing when EVERY section is
924
+ // resolved to hidden — that way `details_mode: hidden` + `sections.tools:
925
+ // expanded` still renders the tools panel. When all panels are hidden
926
+ // AND ambient errors/warnings exist, surface them as a compact inline
927
+ // backstop so quiet-mode users aren't blind to failures.
928
+
929
+ const allHidden =
930
+ visible.thinking === 'hidden' &&
931
+ visible.tools === 'hidden' &&
932
+ visible.subagents === 'hidden' &&
933
+ visible.activity === 'hidden'
934
+
935
+ if (allHidden) {
936
+ const alerts = activity.filter(i => i.tone !== 'info').slice(-2)
937
+
938
+ return alerts.length ? (
939
+ <Box flexDirection="column">
940
+ {alerts.map(i => (
941
+ <Text color={i.tone === 'error' ? t.color.error : t.color.warn} key={`ha-${i.id}`}>
942
+ {i.tone === 'error' ? '✗' : '!'} {i.text}
943
+ </Text>
944
+ ))}
945
+ </Box>
946
+ ) : null
947
+ }
948
+
949
+ // ── Tree render fragments ──────────────────────────────────────
950
+ //
951
+ // Shift+click on any chevron expands every NON-hidden section at once —
952
+ // hidden sections stay hidden so the override is honoured.
953
+
954
+ const expandAll = () => {
955
+ if (visible.thinking !== 'hidden') {
956
+ setOpenThinking(true)
957
+ }
958
+
959
+ if (visible.tools !== 'hidden') {
960
+ setOpenTools(true)
961
+ }
962
+
963
+ if (visible.subagents !== 'hidden') {
964
+ setOpenSubagents(true)
965
+ setDeepSubagents(true)
966
+ }
967
+
968
+ if (visible.activity !== 'hidden') {
969
+ setOpenMeta(true)
970
+ }
971
+ }
972
+
973
+ const metaTone: 'dim' | 'error' | 'warn' = activity.some(i => i.tone === 'error')
974
+ ? 'error'
975
+ : activity.some(i => i.tone === 'warn')
976
+ ? 'warn'
977
+ : 'dim'
978
+
979
+ const renderSubagentList = (rails: boolean[]) => (
980
+ <Box flexDirection="column">
981
+ {spawnTree.map((node, index) => (
982
+ <SubagentAccordion
983
+ branch={index === spawnTree.length - 1 ? 'last' : 'mid'}
984
+ expanded={visible.subagents === 'expanded' || deepSubagents}
985
+ key={node.item.id}
986
+ node={node}
987
+ peak={spawnPeak}
988
+ rails={rails}
989
+ t={t}
990
+ />
991
+ ))}
992
+ </Box>
993
+ )
994
+
995
+ const panels: {
996
+ header: ReactNode
997
+ key: string
998
+ open: boolean
999
+ render: (rails: boolean[]) => ReactNode
1000
+ }[] = []
1001
+
1002
+ if (hasThinking && visible.thinking !== 'hidden') {
1003
+ panels.push({
1004
+ header: (
1005
+ <Box
1006
+ onClick={(e: any) => {
1007
+ if (e?.shiftKey || e?.ctrlKey) {
1008
+ expandAll()
1009
+ } else {
1010
+ setOpenThinking(v => !v)
1011
+ }
1012
+ }}
1013
+ >
1014
+ <Text color={t.color.muted} dim={!thinkingLive}>
1015
+ <Text color={t.color.accent}>{openThinking ? '▾ ' : '▸ '}</Text>
1016
+ {thinkingLive ? (
1017
+ <Text bold color={t.color.text}>
1018
+ Thinking
1019
+ </Text>
1020
+ ) : (
1021
+ <Text color={t.color.muted} dim>
1022
+ Thinking
1023
+ </Text>
1024
+ )}
1025
+ {thinkingTokensLabel ? (
1026
+ <Text color={t.color.statusFg} dim>
1027
+ {' '}
1028
+ {thinkingTokensLabel}
1029
+ </Text>
1030
+ ) : null}
1031
+ </Text>
1032
+ </Box>
1033
+ ),
1034
+ key: 'thinking',
1035
+ open: openThinking,
1036
+ render: rails => (
1037
+ <Thinking
1038
+ active={reasoningActive}
1039
+ branch="last"
1040
+ mode="full"
1041
+ rails={rails}
1042
+ reasoning={busy ? reasoning : cot}
1043
+ streaming={busy && reasoningStreaming}
1044
+ t={t}
1045
+ />
1046
+ )
1047
+ })
1048
+ }
1049
+
1050
+ if (hasTools && visible.tools !== 'hidden') {
1051
+ panels.push({
1052
+ header: (
1053
+ <Chevron
1054
+ count={groups.length}
1055
+ onClick={shift => {
1056
+ if (shift) {
1057
+ expandAll()
1058
+ } else {
1059
+ setOpenTools(v => !v)
1060
+ }
1061
+ }}
1062
+ open={openTools}
1063
+ suffix={toolTokensLabel}
1064
+ t={t}
1065
+ title="Tool calls"
1066
+ />
1067
+ ),
1068
+ key: 'tools',
1069
+ open: openTools,
1070
+ render: rails => (
1071
+ <Box flexDirection="column">
1072
+ {groups.map((group, index) => {
1073
+ const branch: TreeBranch = index === groups.length - 1 ? 'last' : 'mid'
1074
+ const childRails = nextTreeRails(rails, branch)
1075
+ const hasInlineSubagents = inlineDelegateKey === group.key
1076
+ // Surface the /agents hint the moment a delegate group appears —
1077
+ // while it's still in-flight and before any subagent has
1078
+ // registered — so users can open the live monitor immediately.
1079
+ const isDelegateGroup = group.label.startsWith('Delegate Task')
1080
+
1081
+ return (
1082
+ <Box flexDirection="column" key={group.key}>
1083
+ <TreeTextRow
1084
+ branch={branch}
1085
+ color={group.color}
1086
+ content={
1087
+ <>
1088
+ <Text color={t.color.accent}>● </Text>
1089
+ {toolLabel(group)}
1090
+ {isDelegateGroup ? (
1091
+ <Text color={t.color.statusFg} dim>
1092
+ {' (/agents to monitor)'}
1093
+ </Text>
1094
+ ) : null}
1095
+ </>
1096
+ }
1097
+ rails={rails}
1098
+ t={t}
1099
+ />
1100
+ {group.details.map((detail, detailIndex) => (
1101
+ <Detail
1102
+ {...detail}
1103
+ branch={detailIndex === group.details.length - 1 && !hasInlineSubagents ? 'last' : 'mid'}
1104
+ key={detail.key}
1105
+ rails={childRails}
1106
+ t={t}
1107
+ />
1108
+ ))}
1109
+ {hasInlineSubagents ? renderSubagentList(childRails) : null}
1110
+ </Box>
1111
+ )
1112
+ })}
1113
+ </Box>
1114
+ )
1115
+ })
1116
+ }
1117
+
1118
+ if (hasSubagents && !inlineDelegateKey && visible.subagents !== 'hidden') {
1119
+ // Spark + summary give a one-line read on the branch shape before
1120
+ // opening the subtree. `/agents` opens the full-screen audit overlay.
1121
+ const suffix = spawnSpark ? `${spawnSummaryLabel} ${spawnSpark} (/agents)` : `${spawnSummaryLabel} (/agents)`
1122
+
1123
+ panels.push({
1124
+ header: (
1125
+ <Chevron
1126
+ count={spawnTotals.descendantCount}
1127
+ onClick={shift => {
1128
+ if (shift) {
1129
+ expandAll()
1130
+ setDeepSubagents(true)
1131
+ } else {
1132
+ setOpenSubagents(v => !v)
1133
+ setDeepSubagents(false)
1134
+ }
1135
+ }}
1136
+ open={openSubagents}
1137
+ suffix={suffix}
1138
+ t={t}
1139
+ title="Spawn tree"
1140
+ />
1141
+ ),
1142
+ key: 'subagents',
1143
+ open: openSubagents,
1144
+ render: renderSubagentList
1145
+ })
1146
+ }
1147
+
1148
+ if (hasMeta && visible.activity !== 'hidden') {
1149
+ panels.push({
1150
+ header: (
1151
+ <Chevron
1152
+ count={meta.length}
1153
+ onClick={shift => {
1154
+ if (shift) {
1155
+ expandAll()
1156
+ } else {
1157
+ setOpenMeta(v => !v)
1158
+ }
1159
+ }}
1160
+ open={openMeta}
1161
+ t={t}
1162
+ title="Activity"
1163
+ tone={metaTone}
1164
+ />
1165
+ ),
1166
+ key: 'meta',
1167
+ open: openMeta,
1168
+ render: rails => (
1169
+ <Box flexDirection="column">
1170
+ {meta.map((row, index) => (
1171
+ <TreeTextRow
1172
+ branch={index === meta.length - 1 ? 'last' : 'mid'}
1173
+ color={row.color}
1174
+ content={row.content}
1175
+ dimColor={row.dimColor}
1176
+ key={row.key}
1177
+ rails={rails}
1178
+ t={t}
1179
+ />
1180
+ ))}
1181
+ </Box>
1182
+ )
1183
+ })
1184
+ }
1185
+
1186
+ const topCount = panels.length + (totalTokensLabel ? 1 : 0)
1187
+
1188
+ return (
1189
+ <Box flexDirection="column">
1190
+ {panels.map((panel, index) => (
1191
+ <TreeNode
1192
+ branch={index === topCount - 1 ? 'last' : 'mid'}
1193
+ header={panel.header}
1194
+ key={panel.key}
1195
+ open={panel.open}
1196
+ t={t}
1197
+ >
1198
+ {panel.render}
1199
+ </TreeNode>
1200
+ ))}
1201
+ {totalTokensLabel ? (
1202
+ <TreeTextRow
1203
+ branch="last"
1204
+ color={t.color.statusFg}
1205
+ content={
1206
+ <>
1207
+ <Text color={t.color.accent}>Σ </Text>
1208
+ {totalTokensLabel}
1209
+ </>
1210
+ }
1211
+ dimColor
1212
+ t={t}
1213
+ />
1214
+ ) : null}
1215
+ {outcome ? (
1216
+ <Box marginTop={1}>
1217
+ <Text color={t.color.muted} dim>
1218
+ · {outcome}
1219
+ </Text>
1220
+ </Box>
1221
+ ) : null}
1222
+ </Box>
1223
+ )
1224
+ })