@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,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApprovalQueue Component (T030)
|
|
3
|
+
*
|
|
4
|
+
* A queue component for batch approval of multiple tool executions.
|
|
5
|
+
* Displays pending approvals with navigation and batch actions.
|
|
6
|
+
* Supports keyboard navigation and individual/batch approval.
|
|
7
|
+
*
|
|
8
|
+
* @module tui/components/Tools/ApprovalQueue
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { VellumTheme } from "@vellum/shared";
|
|
12
|
+
import { Box, Text, useInput } from "ink";
|
|
13
|
+
import type React from "react";
|
|
14
|
+
import { useCallback, useState } from "react";
|
|
15
|
+
import type { ToolExecution } from "../../context/ToolsContext.js";
|
|
16
|
+
import { useTUITranslation } from "../../i18n/index.js";
|
|
17
|
+
import { useTheme } from "../../theme/index.js";
|
|
18
|
+
import { truncateToDisplayWidth } from "../../utils/index.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props for the ApprovalQueue component.
|
|
26
|
+
*/
|
|
27
|
+
export interface ApprovalQueueProps {
|
|
28
|
+
/** List of tool executions pending approval */
|
|
29
|
+
readonly executions: readonly ToolExecution[];
|
|
30
|
+
/** Callback when a single execution is approved */
|
|
31
|
+
readonly onApprove: (id: string) => void;
|
|
32
|
+
/** Callback when a single execution is rejected */
|
|
33
|
+
readonly onReject: (id: string) => void;
|
|
34
|
+
/** Callback to approve all pending executions */
|
|
35
|
+
readonly onApproveAll: () => void;
|
|
36
|
+
/** Callback to reject all pending executions */
|
|
37
|
+
readonly onRejectAll: () => void;
|
|
38
|
+
/** Whether the queue is currently focused (default: true) */
|
|
39
|
+
readonly isFocused?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Constants
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
/** Maximum number of items visible in the queue */
|
|
47
|
+
const MAX_VISIBLE_ITEMS = 5;
|
|
48
|
+
|
|
49
|
+
/** Number keys for quick selection (1-9) */
|
|
50
|
+
const NUMBER_KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Helper Functions
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Truncate tool name to fit display.
|
|
58
|
+
* Uses string-width for accurate CJK/Emoji handling.
|
|
59
|
+
*
|
|
60
|
+
* @param name - Tool name to truncate
|
|
61
|
+
* @param maxLength - Maximum display width (default: 30)
|
|
62
|
+
* @returns Truncated name
|
|
63
|
+
*/
|
|
64
|
+
function truncateToolName(name: string, maxLength = 30): string {
|
|
65
|
+
return truncateToDisplayWidth(name, maxLength);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get a brief summary of tool parameters.
|
|
70
|
+
*
|
|
71
|
+
* @param params - Tool parameters
|
|
72
|
+
* @returns Brief parameter summary
|
|
73
|
+
*/
|
|
74
|
+
function getParamSummary(params: Record<string, unknown>): string {
|
|
75
|
+
const keys = Object.keys(params);
|
|
76
|
+
if (keys.length === 0) {
|
|
77
|
+
return "no params";
|
|
78
|
+
}
|
|
79
|
+
if (keys.length === 1) {
|
|
80
|
+
const key = keys[0];
|
|
81
|
+
if (key !== undefined) {
|
|
82
|
+
const value = params[key];
|
|
83
|
+
if (typeof value === "string" && value.length < 20) {
|
|
84
|
+
return `${key}: ${value}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return "1 param";
|
|
88
|
+
}
|
|
89
|
+
return `${keys.length} params`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// Sub-Components
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Header showing queue count and batch action hints.
|
|
98
|
+
*/
|
|
99
|
+
function QueueHeader({
|
|
100
|
+
count,
|
|
101
|
+
theme,
|
|
102
|
+
t,
|
|
103
|
+
}: {
|
|
104
|
+
readonly count: number;
|
|
105
|
+
readonly theme: VellumTheme;
|
|
106
|
+
readonly t: (key: string) => string;
|
|
107
|
+
}): React.JSX.Element {
|
|
108
|
+
return (
|
|
109
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
110
|
+
<Box>
|
|
111
|
+
<Text color={theme.colors.warning} bold>
|
|
112
|
+
{t("approval.pending")} {count}
|
|
113
|
+
</Text>
|
|
114
|
+
</Box>
|
|
115
|
+
<Box gap={2}>
|
|
116
|
+
<Text dimColor>
|
|
117
|
+
<Text color={theme.colors.success}>[a]</Text> {t("approval.approveAll")}
|
|
118
|
+
</Text>
|
|
119
|
+
<Text dimColor>
|
|
120
|
+
<Text color={theme.colors.error}>[r]</Text> {t("approval.rejectAll")}
|
|
121
|
+
</Text>
|
|
122
|
+
<Text dimColor>
|
|
123
|
+
<Text color={theme.colors.info}>[↑↓]</Text> {t("approval.navigate")}
|
|
124
|
+
</Text>
|
|
125
|
+
<Text dimColor>
|
|
126
|
+
<Text color={theme.colors.success}>[y]</Text> {t("approval.approve")}
|
|
127
|
+
</Text>
|
|
128
|
+
<Text dimColor>
|
|
129
|
+
<Text color={theme.colors.error}>[n]</Text> {t("approval.reject")}
|
|
130
|
+
</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Single queue item showing tool info.
|
|
138
|
+
*/
|
|
139
|
+
function QueueItem({
|
|
140
|
+
execution,
|
|
141
|
+
index,
|
|
142
|
+
isSelected,
|
|
143
|
+
theme,
|
|
144
|
+
}: {
|
|
145
|
+
readonly execution: ToolExecution;
|
|
146
|
+
readonly index: number;
|
|
147
|
+
readonly isSelected: boolean;
|
|
148
|
+
readonly theme: VellumTheme;
|
|
149
|
+
}): React.JSX.Element {
|
|
150
|
+
const displayIndex = index + 1;
|
|
151
|
+
const bgColor = isSelected ? theme.semantic.background.elevated : undefined;
|
|
152
|
+
const paramSummary = getParamSummary(execution.params);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Box>
|
|
156
|
+
<Text backgroundColor={bgColor}>
|
|
157
|
+
{isSelected ? "▸ " : " "}
|
|
158
|
+
<Text dimColor>{displayIndex}.</Text>{" "}
|
|
159
|
+
<Text color={theme.colors.primary} bold={isSelected}>
|
|
160
|
+
{truncateToolName(execution.toolName)}
|
|
161
|
+
</Text>
|
|
162
|
+
<Text dimColor> ({paramSummary})</Text>
|
|
163
|
+
</Text>
|
|
164
|
+
</Box>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Empty state when no approvals pending.
|
|
170
|
+
*/
|
|
171
|
+
function EmptyState({
|
|
172
|
+
theme,
|
|
173
|
+
t,
|
|
174
|
+
}: {
|
|
175
|
+
readonly theme: VellumTheme;
|
|
176
|
+
readonly t: (key: string) => string;
|
|
177
|
+
}): React.JSX.Element {
|
|
178
|
+
return (
|
|
179
|
+
<Box paddingY={1}>
|
|
180
|
+
<Text color={theme.colors.muted}>{t("approval.empty")}</Text>
|
|
181
|
+
</Box>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Scroll indicator for long lists.
|
|
187
|
+
*/
|
|
188
|
+
function ScrollIndicator({
|
|
189
|
+
currentIndex,
|
|
190
|
+
totalCount,
|
|
191
|
+
visibleCount,
|
|
192
|
+
theme,
|
|
193
|
+
}: {
|
|
194
|
+
readonly currentIndex: number;
|
|
195
|
+
readonly totalCount: number;
|
|
196
|
+
readonly visibleCount: number;
|
|
197
|
+
readonly theme: VellumTheme;
|
|
198
|
+
}): React.JSX.Element | null {
|
|
199
|
+
if (totalCount <= visibleCount) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const hasMore = currentIndex + visibleCount < totalCount;
|
|
204
|
+
const hasPrevious = currentIndex > 0;
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<Box marginTop={1}>
|
|
208
|
+
<Text color={theme.colors.muted} dimColor>
|
|
209
|
+
{hasPrevious && "↑ more above "}
|
|
210
|
+
{hasPrevious && hasMore && "| "}
|
|
211
|
+
{hasMore && "↓ more below"}
|
|
212
|
+
{!hasPrevious && !hasMore && ""}
|
|
213
|
+
</Text>
|
|
214
|
+
</Box>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Main Component
|
|
220
|
+
// =============================================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* ApprovalQueue displays a list of pending tool executions for batch approval.
|
|
224
|
+
*
|
|
225
|
+
* Features:
|
|
226
|
+
* - Shows count of pending approvals
|
|
227
|
+
* - Lists queued executions with tool name and parameter summary
|
|
228
|
+
* - Keyboard navigation with arrow keys
|
|
229
|
+
* - Number keys (1-9) for quick selection
|
|
230
|
+
* - 'a' to approve all, 'r' to reject all
|
|
231
|
+
* - 'y' to approve selected, 'n' to reject selected
|
|
232
|
+
*
|
|
233
|
+
* @param props - Component props
|
|
234
|
+
* @returns The rendered queue component
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```tsx
|
|
238
|
+
* <ApprovalQueue
|
|
239
|
+
* executions={pendingExecutions}
|
|
240
|
+
* onApprove={(id) => handleApprove(id)}
|
|
241
|
+
* onReject={(id) => handleReject(id)}
|
|
242
|
+
* onApproveAll={() => handleApproveAll()}
|
|
243
|
+
* onRejectAll={() => handleRejectAll()}
|
|
244
|
+
* />
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export function ApprovalQueue({
|
|
248
|
+
executions,
|
|
249
|
+
onApprove,
|
|
250
|
+
onReject,
|
|
251
|
+
onApproveAll,
|
|
252
|
+
onRejectAll,
|
|
253
|
+
isFocused = true,
|
|
254
|
+
}: ApprovalQueueProps): React.JSX.Element {
|
|
255
|
+
const { theme } = useTheme();
|
|
256
|
+
const { t } = useTUITranslation();
|
|
257
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
258
|
+
|
|
259
|
+
// Calculate visible window for scrolling
|
|
260
|
+
const startIndex = Math.max(
|
|
261
|
+
0,
|
|
262
|
+
Math.min(
|
|
263
|
+
selectedIndex - Math.floor(MAX_VISIBLE_ITEMS / 2),
|
|
264
|
+
executions.length - MAX_VISIBLE_ITEMS
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
const visibleExecutions = executions.slice(startIndex, startIndex + MAX_VISIBLE_ITEMS);
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Handle navigation to previous item.
|
|
271
|
+
*/
|
|
272
|
+
const navigateUp = useCallback(() => {
|
|
273
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
274
|
+
}, []);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Handle navigation to next item.
|
|
278
|
+
*/
|
|
279
|
+
const navigateDown = useCallback(() => {
|
|
280
|
+
setSelectedIndex((prev) => Math.min(executions.length - 1, prev + 1));
|
|
281
|
+
}, [executions.length]);
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Handle number key selection.
|
|
285
|
+
*/
|
|
286
|
+
const selectByNumber = useCallback(
|
|
287
|
+
(num: number) => {
|
|
288
|
+
const targetIndex = startIndex + num - 1;
|
|
289
|
+
if (targetIndex >= 0 && targetIndex < executions.length) {
|
|
290
|
+
setSelectedIndex(targetIndex);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
[startIndex, executions.length]
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Approve the currently selected execution.
|
|
298
|
+
*/
|
|
299
|
+
const approveSelected = useCallback(() => {
|
|
300
|
+
if (executions.length > 0 && selectedIndex < executions.length) {
|
|
301
|
+
const execution = executions[selectedIndex];
|
|
302
|
+
if (execution) {
|
|
303
|
+
onApprove(execution.id);
|
|
304
|
+
// Adjust selection if needed
|
|
305
|
+
if (selectedIndex >= executions.length - 1 && selectedIndex > 0) {
|
|
306
|
+
setSelectedIndex(selectedIndex - 1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}, [executions, selectedIndex, onApprove]);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Reject the currently selected execution.
|
|
314
|
+
*/
|
|
315
|
+
const rejectSelected = useCallback(() => {
|
|
316
|
+
if (executions.length > 0 && selectedIndex < executions.length) {
|
|
317
|
+
const execution = executions[selectedIndex];
|
|
318
|
+
if (execution) {
|
|
319
|
+
onReject(execution.id);
|
|
320
|
+
// Adjust selection if needed
|
|
321
|
+
if (selectedIndex >= executions.length - 1 && selectedIndex > 0) {
|
|
322
|
+
setSelectedIndex(selectedIndex - 1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}, [executions, selectedIndex, onReject]);
|
|
327
|
+
|
|
328
|
+
// Handle keyboard input
|
|
329
|
+
useInput(
|
|
330
|
+
(input, key) => {
|
|
331
|
+
// Navigation
|
|
332
|
+
if (key.upArrow) {
|
|
333
|
+
navigateUp();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (key.downArrow) {
|
|
337
|
+
navigateDown();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Batch actions
|
|
342
|
+
if (input.toLowerCase() === "a") {
|
|
343
|
+
onApproveAll();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (input.toLowerCase() === "r") {
|
|
347
|
+
onRejectAll();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Individual actions
|
|
352
|
+
if (input.toLowerCase() === "y") {
|
|
353
|
+
approveSelected();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (input.toLowerCase() === "n") {
|
|
357
|
+
rejectSelected();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Number key selection
|
|
362
|
+
const numberIndex = NUMBER_KEYS.indexOf(input);
|
|
363
|
+
if (numberIndex !== -1) {
|
|
364
|
+
selectByNumber(numberIndex + 1);
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{ isActive: isFocused && executions.length > 0 }
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Empty state
|
|
371
|
+
if (executions.length === 0) {
|
|
372
|
+
return <EmptyState theme={theme} t={t} />;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<Box
|
|
377
|
+
flexDirection="column"
|
|
378
|
+
borderStyle="round"
|
|
379
|
+
borderColor={theme.semantic.border.default}
|
|
380
|
+
padding={1}
|
|
381
|
+
>
|
|
382
|
+
<QueueHeader count={executions.length} theme={theme} t={t} />
|
|
383
|
+
|
|
384
|
+
<Box flexDirection="column">
|
|
385
|
+
{visibleExecutions.map((execution, visibleIdx) => {
|
|
386
|
+
const actualIndex = startIndex + visibleIdx;
|
|
387
|
+
return (
|
|
388
|
+
<QueueItem
|
|
389
|
+
key={execution.id}
|
|
390
|
+
execution={execution}
|
|
391
|
+
index={actualIndex}
|
|
392
|
+
isSelected={actualIndex === selectedIndex}
|
|
393
|
+
theme={theme}
|
|
394
|
+
/>
|
|
395
|
+
);
|
|
396
|
+
})}
|
|
397
|
+
</Box>
|
|
398
|
+
|
|
399
|
+
<ScrollIndicator
|
|
400
|
+
currentIndex={startIndex}
|
|
401
|
+
totalCount={executions.length}
|
|
402
|
+
visibleCount={MAX_VISIBLE_ITEMS}
|
|
403
|
+
theme={theme}
|
|
404
|
+
/>
|
|
405
|
+
</Box>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OptionSelector Component
|
|
3
|
+
*
|
|
4
|
+
* TUI component for selecting options from a list with keyboard navigation.
|
|
5
|
+
* Used by ask_followup_question tool when suggestions are provided.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/Tools/OptionSelector
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Box, Text, useInput } from "ink";
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import { useCallback, useState } from "react";
|
|
13
|
+
import { useTheme } from "../../theme/index.js";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for the OptionSelector component.
|
|
21
|
+
*/
|
|
22
|
+
export interface OptionSelectorProps {
|
|
23
|
+
/** Question to display above options */
|
|
24
|
+
readonly question: string;
|
|
25
|
+
/** Options to select from */
|
|
26
|
+
readonly options: readonly string[];
|
|
27
|
+
/** Callback when option is selected */
|
|
28
|
+
readonly onSelect: (option: string, index: number) => void;
|
|
29
|
+
/** Callback when cancelled (Esc pressed) */
|
|
30
|
+
readonly onCancel?: () => void;
|
|
31
|
+
/** Whether the selector is focused/active */
|
|
32
|
+
readonly isFocused?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// OptionSelector Component
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OptionSelector - Interactive component for selecting options from a list.
|
|
41
|
+
*
|
|
42
|
+
* Features:
|
|
43
|
+
* - Arrow key navigation (up/down)
|
|
44
|
+
* - Number shortcuts (1-9 for quick selection)
|
|
45
|
+
* - Enter to confirm selection
|
|
46
|
+
* - Esc to cancel/skip
|
|
47
|
+
* - Visual indication of focused option
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* function MyComponent() {
|
|
52
|
+
* return (
|
|
53
|
+
* <OptionSelector
|
|
54
|
+
* question="Which approach do you prefer?"
|
|
55
|
+
* options={["Option A", "Option B", "Option C"]}
|
|
56
|
+
* onSelect={(option, index) => console.log(`Selected: ${option}`)}
|
|
57
|
+
* onCancel={() => console.log("Cancelled")}
|
|
58
|
+
* isFocused
|
|
59
|
+
* />
|
|
60
|
+
* );
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function OptionSelector({
|
|
65
|
+
question,
|
|
66
|
+
options,
|
|
67
|
+
onSelect,
|
|
68
|
+
onCancel,
|
|
69
|
+
isFocused = true,
|
|
70
|
+
}: OptionSelectorProps): React.ReactElement {
|
|
71
|
+
const { theme } = useTheme();
|
|
72
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
73
|
+
|
|
74
|
+
// Handle keyboard input
|
|
75
|
+
useInput(
|
|
76
|
+
useCallback(
|
|
77
|
+
(input: string, key) => {
|
|
78
|
+
if (!isFocused) return;
|
|
79
|
+
|
|
80
|
+
// Number key shortcuts (1-9)
|
|
81
|
+
const num = Number.parseInt(input, 10);
|
|
82
|
+
if (!Number.isNaN(num) && num >= 1 && num <= Math.min(options.length, 9)) {
|
|
83
|
+
const selectedOption = options[num - 1];
|
|
84
|
+
if (selectedOption !== undefined) {
|
|
85
|
+
onSelect(selectedOption, num - 1);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Arrow key navigation
|
|
91
|
+
if (key.upArrow) {
|
|
92
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (key.downArrow) {
|
|
97
|
+
setSelectedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Enter to confirm
|
|
102
|
+
if (key.return) {
|
|
103
|
+
const selected = options[selectedIndex];
|
|
104
|
+
if (selected !== undefined) {
|
|
105
|
+
onSelect(selected, selectedIndex);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Esc to cancel
|
|
111
|
+
if (key.escape && onCancel) {
|
|
112
|
+
onCancel();
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
[isFocused, options, selectedIndex, onSelect, onCancel]
|
|
116
|
+
),
|
|
117
|
+
{ isActive: isFocused }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const maxShortcut = Math.min(options.length, 9);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Box flexDirection="column" marginTop={1}>
|
|
124
|
+
{/* Question */}
|
|
125
|
+
<Text color={theme.semantic.text.secondary}>↳ {question}</Text>
|
|
126
|
+
|
|
127
|
+
{/* Options list */}
|
|
128
|
+
<Box flexDirection="column" marginTop={1}>
|
|
129
|
+
{options.map((option, index) => {
|
|
130
|
+
const isSelected = index === selectedIndex;
|
|
131
|
+
const prefix = isSelected ? "▸" : " ";
|
|
132
|
+
const shortcut = index < 9 ? `${index + 1}` : " ";
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Text
|
|
136
|
+
key={`opt-${option}`}
|
|
137
|
+
color={isSelected ? theme.colors.primary : undefined}
|
|
138
|
+
bold={isSelected}
|
|
139
|
+
>
|
|
140
|
+
{prefix} [{shortcut}] {option}
|
|
141
|
+
</Text>
|
|
142
|
+
);
|
|
143
|
+
})}
|
|
144
|
+
</Box>
|
|
145
|
+
|
|
146
|
+
{/* Help text */}
|
|
147
|
+
<Box marginTop={1}>
|
|
148
|
+
<Text dimColor>
|
|
149
|
+
↑↓: Navigate | 1-{maxShortcut}: Quick select | Enter: Confirm | Esc: Skip
|
|
150
|
+
</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
</Box>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// =============================================================================
|
|
157
|
+
// Exports
|
|
158
|
+
// =============================================================================
|
|
159
|
+
|
|
160
|
+
export type { OptionSelectorProps as OptionSelectorPropsType };
|