@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,1603 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ import {
5
+ computeSystemPromptKey,
6
+ type SystemPromptSettings,
7
+ } from '../../../core/prompt/mainAgent';
8
+ import { getRuntimeEnvironmentText } from '../../../core/providers/providerEnvironment';
9
+ import { ProviderRegistry } from '../../../core/providers/ProviderRegistry';
10
+ import { ProviderSettingsCoordinator } from '../../../core/providers/ProviderSettingsCoordinator';
11
+ import type {
12
+ ProviderCapabilities,
13
+ } from '../../../core/providers/types';
14
+ import type { ChatRuntime } from '../../../core/runtime/ChatRuntime';
15
+ import type {
16
+ ApprovalCallback,
17
+ ApprovalDecisionOption,
18
+ AskUserQuestionCallback,
19
+ AutoTurnCallback,
20
+ ChatRewindMode,
21
+ ChatRewindResult,
22
+ ChatRuntimeEnsureReadyOptions,
23
+ ChatRuntimeQueryOptions,
24
+ ChatTurnMetadata,
25
+ ChatTurnRequest,
26
+ PreparedChatTurn,
27
+ SessionUpdateResult,
28
+ SubagentRuntimeState,
29
+ } from '../../../core/runtime/types';
30
+ import type {
31
+ ApprovalDecision,
32
+ ChatMessage,
33
+ Conversation,
34
+ ExitPlanModeCallback,
35
+ SlashCommand,
36
+ StreamChunk,
37
+ ToolCallInfo,
38
+ } from '../../../core/types';
39
+ import type ClaudianPlugin from '../../../main';
40
+ import { getEnhancedPath } from '../../../utils/env';
41
+ import { getVaultPath } from '../../../utils/path';
42
+ import {
43
+ AcpClientConnection,
44
+ AcpJsonRpcTransport,
45
+ type AcpReadTextFileRequest,
46
+ type AcpRequestPermissionRequest,
47
+ type AcpRequestPermissionResponse,
48
+ type AcpSessionConfigOption,
49
+ type AcpSessionModelState,
50
+ type AcpSessionModeState,
51
+ type AcpSessionNotification,
52
+ AcpSessionUpdateNormalizer,
53
+ AcpSubprocess,
54
+ type AcpUsage,
55
+ type AcpUsageUpdate,
56
+ type AcpWriteTextFileRequest,
57
+ buildAcpUsageInfo,
58
+ extractAcpSessionModelState,
59
+ extractAcpSessionModeState,
60
+ extractAcpSessionThoughtLevelState,
61
+ } from '../../acp';
62
+ import { OPENCODE_PROVIDER_CAPABILITIES } from '../capabilities';
63
+ import { updateOpencodeDiscoveryState } from '../discoveryState';
64
+ import {
65
+ sameDiscoveredModels,
66
+ sameModes,
67
+ sameStringList,
68
+ sameStringMap,
69
+ sameThinkingOptionsByModel,
70
+ } from '../internal/compareCollections';
71
+ import { ensureProviderProjectionMap } from '../internal/providerProjection';
72
+ import {
73
+ decodeOpencodeModelId,
74
+ encodeOpencodeModelId,
75
+ isOpencodeModelSelectionId,
76
+ normalizeOpencodeDiscoveredModels,
77
+ normalizeOpencodeModelVariants,
78
+ OPENCODE_DEFAULT_THINKING_LEVEL,
79
+ OPENCODE_SYNTHETIC_MODEL_ID,
80
+ resolveOpencodeBaseModelRawId,
81
+ } from '../models';
82
+ import {
83
+ getManagedOpencodeModes,
84
+ isManagedOpencodeModeId,
85
+ normalizeOpencodeAvailableModes,
86
+ resolveOpencodeModeForPermissionMode,
87
+ resolvePermissionModeForManagedOpencodeMode,
88
+ } from '../modes';
89
+ import { createOpencodeToolStreamAdapter } from '../normalization/opencodeToolNormalization';
90
+ import { getOpencodeProviderSettings, updateOpencodeProviderSettings } from '../settings';
91
+ import { getOpencodeState, type OpencodeProviderState } from '../types';
92
+ import { buildOpencodePromptBlocks, buildOpencodePromptText } from './buildOpencodePrompt';
93
+ import { prepareOpencodeLaunchArtifacts } from './OpencodeLaunchArtifacts';
94
+ import { buildOpencodeRuntimeEnv } from './OpencodeRuntimeEnvironment';
95
+
96
+ interface ActiveTurn {
97
+ queue: StreamChunkQueue;
98
+ sessionId: string;
99
+ }
100
+
101
+ class StreamChunkQueue {
102
+ private closed = false;
103
+ private readonly items: StreamChunk[] = [];
104
+ private readonly waiters: Array<(chunk: StreamChunk | null) => void> = [];
105
+
106
+ push(chunk: StreamChunk): void {
107
+ const waiter = this.waiters.shift();
108
+ if (waiter) {
109
+ waiter(chunk);
110
+ return;
111
+ }
112
+ this.items.push(chunk);
113
+ }
114
+
115
+ close(): void {
116
+ if (this.closed) {
117
+ return;
118
+ }
119
+
120
+ this.closed = true;
121
+ while (this.waiters.length > 0) {
122
+ this.waiters.shift()?.(null);
123
+ }
124
+ }
125
+
126
+ async next(): Promise<StreamChunk | null> {
127
+ if (this.items.length > 0) {
128
+ return this.items.shift() ?? null;
129
+ }
130
+
131
+ if (this.closed) {
132
+ return null;
133
+ }
134
+
135
+ return new Promise<StreamChunk | null>((resolve) => {
136
+ this.waiters.push(resolve);
137
+ });
138
+ }
139
+ }
140
+
141
+ export class OpencodeChatRuntime implements ChatRuntime {
142
+ readonly providerId = 'opencode' as const;
143
+
144
+ private activeTurn: ActiveTurn | null = null;
145
+ private approvalCallback: ApprovalCallback | null = null;
146
+ private connection: AcpClientConnection | null = null;
147
+ private contextUsage: AcpUsageUpdate | null = null;
148
+ private currentDatabasePath: string | null = null;
149
+ private currentLaunchKey: string | null = null;
150
+ private currentSessionEffortConfigId: string | null = null;
151
+ private currentSessionEffortValue: string | null = null;
152
+ private currentSessionEffortValues = new Set<string>();
153
+ private currentSessionModelId: string | null = null;
154
+ private currentSessionModeId: string | null = null;
155
+ private currentTurnMetadata: ChatTurnMetadata = {};
156
+ private loadedSessionId: string | null = null;
157
+ private permissionModeSyncCallback: ((mode: string) => void) | null = null;
158
+ private process: AcpSubprocess | null = null;
159
+ private promptUsage: AcpUsage | null = null;
160
+ private readonly readyListeners: Array<(ready: boolean) => void> = [];
161
+ private ready = false;
162
+ private sessionInvalidated = false;
163
+ private readonly supportedCommandWaiters: Array<(commands: SlashCommand[]) => void> = [];
164
+ private supportedCommands: SlashCommand[] = [];
165
+ private sessionCwds = new Map<string, string>();
166
+ private sessionId: string | null = null;
167
+ private readonly sessionUpdateNormalizer = new AcpSessionUpdateNormalizer();
168
+ private readonly toolStreamAdapter = createOpencodeToolStreamAdapter();
169
+ private transport: AcpJsonRpcTransport | null = null;
170
+ private unregisterTransportClose: (() => void) | null = null;
171
+
172
+ constructor(
173
+ private readonly plugin: ClaudianPlugin,
174
+ ) {}
175
+
176
+ getCapabilities(): Readonly<ProviderCapabilities> {
177
+ return OPENCODE_PROVIDER_CAPABILITIES;
178
+ }
179
+
180
+ prepareTurn(request: ChatTurnRequest): PreparedChatTurn {
181
+ return {
182
+ isCompact: false,
183
+ mcpMentions: request.enabledMcpServers ?? new Set(),
184
+ persistedContent: '',
185
+ prompt: buildOpencodePromptText(request),
186
+ request,
187
+ };
188
+ }
189
+
190
+ onReadyStateChange(listener: (ready: boolean) => void): () => void {
191
+ this.readyListeners.push(listener);
192
+ return () => {
193
+ const index = this.readyListeners.indexOf(listener);
194
+ if (index >= 0) {
195
+ this.readyListeners.splice(index, 1);
196
+ }
197
+ };
198
+ }
199
+
200
+ setResumeCheckpoint(_checkpointId: string | undefined): void {}
201
+
202
+ syncConversationState(
203
+ conversation: { providerState?: Record<string, unknown>; sessionId?: string | null } | null,
204
+ ): void {
205
+ const previousSessionId = this.sessionId;
206
+ const nextSessionId = conversation?.sessionId ?? null;
207
+ if (this.sessionId !== nextSessionId) {
208
+ this.currentSessionEffortConfigId = null;
209
+ this.currentSessionEffortValue = null;
210
+ this.currentSessionEffortValues = new Set<string>();
211
+ this.currentSessionModelId = null;
212
+ this.currentSessionModeId = null;
213
+ this.sessionInvalidated = false;
214
+ this.setSupportedCommands([]);
215
+ }
216
+ this.sessionId = nextSessionId;
217
+ const state = getOpencodeState(conversation?.providerState);
218
+ if (state.databasePath) {
219
+ this.currentDatabasePath = state.databasePath;
220
+ return;
221
+ }
222
+
223
+ if (!nextSessionId || nextSessionId !== previousSessionId) {
224
+ this.currentDatabasePath = null;
225
+ }
226
+ }
227
+
228
+ async reloadMcpServers(): Promise<void> {}
229
+
230
+ async warmModelMetadata(model: string): Promise<boolean> {
231
+ const selectedRawModelId = decodeOpencodeModelId(model);
232
+ if (!selectedRawModelId) {
233
+ return false;
234
+ }
235
+
236
+ if (!(await this.ensureReady({ allowSessionCreation: true }))) {
237
+ return false;
238
+ }
239
+ if (!this.connection || !this.sessionId) {
240
+ return false;
241
+ }
242
+
243
+ const discoveredModels = getOpencodeProviderSettings(this.plugin.settings).discoveredModels;
244
+ const selectedBaseRawModelId = resolveOpencodeBaseModelRawId(selectedRawModelId, discoveredModels);
245
+ if (!selectedBaseRawModelId) {
246
+ return false;
247
+ }
248
+
249
+ const availableModelIds = new Set(discoveredModels.map((entry) => entry.rawId));
250
+ if (availableModelIds.size > 0 && !availableModelIds.has(selectedBaseRawModelId)) {
251
+ return false;
252
+ }
253
+
254
+ const response = await this.connection.setConfigOption({
255
+ configId: 'model',
256
+ sessionId: this.sessionId,
257
+ type: 'select',
258
+ value: selectedBaseRawModelId,
259
+ });
260
+ this.currentSessionModelId = selectedBaseRawModelId;
261
+ await this.syncSessionModelState({
262
+ configOptions: response.configOptions,
263
+ });
264
+ return true;
265
+ }
266
+
267
+ async ensureReady(options?: ChatRuntimeEnsureReadyOptions): Promise<boolean> {
268
+ const settings = getOpencodeProviderSettings(this.plugin.settings);
269
+ if (!settings.enabled) {
270
+ this.setReady(false);
271
+ return false;
272
+ }
273
+
274
+ const cwd = getVaultPath(this.plugin.app) ?? process.cwd();
275
+ const targetSessionId = this.sessionId;
276
+ const resolvedCliPath = this.plugin.getResolvedProviderCliPath('opencode') ?? 'opencode';
277
+ const runtimeEnv = this.buildRuntimeEnv(
278
+ resolvedCliPath,
279
+ this.currentDatabasePath,
280
+ );
281
+ const promptSettings = this.getSystemPromptSettings(cwd);
282
+ const artifacts = await prepareOpencodeLaunchArtifacts({
283
+ runtimeEnv,
284
+ settings: promptSettings,
285
+ workspaceRoot: cwd,
286
+ });
287
+ this.currentDatabasePath = artifacts.databasePath;
288
+
289
+ const nextLaunchKey = JSON.stringify({
290
+ command: resolvedCliPath,
291
+ configPath: artifacts.configPath,
292
+ envText: getRuntimeEnvironmentText(this.plugin.settings, 'opencode'),
293
+ promptKey: computeSystemPromptKey(promptSettings),
294
+ artifactKey: artifacts.launchKey,
295
+ });
296
+
297
+ const shouldRestart = !this.process
298
+ || !this.transport
299
+ || !this.connection
300
+ || !this.process.isAlive()
301
+ || this.transport.isClosed
302
+ || options?.force === true
303
+ || this.currentLaunchKey !== nextLaunchKey;
304
+
305
+ if (shouldRestart) {
306
+ await this.shutdownProcess();
307
+ await this.startProcess({
308
+ command: resolvedCliPath,
309
+ configPath: artifacts.configPath,
310
+ cwd,
311
+ runtimeEnv,
312
+ });
313
+ this.currentLaunchKey = nextLaunchKey;
314
+ this.loadedSessionId = null;
315
+ }
316
+
317
+ if (targetSessionId) {
318
+ if (this.loadedSessionId !== targetSessionId) {
319
+ const loaded = await this.loadSession(targetSessionId, cwd);
320
+ if (!loaded) {
321
+ this.sessionInvalidated = true;
322
+ this.clearActiveSession();
323
+ }
324
+ }
325
+ return true;
326
+ }
327
+
328
+ if (!this.sessionId && !this.sessionInvalidated) {
329
+ if (options?.allowSessionCreation === false) {
330
+ return true;
331
+ }
332
+ return Boolean(await this.createSession(cwd));
333
+ }
334
+
335
+ return true;
336
+ }
337
+
338
+ async *query(
339
+ turn: PreparedChatTurn,
340
+ conversationHistory?: ChatMessage[],
341
+ queryOptions?: ChatRuntimeQueryOptions,
342
+ ): AsyncGenerator<StreamChunk> {
343
+ const previousMessages = conversationHistory ?? [];
344
+ const expectedSessionId = this.sessionId;
345
+ let shouldBootstrapHistory = previousMessages.length > 0
346
+ && (!expectedSessionId || this.sessionInvalidated);
347
+
348
+ if (!(await this.ensureReady())) {
349
+ yield { type: 'error', content: 'Failed to start OpenCode. Check the CLI path and login state.' };
350
+ yield { type: 'done' };
351
+ return;
352
+ }
353
+
354
+ if (!this.connection) {
355
+ yield { type: 'error', content: 'OpenCode runtime is not ready.' };
356
+ yield { type: 'done' };
357
+ return;
358
+ }
359
+
360
+ const cwd = getVaultPath(this.plugin.app) ?? process.cwd();
361
+ if (expectedSessionId && !this.sessionId) {
362
+ shouldBootstrapHistory = previousMessages.length > 0;
363
+ }
364
+
365
+ if (!this.sessionId) {
366
+ const sessionId = await this.createSession(cwd);
367
+ if (!sessionId) {
368
+ yield { type: 'error', content: 'Failed to create an OpenCode session.' };
369
+ yield { type: 'done' };
370
+ return;
371
+ }
372
+ }
373
+
374
+ const sessionId = this.sessionId!;
375
+ this.activeTurn?.queue.close();
376
+ this.activeTurn = {
377
+ queue: new StreamChunkQueue(),
378
+ sessionId,
379
+ };
380
+ this.currentTurnMetadata = {};
381
+ this.contextUsage = null;
382
+ this.promptUsage = null;
383
+ this.sessionUpdateNormalizer.reset();
384
+ this.toolStreamAdapter.reset();
385
+
386
+ const activeTurn = this.activeTurn;
387
+ try {
388
+ await this.applySelectedMode(sessionId);
389
+ await this.applySelectedModel(sessionId, queryOptions);
390
+ await this.applySelectedEffort(sessionId);
391
+ } catch (error) {
392
+ yield {
393
+ type: 'error',
394
+ content: this.formatRuntimeError(error),
395
+ };
396
+ yield { type: 'done' };
397
+ activeTurn.queue.close();
398
+ this.activeTurn = null;
399
+ return;
400
+ }
401
+
402
+ const promptPromise = this.connection.prompt({
403
+ prompt: buildOpencodePromptBlocks(
404
+ turn.request,
405
+ shouldBootstrapHistory ? previousMessages : [],
406
+ ),
407
+ sessionId,
408
+ }).then((response) => {
409
+ if (response.userMessageId) {
410
+ this.currentTurnMetadata.userMessageId = response.userMessageId;
411
+ }
412
+ this.promptUsage = response.usage ?? null;
413
+
414
+ const usage = buildAcpUsageInfo({
415
+ contextWindow: this.contextUsage,
416
+ model: this.getActiveDisplayModel(queryOptions),
417
+ promptUsage: this.promptUsage,
418
+ });
419
+ if (usage) {
420
+ activeTurn.queue.push({ sessionId, type: 'usage', usage });
421
+ }
422
+
423
+ activeTurn.queue.push({ type: 'done' });
424
+ activeTurn.queue.close();
425
+ }).catch((error) => {
426
+ activeTurn.queue.push({
427
+ type: 'error',
428
+ content: this.formatRuntimeError(error),
429
+ });
430
+ activeTurn.queue.push({ type: 'done' });
431
+ activeTurn.queue.close();
432
+ }).finally(() => {
433
+ if (this.activeTurn === activeTurn) {
434
+ this.activeTurn = null;
435
+ }
436
+ });
437
+
438
+ try {
439
+ while (true) {
440
+ const chunk = await activeTurn.queue.next();
441
+ if (!chunk) {
442
+ break;
443
+ }
444
+ yield chunk;
445
+ }
446
+ await promptPromise;
447
+ } finally {
448
+ if (this.activeTurn === activeTurn) {
449
+ this.activeTurn = null;
450
+ }
451
+ }
452
+ }
453
+
454
+ cancel(): void {
455
+ if (this.connection && this.sessionId) {
456
+ this.connection.cancel({ sessionId: this.sessionId });
457
+ }
458
+ }
459
+
460
+ resetSession(): void {
461
+ this.clearActiveSession();
462
+ this.sessionInvalidated = false;
463
+ }
464
+
465
+ getSessionId(): string | null {
466
+ return this.sessionId;
467
+ }
468
+
469
+ consumeSessionInvalidation(): boolean {
470
+ const invalidated = this.sessionInvalidated;
471
+ this.sessionInvalidated = false;
472
+ return invalidated;
473
+ }
474
+
475
+ isReady(): boolean {
476
+ return this.ready;
477
+ }
478
+
479
+ async getSupportedCommands(): Promise<SlashCommand[]> {
480
+ if (this.supportedCommands.length > 0 && this.loadedSessionId === this.sessionId) {
481
+ return [...this.supportedCommands];
482
+ }
483
+
484
+ if (this.sessionId && this.loadedSessionId !== this.sessionId) {
485
+ const ready = await this.ensureReady({ allowSessionCreation: false });
486
+ if (!ready) {
487
+ return [];
488
+ }
489
+ }
490
+
491
+ if (!this.sessionId) {
492
+ return [];
493
+ }
494
+
495
+ if (this.supportedCommands.length > 0) {
496
+ return [...this.supportedCommands];
497
+ }
498
+
499
+ if (!this.sessionId || this.loadedSessionId !== this.sessionId) {
500
+ return [];
501
+ }
502
+
503
+ return this.waitForSupportedCommands();
504
+ }
505
+
506
+ cleanup(): void {
507
+ this.activeTurn?.queue.close();
508
+ void this.shutdownProcess();
509
+ }
510
+
511
+ async rewind(
512
+ _userMessageId: string,
513
+ _assistantMessageId: string,
514
+ _mode?: ChatRewindMode,
515
+ ): Promise<ChatRewindResult> {
516
+ return { canRewind: false };
517
+ }
518
+
519
+ setApprovalCallback(callback: ApprovalCallback | null): void {
520
+ this.approvalCallback = callback;
521
+ }
522
+
523
+ setApprovalDismisser(_dismisser: (() => void) | null): void {}
524
+
525
+ setAskUserQuestionCallback(_callback: AskUserQuestionCallback | null): void {}
526
+
527
+ setExitPlanModeCallback(_callback: ExitPlanModeCallback | null): void {}
528
+
529
+ setPermissionModeSyncCallback(callback: ((sdkMode: string) => void) | null): void {
530
+ this.permissionModeSyncCallback = callback;
531
+ }
532
+
533
+ setSubagentHookProvider(_getState: () => SubagentRuntimeState): void {}
534
+
535
+ setAutoTurnCallback(_callback: AutoTurnCallback | null): void {}
536
+
537
+ consumeTurnMetadata(): ChatTurnMetadata {
538
+ const metadata = this.currentTurnMetadata;
539
+ this.currentTurnMetadata = {};
540
+ return metadata;
541
+ }
542
+
543
+ buildSessionUpdates(params: {
544
+ conversation: Conversation | null;
545
+ sessionInvalidated: boolean;
546
+ }): SessionUpdateResult {
547
+ const existingState = params.conversation
548
+ ? getOpencodeState(params.conversation.providerState)
549
+ : null;
550
+ const providerState: OpencodeProviderState = {
551
+ ...(this.currentDatabasePath || existingState?.databasePath
552
+ ? { databasePath: this.currentDatabasePath ?? existingState?.databasePath }
553
+ : {}),
554
+ };
555
+ const updates: Partial<Conversation> = {
556
+ providerState: Object.keys(providerState).length > 0
557
+ ? providerState as Record<string, unknown>
558
+ : undefined,
559
+ sessionId: this.sessionId,
560
+ };
561
+
562
+ if (params.sessionInvalidated) {
563
+ if (!this.sessionId) {
564
+ updates.providerState = undefined;
565
+ updates.sessionId = null;
566
+ }
567
+ }
568
+
569
+ return { updates };
570
+ }
571
+
572
+ resolveSessionIdForFork(conversation: Conversation | null): string | null {
573
+ return this.sessionId ?? conversation?.sessionId ?? null;
574
+ }
575
+
576
+ async loadSubagentToolCalls(_agentId: string): Promise<ToolCallInfo[]> {
577
+ return [];
578
+ }
579
+
580
+ async loadSubagentFinalResult(_agentId: string): Promise<string | null> {
581
+ return null;
582
+ }
583
+
584
+ private async startProcess(params: {
585
+ command: string;
586
+ configPath: string;
587
+ cwd: string;
588
+ runtimeEnv: NodeJS.ProcessEnv;
589
+ }): Promise<void> {
590
+ const processEnv: NodeJS.ProcessEnv = {
591
+ ...process.env,
592
+ ...params.runtimeEnv,
593
+ OPENCODE_CONFIG: params.configPath,
594
+ PATH: getEnhancedPath(
595
+ params.runtimeEnv.PATH,
596
+ path.isAbsolute(params.command) ? params.command : undefined,
597
+ ),
598
+ };
599
+
600
+ this.process = new AcpSubprocess({
601
+ args: ['acp', `--cwd=${params.cwd}`],
602
+ command: params.command,
603
+ cwd: params.cwd,
604
+ env: processEnv,
605
+ });
606
+ this.process.start();
607
+
608
+ this.transport = new AcpJsonRpcTransport({
609
+ input: this.process.stdout,
610
+ onClose: (listener) => this.process!.onClose(listener),
611
+ output: this.process.stdin,
612
+ });
613
+ const transport = this.transport;
614
+ this.unregisterTransportClose = transport.onClose(() => {
615
+ if (this.transport === transport) {
616
+ this.setReady(false);
617
+ }
618
+ });
619
+
620
+ this.connection = new AcpClientConnection({
621
+ clientInfo: {
622
+ name: 'claudian',
623
+ version: this.plugin.manifest?.version ?? '0.0.0',
624
+ },
625
+ delegate: {
626
+ fileSystem: {
627
+ readTextFile: (request) => this.readTextFile(request),
628
+ writeTextFile: (request) => this.writeTextFile(request),
629
+ },
630
+ onSessionNotification: (notification) => this.handleSessionNotification(notification),
631
+ requestPermission: (request) => this.handlePermissionRequest(request),
632
+ },
633
+ transport: this.transport,
634
+ });
635
+
636
+ this.transport.start();
637
+ await this.connection.initialize();
638
+ this.setReady(true);
639
+ }
640
+
641
+ private async shutdownProcess(): Promise<void> {
642
+ this.setReady(false);
643
+ this.activeTurn?.queue.close();
644
+ this.activeTurn = null;
645
+ this.currentSessionModelId = null;
646
+ this.currentSessionModeId = null;
647
+ this.setSupportedCommands([]);
648
+
649
+ this.unregisterTransportClose?.();
650
+ this.unregisterTransportClose = null;
651
+
652
+ this.connection?.dispose();
653
+ this.connection = null;
654
+
655
+ this.transport?.dispose();
656
+ this.transport = null;
657
+
658
+ if (this.process) {
659
+ await this.process.shutdown().catch(() => {});
660
+ this.process = null;
661
+ }
662
+ }
663
+
664
+ private setReady(ready: boolean): void {
665
+ if (this.ready === ready) {
666
+ return;
667
+ }
668
+
669
+ this.ready = ready;
670
+ for (const listener of this.readyListeners) {
671
+ listener(ready);
672
+ }
673
+ }
674
+
675
+ private getSystemPromptSettings(vaultPath: string): SystemPromptSettings {
676
+ return {
677
+ customPrompt: this.plugin.settings.systemPrompt,
678
+ mediaFolder: this.plugin.settings.mediaFolder,
679
+ userName: this.plugin.settings.userName,
680
+ vaultPath,
681
+ };
682
+ }
683
+
684
+ private buildRuntimeEnv(
685
+ cliPath: string,
686
+ databasePathOverride?: string | null,
687
+ ): NodeJS.ProcessEnv {
688
+ return buildOpencodeRuntimeEnv(
689
+ this.plugin.settings,
690
+ cliPath,
691
+ databasePathOverride,
692
+ );
693
+ }
694
+
695
+ private getProviderSettings(): Record<string, unknown> {
696
+ return ProviderSettingsCoordinator.getProviderSettingsSnapshot(
697
+ this.plugin.settings,
698
+ this.providerId,
699
+ );
700
+ }
701
+
702
+ private resolveSelectedRawModelId(queryOptions?: ChatRuntimeQueryOptions): string | null {
703
+ const providerSettings = this.getProviderSettings();
704
+ const selectedModel = typeof queryOptions?.model === 'string'
705
+ ? queryOptions.model
706
+ : typeof providerSettings.model === 'string'
707
+ ? providerSettings.model
708
+ : '';
709
+
710
+ if (!isOpencodeModelSelectionId(selectedModel)) {
711
+ return null;
712
+ }
713
+
714
+ const selectedBaseRawModelId = decodeOpencodeModelId(selectedModel);
715
+ if (!selectedBaseRawModelId) {
716
+ return null;
717
+ }
718
+
719
+ const discoveredModels = getOpencodeProviderSettings(providerSettings).discoveredModels;
720
+ const normalizedBaseRawModelId = resolveOpencodeBaseModelRawId(selectedBaseRawModelId, discoveredModels);
721
+ if (!normalizedBaseRawModelId) {
722
+ return null;
723
+ }
724
+
725
+ const availableModelIds = new Set(discoveredModels.map((model) => model.rawId));
726
+ if (availableModelIds.size > 0 && !availableModelIds.has(normalizedBaseRawModelId)) {
727
+ return null;
728
+ }
729
+
730
+ return normalizedBaseRawModelId;
731
+ }
732
+
733
+ getAuxiliaryModel(): string | null {
734
+ return this.getActiveDisplayModel() ?? null;
735
+ }
736
+
737
+ private getActiveDisplayModel(queryOptions?: ChatRuntimeQueryOptions): string | undefined {
738
+ const providerSettings = this.getProviderSettings();
739
+ const selectedModel = typeof queryOptions?.model === 'string'
740
+ ? queryOptions.model
741
+ : typeof providerSettings.model === 'string'
742
+ ? providerSettings.model
743
+ : '';
744
+
745
+ if (
746
+ selectedModel
747
+ && selectedModel !== OPENCODE_SYNTHETIC_MODEL_ID
748
+ && isOpencodeModelSelectionId(selectedModel)
749
+ ) {
750
+ const selectedRawModelId = this.resolveSelectedRawModelId(queryOptions);
751
+ return selectedRawModelId
752
+ ? encodeOpencodeModelId(selectedRawModelId)
753
+ : selectedModel;
754
+ }
755
+
756
+ return this.currentSessionModelId
757
+ ? encodeOpencodeModelId(this.currentSessionModelId)
758
+ : (selectedModel && isOpencodeModelSelectionId(selectedModel) ? selectedModel : undefined);
759
+ }
760
+
761
+ private resolveSelectedModeId(): string | null {
762
+ const providerSettings = this.getProviderSettings();
763
+ const opencodeSettings = getOpencodeProviderSettings(providerSettings);
764
+ const availableModes = getManagedOpencodeModes(opencodeSettings.availableModes);
765
+ const mappedModeId = resolveOpencodeModeForPermissionMode(
766
+ providerSettings.permissionMode,
767
+ opencodeSettings.availableModes,
768
+ );
769
+ if (mappedModeId) {
770
+ return mappedModeId;
771
+ }
772
+
773
+ if (opencodeSettings.selectedMode) {
774
+ if (
775
+ availableModes.some((mode) => mode.id === opencodeSettings.selectedMode)
776
+ ) {
777
+ return opencodeSettings.selectedMode;
778
+ }
779
+ }
780
+
781
+ return availableModes[0]?.id || null;
782
+ }
783
+
784
+ private async applySelectedMode(sessionId: string): Promise<void> {
785
+ if (!this.connection) {
786
+ return;
787
+ }
788
+
789
+ const selectedModeId = this.resolveSelectedModeId();
790
+ if (!selectedModeId || selectedModeId === this.currentSessionModeId) {
791
+ return;
792
+ }
793
+
794
+ const response = await this.connection.setConfigOption({
795
+ configId: 'mode',
796
+ sessionId,
797
+ type: 'select',
798
+ value: selectedModeId,
799
+ });
800
+ this.currentSessionModeId = selectedModeId;
801
+ await this.syncSessionModeState({
802
+ configOptions: response.configOptions,
803
+ });
804
+ }
805
+
806
+ private async applySelectedModel(
807
+ sessionId: string,
808
+ queryOptions?: ChatRuntimeQueryOptions,
809
+ ): Promise<void> {
810
+ if (!this.connection) {
811
+ return;
812
+ }
813
+
814
+ const selectedRawModelId = this.resolveSelectedRawModelId(queryOptions);
815
+ if (!selectedRawModelId || selectedRawModelId === this.currentSessionModelId) {
816
+ return;
817
+ }
818
+
819
+ const response = await this.connection.setConfigOption({
820
+ configId: 'model',
821
+ sessionId,
822
+ type: 'select',
823
+ value: selectedRawModelId,
824
+ });
825
+ this.currentSessionModelId = selectedRawModelId;
826
+ await this.syncSessionModelState({
827
+ configOptions: response.configOptions,
828
+ });
829
+ }
830
+
831
+ private resolveSelectedEffortValue(): string | null {
832
+ const providerSettings = this.getProviderSettings();
833
+ const selectedEffort = typeof providerSettings.effortLevel === 'string'
834
+ ? providerSettings.effortLevel.trim()
835
+ : '';
836
+ if (!selectedEffort || selectedEffort === OPENCODE_DEFAULT_THINKING_LEVEL) {
837
+ return null;
838
+ }
839
+
840
+ return this.currentSessionEffortValues.has(selectedEffort)
841
+ ? selectedEffort
842
+ : null;
843
+ }
844
+
845
+ private async applySelectedEffort(sessionId: string): Promise<void> {
846
+ if (!this.connection || !this.currentSessionEffortConfigId) {
847
+ return;
848
+ }
849
+
850
+ const selectedEffort = this.resolveSelectedEffortValue();
851
+ if (!selectedEffort || selectedEffort === this.currentSessionEffortValue) {
852
+ return;
853
+ }
854
+
855
+ const response = await this.connection.setConfigOption({
856
+ configId: this.currentSessionEffortConfigId,
857
+ sessionId,
858
+ type: 'select',
859
+ value: selectedEffort,
860
+ });
861
+ this.currentSessionEffortValue = selectedEffort;
862
+ await this.syncSessionModelState({
863
+ configOptions: response.configOptions,
864
+ });
865
+ }
866
+
867
+ private async syncSessionModelState(params: {
868
+ configOptions?: AcpSessionConfigOption[] | null;
869
+ models?: AcpSessionModelState | null;
870
+ }): Promise<void> {
871
+ const acpState = extractAcpSessionModelState(params);
872
+ const currentRawModelId = acpState.currentModelId ?? this.currentSessionModelId;
873
+ const discoveredModels = normalizeOpencodeDiscoveredModels(
874
+ acpState.availableModels.map((model) => ({
875
+ ...(model.description ? { description: model.description } : {}),
876
+ label: model.name,
877
+ rawId: model.id,
878
+ })),
879
+ );
880
+ if (currentRawModelId) {
881
+ this.currentSessionModelId = currentRawModelId;
882
+ }
883
+
884
+ const settingsBag = this.plugin.settings as unknown as Record<string, unknown>;
885
+ const currentSettings = getOpencodeProviderSettings(settingsBag);
886
+ const currentBaseRawModelId = currentRawModelId
887
+ ? resolveOpencodeBaseModelRawId(currentRawModelId, discoveredModels)
888
+ : null;
889
+ const thoughtLevelState = extractAcpSessionThoughtLevelState(params);
890
+ const currentThinkingOptions = normalizeOpencodeModelVariants(
891
+ thoughtLevelState.availableLevels.map((level) => ({
892
+ ...(level.description ? { description: level.description } : {}),
893
+ label: level.name,
894
+ value: level.id,
895
+ })),
896
+ );
897
+ const currentThinkingLevel = thoughtLevelState.currentLevel;
898
+ this.currentSessionEffortConfigId = currentThinkingOptions.length > 0
899
+ ? thoughtLevelState.configId
900
+ : null;
901
+ this.currentSessionEffortValue = currentThinkingOptions.length > 0
902
+ ? currentThinkingLevel
903
+ : null;
904
+ this.currentSessionEffortValues = new Set(currentThinkingOptions.map((option) => option.value));
905
+
906
+ const nextThinkingOptionsByModel = { ...currentSettings.thinkingOptionsByModel };
907
+ if (currentBaseRawModelId) {
908
+ if (currentThinkingOptions.length > 0) {
909
+ nextThinkingOptionsByModel[currentBaseRawModelId] = currentThinkingOptions;
910
+ } else {
911
+ delete nextThinkingOptionsByModel[currentBaseRawModelId];
912
+ }
913
+ }
914
+
915
+ const nextVisibleModels = currentSettings.visibleModels.length === 0 && currentBaseRawModelId
916
+ ? [currentBaseRawModelId]
917
+ : currentSettings.visibleModels;
918
+ const currentPreferredThinking = currentBaseRawModelId
919
+ ? currentSettings.preferredThinkingByModel[currentBaseRawModelId]
920
+ : '';
921
+ const shouldSeedCurrentThinking = currentBaseRawModelId
922
+ && currentThinkingLevel
923
+ && (
924
+ !currentPreferredThinking
925
+ || (
926
+ currentThinkingOptions.length > 0
927
+ && !this.currentSessionEffortValues.has(currentPreferredThinking)
928
+ )
929
+ );
930
+ const nextPreferredThinkingByModel = shouldSeedCurrentThinking && currentBaseRawModelId && currentThinkingLevel
931
+ ? {
932
+ ...currentSettings.preferredThinkingByModel,
933
+ [currentBaseRawModelId]: currentThinkingLevel,
934
+ }
935
+ : currentSettings.preferredThinkingByModel;
936
+ const shouldSeedVisibleModels = !sameStringList(currentSettings.visibleModels, nextVisibleModels);
937
+ const shouldSeedPreferredThinking = !sameStringMap(
938
+ currentSettings.preferredThinkingByModel,
939
+ nextPreferredThinkingByModel,
940
+ );
941
+ const shouldUpdateDiscoveredModels = discoveredModels.length > 0
942
+ && !sameDiscoveredModels(currentSettings.discoveredModels, discoveredModels);
943
+ const shouldUpdateThinkingOptions = !sameThinkingOptionsByModel(
944
+ currentSettings.thinkingOptionsByModel,
945
+ nextThinkingOptionsByModel,
946
+ );
947
+ const discoveryChanged = shouldUpdateDiscoveredModels
948
+ && updateOpencodeDiscoveryState(settingsBag, { discoveredModels });
949
+ let changed = shouldSeedVisibleModels || shouldSeedPreferredThinking;
950
+
951
+ if (currentBaseRawModelId) {
952
+ const seeded = this.seedActiveModelSelection(
953
+ settingsBag,
954
+ encodeOpencodeModelId(currentBaseRawModelId),
955
+ currentThinkingLevel,
956
+ );
957
+ changed = changed || seeded;
958
+ }
959
+
960
+ if (shouldUpdateThinkingOptions || shouldSeedPreferredThinking || shouldSeedVisibleModels) {
961
+ updateOpencodeProviderSettings(settingsBag, {
962
+ ...(shouldSeedPreferredThinking ? { preferredThinkingByModel: nextPreferredThinkingByModel } : {}),
963
+ ...(shouldUpdateThinkingOptions ? { thinkingOptionsByModel: nextThinkingOptionsByModel } : {}),
964
+ ...(shouldSeedVisibleModels ? { visibleModels: nextVisibleModels } : {}),
965
+ });
966
+ }
967
+
968
+ if (!changed && !discoveryChanged && !shouldUpdateThinkingOptions) {
969
+ return;
970
+ }
971
+
972
+ if (changed || shouldUpdateThinkingOptions) {
973
+ await this.plugin.saveSettings();
974
+ }
975
+ this.refreshModelSelectors();
976
+ }
977
+
978
+ private seedActiveModelSelection(
979
+ settingsBag: Record<string, unknown>,
980
+ modelSelection: string,
981
+ thinkingLevel: string | null,
982
+ ): boolean {
983
+ let changed = false;
984
+ const savedProviderModel = ensureProviderProjectionMap(settingsBag, 'savedProviderModel');
985
+ const savedModel = typeof savedProviderModel.opencode === 'string'
986
+ ? savedProviderModel.opencode
987
+ : '';
988
+ if (!savedModel || savedModel === OPENCODE_SYNTHETIC_MODEL_ID) {
989
+ savedProviderModel.opencode = modelSelection;
990
+ changed = true;
991
+ }
992
+
993
+ if (thinkingLevel) {
994
+ const savedProviderEffort = ensureProviderProjectionMap(settingsBag, 'savedProviderEffort');
995
+ const savedEffort = typeof savedProviderEffort.opencode === 'string'
996
+ ? savedProviderEffort.opencode.trim()
997
+ : '';
998
+ if (!savedEffort || savedEffort === OPENCODE_DEFAULT_THINKING_LEVEL) {
999
+ savedProviderEffort.opencode = thinkingLevel;
1000
+ changed = true;
1001
+ }
1002
+ }
1003
+
1004
+ if (ProviderRegistry.resolveSettingsProviderId(settingsBag) !== this.providerId) {
1005
+ return changed;
1006
+ }
1007
+
1008
+ const activeModel = typeof settingsBag.model === 'string' ? settingsBag.model : '';
1009
+ if (!activeModel || activeModel === OPENCODE_SYNTHETIC_MODEL_ID) {
1010
+ settingsBag.model = modelSelection;
1011
+ changed = true;
1012
+ }
1013
+ if (thinkingLevel) {
1014
+ const activeEffort = typeof settingsBag.effortLevel === 'string' ? settingsBag.effortLevel : '';
1015
+ if (!activeEffort || activeEffort === OPENCODE_DEFAULT_THINKING_LEVEL) {
1016
+ settingsBag.effortLevel = thinkingLevel;
1017
+ changed = true;
1018
+ }
1019
+ }
1020
+ return changed;
1021
+ }
1022
+
1023
+ private async syncSessionModeState(params: {
1024
+ configOptions?: AcpSessionConfigOption[] | null;
1025
+ currentModeId?: string | null;
1026
+ modes?: AcpSessionModeState | null;
1027
+ }): Promise<void> {
1028
+ const acpState = extractAcpSessionModeState(params);
1029
+ const availableModes = normalizeOpencodeAvailableModes(acpState.availableModes);
1030
+ const currentModeId = params.currentModeId ?? acpState.currentModeId;
1031
+ if (currentModeId) {
1032
+ this.currentSessionModeId = currentModeId;
1033
+ this.emitPermissionModeSync(currentModeId);
1034
+ }
1035
+
1036
+ const settingsBag = this.plugin.settings as unknown as Record<string, unknown>;
1037
+ const currentSettings = getOpencodeProviderSettings(settingsBag);
1038
+ const shouldSeedSelectedMode = typeof currentModeId === 'string'
1039
+ && !currentSettings.selectedMode
1040
+ && isManagedOpencodeModeId(currentModeId);
1041
+ const discoveryChanged = availableModes.length > 0
1042
+ && !sameModes(currentSettings.availableModes, availableModes)
1043
+ && updateOpencodeDiscoveryState(settingsBag, { availableModes });
1044
+
1045
+ if (!discoveryChanged && !shouldSeedSelectedMode) {
1046
+ return;
1047
+ }
1048
+
1049
+ if (shouldSeedSelectedMode && currentModeId) {
1050
+ updateOpencodeProviderSettings(settingsBag, { selectedMode: currentModeId });
1051
+ await this.plugin.saveSettings();
1052
+ }
1053
+ this.refreshModelSelectors();
1054
+ }
1055
+
1056
+ private refreshModelSelectors(): void {
1057
+ for (const view of this.plugin.getAllViews()) {
1058
+ view.refreshModelSelector();
1059
+ }
1060
+ }
1061
+
1062
+ private emitPermissionModeSync(modeId: string): void {
1063
+ const permissionMode = resolvePermissionModeForManagedOpencodeMode(modeId);
1064
+ if (!permissionMode || !this.permissionModeSyncCallback) {
1065
+ return;
1066
+ }
1067
+
1068
+ try {
1069
+ this.permissionModeSyncCallback(permissionMode);
1070
+ } catch {
1071
+ // Non-critical UI sync callback.
1072
+ }
1073
+ }
1074
+
1075
+ private async createSession(cwd: string): Promise<string | null> {
1076
+ if (!this.connection) {
1077
+ return null;
1078
+ }
1079
+
1080
+ try {
1081
+ this.setSupportedCommands([]);
1082
+ const response = await this.connection.newSession({
1083
+ cwd,
1084
+ mcpServers: [],
1085
+ });
1086
+ this.loadedSessionId = response.sessionId;
1087
+ this.sessionId = response.sessionId;
1088
+ this.sessionCwds.set(response.sessionId, cwd);
1089
+ await this.syncSessionModelState({
1090
+ configOptions: response.configOptions ?? null,
1091
+ models: response.models ?? null,
1092
+ });
1093
+ await this.syncSessionModeState({
1094
+ configOptions: response.configOptions ?? null,
1095
+ modes: response.modes ?? null,
1096
+ });
1097
+ return response.sessionId;
1098
+ } catch {
1099
+ return null;
1100
+ }
1101
+ }
1102
+
1103
+ private async loadSession(sessionId: string, cwd: string): Promise<boolean> {
1104
+ if (!this.connection) {
1105
+ return false;
1106
+ }
1107
+
1108
+ try {
1109
+ this.setSupportedCommands([]);
1110
+ const response = await this.connection.loadSession({
1111
+ cwd,
1112
+ mcpServers: [],
1113
+ sessionId,
1114
+ });
1115
+ this.sessionInvalidated = false;
1116
+ this.loadedSessionId = response.sessionId;
1117
+ this.sessionId = response.sessionId;
1118
+ this.sessionCwds.set(response.sessionId, cwd);
1119
+ await this.syncSessionModelState({
1120
+ configOptions: response.configOptions ?? null,
1121
+ models: response.models ?? null,
1122
+ });
1123
+ await this.syncSessionModeState({
1124
+ configOptions: response.configOptions ?? null,
1125
+ modes: response.modes ?? null,
1126
+ });
1127
+ return true;
1128
+ } catch {
1129
+ return false;
1130
+ }
1131
+ }
1132
+
1133
+ private async handleSessionNotification(
1134
+ notification: AcpSessionNotification,
1135
+ ): Promise<void> {
1136
+ if (notification.sessionId !== this.sessionId) {
1137
+ return;
1138
+ }
1139
+
1140
+ const normalized = this.sessionUpdateNormalizer.normalize(notification.update);
1141
+ if (normalized.type === 'config_options') {
1142
+ await this.syncSessionModelState({
1143
+ configOptions: normalized.configOptions,
1144
+ });
1145
+ await this.syncSessionModeState({
1146
+ configOptions: normalized.configOptions,
1147
+ });
1148
+ return;
1149
+ }
1150
+
1151
+ if (normalized.type === 'current_mode') {
1152
+ await this.syncSessionModeState({
1153
+ currentModeId: normalized.currentModeId,
1154
+ });
1155
+ return;
1156
+ }
1157
+
1158
+ if (normalized.type === 'commands') {
1159
+ this.setSupportedCommands(normalized.commands);
1160
+ return;
1161
+ }
1162
+
1163
+ if (!this.activeTurn || this.activeTurn.sessionId !== notification.sessionId) {
1164
+ return;
1165
+ }
1166
+
1167
+ switch (normalized.type) {
1168
+ case 'message_chunk': {
1169
+ if (normalized.role === 'assistant' && normalized.messageId) {
1170
+ this.currentTurnMetadata.assistantMessageId = normalized.messageId;
1171
+ }
1172
+ if (normalized.role === 'user' && normalized.messageId) {
1173
+ this.currentTurnMetadata.userMessageId = normalized.messageId;
1174
+ }
1175
+ for (const chunk of normalized.streamChunks) {
1176
+ this.activeTurn.queue.push(chunk);
1177
+ }
1178
+ return;
1179
+ }
1180
+ case 'tool_call':
1181
+ case 'tool_call_update': {
1182
+ const streamChunks = normalized.type === 'tool_call'
1183
+ ? this.toolStreamAdapter.normalizeToolCall(normalized.toolCall, normalized.streamChunks)
1184
+ : this.toolStreamAdapter.normalizeToolCallUpdate(normalized.toolCallUpdate, normalized.streamChunks);
1185
+
1186
+ for (const chunk of streamChunks) {
1187
+ this.activeTurn.queue.push(chunk);
1188
+ }
1189
+ return;
1190
+ }
1191
+ case 'usage': {
1192
+ this.contextUsage = normalized.usage;
1193
+ const usage = buildAcpUsageInfo({
1194
+ contextWindow: normalized.usage,
1195
+ model: this.getActiveDisplayModel(),
1196
+ promptUsage: this.promptUsage,
1197
+ });
1198
+ if (usage) {
1199
+ this.activeTurn.queue.push({
1200
+ sessionId: notification.sessionId,
1201
+ type: 'usage',
1202
+ usage,
1203
+ });
1204
+ }
1205
+ return;
1206
+ }
1207
+ default:
1208
+ return;
1209
+ }
1210
+ }
1211
+
1212
+ private async handlePermissionRequest(
1213
+ request: AcpRequestPermissionRequest,
1214
+ ): Promise<AcpRequestPermissionResponse> {
1215
+ if (!this.approvalCallback) {
1216
+ return { outcome: { outcome: 'cancelled' } };
1217
+ }
1218
+
1219
+ const input = normalizeApprovalInput(request.toolCall.rawInput);
1220
+ const presentation = buildOpencodePermissionPresentation(request.toolCall.title, input, request.toolCall.locations);
1221
+ const decision = await this.approvalCallback(
1222
+ presentation.toolName,
1223
+ input,
1224
+ presentation.description,
1225
+ {
1226
+ ...(presentation.blockedPath ? { blockedPath: presentation.blockedPath } : {}),
1227
+ ...(presentation.decisionReason ? { decisionReason: presentation.decisionReason } : {}),
1228
+ decisionOptions: buildAcpApprovalDecisionOptions(request.options),
1229
+ },
1230
+ );
1231
+
1232
+ return mapApprovalDecision(decision, request.options);
1233
+ }
1234
+
1235
+ private setSupportedCommands(commands: SlashCommand[]): void {
1236
+ this.supportedCommands = commands.map((command) => ({ ...command }));
1237
+
1238
+ const waiters = this.supportedCommandWaiters.splice(0);
1239
+ for (const waiter of waiters) {
1240
+ waiter(this.supportedCommands);
1241
+ }
1242
+ }
1243
+
1244
+ private waitForSupportedCommands(timeoutMs = 250): Promise<SlashCommand[]> {
1245
+ if (this.supportedCommands.length > 0) {
1246
+ return Promise.resolve([...this.supportedCommands]);
1247
+ }
1248
+
1249
+ return new Promise<SlashCommand[]>((resolve) => {
1250
+ const waiter = (commands: SlashCommand[]) => {
1251
+ window.clearTimeout(timeoutId);
1252
+ resolve([...commands]);
1253
+ };
1254
+ const timeoutId = window.setTimeout(() => {
1255
+ const index = this.supportedCommandWaiters.indexOf(waiter);
1256
+ if (index >= 0) {
1257
+ this.supportedCommandWaiters.splice(index, 1);
1258
+ }
1259
+ resolve([...this.supportedCommands]);
1260
+ }, timeoutMs);
1261
+
1262
+ this.supportedCommandWaiters.push(waiter);
1263
+ });
1264
+ }
1265
+
1266
+ private async readTextFile(
1267
+ request: AcpReadTextFileRequest,
1268
+ ): Promise<{ content: string }> {
1269
+ const resolvedPath = this.resolveSessionPath(request.sessionId, request.path);
1270
+ const content = await fs.readFile(resolvedPath, 'utf-8');
1271
+
1272
+ if (request.line === undefined && request.limit === undefined) {
1273
+ return { content };
1274
+ }
1275
+
1276
+ const lines = content.split(/\r?\n/);
1277
+ const startIndex = Math.max(0, (request.line ?? 1) - 1);
1278
+ const endIndex = request.limit
1279
+ ? startIndex + Math.max(0, request.limit)
1280
+ : lines.length;
1281
+
1282
+ return {
1283
+ content: lines.slice(startIndex, endIndex).join('\n'),
1284
+ };
1285
+ }
1286
+
1287
+ private async writeTextFile(
1288
+ request: AcpWriteTextFileRequest,
1289
+ ): Promise<Record<string, never>> {
1290
+ const resolvedPath = this.resolveSessionPath(request.sessionId, request.path);
1291
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
1292
+ await fs.writeFile(resolvedPath, request.content, 'utf-8');
1293
+ return {};
1294
+ }
1295
+
1296
+ private resolveSessionPath(sessionId: string, rawPath: string): string {
1297
+ if (path.isAbsolute(rawPath)) {
1298
+ return rawPath;
1299
+ }
1300
+
1301
+ const cwd = this.sessionCwds.get(sessionId)
1302
+ ?? getVaultPath(this.plugin.app)
1303
+ ?? process.cwd();
1304
+ return path.resolve(cwd, rawPath);
1305
+ }
1306
+
1307
+ private formatRuntimeError(error: unknown): string {
1308
+ const baseMessage = error instanceof Error ? error.message : 'OpenCode request failed';
1309
+ const stderr = this.process?.getStderrSnapshot();
1310
+ return stderr ? `${baseMessage}\n\n${stderr}` : baseMessage;
1311
+ }
1312
+
1313
+ private clearActiveSession(): void {
1314
+ this.currentDatabasePath = null;
1315
+ this.sessionId = null;
1316
+ this.loadedSessionId = null;
1317
+ this.currentSessionModelId = null;
1318
+ this.currentSessionModeId = null;
1319
+ this.setSupportedCommands([]);
1320
+ }
1321
+ }
1322
+
1323
+ function normalizeApprovalInput(rawInput: unknown): Record<string, unknown> {
1324
+ if (rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput)) {
1325
+ return rawInput as Record<string, unknown>;
1326
+ }
1327
+ if (rawInput === undefined) {
1328
+ return {};
1329
+ }
1330
+ return { value: rawInput };
1331
+ }
1332
+
1333
+ function buildOpencodePermissionPresentation(
1334
+ rawTitle: string | null | undefined,
1335
+ input: Record<string, unknown>,
1336
+ locations: Array<{ path: string }> | null | undefined,
1337
+ ): {
1338
+ blockedPath?: string;
1339
+ decisionReason?: string;
1340
+ description: string;
1341
+ toolName: string;
1342
+ } {
1343
+ const permissionId = normalizePermissionId(rawTitle);
1344
+ const blockedPath = extractPermissionPath(input, locations);
1345
+
1346
+ switch (permissionId) {
1347
+ case 'bash':
1348
+ return {
1349
+ decisionReason: 'Command execution permission required',
1350
+ description: 'OpenCode wants to run a shell command.',
1351
+ toolName: 'bash',
1352
+ };
1353
+ case 'codesearch':
1354
+ return {
1355
+ description: 'OpenCode wants to search indexed code outside the active buffer.',
1356
+ toolName: 'codesearch',
1357
+ };
1358
+ case 'doom_loop': {
1359
+ const repeatedTool = typeof input.tool === 'string' ? input.tool.trim() : '';
1360
+ return {
1361
+ decisionReason: 'OpenCode detected repeated identical tool calls',
1362
+ description: repeatedTool
1363
+ ? `Allow another repeated \`${repeatedTool}\` call.`
1364
+ : 'Allow another repeated tool call.',
1365
+ toolName: 'Doom Loop Guard',
1366
+ };
1367
+ }
1368
+ case 'edit':
1369
+ return {
1370
+ ...(blockedPath ? { blockedPath } : {}),
1371
+ decisionReason: 'File write permission required',
1372
+ description: blockedPath
1373
+ ? 'OpenCode wants to modify this file.'
1374
+ : 'OpenCode wants to apply file changes.',
1375
+ toolName: 'edit',
1376
+ };
1377
+ case 'external_directory':
1378
+ return {
1379
+ ...(blockedPath ? { blockedPath } : {}),
1380
+ decisionReason: 'Path is outside the session working directory',
1381
+ description: blockedPath
1382
+ ? 'OpenCode wants to access a path outside the working directory.'
1383
+ : 'OpenCode wants to access files outside the working directory.',
1384
+ toolName: 'External Directory',
1385
+ };
1386
+ case 'glob':
1387
+ return {
1388
+ description: 'OpenCode wants to scan file paths with a glob pattern.',
1389
+ toolName: 'glob',
1390
+ };
1391
+ case 'grep':
1392
+ return {
1393
+ description: 'OpenCode wants to search file contents with a pattern.',
1394
+ toolName: 'grep',
1395
+ };
1396
+ case 'lsp':
1397
+ return {
1398
+ description: 'OpenCode wants to query language server data.',
1399
+ toolName: 'lsp',
1400
+ };
1401
+ case 'plan_enter':
1402
+ return {
1403
+ description: 'OpenCode wants to switch this session into planning mode.',
1404
+ toolName: 'Enter Plan Mode',
1405
+ };
1406
+ case 'plan_exit':
1407
+ return {
1408
+ description: 'OpenCode wants to leave planning mode and resume implementation.',
1409
+ toolName: 'Exit Plan Mode',
1410
+ };
1411
+ case 'question':
1412
+ return {
1413
+ description: 'OpenCode wants to ask you a direct question before continuing.',
1414
+ toolName: 'Ask Question',
1415
+ };
1416
+ case 'read':
1417
+ return {
1418
+ ...(blockedPath ? { blockedPath } : {}),
1419
+ description: blockedPath
1420
+ ? 'OpenCode wants to read this path.'
1421
+ : 'OpenCode wants to read project files.',
1422
+ toolName: 'read',
1423
+ };
1424
+ case 'skill':
1425
+ return {
1426
+ description: 'OpenCode wants to load a skill into the current session.',
1427
+ toolName: 'skill',
1428
+ };
1429
+ case 'todowrite':
1430
+ return {
1431
+ description: 'OpenCode wants to update the shared task list.',
1432
+ toolName: 'todowrite',
1433
+ };
1434
+ case 'webfetch':
1435
+ return {
1436
+ description: 'OpenCode wants to fetch content from a URL.',
1437
+ toolName: 'webfetch',
1438
+ };
1439
+ case 'websearch':
1440
+ return {
1441
+ description: 'OpenCode wants to search the web.',
1442
+ toolName: 'websearch',
1443
+ };
1444
+ case 'workflow_tool_approval': {
1445
+ const summary = summarizeWorkflowTools(input);
1446
+ return {
1447
+ decisionReason: 'Session-level workflow approval requested',
1448
+ description: summary
1449
+ ? `Pre-approve workflow tools for this session: ${summary}.`
1450
+ : 'Pre-approve workflow tools for this session.',
1451
+ toolName: 'Workflow Approval',
1452
+ };
1453
+ }
1454
+ default:
1455
+ return {
1456
+ ...(blockedPath ? { blockedPath } : {}),
1457
+ description: blockedPath
1458
+ ? `OpenCode wants permission to use ${formatPermissionLabel(permissionId)} on this path.`
1459
+ : `OpenCode wants permission to use ${formatPermissionLabel(permissionId)}.`,
1460
+ toolName: formatPermissionLabel(permissionId),
1461
+ };
1462
+ }
1463
+ }
1464
+
1465
+ function normalizePermissionId(value: string | null | undefined): string {
1466
+ return value?.trim().toLowerCase() || 'tool';
1467
+ }
1468
+
1469
+ function extractPermissionPath(
1470
+ input: Record<string, unknown>,
1471
+ locations: Array<{ path: string }> | null | undefined,
1472
+ ): string | undefined {
1473
+ const candidateKeys = ['filepath', 'filePath', 'path', 'parentDir'];
1474
+ for (const key of candidateKeys) {
1475
+ const value = input[key];
1476
+ if (typeof value === 'string' && value.trim()) {
1477
+ return value.trim();
1478
+ }
1479
+ }
1480
+
1481
+ const locationPath = locations?.find((location) => location.path.trim())?.path;
1482
+ return locationPath?.trim() || undefined;
1483
+ }
1484
+
1485
+ function summarizeWorkflowTools(input: Record<string, unknown>): string {
1486
+ const tools = Array.isArray(input.tools) ? input.tools : [];
1487
+ const names = tools.flatMap((tool) => {
1488
+ if (!tool || typeof tool !== 'object' || Array.isArray(tool)) {
1489
+ return [];
1490
+ }
1491
+
1492
+ const entry = tool as Record<string, unknown>;
1493
+ const name = typeof entry.name === 'string' ? entry.name.trim() : '';
1494
+ if (!name) {
1495
+ return [];
1496
+ }
1497
+
1498
+ let title = '';
1499
+ if (typeof entry.args === 'string') {
1500
+ try {
1501
+ const parsedArgs = JSON.parse(entry.args) as Record<string, unknown>;
1502
+ title = typeof parsedArgs.title === 'string'
1503
+ ? parsedArgs.title.trim()
1504
+ : typeof parsedArgs.name === 'string'
1505
+ ? parsedArgs.name.trim()
1506
+ : '';
1507
+ } catch {
1508
+ title = '';
1509
+ }
1510
+ }
1511
+
1512
+ return [title ? `${name}: ${title}` : name];
1513
+ });
1514
+
1515
+ if (names.length === 0) {
1516
+ return '';
1517
+ }
1518
+
1519
+ if (names.length <= 3) {
1520
+ return names.join(', ');
1521
+ }
1522
+
1523
+ return `${names.slice(0, 3).join(', ')} +${names.length - 3} more`;
1524
+ }
1525
+
1526
+ function formatPermissionLabel(permissionId: string): string {
1527
+ return permissionId
1528
+ .split(/[_\s]+/)
1529
+ .filter(Boolean)
1530
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
1531
+ .join(' ');
1532
+ }
1533
+
1534
+ function mapApprovalDecision(
1535
+ decision: ApprovalDecision,
1536
+ options: readonly {
1537
+ kind: 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
1538
+ optionId: string;
1539
+ }[],
1540
+ ): AcpRequestPermissionResponse {
1541
+ if (decision === 'allow') {
1542
+ return selectPermissionOption(options, ['allow_once', 'allow_always']);
1543
+ }
1544
+
1545
+ if (decision === 'allow-always') {
1546
+ return selectPermissionOption(options, ['allow_always', 'allow_once']);
1547
+ }
1548
+
1549
+ if (decision === 'deny') {
1550
+ return selectPermissionOption(options, ['reject_once', 'reject_always']);
1551
+ }
1552
+
1553
+ if (typeof decision === 'object' && decision.type === 'select-option') {
1554
+ return {
1555
+ outcome: {
1556
+ optionId: decision.value,
1557
+ outcome: 'selected',
1558
+ },
1559
+ };
1560
+ }
1561
+
1562
+ return { outcome: { outcome: 'cancelled' } };
1563
+ }
1564
+
1565
+ function buildAcpApprovalDecisionOptions(
1566
+ options: readonly {
1567
+ kind: 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
1568
+ name: string;
1569
+ optionId: string;
1570
+ }[],
1571
+ ): ApprovalDecisionOption[] {
1572
+ return options.map((option) => ({
1573
+ ...(option.kind === 'allow_once'
1574
+ ? { decision: 'allow' as const }
1575
+ : option.kind === 'allow_always'
1576
+ ? { decision: 'allow-always' as const }
1577
+ : {}),
1578
+ label: option.name,
1579
+ value: option.optionId,
1580
+ }));
1581
+ }
1582
+
1583
+ function selectPermissionOption(
1584
+ options: readonly {
1585
+ kind: 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
1586
+ optionId: string;
1587
+ }[],
1588
+ preferredKinds: readonly ('allow_once' | 'allow_always' | 'reject_once' | 'reject_always')[],
1589
+ ): AcpRequestPermissionResponse {
1590
+ for (const kind of preferredKinds) {
1591
+ const option = options.find((entry) => entry.kind === kind);
1592
+ if (option) {
1593
+ return {
1594
+ outcome: {
1595
+ optionId: option.optionId,
1596
+ outcome: 'selected',
1597
+ },
1598
+ };
1599
+ }
1600
+ }
1601
+
1602
+ return { outcome: { outcome: 'cancelled' } };
1603
+ }