@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,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for textSanitizer utility
|
|
3
|
+
*
|
|
4
|
+
* @module tui/utils/__tests__/textSanitizer.test
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { sanitize, sanitizeAnsi, sanitizeText } from "../textSanitizer.js";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// sanitizeText Tests
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
describe("sanitizeText", () => {
|
|
15
|
+
describe("line ending normalization", () => {
|
|
16
|
+
it("should convert CRLF to LF", () => {
|
|
17
|
+
expect(sanitizeText("Hello\r\nWorld")).toBe("Hello\nWorld");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should convert lone CR to LF", () => {
|
|
21
|
+
expect(sanitizeText("Hello\rWorld")).toBe("Hello\nWorld");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should convert mixed line endings to LF", () => {
|
|
25
|
+
expect(sanitizeText("A\r\nB\rC\nD")).toBe("A\nB\nC\nD");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should convert Unicode line separator (U+2028) to LF", () => {
|
|
29
|
+
expect(sanitizeText("Hello\u2028World")).toBe("Hello\nWorld");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should convert Unicode paragraph separator (U+2029) to LF", () => {
|
|
33
|
+
expect(sanitizeText("Hello\u2029World")).toBe("Hello\nWorld");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("control character removal", () => {
|
|
38
|
+
it("should remove NUL character", () => {
|
|
39
|
+
expect(sanitizeText("Hello\x00World")).toBe("HelloWorld");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should remove BEL character", () => {
|
|
43
|
+
expect(sanitizeText("Hello\x07World")).toBe("HelloWorld");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should remove backspace character", () => {
|
|
47
|
+
expect(sanitizeText("Hello\x08World")).toBe("HelloWorld");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should remove vertical tab", () => {
|
|
51
|
+
expect(sanitizeText("Hello\x0BWorld")).toBe("HelloWorld");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should remove form feed", () => {
|
|
55
|
+
expect(sanitizeText("Hello\x0CWorld")).toBe("HelloWorld");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should preserve escape character (for ANSI compatibility)", () => {
|
|
59
|
+
// sanitizeText preserves ESC for ANSI sequence support
|
|
60
|
+
// Use sanitize() to strip orphan ESC characters
|
|
61
|
+
expect(sanitizeText("Hello\x1BWorld")).toBe("Hello\x1BWorld");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should remove DEL character", () => {
|
|
65
|
+
expect(sanitizeText("Hello\x7FWorld")).toBe("HelloWorld");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should remove C1 control characters (0x80-0x9F)", () => {
|
|
69
|
+
expect(sanitizeText("Hello\x80\x8F\x9FWorld")).toBe("HelloWorld");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should preserve newline (LF)", () => {
|
|
73
|
+
expect(sanitizeText("Hello\nWorld")).toBe("Hello\nWorld");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should preserve tab (before conversion)", () => {
|
|
77
|
+
// Tab is preserved as control char, then converted to spaces
|
|
78
|
+
expect(sanitizeText("Hello\tWorld")).toBe("Hello World");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("tab conversion", () => {
|
|
83
|
+
it("should convert tab to 2 spaces by default", () => {
|
|
84
|
+
expect(sanitizeText("A\tB")).toBe("A B");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should convert tab to custom width", () => {
|
|
88
|
+
expect(sanitizeText("A\tB", { tabWidth: 4 })).toBe("A B");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should convert multiple tabs", () => {
|
|
92
|
+
expect(sanitizeText("A\t\tB")).toBe("A B");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should handle tab width of 0 (no conversion)", () => {
|
|
96
|
+
expect(sanitizeText("A\tB", { tabWidth: 0 })).toBe("A\tB");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("combined scenarios", () => {
|
|
101
|
+
it("should handle complex input", () => {
|
|
102
|
+
const input = "Hello\r\n\x00World\t!\x07";
|
|
103
|
+
expect(sanitizeText(input)).toBe("Hello\nWorld !");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should handle empty string", () => {
|
|
107
|
+
expect(sanitizeText("")).toBe("");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should handle string with only control characters", () => {
|
|
111
|
+
// ESC is preserved for ANSI compatibility; only combined sanitize() strips orphan ESC
|
|
112
|
+
expect(sanitizeText("\x00\x07\x1B")).toBe("\x1B");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// sanitizeAnsi Tests
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
describe("sanitizeAnsi", () => {
|
|
122
|
+
describe("safe SGR sequences (colors/styles)", () => {
|
|
123
|
+
it("should preserve basic color codes", () => {
|
|
124
|
+
expect(sanitizeAnsi("\x1b[31mRed\x1b[0m")).toBe("\x1b[31mRed\x1b[0m");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should preserve bright color codes", () => {
|
|
128
|
+
expect(sanitizeAnsi("\x1b[91mBright Red\x1b[0m")).toBe("\x1b[91mBright Red\x1b[0m");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should preserve 256-color codes", () => {
|
|
132
|
+
expect(sanitizeAnsi("\x1b[38;5;196mColor\x1b[0m")).toBe("\x1b[38;5;196mColor\x1b[0m");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should preserve RGB color codes", () => {
|
|
136
|
+
expect(sanitizeAnsi("\x1b[38;2;255;0;0mRGB\x1b[0m")).toBe("\x1b[38;2;255;0;0mRGB\x1b[0m");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should preserve text style codes", () => {
|
|
140
|
+
expect(sanitizeAnsi("\x1b[1mBold\x1b[0m")).toBe("\x1b[1mBold\x1b[0m");
|
|
141
|
+
expect(sanitizeAnsi("\x1b[3mItalic\x1b[0m")).toBe("\x1b[3mItalic\x1b[0m");
|
|
142
|
+
expect(sanitizeAnsi("\x1b[4mUnderline\x1b[0m")).toBe("\x1b[4mUnderline\x1b[0m");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should preserve combined style codes", () => {
|
|
146
|
+
expect(sanitizeAnsi("\x1b[1;31;4mStyled\x1b[0m")).toBe("\x1b[1;31;4mStyled\x1b[0m");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("dangerous CSI sequences removal", () => {
|
|
151
|
+
it("should remove cursor position (CUP)", () => {
|
|
152
|
+
expect(sanitizeAnsi("\x1b[10;20HText")).toBe("Text");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should remove cursor up (CUU)", () => {
|
|
156
|
+
expect(sanitizeAnsi("\x1b[5AText")).toBe("Text");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should remove cursor down (CUD)", () => {
|
|
160
|
+
expect(sanitizeAnsi("\x1b[5BText")).toBe("Text");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should remove cursor forward (CUF)", () => {
|
|
164
|
+
expect(sanitizeAnsi("\x1b[5CText")).toBe("Text");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should remove cursor back (CUB)", () => {
|
|
168
|
+
expect(sanitizeAnsi("\x1b[5DText")).toBe("Text");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should remove erase display (ED)", () => {
|
|
172
|
+
expect(sanitizeAnsi("\x1b[2JText")).toBe("Text");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should remove erase line (EL)", () => {
|
|
176
|
+
expect(sanitizeAnsi("\x1b[KText")).toBe("Text");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should remove scroll up (SU)", () => {
|
|
180
|
+
expect(sanitizeAnsi("\x1b[5SText")).toBe("Text");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should remove scroll down (SD)", () => {
|
|
184
|
+
expect(sanitizeAnsi("\x1b[5TText")).toBe("Text");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should remove save cursor position", () => {
|
|
188
|
+
expect(sanitizeAnsi("\x1b[sText\x1b[u")).toBe("Text");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should remove private mode sequences", () => {
|
|
192
|
+
expect(sanitizeAnsi("\x1b[?25lText\x1b[?25h")).toBe("Text");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("OSC sequences removal", () => {
|
|
197
|
+
it("should remove terminal title sequence (BEL terminated)", () => {
|
|
198
|
+
expect(sanitizeAnsi("\x1b]0;Title\x07Text")).toBe("Text");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should remove terminal title sequence (ST terminated)", () => {
|
|
202
|
+
expect(sanitizeAnsi("\x1b]0;Title\x1b\\Text")).toBe("Text");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should remove hyperlink sequences", () => {
|
|
206
|
+
expect(sanitizeAnsi("\x1b]8;;https://example.com\x07Link\x1b]8;;\x07")).toBe("Link");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("stripAllAnsi option", () => {
|
|
211
|
+
it("should strip all ANSI codes when enabled", () => {
|
|
212
|
+
const input = "\x1b[31mRed\x1b[0m \x1b[2JCleared";
|
|
213
|
+
expect(sanitizeAnsi(input, { stripAllAnsi: true })).toBe("Red Cleared");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should strip colors and styles when enabled", () => {
|
|
217
|
+
expect(sanitizeAnsi("\x1b[1;31;4mStyled\x1b[0m", { stripAllAnsi: true })).toBe("Styled");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("mixed content", () => {
|
|
222
|
+
it("should handle colors mixed with dangerous sequences", () => {
|
|
223
|
+
const input = "\x1b[2J\x1b[31mRed\x1b[0m\x1b[H";
|
|
224
|
+
expect(sanitizeAnsi(input)).toBe("\x1b[31mRed\x1b[0m");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should handle empty string", () => {
|
|
228
|
+
expect(sanitizeAnsi("")).toBe("");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle text without ANSI", () => {
|
|
232
|
+
expect(sanitizeAnsi("Plain text")).toBe("Plain text");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// =============================================================================
|
|
238
|
+
// sanitize (combined) Tests
|
|
239
|
+
// =============================================================================
|
|
240
|
+
|
|
241
|
+
describe("sanitize (combined)", () => {
|
|
242
|
+
it("should apply both text and ANSI sanitization", () => {
|
|
243
|
+
const input = "Hello\r\nWorld\x1b[2J\x1b[31mRed\x1b[0m\t!";
|
|
244
|
+
const result = sanitize(input);
|
|
245
|
+
expect(result).toBe("Hello\nWorld\x1b[31mRed\x1b[0m !");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should handle complex LLM output with ANSI codes", () => {
|
|
249
|
+
const input = "\x1b[1mTitle\x1b[0m\r\n\x1b[33mWarning\x1b[0m: Something\thappened";
|
|
250
|
+
const result = sanitize(input);
|
|
251
|
+
expect(result).toBe("\x1b[1mTitle\x1b[0m\n\x1b[33mWarning\x1b[0m: Something happened");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should strip all with combined options", () => {
|
|
255
|
+
const input = "Hello\r\n\x1b[31mRed\x1b[0m\t!";
|
|
256
|
+
const result = sanitize(input, { stripAllAnsi: true, tabWidth: 4 });
|
|
257
|
+
expect(result).toBe("Hello\nRed !");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should handle malicious input safely", () => {
|
|
261
|
+
// Attempt to clear screen and move cursor
|
|
262
|
+
const malicious = "\x1b[2J\x1b[H\x1b[31mTrick\x1b[0m\x1b[1000;1000H";
|
|
263
|
+
const result = sanitize(malicious);
|
|
264
|
+
expect(result).toBe("\x1b[31mTrick\x1b[0m");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for UI Sizing Utilities
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
clamp,
|
|
7
|
+
DEFAULT_TERMINAL_HEIGHT,
|
|
8
|
+
DEFAULT_TERMINAL_WIDTH,
|
|
9
|
+
getContentPadding,
|
|
10
|
+
getMaxContentWidth,
|
|
11
|
+
getTerminalHeight,
|
|
12
|
+
getTerminalSize,
|
|
13
|
+
getTerminalWidth,
|
|
14
|
+
lerp,
|
|
15
|
+
NARROW_CONTENT_RATIO,
|
|
16
|
+
NARROW_WIDTH_BREAKPOINT,
|
|
17
|
+
WIDE_CONTENT_RATIO,
|
|
18
|
+
WIDE_WIDTH_BREAKPOINT,
|
|
19
|
+
} from "../ui-sizing.js";
|
|
20
|
+
|
|
21
|
+
describe("ui-sizing", () => {
|
|
22
|
+
describe("lerp", () => {
|
|
23
|
+
it("returns min when t=0", () => {
|
|
24
|
+
expect(lerp(0, 100, 0)).toBe(0);
|
|
25
|
+
expect(lerp(10, 20, 0)).toBe(10);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns max when t=1", () => {
|
|
29
|
+
expect(lerp(0, 100, 1)).toBe(100);
|
|
30
|
+
expect(lerp(10, 20, 1)).toBe(20);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("interpolates correctly at t=0.5", () => {
|
|
34
|
+
expect(lerp(0, 100, 0.5)).toBe(50);
|
|
35
|
+
expect(lerp(10, 20, 0.5)).toBe(15);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("interpolates correctly at t=0.25", () => {
|
|
39
|
+
expect(lerp(0, 100, 0.25)).toBe(25);
|
|
40
|
+
expect(lerp(10, 20, 0.25)).toBe(12.5);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles negative values", () => {
|
|
44
|
+
expect(lerp(-100, 100, 0.5)).toBe(0);
|
|
45
|
+
expect(lerp(-50, -10, 0.5)).toBe(-30);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("clamp", () => {
|
|
50
|
+
it("returns value when within range", () => {
|
|
51
|
+
expect(clamp(50, 0, 100)).toBe(50);
|
|
52
|
+
expect(clamp(0, 0, 100)).toBe(0);
|
|
53
|
+
expect(clamp(100, 0, 100)).toBe(100);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("clamps to min when value is below range", () => {
|
|
57
|
+
expect(clamp(-10, 0, 100)).toBe(0);
|
|
58
|
+
expect(clamp(-100, -50, 50)).toBe(-50);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("clamps to max when value is above range", () => {
|
|
62
|
+
expect(clamp(150, 0, 100)).toBe(100);
|
|
63
|
+
expect(clamp(200, 0, 100)).toBe(100);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles zero range", () => {
|
|
67
|
+
expect(clamp(50, 10, 10)).toBe(10);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("getTerminalWidth", () => {
|
|
72
|
+
it("returns default when stdout has no columns", () => {
|
|
73
|
+
expect(getTerminalWidth(80)).toBe(process.stdout.columns ?? 80);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("uses custom fallback", () => {
|
|
77
|
+
// When stdout.columns is undefined, uses fallback
|
|
78
|
+
const fallback = 120;
|
|
79
|
+
const result = getTerminalWidth(fallback);
|
|
80
|
+
expect(result).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("getTerminalHeight", () => {
|
|
85
|
+
it("returns default when stdout has no rows", () => {
|
|
86
|
+
expect(getTerminalHeight(24)).toBe(process.stdout.rows ?? 24);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("uses custom fallback", () => {
|
|
90
|
+
const fallback = 50;
|
|
91
|
+
const result = getTerminalHeight(fallback);
|
|
92
|
+
expect(result).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("getMaxContentWidth", () => {
|
|
97
|
+
it("uses narrow ratio for widths <= 80", () => {
|
|
98
|
+
expect(getMaxContentWidth(80)).toBe(Math.floor(80 * NARROW_CONTENT_RATIO));
|
|
99
|
+
expect(getMaxContentWidth(60)).toBe(Math.floor(60 * NARROW_CONTENT_RATIO));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("uses wide ratio for widths >= 132", () => {
|
|
103
|
+
expect(getMaxContentWidth(132)).toBe(Math.floor(132 * WIDE_CONTENT_RATIO));
|
|
104
|
+
expect(getMaxContentWidth(200)).toBe(Math.floor(200 * WIDE_CONTENT_RATIO));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("interpolates for widths between 80 and 132", () => {
|
|
108
|
+
const midWidth = 106; // Midpoint between 80 and 132
|
|
109
|
+
const result = getMaxContentWidth(midWidth);
|
|
110
|
+
// Should be between the two ratios
|
|
111
|
+
const minExpected = Math.floor(midWidth * WIDE_CONTENT_RATIO);
|
|
112
|
+
const maxExpected = Math.floor(midWidth * NARROW_CONTENT_RATIO);
|
|
113
|
+
expect(result).toBeGreaterThanOrEqual(minExpected);
|
|
114
|
+
expect(result).toBeLessThanOrEqual(maxExpected);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("handles zero width", () => {
|
|
118
|
+
expect(getMaxContentWidth(0)).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("getContentPadding", () => {
|
|
123
|
+
it("returns left and right padding", () => {
|
|
124
|
+
const result = getContentPadding(100);
|
|
125
|
+
expect(result).toHaveProperty("left");
|
|
126
|
+
expect(result).toHaveProperty("right");
|
|
127
|
+
expect(typeof result.left).toBe("number");
|
|
128
|
+
expect(typeof result.right).toBe("number");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("padding + content = terminal width", () => {
|
|
132
|
+
const width = 100;
|
|
133
|
+
const { left, right } = getContentPadding(width);
|
|
134
|
+
const contentWidth = getMaxContentWidth(width);
|
|
135
|
+
expect(left + right + contentWidth).toBe(width);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("distributes padding roughly evenly", () => {
|
|
139
|
+
const { left, right } = getContentPadding(100);
|
|
140
|
+
expect(Math.abs(left - right)).toBeLessThanOrEqual(1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("getTerminalSize", () => {
|
|
145
|
+
it("returns object with width and height", () => {
|
|
146
|
+
const size = getTerminalSize();
|
|
147
|
+
expect(size).toHaveProperty("width");
|
|
148
|
+
expect(size).toHaveProperty("height");
|
|
149
|
+
expect(typeof size.width).toBe("number");
|
|
150
|
+
expect(typeof size.height).toBe("number");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("exported constants", () => {
|
|
155
|
+
it("exports expected default values", () => {
|
|
156
|
+
expect(DEFAULT_TERMINAL_WIDTH).toBe(80);
|
|
157
|
+
expect(DEFAULT_TERMINAL_HEIGHT).toBe(24);
|
|
158
|
+
expect(NARROW_WIDTH_BREAKPOINT).toBe(80);
|
|
159
|
+
expect(WIDE_WIDTH_BREAKPOINT).toBe(132);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("exports valid ratios", () => {
|
|
163
|
+
expect(NARROW_CONTENT_RATIO).toBeGreaterThan(0);
|
|
164
|
+
expect(NARROW_CONTENT_RATIO).toBeLessThanOrEqual(1);
|
|
165
|
+
expect(WIDE_CONTENT_RATIO).toBeGreaterThan(0);
|
|
166
|
+
expect(WIDE_CONTENT_RATIO).toBeLessThanOrEqual(1);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bracketed Paste Mode Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to enable/disable bracketed paste mode in terminals.
|
|
5
|
+
* When enabled, pasted text is wrapped with escape sequences that allow
|
|
6
|
+
* applications to distinguish between typed and pasted text.
|
|
7
|
+
*
|
|
8
|
+
* @module tui/utils/bracketedPaste
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getActiveStdout } from "../buffered-stdout.js";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Constants
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/** ANSI escape sequence to enable bracketed paste mode */
|
|
18
|
+
const ENABLE_BRACKETED_PASTE = "\x1b[?2004h";
|
|
19
|
+
|
|
20
|
+
/** ANSI escape sequence to disable bracketed paste mode */
|
|
21
|
+
const DISABLE_BRACKETED_PASTE = "\x1b[?2004l";
|
|
22
|
+
|
|
23
|
+
/** Escape sequence sent by terminal at start of pasted content */
|
|
24
|
+
export const PASTE_START = "\x1b[200~";
|
|
25
|
+
|
|
26
|
+
/** Escape sequence sent by terminal at end of pasted content */
|
|
27
|
+
export const PASTE_END = "\x1b[201~";
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Functions
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enable bracketed paste mode.
|
|
35
|
+
*
|
|
36
|
+
* When enabled, the terminal wraps pasted content with escape sequences:
|
|
37
|
+
* - Start: ESC [ 200 ~
|
|
38
|
+
* - End: ESC [ 201 ~
|
|
39
|
+
*
|
|
40
|
+
* This allows applications to distinguish between typed and pasted text.
|
|
41
|
+
*/
|
|
42
|
+
export function enableBracketedPaste(): void {
|
|
43
|
+
getActiveStdout().write(ENABLE_BRACKETED_PASTE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Disable bracketed paste mode.
|
|
48
|
+
*
|
|
49
|
+
* Should be called when the application exits to restore terminal state.
|
|
50
|
+
*/
|
|
51
|
+
export function disableBracketedPaste(): void {
|
|
52
|
+
getActiveStdout().write(DISABLE_BRACKETED_PASTE);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a string contains the paste start sequence.
|
|
57
|
+
*/
|
|
58
|
+
export function hasPasteStart(data: string): boolean {
|
|
59
|
+
return data.includes(PASTE_START);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a string contains the paste end sequence.
|
|
64
|
+
*/
|
|
65
|
+
export function hasPasteEnd(data: string): boolean {
|
|
66
|
+
return data.includes(PASTE_END);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract pasted content from bracketed paste sequences.
|
|
71
|
+
*
|
|
72
|
+
* @param data - Raw input data that may contain paste sequences
|
|
73
|
+
* @returns Object containing the extracted paste content and whether extraction was complete
|
|
74
|
+
*/
|
|
75
|
+
export function extractPasteContent(data: string): {
|
|
76
|
+
content: string;
|
|
77
|
+
complete: boolean;
|
|
78
|
+
remaining: string;
|
|
79
|
+
} {
|
|
80
|
+
const startIdx = data.indexOf(PASTE_START);
|
|
81
|
+
if (startIdx === -1) {
|
|
82
|
+
return { content: "", complete: false, remaining: data };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const contentStart = startIdx + PASTE_START.length;
|
|
86
|
+
const endIdx = data.indexOf(PASTE_END, contentStart);
|
|
87
|
+
|
|
88
|
+
if (endIdx === -1) {
|
|
89
|
+
// Paste sequence started but not ended - partial paste
|
|
90
|
+
return {
|
|
91
|
+
content: data.slice(contentStart),
|
|
92
|
+
complete: false,
|
|
93
|
+
remaining: data.slice(0, startIdx),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Complete paste sequence found
|
|
98
|
+
const pastedContent = data.slice(contentStart, endIdx);
|
|
99
|
+
const beforePaste = data.slice(0, startIdx);
|
|
100
|
+
const afterPaste = data.slice(endIdx + PASTE_END.length);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
content: pastedContent,
|
|
104
|
+
complete: true,
|
|
105
|
+
remaining: beforePaste + afterPaste,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Cursor Management
|
|
3
|
+
*
|
|
4
|
+
* Singleton to prevent race conditions from multiple components
|
|
5
|
+
* trying to show/hide the cursor simultaneously.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/utils/cursor-manager
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getActiveStdout } from "../buffered-stdout.js";
|
|
11
|
+
|
|
12
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
13
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cursor manager implementation
|
|
17
|
+
*/
|
|
18
|
+
class CursorManagerImpl {
|
|
19
|
+
private _hidden = false;
|
|
20
|
+
private _locked = false;
|
|
21
|
+
|
|
22
|
+
/** Get the active stdout stream (uses BufferedStdout when available) */
|
|
23
|
+
private get _stdout(): NodeJS.WriteStream {
|
|
24
|
+
return getActiveStdout();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hide the cursor (idempotent).
|
|
29
|
+
* Does nothing if cursor is already hidden or locked.
|
|
30
|
+
*/
|
|
31
|
+
hide(): void {
|
|
32
|
+
if (!this._hidden && !this._locked) {
|
|
33
|
+
try {
|
|
34
|
+
this._stdout.write(HIDE_CURSOR);
|
|
35
|
+
this._hidden = true;
|
|
36
|
+
} catch {
|
|
37
|
+
// Ignore write errors (e.g., closed stdout)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Show the cursor (idempotent).
|
|
44
|
+
* Does nothing if cursor is already visible or locked.
|
|
45
|
+
*/
|
|
46
|
+
show(): void {
|
|
47
|
+
if (this._hidden && !this._locked) {
|
|
48
|
+
try {
|
|
49
|
+
this._stdout.write(SHOW_CURSOR);
|
|
50
|
+
this._hidden = false;
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore write errors (e.g., closed stdout)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Lock cursor in hidden state (for TUI mode).
|
|
59
|
+
* While locked, show() has no effect.
|
|
60
|
+
*/
|
|
61
|
+
lock(): void {
|
|
62
|
+
this._locked = true;
|
|
63
|
+
this.forceHide();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Unlock cursor control.
|
|
68
|
+
* After unlocking, show() and hide() work normally.
|
|
69
|
+
*/
|
|
70
|
+
unlock(): void {
|
|
71
|
+
this._locked = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Force hide cursor (bypasses lock, for cleanup).
|
|
76
|
+
* Use only for critical cleanup scenarios.
|
|
77
|
+
*/
|
|
78
|
+
forceHide(): void {
|
|
79
|
+
try {
|
|
80
|
+
this._stdout.write(HIDE_CURSOR);
|
|
81
|
+
this._hidden = true;
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore write errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Force show cursor (bypasses lock, for cleanup on exit).
|
|
89
|
+
* Use only for exit/cleanup scenarios.
|
|
90
|
+
*/
|
|
91
|
+
forceShow(): void {
|
|
92
|
+
try {
|
|
93
|
+
this._stdout.write(SHOW_CURSOR);
|
|
94
|
+
this._hidden = false;
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore write errors
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether the cursor is currently hidden.
|
|
102
|
+
*/
|
|
103
|
+
get isHidden(): boolean {
|
|
104
|
+
return this._hidden;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether cursor control is locked.
|
|
109
|
+
*/
|
|
110
|
+
get isLocked(): boolean {
|
|
111
|
+
return this._locked;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Global cursor manager singleton.
|
|
117
|
+
* Use this instead of directly writing cursor escape codes.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* // Lock cursor hidden for TUI session
|
|
122
|
+
* CursorManager.lock();
|
|
123
|
+
*
|
|
124
|
+
* // On exit, restore cursor
|
|
125
|
+
* CursorManager.unlock();
|
|
126
|
+
* CursorManager.show();
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export const CursorManager = new CursorManagerImpl();
|
|
130
|
+
|
|
131
|
+
export type { CursorManagerImpl };
|