@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,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VirtualizedList Component
|
|
3
|
+
*
|
|
4
|
+
* A high-performance virtualized list component for terminal UIs.
|
|
5
|
+
* Only renders items that are currently visible in the viewport,
|
|
6
|
+
* with support for variable height items and auto-scroll to bottom.
|
|
7
|
+
*
|
|
8
|
+
* Ported from Gemini CLI with Vellum adaptations.
|
|
9
|
+
*
|
|
10
|
+
* @module tui/components/common/VirtualizedList
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Box, type DOMElement } from "ink";
|
|
14
|
+
import type React from "react";
|
|
15
|
+
import {
|
|
16
|
+
forwardRef,
|
|
17
|
+
useCallback,
|
|
18
|
+
useEffect,
|
|
19
|
+
useImperativeHandle,
|
|
20
|
+
useMemo,
|
|
21
|
+
useRef,
|
|
22
|
+
useState,
|
|
23
|
+
} from "react";
|
|
24
|
+
import { useScrollOptional } from "../../../context/ScrollContext.js";
|
|
25
|
+
import { useBatchedScroll, useScrollAnchor, useVirtualization } from "./hooks/index.js";
|
|
26
|
+
import { SCROLL_TO_ITEM_END, type VirtualizedListProps, type VirtualizedListRef } from "./types.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* VirtualizedList - Only renders visible items for optimal performance.
|
|
30
|
+
*
|
|
31
|
+
* Features:
|
|
32
|
+
* - Virtual rendering: Only items in viewport are mounted
|
|
33
|
+
* - Height estimation: Supports fixed or variable item heights
|
|
34
|
+
* - Auto-scroll: Sticks to bottom when new items added
|
|
35
|
+
* - Anchor-based scrolling: Stable during content changes
|
|
36
|
+
* - Imperative API: Control scrolling programmatically
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const listRef = useRef<VirtualizedListRef<Message>>(null);
|
|
41
|
+
*
|
|
42
|
+
* <VirtualizedList
|
|
43
|
+
* ref={listRef}
|
|
44
|
+
* data={messages}
|
|
45
|
+
* renderItem={({ item }) => <MessageItem message={item} />}
|
|
46
|
+
* keyExtractor={(item) => item.id}
|
|
47
|
+
* estimatedItemHeight={3}
|
|
48
|
+
* initialScrollIndex={SCROLL_TO_ITEM_END}
|
|
49
|
+
* initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
|
|
50
|
+
* />
|
|
51
|
+
*
|
|
52
|
+
* // Scroll programmatically
|
|
53
|
+
* listRef.current?.scrollToEnd();
|
|
54
|
+
* listRef.current?.scrollBy(-10);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function VirtualizedListInner<T>(
|
|
58
|
+
props: VirtualizedListProps<T>,
|
|
59
|
+
ref: React.Ref<VirtualizedListRef<T>>
|
|
60
|
+
) {
|
|
61
|
+
const {
|
|
62
|
+
data,
|
|
63
|
+
renderItem,
|
|
64
|
+
estimatedItemHeight,
|
|
65
|
+
keyExtractor,
|
|
66
|
+
initialScrollIndex,
|
|
67
|
+
initialScrollOffsetInIndex,
|
|
68
|
+
scrollbarThumbColor,
|
|
69
|
+
onScrollTopChange,
|
|
70
|
+
onStickingToBottomChange,
|
|
71
|
+
alignToBottom = false,
|
|
72
|
+
} = props;
|
|
73
|
+
|
|
74
|
+
// Note: theme reserved for future scrollbar styling
|
|
75
|
+
// const { theme } = useTheme();
|
|
76
|
+
|
|
77
|
+
// FIX: Initialize container height dynamically from terminal dimensions
|
|
78
|
+
// instead of hardcoded 24 lines. This prevents incorrect scroll calculations
|
|
79
|
+
// on first render when terminal size differs from the default.
|
|
80
|
+
const [containerHeight, setContainerHeight] = useState(() => {
|
|
81
|
+
// Use actual terminal rows if available, with fallback to reasonable default
|
|
82
|
+
const terminalRows = process.stdout.rows;
|
|
83
|
+
// Reserve some space for UI elements (header, input, status)
|
|
84
|
+
const reservedLines = 10;
|
|
85
|
+
return Math.max(8, (terminalRows || 24) - reservedLines);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Initial virtualization pass with estimated heights
|
|
89
|
+
const initialVirtualization = useVirtualization({
|
|
90
|
+
dataLength: data.length,
|
|
91
|
+
estimatedItemHeight,
|
|
92
|
+
scrollTop: 0,
|
|
93
|
+
containerHeight,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Scroll anchor management
|
|
97
|
+
const {
|
|
98
|
+
scrollAnchor,
|
|
99
|
+
setScrollAnchor,
|
|
100
|
+
isStickingToBottom,
|
|
101
|
+
setIsStickingToBottom,
|
|
102
|
+
scrollTop,
|
|
103
|
+
getAnchorForScrollTop,
|
|
104
|
+
} = useScrollAnchor({
|
|
105
|
+
dataLength: data.length,
|
|
106
|
+
offsets: initialVirtualization.offsets,
|
|
107
|
+
heights: initialVirtualization.heights,
|
|
108
|
+
totalHeight: initialVirtualization.totalHeight,
|
|
109
|
+
containerHeight,
|
|
110
|
+
initialScrollIndex,
|
|
111
|
+
initialScrollOffsetInIndex,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Full virtualization with actual scroll position
|
|
115
|
+
const {
|
|
116
|
+
heights: _heights,
|
|
117
|
+
offsets,
|
|
118
|
+
totalHeight,
|
|
119
|
+
startIndex,
|
|
120
|
+
endIndex,
|
|
121
|
+
// Note: spacer heights not used in Ink (no real scroll support)
|
|
122
|
+
// topSpacerHeight and bottomSpacerHeight are kept in hook for API compat
|
|
123
|
+
itemRefCallback,
|
|
124
|
+
containerRef,
|
|
125
|
+
measuredContainerHeight,
|
|
126
|
+
} = useVirtualization({
|
|
127
|
+
dataLength: data.length,
|
|
128
|
+
estimatedItemHeight,
|
|
129
|
+
scrollTop,
|
|
130
|
+
containerHeight,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// FIX: Debounce container height updates to prevent rapid state changes
|
|
134
|
+
// that can cause rendering race conditions and jittery UI
|
|
135
|
+
const containerHeightUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
136
|
+
const lastValidHeightRef = useRef(containerHeight);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (measuredContainerHeight > 0) {
|
|
140
|
+
const rows = process.stdout.rows || 24;
|
|
141
|
+
// FIX: Use a more conservative minimum (8 lines) to prevent degenerate cases
|
|
142
|
+
const MIN_CONTAINER_HEIGHT = 8;
|
|
143
|
+
const safeHeight = Math.max(MIN_CONTAINER_HEIGHT, Math.min(measuredContainerHeight, rows));
|
|
144
|
+
|
|
145
|
+
// Only update if the change is significant (more than 1 line difference)
|
|
146
|
+
// This prevents micro-updates from causing layout thrashing
|
|
147
|
+
if (Math.abs(safeHeight - lastValidHeightRef.current) > 1) {
|
|
148
|
+
// Clear any pending update
|
|
149
|
+
if (containerHeightUpdateTimeoutRef.current) {
|
|
150
|
+
clearTimeout(containerHeightUpdateTimeoutRef.current);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Debounce the update to batch rapid changes
|
|
154
|
+
containerHeightUpdateTimeoutRef.current = setTimeout(() => {
|
|
155
|
+
lastValidHeightRef.current = safeHeight;
|
|
156
|
+
setContainerHeight(safeHeight);
|
|
157
|
+
}, 16); // One frame at 60fps
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Cleanup on unmount
|
|
162
|
+
return () => {
|
|
163
|
+
if (containerHeightUpdateTimeoutRef.current) {
|
|
164
|
+
clearTimeout(containerHeightUpdateTimeoutRef.current);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}, [measuredContainerHeight]);
|
|
168
|
+
|
|
169
|
+
// Batched scroll for smooth updates
|
|
170
|
+
const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
|
|
171
|
+
|
|
172
|
+
// Notify parent of scroll changes and dimension updates
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (onScrollTopChange) {
|
|
175
|
+
onScrollTopChange(scrollTop);
|
|
176
|
+
}
|
|
177
|
+
}, [scrollTop, onScrollTopChange]);
|
|
178
|
+
|
|
179
|
+
// Notify parent of sticking state changes
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (onStickingToBottomChange) {
|
|
182
|
+
onStickingToBottomChange(isStickingToBottom);
|
|
183
|
+
}
|
|
184
|
+
}, [isStickingToBottom, onStickingToBottomChange]);
|
|
185
|
+
|
|
186
|
+
// Imperative handle for external control
|
|
187
|
+
useImperativeHandle(
|
|
188
|
+
ref,
|
|
189
|
+
() => ({
|
|
190
|
+
scrollBy: (delta: number) => {
|
|
191
|
+
if (delta < 0) {
|
|
192
|
+
setIsStickingToBottom(false);
|
|
193
|
+
}
|
|
194
|
+
const currentScrollTop = getScrollTop();
|
|
195
|
+
const newScrollTop = Math.max(
|
|
196
|
+
0,
|
|
197
|
+
Math.min(totalHeight - measuredContainerHeight, currentScrollTop + delta)
|
|
198
|
+
);
|
|
199
|
+
setPendingScrollTop(newScrollTop);
|
|
200
|
+
setScrollAnchor(getAnchorForScrollTop(newScrollTop));
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
scrollTo: (offset: number) => {
|
|
204
|
+
setIsStickingToBottom(false);
|
|
205
|
+
const newScrollTop = Math.max(0, Math.min(totalHeight - measuredContainerHeight, offset));
|
|
206
|
+
setPendingScrollTop(newScrollTop);
|
|
207
|
+
setScrollAnchor(getAnchorForScrollTop(newScrollTop));
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
scrollToEnd: () => {
|
|
211
|
+
setIsStickingToBottom(true);
|
|
212
|
+
if (data.length > 0) {
|
|
213
|
+
setScrollAnchor({
|
|
214
|
+
index: data.length - 1,
|
|
215
|
+
offset: SCROLL_TO_ITEM_END,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
scrollToIndex: ({ index, viewOffset = 0, viewPosition = 0 }) => {
|
|
221
|
+
setIsStickingToBottom(false);
|
|
222
|
+
const offset = offsets[index];
|
|
223
|
+
if (offset !== undefined) {
|
|
224
|
+
const newScrollTop = Math.max(
|
|
225
|
+
0,
|
|
226
|
+
Math.min(
|
|
227
|
+
totalHeight - measuredContainerHeight,
|
|
228
|
+
offset - viewPosition * measuredContainerHeight + viewOffset
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
setPendingScrollTop(newScrollTop);
|
|
232
|
+
setScrollAnchor(getAnchorForScrollTop(newScrollTop));
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
scrollToItem: ({ item, viewOffset = 0, viewPosition = 0 }) => {
|
|
237
|
+
setIsStickingToBottom(false);
|
|
238
|
+
const index = data.indexOf(item);
|
|
239
|
+
if (index !== -1) {
|
|
240
|
+
const offset = offsets[index];
|
|
241
|
+
if (offset !== undefined) {
|
|
242
|
+
const newScrollTop = Math.max(
|
|
243
|
+
0,
|
|
244
|
+
Math.min(
|
|
245
|
+
totalHeight - measuredContainerHeight,
|
|
246
|
+
offset - viewPosition * measuredContainerHeight + viewOffset
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
setPendingScrollTop(newScrollTop);
|
|
250
|
+
setScrollAnchor(getAnchorForScrollTop(newScrollTop));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
getScrollIndex: () => scrollAnchor.index,
|
|
256
|
+
|
|
257
|
+
getScrollState: () => ({
|
|
258
|
+
scrollTop: getScrollTop(),
|
|
259
|
+
scrollHeight: totalHeight,
|
|
260
|
+
innerHeight: measuredContainerHeight,
|
|
261
|
+
}),
|
|
262
|
+
|
|
263
|
+
isAtBottom: () => isStickingToBottom,
|
|
264
|
+
}),
|
|
265
|
+
[
|
|
266
|
+
offsets,
|
|
267
|
+
scrollAnchor,
|
|
268
|
+
totalHeight,
|
|
269
|
+
getAnchorForScrollTop,
|
|
270
|
+
data,
|
|
271
|
+
measuredContainerHeight,
|
|
272
|
+
getScrollTop,
|
|
273
|
+
setPendingScrollTop,
|
|
274
|
+
setScrollAnchor,
|
|
275
|
+
setIsStickingToBottom,
|
|
276
|
+
isStickingToBottom,
|
|
277
|
+
]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// ==========================================================================
|
|
281
|
+
// ScrollContext Integration
|
|
282
|
+
// ==========================================================================
|
|
283
|
+
const scrollContext = useScrollOptional();
|
|
284
|
+
const lastReportedScrollTop = useRef<number>(scrollTop);
|
|
285
|
+
|
|
286
|
+
// Report dimensions to ScrollContext when they change
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (scrollContext) {
|
|
289
|
+
scrollContext.updateDimensions(totalHeight, measuredContainerHeight);
|
|
290
|
+
}
|
|
291
|
+
}, [scrollContext, totalHeight, measuredContainerHeight]);
|
|
292
|
+
|
|
293
|
+
// Sync internal scrollTop changes to ScrollContext (debounced to avoid loops)
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (scrollContext && scrollTop !== lastReportedScrollTop.current) {
|
|
296
|
+
lastReportedScrollTop.current = scrollTop;
|
|
297
|
+
// Only sync if the context's scrollTop differs significantly
|
|
298
|
+
const contextScrollTop = scrollContext.state.scrollTop;
|
|
299
|
+
if (Math.abs(scrollTop - contextScrollTop) > 1) {
|
|
300
|
+
scrollContext.scrollTo(scrollTop);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}, [scrollContext, scrollTop]);
|
|
304
|
+
|
|
305
|
+
// Listen for external scroll commands from ScrollContext
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!scrollContext) return;
|
|
308
|
+
|
|
309
|
+
const unsubscribe = scrollContext.onScrollChange((externalScrollTop) => {
|
|
310
|
+
// Avoid reacting to our own updates
|
|
311
|
+
if (Math.abs(externalScrollTop - scrollTop) <= 1) return;
|
|
312
|
+
|
|
313
|
+
// Apply external scroll command
|
|
314
|
+
const newScrollTop = Math.max(
|
|
315
|
+
0,
|
|
316
|
+
Math.min(totalHeight - measuredContainerHeight, externalScrollTop)
|
|
317
|
+
);
|
|
318
|
+
setPendingScrollTop(newScrollTop);
|
|
319
|
+
setScrollAnchor(getAnchorForScrollTop(newScrollTop));
|
|
320
|
+
|
|
321
|
+
// Update sticking state based on position
|
|
322
|
+
const atBottom = newScrollTop >= totalHeight - measuredContainerHeight - 1;
|
|
323
|
+
setIsStickingToBottom(atBottom);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return unsubscribe;
|
|
327
|
+
}, [
|
|
328
|
+
scrollContext,
|
|
329
|
+
scrollTop,
|
|
330
|
+
totalHeight,
|
|
331
|
+
measuredContainerHeight,
|
|
332
|
+
setPendingScrollTop,
|
|
333
|
+
setScrollAnchor,
|
|
334
|
+
getAnchorForScrollTop,
|
|
335
|
+
setIsStickingToBottom,
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
// Respond to scrollToBottom via context state changes
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
if (scrollContext?.state.isAtBottom && !isStickingToBottom && data.length > 0) {
|
|
341
|
+
// Context indicates we should be at bottom but we're not sticking
|
|
342
|
+
// This happens when scrollToBottom() is called on the context
|
|
343
|
+
setIsStickingToBottom(true);
|
|
344
|
+
setScrollAnchor({
|
|
345
|
+
index: data.length - 1,
|
|
346
|
+
offset: SCROLL_TO_ITEM_END,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}, [
|
|
350
|
+
scrollContext?.state.isAtBottom,
|
|
351
|
+
isStickingToBottom,
|
|
352
|
+
data.length,
|
|
353
|
+
setIsStickingToBottom,
|
|
354
|
+
setScrollAnchor,
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
// Create ref callback wrapper
|
|
358
|
+
const createItemRef = useCallback(
|
|
359
|
+
(index: number) => (el: DOMElement | null) => {
|
|
360
|
+
itemRefCallback(index, el);
|
|
361
|
+
},
|
|
362
|
+
[itemRefCallback]
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Render visible items
|
|
366
|
+
const renderedItems = useMemo(() => {
|
|
367
|
+
const items: React.ReactElement[] = [];
|
|
368
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
369
|
+
const item = data[i];
|
|
370
|
+
if (item) {
|
|
371
|
+
items.push(
|
|
372
|
+
<Box key={keyExtractor(item, i)} width="100%" ref={createItemRef(i)}>
|
|
373
|
+
{renderItem({ item, index: i })}
|
|
374
|
+
</Box>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return items;
|
|
379
|
+
}, [startIndex, endIndex, data, keyExtractor, renderItem, createItemRef]);
|
|
380
|
+
|
|
381
|
+
// Note: scrollbarThumbColor and scrollTop are reserved for future native scroll support
|
|
382
|
+
// Standard Ink doesn't support overflowY="scroll", only "hidden" or "visible"
|
|
383
|
+
// Gemini CLI uses a forked Ink (@jrichman/ink) with scroll support
|
|
384
|
+
void scrollbarThumbColor;
|
|
385
|
+
|
|
386
|
+
// CRITICAL: Ink doesn't support real CSS scrolling.
|
|
387
|
+
// overflow="hidden" just clips content, it doesn't scroll.
|
|
388
|
+
// We must NOT use spacers - instead, render visible items directly at the top.
|
|
389
|
+
// The "scrolling" effect is achieved by changing which items are rendered (startIndex/endIndex).
|
|
390
|
+
//
|
|
391
|
+
// For alignToBottom: use justifyContent="flex-end" to push content to bottom
|
|
392
|
+
// when total content height is less than container height.
|
|
393
|
+
const shouldAlignToBottom = alignToBottom && totalHeight < measuredContainerHeight;
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<Box
|
|
397
|
+
ref={containerRef as React.RefObject<DOMElement>}
|
|
398
|
+
overflowY="hidden"
|
|
399
|
+
overflowX="hidden"
|
|
400
|
+
width="100%"
|
|
401
|
+
height="100%"
|
|
402
|
+
flexDirection="column"
|
|
403
|
+
flexGrow={1}
|
|
404
|
+
minHeight={0}
|
|
405
|
+
paddingRight={1}
|
|
406
|
+
justifyContent={shouldAlignToBottom ? "flex-end" : "flex-start"}
|
|
407
|
+
>
|
|
408
|
+
<Box flexShrink={0} width="100%" flexDirection="column">
|
|
409
|
+
{/* Render visible items directly - no spacers needed in Ink */}
|
|
410
|
+
{/* In Ink, "scrolling" = changing which items we render */}
|
|
411
|
+
{renderedItems}
|
|
412
|
+
</Box>
|
|
413
|
+
</Box>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* VirtualizedList with forwardRef support for generic types.
|
|
419
|
+
*/
|
|
420
|
+
const VirtualizedList = forwardRef(VirtualizedListInner) as <T>(
|
|
421
|
+
props: VirtualizedListProps<T> & { ref?: React.Ref<VirtualizedListRef<T>> }
|
|
422
|
+
) => React.ReactElement;
|
|
423
|
+
|
|
424
|
+
// Add display name for debugging
|
|
425
|
+
(VirtualizedList as React.FC).displayName = "VirtualizedList";
|
|
426
|
+
|
|
427
|
+
export { VirtualizedList };
|
|
428
|
+
export type { VirtualizedListProps, VirtualizedListRef };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks index for VirtualizedList
|
|
3
|
+
*
|
|
4
|
+
* @module tui/components/common/VirtualizedList/hooks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { type UseBatchedScrollReturn, useBatchedScroll } from "./useBatchedScroll.js";
|
|
8
|
+
export {
|
|
9
|
+
type UseScrollAnchorProps,
|
|
10
|
+
type UseScrollAnchorReturn,
|
|
11
|
+
useScrollAnchor,
|
|
12
|
+
} from "./useScrollAnchor.js";
|
|
13
|
+
export {
|
|
14
|
+
MIN_VIEWPORT_HEIGHT,
|
|
15
|
+
MIN_VIEWPORT_WIDTH,
|
|
16
|
+
type UseVirtualizationProps,
|
|
17
|
+
type UseVirtualizationReturn,
|
|
18
|
+
useVirtualization,
|
|
19
|
+
} from "./useVirtualization.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBatchedScroll Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages batched scroll state updates to allow multiple scroll operations
|
|
5
|
+
* within the same tick to accumulate properly.
|
|
6
|
+
*
|
|
7
|
+
* Ported from Gemini CLI.
|
|
8
|
+
*
|
|
9
|
+
* @module tui/components/common/VirtualizedList/hooks/useBatchedScroll
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return type for the useBatchedScroll hook.
|
|
16
|
+
*/
|
|
17
|
+
export interface UseBatchedScrollReturn {
|
|
18
|
+
/** Get the current or pending scroll position */
|
|
19
|
+
readonly getScrollTop: () => number;
|
|
20
|
+
/** Set a pending scroll position for the next render */
|
|
21
|
+
readonly setPendingScrollTop: (scrollTop: number) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A hook to manage batched scroll state updates.
|
|
26
|
+
*
|
|
27
|
+
* It allows multiple scroll operations within the same tick to accumulate
|
|
28
|
+
* by keeping track of a 'pending' state that resets after render.
|
|
29
|
+
*
|
|
30
|
+
* @param currentScrollTop - The current scroll position from state
|
|
31
|
+
* @returns Object with getScrollTop and setPendingScrollTop functions
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
|
|
36
|
+
*
|
|
37
|
+
* // In scroll handler:
|
|
38
|
+
* const current = getScrollTop();
|
|
39
|
+
* const next = current + delta;
|
|
40
|
+
* setPendingScrollTop(next);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function useBatchedScroll(currentScrollTop: number): UseBatchedScrollReturn {
|
|
44
|
+
const pendingScrollTopRef = useRef<number | null>(null);
|
|
45
|
+
// Use a ref for currentScrollTop to allow getScrollTop to be stable
|
|
46
|
+
const currentScrollTopRef = useRef(currentScrollTop);
|
|
47
|
+
|
|
48
|
+
// Reset pending state after each render and update current ref
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
currentScrollTopRef.current = currentScrollTop;
|
|
51
|
+
pendingScrollTopRef.current = null;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const getScrollTop = useCallback(
|
|
55
|
+
() => pendingScrollTopRef.current ?? currentScrollTopRef.current,
|
|
56
|
+
[]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const setPendingScrollTop = useCallback((newScrollTop: number) => {
|
|
60
|
+
pendingScrollTopRef.current = newScrollTop;
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
return { getScrollTop, setPendingScrollTop };
|
|
64
|
+
}
|