@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,679 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DiffView Component (T022)
|
|
3
|
+
*
|
|
4
|
+
* Renders unified diff format with proper styling for added, removed,
|
|
5
|
+
* and context lines. Supports line numbers and file headers.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/Messages/DiffView
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Box, Text } from "ink";
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import type { DiffViewMode } from "../../i18n/index.js";
|
|
14
|
+
import { useTheme } from "../../theme/index.js";
|
|
15
|
+
import { getTerminalWidth } from "../../utils/ui-sizing.js";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimum terminal width required for side-by-side mode.
|
|
23
|
+
* Below this, automatically degrades to unified mode.
|
|
24
|
+
*/
|
|
25
|
+
const SIDE_BY_SIDE_MIN_WIDTH = 100;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Props for the DiffView component.
|
|
29
|
+
*/
|
|
30
|
+
export interface DiffViewProps {
|
|
31
|
+
/** The unified diff content to display */
|
|
32
|
+
readonly diff: string;
|
|
33
|
+
/** Optional file name to show in header */
|
|
34
|
+
readonly fileName?: string;
|
|
35
|
+
/** Show line numbers (old/new) on the left (default: false) */
|
|
36
|
+
readonly showLineNumbers?: boolean;
|
|
37
|
+
/** Reduce spacing for compact display (default: false) */
|
|
38
|
+
readonly compact?: boolean;
|
|
39
|
+
/** Display mode: "unified" or "side-by-side" (default: "unified") */
|
|
40
|
+
readonly mode?: DiffViewMode;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Line type in a diff
|
|
45
|
+
*/
|
|
46
|
+
type DiffLineType = "added" | "removed" | "context" | "hunk" | "header";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A parsed diff line
|
|
50
|
+
*/
|
|
51
|
+
interface ParsedLine {
|
|
52
|
+
readonly type: DiffLineType;
|
|
53
|
+
readonly content: string;
|
|
54
|
+
readonly oldLineNumber?: number;
|
|
55
|
+
readonly newLineNumber?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parsed hunk information
|
|
60
|
+
*/
|
|
61
|
+
interface HunkInfo {
|
|
62
|
+
readonly oldStart: number;
|
|
63
|
+
readonly oldCount: number;
|
|
64
|
+
readonly newStart: number;
|
|
65
|
+
readonly newCount: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Diff Parser
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse hunk header to extract line numbers
|
|
74
|
+
* Format: @@ -oldStart,oldCount +newStart,newCount @@
|
|
75
|
+
*/
|
|
76
|
+
function parseHunkHeader(line: string): HunkInfo | null {
|
|
77
|
+
const match = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
|
|
78
|
+
if (!match || !match[1] || !match[3]) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
oldStart: Number.parseInt(match[1], 10),
|
|
84
|
+
oldCount: Number.parseInt(match[2] ?? "1", 10),
|
|
85
|
+
newStart: Number.parseInt(match[3], 10),
|
|
86
|
+
newCount: Number.parseInt(match[4] ?? "1", 10),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse a unified diff string into structured lines
|
|
92
|
+
*/
|
|
93
|
+
function parseDiff(diff: string): ParsedLine[] {
|
|
94
|
+
const lines = diff.split("\n");
|
|
95
|
+
const result: ParsedLine[] = [];
|
|
96
|
+
|
|
97
|
+
let oldLine = 0;
|
|
98
|
+
let newLine = 0;
|
|
99
|
+
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
// Skip empty lines at end
|
|
102
|
+
if (line === "" && result.length > 0) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// File headers (--- and +++ lines)
|
|
107
|
+
if (line.startsWith("---") || line.startsWith("+++")) {
|
|
108
|
+
result.push({ type: "header", content: line });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Hunk header
|
|
113
|
+
if (line.startsWith("@@")) {
|
|
114
|
+
const hunkInfo = parseHunkHeader(line);
|
|
115
|
+
if (hunkInfo) {
|
|
116
|
+
oldLine = hunkInfo.oldStart;
|
|
117
|
+
newLine = hunkInfo.newStart;
|
|
118
|
+
}
|
|
119
|
+
result.push({ type: "hunk", content: line });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Added line
|
|
124
|
+
if (line.startsWith("+")) {
|
|
125
|
+
result.push({
|
|
126
|
+
type: "added",
|
|
127
|
+
content: line,
|
|
128
|
+
newLineNumber: newLine,
|
|
129
|
+
});
|
|
130
|
+
newLine++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Removed line
|
|
135
|
+
if (line.startsWith("-")) {
|
|
136
|
+
result.push({
|
|
137
|
+
type: "removed",
|
|
138
|
+
content: line,
|
|
139
|
+
oldLineNumber: oldLine,
|
|
140
|
+
});
|
|
141
|
+
oldLine++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Context line (starts with space or is empty context)
|
|
146
|
+
if (line.startsWith(" ") || line === "") {
|
|
147
|
+
result.push({
|
|
148
|
+
type: "context",
|
|
149
|
+
content: line,
|
|
150
|
+
oldLineNumber: oldLine,
|
|
151
|
+
newLineNumber: newLine,
|
|
152
|
+
});
|
|
153
|
+
oldLine++;
|
|
154
|
+
newLine++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Other lines (like "")
|
|
159
|
+
result.push({ type: "context", content: line });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// Sub-components
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* File header component
|
|
171
|
+
*/
|
|
172
|
+
interface FileHeaderProps {
|
|
173
|
+
readonly fileName: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function FileHeader({ fileName }: FileHeaderProps): React.ReactElement {
|
|
177
|
+
const { theme } = useTheme();
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Box
|
|
181
|
+
borderStyle="single"
|
|
182
|
+
borderColor={theme.semantic.border.default}
|
|
183
|
+
borderBottom={false}
|
|
184
|
+
paddingX={1}
|
|
185
|
+
>
|
|
186
|
+
<Text color={theme.semantic.text.secondary} bold>
|
|
187
|
+
📄 {fileName}
|
|
188
|
+
</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Render a single diff line
|
|
195
|
+
*/
|
|
196
|
+
interface DiffLineRendererProps {
|
|
197
|
+
readonly line: ParsedLine;
|
|
198
|
+
readonly showLineNumbers: boolean;
|
|
199
|
+
readonly lineNumberWidth: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Enhanced diff line styling configuration
|
|
204
|
+
*/
|
|
205
|
+
interface DiffLineStyle {
|
|
206
|
+
readonly textColor: string;
|
|
207
|
+
readonly bgColor?: string;
|
|
208
|
+
readonly symbol: string;
|
|
209
|
+
readonly bold: boolean;
|
|
210
|
+
readonly dimContent: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function DiffLineRenderer({
|
|
214
|
+
line,
|
|
215
|
+
showLineNumbers,
|
|
216
|
+
lineNumberWidth,
|
|
217
|
+
}: DiffLineRendererProps): React.ReactElement {
|
|
218
|
+
const { theme } = useTheme();
|
|
219
|
+
const diffColors = theme.semantic.diff;
|
|
220
|
+
|
|
221
|
+
// Enhanced style configuration with better visual distinction
|
|
222
|
+
const getLineStyle = (): DiffLineStyle => {
|
|
223
|
+
switch (line.type) {
|
|
224
|
+
case "added":
|
|
225
|
+
return {
|
|
226
|
+
textColor: theme.colors.success,
|
|
227
|
+
bgColor: diffColors.added,
|
|
228
|
+
symbol: "▶",
|
|
229
|
+
bold: true,
|
|
230
|
+
dimContent: false,
|
|
231
|
+
};
|
|
232
|
+
case "removed":
|
|
233
|
+
return {
|
|
234
|
+
textColor: theme.colors.error,
|
|
235
|
+
bgColor: diffColors.removed,
|
|
236
|
+
symbol: "◀",
|
|
237
|
+
bold: true,
|
|
238
|
+
dimContent: false,
|
|
239
|
+
};
|
|
240
|
+
case "hunk":
|
|
241
|
+
return {
|
|
242
|
+
textColor: theme.colors.info,
|
|
243
|
+
symbol: "≡",
|
|
244
|
+
bold: false,
|
|
245
|
+
dimContent: false,
|
|
246
|
+
};
|
|
247
|
+
case "header":
|
|
248
|
+
return {
|
|
249
|
+
textColor: theme.semantic.text.muted,
|
|
250
|
+
symbol: "",
|
|
251
|
+
bold: false,
|
|
252
|
+
dimContent: true,
|
|
253
|
+
};
|
|
254
|
+
default:
|
|
255
|
+
return {
|
|
256
|
+
textColor: theme.semantic.text.secondary,
|
|
257
|
+
symbol: " ",
|
|
258
|
+
bold: false,
|
|
259
|
+
dimContent: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const style = getLineStyle();
|
|
265
|
+
|
|
266
|
+
// Format line numbers with proper alignment
|
|
267
|
+
const formatLineNumber = (num: number | undefined): string => {
|
|
268
|
+
if (num === undefined) {
|
|
269
|
+
return "".padStart(lineNumberWidth, " ");
|
|
270
|
+
}
|
|
271
|
+
return String(num).padStart(lineNumberWidth, " ");
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Get the content without the prefix character for display
|
|
275
|
+
const displayContent =
|
|
276
|
+
line.type === "added" || line.type === "removed" || line.type === "context"
|
|
277
|
+
? line.content.slice(1)
|
|
278
|
+
: line.content;
|
|
279
|
+
|
|
280
|
+
// Render line numbers section
|
|
281
|
+
const renderLineNumbers = (): React.ReactElement | null => {
|
|
282
|
+
if (!showLineNumbers) return null;
|
|
283
|
+
|
|
284
|
+
if (line.type === "hunk" || line.type === "header") {
|
|
285
|
+
return (
|
|
286
|
+
<Box marginRight={1}>
|
|
287
|
+
<Text color={theme.semantic.text.muted}>{"".padStart(lineNumberWidth * 2 + 2, " ")}</Text>
|
|
288
|
+
<Text color={theme.semantic.border.muted}>│</Text>
|
|
289
|
+
</Box>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (line.type === "added" || line.type === "removed" || line.type === "context") {
|
|
294
|
+
return (
|
|
295
|
+
<Box marginRight={1}>
|
|
296
|
+
{/* Old line number - dimmed for removed lines */}
|
|
297
|
+
<Text
|
|
298
|
+
color={line.type === "removed" ? theme.colors.error : theme.semantic.text.muted}
|
|
299
|
+
dimColor={line.type !== "removed"}
|
|
300
|
+
>
|
|
301
|
+
{formatLineNumber(line.oldLineNumber)}
|
|
302
|
+
</Text>
|
|
303
|
+
<Text color={theme.semantic.border.muted}> </Text>
|
|
304
|
+
{/* New line number - highlighted for added lines */}
|
|
305
|
+
<Text
|
|
306
|
+
color={line.type === "added" ? theme.colors.success : theme.semantic.text.muted}
|
|
307
|
+
dimColor={line.type !== "added"}
|
|
308
|
+
>
|
|
309
|
+
{formatLineNumber(line.newLineNumber)}
|
|
310
|
+
</Text>
|
|
311
|
+
<Text color={theme.semantic.border.muted}>│</Text>
|
|
312
|
+
</Box>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Render the symbol prefix with enhanced visibility
|
|
320
|
+
const renderSymbol = (): React.ReactElement | null => {
|
|
321
|
+
if (line.type === "header") return null;
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<Text color={style.textColor} bold={style.bold}>
|
|
325
|
+
{style.symbol}
|
|
326
|
+
</Text>
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<Box>
|
|
332
|
+
{renderLineNumbers()}
|
|
333
|
+
<Box flexGrow={1}>
|
|
334
|
+
{renderSymbol()}
|
|
335
|
+
{style.symbol && <Text> </Text>}
|
|
336
|
+
<Text
|
|
337
|
+
color={style.textColor}
|
|
338
|
+
bold={style.bold && (line.type === "added" || line.type === "removed")}
|
|
339
|
+
dimColor={style.dimContent}
|
|
340
|
+
>
|
|
341
|
+
{displayContent}
|
|
342
|
+
</Text>
|
|
343
|
+
</Box>
|
|
344
|
+
</Box>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// =============================================================================
|
|
349
|
+
// Side-by-Side Types and Helpers
|
|
350
|
+
// =============================================================================
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A paired line for side-by-side display.
|
|
354
|
+
* Left is the old version, right is the new version.
|
|
355
|
+
* Either side can be empty (for additions/deletions).
|
|
356
|
+
*/
|
|
357
|
+
interface SideBySideLine {
|
|
358
|
+
readonly left: ParsedLine | null;
|
|
359
|
+
readonly right: ParsedLine | null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Convert parsed diff lines to side-by-side pairs.
|
|
364
|
+
* Matches removed and added lines, and preserves context on both sides.
|
|
365
|
+
*/
|
|
366
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Diff pairing logic requires multiple state transitions
|
|
367
|
+
function toSideBySidePairs(lines: ParsedLine[]): SideBySideLine[] {
|
|
368
|
+
const result: SideBySideLine[] = [];
|
|
369
|
+
let i = 0;
|
|
370
|
+
|
|
371
|
+
while (i < lines.length) {
|
|
372
|
+
const line = lines[i];
|
|
373
|
+
if (!line) break;
|
|
374
|
+
|
|
375
|
+
// Headers and hunks span both columns
|
|
376
|
+
if (line.type === "header" || line.type === "hunk") {
|
|
377
|
+
result.push({ left: line, right: line });
|
|
378
|
+
i++;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Context lines appear on both sides
|
|
383
|
+
if (line.type === "context") {
|
|
384
|
+
result.push({ left: line, right: line });
|
|
385
|
+
i++;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Collect consecutive removed lines
|
|
390
|
+
const removedLines: ParsedLine[] = [];
|
|
391
|
+
while (i < lines.length) {
|
|
392
|
+
const current = lines[i];
|
|
393
|
+
if (!current || current.type !== "removed") break;
|
|
394
|
+
removedLines.push(current);
|
|
395
|
+
i++;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Collect consecutive added lines
|
|
399
|
+
const addedLines: ParsedLine[] = [];
|
|
400
|
+
while (i < lines.length) {
|
|
401
|
+
const current = lines[i];
|
|
402
|
+
if (!current || current.type !== "added") break;
|
|
403
|
+
addedLines.push(current);
|
|
404
|
+
i++;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Pair removed and added lines
|
|
408
|
+
const maxLen = Math.max(removedLines.length, addedLines.length);
|
|
409
|
+
for (let j = 0; j < maxLen; j++) {
|
|
410
|
+
const leftLine = removedLines[j];
|
|
411
|
+
const rightLine = addedLines[j];
|
|
412
|
+
result.push({
|
|
413
|
+
left: leftLine ?? null,
|
|
414
|
+
right: rightLine ?? null,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// =============================================================================
|
|
423
|
+
// Side-by-Side Renderer
|
|
424
|
+
// =============================================================================
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Render a single side-by-side row.
|
|
428
|
+
*/
|
|
429
|
+
interface SideBySideRowProps {
|
|
430
|
+
readonly pair: SideBySideLine;
|
|
431
|
+
readonly showLineNumbers: boolean;
|
|
432
|
+
readonly lineNumberWidth: number;
|
|
433
|
+
readonly columnWidth: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function SideBySideRow({
|
|
437
|
+
pair,
|
|
438
|
+
showLineNumbers,
|
|
439
|
+
lineNumberWidth,
|
|
440
|
+
columnWidth,
|
|
441
|
+
}: SideBySideRowProps): React.ReactElement {
|
|
442
|
+
const { theme } = useTheme();
|
|
443
|
+
|
|
444
|
+
// Format line number
|
|
445
|
+
const formatLineNumber = (num: number | undefined): string => {
|
|
446
|
+
if (num === undefined) {
|
|
447
|
+
return "".padStart(lineNumberWidth, " ");
|
|
448
|
+
}
|
|
449
|
+
return String(num).padStart(lineNumberWidth, " ");
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Get content (remove prefix char for display)
|
|
453
|
+
const getContent = (line: ParsedLine | null): string => {
|
|
454
|
+
if (!line) return "";
|
|
455
|
+
if (line.type === "added" || line.type === "removed" || line.type === "context") {
|
|
456
|
+
return line.content.slice(1);
|
|
457
|
+
}
|
|
458
|
+
return line.content;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Truncate content to fit column width
|
|
462
|
+
const truncateContent = (content: string, maxWidth: number): string => {
|
|
463
|
+
if (content.length <= maxWidth) {
|
|
464
|
+
return content;
|
|
465
|
+
}
|
|
466
|
+
return `${content.slice(0, maxWidth - 1)}…`;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// For headers/hunks, span both columns with enhanced styling
|
|
470
|
+
if (pair.left?.type === "header" || pair.left?.type === "hunk") {
|
|
471
|
+
const isHunk = pair.left.type === "hunk";
|
|
472
|
+
return (
|
|
473
|
+
<Box>
|
|
474
|
+
{isHunk && (
|
|
475
|
+
<Text color={theme.colors.info} bold>
|
|
476
|
+
≡{" "}
|
|
477
|
+
</Text>
|
|
478
|
+
)}
|
|
479
|
+
<Text color={isHunk ? theme.colors.info : theme.semantic.text.muted} dimColor={!isHunk}>
|
|
480
|
+
{pair.left.content}
|
|
481
|
+
</Text>
|
|
482
|
+
</Box>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Calculate content width per column
|
|
487
|
+
const lineNumSpace = showLineNumbers ? lineNumberWidth + 2 : 0;
|
|
488
|
+
const contentWidth = columnWidth - lineNumSpace - 3; // -3 for symbol, space, and padding
|
|
489
|
+
|
|
490
|
+
// Render left side (old/removed) with enhanced colors
|
|
491
|
+
const leftContent = getContent(pair.left);
|
|
492
|
+
const leftColor =
|
|
493
|
+
pair.left?.type === "removed" ? theme.colors.error : theme.semantic.text.secondary;
|
|
494
|
+
const leftSymbol = pair.left?.type === "removed" ? "◀" : pair.left ? " " : " ";
|
|
495
|
+
const leftIsBold = pair.left?.type === "removed";
|
|
496
|
+
|
|
497
|
+
// Render right side (new/added) with enhanced colors
|
|
498
|
+
const rightContent = getContent(pair.right);
|
|
499
|
+
const rightColor =
|
|
500
|
+
pair.right?.type === "added" ? theme.colors.success : theme.semantic.text.secondary;
|
|
501
|
+
const rightSymbol = pair.right?.type === "added" ? "▶" : pair.right ? " " : " ";
|
|
502
|
+
const rightIsBold = pair.right?.type === "added";
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
<Box>
|
|
506
|
+
{/* Left column (old/removed) */}
|
|
507
|
+
<Box width={columnWidth}>
|
|
508
|
+
{showLineNumbers && (
|
|
509
|
+
<Text
|
|
510
|
+
color={pair.left?.type === "removed" ? theme.colors.error : theme.semantic.text.muted}
|
|
511
|
+
dimColor={pair.left?.type !== "removed"}
|
|
512
|
+
>
|
|
513
|
+
{formatLineNumber(pair.left?.oldLineNumber)}
|
|
514
|
+
</Text>
|
|
515
|
+
)}
|
|
516
|
+
{showLineNumbers && <Text color={theme.semantic.border.muted}>│</Text>}
|
|
517
|
+
<Text color={leftColor} bold={leftIsBold}>
|
|
518
|
+
{leftSymbol}{" "}
|
|
519
|
+
</Text>
|
|
520
|
+
<Text color={leftColor} bold={leftIsBold}>
|
|
521
|
+
{truncateContent(leftContent, contentWidth)}
|
|
522
|
+
</Text>
|
|
523
|
+
</Box>
|
|
524
|
+
|
|
525
|
+
{/* Center divider */}
|
|
526
|
+
<Text color={theme.semantic.border.default}>║</Text>
|
|
527
|
+
|
|
528
|
+
{/* Right column (new/added) */}
|
|
529
|
+
<Box width={columnWidth}>
|
|
530
|
+
{showLineNumbers && (
|
|
531
|
+
<Text
|
|
532
|
+
color={pair.right?.type === "added" ? theme.colors.success : theme.semantic.text.muted}
|
|
533
|
+
dimColor={pair.right?.type !== "added"}
|
|
534
|
+
>
|
|
535
|
+
{formatLineNumber(pair.right?.newLineNumber)}
|
|
536
|
+
</Text>
|
|
537
|
+
)}
|
|
538
|
+
{showLineNumbers && <Text color={theme.semantic.border.muted}>│</Text>}
|
|
539
|
+
<Text color={rightColor} bold={rightIsBold}>
|
|
540
|
+
{rightSymbol}{" "}
|
|
541
|
+
</Text>
|
|
542
|
+
<Text color={rightColor} bold={rightIsBold}>
|
|
543
|
+
{truncateContent(rightContent, contentWidth)}
|
|
544
|
+
</Text>
|
|
545
|
+
</Box>
|
|
546
|
+
</Box>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// =============================================================================
|
|
551
|
+
// Main Component
|
|
552
|
+
// =============================================================================
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* DiffView displays a diff with proper styling.
|
|
556
|
+
* Supports both unified and side-by-side display modes.
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```tsx
|
|
560
|
+
* <DiffView
|
|
561
|
+
* diff={unifiedDiff}
|
|
562
|
+
* fileName="src/index.ts"
|
|
563
|
+
* showLineNumbers
|
|
564
|
+
* mode="side-by-side"
|
|
565
|
+
* />
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
export function DiffView({
|
|
569
|
+
diff,
|
|
570
|
+
fileName,
|
|
571
|
+
showLineNumbers = false,
|
|
572
|
+
compact = false,
|
|
573
|
+
mode = "unified",
|
|
574
|
+
}: DiffViewProps): React.ReactElement {
|
|
575
|
+
const { theme } = useTheme();
|
|
576
|
+
|
|
577
|
+
// Get terminal width for auto-degradation and column calculation
|
|
578
|
+
const terminalWidth = getTerminalWidth();
|
|
579
|
+
|
|
580
|
+
// Auto-degrade to unified mode if terminal is too narrow
|
|
581
|
+
const effectiveMode = terminalWidth < SIDE_BY_SIDE_MIN_WIDTH ? "unified" : mode;
|
|
582
|
+
|
|
583
|
+
// Parse the diff
|
|
584
|
+
const parsedLines = useMemo(() => parseDiff(diff), [diff]);
|
|
585
|
+
|
|
586
|
+
// Calculate line number width based on max line number
|
|
587
|
+
const lineNumberWidth = useMemo(() => {
|
|
588
|
+
let maxLineNumber = 0;
|
|
589
|
+
for (const line of parsedLines) {
|
|
590
|
+
if (line.oldLineNumber !== undefined && line.oldLineNumber > maxLineNumber) {
|
|
591
|
+
maxLineNumber = line.oldLineNumber;
|
|
592
|
+
}
|
|
593
|
+
if (line.newLineNumber !== undefined && line.newLineNumber > maxLineNumber) {
|
|
594
|
+
maxLineNumber = line.newLineNumber;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return Math.max(3, String(maxLineNumber).length);
|
|
598
|
+
}, [parsedLines]);
|
|
599
|
+
|
|
600
|
+
// Filter out file headers if fileName is provided (we'll show our own)
|
|
601
|
+
const displayLines = fileName
|
|
602
|
+
? parsedLines.filter((line) => line.type !== "header")
|
|
603
|
+
: parsedLines;
|
|
604
|
+
|
|
605
|
+
// Generate stable keys for each line based on type and line numbers
|
|
606
|
+
const getLineKey = (line: ParsedLine, position: number): string => {
|
|
607
|
+
const oldNum = line.oldLineNumber ?? "x";
|
|
608
|
+
const newNum = line.newLineNumber ?? "x";
|
|
609
|
+
return `${line.type}-${oldNum}-${newNum}-${position}`;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Calculate column width for side-by-side mode
|
|
613
|
+
// Account for: borders (4), divider (1), padding (2)
|
|
614
|
+
const columnWidth = Math.floor((terminalWidth - 7) / 2);
|
|
615
|
+
|
|
616
|
+
// Convert to side-by-side pairs if needed
|
|
617
|
+
const sideBySidePairs = useMemo(
|
|
618
|
+
() => (effectiveMode === "side-by-side" ? toSideBySidePairs(displayLines) : []),
|
|
619
|
+
[displayLines, effectiveMode]
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Render unified mode
|
|
623
|
+
if (effectiveMode === "unified") {
|
|
624
|
+
return (
|
|
625
|
+
<Box flexDirection="column">
|
|
626
|
+
{fileName && <FileHeader fileName={fileName} />}
|
|
627
|
+
<Box
|
|
628
|
+
flexDirection="column"
|
|
629
|
+
borderStyle="single"
|
|
630
|
+
borderColor={theme.semantic.border.default}
|
|
631
|
+
paddingX={compact ? 0 : 1}
|
|
632
|
+
paddingY={compact ? 0 : 0}
|
|
633
|
+
>
|
|
634
|
+
{displayLines.map((line, position) => (
|
|
635
|
+
<DiffLineRenderer
|
|
636
|
+
key={getLineKey(line, position)}
|
|
637
|
+
line={line}
|
|
638
|
+
showLineNumbers={showLineNumbers}
|
|
639
|
+
lineNumberWidth={lineNumberWidth}
|
|
640
|
+
/>
|
|
641
|
+
))}
|
|
642
|
+
</Box>
|
|
643
|
+
</Box>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Generate stable key for side-by-side rows
|
|
648
|
+
const getSideBySideKey = (pair: SideBySideLine, position: number): string => {
|
|
649
|
+
const leftNum = pair.left?.oldLineNumber ?? pair.left?.newLineNumber ?? "x";
|
|
650
|
+
const rightNum = pair.right?.newLineNumber ?? pair.right?.oldLineNumber ?? "x";
|
|
651
|
+
const leftType = pair.left?.type ?? "empty";
|
|
652
|
+
const rightType = pair.right?.type ?? "empty";
|
|
653
|
+
return `sbs-${leftType}-${leftNum}-${rightType}-${rightNum}-${position}`;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Render side-by-side mode
|
|
657
|
+
return (
|
|
658
|
+
<Box flexDirection="column">
|
|
659
|
+
{fileName && <FileHeader fileName={fileName} />}
|
|
660
|
+
<Box
|
|
661
|
+
flexDirection="column"
|
|
662
|
+
borderStyle="single"
|
|
663
|
+
borderColor={theme.semantic.border.default}
|
|
664
|
+
paddingX={compact ? 0 : 1}
|
|
665
|
+
paddingY={compact ? 0 : 0}
|
|
666
|
+
>
|
|
667
|
+
{sideBySidePairs.map((pair, index) => (
|
|
668
|
+
<SideBySideRow
|
|
669
|
+
key={getSideBySideKey(pair, index)}
|
|
670
|
+
pair={pair}
|
|
671
|
+
showLineNumbers={showLineNumbers}
|
|
672
|
+
lineNumberWidth={lineNumberWidth}
|
|
673
|
+
columnWidth={columnWidth}
|
|
674
|
+
/>
|
|
675
|
+
))}
|
|
676
|
+
</Box>
|
|
677
|
+
</Box>
|
|
678
|
+
);
|
|
679
|
+
}
|