@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,1759 @@
1
+ import '@/providers';
2
+
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+
7
+ import type { SubagentInfo, ToolCallInfo } from '@/core/types';
8
+ import { SubagentManager } from '@/features/chat/services/SubagentManager';
9
+ import { createStopSubagentHook } from '@/providers/claude/hooks/SubagentHooks';
10
+
11
+ jest.mock('@/features/chat/rendering/SubagentRenderer', () => ({
12
+ createSubagentBlock: jest.fn().mockImplementation((_parentEl: any, toolId: string, input: any) => ({
13
+ wrapperEl: { querySelector: jest.fn().mockReturnValue(null) },
14
+ contentEl: {},
15
+ info: {
16
+ id: toolId,
17
+ description: input?.description || 'Task',
18
+ prompt: input?.prompt || '',
19
+ mode: 'sync',
20
+ isExpanded: false,
21
+ status: 'running',
22
+ toolCalls: [],
23
+ },
24
+ toolCallStates: new Map(),
25
+ })),
26
+ createAsyncSubagentBlock: jest.fn().mockImplementation((_parentEl: any, toolId: string, input: any) => ({
27
+ wrapperEl: { querySelector: jest.fn().mockReturnValue(null) },
28
+ info: {
29
+ id: toolId,
30
+ description: input?.description || 'Background task',
31
+ prompt: input?.prompt || '',
32
+ mode: 'async',
33
+ isExpanded: false,
34
+ status: 'running',
35
+ toolCalls: [],
36
+ asyncStatus: 'pending',
37
+ },
38
+ statusEl: {},
39
+ })),
40
+ addSubagentToolCall: jest.fn(),
41
+ updateSubagentToolResult: jest.fn(),
42
+ finalizeSubagentBlock: jest.fn(),
43
+ updateAsyncSubagentRunning: jest.fn(),
44
+ finalizeAsyncSubagent: jest.fn(),
45
+ markAsyncSubagentOrphaned: jest.fn(),
46
+ }));
47
+
48
+ const createManager = () => {
49
+ const updates: SubagentInfo[] = [];
50
+ const manager = new SubagentManager((subagent) => {
51
+ updates.push({ ...subagent });
52
+ });
53
+ return { manager, updates };
54
+ };
55
+
56
+ const createMockEl = () => ({ createDiv: jest.fn(), appendChild: jest.fn() } as any);
57
+
58
+ describe('SubagentManager', () => {
59
+ beforeEach(() => {
60
+ jest.clearAllMocks();
61
+ });
62
+
63
+ // ============================================
64
+ // Async Lifecycle Tests (migrated from AsyncSubagentManager)
65
+ // ============================================
66
+
67
+ describe('async lifecycle', () => {
68
+ it('transitions from pending to running when agent_id is parsed', () => {
69
+ const { manager, updates } = createManager();
70
+ const parentEl = createMockEl();
71
+
72
+ manager.handleTaskToolUse('task-1', { description: 'Background', run_in_background: true }, parentEl);
73
+ expect(manager.getByTaskId('task-1')?.asyncStatus).toBe('pending');
74
+
75
+ manager.handleTaskToolResult('task-1', JSON.stringify({ agent_id: 'agent-123' }));
76
+
77
+ const running = manager.getByTaskId('task-1');
78
+ expect(running?.asyncStatus).toBe('running');
79
+ expect(running?.agentId).toBe('agent-123');
80
+ expect(updates[updates.length - 1].agentId).toBe('agent-123');
81
+ expect(manager.isPendingAsyncTask('task-1')).toBe(false);
82
+ });
83
+
84
+ it('transitions from pending to running when agent_id exists only in toolUseResult', () => {
85
+ const { manager, updates } = createManager();
86
+ const parentEl = createMockEl();
87
+
88
+ manager.handleTaskToolUse('task-structured', { description: 'Background', run_in_background: true }, parentEl);
89
+ manager.handleTaskToolResult(
90
+ 'task-structured',
91
+ 'Task launched',
92
+ false,
93
+ { data: { agent_id: 'agent-structured-1' } }
94
+ );
95
+
96
+ const running = manager.getByTaskId('task-structured');
97
+ expect(running?.asyncStatus).toBe('running');
98
+ expect(running?.agentId).toBe('agent-structured-1');
99
+ expect(updates[updates.length - 1].agentId).toBe('agent-structured-1');
100
+ expect(manager.isPendingAsyncTask('task-structured')).toBe(false);
101
+ });
102
+
103
+ it('transitions from pending to running when structured content carries agent_id', () => {
104
+ const { manager, updates } = createManager();
105
+ const parentEl = createMockEl();
106
+
107
+ manager.handleTaskToolUse('task-array', { description: 'Background', run_in_background: true }, parentEl);
108
+ manager.handleTaskToolResult(
109
+ 'task-array',
110
+ [{ type: 'text', text: '{"agent_id":"agent-array-1"}' }] as any,
111
+ );
112
+
113
+ const running = manager.getByTaskId('task-array');
114
+ expect(running?.asyncStatus).toBe('running');
115
+ expect(running?.agentId).toBe('agent-array-1');
116
+ expect(updates[updates.length - 1].agentId).toBe('agent-array-1');
117
+ expect(manager.isPendingAsyncTask('task-array')).toBe(false);
118
+ });
119
+
120
+ it('moves to error when Task tool_result parsing fails', () => {
121
+ const { manager, updates } = createManager();
122
+ const parentEl = createMockEl();
123
+
124
+ manager.handleTaskToolUse('task-parse-fail', { description: 'No id', run_in_background: true }, parentEl);
125
+ manager.handleTaskToolResult('task-parse-fail', 'no agent id present');
126
+
127
+ expect(manager.getByTaskId('task-parse-fail')).toBeUndefined();
128
+ const last = updates[updates.length - 1];
129
+ expect(last.asyncStatus).toBe('error');
130
+ expect(last.result).toContain('Failed to parse agent_id');
131
+ });
132
+
133
+ it('moves to error when Task tool_result itself is an error', () => {
134
+ const { manager, updates } = createManager();
135
+ const parentEl = createMockEl();
136
+
137
+ manager.handleTaskToolUse('task-error', { description: 'Will fail', run_in_background: true }, parentEl);
138
+ manager.handleTaskToolResult('task-error', 'launch failed', true);
139
+
140
+ expect(manager.getByTaskId('task-error')).toBeUndefined();
141
+ const last = updates[updates.length - 1];
142
+ expect(last.asyncStatus).toBe('error');
143
+ expect(last.result).toBe('launch failed');
144
+ });
145
+
146
+ it('stays running when AgentOutputTool reports not_ready', () => {
147
+ const { manager } = createManager();
148
+ const parentEl = createMockEl();
149
+
150
+ manager.handleTaskToolUse('task-running', { description: 'Background', run_in_background: true }, parentEl);
151
+ manager.handleTaskToolResult('task-running', JSON.stringify({ agent_id: 'agent-abc' }));
152
+
153
+ const toolCall: ToolCallInfo = {
154
+ id: 'output-not-ready',
155
+ name: 'AgentOutputTool',
156
+ input: { agent_id: 'agent-abc' },
157
+ status: 'running',
158
+ isExpanded: false,
159
+ };
160
+ manager.handleAgentOutputToolUse(toolCall);
161
+
162
+ const stillRunning = manager.handleAgentOutputToolResult(
163
+ 'output-not-ready',
164
+ JSON.stringify({ retrieval_status: 'not_ready', agents: {} }),
165
+ false
166
+ );
167
+
168
+ expect(stillRunning?.asyncStatus).toBe('running');
169
+ expect(manager.getByTaskId('task-running')?.asyncStatus).toBe('running');
170
+ });
171
+
172
+ it('ignores unrelated tool_result when async subagent is active', () => {
173
+ const { manager } = createManager();
174
+ const parentEl = createMockEl();
175
+
176
+ manager.handleTaskToolUse('task-standalone', { description: 'Background', run_in_background: true }, parentEl);
177
+ manager.handleTaskToolResult('task-standalone', JSON.stringify({ agent_id: 'agent-standalone' }));
178
+
179
+ const unrelated = manager.handleAgentOutputToolResult(
180
+ 'non-agent-output',
181
+ 'regular tool output',
182
+ false
183
+ );
184
+
185
+ expect(unrelated).toBeUndefined();
186
+ expect(manager.getByTaskId('task-standalone')?.asyncStatus).toBe('running');
187
+ });
188
+
189
+ it('finalizes to completed when AgentOutputTool succeeds and extracts result', () => {
190
+ const { manager, updates } = createManager();
191
+ const parentEl = createMockEl();
192
+
193
+ manager.handleTaskToolUse('task-complete', { description: 'Background', run_in_background: true }, parentEl);
194
+ manager.handleTaskToolResult('task-complete', JSON.stringify({ agent_id: 'agent-complete' }));
195
+
196
+ const toolCall: ToolCallInfo = {
197
+ id: 'output-success',
198
+ name: 'AgentOutputTool',
199
+ input: { agent_id: 'agent-complete' },
200
+ status: 'running',
201
+ isExpanded: false,
202
+ };
203
+ manager.handleAgentOutputToolUse(toolCall);
204
+
205
+ const completed = manager.handleAgentOutputToolResult(
206
+ 'output-success',
207
+ JSON.stringify({
208
+ retrieval_status: 'success',
209
+ agents: { 'agent-complete': { status: 'completed', result: 'done!' } },
210
+ }),
211
+ false
212
+ );
213
+
214
+ expect(completed?.asyncStatus).toBe('completed');
215
+ expect(completed?.result).toBe('done!');
216
+ expect(updates[updates.length - 1].asyncStatus).toBe('completed');
217
+ expect(manager.getByTaskId('task-complete')).toBeUndefined();
218
+ });
219
+
220
+ it('finalizes to error when AgentOutputTool result has isError=true', () => {
221
+ const { manager, updates } = createManager();
222
+ const parentEl = createMockEl();
223
+
224
+ manager.handleTaskToolUse('task-err', { description: 'Background', run_in_background: true }, parentEl);
225
+ manager.handleTaskToolResult('task-err', JSON.stringify({ agent_id: 'agent-err' }));
226
+
227
+ const toolCall: ToolCallInfo = {
228
+ id: 'output-err',
229
+ name: 'AgentOutputTool',
230
+ input: { agent_id: 'agent-err' },
231
+ status: 'running',
232
+ isExpanded: false,
233
+ };
234
+ manager.handleAgentOutputToolUse(toolCall);
235
+
236
+ const errored = manager.handleAgentOutputToolResult(
237
+ 'output-err',
238
+ 'agent crashed',
239
+ true
240
+ );
241
+
242
+ expect(errored?.asyncStatus).toBe('error');
243
+ expect(errored?.status).toBe('error');
244
+ expect(errored?.result).toBe('agent crashed');
245
+ expect(updates[updates.length - 1].asyncStatus).toBe('error');
246
+ expect(manager.getByTaskId('task-err')).toBeUndefined();
247
+ });
248
+
249
+ it('marks pending and running async subagents as orphaned', () => {
250
+ const { manager } = createManager();
251
+ const parentEl = createMockEl();
252
+
253
+ manager.handleTaskToolUse('pending-task', { description: 'Pending task', run_in_background: true }, parentEl);
254
+ manager.handleTaskToolUse('running-task', { description: 'Running task', run_in_background: true }, parentEl);
255
+ manager.handleTaskToolResult('running-task', JSON.stringify({ agent_id: 'agent-running' }));
256
+
257
+ const orphaned = manager.orphanAllActive();
258
+
259
+ expect(orphaned).toHaveLength(2);
260
+ orphaned.forEach((subagent) => {
261
+ expect(subagent.asyncStatus).toBe('orphaned');
262
+ expect(subagent.result).toContain('Conversation ended');
263
+ });
264
+ expect(manager.getByTaskId('pending-task')).toBeUndefined();
265
+ expect(manager.getByTaskId('running-task')).toBeUndefined();
266
+ });
267
+
268
+ it('ignores Task results for unknown tasks', () => {
269
+ const { manager } = createManager();
270
+
271
+ manager.handleTaskToolResult('missing-task', 'agent_id: x');
272
+
273
+ expect(manager.getByTaskId('missing-task')).toBeUndefined();
274
+ });
275
+
276
+ it('ignores AgentOutputTool when missing agentId', () => {
277
+ const { manager } = createManager();
278
+
279
+ manager.handleAgentOutputToolUse({
280
+ id: 'output-1',
281
+ name: 'AgentOutputTool',
282
+ input: {},
283
+ status: 'running',
284
+ isExpanded: false,
285
+ });
286
+
287
+ expect(manager.isLinkedAgentOutputTool('output-1')).toBe(false);
288
+ });
289
+
290
+ it('ignores AgentOutputTool when referencing unknown agent', () => {
291
+ const { manager } = createManager();
292
+
293
+ manager.handleAgentOutputToolUse({
294
+ id: 'output-unknown',
295
+ name: 'AgentOutputTool',
296
+ input: { agent_id: 'agent-x' },
297
+ status: 'running',
298
+ isExpanded: false,
299
+ });
300
+
301
+ expect(manager.isLinkedAgentOutputTool('output-unknown')).toBe(false);
302
+ });
303
+
304
+ it('handles TaskOutput with task_id parameter (SDK format)', () => {
305
+ const { manager, updates } = createManager();
306
+ const parentEl = createMockEl();
307
+
308
+ manager.handleTaskToolUse('task-sdk', { description: 'SDK test', run_in_background: true }, parentEl);
309
+ manager.handleTaskToolResult('task-sdk', JSON.stringify({ agent_id: 'agent-sdk-123' }));
310
+
311
+ const toolCall: ToolCallInfo = {
312
+ id: 'taskoutput-1',
313
+ name: 'TaskOutput',
314
+ input: { task_id: 'agent-sdk-123' },
315
+ status: 'running',
316
+ isExpanded: false,
317
+ };
318
+ manager.handleAgentOutputToolUse(toolCall);
319
+
320
+ expect(manager.isLinkedAgentOutputTool('taskoutput-1')).toBe(true);
321
+
322
+ const completed = manager.handleAgentOutputToolResult(
323
+ 'taskoutput-1',
324
+ JSON.stringify({
325
+ retrieval_status: 'success',
326
+ agents: { 'agent-sdk-123': { status: 'completed', result: 'task_id works!' } },
327
+ }),
328
+ false
329
+ );
330
+
331
+ expect(completed?.asyncStatus).toBe('completed');
332
+ expect(completed?.result).toBe('task_id works!');
333
+ expect(updates[updates.length - 1].asyncStatus).toBe('completed');
334
+ });
335
+
336
+ it('returns undefined on invalid AgentOutputTool state transition', () => {
337
+ const { manager } = createManager();
338
+ const parentEl = createMockEl();
339
+
340
+ manager.handleTaskToolUse('task-done', { description: 'Background', run_in_background: true }, parentEl);
341
+ manager.handleTaskToolResult('task-done', JSON.stringify({ agent_id: 'agent-done' }));
342
+
343
+ manager.handleAgentOutputToolUse({
344
+ id: 'output-any',
345
+ name: 'AgentOutputTool',
346
+ input: { agent_id: 'agent-done' },
347
+ status: 'running',
348
+ isExpanded: false,
349
+ });
350
+
351
+ // Manually mark completed to force invalid transition
352
+ const sub = manager.getByTaskId('task-done')!;
353
+ sub.asyncStatus = 'completed';
354
+
355
+ const res = manager.handleAgentOutputToolResult('output-any', '{"retrieval_status":"success"}', false);
356
+ expect(res).toBeUndefined();
357
+ });
358
+
359
+ it('treats plain text not_ready as still running', () => {
360
+ const { manager } = createManager();
361
+ const parentEl = createMockEl();
362
+
363
+ manager.handleTaskToolUse('task-plain', { description: 'Background', run_in_background: true }, parentEl);
364
+ manager.handleTaskToolResult('task-plain', JSON.stringify({ agent_id: 'agent-plain' }));
365
+
366
+ const toolCall: ToolCallInfo = {
367
+ id: 'output-plain',
368
+ name: 'AgentOutputTool',
369
+ input: { agent_id: 'agent-plain' },
370
+ status: 'running',
371
+ isExpanded: false,
372
+ };
373
+ manager.handleAgentOutputToolUse(toolCall);
374
+
375
+ const running = manager.handleAgentOutputToolResult('output-plain', 'not ready', false);
376
+ expect(running?.asyncStatus).toBe('running');
377
+ });
378
+
379
+ it('treats XML-style status running as still running', () => {
380
+ const { manager } = createManager();
381
+ const parentEl = createMockEl();
382
+
383
+ manager.handleTaskToolUse('task-xml', { description: 'Background', run_in_background: true }, parentEl);
384
+ manager.handleTaskToolResult('task-xml', JSON.stringify({ agent_id: 'agent-xml' }));
385
+
386
+ const toolCall: ToolCallInfo = {
387
+ id: 'output-xml',
388
+ name: 'AgentOutputTool',
389
+ input: { agent_id: 'agent-xml' },
390
+ status: 'running',
391
+ isExpanded: false,
392
+ };
393
+ manager.handleAgentOutputToolUse(toolCall);
394
+
395
+ const xmlResult = `<retrieval_status>not_ready</retrieval_status>
396
+ <task_id>agent-xml</task_id>
397
+ <task_type>local_agent</task_type>
398
+ <status>running</status>`;
399
+
400
+ const running = manager.handleAgentOutputToolResult('output-xml', xmlResult, false);
401
+ expect(running?.asyncStatus).toBe('running');
402
+ });
403
+
404
+ it('extracts first agent result when agentId is missing', () => {
405
+ const { manager } = createManager();
406
+ const parentEl = createMockEl();
407
+
408
+ manager.handleTaskToolUse('task-first', { description: 'Background', run_in_background: true }, parentEl);
409
+ manager.handleTaskToolResult('task-first', JSON.stringify({ agent_id: 'agent-first' }));
410
+
411
+ const toolCall: ToolCallInfo = {
412
+ id: 'output-first',
413
+ name: 'AgentOutputTool',
414
+ input: { agent_id: 'agent-first' },
415
+ status: 'running',
416
+ isExpanded: false,
417
+ };
418
+ manager.handleAgentOutputToolUse(toolCall);
419
+
420
+ const completed = manager.handleAgentOutputToolResult(
421
+ 'output-first',
422
+ JSON.stringify({ retrieval_status: 'success', agents: { other: { status: 'completed', result: 'ok' } } }),
423
+ false
424
+ );
425
+
426
+ expect(completed?.result).toBe('ok');
427
+ });
428
+
429
+ it('infers agentId from AgentOutputTool result when not linked', () => {
430
+ const { manager } = createManager();
431
+ const parentEl = createMockEl();
432
+
433
+ manager.handleTaskToolUse('task-infer', { description: 'Background', run_in_background: true }, parentEl);
434
+ manager.handleTaskToolResult('task-infer', JSON.stringify({ agent_id: 'agent-infer' }));
435
+
436
+ const result = JSON.stringify({
437
+ retrieval_status: 'success',
438
+ agents: { 'agent-infer': { status: 'completed', result: 'ok' } },
439
+ });
440
+
441
+ const completed = manager.handleAgentOutputToolResult('unlinked', result, false);
442
+ expect(completed?.asyncStatus).toBe('completed');
443
+ expect(completed?.result).toBe('ok');
444
+ });
445
+
446
+ it('gets running subagent by task id after transition', () => {
447
+ const { manager } = createManager();
448
+ const parentEl = createMockEl();
449
+
450
+ manager.handleTaskToolUse('task-map', { description: 'Background', run_in_background: true }, parentEl);
451
+ manager.handleTaskToolResult('task-map', JSON.stringify({ agent_id: 'agent-map' }));
452
+
453
+ expect(manager.getByTaskId('task-map')?.agentId).toBe('agent-map');
454
+ });
455
+ });
456
+
457
+ // ============================================
458
+ // Async Parsing Edge Cases (via public API)
459
+ // ============================================
460
+
461
+ describe('async parsing edge cases', () => {
462
+ const setupLinkedAgentOutput = (
463
+ manager: ReturnType<typeof createManager>['manager'],
464
+ taskId: string,
465
+ agentId: string,
466
+ outputToolId: string
467
+ ) => {
468
+ const parentEl = createMockEl();
469
+ manager.handleTaskToolUse(taskId, { description: 'Background', run_in_background: true }, parentEl);
470
+ manager.handleTaskToolResult(taskId, JSON.stringify({ agent_id: agentId }));
471
+ manager.handleAgentOutputToolUse({
472
+ id: outputToolId,
473
+ name: 'AgentOutputTool',
474
+ input: { agent_id: agentId },
475
+ status: 'running',
476
+ isExpanded: false,
477
+ });
478
+ };
479
+
480
+ // ---- still-running detection with envelope forms ----
481
+
482
+ it('stays running with array envelope containing not_ready', () => {
483
+ const { manager } = createManager();
484
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
485
+
486
+ const arrayEnvelope = JSON.stringify([
487
+ { text: JSON.stringify({ retrieval_status: 'not_ready', agents: {} }) },
488
+ ]);
489
+ const result = manager.handleAgentOutputToolResult('out-1', arrayEnvelope, false);
490
+ expect(result?.asyncStatus).toBe('running');
491
+ });
492
+
493
+ it('stays running with object envelope containing running status', () => {
494
+ const { manager } = createManager();
495
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
496
+
497
+ const objectEnvelope = JSON.stringify({
498
+ text: JSON.stringify({ retrieval_status: 'running', agents: {} }),
499
+ });
500
+ const result = manager.handleAgentOutputToolResult('out-1', objectEnvelope, false);
501
+ expect(result?.asyncStatus).toBe('running');
502
+ });
503
+
504
+ it('finalizes when result is whitespace-only', () => {
505
+ const { manager } = createManager();
506
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
507
+
508
+ const result = manager.handleAgentOutputToolResult('out-1', ' ', false);
509
+ expect(result?.asyncStatus).toBe('completed');
510
+ });
511
+
512
+ it('finalizes to error when isError is true regardless of content', () => {
513
+ const { manager } = createManager();
514
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
515
+
516
+ const result = manager.handleAgentOutputToolResult('out-1', 'whatever', true);
517
+ expect(result?.asyncStatus).toBe('error');
518
+ });
519
+
520
+ it('finalizes when retrieval_status is success without agents', () => {
521
+ const { manager } = createManager();
522
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
523
+
524
+ const result = manager.handleAgentOutputToolResult(
525
+ 'out-1',
526
+ JSON.stringify({ retrieval_status: 'success' }),
527
+ false
528
+ );
529
+ expect(result?.asyncStatus).toBe('completed');
530
+ });
531
+
532
+ it('finalizes when retrieval_status is unknown', () => {
533
+ const { manager } = createManager();
534
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
535
+
536
+ const result = manager.handleAgentOutputToolResult(
537
+ 'out-1',
538
+ JSON.stringify({ retrieval_status: 'unknown' }),
539
+ false
540
+ );
541
+ expect(result?.asyncStatus).toBe('completed');
542
+ });
543
+
544
+ it('marks error when toolUseResult has retrieval_status error', () => {
545
+ const { manager } = createManager();
546
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
547
+
548
+ const toolUseResult = {
549
+ retrieval_status: 'error',
550
+ error: 'Agent process crashed',
551
+ };
552
+
553
+ const result = manager.handleAgentOutputToolResult(
554
+ 'out-1',
555
+ '{}',
556
+ false,
557
+ toolUseResult
558
+ );
559
+ expect(result?.asyncStatus).toBe('error');
560
+ expect(result?.result).toBe('Error: Agent process crashed');
561
+ });
562
+
563
+ it('marks error when retrieval_status is error without error field', () => {
564
+ const { manager } = createManager();
565
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
566
+
567
+ const toolUseResult = {
568
+ retrieval_status: 'error',
569
+ };
570
+
571
+ const result = manager.handleAgentOutputToolResult(
572
+ 'out-1',
573
+ '{}',
574
+ false,
575
+ toolUseResult
576
+ );
577
+ expect(result?.asyncStatus).toBe('error');
578
+ expect(result?.result).toBe('Error: Task retrieval failed');
579
+ });
580
+
581
+ it('finalizes with plain text as result when no running indicators', () => {
582
+ const { manager } = createManager();
583
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-1', 'out-1');
584
+
585
+ const result = manager.handleAgentOutputToolResult('out-1', 'plain output', false);
586
+ expect(result?.asyncStatus).toBe('completed');
587
+ expect(result?.result).toBe('plain output');
588
+ });
589
+
590
+ // ---- result extraction with envelope forms ----
591
+
592
+ it('extracts result from array envelope', () => {
593
+ const { manager } = createManager();
594
+ setupLinkedAgentOutput(manager, 'task-1', 'a', 'out-1');
595
+
596
+ const payloadArray = JSON.stringify([
597
+ { text: JSON.stringify({ retrieval_status: 'success', agents: { a: { result: 'R' } } }) },
598
+ ]);
599
+ const result = manager.handleAgentOutputToolResult('out-1', payloadArray, false);
600
+ expect(result?.result).toBe('R');
601
+ });
602
+
603
+ it('extracts result from object envelope', () => {
604
+ const { manager } = createManager();
605
+ setupLinkedAgentOutput(manager, 'task-1', 'a', 'out-1');
606
+
607
+ const payloadObject = JSON.stringify({
608
+ text: JSON.stringify({ retrieval_status: 'success', agents: { a: { status: 'completed' } } }),
609
+ });
610
+ const result = manager.handleAgentOutputToolResult('out-1', payloadObject, false);
611
+ expect(result?.result).toContain('completed');
612
+ });
613
+
614
+ it('extracts only final assistant result from XML output payload', () => {
615
+ const { manager } = createManager();
616
+ setupLinkedAgentOutput(manager, 'task-1', 'a6ac482', 'out-1');
617
+
618
+ const outputLines = [
619
+ JSON.stringify({
620
+ type: 'assistant',
621
+ message: {
622
+ role: 'assistant',
623
+ content: [{ type: 'text', text: 'I will search first.' }],
624
+ },
625
+ }),
626
+ JSON.stringify({
627
+ type: 'assistant',
628
+ message: {
629
+ role: 'assistant',
630
+ content: [{ type: 'tool_use', id: 'tool-1', name: 'Grep', input: { pattern: 'martini' } }],
631
+ },
632
+ }),
633
+ JSON.stringify({
634
+ type: 'assistant',
635
+ message: {
636
+ role: 'assistant',
637
+ content: [{ type: 'text', text: 'Final answer: 24 matches across 6 files.' }],
638
+ },
639
+ }),
640
+ ].join('\n');
641
+
642
+ const xmlPayload = `<retrieval_status>success</retrieval_status>
643
+ <task_id>a6ac482</task_id>
644
+ <status>completed</status>
645
+ <output>
646
+ ${outputLines}
647
+ </output>`;
648
+
649
+ const result = manager.handleAgentOutputToolResult('out-1', xmlPayload, false);
650
+ expect(result?.result).toBe('Final answer: 24 matches across 6 files.');
651
+ });
652
+
653
+ it('extracts final assistant result from structured toolUseResult.task.result', () => {
654
+ const { manager } = createManager();
655
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-structured', 'out-1');
656
+
657
+ const outputLines = [
658
+ JSON.stringify({
659
+ type: 'assistant',
660
+ message: {
661
+ role: 'assistant',
662
+ content: [{ type: 'text', text: 'Intermediate step' }],
663
+ },
664
+ }),
665
+ JSON.stringify({
666
+ type: 'assistant',
667
+ message: {
668
+ role: 'assistant',
669
+ content: [{ type: 'text', text: 'Final summary from structured result.' }],
670
+ },
671
+ }),
672
+ ].join('\n');
673
+
674
+ const structuredToolUseResult = {
675
+ retrieval_status: 'success',
676
+ task: {
677
+ task_id: 'agent-structured',
678
+ status: 'completed',
679
+ result: outputLines,
680
+ output: outputLines,
681
+ },
682
+ };
683
+
684
+ const result = manager.handleAgentOutputToolResult(
685
+ 'out-1',
686
+ '{"retrieval_status":"success"}',
687
+ false,
688
+ structuredToolUseResult
689
+ );
690
+ expect(result?.result).toBe('Final summary from structured result.');
691
+ });
692
+
693
+ it('extracts full result from SDK toolUseResult.content array', () => {
694
+ const { manager } = createManager();
695
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-sdk-content', 'out-1');
696
+
697
+ const fullResult = 'This is the full multi-line result.\n\nLine 2 of the result.\nLine 3 with details.';
698
+ const sdkToolUseResult = {
699
+ status: 'completed',
700
+ content: [
701
+ { type: 'text', text: fullResult },
702
+ ],
703
+ agentId: 'agent-sdk-content',
704
+ prompt: 'Do something',
705
+ totalDurationMs: 5000,
706
+ totalTokens: 1000,
707
+ totalToolUseCount: 5,
708
+ };
709
+
710
+ const result = manager.handleAgentOutputToolResult(
711
+ 'out-1',
712
+ '{}',
713
+ false,
714
+ sdkToolUseResult
715
+ );
716
+ expect(result?.result).toBe(fullResult);
717
+ });
718
+
719
+ it('extracts result from SDK content array with multiple text blocks', () => {
720
+ const { manager } = createManager();
721
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-multi', 'out-1');
722
+
723
+ const sdkToolUseResult = {
724
+ status: 'completed',
725
+ content: [
726
+ { type: 'text', text: 'Main result text here.' },
727
+ { type: 'text', text: 'agentId: agent-multi\n<usage>total_tokens: 100</usage>' },
728
+ ],
729
+ agentId: 'agent-multi',
730
+ };
731
+
732
+ const result = manager.handleAgentOutputToolResult(
733
+ 'out-1',
734
+ '{}',
735
+ false,
736
+ sdkToolUseResult
737
+ );
738
+ // Should return the first text block (actual result), not the metadata block
739
+ expect(result?.result).toBe('Main result text here.');
740
+ });
741
+
742
+ it('reads full output file when inline output is truncated', () => {
743
+ const { manager } = createManager();
744
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-full-output', 'out-1');
745
+
746
+ const tempDir = mkdtempSync(join(tmpdir(), 'subagent-output-'));
747
+ const fullOutputFile = join(tempDir, 'agent.output');
748
+ const fullOutput = [
749
+ JSON.stringify({
750
+ type: 'assistant',
751
+ message: {
752
+ role: 'assistant',
753
+ content: [{ type: 'text', text: 'Recovered final answer from full output file.' }],
754
+ },
755
+ }),
756
+ ].join('\n');
757
+ writeFileSync(fullOutputFile, fullOutput, 'utf-8');
758
+
759
+ const inlineOutput = `[Truncated. Full output: ${fullOutputFile}]`;
760
+ const xmlPayload = `<retrieval_status>success</retrieval_status>
761
+ <task_id>agent-full-output</task_id>
762
+ <status>completed</status>
763
+ <output>
764
+ ${inlineOutput}
765
+ </output>`;
766
+
767
+ try {
768
+ const result = manager.handleAgentOutputToolResult('out-1', xmlPayload, false);
769
+ expect(result?.result).toBe('Recovered final answer from full output file.');
770
+ } finally {
771
+ rmSync(tempDir, { recursive: true, force: true });
772
+ }
773
+ });
774
+
775
+ it('ignores truncated full output files outside trusted temp roots', () => {
776
+ const { manager } = createManager();
777
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-untrusted-output', 'out-1');
778
+
779
+ const homeDir = process.env.HOME ?? process.cwd();
780
+ const untrustedDir = mkdtempSync(join(homeDir, '.claudian-untrusted-'));
781
+ const fullOutputFile = join(untrustedDir, 'agent-untrusted.output');
782
+ const fullOutput = [
783
+ JSON.stringify({
784
+ type: 'assistant',
785
+ message: {
786
+ role: 'assistant',
787
+ content: [{ type: 'text', text: 'Should not be loaded from untrusted path.' }],
788
+ },
789
+ }),
790
+ ].join('\n');
791
+ writeFileSync(fullOutputFile, fullOutput, 'utf-8');
792
+
793
+ const inlineOutput = `[Truncated. Full output: ${fullOutputFile}]`;
794
+ const xmlPayload = `<retrieval_status>success</retrieval_status>
795
+ <task_id>agent-untrusted-output</task_id>
796
+ <status>completed</status>
797
+ <output>
798
+ ${inlineOutput}
799
+ </output>`;
800
+
801
+ try {
802
+ const result = manager.handleAgentOutputToolResult('out-1', xmlPayload, false);
803
+ expect(result?.result).toBe(inlineOutput);
804
+ } finally {
805
+ rmSync(untrustedDir, { recursive: true, force: true });
806
+ }
807
+ });
808
+
809
+ it('extracts direct result tag when present', () => {
810
+ const { manager } = createManager();
811
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-x', 'out-1');
812
+
813
+ const taggedPayload = `<status>completed</status>
814
+ <result>
815
+ Only this is the final result.
816
+ </result>`;
817
+
818
+ const result = manager.handleAgentOutputToolResult('out-1', taggedPayload, false);
819
+ expect(result?.result).toBe('Only this is the final result.');
820
+ });
821
+
822
+ it('falls back to first agent when agentId is missing from agents map', () => {
823
+ const { manager } = createManager();
824
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-x', 'out-1');
825
+
826
+ const fallback = JSON.stringify({
827
+ retrieval_status: 'success',
828
+ agents: { first: { status: 'completed' } },
829
+ });
830
+ const result = manager.handleAgentOutputToolResult('out-1', fallback, false);
831
+ expect(result?.result).toContain('completed');
832
+ });
833
+
834
+ it('returns raw payload when no agents key is present', () => {
835
+ const { manager } = createManager();
836
+ setupLinkedAgentOutput(manager, 'task-1', 'agent-x', 'out-1');
837
+
838
+ const noAgents = JSON.stringify({ foo: 'bar' });
839
+ const result = manager.handleAgentOutputToolResult('out-1', noAgents, false);
840
+ expect(result?.result).toBe(noAgents);
841
+ });
842
+
843
+ // ---- agent ID parsing from multiple JSON shapes ----
844
+
845
+ it('parses camelCase agentId from task result', () => {
846
+ const { manager } = createManager();
847
+ const parentEl = createMockEl();
848
+ manager.handleTaskToolUse('task-1', { description: 'Bg', run_in_background: true }, parentEl);
849
+
850
+ manager.handleTaskToolResult('task-1', JSON.stringify({ agentId: 'camel' }));
851
+ expect(manager.getByTaskId('task-1')).toBeDefined();
852
+ expect(manager.getByTaskId('task-1')?.agentId).toBe('camel');
853
+ });
854
+
855
+ it('parses nested data.agent_id from task result', () => {
856
+ const { manager } = createManager();
857
+ const parentEl = createMockEl();
858
+ manager.handleTaskToolUse('task-1', { description: 'Bg', run_in_background: true }, parentEl);
859
+
860
+ manager.handleTaskToolResult('task-1', JSON.stringify({ data: { agent_id: 'nested' } }));
861
+ expect(manager.getByTaskId('task-1')?.agentId).toBe('nested');
862
+ });
863
+
864
+ it('parses id field from task result', () => {
865
+ const { manager } = createManager();
866
+ const parentEl = createMockEl();
867
+ manager.handleTaskToolUse('task-1', { description: 'Bg', run_in_background: true }, parentEl);
868
+
869
+ manager.handleTaskToolResult('task-1', JSON.stringify({ id: 'idfield' }));
870
+ expect(manager.getByTaskId('task-1')?.agentId).toBe('idfield');
871
+ });
872
+
873
+ it('parses unicode-escaped agent_id from task result', () => {
874
+ const { manager } = createManager();
875
+ const parentEl = createMockEl();
876
+ manager.handleTaskToolUse('task-1', { description: 'Bg', run_in_background: true }, parentEl);
877
+
878
+ manager.handleTaskToolResult('task-1', '{"agent\\u005fid":"escaped"}');
879
+ expect(manager.getByTaskId('task-1')?.agentId).toBe('escaped');
880
+ });
881
+
882
+ it('parses nested unicode-escaped agent_id from task result', () => {
883
+ const { manager } = createManager();
884
+ const parentEl = createMockEl();
885
+ manager.handleTaskToolUse('task-1', { description: 'Bg', run_in_background: true }, parentEl);
886
+
887
+ manager.handleTaskToolResult('task-1', '{"data": {"agent\\u005fid": "nested2"}}');
888
+ expect(manager.getByTaskId('task-1')?.agentId).toBe('nested2');
889
+ });
890
+
891
+ it('transitions to error when no recognizable agent ID in task result', () => {
892
+ const { manager, updates } = createManager();
893
+ const parentEl = createMockEl();
894
+ manager.handleTaskToolUse('task-1', { description: 'Bg', run_in_background: true }, parentEl);
895
+
896
+ manager.handleTaskToolResult('task-1', '{"foo": "bar"}');
897
+ const last = updates[updates.length - 1];
898
+ expect(last.asyncStatus).toBe('error');
899
+ expect(last.result).toContain('Failed to parse agent_id');
900
+ });
901
+ });
902
+
903
+ // ============================================
904
+ // Unified Task Entry Point
905
+ // ============================================
906
+
907
+ describe('handleTaskToolUse', () => {
908
+ it('buffers task in pendingTasks when currentContentEl is null', () => {
909
+ const { manager } = createManager();
910
+
911
+ const result = manager.handleTaskToolUse('task-1', { prompt: 'test' }, null);
912
+ expect(result.action).toBe('buffered');
913
+ expect(manager.hasPendingTask('task-1')).toBe(true);
914
+ });
915
+
916
+ it('renders task buffered with null parentEl once contentEl becomes available', () => {
917
+ const { manager } = createManager();
918
+ const parentEl = createMockEl();
919
+
920
+ // First chunk: no content element
921
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, null);
922
+ expect(manager.hasPendingTask('task-1')).toBe(true);
923
+
924
+ // Second chunk: content element available, run_in_background known
925
+ const result = manager.handleTaskToolUse('task-1', { run_in_background: false }, parentEl);
926
+ expect(result.action).toBe('created_sync');
927
+ expect(manager.hasPendingTask('task-1')).toBe(false);
928
+ });
929
+
930
+ it('returns created_sync for run_in_background=false', () => {
931
+ const { manager } = createManager();
932
+ const parentEl = createMockEl();
933
+
934
+ const result = manager.handleTaskToolUse(
935
+ 'task-sync',
936
+ { prompt: 'test', run_in_background: false },
937
+ parentEl
938
+ );
939
+
940
+ expect(result.action).toBe('created_sync');
941
+ expect((result as any).subagentState.info.id).toBe('task-sync');
942
+ });
943
+
944
+ it('returns created_async for run_in_background=true', () => {
945
+ const { manager } = createManager();
946
+ const parentEl = createMockEl();
947
+
948
+ const result = manager.handleTaskToolUse(
949
+ 'task-async',
950
+ { description: 'Background', run_in_background: true },
951
+ parentEl
952
+ );
953
+
954
+ expect(result.action).toBe('created_async');
955
+ expect((result as any).info.id).toBe('task-async');
956
+ expect((result as any).info.asyncStatus).toBe('pending');
957
+ });
958
+
959
+ it('buffers task when run_in_background is missing', () => {
960
+ const { manager } = createManager();
961
+ const parentEl = createMockEl();
962
+
963
+ const result = manager.handleTaskToolUse(
964
+ 'task-unknown',
965
+ { prompt: 'test' },
966
+ parentEl
967
+ );
968
+
969
+ expect(result.action).toBe('buffered');
970
+ expect(manager.hasPendingTask('task-unknown')).toBe(true);
971
+ });
972
+
973
+ it('upgrades buffered task to async when run_in_background=true arrives later', () => {
974
+ const { manager } = createManager();
975
+ const parentEl = createMockEl();
976
+
977
+ const first = manager.handleTaskToolUse(
978
+ 'task-upgrade',
979
+ { prompt: 'test' },
980
+ parentEl
981
+ );
982
+ expect(first.action).toBe('buffered');
983
+ expect(manager.hasPendingTask('task-upgrade')).toBe(true);
984
+
985
+ const second = manager.handleTaskToolUse(
986
+ 'task-upgrade',
987
+ { run_in_background: true, description: 'Background' },
988
+ parentEl
989
+ );
990
+
991
+ expect(second.action).toBe('created_async');
992
+ expect((second as any).info.id).toBe('task-upgrade');
993
+ expect(manager.hasPendingTask('task-upgrade')).toBe(false);
994
+ });
995
+
996
+ it('returns label_updated for already rendered sync subagent', () => {
997
+ const { manager } = createManager();
998
+ const parentEl = createMockEl();
999
+
1000
+ // Create sync
1001
+ manager.handleTaskToolUse('task-1', { run_in_background: false, description: 'Initial' }, parentEl);
1002
+
1003
+ // Update input
1004
+ const result = manager.handleTaskToolUse('task-1', { description: 'Updated' }, parentEl);
1005
+ expect(result.action).toBe('label_updated');
1006
+ });
1007
+
1008
+ it('returns label_updated for already rendered async subagent', () => {
1009
+ const { manager } = createManager();
1010
+ const parentEl = createMockEl();
1011
+
1012
+ // Create async
1013
+ manager.handleTaskToolUse('task-1', { run_in_background: true, description: 'Initial' }, parentEl);
1014
+
1015
+ // Update input
1016
+ const result = manager.handleTaskToolUse('task-1', { description: 'Updated' }, parentEl);
1017
+ expect(result.action).toBe('label_updated');
1018
+ });
1019
+
1020
+ it('syncs async label update to canonical SubagentInfo', () => {
1021
+ const { manager } = createManager();
1022
+ const parentEl = createMockEl();
1023
+
1024
+ manager.handleTaskToolUse('task-1', { run_in_background: true, description: 'Initial' }, parentEl);
1025
+
1026
+ // Canonical info should have initial description
1027
+ expect(manager.getByTaskId('task-1')?.description).toBe('Initial');
1028
+
1029
+ // Update label via streaming input
1030
+ manager.handleTaskToolUse('task-1', { description: 'Updated description' }, parentEl);
1031
+
1032
+ // Canonical info should now reflect the update
1033
+ expect(manager.getByTaskId('task-1')?.description).toBe('Updated description');
1034
+ });
1035
+
1036
+ it('propagates prompt updates in label update', () => {
1037
+ const { manager } = createManager();
1038
+ const parentEl = createMockEl();
1039
+
1040
+ manager.handleTaskToolUse('task-1', { run_in_background: true, description: 'Bg', prompt: 'initial' }, parentEl);
1041
+
1042
+ // Update prompt via streaming input
1043
+ manager.handleTaskToolUse('task-1', { prompt: 'full prompt text' }, parentEl);
1044
+
1045
+ // Canonical info should have updated prompt
1046
+ expect(manager.getByTaskId('task-1')?.prompt).toBe('full prompt text');
1047
+ });
1048
+
1049
+ it('merges buffered input and renders once content element becomes available', () => {
1050
+ const { manager } = createManager();
1051
+ const parentEl = createMockEl();
1052
+
1053
+ // First chunk without content target must be buffered.
1054
+ manager.handleTaskToolUse('task-1', { description: 'Initial description' }, null);
1055
+ expect(manager.hasPendingTask('task-1')).toBe(true);
1056
+
1057
+ // Second chunk arrives with a content target, additional input, and confirmed mode.
1058
+ const result = manager.handleTaskToolUse(
1059
+ 'task-1',
1060
+ { prompt: 'latest prompt', run_in_background: false },
1061
+ parentEl
1062
+ );
1063
+
1064
+ expect(result.action).toBe('created_sync');
1065
+ expect((result as any).subagentState.info.description).toBe('Initial description');
1066
+ expect((result as any).subagentState.info.prompt).toBe('latest prompt');
1067
+ expect(manager.hasPendingTask('task-1')).toBe(false);
1068
+ });
1069
+
1070
+ it('increments spawned count when creating sync task', () => {
1071
+ const { manager } = createManager();
1072
+ const parentEl = createMockEl();
1073
+
1074
+ expect(manager.subagentsSpawnedThisStream).toBe(0);
1075
+ manager.handleTaskToolUse('task-1', { run_in_background: false }, parentEl);
1076
+ expect(manager.subagentsSpawnedThisStream).toBe(1);
1077
+ });
1078
+
1079
+ it('increments spawned count when creating async task', () => {
1080
+ const { manager } = createManager();
1081
+ const parentEl = createMockEl();
1082
+
1083
+ expect(manager.subagentsSpawnedThisStream).toBe(0);
1084
+ manager.handleTaskToolUse('task-1', { run_in_background: true }, parentEl);
1085
+ expect(manager.subagentsSpawnedThisStream).toBe(1);
1086
+ });
1087
+ });
1088
+
1089
+ // ============================================
1090
+ // Pending Task Resolution
1091
+ // ============================================
1092
+
1093
+ describe('renderPendingTask', () => {
1094
+ it('returns null for unknown tool id', () => {
1095
+ const { manager } = createManager();
1096
+
1097
+ const result = manager.renderPendingTask('unknown');
1098
+ expect(result).toBeNull();
1099
+ });
1100
+
1101
+ it('renders buffered sync task and increments counter', () => {
1102
+ const { manager } = createManager();
1103
+ const parentEl = createMockEl();
1104
+
1105
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, null);
1106
+
1107
+ const result = manager.renderPendingTask('task-1', parentEl);
1108
+ expect(result).not.toBeNull();
1109
+ expect(result?.mode).toBe('sync');
1110
+ expect(manager.hasPendingTask('task-1')).toBe(false);
1111
+ expect(manager.subagentsSpawnedThisStream).toBe(1);
1112
+ });
1113
+
1114
+ it('returns null and keeps task pending when targetEl is null', () => {
1115
+ const { manager } = createManager();
1116
+
1117
+ // Buffer with null parentEl (no content element)
1118
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, null);
1119
+ expect(manager.hasPendingTask('task-1')).toBe(true);
1120
+
1121
+ // Try to render without override — both parentEl and override are null
1122
+ const result = manager.renderPendingTask('task-1');
1123
+ expect(result).toBeNull();
1124
+ expect(manager.hasPendingTask('task-1')).toBe(true);
1125
+ expect(manager.subagentsSpawnedThisStream).toBe(0);
1126
+ });
1127
+
1128
+ it('renders buffered async task with parentEl override', () => {
1129
+ const { manager } = createManager();
1130
+ const overrideEl = createMockEl();
1131
+
1132
+ // Buffer with null parentEl so the task stays pending despite run_in_background being known
1133
+ manager.handleTaskToolUse('task-1', { prompt: 'test', run_in_background: true }, null);
1134
+ expect(manager.hasPendingTask('task-1')).toBe(true);
1135
+
1136
+ const result = manager.renderPendingTask('task-1', overrideEl);
1137
+ expect(result).not.toBeNull();
1138
+ expect(result?.mode).toBe('async');
1139
+ });
1140
+
1141
+ it('does not increment spawned counter when rendering throws', () => {
1142
+ const { createSubagentBlock } = jest.requireMock('@/features/chat/rendering/SubagentRenderer');
1143
+ createSubagentBlock.mockImplementationOnce(() => { throw new Error('DOM error'); });
1144
+
1145
+ const { manager } = createManager();
1146
+ const parentEl = createMockEl();
1147
+
1148
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, null);
1149
+ expect(manager.subagentsSpawnedThisStream).toBe(0);
1150
+
1151
+ const result = manager.renderPendingTask('task-1', parentEl);
1152
+ expect(result).toBeNull();
1153
+ expect(manager.subagentsSpawnedThisStream).toBe(0);
1154
+ });
1155
+ });
1156
+
1157
+ describe('renderPendingTaskFromTaskResult', () => {
1158
+ it('returns null for unknown tool id', () => {
1159
+ const { manager } = createManager();
1160
+ const result = manager.renderPendingTaskFromTaskResult('unknown', 'ok', false);
1161
+ expect(result).toBeNull();
1162
+ });
1163
+
1164
+ it('infers async from agent_id markers when mode is still unknown', () => {
1165
+ const { manager } = createManager();
1166
+ const parentEl = createMockEl();
1167
+
1168
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1169
+ const result = manager.renderPendingTaskFromTaskResult(
1170
+ 'task-1',
1171
+ '{"agent_id":"agent-123"}',
1172
+ false
1173
+ );
1174
+
1175
+ expect(result).not.toBeNull();
1176
+ expect(result?.mode).toBe('async');
1177
+ expect(manager.hasPendingTask('task-1')).toBe(false);
1178
+ });
1179
+
1180
+ it('infers async from structured task-result content when mode is still unknown', () => {
1181
+ const { manager } = createManager();
1182
+ const parentEl = createMockEl();
1183
+
1184
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1185
+ const result = manager.renderPendingTaskFromTaskResult(
1186
+ 'task-1',
1187
+ [{ type: 'text', text: '{"agent_id":"agent-structured"}' }] as any,
1188
+ false
1189
+ );
1190
+
1191
+ expect(result).not.toBeNull();
1192
+ expect(result?.mode).toBe('async');
1193
+ expect(manager.hasPendingTask('task-1')).toBe(false);
1194
+ });
1195
+
1196
+ it('falls back to sync when no async evidence is present', () => {
1197
+ const { manager } = createManager();
1198
+ const parentEl = createMockEl();
1199
+
1200
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1201
+ const result = manager.renderPendingTaskFromTaskResult(
1202
+ 'task-1',
1203
+ '{"foo":"bar"}',
1204
+ false
1205
+ );
1206
+
1207
+ expect(result).not.toBeNull();
1208
+ expect(result?.mode).toBe('sync');
1209
+ expect(manager.getSyncSubagent('task-1')).toBeDefined();
1210
+ });
1211
+
1212
+ it('honors explicit async mode from input even without task-result markers', () => {
1213
+ const { manager } = createManager();
1214
+ const parentEl = createMockEl();
1215
+
1216
+ manager.handleTaskToolUse(
1217
+ 'task-1',
1218
+ { prompt: 'test', run_in_background: true },
1219
+ null
1220
+ );
1221
+ const result = manager.renderPendingTaskFromTaskResult(
1222
+ 'task-1',
1223
+ '{"foo":"bar"}',
1224
+ false,
1225
+ parentEl
1226
+ );
1227
+
1228
+ expect(result).not.toBeNull();
1229
+ expect(result?.mode).toBe('async');
1230
+ });
1231
+
1232
+ it('infers async from toolUseResult markers when task-result text has no agent id', () => {
1233
+ const { manager } = createManager();
1234
+ const parentEl = createMockEl();
1235
+
1236
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1237
+ const result = manager.renderPendingTaskFromTaskResult(
1238
+ 'task-1',
1239
+ 'Launching task...',
1240
+ false,
1241
+ parentEl,
1242
+ {
1243
+ isAsync: true,
1244
+ status: 'async_launched',
1245
+ agentId: 'agent-xyz',
1246
+ }
1247
+ );
1248
+
1249
+ expect(result).not.toBeNull();
1250
+ expect(result?.mode).toBe('async');
1251
+ });
1252
+
1253
+ it('treats completed toolUseResult metadata with agentId as sync when mode is unknown', () => {
1254
+ const { manager } = createManager();
1255
+ const parentEl = createMockEl();
1256
+
1257
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1258
+ const result = manager.renderPendingTaskFromTaskResult(
1259
+ 'task-1',
1260
+ '{}',
1261
+ false,
1262
+ parentEl,
1263
+ {
1264
+ status: 'completed',
1265
+ agentId: 'agent-sync',
1266
+ content: [
1267
+ { type: 'text', text: 'Full sync result.' },
1268
+ { type: 'text', text: 'agentId: agent-sync' },
1269
+ ],
1270
+ }
1271
+ );
1272
+
1273
+ expect(result).not.toBeNull();
1274
+ expect(result?.mode).toBe('sync');
1275
+ expect(manager.getSyncSubagent('task-1')).toBeDefined();
1276
+ expect(manager.isPendingAsyncTask('task-1')).toBe(false);
1277
+ expect(manager.hasRunningSubagents()).toBe(false);
1278
+ });
1279
+
1280
+ it('treats stringified completed task metadata with agentId as sync when mode is unknown', () => {
1281
+ const { manager } = createManager();
1282
+ const parentEl = createMockEl();
1283
+ const completedToolUseResult = {
1284
+ status: 'completed',
1285
+ agentId: 'agent-sync',
1286
+ };
1287
+
1288
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1289
+ const result = manager.renderPendingTaskFromTaskResult(
1290
+ 'task-1',
1291
+ JSON.stringify(completedToolUseResult, null, 2),
1292
+ false,
1293
+ parentEl,
1294
+ completedToolUseResult,
1295
+ );
1296
+
1297
+ expect(result).not.toBeNull();
1298
+ expect(result?.mode).toBe('sync');
1299
+ expect(manager.getSyncSubagent('task-1')).toBeDefined();
1300
+ expect(manager.isPendingAsyncTask('task-1')).toBe(false);
1301
+ expect(manager.hasRunningSubagents()).toBe(false);
1302
+ });
1303
+
1304
+ it('treats sync result text with agentId metadata as sync when mode is unknown', () => {
1305
+ const { manager } = createManager();
1306
+ const parentEl = createMockEl();
1307
+ const completedToolUseResult = {
1308
+ status: 'completed',
1309
+ agentId: 'agent-sync',
1310
+ content: [
1311
+ { type: 'text', text: 'Full sync result.' },
1312
+ { type: 'text', text: 'agentId: agent-sync\n<usage>total_tokens: 500</usage>' },
1313
+ ],
1314
+ };
1315
+
1316
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1317
+ const result = manager.renderPendingTaskFromTaskResult(
1318
+ 'task-1',
1319
+ 'Full sync result.\nagentId: agent-sync\n<usage>total_tokens: 500</usage>',
1320
+ false,
1321
+ parentEl,
1322
+ completedToolUseResult,
1323
+ );
1324
+
1325
+ expect(result).not.toBeNull();
1326
+ expect(result?.mode).toBe('sync');
1327
+ expect(manager.getSyncSubagent('task-1')).toBeDefined();
1328
+ expect(manager.isPendingAsyncTask('task-1')).toBe(false);
1329
+ expect(manager.hasRunningSubagents()).toBe(false);
1330
+ });
1331
+
1332
+ it('resolves to sync on errored task result when mode is unknown', () => {
1333
+ const { manager } = createManager();
1334
+ const parentEl = createMockEl();
1335
+
1336
+ manager.handleTaskToolUse('task-1', { prompt: 'test' }, parentEl);
1337
+ const result = manager.renderPendingTaskFromTaskResult(
1338
+ 'task-1',
1339
+ '{"agent_id":"agent-123"}',
1340
+ true
1341
+ );
1342
+
1343
+ expect(result).not.toBeNull();
1344
+ expect(result?.mode).toBe('sync');
1345
+ });
1346
+ });
1347
+
1348
+ // ============================================
1349
+ // Sync Subagent Operations
1350
+ // ============================================
1351
+
1352
+ describe('sync subagent operations', () => {
1353
+ it('creates and retrieves sync subagent', () => {
1354
+ const { manager } = createManager();
1355
+ const parentEl = createMockEl();
1356
+
1357
+ manager.handleTaskToolUse('task-1', { run_in_background: false }, parentEl);
1358
+
1359
+ const state = manager.getSyncSubagent('task-1');
1360
+ expect(state).toBeDefined();
1361
+ expect(state?.info.id).toBe('task-1');
1362
+ });
1363
+
1364
+ it('adds tool call to sync subagent', () => {
1365
+ const { addSubagentToolCall } = jest.requireMock('@/features/chat/rendering/SubagentRenderer');
1366
+ const { manager } = createManager();
1367
+ const parentEl = createMockEl();
1368
+
1369
+ manager.handleTaskToolUse('task-1', { run_in_background: false }, parentEl);
1370
+
1371
+ const toolCall: ToolCallInfo = {
1372
+ id: 'read-1',
1373
+ name: 'Read',
1374
+ input: { file_path: 'test.md' },
1375
+ status: 'running',
1376
+ isExpanded: false,
1377
+ };
1378
+ manager.addSyncToolCall('task-1', toolCall);
1379
+
1380
+ expect(addSubagentToolCall).toHaveBeenCalled();
1381
+ });
1382
+
1383
+ it('updates tool result in sync subagent', () => {
1384
+ const { updateSubagentToolResult } = jest.requireMock('@/features/chat/rendering/SubagentRenderer');
1385
+ const { manager } = createManager();
1386
+ const parentEl = createMockEl();
1387
+
1388
+ manager.handleTaskToolUse('task-1', { run_in_background: false }, parentEl);
1389
+
1390
+ const toolCall: ToolCallInfo = {
1391
+ id: 'read-1',
1392
+ name: 'Read',
1393
+ input: {},
1394
+ status: 'completed',
1395
+ isExpanded: false,
1396
+ result: 'file content',
1397
+ };
1398
+ manager.updateSyncToolResult('task-1', 'read-1', toolCall);
1399
+
1400
+ expect(updateSubagentToolResult).toHaveBeenCalled();
1401
+ });
1402
+
1403
+ it('finalizes sync subagent and removes from map', () => {
1404
+ const { finalizeSubagentBlock } = jest.requireMock('@/features/chat/rendering/SubagentRenderer');
1405
+ const { manager } = createManager();
1406
+ const parentEl = createMockEl();
1407
+
1408
+ manager.handleTaskToolUse('task-1', { run_in_background: false }, parentEl);
1409
+
1410
+ const info = manager.finalizeSyncSubagent('task-1', 'done', false);
1411
+
1412
+ expect(info).not.toBeNull();
1413
+ expect(info?.id).toBe('task-1');
1414
+ expect(finalizeSubagentBlock).toHaveBeenCalled();
1415
+ expect(manager.getSyncSubagent('task-1')).toBeUndefined();
1416
+ });
1417
+
1418
+ it('extracts result from SDK toolUseResult.content for sync subagent', () => {
1419
+ const { finalizeSubagentBlock } = jest.requireMock('@/features/chat/rendering/SubagentRenderer');
1420
+ const { manager } = createManager();
1421
+ const parentEl = createMockEl();
1422
+
1423
+ manager.handleTaskToolUse('task-sdk', { run_in_background: false }, parentEl);
1424
+
1425
+ const sdkToolUseResult = {
1426
+ status: 'completed',
1427
+ content: [
1428
+ { type: 'text', text: 'Full sync subagent result with multiple lines.\n\nSecond paragraph.' },
1429
+ { type: 'text', text: 'agentId: agent-sync\n<usage>total_tokens: 500</usage>' },
1430
+ ],
1431
+ agentId: 'agent-sync',
1432
+ };
1433
+
1434
+ const info = manager.finalizeSyncSubagent('task-sdk', '{}', false, sdkToolUseResult);
1435
+
1436
+ expect(info).not.toBeNull();
1437
+ // Verify the extracted result (first content block) was passed to the renderer
1438
+ expect(finalizeSubagentBlock).toHaveBeenCalledWith(
1439
+ expect.anything(),
1440
+ 'Full sync subagent result with multiple lines.\n\nSecond paragraph.',
1441
+ false
1442
+ );
1443
+ });
1444
+
1445
+ it('returns null when finalizing nonexistent subagent', () => {
1446
+ const { manager } = createManager();
1447
+
1448
+ const info = manager.finalizeSyncSubagent('nonexistent', 'done', false);
1449
+ expect(info).toBeNull();
1450
+ });
1451
+
1452
+ it('ignores tool call for nonexistent subagent', () => {
1453
+ const { addSubagentToolCall } = jest.requireMock('@/features/chat/rendering/SubagentRenderer');
1454
+ const { manager } = createManager();
1455
+
1456
+ manager.addSyncToolCall('nonexistent', {
1457
+ id: 'tc-1',
1458
+ name: 'Read',
1459
+ input: {},
1460
+ status: 'running',
1461
+ isExpanded: false,
1462
+ });
1463
+
1464
+ expect(addSubagentToolCall).not.toHaveBeenCalled();
1465
+ });
1466
+ });
1467
+
1468
+ // ============================================
1469
+ // Async Error Resolution (resolveAsyncError)
1470
+ // ============================================
1471
+
1472
+ describe('resolveAsyncError via handleAgentOutputToolResult', () => {
1473
+ function setupRunningSubagent(manager: SubagentManager) {
1474
+ const parentEl = createMockEl();
1475
+ manager.handleTaskToolUse('task-1', { description: 'BG', run_in_background: true }, parentEl);
1476
+ manager.handleTaskToolResult('task-1', JSON.stringify({ agent_id: 'agent-1' }));
1477
+ manager.handleAgentOutputToolUse({
1478
+ id: 'output-1', name: 'AgentOutput',
1479
+ input: { task_id: 'agent-1' }, status: 'running', isExpanded: false,
1480
+ });
1481
+ }
1482
+
1483
+ it('marks completed when toolUseResult.status is "completed" even if chunk isError is true', () => {
1484
+ const { manager, updates } = createManager();
1485
+ setupRunningSubagent(manager);
1486
+
1487
+ manager.handleAgentOutputToolResult(
1488
+ 'output-1', 'result text', true,
1489
+ { status: 'completed', content: [{ type: 'text', text: 'Done' }] }
1490
+ );
1491
+
1492
+ const final = updates[updates.length - 1];
1493
+ expect(final.asyncStatus).toBe('completed');
1494
+ expect(final.status).toBe('completed');
1495
+ });
1496
+
1497
+ it('marks completed when toolUseResult.retrieval_status is "success" even if chunk isError is true', () => {
1498
+ const { manager, updates } = createManager();
1499
+ setupRunningSubagent(manager);
1500
+
1501
+ manager.handleAgentOutputToolResult(
1502
+ 'output-1', 'result text', true,
1503
+ { retrieval_status: 'success', result: 'All good' }
1504
+ );
1505
+
1506
+ const final = updates[updates.length - 1];
1507
+ expect(final.asyncStatus).toBe('completed');
1508
+ });
1509
+
1510
+ it('marks error when toolUseResult.retrieval_status is "error" even if chunk isError is false', () => {
1511
+ const { manager, updates } = createManager();
1512
+ setupRunningSubagent(manager);
1513
+
1514
+ manager.handleAgentOutputToolResult(
1515
+ 'output-1', 'result text', false,
1516
+ { retrieval_status: 'error', error: 'Agent crashed' }
1517
+ );
1518
+
1519
+ const final = updates[updates.length - 1];
1520
+ expect(final.asyncStatus).toBe('error');
1521
+ });
1522
+
1523
+ it('falls back to chunk isError when toolUseResult has no status fields', () => {
1524
+ const { manager, updates } = createManager();
1525
+ setupRunningSubagent(manager);
1526
+
1527
+ manager.handleAgentOutputToolResult('output-1', 'result text', true, { foo: 'bar' });
1528
+
1529
+ const final = updates[updates.length - 1];
1530
+ expect(final.asyncStatus).toBe('error');
1531
+ });
1532
+
1533
+ it('falls back to chunk isError when no toolUseResult is provided', () => {
1534
+ const { manager, updates } = createManager();
1535
+ setupRunningSubagent(manager);
1536
+
1537
+ manager.handleAgentOutputToolResult('output-1', 'result text', false);
1538
+
1539
+ const final = updates[updates.length - 1];
1540
+ expect(final.asyncStatus).toBe('completed');
1541
+ });
1542
+ });
1543
+
1544
+ // ============================================
1545
+ // Hook Delivery Methods
1546
+ // ============================================
1547
+
1548
+ describe('hook delivery', () => {
1549
+ describe('hasRunningSubagents', () => {
1550
+ it('returns false when no subagents exist', () => {
1551
+ const { manager } = createManager();
1552
+ expect(manager.hasRunningSubagents()).toBe(false);
1553
+ });
1554
+
1555
+ it('returns true when pending async subagents exist', () => {
1556
+ const { manager } = createManager();
1557
+ const parentEl = createMockEl();
1558
+ manager.handleTaskToolUse('task-1', { description: 'Background', run_in_background: true }, parentEl);
1559
+
1560
+ expect(manager.hasRunningSubagents()).toBe(true);
1561
+ });
1562
+
1563
+ it('returns true when active running subagents exist', () => {
1564
+ const { manager } = createManager();
1565
+ const parentEl = createMockEl();
1566
+ manager.handleTaskToolUse('task-1', { description: 'Background', run_in_background: true }, parentEl);
1567
+ manager.handleTaskToolResult('task-1', JSON.stringify({ agent_id: 'agent-123' }));
1568
+
1569
+ expect(manager.hasRunningSubagents()).toBe(true);
1570
+ });
1571
+
1572
+ it('returns false when all subagents have completed', () => {
1573
+ const { manager } = createManager();
1574
+ const parentEl = createMockEl();
1575
+ manager.handleTaskToolUse('task-1', { description: 'Task agent-123', run_in_background: true }, parentEl);
1576
+ manager.handleTaskToolResult('task-1', JSON.stringify({ agent_id: 'agent-123' }));
1577
+ manager.handleAgentOutputToolUse({
1578
+ id: 'output-agent-123',
1579
+ name: 'AgentOutput',
1580
+ input: { agent_id: 'agent-123' },
1581
+ status: 'running',
1582
+ isExpanded: false,
1583
+ });
1584
+ manager.handleAgentOutputToolResult(
1585
+ 'output-agent-123',
1586
+ JSON.stringify({ result: 'Result from agent-123' }),
1587
+ false
1588
+ );
1589
+
1590
+ expect(manager.hasRunningSubagents()).toBe(false);
1591
+ });
1592
+
1593
+ it('returns false after a live task notification completes an active async subagent', () => {
1594
+ const { manager, updates } = createManager();
1595
+ const parentEl = createMockEl();
1596
+ manager.handleTaskToolUse('task-1', { description: 'Background', run_in_background: true }, parentEl);
1597
+ manager.handleTaskToolResult('task-1', JSON.stringify({ agent_id: 'agent-123' }));
1598
+
1599
+ expect(manager.hasRunningSubagents()).toBe(true);
1600
+
1601
+ const completed = manager.handleAsyncSubagentResult(
1602
+ 'agent-123',
1603
+ 'completed',
1604
+ 'Background agent finished.'
1605
+ );
1606
+
1607
+ expect(completed?.asyncStatus).toBe('completed');
1608
+ expect(completed?.status).toBe('completed');
1609
+ expect(completed?.result).toBe('Background agent finished.');
1610
+ expect(manager.hasRunningSubagents()).toBe(false);
1611
+ expect(updates[updates.length - 1]).toEqual(
1612
+ expect.objectContaining({
1613
+ agentId: 'agent-123',
1614
+ asyncStatus: 'completed',
1615
+ result: 'Background agent finished.',
1616
+ })
1617
+ );
1618
+ });
1619
+
1620
+ it('returns false after parallel foreground tasks resolve from completed task results', () => {
1621
+ const { manager } = createManager();
1622
+ const parentEl = createMockEl();
1623
+ const syncToolUseResult1 = {
1624
+ status: 'completed',
1625
+ agentId: 'agent-sync-1',
1626
+ content: [{ type: 'text', text: 'Foreground result 1.' }],
1627
+ };
1628
+ const syncToolUseResult2 = {
1629
+ status: 'completed',
1630
+ agentId: 'agent-sync-2',
1631
+ content: [{ type: 'text', text: 'Foreground result 2.' }],
1632
+ };
1633
+
1634
+ manager.handleTaskToolUse('task-1', { prompt: 'Foreground 1' }, parentEl);
1635
+ manager.handleTaskToolUse('task-2', { prompt: 'Foreground 2' }, parentEl);
1636
+
1637
+ const first = manager.renderPendingTaskFromTaskResult(
1638
+ 'task-1',
1639
+ '{}',
1640
+ false,
1641
+ parentEl,
1642
+ syncToolUseResult1
1643
+ );
1644
+ const second = manager.renderPendingTaskFromTaskResult(
1645
+ 'task-2',
1646
+ '{}',
1647
+ false,
1648
+ parentEl,
1649
+ syncToolUseResult2
1650
+ );
1651
+
1652
+ expect(first?.mode).toBe('sync');
1653
+ expect(second?.mode).toBe('sync');
1654
+
1655
+ manager.finalizeSyncSubagent('task-1', '{}', false, syncToolUseResult1);
1656
+ manager.finalizeSyncSubagent('task-2', '{}', false, syncToolUseResult2);
1657
+
1658
+ expect(manager.hasRunningSubagents()).toBe(false);
1659
+ });
1660
+
1661
+ it('allows the Stop hook after a foreground task resolves from completed task metadata', async () => {
1662
+ const { manager } = createManager();
1663
+ const parentEl = createMockEl();
1664
+ const completedToolUseResult = {
1665
+ status: 'completed',
1666
+ agentId: 'agent-sync',
1667
+ };
1668
+ const hook = createStopSubagentHook(() => ({
1669
+ hasRunning: manager.hasRunningSubagents(),
1670
+ }));
1671
+
1672
+ manager.handleTaskToolUse('task-1', { prompt: 'Foreground task' }, parentEl);
1673
+ const rendered = manager.renderPendingTaskFromTaskResult(
1674
+ 'task-1',
1675
+ JSON.stringify(completedToolUseResult, null, 2),
1676
+ false,
1677
+ parentEl,
1678
+ completedToolUseResult,
1679
+ );
1680
+
1681
+ expect(rendered?.mode).toBe('sync');
1682
+
1683
+ manager.finalizeSyncSubagent('task-1', '{}', false, completedToolUseResult);
1684
+
1685
+ const stopResult = await hook.hooks[0](
1686
+ {
1687
+ hook_event_name: 'Stop',
1688
+ session_id: 'test-session',
1689
+ transcript_path: '/tmp/transcript',
1690
+ cwd: '/vault',
1691
+ stop_hook_active: true,
1692
+ },
1693
+ undefined,
1694
+ { signal: new AbortController().signal }
1695
+ );
1696
+
1697
+ expect(stopResult).toEqual({});
1698
+ });
1699
+ });
1700
+ });
1701
+
1702
+ // ============================================
1703
+ // Lifecycle
1704
+ // ============================================
1705
+
1706
+ describe('lifecycle', () => {
1707
+ it('resets spawned count', () => {
1708
+ const { manager } = createManager();
1709
+ const parentEl = createMockEl();
1710
+
1711
+ manager.handleTaskToolUse('task-1', { run_in_background: false }, parentEl);
1712
+ expect(manager.subagentsSpawnedThisStream).toBe(1);
1713
+
1714
+ manager.resetSpawnedCount();
1715
+ expect(manager.subagentsSpawnedThisStream).toBe(0);
1716
+ });
1717
+
1718
+ it('resets streaming state clears sync maps and pending tasks', () => {
1719
+ const { manager } = createManager();
1720
+ const parentEl = createMockEl();
1721
+
1722
+ manager.handleTaskToolUse('task-sync', { run_in_background: false }, parentEl);
1723
+ manager.handleTaskToolUse('task-pending', { prompt: 'test' }, null);
1724
+
1725
+ expect(manager.getSyncSubagent('task-sync')).toBeDefined();
1726
+ expect(manager.hasPendingTask('task-pending')).toBe(true);
1727
+
1728
+ manager.resetStreamingState();
1729
+
1730
+ expect(manager.getSyncSubagent('task-sync')).toBeUndefined();
1731
+ expect(manager.hasPendingTask('task-pending')).toBe(false);
1732
+ });
1733
+
1734
+ it('clears all state', () => {
1735
+ const { manager } = createManager();
1736
+ const parentEl = createMockEl();
1737
+
1738
+ manager.handleTaskToolUse('task-async', { description: 'Background', run_in_background: true }, parentEl);
1739
+
1740
+ manager.clear();
1741
+ expect(manager.getByTaskId('task-async')).toBeUndefined();
1742
+ expect(manager.isPendingAsyncTask('task-async')).toBe(false);
1743
+ });
1744
+
1745
+ it('updates callback via setCallback', () => {
1746
+ const { manager } = createManager();
1747
+ const parentEl = createMockEl();
1748
+ const newUpdates: SubagentInfo[] = [];
1749
+
1750
+ manager.handleTaskToolUse('task-1', { description: 'Background', run_in_background: true }, parentEl);
1751
+ manager.setCallback((subagent) => { newUpdates.push({ ...subagent }); });
1752
+
1753
+ manager.handleTaskToolResult('task-1', JSON.stringify({ agent_id: 'agent-new' }));
1754
+
1755
+ expect(newUpdates.length).toBeGreaterThan(0);
1756
+ expect(newUpdates[newUpdates.length - 1].agentId).toBe('agent-new');
1757
+ });
1758
+ });
1759
+ });