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,1039 @@
1
+ import { type ScrollBoxHandle, useApp, useHasSelection, useSelection, useStdout, useTerminalTitle } from '@nastechai/ink'
2
+ import { useStore } from '@nanostores/react'
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+
5
+ import { STARTUP_RESUME_ID } from '../config/env.js'
6
+ import { MAX_HISTORY, WHEEL_SCROLL_STEP } from '../config/limits.js'
7
+ import { SECTION_NAMES, sectionMode } from '../domain/details.js'
8
+ import { attachedImageNotice, imageTokenMeta } from '../domain/messages.js'
9
+ import { fmtCwdBranch, shortCwd } from '../domain/paths.js'
10
+ import { type GatewayClient } from '../gatewayClient.js'
11
+ import type {
12
+ ClarifyRespondResponse,
13
+ ClipboardPasteResponse,
14
+ ConfigSetResponse,
15
+ GatewayEvent,
16
+ SessionActiveListResponse,
17
+ SessionCloseResponse,
18
+ TerminalResizeResponse
19
+ } from '../gatewayTypes.js'
20
+ import { useGitBranch } from '../hooks/useGitBranch.js'
21
+ import { useVirtualHistory } from '../hooks/useVirtualHistory.js'
22
+ import { composerPromptWidth } from '../lib/inputMetrics.js'
23
+ import { appendTranscriptMessage } from '../lib/messages.js'
24
+ import { DEFAULT_VOICE_RECORD_KEY, isMac, type ParsedVoiceRecordKey } from '../lib/platform.js'
25
+ import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
26
+ import { terminalParityHints } from '../lib/terminalParity.js'
27
+ import { buildToolTrailLine, sameToolTrailGroup, toolTrailLabel } from '../lib/text.js'
28
+ import { estimatedMsgHeight, messageHeightKey } from '../lib/virtualHeights.js'
29
+ import type { Msg, PanelSection, SlashCatalog } from '../types.js'
30
+
31
+ import { createGatewayEventHandler } from './createGatewayEventHandler.js'
32
+ import { createSlashHandler } from './createSlashHandler.js'
33
+ import { getInputSelection } from './inputSelectionStore.js'
34
+ import { type GatewayRpc, type TranscriptRow } from './interfaces.js'
35
+ import { $overlayState, patchOverlayState } from './overlayStore.js'
36
+ import { scrollWithSelectionBy } from './scroll.js'
37
+ import { turnController } from './turnController.js'
38
+ import { patchTurnState, useTurnSelector } from './turnStore.js'
39
+ import { $uiState, getUiState, patchUiState } from './uiStore.js'
40
+ import { useComposerState } from './useComposerState.js'
41
+ import { useConfigSync } from './useConfigSync.js'
42
+ import { useInputHandlers } from './useInputHandlers.js'
43
+ import { useLongRunToolCharms } from './useLongRunToolCharms.js'
44
+ import { useSessionLifecycle } from './useSessionLifecycle.js'
45
+ import { useSubmission } from './useSubmission.js'
46
+
47
+ const GOOD_VIBES_RE = /\b(good bot|thanks|thank you|thx|ty|ily|love you)\b/i
48
+ const BRACKET_PASTE_ON = '\x1b[?2004h'
49
+ const BRACKET_PASTE_OFF = '\x1b[?2004l'
50
+ const MAX_HEIGHT_CACHE_BUCKETS = 12
51
+
52
+ const capHistory = (items: Msg[]): Msg[] => {
53
+ if (items.length <= MAX_HISTORY) {
54
+ return items
55
+ }
56
+
57
+ return items[0]?.kind === 'intro' ? [items[0]!, ...items.slice(-(MAX_HISTORY - 1))] : items.slice(-MAX_HISTORY)
58
+ }
59
+
60
+ const statusColorOf = (status: string, t: { error: string; muted: string; ok: string; warn: string }) => {
61
+ if (status === 'ready') {
62
+ return t.ok
63
+ }
64
+
65
+ if (status.startsWith('error')) {
66
+ return t.error
67
+ }
68
+
69
+ if (status === 'interrupted') {
70
+ return t.warn
71
+ }
72
+
73
+ return t.muted
74
+ }
75
+
76
+ export interface PromptLiveSessionOptions {
77
+ dispatchSubmission: (full: string) => void
78
+ maybeWarn: (value: unknown) => void
79
+ modelArg?: string
80
+ newLiveSession: (msg?: string, title?: string) => Promise<null | string> | null | string | void
81
+ onModelSwitched?: (value: string, result: ConfigSetResponse) => void
82
+ prompt: string
83
+ rpc: GatewayRpc
84
+ sys: (text: string) => void
85
+ }
86
+
87
+ export async function startPromptLiveSession({
88
+ dispatchSubmission,
89
+ maybeWarn,
90
+ modelArg,
91
+ newLiveSession,
92
+ onModelSwitched,
93
+ prompt,
94
+ rpc,
95
+ sys
96
+ }: PromptLiveSessionOptions) {
97
+ const trimmed = prompt.trim()
98
+
99
+ if (!trimmed) {
100
+ return null
101
+ }
102
+
103
+ // Let the backend-created session key (YYYYMMDD_HHMMSS_xxxxxx) remain
104
+ // the initial title. Auto-title generation can rename it after the first
105
+ // response; pre-queuing prompt text here causes duplicate-title errors when
106
+ // users dispatch common prompts like "Hello, what model are you?".
107
+ const sid = (await newLiveSession('new live session started')) ?? null
108
+
109
+ if (!sid) {
110
+ sys('error: failed to start new live session')
111
+
112
+ return null
113
+ }
114
+
115
+ const requestedModel = modelArg?.trim()
116
+
117
+ if (requestedModel) {
118
+ const result = await rpc<ConfigSetResponse>('config.set', { key: 'model', session_id: sid, value: requestedModel })
119
+
120
+ if (!result?.value) {
121
+ sys('error: invalid response: model switch')
122
+
123
+ return sid
124
+ }
125
+
126
+ sys(`model → ${result.value}`)
127
+ maybeWarn(result)
128
+ onModelSwitched?.(result.value, result)
129
+ }
130
+
131
+ dispatchSubmission(trimmed)
132
+
133
+ return sid
134
+ }
135
+
136
+ export function useMainApp(gw: GatewayClient) {
137
+ const { exit } = useApp()
138
+ const { stdout } = useStdout()
139
+ const [cols, setCols] = useState(stdout?.columns ?? 80)
140
+
141
+ useEffect(() => {
142
+ if (!stdout) {
143
+ return
144
+ }
145
+
146
+ const sync = () => setCols(stdout.columns ?? 80)
147
+
148
+ stdout.on('resize', sync)
149
+
150
+ if (stdout.isTTY) {
151
+ stdout.write(BRACKET_PASTE_ON)
152
+ }
153
+
154
+ return () => {
155
+ stdout.off('resize', sync)
156
+
157
+ if (stdout.isTTY) {
158
+ stdout.write(BRACKET_PASTE_OFF)
159
+ }
160
+ }
161
+ }, [stdout])
162
+
163
+ const [historyItems, setHistoryItems] = useState<Msg[]>(() => [{ kind: 'intro', role: 'system', text: '' }])
164
+ const [lastUserMsg, setLastUserMsg] = useState('')
165
+ const [stickyPrompt, setStickyPrompt] = useState('')
166
+ const [catalog, setCatalog] = useState<null | SlashCatalog>(null)
167
+ const [voiceEnabled, setVoiceEnabled] = useState(false)
168
+ const [voiceTts, setVoiceTts] = useState(false)
169
+ const [voiceRecording, setVoiceRecording] = useState(false)
170
+ const [voiceProcessing, setVoiceProcessing] = useState(false)
171
+ const [voiceRecordKey, setVoiceRecordKey] = useState<ParsedVoiceRecordKey>(DEFAULT_VOICE_RECORD_KEY)
172
+ const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now())
173
+ const [turnStartedAt, setTurnStartedAt] = useState<null | number>(null)
174
+ const [goodVibesTick, setGoodVibesTick] = useState(0)
175
+ const [bellOnComplete, setBellOnComplete] = useState(false)
176
+
177
+ const ui = useStore($uiState)
178
+ const overlay = useStore($overlayState)
179
+
180
+ const turnLiveTailActive = useTurnSelector(state =>
181
+ Boolean(
182
+ state.streaming ||
183
+ state.streamPendingTools.length ||
184
+ state.streamSegments.length ||
185
+ state.reasoning.trim() ||
186
+ state.reasoningActive ||
187
+ state.tools.length ||
188
+ state.subagents.length ||
189
+ state.todos.length
190
+ )
191
+ )
192
+
193
+ const slashFlightRef = useRef(0)
194
+ const slashRef = useRef<(cmd: string) => boolean>(() => false)
195
+ const colsRef = useRef(cols)
196
+ const scrollRef = useRef<null | ScrollBoxHandle>(null)
197
+ const onEventRef = useRef<(ev: GatewayEvent) => void>(() => {})
198
+ const clipboardPasteRef = useRef<(quiet?: boolean) => Promise<void> | void>(() => {})
199
+ const submitRef = useRef<(value: string) => void>(() => {})
200
+ const terminalHintsShownRef = useRef(new Set<string>())
201
+ const historyItemsRef = useRef(historyItems)
202
+ const lastUserMsgRef = useRef(lastUserMsg)
203
+ const msgIdsRef = useRef(new WeakMap<Msg, string>())
204
+ const msgIdSeqRef = useRef(0)
205
+ const heightCachesRef = useRef(new Map<string, Map<string, number>>())
206
+
207
+ colsRef.current = cols
208
+ historyItemsRef.current = historyItems
209
+ lastUserMsgRef.current = lastUserMsg
210
+
211
+ const hasSelection = useHasSelection()
212
+ const selection = useSelection()
213
+ const lastCopiedVersionRef = useRef(-1)
214
+
215
+ useEffect(() => {
216
+ selection.setSelectionBgColor(ui.theme.color.selectionBg)
217
+ }, [selection, ui.theme.color.selectionBg])
218
+
219
+ // macOS Terminal.app does not forward Cmd+C to fullscreen TUIs that enable
220
+ // mouse tracking, so the only reliable native-feeling path is iTerm-style
221
+ // copy-on-select: once a drag creates a stable TUI selection, write it to
222
+ // the system clipboard while keeping the highlight visible.
223
+ //
224
+ // Subscribe directly via the ink selection bus (not useSyncExternalStore)
225
+ // so React doesn't re-render MainApp on every drag-move tick. The version
226
+ // ref de-dupes against re-entrant notifications.
227
+ useEffect(() => {
228
+ if (!isMac) {
229
+ return
230
+ }
231
+
232
+ return selection.subscribe(() => {
233
+ if (!selection.hasSelection()) {
234
+ return
235
+ }
236
+
237
+ const state = selection.getState() as { isDragging?: boolean } | null
238
+
239
+ if (state?.isDragging) {
240
+ return
241
+ }
242
+
243
+ const version = selection.version()
244
+
245
+ if (version === lastCopiedVersionRef.current) {
246
+ return
247
+ }
248
+
249
+ lastCopiedVersionRef.current = version
250
+ void selection.copySelectionNoClear()
251
+ })
252
+ }, [selection])
253
+
254
+ const clearSelection = useCallback(() => {
255
+ selection.clearSelection()
256
+ getInputSelection()?.collapseToEnd()
257
+ }, [selection])
258
+
259
+ const composer = useComposerState({
260
+ gw,
261
+ onClipboardPaste: quiet => clipboardPasteRef.current(quiet),
262
+ onImageAttached: info => {
263
+ sys(attachedImageNotice(info))
264
+ },
265
+ submitRef
266
+ })
267
+
268
+ const { actions: composerActions, refs: composerRefs, state: composerState } = composer
269
+ const empty = !historyItems.some(msg => msg.kind !== 'intro')
270
+
271
+ useEffect(() => {
272
+ void terminalParityHints()
273
+ .then(hints => {
274
+ for (const hint of hints) {
275
+ if (terminalHintsShownRef.current.has(hint.key)) {
276
+ continue
277
+ }
278
+
279
+ terminalHintsShownRef.current.add(hint.key)
280
+ turnController.pushActivity(hint.message, hint.tone)
281
+ }
282
+ })
283
+ .catch(() => {})
284
+ }, [])
285
+
286
+ const messageId = useCallback((msg: Msg) => {
287
+ const hit = msgIdsRef.current.get(msg)
288
+
289
+ if (hit) {
290
+ return hit
291
+ }
292
+
293
+ const next = `${messageHeightKey(msg)}:${++msgIdSeqRef.current}`
294
+
295
+ msgIdsRef.current.set(msg, next)
296
+
297
+ return next
298
+ }, [])
299
+
300
+ // Wrapped row heights are width-dependent. Cached layout outlives a resize
301
+ // and lands sticky-scroll at the stale max, cutting off the tail. The
302
+ // hook's "scale heights by oldCols/newCols" path is too approximate for
303
+ // mixed markdown — we deliberately remount every row so yoga re-measures
304
+ // off live geometry. Cost: per-row local state (e.g. systemOpen toggles)
305
+ // resets on resize; small UX hit for a hard correctness win.
306
+ const virtualRows = useMemo<TranscriptRow[]>(
307
+ () => historyItems.map((msg, index) => ({ index, key: `${messageId(msg)}:c${cols}`, msg })),
308
+ [cols, historyItems, messageId]
309
+ )
310
+
311
+ const detailsLayoutKey = useMemo(() => {
312
+ const thinking = sectionMode('thinking', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride)
313
+ const tools = sectionMode('tools', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride)
314
+
315
+ return `${thinking}:${tools}`
316
+ }, [ui.detailsMode, ui.detailsModeCommandOverride, ui.sections])
317
+
318
+ const [thinkingDetailsMode, toolsDetailsMode] = detailsLayoutKey.split(':')
319
+ const thinkingDetailsVisible = thinkingDetailsMode !== 'hidden'
320
+ const toolsDetailsVisible = toolsDetailsMode !== 'hidden'
321
+ const detailsVisible = thinkingDetailsVisible || toolsDetailsVisible
322
+ const userPromptWidth = composerPromptWidth(ui.theme.brand.prompt)
323
+ const heightCacheKey = `${ui.sid ?? 'draft'}:${cols}:${userPromptWidth}:${ui.compact ? '1' : '0'}:${detailsLayoutKey}`
324
+
325
+ const heightCache = useMemo(() => {
326
+ let cache = heightCachesRef.current.get(heightCacheKey)
327
+
328
+ if (!cache) {
329
+ cache = new Map()
330
+ heightCachesRef.current.set(heightCacheKey, cache)
331
+
332
+ if (heightCachesRef.current.size > MAX_HEIGHT_CACHE_BUCKETS) {
333
+ heightCachesRef.current.delete(heightCachesRef.current.keys().next().value!)
334
+ }
335
+ }
336
+
337
+ return cache
338
+ }, [heightCacheKey])
339
+
340
+ // Index of the first user-role message — separator-rendering in
341
+ // appLayout.tsx skips this row, so the height estimator must skip it
342
+ // too. -1 when no user message exists yet (no row will gate true).
343
+ const firstUserIdx = useMemo(() => virtualRows.findIndex(r => r.msg.role === 'user'), [virtualRows])
344
+
345
+ const estimateRowHeight = useCallback(
346
+ (index: number) =>
347
+ estimatedMsgHeight(virtualRows[index]!.msg, cols, {
348
+ compact: ui.compact,
349
+ details: detailsVisible,
350
+ thinkingVisible: thinkingDetailsVisible,
351
+ toolsVisible: toolsDetailsVisible,
352
+ userPrompt: ui.theme.brand.prompt,
353
+ withSeparator: virtualRows[index]!.msg.role === 'user' && firstUserIdx >= 0 && index > firstUserIdx
354
+ }),
355
+ [
356
+ cols,
357
+ detailsVisible,
358
+ firstUserIdx,
359
+ thinkingDetailsVisible,
360
+ toolsDetailsVisible,
361
+ ui.compact,
362
+ ui.theme.brand.prompt,
363
+ virtualRows
364
+ ]
365
+ )
366
+
367
+ const syncHeightCache = useCallback(
368
+ (heights: ReadonlyMap<string, number>) => {
369
+ for (const row of virtualRows) {
370
+ const h = heights.get(row.key)
371
+
372
+ if (h) {
373
+ heightCache.set(row.key, h)
374
+ }
375
+ }
376
+ },
377
+ [heightCache, virtualRows]
378
+ )
379
+
380
+ const virtualHistory = useVirtualHistory(scrollRef, virtualRows, cols, {
381
+ estimateHeight: estimateRowHeight,
382
+ initialHeights: heightCache,
383
+ liveTailActive: turnLiveTailActive,
384
+ onHeightsChange: syncHeightCache
385
+ })
386
+
387
+ const scrollWithSelection = useCallback(
388
+ (delta: number) => scrollWithSelectionBy(delta, { scrollRef, selection }),
389
+ [selection]
390
+ )
391
+
392
+ const appendMessage = useCallback(
393
+ (msg: Msg) => setHistoryItems(prev => capHistory(appendTranscriptMessage(prev, msg))),
394
+ []
395
+ )
396
+
397
+ const sys = useCallback((text: string) => appendMessage({ role: 'system', text }), [appendMessage])
398
+
399
+ const page = useCallback(
400
+ (text: string, title?: string) => patchOverlayState({ pager: { lines: text.split('\n'), offset: 0, title } }),
401
+ []
402
+ )
403
+
404
+ const panel = useCallback(
405
+ (title: string, sections: PanelSection[]) =>
406
+ appendMessage({ kind: 'panel', panelData: { sections, title }, role: 'system', text: '' }),
407
+ [appendMessage]
408
+ )
409
+
410
+ const maybeWarn = useCallback(
411
+ (value: unknown) => {
412
+ const warning = (value as { warning?: unknown } | null)?.warning
413
+
414
+ if (typeof warning === 'string' && warning) {
415
+ sys(`warning: ${warning}`)
416
+ }
417
+ },
418
+ [sys]
419
+ )
420
+
421
+ const maybeGoodVibes = useCallback((text: string) => {
422
+ if (GOOD_VIBES_RE.test(text)) {
423
+ setGoodVibesTick(v => v + 1)
424
+ }
425
+ }, [])
426
+
427
+ const rpc: GatewayRpc = useCallback(
428
+ async <T extends Record<string, any> = Record<string, any>>(
429
+ method: string,
430
+ params: Record<string, unknown> = {}
431
+ ) => {
432
+ try {
433
+ const result = asRpcResult<T>(await gw.request<T>(method, params))
434
+
435
+ if (result) {
436
+ return result
437
+ }
438
+
439
+ sys(`error: invalid response: ${method}`)
440
+ } catch (e) {
441
+ sys(`error: ${rpcErrorMessage(e)}`)
442
+ }
443
+
444
+ return null
445
+ },
446
+ [gw, sys]
447
+ )
448
+
449
+ const gateway = useMemo(() => ({ gw, rpc }), [gw, rpc])
450
+
451
+ const die = useCallback(() => {
452
+ gw.kill('app.die')
453
+ exit()
454
+ // Ink's exit() calls unmount() which resets terminal modes but does NOT
455
+ // call process.exit(). Without an explicit exit the Node process stays
456
+ // alive (stdin listener keeps the event loop open), so the process.on('exit')
457
+ // handler in entry.tsx — which sends the final resetTerminalModes() — never
458
+ // fires. This leaves kitty keyboard protocol, mouse modes, etc. enabled
459
+ // in the parent shell. See issue #19194.
460
+ process.exit(0)
461
+ }, [exit, gw])
462
+
463
+ const dieWithCode = useCallback((code: number) => {
464
+ gw.kill(`app.dieWithCode:${code}`)
465
+ exit()
466
+ process.exit(code)
467
+ }, [exit, gw])
468
+
469
+ const session = useSessionLifecycle({
470
+ colsRef,
471
+ composerActions,
472
+ gw,
473
+ panel,
474
+ rpc,
475
+ scrollRef,
476
+ setHistoryItems,
477
+ setLastUserMsg,
478
+ setSessionStartedAt,
479
+ setStickyPrompt,
480
+ setVoiceProcessing,
481
+ setVoiceRecording,
482
+ sys
483
+ })
484
+
485
+ useEffect(() => {
486
+ if (ui.busy) {
487
+ setTurnStartedAt(prev => prev ?? Date.now())
488
+ } else {
489
+ setTurnStartedAt(null)
490
+ }
491
+ }, [ui.busy])
492
+
493
+ useConfigSync({ gw, setBellOnComplete, setVoiceEnabled, setVoiceRecordKey, sid: ui.sid })
494
+
495
+ useEffect(() => {
496
+ if (!ui.sid) {
497
+ patchUiState({ liveSessionCount: 0 })
498
+
499
+ return
500
+ }
501
+
502
+ let stopped = false
503
+
504
+ const refresh = () => {
505
+ gw.request<SessionActiveListResponse>('session.active_list', { current_session_id: getUiState().sid })
506
+ .then(raw => {
507
+ const result = asRpcResult<SessionActiveListResponse>(raw)
508
+
509
+ if (!stopped && result?.sessions) {
510
+ patchUiState({ liveSessionCount: result.sessions.length })
511
+ }
512
+ })
513
+ .catch(() => {})
514
+ }
515
+
516
+ refresh()
517
+ const timer = setInterval(refresh, 1500)
518
+
519
+ return () => {
520
+ stopped = true
521
+ clearInterval(timer)
522
+ }
523
+ }, [gw, ui.sid])
524
+
525
+ // Tab title: `⚠` waiting on approval/sudo/secret/clarify, `⏳` busy, `✓` idle.
526
+ const model = ui.info?.model?.replace(/^.*\//, '') ?? ''
527
+
528
+ const marker = overlay.approval || overlay.sudo || overlay.secret || overlay.clarify ? '⚠' : ui.busy ? '⏳' : '✓'
529
+
530
+ const tabCwd = ui.info?.cwd
531
+
532
+ useTerminalTitle(model ? `${marker} ${model}${tabCwd ? ` · ${shortCwd(tabCwd, 24)}` : ''}` : 'NasTech')
533
+
534
+ useEffect(() => {
535
+ if (!ui.sid || !stdout) {
536
+ return
537
+ }
538
+
539
+ let timer: ReturnType<typeof setTimeout> | undefined
540
+
541
+ // Resize reflows wrapped lines; if the user is still pinned to the tail
542
+ // we need to re-snap once React has remeasured. virtualRows is keyed on
543
+ // cols so every column change forces a fresh measurement pass before
544
+ // this timer fires. Re-check isSticky() inside the timeout — a manual
545
+ // scroll during the 100ms window otherwise yanks the user back to tail.
546
+ const onResize = () => {
547
+ clearTimeout(timer)
548
+ timer = setTimeout(() => {
549
+ timer = undefined
550
+
551
+ if (scrollRef.current?.isSticky()) {
552
+ scrollRef.current.scrollToBottom()
553
+ }
554
+
555
+ void rpc<TerminalResizeResponse>('terminal.resize', { cols: stdout.columns ?? 80, session_id: ui.sid })
556
+ }, 100)
557
+ }
558
+
559
+ stdout.on('resize', onResize)
560
+
561
+ return () => {
562
+ clearTimeout(timer)
563
+ stdout.off('resize', onResize)
564
+ }
565
+ }, [rpc, stdout, ui.sid])
566
+
567
+ const answerClarify = useCallback(
568
+ (answer: string) => {
569
+ const clarify = overlay.clarify
570
+
571
+ if (!clarify) {
572
+ return
573
+ }
574
+
575
+ const label = toolTrailLabel('clarify')
576
+
577
+ turnController.turnTools = turnController.turnTools.filter(line => !sameToolTrailGroup(label, line))
578
+ patchTurnState({ turnTrail: turnController.turnTools })
579
+
580
+ rpc<ClarifyRespondResponse>('clarify.respond', { answer, request_id: clarify.requestId }).then(r => {
581
+ if (!r) {
582
+ return
583
+ }
584
+
585
+ if (answer) {
586
+ turnController.persistedToolLabels.add(label)
587
+ appendMessage({
588
+ kind: 'trail',
589
+ role: 'system',
590
+ text: '',
591
+ tools: [buildToolTrailLine('clarify', clarify.question)]
592
+ })
593
+ appendMessage({ role: 'user', text: answer })
594
+ patchUiState({ status: 'running…' })
595
+ } else {
596
+ sys('prompt cancelled')
597
+ }
598
+
599
+ patchOverlayState({ clarify: null })
600
+ })
601
+ },
602
+ [appendMessage, overlay.clarify, rpc, sys]
603
+ )
604
+
605
+ const paste = useCallback(
606
+ (quiet = false) =>
607
+ rpc<ClipboardPasteResponse>('clipboard.paste', { session_id: getUiState().sid }).then(r => {
608
+ if (!r) {
609
+ return
610
+ }
611
+
612
+ if (r.attached) {
613
+ const meta = imageTokenMeta(r)
614
+
615
+ return sys(`📎 Image #${r.count} attached from clipboard${meta ? ` · ${meta}` : ''}`)
616
+ }
617
+
618
+ if (!quiet) {
619
+ sys(r.message || 'No image found in clipboard')
620
+ }
621
+ }),
622
+ [rpc, sys]
623
+ )
624
+
625
+ clipboardPasteRef.current = paste
626
+
627
+ const { dispatchSubmission, send, sendQueued, submit } = useSubmission({
628
+ appendMessage,
629
+ composerActions,
630
+ composerRefs,
631
+ composerState,
632
+ gw,
633
+ maybeGoodVibes,
634
+ setLastUserMsg,
635
+ slashRef,
636
+ submitRef,
637
+ sys
638
+ })
639
+
640
+ // Drain one queued message whenever the session settles (busy → false):
641
+ // agent turn ends, interrupt, shell.exec finishes, error recovered, or the
642
+ // session first comes up with pre-queued messages. Without this, shell.exec
643
+ // and error paths never emit message.complete, so anything enqueued while
644
+ // `!sleep` / a failed turn was running would stay stuck forever.
645
+ useEffect(() => {
646
+ if (
647
+ !ui.sid ||
648
+ ui.busy ||
649
+ composerRefs.queueEditRef.current !== null ||
650
+ composerRefs.queueRef.current.length === 0
651
+ ) {
652
+ return
653
+ }
654
+
655
+ const next = composerActions.dequeue()
656
+
657
+ if (next) {
658
+ patchUiState({ busy: true, status: 'running…' })
659
+ sendQueued(next)
660
+ }
661
+ }, [ui.sid, ui.busy, composerActions, composerRefs, sendQueued])
662
+
663
+ const { pagerPageSize } = useInputHandlers({
664
+ actions: {
665
+ answerClarify,
666
+ appendMessage,
667
+ die,
668
+ dispatchSubmission,
669
+ guardBusySessionSwitch: session.guardBusySessionSwitch,
670
+ newSession: session.newSession,
671
+ sys
672
+ },
673
+ composer: { actions: composerActions, refs: composerRefs, state: composerState },
674
+ gateway,
675
+ terminal: { hasSelection, scrollRef, scrollWithSelection, selection, stdout },
676
+ voice: {
677
+ enabled: voiceEnabled,
678
+ recordKey: voiceRecordKey,
679
+ recording: voiceRecording,
680
+ setProcessing: setVoiceProcessing,
681
+ setRecording: setVoiceRecording,
682
+ setVoiceEnabled,
683
+ setVoiceTts
684
+ },
685
+ wheelStep: WHEEL_SCROLL_STEP
686
+ })
687
+
688
+ const onEvent = useMemo(
689
+ () =>
690
+ createGatewayEventHandler({
691
+ composer: { setInput: composerActions.setInput },
692
+ gateway,
693
+ session: {
694
+ STARTUP_RESUME_ID,
695
+ colsRef,
696
+ newSession: session.newSession,
697
+ resetSession: session.resetSession,
698
+ resumeById: session.resumeById,
699
+ setCatalog
700
+ },
701
+ submission: { submitRef },
702
+ system: { bellOnComplete, stdout, sys },
703
+ transcript: { appendMessage, panel, setHistoryItems },
704
+ voice: {
705
+ setProcessing: setVoiceProcessing,
706
+ setRecording: setVoiceRecording,
707
+ setVoiceEnabled,
708
+ setVoiceTts
709
+ }
710
+ }),
711
+ [
712
+ appendMessage,
713
+ bellOnComplete,
714
+ clearSelection,
715
+ composerActions.setInput,
716
+ gateway,
717
+ panel,
718
+ session.newSession,
719
+ session.resetSession,
720
+ session.resumeById,
721
+ setVoiceEnabled,
722
+ setVoiceProcessing,
723
+ setVoiceRecording,
724
+ stdout,
725
+ submitRef,
726
+ sys
727
+ ]
728
+ )
729
+
730
+ onEventRef.current = onEvent
731
+
732
+ useEffect(() => {
733
+ const handler = (ev: GatewayEvent) => onEventRef.current(ev)
734
+
735
+ const exitHandler = () => {
736
+ turnController.reset()
737
+ patchUiState({ busy: false, sid: null, status: 'gateway exited' })
738
+ turnController.pushActivity('gateway exited · /logs to inspect', 'error')
739
+ sys('error: gateway exited')
740
+ }
741
+
742
+ gw.on('event', handler)
743
+ gw.on('exit', exitHandler)
744
+ gw.drain()
745
+
746
+ // entry.tsx's setupGracefulExit handles process cleanup on real exit.
747
+ return () => {
748
+ gw.off('event', handler)
749
+ gw.off('exit', exitHandler)
750
+ }
751
+ }, [gw, sys])
752
+
753
+ useLongRunToolCharms()
754
+
755
+ const slash = useMemo(
756
+ () =>
757
+ createSlashHandler({
758
+ composer: {
759
+ enqueue: composerActions.enqueue,
760
+ hasSelection,
761
+ paste,
762
+ queueRef: composerRefs.queueRef,
763
+ selection,
764
+ setInput: composerActions.setInput
765
+ },
766
+ gateway,
767
+ local: {
768
+ catalog,
769
+ getHistoryItems: () => historyItemsRef.current,
770
+ getLastUserMsg: () => lastUserMsgRef.current,
771
+ maybeWarn,
772
+ setCatalog
773
+ },
774
+ session: {
775
+ closeSession: session.closeSession,
776
+ die,
777
+ dieWithCode,
778
+ guardBusySessionSwitch: session.guardBusySessionSwitch,
779
+ newLiveSession: session.newLiveSession,
780
+ newSession: session.newSession,
781
+ resetVisibleHistory: session.resetVisibleHistory,
782
+ resumeById: session.resumeById,
783
+ setSessionStartedAt
784
+ },
785
+ slashFlightRef,
786
+ transcript: { page, panel, send, setHistoryItems, sys, trimLastExchange: session.trimLastExchange },
787
+ voice: { setVoiceEnabled, setVoiceRecordKey, setVoiceTts }
788
+ }),
789
+ [
790
+ catalog,
791
+ composerActions,
792
+ composerRefs,
793
+ die,
794
+ gateway,
795
+ hasSelection,
796
+ maybeWarn,
797
+ page,
798
+ panel,
799
+ paste,
800
+ selection,
801
+ send,
802
+ session,
803
+ sys
804
+ ]
805
+ )
806
+
807
+ slashRef.current = slash
808
+
809
+ const respondWith = useCallback(
810
+ (method: string, params: Record<string, unknown>, done: () => void) => rpc(method, params).then(r => r && done()),
811
+ [rpc]
812
+ )
813
+
814
+ const answerApproval = useCallback(
815
+ (choice: string) =>
816
+ respondWith('approval.respond', { choice, session_id: ui.sid }, () => {
817
+ patchOverlayState({ approval: null })
818
+ patchTurnState({ outcome: choice === 'deny' ? 'denied' : `approved (${choice})` })
819
+ patchUiState({ status: 'running…' })
820
+ }),
821
+ [respondWith, ui.sid]
822
+ )
823
+
824
+ const answerSudo = useCallback(
825
+ (pw: string) => {
826
+ if (!overlay.sudo) {
827
+ return
828
+ }
829
+
830
+ return respondWith('sudo.respond', { password: pw, request_id: overlay.sudo.requestId }, () => {
831
+ patchOverlayState({ sudo: null })
832
+ patchUiState({ status: 'running…' })
833
+ })
834
+ },
835
+ [overlay.sudo, respondWith]
836
+ )
837
+
838
+ const answerSecret = useCallback(
839
+ (value: string) => {
840
+ if (!overlay.secret) {
841
+ return
842
+ }
843
+
844
+ return respondWith('secret.respond', { request_id: overlay.secret.requestId, value }, () => {
845
+ patchOverlayState({ secret: null })
846
+ patchUiState({ status: 'running…' })
847
+ })
848
+ },
849
+ [overlay.secret, respondWith]
850
+ )
851
+
852
+ const onModelSelect = useCallback((value: string) => {
853
+ patchOverlayState({ modelPicker: false })
854
+ slashRef.current(`/model ${value}`)
855
+ }, [])
856
+
857
+ const closeLiveSession = useCallback(
858
+ async (id: string) => {
859
+ patchUiState({ status: 'closing session…' })
860
+
861
+ try {
862
+ const result = (await session.closeSession(id)) as null | SessionCloseResponse
863
+ patchUiState({ status: 'ready' })
864
+
865
+ return result
866
+ } catch (e: unknown) {
867
+ const message = e instanceof Error ? e.message : String(e)
868
+ sys(`error: ${message}`)
869
+ patchUiState({ status: 'ready' })
870
+
871
+ throw e
872
+ }
873
+ },
874
+ [session, sys]
875
+ )
876
+
877
+ const newPromptSession = useCallback(
878
+ (prompt: string, modelArg?: string) => {
879
+ void startPromptLiveSession({
880
+ dispatchSubmission,
881
+ maybeWarn,
882
+ modelArg,
883
+ newLiveSession: session.newLiveSession,
884
+ onModelSwitched: value =>
885
+ patchUiState(state => ({
886
+ ...state,
887
+ info: state.info ? { ...state.info, model: value } : { model: value, skills: {}, tools: {} }
888
+ })),
889
+ prompt,
890
+ rpc,
891
+ sys
892
+ })
893
+ },
894
+ [dispatchSubmission, maybeWarn, rpc, session.newLiveSession, sys]
895
+ )
896
+
897
+ const hasReasoning = useTurnSelector(state => Boolean(state.reasoning.trim()))
898
+
899
+ // Per-section overrides win over the global mode — when every section is
900
+ // resolved to hidden, the only thing ToolTrail will surface is the
901
+ // floating-alert backstop (errors/warnings). Mirror that so we don't
902
+ // render an empty wrapper Box above the streaming area in quiet mode.
903
+ const anyPanelVisible = SECTION_NAMES.some(
904
+ s => sectionMode(s, ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden'
905
+ )
906
+
907
+ const thinkingPanelVisible =
908
+ sectionMode('thinking', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden'
909
+
910
+ const toolsPanelVisible =
911
+ sectionMode('tools', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden'
912
+
913
+ const activityPanelVisible =
914
+ sectionMode('activity', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden'
915
+
916
+ const showProgressArea = useTurnSelector(state =>
917
+ anyPanelVisible
918
+ ? Boolean(
919
+ ui.busy ||
920
+ state.outcome ||
921
+ state.streamPendingTools.length ||
922
+ state.streamSegments.some(segment => {
923
+ const hasThinking = Boolean(segment.thinking?.trim())
924
+ const hasTrailTools = Boolean(segment.tools?.length)
925
+
926
+ if (segment.kind === 'trail' && !segment.text) {
927
+ return (
928
+ (thinkingPanelVisible && hasThinking) || ((toolsPanelVisible || activityPanelVisible) && hasTrailTools)
929
+ )
930
+ }
931
+
932
+ return (
933
+ Boolean(segment.text?.trim()) ||
934
+ (thinkingPanelVisible && hasThinking) ||
935
+ ((toolsPanelVisible || activityPanelVisible) && hasTrailTools)
936
+ )
937
+ }) ||
938
+ state.subagents.length ||
939
+ state.tools.length ||
940
+ state.todos.length ||
941
+ state.turnTrail.length ||
942
+ (thinkingPanelVisible && hasReasoning) ||
943
+ state.activity.length
944
+ )
945
+ : state.activity.some(item => item.tone !== 'info')
946
+ )
947
+
948
+ const appActions = useMemo(
949
+ () => ({
950
+ activateLiveSession: session.activateLiveSession,
951
+ closeLiveSession,
952
+ answerApproval,
953
+ answerClarify,
954
+ answerSecret,
955
+ answerSudo,
956
+ clearSelection,
957
+ newLiveSession: () => session.newLiveSession(),
958
+ newPromptSession,
959
+ onModelSelect,
960
+ resumeById: session.resumeById,
961
+ setStickyPrompt
962
+ }),
963
+ [
964
+ answerApproval,
965
+ answerClarify,
966
+ answerSecret,
967
+ answerSudo,
968
+ clearSelection,
969
+ closeLiveSession,
970
+ newPromptSession,
971
+ onModelSelect,
972
+ session.activateLiveSession,
973
+ session.newLiveSession,
974
+ session.resumeById
975
+ ]
976
+ )
977
+
978
+ const appComposer = useMemo(
979
+ () => ({
980
+ cols,
981
+ compIdx: composerState.compIdx,
982
+ completions: composerState.completions,
983
+ empty,
984
+ handleTextPaste: composerActions.handleTextPaste,
985
+ input: composerState.input,
986
+ inputBuf: composerState.inputBuf,
987
+ pagerPageSize,
988
+ queueEditIdx: composerState.queueEditIdx,
989
+ queuedDisplay: composerState.queuedDisplay,
990
+ submit,
991
+ updateInput: composerActions.setInput,
992
+ voiceRecordKey
993
+ }),
994
+ [cols, composerActions, composerState, empty, pagerPageSize, submit, voiceRecordKey]
995
+ )
996
+
997
+ // Pass current progress through unfrozen — streaming update throttling
998
+ // handles interaction load; progress must stay truthful so panels don't
999
+ // randomly disappear when the live tail scrolls offscreen.
1000
+ const appProgress = useMemo(() => ({ showProgressArea }), [showProgressArea])
1001
+
1002
+ const cwd = ui.info?.cwd || process.env.NASTECH_CWD || process.cwd()
1003
+ const gitBranch = useGitBranch(cwd)
1004
+
1005
+ const appStatus = useMemo(
1006
+ () => ({
1007
+ cwdLabel: fmtCwdBranch(cwd, gitBranch),
1008
+ goodVibesTick,
1009
+ sessionStartedAt: ui.sid ? sessionStartedAt : null,
1010
+ showStickyPrompt: !!stickyPrompt,
1011
+ statusColor: statusColorOf(ui.status, ui.theme.color),
1012
+ stickyPrompt,
1013
+ turnStartedAt: ui.sid ? turnStartedAt : null,
1014
+ // CLI parity: the classic prompt_toolkit status bar shows a red dot
1015
+ // on REC (cli.py:_get_voice_status_fragments line 2344).
1016
+ voiceLabel: voiceRecording ? '● REC' : voiceProcessing ? '◉ STT' : `voice ${voiceEnabled ? 'on' : 'off'}${voiceTts ? ' [tts]' : ''}`
1017
+ }),
1018
+ [
1019
+ cwd,
1020
+ gitBranch,
1021
+ goodVibesTick,
1022
+ sessionStartedAt,
1023
+ stickyPrompt,
1024
+ turnStartedAt,
1025
+ ui,
1026
+ voiceEnabled,
1027
+ voiceProcessing,
1028
+ voiceRecording,
1029
+ voiceTts
1030
+ ]
1031
+ )
1032
+
1033
+ const appTranscript = useMemo(
1034
+ () => ({ historyItems, scrollRef, virtualHistory, virtualRows }),
1035
+ [historyItems, virtualHistory, virtualRows]
1036
+ )
1037
+
1038
+ return { appActions, appComposer, appProgress, appStatus, appTranscript, gateway }
1039
+ }