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,435 @@
1
+ import { isIP } from 'node:net'
2
+
3
+ import { useEffect, useMemo, useState } from 'react'
4
+
5
+ const titleCache = new Map<string, string>()
6
+ const titleInflight = new Map<string, Promise<string>>()
7
+ const titleSubs = new Map<string, Set<(value: string) => void>>()
8
+
9
+ const TITLE_CACHE_LIMIT = 500
10
+ const TITLE_MAX_LENGTH = 240
11
+ const TITLE_BYTE_BUDGET = 96 * 1024
12
+ const TITLE_TIMEOUT_MS = 5000
13
+
14
+ const TITLE_USER_AGENT =
15
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'
16
+
17
+ const TITLE_ERROR_RE =
18
+ /\b(?:access denied|attention required|captcha|error|forbidden|just a moment|request blocked|too many requests)\b/i
19
+
20
+ const DOMAIN_RE = /^(?:www\.)?[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,}(?::\d+)?(?:[/?#][^\s]*)?$/i
21
+ const SKIP_PROTO_RE = /^(?:file|data|mailto|javascript|blob|chrome|about|nastech):/i
22
+ const LOCAL_HOSTNAME_RE = /^(?:localhost|localhost\.localdomain)$/i
23
+ const LOCAL_HOST_SUFFIXES = ['.corp', '.home', '.internal', '.lan', '.local', '.localdomain']
24
+ const STATUS_PERMALINK_HOST_RE = /^(?:mobile\.)?(?:x|twitter)\.com$/i
25
+ const STATUS_PERMALINK_PATH_RE = /^\/[^/]+\/status\/\d+\/?$/i
26
+
27
+ const HTML_ENTITIES: Record<string, string> = {
28
+ '#39': "'",
29
+ amp: '&',
30
+ apos: "'",
31
+ gt: '>',
32
+ lt: '<',
33
+ nbsp: ' ',
34
+ quot: '"'
35
+ }
36
+
37
+ export function normalizeExternalUrl(value: string): string {
38
+ const trimmed = value.trim()
39
+
40
+ if (!trimmed || /^https?:\/\//i.test(trimmed)) {
41
+ return trimmed
42
+ }
43
+
44
+ return DOMAIN_RE.test(trimmed) ? `https://${trimmed}` : trimmed
45
+ }
46
+
47
+ function parseUrl(value: string): null | URL {
48
+ try {
49
+ return new URL(normalizeExternalUrl(value))
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
55
+ function titleCacheKey(value: string): string {
56
+ const url = parseUrl(value)
57
+
58
+ if (!url) {
59
+ return normalizeExternalUrl(value)
60
+ }
61
+
62
+ const host = url.hostname.replace(/^www\./i, '').toLowerCase()
63
+ const pathname = url.pathname === '/' ? '/' : url.pathname.replace(/\/+$/, '') || '/'
64
+
65
+ return `${host}${pathname}${url.search || ''}`
66
+ }
67
+
68
+ function cacheTitle(key: string, title: string): void {
69
+ if (titleCache.size >= TITLE_CACHE_LIMIT) {
70
+ titleCache.delete(titleCache.keys().next().value as string)
71
+ }
72
+
73
+ titleCache.set(key, title)
74
+ }
75
+
76
+ export function hostPathLabel(value: string): string {
77
+ const url = parseUrl(value)
78
+
79
+ if (!url) {
80
+ return value
81
+ }
82
+
83
+ const host = url.hostname.replace(/^www\./, '')
84
+ const path = url.pathname && url.pathname !== '/' ? url.pathname.replace(/\/$/, '') : ''
85
+
86
+ return `${host}${path}`
87
+ }
88
+
89
+ function cleanSlug(segment: string): string {
90
+ try {
91
+ return decodeURIComponent(segment)
92
+ .replace(/\.a\d+\..*$/i, '')
93
+ .replace(/\.(?:html?|php|aspx?)$/i, '')
94
+ .replace(/(?:[-_.](?:[a-z]{1,3}\d{2,}|i\d{2,}))+$/i, '')
95
+ .replace(/[_-]+/g, ' ')
96
+ .replace(/\s+/g, ' ')
97
+ .trim()
98
+ } catch {
99
+ return ''
100
+ }
101
+ }
102
+
103
+ export function urlSlugTitleLabel(value: string): string {
104
+ const url = parseUrl(value)
105
+
106
+ if (url && STATUS_PERMALINK_HOST_RE.test(url.hostname) && STATUS_PERMALINK_PATH_RE.test(url.pathname)) {
107
+ return hostPathLabel(value)
108
+ }
109
+
110
+ for (const segment of url?.pathname.split('/').filter(Boolean).reverse() ?? []) {
111
+ const cleaned = cleanSlug(segment)
112
+
113
+ if (!cleaned || !/[a-z]/i.test(cleaned)) {
114
+ continue
115
+ }
116
+
117
+ if (/^(?:[a-z]{1,3}\d+|\d+)$/i.test(cleaned.replace(/\s+/g, ''))) {
118
+ continue
119
+ }
120
+
121
+ const titled = cleaned.replace(/\b[a-z]/g, c => c.toUpperCase())
122
+
123
+ if (titled.length >= 4) {
124
+ return titled
125
+ }
126
+ }
127
+
128
+ return hostPathLabel(value)
129
+ }
130
+
131
+ function parseIpv4Octets(value: string): null | [number, number, number, number] {
132
+ const parts = value.split('.')
133
+
134
+ if (parts.length !== 4) {
135
+ return null
136
+ }
137
+
138
+ const octets: number[] = []
139
+
140
+ for (const part of parts) {
141
+ if (!/^\d{1,3}$/.test(part)) {
142
+ return null
143
+ }
144
+
145
+ const next = Number(part)
146
+
147
+ if (!Number.isInteger(next) || next < 0 || next > 255) {
148
+ return null
149
+ }
150
+
151
+ octets.push(next)
152
+ }
153
+
154
+ return [octets[0]!, octets[1]!, octets[2]!, octets[3]!]
155
+ }
156
+
157
+ function isPrivateIpv4(value: string): boolean {
158
+ const octets = parseIpv4Octets(value)
159
+
160
+ if (!octets) {
161
+ return false
162
+ }
163
+
164
+ const [a, b] = octets
165
+
166
+ return (
167
+ a === 0 ||
168
+ a === 10 ||
169
+ a === 127 ||
170
+ a === 255 ||
171
+ (a === 100 && b >= 64 && b <= 127) ||
172
+ (a === 169 && b === 254) ||
173
+ (a === 172 && b >= 16 && b <= 31) ||
174
+ (a === 192 && b === 168) ||
175
+ (a === 198 && (b === 18 || b === 19))
176
+ )
177
+ }
178
+
179
+ function isPrivateIpv6(value: string): boolean {
180
+ const normalized = value.toLowerCase()
181
+
182
+ if (normalized === '::' || normalized === '::1') {
183
+ return true
184
+ }
185
+
186
+ if (normalized.startsWith('fc') || normalized.startsWith('fd')) {
187
+ return true
188
+ }
189
+
190
+ if (normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea') || normalized.startsWith('feb')) {
191
+ return true
192
+ }
193
+
194
+ if (normalized.startsWith('::ffff:')) {
195
+ return isPrivateIpv4(normalized.slice('::ffff:'.length))
196
+ }
197
+
198
+ return false
199
+ }
200
+
201
+ function normalizeHostname(value: string): string {
202
+ const withoutBrackets = value.replace(/^\[/, '').replace(/\]$/, '')
203
+ const withoutZoneId = withoutBrackets.split('%', 1)[0]!
204
+
205
+ return withoutZoneId.replace(/\.$/, '').toLowerCase()
206
+ }
207
+
208
+ function isPrivateOrLocalHost(hostname: string): boolean {
209
+ const normalized = normalizeHostname(hostname)
210
+
211
+ if (!normalized) {
212
+ return true
213
+ }
214
+
215
+ if (LOCAL_HOSTNAME_RE.test(normalized)) {
216
+ return true
217
+ }
218
+
219
+ if (LOCAL_HOST_SUFFIXES.some(suffix => normalized.endsWith(suffix))) {
220
+ return true
221
+ }
222
+
223
+ const ipVersion = isIP(normalized)
224
+
225
+ if (ipVersion === 4) {
226
+ return isPrivateIpv4(normalized)
227
+ }
228
+
229
+ if (ipVersion === 6) {
230
+ return isPrivateIpv6(normalized)
231
+ }
232
+
233
+ // Single-label hostnames are usually LAN names or enterprise intranet aliases.
234
+ return !normalized.includes('.')
235
+ }
236
+
237
+ export function isTitleFetchable(value: string): boolean {
238
+ if (!value || SKIP_PROTO_RE.test(value)) {
239
+ return false
240
+ }
241
+
242
+ const url = parseUrl(value)
243
+
244
+ return Boolean(url && /^https?:$/.test(url.protocol) && !isPrivateOrLocalHost(url.hostname))
245
+ }
246
+
247
+ function decodeHtmlEntities(value: string): string {
248
+ return value
249
+ .replace(/&(amp|lt|gt|quot|apos|nbsp|#39);/gi, (_match, key: string) => HTML_ENTITIES[key.toLowerCase()] ?? '')
250
+ .replace(/&#x([0-9a-f]+);/gi, (_match, hex: string) => String.fromCodePoint(parseInt(hex, 16) || 32))
251
+ .replace(/&#(\d+);/g, (_match, decimal: string) => String.fromCodePoint(parseInt(decimal, 10) || 32))
252
+ }
253
+
254
+ function parseHtmlTitle(html: string): string {
255
+ const raw = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]
256
+
257
+ return raw ? decodeHtmlEntities(raw).replace(/\s+/g, ' ').trim() : ''
258
+ }
259
+
260
+ async function readResponseSnippet(response: Response): Promise<string> {
261
+ const reader = response.body?.getReader()
262
+
263
+ if (!reader) {
264
+ return (await response.text()).slice(0, TITLE_BYTE_BUDGET)
265
+ }
266
+
267
+ const chunks: Uint8Array[] = []
268
+ let done = false
269
+ let bytes = 0
270
+
271
+ try {
272
+ while (bytes < TITLE_BYTE_BUDGET) {
273
+ const chunk = await reader.read()
274
+
275
+ if (chunk.done) {
276
+ done = true
277
+
278
+ break
279
+ }
280
+
281
+ const value = chunk.value
282
+
283
+ if (!value?.length) {
284
+ continue
285
+ }
286
+
287
+ const remaining = TITLE_BYTE_BUDGET - bytes
288
+ const next = value.length > remaining ? value.subarray(0, remaining) : value
289
+
290
+ chunks.push(next)
291
+ bytes += next.length
292
+
293
+ if (next.length < value.length) {
294
+ break
295
+ }
296
+ }
297
+ } catch {
298
+ return ''
299
+ } finally {
300
+ if (!done) {
301
+ try {
302
+ await reader.cancel()
303
+ } catch {
304
+ // Ignore stream teardown failures.
305
+ }
306
+ }
307
+ }
308
+
309
+ if (!chunks.length) {
310
+ return ''
311
+ }
312
+
313
+ const joined = new Uint8Array(bytes)
314
+ let offset = 0
315
+
316
+ for (const chunk of chunks) {
317
+ joined.set(chunk, offset)
318
+ offset += chunk.length
319
+ }
320
+
321
+ return new TextDecoder().decode(joined)
322
+ }
323
+
324
+ function usableTitle(value: string): string {
325
+ const clean = value.replace(/\s+/g, ' ').trim()
326
+
327
+ return clean && !TITLE_ERROR_RE.test(clean) ? clean : ''
328
+ }
329
+
330
+ async function fetchHtmlTitle(normalizedUrl: string): Promise<string> {
331
+ const controller = new AbortController()
332
+ const timeout = setTimeout(() => controller.abort(), TITLE_TIMEOUT_MS)
333
+
334
+ try {
335
+ const response = await fetch(normalizedUrl, {
336
+ headers: {
337
+ Accept: 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.5',
338
+ 'Accept-Language': 'en-US,en;q=0.7',
339
+ 'User-Agent': TITLE_USER_AGENT
340
+ },
341
+ redirect: 'follow',
342
+ signal: controller.signal
343
+ })
344
+
345
+ if (!response.ok) {
346
+ return ''
347
+ }
348
+
349
+ const contentType = response.headers.get('content-type')
350
+
351
+ if (contentType && !/(?:html|xml|text\/html)/i.test(contentType)) {
352
+ return ''
353
+ }
354
+
355
+ const html = await readResponseSnippet(response)
356
+
357
+ return parseHtmlTitle(html).slice(0, TITLE_MAX_LENGTH)
358
+ } catch {
359
+ return ''
360
+ } finally {
361
+ clearTimeout(timeout)
362
+ }
363
+ }
364
+
365
+ export function fetchLinkTitle(url: string): Promise<string> {
366
+ const normalizedUrl = normalizeExternalUrl(url)
367
+ const key = titleCacheKey(normalizedUrl)
368
+
369
+ if (!isTitleFetchable(normalizedUrl)) {
370
+ return Promise.resolve('')
371
+ }
372
+
373
+ if (titleCache.has(key)) {
374
+ return Promise.resolve(titleCache.get(key) ?? '')
375
+ }
376
+
377
+ const pending = titleInflight.get(key)
378
+
379
+ if (pending) {
380
+ return pending
381
+ }
382
+
383
+ const promise = fetchHtmlTitle(normalizedUrl)
384
+ .then(usableTitle)
385
+ .catch(() => '')
386
+ .then(clean => {
387
+ cacheTitle(key, clean)
388
+ titleSubs.get(key)?.forEach(sub => sub(clean))
389
+
390
+ return clean
391
+ })
392
+ .finally(() => {
393
+ titleInflight.delete(key)
394
+ })
395
+
396
+ titleInflight.set(key, promise)
397
+
398
+ return promise
399
+ }
400
+
401
+ export function useLinkTitle(url?: null | string): string {
402
+ const normalizedUrl = useMemo(() => (url ? normalizeExternalUrl(url) : ''), [url])
403
+ const key = useMemo(() => (normalizedUrl ? titleCacheKey(normalizedUrl) : ''), [normalizedUrl])
404
+ const [title, setTitle] = useState(() => (key ? (titleCache.get(key) ?? '') : ''))
405
+
406
+ useEffect(() => {
407
+ setTitle(key ? (titleCache.get(key) ?? '') : '')
408
+
409
+ if (!key || !isTitleFetchable(normalizedUrl)) {
410
+ return
411
+ }
412
+
413
+ const subs = titleSubs.get(key) ?? new Set<(value: string) => void>()
414
+
415
+ subs.add(setTitle)
416
+ titleSubs.set(key, subs)
417
+ void fetchLinkTitle(normalizedUrl)
418
+
419
+ return () => {
420
+ subs.delete(setTitle)
421
+
422
+ if (!subs.size) {
423
+ titleSubs.delete(key)
424
+ }
425
+ }
426
+ }, [key, normalizedUrl])
427
+
428
+ return title
429
+ }
430
+
431
+ export function __resetLinkTitleCache(): void {
432
+ titleCache.clear()
433
+ titleInflight.clear()
434
+ titleSubs.clear()
435
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Targeted 24-bit truecolor override before chalk / supports-color imports.
3
+ *
4
+ * macOS Terminal.app before Tahoe 26 does not support RGB SGR, so do not
5
+ * infer truecolor from TERM_PROGRAM=Apple_Terminal. Users can still opt in
6
+ * explicitly on terminals that support RGB but do not advertise COLORTERM.
7
+ */
8
+
9
+ const TRUE_RE = /^(?:1|true|yes|on)$/i
10
+ const FALSE_RE = /^(?:0|false|no|off)$/i
11
+
12
+ export function shouldForceTruecolor(env: NodeJS.ProcessEnv = process.env): boolean {
13
+ const override = (env.NASTECH_TUI_TRUECOLOR ?? '').trim()
14
+
15
+ if (FALSE_RE.test(override) || 'NO_COLOR' in env) {
16
+ return false
17
+ }
18
+
19
+ return TRUE_RE.test(override)
20
+ }
21
+
22
+ const isAppleTerminal = (env: NodeJS.ProcessEnv = process.env) => (env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal'
23
+
24
+ const isAdvertisedTruecolor = (env: NodeJS.ProcessEnv = process.env) => {
25
+ const colorTerm = (env.COLORTERM ?? '').trim().toLowerCase()
26
+ const forceColor = (env.FORCE_COLOR ?? '').trim()
27
+
28
+ return colorTerm === 'truecolor' || colorTerm === '24bit' || forceColor === '3'
29
+ }
30
+
31
+ export function shouldDowngradeAppleTerminalTruecolor(env: NodeJS.ProcessEnv = process.env): boolean {
32
+ if (!isAppleTerminal(env)) {
33
+ return false
34
+ }
35
+
36
+ if (shouldForceTruecolor(env)) {
37
+ return false
38
+ }
39
+
40
+ return isAdvertisedTruecolor(env)
41
+ }
42
+
43
+ if (shouldForceTruecolor()) {
44
+ if (!process.env.COLORTERM) {
45
+ process.env.COLORTERM = 'truecolor'
46
+ }
47
+
48
+ process.env.FORCE_COLOR = '3'
49
+ } else if (shouldDowngradeAppleTerminalTruecolor()) {
50
+ // Terminal.app may advertise truecolor even when RGB SGR paths render
51
+ // incorrectly. Keep NasTech on the safer TERM-driven 256-color path unless
52
+ // users explicitly opt back in via NASTECH_TUI_TRUECOLOR=1.
53
+ delete process.env.COLORTERM
54
+
55
+ if ((process.env.FORCE_COLOR ?? '').trim() === '3') {
56
+ delete process.env.FORCE_COLOR
57
+ }
58
+ }
59
+
60
+ export {}
@@ -0,0 +1,51 @@
1
+ // Tiny FPS tracker fed by ink's onFrame callback. Each entry is an Ink
2
+ // frame (React commit + drain-only frames) — the right notion for
3
+ // user-perceived motion.
4
+ //
5
+ // Zero-cost when NASTECH_TUI_FPS is unset: trackFrame is undefined so the
6
+ // onFrame callback short-circuits at the optional chain.
7
+
8
+ import { atom } from 'nanostores'
9
+
10
+ import { SHOW_FPS } from '../config/env.js'
11
+
12
+ const WINDOW_SIZE = 30
13
+
14
+ export type FpsState = {
15
+ fps: number
16
+ /** Wraps at JS-safe int — diff pairs in a debug overlay safely. */
17
+ totalFrames: number
18
+ /** Ink render-phase total for the last frame. */
19
+ lastDurationMs: number
20
+ }
21
+
22
+ export const $fpsState = atom<FpsState>({ fps: 0, lastDurationMs: 0, totalFrames: 0 })
23
+
24
+ const timestamps: number[] = []
25
+ let totalFrames = 0
26
+
27
+ export const trackFrame = SHOW_FPS
28
+ ? (durationMs: number) => {
29
+ timestamps.push(performance.now())
30
+
31
+ if (timestamps.length > WINDOW_SIZE) {
32
+ timestamps.shift()
33
+ }
34
+
35
+ totalFrames++
36
+
37
+ if (timestamps.length < 2) {
38
+ return
39
+ }
40
+
41
+ const elapsed = (timestamps[timestamps.length - 1]! - timestamps[0]!) / 1000
42
+
43
+ if (elapsed > 0) {
44
+ $fpsState.set({
45
+ fps: Math.round(((timestamps.length - 1) / elapsed) * 10) / 10,
46
+ lastDurationMs: Math.round(durationMs * 100) / 100,
47
+ totalFrames
48
+ })
49
+ }
50
+ }
51
+ : undefined
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { fuzzyRank, fuzzyScore, fuzzyScoreMulti } from './fuzzy.js'
4
+
5
+ describe('fuzzyScore', () => {
6
+ it('matches a query as a subsequence (g4o → gpt-4o)', () => {
7
+ expect(fuzzyScore('gpt-4o', 'g4o')).not.toBeNull()
8
+ expect(fuzzyScore('gpt-4o', 'gpt')).not.toBeNull()
9
+ expect(fuzzyScore('gpt-4o', '4o')).not.toBeNull()
10
+ })
11
+
12
+ it('returns null when characters are out of order or absent', () => {
13
+ expect(fuzzyScore('gpt-4o', 'o4g')).toBeNull()
14
+ expect(fuzzyScore('gpt-4o', 'xyz')).toBeNull()
15
+ expect(fuzzyScore('gpt-4o', 'gptx')).toBeNull()
16
+ })
17
+
18
+ it('returns matched positions into the original target', () => {
19
+ const m = fuzzyScore('gpt-4o', 'g4o')
20
+ // g@0, 4@4, o@5
21
+ expect(m?.positions).toEqual([0, 4, 5])
22
+ })
23
+
24
+ it('treats an empty query as a zero-score match', () => {
25
+ expect(fuzzyScore('anything', '')).toEqual({ score: 0, positions: [] })
26
+ })
27
+
28
+ it('scores an exact match highest', () => {
29
+ const exact = fuzzyScore('sonnet', 'sonnet')!.score
30
+ const prefix = fuzzyScore('sonnet-extended', 'sonnet')!.score
31
+ // s,o,n,n,e,t all present in order but scattered across word boundaries.
32
+ const scattered = fuzzyScore('snorkel-online-nnet', 'sonnet')!.score
33
+
34
+ expect(exact).toBeGreaterThan(prefix)
35
+ expect(prefix).toBeGreaterThan(scattered)
36
+ })
37
+
38
+ it('ranks a prefix match above a scattered subsequence', () => {
39
+ const prefix = fuzzyScore('gpt-4o-mini', 'gpt')!.score
40
+ const scattered = fuzzyScore('a-g-p-t', 'gpt')!.score
41
+
42
+ expect(prefix).toBeGreaterThan(scattered)
43
+ })
44
+
45
+ it('rewards word-boundary matches', () => {
46
+ // `s4` matching the `s` of sonnet and the `4` after a dash
47
+ const boundary = fuzzyScore('claude-sonnet-4', 'cs4')
48
+ expect(boundary).not.toBeNull()
49
+ })
50
+ })
51
+
52
+ describe('fuzzyScoreMulti', () => {
53
+ it('requires every space-separated token to match (AND)', () => {
54
+ expect(fuzzyScoreMulti('claude-sonnet-4', 'clad snnt')).not.toBeNull()
55
+ expect(fuzzyScoreMulti('claude-sonnet-4', 'claude haiku')).toBeNull()
56
+ })
57
+
58
+ it('unions matched positions across tokens, sorted', () => {
59
+ const m = fuzzyScoreMulti('claude-sonnet', 'son cla')
60
+ expect(m).not.toBeNull()
61
+ expect(m!.positions).toEqual([...m!.positions].sort((a, b) => a - b))
62
+ })
63
+
64
+ it('treats whitespace-only query as a zero-score match', () => {
65
+ expect(fuzzyScoreMulti('x', ' ')).toEqual({ score: 0, positions: [] })
66
+ })
67
+ })
68
+
69
+ describe('fuzzyRank', () => {
70
+ const models = ['gpt-4o', 'gpt-4o-mini', 'claude-sonnet-4', 'claude-haiku', 'o1-preview']
71
+
72
+ it('drops non-matching items and ranks matches by score', () => {
73
+ const ranked = fuzzyRank(models, 'g4o', m => m)
74
+ const ids = ranked.map(r => r.item)
75
+
76
+ expect(ids).toContain('gpt-4o')
77
+ expect(ids).toContain('gpt-4o-mini')
78
+ expect(ids).not.toContain('claude-haiku')
79
+ // Shorter exact-ish prefix should outrank the longer variant.
80
+ expect(ids.indexOf('gpt-4o')).toBeLessThan(ids.indexOf('gpt-4o-mini'))
81
+ })
82
+
83
+ it('ranks son4 so a sonnet model surfaces', () => {
84
+ const ranked = fuzzyRank(models, 'son4', m => m)
85
+ expect(ranked[0]?.item).toBe('claude-sonnet-4')
86
+ })
87
+
88
+ it('returns all items in original order for an empty query', () => {
89
+ const ranked = fuzzyRank(models, '', m => m)
90
+ expect(ranked.map(r => r.item)).toEqual(models)
91
+ expect(ranked.every(r => r.positions.length === 0)).toBe(true)
92
+ })
93
+
94
+ it('is stable for equal scores (original index tiebreak)', () => {
95
+ const items = ['ab', 'ab', 'ab']
96
+ const ranked = fuzzyRank(items.map((v, i) => ({ v, i })), 'ab', x => x.v)
97
+ expect(ranked.map(r => r.item.i)).toEqual([0, 1, 2])
98
+ })
99
+
100
+ it('matches across a derived key, not just the raw string', () => {
101
+ const providers = [
102
+ { slug: 'openai', name: 'OpenAI' },
103
+ { slug: 'anthropic', name: 'Anthropic' }
104
+ ]
105
+
106
+ const ranked = fuzzyRank(providers, 'anth', p => `${p.name} ${p.slug}`)
107
+ expect(ranked[0]?.item.slug).toBe('anthropic')
108
+ })
109
+ })