@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,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Renderer Service
|
|
3
|
+
*
|
|
4
|
+
* Provides marked-based Markdown parsing with ANSI terminal output.
|
|
5
|
+
* Integrates with the syntax-highlighter service for code block highlighting.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/services/markdown-renderer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { Marked, type Tokens } from "marked";
|
|
12
|
+
import { highlightCode, highlightCodeSync, isHighlighterReady } from "./syntax-highlighter.js";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for markdown rendering
|
|
20
|
+
*/
|
|
21
|
+
export interface MarkdownRenderOptions {
|
|
22
|
+
/** Whether to use compact mode (less spacing) */
|
|
23
|
+
readonly compact?: boolean;
|
|
24
|
+
/** Custom colors for rendering */
|
|
25
|
+
readonly colors?: Partial<MarkdownColors>;
|
|
26
|
+
/** Whether to include syntax highlighting for code blocks */
|
|
27
|
+
readonly syntaxHighlight?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Color configuration for markdown elements
|
|
32
|
+
*/
|
|
33
|
+
export interface MarkdownColors {
|
|
34
|
+
/** Heading color */
|
|
35
|
+
readonly heading: string;
|
|
36
|
+
/** Bold text color */
|
|
37
|
+
readonly bold: string;
|
|
38
|
+
/** Italic text color */
|
|
39
|
+
readonly italic: string;
|
|
40
|
+
/** Inline code color */
|
|
41
|
+
readonly inlineCode: string;
|
|
42
|
+
/** Inline code background */
|
|
43
|
+
readonly inlineCodeBg: string;
|
|
44
|
+
/** Link text color */
|
|
45
|
+
readonly link: string;
|
|
46
|
+
/** Link URL color */
|
|
47
|
+
readonly linkUrl: string;
|
|
48
|
+
/** Blockquote color */
|
|
49
|
+
readonly blockquote: string;
|
|
50
|
+
/** List bullet color */
|
|
51
|
+
readonly listBullet: string;
|
|
52
|
+
/** Horizontal rule color */
|
|
53
|
+
readonly hr: string;
|
|
54
|
+
/** Code block border color */
|
|
55
|
+
readonly codeBlockBorder: string;
|
|
56
|
+
/** Code language label color */
|
|
57
|
+
readonly codeLanguage: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Constants
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Default colors for markdown rendering
|
|
66
|
+
*/
|
|
67
|
+
const DEFAULT_COLORS: MarkdownColors = {
|
|
68
|
+
heading: "#58a6ff", // GitHub blue
|
|
69
|
+
bold: "#ffffff",
|
|
70
|
+
italic: "#8b949e",
|
|
71
|
+
inlineCode: "#f0883e",
|
|
72
|
+
inlineCodeBg: "#161b22",
|
|
73
|
+
link: "#58a6ff",
|
|
74
|
+
linkUrl: "#8b949e",
|
|
75
|
+
blockquote: "#8b949e",
|
|
76
|
+
listBullet: "#8b949e",
|
|
77
|
+
hr: "#30363d",
|
|
78
|
+
codeBlockBorder: "#30363d",
|
|
79
|
+
codeLanguage: "#8b949e",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Heading level prefixes for visual hierarchy
|
|
84
|
+
*/
|
|
85
|
+
const HEADING_PREFIXES: Record<number, string> = {
|
|
86
|
+
1: "═══",
|
|
87
|
+
2: "───",
|
|
88
|
+
3: "──",
|
|
89
|
+
4: "─",
|
|
90
|
+
5: "·",
|
|
91
|
+
6: "·",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Rendering State
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
/** Store for pending async code blocks during sync render */
|
|
99
|
+
const pendingCodeBlocks: Map<string, { code: string; lang: string }> = new Map();
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Marked Renderer Factory
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a marked renderer with ANSI terminal output
|
|
107
|
+
*/
|
|
108
|
+
function createTerminalRenderer(
|
|
109
|
+
options: MarkdownRenderOptions = {},
|
|
110
|
+
codeBlockResolver?: (id: string, code: string, lang: string) => string
|
|
111
|
+
): {
|
|
112
|
+
renderer: Partial<import("marked").RendererObject>;
|
|
113
|
+
parseInline: (tokens: Tokens.Generic[]) => string;
|
|
114
|
+
} {
|
|
115
|
+
const colors = { ...DEFAULT_COLORS, ...options.colors };
|
|
116
|
+
const compact = options.compact ?? false;
|
|
117
|
+
const syntaxHighlight = options.syntaxHighlight ?? true;
|
|
118
|
+
|
|
119
|
+
const colorFn = {
|
|
120
|
+
heading: chalk.hex(colors.heading),
|
|
121
|
+
bold: chalk.hex(colors.bold).bold,
|
|
122
|
+
italic: chalk.hex(colors.italic).italic,
|
|
123
|
+
inlineCode: chalk.hex(colors.inlineCode),
|
|
124
|
+
inlineCodeBg: chalk.bgHex(colors.inlineCodeBg).hex(colors.inlineCode),
|
|
125
|
+
link: chalk.hex(colors.link).underline,
|
|
126
|
+
linkUrl: chalk.hex(colors.linkUrl).dim,
|
|
127
|
+
blockquote: chalk.hex(colors.blockquote),
|
|
128
|
+
listBullet: chalk.hex(colors.listBullet),
|
|
129
|
+
hr: chalk.hex(colors.hr),
|
|
130
|
+
codeBlockBorder: chalk.hex(colors.codeBlockBorder),
|
|
131
|
+
codeLanguage: chalk.hex(colors.codeLanguage).italic,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Inline token parser
|
|
135
|
+
const parseInline = (tokens: Tokens.Generic[]): string => {
|
|
136
|
+
return tokens
|
|
137
|
+
.map((token) => {
|
|
138
|
+
switch (token.type) {
|
|
139
|
+
case "text":
|
|
140
|
+
return (token as Tokens.Text).text;
|
|
141
|
+
case "strong":
|
|
142
|
+
return colorFn.bold(parseInline((token as Tokens.Strong).tokens));
|
|
143
|
+
case "em":
|
|
144
|
+
return colorFn.italic(parseInline((token as Tokens.Em).tokens));
|
|
145
|
+
case "codespan":
|
|
146
|
+
return colorFn.inlineCodeBg(` ${(token as Tokens.Codespan).text} `);
|
|
147
|
+
case "link": {
|
|
148
|
+
const linkToken = token as Tokens.Link;
|
|
149
|
+
const text = parseInline(linkToken.tokens);
|
|
150
|
+
return `${colorFn.link(text)} ${colorFn.linkUrl(`(${linkToken.href})`)}`;
|
|
151
|
+
}
|
|
152
|
+
case "image": {
|
|
153
|
+
const imgToken = token as Tokens.Image;
|
|
154
|
+
return colorFn.linkUrl(`[image: ${imgToken.text || imgToken.href}]`);
|
|
155
|
+
}
|
|
156
|
+
case "br":
|
|
157
|
+
return "\n";
|
|
158
|
+
case "del":
|
|
159
|
+
return chalk.strikethrough(parseInline((token as Tokens.Del).tokens));
|
|
160
|
+
case "escape":
|
|
161
|
+
return (token as Tokens.Escape).text;
|
|
162
|
+
default:
|
|
163
|
+
// Fallback for unknown inline tokens
|
|
164
|
+
if ("text" in token && typeof token.text === "string") {
|
|
165
|
+
return token.text;
|
|
166
|
+
}
|
|
167
|
+
if ("tokens" in token && Array.isArray(token.tokens)) {
|
|
168
|
+
return parseInline(token.tokens as Tokens.Generic[]);
|
|
169
|
+
}
|
|
170
|
+
return "";
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
.join("");
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const spacing = compact ? "\n" : "\n\n";
|
|
177
|
+
|
|
178
|
+
const renderer: Partial<import("marked").RendererObject> = {
|
|
179
|
+
// Headings: bold + colored with prefix
|
|
180
|
+
heading({ tokens, depth }: Tokens.Heading): string {
|
|
181
|
+
const text = parseInline(tokens);
|
|
182
|
+
const prefix = HEADING_PREFIXES[depth] ?? "·";
|
|
183
|
+
return colorFn.heading.bold(`${prefix} ${text}`) + spacing;
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Paragraphs
|
|
187
|
+
paragraph({ tokens }: Tokens.Paragraph): string {
|
|
188
|
+
return parseInline(tokens) + spacing;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Bold text
|
|
192
|
+
strong({ tokens }: Tokens.Strong): string {
|
|
193
|
+
return colorFn.bold(parseInline(tokens));
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Italic text
|
|
197
|
+
em({ tokens }: Tokens.Em): string {
|
|
198
|
+
return colorFn.italic(parseInline(tokens));
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// Inline code
|
|
202
|
+
codespan({ text }: Tokens.Codespan): string {
|
|
203
|
+
return colorFn.inlineCodeBg(` ${text} `);
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Code blocks with syntax highlighting
|
|
207
|
+
code({ text, lang }: Tokens.Code): string {
|
|
208
|
+
const language = lang || "text";
|
|
209
|
+
|
|
210
|
+
if (syntaxHighlight && codeBlockResolver) {
|
|
211
|
+
// Async mode: return placeholder only - formatting applied after highlighting
|
|
212
|
+
const id = `code-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
213
|
+
pendingCodeBlocks.set(id, { code: text, lang: language });
|
|
214
|
+
return codeBlockResolver(id, text, language);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Sync mode: format immediately
|
|
218
|
+
const languageLabel = colorFn.codeLanguage(language);
|
|
219
|
+
const border = colorFn.codeBlockBorder("│");
|
|
220
|
+
|
|
221
|
+
let highlightedCode: string;
|
|
222
|
+
|
|
223
|
+
if (syntaxHighlight && isHighlighterReady()) {
|
|
224
|
+
// Sync mode: use cached highlighter
|
|
225
|
+
const result = highlightCodeSync(text, { lang: language });
|
|
226
|
+
highlightedCode = result?.output ?? text;
|
|
227
|
+
} else {
|
|
228
|
+
// Fallback: no highlighting
|
|
229
|
+
highlightedCode = text;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Format code block with border
|
|
233
|
+
const lines = highlightedCode.split("\n");
|
|
234
|
+
const formattedLines = lines.map((line) => `${border} ${line}`).join("\n");
|
|
235
|
+
const topBorder = `${colorFn.codeBlockBorder("┌─")} ${languageLabel}`;
|
|
236
|
+
const bottomBorder = colorFn.codeBlockBorder("└─");
|
|
237
|
+
|
|
238
|
+
return `${topBorder}\n${formattedLines}\n${bottomBorder}${spacing}`;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Links
|
|
242
|
+
link({ href, tokens }: Tokens.Link): string {
|
|
243
|
+
const text = parseInline(tokens);
|
|
244
|
+
return `${colorFn.link(text)} ${colorFn.linkUrl(`(${href})`)}`;
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// Images
|
|
248
|
+
image({ href, text }: Tokens.Image): string {
|
|
249
|
+
return colorFn.linkUrl(`[image: ${text || href}]`);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// Blockquotes
|
|
253
|
+
blockquote({ tokens }: Tokens.Blockquote): string {
|
|
254
|
+
// Parse block content, then prefix each line
|
|
255
|
+
const content = tokens
|
|
256
|
+
.map((token) => {
|
|
257
|
+
if (token.type === "paragraph") {
|
|
258
|
+
return parseInline((token as Tokens.Paragraph).tokens);
|
|
259
|
+
}
|
|
260
|
+
return "";
|
|
261
|
+
})
|
|
262
|
+
.join("\n");
|
|
263
|
+
const lines = content.split("\n");
|
|
264
|
+
const prefixed = lines.map((line) => colorFn.blockquote(`│ ${line}`)).join("\n");
|
|
265
|
+
return prefixed + spacing;
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// Unordered lists
|
|
269
|
+
list({ items, ordered, start }: Tokens.List): string {
|
|
270
|
+
const startNum = typeof start === "number" ? start : 1;
|
|
271
|
+
const result = items
|
|
272
|
+
.map((item, index) => {
|
|
273
|
+
const bullet = ordered
|
|
274
|
+
? colorFn.listBullet(`${startNum + index}.`)
|
|
275
|
+
: colorFn.listBullet("•");
|
|
276
|
+
const indent = " ".repeat(0); // Top-level indent
|
|
277
|
+
const content = item.tokens
|
|
278
|
+
.map((token) => {
|
|
279
|
+
if (token.type === "text") {
|
|
280
|
+
// Handle loose list items
|
|
281
|
+
if ("tokens" in token && Array.isArray(token.tokens)) {
|
|
282
|
+
return parseInline(token.tokens as Tokens.Generic[]);
|
|
283
|
+
}
|
|
284
|
+
return (token as Tokens.Text).text;
|
|
285
|
+
}
|
|
286
|
+
if (token.type === "paragraph") {
|
|
287
|
+
return parseInline((token as Tokens.Paragraph).tokens);
|
|
288
|
+
}
|
|
289
|
+
if (token.type === "list") {
|
|
290
|
+
// Nested list - indent
|
|
291
|
+
const nestedItems = (token as Tokens.List).items
|
|
292
|
+
.map((nestedItem, nestedIndex) => {
|
|
293
|
+
const nestedBullet = (token as Tokens.List).ordered
|
|
294
|
+
? colorFn.listBullet(`${nestedIndex + 1}.`)
|
|
295
|
+
: colorFn.listBullet("◦");
|
|
296
|
+
const nestedContent = nestedItem.tokens
|
|
297
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Nested list rendering requires this complexity
|
|
298
|
+
.map((t) => {
|
|
299
|
+
if (t.type === "text") {
|
|
300
|
+
if ("tokens" in t && Array.isArray(t.tokens)) {
|
|
301
|
+
return parseInline(t.tokens as Tokens.Generic[]);
|
|
302
|
+
}
|
|
303
|
+
return (t as Tokens.Text).text;
|
|
304
|
+
}
|
|
305
|
+
if (t.type === "paragraph") {
|
|
306
|
+
return parseInline((t as Tokens.Paragraph).tokens);
|
|
307
|
+
}
|
|
308
|
+
return "";
|
|
309
|
+
})
|
|
310
|
+
.join("");
|
|
311
|
+
return ` ${nestedBullet} ${nestedContent}`;
|
|
312
|
+
})
|
|
313
|
+
.join("\n");
|
|
314
|
+
return `\n${nestedItems}`;
|
|
315
|
+
}
|
|
316
|
+
return "";
|
|
317
|
+
})
|
|
318
|
+
.join("");
|
|
319
|
+
return `${indent}${bullet} ${content}`;
|
|
320
|
+
})
|
|
321
|
+
.join("\n");
|
|
322
|
+
return result + spacing;
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// List items (handled by list)
|
|
326
|
+
listitem({ tokens }: Tokens.ListItem): string {
|
|
327
|
+
return tokens
|
|
328
|
+
.map((token) => {
|
|
329
|
+
if (token.type === "text") {
|
|
330
|
+
if ("tokens" in token && Array.isArray(token.tokens)) {
|
|
331
|
+
return parseInline(token.tokens as Tokens.Generic[]);
|
|
332
|
+
}
|
|
333
|
+
return (token as Tokens.Text).text;
|
|
334
|
+
}
|
|
335
|
+
if (token.type === "paragraph") {
|
|
336
|
+
return parseInline((token as Tokens.Paragraph).tokens);
|
|
337
|
+
}
|
|
338
|
+
return "";
|
|
339
|
+
})
|
|
340
|
+
.join("");
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// Horizontal rules
|
|
344
|
+
hr(): string {
|
|
345
|
+
return colorFn.hr("─".repeat(40)) + spacing;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
// Line breaks
|
|
349
|
+
br(): string {
|
|
350
|
+
return "\n";
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// HTML (strip tags, show as plain text)
|
|
354
|
+
html(token: Tokens.HTML | Tokens.Tag): string {
|
|
355
|
+
// Strip HTML tags for terminal display
|
|
356
|
+
const text = token.text;
|
|
357
|
+
return text.replace(/<[^>]*>/g, "") + (text.includes("\n") ? "" : "");
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// Tables
|
|
361
|
+
table({ header, rows }: Tokens.Table): string {
|
|
362
|
+
const headerCells = header.map((cell) => parseInline(cell.tokens));
|
|
363
|
+
const headerRow = colorFn.bold(headerCells.join(" │ "));
|
|
364
|
+
const separator = colorFn.hr("─".repeat(headerCells.join(" │ ").length));
|
|
365
|
+
const bodyRows = rows.map((row) => row.map((cell) => parseInline(cell.tokens)).join(" │ "));
|
|
366
|
+
return `${headerRow}\n${separator}\n${bodyRows.join("\n")}${spacing}`;
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
// Table row (handled by table)
|
|
370
|
+
tablerow({ text }: Tokens.TableRow): string {
|
|
371
|
+
return text;
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
// Table cell (handled by table)
|
|
375
|
+
tablecell({ tokens }: Tokens.TableCell): string {
|
|
376
|
+
return parseInline(tokens);
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// Strikethrough
|
|
380
|
+
del({ tokens }: Tokens.Del): string {
|
|
381
|
+
return chalk.strikethrough(parseInline(tokens));
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
// Text
|
|
385
|
+
text(token: Tokens.Text | Tokens.Escape): string {
|
|
386
|
+
if ("tokens" in token && Array.isArray(token.tokens)) {
|
|
387
|
+
return parseInline(token.tokens as Tokens.Generic[]);
|
|
388
|
+
}
|
|
389
|
+
return token.text;
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return { renderer, parseInline };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// Public API
|
|
398
|
+
// =============================================================================
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Render markdown to ANSI terminal string (async version)
|
|
402
|
+
*
|
|
403
|
+
* Uses Shiki for syntax highlighting of code blocks.
|
|
404
|
+
*
|
|
405
|
+
* @param markdown - The markdown string to render
|
|
406
|
+
* @param options - Rendering options
|
|
407
|
+
* @returns Promise resolving to ANSI-formatted string
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* const output = await renderMarkdown('# Hello **world**');
|
|
412
|
+
* console.log(output);
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
export async function renderMarkdown(
|
|
416
|
+
markdown: string,
|
|
417
|
+
options: MarkdownRenderOptions = {}
|
|
418
|
+
): Promise<string> {
|
|
419
|
+
// Collect code blocks for async highlighting
|
|
420
|
+
const codeBlocks: Map<string, { code: string; lang: string }> = new Map();
|
|
421
|
+
const colors = { ...DEFAULT_COLORS, ...options.colors };
|
|
422
|
+
const spacing = options.compact ? "\n" : "\n\n";
|
|
423
|
+
|
|
424
|
+
const colorFn = {
|
|
425
|
+
codeBlockBorder: chalk.hex(colors.codeBlockBorder),
|
|
426
|
+
codeLanguage: chalk.hex(colors.codeLanguage).italic,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const { renderer } = createTerminalRenderer(options, (id, code, lang) => {
|
|
430
|
+
codeBlocks.set(id, { code, lang });
|
|
431
|
+
// Return just the placeholder - formatting will be applied after highlighting
|
|
432
|
+
return `__CODE_BLOCK_${id}__`;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const marked = new Marked({ renderer, async: false, gfm: true, breaks: true });
|
|
436
|
+
let result = marked.parse(markdown) as string;
|
|
437
|
+
|
|
438
|
+
// Highlight code blocks asynchronously
|
|
439
|
+
if (options.syntaxHighlight !== false && codeBlocks.size > 0) {
|
|
440
|
+
const highlights = await Promise.all(
|
|
441
|
+
Array.from(codeBlocks.entries()).map(async ([id, { code, lang }]) => {
|
|
442
|
+
const highlighted = await highlightCode(code, { lang });
|
|
443
|
+
return { id, output: highlighted.output, lang };
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// Replace placeholders with highlighted code (with formatting)
|
|
448
|
+
for (const { id, output, lang } of highlights) {
|
|
449
|
+
const border = colorFn.codeBlockBorder("│");
|
|
450
|
+
const lines = output.split("\n");
|
|
451
|
+
const formattedLines = lines.map((line) => `${border} ${line}`).join("\n");
|
|
452
|
+
const topBorder = `${colorFn.codeBlockBorder("┌─")} ${colorFn.codeLanguage(lang)}`;
|
|
453
|
+
const bottomBorder = colorFn.codeBlockBorder("└─");
|
|
454
|
+
const codeBlock = `${topBorder}\n${formattedLines}\n${bottomBorder}${spacing}`;
|
|
455
|
+
|
|
456
|
+
result = result.replace(`__CODE_BLOCK_${id}__`, codeBlock);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Clean up extra newlines
|
|
461
|
+
return result.replace(/\n{3,}/g, "\n\n").trim();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Render markdown to ANSI terminal string (sync version)
|
|
466
|
+
*
|
|
467
|
+
* Uses cached Shiki highlighter if available, otherwise falls back to plain text.
|
|
468
|
+
* Prefer the async version for initial renders.
|
|
469
|
+
*
|
|
470
|
+
* @param markdown - The markdown string to render
|
|
471
|
+
* @param options - Rendering options
|
|
472
|
+
* @returns ANSI-formatted string
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```ts
|
|
476
|
+
* const output = renderMarkdownSync('# Hello **world**');
|
|
477
|
+
* console.log(output);
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
export function renderMarkdownSync(markdown: string, options: MarkdownRenderOptions = {}): string {
|
|
481
|
+
const { renderer } = createTerminalRenderer(options);
|
|
482
|
+
const marked = new Marked({ renderer, async: false, gfm: true, breaks: true });
|
|
483
|
+
const result = marked.parse(markdown) as string;
|
|
484
|
+
return result.replace(/\n{3,}/g, "\n\n").trim();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Render markdown without syntax highlighting (fast sync version)
|
|
489
|
+
*
|
|
490
|
+
* Useful for real-time streaming where highlighting latency is unacceptable.
|
|
491
|
+
*
|
|
492
|
+
* @param markdown - The markdown string to render
|
|
493
|
+
* @param options - Rendering options (syntaxHighlight is ignored)
|
|
494
|
+
* @returns ANSI-formatted string
|
|
495
|
+
*/
|
|
496
|
+
export function renderMarkdownPlain(
|
|
497
|
+
markdown: string,
|
|
498
|
+
options: Omit<MarkdownRenderOptions, "syntaxHighlight"> = {}
|
|
499
|
+
): string {
|
|
500
|
+
return renderMarkdownSync(markdown, { ...options, syntaxHighlight: false });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Check if a string contains markdown formatting
|
|
505
|
+
*
|
|
506
|
+
* @param text - The text to check
|
|
507
|
+
* @returns True if the text appears to contain markdown
|
|
508
|
+
*/
|
|
509
|
+
export function containsMarkdown(text: string): boolean {
|
|
510
|
+
// Check for common markdown patterns
|
|
511
|
+
const patterns = [
|
|
512
|
+
/^#{1,6}\s+/m, // Headers
|
|
513
|
+
/\*\*[^*]+\*\*/, // Bold
|
|
514
|
+
/\*[^*]+\*/, // Italic
|
|
515
|
+
/__[^_]+__/, // Bold (underscore)
|
|
516
|
+
/_[^_]+_/, // Italic (underscore)
|
|
517
|
+
/`[^`]+`/, // Inline code
|
|
518
|
+
/```[\s\S]*?```/, // Code blocks
|
|
519
|
+
/^\s*[-*+]\s+/m, // Unordered lists
|
|
520
|
+
/^\s*\d+\.\s+/m, // Ordered lists
|
|
521
|
+
/\[[^\]]+\]\([^)]+\)/, // Links
|
|
522
|
+
/^>\s+/m, // Blockquotes
|
|
523
|
+
/^---+$/m, // Horizontal rules
|
|
524
|
+
/^\|.*\|$/m, // Tables
|
|
525
|
+
];
|
|
526
|
+
|
|
527
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Strip markdown formatting from text
|
|
532
|
+
*
|
|
533
|
+
* @param markdown - The markdown string
|
|
534
|
+
* @returns Plain text without markdown formatting
|
|
535
|
+
*/
|
|
536
|
+
export function stripMarkdown(markdown: string): string {
|
|
537
|
+
return (
|
|
538
|
+
markdown
|
|
539
|
+
// Remove code blocks
|
|
540
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
541
|
+
// Remove inline code
|
|
542
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
543
|
+
// Remove bold/italic
|
|
544
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
545
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
546
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
547
|
+
.replace(/_([^_]+)_/g, "$1")
|
|
548
|
+
// Remove links
|
|
549
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
550
|
+
// Remove images
|
|
551
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
552
|
+
// Remove headers
|
|
553
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
554
|
+
// Remove blockquotes
|
|
555
|
+
.replace(/^>\s+/gm, "")
|
|
556
|
+
// Remove horizontal rules
|
|
557
|
+
.replace(/^---+$/gm, "")
|
|
558
|
+
// Remove list markers
|
|
559
|
+
.replace(/^\s*[-*+]\s+/gm, "")
|
|
560
|
+
.replace(/^\s*\d+\.\s+/gm, "")
|
|
561
|
+
// Clean up whitespace
|
|
562
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
563
|
+
.trim()
|
|
564
|
+
);
|
|
565
|
+
}
|