@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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotkeyHelpModal Component (Chain 24)
|
|
3
|
+
*
|
|
4
|
+
* Modal overlay displaying available keyboard shortcuts.
|
|
5
|
+
* Helps users discover and learn hotkey bindings.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/common/HotkeyHelpModal
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Box, Text, useInput } from "ink";
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import { useTheme } from "../../theme/index.js";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A single hotkey binding definition.
|
|
21
|
+
*/
|
|
22
|
+
export interface HotkeyBinding {
|
|
23
|
+
/** Key combination (e.g., "Ctrl+C", "?", "Esc") */
|
|
24
|
+
readonly key: string;
|
|
25
|
+
/** Description of what the hotkey does */
|
|
26
|
+
readonly description: string;
|
|
27
|
+
/** Optional scope/category (e.g., "Global", "Editor", "Navigation") */
|
|
28
|
+
readonly scope?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Props for the HotkeyHelpModal component.
|
|
33
|
+
*/
|
|
34
|
+
export interface HotkeyHelpModalProps {
|
|
35
|
+
/** Whether the modal is visible */
|
|
36
|
+
readonly isVisible: boolean;
|
|
37
|
+
/** Callback when the modal should close */
|
|
38
|
+
readonly onClose: () => void;
|
|
39
|
+
/** List of hotkey bindings to display */
|
|
40
|
+
readonly hotkeys: readonly HotkeyBinding[];
|
|
41
|
+
/** Title for the modal (default: "Keyboard Shortcuts") */
|
|
42
|
+
readonly title?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Constants
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/** Default hotkeys commonly available in the TUI */
|
|
50
|
+
export const DEFAULT_HOTKEYS: HotkeyBinding[] = [
|
|
51
|
+
{ key: "F1", description: "Show this help", scope: "Global" },
|
|
52
|
+
{ key: "Ctrl+Shift+A", description: "Open approval queue", scope: "Global" },
|
|
53
|
+
{ key: "Ctrl+C", description: "Exit application", scope: "Global" },
|
|
54
|
+
{ key: "Ctrl+L", description: "Clear screen", scope: "Global" },
|
|
55
|
+
{ key: "Esc", description: "Cancel / Close modal", scope: "Global" },
|
|
56
|
+
{ key: "↑/↓", description: "Navigate history / options", scope: "Navigation" },
|
|
57
|
+
{ key: "Tab", description: "Autocomplete / Next field", scope: "Input" },
|
|
58
|
+
{ key: "Enter", description: "Submit / Confirm", scope: "Input" },
|
|
59
|
+
{ key: "Ctrl+U", description: "Clear input line", scope: "Input" },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Helper Functions
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Group hotkeys by their scope.
|
|
68
|
+
*/
|
|
69
|
+
function groupHotkeysByScope(hotkeys: readonly HotkeyBinding[]): Map<string, HotkeyBinding[]> {
|
|
70
|
+
const grouped = new Map<string, HotkeyBinding[]>();
|
|
71
|
+
|
|
72
|
+
for (const hotkey of hotkeys) {
|
|
73
|
+
const scope = hotkey.scope ?? "General";
|
|
74
|
+
const existing = grouped.get(scope) ?? [];
|
|
75
|
+
grouped.set(scope, [...existing, hotkey]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return grouped;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculate the maximum key width for alignment.
|
|
83
|
+
*/
|
|
84
|
+
function getMaxKeyWidth(hotkeys: readonly HotkeyBinding[]): number {
|
|
85
|
+
return Math.max(...hotkeys.map((h) => h.key.length), 8);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// HotkeyHelpModal Component
|
|
90
|
+
// =============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* HotkeyHelpModal - Modal overlay for keyboard shortcut help.
|
|
94
|
+
*
|
|
95
|
+
* Features:
|
|
96
|
+
* - Groups hotkeys by scope/category
|
|
97
|
+
* - Aligned key-description columns
|
|
98
|
+
* - Dismissible with Esc or ?
|
|
99
|
+
* - Themeable styling
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* const [showHelp, setShowHelp] = useState(false);
|
|
104
|
+
*
|
|
105
|
+
* <HotkeyHelpModal
|
|
106
|
+
* isVisible={showHelp}
|
|
107
|
+
* onClose={() => setShowHelp(false)}
|
|
108
|
+
* hotkeys={[
|
|
109
|
+
* { key: "Ctrl+S", description: "Save file", scope: "Editor" },
|
|
110
|
+
* { key: "Ctrl+Z", description: "Undo", scope: "Editor" },
|
|
111
|
+
* ]}
|
|
112
|
+
* />
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function HotkeyHelpModal({
|
|
116
|
+
isVisible,
|
|
117
|
+
onClose,
|
|
118
|
+
hotkeys,
|
|
119
|
+
title = "Keyboard Shortcuts",
|
|
120
|
+
}: HotkeyHelpModalProps): React.JSX.Element | null {
|
|
121
|
+
const { theme } = useTheme();
|
|
122
|
+
|
|
123
|
+
// Handle keyboard input for closing
|
|
124
|
+
useInput(
|
|
125
|
+
(input, key) => {
|
|
126
|
+
if (key.escape || input === "q") {
|
|
127
|
+
onClose();
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{ isActive: isVisible }
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Group hotkeys by scope
|
|
134
|
+
const groupedHotkeys = useMemo(() => groupHotkeysByScope(hotkeys), [hotkeys]);
|
|
135
|
+
const maxKeyWidth = useMemo(() => getMaxKeyWidth(hotkeys), [hotkeys]);
|
|
136
|
+
|
|
137
|
+
// Don't render if not visible
|
|
138
|
+
if (!isVisible) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Box
|
|
144
|
+
flexDirection="column"
|
|
145
|
+
borderStyle="round"
|
|
146
|
+
borderColor={theme.colors.primary}
|
|
147
|
+
paddingX={2}
|
|
148
|
+
paddingY={1}
|
|
149
|
+
>
|
|
150
|
+
{/* Title */}
|
|
151
|
+
<Box marginBottom={1}>
|
|
152
|
+
<Text color={theme.colors.primary} bold>
|
|
153
|
+
{title}
|
|
154
|
+
</Text>
|
|
155
|
+
</Box>
|
|
156
|
+
|
|
157
|
+
{/* Hotkey groups */}
|
|
158
|
+
{Array.from(groupedHotkeys.entries()).map(([scope, scopeHotkeys]) => (
|
|
159
|
+
<Box key={scope} flexDirection="column" marginBottom={1}>
|
|
160
|
+
{/* Scope header */}
|
|
161
|
+
<Box marginBottom={0}>
|
|
162
|
+
<Text color={theme.semantic.text.muted} underline>
|
|
163
|
+
{scope}
|
|
164
|
+
</Text>
|
|
165
|
+
</Box>
|
|
166
|
+
|
|
167
|
+
{/* Hotkeys in this scope */}
|
|
168
|
+
{scopeHotkeys.map((hotkey) => (
|
|
169
|
+
<Box key={hotkey.key}>
|
|
170
|
+
<Box width={maxKeyWidth + 2}>
|
|
171
|
+
<Text color={theme.colors.info} bold>
|
|
172
|
+
{hotkey.key.padEnd(maxKeyWidth)}
|
|
173
|
+
</Text>
|
|
174
|
+
</Box>
|
|
175
|
+
<Text color={theme.semantic.text.secondary}>{hotkey.description}</Text>
|
|
176
|
+
</Box>
|
|
177
|
+
))}
|
|
178
|
+
</Box>
|
|
179
|
+
))}
|
|
180
|
+
|
|
181
|
+
{/* Close hint */}
|
|
182
|
+
<Box marginTop={1}>
|
|
183
|
+
<Text dimColor>Press Esc or q to close</Text>
|
|
184
|
+
</Box>
|
|
185
|
+
</Box>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Exports
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
export default HotkeyHelpModal;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotkeyHints Component
|
|
3
|
+
*
|
|
4
|
+
* Small, reusable hint bar for displaying discoverable keybindings.
|
|
5
|
+
* Designed to be safe in narrow Ink layouts: single-line, dim, truncates.
|
|
6
|
+
*
|
|
7
|
+
* Memoized to prevent re-renders when parent context updates (e.g., message streaming).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Box, Text } from "ink";
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import { memo } from "react";
|
|
13
|
+
|
|
14
|
+
export type HotkeyHint = {
|
|
15
|
+
readonly keys: string;
|
|
16
|
+
readonly label: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface HotkeyHintsProps {
|
|
20
|
+
readonly hints: ReadonlyArray<HotkeyHint>;
|
|
21
|
+
/** Separator between items (default: " │ ") */
|
|
22
|
+
readonly separator?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compare two HotkeyHint arrays for equality.
|
|
27
|
+
*/
|
|
28
|
+
function hintsAreEqual(
|
|
29
|
+
prevHints: ReadonlyArray<HotkeyHint>,
|
|
30
|
+
nextHints: ReadonlyArray<HotkeyHint>
|
|
31
|
+
): boolean {
|
|
32
|
+
if (prevHints.length !== nextHints.length) return false;
|
|
33
|
+
for (let i = 0; i < prevHints.length; i++) {
|
|
34
|
+
const prevHint = prevHints[i];
|
|
35
|
+
const nextHint = nextHints[i];
|
|
36
|
+
if (prevHint?.keys !== nextHint?.keys || prevHint?.label !== nextHint?.label) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Custom comparison for React.memo - only re-render when hints or separator actually change.
|
|
45
|
+
*/
|
|
46
|
+
function arePropsEqual(prev: HotkeyHintsProps, next: HotkeyHintsProps): boolean {
|
|
47
|
+
return prev.separator === next.separator && hintsAreEqual(prev.hints, next.hints);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function HotkeyHintsImpl({ hints, separator = " │ " }: HotkeyHintsProps): React.JSX.Element {
|
|
51
|
+
if (hints.length === 0) {
|
|
52
|
+
return <Box />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Box>
|
|
57
|
+
<Text dimColor wrap="truncate">
|
|
58
|
+
{hints.map((hint, index) => (
|
|
59
|
+
<Text key={`${hint.keys}-${hint.label}`}>
|
|
60
|
+
{index > 0 ? separator : ""}
|
|
61
|
+
<Text bold>{hint.keys}</Text>
|
|
62
|
+
<Text> {hint.label}</Text>
|
|
63
|
+
</Text>
|
|
64
|
+
))}
|
|
65
|
+
</Text>
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const HotkeyHints = memo(HotkeyHintsImpl, arePropsEqual);
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MaxSizedBox Component
|
|
3
|
+
*
|
|
4
|
+
* A container component that limits content to a maximum size
|
|
5
|
+
* and shows a "Show More" indicator when content is truncated.
|
|
6
|
+
*
|
|
7
|
+
* This component is essential for preventing overflow in terminal UIs
|
|
8
|
+
* while maintaining a good user experience by indicating when content
|
|
9
|
+
* has been truncated.
|
|
10
|
+
*
|
|
11
|
+
* Ported from Gemini CLI for Vellum TUI.
|
|
12
|
+
*
|
|
13
|
+
* @module tui/components/common/MaxSizedBox
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Box, type DOMElement, Text } from "ink";
|
|
17
|
+
import type React from "react";
|
|
18
|
+
import { type ReactNode, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
19
|
+
import { useOverflowOptional } from "../../context/OverflowContext.js";
|
|
20
|
+
import { useFlickerDetector } from "../../hooks/useFlickerDetector.js";
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Props for the MaxSizedBox component
|
|
28
|
+
*/
|
|
29
|
+
export interface MaxSizedBoxProps {
|
|
30
|
+
/** Content to render (will be truncated if exceeds maxHeight) */
|
|
31
|
+
readonly children: ReactNode;
|
|
32
|
+
/** Maximum height in rows/lines */
|
|
33
|
+
readonly maxHeight: number;
|
|
34
|
+
/** Minimum height in rows/lines (default: 1) */
|
|
35
|
+
readonly minHeight?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Text to show when content is truncated.
|
|
38
|
+
* Set to null to hide the indicator.
|
|
39
|
+
* @default "... (more)"
|
|
40
|
+
*/
|
|
41
|
+
readonly truncationIndicator?: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Color for the truncation indicator
|
|
44
|
+
* @default "dim"
|
|
45
|
+
*/
|
|
46
|
+
readonly indicatorColor?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Callback when expanded state changes
|
|
49
|
+
* @param expanded - Whether the content is now expanded
|
|
50
|
+
*/
|
|
51
|
+
readonly onExpandChange?: (expanded: boolean) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Callback when overflow state changes
|
|
54
|
+
* @param isOverflowing - Whether content is currently overflowing
|
|
55
|
+
*/
|
|
56
|
+
readonly onOverflowChange?: (isOverflowing: boolean) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Initial expanded state (default: false)
|
|
59
|
+
*/
|
|
60
|
+
readonly initialExpanded?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Allow expanding the content (default: false)
|
|
63
|
+
* When true, the truncation indicator becomes "Show More" / "Show Less"
|
|
64
|
+
*/
|
|
65
|
+
readonly expandable?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Unique ID for overflow tracking (auto-generated if not provided)
|
|
68
|
+
*/
|
|
69
|
+
readonly id?: string;
|
|
70
|
+
/** Width of the box (default: 100%) */
|
|
71
|
+
readonly width?: number | string;
|
|
72
|
+
/** Padding within the box */
|
|
73
|
+
readonly padding?: number;
|
|
74
|
+
/** Horizontal padding */
|
|
75
|
+
readonly paddingX?: number;
|
|
76
|
+
/** Vertical padding */
|
|
77
|
+
readonly paddingY?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* State returned by the MaxSizedBox for external control
|
|
82
|
+
*/
|
|
83
|
+
export interface MaxSizedBoxState {
|
|
84
|
+
/** Whether content is currently overflowing */
|
|
85
|
+
readonly isOverflowing: boolean;
|
|
86
|
+
/** Whether content is currently expanded */
|
|
87
|
+
readonly isExpanded: boolean;
|
|
88
|
+
/** Measured content height */
|
|
89
|
+
readonly contentHeight: number;
|
|
90
|
+
/** Amount of overflow (0 if content fits) */
|
|
91
|
+
readonly overflowAmount: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Constants
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
/** Default truncation indicator text */
|
|
99
|
+
const DEFAULT_TRUNCATION_INDICATOR = "... (more)";
|
|
100
|
+
|
|
101
|
+
/** Expandable show more text */
|
|
102
|
+
const SHOW_MORE_TEXT = "▼ Show More";
|
|
103
|
+
|
|
104
|
+
/** Expandable show less text */
|
|
105
|
+
const SHOW_LESS_TEXT = "▲ Show Less";
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Component
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* MaxSizedBox limits content to a maximum height with optional truncation indicator.
|
|
113
|
+
*
|
|
114
|
+
* Features:
|
|
115
|
+
* - **Height limiting**: Prevents content from exceeding specified bounds
|
|
116
|
+
* - **Truncation indicator**: Shows when content is cut off
|
|
117
|
+
* - **Expandable mode**: Optional expand/collapse functionality
|
|
118
|
+
* - **Overflow tracking**: Integrates with OverflowContext
|
|
119
|
+
* - **Flicker prevention**: Uses debounced overflow detection
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```tsx
|
|
123
|
+
* // Basic usage - truncate at 10 rows
|
|
124
|
+
* <MaxSizedBox maxHeight={10}>
|
|
125
|
+
* <Text>{longContent}</Text>
|
|
126
|
+
* </MaxSizedBox>
|
|
127
|
+
*
|
|
128
|
+
* // With expand functionality
|
|
129
|
+
* <MaxSizedBox
|
|
130
|
+
* maxHeight={5}
|
|
131
|
+
* expandable
|
|
132
|
+
* onExpandChange={(expanded) => console.log('Expanded:', expanded)}
|
|
133
|
+
* >
|
|
134
|
+
* <CodeBlock code={sourceCode} />
|
|
135
|
+
* </MaxSizedBox>
|
|
136
|
+
*
|
|
137
|
+
* // Custom truncation indicator
|
|
138
|
+
* <MaxSizedBox
|
|
139
|
+
* maxHeight={20}
|
|
140
|
+
* truncationIndicator="[Content truncated...]"
|
|
141
|
+
* indicatorColor="yellow"
|
|
142
|
+
* >
|
|
143
|
+
* <LogOutput logs={logs} />
|
|
144
|
+
* </MaxSizedBox>
|
|
145
|
+
*
|
|
146
|
+
* // Hide truncation indicator
|
|
147
|
+
* <MaxSizedBox maxHeight={10} truncationIndicator={null}>
|
|
148
|
+
* <Content />
|
|
149
|
+
* </MaxSizedBox>
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function MaxSizedBox({
|
|
153
|
+
children,
|
|
154
|
+
maxHeight,
|
|
155
|
+
minHeight = 1,
|
|
156
|
+
truncationIndicator = DEFAULT_TRUNCATION_INDICATOR,
|
|
157
|
+
indicatorColor = "dim",
|
|
158
|
+
onExpandChange,
|
|
159
|
+
onOverflowChange,
|
|
160
|
+
initialExpanded = false,
|
|
161
|
+
expandable = false,
|
|
162
|
+
id: providedId,
|
|
163
|
+
width = "100%",
|
|
164
|
+
padding,
|
|
165
|
+
paddingX,
|
|
166
|
+
paddingY,
|
|
167
|
+
}: MaxSizedBoxProps): React.JSX.Element {
|
|
168
|
+
// Generate unique ID if not provided
|
|
169
|
+
const generatedId = useId();
|
|
170
|
+
const componentId = providedId ?? generatedId;
|
|
171
|
+
|
|
172
|
+
// Expansion state
|
|
173
|
+
const [isExpanded, setIsExpanded] = useState(initialExpanded);
|
|
174
|
+
|
|
175
|
+
// Content measurement
|
|
176
|
+
const contentRef = useRef<DOMElement | null>(null);
|
|
177
|
+
const [measuredHeight, setMeasuredHeight] = useState(0);
|
|
178
|
+
|
|
179
|
+
// Optional overflow context integration
|
|
180
|
+
const overflowContext = useOverflowOptional();
|
|
181
|
+
|
|
182
|
+
// Flicker detection for stable overflow state
|
|
183
|
+
const { isOverflowing, overflow } = useFlickerDetector({
|
|
184
|
+
contentHeight: measuredHeight,
|
|
185
|
+
containerHeight: maxHeight,
|
|
186
|
+
threshold: 0,
|
|
187
|
+
debounce: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Measure content height on mount and updates
|
|
192
|
+
*/
|
|
193
|
+
const measureContent = useCallback(() => {
|
|
194
|
+
if (contentRef.current) {
|
|
195
|
+
// In Ink, we estimate height based on the rendered output
|
|
196
|
+
// This is a simplified measurement - actual implementation may
|
|
197
|
+
// need to parse ANSI output or use Ink's measurement APIs
|
|
198
|
+
const element = contentRef.current;
|
|
199
|
+
const yogaNode = element.yogaNode;
|
|
200
|
+
if (yogaNode) {
|
|
201
|
+
const computedHeight = yogaNode.getComputedHeight();
|
|
202
|
+
setMeasuredHeight(Math.ceil(computedHeight));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Effect to measure content on mount.
|
|
209
|
+
* Measurement happens on every render since content could change.
|
|
210
|
+
* We call measureContent directly in the effect body which captures
|
|
211
|
+
* the current contentRef value.
|
|
212
|
+
*/
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
measureContent();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Effect to notify parent of overflow changes
|
|
219
|
+
*/
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
onOverflowChange?.(isOverflowing);
|
|
222
|
+
}, [isOverflowing, onOverflowChange]);
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Effect to register/unregister with overflow context
|
|
226
|
+
*/
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!overflowContext) return;
|
|
229
|
+
|
|
230
|
+
if (isOverflowing && !isExpanded) {
|
|
231
|
+
overflowContext.registerOverflow(componentId);
|
|
232
|
+
} else {
|
|
233
|
+
overflowContext.unregisterOverflow(componentId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return () => {
|
|
237
|
+
overflowContext.unregisterOverflow(componentId);
|
|
238
|
+
};
|
|
239
|
+
}, [isOverflowing, isExpanded, componentId, overflowContext]);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Handle expand/collapse toggle.
|
|
243
|
+
* Reserved for future interactive functionality (keyboard/click handlers).
|
|
244
|
+
*/
|
|
245
|
+
const _handleToggleExpand = useCallback(() => {
|
|
246
|
+
const newExpanded = !isExpanded;
|
|
247
|
+
setIsExpanded(newExpanded);
|
|
248
|
+
onExpandChange?.(newExpanded);
|
|
249
|
+
}, [isExpanded, onExpandChange]);
|
|
250
|
+
|
|
251
|
+
// Preserve reference for future use
|
|
252
|
+
void _handleToggleExpand;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Calculate effective height
|
|
256
|
+
*/
|
|
257
|
+
const effectiveHeight = useMemo(() => {
|
|
258
|
+
if (isExpanded) {
|
|
259
|
+
// When expanded, use measured height (no limit)
|
|
260
|
+
return Math.max(minHeight, measuredHeight);
|
|
261
|
+
}
|
|
262
|
+
// When collapsed, limit to maxHeight
|
|
263
|
+
return Math.max(minHeight, Math.min(maxHeight, measuredHeight));
|
|
264
|
+
}, [isExpanded, maxHeight, minHeight, measuredHeight]);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Determine if truncation indicator should show
|
|
268
|
+
*/
|
|
269
|
+
const showTruncationIndicator = useMemo(() => {
|
|
270
|
+
// Don't show if no indicator text
|
|
271
|
+
if (truncationIndicator === null) return false;
|
|
272
|
+
// Show if overflowing and not expanded
|
|
273
|
+
return isOverflowing && !isExpanded;
|
|
274
|
+
}, [truncationIndicator, isOverflowing, isExpanded]);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get indicator text based on mode
|
|
278
|
+
*/
|
|
279
|
+
const indicatorText = useMemo(() => {
|
|
280
|
+
if (expandable) {
|
|
281
|
+
return isExpanded ? SHOW_LESS_TEXT : SHOW_MORE_TEXT;
|
|
282
|
+
}
|
|
283
|
+
return truncationIndicator;
|
|
284
|
+
}, [expandable, isExpanded, truncationIndicator]);
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Render the content with optional truncation
|
|
288
|
+
*/
|
|
289
|
+
const resolvedHeight = measuredHeight > 0 ? effectiveHeight : undefined;
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<Box
|
|
293
|
+
flexDirection="column"
|
|
294
|
+
width={width}
|
|
295
|
+
height={resolvedHeight}
|
|
296
|
+
overflow="hidden"
|
|
297
|
+
padding={padding}
|
|
298
|
+
paddingX={paddingX}
|
|
299
|
+
paddingY={paddingY}
|
|
300
|
+
>
|
|
301
|
+
{/* Content container */}
|
|
302
|
+
<Box ref={contentRef} flexDirection="column" flexGrow={1} overflow="hidden">
|
|
303
|
+
{children}
|
|
304
|
+
</Box>
|
|
305
|
+
|
|
306
|
+
{/* Truncation indicator */}
|
|
307
|
+
{showTruncationIndicator && indicatorText && (
|
|
308
|
+
<Box>
|
|
309
|
+
<Text color={indicatorColor} dimColor={!expandable}>
|
|
310
|
+
{indicatorText}
|
|
311
|
+
{overflow > 0 && !expandable && ` (+${overflow} lines)`}
|
|
312
|
+
</Text>
|
|
313
|
+
</Box>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Show "Show Less" when expanded */}
|
|
317
|
+
{expandable && isExpanded && (
|
|
318
|
+
<Box>
|
|
319
|
+
<Text color={indicatorColor}>{SHOW_LESS_TEXT}</Text>
|
|
320
|
+
</Box>
|
|
321
|
+
)}
|
|
322
|
+
</Box>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// =============================================================================
|
|
327
|
+
// Convenience Hook
|
|
328
|
+
// =============================================================================
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Hook to get MaxSizedBox-like overflow calculations without the component.
|
|
332
|
+
* Useful for custom implementations that need the same logic.
|
|
333
|
+
*
|
|
334
|
+
* @param contentHeight - Current content height
|
|
335
|
+
* @param maxHeight - Maximum allowed height
|
|
336
|
+
* @returns Overflow state and metrics
|
|
337
|
+
*/
|
|
338
|
+
export function useMaxSizedBox(contentHeight: number, maxHeight: number): MaxSizedBoxState {
|
|
339
|
+
const { isOverflowing, overflow } = useFlickerDetector({
|
|
340
|
+
contentHeight,
|
|
341
|
+
containerHeight: maxHeight,
|
|
342
|
+
threshold: 0,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return useMemo(
|
|
346
|
+
() => ({
|
|
347
|
+
isOverflowing,
|
|
348
|
+
isExpanded: false,
|
|
349
|
+
contentHeight,
|
|
350
|
+
overflowAmount: Math.max(0, overflow),
|
|
351
|
+
}),
|
|
352
|
+
[isOverflowing, contentHeight, overflow]
|
|
353
|
+
);
|
|
354
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NewMessagesBadge Component
|
|
3
|
+
*
|
|
4
|
+
* Badge showing unread message count with scroll-to-bottom action.
|
|
5
|
+
* Displays "↓ N new messages" when user is in manual scroll mode.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/common/NewMessagesBadge
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Box, Text } from "ink";
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import { useTheme } from "../../theme/index.js";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props for NewMessagesBadge component.
|
|
20
|
+
*/
|
|
21
|
+
export interface NewMessagesBadgeProps {
|
|
22
|
+
/** Number of new messages */
|
|
23
|
+
readonly count: number;
|
|
24
|
+
/** Callback when clicked/activated */
|
|
25
|
+
readonly onScrollToBottom?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Component
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Badge showing unread message count with scroll-to-bottom hint.
|
|
34
|
+
*
|
|
35
|
+
* Displays "↓ N new messages (press End)" when count > 0.
|
|
36
|
+
* Hidden when count = 0 (no visual clutter).
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <NewMessagesBadge
|
|
41
|
+
* count={unreadCount}
|
|
42
|
+
* onScrollToBottom={() => scrollController.resumeFollow()}
|
|
43
|
+
* />
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function NewMessagesBadge({
|
|
47
|
+
count,
|
|
48
|
+
onScrollToBottom: _onScrollToBottom,
|
|
49
|
+
}: NewMessagesBadgeProps): React.ReactElement | null {
|
|
50
|
+
const { theme } = useTheme();
|
|
51
|
+
|
|
52
|
+
// Don't render if no new messages
|
|
53
|
+
if (count <= 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Format message text
|
|
58
|
+
const messageText = count === 1 ? "1 new message" : `${count} new messages`;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Box justifyContent="center" paddingX={1}>
|
|
62
|
+
<Text color={theme.colors.info}>↓ {messageText} (press End)</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
);
|
|
65
|
+
}
|