@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,1008 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Adapter for AgentLoop ↔ TUI Context Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides event mapping from AgentLoop to MessagesContext and ToolsContext,
|
|
5
|
+
* enabling seamless integration between the agent execution engine and the TUI.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/adapters/agent-adapter
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AgentLoop, ExecutionResult } from "@vellum/core";
|
|
11
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
12
|
+
import { ICONS } from "../../utils/icons.js";
|
|
13
|
+
import type { ToolCallInfo } from "../context/MessagesContext.js";
|
|
14
|
+
import { useMessages } from "../context/MessagesContext.js";
|
|
15
|
+
import { useTools } from "../context/ToolsContext.js";
|
|
16
|
+
import { findLastSafeSplitPoint } from "../utils/findLastSafeSplitPoint.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interface for the Agent Adapter
|
|
24
|
+
*
|
|
25
|
+
* Provides methods to connect and disconnect from an AgentLoop,
|
|
26
|
+
* mapping its events to the TUI context providers.
|
|
27
|
+
*/
|
|
28
|
+
export interface AgentAdapter {
|
|
29
|
+
/**
|
|
30
|
+
* Connect to an AgentLoop and start listening to its events
|
|
31
|
+
*
|
|
32
|
+
* @param agentLoop - The AgentLoop instance to connect to
|
|
33
|
+
*/
|
|
34
|
+
connect: (agentLoop: AgentLoop) => void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Disconnect from the current AgentLoop and stop listening to events
|
|
38
|
+
*/
|
|
39
|
+
disconnect: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for the useAgentAdapter hook
|
|
44
|
+
*/
|
|
45
|
+
export interface UseAgentAdapterOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Whether to automatically clear contexts on disconnect
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
clearOnDisconnect?: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether to enable message splitting for long streaming responses.
|
|
54
|
+
*
|
|
55
|
+
* When enabled, long messages are split at safe points (paragraph breaks)
|
|
56
|
+
* and completed portions are moved to historyMessages for <Static> rendering.
|
|
57
|
+
*
|
|
58
|
+
* **WARNING**: This can cause messages to disappear in VirtualizedList mode
|
|
59
|
+
* because historyMessages may be outside the visible render window.
|
|
60
|
+
* Only enable if using standard (non-virtualized) message rendering.
|
|
61
|
+
*
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
enableMessageSplitting?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Return value of the useAgentAdapter hook
|
|
69
|
+
*/
|
|
70
|
+
export interface UseAgentAdapterReturn extends AgentAdapter {
|
|
71
|
+
/**
|
|
72
|
+
* Whether currently connected to an AgentLoop
|
|
73
|
+
*/
|
|
74
|
+
isConnected: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Message ID Tracking
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Tracks the current streaming message for event correlation
|
|
83
|
+
*/
|
|
84
|
+
interface StreamingMessage {
|
|
85
|
+
/** Message ID in the context */
|
|
86
|
+
id: string;
|
|
87
|
+
/** Accumulated content */
|
|
88
|
+
content: string;
|
|
89
|
+
/** Accumulated thinking content */
|
|
90
|
+
thinking: string;
|
|
91
|
+
/** Whether this message has started streaming */
|
|
92
|
+
hasStarted: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Hook Implementation
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Hook that creates an agent adapter for connecting AgentLoop events
|
|
101
|
+
* to MessagesContext and ToolsContext.
|
|
102
|
+
*
|
|
103
|
+
* Event mappings:
|
|
104
|
+
* - `stateChange` (to streaming) → addMessage to MessagesContext (streaming start)
|
|
105
|
+
* - `text` → appendToMessage in MessagesContext
|
|
106
|
+
* - `complete` → updateMessage (isStreaming: false) in MessagesContext
|
|
107
|
+
* - `toolStart` → addExecution to ToolsContext
|
|
108
|
+
* - `toolEnd` → updateExecution in ToolsContext
|
|
109
|
+
*
|
|
110
|
+
* @param options - Configuration options for the adapter
|
|
111
|
+
* @returns The agent adapter interface with connect/disconnect methods
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```tsx
|
|
115
|
+
* function AgentContainer() {
|
|
116
|
+
* const adapter = useAgentAdapter();
|
|
117
|
+
* const loopRef = useRef<AgentLoop | null>(null);
|
|
118
|
+
*
|
|
119
|
+
* useEffect(() => {
|
|
120
|
+
* const loop = new AgentLoop(config);
|
|
121
|
+
* loopRef.current = loop;
|
|
122
|
+
* adapter.connect(loop);
|
|
123
|
+
*
|
|
124
|
+
* return () => {
|
|
125
|
+
* adapter.disconnect();
|
|
126
|
+
* };
|
|
127
|
+
* }, []);
|
|
128
|
+
*
|
|
129
|
+
* return <ChatUI />;
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function useAgentAdapter(options: UseAgentAdapterOptions = {}): UseAgentAdapterReturn {
|
|
134
|
+
const { clearOnDisconnect = false, enableMessageSplitting = false } = options;
|
|
135
|
+
|
|
136
|
+
// Context hooks
|
|
137
|
+
const {
|
|
138
|
+
addMessage,
|
|
139
|
+
appendToMessage,
|
|
140
|
+
appendToThinking,
|
|
141
|
+
updateMessage,
|
|
142
|
+
clearMessages,
|
|
143
|
+
commitPendingMessage,
|
|
144
|
+
splitMessageAtSafePoint,
|
|
145
|
+
addToolGroup,
|
|
146
|
+
updateToolGroup,
|
|
147
|
+
historyMessages,
|
|
148
|
+
} = useMessages();
|
|
149
|
+
const { addExecution, updateExecution, clearExecutions, registerCallId } = useTools();
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Stable Refs for Context Methods
|
|
153
|
+
// =============================================================================
|
|
154
|
+
// Store context methods in refs to avoid callback recreation on every render.
|
|
155
|
+
// This prevents the connect/disconnect cycle that resets streamingMessageRef
|
|
156
|
+
// during active streaming, which was causing message splitting and flickering.
|
|
157
|
+
|
|
158
|
+
const addMessageRef = useRef(addMessage);
|
|
159
|
+
const appendToMessageRef = useRef(appendToMessage);
|
|
160
|
+
const appendToThinkingRef = useRef(appendToThinking);
|
|
161
|
+
const updateMessageRef = useRef(updateMessage);
|
|
162
|
+
const clearMessagesRef = useRef(clearMessages);
|
|
163
|
+
const commitPendingMessageRef = useRef(commitPendingMessage);
|
|
164
|
+
const splitMessageAtSafePointRef = useRef(splitMessageAtSafePoint);
|
|
165
|
+
const addToolGroupRef = useRef(addToolGroup);
|
|
166
|
+
const updateToolGroupRef = useRef(updateToolGroup);
|
|
167
|
+
const addExecutionRef = useRef(addExecution);
|
|
168
|
+
const updateExecutionRef = useRef(updateExecution);
|
|
169
|
+
const clearExecutionsRef = useRef(clearExecutions);
|
|
170
|
+
const registerCallIdRef = useRef(registerCallId);
|
|
171
|
+
const historyMessagesRef = useRef(historyMessages);
|
|
172
|
+
|
|
173
|
+
// Keep refs up-to-date without triggering callback recreation
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
addMessageRef.current = addMessage;
|
|
176
|
+
appendToMessageRef.current = appendToMessage;
|
|
177
|
+
appendToThinkingRef.current = appendToThinking;
|
|
178
|
+
updateMessageRef.current = updateMessage;
|
|
179
|
+
clearMessagesRef.current = clearMessages;
|
|
180
|
+
commitPendingMessageRef.current = commitPendingMessage;
|
|
181
|
+
splitMessageAtSafePointRef.current = splitMessageAtSafePoint;
|
|
182
|
+
addToolGroupRef.current = addToolGroup;
|
|
183
|
+
updateToolGroupRef.current = updateToolGroup;
|
|
184
|
+
addExecutionRef.current = addExecution;
|
|
185
|
+
updateExecutionRef.current = updateExecution;
|
|
186
|
+
clearExecutionsRef.current = clearExecutions;
|
|
187
|
+
registerCallIdRef.current = registerCallId;
|
|
188
|
+
historyMessagesRef.current = historyMessages;
|
|
189
|
+
}, [
|
|
190
|
+
addMessage,
|
|
191
|
+
appendToMessage,
|
|
192
|
+
appendToThinking,
|
|
193
|
+
updateMessage,
|
|
194
|
+
clearMessages,
|
|
195
|
+
commitPendingMessage,
|
|
196
|
+
splitMessageAtSafePoint,
|
|
197
|
+
addToolGroup,
|
|
198
|
+
updateToolGroup,
|
|
199
|
+
addExecution,
|
|
200
|
+
updateExecution,
|
|
201
|
+
clearExecutions,
|
|
202
|
+
registerCallId,
|
|
203
|
+
historyMessages,
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// =============================================================================
|
|
207
|
+
// Connection State Refs
|
|
208
|
+
// =============================================================================
|
|
209
|
+
|
|
210
|
+
// Track connection state
|
|
211
|
+
const connectedLoopRef = useRef<AgentLoop | null>(null);
|
|
212
|
+
const isConnectedRef = useRef(false);
|
|
213
|
+
|
|
214
|
+
// Track current streaming message
|
|
215
|
+
const streamingMessageRef = useRef<StreamingMessage | null>(null);
|
|
216
|
+
|
|
217
|
+
// Pending tool calls awaiting an assistant message to attach to (for persistence)
|
|
218
|
+
const pendingToolCallsRef = useRef<Map<string, ToolCallInfo>>(new Map());
|
|
219
|
+
|
|
220
|
+
// Map tool call IDs to execution IDs (for context correlation)
|
|
221
|
+
const toolIdMapRef = useRef<Map<string, string>>(new Map());
|
|
222
|
+
|
|
223
|
+
// Map tool call IDs to tool_group message IDs (for inline UI rendering)
|
|
224
|
+
const toolGroupMapRef = useRef<Map<string, string>>(new Map());
|
|
225
|
+
|
|
226
|
+
// =============================================================================
|
|
227
|
+
// Thinking Event Handling
|
|
228
|
+
// =============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Detect if the new assistant message should be marked as a continuation.
|
|
232
|
+
* A continuation occurs when:
|
|
233
|
+
* - The last message in history is a tool_group
|
|
234
|
+
* - The message before that was an assistant message
|
|
235
|
+
*
|
|
236
|
+
* This allows the UI to render a minimal `↳` indicator instead of the full header.
|
|
237
|
+
*/
|
|
238
|
+
const isContinuationAfterToolGroup = useCallback((): boolean => {
|
|
239
|
+
const history = historyMessagesRef.current;
|
|
240
|
+
if (history.length < 2) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const lastMessage = history[history.length - 1];
|
|
244
|
+
const secondLastMessage = history[history.length - 2];
|
|
245
|
+
|
|
246
|
+
// Check if last is tool_group and second-last is assistant
|
|
247
|
+
return lastMessage?.role === "tool_group" && secondLastMessage?.role === "assistant";
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Store or update a pending tool call until a streaming message exists.
|
|
252
|
+
* Ensures tool call data is persisted even when tools fire before text.
|
|
253
|
+
*/
|
|
254
|
+
const upsertPendingToolCall = useCallback((toolCallInfo: ToolCallInfo) => {
|
|
255
|
+
const existing = pendingToolCallsRef.current.get(toolCallInfo.id);
|
|
256
|
+
if (existing) {
|
|
257
|
+
pendingToolCallsRef.current.set(toolCallInfo.id, {
|
|
258
|
+
...existing,
|
|
259
|
+
...toolCallInfo,
|
|
260
|
+
arguments:
|
|
261
|
+
Object.keys(toolCallInfo.arguments).length > 0
|
|
262
|
+
? toolCallInfo.arguments
|
|
263
|
+
: existing.arguments,
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
pendingToolCallsRef.current.set(toolCallInfo.id, toolCallInfo);
|
|
267
|
+
}
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Flush any pending tool calls to attach to the next assistant message.
|
|
272
|
+
*/
|
|
273
|
+
const flushPendingToolCalls = useCallback((): ToolCallInfo[] | undefined => {
|
|
274
|
+
if (pendingToolCallsRef.current.size === 0) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
const calls = Array.from(pendingToolCallsRef.current.values());
|
|
278
|
+
pendingToolCallsRef.current.clear();
|
|
279
|
+
return calls;
|
|
280
|
+
}, []);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Handle thinking streaming from AgentLoop.
|
|
284
|
+
* Appends thinking content directly to the streaming message.
|
|
285
|
+
*/
|
|
286
|
+
const handleThinking = useCallback(
|
|
287
|
+
(text: string) => {
|
|
288
|
+
if (!streamingMessageRef.current) {
|
|
289
|
+
const pendingToolCalls = flushPendingToolCalls();
|
|
290
|
+
const isContinuation = isContinuationAfterToolGroup();
|
|
291
|
+
const id = addMessageRef.current({
|
|
292
|
+
role: "assistant",
|
|
293
|
+
content: "",
|
|
294
|
+
isStreaming: true,
|
|
295
|
+
isContinuation,
|
|
296
|
+
toolCalls: pendingToolCalls && pendingToolCalls.length > 0 ? pendingToolCalls : undefined,
|
|
297
|
+
});
|
|
298
|
+
streamingMessageRef.current = {
|
|
299
|
+
id,
|
|
300
|
+
content: "",
|
|
301
|
+
thinking: "",
|
|
302
|
+
hasStarted: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
streamingMessageRef.current.thinking += text;
|
|
306
|
+
// Append thinking content to the message via context
|
|
307
|
+
appendToThinkingRef.current(streamingMessageRef.current.id, text);
|
|
308
|
+
},
|
|
309
|
+
[flushPendingToolCalls, isContinuationAfterToolGroup]
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handle text streaming from AgentLoop
|
|
314
|
+
* Maps to: appendToMessage in MessagesContext
|
|
315
|
+
*
|
|
316
|
+
* Uses refs for context methods to maintain callback stability and prevent
|
|
317
|
+
* message splitting during re-renders.
|
|
318
|
+
*
|
|
319
|
+
* When content exceeds the threshold, checks for safe split points
|
|
320
|
+
* (paragraph breaks, headers, list items - NOT inside code blocks)
|
|
321
|
+
* and splits to move completed content to Static for better performance.
|
|
322
|
+
*/
|
|
323
|
+
const handleText = useCallback(
|
|
324
|
+
(text: string) => {
|
|
325
|
+
const streaming = streamingMessageRef.current;
|
|
326
|
+
|
|
327
|
+
if (!streaming) {
|
|
328
|
+
// Start a new streaming message if we receive text without a message
|
|
329
|
+
const pendingToolCalls = flushPendingToolCalls();
|
|
330
|
+
const isContinuation = isContinuationAfterToolGroup();
|
|
331
|
+
const id = addMessageRef.current({
|
|
332
|
+
role: "assistant",
|
|
333
|
+
content: text,
|
|
334
|
+
isStreaming: true,
|
|
335
|
+
isContinuation,
|
|
336
|
+
toolCalls: pendingToolCalls && pendingToolCalls.length > 0 ? pendingToolCalls : undefined,
|
|
337
|
+
});
|
|
338
|
+
streamingMessageRef.current = {
|
|
339
|
+
id,
|
|
340
|
+
content: text,
|
|
341
|
+
thinking: "",
|
|
342
|
+
hasStarted: true,
|
|
343
|
+
};
|
|
344
|
+
} else {
|
|
345
|
+
// Append to existing streaming message
|
|
346
|
+
streaming.content += text;
|
|
347
|
+
appendToMessageRef.current(streaming.id, text);
|
|
348
|
+
|
|
349
|
+
// Only split messages if explicitly enabled.
|
|
350
|
+
// Splitting can cause messages to disappear in VirtualizedList mode
|
|
351
|
+
// because split portions are moved to historyMessages which may not be rendered.
|
|
352
|
+
if (enableMessageSplitting) {
|
|
353
|
+
// Check if we should split at a safe point to improve performance
|
|
354
|
+
// This moves completed content to Static where it won't re-render
|
|
355
|
+
// Uses newline-gated strategy - only splits at paragraph breaks (\n\n)
|
|
356
|
+
const splitIndex = findLastSafeSplitPoint(streaming.content);
|
|
357
|
+
if (splitIndex > 0) {
|
|
358
|
+
splitMessageAtSafePointRef.current(splitIndex);
|
|
359
|
+
// Update local tracking to reflect the split
|
|
360
|
+
streaming.content = streaming.content.slice(splitIndex);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
[enableMessageSplitting, flushPendingToolCalls, isContinuationAfterToolGroup]
|
|
366
|
+
); // Depends on enableMessageSplitting flag
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Handle message/complete events from AgentLoop
|
|
370
|
+
* Maps to: commitPendingMessage (move pending to history for Static rendering)
|
|
371
|
+
*
|
|
372
|
+
* When streaming completes, the pending message is committed to history.
|
|
373
|
+
* This moves it to Ink's <Static> component where it will never re-render.
|
|
374
|
+
*/
|
|
375
|
+
const handleComplete = useCallback(() => {
|
|
376
|
+
const streaming = streamingMessageRef.current;
|
|
377
|
+
|
|
378
|
+
if (streaming) {
|
|
379
|
+
// NOTE: Do NOT copy thinking to content - they are separate concerns.
|
|
380
|
+
// Thinking content should stay in the thinking field and be rendered
|
|
381
|
+
// by the ThinkingBlock component, not mixed with regular content.
|
|
382
|
+
// Commit the pending message to history (moves to <Static>)
|
|
383
|
+
// This is more efficient than just marking isStreaming: false
|
|
384
|
+
// because the message will never re-render once in <Static>
|
|
385
|
+
commitPendingMessageRef.current();
|
|
386
|
+
streamingMessageRef.current = null;
|
|
387
|
+
}
|
|
388
|
+
pendingToolCallsRef.current.clear();
|
|
389
|
+
}, []); // Empty deps = stable callback
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Handle error events from AgentLoop
|
|
393
|
+
* Ensures streaming state is properly reset when an error occurs.
|
|
394
|
+
*/
|
|
395
|
+
const handleError = useCallback((error: Error) => {
|
|
396
|
+
const streaming = streamingMessageRef.current;
|
|
397
|
+
|
|
398
|
+
if (streaming) {
|
|
399
|
+
const content =
|
|
400
|
+
streaming.content.trim().length > 0
|
|
401
|
+
? streaming.content
|
|
402
|
+
: `${ICONS.warning} ${error.message}`;
|
|
403
|
+
// Mark the message as no longer streaming and surface the error
|
|
404
|
+
updateMessageRef.current(streaming.id, { isStreaming: false, content });
|
|
405
|
+
commitPendingMessageRef.current();
|
|
406
|
+
streamingMessageRef.current = null;
|
|
407
|
+
pendingToolCallsRef.current.clear();
|
|
408
|
+
} else {
|
|
409
|
+
addMessageRef.current({
|
|
410
|
+
role: "assistant",
|
|
411
|
+
content: `${ICONS.warning} ${error.message}`,
|
|
412
|
+
isStreaming: false,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
pendingToolCallsRef.current.clear();
|
|
416
|
+
|
|
417
|
+
// Log error for debugging (could also emit to a context/store if needed)
|
|
418
|
+
console.error("[AgentAdapter] AgentLoop error:", error.message);
|
|
419
|
+
}, []); // Empty deps = stable callback
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Commit the current streaming message before inserting tool rows.
|
|
423
|
+
* Keeps tool_group messages inline between assistant segments.
|
|
424
|
+
*/
|
|
425
|
+
const finalizeStreamingMessage = useCallback(() => {
|
|
426
|
+
const streaming = streamingMessageRef.current;
|
|
427
|
+
if (!streaming) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const hasContent = streaming.content.trim().length > 0;
|
|
432
|
+
const hasThinking = streaming.thinking.trim().length > 0;
|
|
433
|
+
if (hasContent || hasThinking) {
|
|
434
|
+
commitPendingMessageRef.current();
|
|
435
|
+
}
|
|
436
|
+
streamingMessageRef.current = null;
|
|
437
|
+
}, []);
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Create or update a tool_group message for inline tool display.
|
|
441
|
+
*/
|
|
442
|
+
const upsertToolGroup = useCallback(
|
|
443
|
+
(
|
|
444
|
+
callId: string,
|
|
445
|
+
name: string,
|
|
446
|
+
input: Record<string, unknown>,
|
|
447
|
+
status: "pending" | "running" | "completed" | "error",
|
|
448
|
+
result?: unknown,
|
|
449
|
+
error?: string
|
|
450
|
+
) => {
|
|
451
|
+
const toolCallInfo: ToolCallInfo = {
|
|
452
|
+
id: callId,
|
|
453
|
+
name,
|
|
454
|
+
arguments: input,
|
|
455
|
+
status,
|
|
456
|
+
result,
|
|
457
|
+
error,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const existingGroupId = toolGroupMapRef.current.get(callId);
|
|
461
|
+
if (!existingGroupId) {
|
|
462
|
+
finalizeStreamingMessage();
|
|
463
|
+
const groupId = addToolGroupRef.current([toolCallInfo]);
|
|
464
|
+
toolGroupMapRef.current.set(callId, groupId);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
updateToolGroupRef.current(existingGroupId, toolCallInfo);
|
|
469
|
+
},
|
|
470
|
+
[finalizeStreamingMessage]
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Sync a tool call to the streaming assistant message's toolCalls array.
|
|
475
|
+
* If no message is streaming yet, stash the call for the next assistant message.
|
|
476
|
+
*
|
|
477
|
+
* Tool calls are persisted on assistant messages; UI rendering uses tool_group rows.
|
|
478
|
+
*/
|
|
479
|
+
const syncToolCallToMessage = useCallback(
|
|
480
|
+
(
|
|
481
|
+
callId: string,
|
|
482
|
+
name: string,
|
|
483
|
+
input: Record<string, unknown>,
|
|
484
|
+
status: "pending" | "running" | "completed" | "error",
|
|
485
|
+
result?: unknown,
|
|
486
|
+
error?: string
|
|
487
|
+
) => {
|
|
488
|
+
const toolCallInfo: ToolCallInfo = {
|
|
489
|
+
id: callId,
|
|
490
|
+
name,
|
|
491
|
+
arguments: input,
|
|
492
|
+
status,
|
|
493
|
+
result,
|
|
494
|
+
error,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
if (!streamingMessageRef.current) {
|
|
498
|
+
upsertPendingToolCall(toolCallInfo);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Update existing message's toolCalls (merging handled by context)
|
|
503
|
+
updateMessageRef.current(streamingMessageRef.current.id, {
|
|
504
|
+
toolCalls: [toolCallInfo],
|
|
505
|
+
});
|
|
506
|
+
},
|
|
507
|
+
[upsertPendingToolCall]
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Handle tool start from AgentLoop
|
|
512
|
+
* Maps to: addExecution in ToolsContext + tool_group message
|
|
513
|
+
*/
|
|
514
|
+
const handleToolStart = useCallback(
|
|
515
|
+
(callId: string, name: string, input: Record<string, unknown>) => {
|
|
516
|
+
// Persist tool call info on assistant message and render inline tool row.
|
|
517
|
+
syncToolCallToMessage(callId, name, input, "running");
|
|
518
|
+
upsertToolGroup(callId, name, input, "running");
|
|
519
|
+
|
|
520
|
+
const existingExecutionId = toolIdMapRef.current.get(callId);
|
|
521
|
+
|
|
522
|
+
// If we already created a pending execution due to permissionRequired,
|
|
523
|
+
// treat toolStart as an update (not a new execution) to avoid duplicates.
|
|
524
|
+
if (existingExecutionId) {
|
|
525
|
+
updateExecutionRef.current(existingExecutionId, {
|
|
526
|
+
status: "running",
|
|
527
|
+
startedAt: new Date(),
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Add execution to tools context
|
|
533
|
+
const executionId = addExecutionRef.current({
|
|
534
|
+
toolName: name,
|
|
535
|
+
params: input,
|
|
536
|
+
status: "running",
|
|
537
|
+
startedAt: new Date(),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Map the AgentLoop callId to our execution ID
|
|
541
|
+
toolIdMapRef.current.set(callId, executionId);
|
|
542
|
+
registerCallIdRef.current(callId, executionId);
|
|
543
|
+
},
|
|
544
|
+
[syncToolCallToMessage, upsertToolGroup]
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Handle tool end from AgentLoop
|
|
549
|
+
* Maps to: updateExecution in ToolsContext + tool_group message update
|
|
550
|
+
*/
|
|
551
|
+
const handleToolEnd = useCallback(
|
|
552
|
+
(callId: string, name: string, result: ExecutionResult) => {
|
|
553
|
+
// Look up the execution ID from our map
|
|
554
|
+
const executionId = toolIdMapRef.current.get(callId);
|
|
555
|
+
|
|
556
|
+
if (executionId) {
|
|
557
|
+
// Update the execution with result
|
|
558
|
+
updateExecutionRef.current(executionId, {
|
|
559
|
+
status: result.result.success ? "complete" : "error",
|
|
560
|
+
result: result.result.success ? result.result.output : undefined,
|
|
561
|
+
error: !result.result.success ? new Error(String(result.result.error)) : undefined,
|
|
562
|
+
completedAt: new Date(),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Clean up the map entry
|
|
566
|
+
toolIdMapRef.current.delete(callId);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const isSuccess = result.result.success;
|
|
570
|
+
upsertToolGroup(
|
|
571
|
+
callId,
|
|
572
|
+
name,
|
|
573
|
+
{}, // Args not available here; merge keeps previous arguments if present.
|
|
574
|
+
isSuccess ? "completed" : "error",
|
|
575
|
+
isSuccess ? result.result.output : undefined,
|
|
576
|
+
!isSuccess ? String(result.result.error) : undefined
|
|
577
|
+
);
|
|
578
|
+
toolGroupMapRef.current.delete(callId);
|
|
579
|
+
},
|
|
580
|
+
[upsertToolGroup]
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Handle permission required events
|
|
585
|
+
* Maps to: addExecution with 'pending' status in ToolsContext
|
|
586
|
+
*/
|
|
587
|
+
const handlePermissionRequired = useCallback(
|
|
588
|
+
(callId: string, name: string, input: Record<string, unknown>) => {
|
|
589
|
+
// Persist tool call info and show inline pending tool row.
|
|
590
|
+
syncToolCallToMessage(callId, name, input, "pending");
|
|
591
|
+
upsertToolGroup(callId, name, input, "pending");
|
|
592
|
+
|
|
593
|
+
// Add execution in pending state (awaiting approval)
|
|
594
|
+
const executionId = addExecutionRef.current({
|
|
595
|
+
toolName: name,
|
|
596
|
+
params: input,
|
|
597
|
+
status: "pending",
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Map the callId to execution ID
|
|
601
|
+
toolIdMapRef.current.set(callId, executionId);
|
|
602
|
+
registerCallIdRef.current(callId, executionId);
|
|
603
|
+
},
|
|
604
|
+
[syncToolCallToMessage, upsertToolGroup]
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Handle permission granted events
|
|
609
|
+
* Maps to: updateExecution with 'running' status in ToolsContext
|
|
610
|
+
*/
|
|
611
|
+
const handlePermissionGranted = useCallback(
|
|
612
|
+
(callId: string, _name: string) => {
|
|
613
|
+
const executionId = toolIdMapRef.current.get(callId);
|
|
614
|
+
|
|
615
|
+
if (executionId) {
|
|
616
|
+
updateExecutionRef.current(executionId, {
|
|
617
|
+
status: "running",
|
|
618
|
+
startedAt: new Date(),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
upsertToolGroup(callId, _name, {}, "running");
|
|
623
|
+
},
|
|
624
|
+
[upsertToolGroup]
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Handle permission denied events
|
|
629
|
+
* Maps to: updateExecution with 'rejected' status in ToolsContext
|
|
630
|
+
*/
|
|
631
|
+
const handlePermissionDenied = useCallback(
|
|
632
|
+
(callId: string, _name: string, reason: string) => {
|
|
633
|
+
const executionId = toolIdMapRef.current.get(callId);
|
|
634
|
+
|
|
635
|
+
if (executionId) {
|
|
636
|
+
updateExecutionRef.current(executionId, {
|
|
637
|
+
status: "rejected",
|
|
638
|
+
error: new Error(reason),
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Clean up the map entry
|
|
642
|
+
toolIdMapRef.current.delete(callId);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
upsertToolGroup(callId, _name, {}, "error", undefined, reason);
|
|
646
|
+
toolGroupMapRef.current.delete(callId);
|
|
647
|
+
},
|
|
648
|
+
[upsertToolGroup]
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Connect to an AgentLoop instance
|
|
653
|
+
*
|
|
654
|
+
* Now has empty dependencies since all handlers are stable (using refs).
|
|
655
|
+
* This prevents unnecessary disconnect/reconnect cycles during re-renders.
|
|
656
|
+
*/
|
|
657
|
+
const connect = useCallback(
|
|
658
|
+
(agentLoop: AgentLoop) => {
|
|
659
|
+
// Skip reconnection if already connected to the same loop
|
|
660
|
+
// This prevents resetting streaming state during re-renders
|
|
661
|
+
if (connectedLoopRef.current === agentLoop) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Disconnect from any existing loop first
|
|
666
|
+
if (connectedLoopRef.current) {
|
|
667
|
+
// Remove existing event listeners
|
|
668
|
+
connectedLoopRef.current.off("text", handleText);
|
|
669
|
+
connectedLoopRef.current.off("thinking", handleThinking);
|
|
670
|
+
connectedLoopRef.current.off("complete", handleComplete);
|
|
671
|
+
connectedLoopRef.current.off("error", handleError);
|
|
672
|
+
connectedLoopRef.current.off("toolStart", handleToolStart);
|
|
673
|
+
connectedLoopRef.current.off("toolEnd", handleToolEnd);
|
|
674
|
+
connectedLoopRef.current.off("permissionRequired", handlePermissionRequired);
|
|
675
|
+
connectedLoopRef.current.off("permissionGranted", handlePermissionGranted);
|
|
676
|
+
connectedLoopRef.current.off("permissionDenied", handlePermissionDenied);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Reset state only when connecting to a NEW loop
|
|
680
|
+
streamingMessageRef.current = null;
|
|
681
|
+
pendingToolCallsRef.current.clear();
|
|
682
|
+
toolIdMapRef.current.clear();
|
|
683
|
+
toolGroupMapRef.current.clear();
|
|
684
|
+
|
|
685
|
+
// Subscribe to AgentLoop events
|
|
686
|
+
agentLoop.on("text", handleText);
|
|
687
|
+
agentLoop.on("thinking", handleThinking);
|
|
688
|
+
agentLoop.on("complete", handleComplete);
|
|
689
|
+
agentLoop.on("error", handleError);
|
|
690
|
+
agentLoop.on("toolStart", handleToolStart);
|
|
691
|
+
agentLoop.on("toolEnd", handleToolEnd);
|
|
692
|
+
agentLoop.on("permissionRequired", handlePermissionRequired);
|
|
693
|
+
agentLoop.on("permissionGranted", handlePermissionGranted);
|
|
694
|
+
agentLoop.on("permissionDenied", handlePermissionDenied);
|
|
695
|
+
|
|
696
|
+
// Store reference
|
|
697
|
+
connectedLoopRef.current = agentLoop;
|
|
698
|
+
isConnectedRef.current = true;
|
|
699
|
+
},
|
|
700
|
+
[
|
|
701
|
+
handleText,
|
|
702
|
+
handleThinking,
|
|
703
|
+
handleComplete,
|
|
704
|
+
handleError,
|
|
705
|
+
handleToolStart,
|
|
706
|
+
handleToolEnd,
|
|
707
|
+
handlePermissionRequired,
|
|
708
|
+
handlePermissionGranted,
|
|
709
|
+
handlePermissionDenied,
|
|
710
|
+
]
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Disconnect from the current AgentLoop
|
|
715
|
+
*
|
|
716
|
+
* Uses refs for clear functions to maintain callback stability.
|
|
717
|
+
*/
|
|
718
|
+
const disconnect = useCallback(() => {
|
|
719
|
+
if (connectedLoopRef.current) {
|
|
720
|
+
// Remove all event listeners
|
|
721
|
+
connectedLoopRef.current.off("text", handleText);
|
|
722
|
+
connectedLoopRef.current.off("thinking", handleThinking);
|
|
723
|
+
connectedLoopRef.current.off("complete", handleComplete);
|
|
724
|
+
connectedLoopRef.current.off("error", handleError);
|
|
725
|
+
connectedLoopRef.current.off("toolStart", handleToolStart);
|
|
726
|
+
connectedLoopRef.current.off("toolEnd", handleToolEnd);
|
|
727
|
+
connectedLoopRef.current.off("permissionRequired", handlePermissionRequired);
|
|
728
|
+
connectedLoopRef.current.off("permissionGranted", handlePermissionGranted);
|
|
729
|
+
connectedLoopRef.current.off("permissionDenied", handlePermissionDenied);
|
|
730
|
+
|
|
731
|
+
// Clear reference
|
|
732
|
+
connectedLoopRef.current = null;
|
|
733
|
+
isConnectedRef.current = false;
|
|
734
|
+
|
|
735
|
+
// Reset state
|
|
736
|
+
streamingMessageRef.current = null;
|
|
737
|
+
pendingToolCallsRef.current.clear();
|
|
738
|
+
toolIdMapRef.current.clear();
|
|
739
|
+
toolGroupMapRef.current.clear();
|
|
740
|
+
|
|
741
|
+
// Optionally clear contexts (using refs for stability)
|
|
742
|
+
if (clearOnDisconnect) {
|
|
743
|
+
clearMessagesRef.current();
|
|
744
|
+
clearExecutionsRef.current();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}, [
|
|
748
|
+
handleText,
|
|
749
|
+
handleThinking,
|
|
750
|
+
handleComplete,
|
|
751
|
+
handleError,
|
|
752
|
+
handleToolStart,
|
|
753
|
+
handleToolEnd,
|
|
754
|
+
handlePermissionRequired,
|
|
755
|
+
handlePermissionGranted,
|
|
756
|
+
handlePermissionDenied,
|
|
757
|
+
clearOnDisconnect,
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
// Cleanup on unmount
|
|
761
|
+
useEffect(() => {
|
|
762
|
+
return () => {
|
|
763
|
+
if (connectedLoopRef.current) {
|
|
764
|
+
disconnect();
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}, [disconnect]);
|
|
768
|
+
|
|
769
|
+
// Memoize return value to ensure stable object reference across renders.
|
|
770
|
+
// This prevents useEffect re-runs in consumers that depend on the adapter object,
|
|
771
|
+
// which was causing disconnect/reconnect cycles during streaming.
|
|
772
|
+
return useMemo(
|
|
773
|
+
() => ({
|
|
774
|
+
connect,
|
|
775
|
+
disconnect,
|
|
776
|
+
isConnected: isConnectedRef.current,
|
|
777
|
+
}),
|
|
778
|
+
[connect, disconnect]
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// =============================================================================
|
|
783
|
+
// Factory Function (Non-Hook Alternative)
|
|
784
|
+
// =============================================================================
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Context dispatch functions required by the adapter factory
|
|
788
|
+
*/
|
|
789
|
+
export interface AdapterDispatchers {
|
|
790
|
+
/** Add a message to the messages context */
|
|
791
|
+
addMessage: (message: { role: "assistant"; content: string; isStreaming?: boolean }) => string;
|
|
792
|
+
/** Append content to an existing message */
|
|
793
|
+
appendToMessage: (id: string, content: string) => void;
|
|
794
|
+
/** Update a message's properties */
|
|
795
|
+
updateMessage: (id: string, updates: Partial<{ content: string; isStreaming: boolean }>) => void;
|
|
796
|
+
/** Add a tool execution to the tools context */
|
|
797
|
+
addExecution: (execution: {
|
|
798
|
+
toolName: string;
|
|
799
|
+
params: Record<string, unknown>;
|
|
800
|
+
status?: "pending" | "approved" | "rejected" | "running" | "complete" | "error";
|
|
801
|
+
startedAt?: Date;
|
|
802
|
+
}) => string;
|
|
803
|
+
/** Update a tool execution */
|
|
804
|
+
updateExecution: (
|
|
805
|
+
id: string,
|
|
806
|
+
updates: {
|
|
807
|
+
status?: "pending" | "approved" | "rejected" | "running" | "complete" | "error";
|
|
808
|
+
result?: unknown;
|
|
809
|
+
error?: Error;
|
|
810
|
+
startedAt?: Date;
|
|
811
|
+
completedAt?: Date;
|
|
812
|
+
}
|
|
813
|
+
) => void;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Creates an agent adapter without using React hooks.
|
|
818
|
+
*
|
|
819
|
+
* Useful for testing or non-React environments.
|
|
820
|
+
*
|
|
821
|
+
* @param dispatchers - Context dispatch functions
|
|
822
|
+
* @returns An AgentAdapter interface
|
|
823
|
+
*
|
|
824
|
+
* @example
|
|
825
|
+
* ```typescript
|
|
826
|
+
* const dispatchers = {
|
|
827
|
+
* addMessage: (msg) => { ... },
|
|
828
|
+
* appendToMessage: (id, content) => { ... },
|
|
829
|
+
* updateMessage: (id, updates) => { ... },
|
|
830
|
+
* addExecution: (exec) => { ... },
|
|
831
|
+
* updateExecution: (id, updates) => { ... },
|
|
832
|
+
* };
|
|
833
|
+
*
|
|
834
|
+
* const adapter = createAgentAdapter(dispatchers);
|
|
835
|
+
* adapter.connect(agentLoop);
|
|
836
|
+
*
|
|
837
|
+
* // Later...
|
|
838
|
+
* adapter.disconnect();
|
|
839
|
+
* ```
|
|
840
|
+
*/
|
|
841
|
+
export function createAgentAdapter(dispatchers: AdapterDispatchers): AgentAdapter {
|
|
842
|
+
let connectedLoop: AgentLoop | null = null;
|
|
843
|
+
let streamingMessage: StreamingMessage | null = null;
|
|
844
|
+
const toolIdMap = new Map<string, string>();
|
|
845
|
+
|
|
846
|
+
const handleText = (text: string) => {
|
|
847
|
+
if (!streamingMessage) {
|
|
848
|
+
const id = dispatchers.addMessage({
|
|
849
|
+
role: "assistant",
|
|
850
|
+
content: text,
|
|
851
|
+
isStreaming: true,
|
|
852
|
+
});
|
|
853
|
+
streamingMessage = { id, content: text, thinking: "", hasStarted: true };
|
|
854
|
+
} else {
|
|
855
|
+
streamingMessage.content += text;
|
|
856
|
+
dispatchers.appendToMessage(streamingMessage.id, text);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const handleThinking = (text: string) => {
|
|
861
|
+
if (!streamingMessage) {
|
|
862
|
+
const id = dispatchers.addMessage({
|
|
863
|
+
role: "assistant",
|
|
864
|
+
content: "",
|
|
865
|
+
isStreaming: true,
|
|
866
|
+
});
|
|
867
|
+
streamingMessage = { id, content: "", thinking: "", hasStarted: true };
|
|
868
|
+
}
|
|
869
|
+
streamingMessage.thinking += text;
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
const handleComplete = () => {
|
|
873
|
+
if (streamingMessage) {
|
|
874
|
+
if (streamingMessage.content.trim().length === 0 && streamingMessage.thinking.trim()) {
|
|
875
|
+
dispatchers.updateMessage(streamingMessage.id, { content: streamingMessage.thinking });
|
|
876
|
+
}
|
|
877
|
+
dispatchers.updateMessage(streamingMessage.id, { isStreaming: false });
|
|
878
|
+
streamingMessage = null;
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const handleError = (error: Error) => {
|
|
883
|
+
if (streamingMessage) {
|
|
884
|
+
dispatchers.updateMessage(streamingMessage.id, { isStreaming: false });
|
|
885
|
+
streamingMessage = null;
|
|
886
|
+
}
|
|
887
|
+
console.error("[AgentAdapter] AgentLoop error:", error.message);
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const handleToolStart = (callId: string, name: string, input: Record<string, unknown>) => {
|
|
891
|
+
const executionId = dispatchers.addExecution({
|
|
892
|
+
toolName: name,
|
|
893
|
+
params: input,
|
|
894
|
+
status: "running",
|
|
895
|
+
startedAt: new Date(),
|
|
896
|
+
});
|
|
897
|
+
toolIdMap.set(callId, executionId);
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const handleToolEnd = (callId: string, _name: string, result: ExecutionResult) => {
|
|
901
|
+
const executionId = toolIdMap.get(callId);
|
|
902
|
+
if (executionId) {
|
|
903
|
+
dispatchers.updateExecution(executionId, {
|
|
904
|
+
status: result.result.success ? "complete" : "error",
|
|
905
|
+
result: result.result.success ? result.result.output : undefined,
|
|
906
|
+
error: !result.result.success ? new Error(String(result.result.error)) : undefined,
|
|
907
|
+
completedAt: new Date(),
|
|
908
|
+
});
|
|
909
|
+
toolIdMap.delete(callId);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
const handlePermissionRequired = (
|
|
914
|
+
callId: string,
|
|
915
|
+
name: string,
|
|
916
|
+
input: Record<string, unknown>
|
|
917
|
+
) => {
|
|
918
|
+
const executionId = dispatchers.addExecution({
|
|
919
|
+
toolName: name,
|
|
920
|
+
params: input,
|
|
921
|
+
status: "pending",
|
|
922
|
+
});
|
|
923
|
+
toolIdMap.set(callId, executionId);
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const handlePermissionGranted = (callId: string, _name: string) => {
|
|
927
|
+
const executionId = toolIdMap.get(callId);
|
|
928
|
+
if (executionId) {
|
|
929
|
+
dispatchers.updateExecution(executionId, {
|
|
930
|
+
status: "running",
|
|
931
|
+
startedAt: new Date(),
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
const handlePermissionDenied = (callId: string, _name: string, reason: string) => {
|
|
937
|
+
const executionId = toolIdMap.get(callId);
|
|
938
|
+
if (executionId) {
|
|
939
|
+
dispatchers.updateExecution(executionId, {
|
|
940
|
+
status: "rejected",
|
|
941
|
+
error: new Error(reason),
|
|
942
|
+
});
|
|
943
|
+
toolIdMap.delete(callId);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
connect(agentLoop: AgentLoop) {
|
|
949
|
+
// Skip reconnection if already connected to the same loop
|
|
950
|
+
if (connectedLoop === agentLoop) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Disconnect existing
|
|
955
|
+
if (connectedLoop) {
|
|
956
|
+
connectedLoop.off("text", handleText);
|
|
957
|
+
connectedLoop.off("thinking", handleThinking);
|
|
958
|
+
connectedLoop.off("complete", handleComplete);
|
|
959
|
+
connectedLoop.off("error", handleError);
|
|
960
|
+
connectedLoop.off("toolStart", handleToolStart);
|
|
961
|
+
connectedLoop.off("toolEnd", handleToolEnd);
|
|
962
|
+
connectedLoop.off("permissionRequired", handlePermissionRequired);
|
|
963
|
+
connectedLoop.off("permissionGranted", handlePermissionGranted);
|
|
964
|
+
connectedLoop.off("permissionDenied", handlePermissionDenied);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Reset state only when connecting to a NEW loop
|
|
968
|
+
streamingMessage = null;
|
|
969
|
+
toolIdMap.clear();
|
|
970
|
+
|
|
971
|
+
// Subscribe
|
|
972
|
+
agentLoop.on("text", handleText);
|
|
973
|
+
agentLoop.on("thinking", handleThinking);
|
|
974
|
+
agentLoop.on("complete", handleComplete);
|
|
975
|
+
agentLoop.on("error", handleError);
|
|
976
|
+
agentLoop.on("toolStart", handleToolStart);
|
|
977
|
+
agentLoop.on("toolEnd", handleToolEnd);
|
|
978
|
+
agentLoop.on("permissionRequired", handlePermissionRequired);
|
|
979
|
+
agentLoop.on("permissionGranted", handlePermissionGranted);
|
|
980
|
+
agentLoop.on("permissionDenied", handlePermissionDenied);
|
|
981
|
+
|
|
982
|
+
connectedLoop = agentLoop;
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
disconnect() {
|
|
986
|
+
if (connectedLoop) {
|
|
987
|
+
connectedLoop.off("text", handleText);
|
|
988
|
+
connectedLoop.off("thinking", handleThinking);
|
|
989
|
+
connectedLoop.off("complete", handleComplete);
|
|
990
|
+
connectedLoop.off("error", handleError);
|
|
991
|
+
connectedLoop.off("toolStart", handleToolStart);
|
|
992
|
+
connectedLoop.off("toolEnd", handleToolEnd);
|
|
993
|
+
connectedLoop.off("permissionRequired", handlePermissionRequired);
|
|
994
|
+
connectedLoop.off("permissionGranted", handlePermissionGranted);
|
|
995
|
+
connectedLoop.off("permissionDenied", handlePermissionDenied);
|
|
996
|
+
connectedLoop = null;
|
|
997
|
+
}
|
|
998
|
+
streamingMessage = null;
|
|
999
|
+
toolIdMap.clear();
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// =============================================================================
|
|
1005
|
+
// Exports
|
|
1006
|
+
// =============================================================================
|
|
1007
|
+
|
|
1008
|
+
export default useAgentAdapter;
|