@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,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command System E2E Tests
|
|
3
|
+
*
|
|
4
|
+
* End-to-end tests covering the full command lifecycle:
|
|
5
|
+
* - User input → Parser → Executor → Result
|
|
6
|
+
* - Autocomplete flow
|
|
7
|
+
* - Error handling with suggestions
|
|
8
|
+
*
|
|
9
|
+
* @module cli/__tests__/commands.e2e
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type AutocompleteState,
|
|
16
|
+
autocompleteReducer,
|
|
17
|
+
type CommandContext,
|
|
18
|
+
type CommandContextProvider,
|
|
19
|
+
CommandExecutor,
|
|
20
|
+
CommandParser,
|
|
21
|
+
CommandRegistry,
|
|
22
|
+
type CommandResult,
|
|
23
|
+
clearCommand,
|
|
24
|
+
createTestContextProvider,
|
|
25
|
+
exitCommand,
|
|
26
|
+
fuzzyScore,
|
|
27
|
+
getSelectedCandidate,
|
|
28
|
+
helpCommand,
|
|
29
|
+
initialAutocompleteState,
|
|
30
|
+
type SlashCommandDef,
|
|
31
|
+
setHelpRegistry,
|
|
32
|
+
shouldShowAutocomplete,
|
|
33
|
+
} from "../commands/index.js";
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Test Fixtures
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a mock SlashCommand for testing
|
|
41
|
+
*/
|
|
42
|
+
function createMockCommand(
|
|
43
|
+
overrides: Partial<SlashCommandDef> & { name: string }
|
|
44
|
+
): SlashCommandDef {
|
|
45
|
+
return {
|
|
46
|
+
description: `Mock command: ${overrides.name}`,
|
|
47
|
+
kind: "builtin",
|
|
48
|
+
category: "system",
|
|
49
|
+
execute: async () => ({ kind: "success" as const }),
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* E2E test harness for command system
|
|
56
|
+
*/
|
|
57
|
+
class CommandSystemHarness {
|
|
58
|
+
readonly registry: CommandRegistry;
|
|
59
|
+
readonly parser: CommandParser;
|
|
60
|
+
readonly executor: CommandExecutor;
|
|
61
|
+
readonly contextProvider: CommandContextProvider;
|
|
62
|
+
private emittedEvents: Array<{ event: string; data?: unknown }> = [];
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
this.registry = new CommandRegistry();
|
|
66
|
+
this.parser = new CommandParser();
|
|
67
|
+
this.contextProvider = createTestContextProvider({
|
|
68
|
+
emit: (event, data) => {
|
|
69
|
+
this.emittedEvents.push({ event, data });
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
this.executor = new CommandExecutor(this.registry, this.contextProvider);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register core commands for testing
|
|
77
|
+
*/
|
|
78
|
+
registerCoreCommands(): void {
|
|
79
|
+
this.registry.register(helpCommand);
|
|
80
|
+
this.registry.register(clearCommand);
|
|
81
|
+
this.registry.register(exitCommand);
|
|
82
|
+
setHelpRegistry(this.registry);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute a command string and return the result
|
|
87
|
+
*/
|
|
88
|
+
async execute(input: string): Promise<CommandResult> {
|
|
89
|
+
return this.executor.execute(input);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get autocomplete candidates for input
|
|
94
|
+
*/
|
|
95
|
+
getAutocompleteCandidates(input: string): AutocompleteState {
|
|
96
|
+
const query = input.startsWith("/") ? input.slice(1) : input;
|
|
97
|
+
return autocompleteReducer(initialAutocompleteState, {
|
|
98
|
+
type: "INPUT_CHANGE",
|
|
99
|
+
query,
|
|
100
|
+
registry: this.registry,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Simulate tab completion
|
|
106
|
+
*/
|
|
107
|
+
tabComplete(state: AutocompleteState): AutocompleteState {
|
|
108
|
+
return autocompleteReducer(state, { type: "TAB_COMPLETE" });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get emitted events
|
|
113
|
+
*/
|
|
114
|
+
getEmittedEvents(): Array<{ event: string; data?: unknown }> {
|
|
115
|
+
return [...this.emittedEvents];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear emitted events
|
|
120
|
+
*/
|
|
121
|
+
clearEvents(): void {
|
|
122
|
+
this.emittedEvents = [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// E2E Test: /help Command
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
describe("E2E: /help command", () => {
|
|
131
|
+
let harness: CommandSystemHarness;
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
harness = new CommandSystemHarness();
|
|
135
|
+
harness.registerCoreCommands();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should receive formatted help output for /help", async () => {
|
|
139
|
+
const result = await harness.execute("/help");
|
|
140
|
+
|
|
141
|
+
expect(result.kind).toBe("success");
|
|
142
|
+
if (result.kind === "success") {
|
|
143
|
+
expect(result.message).toBeDefined();
|
|
144
|
+
expect(result.message).toContain("Available Commands");
|
|
145
|
+
expect(result.message).toContain("/help");
|
|
146
|
+
expect(result.message).toContain("/clear");
|
|
147
|
+
expect(result.message).toContain("/exit(quit)");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should show command-specific help for /help <command>", async () => {
|
|
152
|
+
const result = await harness.execute("/help exit");
|
|
153
|
+
|
|
154
|
+
expect(result.kind).toBe("success");
|
|
155
|
+
if (result.kind === "success") {
|
|
156
|
+
expect(result.message).toBeDefined();
|
|
157
|
+
expect(result.message).toContain("/exit(quit)");
|
|
158
|
+
expect(result.message).toContain("Exit the application");
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should show category help for /help system", async () => {
|
|
163
|
+
const result = await harness.execute("/help system");
|
|
164
|
+
|
|
165
|
+
expect(result.kind).toBe("success");
|
|
166
|
+
if (result.kind === "success") {
|
|
167
|
+
expect(result.message).toBeDefined();
|
|
168
|
+
expect(result.message).toContain("System");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should handle alias /h", async () => {
|
|
173
|
+
const result = await harness.execute("/h");
|
|
174
|
+
|
|
175
|
+
expect(result.kind).toBe("success");
|
|
176
|
+
if (result.kind === "success") {
|
|
177
|
+
expect(result.message).toContain("Available Commands");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// E2E Test: /auth set Command (Interactive)
|
|
184
|
+
// =============================================================================
|
|
185
|
+
|
|
186
|
+
describe("E2E: /auth set command (interactive)", () => {
|
|
187
|
+
let harness: CommandSystemHarness;
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
harness = new CommandSystemHarness();
|
|
191
|
+
harness.registerCoreCommands();
|
|
192
|
+
|
|
193
|
+
// Add auth command for testing (with set subcommand)
|
|
194
|
+
const authCommand = createMockCommand({
|
|
195
|
+
name: "auth",
|
|
196
|
+
category: "auth",
|
|
197
|
+
positionalArgs: [
|
|
198
|
+
{
|
|
199
|
+
name: "subcommand",
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Subcommand: status, set, clear",
|
|
202
|
+
required: false,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "provider",
|
|
206
|
+
type: "string",
|
|
207
|
+
description: "Provider name",
|
|
208
|
+
required: false,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
execute: async (ctx: CommandContext) => {
|
|
212
|
+
const subcommand = (ctx.parsedArgs.positional[0] as string) ?? "status";
|
|
213
|
+
const provider =
|
|
214
|
+
(ctx.parsedArgs.positional[1] as string) ?? ctx.session.provider ?? "anthropic";
|
|
215
|
+
|
|
216
|
+
if (subcommand === "set") {
|
|
217
|
+
return {
|
|
218
|
+
kind: "interactive",
|
|
219
|
+
prompt: {
|
|
220
|
+
inputType: "password",
|
|
221
|
+
message: `Enter API key for ${provider}:`,
|
|
222
|
+
placeholder: "sk-...",
|
|
223
|
+
provider,
|
|
224
|
+
handler: async (value: string) => {
|
|
225
|
+
if (!value.trim()) {
|
|
226
|
+
return {
|
|
227
|
+
kind: "error",
|
|
228
|
+
code: "INVALID_ARGUMENT",
|
|
229
|
+
message: "API key cannot be empty",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
kind: "success",
|
|
234
|
+
message: `✅ Credential saved for ${provider}`,
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
onCancel: () => ({ kind: "success", message: "Auth cancelled" }),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
kind: "success",
|
|
244
|
+
message: "Authentication status",
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
harness.registry.register(authCommand);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should return interactive prompt for /auth set", async () => {
|
|
252
|
+
const result = await harness.execute("/auth set");
|
|
253
|
+
|
|
254
|
+
expect(result.kind).toBe("interactive");
|
|
255
|
+
if (result.kind === "interactive") {
|
|
256
|
+
expect(result.prompt.inputType).toBe("password");
|
|
257
|
+
expect(result.prompt.message).toContain("Enter API key");
|
|
258
|
+
expect(result.prompt.placeholder).toBe("sk-...");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should accept provider argument: /auth set anthropic", async () => {
|
|
263
|
+
const result = await harness.execute("/auth set anthropic");
|
|
264
|
+
|
|
265
|
+
expect(result.kind).toBe("interactive");
|
|
266
|
+
if (result.kind === "interactive") {
|
|
267
|
+
expect(result.prompt.message).toContain("anthropic");
|
|
268
|
+
expect(result.prompt.provider).toBe("anthropic");
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should handle input submission via handler", async () => {
|
|
273
|
+
const result = await harness.execute("/auth set openai");
|
|
274
|
+
|
|
275
|
+
expect(result.kind).toBe("interactive");
|
|
276
|
+
if (result.kind === "interactive") {
|
|
277
|
+
const submitResult = await result.prompt.handler("sk-test-key-12345");
|
|
278
|
+
expect(submitResult.kind).toBe("success");
|
|
279
|
+
if (submitResult.kind === "success") {
|
|
280
|
+
expect(submitResult.message).toContain("Credential saved");
|
|
281
|
+
expect(submitResult.message).toContain("openai");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should validate empty input", async () => {
|
|
287
|
+
const result = await harness.execute("/auth set");
|
|
288
|
+
|
|
289
|
+
expect(result.kind).toBe("interactive");
|
|
290
|
+
if (result.kind === "interactive") {
|
|
291
|
+
const submitResult = await result.prompt.handler("");
|
|
292
|
+
expect(submitResult.kind).toBe("error");
|
|
293
|
+
if (submitResult.kind === "error") {
|
|
294
|
+
expect(submitResult.code).toBe("INVALID_ARGUMENT");
|
|
295
|
+
expect(submitResult.message).toContain("cannot be empty");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should handle cancellation", async () => {
|
|
301
|
+
const result = await harness.execute("/auth set");
|
|
302
|
+
|
|
303
|
+
expect(result.kind).toBe("interactive");
|
|
304
|
+
if (result.kind === "interactive") {
|
|
305
|
+
const cancelResult = result.prompt.onCancel?.();
|
|
306
|
+
expect(cancelResult?.kind).toBe("success");
|
|
307
|
+
if (cancelResult?.kind === "success") {
|
|
308
|
+
expect(cancelResult.message).toContain("cancelled");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// =============================================================================
|
|
315
|
+
// E2E Test: /exit Command
|
|
316
|
+
// =============================================================================
|
|
317
|
+
|
|
318
|
+
describe("E2E: /exit command", () => {
|
|
319
|
+
let harness: CommandSystemHarness;
|
|
320
|
+
|
|
321
|
+
beforeEach(() => {
|
|
322
|
+
harness = new CommandSystemHarness();
|
|
323
|
+
harness.registerCoreCommands();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("should exit immediately with /exit", async () => {
|
|
327
|
+
harness.clearEvents();
|
|
328
|
+
const result = await harness.execute("/exit");
|
|
329
|
+
|
|
330
|
+
expect(result.kind).toBe("success");
|
|
331
|
+
if (result.kind === "success") {
|
|
332
|
+
expect(result.data).toEqual({ exit: true });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const events = harness.getEmittedEvents();
|
|
336
|
+
expect(events).toContainEqual({
|
|
337
|
+
event: "app:exit",
|
|
338
|
+
data: { reason: "user-command" },
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should support quit alias", async () => {
|
|
343
|
+
harness.clearEvents();
|
|
344
|
+
const result = await harness.execute("/quit");
|
|
345
|
+
|
|
346
|
+
expect(result.kind).toBe("success");
|
|
347
|
+
if (result.kind === "success") {
|
|
348
|
+
expect(result.data).toEqual({ exit: true });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const events = harness.getEmittedEvents();
|
|
352
|
+
expect(events).toContainEqual({
|
|
353
|
+
event: "app:exit",
|
|
354
|
+
data: { reason: "user-command" },
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should support q alias", async () => {
|
|
359
|
+
harness.clearEvents();
|
|
360
|
+
const result = await harness.execute("/q");
|
|
361
|
+
|
|
362
|
+
expect(result.kind).toBe("success");
|
|
363
|
+
if (result.kind === "success") {
|
|
364
|
+
expect(result.data).toEqual({ exit: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const events = harness.getEmittedEvents();
|
|
368
|
+
expect(events).toContainEqual({
|
|
369
|
+
event: "app:exit",
|
|
370
|
+
data: { reason: "user-command" },
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// =============================================================================
|
|
376
|
+
// E2E Test: Unknown Command
|
|
377
|
+
// =============================================================================
|
|
378
|
+
|
|
379
|
+
describe("E2E: unknown command handling", () => {
|
|
380
|
+
let harness: CommandSystemHarness;
|
|
381
|
+
|
|
382
|
+
beforeEach(() => {
|
|
383
|
+
harness = new CommandSystemHarness();
|
|
384
|
+
harness.registerCoreCommands();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("should receive error with suggestions for /xyz", async () => {
|
|
388
|
+
const result = await harness.execute("/xyz");
|
|
389
|
+
|
|
390
|
+
expect(result.kind).toBe("error");
|
|
391
|
+
if (result.kind === "error") {
|
|
392
|
+
expect(result.code).toBe("COMMAND_NOT_FOUND");
|
|
393
|
+
expect(result.message).toContain("xyz");
|
|
394
|
+
// Message format: "Unknown command: /xyz"
|
|
395
|
+
expect(result.message).toContain("Unknown command");
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("should suggest similar commands for typos", async () => {
|
|
400
|
+
const result = await harness.execute("/hlep"); // typo for help
|
|
401
|
+
|
|
402
|
+
expect(result.kind).toBe("error");
|
|
403
|
+
if (result.kind === "error") {
|
|
404
|
+
expect(result.code).toBe("COMMAND_NOT_FOUND");
|
|
405
|
+
expect(result.suggestions).toBeDefined();
|
|
406
|
+
expect(result.suggestions).toContain("/help");
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("should suggest /exit(quit) for /exti typo", async () => {
|
|
411
|
+
const result = await harness.execute("/exti");
|
|
412
|
+
|
|
413
|
+
expect(result.kind).toBe("error");
|
|
414
|
+
if (result.kind === "error") {
|
|
415
|
+
expect(result.suggestions).toBeDefined();
|
|
416
|
+
expect(result.suggestions).toContain("/exit(quit)");
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should suggest /clear for /cls (known alias)", async () => {
|
|
421
|
+
// Note: cls is an alias, so it should resolve correctly
|
|
422
|
+
const result = await harness.execute("/cls");
|
|
423
|
+
expect(result.kind).toBe("success"); // alias should work
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("should handle completely unrelated command", async () => {
|
|
427
|
+
const result = await harness.execute("/abracadabra");
|
|
428
|
+
|
|
429
|
+
expect(result.kind).toBe("error");
|
|
430
|
+
if (result.kind === "error") {
|
|
431
|
+
expect(result.code).toBe("COMMAND_NOT_FOUND");
|
|
432
|
+
// May or may not have suggestions depending on distance
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// =============================================================================
|
|
438
|
+
// E2E Test: Autocomplete Flow
|
|
439
|
+
// =============================================================================
|
|
440
|
+
|
|
441
|
+
describe("E2E: autocomplete flow", () => {
|
|
442
|
+
let harness: CommandSystemHarness;
|
|
443
|
+
|
|
444
|
+
beforeEach(() => {
|
|
445
|
+
harness = new CommandSystemHarness();
|
|
446
|
+
harness.registerCoreCommands();
|
|
447
|
+
|
|
448
|
+
// Add more commands for better autocomplete testing
|
|
449
|
+
harness.registry.register(createMockCommand({ name: "history", category: "session" }));
|
|
450
|
+
harness.registry.register(createMockCommand({ name: "hello", category: "debug" }));
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("should show candidates when typing /hel", () => {
|
|
454
|
+
const state = harness.getAutocompleteCandidates("/hel");
|
|
455
|
+
|
|
456
|
+
expect(state.active).toBe(true);
|
|
457
|
+
expect(state.candidates.length).toBeGreaterThan(0);
|
|
458
|
+
|
|
459
|
+
const names = state.candidates.map((c) => c.command.name);
|
|
460
|
+
expect(names).toContain("help");
|
|
461
|
+
expect(names).toContain("hello");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("should rank exact prefix match higher", () => {
|
|
465
|
+
const state = harness.getAutocompleteCandidates("/help");
|
|
466
|
+
|
|
467
|
+
expect(state.candidates.length).toBeGreaterThan(0);
|
|
468
|
+
const firstCandidate = state.candidates[0];
|
|
469
|
+
expect(firstCandidate).toBeDefined();
|
|
470
|
+
expect(firstCandidate?.command.name).toBe("help");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("should Tab complete to selected candidate", () => {
|
|
474
|
+
let state = harness.getAutocompleteCandidates("/hel");
|
|
475
|
+
expect(state.active).toBe(true);
|
|
476
|
+
|
|
477
|
+
// Get selected candidate before tab
|
|
478
|
+
const selectedBefore = getSelectedCandidate(state);
|
|
479
|
+
expect(selectedBefore).toBeDefined();
|
|
480
|
+
|
|
481
|
+
// Tab complete - returns state unchanged for caller to read selected candidate
|
|
482
|
+
state = harness.tabComplete(state);
|
|
483
|
+
|
|
484
|
+
// State remains active - caller uses selected candidate then dispatches CANCEL
|
|
485
|
+
expect(state.active).toBe(true);
|
|
486
|
+
expect(getSelectedCandidate(state)).toBe(selectedBefore);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("should navigate candidates with SELECT_NEXT/SELECT_PREV", () => {
|
|
490
|
+
let state = harness.getAutocompleteCandidates("/h");
|
|
491
|
+
const initialIndex = state.selectedIndex;
|
|
492
|
+
|
|
493
|
+
state = autocompleteReducer(state, { type: "SELECT_NEXT" });
|
|
494
|
+
expect(state.selectedIndex).toBe((initialIndex + 1) % state.candidates.length);
|
|
495
|
+
|
|
496
|
+
state = autocompleteReducer(state, { type: "SELECT_PREV" });
|
|
497
|
+
expect(state.selectedIndex).toBe(initialIndex);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("should cancel autocomplete", () => {
|
|
501
|
+
let state = harness.getAutocompleteCandidates("/hel");
|
|
502
|
+
expect(state.active).toBe(true);
|
|
503
|
+
|
|
504
|
+
state = autocompleteReducer(state, { type: "CANCEL" });
|
|
505
|
+
expect(state.active).toBe(false);
|
|
506
|
+
expect(state.candidates).toHaveLength(0);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("should not show autocomplete for non-slash input", () => {
|
|
510
|
+
// Empty query produces inactive state
|
|
511
|
+
const state = harness.getAutocompleteCandidates("");
|
|
512
|
+
expect(shouldShowAutocomplete(state)).toBe(false);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("should show autocomplete for slash input", () => {
|
|
516
|
+
const state = harness.getAutocompleteCandidates("/h");
|
|
517
|
+
expect(shouldShowAutocomplete(state)).toBe(true);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("should filter by query correctly", () => {
|
|
521
|
+
const state = harness.getAutocompleteCandidates("/ex");
|
|
522
|
+
|
|
523
|
+
expect(state.candidates.length).toBeGreaterThan(0);
|
|
524
|
+
const names = state.candidates.map((c) => c.command.name);
|
|
525
|
+
expect(names).toContain("exit(quit)");
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// =============================================================================
|
|
530
|
+
// E2E Test: Full Lifecycle
|
|
531
|
+
// =============================================================================
|
|
532
|
+
|
|
533
|
+
describe("E2E: full command lifecycle", () => {
|
|
534
|
+
let harness: CommandSystemHarness;
|
|
535
|
+
|
|
536
|
+
beforeEach(() => {
|
|
537
|
+
harness = new CommandSystemHarness();
|
|
538
|
+
harness.registerCoreCommands();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("should handle complete flow: type → autocomplete → execute", async () => {
|
|
542
|
+
// Step 1: User starts typing
|
|
543
|
+
const input = "/cle";
|
|
544
|
+
|
|
545
|
+
// Step 2: Autocomplete activates
|
|
546
|
+
const autocompleteState = harness.getAutocompleteCandidates(input);
|
|
547
|
+
expect(autocompleteState.active).toBe(true);
|
|
548
|
+
expect(autocompleteState.candidates.length).toBeGreaterThan(0);
|
|
549
|
+
|
|
550
|
+
// Find clear command in candidates
|
|
551
|
+
const clearCandidate = autocompleteState.candidates.find((c) => c.command.name === "clear");
|
|
552
|
+
expect(clearCandidate).toBeDefined();
|
|
553
|
+
|
|
554
|
+
// Step 3: User completes and executes
|
|
555
|
+
const result = await harness.execute("/clear");
|
|
556
|
+
|
|
557
|
+
// Step 4: Verify result
|
|
558
|
+
expect(result.kind).toBe("success");
|
|
559
|
+
if (result.kind === "success") {
|
|
560
|
+
expect(result.clearScreen).toBe(true);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("should handle error recovery flow", async () => {
|
|
565
|
+
// Step 1: User types invalid command
|
|
566
|
+
const badResult = await harness.execute("/cleear"); // typo
|
|
567
|
+
|
|
568
|
+
// Step 2: Get error with suggestion
|
|
569
|
+
expect(badResult.kind).toBe("error");
|
|
570
|
+
if (badResult.kind === "error") {
|
|
571
|
+
expect(badResult.suggestions).toContain("/clear");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Step 3: User corrects and retries
|
|
575
|
+
const goodResult = await harness.execute("/clear");
|
|
576
|
+
expect(goodResult.kind).toBe("success");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("should maintain state across commands", async () => {
|
|
580
|
+
// Execute multiple commands
|
|
581
|
+
const result1 = await harness.execute("/help");
|
|
582
|
+
expect(result1.kind).toBe("success");
|
|
583
|
+
|
|
584
|
+
const result2 = await harness.execute("/clear");
|
|
585
|
+
expect(result2.kind).toBe("success");
|
|
586
|
+
|
|
587
|
+
const result3 = await harness.execute("/exit");
|
|
588
|
+
expect(result3.kind).toBe("success");
|
|
589
|
+
|
|
590
|
+
// All should succeed independently
|
|
591
|
+
const events = harness.getEmittedEvents();
|
|
592
|
+
expect(events).toContainEqual(expect.objectContaining({ event: "app:exit" }));
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// =============================================================================
|
|
597
|
+
// E2E Test: Parse → Execute Integration
|
|
598
|
+
// =============================================================================
|
|
599
|
+
|
|
600
|
+
describe("E2E: parser → executor integration", () => {
|
|
601
|
+
let harness: CommandSystemHarness;
|
|
602
|
+
|
|
603
|
+
beforeEach(() => {
|
|
604
|
+
harness = new CommandSystemHarness();
|
|
605
|
+
harness.registerCoreCommands();
|
|
606
|
+
|
|
607
|
+
// Add command with complex args
|
|
608
|
+
harness.registry.register(
|
|
609
|
+
createMockCommand({
|
|
610
|
+
name: "config",
|
|
611
|
+
category: "config",
|
|
612
|
+
positionalArgs: [
|
|
613
|
+
{ name: "key", type: "string", description: "Config key", required: true },
|
|
614
|
+
],
|
|
615
|
+
namedArgs: [
|
|
616
|
+
{
|
|
617
|
+
name: "value",
|
|
618
|
+
shorthand: "v",
|
|
619
|
+
type: "string",
|
|
620
|
+
description: "Config value",
|
|
621
|
+
required: false,
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: "global",
|
|
625
|
+
shorthand: "g",
|
|
626
|
+
type: "boolean",
|
|
627
|
+
description: "Global scope",
|
|
628
|
+
required: false,
|
|
629
|
+
default: false,
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
execute: async (ctx: CommandContext) => {
|
|
633
|
+
const key = ctx.parsedArgs.positional[0];
|
|
634
|
+
const value = ctx.parsedArgs.named.value;
|
|
635
|
+
const global = ctx.parsedArgs.named.global;
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
kind: "success",
|
|
639
|
+
message: `Config: ${key}=${value} (global=${global})`,
|
|
640
|
+
data: { key, value, global },
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
})
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("should parse and execute with positional and named args", async () => {
|
|
648
|
+
const result = await harness.execute("/config theme --value dark --global");
|
|
649
|
+
|
|
650
|
+
expect(result.kind).toBe("success");
|
|
651
|
+
if (result.kind === "success") {
|
|
652
|
+
expect(result.data).toEqual({
|
|
653
|
+
key: "theme",
|
|
654
|
+
value: "dark",
|
|
655
|
+
global: true,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("should parse short flags", async () => {
|
|
661
|
+
const result = await harness.execute("/config theme -v light -g");
|
|
662
|
+
|
|
663
|
+
expect(result.kind).toBe("success");
|
|
664
|
+
if (result.kind === "success") {
|
|
665
|
+
expect(result.data).toEqual({
|
|
666
|
+
key: "theme",
|
|
667
|
+
value: "light",
|
|
668
|
+
global: true,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("should parse quoted values", async () => {
|
|
674
|
+
const result = await harness.execute('/config message --value "Hello World"');
|
|
675
|
+
|
|
676
|
+
expect(result.kind).toBe("success");
|
|
677
|
+
if (result.kind === "success") {
|
|
678
|
+
expect(result.data).toMatchObject({
|
|
679
|
+
key: "message",
|
|
680
|
+
value: "Hello World",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("should handle missing required argument", async () => {
|
|
686
|
+
const result = await harness.execute("/config"); // missing 'key'
|
|
687
|
+
|
|
688
|
+
expect(result.kind).toBe("error");
|
|
689
|
+
if (result.kind === "error") {
|
|
690
|
+
expect(result.code).toBe("MISSING_ARGUMENT");
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// =============================================================================
|
|
696
|
+
// E2E Test: Fuzzy Score Integration
|
|
697
|
+
// =============================================================================
|
|
698
|
+
|
|
699
|
+
describe("E2E: fuzzy scoring in autocomplete", () => {
|
|
700
|
+
it("should rank exact match highest", () => {
|
|
701
|
+
const exactScore = fuzzyScore("help", "help");
|
|
702
|
+
const prefixScore = fuzzyScore("hel", "help");
|
|
703
|
+
const fuzzyMatch = fuzzyScore("hp", "help");
|
|
704
|
+
|
|
705
|
+
expect(exactScore).not.toBeNull();
|
|
706
|
+
expect(prefixScore).not.toBeNull();
|
|
707
|
+
expect(fuzzyMatch).not.toBeNull();
|
|
708
|
+
|
|
709
|
+
// Extract scores after null checks (safe to use optional chain in expect)
|
|
710
|
+
if (exactScore && prefixScore && fuzzyMatch) {
|
|
711
|
+
expect(exactScore.score).toBeGreaterThan(prefixScore.score);
|
|
712
|
+
expect(prefixScore.score).toBeGreaterThan(fuzzyMatch.score);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("should return null for no match", () => {
|
|
717
|
+
const result = fuzzyScore("xyz", "help");
|
|
718
|
+
expect(result).toBeNull();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("should handle word boundaries", () => {
|
|
722
|
+
const result = fuzzyScore("gc", "git-commit");
|
|
723
|
+
|
|
724
|
+
expect(result).not.toBeNull();
|
|
725
|
+
// g matches start, c matches after hyphen
|
|
726
|
+
expect(result?.ranges.length).toBe(2);
|
|
727
|
+
});
|
|
728
|
+
});
|