@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,450 @@
1
+ /**
2
+ * Scroll Controller Hook
3
+ *
4
+ * Manages scroll state for message viewport with follow/manual modes.
5
+ * Provides a state machine for automatic scrolling (follow) vs manual
6
+ * navigation, with smooth transitions between modes.
7
+ *
8
+ * State Machine:
9
+ * ```
10
+ * follow ──[scrollUp/PageUp/MouseWheel]──> manual
11
+ * manual ──[scrollToBottom/End/reach-bottom]──> follow
12
+ * manual ──[newMessage]──> stay manual, increment newMessageCount
13
+ * follow ──[newMessage]──> stay follow, auto-scroll
14
+ * ```
15
+ *
16
+ * @module tui/hooks/useScrollController
17
+ */
18
+
19
+ import { useCallback, useMemo, useReducer } from "react";
20
+
21
+ // =============================================================================
22
+ // Types
23
+ // =============================================================================
24
+
25
+ /**
26
+ * Scroll mode determines auto-scroll behavior.
27
+ * - follow: Automatically scroll to bottom on new content
28
+ * - manual: User is manually scrolling, don't auto-scroll
29
+ */
30
+ export type ScrollMode = "follow" | "manual";
31
+
32
+ /**
33
+ * Immutable scroll state for viewport scroll controller.
34
+ * Named ViewportScrollState to avoid conflicts with ScrollContext.ScrollState.
35
+ */
36
+ export interface ViewportScrollState {
37
+ /** Current scroll mode */
38
+ readonly mode: ScrollMode;
39
+ /** Offset from bottom in lines (0 = at bottom) */
40
+ readonly offsetFromBottom: number;
41
+ /** Number of new messages since entering manual mode */
42
+ readonly newMessageCount: number;
43
+ /** Total scrollable height in lines */
44
+ readonly totalHeight: number;
45
+ /** Visible viewport height in lines */
46
+ readonly viewportHeight: number;
47
+ }
48
+
49
+ /**
50
+ * Actions for controlling viewport scroll behavior.
51
+ */
52
+ export interface ViewportScrollActions {
53
+ /** Scroll up by N lines (default: scrollStep from options) */
54
+ scrollUp(lines?: number): void;
55
+ /** Scroll down by N lines (default: scrollStep from options) */
56
+ scrollDown(lines?: number): void;
57
+ /** Jump to specific offset from bottom */
58
+ jumpTo(offset: number): void;
59
+ /** Return to follow mode (scroll to bottom) */
60
+ scrollToBottom(): void;
61
+ /** Update total height (called when messages change) */
62
+ setTotalHeight(height: number): void;
63
+ /** Update viewport height (called when terminal resizes) */
64
+ setViewportHeight(height: number): void;
65
+ /** Notify new message arrived */
66
+ notifyNewMessage(): void;
67
+ }
68
+
69
+ /**
70
+ * Options for useScrollController hook.
71
+ */
72
+ export interface UseScrollControllerOptions {
73
+ /** Viewport height in lines */
74
+ readonly viewportHeight: number;
75
+ /** Initial total height */
76
+ readonly initialTotalHeight?: number;
77
+ /** Lines to scroll per action (default: 3) */
78
+ readonly scrollStep?: number;
79
+ /** Auto-switch to follow when reaching bottom (default: true) */
80
+ readonly autoFollowOnBottom?: boolean;
81
+ }
82
+
83
+ // =============================================================================
84
+ // Reducer
85
+ // =============================================================================
86
+
87
+ /**
88
+ * Internal state shape (same as ScrollState but mutable for reducer)
89
+ */
90
+ interface InternalState {
91
+ mode: ScrollMode;
92
+ offsetFromBottom: number;
93
+ newMessageCount: number;
94
+ totalHeight: number;
95
+ viewportHeight: number;
96
+ }
97
+
98
+ /**
99
+ * Action types for the scroll reducer
100
+ */
101
+ type ScrollAction =
102
+ | { type: "SCROLL_UP"; lines: number; autoFollowOnBottom: boolean }
103
+ | { type: "SCROLL_DOWN"; lines: number; autoFollowOnBottom: boolean }
104
+ | { type: "JUMP_TO"; offset: number; autoFollowOnBottom: boolean }
105
+ | { type: "SCROLL_TO_BOTTOM" }
106
+ | { type: "SET_TOTAL_HEIGHT"; height: number }
107
+ | { type: "SET_VIEWPORT_HEIGHT"; height: number }
108
+ | { type: "NEW_MESSAGE" };
109
+
110
+ /**
111
+ * Computes the maximum scrollable offset (how far up we can scroll)
112
+ */
113
+ function getMaxOffset(totalHeight: number, viewportHeight: number): number {
114
+ return Math.max(0, totalHeight - viewportHeight);
115
+ }
116
+
117
+ /**
118
+ * Clamps offset to valid range [0, maxOffset]
119
+ */
120
+ function clampOffset(offset: number, totalHeight: number, viewportHeight: number): number {
121
+ const maxOffset = getMaxOffset(totalHeight, viewportHeight);
122
+ return Math.max(0, Math.min(offset, maxOffset));
123
+ }
124
+
125
+ /**
126
+ * Reducer for scroll state management
127
+ */
128
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Reducer with many action cases is inherently complex
129
+ function scrollReducer(state: InternalState, action: ScrollAction): InternalState {
130
+ switch (action.type) {
131
+ case "SCROLL_UP": {
132
+ // Scrolling up switches to manual mode
133
+ const newOffset = clampOffset(
134
+ state.offsetFromBottom + action.lines,
135
+ state.totalHeight,
136
+ state.viewportHeight
137
+ );
138
+
139
+ // Only switch to manual if we actually moved
140
+ if (newOffset === state.offsetFromBottom) {
141
+ return state;
142
+ }
143
+
144
+ return {
145
+ ...state,
146
+ mode: "manual",
147
+ offsetFromBottom: newOffset,
148
+ };
149
+ }
150
+
151
+ case "SCROLL_DOWN": {
152
+ const newOffset = clampOffset(
153
+ state.offsetFromBottom - action.lines,
154
+ state.totalHeight,
155
+ state.viewportHeight
156
+ );
157
+
158
+ // If we've reached the bottom and autoFollow is enabled, switch to follow mode
159
+ if (newOffset === 0 && action.autoFollowOnBottom) {
160
+ return {
161
+ ...state,
162
+ mode: "follow",
163
+ offsetFromBottom: 0,
164
+ newMessageCount: 0,
165
+ };
166
+ }
167
+
168
+ // Only update if offset changed
169
+ if (newOffset === state.offsetFromBottom) {
170
+ return state;
171
+ }
172
+
173
+ return {
174
+ ...state,
175
+ offsetFromBottom: newOffset,
176
+ };
177
+ }
178
+
179
+ case "JUMP_TO": {
180
+ const newOffset = clampOffset(action.offset, state.totalHeight, state.viewportHeight);
181
+
182
+ // If jumping to bottom with autoFollow, switch to follow mode
183
+ if (newOffset === 0 && action.autoFollowOnBottom) {
184
+ if (
185
+ state.mode === "follow" &&
186
+ state.offsetFromBottom === 0 &&
187
+ state.newMessageCount === 0
188
+ ) {
189
+ return state;
190
+ }
191
+ return {
192
+ ...state,
193
+ mode: "follow",
194
+ offsetFromBottom: 0,
195
+ newMessageCount: 0,
196
+ };
197
+ }
198
+
199
+ // If jumping away from bottom, switch to manual
200
+ const newMode = newOffset > 0 ? "manual" : state.mode;
201
+
202
+ if (newOffset === state.offsetFromBottom && newMode === state.mode) {
203
+ return state;
204
+ }
205
+
206
+ return {
207
+ ...state,
208
+ mode: newMode,
209
+ offsetFromBottom: newOffset,
210
+ // Reset new message count only if returning to follow
211
+ newMessageCount: newMode === "follow" ? 0 : state.newMessageCount,
212
+ };
213
+ }
214
+
215
+ case "SCROLL_TO_BOTTOM": {
216
+ return {
217
+ ...state,
218
+ mode: "follow",
219
+ offsetFromBottom: 0,
220
+ newMessageCount: 0,
221
+ };
222
+ }
223
+
224
+ case "SET_TOTAL_HEIGHT": {
225
+ const newTotalHeight = Math.max(0, action.height);
226
+
227
+ // In follow mode, keep offset at 0
228
+ if (state.mode === "follow") {
229
+ if (newTotalHeight === state.totalHeight && state.offsetFromBottom === 0) {
230
+ return state;
231
+ }
232
+ return {
233
+ ...state,
234
+ totalHeight: newTotalHeight,
235
+ offsetFromBottom: 0,
236
+ };
237
+ }
238
+
239
+ // In manual mode, maintain the offset but clamp to valid range
240
+ const clampedOffset = clampOffset(
241
+ state.offsetFromBottom,
242
+ newTotalHeight,
243
+ state.viewportHeight
244
+ );
245
+
246
+ if (newTotalHeight === state.totalHeight && clampedOffset === state.offsetFromBottom) {
247
+ return state;
248
+ }
249
+
250
+ return {
251
+ ...state,
252
+ totalHeight: newTotalHeight,
253
+ offsetFromBottom: clampedOffset,
254
+ };
255
+ }
256
+
257
+ case "SET_VIEWPORT_HEIGHT": {
258
+ const newViewportHeight = Math.max(1, action.height);
259
+
260
+ // Clamp offset to valid range with new viewport
261
+ const clampedOffset = clampOffset(
262
+ state.offsetFromBottom,
263
+ state.totalHeight,
264
+ newViewportHeight
265
+ );
266
+
267
+ if (newViewportHeight === state.viewportHeight && clampedOffset === state.offsetFromBottom) {
268
+ return state;
269
+ }
270
+
271
+ return {
272
+ ...state,
273
+ viewportHeight: newViewportHeight,
274
+ offsetFromBottom: clampedOffset,
275
+ };
276
+ }
277
+
278
+ case "NEW_MESSAGE": {
279
+ if (state.mode === "follow") {
280
+ // In follow mode, stay at bottom (no state change needed)
281
+ return state;
282
+ }
283
+
284
+ // In manual mode, increment counter
285
+ return {
286
+ ...state,
287
+ newMessageCount: state.newMessageCount + 1,
288
+ };
289
+ }
290
+
291
+ default:
292
+ return state;
293
+ }
294
+ }
295
+
296
+ // =============================================================================
297
+ // Hook
298
+ // =============================================================================
299
+
300
+ /**
301
+ * useScrollController - Manages scroll state for message viewport
302
+ *
303
+ * Provides a state machine for follow/manual scroll modes with
304
+ * automatic mode transitions based on user actions.
305
+ *
306
+ * @example
307
+ * ```tsx
308
+ * const [scrollState, scrollActions] = useScrollController({
309
+ * viewportHeight: terminalHeight - headerHeight,
310
+ * initialTotalHeight: messages.length * avgLineHeight,
311
+ * });
312
+ *
313
+ * // In your scroll handler
314
+ * useInput((input, key) => {
315
+ * if (key.pageUp) scrollActions.scrollUp(scrollState.viewportHeight / 2);
316
+ * if (key.pageDown) scrollActions.scrollDown(scrollState.viewportHeight / 2);
317
+ * if (key.end) scrollActions.scrollToBottom();
318
+ * });
319
+ *
320
+ * // When messages change
321
+ * useEffect(() => {
322
+ * scrollActions.setTotalHeight(newHeight);
323
+ * scrollActions.notifyNewMessage();
324
+ * }, [messages]);
325
+ *
326
+ * // Show "X new messages" badge when in manual mode
327
+ * if (scrollState.mode === 'manual' && scrollState.newMessageCount > 0) {
328
+ * showNewMessagesBadge(scrollState.newMessageCount);
329
+ * }
330
+ * ```
331
+ *
332
+ * @param options - Configuration options
333
+ * @returns Tuple of [state, actions]
334
+ */
335
+ export function useScrollController(
336
+ options: UseScrollControllerOptions
337
+ ): [ViewportScrollState, ViewportScrollActions] {
338
+ const {
339
+ viewportHeight,
340
+ initialTotalHeight = 0,
341
+ scrollStep = 3,
342
+ autoFollowOnBottom = true,
343
+ } = options;
344
+
345
+ // Initialize reducer state
346
+ const [state, dispatch] = useReducer(scrollReducer, {
347
+ mode: "follow",
348
+ offsetFromBottom: 0,
349
+ newMessageCount: 0,
350
+ totalHeight: Math.max(0, initialTotalHeight),
351
+ viewportHeight: Math.max(1, viewportHeight),
352
+ });
353
+
354
+ // Create memoized actions
355
+ const scrollUp = useCallback(
356
+ (lines: number = scrollStep) => {
357
+ dispatch({ type: "SCROLL_UP", lines, autoFollowOnBottom });
358
+ },
359
+ [scrollStep, autoFollowOnBottom]
360
+ );
361
+
362
+ const scrollDown = useCallback(
363
+ (lines: number = scrollStep) => {
364
+ dispatch({ type: "SCROLL_DOWN", lines, autoFollowOnBottom });
365
+ },
366
+ [scrollStep, autoFollowOnBottom]
367
+ );
368
+
369
+ const jumpTo = useCallback(
370
+ (offset: number) => {
371
+ dispatch({ type: "JUMP_TO", offset, autoFollowOnBottom });
372
+ },
373
+ [autoFollowOnBottom]
374
+ );
375
+
376
+ const scrollToBottom = useCallback(() => {
377
+ dispatch({ type: "SCROLL_TO_BOTTOM" });
378
+ }, []);
379
+
380
+ const setTotalHeight = useCallback((height: number) => {
381
+ dispatch({ type: "SET_TOTAL_HEIGHT", height });
382
+ }, []);
383
+
384
+ const setViewportHeight = useCallback((height: number) => {
385
+ dispatch({ type: "SET_VIEWPORT_HEIGHT", height });
386
+ }, []);
387
+
388
+ const notifyNewMessage = useCallback(() => {
389
+ dispatch({ type: "NEW_MESSAGE" });
390
+ }, []);
391
+
392
+ // Bundle actions
393
+ const actions: ViewportScrollActions = useMemo(
394
+ () => ({
395
+ scrollUp,
396
+ scrollDown,
397
+ jumpTo,
398
+ scrollToBottom,
399
+ setTotalHeight,
400
+ setViewportHeight,
401
+ notifyNewMessage,
402
+ }),
403
+ [
404
+ scrollUp,
405
+ scrollDown,
406
+ jumpTo,
407
+ scrollToBottom,
408
+ setTotalHeight,
409
+ setViewportHeight,
410
+ notifyNewMessage,
411
+ ]
412
+ );
413
+
414
+ return [state, actions];
415
+ }
416
+
417
+ // =============================================================================
418
+ // Utility Functions
419
+ // =============================================================================
420
+
421
+ /**
422
+ * Calculate visible scroll position as a percentage (0-100)
423
+ */
424
+ export function getScrollPercentage(state: ViewportScrollState): number {
425
+ const maxOffset = getMaxOffset(state.totalHeight, state.viewportHeight);
426
+ if (maxOffset === 0) return 100;
427
+ return Math.round(((maxOffset - state.offsetFromBottom) / maxOffset) * 100);
428
+ }
429
+
430
+ /**
431
+ * Check if content is scrollable (exceeds viewport)
432
+ */
433
+ export function isScrollable(state: ViewportScrollState): boolean {
434
+ return state.totalHeight > state.viewportHeight;
435
+ }
436
+
437
+ /**
438
+ * Check if currently at the top
439
+ */
440
+ export function isAtTop(state: ViewportScrollState): boolean {
441
+ const maxOffset = getMaxOffset(state.totalHeight, state.viewportHeight);
442
+ return state.offsetFromBottom >= maxOffset;
443
+ }
444
+
445
+ /**
446
+ * Check if currently at the bottom
447
+ */
448
+ export function isAtBottom(state: ViewportScrollState): boolean {
449
+ return state.offsetFromBottom === 0;
450
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Scroll Event Batcher Hook
3
+ *
4
+ * Batches multiple scroll events within the same tick to prevent jitter.
5
+ * Useful when multiple sources can trigger scroll updates simultaneously.
6
+ *
7
+ * @module tui/hooks/useScrollEventBatcher
8
+ */
9
+
10
+ import { useCallback, useRef, useState } from "react";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Batching strategy for combining scroll deltas
18
+ */
19
+ export type BatchStrategy = "sum" | "last" | "max" | "min";
20
+
21
+ /**
22
+ * Configuration for scroll event batcher behavior
23
+ */
24
+ export interface ScrollEventBatcherConfig {
25
+ /** Batch window in ms (default: 0 - same tick only) */
26
+ readonly batchWindow?: number;
27
+ /** Strategy for combining batched deltas (default: 'sum') */
28
+ readonly strategy?: BatchStrategy;
29
+ /** Maximum absolute delta to allow (default: Infinity) */
30
+ readonly maxDelta?: number;
31
+ }
32
+
33
+ /**
34
+ * Return type for useScrollEventBatcher hook
35
+ */
36
+ export interface UseScrollEventBatcherReturn {
37
+ /** Queue a scroll delta (will be batched) */
38
+ readonly queueScroll: (delta: number) => void;
39
+ /** Number of pending deltas in current batch */
40
+ readonly pendingCount: number;
41
+ /** Force flush any pending scroll */
42
+ readonly flush: () => void;
43
+ }
44
+
45
+ // =============================================================================
46
+ // Constants
47
+ // =============================================================================
48
+
49
+ const DEFAULT_CONFIG: Required<ScrollEventBatcherConfig> = {
50
+ batchWindow: 0,
51
+ strategy: "sum",
52
+ maxDelta: Infinity,
53
+ };
54
+
55
+ // =============================================================================
56
+ // Strategy Functions
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Combine deltas based on strategy
61
+ */
62
+ function combineDelta(deltas: number[], strategy: BatchStrategy): number {
63
+ if (deltas.length === 0) return 0;
64
+
65
+ switch (strategy) {
66
+ case "sum":
67
+ return deltas.reduce((a, b) => a + b, 0);
68
+ case "last": {
69
+ const lastDelta = deltas[deltas.length - 1];
70
+ return lastDelta !== undefined ? lastDelta : 0;
71
+ }
72
+ case "max":
73
+ return Math.max(...deltas);
74
+ case "min":
75
+ return Math.min(...deltas);
76
+ default:
77
+ return deltas.reduce((a, b) => a + b, 0);
78
+ }
79
+ }
80
+
81
+ // =============================================================================
82
+ // Hook
83
+ // =============================================================================
84
+
85
+ /**
86
+ * Hook for batching scroll events
87
+ *
88
+ * Collects scroll deltas within a batch window and combines them
89
+ * according to the configured strategy before calling the scroll handler.
90
+ *
91
+ * @param onScroll - Handler called with combined delta after batching
92
+ * @param config - Optional batching configuration
93
+ * @returns Batched scroll controls
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * const { queueScroll } = useScrollEventBatcher(
98
+ * (delta) => scrollController.scrollBy(delta),
99
+ * { strategy: 'sum' }
100
+ * );
101
+ *
102
+ * // Multiple calls in same tick get batched
103
+ * queueScroll(1);
104
+ * queueScroll(2);
105
+ * queueScroll(3);
106
+ * // onScroll called once with delta=6
107
+ * ```
108
+ */
109
+ export function useScrollEventBatcher(
110
+ onScroll: (delta: number) => void,
111
+ config: ScrollEventBatcherConfig = {}
112
+ ): UseScrollEventBatcherReturn {
113
+ // Merge config with defaults
114
+ const { batchWindow, strategy, maxDelta } = { ...DEFAULT_CONFIG, ...config };
115
+
116
+ // State
117
+ const [pendingCount, setPendingCount] = useState(0);
118
+
119
+ // Refs
120
+ const pendingDeltasRef = useRef<number[]>([]);
121
+ const flushTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
122
+ const onScrollRef = useRef(onScroll);
123
+
124
+ // Keep onScroll ref updated
125
+ onScrollRef.current = onScroll;
126
+
127
+ /**
128
+ * Flush pending deltas
129
+ */
130
+ const flush = useCallback(() => {
131
+ if (flushTimeoutRef.current) {
132
+ clearTimeout(flushTimeoutRef.current);
133
+ flushTimeoutRef.current = null;
134
+ }
135
+
136
+ const deltas = pendingDeltasRef.current;
137
+ if (deltas.length === 0) return;
138
+
139
+ // Combine deltas based on strategy
140
+ let combinedDelta = combineDelta(deltas, strategy);
141
+
142
+ // Clamp to maxDelta
143
+ if (Math.abs(combinedDelta) > maxDelta) {
144
+ combinedDelta = Math.sign(combinedDelta) * maxDelta;
145
+ }
146
+
147
+ // Clear pending state
148
+ pendingDeltasRef.current = [];
149
+ setPendingCount(0);
150
+
151
+ // Execute scroll
152
+ onScrollRef.current(combinedDelta);
153
+ }, [strategy, maxDelta]);
154
+
155
+ /**
156
+ * Queue a scroll delta
157
+ */
158
+ const queueScroll = useCallback(
159
+ (delta: number) => {
160
+ // Add to pending deltas
161
+ pendingDeltasRef.current.push(delta);
162
+ setPendingCount((c) => c + 1);
163
+
164
+ // Schedule flush
165
+ if (flushTimeoutRef.current) {
166
+ clearTimeout(flushTimeoutRef.current);
167
+ }
168
+
169
+ if (batchWindow === 0) {
170
+ // Batch within same tick using microtask
171
+ queueMicrotask(flush);
172
+ } else {
173
+ // Batch within time window
174
+ flushTimeoutRef.current = setTimeout(flush, batchWindow);
175
+ }
176
+ },
177
+ [batchWindow, flush]
178
+ );
179
+
180
+ return {
181
+ queueScroll,
182
+ pendingCount,
183
+ flush,
184
+ };
185
+ }