@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,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnhancedCommandInput Component
|
|
3
|
+
*
|
|
4
|
+
* An enhanced version of CommandInput that supports both slash commands
|
|
5
|
+
* and @ mentions. This component adds @ mention autocomplete alongside
|
|
6
|
+
* the existing slash command system.
|
|
7
|
+
*
|
|
8
|
+
* @module tui/components/Input/EnhancedCommandInput
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Box, useInput } from "ink";
|
|
12
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
13
|
+
import { useInputHistory } from "../../hooks/useInputHistory.js";
|
|
14
|
+
import { useMentionAutocomplete } from "../../hooks/useMentionAutocomplete.js";
|
|
15
|
+
import type { AutocompleteOption } from "./Autocomplete.js";
|
|
16
|
+
import { Autocomplete } from "./Autocomplete.js";
|
|
17
|
+
import { MentionAutocomplete } from "./MentionAutocomplete.js";
|
|
18
|
+
import { parseSlashCommand, type SlashCommand } from "./slash-command-utils.js";
|
|
19
|
+
import { TextInput } from "./TextInput.js";
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Props for the EnhancedCommandInput component.
|
|
27
|
+
*/
|
|
28
|
+
export interface EnhancedCommandInputProps {
|
|
29
|
+
/** Callback when regular text message is submitted */
|
|
30
|
+
readonly onMessage: (text: string) => void;
|
|
31
|
+
/** Callback when a slash command is submitted */
|
|
32
|
+
readonly onCommand: (command: SlashCommand) => void;
|
|
33
|
+
/** Available command names for validation (without slash prefix) */
|
|
34
|
+
readonly commands?: readonly string[] | readonly AutocompleteOption[];
|
|
35
|
+
/** Get subcommands for a command (for two-level autocomplete) */
|
|
36
|
+
readonly getSubcommands?: (commandName: string) => readonly AutocompleteOption[] | undefined;
|
|
37
|
+
/** Get level 3 items for a command and subcommand (for three-level autocomplete) */
|
|
38
|
+
readonly getLevel3Items?: (
|
|
39
|
+
commandName: string,
|
|
40
|
+
arg1: string,
|
|
41
|
+
partial: string
|
|
42
|
+
) => readonly AutocompleteOption[] | undefined;
|
|
43
|
+
/** Enable grouped display with categories (default: false) */
|
|
44
|
+
readonly groupedCommands?: boolean;
|
|
45
|
+
/** Category display order for grouped mode */
|
|
46
|
+
readonly categoryOrder?: readonly string[];
|
|
47
|
+
/** Category labels for i18n */
|
|
48
|
+
readonly categoryLabels?: Record<string, string>;
|
|
49
|
+
/** Placeholder text shown when input is empty */
|
|
50
|
+
readonly placeholder?: string;
|
|
51
|
+
/** Disable input interactions */
|
|
52
|
+
readonly disabled?: boolean;
|
|
53
|
+
/** Whether the input is focused (default: true) */
|
|
54
|
+
readonly focused?: boolean;
|
|
55
|
+
/** Enable multiline mode for regular messages */
|
|
56
|
+
readonly multiline?: boolean;
|
|
57
|
+
/** localStorage key for history persistence */
|
|
58
|
+
readonly historyKey?: string;
|
|
59
|
+
/** Current working directory for @ mention file suggestions */
|
|
60
|
+
readonly cwd?: string;
|
|
61
|
+
/** Enable @ mention support (default: true) */
|
|
62
|
+
readonly enableMentions?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Helper Functions
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if input is a slash command.
|
|
71
|
+
*/
|
|
72
|
+
function isSlashCommand(input: string): boolean {
|
|
73
|
+
return input.startsWith("/") && input.length > 1 && input[1] !== " ";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Component
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* EnhancedCommandInput provides a text input with both slash command
|
|
82
|
+
* and @ mention support.
|
|
83
|
+
*
|
|
84
|
+
* Features:
|
|
85
|
+
* - Slash command parsing with argument handling
|
|
86
|
+
* - @ mention autocomplete for files, folders, URLs, etc.
|
|
87
|
+
* - Input history navigation (up/down arrows)
|
|
88
|
+
* - Keyboard-driven autocomplete selection
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* <EnhancedCommandInput
|
|
93
|
+
* onMessage={(text) => sendChat(text)}
|
|
94
|
+
* onCommand={(cmd) => executeCommand(cmd)}
|
|
95
|
+
* commands={['help', 'clear', 'exit']}
|
|
96
|
+
* cwd="/project"
|
|
97
|
+
* placeholder="Type a message, /command, or @mention..."
|
|
98
|
+
* />
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function EnhancedCommandInput({
|
|
102
|
+
onMessage,
|
|
103
|
+
onCommand,
|
|
104
|
+
commands,
|
|
105
|
+
getSubcommands,
|
|
106
|
+
getLevel3Items,
|
|
107
|
+
groupedCommands = false,
|
|
108
|
+
categoryOrder,
|
|
109
|
+
categoryLabels,
|
|
110
|
+
placeholder = "Type a message, /command, or @mention...",
|
|
111
|
+
disabled = false,
|
|
112
|
+
focused = true,
|
|
113
|
+
multiline = false,
|
|
114
|
+
historyKey,
|
|
115
|
+
cwd = process.cwd(),
|
|
116
|
+
enableMentions = true,
|
|
117
|
+
}: EnhancedCommandInputProps): React.ReactElement {
|
|
118
|
+
const [value, setValue] = useState("");
|
|
119
|
+
|
|
120
|
+
// Track when autocomplete just completed (to suppress Enter and move cursor)
|
|
121
|
+
const [autocompleteJustCompleted, setAutocompleteJustCompleted] = useState(false);
|
|
122
|
+
|
|
123
|
+
// Refs to track current autocomplete selection (avoids state delay on Enter/Tab)
|
|
124
|
+
const slashSelectionRef = useRef<{ index: number; hasOptions: boolean }>({
|
|
125
|
+
index: 0,
|
|
126
|
+
hasOptions: false,
|
|
127
|
+
});
|
|
128
|
+
const mentionSelectionRef = useRef<{ index: number; hasOptions: boolean }>({
|
|
129
|
+
index: 0,
|
|
130
|
+
hasOptions: false,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Ref for TextInput to manage focus
|
|
134
|
+
const inputRef = useRef<{ focus: () => void } | null>(null);
|
|
135
|
+
|
|
136
|
+
// Slash autocomplete state
|
|
137
|
+
const slashAutocomplete = useMemo(() => {
|
|
138
|
+
if (!value.startsWith("/")) {
|
|
139
|
+
return {
|
|
140
|
+
visible: false,
|
|
141
|
+
active: false,
|
|
142
|
+
query: "",
|
|
143
|
+
level: 1 as const,
|
|
144
|
+
commandName: "",
|
|
145
|
+
arg1: "",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const withoutSlash = value.slice(1);
|
|
150
|
+
const spaceIndex = withoutSlash.indexOf(" ");
|
|
151
|
+
|
|
152
|
+
if (spaceIndex === -1) {
|
|
153
|
+
// Level 1: command name completion
|
|
154
|
+
return {
|
|
155
|
+
visible: true,
|
|
156
|
+
active: true,
|
|
157
|
+
query: withoutSlash,
|
|
158
|
+
level: 1 as const,
|
|
159
|
+
commandName: "",
|
|
160
|
+
arg1: "",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Level 2 or 3: subcommand/arg completion
|
|
165
|
+
const commandName = withoutSlash.slice(0, spaceIndex);
|
|
166
|
+
const afterSpace = withoutSlash.slice(spaceIndex + 1);
|
|
167
|
+
// Check if there's a second space (potential level 3)
|
|
168
|
+
const secondSpaceIndex = afterSpace.indexOf(" ");
|
|
169
|
+
|
|
170
|
+
if (secondSpaceIndex === -1) {
|
|
171
|
+
// Level 2: subcommand completion (only one space so far)
|
|
172
|
+
return {
|
|
173
|
+
visible: true,
|
|
174
|
+
active: true,
|
|
175
|
+
query: afterSpace,
|
|
176
|
+
level: 2 as const,
|
|
177
|
+
commandName,
|
|
178
|
+
arg1: "",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Level 3: third-level completion (two spaces)
|
|
183
|
+
const arg1 = afterSpace.slice(0, secondSpaceIndex);
|
|
184
|
+
const afterSecondSpace = afterSpace.slice(secondSpaceIndex + 1);
|
|
185
|
+
// Check if there's a third space (args after level 3)
|
|
186
|
+
const thirdSpaceIndex = afterSecondSpace.indexOf(" ");
|
|
187
|
+
const level3Query =
|
|
188
|
+
thirdSpaceIndex === -1 ? afterSecondSpace : afterSecondSpace.slice(0, thirdSpaceIndex);
|
|
189
|
+
// Only active if we're still typing the level 3 item (no third space yet)
|
|
190
|
+
const isActive = thirdSpaceIndex === -1;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
visible: true,
|
|
194
|
+
active: isActive,
|
|
195
|
+
query: level3Query,
|
|
196
|
+
level: 3 as const,
|
|
197
|
+
commandName,
|
|
198
|
+
arg1,
|
|
199
|
+
};
|
|
200
|
+
}, [value]);
|
|
201
|
+
|
|
202
|
+
// @ Mention autocomplete state
|
|
203
|
+
const mentionAutocomplete = useMentionAutocomplete(value, { cwd });
|
|
204
|
+
|
|
205
|
+
// Slash autocomplete options (computed early for activeAutocomplete check)
|
|
206
|
+
const slashOptions = useMemo(() => {
|
|
207
|
+
if (slashAutocomplete.level === 1) {
|
|
208
|
+
return commands ?? [];
|
|
209
|
+
}
|
|
210
|
+
// Level 2: get subcommands for the command
|
|
211
|
+
if (slashAutocomplete.level === 2) {
|
|
212
|
+
if (getSubcommands && slashAutocomplete.commandName) {
|
|
213
|
+
return getSubcommands(slashAutocomplete.commandName) ?? [];
|
|
214
|
+
}
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
// Level 3: get third-level items (e.g., providers for auth set, models for model command)
|
|
218
|
+
if (slashAutocomplete.level === 3) {
|
|
219
|
+
if (getLevel3Items && slashAutocomplete.commandName && slashAutocomplete.arg1) {
|
|
220
|
+
return (
|
|
221
|
+
getLevel3Items(
|
|
222
|
+
slashAutocomplete.commandName,
|
|
223
|
+
slashAutocomplete.arg1,
|
|
224
|
+
slashAutocomplete.query
|
|
225
|
+
) ?? []
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
return [];
|
|
231
|
+
}, [
|
|
232
|
+
slashAutocomplete.level,
|
|
233
|
+
slashAutocomplete.commandName,
|
|
234
|
+
slashAutocomplete.arg1,
|
|
235
|
+
slashAutocomplete.query,
|
|
236
|
+
commands,
|
|
237
|
+
getSubcommands,
|
|
238
|
+
getLevel3Items,
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Determine which autocomplete is active (priority: slash > mention)
|
|
242
|
+
const activeAutocomplete = useMemo(() => {
|
|
243
|
+
// Only consider slash active if there are actually options to show
|
|
244
|
+
if (slashAutocomplete.visible && slashAutocomplete.active) {
|
|
245
|
+
// Level 1 always shows command list, level 2/3 need actual subcommands
|
|
246
|
+
if (slashAutocomplete.level === 1 || slashOptions.length > 0) {
|
|
247
|
+
return "slash" as const;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (enableMentions && mentionAutocomplete.state.visible && mentionAutocomplete.state.active) {
|
|
251
|
+
return "mention" as const;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}, [slashAutocomplete, slashOptions.length, mentionAutocomplete.state, enableMentions]);
|
|
255
|
+
|
|
256
|
+
// History navigation
|
|
257
|
+
const originalInputRef = useRef<string | null>(null);
|
|
258
|
+
const { navigateHistory, addToHistory, currentIndex } = useInputHistory({
|
|
259
|
+
maxItems: 100,
|
|
260
|
+
persistKey: historyKey,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const handleHistoryUp = useCallback(() => {
|
|
264
|
+
if (originalInputRef.current === null) {
|
|
265
|
+
originalInputRef.current = value;
|
|
266
|
+
}
|
|
267
|
+
const entry = navigateHistory("up");
|
|
268
|
+
if (entry !== null) {
|
|
269
|
+
setValue(entry);
|
|
270
|
+
}
|
|
271
|
+
}, [value, navigateHistory]);
|
|
272
|
+
|
|
273
|
+
const handleHistoryDown = useCallback(() => {
|
|
274
|
+
const entry = navigateHistory("down");
|
|
275
|
+
if (entry !== null) {
|
|
276
|
+
setValue(entry);
|
|
277
|
+
} else if (originalInputRef.current !== null && currentIndex === -1) {
|
|
278
|
+
setValue(originalInputRef.current);
|
|
279
|
+
originalInputRef.current = null;
|
|
280
|
+
}
|
|
281
|
+
}, [navigateHistory, currentIndex]);
|
|
282
|
+
|
|
283
|
+
const handleChange = useCallback((newValue: string) => {
|
|
284
|
+
originalInputRef.current = null;
|
|
285
|
+
setValue(newValue);
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
const handleSubmit = useCallback(
|
|
289
|
+
(submittedValue: string) => {
|
|
290
|
+
const trimmed = submittedValue.trim();
|
|
291
|
+
|
|
292
|
+
if (trimmed.length === 0) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
originalInputRef.current = null;
|
|
297
|
+
addToHistory(trimmed);
|
|
298
|
+
|
|
299
|
+
if (isSlashCommand(trimmed)) {
|
|
300
|
+
const command = parseSlashCommand(trimmed);
|
|
301
|
+
onCommand(command);
|
|
302
|
+
} else {
|
|
303
|
+
onMessage(trimmed);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
setValue("");
|
|
307
|
+
},
|
|
308
|
+
[addToHistory, onCommand, onMessage]
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// History navigation only when no autocomplete is active
|
|
312
|
+
useInput(
|
|
313
|
+
(input, key) => {
|
|
314
|
+
// ↑ or Ctrl+P - previous history
|
|
315
|
+
if (key.upArrow || (key.ctrl && input === "p")) {
|
|
316
|
+
handleHistoryUp();
|
|
317
|
+
}
|
|
318
|
+
// ↓ or Ctrl+N - next history
|
|
319
|
+
else if (key.downArrow || (key.ctrl && input === "n")) {
|
|
320
|
+
handleHistoryDown();
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{ isActive: focused && !disabled && !multiline && activeAutocomplete === null }
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Extract stable values for memoization to reduce unnecessary recomputation
|
|
327
|
+
const slashQuery = slashAutocomplete.query;
|
|
328
|
+
const slashActive = slashAutocomplete.active;
|
|
329
|
+
|
|
330
|
+
// Compute filtered/sorted slash options for Enter/Tab selection
|
|
331
|
+
const sortedSlashOptions = useMemo(() => {
|
|
332
|
+
if (!slashActive || slashOptions.length === 0) return [];
|
|
333
|
+
// Normalize to AutocompleteOption format
|
|
334
|
+
const normalized = slashOptions.map((opt) => (typeof opt === "string" ? { name: opt } : opt));
|
|
335
|
+
// Filter by prefix (check both name and aliases)
|
|
336
|
+
const query = slashQuery.toLowerCase();
|
|
337
|
+
const filtered = query
|
|
338
|
+
? normalized.filter(
|
|
339
|
+
(opt) =>
|
|
340
|
+
opt.name.toLowerCase().startsWith(query) ||
|
|
341
|
+
opt.aliases?.some((alias) => alias.toLowerCase().startsWith(query))
|
|
342
|
+
)
|
|
343
|
+
: normalized;
|
|
344
|
+
// Sort alphabetically
|
|
345
|
+
return [...filtered].sort((a, b) => a.name.localeCompare(b.name));
|
|
346
|
+
}, [slashActive, slashQuery, slashOptions]);
|
|
347
|
+
|
|
348
|
+
// Handle slash autocomplete selection
|
|
349
|
+
const handleSlashSelect = useCallback(
|
|
350
|
+
(selected: string) => {
|
|
351
|
+
if (slashAutocomplete.level === 1) {
|
|
352
|
+
// Level 1: selected is command name
|
|
353
|
+
setValue(`/${selected} `);
|
|
354
|
+
} else if (slashAutocomplete.level === 2) {
|
|
355
|
+
// Level 2: selected is subcommand name, preserve command
|
|
356
|
+
setValue(`/${slashAutocomplete.commandName} ${selected} `);
|
|
357
|
+
} else {
|
|
358
|
+
// Level 3: selected is third-level item (e.g., provider), preserve command and arg1
|
|
359
|
+
setValue(`/${slashAutocomplete.commandName} ${slashAutocomplete.arg1} ${selected} `);
|
|
360
|
+
}
|
|
361
|
+
setAutocompleteJustCompleted(true);
|
|
362
|
+
inputRef.current?.focus();
|
|
363
|
+
},
|
|
364
|
+
[slashAutocomplete.level, slashAutocomplete.commandName, slashAutocomplete.arg1]
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Enter/Tab interception when autocomplete is active
|
|
368
|
+
useInput(
|
|
369
|
+
(_input, key) => {
|
|
370
|
+
if (key.return || key.tab) {
|
|
371
|
+
if (activeAutocomplete === "slash") {
|
|
372
|
+
const { index, hasOptions } = slashSelectionRef.current;
|
|
373
|
+
if (hasOptions && sortedSlashOptions[index]) {
|
|
374
|
+
handleSlashSelect(sortedSlashOptions[index].name);
|
|
375
|
+
}
|
|
376
|
+
} else if (activeAutocomplete === "mention") {
|
|
377
|
+
// Mention selection is handled internally by MentionAutocomplete
|
|
378
|
+
// This is a fallback that shouldn't normally trigger
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{ isActive: focused && !disabled && activeAutocomplete !== null }
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Handle @ mention autocomplete selection
|
|
386
|
+
const handleMentionSelect = useCallback(
|
|
387
|
+
(selectedValue: string, mode: "type" | "value") => {
|
|
388
|
+
const newValue = mentionAutocomplete.handleSelect(selectedValue, mode);
|
|
389
|
+
setValue(newValue);
|
|
390
|
+
setAutocompleteJustCompleted(true);
|
|
391
|
+
inputRef.current?.focus();
|
|
392
|
+
},
|
|
393
|
+
[mentionAutocomplete]
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Handle autocomplete cancel
|
|
397
|
+
const handleAutocompleteCancel = useCallback(() => {
|
|
398
|
+
inputRef.current?.focus();
|
|
399
|
+
}, []);
|
|
400
|
+
|
|
401
|
+
// Track slash autocomplete selection state
|
|
402
|
+
const handleSlashSelectionChange = useCallback((index: number, hasOptions: boolean) => {
|
|
403
|
+
slashSelectionRef.current = { index, hasOptions };
|
|
404
|
+
}, []);
|
|
405
|
+
|
|
406
|
+
// Track mention autocomplete selection state
|
|
407
|
+
const handleMentionSelectionChange = useCallback((index: number, hasOptions: boolean) => {
|
|
408
|
+
mentionSelectionRef.current = { index, hasOptions };
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
411
|
+
// Callback when cursor has been moved to end
|
|
412
|
+
const handleCursorMoved = useCallback(() => {
|
|
413
|
+
setAutocompleteJustCompleted(false);
|
|
414
|
+
}, []);
|
|
415
|
+
|
|
416
|
+
// Determine what to show
|
|
417
|
+
const showSlashAutocomplete = activeAutocomplete === "slash" && slashOptions.length > 0;
|
|
418
|
+
const showMentionAutocomplete =
|
|
419
|
+
activeAutocomplete === "mention" &&
|
|
420
|
+
(mentionAutocomplete.state.mode === "type" ||
|
|
421
|
+
mentionAutocomplete.fileSuggestions.suggestions.length > 0);
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<Box flexDirection="column">
|
|
425
|
+
<TextInput
|
|
426
|
+
value={value}
|
|
427
|
+
onChange={handleChange}
|
|
428
|
+
onSubmit={handleSubmit}
|
|
429
|
+
placeholder={placeholder}
|
|
430
|
+
disabled={disabled}
|
|
431
|
+
focused={focused}
|
|
432
|
+
multiline={multiline}
|
|
433
|
+
suppressEnter={autocompleteJustCompleted || activeAutocomplete !== null}
|
|
434
|
+
suppressTab={activeAutocomplete !== null}
|
|
435
|
+
cursorToEnd={autocompleteJustCompleted}
|
|
436
|
+
onCursorMoved={handleCursorMoved}
|
|
437
|
+
/>
|
|
438
|
+
|
|
439
|
+
{/* Slash command autocomplete */}
|
|
440
|
+
{showSlashAutocomplete && (
|
|
441
|
+
<Autocomplete
|
|
442
|
+
input={slashAutocomplete.query}
|
|
443
|
+
options={slashOptions}
|
|
444
|
+
onSelect={handleSlashSelect}
|
|
445
|
+
onCancel={handleAutocompleteCancel}
|
|
446
|
+
onSelectionChange={handleSlashSelectionChange}
|
|
447
|
+
visible={slashAutocomplete.visible}
|
|
448
|
+
active={slashAutocomplete.active}
|
|
449
|
+
grouped={slashAutocomplete.level === 1 && groupedCommands}
|
|
450
|
+
categoryOrder={slashAutocomplete.level === 1 ? categoryOrder : undefined}
|
|
451
|
+
categoryLabels={slashAutocomplete.level === 1 ? categoryLabels : undefined}
|
|
452
|
+
/>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{/* @ Mention autocomplete */}
|
|
456
|
+
{showMentionAutocomplete && (
|
|
457
|
+
<MentionAutocomplete
|
|
458
|
+
input={mentionAutocomplete.state.filterText}
|
|
459
|
+
mode={mentionAutocomplete.state.mode}
|
|
460
|
+
mentionType={mentionAutocomplete.state.mentionType ?? undefined}
|
|
461
|
+
fileSuggestions={mentionAutocomplete.fileSuggestions.suggestions}
|
|
462
|
+
onSelect={handleMentionSelect}
|
|
463
|
+
onCancel={handleAutocompleteCancel}
|
|
464
|
+
onSelectionChange={handleMentionSelectionChange}
|
|
465
|
+
visible={mentionAutocomplete.state.visible}
|
|
466
|
+
active={mentionAutocomplete.state.active}
|
|
467
|
+
/>
|
|
468
|
+
)}
|
|
469
|
+
</Box>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HighlightedText Component (T009-HL)
|
|
3
|
+
*
|
|
4
|
+
* React component that renders text with syntax highlighting for
|
|
5
|
+
* special patterns (@mentions, /commands, URLs, `code`).
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/Input/HighlightedText
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Text } from "ink";
|
|
11
|
+
import { memo, useMemo } from "react";
|
|
12
|
+
import {
|
|
13
|
+
type HighlightResult,
|
|
14
|
+
type HighlightSegment,
|
|
15
|
+
type HighlightType,
|
|
16
|
+
parseHighlights,
|
|
17
|
+
splitSegmentAtCursor,
|
|
18
|
+
} from "./highlight.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props for the HighlightedText component.
|
|
26
|
+
*/
|
|
27
|
+
export interface HighlightedTextProps {
|
|
28
|
+
/** The text to highlight */
|
|
29
|
+
readonly text: string;
|
|
30
|
+
/** Pre-parsed highlight result (optional, for performance) */
|
|
31
|
+
readonly highlightResult?: HighlightResult;
|
|
32
|
+
/** Current cursor position (optional, for cursor rendering) */
|
|
33
|
+
readonly cursorPosition?: number;
|
|
34
|
+
/** Whether cursor should be shown */
|
|
35
|
+
readonly showCursor?: boolean;
|
|
36
|
+
/** Line start position for multiline support */
|
|
37
|
+
readonly lineStartPosition?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Style Mapping
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Map highlight types to Ink Text props.
|
|
46
|
+
*/
|
|
47
|
+
function getTextProps(type?: HighlightType): Record<string, boolean | string> {
|
|
48
|
+
if (!type) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
switch (type) {
|
|
53
|
+
case "mention":
|
|
54
|
+
return { color: "cyan" };
|
|
55
|
+
case "command":
|
|
56
|
+
return { color: "green" };
|
|
57
|
+
case "url":
|
|
58
|
+
return { color: "blue", underline: true };
|
|
59
|
+
case "code":
|
|
60
|
+
return { dimColor: true };
|
|
61
|
+
default:
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Segment Renderers
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render a single segment without cursor.
|
|
72
|
+
*/
|
|
73
|
+
function SegmentText({ segment }: { segment: HighlightSegment }) {
|
|
74
|
+
const props = getTextProps(segment.type);
|
|
75
|
+
return <Text {...props}>{segment.text}</Text>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a segment with cursor at specified position.
|
|
80
|
+
*/
|
|
81
|
+
function SegmentWithCursor({
|
|
82
|
+
segment,
|
|
83
|
+
cursorPosition,
|
|
84
|
+
}: {
|
|
85
|
+
segment: HighlightSegment;
|
|
86
|
+
cursorPosition: number;
|
|
87
|
+
}) {
|
|
88
|
+
const { before, cursorChar, after } = splitSegmentAtCursor(segment, cursorPosition);
|
|
89
|
+
const props = getTextProps(segment.type);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Text {...props}>
|
|
93
|
+
{before || null}
|
|
94
|
+
<Text inverse>{cursorChar}</Text>
|
|
95
|
+
{after || null}
|
|
96
|
+
</Text>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Main Component
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* HighlightedText renders text with syntax highlighting for special patterns.
|
|
106
|
+
*
|
|
107
|
+
* Supports:
|
|
108
|
+
* - @mentions (cyan)
|
|
109
|
+
* - /commands (green)
|
|
110
|
+
* - URLs (blue underline)
|
|
111
|
+
* - `code` (dim)
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```tsx
|
|
115
|
+
* // Basic usage
|
|
116
|
+
* <HighlightedText text="Check @file.ts with /help" />
|
|
117
|
+
*
|
|
118
|
+
* // With cursor
|
|
119
|
+
* <HighlightedText
|
|
120
|
+
* text="Type /mode to change"
|
|
121
|
+
* cursorPosition={5}
|
|
122
|
+
* showCursor
|
|
123
|
+
* />
|
|
124
|
+
*
|
|
125
|
+
* // With pre-parsed result for performance
|
|
126
|
+
* const result = parseHighlights(text);
|
|
127
|
+
* <HighlightedText text={text} highlightResult={result} />
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
function HighlightedTextComponent({
|
|
131
|
+
text,
|
|
132
|
+
highlightResult,
|
|
133
|
+
cursorPosition,
|
|
134
|
+
showCursor = false,
|
|
135
|
+
lineStartPosition = 0,
|
|
136
|
+
}: HighlightedTextProps) {
|
|
137
|
+
// Parse highlights if not provided
|
|
138
|
+
const result = useMemo(() => {
|
|
139
|
+
return highlightResult ?? parseHighlights(text);
|
|
140
|
+
}, [text, highlightResult]);
|
|
141
|
+
|
|
142
|
+
// Adjust cursor position for line offset
|
|
143
|
+
const adjustedCursor =
|
|
144
|
+
cursorPosition !== undefined ? cursorPosition - lineStartPosition : undefined;
|
|
145
|
+
|
|
146
|
+
// Check if cursor is within this text's range
|
|
147
|
+
const cursorInRange =
|
|
148
|
+
showCursor &&
|
|
149
|
+
adjustedCursor !== undefined &&
|
|
150
|
+
adjustedCursor >= 0 &&
|
|
151
|
+
adjustedCursor <= text.length;
|
|
152
|
+
|
|
153
|
+
// If no highlights and no cursor, render plain text
|
|
154
|
+
if (!result.hasHighlights && !cursorInRange) {
|
|
155
|
+
return <Text>{text || " "}</Text>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If no highlights but has cursor, render with cursor
|
|
159
|
+
if (!result.hasHighlights && cursorInRange && adjustedCursor !== undefined) {
|
|
160
|
+
const before = text.slice(0, adjustedCursor);
|
|
161
|
+
const cursorChar = text[adjustedCursor] || " ";
|
|
162
|
+
const after = text.slice(adjustedCursor + 1);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Text>
|
|
166
|
+
{before || null}
|
|
167
|
+
<Text inverse>{cursorChar}</Text>
|
|
168
|
+
{after || null}
|
|
169
|
+
</Text>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Render segments with potential cursor
|
|
174
|
+
return (
|
|
175
|
+
<Text>
|
|
176
|
+
{result.segments.map((segment, index) => {
|
|
177
|
+
// Check if cursor is in this segment
|
|
178
|
+
const cursorInSegment =
|
|
179
|
+
cursorInRange &&
|
|
180
|
+
adjustedCursor !== undefined &&
|
|
181
|
+
adjustedCursor >= segment.start - lineStartPosition &&
|
|
182
|
+
adjustedCursor < segment.end - lineStartPosition;
|
|
183
|
+
|
|
184
|
+
// Handle cursor at very end (after last segment)
|
|
185
|
+
const cursorAtEnd =
|
|
186
|
+
cursorInRange &&
|
|
187
|
+
adjustedCursor !== undefined &&
|
|
188
|
+
adjustedCursor === text.length &&
|
|
189
|
+
index === result.segments.length - 1;
|
|
190
|
+
|
|
191
|
+
if (cursorInSegment && adjustedCursor !== undefined) {
|
|
192
|
+
return (
|
|
193
|
+
<SegmentWithCursor
|
|
194
|
+
key={`seg-${segment.start}`}
|
|
195
|
+
segment={{
|
|
196
|
+
...segment,
|
|
197
|
+
// Adjust segment positions for line offset
|
|
198
|
+
start: segment.start - lineStartPosition,
|
|
199
|
+
end: segment.end - lineStartPosition,
|
|
200
|
+
}}
|
|
201
|
+
cursorPosition={adjustedCursor}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Render segment plus cursor at end if needed
|
|
207
|
+
if (cursorAtEnd) {
|
|
208
|
+
const props = getTextProps(segment.type);
|
|
209
|
+
return (
|
|
210
|
+
<Text key={`seg-${segment.start}`}>
|
|
211
|
+
<Text {...props}>{segment.text}</Text>
|
|
212
|
+
<Text inverse> </Text>
|
|
213
|
+
</Text>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return <SegmentText key={`seg-${segment.start}`} segment={segment} />;
|
|
218
|
+
})}
|
|
219
|
+
</Text>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Memoized HighlightedText to prevent unnecessary re-renders.
|
|
225
|
+
*/
|
|
226
|
+
export const HighlightedText = memo(HighlightedTextComponent, (prev, next) => {
|
|
227
|
+
return (
|
|
228
|
+
prev.text === next.text &&
|
|
229
|
+
prev.cursorPosition === next.cursorPosition &&
|
|
230
|
+
prev.showCursor === next.showCursor &&
|
|
231
|
+
prev.lineStartPosition === next.lineStartPosition &&
|
|
232
|
+
prev.highlightResult === next.highlightResult
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
export default HighlightedText;
|