@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,398 @@
1
+ /**
2
+ * useAlternateBuffer Hook (T043)
3
+ *
4
+ * React hook for terminal alternate screen buffer management.
5
+ * Provides functionality similar to how vim and other full-screen terminal
6
+ * applications switch between the main and alternate screen buffers.
7
+ *
8
+ * The alternate screen buffer allows the TUI to render without affecting
9
+ * the user's existing terminal scrollback history, and cleanly restores
10
+ * the original buffer when the application exits.
11
+ *
12
+ * @module @vellum/cli
13
+ */
14
+
15
+ import { useCallback, useEffect, useRef, useState } from "react";
16
+
17
+ import { getActiveStdout } from "../buffered-stdout.js";
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Options for the useAlternateBuffer hook.
25
+ */
26
+ export interface UseAlternateBufferOptions {
27
+ /** Whether alternate buffer mode is enabled (default: true) */
28
+ readonly enabled?: boolean;
29
+ /** Whether to constrain the render height (default: false) */
30
+ readonly constrainHeight?: boolean;
31
+ /** Maximum height when constrainHeight is true (default: terminal rows) */
32
+ readonly maxHeight?: number;
33
+ /** Enable viewport calculation for availableHeight (default: false) */
34
+ readonly withViewport?: boolean;
35
+ /** Lines reserved for input area (default: 3) */
36
+ readonly inputReserve?: number;
37
+ /** Lines reserved for status bar (default: 1) */
38
+ readonly statusReserve?: number;
39
+ /** Debounce delay for resize events in ms (default: 100) */
40
+ readonly resizeDebounce?: number;
41
+ }
42
+
43
+ /**
44
+ * Return value of useAlternateBuffer hook.
45
+ */
46
+ export interface UseAlternateBufferReturn {
47
+ /** Whether currently in alternate buffer mode */
48
+ readonly isAlternate: boolean;
49
+ /** Enable alternate buffer mode */
50
+ readonly enable: () => void;
51
+ /** Disable alternate buffer mode and restore original buffer */
52
+ readonly disable: () => void;
53
+ /** Toggle between main and alternate buffer */
54
+ readonly toggle: () => void;
55
+ /** Current effective height (constrained or terminal height) */
56
+ readonly height: number;
57
+ /** Current terminal width */
58
+ readonly width: number;
59
+ /** Available height for content (height - inputReserve - statusReserve) */
60
+ readonly availableHeight: number;
61
+ /** Whether resize is currently being debounced */
62
+ readonly isResizing: boolean;
63
+ }
64
+
65
+ // =============================================================================
66
+ // ANSI Escape Sequences
67
+ // =============================================================================
68
+
69
+ /**
70
+ * ANSI escape sequence to switch to alternate screen buffer.
71
+ * This is the standard DEC private mode 1049.
72
+ */
73
+ const ENTER_ALTERNATE_BUFFER = "\x1b[?1049h";
74
+
75
+ /**
76
+ * ANSI escape sequence to switch back to main screen buffer.
77
+ * This restores the cursor position and screen contents.
78
+ */
79
+ const EXIT_ALTERNATE_BUFFER = "\x1b[?1049l";
80
+
81
+ /**
82
+ * ANSI escape sequence to clear the screen.
83
+ */
84
+ const CLEAR_SCREEN = "\x1b[2J";
85
+
86
+ /**
87
+ * ANSI escape sequence to move cursor to home position (top-left).
88
+ */
89
+ const CURSOR_HOME = "\x1b[H";
90
+
91
+ /**
92
+ * ANSI escape sequence to disable line wrapping.
93
+ * Prevents cursor flickering in VS Code terminal.
94
+ */
95
+ const DISABLE_LINE_WRAPPING = "\x1b[?7l";
96
+
97
+ /**
98
+ * ANSI escape sequence to enable line wrapping.
99
+ * Restores normal terminal behavior on exit.
100
+ */
101
+ const ENABLE_LINE_WRAPPING = "\x1b[?7h";
102
+
103
+ // =============================================================================
104
+ // Input Reserve Calculation
105
+ // =============================================================================
106
+
107
+ /**
108
+ * Calculate the lines to reserve for input area based on mode.
109
+ * Use this to get correct inputReserve value for useAlternateBuffer.
110
+ *
111
+ * @param multiline - Whether the input is multiline
112
+ * @param minHeight - Minimum height for multiline input (default: 5)
113
+ * @returns Number of lines to reserve for input
114
+ */
115
+ export function calculateInputReserve(multiline: boolean, minHeight = 5): number {
116
+ const border = 2; // Top and bottom border
117
+ if (multiline) {
118
+ return minHeight + border; // 5 + 2 = 7 for default multiline
119
+ }
120
+ return 3; // Single line with border
121
+ }
122
+
123
+ // =============================================================================
124
+ // Helper Functions
125
+ // =============================================================================
126
+
127
+ /**
128
+ * Get the current terminal height.
129
+ * Falls back to a reasonable default if unavailable.
130
+ */
131
+ function getTerminalHeight(): number {
132
+ return process.stdout.rows || 24;
133
+ }
134
+
135
+ /**
136
+ * Get the current terminal width.
137
+ * Falls back to a reasonable default if unavailable.
138
+ */
139
+ function getTerminalWidth(): number {
140
+ return process.stdout.columns || 80;
141
+ }
142
+
143
+ /**
144
+ * Write raw data to stdout.
145
+ * Handles potential write errors gracefully.
146
+ * Uses getActiveStdout() to ensure synchronized output when BufferedStdout is active.
147
+ */
148
+ function writeToStdout(data: string): void {
149
+ try {
150
+ getActiveStdout().write(data);
151
+ } catch {
152
+ // Silently ignore write errors (e.g., if stdout is closed)
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Enter the alternate screen buffer.
158
+ */
159
+ function enterAlternateBuffer(): void {
160
+ // No-op: Ink manages alternate buffer switching at render entry.
161
+ // Keeping this hook side-effect free avoids double buffer switching/clearing.
162
+ void ENTER_ALTERNATE_BUFFER;
163
+ void DISABLE_LINE_WRAPPING;
164
+ void CLEAR_SCREEN;
165
+ void CURSOR_HOME;
166
+ }
167
+
168
+ /**
169
+ * Exit the alternate screen buffer.
170
+ */
171
+ function exitAlternateBuffer(): void {
172
+ // No-op: Ink manages alternate buffer switching at render entry.
173
+ void ENABLE_LINE_WRAPPING;
174
+ void EXIT_ALTERNATE_BUFFER;
175
+ }
176
+
177
+ // =============================================================================
178
+ // Hook Implementation
179
+ // =============================================================================
180
+
181
+ /**
182
+ * Hook for managing terminal alternate screen buffer.
183
+ *
184
+ * The alternate screen buffer is a separate buffer that full-screen terminal
185
+ * applications (like vim, less, htop) use to render their UI without affecting
186
+ * the user's existing terminal scrollback history.
187
+ *
188
+ * When enabled, this hook:
189
+ * 1. Switches to the alternate screen buffer
190
+ * 2. Clears the screen and positions the cursor at the top
191
+ * 3. Provides height constraints for TUI rendering
192
+ * 4. Automatically restores the original buffer on unmount
193
+ *
194
+ * @param options - Configuration options
195
+ * @returns Object containing buffer state and control functions
196
+ *
197
+ * @example
198
+ * ```tsx
199
+ * function App() {
200
+ * const { isAlternate, height, enable, disable } = useAlternateBuffer({
201
+ * enabled: true,
202
+ * constrainHeight: true,
203
+ * maxHeight: 40
204
+ * });
205
+ *
206
+ * return (
207
+ * <Box height={height}>
208
+ * <Text>TUI Content (height: {height})</Text>
209
+ * </Box>
210
+ * );
211
+ * }
212
+ * ```
213
+ */
214
+ export function useAlternateBuffer(
215
+ options: UseAlternateBufferOptions = {}
216
+ ): UseAlternateBufferReturn {
217
+ const {
218
+ enabled = true,
219
+ constrainHeight = false,
220
+ maxHeight,
221
+ withViewport = false,
222
+ inputReserve = 7, // Default for multiline (minHeight 5 + border 2)
223
+ statusReserve = 1,
224
+ resizeDebounce = 100,
225
+ } = options;
226
+
227
+ // Track whether we're currently in alternate buffer mode
228
+ const [isAlternate, setIsAlternate] = useState(false);
229
+
230
+ // Track terminal dimensions
231
+ const [terminalHeight, setTerminalHeight] = useState(getTerminalHeight);
232
+ const [terminalWidth, setTerminalWidth] = useState(getTerminalWidth);
233
+
234
+ // Track resize debounce state
235
+ const [isResizing, setIsResizing] = useState(false);
236
+ const resizeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
237
+
238
+ // Ref to track if we've already cleaned up (prevent double cleanup)
239
+ const cleanedUpRef = useRef(false);
240
+
241
+ // Ref to track current alternate state (for cleanup)
242
+ const isAlternateRef = useRef(false);
243
+
244
+ /**
245
+ * Enable alternate buffer mode.
246
+ */
247
+ const enable = useCallback(() => {
248
+ if (!isAlternateRef.current) {
249
+ enterAlternateBuffer();
250
+ isAlternateRef.current = true;
251
+ setIsAlternate(true);
252
+ }
253
+ }, []);
254
+
255
+ /**
256
+ * Disable alternate buffer mode and restore original buffer.
257
+ */
258
+ const disable = useCallback(() => {
259
+ if (isAlternateRef.current) {
260
+ exitAlternateBuffer();
261
+ isAlternateRef.current = false;
262
+ setIsAlternate(false);
263
+ }
264
+ }, []);
265
+
266
+ /**
267
+ * Toggle between main and alternate buffer.
268
+ */
269
+ const toggle = useCallback(() => {
270
+ if (isAlternateRef.current) {
271
+ disable();
272
+ } else {
273
+ enable();
274
+ }
275
+ }, [enable, disable]);
276
+
277
+ /**
278
+ * Calculate the effective height based on constraints.
279
+ */
280
+ const height = constrainHeight
281
+ ? Math.min(terminalHeight, maxHeight ?? terminalHeight)
282
+ : terminalHeight;
283
+
284
+ /**
285
+ * Calculate available height for content (when withViewport is enabled).
286
+ * Ensure minimum of 8 lines to prevent degenerate rendering cases.
287
+ */
288
+ const MIN_AVAILABLE_HEIGHT = 8;
289
+ const availableHeight = withViewport
290
+ ? Math.max(MIN_AVAILABLE_HEIGHT, height - inputReserve - statusReserve)
291
+ : height;
292
+
293
+ // Handle terminal resize events with debounce
294
+ useEffect(() => {
295
+ const handleResize = (): void => {
296
+ if (resizeDebounce > 0) {
297
+ setIsResizing(true);
298
+ if (resizeTimerRef.current) {
299
+ clearTimeout(resizeTimerRef.current);
300
+ }
301
+ resizeTimerRef.current = setTimeout(() => {
302
+ setTerminalHeight(getTerminalHeight());
303
+ setTerminalWidth(getTerminalWidth());
304
+ setIsResizing(false);
305
+ }, resizeDebounce);
306
+ } else {
307
+ setTerminalHeight(getTerminalHeight());
308
+ setTerminalWidth(getTerminalWidth());
309
+ }
310
+ };
311
+
312
+ process.stdout.on("resize", handleResize);
313
+
314
+ return () => {
315
+ process.stdout.off("resize", handleResize);
316
+ if (resizeTimerRef.current) {
317
+ clearTimeout(resizeTimerRef.current);
318
+ }
319
+ };
320
+ }, [resizeDebounce]);
321
+
322
+ // Auto-enable alternate buffer when hook mounts (if enabled option is true)
323
+ useEffect(() => {
324
+ if (enabled && !cleanedUpRef.current) {
325
+ enable();
326
+ }
327
+
328
+ // Cleanup on unmount
329
+ return () => {
330
+ if (!cleanedUpRef.current) {
331
+ cleanedUpRef.current = true;
332
+ if (isAlternateRef.current) {
333
+ exitAlternateBuffer();
334
+ isAlternateRef.current = false;
335
+ }
336
+ }
337
+ };
338
+ }, [enabled, enable]);
339
+
340
+ // Handle process exit signals to ensure buffer is restored
341
+ useEffect(() => {
342
+ const handleExit = (): void => {
343
+ if (!cleanedUpRef.current && isAlternateRef.current) {
344
+ cleanedUpRef.current = true;
345
+ exitAlternateBuffer();
346
+ }
347
+ };
348
+
349
+ // Handle various exit signals
350
+ process.on("exit", handleExit);
351
+ process.on("SIGINT", handleExit);
352
+ process.on("SIGTERM", handleExit);
353
+ process.on("SIGHUP", handleExit);
354
+
355
+ return () => {
356
+ process.off("exit", handleExit);
357
+ process.off("SIGINT", handleExit);
358
+ process.off("SIGTERM", handleExit);
359
+ process.off("SIGHUP", handleExit);
360
+ };
361
+ }, []);
362
+
363
+ return {
364
+ isAlternate,
365
+ enable,
366
+ disable,
367
+ toggle,
368
+ height,
369
+ width: terminalWidth,
370
+ availableHeight,
371
+ isResizing,
372
+ };
373
+ }
374
+
375
+ // =============================================================================
376
+ // Utility Exports
377
+ // =============================================================================
378
+
379
+ /**
380
+ * Raw ANSI sequences for direct use if needed.
381
+ */
382
+ export const ANSI = {
383
+ ENTER_ALTERNATE_BUFFER,
384
+ EXIT_ALTERNATE_BUFFER,
385
+ CLEAR_SCREEN,
386
+ CURSOR_HOME,
387
+ } as const;
388
+
389
+ /**
390
+ * Utility functions for manual buffer management.
391
+ */
392
+ export const bufferUtils = {
393
+ getTerminalHeight,
394
+ getTerminalWidth,
395
+ enterAlternateBuffer,
396
+ exitAlternateBuffer,
397
+ writeToStdout,
398
+ } as const;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Animated Scrollbar Hook
3
+ *
4
+ * Provides animated scrollbar visibility with fade in/out effects based on
5
+ * scroll activity. Inspired by Gemini CLI's useAnimatedScrollbar.
6
+ *
7
+ * Features:
8
+ * - Scrollbar color fades in/out based on activity
9
+ * - Color interpolation (bright → dim over time)
10
+ * - Flash callback for focus events
11
+ * - Activity tracking (scrolling triggers visibility)
12
+ *
13
+ * @module tui/hooks/useAnimatedScrollbar
14
+ */
15
+
16
+ import { useCallback, useEffect, useRef, useState } from "react";
17
+ import { interpolateColor } from "../components/Banner/ShimmerText.js";
18
+ import { useTheme } from "../theme/index.js";
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Animation phase for the scrollbar fade effect
26
+ */
27
+ type AnimationPhase = "idle" | "fade-in" | "visible" | "fade-out";
28
+
29
+ /**
30
+ * Configuration for animated scrollbar behavior
31
+ */
32
+ export interface AnimatedScrollbarConfig {
33
+ /** Duration of fade-in animation in ms (default: 200) */
34
+ readonly fadeInDuration?: number;
35
+ /** Duration scrollbar stays fully visible in ms (default: 1000) */
36
+ readonly visibleDuration?: number;
37
+ /** Duration of fade-out animation in ms (default: 300) */
38
+ readonly fadeOutDuration?: number;
39
+ /** Frame rate for animation updates in ms (default: 33 ~= 30fps) */
40
+ readonly frameInterval?: number;
41
+ }
42
+
43
+ /**
44
+ * Return type for useAnimatedScrollbar hook
45
+ */
46
+ export interface UseAnimatedScrollbarReturn {
47
+ /** Current scrollbar color (interpolated based on animation state) */
48
+ readonly scrollbarColor: string;
49
+ /** Track color (dimmed version) */
50
+ readonly trackColor: string;
51
+ /** Manually trigger a flash animation */
52
+ readonly flashScrollbar: () => void;
53
+ /** Wrapper that calls scrollBy and triggers animation */
54
+ readonly scrollByWithAnimation: (delta: number) => void;
55
+ /** Current animation phase for debugging */
56
+ readonly phase: AnimationPhase;
57
+ }
58
+
59
+ // =============================================================================
60
+ // Constants
61
+ // =============================================================================
62
+
63
+ const DEFAULT_CONFIG: Required<AnimatedScrollbarConfig> = {
64
+ fadeInDuration: 200,
65
+ visibleDuration: 1000,
66
+ fadeOutDuration: 300,
67
+ frameInterval: 33, // ~30fps
68
+ };
69
+
70
+ // =============================================================================
71
+ // Hook
72
+ // =============================================================================
73
+
74
+ /**
75
+ * Hook for animated scrollbar visibility effects
76
+ *
77
+ * @param isFocused - Whether the scrollable area is focused
78
+ * @param scrollBy - Function to scroll by a delta amount
79
+ * @param config - Optional animation configuration
80
+ * @returns Animated scrollbar state and controls
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * const { scrollbarColor, scrollByWithAnimation } = useAnimatedScrollbar(
85
+ * isFocused,
86
+ * (delta) => scrollController.scrollBy(delta)
87
+ * );
88
+ *
89
+ * // In ScrollIndicator
90
+ * <Text color={scrollbarColor}>█</Text>
91
+ * ```
92
+ */
93
+ export function useAnimatedScrollbar(
94
+ isFocused: boolean,
95
+ scrollBy: (delta: number) => void,
96
+ config: AnimatedScrollbarConfig = {}
97
+ ): UseAnimatedScrollbarReturn {
98
+ const { theme } = useTheme();
99
+
100
+ // Merge config with defaults
101
+ const { fadeInDuration, visibleDuration, fadeOutDuration, frameInterval } = {
102
+ ...DEFAULT_CONFIG,
103
+ ...config,
104
+ };
105
+
106
+ // Colors from theme
107
+ const activeColor = theme.semantic.text.muted;
108
+ const dimColor = theme.semantic.border.muted;
109
+ const trackColorTheme = theme.semantic.border.default;
110
+
111
+ // State
112
+ const [scrollbarColor, setScrollbarColor] = useState(dimColor);
113
+ const [phase, setPhase] = useState<AnimationPhase>("idle");
114
+
115
+ // Refs for animation state
116
+ const colorRef = useRef(scrollbarColor);
117
+ const animationFrame = useRef<ReturnType<typeof setInterval> | null>(null);
118
+ const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
119
+ const isAnimatingRef = useRef(false);
120
+ const wasFocusedRef = useRef(isFocused);
121
+
122
+ // Keep colorRef in sync
123
+ colorRef.current = scrollbarColor;
124
+
125
+ /**
126
+ * Cleanup all timers and animation state
127
+ */
128
+ const cleanup = useCallback(() => {
129
+ if (animationFrame.current) {
130
+ clearInterval(animationFrame.current);
131
+ animationFrame.current = null;
132
+ }
133
+ if (timeout.current) {
134
+ clearTimeout(timeout.current);
135
+ timeout.current = null;
136
+ }
137
+ isAnimatingRef.current = false;
138
+ }, []);
139
+
140
+ /**
141
+ * Flash the scrollbar (fade in → visible → fade out)
142
+ */
143
+ const flashScrollbar = useCallback(() => {
144
+ cleanup();
145
+ isAnimatingRef.current = true;
146
+
147
+ const startColor = colorRef.current;
148
+
149
+ // Validate colors exist
150
+ if (!activeColor || !dimColor) {
151
+ return;
152
+ }
153
+
154
+ // Phase 1: Fade In
155
+ setPhase("fade-in");
156
+ let startTime = Date.now();
157
+
158
+ const animateFadeIn = () => {
159
+ const elapsed = Date.now() - startTime;
160
+ const progress = Math.min(elapsed / fadeInDuration, 1);
161
+
162
+ setScrollbarColor(interpolateColor(startColor, activeColor, progress));
163
+
164
+ if (progress >= 1) {
165
+ if (animationFrame.current) {
166
+ clearInterval(animationFrame.current);
167
+ animationFrame.current = null;
168
+ }
169
+
170
+ // Phase 2: Stay visible
171
+ setPhase("visible");
172
+ timeout.current = setTimeout(() => {
173
+ // Phase 3: Fade Out
174
+ setPhase("fade-out");
175
+ startTime = Date.now();
176
+
177
+ const animateFadeOut = () => {
178
+ const elapsed = Date.now() - startTime;
179
+ const progress = Math.min(elapsed / fadeOutDuration, 1);
180
+
181
+ setScrollbarColor(interpolateColor(activeColor, dimColor, progress));
182
+
183
+ if (progress >= 1) {
184
+ cleanup();
185
+ setPhase("idle");
186
+ }
187
+ };
188
+
189
+ animationFrame.current = setInterval(animateFadeOut, frameInterval);
190
+ }, visibleDuration);
191
+ }
192
+ };
193
+
194
+ animationFrame.current = setInterval(animateFadeIn, frameInterval);
195
+ }, [
196
+ cleanup,
197
+ activeColor,
198
+ dimColor,
199
+ fadeInDuration,
200
+ visibleDuration,
201
+ fadeOutDuration,
202
+ frameInterval,
203
+ ]);
204
+
205
+ /**
206
+ * Handle focus changes - flash on focus gain
207
+ */
208
+ useEffect(() => {
209
+ if (isFocused && !wasFocusedRef.current) {
210
+ // Gained focus - flash scrollbar
211
+ flashScrollbar();
212
+ } else if (!isFocused && wasFocusedRef.current) {
213
+ // Lost focus - immediately dim
214
+ cleanup();
215
+ setScrollbarColor(dimColor);
216
+ setPhase("idle");
217
+ }
218
+ wasFocusedRef.current = isFocused;
219
+
220
+ return cleanup;
221
+ }, [isFocused, flashScrollbar, cleanup, dimColor]);
222
+
223
+ /**
224
+ * Scroll with animation - wraps scrollBy and triggers flash
225
+ */
226
+ const scrollByWithAnimation = useCallback(
227
+ (delta: number) => {
228
+ scrollBy(delta);
229
+ flashScrollbar();
230
+ },
231
+ [scrollBy, flashScrollbar]
232
+ );
233
+
234
+ return {
235
+ scrollbarColor,
236
+ trackColor: trackColorTheme,
237
+ flashScrollbar,
238
+ scrollByWithAnimation,
239
+ phase,
240
+ };
241
+ }