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,58 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { stickyPromptFromViewport } from '../domain/viewport.js'
4
+
5
+ describe('stickyPromptFromViewport', () => {
6
+ it('hides the sticky prompt when a newer user message is already visible', () => {
7
+ const messages = [
8
+ { role: 'user' as const, text: 'older prompt' },
9
+ { role: 'assistant' as const, text: 'older answer' },
10
+ { role: 'user' as const, text: 'current prompt' },
11
+ { role: 'assistant' as const, text: 'current answer' }
12
+ ]
13
+
14
+ const offsets = [0, 2, 10, 12, 20]
15
+
16
+ expect(stickyPromptFromViewport(messages, offsets, 8, 16, false)).toBe('')
17
+ })
18
+
19
+ it('shows the latest user message above the viewport when no user message is visible', () => {
20
+ const messages = [
21
+ { role: 'user' as const, text: 'older prompt' },
22
+ { role: 'assistant' as const, text: 'older answer' },
23
+ { role: 'user' as const, text: 'current prompt' },
24
+ { role: 'assistant' as const, text: 'current answer' }
25
+ ]
26
+
27
+ const offsets = [0, 2, 10, 12, 20]
28
+
29
+ expect(stickyPromptFromViewport(messages, offsets, 16, 20, false)).toBe('current prompt')
30
+ })
31
+
32
+ it('shows the last prompt once the viewport starts after the history tail', () => {
33
+ const messages = [
34
+ { role: 'user' as const, text: 'current prompt' },
35
+ { role: 'assistant' as const, text: 'completed answer' }
36
+ ]
37
+
38
+ expect(stickyPromptFromViewport(messages, [0, 2, 5], 8, 14, false)).toBe('current prompt')
39
+ })
40
+
41
+ it('shows a prompt as soon as its full row is above the viewport', () => {
42
+ const messages = [
43
+ { role: 'user' as const, text: 'current prompt' },
44
+ { role: 'assistant' as const, text: 'current answer' }
45
+ ]
46
+
47
+ expect(stickyPromptFromViewport(messages, [0, 2, 10], 2, 8, false)).toBe('current prompt')
48
+ })
49
+
50
+ it('hides the sticky prompt at the bottom', () => {
51
+ const messages = [
52
+ { role: 'user' as const, text: 'current prompt' },
53
+ { role: 'assistant' as const, text: 'current answer' }
54
+ ]
55
+
56
+ expect(stickyPromptFromViewport(messages, [0, 2, 10], 8, 10, true)).toBe('')
57
+ })
58
+ })
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { getScrollbarSnapshot, getViewportSnapshot, scrollbarSnapshotKey, viewportSnapshotKey } from '../lib/viewportStore.js'
4
+
5
+ describe('viewportStore', () => {
6
+ it('normalizes absent scroll handles', () => {
7
+ expect(getViewportSnapshot(null)).toEqual({
8
+ atBottom: true,
9
+ bottom: 0,
10
+ pending: 0,
11
+ scrollHeight: 0,
12
+ top: 0,
13
+ viewportHeight: 0
14
+ })
15
+ })
16
+
17
+ it('includes pending scroll delta in snapshot math and keying', () => {
18
+ const handle = {
19
+ getPendingDelta: () => 3,
20
+ getScrollHeight: () => 40,
21
+ getScrollTop: () => 10,
22
+ getViewportHeight: () => 5,
23
+ isSticky: () => false
24
+ }
25
+
26
+ const snap = getViewportSnapshot(handle as any)
27
+
28
+ expect(snap).toMatchObject({
29
+ atBottom: false,
30
+ bottom: 18,
31
+ pending: 3,
32
+ scrollHeight: 40,
33
+ top: 13,
34
+ viewportHeight: 5
35
+ })
36
+ expect(viewportSnapshotKey(snap)).toBe('0:16:5:40:3')
37
+ })
38
+
39
+ it('uses fresh scroll height to clear stale non-bottom state', () => {
40
+ const handle = {
41
+ getFreshScrollHeight: () => 20,
42
+ getPendingDelta: () => 0,
43
+ getScrollHeight: () => 40,
44
+ getScrollTop: () => 15,
45
+ getViewportHeight: () => 5,
46
+ isSticky: () => false
47
+ }
48
+
49
+ const snap = getViewportSnapshot(handle as any)
50
+
51
+ expect(snap.atBottom).toBe(true)
52
+ expect(snap.scrollHeight).toBe(20)
53
+ })
54
+
55
+ it('keeps scrollbar position tied to committed scrollTop, not pending target', () => {
56
+ const handle = {
57
+ getPendingDelta: () => 24,
58
+ getScrollHeight: () => 100,
59
+ getScrollTop: () => 10,
60
+ getViewportHeight: () => 20,
61
+ isSticky: () => false
62
+ }
63
+
64
+ const viewport = getViewportSnapshot(handle as any)
65
+ const scrollbar = getScrollbarSnapshot(handle as any)
66
+
67
+ expect(viewport.top).toBe(34)
68
+ expect(scrollbar).toEqual({
69
+ scrollHeight: 100,
70
+ top: 10,
71
+ viewportHeight: 20
72
+ })
73
+ expect(scrollbarSnapshotKey(scrollbar)).toBe('10:20:100')
74
+ })
75
+
76
+ it('clamps scrollbar position to committed scroll bounds', () => {
77
+ const handle = {
78
+ getScrollHeight: () => 30,
79
+ getScrollTop: () => 50,
80
+ getViewportHeight: () => 20
81
+ }
82
+
83
+ expect(getScrollbarSnapshot(handle as any).top).toBe(10)
84
+ })
85
+ })
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { estimatedMsgHeight, messageHeightKey, wrappedLines } from '../lib/virtualHeights.js'
4
+ import type { Msg } from '../types.js'
5
+
6
+ describe('virtual height estimates', () => {
7
+ it('uses stable content keys across resumed message objects', () => {
8
+ const msg: Msg = { role: 'assistant', text: 'same text', tools: ['Search Files [long message]'] }
9
+
10
+ expect(messageHeightKey(msg)).toBe(messageHeightKey({ ...msg }))
11
+ })
12
+
13
+ it('accounts for wrapping and preserved blank-block rhythm', () => {
14
+ const msg: Msg = { role: 'assistant', text: `one\n\n${'x'.repeat(90)}` }
15
+
16
+ expect(wrappedLines(msg.text, 30)).toBe(5)
17
+ expect(estimatedMsgHeight(msg, 35, { compact: false, details: false })).toBeGreaterThan(5)
18
+ })
19
+
20
+ it('uses compound user prompt width when estimating user message wrapping', () => {
21
+ const msg: Msg = { role: 'user', text: 'x'.repeat(21) }
22
+
23
+ expect(estimatedMsgHeight(msg, 26, { compact: false, details: false, userPrompt: '❯' })).toBe(3)
24
+ expect(estimatedMsgHeight(msg, 26, { compact: false, details: false, userPrompt: 'Ψ >' })).toBe(4)
25
+ })
26
+
27
+ it('includes detail sections when visible', () => {
28
+ const msg: Msg = { role: 'assistant', text: 'ok', thinking: 'line 1\nline 2', tools: ['Tool A', 'Tool B'] }
29
+
30
+ expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBeGreaterThan(
31
+ estimatedMsgHeight(msg, 80, { compact: false, details: false })
32
+ )
33
+ })
34
+
35
+ it('accounts for the response separator when assistant details are visible', () => {
36
+ const msg: Msg = { role: 'assistant', text: 'ok', thinking: 'plan' }
37
+
38
+ expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBe(
39
+ estimatedMsgHeight(msg, 80, { compact: false, details: false }) + 3
40
+ )
41
+ })
42
+
43
+ it('does not account for a response separator without visible details', () => {
44
+ const msg: Msg = { role: 'assistant', text: 'ok' }
45
+
46
+ expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBe(
47
+ estimatedMsgHeight(msg, 80, { compact: false, details: false })
48
+ )
49
+ })
50
+
51
+ it('honors per-section visibility when estimating response separators', () => {
52
+ const thinkingOnly: Msg = { role: 'assistant', text: 'ok', thinking: 'plan' }
53
+ const toolsOnly: Msg = { role: 'assistant', text: 'ok', tools: ['Tool A'] }
54
+
55
+ expect(
56
+ estimatedMsgHeight(thinkingOnly, 80, {
57
+ compact: false,
58
+ details: true,
59
+ thinkingVisible: false,
60
+ toolsVisible: true
61
+ })
62
+ ).toBe(estimatedMsgHeight(thinkingOnly, 80, { compact: false, details: false }))
63
+
64
+ expect(
65
+ estimatedMsgHeight(toolsOnly, 80, {
66
+ compact: false,
67
+ details: true,
68
+ thinkingVisible: true,
69
+ toolsVisible: false
70
+ })
71
+ ).toBe(estimatedMsgHeight(toolsOnly, 80, { compact: false, details: false }))
72
+ })
73
+
74
+ it('reserves two extra rows for the inter-turn separator on non-first user messages', () => {
75
+ const msg: Msg = { role: 'user', text: 'follow-up question' }
76
+ const base = estimatedMsgHeight(msg, 80, { compact: false, details: false })
77
+ const withSep = estimatedMsgHeight(msg, 80, { compact: false, details: false, withSeparator: true })
78
+
79
+ expect(withSep).toBe(base + 2)
80
+ })
81
+
82
+ it('caps wrapped-line counting so giant assistant turns do not block offset rebuilds', () => {
83
+ // wrappedLines is invoked once per uncached message during
84
+ // useVirtualHistory's offset rebuild. Unbounded counting on a long
85
+ // assistant response (10k+ chars × every row × every rebuild) blocks
86
+ // the UI on cold mount. Cap is ~800 rows; post-mount Yoga
87
+ // measurement converges to the true height regardless.
88
+ const giant = 'x'.repeat(1_000_000)
89
+ const t0 = performance.now()
90
+ const rows = wrappedLines(giant, 80)
91
+ const elapsed = performance.now() - t0
92
+
93
+ expect(rows).toBeLessThanOrEqual(800)
94
+ expect(elapsed).toBeLessThan(50)
95
+ })
96
+ })
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { shouldSetVirtualClamp } from '../hooks/useVirtualHistory.js'
4
+
5
+ describe('virtual history clamp bounds', () => {
6
+ it('does not clamp sticky live tail content', () => {
7
+ expect(shouldSetVirtualClamp({ itemCount: 20, sticky: true, viewportHeight: 10 })).toBe(false)
8
+ })
9
+
10
+ it('sets clamp bounds after manual scroll breaks sticky mode', () => {
11
+ expect(shouldSetVirtualClamp({ itemCount: 20, sticky: false, viewportHeight: 10 })).toBe(true)
12
+ })
13
+
14
+ it('does not clamp while a live tail is growing below virtual history', () => {
15
+ expect(shouldSetVirtualClamp({ itemCount: 20, liveTailActive: true, sticky: false, viewportHeight: 10 })).toBe(
16
+ false
17
+ )
18
+ })
19
+ })
@@ -0,0 +1,282 @@
1
+ import { PassThrough } from 'stream'
2
+
3
+ import { Box, renderSync, ScrollBox, type ScrollBoxHandle, Text } from '@nastechai/ink'
4
+ import React, { useLayoutEffect, useRef } from 'react'
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import { useVirtualHistory, virtualHistorySnapshotKey } from '../hooks/useVirtualHistory.js'
8
+
9
+ interface Item {
10
+ height: number
11
+ heightAfterResize?: number
12
+ key: string
13
+ }
14
+
15
+ interface Exposed {
16
+ scroll: ScrollBoxHandle | null
17
+ virtualHistory: ReturnType<typeof useVirtualHistory>
18
+ }
19
+
20
+ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
21
+
22
+ const makeStreams = () => {
23
+ const stdout = new PassThrough()
24
+ const stdin = new PassThrough()
25
+ const stderr = new PassThrough()
26
+
27
+ Object.assign(stdout, { columns: 80, isTTY: false, rows: 20 })
28
+ Object.assign(stdin, { isTTY: false })
29
+ Object.assign(stderr, { isTTY: false })
30
+ stdout.on('data', () => {})
31
+
32
+ return { stderr, stdin, stdout }
33
+ }
34
+
35
+ const mountedSpan = (items: readonly Item[], virtualHistory: ReturnType<typeof useVirtualHistory>) => {
36
+ let height = 0
37
+
38
+ for (let index = virtualHistory.start; index < virtualHistory.end; index++) {
39
+ height += items[index]?.height ?? 0
40
+ }
41
+
42
+ return { bottom: virtualHistory.topSpacer + height, top: virtualHistory.topSpacer }
43
+ }
44
+
45
+ const viewportIsMounted = (items: readonly Item[], virtualHistory: ReturnType<typeof useVirtualHistory>, scroll: ScrollBoxHandle) => {
46
+ const span = mountedSpan(items, virtualHistory)
47
+ const top = scroll.getScrollTop()
48
+ const bottom = top + scroll.getViewportHeight()
49
+
50
+ return top >= span.top && bottom <= span.bottom
51
+ }
52
+
53
+ const itemHeightForColumns = (item: Item | undefined, columns: number) =>
54
+ columns >= 80 ? (item?.heightAfterResize ?? item?.height ?? 1) : (item?.height ?? 1)
55
+
56
+ function Harness({
57
+ columns = 80,
58
+ expose,
59
+ height = 10,
60
+ items,
61
+ maxMounted = 16
62
+ }: {
63
+ columns?: number
64
+ expose: React.MutableRefObject<Exposed | null>
65
+ height?: number
66
+ items: readonly Item[]
67
+ maxMounted?: number
68
+ }) {
69
+ const scrollRef = useRef<ScrollBoxHandle | null>(null)
70
+
71
+ const virtualHistory = useVirtualHistory(scrollRef, items, columns, {
72
+ coldStartCount: 16,
73
+ estimateHeight: index => itemHeightForColumns(items[index], columns),
74
+ maxMounted,
75
+ overscan: 2
76
+ })
77
+
78
+ useLayoutEffect(() => {
79
+ expose.current = { scroll: scrollRef.current, virtualHistory }
80
+ })
81
+
82
+ return React.createElement(
83
+ ScrollBox,
84
+ { flexDirection: 'column', height, ref: scrollRef, stickyScroll: true },
85
+ React.createElement(
86
+ Box,
87
+ { flexDirection: 'column', width: '100%' },
88
+ virtualHistory.topSpacer > 0 ? React.createElement(Box, { height: virtualHistory.topSpacer }) : null,
89
+ ...items
90
+ .slice(virtualHistory.start, virtualHistory.end)
91
+ .map(item =>
92
+ React.createElement(
93
+ Box,
94
+ {
95
+ height: itemHeightForColumns(item, columns),
96
+ key: item.key,
97
+ ref: virtualHistory.measureRef(item.key)
98
+ },
99
+ React.createElement(Text, null, item.key)
100
+ )
101
+ ),
102
+ virtualHistory.bottomSpacer > 0 ? React.createElement(Box, { height: virtualHistory.bottomSpacer }) : null
103
+ )
104
+ )
105
+ }
106
+
107
+ describe('useVirtualHistory offset cache reuse', () => {
108
+ it('includes viewport height in the external-store snapshot key', () => {
109
+ const base = {
110
+ getPendingDelta: () => 0,
111
+ getScrollTop: () => 20,
112
+ isSticky: () => false
113
+ }
114
+
115
+ const short = virtualHistorySnapshotKey({
116
+ ...base,
117
+ getViewportHeight: () => 5
118
+ } as ScrollBoxHandle)
119
+
120
+ const tall = virtualHistorySnapshotKey({
121
+ ...base,
122
+ getViewportHeight: () => 25
123
+ } as ScrollBoxHandle)
124
+
125
+ expect(short).not.toBe(tall)
126
+ })
127
+
128
+ it('remounts enough tail rows after the scroll viewport grows', async () => {
129
+ const items = Array.from({ length: 100 }, (_, index) => ({ height: 1, key: `item-${index}` }))
130
+ const expose = { current: null as Exposed | null }
131
+ const streams = makeStreams()
132
+
133
+ const instance = renderSync(React.createElement(Harness, { expose, height: 4, items, maxMounted: 80 }), {
134
+ patchConsole: false,
135
+ stderr: streams.stderr as NodeJS.WriteStream,
136
+ stdin: streams.stdin as NodeJS.ReadStream,
137
+ stdout: streams.stdout as NodeJS.WriteStream
138
+ })
139
+
140
+ try {
141
+ await delay(20)
142
+ instance.rerender(React.createElement(Harness, { expose, height: 9, items, maxMounted: 80 }))
143
+ await delay(80)
144
+
145
+ expect(viewportIsMounted(items, expose.current!.virtualHistory, expose.current!.scroll!)).toBe(true)
146
+ } finally {
147
+ instance.unmount()
148
+ instance.cleanup()
149
+ }
150
+ })
151
+
152
+ it('recomputes tail coverage when wrapped rows shrink after a width resize', async () => {
153
+ const items = Array.from({ length: 100 }, (_, index) => ({
154
+ height: 4,
155
+ heightAfterResize: 1,
156
+ key: `item-${index}`
157
+ }))
158
+
159
+ const expose = { current: null as Exposed | null }
160
+ const streams = makeStreams()
161
+
162
+ const instance = renderSync(
163
+ React.createElement(Harness, { columns: 40, expose, height: 10, items, maxMounted: 80 }),
164
+ {
165
+ patchConsole: false,
166
+ stderr: streams.stderr as NodeJS.WriteStream,
167
+ stdin: streams.stdin as NodeJS.ReadStream,
168
+ stdout: streams.stdout as NodeJS.WriteStream
169
+ }
170
+ )
171
+
172
+ try {
173
+ await delay(20)
174
+ instance.rerender(React.createElement(Harness, { columns: 80, expose, height: 10, items, maxMounted: 80 }))
175
+ await delay(80)
176
+
177
+ const resizedItems = items.map(item => ({ height: item.heightAfterResize!, key: item.key }))
178
+
179
+ expect(viewportIsMounted(resizedItems, expose.current!.virtualHistory, expose.current!.scroll!)).toBe(true)
180
+ } finally {
181
+ instance.unmount()
182
+ instance.cleanup()
183
+ }
184
+ })
185
+
186
+ it('keeps sticky scroll at the bottom when one tall tail row resizes', async () => {
187
+ const items = [{ height: 90, heightAfterResize: 50, key: 'tail' }]
188
+ const expose = { current: null as Exposed | null }
189
+ const streams = makeStreams()
190
+
191
+ const instance = renderSync(
192
+ React.createElement(Harness, { columns: 70, expose, height: 18, items, maxMounted: 80 }),
193
+ {
194
+ patchConsole: false,
195
+ stderr: streams.stderr as NodeJS.WriteStream,
196
+ stdin: streams.stdin as NodeJS.ReadStream,
197
+ stdout: streams.stdout as NodeJS.WriteStream
198
+ }
199
+ )
200
+
201
+ try {
202
+ await delay(20)
203
+ instance.rerender(React.createElement(Harness, { columns: 120, expose, height: 36, items, maxMounted: 80 }))
204
+ await delay(80)
205
+
206
+ const scroll = expose.current!.scroll!
207
+
208
+ expect(scroll.getScrollTop()).toBe(scroll.getScrollHeight() - scroll.getViewportHeight())
209
+ } finally {
210
+ instance.unmount()
211
+ instance.cleanup()
212
+ }
213
+ })
214
+
215
+ it('recomputes offsets after a mounted row height changes', async () => {
216
+ const tall = [
217
+ { height: 6, key: 'a' },
218
+ { height: 6, key: 'b' },
219
+ { height: 6, key: 'c' }
220
+ ]
221
+
222
+ const short = tall.map(item => ({ ...item, height: 2 }))
223
+ const expose = { current: null as Exposed | null }
224
+ const streams = makeStreams()
225
+
226
+ const instance = renderSync(React.createElement(Harness, { expose, items: tall }), {
227
+ patchConsole: false,
228
+ stderr: streams.stderr as NodeJS.WriteStream,
229
+ stdin: streams.stdin as NodeJS.ReadStream,
230
+ stdout: streams.stdout as NodeJS.WriteStream
231
+ })
232
+
233
+ try {
234
+ await delay(20)
235
+ expect(expose.current!.virtualHistory.offsets[tall.length]).toBe(18)
236
+
237
+ instance.rerender(React.createElement(Harness, { expose, items: short }))
238
+ await delay(40)
239
+
240
+ expect(expose.current!.virtualHistory.offsets[short.length]).toBe(6)
241
+ expect(expose.current!.virtualHistory.bottomSpacer).toBe(0)
242
+ } finally {
243
+ instance.unmount()
244
+ instance.cleanup()
245
+ }
246
+ })
247
+
248
+ it('ignores stale reused offset-array entries after the item count shrinks', async () => {
249
+ const beforeShrink = Array.from({ length: 1400 }, (_, index) => ({ height: 1, key: `old${index}` }))
250
+ const afterShrink = Array.from({ length: 800 }, (_, index) => ({ height: 7, key: `new${index}` }))
251
+ const expose = { current: null as Exposed | null }
252
+ const streams = makeStreams()
253
+
254
+ const instance = renderSync(React.createElement(Harness, { expose, items: beforeShrink }), {
255
+ patchConsole: false,
256
+ stderr: streams.stderr as NodeJS.WriteStream,
257
+ stdin: streams.stdin as NodeJS.ReadStream,
258
+ stdout: streams.stdout as NodeJS.WriteStream
259
+ })
260
+
261
+ try {
262
+ await delay(20)
263
+ instance.rerender(React.createElement(Harness, { expose, items: afterShrink }))
264
+ await delay(20)
265
+
266
+ const scroll = expose.current!.scroll!
267
+ const transcriptHeight = expose.current!.virtualHistory.offsets[afterShrink.length] ?? 0
268
+
269
+ expect(transcriptHeight).toBe(5600)
270
+ expect(scroll.getScrollTop()).toBe(transcriptHeight - scroll.getViewportHeight())
271
+
272
+ scroll.scrollBy(-1)
273
+ await delay(80)
274
+
275
+ expect(scroll.getPendingDelta()).toBe(0)
276
+ expect(viewportIsMounted(afterShrink, expose.current!.virtualHistory, scroll)).toBe(true)
277
+ } finally {
278
+ instance.unmount()
279
+ instance.cleanup()
280
+ }
281
+ })
282
+ })
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { computeWheelStep, initWheelAccel } from '../lib/wheelAccel.js'
4
+
5
+ describe('wheelAccel — native path', () => {
6
+ it('first click after init returns base', () => {
7
+ const s = initWheelAccel(false, 1)
8
+
9
+ expect(computeWheelStep(s, 1, 1000)).toBe(1)
10
+ })
11
+
12
+ it('same-direction fast events ramp mult (window-mode)', () => {
13
+ const s = initWheelAccel(false, 1)
14
+
15
+ computeWheelStep(s, 1, 1000)
16
+ computeWheelStep(s, 1, 1020)
17
+ computeWheelStep(s, 1, 1040)
18
+
19
+ // Key property: doesn't shrink below base.
20
+ expect(computeWheelStep(s, 1, 1060)).toBeGreaterThanOrEqual(1)
21
+ })
22
+
23
+ it('gap beyond window resets mult to base', () => {
24
+ const s = initWheelAccel(false, 1)
25
+
26
+ for (let t = 1000; t < 1100; t += 20) {
27
+ computeWheelStep(s, 1, t)
28
+ }
29
+
30
+ expect(computeWheelStep(s, 1, 2000)).toBe(1)
31
+ })
32
+
33
+ it('direction flip defers one event for bounce detection', () => {
34
+ const s = initWheelAccel(false, 1)
35
+
36
+ computeWheelStep(s, 1, 1000)
37
+
38
+ expect(computeWheelStep(s, -1, 1050)).toBe(0)
39
+ })
40
+
41
+ it('flip-back within bounce window engages wheelMode', () => {
42
+ const s = initWheelAccel(false, 1)
43
+
44
+ computeWheelStep(s, 1, 1000)
45
+ computeWheelStep(s, -1, 1050)
46
+ computeWheelStep(s, 1, 1100)
47
+
48
+ expect(s.wheelMode).toBe(true)
49
+ })
50
+
51
+ it('flip-back outside bounce window is a real reversal (no wheelMode)', () => {
52
+ const s = initWheelAccel(false, 1)
53
+
54
+ computeWheelStep(s, 1, 1000)
55
+ computeWheelStep(s, -1, 1050)
56
+ computeWheelStep(s, 1, 1400)
57
+
58
+ expect(s.wheelMode).toBe(false)
59
+ })
60
+
61
+ it('5 consecutive sub-5ms events disengage wheelMode (trackpad signature)', () => {
62
+ const s = initWheelAccel(false, 1)
63
+ s.wheelMode = true
64
+ s.dir = 1
65
+ s.time = 1000
66
+
67
+ for (let t = 1002; t <= 1010; t += 2) {
68
+ computeWheelStep(s, 1, t)
69
+ }
70
+
71
+ expect(s.wheelMode).toBe(false)
72
+ })
73
+
74
+ it('1.5s idle disengages wheelMode', () => {
75
+ const s = initWheelAccel(false, 1)
76
+ s.wheelMode = true
77
+ s.dir = 1
78
+ s.time = 1000
79
+
80
+ computeWheelStep(s, 1, 3000)
81
+
82
+ expect(s.wheelMode).toBe(false)
83
+ })
84
+ })
85
+
86
+ describe('wheelAccel — xterm.js path', () => {
87
+ it('first click returns 2 after long idle', () => {
88
+ const s = initWheelAccel(true, 1)
89
+
90
+ expect(computeWheelStep(s, 1, 1000)).toBeGreaterThanOrEqual(1)
91
+ })
92
+
93
+ it('sub-5ms burst returns 1 (same-direction, same-batch)', () => {
94
+ const s = initWheelAccel(true, 1)
95
+
96
+ computeWheelStep(s, 1, 1000)
97
+
98
+ expect(computeWheelStep(s, 1, 1002)).toBe(1)
99
+ })
100
+
101
+ it('slow steady scroll stays in precision range', () => {
102
+ const s = initWheelAccel(true, 1)
103
+
104
+ for (let t = 1000; t < 2000; t += 33) {
105
+ const r = computeWheelStep(s, 1, t)
106
+
107
+ expect(r).toBeGreaterThanOrEqual(1)
108
+ expect(r).toBeLessThanOrEqual(6)
109
+ }
110
+ })
111
+
112
+ it('direction reversal resets mult', () => {
113
+ const s = initWheelAccel(true, 1)
114
+
115
+ for (let t = 1000; t < 1100; t += 20) {
116
+ computeWheelStep(s, 1, t)
117
+ }
118
+
119
+ const beforeFlip = s.mult
120
+
121
+ computeWheelStep(s, -1, 1200)
122
+
123
+ expect(s.mult).toBeLessThanOrEqual(beforeFlip)
124
+ expect(s.mult).toBe(2)
125
+ })
126
+
127
+ it('frac stays in [0,1) across events', () => {
128
+ const s = initWheelAccel(true, 1)
129
+
130
+ // Correctness invariant of fractional carry: never negative, never reaches 1.
131
+ for (let t = 1000; t < 1200; t += 30) {
132
+ computeWheelStep(s, 1, t)
133
+
134
+ expect(s.frac).toBeGreaterThanOrEqual(0)
135
+ expect(s.frac).toBeLessThan(1)
136
+ }
137
+ })
138
+ })