@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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFileSuggestions Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides file and folder suggestions for @ mention autocomplete.
|
|
5
|
+
* Scans the file system and returns matching suggestions based on partial path input.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/hooks/useFileSuggestions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { useCallback, useEffect, useState } from "react";
|
|
13
|
+
import type { FileSuggestion } from "../components/Input/MentionAutocomplete.js";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for file suggestions.
|
|
21
|
+
*/
|
|
22
|
+
export interface UseFileSuggestionsOptions {
|
|
23
|
+
/** Working directory for resolving paths */
|
|
24
|
+
readonly cwd: string;
|
|
25
|
+
/** Whether to include files (default: true) */
|
|
26
|
+
readonly includeFiles?: boolean;
|
|
27
|
+
/** Whether to include directories (default: true) */
|
|
28
|
+
readonly includeDirectories?: boolean;
|
|
29
|
+
/** File extensions to filter (e.g., ['.ts', '.tsx']) */
|
|
30
|
+
readonly extensions?: readonly string[];
|
|
31
|
+
/** Maximum number of suggestions */
|
|
32
|
+
readonly maxSuggestions?: number;
|
|
33
|
+
/** Debounce delay in ms */
|
|
34
|
+
readonly debounceMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Result of the useFileSuggestions hook.
|
|
39
|
+
*/
|
|
40
|
+
export interface UseFileSuggestionsResult {
|
|
41
|
+
/** Current file/folder suggestions */
|
|
42
|
+
readonly suggestions: readonly FileSuggestion[];
|
|
43
|
+
/** Whether suggestions are loading */
|
|
44
|
+
readonly loading: boolean;
|
|
45
|
+
/** Error message if scan failed */
|
|
46
|
+
readonly error: string | null;
|
|
47
|
+
/** Refresh suggestions manually */
|
|
48
|
+
readonly refresh: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Constants
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
/** Directories to skip when scanning */
|
|
56
|
+
const SKIP_DIRECTORIES = new Set([
|
|
57
|
+
"node_modules",
|
|
58
|
+
".git",
|
|
59
|
+
".svn",
|
|
60
|
+
".hg",
|
|
61
|
+
"__pycache__",
|
|
62
|
+
".cache",
|
|
63
|
+
"dist",
|
|
64
|
+
"build",
|
|
65
|
+
"coverage",
|
|
66
|
+
".next",
|
|
67
|
+
".nuxt",
|
|
68
|
+
".output",
|
|
69
|
+
"target",
|
|
70
|
+
"vendor",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/** Default options */
|
|
74
|
+
const DEFAULT_OPTIONS: Required<Omit<UseFileSuggestionsOptions, "cwd">> = {
|
|
75
|
+
includeFiles: true,
|
|
76
|
+
includeDirectories: true,
|
|
77
|
+
extensions: [],
|
|
78
|
+
maxSuggestions: 50,
|
|
79
|
+
debounceMs: 100,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Hook Implementation
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hook to provide file/folder suggestions for a partial path.
|
|
88
|
+
*
|
|
89
|
+
* @param partialPath - The partial path entered by the user
|
|
90
|
+
* @param options - Configuration options
|
|
91
|
+
* @returns File suggestions, loading state, and error
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* const { suggestions, loading } = useFileSuggestions("./src/", {
|
|
96
|
+
* cwd: "/project",
|
|
97
|
+
* includeFiles: true,
|
|
98
|
+
* includeDirectories: true,
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function useFileSuggestions(
|
|
103
|
+
partialPath: string,
|
|
104
|
+
options: UseFileSuggestionsOptions
|
|
105
|
+
): UseFileSuggestionsResult {
|
|
106
|
+
const [suggestions, setSuggestions] = useState<FileSuggestion[]>([]);
|
|
107
|
+
const [loading, setLoading] = useState(false);
|
|
108
|
+
const [error, setError] = useState<string | null>(null);
|
|
109
|
+
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
110
|
+
|
|
111
|
+
// Memoize options to avoid recreating object on each render
|
|
112
|
+
const cwd = options.cwd;
|
|
113
|
+
const includeFiles = options.includeFiles ?? DEFAULT_OPTIONS.includeFiles;
|
|
114
|
+
const includeDirectories = options.includeDirectories ?? DEFAULT_OPTIONS.includeDirectories;
|
|
115
|
+
const extensions = options.extensions ?? DEFAULT_OPTIONS.extensions;
|
|
116
|
+
const maxSuggestions = options.maxSuggestions ?? DEFAULT_OPTIONS.maxSuggestions;
|
|
117
|
+
const debounceMs = options.debounceMs ?? DEFAULT_OPTIONS.debounceMs;
|
|
118
|
+
|
|
119
|
+
const refresh = useCallback(() => {
|
|
120
|
+
setRefreshTrigger((prev) => prev + 1);
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: refreshTrigger enables manual refresh
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
let cancelled = false;
|
|
126
|
+
const opts: Required<UseFileSuggestionsOptions> = {
|
|
127
|
+
cwd,
|
|
128
|
+
includeFiles,
|
|
129
|
+
includeDirectories,
|
|
130
|
+
extensions,
|
|
131
|
+
maxSuggestions,
|
|
132
|
+
debounceMs,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const timeoutId = setTimeout(async () => {
|
|
136
|
+
if (cancelled) return;
|
|
137
|
+
|
|
138
|
+
setLoading(true);
|
|
139
|
+
setError(null);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const result = await scanForSuggestions(partialPath, opts);
|
|
143
|
+
if (!cancelled) {
|
|
144
|
+
setSuggestions(result);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (!cancelled) {
|
|
148
|
+
setError((err as Error).message);
|
|
149
|
+
setSuggestions([]);
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
if (!cancelled) {
|
|
153
|
+
setLoading(false);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}, debounceMs);
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
cancelled = true;
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
};
|
|
162
|
+
}, [
|
|
163
|
+
partialPath,
|
|
164
|
+
cwd,
|
|
165
|
+
includeFiles,
|
|
166
|
+
includeDirectories,
|
|
167
|
+
extensions,
|
|
168
|
+
maxSuggestions,
|
|
169
|
+
debounceMs,
|
|
170
|
+
refreshTrigger,
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
return { suggestions, loading, error, refresh };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Helper Functions
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/** Parse partial path into scan directory and match prefix */
|
|
181
|
+
function parsePartialPath(partialPath: string): { scanDir: string; matchPrefix: string } {
|
|
182
|
+
const normalizedPartial = partialPath.replace(/\\/g, "/");
|
|
183
|
+
|
|
184
|
+
if (normalizedPartial.endsWith("/") || normalizedPartial === "") {
|
|
185
|
+
return { scanDir: normalizedPartial || ".", matchPrefix: "" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const lastSlash = normalizedPartial.lastIndexOf("/");
|
|
189
|
+
if (lastSlash === -1) {
|
|
190
|
+
return { scanDir: ".", matchPrefix: normalizedPartial };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
scanDir: normalizedPartial.slice(0, lastSlash + 1) || ".",
|
|
195
|
+
matchPrefix: normalizedPartial.slice(lastSlash + 1),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Check if entry should be included based on filters */
|
|
200
|
+
function shouldIncludeEntry(
|
|
201
|
+
entry: { name: string; isDirectory: () => boolean },
|
|
202
|
+
matchPrefix: string,
|
|
203
|
+
includeFiles: boolean,
|
|
204
|
+
includeDirectories: boolean,
|
|
205
|
+
extensions: readonly string[]
|
|
206
|
+
): boolean {
|
|
207
|
+
const isDir = entry.isDirectory();
|
|
208
|
+
const lowerName = entry.name.toLowerCase();
|
|
209
|
+
const lowerPrefix = matchPrefix.toLowerCase();
|
|
210
|
+
|
|
211
|
+
// Skip hidden files unless searching for them
|
|
212
|
+
if (entry.name.startsWith(".") && !matchPrefix.startsWith(".")) return false;
|
|
213
|
+
|
|
214
|
+
// Skip ignored directories
|
|
215
|
+
if (isDir && SKIP_DIRECTORIES.has(entry.name)) return false;
|
|
216
|
+
|
|
217
|
+
// Check prefix match
|
|
218
|
+
if (lowerPrefix && !lowerName.startsWith(lowerPrefix)) return false;
|
|
219
|
+
|
|
220
|
+
// Filter by type
|
|
221
|
+
if (isDir && !includeDirectories) return false;
|
|
222
|
+
if (!isDir && !includeFiles) return false;
|
|
223
|
+
|
|
224
|
+
// Filter by extension (only for files)
|
|
225
|
+
if (!isDir && extensions.length > 0) {
|
|
226
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
227
|
+
if (!extensions.includes(ext)) return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Scan for file/folder suggestions based on partial path.
|
|
235
|
+
*/
|
|
236
|
+
async function scanForSuggestions(
|
|
237
|
+
partialPath: string,
|
|
238
|
+
options: Required<UseFileSuggestionsOptions>
|
|
239
|
+
): Promise<FileSuggestion[]> {
|
|
240
|
+
const { cwd, includeFiles, includeDirectories, extensions, maxSuggestions } = options;
|
|
241
|
+
|
|
242
|
+
const { scanDir, matchPrefix } = parsePartialPath(partialPath);
|
|
243
|
+
const fullScanDir = path.resolve(cwd, scanDir);
|
|
244
|
+
|
|
245
|
+
// Check if directory exists
|
|
246
|
+
try {
|
|
247
|
+
const stats = await fs.stat(fullScanDir);
|
|
248
|
+
if (!stats.isDirectory()) return [];
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Read and filter directory contents
|
|
254
|
+
const entries = await fs.readdir(fullScanDir, { withFileTypes: true });
|
|
255
|
+
const suggestions: FileSuggestion[] = [];
|
|
256
|
+
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
if (!shouldIncludeEntry(entry, matchPrefix, includeFiles, includeDirectories, extensions)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const isDirectory = entry.isDirectory();
|
|
263
|
+
const fullPath = scanDir === "." ? entry.name : `${scanDir}${entry.name}`;
|
|
264
|
+
|
|
265
|
+
suggestions.push({
|
|
266
|
+
name: entry.name,
|
|
267
|
+
path: fullPath,
|
|
268
|
+
isDirectory,
|
|
269
|
+
extension: isDirectory ? undefined : path.extname(entry.name).slice(1),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (suggestions.length >= maxSuggestions) break;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Sort: directories first, then alphabetically
|
|
276
|
+
suggestions.sort((a, b) => {
|
|
277
|
+
if (a.isDirectory !== b.isDirectory) {
|
|
278
|
+
return a.isDirectory ? -1 : 1;
|
|
279
|
+
}
|
|
280
|
+
return a.name.localeCompare(b.name);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return suggestions;
|
|
284
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flicker Detector Hook
|
|
3
|
+
*
|
|
4
|
+
* Detects when content exceeds container bounds, which can cause
|
|
5
|
+
* visual flickering or rendering artifacts in terminal UIs.
|
|
6
|
+
*
|
|
7
|
+
* This hook is essential for preventing TUI rendering issues by
|
|
8
|
+
* detecting overflow conditions before they cause visual problems.
|
|
9
|
+
*
|
|
10
|
+
* Ported from Gemini CLI for Vellum TUI.
|
|
11
|
+
*
|
|
12
|
+
* @module tui/hooks/useFlickerDetector
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for the flicker detector hook
|
|
23
|
+
*/
|
|
24
|
+
export interface UseFlickerDetectorOptions {
|
|
25
|
+
/** Height of the content in rows/lines */
|
|
26
|
+
readonly contentHeight: number;
|
|
27
|
+
/** Height of the container in rows/lines */
|
|
28
|
+
readonly containerHeight: number;
|
|
29
|
+
/**
|
|
30
|
+
* Threshold in rows before considering content as overflowing.
|
|
31
|
+
* A small threshold helps prevent edge-case flickering.
|
|
32
|
+
* @default 0
|
|
33
|
+
*/
|
|
34
|
+
readonly threshold?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Enable debouncing to prevent rapid state changes.
|
|
37
|
+
* Useful when content height changes frequently.
|
|
38
|
+
* @default true
|
|
39
|
+
*/
|
|
40
|
+
readonly debounce?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Debounce delay in milliseconds.
|
|
43
|
+
* @default 50
|
|
44
|
+
*/
|
|
45
|
+
readonly debounceDelay?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Result from the flicker detector hook
|
|
50
|
+
*/
|
|
51
|
+
export interface FlickerDetectorResult {
|
|
52
|
+
/** Whether the content is overflowing the container */
|
|
53
|
+
readonly isOverflowing: boolean;
|
|
54
|
+
/** Amount of overflow in rows (negative if content fits) */
|
|
55
|
+
readonly overflow: number;
|
|
56
|
+
/** Percentage of container filled (can exceed 100%) */
|
|
57
|
+
readonly fillPercentage: number;
|
|
58
|
+
/** Whether overflow state recently changed (indicates potential flicker) */
|
|
59
|
+
readonly isTransitioning: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Constants
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
/** Default debounce delay in milliseconds */
|
|
67
|
+
const DEFAULT_DEBOUNCE_DELAY = 50;
|
|
68
|
+
|
|
69
|
+
/** Transition window for detecting rapid state changes (ms) */
|
|
70
|
+
const TRANSITION_WINDOW = 100;
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Hook Implementation
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Hook to detect content overflow that may cause visual flickering.
|
|
78
|
+
*
|
|
79
|
+
* Terminal UIs can flicker when content rapidly alternates between
|
|
80
|
+
* fitting and overflowing the visible area. This hook provides:
|
|
81
|
+
*
|
|
82
|
+
* 1. **Overflow detection**: Know when content exceeds bounds
|
|
83
|
+
* 2. **Transition tracking**: Detect rapid state changes
|
|
84
|
+
* 3. **Debouncing**: Prevent jittery state updates
|
|
85
|
+
*
|
|
86
|
+
* @param options - Configuration for overflow detection
|
|
87
|
+
* @returns Overflow state and metrics
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* function MessageList({ messages, containerHeight }) {
|
|
92
|
+
* const contentHeight = messages.length * 3; // Estimate 3 rows per message
|
|
93
|
+
*
|
|
94
|
+
* const { isOverflowing, overflow, isTransitioning } = useFlickerDetector({
|
|
95
|
+
* contentHeight,
|
|
96
|
+
* containerHeight,
|
|
97
|
+
* threshold: 2, // Allow 2-row buffer
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* if (isOverflowing) {
|
|
101
|
+
* // Enable virtualization or truncation
|
|
102
|
+
* return <VirtualizedList data={messages} />;
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* // Render all messages directly
|
|
106
|
+
* return messages.map(msg => <Message key={msg.id} {...msg} />);
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function useFlickerDetector(options: UseFlickerDetectorOptions): FlickerDetectorResult {
|
|
111
|
+
const {
|
|
112
|
+
contentHeight,
|
|
113
|
+
containerHeight,
|
|
114
|
+
threshold = 0,
|
|
115
|
+
debounce = true,
|
|
116
|
+
debounceDelay = DEFAULT_DEBOUNCE_DELAY,
|
|
117
|
+
} = options;
|
|
118
|
+
|
|
119
|
+
// Track the raw overflow state
|
|
120
|
+
const [debouncedOverflow, setDebouncedOverflow] = useState(() => {
|
|
121
|
+
return contentHeight - containerHeight > threshold;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Track rapid transitions
|
|
125
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
126
|
+
const lastChangeTime = useRef<number>(0);
|
|
127
|
+
const transitionTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Calculate raw overflow state (not debounced)
|
|
131
|
+
*/
|
|
132
|
+
const rawOverflow = useMemo(() => {
|
|
133
|
+
return contentHeight - containerHeight;
|
|
134
|
+
}, [contentHeight, containerHeight]);
|
|
135
|
+
|
|
136
|
+
const rawIsOverflowing = rawOverflow > threshold;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Calculate fill percentage
|
|
140
|
+
*/
|
|
141
|
+
const fillPercentage = useMemo(() => {
|
|
142
|
+
if (containerHeight <= 0) return 0;
|
|
143
|
+
return (contentHeight / containerHeight) * 100;
|
|
144
|
+
}, [contentHeight, containerHeight]);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handle state transitions with optional debouncing
|
|
148
|
+
*/
|
|
149
|
+
const updateOverflowState = useCallback((newState: boolean) => {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const timeSinceLastChange = now - lastChangeTime.current;
|
|
152
|
+
|
|
153
|
+
// Detect rapid transitions (potential flicker source)
|
|
154
|
+
if (timeSinceLastChange < TRANSITION_WINDOW) {
|
|
155
|
+
setIsTransitioning(true);
|
|
156
|
+
|
|
157
|
+
// Clear existing timeout
|
|
158
|
+
if (transitionTimeout.current) {
|
|
159
|
+
clearTimeout(transitionTimeout.current);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Clear transition flag after window
|
|
163
|
+
transitionTimeout.current = setTimeout(() => {
|
|
164
|
+
setIsTransitioning(false);
|
|
165
|
+
}, TRANSITION_WINDOW);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lastChangeTime.current = now;
|
|
169
|
+
setDebouncedOverflow(newState);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Effect to update overflow state with optional debouncing
|
|
174
|
+
*/
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!debounce) {
|
|
177
|
+
// No debouncing - update immediately
|
|
178
|
+
if (rawIsOverflowing !== debouncedOverflow) {
|
|
179
|
+
updateOverflowState(rawIsOverflowing);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Debounced update
|
|
185
|
+
const timeoutId = setTimeout(() => {
|
|
186
|
+
if (rawIsOverflowing !== debouncedOverflow) {
|
|
187
|
+
updateOverflowState(rawIsOverflowing);
|
|
188
|
+
}
|
|
189
|
+
}, debounceDelay);
|
|
190
|
+
|
|
191
|
+
return () => {
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
};
|
|
194
|
+
}, [rawIsOverflowing, debouncedOverflow, debounce, debounceDelay, updateOverflowState]);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Cleanup transition timeout on unmount
|
|
198
|
+
*/
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
return () => {
|
|
201
|
+
if (transitionTimeout.current) {
|
|
202
|
+
clearTimeout(transitionTimeout.current);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Return memoized result
|
|
209
|
+
*/
|
|
210
|
+
return useMemo<FlickerDetectorResult>(
|
|
211
|
+
() => ({
|
|
212
|
+
isOverflowing: debounce ? debouncedOverflow : rawIsOverflowing,
|
|
213
|
+
overflow: rawOverflow,
|
|
214
|
+
fillPercentage,
|
|
215
|
+
isTransitioning,
|
|
216
|
+
}),
|
|
217
|
+
[debounce, debouncedOverflow, rawIsOverflowing, rawOverflow, fillPercentage, isTransitioning]
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// =============================================================================
|
|
222
|
+
// Utility Functions
|
|
223
|
+
// =============================================================================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Simple overflow check without the full hook (for one-off calculations).
|
|
227
|
+
*
|
|
228
|
+
* @param contentHeight - Height of the content
|
|
229
|
+
* @param containerHeight - Height of the container
|
|
230
|
+
* @param threshold - Buffer threshold (default: 0)
|
|
231
|
+
* @returns Whether content is overflowing
|
|
232
|
+
*/
|
|
233
|
+
export function isContentOverflowing(
|
|
234
|
+
contentHeight: number,
|
|
235
|
+
containerHeight: number,
|
|
236
|
+
threshold = 0
|
|
237
|
+
): boolean {
|
|
238
|
+
return contentHeight - containerHeight > threshold;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Calculate recommended container height to fit content with buffer.
|
|
243
|
+
*
|
|
244
|
+
* @param contentHeight - Height of the content
|
|
245
|
+
* @param bufferRows - Additional buffer rows (default: 2)
|
|
246
|
+
* @returns Recommended container height
|
|
247
|
+
*/
|
|
248
|
+
export function calculateSafeContainerHeight(contentHeight: number, bufferRows = 2): number {
|
|
249
|
+
return contentHeight + bufferRows;
|
|
250
|
+
}
|