@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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar panel data loaders.
|
|
3
|
+
*
|
|
4
|
+
* The MemoryPanel and TodoPanel are presentational components; the data is loaded
|
|
5
|
+
* by the app shell. This hook provides:
|
|
6
|
+
* - initial loading
|
|
7
|
+
* - refresh on panel open
|
|
8
|
+
* - refresh after relevant tool executions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MemoryEntry } from "@vellum/core";
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
|
+
import type { TodoItemData } from "../components/TodoItem.js";
|
|
14
|
+
import type { ToolExecution } from "../context/ToolsContext.js";
|
|
15
|
+
|
|
16
|
+
export type SidebarContent = "todo" | "memory" | "tools" | "mcp" | "help" | "snapshots";
|
|
17
|
+
|
|
18
|
+
export type SidebarPanelDataOptions = {
|
|
19
|
+
readonly sidebarVisible: boolean;
|
|
20
|
+
readonly sidebarContent: SidebarContent;
|
|
21
|
+
readonly executions: readonly ToolExecution[];
|
|
22
|
+
|
|
23
|
+
readonly loadTodos: () => Promise<readonly TodoItemData[]>;
|
|
24
|
+
readonly loadMemories: () => Promise<readonly MemoryEntry[]>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SidebarPanelData = {
|
|
28
|
+
readonly todoItems: readonly TodoItemData[];
|
|
29
|
+
readonly memoryEntries: readonly MemoryEntry[];
|
|
30
|
+
readonly refreshTodos: () => void;
|
|
31
|
+
readonly refreshMemories: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function getLastCompletedToolExecutionId(
|
|
35
|
+
executions: readonly ToolExecution[]
|
|
36
|
+
): string | undefined {
|
|
37
|
+
for (let i = executions.length - 1; i >= 0; i -= 1) {
|
|
38
|
+
const exec = executions[i];
|
|
39
|
+
if (exec?.status === "complete") return exec.id;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function shouldRefreshFromToolExecution(execution: ToolExecution | undefined): {
|
|
45
|
+
readonly refreshTodos: boolean;
|
|
46
|
+
readonly refreshMemories: boolean;
|
|
47
|
+
} {
|
|
48
|
+
const toolName = execution?.toolName;
|
|
49
|
+
if (!toolName) return { refreshTodos: false, refreshMemories: false };
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
refreshTodos: toolName === "todo_manage",
|
|
53
|
+
refreshMemories: toolName === "save_memory" || toolName === "recall_memory",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useSidebarPanelData(options: SidebarPanelDataOptions): SidebarPanelData {
|
|
58
|
+
const { sidebarVisible, sidebarContent, executions, loadTodos, loadMemories } = options;
|
|
59
|
+
|
|
60
|
+
const [todoItems, setTodoItems] = useState<readonly TodoItemData[]>([]);
|
|
61
|
+
const [memoryEntries, setMemoryEntries] = useState<readonly MemoryEntry[]>([]);
|
|
62
|
+
|
|
63
|
+
const refreshTodos = useCallback(() => {
|
|
64
|
+
void loadTodos()
|
|
65
|
+
.then(setTodoItems)
|
|
66
|
+
.catch(() => {
|
|
67
|
+
setTodoItems([]);
|
|
68
|
+
});
|
|
69
|
+
}, [loadTodos]);
|
|
70
|
+
|
|
71
|
+
const refreshMemories = useCallback(() => {
|
|
72
|
+
void loadMemories()
|
|
73
|
+
.then(setMemoryEntries)
|
|
74
|
+
.catch(() => {
|
|
75
|
+
setMemoryEntries([]);
|
|
76
|
+
});
|
|
77
|
+
}, [loadMemories]);
|
|
78
|
+
|
|
79
|
+
// Initial load (best-effort).
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
refreshTodos();
|
|
82
|
+
refreshMemories();
|
|
83
|
+
}, [refreshMemories, refreshTodos]);
|
|
84
|
+
|
|
85
|
+
// Refresh when the relevant panel is opened.
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!sidebarVisible) return;
|
|
88
|
+
|
|
89
|
+
if (sidebarContent === "todo") {
|
|
90
|
+
refreshTodos();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (sidebarContent === "memory") {
|
|
94
|
+
refreshMemories();
|
|
95
|
+
}
|
|
96
|
+
}, [refreshMemories, refreshTodos, sidebarContent, sidebarVisible]);
|
|
97
|
+
|
|
98
|
+
// Refresh when relevant tool executions complete.
|
|
99
|
+
const lastHandledExecutionIdRef = useRef<string | undefined>(undefined);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const executionId = getLastCompletedToolExecutionId(executions);
|
|
103
|
+
if (!executionId || executionId === lastHandledExecutionIdRef.current) return;
|
|
104
|
+
|
|
105
|
+
const execution = executions.find((e) => e.id === executionId);
|
|
106
|
+
const decision = shouldRefreshFromToolExecution(execution);
|
|
107
|
+
|
|
108
|
+
if (decision.refreshTodos) refreshTodos();
|
|
109
|
+
if (decision.refreshMemories) refreshMemories();
|
|
110
|
+
|
|
111
|
+
lastHandledExecutionIdRef.current = executionId;
|
|
112
|
+
}, [executions, refreshMemories, refreshTodos]);
|
|
113
|
+
|
|
114
|
+
return { todoItems, memoryEntries, refreshTodos, refreshMemories };
|
|
115
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smooth Scroll Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides smooth animated scrolling with easing functions.
|
|
5
|
+
* Uses setInterval-based updates (terminal-safe, no requestAnimationFrame).
|
|
6
|
+
*
|
|
7
|
+
* @module tui/hooks/useSmoothScroll
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useRef, useState } from "react";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Easing function type: maps progress (0-1) to eased value (0-1)
|
|
18
|
+
*/
|
|
19
|
+
export type EasingFunction = (t: number) => number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for smooth scroll behavior
|
|
23
|
+
*/
|
|
24
|
+
export interface SmoothScrollConfig {
|
|
25
|
+
/** Animation duration in ms (default: 150) */
|
|
26
|
+
readonly duration?: number;
|
|
27
|
+
/** Easing function (default: easeOutCubic) */
|
|
28
|
+
readonly easing?: EasingFunction;
|
|
29
|
+
/** Frame interval in ms (default: 16 ~= 60fps) */
|
|
30
|
+
readonly frameInterval?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return type for useSmoothScroll hook
|
|
35
|
+
*/
|
|
36
|
+
export interface UseSmoothScrollReturn {
|
|
37
|
+
/** Current interpolated scroll position */
|
|
38
|
+
readonly position: number;
|
|
39
|
+
/** Whether currently animating */
|
|
40
|
+
readonly isAnimating: boolean;
|
|
41
|
+
/** Start smooth scroll to target position */
|
|
42
|
+
readonly scrollTo: (target: number) => void;
|
|
43
|
+
/** Immediately jump to position (cancel any animation) */
|
|
44
|
+
readonly jumpTo: (target: number) => void;
|
|
45
|
+
/** Cancel current animation */
|
|
46
|
+
readonly cancel: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Easing Functions
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Standard easing functions for scroll animations
|
|
55
|
+
*/
|
|
56
|
+
export const easings = {
|
|
57
|
+
/** Linear - no easing */
|
|
58
|
+
linear: (t: number): number => t,
|
|
59
|
+
|
|
60
|
+
/** Ease out cubic - fast start, slow end (default) */
|
|
61
|
+
easeOutCubic: (t: number): number => 1 - (1 - t) ** 3,
|
|
62
|
+
|
|
63
|
+
/** Ease out quad - gentler ease out */
|
|
64
|
+
easeOutQuad: (t: number): number => 1 - (1 - t) ** 2,
|
|
65
|
+
|
|
66
|
+
/** Ease in out quad - smooth both ends */
|
|
67
|
+
easeInOutQuad: (t: number): number => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2),
|
|
68
|
+
|
|
69
|
+
/** Ease out expo - very fast start */
|
|
70
|
+
easeOutExpo: (t: number): number => (t === 1 ? 1 : 1 - 2 ** (-10 * t)),
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// Constants
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
const DEFAULT_CONFIG: Required<SmoothScrollConfig> = {
|
|
78
|
+
duration: 150,
|
|
79
|
+
easing: easings.easeOutCubic,
|
|
80
|
+
frameInterval: 16, // ~60fps
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Hook
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Hook for smooth animated scrolling
|
|
89
|
+
*
|
|
90
|
+
* @param initialPosition - Starting scroll position
|
|
91
|
+
* @param config - Optional animation configuration
|
|
92
|
+
* @returns Smooth scroll state and controls
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* const { position, scrollTo, isAnimating } = useSmoothScroll(0);
|
|
97
|
+
*
|
|
98
|
+
* // Smooth scroll to line 50
|
|
99
|
+
* scrollTo(50);
|
|
100
|
+
*
|
|
101
|
+
* // Use position for rendering
|
|
102
|
+
* <VirtualizedList offsetFromBottom={position} />
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function useSmoothScroll(
|
|
106
|
+
initialPosition: number,
|
|
107
|
+
config: SmoothScrollConfig = {}
|
|
108
|
+
): UseSmoothScrollReturn {
|
|
109
|
+
// Merge config with defaults
|
|
110
|
+
const { duration, easing, frameInterval } = { ...DEFAULT_CONFIG, ...config };
|
|
111
|
+
|
|
112
|
+
// State
|
|
113
|
+
const [position, setPosition] = useState(initialPosition);
|
|
114
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
115
|
+
|
|
116
|
+
// Refs for animation state
|
|
117
|
+
const animationRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
118
|
+
const startPositionRef = useRef(initialPosition);
|
|
119
|
+
const targetPositionRef = useRef(initialPosition);
|
|
120
|
+
const startTimeRef = useRef(0);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Cancel any running animation
|
|
124
|
+
*/
|
|
125
|
+
const cancel = useCallback(() => {
|
|
126
|
+
if (animationRef.current) {
|
|
127
|
+
clearInterval(animationRef.current);
|
|
128
|
+
animationRef.current = null;
|
|
129
|
+
}
|
|
130
|
+
setIsAnimating(false);
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Immediately jump to position (no animation)
|
|
135
|
+
*/
|
|
136
|
+
const jumpTo = useCallback(
|
|
137
|
+
(target: number) => {
|
|
138
|
+
cancel();
|
|
139
|
+
setPosition(target);
|
|
140
|
+
startPositionRef.current = target;
|
|
141
|
+
targetPositionRef.current = target;
|
|
142
|
+
},
|
|
143
|
+
[cancel]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Start smooth scroll to target position
|
|
148
|
+
*/
|
|
149
|
+
const scrollTo = useCallback(
|
|
150
|
+
(target: number) => {
|
|
151
|
+
// Cancel any existing animation
|
|
152
|
+
cancel();
|
|
153
|
+
|
|
154
|
+
// Get current position as start
|
|
155
|
+
const start = position;
|
|
156
|
+
|
|
157
|
+
// Skip animation if already at target or very close
|
|
158
|
+
if (Math.abs(target - start) < 0.5) {
|
|
159
|
+
setPosition(target);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Store animation parameters
|
|
164
|
+
startPositionRef.current = start;
|
|
165
|
+
targetPositionRef.current = target;
|
|
166
|
+
startTimeRef.current = Date.now();
|
|
167
|
+
|
|
168
|
+
setIsAnimating(true);
|
|
169
|
+
|
|
170
|
+
// Animation tick
|
|
171
|
+
const tick = () => {
|
|
172
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
173
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
174
|
+
const easedProgress = easing(progress);
|
|
175
|
+
|
|
176
|
+
const newPosition =
|
|
177
|
+
startPositionRef.current +
|
|
178
|
+
(targetPositionRef.current - startPositionRef.current) * easedProgress;
|
|
179
|
+
|
|
180
|
+
setPosition(newPosition);
|
|
181
|
+
|
|
182
|
+
if (progress >= 1) {
|
|
183
|
+
// Animation complete - snap to exact target
|
|
184
|
+
setPosition(targetPositionRef.current);
|
|
185
|
+
cancel();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Start animation loop
|
|
190
|
+
animationRef.current = setInterval(tick, frameInterval);
|
|
191
|
+
},
|
|
192
|
+
[cancel, position, duration, easing, frameInterval]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
position,
|
|
197
|
+
isAnimating,
|
|
198
|
+
scrollTo,
|
|
199
|
+
jumpTo,
|
|
200
|
+
cancel,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSnapshots Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides access to the Snapshot system for managing file state checkpoints.
|
|
5
|
+
* Uses the shadow Git repository in .vellum/.git-shadow/ for tracking.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/hooks/useSnapshots
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Snapshot, SnapshotError, SnapshotErrorCode, type SnapshotInfo } from "@vellum/core";
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result of a restore operation.
|
|
19
|
+
*/
|
|
20
|
+
export interface RestoreResult {
|
|
21
|
+
/** Whether the restore succeeded */
|
|
22
|
+
readonly success: boolean;
|
|
23
|
+
/** List of files restored */
|
|
24
|
+
readonly files: readonly string[];
|
|
25
|
+
/** Error message if failed */
|
|
26
|
+
readonly error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return type for the useSnapshots hook.
|
|
31
|
+
*/
|
|
32
|
+
export interface UseSnapshotsResult {
|
|
33
|
+
/** List of available snapshots (newest first) */
|
|
34
|
+
readonly snapshots: readonly SnapshotInfo[];
|
|
35
|
+
/** Whether snapshots are currently loading */
|
|
36
|
+
readonly isLoading: boolean;
|
|
37
|
+
/** Error message if any operation failed */
|
|
38
|
+
readonly error: string | null;
|
|
39
|
+
/** Whether the snapshot system is initialized */
|
|
40
|
+
readonly isInitialized: boolean;
|
|
41
|
+
/** Refresh the list of snapshots */
|
|
42
|
+
readonly refresh: () => Promise<void>;
|
|
43
|
+
/** Restore files to a specific snapshot */
|
|
44
|
+
readonly restore: (hash: string) => Promise<RestoreResult>;
|
|
45
|
+
/** Get diff between current state and a snapshot */
|
|
46
|
+
readonly diff: (hash: string) => Promise<string>;
|
|
47
|
+
/** Take a new snapshot */
|
|
48
|
+
readonly take: (message?: string) => Promise<string>;
|
|
49
|
+
/** Initialize the snapshot system if needed */
|
|
50
|
+
readonly initialize: () => Promise<boolean>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Constants
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
/** Maximum number of snapshots to show */
|
|
58
|
+
const MAX_SNAPSHOTS = 10;
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Hook Implementation
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hook for managing file state snapshots.
|
|
66
|
+
*
|
|
67
|
+
* Provides methods to list, create, restore, and diff snapshots.
|
|
68
|
+
* The snapshot system uses a shadow Git repository to track file states
|
|
69
|
+
* independently of the user's main repository.
|
|
70
|
+
*
|
|
71
|
+
* @param workingDir - The working directory path (defaults to cwd)
|
|
72
|
+
* @returns Snapshot management functions and state
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* function SnapshotPanel() {
|
|
77
|
+
* const {
|
|
78
|
+
* snapshots,
|
|
79
|
+
* isLoading,
|
|
80
|
+
* error,
|
|
81
|
+
* refresh,
|
|
82
|
+
* restore,
|
|
83
|
+
* take
|
|
84
|
+
* } = useSnapshots();
|
|
85
|
+
*
|
|
86
|
+
* if (isLoading) return <Text>Loading...</Text>;
|
|
87
|
+
*
|
|
88
|
+
* return (
|
|
89
|
+
* <Box flexDirection="column">
|
|
90
|
+
* {snapshots.map(s => (
|
|
91
|
+
* <Text key={s.hash}>{s.hash.slice(0, 7)} - {s.message}</Text>
|
|
92
|
+
* ))}
|
|
93
|
+
* </Box>
|
|
94
|
+
* );
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function useSnapshots(workingDir?: string): UseSnapshotsResult {
|
|
99
|
+
const resolvedDir = workingDir ?? process.cwd();
|
|
100
|
+
|
|
101
|
+
// State
|
|
102
|
+
const [snapshots, setSnapshots] = useState<readonly SnapshotInfo[]>([]);
|
|
103
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
104
|
+
const [error, setError] = useState<string | null>(null);
|
|
105
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
106
|
+
|
|
107
|
+
// Ref to track if we're mounted
|
|
108
|
+
const mountedRef = useRef(true);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Initialize the snapshot system.
|
|
112
|
+
*/
|
|
113
|
+
const initialize = useCallback(async (): Promise<boolean> => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await Snapshot.init(resolvedDir);
|
|
116
|
+
if (result.ok) {
|
|
117
|
+
if (mountedRef.current) {
|
|
118
|
+
setIsInitialized(true);
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (mountedRef.current) {
|
|
123
|
+
setError(result.error.message);
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (mountedRef.current) {
|
|
128
|
+
setError(err instanceof Error ? err.message : "Failed to initialize snapshots");
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}, [resolvedDir]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Refresh the list of snapshots.
|
|
136
|
+
*/
|
|
137
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Snapshot refresh requires multiple state checks and error handling paths
|
|
138
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
139
|
+
if (!mountedRef.current) return;
|
|
140
|
+
|
|
141
|
+
setIsLoading(true);
|
|
142
|
+
setError(null);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Check if initialized
|
|
146
|
+
const initialized = await Snapshot.isInitialized(resolvedDir);
|
|
147
|
+
if (!initialized) {
|
|
148
|
+
if (mountedRef.current) {
|
|
149
|
+
setSnapshots([]);
|
|
150
|
+
setIsInitialized(false);
|
|
151
|
+
setIsLoading(false);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (mountedRef.current) {
|
|
157
|
+
setIsInitialized(true);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// List snapshots
|
|
161
|
+
const result = await Snapshot.listSnapshots(resolvedDir);
|
|
162
|
+
|
|
163
|
+
if (!mountedRef.current) return;
|
|
164
|
+
|
|
165
|
+
if (result.ok) {
|
|
166
|
+
// Limit to MAX_SNAPSHOTS
|
|
167
|
+
setSnapshots(result.value.slice(0, MAX_SNAPSHOTS));
|
|
168
|
+
} else {
|
|
169
|
+
// Handle not initialized error gracefully
|
|
170
|
+
if (result.error.code === SnapshotErrorCode.NOT_INITIALIZED) {
|
|
171
|
+
setSnapshots([]);
|
|
172
|
+
setIsInitialized(false);
|
|
173
|
+
} else {
|
|
174
|
+
setError(result.error.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (mountedRef.current) {
|
|
179
|
+
setError(err instanceof Error ? err.message : "Failed to load snapshots");
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
if (mountedRef.current) {
|
|
183
|
+
setIsLoading(false);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}, [resolvedDir]);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Restore files to a specific snapshot.
|
|
190
|
+
*/
|
|
191
|
+
const restore = useCallback(
|
|
192
|
+
async (hash: string): Promise<RestoreResult> => {
|
|
193
|
+
try {
|
|
194
|
+
const result = await Snapshot.restore(resolvedDir, hash);
|
|
195
|
+
|
|
196
|
+
if (result.ok) {
|
|
197
|
+
// Refresh after successful restore
|
|
198
|
+
void refresh();
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
files: result.value,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
files: [],
|
|
208
|
+
error: result.error.message,
|
|
209
|
+
};
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
files: [],
|
|
214
|
+
error: err instanceof Error ? err.message : "Failed to restore snapshot",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
[resolvedDir, refresh]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get diff between current state and a snapshot.
|
|
223
|
+
*/
|
|
224
|
+
const diff = useCallback(
|
|
225
|
+
async (hash: string): Promise<string> => {
|
|
226
|
+
try {
|
|
227
|
+
const result = await Snapshot.diff(resolvedDir, hash);
|
|
228
|
+
|
|
229
|
+
if (result.ok) {
|
|
230
|
+
return result.value || "(no changes)";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return `Error: ${result.error.message}`;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return `Error: ${err instanceof Error ? err.message : "Failed to get diff"}`;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
[resolvedDir]
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Take a new snapshot.
|
|
243
|
+
*/
|
|
244
|
+
const take = useCallback(
|
|
245
|
+
async (message?: string): Promise<string> => {
|
|
246
|
+
try {
|
|
247
|
+
// Ensure initialized
|
|
248
|
+
const initialized = await Snapshot.isInitialized(resolvedDir);
|
|
249
|
+
if (!initialized) {
|
|
250
|
+
const initResult = await Snapshot.init(resolvedDir);
|
|
251
|
+
if (!initResult.ok) {
|
|
252
|
+
throw new SnapshotError(initResult.error.message, SnapshotErrorCode.OPERATION_FAILED);
|
|
253
|
+
}
|
|
254
|
+
if (mountedRef.current) {
|
|
255
|
+
setIsInitialized(true);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Track all files
|
|
260
|
+
const result = await Snapshot.track(resolvedDir, [], message ?? "Manual checkpoint");
|
|
261
|
+
|
|
262
|
+
if (result.ok) {
|
|
263
|
+
// Refresh after successful snapshot
|
|
264
|
+
void refresh();
|
|
265
|
+
return result.value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw new SnapshotError(result.error.message, result.error.code);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (err instanceof SnapshotError) {
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
throw new Error(err instanceof Error ? err.message : "Failed to take snapshot");
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
[resolvedDir, refresh]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Initial load
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
mountedRef.current = true;
|
|
282
|
+
void refresh();
|
|
283
|
+
|
|
284
|
+
return () => {
|
|
285
|
+
mountedRef.current = false;
|
|
286
|
+
};
|
|
287
|
+
}, [refresh]);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
snapshots,
|
|
291
|
+
isLoading,
|
|
292
|
+
error,
|
|
293
|
+
isInitialized,
|
|
294
|
+
refresh,
|
|
295
|
+
restore,
|
|
296
|
+
diff,
|
|
297
|
+
take,
|
|
298
|
+
initialize,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStateAndRef Hook
|
|
3
|
+
*
|
|
4
|
+
* Combines useState and useRef to provide both reactive state and
|
|
5
|
+
* a stable ref for use in callbacks without stale closures.
|
|
6
|
+
*
|
|
7
|
+
* Pattern from Gemini CLI - solves the problem where callbacks
|
|
8
|
+
* capture stale state values.
|
|
9
|
+
*
|
|
10
|
+
* @module tui/hooks/useStateAndRef
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useRef, useState } from "react";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Combines useState and useRef to provide both reactive state and
|
|
17
|
+
* a stable ref for use in callbacks without stale closures.
|
|
18
|
+
*
|
|
19
|
+
* Pattern from Gemini CLI - solves the problem where callbacks
|
|
20
|
+
* capture stale state values.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const [messages, messagesRef, setMessages] = useStateAndRef<Message[]>([]);
|
|
24
|
+
*
|
|
25
|
+
* // In callbacks, read from ref for latest value:
|
|
26
|
+
* const handleText = useCallback((text) => {
|
|
27
|
+
* const current = messagesRef.current; // Always latest
|
|
28
|
+
* setMessages([...current, { text }]);
|
|
29
|
+
* }, []); // Empty deps = stable callback
|
|
30
|
+
*
|
|
31
|
+
* @param initialValue - Initial state value
|
|
32
|
+
* @returns Tuple of [state, ref, setState]
|
|
33
|
+
*/
|
|
34
|
+
export function useStateAndRef<T>(
|
|
35
|
+
initialValue: T
|
|
36
|
+
): readonly [T, React.RefObject<T>, (newValue: T | ((prev: T) => T)) => void] {
|
|
37
|
+
const [state, setState] = useState<T>(initialValue);
|
|
38
|
+
const ref = useRef<T>(initialValue);
|
|
39
|
+
|
|
40
|
+
const setStateAndRef = useCallback((newValue: T | ((prev: T) => T)) => {
|
|
41
|
+
const resolved =
|
|
42
|
+
typeof newValue === "function" ? (newValue as (prev: T) => T)(ref.current) : newValue;
|
|
43
|
+
ref.current = resolved;
|
|
44
|
+
setState(resolved);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return [state, ref, setStateAndRef] as const;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default useStateAndRef;
|