@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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Input Highlight Utilities
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
applyHighlightStyle,
|
|
7
|
+
findSegmentAtCursor,
|
|
8
|
+
getHighlightStyleDescription,
|
|
9
|
+
type HighlightSegment,
|
|
10
|
+
highlightInput,
|
|
11
|
+
parseHighlights,
|
|
12
|
+
splitSegmentAtCursor,
|
|
13
|
+
} from "../highlight.js";
|
|
14
|
+
|
|
15
|
+
describe("highlight", () => {
|
|
16
|
+
describe("parseHighlights", () => {
|
|
17
|
+
it("returns empty segments for empty string", () => {
|
|
18
|
+
const result = parseHighlights("");
|
|
19
|
+
expect(result.segments).toHaveLength(0);
|
|
20
|
+
expect(result.hasHighlights).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns plain text when no highlights", () => {
|
|
24
|
+
const result = parseHighlights("plain text here");
|
|
25
|
+
expect(result.segments).toHaveLength(1);
|
|
26
|
+
expect(result.segments[0]?.text).toBe("plain text here");
|
|
27
|
+
expect(result.segments[0]?.type).toBeUndefined();
|
|
28
|
+
expect(result.hasHighlights).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("detects @mentions", () => {
|
|
32
|
+
const result = parseHighlights("Check @file.ts");
|
|
33
|
+
expect(result.hasHighlights).toBe(true);
|
|
34
|
+
const mention = result.segments.find((s: HighlightSegment) => s.type === "mention");
|
|
35
|
+
expect(mention?.text).toBe("@file.ts");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("detects slash commands", () => {
|
|
39
|
+
const result = parseHighlights("Use /help command");
|
|
40
|
+
expect(result.hasHighlights).toBe(true);
|
|
41
|
+
const command = result.segments.find((s: HighlightSegment) => s.type === "command");
|
|
42
|
+
expect(command?.text).toBe("/help");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("detects URLs", () => {
|
|
46
|
+
const result = parseHighlights("Visit https://example.com");
|
|
47
|
+
expect(result.hasHighlights).toBe(true);
|
|
48
|
+
const url = result.segments.find((s: HighlightSegment) => s.type === "url");
|
|
49
|
+
expect(url?.text).toBe("https://example.com");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("detects http URLs", () => {
|
|
53
|
+
const result = parseHighlights("Visit http://example.com");
|
|
54
|
+
const url = result.segments.find((s: HighlightSegment) => s.type === "url");
|
|
55
|
+
expect(url?.text).toBe("http://example.com");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("detects inline code", () => {
|
|
59
|
+
const result = parseHighlights("Run `npm install`");
|
|
60
|
+
expect(result.hasHighlights).toBe(true);
|
|
61
|
+
const code = result.segments.find((s: HighlightSegment) => s.type === "code");
|
|
62
|
+
expect(code?.text).toBe("`npm install`");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("handles multiple highlights", () => {
|
|
66
|
+
const result = parseHighlights("Check @file.ts with /help");
|
|
67
|
+
expect(result.hasHighlights).toBe(true);
|
|
68
|
+
const mention = result.segments.find((s: HighlightSegment) => s.type === "mention");
|
|
69
|
+
const command = result.segments.find((s: HighlightSegment) => s.type === "command");
|
|
70
|
+
expect(mention?.text).toBe("@file.ts");
|
|
71
|
+
expect(command?.text).toBe("/help");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("preserves text order", () => {
|
|
75
|
+
const result = parseHighlights("start @mid end");
|
|
76
|
+
expect(result.segments).toHaveLength(3);
|
|
77
|
+
expect(result.segments[0]?.text).toBe("start ");
|
|
78
|
+
expect(result.segments[1]?.text).toBe("@mid");
|
|
79
|
+
expect(result.segments[2]?.text).toBe(" end");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("sets correct start and end positions", () => {
|
|
83
|
+
const result = parseHighlights("prefix @file suffix");
|
|
84
|
+
const mention = result.segments.find((s: HighlightSegment) => s.type === "mention");
|
|
85
|
+
expect(mention?.start).toBe(7);
|
|
86
|
+
expect(mention?.end).toBe(12);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("applyHighlightStyle", () => {
|
|
91
|
+
it("returns plain text for undefined type", () => {
|
|
92
|
+
const result = applyHighlightStyle("text");
|
|
93
|
+
expect(result).toBe("text");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("applies style to mentions and returns text", () => {
|
|
97
|
+
const result = applyHighlightStyle("@file", "mention");
|
|
98
|
+
expect(result).toContain("@file");
|
|
99
|
+
// Result should contain the original text (chalk may or may not add ANSI codes depending on env)
|
|
100
|
+
expect(typeof result).toBe("string");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("applies style to commands and returns text", () => {
|
|
104
|
+
const result = applyHighlightStyle("/help", "command");
|
|
105
|
+
expect(result).toContain("/help");
|
|
106
|
+
expect(typeof result).toBe("string");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("applies style to URLs and returns text", () => {
|
|
110
|
+
const result = applyHighlightStyle("https://x.com", "url");
|
|
111
|
+
expect(result).toContain("https://x.com");
|
|
112
|
+
expect(typeof result).toBe("string");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("applies style to code and returns text", () => {
|
|
116
|
+
const result = applyHighlightStyle("`code`", "code");
|
|
117
|
+
expect(result).toContain("`code`");
|
|
118
|
+
expect(typeof result).toBe("string");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("getHighlightStyleDescription", () => {
|
|
123
|
+
it('returns "plain" for undefined type', () => {
|
|
124
|
+
expect(getHighlightStyleDescription()).toBe("plain");
|
|
125
|
+
expect(getHighlightStyleDescription(undefined)).toBe("plain");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns correct descriptions", () => {
|
|
129
|
+
expect(getHighlightStyleDescription("mention")).toBe("cyan");
|
|
130
|
+
expect(getHighlightStyleDescription("command")).toBe("green");
|
|
131
|
+
expect(getHighlightStyleDescription("url")).toBe("blue underline");
|
|
132
|
+
expect(getHighlightStyleDescription("code")).toBe("dim");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("highlightInput", () => {
|
|
137
|
+
it("returns styled string for input with highlights", () => {
|
|
138
|
+
const result = highlightInput("@file /help");
|
|
139
|
+
expect(result).toContain("@file");
|
|
140
|
+
expect(result).toContain("/help");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns plain string for input without highlights", () => {
|
|
144
|
+
const result = highlightInput("plain text");
|
|
145
|
+
expect(result).toBe("plain text");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("handles empty string", () => {
|
|
149
|
+
expect(highlightInput("")).toBe("");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("findSegmentAtCursor", () => {
|
|
154
|
+
const segments: HighlightSegment[] = [
|
|
155
|
+
{ text: "start ", start: 0, end: 6 },
|
|
156
|
+
{ text: "@file", type: "mention", start: 6, end: 11 },
|
|
157
|
+
{ text: " end", start: 11, end: 15 },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
it("finds segment at cursor position", () => {
|
|
161
|
+
const seg = findSegmentAtCursor(segments, 7);
|
|
162
|
+
expect(seg?.text).toBe("@file");
|
|
163
|
+
expect(seg?.type).toBe("mention");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("finds segment at start boundary", () => {
|
|
167
|
+
const seg = findSegmentAtCursor(segments, 6);
|
|
168
|
+
expect(seg?.text).toBe("@file");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns undefined for position beyond segments", () => {
|
|
172
|
+
const seg = findSegmentAtCursor(segments, 100);
|
|
173
|
+
expect(seg).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("handles empty segments array", () => {
|
|
177
|
+
const seg = findSegmentAtCursor([], 5);
|
|
178
|
+
expect(seg).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("splitSegmentAtCursor", () => {
|
|
183
|
+
const segment: HighlightSegment = {
|
|
184
|
+
text: "hello",
|
|
185
|
+
start: 0,
|
|
186
|
+
end: 5,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
it("splits segment at cursor position", () => {
|
|
190
|
+
const result = splitSegmentAtCursor(segment, 2);
|
|
191
|
+
expect(result.before).toBe("he");
|
|
192
|
+
expect(result.cursorChar).toBe("l");
|
|
193
|
+
expect(result.after).toBe("lo");
|
|
194
|
+
expect(result.localPosition).toBe(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("handles cursor at start", () => {
|
|
198
|
+
const result = splitSegmentAtCursor(segment, 0);
|
|
199
|
+
expect(result.before).toBe("");
|
|
200
|
+
expect(result.cursorChar).toBe("h");
|
|
201
|
+
expect(result.after).toBe("ello");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("handles cursor at end", () => {
|
|
205
|
+
const result = splitSegmentAtCursor(segment, 5);
|
|
206
|
+
expect(result.before).toBe("hello");
|
|
207
|
+
expect(result.cursorChar).toBe(" "); // Default when past end
|
|
208
|
+
expect(result.after).toBe("");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("handles segment with non-zero start", () => {
|
|
212
|
+
const seg: HighlightSegment = { text: "world", start: 10, end: 15 };
|
|
213
|
+
const result = splitSegmentAtCursor(seg, 12);
|
|
214
|
+
expect(result.before).toBe("wo");
|
|
215
|
+
expect(result.cursorChar).toBe("r");
|
|
216
|
+
expect(result.localPosition).toBe(2);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Slash Command Parsing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Tests slash command parsing functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { parseSlashCommand } from "../slash-command-utils.js";
|
|
9
|
+
|
|
10
|
+
describe("parseSlashCommand", () => {
|
|
11
|
+
it("should parse simple command without arguments", () => {
|
|
12
|
+
const result = parseSlashCommand("/help");
|
|
13
|
+
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
name: "help",
|
|
16
|
+
args: [],
|
|
17
|
+
raw: "/help",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should parse command with single argument", () => {
|
|
22
|
+
const result = parseSlashCommand("/search query");
|
|
23
|
+
|
|
24
|
+
expect(result).toEqual({
|
|
25
|
+
name: "search",
|
|
26
|
+
args: ["query"],
|
|
27
|
+
raw: "/search query",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should parse command with multiple arguments", () => {
|
|
32
|
+
const result = parseSlashCommand("/config set value true");
|
|
33
|
+
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
name: "config",
|
|
36
|
+
args: ["set", "value", "true"],
|
|
37
|
+
raw: "/config set value true",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should handle double-quoted arguments with spaces", () => {
|
|
42
|
+
const result = parseSlashCommand('/search "hello world"');
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
name: "search",
|
|
46
|
+
args: ["hello world"],
|
|
47
|
+
raw: '/search "hello world"',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should handle single-quoted arguments with spaces", () => {
|
|
52
|
+
const result = parseSlashCommand("/search 'hello world'");
|
|
53
|
+
|
|
54
|
+
expect(result).toEqual({
|
|
55
|
+
name: "search",
|
|
56
|
+
args: ["hello world"],
|
|
57
|
+
raw: "/search 'hello world'",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle mixed quoted and unquoted arguments", () => {
|
|
62
|
+
const result = parseSlashCommand('/filter "user name" --type admin');
|
|
63
|
+
|
|
64
|
+
expect(result.args).toEqual(["user name", "--type", "admin"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should handle escaped quotes within quoted strings", () => {
|
|
68
|
+
const result = parseSlashCommand('/echo "say \\"hello\\""');
|
|
69
|
+
|
|
70
|
+
expect(result.args).toEqual(['say "hello"']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should handle escaped backslash", () => {
|
|
74
|
+
const result = parseSlashCommand("/path C:\\\\Users\\\\test");
|
|
75
|
+
|
|
76
|
+
expect(result.args).toEqual(["C:\\Users\\test"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should trim whitespace from input", () => {
|
|
80
|
+
const result = parseSlashCommand(" /help ");
|
|
81
|
+
|
|
82
|
+
expect(result.name).toBe("help");
|
|
83
|
+
expect(result.raw).toBe("/help");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should handle multiple spaces between arguments", () => {
|
|
87
|
+
const result = parseSlashCommand("/cmd arg1 arg2");
|
|
88
|
+
|
|
89
|
+
expect(result.args).toEqual(["arg1", "arg2"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should handle command with only spaces after name", () => {
|
|
93
|
+
const result = parseSlashCommand("/help ");
|
|
94
|
+
|
|
95
|
+
expect(result.name).toBe("help");
|
|
96
|
+
expect(result.args).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should handle flag-style arguments", () => {
|
|
100
|
+
const result = parseSlashCommand("/list --all -v --format=json");
|
|
101
|
+
|
|
102
|
+
expect(result.args).toEqual(["--all", "-v", "--format=json"]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Highlight Utilities (T009-HL)
|
|
3
|
+
*
|
|
4
|
+
* Provides text highlighting for special patterns in user input:
|
|
5
|
+
* - @mentions: File paths and folder references
|
|
6
|
+
* - Slash commands: /mode, /model, /help, etc.
|
|
7
|
+
* - URLs: http:// and https:// links
|
|
8
|
+
* - Inline code: `backtick-wrapped` code
|
|
9
|
+
*
|
|
10
|
+
* @module tui/components/Input/highlight
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Types of highlightable patterns.
|
|
21
|
+
*/
|
|
22
|
+
export type HighlightType = "mention" | "command" | "url" | "code";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A segment of highlighted text.
|
|
26
|
+
*/
|
|
27
|
+
export interface HighlightSegment {
|
|
28
|
+
/** The text content */
|
|
29
|
+
readonly text: string;
|
|
30
|
+
/** The type of highlight, or undefined for plain text */
|
|
31
|
+
readonly type?: HighlightType;
|
|
32
|
+
/** Start index in original string */
|
|
33
|
+
readonly start: number;
|
|
34
|
+
/** End index in original string (exclusive) */
|
|
35
|
+
readonly end: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Result of parsing input for highlights.
|
|
40
|
+
*/
|
|
41
|
+
export interface HighlightResult {
|
|
42
|
+
/** Array of text segments with highlight info */
|
|
43
|
+
readonly segments: readonly HighlightSegment[];
|
|
44
|
+
/** Whether any highlights were found */
|
|
45
|
+
readonly hasHighlights: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Patterns
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Caching
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* LRU cache for highlight results to avoid recomputing on every keystroke.
|
|
56
|
+
* Limited to 50 entries to prevent memory bloat.
|
|
57
|
+
*/
|
|
58
|
+
const HIGHLIGHT_CACHE_SIZE = 50;
|
|
59
|
+
const highlightCache = new Map<string, HighlightResult>();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add result to cache with LRU eviction.
|
|
63
|
+
*/
|
|
64
|
+
function cacheResult(input: string, result: HighlightResult): void {
|
|
65
|
+
// Evict oldest entry if at capacity
|
|
66
|
+
if (highlightCache.size >= HIGHLIGHT_CACHE_SIZE) {
|
|
67
|
+
const firstKey = highlightCache.keys().next().value;
|
|
68
|
+
if (firstKey !== undefined) {
|
|
69
|
+
highlightCache.delete(firstKey);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
highlightCache.set(input, result);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get cached result if available.
|
|
77
|
+
*/
|
|
78
|
+
function getCachedResult(input: string): HighlightResult | undefined {
|
|
79
|
+
const cached = highlightCache.get(input);
|
|
80
|
+
if (cached) {
|
|
81
|
+
// Move to end for LRU (delete and re-add)
|
|
82
|
+
highlightCache.delete(input);
|
|
83
|
+
highlightCache.set(input, cached);
|
|
84
|
+
}
|
|
85
|
+
return cached;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Patterns
|
|
90
|
+
// =============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Pattern definitions for highlighting.
|
|
94
|
+
* Order matters - patterns are matched in priority order.
|
|
95
|
+
*/
|
|
96
|
+
const HIGHLIGHT_PATTERNS: readonly { type: HighlightType; pattern: RegExp }[] = [
|
|
97
|
+
// URLs: Match http:// or https:// followed by non-whitespace
|
|
98
|
+
{
|
|
99
|
+
type: "url",
|
|
100
|
+
pattern: /https?:\/\/[^\s]+/g,
|
|
101
|
+
},
|
|
102
|
+
// Inline code: Backtick-wrapped text (non-greedy, no nested backticks)
|
|
103
|
+
{
|
|
104
|
+
type: "code",
|
|
105
|
+
pattern: /`[^`]+`/g,
|
|
106
|
+
},
|
|
107
|
+
// Slash commands: /word at start of input or after whitespace
|
|
108
|
+
{
|
|
109
|
+
type: "command",
|
|
110
|
+
pattern: /(?:^|\s)(\/[a-zA-Z][a-zA-Z0-9_-]*)/g,
|
|
111
|
+
},
|
|
112
|
+
// @mentions: @path/to/file or @filename.ext
|
|
113
|
+
{
|
|
114
|
+
type: "mention",
|
|
115
|
+
pattern: /@[\w./-]+/g,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// Core Functions
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Find all highlight matches in the input text.
|
|
125
|
+
* Returns matches sorted by start position.
|
|
126
|
+
*/
|
|
127
|
+
function findMatches(
|
|
128
|
+
input: string
|
|
129
|
+
): Array<{ type: HighlightType; start: number; end: number; text: string }> {
|
|
130
|
+
const matches: Array<{ type: HighlightType; start: number; end: number; text: string }> = [];
|
|
131
|
+
|
|
132
|
+
for (const { type, pattern } of HIGHLIGHT_PATTERNS) {
|
|
133
|
+
// Reset regex state
|
|
134
|
+
pattern.lastIndex = 0;
|
|
135
|
+
let match = pattern.exec(input);
|
|
136
|
+
|
|
137
|
+
while (match !== null) {
|
|
138
|
+
// For command pattern, we capture group 1 (the actual command without leading whitespace)
|
|
139
|
+
const text = type === "command" && match[1] ? match[1] : match[0];
|
|
140
|
+
const start =
|
|
141
|
+
type === "command" && match[1] ? match.index + match[0].indexOf(match[1]) : match.index;
|
|
142
|
+
const end = start + text.length;
|
|
143
|
+
|
|
144
|
+
matches.push({ type, start, end, text });
|
|
145
|
+
match = pattern.exec(input);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Sort by start position
|
|
150
|
+
return matches.sort((a, b) => a.start - b.start);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Remove overlapping matches, keeping earlier/higher priority matches.
|
|
155
|
+
*/
|
|
156
|
+
function removeOverlaps(
|
|
157
|
+
matches: Array<{ type: HighlightType; start: number; end: number; text: string }>
|
|
158
|
+
): Array<{ type: HighlightType; start: number; end: number; text: string }> {
|
|
159
|
+
const result: Array<{ type: HighlightType; start: number; end: number; text: string }> = [];
|
|
160
|
+
|
|
161
|
+
for (const match of matches) {
|
|
162
|
+
// Check if this match overlaps with any existing match
|
|
163
|
+
const overlaps = result.some(
|
|
164
|
+
(existing) => match.start < existing.end && match.end > existing.start
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!overlaps) {
|
|
168
|
+
result.push(match);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse input text and identify highlighted segments.
|
|
177
|
+
* Results are cached to avoid redundant regex processing.
|
|
178
|
+
*
|
|
179
|
+
* @param input - The input text to parse
|
|
180
|
+
* @returns HighlightResult with segments and metadata
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* const result = parseHighlights("Check @file.ts with /help");
|
|
185
|
+
* // result.segments = [
|
|
186
|
+
* // { text: "Check ", start: 0, end: 6 },
|
|
187
|
+
* // { text: "@file.ts", type: "mention", start: 6, end: 14 },
|
|
188
|
+
* // { text: " with ", start: 14, end: 20 },
|
|
189
|
+
* // { text: "/help", type: "command", start: 20, end: 25 }
|
|
190
|
+
* // ]
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export function parseHighlights(input: string): HighlightResult {
|
|
194
|
+
if (!input) {
|
|
195
|
+
return { segments: [], hasHighlights: false };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check cache first
|
|
199
|
+
const cached = getCachedResult(input);
|
|
200
|
+
if (cached) {
|
|
201
|
+
return cached;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const matches = removeOverlaps(findMatches(input));
|
|
205
|
+
const segments: HighlightSegment[] = [];
|
|
206
|
+
let currentPos = 0;
|
|
207
|
+
|
|
208
|
+
for (const match of matches) {
|
|
209
|
+
// Add plain text before this match
|
|
210
|
+
if (match.start > currentPos) {
|
|
211
|
+
segments.push({
|
|
212
|
+
text: input.slice(currentPos, match.start),
|
|
213
|
+
start: currentPos,
|
|
214
|
+
end: match.start,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add the highlighted segment
|
|
219
|
+
segments.push({
|
|
220
|
+
text: match.text,
|
|
221
|
+
type: match.type,
|
|
222
|
+
start: match.start,
|
|
223
|
+
end: match.end,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
currentPos = match.end;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Add remaining plain text
|
|
230
|
+
if (currentPos < input.length) {
|
|
231
|
+
segments.push({
|
|
232
|
+
text: input.slice(currentPos),
|
|
233
|
+
start: currentPos,
|
|
234
|
+
end: input.length,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const result: HighlightResult = {
|
|
239
|
+
segments,
|
|
240
|
+
hasHighlights: matches.length > 0,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Cache the result for future lookups
|
|
244
|
+
cacheResult(input, result);
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// =============================================================================
|
|
250
|
+
// Styling Functions
|
|
251
|
+
// =============================================================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Apply chalk styling to a highlight type.
|
|
255
|
+
*
|
|
256
|
+
* @param text - The text to style
|
|
257
|
+
* @param type - The highlight type
|
|
258
|
+
* @returns Styled string with ANSI codes
|
|
259
|
+
*/
|
|
260
|
+
export function applyHighlightStyle(text: string, type?: HighlightType): string {
|
|
261
|
+
if (!type) {
|
|
262
|
+
return text;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
switch (type) {
|
|
266
|
+
case "mention":
|
|
267
|
+
return chalk.cyan(text);
|
|
268
|
+
case "command":
|
|
269
|
+
return chalk.green(text);
|
|
270
|
+
case "url":
|
|
271
|
+
return chalk.blue.underline(text);
|
|
272
|
+
case "code":
|
|
273
|
+
return chalk.dim(text);
|
|
274
|
+
default:
|
|
275
|
+
return text;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the style description for a highlight type (for accessibility/testing).
|
|
281
|
+
*
|
|
282
|
+
* @param type - The highlight type
|
|
283
|
+
* @returns Human-readable style description
|
|
284
|
+
*/
|
|
285
|
+
export function getHighlightStyleDescription(type?: HighlightType): string {
|
|
286
|
+
if (!type) {
|
|
287
|
+
return "plain";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
switch (type) {
|
|
291
|
+
case "mention":
|
|
292
|
+
return "cyan";
|
|
293
|
+
case "command":
|
|
294
|
+
return "green";
|
|
295
|
+
case "url":
|
|
296
|
+
return "blue underline";
|
|
297
|
+
case "code":
|
|
298
|
+
return "dim";
|
|
299
|
+
default:
|
|
300
|
+
return "plain";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Apply highlights to input and return styled string.
|
|
306
|
+
* This is a convenience function for simple use cases.
|
|
307
|
+
*
|
|
308
|
+
* @param input - The input text to highlight
|
|
309
|
+
* @returns Styled string with ANSI codes
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* const styled = highlightInput("Check @file.ts with /help");
|
|
314
|
+
* console.log(styled); // Cyan @file.ts, green /help
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
export function highlightInput(input: string): string {
|
|
318
|
+
const { segments } = parseHighlights(input);
|
|
319
|
+
return segments.map((seg) => applyHighlightStyle(seg.text, seg.type)).join("");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// =============================================================================
|
|
323
|
+
// Cursor-Aware Functions
|
|
324
|
+
// =============================================================================
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Find which segment contains a given cursor position.
|
|
328
|
+
*
|
|
329
|
+
* @param segments - Array of highlight segments
|
|
330
|
+
* @param cursorPosition - The cursor position to locate
|
|
331
|
+
* @returns The segment containing the cursor, or undefined if not found
|
|
332
|
+
*/
|
|
333
|
+
export function findSegmentAtCursor(
|
|
334
|
+
segments: readonly HighlightSegment[],
|
|
335
|
+
cursorPosition: number
|
|
336
|
+
): HighlightSegment | undefined {
|
|
337
|
+
return segments.find((seg) => cursorPosition >= seg.start && cursorPosition < seg.end);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Split a segment at a cursor position for rendering with cursor indicator.
|
|
342
|
+
*
|
|
343
|
+
* @param segment - The segment to split
|
|
344
|
+
* @param cursorPosition - The cursor position within the segment
|
|
345
|
+
* @returns Object with before, cursor char, and after portions
|
|
346
|
+
*/
|
|
347
|
+
export function splitSegmentAtCursor(
|
|
348
|
+
segment: HighlightSegment,
|
|
349
|
+
cursorPosition: number
|
|
350
|
+
): {
|
|
351
|
+
before: string;
|
|
352
|
+
cursorChar: string;
|
|
353
|
+
after: string;
|
|
354
|
+
localPosition: number;
|
|
355
|
+
} {
|
|
356
|
+
const localPosition = cursorPosition - segment.start;
|
|
357
|
+
const before = segment.text.slice(0, localPosition);
|
|
358
|
+
const cursorChar = segment.text[localPosition] || " ";
|
|
359
|
+
const after = segment.text.slice(localPosition + 1);
|
|
360
|
+
|
|
361
|
+
return { before, cursorChar, after, localPosition };
|
|
362
|
+
}
|