@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,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Persistence Adapter
|
|
3
|
+
*
|
|
4
|
+
* Provides bidirectional synchronization between TUI MessagesContext
|
|
5
|
+
* and @vellum/core session messages for persistence.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/adapters/session-adapter
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SessionMessage } from "@vellum/core";
|
|
11
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
12
|
+
import type { Message } from "../context/MessagesContext.js";
|
|
13
|
+
import { useMessages } from "../context/MessagesContext.js";
|
|
14
|
+
import { toSessionMessage, toUIMessages } from "./message-adapter.js";
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Session storage interface for persistence
|
|
22
|
+
*
|
|
23
|
+
* Abstracts the underlying storage mechanism to allow for different
|
|
24
|
+
* implementations (file-based, memory, etc.)
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionStorage {
|
|
27
|
+
/**
|
|
28
|
+
* Save messages to the session
|
|
29
|
+
*
|
|
30
|
+
* @param sessionId - Unique session identifier
|
|
31
|
+
* @param messages - Session messages to persist
|
|
32
|
+
*/
|
|
33
|
+
save(sessionId: string, messages: readonly SessionMessage[]): Promise<void>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load messages from a session
|
|
37
|
+
*
|
|
38
|
+
* @param sessionId - Unique session identifier
|
|
39
|
+
* @returns Array of session messages, or null if session not found
|
|
40
|
+
*/
|
|
41
|
+
load(sessionId: string): Promise<readonly SessionMessage[] | null>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Clear all messages from a session
|
|
45
|
+
*
|
|
46
|
+
* @param sessionId - Unique session identifier
|
|
47
|
+
*/
|
|
48
|
+
clear(sessionId: string): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for the useSessionAdapter hook
|
|
53
|
+
*/
|
|
54
|
+
export interface UseSessionAdapterOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Session ID for persistence
|
|
57
|
+
*/
|
|
58
|
+
sessionId: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Storage implementation for session persistence
|
|
62
|
+
*/
|
|
63
|
+
storage: SessionStorage;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether to auto-save on message changes
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
autoSave?: boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Debounce delay for auto-save in milliseconds
|
|
73
|
+
* @default 500
|
|
74
|
+
*/
|
|
75
|
+
saveDebounceMs?: number;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether to load session on mount
|
|
79
|
+
* @default true
|
|
80
|
+
*/
|
|
81
|
+
autoLoad?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Return value of the useSessionAdapter hook
|
|
86
|
+
*/
|
|
87
|
+
export interface UseSessionAdapterReturn {
|
|
88
|
+
/**
|
|
89
|
+
* Manually save current messages to session
|
|
90
|
+
*/
|
|
91
|
+
saveSession: () => Promise<void>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load messages from session and update context
|
|
95
|
+
*/
|
|
96
|
+
loadSession: () => Promise<void>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear the session storage
|
|
100
|
+
*/
|
|
101
|
+
clearSession: () => Promise<void>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether a save operation is in progress
|
|
105
|
+
*/
|
|
106
|
+
isSaving: boolean;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether a load operation is in progress
|
|
110
|
+
*/
|
|
111
|
+
isLoading: boolean;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Last error that occurred during save/load
|
|
115
|
+
*/
|
|
116
|
+
error: Error | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Interface for the Session Adapter (non-hook version)
|
|
121
|
+
*/
|
|
122
|
+
export interface SessionAdapter {
|
|
123
|
+
/**
|
|
124
|
+
* Save messages to session storage
|
|
125
|
+
*
|
|
126
|
+
* @param messages - UI messages to persist
|
|
127
|
+
*/
|
|
128
|
+
save(messages: readonly Message[]): Promise<void>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Load messages from session storage
|
|
132
|
+
*
|
|
133
|
+
* @returns Array of UI messages, or null if session not found
|
|
134
|
+
*/
|
|
135
|
+
load(): Promise<readonly Message[] | null>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Clear the session storage
|
|
139
|
+
*/
|
|
140
|
+
clear(): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// Factory Function
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a session adapter for a given session and storage
|
|
149
|
+
*
|
|
150
|
+
* This factory creates a stateless adapter that can be used outside of React
|
|
151
|
+
* components for session persistence operations.
|
|
152
|
+
*
|
|
153
|
+
* @param sessionId - Unique session identifier
|
|
154
|
+
* @param storage - Storage implementation
|
|
155
|
+
* @returns Session adapter interface
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const storage = createFileSessionStorage('/path/to/sessions');
|
|
160
|
+
* const adapter = createSessionAdapter('session-123', storage);
|
|
161
|
+
*
|
|
162
|
+
* // Save messages
|
|
163
|
+
* await adapter.save(messages);
|
|
164
|
+
*
|
|
165
|
+
* // Load messages
|
|
166
|
+
* const loadedMessages = await adapter.load();
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function createSessionAdapter(sessionId: string, storage: SessionStorage): SessionAdapter {
|
|
170
|
+
return {
|
|
171
|
+
async save(messages: readonly Message[]): Promise<void> {
|
|
172
|
+
const sessionMessages = messages
|
|
173
|
+
.filter((msg): msg is Message => msg.role !== "tool")
|
|
174
|
+
.map((msg) => toSessionMessage(msg));
|
|
175
|
+
await storage.save(sessionId, sessionMessages);
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async load(): Promise<readonly Message[] | null> {
|
|
179
|
+
const sessionMessages = await storage.load(sessionId);
|
|
180
|
+
if (!sessionMessages) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return toUIMessages(sessionMessages);
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async clear(): Promise<void> {
|
|
187
|
+
await storage.clear(sessionId);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// =============================================================================
|
|
193
|
+
// Hook Implementation
|
|
194
|
+
// =============================================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Hook that creates a session adapter for persistence
|
|
198
|
+
*
|
|
199
|
+
* Provides automatic synchronization between MessagesContext and
|
|
200
|
+
* session storage, with support for auto-save and auto-load.
|
|
201
|
+
*
|
|
202
|
+
* @param options - Configuration options for the adapter
|
|
203
|
+
* @returns The session adapter interface with save/load/clear methods
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```tsx
|
|
207
|
+
* function ChatContainer() {
|
|
208
|
+
* const storage = useMemo(() => createFileSessionStorage('/sessions'), []);
|
|
209
|
+
* const { saveSession, loadSession, isSaving, error } = useSessionAdapter({
|
|
210
|
+
* sessionId: 'session-123',
|
|
211
|
+
* storage,
|
|
212
|
+
* autoSave: true,
|
|
213
|
+
* saveDebounceMs: 1000,
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* // Messages will auto-save on change
|
|
217
|
+
* // Manual save available via saveSession()
|
|
218
|
+
*
|
|
219
|
+
* return (
|
|
220
|
+
* <Box>
|
|
221
|
+
* {isSaving && <Text>Saving...</Text>}
|
|
222
|
+
* {error && <Text color="red">{error.message}</Text>}
|
|
223
|
+
* <MessageList />
|
|
224
|
+
* </Box>
|
|
225
|
+
* );
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export function useSessionAdapter(options: UseSessionAdapterOptions): UseSessionAdapterReturn {
|
|
230
|
+
const { sessionId, storage, autoSave = true, saveDebounceMs = 500, autoLoad = true } = options;
|
|
231
|
+
|
|
232
|
+
// Get messages context
|
|
233
|
+
const { messages, addMessage, clearMessages } = useMessages();
|
|
234
|
+
|
|
235
|
+
// State refs for async operations
|
|
236
|
+
const isSavingRef = useRef(false);
|
|
237
|
+
const isLoadingRef = useRef(false);
|
|
238
|
+
const errorRef = useRef<Error | null>(null);
|
|
239
|
+
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
240
|
+
|
|
241
|
+
// Track previous messages for change detection
|
|
242
|
+
const previousMessagesRef = useRef<readonly Message[]>([]);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Save current messages to session storage
|
|
246
|
+
*/
|
|
247
|
+
const saveSession = useCallback(async (): Promise<void> => {
|
|
248
|
+
if (isSavingRef.current) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
isSavingRef.current = true;
|
|
253
|
+
errorRef.current = null;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const adapter = createSessionAdapter(sessionId, storage);
|
|
257
|
+
await adapter.save(messages);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
errorRef.current = err instanceof Error ? err : new Error(String(err));
|
|
260
|
+
} finally {
|
|
261
|
+
isSavingRef.current = false;
|
|
262
|
+
}
|
|
263
|
+
}, [sessionId, storage, messages]);
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Load messages from session storage
|
|
267
|
+
*/
|
|
268
|
+
const loadSession = useCallback(async (): Promise<void> => {
|
|
269
|
+
if (isLoadingRef.current) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
isLoadingRef.current = true;
|
|
274
|
+
errorRef.current = null;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const adapter = createSessionAdapter(sessionId, storage);
|
|
278
|
+
const loadedMessages = await adapter.load();
|
|
279
|
+
|
|
280
|
+
if (loadedMessages && loadedMessages.length > 0) {
|
|
281
|
+
// Clear existing messages first
|
|
282
|
+
clearMessages();
|
|
283
|
+
|
|
284
|
+
// Add loaded messages
|
|
285
|
+
for (const msg of loadedMessages) {
|
|
286
|
+
addMessage({
|
|
287
|
+
role: msg.role,
|
|
288
|
+
content: msg.content,
|
|
289
|
+
isStreaming: msg.isStreaming,
|
|
290
|
+
toolCalls: msg.toolCalls,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Update previous messages ref to avoid triggering auto-save
|
|
295
|
+
previousMessagesRef.current = loadedMessages;
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
errorRef.current = err instanceof Error ? err : new Error(String(err));
|
|
299
|
+
} finally {
|
|
300
|
+
isLoadingRef.current = false;
|
|
301
|
+
}
|
|
302
|
+
}, [sessionId, storage, clearMessages, addMessage]);
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Clear session storage
|
|
306
|
+
*/
|
|
307
|
+
const clearSession = useCallback(async (): Promise<void> => {
|
|
308
|
+
try {
|
|
309
|
+
const adapter = createSessionAdapter(sessionId, storage);
|
|
310
|
+
await adapter.clear();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
errorRef.current = err instanceof Error ? err : new Error(String(err));
|
|
313
|
+
}
|
|
314
|
+
}, [sessionId, storage]);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Debounced save function
|
|
318
|
+
*/
|
|
319
|
+
const debouncedSave = useCallback((): void => {
|
|
320
|
+
// Clear existing timeout
|
|
321
|
+
if (saveTimeoutRef.current) {
|
|
322
|
+
clearTimeout(saveTimeoutRef.current);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Set new timeout
|
|
326
|
+
saveTimeoutRef.current = setTimeout(() => {
|
|
327
|
+
void saveSession();
|
|
328
|
+
saveTimeoutRef.current = null;
|
|
329
|
+
}, saveDebounceMs);
|
|
330
|
+
}, [saveSession, saveDebounceMs]);
|
|
331
|
+
|
|
332
|
+
// Auto-load on mount
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (autoLoad) {
|
|
335
|
+
void loadSession();
|
|
336
|
+
}
|
|
337
|
+
}, [autoLoad, loadSession]);
|
|
338
|
+
|
|
339
|
+
// Auto-save on message changes
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
if (!autoSave) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Skip if messages haven't changed
|
|
346
|
+
if (messages === previousMessagesRef.current) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Skip if this is the initial load (empty to loaded)
|
|
351
|
+
if (previousMessagesRef.current.length === 0 && messages.length > 0 && isLoadingRef.current) {
|
|
352
|
+
previousMessagesRef.current = messages;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Trigger debounced save
|
|
357
|
+
if (messages.length > 0) {
|
|
358
|
+
debouncedSave();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
previousMessagesRef.current = messages;
|
|
362
|
+
}, [autoSave, messages, debouncedSave]);
|
|
363
|
+
|
|
364
|
+
// Cleanup timeout on unmount
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
return () => {
|
|
367
|
+
if (saveTimeoutRef.current) {
|
|
368
|
+
clearTimeout(saveTimeoutRef.current);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}, []);
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
saveSession,
|
|
375
|
+
loadSession,
|
|
376
|
+
clearSession,
|
|
377
|
+
isSaving: isSavingRef.current,
|
|
378
|
+
isLoading: isLoadingRef.current,
|
|
379
|
+
error: errorRef.current,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// =============================================================================
|
|
384
|
+
// Memory Storage Implementation
|
|
385
|
+
// =============================================================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* In-memory session storage implementation
|
|
389
|
+
*
|
|
390
|
+
* Useful for testing and temporary sessions that don't need
|
|
391
|
+
* to persist across application restarts.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```typescript
|
|
395
|
+
* const storage = createMemorySessionStorage();
|
|
396
|
+
* const adapter = createSessionAdapter('session-123', storage);
|
|
397
|
+
*
|
|
398
|
+
* await adapter.save(messages);
|
|
399
|
+
* const loaded = await adapter.load();
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
export function createMemorySessionStorage(): SessionStorage {
|
|
403
|
+
const sessions = new Map<string, readonly SessionMessage[]>();
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
async save(sessionId: string, messages: readonly SessionMessage[]): Promise<void> {
|
|
407
|
+
sessions.set(sessionId, [...messages]);
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
async load(sessionId: string): Promise<readonly SessionMessage[] | null> {
|
|
411
|
+
const messages = sessions.get(sessionId);
|
|
412
|
+
return messages ? [...messages] : null;
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async clear(sessionId: string): Promise<void> {
|
|
416
|
+
sessions.delete(sessionId);
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffered Stdout for VS Code Terminal
|
|
3
|
+
*
|
|
4
|
+
* Implements Synchronized Output (DEC 2026) to prevent flickering in VS Code's
|
|
5
|
+
* integrated terminal on Windows. Batches multiple writes into a single
|
|
6
|
+
* atomic frame, wrapped in begin/end synchronized update sequences.
|
|
7
|
+
*
|
|
8
|
+
* @module tui/buffered-stdout
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import { Writable } from "node:stream";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Shared Stdout Reference
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Shared stdout reference for modules that need to write outside Ink.
|
|
20
|
+
* When BufferedStdout is active, this should point to the BufferedStdout instance.
|
|
21
|
+
* Other modules should use getActiveStdout() instead of process.stdout directly.
|
|
22
|
+
*/
|
|
23
|
+
let activeStdout: NodeJS.WriteStream = process.stdout;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the active stdout stream.
|
|
27
|
+
* Returns BufferedStdout when active, otherwise process.stdout.
|
|
28
|
+
* Use this instead of process.stdout directly to ensure synchronized output.
|
|
29
|
+
*/
|
|
30
|
+
export function getActiveStdout(): NodeJS.WriteStream {
|
|
31
|
+
return activeStdout;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set the active stdout stream.
|
|
36
|
+
* Called by App initialization when BufferedStdout is created.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function setActiveStdout(stream: NodeJS.WriteStream): void {
|
|
40
|
+
activeStdout = stream;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Constants
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/** Begin Synchronized Update (DEC 2026) */
|
|
48
|
+
const BSU = "\x1b[?2026h";
|
|
49
|
+
|
|
50
|
+
/** End Synchronized Update (DEC 2026) */
|
|
51
|
+
const ESU = "\x1b[?2026l";
|
|
52
|
+
|
|
53
|
+
/** Hide cursor during frame render */
|
|
54
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
55
|
+
|
|
56
|
+
/** Show cursor after frame render */
|
|
57
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Environment Detection
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Detects if running inside VS Code integrated terminal.
|
|
65
|
+
*/
|
|
66
|
+
function isVsCodeTerminal(): boolean {
|
|
67
|
+
return (
|
|
68
|
+
process.env.TERM_PROGRAM === "vscode" ||
|
|
69
|
+
process.env.VSCODE_INJECTION === "1" ||
|
|
70
|
+
!!process.env.VSCODE_GIT_IPC_HANDLE
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Atomic Write Helper
|
|
76
|
+
// =============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Writes a string synchronously to stdout (fd=1).
|
|
80
|
+
* This bypasses Node's async buffering for atomic output.
|
|
81
|
+
*/
|
|
82
|
+
function atomicWrite(s: string): void {
|
|
83
|
+
fs.writeSync(1, Buffer.from(s, "utf8"));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// BufferedStdout Class
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A Writable stream that batches writes and flushes them atomically
|
|
92
|
+
* wrapped in synchronized output sequences.
|
|
93
|
+
*
|
|
94
|
+
* This prevents visual tearing/flickering in terminals that support
|
|
95
|
+
* the DEC 2026 synchronized output feature (VS Code terminal).
|
|
96
|
+
*/
|
|
97
|
+
export class BufferedStdout extends Writable {
|
|
98
|
+
private buf = "";
|
|
99
|
+
private scheduled = false;
|
|
100
|
+
private readonly onResize: () => void;
|
|
101
|
+
|
|
102
|
+
constructor() {
|
|
103
|
+
super();
|
|
104
|
+
|
|
105
|
+
// Forward terminal resize events so Ink can re-render on window size changes.
|
|
106
|
+
this.onResize = () => {
|
|
107
|
+
this.emit("resize");
|
|
108
|
+
};
|
|
109
|
+
process.stdout.on("resize", this.onResize);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cleanup any listeners. Safe to call multiple times.
|
|
114
|
+
*/
|
|
115
|
+
dispose(): void {
|
|
116
|
+
process.stdout.off("resize", this.onResize);
|
|
117
|
+
// Best-effort flush to avoid losing a last frame.
|
|
118
|
+
this.flush();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Writable _write implementation - buffers chunks and schedules flush.
|
|
123
|
+
*/
|
|
124
|
+
_write(
|
|
125
|
+
chunk: Buffer | string,
|
|
126
|
+
_encoding: BufferEncoding,
|
|
127
|
+
callback: (error?: Error | null) => void
|
|
128
|
+
): void {
|
|
129
|
+
this.buf += chunk.toString("utf8");
|
|
130
|
+
|
|
131
|
+
if (!this.scheduled) {
|
|
132
|
+
this.scheduled = true;
|
|
133
|
+
// Use setImmediate to batch multiple synchronous writes
|
|
134
|
+
setImmediate(() => this.flush());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
callback();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Flushes the accumulated buffer as a single atomic frame.
|
|
142
|
+
* Wraps output in synchronized update sequences to prevent flickering.
|
|
143
|
+
*/
|
|
144
|
+
flush(): void {
|
|
145
|
+
this.scheduled = false;
|
|
146
|
+
|
|
147
|
+
if (!this.buf) return;
|
|
148
|
+
|
|
149
|
+
const frame = this.buf;
|
|
150
|
+
this.buf = "";
|
|
151
|
+
|
|
152
|
+
// Atomic write with synchronized output wrapping:
|
|
153
|
+
// 1. Hide cursor to prevent cursor flicker
|
|
154
|
+
// 2. Begin synchronized update (terminal holds display)
|
|
155
|
+
// 3. Write the actual frame content
|
|
156
|
+
// 4. End synchronized update (terminal renders atomically)
|
|
157
|
+
// 5. Show cursor again
|
|
158
|
+
atomicWrite(HIDE_CURSOR + BSU + frame + ESU + SHOW_CURSOR);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Proxy common stdout properties for Ink compatibility.
|
|
163
|
+
*/
|
|
164
|
+
get columns(): number {
|
|
165
|
+
return process.stdout.columns ?? 80;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get rows(): number {
|
|
169
|
+
return process.stdout.rows ?? 24;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get isTTY(): boolean {
|
|
173
|
+
return process.stdout.isTTY ?? false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Preserve Ink color capability detection.
|
|
177
|
+
// Node's stdout provides these methods; delegate when available.
|
|
178
|
+
hasColors(count?: number): boolean {
|
|
179
|
+
const stdoutWithHasColors = process.stdout as unknown as {
|
|
180
|
+
hasColors?: (count?: number) => boolean;
|
|
181
|
+
};
|
|
182
|
+
return stdoutWithHasColors.hasColors?.(count) ?? false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
getColorDepth(env?: NodeJS.ProcessEnv): number {
|
|
186
|
+
const stdoutWithGetColorDepth = process.stdout as unknown as {
|
|
187
|
+
getColorDepth?: (env?: NodeJS.ProcessEnv) => number;
|
|
188
|
+
};
|
|
189
|
+
return stdoutWithGetColorDepth.getColorDepth?.(env) ?? 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// =============================================================================
|
|
194
|
+
// Factory Function
|
|
195
|
+
// =============================================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Creates a compatible stdout stream for Ink rendering.
|
|
199
|
+
*
|
|
200
|
+
* On Windows + VS Code terminal: Returns a BufferedStdout that implements
|
|
201
|
+
* synchronized output to prevent flickering.
|
|
202
|
+
*
|
|
203
|
+
* On other platforms/terminals: Returns the native process.stdout.
|
|
204
|
+
*
|
|
205
|
+
* @returns A WriteStream compatible with Ink's render() options
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* import { render } from 'ink';
|
|
210
|
+
* import { createCompatStdout } from './buffered-stdout';
|
|
211
|
+
*
|
|
212
|
+
* render(<App />, { stdout: createCompatStdout() });
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
export function createCompatStdout(): NodeJS.WriteStream {
|
|
216
|
+
// Only use buffered output on Windows + VS Code where flickering is most severe
|
|
217
|
+
if (process.platform === "win32" && isVsCodeTerminal() && (process.stdout.isTTY ?? false)) {
|
|
218
|
+
// Type assertion: BufferedStdout implements write semantics needed by Ink
|
|
219
|
+
// Full WriteStream interface (clearLine, cursorTo, etc.) not required for rendering
|
|
220
|
+
return new BufferedStdout() as unknown as NodeJS.WriteStream;
|
|
221
|
+
}
|
|
222
|
+
return process.stdout;
|
|
223
|
+
}
|