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,833 @@
1
+ import { STARTUP_IMAGE, STARTUP_QUERY } from '../config/env.js'
2
+ import { STREAM_BATCH_MS } from '../config/timing.js'
3
+ import { buildSetupRequiredSections, SETUP_REQUIRED_TITLE } from '../content/setup.js'
4
+ import type {
5
+ CommandsCatalogResponse,
6
+ ConfigFullResponse,
7
+ DelegationStatusResponse,
8
+ GatewayEvent,
9
+ GatewaySkin,
10
+ SessionMostRecentResponse
11
+ } from '../gatewayTypes.js'
12
+ import { rpcErrorMessage } from '../lib/rpc.js'
13
+ import { topLevelSubagents } from '../lib/subagentTree.js'
14
+ import { formatToolCall, stripAnsi } from '../lib/text.js'
15
+ import { fromSkin } from '../theme.js'
16
+ import type { Msg, SubagentProgress, SubagentStatus } from '../types.js'
17
+
18
+ import { applyDelegationStatus, getDelegationState } from './delegationStore.js'
19
+ import type { GatewayEventHandlerContext } from './interfaces.js'
20
+ import { getOverlayState, patchOverlayState } from './overlayStore.js'
21
+ import { turnController } from './turnController.js'
22
+ import { getUiState, patchUiState } from './uiStore.js'
23
+
24
+ const NO_PROVIDER_RE = /\bNo (?:LLM|inference) provider configured\b/i
25
+
26
+ const statusFromBusy = () => (getUiState().busy ? 'running…' : 'ready')
27
+
28
+ const applySkin = (s: GatewaySkin) =>
29
+ patchUiState({
30
+ theme: fromSkin(
31
+ s.colors ?? {},
32
+ s.branding ?? {},
33
+ s.banner_logo ?? '',
34
+ s.banner_hero ?? '',
35
+ s.tool_prefix ?? '',
36
+ s.help_header ?? ''
37
+ )
38
+ })
39
+
40
+ const dropBgTask = (taskId: string) =>
41
+ patchUiState(state => {
42
+ const next = new Set(state.bgTasks)
43
+ next.delete(taskId)
44
+
45
+ return { ...state, bgTasks: next }
46
+ })
47
+
48
+ const pushUnique =
49
+ (max: number) =>
50
+ <T>(xs: T[], x: T): T[] =>
51
+ xs.at(-1) === x ? xs : [...xs, x].slice(-max)
52
+
53
+ const pushThinking = pushUnique(6)
54
+ const pushNote = pushUnique(6)
55
+ const pushTool = pushUnique(8)
56
+
57
+ const KNOWN_SUBAGENT_STATUSES = new Set<SubagentStatus>([
58
+ 'completed',
59
+ 'error',
60
+ 'failed',
61
+ 'interrupted',
62
+ 'queued',
63
+ 'running',
64
+ 'timeout'
65
+ ])
66
+
67
+ const normalizeSubagentStatus = (status: unknown, fallback: SubagentStatus): SubagentStatus => {
68
+ if (typeof status !== 'string') {
69
+ return fallback
70
+ }
71
+
72
+ const normalized = status.toLowerCase() as SubagentStatus
73
+
74
+ return KNOWN_SUBAGENT_STATUSES.has(normalized) ? normalized : fallback
75
+ }
76
+
77
+ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: GatewayEvent) => void {
78
+ const { rpc } = ctx.gateway
79
+ const { STARTUP_RESUME_ID, newSession, resumeById, setCatalog } = ctx.session
80
+ const { bellOnComplete, stdout, sys } = ctx.system
81
+ const { appendMessage, panel, setHistoryItems } = ctx.transcript
82
+ const { setInput } = ctx.composer
83
+ const { submitRef } = ctx.submission
84
+ const { setProcessing: setVoiceProcessing, setRecording: setVoiceRecording, setVoiceEnabled } = ctx.voice
85
+
86
+ let pendingThinkingStatus = ''
87
+ let thinkingStatusTimer: null | ReturnType<typeof setTimeout> = null
88
+ let startupPromptSubmitted = false
89
+
90
+ // Inject the disk-save callback into turnController so recordMessageComplete
91
+ // can fire-and-forget a persist without having to plumb a gateway ref around.
92
+ turnController.persistSpawnTree = async (subagents, sessionId) => {
93
+ try {
94
+ const startedAt = subagents.reduce<number>((min, s) => {
95
+ if (!s.startedAt) {
96
+ return min
97
+ }
98
+
99
+ return min === 0 ? s.startedAt : Math.min(min, s.startedAt)
100
+ }, 0)
101
+
102
+ const top = topLevelSubagents(subagents)
103
+ .map(s => s.goal)
104
+ .filter(Boolean)
105
+ .slice(0, 2)
106
+
107
+ const label = top.length ? top.join(' · ') : `${subagents.length} subagents`
108
+
109
+ await rpc('spawn_tree.save', {
110
+ finished_at: Date.now() / 1000,
111
+ label: label.slice(0, 120),
112
+ session_id: sessionId ?? 'default',
113
+ started_at: startedAt ? startedAt / 1000 : null,
114
+ subagents
115
+ })
116
+ } catch {
117
+ // Persistence is best-effort; in-memory history is the authoritative
118
+ // same-session source. A write failure doesn't block the turn.
119
+ }
120
+ }
121
+
122
+ // Refresh delegation caps at most every 5s so the status bar HUD can
123
+ // render a /warning close to the configured cap without spamming the RPC.
124
+ let lastDelegationFetchAt = 0
125
+
126
+ // ── Shared full-config read ──────────────────────────────────────────
127
+ //
128
+ // Several concerns need `display.*` flags at startup (the /agents nudge
129
+ // gate below, the auto-resume check in the `gateway.ready` handler).
130
+ // Memoize the `config.get full` RPC so we make exactly one round-trip
131
+ // instead of one per concern. Resolves to null on RPC failure; callers
132
+ // treat null as "use defaults".
133
+ let fullConfigPromise: null | Promise<ConfigFullResponse | null> = null
134
+
135
+ const getFullConfigOnce = (): Promise<ConfigFullResponse | null> => {
136
+ fullConfigPromise ??= rpc<ConfigFullResponse>('config.get', { key: 'full' }).catch(() => null)
137
+
138
+ return fullConfigPromise
139
+ }
140
+
141
+ // ── Nudge toward /agents on delegation ───────────────────────────────
142
+ //
143
+ // When `display.tui_agents_nudge` is enabled (default true), the first
144
+ // time a turn starts delegating we drop a single transient activity hint
145
+ // ("subagents working · /agents to watch live") so the user discovers the
146
+ // spawn-tree dashboard instead of staring at a quiet transcript — without
147
+ // hijacking the screen by force-opening an overlay. Guards:
148
+ // • fires at most once per turn (`agentsNudgedThisTurn`)
149
+ // • silent if the overlay is already open (nothing to advertise)
150
+ // Reset on `message.start`. The config flag is fetched once, lazily;
151
+ // until it resolves we assume the default (on).
152
+ let agentsNudgeEnabled = true
153
+ let agentsNudgeConfigFetched = false
154
+ let agentsNudgedThisTurn = false
155
+
156
+ const ensureAgentsNudgeConfig = () => {
157
+ if (agentsNudgeConfigFetched) {
158
+ return
159
+ }
160
+
161
+ agentsNudgeConfigFetched = true
162
+ getFullConfigOnce().then(cfg => {
163
+ // Only an explicit `false` disables it; absent/unknown keeps default on.
164
+ if (cfg?.config?.display?.tui_agents_nudge === false) {
165
+ agentsNudgeEnabled = false
166
+ }
167
+ })
168
+ }
169
+
170
+ const maybeNudgeAgents = () => {
171
+ ensureAgentsNudgeConfig()
172
+
173
+ if (!agentsNudgeEnabled || agentsNudgedThisTurn) {
174
+ return
175
+ }
176
+
177
+ // Already watching → no point advertising the dashboard. Don't burn the
178
+ // turn's nudge credit here: if the user closes the overlay later in the
179
+ // same turn while delegation is still ongoing, a subsequent event should
180
+ // still be allowed to nudge. The flag is only set once we actually push.
181
+ if (getOverlayState().agents) {
182
+ return
183
+ }
184
+
185
+ agentsNudgedThisTurn = true
186
+ turnController.pushActivity('subagents working · /agents to watch live', 'info')
187
+ }
188
+
189
+ const resetAgentsNudgeTurnState = () => {
190
+ agentsNudgedThisTurn = false
191
+ }
192
+
193
+ // Kick off the config fetch eagerly at handler creation so the flag is
194
+ // resolved well before the first delegation of any real session (which
195
+ // only happens after gateway.ready + a user turn).
196
+ ensureAgentsNudgeConfig()
197
+
198
+ const refreshDelegationStatus = (force = false) => {
199
+ const now = Date.now()
200
+
201
+ if (!force && now - lastDelegationFetchAt < 5000) {
202
+ return
203
+ }
204
+
205
+ lastDelegationFetchAt = now
206
+ rpc<DelegationStatusResponse>('delegation.status', {})
207
+ .then(r => applyDelegationStatus(r))
208
+ .catch(() => {})
209
+ }
210
+
211
+ const setStatus = (status: string) => {
212
+ pendingThinkingStatus = ''
213
+
214
+ if (thinkingStatusTimer) {
215
+ clearTimeout(thinkingStatusTimer)
216
+ thinkingStatusTimer = null
217
+ }
218
+
219
+ patchUiState({ status })
220
+ }
221
+
222
+ const scheduleThinkingStatus = (status: string) => {
223
+ pendingThinkingStatus = status
224
+
225
+ if (thinkingStatusTimer) {
226
+ return
227
+ }
228
+
229
+ thinkingStatusTimer = setTimeout(() => {
230
+ thinkingStatusTimer = null
231
+ patchUiState({ status: pendingThinkingStatus || statusFromBusy() })
232
+ }, STREAM_BATCH_MS)
233
+ }
234
+
235
+ const restoreStatusAfter = (ms: number) => {
236
+ turnController.clearStatusTimer()
237
+ turnController.statusTimer = setTimeout(() => {
238
+ turnController.statusTimer = null
239
+ patchUiState({ status: statusFromBusy() })
240
+ }, ms)
241
+ }
242
+
243
+ const scheduleStartupPrompt = () => {
244
+ if (startupPromptSubmitted || (!STARTUP_QUERY && !STARTUP_IMAGE)) {
245
+ return
246
+ }
247
+
248
+ startupPromptSubmitted = true
249
+ setTimeout(async () => {
250
+ let sid = getUiState().sid
251
+
252
+ for (let i = 0; !sid && i < 40; i += 1) {
253
+ await new Promise(resolve => setTimeout(resolve, 100))
254
+ sid = getUiState().sid
255
+ }
256
+
257
+ if (!sid) {
258
+ return sys('startup query skipped: no active session')
259
+ }
260
+
261
+ if (STARTUP_IMAGE) {
262
+ try {
263
+ await rpc('image.attach', { path: STARTUP_IMAGE, session_id: sid })
264
+ } catch (e) {
265
+ sys(`startup image attach failed: ${rpcErrorMessage(e)}`)
266
+ }
267
+ }
268
+
269
+ submitRef.current(STARTUP_QUERY || 'What do you see in this image?')
270
+ }, 0)
271
+ }
272
+
273
+ // Terminal statuses are never overwritten by late-arriving live events —
274
+ // otherwise a stale `subagent.start` / `spawn_requested` can clobber a
275
+ // terminal state from complete (failed/interrupted/timeout/error).
276
+ const isTerminalStatus = (s: SubagentProgress['status']) =>
277
+ s === 'completed' || s === 'error' || s === 'failed' || s === 'interrupted' || s === 'timeout'
278
+
279
+ const keepTerminalElseRunning = (s: SubagentProgress['status']) => (isTerminalStatus(s) ? s : 'running')
280
+
281
+ const handleReady = (skin?: GatewaySkin) => {
282
+ if (skin) {
283
+ applySkin(skin)
284
+ }
285
+
286
+ rpc<CommandsCatalogResponse>('commands.catalog', {})
287
+ .then(r => {
288
+ if (!r?.pairs) {
289
+ return
290
+ }
291
+
292
+ setCatalog({
293
+ canon: (r.canon ?? {}) as Record<string, string>,
294
+ categories: r.categories ?? [],
295
+ pairs: r.pairs as [string, string][],
296
+ skillCount: (r.skill_count ?? 0) as number,
297
+ sub: (r.sub ?? {}) as Record<string, string[]>
298
+ })
299
+
300
+ if (r.warning) {
301
+ turnController.pushActivity(String(r.warning), 'warn')
302
+ }
303
+ })
304
+ .catch((e: unknown) => turnController.pushActivity(`command catalog unavailable: ${rpcErrorMessage(e)}`, 'info'))
305
+
306
+ if (STARTUP_RESUME_ID) {
307
+ patchUiState({ status: 'resuming…' })
308
+ resumeById(STARTUP_RESUME_ID)
309
+ scheduleStartupPrompt()
310
+
311
+ return
312
+ }
313
+
314
+ // Opt-in: when `display.tui_auto_resume_recent` is true, look up
315
+ // the most recent human-facing session and resume it instead of
316
+ // forging a brand-new one. Mirrors classic CLI's `nastech -c` /
317
+ // `nastech --tui` muscle memory and addresses the audit's "session
318
+ // unrecoverable after disconnection" gap. Default off so existing
319
+ // users aren't surprised. (Shares the memoized full-config read.)
320
+ getFullConfigOnce()
321
+ .then(cfg => {
322
+ if (!cfg?.config?.display?.tui_auto_resume_recent) {
323
+ patchUiState({ status: 'forging session…' })
324
+ newSession()
325
+ scheduleStartupPrompt()
326
+
327
+ return
328
+ }
329
+
330
+ return rpc<SessionMostRecentResponse>('session.most_recent', {}).then(r => {
331
+ const target = r?.session_id
332
+
333
+ if (target) {
334
+ patchUiState({ status: 'resuming most recent…' })
335
+ resumeById(target)
336
+ scheduleStartupPrompt()
337
+
338
+ return
339
+ }
340
+
341
+ patchUiState({ status: 'forging session…' })
342
+ newSession()
343
+ scheduleStartupPrompt()
344
+ })
345
+ })
346
+ .catch(() => {
347
+ patchUiState({ status: 'forging session…' })
348
+ newSession()
349
+ scheduleStartupPrompt()
350
+ })
351
+ }
352
+
353
+ return (ev: GatewayEvent) => {
354
+ const sid = getUiState().sid
355
+
356
+ if (ev.session_id && sid && ev.session_id !== sid && !ev.type.startsWith('gateway.')) {
357
+ return
358
+ }
359
+
360
+ switch (ev.type) {
361
+ case 'gateway.ready':
362
+ handleReady(ev.payload?.skin)
363
+
364
+ return
365
+
366
+ case 'skin.changed':
367
+ if (ev.payload) {
368
+ applySkin(ev.payload)
369
+ }
370
+
371
+ return
372
+ case 'session.info': {
373
+ const info = ev.payload
374
+
375
+ patchUiState(state => ({
376
+ ...state,
377
+ info,
378
+ status: state.status === 'starting agent…' ? 'ready' : state.status,
379
+ usage: info.usage ? { ...state.usage, ...info.usage } : state.usage
380
+ }))
381
+
382
+ setHistoryItems(prev => prev.map(m => (m.kind === 'intro' ? { ...m, info } : m)))
383
+
384
+ return
385
+ }
386
+
387
+ case 'thinking.delta': {
388
+ if (!getUiState().busy) {
389
+ return
390
+ }
391
+
392
+ const text = ev.payload?.text
393
+
394
+ if (text !== undefined) {
395
+ const value = String(text)
396
+ scheduleThinkingStatus(value || statusFromBusy())
397
+
398
+ if (value) {
399
+ turnController.recordReasoningDelta(value)
400
+ }
401
+ }
402
+
403
+ return
404
+ }
405
+
406
+ case 'message.start':
407
+ resetAgentsNudgeTurnState()
408
+ turnController.startMessage()
409
+
410
+ return
411
+ case 'status.update': {
412
+ const p = ev.payload
413
+
414
+ if (!p?.text) {
415
+ return
416
+ }
417
+
418
+ if (p.kind === 'goal') {
419
+ sys(p.text)
420
+
421
+ const brief = p.text.startsWith('✓')
422
+ ? '✓ goal complete'
423
+ : p.text.startsWith('↻')
424
+ ? '↻ goal continuing'
425
+ : p.text.startsWith('⏸')
426
+ ? '⏸ goal paused'
427
+ : 'ready'
428
+
429
+ setStatus(brief)
430
+ restoreStatusAfter(6000)
431
+
432
+ return
433
+ }
434
+
435
+ setStatus(p.text)
436
+
437
+ if (p.kind === 'compressing') {
438
+ sys(p.text)
439
+
440
+ return
441
+ }
442
+
443
+ if (!p.kind || p.kind === 'status') {
444
+ return
445
+ }
446
+
447
+ if (turnController.lastStatusNote !== p.text) {
448
+ turnController.lastStatusNote = p.text
449
+ turnController.pushActivity(
450
+ p.text,
451
+ p.kind === 'error' ? 'error' : p.kind === 'warn' || p.kind === 'approval' ? 'warn' : 'info'
452
+ )
453
+ }
454
+
455
+ restoreStatusAfter(4000)
456
+
457
+ return
458
+ }
459
+
460
+ case 'gateway.stderr': {
461
+ const line = String(ev.payload.line).slice(0, 120)
462
+
463
+ turnController.pushActivity(line, 'info')
464
+
465
+ return
466
+ }
467
+
468
+ case 'browser.progress': {
469
+ const message = String(ev.payload?.message ?? '').trim()
470
+
471
+ if (message) {
472
+ sys(message)
473
+ }
474
+
475
+ return
476
+ }
477
+
478
+ case 'voice.status': {
479
+ // Continuous VAD loop reports its internal state so the status bar
480
+ // can show listening / transcribing / idle without polling.
481
+ const state = String(ev.payload?.state ?? '')
482
+
483
+ if (state === 'listening') {
484
+ setVoiceRecording(true)
485
+ setVoiceProcessing(false)
486
+ } else if (state === 'transcribing') {
487
+ setVoiceRecording(false)
488
+ setVoiceProcessing(true)
489
+ } else {
490
+ setVoiceRecording(false)
491
+ setVoiceProcessing(false)
492
+ }
493
+
494
+ return
495
+ }
496
+
497
+ case 'voice.transcript': {
498
+ // CLI parity: the 3-strikes silence detector flipped off automatically.
499
+ // Mirror that on the UI side and tell the user why the mode is off.
500
+ if (ev.payload?.no_speech_limit) {
501
+ setVoiceEnabled(false)
502
+ setVoiceRecording(false)
503
+ setVoiceProcessing(false)
504
+ sys('voice: no speech detected 3 times, continuous mode stopped')
505
+
506
+ return
507
+ }
508
+
509
+ const text = String(ev.payload?.text ?? '').trim()
510
+
511
+ if (!text) {
512
+ return
513
+ }
514
+
515
+ // CLI parity: _pending_input.put(transcript) unconditionally feeds
516
+ // the transcript to the agent as its next turn — draft handling
517
+ // doesn't apply because voice-mode users are speaking, not typing.
518
+ //
519
+ // We can't branch on composer input from inside a setInput updater
520
+ // (React strict mode double-invokes it, duplicating the submit).
521
+ // Just clear + defer submit so the cleared input is committed before
522
+ // submit reads it.
523
+ setInput('')
524
+ setTimeout(() => submitRef.current(text), 0)
525
+
526
+ return
527
+ }
528
+
529
+ case 'gateway.start_timeout': {
530
+ const { cwd, python, stderr_tail: stderrTail } = ev.payload ?? {}
531
+ const trace = python || cwd ? ` · ${String(python || '')} ${String(cwd || '')}`.trim() : ''
532
+
533
+ setStatus('gateway startup timeout')
534
+ turnController.pushActivity(`gateway startup timed out${trace} · /logs to inspect`, 'error')
535
+
536
+ // Surface the most useful stderr lines inline so users can tell
537
+ // "wrong python", "missing dep", and "config parse failure"
538
+ // apart without leaving the TUI. Filter blank rows BEFORE
539
+ // taking the last N so trailing empty lines in the buffer
540
+ // don't crowd out actual content; truncate to match the
541
+ // 120-char clip used for `gateway.stderr` activity entries.
542
+ const STDERR_LINE_CAP = 120
543
+ const STDERR_LINES_MAX = 8
544
+
545
+ const tailLines = (stderrTail ?? '')
546
+ .split('\n')
547
+ .map(l => l.trim())
548
+ .filter(Boolean)
549
+ .slice(-STDERR_LINES_MAX)
550
+
551
+ for (const line of tailLines) {
552
+ turnController.pushActivity(line.slice(0, STDERR_LINE_CAP), 'error')
553
+ }
554
+
555
+ return
556
+ }
557
+
558
+ case 'gateway.protocol_error':
559
+ setStatus('protocol warning')
560
+ restoreStatusAfter(4000)
561
+
562
+ if (!turnController.protocolWarned) {
563
+ turnController.protocolWarned = true
564
+ turnController.pushActivity('protocol noise detected · /logs to inspect', 'info')
565
+ }
566
+
567
+ if (ev.payload?.preview) {
568
+ turnController.pushActivity(`protocol noise: ${String(ev.payload.preview).slice(0, 120)}`, 'info')
569
+ }
570
+
571
+ return
572
+
573
+ case 'reasoning.delta':
574
+ if (ev.payload?.text) {
575
+ turnController.recordReasoningDelta(ev.payload.text, Boolean(ev.payload.verbose))
576
+ }
577
+
578
+ return
579
+
580
+ case 'reasoning.available':
581
+ turnController.recordReasoningAvailable(String(ev.payload?.text ?? ''), Boolean(ev.payload?.verbose))
582
+
583
+ return
584
+
585
+ case 'tool.progress':
586
+ if (ev.payload?.preview && ev.payload.name) {
587
+ turnController.recordToolProgress(ev.payload.name, ev.payload.preview)
588
+ }
589
+
590
+ return
591
+
592
+ case 'tool.generating':
593
+ if (ev.payload?.name) {
594
+ turnController.pushTrail(`drafting ${ev.payload.name}…`)
595
+ }
596
+
597
+ return
598
+
599
+ case 'tool.start':
600
+ turnController.recordTodos(ev.payload.todos)
601
+ turnController.recordToolStart(
602
+ ev.payload.tool_id,
603
+ ev.payload.name ?? 'tool',
604
+ ev.payload.context ?? '',
605
+ ev.payload.args_text ? stripAnsi(String(ev.payload.args_text)) : undefined
606
+ )
607
+
608
+ return
609
+ case 'tool.complete': {
610
+ const inlineDiffText =
611
+ ev.payload.inline_diff && getUiState().inlineDiffs ? stripAnsi(String(ev.payload.inline_diff)).trim() : ''
612
+
613
+ const resultText = ev.payload.result_text ? stripAnsi(String(ev.payload.result_text)) : undefined
614
+
615
+ if (inlineDiffText) {
616
+ turnController.recordInlineDiffToolComplete(
617
+ inlineDiffText,
618
+ ev.payload.tool_id,
619
+ ev.payload.name,
620
+ ev.payload.error,
621
+ ev.payload.duration_s,
622
+ resultText
623
+ )
624
+ } else {
625
+ turnController.recordToolComplete(
626
+ ev.payload.tool_id,
627
+ ev.payload.name,
628
+ ev.payload.error,
629
+ ev.payload.summary,
630
+ ev.payload.duration_s,
631
+ ev.payload.todos,
632
+ resultText
633
+ )
634
+ }
635
+
636
+ return
637
+ }
638
+
639
+ case 'clarify.request':
640
+ patchOverlayState({
641
+ clarify: { choices: ev.payload.choices, question: ev.payload.question, requestId: ev.payload.request_id }
642
+ })
643
+ setStatus('waiting for input…')
644
+
645
+ return
646
+ case 'approval.request': {
647
+ const description = String(ev.payload.description ?? 'dangerous command')
648
+
649
+ patchOverlayState({ approval: { command: String(ev.payload.command ?? ''), description } })
650
+ setStatus('approval needed')
651
+
652
+ return
653
+ }
654
+
655
+ case 'sudo.request':
656
+ patchOverlayState({ sudo: { requestId: ev.payload.request_id } })
657
+ setStatus('sudo password needed')
658
+
659
+ return
660
+
661
+ case 'secret.request':
662
+ patchOverlayState({
663
+ secret: { envVar: ev.payload.env_var, prompt: ev.payload.prompt, requestId: ev.payload.request_id }
664
+ })
665
+ setStatus('secret input needed')
666
+
667
+ return
668
+
669
+ case 'background.complete':
670
+ dropBgTask(ev.payload.task_id)
671
+ sys(`[bg ${ev.payload.task_id}] ${ev.payload.text}`)
672
+
673
+ return
674
+ case 'review.summary': {
675
+ // Self-improvement background review emitted a persistent summary
676
+ // of what it saved to memory/skills. Surface it as a system line
677
+ // in the transcript so it never gets lost to a transient status
678
+ // flash. Python-side already formats it as "💾 Self-improvement
679
+ // review: …".
680
+ const text = String(ev.payload?.text ?? '').trim()
681
+
682
+ if (text) {
683
+ sys(text)
684
+ }
685
+
686
+ return
687
+ }
688
+
689
+ case 'subagent.spawn_requested':
690
+ // Child built but not yet running (waiting on ThreadPoolExecutor slot).
691
+ // Preserve completed state if a later event races in before this one.
692
+ turnController.upsertSubagent(ev.payload, c => (isTerminalStatus(c.status) ? {} : { status: 'queued' }))
693
+
694
+ // First sign of delegation this turn → nudge toward /agents.
695
+ maybeNudgeAgents()
696
+
697
+ // Prime the status-bar HUD: fetch caps (once every 5s) so we can
698
+ // warn as depth/concurrency approaches the configured ceiling.
699
+ if (getDelegationState().maxSpawnDepth === null) {
700
+ refreshDelegationStatus(true)
701
+ } else {
702
+ refreshDelegationStatus()
703
+ }
704
+
705
+ return
706
+
707
+ case 'subagent.start':
708
+ turnController.upsertSubagent(ev.payload, c => (isTerminalStatus(c.status) ? {} : { status: 'running' }))
709
+
710
+ // `subagent.start` is the first delegation event the TUI reliably
711
+ // receives (the delegate callback drops `spawn_requested` in the
712
+ // CLI→gateway path), so nudge here too. Once-per-turn guarded, so
713
+ // hooking both events is safe.
714
+ maybeNudgeAgents()
715
+
716
+ return
717
+ case 'subagent.thinking': {
718
+ const text = String(ev.payload.text ?? '').trim()
719
+
720
+ if (!text) {
721
+ return
722
+ }
723
+
724
+ // Update-only: never resurrect subagents whose spawn_requested/start
725
+ // we missed or that already flushed via message.complete.
726
+ turnController.upsertSubagent(
727
+ ev.payload,
728
+ c => ({
729
+ status: keepTerminalElseRunning(c.status),
730
+ thinking: pushThinking(c.thinking, text)
731
+ }),
732
+ { createIfMissing: false }
733
+ )
734
+
735
+ return
736
+ }
737
+
738
+ case 'subagent.tool': {
739
+ const line = formatToolCall(
740
+ ev.payload.tool_name ?? 'delegate_task',
741
+ ev.payload.tool_preview ?? ev.payload.text ?? ''
742
+ )
743
+
744
+ turnController.upsertSubagent(
745
+ ev.payload,
746
+ c => ({
747
+ status: keepTerminalElseRunning(c.status),
748
+ tools: pushTool(c.tools, line)
749
+ }),
750
+ { createIfMissing: false }
751
+ )
752
+
753
+ return
754
+ }
755
+
756
+ case 'subagent.progress': {
757
+ const text = String(ev.payload.text ?? '').trim()
758
+
759
+ if (!text) {
760
+ return
761
+ }
762
+
763
+ turnController.upsertSubagent(
764
+ ev.payload,
765
+ c => ({
766
+ notes: pushNote(c.notes, text),
767
+ status: keepTerminalElseRunning(c.status)
768
+ }),
769
+ { createIfMissing: false }
770
+ )
771
+
772
+ return
773
+ }
774
+
775
+ case 'subagent.complete':
776
+ turnController.upsertSubagent(
777
+ ev.payload,
778
+ c => ({
779
+ durationSeconds: ev.payload.duration_seconds ?? c.durationSeconds,
780
+ status: normalizeSubagentStatus(ev.payload.status, 'completed'),
781
+ summary: ev.payload.summary || ev.payload.text || c.summary
782
+ }),
783
+ { createIfMissing: false }
784
+ )
785
+
786
+ return
787
+
788
+ case 'message.delta':
789
+ turnController.recordMessageDelta(ev.payload ?? {})
790
+
791
+ return
792
+ case 'message.complete': {
793
+ const { finalMessages, finalText, wasInterrupted } = turnController.recordMessageComplete(ev.payload ?? {})
794
+
795
+ if (!wasInterrupted) {
796
+ const msgs: Msg[] = finalMessages.length ? finalMessages : [{ role: 'assistant', text: finalText }]
797
+ msgs.forEach(appendMessage)
798
+
799
+ if (bellOnComplete && stdout?.isTTY) {
800
+ stdout.write('\x07')
801
+ }
802
+ }
803
+
804
+ setStatus('ready')
805
+
806
+ if (ev.payload?.usage) {
807
+ patchUiState(state => ({ ...state, usage: { ...state.usage, ...ev.payload!.usage } }))
808
+ }
809
+
810
+ return
811
+ }
812
+
813
+ case 'error':
814
+ turnController.recordError()
815
+
816
+ {
817
+ const message = String(ev.payload?.message || 'unknown error')
818
+
819
+ turnController.pushActivity(message, 'error')
820
+
821
+ if (NO_PROVIDER_RE.test(message)) {
822
+ panel(SETUP_REQUIRED_TITLE, buildSetupRequiredSections())
823
+ setStatus('setup required')
824
+
825
+ return
826
+ }
827
+
828
+ sys(`error: ${message}`)
829
+ setStatus('ready')
830
+ }
831
+ }
832
+ }
833
+ }