@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,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Controller Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages scroll state for message viewport with follow/manual modes.
|
|
5
|
+
* Provides a state machine for automatic scrolling (follow) vs manual
|
|
6
|
+
* navigation, with smooth transitions between modes.
|
|
7
|
+
*
|
|
8
|
+
* State Machine:
|
|
9
|
+
* ```
|
|
10
|
+
* follow ──[scrollUp/PageUp/MouseWheel]──> manual
|
|
11
|
+
* manual ──[scrollToBottom/End/reach-bottom]──> follow
|
|
12
|
+
* manual ──[newMessage]──> stay manual, increment newMessageCount
|
|
13
|
+
* follow ──[newMessage]──> stay follow, auto-scroll
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @module tui/hooks/useScrollController
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useCallback, useMemo, useReducer } from "react";
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Scroll mode determines auto-scroll behavior.
|
|
27
|
+
* - follow: Automatically scroll to bottom on new content
|
|
28
|
+
* - manual: User is manually scrolling, don't auto-scroll
|
|
29
|
+
*/
|
|
30
|
+
export type ScrollMode = "follow" | "manual";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Immutable scroll state for viewport scroll controller.
|
|
34
|
+
* Named ViewportScrollState to avoid conflicts with ScrollContext.ScrollState.
|
|
35
|
+
*/
|
|
36
|
+
export interface ViewportScrollState {
|
|
37
|
+
/** Current scroll mode */
|
|
38
|
+
readonly mode: ScrollMode;
|
|
39
|
+
/** Offset from bottom in lines (0 = at bottom) */
|
|
40
|
+
readonly offsetFromBottom: number;
|
|
41
|
+
/** Number of new messages since entering manual mode */
|
|
42
|
+
readonly newMessageCount: number;
|
|
43
|
+
/** Total scrollable height in lines */
|
|
44
|
+
readonly totalHeight: number;
|
|
45
|
+
/** Visible viewport height in lines */
|
|
46
|
+
readonly viewportHeight: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Actions for controlling viewport scroll behavior.
|
|
51
|
+
*/
|
|
52
|
+
export interface ViewportScrollActions {
|
|
53
|
+
/** Scroll up by N lines (default: scrollStep from options) */
|
|
54
|
+
scrollUp(lines?: number): void;
|
|
55
|
+
/** Scroll down by N lines (default: scrollStep from options) */
|
|
56
|
+
scrollDown(lines?: number): void;
|
|
57
|
+
/** Jump to specific offset from bottom */
|
|
58
|
+
jumpTo(offset: number): void;
|
|
59
|
+
/** Return to follow mode (scroll to bottom) */
|
|
60
|
+
scrollToBottom(): void;
|
|
61
|
+
/** Update total height (called when messages change) */
|
|
62
|
+
setTotalHeight(height: number): void;
|
|
63
|
+
/** Update viewport height (called when terminal resizes) */
|
|
64
|
+
setViewportHeight(height: number): void;
|
|
65
|
+
/** Notify new message arrived */
|
|
66
|
+
notifyNewMessage(): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Options for useScrollController hook.
|
|
71
|
+
*/
|
|
72
|
+
export interface UseScrollControllerOptions {
|
|
73
|
+
/** Viewport height in lines */
|
|
74
|
+
readonly viewportHeight: number;
|
|
75
|
+
/** Initial total height */
|
|
76
|
+
readonly initialTotalHeight?: number;
|
|
77
|
+
/** Lines to scroll per action (default: 3) */
|
|
78
|
+
readonly scrollStep?: number;
|
|
79
|
+
/** Auto-switch to follow when reaching bottom (default: true) */
|
|
80
|
+
readonly autoFollowOnBottom?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Reducer
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Internal state shape (same as ScrollState but mutable for reducer)
|
|
89
|
+
*/
|
|
90
|
+
interface InternalState {
|
|
91
|
+
mode: ScrollMode;
|
|
92
|
+
offsetFromBottom: number;
|
|
93
|
+
newMessageCount: number;
|
|
94
|
+
totalHeight: number;
|
|
95
|
+
viewportHeight: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Action types for the scroll reducer
|
|
100
|
+
*/
|
|
101
|
+
type ScrollAction =
|
|
102
|
+
| { type: "SCROLL_UP"; lines: number; autoFollowOnBottom: boolean }
|
|
103
|
+
| { type: "SCROLL_DOWN"; lines: number; autoFollowOnBottom: boolean }
|
|
104
|
+
| { type: "JUMP_TO"; offset: number; autoFollowOnBottom: boolean }
|
|
105
|
+
| { type: "SCROLL_TO_BOTTOM" }
|
|
106
|
+
| { type: "SET_TOTAL_HEIGHT"; height: number }
|
|
107
|
+
| { type: "SET_VIEWPORT_HEIGHT"; height: number }
|
|
108
|
+
| { type: "NEW_MESSAGE" };
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Computes the maximum scrollable offset (how far up we can scroll)
|
|
112
|
+
*/
|
|
113
|
+
function getMaxOffset(totalHeight: number, viewportHeight: number): number {
|
|
114
|
+
return Math.max(0, totalHeight - viewportHeight);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clamps offset to valid range [0, maxOffset]
|
|
119
|
+
*/
|
|
120
|
+
function clampOffset(offset: number, totalHeight: number, viewportHeight: number): number {
|
|
121
|
+
const maxOffset = getMaxOffset(totalHeight, viewportHeight);
|
|
122
|
+
return Math.max(0, Math.min(offset, maxOffset));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Reducer for scroll state management
|
|
127
|
+
*/
|
|
128
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Reducer with many action cases is inherently complex
|
|
129
|
+
function scrollReducer(state: InternalState, action: ScrollAction): InternalState {
|
|
130
|
+
switch (action.type) {
|
|
131
|
+
case "SCROLL_UP": {
|
|
132
|
+
// Scrolling up switches to manual mode
|
|
133
|
+
const newOffset = clampOffset(
|
|
134
|
+
state.offsetFromBottom + action.lines,
|
|
135
|
+
state.totalHeight,
|
|
136
|
+
state.viewportHeight
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Only switch to manual if we actually moved
|
|
140
|
+
if (newOffset === state.offsetFromBottom) {
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...state,
|
|
146
|
+
mode: "manual",
|
|
147
|
+
offsetFromBottom: newOffset,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "SCROLL_DOWN": {
|
|
152
|
+
const newOffset = clampOffset(
|
|
153
|
+
state.offsetFromBottom - action.lines,
|
|
154
|
+
state.totalHeight,
|
|
155
|
+
state.viewportHeight
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// If we've reached the bottom and autoFollow is enabled, switch to follow mode
|
|
159
|
+
if (newOffset === 0 && action.autoFollowOnBottom) {
|
|
160
|
+
return {
|
|
161
|
+
...state,
|
|
162
|
+
mode: "follow",
|
|
163
|
+
offsetFromBottom: 0,
|
|
164
|
+
newMessageCount: 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Only update if offset changed
|
|
169
|
+
if (newOffset === state.offsetFromBottom) {
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...state,
|
|
175
|
+
offsetFromBottom: newOffset,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case "JUMP_TO": {
|
|
180
|
+
const newOffset = clampOffset(action.offset, state.totalHeight, state.viewportHeight);
|
|
181
|
+
|
|
182
|
+
// If jumping to bottom with autoFollow, switch to follow mode
|
|
183
|
+
if (newOffset === 0 && action.autoFollowOnBottom) {
|
|
184
|
+
if (
|
|
185
|
+
state.mode === "follow" &&
|
|
186
|
+
state.offsetFromBottom === 0 &&
|
|
187
|
+
state.newMessageCount === 0
|
|
188
|
+
) {
|
|
189
|
+
return state;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
...state,
|
|
193
|
+
mode: "follow",
|
|
194
|
+
offsetFromBottom: 0,
|
|
195
|
+
newMessageCount: 0,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If jumping away from bottom, switch to manual
|
|
200
|
+
const newMode = newOffset > 0 ? "manual" : state.mode;
|
|
201
|
+
|
|
202
|
+
if (newOffset === state.offsetFromBottom && newMode === state.mode) {
|
|
203
|
+
return state;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...state,
|
|
208
|
+
mode: newMode,
|
|
209
|
+
offsetFromBottom: newOffset,
|
|
210
|
+
// Reset new message count only if returning to follow
|
|
211
|
+
newMessageCount: newMode === "follow" ? 0 : state.newMessageCount,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case "SCROLL_TO_BOTTOM": {
|
|
216
|
+
return {
|
|
217
|
+
...state,
|
|
218
|
+
mode: "follow",
|
|
219
|
+
offsetFromBottom: 0,
|
|
220
|
+
newMessageCount: 0,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case "SET_TOTAL_HEIGHT": {
|
|
225
|
+
const newTotalHeight = Math.max(0, action.height);
|
|
226
|
+
|
|
227
|
+
// In follow mode, keep offset at 0
|
|
228
|
+
if (state.mode === "follow") {
|
|
229
|
+
if (newTotalHeight === state.totalHeight && state.offsetFromBottom === 0) {
|
|
230
|
+
return state;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
...state,
|
|
234
|
+
totalHeight: newTotalHeight,
|
|
235
|
+
offsetFromBottom: 0,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// In manual mode, maintain the offset but clamp to valid range
|
|
240
|
+
const clampedOffset = clampOffset(
|
|
241
|
+
state.offsetFromBottom,
|
|
242
|
+
newTotalHeight,
|
|
243
|
+
state.viewportHeight
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (newTotalHeight === state.totalHeight && clampedOffset === state.offsetFromBottom) {
|
|
247
|
+
return state;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
...state,
|
|
252
|
+
totalHeight: newTotalHeight,
|
|
253
|
+
offsetFromBottom: clampedOffset,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case "SET_VIEWPORT_HEIGHT": {
|
|
258
|
+
const newViewportHeight = Math.max(1, action.height);
|
|
259
|
+
|
|
260
|
+
// Clamp offset to valid range with new viewport
|
|
261
|
+
const clampedOffset = clampOffset(
|
|
262
|
+
state.offsetFromBottom,
|
|
263
|
+
state.totalHeight,
|
|
264
|
+
newViewportHeight
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (newViewportHeight === state.viewportHeight && clampedOffset === state.offsetFromBottom) {
|
|
268
|
+
return state;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
...state,
|
|
273
|
+
viewportHeight: newViewportHeight,
|
|
274
|
+
offsetFromBottom: clampedOffset,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case "NEW_MESSAGE": {
|
|
279
|
+
if (state.mode === "follow") {
|
|
280
|
+
// In follow mode, stay at bottom (no state change needed)
|
|
281
|
+
return state;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// In manual mode, increment counter
|
|
285
|
+
return {
|
|
286
|
+
...state,
|
|
287
|
+
newMessageCount: state.newMessageCount + 1,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
default:
|
|
292
|
+
return state;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// =============================================================================
|
|
297
|
+
// Hook
|
|
298
|
+
// =============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* useScrollController - Manages scroll state for message viewport
|
|
302
|
+
*
|
|
303
|
+
* Provides a state machine for follow/manual scroll modes with
|
|
304
|
+
* automatic mode transitions based on user actions.
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```tsx
|
|
308
|
+
* const [scrollState, scrollActions] = useScrollController({
|
|
309
|
+
* viewportHeight: terminalHeight - headerHeight,
|
|
310
|
+
* initialTotalHeight: messages.length * avgLineHeight,
|
|
311
|
+
* });
|
|
312
|
+
*
|
|
313
|
+
* // In your scroll handler
|
|
314
|
+
* useInput((input, key) => {
|
|
315
|
+
* if (key.pageUp) scrollActions.scrollUp(scrollState.viewportHeight / 2);
|
|
316
|
+
* if (key.pageDown) scrollActions.scrollDown(scrollState.viewportHeight / 2);
|
|
317
|
+
* if (key.end) scrollActions.scrollToBottom();
|
|
318
|
+
* });
|
|
319
|
+
*
|
|
320
|
+
* // When messages change
|
|
321
|
+
* useEffect(() => {
|
|
322
|
+
* scrollActions.setTotalHeight(newHeight);
|
|
323
|
+
* scrollActions.notifyNewMessage();
|
|
324
|
+
* }, [messages]);
|
|
325
|
+
*
|
|
326
|
+
* // Show "X new messages" badge when in manual mode
|
|
327
|
+
* if (scrollState.mode === 'manual' && scrollState.newMessageCount > 0) {
|
|
328
|
+
* showNewMessagesBadge(scrollState.newMessageCount);
|
|
329
|
+
* }
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* @param options - Configuration options
|
|
333
|
+
* @returns Tuple of [state, actions]
|
|
334
|
+
*/
|
|
335
|
+
export function useScrollController(
|
|
336
|
+
options: UseScrollControllerOptions
|
|
337
|
+
): [ViewportScrollState, ViewportScrollActions] {
|
|
338
|
+
const {
|
|
339
|
+
viewportHeight,
|
|
340
|
+
initialTotalHeight = 0,
|
|
341
|
+
scrollStep = 3,
|
|
342
|
+
autoFollowOnBottom = true,
|
|
343
|
+
} = options;
|
|
344
|
+
|
|
345
|
+
// Initialize reducer state
|
|
346
|
+
const [state, dispatch] = useReducer(scrollReducer, {
|
|
347
|
+
mode: "follow",
|
|
348
|
+
offsetFromBottom: 0,
|
|
349
|
+
newMessageCount: 0,
|
|
350
|
+
totalHeight: Math.max(0, initialTotalHeight),
|
|
351
|
+
viewportHeight: Math.max(1, viewportHeight),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Create memoized actions
|
|
355
|
+
const scrollUp = useCallback(
|
|
356
|
+
(lines: number = scrollStep) => {
|
|
357
|
+
dispatch({ type: "SCROLL_UP", lines, autoFollowOnBottom });
|
|
358
|
+
},
|
|
359
|
+
[scrollStep, autoFollowOnBottom]
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const scrollDown = useCallback(
|
|
363
|
+
(lines: number = scrollStep) => {
|
|
364
|
+
dispatch({ type: "SCROLL_DOWN", lines, autoFollowOnBottom });
|
|
365
|
+
},
|
|
366
|
+
[scrollStep, autoFollowOnBottom]
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const jumpTo = useCallback(
|
|
370
|
+
(offset: number) => {
|
|
371
|
+
dispatch({ type: "JUMP_TO", offset, autoFollowOnBottom });
|
|
372
|
+
},
|
|
373
|
+
[autoFollowOnBottom]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const scrollToBottom = useCallback(() => {
|
|
377
|
+
dispatch({ type: "SCROLL_TO_BOTTOM" });
|
|
378
|
+
}, []);
|
|
379
|
+
|
|
380
|
+
const setTotalHeight = useCallback((height: number) => {
|
|
381
|
+
dispatch({ type: "SET_TOTAL_HEIGHT", height });
|
|
382
|
+
}, []);
|
|
383
|
+
|
|
384
|
+
const setViewportHeight = useCallback((height: number) => {
|
|
385
|
+
dispatch({ type: "SET_VIEWPORT_HEIGHT", height });
|
|
386
|
+
}, []);
|
|
387
|
+
|
|
388
|
+
const notifyNewMessage = useCallback(() => {
|
|
389
|
+
dispatch({ type: "NEW_MESSAGE" });
|
|
390
|
+
}, []);
|
|
391
|
+
|
|
392
|
+
// Bundle actions
|
|
393
|
+
const actions: ViewportScrollActions = useMemo(
|
|
394
|
+
() => ({
|
|
395
|
+
scrollUp,
|
|
396
|
+
scrollDown,
|
|
397
|
+
jumpTo,
|
|
398
|
+
scrollToBottom,
|
|
399
|
+
setTotalHeight,
|
|
400
|
+
setViewportHeight,
|
|
401
|
+
notifyNewMessage,
|
|
402
|
+
}),
|
|
403
|
+
[
|
|
404
|
+
scrollUp,
|
|
405
|
+
scrollDown,
|
|
406
|
+
jumpTo,
|
|
407
|
+
scrollToBottom,
|
|
408
|
+
setTotalHeight,
|
|
409
|
+
setViewportHeight,
|
|
410
|
+
notifyNewMessage,
|
|
411
|
+
]
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
return [state, actions];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// =============================================================================
|
|
418
|
+
// Utility Functions
|
|
419
|
+
// =============================================================================
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Calculate visible scroll position as a percentage (0-100)
|
|
423
|
+
*/
|
|
424
|
+
export function getScrollPercentage(state: ViewportScrollState): number {
|
|
425
|
+
const maxOffset = getMaxOffset(state.totalHeight, state.viewportHeight);
|
|
426
|
+
if (maxOffset === 0) return 100;
|
|
427
|
+
return Math.round(((maxOffset - state.offsetFromBottom) / maxOffset) * 100);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Check if content is scrollable (exceeds viewport)
|
|
432
|
+
*/
|
|
433
|
+
export function isScrollable(state: ViewportScrollState): boolean {
|
|
434
|
+
return state.totalHeight > state.viewportHeight;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Check if currently at the top
|
|
439
|
+
*/
|
|
440
|
+
export function isAtTop(state: ViewportScrollState): boolean {
|
|
441
|
+
const maxOffset = getMaxOffset(state.totalHeight, state.viewportHeight);
|
|
442
|
+
return state.offsetFromBottom >= maxOffset;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Check if currently at the bottom
|
|
447
|
+
*/
|
|
448
|
+
export function isAtBottom(state: ViewportScrollState): boolean {
|
|
449
|
+
return state.offsetFromBottom === 0;
|
|
450
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Event Batcher Hook
|
|
3
|
+
*
|
|
4
|
+
* Batches multiple scroll events within the same tick to prevent jitter.
|
|
5
|
+
* Useful when multiple sources can trigger scroll updates simultaneously.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/hooks/useScrollEventBatcher
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useRef, useState } from "react";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Batching strategy for combining scroll deltas
|
|
18
|
+
*/
|
|
19
|
+
export type BatchStrategy = "sum" | "last" | "max" | "min";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for scroll event batcher behavior
|
|
23
|
+
*/
|
|
24
|
+
export interface ScrollEventBatcherConfig {
|
|
25
|
+
/** Batch window in ms (default: 0 - same tick only) */
|
|
26
|
+
readonly batchWindow?: number;
|
|
27
|
+
/** Strategy for combining batched deltas (default: 'sum') */
|
|
28
|
+
readonly strategy?: BatchStrategy;
|
|
29
|
+
/** Maximum absolute delta to allow (default: Infinity) */
|
|
30
|
+
readonly maxDelta?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return type for useScrollEventBatcher hook
|
|
35
|
+
*/
|
|
36
|
+
export interface UseScrollEventBatcherReturn {
|
|
37
|
+
/** Queue a scroll delta (will be batched) */
|
|
38
|
+
readonly queueScroll: (delta: number) => void;
|
|
39
|
+
/** Number of pending deltas in current batch */
|
|
40
|
+
readonly pendingCount: number;
|
|
41
|
+
/** Force flush any pending scroll */
|
|
42
|
+
readonly flush: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Constants
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
const DEFAULT_CONFIG: Required<ScrollEventBatcherConfig> = {
|
|
50
|
+
batchWindow: 0,
|
|
51
|
+
strategy: "sum",
|
|
52
|
+
maxDelta: Infinity,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Strategy Functions
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Combine deltas based on strategy
|
|
61
|
+
*/
|
|
62
|
+
function combineDelta(deltas: number[], strategy: BatchStrategy): number {
|
|
63
|
+
if (deltas.length === 0) return 0;
|
|
64
|
+
|
|
65
|
+
switch (strategy) {
|
|
66
|
+
case "sum":
|
|
67
|
+
return deltas.reduce((a, b) => a + b, 0);
|
|
68
|
+
case "last": {
|
|
69
|
+
const lastDelta = deltas[deltas.length - 1];
|
|
70
|
+
return lastDelta !== undefined ? lastDelta : 0;
|
|
71
|
+
}
|
|
72
|
+
case "max":
|
|
73
|
+
return Math.max(...deltas);
|
|
74
|
+
case "min":
|
|
75
|
+
return Math.min(...deltas);
|
|
76
|
+
default:
|
|
77
|
+
return deltas.reduce((a, b) => a + b, 0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// Hook
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Hook for batching scroll events
|
|
87
|
+
*
|
|
88
|
+
* Collects scroll deltas within a batch window and combines them
|
|
89
|
+
* according to the configured strategy before calling the scroll handler.
|
|
90
|
+
*
|
|
91
|
+
* @param onScroll - Handler called with combined delta after batching
|
|
92
|
+
* @param config - Optional batching configuration
|
|
93
|
+
* @returns Batched scroll controls
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```tsx
|
|
97
|
+
* const { queueScroll } = useScrollEventBatcher(
|
|
98
|
+
* (delta) => scrollController.scrollBy(delta),
|
|
99
|
+
* { strategy: 'sum' }
|
|
100
|
+
* );
|
|
101
|
+
*
|
|
102
|
+
* // Multiple calls in same tick get batched
|
|
103
|
+
* queueScroll(1);
|
|
104
|
+
* queueScroll(2);
|
|
105
|
+
* queueScroll(3);
|
|
106
|
+
* // onScroll called once with delta=6
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function useScrollEventBatcher(
|
|
110
|
+
onScroll: (delta: number) => void,
|
|
111
|
+
config: ScrollEventBatcherConfig = {}
|
|
112
|
+
): UseScrollEventBatcherReturn {
|
|
113
|
+
// Merge config with defaults
|
|
114
|
+
const { batchWindow, strategy, maxDelta } = { ...DEFAULT_CONFIG, ...config };
|
|
115
|
+
|
|
116
|
+
// State
|
|
117
|
+
const [pendingCount, setPendingCount] = useState(0);
|
|
118
|
+
|
|
119
|
+
// Refs
|
|
120
|
+
const pendingDeltasRef = useRef<number[]>([]);
|
|
121
|
+
const flushTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
122
|
+
const onScrollRef = useRef(onScroll);
|
|
123
|
+
|
|
124
|
+
// Keep onScroll ref updated
|
|
125
|
+
onScrollRef.current = onScroll;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Flush pending deltas
|
|
129
|
+
*/
|
|
130
|
+
const flush = useCallback(() => {
|
|
131
|
+
if (flushTimeoutRef.current) {
|
|
132
|
+
clearTimeout(flushTimeoutRef.current);
|
|
133
|
+
flushTimeoutRef.current = null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const deltas = pendingDeltasRef.current;
|
|
137
|
+
if (deltas.length === 0) return;
|
|
138
|
+
|
|
139
|
+
// Combine deltas based on strategy
|
|
140
|
+
let combinedDelta = combineDelta(deltas, strategy);
|
|
141
|
+
|
|
142
|
+
// Clamp to maxDelta
|
|
143
|
+
if (Math.abs(combinedDelta) > maxDelta) {
|
|
144
|
+
combinedDelta = Math.sign(combinedDelta) * maxDelta;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Clear pending state
|
|
148
|
+
pendingDeltasRef.current = [];
|
|
149
|
+
setPendingCount(0);
|
|
150
|
+
|
|
151
|
+
// Execute scroll
|
|
152
|
+
onScrollRef.current(combinedDelta);
|
|
153
|
+
}, [strategy, maxDelta]);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Queue a scroll delta
|
|
157
|
+
*/
|
|
158
|
+
const queueScroll = useCallback(
|
|
159
|
+
(delta: number) => {
|
|
160
|
+
// Add to pending deltas
|
|
161
|
+
pendingDeltasRef.current.push(delta);
|
|
162
|
+
setPendingCount((c) => c + 1);
|
|
163
|
+
|
|
164
|
+
// Schedule flush
|
|
165
|
+
if (flushTimeoutRef.current) {
|
|
166
|
+
clearTimeout(flushTimeoutRef.current);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (batchWindow === 0) {
|
|
170
|
+
// Batch within same tick using microtask
|
|
171
|
+
queueMicrotask(flush);
|
|
172
|
+
} else {
|
|
173
|
+
// Batch within time window
|
|
174
|
+
flushTimeoutRef.current = setTimeout(flush, batchWindow);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
[batchWindow, flush]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
queueScroll,
|
|
182
|
+
pendingCount,
|
|
183
|
+
flush,
|
|
184
|
+
};
|
|
185
|
+
}
|