@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,1271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* End-to-end integration tests for TUI flows covering:
|
|
5
|
+
* 1. Send message → see in MessageList
|
|
6
|
+
* 2. Receive response → streaming updates
|
|
7
|
+
* 3. Tool request → approval dialog
|
|
8
|
+
* 4. Tool result → status update
|
|
9
|
+
*
|
|
10
|
+
* Uses ink-testing-library for rendering tests.
|
|
11
|
+
*
|
|
12
|
+
* @module __tests__/tui-integration.test
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getIcons, type IconSet } from "@vellum/shared";
|
|
16
|
+
import { Box, Text } from "ink";
|
|
17
|
+
import { render } from "ink-testing-library";
|
|
18
|
+
import type React from "react";
|
|
19
|
+
import { act, useEffect } from "react";
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
21
|
+
|
|
22
|
+
// Mock StreamingText to disable typewriter effect in integration tests
|
|
23
|
+
// This ensures content appears immediately for reliable assertions
|
|
24
|
+
vi.mock("../tui/components/Messages/StreamingText.js", async (importOriginal) => {
|
|
25
|
+
const original =
|
|
26
|
+
await importOriginal<typeof import("../tui/components/Messages/StreamingText.js")>();
|
|
27
|
+
return {
|
|
28
|
+
...original,
|
|
29
|
+
StreamingText: (props: React.ComponentProps<typeof original.StreamingText>) => (
|
|
30
|
+
<original.StreamingText {...props} typewriterEffect={false} />
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Icons are fetched in beforeEach to ensure setup has run first
|
|
36
|
+
let icons: IconSet;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
// Get icons after setup has configured Unicode mode
|
|
40
|
+
icons = getIcons();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
type Message,
|
|
45
|
+
MessageList,
|
|
46
|
+
PermissionDialog,
|
|
47
|
+
RootProvider,
|
|
48
|
+
StreamingText,
|
|
49
|
+
ToolsPanel,
|
|
50
|
+
useMessages,
|
|
51
|
+
useToolApprovalController,
|
|
52
|
+
useTools,
|
|
53
|
+
} from "../tui/index.js";
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Test Utilities
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wrapper component to provide all contexts for integration tests
|
|
61
|
+
*/
|
|
62
|
+
function IntegrationWrapper({
|
|
63
|
+
children,
|
|
64
|
+
initialMessages = [],
|
|
65
|
+
}: {
|
|
66
|
+
children: React.ReactNode;
|
|
67
|
+
initialMessages?: readonly Message[];
|
|
68
|
+
}) {
|
|
69
|
+
return (
|
|
70
|
+
<RootProvider theme="dark" initialMessages={initialMessages}>
|
|
71
|
+
{children}
|
|
72
|
+
</RootProvider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a test message with defaults
|
|
78
|
+
*/
|
|
79
|
+
function createTestMessage(overrides: Partial<Message> = {}): Message {
|
|
80
|
+
return {
|
|
81
|
+
id: `msg-${Math.random().toString(36).slice(2)}`,
|
|
82
|
+
role: "user",
|
|
83
|
+
content: "Test message",
|
|
84
|
+
timestamp: new Date(),
|
|
85
|
+
...overrides,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// Flow 1: Send Message → See in MessageList
|
|
91
|
+
// =============================================================================
|
|
92
|
+
|
|
93
|
+
describe("Integration: Send Message → MessageList", () => {
|
|
94
|
+
it("adds user message and displays it in MessageList", async () => {
|
|
95
|
+
// Component that sends a message and displays list
|
|
96
|
+
function TestComponent() {
|
|
97
|
+
const { messages, addMessage } = useMessages();
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
// Simulate sending a message after mount
|
|
101
|
+
addMessage({ role: "user", content: "Hello, AI assistant!" });
|
|
102
|
+
}, [addMessage]);
|
|
103
|
+
|
|
104
|
+
return <MessageList messages={messages} />;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { lastFrame } = render(
|
|
108
|
+
<IntegrationWrapper>
|
|
109
|
+
<TestComponent />
|
|
110
|
+
</IntegrationWrapper>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const frame = lastFrame() ?? "";
|
|
118
|
+
expect(frame).toContain("Hello, AI assistant!");
|
|
119
|
+
expect(frame).toContain("You"); // User role label
|
|
120
|
+
expect(frame).toContain(icons.user); // User role icon
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("adds multiple messages in sequence and displays them all", async () => {
|
|
124
|
+
function TestComponent() {
|
|
125
|
+
const { messages, addMessage } = useMessages();
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
addMessage({ role: "user", content: "First question" });
|
|
129
|
+
addMessage({ role: "assistant", content: "First answer" });
|
|
130
|
+
addMessage({ role: "user", content: "Second question" });
|
|
131
|
+
}, [addMessage]);
|
|
132
|
+
|
|
133
|
+
return <MessageList messages={messages} />;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { lastFrame } = render(
|
|
137
|
+
<IntegrationWrapper>
|
|
138
|
+
<TestComponent />
|
|
139
|
+
</IntegrationWrapper>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await act(async () => {
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const frame = lastFrame() ?? "";
|
|
147
|
+
expect(frame).toContain("First question");
|
|
148
|
+
expect(frame).toContain("First answer");
|
|
149
|
+
expect(frame).toContain("Second question");
|
|
150
|
+
expect(frame).toContain("You"); // User messages
|
|
151
|
+
expect(frame).toContain("Vellum"); // Assistant message
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("displays messages with correct role icons", async () => {
|
|
155
|
+
function TestComponent() {
|
|
156
|
+
const { messages, addMessage } = useMessages();
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
addMessage({ role: "user", content: "User message" });
|
|
160
|
+
addMessage({ role: "assistant", content: "Assistant message" });
|
|
161
|
+
addMessage({ role: "system", content: "System message" });
|
|
162
|
+
addMessage({ role: "tool", content: "Tool result" });
|
|
163
|
+
}, [addMessage]);
|
|
164
|
+
|
|
165
|
+
return <MessageList messages={messages} />;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { lastFrame } = render(
|
|
169
|
+
<IntegrationWrapper>
|
|
170
|
+
<TestComponent />
|
|
171
|
+
</IntegrationWrapper>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await act(async () => {
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const frame = lastFrame() ?? "";
|
|
179
|
+
expect(frame).toContain(icons.user); // user
|
|
180
|
+
expect(frame).toContain(icons.assistant); // assistant
|
|
181
|
+
expect(frame).toContain(icons.system); // system
|
|
182
|
+
expect(frame).toContain(icons.tool); // tool
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("handles initial messages in provider", () => {
|
|
186
|
+
const initialMessages: Message[] = [
|
|
187
|
+
createTestMessage({ id: "init-1", role: "system", content: "System prompt" }),
|
|
188
|
+
createTestMessage({ id: "init-2", role: "user", content: "Initial user query" }),
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
function TestComponent() {
|
|
192
|
+
const { messages } = useMessages();
|
|
193
|
+
return <MessageList messages={messages} />;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { lastFrame } = render(
|
|
197
|
+
<IntegrationWrapper initialMessages={initialMessages}>
|
|
198
|
+
<TestComponent />
|
|
199
|
+
</IntegrationWrapper>
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const frame = lastFrame() ?? "";
|
|
203
|
+
expect(frame).toContain("System prompt");
|
|
204
|
+
expect(frame).toContain("Initial user query");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("clears all messages when clearMessages is called", async () => {
|
|
208
|
+
function TestComponent() {
|
|
209
|
+
const { messages, addMessage, clearMessages } = useMessages();
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
addMessage({ role: "user", content: "Message to be cleared" });
|
|
213
|
+
// Clear after adding
|
|
214
|
+
setTimeout(() => clearMessages(), 5);
|
|
215
|
+
}, [addMessage, clearMessages]);
|
|
216
|
+
|
|
217
|
+
return <MessageList messages={messages} />;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { lastFrame } = render(
|
|
221
|
+
<IntegrationWrapper>
|
|
222
|
+
<TestComponent />
|
|
223
|
+
</IntegrationWrapper>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Wait for clear
|
|
227
|
+
await act(async () => {
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const frame = lastFrame() ?? "";
|
|
232
|
+
expect(frame).toContain("No messages yet");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// Flow 2: Receive Response → Streaming Updates
|
|
238
|
+
// =============================================================================
|
|
239
|
+
|
|
240
|
+
describe("Integration: Receive Response → Streaming Updates", () => {
|
|
241
|
+
beforeEach(() => {
|
|
242
|
+
vi.useFakeTimers();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
afterEach(() => {
|
|
246
|
+
vi.useRealTimers();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("shows streaming indicator during response", async () => {
|
|
250
|
+
function TestComponent() {
|
|
251
|
+
const { messages, addMessage, updateMessage } = useMessages();
|
|
252
|
+
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
// Add a streaming message
|
|
255
|
+
const id = addMessage({
|
|
256
|
+
role: "assistant",
|
|
257
|
+
content: "Generating...",
|
|
258
|
+
isStreaming: true,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Complete streaming after delay
|
|
262
|
+
setTimeout(() => {
|
|
263
|
+
updateMessage(id, { isStreaming: false, content: "Complete response" });
|
|
264
|
+
}, 500);
|
|
265
|
+
}, [addMessage, updateMessage]);
|
|
266
|
+
|
|
267
|
+
return <MessageList messages={messages} />;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const { lastFrame } = render(
|
|
271
|
+
<IntegrationWrapper>
|
|
272
|
+
<TestComponent />
|
|
273
|
+
</IntegrationWrapper>
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Wait for render
|
|
277
|
+
await act(async () => {
|
|
278
|
+
vi.advanceTimersByTime(10);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let frame = lastFrame() ?? "";
|
|
282
|
+
expect(frame).toContain("streaming");
|
|
283
|
+
expect(frame).toContain("Generating...");
|
|
284
|
+
|
|
285
|
+
// After completion
|
|
286
|
+
await act(async () => {
|
|
287
|
+
vi.advanceTimersByTime(600);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
frame = lastFrame() ?? "";
|
|
291
|
+
expect(frame).toContain("Complete response");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("appends content during streaming", async () => {
|
|
295
|
+
function TestComponent() {
|
|
296
|
+
const { messages, addMessage, appendToMessage } = useMessages();
|
|
297
|
+
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
const id = addMessage({
|
|
300
|
+
role: "assistant",
|
|
301
|
+
content: "Hello",
|
|
302
|
+
isStreaming: true,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Simulate streaming chunks
|
|
306
|
+
setTimeout(() => appendToMessage(id, ", world"), 100);
|
|
307
|
+
setTimeout(() => appendToMessage(id, "!"), 200);
|
|
308
|
+
}, [addMessage, appendToMessage]);
|
|
309
|
+
|
|
310
|
+
return <MessageList messages={messages} />;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { lastFrame } = render(
|
|
314
|
+
<IntegrationWrapper>
|
|
315
|
+
<TestComponent />
|
|
316
|
+
</IntegrationWrapper>
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Wait for initial render
|
|
320
|
+
await act(async () => {
|
|
321
|
+
vi.advanceTimersByTime(10);
|
|
322
|
+
});
|
|
323
|
+
expect(lastFrame()).toContain("Hello");
|
|
324
|
+
|
|
325
|
+
// First append
|
|
326
|
+
await act(async () => {
|
|
327
|
+
vi.advanceTimersByTime(100);
|
|
328
|
+
});
|
|
329
|
+
expect(lastFrame()).toContain("Hello, world");
|
|
330
|
+
|
|
331
|
+
// Second append
|
|
332
|
+
await act(async () => {
|
|
333
|
+
vi.advanceTimersByTime(100);
|
|
334
|
+
});
|
|
335
|
+
expect(lastFrame()).toContain("Hello, world!");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("renders StreamingText component with cursor during streaming", async () => {
|
|
339
|
+
// Disable typewriter for immediate content display test
|
|
340
|
+
const { lastFrame } = render(
|
|
341
|
+
<IntegrationWrapper>
|
|
342
|
+
<StreamingText content="Typing in progress" isStreaming={true} typewriterEffect={false} />
|
|
343
|
+
</IntegrationWrapper>
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const frame = lastFrame() ?? "";
|
|
347
|
+
expect(frame).toContain("Typing in progress");
|
|
348
|
+
expect(frame).toContain("▊"); // Default cursor
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("hides cursor when streaming completes", () => {
|
|
352
|
+
const { lastFrame } = render(
|
|
353
|
+
<IntegrationWrapper>
|
|
354
|
+
<StreamingText content="Complete message" isStreaming={false} />
|
|
355
|
+
</IntegrationWrapper>
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const frame = lastFrame() ?? "";
|
|
359
|
+
expect(frame).toContain("Complete message");
|
|
360
|
+
expect(frame).not.toContain("▊");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("shows empty state with ellipsis during streaming", async () => {
|
|
364
|
+
function TestComponent() {
|
|
365
|
+
const { messages, addMessage } = useMessages();
|
|
366
|
+
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
addMessage({
|
|
369
|
+
role: "assistant",
|
|
370
|
+
content: "",
|
|
371
|
+
isStreaming: true,
|
|
372
|
+
});
|
|
373
|
+
}, [addMessage]);
|
|
374
|
+
|
|
375
|
+
return <MessageList messages={messages} />;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const { lastFrame } = render(
|
|
379
|
+
<IntegrationWrapper>
|
|
380
|
+
<TestComponent />
|
|
381
|
+
</IntegrationWrapper>
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Wait for message to be added
|
|
385
|
+
await act(async () => {
|
|
386
|
+
vi.advanceTimersByTime(10);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const frame = lastFrame() ?? "";
|
|
390
|
+
// Empty streaming message shows ellipsis placeholder
|
|
391
|
+
expect(frame).toContain("...");
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// =============================================================================
|
|
396
|
+
// Flow 3: Tool Request → Approval Dialog
|
|
397
|
+
// =============================================================================
|
|
398
|
+
|
|
399
|
+
describe("Integration: Tool Request → Approval Dialog", () => {
|
|
400
|
+
it("shows pending tool execution in dialog", async () => {
|
|
401
|
+
function TestComponent() {
|
|
402
|
+
const { pendingApproval, addExecution, approveExecution, rejectExecution } = useTools();
|
|
403
|
+
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
addExecution({
|
|
406
|
+
toolName: "read_file",
|
|
407
|
+
params: { path: "/test.txt" },
|
|
408
|
+
});
|
|
409
|
+
}, [addExecution]);
|
|
410
|
+
|
|
411
|
+
const pending = pendingApproval[0];
|
|
412
|
+
if (!pending) return <Text>No pending tools</Text>;
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<PermissionDialog
|
|
416
|
+
execution={pending}
|
|
417
|
+
riskLevel="low"
|
|
418
|
+
onApprove={() => approveExecution(pending.id)}
|
|
419
|
+
onReject={() => rejectExecution(pending.id)}
|
|
420
|
+
/>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const { lastFrame } = render(
|
|
425
|
+
<RootProvider>
|
|
426
|
+
<TestComponent />
|
|
427
|
+
</RootProvider>
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// Wait for state update
|
|
431
|
+
await act(async () => {
|
|
432
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const frame = lastFrame() ?? "";
|
|
436
|
+
expect(frame).toContain("read_file");
|
|
437
|
+
expect(frame).toContain("Low Risk");
|
|
438
|
+
expect(frame).toContain("●"); // Low risk icon
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("displays different risk levels for tools", () => {
|
|
442
|
+
const riskLevels = [
|
|
443
|
+
{ level: "low" as const, label: "Low Risk", icon: "●" },
|
|
444
|
+
{ level: "medium" as const, label: "Medium Risk", icon: "▲" },
|
|
445
|
+
{ level: "high" as const, label: "High Risk", icon: "◆" },
|
|
446
|
+
{ level: "critical" as const, label: "Critical Risk", icon: "⬢" },
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
for (const { level, label, icon } of riskLevels) {
|
|
450
|
+
const execution = {
|
|
451
|
+
id: "test-1",
|
|
452
|
+
toolName: "dangerous_operation",
|
|
453
|
+
params: {},
|
|
454
|
+
status: "pending" as const,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const { lastFrame } = render(
|
|
458
|
+
<RootProvider>
|
|
459
|
+
<PermissionDialog
|
|
460
|
+
execution={execution}
|
|
461
|
+
riskLevel={level}
|
|
462
|
+
onApprove={vi.fn()}
|
|
463
|
+
onReject={vi.fn()}
|
|
464
|
+
/>
|
|
465
|
+
</RootProvider>
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const frame = lastFrame() ?? "";
|
|
469
|
+
expect(frame).toContain(label);
|
|
470
|
+
expect(frame).toContain(icon);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("shows tool parameters in dialog", () => {
|
|
475
|
+
const execution = {
|
|
476
|
+
id: "test-1",
|
|
477
|
+
toolName: "write_file",
|
|
478
|
+
params: {
|
|
479
|
+
path: "/output.txt",
|
|
480
|
+
content: "Hello",
|
|
481
|
+
},
|
|
482
|
+
status: "pending" as const,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const { lastFrame } = render(
|
|
486
|
+
<RootProvider>
|
|
487
|
+
<PermissionDialog
|
|
488
|
+
execution={execution}
|
|
489
|
+
riskLevel="medium"
|
|
490
|
+
onApprove={vi.fn()}
|
|
491
|
+
onReject={vi.fn()}
|
|
492
|
+
/>
|
|
493
|
+
</RootProvider>
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const frame = lastFrame() ?? "";
|
|
497
|
+
expect(frame).toContain("write_file");
|
|
498
|
+
expect(frame).toContain("path");
|
|
499
|
+
expect(frame).toContain("/output.txt");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("processes multiple pending tool requests", async () => {
|
|
503
|
+
function TestComponent() {
|
|
504
|
+
const { pendingApproval, addExecution } = useTools();
|
|
505
|
+
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
addExecution({ toolName: "read_file", params: { path: "/a.txt" } });
|
|
508
|
+
addExecution({ toolName: "write_file", params: { path: "/b.txt" } });
|
|
509
|
+
addExecution({ toolName: "execute_command", params: { cmd: "ls" } });
|
|
510
|
+
}, [addExecution]);
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<Box flexDirection="column">
|
|
514
|
+
<Text>Pending: {pendingApproval.length}</Text>
|
|
515
|
+
{pendingApproval.map((exec) => (
|
|
516
|
+
<Text key={exec.id}>{exec.toolName}</Text>
|
|
517
|
+
))}
|
|
518
|
+
</Box>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const { lastFrame } = render(
|
|
523
|
+
<RootProvider>
|
|
524
|
+
<TestComponent />
|
|
525
|
+
</RootProvider>
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// Wait for state updates
|
|
529
|
+
await act(async () => {
|
|
530
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const frame = lastFrame() ?? "";
|
|
534
|
+
expect(frame).toContain("Pending: 3");
|
|
535
|
+
expect(frame).toContain("read_file");
|
|
536
|
+
expect(frame).toContain("write_file");
|
|
537
|
+
expect(frame).toContain("execute_command");
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// =============================================================================
|
|
542
|
+
// Flow 3b: Tool Approval Controller → Resume AgentLoop
|
|
543
|
+
// =============================================================================
|
|
544
|
+
|
|
545
|
+
describe("Integration: Tool Approval Controller → Resume", () => {
|
|
546
|
+
it("approves the active pending tool and calls grantPermission()", async () => {
|
|
547
|
+
const permissionGate = {
|
|
548
|
+
grantPermission: vi.fn(),
|
|
549
|
+
denyPermission: vi.fn(),
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
function TestComponent() {
|
|
553
|
+
const { addExecution, executions } = useTools();
|
|
554
|
+
const { activeApproval, approveActive } = useToolApprovalController({
|
|
555
|
+
agentLoop: permissionGate,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
useEffect(() => {
|
|
559
|
+
addExecution({ toolName: "read_file", params: { path: "/test.txt" } });
|
|
560
|
+
}, [addExecution]);
|
|
561
|
+
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
if (activeApproval) {
|
|
564
|
+
approveActive("once");
|
|
565
|
+
}
|
|
566
|
+
}, [activeApproval, approveActive]);
|
|
567
|
+
|
|
568
|
+
return <Text>{executions[0]?.status ?? "none"}</Text>;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const { lastFrame } = render(
|
|
572
|
+
<RootProvider>
|
|
573
|
+
<TestComponent />
|
|
574
|
+
</RootProvider>
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
await act(async () => {
|
|
578
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
expect(permissionGate.grantPermission).toHaveBeenCalledTimes(1);
|
|
582
|
+
expect(permissionGate.denyPermission).toHaveBeenCalledTimes(0);
|
|
583
|
+
|
|
584
|
+
const frame = lastFrame() ?? "";
|
|
585
|
+
expect(frame).toContain("approved");
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("rejects the active pending tool and calls denyPermission()", async () => {
|
|
589
|
+
const permissionGate = {
|
|
590
|
+
grantPermission: vi.fn(),
|
|
591
|
+
denyPermission: vi.fn(),
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
function TestComponent() {
|
|
595
|
+
const { addExecution, executions } = useTools();
|
|
596
|
+
const { activeApproval, rejectActive } = useToolApprovalController({
|
|
597
|
+
agentLoop: permissionGate,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
addExecution({ toolName: "write_file", params: { path: "/out.txt" } });
|
|
602
|
+
}, [addExecution]);
|
|
603
|
+
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (activeApproval) {
|
|
606
|
+
rejectActive();
|
|
607
|
+
}
|
|
608
|
+
}, [activeApproval, rejectActive]);
|
|
609
|
+
|
|
610
|
+
return <Text>{executions[0]?.status ?? "none"}</Text>;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const { lastFrame } = render(
|
|
614
|
+
<RootProvider>
|
|
615
|
+
<TestComponent />
|
|
616
|
+
</RootProvider>
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
await act(async () => {
|
|
620
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(permissionGate.denyPermission).toHaveBeenCalledTimes(1);
|
|
624
|
+
expect(permissionGate.grantPermission).toHaveBeenCalledTimes(0);
|
|
625
|
+
|
|
626
|
+
const frame = lastFrame() ?? "";
|
|
627
|
+
expect(frame).toContain("rejected");
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// =============================================================================
|
|
632
|
+
// Flow 3c: Tools Panel → Execution Rendering
|
|
633
|
+
// =============================================================================
|
|
634
|
+
|
|
635
|
+
describe("Integration: ToolsPanel", () => {
|
|
636
|
+
it("renders recent tool executions and pending count", async () => {
|
|
637
|
+
function TestComponent() {
|
|
638
|
+
const { addExecution, updateExecution } = useTools();
|
|
639
|
+
|
|
640
|
+
useEffect(() => {
|
|
641
|
+
const first = addExecution({ toolName: "read_file", params: { path: "/a.txt" } });
|
|
642
|
+
const second = addExecution({ toolName: "write_file", params: { path: "/b.txt" } });
|
|
643
|
+
updateExecution(first, { status: "complete", completedAt: new Date() });
|
|
644
|
+
updateExecution(second, { status: "pending" });
|
|
645
|
+
}, [addExecution, updateExecution]);
|
|
646
|
+
|
|
647
|
+
return <ToolsPanel maxItems={10} />;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const { lastFrame } = render(
|
|
651
|
+
<RootProvider>
|
|
652
|
+
<TestComponent />
|
|
653
|
+
</RootProvider>
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
await act(async () => {
|
|
657
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const frame = lastFrame() ?? "";
|
|
661
|
+
expect(frame).toContain("Tools");
|
|
662
|
+
expect(frame).toContain("Pending: 1");
|
|
663
|
+
expect(frame).toContain("Total: 2");
|
|
664
|
+
expect(frame).toContain("read_file");
|
|
665
|
+
expect(frame).toContain("write_file");
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// =============================================================================
|
|
670
|
+
// Flow 4: Tool Result → Status Update
|
|
671
|
+
// =============================================================================
|
|
672
|
+
|
|
673
|
+
describe("Integration: Tool Result → Status Update", () => {
|
|
674
|
+
it("shows running status after approval", async () => {
|
|
675
|
+
function TestComponent() {
|
|
676
|
+
const { executions, addExecution, approveExecution, updateExecution } = useTools();
|
|
677
|
+
|
|
678
|
+
useEffect(() => {
|
|
679
|
+
const id = addExecution({ toolName: "read_file", params: {} });
|
|
680
|
+
// Approve and start running
|
|
681
|
+
setTimeout(() => {
|
|
682
|
+
approveExecution(id);
|
|
683
|
+
updateExecution(id, { status: "running", startedAt: new Date() });
|
|
684
|
+
}, 10);
|
|
685
|
+
}, [addExecution, approveExecution, updateExecution]);
|
|
686
|
+
|
|
687
|
+
const exec = executions[0];
|
|
688
|
+
if (!exec) return <Text>No executions</Text>;
|
|
689
|
+
|
|
690
|
+
return (
|
|
691
|
+
<Text>
|
|
692
|
+
{exec.status} - {exec.toolName}
|
|
693
|
+
</Text>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const { lastFrame } = render(
|
|
698
|
+
<RootProvider>
|
|
699
|
+
<TestComponent />
|
|
700
|
+
</RootProvider>
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
// Wait for status update
|
|
704
|
+
await act(async () => {
|
|
705
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
const frame = lastFrame() ?? "";
|
|
709
|
+
// Running status check
|
|
710
|
+
expect(frame).toContain("running");
|
|
711
|
+
expect(frame).toContain("read_file");
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("shows complete status with result", async () => {
|
|
715
|
+
function TestComponent() {
|
|
716
|
+
const { executions, addExecution, updateExecution } = useTools();
|
|
717
|
+
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
const id = addExecution({ toolName: "read_file", params: {} });
|
|
720
|
+
// Complete execution
|
|
721
|
+
setTimeout(() => {
|
|
722
|
+
updateExecution(id, {
|
|
723
|
+
status: "complete",
|
|
724
|
+
result: "File contents here",
|
|
725
|
+
completedAt: new Date(),
|
|
726
|
+
});
|
|
727
|
+
}, 10);
|
|
728
|
+
}, [addExecution, updateExecution]);
|
|
729
|
+
|
|
730
|
+
const exec = executions[0];
|
|
731
|
+
if (!exec) return <Text>No executions</Text>;
|
|
732
|
+
|
|
733
|
+
return (
|
|
734
|
+
<Text>
|
|
735
|
+
{exec.status} - {exec.toolName}
|
|
736
|
+
</Text>
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const { lastFrame } = render(
|
|
741
|
+
<RootProvider>
|
|
742
|
+
<TestComponent />
|
|
743
|
+
</RootProvider>
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
// Wait for completion
|
|
747
|
+
await act(async () => {
|
|
748
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const frame = lastFrame() ?? "";
|
|
752
|
+
expect(frame).toContain("complete");
|
|
753
|
+
expect(frame).toContain("read_file");
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("shows error status with error info", async () => {
|
|
757
|
+
function TestComponent() {
|
|
758
|
+
const { executions, addExecution, updateExecution } = useTools();
|
|
759
|
+
|
|
760
|
+
useEffect(() => {
|
|
761
|
+
const id = addExecution({ toolName: "execute_command", params: {} });
|
|
762
|
+
// Fail execution
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
updateExecution(id, {
|
|
765
|
+
status: "error",
|
|
766
|
+
error: new Error("Permission denied"),
|
|
767
|
+
completedAt: new Date(),
|
|
768
|
+
});
|
|
769
|
+
}, 10);
|
|
770
|
+
}, [addExecution, updateExecution]);
|
|
771
|
+
|
|
772
|
+
const exec = executions[0];
|
|
773
|
+
if (!exec) return <Text>No executions</Text>;
|
|
774
|
+
|
|
775
|
+
return (
|
|
776
|
+
<Text>
|
|
777
|
+
{exec.status} - {exec.toolName}
|
|
778
|
+
</Text>
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const { lastFrame } = render(
|
|
783
|
+
<RootProvider>
|
|
784
|
+
<TestComponent />
|
|
785
|
+
</RootProvider>
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
// Wait for error
|
|
789
|
+
await act(async () => {
|
|
790
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const frame = lastFrame() ?? "";
|
|
794
|
+
expect(frame).toContain("error");
|
|
795
|
+
expect(frame).toContain("execute_command");
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("shows rejected status when tool is rejected", async () => {
|
|
799
|
+
function TestComponent() {
|
|
800
|
+
const { executions, addExecution, rejectExecution } = useTools();
|
|
801
|
+
|
|
802
|
+
useEffect(() => {
|
|
803
|
+
const id = addExecution({ toolName: "dangerous_tool", params: {} });
|
|
804
|
+
// Reject execution
|
|
805
|
+
setTimeout(() => rejectExecution(id), 10);
|
|
806
|
+
}, [addExecution, rejectExecution]);
|
|
807
|
+
|
|
808
|
+
const exec = executions[0];
|
|
809
|
+
if (!exec) return <Text>No executions</Text>;
|
|
810
|
+
|
|
811
|
+
return (
|
|
812
|
+
<Text>
|
|
813
|
+
{exec.status} - {exec.toolName}
|
|
814
|
+
</Text>
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const { lastFrame } = render(
|
|
819
|
+
<RootProvider>
|
|
820
|
+
<TestComponent />
|
|
821
|
+
</RootProvider>
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// Wait for rejection
|
|
825
|
+
await act(async () => {
|
|
826
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const frame = lastFrame() ?? "";
|
|
830
|
+
expect(frame).toContain("rejected");
|
|
831
|
+
expect(frame).toContain("dangerous_tool");
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("tracks full tool lifecycle: pending → approved → running → complete", async () => {
|
|
835
|
+
// Use fake timers for deterministic control over async state transitions
|
|
836
|
+
vi.useFakeTimers();
|
|
837
|
+
|
|
838
|
+
const statusHistory: string[] = [];
|
|
839
|
+
|
|
840
|
+
function TestComponent() {
|
|
841
|
+
const { executions, addExecution, approveExecution, updateExecution } = useTools();
|
|
842
|
+
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
const id = addExecution({ toolName: "test_tool", params: {} });
|
|
845
|
+
|
|
846
|
+
// Lifecycle simulation with delays
|
|
847
|
+
setTimeout(() => {
|
|
848
|
+
approveExecution(id);
|
|
849
|
+
}, 100);
|
|
850
|
+
|
|
851
|
+
setTimeout(() => {
|
|
852
|
+
updateExecution(id, { status: "running", startedAt: new Date() });
|
|
853
|
+
}, 200);
|
|
854
|
+
|
|
855
|
+
setTimeout(() => {
|
|
856
|
+
updateExecution(id, {
|
|
857
|
+
status: "complete",
|
|
858
|
+
result: "success",
|
|
859
|
+
completedAt: new Date(),
|
|
860
|
+
});
|
|
861
|
+
}, 300);
|
|
862
|
+
}, [addExecution, approveExecution, updateExecution]);
|
|
863
|
+
|
|
864
|
+
const exec = executions[0];
|
|
865
|
+
const status = exec?.status;
|
|
866
|
+
|
|
867
|
+
// Track status changes via useEffect to capture every state transition
|
|
868
|
+
useEffect(() => {
|
|
869
|
+
if (status && !statusHistory.includes(status)) {
|
|
870
|
+
statusHistory.push(status);
|
|
871
|
+
}
|
|
872
|
+
}, [status]);
|
|
873
|
+
|
|
874
|
+
return <Text>{exec?.status ?? "none"}</Text>;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
render(
|
|
878
|
+
<RootProvider>
|
|
879
|
+
<TestComponent />
|
|
880
|
+
</RootProvider>
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
// Initial render captures pending state
|
|
884
|
+
await act(async () => {
|
|
885
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Advance to approved state (100ms)
|
|
889
|
+
await act(async () => {
|
|
890
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// Advance to running state (200ms)
|
|
894
|
+
await act(async () => {
|
|
895
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// Advance to complete state (300ms)
|
|
899
|
+
await act(async () => {
|
|
900
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Restore real timers
|
|
904
|
+
vi.useRealTimers();
|
|
905
|
+
|
|
906
|
+
// Verify we went through the states
|
|
907
|
+
// Note: Due to React's batching, 'running' may be skipped in fast transitions.
|
|
908
|
+
// We assert the essential states and allow running to be optional.
|
|
909
|
+
expect(statusHistory).toContain("pending");
|
|
910
|
+
expect(statusHistory).toContain("approved");
|
|
911
|
+
expect(statusHistory).toContain("complete");
|
|
912
|
+
// Running is expected but may be batched away in some environments
|
|
913
|
+
// The key invariant is: pending → approved → complete happened in order
|
|
914
|
+
expect(statusHistory.indexOf("pending")).toBeLessThan(statusHistory.indexOf("approved"));
|
|
915
|
+
expect(statusHistory.indexOf("approved")).toBeLessThan(statusHistory.indexOf("complete"));
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it("approves all pending tools at once", async () => {
|
|
919
|
+
function TestComponent() {
|
|
920
|
+
const { executions, pendingApproval, addExecution, approveAll } = useTools();
|
|
921
|
+
|
|
922
|
+
useEffect(() => {
|
|
923
|
+
addExecution({ toolName: "tool_1", params: {} });
|
|
924
|
+
addExecution({ toolName: "tool_2", params: {} });
|
|
925
|
+
addExecution({ toolName: "tool_3", params: {} });
|
|
926
|
+
|
|
927
|
+
// Approve all after adding
|
|
928
|
+
setTimeout(() => approveAll(), 10);
|
|
929
|
+
}, [addExecution, approveAll]);
|
|
930
|
+
|
|
931
|
+
return (
|
|
932
|
+
<Box flexDirection="column">
|
|
933
|
+
<Text>Pending: {pendingApproval.length}</Text>
|
|
934
|
+
<Text>Approved: {executions.filter((e) => e.status === "approved").length}</Text>
|
|
935
|
+
</Box>
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const { lastFrame } = render(
|
|
940
|
+
<RootProvider>
|
|
941
|
+
<TestComponent />
|
|
942
|
+
</RootProvider>
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
// Wait for approve all
|
|
946
|
+
await act(async () => {
|
|
947
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const frame = lastFrame() ?? "";
|
|
951
|
+
expect(frame).toContain("Pending: 0");
|
|
952
|
+
expect(frame).toContain("Approved: 3");
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it("clears all tool executions", async () => {
|
|
956
|
+
function TestComponent() {
|
|
957
|
+
const { executions, addExecution, clearExecutions } = useTools();
|
|
958
|
+
|
|
959
|
+
useEffect(() => {
|
|
960
|
+
addExecution({ toolName: "tool_1", params: {} });
|
|
961
|
+
addExecution({ toolName: "tool_2", params: {} });
|
|
962
|
+
|
|
963
|
+
// Clear after adding
|
|
964
|
+
setTimeout(() => clearExecutions(), 10);
|
|
965
|
+
}, [addExecution, clearExecutions]);
|
|
966
|
+
|
|
967
|
+
return <Text>Count: {executions.length}</Text>;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const { lastFrame } = render(
|
|
971
|
+
<RootProvider>
|
|
972
|
+
<TestComponent />
|
|
973
|
+
</RootProvider>
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
// Wait for clear
|
|
977
|
+
await act(async () => {
|
|
978
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
expect(lastFrame()).toContain("Count: 0");
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// =============================================================================
|
|
986
|
+
// Combined Integration Flows
|
|
987
|
+
// =============================================================================
|
|
988
|
+
|
|
989
|
+
describe("Integration: Combined Message and Tool Flows", () => {
|
|
990
|
+
it("shows tool_group message with tool calls", async () => {
|
|
991
|
+
function TestComponent() {
|
|
992
|
+
const { messages, addMessage } = useMessages();
|
|
993
|
+
|
|
994
|
+
useEffect(() => {
|
|
995
|
+
// Assistant message announcing tool usage
|
|
996
|
+
addMessage({
|
|
997
|
+
role: "assistant",
|
|
998
|
+
content: "Let me read that file for you",
|
|
999
|
+
});
|
|
1000
|
+
// Separate tool_group message for tool execution
|
|
1001
|
+
addMessage({
|
|
1002
|
+
role: "tool_group",
|
|
1003
|
+
content: "",
|
|
1004
|
+
toolCalls: [
|
|
1005
|
+
{
|
|
1006
|
+
id: "tc-1",
|
|
1007
|
+
name: "read_file",
|
|
1008
|
+
arguments: { path: "/test.txt" },
|
|
1009
|
+
status: "completed",
|
|
1010
|
+
result: "file contents",
|
|
1011
|
+
},
|
|
1012
|
+
],
|
|
1013
|
+
});
|
|
1014
|
+
}, [addMessage]);
|
|
1015
|
+
|
|
1016
|
+
return <MessageList messages={messages} />;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const { lastFrame } = render(
|
|
1020
|
+
<IntegrationWrapper>
|
|
1021
|
+
<TestComponent />
|
|
1022
|
+
</IntegrationWrapper>
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
await act(async () => {
|
|
1026
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const frame = lastFrame() ?? "";
|
|
1030
|
+
expect(frame).toContain("Let me read that file for you");
|
|
1031
|
+
expect(frame).toContain("read_file");
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it("handles conversation with interleaved messages and tool_group", async () => {
|
|
1035
|
+
function TestComponent() {
|
|
1036
|
+
const { messages, addMessage } = useMessages();
|
|
1037
|
+
|
|
1038
|
+
useEffect(() => {
|
|
1039
|
+
addMessage({ role: "user", content: "Read the config file" });
|
|
1040
|
+
addMessage({
|
|
1041
|
+
role: "assistant",
|
|
1042
|
+
content: "Reading config.json...",
|
|
1043
|
+
});
|
|
1044
|
+
// Tool execution as separate tool_group message
|
|
1045
|
+
addMessage({
|
|
1046
|
+
role: "tool_group",
|
|
1047
|
+
content: "",
|
|
1048
|
+
toolCalls: [
|
|
1049
|
+
{
|
|
1050
|
+
id: "tc-1",
|
|
1051
|
+
name: "read_file",
|
|
1052
|
+
arguments: { path: "config.json" },
|
|
1053
|
+
status: "completed",
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
});
|
|
1057
|
+
addMessage({ role: "tool", content: '{"debug": true}' });
|
|
1058
|
+
addMessage({
|
|
1059
|
+
role: "assistant",
|
|
1060
|
+
content: "The config has debug mode enabled",
|
|
1061
|
+
});
|
|
1062
|
+
}, [addMessage]);
|
|
1063
|
+
|
|
1064
|
+
return <MessageList messages={messages} />;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const { lastFrame } = render(
|
|
1068
|
+
<IntegrationWrapper>
|
|
1069
|
+
<TestComponent />
|
|
1070
|
+
</IntegrationWrapper>
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
await act(async () => {
|
|
1074
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
const frame = lastFrame() ?? "";
|
|
1078
|
+
expect(frame).toContain("Read the config file");
|
|
1079
|
+
expect(frame).toContain("Reading config.json...");
|
|
1080
|
+
expect(frame).toContain("read_file");
|
|
1081
|
+
expect(frame).toContain("debug");
|
|
1082
|
+
expect(frame).toContain("debug mode enabled");
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it("updates tool_group message after tool completion", async () => {
|
|
1086
|
+
function TestComponent() {
|
|
1087
|
+
const { messages, addMessage, updateMessage } = useMessages();
|
|
1088
|
+
|
|
1089
|
+
useEffect(() => {
|
|
1090
|
+
// Assistant announces processing
|
|
1091
|
+
addMessage({
|
|
1092
|
+
role: "assistant",
|
|
1093
|
+
content: "Processing...",
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Tool_group with pending tool
|
|
1097
|
+
const toolGroupId = addMessage({
|
|
1098
|
+
role: "tool_group",
|
|
1099
|
+
content: "",
|
|
1100
|
+
toolCalls: [
|
|
1101
|
+
{
|
|
1102
|
+
id: "tc-1",
|
|
1103
|
+
name: "analyze",
|
|
1104
|
+
arguments: {},
|
|
1105
|
+
status: "pending",
|
|
1106
|
+
},
|
|
1107
|
+
],
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// Update tool_group with completed tool
|
|
1111
|
+
setTimeout(() => {
|
|
1112
|
+
updateMessage(toolGroupId, {
|
|
1113
|
+
toolCalls: [
|
|
1114
|
+
{
|
|
1115
|
+
id: "tc-1",
|
|
1116
|
+
name: "analyze",
|
|
1117
|
+
arguments: {},
|
|
1118
|
+
status: "completed",
|
|
1119
|
+
result: { score: 95 },
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
});
|
|
1123
|
+
// Add completion message
|
|
1124
|
+
addMessage({
|
|
1125
|
+
role: "assistant",
|
|
1126
|
+
content: "Analysis complete",
|
|
1127
|
+
});
|
|
1128
|
+
}, 10);
|
|
1129
|
+
}, [addMessage, updateMessage]);
|
|
1130
|
+
|
|
1131
|
+
return <MessageList messages={messages} />;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const { lastFrame } = render(
|
|
1135
|
+
<IntegrationWrapper>
|
|
1136
|
+
<TestComponent />
|
|
1137
|
+
</IntegrationWrapper>
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
// After update
|
|
1141
|
+
await act(async () => {
|
|
1142
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const frame = lastFrame() ?? "";
|
|
1146
|
+
expect(frame).toContain("analyze");
|
|
1147
|
+
expect(frame).toContain("Analysis complete");
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// =============================================================================
|
|
1152
|
+
// Edge Cases and Error Handling
|
|
1153
|
+
// =============================================================================
|
|
1154
|
+
|
|
1155
|
+
describe("Integration: Edge Cases", () => {
|
|
1156
|
+
it("handles empty message list gracefully", () => {
|
|
1157
|
+
function TestComponent() {
|
|
1158
|
+
const { messages } = useMessages();
|
|
1159
|
+
return <MessageList messages={messages} />;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const { lastFrame } = render(
|
|
1163
|
+
<IntegrationWrapper>
|
|
1164
|
+
<TestComponent />
|
|
1165
|
+
</IntegrationWrapper>
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
expect(lastFrame()).toContain("No messages yet");
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it("handles messages with empty content", async () => {
|
|
1172
|
+
function TestComponent() {
|
|
1173
|
+
const { messages, addMessage } = useMessages();
|
|
1174
|
+
|
|
1175
|
+
useEffect(() => {
|
|
1176
|
+
addMessage({ role: "assistant", content: "", isStreaming: false });
|
|
1177
|
+
}, [addMessage]);
|
|
1178
|
+
|
|
1179
|
+
return <MessageList messages={messages} />;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const { lastFrame } = render(
|
|
1183
|
+
<IntegrationWrapper>
|
|
1184
|
+
<TestComponent />
|
|
1185
|
+
</IntegrationWrapper>
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
await act(async () => {
|
|
1189
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const frame = lastFrame() ?? "";
|
|
1193
|
+
expect(frame).toContain("(empty)");
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it("handles tool execution with no pending approvals", () => {
|
|
1197
|
+
function TestComponent() {
|
|
1198
|
+
const { pendingApproval } = useTools();
|
|
1199
|
+
return <Text>Pending: {pendingApproval.length}</Text>;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const { lastFrame } = render(
|
|
1203
|
+
<RootProvider>
|
|
1204
|
+
<TestComponent />
|
|
1205
|
+
</RootProvider>
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
expect(lastFrame()).toContain("Pending: 0");
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it("handles update to non-existent message gracefully", async () => {
|
|
1212
|
+
function TestComponent() {
|
|
1213
|
+
const { messages, updateMessage, addMessage } = useMessages();
|
|
1214
|
+
|
|
1215
|
+
useEffect(() => {
|
|
1216
|
+
// Add one message
|
|
1217
|
+
addMessage({ role: "user", content: "Real message" });
|
|
1218
|
+
// Try to update non-existent
|
|
1219
|
+
updateMessage("non-existent-id", { content: "Updated" });
|
|
1220
|
+
}, [addMessage, updateMessage]);
|
|
1221
|
+
|
|
1222
|
+
return <MessageList messages={messages} />;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const { lastFrame } = render(
|
|
1226
|
+
<IntegrationWrapper>
|
|
1227
|
+
<TestComponent />
|
|
1228
|
+
</IntegrationWrapper>
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1231
|
+
await act(async () => {
|
|
1232
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// Original message should still be there
|
|
1236
|
+
expect(lastFrame()).toContain("Real message");
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it("handles rapid message updates without losing data", async () => {
|
|
1240
|
+
function TestComponent() {
|
|
1241
|
+
const { messages, addMessage, appendToMessage } = useMessages();
|
|
1242
|
+
|
|
1243
|
+
useEffect(() => {
|
|
1244
|
+
const id = addMessage({ role: "assistant", content: "Start", isStreaming: true });
|
|
1245
|
+
|
|
1246
|
+
// Rapid updates
|
|
1247
|
+
for (let i = 0; i < 10; i++) {
|
|
1248
|
+
setTimeout(() => appendToMessage(id, `.${i}`), i * 2);
|
|
1249
|
+
}
|
|
1250
|
+
}, [addMessage, appendToMessage]);
|
|
1251
|
+
|
|
1252
|
+
return <MessageList messages={messages} />;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const { lastFrame } = render(
|
|
1256
|
+
<IntegrationWrapper>
|
|
1257
|
+
<TestComponent />
|
|
1258
|
+
</IntegrationWrapper>
|
|
1259
|
+
);
|
|
1260
|
+
|
|
1261
|
+
// Wait for all updates with real timers
|
|
1262
|
+
await act(async () => {
|
|
1263
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
const frame = lastFrame() ?? "";
|
|
1267
|
+
expect(frame).toContain("Start");
|
|
1268
|
+
// Should have accumulated updates
|
|
1269
|
+
expect(frame).toContain(".9");
|
|
1270
|
+
});
|
|
1271
|
+
});
|