@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.
Files changed (687) hide show
  1. package/.env.local.example +2 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/claude-code-review.yml +57 -0
  4. package/.github/workflows/claude.yml +50 -0
  5. package/.github/workflows/duplicate-issues.yml +22 -0
  6. package/.github/workflows/release.yml +73 -0
  7. package/.github/workflows/stale.yml +21 -0
  8. package/.node-version +1 -0
  9. package/AGENTS.md +3 -0
  10. package/CLAUDE.md +80 -0
  11. package/LICENSE +21 -0
  12. package/README.md +190 -0
  13. package/assets/Preview.png +0 -0
  14. package/assets/sponsors/MOMA.png +0 -0
  15. package/bun.lock +1618 -0
  16. package/esbuild.config.mjs +195 -0
  17. package/eslint.config.mjs +143 -0
  18. package/jest.config.js +41 -0
  19. package/main.js +104915 -0
  20. package/manifest.json +10 -0
  21. package/package.json +65 -0
  22. package/scripts/build-css.mjs +119 -0
  23. package/scripts/build.mjs +19 -0
  24. package/scripts/postinstall.mjs +25 -0
  25. package/scripts/rendererSafeUnref.js +205 -0
  26. package/scripts/run-jest.js +19 -0
  27. package/scripts/sync-version.js +16 -0
  28. package/src/app/settings/ClaudianSettingsStorage.ts +435 -0
  29. package/src/app/settings/defaultSettings.ts +54 -0
  30. package/src/app/storage/SharedStorageService.ts +106 -0
  31. package/src/core/CLAUDE.md +84 -0
  32. package/src/core/auxiliary/AuxQueryRunner.ts +11 -0
  33. package/src/core/auxiliary/QueryBackedInlineEditService.ts +73 -0
  34. package/src/core/auxiliary/QueryBackedInstructionRefineService.ts +78 -0
  35. package/src/core/auxiliary/QueryBackedTitleGenerationService.ts +90 -0
  36. package/src/core/bootstrap/SessionStorage.ts +179 -0
  37. package/src/core/bootstrap/StoragePaths.ts +7 -0
  38. package/src/core/bootstrap/storage.ts +20 -0
  39. package/src/core/commands/builtInCommands.ts +141 -0
  40. package/src/core/mcp/McpConfigParser.ts +102 -0
  41. package/src/core/mcp/McpServerManager.ts +119 -0
  42. package/src/core/mcp/McpTester.ts +310 -0
  43. package/src/core/prompt/inlineEdit.ts +252 -0
  44. package/src/core/prompt/instructionRefine.ts +72 -0
  45. package/src/core/prompt/mainAgent.ts +213 -0
  46. package/src/core/prompt/titleGeneration.ts +44 -0
  47. package/src/core/providers/ProviderRegistry.ts +253 -0
  48. package/src/core/providers/ProviderSettingsCoordinator.ts +434 -0
  49. package/src/core/providers/ProviderWorkspaceRegistry.ts +119 -0
  50. package/src/core/providers/commands/ProviderCommandCatalog.ts +21 -0
  51. package/src/core/providers/commands/ProviderCommandEntry.ts +33 -0
  52. package/src/core/providers/commands/hiddenCommands.ts +74 -0
  53. package/src/core/providers/modelRouting.ts +17 -0
  54. package/src/core/providers/modelSelection.ts +72 -0
  55. package/src/core/providers/providerConfig.ts +34 -0
  56. package/src/core/providers/providerEnvironment.ts +364 -0
  57. package/src/core/providers/types.ts +544 -0
  58. package/src/core/runtime/ChatRuntime.ts +66 -0
  59. package/src/core/runtime/QueuedTurn.ts +97 -0
  60. package/src/core/runtime/types.ts +118 -0
  61. package/src/core/security/ApprovalManager.ts +142 -0
  62. package/src/core/storage/HomeFileAdapter.ts +75 -0
  63. package/src/core/storage/VaultFileAdapter.ts +132 -0
  64. package/src/core/tools/todo.ts +65 -0
  65. package/src/core/tools/toolIcons.ts +80 -0
  66. package/src/core/tools/toolInput.ts +119 -0
  67. package/src/core/tools/toolNames.ts +149 -0
  68. package/src/core/tools/toolResultContent.ts +26 -0
  69. package/src/core/types/agent.ts +28 -0
  70. package/src/core/types/chat.ts +177 -0
  71. package/src/core/types/diff.ts +31 -0
  72. package/src/core/types/index.ts +78 -0
  73. package/src/core/types/mcp.ts +97 -0
  74. package/src/core/types/plugins.ts +9 -0
  75. package/src/core/types/provider.ts +1 -0
  76. package/src/core/types/settings.ts +152 -0
  77. package/src/core/types/tools.ts +80 -0
  78. package/src/features/chat/CLAUDE.md +136 -0
  79. package/src/features/chat/ClaudianView.ts +762 -0
  80. package/src/features/chat/constants.ts +114 -0
  81. package/src/features/chat/controllers/BrowserSelectionController.ts +295 -0
  82. package/src/features/chat/controllers/CanvasSelectionController.ts +142 -0
  83. package/src/features/chat/controllers/ConversationController.ts +1103 -0
  84. package/src/features/chat/controllers/InputController.ts +1707 -0
  85. package/src/features/chat/controllers/NavigationController.ts +209 -0
  86. package/src/features/chat/controllers/SelectionController.ts +430 -0
  87. package/src/features/chat/controllers/StreamController.ts +1560 -0
  88. package/src/features/chat/controllers/contextRowVisibility.ts +18 -0
  89. package/src/features/chat/rendering/DiffRenderer.ts +134 -0
  90. package/src/features/chat/rendering/InlineAskUserQuestion.ts +702 -0
  91. package/src/features/chat/rendering/InlineExitPlanMode.ts +263 -0
  92. package/src/features/chat/rendering/InlinePlanApproval.ts +183 -0
  93. package/src/features/chat/rendering/MessageRenderer.ts +907 -0
  94. package/src/features/chat/rendering/SubagentRenderer.ts +678 -0
  95. package/src/features/chat/rendering/ThinkingBlockRenderer.ts +126 -0
  96. package/src/features/chat/rendering/TodoListRenderer.ts +5 -0
  97. package/src/features/chat/rendering/ToolCallRenderer.ts +1161 -0
  98. package/src/features/chat/rendering/WriteEditRenderer.ts +232 -0
  99. package/src/features/chat/rendering/collapsible.ts +101 -0
  100. package/src/features/chat/rendering/subagentLifecycleResolution.ts +25 -0
  101. package/src/features/chat/rendering/todoUtils.ts +29 -0
  102. package/src/features/chat/rewind.ts +31 -0
  103. package/src/features/chat/services/BangBashService.ts +56 -0
  104. package/src/features/chat/services/SubagentManager.ts +1107 -0
  105. package/src/features/chat/state/ChatState.ts +436 -0
  106. package/src/features/chat/state/types.ts +138 -0
  107. package/src/features/chat/tabs/Tab.ts +1886 -0
  108. package/src/features/chat/tabs/TabBar.ts +179 -0
  109. package/src/features/chat/tabs/TabManager.ts +1021 -0
  110. package/src/features/chat/tabs/providerResolution.ts +34 -0
  111. package/src/features/chat/tabs/types.ts +287 -0
  112. package/src/features/chat/ui/BangBashModeManager.ts +121 -0
  113. package/src/features/chat/ui/FileContext.ts +385 -0
  114. package/src/features/chat/ui/ImageContext.ts +366 -0
  115. package/src/features/chat/ui/InputToolbar.ts +1244 -0
  116. package/src/features/chat/ui/InstructionModeManager.ts +158 -0
  117. package/src/features/chat/ui/NavigationSidebar.ts +126 -0
  118. package/src/features/chat/ui/StatusPanel.ts +589 -0
  119. package/src/features/chat/ui/file-context/state/FileContextState.ts +83 -0
  120. package/src/features/chat/ui/file-context/view/FileChipsView.ts +70 -0
  121. package/src/features/chat/ui/textareaResize.ts +47 -0
  122. package/src/features/chat/utils/usageInfo.ts +26 -0
  123. package/src/features/inline-edit/ui/InlineEditModal.ts +895 -0
  124. package/src/features/inline-edit/ui/inlineEditMarkdownPreview.ts +55 -0
  125. package/src/features/settings/ClaudianSettings.ts +672 -0
  126. package/src/features/settings/keyboardNavigation.ts +60 -0
  127. package/src/features/settings/ui/EnvSnippetManager.ts +430 -0
  128. package/src/features/settings/ui/EnvironmentSettingsSection.ts +85 -0
  129. package/src/features/settings/ui/McpServerModal.ts +335 -0
  130. package/src/features/settings/ui/McpSettingsManager.ts +400 -0
  131. package/src/features/settings/ui/McpTestModal.ts +346 -0
  132. package/src/i18n/constants.ts +58 -0
  133. package/src/i18n/i18n.ts +140 -0
  134. package/src/i18n/locales/de.json +322 -0
  135. package/src/i18n/locales/en.json +322 -0
  136. package/src/i18n/locales/es.json +322 -0
  137. package/src/i18n/locales/fr.json +322 -0
  138. package/src/i18n/locales/ja.json +322 -0
  139. package/src/i18n/locales/ko.json +322 -0
  140. package/src/i18n/locales/pt.json +322 -0
  141. package/src/i18n/locales/ru.json +322 -0
  142. package/src/i18n/locales/zh-CN.json +322 -0
  143. package/src/i18n/locales/zh-TW.json +322 -0
  144. package/src/i18n/types.ts +248 -0
  145. package/src/main.ts +772 -0
  146. package/src/providers/acp/AcpClientConnection.ts +361 -0
  147. package/src/providers/acp/AcpJsonRpcTransport.ts +427 -0
  148. package/src/providers/acp/AcpSessionConfig.ts +139 -0
  149. package/src/providers/acp/AcpSessionUpdateNormalizer.ts +371 -0
  150. package/src/providers/acp/AcpSubprocess.ts +155 -0
  151. package/src/providers/acp/AcpToolStreamAdapter.ts +132 -0
  152. package/src/providers/acp/buildAcpUsageInfo.ts +41 -0
  153. package/src/providers/acp/index.ts +9 -0
  154. package/src/providers/acp/methodNames.ts +50 -0
  155. package/src/providers/acp/types.ts +566 -0
  156. package/src/providers/claude/CLAUDE.md +78 -0
  157. package/src/providers/claude/agents/AgentManager.ts +225 -0
  158. package/src/providers/claude/agents/AgentStorage.ts +101 -0
  159. package/src/providers/claude/app/ClaudeWorkspaceServices.ts +90 -0
  160. package/src/providers/claude/auxiliary/ClaudeInlineEditService.ts +122 -0
  161. package/src/providers/claude/auxiliary/ClaudeInstructionRefineService.ts +90 -0
  162. package/src/providers/claude/auxiliary/ClaudeTitleGenerationService.ts +129 -0
  163. package/src/providers/claude/auxiliary/extractAssistantText.ts +28 -0
  164. package/src/providers/claude/capabilities.ts +17 -0
  165. package/src/providers/claude/cli/findClaudeCLIPath.ts +261 -0
  166. package/src/providers/claude/commands/ClaudeCommandCatalog.ts +149 -0
  167. package/src/providers/claude/commands/probeRuntimeCommands.ts +86 -0
  168. package/src/providers/claude/env/ClaudeSettingsReconciler.ts +90 -0
  169. package/src/providers/claude/env/claudeModelEnv.ts +86 -0
  170. package/src/providers/claude/history/ClaudeConversationHistoryService.ts +446 -0
  171. package/src/providers/claude/history/ClaudeHistoryStore.ts +170 -0
  172. package/src/providers/claude/history/sdkAsyncSubagent.ts +92 -0
  173. package/src/providers/claude/history/sdkBranchFilter.ts +271 -0
  174. package/src/providers/claude/history/sdkHistoryTypes.ts +61 -0
  175. package/src/providers/claude/history/sdkMessageParsing.ts +413 -0
  176. package/src/providers/claude/history/sdkSessionPaths.ts +98 -0
  177. package/src/providers/claude/history/sdkSubagentSidecar.ts +261 -0
  178. package/src/providers/claude/hooks/SubagentHooks.ts +31 -0
  179. package/src/providers/claude/modelLabels.ts +77 -0
  180. package/src/providers/claude/modelOptions.ts +113 -0
  181. package/src/providers/claude/modelSelection.ts +17 -0
  182. package/src/providers/claude/plugins/PluginManager.ts +194 -0
  183. package/src/providers/claude/prompt/ClaudeTurnEncoder.ts +46 -0
  184. package/src/providers/claude/registration.ts +39 -0
  185. package/src/providers/claude/runtime/ClaudeApprovalHandler.ts +153 -0
  186. package/src/providers/claude/runtime/ClaudeChatRuntime.ts +1812 -0
  187. package/src/providers/claude/runtime/ClaudeCliResolver.ts +94 -0
  188. package/src/providers/claude/runtime/ClaudeDynamicUpdates.ts +164 -0
  189. package/src/providers/claude/runtime/ClaudeMessageChannel.ts +209 -0
  190. package/src/providers/claude/runtime/ClaudeQueryOptionsBuilder.ts +315 -0
  191. package/src/providers/claude/runtime/ClaudeRewindService.ts +220 -0
  192. package/src/providers/claude/runtime/ClaudeSessionManager.ts +92 -0
  193. package/src/providers/claude/runtime/ClaudeTaskResultInterpreter.ts +172 -0
  194. package/src/providers/claude/runtime/ClaudeUserMessageFactory.ts +83 -0
  195. package/src/providers/claude/runtime/claudeColdStartQuery.ts +152 -0
  196. package/src/providers/claude/runtime/customSpawn.ts +87 -0
  197. package/src/providers/claude/runtime/types.ts +134 -0
  198. package/src/providers/claude/sdk/messages.ts +17 -0
  199. package/src/providers/claude/sdk/toolResultContent.ts +4 -0
  200. package/src/providers/claude/sdk/typeGuards.ts +14 -0
  201. package/src/providers/claude/sdk/types.ts +15 -0
  202. package/src/providers/claude/security/ClaudePermissionUpdates.ts +44 -0
  203. package/src/providers/claude/settings.ts +138 -0
  204. package/src/providers/claude/storage/AgentVaultStorage.ts +101 -0
  205. package/src/providers/claude/storage/CCSettingsStorage.ts +153 -0
  206. package/src/providers/claude/storage/ClaudianSettingsStorage.ts +6 -0
  207. package/src/providers/claude/storage/McpStorage.ts +139 -0
  208. package/src/providers/claude/storage/SessionStorage.ts +5 -0
  209. package/src/providers/claude/storage/SkillStorage.ts +61 -0
  210. package/src/providers/claude/storage/SlashCommandStorage.ts +96 -0
  211. package/src/providers/claude/storage/StorageService.ts +185 -0
  212. package/src/providers/claude/stream/toolInputStreamState.ts +318 -0
  213. package/src/providers/claude/stream/transformClaudeMessage.ts +586 -0
  214. package/src/providers/claude/types/agent.ts +2 -0
  215. package/src/providers/claude/types/models.ts +168 -0
  216. package/src/providers/claude/types/plugins.ts +14 -0
  217. package/src/providers/claude/types/providerState.ts +16 -0
  218. package/src/providers/claude/types/settings.ts +98 -0
  219. package/src/providers/claude/ui/AgentSettings.ts +389 -0
  220. package/src/providers/claude/ui/ClaudeChatUIConfig.ts +113 -0
  221. package/src/providers/claude/ui/ClaudeSettingsTab.ts +407 -0
  222. package/src/providers/claude/ui/PluginSettingsManager.ts +149 -0
  223. package/src/providers/claude/ui/SlashCommandSettings.ts +527 -0
  224. package/src/providers/codex/CLAUDE.md +64 -0
  225. package/src/providers/codex/agents/CodexAgentMentionProvider.ts +33 -0
  226. package/src/providers/codex/app/CodexWorkspaceServices.ts +76 -0
  227. package/src/providers/codex/auxiliary/CodexInlineEditService.ts +9 -0
  228. package/src/providers/codex/auxiliary/CodexInstructionRefineService.ts +9 -0
  229. package/src/providers/codex/auxiliary/CodexTaskResultInterpreter.ts +29 -0
  230. package/src/providers/codex/auxiliary/CodexTitleGenerationService.ts +22 -0
  231. package/src/providers/codex/capabilities.ts +16 -0
  232. package/src/providers/codex/commands/CodexSkillCatalog.ts +180 -0
  233. package/src/providers/codex/env/CodexSettingsReconciler.ts +68 -0
  234. package/src/providers/codex/history/CodexConversationHistoryService.ts +212 -0
  235. package/src/providers/codex/history/CodexHistoryStore.ts +1672 -0
  236. package/src/providers/codex/modelOptions.ts +99 -0
  237. package/src/providers/codex/modelSelection.ts +17 -0
  238. package/src/providers/codex/normalization/codexSubagentNormalization.ts +227 -0
  239. package/src/providers/codex/normalization/codexToolNormalization.ts +390 -0
  240. package/src/providers/codex/prompt/encodeCodexTurn.ts +57 -0
  241. package/src/providers/codex/registration.ts +29 -0
  242. package/src/providers/codex/runtime/CodexAppServerProcess.ts +105 -0
  243. package/src/providers/codex/runtime/CodexAuxQueryRunner.ts +180 -0
  244. package/src/providers/codex/runtime/CodexBinaryLocator.ts +49 -0
  245. package/src/providers/codex/runtime/CodexChatRuntime.ts +1296 -0
  246. package/src/providers/codex/runtime/CodexCliResolver.ts +65 -0
  247. package/src/providers/codex/runtime/CodexExecutionTargetResolver.ts +104 -0
  248. package/src/providers/codex/runtime/CodexLaunchSpecBuilder.ts +85 -0
  249. package/src/providers/codex/runtime/CodexNotificationRouter.ts +1033 -0
  250. package/src/providers/codex/runtime/CodexPathMapper.ts +155 -0
  251. package/src/providers/codex/runtime/CodexRpcTransport.ts +171 -0
  252. package/src/providers/codex/runtime/CodexRuntimeContext.ts +109 -0
  253. package/src/providers/codex/runtime/CodexServerRequestRouter.ts +331 -0
  254. package/src/providers/codex/runtime/CodexSessionFileTail.ts +792 -0
  255. package/src/providers/codex/runtime/CodexSessionManager.ts +39 -0
  256. package/src/providers/codex/runtime/codexAppServerSupport.ts +58 -0
  257. package/src/providers/codex/runtime/codexAppServerTypes.ts +705 -0
  258. package/src/providers/codex/runtime/codexLaunchTypes.ts +30 -0
  259. package/src/providers/codex/settings.ts +236 -0
  260. package/src/providers/codex/skills/CodexSkillListingService.ts +173 -0
  261. package/src/providers/codex/storage/CodexSkillStorage.ts +250 -0
  262. package/src/providers/codex/storage/CodexSubagentStorage.ts +212 -0
  263. package/src/providers/codex/types/index.ts +16 -0
  264. package/src/providers/codex/types/models.ts +46 -0
  265. package/src/providers/codex/types/subagent.ts +23 -0
  266. package/src/providers/codex/ui/CodexChatUIConfig.ts +128 -0
  267. package/src/providers/codex/ui/CodexSettingsTab.ts +432 -0
  268. package/src/providers/codex/ui/CodexSkillSettings.ts +275 -0
  269. package/src/providers/codex/ui/CodexSubagentSettings.ts +400 -0
  270. package/src/providers/defaultProviderConfigs.ts +14 -0
  271. package/src/providers/index.ts +30 -0
  272. package/src/providers/opencode/agents/OpencodeAgentMentionProvider.ts +42 -0
  273. package/src/providers/opencode/app/OpencodeRuntimeCommandLoader.ts +67 -0
  274. package/src/providers/opencode/app/OpencodeWorkspaceServices.ts +55 -0
  275. package/src/providers/opencode/auxiliary/OpencodeInlineEditService.ts +13 -0
  276. package/src/providers/opencode/auxiliary/OpencodeInstructionRefineService.ts +12 -0
  277. package/src/providers/opencode/auxiliary/OpencodeTaskResultInterpreter.ts +29 -0
  278. package/src/providers/opencode/auxiliary/OpencodeTitleGenerationService.ts +27 -0
  279. package/src/providers/opencode/capabilities.ts +16 -0
  280. package/src/providers/opencode/commands/OpencodeCommandCatalog.ts +92 -0
  281. package/src/providers/opencode/discoveryState.ts +135 -0
  282. package/src/providers/opencode/env/OpencodeSettingsReconciler.ts +178 -0
  283. package/src/providers/opencode/history/OpencodeConversationHistoryService.ts +84 -0
  284. package/src/providers/opencode/history/OpencodeHistoryStore.ts +472 -0
  285. package/src/providers/opencode/history/OpencodeSqliteReader.ts +284 -0
  286. package/src/providers/opencode/internal/compareCollections.ts +72 -0
  287. package/src/providers/opencode/internal/providerProjection.ts +15 -0
  288. package/src/providers/opencode/models.ts +378 -0
  289. package/src/providers/opencode/modes.ts +150 -0
  290. package/src/providers/opencode/normalization/opencodeToolNormalization.ts +406 -0
  291. package/src/providers/opencode/registration.ts +27 -0
  292. package/src/providers/opencode/runtime/OpencodeAuxQueryRunner.ts +436 -0
  293. package/src/providers/opencode/runtime/OpencodeChatRuntime.ts +1603 -0
  294. package/src/providers/opencode/runtime/OpencodeCliResolver.ts +57 -0
  295. package/src/providers/opencode/runtime/OpencodeLaunchArtifacts.ts +231 -0
  296. package/src/providers/opencode/runtime/OpencodePaths.ts +113 -0
  297. package/src/providers/opencode/runtime/OpencodeRuntimeEnvironment.ts +18 -0
  298. package/src/providers/opencode/runtime/buildOpencodePrompt.ts +66 -0
  299. package/src/providers/opencode/settings.ts +427 -0
  300. package/src/providers/opencode/storage/OpencodeAgentStorage.ts +346 -0
  301. package/src/providers/opencode/types/agent.ts +37 -0
  302. package/src/providers/opencode/types/index.ts +9 -0
  303. package/src/providers/opencode/ui/OpencodeAgentSettings.ts +579 -0
  304. package/src/providers/opencode/ui/OpencodeChatUIConfig.ts +316 -0
  305. package/src/providers/opencode/ui/OpencodeSettingsTab.ts +674 -0
  306. package/src/providers/pi/app/PiRuntimeCommandLoader.ts +57 -0
  307. package/src/providers/pi/app/PiWorkspaceServices.ts +39 -0
  308. package/src/providers/pi/auxiliary/PiInlineEditService.ts +9 -0
  309. package/src/providers/pi/auxiliary/PiInstructionRefineService.ts +9 -0
  310. package/src/providers/pi/auxiliary/PiTaskResultInterpreter.ts +29 -0
  311. package/src/providers/pi/auxiliary/PiTitleGenerationService.ts +19 -0
  312. package/src/providers/pi/capabilities.ts +16 -0
  313. package/src/providers/pi/commands/PiCommandCatalog.ts +92 -0
  314. package/src/providers/pi/env/PiSettingsReconciler.ts +180 -0
  315. package/src/providers/pi/history/PiConversationHistoryService.ts +123 -0
  316. package/src/providers/pi/history/PiHistoryStore.ts +664 -0
  317. package/src/providers/pi/internal/compareCollections.ts +4 -0
  318. package/src/providers/pi/internal/providerProjection.ts +18 -0
  319. package/src/providers/pi/models.ts +302 -0
  320. package/src/providers/pi/normalizations/piEventNormalization.ts +211 -0
  321. package/src/providers/pi/normalizations/piToolNormalization.ts +97 -0
  322. package/src/providers/pi/registration.ts +30 -0
  323. package/src/providers/pi/runtime/PiAuxQueryRunner.ts +216 -0
  324. package/src/providers/pi/runtime/PiChatRuntime.ts +1064 -0
  325. package/src/providers/pi/runtime/PiCliResolver.ts +53 -0
  326. package/src/providers/pi/runtime/PiExtensionUiBridge.ts +161 -0
  327. package/src/providers/pi/runtime/PiJsonl.ts +71 -0
  328. package/src/providers/pi/runtime/PiLaunchSpec.ts +70 -0
  329. package/src/providers/pi/runtime/PiModelDiscoveryService.ts +92 -0
  330. package/src/providers/pi/runtime/PiRpcPayloads.ts +18 -0
  331. package/src/providers/pi/runtime/PiRpcTransport.ts +243 -0
  332. package/src/providers/pi/runtime/PiSubprocess.ts +159 -0
  333. package/src/providers/pi/runtime/buildPiPrompt.ts +62 -0
  334. package/src/providers/pi/runtime/buildPiUsageInfo.ts +69 -0
  335. package/src/providers/pi/settings.ts +468 -0
  336. package/src/providers/pi/types.ts +64 -0
  337. package/src/providers/pi/ui/ObsidianPiExtensionUiRenderer.ts +251 -0
  338. package/src/providers/pi/ui/PiChatUIConfig.ts +265 -0
  339. package/src/providers/pi/ui/PiExtensionUiRenderer.ts +12 -0
  340. package/src/providers/pi/ui/PiSettingsTab.ts +642 -0
  341. package/src/shared/components/ResumeSessionDropdown.ts +185 -0
  342. package/src/shared/components/SelectableDropdown.ts +140 -0
  343. package/src/shared/components/SelectionHighlight.ts +77 -0
  344. package/src/shared/components/SlashCommandDropdown.ts +421 -0
  345. package/src/shared/icons.ts +180 -0
  346. package/src/shared/mention/MentionDropdownController.ts +627 -0
  347. package/src/shared/mention/VaultMentionCache.ts +106 -0
  348. package/src/shared/mention/VaultMentionDataProvider.ts +51 -0
  349. package/src/shared/mention/types.ts +67 -0
  350. package/src/shared/modals/ConfirmModal.ts +60 -0
  351. package/src/shared/modals/ForkTargetModal.ts +47 -0
  352. package/src/shared/modals/InstructionConfirmModal.ts +281 -0
  353. package/src/style/CLAUDE.md +49 -0
  354. package/src/style/accessibility.css +40 -0
  355. package/src/style/base/animations.css +44 -0
  356. package/src/style/base/container.css +20 -0
  357. package/src/style/base/variables.css +46 -0
  358. package/src/style/base/visibility.css +15 -0
  359. package/src/style/components/code.css +97 -0
  360. package/src/style/components/context-footer.css +76 -0
  361. package/src/style/components/header.css +27 -0
  362. package/src/style/components/history.css +221 -0
  363. package/src/style/components/input.css +312 -0
  364. package/src/style/components/messages.css +262 -0
  365. package/src/style/components/nav-sidebar.css +58 -0
  366. package/src/style/components/status-panel.css +202 -0
  367. package/src/style/components/subagent.css +248 -0
  368. package/src/style/components/tabs.css +112 -0
  369. package/src/style/components/thinking.css +88 -0
  370. package/src/style/components/toolcalls.css +278 -0
  371. package/src/style/features/ask-user-question.css +315 -0
  372. package/src/style/features/diff.css +197 -0
  373. package/src/style/features/file-context.css +188 -0
  374. package/src/style/features/file-link.css +22 -0
  375. package/src/style/features/image-context.css +179 -0
  376. package/src/style/features/image-embed.css +40 -0
  377. package/src/style/features/image-modal.css +52 -0
  378. package/src/style/features/inline-edit.css +278 -0
  379. package/src/style/features/plan-mode.css +103 -0
  380. package/src/style/features/resume-session.css +119 -0
  381. package/src/style/features/slash-commands.css +91 -0
  382. package/src/style/index.css +63 -0
  383. package/src/style/modals/fork-target.css +21 -0
  384. package/src/style/modals/instruction.css +161 -0
  385. package/src/style/modals/mcp-modal.css +241 -0
  386. package/src/style/settings/agent-settings.css +2 -0
  387. package/src/style/settings/base.css +300 -0
  388. package/src/style/settings/env-snippets.css +366 -0
  389. package/src/style/settings/mcp-settings.css +211 -0
  390. package/src/style/settings/plugin-settings.css +164 -0
  391. package/src/style/settings/provider-model-picker.css +367 -0
  392. package/src/style/settings/slash-settings.css +16 -0
  393. package/src/style/toolbar/external-context.css +177 -0
  394. package/src/style/toolbar/mcp-selector.css +176 -0
  395. package/src/style/toolbar/mode-selector.css +19 -0
  396. package/src/style/toolbar/model-selector.css +99 -0
  397. package/src/style/toolbar/permission-toggle.css +56 -0
  398. package/src/style/toolbar/service-tier-toggle.css +39 -0
  399. package/src/style/toolbar/thinking-selector.css +83 -0
  400. package/src/types/smol-toml.d.ts +4 -0
  401. package/src/utils/agent.ts +50 -0
  402. package/src/utils/animationFrame.ts +46 -0
  403. package/src/utils/browser.ts +46 -0
  404. package/src/utils/canvas.ts +14 -0
  405. package/src/utils/cliBinaryLocator.ts +97 -0
  406. package/src/utils/context.ts +117 -0
  407. package/src/utils/contextMentionResolver.ts +154 -0
  408. package/src/utils/date.ts +31 -0
  409. package/src/utils/diff.ts +384 -0
  410. package/src/utils/editor.ts +104 -0
  411. package/src/utils/electronCompat.ts +53 -0
  412. package/src/utils/env.ts +465 -0
  413. package/src/utils/externalContext.ts +143 -0
  414. package/src/utils/externalContextScanner.ts +135 -0
  415. package/src/utils/fileLink.ts +263 -0
  416. package/src/utils/frontmatter.ts +194 -0
  417. package/src/utils/imageEmbed.ts +139 -0
  418. package/src/utils/inlineEdit.ts +22 -0
  419. package/src/utils/interrupt.ts +23 -0
  420. package/src/utils/markdown.ts +25 -0
  421. package/src/utils/markdownMath.ts +130 -0
  422. package/src/utils/mcp.ts +96 -0
  423. package/src/utils/obsidianCompat.ts +23 -0
  424. package/src/utils/path.ts +342 -0
  425. package/src/utils/session.ts +240 -0
  426. package/src/utils/slashCommand.ts +152 -0
  427. package/src/utils/subagentJsonl.ts +52 -0
  428. package/src/utils/windowsCmdShim.ts +98 -0
  429. package/tests/__mocks__/claude-agent-sdk.ts +317 -0
  430. package/tests/__mocks__/codex-sdk.ts +88 -0
  431. package/tests/__mocks__/obsidian.ts +434 -0
  432. package/tests/helpers/mockElement.ts +403 -0
  433. package/tests/helpers/sdkMessages.ts +291 -0
  434. package/tests/integration/core/agent/ClaudianService.test.ts +1845 -0
  435. package/tests/integration/core/mcp/mcp.test.ts +905 -0
  436. package/tests/integration/features/chat/imagePersistence.test.ts +38 -0
  437. package/tests/integration/main.test.ts +1701 -0
  438. package/tests/setupWindow.ts +26 -0
  439. package/tests/tsconfig.json +7 -0
  440. package/tests/unit/core/commands/builtInCommands.test.ts +239 -0
  441. package/tests/unit/core/mcp/McpServerManager.test.ts +405 -0
  442. package/tests/unit/core/mcp/McpTester.test.ts +282 -0
  443. package/tests/unit/core/mcp/createNodeFetch.test.ts +188 -0
  444. package/tests/unit/core/providers/ProviderRegistry.test.ts +275 -0
  445. package/tests/unit/core/providers/ProviderSettingsCoordinator.test.ts +490 -0
  446. package/tests/unit/core/providers/ProviderWorkspaceRegistry.test.ts +84 -0
  447. package/tests/unit/core/providers/modelRouting.test.ts +91 -0
  448. package/tests/unit/core/providers/modelSelection.test.ts +155 -0
  449. package/tests/unit/core/providers/providerEnvironment.test.ts +162 -0
  450. package/tests/unit/core/providers/tabLifecycle.test.ts +217 -0
  451. package/tests/unit/core/security/ApprovalManager.test.ts +152 -0
  452. package/tests/unit/core/storage/VaultFileAdapter.test.ts +535 -0
  453. package/tests/unit/core/tools/todo.test.ts +227 -0
  454. package/tests/unit/core/tools/toolIcons.test.ts +75 -0
  455. package/tests/unit/core/tools/toolInput.test.ts +350 -0
  456. package/tests/unit/core/tools/toolNames.test.ts +464 -0
  457. package/tests/unit/core/types/mcp.test.ts +115 -0
  458. package/tests/unit/features/chat/ClaudianView.test.ts +404 -0
  459. package/tests/unit/features/chat/controllers/BrowserSelectionController.test.ts +179 -0
  460. package/tests/unit/features/chat/controllers/CanvasSelectionController.test.ts +216 -0
  461. package/tests/unit/features/chat/controllers/ConversationController.test.ts +2764 -0
  462. package/tests/unit/features/chat/controllers/InputController.test.ts +3188 -0
  463. package/tests/unit/features/chat/controllers/NavigationController.test.ts +640 -0
  464. package/tests/unit/features/chat/controllers/SelectionController.test.ts +695 -0
  465. package/tests/unit/features/chat/controllers/StreamController.test.ts +2534 -0
  466. package/tests/unit/features/chat/controllers/contextRowVisibility.test.ts +46 -0
  467. package/tests/unit/features/chat/controllers/index.test.ts +16 -0
  468. package/tests/unit/features/chat/rendering/DiffRenderer.test.ts +355 -0
  469. package/tests/unit/features/chat/rendering/InlineAskUserQuestion.test.ts +1035 -0
  470. package/tests/unit/features/chat/rendering/InlineExitPlanMode.test.ts +191 -0
  471. package/tests/unit/features/chat/rendering/InlinePlanApproval.test.ts +126 -0
  472. package/tests/unit/features/chat/rendering/MessageRenderer.test.ts +2004 -0
  473. package/tests/unit/features/chat/rendering/SubagentRenderer.test.ts +917 -0
  474. package/tests/unit/features/chat/rendering/ThinkingBlockRenderer.test.ts +124 -0
  475. package/tests/unit/features/chat/rendering/TodoListRenderer.test.ts +173 -0
  476. package/tests/unit/features/chat/rendering/ToolCallRenderer.test.ts +909 -0
  477. package/tests/unit/features/chat/rendering/WriteEditRenderer.test.ts +474 -0
  478. package/tests/unit/features/chat/rendering/collapsible.test.ts +158 -0
  479. package/tests/unit/features/chat/rendering/todoUtils.test.ts +105 -0
  480. package/tests/unit/features/chat/rewind.test.ts +56 -0
  481. package/tests/unit/features/chat/services/BangBashService.test.ts +142 -0
  482. package/tests/unit/features/chat/services/InstructionRefineService.test.ts +371 -0
  483. package/tests/unit/features/chat/services/SubagentManager.test.ts +1759 -0
  484. package/tests/unit/features/chat/services/TitleGenerationService.test.ts +480 -0
  485. package/tests/unit/features/chat/state/ChatState.test.ts +581 -0
  486. package/tests/unit/features/chat/tabs/Tab.test.ts +4287 -0
  487. package/tests/unit/features/chat/tabs/TabBar.test.ts +357 -0
  488. package/tests/unit/features/chat/tabs/TabManager.test.ts +2962 -0
  489. package/tests/unit/features/chat/tabs/index.test.ts +11 -0
  490. package/tests/unit/features/chat/ui/BangBashModeManager.test.ts +321 -0
  491. package/tests/unit/features/chat/ui/ExternalContextSelector.test.ts +555 -0
  492. package/tests/unit/features/chat/ui/FileContextManager.test.ts +876 -0
  493. package/tests/unit/features/chat/ui/ImageContext.test.ts +777 -0
  494. package/tests/unit/features/chat/ui/InputToolbar.test.ts +1139 -0
  495. package/tests/unit/features/chat/ui/InstructionModeManager.test.ts +243 -0
  496. package/tests/unit/features/chat/ui/NavigationSidebar.test.ts +570 -0
  497. package/tests/unit/features/chat/ui/StatusPanel.test.ts +953 -0
  498. package/tests/unit/features/chat/ui/file-context/state/FileContextState.test.ts +155 -0
  499. package/tests/unit/features/chat/ui/textareaResize.test.ts +102 -0
  500. package/tests/unit/features/chat/utils/usageInfo.test.ts +56 -0
  501. package/tests/unit/features/inline-edit/InlineEditService.test.ts +1199 -0
  502. package/tests/unit/features/inline-edit/ui/InlineEditModal.openAndWait.test.ts +1482 -0
  503. package/tests/unit/features/inline-edit/ui/InlineEditModal.test.ts +495 -0
  504. package/tests/unit/features/inline-edit/ui/inlineEditMarkdownPreview.test.ts +92 -0
  505. package/tests/unit/features/settings/AgentSettings.test.ts +82 -0
  506. package/tests/unit/features/settings/keyboardNavigation.test.ts +73 -0
  507. package/tests/unit/features/settings/ui/CodexSkillSettings.test.ts +294 -0
  508. package/tests/unit/features/settings/ui/CodexSubagentSettings.test.ts +207 -0
  509. package/tests/unit/i18n/constants.test.ts +43 -0
  510. package/tests/unit/i18n/i18n.test.ts +244 -0
  511. package/tests/unit/i18n/locales.test.ts +134 -0
  512. package/tests/unit/providers/acp/AcpClientConnection.test.ts +248 -0
  513. package/tests/unit/providers/acp/AcpJsonRpcTransport.test.ts +186 -0
  514. package/tests/unit/providers/acp/AcpSessionConfig.test.ts +247 -0
  515. package/tests/unit/providers/acp/AcpSessionUpdateNormalizer.test.ts +145 -0
  516. package/tests/unit/providers/acp/AcpSubprocess.test.ts +105 -0
  517. package/tests/unit/providers/acp/buildAcpUsageInfo.test.ts +51 -0
  518. package/tests/unit/providers/claude/agents/AgentManager.test.ts +590 -0
  519. package/tests/unit/providers/claude/agents/AgentStorage.test.ts +434 -0
  520. package/tests/unit/providers/claude/agents/index.test.ts +10 -0
  521. package/tests/unit/providers/claude/commands/ClaudeCommandCatalog.test.ts +396 -0
  522. package/tests/unit/providers/claude/commands/probeRuntimeCommands.test.ts +92 -0
  523. package/tests/unit/providers/claude/env/ClaudeSettingsReconciler.test.ts +57 -0
  524. package/tests/unit/providers/claude/env/claudeModelEnv.test.ts +228 -0
  525. package/tests/unit/providers/claude/hooks/SubagentHooks.test.ts +83 -0
  526. package/tests/unit/providers/claude/plugins/PluginManager.test.ts +832 -0
  527. package/tests/unit/providers/claude/plugins/index.test.ts +7 -0
  528. package/tests/unit/providers/claude/prompt/ClaudeTurnEncoder.test.ts +145 -0
  529. package/tests/unit/providers/claude/prompt/instructionRefine.test.ts +185 -0
  530. package/tests/unit/providers/claude/prompt/systemPrompt.test.ts +163 -0
  531. package/tests/unit/providers/claude/prompt/titleGeneration.test.ts +20 -0
  532. package/tests/unit/providers/claude/runtime/ClaudeTaskResultInterpreter.test.ts +28 -0
  533. package/tests/unit/providers/claude/runtime/ClaudianService.test.ts +3796 -0
  534. package/tests/unit/providers/claude/runtime/MessageChannel.test.ts +421 -0
  535. package/tests/unit/providers/claude/runtime/QueryOptionsBuilder.test.ts +775 -0
  536. package/tests/unit/providers/claude/runtime/SessionManager.test.ts +182 -0
  537. package/tests/unit/providers/claude/runtime/claudeColdStartQuery.test.ts +331 -0
  538. package/tests/unit/providers/claude/runtime/customSpawn.test.ts +374 -0
  539. package/tests/unit/providers/claude/runtime/index.test.ts +13 -0
  540. package/tests/unit/providers/claude/runtime/types.test.ts +190 -0
  541. package/tests/unit/providers/claude/sdk/typeGuards.test.ts +50 -0
  542. package/tests/unit/providers/claude/security/ClaudePermissionUpdates.test.ts +198 -0
  543. package/tests/unit/providers/claude/storage/AgentVaultStorage.test.ts +413 -0
  544. package/tests/unit/providers/claude/storage/CCSettingsStorage.test.ts +408 -0
  545. package/tests/unit/providers/claude/storage/ClaudianSettingsStorage.test.ts +653 -0
  546. package/tests/unit/providers/claude/storage/McpStorage.test.ts +619 -0
  547. package/tests/unit/providers/claude/storage/SessionStorage.test.ts +680 -0
  548. package/tests/unit/providers/claude/storage/SkillStorage.test.ts +275 -0
  549. package/tests/unit/providers/claude/storage/SlashCommandStorage.test.ts +612 -0
  550. package/tests/unit/providers/claude/storage/storage.test.ts +360 -0
  551. package/tests/unit/providers/claude/storage/storageService.convenience.test.ts +447 -0
  552. package/tests/unit/providers/claude/stream/transformSDKMessage.test.ts +1729 -0
  553. package/tests/unit/providers/claude/types/types.test.ts +726 -0
  554. package/tests/unit/providers/claude/ui/ClaudeChatUIConfig.test.ts +173 -0
  555. package/tests/unit/providers/claude/ui/ClaudeSettingsTab.test.ts +466 -0
  556. package/tests/unit/providers/codex/agents/CodexAgentMentionProvider.test.ts +89 -0
  557. package/tests/unit/providers/codex/auxiliary/CodexInstructionRefineService.test.ts +81 -0
  558. package/tests/unit/providers/codex/capabilities.test.ts +39 -0
  559. package/tests/unit/providers/codex/commands/CodexSkillCatalog.test.ts +413 -0
  560. package/tests/unit/providers/codex/env/CodexSettingsReconciler.test.ts +106 -0
  561. package/tests/unit/providers/codex/fixtures/codex-session-abort.jsonl +9 -0
  562. package/tests/unit/providers/codex/fixtures/codex-session-agent-lifecycle.jsonl +12 -0
  563. package/tests/unit/providers/codex/fixtures/codex-session-persisted-tools.jsonl +15 -0
  564. package/tests/unit/providers/codex/fixtures/codex-session-simple.jsonl +10 -0
  565. package/tests/unit/providers/codex/fixtures/codex-session-tools.jsonl +12 -0
  566. package/tests/unit/providers/codex/fixtures/codex-session-websearch-persisted.jsonl +3 -0
  567. package/tests/unit/providers/codex/fixtures/codex-session-websearch.jsonl +7 -0
  568. package/tests/unit/providers/codex/history/CodexConversationHistoryService.test.ts +690 -0
  569. package/tests/unit/providers/codex/history/CodexHistoryStore.test.ts +2202 -0
  570. package/tests/unit/providers/codex/normalization/codexSubagentNormalization.test.ts +81 -0
  571. package/tests/unit/providers/codex/normalization/codexToolNormalization.test.ts +322 -0
  572. package/tests/unit/providers/codex/prompt/encodeCodexTurn.test.ts +168 -0
  573. package/tests/unit/providers/codex/runtime/CodexAppServerProcess.test.ts +255 -0
  574. package/tests/unit/providers/codex/runtime/CodexAuxQueryRunner.test.ts +130 -0
  575. package/tests/unit/providers/codex/runtime/CodexBinaryLocator.test.ts +90 -0
  576. package/tests/unit/providers/codex/runtime/CodexChatRuntime.test.ts +2445 -0
  577. package/tests/unit/providers/codex/runtime/CodexCliResolver.test.ts +103 -0
  578. package/tests/unit/providers/codex/runtime/CodexExecutionTargetResolver.test.ts +105 -0
  579. package/tests/unit/providers/codex/runtime/CodexLaunchSpecBuilder.test.ts +150 -0
  580. package/tests/unit/providers/codex/runtime/CodexNotificationRouter.test.ts +1248 -0
  581. package/tests/unit/providers/codex/runtime/CodexPathMapper.test.ts +51 -0
  582. package/tests/unit/providers/codex/runtime/CodexRpcTransport.test.ts +220 -0
  583. package/tests/unit/providers/codex/runtime/CodexRuntimeContext.test.ts +107 -0
  584. package/tests/unit/providers/codex/runtime/CodexServerRequestRouter.test.ts +537 -0
  585. package/tests/unit/providers/codex/runtime/CodexSessionFileTail.test.ts +1305 -0
  586. package/tests/unit/providers/codex/runtime/CodexSessionManager.test.ts +82 -0
  587. package/tests/unit/providers/codex/runtime/codexAppServerTypes.test.ts +336 -0
  588. package/tests/unit/providers/codex/settings.test.ts +189 -0
  589. package/tests/unit/providers/codex/skills/CodexSkillListingService.test.ts +178 -0
  590. package/tests/unit/providers/codex/storage/CodexSkillStorage.test.ts +342 -0
  591. package/tests/unit/providers/codex/storage/CodexSubagentStorage.test.ts +376 -0
  592. package/tests/unit/providers/codex/ui/CodexChatUIConfig.test.ts +211 -0
  593. package/tests/unit/providers/codex/ui/CodexSettingsTab.test.ts +578 -0
  594. package/tests/unit/providers/defaultProviderConfigs.test.ts +18 -0
  595. package/tests/unit/providers/opencode/OpencodeAuxQueryRunner.test.ts +355 -0
  596. package/tests/unit/providers/opencode/OpencodeChatRuntime.test.ts +808 -0
  597. package/tests/unit/providers/opencode/OpencodeCliResolver.test.ts +93 -0
  598. package/tests/unit/providers/opencode/OpencodeCommandCatalog.test.ts +75 -0
  599. package/tests/unit/providers/opencode/OpencodeConversationHistoryService.test.ts +103 -0
  600. package/tests/unit/providers/opencode/OpencodeHistoryStore.test.ts +418 -0
  601. package/tests/unit/providers/opencode/OpencodeLaunchArtifacts.test.ts +268 -0
  602. package/tests/unit/providers/opencode/OpencodePaths.test.ts +35 -0
  603. package/tests/unit/providers/opencode/OpencodeRuntimeCommandLoader.test.ts +129 -0
  604. package/tests/unit/providers/opencode/OpencodeSettingsReconciler.test.ts +96 -0
  605. package/tests/unit/providers/opencode/OpencodeSettingsTab.test.ts +649 -0
  606. package/tests/unit/providers/opencode/OpencodeSqliteReader.test.ts +185 -0
  607. package/tests/unit/providers/opencode/agents/OpencodeAgentMentionProvider.test.ts +56 -0
  608. package/tests/unit/providers/opencode/buildOpencodePrompt.test.ts +87 -0
  609. package/tests/unit/providers/opencode/capabilities.test.ts +39 -0
  610. package/tests/unit/providers/opencode/models.test.ts +273 -0
  611. package/tests/unit/providers/opencode/modes.test.ts +151 -0
  612. package/tests/unit/providers/opencode/opencodeToolNormalization.test.ts +197 -0
  613. package/tests/unit/providers/opencode/settings.test.ts +469 -0
  614. package/tests/unit/providers/opencode/storage/OpencodeAgentStorage.test.ts +377 -0
  615. package/tests/unit/providers/opencode/ui/OpencodeAgentSettings.test.ts +91 -0
  616. package/tests/unit/providers/pi/PiRuntimeCommandLoader.test.ts +149 -0
  617. package/tests/unit/providers/pi/capabilities.test.ts +20 -0
  618. package/tests/unit/providers/pi/commands/PiCommandCatalog.test.ts +69 -0
  619. package/tests/unit/providers/pi/env/PiSettingsReconciler.test.ts +61 -0
  620. package/tests/unit/providers/pi/history/PiConversationHistoryService.test.ts +147 -0
  621. package/tests/unit/providers/pi/history/PiHistoryStore.test.ts +523 -0
  622. package/tests/unit/providers/pi/models.test.ts +96 -0
  623. package/tests/unit/providers/pi/registration.test.ts +54 -0
  624. package/tests/unit/providers/pi/runtime/PiAuxQueryRunner.test.ts +172 -0
  625. package/tests/unit/providers/pi/runtime/PiChatRuntime.test.ts +830 -0
  626. package/tests/unit/providers/pi/runtime/PiCliResolver.test.ts +105 -0
  627. package/tests/unit/providers/pi/runtime/PiEventNormalization.test.ts +147 -0
  628. package/tests/unit/providers/pi/runtime/PiExtensionUiBridge.test.ts +98 -0
  629. package/tests/unit/providers/pi/runtime/PiJsonl.test.ts +42 -0
  630. package/tests/unit/providers/pi/runtime/PiLaunchSpec.test.ts +92 -0
  631. package/tests/unit/providers/pi/runtime/PiModelDiscoveryService.test.ts +135 -0
  632. package/tests/unit/providers/pi/runtime/PiRpcPayloads.test.ts +15 -0
  633. package/tests/unit/providers/pi/runtime/PiRpcTransport.test.ts +116 -0
  634. package/tests/unit/providers/pi/runtime/PiSubprocess.test.ts +151 -0
  635. package/tests/unit/providers/pi/runtime/buildPiPrompt.test.ts +84 -0
  636. package/tests/unit/providers/pi/runtime/buildPiUsageInfo.test.ts +69 -0
  637. package/tests/unit/providers/pi/settings.test.ts +253 -0
  638. package/tests/unit/providers/pi/ui/PiChatUIConfig.test.ts +161 -0
  639. package/tests/unit/providers/pi/ui/PiSettingsTab.test.ts +492 -0
  640. package/tests/unit/scripts/rendererSafeUnref.test.ts +101 -0
  641. package/tests/unit/shared/components/ResumeSessionDropdown.test.ts +356 -0
  642. package/tests/unit/shared/components/SelectableDropdown.test.ts +406 -0
  643. package/tests/unit/shared/components/SlashCommandDropdown.provider.test.ts +354 -0
  644. package/tests/unit/shared/components/SlashCommandDropdown.test.ts +508 -0
  645. package/tests/unit/shared/icons.test.ts +56 -0
  646. package/tests/unit/shared/index.test.ts +46 -0
  647. package/tests/unit/shared/mention/MentionDropdownController.test.ts +823 -0
  648. package/tests/unit/shared/mention/VaultFileCache.test.ts +195 -0
  649. package/tests/unit/shared/mention/VaultFolderCache.test.ts +143 -0
  650. package/tests/unit/shared/mention/VaultMentionDataProvider.test.ts +87 -0
  651. package/tests/unit/shared/modals/ConfirmModal.test.ts +111 -0
  652. package/tests/unit/shared/modals/ForkTargetModal.test.ts +101 -0
  653. package/tests/unit/shared/modals/InstructionConfirmModal.test.ts +305 -0
  654. package/tests/unit/utils/agent.test.ts +395 -0
  655. package/tests/unit/utils/animationFrame.test.ts +59 -0
  656. package/tests/unit/utils/browser.test.ts +73 -0
  657. package/tests/unit/utils/canvas.test.ts +54 -0
  658. package/tests/unit/utils/claudeCli.test.ts +294 -0
  659. package/tests/unit/utils/cliBinaryLocator.test.ts +33 -0
  660. package/tests/unit/utils/context.test.ts +288 -0
  661. package/tests/unit/utils/contextMentionResolver.test.ts +270 -0
  662. package/tests/unit/utils/date.test.ts +80 -0
  663. package/tests/unit/utils/diff.test.ts +291 -0
  664. package/tests/unit/utils/editor.test.ts +249 -0
  665. package/tests/unit/utils/electronCompat.test.ts +87 -0
  666. package/tests/unit/utils/env.test.ts +1240 -0
  667. package/tests/unit/utils/externalContext.test.ts +336 -0
  668. package/tests/unit/utils/externalContextScanner.test.ts +186 -0
  669. package/tests/unit/utils/fileLink.dom.test.ts +273 -0
  670. package/tests/unit/utils/fileLink.handler.test.ts +64 -0
  671. package/tests/unit/utils/fileLink.test.ts +233 -0
  672. package/tests/unit/utils/frontmatter.test.ts +434 -0
  673. package/tests/unit/utils/imageEmbed.test.ts +407 -0
  674. package/tests/unit/utils/inlineEdit.test.ts +63 -0
  675. package/tests/unit/utils/interrupt.test.ts +73 -0
  676. package/tests/unit/utils/markdown.test.ts +35 -0
  677. package/tests/unit/utils/markdownMath.test.ts +54 -0
  678. package/tests/unit/utils/mcp.test.ts +256 -0
  679. package/tests/unit/utils/obsidianCompat.test.ts +18 -0
  680. package/tests/unit/utils/path.test.ts +677 -0
  681. package/tests/unit/utils/sdkSession.test.ts +2359 -0
  682. package/tests/unit/utils/session.test.ts +971 -0
  683. package/tests/unit/utils/slashCommand.test.ts +778 -0
  684. package/tests/unit/utils/utils.test.ts +809 -0
  685. package/tsconfig.jest.json +8 -0
  686. package/tsconfig.json +26 -0
  687. 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
+ }