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,556 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const originalPlatform = process.platform
4
+
5
+ async function importPlatform(platform: NodeJS.Platform) {
6
+ vi.resetModules()
7
+ Object.defineProperty(process, 'platform', { value: platform })
8
+
9
+ return import('../lib/platform.js')
10
+ }
11
+
12
+ afterEach(() => {
13
+ Object.defineProperty(process, 'platform', { value: originalPlatform })
14
+ vi.resetModules()
15
+ })
16
+
17
+ describe('platform action modifier', () => {
18
+ it('treats kitty Cmd sequences as the macOS action modifier', async () => {
19
+ const { isActionMod } = await importPlatform('darwin')
20
+
21
+ expect(isActionMod({ ctrl: false, meta: false, super: true })).toBe(true)
22
+ expect(isActionMod({ ctrl: false, meta: true, super: false })).toBe(true)
23
+ expect(isActionMod({ ctrl: true, meta: false, super: false })).toBe(false)
24
+ })
25
+
26
+ it('still uses Ctrl as the action modifier on non-macOS', async () => {
27
+ const { isActionMod } = await importPlatform('linux')
28
+
29
+ expect(isActionMod({ ctrl: true, meta: false, super: false })).toBe(true)
30
+ expect(isActionMod({ ctrl: false, meta: false, super: true })).toBe(false)
31
+ })
32
+ })
33
+
34
+ describe('isCopyShortcut', () => {
35
+ it('keeps Ctrl+C as the local non-macOS copy chord', async () => {
36
+ const { isCopyShortcut } = await importPlatform('linux')
37
+
38
+ expect(isCopyShortcut({ ctrl: true, meta: false, super: false }, 'c', {})).toBe(true)
39
+ })
40
+
41
+ it('accepts client Cmd+C over SSH even when running on Linux', async () => {
42
+ const { isCopyShortcut } = await importPlatform('linux')
43
+ const env = { SSH_CONNECTION: '1 2 3 4' } as NodeJS.ProcessEnv
44
+
45
+ expect(isCopyShortcut({ ctrl: false, meta: false, super: true }, 'c', env)).toBe(true)
46
+ expect(isCopyShortcut({ ctrl: false, meta: true, super: false }, 'c', env)).toBe(true)
47
+ })
48
+
49
+ it('does not treat local Linux Alt+C as copy', async () => {
50
+ const { isCopyShortcut } = await importPlatform('linux')
51
+
52
+ expect(isCopyShortcut({ ctrl: false, meta: true, super: false }, 'c', {})).toBe(false)
53
+ })
54
+
55
+ it('accepts the VS Code/Cursor forwarded Cmd+C copy sequence on macOS', async () => {
56
+ const { isCopyShortcut } = await importPlatform('darwin')
57
+
58
+ expect(isCopyShortcut({ ctrl: true, meta: false, super: true }, 'c', {})).toBe(true)
59
+ })
60
+ })
61
+
62
+ describe('isVoiceToggleKey', () => {
63
+ it('matches raw Ctrl+B on macOS (doc-default across platforms)', async () => {
64
+ const { isVoiceToggleKey } = await importPlatform('darwin')
65
+
66
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b')).toBe(true)
67
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'B')).toBe(true)
68
+ })
69
+
70
+ it('matches kitty-style Cmd+B on macOS via key.super', async () => {
71
+ const { isVoiceToggleKey } = await importPlatform('darwin')
72
+
73
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b')).toBe(true)
74
+ // ``key.meta`` is NOT accepted as Cmd — nastech-ink uses meta for
75
+ // Alt too, so accepting it leaked Alt+B into the default binding
76
+ // (Copilot round-6 review on #19835). Legacy-terminal mac users
77
+ // get strict Ctrl+B.
78
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b')).toBe(false)
79
+ })
80
+
81
+ it('matches Ctrl+B on non-macOS platforms', async () => {
82
+ const { isVoiceToggleKey } = await importPlatform('linux')
83
+
84
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b')).toBe(true)
85
+ })
86
+
87
+ it('does not match unmodified b or other Ctrl combos', async () => {
88
+ const { isVoiceToggleKey } = await importPlatform('darwin')
89
+
90
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: false }, 'b')).toBe(false)
91
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'a')).toBe(false)
92
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'c')).toBe(false)
93
+ })
94
+ })
95
+
96
+ describe('parseVoiceRecordKey (#18994)', () => {
97
+ it('falls back to Ctrl+B for empty input', async () => {
98
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('linux')
99
+
100
+ expect(parseVoiceRecordKey('')).toEqual(DEFAULT_VOICE_RECORD_KEY)
101
+ })
102
+
103
+ it('parses ctrl+<letter> bindings', async () => {
104
+ const { parseVoiceRecordKey } = await importPlatform('linux')
105
+
106
+ expect(parseVoiceRecordKey('ctrl+o')).toEqual({ ch: 'o', mod: 'ctrl', raw: 'ctrl+o' })
107
+ expect(parseVoiceRecordKey('Ctrl+R')).toEqual({ ch: 'r', mod: 'ctrl', raw: 'ctrl+r' })
108
+ })
109
+
110
+ it('parses alt/super aliases', async () => {
111
+ const { parseVoiceRecordKey } = await importPlatform('linux')
112
+
113
+ expect(parseVoiceRecordKey('alt+b').mod).toBe('alt')
114
+ expect(parseVoiceRecordKey('option+b').mod).toBe('alt')
115
+ expect(parseVoiceRecordKey('super+b').mod).toBe('super')
116
+ expect(parseVoiceRecordKey('win+b').mod).toBe('super')
117
+ })
118
+
119
+ it('treats ambiguous mac modifiers (meta / cmd / command) as unrecognised', async () => {
120
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('linux')
121
+
122
+ // ``meta`` / ``cmd`` / ``command`` are ambiguous on the wire:
123
+ // nastech-ink sets ``key.meta`` for plain Alt on every platform AND
124
+ // for Cmd on legacy macOS terminals. Accepting any of them would
125
+ // produce a display/binding mismatch (Copilot round-6 review on
126
+ // #19835). Users on modern kitty-style terminals spell the
127
+ // platform action modifier ``super`` / ``win``.
128
+ expect(parseVoiceRecordKey('meta+b')).toEqual(DEFAULT_VOICE_RECORD_KEY)
129
+ expect(parseVoiceRecordKey('cmd+b')).toEqual(DEFAULT_VOICE_RECORD_KEY)
130
+ expect(parseVoiceRecordKey('command+b')).toEqual(DEFAULT_VOICE_RECORD_KEY)
131
+ })
132
+
133
+ it('parses named keys (space, enter, tab, escape, backspace, delete)', async () => {
134
+ const { parseVoiceRecordKey } = await importPlatform('linux')
135
+
136
+ // Every named token from the CLI's prompt_toolkit ``c-<name>`` set is
137
+ // accepted with both the canonical name and its common alias.
138
+ expect(parseVoiceRecordKey('ctrl+space')).toEqual({
139
+ ch: 'space',
140
+ mod: 'ctrl',
141
+ named: 'space',
142
+ raw: 'ctrl+space'
143
+ })
144
+ expect(parseVoiceRecordKey('alt+enter').named).toBe('enter')
145
+ expect(parseVoiceRecordKey('alt+return').named).toBe('enter') // ``return`` ↔ ``enter``
146
+ expect(parseVoiceRecordKey('ctrl+tab').named).toBe('tab')
147
+ expect(parseVoiceRecordKey('ctrl+escape').named).toBe('escape')
148
+ expect(parseVoiceRecordKey('ctrl+esc').named).toBe('escape') // ``esc`` alias
149
+ expect(parseVoiceRecordKey('ctrl+backspace').named).toBe('backspace')
150
+ expect(parseVoiceRecordKey('ctrl+delete').named).toBe('delete')
151
+ expect(parseVoiceRecordKey('ctrl+del').named).toBe('delete') // ``del`` alias
152
+ })
153
+
154
+ it('falls back to Ctrl+B for unrecognised multi-character tokens', async () => {
155
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('linux')
156
+
157
+ // Typos / unsupported names (``ctrl+spcae``, ``ctrl+f5``, …) fall back
158
+ // to the documented Ctrl+B default rather than silently disabling the
159
+ // binding.
160
+ expect(parseVoiceRecordKey('ctrl+spcae')).toEqual(DEFAULT_VOICE_RECORD_KEY)
161
+ expect(parseVoiceRecordKey('ctrl+f5')).toEqual(DEFAULT_VOICE_RECORD_KEY)
162
+ })
163
+
164
+ // Round-3 Copilot review regressions on #19835.
165
+ it('does not throw on non-string YAML scalars — falls back instead', async () => {
166
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('linux')
167
+
168
+ // ``config.get full`` surfaces raw YAML values; ``voice.record_key: 1``
169
+ // or ``voice.record_key: true`` would otherwise crash ``.trim()``.
170
+ expect(parseVoiceRecordKey(1 as unknown as string)).toEqual(DEFAULT_VOICE_RECORD_KEY)
171
+ expect(parseVoiceRecordKey(true as unknown as string)).toEqual(DEFAULT_VOICE_RECORD_KEY)
172
+ expect(parseVoiceRecordKey(null as unknown as string)).toEqual(DEFAULT_VOICE_RECORD_KEY)
173
+ expect(parseVoiceRecordKey(undefined as unknown as string)).toEqual(DEFAULT_VOICE_RECORD_KEY)
174
+ expect(parseVoiceRecordKey({} as unknown as string)).toEqual(DEFAULT_VOICE_RECORD_KEY)
175
+ })
176
+
177
+ it('rejects multi-modifier chords rather than silently dropping extras', async () => {
178
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('linux')
179
+
180
+ // Previously ``ctrl+alt+r`` parsed as ``ctrl+r`` and ``cmd+ctrl+b`` as
181
+ // ``super+b`` — a typo silently bound a different shortcut. Now a
182
+ // multi-modifier spelling falls back to the documented default.
183
+ expect(parseVoiceRecordKey('ctrl+alt+r')).toEqual(DEFAULT_VOICE_RECORD_KEY)
184
+ expect(parseVoiceRecordKey('cmd+ctrl+b')).toEqual(DEFAULT_VOICE_RECORD_KEY)
185
+ expect(parseVoiceRecordKey('alt+ctrl+space')).toEqual(DEFAULT_VOICE_RECORD_KEY)
186
+ })
187
+
188
+ // Round-4 Copilot review regressions on #19835.
189
+ it('rejects bare-char configs without an explicit modifier', async () => {
190
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('linux')
191
+
192
+ // The classic CLI's prompt_toolkit binds raw-char configs to the key
193
+ // itself (``c-o`` requires an explicit modifier); rewriting ``o``
194
+ // → ``ctrl+o`` would silently diverge the two runtimes. Refuse.
195
+ expect(parseVoiceRecordKey('o')).toEqual(DEFAULT_VOICE_RECORD_KEY)
196
+ expect(parseVoiceRecordKey('b')).toEqual(DEFAULT_VOICE_RECORD_KEY)
197
+ expect(parseVoiceRecordKey('space')).toEqual(DEFAULT_VOICE_RECORD_KEY)
198
+ expect(parseVoiceRecordKey('escape')).toEqual(DEFAULT_VOICE_RECORD_KEY)
199
+ })
200
+
201
+ it('rejects ctrl+c / ctrl+d / ctrl+l — reserved by the TUI input handler', async () => {
202
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('linux')
203
+
204
+ // ``useInputHandlers()`` intercepts these before the voice check,
205
+ // so a binding like ``ctrl+c`` would be advertised but never fire.
206
+ // Fall back to the documented default instead of lying to the user.
207
+ expect(parseVoiceRecordKey('ctrl+c')).toEqual(DEFAULT_VOICE_RECORD_KEY)
208
+ expect(parseVoiceRecordKey('ctrl+d')).toEqual(DEFAULT_VOICE_RECORD_KEY)
209
+ expect(parseVoiceRecordKey('ctrl+l')).toEqual(DEFAULT_VOICE_RECORD_KEY)
210
+ // Alt-modifier versions of those letters are NOT intercepted, so
211
+ // they remain usable.
212
+ expect(parseVoiceRecordKey('alt+c').mod).toBe('alt')
213
+ // ``ctrl+x`` is intentionally allowed — only intercepted during
214
+ // queue-edit (``queueEditIdx !== null``), so the voice binding
215
+ // works for most of the session (Copilot round-8 review).
216
+ expect(parseVoiceRecordKey('ctrl+x').mod).toBe('ctrl')
217
+ expect(parseVoiceRecordKey('ctrl+x').ch).toBe('x')
218
+ })
219
+
220
+ it('rejects super+{c,d,l,v} on macOS — action-mod chords are claimed before voice', async () => {
221
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('darwin')
222
+
223
+ // On macOS super+c/d/l/v are copy / exit / clear / paste. Reject at
224
+ // parse time so /voice status doesn't advertise dead bindings.
225
+ expect(parseVoiceRecordKey('super+c')).toEqual(DEFAULT_VOICE_RECORD_KEY)
226
+ expect(parseVoiceRecordKey('super+d')).toEqual(DEFAULT_VOICE_RECORD_KEY)
227
+ expect(parseVoiceRecordKey('super+l')).toEqual(DEFAULT_VOICE_RECORD_KEY)
228
+ expect(parseVoiceRecordKey('super+v')).toEqual(DEFAULT_VOICE_RECORD_KEY)
229
+ // Other super letters still work (no global chord claims them).
230
+ expect(parseVoiceRecordKey('super+b').mod).toBe('super')
231
+ expect(parseVoiceRecordKey('super+o').mod).toBe('super')
232
+ })
233
+
234
+ it('allows super+{c,d,l,v} on Linux/Windows — those globals key off Ctrl, not Super', async () => {
235
+ const { parseVoiceRecordKey } = await importPlatform('linux')
236
+
237
+ // Kitty/CSI-u users on non-mac report Cmd/Super as ``key.super``,
238
+ // but the TUI's global shortcuts (copy/exit/clear/paste) key off
239
+ // Ctrl there, so ``super+<letter>`` doesn't collide. Reject would
240
+ // silently coerce valid configs to Ctrl+B (Copilot round-8 review).
241
+ expect(parseVoiceRecordKey('super+c').mod).toBe('super')
242
+ expect(parseVoiceRecordKey('super+d').mod).toBe('super')
243
+ expect(parseVoiceRecordKey('super+l').mod).toBe('super')
244
+ expect(parseVoiceRecordKey('super+v').mod).toBe('super')
245
+ })
246
+
247
+ it('rejects alt+{c,d,l} on macOS — meta-as-alt collides with isAction', async () => {
248
+ const { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } = await importPlatform('darwin')
249
+
250
+ // nastech-ink reports Alt as ``key.meta`` on many terminals, and
251
+ // ``isActionMod`` on darwin accepts ``key.meta`` as the action
252
+ // modifier. So ``alt+c`` / ``alt+d`` / ``alt+l`` get claimed by
253
+ // isCopyShortcut / isAction('d') / isAction('l') before voice
254
+ // runs (Copilot round-12 on #19835).
255
+ expect(parseVoiceRecordKey('alt+c')).toEqual(DEFAULT_VOICE_RECORD_KEY)
256
+ expect(parseVoiceRecordKey('alt+d')).toEqual(DEFAULT_VOICE_RECORD_KEY)
257
+ expect(parseVoiceRecordKey('alt+l')).toEqual(DEFAULT_VOICE_RECORD_KEY)
258
+ // Other alt letters stay usable on darwin.
259
+ expect(parseVoiceRecordKey('alt+r').mod).toBe('alt')
260
+ expect(parseVoiceRecordKey('alt+space').mod).toBe('alt')
261
+ })
262
+
263
+ it('allows alt+{c,d,l} on Linux/Windows — non-mac isAction keys off Ctrl', async () => {
264
+ const { parseVoiceRecordKey } = await importPlatform('linux')
265
+
266
+ // On Linux/Windows ``isActionMod`` ignores key.meta, so alt+<letter>
267
+ // doesn't collide with copy/exit/clear. Those configs stay usable.
268
+ expect(parseVoiceRecordKey('alt+c').mod).toBe('alt')
269
+ expect(parseVoiceRecordKey('alt+d').mod).toBe('alt')
270
+ expect(parseVoiceRecordKey('alt+l').mod).toBe('alt')
271
+ })
272
+
273
+ // Round-5 Copilot review regressions on #19835.
274
+ it('super+<key> does NOT fire on key.meta-only events (Alt+X false-fire guard)', async () => {
275
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('darwin')
276
+
277
+ // nastech-ink sets ``key.meta`` for Alt/Option AND for bare Esc on
278
+ // some macOS terminals. The super branch used to accept
279
+ // ``isMac && key.meta`` as a Cmd fallback, which made super+<key>
280
+ // bindings silently fire on Alt+<key> / bare Esc.
281
+ const superB = parseVoiceRecordKey('super+b')
282
+ const superSpace = parseVoiceRecordKey('super+space')
283
+ const superEscape = parseVoiceRecordKey('super+escape')
284
+
285
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b', superB)).toBe(false)
286
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, ' ', superSpace)).toBe(false)
287
+ expect(isVoiceToggleKey({ ctrl: false, escape: true, meta: true, super: false }, '', superEscape)).toBe(false)
288
+ })
289
+
290
+ // Round-6 Copilot review regressions on #19835.
291
+ it('default ctrl+b does NOT fire on Alt+B via isActionMod meta leak', async () => {
292
+ const { DEFAULT_VOICE_RECORD_KEY, isVoiceToggleKey } = await importPlatform('darwin')
293
+
294
+ // ``isActionMod(key)`` on darwin was accepting ``key.meta`` as the
295
+ // action modifier, so Alt+B (key.meta=true) fired the default
296
+ // ctrl+b binding. Now the Cmd-fallback path requires literal
297
+ // ``key.super`` on macOS and rejects ``key.meta``.
298
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b', DEFAULT_VOICE_RECORD_KEY)).toBe(false)
299
+ // Literal Ctrl+B and Cmd+B (kitty-style) still work on darwin.
300
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b', DEFAULT_VOICE_RECORD_KEY)).toBe(true)
301
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b', DEFAULT_VOICE_RECORD_KEY)).toBe(true)
302
+ })
303
+
304
+ it('ctrl+<key> rejects chords with extra alt / meta / super bits', async () => {
305
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('linux')
306
+ const ctrlO = parseVoiceRecordKey('ctrl+o')
307
+
308
+ // ``ctrl+o`` must fire ONLY on literal Ctrl+O, not on
309
+ // Ctrl+Alt+O / Ctrl+Cmd+O / Ctrl+Meta+O — otherwise the runtime
310
+ // matches a different chord than the parser would let you
311
+ // configure.
312
+ expect(isVoiceToggleKey({ alt: true, ctrl: true, meta: false, super: false }, 'o', ctrlO)).toBe(false)
313
+ expect(isVoiceToggleKey({ ctrl: true, meta: true, super: false }, 'o', ctrlO)).toBe(false)
314
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: true }, 'o', ctrlO)).toBe(false)
315
+ // Sanity: plain Ctrl+O still fires.
316
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'o', ctrlO)).toBe(true)
317
+ })
318
+
319
+ it('super+<key> rejects chords with extra ctrl / alt / meta bits', async () => {
320
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('linux')
321
+ const superB = parseVoiceRecordKey('super+b')
322
+
323
+ expect(isVoiceToggleKey({ alt: true, ctrl: false, meta: false, super: true }, 'b', superB)).toBe(false)
324
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: true }, 'b', superB)).toBe(false)
325
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: true }, 'b', superB)).toBe(false)
326
+ // Sanity: plain Super+B still fires.
327
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b', superB)).toBe(true)
328
+ })
329
+
330
+ it('alt+escape does not fire on bare Esc meta-shape', async () => {
331
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('darwin')
332
+ const altEscape = parseVoiceRecordKey('alt+escape')
333
+
334
+ // Some terminals surface bare Esc as meta=true + escape=true.
335
+ expect(isVoiceToggleKey({ ctrl: false, escape: true, meta: true, super: false }, '', altEscape)).toBe(false)
336
+ // Explicit alt bit (kitty-style) still fires the configured chord.
337
+ expect(isVoiceToggleKey({ alt: true, ctrl: false, escape: true, meta: false, super: false }, '', altEscape)).toBe(true)
338
+ })
339
+
340
+ it('rejects matches when Shift is held (different chord than configured)', async () => {
341
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('linux')
342
+
343
+ // Parser rejects multi-modifier configs like ``ctrl+shift+tab``,
344
+ // so the runtime matcher must also reject Shift-held events —
345
+ // otherwise ``ctrl+tab`` would fire on Ctrl+Shift+Tab.
346
+ const ctrlTab = parseVoiceRecordKey('ctrl+tab')
347
+ const altEnter = parseVoiceRecordKey('alt+enter')
348
+ const ctrlO = parseVoiceRecordKey('ctrl+o')
349
+
350
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, shift: true, super: false, tab: true }, '', ctrlTab)).toBe(false)
351
+ expect(isVoiceToggleKey({ alt: true, ctrl: false, meta: false, return: true, shift: true, super: false }, '', altEnter)).toBe(false)
352
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, shift: true, super: false }, 'o', ctrlO)).toBe(false)
353
+
354
+ // Sanity: same events without Shift still fire.
355
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, shift: false, super: false, tab: true }, '', ctrlTab)).toBe(true)
356
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, shift: false, super: false }, 'o', ctrlO)).toBe(true)
357
+ })
358
+ })
359
+
360
+ describe('formatVoiceRecordKey (#18994)', () => {
361
+ it('renders as the user expects in /voice status', async () => {
362
+ const { formatVoiceRecordKey, parseVoiceRecordKey } = await importPlatform('linux')
363
+
364
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('ctrl+b'))).toBe('Ctrl+B')
365
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('ctrl+o'))).toBe('Ctrl+O')
366
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('alt+r'))).toBe('Alt+R')
367
+ // ``super``/``win`` render as ``Super`` on non-mac so the hint
368
+ // doesn't tell Linux/Windows users to press a Cmd key they don't
369
+ // have.
370
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('super+b'))).toBe('Super+B')
371
+ })
372
+
373
+ it('renders named keys in title case (Ctrl+Space, Ctrl+Enter)', async () => {
374
+ const { formatVoiceRecordKey, parseVoiceRecordKey } = await importPlatform('linux')
375
+
376
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('ctrl+space'))).toBe('Ctrl+Space')
377
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('alt+enter'))).toBe('Alt+Enter')
378
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('ctrl+esc'))).toBe('Ctrl+Escape')
379
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('super+space'))).toBe('Super+Space')
380
+ })
381
+ })
382
+
383
+ describe('isVoiceToggleKey honours configured record key (#18994)', () => {
384
+ it('binds the configured letter, not hardcoded b', async () => {
385
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('linux')
386
+ const ctrlO = parseVoiceRecordKey('ctrl+o')
387
+
388
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'o', ctrlO)).toBe(true)
389
+ // The old hardcoded 'b' must NOT match when the user configured 'o'.
390
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b', ctrlO)).toBe(false)
391
+ })
392
+
393
+ it('alt+<letter> binding matches alt OR meta (terminal-protocol parity)', async () => {
394
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('linux')
395
+ const altR = parseVoiceRecordKey('alt+r')
396
+
397
+ expect(isVoiceToggleKey({ alt: true, ctrl: false, meta: false, super: false }, 'r', altR)).toBe(true)
398
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'r', altR)).toBe(true)
399
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: false }, 'r', altR)).toBe(false)
400
+ })
401
+
402
+ it('binds named keys via ink event flags (space → ch === " ", enter → key.return, …)', async () => {
403
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('linux')
404
+
405
+ const ctrlSpace = parseVoiceRecordKey('ctrl+space')
406
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, ' ', ctrlSpace)).toBe(true)
407
+ // Single-char ``b`` must NOT match a ``space``-configured binding.
408
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b', ctrlSpace)).toBe(false)
409
+ // Space without the configured modifier must not fire either.
410
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: false }, ' ', ctrlSpace)).toBe(false)
411
+
412
+ const ctrlEnter = parseVoiceRecordKey('ctrl+enter')
413
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, return: true, super: false }, '', ctrlEnter)).toBe(true)
414
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, return: false, super: false }, '', ctrlEnter)).toBe(false)
415
+
416
+ const altTab = parseVoiceRecordKey('alt+tab')
417
+ expect(isVoiceToggleKey({ alt: true, ctrl: false, meta: false, super: false, tab: true }, '', altTab)).toBe(true)
418
+ expect(isVoiceToggleKey({ alt: false, ctrl: false, meta: false, super: false, tab: true }, '', altTab)).toBe(false)
419
+
420
+ const ctrlEscape = parseVoiceRecordKey('ctrl+escape')
421
+ expect(isVoiceToggleKey({ ctrl: true, escape: true, meta: false, super: false }, '', ctrlEscape)).toBe(true)
422
+ expect(isVoiceToggleKey({ ctrl: true, escape: false, meta: false, super: false }, '', ctrlEscape)).toBe(false)
423
+
424
+ const ctrlBackspace = parseVoiceRecordKey('ctrl+backspace')
425
+ expect(isVoiceToggleKey({ backspace: true, ctrl: true, meta: false, super: false }, '', ctrlBackspace)).toBe(true)
426
+
427
+ const ctrlDelete = parseVoiceRecordKey('ctrl+delete')
428
+ expect(isVoiceToggleKey({ ctrl: true, delete: true, meta: false, super: false }, '', ctrlDelete)).toBe(true)
429
+ })
430
+
431
+ it('omitted configured key falls back to ctrl+b (back-compat)', async () => {
432
+ const { isVoiceToggleKey } = await importPlatform('linux')
433
+
434
+ // No third arg → DEFAULT_VOICE_RECORD_KEY → Ctrl+B behaviour.
435
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b')).toBe(true)
436
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'o')).toBe(false)
437
+ })
438
+
439
+ // Regressions from Copilot review on #19835: the previous implementation
440
+ // accepted ``isActionMod(key)`` in the ``ctrl`` branch for every
441
+ // configured key, so bare Esc (which nastech-ink reports with
442
+ // ``key.meta`` on some macOS terminals) fired ``ctrl+escape``, and
443
+ // Alt+Space / Alt+Tab fired ``ctrl+space`` / ``ctrl+tab``. The fallback
444
+ // is now gated to the documented default (``ctrl+b``) only.
445
+ it('ctrl+escape does NOT fire on bare Esc via key.meta on macOS', async () => {
446
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('darwin')
447
+ const ctrlEscape = parseVoiceRecordKey('ctrl+escape')
448
+
449
+ // Bare Esc on a legacy macOS terminal: ``key.meta: true``, ``key.escape: true``, no ctrl.
450
+ expect(isVoiceToggleKey({ ctrl: false, escape: true, meta: true, super: false }, '', ctrlEscape)).toBe(false)
451
+ // Real Ctrl+Esc still fires.
452
+ expect(isVoiceToggleKey({ ctrl: true, escape: true, meta: false, super: false }, '', ctrlEscape)).toBe(true)
453
+ })
454
+
455
+ it('ctrl+space does NOT fire on Alt+Space on macOS', async () => {
456
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('darwin')
457
+ const ctrlSpace = parseVoiceRecordKey('ctrl+space')
458
+
459
+ // Alt+Space surfaces as ``key.meta: true`` with space char.
460
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, ' ', ctrlSpace)).toBe(false)
461
+ // Real Ctrl+Space still fires.
462
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, ' ', ctrlSpace)).toBe(true)
463
+ })
464
+
465
+ it('default ctrl+b accepts raw Ctrl+B and kitty-style Cmd+B on macOS', async () => {
466
+ const { DEFAULT_VOICE_RECORD_KEY, isVoiceToggleKey } = await importPlatform('darwin')
467
+
468
+ // Raw Ctrl+B: always works.
469
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b', DEFAULT_VOICE_RECORD_KEY)).toBe(true)
470
+ // Cmd+B via kitty-style ``key.super``: still works.
471
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b', DEFAULT_VOICE_RECORD_KEY)).toBe(true)
472
+ // Cmd+B via legacy ``key.meta`` NO LONGER works — ``key.meta`` is
473
+ // nastech-ink's Alt signal, so accepting it leaked Alt+B into the
474
+ // default binding (Copilot round-6 review on #19835).
475
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b', DEFAULT_VOICE_RECORD_KEY)).toBe(false)
476
+ })
477
+
478
+ it('custom ctrl+<letter> does NOT accept Cmd fallback on macOS', async () => {
479
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('darwin')
480
+ const ctrlO = parseVoiceRecordKey('ctrl+o')
481
+
482
+ // Only ``ctrl+b`` gets the action-modifier fallback; ``ctrl+o`` must
483
+ // be a literal Ctrl bit — otherwise Cmd+O would steal the shortcut.
484
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'o', ctrlO)).toBe(false)
485
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'o', ctrlO)).toBe(false)
486
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'o', ctrlO)).toBe(true)
487
+ })
488
+
489
+ it('super+b renders "Cmd+B" on darwin and requires the literal key.super bit', async () => {
490
+ const { formatVoiceRecordKey, isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('darwin')
491
+ const superB = parseVoiceRecordKey('super+b')
492
+
493
+ expect(formatVoiceRecordKey(superB)).toBe('Cmd+B')
494
+ // Kitty-style: key.super fires the binding.
495
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b', superB)).toBe(true)
496
+ // ``key.meta`` is NOT accepted — nastech-ink uses meta for Alt too,
497
+ // so accepting it here would make super+b silently fire on Alt+B
498
+ // (Copilot round-5 review on #19835).
499
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b', superB)).toBe(false)
500
+ // Ctrl held at the same time → reject (different chord).
501
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: true }, 'b', superB)).toBe(false)
502
+ })
503
+
504
+ // Round-2 Copilot review regressions on #19835.
505
+ it('super+b renders "Super+B" on Linux (not "Cmd+B")', async () => {
506
+ const { formatVoiceRecordKey, parseVoiceRecordKey } = await importPlatform('linux')
507
+
508
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('super+b'))).toBe('Super+B')
509
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('win+b'))).toBe('Super+B')
510
+ })
511
+
512
+ it('super+b still renders "Cmd+B" on macOS', async () => {
513
+ const { formatVoiceRecordKey, parseVoiceRecordKey } = await importPlatform('darwin')
514
+
515
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('super+b'))).toBe('Cmd+B')
516
+ expect(formatVoiceRecordKey(parseVoiceRecordKey('win+b'))).toBe('Cmd+B')
517
+ })
518
+
519
+ it('ctrl+b aliases (control+b, "ctrl + b") still accept Cmd+B fallback on macOS', async () => {
520
+ const { isVoiceToggleKey, parseVoiceRecordKey } = await importPlatform('darwin')
521
+ const controlB = parseVoiceRecordKey('control+b')
522
+ const spacedB = parseVoiceRecordKey('ctrl + b')
523
+
524
+ // Both parse to the documented default semantically; both must keep
525
+ // the macOS Cmd+B muscle-memory fallback via kitty-style key.super.
526
+ // ``key.meta`` is NOT accepted — that's nastech-ink's Alt signal
527
+ // (round-6 review), so legacy-terminal users get strict Ctrl+B.
528
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b', controlB)).toBe(false)
529
+ expect(isVoiceToggleKey({ ctrl: false, meta: true, super: false }, 'b', spacedB)).toBe(false)
530
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b', controlB)).toBe(true)
531
+ expect(isVoiceToggleKey({ ctrl: false, meta: false, super: true }, 'b', spacedB)).toBe(true)
532
+ // Literal Ctrl+B still fires.
533
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'b', controlB)).toBe(true)
534
+ // And still reject a ctrl bit on a different letter.
535
+ expect(isVoiceToggleKey({ ctrl: true, meta: false, super: false }, 'o', controlB)).toBe(false)
536
+ })
537
+ })
538
+
539
+ describe('isMacActionFallback', () => {
540
+ it('routes raw Ctrl+K and Ctrl+W to readline kill-to-end / delete-word on macOS', async () => {
541
+ const { isMacActionFallback } = await importPlatform('darwin')
542
+
543
+ expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'k', 'k')).toBe(true)
544
+ expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'w', 'w')).toBe(true)
545
+ // Must not fire when Cmd (meta/super) is held — those are distinct chords.
546
+ expect(isMacActionFallback({ ctrl: true, meta: true, super: false }, 'k', 'k')).toBe(false)
547
+ expect(isMacActionFallback({ ctrl: true, meta: false, super: true }, 'w', 'w')).toBe(false)
548
+ })
549
+
550
+ it('is a no-op on non-macOS (Linux routes Ctrl+K/W through isActionMod directly)', async () => {
551
+ const { isMacActionFallback } = await importPlatform('linux')
552
+
553
+ expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'k', 'k')).toBe(false)
554
+ expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'w', 'w')).toBe(false)
555
+ })
556
+ })
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { computePrecisionWheelStep, initPrecisionWheel } from '../lib/precisionWheel.js'
4
+
5
+ describe('precisionWheel', () => {
6
+ it('passes the first modifier-held wheel event', () => {
7
+ const s = initPrecisionWheel()
8
+
9
+ expect(computePrecisionWheelStep(s, 1, true, 1000)).toEqual({ active: true, entered: true, rows: 1 })
10
+ })
11
+
12
+ it('coalesces same-frame events without throttling line-by-line scroll', () => {
13
+ const s = initPrecisionWheel()
14
+
15
+ computePrecisionWheelStep(s, 1, true, 1000)
16
+
17
+ expect(computePrecisionWheelStep(s, 1, true, 1008).rows).toBe(0)
18
+ expect(computePrecisionWheelStep(s, 1, true, 1016).rows).toBe(1)
19
+ })
20
+
21
+ it('keeps queued momentum in precision mode briefly after modifier release', () => {
22
+ const s = initPrecisionWheel()
23
+
24
+ computePrecisionWheelStep(s, 1, true, 1000)
25
+
26
+ expect(computePrecisionWheelStep(s, 1, false, 1050)).toMatchObject({ active: true, rows: 1 })
27
+ })
28
+
29
+ it('leaves precision mode once modifier-free momentum goes idle', () => {
30
+ const s = initPrecisionWheel()
31
+
32
+ computePrecisionWheelStep(s, 1, true, 1000)
33
+
34
+ expect(computePrecisionWheelStep(s, 1, false, 1100)).toEqual({ active: false, entered: false, rows: 0 })
35
+ })
36
+
37
+ it('does not coalesce immediate reversals', () => {
38
+ const s = initPrecisionWheel()
39
+
40
+ computePrecisionWheelStep(s, 1, true, 1000)
41
+
42
+ expect(computePrecisionWheelStep(s, -1, true, 1008).rows).toBe(1)
43
+ })
44
+ })
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { composerPromptText } from '../lib/prompt.js'
4
+
5
+ describe('composerPromptText', () => {
6
+ it('returns shell prompt for ! commands', () => {
7
+ expect(composerPromptText('❯', 'coder', true)).toBe('$')
8
+ })
9
+
10
+ it('prefixes named profiles onto the normal prompt', () => {
11
+ expect(composerPromptText('❯', 'coder')).toBe('coder ❯')
12
+ })
13
+
14
+ it('does not prefix default or custom profiles', () => {
15
+ expect(composerPromptText('❯', 'default')).toBe('❯')
16
+ expect(composerPromptText('❯', 'custom')).toBe('❯')
17
+ expect(composerPromptText('❯')).toBe('❯')
18
+ })
19
+
20
+ it('uses a Termux-safe ASCII prompt marker in normal mode', () => {
21
+ expect(composerPromptText('❯', 'coder', false, true, 50)).toBe('>')
22
+ })
23
+
24
+ it('keeps profile prefix suppressed on narrow Termux widths', () => {
25
+ expect(composerPromptText('❯', 'upstr', false, true, 72)).toBe('>')
26
+ })
27
+
28
+ it('allows profile prefix on very wide Termux panes', () => {
29
+ expect(composerPromptText('❯', 'upstr', false, true, 120)).toBe('upstr >')
30
+ })
31
+ })