@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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Context and State Management
|
|
3
|
+
*
|
|
4
|
+
* Provides global application state for the Vellum TUI including
|
|
5
|
+
* mode, loading state, error handling, and focus management.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/context/AppContext
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, {
|
|
11
|
+
createContext,
|
|
12
|
+
type Dispatch,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
useCallback,
|
|
15
|
+
useContext,
|
|
16
|
+
useMemo,
|
|
17
|
+
useReducer,
|
|
18
|
+
} from "react";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Application mode representing the current operational state
|
|
26
|
+
*/
|
|
27
|
+
export type AppMode = "idle" | "loading" | "streaming" | "waiting" | "error";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Focusable areas in the TUI
|
|
31
|
+
*/
|
|
32
|
+
export type FocusedArea = "input" | "messages" | "tools" | "status";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Application state interface
|
|
36
|
+
*/
|
|
37
|
+
export interface AppState {
|
|
38
|
+
/** Current application mode */
|
|
39
|
+
readonly mode: AppMode;
|
|
40
|
+
/** Whether the application is in a loading state */
|
|
41
|
+
readonly loading: boolean;
|
|
42
|
+
/** Current error, if any */
|
|
43
|
+
readonly error: Error | null;
|
|
44
|
+
/** Whether vim mode is enabled for input */
|
|
45
|
+
readonly vimMode: boolean;
|
|
46
|
+
/** Currently focused area of the UI */
|
|
47
|
+
readonly focusedArea: FocusedArea;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initial application state
|
|
52
|
+
*/
|
|
53
|
+
const initialState: AppState = {
|
|
54
|
+
mode: "idle",
|
|
55
|
+
loading: false,
|
|
56
|
+
error: null,
|
|
57
|
+
vimMode: false,
|
|
58
|
+
focusedArea: "input",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Actions (Discriminated Union)
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set the application mode
|
|
67
|
+
*/
|
|
68
|
+
export interface SetModeAction {
|
|
69
|
+
readonly type: "SET_MODE";
|
|
70
|
+
readonly mode: AppMode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set the loading state
|
|
75
|
+
*/
|
|
76
|
+
export interface SetLoadingAction {
|
|
77
|
+
readonly type: "SET_LOADING";
|
|
78
|
+
readonly loading: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set an error
|
|
83
|
+
*/
|
|
84
|
+
export interface SetErrorAction {
|
|
85
|
+
readonly type: "SET_ERROR";
|
|
86
|
+
readonly error: Error | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Toggle vim mode
|
|
91
|
+
*/
|
|
92
|
+
export interface ToggleVimModeAction {
|
|
93
|
+
readonly type: "TOGGLE_VIM_MODE";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set vim mode explicitly
|
|
98
|
+
*/
|
|
99
|
+
export interface SetVimModeAction {
|
|
100
|
+
readonly type: "SET_VIM_MODE";
|
|
101
|
+
readonly vimMode: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Set the focused area
|
|
106
|
+
*/
|
|
107
|
+
export interface SetFocusedAreaAction {
|
|
108
|
+
readonly type: "SET_FOCUSED_AREA";
|
|
109
|
+
readonly focusedArea: FocusedArea;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reset the application state to initial values
|
|
114
|
+
*/
|
|
115
|
+
export interface ResetAction {
|
|
116
|
+
readonly type: "RESET";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Discriminated union of all application actions
|
|
121
|
+
*/
|
|
122
|
+
export type AppAction =
|
|
123
|
+
| SetModeAction
|
|
124
|
+
| SetLoadingAction
|
|
125
|
+
| SetErrorAction
|
|
126
|
+
| ToggleVimModeAction
|
|
127
|
+
| SetVimModeAction
|
|
128
|
+
| SetFocusedAreaAction
|
|
129
|
+
| ResetAction;
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Reducer
|
|
133
|
+
// =============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Application state reducer
|
|
137
|
+
*
|
|
138
|
+
* @param state - Current application state
|
|
139
|
+
* @param action - Action to apply
|
|
140
|
+
* @returns New application state
|
|
141
|
+
*/
|
|
142
|
+
function appReducer(state: AppState, action: AppAction): AppState {
|
|
143
|
+
switch (action.type) {
|
|
144
|
+
case "SET_MODE":
|
|
145
|
+
return {
|
|
146
|
+
...state,
|
|
147
|
+
mode: action.mode,
|
|
148
|
+
// Automatically update loading based on mode
|
|
149
|
+
loading: action.mode === "loading" || action.mode === "streaming",
|
|
150
|
+
// Clear error when transitioning away from error state
|
|
151
|
+
error: action.mode === "error" ? state.error : null,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
case "SET_LOADING":
|
|
155
|
+
return {
|
|
156
|
+
...state,
|
|
157
|
+
loading: action.loading,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
case "SET_ERROR":
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
error: action.error,
|
|
164
|
+
mode: action.error !== null ? "error" : state.mode,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
case "TOGGLE_VIM_MODE":
|
|
168
|
+
return {
|
|
169
|
+
...state,
|
|
170
|
+
vimMode: !state.vimMode,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
case "SET_VIM_MODE":
|
|
174
|
+
return {
|
|
175
|
+
...state,
|
|
176
|
+
vimMode: action.vimMode,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
case "SET_FOCUSED_AREA":
|
|
180
|
+
return {
|
|
181
|
+
...state,
|
|
182
|
+
focusedArea: action.focusedArea,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
case "RESET":
|
|
186
|
+
return initialState;
|
|
187
|
+
|
|
188
|
+
default:
|
|
189
|
+
// Exhaustive check - TypeScript will error if a case is missing
|
|
190
|
+
return state;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// =============================================================================
|
|
195
|
+
// Context
|
|
196
|
+
// =============================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Context value interface
|
|
200
|
+
*/
|
|
201
|
+
export interface AppContextValue {
|
|
202
|
+
/** Current application state */
|
|
203
|
+
readonly state: AppState;
|
|
204
|
+
/** Dispatch function for state updates */
|
|
205
|
+
readonly dispatch: Dispatch<AppAction>;
|
|
206
|
+
/** Reset state to initial values */
|
|
207
|
+
readonly reset: () => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* React context for application state
|
|
212
|
+
*
|
|
213
|
+
* Initialized as undefined to detect usage outside provider
|
|
214
|
+
*/
|
|
215
|
+
const AppContext = createContext<AppContextValue | undefined>(undefined);
|
|
216
|
+
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Hook
|
|
219
|
+
// =============================================================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Hook to access the application state and dispatch
|
|
223
|
+
*
|
|
224
|
+
* Must be used within an AppProvider component.
|
|
225
|
+
*
|
|
226
|
+
* @returns The current app context value with state, dispatch, and reset
|
|
227
|
+
* @throws Error if used outside AppProvider
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```tsx
|
|
231
|
+
* function MyComponent() {
|
|
232
|
+
* const { state, dispatch, reset } = useApp();
|
|
233
|
+
*
|
|
234
|
+
* // Read state
|
|
235
|
+
* if (state.loading) {
|
|
236
|
+
* return <Text>Loading...</Text>;
|
|
237
|
+
* }
|
|
238
|
+
*
|
|
239
|
+
* // Dispatch actions
|
|
240
|
+
* const handleStart = () => {
|
|
241
|
+
* dispatch({ type: 'SET_MODE', mode: 'loading' });
|
|
242
|
+
* };
|
|
243
|
+
*
|
|
244
|
+
* // Reset to initial state
|
|
245
|
+
* const handleReset = () => reset();
|
|
246
|
+
*
|
|
247
|
+
* return <Box>...</Box>;
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export function useApp(): AppContextValue {
|
|
252
|
+
const context = useContext(AppContext);
|
|
253
|
+
|
|
254
|
+
if (context === undefined) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"useApp must be used within an AppProvider. " +
|
|
257
|
+
"Ensure your component is wrapped in <AppProvider>."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return context;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// =============================================================================
|
|
265
|
+
// Provider Props
|
|
266
|
+
// =============================================================================
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Props for the AppProvider component
|
|
270
|
+
*/
|
|
271
|
+
export interface AppProviderProps {
|
|
272
|
+
/**
|
|
273
|
+
* Initial state overrides
|
|
274
|
+
*
|
|
275
|
+
* Partial state that will be merged with the default initial state
|
|
276
|
+
*/
|
|
277
|
+
readonly initialState?: Partial<AppState>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Children to render within the app context
|
|
281
|
+
*/
|
|
282
|
+
readonly children: ReactNode;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// Provider Component
|
|
287
|
+
// =============================================================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Application state provider component
|
|
291
|
+
*
|
|
292
|
+
* Provides application state context to all child components, enabling
|
|
293
|
+
* access to the current state and dispatch via the useApp hook.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```tsx
|
|
297
|
+
* // Using default initial state
|
|
298
|
+
* <AppProvider>
|
|
299
|
+
* <App />
|
|
300
|
+
* </AppProvider>
|
|
301
|
+
*
|
|
302
|
+
* // Using custom initial state
|
|
303
|
+
* <AppProvider initialState={{ vimMode: true }}>
|
|
304
|
+
* <App />
|
|
305
|
+
* </AppProvider>
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
export function AppProvider({
|
|
309
|
+
initialState: initialStateOverrides,
|
|
310
|
+
children,
|
|
311
|
+
}: AppProviderProps): React.JSX.Element {
|
|
312
|
+
// State management with useReducer
|
|
313
|
+
// Initial state is computed once via lazy initializer
|
|
314
|
+
const [state, dispatch] = useReducer(
|
|
315
|
+
appReducer,
|
|
316
|
+
initialStateOverrides,
|
|
317
|
+
(overrides): AppState => ({
|
|
318
|
+
...initialState,
|
|
319
|
+
...overrides,
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reset state to initial values
|
|
325
|
+
*/
|
|
326
|
+
const reset = useCallback((): void => {
|
|
327
|
+
dispatch({ type: "RESET" });
|
|
328
|
+
}, []);
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Memoized context value
|
|
332
|
+
*/
|
|
333
|
+
const contextValue = useMemo<AppContextValue>(
|
|
334
|
+
() => ({
|
|
335
|
+
state,
|
|
336
|
+
dispatch,
|
|
337
|
+
reset,
|
|
338
|
+
}),
|
|
339
|
+
[state, reset]
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
return <AppContext value={contextValue}>{children}</AppContext>;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// =============================================================================
|
|
346
|
+
// Exports
|
|
347
|
+
// =============================================================================
|
|
348
|
+
|
|
349
|
+
export { AppContext, initialState };
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bracketed Paste Context
|
|
3
|
+
*
|
|
4
|
+
* React context for managing bracketed paste mode state and providing
|
|
5
|
+
* paste event callbacks to child components.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/context/BracketedPasteContext
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useStdin } from "ink";
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
13
|
+
import {
|
|
14
|
+
disableBracketedPaste,
|
|
15
|
+
enableBracketedPaste,
|
|
16
|
+
PASTE_END,
|
|
17
|
+
PASTE_START,
|
|
18
|
+
} from "../utils/bracketedPaste.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handler function for paste events
|
|
26
|
+
*/
|
|
27
|
+
export type PasteHandler = (content: string) => void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Context value for bracketed paste
|
|
31
|
+
*/
|
|
32
|
+
interface BracketedPasteContextValue {
|
|
33
|
+
/**
|
|
34
|
+
* Register a paste event handler
|
|
35
|
+
* @returns Unsubscribe function
|
|
36
|
+
*/
|
|
37
|
+
subscribe: (handler: PasteHandler) => () => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether a paste operation is currently in progress
|
|
41
|
+
*/
|
|
42
|
+
isPasting: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Constants
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/** Timeout for incomplete paste sequences (30 seconds) */
|
|
50
|
+
const PASTE_TIMEOUT_MS = 30_000;
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Context
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
const BracketedPasteContext = createContext<BracketedPasteContextValue | null>(null);
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Provider
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
interface BracketedPasteProviderProps {
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
/**
|
|
65
|
+
* Whether bracketed paste should be enabled
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
enabled?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Provider component that enables bracketed paste mode and dispatches
|
|
73
|
+
* paste events to subscribers.
|
|
74
|
+
*
|
|
75
|
+
* This provider intercepts stdin data events to detect paste sequences
|
|
76
|
+
* before Ink's useInput processes them. When a paste is detected, the
|
|
77
|
+
* content is buffered and dispatched as a single event to all subscribers.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* function App() {
|
|
82
|
+
* return (
|
|
83
|
+
* <BracketedPasteProvider>
|
|
84
|
+
* <MyComponents />
|
|
85
|
+
* </BracketedPasteProvider>
|
|
86
|
+
* );
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function BracketedPasteProvider({ children, enabled = true }: BracketedPasteProviderProps) {
|
|
91
|
+
const { stdin } = useStdin();
|
|
92
|
+
const [isPasting, setIsPasting] = useState(false);
|
|
93
|
+
|
|
94
|
+
// Subscribers for paste events
|
|
95
|
+
const subscribersRef = useRef<Set<PasteHandler>>(new Set());
|
|
96
|
+
|
|
97
|
+
// Buffer for accumulating paste content
|
|
98
|
+
const pasteBufferRef = useRef<string>("");
|
|
99
|
+
|
|
100
|
+
// Timeout handle for incomplete pastes
|
|
101
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
102
|
+
|
|
103
|
+
// Track paste state without triggering re-renders (for emit override)
|
|
104
|
+
const isPastingRef = useRef(false);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Subscribe to paste events
|
|
108
|
+
*/
|
|
109
|
+
const subscribe = useCallback((handler: PasteHandler) => {
|
|
110
|
+
subscribersRef.current.add(handler);
|
|
111
|
+
return () => {
|
|
112
|
+
subscribersRef.current.delete(handler);
|
|
113
|
+
};
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Dispatch paste content to all subscribers
|
|
118
|
+
*/
|
|
119
|
+
const dispatchPaste = useCallback((content: string) => {
|
|
120
|
+
if (content.length === 0) return;
|
|
121
|
+
|
|
122
|
+
for (const handler of subscribersRef.current) {
|
|
123
|
+
try {
|
|
124
|
+
handler(content);
|
|
125
|
+
} catch {
|
|
126
|
+
// Ignore handler errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Clear paste state
|
|
133
|
+
*/
|
|
134
|
+
const clearPasteState = useCallback(() => {
|
|
135
|
+
isPastingRef.current = false;
|
|
136
|
+
setIsPasting(false);
|
|
137
|
+
pasteBufferRef.current = "";
|
|
138
|
+
if (timeoutRef.current) {
|
|
139
|
+
clearTimeout(timeoutRef.current);
|
|
140
|
+
timeoutRef.current = null;
|
|
141
|
+
}
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle paste start sequence found in data
|
|
146
|
+
*/
|
|
147
|
+
const handlePasteStart = useCallback(
|
|
148
|
+
(afterStart: string) => {
|
|
149
|
+
isPastingRef.current = true;
|
|
150
|
+
setIsPasting(true);
|
|
151
|
+
pasteBufferRef.current = "";
|
|
152
|
+
|
|
153
|
+
// Set timeout for incomplete paste
|
|
154
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
155
|
+
timeoutRef.current = setTimeout(() => {
|
|
156
|
+
if (pasteBufferRef.current.length > 0) {
|
|
157
|
+
dispatchPaste(pasteBufferRef.current);
|
|
158
|
+
}
|
|
159
|
+
clearPasteState();
|
|
160
|
+
}, PASTE_TIMEOUT_MS);
|
|
161
|
+
|
|
162
|
+
// Check if paste end is in this chunk
|
|
163
|
+
const endIdx = afterStart.indexOf(PASTE_END);
|
|
164
|
+
if (endIdx !== -1) {
|
|
165
|
+
// Complete paste in single data event
|
|
166
|
+
dispatchPaste(afterStart.slice(0, endIdx));
|
|
167
|
+
clearPasteState();
|
|
168
|
+
} else {
|
|
169
|
+
// Partial paste - buffer content
|
|
170
|
+
pasteBufferRef.current = afterStart;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
[dispatchPaste, clearPasteState]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle data while paste is in progress
|
|
178
|
+
*/
|
|
179
|
+
const handlePasteInProgress = useCallback(
|
|
180
|
+
(str: string) => {
|
|
181
|
+
const endIdx = str.indexOf(PASTE_END);
|
|
182
|
+
if (endIdx !== -1) {
|
|
183
|
+
// Found end - complete the paste
|
|
184
|
+
dispatchPaste(pasteBufferRef.current + str.slice(0, endIdx));
|
|
185
|
+
clearPasteState();
|
|
186
|
+
} else {
|
|
187
|
+
// Still pasting - accumulate
|
|
188
|
+
pasteBufferRef.current += str;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
[dispatchPaste, clearPasteState]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Set up stdin listener for paste detection with emit override.
|
|
196
|
+
* This intercepts data events BEFORE they reach Ink, preventing
|
|
197
|
+
* paste sequences from triggering character-by-character rendering.
|
|
198
|
+
*
|
|
199
|
+
* In test environments (non-TTY), we fall back to prependListener which
|
|
200
|
+
* is less complete but doesn't interfere with test mocks.
|
|
201
|
+
*/
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!enabled || !stdin) return;
|
|
204
|
+
|
|
205
|
+
// Check if this is a real TTY (not a test mock)
|
|
206
|
+
// In tests, stdin is often a mock that doesn't support emit override well
|
|
207
|
+
const isRealTty = stdin.isTTY === true && typeof stdin.setRawMode === "function";
|
|
208
|
+
|
|
209
|
+
// Enable bracketed paste mode (only for real TTYs)
|
|
210
|
+
if (isRealTty) {
|
|
211
|
+
enableBracketedPaste();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (isRealTty) {
|
|
215
|
+
// Real TTY: Use emit override for complete blocking
|
|
216
|
+
const originalEmit = stdin.emit.bind(stdin);
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Override emit to intercept and block paste data.
|
|
220
|
+
* This ensures paste sequences never reach Ink's input handlers.
|
|
221
|
+
*/
|
|
222
|
+
// biome-ignore lint/suspicious/noExplicitAny: stdin.emit has complex overloaded signature
|
|
223
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Paste sequence state machine with multiple transitions
|
|
224
|
+
(stdin as any).emit = (event: string | symbol, ...args: unknown[]): boolean => {
|
|
225
|
+
// Only intercept 'data' events
|
|
226
|
+
if (event !== "data") {
|
|
227
|
+
return originalEmit(event, ...args);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const data = args[0];
|
|
231
|
+
const str = typeof data === "string" ? data : (data as Buffer).toString("utf8");
|
|
232
|
+
|
|
233
|
+
// Check for paste start
|
|
234
|
+
const startIdx = str.indexOf(PASTE_START);
|
|
235
|
+
if (startIdx !== -1 && !isPastingRef.current) {
|
|
236
|
+
// Found paste start - handle it and block from Ink
|
|
237
|
+
// Extract any data before the paste sequence (let it through)
|
|
238
|
+
const beforePaste = str.slice(0, startIdx);
|
|
239
|
+
if (beforePaste.length > 0) {
|
|
240
|
+
originalEmit("data", beforePaste);
|
|
241
|
+
}
|
|
242
|
+
// Process paste start (this sets isPastingRef.current = true)
|
|
243
|
+
handlePasteStart(str.slice(startIdx + PASTE_START.length));
|
|
244
|
+
// Block the paste data from reaching Ink
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// If we're in a paste, accumulate and block
|
|
249
|
+
if (isPastingRef.current) {
|
|
250
|
+
handlePasteInProgress(str);
|
|
251
|
+
// Block paste data from reaching Ink
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Normal data - pass through to Ink
|
|
256
|
+
return originalEmit(event, ...args);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Cleanup for real TTY
|
|
260
|
+
return () => {
|
|
261
|
+
// biome-ignore lint/suspicious/noExplicitAny: restoring original emit
|
|
262
|
+
(stdin as any).emit = originalEmit;
|
|
263
|
+
clearPasteState();
|
|
264
|
+
disableBracketedPaste();
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Test environment: Use prependListener (less complete but test-compatible)
|
|
269
|
+
const handleData = (data: Buffer | string) => {
|
|
270
|
+
const str = typeof data === "string" ? data : data.toString("utf8");
|
|
271
|
+
|
|
272
|
+
// Check for paste start
|
|
273
|
+
const startIdx = str.indexOf(PASTE_START);
|
|
274
|
+
if (startIdx !== -1 && !isPastingRef.current) {
|
|
275
|
+
handlePasteStart(str.slice(startIdx + PASTE_START.length));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// If we're in a paste, look for end or accumulate
|
|
280
|
+
if (isPastingRef.current) {
|
|
281
|
+
handlePasteInProgress(str);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
stdin.prependListener("data", handleData);
|
|
286
|
+
|
|
287
|
+
return () => {
|
|
288
|
+
stdin.removeListener("data", handleData);
|
|
289
|
+
clearPasteState();
|
|
290
|
+
};
|
|
291
|
+
}, [enabled, stdin, handlePasteStart, handlePasteInProgress, clearPasteState]);
|
|
292
|
+
|
|
293
|
+
// Handle process exit signals
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!enabled) return;
|
|
296
|
+
|
|
297
|
+
const cleanup = () => {
|
|
298
|
+
disableBracketedPaste();
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
process.on("SIGINT", cleanup);
|
|
302
|
+
process.on("SIGTERM", cleanup);
|
|
303
|
+
process.on("exit", cleanup);
|
|
304
|
+
|
|
305
|
+
return () => {
|
|
306
|
+
process.off("SIGINT", cleanup);
|
|
307
|
+
process.off("SIGTERM", cleanup);
|
|
308
|
+
process.off("exit", cleanup);
|
|
309
|
+
};
|
|
310
|
+
}, [enabled]);
|
|
311
|
+
|
|
312
|
+
const value: BracketedPasteContextValue = {
|
|
313
|
+
subscribe,
|
|
314
|
+
isPasting,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return <BracketedPasteContext.Provider value={value}>{children}</BracketedPasteContext.Provider>;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// =============================================================================
|
|
321
|
+
// Hook
|
|
322
|
+
// =============================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Hook to access the bracketed paste context.
|
|
326
|
+
*
|
|
327
|
+
* @throws Error if used outside of BracketedPasteProvider
|
|
328
|
+
*/
|
|
329
|
+
export function useBracketedPasteContext(): BracketedPasteContextValue {
|
|
330
|
+
const context = useContext(BracketedPasteContext);
|
|
331
|
+
if (!context) {
|
|
332
|
+
throw new Error("useBracketedPasteContext must be used within BracketedPasteProvider");
|
|
333
|
+
}
|
|
334
|
+
return context;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Hook to subscribe to paste events.
|
|
339
|
+
*
|
|
340
|
+
* The provided callback will be called whenever a paste operation completes.
|
|
341
|
+
* The callback receives the full pasted content as a single string.
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```tsx
|
|
345
|
+
* function MyInput({ value, onChange }) {
|
|
346
|
+
* // Handle paste events
|
|
347
|
+
* usePasteHandler((pastedText) => {
|
|
348
|
+
* onChange(value + pastedText);
|
|
349
|
+
* });
|
|
350
|
+
*
|
|
351
|
+
* return <Text>{value}</Text>;
|
|
352
|
+
* }
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
export function usePasteHandler(handler: PasteHandler): void {
|
|
356
|
+
const context = useContext(BracketedPasteContext);
|
|
357
|
+
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (!context) return;
|
|
360
|
+
return context.subscribe(handler);
|
|
361
|
+
}, [context, handler]);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Hook to check if a paste operation is in progress.
|
|
366
|
+
*
|
|
367
|
+
* @returns true if paste is in progress, false otherwise, or undefined if outside provider
|
|
368
|
+
*/
|
|
369
|
+
export function useIsPasting(): boolean {
|
|
370
|
+
const context = useContext(BracketedPasteContext);
|
|
371
|
+
return context?.isPasting ?? false;
|
|
372
|
+
}
|