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,191 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'NASTECH_TUI_TRUECOLOR', 'NO_COLOR', 'TERM', 'TERM_PROGRAM'] as const
4
+ let importId = 0
5
+
6
+ async function withCleanEnv(setup: () => void, body: () => Promise<void>) {
7
+ const saved: Record<string, string | undefined> = {}
8
+
9
+ for (const k of ENV_KEYS) {
10
+ saved[k] = process.env[k]
11
+ delete process.env[k]
12
+ }
13
+
14
+ try {
15
+ setup()
16
+ await body()
17
+ } finally {
18
+ for (const k of ENV_KEYS) {
19
+ if (saved[k] === undefined) {
20
+ delete process.env[k]
21
+ } else {
22
+ process.env[k] = saved[k]
23
+ }
24
+ }
25
+ }
26
+ }
27
+
28
+ describe('forceTruecolor', () => {
29
+ it('does not force truecolor by default', async () => {
30
+ await withCleanEnv(
31
+ () => {},
32
+ async () => {
33
+ await import('../lib/forceTruecolor.js?t=default-' + importId++)
34
+ expect(process.env.COLORTERM).toBeUndefined()
35
+ expect(process.env.FORCE_COLOR).toBeUndefined()
36
+ }
37
+ )
38
+ })
39
+
40
+ it('does not infer truecolor from Apple Terminal on pre-Tahoe macOS', async () => {
41
+ await withCleanEnv(
42
+ () => {
43
+ process.env.TERM_PROGRAM = 'Apple_Terminal'
44
+ process.env.TERM = 'xterm-256color'
45
+ },
46
+ async () => {
47
+ const mod = await import('../lib/forceTruecolor.js?t=apple-' + importId++)
48
+ expect(mod.shouldForceTruecolor({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false)
49
+ expect(process.env.COLORTERM).toBeUndefined()
50
+ expect(process.env.FORCE_COLOR).toBeUndefined()
51
+ }
52
+ )
53
+ })
54
+
55
+ it('downgrades Apple Terminal when truecolor is only advertised by env', async () => {
56
+ await withCleanEnv(
57
+ () => {
58
+ process.env.TERM_PROGRAM = 'Apple_Terminal'
59
+ process.env.COLORTERM = 'truecolor'
60
+ process.env.FORCE_COLOR = '3'
61
+ },
62
+ async () => {
63
+ const mod = await import('../lib/forceTruecolor.js?t=downgrade-' + importId++)
64
+ expect(
65
+ mod.shouldDowngradeAppleTerminalTruecolor({
66
+ TERM_PROGRAM: 'Apple_Terminal',
67
+ COLORTERM: 'truecolor',
68
+ FORCE_COLOR: '3'
69
+ } as NodeJS.ProcessEnv)
70
+ ).toBe(true)
71
+ expect(process.env.COLORTERM).toBeUndefined()
72
+ expect(process.env.FORCE_COLOR).toBeUndefined()
73
+ }
74
+ )
75
+ })
76
+
77
+ it('keeps non-Apple terminals untouched when they advertise truecolor', async () => {
78
+ await withCleanEnv(
79
+ () => {
80
+ process.env.TERM_PROGRAM = 'vscode'
81
+ process.env.COLORTERM = 'truecolor'
82
+ process.env.FORCE_COLOR = '3'
83
+ },
84
+ async () => {
85
+ const mod = await import('../lib/forceTruecolor.js?t=keep-non-apple-' + importId++)
86
+ expect(
87
+ mod.shouldDowngradeAppleTerminalTruecolor({
88
+ TERM_PROGRAM: 'vscode',
89
+ COLORTERM: 'truecolor',
90
+ FORCE_COLOR: '3'
91
+ } as NodeJS.ProcessEnv)
92
+ ).toBe(false)
93
+ expect(process.env.COLORTERM).toBe('truecolor')
94
+ expect(process.env.FORCE_COLOR).toBe('3')
95
+ }
96
+ )
97
+ })
98
+
99
+ it('sets COLORTERM=truecolor and FORCE_COLOR=3 when explicitly enabled', async () => {
100
+ await withCleanEnv(
101
+ () => {
102
+ process.env.NASTECH_TUI_TRUECOLOR = '1'
103
+ },
104
+ async () => {
105
+ await import('../lib/forceTruecolor.js?t=enabled-' + importId++)
106
+ expect(process.env.COLORTERM).toBe('truecolor')
107
+ expect(process.env.FORCE_COLOR).toBe('3')
108
+ }
109
+ )
110
+ })
111
+
112
+ it('respects NASTECH_TUI_TRUECOLOR=0 opt-out', async () => {
113
+ await withCleanEnv(
114
+ () => {
115
+ process.env.NASTECH_TUI_TRUECOLOR = '0'
116
+ process.env.TERM_PROGRAM = 'Apple_Terminal'
117
+ },
118
+ async () => {
119
+ await import('../lib/forceTruecolor.js?t=optout-' + importId++)
120
+ expect(process.env.COLORTERM).toBeUndefined()
121
+ expect(process.env.FORCE_COLOR).toBeUndefined()
122
+ }
123
+ )
124
+ })
125
+
126
+ it('lets explicit opt-in keep Apple truecolor advertisement', async () => {
127
+ await withCleanEnv(
128
+ () => {
129
+ process.env.TERM_PROGRAM = 'Apple_Terminal'
130
+ process.env.COLORTERM = 'truecolor'
131
+ process.env.FORCE_COLOR = '3'
132
+ process.env.NASTECH_TUI_TRUECOLOR = '1'
133
+ },
134
+ async () => {
135
+ const mod = await import('../lib/forceTruecolor.js?t=apple-explicit-on-' + importId++)
136
+ expect(
137
+ mod.shouldDowngradeAppleTerminalTruecolor({
138
+ TERM_PROGRAM: 'Apple_Terminal',
139
+ COLORTERM: 'truecolor',
140
+ FORCE_COLOR: '3',
141
+ NASTECH_TUI_TRUECOLOR: '1'
142
+ } as NodeJS.ProcessEnv)
143
+ ).toBe(false)
144
+ expect(process.env.COLORTERM).toBe('truecolor')
145
+ expect(process.env.FORCE_COLOR).toBe('3')
146
+ }
147
+ )
148
+ })
149
+
150
+ it('respects NO_COLOR', async () => {
151
+ await withCleanEnv(
152
+ () => {
153
+ process.env.NO_COLOR = '1'
154
+ process.env.NASTECH_TUI_TRUECOLOR = '1'
155
+ },
156
+ async () => {
157
+ await import('../lib/forceTruecolor.js?t=no-color-' + importId++)
158
+ expect(process.env.COLORTERM).toBeUndefined()
159
+ expect(process.env.FORCE_COLOR).toBeUndefined()
160
+ }
161
+ )
162
+ })
163
+
164
+ it('respects existing FORCE_COLOR unless NasTech truecolor is explicit', async () => {
165
+ await withCleanEnv(
166
+ () => {
167
+ process.env.FORCE_COLOR = ''
168
+ },
169
+ async () => {
170
+ const mod = await import('../lib/forceTruecolor.js?t=force-color-' + importId++)
171
+ expect(mod.shouldForceTruecolor(process.env)).toBe(false)
172
+ expect(process.env.COLORTERM).toBeUndefined()
173
+ expect(process.env.FORCE_COLOR).toBe('')
174
+ }
175
+ )
176
+ })
177
+
178
+ it('lets explicit NasTech truecolor override existing FORCE_COLOR', async () => {
179
+ await withCleanEnv(
180
+ () => {
181
+ process.env.FORCE_COLOR = '0'
182
+ process.env.NASTECH_TUI_TRUECOLOR = '1'
183
+ },
184
+ async () => {
185
+ await import('../lib/forceTruecolor.js?t=explicit-force-' + importId++)
186
+ expect(process.env.COLORTERM).toBe('truecolor')
187
+ expect(process.env.FORCE_COLOR).toBe('3')
188
+ }
189
+ )
190
+ })
191
+ })
@@ -0,0 +1,394 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { GatewayClient } from '../gatewayClient.js'
4
+
5
+ interface ListenerEntry {
6
+ callback: (event: any) => void
7
+ once: boolean
8
+ }
9
+
10
+ class FakeWebSocket {
11
+ static CONNECTING = 0
12
+ static OPEN = 1
13
+ static CLOSING = 2
14
+ static CLOSED = 3
15
+ static instances: FakeWebSocket[] = []
16
+
17
+ readyState = FakeWebSocket.CONNECTING
18
+ sent: string[] = []
19
+ readonly url: string
20
+ private listeners = new Map<string, ListenerEntry[]>()
21
+
22
+ constructor(url: string) {
23
+ this.url = url
24
+ FakeWebSocket.instances.push(this)
25
+ }
26
+
27
+ static reset() {
28
+ FakeWebSocket.instances = []
29
+ }
30
+
31
+ addEventListener(type: string, callback: (event: any) => void, options?: unknown) {
32
+ const once =
33
+ typeof options === 'object' &&
34
+ options !== null &&
35
+ 'once' in options &&
36
+ Boolean((options as { once?: unknown }).once)
37
+
38
+ const entries = this.listeners.get(type) ?? []
39
+
40
+ entries.push({ callback, once })
41
+ this.listeners.set(type, entries)
42
+ }
43
+
44
+ removeEventListener(type: string, callback: (event: any) => void) {
45
+ const entries = this.listeners.get(type)
46
+
47
+ if (!entries) {
48
+ return
49
+ }
50
+
51
+ this.listeners.set(
52
+ type,
53
+ entries.filter(entry => entry.callback !== callback)
54
+ )
55
+ }
56
+
57
+ send(payload: string) {
58
+ if (this.readyState !== FakeWebSocket.OPEN) {
59
+ throw new Error('socket not open')
60
+ }
61
+
62
+ this.sent.push(payload)
63
+ }
64
+
65
+ close(code = 1000) {
66
+ if (this.readyState === FakeWebSocket.CLOSED) {
67
+ return
68
+ }
69
+
70
+ this.readyState = FakeWebSocket.CLOSED
71
+ this.emit('close', { code })
72
+ }
73
+
74
+ open() {
75
+ this.readyState = FakeWebSocket.OPEN
76
+ this.emit('open', {})
77
+ }
78
+
79
+ message(data: string) {
80
+ this.emit('message', { data })
81
+ }
82
+
83
+ private emit(type: string, event: any) {
84
+ const entries = [...(this.listeners.get(type) ?? [])]
85
+
86
+ for (const entry of entries) {
87
+ entry.callback(event)
88
+
89
+ if (entry.once) {
90
+ this.removeEventListener(type, entry.callback)
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ describe('GatewayClient websocket attach mode', () => {
97
+ const originalWebSocket = globalThis.WebSocket
98
+ let originalGatewayUrl: string | undefined
99
+ let originalSidecarUrl: string | undefined
100
+
101
+ beforeEach(() => {
102
+ originalGatewayUrl = process.env.NASTECH_TUI_GATEWAY_URL
103
+ originalSidecarUrl = process.env.NASTECH_TUI_SIDECAR_URL
104
+ FakeWebSocket.reset()
105
+ ;(globalThis as { WebSocket?: unknown }).WebSocket = FakeWebSocket as unknown as typeof WebSocket
106
+ })
107
+
108
+ afterEach(() => {
109
+ if (originalGatewayUrl === undefined) {
110
+ delete process.env.NASTECH_TUI_GATEWAY_URL
111
+ } else {
112
+ process.env.NASTECH_TUI_GATEWAY_URL = originalGatewayUrl
113
+ }
114
+
115
+ if (originalSidecarUrl === undefined) {
116
+ delete process.env.NASTECH_TUI_SIDECAR_URL
117
+ } else {
118
+ process.env.NASTECH_TUI_SIDECAR_URL = originalSidecarUrl
119
+ }
120
+
121
+ FakeWebSocket.reset()
122
+
123
+ if (originalWebSocket) {
124
+ globalThis.WebSocket = originalWebSocket
125
+ } else {
126
+ delete (globalThis as { WebSocket?: unknown }).WebSocket
127
+ }
128
+ })
129
+
130
+ it('waits for websocket open and resolves RPC requests', async () => {
131
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc'
132
+ const gw = new GatewayClient()
133
+
134
+ gw.start()
135
+ const gatewaySocket = FakeWebSocket.instances[0]!
136
+ const req = gw.request<{ ok: boolean }>('session.create', { cols: 80 })
137
+
138
+ expect(gatewaySocket.sent).toHaveLength(0)
139
+ gatewaySocket.open()
140
+ await vi.waitFor(() => expect(gatewaySocket.sent).toHaveLength(1))
141
+
142
+ const frame = JSON.parse(gatewaySocket.sent[0] ?? '{}') as { id: string; method: string }
143
+ expect(frame.method).toBe('session.create')
144
+
145
+ gatewaySocket.message(JSON.stringify({ id: frame.id, jsonrpc: '2.0', result: { ok: true } }))
146
+ await expect(req).resolves.toEqual({ ok: true })
147
+
148
+ gw.kill()
149
+ })
150
+
151
+ it('mirrors event frames to sidecar websocket when configured', async () => {
152
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc'
153
+ process.env.NASTECH_TUI_SIDECAR_URL = 'ws://gateway.test/api/pub?token=abc&channel=demo'
154
+
155
+ const gw = new GatewayClient()
156
+ const seen: string[] = []
157
+
158
+ gw.on('event', ev => seen.push(ev.type))
159
+ gw.start()
160
+
161
+ const gatewaySocket = FakeWebSocket.instances[0]!
162
+ gatewaySocket.open()
163
+ await vi.waitFor(() => expect(FakeWebSocket.instances).toHaveLength(2))
164
+
165
+ const sidecarSocket = FakeWebSocket.instances[1]!
166
+
167
+ sidecarSocket.open()
168
+ gw.drain()
169
+
170
+ const eventFrame = JSON.stringify({
171
+ jsonrpc: '2.0',
172
+ method: 'event',
173
+ params: { type: 'tool.start', payload: { tool_id: 't1' } }
174
+ })
175
+
176
+ gatewaySocket.message(eventFrame)
177
+
178
+ expect(seen).toContain('tool.start')
179
+ expect(sidecarSocket.sent).toContain(eventFrame)
180
+
181
+ gw.kill()
182
+ })
183
+
184
+ it('emits exit when attached websocket closes', () => {
185
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc'
186
+ const gw = new GatewayClient()
187
+ const exits: Array<null | number> = []
188
+
189
+ gw.on('exit', code => exits.push(code))
190
+ gw.start()
191
+
192
+ const gatewaySocket = FakeWebSocket.instances[0]!
193
+
194
+ gatewaySocket.open()
195
+ gw.drain()
196
+ gatewaySocket.close(1011)
197
+
198
+ expect(exits).toEqual([1011])
199
+ expect(gw.getLogTail(20)).toContain('[lifecycle] websocket close code=1011')
200
+ expect(gw.getLogTail(20)).toContain('[lifecycle] transport exit code=1011')
201
+ })
202
+
203
+ it('rejects pending RPCs with websocket wording when the attached socket closes', async () => {
204
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc'
205
+ const gw = new GatewayClient()
206
+
207
+ gw.start()
208
+ const gatewaySocket = FakeWebSocket.instances[0]!
209
+
210
+ gatewaySocket.open()
211
+ gw.drain()
212
+
213
+ const req = gw.request('session.create', {})
214
+ await vi.waitFor(() => expect(gatewaySocket.sent.length).toBeGreaterThan(0))
215
+
216
+ gatewaySocket.close(1011)
217
+
218
+ await expect(req).rejects.toThrow(/gateway websocket closed \(1011\)/)
219
+ })
220
+
221
+ it('rejects pending RPCs when kill() closes the attached websocket', async () => {
222
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc'
223
+ const gw = new GatewayClient()
224
+
225
+ gw.start()
226
+ const gatewaySocket = FakeWebSocket.instances[0]!
227
+
228
+ gatewaySocket.open()
229
+ gw.drain()
230
+
231
+ const req = gw.request('session.create', {})
232
+ await vi.waitFor(() => expect(gatewaySocket.sent.length).toBeGreaterThan(0))
233
+
234
+ gw.kill('test.shutdown')
235
+
236
+ await expect(req).rejects.toThrow(/gateway closed/)
237
+ expect(gw.getLogTail(20)).toContain('[lifecycle] GatewayClient.kill reason=test.shutdown')
238
+ })
239
+
240
+ it('reattaches when NASTECH_TUI_GATEWAY_URL rotates between requests', async () => {
241
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway-old.test/api/ws?token=abc'
242
+ const gw = new GatewayClient()
243
+
244
+ gw.start()
245
+ const firstSocket = FakeWebSocket.instances[0]!
246
+
247
+ firstSocket.open()
248
+ gw.drain()
249
+
250
+ const stale = gw.request('session.create', {})
251
+ await vi.waitFor(() => expect(firstSocket.sent.length).toBeGreaterThan(0))
252
+
253
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway-new.test/api/ws?token=xyz'
254
+ const next = gw.request('session.create', {})
255
+
256
+ await expect(stale).rejects.toThrow(/gateway attach url changed/)
257
+ await vi.waitFor(() => expect(FakeWebSocket.instances).toHaveLength(2))
258
+
259
+ const secondSocket = FakeWebSocket.instances[1]!
260
+ expect(secondSocket.url).toContain('gateway-new.test')
261
+
262
+ secondSocket.open()
263
+ await vi.waitFor(() => expect(secondSocket.sent.length).toBeGreaterThan(0))
264
+
265
+ const frame = JSON.parse(secondSocket.sent[0] ?? '{}') as { id: string }
266
+ secondSocket.message(JSON.stringify({ id: frame.id, jsonrpc: '2.0', result: { ok: true } }))
267
+
268
+ await expect(next).resolves.toEqual({ ok: true })
269
+ gw.kill()
270
+ })
271
+
272
+ it('redacts query string secrets in attach failure logs and events', () => {
273
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=hunter2&channel=secret'
274
+ delete (globalThis as { WebSocket?: unknown }).WebSocket
275
+
276
+ const gw = new GatewayClient()
277
+ const stderrLines: string[] = []
278
+
279
+ gw.on('event', ev => {
280
+ if (ev.type === 'gateway.stderr' && typeof ev.payload?.line === 'string') {
281
+ stderrLines.push(ev.payload.line)
282
+ }
283
+ })
284
+ gw.start()
285
+ gw.drain()
286
+
287
+ expect(stderrLines.length).toBeGreaterThan(0)
288
+
289
+ for (const line of stderrLines) {
290
+ expect(line).not.toContain('hunter2')
291
+ expect(line).not.toContain('channel=secret')
292
+ }
293
+
294
+ expect(gw.getLogTail(20)).not.toContain('hunter2')
295
+ expect(gw.getLogTail(20)).not.toContain('channel=secret')
296
+
297
+ gw.kill()
298
+ })
299
+
300
+ it('redacts attach URL secrets when the WebSocket constructor throws', () => {
301
+ const secretUrl = 'ws://gateway.test/api/ws?token=hunter2&channel=secret'
302
+
303
+ process.env.NASTECH_TUI_GATEWAY_URL = secretUrl
304
+ ;(globalThis as { WebSocket?: unknown }).WebSocket = class ThrowingWebSocket extends FakeWebSocket {
305
+ constructor(url: string) {
306
+ throw new TypeError(`Invalid URL: ${url}`)
307
+ }
308
+ } as unknown as typeof WebSocket
309
+
310
+ const gw = new GatewayClient()
311
+
312
+ gw.start()
313
+ gw.drain()
314
+
315
+ const tail = gw.getLogTail(20)
316
+ expect(tail).not.toContain('hunter2')
317
+ expect(tail).not.toContain('channel=secret')
318
+ expect(tail).not.toContain(secretUrl)
319
+ expect(tail).toContain('ws://gateway.test/api/ws?***')
320
+
321
+ gw.kill()
322
+ })
323
+
324
+ it('redacts sidecar URL secrets when the WebSocket constructor throws', async () => {
325
+ const sidecarUrl = 'ws://gateway.test/api/pub?token=hunter2&channel=secret'
326
+
327
+ process.env.NASTECH_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc'
328
+ process.env.NASTECH_TUI_SIDECAR_URL = sidecarUrl
329
+ ;(globalThis as { WebSocket?: unknown }).WebSocket = class ThrowingSidecarWebSocket extends FakeWebSocket {
330
+ constructor(url: string) {
331
+ if (url.includes('/api/pub')) {
332
+ throw new TypeError(`Invalid URL: ${url}`)
333
+ }
334
+
335
+ super(url)
336
+ }
337
+ } as unknown as typeof WebSocket
338
+
339
+ const gw = new GatewayClient()
340
+
341
+ gw.start()
342
+ const gatewaySocket = FakeWebSocket.instances[0]!
343
+ gatewaySocket.open()
344
+ await vi.waitFor(() => expect(gw.getLogTail(20)).toContain('[sidecar] failed to connect'))
345
+
346
+ const tail = gw.getLogTail(20)
347
+ expect(tail).not.toContain('hunter2')
348
+ expect(tail).not.toContain('channel=secret')
349
+ expect(tail).not.toContain(sidecarUrl)
350
+ expect(tail).toContain('ws://gateway.test/api/pub?***')
351
+
352
+ gw.kill()
353
+ })
354
+
355
+ it('redacts user-info credentials even on URLs the WHATWG parser rejects', () => {
356
+ // Port 99999 is outside the WHATWG URL parser's valid 0–65535
357
+ // range and survives `.trim()`, so the fixture deterministically
358
+ // exercises `redactUrl()`'s fallback branch across Node versions.
359
+ // (An earlier `%zz` user-info fixture did NOT actually throw in
360
+ // recent Node — WHATWG accepts malformed percent escapes there —
361
+ // which silently routed the test through the structured-URL path.)
362
+ const fixture = 'ws://alice:hunter2@gateway.test:99999/api/ws?token=secret'
363
+ expect(() => new URL(fixture)).toThrow()
364
+
365
+ process.env.NASTECH_TUI_GATEWAY_URL = fixture
366
+ delete (globalThis as { WebSocket?: unknown }).WebSocket
367
+
368
+ const gw = new GatewayClient()
369
+ const stderrLines: string[] = []
370
+
371
+ gw.on('event', ev => {
372
+ if (ev.type === 'gateway.stderr' && typeof ev.payload?.line === 'string') {
373
+ stderrLines.push(ev.payload.line)
374
+ }
375
+ })
376
+ gw.start()
377
+ gw.drain()
378
+
379
+ expect(stderrLines.length).toBeGreaterThan(0)
380
+
381
+ for (const line of stderrLines) {
382
+ expect(line).not.toContain('alice')
383
+ expect(line).not.toContain('hunter2')
384
+ expect(line).not.toContain('token=secret')
385
+ }
386
+
387
+ const tail = gw.getLogTail(20)
388
+ expect(tail).not.toContain('alice')
389
+ expect(tail).not.toContain('hunter2')
390
+ expect(tail).not.toContain('token=secret')
391
+
392
+ gw.kill()
393
+ })
394
+ })
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { GATEWAY_RECOVERY_LIMIT, GATEWAY_RECOVERY_WINDOW_MS, planGatewayRecovery } from '../app/gatewayRecovery.js'
4
+
5
+ describe('planGatewayRecovery', () => {
6
+ it('recovers the live session and records the attempt', () => {
7
+ const plan = planGatewayRecovery('sess-1', null, [], 1000)
8
+
9
+ expect(plan).toEqual({ attempts: [1000], recover: true, sid: 'sess-1' })
10
+ })
11
+
12
+ it('does not recover when there is no session to resume', () => {
13
+ expect(planGatewayRecovery(null, null, [], 1000)).toEqual({ attempts: [], recover: false, sid: null })
14
+ })
15
+
16
+ it('keeps retrying the recovery target through a startup crash-loop, bounded by the budget', () => {
17
+ // First exit: live sid present.
18
+ let attempts: number[] = []
19
+ let plan = planGatewayRecovery('sess-1', null, attempts, 0)
20
+
21
+ expect(plan.recover).toBe(true)
22
+ expect(plan.sid).toBe('sess-1')
23
+ attempts = plan.attempts
24
+
25
+ // Respawn crash-loops before gateway.ready: live sid is now null, but the
26
+ // recovery target carries it forward so we keep trying up to the budget.
27
+ for (let i = 1; i < GATEWAY_RECOVERY_LIMIT; i++) {
28
+ plan = planGatewayRecovery(null, 'sess-1', attempts, i)
29
+ expect(plan.recover).toBe(true)
30
+ expect(plan.sid).toBe('sess-1')
31
+ attempts = plan.attempts
32
+ }
33
+
34
+ // Budget exhausted: fall back to the inert state instead of spawn-storming.
35
+ plan = planGatewayRecovery(null, 'sess-1', attempts, GATEWAY_RECOVERY_LIMIT)
36
+ expect(plan.recover).toBe(false)
37
+ expect(plan.sid).toBe('sess-1')
38
+ })
39
+
40
+ it('prunes attempts older than the window so recovery re-arms', () => {
41
+ const old = Array.from({ length: GATEWAY_RECOVERY_LIMIT }, (_, i) => i)
42
+ const plan = planGatewayRecovery('sess-1', null, old, GATEWAY_RECOVERY_WINDOW_MS + 100)
43
+
44
+ expect(plan.attempts).toEqual([GATEWAY_RECOVERY_WINDOW_MS + 100])
45
+ expect(plan.recover).toBe(true)
46
+ })
47
+ })