@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,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionListPanel Component (T056)
|
|
3
|
+
*
|
|
4
|
+
* Displays a scrollable list of sessions with keyboard navigation.
|
|
5
|
+
*
|
|
6
|
+
* @module tui/components/session/SessionListPanel
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Box, Text, useInput } from "ink";
|
|
10
|
+
import type React from "react";
|
|
11
|
+
import { useCallback, useMemo, useState } from "react";
|
|
12
|
+
import { useTheme } from "../../theme/index.js";
|
|
13
|
+
import { SessionItem } from "./SessionItem.js";
|
|
14
|
+
import type { SessionListPanelProps } from "./types.js";
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/** Default maximum height for the session list */
|
|
21
|
+
const DEFAULT_MAX_HEIGHT = 10;
|
|
22
|
+
|
|
23
|
+
/** Number of sessions to skip when using page up/down */
|
|
24
|
+
const PAGE_SIZE = 5;
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Helper Functions
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculate visible sessions based on scroll position and max height.
|
|
32
|
+
*/
|
|
33
|
+
function getVisibleSessions<T>(
|
|
34
|
+
sessions: readonly T[],
|
|
35
|
+
scrollOffset: number,
|
|
36
|
+
maxVisible: number
|
|
37
|
+
): readonly T[] {
|
|
38
|
+
return sessions.slice(scrollOffset, scrollOffset + maxVisible);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Main Component
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* SessionListPanel displays a scrollable list of sessions.
|
|
47
|
+
*
|
|
48
|
+
* Features:
|
|
49
|
+
* - j/k or arrow keys for navigation
|
|
50
|
+
* - Page up/down support
|
|
51
|
+
* - Home/End to jump to first/last
|
|
52
|
+
* - Visual scroll indicators
|
|
53
|
+
* - Highlights selected and active sessions
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <SessionListPanel
|
|
58
|
+
* sessions={sessionList}
|
|
59
|
+
* selectedSessionId="sess-1"
|
|
60
|
+
* activeSessionId="sess-2"
|
|
61
|
+
* onSelectSession={(id) => handleSelect(id)}
|
|
62
|
+
* maxHeight={10}
|
|
63
|
+
* isFocused={true}
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function SessionListPanel({
|
|
68
|
+
sessions,
|
|
69
|
+
selectedSessionId,
|
|
70
|
+
activeSessionId,
|
|
71
|
+
onSelectSession,
|
|
72
|
+
maxHeight = DEFAULT_MAX_HEIGHT,
|
|
73
|
+
isFocused = true,
|
|
74
|
+
}: SessionListPanelProps): React.JSX.Element {
|
|
75
|
+
const { theme } = useTheme();
|
|
76
|
+
|
|
77
|
+
// Track the index of the selected session
|
|
78
|
+
const [selectedIndex, setSelectedIndex] = useState(() => {
|
|
79
|
+
if (selectedSessionId) {
|
|
80
|
+
const index = sessions.findIndex((s) => s.id === selectedSessionId);
|
|
81
|
+
return index >= 0 ? index : 0;
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Track scroll offset for virtualization
|
|
87
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
88
|
+
|
|
89
|
+
// Calculate visible items (account for header)
|
|
90
|
+
const maxVisible = Math.max(1, maxHeight - 2);
|
|
91
|
+
|
|
92
|
+
// Get visible sessions based on scroll
|
|
93
|
+
const visibleSessions = useMemo(
|
|
94
|
+
() => getVisibleSessions(sessions, scrollOffset, maxVisible),
|
|
95
|
+
[sessions, scrollOffset, maxVisible]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Check if we can scroll
|
|
99
|
+
const canScrollUp = scrollOffset > 0;
|
|
100
|
+
const canScrollDown = scrollOffset + maxVisible < sessions.length;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Navigate to a specific index, adjusting scroll as needed.
|
|
104
|
+
*/
|
|
105
|
+
const navigateToIndex = useCallback(
|
|
106
|
+
(newIndex: number) => {
|
|
107
|
+
// Clamp index to valid range
|
|
108
|
+
const clampedIndex = Math.max(0, Math.min(newIndex, sessions.length - 1));
|
|
109
|
+
setSelectedIndex(clampedIndex);
|
|
110
|
+
|
|
111
|
+
// Adjust scroll to keep selection visible
|
|
112
|
+
if (clampedIndex < scrollOffset) {
|
|
113
|
+
setScrollOffset(clampedIndex);
|
|
114
|
+
} else if (clampedIndex >= scrollOffset + maxVisible) {
|
|
115
|
+
setScrollOffset(clampedIndex - maxVisible + 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Notify parent of selection change
|
|
119
|
+
if (onSelectSession && sessions[clampedIndex]) {
|
|
120
|
+
onSelectSession(sessions[clampedIndex].id);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
[sessions, scrollOffset, maxVisible, onSelectSession]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if key matches navigation down.
|
|
128
|
+
*/
|
|
129
|
+
const isNavigateDown = (input: string, key: { downArrow: boolean }) =>
|
|
130
|
+
input === "j" || key.downArrow;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if key matches navigation up.
|
|
134
|
+
*/
|
|
135
|
+
const isNavigateUp = (input: string, key: { upArrow: boolean }) => input === "k" || key.upArrow;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if key matches page navigation.
|
|
139
|
+
*/
|
|
140
|
+
const isPageNav = (
|
|
141
|
+
input: string,
|
|
142
|
+
key: { pageDown?: boolean; pageUp?: boolean; ctrl?: boolean }
|
|
143
|
+
): "down" | "up" | null => {
|
|
144
|
+
if (key.pageDown || (key.ctrl && input === "d")) return "down";
|
|
145
|
+
if (key.pageUp || (key.ctrl && input === "u")) return "up";
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Handle keyboard input
|
|
150
|
+
useInput(
|
|
151
|
+
(input, key) => {
|
|
152
|
+
if (isNavigateDown(input, key)) {
|
|
153
|
+
navigateToIndex(selectedIndex + 1);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (isNavigateUp(input, key)) {
|
|
157
|
+
navigateToIndex(selectedIndex - 1);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const pageDir = isPageNav(input, key);
|
|
161
|
+
if (pageDir === "down") {
|
|
162
|
+
navigateToIndex(selectedIndex + PAGE_SIZE);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (pageDir === "up") {
|
|
166
|
+
navigateToIndex(selectedIndex - PAGE_SIZE);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// g/G: vim-style jump to first/last
|
|
170
|
+
if (input === "g") {
|
|
171
|
+
navigateToIndex(0);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (input === "G") {
|
|
175
|
+
navigateToIndex(sessions.length - 1);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{ isActive: isFocused }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const textColor = theme.semantic.text.primary;
|
|
182
|
+
const mutedColor = theme.semantic.text.muted;
|
|
183
|
+
const borderColor = theme.semantic.border.default;
|
|
184
|
+
|
|
185
|
+
// Empty state
|
|
186
|
+
if (sessions.length === 0) {
|
|
187
|
+
return (
|
|
188
|
+
<Box
|
|
189
|
+
flexDirection="column"
|
|
190
|
+
borderStyle="single"
|
|
191
|
+
borderColor={borderColor}
|
|
192
|
+
paddingX={1}
|
|
193
|
+
height={maxHeight}
|
|
194
|
+
>
|
|
195
|
+
<Text color={textColor} bold>
|
|
196
|
+
Sessions
|
|
197
|
+
</Text>
|
|
198
|
+
<Box flexGrow={1} justifyContent="center" alignItems="center">
|
|
199
|
+
<Text color={mutedColor} italic>
|
|
200
|
+
No sessions found
|
|
201
|
+
</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
</Box>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Box
|
|
209
|
+
flexDirection="column"
|
|
210
|
+
borderStyle="single"
|
|
211
|
+
borderColor={borderColor}
|
|
212
|
+
paddingX={1}
|
|
213
|
+
height={maxHeight}
|
|
214
|
+
>
|
|
215
|
+
{/* Header with scroll indicators */}
|
|
216
|
+
<Box flexDirection="row" justifyContent="space-between">
|
|
217
|
+
<Text color={textColor} bold>
|
|
218
|
+
Sessions ({sessions.length})
|
|
219
|
+
</Text>
|
|
220
|
+
<Box flexDirection="row" gap={1}>
|
|
221
|
+
{canScrollUp && <Text color={mutedColor}>↑</Text>}
|
|
222
|
+
{canScrollDown && <Text color={mutedColor}>↓</Text>}
|
|
223
|
+
</Box>
|
|
224
|
+
</Box>
|
|
225
|
+
|
|
226
|
+
{/* Session list */}
|
|
227
|
+
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
228
|
+
{visibleSessions.map((session, visibleIndex) => {
|
|
229
|
+
const actualIndex = scrollOffset + visibleIndex;
|
|
230
|
+
return (
|
|
231
|
+
<SessionItem
|
|
232
|
+
key={session.id}
|
|
233
|
+
session={session}
|
|
234
|
+
isSelected={actualIndex === selectedIndex}
|
|
235
|
+
isActive={session.id === activeSessionId}
|
|
236
|
+
onSelect={onSelectSession}
|
|
237
|
+
/>
|
|
238
|
+
);
|
|
239
|
+
})}
|
|
240
|
+
</Box>
|
|
241
|
+
|
|
242
|
+
{/* Footer with navigation hints */}
|
|
243
|
+
<Box marginTop={0}>
|
|
244
|
+
<Text color={mutedColor} dimColor>
|
|
245
|
+
j/k: navigate • Enter: select • g/G: first/last
|
|
246
|
+
</Text>
|
|
247
|
+
</Box>
|
|
248
|
+
</Box>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default SessionListPanel;
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionPicker Component (T056)
|
|
3
|
+
*
|
|
4
|
+
* Modal dialog for selecting a session from the session list.
|
|
5
|
+
* Triggered by Ctrl+S keybinding.
|
|
6
|
+
*
|
|
7
|
+
* @module tui/components/session/SessionPicker
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getIcons } from "@vellum/shared";
|
|
11
|
+
import { Box, Text, useInput } from "ink";
|
|
12
|
+
import type React from "react";
|
|
13
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
14
|
+
import { useTheme } from "../../theme/index.js";
|
|
15
|
+
import { truncateToDisplayWidth } from "../../utils/index.js";
|
|
16
|
+
import { SessionPreview } from "./SessionPreview.js";
|
|
17
|
+
import type { SessionMetadata, SessionPickerProps, SessionPreviewMessage } from "./types.js";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Constants
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/** Maximum height for the session list */
|
|
24
|
+
const LIST_MAX_HEIGHT = 12;
|
|
25
|
+
|
|
26
|
+
/** Maximum height for the preview panel */
|
|
27
|
+
const PREVIEW_MAX_HEIGHT = 6;
|
|
28
|
+
|
|
29
|
+
/** Page size for navigation */
|
|
30
|
+
const PAGE_SIZE = 5;
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Helper Functions
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format timestamp for session item display.
|
|
38
|
+
*/
|
|
39
|
+
function formatTimestamp(date: Date): string {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const isToday =
|
|
42
|
+
date.getDate() === now.getDate() &&
|
|
43
|
+
date.getMonth() === now.getMonth() &&
|
|
44
|
+
date.getFullYear() === now.getFullYear();
|
|
45
|
+
|
|
46
|
+
if (isToday) {
|
|
47
|
+
return date.toLocaleTimeString(undefined, {
|
|
48
|
+
hour: "2-digit",
|
|
49
|
+
minute: "2-digit",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return date.toLocaleDateString(undefined, {
|
|
54
|
+
month: "short",
|
|
55
|
+
day: "numeric",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Truncate text with ellipsis.
|
|
61
|
+
* Uses string-width for accurate CJK/Emoji handling.
|
|
62
|
+
*/
|
|
63
|
+
function truncateText(text: string, maxLength: number): string {
|
|
64
|
+
return truncateToDisplayWidth(text, maxLength);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a minimal preview from session metadata.
|
|
69
|
+
* Used as a graceful fallback when message preview data is unavailable.
|
|
70
|
+
*/
|
|
71
|
+
function buildFallbackPreviewMessages(session: SessionMetadata): SessionPreviewMessage[] {
|
|
72
|
+
const messages: SessionPreviewMessage[] = [];
|
|
73
|
+
|
|
74
|
+
messages.push({
|
|
75
|
+
id: `${session.id}-preview-fallback-title`,
|
|
76
|
+
role: "user",
|
|
77
|
+
content: session.title,
|
|
78
|
+
timestamp: new Date(Math.max(0, session.timestamp.getTime() - 60000)),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (session.lastMessage) {
|
|
82
|
+
messages.push({
|
|
83
|
+
id: `${session.id}-preview-fallback-last`,
|
|
84
|
+
role: "assistant",
|
|
85
|
+
content: session.lastMessage,
|
|
86
|
+
timestamp: session.timestamp,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return messages;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Sub-Components
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Session list item in the picker.
|
|
99
|
+
*/
|
|
100
|
+
interface PickerItemProps {
|
|
101
|
+
readonly session: SessionMetadata;
|
|
102
|
+
readonly isSelected: boolean;
|
|
103
|
+
readonly isActive: boolean;
|
|
104
|
+
readonly primaryColor: string;
|
|
105
|
+
readonly textColor: string;
|
|
106
|
+
readonly mutedColor: string;
|
|
107
|
+
readonly successColor: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function PickerItem({
|
|
111
|
+
session,
|
|
112
|
+
isSelected,
|
|
113
|
+
isActive,
|
|
114
|
+
primaryColor,
|
|
115
|
+
textColor,
|
|
116
|
+
mutedColor,
|
|
117
|
+
successColor,
|
|
118
|
+
}: PickerItemProps): React.JSX.Element {
|
|
119
|
+
const indicator = isSelected ? "▶" : isActive ? "●" : " ";
|
|
120
|
+
const indicatorColor = isSelected ? primaryColor : isActive ? successColor : mutedColor;
|
|
121
|
+
const displayTitle = truncateText(session.title, 35);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Box flexDirection="row" paddingX={1}>
|
|
125
|
+
<Text color={indicatorColor}>{indicator} </Text>
|
|
126
|
+
<Box flexDirection="row" justifyContent="space-between" flexGrow={1}>
|
|
127
|
+
<Text color={isSelected ? primaryColor : textColor} bold={isSelected}>
|
|
128
|
+
{displayTitle}
|
|
129
|
+
</Text>
|
|
130
|
+
<Box flexDirection="row" gap={1}>
|
|
131
|
+
<Text dimColor>({session.messageCount})</Text>
|
|
132
|
+
<Text color={mutedColor}>{formatTimestamp(session.timestamp)}</Text>
|
|
133
|
+
</Box>
|
|
134
|
+
</Box>
|
|
135
|
+
</Box>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Main Component
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* SessionPicker is a modal dialog for selecting sessions.
|
|
145
|
+
*
|
|
146
|
+
* Features:
|
|
147
|
+
* - j/k or arrow keys for navigation
|
|
148
|
+
* - Enter to select session
|
|
149
|
+
* - Escape or q to close
|
|
150
|
+
* - Preview panel shows selected session messages
|
|
151
|
+
* - Page up/down for faster navigation
|
|
152
|
+
* - g/G to jump to first/last
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```tsx
|
|
156
|
+
* <SessionPicker
|
|
157
|
+
* sessions={sessionList}
|
|
158
|
+
* activeSessionId="current-session"
|
|
159
|
+
* onSelect={(id) => switchToSession(id)}
|
|
160
|
+
* onClose={() => setPickerOpen(false)}
|
|
161
|
+
* isOpen={pickerOpen}
|
|
162
|
+
* />
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export function SessionPicker({
|
|
166
|
+
sessions,
|
|
167
|
+
activeSessionId,
|
|
168
|
+
loadPreviewMessages,
|
|
169
|
+
onSelect,
|
|
170
|
+
onClose,
|
|
171
|
+
isOpen,
|
|
172
|
+
}: SessionPickerProps): React.JSX.Element | null {
|
|
173
|
+
const { theme } = useTheme();
|
|
174
|
+
|
|
175
|
+
// Track selection index
|
|
176
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
177
|
+
|
|
178
|
+
// Track scroll offset
|
|
179
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
180
|
+
|
|
181
|
+
// Prevent double-handling of input
|
|
182
|
+
const handledRef = useRef(false);
|
|
183
|
+
|
|
184
|
+
// Calculate visible sessions
|
|
185
|
+
const maxVisible = LIST_MAX_HEIGHT - 4; // Account for header, footer, borders
|
|
186
|
+
|
|
187
|
+
const visibleSessions = useMemo(
|
|
188
|
+
() => sessions.slice(scrollOffset, scrollOffset + maxVisible),
|
|
189
|
+
[sessions, scrollOffset, maxVisible]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Get selected session
|
|
193
|
+
const selectedSession = sessions[selectedIndex];
|
|
194
|
+
|
|
195
|
+
// Preview messages cache and state (performance: cache per session id)
|
|
196
|
+
const previewCacheRef = useRef<Map<string, readonly SessionPreviewMessage[]>>(new Map());
|
|
197
|
+
const [previewMessages, setPreviewMessages] = useState<readonly SessionPreviewMessage[]>([]);
|
|
198
|
+
const previewRequestIdRef = useRef(0);
|
|
199
|
+
|
|
200
|
+
// Keep preview messages in sync with selected session.
|
|
201
|
+
// Uses cache when available; otherwise loads via loadPreviewMessages and stores per id.
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!isOpen) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!selectedSession) {
|
|
208
|
+
setPreviewMessages([]);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const cached = previewCacheRef.current.get(selectedSession.id);
|
|
213
|
+
if (cached) {
|
|
214
|
+
setPreviewMessages(cached);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
previewRequestIdRef.current += 1;
|
|
219
|
+
const requestId = previewRequestIdRef.current;
|
|
220
|
+
|
|
221
|
+
const load = async () => {
|
|
222
|
+
if (!loadPreviewMessages) {
|
|
223
|
+
const fallback = buildFallbackPreviewMessages(selectedSession);
|
|
224
|
+
previewCacheRef.current.set(selectedSession.id, fallback);
|
|
225
|
+
if (previewRequestIdRef.current === requestId) {
|
|
226
|
+
setPreviewMessages(fallback);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const loaded = await loadPreviewMessages(selectedSession.id);
|
|
233
|
+
const messages = loaded ?? buildFallbackPreviewMessages(selectedSession);
|
|
234
|
+
previewCacheRef.current.set(selectedSession.id, messages);
|
|
235
|
+
if (previewRequestIdRef.current === requestId) {
|
|
236
|
+
setPreviewMessages(messages);
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
const fallback = buildFallbackPreviewMessages(selectedSession);
|
|
240
|
+
previewCacheRef.current.set(selectedSession.id, fallback);
|
|
241
|
+
if (previewRequestIdRef.current === requestId) {
|
|
242
|
+
setPreviewMessages(fallback);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
void load();
|
|
248
|
+
}, [isOpen, selectedSession, loadPreviewMessages]);
|
|
249
|
+
|
|
250
|
+
// Scroll indicators
|
|
251
|
+
const canScrollUp = scrollOffset > 0;
|
|
252
|
+
const canScrollDown = scrollOffset + maxVisible < sessions.length;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Navigate to a specific index.
|
|
256
|
+
*/
|
|
257
|
+
const navigateToIndex = useCallback(
|
|
258
|
+
(newIndex: number) => {
|
|
259
|
+
const clampedIndex = Math.max(0, Math.min(newIndex, sessions.length - 1));
|
|
260
|
+
setSelectedIndex(clampedIndex);
|
|
261
|
+
|
|
262
|
+
// Adjust scroll to keep selection visible
|
|
263
|
+
if (clampedIndex < scrollOffset) {
|
|
264
|
+
setScrollOffset(clampedIndex);
|
|
265
|
+
} else if (clampedIndex >= scrollOffset + maxVisible) {
|
|
266
|
+
setScrollOffset(clampedIndex - maxVisible + 1);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
[sessions.length, scrollOffset, maxVisible]
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Handle close action with double-handling prevention.
|
|
274
|
+
*/
|
|
275
|
+
const handleClose = useCallback(() => {
|
|
276
|
+
if (handledRef.current) return false;
|
|
277
|
+
handledRef.current = true;
|
|
278
|
+
onClose();
|
|
279
|
+
setTimeout(() => {
|
|
280
|
+
handledRef.current = false;
|
|
281
|
+
}, 0);
|
|
282
|
+
return true;
|
|
283
|
+
}, [onClose]);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Handle select action with double-handling prevention.
|
|
287
|
+
*/
|
|
288
|
+
const handleSelect = useCallback(() => {
|
|
289
|
+
if (handledRef.current || !selectedSession) return false;
|
|
290
|
+
handledRef.current = true;
|
|
291
|
+
onSelect(selectedSession.id);
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
handledRef.current = false;
|
|
294
|
+
}, 0);
|
|
295
|
+
return true;
|
|
296
|
+
}, [onSelect, selectedSession]);
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check navigation direction from key input.
|
|
300
|
+
*/
|
|
301
|
+
const getNavDirection = (
|
|
302
|
+
input: string,
|
|
303
|
+
key: {
|
|
304
|
+
downArrow: boolean;
|
|
305
|
+
upArrow: boolean;
|
|
306
|
+
pageDown?: boolean;
|
|
307
|
+
pageUp?: boolean;
|
|
308
|
+
ctrl?: boolean;
|
|
309
|
+
}
|
|
310
|
+
): number | null => {
|
|
311
|
+
if (input === "j" || key.downArrow) return 1;
|
|
312
|
+
if (input === "k" || key.upArrow) return -1;
|
|
313
|
+
if (key.pageDown || (key.ctrl && input === "d")) return PAGE_SIZE;
|
|
314
|
+
if (key.pageUp || (key.ctrl && input === "u")) return -PAGE_SIZE;
|
|
315
|
+
if (input === "g") return -selectedIndex; // Jump to 0
|
|
316
|
+
if (input === "G") return sessions.length - 1 - selectedIndex; // Jump to end
|
|
317
|
+
return null;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Handle keyboard input
|
|
321
|
+
useInput(
|
|
322
|
+
(input, key) => {
|
|
323
|
+
if (handledRef.current) return;
|
|
324
|
+
|
|
325
|
+
// Close: Escape or q
|
|
326
|
+
if (key.escape || input.toLowerCase() === "q") {
|
|
327
|
+
handleClose();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Select: Enter
|
|
332
|
+
if (key.return) {
|
|
333
|
+
handleSelect();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Navigation
|
|
338
|
+
const delta = getNavDirection(input, key);
|
|
339
|
+
if (delta !== null) {
|
|
340
|
+
navigateToIndex(selectedIndex + delta);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
{ isActive: isOpen }
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Don't render if not open
|
|
347
|
+
if (!isOpen) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const textColor = theme.semantic.text.primary;
|
|
352
|
+
const mutedColor = theme.semantic.text.muted;
|
|
353
|
+
const primaryColor = theme.colors.primary;
|
|
354
|
+
const successColor = theme.colors.success;
|
|
355
|
+
|
|
356
|
+
// Empty state
|
|
357
|
+
if (sessions.length === 0) {
|
|
358
|
+
return (
|
|
359
|
+
<Box
|
|
360
|
+
flexDirection="column"
|
|
361
|
+
borderStyle="double"
|
|
362
|
+
borderColor={primaryColor}
|
|
363
|
+
paddingX={2}
|
|
364
|
+
paddingY={1}
|
|
365
|
+
>
|
|
366
|
+
<Text color={textColor} bold>
|
|
367
|
+
{getIcons().plan} Select Session
|
|
368
|
+
</Text>
|
|
369
|
+
<Box marginY={1}>
|
|
370
|
+
<Text color={mutedColor} italic>
|
|
371
|
+
No sessions available
|
|
372
|
+
</Text>
|
|
373
|
+
</Box>
|
|
374
|
+
<Text color={mutedColor} dimColor>
|
|
375
|
+
Press <Text bold>Esc</Text> or <Text bold>q</Text> to close
|
|
376
|
+
</Text>
|
|
377
|
+
</Box>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<Box flexDirection="column">
|
|
383
|
+
{/* Main modal */}
|
|
384
|
+
<Box
|
|
385
|
+
flexDirection="column"
|
|
386
|
+
borderStyle="double"
|
|
387
|
+
borderColor={primaryColor}
|
|
388
|
+
paddingX={1}
|
|
389
|
+
paddingY={0}
|
|
390
|
+
>
|
|
391
|
+
{/* Header */}
|
|
392
|
+
<Box flexDirection="row" justifyContent="space-between" marginBottom={1}>
|
|
393
|
+
<Text color={textColor} bold>
|
|
394
|
+
{getIcons().plan} Select Session ({sessions.length})
|
|
395
|
+
</Text>
|
|
396
|
+
<Box flexDirection="row" gap={1}>
|
|
397
|
+
{canScrollUp && <Text color={mutedColor}>↑</Text>}
|
|
398
|
+
{canScrollDown && <Text color={mutedColor}>↓</Text>}
|
|
399
|
+
</Box>
|
|
400
|
+
</Box>
|
|
401
|
+
|
|
402
|
+
{/* Session list */}
|
|
403
|
+
<Box flexDirection="column">
|
|
404
|
+
{visibleSessions.map((session, visibleIndex) => {
|
|
405
|
+
const actualIndex = scrollOffset + visibleIndex;
|
|
406
|
+
return (
|
|
407
|
+
<PickerItem
|
|
408
|
+
key={session.id}
|
|
409
|
+
session={session}
|
|
410
|
+
isSelected={actualIndex === selectedIndex}
|
|
411
|
+
isActive={session.id === activeSessionId}
|
|
412
|
+
primaryColor={primaryColor}
|
|
413
|
+
textColor={textColor}
|
|
414
|
+
mutedColor={mutedColor}
|
|
415
|
+
successColor={successColor}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
})}
|
|
419
|
+
</Box>
|
|
420
|
+
|
|
421
|
+
{/* Footer with keybindings */}
|
|
422
|
+
<Box marginTop={1} flexDirection="row" gap={2}>
|
|
423
|
+
<Text color={mutedColor} dimColor>
|
|
424
|
+
<Text bold>j/k</Text> navigate
|
|
425
|
+
</Text>
|
|
426
|
+
<Text color={mutedColor} dimColor>
|
|
427
|
+
<Text bold>Enter</Text> select
|
|
428
|
+
</Text>
|
|
429
|
+
<Text color={mutedColor} dimColor>
|
|
430
|
+
<Text bold>Esc/q</Text> close
|
|
431
|
+
</Text>
|
|
432
|
+
</Box>
|
|
433
|
+
</Box>
|
|
434
|
+
|
|
435
|
+
{/* Preview panel */}
|
|
436
|
+
{selectedSession && (
|
|
437
|
+
<Box marginTop={1}>
|
|
438
|
+
<SessionPreview
|
|
439
|
+
messages={previewMessages}
|
|
440
|
+
title={selectedSession.title}
|
|
441
|
+
maxHeight={PREVIEW_MAX_HEIGHT}
|
|
442
|
+
/>
|
|
443
|
+
</Box>
|
|
444
|
+
)}
|
|
445
|
+
</Box>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export default SessionPicker;
|