@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,1707 @@
1
+ import { Notice, setIcon } from 'obsidian';
2
+
3
+ import {
4
+ type BuiltInCommand,
5
+ detectBuiltInCommand,
6
+ isBuiltInCommandSupported,
7
+ } from '../../../core/commands/builtInCommands';
8
+ import { ProviderRegistry } from '../../../core/providers/ProviderRegistry';
9
+ import {
10
+ DEFAULT_CHAT_PROVIDER_ID,
11
+ type InstructionRefineService,
12
+ type ProviderCapabilities,
13
+ type ProviderId,
14
+ type TitleGenerationService,
15
+ } from '../../../core/providers/types';
16
+ import type { ChatRuntime } from '../../../core/runtime/ChatRuntime';
17
+ import {
18
+ cloneChatTurnRequest,
19
+ mergeQueuedChatTurns,
20
+ type QueuedChatTurn,
21
+ } from '../../../core/runtime/QueuedTurn';
22
+ import type {
23
+ ApprovalCallbackOptions,
24
+ ApprovalDecisionOption,
25
+ ChatTurnRequest,
26
+ } from '../../../core/runtime/types';
27
+ import { TOOL_EXIT_PLAN_MODE } from '../../../core/tools/toolNames';
28
+ import type { ApprovalDecision, ChatMessage, ExitPlanModeDecision, StreamChunk } from '../../../core/types';
29
+ import type ClaudianPlugin from '../../../main';
30
+ import { ResumeSessionDropdown } from '../../../shared/components/ResumeSessionDropdown';
31
+ import { InstructionModal } from '../../../shared/modals/InstructionConfirmModal';
32
+ import type { BrowserSelectionContext } from '../../../utils/browser';
33
+ import type { CanvasSelectionContext } from '../../../utils/canvas';
34
+ import { extractUserDisplayContent } from '../../../utils/context';
35
+ import { formatDurationMmSs } from '../../../utils/date';
36
+ import type { EditorSelectionContext } from '../../../utils/editor';
37
+ import { appendMarkdownSnippet } from '../../../utils/markdown';
38
+ import { COMPLETION_FLAVOR_WORDS } from '../constants';
39
+ import { type InlineAskQuestionConfig, InlineAskUserQuestion } from '../rendering/InlineAskUserQuestion';
40
+ import { InlineExitPlanMode } from '../rendering/InlineExitPlanMode';
41
+ import { InlinePlanApproval,type PlanApprovalDecision } from '../rendering/InlinePlanApproval';
42
+ import type { MessageRenderer } from '../rendering/MessageRenderer';
43
+ import { setToolIcon, updateToolCallResult } from '../rendering/ToolCallRenderer';
44
+ import type { SubagentManager } from '../services/SubagentManager';
45
+ import type { ChatState } from '../state/ChatState';
46
+ import type { QueuedMessage } from '../state/types';
47
+ import type { FileContextManager } from '../ui/FileContext';
48
+ import type { ImageContextManager } from '../ui/ImageContext';
49
+ import type { AddExternalContextResult, McpServerSelector } from '../ui/InputToolbar';
50
+ import type { InstructionModeManager } from '../ui/InstructionModeManager';
51
+ import type { StatusPanel } from '../ui/StatusPanel';
52
+ import type { BrowserSelectionController } from './BrowserSelectionController';
53
+ import type { CanvasSelectionController } from './CanvasSelectionController';
54
+ import type { ConversationController } from './ConversationController';
55
+ import type { SelectionController } from './SelectionController';
56
+ import type { StreamController } from './StreamController';
57
+
58
+ const APPROVAL_OPTION_MAP: Record<string, ApprovalDecision> = {
59
+ 'Deny': 'deny',
60
+ 'Allow once': 'allow',
61
+ 'Always allow': 'allow-always',
62
+ };
63
+
64
+ const DEFAULT_APPROVAL_DECISION_OPTIONS: ApprovalDecisionOption[] =
65
+ Object.entries(APPROVAL_OPTION_MAP).map(([label, decision]) => ({
66
+ label,
67
+ value: label,
68
+ decision,
69
+ }));
70
+
71
+ function toError(error: unknown): Error {
72
+ return error instanceof Error ? error : new Error(String(error));
73
+ }
74
+
75
+ export interface InputControllerDeps {
76
+ plugin: ClaudianPlugin;
77
+ state: ChatState;
78
+ renderer: MessageRenderer;
79
+ streamController: StreamController;
80
+ selectionController: SelectionController;
81
+ browserSelectionController?: BrowserSelectionController;
82
+ canvasSelectionController: CanvasSelectionController;
83
+ conversationController: ConversationController;
84
+ getInputEl: () => HTMLTextAreaElement;
85
+ getWelcomeEl: () => HTMLElement | null;
86
+ getMessagesEl: () => HTMLElement;
87
+ getFileContextManager: () => FileContextManager | null;
88
+ getImageContextManager: () => ImageContextManager | null;
89
+ getMcpServerSelector: () => McpServerSelector | null;
90
+ getExternalContextSelector: () => {
91
+ getExternalContexts: () => string[];
92
+ addExternalContext: (path: string) => AddExternalContextResult;
93
+ } | null;
94
+ getInstructionModeManager: () => InstructionModeManager | null;
95
+ getInstructionRefineService: () => InstructionRefineService | null;
96
+ getTitleGenerationService: () => TitleGenerationService | null;
97
+ getStatusPanel: () => StatusPanel | null;
98
+ getInputContainerEl: () => HTMLElement;
99
+ generateId: () => string;
100
+ resetInputHeight: () => void;
101
+ getAuxiliaryModel?: () => string | null;
102
+ getAgentService?: () => ChatRuntime | null;
103
+ getSubagentManager: () => SubagentManager;
104
+ /** Tab-level provider fallback for blank tabs (derived from draft model). */
105
+ getTabProviderId?: () => ProviderId;
106
+ /** Returns true if ready. */
107
+ ensureServiceInitialized?: () => Promise<boolean>;
108
+ openConversation?: (conversationId: string) => Promise<void>;
109
+ onForkAll?: () => Promise<void>;
110
+ restorePrePlanPermissionModeIfNeeded?: () => void;
111
+ }
112
+
113
+ export class InputController {
114
+ private deps: InputControllerDeps;
115
+ private pendingApprovalInline: InlineAskUserQuestion | null = null;
116
+ private pendingAskInline: InlineAskUserQuestion | null = null;
117
+ private pendingExitPlanModeInline: InlineExitPlanMode | null = null;
118
+ private pendingPlanApproval: InlinePlanApproval | null = null;
119
+ private pendingPlanApprovalInvalidated = false;
120
+ private activeResumeDropdown: ResumeSessionDropdown | null = null;
121
+ private inputContainerHideDepth = 0;
122
+ private steerInFlight = false;
123
+ private pendingSteerMessage: QueuedMessage | null = null;
124
+ private activeStreamingAssistantMessage: ChatMessage | null = null;
125
+ private pendingProviderUserMessages: Array<{
126
+ displayContent: string;
127
+ persistedContent?: string;
128
+ currentNote?: string;
129
+ images?: ChatMessage['images'];
130
+ }> = [];
131
+ private sawInitialProviderUserMessage = false;
132
+ private awaitingProviderAssistantStart = false;
133
+
134
+ constructor(deps: InputControllerDeps) {
135
+ this.deps = deps;
136
+ }
137
+
138
+ private getAgentService(): ChatRuntime | null {
139
+ return this.deps.getAgentService?.() ?? null;
140
+ }
141
+
142
+ private getAuxiliaryModel(): string | null {
143
+ return this.deps.getAuxiliaryModel?.()
144
+ ?? this.getAgentService()?.getAuxiliaryModel?.()
145
+ ?? null;
146
+ }
147
+
148
+ private syncInstructionRefineModelOverride(
149
+ instructionRefineService: InstructionRefineService,
150
+ ): void {
151
+ instructionRefineService.setModelOverride?.(this.getAuxiliaryModel() ?? undefined);
152
+ }
153
+
154
+ private getActiveProviderId(): ProviderId {
155
+ const agentService = this.getAgentService();
156
+ const conversationId = this.deps.state.currentConversationId;
157
+ if (!conversationId) {
158
+ return this.deps.getTabProviderId?.() ?? agentService?.providerId ?? DEFAULT_CHAT_PROVIDER_ID;
159
+ }
160
+
161
+ if (agentService?.providerId) {
162
+ return agentService.providerId;
163
+ }
164
+
165
+ return this.deps.plugin.getConversationSync(conversationId)?.providerId ?? DEFAULT_CHAT_PROVIDER_ID;
166
+ }
167
+
168
+ private getActiveCapabilities(): ProviderCapabilities {
169
+ const providerId = this.getActiveProviderId();
170
+ const agentService = this.getAgentService();
171
+ if (agentService?.providerId === providerId) {
172
+ return agentService.getCapabilities();
173
+ }
174
+
175
+ return ProviderRegistry.getCapabilities(providerId);
176
+ }
177
+
178
+ private isResumeSessionAtStillNeeded(resumeUuid: string, previousMessages: ChatMessage[]): boolean {
179
+ for (let i = previousMessages.length - 1; i >= 0; i--) {
180
+ if (previousMessages[i].role === 'assistant' && previousMessages[i].assistantMessageId === resumeUuid) {
181
+ // Still needed only if no messages follow the resume point
182
+ return i === previousMessages.length - 1;
183
+ }
184
+ }
185
+ return false;
186
+ }
187
+
188
+ // ============================================
189
+ // Message Sending
190
+ // ============================================
191
+
192
+ async sendMessage(options?: {
193
+ editorContextOverride?: EditorSelectionContext | null;
194
+ browserContextOverride?: BrowserSelectionContext | null;
195
+ canvasContextOverride?: CanvasSelectionContext | null;
196
+ content?: string;
197
+ images?: ChatMessage['images'];
198
+ turnRequestOverride?: ChatTurnRequest;
199
+ }): Promise<void> {
200
+ const {
201
+ plugin,
202
+ state,
203
+ renderer,
204
+ streamController,
205
+ selectionController,
206
+ browserSelectionController,
207
+ canvasSelectionController,
208
+ conversationController
209
+ } = this.deps;
210
+
211
+ // During conversation creation/switching, don't send - input is preserved so user can retry
212
+ if (state.isCreatingConversation || state.isSwitchingConversation) return;
213
+
214
+ const inputEl = this.deps.getInputEl();
215
+ const imageContextManager = this.deps.getImageContextManager();
216
+ const fileContextManager = this.deps.getFileContextManager();
217
+
218
+ const contentOverride = options?.content;
219
+ const shouldUseInput = contentOverride === undefined;
220
+ const content = (contentOverride ?? inputEl.value).trim();
221
+ const imageOverride = options?.images;
222
+ const hasImages = imageOverride !== undefined
223
+ ? imageOverride.length > 0
224
+ : (imageContextManager?.hasImages() ?? false);
225
+ if (!content && !hasImages) return;
226
+
227
+ // Check for built-in commands first (e.g., /clear, /new, /add-dir)
228
+ const builtInCmd = detectBuiltInCommand(content);
229
+ if (builtInCmd) {
230
+ if (shouldUseInput) {
231
+ inputEl.value = '';
232
+ this.deps.resetInputHeight();
233
+ }
234
+ await this.executeBuiltInCommand(builtInCmd.command, builtInCmd.args);
235
+ return;
236
+ }
237
+
238
+ // If agent is working, queue the message instead of dropping it
239
+ if (state.isStreaming) {
240
+ const images = hasImages
241
+ ? [...(imageOverride ?? imageContextManager?.getAttachedImages() ?? [])]
242
+ : undefined;
243
+ const editorContext = selectionController.getContext();
244
+ const browserContext = browserSelectionController?.getContext() ?? null;
245
+ const canvasContext = canvasSelectionController.getContext();
246
+ const { displayContent, turnRequest } = this.buildTurnSubmission({
247
+ content,
248
+ images,
249
+ editorContextOverride: editorContext,
250
+ browserContextOverride: browserContext,
251
+ canvasContextOverride: canvasContext,
252
+ });
253
+ state.queuedMessage = this.mergeQueuedMessages(
254
+ state.queuedMessage,
255
+ this.createQueuedMessage(displayContent, turnRequest),
256
+ );
257
+
258
+ if (shouldUseInput) {
259
+ inputEl.value = '';
260
+ this.deps.resetInputHeight();
261
+ }
262
+ if (shouldUseInput) {
263
+ imageContextManager?.clearImages();
264
+ }
265
+ this.updateQueueIndicator();
266
+ return;
267
+ }
268
+
269
+ if (shouldUseInput) {
270
+ inputEl.value = '';
271
+ this.deps.resetInputHeight();
272
+ }
273
+ state.isStreaming = true;
274
+ state.cancelRequested = false;
275
+ state.ignoreUsageUpdates = false; // Allow usage updates for new query
276
+ this.deps.getSubagentManager().resetSpawnedCount();
277
+ state.autoScrollEnabled = plugin.settings.enableAutoScroll ?? true; // Reset auto-scroll based on setting
278
+ const streamGeneration = state.bumpStreamGeneration();
279
+
280
+ // Hide welcome message when sending first message
281
+ const welcomeEl = this.deps.getWelcomeEl();
282
+ if (welcomeEl) {
283
+ welcomeEl.addClass('claudian-hidden');
284
+ }
285
+
286
+ fileContextManager?.startSession();
287
+
288
+ // Slash commands are passed directly to SDK for handling
289
+ // SDK handles expansion, $ARGUMENTS, @file references, and frontmatter options
290
+ const images = imageOverride ?? imageContextManager?.getAttachedImages() ?? [];
291
+ const imagesForMessage = images.length > 0 ? [...images] : undefined;
292
+ const isCompact = /^\/compact(\s|$)/i.test(content);
293
+
294
+ // Only clear images if we consumed user input (not for programmatic content override)
295
+ if (shouldUseInput) {
296
+ imageContextManager?.clearImages();
297
+ }
298
+
299
+ const turnSubmission = options?.turnRequestOverride
300
+ ? {
301
+ displayContent: content,
302
+ turnRequest: cloneChatTurnRequest(options.turnRequestOverride),
303
+ }
304
+ : this.buildTurnSubmission({
305
+ content,
306
+ images: imagesForMessage,
307
+ editorContextOverride: options?.editorContextOverride,
308
+ browserContextOverride: options?.browserContextOverride,
309
+ canvasContextOverride: options?.canvasContextOverride,
310
+ });
311
+ const { displayContent, turnRequest } = turnSubmission;
312
+
313
+ fileContextManager?.markCurrentNoteSent();
314
+
315
+ const userMsg: ChatMessage = {
316
+ id: this.deps.generateId(),
317
+ role: 'user',
318
+ content: displayContent,
319
+ displayContent, // Original user input (for UI display)
320
+ timestamp: Date.now(),
321
+ images: imagesForMessage,
322
+ };
323
+ state.addMessage(userMsg);
324
+ state.hasPendingConversationSave = true;
325
+ renderer.addMessage(userMsg);
326
+
327
+ await this.triggerTitleGeneration();
328
+
329
+ const assistantMsg: ChatMessage = {
330
+ id: this.deps.generateId(),
331
+ role: 'assistant',
332
+ content: '',
333
+ timestamp: Date.now(),
334
+ toolCalls: [],
335
+ contentBlocks: [],
336
+ };
337
+ state.addMessage(assistantMsg);
338
+ this.activeStreamingAssistantMessage = assistantMsg;
339
+ this.activateStreamingAssistantMessage(assistantMsg);
340
+ this.pendingProviderUserMessages = [{
341
+ displayContent,
342
+ images: imagesForMessage,
343
+ }];
344
+ this.sawInitialProviderUserMessage = false;
345
+ this.awaitingProviderAssistantStart = true;
346
+
347
+ streamController.showThinkingIndicator(
348
+ isCompact ? 'Compacting...' : undefined,
349
+ isCompact ? 'claudian-thinking--compact' : undefined,
350
+ );
351
+ state.responseStartTime = performance.now();
352
+
353
+ let wasInterrupted = false;
354
+ let wasInvalidated = false;
355
+ let didEnqueueToSdk = false;
356
+ let planCompleted = false;
357
+
358
+ // Lazy initialization: ensure service is ready before first query
359
+ if (this.deps.ensureServiceInitialized) {
360
+ const ready = await this.deps.ensureServiceInitialized();
361
+ if (!ready) {
362
+ new Notice('Failed to initialize agent service. Please try again.');
363
+ streamController.hideThinkingIndicator();
364
+ state.isStreaming = false;
365
+ this.activeStreamingAssistantMessage = null;
366
+ this.resetProviderMessageBoundaryState();
367
+ return;
368
+ }
369
+ }
370
+
371
+ const agentService = this.getAgentService();
372
+ if (!agentService) {
373
+ new Notice('Agent service not available. Please reload the plugin.');
374
+ this.activeStreamingAssistantMessage = null;
375
+ this.resetProviderMessageBoundaryState();
376
+ return;
377
+ }
378
+
379
+ // Restore pendingResumeAt from persisted conversation state (survives plugin reload)
380
+ const conversationIdForSend = state.currentConversationId;
381
+ if (conversationIdForSend) {
382
+ const conv = plugin.getConversationSync(conversationIdForSend);
383
+ if (conv?.resumeAtMessageId) {
384
+ if (this.isResumeSessionAtStillNeeded(conv.resumeAtMessageId, state.messages.slice(0, -2))) {
385
+ agentService.setResumeCheckpoint(conv.resumeAtMessageId);
386
+ } else {
387
+ try {
388
+ await plugin.updateConversation(conversationIdForSend, { resumeAtMessageId: undefined });
389
+ } catch {
390
+ // Best-effort — don't block send
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ try {
397
+ const preparedTurn = agentService.prepareTurn(turnRequest);
398
+ userMsg.content = preparedTurn.persistedContent;
399
+ userMsg.currentNote = preparedTurn.isCompact
400
+ ? undefined
401
+ : preparedTurn.request.currentNotePath;
402
+
403
+ // Pass history WITHOUT current turn (userMsg + assistantMsg we just added)
404
+ // This prevents duplication when rebuilding context for new sessions
405
+ const previousMessages = state.messages.slice(0, -2);
406
+ for await (const chunk of agentService.query(preparedTurn, previousMessages)) {
407
+ if (state.streamGeneration !== streamGeneration) {
408
+ wasInvalidated = true;
409
+ break;
410
+ }
411
+ if (state.cancelRequested) {
412
+ wasInterrupted = true;
413
+ break;
414
+ }
415
+
416
+ if (await this.handleProviderMessageBoundaryChunk(chunk)) {
417
+ continue;
418
+ }
419
+
420
+ await streamController.handleStreamChunk(
421
+ chunk,
422
+ this.activeStreamingAssistantMessage ?? assistantMsg,
423
+ );
424
+ }
425
+ } catch (error) {
426
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
427
+ await streamController.appendText(`\n\n**Error:** ${errorMsg}`);
428
+ } finally {
429
+ const finalAssistantMsg = this.activeStreamingAssistantMessage ?? assistantMsg;
430
+ const turnMetadata = agentService.consumeTurnMetadata();
431
+ userMsg.userMessageId = turnMetadata.userMessageId ?? userMsg.userMessageId;
432
+ finalAssistantMsg.assistantMessageId = turnMetadata.assistantMessageId ?? finalAssistantMsg.assistantMessageId;
433
+ didEnqueueToSdk = didEnqueueToSdk || turnMetadata.wasSent === true;
434
+ planCompleted = planCompleted || turnMetadata.planCompleted === true;
435
+
436
+ // ALWAYS clear the timer interval, even on stream invalidation (prevents memory leaks)
437
+ state.clearFlavorTimerInterval();
438
+
439
+ // Skip remaining cleanup if stream was invalidated (tab closed or conversation switched)
440
+ if (!wasInvalidated && state.streamGeneration === streamGeneration) {
441
+ const didCancelThisTurn = wasInterrupted || state.cancelRequested;
442
+ if (didCancelThisTurn && !state.pendingNewSessionPlan) {
443
+ await streamController.appendText('\n\n<span class="claudian-interrupted">Interrupted</span> <span class="claudian-interrupted-hint">· What should Claudian do instead?</span>');
444
+ }
445
+ streamController.hideThinkingIndicator();
446
+ state.isStreaming = false;
447
+ state.cancelRequested = false;
448
+ this.restorePendingSteerMessageToQueue();
449
+
450
+ // Capture response duration before resetting state (skip for interrupted responses and compaction)
451
+ const hasCompactBoundary = finalAssistantMsg.contentBlocks?.some(b => b.type === 'context_compacted');
452
+ if (!didCancelThisTurn && !hasCompactBoundary) {
453
+ const durationSeconds = state.responseStartTime
454
+ ? Math.floor((performance.now() - state.responseStartTime) / 1000)
455
+ : 0;
456
+ if (durationSeconds > 0) {
457
+ const flavorWord =
458
+ COMPLETION_FLAVOR_WORDS[Math.floor(Math.random() * COMPLETION_FLAVOR_WORDS.length)];
459
+ finalAssistantMsg.durationSeconds = durationSeconds;
460
+ finalAssistantMsg.durationFlavorWord = flavorWord;
461
+ // Add footer to live message in DOM
462
+ if (state.currentContentEl) {
463
+ const footerEl = state.currentContentEl.createDiv({ cls: 'claudian-response-footer' });
464
+ footerEl.createSpan({
465
+ text: `* ${flavorWord} for ${formatDurationMmSs(durationSeconds)}`,
466
+ cls: 'claudian-baked-duration',
467
+ });
468
+ }
469
+ }
470
+ }
471
+
472
+ state.currentContentEl = null;
473
+
474
+ await streamController.finalizeCurrentThinkingBlock(finalAssistantMsg);
475
+ await streamController.finalizeCurrentTextBlock(finalAssistantMsg);
476
+ this.deps.getSubagentManager().resetStreamingState();
477
+
478
+ // Auto-hide completed todo panel on response end
479
+ // Panel reappears only when new TodoWrite tool is called
480
+ if (state.currentTodos && state.currentTodos.every(t => t.status === 'completed')) {
481
+ state.currentTodos = null;
482
+ }
483
+ this.syncScrollToBottomAfterRenderUpdates();
484
+
485
+ // approve-new-session: the tool_result chunk is dropped because cancelRequested
486
+ // was set before the stream loop could process it — manually set the result so
487
+ // the saved conversation renders correctly when revisited
488
+ if (state.pendingNewSessionPlan && finalAssistantMsg.toolCalls) {
489
+ for (const tc of finalAssistantMsg.toolCalls) {
490
+ if (tc.name === TOOL_EXIT_PLAN_MODE && !tc.result) {
491
+ tc.status = 'completed';
492
+ tc.result = 'User approved the plan and started a new session.';
493
+ updateToolCallResult(tc.id, tc, state.toolCallElements);
494
+ }
495
+ }
496
+ }
497
+
498
+ // Provider-agnostic post-plan approval: show UI and await decision before save/auto-send
499
+ let planAutoSendContent: string | null = null;
500
+ let planApprovalInvalidated = false;
501
+ let shouldProcessQueuedMessage = true;
502
+ if (planCompleted && !didCancelThisTurn) {
503
+ const { decision, invalidated } = await this.showPlanApproval();
504
+
505
+ // Re-check invalidation after async approval prompt
506
+ if (state.streamGeneration !== streamGeneration || invalidated) {
507
+ planApprovalInvalidated = true;
508
+ } else if (decision?.type === 'implement') {
509
+ this.deps.restorePrePlanPermissionModeIfNeeded?.();
510
+ planAutoSendContent = 'Implement the plan.';
511
+ } else if (decision?.type === 'revise') {
512
+ // Keep plan mode active, populate input with feedback text
513
+ this.deps.getInputEl().value = decision.text;
514
+ shouldProcessQueuedMessage = false;
515
+ } else {
516
+ // cancel or null (dismissed)
517
+ this.deps.restorePrePlanPermissionModeIfNeeded?.();
518
+ }
519
+ }
520
+
521
+ if (!planApprovalInvalidated) {
522
+ // Only clear resumeAtMessageId if enqueue succeeded; preserve checkpoint on failure for retry
523
+ const saveExtras = didEnqueueToSdk ? { resumeAtMessageId: undefined } : undefined;
524
+ await conversationController.save(true, saveExtras);
525
+
526
+ const userMsgIndex = state.messages.indexOf(userMsg);
527
+ renderer.refreshActionButtons(userMsg, state.messages, userMsgIndex >= 0 ? userMsgIndex : undefined);
528
+
529
+ // Auto-implement takes precedence over both approve-new-session and queued input
530
+ if (planAutoSendContent) {
531
+ this.deps.getInputEl().value = planAutoSendContent;
532
+ this.sendMessage().catch(() => {});
533
+ } else {
534
+ // approve-new-session: create fresh conversation and send plan content
535
+ // Must be inside the invalidation guard — if the tab was closed or
536
+ // conversation switched, we must not create a new session on stale state.
537
+ const planContent = state.pendingNewSessionPlan;
538
+ if (planContent) {
539
+ state.pendingNewSessionPlan = null;
540
+ await conversationController.createNew();
541
+ this.deps.getInputEl().value = planContent;
542
+ this.sendMessage().catch(() => {
543
+ // sendMessage() handles its own errors internally; this prevents
544
+ // unhandled rejection if an unexpected error slips through.
545
+ });
546
+ } else if (shouldProcessQueuedMessage) {
547
+ this.processQueuedMessage();
548
+ }
549
+ }
550
+ }
551
+ }
552
+
553
+ if (wasInvalidated) {
554
+ this.clearPendingSteerState();
555
+ this.updateQueueIndicator();
556
+ }
557
+
558
+ this.activeStreamingAssistantMessage = null;
559
+ this.resetProviderMessageBoundaryState();
560
+ }
561
+ }
562
+
563
+ // ============================================
564
+ // Queue Management
565
+ // ============================================
566
+
567
+ updateQueueIndicator(): void {
568
+ const { state } = this.deps;
569
+ const indicatorEl = state.queueIndicatorEl;
570
+ if (!indicatorEl) return;
571
+
572
+ indicatorEl.empty();
573
+
574
+ const visibleQueuedMessage = state.queuedMessage ?? this.pendingSteerMessage;
575
+ if (visibleQueuedMessage) {
576
+ const isPendingSteerOnly = !state.queuedMessage && !!this.pendingSteerMessage;
577
+ indicatorEl.createSpan({
578
+ cls: 'claudian-queue-indicator-text',
579
+ text: `${isPendingSteerOnly ? '⌙ Steering: ' : '⌙ Queued: '}${this.getQueuedMessageDisplay(visibleQueuedMessage)}`,
580
+ });
581
+
582
+ if (state.queuedMessage) {
583
+ const actionsEl = indicatorEl.createDiv({ cls: 'claudian-queue-indicator-actions' });
584
+
585
+ if (this.canSteerQueuedMessage()) {
586
+ const steerButton = actionsEl.createEl('button', {
587
+ cls: 'claudian-queue-indicator-action',
588
+ text: this.steerInFlight ? 'Steering...' : 'Steer Now',
589
+ });
590
+ steerButton.setAttribute('type', 'button');
591
+ if (this.steerInFlight) {
592
+ steerButton.setAttribute('disabled', 'true');
593
+ } else {
594
+ steerButton.addEventListener('click', (event) => {
595
+ event.stopPropagation();
596
+ void this.steerQueuedMessage();
597
+ });
598
+ }
599
+ }
600
+
601
+ const editButton = this.createQueueIconButton(
602
+ actionsEl,
603
+ 'pencil',
604
+ 'Edit queued message',
605
+ );
606
+ editButton.addEventListener('click', (event) => {
607
+ event.stopPropagation();
608
+ this.withdrawQueuedMessageToComposer();
609
+ });
610
+
611
+ const discardButton = this.createQueueIconButton(
612
+ actionsEl,
613
+ 'trash-2',
614
+ 'Discard queued message',
615
+ );
616
+ discardButton.addEventListener('click', (event) => {
617
+ event.stopPropagation();
618
+ this.clearQueuedMessage();
619
+ });
620
+ }
621
+
622
+ indicatorEl.addClass('claudian-visible-flex');
623
+ indicatorEl.removeClass('claudian-hidden');
624
+ return;
625
+ }
626
+
627
+ indicatorEl.removeClass('claudian-visible-flex');
628
+ indicatorEl.addClass('claudian-hidden');
629
+ }
630
+
631
+ clearQueuedMessage(): void {
632
+ const { state } = this.deps;
633
+ state.queuedMessage = null;
634
+ this.updateQueueIndicator();
635
+ }
636
+
637
+ withdrawQueuedMessageToComposer(): void {
638
+ const { state } = this.deps;
639
+ if (!state.queuedMessage) return;
640
+
641
+ const queuedMessage = this.cloneQueuedMessage(state.queuedMessage);
642
+ state.queuedMessage = null;
643
+ this.restoreMessageToInput(queuedMessage, { mergeWithComposer: true });
644
+ this.updateQueueIndicator();
645
+ }
646
+
647
+ private restoreMessageToInput(
648
+ message: QueuedMessage | null,
649
+ options: { mergeWithComposer?: boolean } = {},
650
+ ): void {
651
+ if (!message) return;
652
+
653
+ const { content, images } = message;
654
+ const inputEl = this.deps.getInputEl();
655
+ const currentContent = options.mergeWithComposer ? inputEl.value.trim() : '';
656
+ inputEl.value = currentContent
657
+ ? appendMarkdownSnippet(content, currentContent)
658
+ : content;
659
+
660
+ const imageContextManager = this.deps.getImageContextManager();
661
+ const currentImages = options.mergeWithComposer
662
+ ? (imageContextManager?.getAttachedImages() ?? [])
663
+ : [];
664
+ const restoredImages = [...(images ?? []), ...currentImages];
665
+ if (restoredImages.length > 0) {
666
+ imageContextManager?.setImages(restoredImages);
667
+ }
668
+ this.deps.resetInputHeight();
669
+ inputEl.focus();
670
+ }
671
+
672
+ private restorePendingMessagesToInput(): void {
673
+ const { state } = this.deps;
674
+ const combinedMessage = this.mergePendingMessages(
675
+ this.pendingSteerMessage,
676
+ state.queuedMessage,
677
+ );
678
+ this.restoreMessageToInput(combinedMessage, { mergeWithComposer: true });
679
+ state.queuedMessage = null;
680
+ this.clearPendingSteerState();
681
+ this.updateQueueIndicator();
682
+ }
683
+
684
+ private processQueuedMessage(): void {
685
+ const { state } = this.deps;
686
+ if (!state.queuedMessage) return;
687
+
688
+ const queuedMessage = this.cloneQueuedMessage(state.queuedMessage);
689
+ state.queuedMessage = null;
690
+ this.updateQueueIndicator();
691
+
692
+ window.setTimeout(
693
+ () => {
694
+ void this.sendMessage({
695
+ content: queuedMessage.content,
696
+ images: queuedMessage.images,
697
+ turnRequestOverride: this.toQueuedChatTurn(queuedMessage).request,
698
+ });
699
+ },
700
+ 0
701
+ );
702
+ }
703
+
704
+ private buildTurnSubmission(options: {
705
+ content: string;
706
+ images?: ChatMessage['images'];
707
+ editorContextOverride?: EditorSelectionContext | null;
708
+ browserContextOverride?: BrowserSelectionContext | null;
709
+ canvasContextOverride?: CanvasSelectionContext | null;
710
+ }): {
711
+ displayContent: string;
712
+ turnRequest: ChatTurnRequest;
713
+ } {
714
+ const {
715
+ selectionController,
716
+ browserSelectionController,
717
+ canvasSelectionController,
718
+ } = this.deps;
719
+
720
+ const fileContextManager = this.deps.getFileContextManager();
721
+ const mcpServerSelector = this.deps.getMcpServerSelector();
722
+ const externalContextSelector = this.deps.getExternalContextSelector();
723
+
724
+ const currentNotePath = fileContextManager?.getCurrentNotePath() || null;
725
+ const shouldSendCurrentNote = fileContextManager?.shouldSendCurrentNote(currentNotePath) ?? false;
726
+
727
+ const editorContext = options.editorContextOverride !== undefined
728
+ ? options.editorContextOverride
729
+ : selectionController.getContext();
730
+ const browserContext = options.browserContextOverride !== undefined
731
+ ? options.browserContextOverride
732
+ : (browserSelectionController?.getContext() ?? null);
733
+ const canvasContext = options.canvasContextOverride !== undefined
734
+ ? options.canvasContextOverride
735
+ : canvasSelectionController.getContext();
736
+
737
+ const externalContextPaths = externalContextSelector?.getExternalContexts();
738
+ const isCompact = /^\/compact(\s|$)/i.test(options.content);
739
+ const transformedText = !isCompact && fileContextManager
740
+ ? fileContextManager.transformContextMentions(options.content)
741
+ : options.content;
742
+ const enabledMcpServers = mcpServerSelector?.getEnabledServers();
743
+
744
+ return {
745
+ displayContent: options.content,
746
+ turnRequest: {
747
+ text: transformedText,
748
+ images: options.images,
749
+ currentNotePath: shouldSendCurrentNote && currentNotePath ? currentNotePath : undefined,
750
+ editorSelection: editorContext,
751
+ browserSelection: browserContext,
752
+ canvasSelection: canvasContext,
753
+ externalContextPaths: externalContextPaths && externalContextPaths.length > 0
754
+ ? externalContextPaths
755
+ : undefined,
756
+ enabledMcpServers: enabledMcpServers && enabledMcpServers.size > 0
757
+ ? enabledMcpServers
758
+ : undefined,
759
+ },
760
+ };
761
+ }
762
+
763
+ private getQueuedMessageDisplay(message: QueuedMessage | null): string {
764
+ if (!message) {
765
+ return '';
766
+ }
767
+
768
+ const rawContent = message.content.trim();
769
+ const preview = rawContent.length > 40
770
+ ? rawContent.slice(0, 40) + '...'
771
+ : rawContent;
772
+ const hasImages = (message.images?.length ?? 0) > 0;
773
+
774
+ if (hasImages) {
775
+ return preview ? `${preview} [images]` : '[images]';
776
+ }
777
+
778
+ return preview;
779
+ }
780
+
781
+ private createQueueIconButton(
782
+ parentEl: HTMLElement,
783
+ icon: string,
784
+ label: string,
785
+ ): HTMLElement {
786
+ const button = parentEl.createEl('button', {
787
+ cls: 'claudian-queue-indicator-icon-action',
788
+ attr: {
789
+ 'aria-label': label,
790
+ title: label,
791
+ type: 'button',
792
+ },
793
+ });
794
+ setIcon(button, icon);
795
+ return button;
796
+ }
797
+
798
+ private canSteerQueuedMessage(): boolean {
799
+ const agentService = this.getAgentService();
800
+ return this.deps.state.isStreaming
801
+ && this.getActiveCapabilities().supportsTurnSteer === true
802
+ && typeof agentService?.steer === 'function';
803
+ }
804
+
805
+ private cloneQueuedMessage(message: QueuedMessage): QueuedMessage {
806
+ return {
807
+ ...message,
808
+ images: message.images ? [...message.images] : undefined,
809
+ turnRequest: message.turnRequest
810
+ ? cloneChatTurnRequest(message.turnRequest)
811
+ : undefined,
812
+ };
813
+ }
814
+
815
+ private createQueuedMessage(displayContent: string, turnRequest: ChatTurnRequest): QueuedMessage {
816
+ const request = cloneChatTurnRequest(turnRequest);
817
+ return {
818
+ content: displayContent,
819
+ images: request.images,
820
+ editorContext: request.editorSelection ?? null,
821
+ browserContext: request.browserSelection ?? null,
822
+ canvasContext: request.canvasSelection ?? null,
823
+ turnRequest: request,
824
+ };
825
+ }
826
+
827
+ private toQueuedChatTurn(message: QueuedMessage): QueuedChatTurn {
828
+ if (message.turnRequest) {
829
+ return {
830
+ displayContent: message.content,
831
+ request: cloneChatTurnRequest(message.turnRequest),
832
+ };
833
+ }
834
+
835
+ return {
836
+ displayContent: message.content,
837
+ request: {
838
+ text: message.content,
839
+ images: message.images ? [...message.images] : undefined,
840
+ editorSelection: message.editorContext,
841
+ browserSelection: message.browserContext ?? null,
842
+ canvasSelection: message.canvasContext,
843
+ },
844
+ };
845
+ }
846
+
847
+ private mergePendingMessages(
848
+ first: QueuedMessage | null,
849
+ second: QueuedMessage | null,
850
+ ): QueuedMessage | null {
851
+ if (first && second) {
852
+ return this.mergeQueuedMessages(first, second);
853
+ }
854
+
855
+ if (first) {
856
+ return this.cloneQueuedMessage(first);
857
+ }
858
+
859
+ if (second) {
860
+ return this.cloneQueuedMessage(second);
861
+ }
862
+
863
+ return null;
864
+ }
865
+
866
+ private clearPendingSteerState(): void {
867
+ this.pendingSteerMessage = null;
868
+ this.steerInFlight = false;
869
+ }
870
+
871
+ private restorePendingSteerMessageToQueue(): void {
872
+ if (!this.pendingSteerMessage) {
873
+ return;
874
+ }
875
+
876
+ const { state } = this.deps;
877
+ const pendingSteerMessage = this.cloneQueuedMessage(this.pendingSteerMessage);
878
+ this.clearPendingSteerState();
879
+ state.queuedMessage = state.queuedMessage
880
+ ? this.mergeQueuedMessages(pendingSteerMessage, state.queuedMessage)
881
+ : pendingSteerMessage;
882
+ this.updateQueueIndicator();
883
+ }
884
+
885
+ private mergeQueuedMessages(
886
+ existing: QueuedMessage | null,
887
+ incoming: QueuedMessage,
888
+ ): QueuedMessage {
889
+ if (!existing) {
890
+ return this.cloneQueuedMessage(incoming);
891
+ }
892
+
893
+ const mergedTurn = mergeQueuedChatTurns(
894
+ this.toQueuedChatTurn(existing),
895
+ this.toQueuedChatTurn(incoming),
896
+ );
897
+ return this.createQueuedMessage(mergedTurn.displayContent, mergedTurn.request);
898
+ }
899
+
900
+ private async steerQueuedMessage(): Promise<void> {
901
+ if (this.steerInFlight) {
902
+ return;
903
+ }
904
+
905
+ const { state } = this.deps;
906
+ const agentService = this.getAgentService();
907
+ if (!state.queuedMessage || !this.canSteerQueuedMessage() || !agentService?.steer) {
908
+ return;
909
+ }
910
+
911
+ const queuedMessage = this.cloneQueuedMessage(state.queuedMessage);
912
+ state.queuedMessage = null;
913
+ this.pendingSteerMessage = queuedMessage;
914
+ this.steerInFlight = true;
915
+ this.updateQueueIndicator();
916
+
917
+ try {
918
+ const { displayContent, request } = this.toQueuedChatTurn(queuedMessage);
919
+
920
+ const preparedTurn = agentService.prepareTurn(request);
921
+ const accepted = await agentService.steer(preparedTurn);
922
+ if (state.cancelRequested || !this.pendingSteerMessage) {
923
+ return;
924
+ }
925
+ if (!accepted) {
926
+ this.restoreQueuedMessageAfterSteerFailure(queuedMessage);
927
+ return;
928
+ }
929
+
930
+ this.deps.getFileContextManager()?.markCurrentNoteSent();
931
+
932
+ this.pendingProviderUserMessages.push({
933
+ displayContent,
934
+ persistedContent: preparedTurn.persistedContent,
935
+ currentNote: preparedTurn.isCompact
936
+ ? undefined
937
+ : preparedTurn.request.currentNotePath,
938
+ images: request.images,
939
+ });
940
+ } catch {
941
+ this.restoreQueuedMessageAfterSteerFailure(queuedMessage);
942
+ new Notice('Failed to steer the queued Codex message. It is still available.');
943
+ }
944
+ }
945
+
946
+ private restoreQueuedMessageAfterSteerFailure(
947
+ message: QueuedMessage,
948
+ ): void {
949
+ const { state } = this.deps;
950
+ this.clearPendingSteerState();
951
+ if (state.cancelRequested) {
952
+ this.updateQueueIndicator();
953
+ return;
954
+ }
955
+
956
+ if (state.isStreaming) {
957
+ state.queuedMessage = state.queuedMessage
958
+ ? this.mergeQueuedMessages(message, state.queuedMessage)
959
+ : message;
960
+ this.updateQueueIndicator();
961
+ return;
962
+ }
963
+
964
+ this.restoreMessageToInput(message, { mergeWithComposer: true });
965
+ this.updateQueueIndicator();
966
+ }
967
+
968
+ private activateStreamingAssistantMessage(message: ChatMessage): void {
969
+ const { state, renderer } = this.deps;
970
+ const msgEl = renderer.addMessage(message);
971
+ const contentEl = msgEl.querySelector<HTMLElement>('.claudian-message-content');
972
+
973
+ if (!contentEl) {
974
+ return;
975
+ }
976
+
977
+ if (!state.currentContentEl) {
978
+ state.toolCallElements.clear();
979
+ }
980
+
981
+ state.currentContentEl = contentEl;
982
+ state.currentTextEl = null;
983
+ state.currentTextContent = '';
984
+ state.currentThinkingState = null;
985
+ }
986
+
987
+ private resetProviderMessageBoundaryState(): void {
988
+ this.pendingProviderUserMessages = [];
989
+ this.sawInitialProviderUserMessage = false;
990
+ this.awaitingProviderAssistantStart = false;
991
+ }
992
+
993
+ private async handleProviderMessageBoundaryChunk(chunk: StreamChunk): Promise<boolean> {
994
+ switch (chunk.type) {
995
+ case 'user_message_start':
996
+ await this.handleProviderUserMessageStart(chunk);
997
+ return true;
998
+ case 'assistant_message_start':
999
+ await this.handleProviderAssistantMessageStart();
1000
+ return true;
1001
+ default:
1002
+ return false;
1003
+ }
1004
+ }
1005
+
1006
+ private async handleProviderUserMessageStart(
1007
+ chunk: Extract<StreamChunk, { type: 'user_message_start' }>,
1008
+ ): Promise<void> {
1009
+ const expected = this.pendingProviderUserMessages.shift();
1010
+ if (!this.sawInitialProviderUserMessage) {
1011
+ this.sawInitialProviderUserMessage = true;
1012
+ return;
1013
+ }
1014
+
1015
+ this.clearPendingSteerState();
1016
+ this.updateQueueIndicator();
1017
+
1018
+ const previousAssistant = this.activeStreamingAssistantMessage;
1019
+ const shouldDiscardPlaceholder = this.shouldDiscardPendingAssistantPlaceholder(previousAssistant);
1020
+ if (previousAssistant) {
1021
+ if (shouldDiscardPlaceholder) {
1022
+ this.discardStreamingAssistantMessage(previousAssistant.id);
1023
+ } else {
1024
+ await this.deps.streamController.finalizeCurrentThinkingBlock(previousAssistant);
1025
+ await this.deps.streamController.finalizeCurrentTextBlock(previousAssistant);
1026
+ }
1027
+ }
1028
+ this.deps.streamController.hideThinkingIndicator();
1029
+
1030
+ const displayContent = expected?.displayContent ?? chunk.content;
1031
+ const persistedContent = expected?.persistedContent ?? displayContent;
1032
+ const images = expected?.images;
1033
+ if (displayContent || (images?.length ?? 0) > 0) {
1034
+ const userMessage: ChatMessage = {
1035
+ id: this.deps.generateId(),
1036
+ role: 'user',
1037
+ content: persistedContent,
1038
+ displayContent,
1039
+ timestamp: Date.now(),
1040
+ currentNote: expected?.currentNote,
1041
+ images,
1042
+ };
1043
+ this.deps.state.addMessage(userMessage);
1044
+ this.deps.renderer.addMessage(userMessage);
1045
+ }
1046
+
1047
+ const assistantMessage: ChatMessage = {
1048
+ id: this.deps.generateId(),
1049
+ role: 'assistant',
1050
+ content: '',
1051
+ timestamp: Date.now(),
1052
+ toolCalls: [],
1053
+ contentBlocks: [],
1054
+ };
1055
+ this.deps.state.addMessage(assistantMessage);
1056
+ this.activeStreamingAssistantMessage = assistantMessage;
1057
+ this.activateStreamingAssistantMessage(assistantMessage);
1058
+ this.deps.streamController.showThinkingIndicator();
1059
+ this.deps.state.responseStartTime = performance.now();
1060
+ this.awaitingProviderAssistantStart = true;
1061
+ }
1062
+
1063
+ private async handleProviderAssistantMessageStart(): Promise<void> {
1064
+ if (this.awaitingProviderAssistantStart) {
1065
+ this.awaitingProviderAssistantStart = false;
1066
+ return;
1067
+ }
1068
+
1069
+ const previousAssistant = this.activeStreamingAssistantMessage;
1070
+ if (previousAssistant) {
1071
+ await this.deps.streamController.finalizeCurrentThinkingBlock(previousAssistant);
1072
+ await this.deps.streamController.finalizeCurrentTextBlock(previousAssistant);
1073
+ }
1074
+
1075
+ const assistantMessage: ChatMessage = {
1076
+ id: this.deps.generateId(),
1077
+ role: 'assistant',
1078
+ content: '',
1079
+ timestamp: Date.now(),
1080
+ toolCalls: [],
1081
+ contentBlocks: [],
1082
+ };
1083
+ this.deps.state.addMessage(assistantMessage);
1084
+ this.activeStreamingAssistantMessage = assistantMessage;
1085
+ this.activateStreamingAssistantMessage(assistantMessage);
1086
+ this.deps.streamController.showThinkingIndicator();
1087
+ }
1088
+
1089
+ private shouldDiscardPendingAssistantPlaceholder(message: ChatMessage | null): boolean {
1090
+ return this.awaitingProviderAssistantStart
1091
+ && !!message
1092
+ && !message.content.trim()
1093
+ && (message.toolCalls?.length ?? 0) === 0
1094
+ && (message.contentBlocks?.length ?? 0) === 0;
1095
+ }
1096
+
1097
+ private discardStreamingAssistantMessage(messageId: string): void {
1098
+ const { state, renderer } = this.deps;
1099
+ state.messages = state.messages.filter((message) => message.id !== messageId);
1100
+ renderer.removeMessage(messageId);
1101
+ state.currentContentEl = null;
1102
+ state.currentTextEl = null;
1103
+ state.currentTextContent = '';
1104
+ state.currentThinkingState = null;
1105
+ }
1106
+
1107
+ // ============================================
1108
+ // Title Generation
1109
+ // ============================================
1110
+
1111
+ /**
1112
+ * Triggers AI title generation after first user message.
1113
+ * Handles setting fallback title, firing async generation, and updating UI.
1114
+ */
1115
+ private async triggerTitleGeneration(): Promise<void> {
1116
+ const { plugin, state, conversationController } = this.deps;
1117
+
1118
+ if (state.messages.length !== 1) {
1119
+ return;
1120
+ }
1121
+
1122
+ if (!state.currentConversationId) {
1123
+ const sessionId = this.getAgentService()?.getSessionId() ?? undefined;
1124
+ const conversation = await plugin.createConversation({
1125
+ providerId: this.getActiveProviderId(),
1126
+ sessionId,
1127
+ });
1128
+ state.currentConversationId = conversation.id;
1129
+ }
1130
+
1131
+ // Find first user message by role (not by index)
1132
+ const firstUserMsg = state.messages.find(m => m.role === 'user');
1133
+
1134
+ if (!firstUserMsg) {
1135
+ return;
1136
+ }
1137
+
1138
+ const userContent = firstUserMsg.displayContent
1139
+ ?? extractUserDisplayContent(firstUserMsg.content)
1140
+ ?? firstUserMsg.content;
1141
+
1142
+ // Set immediate fallback title
1143
+ const fallbackTitle = conversationController.generateFallbackTitle(userContent);
1144
+ await plugin.renameConversation(state.currentConversationId, fallbackTitle);
1145
+
1146
+ if (!plugin.settings.enableAutoTitleGeneration) {
1147
+ return;
1148
+ }
1149
+
1150
+ // Fire async AI title generation only if service available
1151
+ const titleService = this.deps.getTitleGenerationService();
1152
+ if (!titleService) {
1153
+ // No titleService, just keep the fallback title with no status
1154
+ return;
1155
+ }
1156
+
1157
+ // Mark as pending only when we're actually starting generation
1158
+ await plugin.updateConversation(state.currentConversationId, { titleGenerationStatus: 'pending' });
1159
+ conversationController.updateHistoryDropdown();
1160
+
1161
+ const convId = state.currentConversationId;
1162
+ const expectedTitle = fallbackTitle; // Store to check if user renamed during generation
1163
+
1164
+ titleService.generateTitle(
1165
+ convId,
1166
+ userContent,
1167
+ async (conversationId, result) => {
1168
+ // Check if conversation still exists and user hasn't manually renamed
1169
+ const currentConv = await plugin.getConversationById(conversationId);
1170
+ if (!currentConv) return;
1171
+
1172
+ // Only apply AI title if user hasn't manually renamed (title still matches fallback)
1173
+ const userManuallyRenamed = currentConv.title !== expectedTitle;
1174
+
1175
+ if (result.success && !userManuallyRenamed) {
1176
+ await plugin.renameConversation(conversationId, result.title);
1177
+ await plugin.updateConversation(conversationId, { titleGenerationStatus: 'success' });
1178
+ } else if (!userManuallyRenamed) {
1179
+ // Keep fallback title, mark as failed (only if user hasn't renamed)
1180
+ await plugin.updateConversation(conversationId, { titleGenerationStatus: 'failed' });
1181
+ } else {
1182
+ // User manually renamed, clear the status (user's choice takes precedence)
1183
+ await plugin.updateConversation(conversationId, { titleGenerationStatus: undefined });
1184
+ }
1185
+ conversationController.updateHistoryDropdown();
1186
+ }
1187
+ ).catch(() => {
1188
+ // Silently ignore title generation errors
1189
+ });
1190
+ }
1191
+
1192
+ // ============================================
1193
+ // Streaming Control
1194
+ // ============================================
1195
+
1196
+ cancelStreaming(): void {
1197
+ const { state, streamController } = this.deps;
1198
+ if (!state.isStreaming) return;
1199
+ state.cancelRequested = true;
1200
+ // Restore queued message to input instead of discarding
1201
+ this.restorePendingMessagesToInput();
1202
+ this.getAgentService()?.cancel();
1203
+ streamController.hideThinkingIndicator();
1204
+ }
1205
+
1206
+ private syncScrollToBottomAfterRenderUpdates(): void {
1207
+ const { plugin, state } = this.deps;
1208
+ if (!(plugin.settings.enableAutoScroll ?? true)) return;
1209
+ if (!state.autoScrollEnabled) return;
1210
+
1211
+ window.requestAnimationFrame(() => {
1212
+ if (!(this.deps.plugin.settings.enableAutoScroll ?? true)) return;
1213
+ if (!this.deps.state.autoScrollEnabled) return;
1214
+
1215
+ const messagesEl = this.deps.getMessagesEl();
1216
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1217
+ });
1218
+ }
1219
+
1220
+ // ============================================
1221
+ // Instruction Mode
1222
+ // ============================================
1223
+
1224
+ async handleInstructionSubmit(rawInstruction: string): Promise<void> {
1225
+ const { plugin } = this.deps;
1226
+
1227
+ const instructionRefineService = this.deps.getInstructionRefineService();
1228
+ const instructionModeManager = this.deps.getInstructionModeManager();
1229
+
1230
+ if (!instructionRefineService) return;
1231
+
1232
+ const existingPrompt = plugin.settings.systemPrompt;
1233
+ let modal: InstructionModal | null = null;
1234
+ let wasCancelled = false;
1235
+
1236
+ try {
1237
+ modal = new InstructionModal(
1238
+ plugin.app,
1239
+ rawInstruction,
1240
+ {
1241
+ onAccept: (finalInstruction) => {
1242
+ void (async (): Promise<void> => {
1243
+ const currentPrompt = plugin.settings.systemPrompt;
1244
+ plugin.settings.systemPrompt = appendMarkdownSnippet(currentPrompt, finalInstruction);
1245
+ await plugin.saveSettings();
1246
+
1247
+ new Notice('Instruction added to custom system prompt');
1248
+ instructionModeManager?.clear();
1249
+ })();
1250
+ },
1251
+ onReject: () => {
1252
+ wasCancelled = true;
1253
+ instructionRefineService.cancel();
1254
+ instructionModeManager?.clear();
1255
+ },
1256
+ onClarificationSubmit: async (response) => {
1257
+ this.syncInstructionRefineModelOverride(instructionRefineService);
1258
+ const result = await instructionRefineService.continueConversation(response);
1259
+
1260
+ if (wasCancelled) {
1261
+ return;
1262
+ }
1263
+
1264
+ if (!result.success) {
1265
+ if (result.error === 'Cancelled') {
1266
+ return;
1267
+ }
1268
+ new Notice(result.error || 'Failed to process response');
1269
+ modal?.showError(result.error || 'Failed to process response');
1270
+ return;
1271
+ }
1272
+
1273
+ if (result.clarification) {
1274
+ modal?.showClarification(result.clarification);
1275
+ } else if (result.refinedInstruction) {
1276
+ modal?.showConfirmation(result.refinedInstruction);
1277
+ }
1278
+ }
1279
+ }
1280
+ );
1281
+ modal.open();
1282
+
1283
+ this.syncInstructionRefineModelOverride(instructionRefineService);
1284
+ instructionRefineService.resetConversation();
1285
+ const result = await instructionRefineService.refineInstruction(
1286
+ rawInstruction,
1287
+ existingPrompt
1288
+ );
1289
+
1290
+ if (wasCancelled) {
1291
+ return;
1292
+ }
1293
+
1294
+ if (!result.success) {
1295
+ if (result.error === 'Cancelled') {
1296
+ instructionModeManager?.clear();
1297
+ return;
1298
+ }
1299
+ new Notice(result.error || 'Failed to refine instruction');
1300
+ modal.showError(result.error || 'Failed to refine instruction');
1301
+ instructionModeManager?.clear();
1302
+ return;
1303
+ }
1304
+
1305
+ if (result.clarification) {
1306
+ modal.showClarification(result.clarification);
1307
+ } else if (result.refinedInstruction) {
1308
+ modal.showConfirmation(result.refinedInstruction);
1309
+ } else {
1310
+ new Notice('No instruction received');
1311
+ modal.showError('No instruction received');
1312
+ instructionModeManager?.clear();
1313
+ }
1314
+ } catch (error) {
1315
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
1316
+ new Notice(`Error: ${errorMsg}`);
1317
+ modal?.showError(errorMsg);
1318
+ instructionModeManager?.clear();
1319
+ }
1320
+ }
1321
+
1322
+ // ============================================
1323
+ // Approval Dialogs
1324
+ // ============================================
1325
+
1326
+ async handleApprovalRequest(
1327
+ toolName: string,
1328
+ _input: Record<string, unknown>,
1329
+ description: string,
1330
+ approvalOptions?: ApprovalCallbackOptions,
1331
+ ): Promise<ApprovalDecision> {
1332
+ const inputContainerEl = this.deps.getInputContainerEl();
1333
+ const parentEl = inputContainerEl.parentElement;
1334
+ if (!parentEl) {
1335
+ throw new Error('Input container is detached from DOM');
1336
+ }
1337
+
1338
+ // Build header element, then detach — InlineAskUserQuestion will re-attach it
1339
+ const headerEl = parentEl.createDiv({ cls: 'claudian-ask-approval-info' });
1340
+ headerEl.remove();
1341
+
1342
+ const toolEl = headerEl.createDiv({ cls: 'claudian-ask-approval-tool' });
1343
+ const iconEl = toolEl.createSpan({ cls: 'claudian-ask-approval-icon' });
1344
+ iconEl.setAttribute('aria-hidden', 'true');
1345
+ setToolIcon(iconEl, toolName);
1346
+ toolEl.createSpan({ text: toolName, cls: 'claudian-ask-approval-tool-name' });
1347
+
1348
+ if (approvalOptions?.decisionReason) {
1349
+ headerEl.createDiv({ text: approvalOptions.decisionReason, cls: 'claudian-ask-approval-reason' });
1350
+ }
1351
+ if (approvalOptions?.blockedPath) {
1352
+ headerEl.createDiv({ text: approvalOptions.blockedPath, cls: 'claudian-ask-approval-blocked-path' });
1353
+ }
1354
+ if (approvalOptions?.agentID) {
1355
+ headerEl.createDiv({ text: `Agent: ${approvalOptions.agentID}`, cls: 'claudian-ask-approval-agent' });
1356
+ }
1357
+
1358
+ headerEl.createDiv({ text: description, cls: 'claudian-ask-approval-desc' });
1359
+
1360
+ const decisionOptions = approvalOptions?.decisionOptions ?? DEFAULT_APPROVAL_DECISION_OPTIONS;
1361
+ const optionDecisionMap = new Map<string, ApprovalDecision>();
1362
+ const questionOptions = decisionOptions.map((option, index) => {
1363
+ const value = option.value || `approval-option-${index}`;
1364
+ if (option.decision) {
1365
+ optionDecisionMap.set(value, option.decision);
1366
+ }
1367
+ return {
1368
+ label: option.label,
1369
+ description: option.description ?? '',
1370
+ value,
1371
+ };
1372
+ });
1373
+ const input = {
1374
+ questions: [{
1375
+ question: 'Allow this action?',
1376
+ options: questionOptions,
1377
+ isOther: false,
1378
+ isSecret: false,
1379
+ }],
1380
+ };
1381
+
1382
+ const result = await this.showInlineQuestion(
1383
+ parentEl,
1384
+ inputContainerEl,
1385
+ input,
1386
+ (inline) => { this.pendingApprovalInline = inline; },
1387
+ undefined,
1388
+ { title: 'Permission required', headerEl, showCustomInput: false, immediateSelect: true },
1389
+ );
1390
+
1391
+ if (!result) return 'cancel';
1392
+ const selected = Object.values(result)[0];
1393
+ const selectedValue = Array.isArray(selected) ? selected[0] : selected;
1394
+ if (typeof selectedValue !== 'string') {
1395
+ new Notice(`Unexpected approval selection: "${String(selectedValue)}"`);
1396
+ return 'cancel';
1397
+ }
1398
+
1399
+ const decision = optionDecisionMap.get(selectedValue);
1400
+ if (decision) {
1401
+ return decision;
1402
+ }
1403
+
1404
+ return {
1405
+ type: 'select-option',
1406
+ value: selectedValue,
1407
+ };
1408
+ }
1409
+
1410
+ async handleAskUserQuestion(
1411
+ input: Record<string, unknown>,
1412
+ signal?: AbortSignal,
1413
+ ): Promise<Record<string, string | string[]> | null> {
1414
+ const inputContainerEl = this.deps.getInputContainerEl();
1415
+ const parentEl = inputContainerEl.parentElement;
1416
+ if (!parentEl) {
1417
+ throw new Error('Input container is detached from DOM');
1418
+ }
1419
+
1420
+ return this.showInlineQuestion(
1421
+ parentEl,
1422
+ inputContainerEl,
1423
+ input,
1424
+ (inline) => { this.pendingAskInline = inline; },
1425
+ signal,
1426
+ );
1427
+ }
1428
+
1429
+ private showInlineQuestion(
1430
+ parentEl: HTMLElement,
1431
+ inputContainerEl: HTMLElement,
1432
+ input: Record<string, unknown>,
1433
+ setPending: (inline: InlineAskUserQuestion | null) => void,
1434
+ signal?: AbortSignal,
1435
+ config?: InlineAskQuestionConfig,
1436
+ ): Promise<Record<string, string | string[]> | null> {
1437
+ this.deps.streamController.hideThinkingIndicator();
1438
+ this.hideInputContainer(inputContainerEl);
1439
+
1440
+ return new Promise<Record<string, string | string[]> | null>((resolve, reject) => {
1441
+ const inline = new InlineAskUserQuestion(
1442
+ parentEl,
1443
+ input,
1444
+ (result: Record<string, string | string[]> | null) => {
1445
+ setPending(null);
1446
+ this.restoreInputContainer(inputContainerEl);
1447
+ resolve(result);
1448
+ },
1449
+ signal,
1450
+ config,
1451
+ );
1452
+ setPending(inline);
1453
+ try {
1454
+ inline.render();
1455
+ } catch (err) {
1456
+ setPending(null);
1457
+ this.restoreInputContainer(inputContainerEl);
1458
+ reject(toError(err));
1459
+ }
1460
+ });
1461
+ }
1462
+
1463
+ async handleExitPlanMode(
1464
+ input: Record<string, unknown>,
1465
+ signal?: AbortSignal,
1466
+ ): Promise<ExitPlanModeDecision | null> {
1467
+ const { state, streamController } = this.deps;
1468
+ const inputContainerEl = this.deps.getInputContainerEl();
1469
+ const parentEl = inputContainerEl.parentElement;
1470
+ if (!parentEl) {
1471
+ throw new Error('Input container is detached from DOM');
1472
+ }
1473
+
1474
+ streamController.hideThinkingIndicator();
1475
+ this.hideInputContainer(inputContainerEl);
1476
+
1477
+ const enrichedInput = state.planFilePath
1478
+ ? { ...input, planFilePath: state.planFilePath }
1479
+ : input;
1480
+
1481
+ const renderContent = (el: HTMLElement, markdown: string) =>
1482
+ this.deps.renderer.renderContent(el, markdown);
1483
+
1484
+ const planPathPrefix = this.getActiveCapabilities().planPathPrefix;
1485
+
1486
+ return new Promise<ExitPlanModeDecision | null>((resolve, reject) => {
1487
+ const inline = new InlineExitPlanMode(
1488
+ parentEl,
1489
+ enrichedInput,
1490
+ (decision: ExitPlanModeDecision | null) => {
1491
+ this.pendingExitPlanModeInline = null;
1492
+ this.restoreInputContainer(inputContainerEl);
1493
+ resolve(decision);
1494
+ },
1495
+ signal,
1496
+ renderContent,
1497
+ planPathPrefix,
1498
+ );
1499
+ this.pendingExitPlanModeInline = inline;
1500
+ try {
1501
+ inline.render();
1502
+ } catch (err) {
1503
+ this.pendingExitPlanModeInline = null;
1504
+ this.restoreInputContainer(inputContainerEl);
1505
+ reject(toError(err));
1506
+ }
1507
+ });
1508
+ }
1509
+
1510
+ dismissPendingApprovalPrompt(): void {
1511
+ if (this.pendingApprovalInline) {
1512
+ this.pendingApprovalInline.destroy();
1513
+ this.pendingApprovalInline = null;
1514
+ }
1515
+ }
1516
+
1517
+ dismissPendingApproval(): void {
1518
+ this.dismissPendingApprovalPrompt();
1519
+ if (this.pendingAskInline) {
1520
+ this.pendingAskInline.destroy();
1521
+ this.pendingAskInline = null;
1522
+ }
1523
+ if (this.pendingExitPlanModeInline) {
1524
+ this.pendingExitPlanModeInline.destroy();
1525
+ this.pendingExitPlanModeInline = null;
1526
+ }
1527
+ this.dismissPendingPlanApproval(true);
1528
+ this.resetInputContainerVisibility();
1529
+ }
1530
+
1531
+ private showPlanApproval(): Promise<{ decision: PlanApprovalDecision | null; invalidated: boolean }> {
1532
+ const inputContainerEl = this.deps.getInputContainerEl();
1533
+ const parentEl = inputContainerEl.parentElement;
1534
+ if (!parentEl) {
1535
+ return Promise.resolve({ decision: null, invalidated: false });
1536
+ }
1537
+
1538
+ this.hideInputContainer(inputContainerEl);
1539
+ this.pendingPlanApprovalInvalidated = false;
1540
+
1541
+ return new Promise<{ decision: PlanApprovalDecision | null; invalidated: boolean }>((resolve, reject) => {
1542
+ const inline = new InlinePlanApproval(
1543
+ parentEl,
1544
+ (decision: PlanApprovalDecision | null) => {
1545
+ const invalidated = this.pendingPlanApprovalInvalidated;
1546
+ this.pendingPlanApprovalInvalidated = false;
1547
+ this.pendingPlanApproval = null;
1548
+ this.restoreInputContainer(inputContainerEl);
1549
+ resolve({ decision, invalidated });
1550
+ },
1551
+ );
1552
+ this.pendingPlanApproval = inline;
1553
+ try {
1554
+ inline.render();
1555
+ } catch (err) {
1556
+ this.pendingPlanApproval = null;
1557
+ this.pendingPlanApprovalInvalidated = false;
1558
+ this.restoreInputContainer(inputContainerEl);
1559
+ reject(toError(err));
1560
+ }
1561
+ });
1562
+ }
1563
+
1564
+ private dismissPendingPlanApproval(invalidated: boolean): void {
1565
+ if (!this.pendingPlanApproval) {
1566
+ return;
1567
+ }
1568
+
1569
+ if (invalidated) {
1570
+ this.pendingPlanApprovalInvalidated = true;
1571
+ }
1572
+ this.pendingPlanApproval.destroy();
1573
+ this.pendingPlanApproval = null;
1574
+ }
1575
+
1576
+ private hideInputContainer(inputContainerEl: HTMLElement): void {
1577
+ this.inputContainerHideDepth++;
1578
+ inputContainerEl.addClass('claudian-hidden');
1579
+ }
1580
+
1581
+ private restoreInputContainer(inputContainerEl: HTMLElement): void {
1582
+ if (this.inputContainerHideDepth <= 0) return;
1583
+ this.inputContainerHideDepth--;
1584
+ if (this.inputContainerHideDepth === 0) {
1585
+ inputContainerEl.removeClass('claudian-hidden');
1586
+ }
1587
+ }
1588
+
1589
+ private resetInputContainerVisibility(): void {
1590
+ if (this.inputContainerHideDepth > 0) {
1591
+ this.inputContainerHideDepth = 0;
1592
+ this.deps.getInputContainerEl().removeClass('claudian-hidden');
1593
+ }
1594
+ }
1595
+
1596
+ // ============================================
1597
+ // Built-in Commands
1598
+ // ============================================
1599
+
1600
+ private async executeBuiltInCommand(command: BuiltInCommand, args: string): Promise<void> {
1601
+ const { conversationController } = this.deps;
1602
+ const capabilities = this.getActiveCapabilities();
1603
+
1604
+ if (!isBuiltInCommandSupported(command, capabilities)) {
1605
+ new Notice(`/${command.name} is not supported by this provider.`);
1606
+ return;
1607
+ }
1608
+
1609
+ switch (command.action) {
1610
+ case 'clear':
1611
+ await conversationController.createNew();
1612
+ break;
1613
+ case 'add-dir': {
1614
+ const externalContextSelector = this.deps.getExternalContextSelector();
1615
+ if (!externalContextSelector) {
1616
+ new Notice('External context selector not available.');
1617
+ return;
1618
+ }
1619
+ const result = externalContextSelector.addExternalContext(args);
1620
+ if (result.success) {
1621
+ new Notice(`Added external context: ${result.normalizedPath}`);
1622
+ } else {
1623
+ new Notice(result.error);
1624
+ }
1625
+ break;
1626
+ }
1627
+ case 'resume':
1628
+ this.showResumeDropdown();
1629
+ break;
1630
+ case 'fork': {
1631
+ if (!this.getActiveCapabilities().supportsFork) {
1632
+ new Notice('Fork is not supported by this provider.');
1633
+ return;
1634
+ }
1635
+ if (!this.deps.onForkAll) {
1636
+ new Notice('Fork not available.');
1637
+ return;
1638
+ }
1639
+ await this.deps.onForkAll();
1640
+ break;
1641
+ }
1642
+ default: {
1643
+ // Unknown command - notify user
1644
+ const unknownAction = typeof (command as { action?: unknown }).action === 'string'
1645
+ ? (command as { action: string }).action
1646
+ : 'unknown';
1647
+ new Notice(`Unknown command: ${unknownAction}`);
1648
+ break;
1649
+ }
1650
+ }
1651
+ }
1652
+
1653
+ // ============================================
1654
+ // Resume Session Dropdown
1655
+ // ============================================
1656
+
1657
+ handleResumeKeydown(e: KeyboardEvent): boolean {
1658
+ if (!this.activeResumeDropdown?.isVisible()) return false;
1659
+ return this.activeResumeDropdown.handleKeydown(e);
1660
+ }
1661
+
1662
+ isResumeDropdownVisible(): boolean {
1663
+ return this.activeResumeDropdown?.isVisible() ?? false;
1664
+ }
1665
+
1666
+ destroyResumeDropdown(): void {
1667
+ if (this.activeResumeDropdown) {
1668
+ this.activeResumeDropdown.destroy();
1669
+ this.activeResumeDropdown = null;
1670
+ }
1671
+ }
1672
+
1673
+ private showResumeDropdown(): void {
1674
+ const { plugin, state, conversationController } = this.deps;
1675
+
1676
+ // Clean up any existing dropdown
1677
+ this.destroyResumeDropdown();
1678
+
1679
+ const conversations = plugin.getConversationList();
1680
+ if (conversations.length === 0) {
1681
+ new Notice('No conversations to resume');
1682
+ return;
1683
+ }
1684
+
1685
+ const openConversation = this.deps.openConversation
1686
+ ?? ((id: string) => conversationController.switchTo(id));
1687
+
1688
+ this.activeResumeDropdown = new ResumeSessionDropdown(
1689
+ this.deps.getInputContainerEl(),
1690
+ this.deps.getInputEl(),
1691
+ conversations,
1692
+ state.currentConversationId,
1693
+ {
1694
+ onSelect: (id) => {
1695
+ this.destroyResumeDropdown();
1696
+ openConversation(id).catch((err: unknown) => {
1697
+ const msg = err instanceof Error ? err.message : String(err);
1698
+ new Notice(`Failed to open conversation: ${msg}`);
1699
+ });
1700
+ },
1701
+ onDismiss: () => {
1702
+ this.destroyResumeDropdown();
1703
+ },
1704
+ }
1705
+ );
1706
+ }
1707
+ }