@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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamingText Component (T019)
|
|
3
|
+
*
|
|
4
|
+
* Displays text content with an animated blinking cursor while streaming.
|
|
5
|
+
* Supports optional typewriter effect for smoother character-by-character display.
|
|
6
|
+
* The cursor is removed when streaming completes, and an optional callback
|
|
7
|
+
* is invoked.
|
|
8
|
+
*
|
|
9
|
+
* @module tui/components/Messages/StreamingText
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Text } from "ink";
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
14
|
+
import { useAnimation } from "../../context/AnimationContext.js";
|
|
15
|
+
import { sanitize } from "../../utils/textSanitizer.js";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props for the StreamingText component.
|
|
23
|
+
*/
|
|
24
|
+
export interface StreamingTextProps {
|
|
25
|
+
/** The text content to display */
|
|
26
|
+
readonly content: string;
|
|
27
|
+
/** Whether the text is currently streaming */
|
|
28
|
+
readonly isStreaming: boolean;
|
|
29
|
+
/** Character to use for the cursor (default: '▊') */
|
|
30
|
+
readonly cursorChar?: string;
|
|
31
|
+
/** Whether the cursor should blink (default: true) */
|
|
32
|
+
readonly cursorBlink?: boolean;
|
|
33
|
+
/** Callback invoked when streaming completes */
|
|
34
|
+
readonly onComplete?: () => void;
|
|
35
|
+
/** Enable typewriter effect for smoother display (default: true) */
|
|
36
|
+
readonly typewriterEffect?: boolean;
|
|
37
|
+
/** Delay between characters in ms when typewriter is enabled (default: 8) */
|
|
38
|
+
readonly typewriterSpeed?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Constants
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/** Default cursor character */
|
|
46
|
+
const DEFAULT_CURSOR_CHAR = "▊";
|
|
47
|
+
|
|
48
|
+
/** Default typewriter speed (characters per interval) */
|
|
49
|
+
const DEFAULT_TYPEWRITER_SPEED_MS = 8;
|
|
50
|
+
|
|
51
|
+
/** Characters to release per tick for faster catch-up */
|
|
52
|
+
const CHARS_PER_TICK = 3;
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Main Component
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* StreamingText displays text with an animated cursor while streaming.
|
|
60
|
+
*
|
|
61
|
+
* Features:
|
|
62
|
+
* - Displays text content
|
|
63
|
+
* - Shows blinking cursor at end while streaming
|
|
64
|
+
* - Removes cursor when streaming completes
|
|
65
|
+
* - Supports cursor customization (character and blink behavior)
|
|
66
|
+
* - Optional typewriter effect for smoother display
|
|
67
|
+
* - Calls onComplete callback when isStreaming changes to false
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* // Basic usage
|
|
72
|
+
* <StreamingText
|
|
73
|
+
* content={text}
|
|
74
|
+
* isStreaming={isTyping}
|
|
75
|
+
* />
|
|
76
|
+
*
|
|
77
|
+
* // With completion callback
|
|
78
|
+
* <StreamingText
|
|
79
|
+
* content={text}
|
|
80
|
+
* isStreaming={isTyping}
|
|
81
|
+
* onComplete={() => enableInput()}
|
|
82
|
+
* />
|
|
83
|
+
*
|
|
84
|
+
* // Custom cursor
|
|
85
|
+
* <StreamingText
|
|
86
|
+
* content={text}
|
|
87
|
+
* isStreaming={isTyping}
|
|
88
|
+
* cursorChar="_"
|
|
89
|
+
* cursorBlink={false}
|
|
90
|
+
* />
|
|
91
|
+
*
|
|
92
|
+
* // With typewriter effect
|
|
93
|
+
* <StreamingText
|
|
94
|
+
* content={text}
|
|
95
|
+
* isStreaming={isTyping}
|
|
96
|
+
* typewriterEffect={true}
|
|
97
|
+
* typewriterSpeed={10}
|
|
98
|
+
* />
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function StreamingText({
|
|
102
|
+
content,
|
|
103
|
+
isStreaming,
|
|
104
|
+
cursorChar = DEFAULT_CURSOR_CHAR,
|
|
105
|
+
cursorBlink = true,
|
|
106
|
+
onComplete,
|
|
107
|
+
typewriterEffect = true,
|
|
108
|
+
typewriterSpeed = DEFAULT_TYPEWRITER_SPEED_MS,
|
|
109
|
+
}: StreamingTextProps): React.JSX.Element {
|
|
110
|
+
// Track cursor visibility for blinking effect
|
|
111
|
+
const [cursorVisible, setCursorVisible] = useState(true);
|
|
112
|
+
|
|
113
|
+
// Track previous streaming state to detect completion
|
|
114
|
+
const prevIsStreamingRef = useRef<boolean | null>(null);
|
|
115
|
+
|
|
116
|
+
// Track if this is the initial mount (for immediate display when not streaming)
|
|
117
|
+
const isInitialMountRef = useRef(true);
|
|
118
|
+
|
|
119
|
+
// Typewriter effect state - initialize to full length if not streaming on mount
|
|
120
|
+
const [displayedLength, setDisplayedLength] = useState(() => (isStreaming ? 0 : content.length));
|
|
121
|
+
const prevContentLengthRef = useRef(content.length);
|
|
122
|
+
|
|
123
|
+
// Handle typewriter effect - gradually reveal characters
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
// Skip typewriter if disabled
|
|
126
|
+
if (!typewriterEffect) {
|
|
127
|
+
setDisplayedLength(content.length);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// When streaming stops, immediately show all content
|
|
132
|
+
if (!isStreaming) {
|
|
133
|
+
setDisplayedLength(content.length);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Mark initial mount as complete
|
|
138
|
+
isInitialMountRef.current = false;
|
|
139
|
+
|
|
140
|
+
// If we're caught up, nothing to do
|
|
141
|
+
if (displayedLength >= content.length) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Release characters progressively
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
setDisplayedLength((prev) => {
|
|
148
|
+
// Release multiple chars per tick for faster catch-up when behind
|
|
149
|
+
const behind = content.length - prev;
|
|
150
|
+
const charsToRelease = behind > 20 ? Math.min(behind, CHARS_PER_TICK * 3) : CHARS_PER_TICK;
|
|
151
|
+
return Math.min(prev + charsToRelease, content.length);
|
|
152
|
+
});
|
|
153
|
+
}, typewriterSpeed);
|
|
154
|
+
|
|
155
|
+
return () => clearTimeout(timer);
|
|
156
|
+
}, [content.length, displayedLength, isStreaming, typewriterEffect, typewriterSpeed]);
|
|
157
|
+
|
|
158
|
+
// Reset displayed length when content is cleared (new message)
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (content.length < prevContentLengthRef.current) {
|
|
161
|
+
// Content was reset (new message started)
|
|
162
|
+
setDisplayedLength(0);
|
|
163
|
+
}
|
|
164
|
+
prevContentLengthRef.current = content.length;
|
|
165
|
+
}, [content.length]);
|
|
166
|
+
|
|
167
|
+
// Use global animation context for cursor blink to prevent flickering
|
|
168
|
+
const { frame, isPaused } = useAnimation();
|
|
169
|
+
|
|
170
|
+
// Derive cursor visibility from animation frame instead of independent timer
|
|
171
|
+
// This prevents competing setIntervals from causing flicker
|
|
172
|
+
const derivedCursorVisible = useMemo(() => {
|
|
173
|
+
// Always show cursor when not streaming or blink disabled
|
|
174
|
+
if (!isStreaming || !cursorBlink) return true;
|
|
175
|
+
// Show cursor when animation is paused (e.g., input focused)
|
|
176
|
+
if (isPaused) return true;
|
|
177
|
+
// Toggle every ~4 frames (~500ms blink cycle at 120ms/200ms tick rate)
|
|
178
|
+
return Math.floor(frame / 4) % 2 === 0;
|
|
179
|
+
}, [frame, isPaused, isStreaming, cursorBlink]);
|
|
180
|
+
|
|
181
|
+
// Sync local state with derived value for compatibility with existing logic
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
setCursorVisible(derivedCursorVisible);
|
|
184
|
+
}, [derivedCursorVisible]);
|
|
185
|
+
|
|
186
|
+
// Handle streaming completion callback
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
// Only trigger callback if we were previously streaming and now we're not
|
|
189
|
+
// Skip on initial mount (prevIsStreamingRef.current is null)
|
|
190
|
+
if (prevIsStreamingRef.current === true && !isStreaming) {
|
|
191
|
+
onComplete?.();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update ref after callback logic
|
|
195
|
+
prevIsStreamingRef.current = isStreaming;
|
|
196
|
+
}, [isStreaming, onComplete]);
|
|
197
|
+
|
|
198
|
+
// Sanitize content (normalize line endings, strip dangerous ANSI)
|
|
199
|
+
const sanitizedContent = useMemo(() => sanitize(content), [content]);
|
|
200
|
+
|
|
201
|
+
// Determine what text to display
|
|
202
|
+
const displayText = typewriterEffect
|
|
203
|
+
? sanitizedContent.slice(0, displayedLength)
|
|
204
|
+
: sanitizedContent;
|
|
205
|
+
|
|
206
|
+
// Determine cursor to display (show cursor while typewriter is still catching up)
|
|
207
|
+
const showCursor = isStreaming || (typewriterEffect && displayedLength < sanitizedContent.length);
|
|
208
|
+
const cursor = showCursor && cursorVisible ? cursorChar : "";
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Text wrap="wrap">
|
|
212
|
+
{displayText}
|
|
213
|
+
{cursor}
|
|
214
|
+
</Text>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThinkingBlock Component
|
|
3
|
+
*
|
|
4
|
+
* Displays thinking/reasoning content from extended thinking models
|
|
5
|
+
* with collapsible UI, duration display, and streaming indicator.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Collapsible: Toggle between collapsed (1-line summary) and expanded view
|
|
9
|
+
* - Duration display: Shows how long thinking took (e.g., "Thought for 3.2s")
|
|
10
|
+
* - Streaming indicator: Shows spinner while thinking is in progress
|
|
11
|
+
* - Character count: Shows length in collapsed mode (e.g., "[...] (1,234 chars)")
|
|
12
|
+
* - Keyboard toggle: 't' key to toggle expand/collapse
|
|
13
|
+
* - Visual distinction: Box border to separate from main content
|
|
14
|
+
* - Tool calls: Optional inline display of tool calls within thinking block
|
|
15
|
+
*
|
|
16
|
+
* @module tui/components/Messages/ThinkingBlock
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Box, Text } from "ink";
|
|
20
|
+
import type React from "react";
|
|
21
|
+
import { useMemo } from "react";
|
|
22
|
+
import { useAnimationFrame } from "../../context/AnimationContext.js";
|
|
23
|
+
import type { ToolCallInfo } from "../../context/MessagesContext.js";
|
|
24
|
+
import { useCollapsible } from "../../hooks/useCollapsible.js";
|
|
25
|
+
import type { ThinkingDisplayMode } from "../../i18n/index.js";
|
|
26
|
+
import { useTheme } from "../../theme/index.js";
|
|
27
|
+
import { SPINNER_STYLES, Spinner } from "../common/Spinner.js";
|
|
28
|
+
|
|
29
|
+
// Spinner frames for tool call status
|
|
30
|
+
const TOOL_SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Types
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Props for the ThinkingBlock component.
|
|
38
|
+
*/
|
|
39
|
+
export interface ThinkingBlockProps {
|
|
40
|
+
/** Thinking/reasoning content to display */
|
|
41
|
+
readonly content: string;
|
|
42
|
+
/** Duration of thinking in milliseconds (optional) */
|
|
43
|
+
readonly durationMs?: number;
|
|
44
|
+
/** Whether thinking is still in progress (shows spinner) */
|
|
45
|
+
readonly isStreaming?: boolean;
|
|
46
|
+
/** Whether initially collapsed (default: true) */
|
|
47
|
+
readonly initialCollapsed?: boolean;
|
|
48
|
+
/** Unique ID for state persistence (optional) */
|
|
49
|
+
readonly persistenceId?: string;
|
|
50
|
+
/** Enable keyboard toggle with 't' key (default: false to avoid conflicts) */
|
|
51
|
+
readonly enableKeyboardToggle?: boolean;
|
|
52
|
+
/** Maximum lines to show in collapsed preview (default: 1) */
|
|
53
|
+
readonly collapsedPreviewLines?: number;
|
|
54
|
+
/** Maximum characters to show in collapsed preview (default: 80) */
|
|
55
|
+
readonly collapsedPreviewChars?: number;
|
|
56
|
+
/** Show character count in header (default: true) */
|
|
57
|
+
readonly showCharCount?: boolean;
|
|
58
|
+
/** Callback when toggle state changes */
|
|
59
|
+
readonly onToggle?: (collapsed: boolean) => void;
|
|
60
|
+
/** Tool calls to display inline within the thinking block */
|
|
61
|
+
readonly toolCalls?: readonly ToolCallInfo[];
|
|
62
|
+
/**
|
|
63
|
+
* Display mode for thinking content.
|
|
64
|
+
* - "full": Show content (default, can expand/collapse)
|
|
65
|
+
* - "compact": Only show header, no content preview, cannot expand
|
|
66
|
+
*/
|
|
67
|
+
readonly displayMode?: ThinkingDisplayMode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Helper Functions
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format character count for display.
|
|
76
|
+
*/
|
|
77
|
+
function formatCharCount(count: number): string {
|
|
78
|
+
if (count < 1000) {
|
|
79
|
+
return `${count} chars`;
|
|
80
|
+
}
|
|
81
|
+
const k = count / 1000;
|
|
82
|
+
return k >= 10 ? `${Math.round(k)}K chars` : `${k.toFixed(1)}K chars`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format duration in milliseconds to human-readable string.
|
|
87
|
+
*/
|
|
88
|
+
function formatThinkingDuration(ms: number): string {
|
|
89
|
+
if (ms < 1000) {
|
|
90
|
+
return `${ms}ms`;
|
|
91
|
+
}
|
|
92
|
+
const seconds = ms / 1000;
|
|
93
|
+
if (seconds < 60) {
|
|
94
|
+
return `${seconds.toFixed(1)}s`;
|
|
95
|
+
}
|
|
96
|
+
const minutes = Math.floor(seconds / 60);
|
|
97
|
+
const remainingSeconds = (seconds % 60).toFixed(0);
|
|
98
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get a preview of the content for collapsed mode.
|
|
103
|
+
*/
|
|
104
|
+
function getPreview(content: string, maxLines: number, maxChars: number): string {
|
|
105
|
+
if (!content) return "";
|
|
106
|
+
|
|
107
|
+
// Split by newlines and take first N lines
|
|
108
|
+
const lines = content.split("\n").slice(0, maxLines);
|
|
109
|
+
let preview = lines.join(" ").trim();
|
|
110
|
+
|
|
111
|
+
// Truncate to max chars
|
|
112
|
+
if (preview.length > maxChars) {
|
|
113
|
+
preview = `${preview.slice(0, maxChars - 3)}...`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return preview;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// Component
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* ThinkingBlock - Collapsible display for model thinking/reasoning content.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```tsx
|
|
128
|
+
* // Basic usage
|
|
129
|
+
* <ThinkingBlock content="Let me think about this..." />
|
|
130
|
+
*
|
|
131
|
+
* // With streaming indicator
|
|
132
|
+
* <ThinkingBlock
|
|
133
|
+
* content={thinkingContent}
|
|
134
|
+
* isStreaming={true}
|
|
135
|
+
* />
|
|
136
|
+
*
|
|
137
|
+
* // With duration and keyboard toggle
|
|
138
|
+
* <ThinkingBlock
|
|
139
|
+
* content={thinkingContent}
|
|
140
|
+
* durationMs={3200}
|
|
141
|
+
* enableKeyboardToggle
|
|
142
|
+
* />
|
|
143
|
+
*
|
|
144
|
+
* // Initially expanded
|
|
145
|
+
* <ThinkingBlock
|
|
146
|
+
* content={thinkingContent}
|
|
147
|
+
* initialCollapsed={false}
|
|
148
|
+
* />
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ThinkingBlock UI inherently complex due to multiple render modes
|
|
152
|
+
export function ThinkingBlock({
|
|
153
|
+
content,
|
|
154
|
+
durationMs,
|
|
155
|
+
isStreaming = false,
|
|
156
|
+
initialCollapsed = true,
|
|
157
|
+
persistenceId,
|
|
158
|
+
enableKeyboardToggle = false,
|
|
159
|
+
collapsedPreviewLines = 1,
|
|
160
|
+
collapsedPreviewChars = 80,
|
|
161
|
+
showCharCount = true,
|
|
162
|
+
onToggle,
|
|
163
|
+
toolCalls,
|
|
164
|
+
displayMode = "full",
|
|
165
|
+
}: ThinkingBlockProps): React.JSX.Element | null {
|
|
166
|
+
const { theme } = useTheme();
|
|
167
|
+
|
|
168
|
+
// Animation frame for tool call spinners
|
|
169
|
+
const frameIndex = useAnimationFrame(TOOL_SPINNER_FRAMES);
|
|
170
|
+
|
|
171
|
+
// In compact mode, force collapsed and disable keyboard toggle
|
|
172
|
+
const isCompactMode = displayMode === "compact";
|
|
173
|
+
const effectiveInitialCollapsed = isCompactMode ? true : initialCollapsed;
|
|
174
|
+
const effectiveKeyboardToggle = isCompactMode ? false : enableKeyboardToggle;
|
|
175
|
+
|
|
176
|
+
const { isCollapsed, toggle: _toggle } = useCollapsible({
|
|
177
|
+
initialCollapsed: effectiveInitialCollapsed,
|
|
178
|
+
toggleKey: effectiveKeyboardToggle ? "t" : undefined,
|
|
179
|
+
keyboardEnabled: effectiveKeyboardToggle,
|
|
180
|
+
persistenceId,
|
|
181
|
+
onToggle,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// In compact mode, always show as collapsed (never expandable)
|
|
185
|
+
const effectiveIsCollapsed = isCompactMode ? true : isCollapsed;
|
|
186
|
+
|
|
187
|
+
// Theme colors
|
|
188
|
+
const thinkingColor = theme.colors.warning ?? "yellow";
|
|
189
|
+
const mutedColor = theme.semantic.text.muted;
|
|
190
|
+
const borderColor = theme.colors.warning ?? "yellow";
|
|
191
|
+
const accentColor = theme.colors.accent ?? "cyan";
|
|
192
|
+
const successColor = theme.colors.success ?? "green";
|
|
193
|
+
const errorColor = theme.colors.error ?? "red";
|
|
194
|
+
|
|
195
|
+
// Memoized values
|
|
196
|
+
const charCount = useMemo(() => content.length, [content]);
|
|
197
|
+
const preview = useMemo(
|
|
198
|
+
() => getPreview(content, collapsedPreviewLines, collapsedPreviewChars),
|
|
199
|
+
[content, collapsedPreviewLines, collapsedPreviewChars]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Don't render if no content
|
|
203
|
+
if (!content && !isStreaming) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Build header text
|
|
208
|
+
const headerParts: string[] = [];
|
|
209
|
+
|
|
210
|
+
// Status icon and text
|
|
211
|
+
if (isStreaming) {
|
|
212
|
+
headerParts.push("Thinking");
|
|
213
|
+
} else if (durationMs !== undefined && durationMs > 0) {
|
|
214
|
+
headerParts.push(`Thought for ${formatThinkingDuration(durationMs)}`);
|
|
215
|
+
} else {
|
|
216
|
+
headerParts.push("Thought");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Character count (when collapsed or streaming)
|
|
220
|
+
if (showCharCount && charCount > 0 && (effectiveIsCollapsed || isStreaming)) {
|
|
221
|
+
headerParts.push(`(${formatCharCount(charCount)})`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Toggle hint (not shown in compact mode since it's not toggleable)
|
|
225
|
+
if (!isStreaming && !isCompactMode) {
|
|
226
|
+
headerParts.push(effectiveIsCollapsed ? "[expand ▼]" : "[collapse ▲]");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<Box
|
|
231
|
+
flexDirection="column"
|
|
232
|
+
marginLeft={2}
|
|
233
|
+
marginTop={0}
|
|
234
|
+
marginBottom={0}
|
|
235
|
+
borderStyle="round"
|
|
236
|
+
borderColor={borderColor}
|
|
237
|
+
paddingX={1}
|
|
238
|
+
borderLeft
|
|
239
|
+
borderRight={false}
|
|
240
|
+
borderTop={false}
|
|
241
|
+
borderBottom={false}
|
|
242
|
+
>
|
|
243
|
+
{/* Header row with toggle */}
|
|
244
|
+
<Box flexDirection="row" alignItems="center">
|
|
245
|
+
{/* Streaming spinner */}
|
|
246
|
+
{isStreaming && (
|
|
247
|
+
<>
|
|
248
|
+
<Spinner color={thinkingColor} frames={SPINNER_STYLES.dots} />
|
|
249
|
+
<Text> </Text>
|
|
250
|
+
</>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Icon */}
|
|
254
|
+
<Text color={thinkingColor}>[...] </Text>
|
|
255
|
+
|
|
256
|
+
{/* Header text - clickable area concept (visual only in TUI) */}
|
|
257
|
+
<Text
|
|
258
|
+
color={thinkingColor}
|
|
259
|
+
dimColor={!isStreaming}
|
|
260
|
+
italic
|
|
261
|
+
// In terminal, we can't have onClick, but visual cue
|
|
262
|
+
>
|
|
263
|
+
{headerParts.join(" ")}
|
|
264
|
+
</Text>
|
|
265
|
+
|
|
266
|
+
{/* Keyboard hint */}
|
|
267
|
+
{effectiveKeyboardToggle && !isStreaming && (
|
|
268
|
+
<Text color={mutedColor} dimColor>
|
|
269
|
+
{" "}
|
|
270
|
+
(press 't')
|
|
271
|
+
</Text>
|
|
272
|
+
)}
|
|
273
|
+
</Box>
|
|
274
|
+
|
|
275
|
+
{/* Content area - In compact mode, don't show any content (no preview) */}
|
|
276
|
+
{isCompactMode ? null : effectiveIsCollapsed ? (
|
|
277
|
+
// Collapsed: show preview only
|
|
278
|
+
content && (
|
|
279
|
+
<Box marginLeft={2} marginTop={0}>
|
|
280
|
+
<Text color={mutedColor} dimColor wrap="truncate">
|
|
281
|
+
{preview}
|
|
282
|
+
</Text>
|
|
283
|
+
</Box>
|
|
284
|
+
)
|
|
285
|
+
) : (
|
|
286
|
+
// Expanded: show full content
|
|
287
|
+
<Box marginLeft={2} marginTop={0} flexDirection="column">
|
|
288
|
+
<Text color={thinkingColor} dimColor wrap="wrap">
|
|
289
|
+
{content}
|
|
290
|
+
</Text>
|
|
291
|
+
</Box>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
{/* Tool calls (when present) */}
|
|
295
|
+
{toolCalls && toolCalls.length > 0 && (
|
|
296
|
+
<Box marginLeft={2} marginTop={0} flexDirection="column">
|
|
297
|
+
{toolCalls.map((toolCall) => {
|
|
298
|
+
// Determine status indicator and color
|
|
299
|
+
let statusIcon: string;
|
|
300
|
+
let statusColor: string;
|
|
301
|
+
|
|
302
|
+
switch (toolCall.status) {
|
|
303
|
+
case "running":
|
|
304
|
+
case "pending":
|
|
305
|
+
statusIcon = TOOL_SPINNER_FRAMES[frameIndex] ?? "-";
|
|
306
|
+
statusColor = accentColor;
|
|
307
|
+
break;
|
|
308
|
+
case "completed":
|
|
309
|
+
statusIcon = "+";
|
|
310
|
+
statusColor = successColor;
|
|
311
|
+
break;
|
|
312
|
+
case "error":
|
|
313
|
+
statusIcon = "x";
|
|
314
|
+
statusColor = errorColor;
|
|
315
|
+
break;
|
|
316
|
+
default:
|
|
317
|
+
statusIcon = "o";
|
|
318
|
+
statusColor = mutedColor;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<Box key={toolCall.id} flexDirection="row">
|
|
323
|
+
<Text color={statusColor}>{statusIcon}</Text>
|
|
324
|
+
<Text> </Text>
|
|
325
|
+
<Text color={accentColor} bold>
|
|
326
|
+
{toolCall.name}
|
|
327
|
+
</Text>
|
|
328
|
+
{toolCall.status === "error" && toolCall.error && (
|
|
329
|
+
<Text color={errorColor} dimColor>
|
|
330
|
+
{" "}
|
|
331
|
+
— {toolCall.error}
|
|
332
|
+
</Text>
|
|
333
|
+
)}
|
|
334
|
+
</Box>
|
|
335
|
+
);
|
|
336
|
+
})}
|
|
337
|
+
</Box>
|
|
338
|
+
)}
|
|
339
|
+
</Box>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// =============================================================================
|
|
344
|
+
// Compact Variant
|
|
345
|
+
// =============================================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Props for the CompactThinkingIndicator component.
|
|
349
|
+
*/
|
|
350
|
+
export interface CompactThinkingIndicatorProps {
|
|
351
|
+
/** Duration of thinking in milliseconds */
|
|
352
|
+
readonly durationMs?: number;
|
|
353
|
+
/** Whether thinking is still in progress */
|
|
354
|
+
readonly isStreaming?: boolean;
|
|
355
|
+
/** Character count to display */
|
|
356
|
+
readonly charCount?: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* CompactThinkingIndicator - Minimal inline thinking status.
|
|
361
|
+
*
|
|
362
|
+
* Use this when you just want to show that thinking occurred
|
|
363
|
+
* without the full collapsible content.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```tsx
|
|
367
|
+
* <CompactThinkingIndicator durationMs={3200} charCount={1500} />
|
|
368
|
+
* // Renders: 💭 Thought for 3.2s (1.5K chars)
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
export function CompactThinkingIndicator({
|
|
372
|
+
durationMs,
|
|
373
|
+
isStreaming = false,
|
|
374
|
+
charCount,
|
|
375
|
+
}: CompactThinkingIndicatorProps): React.JSX.Element {
|
|
376
|
+
const { theme } = useTheme();
|
|
377
|
+
const thinkingColor = theme.colors.warning ?? "yellow";
|
|
378
|
+
|
|
379
|
+
const parts: string[] = ["[...]"];
|
|
380
|
+
|
|
381
|
+
if (isStreaming) {
|
|
382
|
+
parts.push("Thinking...");
|
|
383
|
+
} else if (durationMs !== undefined && durationMs > 0) {
|
|
384
|
+
parts.push(`Thought for ${formatThinkingDuration(durationMs)}`);
|
|
385
|
+
} else {
|
|
386
|
+
parts.push("Thought");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (charCount !== undefined && charCount > 0) {
|
|
390
|
+
parts.push(`(${formatCharCount(charCount)})`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<Box flexDirection="row" alignItems="center">
|
|
395
|
+
{isStreaming && (
|
|
396
|
+
<>
|
|
397
|
+
<Spinner color={thinkingColor} frames={SPINNER_STYLES.dots} />
|
|
398
|
+
<Text> </Text>
|
|
399
|
+
</>
|
|
400
|
+
)}
|
|
401
|
+
<Text color={thinkingColor} dimColor italic>
|
|
402
|
+
{parts.join(" ")}
|
|
403
|
+
</Text>
|
|
404
|
+
</Box>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export default ThinkingBlock;
|