@butlerw/vellum 0.1.0

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 (446) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/__fixtures__/responses/code-generation.json +42 -0
  4. package/__fixtures__/responses/error-response.json +20 -0
  5. package/__fixtures__/responses/hello-world.json +32 -0
  6. package/dist/auth-6MCXESOH.js +26 -0
  7. package/dist/chunk-SECXJGWA.js +597 -0
  8. package/dist/index.js +34023 -0
  9. package/package.json +67 -0
  10. package/src/__tests__/commands.e2e.test.ts +728 -0
  11. package/src/__tests__/credentials.test.ts +713 -0
  12. package/src/__tests__/mode-e2e.test.ts +391 -0
  13. package/src/__tests__/tui-integration.test.tsx +1271 -0
  14. package/src/agents/__tests__/task-persistence.test.ts +235 -0
  15. package/src/agents/commands/delegate.ts +240 -0
  16. package/src/agents/commands/index.ts +10 -0
  17. package/src/agents/commands/resume.ts +335 -0
  18. package/src/agents/index.ts +29 -0
  19. package/src/agents/task-persistence.ts +272 -0
  20. package/src/agents/task-resumption.ts +242 -0
  21. package/src/app.tsx +4737 -0
  22. package/src/commands/__tests__/.gitkeep +1 -0
  23. package/src/commands/__tests__/agents.test.ts +606 -0
  24. package/src/commands/__tests__/auth.test.ts +626 -0
  25. package/src/commands/__tests__/autocomplete.test.ts +683 -0
  26. package/src/commands/__tests__/batch.test.ts +287 -0
  27. package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
  28. package/src/commands/__tests__/completion.test.ts +238 -0
  29. package/src/commands/__tests__/core.test.ts +363 -0
  30. package/src/commands/__tests__/executor.test.ts +496 -0
  31. package/src/commands/__tests__/exit-codes.test.ts +220 -0
  32. package/src/commands/__tests__/init.test.ts +243 -0
  33. package/src/commands/__tests__/language.test.ts +353 -0
  34. package/src/commands/__tests__/mode-cli.test.ts +667 -0
  35. package/src/commands/__tests__/model.test.ts +277 -0
  36. package/src/commands/__tests__/parser.test.ts +493 -0
  37. package/src/commands/__tests__/performance.bench.ts +380 -0
  38. package/src/commands/__tests__/registry.test.ts +534 -0
  39. package/src/commands/__tests__/resume.test.ts +449 -0
  40. package/src/commands/__tests__/security.test.ts +845 -0
  41. package/src/commands/__tests__/stream-json.test.ts +372 -0
  42. package/src/commands/__tests__/user-commands.test.ts +597 -0
  43. package/src/commands/adapters.ts +267 -0
  44. package/src/commands/agent.ts +395 -0
  45. package/src/commands/agents/generate.ts +506 -0
  46. package/src/commands/agents/index.ts +272 -0
  47. package/src/commands/agents/show.ts +271 -0
  48. package/src/commands/agents/validate.ts +387 -0
  49. package/src/commands/auth.ts +883 -0
  50. package/src/commands/autocomplete.ts +480 -0
  51. package/src/commands/batch/command.ts +388 -0
  52. package/src/commands/batch/executor.ts +361 -0
  53. package/src/commands/batch/index.ts +12 -0
  54. package/src/commands/commit.ts +235 -0
  55. package/src/commands/completion/index.ts +371 -0
  56. package/src/commands/condense.ts +191 -0
  57. package/src/commands/config.ts +344 -0
  58. package/src/commands/context-provider.ts +173 -0
  59. package/src/commands/copy.ts +329 -0
  60. package/src/commands/core/clear.ts +38 -0
  61. package/src/commands/core/exit.ts +43 -0
  62. package/src/commands/core/help.ts +354 -0
  63. package/src/commands/core/index.ts +15 -0
  64. package/src/commands/cost.ts +179 -0
  65. package/src/commands/credentials.tsx +618 -0
  66. package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
  67. package/src/commands/custom-agents/create.ts +377 -0
  68. package/src/commands/custom-agents/export.ts +135 -0
  69. package/src/commands/custom-agents/import.ts +199 -0
  70. package/src/commands/custom-agents/index.ts +372 -0
  71. package/src/commands/custom-agents/info.ts +318 -0
  72. package/src/commands/custom-agents/list.ts +267 -0
  73. package/src/commands/custom-agents/validate.ts +388 -0
  74. package/src/commands/diff-mode.ts +241 -0
  75. package/src/commands/env.ts +53 -0
  76. package/src/commands/executor.ts +579 -0
  77. package/src/commands/exit-codes.ts +202 -0
  78. package/src/commands/index.ts +701 -0
  79. package/src/commands/init/index.ts +15 -0
  80. package/src/commands/init/prompts.ts +366 -0
  81. package/src/commands/init/templates/commands-readme.md +80 -0
  82. package/src/commands/init/templates/example-command.md +79 -0
  83. package/src/commands/init/templates/example-skill.md +168 -0
  84. package/src/commands/init/templates/example-workflow.md +101 -0
  85. package/src/commands/init/templates/prompts-readme.md +52 -0
  86. package/src/commands/init/templates/rules-readme.md +63 -0
  87. package/src/commands/init/templates/skills-readme.md +83 -0
  88. package/src/commands/init/templates/workflows-readme.md +94 -0
  89. package/src/commands/init.ts +391 -0
  90. package/src/commands/install.ts +90 -0
  91. package/src/commands/language.ts +191 -0
  92. package/src/commands/loaders/.gitkeep +1 -0
  93. package/src/commands/lsp.ts +199 -0
  94. package/src/commands/markdown-commands.ts +253 -0
  95. package/src/commands/mcp.ts +588 -0
  96. package/src/commands/memory/export.ts +341 -0
  97. package/src/commands/memory/index.ts +148 -0
  98. package/src/commands/memory/list.ts +261 -0
  99. package/src/commands/memory/search.ts +346 -0
  100. package/src/commands/memory/utils.ts +15 -0
  101. package/src/commands/metrics.ts +75 -0
  102. package/src/commands/migrate/index.ts +16 -0
  103. package/src/commands/migrate/prompts.ts +477 -0
  104. package/src/commands/mode.ts +331 -0
  105. package/src/commands/model.ts +298 -0
  106. package/src/commands/onboard.ts +205 -0
  107. package/src/commands/open.ts +169 -0
  108. package/src/commands/output/stream-json.ts +373 -0
  109. package/src/commands/parser/chain-parser.ts +370 -0
  110. package/src/commands/parser/index.ts +29 -0
  111. package/src/commands/parser/pipe-parser.ts +480 -0
  112. package/src/commands/parser.ts +588 -0
  113. package/src/commands/persistence.ts +355 -0
  114. package/src/commands/progress.ts +18 -0
  115. package/src/commands/prompt/index.ts +17 -0
  116. package/src/commands/prompt/validate.ts +621 -0
  117. package/src/commands/prompt-priority.ts +401 -0
  118. package/src/commands/registry.ts +374 -0
  119. package/src/commands/sandbox/index.ts +131 -0
  120. package/src/commands/security/index.ts +21 -0
  121. package/src/commands/security/input-sanitizer.ts +168 -0
  122. package/src/commands/security/permission-checker.ts +456 -0
  123. package/src/commands/security/sensitive-data.ts +350 -0
  124. package/src/commands/session/delete.ts +38 -0
  125. package/src/commands/session/export.ts +39 -0
  126. package/src/commands/session/index.ts +26 -0
  127. package/src/commands/session/list.ts +26 -0
  128. package/src/commands/session/resume.ts +562 -0
  129. package/src/commands/session/search.ts +434 -0
  130. package/src/commands/session/show.ts +26 -0
  131. package/src/commands/settings.ts +368 -0
  132. package/src/commands/setup.ts +23 -0
  133. package/src/commands/shell/index.ts +16 -0
  134. package/src/commands/shell/setup.ts +422 -0
  135. package/src/commands/shell-init.ts +50 -0
  136. package/src/commands/shell-integration/index.ts +194 -0
  137. package/src/commands/skill.ts +1220 -0
  138. package/src/commands/spec.ts +558 -0
  139. package/src/commands/status.ts +246 -0
  140. package/src/commands/theme.ts +211 -0
  141. package/src/commands/think.ts +551 -0
  142. package/src/commands/trust.ts +211 -0
  143. package/src/commands/tutorial.ts +522 -0
  144. package/src/commands/types.ts +512 -0
  145. package/src/commands/update.ts +274 -0
  146. package/src/commands/usage.ts +213 -0
  147. package/src/commands/user-commands.ts +630 -0
  148. package/src/commands/utils.ts +142 -0
  149. package/src/commands/vim.ts +152 -0
  150. package/src/commands/workflow.ts +257 -0
  151. package/src/components/header.tsx +25 -0
  152. package/src/components/input.tsx +25 -0
  153. package/src/components/message-list.tsx +32 -0
  154. package/src/components/status-bar.tsx +23 -0
  155. package/src/index.tsx +614 -0
  156. package/src/onboarding/__tests__/tutorial.test.ts +740 -0
  157. package/src/onboarding/index.ts +69 -0
  158. package/src/onboarding/tips/index.ts +9 -0
  159. package/src/onboarding/tips/tip-engine.ts +459 -0
  160. package/src/onboarding/tutorial/index.ts +88 -0
  161. package/src/onboarding/tutorial/lessons/basics.ts +151 -0
  162. package/src/onboarding/tutorial/lessons/index.ts +151 -0
  163. package/src/onboarding/tutorial/lessons/modes.ts +230 -0
  164. package/src/onboarding/tutorial/lessons/tools.ts +172 -0
  165. package/src/onboarding/tutorial/progress-tracker.ts +350 -0
  166. package/src/onboarding/tutorial/storage.ts +249 -0
  167. package/src/onboarding/tutorial/tutorial-system.ts +462 -0
  168. package/src/onboarding/tutorial/types.ts +310 -0
  169. package/src/orchestrator-singleton.ts +129 -0
  170. package/src/shutdown.ts +33 -0
  171. package/src/test/e2e/assertions.ts +267 -0
  172. package/src/test/e2e/fixtures.ts +204 -0
  173. package/src/test/e2e/harness.ts +575 -0
  174. package/src/test/e2e/index.ts +57 -0
  175. package/src/test/e2e/types.ts +228 -0
  176. package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
  177. package/src/test/fixtures/fake-response-loader.ts +314 -0
  178. package/src/test/fixtures/index.ts +20 -0
  179. package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
  180. package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
  181. package/src/tui/__tests__/mode-components.test.tsx +395 -0
  182. package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
  183. package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
  184. package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
  185. package/src/tui/adapters/agent-adapter.ts +1008 -0
  186. package/src/tui/adapters/index.ts +48 -0
  187. package/src/tui/adapters/message-adapter.ts +315 -0
  188. package/src/tui/adapters/persistence-bridge.ts +331 -0
  189. package/src/tui/adapters/session-adapter.ts +419 -0
  190. package/src/tui/buffered-stdout.ts +223 -0
  191. package/src/tui/components/AgentProgress.tsx +424 -0
  192. package/src/tui/components/Banner/AsciiArt.ts +160 -0
  193. package/src/tui/components/Banner/Banner.tsx +355 -0
  194. package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
  195. package/src/tui/components/Banner/ShimmerText.tsx +193 -0
  196. package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
  197. package/src/tui/components/Banner/index.ts +61 -0
  198. package/src/tui/components/Banner/useShimmer.ts +241 -0
  199. package/src/tui/components/ChatView.tsx +11 -0
  200. package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
  201. package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
  202. package/src/tui/components/Checkpoint/index.ts +19 -0
  203. package/src/tui/components/CostDisplay.tsx +226 -0
  204. package/src/tui/components/InitErrorBanner.tsx +122 -0
  205. package/src/tui/components/Input/Autocomplete.tsx +603 -0
  206. package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
  207. package/src/tui/components/Input/HighlightedText.tsx +236 -0
  208. package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
  209. package/src/tui/components/Input/TextInput.tsx +1002 -0
  210. package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
  211. package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
  212. package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
  213. package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
  214. package/src/tui/components/Input/highlight.ts +362 -0
  215. package/src/tui/components/Input/index.ts +36 -0
  216. package/src/tui/components/Input/slash-command-utils.ts +135 -0
  217. package/src/tui/components/Layout.tsx +432 -0
  218. package/src/tui/components/McpPanel.tsx +137 -0
  219. package/src/tui/components/MemoryPanel.tsx +448 -0
  220. package/src/tui/components/Messages/CodeBlock.tsx +527 -0
  221. package/src/tui/components/Messages/DiffView.tsx +679 -0
  222. package/src/tui/components/Messages/ImageReference.tsx +89 -0
  223. package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
  224. package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
  225. package/src/tui/components/Messages/MessageBubble.tsx +270 -0
  226. package/src/tui/components/Messages/MessageList.tsx +1719 -0
  227. package/src/tui/components/Messages/StreamingText.tsx +216 -0
  228. package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
  229. package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
  230. package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
  231. package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
  232. package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
  233. package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
  234. package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
  235. package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
  236. package/src/tui/components/Messages/index.ts +25 -0
  237. package/src/tui/components/ModeIndicator.tsx +177 -0
  238. package/src/tui/components/ModeSelector.tsx +216 -0
  239. package/src/tui/components/ModelSelector.tsx +339 -0
  240. package/src/tui/components/OnboardingWizard.tsx +670 -0
  241. package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
  242. package/src/tui/components/RateLimitIndicator.tsx +82 -0
  243. package/src/tui/components/ScreenReaderLayout.tsx +295 -0
  244. package/src/tui/components/SettingsPanel.tsx +643 -0
  245. package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
  246. package/src/tui/components/Sidebar/index.ts +9 -0
  247. package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
  248. package/src/tui/components/Status/index.ts +12 -0
  249. package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
  250. package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
  251. package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
  252. package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
  253. package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
  254. package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
  255. package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
  256. package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
  257. package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
  258. package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
  259. package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
  260. package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
  261. package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
  262. package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
  263. package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
  264. package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
  265. package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
  266. package/src/tui/components/StatusBar/index.ts +60 -0
  267. package/src/tui/components/TipBanner.tsx +115 -0
  268. package/src/tui/components/TodoItem.tsx +208 -0
  269. package/src/tui/components/TodoPanel.tsx +455 -0
  270. package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
  271. package/src/tui/components/Tools/OptionSelector.tsx +160 -0
  272. package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
  273. package/src/tui/components/Tools/ToolParams.tsx +483 -0
  274. package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
  275. package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
  276. package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
  277. package/src/tui/components/Tools/index.ts +21 -0
  278. package/src/tui/components/TrustPrompt.tsx +279 -0
  279. package/src/tui/components/UpdateBanner.tsx +166 -0
  280. package/src/tui/components/VimModeIndicator.tsx +112 -0
  281. package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
  282. package/src/tui/components/backtrack/index.ts +13 -0
  283. package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
  284. package/src/tui/components/common/CostWarning.tsx +294 -0
  285. package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
  286. package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
  287. package/src/tui/components/common/ErrorBoundary.tsx +140 -0
  288. package/src/tui/components/common/GradientText.tsx +224 -0
  289. package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
  290. package/src/tui/components/common/HotkeyHints.tsx +70 -0
  291. package/src/tui/components/common/MaxSizedBox.tsx +354 -0
  292. package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
  293. package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
  294. package/src/tui/components/common/ScrollIndicator.tsx +160 -0
  295. package/src/tui/components/common/Spinner.tsx +342 -0
  296. package/src/tui/components/common/StreamingIndicator.tsx +316 -0
  297. package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
  298. package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
  299. package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
  300. package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
  301. package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
  302. package/src/tui/components/common/VirtualizedList/index.ts +30 -0
  303. package/src/tui/components/common/VirtualizedList/types.ts +107 -0
  304. package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
  305. package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
  306. package/src/tui/components/common/index.ts +110 -0
  307. package/src/tui/components/index.ts +79 -0
  308. package/src/tui/components/session/CheckpointPanel.tsx +323 -0
  309. package/src/tui/components/session/RollbackDialog.tsx +169 -0
  310. package/src/tui/components/session/SessionItem.tsx +136 -0
  311. package/src/tui/components/session/SessionListPanel.tsx +252 -0
  312. package/src/tui/components/session/SessionPicker.tsx +449 -0
  313. package/src/tui/components/session/SessionPreview.tsx +240 -0
  314. package/src/tui/components/session/__tests__/session.test.tsx +408 -0
  315. package/src/tui/components/session/index.ts +28 -0
  316. package/src/tui/components/session/types.ts +116 -0
  317. package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
  318. package/src/tui/components/theme/index.ts +227 -0
  319. package/src/tui/components/theme/tokens.ts +484 -0
  320. package/src/tui/config/defaults.ts +134 -0
  321. package/src/tui/config/index.ts +17 -0
  322. package/src/tui/context/AnimationContext.tsx +284 -0
  323. package/src/tui/context/AppContext.tsx +349 -0
  324. package/src/tui/context/BracketedPasteContext.tsx +372 -0
  325. package/src/tui/context/LspContext.tsx +192 -0
  326. package/src/tui/context/McpContext.tsx +325 -0
  327. package/src/tui/context/MessagesContext.tsx +870 -0
  328. package/src/tui/context/OverflowContext.tsx +213 -0
  329. package/src/tui/context/RateLimitContext.tsx +108 -0
  330. package/src/tui/context/ResilienceContext.tsx +275 -0
  331. package/src/tui/context/RootProvider.tsx +136 -0
  332. package/src/tui/context/ScrollContext.tsx +331 -0
  333. package/src/tui/context/ToolsContext.tsx +702 -0
  334. package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
  335. package/src/tui/context/index.ts +140 -0
  336. package/src/tui/enterprise-integration.ts +282 -0
  337. package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
  338. package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
  339. package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
  340. package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
  341. package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
  342. package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
  343. package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
  344. package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
  345. package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
  346. package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
  347. package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
  348. package/src/tui/hooks/index.ts +252 -0
  349. package/src/tui/hooks/useAgentLoop.ts +712 -0
  350. package/src/tui/hooks/useAlternateBuffer.ts +398 -0
  351. package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
  352. package/src/tui/hooks/useBacktrack.ts +443 -0
  353. package/src/tui/hooks/useBracketedPaste.ts +104 -0
  354. package/src/tui/hooks/useCollapsible.ts +240 -0
  355. package/src/tui/hooks/useCopyMode.ts +382 -0
  356. package/src/tui/hooks/useCostSummary.ts +75 -0
  357. package/src/tui/hooks/useDesktopNotification.ts +414 -0
  358. package/src/tui/hooks/useDiffMode.ts +44 -0
  359. package/src/tui/hooks/useFileChangeStats.ts +110 -0
  360. package/src/tui/hooks/useFileSuggestions.ts +284 -0
  361. package/src/tui/hooks/useFlickerDetector.ts +250 -0
  362. package/src/tui/hooks/useGitStatus.ts +200 -0
  363. package/src/tui/hooks/useHotkeys.ts +579 -0
  364. package/src/tui/hooks/useImagePaste.ts +114 -0
  365. package/src/tui/hooks/useInputHighlight.ts +145 -0
  366. package/src/tui/hooks/useInputHistory.ts +246 -0
  367. package/src/tui/hooks/useKeyboardScroll.ts +209 -0
  368. package/src/tui/hooks/useLineBuffer.ts +356 -0
  369. package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
  370. package/src/tui/hooks/useModeController.ts +167 -0
  371. package/src/tui/hooks/useModeShortcuts.ts +196 -0
  372. package/src/tui/hooks/usePermissionHandler.ts +146 -0
  373. package/src/tui/hooks/usePersistence.ts +480 -0
  374. package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
  375. package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
  376. package/src/tui/hooks/useProviderStatus.ts +270 -0
  377. package/src/tui/hooks/useRateLimitStatus.ts +90 -0
  378. package/src/tui/hooks/useScreenReader.ts +315 -0
  379. package/src/tui/hooks/useScrollController.ts +450 -0
  380. package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
  381. package/src/tui/hooks/useSidebarPanelData.ts +115 -0
  382. package/src/tui/hooks/useSmoothScroll.ts +202 -0
  383. package/src/tui/hooks/useSnapshots.ts +300 -0
  384. package/src/tui/hooks/useStateAndRef.ts +50 -0
  385. package/src/tui/hooks/useTerminalSize.ts +206 -0
  386. package/src/tui/hooks/useToolApprovalController.ts +91 -0
  387. package/src/tui/hooks/useVim.ts +334 -0
  388. package/src/tui/hooks/useWorkspace.ts +56 -0
  389. package/src/tui/i18n/__tests__/init.test.ts +278 -0
  390. package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
  391. package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
  392. package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
  393. package/src/tui/i18n/index.ts +72 -0
  394. package/src/tui/i18n/init.ts +131 -0
  395. package/src/tui/i18n/language-config.ts +106 -0
  396. package/src/tui/i18n/locale-detection.ts +173 -0
  397. package/src/tui/i18n/settings-integration.ts +557 -0
  398. package/src/tui/i18n/tui-namespace.ts +538 -0
  399. package/src/tui/i18n/types.ts +312 -0
  400. package/src/tui/index.ts +43 -0
  401. package/src/tui/lsp-integration.ts +409 -0
  402. package/src/tui/metrics-integration.ts +366 -0
  403. package/src/tui/plugins.ts +383 -0
  404. package/src/tui/resilience.ts +342 -0
  405. package/src/tui/sandbox-integration.ts +317 -0
  406. package/src/tui/services/clipboard.ts +348 -0
  407. package/src/tui/services/fuzzy-search.ts +441 -0
  408. package/src/tui/services/index.ts +72 -0
  409. package/src/tui/services/markdown-renderer.ts +565 -0
  410. package/src/tui/services/open-external.ts +247 -0
  411. package/src/tui/services/syntax-highlighter.ts +483 -0
  412. package/src/tui/slash-commands.ts +12 -0
  413. package/src/tui/theme/index.ts +15 -0
  414. package/src/tui/theme/provider.tsx +206 -0
  415. package/src/tui/tip-integration.ts +300 -0
  416. package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
  417. package/src/tui/types/ink-extended.ts +87 -0
  418. package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
  419. package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
  420. package/src/tui/utils/__tests__/text-width.test.ts +158 -0
  421. package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
  422. package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
  423. package/src/tui/utils/bracketedPaste.ts +107 -0
  424. package/src/tui/utils/cursor-manager.ts +131 -0
  425. package/src/tui/utils/detectTerminal.ts +596 -0
  426. package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
  427. package/src/tui/utils/heightEstimator.ts +198 -0
  428. package/src/tui/utils/index.ts +91 -0
  429. package/src/tui/utils/isNarrowWidth.ts +52 -0
  430. package/src/tui/utils/stdoutGuard.ts +90 -0
  431. package/src/tui/utils/synchronized-update.ts +70 -0
  432. package/src/tui/utils/text-width.ts +225 -0
  433. package/src/tui/utils/textSanitizer.ts +225 -0
  434. package/src/tui/utils/textUtils.ts +114 -0
  435. package/src/tui/utils/ui-sizing.ts +192 -0
  436. package/src/tui-blessed/app.ts +160 -0
  437. package/src/tui-blessed/index.ts +2 -0
  438. package/src/tui-blessed/neo-blessed.d.ts +6 -0
  439. package/src/tui-blessed/test.ts +21 -0
  440. package/src/tui-blessed/types.ts +14 -0
  441. package/src/utils/icons.ts +130 -0
  442. package/src/utils/index.ts +33 -0
  443. package/src/utils/resume-hint.ts +86 -0
  444. package/src/version.ts +1 -0
  445. package/tsconfig.json +8 -0
  446. package/vitest.config.ts +35 -0
