@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.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/__fixtures__/responses/code-generation.json +42 -0
- package/__fixtures__/responses/error-response.json +20 -0
- package/__fixtures__/responses/hello-world.json +32 -0
- package/dist/auth-6MCXESOH.js +26 -0
- package/dist/chunk-SECXJGWA.js +597 -0
- package/dist/index.js +34023 -0
- package/package.json +67 -0
- package/src/__tests__/commands.e2e.test.ts +728 -0
- package/src/__tests__/credentials.test.ts +713 -0
- package/src/__tests__/mode-e2e.test.ts +391 -0
- package/src/__tests__/tui-integration.test.tsx +1271 -0
- package/src/agents/__tests__/task-persistence.test.ts +235 -0
- package/src/agents/commands/delegate.ts +240 -0
- package/src/agents/commands/index.ts +10 -0
- package/src/agents/commands/resume.ts +335 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/task-persistence.ts +272 -0
- package/src/agents/task-resumption.ts +242 -0
- package/src/app.tsx +4737 -0
- package/src/commands/__tests__/.gitkeep +1 -0
- package/src/commands/__tests__/agents.test.ts +606 -0
- package/src/commands/__tests__/auth.test.ts +626 -0
- package/src/commands/__tests__/autocomplete.test.ts +683 -0
- package/src/commands/__tests__/batch.test.ts +287 -0
- package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
- package/src/commands/__tests__/completion.test.ts +238 -0
- package/src/commands/__tests__/core.test.ts +363 -0
- package/src/commands/__tests__/executor.test.ts +496 -0
- package/src/commands/__tests__/exit-codes.test.ts +220 -0
- package/src/commands/__tests__/init.test.ts +243 -0
- package/src/commands/__tests__/language.test.ts +353 -0
- package/src/commands/__tests__/mode-cli.test.ts +667 -0
- package/src/commands/__tests__/model.test.ts +277 -0
- package/src/commands/__tests__/parser.test.ts +493 -0
- package/src/commands/__tests__/performance.bench.ts +380 -0
- package/src/commands/__tests__/registry.test.ts +534 -0
- package/src/commands/__tests__/resume.test.ts +449 -0
- package/src/commands/__tests__/security.test.ts +845 -0
- package/src/commands/__tests__/stream-json.test.ts +372 -0
- package/src/commands/__tests__/user-commands.test.ts +597 -0
- package/src/commands/adapters.ts +267 -0
- package/src/commands/agent.ts +395 -0
- package/src/commands/agents/generate.ts +506 -0
- package/src/commands/agents/index.ts +272 -0
- package/src/commands/agents/show.ts +271 -0
- package/src/commands/agents/validate.ts +387 -0
- package/src/commands/auth.ts +883 -0
- package/src/commands/autocomplete.ts +480 -0
- package/src/commands/batch/command.ts +388 -0
- package/src/commands/batch/executor.ts +361 -0
- package/src/commands/batch/index.ts +12 -0
- package/src/commands/commit.ts +235 -0
- package/src/commands/completion/index.ts +371 -0
- package/src/commands/condense.ts +191 -0
- package/src/commands/config.ts +344 -0
- package/src/commands/context-provider.ts +173 -0
- package/src/commands/copy.ts +329 -0
- package/src/commands/core/clear.ts +38 -0
- package/src/commands/core/exit.ts +43 -0
- package/src/commands/core/help.ts +354 -0
- package/src/commands/core/index.ts +15 -0
- package/src/commands/cost.ts +179 -0
- package/src/commands/credentials.tsx +618 -0
- package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
- package/src/commands/custom-agents/create.ts +377 -0
- package/src/commands/custom-agents/export.ts +135 -0
- package/src/commands/custom-agents/import.ts +199 -0
- package/src/commands/custom-agents/index.ts +372 -0
- package/src/commands/custom-agents/info.ts +318 -0
- package/src/commands/custom-agents/list.ts +267 -0
- package/src/commands/custom-agents/validate.ts +388 -0
- package/src/commands/diff-mode.ts +241 -0
- package/src/commands/env.ts +53 -0
- package/src/commands/executor.ts +579 -0
- package/src/commands/exit-codes.ts +202 -0
- package/src/commands/index.ts +701 -0
- package/src/commands/init/index.ts +15 -0
- package/src/commands/init/prompts.ts +366 -0
- package/src/commands/init/templates/commands-readme.md +80 -0
- package/src/commands/init/templates/example-command.md +79 -0
- package/src/commands/init/templates/example-skill.md +168 -0
- package/src/commands/init/templates/example-workflow.md +101 -0
- package/src/commands/init/templates/prompts-readme.md +52 -0
- package/src/commands/init/templates/rules-readme.md +63 -0
- package/src/commands/init/templates/skills-readme.md +83 -0
- package/src/commands/init/templates/workflows-readme.md +94 -0
- package/src/commands/init.ts +391 -0
- package/src/commands/install.ts +90 -0
- package/src/commands/language.ts +191 -0
- package/src/commands/loaders/.gitkeep +1 -0
- package/src/commands/lsp.ts +199 -0
- package/src/commands/markdown-commands.ts +253 -0
- package/src/commands/mcp.ts +588 -0
- package/src/commands/memory/export.ts +341 -0
- package/src/commands/memory/index.ts +148 -0
- package/src/commands/memory/list.ts +261 -0
- package/src/commands/memory/search.ts +346 -0
- package/src/commands/memory/utils.ts +15 -0
- package/src/commands/metrics.ts +75 -0
- package/src/commands/migrate/index.ts +16 -0
- package/src/commands/migrate/prompts.ts +477 -0
- package/src/commands/mode.ts +331 -0
- package/src/commands/model.ts +298 -0
- package/src/commands/onboard.ts +205 -0
- package/src/commands/open.ts +169 -0
- package/src/commands/output/stream-json.ts +373 -0
- package/src/commands/parser/chain-parser.ts +370 -0
- package/src/commands/parser/index.ts +29 -0
- package/src/commands/parser/pipe-parser.ts +480 -0
- package/src/commands/parser.ts +588 -0
- package/src/commands/persistence.ts +355 -0
- package/src/commands/progress.ts +18 -0
- package/src/commands/prompt/index.ts +17 -0
- package/src/commands/prompt/validate.ts +621 -0
- package/src/commands/prompt-priority.ts +401 -0
- package/src/commands/registry.ts +374 -0
- package/src/commands/sandbox/index.ts +131 -0
- package/src/commands/security/index.ts +21 -0
- package/src/commands/security/input-sanitizer.ts +168 -0
- package/src/commands/security/permission-checker.ts +456 -0
- package/src/commands/security/sensitive-data.ts +350 -0
- package/src/commands/session/delete.ts +38 -0
- package/src/commands/session/export.ts +39 -0
- package/src/commands/session/index.ts +26 -0
- package/src/commands/session/list.ts +26 -0
- package/src/commands/session/resume.ts +562 -0
- package/src/commands/session/search.ts +434 -0
- package/src/commands/session/show.ts +26 -0
- package/src/commands/settings.ts +368 -0
- package/src/commands/setup.ts +23 -0
- package/src/commands/shell/index.ts +16 -0
- package/src/commands/shell/setup.ts +422 -0
- package/src/commands/shell-init.ts +50 -0
- package/src/commands/shell-integration/index.ts +194 -0
- package/src/commands/skill.ts +1220 -0
- package/src/commands/spec.ts +558 -0
- package/src/commands/status.ts +246 -0
- package/src/commands/theme.ts +211 -0
- package/src/commands/think.ts +551 -0
- package/src/commands/trust.ts +211 -0
- package/src/commands/tutorial.ts +522 -0
- package/src/commands/types.ts +512 -0
- package/src/commands/update.ts +274 -0
- package/src/commands/usage.ts +213 -0
- package/src/commands/user-commands.ts +630 -0
- package/src/commands/utils.ts +142 -0
- package/src/commands/vim.ts +152 -0
- package/src/commands/workflow.ts +257 -0
- package/src/components/header.tsx +25 -0
- package/src/components/input.tsx +25 -0
- package/src/components/message-list.tsx +32 -0
- package/src/components/status-bar.tsx +23 -0
- package/src/index.tsx +614 -0
- package/src/onboarding/__tests__/tutorial.test.ts +740 -0
- package/src/onboarding/index.ts +69 -0
- package/src/onboarding/tips/index.ts +9 -0
- package/src/onboarding/tips/tip-engine.ts +459 -0
- package/src/onboarding/tutorial/index.ts +88 -0
- package/src/onboarding/tutorial/lessons/basics.ts +151 -0
- package/src/onboarding/tutorial/lessons/index.ts +151 -0
- package/src/onboarding/tutorial/lessons/modes.ts +230 -0
- package/src/onboarding/tutorial/lessons/tools.ts +172 -0
- package/src/onboarding/tutorial/progress-tracker.ts +350 -0
- package/src/onboarding/tutorial/storage.ts +249 -0
- package/src/onboarding/tutorial/tutorial-system.ts +462 -0
- package/src/onboarding/tutorial/types.ts +310 -0
- package/src/orchestrator-singleton.ts +129 -0
- package/src/shutdown.ts +33 -0
- package/src/test/e2e/assertions.ts +267 -0
- package/src/test/e2e/fixtures.ts +204 -0
- package/src/test/e2e/harness.ts +575 -0
- package/src/test/e2e/index.ts +57 -0
- package/src/test/e2e/types.ts +228 -0
- package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
- package/src/test/fixtures/fake-response-loader.ts +314 -0
- package/src/test/fixtures/index.ts +20 -0
- package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
- package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
- package/src/tui/__tests__/mode-components.test.tsx +395 -0
- package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
- package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
- package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
- package/src/tui/adapters/agent-adapter.ts +1008 -0
- package/src/tui/adapters/index.ts +48 -0
- package/src/tui/adapters/message-adapter.ts +315 -0
- package/src/tui/adapters/persistence-bridge.ts +331 -0
- package/src/tui/adapters/session-adapter.ts +419 -0
- package/src/tui/buffered-stdout.ts +223 -0
- package/src/tui/components/AgentProgress.tsx +424 -0
- package/src/tui/components/Banner/AsciiArt.ts +160 -0
- package/src/tui/components/Banner/Banner.tsx +355 -0
- package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
- package/src/tui/components/Banner/ShimmerText.tsx +193 -0
- package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
- package/src/tui/components/Banner/index.ts +61 -0
- package/src/tui/components/Banner/useShimmer.ts +241 -0
- package/src/tui/components/ChatView.tsx +11 -0
- package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
- package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
- package/src/tui/components/Checkpoint/index.ts +19 -0
- package/src/tui/components/CostDisplay.tsx +226 -0
- package/src/tui/components/InitErrorBanner.tsx +122 -0
- package/src/tui/components/Input/Autocomplete.tsx +603 -0
- package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
- package/src/tui/components/Input/HighlightedText.tsx +236 -0
- package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
- package/src/tui/components/Input/TextInput.tsx +1002 -0
- package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
- package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
- package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
- package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
- package/src/tui/components/Input/highlight.ts +362 -0
- package/src/tui/components/Input/index.ts +36 -0
- package/src/tui/components/Input/slash-command-utils.ts +135 -0
- package/src/tui/components/Layout.tsx +432 -0
- package/src/tui/components/McpPanel.tsx +137 -0
- package/src/tui/components/MemoryPanel.tsx +448 -0
- package/src/tui/components/Messages/CodeBlock.tsx +527 -0
- package/src/tui/components/Messages/DiffView.tsx +679 -0
- package/src/tui/components/Messages/ImageReference.tsx +89 -0
- package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
- package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
- package/src/tui/components/Messages/MessageBubble.tsx +270 -0
- package/src/tui/components/Messages/MessageList.tsx +1719 -0
- package/src/tui/components/Messages/StreamingText.tsx +216 -0
- package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
- package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
- package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
- package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
- package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
- package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
- package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
- package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
- package/src/tui/components/Messages/index.ts +25 -0
- package/src/tui/components/ModeIndicator.tsx +177 -0
- package/src/tui/components/ModeSelector.tsx +216 -0
- package/src/tui/components/ModelSelector.tsx +339 -0
- package/src/tui/components/OnboardingWizard.tsx +670 -0
- package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
- package/src/tui/components/RateLimitIndicator.tsx +82 -0
- package/src/tui/components/ScreenReaderLayout.tsx +295 -0
- package/src/tui/components/SettingsPanel.tsx +643 -0
- package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
- package/src/tui/components/Sidebar/index.ts +9 -0
- package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
- package/src/tui/components/Status/index.ts +12 -0
- package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
- package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
- package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
- package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
- package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
- package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
- package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
- package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
- package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
- package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
- package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
- package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
- package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
- package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
- package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
- package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
- package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
- package/src/tui/components/StatusBar/index.ts +60 -0
- package/src/tui/components/TipBanner.tsx +115 -0
- package/src/tui/components/TodoItem.tsx +208 -0
- package/src/tui/components/TodoPanel.tsx +455 -0
- package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
- package/src/tui/components/Tools/OptionSelector.tsx +160 -0
- package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
- package/src/tui/components/Tools/ToolParams.tsx +483 -0
- package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
- package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
- package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
- package/src/tui/components/Tools/index.ts +21 -0
- package/src/tui/components/TrustPrompt.tsx +279 -0
- package/src/tui/components/UpdateBanner.tsx +166 -0
- package/src/tui/components/VimModeIndicator.tsx +112 -0
- package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
- package/src/tui/components/backtrack/index.ts +13 -0
- package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
- package/src/tui/components/common/CostWarning.tsx +294 -0
- package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
- package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
- package/src/tui/components/common/ErrorBoundary.tsx +140 -0
- package/src/tui/components/common/GradientText.tsx +224 -0
- package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
- package/src/tui/components/common/HotkeyHints.tsx +70 -0
- package/src/tui/components/common/MaxSizedBox.tsx +354 -0
- package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
- package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
- package/src/tui/components/common/ScrollIndicator.tsx +160 -0
- package/src/tui/components/common/Spinner.tsx +342 -0
- package/src/tui/components/common/StreamingIndicator.tsx +316 -0
- package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
- package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
- package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
- package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
- package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
- package/src/tui/components/common/VirtualizedList/index.ts +30 -0
- package/src/tui/components/common/VirtualizedList/types.ts +107 -0
- package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
- package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
- package/src/tui/components/common/index.ts +110 -0
- package/src/tui/components/index.ts +79 -0
- package/src/tui/components/session/CheckpointPanel.tsx +323 -0
- package/src/tui/components/session/RollbackDialog.tsx +169 -0
- package/src/tui/components/session/SessionItem.tsx +136 -0
- package/src/tui/components/session/SessionListPanel.tsx +252 -0
- package/src/tui/components/session/SessionPicker.tsx +449 -0
- package/src/tui/components/session/SessionPreview.tsx +240 -0
- package/src/tui/components/session/__tests__/session.test.tsx +408 -0
- package/src/tui/components/session/index.ts +28 -0
- package/src/tui/components/session/types.ts +116 -0
- package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
- package/src/tui/components/theme/index.ts +227 -0
- package/src/tui/components/theme/tokens.ts +484 -0
- package/src/tui/config/defaults.ts +134 -0
- package/src/tui/config/index.ts +17 -0
- package/src/tui/context/AnimationContext.tsx +284 -0
- package/src/tui/context/AppContext.tsx +349 -0
- package/src/tui/context/BracketedPasteContext.tsx +372 -0
- package/src/tui/context/LspContext.tsx +192 -0
- package/src/tui/context/McpContext.tsx +325 -0
- package/src/tui/context/MessagesContext.tsx +870 -0
- package/src/tui/context/OverflowContext.tsx +213 -0
- package/src/tui/context/RateLimitContext.tsx +108 -0
- package/src/tui/context/ResilienceContext.tsx +275 -0
- package/src/tui/context/RootProvider.tsx +136 -0
- package/src/tui/context/ScrollContext.tsx +331 -0
- package/src/tui/context/ToolsContext.tsx +702 -0
- package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
- package/src/tui/context/index.ts +140 -0
- package/src/tui/enterprise-integration.ts +282 -0
- package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
- package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
- package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
- package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
- package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
- package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
- package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
- package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
- package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
- package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
- package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
- package/src/tui/hooks/index.ts +252 -0
- package/src/tui/hooks/useAgentLoop.ts +712 -0
- package/src/tui/hooks/useAlternateBuffer.ts +398 -0
- package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
- package/src/tui/hooks/useBacktrack.ts +443 -0
- package/src/tui/hooks/useBracketedPaste.ts +104 -0
- package/src/tui/hooks/useCollapsible.ts +240 -0
- package/src/tui/hooks/useCopyMode.ts +382 -0
- package/src/tui/hooks/useCostSummary.ts +75 -0
- package/src/tui/hooks/useDesktopNotification.ts +414 -0
- package/src/tui/hooks/useDiffMode.ts +44 -0
- package/src/tui/hooks/useFileChangeStats.ts +110 -0
- package/src/tui/hooks/useFileSuggestions.ts +284 -0
- package/src/tui/hooks/useFlickerDetector.ts +250 -0
- package/src/tui/hooks/useGitStatus.ts +200 -0
- package/src/tui/hooks/useHotkeys.ts +579 -0
- package/src/tui/hooks/useImagePaste.ts +114 -0
- package/src/tui/hooks/useInputHighlight.ts +145 -0
- package/src/tui/hooks/useInputHistory.ts +246 -0
- package/src/tui/hooks/useKeyboardScroll.ts +209 -0
- package/src/tui/hooks/useLineBuffer.ts +356 -0
- package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
- package/src/tui/hooks/useModeController.ts +167 -0
- package/src/tui/hooks/useModeShortcuts.ts +196 -0
- package/src/tui/hooks/usePermissionHandler.ts +146 -0
- package/src/tui/hooks/usePersistence.ts +480 -0
- package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
- package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
- package/src/tui/hooks/useProviderStatus.ts +270 -0
- package/src/tui/hooks/useRateLimitStatus.ts +90 -0
- package/src/tui/hooks/useScreenReader.ts +315 -0
- package/src/tui/hooks/useScrollController.ts +450 -0
- package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
- package/src/tui/hooks/useSidebarPanelData.ts +115 -0
- package/src/tui/hooks/useSmoothScroll.ts +202 -0
- package/src/tui/hooks/useSnapshots.ts +300 -0
- package/src/tui/hooks/useStateAndRef.ts +50 -0
- package/src/tui/hooks/useTerminalSize.ts +206 -0
- package/src/tui/hooks/useToolApprovalController.ts +91 -0
- package/src/tui/hooks/useVim.ts +334 -0
- package/src/tui/hooks/useWorkspace.ts +56 -0
- package/src/tui/i18n/__tests__/init.test.ts +278 -0
- package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
- package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
- package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
- package/src/tui/i18n/index.ts +72 -0
- package/src/tui/i18n/init.ts +131 -0
- package/src/tui/i18n/language-config.ts +106 -0
- package/src/tui/i18n/locale-detection.ts +173 -0
- package/src/tui/i18n/settings-integration.ts +557 -0
- package/src/tui/i18n/tui-namespace.ts +538 -0
- package/src/tui/i18n/types.ts +312 -0
- package/src/tui/index.ts +43 -0
- package/src/tui/lsp-integration.ts +409 -0
- package/src/tui/metrics-integration.ts +366 -0
- package/src/tui/plugins.ts +383 -0
- package/src/tui/resilience.ts +342 -0
- package/src/tui/sandbox-integration.ts +317 -0
- package/src/tui/services/clipboard.ts +348 -0
- package/src/tui/services/fuzzy-search.ts +441 -0
- package/src/tui/services/index.ts +72 -0
- package/src/tui/services/markdown-renderer.ts +565 -0
- package/src/tui/services/open-external.ts +247 -0
- package/src/tui/services/syntax-highlighter.ts +483 -0
- package/src/tui/slash-commands.ts +12 -0
- package/src/tui/theme/index.ts +15 -0
- package/src/tui/theme/provider.tsx +206 -0
- package/src/tui/tip-integration.ts +300 -0
- package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
- package/src/tui/types/ink-extended.ts +87 -0
- package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
- package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
- package/src/tui/utils/__tests__/text-width.test.ts +158 -0
- package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
- package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
- package/src/tui/utils/bracketedPaste.ts +107 -0
- package/src/tui/utils/cursor-manager.ts +131 -0
- package/src/tui/utils/detectTerminal.ts +596 -0
- package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
- package/src/tui/utils/heightEstimator.ts +198 -0
- package/src/tui/utils/index.ts +91 -0
- package/src/tui/utils/isNarrowWidth.ts +52 -0
- package/src/tui/utils/stdoutGuard.ts +90 -0
- package/src/tui/utils/synchronized-update.ts +70 -0
- package/src/tui/utils/text-width.ts +225 -0
- package/src/tui/utils/textSanitizer.ts +225 -0
- package/src/tui/utils/textUtils.ts +114 -0
- package/src/tui/utils/ui-sizing.ts +192 -0
- package/src/tui-blessed/app.ts +160 -0
- package/src/tui-blessed/index.ts +2 -0
- package/src/tui-blessed/neo-blessed.d.ts +6 -0
- package/src/tui-blessed/test.ts +21 -0
- package/src/tui-blessed/types.ts +14 -0
- package/src/utils/icons.ts +130 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/resume-hint.ts +86 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +8 -0
- 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
|
+
}
|