@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLineBuffer Hook Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the line buffer hook that pre-wraps messages for scrolling.
|
|
5
|
+
*
|
|
6
|
+
* @module tui/hooks/__tests__/useLineBuffer.test
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { wrapLine, wrapText } from "../useLineBuffer.js";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Pure Function Tests
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
describe("wrapLine", () => {
|
|
17
|
+
describe("basic wrapping", () => {
|
|
18
|
+
it("should return single line when text fits", () => {
|
|
19
|
+
const result = wrapLine("hello", 10);
|
|
20
|
+
expect(result).toEqual(["hello"]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should wrap text that exceeds width", () => {
|
|
24
|
+
const result = wrapLine("hello world", 5);
|
|
25
|
+
expect(result).toEqual(["hello", " worl", "d"]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should handle empty string", () => {
|
|
29
|
+
const result = wrapLine("", 10);
|
|
30
|
+
expect(result).toEqual([""]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle whitespace-only string", () => {
|
|
34
|
+
const result = wrapLine(" ", 10);
|
|
35
|
+
expect(result).toEqual([" "]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should handle single character", () => {
|
|
39
|
+
const result = wrapLine("a", 10);
|
|
40
|
+
expect(result).toEqual(["a"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should handle exact width match", () => {
|
|
44
|
+
const result = wrapLine("hello", 5);
|
|
45
|
+
expect(result).toEqual(["hello"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should handle minimum width of 1", () => {
|
|
49
|
+
const result = wrapLine("abc", 0);
|
|
50
|
+
// Width 0 should be treated as 1
|
|
51
|
+
expect(result).toEqual(["a", "b", "c"]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("CJK and special characters", () => {
|
|
56
|
+
it("should handle CJK characters (2-width)", () => {
|
|
57
|
+
// 中文 = 4 display width (2 chars * 2 width each)
|
|
58
|
+
const result = wrapLine("中文测试", 4);
|
|
59
|
+
// Each CJK char is 2 width, so 4 width fits 2 chars
|
|
60
|
+
expect(result).toEqual(["中文", "测试"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should wrap mixed ASCII and CJK", () => {
|
|
64
|
+
// "ab中" = 2 + 2 = 4 width
|
|
65
|
+
const result = wrapLine("ab中文", 4);
|
|
66
|
+
expect(result).toEqual(["ab中", "文"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("edge cases", () => {
|
|
71
|
+
it("should handle very long line", () => {
|
|
72
|
+
const longText = "a".repeat(100);
|
|
73
|
+
const result = wrapLine(longText, 10);
|
|
74
|
+
expect(result.length).toBe(10);
|
|
75
|
+
expect(result.every((line) => line.length === 10)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should handle width of 1", () => {
|
|
79
|
+
const result = wrapLine("abc", 1);
|
|
80
|
+
expect(result).toEqual(["a", "b", "c"]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("wrapText", () => {
|
|
86
|
+
describe("multi-line wrapping", () => {
|
|
87
|
+
it("should preserve existing newlines", () => {
|
|
88
|
+
const result = wrapText("hello\nworld", 20);
|
|
89
|
+
expect(result).toEqual(["hello", "world"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should wrap each line independently", () => {
|
|
93
|
+
const result = wrapText("hello world\nfoo bar", 5);
|
|
94
|
+
expect(result).toEqual(["hello", " worl", "d", "foo b", "ar"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle empty lines", () => {
|
|
98
|
+
const result = wrapText("hello\n\nworld", 20);
|
|
99
|
+
expect(result).toEqual(["hello", "", "world"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should handle empty string", () => {
|
|
103
|
+
const result = wrapText("", 20);
|
|
104
|
+
expect(result).toEqual([""]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle string with only newlines", () => {
|
|
108
|
+
const result = wrapText("\n\n", 20);
|
|
109
|
+
expect(result).toEqual(["", "", ""]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("combined scenarios", () => {
|
|
114
|
+
it("should handle complex multi-line text", () => {
|
|
115
|
+
const text = "First line that is very long\nShort\n\nLast";
|
|
116
|
+
const result = wrapText(text, 10);
|
|
117
|
+
expect(result).toEqual(["First line", " that is v", "ery long", "Short", "", "Last"]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// Hook Integration Tests (using renderHook pattern simulation)
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
describe("useLineBuffer logic", () => {
|
|
127
|
+
// Simulate the createEntry logic for testing
|
|
128
|
+
function createEntry(
|
|
129
|
+
messageId: string,
|
|
130
|
+
content: string,
|
|
131
|
+
width: number,
|
|
132
|
+
padding = 4
|
|
133
|
+
): { messageId: string; lines: string[]; wrapWidth: number } {
|
|
134
|
+
const contentWidth = Math.max(10, width - padding);
|
|
135
|
+
const lines = wrapText(content, contentWidth);
|
|
136
|
+
return { messageId, lines, wrapWidth: width };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
describe("basic buffering", () => {
|
|
140
|
+
it("should create entry with wrapped lines", () => {
|
|
141
|
+
const entry = createEntry("msg1", "hello world", 20);
|
|
142
|
+
expect(entry.messageId).toBe("msg1");
|
|
143
|
+
expect(entry.wrapWidth).toBe(20);
|
|
144
|
+
expect(entry.lines.length).toBeGreaterThan(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should wrap content at correct width", () => {
|
|
148
|
+
// Width 20 - padding 4 = 16 char content width
|
|
149
|
+
const entry = createEntry("msg1", "a".repeat(32), 20);
|
|
150
|
+
// 32 chars at 16 width = 2 lines
|
|
151
|
+
expect(entry.lines.length).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("width change detection", () => {
|
|
156
|
+
it("should re-wrap when width changes", () => {
|
|
157
|
+
const entry1 = createEntry("msg1", "a".repeat(30), 20); // 16 char width -> 2 lines
|
|
158
|
+
const entry2 = createEntry("msg1", "a".repeat(30), 40); // 36 char width -> 1 line
|
|
159
|
+
|
|
160
|
+
expect(entry1.wrapWidth).toBe(20);
|
|
161
|
+
expect(entry2.wrapWidth).toBe(40);
|
|
162
|
+
expect(entry1.lines.length).toBeGreaterThan(entry2.lines.length);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("getVisibleLines logic", () => {
|
|
167
|
+
it("should return correct range of lines", () => {
|
|
168
|
+
const entries = [
|
|
169
|
+
createEntry("msg1", "line1\nline2", 80),
|
|
170
|
+
createEntry("msg2", "line3\nline4", 80),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// Flatten all lines
|
|
174
|
+
const allLines = entries.flatMap((e) => e.lines);
|
|
175
|
+
|
|
176
|
+
// Simulate getVisibleLines(1, 3) - lines at index 1 and 2
|
|
177
|
+
const visible = allLines.slice(1, 3);
|
|
178
|
+
expect(visible.length).toBe(2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should handle out-of-bounds range gracefully", () => {
|
|
182
|
+
const entries = [createEntry("msg1", "hello", 80)];
|
|
183
|
+
const allLines = entries.flatMap((e) => e.lines);
|
|
184
|
+
|
|
185
|
+
// Request range beyond available lines
|
|
186
|
+
const start = Math.max(0, -5);
|
|
187
|
+
const end = Math.min(allLines.length, 100);
|
|
188
|
+
const visible = allLines.slice(start, end);
|
|
189
|
+
|
|
190
|
+
expect(visible.length).toBeLessThanOrEqual(allLines.length);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should return empty array for invalid range", () => {
|
|
194
|
+
const entries = [createEntry("msg1", "hello", 80)];
|
|
195
|
+
const allLines = entries.flatMap((e) => e.lines);
|
|
196
|
+
|
|
197
|
+
// Start >= end should return empty
|
|
198
|
+
const start = 5;
|
|
199
|
+
const end = Math.min(allLines.length, start);
|
|
200
|
+
const visible = allLines.slice(start, end);
|
|
201
|
+
|
|
202
|
+
expect(visible).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("ring buffer logic", () => {
|
|
207
|
+
it("should limit total lines to maxLines", () => {
|
|
208
|
+
const maxLines = 10;
|
|
209
|
+
const entries: Array<{ messageId: string; lines: string[]; wrapWidth: number }> = [];
|
|
210
|
+
let totalLineCount = 0;
|
|
211
|
+
|
|
212
|
+
// Create entries that exceed maxLines
|
|
213
|
+
for (let i = 0; i < 5; i++) {
|
|
214
|
+
const entry = createEntry(`msg${i}`, "line1\nline2\nline3", 80);
|
|
215
|
+
entries.push(entry);
|
|
216
|
+
totalLineCount += entry.lines.length;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Simulate ring buffer trimming
|
|
220
|
+
while (totalLineCount > maxLines && entries.length > 1) {
|
|
221
|
+
const removed = entries.shift();
|
|
222
|
+
if (removed) {
|
|
223
|
+
totalLineCount -= removed.lines.length;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
expect(totalLineCount).toBeLessThanOrEqual(maxLines);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should always keep at least one entry", () => {
|
|
231
|
+
const maxLines = 1;
|
|
232
|
+
const entries = [createEntry("msg1", "line1\nline2\nline3\nline4\nline5", 80)];
|
|
233
|
+
let totalLineCount = entries.reduce((sum, e) => sum + e.lines.length, 0);
|
|
234
|
+
|
|
235
|
+
// Even with maxLines=1, we should keep the one entry
|
|
236
|
+
while (totalLineCount > maxLines && entries.length > 1) {
|
|
237
|
+
const removed = entries.shift();
|
|
238
|
+
if (removed) {
|
|
239
|
+
totalLineCount -= removed.lines.length;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
expect(entries.length).toBe(1);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("total lines calculation", () => {
|
|
248
|
+
it("should sum all entry line counts", () => {
|
|
249
|
+
const entries = [
|
|
250
|
+
createEntry("msg1", "a\nb", 80), // 2 lines
|
|
251
|
+
createEntry("msg2", "c\nd\ne", 80), // 3 lines
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
const totalLines = entries.reduce((sum, e) => sum + e.lines.length, 0);
|
|
255
|
+
expect(totalLines).toBe(5);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should handle empty entries", () => {
|
|
259
|
+
const entries = [createEntry("msg1", "", 80), createEntry("msg2", "hello", 80)];
|
|
260
|
+
|
|
261
|
+
const totalLines = entries.reduce((sum, e) => sum + e.lines.length, 0);
|
|
262
|
+
expect(totalLines).toBeGreaterThan(0);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// =============================================================================
|
|
268
|
+
// Performance Tests
|
|
269
|
+
// =============================================================================
|
|
270
|
+
|
|
271
|
+
describe("performance characteristics", () => {
|
|
272
|
+
it("should handle large content efficiently", () => {
|
|
273
|
+
const largeContent = "x".repeat(10000);
|
|
274
|
+
const start = performance.now();
|
|
275
|
+
|
|
276
|
+
const result = wrapText(largeContent, 80);
|
|
277
|
+
|
|
278
|
+
const duration = performance.now() - start;
|
|
279
|
+
// Use relaxed threshold for CI environments where performance varies
|
|
280
|
+
expect(duration).toBeLessThan(500);
|
|
281
|
+
expect(result.length).toBeGreaterThan(100);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should handle many small lines efficiently", () => {
|
|
285
|
+
const manyLines = Array(1000).fill("short line").join("\n");
|
|
286
|
+
const start = performance.now();
|
|
287
|
+
|
|
288
|
+
const result = wrapText(manyLines, 80);
|
|
289
|
+
|
|
290
|
+
const duration = performance.now() - start;
|
|
291
|
+
// Use relaxed threshold for CI environments where performance varies
|
|
292
|
+
expect(duration).toBeLessThan(500);
|
|
293
|
+
expect(result.length).toBe(1000);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useModeController Hook Tests (T010)
|
|
3
|
+
*
|
|
4
|
+
* Tests the core mode selection logic extracted from the hook.
|
|
5
|
+
*
|
|
6
|
+
* @module tui/hooks/__tests__/useModeController.test
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import type {
|
|
11
|
+
ModeControllerConfig,
|
|
12
|
+
ModeControllerState,
|
|
13
|
+
ModeReason,
|
|
14
|
+
RenderMode,
|
|
15
|
+
} from "../useModeController.js";
|
|
16
|
+
|
|
17
|
+
// Extract pure logic from the hook for testing (mirrors hook internals)
|
|
18
|
+
function computeModeState(
|
|
19
|
+
availableHeight: number,
|
|
20
|
+
totalContentHeight: number,
|
|
21
|
+
config: ModeControllerConfig = {}
|
|
22
|
+
): ModeControllerState {
|
|
23
|
+
const staticMultiplier = config.staticMultiplier ?? 1.2;
|
|
24
|
+
const virtualMultiplier = config.virtualMultiplier ?? 5.0;
|
|
25
|
+
const minWindowSize = config.minWindowSize ?? 10;
|
|
26
|
+
const maxWindowSizeRatio = config.maxWindowSizeRatio ?? 0.8;
|
|
27
|
+
const forceMode = config.forceMode;
|
|
28
|
+
|
|
29
|
+
const staticThreshold = Math.max(1, availableHeight * staticMultiplier);
|
|
30
|
+
const virtualThreshold = Math.max(1, availableHeight * virtualMultiplier);
|
|
31
|
+
const windowSize = Math.max(minWindowSize, Math.floor(availableHeight * maxWindowSizeRatio));
|
|
32
|
+
const isAutoMode = !forceMode;
|
|
33
|
+
|
|
34
|
+
if (forceMode) {
|
|
35
|
+
return {
|
|
36
|
+
mode: forceMode,
|
|
37
|
+
windowSize,
|
|
38
|
+
modeReason: "forced",
|
|
39
|
+
staticThreshold,
|
|
40
|
+
virtualThreshold,
|
|
41
|
+
isAutoMode,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let mode: RenderMode;
|
|
46
|
+
let modeReason: ModeReason;
|
|
47
|
+
if (totalContentHeight <= staticThreshold) {
|
|
48
|
+
mode = "static";
|
|
49
|
+
modeReason = "content-fits";
|
|
50
|
+
} else if (totalContentHeight <= virtualThreshold) {
|
|
51
|
+
mode = "windowed";
|
|
52
|
+
modeReason = "content-exceeds-viewport";
|
|
53
|
+
} else {
|
|
54
|
+
mode = "virtualized";
|
|
55
|
+
modeReason = "content-very-large";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { mode, windowSize, modeReason, staticThreshold, virtualThreshold, isAutoMode };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("useModeController (core logic)", () => {
|
|
62
|
+
describe("default mode selection", () => {
|
|
63
|
+
it("selects static for small content", () => {
|
|
64
|
+
const result = computeModeState(20, 10);
|
|
65
|
+
expect(result.mode).toBe("static");
|
|
66
|
+
expect(result.modeReason).toBe("content-fits");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("selects windowed for medium content", () => {
|
|
70
|
+
const result = computeModeState(20, 50);
|
|
71
|
+
expect(result.mode).toBe("windowed");
|
|
72
|
+
expect(result.modeReason).toBe("content-exceeds-viewport");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("selects virtualized for large content", () => {
|
|
76
|
+
const result = computeModeState(20, 200);
|
|
77
|
+
expect(result.mode).toBe("virtualized");
|
|
78
|
+
expect(result.modeReason).toBe("content-very-large");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("threshold calculations", () => {
|
|
83
|
+
it("computes staticThreshold = availableHeight * 1.2", () => {
|
|
84
|
+
const result = computeModeState(100, 50);
|
|
85
|
+
expect(result.staticThreshold).toBe(120);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("computes virtualThreshold = availableHeight * 5.0", () => {
|
|
89
|
+
const result = computeModeState(100, 50);
|
|
90
|
+
expect(result.virtualThreshold).toBe(500);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("forceMode override", () => {
|
|
95
|
+
it("forces windowed mode when configured", () => {
|
|
96
|
+
const result = computeModeState(20, 10, { forceMode: "windowed" });
|
|
97
|
+
expect(result.mode).toBe("windowed");
|
|
98
|
+
expect(result.modeReason).toBe("forced");
|
|
99
|
+
expect(result.isAutoMode).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns isAutoMode true when not forced", () => {
|
|
103
|
+
const result = computeModeState(20, 10);
|
|
104
|
+
expect(result.isAutoMode).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("config overrides", () => {
|
|
109
|
+
it("applies minWindowSize", () => {
|
|
110
|
+
const result = computeModeState(10, 50, { minWindowSize: 15 });
|
|
111
|
+
expect(result.windowSize).toBe(15);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("applies maxWindowSizeRatio", () => {
|
|
115
|
+
const result = computeModeState(100, 50, { maxWindowSizeRatio: 0.5 });
|
|
116
|
+
expect(result.windowSize).toBe(50);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("applies custom staticMultiplier and virtualMultiplier", () => {
|
|
120
|
+
const result = computeModeState(100, 50, { staticMultiplier: 2.0, virtualMultiplier: 10.0 });
|
|
121
|
+
expect(result.staticThreshold).toBe(200);
|
|
122
|
+
expect(result.virtualThreshold).toBe(1000);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("modeReason content", () => {
|
|
127
|
+
it("includes reason for static mode", () => {
|
|
128
|
+
const result = computeModeState(20, 10);
|
|
129
|
+
expect(result.modeReason).toBe("content-fits");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("includes reason for virtualized mode", () => {
|
|
133
|
+
const result = computeModeState(20, 200);
|
|
134
|
+
expect(result.modeReason).toBe("content-very-large");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useModeShortcuts Hook Tests (T046)
|
|
3
|
+
*
|
|
4
|
+
* Focus: ensure mode switching goes through ModeManager (including spec confirmation).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CodingMode, ModeManager } from "@vellum/core";
|
|
8
|
+
import { createModeManager } from "@vellum/core";
|
|
9
|
+
import { render } from "ink-testing-library";
|
|
10
|
+
import type React from "react";
|
|
11
|
+
import { describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { type UseModeShortcutsReturn, useModeShortcuts } from "../useModeShortcuts.js";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Test Harness
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
interface TestHarnessProps {
|
|
19
|
+
readonly modeManager: ModeManager | null;
|
|
20
|
+
readonly enabled?: boolean;
|
|
21
|
+
readonly onHookReturn: (hookReturn: UseModeShortcutsReturn) => void;
|
|
22
|
+
readonly onModeSwitch?: (mode: CodingMode, success: boolean) => void;
|
|
23
|
+
readonly onError?: (mode: CodingMode, error: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TestHarness({
|
|
27
|
+
modeManager,
|
|
28
|
+
enabled,
|
|
29
|
+
onHookReturn,
|
|
30
|
+
onModeSwitch,
|
|
31
|
+
onError,
|
|
32
|
+
}: TestHarnessProps): React.ReactElement {
|
|
33
|
+
const hookReturn = useModeShortcuts({ modeManager, enabled, onModeSwitch, onError });
|
|
34
|
+
onHookReturn(hookReturn);
|
|
35
|
+
return null as unknown as React.ReactElement;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderUseModeShortcutsHook(options: Omit<TestHarnessProps, "onHookReturn">) {
|
|
39
|
+
let hookReturn: UseModeShortcutsReturn | null = null;
|
|
40
|
+
|
|
41
|
+
const { rerender, unmount } = render(
|
|
42
|
+
<TestHarness
|
|
43
|
+
modeManager={options.modeManager}
|
|
44
|
+
enabled={options.enabled}
|
|
45
|
+
onModeSwitch={options.onModeSwitch}
|
|
46
|
+
onError={options.onError}
|
|
47
|
+
onHookReturn={(r) => {
|
|
48
|
+
hookReturn = r;
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
get current() {
|
|
55
|
+
if (!hookReturn) {
|
|
56
|
+
throw new Error("Hook return not initialized");
|
|
57
|
+
}
|
|
58
|
+
return hookReturn;
|
|
59
|
+
},
|
|
60
|
+
rerender: (next: Omit<TestHarnessProps, "onHookReturn">) => {
|
|
61
|
+
rerender(
|
|
62
|
+
<TestHarness
|
|
63
|
+
modeManager={next.modeManager}
|
|
64
|
+
enabled={next.enabled}
|
|
65
|
+
onModeSwitch={next.onModeSwitch}
|
|
66
|
+
onError={next.onError}
|
|
67
|
+
onHookReturn={(r) => {
|
|
68
|
+
hookReturn = r;
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
unmount,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Tests
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
describe("useModeShortcuts", () => {
|
|
82
|
+
it("switches mode through ModeManager", async () => {
|
|
83
|
+
const manager = createModeManager({ initialMode: "vibe", requireSpecConfirmation: true });
|
|
84
|
+
const onModeSwitch = vi.fn();
|
|
85
|
+
const onError = vi.fn();
|
|
86
|
+
|
|
87
|
+
const result = renderUseModeShortcutsHook({
|
|
88
|
+
modeManager: manager,
|
|
89
|
+
enabled: true,
|
|
90
|
+
onModeSwitch,
|
|
91
|
+
onError,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const success = await result.current.switchMode("plan");
|
|
95
|
+
|
|
96
|
+
expect(success).toBe(true);
|
|
97
|
+
expect(manager.getCurrentMode()).toBe("plan");
|
|
98
|
+
expect(onModeSwitch).toHaveBeenCalledWith("plan", true);
|
|
99
|
+
expect(onError).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not bypass spec confirmation when required", async () => {
|
|
103
|
+
const manager = createModeManager({ initialMode: "vibe", requireSpecConfirmation: true });
|
|
104
|
+
const onModeSwitch = vi.fn();
|
|
105
|
+
const onError = vi.fn();
|
|
106
|
+
|
|
107
|
+
const result = renderUseModeShortcutsHook({
|
|
108
|
+
modeManager: manager,
|
|
109
|
+
enabled: true,
|
|
110
|
+
onModeSwitch,
|
|
111
|
+
onError,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const success = await result.current.switchMode("spec");
|
|
115
|
+
|
|
116
|
+
// When spec confirmation is required, ModeManager should not immediately switch.
|
|
117
|
+
expect(success).toBe(false);
|
|
118
|
+
expect(manager.getCurrentMode()).toBe("vibe");
|
|
119
|
+
expect(manager.isPendingSpecConfirmation()).toBe(true);
|
|
120
|
+
expect(onModeSwitch).toHaveBeenCalledWith("spec", false);
|
|
121
|
+
expect(onError).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("is inactive when disabled or manager is null", async () => {
|
|
125
|
+
const onModeSwitch = vi.fn();
|
|
126
|
+
const onError = vi.fn();
|
|
127
|
+
|
|
128
|
+
const result = renderUseModeShortcutsHook({
|
|
129
|
+
modeManager: null,
|
|
130
|
+
enabled: true,
|
|
131
|
+
onModeSwitch,
|
|
132
|
+
onError,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.current.isActive).toBe(false);
|
|
136
|
+
|
|
137
|
+
const success = await result.current.switchMode("plan");
|
|
138
|
+
expect(success).toBe(false);
|
|
139
|
+
expect(onModeSwitch).not.toHaveBeenCalled();
|
|
140
|
+
expect(onError).toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
});
|