@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
package/src/app.tsx
ADDED
|
@@ -0,0 +1,4737 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
AgentLoop,
|
|
5
|
+
ApprovalPolicy,
|
|
6
|
+
CodingMode,
|
|
7
|
+
EnterpriseHooks as CoreEnterpriseHooks,
|
|
8
|
+
CredentialManager,
|
|
9
|
+
EnterpriseToolCallInfo,
|
|
10
|
+
SandboxPolicy,
|
|
11
|
+
Session,
|
|
12
|
+
SessionMode,
|
|
13
|
+
TaskChain,
|
|
14
|
+
TaskChainNode,
|
|
15
|
+
ToolExecutor,
|
|
16
|
+
ToolRegistry,
|
|
17
|
+
} from "@vellum/core";
|
|
18
|
+
import {
|
|
19
|
+
BUILTIN_CODING_MODES,
|
|
20
|
+
OnboardingWizard as CoreOnboardingWizard,
|
|
21
|
+
createCostService,
|
|
22
|
+
createModeManager,
|
|
23
|
+
createSession,
|
|
24
|
+
createToolRegistry,
|
|
25
|
+
createUserMessage,
|
|
26
|
+
getTextContent,
|
|
27
|
+
ProjectMemoryService,
|
|
28
|
+
registerAllBuiltinTools,
|
|
29
|
+
registerGitTools,
|
|
30
|
+
SearchService,
|
|
31
|
+
SessionListService,
|
|
32
|
+
SessionParts,
|
|
33
|
+
StorageManager,
|
|
34
|
+
setBatchToolRegistry,
|
|
35
|
+
setTuiModeActive,
|
|
36
|
+
updateSessionMetadata,
|
|
37
|
+
} from "@vellum/core";
|
|
38
|
+
import { createId } from "@vellum/shared";
|
|
39
|
+
import { Box, Text, useApp as useInkApp, useInput, useStdout } from "ink";
|
|
40
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
41
|
+
import type { DefaultContextProvider } from "./commands/index.js";
|
|
42
|
+
import {
|
|
43
|
+
agentsCommand,
|
|
44
|
+
CommandExecutor,
|
|
45
|
+
CommandRegistry,
|
|
46
|
+
clearCommand,
|
|
47
|
+
condenseCommand,
|
|
48
|
+
configSlashCommands,
|
|
49
|
+
costCommand,
|
|
50
|
+
costResetCommand,
|
|
51
|
+
createBatchCommand,
|
|
52
|
+
createContextProvider,
|
|
53
|
+
createCredentialManager,
|
|
54
|
+
createResumeCommand,
|
|
55
|
+
createSearchCommand,
|
|
56
|
+
customAgentsCommand,
|
|
57
|
+
diffModeSlashCommands,
|
|
58
|
+
enhancedAuthCommands,
|
|
59
|
+
exitCommand,
|
|
60
|
+
getEffectiveThinkingConfig,
|
|
61
|
+
getThinkingState,
|
|
62
|
+
helpCommand,
|
|
63
|
+
initSlashCommand,
|
|
64
|
+
languageCommand,
|
|
65
|
+
memoryCommand,
|
|
66
|
+
metricsCommands,
|
|
67
|
+
modelCommand,
|
|
68
|
+
onboardCommand,
|
|
69
|
+
persistenceCommands,
|
|
70
|
+
promptPrioritySlashCommands,
|
|
71
|
+
type ResumeSessionEventData,
|
|
72
|
+
registerUserCommands,
|
|
73
|
+
setCondenseCommandLoop,
|
|
74
|
+
setCostCommandsService,
|
|
75
|
+
setHelpRegistry,
|
|
76
|
+
setModeCommandsManager,
|
|
77
|
+
setModelCommandConfig,
|
|
78
|
+
setPersistenceRef,
|
|
79
|
+
setThemeContext,
|
|
80
|
+
settingsSlashCommands,
|
|
81
|
+
setVimCallbacks,
|
|
82
|
+
subscribeToThinkingState,
|
|
83
|
+
themeSlashCommands,
|
|
84
|
+
thinkSlashCommands,
|
|
85
|
+
toggleThinking,
|
|
86
|
+
tutorialCommand,
|
|
87
|
+
vimSlashCommands,
|
|
88
|
+
} from "./commands/index.js";
|
|
89
|
+
import { modeSlashCommands } from "./commands/mode.js";
|
|
90
|
+
import type { AsyncOperation, CommandResult, InteractivePrompt } from "./commands/types.js";
|
|
91
|
+
import { setShutdownCleanup } from "./shutdown.js";
|
|
92
|
+
import { useAgentAdapter } from "./tui/adapters/agent-adapter.js";
|
|
93
|
+
import { toUIMessages } from "./tui/adapters/message-adapter.js";
|
|
94
|
+
import {
|
|
95
|
+
createMemorySessionStorage,
|
|
96
|
+
type SessionStorage,
|
|
97
|
+
useSessionAdapter,
|
|
98
|
+
} from "./tui/adapters/session-adapter.js";
|
|
99
|
+
import { AgentProgress } from "./tui/components/AgentProgress.js";
|
|
100
|
+
import { Banner } from "./tui/components/Banner/index.js";
|
|
101
|
+
import { BacktrackControls } from "./tui/components/backtrack/BacktrackControls.js";
|
|
102
|
+
import { CheckpointDiffView } from "./tui/components/Checkpoint/CheckpointDiffView.js";
|
|
103
|
+
import { SnapshotCheckpointPanel } from "./tui/components/Checkpoint/SnapshotCheckpointPanel.js";
|
|
104
|
+
import { CostDisplay } from "./tui/components/CostDisplay.js";
|
|
105
|
+
// New status components (Phase 35+)
|
|
106
|
+
import { AutoApprovalStatus } from "./tui/components/common/AutoApprovalStatus.js";
|
|
107
|
+
import { CostWarning } from "./tui/components/common/CostWarning.js";
|
|
108
|
+
import { ErrorBoundary } from "./tui/components/common/ErrorBoundary.js";
|
|
109
|
+
import { DEFAULT_HOTKEYS, HotkeyHelpModal } from "./tui/components/common/HotkeyHelpModal.js";
|
|
110
|
+
import { MaxSizedBox } from "./tui/components/common/MaxSizedBox.js";
|
|
111
|
+
import { LoadingIndicator } from "./tui/components/common/Spinner.js";
|
|
112
|
+
import type { AutocompleteOption } from "./tui/components/Input/Autocomplete.js";
|
|
113
|
+
import { EnhancedCommandInput } from "./tui/components/Input/EnhancedCommandInput.js";
|
|
114
|
+
import type { SlashCommand } from "./tui/components/Input/slash-command-utils.js";
|
|
115
|
+
import { TextInput } from "./tui/components/Input/TextInput.js";
|
|
116
|
+
import { InitErrorBanner, McpPanel } from "./tui/components/index.js";
|
|
117
|
+
import { Layout } from "./tui/components/Layout.js";
|
|
118
|
+
import { MemoryPanel, type MemoryPanelProps } from "./tui/components/MemoryPanel.js";
|
|
119
|
+
import { MessageList } from "./tui/components/Messages/MessageList.js";
|
|
120
|
+
import { ModeIndicator } from "./tui/components/ModeIndicator.js";
|
|
121
|
+
import { ModelSelector } from "./tui/components/ModelSelector.js";
|
|
122
|
+
import { ModeSelector } from "./tui/components/ModeSelector.js";
|
|
123
|
+
import { OnboardingWizard } from "./tui/components/OnboardingWizard.js";
|
|
124
|
+
import { PhaseProgressIndicator } from "./tui/components/PhaseProgressIndicator.js";
|
|
125
|
+
import { AdaptiveLayout } from "./tui/components/ScreenReaderLayout.js";
|
|
126
|
+
import { SystemStatusPanel } from "./tui/components/Sidebar/SystemStatusPanel.js";
|
|
127
|
+
import { ModelStatusBar } from "./tui/components/Status/ModelStatusBar.js";
|
|
128
|
+
import { FileChangesIndicator } from "./tui/components/StatusBar/FileChangesIndicator.js";
|
|
129
|
+
import { StatusBar } from "./tui/components/StatusBar/StatusBar.js";
|
|
130
|
+
import type { TrustMode } from "./tui/components/StatusBar/TrustModeIndicator.js";
|
|
131
|
+
import { SessionPicker } from "./tui/components/session/SessionPicker.js";
|
|
132
|
+
// Note: ProtectedFileLegend is rendered by tool output formatters, not app.tsx directly
|
|
133
|
+
import type { SessionMetadata, SessionPreviewMessage } from "./tui/components/session/types.js";
|
|
134
|
+
import { TipBanner } from "./tui/components/TipBanner.js";
|
|
135
|
+
import type { TodoItemData } from "./tui/components/TodoItem.js";
|
|
136
|
+
import { TodoPanel } from "./tui/components/TodoPanel.js";
|
|
137
|
+
import { ApprovalQueue } from "./tui/components/Tools/ApprovalQueue.js";
|
|
138
|
+
import { OptionSelector } from "./tui/components/Tools/OptionSelector.js";
|
|
139
|
+
import { PermissionDialog } from "./tui/components/Tools/PermissionDialog.js";
|
|
140
|
+
import { ToolsPanel } from "./tui/components/Tools/ToolsPanel.js";
|
|
141
|
+
import { UpdateBanner } from "./tui/components/UpdateBanner.js";
|
|
142
|
+
import { VimModeIndicator } from "./tui/components/VimModeIndicator.js";
|
|
143
|
+
import type { Message } from "./tui/context/MessagesContext.js";
|
|
144
|
+
import { useMessages } from "./tui/context/MessagesContext.js";
|
|
145
|
+
import { RootProvider } from "./tui/context/RootProvider.js";
|
|
146
|
+
import { type ToolExecution, useTools } from "./tui/context/ToolsContext.js";
|
|
147
|
+
import {
|
|
148
|
+
type PersistenceStatus,
|
|
149
|
+
usePersistence,
|
|
150
|
+
usePersistenceShortcuts,
|
|
151
|
+
} from "./tui/hooks/index.js";
|
|
152
|
+
import { useAlternateBuffer } from "./tui/hooks/useAlternateBuffer.js";
|
|
153
|
+
import { useBacktrack } from "./tui/hooks/useBacktrack.js";
|
|
154
|
+
import { useCopyMode } from "./tui/hooks/useCopyMode.js";
|
|
155
|
+
import { useDesktopNotification } from "./tui/hooks/useDesktopNotification.js";
|
|
156
|
+
import { useFileChangeStats } from "./tui/hooks/useFileChangeStats.js";
|
|
157
|
+
import { useGitStatus } from "./tui/hooks/useGitStatus.js";
|
|
158
|
+
import { type HotkeyDefinition, useHotkeys } from "./tui/hooks/useHotkeys.js";
|
|
159
|
+
import { useInputHistory } from "./tui/hooks/useInputHistory.js";
|
|
160
|
+
import { useModeShortcuts } from "./tui/hooks/useModeShortcuts.js";
|
|
161
|
+
import { useProviderStatus } from "./tui/hooks/useProviderStatus.js";
|
|
162
|
+
import { isScreenReaderEnabled, useScreenReader } from "./tui/hooks/useScreenReader.js";
|
|
163
|
+
import { type SidebarContent, useSidebarPanelData } from "./tui/hooks/useSidebarPanelData.js";
|
|
164
|
+
import { useSnapshots } from "./tui/hooks/useSnapshots.js";
|
|
165
|
+
import { useToolApprovalController } from "./tui/hooks/useToolApprovalController.js";
|
|
166
|
+
import type { VimMode } from "./tui/hooks/useVim.js";
|
|
167
|
+
import { useVim } from "./tui/hooks/useVim.js";
|
|
168
|
+
import { useWorkspace } from "./tui/hooks/useWorkspace.js";
|
|
169
|
+
import {
|
|
170
|
+
getAlternateBufferEnabled,
|
|
171
|
+
getBannerSeen,
|
|
172
|
+
setBannerSeen as saveBannerSeen,
|
|
173
|
+
} from "./tui/i18n/settings-integration.js";
|
|
174
|
+
import {
|
|
175
|
+
disposeLsp,
|
|
176
|
+
initializeLsp,
|
|
177
|
+
type LspIntegrationOptions,
|
|
178
|
+
type LspIntegrationResult,
|
|
179
|
+
} from "./tui/lsp-integration.js";
|
|
180
|
+
import {
|
|
181
|
+
disposePlugins,
|
|
182
|
+
getPluginCommands,
|
|
183
|
+
initializePlugins,
|
|
184
|
+
type PluginInitResult,
|
|
185
|
+
} from "./tui/plugins.js";
|
|
186
|
+
|
|
187
|
+
// =============================================================================
|
|
188
|
+
// Feature Integrations-
|
|
189
|
+
// =============================================================================
|
|
190
|
+
|
|
191
|
+
import { getProviderModels } from "@vellum/provider";
|
|
192
|
+
// Enterprise integration
|
|
193
|
+
import {
|
|
194
|
+
createEnterpriseHooks,
|
|
195
|
+
type EnterpriseHooks,
|
|
196
|
+
initializeEnterprise,
|
|
197
|
+
shutdownEnterprise,
|
|
198
|
+
} from "./tui/enterprise-integration.js";
|
|
199
|
+
// Metrics integration
|
|
200
|
+
import { getMetricsManager, type TuiMetricsManager } from "./tui/metrics-integration.js";
|
|
201
|
+
// Resilience integration-
|
|
202
|
+
import { createResilientProvider, type ResilientProvider } from "./tui/resilience.js";
|
|
203
|
+
// Sandbox integration
|
|
204
|
+
import { cleanupSandbox, initializeSandbox } from "./tui/sandbox-integration.js";
|
|
205
|
+
import { type ThemeName, useTheme } from "./tui/theme/index.js";
|
|
206
|
+
// Tip integration
|
|
207
|
+
import { buildTipContext, useTipEngine } from "./tui/tip-integration.js";
|
|
208
|
+
// Cursor management utilities
|
|
209
|
+
import { CursorManager } from "./tui/utils/cursor-manager.js";
|
|
210
|
+
import { calculateCost, getContextWindow, getModelInfo } from "./utils/index.js";
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get default model for a given provider
|
|
214
|
+
*/
|
|
215
|
+
function getDefaultModelForProvider(provider: string): string {
|
|
216
|
+
const defaults: Record<string, string> = {
|
|
217
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
218
|
+
openai: "gpt-4o",
|
|
219
|
+
google: "gemini-2.0-flash",
|
|
220
|
+
"azure-openai": "gpt-4o",
|
|
221
|
+
gemini: "gemini-2.0-flash",
|
|
222
|
+
"vertex-ai": "gemini-2.0-flash",
|
|
223
|
+
cohere: "command-r-plus",
|
|
224
|
+
mistral: "mistral-large-latest",
|
|
225
|
+
groq: "llama-3.3-70b-versatile",
|
|
226
|
+
fireworks: "accounts/fireworks/models/llama-v3p1-70b-instruct",
|
|
227
|
+
together: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
|
228
|
+
perplexity: "llama-3.1-sonar-large-128k-online",
|
|
229
|
+
bedrock: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
230
|
+
ollama: "llama3.2",
|
|
231
|
+
openrouter: "anthropic/claude-3.5-sonnet",
|
|
232
|
+
deepseek: "deepseek-chat",
|
|
233
|
+
qwen: "qwen-max",
|
|
234
|
+
moonshot: "moonshot-v1-8k",
|
|
235
|
+
};
|
|
236
|
+
return defaults[provider] ?? "claude-sonnet-4-20250514";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function approvalPolicyToTrustMode(policy: ApprovalPolicy): TrustMode {
|
|
240
|
+
switch (policy) {
|
|
241
|
+
case "suggest":
|
|
242
|
+
return "ask";
|
|
243
|
+
case "auto-edit":
|
|
244
|
+
case "on-request":
|
|
245
|
+
return "auto";
|
|
246
|
+
case "full-auto":
|
|
247
|
+
return "full";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getDefaultApprovalPolicyForMode(mode: CodingMode): ApprovalPolicy {
|
|
252
|
+
switch (mode) {
|
|
253
|
+
case "vibe":
|
|
254
|
+
return "full-auto";
|
|
255
|
+
case "plan":
|
|
256
|
+
return "auto-edit";
|
|
257
|
+
case "spec":
|
|
258
|
+
return "suggest";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Props for the App component.
|
|
264
|
+
* Extended with coding mode options-.
|
|
265
|
+
*/
|
|
266
|
+
interface AppProps {
|
|
267
|
+
/** Model to use for AI responses */
|
|
268
|
+
model: string;
|
|
269
|
+
/** Provider to use (anthropic, openai, etc.) */
|
|
270
|
+
provider: string;
|
|
271
|
+
/** Initial coding mode */
|
|
272
|
+
mode?: CodingMode;
|
|
273
|
+
/** Approval policy override */
|
|
274
|
+
approval?: ApprovalPolicy;
|
|
275
|
+
/** Sandbox policy override */
|
|
276
|
+
sandbox?: SandboxPolicy;
|
|
277
|
+
/** Optional AgentLoop instance for real agent integration */
|
|
278
|
+
agentLoop?: AgentLoop;
|
|
279
|
+
/** Optional shared ToolRegistry for the running tool system (avoids internal registry duplication) */
|
|
280
|
+
toolRegistry?: ToolRegistry;
|
|
281
|
+
/** Optional shared ToolExecutor for executing tools (defaults to AgentLoop's executor when available) */
|
|
282
|
+
toolExecutor?: ToolExecutor;
|
|
283
|
+
/** UI theme (dark, parchment, dracula, etc.) */
|
|
284
|
+
theme?: ThemeName;
|
|
285
|
+
/** Force banner display on startup */
|
|
286
|
+
banner?: boolean;
|
|
287
|
+
/** Initialization error (when provider fails to initialize) */
|
|
288
|
+
initError?: Error;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type AppContentProps = AppProps & {
|
|
292
|
+
readonly toolRegistry: ToolRegistry;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Cancellation controller for the current agent operation.
|
|
297
|
+
* Used to wire Ctrl+C and ESC to cancel running operations.
|
|
298
|
+
*/
|
|
299
|
+
interface CancellationController {
|
|
300
|
+
cancel: (reason?: string) => void;
|
|
301
|
+
isCancelled: boolean;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Map coding mode to session mode for persistence.
|
|
306
|
+
*/
|
|
307
|
+
function mapCodingModeToSessionMode(mode: CodingMode): SessionMode {
|
|
308
|
+
switch (mode) {
|
|
309
|
+
case "vibe":
|
|
310
|
+
return "code";
|
|
311
|
+
case "plan":
|
|
312
|
+
return "plan";
|
|
313
|
+
case "spec":
|
|
314
|
+
return "plan";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Derive a session title from messages.
|
|
320
|
+
*/
|
|
321
|
+
type SessionMessage = Session["messages"][number];
|
|
322
|
+
|
|
323
|
+
function buildSessionTitle(messages: readonly SessionMessage[]): string {
|
|
324
|
+
const firstUser = messages.find((message) => message.role === "user");
|
|
325
|
+
const content = firstUser ? getTextContent(firstUser).trim() : "";
|
|
326
|
+
if (!content) {
|
|
327
|
+
return "New Session";
|
|
328
|
+
}
|
|
329
|
+
return content.length > 60 ? `${content.slice(0, 57)}...` : content;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Derive a summary/preview from messages.
|
|
334
|
+
*/
|
|
335
|
+
function buildSessionSummary(messages: readonly SessionMessage[]): string | undefined {
|
|
336
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
337
|
+
const message = messages[i];
|
|
338
|
+
if (!message) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const content = getTextContent(message).trim();
|
|
342
|
+
if (content) {
|
|
343
|
+
return content.length > 140 ? `${content.slice(0, 137)}...` : content;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// =============================================================================
|
|
350
|
+
// Task 2: Focus Debug Component
|
|
351
|
+
// =============================================================================
|
|
352
|
+
|
|
353
|
+
interface FocusDebuggerProps {
|
|
354
|
+
isLoading: boolean;
|
|
355
|
+
showModeSelector: boolean;
|
|
356
|
+
showModelSelector: boolean;
|
|
357
|
+
showSessionManager: boolean;
|
|
358
|
+
showHelpModal: boolean;
|
|
359
|
+
activeApproval: unknown;
|
|
360
|
+
interactivePrompt: unknown;
|
|
361
|
+
pendingOperation: unknown;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Debug component that logs focus conditions when they change.
|
|
366
|
+
* Helps diagnose input focus issues.
|
|
367
|
+
*/
|
|
368
|
+
function FocusDebugger({
|
|
369
|
+
isLoading,
|
|
370
|
+
showModeSelector,
|
|
371
|
+
showModelSelector,
|
|
372
|
+
showSessionManager,
|
|
373
|
+
showHelpModal,
|
|
374
|
+
activeApproval,
|
|
375
|
+
interactivePrompt,
|
|
376
|
+
pendingOperation,
|
|
377
|
+
}: FocusDebuggerProps): null {
|
|
378
|
+
const shouldFocus =
|
|
379
|
+
!isLoading &&
|
|
380
|
+
!showModeSelector &&
|
|
381
|
+
!showModelSelector &&
|
|
382
|
+
!showSessionManager &&
|
|
383
|
+
!showHelpModal &&
|
|
384
|
+
!activeApproval &&
|
|
385
|
+
!interactivePrompt &&
|
|
386
|
+
!pendingOperation;
|
|
387
|
+
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
console.log("[Focus Debug]", {
|
|
390
|
+
isLoading,
|
|
391
|
+
showModeSelector,
|
|
392
|
+
showModelSelector,
|
|
393
|
+
showSessionManager,
|
|
394
|
+
showHelpModal,
|
|
395
|
+
activeApproval: !!activeApproval,
|
|
396
|
+
interactivePrompt: !!interactivePrompt,
|
|
397
|
+
pendingOperation: !!pendingOperation,
|
|
398
|
+
shouldFocus,
|
|
399
|
+
});
|
|
400
|
+
}, [
|
|
401
|
+
shouldFocus,
|
|
402
|
+
isLoading,
|
|
403
|
+
showModeSelector,
|
|
404
|
+
showModelSelector,
|
|
405
|
+
showSessionManager,
|
|
406
|
+
showHelpModal,
|
|
407
|
+
activeApproval,
|
|
408
|
+
interactivePrompt,
|
|
409
|
+
pendingOperation,
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// =============================================================================
|
|
416
|
+
//: Command Registry Initialization
|
|
417
|
+
// =============================================================================
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create and initialize the command registry with all builtin commands
|
|
421
|
+
*/
|
|
422
|
+
function createCommandRegistry(): CommandRegistry {
|
|
423
|
+
const registry = new CommandRegistry();
|
|
424
|
+
|
|
425
|
+
// Register core system commands
|
|
426
|
+
registry.register(helpCommand);
|
|
427
|
+
registry.register(clearCommand);
|
|
428
|
+
registry.register(exitCommand);
|
|
429
|
+
|
|
430
|
+
// Register additional builtin commands
|
|
431
|
+
registry.register(languageCommand);
|
|
432
|
+
registry.register(modelCommand);
|
|
433
|
+
registry.register(costCommand);
|
|
434
|
+
registry.register(costResetCommand);
|
|
435
|
+
registry.register(initSlashCommand);
|
|
436
|
+
registry.register(onboardCommand);
|
|
437
|
+
registry.register(agentsCommand);
|
|
438
|
+
registry.register(customAgentsCommand);
|
|
439
|
+
|
|
440
|
+
// Register memory dispatcher (subcommands handled via /memory)
|
|
441
|
+
registry.register(memoryCommand);
|
|
442
|
+
|
|
443
|
+
// Register tutorial command (Phase 38)
|
|
444
|
+
registry.register(tutorialCommand);
|
|
445
|
+
|
|
446
|
+
// Register auth commands
|
|
447
|
+
for (const cmd of enhancedAuthCommands) {
|
|
448
|
+
registry.register(cmd);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
//: Register mode slash commands
|
|
452
|
+
for (const cmd of modeSlashCommands) {
|
|
453
|
+
registry.register(cmd);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
//: Register vim slash commands
|
|
457
|
+
for (const cmd of vimSlashCommands) {
|
|
458
|
+
registry.register(cmd);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Register think slash commands
|
|
462
|
+
for (const cmd of thinkSlashCommands) {
|
|
463
|
+
registry.register(cmd);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Register diff-mode slash commands
|
|
467
|
+
for (const cmd of diffModeSlashCommands) {
|
|
468
|
+
registry.register(cmd);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
//: Register context management command
|
|
472
|
+
registry.register(condenseCommand);
|
|
473
|
+
|
|
474
|
+
//: Register theme slash commands
|
|
475
|
+
for (const cmd of themeSlashCommands) {
|
|
476
|
+
registry.register(cmd);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
//: Register metrics commands
|
|
480
|
+
for (const cmd of metricsCommands) {
|
|
481
|
+
registry.register(cmd);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Register persistence commands
|
|
485
|
+
for (const cmd of persistenceCommands) {
|
|
486
|
+
registry.register(cmd);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Register settings system commands
|
|
490
|
+
for (const cmd of settingsSlashCommands) {
|
|
491
|
+
registry.register(cmd);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
for (const cmd of configSlashCommands) {
|
|
495
|
+
registry.register(cmd);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (const cmd of promptPrioritySlashCommands) {
|
|
499
|
+
registry.register(cmd);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
//: Plugin commands are registered via registerPluginCommands()
|
|
503
|
+
// after PluginManager initialization in AppContent
|
|
504
|
+
|
|
505
|
+
// Wire up help command to access registry
|
|
506
|
+
setHelpRegistry(registry);
|
|
507
|
+
|
|
508
|
+
return registry;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Registers plugin commands into the command registry.
|
|
513
|
+
* Called after plugin initialization completes.
|
|
514
|
+
*
|
|
515
|
+
* @param registry - Command registry to register commands into
|
|
516
|
+
* @param pluginResult - Result from plugin initialization
|
|
517
|
+
*/
|
|
518
|
+
function registerPluginCommands(registry: CommandRegistry, pluginResult: PluginInitResult): void {
|
|
519
|
+
const commands = getPluginCommands(pluginResult.manager);
|
|
520
|
+
for (const cmd of commands) {
|
|
521
|
+
try {
|
|
522
|
+
registry.register(cmd);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
// Log but don't fail on command conflicts
|
|
525
|
+
console.warn(`[plugin] Failed to register command '${cmd.name}':`, error);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function App({
|
|
531
|
+
model,
|
|
532
|
+
provider,
|
|
533
|
+
mode: _mode = "vibe",
|
|
534
|
+
approval: _approval,
|
|
535
|
+
sandbox: _sandbox,
|
|
536
|
+
agentLoop: agentLoopProp,
|
|
537
|
+
toolRegistry: toolRegistryProp,
|
|
538
|
+
toolExecutor: toolExecutorProp,
|
|
539
|
+
theme = "parchment",
|
|
540
|
+
banner,
|
|
541
|
+
initError,
|
|
542
|
+
}: AppProps) {
|
|
543
|
+
// Shared tool registry for the running tool system.
|
|
544
|
+
// This registry is used by commands, the tools UI, and MCP tool registration.
|
|
545
|
+
const toolRegistry = useMemo(() => {
|
|
546
|
+
if (toolRegistryProp) {
|
|
547
|
+
return toolRegistryProp;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const registry = createToolRegistry();
|
|
551
|
+
registerAllBuiltinTools(registry);
|
|
552
|
+
registerGitTools(registry);
|
|
553
|
+
setBatchToolRegistry(registry);
|
|
554
|
+
return registry;
|
|
555
|
+
}, [toolRegistryProp]);
|
|
556
|
+
|
|
557
|
+
// If an AgentLoop is provided, MCP tools must execute via the same ToolExecutor.
|
|
558
|
+
const toolExecutor: ToolExecutor | undefined = useMemo(() => {
|
|
559
|
+
if (toolExecutorProp) {
|
|
560
|
+
return toolExecutorProp;
|
|
561
|
+
}
|
|
562
|
+
return agentLoopProp?.getToolExecutor();
|
|
563
|
+
}, [agentLoopProp, toolExecutorProp]);
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<RootProvider theme={theme} toolRegistry={toolRegistry} toolExecutor={toolExecutor}>
|
|
567
|
+
<ErrorBoundary
|
|
568
|
+
onError={(error, errorInfo) => {
|
|
569
|
+
console.error("[ErrorBoundary] Caught error:", error, errorInfo);
|
|
570
|
+
}}
|
|
571
|
+
showDetails
|
|
572
|
+
>
|
|
573
|
+
<AppContent
|
|
574
|
+
model={model}
|
|
575
|
+
provider={provider}
|
|
576
|
+
mode={_mode}
|
|
577
|
+
approval={_approval}
|
|
578
|
+
sandbox={_sandbox}
|
|
579
|
+
agentLoop={agentLoopProp}
|
|
580
|
+
toolRegistry={toolRegistry}
|
|
581
|
+
banner={banner}
|
|
582
|
+
initError={initError}
|
|
583
|
+
/>
|
|
584
|
+
</ErrorBoundary>
|
|
585
|
+
</RootProvider>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Inner component that contains the actual app logic.
|
|
591
|
+
* Separated from App to access ThemeContext via useTheme().
|
|
592
|
+
*/
|
|
593
|
+
function AppContent({
|
|
594
|
+
model,
|
|
595
|
+
provider,
|
|
596
|
+
mode: _mode = "vibe",
|
|
597
|
+
approval: _approval,
|
|
598
|
+
sandbox: _sandbox,
|
|
599
|
+
agentLoop: agentLoopProp,
|
|
600
|
+
banner,
|
|
601
|
+
toolRegistry,
|
|
602
|
+
initError,
|
|
603
|
+
}: AppContentProps) {
|
|
604
|
+
const { exit } = useInkApp();
|
|
605
|
+
const themeContext = useTheme();
|
|
606
|
+
const { messages, addMessage, clearMessages, setMessages, pendingMessage } = useMessages();
|
|
607
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
608
|
+
const [interactivePrompt, setInteractivePrompt] = useState<InteractivePrompt | null>(null);
|
|
609
|
+
const [followupPrompt, setFollowupPrompt] = useState<{
|
|
610
|
+
question: string;
|
|
611
|
+
suggestions: string[];
|
|
612
|
+
} | null>(null);
|
|
613
|
+
const [promptValue, setPromptValue] = useState("");
|
|
614
|
+
const [pendingOperation, setPendingOperation] = useState<AsyncOperation | null>(null);
|
|
615
|
+
|
|
616
|
+
// Suppress initial Enter key event when interactive prompt is mounted (fixes race condition)
|
|
617
|
+
const [suppressPromptEnter, setSuppressPromptEnter] = useState(false);
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
if (interactivePrompt) {
|
|
620
|
+
setSuppressPromptEnter(true);
|
|
621
|
+
const timer = setTimeout(() => setSuppressPromptEnter(false), 50);
|
|
622
|
+
return () => clearTimeout(timer);
|
|
623
|
+
}
|
|
624
|
+
}, [interactivePrompt]);
|
|
625
|
+
|
|
626
|
+
// ==========================================================================
|
|
627
|
+
// New TUI Hooks Integration-
|
|
628
|
+
// ==========================================================================
|
|
629
|
+
|
|
630
|
+
// Vim modal editing mode
|
|
631
|
+
const [vimEnabled, setVimEnabled] = useState(false);
|
|
632
|
+
const vim = useVim();
|
|
633
|
+
|
|
634
|
+
// Wire up vim callbacks for /vim command
|
|
635
|
+
useEffect(() => {
|
|
636
|
+
const handleToggle = () => {
|
|
637
|
+
setVimEnabled((prev) => !prev);
|
|
638
|
+
vim.toggle();
|
|
639
|
+
};
|
|
640
|
+
const isEnabled = () => vimEnabled;
|
|
641
|
+
setVimCallbacks(handleToggle, isEnabled);
|
|
642
|
+
return () =>
|
|
643
|
+
setVimCallbacks(
|
|
644
|
+
() => {},
|
|
645
|
+
() => false
|
|
646
|
+
);
|
|
647
|
+
}, [vim, vimEnabled]);
|
|
648
|
+
|
|
649
|
+
// Copy mode for visual selection
|
|
650
|
+
const copyMode = useCopyMode();
|
|
651
|
+
|
|
652
|
+
// Desktop notifications
|
|
653
|
+
const {
|
|
654
|
+
notify: _notify,
|
|
655
|
+
notifyTaskComplete,
|
|
656
|
+
notifyError,
|
|
657
|
+
} = useDesktopNotification({ enabled: true });
|
|
658
|
+
|
|
659
|
+
// Workspace and git status for header separator
|
|
660
|
+
const { name: workspaceName } = useWorkspace();
|
|
661
|
+
const { branch: gitBranch, changedFiles: gitChangedFiles } = useGitStatus();
|
|
662
|
+
|
|
663
|
+
// Alternate buffer configuration
|
|
664
|
+
// Enabled by default (config defaults to true)
|
|
665
|
+
// Automatically disabled when screen reader is detected for accessibility
|
|
666
|
+
const { stdout } = useStdout();
|
|
667
|
+
const alternateBufferConfig = getAlternateBufferEnabled();
|
|
668
|
+
const screenReaderDetected = isScreenReaderEnabled();
|
|
669
|
+
// Enable alternate buffer in VS Code terminal to fix cursor flickering
|
|
670
|
+
const alternateBufferEnabled = alternateBufferConfig && !screenReaderDetected;
|
|
671
|
+
|
|
672
|
+
// Alternate buffer for full-screen rendering
|
|
673
|
+
// Benefits: Clean exit (restores original terminal), no scrollback pollution
|
|
674
|
+
const alternateBuffer = useAlternateBuffer({
|
|
675
|
+
enabled: alternateBufferEnabled,
|
|
676
|
+
});
|
|
677
|
+
// Destructure for convenience
|
|
678
|
+
const { isAlternate } = alternateBuffer;
|
|
679
|
+
|
|
680
|
+
// Terminal height for layout constraint when in alternate buffer mode
|
|
681
|
+
const terminalHeight = process.stdout.rows || 24;
|
|
682
|
+
void isAlternate; // Used for layout height calculation
|
|
683
|
+
void terminalHeight; // Used for layout height calculation
|
|
684
|
+
|
|
685
|
+
// Hide the terminal cursor to avoid VS Code's blinking block over the message area.
|
|
686
|
+
// We draw our own cursor in inputs and streaming text.
|
|
687
|
+
// Uses centralized CursorManager to prevent race conditions.
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
if (screenReaderDetected || !stdout.isTTY) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (process.env.VELLUM_SHOW_CURSOR === "1") {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Lock cursor in hidden state for entire TUI session
|
|
697
|
+
CursorManager.lock();
|
|
698
|
+
|
|
699
|
+
// Setup exit handlers to restore cursor
|
|
700
|
+
const handleExit = (): void => {
|
|
701
|
+
CursorManager.unlock();
|
|
702
|
+
CursorManager.forceShow();
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
process.on("exit", handleExit);
|
|
706
|
+
process.on("SIGINT", handleExit);
|
|
707
|
+
process.on("SIGTERM", handleExit);
|
|
708
|
+
process.on("SIGHUP", handleExit);
|
|
709
|
+
|
|
710
|
+
return () => {
|
|
711
|
+
process.off("exit", handleExit);
|
|
712
|
+
process.off("SIGINT", handleExit);
|
|
713
|
+
process.off("SIGTERM", handleExit);
|
|
714
|
+
process.off("SIGHUP", handleExit);
|
|
715
|
+
CursorManager.unlock();
|
|
716
|
+
CursorManager.forceShow();
|
|
717
|
+
};
|
|
718
|
+
}, [screenReaderDetected, stdout]);
|
|
719
|
+
|
|
720
|
+
// ==========================================================================
|
|
721
|
+
// TUI Mode Console Suppression (Overflow Prevention)
|
|
722
|
+
// ==========================================================================
|
|
723
|
+
// Enable TUI mode to suppress console.log output from loggers.
|
|
724
|
+
// This prevents console output from bypassing Ink and causing terminal overflow.
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
// Activate TUI mode to suppress console transport
|
|
727
|
+
setTuiModeActive(true);
|
|
728
|
+
|
|
729
|
+
// Setup exit handlers to restore console on exit
|
|
730
|
+
const restoreConsole = (): void => {
|
|
731
|
+
setTuiModeActive(false);
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// Standard exit signals
|
|
735
|
+
process.on("exit", restoreConsole);
|
|
736
|
+
process.on("SIGINT", restoreConsole);
|
|
737
|
+
process.on("SIGTERM", restoreConsole);
|
|
738
|
+
|
|
739
|
+
// Exception handlers for complete coverage Hardening)
|
|
740
|
+
// These ensure TUI mode is disabled even on unexpected crashes
|
|
741
|
+
process.on("uncaughtException", restoreConsole);
|
|
742
|
+
process.on("unhandledRejection", restoreConsole);
|
|
743
|
+
|
|
744
|
+
return () => {
|
|
745
|
+
process.off("exit", restoreConsole);
|
|
746
|
+
process.off("SIGINT", restoreConsole);
|
|
747
|
+
process.off("SIGTERM", restoreConsole);
|
|
748
|
+
process.off("uncaughtException", restoreConsole);
|
|
749
|
+
process.off("unhandledRejection", restoreConsole);
|
|
750
|
+
setTuiModeActive(false);
|
|
751
|
+
};
|
|
752
|
+
}, []);
|
|
753
|
+
|
|
754
|
+
// NOTE: Previous useInput and setInterval for cursor hiding removed.
|
|
755
|
+
// CursorManager.lock() now handles cursor state centrally, preventing
|
|
756
|
+
// race conditions and flickering from multiple cursor hide/show operations.
|
|
757
|
+
|
|
758
|
+
// ==========================================================================
|
|
759
|
+
// Feature Integrations-
|
|
760
|
+
// ==========================================================================
|
|
761
|
+
|
|
762
|
+
//: Sandbox integration for shell tool execution
|
|
763
|
+
const sandboxRef = useRef<ReturnType<typeof initializeSandbox> | null>(null);
|
|
764
|
+
useEffect(() => {
|
|
765
|
+
// Initialize sandbox on mount
|
|
766
|
+
sandboxRef.current = initializeSandbox({
|
|
767
|
+
workingDirectory: process.cwd(),
|
|
768
|
+
allowNetwork: false,
|
|
769
|
+
allowFileSystem: true,
|
|
770
|
+
timeoutMs: 30000,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
return () => {
|
|
774
|
+
// Cleanup sandbox on unmount
|
|
775
|
+
void cleanupSandbox();
|
|
776
|
+
};
|
|
777
|
+
}, []);
|
|
778
|
+
|
|
779
|
+
//: Resilience (circuit breaker, rate limiter, fallback)
|
|
780
|
+
const [resilientProvider, setResilientProvider] = useState<ResilientProvider | null>(null);
|
|
781
|
+
useEffect(() => {
|
|
782
|
+
// Create resilient provider wrapper
|
|
783
|
+
// In production, this would wrap actual provider clients
|
|
784
|
+
const resilient = createResilientProvider(
|
|
785
|
+
[
|
|
786
|
+
{
|
|
787
|
+
id: provider,
|
|
788
|
+
name: provider,
|
|
789
|
+
priority: 0,
|
|
790
|
+
execute: async <T,>(request: () => Promise<T>) => request(),
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
{
|
|
794
|
+
circuitBreaker: { failureThreshold: 5, resetTimeoutMs: 30000 },
|
|
795
|
+
rateLimiter: { defaultBucket: { capacity: 60, refillRate: 1 } },
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
setResilientProvider(resilient);
|
|
800
|
+
|
|
801
|
+
return () => {
|
|
802
|
+
resilient.dispose();
|
|
803
|
+
};
|
|
804
|
+
}, [provider]);
|
|
805
|
+
|
|
806
|
+
//: Metrics integration
|
|
807
|
+
const metricsManager = useMemo<TuiMetricsManager>(() => getMetricsManager(), []);
|
|
808
|
+
|
|
809
|
+
// Track message processing
|
|
810
|
+
useEffect(() => {
|
|
811
|
+
if (messages.length > 0) {
|
|
812
|
+
metricsManager.recordMessage();
|
|
813
|
+
}
|
|
814
|
+
}, [messages.length, metricsManager]);
|
|
815
|
+
|
|
816
|
+
//: Enterprise integration
|
|
817
|
+
const [enterpriseHooks, setEnterpriseHooks] = useState<EnterpriseHooks | null>(null);
|
|
818
|
+
useEffect(() => {
|
|
819
|
+
let cancelled = false;
|
|
820
|
+
|
|
821
|
+
const loadEnterprise = async () => {
|
|
822
|
+
const result = await initializeEnterprise();
|
|
823
|
+
if (!cancelled && result.enabled) {
|
|
824
|
+
setEnterpriseHooks(createEnterpriseHooks());
|
|
825
|
+
console.debug("[enterprise] Enterprise mode active");
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
void loadEnterprise();
|
|
830
|
+
|
|
831
|
+
return () => {
|
|
832
|
+
cancelled = true;
|
|
833
|
+
void shutdownEnterprise();
|
|
834
|
+
};
|
|
835
|
+
}, []);
|
|
836
|
+
|
|
837
|
+
//: Wire enterprise hooks to ToolExecutor when both are available
|
|
838
|
+
useEffect(() => {
|
|
839
|
+
if (!enterpriseHooks) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Get the tool executor from the agent loop
|
|
844
|
+
const toolExecutor = agentLoopProp?.getToolExecutor();
|
|
845
|
+
if (!toolExecutor) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Wire the hooks using the adapter interface (EnterpriseToolCallInfo → ToolCallInfo)
|
|
850
|
+
const coreHooks: CoreEnterpriseHooks = {
|
|
851
|
+
onBeforeToolCall: async (tool: EnterpriseToolCallInfo) => {
|
|
852
|
+
return enterpriseHooks.onBeforeToolCall({
|
|
853
|
+
serverName: tool.serverName ?? "vellum",
|
|
854
|
+
toolName: tool.toolName,
|
|
855
|
+
arguments: tool.arguments,
|
|
856
|
+
});
|
|
857
|
+
},
|
|
858
|
+
onAfterToolCall: async (
|
|
859
|
+
tool: EnterpriseToolCallInfo,
|
|
860
|
+
result: unknown,
|
|
861
|
+
durationMs: number
|
|
862
|
+
) => {
|
|
863
|
+
return enterpriseHooks.onAfterToolCall(
|
|
864
|
+
{
|
|
865
|
+
serverName: tool.serverName ?? "vellum",
|
|
866
|
+
toolName: tool.toolName,
|
|
867
|
+
arguments: tool.arguments,
|
|
868
|
+
},
|
|
869
|
+
result,
|
|
870
|
+
durationMs
|
|
871
|
+
);
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
toolExecutor.setEnterpriseHooks(coreHooks);
|
|
875
|
+
|
|
876
|
+
console.debug("[enterprise] Wired enterprise hooks to ToolExecutor");
|
|
877
|
+
|
|
878
|
+
return () => {
|
|
879
|
+
// Clear hooks on cleanup
|
|
880
|
+
toolExecutor.setEnterpriseHooks(null);
|
|
881
|
+
};
|
|
882
|
+
}, [enterpriseHooks, agentLoopProp]);
|
|
883
|
+
|
|
884
|
+
//: Tip engine integration
|
|
885
|
+
const { currentTip, showTip, dismissTip, tipsEnabled } = useTipEngine({
|
|
886
|
+
enabled: true,
|
|
887
|
+
maxTipsPerSession: 5,
|
|
888
|
+
tipIntervalMs: 60000,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
// Note: The tip context useEffect is placed after state declarations below
|
|
892
|
+
|
|
893
|
+
// ==========================================================================
|
|
894
|
+
// Adapter Integration - Agent Adapter
|
|
895
|
+
// ==========================================================================
|
|
896
|
+
|
|
897
|
+
// Agent adapter for AgentLoop ↔ Context integration
|
|
898
|
+
// The hook connects AgentLoop events to MessagesContext and ToolsContext
|
|
899
|
+
const agentAdapter = useAgentAdapter({
|
|
900
|
+
clearOnDisconnect: false, // Preserve messages when disconnecting
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Destructure for stable references in useEffect dependency array.
|
|
904
|
+
// Even though agentAdapter is now memoized, this makes the dependency explicit
|
|
905
|
+
// and avoids re-running the effect if agentAdapter reference changes.
|
|
906
|
+
const { connect: adapterConnect, disconnect: adapterDisconnect } = agentAdapter;
|
|
907
|
+
|
|
908
|
+
// Connect to AgentLoop when provided
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
if (agentLoopProp) {
|
|
911
|
+
adapterConnect(agentLoopProp);
|
|
912
|
+
// Wire up context management command
|
|
913
|
+
setCondenseCommandLoop(agentLoopProp);
|
|
914
|
+
// Thinking content is now handled directly in the agent-adapter
|
|
915
|
+
// and integrated into the streaming message's `thinking` field.
|
|
916
|
+
}
|
|
917
|
+
return () => {
|
|
918
|
+
adapterDisconnect();
|
|
919
|
+
// Clear context management command reference
|
|
920
|
+
setCondenseCommandLoop(null);
|
|
921
|
+
};
|
|
922
|
+
}, [agentLoopProp, adapterConnect, adapterDisconnect]);
|
|
923
|
+
|
|
924
|
+
const upsertTaskChainNode = useCallback(
|
|
925
|
+
(taskId: string, agentSlug: string | undefined, status: TaskChainNode["status"]) => {
|
|
926
|
+
setTaskChain((prev) => {
|
|
927
|
+
const now = new Date();
|
|
928
|
+
if (!prev) {
|
|
929
|
+
const rootNode: TaskChainNode = {
|
|
930
|
+
taskId,
|
|
931
|
+
parentTaskId: undefined,
|
|
932
|
+
agentSlug: agentSlug ?? "agent",
|
|
933
|
+
depth: 0,
|
|
934
|
+
createdAt: now,
|
|
935
|
+
status,
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
return {
|
|
939
|
+
chainId: `ui-${createId()}`,
|
|
940
|
+
rootTaskId: taskId,
|
|
941
|
+
nodes: new Map([[taskId, rootNode]]),
|
|
942
|
+
maxDepth: 0,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const nodes = new Map(prev.nodes);
|
|
947
|
+
const existing = nodes.get(taskId);
|
|
948
|
+
const node: TaskChainNode = {
|
|
949
|
+
taskId,
|
|
950
|
+
parentTaskId: existing?.parentTaskId,
|
|
951
|
+
agentSlug: agentSlug ?? existing?.agentSlug ?? "agent",
|
|
952
|
+
depth: existing?.depth ?? 0,
|
|
953
|
+
createdAt: existing?.createdAt ?? now,
|
|
954
|
+
status,
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
nodes.set(taskId, node);
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
...prev,
|
|
961
|
+
nodes,
|
|
962
|
+
rootTaskId: prev.rootTaskId ?? taskId,
|
|
963
|
+
maxDepth: prev.maxDepth ?? 0,
|
|
964
|
+
};
|
|
965
|
+
});
|
|
966
|
+
},
|
|
967
|
+
[]
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
useEffect(() => {
|
|
971
|
+
if (!agentLoopProp) {
|
|
972
|
+
setTaskChain(null);
|
|
973
|
+
setCurrentTaskId(undefined);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const handleDelegationStart = (delegationId: string, agent: string) => {
|
|
978
|
+
upsertTaskChainNode(delegationId, agent, "running");
|
|
979
|
+
setCurrentTaskId(delegationId);
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
const handleDelegationComplete = (delegationId: string) => {
|
|
983
|
+
upsertTaskChainNode(delegationId, undefined, "completed");
|
|
984
|
+
setCurrentTaskId((prev) => (prev === delegationId ? undefined : prev));
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
agentLoopProp.on("delegationStart", handleDelegationStart);
|
|
988
|
+
agentLoopProp.on("delegationComplete", handleDelegationComplete);
|
|
989
|
+
|
|
990
|
+
return () => {
|
|
991
|
+
agentLoopProp.off("delegationStart", handleDelegationStart);
|
|
992
|
+
agentLoopProp.off("delegationComplete", handleDelegationComplete);
|
|
993
|
+
};
|
|
994
|
+
}, [agentLoopProp, upsertTaskChainNode]);
|
|
995
|
+
|
|
996
|
+
useEffect(() => {
|
|
997
|
+
const orchestrator = agentLoopProp?.getConfig().orchestrator;
|
|
998
|
+
if (!orchestrator) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const handleSpawned = (event: { data?: { taskId?: string; agentSlug?: string } }) => {
|
|
1003
|
+
if (!event.data?.taskId) return;
|
|
1004
|
+
upsertTaskChainNode(event.data.taskId, event.data.agentSlug, "running");
|
|
1005
|
+
setCurrentTaskId(event.data.taskId);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
const handleCompleted = (event: { data?: { taskId?: string; agentSlug?: string } }) => {
|
|
1009
|
+
if (!event.data?.taskId) return;
|
|
1010
|
+
upsertTaskChainNode(event.data.taskId, event.data.agentSlug, "completed");
|
|
1011
|
+
setCurrentTaskId((prev) => (prev === event.data?.taskId ? undefined : prev));
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const handleFailed = (event: { data?: { taskId?: string; agentSlug?: string } }) => {
|
|
1015
|
+
if (!event.data?.taskId) return;
|
|
1016
|
+
upsertTaskChainNode(event.data.taskId, event.data.agentSlug, "failed");
|
|
1017
|
+
setCurrentTaskId((prev) => (prev === event.data?.taskId ? undefined : prev));
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
const handleStarted = (event: { data?: { taskId?: string; agentSlug?: string } }) => {
|
|
1021
|
+
if (!event.data?.taskId) return;
|
|
1022
|
+
upsertTaskChainNode(event.data.taskId, event.data.agentSlug, "running");
|
|
1023
|
+
setCurrentTaskId(event.data.taskId);
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
orchestrator.on("subagent_spawned", handleSpawned);
|
|
1027
|
+
orchestrator.on("task_started", handleStarted);
|
|
1028
|
+
orchestrator.on("task_completed", handleCompleted);
|
|
1029
|
+
orchestrator.on("task_failed", handleFailed);
|
|
1030
|
+
orchestrator.on("subagent_cancelled", handleFailed);
|
|
1031
|
+
|
|
1032
|
+
return () => {
|
|
1033
|
+
orchestrator.off("subagent_spawned", handleSpawned);
|
|
1034
|
+
orchestrator.off("task_started", handleStarted);
|
|
1035
|
+
orchestrator.off("task_completed", handleCompleted);
|
|
1036
|
+
orchestrator.off("task_failed", handleFailed);
|
|
1037
|
+
orchestrator.off("subagent_cancelled", handleFailed);
|
|
1038
|
+
};
|
|
1039
|
+
}, [agentLoopProp, upsertTaskChainNode]);
|
|
1040
|
+
|
|
1041
|
+
// ==========================================================================
|
|
1042
|
+
// Core Services - Tools, Credentials, Sessions
|
|
1043
|
+
// ==========================================================================
|
|
1044
|
+
|
|
1045
|
+
const [credentialManager, setCredentialManager] = useState<CredentialManager | null>(null);
|
|
1046
|
+
|
|
1047
|
+
useEffect(() => {
|
|
1048
|
+
let cancelled = false;
|
|
1049
|
+
|
|
1050
|
+
const initializeCredentials = async () => {
|
|
1051
|
+
try {
|
|
1052
|
+
const manager = await createCredentialManager();
|
|
1053
|
+
if (!cancelled) {
|
|
1054
|
+
setCredentialManager(manager);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
console.warn(
|
|
1058
|
+
"[credentials] Failed to initialize credential manager:",
|
|
1059
|
+
error instanceof Error ? error.message : String(error)
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
void initializeCredentials();
|
|
1065
|
+
|
|
1066
|
+
return () => {
|
|
1067
|
+
cancelled = true;
|
|
1068
|
+
};
|
|
1069
|
+
}, []);
|
|
1070
|
+
|
|
1071
|
+
const storageManagerRef = useRef<StorageManager | null>(null);
|
|
1072
|
+
const sessionListServiceRef = useRef<SessionListService | null>(null);
|
|
1073
|
+
const searchServiceRef = useRef<SearchService | null>(null);
|
|
1074
|
+
const sessionCacheRef = useRef<Session | null>(null);
|
|
1075
|
+
const [storageReady, setStorageReady] = useState(false);
|
|
1076
|
+
|
|
1077
|
+
useEffect(() => {
|
|
1078
|
+
let cancelled = false;
|
|
1079
|
+
|
|
1080
|
+
const initializeStorage = async () => {
|
|
1081
|
+
try {
|
|
1082
|
+
const manager = await StorageManager.create();
|
|
1083
|
+
const listService = new SessionListService(manager);
|
|
1084
|
+
const searchService = new SearchService(manager);
|
|
1085
|
+
await searchService.initialize();
|
|
1086
|
+
|
|
1087
|
+
if (!cancelled) {
|
|
1088
|
+
storageManagerRef.current = manager;
|
|
1089
|
+
sessionListServiceRef.current = listService;
|
|
1090
|
+
searchServiceRef.current = searchService;
|
|
1091
|
+
setStorageReady(true);
|
|
1092
|
+
}
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (!cancelled) {
|
|
1095
|
+
console.warn(
|
|
1096
|
+
"[sessions] Failed to initialize session storage:",
|
|
1097
|
+
error instanceof Error ? error.message : String(error)
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
void initializeStorage();
|
|
1104
|
+
|
|
1105
|
+
return () => {
|
|
1106
|
+
cancelled = true;
|
|
1107
|
+
};
|
|
1108
|
+
}, []);
|
|
1109
|
+
|
|
1110
|
+
// ==========================================================================
|
|
1111
|
+
// UI State Management
|
|
1112
|
+
// ==========================================================================
|
|
1113
|
+
|
|
1114
|
+
// Current coding mode state
|
|
1115
|
+
const [currentMode, setCurrentMode] = useState<CodingMode>(_mode);
|
|
1116
|
+
const modeManager = useMemo(
|
|
1117
|
+
() => createModeManager({ initialMode: _mode, requireSpecConfirmation: true }),
|
|
1118
|
+
[_mode]
|
|
1119
|
+
);
|
|
1120
|
+
const currentModeRef = useRef<CodingMode>(_mode);
|
|
1121
|
+
|
|
1122
|
+
// Modal visibility states
|
|
1123
|
+
const [showModeSelector, setShowModeSelector] = useState(false);
|
|
1124
|
+
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
1125
|
+
const [showSessionManager, setShowSessionManager] = useState(false);
|
|
1126
|
+
const [showHelpModal, setShowHelpModal] = useState(false);
|
|
1127
|
+
const [showApprovalQueue, setShowApprovalQueue] = useState(false);
|
|
1128
|
+
const [checkpointDiff, setCheckpointDiff] = useState<{
|
|
1129
|
+
content: string;
|
|
1130
|
+
snapshotHash?: string;
|
|
1131
|
+
isLoading: boolean;
|
|
1132
|
+
isVisible: boolean;
|
|
1133
|
+
}>({ content: "", isLoading: false, isVisible: false });
|
|
1134
|
+
|
|
1135
|
+
// Onboarding state
|
|
1136
|
+
const [showOnboarding, setShowOnboarding] = useState(false);
|
|
1137
|
+
const [isFirstRun, setIsFirstRun] = useState(false);
|
|
1138
|
+
const [bannerSeen, setBannerSeenState] = useState(() => getBannerSeen());
|
|
1139
|
+
const [bannerSplashComplete, setBannerSplashComplete] = useState(false);
|
|
1140
|
+
|
|
1141
|
+
// Model selection state (moved earlier for onboarding config loading)
|
|
1142
|
+
const [currentModel, setCurrentModel] = useState(model);
|
|
1143
|
+
const [currentProvider, setCurrentProvider] = useState(provider);
|
|
1144
|
+
|
|
1145
|
+
// Agent task chain state for AgentProgress display
|
|
1146
|
+
const [taskChain, setTaskChain] = useState<TaskChain | null>(null);
|
|
1147
|
+
const [currentTaskId, setCurrentTaskId] = useState<string | undefined>(undefined);
|
|
1148
|
+
|
|
1149
|
+
useEffect(() => {
|
|
1150
|
+
currentModeRef.current = currentMode;
|
|
1151
|
+
}, [currentMode]);
|
|
1152
|
+
|
|
1153
|
+
useEffect(() => {
|
|
1154
|
+
setModeCommandsManager(modeManager);
|
|
1155
|
+
return () => setModeCommandsManager(null);
|
|
1156
|
+
}, [modeManager]);
|
|
1157
|
+
|
|
1158
|
+
const bannerOverride = banner ?? false;
|
|
1159
|
+
const shouldShowBanner = !showOnboarding && (bannerOverride || !bannerSeen);
|
|
1160
|
+
const bannerCycleDurationMs = 1600;
|
|
1161
|
+
const bannerUpdateIntervalMs = 16;
|
|
1162
|
+
const bannerCycles = 2;
|
|
1163
|
+
const bannerDisplayDurationMs = bannerCycleDurationMs * bannerCycles + 300;
|
|
1164
|
+
|
|
1165
|
+
const handleBannerComplete = useCallback(() => {
|
|
1166
|
+
setBannerSplashComplete(true);
|
|
1167
|
+
if (!bannerSeen) {
|
|
1168
|
+
saveBannerSeen(true);
|
|
1169
|
+
setBannerSeenState(true);
|
|
1170
|
+
}
|
|
1171
|
+
}, [bannerSeen]);
|
|
1172
|
+
|
|
1173
|
+
// Check onboarding completion status on mount and load saved config
|
|
1174
|
+
useEffect(() => {
|
|
1175
|
+
const checkOnboarding = async () => {
|
|
1176
|
+
const completed = await CoreOnboardingWizard.isCompleted();
|
|
1177
|
+
setIsFirstRun(!completed);
|
|
1178
|
+
|
|
1179
|
+
// Issue 2 Fix: Load saved config if onboarding was completed
|
|
1180
|
+
if (completed) {
|
|
1181
|
+
const wizard = new CoreOnboardingWizard();
|
|
1182
|
+
const loadResult = await wizard.loadState();
|
|
1183
|
+
if (loadResult.ok) {
|
|
1184
|
+
const config = wizard.generateConfig();
|
|
1185
|
+
if (config.provider && config.model) {
|
|
1186
|
+
setCurrentProvider(config.provider);
|
|
1187
|
+
setCurrentModel(config.model);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
void checkOnboarding();
|
|
1193
|
+
}, []);
|
|
1194
|
+
|
|
1195
|
+
// Spec mode phase tracking
|
|
1196
|
+
const [specPhase, setSpecPhase] = useState(1);
|
|
1197
|
+
|
|
1198
|
+
// Sidebar visibility states
|
|
1199
|
+
const [showSidebar, setShowSidebar] = useState(true);
|
|
1200
|
+
const [sidebarContent, setSidebarContent] = useState<SidebarContent>("memory");
|
|
1201
|
+
|
|
1202
|
+
// Warning ref for thinking mode (used for model capability warnings)
|
|
1203
|
+
const thinkingWarningRef = useRef<Set<string>>(new Set());
|
|
1204
|
+
|
|
1205
|
+
// ==========================================================================
|
|
1206
|
+
// FIX 2: Session Management - Connect Real Session Data
|
|
1207
|
+
// ==========================================================================
|
|
1208
|
+
|
|
1209
|
+
// Session list state - loaded from storage
|
|
1210
|
+
const [sessions, setSessions] = useState<SessionMetadata[]>([]);
|
|
1211
|
+
const [activeSessionId, setActiveSessionId] = useState<string>(() => createId());
|
|
1212
|
+
|
|
1213
|
+
// Extended token usage state (Fix 2: TUI layer token counting)
|
|
1214
|
+
const tokenUsageRef = useRef({
|
|
1215
|
+
inputTokens: 0,
|
|
1216
|
+
outputTokens: 0,
|
|
1217
|
+
thinkingTokens: 0,
|
|
1218
|
+
cacheReadTokens: 0,
|
|
1219
|
+
cacheWriteTokens: 0,
|
|
1220
|
+
totalCost: 0,
|
|
1221
|
+
});
|
|
1222
|
+
const [tokenUsage, setTokenUsage] = useState({
|
|
1223
|
+
inputTokens: 0,
|
|
1224
|
+
outputTokens: 0,
|
|
1225
|
+
thinkingTokens: 0,
|
|
1226
|
+
cacheReadTokens: 0,
|
|
1227
|
+
cacheWriteTokens: 0,
|
|
1228
|
+
totalCost: 0,
|
|
1229
|
+
});
|
|
1230
|
+
// Per-turn token usage for granular display
|
|
1231
|
+
const [turnUsage, setTurnUsage] = useState({
|
|
1232
|
+
inputTokens: 0,
|
|
1233
|
+
outputTokens: 0,
|
|
1234
|
+
thinkingTokens: 0,
|
|
1235
|
+
cacheReadTokens: 0,
|
|
1236
|
+
cacheWriteTokens: 0,
|
|
1237
|
+
});
|
|
1238
|
+
const previousTokenUsageRef = useRef({ inputTokens: 0, outputTokens: 0 });
|
|
1239
|
+
|
|
1240
|
+
const switchToSession = useCallback((sessionId: string, session?: Session) => {
|
|
1241
|
+
sessionCacheRef.current = session ?? null;
|
|
1242
|
+
previousTokenUsageRef.current = { inputTokens: 0, outputTokens: 0 };
|
|
1243
|
+
tokenUsageRef.current = {
|
|
1244
|
+
inputTokens: 0,
|
|
1245
|
+
outputTokens: 0,
|
|
1246
|
+
thinkingTokens: 0,
|
|
1247
|
+
cacheReadTokens: 0,
|
|
1248
|
+
cacheWriteTokens: 0,
|
|
1249
|
+
totalCost: 0,
|
|
1250
|
+
};
|
|
1251
|
+
setTokenUsage({
|
|
1252
|
+
inputTokens: 0,
|
|
1253
|
+
outputTokens: 0,
|
|
1254
|
+
thinkingTokens: 0,
|
|
1255
|
+
cacheReadTokens: 0,
|
|
1256
|
+
cacheWriteTokens: 0,
|
|
1257
|
+
totalCost: 0,
|
|
1258
|
+
});
|
|
1259
|
+
setTurnUsage({
|
|
1260
|
+
inputTokens: 0,
|
|
1261
|
+
outputTokens: 0,
|
|
1262
|
+
thinkingTokens: 0,
|
|
1263
|
+
cacheReadTokens: 0,
|
|
1264
|
+
cacheWriteTokens: 0,
|
|
1265
|
+
});
|
|
1266
|
+
setActiveSessionId(sessionId);
|
|
1267
|
+
}, []);
|
|
1268
|
+
|
|
1269
|
+
const refreshSessions = useCallback(async () => {
|
|
1270
|
+
const listService = sessionListServiceRef.current;
|
|
1271
|
+
if (!listService) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
try {
|
|
1276
|
+
const recent = await listService.getRecentSessions(50);
|
|
1277
|
+
setSessions(
|
|
1278
|
+
recent.map((session) => ({
|
|
1279
|
+
id: session.id,
|
|
1280
|
+
title: session.title,
|
|
1281
|
+
timestamp: session.lastActive,
|
|
1282
|
+
messageCount: session.messageCount,
|
|
1283
|
+
lastMessage: session.summary,
|
|
1284
|
+
}))
|
|
1285
|
+
);
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
console.warn(
|
|
1288
|
+
"[sessions] Failed to refresh session list:",
|
|
1289
|
+
error instanceof Error ? error.message : String(error)
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
}, []);
|
|
1293
|
+
|
|
1294
|
+
// Session storage (file-backed with memory fallback while initializing)
|
|
1295
|
+
const sessionStorage = useMemo<SessionStorage>(() => {
|
|
1296
|
+
const fallbackStorage = createMemorySessionStorage();
|
|
1297
|
+
|
|
1298
|
+
return {
|
|
1299
|
+
async save(sessionId, sessionMessages) {
|
|
1300
|
+
const storage = storageManagerRef.current;
|
|
1301
|
+
if (!storage) {
|
|
1302
|
+
await fallbackStorage.save(sessionId, sessionMessages);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
let session = sessionCacheRef.current;
|
|
1307
|
+
if (!session || session.metadata.id !== sessionId) {
|
|
1308
|
+
try {
|
|
1309
|
+
session = await storage.load(sessionId);
|
|
1310
|
+
} catch {
|
|
1311
|
+
session = createSession({
|
|
1312
|
+
id: sessionId,
|
|
1313
|
+
title: "New Session",
|
|
1314
|
+
mode: mapCodingModeToSessionMode(currentModeRef.current),
|
|
1315
|
+
workingDirectory: process.cwd(),
|
|
1316
|
+
messages: [],
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const title =
|
|
1322
|
+
sessionMessages.length > 0 ? buildSessionTitle(sessionMessages) : session.metadata.title;
|
|
1323
|
+
const summary =
|
|
1324
|
+
sessionMessages.length > 0
|
|
1325
|
+
? buildSessionSummary(sessionMessages)
|
|
1326
|
+
: session.metadata.summary;
|
|
1327
|
+
const tokenCount = tokenUsageRef.current.inputTokens + tokenUsageRef.current.outputTokens;
|
|
1328
|
+
|
|
1329
|
+
const updatedSession = updateSessionMetadata(
|
|
1330
|
+
{
|
|
1331
|
+
...session,
|
|
1332
|
+
messages: [...sessionMessages],
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
title,
|
|
1336
|
+
summary,
|
|
1337
|
+
lastActive: new Date(),
|
|
1338
|
+
workingDirectory: process.cwd(),
|
|
1339
|
+
messageCount: sessionMessages.length,
|
|
1340
|
+
tokenCount,
|
|
1341
|
+
mode: mapCodingModeToSessionMode(currentModeRef.current),
|
|
1342
|
+
}
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
sessionCacheRef.current = updatedSession;
|
|
1346
|
+
await storage.save(updatedSession);
|
|
1347
|
+
await refreshSessions();
|
|
1348
|
+
},
|
|
1349
|
+
|
|
1350
|
+
async load(sessionId) {
|
|
1351
|
+
const storage = storageManagerRef.current;
|
|
1352
|
+
if (!storage) {
|
|
1353
|
+
return fallbackStorage.load(sessionId);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
try {
|
|
1357
|
+
const session = await storage.load(sessionId);
|
|
1358
|
+
sessionCacheRef.current = session;
|
|
1359
|
+
return session.messages;
|
|
1360
|
+
} catch {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
|
|
1365
|
+
async clear(sessionId) {
|
|
1366
|
+
const storage = storageManagerRef.current;
|
|
1367
|
+
if (!storage) {
|
|
1368
|
+
await fallbackStorage.clear(sessionId);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
try {
|
|
1373
|
+
await storage.delete(sessionId);
|
|
1374
|
+
if (sessionCacheRef.current?.metadata.id === sessionId) {
|
|
1375
|
+
sessionCacheRef.current = null;
|
|
1376
|
+
}
|
|
1377
|
+
await refreshSessions();
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
console.warn(
|
|
1380
|
+
"[sessions] Failed to clear session:",
|
|
1381
|
+
error instanceof Error ? error.message : String(error)
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
};
|
|
1386
|
+
}, [refreshSessions]);
|
|
1387
|
+
|
|
1388
|
+
// ==========================================================================
|
|
1389
|
+
// Adapter Integration - Session Adapter
|
|
1390
|
+
// ==========================================================================
|
|
1391
|
+
|
|
1392
|
+
// Session adapter for persistence with auto-save
|
|
1393
|
+
const {
|
|
1394
|
+
saveSession,
|
|
1395
|
+
clearSession,
|
|
1396
|
+
isSaving: _isSaving,
|
|
1397
|
+
isLoading: _isSessionLoading,
|
|
1398
|
+
error: sessionError,
|
|
1399
|
+
} = useSessionAdapter({
|
|
1400
|
+
sessionId: activeSessionId,
|
|
1401
|
+
storage: sessionStorage,
|
|
1402
|
+
autoSave: true,
|
|
1403
|
+
saveDebounceMs: 2000, // Auto-save after 2 seconds of inactivity
|
|
1404
|
+
autoLoad: true, // Load session on mount
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
const costService = useMemo(
|
|
1408
|
+
() => createCostService({ sessionId: activeSessionId }),
|
|
1409
|
+
[activeSessionId]
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
useEffect(() => {
|
|
1413
|
+
setCostCommandsService(costService);
|
|
1414
|
+
return () => setCostCommandsService(null);
|
|
1415
|
+
}, [costService]);
|
|
1416
|
+
|
|
1417
|
+
// Handle session errors
|
|
1418
|
+
useEffect(() => {
|
|
1419
|
+
if (sessionError) {
|
|
1420
|
+
console.error("Session error:", sessionError.message);
|
|
1421
|
+
notifyError(`Session error: ${sessionError.message}`);
|
|
1422
|
+
}
|
|
1423
|
+
}, [sessionError, notifyError]);
|
|
1424
|
+
|
|
1425
|
+
// Load sessions when storage is ready
|
|
1426
|
+
useEffect(() => {
|
|
1427
|
+
if (storageReady) {
|
|
1428
|
+
void refreshSessions();
|
|
1429
|
+
}
|
|
1430
|
+
}, [storageReady, refreshSessions]);
|
|
1431
|
+
|
|
1432
|
+
// ==========================================================================
|
|
1433
|
+
// Persistence Hook Integration
|
|
1434
|
+
// ==========================================================================
|
|
1435
|
+
|
|
1436
|
+
// Initialize persistence hook with advanced features
|
|
1437
|
+
const persistence = usePersistence({
|
|
1438
|
+
sessionId: activeSessionId,
|
|
1439
|
+
storage: sessionStorage,
|
|
1440
|
+
storageManager: storageManagerRef.current ?? undefined,
|
|
1441
|
+
enableAdvancedPersistence: !!storageManagerRef.current,
|
|
1442
|
+
autoSave: true,
|
|
1443
|
+
saveDebounceMs: 2000,
|
|
1444
|
+
autoLoad: true,
|
|
1445
|
+
onError: (error) => {
|
|
1446
|
+
console.error("[persistence] Error:", error.message);
|
|
1447
|
+
notifyError(`Persistence error: ${error.message}`);
|
|
1448
|
+
},
|
|
1449
|
+
onCheckpointCreated: (checkpointId) => {
|
|
1450
|
+
announce(`Checkpoint created: ${checkpointId.slice(0, 8)}`);
|
|
1451
|
+
},
|
|
1452
|
+
onRollbackComplete: (success) => {
|
|
1453
|
+
if (success) {
|
|
1454
|
+
announce("Rollback complete");
|
|
1455
|
+
} else {
|
|
1456
|
+
notifyError("Rollback failed");
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
// Set persistence ref for slash commands
|
|
1462
|
+
useEffect(() => {
|
|
1463
|
+
setPersistenceRef({
|
|
1464
|
+
status: persistence.status,
|
|
1465
|
+
unsavedCount: persistence.unsavedCount,
|
|
1466
|
+
checkpoints: persistence.checkpoints,
|
|
1467
|
+
isAdvancedEnabled: persistence.isAdvancedEnabled,
|
|
1468
|
+
createCheckpoint: persistence.createCheckpoint,
|
|
1469
|
+
rollbackToCheckpoint: persistence.rollbackToCheckpoint,
|
|
1470
|
+
deleteCheckpoint: persistence.deleteCheckpoint,
|
|
1471
|
+
getMessagesToLose: persistence.getMessagesToLose,
|
|
1472
|
+
forceSave: persistence.forceSave,
|
|
1473
|
+
});
|
|
1474
|
+
return () => setPersistenceRef(null);
|
|
1475
|
+
}, [persistence]);
|
|
1476
|
+
|
|
1477
|
+
// Initialize persistence keyboard shortcuts
|
|
1478
|
+
usePersistenceShortcuts({
|
|
1479
|
+
persistence,
|
|
1480
|
+
enabled: true,
|
|
1481
|
+
onSave: () => announce("Session saved"),
|
|
1482
|
+
onCheckpointCreated: (id) => announce(`Checkpoint: ${id.slice(0, 8)}`),
|
|
1483
|
+
onError: (error) => notifyError(error),
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
// ==========================================================================
|
|
1487
|
+
// FIX 4: Real Todo and Memory Data
|
|
1488
|
+
// ==========================================================================
|
|
1489
|
+
|
|
1490
|
+
const { executions, pendingApproval, approveExecution, rejectExecution, approveAll } = useTools();
|
|
1491
|
+
const pendingApprovalCountRef = useRef(pendingApproval.length);
|
|
1492
|
+
|
|
1493
|
+
useEffect(() => {
|
|
1494
|
+
const previousCount = pendingApprovalCountRef.current;
|
|
1495
|
+
const currentCount = pendingApproval.length;
|
|
1496
|
+
pendingApprovalCountRef.current = currentCount;
|
|
1497
|
+
|
|
1498
|
+
if (currentCount > 1 && previousCount <= 1) {
|
|
1499
|
+
setShowApprovalQueue(true);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (currentCount <= 1 && showApprovalQueue) {
|
|
1503
|
+
setShowApprovalQueue(false);
|
|
1504
|
+
}
|
|
1505
|
+
}, [pendingApproval.length, showApprovalQueue]);
|
|
1506
|
+
|
|
1507
|
+
const loadTodos = useCallback(async (): Promise<readonly TodoItemData[]> => {
|
|
1508
|
+
const todoFilePath = join(process.cwd(), ".vellum", "todos.json");
|
|
1509
|
+
|
|
1510
|
+
try {
|
|
1511
|
+
const content = await readFile(todoFilePath, { encoding: "utf-8" });
|
|
1512
|
+
const parsed = JSON.parse(content) as unknown;
|
|
1513
|
+
|
|
1514
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1515
|
+
return [];
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const items = (parsed as { items?: unknown }).items;
|
|
1519
|
+
if (!Array.isArray(items)) {
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
return items
|
|
1524
|
+
.map((item): TodoItemData | null => {
|
|
1525
|
+
if (!item || typeof item !== "object") return null;
|
|
1526
|
+
|
|
1527
|
+
const id = (item as { id?: unknown }).id;
|
|
1528
|
+
const text = (item as { text?: unknown }).text;
|
|
1529
|
+
const completed = (item as { completed?: unknown }).completed;
|
|
1530
|
+
const createdAt = (item as { createdAt?: unknown }).createdAt;
|
|
1531
|
+
const completedAt = (item as { completedAt?: unknown }).completedAt;
|
|
1532
|
+
|
|
1533
|
+
if (
|
|
1534
|
+
typeof id !== "number" ||
|
|
1535
|
+
typeof text !== "string" ||
|
|
1536
|
+
typeof completed !== "boolean"
|
|
1537
|
+
) {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
const createdAtStr = typeof createdAt === "string" ? createdAt : new Date().toISOString();
|
|
1542
|
+
const completedAtStr = typeof completedAt === "string" ? completedAt : undefined;
|
|
1543
|
+
|
|
1544
|
+
const mapped: TodoItemData = {
|
|
1545
|
+
id,
|
|
1546
|
+
title: text,
|
|
1547
|
+
status: completed ? "completed" : "pending",
|
|
1548
|
+
createdAt: createdAtStr,
|
|
1549
|
+
completedAt: completedAtStr,
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
return mapped;
|
|
1553
|
+
})
|
|
1554
|
+
.filter((item): item is TodoItemData => item !== null);
|
|
1555
|
+
} catch {
|
|
1556
|
+
return [];
|
|
1557
|
+
}
|
|
1558
|
+
}, []);
|
|
1559
|
+
|
|
1560
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex memory loading from multiple sources
|
|
1561
|
+
const loadMemories = useCallback(async (): Promise<MemoryPanelProps["entries"]> => {
|
|
1562
|
+
const projectEntries: Array<MemoryPanelProps["entries"][number]> = [];
|
|
1563
|
+
|
|
1564
|
+
// 1) Project memory service entries (.vellum/memory.json)
|
|
1565
|
+
try {
|
|
1566
|
+
const service = new ProjectMemoryService();
|
|
1567
|
+
await service.initialize(process.cwd());
|
|
1568
|
+
try {
|
|
1569
|
+
projectEntries.push(...(await service.listEntries()));
|
|
1570
|
+
} finally {
|
|
1571
|
+
await service.close();
|
|
1572
|
+
}
|
|
1573
|
+
} catch {
|
|
1574
|
+
// Best-effort: ignore load failures and fall back to other sources.
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// 2) save_memory tool entries (.vellum/memory/{namespace}/{key}.json)
|
|
1578
|
+
const toolEntries: Array<MemoryPanelProps["entries"][number]> = [];
|
|
1579
|
+
const toolMemoryBaseDir = join(process.cwd(), ".vellum", "memory");
|
|
1580
|
+
|
|
1581
|
+
try {
|
|
1582
|
+
const namespaceEntries = await readdir(toolMemoryBaseDir, { withFileTypes: true });
|
|
1583
|
+
|
|
1584
|
+
for (const namespaceDirent of namespaceEntries) {
|
|
1585
|
+
if (!namespaceDirent.isDirectory()) continue;
|
|
1586
|
+
const namespace = namespaceDirent.name;
|
|
1587
|
+
const namespaceDir = join(toolMemoryBaseDir, namespace);
|
|
1588
|
+
|
|
1589
|
+
const files = await readdir(namespaceDir, { withFileTypes: true });
|
|
1590
|
+
for (const file of files) {
|
|
1591
|
+
if (!file.isFile()) continue;
|
|
1592
|
+
if (!file.name.endsWith(".json")) continue;
|
|
1593
|
+
|
|
1594
|
+
const memoryFilePath = join(namespaceDir, file.name);
|
|
1595
|
+
|
|
1596
|
+
try {
|
|
1597
|
+
const content = await readFile(memoryFilePath, { encoding: "utf-8" });
|
|
1598
|
+
const parsed = JSON.parse(content) as unknown;
|
|
1599
|
+
|
|
1600
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
1601
|
+
|
|
1602
|
+
const value = (parsed as { value?: unknown }).value;
|
|
1603
|
+
const storedAt = (parsed as { storedAt?: unknown }).storedAt;
|
|
1604
|
+
const updatedAt = (parsed as { updatedAt?: unknown }).updatedAt;
|
|
1605
|
+
const key = (parsed as { key?: unknown }).key;
|
|
1606
|
+
|
|
1607
|
+
if (typeof value !== "string" || typeof key !== "string") continue;
|
|
1608
|
+
|
|
1609
|
+
const createdAtDate = typeof storedAt === "string" ? new Date(storedAt) : new Date();
|
|
1610
|
+
const updatedAtDate =
|
|
1611
|
+
typeof updatedAt === "string" ? new Date(updatedAt) : createdAtDate;
|
|
1612
|
+
|
|
1613
|
+
toolEntries.push({
|
|
1614
|
+
key: `${namespace}/${key}`,
|
|
1615
|
+
type: "context",
|
|
1616
|
+
content: value,
|
|
1617
|
+
createdAt: createdAtDate,
|
|
1618
|
+
updatedAt: updatedAtDate,
|
|
1619
|
+
metadata: {
|
|
1620
|
+
tags: ["tool:save_memory", `namespace:${namespace}`],
|
|
1621
|
+
importance: 0.5,
|
|
1622
|
+
},
|
|
1623
|
+
});
|
|
1624
|
+
} catch {
|
|
1625
|
+
// Skip unreadable/invalid entries.
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
} catch {
|
|
1630
|
+
// No tool memory directory (or unreadable). Ignore.
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
const combined: Array<MemoryPanelProps["entries"][number]> = [
|
|
1634
|
+
...projectEntries,
|
|
1635
|
+
...toolEntries,
|
|
1636
|
+
];
|
|
1637
|
+
combined.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
1638
|
+
return combined;
|
|
1639
|
+
}, []);
|
|
1640
|
+
|
|
1641
|
+
const { todoItems, memoryEntries, refreshTodos } = useSidebarPanelData({
|
|
1642
|
+
sidebarVisible: showSidebar,
|
|
1643
|
+
sidebarContent,
|
|
1644
|
+
executions,
|
|
1645
|
+
loadTodos,
|
|
1646
|
+
loadMemories,
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
const [thinkingModeEnabled, setThinkingModeEnabled] = useState(() => getThinkingState().enabled);
|
|
1650
|
+
|
|
1651
|
+
// Subscribe to global thinking state changes (mode toggle via /think)
|
|
1652
|
+
useEffect(() => {
|
|
1653
|
+
const unsubscribe = subscribeToThinkingState((state) => {
|
|
1654
|
+
setThinkingModeEnabled(state.enabled);
|
|
1655
|
+
});
|
|
1656
|
+
return unsubscribe;
|
|
1657
|
+
}, []);
|
|
1658
|
+
|
|
1659
|
+
const effectiveApprovalPolicy = useMemo<ApprovalPolicy>(() => {
|
|
1660
|
+
if (_approval) {
|
|
1661
|
+
return _approval;
|
|
1662
|
+
}
|
|
1663
|
+
// Fall back to mode-specific defaults.
|
|
1664
|
+
// Using explicit mapping avoids assumptions about ModeManager's internal config.
|
|
1665
|
+
return getDefaultApprovalPolicyForMode(currentMode);
|
|
1666
|
+
}, [_approval, currentMode]);
|
|
1667
|
+
|
|
1668
|
+
const trustMode = useMemo<TrustMode>(
|
|
1669
|
+
() => approvalPolicyToTrustMode(effectiveApprovalPolicy),
|
|
1670
|
+
[effectiveApprovalPolicy]
|
|
1671
|
+
);
|
|
1672
|
+
|
|
1673
|
+
// ==========================================================================
|
|
1674
|
+
// Provider Status (ModelStatusBar integration)
|
|
1675
|
+
// ==========================================================================
|
|
1676
|
+
|
|
1677
|
+
// Track provider health status with circuit breaker states
|
|
1678
|
+
const providerStatus = useProviderStatus({
|
|
1679
|
+
initialProviders: [{ id: provider, name: provider, isActive: true }],
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// ==========================================================================
|
|
1683
|
+
// Snapshots (SnapshotCheckpointPanel integration)
|
|
1684
|
+
// ==========================================================================
|
|
1685
|
+
|
|
1686
|
+
const snapshots = useSnapshots();
|
|
1687
|
+
|
|
1688
|
+
const openCheckpointDiff = useCallback(
|
|
1689
|
+
async (hash: string) => {
|
|
1690
|
+
setCheckpointDiff({ content: "", snapshotHash: hash, isLoading: true, isVisible: true });
|
|
1691
|
+
|
|
1692
|
+
try {
|
|
1693
|
+
const diff = await snapshots.diff(hash);
|
|
1694
|
+
setCheckpointDiff({
|
|
1695
|
+
content: diff,
|
|
1696
|
+
snapshotHash: hash,
|
|
1697
|
+
isLoading: false,
|
|
1698
|
+
isVisible: true,
|
|
1699
|
+
});
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
setCheckpointDiff({
|
|
1702
|
+
content: `Failed to load diff: ${error instanceof Error ? error.message : String(error)}`,
|
|
1703
|
+
snapshotHash: hash,
|
|
1704
|
+
isLoading: false,
|
|
1705
|
+
isVisible: true,
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
},
|
|
1709
|
+
[snapshots]
|
|
1710
|
+
);
|
|
1711
|
+
|
|
1712
|
+
const closeCheckpointDiff = useCallback(() => {
|
|
1713
|
+
setCheckpointDiff((prev) => ({ ...prev, isVisible: false }));
|
|
1714
|
+
}, []);
|
|
1715
|
+
|
|
1716
|
+
// Cost tracking state for CostWarning component
|
|
1717
|
+
const [costWarningState, setCostWarningState] = useState<{
|
|
1718
|
+
show: boolean;
|
|
1719
|
+
limitReached: boolean;
|
|
1720
|
+
percentUsed: number;
|
|
1721
|
+
costLimit: number;
|
|
1722
|
+
requestLimit: number;
|
|
1723
|
+
}>({ show: false, limitReached: false, percentUsed: 0, costLimit: 10, requestLimit: 100 });
|
|
1724
|
+
|
|
1725
|
+
// Auto-approval status state for AutoApprovalStatus component
|
|
1726
|
+
// NOTE: setAutoApprovalState is kept for future integration with AgentLoop's
|
|
1727
|
+
// getAutoApprovalStatus() method. Currently set via useEffect below.
|
|
1728
|
+
const [autoApprovalState, setAutoApprovalState] = useState<{
|
|
1729
|
+
consecutiveRequests: number;
|
|
1730
|
+
requestLimit: number;
|
|
1731
|
+
consecutiveCost: number;
|
|
1732
|
+
costLimit: number;
|
|
1733
|
+
requestPercentUsed: number;
|
|
1734
|
+
costPercentUsed: number;
|
|
1735
|
+
limitReached: boolean;
|
|
1736
|
+
limitType?: "requests" | "cost";
|
|
1737
|
+
} | null>(null);
|
|
1738
|
+
|
|
1739
|
+
// Update auto-approval state from AgentLoop when available
|
|
1740
|
+
useEffect(() => {
|
|
1741
|
+
if (!agentLoopProp) return;
|
|
1742
|
+
|
|
1743
|
+
// Check if AgentLoop has getAutoApprovalStatus method (Phase 35+)
|
|
1744
|
+
const loopWithStatus = agentLoopProp as typeof agentLoopProp & {
|
|
1745
|
+
getAutoApprovalStatus?: () => {
|
|
1746
|
+
consecutiveRequests: number;
|
|
1747
|
+
requestLimit: number;
|
|
1748
|
+
consecutiveCost: number;
|
|
1749
|
+
costLimit: number;
|
|
1750
|
+
requestPercentUsed: number;
|
|
1751
|
+
costPercentUsed: number;
|
|
1752
|
+
requestLimitReached: boolean;
|
|
1753
|
+
costLimitReached: boolean;
|
|
1754
|
+
} | null;
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
if (typeof loopWithStatus.getAutoApprovalStatus !== "function") {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Periodic polling for auto-approval status
|
|
1762
|
+
const updateStatus = () => {
|
|
1763
|
+
const status = loopWithStatus.getAutoApprovalStatus?.();
|
|
1764
|
+
if (status) {
|
|
1765
|
+
setAutoApprovalState({
|
|
1766
|
+
consecutiveRequests: status.consecutiveRequests,
|
|
1767
|
+
requestLimit: status.requestLimit,
|
|
1768
|
+
consecutiveCost: status.consecutiveCost,
|
|
1769
|
+
costLimit: status.costLimit,
|
|
1770
|
+
requestPercentUsed: status.requestPercentUsed,
|
|
1771
|
+
costPercentUsed: status.costPercentUsed,
|
|
1772
|
+
limitReached: status.requestLimitReached || status.costLimitReached,
|
|
1773
|
+
limitType: status.requestLimitReached
|
|
1774
|
+
? "requests"
|
|
1775
|
+
: status.costLimitReached
|
|
1776
|
+
? "cost"
|
|
1777
|
+
: undefined,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
// Update immediately and then on interval
|
|
1783
|
+
updateStatus();
|
|
1784
|
+
const interval = setInterval(updateStatus, 1000);
|
|
1785
|
+
return () => clearInterval(interval);
|
|
1786
|
+
}, [agentLoopProp]);
|
|
1787
|
+
|
|
1788
|
+
// Update cost warning based on token usage
|
|
1789
|
+
useEffect(() => {
|
|
1790
|
+
const costLimit = 10; // Default $10 limit
|
|
1791
|
+
const requestLimit = 100;
|
|
1792
|
+
const percentUsed = costLimit > 0 ? (tokenUsage.totalCost / costLimit) * 100 : 0;
|
|
1793
|
+
const showWarning = percentUsed >= 80; // Show when 80%+ of limit used
|
|
1794
|
+
const limitReached = percentUsed >= 100;
|
|
1795
|
+
|
|
1796
|
+
setCostWarningState({
|
|
1797
|
+
show: showWarning,
|
|
1798
|
+
limitReached,
|
|
1799
|
+
percentUsed: Math.min(percentUsed, 100),
|
|
1800
|
+
costLimit,
|
|
1801
|
+
requestLimit,
|
|
1802
|
+
});
|
|
1803
|
+
}, [tokenUsage.totalCost]);
|
|
1804
|
+
|
|
1805
|
+
// ==========================================================================
|
|
1806
|
+
// FIX 3: Permission System Integration
|
|
1807
|
+
// ==========================================================================
|
|
1808
|
+
|
|
1809
|
+
// Drive tool approvals from ToolsContext (source of truth) and resume the AgentLoop
|
|
1810
|
+
// by calling grantPermission()/denyPermission() when the user decides.
|
|
1811
|
+
const { activeApproval, activeRiskLevel, approveActive, rejectActive } =
|
|
1812
|
+
useToolApprovalController({ agentLoop: agentLoopProp });
|
|
1813
|
+
|
|
1814
|
+
const hasActiveApproval = activeApproval !== null;
|
|
1815
|
+
|
|
1816
|
+
// Update banner state
|
|
1817
|
+
const [updateAvailable, setUpdateAvailable] = useState<{
|
|
1818
|
+
current: string;
|
|
1819
|
+
latest: string;
|
|
1820
|
+
} | null>(null);
|
|
1821
|
+
|
|
1822
|
+
// ==========================================================================
|
|
1823
|
+
// FIX 5: Mode & Theme Persistence
|
|
1824
|
+
// ==========================================================================
|
|
1825
|
+
|
|
1826
|
+
// Load persisted mode and theme on mount
|
|
1827
|
+
useEffect(() => {
|
|
1828
|
+
// Note: In terminal environment, we use process.env or config files
|
|
1829
|
+
// For now, we can use a simple in-memory approach or file-based config
|
|
1830
|
+
// This demonstrates the persistence pattern
|
|
1831
|
+
const savedMode = process.env.VELLUM_MODE as CodingMode | undefined;
|
|
1832
|
+
if (savedMode && ["vibe", "plan", "spec"].includes(savedMode)) {
|
|
1833
|
+
setCurrentMode(savedMode);
|
|
1834
|
+
}
|
|
1835
|
+
}, []);
|
|
1836
|
+
|
|
1837
|
+
// Show onboarding wizard on first run
|
|
1838
|
+
useEffect(() => {
|
|
1839
|
+
if (isFirstRun) {
|
|
1840
|
+
setShowOnboarding(true);
|
|
1841
|
+
}
|
|
1842
|
+
}, [isFirstRun]);
|
|
1843
|
+
|
|
1844
|
+
//: Show contextual tips based on state (placed after state declarations)
|
|
1845
|
+
useEffect(() => {
|
|
1846
|
+
if (!tipsEnabled) return;
|
|
1847
|
+
|
|
1848
|
+
const context = buildTipContext({
|
|
1849
|
+
screen: showOnboarding ? "onboarding" : "main",
|
|
1850
|
+
mode: currentMode,
|
|
1851
|
+
featuresUsedCount: messages.length,
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
showTip(context);
|
|
1855
|
+
}, [currentMode, messages.length, showOnboarding, tipsEnabled, showTip]);
|
|
1856
|
+
|
|
1857
|
+
// Ref to track current cancellation controller
|
|
1858
|
+
const cancellationRef = useRef<CancellationController | null>(null);
|
|
1859
|
+
|
|
1860
|
+
// ==========================================================================
|
|
1861
|
+
// Hooks Integration
|
|
1862
|
+
// ==========================================================================
|
|
1863
|
+
|
|
1864
|
+
// Screen reader accessibility hook
|
|
1865
|
+
const { announce } = useScreenReader({
|
|
1866
|
+
verbose: false,
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
// Input history hook for up/down arrow navigation
|
|
1870
|
+
const { addToHistory } = useInputHistory({
|
|
1871
|
+
maxItems: 100,
|
|
1872
|
+
persistKey: "vellum-command-history",
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
// Backtrack sync helpers
|
|
1876
|
+
// - suppressBacktrackPushRef prevents the message->backtrack sync effect from creating new
|
|
1877
|
+
// history snapshots when we are *restoring* state (undo/redo/branch switch).
|
|
1878
|
+
// - lastMessageCountRef tracks the last "real" message count that we recorded into backtrack.
|
|
1879
|
+
const suppressBacktrackPushRef = useRef(false);
|
|
1880
|
+
const lastMessageCountRef = useRef(0);
|
|
1881
|
+
|
|
1882
|
+
const applyBacktrackMessages = useCallback(
|
|
1883
|
+
(nextMessages: Message[], announcement?: string) => {
|
|
1884
|
+
suppressBacktrackPushRef.current = true;
|
|
1885
|
+
lastMessageCountRef.current = nextMessages.length;
|
|
1886
|
+
setMessages([...nextMessages]);
|
|
1887
|
+
if (announcement) {
|
|
1888
|
+
announce(announcement);
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
[announce, setMessages]
|
|
1892
|
+
);
|
|
1893
|
+
|
|
1894
|
+
// Backtrack hook for undo/redo conversation state
|
|
1895
|
+
const {
|
|
1896
|
+
backtrackState,
|
|
1897
|
+
branches,
|
|
1898
|
+
push: pushBacktrack,
|
|
1899
|
+
undo: undoBacktrack,
|
|
1900
|
+
redo: redoBacktrack,
|
|
1901
|
+
createBranch: createBacktrackBranch,
|
|
1902
|
+
switchBranch: switchBacktrackBranch,
|
|
1903
|
+
} = useBacktrack({
|
|
1904
|
+
initialState: { messages: [] as Message[] },
|
|
1905
|
+
maxHistory: 50,
|
|
1906
|
+
enableBranching: true,
|
|
1907
|
+
onStateChange: (state, action) => {
|
|
1908
|
+
if (action === "undo" || action === "redo") {
|
|
1909
|
+
applyBacktrackMessages(
|
|
1910
|
+
state.messages,
|
|
1911
|
+
`${action === "undo" ? "Undid" : "Redid"} last message`
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
const handleCreateBacktrackBranch = useCallback(() => {
|
|
1918
|
+
// Match useBacktrack's default naming behavior: `Branch ${Object.keys(state.branches).length}`
|
|
1919
|
+
const branchName = `Branch ${branches.length}`;
|
|
1920
|
+
createBacktrackBranch(branchName);
|
|
1921
|
+
notifyTaskComplete(`Created branch: ${branchName}`);
|
|
1922
|
+
announce(`Created branch: ${branchName}`);
|
|
1923
|
+
}, [announce, branches.length, createBacktrackBranch, notifyTaskComplete]);
|
|
1924
|
+
|
|
1925
|
+
const handleSwitchBacktrackBranch = useCallback(
|
|
1926
|
+
(branchId: string) => {
|
|
1927
|
+
const targetBranch = branches.find((b) => b.id === branchId);
|
|
1928
|
+
if (!targetBranch) {
|
|
1929
|
+
notifyError("Branch not found");
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// Update the underlying backtrack state first.
|
|
1934
|
+
switchBacktrackBranch(branchId);
|
|
1935
|
+
|
|
1936
|
+
// Then apply the target branch's latest snapshot to the messages view.
|
|
1937
|
+
const latestSnapshot = targetBranch.history.at(-1);
|
|
1938
|
+
const latestState = latestSnapshot?.state as { messages?: Message[] } | undefined;
|
|
1939
|
+
const nextMessages = latestState?.messages;
|
|
1940
|
+
if (Array.isArray(nextMessages)) {
|
|
1941
|
+
applyBacktrackMessages(nextMessages);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
notifyTaskComplete(`Switched to branch: ${targetBranch.name}`);
|
|
1945
|
+
announce(`Switched to branch: ${targetBranch.name}`);
|
|
1946
|
+
},
|
|
1947
|
+
[
|
|
1948
|
+
announce,
|
|
1949
|
+
applyBacktrackMessages,
|
|
1950
|
+
branches,
|
|
1951
|
+
notifyError,
|
|
1952
|
+
notifyTaskComplete,
|
|
1953
|
+
switchBacktrackBranch,
|
|
1954
|
+
]
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
// Sync messages with backtrack state
|
|
1958
|
+
// Use a ref to track last message count to avoid unnecessary pushBacktrack calls
|
|
1959
|
+
useEffect(() => {
|
|
1960
|
+
if (suppressBacktrackPushRef.current) {
|
|
1961
|
+
suppressBacktrackPushRef.current = false;
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Only push backtrack when messages are actually added (not undo/redo/branch restores).
|
|
1966
|
+
if (messages.length > lastMessageCountRef.current) {
|
|
1967
|
+
lastMessageCountRef.current = messages.length;
|
|
1968
|
+
pushBacktrack({ messages: [...messages] }, "Message added");
|
|
1969
|
+
}
|
|
1970
|
+
}, [messages, pushBacktrack]);
|
|
1971
|
+
|
|
1972
|
+
// Mode shortcuts hook (Alt+1/2/3)
|
|
1973
|
+
useModeShortcuts({
|
|
1974
|
+
modeManager,
|
|
1975
|
+
enabled:
|
|
1976
|
+
!showModeSelector &&
|
|
1977
|
+
!showModelSelector &&
|
|
1978
|
+
!hasActiveApproval &&
|
|
1979
|
+
!showSessionManager &&
|
|
1980
|
+
!showOnboarding &&
|
|
1981
|
+
!interactivePrompt &&
|
|
1982
|
+
!followupPrompt &&
|
|
1983
|
+
!pendingOperation,
|
|
1984
|
+
onModeSwitch: (mode, success) => {
|
|
1985
|
+
if (success) {
|
|
1986
|
+
setCurrentMode(mode);
|
|
1987
|
+
announce(`Switched to ${mode} mode`);
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
onError: (mode, error) => {
|
|
1991
|
+
if (modeManager.isPendingSpecConfirmation()) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
announce(`Failed to switch to ${mode}: ${error}`);
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
const openSpecConfirmation = useCallback(() => {
|
|
1999
|
+
if (interactivePrompt || pendingOperation) {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
setPromptValue("");
|
|
2004
|
+
setInteractivePrompt({
|
|
2005
|
+
inputType: "confirm",
|
|
2006
|
+
message: "⚠️ Switch to spec mode? This enables a 6-phase structured workflow.",
|
|
2007
|
+
defaultValue: "n",
|
|
2008
|
+
handler: async (value: string): Promise<CommandResult> => {
|
|
2009
|
+
const confirmed = value.toLowerCase() === "y" || value.toLowerCase() === "yes";
|
|
2010
|
+
if (!confirmed) {
|
|
2011
|
+
modeManager.cancelSpecSwitch();
|
|
2012
|
+
return { kind: "success", message: "Mode switch cancelled." };
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
const result = await modeManager.confirmSpecMode();
|
|
2016
|
+
if (result.success) {
|
|
2017
|
+
return { kind: "success", message: "📐 Switched to spec mode." };
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
return {
|
|
2021
|
+
kind: "error",
|
|
2022
|
+
code: "OPERATION_NOT_ALLOWED",
|
|
2023
|
+
message: result.reason ?? "Unable to switch to spec mode.",
|
|
2024
|
+
};
|
|
2025
|
+
},
|
|
2026
|
+
onCancel: () => {
|
|
2027
|
+
modeManager.cancelSpecSwitch();
|
|
2028
|
+
return { kind: "success", message: "Mode switch cancelled." };
|
|
2029
|
+
},
|
|
2030
|
+
});
|
|
2031
|
+
}, [interactivePrompt, pendingOperation, modeManager]);
|
|
2032
|
+
|
|
2033
|
+
useEffect(() => {
|
|
2034
|
+
const handleModeChanged = (event: { currentMode: CodingMode }) => {
|
|
2035
|
+
setCurrentMode(event.currentMode);
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
const handleSpecRequired = () => {
|
|
2039
|
+
openSpecConfirmation();
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
modeManager.on("mode-changed", handleModeChanged);
|
|
2043
|
+
modeManager.on("spec-confirmation-required", handleSpecRequired);
|
|
2044
|
+
|
|
2045
|
+
return () => {
|
|
2046
|
+
modeManager.off("mode-changed", handleModeChanged);
|
|
2047
|
+
modeManager.off("spec-confirmation-required", handleSpecRequired);
|
|
2048
|
+
};
|
|
2049
|
+
}, [modeManager, openSpecConfirmation]);
|
|
2050
|
+
|
|
2051
|
+
// Hotkeys hook for global keyboard shortcuts
|
|
2052
|
+
const hotkeyDefinitions: HotkeyDefinition[] = useMemo(() => {
|
|
2053
|
+
const hotkeys: HotkeyDefinition[] = [
|
|
2054
|
+
{
|
|
2055
|
+
key: "m",
|
|
2056
|
+
alt: true,
|
|
2057
|
+
handler: () => setShowModeSelector((prev) => !prev),
|
|
2058
|
+
description: "Toggle mode selector",
|
|
2059
|
+
scope: "global",
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
key: "k",
|
|
2063
|
+
alt: true,
|
|
2064
|
+
handler: () => setShowSidebar((prev) => !prev),
|
|
2065
|
+
description: "Toggle sidebar",
|
|
2066
|
+
scope: "global",
|
|
2067
|
+
},
|
|
2068
|
+
{
|
|
2069
|
+
key: "t",
|
|
2070
|
+
ctrl: true,
|
|
2071
|
+
handler: () => {
|
|
2072
|
+
const newState = toggleThinking();
|
|
2073
|
+
announce(newState ? "Thinking mode enabled" : "Thinking mode disabled");
|
|
2074
|
+
},
|
|
2075
|
+
description: "Toggle thinking mode",
|
|
2076
|
+
scope: "global",
|
|
2077
|
+
},
|
|
2078
|
+
// Alt+T alternative for todo panel (thinking uses Ctrl+T)
|
|
2079
|
+
{
|
|
2080
|
+
key: "t",
|
|
2081
|
+
alt: true,
|
|
2082
|
+
handler: () => {
|
|
2083
|
+
setShowSidebar(true);
|
|
2084
|
+
setSidebarContent("todo");
|
|
2085
|
+
},
|
|
2086
|
+
description: "Show todo panel (Alt)",
|
|
2087
|
+
scope: "global",
|
|
2088
|
+
},
|
|
2089
|
+
{
|
|
2090
|
+
key: "p",
|
|
2091
|
+
alt: true,
|
|
2092
|
+
handler: () => {
|
|
2093
|
+
setShowSidebar(true);
|
|
2094
|
+
setSidebarContent("memory");
|
|
2095
|
+
},
|
|
2096
|
+
description: "Show memory panel",
|
|
2097
|
+
scope: "global",
|
|
2098
|
+
},
|
|
2099
|
+
{
|
|
2100
|
+
key: "g",
|
|
2101
|
+
alt: true,
|
|
2102
|
+
handler: () => {
|
|
2103
|
+
setShowSidebar(true);
|
|
2104
|
+
setSidebarContent("tools");
|
|
2105
|
+
},
|
|
2106
|
+
description: "Show tools panel",
|
|
2107
|
+
scope: "global",
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
key: "o",
|
|
2111
|
+
alt: true,
|
|
2112
|
+
handler: () => {
|
|
2113
|
+
setShowSidebar(true);
|
|
2114
|
+
setSidebarContent("mcp");
|
|
2115
|
+
},
|
|
2116
|
+
description: "Show MCP panel",
|
|
2117
|
+
scope: "global",
|
|
2118
|
+
},
|
|
2119
|
+
// Snapshots panel (Alt+S)
|
|
2120
|
+
{
|
|
2121
|
+
key: "s",
|
|
2122
|
+
alt: true,
|
|
2123
|
+
handler: () => {
|
|
2124
|
+
setShowSidebar(true);
|
|
2125
|
+
setSidebarContent("snapshots");
|
|
2126
|
+
},
|
|
2127
|
+
description: "Show snapshots panel",
|
|
2128
|
+
scope: "global",
|
|
2129
|
+
},
|
|
2130
|
+
{
|
|
2131
|
+
key: "s",
|
|
2132
|
+
ctrl: true,
|
|
2133
|
+
handler: () => setShowSessionManager((prev) => !prev),
|
|
2134
|
+
description: "Session manager",
|
|
2135
|
+
scope: "global",
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
key: "f1",
|
|
2139
|
+
handler: () => setShowHelpModal(true),
|
|
2140
|
+
description: "Show help",
|
|
2141
|
+
scope: "global",
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
key: "?",
|
|
2145
|
+
shift: true,
|
|
2146
|
+
handler: () => setShowHelpModal(true),
|
|
2147
|
+
description: "Show help",
|
|
2148
|
+
scope: "global",
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
key: "a",
|
|
2152
|
+
ctrl: true,
|
|
2153
|
+
shift: true,
|
|
2154
|
+
handler: () => setShowApprovalQueue((prev) => !prev),
|
|
2155
|
+
description: "Toggle approval queue",
|
|
2156
|
+
scope: "global",
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
key: "z",
|
|
2160
|
+
ctrl: true,
|
|
2161
|
+
handler: () => {
|
|
2162
|
+
if (backtrackState.canUndo) {
|
|
2163
|
+
undoBacktrack();
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
description: "Undo",
|
|
2167
|
+
scope: "global",
|
|
2168
|
+
},
|
|
2169
|
+
{
|
|
2170
|
+
key: "y",
|
|
2171
|
+
ctrl: true,
|
|
2172
|
+
handler: () => {
|
|
2173
|
+
if (backtrackState.canRedo) {
|
|
2174
|
+
redoBacktrack();
|
|
2175
|
+
}
|
|
2176
|
+
},
|
|
2177
|
+
description: "Redo",
|
|
2178
|
+
scope: "global",
|
|
2179
|
+
},
|
|
2180
|
+
// Model selector toggle (Alt+Shift+M)
|
|
2181
|
+
{
|
|
2182
|
+
key: "m",
|
|
2183
|
+
alt: true,
|
|
2184
|
+
shift: true,
|
|
2185
|
+
handler: () => {
|
|
2186
|
+
setShowModelSelector((prev) => !prev);
|
|
2187
|
+
announce(showModelSelector ? "Model selector closed" : "Model selector opened");
|
|
2188
|
+
},
|
|
2189
|
+
description: "Toggle model selector",
|
|
2190
|
+
scope: "global",
|
|
2191
|
+
},
|
|
2192
|
+
// Vim mode toggle
|
|
2193
|
+
{
|
|
2194
|
+
key: "v",
|
|
2195
|
+
ctrl: true,
|
|
2196
|
+
handler: () => {
|
|
2197
|
+
setVimEnabled((prev) => !prev);
|
|
2198
|
+
vim.toggle();
|
|
2199
|
+
announce(vimEnabled ? "Vim mode disabled" : "Vim mode enabled");
|
|
2200
|
+
},
|
|
2201
|
+
description: "Toggle vim mode",
|
|
2202
|
+
scope: "global",
|
|
2203
|
+
},
|
|
2204
|
+
// Copy mode toggle
|
|
2205
|
+
{
|
|
2206
|
+
key: "c",
|
|
2207
|
+
ctrl: true,
|
|
2208
|
+
shift: true,
|
|
2209
|
+
handler: () => {
|
|
2210
|
+
if (copyMode.state.active) {
|
|
2211
|
+
copyMode.exitCopyMode();
|
|
2212
|
+
announce("Copy mode exited");
|
|
2213
|
+
} else {
|
|
2214
|
+
copyMode.enterCopyMode();
|
|
2215
|
+
announce("Copy mode entered - use arrow keys to select");
|
|
2216
|
+
}
|
|
2217
|
+
},
|
|
2218
|
+
description: "Toggle copy mode",
|
|
2219
|
+
scope: "global",
|
|
2220
|
+
},
|
|
2221
|
+
];
|
|
2222
|
+
|
|
2223
|
+
// Alternate buffer toggle for full-screen views
|
|
2224
|
+
if (alternateBufferEnabled) {
|
|
2225
|
+
hotkeys.push({
|
|
2226
|
+
key: "f",
|
|
2227
|
+
ctrl: true,
|
|
2228
|
+
handler: () => {
|
|
2229
|
+
alternateBuffer.toggle();
|
|
2230
|
+
announce(alternateBuffer.isAlternate ? "Exited fullscreen" : "Entered fullscreen");
|
|
2231
|
+
},
|
|
2232
|
+
description: "Toggle fullscreen mode",
|
|
2233
|
+
scope: "global",
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
return hotkeys;
|
|
2238
|
+
}, [
|
|
2239
|
+
backtrackState.canUndo,
|
|
2240
|
+
backtrackState.canRedo,
|
|
2241
|
+
undoBacktrack,
|
|
2242
|
+
redoBacktrack,
|
|
2243
|
+
announce,
|
|
2244
|
+
vimEnabled,
|
|
2245
|
+
vim,
|
|
2246
|
+
copyMode,
|
|
2247
|
+
alternateBuffer,
|
|
2248
|
+
alternateBufferEnabled,
|
|
2249
|
+
showModelSelector,
|
|
2250
|
+
]);
|
|
2251
|
+
|
|
2252
|
+
useHotkeys(hotkeyDefinitions, {
|
|
2253
|
+
enabled:
|
|
2254
|
+
!showModeSelector &&
|
|
2255
|
+
!showModelSelector &&
|
|
2256
|
+
!hasActiveApproval &&
|
|
2257
|
+
!showSessionManager &&
|
|
2258
|
+
!showHelpModal &&
|
|
2259
|
+
!showOnboarding &&
|
|
2260
|
+
!interactivePrompt &&
|
|
2261
|
+
!followupPrompt &&
|
|
2262
|
+
!pendingOperation,
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
//: Wire theme context to theme commands
|
|
2266
|
+
useEffect(() => {
|
|
2267
|
+
setThemeContext(themeContext);
|
|
2268
|
+
return () => setThemeContext(null);
|
|
2269
|
+
}, [themeContext]);
|
|
2270
|
+
|
|
2271
|
+
//: Initialize command registry once on mount
|
|
2272
|
+
const [commandRegistryVersion, setCommandRegistryVersion] = useState(0);
|
|
2273
|
+
const bumpCommandRegistryVersion = useCallback(
|
|
2274
|
+
() => setCommandRegistryVersion((prev) => prev + 1),
|
|
2275
|
+
[]
|
|
2276
|
+
);
|
|
2277
|
+
const commandRegistry = useMemo(() => createCommandRegistry(), []);
|
|
2278
|
+
|
|
2279
|
+
// ==========================================================================
|
|
2280
|
+
//: Plugin System Integration
|
|
2281
|
+
// ==========================================================================
|
|
2282
|
+
|
|
2283
|
+
// Plugin initialization state
|
|
2284
|
+
// Note: pluginResult can be used for status display (plugin count, errors)
|
|
2285
|
+
// Note: pluginsLoading can be used for loading indicator
|
|
2286
|
+
const [_pluginResult, setPluginResult] = useState<PluginInitResult | null>(null);
|
|
2287
|
+
const [_pluginsLoading, setPluginsLoading] = useState(true);
|
|
2288
|
+
|
|
2289
|
+
// Initialize plugins on mount
|
|
2290
|
+
useEffect(() => {
|
|
2291
|
+
let cancelled = false;
|
|
2292
|
+
|
|
2293
|
+
const loadPlugins = async () => {
|
|
2294
|
+
try {
|
|
2295
|
+
const result = await initializePlugins({
|
|
2296
|
+
projectRoot: process.cwd(),
|
|
2297
|
+
autoTrust: false,
|
|
2298
|
+
eagerLoad: false,
|
|
2299
|
+
includeBuiltin: true,
|
|
2300
|
+
includeUser: true,
|
|
2301
|
+
includeGlobal: true,
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
if (!cancelled) {
|
|
2305
|
+
setPluginResult(result);
|
|
2306
|
+
|
|
2307
|
+
// Register plugin commands into the registry
|
|
2308
|
+
registerPluginCommands(commandRegistry, result);
|
|
2309
|
+
bumpCommandRegistryVersion();
|
|
2310
|
+
|
|
2311
|
+
// Log plugin loading results
|
|
2312
|
+
if (result.errors.length > 0) {
|
|
2313
|
+
console.warn(
|
|
2314
|
+
`[plugins] Loaded ${result.pluginCount} plugins with ${result.errors.length} errors`
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
setPluginsLoading(false);
|
|
2319
|
+
}
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
if (!cancelled) {
|
|
2322
|
+
console.error("[plugins] Failed to initialize plugins:", error);
|
|
2323
|
+
setPluginsLoading(false);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
|
|
2328
|
+
void loadPlugins();
|
|
2329
|
+
|
|
2330
|
+
return () => {
|
|
2331
|
+
cancelled = true;
|
|
2332
|
+
disposePlugins();
|
|
2333
|
+
};
|
|
2334
|
+
}, [commandRegistry, bumpCommandRegistryVersion]);
|
|
2335
|
+
|
|
2336
|
+
// Load user-defined commands from ~/.vellum/commands
|
|
2337
|
+
useEffect(() => {
|
|
2338
|
+
let cancelled = false;
|
|
2339
|
+
|
|
2340
|
+
const loadUserCommands = async () => {
|
|
2341
|
+
try {
|
|
2342
|
+
const result = await registerUserCommands(commandRegistry);
|
|
2343
|
+
if (cancelled) return;
|
|
2344
|
+
|
|
2345
|
+
if (result.commands.length > 0) {
|
|
2346
|
+
bumpCommandRegistryVersion();
|
|
2347
|
+
}
|
|
2348
|
+
} catch (error) {
|
|
2349
|
+
if (!cancelled) {
|
|
2350
|
+
console.warn(
|
|
2351
|
+
"[user-commands] Failed to load commands:",
|
|
2352
|
+
error instanceof Error ? error.message : String(error)
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
|
|
2358
|
+
void loadUserCommands();
|
|
2359
|
+
|
|
2360
|
+
return () => {
|
|
2361
|
+
cancelled = true;
|
|
2362
|
+
};
|
|
2363
|
+
}, [commandRegistry, bumpCommandRegistryVersion]);
|
|
2364
|
+
|
|
2365
|
+
// ==========================================================================
|
|
2366
|
+
//: LSP Integration
|
|
2367
|
+
// ==========================================================================
|
|
2368
|
+
|
|
2369
|
+
// LSP initialization state
|
|
2370
|
+
const [_lspResult, setLspResult] = useState<LspIntegrationResult | null>(null);
|
|
2371
|
+
const [_lspLoading, setLspLoading] = useState(true);
|
|
2372
|
+
|
|
2373
|
+
// Initialize LSP on mount (non-blocking, graceful fallback)
|
|
2374
|
+
useEffect(() => {
|
|
2375
|
+
let cancelled = false;
|
|
2376
|
+
const isDebug = !!process.env.VELLUM_DEBUG;
|
|
2377
|
+
|
|
2378
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: LSP init with conditional logging
|
|
2379
|
+
const loadLsp = async () => {
|
|
2380
|
+
try {
|
|
2381
|
+
const result = await initializeLsp({
|
|
2382
|
+
workspaceRoot: process.cwd(),
|
|
2383
|
+
toolRegistry: toolRegistry as LspIntegrationOptions["toolRegistry"],
|
|
2384
|
+
autoInstall: true, // Auto-install missing language servers
|
|
2385
|
+
logger: isDebug
|
|
2386
|
+
? {
|
|
2387
|
+
debug: (msg) => console.debug(`[lsp] ${msg}`),
|
|
2388
|
+
info: (msg) => console.info(`[lsp] ${msg}`),
|
|
2389
|
+
warn: (msg) => console.warn(`[lsp] ${msg}`),
|
|
2390
|
+
error: (msg) => console.error(`[lsp] ${msg}`),
|
|
2391
|
+
}
|
|
2392
|
+
: undefined,
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
if (cancelled) return;
|
|
2396
|
+
|
|
2397
|
+
setLspResult(result);
|
|
2398
|
+
if (isDebug) {
|
|
2399
|
+
const msg = result.success
|
|
2400
|
+
? `[lsp] Initialized with ${result.toolCount} tools, ${result.availableServers.length} servers available`
|
|
2401
|
+
: `[lsp] Initialization skipped: ${result.error}`;
|
|
2402
|
+
console.debug(msg);
|
|
2403
|
+
}
|
|
2404
|
+
setLspLoading(false);
|
|
2405
|
+
} catch (error) {
|
|
2406
|
+
if (cancelled) return;
|
|
2407
|
+
// LSP is optional - log but don't fail
|
|
2408
|
+
if (isDebug) console.debug("[lsp] Failed to initialize (non-critical):", error);
|
|
2409
|
+
setLspLoading(false);
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
|
|
2413
|
+
void loadLsp();
|
|
2414
|
+
|
|
2415
|
+
return () => {
|
|
2416
|
+
cancelled = true;
|
|
2417
|
+
void disposeLsp();
|
|
2418
|
+
};
|
|
2419
|
+
}, [toolRegistry]);
|
|
2420
|
+
|
|
2421
|
+
const handleCommandEvent = useCallback(
|
|
2422
|
+
(event: string, data?: unknown) => {
|
|
2423
|
+
if (event === "app:exit") {
|
|
2424
|
+
// Show goodbye message
|
|
2425
|
+
addMessage({ role: "assistant", content: "Goodbye! See you next time." });
|
|
2426
|
+
// Give time for message to render, then exit
|
|
2427
|
+
setTimeout(() => {
|
|
2428
|
+
exit();
|
|
2429
|
+
setTimeout(() => process.exit(0), 50);
|
|
2430
|
+
}, 150);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
if (event === "session:resume") {
|
|
2435
|
+
const payload = data as ResumeSessionEventData | undefined;
|
|
2436
|
+
if (payload?.session) {
|
|
2437
|
+
switchToSession(payload.session.metadata.id, payload.session);
|
|
2438
|
+
setMessages([...toUIMessages(payload.session.messages)]);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
},
|
|
2442
|
+
[exit, addMessage, setMessages, switchToSession]
|
|
2443
|
+
);
|
|
2444
|
+
|
|
2445
|
+
const contextProviderRef = useRef<DefaultContextProvider | null>(null);
|
|
2446
|
+
|
|
2447
|
+
//: Create command executor with context provider
|
|
2448
|
+
const commandExecutor = useMemo(() => {
|
|
2449
|
+
if (!credentialManager) {
|
|
2450
|
+
return null;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
const contextProvider = createContextProvider({
|
|
2454
|
+
session: {
|
|
2455
|
+
id: activeSessionId,
|
|
2456
|
+
provider: currentProvider,
|
|
2457
|
+
cwd: process.cwd(),
|
|
2458
|
+
},
|
|
2459
|
+
credentials: credentialManager,
|
|
2460
|
+
toolRegistry,
|
|
2461
|
+
emit: handleCommandEvent,
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
contextProviderRef.current = contextProvider as DefaultContextProvider;
|
|
2465
|
+
return new CommandExecutor(commandRegistry, contextProvider);
|
|
2466
|
+
}, [
|
|
2467
|
+
commandRegistry,
|
|
2468
|
+
credentialManager,
|
|
2469
|
+
toolRegistry,
|
|
2470
|
+
activeSessionId,
|
|
2471
|
+
currentProvider,
|
|
2472
|
+
handleCommandEvent,
|
|
2473
|
+
]);
|
|
2474
|
+
|
|
2475
|
+
useEffect(() => {
|
|
2476
|
+
if (!contextProviderRef.current) {
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
contextProviderRef.current.updateSession({
|
|
2481
|
+
id: activeSessionId,
|
|
2482
|
+
provider: currentProvider,
|
|
2483
|
+
cwd: process.cwd(),
|
|
2484
|
+
});
|
|
2485
|
+
}, [activeSessionId, currentProvider]);
|
|
2486
|
+
|
|
2487
|
+
useEffect(() => {
|
|
2488
|
+
if (!commandExecutor) {
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
try {
|
|
2493
|
+
const batchWithExecutor = createBatchCommand(commandExecutor);
|
|
2494
|
+
commandRegistry.unregister(batchWithExecutor.name);
|
|
2495
|
+
commandRegistry.register(batchWithExecutor);
|
|
2496
|
+
bumpCommandRegistryVersion();
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
console.warn(
|
|
2499
|
+
"[commands] Failed to register batch command:",
|
|
2500
|
+
error instanceof Error ? error.message : String(error)
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
}, [commandExecutor, commandRegistry, bumpCommandRegistryVersion]);
|
|
2504
|
+
|
|
2505
|
+
useEffect(() => {
|
|
2506
|
+
if (!storageReady) {
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
const storage = storageManagerRef.current;
|
|
2511
|
+
const listService = sessionListServiceRef.current;
|
|
2512
|
+
|
|
2513
|
+
if (!storage || !listService) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
try {
|
|
2518
|
+
const resumeWithStorage = createResumeCommand(storage, listService);
|
|
2519
|
+
commandRegistry.unregister(resumeWithStorage.name);
|
|
2520
|
+
commandRegistry.register(resumeWithStorage);
|
|
2521
|
+
bumpCommandRegistryVersion();
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
console.warn(
|
|
2524
|
+
"[commands] Failed to register resume command:",
|
|
2525
|
+
error instanceof Error ? error.message : String(error)
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
}, [storageReady, commandRegistry, bumpCommandRegistryVersion]);
|
|
2529
|
+
|
|
2530
|
+
// Register search command when storage is ready
|
|
2531
|
+
useEffect(() => {
|
|
2532
|
+
if (!storageReady) {
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
const storage = storageManagerRef.current;
|
|
2537
|
+
const searchService = searchServiceRef.current;
|
|
2538
|
+
|
|
2539
|
+
if (!storage || !searchService) {
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
try {
|
|
2544
|
+
const searchWithStorage = createSearchCommand(storage, searchService);
|
|
2545
|
+
commandRegistry.unregister(searchWithStorage.name);
|
|
2546
|
+
commandRegistry.register(searchWithStorage);
|
|
2547
|
+
bumpCommandRegistryVersion();
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
console.warn(
|
|
2550
|
+
"[commands] Failed to register search command:",
|
|
2551
|
+
error instanceof Error ? error.message : String(error)
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
}, [storageReady, commandRegistry, bumpCommandRegistryVersion]);
|
|
2555
|
+
|
|
2556
|
+
// Register shutdown cleanup on mount
|
|
2557
|
+
useEffect(() => {
|
|
2558
|
+
setShutdownCleanup(() => {
|
|
2559
|
+
if (cancellationRef.current) {
|
|
2560
|
+
cancellationRef.current.cancel("shutdown");
|
|
2561
|
+
}
|
|
2562
|
+
// Save session on shutdown
|
|
2563
|
+
void saveSession();
|
|
2564
|
+
});
|
|
2565
|
+
|
|
2566
|
+
return () => {
|
|
2567
|
+
setShutdownCleanup(null);
|
|
2568
|
+
};
|
|
2569
|
+
}, [saveSession]);
|
|
2570
|
+
|
|
2571
|
+
const handleCommandResult = useCallback(
|
|
2572
|
+
async function process(result: CommandResult): Promise<void> {
|
|
2573
|
+
switch (result.kind) {
|
|
2574
|
+
case "success": {
|
|
2575
|
+
if (result.message) {
|
|
2576
|
+
addMessage({ role: "assistant", content: result.message });
|
|
2577
|
+
}
|
|
2578
|
+
if (result.clearScreen) {
|
|
2579
|
+
clearMessages();
|
|
2580
|
+
void clearSession();
|
|
2581
|
+
}
|
|
2582
|
+
break;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
case "error": {
|
|
2586
|
+
addMessage({
|
|
2587
|
+
role: "assistant",
|
|
2588
|
+
content: `[x] ${result.message}${
|
|
2589
|
+
result.suggestions ? `\n Did you mean: ${result.suggestions.join(", ")}?` : ""
|
|
2590
|
+
}`,
|
|
2591
|
+
});
|
|
2592
|
+
break;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
case "interactive": {
|
|
2596
|
+
setPromptValue("");
|
|
2597
|
+
setInteractivePrompt(result.prompt);
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
case "pending": {
|
|
2602
|
+
setPendingOperation(result.operation);
|
|
2603
|
+
try {
|
|
2604
|
+
const resolved = await result.operation.promise;
|
|
2605
|
+
await process(resolved);
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
addMessage({
|
|
2608
|
+
role: "assistant",
|
|
2609
|
+
content: `[x] ${error instanceof Error ? error.message : String(error)}`,
|
|
2610
|
+
});
|
|
2611
|
+
} finally {
|
|
2612
|
+
setPendingOperation(null);
|
|
2613
|
+
}
|
|
2614
|
+
break;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
},
|
|
2618
|
+
[addMessage, clearMessages, clearSession]
|
|
2619
|
+
);
|
|
2620
|
+
|
|
2621
|
+
const handlePromptSubmit = useCallback(async () => {
|
|
2622
|
+
if (!interactivePrompt) {
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
const prompt = interactivePrompt;
|
|
2627
|
+
const input = promptValue.trim();
|
|
2628
|
+
const resolvedValue = input === "" ? (prompt.defaultValue ?? "") : input;
|
|
2629
|
+
|
|
2630
|
+
setInteractivePrompt(null);
|
|
2631
|
+
setPromptValue("");
|
|
2632
|
+
|
|
2633
|
+
try {
|
|
2634
|
+
const result = await prompt.handler(resolvedValue);
|
|
2635
|
+
await handleCommandResult(result);
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
addMessage({
|
|
2638
|
+
role: "assistant",
|
|
2639
|
+
content: `[x] ${error instanceof Error ? error.message : String(error)}`,
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
}, [interactivePrompt, promptValue, handleCommandResult, addMessage]);
|
|
2643
|
+
|
|
2644
|
+
const handlePromptCancel = useCallback(() => {
|
|
2645
|
+
if (!interactivePrompt) {
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
const prompt = interactivePrompt;
|
|
2650
|
+
setInteractivePrompt(null);
|
|
2651
|
+
setPromptValue("");
|
|
2652
|
+
|
|
2653
|
+
if (prompt.onCancel) {
|
|
2654
|
+
void handleCommandResult(prompt.onCancel());
|
|
2655
|
+
}
|
|
2656
|
+
}, [interactivePrompt, handleCommandResult]);
|
|
2657
|
+
|
|
2658
|
+
const resolveFollowupResponse = useCallback(
|
|
2659
|
+
(rawValue: string, suggestions: readonly string[]): string => {
|
|
2660
|
+
const trimmed = rawValue.trim();
|
|
2661
|
+
let response = trimmed;
|
|
2662
|
+
|
|
2663
|
+
if (suggestions.length > 0 && trimmed.length > 0) {
|
|
2664
|
+
const index = Number.parseInt(trimmed, 10);
|
|
2665
|
+
if (!Number.isNaN(index) && index >= 1 && index <= suggestions.length) {
|
|
2666
|
+
response = suggestions[index - 1] ?? trimmed;
|
|
2667
|
+
} else {
|
|
2668
|
+
const match = suggestions.find(
|
|
2669
|
+
(option) => option.toLowerCase() === trimmed.toLowerCase()
|
|
2670
|
+
);
|
|
2671
|
+
if (match) {
|
|
2672
|
+
response = match;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
return response;
|
|
2678
|
+
},
|
|
2679
|
+
[]
|
|
2680
|
+
);
|
|
2681
|
+
|
|
2682
|
+
// Handle Ctrl+C and ESC for cancellation
|
|
2683
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: This handler must process multiple input types in a single callback for proper event handling
|
|
2684
|
+
useInput((inputChar, key) => {
|
|
2685
|
+
if (interactivePrompt) {
|
|
2686
|
+
if (key.escape) {
|
|
2687
|
+
handlePromptCancel();
|
|
2688
|
+
}
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
if (followupPrompt && key.escape) {
|
|
2693
|
+
agentLoopProp?.submitUserResponse("");
|
|
2694
|
+
setFollowupPrompt(null);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
if (pendingOperation) {
|
|
2699
|
+
if (key.escape && pendingOperation.cancel) {
|
|
2700
|
+
pendingOperation.cancel();
|
|
2701
|
+
setPendingOperation(null);
|
|
2702
|
+
}
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// Handle vim mode key processing
|
|
2707
|
+
if (vimEnabled && vim.enabled) {
|
|
2708
|
+
const vimAction = vim.handleKey(inputChar, { ctrl: key.ctrl, shift: key.shift });
|
|
2709
|
+
if (vimAction) {
|
|
2710
|
+
// Vim action was processed - announce mode changes
|
|
2711
|
+
if (vimAction.type === "mode") {
|
|
2712
|
+
announce(`Vim: ${vimAction.target} mode`);
|
|
2713
|
+
}
|
|
2714
|
+
// Don't process further if vim handled it (unless it's a mode exit to NORMAL)
|
|
2715
|
+
if (vimAction.type !== "mode" || vimAction.target !== "NORMAL") {
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// Handle copy mode navigation
|
|
2722
|
+
if (copyMode.state.active) {
|
|
2723
|
+
if (key.escape) {
|
|
2724
|
+
copyMode.exitCopyMode();
|
|
2725
|
+
announce("Copy mode exited");
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (key.upArrow) {
|
|
2729
|
+
copyMode.expandSelection("up");
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
if (key.downArrow) {
|
|
2733
|
+
copyMode.expandSelection("down");
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
if (key.leftArrow) {
|
|
2737
|
+
copyMode.expandSelection("left");
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
if (key.rightArrow) {
|
|
2741
|
+
copyMode.expandSelection("right");
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
// Enter to copy selection
|
|
2745
|
+
if (key.return) {
|
|
2746
|
+
// Get content as 2D array from messages
|
|
2747
|
+
const content = messages.map((m) => m.content.split(""));
|
|
2748
|
+
void copyMode.copySelection(content).then(() => {
|
|
2749
|
+
announce("Selection copied to clipboard");
|
|
2750
|
+
copyMode.exitCopyMode();
|
|
2751
|
+
});
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// ESC - cancel operation, exit copy mode, or exit app
|
|
2757
|
+
if (key.escape) {
|
|
2758
|
+
if (isLoading && cancellationRef.current) {
|
|
2759
|
+
// Cancel running operation
|
|
2760
|
+
cancellationRef.current.cancel("user_escape");
|
|
2761
|
+
setIsLoading(false);
|
|
2762
|
+
addMessage({ role: "assistant", content: "[Operation cancelled]" });
|
|
2763
|
+
} else {
|
|
2764
|
+
// No operation running, exit app
|
|
2765
|
+
addMessage({ role: "assistant", content: "Goodbye! See you next time." });
|
|
2766
|
+
setTimeout(() => {
|
|
2767
|
+
exit();
|
|
2768
|
+
setTimeout(() => process.exit(0), 50);
|
|
2769
|
+
}, 150);
|
|
2770
|
+
}
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// Ctrl+C - cancel operation when loading; otherwise exit
|
|
2775
|
+
if (key.ctrl && inputChar === "c") {
|
|
2776
|
+
if (isLoading && cancellationRef.current) {
|
|
2777
|
+
cancellationRef.current.cancel("user_ctrl_c");
|
|
2778
|
+
setIsLoading(false);
|
|
2779
|
+
addMessage({ role: "assistant", content: "[Operation cancelled by Ctrl+C]" });
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
addMessage({ role: "assistant", content: "Goodbye! See you next time." });
|
|
2784
|
+
setTimeout(() => {
|
|
2785
|
+
exit();
|
|
2786
|
+
setTimeout(() => process.exit(0), 50);
|
|
2787
|
+
}, 150);
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
//: Handle slash command detection and execution
|
|
2793
|
+
const handleSlashCommand = useCallback(
|
|
2794
|
+
async (text: string): Promise<boolean> => {
|
|
2795
|
+
if (!text.trim().startsWith("/")) {
|
|
2796
|
+
return false; // Not a slash command
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
if (!commandExecutor) {
|
|
2800
|
+
const normalized = text.trim().toLowerCase();
|
|
2801
|
+
const isExitCommand =
|
|
2802
|
+
normalized === "/exit" ||
|
|
2803
|
+
normalized === "/quit" ||
|
|
2804
|
+
normalized === "/q" ||
|
|
2805
|
+
normalized.startsWith("/exit ") ||
|
|
2806
|
+
normalized.startsWith("/quit ") ||
|
|
2807
|
+
normalized.startsWith("/q ");
|
|
2808
|
+
|
|
2809
|
+
if (isExitCommand) {
|
|
2810
|
+
addMessage({ role: "assistant", content: "Goodbye! See you next time." });
|
|
2811
|
+
setTimeout(() => {
|
|
2812
|
+
exit();
|
|
2813
|
+
setTimeout(() => process.exit(0), 50);
|
|
2814
|
+
}, 150);
|
|
2815
|
+
return true;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
addMessage({
|
|
2819
|
+
role: "assistant",
|
|
2820
|
+
content: "[x] Command system not ready yet. Please try again in a moment.",
|
|
2821
|
+
});
|
|
2822
|
+
return true;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
try {
|
|
2826
|
+
const result = await commandExecutor.execute(text);
|
|
2827
|
+
await handleCommandResult(result);
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
addMessage({
|
|
2830
|
+
role: "assistant",
|
|
2831
|
+
content: `[x] ${error instanceof Error ? error.message : String(error)}`,
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
return true; // Was a slash command
|
|
2836
|
+
},
|
|
2837
|
+
[commandExecutor, addMessage, handleCommandResult, exit]
|
|
2838
|
+
);
|
|
2839
|
+
|
|
2840
|
+
// Handle message submission (for CommandInput onMessage)
|
|
2841
|
+
const handleMessage = useCallback(
|
|
2842
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex message handling with multiple code paths
|
|
2843
|
+
async (text: string) => {
|
|
2844
|
+
if (!text.trim()) return;
|
|
2845
|
+
|
|
2846
|
+
if (followupPrompt && agentLoopProp) {
|
|
2847
|
+
const response = resolveFollowupResponse(text, followupPrompt.suggestions);
|
|
2848
|
+
setFollowupPrompt(null);
|
|
2849
|
+
addToHistory(response);
|
|
2850
|
+
addMessage({ role: "user", content: response });
|
|
2851
|
+
announce(`You said: ${response}`);
|
|
2852
|
+
agentLoopProp.submitUserResponse(response);
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// Fix 4: Reset turn usage when a new turn starts
|
|
2857
|
+
setTurnUsage({
|
|
2858
|
+
inputTokens: 0,
|
|
2859
|
+
outputTokens: 0,
|
|
2860
|
+
thinkingTokens: 0,
|
|
2861
|
+
cacheReadTokens: 0,
|
|
2862
|
+
cacheWriteTokens: 0,
|
|
2863
|
+
});
|
|
2864
|
+
|
|
2865
|
+
// Apply coding-mode handler transformations (vibe/plan/spec) and keep UI phase
|
|
2866
|
+
// progress in sync with SpecModeHandler's injected metadata.
|
|
2867
|
+
let processedText = text;
|
|
2868
|
+
try {
|
|
2869
|
+
const handlerResult = await modeManager.processMessage({
|
|
2870
|
+
content: text,
|
|
2871
|
+
timestamp: Date.now(),
|
|
2872
|
+
});
|
|
2873
|
+
|
|
2874
|
+
const modified = handlerResult.modifiedMessage;
|
|
2875
|
+
if (modified?.content) {
|
|
2876
|
+
processedText = modified.content;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
const phaseNumber = modified?.metadata?.phaseNumber;
|
|
2880
|
+
if (typeof phaseNumber === "number" && Number.isFinite(phaseNumber)) {
|
|
2881
|
+
setSpecPhase(phaseNumber);
|
|
2882
|
+
} else if (currentMode !== "spec") {
|
|
2883
|
+
// Keep PhaseProgressIndicator stable when leaving spec mode.
|
|
2884
|
+
setSpecPhase(1);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
if (handlerResult.requiresCheckpoint) {
|
|
2888
|
+
// Pause downstream processing until the user approves the checkpoint.
|
|
2889
|
+
setPromptValue("");
|
|
2890
|
+
setInteractivePrompt({
|
|
2891
|
+
inputType: "confirm",
|
|
2892
|
+
message: "Checkpoint required. Continue?",
|
|
2893
|
+
defaultValue: "n",
|
|
2894
|
+
handler: async (value: string): Promise<CommandResult> => {
|
|
2895
|
+
const confirmed = value.toLowerCase() === "y" || value.toLowerCase() === "yes";
|
|
2896
|
+
if (!confirmed) {
|
|
2897
|
+
return { kind: "success", message: "Checkpoint declined." };
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
// Advance plan/spec handler state when possible.
|
|
2901
|
+
const checkpointResult = await modeManager.processMessage({
|
|
2902
|
+
content: "yes",
|
|
2903
|
+
timestamp: Date.now(),
|
|
2904
|
+
metadata: { advancePhase: true },
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
const checkpointPhase = checkpointResult.modifiedMessage?.metadata?.phaseNumber;
|
|
2908
|
+
if (typeof checkpointPhase === "number" && Number.isFinite(checkpointPhase)) {
|
|
2909
|
+
setSpecPhase(checkpointPhase);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
return { kind: "success", message: "Checkpoint approved." };
|
|
2913
|
+
},
|
|
2914
|
+
onCancel: () => ({ kind: "success", message: "Checkpoint cancelled." }),
|
|
2915
|
+
});
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
} catch (error) {
|
|
2919
|
+
// Mode handling should never block primary chat; fall back to raw text.
|
|
2920
|
+
console.warn(
|
|
2921
|
+
"[mode] Failed to process message through mode handler:",
|
|
2922
|
+
error instanceof Error ? error.message : String(error)
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// Add to input history
|
|
2927
|
+
addToHistory(processedText);
|
|
2928
|
+
|
|
2929
|
+
addMessage({ role: "user", content: processedText });
|
|
2930
|
+
|
|
2931
|
+
// Announce for screen reader
|
|
2932
|
+
announce(`You said: ${processedText}`);
|
|
2933
|
+
|
|
2934
|
+
const effectiveThinking = getEffectiveThinkingConfig(
|
|
2935
|
+
BUILTIN_CODING_MODES[currentMode]?.extendedThinking
|
|
2936
|
+
);
|
|
2937
|
+
if (effectiveThinking.enabled) {
|
|
2938
|
+
const modelInfo = getModelInfo(currentProvider, currentModel);
|
|
2939
|
+
const warningKey = `${currentProvider}/${currentModel}`;
|
|
2940
|
+
if (!modelInfo.supportsReasoning && !thinkingWarningRef.current.has(warningKey)) {
|
|
2941
|
+
addMessage({
|
|
2942
|
+
role: "assistant",
|
|
2943
|
+
content:
|
|
2944
|
+
`⚠️ Thinking mode is enabled, but ${currentProvider}/${modelInfo.name} ` +
|
|
2945
|
+
"does not support reasoning. Running this request without thinking.",
|
|
2946
|
+
});
|
|
2947
|
+
thinkingWarningRef.current.add(warningKey);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
setIsLoading(true);
|
|
2952
|
+
// Thinking content is now integrated into the streaming message
|
|
2953
|
+
// via the agent-adapter's handleThinking function.
|
|
2954
|
+
|
|
2955
|
+
// Use AgentLoop if available
|
|
2956
|
+
if (agentLoopProp) {
|
|
2957
|
+
// Wire cancellation to AgentLoop
|
|
2958
|
+
cancellationRef.current = {
|
|
2959
|
+
cancel: (reason) => agentLoopProp.cancel(reason),
|
|
2960
|
+
get isCancelled() {
|
|
2961
|
+
const state = agentLoopProp.getState();
|
|
2962
|
+
return state === "terminated" || state === "shutdown";
|
|
2963
|
+
},
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
try {
|
|
2967
|
+
agentLoopProp.addMessage(createUserMessage([SessionParts.text(processedText)]));
|
|
2968
|
+
|
|
2969
|
+
// Wrap agentLoop.run() with resilience (circuit breaker + rate limiter)
|
|
2970
|
+
if (resilientProvider) {
|
|
2971
|
+
const result = await resilientProvider.execute(currentProvider, () =>
|
|
2972
|
+
agentLoopProp.run()
|
|
2973
|
+
);
|
|
2974
|
+
if (!result.success && result.error) {
|
|
2975
|
+
// Check if circuit is open or rate limited
|
|
2976
|
+
const circuitState = resilientProvider.getCircuitState(currentProvider);
|
|
2977
|
+
if (circuitState === "OPEN") {
|
|
2978
|
+
addMessage({
|
|
2979
|
+
role: "assistant",
|
|
2980
|
+
content: `⚠️ Provider ${currentProvider} circuit breaker is open. Too many failures recently.`,
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
throw result.error;
|
|
2984
|
+
}
|
|
2985
|
+
} else {
|
|
2986
|
+
// Fallback to direct execution if resilient provider not ready
|
|
2987
|
+
await agentLoopProp.run();
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// Messages are synced via AgentLoop adapter event handlers (handleText)
|
|
2991
|
+
// User message was already added via addMessage() above
|
|
2992
|
+
// Notify on completion
|
|
2993
|
+
notifyTaskComplete("Response received");
|
|
2994
|
+
announce("Response received");
|
|
2995
|
+
} catch (err) {
|
|
2996
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2997
|
+
// Log resilience metrics for debugging
|
|
2998
|
+
if (resilientProvider) {
|
|
2999
|
+
const stats = resilientProvider.getRateLimiterStats();
|
|
3000
|
+
console.debug("[Resilience] Stats:", stats);
|
|
3001
|
+
}
|
|
3002
|
+
notifyError(errorMsg);
|
|
3003
|
+
addMessage({ role: "assistant", content: `[x] Error: ${errorMsg}` });
|
|
3004
|
+
} finally {
|
|
3005
|
+
setIsLoading(false);
|
|
3006
|
+
cancellationRef.current = null;
|
|
3007
|
+
}
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// Fallback: Create a simple cancellation controller for simulated operation
|
|
3012
|
+
let cancelled = false;
|
|
3013
|
+
cancellationRef.current = {
|
|
3014
|
+
cancel: (reason) => {
|
|
3015
|
+
cancelled = true;
|
|
3016
|
+
console.log(`[Cancel] ${reason ?? "user request"}`);
|
|
3017
|
+
},
|
|
3018
|
+
get isCancelled() {
|
|
3019
|
+
return cancelled;
|
|
3020
|
+
},
|
|
3021
|
+
};
|
|
3022
|
+
|
|
3023
|
+
// Simulated response (with cancellation check) - fallback when no AgentLoop
|
|
3024
|
+
await new Promise<void>((resolve) => {
|
|
3025
|
+
const timeoutId = setTimeout(() => {
|
|
3026
|
+
if (!cancelled) {
|
|
3027
|
+
setTimeout(() => {
|
|
3028
|
+
if (!cancelled) {
|
|
3029
|
+
addMessage({ role: "assistant", content: `[Echo] ${processedText}` });
|
|
3030
|
+
notifyTaskComplete("Response received");
|
|
3031
|
+
announce("Response received");
|
|
3032
|
+
}
|
|
3033
|
+
resolve();
|
|
3034
|
+
}, 300);
|
|
3035
|
+
} else {
|
|
3036
|
+
resolve();
|
|
3037
|
+
}
|
|
3038
|
+
}, 500);
|
|
3039
|
+
|
|
3040
|
+
// Check for cancellation
|
|
3041
|
+
const checkInterval = setInterval(() => {
|
|
3042
|
+
if (cancelled) {
|
|
3043
|
+
clearTimeout(timeoutId);
|
|
3044
|
+
clearInterval(checkInterval);
|
|
3045
|
+
resolve();
|
|
3046
|
+
}
|
|
3047
|
+
}, 50);
|
|
3048
|
+
});
|
|
3049
|
+
|
|
3050
|
+
setIsLoading(false);
|
|
3051
|
+
cancellationRef.current = null;
|
|
3052
|
+
},
|
|
3053
|
+
[
|
|
3054
|
+
addToHistory,
|
|
3055
|
+
addMessage,
|
|
3056
|
+
announce,
|
|
3057
|
+
agentLoopProp,
|
|
3058
|
+
currentModel,
|
|
3059
|
+
currentMode,
|
|
3060
|
+
currentProvider,
|
|
3061
|
+
followupPrompt,
|
|
3062
|
+
modeManager,
|
|
3063
|
+
notifyTaskComplete,
|
|
3064
|
+
notifyError,
|
|
3065
|
+
resolveFollowupResponse,
|
|
3066
|
+
resilientProvider,
|
|
3067
|
+
]
|
|
3068
|
+
);
|
|
3069
|
+
|
|
3070
|
+
// Handle slash command submission (for CommandInput onCommand)
|
|
3071
|
+
const handleCommand = useCallback(
|
|
3072
|
+
async (command: SlashCommand) => {
|
|
3073
|
+
// Add to history
|
|
3074
|
+
addToHistory(command.raw);
|
|
3075
|
+
|
|
3076
|
+
// Execute via the command executor
|
|
3077
|
+
const wasCommand = await handleSlashCommand(command.raw);
|
|
3078
|
+
if (!wasCommand) {
|
|
3079
|
+
addMessage({ role: "assistant", content: `Unknown command: /${command.name}` });
|
|
3080
|
+
}
|
|
3081
|
+
},
|
|
3082
|
+
[addToHistory, addMessage, handleSlashCommand]
|
|
3083
|
+
);
|
|
3084
|
+
|
|
3085
|
+
// Category order and labels for grouped slash command menu
|
|
3086
|
+
const categoryOrder = useMemo(
|
|
3087
|
+
() => ["system", "session", "navigation", "tools", "config", "auth", "debug"] as const,
|
|
3088
|
+
[]
|
|
3089
|
+
);
|
|
3090
|
+
|
|
3091
|
+
const categoryLabels = useMemo(
|
|
3092
|
+
() => ({
|
|
3093
|
+
system: "System",
|
|
3094
|
+
session: "Session",
|
|
3095
|
+
navigation: "Navigation",
|
|
3096
|
+
tools: "Tools",
|
|
3097
|
+
config: "Config",
|
|
3098
|
+
auth: "Authentication",
|
|
3099
|
+
debug: "Debug",
|
|
3100
|
+
}),
|
|
3101
|
+
[]
|
|
3102
|
+
);
|
|
3103
|
+
|
|
3104
|
+
// Get available command options for CommandInput autocomplete (structured with categories)
|
|
3105
|
+
const commandOptions = useMemo((): AutocompleteOption[] => {
|
|
3106
|
+
// Recompute when commands are registered dynamically (plugins/user commands).
|
|
3107
|
+
void commandRegistryVersion;
|
|
3108
|
+
|
|
3109
|
+
const options: AutocompleteOption[] = [];
|
|
3110
|
+
const seenNames = new Set<string>();
|
|
3111
|
+
|
|
3112
|
+
for (const cmd of commandRegistry.list()) {
|
|
3113
|
+
// Skip aliases as separate entries - they clutter the menu
|
|
3114
|
+
// (aliases still work when typed directly)
|
|
3115
|
+
if (!seenNames.has(cmd.name)) {
|
|
3116
|
+
seenNames.add(cmd.name);
|
|
3117
|
+
options.push({
|
|
3118
|
+
name: cmd.name,
|
|
3119
|
+
description: cmd.description,
|
|
3120
|
+
category: cmd.category,
|
|
3121
|
+
aliases: cmd.aliases,
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
return options;
|
|
3127
|
+
}, [commandRegistry, commandRegistryVersion]);
|
|
3128
|
+
|
|
3129
|
+
// Get subcommands for a command (for two-level autocomplete)
|
|
3130
|
+
const getSubcommands = useCallback(
|
|
3131
|
+
(commandName: string): AutocompleteOption[] | undefined => {
|
|
3132
|
+
const cmd = commandRegistry.get(commandName);
|
|
3133
|
+
if (!cmd?.subcommands || cmd.subcommands.length === 0) {
|
|
3134
|
+
return undefined;
|
|
3135
|
+
}
|
|
3136
|
+
return cmd.subcommands.map((sub) => ({
|
|
3137
|
+
name: sub.name,
|
|
3138
|
+
description: sub.description,
|
|
3139
|
+
}));
|
|
3140
|
+
},
|
|
3141
|
+
[commandRegistry]
|
|
3142
|
+
);
|
|
3143
|
+
|
|
3144
|
+
// Get level 3 items for three-level autocomplete (e.g., /model anthropic claude-)
|
|
3145
|
+
const getLevel3Items = useCallback(
|
|
3146
|
+
(commandName: string, arg1: string, partial: string): AutocompleteOption[] | undefined => {
|
|
3147
|
+
// /model command: level 3 shows model IDs for the selected provider
|
|
3148
|
+
if (commandName === "model") {
|
|
3149
|
+
// arg1 is the provider name
|
|
3150
|
+
const models = getProviderModels(arg1);
|
|
3151
|
+
if (models.length === 0) {
|
|
3152
|
+
return undefined;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// Filter models by partial match
|
|
3156
|
+
const lowerPartial = partial.toLowerCase();
|
|
3157
|
+
const filtered = lowerPartial
|
|
3158
|
+
? models.filter(
|
|
3159
|
+
(m) =>
|
|
3160
|
+
m.id.toLowerCase().includes(lowerPartial) ||
|
|
3161
|
+
m.name.toLowerCase().includes(lowerPartial)
|
|
3162
|
+
)
|
|
3163
|
+
: models;
|
|
3164
|
+
|
|
3165
|
+
return filtered.map((m) => ({
|
|
3166
|
+
name: m.id,
|
|
3167
|
+
description: m.name,
|
|
3168
|
+
category: arg1,
|
|
3169
|
+
}));
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// /auth command: level 3 shows provider list for set/clear subcommands
|
|
3173
|
+
if (commandName === "auth") {
|
|
3174
|
+
const setAliases = ["set", "add", "login"];
|
|
3175
|
+
const clearAliases = ["clear", "remove", "delete", "logout"];
|
|
3176
|
+
|
|
3177
|
+
// Only show providers for set/clear subcommands (not for status)
|
|
3178
|
+
if (setAliases.includes(arg1) || clearAliases.includes(arg1)) {
|
|
3179
|
+
const providers = [
|
|
3180
|
+
"anthropic",
|
|
3181
|
+
"openai",
|
|
3182
|
+
"google",
|
|
3183
|
+
"azure",
|
|
3184
|
+
"bedrock",
|
|
3185
|
+
"vertex",
|
|
3186
|
+
"ollama",
|
|
3187
|
+
"openrouter",
|
|
3188
|
+
"together",
|
|
3189
|
+
"mistral",
|
|
3190
|
+
"cohere",
|
|
3191
|
+
"groq",
|
|
3192
|
+
"deepseek",
|
|
3193
|
+
"qwen",
|
|
3194
|
+
"xai",
|
|
3195
|
+
];
|
|
3196
|
+
|
|
3197
|
+
const lowerPartial = partial.toLowerCase();
|
|
3198
|
+
const filtered = lowerPartial
|
|
3199
|
+
? providers.filter((p) => p.toLowerCase().startsWith(lowerPartial))
|
|
3200
|
+
: providers;
|
|
3201
|
+
|
|
3202
|
+
return filtered.map((p) => ({
|
|
3203
|
+
name: p,
|
|
3204
|
+
description: `Configure ${p} API key`,
|
|
3205
|
+
}));
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
return undefined; // status doesn't need level 3
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
return undefined;
|
|
3212
|
+
},
|
|
3213
|
+
[]
|
|
3214
|
+
);
|
|
3215
|
+
|
|
3216
|
+
// Handle permission dialog responses
|
|
3217
|
+
const handleApprove = useCallback(() => {
|
|
3218
|
+
if (!activeApproval) {
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
announce("Tool execution approved");
|
|
3223
|
+
approveActive("once");
|
|
3224
|
+
}, [activeApproval, approveActive, announce]);
|
|
3225
|
+
|
|
3226
|
+
const handleApproveAlways = useCallback(() => {
|
|
3227
|
+
if (!activeApproval) {
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
announce("Tool execution approved (always)");
|
|
3232
|
+
approveActive("always");
|
|
3233
|
+
}, [activeApproval, approveActive, announce]);
|
|
3234
|
+
|
|
3235
|
+
const handleReject = useCallback(() => {
|
|
3236
|
+
if (!activeApproval) {
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
announce("Tool execution rejected");
|
|
3241
|
+
rejectActive();
|
|
3242
|
+
}, [activeApproval, rejectActive, announce]);
|
|
3243
|
+
|
|
3244
|
+
const requestModeSwitch = useCallback(
|
|
3245
|
+
async (mode: CodingMode) => {
|
|
3246
|
+
const result = await modeManager.switchMode(mode);
|
|
3247
|
+
|
|
3248
|
+
if (result.success) {
|
|
3249
|
+
setShowModeSelector(false);
|
|
3250
|
+
process.env.VELLUM_MODE = mode;
|
|
3251
|
+
announce(`Mode changed to ${mode}`);
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
if (result.requiresConfirmation) {
|
|
3256
|
+
setShowModeSelector(false);
|
|
3257
|
+
openSpecConfirmation();
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
addMessage({
|
|
3262
|
+
role: "assistant",
|
|
3263
|
+
content: `Unable to switch to ${mode}: ${result.reason ?? "Unknown error"}`,
|
|
3264
|
+
});
|
|
3265
|
+
},
|
|
3266
|
+
[modeManager, openSpecConfirmation, addMessage, announce]
|
|
3267
|
+
);
|
|
3268
|
+
|
|
3269
|
+
// Handle mode selection with persistence (FIX 5)
|
|
3270
|
+
const handleModeSelect = useCallback(
|
|
3271
|
+
(mode: CodingMode) => {
|
|
3272
|
+
void requestModeSwitch(mode);
|
|
3273
|
+
},
|
|
3274
|
+
[requestModeSwitch]
|
|
3275
|
+
);
|
|
3276
|
+
|
|
3277
|
+
// Handle model selection
|
|
3278
|
+
const handleModelSelect = useCallback(
|
|
3279
|
+
(selectedProvider: string, selectedModel: string) => {
|
|
3280
|
+
setCurrentProvider(selectedProvider);
|
|
3281
|
+
setCurrentModel(selectedModel);
|
|
3282
|
+
setShowModelSelector(false);
|
|
3283
|
+
announce(`Model changed to ${selectedModel} (${selectedProvider})`);
|
|
3284
|
+
},
|
|
3285
|
+
[announce]
|
|
3286
|
+
);
|
|
3287
|
+
|
|
3288
|
+
useEffect(() => {
|
|
3289
|
+
setModelCommandConfig(currentProvider, currentModel, handleModelSelect);
|
|
3290
|
+
}, [currentProvider, currentModel, handleModelSelect]);
|
|
3291
|
+
|
|
3292
|
+
// Handle onboarding completion
|
|
3293
|
+
const handleOnboardingComplete = useCallback(
|
|
3294
|
+
(result: { provider: string; mode: string; credentialsConfigured: boolean }) => {
|
|
3295
|
+
// Note: OnboardingWizard.saveConfig() already persists completed state
|
|
3296
|
+
setIsFirstRun(false);
|
|
3297
|
+
setShowOnboarding(false);
|
|
3298
|
+
setCurrentProvider(result.provider);
|
|
3299
|
+
// Task 4: Sync model with provider selection
|
|
3300
|
+
const defaultModel = getDefaultModelForProvider(result.provider);
|
|
3301
|
+
setCurrentModel(defaultModel);
|
|
3302
|
+
setCurrentMode(result.mode as CodingMode);
|
|
3303
|
+
void modeManager.forceSwitch(result.mode as CodingMode);
|
|
3304
|
+
// Task 5: Persist configuration (environment-based for now, config file in production)
|
|
3305
|
+
process.env.VELLUM_PROVIDER = result.provider;
|
|
3306
|
+
process.env.VELLUM_MODEL = defaultModel;
|
|
3307
|
+
process.env.VELLUM_MODE = result.mode;
|
|
3308
|
+
announce("Welcome to Vellum! Onboarding complete.");
|
|
3309
|
+
},
|
|
3310
|
+
[announce, modeManager]
|
|
3311
|
+
);
|
|
3312
|
+
|
|
3313
|
+
// Get context window for the current model
|
|
3314
|
+
const contextWindow = useMemo(
|
|
3315
|
+
() => getContextWindow(currentProvider, currentModel),
|
|
3316
|
+
[currentProvider, currentModel]
|
|
3317
|
+
);
|
|
3318
|
+
|
|
3319
|
+
// ==========================================================================
|
|
3320
|
+
// FIX 1: Subscribe to AgentLoop usage events for real token counting
|
|
3321
|
+
// ==========================================================================
|
|
3322
|
+
useEffect(() => {
|
|
3323
|
+
if (!agentLoopProp) {
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
// Handle real usage events from AgentLoop
|
|
3328
|
+
const handleUsage = (usage: {
|
|
3329
|
+
inputTokens: number;
|
|
3330
|
+
outputTokens: number;
|
|
3331
|
+
thinkingTokens?: number;
|
|
3332
|
+
cacheReadTokens?: number;
|
|
3333
|
+
cacheWriteTokens?: number;
|
|
3334
|
+
}) => {
|
|
3335
|
+
// Update turn usage (per-turn tracking)
|
|
3336
|
+
setTurnUsage({
|
|
3337
|
+
inputTokens: usage.inputTokens,
|
|
3338
|
+
outputTokens: usage.outputTokens,
|
|
3339
|
+
thinkingTokens: usage.thinkingTokens ?? 0,
|
|
3340
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
3341
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
3342
|
+
});
|
|
3343
|
+
|
|
3344
|
+
// Update cumulative usage
|
|
3345
|
+
setTokenUsage((prev) => {
|
|
3346
|
+
const newUsage = {
|
|
3347
|
+
inputTokens: prev.inputTokens + usage.inputTokens,
|
|
3348
|
+
outputTokens: prev.outputTokens + usage.outputTokens,
|
|
3349
|
+
thinkingTokens: prev.thinkingTokens + (usage.thinkingTokens ?? 0),
|
|
3350
|
+
cacheReadTokens: prev.cacheReadTokens + (usage.cacheReadTokens ?? 0),
|
|
3351
|
+
cacheWriteTokens: prev.cacheWriteTokens + (usage.cacheWriteTokens ?? 0),
|
|
3352
|
+
totalCost: calculateCost(
|
|
3353
|
+
currentProvider,
|
|
3354
|
+
currentModel,
|
|
3355
|
+
prev.inputTokens + usage.inputTokens,
|
|
3356
|
+
prev.outputTokens + usage.outputTokens
|
|
3357
|
+
),
|
|
3358
|
+
};
|
|
3359
|
+
tokenUsageRef.current = newUsage;
|
|
3360
|
+
return newUsage;
|
|
3361
|
+
});
|
|
3362
|
+
|
|
3363
|
+
// Track usage in cost service
|
|
3364
|
+
costService.trackUsage(
|
|
3365
|
+
{
|
|
3366
|
+
inputTokens: usage.inputTokens,
|
|
3367
|
+
outputTokens: usage.outputTokens,
|
|
3368
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
3369
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
3370
|
+
thinkingTokens: usage.thinkingTokens ?? 0,
|
|
3371
|
+
},
|
|
3372
|
+
currentModel,
|
|
3373
|
+
currentProvider
|
|
3374
|
+
);
|
|
3375
|
+
};
|
|
3376
|
+
|
|
3377
|
+
agentLoopProp.on("usage", handleUsage);
|
|
3378
|
+
|
|
3379
|
+
return () => {
|
|
3380
|
+
agentLoopProp.off("usage", handleUsage);
|
|
3381
|
+
};
|
|
3382
|
+
}, [agentLoopProp, currentModel, currentProvider, costService]);
|
|
3383
|
+
|
|
3384
|
+
// Handle user prompt requests from ask_followup_question (GAP 1)
|
|
3385
|
+
useEffect(() => {
|
|
3386
|
+
if (!agentLoopProp) {
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
const handleUserPromptRequired = (prompt: { question: string; suggestions?: string[] }) => {
|
|
3391
|
+
const suggestions = prompt.suggestions ?? [];
|
|
3392
|
+
|
|
3393
|
+
setFollowupPrompt({
|
|
3394
|
+
question: prompt.question,
|
|
3395
|
+
suggestions,
|
|
3396
|
+
});
|
|
3397
|
+
};
|
|
3398
|
+
|
|
3399
|
+
agentLoopProp.on("userPrompt:required", handleUserPromptRequired);
|
|
3400
|
+
|
|
3401
|
+
return () => {
|
|
3402
|
+
agentLoopProp.off("userPrompt:required", handleUserPromptRequired);
|
|
3403
|
+
};
|
|
3404
|
+
}, [agentLoopProp]);
|
|
3405
|
+
|
|
3406
|
+
// Fallback: Update token usage from messages when no AgentLoop (simulated)
|
|
3407
|
+
useEffect(() => {
|
|
3408
|
+
// Only use fallback when no agentLoop is provided
|
|
3409
|
+
if (agentLoopProp) {
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// Approximate token count: ~4 chars per token (fallback only)
|
|
3414
|
+
const inputChars = messages
|
|
3415
|
+
.filter((m) => m.role === "user")
|
|
3416
|
+
.reduce((sum, m) => sum + m.content.length, 0);
|
|
3417
|
+
const outputChars = messages
|
|
3418
|
+
.filter((m) => m.role === "assistant")
|
|
3419
|
+
.reduce((sum, m) => sum + m.content.length, 0);
|
|
3420
|
+
|
|
3421
|
+
const inputTokens = Math.ceil(inputChars / 4);
|
|
3422
|
+
const outputTokens = Math.ceil(outputChars / 4);
|
|
3423
|
+
const totalCost = calculateCost(currentProvider, currentModel, inputTokens, outputTokens);
|
|
3424
|
+
|
|
3425
|
+
setTokenUsage((prev) => ({
|
|
3426
|
+
...prev,
|
|
3427
|
+
inputTokens,
|
|
3428
|
+
outputTokens,
|
|
3429
|
+
totalCost,
|
|
3430
|
+
}));
|
|
3431
|
+
}, [messages, currentProvider, currentModel, agentLoopProp]);
|
|
3432
|
+
|
|
3433
|
+
// Calculate total tokens for StatusBar
|
|
3434
|
+
const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;
|
|
3435
|
+
|
|
3436
|
+
const dismissUpdateBanner = useCallback(() => {
|
|
3437
|
+
setUpdateAvailable(null);
|
|
3438
|
+
}, []);
|
|
3439
|
+
|
|
3440
|
+
const cancelOnboarding = useCallback(() => {
|
|
3441
|
+
setShowOnboarding(false);
|
|
3442
|
+
}, []);
|
|
3443
|
+
|
|
3444
|
+
const closeSessionManager = useCallback(() => {
|
|
3445
|
+
setShowSessionManager(false);
|
|
3446
|
+
}, []);
|
|
3447
|
+
|
|
3448
|
+
const handleSessionSelected = useCallback(
|
|
3449
|
+
(id: string) => {
|
|
3450
|
+
announce(`Selected session: ${id}`);
|
|
3451
|
+
switchToSession(id);
|
|
3452
|
+
setShowSessionManager(false);
|
|
3453
|
+
},
|
|
3454
|
+
[announce, switchToSession]
|
|
3455
|
+
);
|
|
3456
|
+
|
|
3457
|
+
const loadSessionPreviewMessages = useCallback(
|
|
3458
|
+
async (sessionId: string): Promise<readonly SessionPreviewMessage[] | null> => {
|
|
3459
|
+
const storage = storageManagerRef.current;
|
|
3460
|
+
if (!storage) {
|
|
3461
|
+
return null;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
try {
|
|
3465
|
+
const session = await storage.load(sessionId);
|
|
3466
|
+
const messages = session.messages;
|
|
3467
|
+
|
|
3468
|
+
// Keep preview lightweight: show last few messages only.
|
|
3469
|
+
const tail = messages.slice(Math.max(0, messages.length - 6));
|
|
3470
|
+
|
|
3471
|
+
return tail
|
|
3472
|
+
.map((message) => {
|
|
3473
|
+
const content = getTextContent(message).trim();
|
|
3474
|
+
if (!content) {
|
|
3475
|
+
return null;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
const role: SessionPreviewMessage["role"] =
|
|
3479
|
+
message.role === "tool_result"
|
|
3480
|
+
? "tool"
|
|
3481
|
+
: message.role === "user"
|
|
3482
|
+
? "user"
|
|
3483
|
+
: message.role === "assistant"
|
|
3484
|
+
? "assistant"
|
|
3485
|
+
: "system";
|
|
3486
|
+
|
|
3487
|
+
return {
|
|
3488
|
+
id: message.id,
|
|
3489
|
+
role,
|
|
3490
|
+
content,
|
|
3491
|
+
timestamp: new Date(message.metadata.createdAt),
|
|
3492
|
+
};
|
|
3493
|
+
})
|
|
3494
|
+
.filter((msg): msg is NonNullable<typeof msg> => msg !== null);
|
|
3495
|
+
} catch {
|
|
3496
|
+
return null;
|
|
3497
|
+
}
|
|
3498
|
+
},
|
|
3499
|
+
[]
|
|
3500
|
+
);
|
|
3501
|
+
|
|
3502
|
+
const promptPlaceholder = useMemo(() => {
|
|
3503
|
+
if (!interactivePrompt) {
|
|
3504
|
+
return "";
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
if (interactivePrompt.placeholder) {
|
|
3508
|
+
return interactivePrompt.placeholder;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
if (interactivePrompt.inputType === "confirm") {
|
|
3512
|
+
return interactivePrompt.defaultValue?.toLowerCase() === "y" ? "Y/n" : "y/N";
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
if (interactivePrompt.inputType === "select" && interactivePrompt.options?.length) {
|
|
3516
|
+
return `Choose 1-${interactivePrompt.options.length}`;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
return "";
|
|
3520
|
+
}, [interactivePrompt]);
|
|
3521
|
+
|
|
3522
|
+
// Get agent level from AgentConfig via registry
|
|
3523
|
+
const agentName = BUILTIN_CODING_MODES[currentMode].agentName;
|
|
3524
|
+
|
|
3525
|
+
return (
|
|
3526
|
+
<AppContentView
|
|
3527
|
+
agentName={agentName}
|
|
3528
|
+
announce={announce}
|
|
3529
|
+
alternateBufferEnabled={alternateBufferEnabled}
|
|
3530
|
+
activeApproval={activeApproval}
|
|
3531
|
+
activeRiskLevel={activeRiskLevel}
|
|
3532
|
+
activeSessionId={activeSessionId}
|
|
3533
|
+
backtrackState={backtrackState}
|
|
3534
|
+
bannerCycleDurationMs={bannerCycleDurationMs}
|
|
3535
|
+
bannerCycles={bannerCycles}
|
|
3536
|
+
bannerDisplayDurationMs={bannerDisplayDurationMs}
|
|
3537
|
+
bannerSplashComplete={bannerSplashComplete}
|
|
3538
|
+
bannerUpdateIntervalMs={bannerUpdateIntervalMs}
|
|
3539
|
+
branches={branches}
|
|
3540
|
+
cancelOnboarding={cancelOnboarding}
|
|
3541
|
+
closeSessionManager={closeSessionManager}
|
|
3542
|
+
commandOptions={commandOptions}
|
|
3543
|
+
credentialManager={credentialManager}
|
|
3544
|
+
getSubcommands={getSubcommands}
|
|
3545
|
+
getLevel3Items={getLevel3Items}
|
|
3546
|
+
categoryOrder={categoryOrder}
|
|
3547
|
+
categoryLabels={categoryLabels}
|
|
3548
|
+
checkpointDiff={checkpointDiff}
|
|
3549
|
+
closeCheckpointDiff={closeCheckpointDiff}
|
|
3550
|
+
onOpenCheckpointDiff={openCheckpointDiff}
|
|
3551
|
+
contextWindow={contextWindow}
|
|
3552
|
+
currentMode={currentMode}
|
|
3553
|
+
currentModel={currentModel}
|
|
3554
|
+
currentProvider={currentProvider}
|
|
3555
|
+
currentTip={currentTip}
|
|
3556
|
+
dismissTip={dismissTip}
|
|
3557
|
+
dismissUpdateBanner={dismissUpdateBanner}
|
|
3558
|
+
handleApprove={handleApprove}
|
|
3559
|
+
handleApproveAlways={handleApproveAlways}
|
|
3560
|
+
handleBannerComplete={handleBannerComplete}
|
|
3561
|
+
handleCommand={handleCommand}
|
|
3562
|
+
handleCreateBacktrackBranch={handleCreateBacktrackBranch}
|
|
3563
|
+
handleMessage={handleMessage}
|
|
3564
|
+
handleModeSelect={handleModeSelect}
|
|
3565
|
+
handleModelSelect={handleModelSelect}
|
|
3566
|
+
handleOnboardingComplete={handleOnboardingComplete}
|
|
3567
|
+
handlePromptSubmit={handlePromptSubmit}
|
|
3568
|
+
handleReject={handleReject}
|
|
3569
|
+
handleSessionSelected={handleSessionSelected}
|
|
3570
|
+
handleSwitchBacktrackBranch={handleSwitchBacktrackBranch}
|
|
3571
|
+
initError={initError}
|
|
3572
|
+
followupPrompt={followupPrompt}
|
|
3573
|
+
interactivePrompt={interactivePrompt}
|
|
3574
|
+
loadSessionPreviewMessages={loadSessionPreviewMessages}
|
|
3575
|
+
isLoading={isLoading}
|
|
3576
|
+
thinkingModeEnabled={thinkingModeEnabled}
|
|
3577
|
+
memoryEntries={memoryEntries}
|
|
3578
|
+
messages={messages}
|
|
3579
|
+
pendingMessage={pendingMessage}
|
|
3580
|
+
pendingOperation={pendingOperation}
|
|
3581
|
+
promptPlaceholder={promptPlaceholder}
|
|
3582
|
+
promptValue={promptValue}
|
|
3583
|
+
setPromptValue={setPromptValue}
|
|
3584
|
+
sessions={sessions}
|
|
3585
|
+
suppressPromptEnter={suppressPromptEnter}
|
|
3586
|
+
shouldShowBanner={shouldShowBanner}
|
|
3587
|
+
showModeSelector={showModeSelector}
|
|
3588
|
+
showModelSelector={showModelSelector}
|
|
3589
|
+
showOnboarding={showOnboarding}
|
|
3590
|
+
showSessionManager={showSessionManager}
|
|
3591
|
+
showHelpModal={showHelpModal}
|
|
3592
|
+
closeHelpModal={() => setShowHelpModal(false)}
|
|
3593
|
+
showApprovalQueue={showApprovalQueue}
|
|
3594
|
+
closeApprovalQueue={() => setShowApprovalQueue(false)}
|
|
3595
|
+
pendingApprovals={pendingApproval}
|
|
3596
|
+
onApproveQueueItem={(id) => approveExecution(id)}
|
|
3597
|
+
onRejectQueueItem={(id) => rejectExecution(id)}
|
|
3598
|
+
onApproveAll={() => approveAll()}
|
|
3599
|
+
onRejectAll={() =>
|
|
3600
|
+
pendingApproval.forEach((e) => {
|
|
3601
|
+
rejectExecution(e.id);
|
|
3602
|
+
})
|
|
3603
|
+
}
|
|
3604
|
+
showSidebar={showSidebar}
|
|
3605
|
+
sidebarContent={sidebarContent}
|
|
3606
|
+
specPhase={specPhase}
|
|
3607
|
+
themeContext={themeContext}
|
|
3608
|
+
taskChain={taskChain}
|
|
3609
|
+
currentTaskId={currentTaskId}
|
|
3610
|
+
todoItems={todoItems}
|
|
3611
|
+
refreshTodos={refreshTodos}
|
|
3612
|
+
toolRegistry={toolRegistry}
|
|
3613
|
+
tokenUsage={tokenUsage}
|
|
3614
|
+
turnUsage={turnUsage}
|
|
3615
|
+
totalTokens={totalTokens}
|
|
3616
|
+
trustMode={trustMode}
|
|
3617
|
+
undoBacktrack={undoBacktrack}
|
|
3618
|
+
updateAvailable={updateAvailable}
|
|
3619
|
+
redoBacktrack={redoBacktrack}
|
|
3620
|
+
workspace={workspaceName}
|
|
3621
|
+
branch={gitBranch}
|
|
3622
|
+
changedFiles={gitChangedFiles}
|
|
3623
|
+
persistence={{
|
|
3624
|
+
status: persistence.status,
|
|
3625
|
+
unsavedCount: persistence.unsavedCount,
|
|
3626
|
+
lastSavedAt: persistence.lastSavedAt,
|
|
3627
|
+
}}
|
|
3628
|
+
snapshots={snapshots}
|
|
3629
|
+
providerStatus={providerStatus}
|
|
3630
|
+
costWarningState={costWarningState}
|
|
3631
|
+
autoApprovalState={autoApprovalState}
|
|
3632
|
+
vimEnabled={vimEnabled}
|
|
3633
|
+
vimMode={vim.mode}
|
|
3634
|
+
/>
|
|
3635
|
+
);
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
type ThemeContextValue = ReturnType<typeof useTheme>;
|
|
3639
|
+
type TipValue = ReturnType<typeof useTipEngine>["currentTip"];
|
|
3640
|
+
type ToolApprovalState = ReturnType<typeof useToolApprovalController>;
|
|
3641
|
+
|
|
3642
|
+
interface AppContentViewProps {
|
|
3643
|
+
readonly agentName?: string;
|
|
3644
|
+
readonly announce: (message: string) => void;
|
|
3645
|
+
readonly alternateBufferEnabled: boolean;
|
|
3646
|
+
readonly activeApproval: ToolApprovalState["activeApproval"];
|
|
3647
|
+
readonly activeRiskLevel: ToolApprovalState["activeRiskLevel"];
|
|
3648
|
+
readonly activeSessionId: string;
|
|
3649
|
+
readonly backtrackState: ReturnType<typeof useBacktrack>["backtrackState"];
|
|
3650
|
+
readonly bannerCycleDurationMs: number;
|
|
3651
|
+
readonly bannerCycles: number;
|
|
3652
|
+
readonly bannerDisplayDurationMs: number;
|
|
3653
|
+
readonly bannerSplashComplete: boolean;
|
|
3654
|
+
readonly bannerUpdateIntervalMs: number;
|
|
3655
|
+
readonly branches: ReturnType<typeof useBacktrack>["branches"];
|
|
3656
|
+
readonly cancelOnboarding: () => void;
|
|
3657
|
+
readonly closeSessionManager: () => void;
|
|
3658
|
+
readonly commandOptions: readonly AutocompleteOption[];
|
|
3659
|
+
readonly credentialManager: CredentialManager | null;
|
|
3660
|
+
readonly getSubcommands: (commandName: string) => AutocompleteOption[] | undefined;
|
|
3661
|
+
readonly getLevel3Items: (
|
|
3662
|
+
commandName: string,
|
|
3663
|
+
arg1: string,
|
|
3664
|
+
partial: string
|
|
3665
|
+
) => AutocompleteOption[] | undefined;
|
|
3666
|
+
readonly categoryOrder: readonly string[];
|
|
3667
|
+
readonly categoryLabels: Record<string, string>;
|
|
3668
|
+
readonly checkpointDiff: {
|
|
3669
|
+
content: string;
|
|
3670
|
+
snapshotHash?: string;
|
|
3671
|
+
isLoading: boolean;
|
|
3672
|
+
isVisible: boolean;
|
|
3673
|
+
};
|
|
3674
|
+
readonly closeCheckpointDiff: () => void;
|
|
3675
|
+
readonly onOpenCheckpointDiff: (hash: string) => void;
|
|
3676
|
+
readonly contextWindow: number;
|
|
3677
|
+
readonly currentMode: CodingMode;
|
|
3678
|
+
readonly currentModel: string;
|
|
3679
|
+
readonly currentProvider: string;
|
|
3680
|
+
readonly currentTip: TipValue;
|
|
3681
|
+
readonly dismissTip: () => void;
|
|
3682
|
+
readonly dismissUpdateBanner: () => void;
|
|
3683
|
+
readonly handleApprove: () => void;
|
|
3684
|
+
readonly handleApproveAlways: () => void;
|
|
3685
|
+
readonly handleBannerComplete: () => void;
|
|
3686
|
+
readonly handleCommand: (command: SlashCommand) => void;
|
|
3687
|
+
readonly handleCreateBacktrackBranch: () => void;
|
|
3688
|
+
readonly handleMessage: (text: string) => void;
|
|
3689
|
+
readonly handleModeSelect: (mode: CodingMode) => void;
|
|
3690
|
+
readonly handleModelSelect: (selectedProvider: string, selectedModel: string) => void;
|
|
3691
|
+
readonly handleOnboardingComplete: (result: {
|
|
3692
|
+
provider: string;
|
|
3693
|
+
mode: string;
|
|
3694
|
+
credentialsConfigured: boolean;
|
|
3695
|
+
}) => void;
|
|
3696
|
+
readonly handlePromptSubmit: () => void;
|
|
3697
|
+
readonly handleReject: () => void;
|
|
3698
|
+
readonly handleSessionSelected: (id: string) => void;
|
|
3699
|
+
readonly handleSwitchBacktrackBranch: (branchId: string) => void;
|
|
3700
|
+
readonly followupPrompt: { question: string; suggestions: string[] } | null;
|
|
3701
|
+
readonly interactivePrompt: InteractivePrompt | null;
|
|
3702
|
+
readonly loadSessionPreviewMessages: (
|
|
3703
|
+
sessionId: string
|
|
3704
|
+
) => Promise<readonly SessionPreviewMessage[] | null>;
|
|
3705
|
+
readonly initError?: Error;
|
|
3706
|
+
readonly isLoading: boolean;
|
|
3707
|
+
readonly thinkingModeEnabled: boolean;
|
|
3708
|
+
readonly memoryEntries: MemoryPanelProps["entries"];
|
|
3709
|
+
readonly messages: readonly Message[];
|
|
3710
|
+
readonly pendingMessage: Message | null;
|
|
3711
|
+
readonly pendingOperation: AsyncOperation | null;
|
|
3712
|
+
readonly promptPlaceholder: string;
|
|
3713
|
+
readonly promptValue: string;
|
|
3714
|
+
readonly setPromptValue: (value: string) => void;
|
|
3715
|
+
readonly sessions: SessionMetadata[];
|
|
3716
|
+
readonly suppressPromptEnter: boolean;
|
|
3717
|
+
readonly shouldShowBanner: boolean;
|
|
3718
|
+
readonly showModeSelector: boolean;
|
|
3719
|
+
readonly showModelSelector: boolean;
|
|
3720
|
+
readonly showOnboarding: boolean;
|
|
3721
|
+
readonly showSessionManager: boolean;
|
|
3722
|
+
readonly showHelpModal: boolean;
|
|
3723
|
+
readonly closeHelpModal: () => void;
|
|
3724
|
+
readonly showApprovalQueue: boolean;
|
|
3725
|
+
readonly closeApprovalQueue: () => void;
|
|
3726
|
+
readonly pendingApprovals: readonly ToolExecution[];
|
|
3727
|
+
readonly onApproveQueueItem: (id: string) => void;
|
|
3728
|
+
readonly onRejectQueueItem: (id: string) => void;
|
|
3729
|
+
readonly onApproveAll: () => void;
|
|
3730
|
+
readonly onRejectAll: () => void;
|
|
3731
|
+
readonly showSidebar: boolean;
|
|
3732
|
+
readonly sidebarContent: SidebarContent;
|
|
3733
|
+
readonly specPhase: number;
|
|
3734
|
+
readonly themeContext: ThemeContextValue;
|
|
3735
|
+
readonly taskChain: TaskChain | null;
|
|
3736
|
+
readonly currentTaskId?: string;
|
|
3737
|
+
readonly todoItems: readonly TodoItemData[];
|
|
3738
|
+
readonly refreshTodos: () => void;
|
|
3739
|
+
readonly toolRegistry: ToolRegistry;
|
|
3740
|
+
readonly tokenUsage: {
|
|
3741
|
+
inputTokens: number;
|
|
3742
|
+
outputTokens: number;
|
|
3743
|
+
thinkingTokens: number;
|
|
3744
|
+
cacheReadTokens: number;
|
|
3745
|
+
cacheWriteTokens: number;
|
|
3746
|
+
totalCost: number;
|
|
3747
|
+
};
|
|
3748
|
+
readonly turnUsage: {
|
|
3749
|
+
inputTokens: number;
|
|
3750
|
+
outputTokens: number;
|
|
3751
|
+
thinkingTokens: number;
|
|
3752
|
+
cacheReadTokens: number;
|
|
3753
|
+
cacheWriteTokens: number;
|
|
3754
|
+
};
|
|
3755
|
+
readonly totalTokens: number;
|
|
3756
|
+
readonly trustMode: TrustMode;
|
|
3757
|
+
readonly undoBacktrack: () => void;
|
|
3758
|
+
readonly redoBacktrack: () => void;
|
|
3759
|
+
readonly updateAvailable: { current: string; latest: string } | null;
|
|
3760
|
+
/** Workspace name for header separator */
|
|
3761
|
+
readonly workspace: string;
|
|
3762
|
+
/** Git branch for header separator */
|
|
3763
|
+
readonly branch: string | null;
|
|
3764
|
+
/** Number of changed files for header separator */
|
|
3765
|
+
readonly changedFiles: number;
|
|
3766
|
+
/** Persistence status for session save indicator */
|
|
3767
|
+
readonly persistence?: {
|
|
3768
|
+
status: PersistenceStatus;
|
|
3769
|
+
unsavedCount: number;
|
|
3770
|
+
lastSavedAt: Date | null;
|
|
3771
|
+
};
|
|
3772
|
+
/** Snapshots hook result for checkpoint panel */
|
|
3773
|
+
readonly snapshots: ReturnType<typeof useSnapshots>;
|
|
3774
|
+
/** Provider status for ModelStatusBar */
|
|
3775
|
+
readonly providerStatus: ReturnType<typeof useProviderStatus>;
|
|
3776
|
+
/** Cost warning state */
|
|
3777
|
+
readonly costWarningState: {
|
|
3778
|
+
show: boolean;
|
|
3779
|
+
limitReached: boolean;
|
|
3780
|
+
percentUsed: number;
|
|
3781
|
+
costLimit: number;
|
|
3782
|
+
requestLimit: number;
|
|
3783
|
+
};
|
|
3784
|
+
/** Auto-approval status */
|
|
3785
|
+
readonly autoApprovalState: {
|
|
3786
|
+
consecutiveRequests: number;
|
|
3787
|
+
requestLimit: number;
|
|
3788
|
+
consecutiveCost: number;
|
|
3789
|
+
costLimit: number;
|
|
3790
|
+
requestPercentUsed: number;
|
|
3791
|
+
costPercentUsed: number;
|
|
3792
|
+
limitReached: boolean;
|
|
3793
|
+
limitType?: "requests" | "cost";
|
|
3794
|
+
} | null;
|
|
3795
|
+
/** Whether vim mode is enabled */
|
|
3796
|
+
readonly vimEnabled: boolean;
|
|
3797
|
+
/** Current vim mode */
|
|
3798
|
+
readonly vimMode: VimMode;
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
function renderSidebarContent({
|
|
3802
|
+
announce,
|
|
3803
|
+
showSidebar,
|
|
3804
|
+
sidebarContent,
|
|
3805
|
+
todoItems,
|
|
3806
|
+
refreshTodos,
|
|
3807
|
+
memoryEntries,
|
|
3808
|
+
toolRegistry,
|
|
3809
|
+
persistence,
|
|
3810
|
+
snapshots,
|
|
3811
|
+
taskChain,
|
|
3812
|
+
currentTaskId,
|
|
3813
|
+
onOpenCheckpointDiff,
|
|
3814
|
+
}: {
|
|
3815
|
+
readonly announce: (message: string) => void;
|
|
3816
|
+
readonly showSidebar: boolean;
|
|
3817
|
+
readonly sidebarContent: SidebarContent;
|
|
3818
|
+
readonly todoItems: readonly TodoItemData[];
|
|
3819
|
+
readonly refreshTodos: () => void;
|
|
3820
|
+
readonly memoryEntries: MemoryPanelProps["entries"];
|
|
3821
|
+
readonly toolRegistry: ToolRegistry;
|
|
3822
|
+
readonly persistence?: {
|
|
3823
|
+
status: PersistenceStatus;
|
|
3824
|
+
unsavedCount: number;
|
|
3825
|
+
lastSavedAt: Date | null;
|
|
3826
|
+
};
|
|
3827
|
+
readonly snapshots: ReturnType<typeof useSnapshots>;
|
|
3828
|
+
readonly taskChain: TaskChain | null;
|
|
3829
|
+
readonly currentTaskId?: string;
|
|
3830
|
+
readonly onOpenCheckpointDiff: (hash: string) => void;
|
|
3831
|
+
}): React.ReactNode | undefined {
|
|
3832
|
+
if (!showSidebar) return undefined;
|
|
3833
|
+
|
|
3834
|
+
let panelContent: React.ReactNode;
|
|
3835
|
+
|
|
3836
|
+
if (sidebarContent === "todo") {
|
|
3837
|
+
panelContent = (
|
|
3838
|
+
<TodoPanel
|
|
3839
|
+
items={todoItems}
|
|
3840
|
+
isFocused={showSidebar}
|
|
3841
|
+
maxHeight={20}
|
|
3842
|
+
onRefresh={refreshTodos}
|
|
3843
|
+
onActivateItem={(item) => {
|
|
3844
|
+
announce(`Selected: ${item.title}`);
|
|
3845
|
+
}}
|
|
3846
|
+
/>
|
|
3847
|
+
);
|
|
3848
|
+
} else if (sidebarContent === "tools") {
|
|
3849
|
+
panelContent = <ToolsPanel isFocused={showSidebar} maxItems={20} />;
|
|
3850
|
+
} else if (sidebarContent === "mcp") {
|
|
3851
|
+
panelContent = <McpPanel isFocused={showSidebar} toolRegistry={toolRegistry} />;
|
|
3852
|
+
} else if (sidebarContent === "snapshots") {
|
|
3853
|
+
panelContent = (
|
|
3854
|
+
<SnapshotCheckpointPanel
|
|
3855
|
+
snapshots={snapshots.snapshots}
|
|
3856
|
+
isLoading={snapshots.isLoading}
|
|
3857
|
+
error={snapshots.error}
|
|
3858
|
+
isInitialized={snapshots.isInitialized}
|
|
3859
|
+
isFocused={showSidebar}
|
|
3860
|
+
maxHeight={20}
|
|
3861
|
+
onRestore={async (hash) => {
|
|
3862
|
+
const result = await snapshots.restore(hash);
|
|
3863
|
+
if (result.success) {
|
|
3864
|
+
announce(`Restored ${result.files.length} files from checkpoint`);
|
|
3865
|
+
} else {
|
|
3866
|
+
announce(`Restore failed: ${result.error}`);
|
|
3867
|
+
}
|
|
3868
|
+
}}
|
|
3869
|
+
onDiff={async (hash) => {
|
|
3870
|
+
onOpenCheckpointDiff(hash);
|
|
3871
|
+
}}
|
|
3872
|
+
onTakeCheckpoint={async () => {
|
|
3873
|
+
try {
|
|
3874
|
+
await snapshots.take("Manual checkpoint");
|
|
3875
|
+
announce("Checkpoint created");
|
|
3876
|
+
} catch (err) {
|
|
3877
|
+
announce(
|
|
3878
|
+
`Failed to create checkpoint: ${err instanceof Error ? err.message : String(err)}`
|
|
3879
|
+
);
|
|
3880
|
+
}
|
|
3881
|
+
}}
|
|
3882
|
+
onRefresh={() => void snapshots.refresh()}
|
|
3883
|
+
/>
|
|
3884
|
+
);
|
|
3885
|
+
} else {
|
|
3886
|
+
panelContent = <MemoryPanel entries={memoryEntries} isFocused={showSidebar} maxHeight={20} />;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
return (
|
|
3890
|
+
<Box flexDirection="column" height="100%">
|
|
3891
|
+
<Box flexGrow={1}>{panelContent}</Box>
|
|
3892
|
+
{taskChain && taskChain.nodes.size > 0 && (
|
|
3893
|
+
<Box marginTop={1}>
|
|
3894
|
+
<MaxSizedBox maxHeight={12} truncationIndicator="... (more tasks)">
|
|
3895
|
+
<AgentProgress
|
|
3896
|
+
chain={taskChain}
|
|
3897
|
+
currentTaskId={currentTaskId}
|
|
3898
|
+
showDetails={false}
|
|
3899
|
+
progressBarWidth={12}
|
|
3900
|
+
/>
|
|
3901
|
+
</MaxSizedBox>
|
|
3902
|
+
</Box>
|
|
3903
|
+
)}
|
|
3904
|
+
<SystemStatusPanel compact={false} persistence={persistence} />
|
|
3905
|
+
</Box>
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
interface AppOverlaysProps {
|
|
3910
|
+
readonly activeApproval: ToolApprovalState["activeApproval"];
|
|
3911
|
+
readonly activeRiskLevel: ToolApprovalState["activeRiskLevel"];
|
|
3912
|
+
readonly activeSessionId: string;
|
|
3913
|
+
readonly checkpointDiff: {
|
|
3914
|
+
content: string;
|
|
3915
|
+
snapshotHash?: string;
|
|
3916
|
+
isLoading: boolean;
|
|
3917
|
+
isVisible: boolean;
|
|
3918
|
+
};
|
|
3919
|
+
readonly closeCheckpointDiff: () => void;
|
|
3920
|
+
readonly closeSessionManager: () => void;
|
|
3921
|
+
readonly currentMode: CodingMode;
|
|
3922
|
+
readonly currentModel: string;
|
|
3923
|
+
readonly currentProvider: string;
|
|
3924
|
+
readonly dismissUpdateBanner: () => void;
|
|
3925
|
+
readonly handleApprove: () => void;
|
|
3926
|
+
readonly handleApproveAlways: () => void;
|
|
3927
|
+
readonly handleModeSelect: (mode: CodingMode) => void;
|
|
3928
|
+
readonly handleModelSelect: (selectedProvider: string, selectedModel: string) => void;
|
|
3929
|
+
readonly handleReject: () => void;
|
|
3930
|
+
readonly handleSessionSelected: (id: string) => void;
|
|
3931
|
+
readonly loadSessionPreviewMessages: (
|
|
3932
|
+
sessionId: string
|
|
3933
|
+
) => Promise<readonly import("./tui/components/session/types.js").SessionPreviewMessage[] | null>;
|
|
3934
|
+
readonly pendingOperation: AsyncOperation | null;
|
|
3935
|
+
readonly sessions: SessionMetadata[];
|
|
3936
|
+
readonly showModeSelector: boolean;
|
|
3937
|
+
readonly showModelSelector: boolean;
|
|
3938
|
+
readonly showSessionManager: boolean;
|
|
3939
|
+
readonly showHelpModal: boolean;
|
|
3940
|
+
readonly closeHelpModal: () => void;
|
|
3941
|
+
readonly showApprovalQueue: boolean;
|
|
3942
|
+
readonly closeApprovalQueue: () => void;
|
|
3943
|
+
readonly pendingApprovals: readonly ToolExecution[];
|
|
3944
|
+
readonly onApproveQueueItem: (id: string) => void;
|
|
3945
|
+
readonly onRejectQueueItem: (id: string) => void;
|
|
3946
|
+
readonly onApproveAll: () => void;
|
|
3947
|
+
readonly onRejectAll: () => void;
|
|
3948
|
+
readonly themeContext: ThemeContextValue;
|
|
3949
|
+
readonly updateAvailable: { current: string; latest: string } | null;
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
function AppOverlays({
|
|
3953
|
+
activeApproval,
|
|
3954
|
+
activeRiskLevel,
|
|
3955
|
+
activeSessionId,
|
|
3956
|
+
checkpointDiff,
|
|
3957
|
+
closeCheckpointDiff,
|
|
3958
|
+
closeSessionManager,
|
|
3959
|
+
currentMode,
|
|
3960
|
+
currentModel,
|
|
3961
|
+
currentProvider,
|
|
3962
|
+
dismissUpdateBanner,
|
|
3963
|
+
handleApprove,
|
|
3964
|
+
handleApproveAlways,
|
|
3965
|
+
handleModeSelect,
|
|
3966
|
+
handleModelSelect,
|
|
3967
|
+
handleReject,
|
|
3968
|
+
handleSessionSelected,
|
|
3969
|
+
loadSessionPreviewMessages,
|
|
3970
|
+
pendingOperation,
|
|
3971
|
+
sessions,
|
|
3972
|
+
showModeSelector,
|
|
3973
|
+
showModelSelector,
|
|
3974
|
+
showSessionManager,
|
|
3975
|
+
showHelpModal,
|
|
3976
|
+
closeHelpModal,
|
|
3977
|
+
showApprovalQueue,
|
|
3978
|
+
closeApprovalQueue: _closeApprovalQueue,
|
|
3979
|
+
pendingApprovals,
|
|
3980
|
+
onApproveQueueItem,
|
|
3981
|
+
onRejectQueueItem,
|
|
3982
|
+
onApproveAll,
|
|
3983
|
+
onRejectAll,
|
|
3984
|
+
themeContext,
|
|
3985
|
+
updateAvailable,
|
|
3986
|
+
}: AppOverlaysProps): React.JSX.Element {
|
|
3987
|
+
return (
|
|
3988
|
+
<>
|
|
3989
|
+
{updateAvailable && (
|
|
3990
|
+
<UpdateBanner
|
|
3991
|
+
currentVersion={updateAvailable.current}
|
|
3992
|
+
latestVersion={updateAvailable.latest}
|
|
3993
|
+
dismissible
|
|
3994
|
+
onDismiss={dismissUpdateBanner}
|
|
3995
|
+
compact
|
|
3996
|
+
/>
|
|
3997
|
+
)}
|
|
3998
|
+
|
|
3999
|
+
{showModeSelector && (
|
|
4000
|
+
<Box
|
|
4001
|
+
position="absolute"
|
|
4002
|
+
marginTop={5}
|
|
4003
|
+
marginLeft={10}
|
|
4004
|
+
borderStyle="round"
|
|
4005
|
+
borderColor={themeContext.theme.colors.info}
|
|
4006
|
+
padding={1}
|
|
4007
|
+
>
|
|
4008
|
+
<ModeSelector
|
|
4009
|
+
currentMode={currentMode}
|
|
4010
|
+
onSelect={handleModeSelect}
|
|
4011
|
+
isActive={showModeSelector}
|
|
4012
|
+
showDescriptions
|
|
4013
|
+
/>
|
|
4014
|
+
</Box>
|
|
4015
|
+
)}
|
|
4016
|
+
|
|
4017
|
+
{showSessionManager && (
|
|
4018
|
+
<Box
|
|
4019
|
+
position="absolute"
|
|
4020
|
+
marginTop={3}
|
|
4021
|
+
marginLeft={5}
|
|
4022
|
+
borderStyle="round"
|
|
4023
|
+
borderColor={themeContext.theme.colors.primary}
|
|
4024
|
+
padding={1}
|
|
4025
|
+
>
|
|
4026
|
+
<SessionPicker
|
|
4027
|
+
sessions={sessions}
|
|
4028
|
+
activeSessionId={activeSessionId}
|
|
4029
|
+
loadPreviewMessages={loadSessionPreviewMessages}
|
|
4030
|
+
onSelect={handleSessionSelected}
|
|
4031
|
+
onClose={closeSessionManager}
|
|
4032
|
+
isOpen={showSessionManager}
|
|
4033
|
+
/>
|
|
4034
|
+
</Box>
|
|
4035
|
+
)}
|
|
4036
|
+
|
|
4037
|
+
{activeApproval && (
|
|
4038
|
+
<Box
|
|
4039
|
+
position="absolute"
|
|
4040
|
+
marginTop={5}
|
|
4041
|
+
marginLeft={10}
|
|
4042
|
+
borderStyle="double"
|
|
4043
|
+
borderColor={themeContext.theme.colors.warning}
|
|
4044
|
+
padding={1}
|
|
4045
|
+
>
|
|
4046
|
+
<PermissionDialog
|
|
4047
|
+
execution={activeApproval}
|
|
4048
|
+
riskLevel={activeRiskLevel}
|
|
4049
|
+
onApprove={handleApprove}
|
|
4050
|
+
onApproveAlways={handleApproveAlways}
|
|
4051
|
+
onReject={handleReject}
|
|
4052
|
+
isFocused
|
|
4053
|
+
/>
|
|
4054
|
+
</Box>
|
|
4055
|
+
)}
|
|
4056
|
+
|
|
4057
|
+
{showModelSelector && (
|
|
4058
|
+
<Box
|
|
4059
|
+
position="absolute"
|
|
4060
|
+
marginTop={5}
|
|
4061
|
+
marginLeft={10}
|
|
4062
|
+
borderStyle="round"
|
|
4063
|
+
borderColor={themeContext.theme.colors.success}
|
|
4064
|
+
padding={1}
|
|
4065
|
+
>
|
|
4066
|
+
<ModelSelector
|
|
4067
|
+
currentModel={currentModel}
|
|
4068
|
+
currentProvider={currentProvider}
|
|
4069
|
+
onSelect={handleModelSelect}
|
|
4070
|
+
isActive={showModelSelector}
|
|
4071
|
+
showDetails
|
|
4072
|
+
/>
|
|
4073
|
+
</Box>
|
|
4074
|
+
)}
|
|
4075
|
+
|
|
4076
|
+
{pendingOperation && (
|
|
4077
|
+
<Box
|
|
4078
|
+
position="absolute"
|
|
4079
|
+
marginTop={4}
|
|
4080
|
+
marginLeft={8}
|
|
4081
|
+
borderStyle="round"
|
|
4082
|
+
borderColor={themeContext.theme.colors.warning}
|
|
4083
|
+
padding={1}
|
|
4084
|
+
flexDirection="column"
|
|
4085
|
+
minWidth={40}
|
|
4086
|
+
>
|
|
4087
|
+
<LoadingIndicator message={pendingOperation.message} />
|
|
4088
|
+
{pendingOperation.cancel && <Text dimColor>Press Esc to cancel</Text>}
|
|
4089
|
+
</Box>
|
|
4090
|
+
)}
|
|
4091
|
+
|
|
4092
|
+
{showHelpModal && (
|
|
4093
|
+
<Box
|
|
4094
|
+
position="absolute"
|
|
4095
|
+
marginTop={5}
|
|
4096
|
+
marginLeft={10}
|
|
4097
|
+
borderStyle="round"
|
|
4098
|
+
borderColor={themeContext.theme.colors.info}
|
|
4099
|
+
padding={1}
|
|
4100
|
+
>
|
|
4101
|
+
<HotkeyHelpModal
|
|
4102
|
+
isVisible={showHelpModal}
|
|
4103
|
+
onClose={closeHelpModal}
|
|
4104
|
+
hotkeys={DEFAULT_HOTKEYS}
|
|
4105
|
+
/>
|
|
4106
|
+
</Box>
|
|
4107
|
+
)}
|
|
4108
|
+
|
|
4109
|
+
{showApprovalQueue && pendingApprovals.length > 1 && (
|
|
4110
|
+
<Box
|
|
4111
|
+
position="absolute"
|
|
4112
|
+
marginTop={3}
|
|
4113
|
+
marginLeft={5}
|
|
4114
|
+
borderStyle="double"
|
|
4115
|
+
borderColor={themeContext.theme.colors.warning}
|
|
4116
|
+
padding={1}
|
|
4117
|
+
>
|
|
4118
|
+
<ApprovalQueue
|
|
4119
|
+
executions={pendingApprovals}
|
|
4120
|
+
onApprove={onApproveQueueItem}
|
|
4121
|
+
onReject={onRejectQueueItem}
|
|
4122
|
+
onApproveAll={onApproveAll}
|
|
4123
|
+
onRejectAll={onRejectAll}
|
|
4124
|
+
isFocused={showApprovalQueue}
|
|
4125
|
+
/>
|
|
4126
|
+
</Box>
|
|
4127
|
+
)}
|
|
4128
|
+
|
|
4129
|
+
{checkpointDiff.isVisible && (
|
|
4130
|
+
<Box
|
|
4131
|
+
position="absolute"
|
|
4132
|
+
marginTop={3}
|
|
4133
|
+
marginLeft={5}
|
|
4134
|
+
borderStyle="double"
|
|
4135
|
+
borderColor={themeContext.theme.colors.info}
|
|
4136
|
+
padding={1}
|
|
4137
|
+
>
|
|
4138
|
+
<CheckpointDiffView
|
|
4139
|
+
diffContent={checkpointDiff.content}
|
|
4140
|
+
snapshotHash={checkpointDiff.snapshotHash}
|
|
4141
|
+
isFocused={checkpointDiff.isVisible}
|
|
4142
|
+
isLoading={checkpointDiff.isLoading}
|
|
4143
|
+
maxHeight={24}
|
|
4144
|
+
onClose={closeCheckpointDiff}
|
|
4145
|
+
/>
|
|
4146
|
+
</Box>
|
|
4147
|
+
)}
|
|
4148
|
+
</>
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
interface AppHeaderProps {
|
|
4153
|
+
readonly backtrackState: ReturnType<typeof useBacktrack>["backtrackState"];
|
|
4154
|
+
readonly branches: ReturnType<typeof useBacktrack>["branches"];
|
|
4155
|
+
readonly currentMode: CodingMode;
|
|
4156
|
+
readonly currentTip: TipValue;
|
|
4157
|
+
readonly dismissTip: () => void;
|
|
4158
|
+
readonly handleCreateBacktrackBranch: () => void;
|
|
4159
|
+
readonly handleSwitchBacktrackBranch: (branchId: string) => void;
|
|
4160
|
+
readonly initError?: Error;
|
|
4161
|
+
readonly redoBacktrack: () => void;
|
|
4162
|
+
readonly specPhase: number;
|
|
4163
|
+
readonly tokenUsage: {
|
|
4164
|
+
inputTokens: number;
|
|
4165
|
+
outputTokens: number;
|
|
4166
|
+
totalCost: number;
|
|
4167
|
+
};
|
|
4168
|
+
readonly undoBacktrack: () => void;
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
function AppHeader({
|
|
4172
|
+
backtrackState,
|
|
4173
|
+
branches,
|
|
4174
|
+
currentMode,
|
|
4175
|
+
currentTip,
|
|
4176
|
+
dismissTip,
|
|
4177
|
+
handleCreateBacktrackBranch,
|
|
4178
|
+
handleSwitchBacktrackBranch,
|
|
4179
|
+
initError,
|
|
4180
|
+
redoBacktrack,
|
|
4181
|
+
specPhase,
|
|
4182
|
+
tokenUsage,
|
|
4183
|
+
undoBacktrack,
|
|
4184
|
+
}: AppHeaderProps): React.JSX.Element {
|
|
4185
|
+
const fileStats = useFileChangeStats();
|
|
4186
|
+
const showFileChanges = fileStats.additions > 0 || fileStats.deletions > 0;
|
|
4187
|
+
|
|
4188
|
+
return (
|
|
4189
|
+
<Box flexDirection="column">
|
|
4190
|
+
<Box flexDirection="row" justifyContent="space-between" marginBottom={1}>
|
|
4191
|
+
<ModeIndicator mode={currentMode} specPhase={specPhase} compact />
|
|
4192
|
+
<CostDisplay
|
|
4193
|
+
inputTokens={tokenUsage.inputTokens}
|
|
4194
|
+
outputTokens={tokenUsage.outputTokens}
|
|
4195
|
+
totalCost={tokenUsage.totalCost}
|
|
4196
|
+
compact
|
|
4197
|
+
/>
|
|
4198
|
+
</Box>
|
|
4199
|
+
{initError && <InitErrorBanner error={initError} />}
|
|
4200
|
+
{(currentTip || showFileChanges) && (
|
|
4201
|
+
<Box flexDirection="row" justifyContent="space-between">
|
|
4202
|
+
<Box flexGrow={1}>
|
|
4203
|
+
{currentTip && <TipBanner tip={currentTip} onDismiss={dismissTip} compact />}
|
|
4204
|
+
</Box>
|
|
4205
|
+
{showFileChanges && (
|
|
4206
|
+
<Box flexDirection="row">
|
|
4207
|
+
<Text color="gray">diff </Text>
|
|
4208
|
+
<FileChangesIndicator
|
|
4209
|
+
additions={fileStats.additions}
|
|
4210
|
+
deletions={fileStats.deletions}
|
|
4211
|
+
/>
|
|
4212
|
+
</Box>
|
|
4213
|
+
)}
|
|
4214
|
+
</Box>
|
|
4215
|
+
)}
|
|
4216
|
+
{currentMode === "spec" && (
|
|
4217
|
+
<PhaseProgressIndicator currentPhase={specPhase} showLabels showPercentage />
|
|
4218
|
+
)}
|
|
4219
|
+
{backtrackState.historyLength > 1 && (
|
|
4220
|
+
<BacktrackControls
|
|
4221
|
+
backtrackState={backtrackState}
|
|
4222
|
+
branches={branches}
|
|
4223
|
+
onUndo={undoBacktrack}
|
|
4224
|
+
onRedo={redoBacktrack}
|
|
4225
|
+
onCreateBranch={handleCreateBacktrackBranch}
|
|
4226
|
+
onSwitchBranch={handleSwitchBacktrackBranch}
|
|
4227
|
+
/>
|
|
4228
|
+
)}
|
|
4229
|
+
</Box>
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex TUI view with many conditional renders and state handlers
|
|
4234
|
+
function AppContentView({
|
|
4235
|
+
agentName,
|
|
4236
|
+
announce,
|
|
4237
|
+
alternateBufferEnabled,
|
|
4238
|
+
activeApproval,
|
|
4239
|
+
activeRiskLevel,
|
|
4240
|
+
activeSessionId,
|
|
4241
|
+
backtrackState,
|
|
4242
|
+
bannerCycleDurationMs,
|
|
4243
|
+
bannerCycles,
|
|
4244
|
+
bannerDisplayDurationMs,
|
|
4245
|
+
bannerSplashComplete,
|
|
4246
|
+
bannerUpdateIntervalMs,
|
|
4247
|
+
branches,
|
|
4248
|
+
cancelOnboarding,
|
|
4249
|
+
closeSessionManager,
|
|
4250
|
+
commandOptions,
|
|
4251
|
+
credentialManager,
|
|
4252
|
+
getSubcommands,
|
|
4253
|
+
getLevel3Items,
|
|
4254
|
+
categoryOrder,
|
|
4255
|
+
categoryLabels,
|
|
4256
|
+
checkpointDiff,
|
|
4257
|
+
closeCheckpointDiff,
|
|
4258
|
+
onOpenCheckpointDiff,
|
|
4259
|
+
contextWindow,
|
|
4260
|
+
currentMode,
|
|
4261
|
+
currentModel,
|
|
4262
|
+
currentProvider,
|
|
4263
|
+
currentTip,
|
|
4264
|
+
dismissTip,
|
|
4265
|
+
dismissUpdateBanner,
|
|
4266
|
+
handleApprove,
|
|
4267
|
+
handleApproveAlways,
|
|
4268
|
+
handleBannerComplete,
|
|
4269
|
+
handleCommand,
|
|
4270
|
+
handleCreateBacktrackBranch,
|
|
4271
|
+
handleMessage,
|
|
4272
|
+
handleModeSelect,
|
|
4273
|
+
handleModelSelect,
|
|
4274
|
+
handleOnboardingComplete,
|
|
4275
|
+
handlePromptSubmit,
|
|
4276
|
+
handleReject,
|
|
4277
|
+
handleSessionSelected,
|
|
4278
|
+
handleSwitchBacktrackBranch,
|
|
4279
|
+
initError,
|
|
4280
|
+
followupPrompt,
|
|
4281
|
+
interactivePrompt,
|
|
4282
|
+
loadSessionPreviewMessages,
|
|
4283
|
+
isLoading,
|
|
4284
|
+
thinkingModeEnabled,
|
|
4285
|
+
memoryEntries,
|
|
4286
|
+
messages,
|
|
4287
|
+
pendingMessage,
|
|
4288
|
+
pendingOperation,
|
|
4289
|
+
promptPlaceholder,
|
|
4290
|
+
promptValue,
|
|
4291
|
+
setPromptValue,
|
|
4292
|
+
sessions,
|
|
4293
|
+
suppressPromptEnter,
|
|
4294
|
+
shouldShowBanner,
|
|
4295
|
+
showModeSelector,
|
|
4296
|
+
showModelSelector,
|
|
4297
|
+
showOnboarding,
|
|
4298
|
+
showSessionManager,
|
|
4299
|
+
showHelpModal,
|
|
4300
|
+
closeHelpModal,
|
|
4301
|
+
showApprovalQueue,
|
|
4302
|
+
closeApprovalQueue,
|
|
4303
|
+
pendingApprovals,
|
|
4304
|
+
onApproveQueueItem,
|
|
4305
|
+
onRejectQueueItem,
|
|
4306
|
+
onApproveAll,
|
|
4307
|
+
onRejectAll,
|
|
4308
|
+
showSidebar,
|
|
4309
|
+
sidebarContent,
|
|
4310
|
+
specPhase,
|
|
4311
|
+
themeContext,
|
|
4312
|
+
taskChain,
|
|
4313
|
+
currentTaskId,
|
|
4314
|
+
todoItems,
|
|
4315
|
+
refreshTodos,
|
|
4316
|
+
toolRegistry,
|
|
4317
|
+
tokenUsage,
|
|
4318
|
+
turnUsage,
|
|
4319
|
+
totalTokens,
|
|
4320
|
+
trustMode,
|
|
4321
|
+
undoBacktrack,
|
|
4322
|
+
redoBacktrack,
|
|
4323
|
+
updateAvailable,
|
|
4324
|
+
workspace,
|
|
4325
|
+
branch,
|
|
4326
|
+
changedFiles,
|
|
4327
|
+
persistence,
|
|
4328
|
+
snapshots,
|
|
4329
|
+
providerStatus,
|
|
4330
|
+
costWarningState,
|
|
4331
|
+
autoApprovalState,
|
|
4332
|
+
vimEnabled,
|
|
4333
|
+
vimMode,
|
|
4334
|
+
}: AppContentViewProps): React.JSX.Element {
|
|
4335
|
+
const sidebar = renderSidebarContent({
|
|
4336
|
+
announce,
|
|
4337
|
+
showSidebar,
|
|
4338
|
+
sidebarContent,
|
|
4339
|
+
todoItems,
|
|
4340
|
+
refreshTodos,
|
|
4341
|
+
memoryEntries,
|
|
4342
|
+
toolRegistry,
|
|
4343
|
+
persistence,
|
|
4344
|
+
snapshots,
|
|
4345
|
+
taskChain,
|
|
4346
|
+
currentTaskId,
|
|
4347
|
+
onOpenCheckpointDiff,
|
|
4348
|
+
});
|
|
4349
|
+
|
|
4350
|
+
const footer = (
|
|
4351
|
+
<StatusBar
|
|
4352
|
+
mode={currentMode}
|
|
4353
|
+
agentName={agentName}
|
|
4354
|
+
modelName={currentModel}
|
|
4355
|
+
tokens={{
|
|
4356
|
+
current: totalTokens,
|
|
4357
|
+
max: contextWindow,
|
|
4358
|
+
breakdown: {
|
|
4359
|
+
inputTokens: tokenUsage.inputTokens,
|
|
4360
|
+
outputTokens: tokenUsage.outputTokens,
|
|
4361
|
+
thinkingTokens: tokenUsage.thinkingTokens,
|
|
4362
|
+
cacheReadTokens: tokenUsage.cacheReadTokens,
|
|
4363
|
+
cacheWriteTokens: tokenUsage.cacheWriteTokens,
|
|
4364
|
+
},
|
|
4365
|
+
turnUsage: {
|
|
4366
|
+
inputTokens: turnUsage.inputTokens,
|
|
4367
|
+
outputTokens: turnUsage.outputTokens,
|
|
4368
|
+
thinkingTokens: turnUsage.thinkingTokens,
|
|
4369
|
+
cacheReadTokens: turnUsage.cacheReadTokens,
|
|
4370
|
+
cacheWriteTokens: turnUsage.cacheWriteTokens,
|
|
4371
|
+
},
|
|
4372
|
+
showBreakdown: true,
|
|
4373
|
+
}}
|
|
4374
|
+
cost={tokenUsage.totalCost}
|
|
4375
|
+
trustMode={trustMode}
|
|
4376
|
+
thinking={{ active: thinkingModeEnabled }}
|
|
4377
|
+
showAllModes={showModeSelector}
|
|
4378
|
+
persistence={persistence}
|
|
4379
|
+
/>
|
|
4380
|
+
);
|
|
4381
|
+
|
|
4382
|
+
// Extended footer with ModelStatusBar and warnings
|
|
4383
|
+
const extendedFooter = (
|
|
4384
|
+
<Box flexDirection="column">
|
|
4385
|
+
{/* Cost warning - show above status bar when approaching/exceeding limit */}
|
|
4386
|
+
{costWarningState.show && (
|
|
4387
|
+
<CostWarning
|
|
4388
|
+
costUsed={tokenUsage.totalCost}
|
|
4389
|
+
costLimit={costWarningState.costLimit}
|
|
4390
|
+
requestsUsed={0}
|
|
4391
|
+
requestLimit={costWarningState.requestLimit}
|
|
4392
|
+
percentUsed={costWarningState.percentUsed}
|
|
4393
|
+
limitReached={costWarningState.limitReached}
|
|
4394
|
+
compact={true}
|
|
4395
|
+
severity={costWarningState.limitReached ? "error" : "warning"}
|
|
4396
|
+
/>
|
|
4397
|
+
)}
|
|
4398
|
+
{/* Auto-approval status - show when auto-approvals are active */}
|
|
4399
|
+
{autoApprovalState && autoApprovalState.consecutiveRequests > 0 && (
|
|
4400
|
+
<AutoApprovalStatus
|
|
4401
|
+
consecutiveRequests={autoApprovalState.consecutiveRequests}
|
|
4402
|
+
requestLimit={autoApprovalState.requestLimit}
|
|
4403
|
+
consecutiveCost={autoApprovalState.consecutiveCost}
|
|
4404
|
+
costLimit={autoApprovalState.costLimit}
|
|
4405
|
+
requestPercentUsed={autoApprovalState.requestPercentUsed}
|
|
4406
|
+
costPercentUsed={autoApprovalState.costPercentUsed}
|
|
4407
|
+
limitReached={autoApprovalState.limitReached}
|
|
4408
|
+
limitType={autoApprovalState.limitType}
|
|
4409
|
+
compact={true}
|
|
4410
|
+
/>
|
|
4411
|
+
)}
|
|
4412
|
+
{/* Model status bar - shows provider health */}
|
|
4413
|
+
{providerStatus.providers.length > 1 && (
|
|
4414
|
+
<Box marginBottom={1}>
|
|
4415
|
+
<ModelStatusBar providers={providerStatus.providers} compact={true} maxVisible={5} />
|
|
4416
|
+
</Box>
|
|
4417
|
+
)}
|
|
4418
|
+
{/* Main status bar */}
|
|
4419
|
+
{footer}
|
|
4420
|
+
</Box>
|
|
4421
|
+
);
|
|
4422
|
+
|
|
4423
|
+
// Conditional rendering to avoid hook count mismatch from early returns
|
|
4424
|
+
const showBannerView = shouldShowBanner && !bannerSplashComplete;
|
|
4425
|
+
const showMainView = !showOnboarding && !showBannerView;
|
|
4426
|
+
|
|
4427
|
+
const commandPlaceholder = followupPrompt
|
|
4428
|
+
? "Reply to follow-up..."
|
|
4429
|
+
: isLoading
|
|
4430
|
+
? "Thinking..."
|
|
4431
|
+
: "Type a message or /command...";
|
|
4432
|
+
|
|
4433
|
+
const commandInputDisabled =
|
|
4434
|
+
(isLoading && !followupPrompt) || !!interactivePrompt || !!pendingOperation;
|
|
4435
|
+
|
|
4436
|
+
const commandInputFocused =
|
|
4437
|
+
(!isLoading || !!followupPrompt) &&
|
|
4438
|
+
!showModeSelector &&
|
|
4439
|
+
!showModelSelector &&
|
|
4440
|
+
!showSessionManager &&
|
|
4441
|
+
!showHelpModal &&
|
|
4442
|
+
!activeApproval &&
|
|
4443
|
+
!interactivePrompt &&
|
|
4444
|
+
!pendingOperation;
|
|
4445
|
+
|
|
4446
|
+
const headerContent = (
|
|
4447
|
+
<AppHeader
|
|
4448
|
+
backtrackState={backtrackState}
|
|
4449
|
+
branches={branches}
|
|
4450
|
+
currentMode={currentMode}
|
|
4451
|
+
currentTip={currentTip}
|
|
4452
|
+
dismissTip={dismissTip}
|
|
4453
|
+
handleCreateBacktrackBranch={handleCreateBacktrackBranch}
|
|
4454
|
+
handleSwitchBacktrackBranch={handleSwitchBacktrackBranch}
|
|
4455
|
+
initError={initError}
|
|
4456
|
+
redoBacktrack={redoBacktrack}
|
|
4457
|
+
specPhase={specPhase}
|
|
4458
|
+
tokenUsage={tokenUsage}
|
|
4459
|
+
undoBacktrack={undoBacktrack}
|
|
4460
|
+
/>
|
|
4461
|
+
);
|
|
4462
|
+
|
|
4463
|
+
const layoutBody = (
|
|
4464
|
+
<>
|
|
4465
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
4466
|
+
{/* Thinking content is now integrated into messages via the `thinking` field */}
|
|
4467
|
+
{/* T-VIRTUAL-SCROLL: Pass historyMessages for Static rendering optimization */}
|
|
4468
|
+
<MessageList
|
|
4469
|
+
messages={messages}
|
|
4470
|
+
historyMessages={messages.filter((m) => !m.isStreaming)}
|
|
4471
|
+
pendingMessage={pendingMessage}
|
|
4472
|
+
isLoading={isLoading}
|
|
4473
|
+
useVirtualizedList={true}
|
|
4474
|
+
estimatedItemHeight={4}
|
|
4475
|
+
scrollKeyMode={commandInputFocused ? "page" : "all"}
|
|
4476
|
+
forceFollowOnInput={true}
|
|
4477
|
+
useAltBuffer={alternateBufferEnabled}
|
|
4478
|
+
enableScroll={!alternateBufferEnabled}
|
|
4479
|
+
isFocused={
|
|
4480
|
+
!showModeSelector &&
|
|
4481
|
+
!showModelSelector &&
|
|
4482
|
+
!showSessionManager &&
|
|
4483
|
+
!showHelpModal &&
|
|
4484
|
+
!activeApproval &&
|
|
4485
|
+
!interactivePrompt &&
|
|
4486
|
+
!pendingOperation
|
|
4487
|
+
}
|
|
4488
|
+
/>
|
|
4489
|
+
</Box>
|
|
4490
|
+
|
|
4491
|
+
<Box flexShrink={0} flexDirection="column">
|
|
4492
|
+
{/* Followup prompt with suggestions - use OptionSelector for keyboard navigation */}
|
|
4493
|
+
{followupPrompt && followupPrompt.suggestions.length > 0 && (
|
|
4494
|
+
<OptionSelector
|
|
4495
|
+
question={followupPrompt.question}
|
|
4496
|
+
options={followupPrompt.suggestions}
|
|
4497
|
+
onSelect={(option) => {
|
|
4498
|
+
handleMessage(option);
|
|
4499
|
+
}}
|
|
4500
|
+
onCancel={() => {
|
|
4501
|
+
handleMessage("");
|
|
4502
|
+
}}
|
|
4503
|
+
isFocused={commandInputFocused}
|
|
4504
|
+
/>
|
|
4505
|
+
)}
|
|
4506
|
+
{/* Followup prompt without suggestions - show simple text prompt */}
|
|
4507
|
+
{followupPrompt && followupPrompt.suggestions.length === 0 && (
|
|
4508
|
+
<Box marginTop={1} flexDirection="column">
|
|
4509
|
+
<Text color={themeContext.theme.semantic.text.secondary}>
|
|
4510
|
+
↳ {followupPrompt.question}
|
|
4511
|
+
</Text>
|
|
4512
|
+
<Text dimColor>Type your reply and press Enter (Esc to skip)</Text>
|
|
4513
|
+
</Box>
|
|
4514
|
+
)}
|
|
4515
|
+
{interactivePrompt && (
|
|
4516
|
+
<Box
|
|
4517
|
+
borderStyle="round"
|
|
4518
|
+
borderColor={themeContext.theme.colors.warning}
|
|
4519
|
+
paddingX={2}
|
|
4520
|
+
paddingY={1}
|
|
4521
|
+
marginY={1}
|
|
4522
|
+
flexDirection="column"
|
|
4523
|
+
>
|
|
4524
|
+
{/* Title section */}
|
|
4525
|
+
{interactivePrompt.title && (
|
|
4526
|
+
<Box
|
|
4527
|
+
borderStyle="single"
|
|
4528
|
+
borderBottom
|
|
4529
|
+
borderColor={themeContext.theme.colors.warning}
|
|
4530
|
+
marginBottom={1}
|
|
4531
|
+
>
|
|
4532
|
+
<Text bold color={themeContext.theme.colors.warning}>
|
|
4533
|
+
🔐 {interactivePrompt.title}
|
|
4534
|
+
</Text>
|
|
4535
|
+
</Box>
|
|
4536
|
+
)}
|
|
4537
|
+
{/* Help text section */}
|
|
4538
|
+
{interactivePrompt.helpText && (
|
|
4539
|
+
<Box marginBottom={1}>
|
|
4540
|
+
<Text color={themeContext.theme.semantic.text.muted}>
|
|
4541
|
+
{interactivePrompt.helpText}
|
|
4542
|
+
</Text>
|
|
4543
|
+
</Box>
|
|
4544
|
+
)}
|
|
4545
|
+
{/* Format hint */}
|
|
4546
|
+
{interactivePrompt.formatHint && (
|
|
4547
|
+
<Text color={themeContext.theme.semantic.text.muted}>
|
|
4548
|
+
📋 Format: {interactivePrompt.formatHint}
|
|
4549
|
+
</Text>
|
|
4550
|
+
)}
|
|
4551
|
+
{/* Documentation URL hint */}
|
|
4552
|
+
{interactivePrompt.documentationUrl && (
|
|
4553
|
+
<Text color={themeContext.theme.semantic.text.muted}>
|
|
4554
|
+
📚 Docs: {interactivePrompt.documentationUrl}
|
|
4555
|
+
</Text>
|
|
4556
|
+
)}
|
|
4557
|
+
{/* Input area with spacing */}
|
|
4558
|
+
<Box flexDirection="column" marginTop={1}>
|
|
4559
|
+
{/* Original message (e.g., "API Key:") */}
|
|
4560
|
+
<Text>{interactivePrompt.message}</Text>
|
|
4561
|
+
{/* Select options */}
|
|
4562
|
+
{interactivePrompt.inputType === "select" && interactivePrompt.options && (
|
|
4563
|
+
<Box flexDirection="column" marginTop={1}>
|
|
4564
|
+
{interactivePrompt.options.map((option, index) => (
|
|
4565
|
+
<Text key={option}>{`${index + 1}. ${option}`}</Text>
|
|
4566
|
+
))}
|
|
4567
|
+
</Box>
|
|
4568
|
+
)}
|
|
4569
|
+
{/* Input field */}
|
|
4570
|
+
<Box marginTop={1} flexGrow={1}>
|
|
4571
|
+
<Text color={themeContext.theme.semantic.text.muted}>{promptPlaceholder} </Text>
|
|
4572
|
+
<Box flexGrow={1}>
|
|
4573
|
+
<TextInput
|
|
4574
|
+
value={promptValue}
|
|
4575
|
+
onChange={setPromptValue}
|
|
4576
|
+
onSubmit={handlePromptSubmit}
|
|
4577
|
+
mask={interactivePrompt.inputType === "password" ? "*" : undefined}
|
|
4578
|
+
focused={!suppressPromptEnter}
|
|
4579
|
+
suppressEnter={suppressPromptEnter}
|
|
4580
|
+
showBorder={false}
|
|
4581
|
+
/>
|
|
4582
|
+
</Box>
|
|
4583
|
+
</Box>
|
|
4584
|
+
</Box>
|
|
4585
|
+
{/* Footer hint */}
|
|
4586
|
+
<Box marginTop={1}>
|
|
4587
|
+
<Text dimColor>Press Enter to submit, Esc to cancel</Text>
|
|
4588
|
+
</Box>
|
|
4589
|
+
</Box>
|
|
4590
|
+
)}
|
|
4591
|
+
|
|
4592
|
+
{/* Focus Debug: logs focus conditions when they change */}
|
|
4593
|
+
<FocusDebugger
|
|
4594
|
+
isLoading={isLoading}
|
|
4595
|
+
showModeSelector={showModeSelector}
|
|
4596
|
+
showModelSelector={showModelSelector}
|
|
4597
|
+
showSessionManager={showSessionManager}
|
|
4598
|
+
showHelpModal={showHelpModal}
|
|
4599
|
+
activeApproval={activeApproval}
|
|
4600
|
+
interactivePrompt={interactivePrompt}
|
|
4601
|
+
pendingOperation={pendingOperation}
|
|
4602
|
+
/>
|
|
4603
|
+
{/* Vim mode indicator (shown above input when vim mode is enabled) */}
|
|
4604
|
+
{vimEnabled && (
|
|
4605
|
+
<Box marginBottom={0}>
|
|
4606
|
+
<VimModeIndicator enabled={vimEnabled} mode={vimMode} />
|
|
4607
|
+
</Box>
|
|
4608
|
+
)}
|
|
4609
|
+
<EnhancedCommandInput
|
|
4610
|
+
onMessage={handleMessage}
|
|
4611
|
+
onCommand={handleCommand}
|
|
4612
|
+
commands={commandOptions}
|
|
4613
|
+
getSubcommands={getSubcommands}
|
|
4614
|
+
getLevel3Items={getLevel3Items}
|
|
4615
|
+
groupedCommands={true}
|
|
4616
|
+
categoryOrder={categoryOrder}
|
|
4617
|
+
categoryLabels={categoryLabels}
|
|
4618
|
+
placeholder={commandPlaceholder}
|
|
4619
|
+
disabled={commandInputDisabled}
|
|
4620
|
+
focused={commandInputFocused}
|
|
4621
|
+
historyKey="vellum-command-history"
|
|
4622
|
+
cwd={process.cwd()}
|
|
4623
|
+
/>
|
|
4624
|
+
</Box>
|
|
4625
|
+
</>
|
|
4626
|
+
);
|
|
4627
|
+
|
|
4628
|
+
const screenReaderStatus = pendingOperation
|
|
4629
|
+
? pendingOperation.message
|
|
4630
|
+
: isLoading
|
|
4631
|
+
? "Thinking..."
|
|
4632
|
+
: "Ready";
|
|
4633
|
+
|
|
4634
|
+
const screenReaderContent = (
|
|
4635
|
+
<Box flexDirection="column">
|
|
4636
|
+
{layoutBody}
|
|
4637
|
+
{showSidebar && sidebar && (
|
|
4638
|
+
<Box marginTop={1} flexDirection="column">
|
|
4639
|
+
<Text color={themeContext.theme.semantic.text.muted}>Sidebar</Text>
|
|
4640
|
+
{sidebar}
|
|
4641
|
+
</Box>
|
|
4642
|
+
)}
|
|
4643
|
+
</Box>
|
|
4644
|
+
);
|
|
4645
|
+
|
|
4646
|
+
return (
|
|
4647
|
+
<>
|
|
4648
|
+
{showOnboarding && (
|
|
4649
|
+
<OnboardingWizard
|
|
4650
|
+
onComplete={handleOnboardingComplete}
|
|
4651
|
+
onCancel={cancelOnboarding}
|
|
4652
|
+
credentialManager={credentialManager ?? undefined}
|
|
4653
|
+
/>
|
|
4654
|
+
)}
|
|
4655
|
+
|
|
4656
|
+
{!showOnboarding && showBannerView && (
|
|
4657
|
+
<Box
|
|
4658
|
+
flexDirection="column"
|
|
4659
|
+
alignItems="center"
|
|
4660
|
+
justifyContent="center"
|
|
4661
|
+
height={process.stdout.rows ?? 24}
|
|
4662
|
+
>
|
|
4663
|
+
<Banner
|
|
4664
|
+
animated
|
|
4665
|
+
autoHide
|
|
4666
|
+
cycles={bannerCycles}
|
|
4667
|
+
displayDuration={bannerDisplayDurationMs}
|
|
4668
|
+
cycleDuration={bannerCycleDurationMs}
|
|
4669
|
+
updateInterval={bannerUpdateIntervalMs}
|
|
4670
|
+
onComplete={handleBannerComplete}
|
|
4671
|
+
/>
|
|
4672
|
+
</Box>
|
|
4673
|
+
)}
|
|
4674
|
+
|
|
4675
|
+
{showMainView && (
|
|
4676
|
+
<>
|
|
4677
|
+
<AppOverlays
|
|
4678
|
+
activeApproval={activeApproval}
|
|
4679
|
+
activeRiskLevel={activeRiskLevel}
|
|
4680
|
+
activeSessionId={activeSessionId}
|
|
4681
|
+
checkpointDiff={checkpointDiff}
|
|
4682
|
+
closeCheckpointDiff={closeCheckpointDiff}
|
|
4683
|
+
closeSessionManager={closeSessionManager}
|
|
4684
|
+
currentMode={currentMode}
|
|
4685
|
+
currentModel={currentModel}
|
|
4686
|
+
currentProvider={currentProvider}
|
|
4687
|
+
dismissUpdateBanner={dismissUpdateBanner}
|
|
4688
|
+
handleApprove={handleApprove}
|
|
4689
|
+
handleApproveAlways={handleApproveAlways}
|
|
4690
|
+
handleModeSelect={handleModeSelect}
|
|
4691
|
+
handleModelSelect={handleModelSelect}
|
|
4692
|
+
handleReject={handleReject}
|
|
4693
|
+
handleSessionSelected={handleSessionSelected}
|
|
4694
|
+
loadSessionPreviewMessages={loadSessionPreviewMessages}
|
|
4695
|
+
pendingOperation={pendingOperation}
|
|
4696
|
+
sessions={sessions}
|
|
4697
|
+
showModeSelector={showModeSelector}
|
|
4698
|
+
showModelSelector={showModelSelector}
|
|
4699
|
+
showSessionManager={showSessionManager}
|
|
4700
|
+
showHelpModal={showHelpModal}
|
|
4701
|
+
closeHelpModal={closeHelpModal}
|
|
4702
|
+
showApprovalQueue={showApprovalQueue}
|
|
4703
|
+
closeApprovalQueue={closeApprovalQueue}
|
|
4704
|
+
pendingApprovals={pendingApprovals}
|
|
4705
|
+
onApproveQueueItem={onApproveQueueItem}
|
|
4706
|
+
onRejectQueueItem={onRejectQueueItem}
|
|
4707
|
+
onApproveAll={onApproveAll}
|
|
4708
|
+
onRejectAll={onRejectAll}
|
|
4709
|
+
themeContext={themeContext}
|
|
4710
|
+
updateAvailable={updateAvailable}
|
|
4711
|
+
/>
|
|
4712
|
+
|
|
4713
|
+
<AdaptiveLayout
|
|
4714
|
+
regularLayout={
|
|
4715
|
+
<Layout
|
|
4716
|
+
header={headerContent}
|
|
4717
|
+
footer={extendedFooter}
|
|
4718
|
+
sidebar={sidebar}
|
|
4719
|
+
showSidebar={showSidebar}
|
|
4720
|
+
workspace={workspace}
|
|
4721
|
+
branch={branch ?? undefined}
|
|
4722
|
+
changedFiles={changedFiles}
|
|
4723
|
+
>
|
|
4724
|
+
{layoutBody}
|
|
4725
|
+
</Layout>
|
|
4726
|
+
}
|
|
4727
|
+
header={headerContent}
|
|
4728
|
+
footer={extendedFooter}
|
|
4729
|
+
status={screenReaderStatus}
|
|
4730
|
+
>
|
|
4731
|
+
{screenReaderContent}
|
|
4732
|
+
</AdaptiveLayout>
|
|
4733
|
+
</>
|
|
4734
|
+
)}
|
|
4735
|
+
</>
|
|
4736
|
+
);
|
|
4737
|
+
}
|