@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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useScrollAnchor Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages scroll position as an anchor (index + offset) for stability
|
|
5
|
+
* during content changes. This approach is more robust than pure pixel-based
|
|
6
|
+
* scrolling when items resize or new items are added.
|
|
7
|
+
*
|
|
8
|
+
* @module tui/components/common/VirtualizedList/hooks/useScrollAnchor
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
12
|
+
import { SCROLL_TO_ITEM_END, type ScrollAnchor } from "../types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Props for the useScrollAnchor hook.
|
|
16
|
+
*/
|
|
17
|
+
export interface UseScrollAnchorProps {
|
|
18
|
+
/** Number of data items */
|
|
19
|
+
readonly dataLength: number;
|
|
20
|
+
/** Array of cumulative offsets for each item */
|
|
21
|
+
readonly offsets: readonly number[];
|
|
22
|
+
/** Array of measured or estimated heights */
|
|
23
|
+
readonly heights: readonly number[];
|
|
24
|
+
/** Total height of all content */
|
|
25
|
+
readonly totalHeight: number;
|
|
26
|
+
/** Height of the visible container */
|
|
27
|
+
readonly containerHeight: number;
|
|
28
|
+
/** Initial scroll index */
|
|
29
|
+
readonly initialScrollIndex?: number;
|
|
30
|
+
/** Initial offset within the scroll index */
|
|
31
|
+
readonly initialScrollOffsetInIndex?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return type for the useScrollAnchor hook.
|
|
36
|
+
*/
|
|
37
|
+
export interface UseScrollAnchorReturn {
|
|
38
|
+
/** Current scroll anchor */
|
|
39
|
+
readonly scrollAnchor: ScrollAnchor;
|
|
40
|
+
/** Set the scroll anchor directly */
|
|
41
|
+
readonly setScrollAnchor: (anchor: ScrollAnchor) => void;
|
|
42
|
+
/** Whether currently sticking to bottom (auto-scroll enabled) */
|
|
43
|
+
readonly isStickingToBottom: boolean;
|
|
44
|
+
/** Set sticking to bottom state */
|
|
45
|
+
readonly setIsStickingToBottom: (value: boolean) => void;
|
|
46
|
+
/** Computed pixel scroll position from anchor */
|
|
47
|
+
readonly scrollTop: number;
|
|
48
|
+
/** Get anchor for a given scroll position */
|
|
49
|
+
readonly getAnchorForScrollTop: (scrollTop: number) => ScrollAnchor;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find the last index where predicate returns true.
|
|
54
|
+
*/
|
|
55
|
+
function findLastIndex<T>(
|
|
56
|
+
array: readonly T[],
|
|
57
|
+
predicate: (value: T, index: number) => boolean
|
|
58
|
+
): number {
|
|
59
|
+
for (let i = array.length - 1; i >= 0; i--) {
|
|
60
|
+
const item = array[i];
|
|
61
|
+
if (item !== undefined && predicate(item, i)) {
|
|
62
|
+
return i;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook for managing scroll position as an anchor.
|
|
70
|
+
*
|
|
71
|
+
* @param props - Configuration for scroll anchor behavior
|
|
72
|
+
* @returns Scroll anchor state and utilities
|
|
73
|
+
*/
|
|
74
|
+
export function useScrollAnchor(props: UseScrollAnchorProps): UseScrollAnchorReturn {
|
|
75
|
+
const {
|
|
76
|
+
dataLength,
|
|
77
|
+
offsets,
|
|
78
|
+
heights,
|
|
79
|
+
totalHeight,
|
|
80
|
+
containerHeight,
|
|
81
|
+
initialScrollIndex,
|
|
82
|
+
initialScrollOffsetInIndex,
|
|
83
|
+
} = props;
|
|
84
|
+
|
|
85
|
+
// Initialize scroll anchor based on initial props
|
|
86
|
+
const [scrollAnchor, setScrollAnchor] = useState<ScrollAnchor>(() => {
|
|
87
|
+
const scrollToEnd =
|
|
88
|
+
initialScrollIndex === SCROLL_TO_ITEM_END ||
|
|
89
|
+
(typeof initialScrollIndex === "number" &&
|
|
90
|
+
initialScrollIndex >= dataLength - 1 &&
|
|
91
|
+
initialScrollOffsetInIndex === SCROLL_TO_ITEM_END);
|
|
92
|
+
|
|
93
|
+
if (scrollToEnd) {
|
|
94
|
+
return {
|
|
95
|
+
index: dataLength > 0 ? dataLength - 1 : 0,
|
|
96
|
+
offset: SCROLL_TO_ITEM_END,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof initialScrollIndex === "number") {
|
|
101
|
+
return {
|
|
102
|
+
index: Math.max(0, Math.min(dataLength - 1, initialScrollIndex)),
|
|
103
|
+
offset: initialScrollOffsetInIndex ?? 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { index: 0, offset: 0 };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Track whether we're sticking to bottom (auto-scroll)
|
|
111
|
+
const [isStickingToBottom, setIsStickingToBottom] = useState(() => {
|
|
112
|
+
const scrollToEnd =
|
|
113
|
+
initialScrollIndex === SCROLL_TO_ITEM_END ||
|
|
114
|
+
(typeof initialScrollIndex === "number" &&
|
|
115
|
+
initialScrollIndex >= dataLength - 1 &&
|
|
116
|
+
initialScrollOffsetInIndex === SCROLL_TO_ITEM_END);
|
|
117
|
+
return scrollToEnd;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Track if initial scroll has been set
|
|
121
|
+
const isInitialScrollSet = useRef(false);
|
|
122
|
+
|
|
123
|
+
// Convert scroll position to anchor
|
|
124
|
+
// FIX: Added bounds validation to prevent invalid anchor indices
|
|
125
|
+
const getAnchorForScrollTop = useCallback(
|
|
126
|
+
(scrollTop: number): ScrollAnchor => {
|
|
127
|
+
// Handle edge cases
|
|
128
|
+
if (dataLength === 0) {
|
|
129
|
+
return { index: 0, offset: 0 };
|
|
130
|
+
}
|
|
131
|
+
if (scrollTop <= 0) {
|
|
132
|
+
return { index: 0, offset: 0 };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const index = findLastIndex(offsets, (offset) => offset <= scrollTop);
|
|
136
|
+
if (index === -1) {
|
|
137
|
+
return { index: 0, offset: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// FIX: Ensure index is within valid bounds
|
|
141
|
+
const safeIndex = Math.max(0, Math.min(dataLength - 1, index));
|
|
142
|
+
const offsetValue = offsets[safeIndex] ?? 0;
|
|
143
|
+
const itemHeight = heights[safeIndex] ?? 0;
|
|
144
|
+
|
|
145
|
+
// FIX: Clamp offset to be within the item's height
|
|
146
|
+
const rawOffset = scrollTop - offsetValue;
|
|
147
|
+
const safeOffset = Math.max(0, Math.min(itemHeight, rawOffset));
|
|
148
|
+
|
|
149
|
+
return { index: safeIndex, offset: safeOffset };
|
|
150
|
+
},
|
|
151
|
+
[offsets, dataLength, heights]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Compute pixel scroll position from anchor
|
|
155
|
+
const scrollTop = (() => {
|
|
156
|
+
const offset = offsets[scrollAnchor.index];
|
|
157
|
+
if (typeof offset !== "number") {
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (scrollAnchor.offset === SCROLL_TO_ITEM_END) {
|
|
162
|
+
const itemHeight = heights[scrollAnchor.index] ?? 0;
|
|
163
|
+
return Math.max(0, offset + itemHeight - containerHeight);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Math.max(0, offset + scrollAnchor.offset);
|
|
167
|
+
})();
|
|
168
|
+
|
|
169
|
+
// Track previous values for auto-scroll logic
|
|
170
|
+
const prevDataLength = useRef(dataLength);
|
|
171
|
+
const prevTotalHeight = useRef(totalHeight);
|
|
172
|
+
const prevScrollTop = useRef(scrollTop);
|
|
173
|
+
const prevContainerHeight = useRef(containerHeight);
|
|
174
|
+
|
|
175
|
+
// Handle auto-scroll and anchor adjustments
|
|
176
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Scroll anchor logic with multiple edge cases
|
|
177
|
+
useLayoutEffect(() => {
|
|
178
|
+
const contentPreviouslyFit = prevTotalHeight.current <= prevContainerHeight.current;
|
|
179
|
+
const wasScrolledToBottomPixels =
|
|
180
|
+
prevScrollTop.current >= prevTotalHeight.current - prevContainerHeight.current - 1;
|
|
181
|
+
const wasAtBottom = contentPreviouslyFit || wasScrolledToBottomPixels;
|
|
182
|
+
|
|
183
|
+
// If the user was at the bottom, they are now sticking
|
|
184
|
+
if (wasAtBottom && scrollTop >= prevScrollTop.current) {
|
|
185
|
+
setIsStickingToBottom(true);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const listGrew = dataLength > prevDataLength.current;
|
|
189
|
+
const containerChanged = prevContainerHeight.current !== containerHeight;
|
|
190
|
+
// Detect content height growth (triggers during streaming output)
|
|
191
|
+
const contentHeightGrew = totalHeight > prevTotalHeight.current;
|
|
192
|
+
|
|
193
|
+
// Scroll to end conditions:
|
|
194
|
+
// 1. List grew AND we were already at the bottom (or sticking)
|
|
195
|
+
// 2. We are sticking to bottom AND container size changed
|
|
196
|
+
// 3. We are sticking to bottom AND content height grew (streaming content)
|
|
197
|
+
if (
|
|
198
|
+
(listGrew && (isStickingToBottom || wasAtBottom)) ||
|
|
199
|
+
(isStickingToBottom && containerChanged) ||
|
|
200
|
+
(isStickingToBottom && contentHeightGrew)
|
|
201
|
+
) {
|
|
202
|
+
setScrollAnchor({
|
|
203
|
+
index: dataLength > 0 ? dataLength - 1 : 0,
|
|
204
|
+
offset: SCROLL_TO_ITEM_END,
|
|
205
|
+
});
|
|
206
|
+
if (!isStickingToBottom) {
|
|
207
|
+
setIsStickingToBottom(true);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// List shrunk or scroll position is invalid
|
|
211
|
+
else if (
|
|
212
|
+
(scrollAnchor.index >= dataLength || scrollTop > totalHeight - containerHeight) &&
|
|
213
|
+
dataLength > 0
|
|
214
|
+
) {
|
|
215
|
+
const newScrollTop = Math.max(0, totalHeight - containerHeight);
|
|
216
|
+
setScrollAnchor(getAnchorForScrollTop(newScrollTop));
|
|
217
|
+
} else if (dataLength === 0) {
|
|
218
|
+
// List is empty, reset to top
|
|
219
|
+
setScrollAnchor({ index: 0, offset: 0 });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Update refs for next render
|
|
223
|
+
prevDataLength.current = dataLength;
|
|
224
|
+
prevTotalHeight.current = totalHeight;
|
|
225
|
+
prevScrollTop.current = scrollTop;
|
|
226
|
+
prevContainerHeight.current = containerHeight;
|
|
227
|
+
}, [
|
|
228
|
+
dataLength,
|
|
229
|
+
totalHeight,
|
|
230
|
+
scrollTop,
|
|
231
|
+
containerHeight,
|
|
232
|
+
scrollAnchor.index,
|
|
233
|
+
getAnchorForScrollTop,
|
|
234
|
+
isStickingToBottom,
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
// Handle initial scroll position
|
|
238
|
+
useLayoutEffect(() => {
|
|
239
|
+
if (
|
|
240
|
+
isInitialScrollSet.current ||
|
|
241
|
+
offsets.length <= 1 ||
|
|
242
|
+
totalHeight <= 0 ||
|
|
243
|
+
containerHeight <= 0
|
|
244
|
+
) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (typeof initialScrollIndex === "number") {
|
|
249
|
+
const scrollToEnd =
|
|
250
|
+
initialScrollIndex === SCROLL_TO_ITEM_END ||
|
|
251
|
+
(initialScrollIndex >= dataLength - 1 && initialScrollOffsetInIndex === SCROLL_TO_ITEM_END);
|
|
252
|
+
|
|
253
|
+
if (scrollToEnd) {
|
|
254
|
+
setScrollAnchor({
|
|
255
|
+
index: dataLength - 1,
|
|
256
|
+
offset: SCROLL_TO_ITEM_END,
|
|
257
|
+
});
|
|
258
|
+
setIsStickingToBottom(true);
|
|
259
|
+
isInitialScrollSet.current = true;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const index = Math.max(0, Math.min(dataLength - 1, initialScrollIndex));
|
|
264
|
+
const offset = initialScrollOffsetInIndex ?? 0;
|
|
265
|
+
const newScrollTop = (offsets[index] ?? 0) + offset;
|
|
266
|
+
|
|
267
|
+
const clampedScrollTop = Math.max(0, Math.min(totalHeight - containerHeight, newScrollTop));
|
|
268
|
+
|
|
269
|
+
setScrollAnchor(getAnchorForScrollTop(clampedScrollTop));
|
|
270
|
+
isInitialScrollSet.current = true;
|
|
271
|
+
}
|
|
272
|
+
}, [
|
|
273
|
+
initialScrollIndex,
|
|
274
|
+
initialScrollOffsetInIndex,
|
|
275
|
+
offsets,
|
|
276
|
+
totalHeight,
|
|
277
|
+
containerHeight,
|
|
278
|
+
getAnchorForScrollTop,
|
|
279
|
+
dataLength,
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
scrollAnchor,
|
|
284
|
+
setScrollAnchor,
|
|
285
|
+
isStickingToBottom,
|
|
286
|
+
setIsStickingToBottom,
|
|
287
|
+
scrollTop,
|
|
288
|
+
getAnchorForScrollTop,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVirtualization Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages height calculations, measurement, and visible range computation
|
|
5
|
+
* for virtualized list rendering.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/common/VirtualizedList/hooks/useVirtualization
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { DOMElement } from "ink";
|
|
11
|
+
import { measureElement } from "ink";
|
|
12
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Props for the useVirtualization hook.
|
|
16
|
+
*/
|
|
17
|
+
export interface UseVirtualizationProps {
|
|
18
|
+
/** Number of data items */
|
|
19
|
+
readonly dataLength: number;
|
|
20
|
+
/** Function or fixed value for estimated item height */
|
|
21
|
+
readonly estimatedItemHeight: number | ((index: number) => number);
|
|
22
|
+
/** Current scroll position in pixels */
|
|
23
|
+
readonly scrollTop: number;
|
|
24
|
+
/** Height of the visible container */
|
|
25
|
+
readonly containerHeight: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Minimum viewport dimensions to prevent degenerate cases.
|
|
30
|
+
*/
|
|
31
|
+
export const MIN_VIEWPORT_HEIGHT = 8;
|
|
32
|
+
export const MIN_VIEWPORT_WIDTH = 20;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return type for the useVirtualization hook.
|
|
36
|
+
*/
|
|
37
|
+
export interface UseVirtualizationReturn {
|
|
38
|
+
/** Array of measured or estimated heights */
|
|
39
|
+
readonly heights: readonly number[];
|
|
40
|
+
/** Array of cumulative offsets */
|
|
41
|
+
readonly offsets: readonly number[];
|
|
42
|
+
/** Total height of all content */
|
|
43
|
+
readonly totalHeight: number;
|
|
44
|
+
/** First visible item index */
|
|
45
|
+
readonly startIndex: number;
|
|
46
|
+
/** Last visible item index */
|
|
47
|
+
readonly endIndex: number;
|
|
48
|
+
/** Height of spacer above visible items */
|
|
49
|
+
readonly topSpacerHeight: number;
|
|
50
|
+
/** Height of spacer below visible items */
|
|
51
|
+
readonly bottomSpacerHeight: number;
|
|
52
|
+
/** Ref callback for item elements */
|
|
53
|
+
readonly itemRefCallback: (index: number, el: DOMElement | null) => void;
|
|
54
|
+
/** Ref for the container element */
|
|
55
|
+
readonly containerRef: React.RefObject<DOMElement | null>;
|
|
56
|
+
/** Measured container height */
|
|
57
|
+
readonly measuredContainerHeight: number;
|
|
58
|
+
/** True if the last item exceeds viewport height (needs clipping) */
|
|
59
|
+
readonly isOversize: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find the last index where predicate returns true.
|
|
64
|
+
*/
|
|
65
|
+
function findLastIndex<T>(
|
|
66
|
+
array: readonly T[],
|
|
67
|
+
predicate: (value: T, index: number) => boolean
|
|
68
|
+
): number {
|
|
69
|
+
for (let i = array.length - 1; i >= 0; i--) {
|
|
70
|
+
const item = array[i];
|
|
71
|
+
if (item !== undefined && predicate(item, i)) {
|
|
72
|
+
return i;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return -1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Minimum valid height for any item to prevent invisible/zero-height items
|
|
80
|
+
*/
|
|
81
|
+
const MIN_ITEM_HEIGHT = 1;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get estimated height for an index using the estimator.
|
|
85
|
+
* FIX: Always returns at least MIN_ITEM_HEIGHT to prevent zero/negative heights
|
|
86
|
+
* that can cause infinite scroll loops or invisible items.
|
|
87
|
+
*/
|
|
88
|
+
function getEstimatedHeight(
|
|
89
|
+
estimator: number | ((index: number) => number),
|
|
90
|
+
index: number
|
|
91
|
+
): number {
|
|
92
|
+
const estimated = typeof estimator === "function" ? estimator(index) : estimator;
|
|
93
|
+
// FIX: Ensure height is always valid (positive integer)
|
|
94
|
+
return Math.max(MIN_ITEM_HEIGHT, Math.round(estimated));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Hook for managing virtualization state.
|
|
99
|
+
*
|
|
100
|
+
* @param props - Configuration for virtualization
|
|
101
|
+
* @returns Virtualization state and utilities
|
|
102
|
+
*/
|
|
103
|
+
export function useVirtualization(props: UseVirtualizationProps): UseVirtualizationReturn {
|
|
104
|
+
const { dataLength, estimatedItemHeight, scrollTop, containerHeight } = props;
|
|
105
|
+
|
|
106
|
+
// Apply minimum viewport clamping to prevent degenerate cases
|
|
107
|
+
const safeContainerHeight = Math.max(MIN_VIEWPORT_HEIGHT, containerHeight);
|
|
108
|
+
|
|
109
|
+
// Container ref for measuring viewport
|
|
110
|
+
const containerRef = useRef<DOMElement | null>(null);
|
|
111
|
+
const [measuredContainerHeight, setMeasuredContainerHeight] = useState(safeContainerHeight);
|
|
112
|
+
|
|
113
|
+
// Item refs for measurement
|
|
114
|
+
const itemRefs = useRef<Array<DOMElement | null>>([]);
|
|
115
|
+
|
|
116
|
+
// Heights cache - measured or estimated
|
|
117
|
+
const [heights, setHeights] = useState<number[]>(() => {
|
|
118
|
+
const initial: number[] = [];
|
|
119
|
+
for (let i = 0; i < dataLength; i++) {
|
|
120
|
+
initial[i] = getEstimatedHeight(estimatedItemHeight, i);
|
|
121
|
+
}
|
|
122
|
+
return initial;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Calculate offsets and total height
|
|
126
|
+
const { totalHeight, offsets } = useMemo(() => {
|
|
127
|
+
const offsets: number[] = [0];
|
|
128
|
+
let totalHeight = 0;
|
|
129
|
+
for (let i = 0; i < dataLength; i++) {
|
|
130
|
+
const height = heights[i] ?? getEstimatedHeight(estimatedItemHeight, i);
|
|
131
|
+
totalHeight += height;
|
|
132
|
+
offsets.push(totalHeight);
|
|
133
|
+
}
|
|
134
|
+
return { totalHeight, offsets };
|
|
135
|
+
}, [heights, dataLength, estimatedItemHeight]);
|
|
136
|
+
|
|
137
|
+
// Sync heights array with data length changes
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
setHeights((prevHeights) => {
|
|
140
|
+
if (dataLength === prevHeights.length) {
|
|
141
|
+
return prevHeights;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const newHeights = [...prevHeights];
|
|
145
|
+
if (dataLength < prevHeights.length) {
|
|
146
|
+
// Shrink
|
|
147
|
+
newHeights.length = dataLength;
|
|
148
|
+
} else {
|
|
149
|
+
// Grow - add estimated heights for new items
|
|
150
|
+
for (let i = prevHeights.length; i < dataLength; i++) {
|
|
151
|
+
newHeights[i] = getEstimatedHeight(estimatedItemHeight, i);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return newHeights;
|
|
155
|
+
});
|
|
156
|
+
}, [dataLength, estimatedItemHeight]);
|
|
157
|
+
|
|
158
|
+
// Calculate visible range with OVERFLOW GUARD
|
|
159
|
+
// This ensures we NEVER render more items than fit in the viewport
|
|
160
|
+
// FIX: Improved off-by-one handling - findLastIndex returns the index where offset <= scrollTop
|
|
161
|
+
// We don't need to subtract 1; that was causing negative indices and blank areas
|
|
162
|
+
const foundStartIndex = findLastIndex(offsets, (offset) => offset <= scrollTop);
|
|
163
|
+
// If no offset found (scrollTop < 0 or empty), start at 0
|
|
164
|
+
// Otherwise use the found index directly (no -1 subtraction)
|
|
165
|
+
const rawStartIndex = Math.max(0, foundStartIndex === -1 ? 0 : foundStartIndex);
|
|
166
|
+
|
|
167
|
+
const endIndexOffset = offsets.findIndex((offset) => offset > scrollTop + safeContainerHeight);
|
|
168
|
+
// FIX: Handle edge case where no offset exceeds viewport - show all remaining items
|
|
169
|
+
const rawEndIndex =
|
|
170
|
+
endIndexOffset === -1
|
|
171
|
+
? dataLength - 1
|
|
172
|
+
: Math.min(dataLength - 1, Math.max(0, endIndexOffset - 1));
|
|
173
|
+
|
|
174
|
+
// FRAME HEIGHT GUARD: Calculate safe render range to prevent overflow
|
|
175
|
+
// Simplified version: no isOversize (ClippedMessage doesn't exist yet)
|
|
176
|
+
// Always show at least MIN_ITEMS_TO_SHOW to prevent content disappearing
|
|
177
|
+
const { startIndex, endIndex } = useMemo(() => {
|
|
178
|
+
// CRITICAL FIX: Always render at least one item if data exists
|
|
179
|
+
// Previous logic returned empty range causing blank screen
|
|
180
|
+
if (dataLength === 0) {
|
|
181
|
+
return { startIndex: 0, endIndex: -1 };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Ensure we have valid indices even if raw calculation failed
|
|
185
|
+
const safeRawEndIndex = rawEndIndex < 0 ? dataLength - 1 : rawEndIndex;
|
|
186
|
+
const safeRawStartIndex = Math.min(rawStartIndex, safeRawEndIndex);
|
|
187
|
+
|
|
188
|
+
// Safety: ensure we have valid container height
|
|
189
|
+
const effectiveContainerHeight = Math.max(MIN_VIEWPORT_HEIGHT, safeContainerHeight);
|
|
190
|
+
|
|
191
|
+
// If raw range is small enough, just use it directly (don't over-optimize)
|
|
192
|
+
const rawCount = safeRawEndIndex - safeRawStartIndex + 1;
|
|
193
|
+
if (rawCount <= 3) {
|
|
194
|
+
return { startIndex: safeRawStartIndex, endIndex: safeRawEndIndex };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// For larger ranges, do soft trimming but always keep at least 2 items
|
|
198
|
+
let totalRenderedHeight = 0;
|
|
199
|
+
let safeStartIndex = safeRawEndIndex;
|
|
200
|
+
const MIN_ITEMS_TO_SHOW = 2;
|
|
201
|
+
|
|
202
|
+
for (let i = safeRawEndIndex; i >= safeRawStartIndex; i--) {
|
|
203
|
+
const itemHeight = heights[i] ?? getEstimatedHeight(estimatedItemHeight, i);
|
|
204
|
+
// Use actual height, not capped - let Ink handle overflow
|
|
205
|
+
|
|
206
|
+
const itemCount = safeRawEndIndex - safeStartIndex + 1;
|
|
207
|
+
if (
|
|
208
|
+
totalRenderedHeight + itemHeight > effectiveContainerHeight &&
|
|
209
|
+
itemCount >= MIN_ITEMS_TO_SHOW
|
|
210
|
+
) {
|
|
211
|
+
// Would overflow AND we have minimum items - stop
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
totalRenderedHeight += itemHeight;
|
|
216
|
+
safeStartIndex = i;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { startIndex: safeStartIndex, endIndex: safeRawEndIndex };
|
|
220
|
+
}, [rawStartIndex, rawEndIndex, heights, safeContainerHeight, estimatedItemHeight, dataLength]);
|
|
221
|
+
|
|
222
|
+
// isOversize is kept for API compatibility but always false
|
|
223
|
+
// (ClippedMessage doesn't exist yet, so we can't handle oversize items)
|
|
224
|
+
const isOversize = false;
|
|
225
|
+
|
|
226
|
+
// Calculate spacer heights
|
|
227
|
+
const topSpacerHeight = offsets[startIndex] ?? 0;
|
|
228
|
+
const bottomSpacerHeight = totalHeight - (offsets[endIndex + 1] ?? totalHeight);
|
|
229
|
+
|
|
230
|
+
// Item ref callback for measurement
|
|
231
|
+
const itemRefCallback = (index: number, el: DOMElement | null) => {
|
|
232
|
+
itemRefs.current[index] = el;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Track previous values to avoid unnecessary measurements
|
|
236
|
+
const prevStartIndexRef = useRef(startIndex);
|
|
237
|
+
const prevEndIndexRef = useRef(endIndex);
|
|
238
|
+
const prevDataLengthRef = useRef(dataLength);
|
|
239
|
+
|
|
240
|
+
// Periodic measurement tick to catch dynamic content height changes
|
|
241
|
+
// This ensures collapsible content like ThinkingBlock gets re-measured when expanded
|
|
242
|
+
const [measureTick, setMeasureTick] = useState(0);
|
|
243
|
+
const REMEASURE_INTERVAL_MS = 250; // Check every 250ms for height changes
|
|
244
|
+
|
|
245
|
+
// Set up periodic measurement timer
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
const timer = setInterval(() => {
|
|
248
|
+
setMeasureTick((t) => t + 1);
|
|
249
|
+
}, REMEASURE_INTERVAL_MS);
|
|
250
|
+
return () => clearInterval(timer);
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
// Measure container and visible items when visible range changes or on periodic tick
|
|
254
|
+
// FIX: Added proper dependency array to prevent measuring on every single render
|
|
255
|
+
// which was causing extreme CPU usage and frame drops
|
|
256
|
+
// FIX2: Added periodic re-measurement to catch dynamic content changes (ThinkingBlock expand/collapse)
|
|
257
|
+
// FIX3: Enhanced detection for significant height changes (>5px threshold) to catch collapsible content
|
|
258
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Item measurement logic with height tracking
|
|
259
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: measureTick intentionally triggers periodic re-measurement
|
|
260
|
+
useLayoutEffect(() => {
|
|
261
|
+
// Check if we actually need to re-measure
|
|
262
|
+
const rangeChanged =
|
|
263
|
+
prevStartIndexRef.current !== startIndex ||
|
|
264
|
+
prevEndIndexRef.current !== endIndex ||
|
|
265
|
+
prevDataLengthRef.current !== dataLength;
|
|
266
|
+
|
|
267
|
+
prevStartIndexRef.current = startIndex;
|
|
268
|
+
prevEndIndexRef.current = endIndex;
|
|
269
|
+
prevDataLengthRef.current = dataLength;
|
|
270
|
+
|
|
271
|
+
// Measure container
|
|
272
|
+
if (containerRef.current) {
|
|
273
|
+
const height = Math.round(measureElement(containerRef.current).height);
|
|
274
|
+
if (measuredContainerHeight !== height && height > 0) {
|
|
275
|
+
setMeasuredContainerHeight(height);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// FIX3: Check for significant height changes in visible items
|
|
280
|
+
// This catches ThinkingBlock expand/collapse which can cause large height deltas
|
|
281
|
+
const HEIGHT_CHANGE_THRESHOLD = 5; // pixels
|
|
282
|
+
let forceRemeasure = false;
|
|
283
|
+
|
|
284
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
285
|
+
const itemRef = itemRefs.current[i];
|
|
286
|
+
if (itemRef) {
|
|
287
|
+
const currentHeight = Math.round(measureElement(itemRef).height);
|
|
288
|
+
const cachedHeight = heights[i] ?? 0;
|
|
289
|
+
if (Math.abs(currentHeight - cachedHeight) > HEIGHT_CHANGE_THRESHOLD) {
|
|
290
|
+
forceRemeasure = true;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Measure visible items when:
|
|
297
|
+
// 1. Range changed (scroll/data update)
|
|
298
|
+
// 2. Initial mount (heights.length === 0)
|
|
299
|
+
// 3. measureTick changed (periodic check for dynamic content)
|
|
300
|
+
// 4. FIX3: Significant height change detected (forceRemeasure)
|
|
301
|
+
// Note: measureTick is in deps, so this runs periodically
|
|
302
|
+
if (!rangeChanged && heights.length > 0 && !forceRemeasure) {
|
|
303
|
+
// No changes needed - skip expensive remeasurement
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Measure visible items
|
|
308
|
+
let newHeights: number[] | null = null;
|
|
309
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
310
|
+
const itemRef = itemRefs.current[i];
|
|
311
|
+
if (itemRef) {
|
|
312
|
+
const height = Math.round(measureElement(itemRef).height);
|
|
313
|
+
// Only update if height actually changed and is valid
|
|
314
|
+
if (height > 0 && height !== heights[i]) {
|
|
315
|
+
if (!newHeights) {
|
|
316
|
+
newHeights = [...heights];
|
|
317
|
+
}
|
|
318
|
+
newHeights[i] = height;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (newHeights) {
|
|
323
|
+
setHeights(newHeights);
|
|
324
|
+
}
|
|
325
|
+
}, [startIndex, endIndex, dataLength, heights, measuredContainerHeight, measureTick]);
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
heights,
|
|
329
|
+
offsets,
|
|
330
|
+
totalHeight,
|
|
331
|
+
startIndex,
|
|
332
|
+
endIndex,
|
|
333
|
+
topSpacerHeight,
|
|
334
|
+
bottomSpacerHeight,
|
|
335
|
+
itemRefCallback,
|
|
336
|
+
containerRef,
|
|
337
|
+
measuredContainerHeight,
|
|
338
|
+
isOversize,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VirtualizedList Component
|
|
3
|
+
*
|
|
4
|
+
* A high-performance virtualized list for terminal UIs that only
|
|
5
|
+
* renders visible items. Ported from Gemini CLI.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/common/VirtualizedList
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
type UseBatchedScrollReturn,
|
|
12
|
+
type UseScrollAnchorProps,
|
|
13
|
+
type UseScrollAnchorReturn,
|
|
14
|
+
type UseVirtualizationProps,
|
|
15
|
+
type UseVirtualizationReturn,
|
|
16
|
+
useBatchedScroll,
|
|
17
|
+
useScrollAnchor,
|
|
18
|
+
useVirtualization,
|
|
19
|
+
} from "./hooks/index.js";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
type HeightCache,
|
|
23
|
+
SCROLL_TO_ITEM_END,
|
|
24
|
+
type ScrollAnchor,
|
|
25
|
+
} from "./types.js";
|
|
26
|
+
export {
|
|
27
|
+
VirtualizedList,
|
|
28
|
+
type VirtualizedListProps,
|
|
29
|
+
type VirtualizedListRef,
|
|
30
|
+
} from "./VirtualizedList.js";
|