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,40 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { applyPrintableInsert, shouldRouteMultiCharInputAsPaste } from '../components/textInput.js'
4
+
5
+ describe('applyPrintableInsert', () => {
6
+ it('applies non-bracketed multi-character bursts immediately', () => {
7
+ const burst = applyPrintableInsert('abc', 3, 'xxxxx')
8
+
9
+ const repeated = [...'xxxxx'].reduce(
10
+ (state, ch) => applyPrintableInsert(state.value, state.cursor, ch)!,
11
+ { cursor: 3, value: 'abc' }
12
+ )
13
+
14
+ expect(burst).toEqual({ cursor: 8, value: 'abcxxxxx' })
15
+ expect(burst).toEqual(repeated)
16
+ })
17
+
18
+ it('replaces the selected range for burst input', () => {
19
+ expect(applyPrintableInsert('abZZef', 4, 'cd', { end: 4, start: 2 })).toEqual({
20
+ cursor: 4,
21
+ value: 'abcdef'
22
+ })
23
+ })
24
+
25
+ it('rejects control or escape-bearing input', () => {
26
+ expect(applyPrintableInsert('abc', 3, '\x1b[200~pasted')).toBeNull()
27
+ expect(applyPrintableInsert('abc', 3, '\t')).toBeNull()
28
+ })
29
+ })
30
+
31
+ describe('shouldRouteMultiCharInputAsPaste', () => {
32
+ it('keeps newline-bearing chunks on the paste path', () => {
33
+ expect(shouldRouteMultiCharInputAsPaste('hello\nworld')).toBe(true)
34
+ expect(shouldRouteMultiCharInputAsPaste('hello\r\nworld'.replace(/\r\n/g, '\n'))).toBe(true)
35
+ })
36
+
37
+ it('treats repeated printable key bursts as immediate input', () => {
38
+ expect(shouldRouteMultiCharInputAsPaste('xxxxx')).toBe(false)
39
+ })
40
+ })
@@ -0,0 +1,50 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ // Locate textInput.tsx relative to this test file so the assertion
8
+ // survives moves of the test fixture itself.
9
+ const TEXT_INPUT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', 'components', 'textInput.tsx')
10
+ const source = readFileSync(TEXT_INPUT_PATH, 'utf8')
11
+
12
+ // Closes Copilot follow-up on PR #26717: the original cursor-drift
13
+ // fix bumped Ink's displayCursor / cursorDeclaration on fast-echo, but
14
+ // if TextInput itself re-renders before the deferred 16ms `setCur`
15
+ // flushes (parent state change, status-bar tick, spinner) the layout
16
+ // effect inside `useDeclaredCursor` re-publishes a declaration
17
+ // computed from the STALE React `cur` state and clobbers the Ink-level
18
+ // bump. The fix is structural: read `curRef.current` (always
19
+ // up-to-date) when computing the layout, not the `cur` state.
20
+ //
21
+ // This file pins that invariant. Switching back to `cur` state — or
22
+ // re-introducing a memo keyed on `cur` that uses `curRef.current`
23
+ // inside but stops re-computing on rerender — is a regression and
24
+ // should be caught here, not via a flaky integration test that mounts
25
+ // Ink + stdin.
26
+ describe('textInput cursor-layout source of truth', () => {
27
+ it('reads curRef.current (not the cur React state) for cursorLayout', () => {
28
+ // The line we care about. We allow whitespace / formatting drift,
29
+ // but the call itself must use `curRef.current`.
30
+ expect(source).toMatch(/cursorLayout\(\s*display\s*,\s*curRef\.current\s*,\s*columns\s*\)/)
31
+ })
32
+
33
+ it('does not pass the bare `cur` React state into cursorLayout', () => {
34
+ // Any `cursorLayout(display, cur, columns)` invocation would
35
+ // reintroduce the stale-declaration window.
36
+ expect(source).not.toMatch(/cursorLayout\(\s*display\s*,\s*cur\s*,\s*columns\s*\)/)
37
+ })
38
+
39
+ it('keeps the fast-echo notifier calls paired with the stdout writes', () => {
40
+ // Both fast-echo paths must call noteCursorAdvance, otherwise Ink
41
+ // never learns about the out-of-band write and drifts again. We
42
+ // tolerate explanatory comments in between (the rationale block is
43
+ // intentionally long), but the pairing itself must hold.
44
+ const backspacePattern = /stdout!\.write\(['"`]\\b \\b['"`]\)[\s\S]{0,1000}?noteCursorAdvance\(-1\)/
45
+ expect(source).toMatch(backspacePattern)
46
+
47
+ const appendPattern = /stdout!\.write\(text\)[\s\S]{0,1000}?noteCursorAdvance\(text\.length\)/
48
+ expect(source).toMatch(appendPattern)
49
+ })
50
+ })
@@ -0,0 +1,200 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { canFastAppendShape, canFastBackspaceShape, supportsFastEchoTerminal } from '../components/textInput.js'
4
+
5
+ // The fast-echo path bypasses Ink and writes characters directly to stdout
6
+ // for the common case of typing plain English at the end of the line. These
7
+ // tests pin the shape preconditions that make that bypass safe.
8
+ //
9
+ // Regression intent: any non-ASCII text — Vietnamese precomposed letters
10
+ // (one grapheme, `text.length === 1`, `stringWidth === 1`, but produced
11
+ // via IME composition across multiple keystrokes), combining marks
12
+ // (zero width), CJK (double width), emoji (variable width), or anything
13
+ // that could be produced by an in-flight IME composition — must NOT
14
+ // take the bypass. Closes:
15
+ // - "TUI is experiencing font errors when using Unicode to type Vietnamese"
16
+ // - #5221 TUI input box renders incorrectly for CJK / East-Asian wide
17
+ // - #7443 CLI TUI renders and deletes Chinese characters incorrectly
18
+ // - #17602 / #17603 Chinese text scattering / ghosting
19
+
20
+ describe('canFastAppendShape', () => {
21
+ const COLS = 40
22
+
23
+ it('accepts plain ASCII appended at end of single-line input', () => {
24
+ expect(canFastAppendShape('hello', 5, 'x', COLS, 5)).toBe(true)
25
+ expect(canFastAppendShape('hello', 5, ' world', COLS, 5)).toBe(true)
26
+ })
27
+
28
+ it('rejects when cursor is not at end of line', () => {
29
+ expect(canFastAppendShape('hello', 3, 'x', COLS, 5)).toBe(false)
30
+ })
31
+
32
+ it('rejects when current is empty (placeholder render path needed)', () => {
33
+ expect(canFastAppendShape('', 0, 'x', COLS, 0)).toBe(false)
34
+ })
35
+
36
+ it('rejects when current contains a newline (multi-line layout)', () => {
37
+ expect(canFastAppendShape('hi\nthere', 8, 'x', COLS, 5)).toBe(false)
38
+ })
39
+
40
+ it('rejects when appending would hit the wrap column', () => {
41
+ // Reaching cols on append must trigger a wrap, which the bypass
42
+ // cannot draw. Stay strictly below cols.
43
+ expect(canFastAppendShape('hello', 5, 'x', 6, 5)).toBe(false)
44
+ })
45
+
46
+ // -- Regression coverage: Vietnamese / combining marks / IME --
47
+
48
+ it('rejects Vietnamese precomposed letter ề (U+1EC1) — IME composition path', () => {
49
+ // 'ề' is one grapheme, length 1, width 1, but Vietnamese Telex/IME
50
+ // produces it via a multi-key composition. Fast-echo would commit the
51
+ // intermediate state to stdout and desync once the final commit
52
+ // arrives.
53
+ expect(canFastAppendShape('hello', 5, 'ề', COLS, 5)).toBe(false)
54
+ })
55
+
56
+ it('rejects Vietnamese tone marks ă, ơ, ư (Latin-Extended-A/B)', () => {
57
+ for (const ch of ['ă', 'ắ', 'ơ', 'ờ', 'ư', 'ự']) {
58
+ expect(canFastAppendShape('hello', 5, ch, COLS, 5)).toBe(false)
59
+ }
60
+ })
61
+
62
+ it('rejects NFD combining marks (U+0300 grave, U+0301 acute, U+0302 circumflex)', () => {
63
+ // Decomposed Vietnamese: 'e' + combining circumflex + combining grave
64
+ // = 'ề'. Each combining mark is zero-width but length 1; without the
65
+ // ASCII guard the second/third keypress would be fast-echoed and
66
+ // desync the cell column.
67
+ expect(canFastAppendShape('hello', 5, '\u0300', COLS, 5)).toBe(false)
68
+ expect(canFastAppendShape('hello', 5, '\u0301', COLS, 5)).toBe(false)
69
+ expect(canFastAppendShape('hello', 5, '\u0302', COLS, 5)).toBe(false)
70
+ })
71
+
72
+ it('rejects CJK (East-Asian wide) characters', () => {
73
+ expect(canFastAppendShape('hello', 5, '你', COLS, 5)).toBe(false)
74
+ expect(canFastAppendShape('hello', 5, '日本', COLS, 5)).toBe(false)
75
+ })
76
+
77
+ it('rejects emoji', () => {
78
+ expect(canFastAppendShape('hello', 5, '🙂', COLS, 5)).toBe(false)
79
+ })
80
+
81
+ it('rejects ANSI-bearing or control text', () => {
82
+ expect(canFastAppendShape('hello', 5, '\x1b[31m', COLS, 5)).toBe(false)
83
+ expect(canFastAppendShape('hello', 5, '\t', COLS, 5)).toBe(false)
84
+ expect(canFastAppendShape('hello', 5, '\x7f', COLS, 5)).toBe(false)
85
+ })
86
+
87
+ it('rejects NBSP and Latin-1 letters that would change the line shape', () => {
88
+ expect(canFastAppendShape('hello', 5, '\u00a0', COLS, 5)).toBe(false)
89
+ expect(canFastAppendShape('hello', 5, 'é', COLS, 5)).toBe(false)
90
+ expect(canFastAppendShape('hello', 5, 'ñ', COLS, 5)).toBe(false)
91
+ })
92
+ })
93
+
94
+ describe('canFastBackspaceShape', () => {
95
+ it('accepts deleting the last ASCII char', () => {
96
+ expect(canFastBackspaceShape('hello', 5)).toBe(true)
97
+ })
98
+
99
+ it('rejects when cursor is not at end', () => {
100
+ expect(canFastBackspaceShape('hello', 3)).toBe(false)
101
+ })
102
+
103
+ it('rejects when there is nothing to delete', () => {
104
+ expect(canFastBackspaceShape('', 0)).toBe(false)
105
+ expect(canFastBackspaceShape('hello', 0)).toBe(false)
106
+ })
107
+
108
+ it('rejects when value contains a newline', () => {
109
+ expect(canFastBackspaceShape('hi\nthere', 8)).toBe(false)
110
+ })
111
+
112
+ it('rejects deleting Vietnamese precomposed letter ề', () => {
113
+ // The "\b \b" shortcut clears one terminal cell; that's fine for a
114
+ // 1-cell ASCII char but if the previous grapheme is a Vietnamese
115
+ // letter that the IME may still be holding open, we want Ink to
116
+ // re-render so composition state stays consistent.
117
+ expect(canFastBackspaceShape('helloề', 'helloề'.length)).toBe(false)
118
+ })
119
+
120
+ it('rejects deleting a CJK character (2 cells)', () => {
121
+ expect(canFastBackspaceShape('hi你', 'hi你'.length)).toBe(false)
122
+ })
123
+
124
+ it('rejects deleting a NFD-composed grapheme with combining marks', () => {
125
+ // 'e' + U+0302 (circumflex) + U+0300 (grave) — final grapheme is one
126
+ // cluster but the previous-grapheme slice is multi-codepoint. Width
127
+ // is 1 but the bypass would be unsafe because the rendered cell
128
+ // already contained the combined glyph.
129
+ const s = 'hello' + 'e\u0302\u0300'
130
+ expect(canFastBackspaceShape(s, s.length)).toBe(false)
131
+ })
132
+
133
+ it('rejects deleting an emoji', () => {
134
+ expect(canFastBackspaceShape('hi🙂', 'hi🙂'.length)).toBe(false)
135
+ })
136
+
137
+ // Closes Copilot PR #26717 round 3: the "\b \b" sequence cannot move
138
+ // the terminal cursor onto the previous visual row across a
139
+ // soft-wrap boundary. When the caret sits at visual column 0 of a
140
+ // wrapped row (column == 0 in the computed cursor layout), backspace
141
+ // would leave the physical cursor in place while the logical caret
142
+ // moves up to the end of the previous visual line — desyncing both
143
+ // Ink's displayCursor model and the user-visible position. The fast
144
+ // path must fall through in that case so the normal Ink render path
145
+ // can lay out the correct cursor position.
146
+ it('rejects fast-backspace at a soft-wrap boundary when columns is known', () => {
147
+ // value width 6 in a column of 6 → cursorLayout produces (line 1, col 0)
148
+ // i.e. the caret has overflowed onto the next visual line.
149
+ const value = 'hello '
150
+ expect(canFastBackspaceShape(value, value.length, 6)).toBe(false)
151
+ })
152
+
153
+ it('rejects fast-backspace at an exact multiple of columns (wide wrap)', () => {
154
+ // 12 chars at width 6 → two full visual rows, caret at (line 2, col 0).
155
+ const value = 'abcdefghijkl'
156
+ expect(canFastBackspaceShape(value, value.length, 6)).toBe(false)
157
+ })
158
+
159
+ it('still accepts fast-backspace inside a wrapped line', () => {
160
+ // Caret mid-visual-line — "\b \b" can move the cursor one cell left
161
+ // without crossing a wrap boundary.
162
+ expect(canFastBackspaceShape('hello world', 'hello world'.length, 20)).toBe(true)
163
+ expect(canFastBackspaceShape('abcdefghi', 9, 6)).toBe(true) // visual line 1, col 3 → ok
164
+ })
165
+
166
+ it('skips the wrap-boundary check when columns is omitted (legacy contract)', () => {
167
+ // Callers that don't pass `columns` fall back to the pre-wrap-aware
168
+ // behavior — the function does NOT magically reject anything that
169
+ // could be a wrap boundary without the width. Production callers
170
+ // must always pass `columns`; this case is for unit tests of the
171
+ // pre-wrap shape contract.
172
+ expect(canFastBackspaceShape('hello ', 'hello '.length)).toBe(true)
173
+ })
174
+ })
175
+
176
+ describe('supportsFastEchoTerminal', () => {
177
+ it('disables fast-echo in Apple Terminal', () => {
178
+ expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false)
179
+ })
180
+
181
+ it('disables fast-echo by default in Termux mode', () => {
182
+ expect(
183
+ supportsFastEchoTerminal({ TERMUX_VERSION: '0.118.0', PREFIX: '/data/data/com.termux/files/usr' } as NodeJS.ProcessEnv)
184
+ ).toBe(false)
185
+ })
186
+
187
+ it('allows explicit Termux fast-echo opt-in via env override', () => {
188
+ expect(
189
+ supportsFastEchoTerminal({
190
+ NASTECH_TUI_TERMUX_FAST_ECHO: '1',
191
+ TERMUX_VERSION: '0.118.0'
192
+ } as NodeJS.ProcessEnv)
193
+ ).toBe(true)
194
+ })
195
+
196
+ it('keeps fast-echo enabled in VS Code and unknown non-Termux terminals', () => {
197
+ expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv)).toBe(true)
198
+ expect(supportsFastEchoTerminal({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true)
199
+ })
200
+ })
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { lineNav } from '../components/textInput.js'
4
+
5
+ describe('lineNav', () => {
6
+ it('returns null for single-line input (up)', () => {
7
+ expect(lineNav('hello world', 6, -1)).toBeNull()
8
+ })
9
+
10
+ it('returns null for single-line input (down)', () => {
11
+ expect(lineNav('hello world', 6, 1)).toBeNull()
12
+ })
13
+
14
+ it('returns null when cursor already on first line of a multiline block', () => {
15
+ expect(lineNav('one\ntwo\nthree', 2, -1)).toBeNull()
16
+ })
17
+
18
+ it('returns null when cursor on last line of a multiline block', () => {
19
+ expect(lineNav('one\ntwo\nthree', 10, 1)).toBeNull()
20
+ })
21
+
22
+ it('moves cursor up one line preserving column', () => {
23
+ // "hello\nworld" — cursor at col 3 of line 1 ('l' in world) → col 3 of line 0 ('l' in hello)
24
+ expect(lineNav('hello\nworld', 9, -1)).toBe(3)
25
+ })
26
+
27
+ it('moves cursor down one line preserving column', () => {
28
+ // cursor at col 2 of line 0 → col 2 of line 1
29
+ expect(lineNav('hello\nworld', 2, 1)).toBe(8)
30
+ })
31
+
32
+ it('clamps to end of shorter destination line on up', () => {
33
+ // col 10 on long line → clamp to end of short line "abc"
34
+ const s = 'abc\nlong long text'
35
+ const from = 14
36
+
37
+ expect(lineNav(s, from, -1)).toBe(3)
38
+ })
39
+
40
+ it('clamps to end of shorter destination line on down', () => {
41
+ // col 10 on line 0 → clamp to end of "abc" on line 1
42
+ const s = 'long long text\nabc'
43
+
44
+ expect(lineNav(s, 10, 1)).toBe(18)
45
+ })
46
+
47
+ it('handles empty lines correctly', () => {
48
+ // "a\n\nb" — cursor at line 2 (b) → up to empty line 1
49
+ expect(lineNav('a\n\nb', 3, -1)).toBe(2)
50
+ })
51
+
52
+ it('handles leading newline without crashing', () => {
53
+ expect(lineNav('\nfoo', 2, -1)).toBe(0)
54
+ })
55
+ })
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { shouldPassThroughToGlobalHandler, shouldPreserveCtrlJNewline } from '../components/textInput.js'
4
+ import { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } from '../lib/platform.js'
5
+
6
+ const key = (overrides: Record<string, unknown> = {}) =>
7
+ ({ ctrl: false, meta: false, ...overrides }) as any
8
+
9
+ describe('shouldPreserveCtrlJNewline', () => {
10
+ it('preserves Ctrl+J as newline in Ghostty even when tmux masks TERM/TERM_PROGRAM', () => {
11
+ expect(
12
+ shouldPreserveCtrlJNewline({
13
+ GHOSTTY_RESOURCES_DIR: '/usr/share/ghostty',
14
+ TERM: 'tmux-256color',
15
+ TERM_PROGRAM: 'tmux'
16
+ })
17
+ ).toBe(true)
18
+ })
19
+
20
+ it('keeps bare local POSIX LF-compatible prompts submitting on Ctrl+J', () => {
21
+ expect(shouldPreserveCtrlJNewline({ TERM: 'xterm-256color' })).toBe(false)
22
+ })
23
+ })
24
+
25
+ describe('shouldPassThroughToGlobalHandler', () => {
26
+ it('passes through the configured voice shortcut while composer is focused', () => {
27
+ expect(
28
+ shouldPassThroughToGlobalHandler('o', key({ ctrl: true }), parseVoiceRecordKey('ctrl+o'))
29
+ ).toBe(true)
30
+ expect(
31
+ shouldPassThroughToGlobalHandler('r', key({ meta: true }), parseVoiceRecordKey('alt+r'))
32
+ ).toBe(true)
33
+ expect(
34
+ shouldPassThroughToGlobalHandler(' ', key({ ctrl: true }), parseVoiceRecordKey('ctrl+space'))
35
+ ).toBe(true)
36
+ expect(
37
+ shouldPassThroughToGlobalHandler('', key({ ctrl: true, return: true }), parseVoiceRecordKey('ctrl+enter'))
38
+ ).toBe(true)
39
+ })
40
+
41
+ it('keeps the legacy default pass-through when no custom key is provided', () => {
42
+ expect(shouldPassThroughToGlobalHandler('b', key({ ctrl: true }), DEFAULT_VOICE_RECORD_KEY)).toBe(true)
43
+ expect(shouldPassThroughToGlobalHandler('b', key({ ctrl: true }))).toBe(true)
44
+ })
45
+
46
+ it('does not swallow ordinary typing keys', () => {
47
+ expect(shouldPassThroughToGlobalHandler('h', key(), parseVoiceRecordKey('ctrl+o'))).toBe(false)
48
+ expect(shouldPassThroughToGlobalHandler('o', key(), parseVoiceRecordKey('ctrl+o'))).toBe(false)
49
+ })
50
+
51
+ it('always passes through non-voice global control keys', () => {
52
+ expect(shouldPassThroughToGlobalHandler('c', key({ ctrl: true }))).toBe(true)
53
+ expect(shouldPassThroughToGlobalHandler('x', key({ ctrl: true }))).toBe(true)
54
+ expect(shouldPassThroughToGlobalHandler('', key({ escape: true }))).toBe(true)
55
+ expect(shouldPassThroughToGlobalHandler('', key({ tab: true }))).toBe(true)
56
+ expect(shouldPassThroughToGlobalHandler('', key({ pageUp: true }))).toBe(true)
57
+ expect(shouldPassThroughToGlobalHandler('', key({ pageDown: true }))).toBe(true)
58
+ })
59
+ })
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { decideRightClickAction } from '../components/textInput.js'
4
+
5
+ describe('decideRightClickAction', () => {
6
+ it('returns paste when there is no selection', () => {
7
+ expect(decideRightClickAction('hello world', null)).toEqual({ action: 'paste' })
8
+ })
9
+
10
+ it('returns paste for a collapsed (empty) range', () => {
11
+ expect(decideRightClickAction('hello world', { end: 5, start: 5 })).toEqual({
12
+ action: 'paste'
13
+ })
14
+ })
15
+
16
+ it('copies the slice when range covers non-empty text', () => {
17
+ expect(decideRightClickAction('hello world', { end: 5, start: 0 })).toEqual({
18
+ action: 'copy',
19
+ text: 'hello'
20
+ })
21
+ })
22
+
23
+ it('copies a middle slice', () => {
24
+ expect(decideRightClickAction('hello world', { end: 11, start: 6 })).toEqual({
25
+ action: 'copy',
26
+ text: 'world'
27
+ })
28
+ })
29
+
30
+ it('falls back to paste when slice is empty (out-of-range indices)', () => {
31
+ expect(decideRightClickAction('', { end: 5, start: 0 })).toEqual({ action: 'paste' })
32
+ })
33
+
34
+ it('handles unicode (emoji, CJK) in the slice', () => {
35
+ const value = 'hi 你好 🎉'
36
+ expect(decideRightClickAction(value, { end: 5, start: 3 })).toEqual({
37
+ action: 'copy',
38
+ text: '你好'
39
+ })
40
+ })
41
+
42
+ it('preserves leading/trailing whitespace in the copied slice', () => {
43
+ expect(decideRightClickAction(' spaced ', { end: 10, start: 0 })).toEqual({
44
+ action: 'copy',
45
+ text: ' spaced '
46
+ })
47
+ })
48
+ })
@@ -0,0 +1,151 @@
1
+ import { wrapAnsi } from '@nastechai/ink'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { offsetFromPosition } from '../components/textInput.js'
5
+ import { composerPromptWidth, cursorLayout, inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js'
6
+
7
+ // Helper: compute the "end of text" position that wrap-ansi would render
8
+ // the input to. This is what Ink's <Text wrap="wrap"> uses, so cursorLayout
9
+ // MUST agree. Disagreement is the cursor-drift bug.
10
+ function wrapAnsiEndPosition(text: string, cols: number): { line: number; column: number } {
11
+ const wrapped = wrapAnsi(text, cols, { hard: true, trim: false })
12
+ const lines = wrapped.split('\n')
13
+ const last = lines[lines.length - 1] ?? ''
14
+
15
+ return { line: lines.length - 1, column: last.length }
16
+ }
17
+
18
+ describe('cursorLayout — word-wrap parity with wrap-ansi', () => {
19
+ it('places cursor mid-line at its column', () => {
20
+ expect(cursorLayout('hello world', 6, 40)).toEqual({ column: 6, line: 0 })
21
+ })
22
+
23
+ it('places cursor at end of a non-full line', () => {
24
+ expect(cursorLayout('hi', 2, 10)).toEqual({ column: 2, line: 0 })
25
+ })
26
+
27
+ it('does not push exact-fill text onto a phantom next line', () => {
28
+ // Regression: the previous hand-rolled wrap algorithm forced the cursor
29
+ // onto (line+1, 0) when the text exactly filled the row. wrap-ansi keeps
30
+ // it on the same row (no soft-wrap), so the cursor must too — otherwise
31
+ // useDeclaredCursor parks the hardware cursor below the last char and
32
+ // the user sees several blank cells between text and cursor block
33
+ // (#cursor-drift-multiline).
34
+ expect(cursorLayout('abcdefgh', 8, 8)).toEqual({ column: 8, line: 0 })
35
+ expect(cursorLayout('abcdefgh', 8, 8)).toEqual(wrapAnsiEndPosition('abcdefgh', 8))
36
+ })
37
+
38
+ it('keeps short words on the current line when they fit (no phantom wrap)', () => {
39
+ // wrap-ansi: "hello wo" at cols=8 stays as one line "hello wo".
40
+ // The old cursorLayout incorrectly pushed to (1,0) because column=8 hit
41
+ // the column>=width check, but that disagreed with what Ink actually
42
+ // rendered.
43
+ expect(cursorLayout('hello wo', 8, 8)).toEqual({ column: 8, line: 0 })
44
+ expect(cursorLayout('hello wo', 8, 8)).toEqual(wrapAnsiEndPosition('hello wo', 8))
45
+ })
46
+
47
+ it('moves words across wrap boundaries instead of splitting them', () => {
48
+ // "hello wor" at cols=8: wrap-ansi breaks at the space, "hello \nwor".
49
+ expect(cursorLayout('hello wor', 9, 8)).toEqual({ column: 3, line: 1 })
50
+ expect(cursorLayout('hello worl', 10, 8)).toEqual({ column: 4, line: 1 })
51
+ expect(cursorLayout('hello world', 11, 8)).toEqual({ column: 5, line: 1 })
52
+
53
+ // Each must match what wrap-ansi would actually render.
54
+ expect(cursorLayout('hello wor', 9, 8)).toEqual(wrapAnsiEndPosition('hello wor', 8))
55
+ expect(cursorLayout('hello worl', 10, 8)).toEqual(wrapAnsiEndPosition('hello worl', 8))
56
+ expect(cursorLayout('hello world', 11, 8)).toEqual(wrapAnsiEndPosition('hello world', 8))
57
+ })
58
+
59
+ it('wraps the next word instead of splitting it at the right edge', () => {
60
+ const text = 'hello world baby chickens are so cool its really rainy outside but wish'
61
+
62
+ expect(cursorLayout(text, text.length, 70)).toEqual({ column: 4, line: 1 })
63
+ expect(inputVisualHeight(text, 70)).toBe(2)
64
+ })
65
+
66
+ it('honours explicit newlines', () => {
67
+ expect(cursorLayout('one\ntwo', 5, 40)).toEqual({ column: 1, line: 1 })
68
+ expect(cursorLayout('one\ntwo', 4, 40)).toEqual({ column: 0, line: 1 })
69
+ })
70
+
71
+ it('does not wrap when cursor is before the right edge', () => {
72
+ expect(cursorLayout('abcdefg', 7, 8)).toEqual({ column: 7, line: 0 })
73
+ })
74
+
75
+ it('matches wrap-ansi end-position for typing-style incremental input', () => {
76
+ // Pins the actual fix: type a long message char-by-char at a narrow
77
+ // width and assert the cursor follows wrap-ansi every step of the way.
78
+ // Before the fix, ~5 boundary positions per pass disagreed and Ink
79
+ // parked the cursor several cells past the last rendered character.
80
+ const MSG = 'on a new bb branch investigate and fix the cursor drift bug here'
81
+
82
+ for (const cols of [10, 14, 20, 30, 50, 80]) {
83
+ let acc = ''
84
+
85
+ for (const ch of MSG) {
86
+ acc += ch
87
+ expect(cursorLayout(acc, acc.length, cols)).toEqual(wrapAnsiEndPosition(acc, cols))
88
+ }
89
+ }
90
+ })
91
+ })
92
+
93
+ describe('input metrics helpers', () => {
94
+ it('computes visual height matching wrap-ansi line count', () => {
95
+ // Exact-fill text stays on one line in wrap-ansi (no phantom wrap), so
96
+ // visual height is 1. The previous implementation reported 2 here.
97
+ expect(inputVisualHeight('abcdefgh', 8)).toBe(1)
98
+ expect(inputVisualHeight('one\ntwo', 40)).toBe(2)
99
+ // Multi-line wrap case sanity
100
+ expect(inputVisualHeight('hello world', 8)).toBe(2)
101
+ })
102
+
103
+ it('counts the prompt gap as its own cell', () => {
104
+ expect(composerPromptWidth('>')).toBe(2)
105
+ expect(composerPromptWidth('❯')).toBe(2)
106
+ expect(composerPromptWidth('Ψ >')).toBe(4)
107
+ })
108
+
109
+ it('reserves gutters on wide panes without starving narrow composer width', () => {
110
+ expect(stableComposerColumns(100, 3)).toBe(93)
111
+ expect(stableComposerColumns(100, 5)).toBe(91)
112
+ expect(stableComposerColumns(10, 3)).toBe(5)
113
+ expect(stableComposerColumns(6, 3)).toBe(1)
114
+ })
115
+ })
116
+
117
+ describe('offsetFromPosition — word-wrap inverse of cursorLayout', () => {
118
+ it('returns 0 for empty input', () => {
119
+ expect(offsetFromPosition('', 0, 0, 10)).toBe(0)
120
+ })
121
+
122
+ it('maps clicks within a single line', () => {
123
+ expect(offsetFromPosition('hello', 0, 3, 40)).toBe(3)
124
+ })
125
+
126
+ it('maps clicks past end to value length', () => {
127
+ expect(offsetFromPosition('hi', 0, 10, 40)).toBe(2)
128
+ })
129
+
130
+ it('maps clicks on a wrapped second row at cols boundary', () => {
131
+ // Long words still hard-wrap when there is no word boundary.
132
+ expect(offsetFromPosition('abcdefghij', 1, 0, 8)).toBe(8)
133
+ })
134
+
135
+ it('maps clicks on a word-wrapped second row', () => {
136
+ // "hello world" at cols=8 wraps to "hello \nworld".
137
+ expect(offsetFromPosition('hello world', 1, 0, 8)).toBe(6)
138
+ expect(offsetFromPosition('hello world', 1, 3, 8)).toBe(9)
139
+ })
140
+
141
+ it('maps clicks on the moved final word', () => {
142
+ const text = 'hello world baby chickens are so cool its really rainy outside but wish'
143
+
144
+ expect(offsetFromPosition(text, 1, 0, 70)).toBe(text.indexOf('wish'))
145
+ expect(offsetFromPosition(text, 1, 3, 70)).toBe(text.indexOf('wish') + 3)
146
+ })
147
+
148
+ it('maps clicks past a \\n into the target line', () => {
149
+ expect(offsetFromPosition('one\ntwo', 1, 2, 40)).toBe(6)
150
+ })
151
+ })