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,217 @@
1
+ import type { ChildProcess, spawn as SpawnFn } from 'node:child_process'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ import { describe, expect, it, vi } from 'vitest'
5
+
6
+ import { openCommand, openExternalUrl, parseSafeUrl } from './openExternalUrl.js'
7
+
8
+ type SpawnLike = typeof SpawnFn
9
+
10
+ describe('parseSafeUrl', () => {
11
+ it('accepts http and https URLs', () => {
12
+ expect(parseSafeUrl('https://example.com')?.href).toBe('https://example.com/')
13
+ expect(parseSafeUrl('http://example.com/path?q=1')?.href).toBe('http://example.com/path?q=1')
14
+ })
15
+
16
+ it('rejects file: URLs (would let a hostile model trigger arbitrary local handlers)', () => {
17
+ expect(parseSafeUrl('file:///etc/passwd')).toBeNull()
18
+ })
19
+
20
+ it('rejects javascript:, data:, and vbscript: URLs', () => {
21
+ expect(parseSafeUrl('javascript:alert(1)')).toBeNull()
22
+ expect(parseSafeUrl('data:text/html,<script>alert(1)</script>')).toBeNull()
23
+ expect(parseSafeUrl('vbscript:msgbox')).toBeNull()
24
+ })
25
+
26
+ it('rejects mailto:, ftp:, and other non-web protocols', () => {
27
+ expect(parseSafeUrl('mailto:test@example.com')).toBeNull()
28
+ expect(parseSafeUrl('ftp://example.com')).toBeNull()
29
+ expect(parseSafeUrl('ssh://example.com')).toBeNull()
30
+ })
31
+
32
+ it('rejects unparseable strings', () => {
33
+ expect(parseSafeUrl('not a url')).toBeNull()
34
+ expect(parseSafeUrl('')).toBeNull()
35
+ })
36
+
37
+ it('rejects non-string inputs defensively', () => {
38
+ expect(parseSafeUrl(undefined as unknown as string)).toBeNull()
39
+ expect(parseSafeUrl(null as unknown as string)).toBeNull()
40
+ expect(parseSafeUrl(123 as unknown as string)).toBeNull()
41
+ })
42
+ })
43
+
44
+ describe('openCommand', () => {
45
+ it('returns macOS open(1) on darwin', () => {
46
+ expect(openCommand('darwin')).toEqual({ command: 'open', args: [] })
47
+ })
48
+
49
+ it('routes through explorer.exe on win32 — not cmd.exe — so URLs with & | ^ < > stay safe', () => {
50
+ // win32 must not route through cmd.exe — see comment in openCommand.
51
+ // Test pins the contract that we use explorer.exe (non-shell) so URLs
52
+ // with `&`/`|`/`^`/`<`/`>` aren't reparsed by cmd's tokenizer.
53
+ const cmd = openCommand('win32')
54
+ expect(cmd?.command).toBe('explorer.exe')
55
+ expect(cmd?.args).toEqual([])
56
+ })
57
+
58
+ it('falls back to xdg-open on linux/bsd', () => {
59
+ expect(openCommand('linux')).toEqual({ command: 'xdg-open', args: [] })
60
+ expect(openCommand('freebsd')).toEqual({ command: 'xdg-open', args: [] })
61
+ expect(openCommand('openbsd')).toEqual({ command: 'xdg-open', args: [] })
62
+ })
63
+
64
+ it('returns null for unknown platforms (aix, sunos, cygwin, etc.)', () => {
65
+ // Avoid optimistically dispatching xdg-open on platforms where it
66
+ // probably isn't installed — the caller's `if (!command) return false`
67
+ // path surfaces "no opener" honestly instead.
68
+ expect(openCommand('aix')).toBeNull()
69
+ expect(openCommand('sunos')).toBeNull()
70
+ expect(openCommand('cygwin')).toBeNull()
71
+ expect(openCommand('haiku')).toBeNull()
72
+ expect(openCommand('')).toBeNull()
73
+ })
74
+ })
75
+
76
+ describe('openExternalUrl on unsupported platforms', () => {
77
+ it('returns false without spawning when the platform has no known opener', () => {
78
+ const spawn = vi.fn() as unknown as SpawnLike
79
+
80
+ expect(openExternalUrl('https://example.com/', { spawn, platform: () => 'aix' })).toBe(false)
81
+ expect(spawn).not.toHaveBeenCalled()
82
+ })
83
+ })
84
+
85
+ describe('openExternalUrl', () => {
86
+ // Tracks the most recent fake child so tests can inspect its 'error'
87
+ // handlers and emit on it. Use a loose EventEmitter alias rather than
88
+ // ChildProcess — the latter's `unref` signature is strictly `() => void`
89
+ // and doesn't accept `vi.fn()` without a generic.
90
+ type FakeChild = EventEmitter & { unref: () => void }
91
+
92
+ function mockSpawn(): {
93
+ spawn: SpawnLike
94
+ calls: Array<{ command: string; args: readonly string[] }>
95
+ lastChild: () => FakeChild | undefined
96
+ } {
97
+ const calls: Array<{ command: string; args: readonly string[] }> = []
98
+ let lastChild: FakeChild | undefined
99
+
100
+ const spawn = vi.fn((command: string, args: readonly string[]) => {
101
+ calls.push({ command, args })
102
+
103
+ // Use a real EventEmitter so .once('error', cb) wires up correctly
104
+ // and we can synthesize async failures by emitting 'error' from the
105
+ // test. The cast is the same one Node uses internally — ChildProcess
106
+ // extends EventEmitter.
107
+ const child = new EventEmitter() as FakeChild
108
+
109
+ child.unref = () => {}
110
+ lastChild = child
111
+
112
+ return child as unknown as ChildProcess
113
+ }) as unknown as SpawnLike
114
+
115
+ return { spawn, calls, lastChild: () => lastChild }
116
+ }
117
+
118
+ it('opens a normal https URL via the platform command', () => {
119
+ const { spawn, calls } = mockSpawn()
120
+
121
+ expect(openExternalUrl('https://example.com/foo', { spawn, platform: () => 'darwin' })).toBe(true)
122
+ expect(calls).toHaveLength(1)
123
+ expect(calls[0]!.command).toBe('open')
124
+ expect(calls[0]!.args).toEqual(['https://example.com/foo'])
125
+ })
126
+
127
+ it('uses xdg-open on linux', () => {
128
+ const { spawn, calls } = mockSpawn()
129
+
130
+ openExternalUrl('https://example.com/', { spawn, platform: () => 'linux' })
131
+ expect(calls[0]!.command).toBe('xdg-open')
132
+ })
133
+
134
+ it('refuses to open file: URLs and does not spawn', () => {
135
+ const { spawn, calls } = mockSpawn()
136
+
137
+ expect(openExternalUrl('file:///etc/passwd', { spawn, platform: () => 'darwin' })).toBe(false)
138
+ expect(calls).toHaveLength(0)
139
+ })
140
+
141
+ it('refuses to open javascript: URLs and does not spawn', () => {
142
+ const { spawn, calls } = mockSpawn()
143
+
144
+ expect(openExternalUrl('javascript:alert(1)', { spawn, platform: () => 'darwin' })).toBe(false)
145
+ expect(calls).toHaveLength(0)
146
+ })
147
+
148
+ it('passes URLs containing shell metacharacters as plain args (no shell interpolation)', () => {
149
+ const { spawn, calls } = mockSpawn()
150
+
151
+ // A URL with `; & ` plus URL-encoded backticks. spawn(..., args) without
152
+ // shell:true means the OS receives these as a single argv element.
153
+ const hostile = 'https://example.com/path%3Bevil%20%26%20rm%20-rf'
154
+
155
+ openExternalUrl(hostile, { spawn, platform: () => 'darwin' })
156
+ expect(calls).toHaveLength(1)
157
+ expect(calls[0]!.args[calls[0]!.args.length - 1]).toBe(hostile)
158
+ })
159
+
160
+ it('on win32, a URL with & | ^ < > is forwarded as a single argv element via explorer.exe', () => {
161
+ const { spawn, calls } = mockSpawn()
162
+
163
+ // Plain http URL with & in query (very common, e.g. analytics params)
164
+ // plus other cmd metacharacters that would split or reinterpret the
165
+ // command if win32 routed through cmd.exe /c start. Note that the URL
166
+ // parser percent-encodes `<` and `>` (which is fine — encoded forms
167
+ // can't be reinterpreted by any shell), but `&`, `|`, `^` survive
168
+ // and would tokenize cmd.exe if we ever regressed back to it.
169
+ const meta = 'https://example.com/q?a=1&b=2|c^d<e>f'
170
+
171
+ expect(openExternalUrl(meta, { spawn, platform: () => 'win32' })).toBe(true)
172
+ expect(calls).toHaveLength(1)
173
+ expect(calls[0]!.command).toBe('explorer.exe')
174
+ // The URL must arrive as exactly one argv element — not split on &/|/^/etc.
175
+ const forwarded = calls[0]!.args[0]!
176
+ expect(calls[0]!.args).toHaveLength(1)
177
+ expect(forwarded).toContain('a=1&b=2')
178
+ expect(forwarded).toContain('|c^d')
179
+ })
180
+
181
+ it('on win32, common http URLs with & query params are forwarded intact', () => {
182
+ const { spawn, calls } = mockSpawn()
183
+ const url = 'https://example.com/search?q=foo&page=2&utm_source=nastech'
184
+
185
+ openExternalUrl(url, { spawn, platform: () => 'win32' })
186
+ expect(calls[0]!.args).toEqual([url])
187
+ })
188
+
189
+ it('returns false on synchronous spawn failure', () => {
190
+ const spawn = vi.fn(() => {
191
+ throw new Error('ENOENT')
192
+ }) as unknown as SpawnLike
193
+
194
+ expect(openExternalUrl('https://example.com/', { spawn, platform: () => 'linux' })).toBe(false)
195
+ })
196
+
197
+ it('does not crash the host when the spawned process emits an async error', () => {
198
+ // Real-world case: `xdg-open` / `explorer.exe` missing on PATH. spawn()
199
+ // returns a ChildProcess synchronously, then emits 'error' once the
200
+ // exec actually fails. Without a registered 'error' listener, Node
201
+ // re-throws the event as an uncaught exception → TUI dies. We attach
202
+ // a no-op listener inside openExternalUrl; this test pins that contract.
203
+ const { spawn, lastChild } = mockSpawn()
204
+
205
+ expect(openExternalUrl('https://example.com/', { spawn, platform: () => 'linux' })).toBe(true)
206
+
207
+ const child = lastChild()
208
+ expect(child).toBeDefined()
209
+ // Must have a listener registered BEFORE we emit, or EventEmitter will
210
+ // throw synchronously here (which is exactly the crash we're preventing).
211
+ expect(child!.listenerCount('error')).toBeGreaterThan(0)
212
+
213
+ // Emit and assert it doesn't throw. If the listener weren't attached,
214
+ // this would throw 'Unhandled error' and fail the test.
215
+ expect(() => child!.emit('error', new Error('ENOENT: xdg-open not found'))).not.toThrow()
216
+ })
217
+ })
@@ -0,0 +1,158 @@
1
+ import { spawn, type SpawnOptions } from 'node:child_process'
2
+ import { platform } from 'node:os'
3
+
4
+ /**
5
+ * Opens an external URL in the user's default browser/handler.
6
+ *
7
+ * Wired into the Ink instance via `onHyperlinkClick` in entry.tsx, so any
8
+ * mouse click on a `<Link>` cell (or a row containing a plain-text URL the
9
+ * renderer detected) goes here. Mouse tracking inside the TUI prevents
10
+ * Terminal.app's native Cmd+click from firing — the click is captured
11
+ * before the terminal application sees it — so we have to handle the open
12
+ * ourselves.
13
+ *
14
+ * Safety:
15
+ * - http(s) only. Anything else (`file:`, `data:`, `javascript:`, etc.) is
16
+ * rejected — a hostile model could otherwise emit `<Link url="file:///">`
17
+ * and trick a click into running an arbitrary local handler.
18
+ * - Hostname is parsed via `URL`; only well-formed URLs are forwarded.
19
+ * - Spawned via `child_process.spawn` with arg array (no shell), so a URL
20
+ * containing shell metacharacters (`;`, `&`, backticks) cannot be
21
+ * interpreted as a command.
22
+ *
23
+ * Returns `true` if the spawn was attempted, `false` if the open could
24
+ * not proceed — covers (a) URL rejected by `parseSafeUrl` (non-http(s),
25
+ * malformed, etc.), (b) no known opener for the current platform
26
+ * (`openCommand` returned null), or (c) `spawn()` threw synchronously
27
+ * before the child was created. Async failures after spawn (`'error'`
28
+ * event because the binary couldn't exec) still return `true` because
29
+ * the spawn was attempted — the no-op error listener absorbs the event
30
+ * so the TUI doesn't crash, and the user just doesn't see their browser
31
+ * pop.
32
+ */
33
+ export function openExternalUrl(rawUrl: string, dependencies: OpenDependencies = {}): boolean {
34
+ const url = parseSafeUrl(rawUrl)
35
+
36
+ if (!url) {
37
+ return false
38
+ }
39
+
40
+ const spawnFn = dependencies.spawn ?? spawn
41
+ const platformId = dependencies.platform?.() ?? platform()
42
+
43
+ const command = openCommand(platformId)
44
+
45
+ if (!command) {
46
+ return false
47
+ }
48
+
49
+ try {
50
+ const child = spawnFn(command.command, [...command.args, url.toString()], {
51
+ // Detach so closing the TUI later doesn't kill the browser process,
52
+ // and ignore stdio so we don't leak FDs into our raw-mode terminal.
53
+ // Without `ignore` here, Chrome's stderr can land in the alt screen.
54
+ detached: true,
55
+ stdio: 'ignore'
56
+ } satisfies SpawnOptions)
57
+
58
+ // Async failure path: spawn returns a ChildProcess synchronously even
59
+ // when the binary is missing (ENOENT on `xdg-open` / `explorer.exe`),
60
+ // unreachable (EACCES), or otherwise unusable — the failure surfaces
61
+ // later as an 'error' event. Without a handler, an unhandled 'error'
62
+ // on an EventEmitter crashes Node, which would tear down the whole
63
+ // TUI. Attach a no-op listener BEFORE unref() so the event has a
64
+ // consumer; we already returned `true` synchronously, so the user
65
+ // just won't see their browser open — same as if the URL had been
66
+ // rejected upstream.
67
+ child.once('error', () => {
68
+ // Intentional no-op. The TUI keeps running; user gets no browser
69
+ // pop, which is the failure mode we promised in the doc comment.
70
+ })
71
+
72
+ child.unref()
73
+
74
+ return true
75
+ } catch {
76
+ // spawn can also throw synchronously on argv-validation failures
77
+ // (e.g. NUL in the path). Treat it as a no-op rather than crashing.
78
+ return false
79
+ }
80
+ }
81
+
82
+ export type OpenDependencies = {
83
+ spawn?: typeof spawn
84
+ platform?: () => string
85
+ }
86
+
87
+ /**
88
+ * Validate and normalize a URL for opening externally.
89
+ * Exported for testing.
90
+ */
91
+ export function parseSafeUrl(value: string): null | URL {
92
+ if (!value || typeof value !== 'string') {
93
+ return null
94
+ }
95
+
96
+ let parsed: URL
97
+
98
+ try {
99
+ parsed = new URL(value)
100
+ } catch {
101
+ return null
102
+ }
103
+
104
+ // http(s) only — opening file://, data:, javascript:, vbscript:, etc.
105
+ // would let a malicious model run a local handler with attacker-controlled
106
+ // input on a single click.
107
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
108
+ return null
109
+ }
110
+
111
+ // Reject empty or all-whitespace hostnames defensively. URL parsing
112
+ // accepts URLs like 'http:///foo' on some Node versions; we don't want
113
+ // to forward those to `open`.
114
+ if (!parsed.hostname.trim()) {
115
+ return null
116
+ }
117
+
118
+ return parsed
119
+ }
120
+
121
+ type OpenCommand = { command: string; args: readonly string[] }
122
+
123
+ /**
124
+ * Per-platform open command. We deliberately avoid `cmd.exe /c start` on
125
+ * Windows even though it's the canonical example, because `start` is a cmd
126
+ * builtin: the URL string is reparsed by cmd's command-line tokenizer and
127
+ * characters like `&`, `|`, `^`, `<`, `>` either break the command or get
128
+ * interpreted as additional commands. That undermines the protocol
129
+ * allowlist's safety story and also breaks plain http(s) URLs with `&` in
130
+ * query strings. `explorer.exe <url>` is the safe, non-shell alternative —
131
+ * it invokes the registered protocol handler for http(s) without going
132
+ * through cmd. Linux/BSD use `xdg-open` directly with no shell wrapping.
133
+ *
134
+ * Returns null for platforms where we don't know a safe opener (e.g. `aix`,
135
+ * `sunos`, `cygwin`). The caller's `if (!command) return false` path then
136
+ * surfaces "no opener" instead of optimistically trying `xdg-open` on a
137
+ * platform that probably doesn't have it.
138
+ */
139
+ export function openCommand(platformId: string): OpenCommand | null {
140
+ if (platformId === 'darwin') {
141
+ return { command: 'open', args: [] }
142
+ }
143
+
144
+ if (platformId === 'win32') {
145
+ return { command: 'explorer.exe', args: [] }
146
+ }
147
+
148
+ // Linux + the BSD family ship xdg-open via xdg-utils. Everything else
149
+ // (aix, sunos, cygwin, haiku, etc.) returns null so openExternalUrl's
150
+ // command-not-found fallback fires honestly.
151
+ const XDG_OPEN_PLATFORMS = new Set(['linux', 'freebsd', 'openbsd', 'netbsd', 'dragonfly'])
152
+
153
+ if (XDG_OPEN_PLATFORMS.has(platformId)) {
154
+ return { command: 'xdg-open', args: [] }
155
+ }
156
+
157
+ return null
158
+ }
@@ -0,0 +1,73 @@
1
+ const ESC = '\x1b'
2
+ const BEL = '\x07'
3
+ const ST = `${ESC}\\`
4
+
5
+ export const OSC52_CLIPBOARD_QUERY = `${ESC}]52;c;?${BEL}`
6
+
7
+ type OscResponse = { code: number; data: string; type: 'osc' }
8
+
9
+ type OscQuerier = {
10
+ flush: () => Promise<void>
11
+ send: <T>(query: { match: (r: unknown) => r is T; request: string }) => Promise<T | undefined>
12
+ }
13
+
14
+ function wrapForMultiplexer(sequence: string): string {
15
+ if (process.env['TMUX']) {
16
+ return `${ESC}Ptmux;${sequence.split(ESC).join(ESC + ESC)}${ST}`
17
+ }
18
+
19
+ if (process.env['STY']) {
20
+ return `${ESC}P${sequence}${ST}`
21
+ }
22
+
23
+ return sequence
24
+ }
25
+
26
+ export function buildOsc52ClipboardQuery(): string {
27
+ return wrapForMultiplexer(OSC52_CLIPBOARD_QUERY)
28
+ }
29
+
30
+ export function parseOsc52ClipboardData(data: string): null | string {
31
+ const firstSep = data.indexOf(';')
32
+
33
+ if (firstSep === -1) {
34
+ return null
35
+ }
36
+
37
+ const selection = data.slice(0, firstSep)
38
+ const payload = data.slice(firstSep + 1)
39
+
40
+ if ((selection !== 'c' && selection !== 'p') || !payload || payload === '?') {
41
+ return null
42
+ }
43
+
44
+ try {
45
+ return Buffer.from(payload, 'base64').toString('utf8')
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+
51
+ export async function readOsc52Clipboard(querier: null | OscQuerier, timeoutMs = 500): Promise<null | string> {
52
+ if (!querier) {
53
+ return null
54
+ }
55
+
56
+ const timeout = new Promise<undefined>(resolve => setTimeout(resolve, timeoutMs))
57
+
58
+ const query = querier.send<OscResponse>({
59
+ request: buildOsc52ClipboardQuery(),
60
+ match: (r: unknown): r is OscResponse => {
61
+ return !!r && typeof r === 'object' && (r as OscResponse).type === 'osc' && (r as OscResponse).code === 52
62
+ }
63
+ })
64
+
65
+ const response = await Promise.race([query, timeout])
66
+
67
+ await querier.flush()
68
+
69
+ return response ? parseOsc52ClipboardData(response.data) : null
70
+ }
71
+
72
+ export const writeOsc52Clipboard = (s: string) =>
73
+ process.stdout.write(`\x1b]52;c;${Buffer.from(s, 'utf8').toString('base64')}\x07`)
@@ -0,0 +1,57 @@
1
+ import { appendFileSync, mkdirSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ // Mirror the Python gateway's panic log (tui_gateway/server.py::_CRASH_LOG) from
6
+ // the Node parent so lifecycle breadcrumbs interleave, by timestamp, with the
7
+ // child's `=== SIGTERM received ===` / `=== gateway exit ===` entries.
8
+ //
9
+ // A backend SIGTERM is *usually* a parent action — `gw.kill()` (graceful-exit on
10
+ // a signal to Node, or an explicit /quit) or `start()` replacing a live child —
11
+ // but it can also come straight from an external supervisor (s6, a cgroup OOM
12
+ // reaper, a stray `kill`) signalling the child directly. Telling those apart is
13
+ // exactly the point: #31051 left these breadcrumbs in an in-memory CircularBuffer
14
+ // that dies with the process, so SIGTERM crash reports arrived with no parent
15
+ // context. A `[tui-parent]` line immediately before the child's panic means a
16
+ // parent kill; its absence *suggests* an external signal — not definitive,
17
+ // since this logger is best-effort (disabled under VITEST, and a failed append
18
+ // is swallowed). Persisting the death-explaining events here is what makes that
19
+ // distinction (and a memory-critical `process.exit(137)`, which closes stdin →
20
+ // clean EOF, not SIGTERM) diagnosable after the fact.
21
+ const logDir = join(process.env.NASTECH_HOME?.trim() || join(homedir(), '.nastech'), 'logs')
22
+ const CRASH_LOG = join(logDir, 'tui_gateway_crash.log')
23
+
24
+ // Skipped under vitest so unit tests exercising start()/kill() can't write into
25
+ // a real ~/.nastech (tests must stay hermetic — see AGENTS.md).
26
+ const enabled = !process.env.VITEST
27
+ // Slice a single breadcrumb's value to MAX_BREADCRUMB chars (a short
28
+ // "[truncated …]" marker is appended, so the written line is slightly longer)
29
+ // so a pathological value (e.g. a giant error) can't bloat the shared crash log
30
+ // or add noticeable blocking on the synchronous append. Mirrors the spirit of
31
+ // GatewayClient's in-memory log-line cap.
32
+ const MAX_BREADCRUMB = 4096
33
+ let warned = false
34
+
35
+ export function recordParentLifecycle(line: string): void {
36
+ if (!enabled) {
37
+ return
38
+ }
39
+
40
+ try {
41
+ // Collapse embedded newlines so a multi-line value (e.g. an error message)
42
+ // stays one breadcrumb and can't masquerade as a separate log entry or as
43
+ // the child's panic output sharing this file.
44
+ const oneLine = line.replace(/[\r\n]+/g, ' ↵ ')
45
+
46
+ const capped =
47
+ oneLine.length > MAX_BREADCRUMB ? `${oneLine.slice(0, MAX_BREADCRUMB)}… [truncated ${oneLine.length} chars]` : oneLine
48
+
49
+ mkdirSync(logDir, { recursive: true })
50
+ appendFileSync(CRASH_LOG, `[tui-parent] ${new Date().toISOString()} ${capped}\n`)
51
+ } catch {
52
+ if (!warned) {
53
+ warned = true
54
+ process.stderr.write('nastech-tui: parent lifecycle log unavailable\n')
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,107 @@
1
+ // Perf instrumentation for the full render pipeline.
2
+ //
3
+ // PerfPane (React.Profiler) → per-pane commit times
4
+ // logFrameEvent (ink.onFrame) → yoga / renderer / diff / optimize / write
5
+ // phases + yoga counters + scroll fast-path
6
+ //
7
+ // Both gate on NASTECH_DEV_PERF=1 and dump JSON-lines (default ~/.nastech/perf.log,
8
+ // override NASTECH_DEV_PERF_LOG). Tagged { src: 'react' | 'frame' } for jq.
9
+ // NASTECH_DEV_PERF_MS (default 2) skips sub-ms idle frames; set 0 to capture all.
10
+ //
11
+ // Zero cost when unset: PerfPane returns children directly, logFrameEvent is
12
+ // undefined so ink doesn't pay the timing cost.
13
+
14
+ import { appendFileSync, mkdirSync } from 'node:fs'
15
+ import { homedir } from 'node:os'
16
+ import { dirname, join } from 'node:path'
17
+
18
+ import type { FrameEvent } from '@nastechai/ink'
19
+ import { scrollFastPathStats } from '@nastechai/ink'
20
+ import { Profiler, type ProfilerOnRenderCallback, type ReactNode } from 'react'
21
+
22
+ const ENABLED = /^(?:1|true|yes|on)$/i.test((process.env.NASTECH_DEV_PERF ?? '').trim())
23
+ const THRESHOLD_MS = Number(process.env.NASTECH_DEV_PERF_MS ?? '2') || 0
24
+ const LOG_PATH = process.env.NASTECH_DEV_PERF_LOG?.trim() || join(homedir(), '.nastech', 'perf.log')
25
+
26
+ let logReady = false
27
+
28
+ const writeRow = (row: Record<string, unknown>) => {
29
+ if (!logReady) {
30
+ logReady = true
31
+
32
+ try {
33
+ mkdirSync(dirname(LOG_PATH), { recursive: true })
34
+ } catch {
35
+ // Best-effort — never crash the TUI to log a sample.
36
+ }
37
+ }
38
+
39
+ try {
40
+ appendFileSync(LOG_PATH, `${JSON.stringify(row)}\n`)
41
+ } catch {
42
+ /* best-effort */
43
+ }
44
+ }
45
+
46
+ const round2 = (n: number) => Math.round(n * 100) / 100
47
+
48
+ const onRender: ProfilerOnRenderCallback = (id, phase, actualMs, baseMs, startTime, commitTime) => {
49
+ if (actualMs < THRESHOLD_MS) {
50
+ return
51
+ }
52
+
53
+ writeRow({
54
+ actualMs: round2(actualMs),
55
+ baseMs: round2(baseMs),
56
+ commitTimeMs: round2(commitTime),
57
+ id,
58
+ phase,
59
+ src: 'react',
60
+ startTimeMs: round2(startTime),
61
+ ts: Date.now()
62
+ })
63
+ }
64
+
65
+ export function PerfPane({ children, id }: { children: ReactNode; id: string }) {
66
+ if (!ENABLED) {
67
+ return children
68
+ }
69
+
70
+ return (
71
+ <Profiler id={id} onRender={onRender}>
72
+ {children}
73
+ </Profiler>
74
+ )
75
+ }
76
+
77
+ export const logFrameEvent = ENABLED
78
+ ? (event: FrameEvent) => {
79
+ if (event.durationMs < THRESHOLD_MS) {
80
+ return
81
+ }
82
+
83
+ writeRow({
84
+ durationMs: round2(event.durationMs),
85
+ // Cumulative counters — consumers diff pairs to get per-frame deltas.
86
+ fastPath: { ...scrollFastPathStats, declined: { ...scrollFastPathStats.declined } },
87
+ flickers: event.flickers.length ? event.flickers : undefined,
88
+ phases: event.phases
89
+ ? {
90
+ ...event.phases,
91
+ commit: round2(event.phases.commit),
92
+ diff: round2(event.phases.diff),
93
+ optimize: round2(event.phases.optimize),
94
+ prevFrameDrainMs: round2(event.phases.prevFrameDrainMs),
95
+ renderer: round2(event.phases.renderer),
96
+ write: round2(event.phases.write),
97
+ yoga: round2(event.phases.yoga)
98
+ }
99
+ : undefined,
100
+ src: 'frame',
101
+ ts: Date.now()
102
+ })
103
+ }
104
+ : undefined
105
+
106
+ export const PERF_ENABLED = ENABLED
107
+ export const PERF_LOG_PATH = LOG_PATH