@@ -0,0 +1,428 @@
1
+ /**
2
+ * VirtualizedList Component
3
+ *
4
+ * A high-performance virtualized list component for terminal UIs.
5
+ * Only renders items that are currently visible in the viewport,
6
+ * with support for variable height items and auto-scroll to bottom.
7
+ *
8
+ * Ported from Gemini CLI with Vellum adaptations.
9
+ *
10
+ * @module tui/components/common/VirtualizedList
11
+ */
12
+
13
+ import { Box, type DOMElement } from "ink";
14
+ import type React from "react";
15
+ import {
16
+ forwardRef,
17
+ useCallback,
18
+ useEffect,
19
+ useImperativeHandle,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from "react";
24
+ import { useScrollOptional } from "../../../context/ScrollContext.js";
25
+ import { useBatchedScroll, useScrollAnchor, useVirtualization } from "./hooks/index.js";
26
+ import { SCROLL_TO_ITEM_END, type VirtualizedListProps, type VirtualizedListRef } from "./types.js";
27
+
28
+ /**
29
+ * VirtualizedList - Only renders visible items for optimal performance.
30
+ *
31
+ * Features:
32
+ * - Virtual rendering: Only items in viewport are mounted
33
+ * - Height estimation: Supports fixed or variable item heights
34
+ * - Auto-scroll: Sticks to bottom when new items added
35
+ * - Anchor-based scrolling: Stable during content changes
36
+ * - Imperative API: Control scrolling programmatically
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const listRef = useRef<VirtualizedListRef<Message>>(null);
41
+ *
42
+ * <VirtualizedList
43
+ * ref={listRef}
44
+ * data={messages}
45
+ * renderItem={({ item }) => <MessageItem message={item} />}
46
+ * keyExtractor={(item) => item.id}
47
+ * estimatedItemHeight={3}
48
+ * initialScrollIndex={SCROLL_TO_ITEM_END}
49
+ * initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
50
+ * />
51
+ *
52
+ * // Scroll programmatically
53
+ * listRef.current?.scrollToEnd();
54
+ * listRef.current?.scrollBy(-10);
55
+ * ```
56
+ */
57
+ function VirtualizedListInner<T>(
58
+ props: VirtualizedListProps<T>,
59
+ ref: React.Ref<VirtualizedListRef<T>>
60
+ ) {
61
+ const {
62
+ data,
63
+ renderItem,
64
+ estimatedItemHeight,
65
+ keyExtractor,
66
+ initialScrollIndex,
67
+ initialScrollOffsetInIndex,
68
+ scrollbarThumbColor,
69
+ onScrollTopChange,
70
+ onStickingToBottomChange,
71
+ alignToBottom = false,
72
+ } = props;
73
+
74
+ // Note: theme reserved for future scrollbar styling
75
+ // const { theme } = useTheme();
76
+
77
+ // FIX: Initialize container height dynamically from terminal dimensions
78
+ // instead of hardcoded 24 lines. This prevents incorrect scroll calculations
79
+ // on first render when terminal size differs from the default.
80
+ const [containerHeight, setContainerHeight] = useState(() => {
81
+ // Use actual terminal rows if available, with fallback to reasonable default
82
+ const terminalRows = process.stdout.rows;
83
+ // Reserve some space for UI elements (header, input, status)
84
+ const reservedLines = 10;
85
+ return Math.max(8, (terminalRows || 24) - reservedLines);
86
+ });
87
+
88
+ // Initial virtualization pass with estimated heights
89
+ const initialVirtualization = useVirtualization({
90
+ dataLength: data.length,
91
+ estimatedItemHeight,
92
+ scrollTop: 0,
93
+ containerHeight,
94
+ });
95
+
96
+ // Scroll anchor management
97
+ const {
98
+ scrollAnchor,
99
+ setScrollAnchor,
100
+ isStickingToBottom,
101
+ setIsStickingToBottom,
102
+ scrollTop,
103
+ getAnchorForScrollTop,
104
+ } = useScrollAnchor({
105
+ dataLength: data.length,
106
+ offsets: initialVirtualization.offsets,
107
+ heights: initialVirtualization.heights,
108
+ totalHeight: initialVirtualization.totalHeight,
109
+ containerHeight,
110
+ initialScrollIndex,
111
+ initialScrollOffsetInIndex,
112
+ });
113
+
114
+ // Full virtualization with actual scroll position
115
+ const {
116
+ heights: _heights,
117
+ offsets,
118
+ totalHeight,
119
+ startIndex,
120
+ endIndex,
121
+ // Note: spacer heights not used in Ink (no real scroll support)
122
+ // topSpacerHeight and bottomSpacerHeight are kept in hook for API compat
123
+ itemRefCallback,
124
+ containerRef,
125
+ measuredContainerHeight,
126
+ } = useVirtualization({
127
+ dataLength: data.length,
128
+ estimatedItemHeight,
129
+ scrollTop,
130
+ containerHeight,
131
+ });
132
+
133
+ // FIX: Debounce container height updates to prevent rapid state changes
134
+ // that can cause rendering race conditions and jittery UI
135
+ const containerHeightUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
136
+ const lastValidHeightRef = useRef(containerHeight);
137
+
138
+ useEffect(() => {
139
+ if (measuredContainerHeight > 0) {
140
+ const rows = process.stdout.rows || 24;
141
+ // FIX: Use a more conservative minimum (8 lines) to prevent degenerate cases
142
+ const MIN_CONTAINER_HEIGHT = 8;
143
+ const safeHeight = Math.max(MIN_CONTAINER_HEIGHT, Math.min(measuredContainerHeight, rows));
144
+
145
+ // Only update if the change is significant (more than 1 line difference)
146
+ // This prevents micro-updates from causing layout thrashing
147
+ if (Math.abs(safeHeight - lastValidHeightRef.current) > 1) {
148
+ // Clear any pending update
149
+ if (containerHeightUpdateTimeoutRef.current) {
150
+ clearTimeout(containerHeightUpdateTimeoutRef.current);
151
+ }
152
+
153
+ // Debounce the update to batch rapid changes
154
+ containerHeightUpdateTimeoutRef.current = setTimeout(() => {
155
+ lastValidHeightRef.current = safeHeight;
156
+ setContainerHeight(safeHeight);
157
+ }, 16); // One frame at 60fps
158
+ }
159
+ }
160
+
161
+ // Cleanup on unmount
162
+ return () => {
163
+ if (containerHeightUpdateTimeoutRef.current) {
164
+ clearTimeout(containerHeightUpdateTimeoutRef.current);
165
+ }
166
+ };
167
+ }, [measuredContainerHeight]);
168
+
169
+ // Batched scroll for smooth updates
170
+ const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
171
+
172
+ // Notify parent of scroll changes and dimension updates
173
+ useEffect(() => {
174
+ if (onScrollTopChange) {
175
+ onScrollTopChange(scrollTop);
176
+ }
177
+ }, [scrollTop, onScrollTopChange]);
178
+
179
+ // Notify parent of sticking state changes
180
+ useEffect(() => {
181
+ if (onStickingToBottomChange) {
182
+ onStickingToBottomChange(isStickingToBottom);
183
+ }
184
+ }, [isStickingToBottom, onStickingToBottomChange]);
185
+
186
+ // Imperative handle for external control
187
+ useImperativeHandle(
188
+ ref,
189
+ () => ({
190
+ scrollBy: (delta: number) => {
191
+ if (delta < 0) {
192
+ setIsStickingToBottom(false);
193
+ }
194
+ const currentScrollTop = getScrollTop();
195
+ const newScrollTop = Math.max(
196
+ 0,
197
+ Math.min(totalHeight - measuredContainerHeight, currentScrollTop + delta)
198
+ );
199
+ setPendingScrollTop(newScrollTop);
200
+ setScrollAnchor(getAnchorForScrollTop(newScrollTop));
201
+ },
202
+
203
+ scrollTo: (offset: number) => {
204
+ setIsStickingToBottom(false);
205
+ const newScrollTop = Math.max(0, Math.min(totalHeight - measuredContainerHeight, offset));
206
+ setPendingScrollTop(newScrollTop);
207
+ setScrollAnchor(getAnchorForScrollTop(newScrollTop));
208
+ },
209
+
210
+ scrollToEnd: () => {
211
+ setIsStickingToBottom(true);
212
+ if (data.length > 0) {
213
+ setScrollAnchor({
214
+ index: data.length - 1,
215
+ offset: SCROLL_TO_ITEM_END,
216
+ });
217
+ }
218
+ },
219
+
220
+ scrollToIndex: ({ index, viewOffset = 0, viewPosition = 0 }) => {
221
+ setIsStickingToBottom(false);
222
+ const offset = offsets[index];
223
+ if (offset !== undefined) {
224
+ const newScrollTop = Math.max(
225
+ 0,
226
+ Math.min(
227
+ totalHeight - measuredContainerHeight,
228
+ offset - viewPosition * measuredContainerHeight + viewOffset
229
+ )
230
+ );
231
+ setPendingScrollTop(newScrollTop);
232
+ setScrollAnchor(getAnchorForScrollTop(newScrollTop));
233
+ }
234
+ },
235
+
236
+ scrollToItem: ({ item, viewOffset = 0, viewPosition = 0 }) => {
237
+ setIsStickingToBottom(false);
238
+ const index = data.indexOf(item);
239
+ if (index !== -1) {
240
+ const offset = offsets[index];
241
+ if (offset !== undefined) {
242
+ const newScrollTop = Math.max(
243
+ 0,
244
+ Math.min(
245
+ totalHeight - measuredContainerHeight,
246
+ offset - viewPosition * measuredContainerHeight + viewOffset
247
+ )
248
+ );
249
+ setPendingScrollTop(newScrollTop);
250
+ setScrollAnchor(getAnchorForScrollTop(newScrollTop));
251
+ }
252
+ }
253
+ },
254
+
255
+ getScrollIndex: () => scrollAnchor.index,
256
+
257
+ getScrollState: () => ({
258
+ scrollTop: getScrollTop(),
259
+ scrollHeight: totalHeight,
260
+ innerHeight: measuredContainerHeight,
261
+ }),
262
+
263
+ isAtBottom: () => isStickingToBottom,
264
+ }),
265
+ [
266
+ offsets,
267
+ scrollAnchor,
268
+ totalHeight,
269
+ getAnchorForScrollTop,
270
+ data,
271
+ measuredContainerHeight,
272
+ getScrollTop,
273
+ setPendingScrollTop,
274
+ setScrollAnchor,
275
+ setIsStickingToBottom,
276
+ isStickingToBottom,
277
+ ]
278
+ );
279
+
280
+ // ==========================================================================
281
+ // ScrollContext Integration
282
+ // ==========================================================================
283
+ const scrollContext = useScrollOptional();
284
+ const lastReportedScrollTop = useRef<number>(scrollTop);
285
+
286
+ // Report dimensions to ScrollContext when they change
287
+ useEffect(() => {
288
+ if (scrollContext) {
289
+ scrollContext.updateDimensions(totalHeight, measuredContainerHeight);
290
+ }
291
+ }, [scrollContext, totalHeight, measuredContainerHeight]);
292
+
293
+ // Sync internal scrollTop changes to ScrollContext (debounced to avoid loops)
294
+ useEffect(() => {
295
+ if (scrollContext && scrollTop !== lastReportedScrollTop.current) {
296
+ lastReportedScrollTop.current = scrollTop;
297
+ // Only sync if the context's scrollTop differs significantly
298
+ const contextScrollTop = scrollContext.state.scrollTop;
299
+ if (Math.abs(scrollTop - contextScrollTop) > 1) {
300
+ scrollContext.scrollTo(scrollTop);
301
+ }
302
+ }
303
+ }, [scrollContext, scrollTop]);
304
+
305
+ // Listen for external scroll commands from ScrollContext
306
+ useEffect(() => {
307
+ if (!scrollContext) return;
308
+
309
+ const unsubscribe = scrollContext.onScrollChange((externalScrollTop) => {
310
+ // Avoid reacting to our own updates
311
+ if (Math.abs(externalScrollTop - scrollTop) <= 1) return;
312
+
313
+ // Apply external scroll command
314
+ const newScrollTop = Math.max(
315
+ 0,
316
+ Math.min(totalHeight - measuredContainerHeight, externalScrollTop)
317
+ );
318
+ setPendingScrollTop(newScrollTop);
319
+ setScrollAnchor(getAnchorForScrollTop(newScrollTop));
320
+
321
+ // Update sticking state based on position
322
+ const atBottom = newScrollTop >= totalHeight - measuredContainerHeight - 1;
323
+ setIsStickingToBottom(atBottom);
324
+ });
325
+
326
+ return unsubscribe;
327
+ }, [
328
+ scrollContext,
329
+ scrollTop,
330
+ totalHeight,
331
+ measuredContainerHeight,
332
+ setPendingScrollTop,
333
+ setScrollAnchor,
334
+ getAnchorForScrollTop,
335
+ setIsStickingToBottom,
336
+ ]);
337
+
338
+ // Respond to scrollToBottom via context state changes
339
+ useEffect(() => {
340
+ if (scrollContext?.state.isAtBottom && !isStickingToBottom && data.length > 0) {
341
+ // Context indicates we should be at bottom but we're not sticking
342
+ // This happens when scrollToBottom() is called on the context
343
+ setIsStickingToBottom(true);
344
+ setScrollAnchor({
345
+ index: data.length - 1,
346
+ offset: SCROLL_TO_ITEM_END,
347
+ });
348
+ }
349
+ }, [
350
+ scrollContext?.state.isAtBottom,
351
+ isStickingToBottom,
352
+ data.length,
353
+ setIsStickingToBottom,
354
+ setScrollAnchor,
355
+ ]);
356
+
357
+ // Create ref callback wrapper
358
+ const createItemRef = useCallback(
359
+ (index: number) => (el: DOMElement | null) => {
360
+ itemRefCallback(index, el);
361
+ },
362
+ [itemRefCallback]
363
+ );
364
+
365
+ // Render visible items
366
+ const renderedItems = useMemo(() => {
367
+ const items: React.ReactElement[] = [];
368
+ for (let i = startIndex; i <= endIndex; i++) {
369
+ const item = data[i];
370
+ if (item) {
371
+ items.push(
372
+ <Box key={keyExtractor(item, i)} width="100%" ref={createItemRef(i)}>
373
+ {renderItem({ item, index: i })}
374
+ </Box>
375
+ );
376
+ }
377
+ }
378
+ return items;
379
+ }, [startIndex, endIndex, data, keyExtractor, renderItem, createItemRef]);
380
+
381
+ // Note: scrollbarThumbColor and scrollTop are reserved for future native scroll support
382
+ // Standard Ink doesn't support overflowY="scroll", only "hidden" or "visible"
383
+ // Gemini CLI uses a forked Ink (@jrichman/ink) with scroll support
384
+ void scrollbarThumbColor;
385
+
386
+ // CRITICAL: Ink doesn't support real CSS scrolling.
387
+ // overflow="hidden" just clips content, it doesn't scroll.
388
+ // We must NOT use spacers - instead, render visible items directly at the top.
389
+ // The "scrolling" effect is achieved by changing which items are rendered (startIndex/endIndex).
390
+ //
391
+ // For alignToBottom: use justifyContent="flex-end" to push content to bottom
392
+ // when total content height is less than container height.
393
+ const shouldAlignToBottom = alignToBottom && totalHeight < measuredContainerHeight;
394
+
395
+ return (
396
+ <Box
397
+ ref={containerRef as React.RefObject<DOMElement>}
398
+ overflowY="hidden"
399
+ overflowX="hidden"
400
+ width="100%"
401
+ height="100%"
402
+ flexDirection="column"
403
+ flexGrow={1}
404
+ minHeight={0}
405
+ paddingRight={1}
406
+ justifyContent={shouldAlignToBottom ? "flex-end" : "flex-start"}
407
+ >
408
+ <Box flexShrink={0} width="100%" flexDirection="column">
409
+ {/* Render visible items directly - no spacers needed in Ink */}
410
+ {/* In Ink, "scrolling" = changing which items we render */}
411
+ {renderedItems}
412
+ </Box>
413
+ </Box>
414
+ );
415
+ }
416
+
417
+ /**
418
+ * VirtualizedList with forwardRef support for generic types.
419
+ */
420
+ const VirtualizedList = forwardRef(VirtualizedListInner) as <T>(
421
+ props: VirtualizedListProps<T> & { ref?: React.Ref<VirtualizedListRef<T>> }
422
+ ) => React.ReactElement;
423
+
424
+ // Add display name for debugging
425
+ (VirtualizedList as React.FC).displayName = "VirtualizedList";
426
+
427
+ export { VirtualizedList };
428
+ export type { VirtualizedListProps, VirtualizedListRef };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Hooks index for VirtualizedList
3
+ *
4
+ * @module tui/components/common/VirtualizedList/hooks
5
+ */
6
+
7
+ export { type UseBatchedScrollReturn, useBatchedScroll } from "./useBatchedScroll.js";
8
+ export {
9
+ type UseScrollAnchorProps,
10
+ type UseScrollAnchorReturn,
11
+ useScrollAnchor,
12
+ } from "./useScrollAnchor.js";
13
+ export {
14
+ MIN_VIEWPORT_HEIGHT,
15
+ MIN_VIEWPORT_WIDTH,
16
+ type UseVirtualizationProps,
17
+ type UseVirtualizationReturn,
18
+ useVirtualization,
19
+ } from "./useVirtualization.js";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * useBatchedScroll Hook
3
+ *
4
+ * Manages batched scroll state updates to allow multiple scroll operations
5
+ * within the same tick to accumulate properly.
6
+ *
7
+ * Ported from Gemini CLI.
8
+ *
9
+ * @module tui/components/common/VirtualizedList/hooks/useBatchedScroll
10
+ */
11
+
12
+ import { useCallback, useEffect, useRef } from "react";
13
+
14
+ /**
15
+ * Return type for the useBatchedScroll hook.
16
+ */
17
+ export interface UseBatchedScrollReturn {
18
+ /** Get the current or pending scroll position */
19
+ readonly getScrollTop: () => number;
20
+ /** Set a pending scroll position for the next render */
21
+ readonly setPendingScrollTop: (scrollTop: number) => void;
22
+ }
23
+
24
+ /**
25
+ * A hook to manage batched scroll state updates.
26
+ *
27
+ * It allows multiple scroll operations within the same tick to accumulate
28
+ * by keeping track of a 'pending' state that resets after render.
29
+ *
30
+ * @param currentScrollTop - The current scroll position from state
31
+ * @returns Object with getScrollTop and setPendingScrollTop functions
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
36
+ *
37
+ * // In scroll handler:
38
+ * const current = getScrollTop();
39
+ * const next = current + delta;
40
+ * setPendingScrollTop(next);
41
+ * ```
42
+ */
43
+ export function useBatchedScroll(currentScrollTop: number): UseBatchedScrollReturn {
44
+ const pendingScrollTopRef = useRef<number | null>(null);
45
+ // Use a ref for currentScrollTop to allow getScrollTop to be stable
46
+ const currentScrollTopRef = useRef(currentScrollTop);
47
+
48
+ // Reset pending state after each render and update current ref
49
+ useEffect(() => {
50
+ currentScrollTopRef.current = currentScrollTop;
51
+ pendingScrollTopRef.current = null;
52
+ });
53
+
54
+ const getScrollTop = useCallback(
55
+ () => pendingScrollTopRef.current ?? currentScrollTopRef.current,
56
+ []
57
+ );
58
+
59
+ const setPendingScrollTop = useCallback((newScrollTop: number) => {
60
+ pendingScrollTopRef.current = newScrollTop;
61
+ }, []);
62
+
63
+ return { getScrollTop, setPendingScrollTop };
64
+ }