@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,1021 @@
1
+ import { Notice } from 'obsidian';
2
+
3
+ import { ProviderRegistry } from '../../../core/providers/ProviderRegistry';
4
+ import { ProviderSettingsCoordinator } from '../../../core/providers/ProviderSettingsCoordinator';
5
+ import { ProviderWorkspaceRegistry } from '../../../core/providers/ProviderWorkspaceRegistry';
6
+ import type {
7
+ ProviderId,
8
+ ProviderTabWarmupContext,
9
+ ProviderTabWarmupMode,
10
+ } from '../../../core/providers/types';
11
+ import type { ChatRuntime } from '../../../core/runtime/ChatRuntime';
12
+ import type { Conversation, SlashCommand } from '../../../core/types';
13
+ import { t } from '../../../i18n/i18n';
14
+ import type ClaudianPlugin from '../../../main';
15
+ import { chooseForkTarget } from '../../../shared/modals/ForkTargetModal';
16
+ import { revealWorkspaceLeaf } from '../../../utils/obsidianCompat';
17
+ import { getTabProviderId } from './providerResolution';
18
+ import {
19
+ activateTab,
20
+ createTab,
21
+ deactivateTab,
22
+ destroyTab,
23
+ type ForkContext,
24
+ getTabTitle,
25
+ initializeTabControllers,
26
+ initializeTabService,
27
+ initializeTabUI,
28
+ setupServiceCallbacks,
29
+ wireTabInputEvents,
30
+ } from './Tab';
31
+ import {
32
+ DEFAULT_MAX_TABS,
33
+ MAX_TABS,
34
+ MIN_TABS,
35
+ type PersistedTabManagerState,
36
+ type PersistedTabState,
37
+ type TabBarItem,
38
+ type TabData,
39
+ type TabId,
40
+ type TabManagerCallbacks,
41
+ type TabManagerInterface,
42
+ type TabManagerViewHost,
43
+ } from './types';
44
+
45
+ function isTabManagerViewHost(value: unknown): value is TabManagerViewHost {
46
+ return !!value
47
+ && typeof value === 'object'
48
+ && 'getTabManager' in (value as Record<string, unknown>);
49
+ }
50
+
51
+ type CreateTabOptions = {
52
+ activate?: boolean;
53
+ draftModel?: string;
54
+ };
55
+
56
+ type OpenConversationOptions = {
57
+ preferNewTab?: boolean;
58
+ activate?: boolean;
59
+ };
60
+
61
+ type ProviderCommandCacheEntry = {
62
+ commands: SlashCommand[];
63
+ key: string;
64
+ };
65
+
66
+ type ProviderWarmupContext = {
67
+ conversation: Conversation | null;
68
+ externalContextPaths: string[];
69
+ runtime: ChatRuntime | null;
70
+ tab: ProviderTabWarmupContext['tab'];
71
+ warmupMode: ProviderTabWarmupMode;
72
+ };
73
+
74
+ type ProviderCommandContext = ProviderWarmupContext & {
75
+ cacheKey: string;
76
+ };
77
+
78
+ type ProviderCommandWarmupEntry = {
79
+ key: string;
80
+ promise: Promise<SlashCommand[]>;
81
+ };
82
+
83
+ /**
84
+ * TabManager coordinates multiple chat tabs.
85
+ */
86
+ export class TabManager implements TabManagerInterface {
87
+ private plugin: ClaudianPlugin;
88
+ private containerEl: HTMLElement;
89
+ private view: TabManagerViewHost;
90
+
91
+ private tabs: Map<TabId, TabData> = new Map();
92
+ private activeTabId: TabId | null = null;
93
+ private callbacks: TabManagerCallbacks;
94
+ private providerCommandWarmups = new Map<TabId, ProviderCommandWarmupEntry>();
95
+ private providerCommandCache = new Map<TabId, ProviderCommandCacheEntry>();
96
+ private isRestoringState = false;
97
+
98
+ /** Guard to prevent concurrent tab switches. */
99
+ private isSwitchingTab = false;
100
+
101
+ /**
102
+ * Gets the current max tabs limit from settings.
103
+ * Clamps to MIN_TABS and MAX_TABS bounds.
104
+ */
105
+ private getMaxTabs(): number {
106
+ const settingsValue = this.plugin.settings.maxTabs ?? DEFAULT_MAX_TABS;
107
+ return Math.max(MIN_TABS, Math.min(MAX_TABS, settingsValue));
108
+ }
109
+
110
+ constructor(
111
+ plugin: ClaudianPlugin,
112
+ containerEl: HTMLElement,
113
+ view: TabManagerViewHost,
114
+ callbacks?: TabManagerCallbacks,
115
+ );
116
+ constructor(
117
+ plugin: ClaudianPlugin,
118
+ legacyArg: unknown,
119
+ containerEl: HTMLElement,
120
+ view: TabManagerViewHost,
121
+ callbacks?: TabManagerCallbacks,
122
+ );
123
+ constructor(
124
+ plugin: ClaudianPlugin,
125
+ arg2: unknown,
126
+ arg3: HTMLElement | TabManagerViewHost,
127
+ arg4?: TabManagerViewHost | TabManagerCallbacks,
128
+ arg5: TabManagerCallbacks = {},
129
+ ) {
130
+ this.plugin = plugin;
131
+
132
+ if (isTabManagerViewHost(arg3)) {
133
+ this.containerEl = arg2 as HTMLElement;
134
+ this.view = arg3;
135
+ this.callbacks = (arg4 as TabManagerCallbacks | undefined) ?? {};
136
+ return;
137
+ }
138
+
139
+ this.containerEl = arg3;
140
+ this.view = arg4 as TabManagerViewHost;
141
+ this.callbacks = arg5;
142
+ }
143
+
144
+ // ============================================
145
+ // Tab Lifecycle
146
+ // ============================================
147
+
148
+ /**
149
+ * Creates a new tab.
150
+ * @param conversationId Optional conversation to load into the tab.
151
+ * @param tabId Optional tab ID (for restoration).
152
+ * @param options Controls whether the new tab becomes active immediately.
153
+ * @returns The created tab, or null if max tabs reached.
154
+ */
155
+ async createTab(
156
+ conversationId?: string | null,
157
+ tabId?: TabId,
158
+ options: CreateTabOptions = {},
159
+ ): Promise<TabData | null> {
160
+ const maxTabs = this.getMaxTabs();
161
+ if (this.tabs.size >= maxTabs) {
162
+ return null;
163
+ }
164
+
165
+ const { activate = true, draftModel } = options;
166
+
167
+ const conversation = conversationId
168
+ ? await this.plugin.getConversationById(conversationId)
169
+ : undefined;
170
+
171
+ // Inherit the active tab's provider so the new blank tab picks up its model
172
+ const activeTab = this.getActiveTab();
173
+ const defaultProviderId = conversation
174
+ ? undefined
175
+ : (activeTab ? getTabProviderId(activeTab, this.plugin) : undefined);
176
+
177
+ const tab = createTab({
178
+ plugin: this.plugin,
179
+ containerEl: this.containerEl,
180
+ conversation: conversation ?? undefined,
181
+ tabId,
182
+ ...(typeof draftModel === 'string' ? { draftModel } : {}),
183
+ defaultProviderId,
184
+ onStreamingChanged: (isStreaming) => {
185
+ this.callbacks.onTabStreamingChanged?.(tab.id, isStreaming);
186
+ },
187
+ onTitleChanged: (title) => {
188
+ this.callbacks.onTabTitleChanged?.(tab.id, title);
189
+ },
190
+ onAttentionChanged: (needsAttention) => {
191
+ this.callbacks.onTabAttentionChanged?.(tab.id, needsAttention);
192
+ },
193
+ onConversationIdChanged: (conversationId) => {
194
+ // Sync tab.conversationId when conversation is lazily created
195
+ tab.conversationId = conversationId;
196
+ this.callbacks.onTabConversationChanged?.(tab.id, conversationId);
197
+ },
198
+ });
199
+
200
+ // Initialize UI components with provider catalog
201
+ initializeTabUI(tab, this.plugin, {
202
+ getProviderCatalogConfig: () => this.getProviderCatalogConfig(tab),
203
+ onProviderChanged: (providerId) => {
204
+ this.callbacks.onTabProviderChanged?.(tab.id, providerId);
205
+ void this.prewarmProviderTab(tab).catch(() => {
206
+ // Keep provider switching non-blocking even if command warmup fails.
207
+ });
208
+ },
209
+ });
210
+
211
+ initializeTabControllers(
212
+ tab,
213
+ this.plugin,
214
+ this.view,
215
+ (forkContext) => this.handleForkRequest(forkContext),
216
+ (conversationId) => this.openConversation(conversationId),
217
+ () => this.getProviderCatalogConfig(tab),
218
+ );
219
+
220
+ // Wire input event handlers
221
+ wireTabInputEvents(tab, this.plugin);
222
+
223
+ this.tabs.set(tab.id, tab);
224
+ this.callbacks.onTabCreated?.(tab);
225
+
226
+ if (!this.isRestoringState && (activate || !this.activeTabId)) {
227
+ await this.switchToTab(tab.id);
228
+ } else if (!this.isRestoringState) {
229
+ this.maybePrimeProviderRuntime(tab);
230
+ }
231
+
232
+ return tab;
233
+ }
234
+
235
+ /**
236
+ * Switches to a different tab.
237
+ * @param tabId The tab to switch to.
238
+ */
239
+ async switchToTab(tabId: TabId): Promise<void> {
240
+ const tab = this.tabs.get(tabId);
241
+ if (!tab) {
242
+ return;
243
+ }
244
+
245
+ // Guard against concurrent tab switches
246
+ if (this.isSwitchingTab) {
247
+ return;
248
+ }
249
+
250
+ this.isSwitchingTab = true;
251
+ const previousTabId = this.activeTabId;
252
+
253
+ try {
254
+ // Deactivate current tab
255
+ if (previousTabId && previousTabId !== tabId) {
256
+ const currentTab = this.tabs.get(previousTabId);
257
+ if (currentTab) {
258
+ deactivateTab(currentTab);
259
+ }
260
+ }
261
+
262
+ // Activate new tab
263
+ this.activeTabId = tabId;
264
+ activateTab(tab);
265
+ this.callbacks.onActiveTabChanged?.(previousTabId, tabId);
266
+
267
+ // Load conversation if not already loaded
268
+ if (tab.conversationId && tab.state.messages.length === 0) {
269
+ await tab.controllers.conversationController?.switchTo(tab.conversationId);
270
+ } else if (
271
+ tab.conversationId
272
+ && tab.state.messages.length > 0
273
+ && tab.service
274
+ && !tab.state.isStreaming
275
+ && !tab.state.hasPendingConversationSave
276
+ ) {
277
+ // Passive sync is only safe once local tab state has been persisted.
278
+ const conversation = this.plugin.getConversationSync(tab.conversationId);
279
+ if (conversation) {
280
+ const hasMessages = conversation.messages.length > 0;
281
+ const externalContextPaths = hasMessages
282
+ ? conversation.externalContextPaths || []
283
+ : (this.plugin.settings.persistentExternalContextPaths || []);
284
+
285
+ tab.service.syncConversationState(conversation, externalContextPaths);
286
+ }
287
+ } else if (!tab.conversationId && tab.state.messages.length === 0) {
288
+ // New tab with no conversation - initialize welcome greeting
289
+ tab.controllers.conversationController?.initializeWelcome();
290
+ }
291
+
292
+ this.callbacks.onTabSwitched?.(previousTabId, tabId);
293
+ this.maybePrimeProviderRuntime(tab);
294
+ } finally {
295
+ this.isSwitchingTab = false;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Closes a tab.
301
+ * @param tabId The tab to close.
302
+ * @param force If true, close even if streaming.
303
+ * @returns True if the tab was closed.
304
+ */
305
+ async closeTab(tabId: TabId, force = false): Promise<boolean> {
306
+ const tab = this.tabs.get(tabId);
307
+ if (!tab) {
308
+ return false;
309
+ }
310
+
311
+ // Don't close if streaming unless forced
312
+ if (tab.state.isStreaming && !force) {
313
+ return false;
314
+ }
315
+
316
+ // If this is the last tab and it's already empty (no conversation),
317
+ // don't close it - it's already a blank draft container.
318
+ if (this.tabs.size === 1 && !tab.conversationId && tab.state.messages.length === 0) {
319
+ return false;
320
+ }
321
+
322
+ // Save conversation before closing
323
+ await tab.controllers.conversationController?.save();
324
+
325
+ // Capture tab order BEFORE deletion for fallback calculation
326
+ const tabIdsBefore = Array.from(this.tabs.keys());
327
+ const closingIndex = tabIdsBefore.indexOf(tabId);
328
+
329
+ // Destroy tab resources (async for proper cleanup)
330
+ await destroyTab(tab);
331
+ this.providerCommandWarmups.delete(tabId);
332
+ this.providerCommandCache.delete(tabId);
333
+ this.tabs.delete(tabId);
334
+ this.callbacks.onTabClosed?.(tabId);
335
+
336
+ // If we closed the active tab, switch to another
337
+ if (this.activeTabId === tabId) {
338
+ this.activeTabId = null;
339
+
340
+ if (this.tabs.size > 0) {
341
+ // Fallback strategy: prefer previous tab, except for first tab (go to next)
342
+ const fallbackTabId = closingIndex === 0
343
+ ? tabIdsBefore[1] // First tab: go to next
344
+ : tabIdsBefore[closingIndex - 1]; // Others: go to previous
345
+
346
+ if (fallbackTabId && this.tabs.has(fallbackTabId)) {
347
+ await this.switchToTab(fallbackTabId);
348
+ }
349
+ } else {
350
+ // Create a replacement blank tab.
351
+ await this.createTab();
352
+ }
353
+ }
354
+
355
+ return true;
356
+ }
357
+
358
+ // ============================================
359
+ // Tab Queries
360
+ // ============================================
361
+
362
+ /** Gets the currently active tab. */
363
+ getActiveTab(): TabData | null {
364
+ return this.activeTabId ? this.tabs.get(this.activeTabId) ?? null : null;
365
+ }
366
+
367
+ /** Gets the active tab ID. */
368
+ getActiveTabId(): TabId | null {
369
+ return this.activeTabId;
370
+ }
371
+
372
+ /** Gets a tab by ID. */
373
+ getTab(tabId: TabId): TabData | null {
374
+ return this.tabs.get(tabId) ?? null;
375
+ }
376
+
377
+ /** Gets all tabs. */
378
+ getAllTabs(): TabData[] {
379
+ return Array.from(this.tabs.values());
380
+ }
381
+
382
+ /** Gets the number of tabs. */
383
+ getTabCount(): number {
384
+ return this.tabs.size;
385
+ }
386
+
387
+ /** Checks if more tabs can be created. */
388
+ canCreateTab(): boolean {
389
+ return this.tabs.size < this.getMaxTabs();
390
+ }
391
+
392
+ // ============================================
393
+ // Tab Bar Data
394
+ // ============================================
395
+
396
+ /** Gets data for rendering the tab bar. */
397
+ getTabBarItems(): TabBarItem[] {
398
+ const items: TabBarItem[] = [];
399
+ let index = 1;
400
+
401
+ for (const tab of this.tabs.values()) {
402
+ items.push({
403
+ id: tab.id,
404
+ index: index++,
405
+ title: getTabTitle(tab, this.plugin),
406
+ providerId: getTabProviderId(tab, this.plugin),
407
+ isActive: tab.id === this.activeTabId,
408
+ isStreaming: tab.state.isStreaming,
409
+ needsAttention: tab.state.needsAttention,
410
+ canClose: this.tabs.size > 1 || !tab.state.isStreaming,
411
+ });
412
+ }
413
+
414
+ return items;
415
+ }
416
+
417
+ // ============================================
418
+ // Conversation Management
419
+ // ============================================
420
+
421
+ /**
422
+ * Opens a conversation in a new tab or existing tab.
423
+ * @param conversationId The conversation to open.
424
+ * @param options Controls tab creation behavior (backward-compatible with boolean).
425
+ */
426
+ async openConversation(
427
+ conversationId: string,
428
+ options: boolean | OpenConversationOptions = false,
429
+ ): Promise<void> {
430
+ const preferNewTab = typeof options === 'boolean'
431
+ ? options
432
+ : options.preferNewTab ?? false;
433
+ const activate = typeof options === 'boolean'
434
+ ? true
435
+ : options.activate ?? true;
436
+
437
+ // Check if conversation is already open in this view's tabs
438
+ for (const tab of this.tabs.values()) {
439
+ if (tab.conversationId === conversationId) {
440
+ await this.switchToTab(tab.id);
441
+ return;
442
+ }
443
+ }
444
+
445
+ // Check if conversation is open in another view (split workspace scenario)
446
+ // Compare view references directly (more robust than leaf comparison)
447
+ const crossViewResult = this.plugin.findConversationAcrossViews(conversationId);
448
+ const isSameView = crossViewResult?.view === this.view;
449
+ if (crossViewResult && !isSameView) {
450
+ // Focus the other view and switch to its tab instead of opening duplicate
451
+ await revealWorkspaceLeaf(this.plugin.app.workspace, crossViewResult.view.leaf);
452
+ await crossViewResult.view.getTabManager()?.switchToTab(crossViewResult.tabId);
453
+ return;
454
+ }
455
+
456
+ // Open in current tab or new tab
457
+ if (preferNewTab && this.canCreateTab()) {
458
+ await this.createTab(conversationId, undefined, { activate });
459
+ } else {
460
+ // Open in current tab
461
+ // Note: Don't set tab.conversationId here - the onConversationIdChanged callback
462
+ // will sync it after successful switch. Setting it before switchTo() would cause
463
+ // incorrect tab metadata if switchTo() returns early (streaming/switching/creating).
464
+ const activeTab = this.getActiveTab();
465
+ if (activeTab) {
466
+ await activeTab.controllers.conversationController?.switchTo(conversationId);
467
+ }
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Creates a new conversation in the active tab.
473
+ */
474
+ async createNewConversation(): Promise<void> {
475
+ const activeTab = this.getActiveTab();
476
+ if (activeTab) {
477
+ await activeTab.controllers.conversationController?.createNew();
478
+ // Sync tab.conversationId with the newly created conversation
479
+ activeTab.conversationId = activeTab.state.currentConversationId;
480
+ this.maybePrimeProviderRuntime(activeTab);
481
+ }
482
+ }
483
+
484
+ invalidateProviderCommandCaches(providerIds?: ProviderId | ProviderId[]): void {
485
+ for (const tab of this.filterTabsByProvider(providerIds, (tab) => getTabProviderId(tab, this.plugin))) {
486
+ this.providerCommandWarmups.delete(tab.id);
487
+ this.providerCommandCache.delete(tab.id);
488
+ tab.ui?.slashCommandDropdown?.resetSdkSkillsCache();
489
+ }
490
+ }
491
+
492
+ primeProviderRuntime(providerIds?: ProviderId | ProviderId[]): void {
493
+ for (const tab of this.filterTabsByProvider(providerIds, (tab) => tab.service?.providerId ?? tab.providerId)) {
494
+ this.maybePrimeProviderRuntime(tab);
495
+ }
496
+ }
497
+
498
+ private *filterTabsByProvider(
499
+ providerIds: ProviderId | ProviderId[] | undefined,
500
+ resolve: (tab: TabData) => ProviderId,
501
+ ): Iterable<TabData> {
502
+ const filter = providerIds
503
+ ? new Set(Array.isArray(providerIds) ? providerIds : [providerIds])
504
+ : null;
505
+
506
+ for (const tab of this.tabs.values()) {
507
+ if (filter && !filter.has(resolve(tab))) {
508
+ continue;
509
+ }
510
+ yield tab;
511
+ }
512
+ }
513
+
514
+ // ============================================
515
+ // Fork
516
+ // ============================================
517
+
518
+ private async handleForkRequest(context: ForkContext): Promise<void> {
519
+ const target = await chooseForkTarget(this.plugin.app);
520
+ if (!target) return;
521
+
522
+ if (target === 'new-tab') {
523
+ const tab = await this.forkToNewTab(context);
524
+ if (!tab) {
525
+ const maxTabs = this.getMaxTabs();
526
+ new Notice(t('chat.fork.maxTabsReached', { count: String(maxTabs) }));
527
+ return;
528
+ }
529
+ new Notice(t('chat.fork.notice'));
530
+ } else {
531
+ const success = await this.forkInCurrentTab(context);
532
+ if (!success) {
533
+ new Notice(t('chat.fork.failed', { error: t('chat.fork.errorNoActiveTab') }));
534
+ return;
535
+ }
536
+ new Notice(t('chat.fork.noticeCurrentTab'));
537
+ }
538
+ }
539
+
540
+ async forkToNewTab(context: ForkContext): Promise<TabData | null> {
541
+ const maxTabs = this.getMaxTabs();
542
+ if (this.tabs.size >= maxTabs) {
543
+ return null;
544
+ }
545
+
546
+ const conversationId = await this.createForkConversation(context);
547
+ try {
548
+ return await this.createTab(conversationId);
549
+ } catch (error) {
550
+ await this.plugin.deleteConversation(conversationId).catch(() => {});
551
+ throw error;
552
+ }
553
+ }
554
+
555
+ async forkInCurrentTab(context: ForkContext): Promise<boolean> {
556
+ const activeTab = this.getActiveTab();
557
+ if (!activeTab?.controllers.conversationController) return false;
558
+
559
+ const conversationId = await this.createForkConversation(context);
560
+ try {
561
+ await activeTab.controllers.conversationController.switchTo(conversationId);
562
+ } catch (error) {
563
+ await this.plugin.deleteConversation(conversationId).catch(() => {});
564
+ throw error;
565
+ }
566
+ return true;
567
+ }
568
+
569
+ private async createForkConversation(context: ForkContext): Promise<string> {
570
+ const conversation = await this.plugin.createConversation({
571
+ providerId: context.providerId,
572
+ });
573
+
574
+ const title = context.sourceTitle
575
+ ? this.buildForkTitle(context.sourceTitle, context.forkAtUserMessage)
576
+ : undefined;
577
+
578
+ const forkProviderState = ProviderRegistry
579
+ .getConversationHistoryService(conversation.providerId)
580
+ .buildForkProviderState(
581
+ context.sourceSessionId,
582
+ context.resumeAt,
583
+ context.sourceProviderState,
584
+ );
585
+
586
+ await this.plugin.updateConversation(conversation.id, {
587
+ messages: context.messages,
588
+ providerState: forkProviderState,
589
+ ...(title && { title }),
590
+ ...(context.currentNote && { currentNote: context.currentNote }),
591
+ });
592
+
593
+ return conversation.id;
594
+ }
595
+
596
+ private buildForkTitle(sourceTitle: string, forkAtUserMessage?: number): string {
597
+ const MAX_TITLE_LENGTH = 50;
598
+ const forkSuffix = forkAtUserMessage ? ` (#${forkAtUserMessage})` : '';
599
+ const forkPrefix = 'Fork: ';
600
+ const maxSourceLength = MAX_TITLE_LENGTH - forkPrefix.length - forkSuffix.length;
601
+ const truncatedSource = sourceTitle.length > maxSourceLength
602
+ ? sourceTitle.slice(0, maxSourceLength - 1) + '…'
603
+ : sourceTitle;
604
+ let title = forkPrefix + truncatedSource + forkSuffix;
605
+
606
+ const existingTitles = new Set(this.plugin.getConversationList().map(c => c.title));
607
+ if (existingTitles.has(title)) {
608
+ let n = 2;
609
+ while (existingTitles.has(`${title} ${n}`)) n++;
610
+ title = `${title} ${n}`;
611
+ }
612
+
613
+ return title;
614
+ }
615
+
616
+ // ============================================
617
+ // Persistence
618
+ // ============================================
619
+
620
+ /** Gets the state to persist. */
621
+ getPersistedState(): PersistedTabManagerState {
622
+ const openTabs: PersistedTabState[] = [];
623
+
624
+ for (const tab of this.tabs.values()) {
625
+ openTabs.push({
626
+ ...(tab.lifecycleState === 'blank' && tab.draftModel
627
+ ? { draftModel: tab.draftModel }
628
+ : {}),
629
+ tabId: tab.id,
630
+ conversationId: tab.conversationId,
631
+ });
632
+ }
633
+
634
+ return {
635
+ openTabs,
636
+ activeTabId: this.activeTabId,
637
+ };
638
+ }
639
+
640
+ /** Restores state from persisted data. */
641
+ async restoreState(state: PersistedTabManagerState): Promise<void> {
642
+ this.isRestoringState = true;
643
+ try {
644
+ // Create tabs from persisted state with error handling.
645
+ for (const tabState of state.openTabs) {
646
+ try {
647
+ await this.createTab(tabState.conversationId, tabState.tabId, {
648
+ activate: false,
649
+ ...(typeof tabState.draftModel === 'string' ? { draftModel: tabState.draftModel } : {}),
650
+ });
651
+ } catch {
652
+ // Continue restoring other tabs
653
+ }
654
+ }
655
+ } finally {
656
+ this.isRestoringState = false;
657
+ }
658
+
659
+ const fallbackTabId = state.openTabs.find((tabState) => this.tabs.has(tabState.tabId))?.tabId
660
+ ?? Array.from(this.tabs.keys())[0]
661
+ ?? null;
662
+ const targetTabId = state.activeTabId && this.tabs.has(state.activeTabId)
663
+ ? state.activeTabId
664
+ : fallbackTabId;
665
+
666
+ // Switch to the previously active tab after all tabs are restored so background
667
+ // restore does not warm the first restored tab by accident.
668
+ if (targetTabId) {
669
+ try {
670
+ await this.switchToTab(targetTabId);
671
+ } catch {
672
+ // Ignore switch errors
673
+ }
674
+ }
675
+
676
+ // If no tabs were restored, create a default one
677
+ if (this.tabs.size === 0) {
678
+ await this.createTab();
679
+ }
680
+ }
681
+
682
+ // ============================================
683
+ // SDK Commands (Shared)
684
+ // ============================================
685
+
686
+ /**
687
+ * Gets provider-scoped SDK supported commands for a tab.
688
+ * Reuses a ready runtime from the same provider when available to avoid
689
+ * leaking commands across providers in mixed-provider workspaces.
690
+ * @returns Array of SDK commands, or empty array if no service is ready.
691
+ */
692
+ async getSdkCommands(tabId?: TabId): Promise<SlashCommand[]> {
693
+ const targetTab = (tabId ? this.tabs.get(tabId) : this.getActiveTab()) ?? null;
694
+ if (!targetTab) {
695
+ return [];
696
+ }
697
+
698
+ const providerId = getTabProviderId(targetTab, this.plugin);
699
+ const staticCapabilities = ProviderRegistry.getCapabilities(providerId);
700
+ if (!staticCapabilities.supportsProviderCommands) {
701
+ return [];
702
+ }
703
+
704
+ const catalog = ProviderWorkspaceRegistry.getCommandCatalog(providerId);
705
+ const runtimeCommandLoader = ProviderWorkspaceRegistry.getRuntimeCommandLoader(providerId);
706
+ const context = await this.buildProviderWarmupContext(targetTab, providerId);
707
+ if (
708
+ targetTab.lifecycleState === 'blank'
709
+ && runtimeCommandLoader
710
+ && (context.warmupMode !== 'commands' || targetTab.id !== this.activeTabId)
711
+ ) {
712
+ catalog?.setRuntimeCommands([]);
713
+ return [];
714
+ }
715
+
716
+ let sdkCommands: SlashCommand[] = [];
717
+
718
+ const targetService = targetTab.service;
719
+ if (targetService?.providerId === providerId && targetService.isReady()) {
720
+ sdkCommands = await targetService.getSupportedCommands();
721
+ } else if (!runtimeCommandLoader) {
722
+ for (const tab of this.tabs.values()) {
723
+ if (tab.id === targetTab.id) {
724
+ continue;
725
+ }
726
+ if (tab.service?.providerId === providerId && tab.service.isReady()) {
727
+ sdkCommands = await tab.service.getSupportedCommands();
728
+ break;
729
+ }
730
+ }
731
+ }
732
+
733
+ if (sdkCommands.length === 0) {
734
+ sdkCommands = await this.ensureProviderCommandRuntime(targetTab, providerId, context);
735
+ }
736
+
737
+ catalog?.setRuntimeCommands(sdkCommands);
738
+
739
+ return sdkCommands;
740
+ }
741
+
742
+ private async ensureProviderCommandRuntime(
743
+ tab: TabData,
744
+ providerId: ProviderId,
745
+ warmupContext?: ProviderWarmupContext,
746
+ ): Promise<SlashCommand[]> {
747
+ if (!this.isProviderCommandLoaderAvailable(providerId)) {
748
+ return [];
749
+ }
750
+
751
+ const resolvedWarmupContext = warmupContext
752
+ ?? await this.buildProviderWarmupContext(tab, providerId);
753
+ const context = this.buildProviderCommandContext(
754
+ tab,
755
+ providerId,
756
+ resolvedWarmupContext,
757
+ );
758
+ const cached = this.providerCommandCache.get(tab.id);
759
+ if (
760
+ (!context.runtime || !context.runtime.isReady())
761
+ && cached
762
+ && cached.key === context.cacheKey
763
+ ) {
764
+ return cached.commands.map((command) => ({ ...command }));
765
+ }
766
+
767
+ const existing = this.providerCommandWarmups.get(tab.id);
768
+ if (existing?.key === context.cacheKey) {
769
+ return await existing.promise;
770
+ }
771
+ this.providerCommandWarmups.delete(tab.id);
772
+
773
+ const warmup = this.warmProviderCommandRuntime(tab, providerId, context).finally(() => {
774
+ if (this.providerCommandWarmups.get(tab.id)?.promise === warmup) {
775
+ this.providerCommandWarmups.delete(tab.id);
776
+ }
777
+ });
778
+ this.providerCommandWarmups.set(tab.id, {
779
+ key: context.cacheKey,
780
+ promise: warmup,
781
+ });
782
+ return await warmup;
783
+ }
784
+
785
+ private maybePrimeProviderRuntime(tab: TabData): void {
786
+ void this.prewarmProviderTab(tab).catch(() => {});
787
+ }
788
+
789
+ private isProviderCommandLoaderAvailable(providerId: ProviderId): boolean {
790
+ const loader = ProviderWorkspaceRegistry.getRuntimeCommandLoader(providerId);
791
+ if (!loader) return false;
792
+ return loader.isAvailable(this.plugin.settings);
793
+ }
794
+
795
+ private async prewarmProviderTab(tab: TabData): Promise<void> {
796
+ const providerId = tab.service?.providerId ?? tab.providerId;
797
+ const context = await this.buildProviderWarmupContext(tab, providerId);
798
+ const hasReadyRuntime = tab.service?.providerId === providerId && tab.service.isReady();
799
+ if (!hasReadyRuntime && tab.id !== this.activeTabId) {
800
+ return;
801
+ }
802
+
803
+ switch (context.warmupMode) {
804
+ case 'commands':
805
+ await this.getSdkCommands(tab.id);
806
+ return;
807
+ case 'runtime':
808
+ await this.ensureProviderTabRuntimeReady(tab, providerId, context);
809
+ return;
810
+ default:
811
+ return;
812
+ }
813
+ }
814
+
815
+ private async ensureProviderTabRuntimeReady(
816
+ tab: TabData,
817
+ providerId: ProviderId,
818
+ context: ProviderWarmupContext,
819
+ ): Promise<void> {
820
+ if (!context.runtime || context.runtime.providerId !== providerId || !tab.serviceInitialized) {
821
+ await initializeTabService(tab, this.plugin, context.conversation);
822
+ setupServiceCallbacks(tab, this.plugin);
823
+ }
824
+
825
+ const runtime = tab.service?.providerId === providerId ? tab.service : null;
826
+ if (!runtime) {
827
+ return;
828
+ }
829
+
830
+ runtime.syncConversationState(context.conversation, context.externalContextPaths);
831
+ await runtime.ensureReady();
832
+ if (ProviderRegistry.getCapabilities(providerId).supportsProviderCommands) {
833
+ await this.getSdkCommands(tab.id);
834
+ }
835
+ }
836
+
837
+ private async buildProviderWarmupContext(
838
+ tab: TabData,
839
+ providerId: ProviderId,
840
+ ): Promise<ProviderWarmupContext> {
841
+ const conversation = tab.conversationId
842
+ ? await this.plugin.getConversationById(tab.conversationId)
843
+ : null;
844
+ const hasConversationContext = (conversation?.messages.length ?? 0) > 0;
845
+ const externalContextPaths = tab.ui.externalContextSelector?.getExternalContexts()
846
+ ?? (hasConversationContext
847
+ ? conversation?.externalContextPaths ?? []
848
+ : this.plugin.settings.persistentExternalContextPaths ?? []);
849
+ const runtime = tab.service?.providerId === providerId ? tab.service : null;
850
+ const warmupMode = this.resolveProviderTabWarmupMode({
851
+ conversation,
852
+ externalContextPaths,
853
+ plugin: this.plugin,
854
+ runtime,
855
+ tab: {
856
+ conversationId: tab.conversationId,
857
+ draftModel: tab.draftModel,
858
+ lifecycleState: tab.lifecycleState,
859
+ providerId,
860
+ },
861
+ });
862
+
863
+ return {
864
+ conversation,
865
+ externalContextPaths,
866
+ runtime,
867
+ tab: {
868
+ conversationId: tab.conversationId,
869
+ draftModel: tab.draftModel,
870
+ lifecycleState: tab.lifecycleState,
871
+ providerId,
872
+ },
873
+ warmupMode,
874
+ };
875
+ }
876
+
877
+ private resolveProviderTabWarmupMode(context: ProviderTabWarmupContext): ProviderTabWarmupMode {
878
+ return ProviderWorkspaceRegistry.getTabWarmupPolicy(context.tab.providerId)?.resolveMode(context) ?? 'none';
879
+ }
880
+
881
+ private buildProviderCommandContext(
882
+ tab: TabData,
883
+ providerId: ProviderId,
884
+ warmupContext: ProviderWarmupContext,
885
+ ): ProviderCommandContext {
886
+ const providerSettings = ProviderSettingsCoordinator.getProviderSettingsSnapshot(
887
+ this.plugin.settings,
888
+ providerId,
889
+ );
890
+
891
+ return {
892
+ ...warmupContext,
893
+ cacheKey: JSON.stringify({
894
+ allowSessionCreation: warmupContext.warmupMode === 'commands'
895
+ && tab.lifecycleState === 'blank'
896
+ && tab.id === this.activeTabId,
897
+ conversationId: warmupContext.conversation?.id ?? null,
898
+ draftModel: tab.draftModel ?? null,
899
+ externalContextPaths: warmupContext.externalContextPaths,
900
+ lifecycleState: tab.lifecycleState,
901
+ providerId,
902
+ providerSettings,
903
+ providerState: warmupContext.conversation?.providerState ?? null,
904
+ sessionId: warmupContext.conversation?.sessionId ?? null,
905
+ warmupMode: warmupContext.warmupMode,
906
+ }),
907
+ };
908
+ }
909
+
910
+ private async warmProviderCommandRuntime(
911
+ tab: TabData,
912
+ providerId: ProviderId,
913
+ context: ProviderCommandContext,
914
+ ): Promise<SlashCommand[]> {
915
+ const catalog = ProviderWorkspaceRegistry.getCommandCatalog(providerId);
916
+ const loader = ProviderWorkspaceRegistry.getRuntimeCommandLoader(providerId);
917
+ if (!catalog || !loader) {
918
+ return [];
919
+ }
920
+ const commands = await loader.loadCommands({
921
+ allowSessionCreation: context.warmupMode === 'commands'
922
+ && tab.lifecycleState === 'blank'
923
+ && tab.id === this.activeTabId,
924
+ conversation: context.conversation,
925
+ externalContextPaths: context.externalContextPaths,
926
+ plugin: this.plugin,
927
+ runtime: context.runtime,
928
+ });
929
+
930
+ if (!context.runtime || !context.runtime.isReady()) {
931
+ this.providerCommandCache.set(tab.id, {
932
+ key: context.cacheKey,
933
+ commands: commands.map((command) => ({ ...command })),
934
+ });
935
+ } else {
936
+ this.providerCommandCache.delete(tab.id);
937
+ }
938
+ catalog.setRuntimeCommands(commands);
939
+ return commands;
940
+ }
941
+
942
+ // ============================================
943
+ // Provider Command Catalog
944
+ // ============================================
945
+
946
+ private getProviderCatalogConfig(tab: TabData) {
947
+ const providerId = getTabProviderId(tab, this.plugin);
948
+ const catalog = ProviderWorkspaceRegistry.getCommandCatalog(providerId);
949
+ if (!catalog) return null;
950
+
951
+ return {
952
+ config: catalog.getDropdownConfig(),
953
+ getEntries: async () => {
954
+ await this.getSdkCommands(tab.id);
955
+ return catalog.listDropdownEntries({ includeBuiltIns: false });
956
+ },
957
+ };
958
+ }
959
+
960
+ // ============================================
961
+ // Broadcast
962
+ // ============================================
963
+
964
+ /**
965
+ * Broadcasts a function call to all initialized tab runtimes.
966
+ * Used by settings managers to apply configuration changes to all tabs.
967
+ * @param fn Function to call on each runtime.
968
+ */
969
+ async broadcastToAllTabs(fn: (service: ChatRuntime) => Promise<void>): Promise<void> {
970
+ await this.broadcastToTabs(this.tabs.values(), fn);
971
+ }
972
+
973
+ async broadcastToProviderTabs(
974
+ providerIds: ProviderId | ProviderId[],
975
+ fn: (service: ChatRuntime) => Promise<void>,
976
+ ): Promise<void> {
977
+ await this.broadcastToTabs(
978
+ this.filterTabsByProvider(providerIds, (tab) => tab.service?.providerId ?? tab.providerId),
979
+ fn,
980
+ );
981
+ }
982
+
983
+ private async broadcastToTabs(
984
+ tabs: Iterable<TabData>,
985
+ fn: (service: ChatRuntime) => Promise<void>,
986
+ ): Promise<void> {
987
+ const promises: Promise<void>[] = [];
988
+
989
+ for (const tab of tabs) {
990
+ if (tab.service && tab.serviceInitialized) {
991
+ promises.push(
992
+ fn(tab.service).catch(() => {
993
+ // Silently ignore broadcast errors
994
+ })
995
+ );
996
+ }
997
+ }
998
+
999
+ await Promise.all(promises);
1000
+ }
1001
+
1002
+ // ============================================
1003
+ // Cleanup
1004
+ // ============================================
1005
+
1006
+ /** Destroys all tabs and cleans up resources. */
1007
+ async destroy(): Promise<void> {
1008
+ // Save all conversations in parallel (independent per-tab)
1009
+ await Promise.all(
1010
+ Array.from(this.tabs.values()).map(
1011
+ tab => tab.controllers.conversationController?.save() ?? Promise.resolve()
1012
+ )
1013
+ );
1014
+
1015
+ // Destroy all tabs in parallel (independent per-tab, must run after saves complete)
1016
+ await Promise.all(Array.from(this.tabs.values()).map(tab => destroyTab(tab)));
1017
+
1018
+ this.tabs.clear();
1019
+ this.activeTabId = null;
1020
+ }
1021
+ }