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,730 @@
1
+ import { type ChildProcess, spawn } from 'node:child_process'
2
+ import { EventEmitter } from 'node:events'
3
+ import { existsSync } from 'node:fs'
4
+ import { delimiter, resolve } from 'node:path'
5
+ import { createInterface } from 'node:readline'
6
+
7
+ import type { GatewayEvent } from './gatewayTypes.js'
8
+ import { CircularBuffer } from './lib/circularBuffer.js'
9
+
10
+ const MAX_GATEWAY_LOG_LINES = 200
11
+ const MAX_LOG_LINE_BYTES = 4096
12
+ const MAX_BUFFERED_EVENTS = 2000
13
+ const MAX_LOG_PREVIEW = 240
14
+ const STARTUP_TIMEOUT_MS = Math.max(5000, parseInt(process.env.NASTECH_TUI_STARTUP_TIMEOUT_MS ?? '15000', 10) || 15000)
15
+ const REQUEST_TIMEOUT_MS = Math.max(30000, parseInt(process.env.NASTECH_TUI_RPC_TIMEOUT_MS ?? '120000', 10) || 120000)
16
+ const WS_CONNECTING = 0
17
+ const WS_OPEN = 1
18
+ const WS_CLOSING = 2
19
+ const WS_CLOSED = 3
20
+
21
+ const truncateLine = (line: string) =>
22
+ line.length > MAX_LOG_LINE_BYTES ? `${line.slice(0, MAX_LOG_LINE_BYTES)}… [truncated ${line.length} bytes]` : line
23
+
24
+ const describeChild = (proc: ChildProcess | null) => {
25
+ if (!proc) {
26
+ return 'pid=none'
27
+ }
28
+
29
+ return `pid=${proc.pid ?? 'unknown'} killed=${proc.killed} exitCode=${proc.exitCode ?? 'null'} signal=${proc.signalCode ?? 'null'}`
30
+ }
31
+
32
+ const resolveGatewayAttachUrl = () => {
33
+ const raw = process.env.NASTECH_TUI_GATEWAY_URL?.trim()
34
+
35
+ return raw ? raw : null
36
+ }
37
+
38
+ const resolveSidecarUrl = () => {
39
+ const raw = process.env.NASTECH_TUI_SIDECAR_URL?.trim()
40
+
41
+ return raw ? raw : null
42
+ }
43
+
44
+ const resolvePython = (root: string) => {
45
+ const configured = process.env.NASTECH_PYTHON?.trim() || process.env.PYTHON?.trim()
46
+
47
+ if (configured) {
48
+ return configured
49
+ }
50
+
51
+ const venv = process.env.VIRTUAL_ENV?.trim()
52
+
53
+ const hit = [
54
+ venv && resolve(venv, 'bin/python'),
55
+ venv && resolve(venv, 'Scripts/python.exe'),
56
+ resolve(root, '.venv/bin/python'),
57
+ resolve(root, '.venv/bin/python3'),
58
+ resolve(root, 'venv/bin/python'),
59
+ resolve(root, 'venv/bin/python3')
60
+ ].find(p => p && existsSync(p))
61
+
62
+ return hit || (process.platform === 'win32' ? 'python' : 'python3')
63
+ }
64
+
65
+ const asGatewayEvent = (value: unknown): GatewayEvent | null =>
66
+ value && typeof value === 'object' && !Array.isArray(value) && typeof (value as { type?: unknown }).type === 'string'
67
+ ? (value as GatewayEvent)
68
+ : null
69
+
70
+ // Hoisted decoder: attach mode can drive high-frequency binary frames
71
+ // (tool deltas, reasoning streams) and constructing a fresh TextDecoder
72
+ // per message creates avoidable GC pressure. One module-level instance
73
+ // is fine because UTF-8 is stateless and we always pass entire frames.
74
+ const _wireDecoder = new TextDecoder()
75
+
76
+ const asWireText = (raw: unknown): string | null => {
77
+ if (typeof raw === 'string') {
78
+ return raw
79
+ }
80
+
81
+ if (raw instanceof ArrayBuffer) {
82
+ return _wireDecoder.decode(raw)
83
+ }
84
+
85
+ if (ArrayBuffer.isView(raw)) {
86
+ return _wireDecoder.decode(raw)
87
+ }
88
+
89
+ return null
90
+ }
91
+
92
+ // Matches `<scheme>://user:pass@host…` style user-info segments in
93
+ // otherwise-malformed URLs that the WHATWG `URL` parser can't accept.
94
+ // Used by the `redactUrl` fallback so embedded credentials are
95
+ // scrubbed from log lines even when the URL is unparseable.
96
+ const _USERINFO_FALLBACK_RE = /^([a-z][a-z0-9+.-]*:\/\/)[^/?#@]*@/i
97
+
98
+ // Connection URLs (gateway, sidecar) often carry bearer tokens in the query
99
+ // string. We surface them in user-facing log lines and the
100
+ // `gateway.start_timeout` payload, so always strip the query string and any
101
+ // embedded user-info before logging.
102
+ const redactUrl = (raw: string): string => {
103
+ if (!raw) {
104
+ return raw
105
+ }
106
+
107
+ try {
108
+ const url = new URL(raw)
109
+ const userInfo = url.username || url.password ? '***@' : ''
110
+ const query = url.search ? '?***' : ''
111
+
112
+ return `${url.protocol}//${userInfo}${url.host}${url.pathname}${query}`
113
+ } catch {
114
+ // WHATWG URL rejected the input. Best-effort: strip an embedded
115
+ // `user:pass@` segment AND the query string so a malformed token
116
+ // bearer can never escape into the log tail.
117
+ const noUserInfo = raw.replace(_USERINFO_FALLBACK_RE, '$1***@')
118
+ const queryIdx = noUserInfo.indexOf('?')
119
+
120
+ return queryIdx >= 0 ? `${noUserInfo.slice(0, queryIdx)}?***` : noUserInfo
121
+ }
122
+ }
123
+
124
+ interface Pending {
125
+ id: string
126
+ method: string
127
+ reject: (e: Error) => void
128
+ resolve: (v: unknown) => void
129
+ timeout: ReturnType<typeof setTimeout>
130
+ }
131
+
132
+ export class GatewayClient extends EventEmitter {
133
+ private proc: ChildProcess | null = null
134
+ private ws: WebSocket | null = null
135
+ private wsConnectPromise: Promise<void> | null = null
136
+ private sidecarWs: WebSocket | null = null
137
+ private attachUrl: null | string = null
138
+ private sidecarUrl: null | string = null
139
+ private reqId = 0
140
+ private logs = new CircularBuffer<string>(MAX_GATEWAY_LOG_LINES)
141
+ private pending = new Map<string, Pending>()
142
+ private bufferedEvents = new CircularBuffer<GatewayEvent>(MAX_BUFFERED_EVENTS)
143
+ private pendingExit: number | null | undefined
144
+ private ready = false
145
+ private readyTimer: ReturnType<typeof setTimeout> | null = null
146
+ private subscribed = false
147
+ private stdoutRl: ReturnType<typeof createInterface> | null = null
148
+ private stderrRl: ReturnType<typeof createInterface> | null = null
149
+
150
+ constructor() {
151
+ super()
152
+ // useInput / createGatewayEventHandler can legitimately attach many
153
+ // listeners. Default 10-cap triggers spurious warnings.
154
+ this.setMaxListeners(0)
155
+ }
156
+
157
+ private publish(ev: GatewayEvent) {
158
+ if (ev.type === 'gateway.ready') {
159
+ this.ready = true
160
+
161
+ if (this.readyTimer) {
162
+ clearTimeout(this.readyTimer)
163
+ this.readyTimer = null
164
+ }
165
+ }
166
+
167
+ if (this.subscribed) {
168
+ return void this.emit('event', ev)
169
+ }
170
+
171
+ this.bufferedEvents.push(ev)
172
+ }
173
+
174
+ private clearReadyTimer() {
175
+ if (this.readyTimer) {
176
+ clearTimeout(this.readyTimer)
177
+ this.readyTimer = null
178
+ }
179
+ }
180
+
181
+ private closeSidecarSocket() {
182
+ try {
183
+ this.sidecarWs?.close()
184
+ } catch {
185
+ // best effort
186
+ } finally {
187
+ this.sidecarWs = null
188
+ }
189
+ }
190
+
191
+ private closeGatewaySocket() {
192
+ // Null the active reference BEFORE invoking close(): real WebSocket
193
+ // implementations dispatch the 'close' event after a microtask hop,
194
+ // so by the time the handler runs `this.ws` should already be null
195
+ // and the identity guard will correctly classify the close as
196
+ // belonging to a discarded socket. (Test fakes emit synchronously,
197
+ // so doing the swap up front is also what makes the identity guard
198
+ // match real timing in tests.)
199
+ const ws = this.ws
200
+ this.ws = null
201
+ this.wsConnectPromise = null
202
+
203
+ try {
204
+ ws?.close()
205
+ } catch {
206
+ // best effort
207
+ }
208
+ }
209
+
210
+ private resetStartupState() {
211
+ // Reject any in-flight RPCs left over from the previous transport
212
+ // before we swap. Otherwise the old transport's stale exit/close
213
+ // handlers (now identity-gated to ignore unrelated transports)
214
+ // never fire `rejectPending`, leaving callers hanging on promises
215
+ // attached to a discarded child / socket.
216
+ this.rejectPending(new Error('gateway restarting'))
217
+ this.ready = false
218
+ this.bufferedEvents.clear()
219
+ this.pendingExit = undefined
220
+ this.stdoutRl?.close()
221
+ this.stderrRl?.close()
222
+ this.stdoutRl = null
223
+ this.stderrRl = null
224
+ this.clearReadyTimer()
225
+ }
226
+
227
+ private startReadyTimer(python: string, cwd: string) {
228
+ this.readyTimer = setTimeout(() => {
229
+ if (this.ready) {
230
+ return
231
+ }
232
+
233
+ // Append the most recent gateway stderr/log lines to the timeout
234
+ // event so users can tell apart "wrong python", "missing dep",
235
+ // and "config parse failure" from one glance instead of having
236
+ // to dig through `/logs`. Capped to keep the activity feed
237
+ // readable on slow boots.
238
+ const stderrTail = this.getLogTail(20)
239
+
240
+ this.pushLog(`[startup] timed out waiting for gateway.ready (python=${python}, cwd=${cwd})`)
241
+ this.publish({
242
+ type: 'gateway.start_timeout',
243
+ payload: { cwd, python, stderr_tail: stderrTail }
244
+ })
245
+ }, STARTUP_TIMEOUT_MS)
246
+ }
247
+
248
+ private handleTransportExit(code: null | number, reason?: string) {
249
+ this.clearReadyTimer()
250
+ this.closeSidecarSocket()
251
+ this.pushLog(`[lifecycle] transport exit code=${code ?? 'null'} reason=${reason ?? 'none'}`)
252
+ this.rejectPending(new Error(reason || `gateway exited${code === null ? '' : ` (${code})`}`))
253
+
254
+ if (this.subscribed) {
255
+ this.emit('exit', code)
256
+ } else {
257
+ this.pendingExit = code
258
+ }
259
+ }
260
+
261
+ private connectSidecarMirror() {
262
+ this.closeSidecarSocket()
263
+
264
+ if (!this.sidecarUrl) {
265
+ return
266
+ }
267
+
268
+ if (typeof WebSocket === 'undefined') {
269
+ this.pushLog(`[sidecar] WebSocket unavailable; skipping mirror to ${redactUrl(this.sidecarUrl)}`)
270
+
271
+ return
272
+ }
273
+
274
+ try {
275
+ const ws = new WebSocket(this.sidecarUrl)
276
+
277
+ this.sidecarWs = ws
278
+ ws.addEventListener('close', () => {
279
+ if (this.sidecarWs === ws) {
280
+ this.sidecarWs = null
281
+ }
282
+ })
283
+ ws.addEventListener('error', () => {
284
+ this.pushLog('[sidecar] mirror connection error')
285
+ })
286
+ } catch (err) {
287
+ this.pushLog(`[sidecar] failed to connect ${redactUrl(this.sidecarUrl)} (constructor error)`)
288
+ this.sidecarWs = null
289
+ }
290
+ }
291
+
292
+ private mirrorEventToSidecar(rawFrame: string) {
293
+ const ws = this.sidecarWs
294
+
295
+ if (!ws || ws.readyState !== WS_OPEN) {
296
+ return
297
+ }
298
+
299
+ try {
300
+ ws.send(rawFrame)
301
+ } catch {
302
+ // best effort
303
+ }
304
+ }
305
+
306
+ private handleWebSocketFrame(raw: unknown) {
307
+ const text = asWireText(raw)
308
+
309
+ if (!text) {
310
+ return
311
+ }
312
+
313
+ try {
314
+ const frame = JSON.parse(text) as Record<string, unknown>
315
+
316
+ if (frame.method === 'event') {
317
+ this.mirrorEventToSidecar(text)
318
+ }
319
+
320
+ this.dispatch(frame)
321
+ } catch {
322
+ const preview = text.trim().slice(0, MAX_LOG_PREVIEW) || '(empty frame)'
323
+
324
+ this.pushLog(`[protocol] malformed websocket frame: ${preview}`)
325
+ this.publish({ type: 'gateway.protocol_error', payload: { preview } })
326
+ }
327
+ }
328
+
329
+ private startSpawnedGateway(root: string) {
330
+ const python = resolvePython(root)
331
+ const cwd = process.env.NASTECH_CWD || root
332
+ const env = { ...process.env }
333
+ const pyPath = env.PYTHONPATH?.trim()
334
+
335
+ env.PYTHONPATH = pyPath ? `${root}${delimiter}${pyPath}` : root
336
+ this.startReadyTimer(python, cwd)
337
+ this.proc = spawn(python, ['-m', 'tui_gateway.entry'], { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
338
+ this.pushLog(`[lifecycle] spawned gateway child ${describeChild(this.proc)} python=${python} cwd=${cwd}`)
339
+
340
+ this.stdoutRl = createInterface({ input: this.proc.stdout! })
341
+ this.stdoutRl.on('line', raw => {
342
+ try {
343
+ this.dispatch(JSON.parse(raw))
344
+ } catch {
345
+ const preview = raw.trim().slice(0, MAX_LOG_PREVIEW) || '(empty line)'
346
+
347
+ this.pushLog(`[protocol] malformed stdout: ${preview}`)
348
+ this.publish({ type: 'gateway.protocol_error', payload: { preview } })
349
+ }
350
+ })
351
+
352
+ this.stderrRl = createInterface({ input: this.proc.stderr! })
353
+ this.stderrRl.on('line', raw => {
354
+ const line = truncateLine(raw.trim())
355
+
356
+ if (!line) {
357
+ return
358
+ }
359
+
360
+ this.pushLog(line)
361
+ this.publish({ type: 'gateway.stderr', payload: { line } })
362
+ })
363
+
364
+ const ownedProc = this.proc
365
+ this.proc.on('error', err => {
366
+ // Skip stale errors on an already-replaced child.
367
+ if (this.proc !== ownedProc) {
368
+ this.pushLog(`[lifecycle] stale child error ignored ${describeChild(ownedProc)} message=${err.message}`)
369
+
370
+ return
371
+ }
372
+
373
+ const line = `[spawn] ${err.message}`
374
+
375
+ this.pushLog(`[lifecycle] child error ${describeChild(ownedProc)} message=${err.message}`)
376
+ this.pushLog(line)
377
+ this.publish({ type: 'gateway.stderr', payload: { line } })
378
+ // Detach the reference up front so the late `exit` event for
379
+ // this same child is identity-skipped (we don't want to emit
380
+ // 'exit' twice). Then run the full teardown — clears the
381
+ // startup timer so we don't fire a misleading
382
+ // `gateway.start_timeout`, rejects pending RPCs, and emits or
383
+ // queues a single `exit`.
384
+ this.proc = null
385
+ this.handleTransportExit(1, `gateway error: ${err.message}`)
386
+ })
387
+ this.proc.on('exit', (code, signal) => {
388
+ // start() can replace `this.proc` while an old child is still
389
+ // tearing down. Skip stale exits so we don't clear the new
390
+ // startup timer or reject newly-issued pending requests.
391
+ if (this.proc !== ownedProc) {
392
+ this.pushLog(
393
+ `[lifecycle] stale child exit ignored ${describeChild(ownedProc)} code=${code ?? 'null'} signal=${signal ?? 'null'}`
394
+ )
395
+
396
+ return
397
+ }
398
+
399
+ this.pushLog(`[lifecycle] child exit ${describeChild(ownedProc)} code=${code ?? 'null'} signal=${signal ?? 'null'}`)
400
+ this.handleTransportExit(code)
401
+ })
402
+ }
403
+
404
+ private startAttachedGateway(attachUrl: string) {
405
+ const safeAttachUrl = redactUrl(attachUrl)
406
+ this.startReadyTimer('websocket', safeAttachUrl)
407
+
408
+ if (typeof WebSocket === 'undefined') {
409
+ const line = `[startup] WebSocket API unavailable; cannot attach to ${safeAttachUrl}`
410
+
411
+ this.pushLog(line)
412
+ this.publish({ type: 'gateway.stderr', payload: { line } })
413
+ this.handleTransportExit(1, 'gateway websocket unavailable')
414
+
415
+ return
416
+ }
417
+
418
+ try {
419
+ const ws = new WebSocket(attachUrl)
420
+ let settled = false
421
+
422
+ this.ws = ws
423
+
424
+ const connectPromise = new Promise<void>((resolve, reject) => {
425
+ ws.addEventListener(
426
+ 'open',
427
+ () => {
428
+ if (!settled) {
429
+ settled = true
430
+ resolve()
431
+ }
432
+
433
+ this.connectSidecarMirror()
434
+ },
435
+ { once: true }
436
+ )
437
+
438
+ ws.addEventListener(
439
+ 'error',
440
+ () => {
441
+ if (!settled) {
442
+ this.pushLog('[startup] gateway websocket connect error')
443
+ settled = true
444
+ reject(new Error('gateway websocket connection failed'))
445
+ }
446
+ },
447
+ { once: true }
448
+ )
449
+ ws.addEventListener(
450
+ 'close',
451
+ ev => {
452
+ if (!settled) {
453
+ settled = true
454
+ reject(new Error(`gateway websocket closed (${ev.code}) during connect`))
455
+ }
456
+ },
457
+ { once: true }
458
+ )
459
+ })
460
+
461
+ // The connect promise is only awaited by RPCs that arrive while
462
+ // the socket is still connecting. If no request races the open
463
+ // (or a teardown drops the reference before anyone observes it),
464
+ // a connect-error / early-close rejection would surface as an
465
+ // unhandled promise rejection in Node. Attach a no-op handler to
466
+ // ensure the rejection is always observed.
467
+ connectPromise.catch(() => {})
468
+ this.wsConnectPromise = connectPromise
469
+
470
+ ws.addEventListener('message', ev => this.handleWebSocketFrame(ev.data))
471
+ ws.addEventListener('close', ev => {
472
+ // Skip close events from sockets that have already been
473
+ // replaced — start() / closeGatewaySocket() can swap `this.ws`
474
+ // before an in-flight close lands, and we must not clear the
475
+ // new ready timer or reject the new pending requests on behalf
476
+ // of a stale socket.
477
+ if (this.ws !== ws) {
478
+ this.pushLog(`[lifecycle] stale websocket close ignored code=${ev.code}`)
479
+
480
+ return
481
+ }
482
+
483
+ this.pushLog(`[lifecycle] websocket close code=${ev.code}`)
484
+ this.ws = null
485
+ this.wsConnectPromise = null
486
+ this.handleTransportExit(ev.code, `gateway websocket closed${ev.code ? ` (${ev.code})` : ''}`)
487
+ })
488
+ ws.addEventListener('error', () => {
489
+ const line = '[gateway] websocket transport error'
490
+
491
+ this.pushLog(line)
492
+ this.publish({ type: 'gateway.stderr', payload: { line } })
493
+ })
494
+ } catch (err) {
495
+ this.pushLog(`[startup] failed to connect websocket gateway ${safeAttachUrl} (constructor error)`)
496
+ this.handleTransportExit(1, 'gateway websocket startup failed')
497
+ }
498
+ }
499
+
500
+ start() {
501
+ const root = process.env.NASTECH_PYTHON_SRC_ROOT ?? resolve(import.meta.dirname, '../../')
502
+ const attachUrl = resolveGatewayAttachUrl()
503
+ const sidecarUrl = resolveSidecarUrl()
504
+
505
+ this.attachUrl = attachUrl
506
+ this.sidecarUrl = sidecarUrl
507
+ this.resetStartupState()
508
+
509
+ if (this.proc && !this.proc.killed && this.proc.exitCode === null) {
510
+ this.pushLog(`[lifecycle] replacing live gateway child ${describeChild(this.proc)}`)
511
+ this.proc.kill()
512
+ }
513
+
514
+ this.proc = null
515
+ this.closeGatewaySocket()
516
+ this.closeSidecarSocket()
517
+
518
+ if (attachUrl) {
519
+ this.startAttachedGateway(attachUrl)
520
+
521
+ return
522
+ }
523
+
524
+ this.startSpawnedGateway(root)
525
+ }
526
+
527
+ private dispatch(msg: Record<string, unknown>) {
528
+ const id = msg.id as string | undefined
529
+ const p = id ? this.pending.get(id) : undefined
530
+
531
+ if (p) {
532
+ this.settle(p, msg.error ? this.toError(msg.error) : null, msg.result)
533
+
534
+ return
535
+ }
536
+
537
+ if (msg.method === 'event') {
538
+ const ev = asGatewayEvent(msg.params)
539
+
540
+ if (ev) {
541
+ this.publish(ev)
542
+ }
543
+ }
544
+ }
545
+
546
+ private toError(raw: unknown): Error {
547
+ const err = raw as { message?: unknown } | null | undefined
548
+
549
+ return new Error(typeof err?.message === 'string' ? err.message : 'request failed')
550
+ }
551
+
552
+ private settle(p: Pending, err: Error | null, result: unknown) {
553
+ clearTimeout(p.timeout)
554
+ this.pending.delete(p.id)
555
+
556
+ if (err) {
557
+ p.reject(err)
558
+ } else {
559
+ p.resolve(result)
560
+ }
561
+ }
562
+
563
+ private pushLog(line: string) {
564
+ this.logs.push(truncateLine(line))
565
+ }
566
+
567
+ private rejectPending(err: Error) {
568
+ for (const p of this.pending.values()) {
569
+ clearTimeout(p.timeout)
570
+ p.reject(err)
571
+ }
572
+
573
+ this.pending.clear()
574
+ }
575
+
576
+ // Arrow class-field — stable identity, so `setTimeout(this.onTimeout, …, id)`
577
+ // doesn't allocate a bound function per request.
578
+ private onTimeout = (id: string) => {
579
+ const p = this.pending.get(id)
580
+
581
+ if (p) {
582
+ this.pending.delete(id)
583
+ p.reject(new Error(`timeout: ${p.method}`))
584
+ }
585
+ }
586
+
587
+ drain() {
588
+ this.subscribed = true
589
+
590
+ for (const ev of this.bufferedEvents.drain()) {
591
+ this.emit('event', ev)
592
+ }
593
+
594
+ if (this.pendingExit !== undefined) {
595
+ const code = this.pendingExit
596
+
597
+ this.pendingExit = undefined
598
+ this.emit('exit', code)
599
+ }
600
+ }
601
+
602
+ getLogTail(limit = 20): string {
603
+ return this.logs.tail(Math.max(1, limit)).join('\n')
604
+ }
605
+
606
+ private async ensureAttachedWebSocket(method: string): Promise<WebSocket> {
607
+ if (!this.attachUrl) {
608
+ throw new Error('gateway not running')
609
+ }
610
+
611
+ if (!this.ws || this.ws.readyState === WS_CLOSED || this.ws.readyState === WS_CLOSING) {
612
+ this.start()
613
+ }
614
+
615
+ if (this.ws?.readyState === WS_CONNECTING) {
616
+ try {
617
+ await this.wsConnectPromise
618
+ } catch (err) {
619
+ throw err instanceof Error ? err : new Error(String(err))
620
+ }
621
+ }
622
+
623
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
624
+ throw new Error(`gateway not connected: ${method}`)
625
+ }
626
+
627
+ return this.ws
628
+ }
629
+
630
+ private requestOverWebSocket<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
631
+ return this.ensureAttachedWebSocket(method).then(
632
+ ws =>
633
+ new Promise<T>((resolve, reject) => {
634
+ const id = `r${++this.reqId}`
635
+ const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id)
636
+
637
+ timeout.unref?.()
638
+ this.pending.set(id, {
639
+ id,
640
+ method,
641
+ reject,
642
+ resolve: v => resolve(v as T),
643
+ timeout
644
+ })
645
+
646
+ try {
647
+ ws.send(JSON.stringify({ id, jsonrpc: '2.0', method, params }))
648
+ } catch (e) {
649
+ const pending = this.pending.get(id)
650
+
651
+ if (pending) {
652
+ clearTimeout(pending.timeout)
653
+ this.pending.delete(id)
654
+ }
655
+
656
+ reject(e instanceof Error ? e : new Error(String(e)))
657
+ }
658
+ })
659
+ )
660
+ }
661
+
662
+ request<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
663
+ const attachUrl = resolveGatewayAttachUrl()
664
+
665
+ if (attachUrl) {
666
+ if (this.attachUrl !== attachUrl) {
667
+ // The env var rotated at runtime — restart the transport so
668
+ // switching from spawned-gateway mode to attach mode also
669
+ // tears down the old Python child. Merely closing `this.ws`
670
+ // would leave a previously spawned gateway process alive.
671
+ this.rejectPending(new Error('gateway attach url changed'))
672
+ this.start()
673
+ }
674
+
675
+ return this.requestOverWebSocket<T>(method, params)
676
+ }
677
+
678
+ if (!this.proc?.stdin || this.proc.killed || this.proc.exitCode !== null) {
679
+ this.start()
680
+ }
681
+
682
+ if (!this.proc?.stdin) {
683
+ return Promise.reject(new Error('gateway not running'))
684
+ }
685
+
686
+ const id = `r${++this.reqId}`
687
+
688
+ return new Promise<T>((resolve, reject) => {
689
+ const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id)
690
+
691
+ timeout.unref?.()
692
+
693
+ this.pending.set(id, {
694
+ id,
695
+ method,
696
+ reject,
697
+ resolve: v => resolve(v as T),
698
+ timeout
699
+ })
700
+
701
+ try {
702
+ this.proc!.stdin!.write(JSON.stringify({ id, jsonrpc: '2.0', method, params }) + '\n')
703
+ } catch (e) {
704
+ const pending = this.pending.get(id)
705
+
706
+ if (pending) {
707
+ clearTimeout(pending.timeout)
708
+ this.pending.delete(id)
709
+ }
710
+
711
+ reject(e instanceof Error ? e : new Error(String(e)))
712
+ }
713
+ })
714
+ }
715
+
716
+ kill(reason = 'requested') {
717
+ const proc = this.proc
718
+ const killed = proc?.kill()
719
+
720
+ this.pushLog(`[lifecycle] GatewayClient.kill reason=${reason} ${describeChild(proc)} killResult=${killed ?? 'none'}`)
721
+ this.closeGatewaySocket()
722
+ this.closeSidecarSocket()
723
+ this.clearReadyTimer()
724
+ // The ws 'close' handler is identity-gated on `this.ws === ws`
725
+ // and we just nulled `this.ws`, so it will short-circuit and
726
+ // skip handleTransportExit. Reject pending RPCs explicitly so
727
+ // attach-mode promises do not hang after an intentional kill.
728
+ this.rejectPending(new Error('gateway closed'))
729
+ }
730
+ }