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,331 @@
1
+ import { PassThrough } from 'stream'
2
+
3
+ import { Box, renderSync } from '@nastechai/ink'
4
+ import React from 'react'
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import { AUDIO_DIRECTIVE_RE, INLINE_RE, Md, MEDIA_LINE_RE, stripInlineMarkup } from '../components/markdown.js'
8
+ import { stripAnsi } from '../lib/text.js'
9
+ import { DEFAULT_THEME } from '../theme.js'
10
+
11
+ const matches = (text: string) => [...text.matchAll(INLINE_RE)].map(m => m[0])
12
+ const BEL = String.fromCharCode(7)
13
+ const ESC = String.fromCharCode(27)
14
+ const CSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, 'g')
15
+ const OSC_RE = new RegExp(`${ESC}\\][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g')
16
+
17
+ const renderPlain = (node: React.ReactNode) => {
18
+ const stdout = new PassThrough()
19
+ const stdin = new PassThrough()
20
+ const stderr = new PassThrough()
21
+ let output = ''
22
+
23
+ Object.assign(stdout, { columns: 80, isTTY: false, rows: 24 })
24
+ Object.assign(stdin, { isTTY: false })
25
+ Object.assign(stderr, { isTTY: false })
26
+ stdout.on('data', chunk => {
27
+ output += chunk.toString()
28
+ })
29
+
30
+ const instance = renderSync(node, {
31
+ patchConsole: false,
32
+ stderr: stderr as NodeJS.WriteStream,
33
+ stdin: stdin as NodeJS.ReadStream,
34
+ stdout: stdout as NodeJS.WriteStream
35
+ })
36
+
37
+ instance.unmount()
38
+ instance.cleanup()
39
+
40
+ return output
41
+ .replace(OSC_RE, '')
42
+ .split('\n')
43
+ .map(line => stripAnsi(line).replace(CSI_RE, '').trimEnd())
44
+ }
45
+
46
+ describe('INLINE_RE emphasis', () => {
47
+ it('matches word-boundary italic/bold', () => {
48
+ expect(matches('say _hi_ there')).toEqual(['_hi_'])
49
+ expect(matches('very __bold move__ today')).toEqual(['__bold move__'])
50
+ expect(matches('(_paren_) and [_bracket_]')).toEqual(['_paren_', '_bracket_'])
51
+ })
52
+
53
+ it('keeps intraword underscores literal', () => {
54
+ const path = '/home/me/.nastech/cache/screenshots/browser_screenshot_ecc1c3feab.png'
55
+
56
+ expect(matches(path)).toEqual([])
57
+ expect(matches('snake_case_var and MY_CONST')).toEqual([])
58
+ expect(matches('foo__bar__baz')).toEqual([])
59
+ })
60
+
61
+ it('keeps Python dunder identifiers literal', () => {
62
+ expect(matches('if __name__ == "__main__":')).toEqual([])
63
+ expect(matches('def __init__(self):')).toEqual([])
64
+ expect(matches('print(__file__)')).toEqual([])
65
+ })
66
+
67
+ it('still matches asterisk emphasis intraword', () => {
68
+ expect(matches('a*b*c')).toEqual(['*b*'])
69
+ expect(matches('a**bold**c')).toEqual(['**bold**'])
70
+ })
71
+
72
+ it('matches short alphanumeric subscript (H~2~O, CO~2~, X~n~)', () => {
73
+ expect(matches('H~2~O')).toEqual(['~2~'])
74
+ expect(matches('CO~2~ levels')).toEqual(['~2~'])
75
+ expect(matches('the X~n~ term')).toEqual(['~n~'])
76
+ })
77
+
78
+ it('ignores kaomoji-style ~! and ~? punctuation', () => {
79
+ // Kimi / Qwen / GLM emit these as decorators and the whole span between
80
+ // two tildes used to get collapsed into one dim blob.
81
+ expect(matches('Aww ~! Building step by step, I love it ~!')).toEqual([])
82
+ expect(matches('cool ~? yeah ~?')).toEqual([])
83
+ expect(matches('mixed ~! and ~? flow')).toEqual([])
84
+ })
85
+
86
+ it('ignores tilde spans that contain spaces or punctuation', () => {
87
+ // Real subscript doesn't contain spaces; a tilde followed by words-then-
88
+ // tilde is almost always conversational. Matching it swallows text.
89
+ expect(matches('hello ~good idea~ there')).toEqual([])
90
+ expect(matches('x ~oh no!~ y')).toEqual([])
91
+ })
92
+
93
+ it('does not let strikethrough eat subscript', () => {
94
+ expect(matches('~~strike~~ and H~2~O')).toEqual(['~~strike~~', '~2~'])
95
+ })
96
+ })
97
+
98
+ describe('stripInlineMarkup', () => {
99
+ it('strips word-boundary emphasis only', () => {
100
+ expect(stripInlineMarkup('say _hi_ there')).toBe('say hi there')
101
+ expect(stripInlineMarkup('browser_screenshot_ecc.png')).toBe('browser_screenshot_ecc.png')
102
+ expect(stripInlineMarkup('__bold move__ and foo__bar__')).toBe('bold move and foo__bar__')
103
+ })
104
+
105
+ it('preserves Python dunder identifiers', () => {
106
+ expect(stripInlineMarkup('if __name__ == "__main__":')).toBe('if __name__ == "__main__":')
107
+ expect(stripInlineMarkup('class X: def __init__(self): pass')).toBe('class X: def __init__(self): pass')
108
+ })
109
+
110
+ it('leaves ~!/~? kaomoji alone and still handles real subscript', () => {
111
+ expect(stripInlineMarkup('Yay ~! nice work ~!')).toBe('Yay ~! nice work ~!')
112
+ expect(stripInlineMarkup('H~2~O and CO~2~')).toBe('H_2O and CO_2')
113
+ })
114
+
115
+ it('strips inline math delimiters but keeps the formula text', () => {
116
+ expect(stripInlineMarkup('$\\mathbb{Z}$ is a ring')).toBe('\\mathbb{Z} is a ring')
117
+ expect(stripInlineMarkup('see \\(a + b\\) ok')).toBe('see a + b ok')
118
+ })
119
+ })
120
+
121
+ describe('INLINE_RE inline math', () => {
122
+ it('matches single-dollar math and beats emphasis at the same start', () => {
123
+ // Without math handling, `*b*` would have matched as italics and
124
+ // corrupted the formula. With math added to INLINE_RE, the leftmost
125
+ // match at column 0 (`$P=a*b*c$`) wins.
126
+ expect(matches('$P=a*b*c$')).toEqual(['$P=a*b*c$'])
127
+ expect(matches('see $\\mathbb{Z}$ here')).toEqual(['$\\mathbb{Z}$'])
128
+ })
129
+
130
+ it('does not match currency-style prose', () => {
131
+ expect(matches('it costs $5 and $10')).toEqual([])
132
+ expect(matches('paid $5')).toEqual([])
133
+ })
134
+
135
+ it('does not let inline math swallow a $$ display fence', () => {
136
+ // `$$x$$` is a display block, not two abutting inline-math spans.
137
+ expect(matches('$$x$$')).toEqual([])
138
+ })
139
+
140
+ it('matches \\(...\\) inline math', () => {
141
+ expect(matches('foo \\(x + y\\) bar')).toEqual(['\\(x + y\\)'])
142
+ })
143
+
144
+ it('does not corrupt subscripts/superscripts inside math', () => {
145
+ // `_n` and `^r` are markdown emphasis/superscript markers in prose, but
146
+ // inside a `$...$` span the entire formula is captured as a single
147
+ // inline-math token so the inner regexes never see those characters.
148
+ expect(matches('$P=a_n x^n + a_0$')).toEqual(['$P=a_n x^n + a_0$'])
149
+ expect(matches('$\\beta_1,\\dots,\\beta_r$')).toEqual(['$\\beta_1,\\dots,\\beta_r$'])
150
+ })
151
+
152
+ it('places math content in the correct capture group (regression: m[16] is bare URL)', () => {
153
+ // When `m[16]` was the bare URL group AND the inline-math `$...$`
154
+ // group simultaneously (because the bare URL pattern lacked its own
155
+ // capturing parens), MdInline rendered `$\\mathbb{R}$` as an
156
+ // underlined autolink instead of italic amber math. Lock down the
157
+ // numbering: math goes in m[17] / m[18], URLs go in m[16].
158
+ const url = [...'see https://example.com here'.matchAll(INLINE_RE)][0]!
159
+ const dollarMath = [...'$\\mathbb{R}$'.matchAll(INLINE_RE)][0]!
160
+ const parenMath = [...'\\(\\pi\\)'.matchAll(INLINE_RE)][0]!
161
+
162
+ expect(url[16]).toBe('https://example.com')
163
+ expect(url[17]).toBeUndefined()
164
+ expect(url[18]).toBeUndefined()
165
+
166
+ expect(dollarMath[16]).toBeUndefined()
167
+ expect(dollarMath[17]).toBe('\\mathbb{R}')
168
+ expect(dollarMath[18]).toBeUndefined()
169
+
170
+ expect(parenMath[16]).toBeUndefined()
171
+ expect(parenMath[17]).toBeUndefined()
172
+ expect(parenMath[18]).toBe('\\pi')
173
+ })
174
+ })
175
+
176
+ describe('protocol sentinels', () => {
177
+ it('captures MEDIA: paths with surrounding quotes or backticks', () => {
178
+ expect('MEDIA:/tmp/a.png'.match(MEDIA_LINE_RE)?.[1]).toBe('/tmp/a.png')
179
+ expect(' MEDIA: /home/me/.nastech/cache/screenshots/browser_screenshot_ecc.png '.match(MEDIA_LINE_RE)?.[1]).toBe(
180
+ '/home/me/.nastech/cache/screenshots/browser_screenshot_ecc.png'
181
+ )
182
+ expect('`MEDIA:/tmp/a.png`'.match(MEDIA_LINE_RE)?.[1]).toBe('/tmp/a.png')
183
+ expect('"MEDIA:C:\\files\\a.png"'.match(MEDIA_LINE_RE)?.[1]).toBe('C:\\files\\a.png')
184
+ })
185
+
186
+ it('ignores MEDIA: tokens embedded in prose', () => {
187
+ expect('here is MEDIA:/tmp/a.png for you'.match(MEDIA_LINE_RE)).toBeNull()
188
+ expect('the media: section is empty'.match(MEDIA_LINE_RE)).toBeNull()
189
+ })
190
+
191
+ it('matches the [[audio_as_voice]] directive', () => {
192
+ expect(AUDIO_DIRECTIVE_RE.test('[[audio_as_voice]]')).toBe(true)
193
+ expect(AUDIO_DIRECTIVE_RE.test(' [[audio_as_voice]] ')).toBe(true)
194
+ expect(AUDIO_DIRECTIVE_RE.test('audio_as_voice')).toBe(false)
195
+ })
196
+ })
197
+
198
+ describe('Md wrapping', () => {
199
+ it('trims spaces from word-wrap continuation lines', () => {
200
+ const lines = renderPlain(
201
+ React.createElement(Box, { width: 5 }, React.createElement(Md, { t: DEFAULT_THEME, text: 'Let me' }))
202
+ )
203
+
204
+ expect(lines).toContain('Let')
205
+ expect(lines).toContain('me')
206
+ expect(lines).not.toContain(' me')
207
+ })
208
+
209
+ it('keeps nested list and quote indentation out of trim-sensitive text', () => {
210
+ const lines = renderPlain(
211
+ React.createElement(
212
+ Box,
213
+ { flexDirection: 'column', width: 24 },
214
+ React.createElement(Md, { t: DEFAULT_THEME, text: ' - nested bullet' }),
215
+ React.createElement(Md, { t: DEFAULT_THEME, text: '>> nested quote' })
216
+ )
217
+ )
218
+
219
+ expect(lines).toContain(' • nested bullet')
220
+ expect(lines).toContain(' │ nested quote')
221
+ })
222
+
223
+ it('preserves original inline-code edge spaces', () => {
224
+ const lines = renderPlain(
225
+ React.createElement(Box, { width: 24 }, React.createElement(Md, { t: DEFAULT_THEME, text: '` hi ` ok' }))
226
+ )
227
+
228
+ expect(lines.some(line => line.startsWith(' hi ok'))).toBe(true)
229
+ })
230
+
231
+ it('renders Python dunder identifiers literally outside code fences', () => {
232
+ const lines = renderPlain(
233
+ React.createElement(
234
+ Box,
235
+ { width: 80 },
236
+ React.createElement(Md, {
237
+ t: DEFAULT_THEME,
238
+ text: 'if __name__ == "__main__":\n obj.__init__()'
239
+ })
240
+ )
241
+ )
242
+
243
+ const rendered = lines.join('\n')
244
+
245
+ expect(rendered).toContain('if __name__ == "__main__":')
246
+ expect(rendered).toContain('obj.__init__()')
247
+ })
248
+ })
249
+
250
+ describe('Md link labels', () => {
251
+ it('renders bare URLs with readable slug labels', () => {
252
+ const lines = renderPlain(
253
+ React.createElement(
254
+ Box,
255
+ { width: 120 },
256
+ React.createElement(Md, {
257
+ t: DEFAULT_THEME,
258
+ text: 'see https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure for details'
259
+ })
260
+ )
261
+ )
262
+
263
+ const rendered = lines.join('\n')
264
+
265
+ expect(rendered).toContain('Puerto Rico El Yunque Rainforest Adventure')
266
+ expect(rendered).not.toContain('https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure')
267
+ })
268
+
269
+ it('keeps explicit markdown labels as the immediate fallback', () => {
270
+ const lines = renderPlain(
271
+ React.createElement(
272
+ Box,
273
+ { width: 80 },
274
+ React.createElement(Md, {
275
+ t: DEFAULT_THEME,
276
+ text: '[Trip details](https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure)'
277
+ })
278
+ )
279
+ )
280
+
281
+ expect(lines.join('\n')).toContain('Trip details')
282
+ })
283
+ })
284
+
285
+ describe('renderTable CJK width alignment', () => {
286
+ it('column starts share the same display offset across CJK rows', async () => {
287
+ const { stringWidth } = await import('@nastechai/ink')
288
+
289
+ const md = [
290
+ '| 配置 | Config | 状态 |',
291
+ '|------|--------|------|',
292
+ '| Vicuna (report) | dense | × |',
293
+ '| ChatGLM | chat | ✓ |',
294
+ '| 通义千问 | qwen | × |'
295
+ ].join('\n')
296
+
297
+ // Pre-fix bug: ` `.repeat(w - stripInlineMarkup(...).length) used
298
+ // UTF-16 code units, so a CJK header cell padded to 2 cells while
299
+ // the body cell padded to 4, drifting subsequent columns by 2
300
+ // cells per CJK char.
301
+ //
302
+ // Post-fix contract: the prefix preceding the start of column N
303
+ // has the same display width across the header and every body row
304
+ // (deduped to skip the divider, which renders independently).
305
+ const lines = renderPlain(
306
+ React.createElement(Box, null, React.createElement(Md, { compact: true, t: DEFAULT_THEME, text: md }))
307
+ ).filter(line => line.trim().length > 0)
308
+
309
+ // Heuristic: a "data row" line either contains 'Config' (header)
310
+ // or one of the body labels; a divider is all box-drawing. Use
311
+ // the substring 'Config' / 'dense' / 'chat' / 'qwen' as the
312
+ // unique anchor for column 2's start position on each row.
313
+ const colStarts = (line: string, anchor: string): number => {
314
+ const idx = line.indexOf(anchor)
315
+
316
+ return idx < 0 ? -1 : stringWidth(line.slice(0, idx))
317
+ }
318
+
319
+ const headerCol2 = lines.map(l => colStarts(l, 'Config')).find(v => v >= 0)
320
+ const denseCol2 = lines.map(l => colStarts(l, 'dense')).find(v => v >= 0)
321
+ const chatCol2 = lines.map(l => colStarts(l, 'chat')).find(v => v >= 0)
322
+ const qwenCol2 = lines.map(l => colStarts(l, 'qwen')).find(v => v >= 0)
323
+
324
+ expect(headerCol2).toBeDefined()
325
+ expect(denseCol2).toBe(headerCol2)
326
+ expect(chatCol2).toBe(headerCol2)
327
+ // The CJK row is the one that drifted before the fix. It must
328
+ // align with the rest now.
329
+ expect(qwenCol2).toBe(headerCol2)
330
+ })
331
+ })
@@ -0,0 +1,293 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { BOX_CLOSE, BOX_OPEN, BOX_RE, texToUnicode } from '../lib/mathUnicode.js'
4
+
5
+ const stripBox = (s: string) => s.replace(BOX_RE, '$1')
6
+
7
+ describe('texToUnicode — symbols', () => {
8
+ it('substitutes lowercase Greek', () => {
9
+ expect(texToUnicode('\\alpha + \\beta + \\pi')).toBe('α + β + π')
10
+ expect(texToUnicode('\\omega')).toBe('ω')
11
+ })
12
+
13
+ it('substitutes uppercase Greek', () => {
14
+ expect(texToUnicode('\\Sigma \\Omega \\Pi')).toBe('Σ Ω Π')
15
+ })
16
+
17
+ it('substitutes set theory and logic operators', () => {
18
+ expect(texToUnicode('A \\cup B \\cap C')).toBe('A ∪ B ∩ C')
19
+ expect(texToUnicode('\\forall x \\in \\emptyset')).toBe('∀ x ∈ ∅')
20
+ expect(texToUnicode('p \\implies q \\iff r')).toBe('p ⟹ q ⟺ r')
21
+ })
22
+
23
+ it('substitutes relations and arrows', () => {
24
+ expect(texToUnicode('a \\le b \\ge c \\ne d')).toBe('a ≤ b ≥ c ≠ d')
25
+ expect(texToUnicode('f: A \\to B')).toBe('f: A → B')
26
+ })
27
+
28
+ it('uses longest-match-first so \\leq beats \\le', () => {
29
+ expect(texToUnicode('\\leq')).toBe('≤')
30
+ })
31
+
32
+ it('preserves unknown commands that share a prefix with known ones', () => {
33
+ // `\leqq` is a real LaTeX command (≦) we don't have in our table.
34
+ // The word-boundary lookahead prevents `\le` from matching, so the
35
+ // whole thing is preserved verbatim — much better than `≤qq`.
36
+ expect(texToUnicode('\\leqq')).toBe('\\leqq')
37
+ })
38
+
39
+ it('refuses to substitute a partial command (word boundary)', () => {
40
+ expect(texToUnicode('\\alphabet')).toBe('\\alphabet')
41
+ expect(texToUnicode('\\pin')).toBe('\\pin')
42
+ })
43
+ })
44
+
45
+ describe('texToUnicode — blackboard / calligraphic / fraktur', () => {
46
+ it('renders \\mathbb capitals', () => {
47
+ expect(texToUnicode('\\mathbb{R}')).toBe('ℝ')
48
+ expect(texToUnicode('\\mathbb{N} \\subset \\mathbb{Z} \\subset \\mathbb{Q} \\subset \\mathbb{R}')).toBe('ℕ ⊂ ℤ ⊂ ℚ ⊂ ℝ')
49
+ })
50
+
51
+ it('renders \\mathcal and \\mathfrak', () => {
52
+ expect(texToUnicode('\\mathcal{F} \\subset \\mathfrak{A}')).toBe('ℱ ⊂ 𝔄')
53
+ })
54
+
55
+ it('preserves \\mathbb{...} when argument is multi-letter or non-letter', () => {
56
+ expect(texToUnicode('\\mathbb{NN}')).toBe('\\mathbb{NN}')
57
+ expect(texToUnicode('\\mathbb{1}')).toBe('\\mathbb{1}')
58
+ })
59
+
60
+ it('strips \\mathbf / \\mathit / \\mathrm / \\text wrappers (no Unicode bold/italic in monospace)', () => {
61
+ expect(texToUnicode('\\mathbf{x}')).toBe('x')
62
+ expect(texToUnicode('\\text{if } x > 0')).toBe('if x > 0')
63
+ expect(texToUnicode('\\operatorname{rank}(A)')).toBe('rank(A)')
64
+ })
65
+ })
66
+
67
+ describe('texToUnicode — sub / superscripts', () => {
68
+ it('converts simple superscripts', () => {
69
+ expect(texToUnicode('x^2 + y^2')).toBe('x² + y²')
70
+ expect(texToUnicode('e^{n}')).toBe('eⁿ')
71
+ })
72
+
73
+ it('converts simple subscripts', () => {
74
+ expect(texToUnicode('a_1 + a_2 + a_n')).toBe('a₁ + a₂ + aₙ')
75
+ expect(texToUnicode('x_{0}')).toBe('x₀')
76
+ })
77
+
78
+ it('converts mixed-content scripts when every glyph has a Unicode form', () => {
79
+ // `+`, digits, and lowercase letters all have superscript glyphs,
80
+ // so `n+1` → `ⁿ⁺¹`. Comma has no subscript form, so `i,j` falls
81
+ // back to `_(i,j)` (parens) rather than partially substituting —
82
+ // parens read as ordinary grouping while braces look like leftover
83
+ // unrendered LaTeX.
84
+ expect(texToUnicode('x^{n+1}')).toBe('xⁿ⁺¹')
85
+ expect(texToUnicode('a_{i,j}')).toBe('a_(i,j)')
86
+ })
87
+
88
+ it('uses parens (not braces) when the body has Greek with no superscript form', () => {
89
+ // π has no Unicode superscript, so `e^{i\pi}` after symbol pass is
90
+ // `e^{iπ}` and the script fallback emits `e^(iπ)` — much more
91
+ // readable than the LaTeX-looking `e^{iπ}`.
92
+ expect(texToUnicode('e^{i\\pi}')).toBe('e^(iπ)')
93
+ })
94
+
95
+ it('strips braces on script fallback when body collapses to a single char', () => {
96
+ // `^{\infty}` → symbol pass produces `^{∞}` → convertScript can't
97
+ // find ∞ in SUPERSCRIPT, but the body is one char so we drop the
98
+ // braces and emit `^∞` (much more readable than `^{∞}`).
99
+ expect(texToUnicode('e^{\\infty}')).toBe('e^∞')
100
+ })
101
+
102
+ it('handles a real-world sum', () => {
103
+ expect(texToUnicode('\\sum_{n=0}^{\\infty} \\frac{1}{n!}')).toBe('∑ₙ₌₀^∞ 1/n!')
104
+ })
105
+ })
106
+
107
+ describe('texToUnicode — fractions', () => {
108
+ it('collapses \\frac to a/b', () => {
109
+ expect(texToUnicode('\\frac{1}{2}')).toBe('1/2')
110
+ expect(texToUnicode('\\frac{a}{b}')).toBe('a/b')
111
+ })
112
+
113
+ it('parenthesises multi-token numerator / denominator', () => {
114
+ expect(texToUnicode('\\frac{n+1}{2}')).toBe('(n+1)/2')
115
+ expect(texToUnicode('\\frac{a + b}{c - d}')).toBe('(a + b)/(c - d)')
116
+ })
117
+
118
+ it('handles nested fractions', () => {
119
+ expect(texToUnicode('\\frac{1}{\\frac{1}{x}}')).toBe('1/(1/x)')
120
+ })
121
+
122
+ it('handles braces inside numerator / denominator (regression: regex \\frac couldn\'t)', () => {
123
+ // The regex-only `\frac` matcher used `[^{}]*` for each arg, which
124
+ // failed the moment a numerator contained its own braces (here the
125
+ // `{p-1}` from a superscript). The balanced-brace parser handles it.
126
+ expect(texToUnicode('\\frac{|t|^{p-1}|P(t)|^p}{(p-1)!}')).toBe('(|t|ᵖ⁻¹|P(t)|ᵖ)/((p-1)!)')
127
+ })
128
+
129
+ it('preserves \\frac when arguments are malformed', () => {
130
+ expect(texToUnicode('\\frac{a}')).toBe('\\frac{a}')
131
+ expect(texToUnicode('\\fraction{a}{b}')).toBe('\\fraction{a}{b}')
132
+ })
133
+ })
134
+
135
+ describe('texToUnicode — typography no-ops', () => {
136
+ it('strips \\displaystyle / \\textstyle / \\scriptstyle / \\scriptscriptstyle', () => {
137
+ expect(texToUnicode('\\displaystyle\\sum_{i=1}^n x_i')).toBe('∑ᵢ₌₁ⁿ xᵢ')
138
+ expect(texToUnicode('f(x) = \\displaystyle \\frac{1}{2}')).toBe('f(x) = 1/2')
139
+ expect(texToUnicode('\\textstyle x + y')).toBe('x + y')
140
+ })
141
+
142
+ it('strips \\limits / \\nolimits which only affect bound positioning', () => {
143
+ expect(texToUnicode('\\sum\\limits_{k=1}^n a_k')).toBe('∑ₖ₌₁ⁿ aₖ')
144
+ expect(texToUnicode('\\int\\nolimits_0^1 f(x) dx')).toBe('∫₀¹ f(x) dx')
145
+ })
146
+
147
+ it('does not eat letter-continuation commands like \\limit_inf', () => {
148
+ // The `(?![A-Za-z])` lookahead protects hypothetical commands that
149
+ // start with `\limit` / `\display` / etc. The bare names are stripped
150
+ // but anything longer is preserved verbatim.
151
+ expect(texToUnicode('\\limitinf x')).toBe('\\limitinf x')
152
+ })
153
+ })
154
+
155
+ describe('texToUnicode — sizing wrappers', () => {
156
+ it('strips \\big / \\Big / \\bigg / \\Bigg before delimiters', () => {
157
+ expect(texToUnicode('\\bigl[ x \\bigr]')).toBe('[ x ]')
158
+ expect(texToUnicode('\\Big( y \\Big)')).toBe('( y )')
159
+ expect(texToUnicode('\\bigg| z \\bigg|')).toBe('| z |')
160
+ expect(texToUnicode('\\Biggl\\{ a \\Biggr\\}')).toBe('{ a }')
161
+ })
162
+
163
+ it('does not eat \\bigtriangleup or other letter-continuations', () => {
164
+ expect(texToUnicode('A \\bigtriangleup B')).toBe('A \\bigtriangleup B')
165
+ })
166
+ })
167
+
168
+ describe('texToUnicode — modular arithmetic and tags', () => {
169
+ it('renders \\pmod{p} as " (mod p)"', () => {
170
+ expect(texToUnicode('a \\equiv b \\pmod{p}')).toBe('a ≡ b (mod p)')
171
+ })
172
+
173
+ it('renders \\bmod / \\mod inline', () => {
174
+ expect(texToUnicode('a \\bmod n')).toBe('a mod n')
175
+ })
176
+
177
+ it('collapses \\tag{n} to " (n)"', () => {
178
+ expect(texToUnicode('x = y \\tag{24}')).toBe('x = y (24)')
179
+ })
180
+ })
181
+
182
+ describe('texToUnicode — newly added symbols', () => {
183
+ it('renders \\nmid, \\blacksquare, \\qed', () => {
184
+ expect(texToUnicode('p \\nmid q')).toBe('p ∤ q')
185
+ expect(texToUnicode('Therefore \\blacksquare')).toBe('Therefore ■')
186
+ expect(texToUnicode('done \\qed')).toBe('done ∎')
187
+ })
188
+ })
189
+
190
+ describe('texToUnicode — \\boxed / \\fbox', () => {
191
+ // `\boxed` produces non-printable U+0001 / U+0002 sentinels around its
192
+ // content so the markdown renderer can apply highlight styling. These
193
+ // tests assert both the sentinel form and the human-readable
194
+ // strip-fallback (BOX_RE).
195
+ it('wraps simple boxed content in BOX_OPEN/BOX_CLOSE sentinels', () => {
196
+ expect(texToUnicode('\\boxed{x = 0}')).toBe(`${BOX_OPEN}x = 0${BOX_CLOSE}`)
197
+ expect(stripBox(texToUnicode('\\boxed{x = 0}'))).toBe('x = 0')
198
+ expect(stripBox(texToUnicode('\\fbox{answer}'))).toBe('answer')
199
+ })
200
+
201
+ it('handles boxed expressions with nested braces (regression: regex couldn\'t)', () => {
202
+ // A `[^{}]*` regex would stop at the first `{` inside the body. The
203
+ // balanced-brace parser walks past it.
204
+ expect(stripBox(texToUnicode('\\boxed{x^{n+1}}'))).toBe('xⁿ⁺¹')
205
+ expect(stripBox(texToUnicode('\\boxed{\\frac{a}{b}}'))).toBe('a/b')
206
+ })
207
+
208
+ it('handles real-world boxed final answer', () => {
209
+ expect(stripBox(texToUnicode('\\boxed{J = -\\sum_{k=0}^n a_k F(k)}'))).toBe('J = -∑ₖ₌₀ⁿ aₖ F(k)')
210
+ })
211
+
212
+ it('preserves \\boxed without a brace argument', () => {
213
+ expect(texToUnicode('\\boxed something')).toBe('\\boxed something')
214
+ })
215
+ })
216
+
217
+ describe('texToUnicode — combining marks', () => {
218
+ it('applies \\overline / \\bar / \\hat / \\vec / \\tilde', () => {
219
+ expect(texToUnicode('\\overline{x}')).toBe('x\u0305')
220
+ expect(texToUnicode('\\hat{y}')).toBe('y\u0302')
221
+ expect(texToUnicode('\\vec{v}')).toBe('v\u20D7')
222
+ })
223
+ })
224
+
225
+ describe('texToUnicode — left/right delimiters', () => {
226
+ it('strips \\left and \\right keeping the delimiter character', () => {
227
+ expect(texToUnicode('\\left( x + y \\right)')).toBe('( x + y )')
228
+ expect(texToUnicode('\\left| x \\right|')).toBe('| x |')
229
+ })
230
+
231
+ it('handles escaped delimiters \\left\\{ ... \\right\\}', () => {
232
+ expect(texToUnicode('\\left\\{p/q \\mid q \\neq 0\\right\\}')).toBe('{p/q ∣ q ≠ 0}')
233
+ })
234
+
235
+ it('handles named delimiters via \\left\\langle / \\right\\rangle', () => {
236
+ expect(texToUnicode('\\left\\langle u, v \\right\\rangle')).toBe('⟨ u, v ⟩')
237
+ })
238
+
239
+ it('drops \\left. and \\right. (which are explicit "no delimiter")', () => {
240
+ expect(texToUnicode('\\left. f \\right|')).toBe(' f |')
241
+ })
242
+
243
+ it('preserves \\leftarrow / \\rightarrow (word boundary blocks the strip)', () => {
244
+ expect(texToUnicode('A \\leftarrow B \\rightarrow C')).toBe('A ← B → C')
245
+ })
246
+ })
247
+
248
+ describe('texToUnicode — labelled arrows', () => {
249
+ it('renders \\xrightarrow{label} as ─label→', () => {
250
+ expect(texToUnicode('a \\xrightarrow{x=1} b')).toBe('a ─x=1→ b')
251
+ })
252
+
253
+ it('renders \\xleftarrow{label} as ←label─', () => {
254
+ expect(texToUnicode('a \\xleftarrow{n} b')).toBe('a ←n─ b')
255
+ })
256
+
257
+ it('still applies symbol substitution inside the label', () => {
258
+ expect(texToUnicode('a \\xrightarrow{n \\to \\infty} L')).toBe('a ─n → ∞→ L')
259
+ })
260
+ })
261
+
262
+ describe('texToUnicode — punctuation commands without lookahead', () => {
263
+ it('substitutes \\{ even when immediately followed by a letter', () => {
264
+ // Regression: with a global `(?![A-Za-z])` lookahead, `\{p` refused
265
+ // to substitute (because `p` is a letter) and rendered as `\{p`.
266
+ expect(texToUnicode('\\{p, q\\}')).toBe('{p, q}')
267
+ })
268
+
269
+ it('substitutes thin-space \\, before a letter', () => {
270
+ expect(texToUnicode('a\\,b')).toBe('a b')
271
+ })
272
+ })
273
+
274
+ describe('texToUnicode — round-trip realism', () => {
275
+ it('renders a typical model-emitted formula', () => {
276
+ expect(texToUnicode('\\alpha \\in \\mathbb{R}, \\alpha \\notin \\mathbb{Q}')).toBe('α ∈ ℝ, α ∉ ℚ')
277
+ })
278
+
279
+ it('preserves unknown commands verbatim', () => {
280
+ expect(texToUnicode('\\bigtriangleup \\circledast')).toBe('\\bigtriangleup \\circledast')
281
+ })
282
+
283
+ it('handles commands without delimiters between', () => {
284
+ // Word-boundary lookahead means `\alpha\beta` doesn't accidentally
285
+ // match `\alphabeta` as one ungrouped token.
286
+ expect(texToUnicode('\\alpha\\beta')).toBe('αβ')
287
+ })
288
+
289
+ it('leaves plain text alone', () => {
290
+ expect(texToUnicode('hello world')).toBe('hello world')
291
+ expect(texToUnicode('')).toBe('')
292
+ })
293
+ })