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,554 @@
1
+ import type { ScrollBoxHandle } from '@nastechai/ink'
2
+ import {
3
+ type RefObject,
4
+ useCallback,
5
+ useDeferredValue,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useRef,
9
+ useState,
10
+ useSyncExternalStore
11
+ } from 'react'
12
+
13
+ const ESTIMATE = 4
14
+ // Overscan was 40 (= viewport) which is way more than needed when heights
15
+ // are well-estimated. Cutting in half saves ~20 mounted items per scroll
16
+ // edge → smaller fiber tree → less buffer-compose work per frame. HN/CC
17
+ // dev (https://news.ycombinator.com/item?id=46699072) confirmed GC pressure
18
+ // from large JSX trees was their main perf issue post-rewrite.
19
+ const OVERSCAN = 20
20
+ // Hard cap on mounted items. Was 260; profiling showed ~23k live Yoga
21
+ // nodes during sustained PageUp catch-up (renderer p99=106ms). The
22
+ // viewport+2*overscan = 80 rows of needed coverage = ~25 items at avg 3
23
+ // rows/item, so 120 leaves >4× headroom and never blanks the viewport
24
+ // even when items are tiny.
25
+ const MAX_MOUNTED = 120
26
+ const COLD_START = 30
27
+ // Floor on unmeasured row height used when computing coverage — guarantees
28
+ // the mounted span physically reaches the viewport bottom regardless of how
29
+ // small items actually are (at the cost of over-mounting when items are
30
+ // larger; overscan absorbs that).
31
+ const PESSIMISTIC = 1
32
+ // Tightest safe scrollTop bin for the useSyncExternalStore snapshot. Small
33
+ // wheel ticks that don't cross a bin short-circuit React's commit entirely;
34
+ // Ink keeps painting via ScrollBox.forceRender + direct scrollTop reads.
35
+ // Half of OVERSCAN keeps ≥20 rows of cushion before the mounted range
36
+ // would actually need to shift.
37
+ const QUANTUM = OVERSCAN >> 1
38
+ // Renders to keep the mount range frozen after width change (heights scaled
39
+ // but not yet re-measured). Render #1 skips measurement so pre-resize Yoga
40
+ // doesn't poison the scaled cache; render #2's useLayoutEffect captures
41
+ // post-resize heights; render #3 recomputes range with accurate data.
42
+ const FREEZE_RENDERS = 2
43
+ // Cap on NEW items mounted per commit when scrolling fast. Without this,
44
+ // a single PageUp into unmeasured territory mounts ~190 rows with
45
+ // PESSIMISTIC=1 coverage — each row running marked lexer + syntax
46
+ // highlighting for ~3ms = ~600ms sync block. Sliding toward the target
47
+ // over several commits keeps per-commit mount cost bounded. Tightened
48
+ // from 25 → 12: each new item adds ~100 fibers / Yoga nodes, and a
49
+ // 25-item commit was the dominant contributor to the 100ms+ p99 frames.
50
+ const SLIDE_STEP = 12
51
+
52
+ const NOOP = () => {}
53
+
54
+ export const virtualHistorySnapshotKey = (s?: ScrollBoxHandle | null): string => {
55
+ if (!s) {
56
+ return 'none'
57
+ }
58
+
59
+ const target = s.getScrollTop() + s.getPendingDelta()
60
+ const bin = Math.floor(target / QUANTUM)
61
+ const viewportHeight = Math.max(0, s.getViewportHeight())
62
+
63
+ return `${s.isSticky() ? ~bin : bin}:${viewportHeight}`
64
+ }
65
+
66
+ const upperBound = (arr: ArrayLike<number>, target: number, length = arr.length) => {
67
+ let lo = 0
68
+ let hi = length
69
+
70
+ while (lo < hi) {
71
+ const mid = (lo + hi) >> 1
72
+
73
+ arr[mid]! <= target ? (lo = mid + 1) : (hi = mid)
74
+ }
75
+
76
+ return lo
77
+ }
78
+
79
+ export const shouldSetVirtualClamp = ({
80
+ itemCount,
81
+ liveTailActive = false,
82
+ sticky,
83
+ viewportHeight
84
+ }: {
85
+ itemCount: number
86
+ liveTailActive?: boolean
87
+ sticky: boolean
88
+ viewportHeight: number
89
+ }) => itemCount > 0 && viewportHeight > 0 && !sticky && !liveTailActive
90
+
91
+ export const ensureVirtualItemHeight = (
92
+ heights: Map<string, number>,
93
+ key: string,
94
+ index: number,
95
+ estimate: number,
96
+ estimateHeight?: (index: number, key: string) => number
97
+ ) => {
98
+ const cached = heights.get(key)
99
+
100
+ if (cached !== undefined) {
101
+ return Math.max(1, Math.floor(cached))
102
+ }
103
+
104
+ const seeded = Math.max(1, Math.floor(estimateHeight?.(index, key) ?? estimate))
105
+ heights.set(key, seeded)
106
+
107
+ return seeded
108
+ }
109
+
110
+ export function useVirtualHistory(
111
+ scrollRef: RefObject<ScrollBoxHandle | null>,
112
+ items: readonly { key: string }[],
113
+ columns: number,
114
+ {
115
+ estimate = ESTIMATE,
116
+ estimateHeight,
117
+ initialHeights,
118
+ liveTailActive = false,
119
+ onHeightsChange,
120
+ overscan = OVERSCAN,
121
+ maxMounted = MAX_MOUNTED,
122
+ coldStartCount = COLD_START
123
+ }: VirtualHistoryOptions = {}
124
+ ) {
125
+ const nodes = useRef(new Map<string, unknown>())
126
+ const heights = useRef(new Map(initialHeights))
127
+ const initialHeightsRef = useRef(initialHeights)
128
+ const refs = useRef(new Map<string, (el: unknown) => void>())
129
+ const onHeightsChangeRef = useRef(onHeightsChange)
130
+ // Bump whenever heightCache mutates so offsets rebuild on next read.
131
+ // Ref (not state) — checked during render phase, zero extra commits.
132
+ const offsetVersion = useRef(0)
133
+
134
+ // Cached offsets: reused Float64Array keyed on (itemCount, version) so we
135
+ // only rebuild when something actually changed. Previous approach allocated
136
+ // a fresh Array(n+1) every render — at n=10k that's ~80KB/render of GC
137
+ // pressure during streaming.
138
+ const offsetsCache = useRef<{ arr: Float64Array; n: number; version: number }>({
139
+ arr: new Float64Array(0),
140
+ n: -1,
141
+ version: -1
142
+ })
143
+
144
+ const [hasScrollRef, setHasScrollRef] = useState(false)
145
+ // Height cache writes happen in layout effects; bump once so offsets and
146
+ // clamp bounds rebuild without waiting for the next scroll/input event.
147
+ const [measuredHeightVersion, bumpMeasuredHeightVersion] = useState(0)
148
+ const metrics = useRef({ sticky: true, top: 0, vp: 0 })
149
+ const lastScrollTopRef = useRef(0)
150
+
151
+ // Width change: scale cached heights by oldCols/newCols instead of clearing
152
+ // (clearing forces a pessimistic back-walk mounting ~190 rows at once, each
153
+ // a fresh marked.lexer + syntax highlight ≈ 3ms). Freeze the mount range
154
+ // for 2 renders so warm memos survive; skip one measurement pass so
155
+ // useLayoutEffect doesn't poison the scaled cache with pre-resize Yoga
156
+ // heights.
157
+ const prevColumns = useRef(columns)
158
+ const skipMeasurement = useRef(false)
159
+ const prevRange = useRef<null | readonly [number, number]>(null)
160
+ const freezeRenders = useRef(0)
161
+
162
+ onHeightsChangeRef.current = onHeightsChange
163
+
164
+ if (initialHeightsRef.current !== initialHeights) {
165
+ initialHeightsRef.current = initialHeights
166
+ heights.current = new Map(initialHeights)
167
+ offsetVersion.current++
168
+ }
169
+
170
+ if (prevColumns.current !== columns && prevColumns.current > 0 && columns > 0) {
171
+ const ratio = prevColumns.current / columns
172
+
173
+ prevColumns.current = columns
174
+
175
+ for (const [k, h] of heights.current) {
176
+ heights.current.set(k, Math.max(1, Math.round(h * ratio)))
177
+ }
178
+
179
+ offsetVersion.current++
180
+ skipMeasurement.current = true
181
+ freezeRenders.current = FREEZE_RENDERS
182
+ }
183
+
184
+ useLayoutEffect(() => {
185
+ setHasScrollRef(Boolean(scrollRef.current))
186
+ }, [scrollRef])
187
+
188
+ // Quantized snapshot: same-bin scrolls (most wheel ticks) produce the same
189
+ // key → React.Object.is short-circuits the commit entirely. The key includes
190
+ // sticky state, target scroll position, and viewport height so resize-only
191
+ // changes still recompute the mounted transcript window.
192
+ const subscribe = useCallback(
193
+ (cb: () => void) => (hasScrollRef ? scrollRef.current?.subscribe(cb) : null) ?? NOOP,
194
+ [hasScrollRef, scrollRef]
195
+ )
196
+
197
+ useSyncExternalStore(
198
+ subscribe,
199
+ () => virtualHistorySnapshotKey(scrollRef.current),
200
+ () => 'none'
201
+ )
202
+
203
+ useEffect(() => {
204
+ const keep = new Set(items.map(i => i.key))
205
+ let dirty = false
206
+
207
+ for (const k of heights.current.keys()) {
208
+ if (!keep.has(k)) {
209
+ heights.current.delete(k)
210
+ nodes.current.delete(k)
211
+ refs.current.delete(k)
212
+ dirty = true
213
+ }
214
+ }
215
+
216
+ if (dirty) {
217
+ offsetVersion.current++
218
+ }
219
+ }, [items])
220
+
221
+ // Offsets: Float64Array reused across renders, invalidated by offsetVersion
222
+ // bumps from heightCache writers (measureRef, resize-scale, GC). Binary
223
+ // search tolerates either monotone source, so no need to rebuild unless
224
+ // something changed.
225
+ const n = items.length
226
+
227
+ if (offsetsCache.current.version !== offsetVersion.current || offsetsCache.current.n !== n) {
228
+ const arr = offsetsCache.current.arr.length >= n + 1 ? offsetsCache.current.arr : new Float64Array(n + 1)
229
+
230
+ arr[0] = 0
231
+
232
+ for (let i = 0; i < n; i++) {
233
+ arr[i + 1] = arr[i]! + ensureVirtualItemHeight(heights.current, items[i]!.key, i, estimate, estimateHeight)
234
+ }
235
+
236
+ offsetsCache.current = { arr, n, version: offsetVersion.current }
237
+ }
238
+
239
+ const offsets = offsetsCache.current.arr
240
+ const total = offsets[n] ?? 0
241
+ const top = Math.max(0, scrollRef.current?.getScrollTop() ?? 0)
242
+ const pendingDelta = scrollRef.current?.getPendingDelta() ?? 0
243
+ const target = Math.max(0, top + pendingDelta)
244
+ const vp = Math.max(0, scrollRef.current?.getViewportHeight() ?? 0)
245
+ const sticky = scrollRef.current?.isSticky() ?? true
246
+ const recentManual = Date.now() - (scrollRef.current?.getLastManualScrollAt() ?? 0) < 1200
247
+
248
+ // During a freeze, drop the frozen range if items shrank past its start
249
+ // (/clear, compaction) — clamping would collapse to an empty mount and
250
+ // flash blank. Fall through to the normal path in that case.
251
+ const frozenRangeCandidate =
252
+ freezeRenders.current > 0 && prevRange.current && prevRange.current[0] < n
253
+ ? ([prevRange.current[0], Math.min(prevRange.current[1], n)] as const)
254
+ : null
255
+
256
+ // Width grows can shrink wrapped rows enough that the old tail window no
257
+ // longer covers the viewport. In that case freezing preserves stale spacers
258
+ // and visually cuts off the last message, so recompute immediately.
259
+ const frozenRange = (() => {
260
+ if (!frozenRangeCandidate || vp <= 0) {
261
+ return frozenRangeCandidate
262
+ }
263
+
264
+ const visibleTop = sticky && !recentManual ? Math.max(0, total - vp) : target
265
+ const visibleBottom = visibleTop + vp
266
+ const rangeTop = offsets[frozenRangeCandidate[0]] ?? 0
267
+ const rangeBottom = offsets[frozenRangeCandidate[1]] ?? total
268
+
269
+ return rangeTop <= visibleTop && rangeBottom >= visibleBottom ? frozenRangeCandidate : null
270
+ })()
271
+
272
+ let start = 0
273
+ let end = n
274
+
275
+ if (frozenRange) {
276
+ start = frozenRange[0]
277
+ end = Math.min(frozenRange[1], n)
278
+ } else if (n > 0) {
279
+ if (vp <= 0) {
280
+ start = Math.max(0, n - coldStartCount)
281
+ } else if (sticky && !recentManual) {
282
+ const budget = vp + overscan
283
+ start = n
284
+
285
+ while (start > 0 && total - offsets[start - 1]! < budget) {
286
+ start--
287
+ }
288
+ } else {
289
+ // User scrolled up. Span [committed..target] so every drain frame is
290
+ // covered. Claude-code caps the span at 3×viewport so pendingDelta
291
+ // growing unbounded (MX Master free-spin) doesn't blow the mount
292
+ // budget; the clamp (setClampBounds) shows edge-of-mounted content
293
+ // during catch-up.
294
+ const MAX_SPAN = vp * 3
295
+ const rawLo = Math.min(top, target)
296
+ const rawHi = Math.max(top, target)
297
+ const span = rawHi - rawLo
298
+ const clampedLo = span > MAX_SPAN ? (pendingDelta < 0 ? rawHi - MAX_SPAN : rawLo) : rawLo
299
+ const clampedHi = clampedLo + Math.min(span, MAX_SPAN)
300
+ const lo = Math.max(0, clampedLo - overscan)
301
+ const hi = clampedHi + vp + overscan
302
+
303
+ // Binary search — offsets is monotone. Linear walk was O(n) at n=10k+,
304
+ // ~2ms per render during scroll.
305
+ start = Math.max(0, Math.min(n - 1, upperBound(offsets, lo, n + 1) - 1))
306
+ end = Math.max(start + 1, Math.min(n, upperBound(offsets, hi, n + 1)))
307
+ }
308
+ }
309
+
310
+ if (end - start > maxMounted) {
311
+ sticky ? (start = Math.max(0, end - maxMounted)) : (end = Math.min(n, start + maxMounted))
312
+ }
313
+
314
+ // Coverage guarantee: ensure sum(real or pessimistic heights) ≥
315
+ // viewportH + 2*overscan so the viewport is physically covered even when
316
+ // items are tiny. Pessimistic because uncached items use a floor of 1 —
317
+ // over-mounts when items are large, never leaves blank spacer showing.
318
+ if (n > 0 && vp > 0 && !frozenRange) {
319
+ const needed = vp + 2 * overscan
320
+ let coverage = 0
321
+
322
+ for (let i = start; i < end; i++) {
323
+ coverage += ensureVirtualItemHeight(heights.current, items[i]!.key, i, PESSIMISTIC, estimateHeight)
324
+ }
325
+
326
+ if (sticky) {
327
+ const minStart = Math.max(0, end - maxMounted)
328
+
329
+ while (start > minStart && coverage < needed) {
330
+ start--
331
+ coverage += ensureVirtualItemHeight(heights.current, items[start]!.key, start, PESSIMISTIC, estimateHeight)
332
+ }
333
+ } else {
334
+ const maxEnd = Math.min(n, start + maxMounted)
335
+
336
+ while (end < maxEnd && coverage < needed) {
337
+ coverage += ensureVirtualItemHeight(heights.current, items[end]!.key, end, PESSIMISTIC, estimateHeight)
338
+ end++
339
+ }
340
+ }
341
+ }
342
+
343
+ // Slide cap: limit how many NEW items mount this commit. Gates on scroll
344
+ // VELOCITY (|scrollTop delta since last commit| + |pendingDelta| >
345
+ // 2×viewport — key-repeat PageUp moves ~viewport/2 per press). Covers
346
+ // both scrollBy (pendingDelta) and scrollTo (direct write). Normal single
347
+ // PageUp skips this; the clamp holds the viewport at the mounted edge
348
+ // during catch-up so there's no blank screen. Only caps range GROWTH;
349
+ // shrinking is unbounded.
350
+ if (!frozenRange && prevRange.current && vp > 0) {
351
+ const velocity = Math.abs(top - lastScrollTopRef.current) + Math.abs(pendingDelta)
352
+
353
+ if (velocity > vp * 2) {
354
+ const [pS, pE] = prevRange.current
355
+
356
+ start = Math.max(start, pS - SLIDE_STEP)
357
+ end = Math.min(end, pE + SLIDE_STEP)
358
+
359
+ // A large jump past the capped end can invert (start > end); mount
360
+ // SLIDE_STEP items from the new start so the viewport isn't blank
361
+ // during catch-up.
362
+ if (start > end) {
363
+ end = Math.min(start + SLIDE_STEP, n)
364
+ }
365
+ }
366
+ }
367
+
368
+ lastScrollTopRef.current = top
369
+
370
+ if (freezeRenders.current > 0) {
371
+ freezeRenders.current--
372
+ } else {
373
+ prevRange.current = [start, end]
374
+ }
375
+
376
+ // Time-slice range growth via useDeferredValue. Urgent render keeps Ink
377
+ // painting with the OLD range (all memo hits, fast); deferred render
378
+ // transitions to the NEW range (fresh mounts: Md, syntax highlight) in a
379
+ // non-blocking background commit. The clamp (setClampBounds) pins the
380
+ // viewport to the mounted edge so there's no visual artifact from the
381
+ // deferred range lagging briefly. Only deferral range GROWTH — shrinking
382
+ // is cheap (unmount = remove fiber, no parse).
383
+ const dStart = useDeferredValue(start)
384
+ const dEnd = useDeferredValue(end)
385
+ let effStart = start < dStart ? dStart : start
386
+ let effEnd = end > dEnd ? dEnd : end
387
+
388
+ // Inverted range (large jump with deferred value lagging) or sticky snap
389
+ // (scrollToBottom needs the tail mounted NOW so maxScroll lands on content,
390
+ // not bottomSpacer) — skip deferral.
391
+ if (effStart > effEnd || sticky) {
392
+ effStart = start
393
+ effEnd = end
394
+ }
395
+
396
+ // Scrolling DOWN — bypass effEnd deferral so the tail mounts immediately.
397
+ // Without this, the clamp holds scrollTop short of the real bottom and
398
+ // the user feels "stuck before bottom". effStart stays deferred so scroll-
399
+ // UP keeps time-slicing (older messages parse on mount).
400
+ if (pendingDelta > 0) {
401
+ effEnd = end
402
+ }
403
+
404
+ // Final O(viewport) enforcement. Deferred+bypass combinations above can
405
+ // leak: during sustained PageUp, concurrent mode interleaves dStart updates
406
+ // with effEnd=end bypasses across commits and the effective window drifts
407
+ // wider than either bound alone. Trim the far edge by viewport position
408
+ // (not pendingDelta direction — that flips mid-settle under concurrent
409
+ // scheduling and yanks scrollTop).
410
+ if (effEnd - effStart > maxMounted && vp > 0) {
411
+ const mid = (offsets[effStart]! + offsets[effEnd]!) / 2
412
+
413
+ if (top < mid) {
414
+ effEnd = effStart + maxMounted
415
+ } else {
416
+ effStart = effEnd - maxMounted
417
+ }
418
+ }
419
+
420
+ const measureRef = useCallback((key: string) => {
421
+ let fn = refs.current.get(key)
422
+
423
+ if (!fn) {
424
+ fn = (el: unknown) => {
425
+ if (el) {
426
+ nodes.current.set(key, el)
427
+
428
+ return
429
+ }
430
+
431
+ // Measure-at-unmount: the yogaNode is still valid here (reconciler
432
+ // calls ref(null) before removeChild → freeRecursive), so we grab
433
+ // the final height before WASM release. Without this, items
434
+ // scrolled out during fast pan keep a stale estimate in heightCache
435
+ // and offset math drifts until the next mount/remount cycle.
436
+ const existing = nodes.current.get(key) as MeasuredNode | undefined
437
+ const h = Math.ceil(existing?.yogaNode?.getComputedHeight?.() ?? 0)
438
+
439
+ if (h > 0 && heights.current.get(key) !== h) {
440
+ heights.current.set(key, h)
441
+ offsetVersion.current++
442
+ onHeightsChangeRef.current?.(heights.current)
443
+ }
444
+
445
+ nodes.current.delete(key)
446
+ }
447
+
448
+ refs.current.set(key, fn)
449
+ }
450
+
451
+ return fn
452
+ }, [])
453
+
454
+ useLayoutEffect(() => {
455
+ const s = scrollRef.current
456
+ let dirty = false
457
+ let heightDirty = false
458
+
459
+ // Give the renderer the mounted-row coverage for passive scroll clamping.
460
+ // Clamp MUST use the EFFECTIVE (deferred) range, not the immediate one.
461
+ // During fast scroll, immediate [start,end] may already cover the new
462
+ // scrollTop position, but children still render at the deferred range.
463
+ // If clamp used immediate bounds, render-node-to-output's drain-gate
464
+ // would drain past the deferred children's span → viewport lands in
465
+ // spacer → white flash.
466
+ if (s && shouldSetVirtualClamp({ itemCount: n, liveTailActive, sticky, viewportHeight: vp })) {
467
+ const effTopSpacer = offsets[effStart] ?? 0
468
+ const effBottom = offsets[effEnd] ?? total
469
+ // At effEnd=n there's no bottomSpacer — use Infinity so render-node-
470
+ // to-output's own Math.min(cur, maxScroll) governs. Using offsets[n]
471
+ // here would bake in heightCache (one render behind Yoga), and during
472
+ // streaming the tail item's cached height lags its real height —
473
+ // sticky-break would then clamp below the real max and push
474
+ // streaming text off-viewport.
475
+ const clampMin = effStart === 0 ? 0 : effTopSpacer
476
+ const clampMax = effEnd === n ? Infinity : Math.max(effTopSpacer, effBottom - vp)
477
+
478
+ s.setClampBounds(clampMin, clampMax)
479
+ } else {
480
+ s?.setClampBounds(undefined, undefined)
481
+ }
482
+
483
+ if (skipMeasurement.current) {
484
+ skipMeasurement.current = false
485
+ bumpMeasuredHeightVersion(n => n + 1)
486
+ } else {
487
+ for (let i = effStart; i < effEnd; i++) {
488
+ const k = items[i]?.key
489
+
490
+ if (!k) {
491
+ continue
492
+ }
493
+
494
+ const h = Math.ceil((nodes.current.get(k) as MeasuredNode | undefined)?.yogaNode?.getComputedHeight?.() ?? 0)
495
+
496
+ if (h > 0 && heights.current.get(k) !== h) {
497
+ heights.current.set(k, h)
498
+ dirty = true
499
+ heightDirty = true
500
+ }
501
+ }
502
+ }
503
+
504
+ if (s) {
505
+ const next = {
506
+ sticky: s.isSticky(),
507
+ top: Math.max(0, s.getScrollTop() + s.getPendingDelta()),
508
+ vp: Math.max(0, s.getViewportHeight())
509
+ }
510
+
511
+ if (
512
+ next.sticky !== metrics.current.sticky ||
513
+ next.top !== metrics.current.top ||
514
+ next.vp !== metrics.current.vp
515
+ ) {
516
+ metrics.current = next
517
+ dirty = true
518
+ }
519
+ }
520
+
521
+ if (dirty) {
522
+ offsetVersion.current++
523
+ onHeightsChangeRef.current?.(heights.current)
524
+ }
525
+
526
+ if (heightDirty) {
527
+ bumpMeasuredHeightVersion(n => n + 1)
528
+ }
529
+ }, [effEnd, effStart, items, liveTailActive, measuredHeightVersion, n, offsets, scrollRef, sticky, total, vp])
530
+
531
+ return {
532
+ bottomSpacer: Math.max(0, total - (offsets[effEnd] ?? total)),
533
+ end: effEnd,
534
+ measureRef,
535
+ offsets,
536
+ start: effStart,
537
+ topSpacer: offsets[effStart] ?? 0
538
+ }
539
+ }
540
+
541
+ interface MeasuredNode {
542
+ yogaNode?: { getComputedHeight?: () => number } | null
543
+ }
544
+
545
+ interface VirtualHistoryOptions {
546
+ coldStartCount?: number
547
+ estimate?: number
548
+ estimateHeight?: (index: number, key: string) => number
549
+ initialHeights?: ReadonlyMap<string, number>
550
+ liveTailActive?: boolean
551
+ maxMounted?: number
552
+ onHeightsChange?: (heights: ReadonlyMap<string, number>) => void
553
+ overscan?: number
554
+ }
@@ -0,0 +1,48 @@
1
+ export class CircularBuffer<T> {
2
+ private buf: T[]
3
+ private head = 0
4
+ private len = 0
5
+
6
+ constructor(private capacity: number) {
7
+ if (!Number.isInteger(capacity) || capacity <= 0) {
8
+ throw new RangeError(`CircularBuffer capacity must be a positive integer, got ${capacity}`)
9
+ }
10
+
11
+ this.buf = new Array<T>(capacity)
12
+ }
13
+
14
+ push(item: T) {
15
+ this.buf[this.head] = item
16
+ this.head = (this.head + 1) % this.capacity
17
+
18
+ if (this.len < this.capacity) {
19
+ this.len++
20
+ }
21
+ }
22
+
23
+ tail(n = this.len): T[] {
24
+ const take = Math.min(Math.max(0, n), this.len)
25
+ const start = this.len < this.capacity ? 0 : this.head
26
+ const out: T[] = new Array<T>(take)
27
+
28
+ for (let i = 0; i < take; i++) {
29
+ out[i] = this.buf[(start + this.len - take + i) % this.capacity]!
30
+ }
31
+
32
+ return out
33
+ }
34
+
35
+ drain(): T[] {
36
+ const out = this.tail()
37
+
38
+ this.clear()
39
+
40
+ return out
41
+ }
42
+
43
+ clear() {
44
+ this.buf = new Array<T>(this.capacity)
45
+ this.head = 0
46
+ this.len = 0
47
+ }
48
+ }