@butlerw/vellum 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/__fixtures__/responses/code-generation.json +42 -0
- package/__fixtures__/responses/error-response.json +20 -0
- package/__fixtures__/responses/hello-world.json +32 -0
- package/dist/auth-6MCXESOH.js +26 -0
- package/dist/chunk-SECXJGWA.js +597 -0
- package/dist/index.js +34023 -0
- package/package.json +67 -0
- package/src/__tests__/commands.e2e.test.ts +728 -0
- package/src/__tests__/credentials.test.ts +713 -0
- package/src/__tests__/mode-e2e.test.ts +391 -0
- package/src/__tests__/tui-integration.test.tsx +1271 -0
- package/src/agents/__tests__/task-persistence.test.ts +235 -0
- package/src/agents/commands/delegate.ts +240 -0
- package/src/agents/commands/index.ts +10 -0
- package/src/agents/commands/resume.ts +335 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/task-persistence.ts +272 -0
- package/src/agents/task-resumption.ts +242 -0
- package/src/app.tsx +4737 -0
- package/src/commands/__tests__/.gitkeep +1 -0
- package/src/commands/__tests__/agents.test.ts +606 -0
- package/src/commands/__tests__/auth.test.ts +626 -0
- package/src/commands/__tests__/autocomplete.test.ts +683 -0
- package/src/commands/__tests__/batch.test.ts +287 -0
- package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
- package/src/commands/__tests__/completion.test.ts +238 -0
- package/src/commands/__tests__/core.test.ts +363 -0
- package/src/commands/__tests__/executor.test.ts +496 -0
- package/src/commands/__tests__/exit-codes.test.ts +220 -0
- package/src/commands/__tests__/init.test.ts +243 -0
- package/src/commands/__tests__/language.test.ts +353 -0
- package/src/commands/__tests__/mode-cli.test.ts +667 -0
- package/src/commands/__tests__/model.test.ts +277 -0
- package/src/commands/__tests__/parser.test.ts +493 -0
- package/src/commands/__tests__/performance.bench.ts +380 -0
- package/src/commands/__tests__/registry.test.ts +534 -0
- package/src/commands/__tests__/resume.test.ts +449 -0
- package/src/commands/__tests__/security.test.ts +845 -0
- package/src/commands/__tests__/stream-json.test.ts +372 -0
- package/src/commands/__tests__/user-commands.test.ts +597 -0
- package/src/commands/adapters.ts +267 -0
- package/src/commands/agent.ts +395 -0
- package/src/commands/agents/generate.ts +506 -0
- package/src/commands/agents/index.ts +272 -0
- package/src/commands/agents/show.ts +271 -0
- package/src/commands/agents/validate.ts +387 -0
- package/src/commands/auth.ts +883 -0
- package/src/commands/autocomplete.ts +480 -0
- package/src/commands/batch/command.ts +388 -0
- package/src/commands/batch/executor.ts +361 -0
- package/src/commands/batch/index.ts +12 -0
- package/src/commands/commit.ts +235 -0
- package/src/commands/completion/index.ts +371 -0
- package/src/commands/condense.ts +191 -0
- package/src/commands/config.ts +344 -0
- package/src/commands/context-provider.ts +173 -0
- package/src/commands/copy.ts +329 -0
- package/src/commands/core/clear.ts +38 -0
- package/src/commands/core/exit.ts +43 -0
- package/src/commands/core/help.ts +354 -0
- package/src/commands/core/index.ts +15 -0
- package/src/commands/cost.ts +179 -0
- package/src/commands/credentials.tsx +618 -0
- package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
- package/src/commands/custom-agents/create.ts +377 -0
- package/src/commands/custom-agents/export.ts +135 -0
- package/src/commands/custom-agents/import.ts +199 -0
- package/src/commands/custom-agents/index.ts +372 -0
- package/src/commands/custom-agents/info.ts +318 -0
- package/src/commands/custom-agents/list.ts +267 -0
- package/src/commands/custom-agents/validate.ts +388 -0
- package/src/commands/diff-mode.ts +241 -0
- package/src/commands/env.ts +53 -0
- package/src/commands/executor.ts +579 -0
- package/src/commands/exit-codes.ts +202 -0
- package/src/commands/index.ts +701 -0
- package/src/commands/init/index.ts +15 -0
- package/src/commands/init/prompts.ts +366 -0
- package/src/commands/init/templates/commands-readme.md +80 -0
- package/src/commands/init/templates/example-command.md +79 -0
- package/src/commands/init/templates/example-skill.md +168 -0
- package/src/commands/init/templates/example-workflow.md +101 -0
- package/src/commands/init/templates/prompts-readme.md +52 -0
- package/src/commands/init/templates/rules-readme.md +63 -0
- package/src/commands/init/templates/skills-readme.md +83 -0
- package/src/commands/init/templates/workflows-readme.md +94 -0
- package/src/commands/init.ts +391 -0
- package/src/commands/install.ts +90 -0
- package/src/commands/language.ts +191 -0
- package/src/commands/loaders/.gitkeep +1 -0
- package/src/commands/lsp.ts +199 -0
- package/src/commands/markdown-commands.ts +253 -0
- package/src/commands/mcp.ts +588 -0
- package/src/commands/memory/export.ts +341 -0
- package/src/commands/memory/index.ts +148 -0
- package/src/commands/memory/list.ts +261 -0
- package/src/commands/memory/search.ts +346 -0
- package/src/commands/memory/utils.ts +15 -0
- package/src/commands/metrics.ts +75 -0
- package/src/commands/migrate/index.ts +16 -0
- package/src/commands/migrate/prompts.ts +477 -0
- package/src/commands/mode.ts +331 -0
- package/src/commands/model.ts +298 -0
- package/src/commands/onboard.ts +205 -0
- package/src/commands/open.ts +169 -0
- package/src/commands/output/stream-json.ts +373 -0
- package/src/commands/parser/chain-parser.ts +370 -0
- package/src/commands/parser/index.ts +29 -0
- package/src/commands/parser/pipe-parser.ts +480 -0
- package/src/commands/parser.ts +588 -0
- package/src/commands/persistence.ts +355 -0
- package/src/commands/progress.ts +18 -0
- package/src/commands/prompt/index.ts +17 -0
- package/src/commands/prompt/validate.ts +621 -0
- package/src/commands/prompt-priority.ts +401 -0
- package/src/commands/registry.ts +374 -0
- package/src/commands/sandbox/index.ts +131 -0
- package/src/commands/security/index.ts +21 -0
- package/src/commands/security/input-sanitizer.ts +168 -0
- package/src/commands/security/permission-checker.ts +456 -0
- package/src/commands/security/sensitive-data.ts +350 -0
- package/src/commands/session/delete.ts +38 -0
- package/src/commands/session/export.ts +39 -0
- package/src/commands/session/index.ts +26 -0
- package/src/commands/session/list.ts +26 -0
- package/src/commands/session/resume.ts +562 -0
- package/src/commands/session/search.ts +434 -0
- package/src/commands/session/show.ts +26 -0
- package/src/commands/settings.ts +368 -0
- package/src/commands/setup.ts +23 -0
- package/src/commands/shell/index.ts +16 -0
- package/src/commands/shell/setup.ts +422 -0
- package/src/commands/shell-init.ts +50 -0
- package/src/commands/shell-integration/index.ts +194 -0
- package/src/commands/skill.ts +1220 -0
- package/src/commands/spec.ts +558 -0
- package/src/commands/status.ts +246 -0
- package/src/commands/theme.ts +211 -0
- package/src/commands/think.ts +551 -0
- package/src/commands/trust.ts +211 -0
- package/src/commands/tutorial.ts +522 -0
- package/src/commands/types.ts +512 -0
- package/src/commands/update.ts +274 -0
- package/src/commands/usage.ts +213 -0
- package/src/commands/user-commands.ts +630 -0
- package/src/commands/utils.ts +142 -0
- package/src/commands/vim.ts +152 -0
- package/src/commands/workflow.ts +257 -0
- package/src/components/header.tsx +25 -0
- package/src/components/input.tsx +25 -0
- package/src/components/message-list.tsx +32 -0
- package/src/components/status-bar.tsx +23 -0
- package/src/index.tsx +614 -0
- package/src/onboarding/__tests__/tutorial.test.ts +740 -0
- package/src/onboarding/index.ts +69 -0
- package/src/onboarding/tips/index.ts +9 -0
- package/src/onboarding/tips/tip-engine.ts +459 -0
- package/src/onboarding/tutorial/index.ts +88 -0
- package/src/onboarding/tutorial/lessons/basics.ts +151 -0
- package/src/onboarding/tutorial/lessons/index.ts +151 -0
- package/src/onboarding/tutorial/lessons/modes.ts +230 -0
- package/src/onboarding/tutorial/lessons/tools.ts +172 -0
- package/src/onboarding/tutorial/progress-tracker.ts +350 -0
- package/src/onboarding/tutorial/storage.ts +249 -0
- package/src/onboarding/tutorial/tutorial-system.ts +462 -0
- package/src/onboarding/tutorial/types.ts +310 -0
- package/src/orchestrator-singleton.ts +129 -0
- package/src/shutdown.ts +33 -0
- package/src/test/e2e/assertions.ts +267 -0
- package/src/test/e2e/fixtures.ts +204 -0
- package/src/test/e2e/harness.ts +575 -0
- package/src/test/e2e/index.ts +57 -0
- package/src/test/e2e/types.ts +228 -0
- package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
- package/src/test/fixtures/fake-response-loader.ts +314 -0
- package/src/test/fixtures/index.ts +20 -0
- package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
- package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
- package/src/tui/__tests__/mode-components.test.tsx +395 -0
- package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
- package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
- package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
- package/src/tui/adapters/agent-adapter.ts +1008 -0
- package/src/tui/adapters/index.ts +48 -0
- package/src/tui/adapters/message-adapter.ts +315 -0
- package/src/tui/adapters/persistence-bridge.ts +331 -0
- package/src/tui/adapters/session-adapter.ts +419 -0
- package/src/tui/buffered-stdout.ts +223 -0
- package/src/tui/components/AgentProgress.tsx +424 -0
- package/src/tui/components/Banner/AsciiArt.ts +160 -0
- package/src/tui/components/Banner/Banner.tsx +355 -0
- package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
- package/src/tui/components/Banner/ShimmerText.tsx +193 -0
- package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
- package/src/tui/components/Banner/index.ts +61 -0
- package/src/tui/components/Banner/useShimmer.ts +241 -0
- package/src/tui/components/ChatView.tsx +11 -0
- package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
- package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
- package/src/tui/components/Checkpoint/index.ts +19 -0
- package/src/tui/components/CostDisplay.tsx +226 -0
- package/src/tui/components/InitErrorBanner.tsx +122 -0
- package/src/tui/components/Input/Autocomplete.tsx +603 -0
- package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
- package/src/tui/components/Input/HighlightedText.tsx +236 -0
- package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
- package/src/tui/components/Input/TextInput.tsx +1002 -0
- package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
- package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
- package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
- package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
- package/src/tui/components/Input/highlight.ts +362 -0
- package/src/tui/components/Input/index.ts +36 -0
- package/src/tui/components/Input/slash-command-utils.ts +135 -0
- package/src/tui/components/Layout.tsx +432 -0
- package/src/tui/components/McpPanel.tsx +137 -0
- package/src/tui/components/MemoryPanel.tsx +448 -0
- package/src/tui/components/Messages/CodeBlock.tsx +527 -0
- package/src/tui/components/Messages/DiffView.tsx +679 -0
- package/src/tui/components/Messages/ImageReference.tsx +89 -0
- package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
- package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
- package/src/tui/components/Messages/MessageBubble.tsx +270 -0
- package/src/tui/components/Messages/MessageList.tsx +1719 -0
- package/src/tui/components/Messages/StreamingText.tsx +216 -0
- package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
- package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
- package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
- package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
- package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
- package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
- package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
- package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
- package/src/tui/components/Messages/index.ts +25 -0
- package/src/tui/components/ModeIndicator.tsx +177 -0
- package/src/tui/components/ModeSelector.tsx +216 -0
- package/src/tui/components/ModelSelector.tsx +339 -0
- package/src/tui/components/OnboardingWizard.tsx +670 -0
- package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
- package/src/tui/components/RateLimitIndicator.tsx +82 -0
- package/src/tui/components/ScreenReaderLayout.tsx +295 -0
- package/src/tui/components/SettingsPanel.tsx +643 -0
- package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
- package/src/tui/components/Sidebar/index.ts +9 -0
- package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
- package/src/tui/components/Status/index.ts +12 -0
- package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
- package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
- package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
- package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
- package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
- package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
- package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
- package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
- package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
- package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
- package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
- package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
- package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
- package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
- package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
- package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
- package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
- package/src/tui/components/StatusBar/index.ts +60 -0
- package/src/tui/components/TipBanner.tsx +115 -0
- package/src/tui/components/TodoItem.tsx +208 -0
- package/src/tui/components/TodoPanel.tsx +455 -0
- package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
- package/src/tui/components/Tools/OptionSelector.tsx +160 -0
- package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
- package/src/tui/components/Tools/ToolParams.tsx +483 -0
- package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
- package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
- package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
- package/src/tui/components/Tools/index.ts +21 -0
- package/src/tui/components/TrustPrompt.tsx +279 -0
- package/src/tui/components/UpdateBanner.tsx +166 -0
- package/src/tui/components/VimModeIndicator.tsx +112 -0
- package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
- package/src/tui/components/backtrack/index.ts +13 -0
- package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
- package/src/tui/components/common/CostWarning.tsx +294 -0
- package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
- package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
- package/src/tui/components/common/ErrorBoundary.tsx +140 -0
- package/src/tui/components/common/GradientText.tsx +224 -0
- package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
- package/src/tui/components/common/HotkeyHints.tsx +70 -0
- package/src/tui/components/common/MaxSizedBox.tsx +354 -0
- package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
- package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
- package/src/tui/components/common/ScrollIndicator.tsx +160 -0
- package/src/tui/components/common/Spinner.tsx +342 -0
- package/src/tui/components/common/StreamingIndicator.tsx +316 -0
- package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
- package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
- package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
- package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
- package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
- package/src/tui/components/common/VirtualizedList/index.ts +30 -0
- package/src/tui/components/common/VirtualizedList/types.ts +107 -0
- package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
- package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
- package/src/tui/components/common/index.ts +110 -0
- package/src/tui/components/index.ts +79 -0
- package/src/tui/components/session/CheckpointPanel.tsx +323 -0
- package/src/tui/components/session/RollbackDialog.tsx +169 -0
- package/src/tui/components/session/SessionItem.tsx +136 -0
- package/src/tui/components/session/SessionListPanel.tsx +252 -0
- package/src/tui/components/session/SessionPicker.tsx +449 -0
- package/src/tui/components/session/SessionPreview.tsx +240 -0
- package/src/tui/components/session/__tests__/session.test.tsx +408 -0
- package/src/tui/components/session/index.ts +28 -0
- package/src/tui/components/session/types.ts +116 -0
- package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
- package/src/tui/components/theme/index.ts +227 -0
- package/src/tui/components/theme/tokens.ts +484 -0
- package/src/tui/config/defaults.ts +134 -0
- package/src/tui/config/index.ts +17 -0
- package/src/tui/context/AnimationContext.tsx +284 -0
- package/src/tui/context/AppContext.tsx +349 -0
- package/src/tui/context/BracketedPasteContext.tsx +372 -0
- package/src/tui/context/LspContext.tsx +192 -0
- package/src/tui/context/McpContext.tsx +325 -0
- package/src/tui/context/MessagesContext.tsx +870 -0
- package/src/tui/context/OverflowContext.tsx +213 -0
- package/src/tui/context/RateLimitContext.tsx +108 -0
- package/src/tui/context/ResilienceContext.tsx +275 -0
- package/src/tui/context/RootProvider.tsx +136 -0
- package/src/tui/context/ScrollContext.tsx +331 -0
- package/src/tui/context/ToolsContext.tsx +702 -0
- package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
- package/src/tui/context/index.ts +140 -0
- package/src/tui/enterprise-integration.ts +282 -0
- package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
- package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
- package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
- package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
- package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
- package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
- package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
- package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
- package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
- package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
- package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
- package/src/tui/hooks/index.ts +252 -0
- package/src/tui/hooks/useAgentLoop.ts +712 -0
- package/src/tui/hooks/useAlternateBuffer.ts +398 -0
- package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
- package/src/tui/hooks/useBacktrack.ts +443 -0
- package/src/tui/hooks/useBracketedPaste.ts +104 -0
- package/src/tui/hooks/useCollapsible.ts +240 -0
- package/src/tui/hooks/useCopyMode.ts +382 -0
- package/src/tui/hooks/useCostSummary.ts +75 -0
- package/src/tui/hooks/useDesktopNotification.ts +414 -0
- package/src/tui/hooks/useDiffMode.ts +44 -0
- package/src/tui/hooks/useFileChangeStats.ts +110 -0
- package/src/tui/hooks/useFileSuggestions.ts +284 -0
- package/src/tui/hooks/useFlickerDetector.ts +250 -0
- package/src/tui/hooks/useGitStatus.ts +200 -0
- package/src/tui/hooks/useHotkeys.ts +579 -0
- package/src/tui/hooks/useImagePaste.ts +114 -0
- package/src/tui/hooks/useInputHighlight.ts +145 -0
- package/src/tui/hooks/useInputHistory.ts +246 -0
- package/src/tui/hooks/useKeyboardScroll.ts +209 -0
- package/src/tui/hooks/useLineBuffer.ts +356 -0
- package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
- package/src/tui/hooks/useModeController.ts +167 -0
- package/src/tui/hooks/useModeShortcuts.ts +196 -0
- package/src/tui/hooks/usePermissionHandler.ts +146 -0
- package/src/tui/hooks/usePersistence.ts +480 -0
- package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
- package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
- package/src/tui/hooks/useProviderStatus.ts +270 -0
- package/src/tui/hooks/useRateLimitStatus.ts +90 -0
- package/src/tui/hooks/useScreenReader.ts +315 -0
- package/src/tui/hooks/useScrollController.ts +450 -0
- package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
- package/src/tui/hooks/useSidebarPanelData.ts +115 -0
- package/src/tui/hooks/useSmoothScroll.ts +202 -0
- package/src/tui/hooks/useSnapshots.ts +300 -0
- package/src/tui/hooks/useStateAndRef.ts +50 -0
- package/src/tui/hooks/useTerminalSize.ts +206 -0
- package/src/tui/hooks/useToolApprovalController.ts +91 -0
- package/src/tui/hooks/useVim.ts +334 -0
- package/src/tui/hooks/useWorkspace.ts +56 -0
- package/src/tui/i18n/__tests__/init.test.ts +278 -0
- package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
- package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
- package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
- package/src/tui/i18n/index.ts +72 -0
- package/src/tui/i18n/init.ts +131 -0
- package/src/tui/i18n/language-config.ts +106 -0
- package/src/tui/i18n/locale-detection.ts +173 -0
- package/src/tui/i18n/settings-integration.ts +557 -0
- package/src/tui/i18n/tui-namespace.ts +538 -0
- package/src/tui/i18n/types.ts +312 -0
- package/src/tui/index.ts +43 -0
- package/src/tui/lsp-integration.ts +409 -0
- package/src/tui/metrics-integration.ts +366 -0
- package/src/tui/plugins.ts +383 -0
- package/src/tui/resilience.ts +342 -0
- package/src/tui/sandbox-integration.ts +317 -0
- package/src/tui/services/clipboard.ts +348 -0
- package/src/tui/services/fuzzy-search.ts +441 -0
- package/src/tui/services/index.ts +72 -0
- package/src/tui/services/markdown-renderer.ts +565 -0
- package/src/tui/services/open-external.ts +247 -0
- package/src/tui/services/syntax-highlighter.ts +483 -0
- package/src/tui/slash-commands.ts +12 -0
- package/src/tui/theme/index.ts +15 -0
- package/src/tui/theme/provider.tsx +206 -0
- package/src/tui/tip-integration.ts +300 -0
- package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
- package/src/tui/types/ink-extended.ts +87 -0
- package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
- package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
- package/src/tui/utils/__tests__/text-width.test.ts +158 -0
- package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
- package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
- package/src/tui/utils/bracketedPaste.ts +107 -0
- package/src/tui/utils/cursor-manager.ts +131 -0
- package/src/tui/utils/detectTerminal.ts +596 -0
- package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
- package/src/tui/utils/heightEstimator.ts +198 -0
- package/src/tui/utils/index.ts +91 -0
- package/src/tui/utils/isNarrowWidth.ts +52 -0
- package/src/tui/utils/stdoutGuard.ts +90 -0
- package/src/tui/utils/synchronized-update.ts +70 -0
- package/src/tui/utils/text-width.ts +225 -0
- package/src/tui/utils/textSanitizer.ts +225 -0
- package/src/tui/utils/textUtils.ts +114 -0
- package/src/tui/utils/ui-sizing.ts +192 -0
- package/src/tui-blessed/app.ts +160 -0
- package/src/tui-blessed/index.ts +2 -0
- package/src/tui-blessed/neo-blessed.d.ts +6 -0
- package/src/tui-blessed/test.ts +21 -0
- package/src/tui-blessed/types.ts +14 -0
- package/src/utils/icons.ts +130 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/resume-hint.ts +86 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages Context and State Management
|
|
3
|
+
*
|
|
4
|
+
* Provides message state management for the Vellum TUI including
|
|
5
|
+
* message storage, streaming support, and tool call tracking.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/context/MessagesContext
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, {
|
|
11
|
+
createContext,
|
|
12
|
+
type Dispatch,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
useCallback,
|
|
15
|
+
useContext,
|
|
16
|
+
useMemo,
|
|
17
|
+
useReducer,
|
|
18
|
+
} from "react";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Role of a message sender
|
|
26
|
+
*/
|
|
27
|
+
export type MessageRole = "user" | "assistant" | "system" | "tool" | "tool_group";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Status of a tool call within a message.
|
|
31
|
+
* - pending: Tool call created but not started
|
|
32
|
+
* - running: Tool is currently executing (show spinner)
|
|
33
|
+
* - completed: Tool finished successfully (show checkmark)
|
|
34
|
+
* - error: Tool failed (show error icon)
|
|
35
|
+
*/
|
|
36
|
+
export type ToolCallStatus = "pending" | "running" | "completed" | "error";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Information about a tool call within a message
|
|
40
|
+
*/
|
|
41
|
+
export interface ToolCallInfo {
|
|
42
|
+
/** Unique identifier for the tool call */
|
|
43
|
+
readonly id: string;
|
|
44
|
+
/** Name of the tool being called */
|
|
45
|
+
readonly name: string;
|
|
46
|
+
/** Arguments passed to the tool */
|
|
47
|
+
readonly arguments: Record<string, unknown>;
|
|
48
|
+
/** Result of the tool call, if completed successfully */
|
|
49
|
+
readonly result?: unknown;
|
|
50
|
+
/** Error message if the tool call failed */
|
|
51
|
+
readonly error?: string;
|
|
52
|
+
/** Status of the tool call */
|
|
53
|
+
readonly status: ToolCallStatus;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Token usage information for a message turn.
|
|
58
|
+
*/
|
|
59
|
+
export interface MessageTokenUsage {
|
|
60
|
+
/** Number of input tokens */
|
|
61
|
+
readonly inputTokens: number;
|
|
62
|
+
/** Number of output tokens */
|
|
63
|
+
readonly outputTokens: number;
|
|
64
|
+
/** Number of tokens used for thinking/reasoning (if applicable) */
|
|
65
|
+
readonly thinkingTokens?: number;
|
|
66
|
+
/** Number of tokens read from cache (if applicable) */
|
|
67
|
+
readonly cacheReadTokens?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A single message in the conversation
|
|
72
|
+
*/
|
|
73
|
+
export interface Message {
|
|
74
|
+
/** Unique identifier for the message */
|
|
75
|
+
readonly id: string;
|
|
76
|
+
/** Role of the message sender */
|
|
77
|
+
readonly role: MessageRole;
|
|
78
|
+
/** Content of the message */
|
|
79
|
+
readonly content: string;
|
|
80
|
+
/** Timestamp when the message was created */
|
|
81
|
+
readonly timestamp: Date;
|
|
82
|
+
/** Whether the message is currently being streamed */
|
|
83
|
+
readonly isStreaming?: boolean;
|
|
84
|
+
/** Tool calls associated with this message */
|
|
85
|
+
readonly toolCalls?: readonly ToolCallInfo[];
|
|
86
|
+
/** Token usage for this message turn (assistant messages only) */
|
|
87
|
+
readonly tokenUsage?: MessageTokenUsage;
|
|
88
|
+
/** Whether this message is a continuation of a previous split message */
|
|
89
|
+
readonly isContinuation?: boolean;
|
|
90
|
+
/** Thinking/reasoning content (for models with extended thinking) */
|
|
91
|
+
readonly thinking?: string;
|
|
92
|
+
/** Duration of thinking in milliseconds (for extended thinking models) */
|
|
93
|
+
readonly thinkingDuration?: number;
|
|
94
|
+
/** Whether thinking phase is complete (false during streaming) */
|
|
95
|
+
readonly isThinkingComplete?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Messages state interface
|
|
100
|
+
*
|
|
101
|
+
* Uses a split architecture for optimal rendering:
|
|
102
|
+
* - `historyMessages`: Completed messages rendered in Ink's <Static> (never re-render)
|
|
103
|
+
* - `pendingMessage`: Currently streaming message (only this causes re-renders)
|
|
104
|
+
*/
|
|
105
|
+
export interface MessagesState {
|
|
106
|
+
/** Completed messages - rendered in <Static>, never re-render */
|
|
107
|
+
readonly historyMessages: readonly Message[];
|
|
108
|
+
/** Currently streaming message - the only thing that re-renders */
|
|
109
|
+
readonly pendingMessage: Message | null;
|
|
110
|
+
/** List of all messages in the conversation (computed: history + pending) */
|
|
111
|
+
readonly messages: readonly Message[];
|
|
112
|
+
/** Whether any message is currently streaming */
|
|
113
|
+
readonly isStreaming: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Initial messages state
|
|
118
|
+
*/
|
|
119
|
+
const initialState: MessagesState = {
|
|
120
|
+
historyMessages: [],
|
|
121
|
+
pendingMessage: null,
|
|
122
|
+
messages: [],
|
|
123
|
+
isStreaming: false,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Actions (Discriminated Union)
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Add a new message
|
|
132
|
+
*/
|
|
133
|
+
export interface AddMessageAction {
|
|
134
|
+
readonly type: "ADD_MESSAGE";
|
|
135
|
+
readonly message: Message;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update an existing message
|
|
140
|
+
*/
|
|
141
|
+
export interface UpdateMessageAction {
|
|
142
|
+
readonly type: "UPDATE_MESSAGE";
|
|
143
|
+
readonly id: string;
|
|
144
|
+
readonly updates: Partial<Omit<Message, "id">>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Append content to an existing message (for streaming)
|
|
149
|
+
*/
|
|
150
|
+
export interface AppendToMessageAction {
|
|
151
|
+
readonly type: "APPEND_TO_MESSAGE";
|
|
152
|
+
readonly id: string;
|
|
153
|
+
readonly content: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Replace the entire message list
|
|
158
|
+
*/
|
|
159
|
+
export interface SetMessagesAction {
|
|
160
|
+
readonly type: "SET_MESSAGES";
|
|
161
|
+
readonly messages: readonly Message[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Clear all messages
|
|
166
|
+
*/
|
|
167
|
+
export interface ClearMessagesAction {
|
|
168
|
+
readonly type: "CLEAR_MESSAGES";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Set streaming state
|
|
173
|
+
*/
|
|
174
|
+
export interface SetStreamingAction {
|
|
175
|
+
readonly type: "SET_STREAMING";
|
|
176
|
+
readonly isStreaming: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Commit pending message to history (for Static rendering)
|
|
181
|
+
*/
|
|
182
|
+
export interface CommitPendingMessageAction {
|
|
183
|
+
readonly type: "COMMIT_PENDING_MESSAGE";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Split a long streaming message at a safe point
|
|
188
|
+
* Moves completed content to history and keeps remainder as pending
|
|
189
|
+
*/
|
|
190
|
+
export interface SplitMessageAction {
|
|
191
|
+
readonly type: "SPLIT_MESSAGE";
|
|
192
|
+
/** Index to split at (content before this becomes history) */
|
|
193
|
+
readonly splitIndex: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Append thinking/reasoning content to an existing message (for streaming)
|
|
198
|
+
*/
|
|
199
|
+
export interface AppendToThinkingAction {
|
|
200
|
+
readonly type: "APPEND_TO_THINKING";
|
|
201
|
+
readonly id: string;
|
|
202
|
+
readonly thinking: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Add a new tool_group message (Gemini-style independent tool execution)
|
|
207
|
+
*/
|
|
208
|
+
export interface AddToolGroupAction {
|
|
209
|
+
readonly type: "ADD_TOOL_GROUP";
|
|
210
|
+
readonly toolCalls: readonly ToolCallInfo[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Update an existing tool_group message with tool call status
|
|
215
|
+
*/
|
|
216
|
+
export interface UpdateToolGroupAction {
|
|
217
|
+
readonly type: "UPDATE_TOOL_GROUP";
|
|
218
|
+
readonly groupId: string;
|
|
219
|
+
readonly toolCall: ToolCallInfo;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Discriminated union of all message actions
|
|
224
|
+
*/
|
|
225
|
+
export type MessagesAction =
|
|
226
|
+
| AddMessageAction
|
|
227
|
+
| UpdateMessageAction
|
|
228
|
+
| AppendToMessageAction
|
|
229
|
+
| SetMessagesAction
|
|
230
|
+
| ClearMessagesAction
|
|
231
|
+
| SetStreamingAction
|
|
232
|
+
| CommitPendingMessageAction
|
|
233
|
+
| SplitMessageAction
|
|
234
|
+
| AppendToThinkingAction
|
|
235
|
+
| AddToolGroupAction
|
|
236
|
+
| UpdateToolGroupAction;
|
|
237
|
+
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// Helper Functions
|
|
240
|
+
// =============================================================================
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Compute the combined messages array from history + pending
|
|
244
|
+
*/
|
|
245
|
+
function computeMessages(
|
|
246
|
+
historyMessages: readonly Message[],
|
|
247
|
+
pendingMessage: Message | null
|
|
248
|
+
): readonly Message[] {
|
|
249
|
+
if (pendingMessage) {
|
|
250
|
+
return [...historyMessages, pendingMessage];
|
|
251
|
+
}
|
|
252
|
+
return historyMessages;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Merge tool calls: Update existing calls by ID or add new ones.
|
|
257
|
+
* This allows updating tool status (running → completed/error) without losing other calls.
|
|
258
|
+
*/
|
|
259
|
+
function mergeToolCalls(
|
|
260
|
+
existing: readonly ToolCallInfo[] | undefined,
|
|
261
|
+
incoming: readonly ToolCallInfo[] | undefined
|
|
262
|
+
): readonly ToolCallInfo[] | undefined {
|
|
263
|
+
if (!incoming || incoming.length === 0) {
|
|
264
|
+
return existing;
|
|
265
|
+
}
|
|
266
|
+
if (!existing || existing.length === 0) {
|
|
267
|
+
return incoming;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Create a map of existing calls for efficient lookup
|
|
271
|
+
const callMap = new Map<string, ToolCallInfo>();
|
|
272
|
+
for (const call of existing) {
|
|
273
|
+
callMap.set(call.id, call);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Merge incoming calls
|
|
277
|
+
for (const call of incoming) {
|
|
278
|
+
const existingCall = callMap.get(call.id);
|
|
279
|
+
if (existingCall) {
|
|
280
|
+
// Merge: keep existing args if incoming doesn't have them
|
|
281
|
+
callMap.set(call.id, {
|
|
282
|
+
...existingCall,
|
|
283
|
+
...call,
|
|
284
|
+
// Preserve arguments if not provided in incoming
|
|
285
|
+
arguments: Object.keys(call.arguments).length > 0 ? call.arguments : existingCall.arguments,
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
// Add new call
|
|
289
|
+
callMap.set(call.id, call);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return Array.from(callMap.values());
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// =============================================================================
|
|
297
|
+
// Reducer
|
|
298
|
+
// =============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Messages state reducer
|
|
302
|
+
*
|
|
303
|
+
* Uses a split architecture for optimal rendering:
|
|
304
|
+
* - historyMessages: Completed messages (for <Static>)
|
|
305
|
+
* - pendingMessage: Currently streaming message (causes re-renders)
|
|
306
|
+
*
|
|
307
|
+
* @param state - Current messages state
|
|
308
|
+
* @param action - Action to apply
|
|
309
|
+
* @returns New messages state
|
|
310
|
+
*/
|
|
311
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Reducer with many action types for message state management
|
|
312
|
+
function messagesReducer(state: MessagesState, action: MessagesAction): MessagesState {
|
|
313
|
+
switch (action.type) {
|
|
314
|
+
case "ADD_MESSAGE": {
|
|
315
|
+
const isStreaming = action.message.isStreaming ?? false;
|
|
316
|
+
|
|
317
|
+
if (isStreaming) {
|
|
318
|
+
// Streaming message becomes pendingMessage
|
|
319
|
+
return {
|
|
320
|
+
...state,
|
|
321
|
+
pendingMessage: action.message,
|
|
322
|
+
messages: computeMessages(state.historyMessages, action.message),
|
|
323
|
+
isStreaming: true,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Non-streaming message goes directly to history
|
|
327
|
+
const newHistory = [...state.historyMessages, action.message];
|
|
328
|
+
return {
|
|
329
|
+
...state,
|
|
330
|
+
historyMessages: newHistory,
|
|
331
|
+
messages: computeMessages(newHistory, state.pendingMessage),
|
|
332
|
+
isStreaming: state.pendingMessage?.isStreaming ?? false,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
case "UPDATE_MESSAGE": {
|
|
337
|
+
// Check if updating pending message
|
|
338
|
+
if (state.pendingMessage?.id === action.id) {
|
|
339
|
+
// Merge toolCalls instead of replacing
|
|
340
|
+
const mergedToolCalls = mergeToolCalls(
|
|
341
|
+
state.pendingMessage.toolCalls,
|
|
342
|
+
action.updates.toolCalls
|
|
343
|
+
);
|
|
344
|
+
const updatedPending = {
|
|
345
|
+
...state.pendingMessage,
|
|
346
|
+
...action.updates,
|
|
347
|
+
toolCalls: mergedToolCalls,
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
...state,
|
|
351
|
+
pendingMessage: updatedPending,
|
|
352
|
+
messages: computeMessages(state.historyMessages, updatedPending),
|
|
353
|
+
isStreaming: updatedPending.isStreaming ?? false,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Update in history
|
|
358
|
+
const messageIndex = state.historyMessages.findIndex((m) => m.id === action.id);
|
|
359
|
+
if (messageIndex === -1) {
|
|
360
|
+
return state;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const updatedHistory = [...state.historyMessages];
|
|
364
|
+
const existingMessage = updatedHistory[messageIndex];
|
|
365
|
+
if (existingMessage) {
|
|
366
|
+
// Merge toolCalls for history messages too
|
|
367
|
+
const mergedToolCalls = mergeToolCalls(existingMessage.toolCalls, action.updates.toolCalls);
|
|
368
|
+
updatedHistory[messageIndex] = {
|
|
369
|
+
...existingMessage,
|
|
370
|
+
...action.updates,
|
|
371
|
+
toolCalls: mergedToolCalls,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
...state,
|
|
377
|
+
historyMessages: updatedHistory,
|
|
378
|
+
messages: computeMessages(updatedHistory, state.pendingMessage),
|
|
379
|
+
isStreaming: state.pendingMessage?.isStreaming ?? false,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case "APPEND_TO_MESSAGE": {
|
|
384
|
+
// Appending only makes sense for pending (streaming) message
|
|
385
|
+
if (state.pendingMessage?.id === action.id) {
|
|
386
|
+
const updatedPending = {
|
|
387
|
+
...state.pendingMessage,
|
|
388
|
+
content: state.pendingMessage.content + action.content,
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
...state,
|
|
392
|
+
pendingMessage: updatedPending,
|
|
393
|
+
messages: computeMessages(state.historyMessages, updatedPending),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Fallback: update in history (for backward compat)
|
|
398
|
+
const messageIndex = state.historyMessages.findIndex((m) => m.id === action.id);
|
|
399
|
+
if (messageIndex === -1) {
|
|
400
|
+
return state;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const updatedHistory = [...state.historyMessages];
|
|
404
|
+
const existingMessage = updatedHistory[messageIndex];
|
|
405
|
+
if (existingMessage) {
|
|
406
|
+
updatedHistory[messageIndex] = {
|
|
407
|
+
...existingMessage,
|
|
408
|
+
content: existingMessage.content + action.content,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
...state,
|
|
414
|
+
historyMessages: updatedHistory,
|
|
415
|
+
messages: computeMessages(updatedHistory, state.pendingMessage),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
case "SET_MESSAGES": {
|
|
420
|
+
// Separate streaming and non-streaming messages
|
|
421
|
+
const streaming = action.messages.find((m) => m.isStreaming === true) ?? null;
|
|
422
|
+
const history = action.messages.filter((m) => m.isStreaming !== true);
|
|
423
|
+
return {
|
|
424
|
+
historyMessages: history,
|
|
425
|
+
pendingMessage: streaming,
|
|
426
|
+
messages: action.messages,
|
|
427
|
+
isStreaming: streaming !== null,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
case "CLEAR_MESSAGES":
|
|
432
|
+
return {
|
|
433
|
+
...initialState,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
case "SET_STREAMING":
|
|
437
|
+
return {
|
|
438
|
+
...state,
|
|
439
|
+
isStreaming: action.isStreaming,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
case "COMMIT_PENDING_MESSAGE": {
|
|
443
|
+
if (!state.pendingMessage) {
|
|
444
|
+
return state;
|
|
445
|
+
}
|
|
446
|
+
// Move pending to history with isStreaming: false
|
|
447
|
+
const completedMessage = {
|
|
448
|
+
...state.pendingMessage,
|
|
449
|
+
isStreaming: false,
|
|
450
|
+
};
|
|
451
|
+
const newHistory = [...state.historyMessages, completedMessage];
|
|
452
|
+
return {
|
|
453
|
+
historyMessages: newHistory,
|
|
454
|
+
pendingMessage: null,
|
|
455
|
+
messages: newHistory,
|
|
456
|
+
isStreaming: false,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
case "SPLIT_MESSAGE": {
|
|
461
|
+
if (!state.pendingMessage) {
|
|
462
|
+
return state;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const content = state.pendingMessage.content;
|
|
466
|
+
if (action.splitIndex <= 0 || action.splitIndex >= content.length) {
|
|
467
|
+
return state;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Create completed portion for history
|
|
471
|
+
const completedMessage: Message = {
|
|
472
|
+
...state.pendingMessage,
|
|
473
|
+
id: generateMessageId(), // New ID for the split-off portion
|
|
474
|
+
content: content.slice(0, action.splitIndex),
|
|
475
|
+
isStreaming: false,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// Keep remainder as pending - mark as continuation of split message
|
|
479
|
+
const remainingMessage: Message = {
|
|
480
|
+
...state.pendingMessage,
|
|
481
|
+
content: content.slice(action.splitIndex),
|
|
482
|
+
isContinuation: true,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const newHistory = [...state.historyMessages, completedMessage];
|
|
486
|
+
return {
|
|
487
|
+
historyMessages: newHistory,
|
|
488
|
+
pendingMessage: remainingMessage,
|
|
489
|
+
messages: computeMessages(newHistory, remainingMessage),
|
|
490
|
+
isStreaming: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
case "APPEND_TO_THINKING": {
|
|
495
|
+
// Appending thinking only makes sense for pending (streaming) message
|
|
496
|
+
if (state.pendingMessage?.id === action.id) {
|
|
497
|
+
const updatedPending = {
|
|
498
|
+
...state.pendingMessage,
|
|
499
|
+
thinking: (state.pendingMessage.thinking ?? "") + action.thinking,
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
...state,
|
|
503
|
+
pendingMessage: updatedPending,
|
|
504
|
+
messages: computeMessages(state.historyMessages, updatedPending),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return state;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
case "ADD_TOOL_GROUP": {
|
|
511
|
+
// Create a new tool_group message (Gemini-style independent tool execution)
|
|
512
|
+
const toolGroupMessage: Message = {
|
|
513
|
+
id: generateMessageId(),
|
|
514
|
+
role: "tool_group",
|
|
515
|
+
content: "", // tool_group doesn't need content
|
|
516
|
+
timestamp: new Date(),
|
|
517
|
+
toolCalls: action.toolCalls,
|
|
518
|
+
isStreaming: false,
|
|
519
|
+
};
|
|
520
|
+
const newHistory = [...state.historyMessages, toolGroupMessage];
|
|
521
|
+
return {
|
|
522
|
+
...state,
|
|
523
|
+
historyMessages: newHistory,
|
|
524
|
+
messages: computeMessages(newHistory, state.pendingMessage),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
case "UPDATE_TOOL_GROUP": {
|
|
529
|
+
// Find and update the tool_group message with the specified groupId
|
|
530
|
+
const groupIndex = state.historyMessages.findIndex(
|
|
531
|
+
(m) => m.id === action.groupId && m.role === "tool_group"
|
|
532
|
+
);
|
|
533
|
+
if (groupIndex === -1) {
|
|
534
|
+
return state;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const updatedHistory = [...state.historyMessages];
|
|
538
|
+
const existingGroup = updatedHistory[groupIndex];
|
|
539
|
+
if (existingGroup) {
|
|
540
|
+
// Merge the tool call update into existing toolCalls
|
|
541
|
+
const mergedToolCalls = mergeToolCalls(existingGroup.toolCalls, [action.toolCall]);
|
|
542
|
+
updatedHistory[groupIndex] = {
|
|
543
|
+
...existingGroup,
|
|
544
|
+
toolCalls: mergedToolCalls,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
...state,
|
|
550
|
+
historyMessages: updatedHistory,
|
|
551
|
+
messages: computeMessages(updatedHistory, state.pendingMessage),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
default:
|
|
556
|
+
// Exhaustive check - TypeScript will error if a case is missing
|
|
557
|
+
return state;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// =============================================================================
|
|
562
|
+
// ID Generation
|
|
563
|
+
// =============================================================================
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Generate a unique message ID
|
|
567
|
+
*
|
|
568
|
+
* Uses crypto.randomUUID() when available, falls back to timestamp-based ID
|
|
569
|
+
*/
|
|
570
|
+
function generateMessageId(): string {
|
|
571
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
572
|
+
return crypto.randomUUID();
|
|
573
|
+
}
|
|
574
|
+
// Fallback for environments without crypto.randomUUID
|
|
575
|
+
return `msg-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// =============================================================================
|
|
579
|
+
// Context
|
|
580
|
+
// =============================================================================
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Context value interface
|
|
584
|
+
*/
|
|
585
|
+
export interface MessagesContextValue {
|
|
586
|
+
/** Current messages state */
|
|
587
|
+
readonly state: MessagesState;
|
|
588
|
+
/** Dispatch function for state updates */
|
|
589
|
+
readonly dispatch: Dispatch<MessagesAction>;
|
|
590
|
+
/** All messages in the conversation (history + pending) */
|
|
591
|
+
readonly messages: readonly Message[];
|
|
592
|
+
/** Completed messages for <Static> rendering (never re-render) */
|
|
593
|
+
readonly historyMessages: readonly Message[];
|
|
594
|
+
/** Currently streaming message (only this causes re-renders) */
|
|
595
|
+
readonly pendingMessage: Message | null;
|
|
596
|
+
/** Add a new message, returns the generated ID */
|
|
597
|
+
readonly addMessage: (message: Omit<Message, "id" | "timestamp">) => string;
|
|
598
|
+
/** Update an existing message */
|
|
599
|
+
readonly updateMessage: (id: string, updates: Partial<Omit<Message, "id">>) => void;
|
|
600
|
+
/** Append content to a message (for streaming) */
|
|
601
|
+
readonly appendToMessage: (id: string, content: string) => void;
|
|
602
|
+
/** Append thinking/reasoning content to a message (for streaming) */
|
|
603
|
+
readonly appendToThinking: (id: string, thinking: string) => void;
|
|
604
|
+
/** Replace the entire message list */
|
|
605
|
+
readonly setMessages: (messages: readonly Message[]) => void;
|
|
606
|
+
/** Clear all messages */
|
|
607
|
+
readonly clearMessages: () => void;
|
|
608
|
+
/** Commit pending message to history (for Static rendering) */
|
|
609
|
+
readonly commitPendingMessage: () => void;
|
|
610
|
+
/** Split a long streaming message at a safe point (e.g., paragraph boundary) */
|
|
611
|
+
readonly splitMessageAtSafePoint: (splitIndex: number) => void;
|
|
612
|
+
/** Add a new tool_group message (Gemini-style), returns the group ID */
|
|
613
|
+
readonly addToolGroup: (toolCalls: readonly ToolCallInfo[]) => string;
|
|
614
|
+
/** Update an existing tool_group message with a tool call update */
|
|
615
|
+
readonly updateToolGroup: (groupId: string, toolCall: ToolCallInfo) => void;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* React context for messages state
|
|
620
|
+
*
|
|
621
|
+
* Initialized as undefined to detect usage outside provider
|
|
622
|
+
*/
|
|
623
|
+
const MessagesContext = createContext<MessagesContextValue | undefined>(undefined);
|
|
624
|
+
|
|
625
|
+
// =============================================================================
|
|
626
|
+
// Hook
|
|
627
|
+
// =============================================================================
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Hook to access the messages state and actions
|
|
631
|
+
*
|
|
632
|
+
* Must be used within a MessagesProvider component.
|
|
633
|
+
*
|
|
634
|
+
* @returns The current messages context value with state and actions
|
|
635
|
+
* @throws Error if used outside MessagesProvider
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* ```tsx
|
|
639
|
+
* function ChatComponent() {
|
|
640
|
+
* const { messages, addMessage, appendToMessage, clearMessages } = useMessages();
|
|
641
|
+
*
|
|
642
|
+
* // Add a new message
|
|
643
|
+
* const handleSend = (content: string) => {
|
|
644
|
+
* const id = addMessage({ role: 'user', content });
|
|
645
|
+
* console.log('Created message:', id);
|
|
646
|
+
* };
|
|
647
|
+
*
|
|
648
|
+
* // Handle streaming content
|
|
649
|
+
* const handleStream = (id: string, chunk: string) => {
|
|
650
|
+
* appendToMessage(id, chunk);
|
|
651
|
+
* };
|
|
652
|
+
*
|
|
653
|
+
* // Clear conversation
|
|
654
|
+
* const handleClear = () => clearMessages();
|
|
655
|
+
*
|
|
656
|
+
* return <Box>...</Box>;
|
|
657
|
+
* }
|
|
658
|
+
* ```
|
|
659
|
+
*/
|
|
660
|
+
export function useMessages(): MessagesContextValue {
|
|
661
|
+
const context = useContext(MessagesContext);
|
|
662
|
+
|
|
663
|
+
if (context === undefined) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
"useMessages must be used within a MessagesProvider. " +
|
|
666
|
+
"Ensure your component is wrapped in <MessagesProvider>."
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return context;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// =============================================================================
|
|
674
|
+
// Provider Props
|
|
675
|
+
// =============================================================================
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Props for the MessagesProvider component
|
|
679
|
+
*/
|
|
680
|
+
export interface MessagesProviderProps {
|
|
681
|
+
/**
|
|
682
|
+
* Initial messages to populate the conversation
|
|
683
|
+
*/
|
|
684
|
+
readonly initialMessages?: readonly Message[];
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Children to render within the messages context
|
|
688
|
+
*/
|
|
689
|
+
readonly children: ReactNode;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// =============================================================================
|
|
693
|
+
// Provider Component
|
|
694
|
+
// =============================================================================
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Messages state provider component
|
|
698
|
+
*
|
|
699
|
+
* Provides messages state context to all child components, enabling
|
|
700
|
+
* access to the message list and actions via the useMessages hook.
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* ```tsx
|
|
704
|
+
* // Using default initial state
|
|
705
|
+
* <MessagesProvider>
|
|
706
|
+
* <ChatApp />
|
|
707
|
+
* </MessagesProvider>
|
|
708
|
+
*
|
|
709
|
+
* // Using initial messages
|
|
710
|
+
* <MessagesProvider initialMessages={[{ id: '1', role: 'system', content: 'Hello', timestamp: new Date() }]}>
|
|
711
|
+
* <ChatApp />
|
|
712
|
+
* </MessagesProvider>
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
export function MessagesProvider({
|
|
716
|
+
initialMessages,
|
|
717
|
+
children,
|
|
718
|
+
}: MessagesProviderProps): React.JSX.Element {
|
|
719
|
+
// State management with useReducer
|
|
720
|
+
const [state, dispatch] = useReducer(
|
|
721
|
+
messagesReducer,
|
|
722
|
+
initialMessages,
|
|
723
|
+
(messages): MessagesState => {
|
|
724
|
+
// Separate streaming and non-streaming from initial messages
|
|
725
|
+
const streaming = messages?.find((m) => m.isStreaming === true) ?? null;
|
|
726
|
+
const history = messages?.filter((m) => m.isStreaming !== true) ?? [];
|
|
727
|
+
return {
|
|
728
|
+
historyMessages: history,
|
|
729
|
+
pendingMessage: streaming,
|
|
730
|
+
messages: messages ?? [],
|
|
731
|
+
isStreaming: streaming !== null,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Add a new message to the conversation
|
|
738
|
+
* @returns The generated message ID
|
|
739
|
+
*/
|
|
740
|
+
const addMessage = useCallback((message: Omit<Message, "id" | "timestamp">): string => {
|
|
741
|
+
const id = generateMessageId();
|
|
742
|
+
const fullMessage: Message = {
|
|
743
|
+
...message,
|
|
744
|
+
id,
|
|
745
|
+
timestamp: new Date(),
|
|
746
|
+
};
|
|
747
|
+
dispatch({ type: "ADD_MESSAGE", message: fullMessage });
|
|
748
|
+
return id;
|
|
749
|
+
}, []);
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Update an existing message
|
|
753
|
+
*/
|
|
754
|
+
const updateMessage = useCallback((id: string, updates: Partial<Omit<Message, "id">>): void => {
|
|
755
|
+
dispatch({ type: "UPDATE_MESSAGE", id, updates });
|
|
756
|
+
}, []);
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Append content to an existing message (for streaming)
|
|
760
|
+
*/
|
|
761
|
+
const appendToMessage = useCallback((id: string, content: string): void => {
|
|
762
|
+
dispatch({ type: "APPEND_TO_MESSAGE", id, content });
|
|
763
|
+
}, []);
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Append thinking/reasoning content to an existing message (for streaming)
|
|
767
|
+
*/
|
|
768
|
+
const appendToThinking = useCallback((id: string, thinking: string): void => {
|
|
769
|
+
dispatch({ type: "APPEND_TO_THINKING", id, thinking });
|
|
770
|
+
}, []);
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Clear all messages
|
|
774
|
+
*/
|
|
775
|
+
const clearMessages = useCallback((): void => {
|
|
776
|
+
dispatch({ type: "CLEAR_MESSAGES" });
|
|
777
|
+
}, []);
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Replace the entire message list
|
|
781
|
+
*/
|
|
782
|
+
const setMessages = useCallback((messages: readonly Message[]): void => {
|
|
783
|
+
dispatch({ type: "SET_MESSAGES", messages });
|
|
784
|
+
}, []);
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Commit pending message to history (for Static rendering)
|
|
788
|
+
*/
|
|
789
|
+
const commitPendingMessage = useCallback((): void => {
|
|
790
|
+
dispatch({ type: "COMMIT_PENDING_MESSAGE" });
|
|
791
|
+
}, []);
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Split a long streaming message at a safe point
|
|
795
|
+
*/
|
|
796
|
+
const splitMessageAtSafePoint = useCallback((splitIndex: number): void => {
|
|
797
|
+
dispatch({ type: "SPLIT_MESSAGE", splitIndex });
|
|
798
|
+
}, []);
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Add a new tool_group message (Gemini-style independent tool execution)
|
|
802
|
+
* @returns The generated group ID
|
|
803
|
+
*/
|
|
804
|
+
const addToolGroup = useCallback((toolCalls: readonly ToolCallInfo[]): string => {
|
|
805
|
+
const id = generateMessageId();
|
|
806
|
+
// Dispatch with ID embedded in toolCalls (reducer will use generateMessageId for the message)
|
|
807
|
+
// Actually, we need the reducer to return the ID. For now, we generate it here and pass via action.
|
|
808
|
+
const toolGroupMessage: Message = {
|
|
809
|
+
id,
|
|
810
|
+
role: "tool_group",
|
|
811
|
+
content: "",
|
|
812
|
+
timestamp: new Date(),
|
|
813
|
+
toolCalls,
|
|
814
|
+
isStreaming: false,
|
|
815
|
+
};
|
|
816
|
+
dispatch({ type: "ADD_MESSAGE", message: toolGroupMessage });
|
|
817
|
+
return id;
|
|
818
|
+
}, []);
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Update an existing tool_group message with a tool call update
|
|
822
|
+
*/
|
|
823
|
+
const updateToolGroup = useCallback((groupId: string, toolCall: ToolCallInfo): void => {
|
|
824
|
+
dispatch({ type: "UPDATE_TOOL_GROUP", groupId, toolCall });
|
|
825
|
+
}, []);
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Memoized context value
|
|
829
|
+
*/
|
|
830
|
+
const contextValue = useMemo<MessagesContextValue>(
|
|
831
|
+
() => ({
|
|
832
|
+
state,
|
|
833
|
+
dispatch,
|
|
834
|
+
messages: state.messages,
|
|
835
|
+
historyMessages: state.historyMessages,
|
|
836
|
+
pendingMessage: state.pendingMessage,
|
|
837
|
+
addMessage,
|
|
838
|
+
updateMessage,
|
|
839
|
+
appendToMessage,
|
|
840
|
+
appendToThinking,
|
|
841
|
+
setMessages,
|
|
842
|
+
clearMessages,
|
|
843
|
+
commitPendingMessage,
|
|
844
|
+
splitMessageAtSafePoint,
|
|
845
|
+
addToolGroup,
|
|
846
|
+
updateToolGroup,
|
|
847
|
+
}),
|
|
848
|
+
[
|
|
849
|
+
state,
|
|
850
|
+
addMessage,
|
|
851
|
+
updateMessage,
|
|
852
|
+
appendToMessage,
|
|
853
|
+
appendToThinking,
|
|
854
|
+
setMessages,
|
|
855
|
+
clearMessages,
|
|
856
|
+
commitPendingMessage,
|
|
857
|
+
splitMessageAtSafePoint,
|
|
858
|
+
addToolGroup,
|
|
859
|
+
updateToolGroup,
|
|
860
|
+
]
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
return <MessagesContext value={contextValue}>{children}</MessagesContext>;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// =============================================================================
|
|
867
|
+
// Exports
|
|
868
|
+
// =============================================================================
|
|
869
|
+
|
|
870
|
+
export { MessagesContext, initialState };
|