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,866 @@
1
+ import {
2
+ REASONING_PULSE_MS,
3
+ STREAM_BATCH_MS,
4
+ STREAM_IDLE_BATCH_MS,
5
+ STREAM_SCROLL_BATCH_MS,
6
+ STREAM_TYPING_BATCH_MS
7
+ } from '../config/timing.js'
8
+ import type { SessionInterruptResponse, SubagentEventPayload } from '../gatewayTypes.js'
9
+ import { appendToolShelfMessage, isToolShelfMessage } from '../lib/liveProgress.js'
10
+ import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js'
11
+ import {
12
+ boundedLiveRenderText,
13
+ buildToolTrailLine,
14
+ buildVerboseToolTrailLine,
15
+ estimateTokensRough,
16
+ isTransientTrailLine,
17
+ sameToolTrailGroup,
18
+ toolTrailLabel
19
+ } from '../lib/text.js'
20
+ import type { ActiveTool, ActivityItem, Msg, SubagentProgress, TodoItem } from '../types.js'
21
+
22
+ import { resetFlowOverlays } from './overlayStore.js'
23
+ import { pushSnapshot } from './spawnHistoryStore.js'
24
+ import { archiveDoneTodos, getTurnState, patchTurnState, resetTurnState } from './turnStore.js'
25
+ import { getUiState, patchUiState } from './uiStore.js'
26
+
27
+ const INTERRUPT_COOLDOWN_MS = 1500
28
+ const ACTIVITY_LIMIT = 8
29
+ const TRAIL_LIMIT = 8
30
+
31
+ // Extracts the raw patch from a diff-only segment produced by
32
+ // pushInlineDiffSegment. Used at message.complete to dedupe against final
33
+ // assistant text that narrates the same patch. Returns null for anything
34
+ // else so real assistant narration never gets touched.
35
+ const diffSegmentBody = (msg: Msg): null | string => {
36
+ if (msg.kind !== 'diff') {
37
+ return null
38
+ }
39
+
40
+ const m = msg.text.match(/^```diff\n([\s\S]*?)\n```$/)
41
+
42
+ return m ? m[1]! : null
43
+ }
44
+
45
+ const hasDetails = (msg: Msg): boolean => Boolean(msg.thinking || msg.tools?.length || msg.toolTokens)
46
+
47
+ const isTodoStatus = (status: unknown): status is TodoItem['status'] =>
48
+ status === 'pending' || status === 'in_progress' || status === 'completed' || status === 'cancelled'
49
+
50
+ const parseTodos = (value: unknown): null | TodoItem[] => {
51
+ if (!Array.isArray(value)) {
52
+ return null
53
+ }
54
+
55
+ return value
56
+ .map(item => {
57
+ if (!item || typeof item !== 'object') {
58
+ return null
59
+ }
60
+
61
+ const row = item as Record<string, unknown>
62
+ const status = row.status
63
+
64
+ if (!isTodoStatus(status)) {
65
+ return null
66
+ }
67
+
68
+ return {
69
+ content: String(row.content ?? '').trim(),
70
+ id: String(row.id ?? '').trim(),
71
+ status
72
+ }
73
+ })
74
+ .filter((item): item is TodoItem => Boolean(item?.id && item.content))
75
+ }
76
+
77
+ const textSegments = (segments: Msg[]) =>
78
+ segments.filter(msg => msg.role === 'assistant' && msg.kind !== 'diff').map(msg => msg.text)
79
+
80
+ const finalTail = (finalText: string, segments: Msg[]) => {
81
+ let tail = finalText
82
+
83
+ for (const text of textSegments(segments)) {
84
+ const trimmed = text.trim()
85
+
86
+ if (trimmed && tail.startsWith(trimmed)) {
87
+ tail = tail.slice(trimmed.length).trimStart()
88
+ }
89
+ }
90
+
91
+ return tail
92
+ }
93
+
94
+ export interface InterruptDeps {
95
+ appendMessage: (msg: Msg) => void
96
+ gw: { request: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T> }
97
+ sid: string
98
+ sys: (text: string) => void
99
+ }
100
+
101
+ type Timer = null | ReturnType<typeof setTimeout>
102
+
103
+ const clear = (t: Timer): null => {
104
+ if (t) {
105
+ clearTimeout(t)
106
+ }
107
+
108
+ return null
109
+ }
110
+
111
+ class TurnController {
112
+ bufRef = ''
113
+ interrupted = false
114
+ lastStatusNote = ''
115
+ persistedToolLabels = new Set<string>()
116
+ persistSpawnTree?: (subagents: SubagentProgress[], sessionId: null | string) => Promise<void>
117
+ protocolWarned = false
118
+ reasoningText = ''
119
+ segmentMessages: Msg[] = []
120
+ pendingSegmentTools: string[] = []
121
+ statusTimer: Timer = null
122
+ toolTokenAcc = 0
123
+ turnTools: string[] = []
124
+
125
+ private activeTools: ActiveTool[] = []
126
+ private activeReasoningText = ''
127
+ private reasoningSegmentIndex: null | number = null
128
+ private activityId = 0
129
+ private reasoningStreamingTimer: Timer = null
130
+ private reasoningTimer: Timer = null
131
+ private streamTimer: Timer = null
132
+ private streamDelay = STREAM_IDLE_BATCH_MS
133
+ private toolProgressTimer: Timer = null
134
+
135
+ boostStreamingForTyping() {
136
+ this.streamDelay = STREAM_TYPING_BATCH_MS
137
+ }
138
+
139
+ boostStreamingForScroll() {
140
+ this.streamDelay = Math.max(this.streamDelay, STREAM_SCROLL_BATCH_MS)
141
+ }
142
+
143
+ relaxStreaming() {
144
+ this.streamDelay = STREAM_IDLE_BATCH_MS
145
+ }
146
+
147
+ clearReasoning() {
148
+ this.reasoningTimer = clear(this.reasoningTimer)
149
+ this.activeReasoningText = ''
150
+ this.reasoningSegmentIndex = null
151
+ this.reasoningText = ''
152
+ this.toolTokenAcc = 0
153
+ patchTurnState({ reasoning: '', reasoningTokens: 0, toolTokens: 0 })
154
+ }
155
+
156
+ clearStatusTimer() {
157
+ this.statusTimer = clear(this.statusTimer)
158
+ }
159
+
160
+ endReasoningPhase() {
161
+ this.reasoningStreamingTimer = clear(this.reasoningStreamingTimer)
162
+ patchTurnState({ reasoningActive: false, reasoningStreaming: false })
163
+ }
164
+
165
+ idle() {
166
+ this.endReasoningPhase()
167
+ this.activeTools = []
168
+ this.streamTimer = clear(this.streamTimer)
169
+ this.bufRef = ''
170
+ this.pendingSegmentTools = []
171
+ this.segmentMessages = []
172
+
173
+ patchTurnState({
174
+ streamPendingTools: [],
175
+ streamSegments: [],
176
+ streaming: '',
177
+ subagents: [],
178
+ tools: [],
179
+ turnTrail: []
180
+ })
181
+ patchUiState({ busy: false })
182
+ resetFlowOverlays()
183
+ }
184
+
185
+ interruptTurn({ appendMessage, gw, sid, sys }: InterruptDeps) {
186
+ this.interrupted = true
187
+ gw.request<SessionInterruptResponse>('session.interrupt', { session_id: sid }).catch(() => {})
188
+
189
+ this.closeReasoningSegment()
190
+
191
+ const segments = this.segmentMessages
192
+ const partial = this.bufRef.trimStart()
193
+ const tools = this.pendingSegmentTools
194
+
195
+ // Drain streaming/segment state off the nanostore before writing the
196
+ // preserved snapshot to the transcript — otherwise each flushed segment
197
+ // appears in both `turn.streamSegments` and the transcript for one frame.
198
+ this.idle()
199
+ this.clearReasoning()
200
+ this.turnTools = []
201
+ patchTurnState({ activity: [], outcome: '' })
202
+
203
+ for (const msg of segments) {
204
+ appendMessage(msg)
205
+ }
206
+
207
+ // Always surface an interruption indicator — if there's an in-flight
208
+ // `partial` or pending tools, fold them into a single assistant message;
209
+ // otherwise emit a sys note so the transcript always records that the
210
+ // turn was cancelled, even when only prior `segments` were preserved.
211
+ if (partial || tools.length) {
212
+ appendMessage({
213
+ role: 'assistant',
214
+ text: partial ? `${partial}\n\n*[interrupted]*` : '*[interrupted]*',
215
+ ...(tools.length && { tools })
216
+ })
217
+ } else {
218
+ sys('interrupted')
219
+ }
220
+
221
+ patchUiState({ status: 'interrupted' })
222
+ this.clearStatusTimer()
223
+
224
+ this.statusTimer = setTimeout(() => {
225
+ this.statusTimer = null
226
+ patchUiState({ status: 'ready' })
227
+ }, INTERRUPT_COOLDOWN_MS)
228
+ }
229
+
230
+ pruneTransient() {
231
+ this.turnTools = this.turnTools.filter(line => !isTransientTrailLine(line))
232
+ patchTurnState(state => {
233
+ const next = state.turnTrail.filter(line => !isTransientTrailLine(line))
234
+
235
+ return next.length === state.turnTrail.length ? state : { ...state, turnTrail: next }
236
+ })
237
+ }
238
+
239
+ private syncReasoningSegment() {
240
+ const thinking = this.activeReasoningText.trim()
241
+
242
+ if (!thinking) {
243
+ return
244
+ }
245
+
246
+ const msg: Msg = {
247
+ kind: 'trail',
248
+ role: 'system',
249
+ text: '',
250
+ thinking,
251
+ thinkingTokens: estimateTokensRough(thinking),
252
+ toolTokens: this.toolTokenAcc || undefined
253
+ }
254
+
255
+ if (this.reasoningSegmentIndex === null) {
256
+ this.reasoningSegmentIndex = this.segmentMessages.length
257
+ this.segmentMessages = [...this.segmentMessages, msg]
258
+ } else {
259
+ this.segmentMessages = this.segmentMessages.map((item, i) => (i === this.reasoningSegmentIndex ? msg : item))
260
+ }
261
+
262
+ patchTurnState({ streamSegments: this.segmentMessages })
263
+ }
264
+
265
+ private closeReasoningSegment() {
266
+ this.syncReasoningSegment()
267
+ this.activeReasoningText = ''
268
+ this.reasoningSegmentIndex = null
269
+ }
270
+
271
+ private pushSegment(msg: Msg) {
272
+ this.segmentMessages = appendToolShelfMessage(this.segmentMessages, msg)
273
+ }
274
+
275
+ flushStreamingSegment() {
276
+ const raw = this.bufRef.trimStart()
277
+
278
+ const split = raw
279
+ ? hasReasoningTag(raw)
280
+ ? splitReasoning(raw)
281
+ : { reasoning: '', text: raw }
282
+ : { reasoning: '', text: '' }
283
+
284
+ if (split.reasoning && !this.reasoningText.trim()) {
285
+ this.reasoningText = split.reasoning
286
+ this.activeReasoningText = split.reasoning
287
+ patchTurnState({ reasoning: this.reasoningText, reasoningTokens: estimateTokensRough(this.reasoningText) })
288
+ this.syncReasoningSegment()
289
+ }
290
+
291
+ const msg: Msg = {
292
+ role: split.text ? 'assistant' : 'system',
293
+ text: split.text,
294
+ ...(!split.text && { kind: 'trail' as const }),
295
+ ...(this.pendingSegmentTools.length && { tools: this.pendingSegmentTools })
296
+ }
297
+
298
+ this.streamTimer = clear(this.streamTimer)
299
+
300
+ if (split.text || hasDetails(msg)) {
301
+ this.pushSegment(msg)
302
+ }
303
+
304
+ this.pendingSegmentTools = []
305
+ this.bufRef = ''
306
+ patchTurnState({ streamPendingTools: [], streamSegments: this.segmentMessages, streaming: '' })
307
+ }
308
+
309
+ pulseReasoningStreaming() {
310
+ this.reasoningStreamingTimer = clear(this.reasoningStreamingTimer)
311
+ patchTurnState({ reasoningActive: true, reasoningStreaming: true })
312
+
313
+ this.reasoningStreamingTimer = setTimeout(() => {
314
+ this.reasoningStreamingTimer = null
315
+ patchTurnState({ reasoningStreaming: false })
316
+ }, REASONING_PULSE_MS)
317
+ }
318
+
319
+ recordTodos(value: unknown) {
320
+ if (this.interrupted) {
321
+ return
322
+ }
323
+
324
+ const todos = parseTodos(value)
325
+
326
+ if (todos !== null) {
327
+ patchTurnState({ todos })
328
+ }
329
+ }
330
+
331
+ private flushPendingToolsIntoLastSegment() {
332
+ if (!this.pendingSegmentTools.length) {
333
+ return false
334
+ }
335
+
336
+ const next = appendToolShelfMessage(this.segmentMessages, {
337
+ kind: 'trail',
338
+ role: 'system',
339
+ text: '',
340
+ tools: this.pendingSegmentTools
341
+ })
342
+
343
+ if (next.length === this.segmentMessages.length + 1) {
344
+ return false
345
+ }
346
+
347
+ this.segmentMessages = next
348
+ this.pendingSegmentTools = []
349
+ patchTurnState({ streamPendingTools: [], streamSegments: this.segmentMessages })
350
+
351
+ return true
352
+ }
353
+
354
+ pushInlineDiffSegment(diffText: string, tools: string[] = []) {
355
+ // Strip CLI chrome the gateway emits before the unified diff (e.g. a
356
+ // leading "┊ review diff" header written by `_emit_inline_diff` for the
357
+ // terminal printer). That header only makes sense as stdout dressing,
358
+ // not inside a markdown ```diff block.
359
+ const stripped = diffText.replace(/^\s*┊[^\n]*\n?/, '').trim()
360
+
361
+ if (!stripped) {
362
+ return
363
+ }
364
+
365
+ // Flush any in-progress streaming text as its own segment first, so the
366
+ // diff lands BETWEEN the assistant narration that preceded the edit and
367
+ // whatever the agent streams afterwards — not glued onto the final
368
+ // message. This is the whole point of segment-anchored diffs: the diff
369
+ // renders where the edit actually happened.
370
+ this.flushStreamingSegment()
371
+
372
+ const block = `\`\`\`diff\n${stripped}\n\`\`\``
373
+
374
+ // Skip consecutive duplicates (same tool firing tool.complete twice, or
375
+ // two edits producing the same patch). Keeping this cheap — deeper
376
+ // dedupe against the final assistant text happens at message.complete.
377
+ if (this.segmentMessages.at(-1)?.text === block) {
378
+ return
379
+ }
380
+
381
+ this.segmentMessages = [
382
+ ...this.segmentMessages,
383
+ { kind: 'diff', role: 'assistant', text: block, ...(tools.length && { tools }) }
384
+ ]
385
+ patchTurnState({ streamSegments: this.segmentMessages })
386
+ }
387
+
388
+ pushActivity(text: string, tone: ActivityItem['tone'] = 'info', replaceLabel?: string) {
389
+ patchTurnState(state => {
390
+ const base = replaceLabel
391
+ ? state.activity.filter(item => !sameToolTrailGroup(replaceLabel, item.text))
392
+ : state.activity
393
+
394
+ const tail = base.at(-1)
395
+
396
+ if (tail?.text === text && tail.tone === tone) {
397
+ return state
398
+ }
399
+
400
+ return { ...state, activity: [...base, { id: ++this.activityId, text, tone }].slice(-ACTIVITY_LIMIT) }
401
+ })
402
+ }
403
+
404
+ pushTrail(line: string) {
405
+ if (this.interrupted) {
406
+ return
407
+ }
408
+
409
+ patchTurnState(state => {
410
+ if (state.turnTrail.at(-1) === line) {
411
+ return state
412
+ }
413
+
414
+ const next = [...state.turnTrail.filter(item => !isTransientTrailLine(item)), line].slice(-TRAIL_LIMIT)
415
+
416
+ this.turnTools = next
417
+
418
+ return { ...state, turnTrail: next }
419
+ })
420
+ }
421
+
422
+ recordError() {
423
+ this.idle()
424
+ this.clearReasoning()
425
+ this.clearStatusTimer()
426
+ this.pendingSegmentTools = []
427
+ this.segmentMessages = []
428
+ this.turnTools = []
429
+ this.persistedToolLabels.clear()
430
+ }
431
+
432
+ recordMessageComplete(payload: { rendered?: string; reasoning?: string; text?: string }) {
433
+ this.closeReasoningSegment()
434
+
435
+ // Ink renders markdown via <Md>; the gateway's Rich-rendered ANSI
436
+ // (`payload.rendered`) is for terminals that can't. Prioritising
437
+ // `rendered` here garbles output whenever a user opts into
438
+ // `display.final_response_markdown: render` because raw ANSI escapes
439
+ // pass through into the React tree. Prefer raw text and fall back
440
+ // only when the gateway elected not to send any (#16391).
441
+ const rawText = (payload.text ?? payload.rendered ?? this.bufRef).trimStart()
442
+ const split = splitReasoning(rawText)
443
+ const finalText = finalTail(split.text, this.segmentMessages)
444
+ const existingReasoning = this.reasoningText.trim() || String(payload.reasoning ?? '').trim()
445
+ const savedReasoning = [existingReasoning, existingReasoning ? '' : split.reasoning].filter(Boolean).join('\n\n')
446
+ const savedToolTokens = this.toolTokenAcc
447
+ let tools = this.pendingSegmentTools
448
+ const last = this.segmentMessages[this.segmentMessages.length - 1]
449
+
450
+ if (tools.length && isToolShelfMessage(last)) {
451
+ this.segmentMessages = [
452
+ ...this.segmentMessages.slice(0, -1),
453
+ { ...last, tools: [...(last.tools ?? []), ...tools] }
454
+ ]
455
+ this.pendingSegmentTools = []
456
+ tools = []
457
+ }
458
+
459
+ // Drop diff-only segments the agent is about to narrate in the final
460
+ // reply. Without this, a closing "here's the diff …" message would
461
+ // render two stacked copies of the same patch. Only touches segments
462
+ // with `kind: 'diff'` emitted by pushInlineDiffSegment — real
463
+ // assistant narration stays put.
464
+ const finalHasOwnDiffFence = /```(?:diff|patch)\b/i.test(finalText)
465
+
466
+ const segments = this.segmentMessages.filter(msg => {
467
+ const body = diffSegmentBody(msg)
468
+
469
+ return body === null || (!finalHasOwnDiffFence && !finalText.includes(body))
470
+ })
471
+
472
+ const hasReasoningSegment =
473
+ this.reasoningSegmentIndex !== null || segments.some(msg => Boolean(msg.thinking?.trim()))
474
+
475
+ const finalThinking = hasReasoningSegment ? '' : savedReasoning.trim()
476
+
477
+ const finalDetails: Msg = {
478
+ kind: 'trail',
479
+ role: 'system',
480
+ text: '',
481
+ thinking: finalThinking || undefined,
482
+ thinkingTokens: finalThinking ? estimateTokensRough(finalThinking) : undefined,
483
+ toolTokens: savedToolTokens || undefined,
484
+ ...(tools.length && { tools })
485
+ }
486
+
487
+ // Archive prepended so the trail msg anchors under the user prompt,
488
+ // not between thinking/tools and final assistant text.
489
+ const finalMessages: Msg[] = [
490
+ ...archiveDoneTodos(),
491
+ ...segments,
492
+ ...(hasDetails(finalDetails) ? [finalDetails] : [])
493
+ ]
494
+
495
+ if (finalText) {
496
+ finalMessages.push({ role: 'assistant', text: finalText })
497
+ }
498
+
499
+ const wasInterrupted = this.interrupted
500
+
501
+ // Archive the turn's spawn tree to history BEFORE idle() drops subagents
502
+ // from turnState. Lets /replay and the overlay's history nav pull up
503
+ // finished fan-outs without a round-trip to disk.
504
+ const finishedSubagents = getTurnState().subagents
505
+ const sessionId = getUiState().sid
506
+
507
+ if (finishedSubagents.length > 0) {
508
+ pushSnapshot(finishedSubagents, { sessionId, startedAt: null })
509
+ // Fire-and-forget disk persistence so /replay survives process restarts.
510
+ // The same snapshot lives in memory via spawnHistoryStore for immediate
511
+ // recall — disk is the long-term archive.
512
+ void this.persistSpawnTree?.(finishedSubagents, sessionId)
513
+ }
514
+
515
+ this.idle()
516
+ this.clearReasoning()
517
+ this.turnTools = []
518
+ this.persistedToolLabels.clear()
519
+ this.bufRef = ''
520
+ this.interrupted = false
521
+ patchTurnState({ activity: [], outcome: '' })
522
+
523
+ return { finalMessages, finalText, wasInterrupted }
524
+ }
525
+
526
+ recordMessageDelta({ text }: { rendered?: string; text?: string }) {
527
+ if (this.interrupted || !text) {
528
+ return
529
+ }
530
+
531
+ this.pruneTransient()
532
+ this.endReasoningPhase()
533
+
534
+ // Always accumulate the raw text delta. The pre-#16391 path replaced
535
+ // the entire buffer with `rendered` (an *incremental* Rich ANSI
536
+ // fragment), which on every tick discarded everything streamed so far
537
+ // — visible as overlapping coloured text and lost prose under
538
+ // `display.final_response_markdown: render`.
539
+ this.bufRef += text
540
+
541
+ if (getUiState().streaming) {
542
+ this.scheduleStreaming()
543
+ }
544
+ }
545
+
546
+ recordReasoningAvailable(text: string, force = false) {
547
+ if (this.interrupted || (!force && !getUiState().showReasoning)) {
548
+ return
549
+ }
550
+
551
+ const incoming = text.trim()
552
+
553
+ if (!incoming || this.reasoningText.trim()) {
554
+ return
555
+ }
556
+
557
+ this.reasoningText = incoming
558
+ this.activeReasoningText = incoming
559
+ this.scheduleReasoning()
560
+ this.syncReasoningSegment()
561
+ this.pulseReasoningStreaming()
562
+ }
563
+
564
+ recordReasoningDelta(text: string, force = false) {
565
+ if (this.interrupted || (!force && !getUiState().showReasoning)) {
566
+ return
567
+ }
568
+
569
+ if (!this.activeReasoningText.trim() && this.pendingSegmentTools.length) {
570
+ this.flushStreamingSegment()
571
+ }
572
+
573
+ this.reasoningText += text
574
+ this.activeReasoningText += text
575
+
576
+ if (this.reasoningText.length > 80_000) {
577
+ this.reasoningText = this.reasoningText.slice(-60_000)
578
+ }
579
+
580
+ this.scheduleReasoning()
581
+ this.syncReasoningSegment()
582
+ this.pulseReasoningStreaming()
583
+ }
584
+
585
+ recordToolComplete(
586
+ toolId: string,
587
+ fallbackName?: string,
588
+ error?: string,
589
+ summary?: string,
590
+ duration?: number,
591
+ todos?: unknown,
592
+ resultText?: string
593
+ ) {
594
+ if (this.interrupted) {
595
+ return
596
+ }
597
+
598
+ this.recordTodos(todos)
599
+ const line = this.completeTool(toolId, fallbackName, error, summary, duration, resultText)
600
+
601
+ this.pendingSegmentTools = [...this.pendingSegmentTools, line]
602
+ this.flushPendingToolsIntoLastSegment()
603
+ this.publishToolState()
604
+ }
605
+
606
+ recordInlineDiffToolComplete(
607
+ diffText: string,
608
+ toolId: string,
609
+ fallbackName?: string,
610
+ error?: string,
611
+ duration?: number,
612
+ resultText?: string
613
+ ) {
614
+ if (this.interrupted) {
615
+ return
616
+ }
617
+
618
+ this.flushStreamingSegment()
619
+ this.pushInlineDiffSegment(diffText, [this.completeTool(toolId, fallbackName, error, '', duration, resultText)])
620
+ this.publishToolState()
621
+ }
622
+
623
+ private completeTool(
624
+ toolId: string,
625
+ fallbackName?: string,
626
+ error?: string,
627
+ summary?: string,
628
+ duration?: number,
629
+ resultText?: string
630
+ ) {
631
+ const done = this.activeTools.find(tool => tool.id === toolId)
632
+ const name = done?.name ?? fallbackName ?? 'tool'
633
+ const label = toolTrailLabel(name)
634
+ const fallbackDuration = done?.startedAt ? (Date.now() - done.startedAt) / 1000 : undefined
635
+
636
+ const line =
637
+ done?.verboseArgs || resultText
638
+ ? buildVerboseToolTrailLine(
639
+ name,
640
+ done?.context || '',
641
+ Boolean(error),
642
+ duration ?? fallbackDuration,
643
+ done?.verboseArgs,
644
+ error || resultText || summary || ''
645
+ )
646
+ : buildToolTrailLine(name, done?.context || '', Boolean(error), error || summary || '', duration ?? fallbackDuration)
647
+
648
+ this.activeTools = this.activeTools.filter(tool => tool.id !== toolId)
649
+
650
+ const next = this.turnTools.filter(item => !sameToolTrailGroup(label, item))
651
+
652
+ if (!this.activeTools.length) {
653
+ next.push('analyzing tool output…')
654
+ }
655
+
656
+ this.turnTools = next.slice(-TRAIL_LIMIT)
657
+
658
+ return line
659
+ }
660
+
661
+ private publishToolState() {
662
+ patchTurnState({
663
+ streamPendingTools: this.pendingSegmentTools,
664
+ tools: this.activeTools,
665
+ turnTrail: this.turnTools
666
+ })
667
+ }
668
+
669
+ recordToolProgress(toolName: string, preview: string) {
670
+ if (this.interrupted) {
671
+ return
672
+ }
673
+
674
+ const index = this.activeTools.findIndex(tool => tool.name === toolName)
675
+
676
+ if (index < 0) {
677
+ return
678
+ }
679
+
680
+ this.activeTools = this.activeTools.map((tool, i) => (i === index ? { ...tool, context: preview } : tool))
681
+
682
+ if (this.toolProgressTimer) {
683
+ return
684
+ }
685
+
686
+ this.toolProgressTimer = setTimeout(() => {
687
+ this.toolProgressTimer = null
688
+ patchTurnState({ tools: [...this.activeTools] })
689
+ }, STREAM_BATCH_MS)
690
+ }
691
+
692
+ recordToolStart(toolId: string, name: string, context: string, verboseArgs?: string) {
693
+ if (this.interrupted) {
694
+ return
695
+ }
696
+
697
+ this.flushStreamingSegment()
698
+ this.closeReasoningSegment()
699
+ this.pruneTransient()
700
+ this.endReasoningPhase()
701
+
702
+ const sample = `${name} ${context}`.trim()
703
+
704
+ this.toolTokenAcc += sample ? estimateTokensRough(sample) : 0
705
+ this.activeTools = [...this.activeTools, { context, id: toolId, name, startedAt: Date.now(), verboseArgs }]
706
+
707
+ patchTurnState({ toolTokens: this.toolTokenAcc, tools: this.activeTools })
708
+ }
709
+
710
+ reset() {
711
+ this.clearReasoning()
712
+ this.clearStatusTimer()
713
+ this.idle()
714
+ this.bufRef = ''
715
+ this.interrupted = false
716
+ this.lastStatusNote = ''
717
+ this.activeReasoningText = ''
718
+ this.pendingSegmentTools = []
719
+ this.protocolWarned = false
720
+ this.reasoningSegmentIndex = null
721
+ this.segmentMessages = []
722
+ this.turnTools = []
723
+ this.toolTokenAcc = 0
724
+ this.persistedToolLabels.clear()
725
+ patchTurnState({ activity: [], outcome: '' })
726
+ }
727
+
728
+ fullReset() {
729
+ this.reset()
730
+ resetTurnState()
731
+ }
732
+
733
+ scheduleReasoning() {
734
+ if (this.reasoningTimer) {
735
+ return
736
+ }
737
+
738
+ this.reasoningTimer = setTimeout(() => {
739
+ this.reasoningTimer = null
740
+ patchTurnState({
741
+ reasoning: this.reasoningText,
742
+ reasoningTokens: estimateTokensRough(this.reasoningText)
743
+ })
744
+ }, STREAM_BATCH_MS)
745
+ }
746
+
747
+ scheduleStreaming() {
748
+ if (this.streamTimer) {
749
+ return
750
+ }
751
+
752
+ this.streamTimer = setTimeout(() => {
753
+ this.streamTimer = null
754
+ const raw = this.bufRef.trimStart()
755
+ const visible = hasReasoningTag(raw) ? splitReasoning(raw).text : raw
756
+ patchTurnState({ streaming: boundedLiveRenderText(visible) })
757
+ }, this.streamDelay)
758
+ }
759
+
760
+ hydrateStreamingText(text: string) {
761
+ this.streamTimer = clear(this.streamTimer)
762
+ this.bufRef = text
763
+ const raw = this.bufRef.trimStart()
764
+ const visible = hasReasoningTag(raw) ? splitReasoning(raw).text : raw
765
+ patchTurnState({ streaming: boundedLiveRenderText(visible) })
766
+ }
767
+
768
+ startMessage() {
769
+ this.endReasoningPhase()
770
+ this.clearReasoning()
771
+ this.activeTools = []
772
+ this.activeReasoningText = ''
773
+ this.reasoningSegmentIndex = null
774
+ this.turnTools = []
775
+ this.toolTokenAcc = 0
776
+ this.interrupted = false
777
+ this.persistedToolLabels.clear()
778
+ patchUiState({ busy: true })
779
+ patchTurnState({ activity: [], outcome: '', subagents: [], toolTokens: 0, tools: [], turnTrail: [] })
780
+ }
781
+
782
+ upsertSubagent(
783
+ p: SubagentEventPayload,
784
+ patch: (current: SubagentProgress) => Partial<SubagentProgress>,
785
+ opts: { createIfMissing?: boolean } = { createIfMissing: true }
786
+ ) {
787
+ // Stable id: prefer the server-issued subagent_id (survives nested
788
+ // grandchildren + cross-tree joins). Fall back to the composite key
789
+ // for older gateways that omit the field — those produce a flat list.
790
+ const id = p.subagent_id || `sa:${p.task_index}:${p.goal || 'subagent'}`
791
+
792
+ patchTurnState(state => {
793
+ const existing = state.subagents.find(item => item.id === id)
794
+
795
+ // Late events (subagent.complete/tool/progress arriving after message.complete
796
+ // has already fired idle()) would otherwise resurrect a finished
797
+ // subagent into turn.subagents and block the "finished" title on the
798
+ // /agents overlay. When `createIfMissing` is false we drop silently.
799
+ if (!existing && !opts.createIfMissing) {
800
+ return state
801
+ }
802
+
803
+ const base: SubagentProgress = existing ?? {
804
+ depth: p.depth ?? 0,
805
+ goal: p.goal,
806
+ id,
807
+ index: p.task_index,
808
+ model: p.model,
809
+ notes: [],
810
+ parentId: p.parent_id ?? null,
811
+ startedAt: Date.now(),
812
+ status: 'running',
813
+ taskCount: p.task_count ?? 1,
814
+ thinking: [],
815
+ toolCount: p.tool_count ?? 0,
816
+ tools: [],
817
+ toolsets: p.toolsets
818
+ }
819
+
820
+ // Map snake_case payload keys onto camelCase state. Only overwrite
821
+ // when the event actually carries the field; `??` preserves prior
822
+ // values across streaming events that emit partial payloads.
823
+ const outputTail = p.output_tail
824
+ ? p.output_tail.map(e => ({
825
+ isError: Boolean(e.is_error),
826
+ preview: String(e.preview ?? ''),
827
+ tool: String(e.tool ?? 'tool')
828
+ }))
829
+ : base.outputTail
830
+
831
+ const next: SubagentProgress = {
832
+ ...base,
833
+ apiCalls: p.api_calls ?? base.apiCalls,
834
+ costUsd: p.cost_usd ?? base.costUsd,
835
+ depth: p.depth ?? base.depth,
836
+ filesRead: p.files_read ?? base.filesRead,
837
+ filesWritten: p.files_written ?? base.filesWritten,
838
+ goal: p.goal || base.goal,
839
+ inputTokens: p.input_tokens ?? base.inputTokens,
840
+ iteration: p.iteration ?? base.iteration,
841
+ model: p.model ?? base.model,
842
+ outputTail,
843
+ outputTokens: p.output_tokens ?? base.outputTokens,
844
+ parentId: p.parent_id ?? base.parentId,
845
+ reasoningTokens: p.reasoning_tokens ?? base.reasoningTokens,
846
+ taskCount: p.task_count ?? base.taskCount,
847
+ toolCount: p.tool_count ?? base.toolCount,
848
+ toolsets: p.toolsets ?? base.toolsets,
849
+ ...patch(base)
850
+ }
851
+
852
+ // Stable order: by spawn (depth, parent, index) rather than insert time.
853
+ // Without it, grandchildren can shuffle relative to siblings when
854
+ // events arrive out of order under high concurrency.
855
+ const subagents = existing
856
+ ? state.subagents.map(item => (item.id === id ? next : item))
857
+ : [...state.subagents, next].sort((a, b) => a.depth - b.depth || a.index - b.index)
858
+
859
+ return { ...state, subagents }
860
+ })
861
+ }
862
+ }
863
+
864
+ export const turnController = new TurnController()
865
+
866
+ export type { TurnController }