@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,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Width Utilities
|
|
3
|
+
*
|
|
4
|
+
* Visual width calculation and manipulation utilities for terminal text.
|
|
5
|
+
* Handles CJK characters, emojis, and ANSI escape sequences correctly.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/utils/text-width
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import stringWidth from "string-width";
|
|
11
|
+
import wrapAnsi from "wrap-ansi";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Text alignment options for padding.
|
|
15
|
+
*/
|
|
16
|
+
export type TextAlign = "left" | "center" | "right";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the visual width of text in terminal columns.
|
|
20
|
+
*
|
|
21
|
+
* This accounts for:
|
|
22
|
+
* - CJK characters (width 2)
|
|
23
|
+
* - Emojis (width 2)
|
|
24
|
+
* - ANSI escape sequences (width 0)
|
|
25
|
+
* - Zero-width characters
|
|
26
|
+
*
|
|
27
|
+
* @param text - Text to measure
|
|
28
|
+
* @returns Visual width in terminal columns
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* getVisualWidth("hello") // 5
|
|
33
|
+
* getVisualWidth("你好") // 4 (2 chars × 2 width)
|
|
34
|
+
* getVisualWidth("👋") // 2
|
|
35
|
+
* getVisualWidth("\x1b[31mred\x1b[0m") // 3 (ANSI codes have 0 width)
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function getVisualWidth(text: string): number {
|
|
39
|
+
return stringWidth(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Truncate text to fit within a maximum visual width.
|
|
44
|
+
*
|
|
45
|
+
* Adds ellipsis if truncation is needed. Handles ANSI codes correctly,
|
|
46
|
+
* ensuring escape sequences are properly closed.
|
|
47
|
+
*
|
|
48
|
+
* @param text - Text to truncate
|
|
49
|
+
* @param maxWidth - Maximum visual width
|
|
50
|
+
* @param ellipsis - Ellipsis string (default: "…")
|
|
51
|
+
* @returns Truncated text with ellipsis if needed
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* truncateToWidth("Hello World", 8) // "Hello W…"
|
|
56
|
+
* truncateToWidth("Short", 10) // "Short"
|
|
57
|
+
* truncateToWidth("你好世界", 5) // "你好…"
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function truncateToWidth(text: string, maxWidth: number, ellipsis = "…"): string {
|
|
61
|
+
const textWidth = getVisualWidth(text);
|
|
62
|
+
|
|
63
|
+
if (textWidth <= maxWidth) {
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ellipsisWidth = getVisualWidth(ellipsis);
|
|
68
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
69
|
+
|
|
70
|
+
if (targetWidth <= 0) {
|
|
71
|
+
return ellipsis.slice(0, maxWidth);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build truncated string character by character
|
|
75
|
+
let result = "";
|
|
76
|
+
let currentWidth = 0;
|
|
77
|
+
|
|
78
|
+
for (const char of text) {
|
|
79
|
+
const charWidth = getVisualWidth(char);
|
|
80
|
+
if (currentWidth + charWidth > targetWidth) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
result += char;
|
|
84
|
+
currentWidth += charWidth;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result + ellipsis;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pad text to a specific visual width with alignment.
|
|
92
|
+
*
|
|
93
|
+
* @param text - Text to pad
|
|
94
|
+
* @param width - Target visual width
|
|
95
|
+
* @param align - Alignment: "left", "center", or "right"
|
|
96
|
+
* @param padChar - Character to use for padding (default: space)
|
|
97
|
+
* @returns Padded text
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* padToWidth("Hi", 10, "left") // "Hi "
|
|
102
|
+
* padToWidth("Hi", 10, "right") // " Hi"
|
|
103
|
+
* padToWidth("Hi", 10, "center") // " Hi "
|
|
104
|
+
* padToWidth("你好", 8, "center") // " 你好 "
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function padToWidth(
|
|
108
|
+
text: string,
|
|
109
|
+
width: number,
|
|
110
|
+
align: TextAlign = "left",
|
|
111
|
+
padChar = " "
|
|
112
|
+
): string {
|
|
113
|
+
const textWidth = getVisualWidth(text);
|
|
114
|
+
const padCharWidth = getVisualWidth(padChar);
|
|
115
|
+
|
|
116
|
+
if (textWidth >= width) {
|
|
117
|
+
return text;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const totalPadding = width - textWidth;
|
|
121
|
+
const padCount = Math.floor(totalPadding / padCharWidth);
|
|
122
|
+
|
|
123
|
+
switch (align) {
|
|
124
|
+
case "right": {
|
|
125
|
+
return padChar.repeat(padCount) + text;
|
|
126
|
+
}
|
|
127
|
+
case "center": {
|
|
128
|
+
const leftPad = Math.floor(padCount / 2);
|
|
129
|
+
const rightPad = padCount - leftPad;
|
|
130
|
+
return padChar.repeat(leftPad) + text + padChar.repeat(rightPad);
|
|
131
|
+
}
|
|
132
|
+
default: {
|
|
133
|
+
// "left" alignment (default)
|
|
134
|
+
return text + padChar.repeat(padCount);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Wrap text at visual width boundaries.
|
|
141
|
+
*
|
|
142
|
+
* Uses wrap-ansi for proper handling of ANSI codes and
|
|
143
|
+
* word boundaries. Preserves ANSI styling across lines.
|
|
144
|
+
*
|
|
145
|
+
* @param text - Text to wrap
|
|
146
|
+
* @param width - Maximum line width
|
|
147
|
+
* @param options - Wrapping options
|
|
148
|
+
* @returns Wrapped text with newlines
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* wrapToWidth("This is a long line that needs wrapping", 20)
|
|
153
|
+
* // "This is a long line\nthat needs wrapping"
|
|
154
|
+
*
|
|
155
|
+
* wrapToWidth("LongWordThatCantBreak", 10, { hard: true })
|
|
156
|
+
* // "LongWordTh\natCantBrea\nk"
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export function wrapToWidth(
|
|
160
|
+
text: string,
|
|
161
|
+
width: number,
|
|
162
|
+
options: {
|
|
163
|
+
/** Hard wrap long words (default: false) */
|
|
164
|
+
hard?: boolean;
|
|
165
|
+
/** Trim whitespace at line ends (default: true) */
|
|
166
|
+
trim?: boolean;
|
|
167
|
+
/** Preserve leading whitespace (default: false) */
|
|
168
|
+
wordWrap?: boolean;
|
|
169
|
+
} = {}
|
|
170
|
+
): string {
|
|
171
|
+
const { hard = false, trim = true, wordWrap = true } = options;
|
|
172
|
+
|
|
173
|
+
return wrapAnsi(text, width, {
|
|
174
|
+
hard,
|
|
175
|
+
trim,
|
|
176
|
+
wordWrap,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Split text into lines respecting visual width.
|
|
182
|
+
*
|
|
183
|
+
* Similar to wrapToWidth but returns an array of lines
|
|
184
|
+
* instead of a single string with newlines.
|
|
185
|
+
*
|
|
186
|
+
* @param text - Text to split
|
|
187
|
+
* @param width - Maximum line width
|
|
188
|
+
* @param options - Wrapping options
|
|
189
|
+
* @returns Array of lines
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```ts
|
|
193
|
+
* splitLines("Hello World", 6)
|
|
194
|
+
* // ["Hello", "World"]
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function splitLines(
|
|
198
|
+
text: string,
|
|
199
|
+
width: number,
|
|
200
|
+
options: Parameters<typeof wrapToWidth>[2] = {}
|
|
201
|
+
): string[] {
|
|
202
|
+
return wrapToWidth(text, width, options).split("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calculate the number of lines text will occupy at a given width.
|
|
207
|
+
*
|
|
208
|
+
* @param text - Text to measure
|
|
209
|
+
* @param width - Available width
|
|
210
|
+
* @param options - Wrapping options
|
|
211
|
+
* @returns Number of lines
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* countLines("Hello World", 6) // 2
|
|
216
|
+
* countLines("Hi", 10) // 1
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export function countLines(
|
|
220
|
+
text: string,
|
|
221
|
+
width: number,
|
|
222
|
+
options: Parameters<typeof wrapToWidth>[2] = {}
|
|
223
|
+
): number {
|
|
224
|
+
return splitLines(text, width, options).length;
|
|
225
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Sanitizer for TUI
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent text normalization across all TUI components.
|
|
5
|
+
* Centralizes sanitization logic previously scattered in TextInput.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/utils/textSanitizer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for text sanitization.
|
|
16
|
+
*/
|
|
17
|
+
export interface SanitizeOptions {
|
|
18
|
+
/** Convert tabs to spaces (default: 2) */
|
|
19
|
+
readonly tabWidth?: number;
|
|
20
|
+
/** Maximum line length before wrap hint (default: 0 = no limit) */
|
|
21
|
+
readonly maxLineLength?: number;
|
|
22
|
+
/** Strip ALL ANSI codes (default: false, just sanitize dangerous ones) */
|
|
23
|
+
readonly stripAllAnsi?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Constants
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/** Default tab width in spaces */
|
|
31
|
+
const DEFAULT_TAB_WIDTH = 2;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* SGR (Select Graphic Rendition) pattern - safe color/style codes
|
|
35
|
+
* Format: ESC [ <params> m
|
|
36
|
+
* Params: 0-109 for colors/styles
|
|
37
|
+
*/
|
|
38
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching ANSI SGR sequences
|
|
39
|
+
const SGR_PATTERN = /\x1b\[[\d;]*m/g;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* All CSI (Control Sequence Introducer) sequences
|
|
43
|
+
* Format: ESC [ <params> <final byte>
|
|
44
|
+
* Final bytes: 0x40-0x7E (@-~)
|
|
45
|
+
*/
|
|
46
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching all ANSI CSI sequences
|
|
47
|
+
const CSI_PATTERN = /\x1b\[[\d;?]*[ -/]*[@-~]/g;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* OSC (Operating System Command) sequences
|
|
51
|
+
* Format: ESC ] ... BEL or ESC ] ... ST
|
|
52
|
+
*/
|
|
53
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching ANSI OSC sequences
|
|
54
|
+
const OSC_PATTERN = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Control characters to strip (except \t=0x09, \n=0x0A, \x1B=ESC for ANSI)
|
|
58
|
+
* Includes: NUL-HT(0x00-0x08), VT-FF(0x0B-0x0C), SO-SUB(0x0E-0x1A), FS-US(0x1C-0x1F), DEL(0x7F), C1(0x80-0x9F)
|
|
59
|
+
* Note: ESC(0x1B) is preserved for ANSI sequences
|
|
60
|
+
*/
|
|
61
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - stripping dangerous control characters
|
|
62
|
+
const CONTROL_CHARS_PATTERN = /[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f\x7f\x80-\x9f]/g;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Standalone ESC characters (not part of a valid sequence)
|
|
66
|
+
* Matches ESC not followed by [ or ]
|
|
67
|
+
*/
|
|
68
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - matching orphan escape characters
|
|
69
|
+
const ORPHAN_ESC_PATTERN = /\x1b(?![[\]])/g;
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Core Functions
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sanitize text for safe TUI rendering (without ANSI awareness).
|
|
77
|
+
*
|
|
78
|
+
* Performs the following transformations:
|
|
79
|
+
* 1. CRLF → LF (Windows line endings)
|
|
80
|
+
* 2. Lone CR → LF (old Mac line endings)
|
|
81
|
+
* 3. Unicode line/paragraph separators → LF
|
|
82
|
+
* 4. Remove dangerous control characters (keeps \n, \t, \x1B for ANSI)
|
|
83
|
+
* 5. Tab → spaces (configurable width)
|
|
84
|
+
*
|
|
85
|
+
* @param text - Raw text to sanitize
|
|
86
|
+
* @param options - Sanitization options
|
|
87
|
+
* @returns Sanitized text safe for rendering
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* // Basic usage
|
|
92
|
+
* const clean = sanitizeText("Hello\r\nWorld\t!");
|
|
93
|
+
* // => "Hello\nWorld !"
|
|
94
|
+
*
|
|
95
|
+
* // Custom tab width
|
|
96
|
+
* const clean = sanitizeText("A\tB", { tabWidth: 4 });
|
|
97
|
+
* // => "A B"
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function sanitizeText(text: string, options?: SanitizeOptions): string {
|
|
101
|
+
const tabWidth = options?.tabWidth ?? DEFAULT_TAB_WIDTH;
|
|
102
|
+
|
|
103
|
+
// Step 1-3: Normalize line endings
|
|
104
|
+
let result = text
|
|
105
|
+
.replace(/\r\n/g, "\n") // CRLF → LF
|
|
106
|
+
.replace(/\r/g, "\n") // Lone CR → LF
|
|
107
|
+
.replace(/\u2028|\u2029/g, "\n"); // Unicode line separators
|
|
108
|
+
|
|
109
|
+
// Step 4: Remove dangerous control characters (keep \n=0x0A, \t=0x09, \x1B=ESC)
|
|
110
|
+
result = result.replace(CONTROL_CHARS_PATTERN, "");
|
|
111
|
+
|
|
112
|
+
// Step 5: Convert tabs to spaces
|
|
113
|
+
if (tabWidth > 0) {
|
|
114
|
+
const spaces = " ".repeat(tabWidth);
|
|
115
|
+
result = result.replace(/\t/g, spaces);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sanitize ANSI codes using an allow-list approach.
|
|
123
|
+
*
|
|
124
|
+
* Keeps safe codes:
|
|
125
|
+
* - SGR (Select Graphic Rendition): Colors and text styles `\x1b[...m`
|
|
126
|
+
*
|
|
127
|
+
* Removes dangerous codes:
|
|
128
|
+
* - Cursor movement: `\x1b[...H`, `\x1b[...A/B/C/D/E/F/G`
|
|
129
|
+
* - Screen clearing: `\x1b[...J`, `\x1b[...K`
|
|
130
|
+
* - Scrolling: `\x1b[...S`, `\x1b[...T`
|
|
131
|
+
* - Other CSI sequences
|
|
132
|
+
* - OSC sequences (terminal titles, hyperlinks in raw form)
|
|
133
|
+
*
|
|
134
|
+
* @param text - Text containing ANSI codes
|
|
135
|
+
* @param options - Sanitization options
|
|
136
|
+
* @returns Text with only safe ANSI codes preserved
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* // Colors preserved
|
|
141
|
+
* const text = "\x1b[31mRed\x1b[0m";
|
|
142
|
+
* sanitizeAnsi(text) // => "\x1b[31mRed\x1b[0m"
|
|
143
|
+
*
|
|
144
|
+
* // Cursor movement removed
|
|
145
|
+
* const text = "\x1b[2JCleared\x1b[H";
|
|
146
|
+
* sanitizeAnsi(text) // => "Cleared"
|
|
147
|
+
*
|
|
148
|
+
* // Strip all ANSI
|
|
149
|
+
* sanitizeAnsi(text, { stripAllAnsi: true }) // => "Cleared"
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function sanitizeAnsi(text: string, options?: SanitizeOptions): string {
|
|
153
|
+
if (options?.stripAllAnsi) {
|
|
154
|
+
// Remove ALL ANSI sequences
|
|
155
|
+
return text.replace(CSI_PATTERN, "").replace(OSC_PATTERN, "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Collect safe SGR sequences and their positions
|
|
159
|
+
const sgrRegex = new RegExp(SGR_PATTERN.source, "g");
|
|
160
|
+
|
|
161
|
+
// Build set of exact positions of safe SGR sequences
|
|
162
|
+
const safePositions = new Map<number, number>(); // start -> end
|
|
163
|
+
for (let match = sgrRegex.exec(text); match !== null; match = sgrRegex.exec(text)) {
|
|
164
|
+
safePositions.set(match.index, match.index + match[0].length);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Remove dangerous CSI sequences (not SGR)
|
|
168
|
+
let result = "";
|
|
169
|
+
let lastIndex = 0;
|
|
170
|
+
const csiRegex = new RegExp(CSI_PATTERN.source, "g");
|
|
171
|
+
|
|
172
|
+
for (let match = csiRegex.exec(text); match !== null; match = csiRegex.exec(text)) {
|
|
173
|
+
const start = match.index;
|
|
174
|
+
const end = start + match[0].length;
|
|
175
|
+
|
|
176
|
+
// Add text before this match
|
|
177
|
+
result += text.slice(lastIndex, start);
|
|
178
|
+
|
|
179
|
+
// Check if this is a safe SGR sequence
|
|
180
|
+
const isSafe = safePositions.has(start) && safePositions.get(start) === end;
|
|
181
|
+
if (isSafe) {
|
|
182
|
+
result += match[0];
|
|
183
|
+
}
|
|
184
|
+
// Otherwise, skip (remove) the dangerous sequence
|
|
185
|
+
|
|
186
|
+
lastIndex = end;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Add remaining text
|
|
190
|
+
result += text.slice(lastIndex);
|
|
191
|
+
|
|
192
|
+
// Remove OSC sequences (titles, hyperlinks, etc.)
|
|
193
|
+
result = result.replace(OSC_PATTERN, "");
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Combined sanitization: text normalization + ANSI filtering.
|
|
200
|
+
*
|
|
201
|
+
* This is the recommended function for most use cases.
|
|
202
|
+
* Applies both `sanitizeAnsi` and `sanitizeText` in sequence.
|
|
203
|
+
*
|
|
204
|
+
* @param text - Raw text to sanitize
|
|
205
|
+
* @param options - Sanitization options
|
|
206
|
+
* @returns Fully sanitized text safe for TUI rendering
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* // Combined sanitization
|
|
211
|
+
* const raw = "Hello\r\nWorld\x1b[2J\x1b[31mRed\x1b[0m\t!";
|
|
212
|
+
* const clean = sanitize(raw);
|
|
213
|
+
* // => "Hello\nWorld\x1b[31mRed\x1b[0m !"
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export function sanitize(text: string, options?: SanitizeOptions): string {
|
|
217
|
+
// Step 1: Sanitize ANSI (remove dangerous sequences, keep colors)
|
|
218
|
+
const ansiClean = sanitizeAnsi(text, options);
|
|
219
|
+
|
|
220
|
+
// Step 2: Sanitize text (normalize line endings, remove control chars, convert tabs)
|
|
221
|
+
const textClean = sanitizeText(ansiClean, options);
|
|
222
|
+
|
|
223
|
+
// Step 3: Remove any orphan ESC characters left behind (incomplete sequences)
|
|
224
|
+
return textClean.replace(ORPHAN_ESC_PATTERN, "");
|
|
225
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Utilities (T002 Hardening)
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for text manipulation in the TUI.
|
|
5
|
+
* Provides hard-wrapping to prevent unpredictable terminal soft-wrapping.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/utils/textUtils
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import stringWidth from "string-width";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hard-wrap text to prevent terminal soft-wrapping.
|
|
14
|
+
* Forces newlines at column boundaries so terminal doesn't wrap unpredictably.
|
|
15
|
+
*
|
|
16
|
+
* Uses string-width for accurate CJK/Emoji/ANSI handling.
|
|
17
|
+
* This is a simpler implementation that doesn't require wrap-ansi as a direct dependency.
|
|
18
|
+
*
|
|
19
|
+
* @param text - The text to wrap
|
|
20
|
+
* @param columns - Maximum column width
|
|
21
|
+
* @returns Text with hard line breaks inserted
|
|
22
|
+
*/
|
|
23
|
+
export function hardWrap(text: string, columns: number): string {
|
|
24
|
+
if (!text || columns <= 0) {
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lines = text.split("\n");
|
|
29
|
+
const wrappedLines: string[] = [];
|
|
30
|
+
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (stringWidth(line) <= columns) {
|
|
33
|
+
wrappedLines.push(line);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Need to hard-wrap this line
|
|
38
|
+
let remaining = line;
|
|
39
|
+
while (remaining.length > 0) {
|
|
40
|
+
const segment = truncateToWidth(remaining, columns);
|
|
41
|
+
wrappedLines.push(segment);
|
|
42
|
+
remaining = remaining.slice(segment.length);
|
|
43
|
+
|
|
44
|
+
// Safety: prevent infinite loop if no progress
|
|
45
|
+
if (segment.length === 0 && remaining.length > 0) {
|
|
46
|
+
// Force at least one character if we're stuck
|
|
47
|
+
const firstChar = remaining[0] ?? "";
|
|
48
|
+
wrappedLines.push(firstChar);
|
|
49
|
+
remaining = remaining.slice(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return wrappedLines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Truncate a string to fit within a specified display width.
|
|
59
|
+
* Uses string-width for accurate width calculation.
|
|
60
|
+
*
|
|
61
|
+
* @param str - The string to truncate
|
|
62
|
+
* @param maxWidth - Maximum display width
|
|
63
|
+
* @returns Truncated string that fits within maxWidth
|
|
64
|
+
*/
|
|
65
|
+
function truncateToWidth(str: string, maxWidth: number): string {
|
|
66
|
+
if (stringWidth(str) <= maxWidth) {
|
|
67
|
+
return str;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Binary search for the right length
|
|
71
|
+
let low = 0;
|
|
72
|
+
let high = str.length;
|
|
73
|
+
|
|
74
|
+
while (low < high) {
|
|
75
|
+
const mid = Math.ceil((low + high) / 2);
|
|
76
|
+
const slice = str.slice(0, mid);
|
|
77
|
+
if (stringWidth(slice) <= maxWidth) {
|
|
78
|
+
low = mid;
|
|
79
|
+
} else {
|
|
80
|
+
high = mid - 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return str.slice(0, low);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Truncate text to fit within a maximum display width with ellipsis.
|
|
89
|
+
* Uses string-width for accurate CJK/Emoji/ANSI handling.
|
|
90
|
+
*
|
|
91
|
+
* @param text - The text to truncate
|
|
92
|
+
* @param maxWidth - Maximum display width in terminal cells
|
|
93
|
+
* @param ellipsis - Ellipsis character (default: "…")
|
|
94
|
+
* @returns Truncated text that fits within maxWidth cells
|
|
95
|
+
*/
|
|
96
|
+
export function truncateToDisplayWidth(
|
|
97
|
+
text: string,
|
|
98
|
+
maxWidth: number,
|
|
99
|
+
ellipsis: string = "…"
|
|
100
|
+
): string {
|
|
101
|
+
if (!text || maxWidth <= 0) return "";
|
|
102
|
+
|
|
103
|
+
const textWidth = stringWidth(text);
|
|
104
|
+
if (textWidth <= maxWidth) return text;
|
|
105
|
+
|
|
106
|
+
const ellipsisWidth = stringWidth(ellipsis);
|
|
107
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
108
|
+
|
|
109
|
+
if (targetWidth <= 0) return ellipsis.slice(0, maxWidth);
|
|
110
|
+
|
|
111
|
+
// Use existing truncateToWidth for the main content
|
|
112
|
+
const truncated = truncateToWidth(text, targetWidth);
|
|
113
|
+
return truncated + ellipsis;
|
|
114
|
+
}
|