@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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line Buffer Hook
|
|
3
|
+
*
|
|
4
|
+
* Pre-wraps messages into line arrays for efficient scroll calculations.
|
|
5
|
+
* Uses a ring buffer to limit memory usage with very long conversations.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Pre-wraps text at specified terminal width
|
|
9
|
+
* - Caches wrap results by message ID + width
|
|
10
|
+
* - Ring buffer discards oldest entries when maxLines exceeded
|
|
11
|
+
* - O(1) line range queries via index lookup
|
|
12
|
+
*
|
|
13
|
+
* @module tui/hooks/useLineBuffer
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useMemo, useRef } from "react";
|
|
17
|
+
import stringWidth from "string-width";
|
|
18
|
+
import type { Message } from "../context/MessagesContext.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A single entry in the line buffer representing one message's wrapped lines.
|
|
26
|
+
*/
|
|
27
|
+
export interface LineBufferEntry {
|
|
28
|
+
/** Original message ID */
|
|
29
|
+
readonly messageId: string;
|
|
30
|
+
/** Pre-wrapped lines (each line fits within wrapWidth) */
|
|
31
|
+
readonly lines: readonly string[];
|
|
32
|
+
/** Width used for wrapping (for cache invalidation) */
|
|
33
|
+
readonly wrapWidth: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* State returned by useLineBuffer hook.
|
|
38
|
+
*/
|
|
39
|
+
export interface LineBufferState {
|
|
40
|
+
/** All entries in the buffer */
|
|
41
|
+
readonly entries: readonly LineBufferEntry[];
|
|
42
|
+
/** Total line count across all entries */
|
|
43
|
+
readonly totalLines: number;
|
|
44
|
+
/**
|
|
45
|
+
* Get lines in range [start, end).
|
|
46
|
+
* Uses 0-based line indexing across all messages.
|
|
47
|
+
*/
|
|
48
|
+
getVisibleLines(start: number, end: number): string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for useLineBuffer hook.
|
|
53
|
+
*/
|
|
54
|
+
export interface UseLineBufferOptions {
|
|
55
|
+
/** Terminal width for wrapping */
|
|
56
|
+
readonly width: number;
|
|
57
|
+
/** Max lines to keep (ring buffer). Default: 10000 */
|
|
58
|
+
readonly maxLines?: number;
|
|
59
|
+
/** Content padding to subtract from width. Default: 4 */
|
|
60
|
+
readonly contentPadding?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Constants
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/** Default maximum lines to keep in buffer */
|
|
68
|
+
const DEFAULT_MAX_LINES = 10000;
|
|
69
|
+
|
|
70
|
+
/** Default content padding for wrapping */
|
|
71
|
+
const DEFAULT_CONTENT_PADDING = 4;
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// Pure Functions
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wrap a single line of text to fit within a given width.
|
|
79
|
+
* Uses string-width for accurate CJK/Emoji/ANSI handling.
|
|
80
|
+
*
|
|
81
|
+
* @param text - Text to wrap (single logical line, no newlines)
|
|
82
|
+
* @param width - Maximum display width per line
|
|
83
|
+
* @returns Array of wrapped lines
|
|
84
|
+
*/
|
|
85
|
+
export function wrapLine(text: string, width: number): string[] {
|
|
86
|
+
const safeWidth = Math.max(1, width);
|
|
87
|
+
|
|
88
|
+
// Empty or whitespace-only lines
|
|
89
|
+
if (!text || text.trim().length === 0) {
|
|
90
|
+
return [text || ""];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result: string[] = [];
|
|
94
|
+
let currentLine = "";
|
|
95
|
+
let currentWidth = 0;
|
|
96
|
+
|
|
97
|
+
// Process character by character for accurate width handling
|
|
98
|
+
// This handles CJK (2-width), emoji, and combining characters correctly
|
|
99
|
+
const chars = [...text]; // Spread to handle multi-byte chars
|
|
100
|
+
|
|
101
|
+
for (const char of chars) {
|
|
102
|
+
const charWidth = stringWidth(char);
|
|
103
|
+
|
|
104
|
+
// Would adding this char exceed the width?
|
|
105
|
+
if (currentWidth + charWidth > safeWidth && currentLine.length > 0) {
|
|
106
|
+
result.push(currentLine);
|
|
107
|
+
currentLine = char;
|
|
108
|
+
currentWidth = charWidth;
|
|
109
|
+
} else {
|
|
110
|
+
currentLine += char;
|
|
111
|
+
currentWidth += charWidth;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Don't forget the last line
|
|
116
|
+
if (currentLine.length > 0) {
|
|
117
|
+
result.push(currentLine);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle edge case: empty result
|
|
121
|
+
if (result.length === 0) {
|
|
122
|
+
result.push("");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Wrap multi-line text (with \n) to fit within width.
|
|
130
|
+
*
|
|
131
|
+
* @param text - Text to wrap (may contain newlines)
|
|
132
|
+
* @param width - Maximum display width per line
|
|
133
|
+
* @returns Array of wrapped lines
|
|
134
|
+
*/
|
|
135
|
+
export function wrapText(text: string, width: number): string[] {
|
|
136
|
+
if (!text) {
|
|
137
|
+
return [""];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lines = text.split("\n");
|
|
141
|
+
const result: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const wrapped = wrapLine(line, width);
|
|
145
|
+
result.push(...wrapped);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract displayable content from a message for line buffering.
|
|
153
|
+
*
|
|
154
|
+
* @param message - Message to extract content from
|
|
155
|
+
* @returns Combined text content (content + thinking + tool info)
|
|
156
|
+
*/
|
|
157
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Message content extraction with multiple optional fields
|
|
158
|
+
function extractMessageContent(message: Message): string {
|
|
159
|
+
const parts: string[] = [];
|
|
160
|
+
|
|
161
|
+
// Message header line
|
|
162
|
+
const role =
|
|
163
|
+
message.role === "user" ? "You" : message.role === "assistant" ? "Vellum" : message.role;
|
|
164
|
+
parts.push(`${role}:`);
|
|
165
|
+
|
|
166
|
+
// Thinking content (if present)
|
|
167
|
+
if (message.thinking && message.thinking.length > 0) {
|
|
168
|
+
parts.push(" Thinking...");
|
|
169
|
+
// Indent thinking content
|
|
170
|
+
const thinkingLines = message.thinking.split("\n");
|
|
171
|
+
for (const line of thinkingLines) {
|
|
172
|
+
parts.push(` ${line}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Main content
|
|
177
|
+
if (message.content) {
|
|
178
|
+
const contentLines = message.content.split("\n");
|
|
179
|
+
for (const line of contentLines) {
|
|
180
|
+
parts.push(` ${line}`);
|
|
181
|
+
}
|
|
182
|
+
} else if (!message.isStreaming) {
|
|
183
|
+
parts.push(" (empty)");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Tool calls (simplified representation)
|
|
187
|
+
if (message.toolCalls && message.toolCalls.length > 0) {
|
|
188
|
+
for (const tool of message.toolCalls) {
|
|
189
|
+
const status = tool.status === "completed" ? "+" : tool.status === "error" ? "x" : "-";
|
|
190
|
+
parts.push(` ${status} ${tool.name}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return parts.join("\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a LineBufferEntry for a message.
|
|
199
|
+
*
|
|
200
|
+
* @param message - Message to wrap
|
|
201
|
+
* @param width - Terminal width
|
|
202
|
+
* @param contentPadding - Padding to subtract from width
|
|
203
|
+
* @returns LineBufferEntry with wrapped lines
|
|
204
|
+
*/
|
|
205
|
+
function createEntry(message: Message, width: number, contentPadding: number): LineBufferEntry {
|
|
206
|
+
const contentWidth = Math.max(10, width - contentPadding);
|
|
207
|
+
const content = extractMessageContent(message);
|
|
208
|
+
const lines = wrapText(content, contentWidth);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
messageId: message.id,
|
|
212
|
+
lines,
|
|
213
|
+
wrapWidth: width,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Cache Key Generation
|
|
219
|
+
// =============================================================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generate a cache key for a message + width combination.
|
|
223
|
+
*/
|
|
224
|
+
function getCacheKey(messageId: string, width: number): string {
|
|
225
|
+
return `${messageId}:${width}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// Hook Implementation
|
|
230
|
+
// =============================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* useLineBuffer - Pre-wraps messages into line arrays for efficient scrolling.
|
|
234
|
+
*
|
|
235
|
+
* This hook maintains a cache of wrapped lines per message, re-wrapping only
|
|
236
|
+
* when width changes or new messages arrive. Uses a ring buffer to limit
|
|
237
|
+
* memory usage.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```tsx
|
|
241
|
+
* const lineBuffer = useLineBuffer(messages, {
|
|
242
|
+
* width: terminalWidth,
|
|
243
|
+
* maxLines: 5000,
|
|
244
|
+
* });
|
|
245
|
+
*
|
|
246
|
+
* // Get visible lines for current scroll position
|
|
247
|
+
* const visibleLines = lineBuffer.getVisibleLines(
|
|
248
|
+
* scrollState.offsetFromBottom,
|
|
249
|
+
* scrollState.offsetFromBottom + viewportHeight
|
|
250
|
+
* );
|
|
251
|
+
* ```
|
|
252
|
+
*
|
|
253
|
+
* @param messages - Array of messages to buffer
|
|
254
|
+
* @param options - Buffer configuration options
|
|
255
|
+
* @returns LineBufferState with entries and query methods
|
|
256
|
+
*/
|
|
257
|
+
export function useLineBuffer(
|
|
258
|
+
messages: readonly Message[],
|
|
259
|
+
options: UseLineBufferOptions
|
|
260
|
+
): LineBufferState {
|
|
261
|
+
const { width, maxLines = DEFAULT_MAX_LINES, contentPadding = DEFAULT_CONTENT_PADDING } = options;
|
|
262
|
+
|
|
263
|
+
// Cache for wrapped entries: Map<cacheKey, LineBufferEntry>
|
|
264
|
+
const cacheRef = useRef<Map<string, LineBufferEntry>>(new Map());
|
|
265
|
+
|
|
266
|
+
// Compute entries with memoization
|
|
267
|
+
const entries = useMemo(() => {
|
|
268
|
+
const cache = cacheRef.current;
|
|
269
|
+
const result: LineBufferEntry[] = [];
|
|
270
|
+
let totalLineCount = 0;
|
|
271
|
+
|
|
272
|
+
// Build entries, using cache when available
|
|
273
|
+
for (const message of messages) {
|
|
274
|
+
const cacheKey = getCacheKey(message.id, width);
|
|
275
|
+
let entry = cache.get(cacheKey);
|
|
276
|
+
|
|
277
|
+
// Cache miss or width changed - recompute
|
|
278
|
+
if (!entry || entry.wrapWidth !== width) {
|
|
279
|
+
entry = createEntry(message, width, contentPadding);
|
|
280
|
+
cache.set(cacheKey, entry);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
result.push(entry);
|
|
284
|
+
totalLineCount += entry.lines.length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Ring buffer: trim oldest entries if exceeding maxLines
|
|
288
|
+
while (totalLineCount > maxLines && result.length > 1) {
|
|
289
|
+
const removed = result.shift();
|
|
290
|
+
if (removed) {
|
|
291
|
+
totalLineCount -= removed.lines.length;
|
|
292
|
+
// Clean cache for removed entry
|
|
293
|
+
cache.delete(getCacheKey(removed.messageId, width));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Clean stale cache entries (messages no longer in list)
|
|
298
|
+
const activeKeys = new Set(result.map((e) => getCacheKey(e.messageId, width)));
|
|
299
|
+
for (const key of cache.keys()) {
|
|
300
|
+
if (!activeKeys.has(key)) {
|
|
301
|
+
cache.delete(key);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}, [messages, width, maxLines, contentPadding]);
|
|
307
|
+
|
|
308
|
+
// Compute total lines
|
|
309
|
+
const totalLines = useMemo(() => {
|
|
310
|
+
return entries.reduce((sum, entry) => sum + entry.lines.length, 0);
|
|
311
|
+
}, [entries]);
|
|
312
|
+
|
|
313
|
+
// Build line index for O(1) range queries
|
|
314
|
+
// lineIndex[i] = { entryIndex, lineOffset } for global line i
|
|
315
|
+
const lineIndex = useMemo(() => {
|
|
316
|
+
const index: Array<{ entryIndex: number; lineOffset: number }> = [];
|
|
317
|
+
let entryIdx = 0;
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
for (let lineOffset = 0; lineOffset < entry.lines.length; lineOffset++) {
|
|
320
|
+
index.push({ entryIndex: entryIdx, lineOffset });
|
|
321
|
+
}
|
|
322
|
+
entryIdx++;
|
|
323
|
+
}
|
|
324
|
+
return index;
|
|
325
|
+
}, [entries]);
|
|
326
|
+
|
|
327
|
+
// Get visible lines in range [start, end)
|
|
328
|
+
const getVisibleLines = useMemo(() => {
|
|
329
|
+
return (start: number, end: number): string[] => {
|
|
330
|
+
const result: string[] = [];
|
|
331
|
+
const safeStart = Math.max(0, start);
|
|
332
|
+
const safeEnd = Math.min(totalLines, end);
|
|
333
|
+
|
|
334
|
+
for (let i = safeStart; i < safeEnd; i++) {
|
|
335
|
+
const loc = lineIndex[i];
|
|
336
|
+
if (loc) {
|
|
337
|
+
const entry = entries[loc.entryIndex];
|
|
338
|
+
if (entry) {
|
|
339
|
+
const line = entry.lines[loc.lineOffset];
|
|
340
|
+
if (line !== undefined) {
|
|
341
|
+
result.push(line);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return result;
|
|
348
|
+
};
|
|
349
|
+
}, [entries, lineIndex, totalLines]);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
entries,
|
|
353
|
+
totalLines,
|
|
354
|
+
getVisibleLines,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMentionAutocomplete Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages @ mention autocomplete state and logic.
|
|
5
|
+
* Detects @ patterns in input and provides suggestions for types or values.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/hooks/useMentionAutocomplete
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
MENTION_PARTIAL_REGEX,
|
|
12
|
+
MENTION_VALUE_PARTIAL_REGEX,
|
|
13
|
+
type MentionType,
|
|
14
|
+
mentionRequiresValue,
|
|
15
|
+
} from "@vellum/shared";
|
|
16
|
+
import { useCallback, useMemo } from "react";
|
|
17
|
+
import type { MentionAutocompleteMode } from "../components/Input/MentionAutocomplete.js";
|
|
18
|
+
import { useFileSuggestions } from "./useFileSuggestions.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* State of the mention autocomplete system.
|
|
26
|
+
*/
|
|
27
|
+
export interface MentionAutocompleteState {
|
|
28
|
+
/** Whether autocomplete should be visible */
|
|
29
|
+
readonly visible: boolean;
|
|
30
|
+
/** Whether autocomplete should capture keyboard input */
|
|
31
|
+
readonly active: boolean;
|
|
32
|
+
/** Current mode: type selection or value completion */
|
|
33
|
+
readonly mode: MentionAutocompleteMode;
|
|
34
|
+
/** The detected mention type (in value mode) */
|
|
35
|
+
readonly mentionType: MentionType | null;
|
|
36
|
+
/** The partial input to filter against */
|
|
37
|
+
readonly filterText: string;
|
|
38
|
+
/** Start position of the mention in the input */
|
|
39
|
+
readonly mentionStart: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for useMentionAutocomplete.
|
|
44
|
+
*/
|
|
45
|
+
export interface UseMentionAutocompleteOptions {
|
|
46
|
+
/** Current working directory for file suggestions */
|
|
47
|
+
readonly cwd: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Result of the useMentionAutocomplete hook.
|
|
52
|
+
*/
|
|
53
|
+
export interface UseMentionAutocompleteResult {
|
|
54
|
+
/** Current autocomplete state */
|
|
55
|
+
readonly state: MentionAutocompleteState;
|
|
56
|
+
/** File suggestions (for file/folder mentions) */
|
|
57
|
+
readonly fileSuggestions: ReturnType<typeof useFileSuggestions>;
|
|
58
|
+
/** Handle selection of a type or value */
|
|
59
|
+
readonly handleSelect: (value: string, mode: MentionAutocompleteMode) => string;
|
|
60
|
+
/** Get completed input value after selection */
|
|
61
|
+
readonly getCompletedValue: (
|
|
62
|
+
currentValue: string,
|
|
63
|
+
selectedValue: string,
|
|
64
|
+
mode: MentionAutocompleteMode
|
|
65
|
+
) => string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Helper Functions
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detect @ mention pattern in input and determine autocomplete state.
|
|
74
|
+
*/
|
|
75
|
+
function detectMentionPattern(value: string): MentionAutocompleteState {
|
|
76
|
+
const defaultState: MentionAutocompleteState = {
|
|
77
|
+
visible: false,
|
|
78
|
+
active: false,
|
|
79
|
+
mode: "type",
|
|
80
|
+
mentionType: null,
|
|
81
|
+
filterText: "",
|
|
82
|
+
mentionStart: -1,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (!value.includes("@")) {
|
|
86
|
+
return defaultState;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for @type: pattern (value mode)
|
|
90
|
+
const valueMatch = value.match(MENTION_VALUE_PARTIAL_REGEX);
|
|
91
|
+
if (valueMatch) {
|
|
92
|
+
const mentionType = valueMatch[1] as MentionType;
|
|
93
|
+
const partialValue = valueMatch[2] || "";
|
|
94
|
+
const fullMatch = valueMatch[0];
|
|
95
|
+
const mentionStart = value.lastIndexOf(fullMatch);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
visible: true,
|
|
99
|
+
active: true,
|
|
100
|
+
mode: "value",
|
|
101
|
+
mentionType,
|
|
102
|
+
filterText: partialValue,
|
|
103
|
+
mentionStart,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for @ or @partial pattern (type mode)
|
|
108
|
+
const typeMatch = value.match(MENTION_PARTIAL_REGEX);
|
|
109
|
+
if (typeMatch) {
|
|
110
|
+
const partialType = typeMatch[1] || "";
|
|
111
|
+
const atIndex = value.lastIndexOf("@");
|
|
112
|
+
|
|
113
|
+
// Check if @ is at start or after whitespace
|
|
114
|
+
if (atIndex === 0 || /\s/.test(value[atIndex - 1] || "")) {
|
|
115
|
+
return {
|
|
116
|
+
visible: true,
|
|
117
|
+
active: true,
|
|
118
|
+
mode: "type",
|
|
119
|
+
mentionType: null,
|
|
120
|
+
filterText: partialType,
|
|
121
|
+
mentionStart: atIndex,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return defaultState;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// Hook Implementation
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Hook to manage @ mention autocomplete.
|
|
135
|
+
*
|
|
136
|
+
* @param inputValue - Current input value
|
|
137
|
+
* @param options - Configuration options
|
|
138
|
+
* @returns Autocomplete state and handlers
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```tsx
|
|
142
|
+
* const { state, fileSuggestions, handleSelect } = useMentionAutocomplete(
|
|
143
|
+
* inputValue,
|
|
144
|
+
* { cwd: "/project" }
|
|
145
|
+
* );
|
|
146
|
+
*
|
|
147
|
+
* if (state.visible) {
|
|
148
|
+
* return (
|
|
149
|
+
* <MentionAutocomplete
|
|
150
|
+
* mode={state.mode}
|
|
151
|
+
* input={state.filterText}
|
|
152
|
+
* mentionType={state.mentionType}
|
|
153
|
+
* fileSuggestions={fileSuggestions.suggestions}
|
|
154
|
+
* onSelect={(v, m) => setValue(handleSelect(v, m))}
|
|
155
|
+
* />
|
|
156
|
+
* );
|
|
157
|
+
* }
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function useMentionAutocomplete(
|
|
161
|
+
inputValue: string,
|
|
162
|
+
options: UseMentionAutocompleteOptions
|
|
163
|
+
): UseMentionAutocompleteResult {
|
|
164
|
+
// Detect mention pattern in input
|
|
165
|
+
const state = useMemo(() => detectMentionPattern(inputValue), [inputValue]);
|
|
166
|
+
|
|
167
|
+
// Get file suggestions when in value mode for file/folder types
|
|
168
|
+
const shouldLoadFiles =
|
|
169
|
+
state.visible &&
|
|
170
|
+
state.mode === "value" &&
|
|
171
|
+
(state.mentionType === "file" || state.mentionType === "folder");
|
|
172
|
+
|
|
173
|
+
const fileSuggestions = useFileSuggestions(shouldLoadFiles ? state.filterText : "", {
|
|
174
|
+
cwd: options.cwd,
|
|
175
|
+
includeFiles: state.mentionType === "file",
|
|
176
|
+
includeDirectories: true, // Always include directories for navigation
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the completed input value after a selection.
|
|
181
|
+
*/
|
|
182
|
+
const getCompletedValue = useCallback(
|
|
183
|
+
(currentValue: string, selectedValue: string, mode: MentionAutocompleteMode): string => {
|
|
184
|
+
if (state.mentionStart === -1) return currentValue;
|
|
185
|
+
|
|
186
|
+
const beforeMention = currentValue.slice(0, state.mentionStart);
|
|
187
|
+
|
|
188
|
+
if (mode === "type") {
|
|
189
|
+
// Type was selected, build the mention prefix
|
|
190
|
+
const needsValue = mentionRequiresValue(selectedValue as MentionType);
|
|
191
|
+
if (needsValue) {
|
|
192
|
+
// Add colon and prepare for value input
|
|
193
|
+
return `${beforeMention}@${selectedValue}:`;
|
|
194
|
+
}
|
|
195
|
+
// Standalone mention, add space
|
|
196
|
+
return `${beforeMention}@${selectedValue} `;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Value was selected
|
|
200
|
+
if (state.mentionType) {
|
|
201
|
+
// Check if selected value is a directory
|
|
202
|
+
const isDir = fileSuggestions.suggestions.find(
|
|
203
|
+
(s) => s.path === selectedValue
|
|
204
|
+
)?.isDirectory;
|
|
205
|
+
|
|
206
|
+
if (isDir) {
|
|
207
|
+
// Directory selected, allow further navigation
|
|
208
|
+
return `${beforeMention}@${state.mentionType}:${selectedValue}/`;
|
|
209
|
+
}
|
|
210
|
+
// File selected, complete the mention
|
|
211
|
+
return `${beforeMention}@${state.mentionType}:${selectedValue} `;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return currentValue;
|
|
215
|
+
},
|
|
216
|
+
[state.mentionStart, state.mentionType, fileSuggestions.suggestions]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Handle selection and return the new input value.
|
|
221
|
+
*/
|
|
222
|
+
const handleSelect = useCallback(
|
|
223
|
+
(selectedValue: string, mode: MentionAutocompleteMode): string => {
|
|
224
|
+
return getCompletedValue(inputValue, selectedValue, mode);
|
|
225
|
+
},
|
|
226
|
+
[inputValue, getCompletedValue]
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
state,
|
|
231
|
+
fileSuggestions,
|
|
232
|
+
handleSelect,
|
|
233
|
+
getCompletedValue,
|
|
234
|
+
};
|
|
235
|
+
}
|