@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,1560 @@
1
+ import { TFile } from 'obsidian';
2
+
3
+ import { ProviderSettingsCoordinator } from '../../../core/providers/ProviderSettingsCoordinator';
4
+ import {
5
+ DEFAULT_CHAT_PROVIDER_ID,
6
+ type ProviderId,
7
+ type ProviderSubagentLifecycleAdapter,
8
+ } from '../../../core/providers/types';
9
+ import type { ChatRuntime } from '../../../core/runtime/ChatRuntime';
10
+ import { parseTodoInput } from '../../../core/tools/todo';
11
+ import { extractResolvedAnswers, extractResolvedAnswersFromResultText } from '../../../core/tools/toolInput';
12
+ import {
13
+ isEditTool,
14
+ isSubagentToolName,
15
+ isWriteEditTool,
16
+ skipsBlockedDetection,
17
+ TOOL_AGENT_OUTPUT,
18
+ TOOL_APPLY_PATCH,
19
+ TOOL_ASK_USER_QUESTION,
20
+ TOOL_TASK,
21
+ TOOL_TODO_WRITE,
22
+ TOOL_WRITE,
23
+ } from '../../../core/tools/toolNames';
24
+ import { extractToolResultContent } from '../../../core/tools/toolResultContent';
25
+ import type { ChatMessage, StreamChunk, SubagentInfo, ToolCallInfo } from '../../../core/types';
26
+ import type { SDKToolUseResult } from '../../../core/types/diff';
27
+ import type ClaudianPlugin from '../../../main';
28
+ import {
29
+ cancelScheduledAnimationFrame,
30
+ scheduleAnimationFrame,
31
+ type ScheduledAnimationFrame,
32
+ } from '../../../utils/animationFrame';
33
+ import { formatDurationMmSs } from '../../../utils/date';
34
+ import { extractDiffData } from '../../../utils/diff';
35
+ import { hasStreamingMathDelimiters } from '../../../utils/markdownMath';
36
+ import { getVaultPath, normalizePathForVault } from '../../../utils/path';
37
+ import { FLAVOR_TEXTS } from '../constants';
38
+ import type { MessageRenderer, RenderContentOptions } from '../rendering/MessageRenderer';
39
+ import { resolveSubagentLifecycleAdapter } from '../rendering/subagentLifecycleResolution';
40
+ import {
41
+ createSubagentBlock,
42
+ finalizeSubagentBlock,
43
+ type SubagentState,
44
+ } from '../rendering/SubagentRenderer';
45
+ import {
46
+ createThinkingBlock,
47
+ finalizeThinkingBlock,
48
+ } from '../rendering/ThinkingBlockRenderer';
49
+ import {
50
+ getToolName,
51
+ getToolSummary,
52
+ isBlockedToolResult,
53
+ renderToolCall,
54
+ updateToolCallResult,
55
+ } from '../rendering/ToolCallRenderer';
56
+ import {
57
+ createWriteEditBlock,
58
+ finalizeWriteEditBlock,
59
+ updateWriteEditWithDiff,
60
+ } from '../rendering/WriteEditRenderer';
61
+ import type { SubagentManager } from '../services/SubagentManager';
62
+ import type { ChatState } from '../state/ChatState';
63
+ import type { FileContextManager } from '../ui/FileContext';
64
+
65
+ export interface StreamControllerDeps {
66
+ plugin: ClaudianPlugin;
67
+ state: ChatState;
68
+ renderer: MessageRenderer;
69
+ subagentManager: SubagentManager;
70
+ getMessagesEl: () => HTMLElement;
71
+ getFileContextManager: () => FileContextManager | null;
72
+ updateQueueIndicator: () => void;
73
+ /** Get the agent service from the tab. */
74
+ getAgentService?: () => ChatRuntime | null;
75
+ }
76
+
77
+ export class StreamController {
78
+ private static readonly ASYNC_SUBAGENT_RESULT_RETRY_DELAYS_MS = [200, 600, 1500] as const;
79
+
80
+ private deps: StreamControllerDeps;
81
+ private pendingTextRenderFrame: ScheduledAnimationFrame | null = null;
82
+ private pendingTextRenderPromise: Promise<void> | null = null;
83
+ private resolvePendingTextRender: (() => void) | null = null;
84
+ private isTextRenderRunning = false;
85
+ private pendingThinkingRenderFrame: ScheduledAnimationFrame | null = null;
86
+ private pendingThinkingRenderPromise: Promise<void> | null = null;
87
+ private resolvePendingThinkingRender: (() => void) | null = null;
88
+ private isThinkingRenderRunning = false;
89
+ private pendingToolOutputFrames = new Map<string, ScheduledAnimationFrame>();
90
+ private pendingScrollFrame: ScheduledAnimationFrame | null = null;
91
+
92
+ // Provider lifecycle agent tracking (spawn → wait/close lifecycle)
93
+ private lifecycleSubagentStates = new Map<string, SubagentState>(); // spawn callId → SubagentState
94
+ private lifecycleAgentIdToSpawnId = new Map<string, string>(); // agentId → spawn callId
95
+
96
+ constructor(deps: StreamControllerDeps) {
97
+ this.deps = deps;
98
+ }
99
+
100
+ private getActiveProviderId(): ProviderId {
101
+ return this.deps.getAgentService?.()?.providerId ?? DEFAULT_CHAT_PROVIDER_ID;
102
+ }
103
+
104
+ private getSubagentLifecycleAdapter(toolName?: string): ProviderSubagentLifecycleAdapter | null {
105
+ return resolveSubagentLifecycleAdapter(this.getActiveProviderId(), toolName);
106
+ }
107
+
108
+ private normalizeToolResultContent(content: unknown): string {
109
+ return extractToolResultContent(content, { fallbackIndent: 2 });
110
+ }
111
+
112
+ // ============================================
113
+ // Stream Chunk Handling
114
+ // ============================================
115
+
116
+ async handleStreamChunk(chunk: StreamChunk, msg: ChatMessage): Promise<void> {
117
+ const { state } = this.deps;
118
+
119
+ switch (chunk.type) {
120
+ case 'thinking':
121
+ // Flush pending tools before rendering new content type
122
+ this.flushPendingTools();
123
+ if (state.currentTextEl) {
124
+ await this.finalizeCurrentTextBlock(msg);
125
+ }
126
+ await this.appendThinking(chunk.content);
127
+ break;
128
+
129
+ case 'text':
130
+ // Flush pending tools before rendering new content type
131
+ this.flushPendingTools();
132
+ if (state.currentThinkingState) {
133
+ await this.finalizeCurrentThinkingBlock(msg);
134
+ }
135
+ msg.content += chunk.content;
136
+ await this.appendText(chunk.content);
137
+ break;
138
+
139
+ case 'tool_use': {
140
+ if (state.currentThinkingState) {
141
+ await this.finalizeCurrentThinkingBlock(msg);
142
+ }
143
+ await this.finalizeCurrentTextBlock(msg);
144
+
145
+ if (isSubagentToolName(chunk.name)) {
146
+ // Flush pending tools before Agent
147
+ this.flushPendingTools();
148
+ this.handleTaskToolUseViaManager(chunk, msg);
149
+ break;
150
+ }
151
+
152
+ if (chunk.name === TOOL_AGENT_OUTPUT) {
153
+ this.handleAgentOutputToolUse(chunk, msg);
154
+ break;
155
+ }
156
+
157
+ const subagentLifecycleAdapter = this.getSubagentLifecycleAdapter(chunk.name);
158
+ if (subagentLifecycleAdapter?.isSpawnTool(chunk.name)) {
159
+ this.handleProviderSubagentSpawn(chunk, msg, subagentLifecycleAdapter);
160
+ break;
161
+ }
162
+ if (subagentLifecycleAdapter?.isHiddenTool(chunk.name)) {
163
+ this.handleProviderHiddenSubagentTool(chunk, msg);
164
+ break;
165
+ }
166
+
167
+ this.handleRegularToolUse(chunk, msg);
168
+ break;
169
+ }
170
+
171
+ case 'tool_result': {
172
+ await this.handleToolResult(chunk, msg);
173
+ break;
174
+ }
175
+
176
+ case 'subagent_tool_use':
177
+ case 'subagent_tool_result':
178
+ await this.handleSubagentChunk(chunk, msg);
179
+ break;
180
+
181
+ case 'async_subagent_result':
182
+ await this.handleAsyncSubagentResult(chunk);
183
+ break;
184
+
185
+ case 'tool_output':
186
+ this.handleToolOutput(chunk, msg);
187
+ break;
188
+
189
+ case 'notice':
190
+ this.flushPendingTools();
191
+ await this.appendText(`\n\n⚠️ **${chunk.level === 'warning' ? 'Blocked' : 'Notice'}:** ${chunk.content}`);
192
+ break;
193
+
194
+ case 'error':
195
+ // Flush pending tools before rendering error message
196
+ this.flushPendingTools();
197
+ await this.appendText(`\n\n❌ **Error:** ${chunk.content}`);
198
+ break;
199
+
200
+ case 'done':
201
+ // Flush any remaining pending tools
202
+ this.flushPendingTools();
203
+ break;
204
+
205
+ case 'context_compacted': {
206
+ this.flushPendingTools();
207
+ if (state.currentThinkingState) {
208
+ await this.finalizeCurrentThinkingBlock(msg);
209
+ }
210
+ await this.finalizeCurrentTextBlock(msg);
211
+ msg.contentBlocks = msg.contentBlocks || [];
212
+ msg.contentBlocks.push({ type: 'context_compacted' });
213
+ this.renderCompactBoundary();
214
+ break;
215
+ }
216
+
217
+ case 'usage': {
218
+ // Skip usage updates from other sessions or when flagged (during session reset)
219
+ const currentSessionId = this.deps.getAgentService?.()?.getSessionId() ?? null;
220
+ const chunkSessionId = chunk.sessionId ?? null;
221
+ if (
222
+ (chunkSessionId && currentSessionId && chunkSessionId !== currentSessionId) ||
223
+ (chunkSessionId && !currentSessionId)
224
+ ) {
225
+ break;
226
+ }
227
+ // Skip usage updates when subagents ran (SDK reports cumulative usage including subagents)
228
+ if (this.deps.subagentManager.subagentsSpawnedThisStream > 0) {
229
+ break;
230
+ }
231
+ if (!state.ignoreUsageUpdates) {
232
+ const activeModel = this.getActiveProviderModel();
233
+ state.usage = activeModel && !chunk.usage.model
234
+ ? { ...chunk.usage, model: activeModel }
235
+ : chunk.usage;
236
+ }
237
+ break;
238
+ }
239
+
240
+ default:
241
+ break;
242
+ }
243
+
244
+ this.scrollToBottom();
245
+ }
246
+
247
+ // ============================================
248
+ // Tool Use Handling
249
+ // ============================================
250
+
251
+ /**
252
+ * Handles regular tool_use chunks by buffering them.
253
+ * Tools are rendered when flushPendingTools is called (on next content type or tool_result).
254
+ */
255
+ private handleRegularToolUse(
256
+ chunk: { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> },
257
+ msg: ChatMessage
258
+ ): void {
259
+ const { state } = this.deps;
260
+
261
+ // Check if this is an update to an existing tool call
262
+ const existingToolCall = msg.toolCalls?.find(tc => tc.id === chunk.id);
263
+ if (existingToolCall) {
264
+ const newInput = chunk.input || {};
265
+ if (Object.keys(newInput).length > 0) {
266
+ existingToolCall.input = { ...existingToolCall.input, ...newInput };
267
+
268
+ // Re-parse TodoWrite on input updates (streaming may complete the input)
269
+ if (existingToolCall.name === TOOL_TODO_WRITE) {
270
+ const todos = parseTodoInput(existingToolCall.input);
271
+ if (todos) {
272
+ this.deps.state.currentTodos = todos;
273
+ }
274
+ }
275
+
276
+ // Capture plan file path on input updates (file_path may arrive in a later chunk)
277
+ if (existingToolCall.name === TOOL_WRITE) {
278
+ this.capturePlanFilePath(existingToolCall.input);
279
+ }
280
+
281
+ // If already rendered, update the header name + summary
282
+ const toolEl = state.toolCallElements.get(chunk.id);
283
+ if (toolEl) {
284
+ const nameEl = toolEl.querySelector('.claudian-tool-name')
285
+ ?? toolEl.querySelector('.claudian-write-edit-name');
286
+ if (nameEl) {
287
+ nameEl.setText(getToolName(existingToolCall.name, existingToolCall.input));
288
+ }
289
+ const summaryEl = toolEl.querySelector('.claudian-tool-summary')
290
+ ?? toolEl.querySelector('.claudian-write-edit-summary');
291
+ if (summaryEl) {
292
+ summaryEl.setText(getToolSummary(existingToolCall.name, existingToolCall.input));
293
+ }
294
+ }
295
+ // If still pending, the updated input is already in the toolCall object
296
+ }
297
+ return;
298
+ }
299
+
300
+ // Create new tool call
301
+ const toolCall: ToolCallInfo = {
302
+ id: chunk.id,
303
+ name: chunk.name,
304
+ input: chunk.input,
305
+ status: 'running',
306
+ isExpanded: false,
307
+ };
308
+ msg.toolCalls = msg.toolCalls || [];
309
+ msg.toolCalls.push(toolCall);
310
+
311
+ // Add to contentBlocks for ordering
312
+ msg.contentBlocks = msg.contentBlocks || [];
313
+ msg.contentBlocks.push({ type: 'tool_use', toolId: chunk.id });
314
+
315
+ // TodoWrite: update panel state immediately (side effect), but still buffer render
316
+ if (chunk.name === TOOL_TODO_WRITE) {
317
+ const todos = parseTodoInput(chunk.input);
318
+ if (todos) {
319
+ this.deps.state.currentTodos = todos;
320
+ }
321
+ }
322
+
323
+ // Track Write to provider plan directory for plan mode (used by approve-new-session)
324
+ if (chunk.name === TOOL_WRITE) {
325
+ this.capturePlanFilePath(chunk.input);
326
+ }
327
+
328
+ // Buffer the tool call instead of rendering immediately
329
+ if (state.currentContentEl) {
330
+ state.pendingTools.set(chunk.id, {
331
+ toolCall,
332
+ parentEl: state.currentContentEl,
333
+ });
334
+ this.showThinkingIndicator();
335
+ }
336
+ }
337
+
338
+ private getActiveProviderModel(): string | undefined {
339
+ const providerId = this.deps.getAgentService?.()?.providerId;
340
+ if (!providerId) {
341
+ return undefined;
342
+ }
343
+
344
+ const settings = ProviderSettingsCoordinator.getProviderSettingsSnapshot(
345
+ this.deps.plugin.settings,
346
+ providerId,
347
+ );
348
+ return typeof settings.model === 'string' ? settings.model : undefined;
349
+ }
350
+
351
+ private shouldDeferMathRendering(): boolean {
352
+ return this.deps.plugin.settings.deferMathRenderingDuringStreaming !== false;
353
+ }
354
+
355
+ private shouldExpandFileEditsByDefault(): boolean {
356
+ return this.deps.plugin.settings.expandFileEditsByDefault === true;
357
+ }
358
+
359
+ private getStreamingRenderOptions(content: string): RenderContentOptions | undefined {
360
+ return this.shouldDeferMathRendering() && hasStreamingMathDelimiters(content)
361
+ ? { deferMath: true }
362
+ : undefined;
363
+ }
364
+
365
+ private capturePlanFilePath(input: Record<string, unknown>): void {
366
+ const filePath = input.file_path as string | undefined;
367
+ if (!filePath) return;
368
+
369
+ const planPathPrefix = this.deps.getAgentService?.()?.getCapabilities().planPathPrefix;
370
+ if (planPathPrefix && filePath.replace(/\\/g, '/').includes(planPathPrefix)) {
371
+ this.deps.state.planFilePath = filePath;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Flushes all pending tool calls by rendering them.
377
+ * Called when a different content type arrives or stream ends.
378
+ */
379
+ private flushPendingTools(): void {
380
+ const { state } = this.deps;
381
+
382
+ if (state.pendingTools.size === 0) {
383
+ return;
384
+ }
385
+
386
+ // Render pending tools in order (Map preserves insertion order)
387
+ for (const toolId of state.pendingTools.keys()) {
388
+ this.renderPendingTool(toolId);
389
+ }
390
+
391
+ state.pendingTools.clear();
392
+ }
393
+
394
+ /**
395
+ * Renders a single pending tool call and moves it from pending to rendered state.
396
+ */
397
+ private renderPendingTool(toolId: string): void {
398
+ const { state } = this.deps;
399
+ const pending = state.pendingTools.get(toolId);
400
+ if (!pending) return;
401
+
402
+ const { toolCall, parentEl } = pending;
403
+ if (!parentEl) return;
404
+ if (isWriteEditTool(toolCall.name)) {
405
+ const writeEditState = createWriteEditBlock(parentEl, toolCall, {
406
+ initiallyExpanded: this.shouldExpandFileEditsByDefault(),
407
+ });
408
+ state.writeEditStates.set(toolId, writeEditState);
409
+ state.toolCallElements.set(toolId, writeEditState.wrapperEl);
410
+ } else {
411
+ renderToolCall(parentEl, toolCall, state.toolCallElements, {
412
+ initiallyExpanded: toolCall.name === TOOL_APPLY_PATCH && this.shouldExpandFileEditsByDefault(),
413
+ });
414
+ }
415
+ state.pendingTools.delete(toolId);
416
+ }
417
+
418
+ private handleToolOutput(
419
+ chunk: { type: 'tool_output'; id: string; content: string },
420
+ msg: ChatMessage,
421
+ ): void {
422
+ const { state } = this.deps;
423
+
424
+ if (state.pendingTools.has(chunk.id)) {
425
+ this.renderPendingTool(chunk.id);
426
+ }
427
+
428
+ const existingToolCall = msg.toolCalls?.find(tc => tc.id === chunk.id);
429
+ if (!existingToolCall) {
430
+ return;
431
+ }
432
+
433
+ existingToolCall.result = (existingToolCall.result ?? '') + chunk.content;
434
+ this.scheduleToolOutputRender(chunk.id, existingToolCall);
435
+ this.showThinkingIndicator();
436
+ }
437
+
438
+ // ============================================
439
+ // Provider lifecycle subagents (spawn → wait/close)
440
+ // ============================================
441
+
442
+ private handleProviderSubagentSpawn(
443
+ chunk: { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> },
444
+ msg: ChatMessage,
445
+ adapter: ProviderSubagentLifecycleAdapter,
446
+ ): void {
447
+ const { state } = this.deps;
448
+
449
+ const toolCall: ToolCallInfo = {
450
+ id: chunk.id,
451
+ name: chunk.name,
452
+ input: chunk.input,
453
+ status: 'running',
454
+ isExpanded: false,
455
+ };
456
+ msg.toolCalls = msg.toolCalls || [];
457
+ msg.toolCalls.push(toolCall);
458
+ msg.contentBlocks = msg.contentBlocks || [];
459
+ msg.contentBlocks.push({ type: 'tool_use', toolId: chunk.id });
460
+
461
+ // Render as subagent block immediately
462
+ if (state.currentContentEl) {
463
+ this.flushPendingTools();
464
+ const subagentInfo = adapter.buildSubagentInfo(toolCall, msg.toolCalls);
465
+
466
+ const subagentState = createSubagentBlock(state.currentContentEl, chunk.id, {
467
+ description: subagentInfo.description,
468
+ prompt: subagentInfo.prompt,
469
+ });
470
+ this.lifecycleSubagentStates.set(chunk.id, subagentState);
471
+ }
472
+ }
473
+
474
+ private handleProviderHiddenSubagentTool(
475
+ chunk: { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> },
476
+ msg: ChatMessage
477
+ ): void {
478
+ // Track in toolCalls for data completeness, but don't create DOM or content block
479
+ const toolCall: ToolCallInfo = {
480
+ id: chunk.id,
481
+ name: chunk.name,
482
+ input: chunk.input,
483
+ status: 'running',
484
+ isExpanded: false,
485
+ };
486
+ msg.toolCalls = msg.toolCalls || [];
487
+ msg.toolCalls.push(toolCall);
488
+ }
489
+
490
+ /**
491
+ * Handles tool_result for provider lifecycle subagent tools.
492
+ * Returns true if the result was consumed (caller should return early).
493
+ */
494
+ private handleProviderSubagentResult(
495
+ chunk: { type: 'tool_result'; id: string; content: string; isError?: boolean },
496
+ msg: ChatMessage
497
+ ): boolean {
498
+ const existingToolCall = msg.toolCalls?.find(tc => tc.id === chunk.id);
499
+ if (!existingToolCall) return false;
500
+ const normalizedContent = this.normalizeToolResultContent(chunk.content);
501
+
502
+ const adapter = this.getSubagentLifecycleAdapter(existingToolCall.name);
503
+ if (!adapter) return false;
504
+
505
+ if (adapter.isSpawnTool(existingToolCall.name)) {
506
+ existingToolCall.status = chunk.isError ? 'error' : 'completed';
507
+ existingToolCall.result = normalizedContent;
508
+
509
+ const spawnResult = adapter.extractSpawnResult(normalizedContent);
510
+ if (spawnResult.agentId) {
511
+ this.lifecycleAgentIdToSpawnId.set(spawnResult.agentId, chunk.id);
512
+ }
513
+
514
+ const subagentInfo = adapter.buildSubagentInfo(existingToolCall, msg.toolCalls ?? []);
515
+ const subagentState = this.lifecycleSubagentStates.get(chunk.id);
516
+ if (subagentState) {
517
+ subagentState.info.description = subagentInfo.description;
518
+ subagentState.info.prompt = subagentInfo.prompt;
519
+ subagentState.labelEl.setText(
520
+ subagentInfo.description.length > 40
521
+ ? subagentInfo.description.substring(0, 40) + '...'
522
+ : subagentInfo.description
523
+ );
524
+ }
525
+
526
+ if (chunk.isError) {
527
+ if (subagentState) {
528
+ finalizeSubagentBlock(subagentState, normalizedContent || 'Error', true);
529
+ }
530
+ }
531
+ return true;
532
+ }
533
+
534
+ if (adapter.isWaitTool(existingToolCall.name)) {
535
+ existingToolCall.status = chunk.isError ? 'error' : 'completed';
536
+ existingToolCall.result = normalizedContent;
537
+
538
+ for (const spawnId of adapter.resolveSpawnToolIds(
539
+ existingToolCall,
540
+ this.lifecycleAgentIdToSpawnId,
541
+ )) {
542
+ const spawnToolCall = msg.toolCalls?.find(tc => tc.id === spawnId);
543
+ const subagentState = this.lifecycleSubagentStates.get(spawnId);
544
+ if (!spawnToolCall || !subagentState) continue;
545
+
546
+ const subagentInfo = adapter.buildSubagentInfo(spawnToolCall, msg.toolCalls ?? []);
547
+ subagentState.info.description = subagentInfo.description;
548
+ subagentState.info.prompt = subagentInfo.prompt;
549
+
550
+ if (subagentInfo.status === 'completed' || subagentInfo.status === 'error') {
551
+ finalizeSubagentBlock(
552
+ subagentState,
553
+ subagentInfo.result || (subagentInfo.status === 'error' ? 'Error' : 'DONE'),
554
+ subagentInfo.status === 'error'
555
+ );
556
+ }
557
+ }
558
+ return true;
559
+ }
560
+
561
+ if (adapter.isCloseTool(existingToolCall.name)) {
562
+ existingToolCall.status = chunk.isError ? 'error' : 'completed';
563
+ existingToolCall.result = normalizedContent;
564
+ return true;
565
+ }
566
+
567
+ return false;
568
+ }
569
+
570
+ private async handleToolResult(
571
+ chunk: { type: 'tool_result'; id: string; content: string; isError?: boolean; toolUseResult?: SDKToolUseResult },
572
+ msg: ChatMessage
573
+ ): Promise<void> {
574
+ const { state, subagentManager } = this.deps;
575
+ const normalizedContent = this.normalizeToolResultContent(chunk.content);
576
+
577
+ // Resolve pending Task before processing result.
578
+ if (subagentManager.hasPendingTask(chunk.id)) {
579
+ this.renderPendingTaskFromTaskResultViaManager(chunk, msg);
580
+ }
581
+
582
+ // Check if it's a sync subagent result
583
+ const subagentState = subagentManager.getSyncSubagent(chunk.id);
584
+ if (subagentState) {
585
+ this.finalizeSubagent(chunk, msg);
586
+ return;
587
+ }
588
+
589
+ // Check if it's an async task result
590
+ if (this.handleAsyncTaskToolResult(chunk)) {
591
+ this.showThinkingIndicator();
592
+ return;
593
+ }
594
+
595
+ // Check if it's an agent output result
596
+ if (await this.handleAgentOutputToolResult(chunk)) {
597
+ this.showThinkingIndicator();
598
+ return;
599
+ }
600
+
601
+ if (this.handleProviderSubagentResult(chunk, msg)) {
602
+ this.showThinkingIndicator();
603
+ return;
604
+ }
605
+
606
+ // Check if tool is still pending (buffered) - render it now before applying result
607
+ if (state.pendingTools.has(chunk.id)) {
608
+ this.renderPendingTool(chunk.id);
609
+ }
610
+
611
+ const existingToolCall = msg.toolCalls?.find(tc => tc.id === chunk.id);
612
+
613
+ // Regular tool result
614
+ const isBlocked = isBlockedToolResult(normalizedContent, chunk.isError);
615
+
616
+ if (existingToolCall) {
617
+ // Tools that resolve via dedicated callbacks (not content-based) skip
618
+ // blocked detection — their status is determined solely by isError
619
+ if (chunk.isError) {
620
+ existingToolCall.status = 'error';
621
+ } else if (!skipsBlockedDetection(existingToolCall.name) && isBlocked) {
622
+ existingToolCall.status = 'blocked';
623
+ } else {
624
+ existingToolCall.status = 'completed';
625
+ }
626
+ existingToolCall.result = normalizedContent;
627
+
628
+ if (existingToolCall.name === TOOL_ASK_USER_QUESTION) {
629
+ const answers =
630
+ extractResolvedAnswers(chunk.toolUseResult) ??
631
+ extractResolvedAnswersFromResultText(normalizedContent);
632
+ if (answers) existingToolCall.resolvedAnswers = answers;
633
+ }
634
+
635
+ const writeEditState = state.writeEditStates.get(chunk.id);
636
+ if (writeEditState && isWriteEditTool(existingToolCall.name)) {
637
+ if (!chunk.isError && !isBlocked) {
638
+ const diffData = extractDiffData(chunk.toolUseResult, existingToolCall);
639
+ if (diffData) {
640
+ existingToolCall.diffData = diffData;
641
+ updateWriteEditWithDiff(writeEditState, diffData);
642
+ }
643
+ }
644
+ finalizeWriteEditBlock(writeEditState, chunk.isError || isBlocked);
645
+ } else {
646
+ this.cancelPendingToolOutputRender(chunk.id);
647
+ updateToolCallResult(chunk.id, existingToolCall, state.toolCallElements);
648
+ }
649
+
650
+ // Notify Obsidian vault so the file tree refreshes after Write/Edit/NotebookEdit
651
+ if (!chunk.isError && !isBlocked && isEditTool(existingToolCall.name)) {
652
+ this.notifyVaultFileChange(existingToolCall.input);
653
+ }
654
+
655
+ // Runtime apply_patch: refresh each changed file path
656
+ if (!chunk.isError && !isBlocked && existingToolCall.name === TOOL_APPLY_PATCH) {
657
+ this.notifyApplyPatchFileChanges(existingToolCall.input);
658
+ }
659
+ }
660
+
661
+ this.showThinkingIndicator();
662
+ }
663
+
664
+ // ============================================
665
+ // Text Block Management
666
+ // ============================================
667
+
668
+ async appendText(text: string): Promise<void> {
669
+ const { state } = this.deps;
670
+ if (!state.currentContentEl) return;
671
+
672
+ this.hideThinkingIndicator();
673
+
674
+ if (!state.currentTextEl) {
675
+ state.currentTextEl = state.currentContentEl.createDiv({ cls: 'claudian-text-block' });
676
+ state.currentTextContent = '';
677
+ }
678
+
679
+ state.currentTextContent += text;
680
+ void this.scheduleCurrentTextRender();
681
+ }
682
+
683
+ async finalizeCurrentTextBlock(msg?: ChatMessage): Promise<void> {
684
+ const { state, renderer } = this.deps;
685
+ await this.flushPendingTextRender();
686
+
687
+ if (msg && state.currentTextContent) {
688
+ if (
689
+ state.currentTextEl
690
+ && this.shouldDeferMathRendering()
691
+ && hasStreamingMathDelimiters(state.currentTextContent)
692
+ ) {
693
+ await renderer.renderContent(state.currentTextEl, state.currentTextContent);
694
+ }
695
+ msg.contentBlocks = msg.contentBlocks || [];
696
+ msg.contentBlocks.push({ type: 'text', content: state.currentTextContent });
697
+ // Copy button added here (not during streaming) to match history-loaded messages
698
+ if (state.currentTextEl) {
699
+ renderer.addTextCopyButton(state.currentTextEl, state.currentTextContent);
700
+ }
701
+ }
702
+ state.currentTextEl = null;
703
+ state.currentTextContent = '';
704
+ }
705
+
706
+ private scheduleCurrentTextRender(): Promise<void> {
707
+ if (!this.pendingTextRenderPromise) {
708
+ this.pendingTextRenderPromise = new Promise(resolve => {
709
+ this.resolvePendingTextRender = resolve;
710
+ });
711
+ }
712
+
713
+ if (this.pendingTextRenderFrame === null && !this.isTextRenderRunning) {
714
+ this.pendingTextRenderFrame = scheduleAnimationFrame(() => {
715
+ this.pendingTextRenderFrame = null;
716
+ void this.renderPendingText();
717
+ }, this.getStreamingRenderWindow());
718
+ }
719
+
720
+ return this.pendingTextRenderPromise;
721
+ }
722
+
723
+ private async flushPendingTextRender(): Promise<void> {
724
+ const pendingRender = this.pendingTextRenderPromise;
725
+ if (!pendingRender) return;
726
+
727
+ if (this.pendingTextRenderFrame !== null) {
728
+ cancelScheduledAnimationFrame(this.pendingTextRenderFrame);
729
+ this.pendingTextRenderFrame = null;
730
+ void this.renderPendingText();
731
+ }
732
+
733
+ await pendingRender;
734
+ }
735
+
736
+ private async renderPendingText(): Promise<void> {
737
+ if (this.isTextRenderRunning) return;
738
+ this.isTextRenderRunning = true;
739
+
740
+ const { state, renderer } = this.deps;
741
+ const textEl = state.currentTextEl;
742
+ const content = state.currentTextContent;
743
+
744
+ try {
745
+ if (textEl) {
746
+ const options = this.getStreamingRenderOptions(content);
747
+ if (options) {
748
+ await renderer.renderContent(textEl, content, options);
749
+ } else {
750
+ await renderer.renderContent(textEl, content);
751
+ }
752
+ this.scrollToBottom();
753
+ }
754
+ } catch {
755
+ // MessageRenderer owns user-visible render fallback; keep stream state moving.
756
+ } finally {
757
+ this.isTextRenderRunning = false;
758
+ }
759
+
760
+ if (state.currentTextEl === textEl && state.currentTextContent !== content) {
761
+ this.pendingTextRenderFrame = scheduleAnimationFrame(() => {
762
+ this.pendingTextRenderFrame = null;
763
+ void this.renderPendingText();
764
+ }, this.getStreamingRenderWindow());
765
+ return;
766
+ }
767
+
768
+ const resolve = this.resolvePendingTextRender;
769
+ this.pendingTextRenderPromise = null;
770
+ this.resolvePendingTextRender = null;
771
+ resolve?.();
772
+ }
773
+
774
+ private cancelPendingTextRender(): void {
775
+ if (this.pendingTextRenderFrame !== null) {
776
+ cancelScheduledAnimationFrame(this.pendingTextRenderFrame);
777
+ this.pendingTextRenderFrame = null;
778
+ }
779
+
780
+ const resolve = this.resolvePendingTextRender;
781
+ this.pendingTextRenderPromise = null;
782
+ this.resolvePendingTextRender = null;
783
+ resolve?.();
784
+ }
785
+
786
+ private scheduleToolOutputRender(toolId: string, toolCall: ToolCallInfo): void {
787
+ if (this.pendingToolOutputFrames.has(toolId)) return;
788
+
789
+ const frame = scheduleAnimationFrame(() => {
790
+ this.pendingToolOutputFrames.delete(toolId);
791
+ updateToolCallResult(toolId, toolCall, this.deps.state.toolCallElements);
792
+ this.scrollToBottom();
793
+ }, this.getMessagesWindow());
794
+ this.pendingToolOutputFrames.set(toolId, frame);
795
+ }
796
+
797
+ private cancelPendingToolOutputRender(toolId: string): void {
798
+ const frame = this.pendingToolOutputFrames.get(toolId);
799
+ if (!frame) return;
800
+
801
+ cancelScheduledAnimationFrame(frame);
802
+ this.pendingToolOutputFrames.delete(toolId);
803
+ }
804
+
805
+ private cancelPendingToolOutputRenders(): void {
806
+ for (const frame of this.pendingToolOutputFrames.values()) {
807
+ cancelScheduledAnimationFrame(frame);
808
+ }
809
+ this.pendingToolOutputFrames.clear();
810
+ }
811
+
812
+ // ============================================
813
+ // Thinking Block Management
814
+ // ============================================
815
+
816
+ async appendThinking(content: string): Promise<void> {
817
+ const { state, renderer } = this.deps;
818
+ if (!state.currentContentEl) return;
819
+
820
+ this.hideThinkingIndicator();
821
+ if (!state.currentThinkingState) {
822
+ state.currentThinkingState = createThinkingBlock(
823
+ state.currentContentEl,
824
+ (el, md) => renderer.renderContent(el, md)
825
+ );
826
+ }
827
+
828
+ state.currentThinkingState.content += content;
829
+ void this.scheduleCurrentThinkingRender();
830
+ }
831
+
832
+ async finalizeCurrentThinkingBlock(msg?: ChatMessage): Promise<void> {
833
+ const { state, renderer } = this.deps;
834
+ if (!state.currentThinkingState) return;
835
+ await this.flushPendingThinkingRender();
836
+
837
+ const thinkingState = state.currentThinkingState;
838
+ if (this.getStreamingRenderOptions(thinkingState.content)) {
839
+ await renderer.renderContent(thinkingState.contentEl, thinkingState.content);
840
+ }
841
+
842
+ const durationSeconds = finalizeThinkingBlock(thinkingState);
843
+
844
+ if (msg && thinkingState.content) {
845
+ msg.contentBlocks = msg.contentBlocks || [];
846
+ msg.contentBlocks.push({
847
+ type: 'thinking',
848
+ content: thinkingState.content,
849
+ durationSeconds,
850
+ });
851
+ }
852
+
853
+ state.currentThinkingState = null;
854
+ }
855
+
856
+ private scheduleCurrentThinkingRender(): Promise<void> {
857
+ if (!this.pendingThinkingRenderPromise) {
858
+ this.pendingThinkingRenderPromise = new Promise(resolve => {
859
+ this.resolvePendingThinkingRender = resolve;
860
+ });
861
+ }
862
+
863
+ if (this.pendingThinkingRenderFrame === null && !this.isThinkingRenderRunning) {
864
+ this.pendingThinkingRenderFrame = scheduleAnimationFrame(() => {
865
+ this.pendingThinkingRenderFrame = null;
866
+ void this.renderPendingThinking();
867
+ }, this.getThinkingRenderWindow());
868
+ }
869
+
870
+ return this.pendingThinkingRenderPromise;
871
+ }
872
+
873
+ private async flushPendingThinkingRender(): Promise<void> {
874
+ const pendingRender = this.pendingThinkingRenderPromise;
875
+ if (!pendingRender) return;
876
+
877
+ if (this.pendingThinkingRenderFrame !== null) {
878
+ cancelScheduledAnimationFrame(this.pendingThinkingRenderFrame);
879
+ this.pendingThinkingRenderFrame = null;
880
+ void this.renderPendingThinking();
881
+ }
882
+
883
+ await pendingRender;
884
+ }
885
+
886
+ private async renderPendingThinking(): Promise<void> {
887
+ if (this.isThinkingRenderRunning) return;
888
+ this.isThinkingRenderRunning = true;
889
+
890
+ const { state, renderer } = this.deps;
891
+ const thinkingState = state.currentThinkingState;
892
+ const content = thinkingState?.content ?? '';
893
+
894
+ try {
895
+ if (thinkingState) {
896
+ const options = this.getStreamingRenderOptions(content);
897
+ if (options) {
898
+ await renderer.renderContent(thinkingState.contentEl, content, options);
899
+ } else {
900
+ await renderer.renderContent(thinkingState.contentEl, content);
901
+ }
902
+ this.scrollToBottom();
903
+ }
904
+ } catch {
905
+ // MessageRenderer owns user-visible render fallback; keep stream state moving.
906
+ } finally {
907
+ this.isThinkingRenderRunning = false;
908
+ }
909
+
910
+ if (state.currentThinkingState === thinkingState && thinkingState && thinkingState.content !== content) {
911
+ this.pendingThinkingRenderFrame = scheduleAnimationFrame(() => {
912
+ this.pendingThinkingRenderFrame = null;
913
+ void this.renderPendingThinking();
914
+ }, this.getThinkingRenderWindow());
915
+ return;
916
+ }
917
+
918
+ const resolve = this.resolvePendingThinkingRender;
919
+ this.pendingThinkingRenderPromise = null;
920
+ this.resolvePendingThinkingRender = null;
921
+ resolve?.();
922
+ }
923
+
924
+ private cancelPendingThinkingRender(): void {
925
+ if (this.pendingThinkingRenderFrame !== null) {
926
+ cancelScheduledAnimationFrame(this.pendingThinkingRenderFrame);
927
+ this.pendingThinkingRenderFrame = null;
928
+ }
929
+
930
+ const resolve = this.resolvePendingThinkingRender;
931
+ this.pendingThinkingRenderPromise = null;
932
+ this.resolvePendingThinkingRender = null;
933
+ resolve?.();
934
+ }
935
+
936
+ // ============================================
937
+ // Subagent Tool Handling (via SubagentManager)
938
+ // ============================================
939
+
940
+ /** Delegates Agent tool_use to SubagentManager and updates message based on result. */
941
+ private handleTaskToolUseViaManager(
942
+ chunk: { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> },
943
+ msg: ChatMessage
944
+ ): void {
945
+ const { state, subagentManager } = this.deps;
946
+ this.ensureTaskToolCall(msg, chunk.id, chunk.input);
947
+
948
+ const result = subagentManager.handleTaskToolUse(chunk.id, chunk.input, state.currentContentEl);
949
+
950
+ switch (result.action) {
951
+ case 'created_sync':
952
+ this.recordSubagentInMessage(msg, result.subagentState.info, chunk.id);
953
+ this.showThinkingIndicator();
954
+ break;
955
+ case 'created_async':
956
+ this.recordSubagentInMessage(msg, result.info, chunk.id, 'async');
957
+ this.showThinkingIndicator();
958
+ break;
959
+ case 'buffered':
960
+ this.showThinkingIndicator();
961
+ break;
962
+ case 'label_updated':
963
+ break;
964
+ }
965
+ }
966
+
967
+ /** Renders a pending Agent tool call via SubagentManager and updates message. */
968
+ private renderPendingTaskViaManager(toolId: string, msg: ChatMessage): void {
969
+ const result = this.deps.subagentManager.renderPendingTask(toolId, this.deps.state.currentContentEl);
970
+ if (!result) return;
971
+
972
+ if (result.mode === 'sync') {
973
+ this.recordSubagentInMessage(msg, result.subagentState.info, toolId);
974
+ } else {
975
+ this.recordSubagentInMessage(msg, result.info, toolId, 'async');
976
+ }
977
+ }
978
+
979
+ /** Resolves a pending Agent tool call when its own tool_result arrives. */
980
+ private renderPendingTaskFromTaskResultViaManager(
981
+ chunk: { id: string; content: string; isError?: boolean; toolUseResult?: unknown },
982
+ msg: ChatMessage
983
+ ): void {
984
+ const result = this.deps.subagentManager.renderPendingTaskFromTaskResult(
985
+ chunk.id,
986
+ chunk.content,
987
+ chunk.isError || false,
988
+ this.deps.state.currentContentEl,
989
+ chunk.toolUseResult
990
+ );
991
+ if (!result) return;
992
+
993
+ if (result.mode === 'sync') {
994
+ this.recordSubagentInMessage(msg, result.subagentState.info, chunk.id);
995
+ } else {
996
+ this.recordSubagentInMessage(msg, result.info, chunk.id, 'async');
997
+ }
998
+ }
999
+
1000
+ private recordSubagentInMessage(
1001
+ msg: ChatMessage,
1002
+ info: SubagentInfo,
1003
+ toolId: string,
1004
+ mode?: 'async'
1005
+ ): void {
1006
+ const taskToolCall = this.ensureTaskToolCall(msg, toolId);
1007
+ this.applySubagentToTaskToolCall(taskToolCall, info);
1008
+
1009
+ msg.contentBlocks = msg.contentBlocks || [];
1010
+ const existingBlock = msg.contentBlocks.find(
1011
+ block => block.type === 'subagent' && block.subagentId === toolId
1012
+ );
1013
+ if (existingBlock && mode && existingBlock.type === 'subagent') {
1014
+ existingBlock.mode = mode;
1015
+ } else if (!existingBlock) {
1016
+ msg.contentBlocks.push(mode
1017
+ ? { type: 'subagent', subagentId: toolId, mode }
1018
+ : { type: 'subagent', subagentId: toolId }
1019
+ );
1020
+ }
1021
+ }
1022
+
1023
+ private async handleSubagentChunk(
1024
+ chunk: Extract<StreamChunk, { type: 'subagent_tool_use' | 'subagent_tool_result' }>,
1025
+ msg: ChatMessage,
1026
+ ): Promise<void> {
1027
+ const parentToolUseId = chunk.subagentId;
1028
+ const { subagentManager } = this.deps;
1029
+
1030
+ // If parent Agent call is still pending, child chunk confirms it's sync - render now
1031
+ if (subagentManager.hasPendingTask(parentToolUseId)) {
1032
+ this.renderPendingTaskViaManager(parentToolUseId, msg);
1033
+ }
1034
+
1035
+ const subagentState = subagentManager.getSyncSubagent(parentToolUseId);
1036
+
1037
+ if (!subagentState) {
1038
+ return;
1039
+ }
1040
+
1041
+ switch (chunk.type) {
1042
+ case 'subagent_tool_use': {
1043
+ const toolCall: ToolCallInfo = {
1044
+ id: chunk.id,
1045
+ name: chunk.name,
1046
+ input: chunk.input,
1047
+ status: 'running',
1048
+ isExpanded: false,
1049
+ };
1050
+ subagentManager.addSyncToolCall(parentToolUseId, toolCall);
1051
+ this.showThinkingIndicator();
1052
+ break;
1053
+ }
1054
+
1055
+ case 'subagent_tool_result': {
1056
+ const toolCall = subagentState.info.toolCalls.find((tc: ToolCallInfo) => tc.id === chunk.id);
1057
+ if (toolCall) {
1058
+ const normalizedContent = this.normalizeToolResultContent(chunk.content);
1059
+ const isBlocked = isBlockedToolResult(normalizedContent, chunk.isError);
1060
+ toolCall.status = isBlocked ? 'blocked' : (chunk.isError ? 'error' : 'completed');
1061
+ toolCall.result = normalizedContent;
1062
+ subagentManager.updateSyncToolResult(parentToolUseId, chunk.id, toolCall);
1063
+ }
1064
+ break;
1065
+ }
1066
+
1067
+ default:
1068
+ break;
1069
+ }
1070
+ }
1071
+
1072
+ /** Finalizes a sync subagent when its Agent tool_result is received. */
1073
+ private finalizeSubagent(
1074
+ chunk: { type: 'tool_result'; id: string; content: string; isError?: boolean; toolUseResult?: unknown },
1075
+ msg: ChatMessage
1076
+ ): void {
1077
+ const isError = chunk.isError || false;
1078
+ const normalizedContent = this.normalizeToolResultContent(chunk.content);
1079
+ const finalized = this.deps.subagentManager.finalizeSyncSubagent(
1080
+ chunk.id, chunk.content, isError, chunk.toolUseResult
1081
+ );
1082
+
1083
+ const extractedResult = finalized?.result ?? normalizedContent;
1084
+
1085
+ const taskToolCall = this.ensureTaskToolCall(msg, chunk.id);
1086
+ taskToolCall.status = isError ? 'error' : 'completed';
1087
+ taskToolCall.result = extractedResult;
1088
+ if (taskToolCall.subagent) {
1089
+ taskToolCall.subagent.status = isError ? 'error' : 'completed';
1090
+ taskToolCall.subagent.result = extractedResult;
1091
+ }
1092
+
1093
+ if (finalized) {
1094
+ this.applySubagentToTaskToolCall(taskToolCall, finalized);
1095
+ }
1096
+
1097
+ this.showThinkingIndicator();
1098
+ }
1099
+
1100
+ // ============================================
1101
+ // Async Subagent Handling
1102
+ // ============================================
1103
+
1104
+ /** Handles TaskOutput tool_use (invisible, links to async subagent). */
1105
+ private handleAgentOutputToolUse(
1106
+ chunk: { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> },
1107
+ _msg: ChatMessage
1108
+ ): void {
1109
+ const toolCall: ToolCallInfo = {
1110
+ id: chunk.id,
1111
+ name: chunk.name,
1112
+ input: chunk.input,
1113
+ status: 'running',
1114
+ isExpanded: false,
1115
+ };
1116
+
1117
+ this.deps.subagentManager.handleAgentOutputToolUse(toolCall);
1118
+
1119
+ // Show flavor text while waiting for TaskOutput result
1120
+ this.showThinkingIndicator();
1121
+ }
1122
+
1123
+ private handleAsyncTaskToolResult(
1124
+ chunk: { type: 'tool_result'; id: string; content: string; isError?: boolean; toolUseResult?: unknown }
1125
+ ): boolean {
1126
+ const { subagentManager } = this.deps;
1127
+ if (!subagentManager.isPendingAsyncTask(chunk.id)) {
1128
+ return false;
1129
+ }
1130
+
1131
+ subagentManager.handleTaskToolResult(chunk.id, chunk.content, chunk.isError, chunk.toolUseResult);
1132
+ return true;
1133
+ }
1134
+
1135
+ /** Handles TaskOutput result to finalize async subagent. */
1136
+ private async handleAgentOutputToolResult(
1137
+ chunk: { type: 'tool_result'; id: string; content: string; isError?: boolean; toolUseResult?: unknown }
1138
+ ): Promise<boolean> {
1139
+ const { subagentManager } = this.deps;
1140
+ const isLinked = subagentManager.isLinkedAgentOutputTool(chunk.id);
1141
+
1142
+ const handled = subagentManager.handleAgentOutputToolResult(
1143
+ chunk.id,
1144
+ chunk.content,
1145
+ chunk.isError || false,
1146
+ chunk.toolUseResult
1147
+ );
1148
+
1149
+ await this.hydrateAsyncSubagentToolCalls(handled);
1150
+
1151
+ return isLinked || handled !== undefined;
1152
+ }
1153
+
1154
+ private async handleAsyncSubagentResult(
1155
+ chunk: Extract<StreamChunk, { type: 'async_subagent_result' }>
1156
+ ): Promise<void> {
1157
+ const handled = this.deps.subagentManager.handleAsyncSubagentResult(
1158
+ chunk.agentId,
1159
+ chunk.status,
1160
+ chunk.result
1161
+ );
1162
+
1163
+ await this.hydrateAsyncSubagentToolCalls(handled);
1164
+ if (handled) {
1165
+ this.showThinkingIndicator();
1166
+ }
1167
+ }
1168
+
1169
+ private async hydrateAsyncSubagentToolCalls(subagent: SubagentInfo | undefined): Promise<void> {
1170
+ if (!subagent) return;
1171
+ if (subagent.mode !== 'async') return;
1172
+ if (!subagent.agentId) return;
1173
+
1174
+ const asyncStatus = subagent.asyncStatus ?? subagent.status;
1175
+ if (asyncStatus !== 'completed' && asyncStatus !== 'error') return;
1176
+
1177
+ const runtime = this.deps.getAgentService?.();
1178
+ if (!runtime) return;
1179
+
1180
+ const { hasHydrated, finalResultHydrated } = await this.tryHydrateAsyncSubagent(
1181
+ subagent,
1182
+ runtime,
1183
+ true
1184
+ );
1185
+
1186
+ if (hasHydrated) {
1187
+ this.deps.subagentManager.refreshAsyncSubagent(subagent);
1188
+ }
1189
+
1190
+ if (!finalResultHydrated) {
1191
+ this.scheduleAsyncSubagentResultRetry(subagent, runtime, 0);
1192
+ }
1193
+ }
1194
+
1195
+ private async tryHydrateAsyncSubagent(
1196
+ subagent: SubagentInfo,
1197
+ runtime: ChatRuntime,
1198
+ hydrateToolCalls: boolean
1199
+ ): Promise<{ hasHydrated: boolean; finalResultHydrated: boolean }> {
1200
+ let hasHydrated = false;
1201
+ let finalResultHydrated = false;
1202
+
1203
+ if (hydrateToolCalls && !subagent.toolCalls?.length) {
1204
+ const recoveredToolCalls = await runtime.loadSubagentToolCalls?.(
1205
+ subagent.agentId || ''
1206
+ ) ?? [];
1207
+ if (recoveredToolCalls.length > 0) {
1208
+ subagent.toolCalls = recoveredToolCalls.map((toolCall) => ({
1209
+ ...toolCall,
1210
+ input: { ...toolCall.input },
1211
+ }));
1212
+ hasHydrated = true;
1213
+ }
1214
+ }
1215
+
1216
+ const recoveredFinalResult = await runtime.loadSubagentFinalResult?.(
1217
+ subagent.agentId || ''
1218
+ ) ?? null;
1219
+ if (recoveredFinalResult && recoveredFinalResult.trim().length > 0) {
1220
+ finalResultHydrated = true;
1221
+ if (recoveredFinalResult !== subagent.result) {
1222
+ subagent.result = recoveredFinalResult;
1223
+ hasHydrated = true;
1224
+ }
1225
+ }
1226
+
1227
+ return { hasHydrated, finalResultHydrated };
1228
+ }
1229
+
1230
+ private scheduleAsyncSubagentResultRetry(
1231
+ subagent: SubagentInfo,
1232
+ runtime: ChatRuntime,
1233
+ attempt: number
1234
+ ): void {
1235
+ if (!subagent.agentId) return;
1236
+ if (attempt >= StreamController.ASYNC_SUBAGENT_RESULT_RETRY_DELAYS_MS.length) return;
1237
+
1238
+ const delay = StreamController.ASYNC_SUBAGENT_RESULT_RETRY_DELAYS_MS[attempt];
1239
+ window.setTimeout(() => {
1240
+ void this.retryAsyncSubagentResult(subagent, runtime, attempt);
1241
+ }, delay);
1242
+ }
1243
+
1244
+ private async retryAsyncSubagentResult(
1245
+ subagent: SubagentInfo,
1246
+ runtime: ChatRuntime,
1247
+ attempt: number
1248
+ ): Promise<void> {
1249
+ if (!subagent.agentId) return;
1250
+ const asyncStatus = subagent.asyncStatus ?? subagent.status;
1251
+ if (asyncStatus !== 'completed' && asyncStatus !== 'error') return;
1252
+
1253
+ const { hasHydrated, finalResultHydrated } = await this.tryHydrateAsyncSubagent(
1254
+ subagent,
1255
+ runtime,
1256
+ false
1257
+ );
1258
+ if (hasHydrated) {
1259
+ this.deps.subagentManager.refreshAsyncSubagent(subagent);
1260
+ }
1261
+
1262
+ if (!finalResultHydrated) {
1263
+ this.scheduleAsyncSubagentResultRetry(subagent, runtime, attempt + 1);
1264
+ }
1265
+ }
1266
+
1267
+ /** Callback from SubagentManager when async state changes. Updates messages only (DOM handled by manager). */
1268
+ onAsyncSubagentStateChange(subagent: SubagentInfo): void {
1269
+ this.updateSubagentInMessages(subagent);
1270
+ this.scrollToBottom();
1271
+ }
1272
+
1273
+ private updateSubagentInMessages(subagent: SubagentInfo): void {
1274
+ const { state } = this.deps;
1275
+ for (let i = state.messages.length - 1; i >= 0; i--) {
1276
+ const msg = state.messages[i];
1277
+ if (msg.role !== 'assistant') continue;
1278
+ if (this.linkTaskToolCallToSubagent(msg, subagent)) {
1279
+ return;
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ private ensureTaskToolCall(
1285
+ msg: ChatMessage,
1286
+ toolId: string,
1287
+ input?: Record<string, unknown>
1288
+ ): ToolCallInfo {
1289
+ msg.toolCalls = msg.toolCalls || [];
1290
+ const existing = msg.toolCalls.find(
1291
+ tc => tc.id === toolId && isSubagentToolName(tc.name)
1292
+ );
1293
+ if (existing) {
1294
+ if (input && Object.keys(input).length > 0) {
1295
+ existing.input = { ...existing.input, ...input };
1296
+ }
1297
+ return existing;
1298
+ }
1299
+
1300
+ const taskToolCall: ToolCallInfo = {
1301
+ id: toolId,
1302
+ name: TOOL_TASK,
1303
+ input: input ? { ...input } : {},
1304
+ status: 'running',
1305
+ isExpanded: false,
1306
+ };
1307
+ msg.toolCalls.push(taskToolCall);
1308
+ return taskToolCall;
1309
+ }
1310
+
1311
+ private applySubagentToTaskToolCall(taskToolCall: ToolCallInfo, subagent: SubagentInfo): void {
1312
+ taskToolCall.subagent = subagent;
1313
+ if (subagent.status === 'completed') taskToolCall.status = 'completed';
1314
+ else if (subagent.status === 'error') taskToolCall.status = 'error';
1315
+ else taskToolCall.status = 'running';
1316
+ if (subagent.result !== undefined) {
1317
+ taskToolCall.result = subagent.result;
1318
+ }
1319
+ }
1320
+
1321
+ private linkTaskToolCallToSubagent(msg: ChatMessage, subagent: SubagentInfo): boolean {
1322
+ const taskToolCall = msg.toolCalls?.find(
1323
+ tc => tc.id === subagent.id && isSubagentToolName(tc.name)
1324
+ );
1325
+ if (!taskToolCall) return false;
1326
+ this.applySubagentToTaskToolCall(taskToolCall, subagent);
1327
+ return true;
1328
+ }
1329
+
1330
+ // ============================================
1331
+ // Thinking Indicator
1332
+ // ============================================
1333
+
1334
+ /** Debounce delay before showing thinking indicator (ms). */
1335
+ private static readonly THINKING_INDICATOR_DELAY = 400;
1336
+
1337
+ /**
1338
+ * Schedules showing the thinking indicator after a delay.
1339
+ * If content arrives before the delay, the indicator won't show.
1340
+ * This prevents the indicator from appearing during active streaming.
1341
+ * Note: Flavor text is hidden when model thinking block is active (thinking takes priority).
1342
+ */
1343
+ showThinkingIndicator(overrideText?: string, overrideCls?: string): void {
1344
+ const { state } = this.deps;
1345
+
1346
+ // Early return if no content element
1347
+ if (!state.currentContentEl) return;
1348
+
1349
+ // Clear any existing timeout
1350
+ if (state.thinkingIndicatorTimeout) {
1351
+ const timerWindow = state.currentContentEl.ownerDocument.defaultView ?? window;
1352
+ state.clearThinkingIndicatorTimeout(timerWindow);
1353
+ }
1354
+
1355
+ // Don't show flavor text while model thinking block is active
1356
+ if (state.currentThinkingState) {
1357
+ return;
1358
+ }
1359
+
1360
+ // If indicator already exists, just re-append it to the bottom
1361
+ if (state.thinkingEl) {
1362
+ state.currentContentEl.appendChild(state.thinkingEl);
1363
+ this.deps.updateQueueIndicator();
1364
+ return;
1365
+ }
1366
+
1367
+ // Schedule showing the indicator after a delay
1368
+ const timerWindow = state.currentContentEl.ownerDocument.defaultView ?? window;
1369
+ state.setThinkingIndicatorTimeout(timerWindow.setTimeout(() => {
1370
+ state.setThinkingIndicatorTimeout(null, null);
1371
+ // Double-check we still have a content element, no indicator exists, and no thinking block
1372
+ if (!state.currentContentEl || state.thinkingEl || state.currentThinkingState) return;
1373
+
1374
+ const cls = overrideCls
1375
+ ? `claudian-thinking ${overrideCls}`
1376
+ : 'claudian-thinking';
1377
+ state.thinkingEl = state.currentContentEl.createDiv({ cls });
1378
+ const text = overrideText || FLAVOR_TEXTS[Math.floor(Math.random() * FLAVOR_TEXTS.length)];
1379
+ state.thinkingEl.createSpan({ text });
1380
+
1381
+ // Create timer span with initial value
1382
+ const timerSpan = state.thinkingEl.createSpan({ cls: 'claudian-thinking-hint' });
1383
+ const updateTimer = () => {
1384
+ if (!state.responseStartTime) return;
1385
+ // Check if element is still connected to DOM (prevents orphaned interval updates)
1386
+ if (!timerSpan.isConnected) {
1387
+ if (state.flavorTimerInterval) {
1388
+ state.clearFlavorTimerInterval();
1389
+ }
1390
+ return;
1391
+ }
1392
+ const elapsedSeconds = Math.floor((performance.now() - state.responseStartTime) / 1000);
1393
+ timerSpan.setText(` (esc to interrupt · ${formatDurationMmSs(elapsedSeconds)})`);
1394
+ };
1395
+ updateTimer(); // Initial update
1396
+
1397
+ // Start interval to update timer every second
1398
+ if (state.flavorTimerInterval) {
1399
+ state.clearFlavorTimerInterval();
1400
+ }
1401
+ const thinkingWindow = state.currentContentEl.ownerDocument.defaultView ?? timerWindow;
1402
+ state.setFlavorTimerInterval(thinkingWindow.setInterval(updateTimer, 1000), thinkingWindow);
1403
+
1404
+ }, StreamController.THINKING_INDICATOR_DELAY), timerWindow);
1405
+ }
1406
+
1407
+ /** Hides the thinking indicator and cancels any pending show timeout. */
1408
+ hideThinkingIndicator(): void {
1409
+ const { state } = this.deps;
1410
+
1411
+ // Cancel any pending show timeout
1412
+ if (state.thinkingIndicatorTimeout) {
1413
+ const activeWindow = this.deps.getMessagesEl().ownerDocument.defaultView ?? window;
1414
+ state.clearThinkingIndicatorTimeout(activeWindow);
1415
+ }
1416
+
1417
+ // Clear timer interval (but preserve responseStartTime for duration capture)
1418
+ state.clearFlavorTimerInterval();
1419
+
1420
+ if (state.thinkingEl) {
1421
+ state.thinkingEl.remove();
1422
+ state.thinkingEl = null;
1423
+ }
1424
+ }
1425
+
1426
+ // ============================================
1427
+ // Compact Boundary
1428
+ // ============================================
1429
+
1430
+ private renderCompactBoundary(): void {
1431
+ const { state } = this.deps;
1432
+ if (!state.currentContentEl) return;
1433
+ this.hideThinkingIndicator();
1434
+ const el = state.currentContentEl.createDiv({ cls: 'claudian-compact-boundary' });
1435
+ el.createSpan({ cls: 'claudian-compact-boundary-label', text: 'Conversation compacted' });
1436
+ }
1437
+
1438
+ // ============================================
1439
+ // Utilities
1440
+ // ============================================
1441
+
1442
+ /**
1443
+ * Nudges Obsidian's vault after a Write/Edit/NotebookEdit so the file tree
1444
+ * refreshes. Direct `fs` writes bypass the Vault API, and macOS + iCloud
1445
+ * FSWatcher often misses the event.
1446
+ */
1447
+ private notifyVaultFileChange(input: Record<string, unknown>): void {
1448
+ const rawPathValue = input.file_path ?? input.notebook_path;
1449
+ const rawPath = typeof rawPathValue === 'string' ? rawPathValue : undefined;
1450
+ const vaultPath = getVaultPath(this.deps.plugin.app);
1451
+ const relativePath = normalizePathForVault(rawPath, vaultPath);
1452
+ if (!relativePath || relativePath.startsWith('/')) return;
1453
+
1454
+ window.setTimeout(() => {
1455
+ const { vault } = this.deps.plugin.app;
1456
+ const file = vault.getAbstractFileByPath(relativePath);
1457
+ if (file instanceof TFile) {
1458
+ // Existing file — tell listeners the content changed
1459
+ vault.trigger('modify', file);
1460
+ } else {
1461
+ // New file — scan parent directory so Obsidian discovers it
1462
+ const parentDir = relativePath.includes('/')
1463
+ ? relativePath.substring(0, relativePath.lastIndexOf('/'))
1464
+ : '';
1465
+ vault.adapter.list(parentDir).catch(() => { /* ignore */ });
1466
+ }
1467
+ }, 200);
1468
+ }
1469
+
1470
+ /** Refreshes vault for each file path in an apply_patch changes array or patch text. */
1471
+ private notifyApplyPatchFileChanges(input: Record<string, unknown>): void {
1472
+ const notified = new Set<string>();
1473
+
1474
+ // Legacy changes array
1475
+ const changes = input.changes;
1476
+ if (Array.isArray(changes)) {
1477
+ for (const change of changes) {
1478
+ if (change && typeof change === 'object' && !Array.isArray(change)) {
1479
+ const changeRecord = change as Record<string, unknown>;
1480
+ if (typeof changeRecord.path === 'string') {
1481
+ notified.add(changeRecord.path);
1482
+ this.notifyVaultFileChange({ file_path: changeRecord.path });
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1487
+
1488
+ // Parse file paths from patch text markers (current custom_tool_call format)
1489
+ const patchText = typeof input.patch === 'string' ? input.patch : '';
1490
+ if (patchText) {
1491
+ for (const match of patchText.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) {
1492
+ const filePath = match[1]?.trim();
1493
+ if (filePath && !notified.has(filePath)) {
1494
+ this.notifyVaultFileChange({ file_path: filePath });
1495
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+
1500
+ /** Scrolls messages to bottom if auto-scroll is enabled. */
1501
+ private scrollToBottom(): void {
1502
+ if (this.pendingScrollFrame !== null) return;
1503
+
1504
+ this.pendingScrollFrame = scheduleAnimationFrame(() => {
1505
+ this.pendingScrollFrame = null;
1506
+ this.applyScrollToBottom();
1507
+ }, this.getMessagesWindow());
1508
+ }
1509
+
1510
+ private applyScrollToBottom(): void {
1511
+ const { state, plugin } = this.deps;
1512
+ if (!(plugin.settings.enableAutoScroll ?? true)) return;
1513
+ if (!state.autoScrollEnabled) return;
1514
+
1515
+ const messagesEl = this.deps.getMessagesEl();
1516
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1517
+ }
1518
+
1519
+ private cancelPendingScroll(): void {
1520
+ if (this.pendingScrollFrame === null) return;
1521
+
1522
+ cancelScheduledAnimationFrame(this.pendingScrollFrame);
1523
+ this.pendingScrollFrame = null;
1524
+ }
1525
+
1526
+ private getMessagesWindow(): Window | null {
1527
+ return this.deps.getMessagesEl().ownerDocument.defaultView ?? null;
1528
+ }
1529
+
1530
+ private getStreamingRenderWindow(): Window | null {
1531
+ const { state } = this.deps;
1532
+ return state.currentTextEl?.ownerDocument?.defaultView
1533
+ ?? state.currentContentEl?.ownerDocument?.defaultView
1534
+ ?? this.getMessagesWindow();
1535
+ }
1536
+
1537
+ private getThinkingRenderWindow(): Window | null {
1538
+ const { state } = this.deps;
1539
+ return state.currentThinkingState?.contentEl.ownerDocument?.defaultView
1540
+ ?? state.currentContentEl?.ownerDocument?.defaultView
1541
+ ?? this.getMessagesWindow();
1542
+ }
1543
+
1544
+ resetStreamingState(): void {
1545
+ const { state } = this.deps;
1546
+ this.cancelPendingTextRender();
1547
+ this.cancelPendingThinkingRender();
1548
+ this.cancelPendingToolOutputRenders();
1549
+ this.cancelPendingScroll();
1550
+ this.hideThinkingIndicator();
1551
+ state.currentContentEl = null;
1552
+ state.currentTextEl = null;
1553
+ state.currentTextContent = '';
1554
+ state.currentThinkingState = null;
1555
+ this.deps.subagentManager.resetStreamingState();
1556
+ state.pendingTools.clear();
1557
+ // Reset response timer (duration already captured at this point)
1558
+ state.responseStartTime = null;
1559
+ }
1560
+ }