@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,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autocomplete Component (T011)
|
|
3
|
+
*
|
|
4
|
+
* A dropdown component for command/option suggestions with keyboard navigation.
|
|
5
|
+
* Filters options based on prefix match and displays highlighted results.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/Input/Autocomplete
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Box, Text, useInput } from "ink";
|
|
11
|
+
import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
+
import {
|
|
13
|
+
type FuzzyResult,
|
|
14
|
+
fuzzySearch,
|
|
15
|
+
getHighlightSegments,
|
|
16
|
+
type HighlightRange,
|
|
17
|
+
} from "../../services/fuzzy-search.js";
|
|
18
|
+
import { useTheme } from "../../theme/index.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Structured option for autocomplete with optional metadata.
|
|
26
|
+
*/
|
|
27
|
+
export interface AutocompleteOption {
|
|
28
|
+
/** Option name/value */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** Optional description */
|
|
31
|
+
readonly description?: string;
|
|
32
|
+
/** Optional category for grouping */
|
|
33
|
+
readonly category?: string;
|
|
34
|
+
/** Optional aliases for matching (e.g., quit -> exit) */
|
|
35
|
+
readonly aliases?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props for the Autocomplete component.
|
|
40
|
+
*/
|
|
41
|
+
export interface AutocompleteProps {
|
|
42
|
+
/** Current input value to filter options against */
|
|
43
|
+
readonly input: string;
|
|
44
|
+
/** All available options to filter from (string[] for backward compat, or structured) */
|
|
45
|
+
readonly options: readonly string[] | readonly AutocompleteOption[];
|
|
46
|
+
/** Callback when an option is selected (Tab or Enter) */
|
|
47
|
+
readonly onSelect: (value: string) => void;
|
|
48
|
+
/** Callback when autocomplete is cancelled (Escape) */
|
|
49
|
+
readonly onCancel: () => void;
|
|
50
|
+
/** Callback when selection index changes (for parent to track selection state) */
|
|
51
|
+
readonly onSelectionChange?: (index: number, hasOptions: boolean) => void;
|
|
52
|
+
/** Whether the autocomplete dropdown is visible (default: true) */
|
|
53
|
+
readonly visible?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the autocomplete should capture keyboard input (default: same as `visible`).
|
|
56
|
+
*
|
|
57
|
+
* This allows rendering suggestions while the input cursor is no longer in the
|
|
58
|
+
* command token, without hijacking Enter/history behavior.
|
|
59
|
+
*/
|
|
60
|
+
readonly active?: boolean;
|
|
61
|
+
/** Maximum number of items to show (default: 5) */
|
|
62
|
+
readonly maxVisible?: number;
|
|
63
|
+
/** Enable category grouping (default: false) */
|
|
64
|
+
readonly grouped?: boolean;
|
|
65
|
+
/** Category display order (optional, unspecified categories go last) */
|
|
66
|
+
readonly categoryOrder?: readonly string[];
|
|
67
|
+
/** Category labels for i18n (category key -> display label) */
|
|
68
|
+
readonly categoryLabels?: Record<string, string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Helper Functions
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize options to structured format.
|
|
77
|
+
* Handles both string[] and AutocompleteOption[] inputs.
|
|
78
|
+
*/
|
|
79
|
+
function normalizeOptions(
|
|
80
|
+
options: readonly string[] | readonly AutocompleteOption[]
|
|
81
|
+
): AutocompleteOption[] {
|
|
82
|
+
if (options.length === 0) return [];
|
|
83
|
+
|
|
84
|
+
// Check if first item is a string
|
|
85
|
+
if (typeof options[0] === "string") {
|
|
86
|
+
return (options as readonly string[]).map((opt) => ({ name: opt }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [...(options as readonly AutocompleteOption[])];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fuzzy filtered option with highlight information.
|
|
94
|
+
*/
|
|
95
|
+
interface FilteredOption {
|
|
96
|
+
readonly option: AutocompleteOption;
|
|
97
|
+
readonly highlights: readonly HighlightRange[];
|
|
98
|
+
readonly score: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Filter options using fuzzy matching.
|
|
103
|
+
*
|
|
104
|
+
* @param options - All available options (normalized)
|
|
105
|
+
* @param input - Current input to match against
|
|
106
|
+
* @returns Filtered array of matching options with highlights, sorted by score
|
|
107
|
+
*/
|
|
108
|
+
function filterStructuredOptions(
|
|
109
|
+
options: readonly AutocompleteOption[],
|
|
110
|
+
input: string
|
|
111
|
+
): FilteredOption[] {
|
|
112
|
+
if (!input) {
|
|
113
|
+
// No input - return all options sorted alphabetically
|
|
114
|
+
return [...options]
|
|
115
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
116
|
+
.map((opt) => ({ option: opt, highlights: [], score: 0 }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Use fuzzy search on the name field
|
|
120
|
+
const results = fuzzySearch(options, input, "name", {
|
|
121
|
+
threshold: -10000, // Allow weak matches for better UX
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Also check aliases with separate fuzzy search
|
|
125
|
+
const optionsWithAliases = options.filter((opt) => opt.aliases && opt.aliases.length > 0);
|
|
126
|
+
const aliasMatches = new Map<AutocompleteOption, FuzzyResult<AutocompleteOption>>();
|
|
127
|
+
|
|
128
|
+
for (const opt of optionsWithAliases) {
|
|
129
|
+
if (!opt.aliases) continue;
|
|
130
|
+
for (const alias of opt.aliases) {
|
|
131
|
+
const aliasResult = fuzzySearch([{ ...opt, name: alias }], input, "name");
|
|
132
|
+
if (aliasResult.length > 0 && aliasResult[0]) {
|
|
133
|
+
const existing = aliasMatches.get(opt);
|
|
134
|
+
if (!existing || aliasResult[0].score > existing.score) {
|
|
135
|
+
aliasMatches.set(opt, { ...aliasResult[0], item: opt });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Merge results: prefer name match, but include alias-only matches
|
|
142
|
+
const resultMap = new Map<AutocompleteOption, FilteredOption>();
|
|
143
|
+
|
|
144
|
+
for (const result of results) {
|
|
145
|
+
resultMap.set(result.item, {
|
|
146
|
+
option: result.item,
|
|
147
|
+
highlights: result.highlights,
|
|
148
|
+
score: result.score,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add alias matches that aren't already in results (or have better score)
|
|
153
|
+
for (const [opt, aliasResult] of aliasMatches) {
|
|
154
|
+
const existing = resultMap.get(opt);
|
|
155
|
+
if (!existing) {
|
|
156
|
+
// Not matched by name, add with empty highlights (matched via alias)
|
|
157
|
+
resultMap.set(opt, {
|
|
158
|
+
option: opt,
|
|
159
|
+
highlights: [], // Don't highlight name since alias matched
|
|
160
|
+
score: aliasResult.score,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Sort by score (higher is better)
|
|
166
|
+
return Array.from(resultMap.values()).sort((a, b) => b.score - a.score);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Group options by category.
|
|
171
|
+
*
|
|
172
|
+
* @param options - Filtered options to group (already sorted by score)
|
|
173
|
+
* @param categoryOrder - Preferred order of categories
|
|
174
|
+
* @returns Map of category -> options, ordered by categoryOrder
|
|
175
|
+
*/
|
|
176
|
+
function groupByCategory(
|
|
177
|
+
options: readonly FilteredOption[],
|
|
178
|
+
categoryOrder: readonly string[] = []
|
|
179
|
+
): Map<string, FilteredOption[]> {
|
|
180
|
+
const groups = new Map<string, FilteredOption[]>();
|
|
181
|
+
const uncategorized: FilteredOption[] = [];
|
|
182
|
+
|
|
183
|
+
// First pass: collect all options by category
|
|
184
|
+
for (const filteredOpt of options) {
|
|
185
|
+
const category = filteredOpt.option.category || "";
|
|
186
|
+
if (!category) {
|
|
187
|
+
uncategorized.push(filteredOpt);
|
|
188
|
+
} else {
|
|
189
|
+
const group = groups.get(category);
|
|
190
|
+
if (group) {
|
|
191
|
+
group.push(filteredOpt);
|
|
192
|
+
} else {
|
|
193
|
+
groups.set(category, [filteredOpt]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Build ordered result
|
|
199
|
+
const result = new Map<string, FilteredOption[]>();
|
|
200
|
+
|
|
201
|
+
// Add categories in specified order first
|
|
202
|
+
for (const cat of categoryOrder) {
|
|
203
|
+
const group = groups.get(cat);
|
|
204
|
+
if (group && group.length > 0) {
|
|
205
|
+
// Sort by score within category (already sorted, but re-sort for consistency)
|
|
206
|
+
group.sort((a, b) => b.score - a.score);
|
|
207
|
+
result.set(cat, group);
|
|
208
|
+
groups.delete(cat);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Add remaining categories alphabetically
|
|
213
|
+
const remainingCategories = Array.from(groups.keys()).sort();
|
|
214
|
+
for (const cat of remainingCategories) {
|
|
215
|
+
const group = groups.get(cat);
|
|
216
|
+
if (group && group.length > 0) {
|
|
217
|
+
group.sort((a, b) => b.score - a.score);
|
|
218
|
+
result.set(cat, group);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Add uncategorized at the end if any
|
|
223
|
+
if (uncategorized.length > 0) {
|
|
224
|
+
uncategorized.sort((a, b) => b.score - a.score);
|
|
225
|
+
result.set("", uncategorized);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Flatten grouped options into a single array with category markers.
|
|
233
|
+
* Returns items in display order with indices for keyboard navigation.
|
|
234
|
+
*/
|
|
235
|
+
interface FlattenedItem {
|
|
236
|
+
type: "category" | "option";
|
|
237
|
+
option?: AutocompleteOption;
|
|
238
|
+
category?: string;
|
|
239
|
+
selectableIndex?: number; // Only for options
|
|
240
|
+
highlights?: readonly HighlightRange[]; // For fuzzy match highlighting
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function flattenGroupedOptions(
|
|
244
|
+
grouped: Map<string, FilteredOption[]>,
|
|
245
|
+
categoryLabels: Record<string, string> = {}
|
|
246
|
+
): { items: FlattenedItem[]; selectableOptions: FilteredOption[] } {
|
|
247
|
+
const items: FlattenedItem[] = [];
|
|
248
|
+
const selectableOptions: FilteredOption[] = [];
|
|
249
|
+
let selectableIndex = 0;
|
|
250
|
+
|
|
251
|
+
for (const [category, options] of grouped) {
|
|
252
|
+
// Add category header (if category name is not empty)
|
|
253
|
+
if (category) {
|
|
254
|
+
items.push({
|
|
255
|
+
type: "category",
|
|
256
|
+
category: categoryLabels[category] || category,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Add options
|
|
261
|
+
for (const filteredOpt of options) {
|
|
262
|
+
items.push({
|
|
263
|
+
type: "option",
|
|
264
|
+
option: filteredOpt.option,
|
|
265
|
+
selectableIndex,
|
|
266
|
+
highlights: filteredOpt.highlights,
|
|
267
|
+
});
|
|
268
|
+
selectableOptions.push(filteredOpt);
|
|
269
|
+
selectableIndex++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { items, selectableOptions };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function clamp(value: number, min: number, max: number): number {
|
|
277
|
+
return Math.max(min, Math.min(max, value));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Highlight the matching portions of an option using fuzzy match ranges.
|
|
282
|
+
*
|
|
283
|
+
* @param option - The option text
|
|
284
|
+
* @param highlights - Highlight ranges from fuzzy matching
|
|
285
|
+
* @returns JSX elements with highlighted matches
|
|
286
|
+
*/
|
|
287
|
+
function HighlightedOption({
|
|
288
|
+
option,
|
|
289
|
+
highlights,
|
|
290
|
+
highlightColor,
|
|
291
|
+
normalColor,
|
|
292
|
+
}: {
|
|
293
|
+
option: string;
|
|
294
|
+
highlights: readonly HighlightRange[];
|
|
295
|
+
highlightColor: string;
|
|
296
|
+
normalColor: string;
|
|
297
|
+
}) {
|
|
298
|
+
const segments = getHighlightSegments(option, highlights);
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<Text>
|
|
302
|
+
{segments.map((segment, index) =>
|
|
303
|
+
segment.highlighted ? (
|
|
304
|
+
<Text key={`${index}-${segment.text}`} color={highlightColor} bold>
|
|
305
|
+
{segment.text}
|
|
306
|
+
</Text>
|
|
307
|
+
) : (
|
|
308
|
+
<Text key={`${index}-${segment.text}`} color={normalColor}>
|
|
309
|
+
{segment.text}
|
|
310
|
+
</Text>
|
|
311
|
+
)
|
|
312
|
+
)}
|
|
313
|
+
</Text>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// =============================================================================
|
|
318
|
+
// Component
|
|
319
|
+
// =============================================================================
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Autocomplete provides a dropdown for filtering and selecting options.
|
|
323
|
+
*
|
|
324
|
+
* Features:
|
|
325
|
+
* - Case-insensitive prefix filtering
|
|
326
|
+
* - Keyboard navigation (Up/Down arrows)
|
|
327
|
+
* - Selection via Tab or Enter
|
|
328
|
+
* - Cancellation via Escape
|
|
329
|
+
* - Highlighted matching portions
|
|
330
|
+
* - Scrollable list with "[X more]" indicator
|
|
331
|
+
* - Category grouping with headers
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```tsx
|
|
335
|
+
* // Simple string options (backward compatible)
|
|
336
|
+
* <Autocomplete
|
|
337
|
+
* input="/he"
|
|
338
|
+
* options={['/help', '/history', '/hello']}
|
|
339
|
+
* onSelect={(cmd) => setInput(cmd)}
|
|
340
|
+
* onCancel={() => setShowAutocomplete(false)}
|
|
341
|
+
* />
|
|
342
|
+
*
|
|
343
|
+
* // Grouped options with categories
|
|
344
|
+
* <Autocomplete
|
|
345
|
+
* input="/he"
|
|
346
|
+
* options={[
|
|
347
|
+
* { name: 'help', description: 'Show help', category: 'system' },
|
|
348
|
+
* { name: 'history', description: 'Show history', category: 'session' },
|
|
349
|
+
* ]}
|
|
350
|
+
* grouped={true}
|
|
351
|
+
* categoryOrder={['system', 'session']}
|
|
352
|
+
* categoryLabels={{ system: 'System', session: 'Session' }}
|
|
353
|
+
* onSelect={(cmd) => setInput(cmd)}
|
|
354
|
+
* onCancel={() => setShowAutocomplete(false)}
|
|
355
|
+
* />
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
function AutocompleteComponent({
|
|
359
|
+
input,
|
|
360
|
+
options,
|
|
361
|
+
onSelect: _onSelect, // kept for API compatibility; selection handled by parent CommandInput
|
|
362
|
+
onCancel,
|
|
363
|
+
onSelectionChange,
|
|
364
|
+
visible = true,
|
|
365
|
+
active,
|
|
366
|
+
maxVisible = 10,
|
|
367
|
+
grouped = false,
|
|
368
|
+
categoryOrder = [],
|
|
369
|
+
categoryLabels = {},
|
|
370
|
+
}: AutocompleteProps) {
|
|
371
|
+
const { theme } = useTheme();
|
|
372
|
+
|
|
373
|
+
const isActive = active ?? visible;
|
|
374
|
+
|
|
375
|
+
// Currently selected index in the selectable options
|
|
376
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
377
|
+
|
|
378
|
+
// Window start index for scrollable rendering (in terms of display items)
|
|
379
|
+
const [windowStart, setWindowStart] = useState(0);
|
|
380
|
+
|
|
381
|
+
// Normalize and filter options
|
|
382
|
+
const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
|
|
383
|
+
|
|
384
|
+
const filteredOptions = useMemo(
|
|
385
|
+
() => filterStructuredOptions(normalizedOptions, input),
|
|
386
|
+
[normalizedOptions, input]
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Group and flatten for display
|
|
390
|
+
const { displayItems, selectableOptions } = useMemo(() => {
|
|
391
|
+
if (!grouped) {
|
|
392
|
+
// Non-grouped mode: already sorted by score from filterStructuredOptions
|
|
393
|
+
return {
|
|
394
|
+
displayItems: filteredOptions.map(
|
|
395
|
+
(filtered, i): FlattenedItem => ({
|
|
396
|
+
type: "option",
|
|
397
|
+
option: filtered.option,
|
|
398
|
+
selectableIndex: i,
|
|
399
|
+
highlights: filtered.highlights,
|
|
400
|
+
})
|
|
401
|
+
),
|
|
402
|
+
selectableOptions: filteredOptions,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Grouped mode
|
|
407
|
+
const groupedMap = groupByCategory(filteredOptions, categoryOrder);
|
|
408
|
+
const { items, selectableOptions: selectable } = flattenGroupedOptions(
|
|
409
|
+
groupedMap,
|
|
410
|
+
categoryLabels
|
|
411
|
+
);
|
|
412
|
+
return { displayItems: items, selectableOptions: selectable };
|
|
413
|
+
}, [filteredOptions, grouped, categoryOrder, categoryLabels]);
|
|
414
|
+
|
|
415
|
+
// Calculate visible items (windowed) for grouped display
|
|
416
|
+
const { visibleItems, overflowCount } = useMemo(() => {
|
|
417
|
+
if (displayItems.length <= maxVisible) {
|
|
418
|
+
return { visibleItems: displayItems, overflowCount: 0 };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Find window that includes the selected item
|
|
422
|
+
// We need to map selectedIndex to display item position
|
|
423
|
+
let selectedDisplayIndex = 0;
|
|
424
|
+
for (let i = 0; i < displayItems.length; i++) {
|
|
425
|
+
const item = displayItems[i];
|
|
426
|
+
if (!item) continue;
|
|
427
|
+
if (item.type === "option" && item.selectableIndex === selectedIndex) {
|
|
428
|
+
selectedDisplayIndex = i;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Adjust window to keep selected visible
|
|
434
|
+
let start = windowStart;
|
|
435
|
+
if (selectedDisplayIndex < start) {
|
|
436
|
+
start = selectedDisplayIndex;
|
|
437
|
+
} else if (selectedDisplayIndex >= start + maxVisible) {
|
|
438
|
+
start = selectedDisplayIndex - maxVisible + 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Clamp to valid range
|
|
442
|
+
const maxStart = Math.max(0, displayItems.length - maxVisible);
|
|
443
|
+
start = Math.max(0, Math.min(start, maxStart));
|
|
444
|
+
|
|
445
|
+
const items = displayItems.slice(start, start + maxVisible);
|
|
446
|
+
const overflow = Math.max(0, displayItems.length - (start + maxVisible));
|
|
447
|
+
|
|
448
|
+
return { visibleItems: items, overflowCount: overflow };
|
|
449
|
+
}, [displayItems, maxVisible, windowStart, selectedIndex]);
|
|
450
|
+
|
|
451
|
+
// Track previous input to only reset selection when input actually changes
|
|
452
|
+
const prevInputRef = useRef(input);
|
|
453
|
+
|
|
454
|
+
// Reset selection when input value actually changes
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (prevInputRef.current !== input) {
|
|
457
|
+
startTransition(() => {
|
|
458
|
+
setSelectedIndex(0);
|
|
459
|
+
setWindowStart(0);
|
|
460
|
+
});
|
|
461
|
+
prevInputRef.current = input;
|
|
462
|
+
}
|
|
463
|
+
}, [input]);
|
|
464
|
+
|
|
465
|
+
// Notify parent of selection changes
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
onSelectionChange?.(selectedIndex, selectableOptions.length > 0);
|
|
468
|
+
}, [selectedIndex, selectableOptions.length, onSelectionChange]);
|
|
469
|
+
|
|
470
|
+
// Keep selection in bounds when filtered list changes
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
if (selectableOptions.length === 0) {
|
|
473
|
+
setSelectedIndex(0);
|
|
474
|
+
setWindowStart(0);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const clampedIndex = clamp(selectedIndex, 0, selectableOptions.length - 1);
|
|
479
|
+
if (clampedIndex !== selectedIndex) {
|
|
480
|
+
setSelectedIndex(clampedIndex);
|
|
481
|
+
}
|
|
482
|
+
}, [selectableOptions.length, selectedIndex]);
|
|
483
|
+
|
|
484
|
+
// Handle keyboard input for arrow navigation and escape only
|
|
485
|
+
// Note: Enter/Tab selection is handled by parent CommandInput to avoid race condition
|
|
486
|
+
useInput(
|
|
487
|
+
useCallback(
|
|
488
|
+
(_char, key) => {
|
|
489
|
+
if (!visible || selectableOptions.length === 0) return;
|
|
490
|
+
|
|
491
|
+
// Arrow down - move selection down
|
|
492
|
+
if (key.downArrow) {
|
|
493
|
+
setSelectedIndex((prev) => Math.min(prev + 1, selectableOptions.length - 1));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Arrow up - move selection up
|
|
498
|
+
if (key.upArrow) {
|
|
499
|
+
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Escape - cancel autocomplete
|
|
504
|
+
if (key.escape) {
|
|
505
|
+
onCancel();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
[visible, selectableOptions, onCancel]
|
|
510
|
+
),
|
|
511
|
+
{ isActive: isActive && selectableOptions.length > 0 }
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
// Don't render if not visible or no matching options
|
|
515
|
+
if (!visible || selectableOptions.length === 0) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Theme-based styling
|
|
520
|
+
const borderColor = theme.semantic.border.default;
|
|
521
|
+
const highlightColor = theme.colors.primary;
|
|
522
|
+
const normalColor = theme.semantic.text.primary;
|
|
523
|
+
const mutedColor = theme.semantic.text.muted;
|
|
524
|
+
const categoryColor = theme.colors.secondary;
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<Box
|
|
528
|
+
flexDirection="column"
|
|
529
|
+
borderStyle="single"
|
|
530
|
+
borderColor={borderColor}
|
|
531
|
+
paddingLeft={1}
|
|
532
|
+
paddingRight={1}
|
|
533
|
+
>
|
|
534
|
+
{visibleItems.map((item, displayIdx) => {
|
|
535
|
+
if (item.type === "category") {
|
|
536
|
+
// Render category header
|
|
537
|
+
return (
|
|
538
|
+
<Box key={`cat-${item.category}`} marginTop={displayIdx > 0 ? 1 : 0}>
|
|
539
|
+
<Text color={categoryColor} bold dimColor>
|
|
540
|
+
─── {item.category} ───
|
|
541
|
+
</Text>
|
|
542
|
+
</Box>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Render option
|
|
547
|
+
const opt = item.option;
|
|
548
|
+
if (!opt) return null;
|
|
549
|
+
const isSelected = item.selectableIndex === selectedIndex;
|
|
550
|
+
const highlights = item.highlights ?? [];
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
<Box key={`${opt.name}-${displayIdx}`} flexDirection="row">
|
|
554
|
+
{isSelected ? (
|
|
555
|
+
<Text inverse>
|
|
556
|
+
<Text color={highlightColor} bold>
|
|
557
|
+
{"› "}
|
|
558
|
+
</Text>
|
|
559
|
+
<HighlightedOption
|
|
560
|
+
option={opt.name}
|
|
561
|
+
highlights={highlights}
|
|
562
|
+
highlightColor={highlightColor}
|
|
563
|
+
normalColor={normalColor}
|
|
564
|
+
/>
|
|
565
|
+
{opt.description && <Text color={mutedColor}> - {opt.description}</Text>}
|
|
566
|
+
</Text>
|
|
567
|
+
) : (
|
|
568
|
+
<Text>
|
|
569
|
+
{" "}
|
|
570
|
+
<HighlightedOption
|
|
571
|
+
option={opt.name}
|
|
572
|
+
highlights={highlights}
|
|
573
|
+
highlightColor={highlightColor}
|
|
574
|
+
normalColor={normalColor}
|
|
575
|
+
/>
|
|
576
|
+
{opt.description && (
|
|
577
|
+
<Text color={mutedColor} dimColor>
|
|
578
|
+
{" "}
|
|
579
|
+
- {opt.description}
|
|
580
|
+
</Text>
|
|
581
|
+
)}
|
|
582
|
+
</Text>
|
|
583
|
+
)}
|
|
584
|
+
</Box>
|
|
585
|
+
);
|
|
586
|
+
})}
|
|
587
|
+
|
|
588
|
+
{/* Show overflow indicator if there are more items */}
|
|
589
|
+
{overflowCount > 0 && (
|
|
590
|
+
<Box paddingTop={0}>
|
|
591
|
+
<Text color={mutedColor} dimColor>
|
|
592
|
+
[{overflowCount} more]
|
|
593
|
+
</Text>
|
|
594
|
+
</Box>
|
|
595
|
+
)}
|
|
596
|
+
</Box>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Memoized Autocomplete to prevent unnecessary re-renders.
|
|
602
|
+
*/
|
|
603
|
+
export const Autocomplete = memo(AutocompleteComponent);
|