@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,1103 @@
1
+ import { Menu, Notice, setIcon } from 'obsidian';
2
+
3
+ import type { TitleGenerationService } from '../../../core/providers/types';
4
+ import type { ChatRuntime } from '../../../core/runtime/ChatRuntime';
5
+ import type { ChatRewindMode } from '../../../core/runtime/types';
6
+ import type { Conversation } from '../../../core/types';
7
+ import { t } from '../../../i18n/i18n';
8
+ import type ClaudianPlugin from '../../../main';
9
+ import { confirm } from '../../../shared/modals/ConfirmModal';
10
+ import { extractUserDisplayContent } from '../../../utils/context';
11
+ import type { MessageRenderer } from '../rendering/MessageRenderer';
12
+ import { cleanupThinkingBlock } from '../rendering/ThinkingBlockRenderer';
13
+ import { findRewindContext } from '../rewind';
14
+ import type { SubagentManager } from '../services/SubagentManager';
15
+ import type { ChatState } from '../state/ChatState';
16
+ import type { FileContextManager } from '../ui/FileContext';
17
+ import type { ImageContextManager } from '../ui/ImageContext';
18
+ import type { ExternalContextSelector, McpServerSelector } from '../ui/InputToolbar';
19
+ import type { StatusPanel } from '../ui/StatusPanel';
20
+
21
+ function runConversationAction(action: () => Promise<void>, failureMessage: string): void {
22
+ void action().catch(() => {
23
+ new Notice(failureMessage);
24
+ });
25
+ }
26
+
27
+ export interface ConversationCallbacks {
28
+ onNewConversation?: () => void;
29
+ onConversationLoaded?: () => void;
30
+ onConversationSwitched?: () => void;
31
+ }
32
+
33
+ export interface ConversationControllerDeps {
34
+ plugin: ClaudianPlugin;
35
+ state: ChatState;
36
+ renderer: MessageRenderer;
37
+ subagentManager: SubagentManager;
38
+ getHistoryDropdown: () => HTMLElement | null;
39
+ getWelcomeEl: () => HTMLElement | null;
40
+ setWelcomeEl: (el: HTMLElement | null) => void;
41
+ getMessagesEl: () => HTMLElement;
42
+ getInputEl: () => HTMLTextAreaElement;
43
+ getFileContextManager: () => FileContextManager | null;
44
+ getImageContextManager: () => ImageContextManager | null;
45
+ getMcpServerSelector: () => McpServerSelector | null;
46
+ getExternalContextSelector: () => ExternalContextSelector | null;
47
+ clearQueuedMessage: () => void;
48
+ getTitleGenerationService: () => TitleGenerationService | null;
49
+ getStatusPanel: () => StatusPanel | null;
50
+ getAgentService?: () => ChatRuntime | null;
51
+ ensureServiceForConversation?: (conversation: Conversation | null) => Promise<void>;
52
+ dismissPendingInlinePrompts?: () => void;
53
+ }
54
+
55
+ type SaveOptions = {
56
+ resumeAtMessageId?: string;
57
+ };
58
+
59
+ export type HistoryConversationOpenState = 'closed' | 'open' | 'current';
60
+
61
+ export type HistoryConversationStatus = {
62
+ openState: HistoryConversationOpenState;
63
+ isRunning: boolean;
64
+ location?: 'current-view' | 'other-view';
65
+ tabIndex?: number;
66
+ };
67
+
68
+ type HistoryRenderOptions = {
69
+ onSelectConversation: (id: string) => Promise<void>;
70
+ onOpenConversationInNewTab?: (id: string, activate?: boolean) => Promise<void>;
71
+ getConversationOpenState?: (id: string) => HistoryConversationOpenState;
72
+ getConversationStatus?: (id: string) => HistoryConversationStatus;
73
+ onRerender: () => void;
74
+ };
75
+
76
+ export class ConversationController {
77
+ private deps: ConversationControllerDeps;
78
+ private callbacks: ConversationCallbacks;
79
+
80
+ constructor(deps: ConversationControllerDeps, callbacks: ConversationCallbacks = {}) {
81
+ this.deps = deps;
82
+ this.callbacks = callbacks;
83
+ }
84
+
85
+ private getAgentService(): ChatRuntime | null {
86
+ return this.deps.getAgentService?.() ?? null;
87
+ }
88
+
89
+ // ============================================
90
+ // Conversation Lifecycle
91
+ // ============================================
92
+
93
+ /**
94
+ * Resets to entry point state (New Chat).
95
+ *
96
+ * Entry point is a blank UI state - no conversation is created until the
97
+ * first message is sent. This prevents empty conversations cluttering history.
98
+ */
99
+ async createNew(options: { force?: boolean } = {}): Promise<void> {
100
+ const { plugin, state, subagentManager } = this.deps;
101
+ const force = !!options.force;
102
+ if (state.isStreaming && !force) return;
103
+ if (state.isCreatingConversation) return;
104
+ if (state.isSwitchingConversation) return;
105
+
106
+ // Set flag to block message sending during reset
107
+ state.isCreatingConversation = true;
108
+
109
+ try {
110
+ this.deps.dismissPendingInlinePrompts?.();
111
+
112
+ if (force && state.isStreaming) {
113
+ state.cancelRequested = true;
114
+ state.bumpStreamGeneration();
115
+ this.getAgentService()?.cancel();
116
+ }
117
+
118
+ // Save current conversation if it has messages
119
+ if (state.currentConversationId && state.messages.length > 0) {
120
+ await this.save();
121
+ }
122
+
123
+ subagentManager.orphanAllActive();
124
+ subagentManager.clear();
125
+
126
+ // Clear streaming state and related DOM references
127
+ cleanupThinkingBlock(state.currentThinkingState);
128
+ state.currentContentEl = null;
129
+ state.currentTextEl = null;
130
+ state.currentTextContent = '';
131
+ state.currentThinkingState = null;
132
+ state.toolCallElements.clear();
133
+ state.writeEditStates.clear();
134
+ state.isStreaming = false;
135
+
136
+ // Reset to entry point state - no conversation created yet
137
+ state.currentConversationId = null;
138
+ state.clearMessages();
139
+ state.usage = null;
140
+ state.currentTodos = null;
141
+ state.pendingNewSessionPlan = null;
142
+ state.planFilePath = null;
143
+ state.prePlanPermissionMode = null;
144
+ state.autoScrollEnabled = plugin.settings.enableAutoScroll ?? true;
145
+ state.hasPendingConversationSave = false;
146
+
147
+ // Reset agent service session (no session ID for entry point)
148
+ // Pass persistent paths to prevent stale external contexts
149
+ this.getAgentService()?.syncConversationState(
150
+ null,
151
+ plugin.settings.persistentExternalContextPaths || []
152
+ );
153
+
154
+ const messagesEl = this.deps.getMessagesEl();
155
+ messagesEl.empty();
156
+
157
+ // Recreate welcome element first (before StatusPanel for consistent ordering)
158
+ const welcomeEl = messagesEl.createDiv({ cls: 'claudian-welcome' });
159
+ welcomeEl.createDiv({ cls: 'claudian-welcome-greeting', text: this.getGreeting() });
160
+ this.deps.setWelcomeEl(welcomeEl);
161
+
162
+ // Remount StatusPanel to restore state for new conversation
163
+ this.deps.getStatusPanel()?.remount();
164
+
165
+ this.deps.getInputEl().value = '';
166
+
167
+ const fileCtx = this.deps.getFileContextManager();
168
+ fileCtx?.resetForNewConversation();
169
+ fileCtx?.autoAttachActiveFile();
170
+
171
+ this.deps.getImageContextManager()?.clearImages();
172
+ this.deps.getMcpServerSelector()?.clearEnabled();
173
+ // Pass current settings to ensure we have the most up-to-date persistent paths
174
+ this.deps.getExternalContextSelector()?.clearExternalContexts(
175
+ plugin.settings.persistentExternalContextPaths || []
176
+ );
177
+ this.deps.clearQueuedMessage();
178
+
179
+ this.callbacks.onNewConversation?.();
180
+ } finally {
181
+ state.isCreatingConversation = false;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Loads the current tab conversation, or starts at entry point if none.
187
+ *
188
+ * Entry point (no conversation) shows welcome screen without
189
+ * creating a conversation. Conversation is created lazily on first message.
190
+ */
191
+ async loadActive(): Promise<void> {
192
+ const { plugin, state, renderer } = this.deps;
193
+
194
+ const conversationId = state.currentConversationId;
195
+ const conversation = conversationId ? await plugin.getConversationById(conversationId) : null;
196
+
197
+ // No active conversation - start at entry point
198
+ if (!conversation) {
199
+ state.currentConversationId = null;
200
+ state.clearMessages();
201
+ state.usage = null;
202
+ state.currentTodos = null;
203
+ state.pendingNewSessionPlan = null;
204
+ state.planFilePath = null;
205
+ state.prePlanPermissionMode = null;
206
+ state.autoScrollEnabled = plugin.settings.enableAutoScroll ?? true;
207
+ state.hasPendingConversationSave = false;
208
+
209
+ // Pass persistent paths to prevent stale external contexts
210
+ this.getAgentService()?.syncConversationState(
211
+ null,
212
+ plugin.settings.persistentExternalContextPaths || []
213
+ );
214
+
215
+ const fileCtx = this.deps.getFileContextManager();
216
+ fileCtx?.resetForNewConversation();
217
+ fileCtx?.autoAttachActiveFile();
218
+
219
+ // Initialize external contexts with persistent paths from settings
220
+ this.deps.getExternalContextSelector()?.clearExternalContexts(
221
+ plugin.settings.persistentExternalContextPaths || []
222
+ );
223
+
224
+ this.deps.getMcpServerSelector()?.clearEnabled();
225
+
226
+ const welcomeEl = renderer.renderMessages(
227
+ [],
228
+ () => this.getGreeting()
229
+ );
230
+ this.deps.setWelcomeEl(welcomeEl);
231
+ this.updateWelcomeVisibility();
232
+
233
+ this.callbacks.onConversationLoaded?.();
234
+ return;
235
+ }
236
+
237
+ await this.deps.ensureServiceForConversation?.(conversation);
238
+ this.restoreConversation(conversation, { autoAttachFile: true });
239
+ this.updateWelcomeVisibility();
240
+
241
+ this.callbacks.onConversationLoaded?.();
242
+ }
243
+
244
+ /** Switches to a different conversation. */
245
+ async switchTo(id: string): Promise<void> {
246
+ const { plugin, state, subagentManager } = this.deps;
247
+
248
+ if (id === state.currentConversationId) return;
249
+ if (state.isStreaming) return;
250
+ if (state.isSwitchingConversation) return;
251
+ if (state.isCreatingConversation) return;
252
+
253
+ state.isSwitchingConversation = true;
254
+
255
+ try {
256
+ this.deps.dismissPendingInlinePrompts?.();
257
+ await this.save();
258
+
259
+ subagentManager.orphanAllActive();
260
+ subagentManager.clear();
261
+
262
+ const conversation = await plugin.switchConversation(id);
263
+ if (!conversation) {
264
+ return;
265
+ }
266
+
267
+ await this.deps.ensureServiceForConversation?.(conversation);
268
+
269
+ this.deps.getInputEl().value = '';
270
+ this.deps.clearQueuedMessage();
271
+
272
+ this.restoreConversation(conversation);
273
+
274
+ this.deps.getHistoryDropdown()?.removeClass('visible');
275
+ this.updateWelcomeVisibility();
276
+
277
+ this.callbacks.onConversationSwitched?.();
278
+ } finally {
279
+ state.isSwitchingConversation = false;
280
+ }
281
+ }
282
+
283
+ async rewind(
284
+ userMessageId: string,
285
+ mode: ChatRewindMode = 'code-and-conversation',
286
+ ): Promise<void> {
287
+ const { plugin, state, renderer } = this.deps;
288
+
289
+ const agentServiceForCheck = this.getAgentService();
290
+ if (agentServiceForCheck && !agentServiceForCheck.getCapabilities().supportsRewind) {
291
+ new Notice(t('chat.rewind.failed', { error: 'Rewind is not supported by this provider.' }));
292
+ return;
293
+ }
294
+
295
+ if (state.isStreaming) {
296
+ new Notice(t('chat.rewind.unavailableStreaming'));
297
+ return;
298
+ }
299
+
300
+ const msgs = state.messages;
301
+ const userIdx = msgs.findIndex(m => m.id === userMessageId);
302
+ if (userIdx === -1) {
303
+ new Notice(t('chat.rewind.failed', { error: 'Message not found' }));
304
+ return;
305
+ }
306
+ const userMsg = msgs[userIdx];
307
+ if (!userMsg.userMessageId) {
308
+ new Notice(t('chat.rewind.unavailableNoUuid'));
309
+ return;
310
+ }
311
+
312
+ const rewindCtx = findRewindContext(msgs, userIdx);
313
+ if (!rewindCtx.hasResponse || !rewindCtx.prevAssistantUuid) {
314
+ new Notice(t('chat.rewind.unavailableNoUuid'));
315
+ return;
316
+ }
317
+ const prevAssistantUuid = rewindCtx.prevAssistantUuid;
318
+
319
+ const confirmed = await confirm(
320
+ plugin.app,
321
+ mode === 'conversation'
322
+ ? t('chat.rewind.confirmMessageConversationOnly')
323
+ : t('chat.rewind.confirmMessage'),
324
+ t('chat.rewind.confirmButton')
325
+ );
326
+ if (!confirmed) return;
327
+
328
+ if (state.isStreaming) {
329
+ new Notice(t('chat.rewind.unavailableStreaming'));
330
+ return;
331
+ }
332
+
333
+ const agentService = this.getAgentService();
334
+ if (!agentService) {
335
+ new Notice(t('chat.rewind.failed', { error: 'Agent service not available' }));
336
+ return;
337
+ }
338
+
339
+ let result;
340
+ try {
341
+ result = await agentService.rewind(userMsg.userMessageId, prevAssistantUuid, mode);
342
+ } catch (e) {
343
+ new Notice(t('chat.rewind.failed', { error: e instanceof Error ? e.message : 'Unknown error' }));
344
+ return;
345
+ }
346
+ if (!result.canRewind) {
347
+ new Notice(t('chat.rewind.cannot', { error: result.error ?? 'Unknown error' }));
348
+ return;
349
+ }
350
+
351
+ state.truncateAt(userMessageId);
352
+
353
+ const inputEl = this.deps.getInputEl();
354
+ inputEl.value = userMsg.content;
355
+ inputEl.focus();
356
+
357
+ const welcomeEl = renderer.renderMessages(state.messages, () => this.getGreeting());
358
+ this.deps.setWelcomeEl(welcomeEl);
359
+ this.updateWelcomeVisibility();
360
+
361
+ const filesChanged = result.filesChanged?.length ?? 0;
362
+ let saveError: string | null = null;
363
+ try {
364
+ await this.save(false, { resumeAtMessageId: prevAssistantUuid });
365
+ } catch (e) {
366
+ saveError = e instanceof Error ? e.message : 'Failed to save';
367
+ }
368
+
369
+ if (saveError) {
370
+ new Notice(
371
+ mode === 'conversation'
372
+ ? t('chat.rewind.noticeConversationOnlySaveFailed', { error: saveError })
373
+ : t('chat.rewind.noticeSaveFailed', { count: String(filesChanged), error: saveError })
374
+ );
375
+ return;
376
+ }
377
+
378
+ new Notice(
379
+ mode === 'conversation'
380
+ ? t('chat.rewind.noticeConversationOnly')
381
+ : t('chat.rewind.notice', { count: String(filesChanged) })
382
+ );
383
+ }
384
+
385
+ /**
386
+ * Saves the current conversation.
387
+ *
388
+ * If we're at an entry point (no conversation yet) and have messages,
389
+ * creates a new conversation first (lazy creation).
390
+ *
391
+ * For native sessions (new conversations with sessionId from SDK),
392
+ * only metadata is saved - the SDK handles message persistence.
393
+ */
394
+ async save(updateLastResponse = false, options?: SaveOptions): Promise<void> {
395
+ const { plugin, state } = this.deps;
396
+
397
+ // Entry point with no messages - nothing to save
398
+ if (!state.currentConversationId && state.messages.length === 0) {
399
+ return;
400
+ }
401
+
402
+ const agentService = this.getAgentService();
403
+ const sessionInvalidated = agentService?.consumeSessionInvalidation?.() ?? false;
404
+
405
+ // Entry point with messages - create conversation lazily
406
+ // New conversations always use SDK-native storage.
407
+ if (!state.currentConversationId && state.messages.length > 0) {
408
+ const initialSessionId = agentService?.getSessionId() ?? undefined;
409
+ const conversation = await plugin.createConversation({
410
+ providerId: agentService?.providerId,
411
+ sessionId: initialSessionId,
412
+ });
413
+ state.currentConversationId = conversation.id;
414
+ }
415
+
416
+ const fileCtx = this.deps.getFileContextManager();
417
+ const currentNote = fileCtx?.getCurrentNotePath() || undefined;
418
+ const externalContextSelector = this.deps.getExternalContextSelector();
419
+ const externalContextPaths = externalContextSelector?.getExternalContexts() ?? [];
420
+ const mcpServerSelector = this.deps.getMcpServerSelector();
421
+ const enabledMcpServers = mcpServerSelector ? Array.from(mcpServerSelector.getEnabledServers()) : [];
422
+
423
+ const conversation = plugin.getConversationSync(state.currentConversationId!);
424
+
425
+ const { updates: sessionUpdates } = agentService
426
+ ? agentService.buildSessionUpdates({ conversation, sessionInvalidated })
427
+ : { updates: {} };
428
+
429
+ const updates: Partial<Conversation> = {
430
+ ...sessionUpdates,
431
+ messages: state.messages,
432
+ currentNote: currentNote,
433
+ externalContextPaths: externalContextPaths.length > 0 ? externalContextPaths : undefined,
434
+ usage: state.usage ?? undefined,
435
+ enabledMcpServers: enabledMcpServers.length > 0 ? enabledMcpServers : undefined,
436
+ };
437
+
438
+ if (updateLastResponse) {
439
+ updates.lastResponseAt = Date.now();
440
+ }
441
+
442
+ if (options) {
443
+ updates.resumeAtMessageId = options.resumeAtMessageId;
444
+ }
445
+
446
+ await plugin.updateConversation(state.currentConversationId!, updates);
447
+ state.hasPendingConversationSave = false;
448
+ }
449
+
450
+ /**
451
+ * Shared logic for restoring a conversation into the current tab.
452
+ * Used by both loadActive() and switchTo() to avoid duplication.
453
+ */
454
+ private restoreConversation(
455
+ conversation: Conversation,
456
+ options?: { autoAttachFile?: boolean }
457
+ ): void {
458
+ const { plugin, state, renderer } = this.deps;
459
+
460
+ state.currentConversationId = conversation.id;
461
+ state.messages = [...conversation.messages];
462
+ state.usage = conversation.usage ?? null;
463
+ state.autoScrollEnabled = plugin.settings.enableAutoScroll ?? true;
464
+ state.hasPendingConversationSave = false;
465
+
466
+ // Clear status panels (auto-hide: panels reappear when agent creates new todos)
467
+ state.currentTodos = null;
468
+
469
+ const hasMessages = state.messages.length > 0;
470
+
471
+ // Determine external context paths for this session
472
+ // Empty session: use persistent paths; session with messages: use saved paths
473
+ const externalContextPaths = hasMessages
474
+ ? conversation.externalContextPaths || []
475
+ : plugin.settings.persistentExternalContextPaths || [];
476
+
477
+ this.getAgentService()?.syncConversationState(conversation, externalContextPaths);
478
+
479
+ const fileCtx = this.deps.getFileContextManager();
480
+ fileCtx?.resetForLoadedConversation(hasMessages);
481
+
482
+ if (conversation.currentNote) {
483
+ fileCtx?.setCurrentNote(conversation.currentNote);
484
+ } else if (!hasMessages && options?.autoAttachFile) {
485
+ fileCtx?.autoAttachActiveFile();
486
+ }
487
+
488
+ this.restoreExternalContextPaths(conversation.externalContextPaths, !hasMessages);
489
+
490
+ const mcpServerSelector = this.deps.getMcpServerSelector();
491
+ if (conversation.enabledMcpServers && conversation.enabledMcpServers.length > 0) {
492
+ mcpServerSelector?.setEnabledServers(conversation.enabledMcpServers);
493
+ } else {
494
+ mcpServerSelector?.clearEnabled();
495
+ }
496
+
497
+ const welcomeEl = renderer.renderMessages(
498
+ state.messages,
499
+ () => this.getGreeting()
500
+ );
501
+ this.deps.setWelcomeEl(welcomeEl);
502
+ }
503
+
504
+ /**
505
+ * Restores external context paths based on session state.
506
+ * New or empty sessions get current persistent paths from settings.
507
+ * Sessions with messages restore exactly what was saved.
508
+ */
509
+ private restoreExternalContextPaths(
510
+ savedPaths: string[] | undefined,
511
+ isEmptySession: boolean
512
+ ): void {
513
+ const { plugin } = this.deps;
514
+ const externalContextSelector = this.deps.getExternalContextSelector();
515
+ if (!externalContextSelector) {
516
+ return;
517
+ }
518
+
519
+ if (isEmptySession) {
520
+ // Empty session: use current persistent paths from settings
521
+ externalContextSelector.clearExternalContexts(
522
+ plugin.settings.persistentExternalContextPaths || []
523
+ );
524
+ } else {
525
+ // Session with messages: restore exactly what was saved
526
+ externalContextSelector.setExternalContexts(savedPaths || []);
527
+ }
528
+ }
529
+
530
+ // ============================================
531
+ // History Dropdown
532
+ // ============================================
533
+
534
+ toggleHistoryDropdown(): void {
535
+ const dropdown = this.deps.getHistoryDropdown();
536
+ if (!dropdown) return;
537
+
538
+ const isVisible = dropdown.hasClass('visible');
539
+ if (isVisible) {
540
+ dropdown.removeClass('visible');
541
+ } else {
542
+ this.updateHistoryDropdown();
543
+ dropdown.addClass('visible');
544
+ }
545
+ }
546
+
547
+ updateHistoryDropdown(): void {
548
+ const dropdown = this.deps.getHistoryDropdown();
549
+ if (!dropdown) return;
550
+
551
+ this.renderHistoryItems(dropdown, {
552
+ onSelectConversation: (id) => this.switchTo(id),
553
+ onRerender: () => this.updateHistoryDropdown(),
554
+ });
555
+ }
556
+
557
+ /**
558
+ * Renders history dropdown items to a container.
559
+ * Shared implementation for updateHistoryDropdown() and renderHistoryDropdown().
560
+ */
561
+ private renderHistoryItems(
562
+ container: HTMLElement,
563
+ options: HistoryRenderOptions
564
+ ): void {
565
+ const { plugin, state } = this.deps;
566
+
567
+ container.empty();
568
+
569
+ const dropdownHeader = container.createDiv({ cls: 'claudian-history-header' });
570
+ dropdownHeader.createSpan({ text: 'Conversations' });
571
+
572
+ const list = container.createDiv({ cls: 'claudian-history-list' });
573
+ const allConversations = plugin.getConversationList();
574
+
575
+ if (allConversations.length === 0) {
576
+ list.createDiv({ cls: 'claudian-history-empty', text: 'No conversations' });
577
+ return;
578
+ }
579
+
580
+ // Sort by lastResponseAt (fallback to createdAt) descending
581
+ const conversations = [...allConversations].sort((a, b) => {
582
+ return (b.lastResponseAt ?? b.createdAt) - (a.lastResponseAt ?? a.createdAt);
583
+ });
584
+
585
+ for (const conv of conversations) {
586
+ const fallbackOpenState: HistoryConversationOpenState =
587
+ conv.id === state.currentConversationId ? 'current' : 'closed';
588
+ const conversationStatus = this.getHistoryConversationStatus(conv.id, fallbackOpenState, options);
589
+ const { openState, isRunning } = conversationStatus;
590
+ const isCurrent = openState === 'current';
591
+ const isOpen = openState === 'open';
592
+ const item = list.createDiv({
593
+ cls: [
594
+ 'claudian-history-item',
595
+ isCurrent ? 'active' : '',
596
+ isOpen ? 'open' : '',
597
+ isRunning ? 'running' : '',
598
+ ].filter(Boolean).join(' '),
599
+ });
600
+ item.setAttribute('data-open-state', openState);
601
+ item.setAttribute('data-running', isRunning ? 'true' : 'false');
602
+ item.setAttribute('data-tab-location', conversationStatus.location ?? 'current-view');
603
+ if (typeof conversationStatus.tabIndex === 'number') {
604
+ item.setAttribute('data-tab-index', String(conversationStatus.tabIndex));
605
+ }
606
+
607
+ const iconEl = item.createDiv({ cls: 'claudian-history-item-icon' });
608
+ setIcon(iconEl, this.getHistoryItemIcon(openState, isRunning));
609
+
610
+ const content = item.createDiv({ cls: 'claudian-history-item-content' });
611
+ const titleEl = content.createDiv({ cls: 'claudian-history-item-title', text: conv.title });
612
+ titleEl.setAttribute('title', conv.title);
613
+ content.createDiv({
614
+ cls: 'claudian-history-item-date',
615
+ text: this.getHistoryItemStatusText(conversationStatus, conv.lastResponseAt ?? conv.createdAt),
616
+ });
617
+
618
+ if (!isCurrent) {
619
+ content.addEventListener('click', (e) => {
620
+ e.stopPropagation();
621
+ if (this.isHistoryNewTabModifierClick(e) && options.onOpenConversationInNewTab) {
622
+ e.preventDefault();
623
+ runConversationAction(
624
+ () => this.runHistoryAction(
625
+ () => options.onOpenConversationInNewTab?.(conv.id, true),
626
+ 'Failed to load conversation',
627
+ ),
628
+ 'Failed to load conversation',
629
+ );
630
+ return;
631
+ }
632
+
633
+ runConversationAction(
634
+ () => this.runHistoryAction(
635
+ () => options.onSelectConversation(conv.id),
636
+ 'Failed to load conversation',
637
+ ),
638
+ 'Failed to load conversation',
639
+ );
640
+ });
641
+
642
+ if (options.onOpenConversationInNewTab) {
643
+ content.addEventListener('auxclick', (e) => {
644
+ if (e.button !== 1) return;
645
+ e.preventDefault();
646
+ e.stopPropagation();
647
+ runConversationAction(
648
+ () => this.runHistoryAction(
649
+ () => options.onOpenConversationInNewTab?.(conv.id, true),
650
+ 'Failed to load conversation',
651
+ ),
652
+ 'Failed to load conversation',
653
+ );
654
+ });
655
+ }
656
+ }
657
+
658
+ item.addEventListener('contextmenu', (e) => {
659
+ e.preventDefault();
660
+ e.stopPropagation();
661
+ this.showHistoryContextMenu(item, conv.id, conv.title, isCurrent, options, e);
662
+ });
663
+
664
+ const actions = item.createDiv({ cls: 'claudian-history-item-actions' });
665
+
666
+ // Show regenerate button if title generation failed, or loading indicator if pending
667
+ if (conv.titleGenerationStatus === 'pending') {
668
+ const loadingEl = actions.createEl('span', { cls: 'claudian-action-btn claudian-action-loading' });
669
+ setIcon(loadingEl, 'loader-2');
670
+ loadingEl.setAttribute('aria-label', 'Generating title...');
671
+ } else if (conv.titleGenerationStatus === 'failed') {
672
+ const regenerateBtn = actions.createEl('button', { cls: 'claudian-action-btn' });
673
+ setIcon(regenerateBtn, 'refresh-cw');
674
+ regenerateBtn.setAttribute('aria-label', 'Regenerate title');
675
+ regenerateBtn.addEventListener('click', (e) => {
676
+ e.stopPropagation();
677
+ runConversationAction(
678
+ () => this.regenerateTitle(conv.id),
679
+ 'Failed to regenerate response',
680
+ );
681
+ });
682
+ }
683
+
684
+ if (openState === 'closed' && options.onOpenConversationInNewTab) {
685
+ const openInNewTabBtn = actions.createEl('button', {
686
+ cls: 'claudian-action-btn claudian-open-new-tab-btn',
687
+ });
688
+ setIcon(openInNewTabBtn, 'square-plus');
689
+ openInNewTabBtn.setAttribute('aria-label', 'Open in new tab');
690
+ openInNewTabBtn.addEventListener('click', (e) => {
691
+ e.stopPropagation();
692
+ runConversationAction(
693
+ () => this.runHistoryAction(
694
+ () => options.onOpenConversationInNewTab?.(conv.id, true),
695
+ 'Failed to load conversation',
696
+ ),
697
+ 'Failed to load conversation',
698
+ );
699
+ });
700
+ }
701
+
702
+ const renameBtn = actions.createEl('button', { cls: 'claudian-action-btn' });
703
+ setIcon(renameBtn, 'pencil');
704
+ renameBtn.setAttribute('aria-label', 'Rename');
705
+ renameBtn.addEventListener('click', (e) => {
706
+ e.stopPropagation();
707
+ this.showRenameInput(item, conv.id, conv.title);
708
+ });
709
+
710
+ const deleteBtn = actions.createEl('button', { cls: 'claudian-action-btn claudian-delete-btn' });
711
+ setIcon(deleteBtn, 'trash-2');
712
+ deleteBtn.setAttribute('aria-label', 'Delete');
713
+ deleteBtn.addEventListener('click', (e) => {
714
+ e.stopPropagation();
715
+ runConversationAction(
716
+ () => this.runHistoryAction(
717
+ () => this.deleteHistoryConversation(conv.id, options),
718
+ 'Failed to delete conversation',
719
+ ),
720
+ 'Failed to delete conversation',
721
+ );
722
+ });
723
+ }
724
+ }
725
+
726
+ private getHistoryConversationStatus(
727
+ conversationId: string,
728
+ fallbackOpenState: HistoryConversationOpenState,
729
+ options: HistoryRenderOptions,
730
+ ): HistoryConversationStatus {
731
+ const status = options.getConversationStatus?.(conversationId);
732
+ if (status) return status;
733
+
734
+ return {
735
+ openState: options.getConversationOpenState?.(conversationId) ?? fallbackOpenState,
736
+ isRunning: false,
737
+ };
738
+ }
739
+
740
+ private getHistoryItemStatusText(
741
+ status: HistoryConversationStatus,
742
+ timestamp: number,
743
+ ): string {
744
+ const { openState, isRunning } = status;
745
+ const location = status.location ?? 'current-view';
746
+
747
+ if (openState !== 'closed' && location === 'other-view') {
748
+ return isRunning ? 'Running in another pane' : 'Open in another pane';
749
+ }
750
+
751
+ if (isRunning) {
752
+ if (openState === 'closed') return 'Running';
753
+ return `Running in ${this.getHistoryTabLabel(status)}`;
754
+ }
755
+
756
+ switch (openState) {
757
+ case 'current':
758
+ return typeof status.tabIndex === 'number'
759
+ ? `Current tab ${status.tabIndex}`
760
+ : 'Current session';
761
+ case 'open':
762
+ return `Open in ${this.getHistoryTabLabel(status)}`;
763
+ case 'closed':
764
+ return this.formatDate(timestamp);
765
+ }
766
+ }
767
+
768
+ private getHistoryTabLabel(status: HistoryConversationStatus): string {
769
+ if (typeof status.tabIndex === 'number') {
770
+ return `tab ${status.tabIndex}`;
771
+ }
772
+
773
+ if (status.openState === 'current') {
774
+ return 'current tab';
775
+ }
776
+
777
+ return 'tab';
778
+ }
779
+
780
+ private getHistoryItemIcon(
781
+ openState: HistoryConversationOpenState,
782
+ isRunning: boolean,
783
+ ): string {
784
+ if (isRunning) return 'loader-2';
785
+ if (openState === 'current') return 'message-square-dot';
786
+ return 'message-square';
787
+ }
788
+
789
+ private isHistoryNewTabModifierClick(event: MouseEvent): boolean {
790
+ return !event.altKey && !event.shiftKey && (event.metaKey || event.ctrlKey);
791
+ }
792
+
793
+ private async runHistoryAction(
794
+ action: () => Promise<void> | void,
795
+ errorMessage: string,
796
+ ): Promise<void> {
797
+ try {
798
+ await action();
799
+ } catch {
800
+ new Notice(errorMessage);
801
+ }
802
+ }
803
+
804
+ private showHistoryContextMenu(
805
+ item: HTMLElement,
806
+ conversationId: string,
807
+ title: string,
808
+ isCurrent: boolean,
809
+ options: HistoryRenderOptions,
810
+ event: MouseEvent,
811
+ ): void {
812
+ const menu = new Menu();
813
+ const fallbackOpenState: HistoryConversationOpenState = isCurrent ? 'current' : 'closed';
814
+ const { openState } = this.getHistoryConversationStatus(conversationId, fallbackOpenState, options);
815
+
816
+ if (openState !== 'current') {
817
+ if (openState === 'closed' && options.onOpenConversationInNewTab) {
818
+ menu.addItem((menuItem) => menuItem
819
+ .setTitle('Open in new tab')
820
+ .onClick(() => {
821
+ void this.runHistoryAction(
822
+ () => options.onOpenConversationInNewTab?.(conversationId, true),
823
+ 'Failed to load conversation',
824
+ );
825
+ }));
826
+ menu.addItem((menuItem) => menuItem
827
+ .setTitle('Open in background tab')
828
+ .onClick(() => {
829
+ void this.runHistoryAction(
830
+ () => options.onOpenConversationInNewTab?.(conversationId, false),
831
+ 'Failed to load conversation',
832
+ );
833
+ }));
834
+ } else if (openState === 'open') {
835
+ menu.addItem((menuItem) => menuItem
836
+ .setTitle('Switch to open session')
837
+ .onClick(() => {
838
+ void this.runHistoryAction(
839
+ () => options.onSelectConversation(conversationId),
840
+ 'Failed to load conversation',
841
+ );
842
+ }));
843
+ }
844
+ }
845
+
846
+ menu.addItem((menuItem) => menuItem
847
+ .setTitle('Rename')
848
+ .onClick(() => {
849
+ this.showRenameInput(item, conversationId, title);
850
+ }));
851
+ menu.addItem((menuItem) => menuItem
852
+ .setTitle('Delete')
853
+ .onClick(() => {
854
+ void this.runHistoryAction(
855
+ () => this.deleteHistoryConversation(conversationId, options),
856
+ 'Failed to delete conversation',
857
+ );
858
+ }));
859
+
860
+ menu.showAtMouseEvent(event);
861
+ }
862
+
863
+ private async deleteHistoryConversation(
864
+ conversationId: string,
865
+ options: HistoryRenderOptions,
866
+ ): Promise<void> {
867
+ const { plugin, state } = this.deps;
868
+ if (state.isStreaming) return;
869
+
870
+ await plugin.deleteConversation(conversationId);
871
+ options.onRerender();
872
+
873
+ if (conversationId === state.currentConversationId) {
874
+ await this.loadActive();
875
+ }
876
+ }
877
+
878
+ /** Shows inline rename input for a conversation. */
879
+ private showRenameInput(item: HTMLElement, convId: string, currentTitle: string): void {
880
+ const titleEl = item.querySelector('.claudian-history-item-title') as HTMLElement;
881
+ if (!titleEl) return;
882
+
883
+ const input = (item.ownerDocument ?? window.document).createElement('input');
884
+ input.type = 'text';
885
+ input.className = 'claudian-rename-input';
886
+ input.value = currentTitle;
887
+
888
+ titleEl.replaceWith(input);
889
+ input.focus();
890
+ input.select();
891
+
892
+ const finishRename = async () => {
893
+ try {
894
+ const newTitle = input.value.trim() || currentTitle;
895
+ await this.deps.plugin.renameConversation(convId, newTitle);
896
+ this.updateHistoryDropdown();
897
+ } catch {
898
+ new Notice('Failed to rename conversation');
899
+ }
900
+ };
901
+
902
+ input.addEventListener('blur', () => {
903
+ runConversationAction(finishRename, 'Failed to rename conversation');
904
+ });
905
+ input.addEventListener('keydown', (e) => {
906
+ // Check !e.isComposing for IME support (Chinese, Japanese, Korean, etc.)
907
+ if (e.key === 'Enter' && !e.isComposing) {
908
+ input.blur();
909
+ } else if (e.key === 'Escape' && !e.isComposing) {
910
+ input.value = currentTitle;
911
+ input.blur();
912
+ }
913
+ });
914
+ }
915
+
916
+ // ============================================
917
+ // Welcome & Greeting
918
+ // ============================================
919
+
920
+ /** Generates a dynamic greeting based on time/day. */
921
+ getGreeting(): string {
922
+ const now = new Date();
923
+ const hour = now.getHours();
924
+ const day = now.getDay(); // 0 = Sunday, 6 = Saturday
925
+ const name = this.deps.plugin.settings.userName?.trim();
926
+
927
+ // Helper to optionally personalize a greeting (with fallback for no-name case)
928
+ const personalize = (base: string, noNameFallback?: string): string =>
929
+ name ? `${base}, ${name}` : (noNameFallback ?? base);
930
+
931
+ // Day-specific greetings (some personalized, some universal)
932
+ const dayGreetings: Record<number, string[]> = {
933
+ 0: [personalize('Happy Sunday'), 'Sunday session?', 'Welcome to the weekend'],
934
+ 1: [personalize('Happy Monday'), personalize('Back at it', 'Back at it!')],
935
+ 2: [personalize('Happy Tuesday')],
936
+ 3: [personalize('Happy Wednesday')],
937
+ 4: [personalize('Happy Thursday')],
938
+ 5: [personalize('Happy Friday'), personalize('That Friday feeling')],
939
+ 6: [personalize('Happy Saturday', 'Happy Saturday!'), personalize('Welcome to the weekend')],
940
+ };
941
+
942
+ // Time-specific greetings
943
+ const getTimeGreetings = (): string[] => {
944
+ if (hour >= 5 && hour < 12) {
945
+ return [personalize('Good morning'), 'Coffee and Claudian time?'];
946
+ } else if (hour >= 12 && hour < 18) {
947
+ return [personalize('Good afternoon'), personalize('Hey there'), personalize("How's it going") + '?'];
948
+ } else if (hour >= 18 && hour < 22) {
949
+ return [personalize('Good evening'), personalize('Evening'), personalize('How was your day') + '?'];
950
+ } else {
951
+ return ['Hello, night owl', personalize('Evening')];
952
+ }
953
+ };
954
+
955
+ // General greetings
956
+ const generalGreetings = [
957
+ personalize('Hey there'),
958
+ name ? `Hi ${name}, how are you?` : 'Hi, how are you?',
959
+ personalize("How's it going") + '?',
960
+ personalize('Welcome back') + '!',
961
+ personalize("What's new") + '?',
962
+ ...(name ? [`${name} returns!`] : []),
963
+ 'You are absolutely right!',
964
+ ];
965
+
966
+ // Combine day + time + general greetings, pick randomly
967
+ const allGreetings = [
968
+ ...(dayGreetings[day] || []),
969
+ ...getTimeGreetings(),
970
+ ...generalGreetings,
971
+ ];
972
+
973
+ return allGreetings[Math.floor(Math.random() * allGreetings.length)];
974
+ }
975
+
976
+ /** Updates welcome element visibility based on message count. */
977
+ updateWelcomeVisibility(): void {
978
+ const welcomeEl = this.deps.getWelcomeEl();
979
+ if (!welcomeEl) return;
980
+
981
+ if (this.deps.state.messages.length === 0) {
982
+ welcomeEl.removeClass('claudian-hidden');
983
+ } else {
984
+ welcomeEl.addClass('claudian-hidden');
985
+ }
986
+ }
987
+
988
+ /**
989
+ * Initializes the welcome greeting for a new tab without a conversation.
990
+ * Called when a new tab is activated and has no conversation loaded.
991
+ */
992
+ initializeWelcome(): void {
993
+ const welcomeEl = this.deps.getWelcomeEl();
994
+ if (!welcomeEl) return;
995
+
996
+ // Initialize file context to auto-attach the currently focused note
997
+ const fileCtx = this.deps.getFileContextManager();
998
+ fileCtx?.resetForNewConversation();
999
+ fileCtx?.autoAttachActiveFile();
1000
+
1001
+ // Only add greeting if not already present
1002
+ if (!welcomeEl.querySelector('.claudian-welcome-greeting')) {
1003
+ welcomeEl.createDiv({ cls: 'claudian-welcome-greeting', text: this.getGreeting() });
1004
+ }
1005
+
1006
+ this.updateWelcomeVisibility();
1007
+ }
1008
+
1009
+ // ============================================
1010
+ // Utilities
1011
+ // ============================================
1012
+
1013
+ /** Generates a fallback title from the first message (used when AI fails). */
1014
+ generateFallbackTitle(firstMessage: string): string {
1015
+ const firstSentence = firstMessage.split(/[.!?\n]/)[0].trim();
1016
+ const autoTitle = firstSentence.substring(0, 50);
1017
+ const suffix = firstSentence.length > 50 ? '...' : '';
1018
+ return `${autoTitle}${suffix}`;
1019
+ }
1020
+
1021
+ /** Regenerates AI title for a conversation. */
1022
+ async regenerateTitle(conversationId: string): Promise<void> {
1023
+ const { plugin } = this.deps;
1024
+ if (!plugin.settings.enableAutoTitleGeneration) return;
1025
+
1026
+ // Title generation is delegated to the active provider service
1027
+ const fullConv = await plugin.getConversationById(conversationId);
1028
+ if (!fullConv || fullConv.messages.length < 1) return;
1029
+
1030
+ const titleService = this.deps.getTitleGenerationService();
1031
+ if (!titleService) return;
1032
+
1033
+ // Find first user message by role (not by index)
1034
+ const firstUserMsg = fullConv.messages.find(m => m.role === 'user');
1035
+ if (!firstUserMsg) return;
1036
+
1037
+ const userContent = firstUserMsg.displayContent
1038
+ ?? extractUserDisplayContent(firstUserMsg.content)
1039
+ ?? firstUserMsg.content;
1040
+
1041
+ // Store current title to check if user renames during generation
1042
+ const expectedTitle = fullConv.title;
1043
+
1044
+ // Set pending status before starting generation
1045
+ await plugin.updateConversation(conversationId, { titleGenerationStatus: 'pending' });
1046
+ this.updateHistoryDropdown();
1047
+
1048
+ // Fire async AI title generation
1049
+ await titleService.generateTitle(
1050
+ conversationId,
1051
+ userContent,
1052
+ async (convId, result) => {
1053
+ // Check if conversation still exists and user hasn't manually renamed
1054
+ const currentConv = await plugin.getConversationById(convId);
1055
+ if (!currentConv) return;
1056
+
1057
+ // Only apply AI title if user hasn't manually renamed (title still matches expected)
1058
+ const userManuallyRenamed = currentConv.title !== expectedTitle;
1059
+
1060
+ if (result.success && !userManuallyRenamed) {
1061
+ await plugin.renameConversation(convId, result.title);
1062
+ await plugin.updateConversation(convId, { titleGenerationStatus: 'success' });
1063
+ } else if (!userManuallyRenamed) {
1064
+ // Keep existing title, mark as failed (only if user hasn't renamed)
1065
+ await plugin.updateConversation(convId, { titleGenerationStatus: 'failed' });
1066
+ } else {
1067
+ // User manually renamed, clear the status (user's choice takes precedence)
1068
+ await plugin.updateConversation(convId, { titleGenerationStatus: undefined });
1069
+ }
1070
+ this.updateHistoryDropdown();
1071
+ }
1072
+ );
1073
+ }
1074
+
1075
+ /** Formats a timestamp for display. */
1076
+ formatDate(timestamp: number): string {
1077
+ const date = new Date(timestamp);
1078
+ const now = new Date();
1079
+
1080
+ if (date.toDateString() === now.toDateString()) {
1081
+ return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
1082
+ }
1083
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
1084
+ }
1085
+
1086
+ // ============================================
1087
+ // History Dropdown Rendering (for ClaudianView)
1088
+ // ============================================
1089
+
1090
+ /**
1091
+ * Renders the history dropdown content to a provided container.
1092
+ * Used by ClaudianView to render the dropdown with custom selection callback.
1093
+ */
1094
+ renderHistoryDropdown(
1095
+ container: HTMLElement,
1096
+ options: Omit<HistoryRenderOptions, 'onRerender'>,
1097
+ ): void {
1098
+ this.renderHistoryItems(container, {
1099
+ ...options,
1100
+ onRerender: () => this.renderHistoryDropdown(container, options),
1101
+ });
1102
+ }
1103
+ }