@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,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInputHighlight Hook (T009-HL)
|
|
3
|
+
*
|
|
4
|
+
* React hook that memoizes input text highlighting transformations.
|
|
5
|
+
* Provides efficient highlight parsing with caching.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/hooks/useInputHighlight
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useMemo, useRef } from "react";
|
|
11
|
+
import {
|
|
12
|
+
findSegmentAtCursor,
|
|
13
|
+
type HighlightResult,
|
|
14
|
+
type HighlightSegment,
|
|
15
|
+
parseHighlights,
|
|
16
|
+
} from "../components/Input/highlight.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for the useInputHighlight hook.
|
|
24
|
+
*/
|
|
25
|
+
export interface UseInputHighlightOptions {
|
|
26
|
+
/** The input text to highlight */
|
|
27
|
+
readonly text: string;
|
|
28
|
+
/** Whether highlighting is enabled */
|
|
29
|
+
readonly enabled?: boolean;
|
|
30
|
+
/** Current cursor position (optional) */
|
|
31
|
+
readonly cursorPosition?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return value from useInputHighlight hook.
|
|
36
|
+
*/
|
|
37
|
+
export interface UseInputHighlightReturn {
|
|
38
|
+
/** Parsed highlight result */
|
|
39
|
+
readonly result: HighlightResult;
|
|
40
|
+
/** Whether any highlights were found */
|
|
41
|
+
readonly hasHighlights: boolean;
|
|
42
|
+
/** The segment containing the cursor (if any) */
|
|
43
|
+
readonly cursorSegment: HighlightSegment | undefined;
|
|
44
|
+
/** Whether highlighting is active */
|
|
45
|
+
readonly isHighlighting: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Hook Implementation
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hook for memoized input text highlighting.
|
|
54
|
+
*
|
|
55
|
+
* Caches the highlight parsing result and only recomputes when text changes.
|
|
56
|
+
* Also provides cursor-aware segment information.
|
|
57
|
+
*
|
|
58
|
+
* @param options - Hook configuration
|
|
59
|
+
* @returns Highlight result and metadata
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* function MyInput({ value, cursorPosition }) {
|
|
64
|
+
* const { result, hasHighlights, cursorSegment } = useInputHighlight({
|
|
65
|
+
* text: value,
|
|
66
|
+
* enabled: true,
|
|
67
|
+
* cursorPosition,
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* return (
|
|
71
|
+
* <HighlightedText
|
|
72
|
+
* text={value}
|
|
73
|
+
* highlightResult={result}
|
|
74
|
+
* cursorPosition={cursorPosition}
|
|
75
|
+
* showCursor
|
|
76
|
+
* />
|
|
77
|
+
* );
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function useInputHighlight({
|
|
82
|
+
text,
|
|
83
|
+
enabled = true,
|
|
84
|
+
cursorPosition,
|
|
85
|
+
}: UseInputHighlightOptions): UseInputHighlightReturn {
|
|
86
|
+
// Cache for avoiding reparse on cursor-only changes
|
|
87
|
+
const cacheRef = useRef<{ text: string; result: HighlightResult } | null>(null);
|
|
88
|
+
|
|
89
|
+
// Parse highlights (memoized by text)
|
|
90
|
+
const result = useMemo(() => {
|
|
91
|
+
// Return empty result if disabled
|
|
92
|
+
if (!enabled) {
|
|
93
|
+
return { segments: [], hasHighlights: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check cache first
|
|
97
|
+
if (cacheRef.current?.text === text) {
|
|
98
|
+
return cacheRef.current.result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Parse and cache
|
|
102
|
+
const parsed = parseHighlights(text);
|
|
103
|
+
cacheRef.current = { text, result: parsed };
|
|
104
|
+
return parsed;
|
|
105
|
+
}, [text, enabled]);
|
|
106
|
+
|
|
107
|
+
// Find cursor segment (memoized by cursor position and segments)
|
|
108
|
+
const cursorSegment = useMemo(() => {
|
|
109
|
+
if (cursorPosition === undefined || !result.hasHighlights) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return findSegmentAtCursor(result.segments, cursorPosition);
|
|
113
|
+
}, [result.segments, cursorPosition, result.hasHighlights]);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
result,
|
|
117
|
+
hasHighlights: result.hasHighlights,
|
|
118
|
+
cursorSegment,
|
|
119
|
+
isHighlighting: enabled && result.hasHighlights,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// Utility Hooks
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Hook for multiline input highlighting.
|
|
129
|
+
* Parses each line separately for better performance with large inputs.
|
|
130
|
+
*
|
|
131
|
+
* @param lines - Array of line strings
|
|
132
|
+
* @param enabled - Whether highlighting is enabled
|
|
133
|
+
* @returns Array of highlight results, one per line
|
|
134
|
+
*/
|
|
135
|
+
export function useMultilineHighlight(
|
|
136
|
+
lines: readonly string[],
|
|
137
|
+
enabled = true
|
|
138
|
+
): readonly HighlightResult[] {
|
|
139
|
+
return useMemo(() => {
|
|
140
|
+
if (!enabled) {
|
|
141
|
+
return lines.map(() => ({ segments: [], hasHighlights: false }));
|
|
142
|
+
}
|
|
143
|
+
return lines.map((line) => parseHighlights(line));
|
|
144
|
+
}, [lines, enabled]);
|
|
145
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInputHistory Hook (T012)
|
|
3
|
+
*
|
|
4
|
+
* React hook for managing input history with navigation.
|
|
5
|
+
* Supports navigating through previous entries and optional persistence.
|
|
6
|
+
*
|
|
7
|
+
* @module @vellum/cli
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useRef, useState } from "react";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for the useInputHistory hook.
|
|
14
|
+
*/
|
|
15
|
+
export interface UseInputHistoryOptions {
|
|
16
|
+
/** Maximum number of history entries to keep (default: 100) */
|
|
17
|
+
maxItems?: number;
|
|
18
|
+
/** localStorage key for persistence (if provided, history will be persisted) */
|
|
19
|
+
persistKey?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Return value of useInputHistory hook.
|
|
24
|
+
*/
|
|
25
|
+
export interface UseInputHistoryReturn {
|
|
26
|
+
/** Read-only array of history entries (newest last) */
|
|
27
|
+
history: readonly string[];
|
|
28
|
+
/** Current navigation index (-1 when not navigating) */
|
|
29
|
+
currentIndex: number;
|
|
30
|
+
/** Add a new entry to history */
|
|
31
|
+
addToHistory: (entry: string) => void;
|
|
32
|
+
/** Navigate up (older) or down (newer) through history */
|
|
33
|
+
navigateHistory: (direction: "up" | "down") => string | null;
|
|
34
|
+
/** Clear all history entries */
|
|
35
|
+
clearHistory: () => void;
|
|
36
|
+
/** Get the current entry at navigation index */
|
|
37
|
+
getCurrentEntry: () => string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load history from storage.
|
|
42
|
+
*/
|
|
43
|
+
function loadFromStorage(key: string): string[] {
|
|
44
|
+
try {
|
|
45
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
46
|
+
const stored = globalThis.localStorage.getItem(key);
|
|
47
|
+
if (stored) {
|
|
48
|
+
const parsed = JSON.parse(stored);
|
|
49
|
+
if (Array.isArray(parsed)) {
|
|
50
|
+
return parsed.filter((item): item is string => typeof item === "string");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore storage errors
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Save history to storage.
|
|
62
|
+
*/
|
|
63
|
+
function saveToStorage(key: string, history: string[]): void {
|
|
64
|
+
try {
|
|
65
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
66
|
+
globalThis.localStorage.setItem(key, JSON.stringify(history));
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore storage errors
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Hook for managing input history with navigation.
|
|
75
|
+
*
|
|
76
|
+
* Provides functionality for storing command history, navigating through
|
|
77
|
+
* previous entries with up/down arrows, and optional persistence.
|
|
78
|
+
*
|
|
79
|
+
* @param options - Configuration options
|
|
80
|
+
* @returns History state and manipulation functions
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* function InputComponent() {
|
|
85
|
+
* const [input, setInput] = useState('');
|
|
86
|
+
* const { navigateHistory, addToHistory } = useInputHistory({ maxItems: 50 });
|
|
87
|
+
*
|
|
88
|
+
* const handleKeyDown = (key: string) => {
|
|
89
|
+
* if (key === 'up') {
|
|
90
|
+
* const prev = navigateHistory('up');
|
|
91
|
+
* if (prev !== null) setInput(prev);
|
|
92
|
+
* } else if (key === 'down') {
|
|
93
|
+
* const next = navigateHistory('down');
|
|
94
|
+
* if (next !== null) setInput(next);
|
|
95
|
+
* }
|
|
96
|
+
* };
|
|
97
|
+
*
|
|
98
|
+
* const handleSubmit = () => {
|
|
99
|
+
* if (input.trim()) {
|
|
100
|
+
* addToHistory(input);
|
|
101
|
+
* // process input...
|
|
102
|
+
* setInput('');
|
|
103
|
+
* }
|
|
104
|
+
* };
|
|
105
|
+
*
|
|
106
|
+
* return <TextInput value={input} onSubmit={handleSubmit} />;
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function useInputHistory(options: UseInputHistoryOptions = {}): UseInputHistoryReturn {
|
|
111
|
+
const { maxItems = 100, persistKey } = options;
|
|
112
|
+
|
|
113
|
+
// Initialize history from storage if persistKey provided
|
|
114
|
+
const [history, setHistory] = useState<string[]>(() => {
|
|
115
|
+
if (persistKey) {
|
|
116
|
+
return loadFromStorage(persistKey);
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Current navigation index: -1 means not navigating (at the "new entry" position)
|
|
122
|
+
const [currentIndex, setCurrentIndex] = useState(-1);
|
|
123
|
+
|
|
124
|
+
// Ref to track the temp entry when user starts navigating
|
|
125
|
+
const tempEntryRef = useRef<string>("");
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Add a new entry to history.
|
|
129
|
+
* Skips if entry is empty or same as the last entry (no consecutive duplicates).
|
|
130
|
+
* Resets navigation index.
|
|
131
|
+
*/
|
|
132
|
+
const addToHistory = useCallback(
|
|
133
|
+
(entry: string) => {
|
|
134
|
+
const trimmed = entry.trim();
|
|
135
|
+
if (!trimmed) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setHistory((prev) => {
|
|
140
|
+
// Skip if same as last entry (no consecutive duplicates)
|
|
141
|
+
if (prev.length > 0 && prev[prev.length - 1] === trimmed) {
|
|
142
|
+
return prev;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const newHistory = [...prev, trimmed];
|
|
146
|
+
|
|
147
|
+
// Trim to maxItems if exceeded
|
|
148
|
+
const trimmedHistory =
|
|
149
|
+
newHistory.length > maxItems ? newHistory.slice(-maxItems) : newHistory;
|
|
150
|
+
|
|
151
|
+
// Persist if key provided
|
|
152
|
+
if (persistKey) {
|
|
153
|
+
saveToStorage(persistKey, trimmedHistory);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return trimmedHistory;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Reset navigation index
|
|
160
|
+
setCurrentIndex(-1);
|
|
161
|
+
tempEntryRef.current = "";
|
|
162
|
+
},
|
|
163
|
+
[maxItems, persistKey]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Navigate through history.
|
|
168
|
+
* 'up' moves to older entries, 'down' moves to newer entries.
|
|
169
|
+
* Returns the entry at the new position, or null if at boundary.
|
|
170
|
+
*/
|
|
171
|
+
const navigateHistory = useCallback(
|
|
172
|
+
(direction: "up" | "down"): string | null => {
|
|
173
|
+
if (history.length === 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let newIndex: number;
|
|
178
|
+
|
|
179
|
+
if (direction === "up") {
|
|
180
|
+
// Moving to older entries
|
|
181
|
+
if (currentIndex === -1) {
|
|
182
|
+
// Start navigating from the most recent entry
|
|
183
|
+
newIndex = history.length - 1;
|
|
184
|
+
} else if (currentIndex > 0) {
|
|
185
|
+
// Move to older entry
|
|
186
|
+
newIndex = currentIndex - 1;
|
|
187
|
+
} else {
|
|
188
|
+
// Already at the oldest entry
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// direction === 'down'
|
|
193
|
+
// Moving to newer entries
|
|
194
|
+
if (currentIndex === -1) {
|
|
195
|
+
// Not navigating, nothing to do
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (currentIndex < history.length - 1) {
|
|
199
|
+
// Move to newer entry
|
|
200
|
+
newIndex = currentIndex + 1;
|
|
201
|
+
} else {
|
|
202
|
+
// At the newest entry, return to "new entry" position
|
|
203
|
+
setCurrentIndex(-1);
|
|
204
|
+
return tempEntryRef.current || null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setCurrentIndex(newIndex);
|
|
209
|
+
return history[newIndex] ?? null;
|
|
210
|
+
},
|
|
211
|
+
[history, currentIndex]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Clear all history entries.
|
|
216
|
+
*/
|
|
217
|
+
const clearHistory = useCallback(() => {
|
|
218
|
+
setHistory([]);
|
|
219
|
+
setCurrentIndex(-1);
|
|
220
|
+
tempEntryRef.current = "";
|
|
221
|
+
|
|
222
|
+
if (persistKey) {
|
|
223
|
+
saveToStorage(persistKey, []);
|
|
224
|
+
}
|
|
225
|
+
}, [persistKey]);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the current entry at navigation index.
|
|
229
|
+
* Returns null if not navigating or history is empty.
|
|
230
|
+
*/
|
|
231
|
+
const getCurrentEntry = useCallback((): string | null => {
|
|
232
|
+
if (currentIndex === -1 || history.length === 0) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
return history[currentIndex] ?? null;
|
|
236
|
+
}, [history, currentIndex]);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
history,
|
|
240
|
+
currentIndex,
|
|
241
|
+
addToHistory,
|
|
242
|
+
navigateHistory,
|
|
243
|
+
clearHistory,
|
|
244
|
+
getCurrentEntry,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard Scroll Hook
|
|
3
|
+
*
|
|
4
|
+
* Handles keyboard events for scrolling in the TUI.
|
|
5
|
+
* Integrates with useScrollController to provide intuitive keyboard navigation.
|
|
6
|
+
*
|
|
7
|
+
* Supported keys:
|
|
8
|
+
* - PageUp / Ctrl+U: Scroll up half viewport
|
|
9
|
+
* - PageDown / Ctrl+D: Scroll down half viewport
|
|
10
|
+
* - Home / Ctrl+Home: Jump to top
|
|
11
|
+
* - End / Ctrl+End: Jump to bottom (follow mode)
|
|
12
|
+
* - ↑ / k: Scroll up one line
|
|
13
|
+
* - ↓ / j: Scroll down one line
|
|
14
|
+
*
|
|
15
|
+
* @module tui/hooks/useKeyboardScroll
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Key } from "ink";
|
|
19
|
+
import { useInput } from "ink";
|
|
20
|
+
import { useMemo } from "react";
|
|
21
|
+
import { isEndKey, isHomeKey } from "../types/ink-extended.js";
|
|
22
|
+
import type { ViewportScrollActions, ViewportScrollState } from "./useScrollController.js";
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for useKeyboardScroll hook
|
|
30
|
+
*/
|
|
31
|
+
export interface UseKeyboardScrollOptions {
|
|
32
|
+
/** Current scroll state from useScrollController */
|
|
33
|
+
readonly state: ViewportScrollState;
|
|
34
|
+
/** Scroll actions from useScrollController */
|
|
35
|
+
readonly actions: ViewportScrollActions;
|
|
36
|
+
/** Whether keyboard handling is enabled (default: true) */
|
|
37
|
+
readonly enabled?: boolean;
|
|
38
|
+
/** Enable vim-style keys (j/k for up/down) (default: true) */
|
|
39
|
+
readonly vimKeys?: boolean;
|
|
40
|
+
/** Custom lines per single step (default: 1) */
|
|
41
|
+
readonly stepLines?: number;
|
|
42
|
+
/** Custom lines per half-page (default: viewportHeight / 2) */
|
|
43
|
+
readonly halfPageLines?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return value of useKeyboardScroll hook
|
|
48
|
+
*/
|
|
49
|
+
export interface UseKeyboardScrollReturn {
|
|
50
|
+
/** Manual key handler for custom input handling */
|
|
51
|
+
readonly handleKey: (input: string, key: Key) => boolean;
|
|
52
|
+
/** Shortcut definitions for help display */
|
|
53
|
+
readonly shortcuts: ReadonlyArray<KeyboardScrollShortcut>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shortcut definition for display
|
|
58
|
+
*/
|
|
59
|
+
export interface KeyboardScrollShortcut {
|
|
60
|
+
/** Key combination (e.g., "PageUp", "Ctrl+U") */
|
|
61
|
+
readonly key: string;
|
|
62
|
+
/** Description of the action */
|
|
63
|
+
readonly description: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Constants
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Standard shortcuts for help display
|
|
72
|
+
*/
|
|
73
|
+
const SCROLL_SHORTCUTS: ReadonlyArray<KeyboardScrollShortcut> = [
|
|
74
|
+
{ key: "↑/k", description: "Scroll up one line" },
|
|
75
|
+
{ key: "↓/j", description: "Scroll down one line" },
|
|
76
|
+
{ key: "PageUp/Ctrl+U", description: "Scroll up half page" },
|
|
77
|
+
{ key: "PageDown/Ctrl+D", description: "Scroll down half page" },
|
|
78
|
+
{ key: "Home", description: "Jump to top" },
|
|
79
|
+
{ key: "End", description: "Jump to bottom (follow mode)" },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Hook
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* useKeyboardScroll - Handles keyboard events for scrolling
|
|
88
|
+
*
|
|
89
|
+
* Automatically registers keyboard handlers with useInput when enabled.
|
|
90
|
+
* Also provides a manual handleKey function for custom input handling.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* const [scrollState, scrollActions] = useScrollController({ viewportHeight: 20 });
|
|
95
|
+
*
|
|
96
|
+
* // Automatic keyboard handling
|
|
97
|
+
* useKeyboardScroll({
|
|
98
|
+
* state: scrollState,
|
|
99
|
+
* actions: scrollActions,
|
|
100
|
+
* enabled: isFocused,
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* // Or manual handling in existing useInput
|
|
104
|
+
* const { handleKey } = useKeyboardScroll({
|
|
105
|
+
* state: scrollState,
|
|
106
|
+
* actions: scrollActions,
|
|
107
|
+
* enabled: false, // Don't auto-register
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* useInput((input, key) => {
|
|
111
|
+
* if (handleKey(input, key)) return; // Handled by scroll
|
|
112
|
+
* // ... other handlers
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @param options - Configuration options
|
|
117
|
+
* @returns Scroll handling utilities
|
|
118
|
+
*/
|
|
119
|
+
export function useKeyboardScroll(options: UseKeyboardScrollOptions): UseKeyboardScrollReturn {
|
|
120
|
+
const { state, actions, enabled = true, vimKeys = true, stepLines = 1, halfPageLines } = options;
|
|
121
|
+
|
|
122
|
+
// Calculate half-page lines (default to half viewport)
|
|
123
|
+
const computedHalfPage = halfPageLines ?? Math.max(1, Math.floor(state.viewportHeight / 2));
|
|
124
|
+
|
|
125
|
+
// Maximum offset for jump to top
|
|
126
|
+
const maxOffset = Math.max(0, state.totalHeight - state.viewportHeight);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Handle a keyboard input, returning true if handled
|
|
130
|
+
*/
|
|
131
|
+
const handleKey = useMemo(() => {
|
|
132
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Keyboard handler needs many key combinations
|
|
133
|
+
return (input: string, key: Key): boolean => {
|
|
134
|
+
// Arrow up or vim 'k'
|
|
135
|
+
if (key.upArrow || (vimKeys && input === "k")) {
|
|
136
|
+
actions.scrollUp(stepLines);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Arrow down or vim 'j'
|
|
141
|
+
if (key.downArrow || (vimKeys && input === "j")) {
|
|
142
|
+
actions.scrollDown(stepLines);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// PageUp (Ink provides this as key.pageUp in some versions)
|
|
147
|
+
// Also handle Ctrl+U
|
|
148
|
+
if (key.pageUp || (key.ctrl && input === "u")) {
|
|
149
|
+
actions.scrollUp(computedHalfPage);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// PageDown
|
|
154
|
+
// Also handle Ctrl+D
|
|
155
|
+
if (key.pageDown || (key.ctrl && input === "d")) {
|
|
156
|
+
actions.scrollDown(computedHalfPage);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Home - jump to top
|
|
161
|
+
if (isHomeKey(input)) {
|
|
162
|
+
actions.jumpTo(maxOffset);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// End - jump to bottom (follow mode)
|
|
167
|
+
if (isEndKey(input)) {
|
|
168
|
+
actions.scrollToBottom();
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
}, [actions, vimKeys, stepLines, computedHalfPage, maxOffset]);
|
|
175
|
+
|
|
176
|
+
// Auto-register with useInput when enabled
|
|
177
|
+
useInput(
|
|
178
|
+
(input, key) => {
|
|
179
|
+
handleKey(input, key);
|
|
180
|
+
},
|
|
181
|
+
{ isActive: enabled }
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
handleKey,
|
|
186
|
+
shortcuts: SCROLL_SHORTCUTS,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Utility Functions
|
|
192
|
+
// =============================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Format shortcuts for display in help
|
|
196
|
+
*/
|
|
197
|
+
export function formatScrollShortcuts(): string {
|
|
198
|
+
return SCROLL_SHORTCUTS.map((s) => `${s.key}: ${s.description}`).join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get shortcuts without vim keys
|
|
203
|
+
*/
|
|
204
|
+
export function getScrollShortcutsNoVim(): ReadonlyArray<KeyboardScrollShortcut> {
|
|
205
|
+
return SCROLL_SHORTCUTS.map((shortcut) => ({
|
|
206
|
+
...shortcut,
|
|
207
|
+
key: shortcut.key.replace("/k", "").replace("/j", ""),
|
|
208
|
+
}));
|
|
209
|
+
}
|