@mseep/claudian 2.0.25
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/.env.local.example +2 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/duplicate-issues.yml +22 -0
- package/.github/workflows/release.yml +73 -0
- package/.github/workflows/stale.yml +21 -0
- package/.node-version +1 -0
- package/AGENTS.md +3 -0
- package/CLAUDE.md +80 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/assets/Preview.png +0 -0
- package/assets/sponsors/MOMA.png +0 -0
- package/bun.lock +1618 -0
- package/esbuild.config.mjs +195 -0
- package/eslint.config.mjs +143 -0
- package/jest.config.js +41 -0
- package/main.js +104915 -0
- package/manifest.json +10 -0
- package/package.json +65 -0
- package/scripts/build-css.mjs +119 -0
- package/scripts/build.mjs +19 -0
- package/scripts/postinstall.mjs +25 -0
- package/scripts/rendererSafeUnref.js +205 -0
- package/scripts/run-jest.js +19 -0
- package/scripts/sync-version.js +16 -0
- package/src/app/settings/ClaudianSettingsStorage.ts +435 -0
- package/src/app/settings/defaultSettings.ts +54 -0
- package/src/app/storage/SharedStorageService.ts +106 -0
- package/src/core/CLAUDE.md +84 -0
- package/src/core/auxiliary/AuxQueryRunner.ts +11 -0
- package/src/core/auxiliary/QueryBackedInlineEditService.ts +73 -0
- package/src/core/auxiliary/QueryBackedInstructionRefineService.ts +78 -0
- package/src/core/auxiliary/QueryBackedTitleGenerationService.ts +90 -0
- package/src/core/bootstrap/SessionStorage.ts +179 -0
- package/src/core/bootstrap/StoragePaths.ts +7 -0
- package/src/core/bootstrap/storage.ts +20 -0
- package/src/core/commands/builtInCommands.ts +141 -0
- package/src/core/mcp/McpConfigParser.ts +102 -0
- package/src/core/mcp/McpServerManager.ts +119 -0
- package/src/core/mcp/McpTester.ts +310 -0
- package/src/core/prompt/inlineEdit.ts +252 -0
- package/src/core/prompt/instructionRefine.ts +72 -0
- package/src/core/prompt/mainAgent.ts +213 -0
- package/src/core/prompt/titleGeneration.ts +44 -0
- package/src/core/providers/ProviderRegistry.ts +253 -0
- package/src/core/providers/ProviderSettingsCoordinator.ts +434 -0
- package/src/core/providers/ProviderWorkspaceRegistry.ts +119 -0
- package/src/core/providers/commands/ProviderCommandCatalog.ts +21 -0
- package/src/core/providers/commands/ProviderCommandEntry.ts +33 -0
- package/src/core/providers/commands/hiddenCommands.ts +74 -0
- package/src/core/providers/modelRouting.ts +17 -0
- package/src/core/providers/modelSelection.ts +72 -0
- package/src/core/providers/providerConfig.ts +34 -0
- package/src/core/providers/providerEnvironment.ts +364 -0
- package/src/core/providers/types.ts +544 -0
- package/src/core/runtime/ChatRuntime.ts +66 -0
- package/src/core/runtime/QueuedTurn.ts +97 -0
- package/src/core/runtime/types.ts +118 -0
- package/src/core/security/ApprovalManager.ts +142 -0
- package/src/core/storage/HomeFileAdapter.ts +75 -0
- package/src/core/storage/VaultFileAdapter.ts +132 -0
- package/src/core/tools/todo.ts +65 -0
- package/src/core/tools/toolIcons.ts +80 -0
- package/src/core/tools/toolInput.ts +119 -0
- package/src/core/tools/toolNames.ts +149 -0
- package/src/core/tools/toolResultContent.ts +26 -0
- package/src/core/types/agent.ts +28 -0
- package/src/core/types/chat.ts +177 -0
- package/src/core/types/diff.ts +31 -0
- package/src/core/types/index.ts +78 -0
- package/src/core/types/mcp.ts +97 -0
- package/src/core/types/plugins.ts +9 -0
- package/src/core/types/provider.ts +1 -0
- package/src/core/types/settings.ts +152 -0
- package/src/core/types/tools.ts +80 -0
- package/src/features/chat/CLAUDE.md +136 -0
- package/src/features/chat/ClaudianView.ts +762 -0
- package/src/features/chat/constants.ts +114 -0
- package/src/features/chat/controllers/BrowserSelectionController.ts +295 -0
- package/src/features/chat/controllers/CanvasSelectionController.ts +142 -0
- package/src/features/chat/controllers/ConversationController.ts +1103 -0
- package/src/features/chat/controllers/InputController.ts +1707 -0
- package/src/features/chat/controllers/NavigationController.ts +209 -0
- package/src/features/chat/controllers/SelectionController.ts +430 -0
- package/src/features/chat/controllers/StreamController.ts +1560 -0
- package/src/features/chat/controllers/contextRowVisibility.ts +18 -0
- package/src/features/chat/rendering/DiffRenderer.ts +134 -0
- package/src/features/chat/rendering/InlineAskUserQuestion.ts +702 -0
- package/src/features/chat/rendering/InlineExitPlanMode.ts +263 -0
- package/src/features/chat/rendering/InlinePlanApproval.ts +183 -0
- package/src/features/chat/rendering/MessageRenderer.ts +907 -0
- package/src/features/chat/rendering/SubagentRenderer.ts +678 -0
- package/src/features/chat/rendering/ThinkingBlockRenderer.ts +126 -0
- package/src/features/chat/rendering/TodoListRenderer.ts +5 -0
- package/src/features/chat/rendering/ToolCallRenderer.ts +1161 -0
- package/src/features/chat/rendering/WriteEditRenderer.ts +232 -0
- package/src/features/chat/rendering/collapsible.ts +101 -0
- package/src/features/chat/rendering/subagentLifecycleResolution.ts +25 -0
- package/src/features/chat/rendering/todoUtils.ts +29 -0
- package/src/features/chat/rewind.ts +31 -0
- package/src/features/chat/services/BangBashService.ts +56 -0
- package/src/features/chat/services/SubagentManager.ts +1107 -0
- package/src/features/chat/state/ChatState.ts +436 -0
- package/src/features/chat/state/types.ts +138 -0
- package/src/features/chat/tabs/Tab.ts +1886 -0
- package/src/features/chat/tabs/TabBar.ts +179 -0
- package/src/features/chat/tabs/TabManager.ts +1021 -0
- package/src/features/chat/tabs/providerResolution.ts +34 -0
- package/src/features/chat/tabs/types.ts +287 -0
- package/src/features/chat/ui/BangBashModeManager.ts +121 -0
- package/src/features/chat/ui/FileContext.ts +385 -0
- package/src/features/chat/ui/ImageContext.ts +366 -0
- package/src/features/chat/ui/InputToolbar.ts +1244 -0
- package/src/features/chat/ui/InstructionModeManager.ts +158 -0
- package/src/features/chat/ui/NavigationSidebar.ts +126 -0
- package/src/features/chat/ui/StatusPanel.ts +589 -0
- package/src/features/chat/ui/file-context/state/FileContextState.ts +83 -0
- package/src/features/chat/ui/file-context/view/FileChipsView.ts +70 -0
- package/src/features/chat/ui/textareaResize.ts +47 -0
- package/src/features/chat/utils/usageInfo.ts +26 -0
- package/src/features/inline-edit/ui/InlineEditModal.ts +895 -0
- package/src/features/inline-edit/ui/inlineEditMarkdownPreview.ts +55 -0
- package/src/features/settings/ClaudianSettings.ts +672 -0
- package/src/features/settings/keyboardNavigation.ts +60 -0
- package/src/features/settings/ui/EnvSnippetManager.ts +430 -0
- package/src/features/settings/ui/EnvironmentSettingsSection.ts +85 -0
- package/src/features/settings/ui/McpServerModal.ts +335 -0
- package/src/features/settings/ui/McpSettingsManager.ts +400 -0
- package/src/features/settings/ui/McpTestModal.ts +346 -0
- package/src/i18n/constants.ts +58 -0
- package/src/i18n/i18n.ts +140 -0
- package/src/i18n/locales/de.json +322 -0
- package/src/i18n/locales/en.json +322 -0
- package/src/i18n/locales/es.json +322 -0
- package/src/i18n/locales/fr.json +322 -0
- package/src/i18n/locales/ja.json +322 -0
- package/src/i18n/locales/ko.json +322 -0
- package/src/i18n/locales/pt.json +322 -0
- package/src/i18n/locales/ru.json +322 -0
- package/src/i18n/locales/zh-CN.json +322 -0
- package/src/i18n/locales/zh-TW.json +322 -0
- package/src/i18n/types.ts +248 -0
- package/src/main.ts +772 -0
- package/src/providers/acp/AcpClientConnection.ts +361 -0
- package/src/providers/acp/AcpJsonRpcTransport.ts +427 -0
- package/src/providers/acp/AcpSessionConfig.ts +139 -0
- package/src/providers/acp/AcpSessionUpdateNormalizer.ts +371 -0
- package/src/providers/acp/AcpSubprocess.ts +155 -0
- package/src/providers/acp/AcpToolStreamAdapter.ts +132 -0
- package/src/providers/acp/buildAcpUsageInfo.ts +41 -0
- package/src/providers/acp/index.ts +9 -0
- package/src/providers/acp/methodNames.ts +50 -0
- package/src/providers/acp/types.ts +566 -0
- package/src/providers/claude/CLAUDE.md +78 -0
- package/src/providers/claude/agents/AgentManager.ts +225 -0
- package/src/providers/claude/agents/AgentStorage.ts +101 -0
- package/src/providers/claude/app/ClaudeWorkspaceServices.ts +90 -0
- package/src/providers/claude/auxiliary/ClaudeInlineEditService.ts +122 -0
- package/src/providers/claude/auxiliary/ClaudeInstructionRefineService.ts +90 -0
- package/src/providers/claude/auxiliary/ClaudeTitleGenerationService.ts +129 -0
- package/src/providers/claude/auxiliary/extractAssistantText.ts +28 -0
- package/src/providers/claude/capabilities.ts +17 -0
- package/src/providers/claude/cli/findClaudeCLIPath.ts +261 -0
- package/src/providers/claude/commands/ClaudeCommandCatalog.ts +149 -0
- package/src/providers/claude/commands/probeRuntimeCommands.ts +86 -0
- package/src/providers/claude/env/ClaudeSettingsReconciler.ts +90 -0
- package/src/providers/claude/env/claudeModelEnv.ts +86 -0
- package/src/providers/claude/history/ClaudeConversationHistoryService.ts +446 -0
- package/src/providers/claude/history/ClaudeHistoryStore.ts +170 -0
- package/src/providers/claude/history/sdkAsyncSubagent.ts +92 -0
- package/src/providers/claude/history/sdkBranchFilter.ts +271 -0
- package/src/providers/claude/history/sdkHistoryTypes.ts +61 -0
- package/src/providers/claude/history/sdkMessageParsing.ts +413 -0
- package/src/providers/claude/history/sdkSessionPaths.ts +98 -0
- package/src/providers/claude/history/sdkSubagentSidecar.ts +261 -0
- package/src/providers/claude/hooks/SubagentHooks.ts +31 -0
- package/src/providers/claude/modelLabels.ts +77 -0
- package/src/providers/claude/modelOptions.ts +113 -0
- package/src/providers/claude/modelSelection.ts +17 -0
- package/src/providers/claude/plugins/PluginManager.ts +194 -0
- package/src/providers/claude/prompt/ClaudeTurnEncoder.ts +46 -0
- package/src/providers/claude/registration.ts +39 -0
- package/src/providers/claude/runtime/ClaudeApprovalHandler.ts +153 -0
- package/src/providers/claude/runtime/ClaudeChatRuntime.ts +1812 -0
- package/src/providers/claude/runtime/ClaudeCliResolver.ts +94 -0
- package/src/providers/claude/runtime/ClaudeDynamicUpdates.ts +164 -0
- package/src/providers/claude/runtime/ClaudeMessageChannel.ts +209 -0
- package/src/providers/claude/runtime/ClaudeQueryOptionsBuilder.ts +315 -0
- package/src/providers/claude/runtime/ClaudeRewindService.ts +220 -0
- package/src/providers/claude/runtime/ClaudeSessionManager.ts +92 -0
- package/src/providers/claude/runtime/ClaudeTaskResultInterpreter.ts +172 -0
- package/src/providers/claude/runtime/ClaudeUserMessageFactory.ts +83 -0
- package/src/providers/claude/runtime/claudeColdStartQuery.ts +152 -0
- package/src/providers/claude/runtime/customSpawn.ts +87 -0
- package/src/providers/claude/runtime/types.ts +134 -0
- package/src/providers/claude/sdk/messages.ts +17 -0
- package/src/providers/claude/sdk/toolResultContent.ts +4 -0
- package/src/providers/claude/sdk/typeGuards.ts +14 -0
- package/src/providers/claude/sdk/types.ts +15 -0
- package/src/providers/claude/security/ClaudePermissionUpdates.ts +44 -0
- package/src/providers/claude/settings.ts +138 -0
- package/src/providers/claude/storage/AgentVaultStorage.ts +101 -0
- package/src/providers/claude/storage/CCSettingsStorage.ts +153 -0
- package/src/providers/claude/storage/ClaudianSettingsStorage.ts +6 -0
- package/src/providers/claude/storage/McpStorage.ts +139 -0
- package/src/providers/claude/storage/SessionStorage.ts +5 -0
- package/src/providers/claude/storage/SkillStorage.ts +61 -0
- package/src/providers/claude/storage/SlashCommandStorage.ts +96 -0
- package/src/providers/claude/storage/StorageService.ts +185 -0
- package/src/providers/claude/stream/toolInputStreamState.ts +318 -0
- package/src/providers/claude/stream/transformClaudeMessage.ts +586 -0
- package/src/providers/claude/types/agent.ts +2 -0
- package/src/providers/claude/types/models.ts +168 -0
- package/src/providers/claude/types/plugins.ts +14 -0
- package/src/providers/claude/types/providerState.ts +16 -0
- package/src/providers/claude/types/settings.ts +98 -0
- package/src/providers/claude/ui/AgentSettings.ts +389 -0
- package/src/providers/claude/ui/ClaudeChatUIConfig.ts +113 -0
- package/src/providers/claude/ui/ClaudeSettingsTab.ts +407 -0
- package/src/providers/claude/ui/PluginSettingsManager.ts +149 -0
- package/src/providers/claude/ui/SlashCommandSettings.ts +527 -0
- package/src/providers/codex/CLAUDE.md +64 -0
- package/src/providers/codex/agents/CodexAgentMentionProvider.ts +33 -0
- package/src/providers/codex/app/CodexWorkspaceServices.ts +76 -0
- package/src/providers/codex/auxiliary/CodexInlineEditService.ts +9 -0
- package/src/providers/codex/auxiliary/CodexInstructionRefineService.ts +9 -0
- package/src/providers/codex/auxiliary/CodexTaskResultInterpreter.ts +29 -0
- package/src/providers/codex/auxiliary/CodexTitleGenerationService.ts +22 -0
- package/src/providers/codex/capabilities.ts +16 -0
- package/src/providers/codex/commands/CodexSkillCatalog.ts +180 -0
- package/src/providers/codex/env/CodexSettingsReconciler.ts +68 -0
- package/src/providers/codex/history/CodexConversationHistoryService.ts +212 -0
- package/src/providers/codex/history/CodexHistoryStore.ts +1672 -0
- package/src/providers/codex/modelOptions.ts +99 -0
- package/src/providers/codex/modelSelection.ts +17 -0
- package/src/providers/codex/normalization/codexSubagentNormalization.ts +227 -0
- package/src/providers/codex/normalization/codexToolNormalization.ts +390 -0
- package/src/providers/codex/prompt/encodeCodexTurn.ts +57 -0
- package/src/providers/codex/registration.ts +29 -0
- package/src/providers/codex/runtime/CodexAppServerProcess.ts +105 -0
- package/src/providers/codex/runtime/CodexAuxQueryRunner.ts +180 -0
- package/src/providers/codex/runtime/CodexBinaryLocator.ts +49 -0
- package/src/providers/codex/runtime/CodexChatRuntime.ts +1296 -0
- package/src/providers/codex/runtime/CodexCliResolver.ts +65 -0
- package/src/providers/codex/runtime/CodexExecutionTargetResolver.ts +104 -0
- package/src/providers/codex/runtime/CodexLaunchSpecBuilder.ts +85 -0
- package/src/providers/codex/runtime/CodexNotificationRouter.ts +1033 -0
- package/src/providers/codex/runtime/CodexPathMapper.ts +155 -0
- package/src/providers/codex/runtime/CodexRpcTransport.ts +171 -0
- package/src/providers/codex/runtime/CodexRuntimeContext.ts +109 -0
- package/src/providers/codex/runtime/CodexServerRequestRouter.ts +331 -0
- package/src/providers/codex/runtime/CodexSessionFileTail.ts +792 -0
- package/src/providers/codex/runtime/CodexSessionManager.ts +39 -0
- package/src/providers/codex/runtime/codexAppServerSupport.ts +58 -0
- package/src/providers/codex/runtime/codexAppServerTypes.ts +705 -0
- package/src/providers/codex/runtime/codexLaunchTypes.ts +30 -0
- package/src/providers/codex/settings.ts +236 -0
- package/src/providers/codex/skills/CodexSkillListingService.ts +173 -0
- package/src/providers/codex/storage/CodexSkillStorage.ts +250 -0
- package/src/providers/codex/storage/CodexSubagentStorage.ts +212 -0
- package/src/providers/codex/types/index.ts +16 -0
- package/src/providers/codex/types/models.ts +46 -0
- package/src/providers/codex/types/subagent.ts +23 -0
- package/src/providers/codex/ui/CodexChatUIConfig.ts +128 -0
- package/src/providers/codex/ui/CodexSettingsTab.ts +432 -0
- package/src/providers/codex/ui/CodexSkillSettings.ts +275 -0
- package/src/providers/codex/ui/CodexSubagentSettings.ts +400 -0
- package/src/providers/defaultProviderConfigs.ts +14 -0
- package/src/providers/index.ts +30 -0
- package/src/providers/opencode/agents/OpencodeAgentMentionProvider.ts +42 -0
- package/src/providers/opencode/app/OpencodeRuntimeCommandLoader.ts +67 -0
- package/src/providers/opencode/app/OpencodeWorkspaceServices.ts +55 -0
- package/src/providers/opencode/auxiliary/OpencodeInlineEditService.ts +13 -0
- package/src/providers/opencode/auxiliary/OpencodeInstructionRefineService.ts +12 -0
- package/src/providers/opencode/auxiliary/OpencodeTaskResultInterpreter.ts +29 -0
- package/src/providers/opencode/auxiliary/OpencodeTitleGenerationService.ts +27 -0
- package/src/providers/opencode/capabilities.ts +16 -0
- package/src/providers/opencode/commands/OpencodeCommandCatalog.ts +92 -0
- package/src/providers/opencode/discoveryState.ts +135 -0
- package/src/providers/opencode/env/OpencodeSettingsReconciler.ts +178 -0
- package/src/providers/opencode/history/OpencodeConversationHistoryService.ts +84 -0
- package/src/providers/opencode/history/OpencodeHistoryStore.ts +472 -0
- package/src/providers/opencode/history/OpencodeSqliteReader.ts +284 -0
- package/src/providers/opencode/internal/compareCollections.ts +72 -0
- package/src/providers/opencode/internal/providerProjection.ts +15 -0
- package/src/providers/opencode/models.ts +378 -0
- package/src/providers/opencode/modes.ts +150 -0
- package/src/providers/opencode/normalization/opencodeToolNormalization.ts +406 -0
- package/src/providers/opencode/registration.ts +27 -0
- package/src/providers/opencode/runtime/OpencodeAuxQueryRunner.ts +436 -0
- package/src/providers/opencode/runtime/OpencodeChatRuntime.ts +1603 -0
- package/src/providers/opencode/runtime/OpencodeCliResolver.ts +57 -0
- package/src/providers/opencode/runtime/OpencodeLaunchArtifacts.ts +231 -0
- package/src/providers/opencode/runtime/OpencodePaths.ts +113 -0
- package/src/providers/opencode/runtime/OpencodeRuntimeEnvironment.ts +18 -0
- package/src/providers/opencode/runtime/buildOpencodePrompt.ts +66 -0
- package/src/providers/opencode/settings.ts +427 -0
- package/src/providers/opencode/storage/OpencodeAgentStorage.ts +346 -0
- package/src/providers/opencode/types/agent.ts +37 -0
- package/src/providers/opencode/types/index.ts +9 -0
- package/src/providers/opencode/ui/OpencodeAgentSettings.ts +579 -0
- package/src/providers/opencode/ui/OpencodeChatUIConfig.ts +316 -0
- package/src/providers/opencode/ui/OpencodeSettingsTab.ts +674 -0
- package/src/providers/pi/app/PiRuntimeCommandLoader.ts +57 -0
- package/src/providers/pi/app/PiWorkspaceServices.ts +39 -0
- package/src/providers/pi/auxiliary/PiInlineEditService.ts +9 -0
- package/src/providers/pi/auxiliary/PiInstructionRefineService.ts +9 -0
- package/src/providers/pi/auxiliary/PiTaskResultInterpreter.ts +29 -0
- package/src/providers/pi/auxiliary/PiTitleGenerationService.ts +19 -0
- package/src/providers/pi/capabilities.ts +16 -0
- package/src/providers/pi/commands/PiCommandCatalog.ts +92 -0
- package/src/providers/pi/env/PiSettingsReconciler.ts +180 -0
- package/src/providers/pi/history/PiConversationHistoryService.ts +123 -0
- package/src/providers/pi/history/PiHistoryStore.ts +664 -0
- package/src/providers/pi/internal/compareCollections.ts +4 -0
- package/src/providers/pi/internal/providerProjection.ts +18 -0
- package/src/providers/pi/models.ts +302 -0
- package/src/providers/pi/normalizations/piEventNormalization.ts +211 -0
- package/src/providers/pi/normalizations/piToolNormalization.ts +97 -0
- package/src/providers/pi/registration.ts +30 -0
- package/src/providers/pi/runtime/PiAuxQueryRunner.ts +216 -0
- package/src/providers/pi/runtime/PiChatRuntime.ts +1064 -0
- package/src/providers/pi/runtime/PiCliResolver.ts +53 -0
- package/src/providers/pi/runtime/PiExtensionUiBridge.ts +161 -0
- package/src/providers/pi/runtime/PiJsonl.ts +71 -0
- package/src/providers/pi/runtime/PiLaunchSpec.ts +70 -0
- package/src/providers/pi/runtime/PiModelDiscoveryService.ts +92 -0
- package/src/providers/pi/runtime/PiRpcPayloads.ts +18 -0
- package/src/providers/pi/runtime/PiRpcTransport.ts +243 -0
- package/src/providers/pi/runtime/PiSubprocess.ts +159 -0
- package/src/providers/pi/runtime/buildPiPrompt.ts +62 -0
- package/src/providers/pi/runtime/buildPiUsageInfo.ts +69 -0
- package/src/providers/pi/settings.ts +468 -0
- package/src/providers/pi/types.ts +64 -0
- package/src/providers/pi/ui/ObsidianPiExtensionUiRenderer.ts +251 -0
- package/src/providers/pi/ui/PiChatUIConfig.ts +265 -0
- package/src/providers/pi/ui/PiExtensionUiRenderer.ts +12 -0
- package/src/providers/pi/ui/PiSettingsTab.ts +642 -0
- package/src/shared/components/ResumeSessionDropdown.ts +185 -0
- package/src/shared/components/SelectableDropdown.ts +140 -0
- package/src/shared/components/SelectionHighlight.ts +77 -0
- package/src/shared/components/SlashCommandDropdown.ts +421 -0
- package/src/shared/icons.ts +180 -0
- package/src/shared/mention/MentionDropdownController.ts +627 -0
- package/src/shared/mention/VaultMentionCache.ts +106 -0
- package/src/shared/mention/VaultMentionDataProvider.ts +51 -0
- package/src/shared/mention/types.ts +67 -0
- package/src/shared/modals/ConfirmModal.ts +60 -0
- package/src/shared/modals/ForkTargetModal.ts +47 -0
- package/src/shared/modals/InstructionConfirmModal.ts +281 -0
- package/src/style/CLAUDE.md +49 -0
- package/src/style/accessibility.css +40 -0
- package/src/style/base/animations.css +44 -0
- package/src/style/base/container.css +20 -0
- package/src/style/base/variables.css +46 -0
- package/src/style/base/visibility.css +15 -0
- package/src/style/components/code.css +97 -0
- package/src/style/components/context-footer.css +76 -0
- package/src/style/components/header.css +27 -0
- package/src/style/components/history.css +221 -0
- package/src/style/components/input.css +312 -0
- package/src/style/components/messages.css +262 -0
- package/src/style/components/nav-sidebar.css +58 -0
- package/src/style/components/status-panel.css +202 -0
- package/src/style/components/subagent.css +248 -0
- package/src/style/components/tabs.css +112 -0
- package/src/style/components/thinking.css +88 -0
- package/src/style/components/toolcalls.css +278 -0
- package/src/style/features/ask-user-question.css +315 -0
- package/src/style/features/diff.css +197 -0
- package/src/style/features/file-context.css +188 -0
- package/src/style/features/file-link.css +22 -0
- package/src/style/features/image-context.css +179 -0
- package/src/style/features/image-embed.css +40 -0
- package/src/style/features/image-modal.css +52 -0
- package/src/style/features/inline-edit.css +278 -0
- package/src/style/features/plan-mode.css +103 -0
- package/src/style/features/resume-session.css +119 -0
- package/src/style/features/slash-commands.css +91 -0
- package/src/style/index.css +63 -0
- package/src/style/modals/fork-target.css +21 -0
- package/src/style/modals/instruction.css +161 -0
- package/src/style/modals/mcp-modal.css +241 -0
- package/src/style/settings/agent-settings.css +2 -0
- package/src/style/settings/base.css +300 -0
- package/src/style/settings/env-snippets.css +366 -0
- package/src/style/settings/mcp-settings.css +211 -0
- package/src/style/settings/plugin-settings.css +164 -0
- package/src/style/settings/provider-model-picker.css +367 -0
- package/src/style/settings/slash-settings.css +16 -0
- package/src/style/toolbar/external-context.css +177 -0
- package/src/style/toolbar/mcp-selector.css +176 -0
- package/src/style/toolbar/mode-selector.css +19 -0
- package/src/style/toolbar/model-selector.css +99 -0
- package/src/style/toolbar/permission-toggle.css +56 -0
- package/src/style/toolbar/service-tier-toggle.css +39 -0
- package/src/style/toolbar/thinking-selector.css +83 -0
- package/src/types/smol-toml.d.ts +4 -0
- package/src/utils/agent.ts +50 -0
- package/src/utils/animationFrame.ts +46 -0
- package/src/utils/browser.ts +46 -0
- package/src/utils/canvas.ts +14 -0
- package/src/utils/cliBinaryLocator.ts +97 -0
- package/src/utils/context.ts +117 -0
- package/src/utils/contextMentionResolver.ts +154 -0
- package/src/utils/date.ts +31 -0
- package/src/utils/diff.ts +384 -0
- package/src/utils/editor.ts +104 -0
- package/src/utils/electronCompat.ts +53 -0
- package/src/utils/env.ts +465 -0
- package/src/utils/externalContext.ts +143 -0
- package/src/utils/externalContextScanner.ts +135 -0
- package/src/utils/fileLink.ts +263 -0
- package/src/utils/frontmatter.ts +194 -0
- package/src/utils/imageEmbed.ts +139 -0
- package/src/utils/inlineEdit.ts +22 -0
- package/src/utils/interrupt.ts +23 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/markdownMath.ts +130 -0
- package/src/utils/mcp.ts +96 -0
- package/src/utils/obsidianCompat.ts +23 -0
- package/src/utils/path.ts +342 -0
- package/src/utils/session.ts +240 -0
- package/src/utils/slashCommand.ts +152 -0
- package/src/utils/subagentJsonl.ts +52 -0
- package/src/utils/windowsCmdShim.ts +98 -0
- package/tests/__mocks__/claude-agent-sdk.ts +317 -0
- package/tests/__mocks__/codex-sdk.ts +88 -0
- package/tests/__mocks__/obsidian.ts +434 -0
- package/tests/helpers/mockElement.ts +403 -0
- package/tests/helpers/sdkMessages.ts +291 -0
- package/tests/integration/core/agent/ClaudianService.test.ts +1845 -0
- package/tests/integration/core/mcp/mcp.test.ts +905 -0
- package/tests/integration/features/chat/imagePersistence.test.ts +38 -0
- package/tests/integration/main.test.ts +1701 -0
- package/tests/setupWindow.ts +26 -0
- package/tests/tsconfig.json +7 -0
- package/tests/unit/core/commands/builtInCommands.test.ts +239 -0
- package/tests/unit/core/mcp/McpServerManager.test.ts +405 -0
- package/tests/unit/core/mcp/McpTester.test.ts +282 -0
- package/tests/unit/core/mcp/createNodeFetch.test.ts +188 -0
- package/tests/unit/core/providers/ProviderRegistry.test.ts +275 -0
- package/tests/unit/core/providers/ProviderSettingsCoordinator.test.ts +490 -0
- package/tests/unit/core/providers/ProviderWorkspaceRegistry.test.ts +84 -0
- package/tests/unit/core/providers/modelRouting.test.ts +91 -0
- package/tests/unit/core/providers/modelSelection.test.ts +155 -0
- package/tests/unit/core/providers/providerEnvironment.test.ts +162 -0
- package/tests/unit/core/providers/tabLifecycle.test.ts +217 -0
- package/tests/unit/core/security/ApprovalManager.test.ts +152 -0
- package/tests/unit/core/storage/VaultFileAdapter.test.ts +535 -0
- package/tests/unit/core/tools/todo.test.ts +227 -0
- package/tests/unit/core/tools/toolIcons.test.ts +75 -0
- package/tests/unit/core/tools/toolInput.test.ts +350 -0
- package/tests/unit/core/tools/toolNames.test.ts +464 -0
- package/tests/unit/core/types/mcp.test.ts +115 -0
- package/tests/unit/features/chat/ClaudianView.test.ts +404 -0
- package/tests/unit/features/chat/controllers/BrowserSelectionController.test.ts +179 -0
- package/tests/unit/features/chat/controllers/CanvasSelectionController.test.ts +216 -0
- package/tests/unit/features/chat/controllers/ConversationController.test.ts +2764 -0
- package/tests/unit/features/chat/controllers/InputController.test.ts +3188 -0
- package/tests/unit/features/chat/controllers/NavigationController.test.ts +640 -0
- package/tests/unit/features/chat/controllers/SelectionController.test.ts +695 -0
- package/tests/unit/features/chat/controllers/StreamController.test.ts +2534 -0
- package/tests/unit/features/chat/controllers/contextRowVisibility.test.ts +46 -0
- package/tests/unit/features/chat/controllers/index.test.ts +16 -0
- package/tests/unit/features/chat/rendering/DiffRenderer.test.ts +355 -0
- package/tests/unit/features/chat/rendering/InlineAskUserQuestion.test.ts +1035 -0
- package/tests/unit/features/chat/rendering/InlineExitPlanMode.test.ts +191 -0
- package/tests/unit/features/chat/rendering/InlinePlanApproval.test.ts +126 -0
- package/tests/unit/features/chat/rendering/MessageRenderer.test.ts +2004 -0
- package/tests/unit/features/chat/rendering/SubagentRenderer.test.ts +917 -0
- package/tests/unit/features/chat/rendering/ThinkingBlockRenderer.test.ts +124 -0
- package/tests/unit/features/chat/rendering/TodoListRenderer.test.ts +173 -0
- package/tests/unit/features/chat/rendering/ToolCallRenderer.test.ts +909 -0
- package/tests/unit/features/chat/rendering/WriteEditRenderer.test.ts +474 -0
- package/tests/unit/features/chat/rendering/collapsible.test.ts +158 -0
- package/tests/unit/features/chat/rendering/todoUtils.test.ts +105 -0
- package/tests/unit/features/chat/rewind.test.ts +56 -0
- package/tests/unit/features/chat/services/BangBashService.test.ts +142 -0
- package/tests/unit/features/chat/services/InstructionRefineService.test.ts +371 -0
- package/tests/unit/features/chat/services/SubagentManager.test.ts +1759 -0
- package/tests/unit/features/chat/services/TitleGenerationService.test.ts +480 -0
- package/tests/unit/features/chat/state/ChatState.test.ts +581 -0
- package/tests/unit/features/chat/tabs/Tab.test.ts +4287 -0
- package/tests/unit/features/chat/tabs/TabBar.test.ts +357 -0
- package/tests/unit/features/chat/tabs/TabManager.test.ts +2962 -0
- package/tests/unit/features/chat/tabs/index.test.ts +11 -0
- package/tests/unit/features/chat/ui/BangBashModeManager.test.ts +321 -0
- package/tests/unit/features/chat/ui/ExternalContextSelector.test.ts +555 -0
- package/tests/unit/features/chat/ui/FileContextManager.test.ts +876 -0
- package/tests/unit/features/chat/ui/ImageContext.test.ts +777 -0
- package/tests/unit/features/chat/ui/InputToolbar.test.ts +1139 -0
- package/tests/unit/features/chat/ui/InstructionModeManager.test.ts +243 -0
- package/tests/unit/features/chat/ui/NavigationSidebar.test.ts +570 -0
- package/tests/unit/features/chat/ui/StatusPanel.test.ts +953 -0
- package/tests/unit/features/chat/ui/file-context/state/FileContextState.test.ts +155 -0
- package/tests/unit/features/chat/ui/textareaResize.test.ts +102 -0
- package/tests/unit/features/chat/utils/usageInfo.test.ts +56 -0
- package/tests/unit/features/inline-edit/InlineEditService.test.ts +1199 -0
- package/tests/unit/features/inline-edit/ui/InlineEditModal.openAndWait.test.ts +1482 -0
- package/tests/unit/features/inline-edit/ui/InlineEditModal.test.ts +495 -0
- package/tests/unit/features/inline-edit/ui/inlineEditMarkdownPreview.test.ts +92 -0
- package/tests/unit/features/settings/AgentSettings.test.ts +82 -0
- package/tests/unit/features/settings/keyboardNavigation.test.ts +73 -0
- package/tests/unit/features/settings/ui/CodexSkillSettings.test.ts +294 -0
- package/tests/unit/features/settings/ui/CodexSubagentSettings.test.ts +207 -0
- package/tests/unit/i18n/constants.test.ts +43 -0
- package/tests/unit/i18n/i18n.test.ts +244 -0
- package/tests/unit/i18n/locales.test.ts +134 -0
- package/tests/unit/providers/acp/AcpClientConnection.test.ts +248 -0
- package/tests/unit/providers/acp/AcpJsonRpcTransport.test.ts +186 -0
- package/tests/unit/providers/acp/AcpSessionConfig.test.ts +247 -0
- package/tests/unit/providers/acp/AcpSessionUpdateNormalizer.test.ts +145 -0
- package/tests/unit/providers/acp/AcpSubprocess.test.ts +105 -0
- package/tests/unit/providers/acp/buildAcpUsageInfo.test.ts +51 -0
- package/tests/unit/providers/claude/agents/AgentManager.test.ts +590 -0
- package/tests/unit/providers/claude/agents/AgentStorage.test.ts +434 -0
- package/tests/unit/providers/claude/agents/index.test.ts +10 -0
- package/tests/unit/providers/claude/commands/ClaudeCommandCatalog.test.ts +396 -0
- package/tests/unit/providers/claude/commands/probeRuntimeCommands.test.ts +92 -0
- package/tests/unit/providers/claude/env/ClaudeSettingsReconciler.test.ts +57 -0
- package/tests/unit/providers/claude/env/claudeModelEnv.test.ts +228 -0
- package/tests/unit/providers/claude/hooks/SubagentHooks.test.ts +83 -0
- package/tests/unit/providers/claude/plugins/PluginManager.test.ts +832 -0
- package/tests/unit/providers/claude/plugins/index.test.ts +7 -0
- package/tests/unit/providers/claude/prompt/ClaudeTurnEncoder.test.ts +145 -0
- package/tests/unit/providers/claude/prompt/instructionRefine.test.ts +185 -0
- package/tests/unit/providers/claude/prompt/systemPrompt.test.ts +163 -0
- package/tests/unit/providers/claude/prompt/titleGeneration.test.ts +20 -0
- package/tests/unit/providers/claude/runtime/ClaudeTaskResultInterpreter.test.ts +28 -0
- package/tests/unit/providers/claude/runtime/ClaudianService.test.ts +3796 -0
- package/tests/unit/providers/claude/runtime/MessageChannel.test.ts +421 -0
- package/tests/unit/providers/claude/runtime/QueryOptionsBuilder.test.ts +775 -0
- package/tests/unit/providers/claude/runtime/SessionManager.test.ts +182 -0
- package/tests/unit/providers/claude/runtime/claudeColdStartQuery.test.ts +331 -0
- package/tests/unit/providers/claude/runtime/customSpawn.test.ts +374 -0
- package/tests/unit/providers/claude/runtime/index.test.ts +13 -0
- package/tests/unit/providers/claude/runtime/types.test.ts +190 -0
- package/tests/unit/providers/claude/sdk/typeGuards.test.ts +50 -0
- package/tests/unit/providers/claude/security/ClaudePermissionUpdates.test.ts +198 -0
- package/tests/unit/providers/claude/storage/AgentVaultStorage.test.ts +413 -0
- package/tests/unit/providers/claude/storage/CCSettingsStorage.test.ts +408 -0
- package/tests/unit/providers/claude/storage/ClaudianSettingsStorage.test.ts +653 -0
- package/tests/unit/providers/claude/storage/McpStorage.test.ts +619 -0
- package/tests/unit/providers/claude/storage/SessionStorage.test.ts +680 -0
- package/tests/unit/providers/claude/storage/SkillStorage.test.ts +275 -0
- package/tests/unit/providers/claude/storage/SlashCommandStorage.test.ts +612 -0
- package/tests/unit/providers/claude/storage/storage.test.ts +360 -0
- package/tests/unit/providers/claude/storage/storageService.convenience.test.ts +447 -0
- package/tests/unit/providers/claude/stream/transformSDKMessage.test.ts +1729 -0
- package/tests/unit/providers/claude/types/types.test.ts +726 -0
- package/tests/unit/providers/claude/ui/ClaudeChatUIConfig.test.ts +173 -0
- package/tests/unit/providers/claude/ui/ClaudeSettingsTab.test.ts +466 -0
- package/tests/unit/providers/codex/agents/CodexAgentMentionProvider.test.ts +89 -0
- package/tests/unit/providers/codex/auxiliary/CodexInstructionRefineService.test.ts +81 -0
- package/tests/unit/providers/codex/capabilities.test.ts +39 -0
- package/tests/unit/providers/codex/commands/CodexSkillCatalog.test.ts +413 -0
- package/tests/unit/providers/codex/env/CodexSettingsReconciler.test.ts +106 -0
- package/tests/unit/providers/codex/fixtures/codex-session-abort.jsonl +9 -0
- package/tests/unit/providers/codex/fixtures/codex-session-agent-lifecycle.jsonl +12 -0
- package/tests/unit/providers/codex/fixtures/codex-session-persisted-tools.jsonl +15 -0
- package/tests/unit/providers/codex/fixtures/codex-session-simple.jsonl +10 -0
- package/tests/unit/providers/codex/fixtures/codex-session-tools.jsonl +12 -0
- package/tests/unit/providers/codex/fixtures/codex-session-websearch-persisted.jsonl +3 -0
- package/tests/unit/providers/codex/fixtures/codex-session-websearch.jsonl +7 -0
- package/tests/unit/providers/codex/history/CodexConversationHistoryService.test.ts +690 -0
- package/tests/unit/providers/codex/history/CodexHistoryStore.test.ts +2202 -0
- package/tests/unit/providers/codex/normalization/codexSubagentNormalization.test.ts +81 -0
- package/tests/unit/providers/codex/normalization/codexToolNormalization.test.ts +322 -0
- package/tests/unit/providers/codex/prompt/encodeCodexTurn.test.ts +168 -0
- package/tests/unit/providers/codex/runtime/CodexAppServerProcess.test.ts +255 -0
- package/tests/unit/providers/codex/runtime/CodexAuxQueryRunner.test.ts +130 -0
- package/tests/unit/providers/codex/runtime/CodexBinaryLocator.test.ts +90 -0
- package/tests/unit/providers/codex/runtime/CodexChatRuntime.test.ts +2445 -0
- package/tests/unit/providers/codex/runtime/CodexCliResolver.test.ts +103 -0
- package/tests/unit/providers/codex/runtime/CodexExecutionTargetResolver.test.ts +105 -0
- package/tests/unit/providers/codex/runtime/CodexLaunchSpecBuilder.test.ts +150 -0
- package/tests/unit/providers/codex/runtime/CodexNotificationRouter.test.ts +1248 -0
- package/tests/unit/providers/codex/runtime/CodexPathMapper.test.ts +51 -0
- package/tests/unit/providers/codex/runtime/CodexRpcTransport.test.ts +220 -0
- package/tests/unit/providers/codex/runtime/CodexRuntimeContext.test.ts +107 -0
- package/tests/unit/providers/codex/runtime/CodexServerRequestRouter.test.ts +537 -0
- package/tests/unit/providers/codex/runtime/CodexSessionFileTail.test.ts +1305 -0
- package/tests/unit/providers/codex/runtime/CodexSessionManager.test.ts +82 -0
- package/tests/unit/providers/codex/runtime/codexAppServerTypes.test.ts +336 -0
- package/tests/unit/providers/codex/settings.test.ts +189 -0
- package/tests/unit/providers/codex/skills/CodexSkillListingService.test.ts +178 -0
- package/tests/unit/providers/codex/storage/CodexSkillStorage.test.ts +342 -0
- package/tests/unit/providers/codex/storage/CodexSubagentStorage.test.ts +376 -0
- package/tests/unit/providers/codex/ui/CodexChatUIConfig.test.ts +211 -0
- package/tests/unit/providers/codex/ui/CodexSettingsTab.test.ts +578 -0
- package/tests/unit/providers/defaultProviderConfigs.test.ts +18 -0
- package/tests/unit/providers/opencode/OpencodeAuxQueryRunner.test.ts +355 -0
- package/tests/unit/providers/opencode/OpencodeChatRuntime.test.ts +808 -0
- package/tests/unit/providers/opencode/OpencodeCliResolver.test.ts +93 -0
- package/tests/unit/providers/opencode/OpencodeCommandCatalog.test.ts +75 -0
- package/tests/unit/providers/opencode/OpencodeConversationHistoryService.test.ts +103 -0
- package/tests/unit/providers/opencode/OpencodeHistoryStore.test.ts +418 -0
- package/tests/unit/providers/opencode/OpencodeLaunchArtifacts.test.ts +268 -0
- package/tests/unit/providers/opencode/OpencodePaths.test.ts +35 -0
- package/tests/unit/providers/opencode/OpencodeRuntimeCommandLoader.test.ts +129 -0
- package/tests/unit/providers/opencode/OpencodeSettingsReconciler.test.ts +96 -0
- package/tests/unit/providers/opencode/OpencodeSettingsTab.test.ts +649 -0
- package/tests/unit/providers/opencode/OpencodeSqliteReader.test.ts +185 -0
- package/tests/unit/providers/opencode/agents/OpencodeAgentMentionProvider.test.ts +56 -0
- package/tests/unit/providers/opencode/buildOpencodePrompt.test.ts +87 -0
- package/tests/unit/providers/opencode/capabilities.test.ts +39 -0
- package/tests/unit/providers/opencode/models.test.ts +273 -0
- package/tests/unit/providers/opencode/modes.test.ts +151 -0
- package/tests/unit/providers/opencode/opencodeToolNormalization.test.ts +197 -0
- package/tests/unit/providers/opencode/settings.test.ts +469 -0
- package/tests/unit/providers/opencode/storage/OpencodeAgentStorage.test.ts +377 -0
- package/tests/unit/providers/opencode/ui/OpencodeAgentSettings.test.ts +91 -0
- package/tests/unit/providers/pi/PiRuntimeCommandLoader.test.ts +149 -0
- package/tests/unit/providers/pi/capabilities.test.ts +20 -0
- package/tests/unit/providers/pi/commands/PiCommandCatalog.test.ts +69 -0
- package/tests/unit/providers/pi/env/PiSettingsReconciler.test.ts +61 -0
- package/tests/unit/providers/pi/history/PiConversationHistoryService.test.ts +147 -0
- package/tests/unit/providers/pi/history/PiHistoryStore.test.ts +523 -0
- package/tests/unit/providers/pi/models.test.ts +96 -0
- package/tests/unit/providers/pi/registration.test.ts +54 -0
- package/tests/unit/providers/pi/runtime/PiAuxQueryRunner.test.ts +172 -0
- package/tests/unit/providers/pi/runtime/PiChatRuntime.test.ts +830 -0
- package/tests/unit/providers/pi/runtime/PiCliResolver.test.ts +105 -0
- package/tests/unit/providers/pi/runtime/PiEventNormalization.test.ts +147 -0
- package/tests/unit/providers/pi/runtime/PiExtensionUiBridge.test.ts +98 -0
- package/tests/unit/providers/pi/runtime/PiJsonl.test.ts +42 -0
- package/tests/unit/providers/pi/runtime/PiLaunchSpec.test.ts +92 -0
- package/tests/unit/providers/pi/runtime/PiModelDiscoveryService.test.ts +135 -0
- package/tests/unit/providers/pi/runtime/PiRpcPayloads.test.ts +15 -0
- package/tests/unit/providers/pi/runtime/PiRpcTransport.test.ts +116 -0
- package/tests/unit/providers/pi/runtime/PiSubprocess.test.ts +151 -0
- package/tests/unit/providers/pi/runtime/buildPiPrompt.test.ts +84 -0
- package/tests/unit/providers/pi/runtime/buildPiUsageInfo.test.ts +69 -0
- package/tests/unit/providers/pi/settings.test.ts +253 -0
- package/tests/unit/providers/pi/ui/PiChatUIConfig.test.ts +161 -0
- package/tests/unit/providers/pi/ui/PiSettingsTab.test.ts +492 -0
- package/tests/unit/scripts/rendererSafeUnref.test.ts +101 -0
- package/tests/unit/shared/components/ResumeSessionDropdown.test.ts +356 -0
- package/tests/unit/shared/components/SelectableDropdown.test.ts +406 -0
- package/tests/unit/shared/components/SlashCommandDropdown.provider.test.ts +354 -0
- package/tests/unit/shared/components/SlashCommandDropdown.test.ts +508 -0
- package/tests/unit/shared/icons.test.ts +56 -0
- package/tests/unit/shared/index.test.ts +46 -0
- package/tests/unit/shared/mention/MentionDropdownController.test.ts +823 -0
- package/tests/unit/shared/mention/VaultFileCache.test.ts +195 -0
- package/tests/unit/shared/mention/VaultFolderCache.test.ts +143 -0
- package/tests/unit/shared/mention/VaultMentionDataProvider.test.ts +87 -0
- package/tests/unit/shared/modals/ConfirmModal.test.ts +111 -0
- package/tests/unit/shared/modals/ForkTargetModal.test.ts +101 -0
- package/tests/unit/shared/modals/InstructionConfirmModal.test.ts +305 -0
- package/tests/unit/utils/agent.test.ts +395 -0
- package/tests/unit/utils/animationFrame.test.ts +59 -0
- package/tests/unit/utils/browser.test.ts +73 -0
- package/tests/unit/utils/canvas.test.ts +54 -0
- package/tests/unit/utils/claudeCli.test.ts +294 -0
- package/tests/unit/utils/cliBinaryLocator.test.ts +33 -0
- package/tests/unit/utils/context.test.ts +288 -0
- package/tests/unit/utils/contextMentionResolver.test.ts +270 -0
- package/tests/unit/utils/date.test.ts +80 -0
- package/tests/unit/utils/diff.test.ts +291 -0
- package/tests/unit/utils/editor.test.ts +249 -0
- package/tests/unit/utils/electronCompat.test.ts +87 -0
- package/tests/unit/utils/env.test.ts +1240 -0
- package/tests/unit/utils/externalContext.test.ts +336 -0
- package/tests/unit/utils/externalContextScanner.test.ts +186 -0
- package/tests/unit/utils/fileLink.dom.test.ts +273 -0
- package/tests/unit/utils/fileLink.handler.test.ts +64 -0
- package/tests/unit/utils/fileLink.test.ts +233 -0
- package/tests/unit/utils/frontmatter.test.ts +434 -0
- package/tests/unit/utils/imageEmbed.test.ts +407 -0
- package/tests/unit/utils/inlineEdit.test.ts +63 -0
- package/tests/unit/utils/interrupt.test.ts +73 -0
- package/tests/unit/utils/markdown.test.ts +35 -0
- package/tests/unit/utils/markdownMath.test.ts +54 -0
- package/tests/unit/utils/mcp.test.ts +256 -0
- package/tests/unit/utils/obsidianCompat.test.ts +18 -0
- package/tests/unit/utils/path.test.ts +677 -0
- package/tests/unit/utils/sdkSession.test.ts +2359 -0
- package/tests/unit/utils/session.test.ts +971 -0
- package/tests/unit/utils/slashCommand.test.ts +778 -0
- package/tests/unit/utils/utils.test.ts +809 -0
- package/tsconfig.jest.json +8 -0
- package/tsconfig.json +26 -0
- package/versions.json +4 -0
|
@@ -0,0 +1,1812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claudian - Claude Agent SDK wrapper
|
|
3
|
+
*
|
|
4
|
+
* Handles communication with Claude via the Agent SDK. Manages streaming,
|
|
5
|
+
* session persistence, permission modes, and security hooks.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Persistent query for active chat conversation (eliminates cold-start latency)
|
|
9
|
+
* - Cold-start queries for inline edit, title generation
|
|
10
|
+
* - MessageChannel for message queueing and turn management
|
|
11
|
+
* - Dynamic updates (model, effort level, permission mode, MCP servers)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
CanUseTool,
|
|
16
|
+
Options,
|
|
17
|
+
PermissionMode as SDKPermissionMode,
|
|
18
|
+
Query,
|
|
19
|
+
RewindFilesResult,
|
|
20
|
+
SDKMessage,
|
|
21
|
+
SDKUserMessage,
|
|
22
|
+
SlashCommand as SDKSlashCommand,
|
|
23
|
+
} from '@anthropic-ai/claude-agent-sdk';
|
|
24
|
+
import { query as agentQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
+
import { Notice } from 'obsidian';
|
|
26
|
+
|
|
27
|
+
import type { McpServerManager } from '../../../core/mcp/McpServerManager';
|
|
28
|
+
import { ProviderSettingsCoordinator } from '../../../core/providers/ProviderSettingsCoordinator';
|
|
29
|
+
import type {
|
|
30
|
+
AppAgentManager,
|
|
31
|
+
AppPluginManager,
|
|
32
|
+
} from '../../../core/providers/types';
|
|
33
|
+
import type { ChatRuntime } from '../../../core/runtime/ChatRuntime';
|
|
34
|
+
import type {
|
|
35
|
+
ApprovalCallback,
|
|
36
|
+
AskUserQuestionCallback,
|
|
37
|
+
AutoTurnCallback,
|
|
38
|
+
ChatRewindMode,
|
|
39
|
+
ChatRewindResult,
|
|
40
|
+
ChatRuntimeConversationState,
|
|
41
|
+
ChatRuntimeQueryOptions,
|
|
42
|
+
ChatTurnMetadata,
|
|
43
|
+
ChatTurnRequest,
|
|
44
|
+
PreparedChatTurn,
|
|
45
|
+
SessionUpdateResult,
|
|
46
|
+
} from '../../../core/runtime/types';
|
|
47
|
+
import { TOOL_ENTER_PLAN_MODE, TOOL_SKILL } from '../../../core/tools/toolNames';
|
|
48
|
+
import type {
|
|
49
|
+
ApprovalDecision,
|
|
50
|
+
ChatMessage,
|
|
51
|
+
Conversation,
|
|
52
|
+
ExitPlanModeCallback,
|
|
53
|
+
ImageAttachment,
|
|
54
|
+
SlashCommand,
|
|
55
|
+
StreamChunk,
|
|
56
|
+
ToolCallInfo,
|
|
57
|
+
} from '../../../core/types';
|
|
58
|
+
import type { ClaudianSettings, PermissionMode } from '../../../core/types/settings';
|
|
59
|
+
import type ClaudianPlugin from '../../../main';
|
|
60
|
+
import { stripCurrentNoteContext } from '../../../utils/context';
|
|
61
|
+
import { getEnhancedPath, getMissingNodeError, parseEnvironmentVariables } from '../../../utils/env';
|
|
62
|
+
import { getVaultPath } from '../../../utils/path';
|
|
63
|
+
import {
|
|
64
|
+
buildContextFromHistory,
|
|
65
|
+
buildPromptWithHistoryContext,
|
|
66
|
+
getLastUserMessage,
|
|
67
|
+
isSessionExpiredError,
|
|
68
|
+
} from '../../../utils/session';
|
|
69
|
+
import { CLAUDE_PROVIDER_CAPABILITIES } from '../capabilities';
|
|
70
|
+
import { loadSubagentFinalResult, loadSubagentToolCalls } from '../history/ClaudeHistoryStore';
|
|
71
|
+
import { createStopSubagentHook, type SubagentHookState } from '../hooks/SubagentHooks';
|
|
72
|
+
import { toClaudeRuntimeModelId } from '../modelSelection';
|
|
73
|
+
import { encodeClaudeTurn } from '../prompt/ClaudeTurnEncoder';
|
|
74
|
+
import { isContextWindowEvent, isSessionInitEvent, isStreamChunk } from '../sdk/typeGuards';
|
|
75
|
+
import type { TransformEvent } from '../sdk/types';
|
|
76
|
+
import { getClaudeProviderSettings } from '../settings';
|
|
77
|
+
import {
|
|
78
|
+
createTransformStreamState,
|
|
79
|
+
createTransformUsageState,
|
|
80
|
+
transformSDKMessage,
|
|
81
|
+
} from '../stream/transformClaudeMessage';
|
|
82
|
+
import { type ClaudeProviderState, getClaudeState } from '../types/providerState';
|
|
83
|
+
import { createClaudeApprovalCallback } from './ClaudeApprovalHandler';
|
|
84
|
+
import { applyClaudeDynamicUpdates } from './ClaudeDynamicUpdates';
|
|
85
|
+
import { MessageChannel } from './ClaudeMessageChannel';
|
|
86
|
+
import {
|
|
87
|
+
type ColdStartQueryContext,
|
|
88
|
+
type PersistentQueryContext,
|
|
89
|
+
QueryOptionsBuilder,
|
|
90
|
+
type QueryOptionsContext,
|
|
91
|
+
} from './ClaudeQueryOptionsBuilder';
|
|
92
|
+
import { executeClaudeRewind } from './ClaudeRewindService';
|
|
93
|
+
import { SessionManager } from './ClaudeSessionManager';
|
|
94
|
+
import {
|
|
95
|
+
buildClaudePromptWithImages,
|
|
96
|
+
buildClaudeSDKUserMessage,
|
|
97
|
+
} from './ClaudeUserMessageFactory';
|
|
98
|
+
import {
|
|
99
|
+
type ClaudeEnsureReadyOptions,
|
|
100
|
+
type ClosePersistentQueryOptions,
|
|
101
|
+
createResponseHandler,
|
|
102
|
+
isTurnCompleteMessage,
|
|
103
|
+
type PersistentQueryConfig,
|
|
104
|
+
type ResponseHandler,
|
|
105
|
+
} from './types';
|
|
106
|
+
|
|
107
|
+
export type { ApprovalDecision };
|
|
108
|
+
export type {
|
|
109
|
+
ApprovalCallback,
|
|
110
|
+
ApprovalCallbackOptions,
|
|
111
|
+
AskUserQuestionCallback,
|
|
112
|
+
} from '../../../core/runtime/types';
|
|
113
|
+
|
|
114
|
+
export interface ClaudeRuntimeServices {
|
|
115
|
+
mcpManager: McpServerManager;
|
|
116
|
+
pluginManager: AppPluginManager;
|
|
117
|
+
agentManager: Pick<AppAgentManager, 'setBuiltinAgentNames'>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type QueryOptions = ChatRuntimeQueryOptions;
|
|
121
|
+
|
|
122
|
+
function isChatMessageArray(value: unknown): value is ChatMessage[] {
|
|
123
|
+
return Array.isArray(value) && value.length > 0 &&
|
|
124
|
+
!!value[0] && typeof value[0] === 'object' && 'role' in value[0] && 'content' in value[0];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isImageAttachmentArray(value: unknown): value is ImageAttachment[] {
|
|
128
|
+
return Array.isArray(value) && value.length > 0 &&
|
|
129
|
+
!!value[0] && typeof value[0] === 'object' && 'mediaType' in value[0] && 'data' in value[0];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class ClaudianService implements ChatRuntime {
|
|
133
|
+
readonly providerId = CLAUDE_PROVIDER_CAPABILITIES.providerId;
|
|
134
|
+
private plugin: ClaudianPlugin;
|
|
135
|
+
private agentManager: Pick<AppAgentManager, 'setBuiltinAgentNames'> | null;
|
|
136
|
+
private pluginManager: AppPluginManager | null;
|
|
137
|
+
private abortController: AbortController | null = null;
|
|
138
|
+
private approvalCallback: ApprovalCallback | null = null;
|
|
139
|
+
private approvalDismisser: (() => void) | null = null;
|
|
140
|
+
private askUserQuestionCallback: AskUserQuestionCallback | null = null;
|
|
141
|
+
private exitPlanModeCallback: ExitPlanModeCallback | null = null;
|
|
142
|
+
private permissionModeSyncCallback: ((sdkMode: string) => void) | null = null;
|
|
143
|
+
private vaultPath: string | null = null;
|
|
144
|
+
private currentExternalContextPaths: string[] = [];
|
|
145
|
+
private readyStateListeners = new Set<(ready: boolean) => void>();
|
|
146
|
+
|
|
147
|
+
// Modular components
|
|
148
|
+
private sessionManager = new SessionManager();
|
|
149
|
+
private mcpManager: McpServerManager;
|
|
150
|
+
|
|
151
|
+
private persistentQuery: Query | null = null;
|
|
152
|
+
private messageChannel: MessageChannel | null = null;
|
|
153
|
+
private queryAbortController: AbortController | null = null;
|
|
154
|
+
private responseHandlers: ResponseHandler[] = [];
|
|
155
|
+
private responseConsumerRunning = false;
|
|
156
|
+
private responseConsumerPromise: Promise<void> | null = null;
|
|
157
|
+
private shuttingDown = false;
|
|
158
|
+
|
|
159
|
+
// Tracked configuration for detecting changes that require restart
|
|
160
|
+
private currentConfig: PersistentQueryConfig | null = null;
|
|
161
|
+
|
|
162
|
+
// Current allowed tools for canUseTool enforcement (null = no restriction)
|
|
163
|
+
private currentAllowedTools: string[] | null = null;
|
|
164
|
+
|
|
165
|
+
private pendingResumeAt?: string;
|
|
166
|
+
private pendingForkSession = false;
|
|
167
|
+
|
|
168
|
+
// Last sent message for crash recovery (Phase 1.3)
|
|
169
|
+
private lastSentMessage: SDKUserMessage | null = null;
|
|
170
|
+
private lastSentQueryOptions: QueryOptions | null = null;
|
|
171
|
+
private crashRecoveryAttempted = false;
|
|
172
|
+
private coldStartInProgress = false; // Prevent consumer error restarts during cold-start
|
|
173
|
+
|
|
174
|
+
// SDK command cache — populated on system/init, cleared on persistent query close
|
|
175
|
+
private cachedSdkCommands: SlashCommand[] = [];
|
|
176
|
+
|
|
177
|
+
// Subagent hook state provider (set from feature layer to avoid core→feature dependency)
|
|
178
|
+
private _subagentStateProvider: (() => SubagentHookState) | null = null;
|
|
179
|
+
|
|
180
|
+
// Auto-triggered turn handling (e.g., task-notification delivery by the SDK)
|
|
181
|
+
private _autoTurnBuffer: StreamChunk[] = [];
|
|
182
|
+
private _autoTurnSawStreamText = false;
|
|
183
|
+
private _autoTurnSawStreamThinking = false;
|
|
184
|
+
private _autoTurnCallback: AutoTurnCallback | null = null;
|
|
185
|
+
private turnMetadata: ChatTurnMetadata = {};
|
|
186
|
+
private bufferedUsageChunk: StreamChunk & { type: 'usage' } | null = null;
|
|
187
|
+
private streamTransformState = createTransformStreamState();
|
|
188
|
+
private usageTransformState = createTransformUsageState();
|
|
189
|
+
|
|
190
|
+
private getLegacyPluginDeps(): ClaudianPlugin & {
|
|
191
|
+
agentManager?: Pick<AppAgentManager, 'setBuiltinAgentNames'>;
|
|
192
|
+
pluginManager?: AppPluginManager;
|
|
193
|
+
} {
|
|
194
|
+
return this.plugin;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
constructor(plugin: ClaudianPlugin, services: ClaudeRuntimeServices | McpServerManager) {
|
|
198
|
+
this.plugin = plugin;
|
|
199
|
+
const legacyPlugin = this.getLegacyPluginDeps();
|
|
200
|
+
|
|
201
|
+
if ('mcpManager' in services) {
|
|
202
|
+
this.mcpManager = services.mcpManager;
|
|
203
|
+
this.pluginManager = services.pluginManager ?? legacyPlugin.pluginManager ?? null;
|
|
204
|
+
this.agentManager = services.agentManager ?? legacyPlugin.agentManager ?? null;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.mcpManager = services;
|
|
209
|
+
this.pluginManager = legacyPlugin.pluginManager ?? null;
|
|
210
|
+
this.agentManager = legacyPlugin.agentManager ?? null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getCapabilities() {
|
|
214
|
+
return CLAUDE_PROVIDER_CAPABILITIES;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
prepareTurn(request: ChatTurnRequest): PreparedChatTurn {
|
|
218
|
+
return encodeClaudeTurn(request, this.mcpManager);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
consumeTurnMetadata(): ChatTurnMetadata {
|
|
222
|
+
const metadata = { ...this.turnMetadata };
|
|
223
|
+
this.turnMetadata = {};
|
|
224
|
+
this.bufferedUsageChunk = null;
|
|
225
|
+
return metadata;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
onReadyStateChange(listener: (ready: boolean) => void): () => void {
|
|
229
|
+
this.readyStateListeners.add(listener);
|
|
230
|
+
try {
|
|
231
|
+
listener(this.isReady());
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore listener errors
|
|
234
|
+
}
|
|
235
|
+
return () => {
|
|
236
|
+
this.readyStateListeners.delete(listener);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private notifyReadyStateChange(): void {
|
|
241
|
+
if (this.readyStateListeners.size === 0) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const isReady = this.isReady();
|
|
246
|
+
for (const listener of this.readyStateListeners) {
|
|
247
|
+
try {
|
|
248
|
+
listener(isReady);
|
|
249
|
+
} catch {
|
|
250
|
+
// Ignore listener errors
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private resetTurnMetadata(): void {
|
|
256
|
+
this.turnMetadata = {};
|
|
257
|
+
this.bufferedUsageChunk = null;
|
|
258
|
+
this.usageTransformState.clear();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private recordTurnMetadata(update: Partial<ChatTurnMetadata>): void {
|
|
262
|
+
this.turnMetadata = {
|
|
263
|
+
...this.turnMetadata,
|
|
264
|
+
...update,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private bufferUsageChunk(chunk: Extract<StreamChunk, { type: 'usage' }>): Extract<StreamChunk, { type: 'usage' }> {
|
|
269
|
+
this.bufferedUsageChunk = chunk;
|
|
270
|
+
return chunk;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private updateBufferedUsageContextWindow(contextWindow: number): Extract<StreamChunk, { type: 'usage' }> | null {
|
|
274
|
+
if (!this.bufferedUsageChunk || contextWindow <= 0) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const usage = this.bufferedUsageChunk.usage;
|
|
279
|
+
const percentage = Math.min(
|
|
280
|
+
100,
|
|
281
|
+
Math.max(0, Math.round((usage.contextTokens / contextWindow) * 100)),
|
|
282
|
+
);
|
|
283
|
+
const nextChunk: Extract<StreamChunk, { type: 'usage' }> = {
|
|
284
|
+
...this.bufferedUsageChunk,
|
|
285
|
+
usage: {
|
|
286
|
+
...usage,
|
|
287
|
+
contextWindow,
|
|
288
|
+
contextWindowIsAuthoritative: true,
|
|
289
|
+
percentage,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
this.bufferedUsageChunk = nextChunk;
|
|
293
|
+
return nextChunk;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
setPendingResumeAt(uuid: string | undefined): void {
|
|
297
|
+
this.pendingResumeAt = uuid;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
setResumeCheckpoint(checkpointId: string | undefined): void {
|
|
301
|
+
this.setPendingResumeAt(checkpointId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** One-shot: consumed on the next query, then cleared by routeMessage on session init. */
|
|
305
|
+
private applyForkState(conv: ChatRuntimeConversationState): string | null {
|
|
306
|
+
const state = getClaudeState(conv.providerState);
|
|
307
|
+
const isPending = !conv.sessionId && !state.providerSessionId && !!state.forkSource;
|
|
308
|
+
this.pendingForkSession = isPending;
|
|
309
|
+
if (isPending) {
|
|
310
|
+
this.pendingResumeAt = state.forkSource!.resumeAt;
|
|
311
|
+
} else {
|
|
312
|
+
this.pendingResumeAt = undefined;
|
|
313
|
+
}
|
|
314
|
+
return conv.sessionId ?? state.forkSource?.sessionId ?? null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
syncConversationState(
|
|
318
|
+
conversation: ChatRuntimeConversationState | null,
|
|
319
|
+
externalContextPaths?: string[],
|
|
320
|
+
): void {
|
|
321
|
+
if (!conversation) {
|
|
322
|
+
this.pendingForkSession = false;
|
|
323
|
+
this.pendingResumeAt = undefined;
|
|
324
|
+
this.setSessionId(null, externalContextPaths);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const resolvedSessionId = this.applyForkState(conversation);
|
|
329
|
+
this.setSessionId(resolvedSessionId, externalContextPaths);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
buildSessionUpdates({ conversation, sessionInvalidated }: {
|
|
333
|
+
conversation: Conversation | null;
|
|
334
|
+
sessionInvalidated: boolean;
|
|
335
|
+
}): SessionUpdateResult {
|
|
336
|
+
const sessionId = this.getSessionId();
|
|
337
|
+
const existingState = getClaudeState(conversation?.providerState);
|
|
338
|
+
|
|
339
|
+
const oldSdkSessionId = existingState.providerSessionId;
|
|
340
|
+
const sessionChanged = sessionId && oldSdkSessionId && sessionId !== oldSdkSessionId;
|
|
341
|
+
const previousProviderSessionIds = sessionChanged
|
|
342
|
+
? [...new Set([...(existingState.previousProviderSessionIds || []), oldSdkSessionId])]
|
|
343
|
+
: existingState.previousProviderSessionIds;
|
|
344
|
+
|
|
345
|
+
const isForkSourceOnly = !!existingState.forkSource &&
|
|
346
|
+
!existingState.providerSessionId &&
|
|
347
|
+
sessionId === existingState.forkSource.sessionId;
|
|
348
|
+
|
|
349
|
+
let resolvedSessionId: string | null;
|
|
350
|
+
if (sessionInvalidated) {
|
|
351
|
+
resolvedSessionId = null;
|
|
352
|
+
} else if (isForkSourceOnly) {
|
|
353
|
+
resolvedSessionId = conversation?.sessionId ?? null;
|
|
354
|
+
} else {
|
|
355
|
+
resolvedSessionId = sessionId ?? conversation?.sessionId ?? null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const newProviderState: ClaudeProviderState = {
|
|
359
|
+
...existingState,
|
|
360
|
+
providerSessionId: sessionId && !isForkSourceOnly ? sessionId : existingState.providerSessionId,
|
|
361
|
+
previousProviderSessionIds,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
if (existingState.forkSource && sessionId && sessionId !== existingState.forkSource.sessionId) {
|
|
365
|
+
delete newProviderState.forkSource;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
updates: {
|
|
370
|
+
sessionId: resolvedSessionId,
|
|
371
|
+
providerState: newProviderState as Record<string, unknown>,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
resolveSessionIdForFork(conversation: Conversation | null): string | null {
|
|
377
|
+
const sessionId = this.getSessionId();
|
|
378
|
+
if (sessionId) return sessionId;
|
|
379
|
+
if (!conversation) return null;
|
|
380
|
+
const state = getClaudeState(conversation.providerState);
|
|
381
|
+
return state.providerSessionId ?? conversation.sessionId ?? state.forkSource?.sessionId ?? null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async loadSubagentToolCalls(agentId: string): Promise<ToolCallInfo[]> {
|
|
385
|
+
const sessionId = this.getSessionId();
|
|
386
|
+
const vaultPath = getVaultPath(this.plugin.app);
|
|
387
|
+
if (!sessionId || !vaultPath) return [];
|
|
388
|
+
return loadSubagentToolCalls(vaultPath, sessionId, agentId);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async loadSubagentFinalResult(agentId: string): Promise<string | null> {
|
|
392
|
+
const sessionId = this.getSessionId();
|
|
393
|
+
const vaultPath = getVaultPath(this.plugin.app);
|
|
394
|
+
if (!sessionId || !vaultPath) return null;
|
|
395
|
+
return loadSubagentFinalResult(vaultPath, sessionId, agentId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async reloadMcpServers(): Promise<void> {
|
|
399
|
+
await this.mcpManager.loadServers();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Ensures the persistent query is running with current configuration.
|
|
404
|
+
* Unified API that replaces preWarm() and restartPersistentQuery().
|
|
405
|
+
*
|
|
406
|
+
* Behavior:
|
|
407
|
+
* - If not running → start (if paths available)
|
|
408
|
+
* - If running and force=true → close and restart
|
|
409
|
+
* - If running and config changed → close and restart
|
|
410
|
+
* - If running and config unchanged → no-op
|
|
411
|
+
*
|
|
412
|
+
* Note: When restart is needed, the query is closed BEFORE checking if we can
|
|
413
|
+
* start a new one. This ensures fallback to cold-start if CLI becomes unavailable.
|
|
414
|
+
*
|
|
415
|
+
* @returns true if the query was (re)started, false otherwise
|
|
416
|
+
*/
|
|
417
|
+
async ensureReady(options?: ClaudeEnsureReadyOptions): Promise<boolean> {
|
|
418
|
+
const vaultPath = getVaultPath(this.plugin.app);
|
|
419
|
+
|
|
420
|
+
// Track external context paths for dynamic updates (empty list clears)
|
|
421
|
+
if (options && options.externalContextPaths !== undefined) {
|
|
422
|
+
this.currentExternalContextPaths = options.externalContextPaths;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Auto-resolve session ID from sessionManager if not explicitly provided
|
|
426
|
+
const effectiveSessionId = options?.sessionId ?? this.sessionManager.getSessionId() ?? undefined;
|
|
427
|
+
const externalContextPaths = options?.externalContextPaths ?? this.currentExternalContextPaths;
|
|
428
|
+
|
|
429
|
+
// Case 1: Not running → try to start
|
|
430
|
+
if (!this.persistentQuery) {
|
|
431
|
+
if (!vaultPath) return false;
|
|
432
|
+
const cliPath = this.plugin.getResolvedProviderCliPath('claude');
|
|
433
|
+
if (!cliPath) return false;
|
|
434
|
+
await this.startPersistentQuery(vaultPath, cliPath, effectiveSessionId, externalContextPaths);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Case 2: Force restart (session switch, crash recovery)
|
|
439
|
+
// Close FIRST, then try to start new one (allows fallback if CLI unavailable)
|
|
440
|
+
if (options?.force) {
|
|
441
|
+
this.closePersistentQuery('forced restart', { preserveHandlers: options.preserveHandlers });
|
|
442
|
+
if (!vaultPath) return false;
|
|
443
|
+
const cliPath = this.plugin.getResolvedProviderCliPath('claude');
|
|
444
|
+
if (!cliPath) return false;
|
|
445
|
+
await this.startPersistentQuery(vaultPath, cliPath, effectiveSessionId, externalContextPaths);
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Case 3: Check if config changed → restart if needed
|
|
450
|
+
// We need vaultPath and cliPath to build config for comparison
|
|
451
|
+
if (!vaultPath) return false;
|
|
452
|
+
const cliPath = this.plugin.getResolvedProviderCliPath('claude');
|
|
453
|
+
if (!cliPath) return false;
|
|
454
|
+
|
|
455
|
+
const newConfig = this.buildPersistentQueryConfig(vaultPath, cliPath, externalContextPaths);
|
|
456
|
+
if (this.needsRestart(newConfig)) {
|
|
457
|
+
// Close FIRST, then try to start new one (allows fallback if CLI unavailable)
|
|
458
|
+
this.closePersistentQuery('config changed', { preserveHandlers: options?.preserveHandlers });
|
|
459
|
+
// Re-check CLI path as it might have changed during close
|
|
460
|
+
const cliPathAfterClose = this.plugin.getResolvedProviderCliPath('claude');
|
|
461
|
+
if (cliPathAfterClose) {
|
|
462
|
+
await this.startPersistentQuery(vaultPath, cliPathAfterClose, effectiveSessionId, externalContextPaths);
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
// CLI unavailable after close - query is closed, will fallback to cold-start
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Case 4: Running and config unchanged → no-op
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Starts the persistent query for the active chat conversation.
|
|
475
|
+
*/
|
|
476
|
+
private async startPersistentQuery(
|
|
477
|
+
vaultPath: string,
|
|
478
|
+
cliPath: string,
|
|
479
|
+
resumeSessionId?: string,
|
|
480
|
+
externalContextPaths?: string[]
|
|
481
|
+
): Promise<void> {
|
|
482
|
+
if (this.persistentQuery) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.shuttingDown = false;
|
|
487
|
+
this.vaultPath = vaultPath;
|
|
488
|
+
|
|
489
|
+
this.messageChannel = new MessageChannel();
|
|
490
|
+
|
|
491
|
+
if (resumeSessionId) {
|
|
492
|
+
this.messageChannel.setSessionId(resumeSessionId);
|
|
493
|
+
this.sessionManager.setSessionId(resumeSessionId, this.getScopedSettings().model);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
this.queryAbortController = new AbortController();
|
|
497
|
+
|
|
498
|
+
const config = this.buildPersistentQueryConfig(vaultPath, cliPath, externalContextPaths);
|
|
499
|
+
this.currentConfig = config;
|
|
500
|
+
|
|
501
|
+
const resumeAtMessageId = this.pendingResumeAt;
|
|
502
|
+
const options = this.buildPersistentQueryOptions(
|
|
503
|
+
vaultPath,
|
|
504
|
+
cliPath,
|
|
505
|
+
resumeSessionId,
|
|
506
|
+
resumeAtMessageId,
|
|
507
|
+
externalContextPaths
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
this.persistentQuery = agentQuery({
|
|
511
|
+
prompt: this.messageChannel,
|
|
512
|
+
options,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
if (this.pendingResumeAt === resumeAtMessageId) {
|
|
516
|
+
this.pendingResumeAt = undefined;
|
|
517
|
+
}
|
|
518
|
+
this.attachPersistentQueryStdinErrorHandler(this.persistentQuery);
|
|
519
|
+
|
|
520
|
+
this.startResponseConsumer();
|
|
521
|
+
this.notifyReadyStateChange();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private attachPersistentQueryStdinErrorHandler(query: Query): void {
|
|
525
|
+
const stdin = (query as { transport?: { processStdin?: NodeJS.WritableStream } }).transport?.processStdin;
|
|
526
|
+
if (!stdin || typeof stdin.on !== 'function' || typeof stdin.once !== 'function') {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const handler = (error: NodeJS.ErrnoException) => {
|
|
531
|
+
if (this.shuttingDown || this.isPipeError(error)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.closePersistentQuery('stdin error');
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
stdin.on('error', handler);
|
|
538
|
+
stdin.once('close', () => {
|
|
539
|
+
stdin.removeListener('error', handler);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private isPipeError(error: unknown): boolean {
|
|
544
|
+
if (!error || typeof error !== 'object') return false;
|
|
545
|
+
const e = error as { code?: string; message?: string };
|
|
546
|
+
return e.code === 'EPIPE' || (typeof e.message === 'string' && e.message.includes('EPIPE'));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Closes the persistent query and cleans up resources.
|
|
551
|
+
*/
|
|
552
|
+
closePersistentQuery(_reason?: string, options?: ClosePersistentQueryOptions): void {
|
|
553
|
+
if (!this.persistentQuery) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const preserveHandlers = options?.preserveHandlers ?? false;
|
|
558
|
+
|
|
559
|
+
this.shuttingDown = true;
|
|
560
|
+
|
|
561
|
+
// Close the message channel (ends the async iterable)
|
|
562
|
+
this.messageChannel?.close();
|
|
563
|
+
|
|
564
|
+
// Interrupt the query
|
|
565
|
+
void this.persistentQuery.interrupt().catch(() => {
|
|
566
|
+
// Silence abort/interrupt errors during shutdown
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Abort as backup
|
|
570
|
+
this.queryAbortController?.abort();
|
|
571
|
+
|
|
572
|
+
if (!preserveHandlers) {
|
|
573
|
+
// Notify all handlers before clearing so generators don't hang forever.
|
|
574
|
+
// This ensures queryViaPersistent() exits its while(!state.done) loop.
|
|
575
|
+
for (const handler of this.responseHandlers) {
|
|
576
|
+
handler.onDone();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Reset shuttingDown synchronously. The consumer loop sees shuttingDown=true
|
|
581
|
+
// on its next iteration check (line 549) and breaks. The messageChannel.close()
|
|
582
|
+
// above also terminates the for-await loop. Resetting here allows new queries
|
|
583
|
+
// to proceed immediately without waiting for consumer loop teardown.
|
|
584
|
+
this.shuttingDown = false;
|
|
585
|
+
this.notifyReadyStateChange();
|
|
586
|
+
|
|
587
|
+
// Clear state
|
|
588
|
+
this.persistentQuery = null;
|
|
589
|
+
this.messageChannel = null;
|
|
590
|
+
this.queryAbortController = null;
|
|
591
|
+
this.responseConsumerRunning = false;
|
|
592
|
+
this.responseConsumerPromise = null;
|
|
593
|
+
this.currentConfig = null;
|
|
594
|
+
this.cachedSdkCommands = [];
|
|
595
|
+
this.streamTransformState.clearAll();
|
|
596
|
+
this.usageTransformState.clear();
|
|
597
|
+
this._autoTurnBuffer = [];
|
|
598
|
+
this._autoTurnSawStreamText = false;
|
|
599
|
+
this._autoTurnSawStreamThinking = false;
|
|
600
|
+
if (!preserveHandlers) {
|
|
601
|
+
this.responseHandlers = [];
|
|
602
|
+
this.currentAllowedTools = null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// NOTE: Do NOT reset crashRecoveryAttempted here.
|
|
606
|
+
// It's reset in queryViaPersistent after a successful message send,
|
|
607
|
+
// or in resetSession/setSessionId when switching sessions.
|
|
608
|
+
// Resetting it here would cause infinite restart loops on persistent errors.
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Checks if the persistent query needs to be restarted based on configuration changes.
|
|
613
|
+
*/
|
|
614
|
+
private needsRestart(newConfig: PersistentQueryConfig): boolean {
|
|
615
|
+
return QueryOptionsBuilder.needsRestart(this.currentConfig, newConfig);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Builds configuration object for tracking changes.
|
|
620
|
+
*/
|
|
621
|
+
private buildPersistentQueryConfig(
|
|
622
|
+
vaultPath: string,
|
|
623
|
+
cliPath: string,
|
|
624
|
+
externalContextPaths?: string[]
|
|
625
|
+
): PersistentQueryConfig {
|
|
626
|
+
return QueryOptionsBuilder.buildPersistentQueryConfig(
|
|
627
|
+
this.buildQueryOptionsContext(vaultPath, cliPath),
|
|
628
|
+
externalContextPaths
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Builds the base query options context from current state.
|
|
634
|
+
*/
|
|
635
|
+
private getScopedSettings(): ClaudianSettings {
|
|
636
|
+
return ProviderSettingsCoordinator.getProviderSettingsSnapshot(
|
|
637
|
+
this.plugin.settings,
|
|
638
|
+
this.providerId,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private buildQueryOptionsContext(vaultPath: string, cliPath: string): QueryOptionsContext {
|
|
643
|
+
const customEnv = parseEnvironmentVariables(this.plugin.getActiveEnvironmentVariables(this.providerId));
|
|
644
|
+
const enhancedPath = getEnhancedPath(customEnv.PATH, cliPath);
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
vaultPath,
|
|
648
|
+
cliPath,
|
|
649
|
+
settings: this.getScopedSettings(),
|
|
650
|
+
customEnv,
|
|
651
|
+
enhancedPath,
|
|
652
|
+
mcpManager: this.mcpManager,
|
|
653
|
+
pluginManager: this.requirePluginManager(),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private requirePluginManager(): AppPluginManager {
|
|
658
|
+
const pluginManager = this.pluginManager ?? this.getLegacyPluginDeps().pluginManager ?? null;
|
|
659
|
+
if (!pluginManager) {
|
|
660
|
+
throw new Error('Claude plugin manager is unavailable.');
|
|
661
|
+
}
|
|
662
|
+
return pluginManager;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private getAgentManager(): Pick<AppAgentManager, 'setBuiltinAgentNames'> | null {
|
|
666
|
+
return this.agentManager ?? this.getLegacyPluginDeps().agentManager ?? null;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Builds SDK options for the persistent query.
|
|
671
|
+
*/
|
|
672
|
+
private buildPersistentQueryOptions(
|
|
673
|
+
vaultPath: string,
|
|
674
|
+
cliPath: string,
|
|
675
|
+
resumeSessionId?: string,
|
|
676
|
+
resumeAtMessageId?: string,
|
|
677
|
+
externalContextPaths?: string[]
|
|
678
|
+
): Options {
|
|
679
|
+
const baseContext = this.buildQueryOptionsContext(vaultPath, cliPath);
|
|
680
|
+
const hooks = this.buildHooks();
|
|
681
|
+
|
|
682
|
+
const ctx: PersistentQueryContext = {
|
|
683
|
+
...baseContext,
|
|
684
|
+
abortController: this.queryAbortController ?? undefined,
|
|
685
|
+
resume: resumeSessionId
|
|
686
|
+
? { sessionId: resumeSessionId, sessionAt: resumeAtMessageId, fork: this.pendingForkSession || undefined }
|
|
687
|
+
: undefined,
|
|
688
|
+
canUseTool: this.createApprovalCallback(),
|
|
689
|
+
hooks,
|
|
690
|
+
externalContextPaths,
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
return QueryOptionsBuilder.buildPersistentQueryOptions(ctx);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Builds the hooks for SDK options.
|
|
698
|
+
* Hooks need access to `this` for dynamic settings, so they're built here.
|
|
699
|
+
*/
|
|
700
|
+
private buildHooks() {
|
|
701
|
+
const hooks: Options['hooks'] = {};
|
|
702
|
+
|
|
703
|
+
// Always register subagent hooks — closures resolve provider at execution time
|
|
704
|
+
// so hooks work even when provider is set after the persistent query starts.
|
|
705
|
+
hooks.Stop = [createStopSubagentHook(
|
|
706
|
+
() => this._subagentStateProvider?.() ?? { hasRunning: false }
|
|
707
|
+
)];
|
|
708
|
+
|
|
709
|
+
return hooks;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Starts the background consumer loop that routes chunks to handlers.
|
|
714
|
+
*/
|
|
715
|
+
private startResponseConsumer(): void {
|
|
716
|
+
if (this.responseConsumerRunning) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
this.responseConsumerRunning = true;
|
|
721
|
+
|
|
722
|
+
// Track which query this consumer is for, to detect if we were replaced
|
|
723
|
+
const queryForThisConsumer = this.persistentQuery;
|
|
724
|
+
|
|
725
|
+
this.responseConsumerPromise = (async () => {
|
|
726
|
+
if (!this.persistentQuery) return;
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
for await (const message of this.persistentQuery) {
|
|
730
|
+
if (this.shuttingDown) break;
|
|
731
|
+
|
|
732
|
+
await this.routeMessage(message);
|
|
733
|
+
}
|
|
734
|
+
} catch (error) {
|
|
735
|
+
// Skip error handling if this consumer was replaced by a new one.
|
|
736
|
+
// This prevents race conditions where the OLD consumer's error handler
|
|
737
|
+
// interferes with the NEW handler after a restart (e.g., from applyDynamicUpdates).
|
|
738
|
+
if (this.persistentQuery !== queryForThisConsumer && this.persistentQuery !== null) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Skip restart if cold-start is in progress (it will handle session capture)
|
|
743
|
+
if (!this.shuttingDown && !this.coldStartInProgress) {
|
|
744
|
+
const handler = this.responseHandlers[this.responseHandlers.length - 1];
|
|
745
|
+
const errorInstance = error instanceof Error ? error : new Error(String(error));
|
|
746
|
+
const messageToReplay = this.lastSentMessage;
|
|
747
|
+
|
|
748
|
+
if (!this.crashRecoveryAttempted && messageToReplay && handler && !handler.sawAnyChunk) {
|
|
749
|
+
this.crashRecoveryAttempted = true;
|
|
750
|
+
try {
|
|
751
|
+
await this.ensureReady({ force: true, preserveHandlers: true });
|
|
752
|
+
if (!this.messageChannel) {
|
|
753
|
+
throw new Error('Persistent query restart did not create message channel', {
|
|
754
|
+
cause: error,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
await this.applyDynamicUpdates(this.lastSentQueryOptions ?? undefined, { preserveHandlers: true });
|
|
758
|
+
this.messageChannel.enqueue(messageToReplay);
|
|
759
|
+
return;
|
|
760
|
+
} catch (restartError) {
|
|
761
|
+
// If restart failed due to session expiration, invalidate session
|
|
762
|
+
// so next query triggers noSessionButHasHistory → history rebuild
|
|
763
|
+
if (isSessionExpiredError(restartError)) {
|
|
764
|
+
this.sessionManager.invalidateSession();
|
|
765
|
+
}
|
|
766
|
+
handler.onError(errorInstance);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Notify active handler of error
|
|
772
|
+
if (handler) {
|
|
773
|
+
handler.onError(errorInstance);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Crash recovery: restart persistent query to prepare for next user message.
|
|
777
|
+
if (!this.crashRecoveryAttempted) {
|
|
778
|
+
this.crashRecoveryAttempted = true;
|
|
779
|
+
try {
|
|
780
|
+
await this.ensureReady({ force: true });
|
|
781
|
+
} catch (restartError) {
|
|
782
|
+
// If restart failed due to session expiration, invalidate session
|
|
783
|
+
// so next query triggers noSessionButHasHistory → history rebuild
|
|
784
|
+
if (isSessionExpiredError(restartError)) {
|
|
785
|
+
this.sessionManager.invalidateSession();
|
|
786
|
+
}
|
|
787
|
+
// Restart failed - next query will start fresh.
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
} finally {
|
|
792
|
+
// Only clear the flag if this consumer wasn't replaced by a new one (e.g., after restart)
|
|
793
|
+
// If ensureReady() restarted, it starts a new consumer which sets the flag true,
|
|
794
|
+
// so we shouldn't clear it here.
|
|
795
|
+
if (this.persistentQuery === queryForThisConsumer || this.persistentQuery === null) {
|
|
796
|
+
this.responseConsumerRunning = false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
})();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** @param modelOverride - Optional model override for cold-start queries */
|
|
803
|
+
private getTransformOptions(
|
|
804
|
+
modelOverride?: string,
|
|
805
|
+
streamState = this.streamTransformState,
|
|
806
|
+
usageState = this.usageTransformState,
|
|
807
|
+
) {
|
|
808
|
+
const settings = this.getScopedSettings();
|
|
809
|
+
return {
|
|
810
|
+
intendedModel: toClaudeRuntimeModelId(modelOverride ?? settings.model),
|
|
811
|
+
customContextLimits: settings.customContextLimits,
|
|
812
|
+
streamState,
|
|
813
|
+
usageState,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Routes an SDK message to the active response handler.
|
|
819
|
+
*
|
|
820
|
+
* Design: Only one handler exists at a time because MessageChannel enforces
|
|
821
|
+
* single-turn processing. When a turn is active, new messages are queued/merged.
|
|
822
|
+
* The next message only dequeues after onTurnComplete(), which calls onDone()
|
|
823
|
+
* on the current handler. A new handler is registered only when the next query starts.
|
|
824
|
+
*/
|
|
825
|
+
private async routeMessage(message: SDKMessage): Promise<void> {
|
|
826
|
+
// Note: Session expiration errors are handled in catch blocks (queryViaSDK, handleAbort)
|
|
827
|
+
// The SDK throws errors as exceptions, not as message types
|
|
828
|
+
|
|
829
|
+
// Safe to use last handler - design guarantees single handler at a time
|
|
830
|
+
const handler = this.responseHandlers[this.responseHandlers.length - 1];
|
|
831
|
+
const autoTurnBufferStartLength = this._autoTurnBuffer.length;
|
|
832
|
+
|
|
833
|
+
// Transform SDK message to StreamChunks
|
|
834
|
+
for (const event of transformSDKMessage(message, this.getTransformOptions())) {
|
|
835
|
+
this.noteVisibleStreamContent(message, event, {
|
|
836
|
+
onText: () => {
|
|
837
|
+
if (handler) {
|
|
838
|
+
handler.markStreamTextSeen();
|
|
839
|
+
} else {
|
|
840
|
+
this._autoTurnSawStreamText = true;
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
onThinking: () => {
|
|
844
|
+
if (handler) {
|
|
845
|
+
handler.markStreamThinkingSeen();
|
|
846
|
+
} else {
|
|
847
|
+
this._autoTurnSawStreamThinking = true;
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (isSessionInitEvent(event)) {
|
|
853
|
+
// Fork: suppress needsHistoryRebuild since SDK returns a different session ID by design
|
|
854
|
+
const wasFork = this.pendingForkSession;
|
|
855
|
+
this.sessionManager.captureSession(event.sessionId);
|
|
856
|
+
if (wasFork) {
|
|
857
|
+
this.sessionManager.clearHistoryRebuild();
|
|
858
|
+
this.pendingForkSession = false;
|
|
859
|
+
}
|
|
860
|
+
this.messageChannel?.setSessionId(event.sessionId);
|
|
861
|
+
if (event.agents) {
|
|
862
|
+
try { this.getAgentManager()?.setBuiltinAgentNames(event.agents); } catch { /* non-critical */ }
|
|
863
|
+
}
|
|
864
|
+
if (event.permissionMode && this.permissionModeSyncCallback) {
|
|
865
|
+
try { this.permissionModeSyncCallback(event.permissionMode); } catch { /* non-critical */ }
|
|
866
|
+
}
|
|
867
|
+
// Cache SDK commands on init (SDK already scans the vault).
|
|
868
|
+
// Pass the current query instance so late completions from a dead query
|
|
869
|
+
// cannot overwrite the active cache after a restart or shutdown.
|
|
870
|
+
void this.fetchAndCacheCommands(this.persistentQuery);
|
|
871
|
+
} else if (isContextWindowEvent(event)) {
|
|
872
|
+
const usageChunk = this.updateBufferedUsageContextWindow(event.contextWindow);
|
|
873
|
+
if (!usageChunk) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (handler) {
|
|
877
|
+
handler.onChunk(usageChunk);
|
|
878
|
+
} else {
|
|
879
|
+
this._autoTurnBuffer.push(usageChunk);
|
|
880
|
+
}
|
|
881
|
+
} else if (isStreamChunk(event)) {
|
|
882
|
+
// Dedup: SDK delivers text via stream_events (incremental) AND the assistant message
|
|
883
|
+
// (complete). Skip the assistant message text if stream text was already seen.
|
|
884
|
+
if (message.type === 'assistant' && event.type === 'text') {
|
|
885
|
+
if (handler?.sawStreamText || (!handler && this._autoTurnSawStreamText)) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (message.type === 'assistant' && event.type === 'thinking') {
|
|
890
|
+
if (handler?.sawStreamThinking || (!handler && this._autoTurnSawStreamThinking)) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// SDK auto-approves EnterPlanMode (checkPermissions → allow),
|
|
896
|
+
// so canUseTool is never called. Detect the tool_use in the stream
|
|
897
|
+
// and fire the sync callback to update the UI.
|
|
898
|
+
if (event.type === 'tool_use' && event.name === TOOL_ENTER_PLAN_MODE) {
|
|
899
|
+
if (this.currentConfig) {
|
|
900
|
+
this.currentConfig.permissionMode = 'plan';
|
|
901
|
+
this.currentConfig.sdkPermissionMode = 'plan';
|
|
902
|
+
}
|
|
903
|
+
if (this.permissionModeSyncCallback) {
|
|
904
|
+
try { this.permissionModeSyncCallback('plan'); } catch { /* non-critical */ }
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const normalizedChunk = event.type === 'usage'
|
|
909
|
+
? this.bufferUsageChunk({ ...event, sessionId: this.sessionManager.getSessionId() })
|
|
910
|
+
: event;
|
|
911
|
+
|
|
912
|
+
if (handler) {
|
|
913
|
+
handler.onChunk(normalizedChunk);
|
|
914
|
+
} else {
|
|
915
|
+
// No handler — buffer for auto-triggered turn (e.g., task-notification delivery)
|
|
916
|
+
this._autoTurnBuffer.push(normalizedChunk);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (
|
|
922
|
+
!handler
|
|
923
|
+
&& message.type === 'system'
|
|
924
|
+
&& message.subtype === 'task_notification'
|
|
925
|
+
&& this._autoTurnBuffer.length > autoTurnBufferStartLength
|
|
926
|
+
) {
|
|
927
|
+
await this.flushAutoTurnBuffer();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (message.type === 'assistant' && message.uuid) {
|
|
931
|
+
this.recordTurnMetadata({ assistantMessageId: message.uuid });
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Check for turn completion
|
|
935
|
+
if (isTurnCompleteMessage(message)) {
|
|
936
|
+
// Signal turn complete to message channel
|
|
937
|
+
this.messageChannel?.onTurnComplete();
|
|
938
|
+
|
|
939
|
+
// Notify handler
|
|
940
|
+
if (handler) {
|
|
941
|
+
handler.resetStreamText();
|
|
942
|
+
handler.resetStreamThinking();
|
|
943
|
+
handler.onDone();
|
|
944
|
+
} else {
|
|
945
|
+
await this.flushAutoTurnBuffer();
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
private async flushAutoTurnBuffer(): Promise<void> {
|
|
951
|
+
this._autoTurnSawStreamText = false;
|
|
952
|
+
this._autoTurnSawStreamThinking = false;
|
|
953
|
+
if (this._autoTurnBuffer.length === 0) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Flush buffered chunks from auto-triggered turn (no handler was registered)
|
|
958
|
+
const chunks = [...this._autoTurnBuffer];
|
|
959
|
+
const metadata = this.consumeTurnMetadata();
|
|
960
|
+
this._autoTurnBuffer = [];
|
|
961
|
+
try {
|
|
962
|
+
await this._autoTurnCallback?.({ chunks, metadata });
|
|
963
|
+
} catch {
|
|
964
|
+
new Notice('Background task completed, but the result could not be rendered.');
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private registerResponseHandler(handler: ResponseHandler): void {
|
|
969
|
+
this.responseHandlers.push(handler);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private unregisterResponseHandler(handlerId: string): void {
|
|
973
|
+
const idx = this.responseHandlers.findIndex(h => h.id === handlerId);
|
|
974
|
+
if (idx >= 0) {
|
|
975
|
+
this.responseHandlers.splice(idx, 1);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private buildLegacyTurnRequest(
|
|
980
|
+
prompt: string,
|
|
981
|
+
images?: ImageAttachment[],
|
|
982
|
+
queryOptions?: QueryOptions,
|
|
983
|
+
): ChatTurnRequest {
|
|
984
|
+
return {
|
|
985
|
+
text: prompt,
|
|
986
|
+
images,
|
|
987
|
+
externalContextPaths: queryOptions?.externalContextPaths,
|
|
988
|
+
enabledMcpServers: queryOptions?.enabledMcpServers,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private buildQueryOptionsFromTurnRequest(
|
|
993
|
+
request: ChatTurnRequest,
|
|
994
|
+
encodedTurn: PreparedChatTurn,
|
|
995
|
+
legacyQueryOptions?: QueryOptions,
|
|
996
|
+
): QueryOptions | undefined {
|
|
997
|
+
const mcpMentions = legacyQueryOptions?.mcpMentions
|
|
998
|
+
? new Set([...legacyQueryOptions.mcpMentions, ...encodedTurn.mcpMentions])
|
|
999
|
+
: encodedTurn.mcpMentions;
|
|
1000
|
+
|
|
1001
|
+
const effectiveQueryOptions: QueryOptions = {
|
|
1002
|
+
allowedTools: legacyQueryOptions?.allowedTools,
|
|
1003
|
+
model: legacyQueryOptions?.model,
|
|
1004
|
+
mcpMentions,
|
|
1005
|
+
enabledMcpServers: request.enabledMcpServers ?? legacyQueryOptions?.enabledMcpServers,
|
|
1006
|
+
forceColdStart: legacyQueryOptions?.forceColdStart,
|
|
1007
|
+
externalContextPaths: request.externalContextPaths ?? legacyQueryOptions?.externalContextPaths,
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (
|
|
1011
|
+
effectiveQueryOptions.allowedTools === undefined &&
|
|
1012
|
+
effectiveQueryOptions.model === undefined &&
|
|
1013
|
+
effectiveQueryOptions.enabledMcpServers === undefined &&
|
|
1014
|
+
effectiveQueryOptions.forceColdStart === undefined &&
|
|
1015
|
+
effectiveQueryOptions.externalContextPaths === undefined &&
|
|
1016
|
+
(effectiveQueryOptions.mcpMentions?.size ?? 0) === 0
|
|
1017
|
+
) {
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return effectiveQueryOptions;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private normalizeTurnInvocation(
|
|
1025
|
+
turnOrPrompt: PreparedChatTurn | string,
|
|
1026
|
+
imagesOrHistory?: ImageAttachment[] | ChatMessage[],
|
|
1027
|
+
conversationHistoryOrQueryOptions?: ChatMessage[] | QueryOptions,
|
|
1028
|
+
legacyQueryOptions?: QueryOptions,
|
|
1029
|
+
): {
|
|
1030
|
+
request: ChatTurnRequest;
|
|
1031
|
+
encodedTurn: PreparedChatTurn;
|
|
1032
|
+
conversationHistory?: ChatMessage[];
|
|
1033
|
+
queryOptions?: QueryOptions;
|
|
1034
|
+
} {
|
|
1035
|
+
if (typeof turnOrPrompt !== 'string') {
|
|
1036
|
+
const turn = turnOrPrompt;
|
|
1037
|
+
const conversationHistory = isChatMessageArray(imagesOrHistory)
|
|
1038
|
+
? imagesOrHistory
|
|
1039
|
+
: undefined;
|
|
1040
|
+
const explicitQueryOptions = isChatMessageArray(conversationHistoryOrQueryOptions)
|
|
1041
|
+
? undefined
|
|
1042
|
+
: conversationHistoryOrQueryOptions;
|
|
1043
|
+
return {
|
|
1044
|
+
request: turn.request,
|
|
1045
|
+
encodedTurn: turn,
|
|
1046
|
+
conversationHistory,
|
|
1047
|
+
queryOptions: this.buildQueryOptionsFromTurnRequest(turn.request, turn, explicitQueryOptions),
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const images = isImageAttachmentArray(imagesOrHistory) ? imagesOrHistory : undefined;
|
|
1052
|
+
const conversationHistory = isChatMessageArray(conversationHistoryOrQueryOptions)
|
|
1053
|
+
? conversationHistoryOrQueryOptions
|
|
1054
|
+
: undefined;
|
|
1055
|
+
const queryOptions = isChatMessageArray(conversationHistoryOrQueryOptions)
|
|
1056
|
+
? legacyQueryOptions
|
|
1057
|
+
: conversationHistoryOrQueryOptions ?? legacyQueryOptions;
|
|
1058
|
+
const request = this.buildLegacyTurnRequest(turnOrPrompt, images, queryOptions);
|
|
1059
|
+
const encodedTurn = this.prepareTurn(request);
|
|
1060
|
+
|
|
1061
|
+
return {
|
|
1062
|
+
request,
|
|
1063
|
+
encodedTurn,
|
|
1064
|
+
conversationHistory,
|
|
1065
|
+
queryOptions: this.buildQueryOptionsFromTurnRequest(request, encodedTurn, queryOptions),
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
isPersistentQueryActive(): boolean {
|
|
1070
|
+
return this.persistentQuery !== null && !this.shuttingDown;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Sends a query to Claude and streams the response.
|
|
1075
|
+
*
|
|
1076
|
+
* Query selection:
|
|
1077
|
+
* - Persistent query: default chat conversation
|
|
1078
|
+
* - Cold-start query: only when forceColdStart is set
|
|
1079
|
+
*/
|
|
1080
|
+
query(
|
|
1081
|
+
turn: PreparedChatTurn,
|
|
1082
|
+
conversationHistory?: ChatMessage[],
|
|
1083
|
+
queryOptions?: QueryOptions,
|
|
1084
|
+
): AsyncGenerator<StreamChunk>;
|
|
1085
|
+
query(
|
|
1086
|
+
prompt: string,
|
|
1087
|
+
images?: ImageAttachment[],
|
|
1088
|
+
conversationHistory?: ChatMessage[],
|
|
1089
|
+
queryOptions?: QueryOptions,
|
|
1090
|
+
): AsyncGenerator<StreamChunk>;
|
|
1091
|
+
async *query(
|
|
1092
|
+
turnOrPrompt: PreparedChatTurn | string,
|
|
1093
|
+
imagesOrHistory?: ImageAttachment[] | ChatMessage[],
|
|
1094
|
+
conversationHistoryOrQueryOptions?: ChatMessage[] | QueryOptions,
|
|
1095
|
+
legacyQueryOptions?: QueryOptions,
|
|
1096
|
+
): AsyncGenerator<StreamChunk> {
|
|
1097
|
+
const normalized = this.normalizeTurnInvocation(
|
|
1098
|
+
turnOrPrompt,
|
|
1099
|
+
imagesOrHistory,
|
|
1100
|
+
conversationHistoryOrQueryOptions,
|
|
1101
|
+
legacyQueryOptions,
|
|
1102
|
+
);
|
|
1103
|
+
const prompt = normalized.encodedTurn.prompt;
|
|
1104
|
+
const images = normalized.request.images;
|
|
1105
|
+
const conversationHistory = normalized.conversationHistory;
|
|
1106
|
+
const queryOptions = normalized.queryOptions;
|
|
1107
|
+
|
|
1108
|
+
const vaultPath = getVaultPath(this.plugin.app);
|
|
1109
|
+
if (!vaultPath) {
|
|
1110
|
+
yield { type: 'error', content: 'Could not determine vault path' };
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const resolvedClaudePath = this.plugin.getResolvedProviderCliPath('claude');
|
|
1115
|
+
if (!resolvedClaudePath) {
|
|
1116
|
+
yield { type: 'error', content: 'Claude CLI not found. Please install Claude Code CLI.' };
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const customEnv = parseEnvironmentVariables(this.plugin.getActiveEnvironmentVariables(this.providerId));
|
|
1121
|
+
const enhancedPath = getEnhancedPath(customEnv.PATH, resolvedClaudePath);
|
|
1122
|
+
const missingNodeError = getMissingNodeError(resolvedClaudePath, enhancedPath);
|
|
1123
|
+
if (missingNodeError) {
|
|
1124
|
+
yield { type: 'error', content: missingNodeError };
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Rebuild history if needed before choosing persistent vs cold-start
|
|
1129
|
+
let promptToSend = prompt;
|
|
1130
|
+
let forceColdStart = false;
|
|
1131
|
+
|
|
1132
|
+
// Clear interrupted flag - persistent query handles interruption gracefully,
|
|
1133
|
+
// no need to force cold-start just because user cancelled previous response
|
|
1134
|
+
if (this.sessionManager.wasInterrupted()) {
|
|
1135
|
+
this.sessionManager.clearInterrupted();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Session mismatch recovery: SDK returned a different session ID (context lost)
|
|
1139
|
+
// Inject history to restore context without forcing cold-start
|
|
1140
|
+
if (this.sessionManager.needsHistoryRebuild() && conversationHistory && conversationHistory.length > 0) {
|
|
1141
|
+
const historyContext = buildContextFromHistory(conversationHistory);
|
|
1142
|
+
const actualPrompt = stripCurrentNoteContext(prompt);
|
|
1143
|
+
promptToSend = buildPromptWithHistoryContext(historyContext, prompt, actualPrompt, conversationHistory);
|
|
1144
|
+
this.sessionManager.clearHistoryRebuild();
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const noSessionButHasHistory = !this.sessionManager.getSessionId() &&
|
|
1148
|
+
conversationHistory && conversationHistory.length > 0;
|
|
1149
|
+
|
|
1150
|
+
if (noSessionButHasHistory) {
|
|
1151
|
+
const historyContext = buildContextFromHistory(conversationHistory);
|
|
1152
|
+
const actualPrompt = stripCurrentNoteContext(prompt);
|
|
1153
|
+
promptToSend = buildPromptWithHistoryContext(historyContext, prompt, actualPrompt, conversationHistory);
|
|
1154
|
+
|
|
1155
|
+
// Note: Do NOT call invalidateSession() here. The cold-start will capture
|
|
1156
|
+
// a new session ID anyway, and invalidating would break any persistent query
|
|
1157
|
+
// restart that happens during the cold-start (causing SESSION MISMATCH).
|
|
1158
|
+
forceColdStart = true;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const effectiveQueryOptions = forceColdStart
|
|
1162
|
+
? { ...queryOptions, forceColdStart: true }
|
|
1163
|
+
: queryOptions;
|
|
1164
|
+
|
|
1165
|
+
if (forceColdStart) {
|
|
1166
|
+
// Set flag BEFORE closing to prevent consumer error from triggering restart
|
|
1167
|
+
this.coldStartInProgress = true;
|
|
1168
|
+
this.closePersistentQuery('session invalidated');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Determine query path: persistent vs cold-start
|
|
1172
|
+
const shouldUsePersistent = !effectiveQueryOptions?.forceColdStart;
|
|
1173
|
+
|
|
1174
|
+
if (shouldUsePersistent) {
|
|
1175
|
+
// Start persistent query if not running
|
|
1176
|
+
if (!this.persistentQuery && !this.shuttingDown) {
|
|
1177
|
+
await this.startPersistentQuery(
|
|
1178
|
+
vaultPath,
|
|
1179
|
+
resolvedClaudePath,
|
|
1180
|
+
this.sessionManager.getSessionId() ?? undefined
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (this.persistentQuery && !this.shuttingDown) {
|
|
1185
|
+
// Use persistent query path
|
|
1186
|
+
try {
|
|
1187
|
+
yield* this.queryViaPersistent(promptToSend, images, vaultPath, resolvedClaudePath, effectiveQueryOptions);
|
|
1188
|
+
return;
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
if (isSessionExpiredError(error) && conversationHistory && conversationHistory.length > 0) {
|
|
1191
|
+
this.sessionManager.invalidateSession();
|
|
1192
|
+
const retryRequest = this.buildHistoryRebuildRequest(prompt, conversationHistory);
|
|
1193
|
+
|
|
1194
|
+
this.coldStartInProgress = true;
|
|
1195
|
+
this.abortController = new AbortController();
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
yield* this.queryViaSDK(
|
|
1199
|
+
retryRequest.prompt,
|
|
1200
|
+
vaultPath,
|
|
1201
|
+
resolvedClaudePath,
|
|
1202
|
+
// Use current message's images, fallback to history images
|
|
1203
|
+
images ?? retryRequest.images,
|
|
1204
|
+
effectiveQueryOptions
|
|
1205
|
+
);
|
|
1206
|
+
} catch (retryError) {
|
|
1207
|
+
const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
|
|
1208
|
+
yield { type: 'error', content: msg };
|
|
1209
|
+
} finally {
|
|
1210
|
+
this.coldStartInProgress = false;
|
|
1211
|
+
this.abortController = null;
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
throw error;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Cold-start path (existing logic)
|
|
1222
|
+
// Set flag to prevent consumer error restarts from interfering
|
|
1223
|
+
this.coldStartInProgress = true;
|
|
1224
|
+
this.abortController = new AbortController();
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
yield* this.queryViaSDK(promptToSend, vaultPath, resolvedClaudePath, images, effectiveQueryOptions);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
if (isSessionExpiredError(error) && conversationHistory && conversationHistory.length > 0) {
|
|
1230
|
+
this.sessionManager.invalidateSession();
|
|
1231
|
+
const retryRequest = this.buildHistoryRebuildRequest(prompt, conversationHistory);
|
|
1232
|
+
|
|
1233
|
+
try {
|
|
1234
|
+
yield* this.queryViaSDK(
|
|
1235
|
+
retryRequest.prompt,
|
|
1236
|
+
vaultPath,
|
|
1237
|
+
resolvedClaudePath,
|
|
1238
|
+
// Use current message's images, fallback to history images
|
|
1239
|
+
images ?? retryRequest.images,
|
|
1240
|
+
effectiveQueryOptions
|
|
1241
|
+
);
|
|
1242
|
+
} catch (retryError) {
|
|
1243
|
+
const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
|
|
1244
|
+
yield { type: 'error', content: msg };
|
|
1245
|
+
}
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
1250
|
+
yield { type: 'error', content: msg };
|
|
1251
|
+
} finally {
|
|
1252
|
+
this.coldStartInProgress = false;
|
|
1253
|
+
this.abortController = null;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
private buildHistoryRebuildRequest(
|
|
1258
|
+
prompt: string,
|
|
1259
|
+
conversationHistory: ChatMessage[]
|
|
1260
|
+
): { prompt: string; images?: ImageAttachment[] } {
|
|
1261
|
+
const historyContext = buildContextFromHistory(conversationHistory);
|
|
1262
|
+
const actualPrompt = stripCurrentNoteContext(prompt);
|
|
1263
|
+
const fullPrompt = buildPromptWithHistoryContext(historyContext, prompt, actualPrompt, conversationHistory);
|
|
1264
|
+
const lastUserMessage = getLastUserMessage(conversationHistory);
|
|
1265
|
+
|
|
1266
|
+
return {
|
|
1267
|
+
prompt: fullPrompt,
|
|
1268
|
+
images: lastUserMessage?.images,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Query via persistent query (Phase 1.5).
|
|
1274
|
+
* Uses the message channel to send messages without cold-start latency.
|
|
1275
|
+
*/
|
|
1276
|
+
private async *queryViaPersistent(
|
|
1277
|
+
prompt: string,
|
|
1278
|
+
images: ImageAttachment[] | undefined,
|
|
1279
|
+
vaultPath: string,
|
|
1280
|
+
cliPath: string,
|
|
1281
|
+
queryOptions?: QueryOptions
|
|
1282
|
+
): AsyncGenerator<StreamChunk> {
|
|
1283
|
+
this.resetTurnMetadata();
|
|
1284
|
+
|
|
1285
|
+
if (!this.persistentQuery || !this.messageChannel) {
|
|
1286
|
+
// Fallback to cold-start if persistent query not available
|
|
1287
|
+
yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Set allowed tools for canUseTool enforcement
|
|
1292
|
+
// undefined = no restriction, [] = no tools, [...] = restricted
|
|
1293
|
+
if (queryOptions?.allowedTools !== undefined) {
|
|
1294
|
+
this.currentAllowedTools = queryOptions.allowedTools.length > 0
|
|
1295
|
+
? [...queryOptions.allowedTools, TOOL_SKILL]
|
|
1296
|
+
: [];
|
|
1297
|
+
} else {
|
|
1298
|
+
this.currentAllowedTools = null;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Save allowedTools before applyDynamicUpdates - restart would clear it
|
|
1302
|
+
const savedAllowedTools = this.currentAllowedTools;
|
|
1303
|
+
|
|
1304
|
+
// Apply dynamic updates before sending (Phase 1.6)
|
|
1305
|
+
await this.applyDynamicUpdates(queryOptions);
|
|
1306
|
+
|
|
1307
|
+
// Restore allowedTools in case restart cleared it
|
|
1308
|
+
this.currentAllowedTools = savedAllowedTools;
|
|
1309
|
+
|
|
1310
|
+
// Check if applyDynamicUpdates triggered a restart that failed
|
|
1311
|
+
// (e.g., CLI path not found, vault path missing)
|
|
1312
|
+
if (!this.persistentQuery || !this.messageChannel) {
|
|
1313
|
+
yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
if (!this.responseConsumerRunning) {
|
|
1317
|
+
yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const message = this.buildSDKUserMessage(prompt, images);
|
|
1322
|
+
|
|
1323
|
+
// Create a promise-based handler to yield chunks
|
|
1324
|
+
// Use a mutable state object to work around TypeScript's control flow analysis
|
|
1325
|
+
const state = {
|
|
1326
|
+
chunks: [] as StreamChunk[],
|
|
1327
|
+
resolveChunk: null as ((chunk: StreamChunk | null) => void) | null,
|
|
1328
|
+
done: false,
|
|
1329
|
+
error: null as Error | null,
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
const handlerId = `handler-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1333
|
+
const handler = createResponseHandler({
|
|
1334
|
+
id: handlerId,
|
|
1335
|
+
onChunk: (chunk) => {
|
|
1336
|
+
handler.markChunkSeen();
|
|
1337
|
+
if (state.resolveChunk) {
|
|
1338
|
+
state.resolveChunk(chunk);
|
|
1339
|
+
state.resolveChunk = null;
|
|
1340
|
+
} else {
|
|
1341
|
+
state.chunks.push(chunk);
|
|
1342
|
+
}
|
|
1343
|
+
},
|
|
1344
|
+
onDone: () => {
|
|
1345
|
+
state.done = true;
|
|
1346
|
+
if (state.resolveChunk) {
|
|
1347
|
+
state.resolveChunk(null);
|
|
1348
|
+
state.resolveChunk = null;
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
onError: (err) => {
|
|
1352
|
+
state.error = err;
|
|
1353
|
+
state.done = true;
|
|
1354
|
+
if (state.resolveChunk) {
|
|
1355
|
+
state.resolveChunk(null);
|
|
1356
|
+
state.resolveChunk = null;
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
this.registerResponseHandler(handler);
|
|
1362
|
+
|
|
1363
|
+
try {
|
|
1364
|
+
// Track message for crash recovery (Phase 1.3)
|
|
1365
|
+
this.lastSentMessage = message;
|
|
1366
|
+
this.lastSentQueryOptions = queryOptions ?? null;
|
|
1367
|
+
this.crashRecoveryAttempted = false;
|
|
1368
|
+
|
|
1369
|
+
// Enqueue the message with race condition protection
|
|
1370
|
+
// The channel could close between our null check above and this call
|
|
1371
|
+
try {
|
|
1372
|
+
this.messageChannel.enqueue(message);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (error instanceof Error && error.message.includes('closed')) {
|
|
1375
|
+
yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
this.recordTurnMetadata({
|
|
1381
|
+
userMessageId: message.uuid ?? undefined,
|
|
1382
|
+
wasSent: true,
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// Yield chunks as they arrive
|
|
1386
|
+
while (!state.done) {
|
|
1387
|
+
if (state.chunks.length > 0) {
|
|
1388
|
+
yield state.chunks.shift()!;
|
|
1389
|
+
} else {
|
|
1390
|
+
const chunk = await new Promise<StreamChunk | null>((resolve) => {
|
|
1391
|
+
state.resolveChunk = resolve;
|
|
1392
|
+
});
|
|
1393
|
+
if (chunk) {
|
|
1394
|
+
yield chunk;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Yield any remaining chunks
|
|
1400
|
+
while (state.chunks.length > 0) {
|
|
1401
|
+
yield state.chunks.shift()!;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Check if an error occurred (assigned in onError callback)
|
|
1405
|
+
if (state.error) {
|
|
1406
|
+
// Re-throw session expired errors for outer retry logic to handle
|
|
1407
|
+
if (isSessionExpiredError(state.error)) {
|
|
1408
|
+
throw state.error;
|
|
1409
|
+
}
|
|
1410
|
+
yield { type: 'error', content: state.error.message };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// Clear message tracking after completion
|
|
1414
|
+
this.lastSentMessage = null;
|
|
1415
|
+
this.lastSentQueryOptions = null;
|
|
1416
|
+
|
|
1417
|
+
yield { type: 'done' };
|
|
1418
|
+
} finally {
|
|
1419
|
+
this.unregisterResponseHandler(handlerId);
|
|
1420
|
+
this.currentAllowedTools = null;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
private buildSDKUserMessage(prompt: string, images?: ImageAttachment[]): SDKUserMessage {
|
|
1425
|
+
return buildClaudeSDKUserMessage(
|
|
1426
|
+
prompt,
|
|
1427
|
+
this.sessionManager.getSessionId() || '',
|
|
1428
|
+
images,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Apply dynamic updates to the persistent query before sending a message (Phase 1.6).
|
|
1434
|
+
*/
|
|
1435
|
+
private async applyDynamicUpdates(
|
|
1436
|
+
queryOptions?: QueryOptions,
|
|
1437
|
+
restartOptions?: ClosePersistentQueryOptions,
|
|
1438
|
+
allowRestart = true
|
|
1439
|
+
): Promise<void> {
|
|
1440
|
+
await applyClaudeDynamicUpdates(
|
|
1441
|
+
{
|
|
1442
|
+
getPersistentQuery: () => this.persistentQuery,
|
|
1443
|
+
getCurrentConfig: () => this.currentConfig,
|
|
1444
|
+
mutateCurrentConfig: (mutate) => {
|
|
1445
|
+
if (this.currentConfig) {
|
|
1446
|
+
mutate(this.currentConfig);
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
getVaultPath: () => this.vaultPath,
|
|
1450
|
+
getCliPath: () => this.plugin.getResolvedProviderCliPath('claude'),
|
|
1451
|
+
getScopedSettings: () => this.getScopedSettings(),
|
|
1452
|
+
getPermissionMode: () => this.plugin.settings.permissionMode,
|
|
1453
|
+
resolveSDKPermissionMode: (mode) => this.resolveSDKPermissionMode(mode),
|
|
1454
|
+
mcpManager: this.mcpManager,
|
|
1455
|
+
buildPersistentQueryConfig: (vaultPath, cliPath, externalContextPaths) =>
|
|
1456
|
+
this.buildPersistentQueryConfig(vaultPath, cliPath, externalContextPaths),
|
|
1457
|
+
needsRestart: (newConfig) => this.needsRestart(newConfig),
|
|
1458
|
+
ensureReady: (options) => this.ensureReady(options),
|
|
1459
|
+
setCurrentExternalContextPaths: (paths) => {
|
|
1460
|
+
this.currentExternalContextPaths = paths;
|
|
1461
|
+
},
|
|
1462
|
+
notifyFailure: (message) => {
|
|
1463
|
+
new Notice(message);
|
|
1464
|
+
},
|
|
1465
|
+
},
|
|
1466
|
+
queryOptions,
|
|
1467
|
+
restartOptions,
|
|
1468
|
+
allowRestart,
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private noteVisibleStreamContent(
|
|
1473
|
+
message: SDKMessage,
|
|
1474
|
+
event: TransformEvent,
|
|
1475
|
+
callbacks: { onText: () => void; onThinking: () => void },
|
|
1476
|
+
): void {
|
|
1477
|
+
// Drive dedup off transformed chunks rather than raw SDK message shapes.
|
|
1478
|
+
// transformSDKMessage already filters out empty payloads and subagent-only
|
|
1479
|
+
// stream events, so these callbacks only fire for content the user can see.
|
|
1480
|
+
if (message.type !== 'stream_event') {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (event.type === 'text') {
|
|
1485
|
+
callbacks.onText();
|
|
1486
|
+
} else if (event.type === 'thinking') {
|
|
1487
|
+
callbacks.onThinking();
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private buildPromptWithImages(prompt: string, images?: ImageAttachment[]): ReturnType<typeof buildClaudePromptWithImages> {
|
|
1492
|
+
return buildClaudePromptWithImages(prompt, images);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private async *queryViaSDK(
|
|
1496
|
+
prompt: string,
|
|
1497
|
+
cwd: string,
|
|
1498
|
+
cliPath: string,
|
|
1499
|
+
images?: ImageAttachment[],
|
|
1500
|
+
queryOptions?: QueryOptions
|
|
1501
|
+
): AsyncGenerator<StreamChunk> {
|
|
1502
|
+
this.resetTurnMetadata();
|
|
1503
|
+
const selectedModel = toClaudeRuntimeModelId(queryOptions?.model || this.getScopedSettings().model);
|
|
1504
|
+
|
|
1505
|
+
this.sessionManager.setPendingModel(selectedModel);
|
|
1506
|
+
this.vaultPath = cwd;
|
|
1507
|
+
|
|
1508
|
+
const queryPrompt = this.buildPromptWithImages(prompt, images);
|
|
1509
|
+
const baseContext = this.buildQueryOptionsContext(cwd, cliPath);
|
|
1510
|
+
const externalContextPaths = queryOptions?.externalContextPaths || [];
|
|
1511
|
+
const hooks = this.buildHooks();
|
|
1512
|
+
const hasEditorContext = prompt.includes('<editor_selection');
|
|
1513
|
+
|
|
1514
|
+
let allowedTools: string[] | undefined;
|
|
1515
|
+
if (queryOptions?.allowedTools !== undefined && queryOptions.allowedTools.length > 0) {
|
|
1516
|
+
const toolSet = new Set([...queryOptions.allowedTools, TOOL_SKILL]);
|
|
1517
|
+
allowedTools = [...toolSet];
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const ctx: ColdStartQueryContext = {
|
|
1521
|
+
...baseContext,
|
|
1522
|
+
abortController: this.abortController ?? undefined,
|
|
1523
|
+
sessionId: this.sessionManager.getSessionId() ?? undefined,
|
|
1524
|
+
modelOverride: queryOptions?.model,
|
|
1525
|
+
canUseTool: this.createApprovalCallback(),
|
|
1526
|
+
hooks,
|
|
1527
|
+
mcpMentions: queryOptions?.mcpMentions,
|
|
1528
|
+
enabledMcpServers: queryOptions?.enabledMcpServers,
|
|
1529
|
+
allowedTools,
|
|
1530
|
+
hasEditorContext,
|
|
1531
|
+
externalContextPaths,
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
const options = QueryOptionsBuilder.buildColdStartQueryOptions(ctx);
|
|
1535
|
+
|
|
1536
|
+
let sawStreamText = false;
|
|
1537
|
+
let sawStreamThinking = false;
|
|
1538
|
+
const streamState = createTransformStreamState();
|
|
1539
|
+
const usageState = createTransformUsageState();
|
|
1540
|
+
try {
|
|
1541
|
+
const response = agentQuery({ prompt: queryPrompt, options });
|
|
1542
|
+
this.recordTurnMetadata({ wasSent: true });
|
|
1543
|
+
let streamSessionId: string | null = this.sessionManager.getSessionId();
|
|
1544
|
+
|
|
1545
|
+
for await (const message of response) {
|
|
1546
|
+
if (this.abortController?.signal.aborted) {
|
|
1547
|
+
await response.interrupt();
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
for (const event of transformSDKMessage(message, this.getTransformOptions(selectedModel, streamState, usageState))) {
|
|
1552
|
+
this.noteVisibleStreamContent(message, event, {
|
|
1553
|
+
onText: () => {
|
|
1554
|
+
sawStreamText = true;
|
|
1555
|
+
},
|
|
1556
|
+
onThinking: () => {
|
|
1557
|
+
sawStreamThinking = true;
|
|
1558
|
+
},
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
if (isSessionInitEvent(event)) {
|
|
1562
|
+
this.sessionManager.captureSession(event.sessionId);
|
|
1563
|
+
streamSessionId = event.sessionId;
|
|
1564
|
+
} else if (isContextWindowEvent(event)) {
|
|
1565
|
+
const usageChunk = this.updateBufferedUsageContextWindow(event.contextWindow);
|
|
1566
|
+
if (usageChunk) {
|
|
1567
|
+
yield usageChunk;
|
|
1568
|
+
}
|
|
1569
|
+
} else if (isStreamChunk(event)) {
|
|
1570
|
+
if (message.type === 'assistant' && sawStreamText && event.type === 'text') {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
if (message.type === 'assistant' && sawStreamThinking && event.type === 'thinking') {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
if (event.type === 'usage') {
|
|
1577
|
+
yield this.bufferUsageChunk({ ...event, sessionId: streamSessionId });
|
|
1578
|
+
} else {
|
|
1579
|
+
yield event;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
if (message.type === 'assistant' && message.uuid) {
|
|
1585
|
+
this.recordTurnMetadata({ assistantMessageId: message.uuid });
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
if (message.type === 'result') {
|
|
1589
|
+
sawStreamText = false;
|
|
1590
|
+
sawStreamThinking = false;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
// Re-throw session expired errors for outer retry logic to handle
|
|
1595
|
+
if (isSessionExpiredError(error)) {
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1598
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
1599
|
+
yield { type: 'error', content: msg };
|
|
1600
|
+
} finally {
|
|
1601
|
+
this.sessionManager.clearPendingModel();
|
|
1602
|
+
this.currentAllowedTools = null; // Clear tool restriction after query
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
yield { type: 'done' };
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
cancel() {
|
|
1609
|
+
this.approvalDismisser?.();
|
|
1610
|
+
|
|
1611
|
+
if (this.abortController) {
|
|
1612
|
+
this.abortController.abort();
|
|
1613
|
+
this.sessionManager.markInterrupted();
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Interrupt persistent query (Phase 1.9)
|
|
1617
|
+
if (this.persistentQuery && !this.shuttingDown) {
|
|
1618
|
+
void this.persistentQuery.interrupt().catch(() => {
|
|
1619
|
+
// Silence abort/interrupt errors
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Reset the conversation session.
|
|
1626
|
+
* Closes the persistent query since session is changing.
|
|
1627
|
+
*/
|
|
1628
|
+
resetSession() {
|
|
1629
|
+
// Close persistent query (new session will use cold-start resume)
|
|
1630
|
+
this.closePersistentQuery('session reset');
|
|
1631
|
+
|
|
1632
|
+
// Reset crash recovery for fresh start
|
|
1633
|
+
this.crashRecoveryAttempted = false;
|
|
1634
|
+
|
|
1635
|
+
this.sessionManager.reset();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
getSessionId(): string | null {
|
|
1639
|
+
return this.sessionManager.getSessionId();
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/** Consume session invalidation flag for persistence updates. */
|
|
1643
|
+
consumeSessionInvalidation(): boolean {
|
|
1644
|
+
return this.sessionManager.consumeInvalidation();
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* Check if the service is ready (persistent query is active).
|
|
1649
|
+
* Used to determine if SDK skills are available.
|
|
1650
|
+
*/
|
|
1651
|
+
isReady(): boolean {
|
|
1652
|
+
return this.isPersistentQueryActive();
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Get supported commands (SDK skills).
|
|
1657
|
+
* Returns cached commands populated on system/init. Falls back to a fresh
|
|
1658
|
+
* supportedCommands() call if the cache is empty (e.g., dropdown opened
|
|
1659
|
+
* before the first init event).
|
|
1660
|
+
*/
|
|
1661
|
+
async getSupportedCommands(): Promise<SlashCommand[]> {
|
|
1662
|
+
if (this.cachedSdkCommands.length > 0) {
|
|
1663
|
+
return this.cachedSdkCommands;
|
|
1664
|
+
}
|
|
1665
|
+
if (!this.persistentQuery) {
|
|
1666
|
+
return [];
|
|
1667
|
+
}
|
|
1668
|
+
return this.fetchAndCacheCommands(this.persistentQuery);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Fetches commands from the SDK and caches them. Called on system/init
|
|
1673
|
+
* (fire-and-forget) and as a fallback from getSupportedCommands().
|
|
1674
|
+
*/
|
|
1675
|
+
private async fetchAndCacheCommands(query: Query | null): Promise<SlashCommand[]> {
|
|
1676
|
+
if (!query) return [];
|
|
1677
|
+
try {
|
|
1678
|
+
const sdkCommands: SDKSlashCommand[] = await query.supportedCommands();
|
|
1679
|
+
const mappedCommands = sdkCommands.map((cmd) => ({
|
|
1680
|
+
id: `sdk:${cmd.name}`,
|
|
1681
|
+
name: cmd.name,
|
|
1682
|
+
description: cmd.description,
|
|
1683
|
+
argumentHint: cmd.argumentHint,
|
|
1684
|
+
content: '',
|
|
1685
|
+
source: 'sdk' as const,
|
|
1686
|
+
}));
|
|
1687
|
+
if (this.persistentQuery !== query) {
|
|
1688
|
+
return this.cachedSdkCommands;
|
|
1689
|
+
}
|
|
1690
|
+
this.cachedSdkCommands = mappedCommands;
|
|
1691
|
+
return this.cachedSdkCommands;
|
|
1692
|
+
} catch {
|
|
1693
|
+
return [];
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Set the session ID (for restoring from saved conversation).
|
|
1699
|
+
* Closes persistent query synchronously if session is changing, then ensures query is ready.
|
|
1700
|
+
*
|
|
1701
|
+
* @param id - Session ID to restore, or null for new session
|
|
1702
|
+
* @param externalContextPaths - External context paths for the session (prevents stale contexts)
|
|
1703
|
+
*/
|
|
1704
|
+
setSessionId(id: string | null, externalContextPaths?: string[]): void {
|
|
1705
|
+
const currentId = this.sessionManager.getSessionId();
|
|
1706
|
+
const sessionChanged = currentId !== id;
|
|
1707
|
+
|
|
1708
|
+
// Close synchronously when session changes
|
|
1709
|
+
if (sessionChanged) {
|
|
1710
|
+
this.closePersistentQuery('session switch');
|
|
1711
|
+
this.crashRecoveryAttempted = false;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
this.sessionManager.setSessionId(id, this.getScopedSettings().model);
|
|
1715
|
+
|
|
1716
|
+
// Track external context paths for when the runtime starts on demand
|
|
1717
|
+
if (externalContextPaths !== undefined) {
|
|
1718
|
+
this.currentExternalContextPaths = externalContextPaths;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Passive: do NOT call ensureReady() here.
|
|
1722
|
+
// Runtime starts on demand when query() is called.
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Cleanup resources (Phase 5).
|
|
1727
|
+
* Called on plugin unload to close persistent query and abort any cold-start query.
|
|
1728
|
+
*/
|
|
1729
|
+
cleanup() {
|
|
1730
|
+
// Close persistent query
|
|
1731
|
+
this.closePersistentQuery('plugin cleanup');
|
|
1732
|
+
|
|
1733
|
+
// Cancel any in-flight cold-start query
|
|
1734
|
+
this.cancel();
|
|
1735
|
+
this.resetSession();
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
async rewindFiles(userMessageId: string, dryRun?: boolean): Promise<RewindFilesResult> {
|
|
1739
|
+
if (!this.persistentQuery) throw new Error('No active query');
|
|
1740
|
+
if (this.shuttingDown) throw new Error('Service is shutting down');
|
|
1741
|
+
return this.persistentQuery.rewindFiles(userMessageId, { dryRun });
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
async rewind(
|
|
1745
|
+
userMessageId: string,
|
|
1746
|
+
assistantMessageId: string,
|
|
1747
|
+
mode: ChatRewindMode = 'code-and-conversation',
|
|
1748
|
+
): Promise<ChatRewindResult> {
|
|
1749
|
+
return executeClaudeRewind(userMessageId, {
|
|
1750
|
+
assistantMessageId,
|
|
1751
|
+
mode,
|
|
1752
|
+
rewindFiles: (id, dryRun) => this.rewindFiles(id, dryRun),
|
|
1753
|
+
closePersistentQuery: (reason) => this.closePersistentQuery(reason),
|
|
1754
|
+
setPendingResumeAt: (resumeAt) => {
|
|
1755
|
+
this.pendingResumeAt = resumeAt;
|
|
1756
|
+
},
|
|
1757
|
+
vaultPath: this.vaultPath,
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
setApprovalCallback(callback: ApprovalCallback | null) {
|
|
1762
|
+
this.approvalCallback = callback;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
setApprovalDismisser(dismisser: (() => void) | null) {
|
|
1766
|
+
this.approvalDismisser = dismisser;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
setAskUserQuestionCallback(callback: AskUserQuestionCallback | null) {
|
|
1770
|
+
this.askUserQuestionCallback = callback;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
setExitPlanModeCallback(callback: ExitPlanModeCallback | null): void {
|
|
1774
|
+
this.exitPlanModeCallback = callback;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
setPermissionModeSyncCallback(callback: ((sdkMode: string) => void) | null): void {
|
|
1778
|
+
this.permissionModeSyncCallback = callback;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
setSubagentHookProvider(getState: () => SubagentHookState): void {
|
|
1782
|
+
this._subagentStateProvider = getState;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
setAutoTurnCallback(callback: AutoTurnCallback | null): void {
|
|
1786
|
+
this._autoTurnCallback = callback;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
private createApprovalCallback(): CanUseTool {
|
|
1790
|
+
return createClaudeApprovalCallback({
|
|
1791
|
+
getAllowedTools: () => this.currentAllowedTools,
|
|
1792
|
+
getApprovalCallback: () => this.approvalCallback,
|
|
1793
|
+
getAskUserQuestionCallback: () => this.askUserQuestionCallback,
|
|
1794
|
+
getExitPlanModeCallback: () => this.exitPlanModeCallback,
|
|
1795
|
+
getPermissionMode: () => this.plugin.settings.permissionMode,
|
|
1796
|
+
resolveSDKPermissionMode: (mode) => this.resolveSDKPermissionMode(mode),
|
|
1797
|
+
syncPermissionMode: (mode, sdkMode) => {
|
|
1798
|
+
if (this.currentConfig) {
|
|
1799
|
+
this.currentConfig.permissionMode = mode;
|
|
1800
|
+
this.currentConfig.sdkPermissionMode = sdkMode;
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
private resolveSDKPermissionMode(mode: PermissionMode): SDKPermissionMode {
|
|
1807
|
+
return QueryOptionsBuilder.resolveClaudeSdkPermissionMode(
|
|
1808
|
+
mode,
|
|
1809
|
+
getClaudeProviderSettings(this.plugin.settings).safeMode,
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|