@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,1244 @@
1
+ import { Notice, setIcon } from 'obsidian';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ import type { McpServerManager } from '../../../core/mcp/McpServerManager';
6
+ import type {
7
+ ProviderCapabilities,
8
+ ProviderChatUIConfig,
9
+ ProviderModeSelectorConfig,
10
+ ProviderPermissionModeToggleConfig,
11
+ ProviderReasoningOption,
12
+ ProviderServiceTierToggleConfig,
13
+ ProviderUIOption,
14
+ } from '../../../core/providers/types';
15
+ import type {
16
+ ManagedMcpServer,
17
+ UsageInfo,
18
+ } from '../../../core/types';
19
+ import { appendCheckIcon, appendMcpIcon, createProviderIconSvg } from '../../../shared/icons';
20
+ import { filterValidPaths, findConflictingPath, isDuplicatePath, isValidDirectoryPath, validateDirectoryPath } from '../../../utils/externalContext';
21
+ import { expandHomePath, normalizePathForFilesystem } from '../../../utils/path';
22
+
23
+ interface ElectronOpenDialogResult {
24
+ canceled: boolean;
25
+ filePaths: string[];
26
+ }
27
+
28
+ interface ElectronRemoteApi {
29
+ dialog: {
30
+ showOpenDialog(options: { properties: string[]; title: string }): Promise<ElectronOpenDialogResult>;
31
+ };
32
+ }
33
+
34
+ function runToolbarAction(action: () => Promise<void>, failureMessage: string): void {
35
+ void action().catch(() => {
36
+ new Notice(failureMessage);
37
+ });
38
+ }
39
+
40
+ export interface ToolbarSettings {
41
+ model: string;
42
+ thinkingBudget: string;
43
+ effortLevel: string;
44
+ serviceTier: string;
45
+ permissionMode: string;
46
+ [key: string]: unknown;
47
+ }
48
+
49
+ export interface ToolbarCallbacks {
50
+ onModelChange: (model: string) => Promise<void>;
51
+ onModeChange: (mode: string) => Promise<void>;
52
+ onThinkingBudgetChange: (budget: string) => Promise<void>;
53
+ onEffortLevelChange: (effort: string) => Promise<void>;
54
+ onServiceTierChange: (serviceTier: string) => Promise<void>;
55
+ onPermissionModeChange: (mode: string) => Promise<void>;
56
+ getSettings: () => ToolbarSettings;
57
+ getEnvironmentVariables?: () => string;
58
+ getUIConfig: () => ProviderChatUIConfig;
59
+ getCapabilities: () => ProviderCapabilities;
60
+ }
61
+
62
+ export class ModelSelector {
63
+ private container: HTMLElement;
64
+ private buttonEl: HTMLElement | null = null;
65
+ private dropdownEl: HTMLElement | null = null;
66
+ private callbacks: ToolbarCallbacks;
67
+ constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
68
+ this.callbacks = callbacks;
69
+ this.container = parentEl.createDiv({ cls: 'claudian-model-selector' });
70
+ this.render();
71
+ }
72
+
73
+ private getAvailableModels() {
74
+ const settings = this.callbacks.getSettings();
75
+ const uiConfig = this.callbacks.getUIConfig();
76
+ return uiConfig.getModelOptions({
77
+ ...settings,
78
+ environmentVariables: this.callbacks.getEnvironmentVariables?.(),
79
+ });
80
+ }
81
+
82
+ private render() {
83
+ this.container.empty();
84
+
85
+ this.buttonEl = this.container.createDiv({ cls: 'claudian-model-btn' });
86
+ this.updateDisplay();
87
+
88
+ this.dropdownEl = this.container.createDiv({ cls: 'claudian-model-dropdown' });
89
+ this.renderOptions();
90
+ }
91
+
92
+ updateDisplay() {
93
+ if (!this.buttonEl) return;
94
+ const currentModel = this.callbacks.getSettings().model;
95
+ const models = this.getAvailableModels();
96
+ const modelInfo = models.find(m => m.value === currentModel);
97
+
98
+ const displayModel = modelInfo || models[0];
99
+
100
+ this.buttonEl.empty();
101
+
102
+ const labelEl = this.buttonEl.createSpan({ cls: 'claudian-model-label' });
103
+ labelEl.setText(displayModel?.label || 'Unknown');
104
+ }
105
+
106
+ renderOptions() {
107
+ if (!this.dropdownEl) return;
108
+ this.dropdownEl.empty();
109
+
110
+ const currentModel = this.callbacks.getSettings().model;
111
+ const models = this.getAvailableModels();
112
+ const reversed = [...models].reverse();
113
+
114
+ let lastGroup: string | undefined;
115
+ for (const model of reversed) {
116
+ if (model.group && model.group !== lastGroup) {
117
+ const separator = this.dropdownEl.createDiv({ cls: 'claudian-model-group' });
118
+ separator.setText(model.group);
119
+ lastGroup = model.group;
120
+ }
121
+
122
+ const option = this.dropdownEl.createDiv({ cls: 'claudian-model-option' });
123
+ if (model.value === currentModel) {
124
+ option.addClass('selected');
125
+ }
126
+
127
+ const icon = model.providerIcon ?? this.callbacks.getUIConfig().getProviderIcon?.();
128
+ if (icon) {
129
+ option.appendChild(createProviderIconSvg(icon, {
130
+ className: 'claudian-model-provider-icon',
131
+ height: 12,
132
+ ownerDocument: option.ownerDocument,
133
+ width: 12,
134
+ }));
135
+ }
136
+ option.createSpan({ text: model.label });
137
+ if (model.description) {
138
+ option.setAttribute('title', model.description);
139
+ }
140
+
141
+ option.addEventListener('click', (e) => {
142
+ e.stopPropagation();
143
+ runToolbarAction(async () => {
144
+ await this.callbacks.onModelChange(model.value);
145
+ this.updateDisplay();
146
+ this.renderOptions();
147
+ }, 'Failed to change model');
148
+ });
149
+ }
150
+ }
151
+ }
152
+
153
+ export class ModeSelector {
154
+ private container: HTMLElement;
155
+ private labelEl: HTMLElement | null = null;
156
+ private toggleEl: HTMLElement | null = null;
157
+ private callbacks: ToolbarCallbacks;
158
+
159
+ constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
160
+ this.callbacks = callbacks;
161
+ this.container = parentEl.createDiv({ cls: 'claudian-mode-selector' });
162
+ this.render();
163
+ }
164
+
165
+ private getSelectorConfig(): ProviderModeSelectorConfig | null {
166
+ return this.callbacks.getUIConfig().getModeSelector?.(this.callbacks.getSettings()) ?? null;
167
+ }
168
+
169
+ private render() {
170
+ this.container.empty();
171
+
172
+ this.labelEl = this.container.createSpan({ cls: 'claudian-mode-label' });
173
+ this.toggleEl = this.container.createDiv({ cls: 'claudian-toggle-switch' });
174
+
175
+ this.toggleEl.addEventListener('click', () => {
176
+ runToolbarAction(() => this.toggle(), 'Failed to change mode');
177
+ });
178
+
179
+ this.updateDisplay();
180
+ }
181
+
182
+ /** Resolves the active/inactive option pair for a two-option toggle. */
183
+ private resolveOptionPair(
184
+ selectorConfig: ProviderModeSelectorConfig,
185
+ ): { active: ProviderUIOption; inactive: ProviderUIOption } {
186
+ const [first, second] = selectorConfig.options;
187
+ const active = selectorConfig.activeValue
188
+ ? selectorConfig.options.find((option) => option.value === selectorConfig.activeValue) ?? second
189
+ : second;
190
+ const inactive = active.value === first.value ? second : first;
191
+ return { active, inactive };
192
+ }
193
+
194
+ updateDisplay() {
195
+ if (!this.toggleEl || !this.labelEl) {
196
+ return;
197
+ }
198
+
199
+ const selectorConfig = this.getSelectorConfig();
200
+ if (!selectorConfig || selectorConfig.options.length !== 2) {
201
+ this.container.addClass('claudian-hidden');
202
+ return;
203
+ }
204
+
205
+ this.container.removeClass('claudian-hidden');
206
+ const { active, inactive } = this.resolveOptionPair(selectorConfig);
207
+ const currentOption = selectorConfig.options.find((option) => option.value === selectorConfig.value)
208
+ ?? selectorConfig.options[0];
209
+ const isActive = currentOption.value === active.value;
210
+
211
+ this.labelEl.setText(currentOption.label || selectorConfig.label);
212
+ this.labelEl.toggleClass('active', isActive);
213
+ if (isActive) {
214
+ this.toggleEl.addClass('active');
215
+ } else {
216
+ this.toggleEl.removeClass('active');
217
+ }
218
+
219
+ const titleParts = [`${inactive.label} <-> ${active.label}`];
220
+ if (currentOption.description) {
221
+ titleParts.push(currentOption.description);
222
+ }
223
+ this.container.setAttribute('title', titleParts.join('\n'));
224
+ }
225
+
226
+ renderOptions() {
227
+ this.updateDisplay();
228
+ }
229
+
230
+ private async toggle() {
231
+ const selectorConfig = this.getSelectorConfig();
232
+ if (!selectorConfig || selectorConfig.options.length !== 2) {
233
+ return;
234
+ }
235
+
236
+ const { active, inactive } = this.resolveOptionPair(selectorConfig);
237
+ const nextValue = selectorConfig.value === active.value ? inactive.value : active.value;
238
+ await this.callbacks.onModeChange(nextValue);
239
+ this.updateDisplay();
240
+ }
241
+ }
242
+
243
+ export class ThinkingBudgetSelector {
244
+ private container: HTMLElement;
245
+ private effortEl: HTMLElement | null = null;
246
+ private effortGearsEl: HTMLElement | null = null;
247
+ private budgetEl: HTMLElement | null = null;
248
+ private budgetGearsEl: HTMLElement | null = null;
249
+ private callbacks: ToolbarCallbacks;
250
+
251
+ constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
252
+ this.callbacks = callbacks;
253
+ this.container = parentEl.createDiv({ cls: 'claudian-thinking-selector' });
254
+ this.render();
255
+ }
256
+
257
+ private render() {
258
+ this.container.empty();
259
+
260
+ // Effort selector (for adaptive thinking models)
261
+ this.effortEl = this.container.createDiv({ cls: 'claudian-thinking-effort' });
262
+ const effortLabel = this.effortEl.createSpan({ cls: 'claudian-thinking-label-text' });
263
+ effortLabel.setText('Effort:');
264
+ this.effortGearsEl = this.effortEl.createDiv({ cls: 'claudian-thinking-gears' });
265
+
266
+ // Legacy budget selector (for custom models)
267
+ this.budgetEl = this.container.createDiv({ cls: 'claudian-thinking-budget' });
268
+ const budgetLabel = this.budgetEl.createSpan({ cls: 'claudian-thinking-label-text' });
269
+ budgetLabel.setText('Thinking:');
270
+ this.budgetGearsEl = this.budgetEl.createDiv({ cls: 'claudian-thinking-gears' });
271
+
272
+ this.updateDisplay();
273
+ }
274
+
275
+ private renderEffortGears() {
276
+ if (!this.effortGearsEl) return;
277
+ this.effortGearsEl.empty();
278
+
279
+ const currentEffort = this.callbacks.getSettings().effortLevel;
280
+ const uiConfig = this.callbacks.getUIConfig();
281
+ const settings = this.callbacks.getSettings();
282
+ const model = settings.model;
283
+ const options = uiConfig.getReasoningOptions(model, settings);
284
+ const currentInfo = options.find(e => e.value === currentEffort);
285
+
286
+ const currentEl = this.effortGearsEl.createDiv({ cls: 'claudian-thinking-current' });
287
+ currentEl.setText(currentInfo?.label || options[0]?.label || 'High');
288
+
289
+ const optionsEl = this.effortGearsEl.createDiv({ cls: 'claudian-thinking-options' });
290
+
291
+ for (const effort of [...options].reverse()) {
292
+ const gearEl = optionsEl.createDiv({ cls: 'claudian-thinking-gear' });
293
+ gearEl.setText(effort.label);
294
+
295
+ if (effort.value === currentEffort) {
296
+ gearEl.addClass('selected');
297
+ }
298
+
299
+ gearEl.addEventListener('click', (e) => {
300
+ e.stopPropagation();
301
+ runToolbarAction(async () => {
302
+ await this.callbacks.onEffortLevelChange(effort.value);
303
+ this.updateDisplay();
304
+ }, 'Failed to change effort level');
305
+ });
306
+ }
307
+ }
308
+
309
+ private renderBudgetGears() {
310
+ if (!this.budgetGearsEl) return;
311
+ this.budgetGearsEl.empty();
312
+
313
+ const currentBudget = this.callbacks.getSettings().thinkingBudget;
314
+ const uiConfig = this.callbacks.getUIConfig();
315
+ const settings = this.callbacks.getSettings();
316
+ const model = settings.model;
317
+ const options: ProviderReasoningOption[] = uiConfig.getReasoningOptions(model, settings);
318
+ const currentBudgetInfo = options.find(b => b.value === currentBudget);
319
+
320
+ const currentEl = this.budgetGearsEl.createDiv({ cls: 'claudian-thinking-current' });
321
+ currentEl.setText(currentBudgetInfo?.label || options[0]?.label || 'Off');
322
+
323
+ const optionsEl = this.budgetGearsEl.createDiv({ cls: 'claudian-thinking-options' });
324
+
325
+ for (const budget of [...options].reverse()) {
326
+ const gearEl = optionsEl.createDiv({ cls: 'claudian-thinking-gear' });
327
+ gearEl.setText(budget.label);
328
+ const tokens = budget.tokens ?? 0;
329
+ gearEl.setAttribute('title', tokens > 0 ? `${tokens.toLocaleString()} tokens` : 'Disabled');
330
+
331
+ if (budget.value === currentBudget) {
332
+ gearEl.addClass('selected');
333
+ }
334
+
335
+ gearEl.addEventListener('click', (e) => {
336
+ e.stopPropagation();
337
+ runToolbarAction(async () => {
338
+ await this.callbacks.onThinkingBudgetChange(budget.value);
339
+ this.updateDisplay();
340
+ }, 'Failed to change thinking budget');
341
+ });
342
+ }
343
+ }
344
+
345
+ updateDisplay() {
346
+ const capabilities = this.callbacks.getCapabilities();
347
+ if (capabilities.reasoningControl === 'none') {
348
+ this.effortEl?.addClass('claudian-hidden');
349
+ this.budgetEl?.addClass('claudian-hidden');
350
+ return;
351
+ }
352
+
353
+ const settings = this.callbacks.getSettings();
354
+ const model = settings.model;
355
+ const uiConfig = this.callbacks.getUIConfig();
356
+ const options = uiConfig.getReasoningOptions(model, settings);
357
+ const defaultValue = uiConfig.getDefaultReasoningValue(model, settings);
358
+ const shouldHide = options.length === 0
359
+ || (options.length === 1 && options[0]?.value === defaultValue);
360
+
361
+ if (shouldHide) {
362
+ this.effortEl?.addClass('claudian-hidden');
363
+ this.budgetEl?.addClass('claudian-hidden');
364
+ return;
365
+ }
366
+
367
+ const adaptive = uiConfig.isAdaptiveReasoningModel(model, settings);
368
+
369
+ if (this.effortEl) {
370
+ this.effortEl.toggleClass('claudian-hidden', !adaptive);
371
+ }
372
+ if (this.budgetEl) {
373
+ this.budgetEl.toggleClass('claudian-hidden', adaptive);
374
+ }
375
+
376
+ if (adaptive) {
377
+ this.renderEffortGears();
378
+ } else {
379
+ this.renderBudgetGears();
380
+ }
381
+ }
382
+ }
383
+
384
+ export class PermissionToggle {
385
+ private container: HTMLElement;
386
+ private toggleEl: HTMLElement | null = null;
387
+ private labelEl: HTMLElement | null = null;
388
+ private callbacks: ToolbarCallbacks;
389
+ private visible = true;
390
+
391
+ constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
392
+ this.callbacks = callbacks;
393
+ this.container = parentEl.createDiv({ cls: 'claudian-permission-toggle' });
394
+ this.render();
395
+ }
396
+
397
+ setVisible(visible: boolean): void {
398
+ this.visible = visible;
399
+ this.updateDisplay();
400
+ }
401
+
402
+ private render() {
403
+ this.container.empty();
404
+
405
+ this.labelEl = this.container.createSpan({ cls: 'claudian-permission-label' });
406
+ this.toggleEl = this.container.createDiv({ cls: 'claudian-toggle-switch' });
407
+
408
+ this.updateDisplay();
409
+
410
+ this.toggleEl.addEventListener('click', () => {
411
+ runToolbarAction(() => this.toggle(), 'Failed to change permission mode');
412
+ });
413
+ }
414
+
415
+ private getToggleConfig(): ProviderPermissionModeToggleConfig | null {
416
+ const uiConfig = this.callbacks.getUIConfig();
417
+ return uiConfig.getPermissionModeToggle?.() ?? null;
418
+ }
419
+
420
+ updateDisplay() {
421
+ if (!this.toggleEl || !this.labelEl) return;
422
+
423
+ const toggleConfig = this.getToggleConfig();
424
+ const capabilities = this.callbacks.getCapabilities();
425
+ if (!this.visible || !toggleConfig) {
426
+ this.container.addClass('claudian-hidden');
427
+ return;
428
+ }
429
+
430
+ this.container.removeClass('claudian-hidden');
431
+ const mode = this.callbacks.getSettings().permissionMode;
432
+ const planValue = toggleConfig.planValue;
433
+ const planLabel = toggleConfig.planLabel ?? 'PLAN';
434
+ const canShowPlan = Boolean(planValue) && capabilities.supportsPlanMode;
435
+
436
+ if (canShowPlan && planValue && mode === planValue) {
437
+ this.toggleEl.addClass('claudian-hidden');
438
+ this.labelEl.setText(planLabel);
439
+ this.labelEl.addClass('plan-active');
440
+ } else {
441
+ this.toggleEl.removeClass('claudian-hidden');
442
+ this.labelEl.removeClass('plan-active');
443
+ if (mode === toggleConfig.activeValue) {
444
+ this.toggleEl.addClass('active');
445
+ this.labelEl.setText(toggleConfig.activeLabel);
446
+ } else {
447
+ this.toggleEl.removeClass('active');
448
+ this.labelEl.setText(toggleConfig.inactiveLabel);
449
+ }
450
+ }
451
+ }
452
+
453
+ private async toggle() {
454
+ const toggleConfig = this.getToggleConfig();
455
+ if (!toggleConfig) return;
456
+
457
+ const current = this.callbacks.getSettings().permissionMode;
458
+ const newMode = current === toggleConfig.activeValue
459
+ ? toggleConfig.inactiveValue
460
+ : toggleConfig.activeValue;
461
+ await this.callbacks.onPermissionModeChange(newMode);
462
+ this.updateDisplay();
463
+ }
464
+ }
465
+
466
+ export class ServiceTierToggle {
467
+ private container: HTMLElement;
468
+ private buttonEl: HTMLElement | null = null;
469
+ private iconEl: HTMLElement | null = null;
470
+ private callbacks: ToolbarCallbacks;
471
+
472
+ constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
473
+ this.callbacks = callbacks;
474
+ this.container = parentEl.createDiv({ cls: 'claudian-service-tier-toggle' });
475
+ this.render();
476
+ }
477
+
478
+ private render() {
479
+ this.container.empty();
480
+
481
+ this.buttonEl = this.container.createDiv({ cls: 'claudian-service-tier-button' });
482
+ this.iconEl = this.buttonEl.createSpan({ cls: 'claudian-service-tier-icon' });
483
+ setIcon(this.iconEl, 'zap');
484
+
485
+ this.updateDisplay();
486
+
487
+ this.buttonEl.addEventListener('click', () => {
488
+ runToolbarAction(() => this.toggle(), 'Failed to change service tier');
489
+ });
490
+ }
491
+
492
+ private getToggleConfig(): ProviderServiceTierToggleConfig | null {
493
+ const uiConfig = this.callbacks.getUIConfig();
494
+ return uiConfig.getServiceTierToggle?.(this.callbacks.getSettings()) ?? null;
495
+ }
496
+
497
+ updateDisplay() {
498
+ if (!this.buttonEl || !this.iconEl) return;
499
+
500
+ const toggleConfig = this.getToggleConfig();
501
+ if (!toggleConfig) {
502
+ this.container.addClass('claudian-hidden');
503
+ return;
504
+ }
505
+
506
+ this.container.removeClass('claudian-hidden');
507
+ const current = this.callbacks.getSettings().serviceTier;
508
+ const isActive = current === toggleConfig.activeValue;
509
+ if (isActive) {
510
+ this.buttonEl.addClass('active');
511
+ } else {
512
+ this.buttonEl.removeClass('active');
513
+ }
514
+
515
+ this.container.setAttribute('title', 'Toggle on/off fast mode');
516
+ }
517
+
518
+ private async toggle() {
519
+ const toggleConfig = this.getToggleConfig();
520
+ if (!toggleConfig) return;
521
+
522
+ const current = this.callbacks.getSettings().serviceTier;
523
+ const next = current === toggleConfig.activeValue
524
+ ? toggleConfig.inactiveValue
525
+ : toggleConfig.activeValue;
526
+ await this.callbacks.onServiceTierChange(next);
527
+ this.updateDisplay();
528
+ }
529
+ }
530
+
531
+ export type AddExternalContextResult =
532
+ | { success: true; normalizedPath: string }
533
+ | { success: false; error: string };
534
+
535
+ export class ExternalContextSelector {
536
+ private container: HTMLElement;
537
+ private iconEl: HTMLElement | null = null;
538
+ private badgeEl: HTMLElement | null = null;
539
+ private dropdownEl: HTMLElement | null = null;
540
+ private callbacks: ToolbarCallbacks;
541
+ /**
542
+ * Current external context paths. May contain:
543
+ * - Persistent paths only (new sessions via clearExternalContexts)
544
+ * - Restored session paths (loaded sessions via setExternalContexts)
545
+ * - Mixed paths during active sessions
546
+ */
547
+ private externalContextPaths: string[] = [];
548
+ /** Paths that persist across all sessions (stored in settings). */
549
+ private persistentPaths: Set<string> = new Set();
550
+ private onChangeCallback: ((paths: string[]) => void) | null = null;
551
+ private onPersistenceChangeCallback: ((paths: string[]) => void) | null = null;
552
+
553
+ constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
554
+ this.callbacks = callbacks;
555
+ this.container = parentEl.createDiv({ cls: 'claudian-external-context-selector' });
556
+ this.render();
557
+ }
558
+
559
+ setOnChange(callback: (paths: string[]) => void): void {
560
+ this.onChangeCallback = callback;
561
+ }
562
+
563
+ setOnPersistenceChange(callback: (paths: string[]) => void): void {
564
+ this.onPersistenceChangeCallback = callback;
565
+ }
566
+
567
+ getExternalContexts(): string[] {
568
+ return [...this.externalContextPaths];
569
+ }
570
+
571
+ getPersistentPaths(): string[] {
572
+ return [...this.persistentPaths];
573
+ }
574
+
575
+ setPersistentPaths(paths: string[]): void {
576
+ // Validate paths - remove non-existent directories
577
+ const validPaths = filterValidPaths(paths);
578
+ const invalidPaths = paths.filter(p => !validPaths.includes(p));
579
+
580
+ this.persistentPaths = new Set(validPaths);
581
+ // Merge persistent paths into external context paths
582
+ this.mergePersistentPaths();
583
+ this.updateDisplay();
584
+ this.renderDropdown();
585
+
586
+ // If invalid paths were removed, notify user and save updated list
587
+ if (invalidPaths.length > 0) {
588
+ const pathNames = invalidPaths.map(p => this.shortenPath(p)).join(', ');
589
+ new Notice(`Removed ${invalidPaths.length} invalid external context path(s): ${pathNames}`, 5000);
590
+ this.onPersistenceChangeCallback?.([...this.persistentPaths]);
591
+ }
592
+ }
593
+
594
+ togglePersistence(path: string): void {
595
+ if (this.persistentPaths.has(path)) {
596
+ this.persistentPaths.delete(path);
597
+ } else {
598
+ // Validate path still exists before persisting
599
+ if (!isValidDirectoryPath(path)) {
600
+ new Notice(`Cannot persist "${this.shortenPath(path)}" - directory no longer exists`, 4000);
601
+ return;
602
+ }
603
+ this.persistentPaths.add(path);
604
+ }
605
+ this.onPersistenceChangeCallback?.([...this.persistentPaths]);
606
+ this.renderDropdown();
607
+ }
608
+
609
+ private mergePersistentPaths(): void {
610
+ const pathSet = new Set(this.externalContextPaths);
611
+ for (const path of this.persistentPaths) {
612
+ pathSet.add(path);
613
+ }
614
+ this.externalContextPaths = [...pathSet];
615
+ }
616
+
617
+ /**
618
+ * Restore exact external context paths from a saved conversation.
619
+ * Does NOT merge with persistent paths - preserves the session's historical state.
620
+ * Use clearExternalContexts() for new sessions to start with current persistent paths.
621
+ */
622
+ setExternalContexts(paths: string[]): void {
623
+ this.externalContextPaths = [...paths];
624
+ this.updateDisplay();
625
+ this.renderDropdown();
626
+ }
627
+
628
+ /**
629
+ * Remove a path from external contexts (and persistent paths if applicable).
630
+ * Exposed for testing the remove button behavior.
631
+ */
632
+ removePath(pathStr: string): void {
633
+ this.externalContextPaths = this.externalContextPaths.filter(p => p !== pathStr);
634
+ // Also remove from persistent paths if it was persistent
635
+ if (this.persistentPaths.has(pathStr)) {
636
+ this.persistentPaths.delete(pathStr);
637
+ this.onPersistenceChangeCallback?.([...this.persistentPaths]);
638
+ }
639
+ this.onChangeCallback?.(this.externalContextPaths);
640
+ this.updateDisplay();
641
+ this.renderDropdown();
642
+ }
643
+
644
+ /**
645
+ * Add an external context path programmatically (e.g., from /add-dir command).
646
+ * Validates the path and handles duplicates/conflicts.
647
+ * @param pathInput - Path string (supports ~/ expansion)
648
+ * @returns Result with success status and normalized path, or error message on failure
649
+ */
650
+ addExternalContext(pathInput: string): AddExternalContextResult {
651
+ const trimmed = pathInput?.trim();
652
+ if (!trimmed) {
653
+ return { success: false, error: 'No path provided. Usage: /add-dir /absolute/path' };
654
+ }
655
+
656
+ // Strip surrounding quotes if present (e.g., "/path/with spaces")
657
+ let cleanPath = trimmed;
658
+ if ((cleanPath.startsWith('"') && cleanPath.endsWith('"')) ||
659
+ (cleanPath.startsWith("'") && cleanPath.endsWith("'"))) {
660
+ cleanPath = cleanPath.slice(1, -1);
661
+ }
662
+
663
+ // Expand home directory and normalize path
664
+ const expandedPath = expandHomePath(cleanPath);
665
+ const normalizedPath = normalizePathForFilesystem(expandedPath);
666
+
667
+ if (!path.isAbsolute(normalizedPath)) {
668
+ return { success: false, error: 'Path must be absolute. Usage: /add-dir /absolute/path' };
669
+ }
670
+
671
+ // Validate path exists and is a directory with specific error messages
672
+ const validation = validateDirectoryPath(normalizedPath);
673
+ if (!validation.valid) {
674
+ return { success: false, error: `${validation.error}: ${pathInput}` };
675
+ }
676
+
677
+ // Check for duplicate (normalized comparison for cross-platform support)
678
+ if (isDuplicatePath(normalizedPath, this.externalContextPaths)) {
679
+ return { success: false, error: 'This folder is already added as an external context.' };
680
+ }
681
+
682
+ // Check for nested/overlapping paths
683
+ const conflict = findConflictingPath(normalizedPath, this.externalContextPaths);
684
+ if (conflict) {
685
+ return { success: false, error: this.formatConflictMessage(normalizedPath, conflict) };
686
+ }
687
+
688
+ // Add the path
689
+ this.externalContextPaths = [...this.externalContextPaths, normalizedPath];
690
+ this.onChangeCallback?.(this.externalContextPaths);
691
+ this.updateDisplay();
692
+ this.renderDropdown();
693
+
694
+ return { success: true, normalizedPath };
695
+ }
696
+
697
+ /**
698
+ * Clear session-only external context paths (call on new conversation).
699
+ * Uses persistent paths from settings if provided, otherwise falls back to local cache.
700
+ * Validates paths before using them (silently filters invalid during session init).
701
+ */
702
+ clearExternalContexts(persistentPathsFromSettings?: string[]): void {
703
+ // Use settings value if provided (most up-to-date), otherwise use local cache
704
+ if (persistentPathsFromSettings) {
705
+ // Validate paths - silently filter during session initialization (not user action)
706
+ const validPaths = filterValidPaths(persistentPathsFromSettings);
707
+ this.persistentPaths = new Set(validPaths);
708
+ }
709
+ this.externalContextPaths = [...this.persistentPaths];
710
+ this.updateDisplay();
711
+ this.renderDropdown();
712
+ }
713
+
714
+ private render() {
715
+ this.container.empty();
716
+
717
+ const iconWrapper = this.container.createDiv({ cls: 'claudian-external-context-icon-wrapper' });
718
+
719
+ this.iconEl = iconWrapper.createDiv({ cls: 'claudian-external-context-icon' });
720
+ setIcon(this.iconEl, 'folder');
721
+
722
+ this.badgeEl = iconWrapper.createDiv({ cls: 'claudian-external-context-badge' });
723
+
724
+ this.updateDisplay();
725
+
726
+ // Click to open native folder picker
727
+ iconWrapper.addEventListener('click', (e) => {
728
+ e.stopPropagation();
729
+ void this.openFolderPicker();
730
+ });
731
+
732
+ this.dropdownEl = this.container.createDiv({ cls: 'claudian-external-context-dropdown' });
733
+ this.renderDropdown();
734
+ }
735
+
736
+ private async openFolderPicker() {
737
+ try {
738
+ // Access Electron's dialog through remote
739
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- Electron remote is exposed only at runtime in Obsidian's renderer.
740
+ const { remote } = require('electron') as { remote?: ElectronRemoteApi };
741
+ if (!remote) {
742
+ throw new Error('Electron remote API is unavailable');
743
+ }
744
+ const result = await remote.dialog.showOpenDialog({
745
+ properties: ['openDirectory'],
746
+ title: 'Select External Context',
747
+ });
748
+
749
+ if (!result.canceled && result.filePaths.length > 0) {
750
+ const selectedPath = result.filePaths[0];
751
+
752
+ // Check for duplicate (normalized comparison for cross-platform support)
753
+ if (isDuplicatePath(selectedPath, this.externalContextPaths)) {
754
+ new Notice('This folder is already added as an external context.', 3000);
755
+ return;
756
+ }
757
+
758
+ // Check for nested/overlapping paths
759
+ const conflict = findConflictingPath(selectedPath, this.externalContextPaths);
760
+ if (conflict) {
761
+ new Notice(this.formatConflictMessage(selectedPath, conflict), 5000);
762
+ return;
763
+ }
764
+
765
+ this.externalContextPaths = [...this.externalContextPaths, selectedPath];
766
+ this.onChangeCallback?.(this.externalContextPaths);
767
+ this.updateDisplay();
768
+ this.renderDropdown();
769
+ }
770
+ } catch {
771
+ new Notice('Unable to open folder picker.', 5000);
772
+ }
773
+ }
774
+
775
+ /** Formats a conflict error message for display. */
776
+ private formatConflictMessage(newPath: string, conflict: { path: string; type: 'parent' | 'child' }): string {
777
+ const shortNew = this.shortenPath(newPath);
778
+ const shortExisting = this.shortenPath(conflict.path);
779
+ return conflict.type === 'parent'
780
+ ? `Cannot add "${shortNew}" - it's inside existing path "${shortExisting}"`
781
+ : `Cannot add "${shortNew}" - it contains existing path "${shortExisting}"`;
782
+ }
783
+
784
+ private renderDropdown() {
785
+ if (!this.dropdownEl) return;
786
+
787
+ this.dropdownEl.empty();
788
+
789
+ // Header
790
+ const headerEl = this.dropdownEl.createDiv({ cls: 'claudian-external-context-header' });
791
+ headerEl.setText('External contexts');
792
+
793
+ // Path list
794
+ const listEl = this.dropdownEl.createDiv({ cls: 'claudian-external-context-list' });
795
+
796
+ if (this.externalContextPaths.length === 0) {
797
+ const emptyEl = listEl.createDiv({ cls: 'claudian-external-context-empty' });
798
+ emptyEl.setText('Click folder icon to add');
799
+ } else {
800
+ for (const pathStr of this.externalContextPaths) {
801
+ const itemEl = listEl.createDiv({ cls: 'claudian-external-context-item' });
802
+
803
+ const pathTextEl = itemEl.createSpan({ cls: 'claudian-external-context-text' });
804
+ // Show shortened path for display
805
+ const displayPath = this.shortenPath(pathStr);
806
+ pathTextEl.setText(displayPath);
807
+ pathTextEl.setAttribute('title', pathStr);
808
+
809
+ // Lock toggle button
810
+ const isPersistent = this.persistentPaths.has(pathStr);
811
+ const lockBtn = itemEl.createSpan({ cls: 'claudian-external-context-lock' });
812
+ if (isPersistent) {
813
+ lockBtn.addClass('locked');
814
+ }
815
+ setIcon(lockBtn, isPersistent ? 'lock' : 'unlock');
816
+ lockBtn.setAttribute('title', isPersistent ? 'Persistent (click to make session-only)' : 'Session-only (click to persist)');
817
+ lockBtn.addEventListener('click', (e) => {
818
+ e.stopPropagation();
819
+ this.togglePersistence(pathStr);
820
+ });
821
+
822
+ const removeBtn = itemEl.createSpan({ cls: 'claudian-external-context-remove' });
823
+ setIcon(removeBtn, 'x');
824
+ removeBtn.setAttribute('title', 'Remove path');
825
+ removeBtn.addEventListener('click', (e) => {
826
+ e.stopPropagation();
827
+ this.removePath(pathStr);
828
+ });
829
+ }
830
+ }
831
+ }
832
+
833
+ /** Shorten path for display (replace home dir with ~) */
834
+ private shortenPath(fullPath: string): string {
835
+ try {
836
+ const homeDir = os.homedir();
837
+ const normalize = (value: string) => value.replace(/\\/g, '/');
838
+ const normalizedFull = normalize(fullPath);
839
+ const normalizedHome = normalize(homeDir);
840
+ const compareFull = process.platform === 'win32'
841
+ ? normalizedFull.toLowerCase()
842
+ : normalizedFull;
843
+ const compareHome = process.platform === 'win32'
844
+ ? normalizedHome.toLowerCase()
845
+ : normalizedHome;
846
+ if (compareFull.startsWith(compareHome)) {
847
+ // Use normalized path length and normalize the result for consistent display
848
+ const remainder = normalizedFull.slice(normalizedHome.length);
849
+ return '~' + remainder;
850
+ }
851
+ } catch {
852
+ // Fall through to return full path
853
+ }
854
+ return fullPath;
855
+ }
856
+
857
+ updateDisplay() {
858
+ if (!this.iconEl || !this.badgeEl) return;
859
+
860
+ const count = this.externalContextPaths.length;
861
+
862
+ if (count > 0) {
863
+ this.iconEl.addClass('active');
864
+ this.iconEl.setAttribute('title', `${count} external context${count > 1 ? 's' : ''} (click to add more)`);
865
+
866
+ // Show badge only when more than 1 path
867
+ if (count > 1) {
868
+ this.badgeEl.setText(String(count));
869
+ this.badgeEl.addClass('visible');
870
+ } else {
871
+ this.badgeEl.removeClass('visible');
872
+ }
873
+ } else {
874
+ this.iconEl.removeClass('active');
875
+ this.iconEl.setAttribute('title', 'Add external contexts (click)');
876
+ this.badgeEl.removeClass('visible');
877
+ }
878
+ }
879
+ }
880
+
881
+ export class McpServerSelector {
882
+ private container: HTMLElement;
883
+ private iconEl: HTMLElement | null = null;
884
+ private badgeEl: HTMLElement | null = null;
885
+ private dropdownEl: HTMLElement | null = null;
886
+ private mcpManager: McpServerManager | null = null;
887
+ private enabledServers: Set<string> = new Set();
888
+ private onChangeCallback: ((enabled: Set<string>) => void) | null = null;
889
+ private visible = true;
890
+
891
+ constructor(parentEl: HTMLElement) {
892
+ this.container = parentEl.createDiv({ cls: 'claudian-mcp-selector' });
893
+ this.render();
894
+ }
895
+
896
+ setVisible(visible: boolean): void {
897
+ this.visible = visible;
898
+ if (!visible) {
899
+ this.container.addClass('claudian-hidden');
900
+ } else {
901
+ this.updateDisplay();
902
+ }
903
+ }
904
+
905
+ setMcpManager(manager: McpServerManager | null): void {
906
+ this.mcpManager = manager;
907
+ if (!manager && this.enabledServers.size > 0) {
908
+ this.enabledServers.clear();
909
+ this.onChangeCallback?.(this.enabledServers);
910
+ }
911
+ this.pruneEnabledServers();
912
+ this.updateDisplay();
913
+ this.renderDropdown();
914
+ }
915
+
916
+ setOnChange(callback: (enabled: Set<string>) => void): void {
917
+ this.onChangeCallback = callback;
918
+ }
919
+
920
+ getEnabledServers(): Set<string> {
921
+ return new Set(this.enabledServers);
922
+ }
923
+
924
+ addMentionedServers(names: Set<string>): void {
925
+ let changed = false;
926
+ for (const name of names) {
927
+ if (!this.enabledServers.has(name)) {
928
+ this.enabledServers.add(name);
929
+ changed = true;
930
+ }
931
+ }
932
+ if (changed) {
933
+ this.updateDisplay();
934
+ this.renderDropdown();
935
+ }
936
+ }
937
+
938
+ clearEnabled(): void {
939
+ this.enabledServers.clear();
940
+ this.updateDisplay();
941
+ this.renderDropdown();
942
+ }
943
+
944
+ setEnabledServers(names: string[]): void {
945
+ this.enabledServers = new Set(names);
946
+ this.pruneEnabledServers();
947
+ this.updateDisplay();
948
+ this.renderDropdown();
949
+ }
950
+
951
+ private pruneEnabledServers(): void {
952
+ if (!this.mcpManager) return;
953
+ const activeNames = new Set(this.mcpManager.getServers().filter((s) => s.enabled).map((s) => s.name));
954
+ let changed = false;
955
+ for (const name of this.enabledServers) {
956
+ if (!activeNames.has(name)) {
957
+ this.enabledServers.delete(name);
958
+ changed = true;
959
+ }
960
+ }
961
+ if (changed) {
962
+ this.onChangeCallback?.(this.enabledServers);
963
+ }
964
+ }
965
+
966
+ private render() {
967
+ this.container.empty();
968
+
969
+ const iconWrapper = this.container.createDiv({ cls: 'claudian-mcp-selector-icon-wrapper' });
970
+
971
+ this.iconEl = iconWrapper.createDiv({ cls: 'claudian-mcp-selector-icon' });
972
+ appendMcpIcon(this.iconEl);
973
+
974
+ this.badgeEl = iconWrapper.createDiv({ cls: 'claudian-mcp-selector-badge' });
975
+
976
+ this.updateDisplay();
977
+
978
+ this.dropdownEl = this.container.createDiv({ cls: 'claudian-mcp-selector-dropdown' });
979
+ this.renderDropdown();
980
+
981
+ // Re-render dropdown content on hover (CSS handles visibility)
982
+ this.container.addEventListener('mouseenter', () => {
983
+ this.renderDropdown();
984
+ });
985
+ }
986
+
987
+ private renderDropdown() {
988
+ if (!this.dropdownEl) return;
989
+ this.pruneEnabledServers();
990
+ this.dropdownEl.empty();
991
+
992
+ // Header
993
+ const headerEl = this.dropdownEl.createDiv({ cls: 'claudian-mcp-selector-header' });
994
+ headerEl.setText('Mcp servers');
995
+
996
+ // Server list
997
+ const listEl = this.dropdownEl.createDiv({ cls: 'claudian-mcp-selector-list' });
998
+
999
+ const allServers = this.mcpManager?.getServers() || [];
1000
+ const servers = allServers.filter(s => s.enabled);
1001
+
1002
+ if (servers.length === 0) {
1003
+ const emptyEl = listEl.createDiv({ cls: 'claudian-mcp-selector-empty' });
1004
+ emptyEl.setText(allServers.length === 0 ? 'No MCP servers configured' : 'All MCP servers disabled');
1005
+ return;
1006
+ }
1007
+
1008
+ for (const server of servers) {
1009
+ this.renderServerItem(listEl, server);
1010
+ }
1011
+ }
1012
+
1013
+ private renderServerItem(listEl: HTMLElement, server: ManagedMcpServer) {
1014
+ const itemEl = listEl.createDiv({ cls: 'claudian-mcp-selector-item' });
1015
+ itemEl.dataset.serverName = server.name;
1016
+
1017
+ const isEnabled = this.enabledServers.has(server.name);
1018
+ if (isEnabled) {
1019
+ itemEl.addClass('enabled');
1020
+ }
1021
+
1022
+ // Checkbox
1023
+ const checkEl = itemEl.createDiv({ cls: 'claudian-mcp-selector-check' });
1024
+ if (isEnabled) {
1025
+ appendCheckIcon(checkEl);
1026
+ }
1027
+
1028
+ // Info
1029
+ const infoEl = itemEl.createDiv({ cls: 'claudian-mcp-selector-item-info' });
1030
+
1031
+ const nameEl = infoEl.createSpan({ cls: 'claudian-mcp-selector-item-name' });
1032
+ nameEl.setText(server.name);
1033
+
1034
+ // Badges
1035
+ if (server.contextSaving) {
1036
+ const csEl = infoEl.createSpan({ cls: 'claudian-mcp-selector-cs-badge' });
1037
+ csEl.setText('@');
1038
+ csEl.setAttribute('title', 'Context-saving: can also enable via @' + server.name);
1039
+ }
1040
+
1041
+ // Click to toggle (use mousedown for more reliable capture)
1042
+ itemEl.addEventListener('mousedown', (e) => {
1043
+ e.preventDefault();
1044
+ e.stopPropagation();
1045
+ this.toggleServer(server.name, itemEl);
1046
+ });
1047
+ }
1048
+
1049
+ private toggleServer(name: string, itemEl: HTMLElement) {
1050
+ if (this.enabledServers.has(name)) {
1051
+ this.enabledServers.delete(name);
1052
+ } else {
1053
+ this.enabledServers.add(name);
1054
+ }
1055
+
1056
+ // Update item visually in-place (immediate feedback)
1057
+ const isEnabled = this.enabledServers.has(name);
1058
+ const checkEl = itemEl.querySelector<HTMLElement>('.claudian-mcp-selector-check');
1059
+
1060
+ if (isEnabled) {
1061
+ itemEl.addClass('enabled');
1062
+ if (checkEl) appendCheckIcon(checkEl);
1063
+ } else {
1064
+ itemEl.removeClass('enabled');
1065
+ if (checkEl) checkEl.empty();
1066
+ }
1067
+
1068
+ this.updateDisplay();
1069
+ this.onChangeCallback?.(this.enabledServers);
1070
+ }
1071
+
1072
+ updateDisplay() {
1073
+ this.pruneEnabledServers();
1074
+ if (!this.iconEl || !this.badgeEl) return;
1075
+
1076
+ const count = this.enabledServers.size;
1077
+ const hasServers = (this.mcpManager?.getServers().length || 0) > 0;
1078
+
1079
+ // Show/hide container based on whether there are servers and visibility
1080
+ if (!hasServers || !this.visible) {
1081
+ this.container.addClass('claudian-hidden');
1082
+ return;
1083
+ }
1084
+ this.container.removeClass('claudian-hidden');
1085
+
1086
+ if (count > 0) {
1087
+ this.iconEl.addClass('active');
1088
+ this.iconEl.setAttribute('title', `${count} MCP server${count > 1 ? 's' : ''} enabled (click to manage)`);
1089
+
1090
+ // Show badge only when more than 1
1091
+ if (count > 1) {
1092
+ this.badgeEl.setText(String(count));
1093
+ this.badgeEl.addClass('visible');
1094
+ } else {
1095
+ this.badgeEl.removeClass('visible');
1096
+ }
1097
+ } else {
1098
+ this.iconEl.removeClass('active');
1099
+ this.iconEl.setAttribute('title', 'Mcp servers (click to enable)');
1100
+ this.badgeEl.removeClass('visible');
1101
+ }
1102
+ }
1103
+ }
1104
+
1105
+ export class ContextUsageMeter {
1106
+ private container: HTMLElement;
1107
+ private fillPath: SVGPathElement | null = null;
1108
+ private percentEl: HTMLElement | null = null;
1109
+ private circumference: number = 0;
1110
+
1111
+ constructor(parentEl: HTMLElement) {
1112
+ this.container = parentEl.createDiv({ cls: 'claudian-context-meter' });
1113
+ this.render();
1114
+ // Initially hidden
1115
+ this.container.addClass('claudian-hidden');
1116
+ }
1117
+
1118
+ setVisible(visible: boolean): void {
1119
+ this.container.toggleClass('claudian-hidden', !visible);
1120
+ }
1121
+
1122
+ private render() {
1123
+ const size = 16;
1124
+ const strokeWidth = 2;
1125
+ const radius = (size - strokeWidth) / 2;
1126
+ const cx = size / 2;
1127
+ const cy = size / 2;
1128
+
1129
+ // 240° arc: from 150° to 390° (upper-left through bottom to upper-right)
1130
+ const startAngle = 150;
1131
+ const endAngle = 390;
1132
+ const arcDegrees = endAngle - startAngle;
1133
+ const arcRadians = (arcDegrees * Math.PI) / 180;
1134
+ this.circumference = radius * arcRadians;
1135
+
1136
+ const startRad = (startAngle * Math.PI) / 180;
1137
+ const endRad = (endAngle * Math.PI) / 180;
1138
+ const x1 = cx + radius * Math.cos(startRad);
1139
+ const y1 = cy + radius * Math.sin(startRad);
1140
+ const x2 = cx + radius * Math.cos(endRad);
1141
+ const y2 = cy + radius * Math.sin(endRad);
1142
+
1143
+ const gaugeEl = this.container.createDiv({ cls: 'claudian-context-meter-gauge' });
1144
+ const svg = gaugeEl.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
1145
+ svg.setAttribute('width', String(size));
1146
+ svg.setAttribute('height', String(size));
1147
+ svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
1148
+
1149
+ const pathData = `M ${x1} ${y1} A ${radius} ${radius} 0 1 1 ${x2} ${y2}`;
1150
+ const backgroundPath = gaugeEl.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
1151
+ backgroundPath.classList.add('claudian-meter-bg');
1152
+ backgroundPath.setAttribute('d', pathData);
1153
+ backgroundPath.setAttribute('fill', 'none');
1154
+ backgroundPath.setAttribute('stroke-width', String(strokeWidth));
1155
+ backgroundPath.setAttribute('stroke-linecap', 'round');
1156
+
1157
+ const fillPath = gaugeEl.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
1158
+ fillPath.classList.add('claudian-meter-fill');
1159
+ fillPath.setAttribute('d', pathData);
1160
+ fillPath.setAttribute('fill', 'none');
1161
+ fillPath.setAttribute('stroke-width', String(strokeWidth));
1162
+ fillPath.setAttribute('stroke-linecap', 'round');
1163
+ fillPath.setAttribute('stroke-dasharray', String(this.circumference));
1164
+ fillPath.setAttribute('stroke-dashoffset', String(this.circumference));
1165
+
1166
+ svg.appendChild(backgroundPath);
1167
+ svg.appendChild(fillPath);
1168
+ gaugeEl.appendChild(svg);
1169
+ this.fillPath = fillPath;
1170
+
1171
+ this.percentEl = this.container.createSpan({ cls: 'claudian-context-meter-percent' });
1172
+ }
1173
+
1174
+ update(usage: UsageInfo | null): void {
1175
+ if (!usage || usage.contextTokens <= 0) {
1176
+ this.container.addClass('claudian-hidden');
1177
+ return;
1178
+ }
1179
+ this.container.removeClass('claudian-hidden');
1180
+ const fillLength = (usage.percentage / 100) * this.circumference;
1181
+ if (this.fillPath) {
1182
+ this.fillPath.setAttribute('stroke-dashoffset', String(this.circumference - fillLength));
1183
+ }
1184
+
1185
+ if (this.percentEl) {
1186
+ this.percentEl.setText(`${usage.percentage}%`);
1187
+ }
1188
+
1189
+ // Toggle warning class for > 80%
1190
+ if (usage.percentage > 80) {
1191
+ this.container.addClass('warning');
1192
+ } else {
1193
+ this.container.removeClass('warning');
1194
+ }
1195
+
1196
+ // Set tooltip with detailed usage
1197
+ let tooltip = `${this.formatTokens(usage.contextTokens)} / ${this.formatTokens(usage.contextWindow)}`;
1198
+ if (usage.percentage > 80) {
1199
+ tooltip += ' (Approaching limit, run `/compact` to continue)';
1200
+ }
1201
+ this.container.setAttribute('data-tooltip', tooltip);
1202
+ }
1203
+
1204
+ private formatTokens(tokens: number): string {
1205
+ if (tokens >= 1000) {
1206
+ return `${Math.round(tokens / 1000)}k`;
1207
+ }
1208
+ return String(tokens);
1209
+ }
1210
+ }
1211
+
1212
+ export function createInputToolbar(
1213
+ parentEl: HTMLElement,
1214
+ callbacks: ToolbarCallbacks
1215
+ ): {
1216
+ modelSelector: ModelSelector;
1217
+ modeSelector: ModeSelector;
1218
+ thinkingBudgetSelector: ThinkingBudgetSelector;
1219
+ contextUsageMeter: ContextUsageMeter | null;
1220
+ externalContextSelector: ExternalContextSelector;
1221
+ mcpServerSelector: McpServerSelector;
1222
+ permissionToggle: PermissionToggle;
1223
+ serviceTierToggle: ServiceTierToggle;
1224
+ } {
1225
+ const modelSelector = new ModelSelector(parentEl, callbacks);
1226
+ const thinkingBudgetSelector = new ThinkingBudgetSelector(parentEl, callbacks);
1227
+ const serviceTierToggle = new ServiceTierToggle(parentEl, callbacks);
1228
+ const contextUsageMeter = new ContextUsageMeter(parentEl);
1229
+ const externalContextSelector = new ExternalContextSelector(parentEl, callbacks);
1230
+ const mcpServerSelector = new McpServerSelector(parentEl);
1231
+ const permissionToggle = new PermissionToggle(parentEl, callbacks);
1232
+ const modeSelector = new ModeSelector(parentEl, callbacks);
1233
+
1234
+ return {
1235
+ modelSelector,
1236
+ modeSelector,
1237
+ thinkingBudgetSelector,
1238
+ serviceTierToggle,
1239
+ contextUsageMeter,
1240
+ externalContextSelector,
1241
+ mcpServerSelector,
1242
+ permissionToggle,
1243
+ };
1244
+ }