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,144 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { creditsCommands } from '../app/slash/commands/credits.js'
4
+ import { getOverlayState, resetOverlayState } from '../app/overlayStore.js'
5
+ import type { CreditsViewResponse } from '../gatewayTypes.js'
6
+
7
+ // The command opens the top-up URL through this helper on confirm. Mock it so
8
+ // the test never shells out to a real browser/`xdg-open` and we can assert the
9
+ // success/failure messaging deterministically.
10
+ vi.mock('../lib/openExternalUrl.js', () => ({
11
+ openExternalUrl: vi.fn(() => true)
12
+ }))
13
+
14
+ import { openExternalUrl } from '../lib/openExternalUrl.js'
15
+
16
+ const openExternalUrlMock = vi.mocked(openExternalUrl)
17
+
18
+ const creditsCommand = creditsCommands.find(cmd => cmd.name === 'credits')!
19
+
20
+ const buildView = (overrides: Partial<CreditsViewResponse> = {}): CreditsViewResponse => ({
21
+ balance_lines: ['Grant: $9.50 left', 'Top-up: $25.00'],
22
+ depleted: false,
23
+ identity_line: 'Signed in as ada@example.com',
24
+ logged_in: true,
25
+ topup_url: 'https://portal.nastechai.com/billing/topup',
26
+ ...overrides
27
+ })
28
+
29
+ // Mirror createSlashHandler's real `guarded` wrapper: skip the handler when the
30
+ // command is stale OR the response is falsy. Tests stay non-stale, so this is a
31
+ // straightforward "run the handler when we got a response" shim.
32
+ const guarded =
33
+ <T,>(fn: (r: T) => void) =>
34
+ (r: null | T) => {
35
+ if (r) {
36
+ fn(r)
37
+ }
38
+ }
39
+
40
+ const buildCtx = (rpcResult: CreditsViewResponse) => {
41
+ const sys = vi.fn()
42
+ const rpc = vi.fn(() => Promise.resolve(rpcResult))
43
+ const guardedErr = vi.fn()
44
+
45
+ const ctx = {
46
+ gateway: { rpc },
47
+ guarded,
48
+ guardedErr,
49
+ sid: 'sid-abc',
50
+ stale: () => false,
51
+ transcript: { page: vi.fn(), panel: vi.fn(), sys }
52
+ }
53
+
54
+ // Run the command, then await the rpc promise so the .then() handler has
55
+ // flushed before assertions — deterministic, no polling/timeouts.
56
+ const run = async () => {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ creditsCommand.run('', ctx as any, 'credits')
59
+ await rpc.mock.results[0]?.value
60
+ // Allow the chained .then() microtask to settle.
61
+ await Promise.resolve()
62
+ }
63
+
64
+ return { ctx, rpc, run, sys }
65
+ }
66
+
67
+ describe('/credits slash command', () => {
68
+ beforeEach(() => {
69
+ resetOverlayState()
70
+ openExternalUrlMock.mockClear()
71
+ openExternalUrlMock.mockReturnValue(true)
72
+ })
73
+
74
+ it('renders the balance (including top-up URL) and arms the confirm overlay', async () => {
75
+ const view = buildView()
76
+ const { rpc, run, sys } = buildCtx(view)
77
+
78
+ await run()
79
+
80
+ expect(rpc).toHaveBeenCalledWith('credits.view', { session_id: 'sid-abc' })
81
+
82
+ // (a) sys received the balance text including the topup_url
83
+ const printed = sys.mock.calls.map(call => call[0]).join('\n')
84
+ expect(printed).toContain('💳 NasTech credits')
85
+ expect(printed).toContain('Grant: $9.50 left')
86
+ expect(printed).toContain('Signed in as ada@example.com')
87
+ expect(printed).toContain(view.topup_url)
88
+
89
+ // (b) confirm overlay set with the expected label + detail
90
+ const confirm = getOverlayState().confirm
91
+ expect(confirm).toBeTruthy()
92
+ expect(confirm?.confirmLabel).toBe('Open top-up in browser')
93
+ expect(confirm?.cancelLabel).toBe('Cancel')
94
+ expect(confirm?.title).toBe('Add credits?')
95
+ expect(confirm?.detail).toBe(view.topup_url)
96
+
97
+ // onConfirm opens the URL and reports success back to the transcript
98
+ confirm?.onConfirm()
99
+ expect(openExternalUrlMock).toHaveBeenCalledWith(view.topup_url)
100
+ expect(sys).toHaveBeenCalledWith(
101
+ 'Complete your top-up in the browser — credits will appear in /credits shortly.'
102
+ )
103
+ })
104
+
105
+ it('falls back to printing the URL when the browser open is rejected', async () => {
106
+ openExternalUrlMock.mockReturnValue(false)
107
+ const view = buildView()
108
+ const { run, sys } = buildCtx(view)
109
+
110
+ await run()
111
+
112
+ const confirm = getOverlayState().confirm
113
+ expect(confirm).toBeTruthy()
114
+ confirm?.onConfirm()
115
+ expect(sys).toHaveBeenCalledWith(`Open this URL to top up: ${view.topup_url}`)
116
+ })
117
+
118
+ it('does not arm the confirm overlay when there is no top-up URL', async () => {
119
+ const view = buildView({ topup_url: null })
120
+ const { run, sys } = buildCtx(view)
121
+
122
+ await run()
123
+
124
+ const printed = sys.mock.calls.map(call => call[0]).join('\n')
125
+ expect(printed).toContain('💳 NasTech credits')
126
+ expect(getOverlayState().confirm).toBeNull()
127
+ })
128
+
129
+ it('shows the not-logged-in message and does NOT arm the confirm overlay', async () => {
130
+ const view = buildView({
131
+ balance_lines: [],
132
+ identity_line: null,
133
+ logged_in: false,
134
+ topup_url: null
135
+ })
136
+ const { run, sys } = buildCtx(view)
137
+
138
+ await run()
139
+
140
+ expect(sys).toHaveBeenCalledWith('💳 Not logged into NasTech Portal — run /portal to log in.')
141
+ expect(getOverlayState().confirm).toBeNull()
142
+ expect(openExternalUrlMock).not.toHaveBeenCalled()
143
+ })
144
+ })
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Pinned regression for the multi-line composer cursor-drift bug.
3
+ *
4
+ * Symptom: in `nastech --tui`, typing into the composer until the input
5
+ * wraps across multiple visual rows would leave several blank cells
6
+ * between the last typed character and the (hardware) cursor block.
7
+ * Worse on narrow terminals (the Cursor IDE built-in terminal in
8
+ * particular).
9
+ *
10
+ * Root cause: the composer's `cursorLayout` (used by `useDeclaredCursor`
11
+ * to place the hardware cursor) ran a hand-rolled word-wrap algorithm,
12
+ * while Ink's `<Text wrap="wrap">` renders via `wrap-ansi`. The two
13
+ * disagreed on many real inputs — wrap-ansi would keep "branch
14
+ * investigate" on one row while cursorLayout claimed it had wrapped,
15
+ * etc. — so the declared cursor position drifted from where the text
16
+ * was actually rendered. The fix sources cursorLayout's line breaks
17
+ * directly from wrap-ansi, guaranteeing agreement.
18
+ *
19
+ * This test pins the contract: for every char that would be typed into
20
+ * the composer, the cursor position reported by cursorLayout MUST equal
21
+ * the end-of-text position that wrap-ansi would render. Any future
22
+ * regression that lets the two diverge re-introduces the drift.
23
+ */
24
+ import { wrapAnsi } from '@nastechai/ink'
25
+ import { describe, expect, it } from 'vitest'
26
+
27
+ import { cursorLayout, inputVisualHeight } from '../lib/inputMetrics.js'
28
+
29
+ function wrapAnsiEnd(text: string, cols: number): { line: number; column: number } {
30
+ const wrapped = wrapAnsi(text, cols, { hard: true, trim: false })
31
+ const lines = wrapped.split('\n')
32
+ const last = lines[lines.length - 1] ?? ''
33
+
34
+ return { line: lines.length - 1, column: last.length }
35
+ }
36
+
37
+ const USER_REPORT_MESSAGE =
38
+ // Paraphrase of the user's actual bug report, included verbatim so the
39
+ // test is grounded in a realistic typing pattern (long single line,
40
+ // mixed-length words, punctuation, no hard newlines).
41
+ 'im in cursor terminal using nastech --tui and as i type multiline my caret at the end will often ' +
42
+ 'go.. randomly.. like multiple spaces away lol and idk why. theres no rhyme/reason really but ' +
43
+ 'there should literally never be a non-user added space at the end of my composer input right? ' +
44
+ 'i dont think it happens on new sessions but only existing ones. there have been a few prs to ' +
45
+ 'try to fix this and all not working. ok it just happened, to me, nowso attaching screenshot ' +
46
+ 'and you can see its multiline, new session. on a new bb/<xxx> branch investigate'
47
+
48
+ describe('cursor-drift regression — composer cursorLayout matches Ink rendering', () => {
49
+ it('agrees with wrap-ansi at every typing-prefix of the user-reported message', () => {
50
+ // Walks the message char-by-char (mirroring what the TUI sees when a
51
+ // user types). At every prefix, cursorLayout must place the cursor
52
+ // exactly where wrap-ansi would render the end of the text.
53
+ //
54
+ // Pre-fix: this failed on most narrow widths because the hand-rolled
55
+ // wrap algorithm broke at slightly different points than wrap-ansi.
56
+ for (const cols of [40, 50, 55, 60, 65, 70, 80]) {
57
+ let acc = ''
58
+
59
+ for (const ch of USER_REPORT_MESSAGE) {
60
+ acc += ch
61
+ const layout = cursorLayout(acc, acc.length, cols)
62
+ const expected = wrapAnsiEnd(acc, cols)
63
+
64
+ expect(
65
+ layout,
66
+ `mismatch at cols=${cols}, len=${acc.length}, last-char=${JSON.stringify(ch)}, ` +
67
+ `tail=${JSON.stringify(acc.slice(-30))}`
68
+ ).toEqual(expected)
69
+ }
70
+ }
71
+ })
72
+
73
+ it('keeps cursor on the same row when text exactly fills the terminal width', () => {
74
+ // wrap-ansi does NOT push exact-fill text onto a phantom next line.
75
+ // The previous algorithm did — that's what produced the visible
76
+ // "cursor parked one row below the last char" symptom on narrow
77
+ // terminals at certain message lengths.
78
+ for (const cols of [8, 12, 18, 24]) {
79
+ const text = 'a'.repeat(cols)
80
+ const layout = cursorLayout(text, text.length, cols)
81
+ const inkLines = wrapAnsi(text, cols, { hard: true, trim: false }).split('\n')
82
+
83
+ expect(layout.line).toBe(0)
84
+ expect(layout.column).toBe(cols)
85
+ expect(inkLines).toHaveLength(1)
86
+ expect(inputVisualHeight(text, cols)).toBe(1)
87
+ }
88
+ })
89
+
90
+ it('does not stuff a trailing whitespace word onto a phantom line', () => {
91
+ // "branch investigate" at cols=20 fits on one row in wrap-ansi. The
92
+ // bug claimed otherwise, parking the cursor at (line=1, col=?) and
93
+ // leaving the user's "branch investigate" rendered alone on row 0
94
+ // with the cursor block several cells past it.
95
+ const text = 'branch investigate'
96
+ const cols = 20
97
+
98
+ expect(cursorLayout(text, text.length, cols)).toEqual({ column: text.length, line: 0 })
99
+ expect(cursorLayout(text, text.length, cols)).toEqual(wrapAnsiEnd(text, cols))
100
+ })
101
+
102
+ it('agrees with wrap-ansi for word-wrap that pushes a word onto the next line', () => {
103
+ // "hello world" at cols=8 wraps to ["hello ", "world"] in wrap-ansi.
104
+ // The cursor at end-of-text must land at line=1, col=5 — where Ink
105
+ // actually renders the last 'd'. The previous algorithm reported
106
+ // (line=2, col=0) here (phantom extra wrap), which parked the
107
+ // cursor on a row Ink never painted.
108
+ const text = 'hello world'
109
+ const cols = 8
110
+
111
+ expect(cursorLayout(text, text.length, cols)).toEqual({ column: 5, line: 1 })
112
+ expect(cursorLayout(text, text.length, cols)).toEqual(wrapAnsiEnd(text, cols))
113
+ })
114
+ })
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { isSectionName, parseDetailsMode, resolveSections, SECTION_NAMES, sectionMode } from '../domain/details.js'
4
+
5
+ describe('parseDetailsMode', () => {
6
+ it('accepts the canonical modes case-insensitively', () => {
7
+ expect(parseDetailsMode('hidden')).toBe('hidden')
8
+ expect(parseDetailsMode(' COLLAPSED ')).toBe('collapsed')
9
+ expect(parseDetailsMode('Expanded')).toBe('expanded')
10
+ })
11
+
12
+ it('rejects junk', () => {
13
+ expect(parseDetailsMode('truncated')).toBeNull()
14
+ expect(parseDetailsMode('')).toBeNull()
15
+ expect(parseDetailsMode(undefined)).toBeNull()
16
+ expect(parseDetailsMode(42)).toBeNull()
17
+ })
18
+ })
19
+
20
+ describe('isSectionName', () => {
21
+ it('only lets the four canonical sections through', () => {
22
+ expect(isSectionName('thinking')).toBe(true)
23
+ expect(isSectionName('tools')).toBe(true)
24
+ expect(isSectionName('subagents')).toBe(true)
25
+ expect(isSectionName('activity')).toBe(true)
26
+
27
+ expect(isSectionName('Thinking')).toBe(false) // case-sensitive on purpose
28
+ expect(isSectionName('bogus')).toBe(false)
29
+ expect(isSectionName('')).toBe(false)
30
+ expect(isSectionName(7)).toBe(false)
31
+ })
32
+
33
+ it('SECTION_NAMES exposes them all', () => {
34
+ expect([...SECTION_NAMES].sort()).toEqual(['activity', 'subagents', 'thinking', 'tools'])
35
+ })
36
+ })
37
+
38
+ describe('resolveSections', () => {
39
+ it('parses a well-formed sections object', () => {
40
+ expect(
41
+ resolveSections({
42
+ thinking: 'expanded',
43
+ tools: 'expanded',
44
+ subagents: 'collapsed',
45
+ activity: 'hidden'
46
+ })
47
+ ).toEqual({
48
+ thinking: 'expanded',
49
+ tools: 'expanded',
50
+ subagents: 'collapsed',
51
+ activity: 'hidden'
52
+ })
53
+ })
54
+
55
+ it('drops unknown section names and unknown modes', () => {
56
+ expect(
57
+ resolveSections({
58
+ thinking: 'expanded',
59
+ tools: 'maximised',
60
+ bogus: 'hidden',
61
+ activity: 'hidden'
62
+ })
63
+ ).toEqual({ thinking: 'expanded', activity: 'hidden' })
64
+ })
65
+
66
+ it('treats nullish/non-objects as empty overrides', () => {
67
+ expect(resolveSections(undefined)).toEqual({})
68
+ expect(resolveSections(null)).toEqual({})
69
+ expect(resolveSections('hidden')).toEqual({})
70
+ expect(resolveSections([])).toEqual({})
71
+ })
72
+ })
73
+
74
+ describe('sectionMode', () => {
75
+ it('falls back to the global mode for sections without a built-in default', () => {
76
+ expect(sectionMode('subagents', 'collapsed', {})).toBe('collapsed')
77
+ expect(sectionMode('subagents', 'expanded', undefined)).toBe('expanded')
78
+ expect(sectionMode('subagents', 'hidden', {})).toBe('hidden')
79
+ })
80
+
81
+ it('streams thinking + tools expanded by default for persisted config values', () => {
82
+ expect(sectionMode('thinking', 'collapsed', {})).toBe('expanded')
83
+ expect(sectionMode('thinking', 'hidden', undefined)).toBe('expanded')
84
+ expect(sectionMode('tools', 'collapsed', {})).toBe('expanded')
85
+ expect(sectionMode('tools', 'hidden', undefined)).toBe('expanded')
86
+ })
87
+
88
+ it('hides the activity panel by default for persisted config values', () => {
89
+ expect(sectionMode('activity', 'collapsed', {})).toBe('hidden')
90
+ expect(sectionMode('activity', 'expanded', undefined)).toBe('hidden')
91
+ expect(sectionMode('activity', 'hidden', {})).toBe('hidden')
92
+ })
93
+
94
+ it('applies in-session /details mode globally over built-in defaults', () => {
95
+ expect(sectionMode('thinking', 'collapsed', {}, true)).toBe('collapsed')
96
+ expect(sectionMode('tools', 'hidden', {}, true)).toBe('hidden')
97
+ expect(sectionMode('activity', 'expanded', undefined, true)).toBe('expanded')
98
+ })
99
+
100
+ it('honours per-section overrides over both the section default and global mode', () => {
101
+ expect(sectionMode('thinking', 'collapsed', { thinking: 'collapsed' })).toBe('collapsed')
102
+ expect(sectionMode('tools', 'collapsed', { tools: 'hidden' })).toBe('hidden')
103
+ expect(sectionMode('activity', 'collapsed', { activity: 'expanded' })).toBe('expanded')
104
+ expect(sectionMode('activity', 'expanded', { activity: 'collapsed' })).toBe('collapsed')
105
+ })
106
+
107
+ it('lets per-section overrides escape the global hidden mode', () => {
108
+ // Regression for the case where global details_mode: hidden used to
109
+ // short-circuit the entire accordion and prevent overrides from
110
+ // surfacing — `sections.tools: expanded` must still resolve to expanded.
111
+ expect(sectionMode('subagents', 'hidden', { subagents: 'expanded' })).toBe('expanded')
112
+ expect(sectionMode('thinking', 'hidden', { thinking: 'collapsed' })).toBe('collapsed')
113
+ expect(sectionMode('activity', 'hidden', { activity: 'expanded' })).toBe('expanded')
114
+ })
115
+ })
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { ensureEmojiPresentation } from '../lib/emoji.js'
4
+
5
+ const VS16 = '\uFE0F'
6
+
7
+ describe('ensureEmojiPresentation', () => {
8
+ it('passes through ASCII unchanged', () => {
9
+ expect(ensureEmojiPresentation('hello world')).toBe('hello world')
10
+ expect(ensureEmojiPresentation('')).toBe('')
11
+ })
12
+
13
+ it('passes through emoji that already defaults to emoji presentation', () => {
14
+ expect(ensureEmojiPresentation('🚀 rocket')).toBe('🚀 rocket')
15
+ expect(ensureEmojiPresentation('😀')).toBe('😀')
16
+ })
17
+
18
+ it('injects VS16 after text-default emoji codepoints', () => {
19
+ expect(ensureEmojiPresentation('⚠ careful')).toBe(`⚠${VS16} careful`)
20
+ expect(ensureEmojiPresentation('ℹ info')).toBe(`ℹ${VS16} info`)
21
+ expect(ensureEmojiPresentation('love ❤ you')).toBe(`love ❤${VS16} you`)
22
+ expect(ensureEmojiPresentation('✔ done')).toBe(`✔${VS16} done`)
23
+ })
24
+
25
+ it('is idempotent when VS16 is already present', () => {
26
+ const already = `⚠${VS16} ℹ${VS16} ❤${VS16}`
27
+
28
+ expect(ensureEmojiPresentation(already)).toBe(already)
29
+ expect(ensureEmojiPresentation(ensureEmojiPresentation('⚠'))).toBe(`⚠${VS16}`)
30
+ })
31
+
32
+ it('leaves keycap sequences alone when the base is not a text-default emoji', () => {
33
+ expect(ensureEmojiPresentation('1\u20e3')).toBe('1\u20e3')
34
+ })
35
+
36
+ it('injects VS16 before ZWJ so text-default bases participate in emoji sequences', () => {
37
+ // ❤ + ZWJ + 🔥 → ❤️‍🔥 (heart on fire). Without VS16 between the heart
38
+ // and the ZWJ, terminals render the heart in text/monochrome form and
39
+ // the ZWJ ligature can fail to form.
40
+ const heartFire = '\u2764\u200d\ud83d\udd25'
41
+
42
+ expect(ensureEmojiPresentation(heartFire)).toBe(`\u2764\uFE0F\u200d\ud83d\udd25`)
43
+ })
44
+
45
+ it('leaves explicit text-presentation selector (VS15) alone', () => {
46
+ // `❤︎` (U+2764 + U+FE0E) asks for text presentation — injecting VS16
47
+ // would create an invalid double-variation sequence.
48
+ const explicitText = '\u2764\ufe0e'
49
+
50
+ expect(ensureEmojiPresentation(explicitText)).toBe(explicitText)
51
+ })
52
+
53
+ it('returns the original reference when no change is needed', () => {
54
+ const already = `⚠${VS16} ℹ${VS16} ❤${VS16}`
55
+
56
+ // Reference equality — the lazy allocator should short-circuit to the
57
+ // input when nothing needed injection.
58
+ expect(ensureEmojiPresentation(already)).toBe(already)
59
+ })
60
+
61
+ it('handles mixed content', () => {
62
+ expect(ensureEmojiPresentation('⚠ path: /tmp/x ❤ done')).toBe(`⚠${VS16} path: /tmp/x ❤${VS16} done`)
63
+ })
64
+ })
@@ -0,0 +1,144 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import {
4
+ __resetLinkTitleCache,
5
+ fetchLinkTitle,
6
+ hostPathLabel,
7
+ isTitleFetchable,
8
+ normalizeExternalUrl,
9
+ urlSlugTitleLabel
10
+ } from '../lib/externalLink.js'
11
+
12
+ afterEach(() => {
13
+ __resetLinkTitleCache()
14
+ vi.restoreAllMocks()
15
+ vi.unstubAllGlobals()
16
+ })
17
+
18
+ describe('external link helpers', () => {
19
+ it('formats URL fallbacks as host + path', () => {
20
+ expect(
21
+ hostPathLabel(
22
+ 'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
23
+ )
24
+ ).toBe('getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894')
25
+ })
26
+
27
+ it('derives readable title fallbacks from URL slugs', () => {
28
+ expect(
29
+ urlSlugTitleLabel('https://www.getyourguide.com/fajardo-l882/from-fajardo-icacos-island-full-day-catamaran-trip-t19891/')
30
+ ).toBe('From Fajardo Icacos Island Full Day Catamaran Trip')
31
+ })
32
+
33
+ it('keeps x.com status fallbacks link-like instead of generic Status labels', () => {
34
+ expect(urlSlugTitleLabel('https://x.com/grok/status/2056065022749479209')).toBe(
35
+ 'x.com/grok/status/2056065022749479209'
36
+ )
37
+ })
38
+
39
+ it('normalizes scheme-less links', () => {
40
+ expect(normalizeExternalUrl(' expedia.com/things-to-do/puerto-rico-el-yunque ')).toBe(
41
+ 'https://expedia.com/things-to-do/puerto-rico-el-yunque'
42
+ )
43
+ })
44
+
45
+ it('filters out local/non-http targets for title fetches', () => {
46
+ expect(isTitleFetchable('https://www.expedia.com/things-to-do/foo')).toBe(true)
47
+ expect(isTitleFetchable('http://localhost:5174')).toBe(false)
48
+ expect(isTitleFetchable('file:///tmp/demo.html')).toBe(false)
49
+ expect(isTitleFetchable('mailto:hello@example.com')).toBe(false)
50
+ })
51
+
52
+ it('blocks private, link-local, and intranet hosts', () => {
53
+ expect(isTitleFetchable('http://10.0.0.12/path')).toBe(false)
54
+ expect(isTitleFetchable('http://172.22.5.4/path')).toBe(false)
55
+ expect(isTitleFetchable('http://192.168.1.22/path')).toBe(false)
56
+ expect(isTitleFetchable('http://169.254.169.254/latest/meta-data')).toBe(false)
57
+ expect(isTitleFetchable('http://[fd00::1]/')).toBe(false)
58
+ expect(isTitleFetchable('http://[fe80::1]/')).toBe(false)
59
+ expect(isTitleFetchable('http://printer.local/status')).toBe(false)
60
+ expect(isTitleFetchable('http://intranet/status')).toBe(false)
61
+ expect(isTitleFetchable('https://8.8.8.8/status')).toBe(true)
62
+ })
63
+
64
+ it('deduplicates in-flight title fetches and caches results', async () => {
65
+ const fetchMock = vi.fn().mockResolvedValue(
66
+ new Response('<html><head><title>El Yunque Tour Water Slide, Rope Swing & Pickup</title></head></html>', {
67
+ headers: { 'content-type': 'text/html; charset=utf-8' },
68
+ status: 200
69
+ })
70
+ )
71
+
72
+ vi.stubGlobal('fetch', fetchMock)
73
+
74
+ const url = 'https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure.a46272756.activity-details'
75
+ const [first, second] = await Promise.all([fetchLinkTitle(url), fetchLinkTitle(url)])
76
+
77
+ expect(first).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
78
+ expect(second).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
79
+ expect(fetchMock).toHaveBeenCalledTimes(1)
80
+
81
+ const third = await fetchLinkTitle(url)
82
+
83
+ expect(third).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
84
+ expect(fetchMock).toHaveBeenCalledTimes(1)
85
+ })
86
+
87
+ it('shares cache across protocol/www URL variants', async () => {
88
+ const fetchMock = vi.fn().mockResolvedValue(
89
+ new Response('<html><head><title>Shared Canonical Title</title></head></html>', {
90
+ headers: { 'content-type': 'text/html' },
91
+ status: 200
92
+ })
93
+ )
94
+
95
+ vi.stubGlobal('fetch', fetchMock)
96
+
97
+ const first = 'https://www.getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
98
+ const second = 'http://getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
99
+
100
+ const [a, b] = await Promise.all([fetchLinkTitle(first), fetchLinkTitle(second)])
101
+
102
+ expect(a).toBe('Shared Canonical Title')
103
+ expect(b).toBe('Shared Canonical Title')
104
+ expect(fetchMock).toHaveBeenCalledTimes(1)
105
+ })
106
+
107
+ it('ignores error-like fetched titles', async () => {
108
+ const fetchMock = vi.fn().mockResolvedValue(
109
+ new Response('<html><head><title>Just a moment...</title></head></html>', {
110
+ headers: { 'content-type': 'text/html' },
111
+ status: 200
112
+ })
113
+ )
114
+
115
+ vi.stubGlobal('fetch', fetchMock)
116
+
117
+ const url = 'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
118
+
119
+ await expect(fetchLinkTitle(url)).resolves.toBe('')
120
+ })
121
+
122
+ it('decodes HTML entities in fetched titles', async () => {
123
+ const fetchMock = vi.fn().mockResolvedValue(
124
+ new Response('<html><head><title>AT&amp;T &#39;Deals&#39;</title></head></html>', {
125
+ headers: { 'content-type': 'text/html' },
126
+ status: 200
127
+ })
128
+ )
129
+
130
+ vi.stubGlobal('fetch', fetchMock)
131
+
132
+ await expect(fetchLinkTitle('https://example.com/offers')).resolves.toBe("AT&T 'Deals'")
133
+ })
134
+
135
+ it('skips network fetch for non-fetchable targets', async () => {
136
+ const fetchMock = vi.fn()
137
+ vi.stubGlobal('fetch', fetchMock)
138
+
139
+ await expect(fetchLinkTitle('http://localhost:3000/path')).resolves.toBe('')
140
+ await expect(fetchLinkTitle('mailto:hello@example.com')).resolves.toBe('')
141
+ await expect(fetchLinkTitle('file:///tmp/demo.html')).resolves.toBe('')
142
+ expect(fetchMock).not.toHaveBeenCalled()
143
+ })
144
+ })