@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,1199 @@
1
+ import '@/providers';
2
+
3
+ // eslint-disable-next-line jest/no-mocks-import
4
+ import {
5
+ getLastOptions,
6
+ resetMockMessages,
7
+ setMockMessages,
8
+ } from '@test/__mocks__/claude-agent-sdk';
9
+ import * as fs from 'fs';
10
+
11
+ // Mock fs module
12
+ jest.mock('fs');
13
+
14
+ // Now import after all mocks are set up
15
+ import { buildInlineEditPrompt, parseInlineEditResponse } from '@/core/prompt/inlineEdit';
16
+ import { getPathFromToolInput } from '@/core/tools/toolInput';
17
+ import type { InlineEditRequest } from '@/providers/claude/auxiliary/ClaudeInlineEditService';
18
+ import {
19
+ createReadOnlyHook,
20
+ InlineEditService,
21
+ } from '@/providers/claude/auxiliary/ClaudeInlineEditService';
22
+ import { buildCursorContext } from '@/utils/editor';
23
+
24
+ // Create a mock plugin
25
+ function createMockPlugin(settings = {}) {
26
+ return {
27
+ settings: {
28
+ model: 'sonnet',
29
+ thinkingBudget: 'off',
30
+ ...settings,
31
+ },
32
+ app: {
33
+ vault: {
34
+ adapter: {
35
+ basePath: '/test/vault/path',
36
+ },
37
+ },
38
+ },
39
+ getActiveEnvironmentVariables: jest.fn().mockReturnValue(''),
40
+ getResolvedProviderCliPath: jest.fn().mockReturnValue('/fake/claude'),
41
+ } as any;
42
+ }
43
+
44
+ // Hook functions accept typed HookInput / return typed HookJSONOutput, but the
45
+ // implementation only reads tool_name/tool_input. Cast I/O to any in tests.
46
+ const callHook = async (hook: any, input: any, ...rest: any[]): Promise<any> =>
47
+ hook(input, ...rest);
48
+
49
+ describe('InlineEditService', () => {
50
+ let service: InlineEditService;
51
+ let mockPlugin: any;
52
+
53
+ beforeEach(() => {
54
+ jest.clearAllMocks();
55
+ resetMockMessages();
56
+ mockPlugin = createMockPlugin();
57
+ service = new InlineEditService(mockPlugin);
58
+ });
59
+
60
+
61
+ describe('buildPrompt', () => {
62
+ it('should build prompt with correct format', () => {
63
+ const request: InlineEditRequest = {
64
+ mode: 'selection',
65
+ selectedText: 'Hello world',
66
+ instruction: 'Fix the greeting',
67
+ notePath: 'notes/test.md',
68
+ };
69
+
70
+ const prompt = buildInlineEditPrompt(request);
71
+
72
+ // New format: instruction first, then XML context (no <query> wrapper)
73
+ expect(prompt).toContain('Fix the greeting');
74
+ expect(prompt).toContain('<editor_selection path="notes/test.md">');
75
+ expect(prompt).toContain('Hello world');
76
+ expect(prompt).toContain('</editor_selection>');
77
+ // Verify instruction comes before selection
78
+ expect(prompt.indexOf('Fix the greeting')).toBeLessThan(prompt.indexOf('<editor_selection'));
79
+ });
80
+
81
+ it('should preserve selected text with newlines', () => {
82
+ const request: InlineEditRequest = {
83
+ mode: 'selection',
84
+ selectedText: 'Line 1\nLine 2\nLine 3',
85
+ instruction: 'Fix formatting',
86
+ notePath: 'doc.md',
87
+ };
88
+
89
+ const prompt = buildInlineEditPrompt(request);
90
+
91
+ expect(prompt).toContain('Line 1\nLine 2\nLine 3');
92
+ });
93
+
94
+ it('should handle empty selected text', () => {
95
+ const request: InlineEditRequest = {
96
+ mode: 'selection',
97
+ selectedText: '',
98
+ instruction: 'Add content',
99
+ notePath: 'empty.md',
100
+ };
101
+
102
+ const prompt = buildInlineEditPrompt(request);
103
+
104
+ // New format: instruction first, then XML context (no <query> wrapper)
105
+ expect(prompt).toContain('Add content');
106
+ expect(prompt).toContain('<editor_selection path="empty.md">');
107
+ // Verify instruction comes before selection
108
+ expect(prompt.indexOf('Add content')).toBeLessThan(prompt.indexOf('<editor_selection'));
109
+ });
110
+
111
+ it('should append context files when provided', () => {
112
+ const request: InlineEditRequest = {
113
+ mode: 'selection',
114
+ selectedText: 'test',
115
+ instruction: 'Fix this',
116
+ notePath: 'test.md',
117
+ contextFiles: ['notes/helper.md', 'docs/api.md'],
118
+ };
119
+
120
+ const prompt = buildInlineEditPrompt(request);
121
+
122
+ // Context files should be appended with <context_files> tag
123
+ expect(prompt).toContain('<context_files>');
124
+ expect(prompt).toContain('notes/helper.md');
125
+ expect(prompt).toContain('docs/api.md');
126
+ expect(prompt).toContain('</context_files>');
127
+ // Original content should still be present
128
+ expect(prompt).toContain('<editor_selection');
129
+ expect(prompt).toContain('Fix this');
130
+ // Verify order: instruction, selection, context_files
131
+ expect(prompt.indexOf('Fix this')).toBeLessThan(prompt.indexOf('<editor_selection'));
132
+ expect(prompt.indexOf('<editor_selection')).toBeLessThan(prompt.indexOf('<context_files'));
133
+ });
134
+
135
+ it('should not modify prompt when contextFiles is empty', () => {
136
+ const request: InlineEditRequest = {
137
+ mode: 'selection',
138
+ selectedText: 'test',
139
+ instruction: 'Fix this',
140
+ notePath: 'test.md',
141
+ contextFiles: [],
142
+ };
143
+
144
+ const prompt = buildInlineEditPrompt(request);
145
+
146
+ expect(prompt).not.toContain('<context_files>');
147
+ expect(prompt).toContain('<editor_selection');
148
+ });
149
+
150
+ it('should not modify prompt when contextFiles is undefined', () => {
151
+ const request: InlineEditRequest = {
152
+ mode: 'selection',
153
+ selectedText: 'test',
154
+ instruction: 'Fix this',
155
+ notePath: 'test.md',
156
+ };
157
+
158
+ const prompt = buildInlineEditPrompt(request);
159
+
160
+ expect(prompt).not.toContain('<context_files>');
161
+ expect(prompt).toContain('<editor_selection');
162
+ });
163
+
164
+ it('should prepend context files for cursor mode', () => {
165
+ const request: InlineEditRequest = {
166
+ mode: 'cursor',
167
+ instruction: 'insert here',
168
+ notePath: 'test.md',
169
+ cursorContext: {
170
+ beforeCursor: 'before',
171
+ afterCursor: 'after',
172
+ isInbetween: false,
173
+ line: 0,
174
+ column: 6,
175
+ },
176
+ contextFiles: ['utils.ts'],
177
+ };
178
+
179
+ const prompt = buildInlineEditPrompt(request);
180
+
181
+ expect(prompt).toContain('<context_files>');
182
+ expect(prompt).toContain('utils.ts');
183
+ expect(prompt).toContain('<editor_cursor');
184
+ });
185
+ });
186
+
187
+ describe('parseResponse', () => {
188
+ it('should extract text from replacement tags', () => {
189
+ const response = 'Here is the edit:\n<replacement>Fixed text here</replacement>';
190
+
191
+ const result = parseInlineEditResponse(response);
192
+
193
+ expect(result.success).toBe(true);
194
+ expect(result.editedText).toBe('Fixed text here');
195
+ });
196
+
197
+ it('should handle multiline replacement content', () => {
198
+ const response = '<replacement>Line 1\nLine 2\nLine 3</replacement>';
199
+
200
+ const result = parseInlineEditResponse(response);
201
+
202
+ expect(result.success).toBe(true);
203
+ expect(result.editedText).toBe('Line 1\nLine 2\nLine 3');
204
+ });
205
+
206
+ it('should return clarification when no replacement tags', () => {
207
+ const response = 'Could you please clarify what you mean by "fix"?';
208
+
209
+ const result = parseInlineEditResponse(response);
210
+
211
+ expect(result.success).toBe(true);
212
+ expect(result.clarification).toBe('Could you please clarify what you mean by "fix"?');
213
+ expect(result.editedText).toBeUndefined();
214
+ });
215
+
216
+ it('should return error for empty response', () => {
217
+ const result = parseInlineEditResponse('');
218
+
219
+ expect(result.success).toBe(false);
220
+ expect(result.error).toBe('Empty response');
221
+ });
222
+
223
+ it('should return error for whitespace-only response', () => {
224
+ const result = parseInlineEditResponse(' \n\t ');
225
+
226
+ expect(result.success).toBe(false);
227
+ expect(result.error).toBe('Empty response');
228
+ });
229
+
230
+ it('should handle replacement tags with special characters', () => {
231
+ const response = '<replacement>const x = a < b && c > d;</replacement>';
232
+
233
+ const result = parseInlineEditResponse(response);
234
+
235
+ expect(result.success).toBe(true);
236
+ expect(result.editedText).toBe('const x = a < b && c > d;');
237
+ });
238
+
239
+ it('should extract first replacement tag if multiple exist', () => {
240
+ const response = '<replacement>first</replacement> then <replacement>second</replacement>';
241
+
242
+ const result = parseInlineEditResponse(response);
243
+
244
+ expect(result.success).toBe(true);
245
+ expect(result.editedText).toBe('first');
246
+ });
247
+
248
+ it('should handle empty replacement tags', () => {
249
+ const response = '<replacement></replacement>';
250
+
251
+ const result = parseInlineEditResponse(response);
252
+
253
+ expect(result.success).toBe(true);
254
+ expect(result.editedText).toBe('');
255
+ });
256
+ });
257
+
258
+ describe('editText', () => {
259
+ beforeEach(() => {
260
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
261
+ });
262
+
263
+ it('should return error when vault path cannot be determined', async () => {
264
+ mockPlugin.app.vault.adapter.basePath = undefined;
265
+ service = new InlineEditService(mockPlugin);
266
+
267
+ const result = await service.editText({
268
+ mode: 'selection',
269
+ selectedText: 'test',
270
+ instruction: 'fix',
271
+ notePath: 'test.md',
272
+ });
273
+
274
+ expect(result.success).toBe(false);
275
+ expect(result.error).toContain('vault path');
276
+ });
277
+
278
+ it('should return error when claude CLI not found', async () => {
279
+ mockPlugin.getResolvedProviderCliPath.mockReturnValue(null);
280
+
281
+ const result = await service.editText({
282
+ mode: 'selection',
283
+ selectedText: 'test',
284
+ instruction: 'fix',
285
+ notePath: 'test.md',
286
+ });
287
+
288
+ expect(result.success).toBe(false);
289
+ expect(result.error).toContain('Claude CLI not found');
290
+ });
291
+
292
+ it('should use restricted read-only tools', async () => {
293
+ setMockMessages([
294
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
295
+ {
296
+ type: 'assistant',
297
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
298
+ },
299
+ { type: 'result' },
300
+ ]);
301
+
302
+ await service.editText({
303
+ mode: 'selection',
304
+ selectedText: 'test',
305
+ instruction: 'fix',
306
+ notePath: 'test.md',
307
+ });
308
+
309
+ const options = getLastOptions();
310
+ expect(options?.tools).toContain('Read');
311
+ expect(options?.tools).toContain('Grep');
312
+ expect(options?.tools).toContain('Glob');
313
+ expect(options?.tools).toContain('LS');
314
+ expect(options?.tools).toContain('WebSearch');
315
+ expect(options?.tools).toContain('WebFetch');
316
+ // Should NOT include write tools
317
+ expect(options?.tools).not.toContain('Write');
318
+ expect(options?.tools).not.toContain('Edit');
319
+ expect(options?.tools).not.toContain('Bash');
320
+ });
321
+
322
+ it('should bypass permissions for read-only tools', async () => {
323
+ setMockMessages([
324
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
325
+ {
326
+ type: 'assistant',
327
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
328
+ },
329
+ { type: 'result' },
330
+ ]);
331
+
332
+ await service.editText({
333
+ mode: 'selection',
334
+ selectedText: 'test',
335
+ instruction: 'fix',
336
+ notePath: 'test.md',
337
+ });
338
+
339
+ const options = getLastOptions();
340
+ expect(options?.permissionMode).toBe('bypassPermissions');
341
+ });
342
+
343
+ it('should set settingSources to project only when loadUserClaudeSettings is false', async () => {
344
+ mockPlugin.settings.loadUserClaudeSettings = false;
345
+ service = new InlineEditService(mockPlugin);
346
+
347
+ setMockMessages([
348
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
349
+ {
350
+ type: 'assistant',
351
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
352
+ },
353
+ { type: 'result' },
354
+ ]);
355
+
356
+ await service.editText({
357
+ mode: 'selection',
358
+ selectedText: 'test',
359
+ instruction: 'fix',
360
+ notePath: 'test.md',
361
+ });
362
+
363
+ const options = getLastOptions();
364
+ expect(options?.settingSources).toEqual(['project', 'local']);
365
+ });
366
+
367
+ it('should set settingSources to include user when loadUserClaudeSettings is true', async () => {
368
+ mockPlugin.settings.loadUserClaudeSettings = true;
369
+ service = new InlineEditService(mockPlugin);
370
+
371
+ setMockMessages([
372
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
373
+ {
374
+ type: 'assistant',
375
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
376
+ },
377
+ { type: 'result' },
378
+ ]);
379
+
380
+ await service.editText({
381
+ mode: 'selection',
382
+ selectedText: 'test',
383
+ instruction: 'fix',
384
+ notePath: 'test.md',
385
+ });
386
+
387
+ const options = getLastOptions();
388
+ expect(options?.settingSources).toEqual(['user', 'project', 'local']);
389
+ });
390
+
391
+ it('should set adaptive thinking for Claude models', async () => {
392
+ mockPlugin.settings.model = 'sonnet';
393
+ service = new InlineEditService(mockPlugin);
394
+
395
+ setMockMessages([
396
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
397
+ {
398
+ type: 'assistant',
399
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
400
+ },
401
+ { type: 'result' },
402
+ ]);
403
+
404
+ await service.editText({
405
+ mode: 'selection',
406
+ selectedText: 'test',
407
+ instruction: 'fix',
408
+ notePath: 'test.md',
409
+ });
410
+
411
+ const options = getLastOptions();
412
+ expect(options?.thinking).toEqual({ type: 'adaptive' });
413
+ expect(options?.maxThinkingTokens).toBeUndefined();
414
+ });
415
+
416
+ it('should set adaptive thinking with effort for custom models', async () => {
417
+ mockPlugin.settings.model = 'custom-model';
418
+ mockPlugin.settings.thinkingBudget = 'medium';
419
+ mockPlugin.settings.effortLevel = 'medium';
420
+ service = new InlineEditService(mockPlugin);
421
+
422
+ setMockMessages([
423
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
424
+ {
425
+ type: 'assistant',
426
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
427
+ },
428
+ { type: 'result' },
429
+ ]);
430
+
431
+ await service.editText({
432
+ mode: 'selection',
433
+ selectedText: 'test',
434
+ instruction: 'fix',
435
+ notePath: 'test.md',
436
+ });
437
+
438
+ const options = getLastOptions();
439
+ expect(options?.thinking).toEqual({ type: 'adaptive' });
440
+ expect(options?.effort).toBe('medium');
441
+ expect(options?.maxThinkingTokens).toBeUndefined();
442
+ });
443
+
444
+ it('should capture session ID for conversation continuity', async () => {
445
+ setMockMessages([
446
+ { type: 'system', subtype: 'init', session_id: 'inline-session-123' },
447
+ {
448
+ type: 'assistant',
449
+ message: { content: [{ type: 'text', text: 'What do you want to change?' }] },
450
+ },
451
+ { type: 'result' },
452
+ ]);
453
+
454
+ await service.editText({
455
+ mode: 'selection',
456
+ selectedText: 'test',
457
+ instruction: 'fix',
458
+ notePath: 'test.md',
459
+ });
460
+
461
+ // Verify session was captured by checking continueConversation resumes it
462
+ setMockMessages([
463
+ {
464
+ type: 'assistant',
465
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
466
+ },
467
+ { type: 'result' },
468
+ ]);
469
+
470
+ await service.continueConversation('make it better');
471
+
472
+ const options = getLastOptions();
473
+ expect(options?.resume).toBe('inline-session-123');
474
+ });
475
+
476
+ it('should return clarification response', async () => {
477
+ setMockMessages([
478
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
479
+ {
480
+ type: 'assistant',
481
+ message: { content: [{ type: 'text', text: 'Could you clarify what "fix" means?' }] },
482
+ },
483
+ { type: 'result' },
484
+ ]);
485
+
486
+ const result = await service.editText({
487
+ mode: 'selection',
488
+ selectedText: 'broken code',
489
+ instruction: 'fix',
490
+ notePath: 'test.md',
491
+ });
492
+
493
+ expect(result.success).toBe(true);
494
+ expect(result.clarification).toBe('Could you clarify what "fix" means?');
495
+ });
496
+ });
497
+
498
+ describe('continueConversation', () => {
499
+ beforeEach(() => {
500
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
501
+ });
502
+
503
+ it('should return error when no active conversation', async () => {
504
+ const result = await service.continueConversation('more details');
505
+
506
+ expect(result.success).toBe(false);
507
+ expect(result.error).toContain('No active conversation');
508
+ });
509
+
510
+ it('should resume session on follow-up', async () => {
511
+ // First message to establish session
512
+ setMockMessages([
513
+ { type: 'system', subtype: 'init', session_id: 'continue-session' },
514
+ {
515
+ type: 'assistant',
516
+ message: { content: [{ type: 'text', text: 'What do you want?' }] },
517
+ },
518
+ { type: 'result' },
519
+ ]);
520
+
521
+ await service.editText({
522
+ mode: 'selection',
523
+ selectedText: 'test',
524
+ instruction: 'fix',
525
+ notePath: 'test.md',
526
+ });
527
+
528
+ // Follow-up message
529
+ setMockMessages([
530
+ {
531
+ type: 'assistant',
532
+ message: { content: [{ type: 'text', text: '<replacement>final result</replacement>' }] },
533
+ },
534
+ { type: 'result' },
535
+ ]);
536
+
537
+ await service.continueConversation('make it blue');
538
+
539
+ const options = getLastOptions();
540
+ expect(options?.resume).toBe('continue-session');
541
+ });
542
+
543
+ it('should prepend context files when provided', async () => {
544
+ // First message to establish session
545
+ setMockMessages([
546
+ { type: 'system', subtype: 'init', session_id: 'context-session' },
547
+ {
548
+ type: 'assistant',
549
+ message: { content: [{ type: 'text', text: 'What do you want?' }] },
550
+ },
551
+ { type: 'result' },
552
+ ]);
553
+
554
+ await service.editText({
555
+ mode: 'selection',
556
+ selectedText: 'test',
557
+ instruction: 'fix',
558
+ notePath: 'test.md',
559
+ });
560
+
561
+ // Follow-up message with context files
562
+ setMockMessages([
563
+ {
564
+ type: 'assistant',
565
+ message: { content: [{ type: 'text', text: '<replacement>final result</replacement>' }] },
566
+ },
567
+ { type: 'result' },
568
+ ]);
569
+
570
+ await service.continueConversation('make it blue', ['notes/helper.md', 'docs/api.md']);
571
+
572
+ // The prompt should include the context files
573
+ // Since we can't directly access the prompt, we verify the session resumed
574
+ const options = getLastOptions();
575
+ expect(options?.resume).toBe('context-session');
576
+ });
577
+
578
+ it('should not modify prompt when no context files provided', async () => {
579
+ // First message to establish session
580
+ setMockMessages([
581
+ { type: 'system', subtype: 'init', session_id: 'no-context-session' },
582
+ {
583
+ type: 'assistant',
584
+ message: { content: [{ type: 'text', text: 'What do you want?' }] },
585
+ },
586
+ { type: 'result' },
587
+ ]);
588
+
589
+ await service.editText({
590
+ mode: 'selection',
591
+ selectedText: 'test',
592
+ instruction: 'fix',
593
+ notePath: 'test.md',
594
+ });
595
+
596
+ // Follow-up without context files
597
+ setMockMessages([
598
+ {
599
+ type: 'assistant',
600
+ message: { content: [{ type: 'text', text: '<replacement>result</replacement>' }] },
601
+ },
602
+ { type: 'result' },
603
+ ]);
604
+
605
+ await service.continueConversation('make it blue');
606
+
607
+ const options = getLastOptions();
608
+ expect(options?.resume).toBe('no-context-session');
609
+ });
610
+
611
+ it('should handle empty context files array', async () => {
612
+ // First message to establish session
613
+ setMockMessages([
614
+ { type: 'system', subtype: 'init', session_id: 'empty-context-session' },
615
+ {
616
+ type: 'assistant',
617
+ message: { content: [{ type: 'text', text: 'What do you want?' }] },
618
+ },
619
+ { type: 'result' },
620
+ ]);
621
+
622
+ await service.editText({
623
+ mode: 'selection',
624
+ selectedText: 'test',
625
+ instruction: 'fix',
626
+ notePath: 'test.md',
627
+ });
628
+
629
+ // Follow-up with empty context files array
630
+ setMockMessages([
631
+ {
632
+ type: 'assistant',
633
+ message: { content: [{ type: 'text', text: '<replacement>result</replacement>' }] },
634
+ },
635
+ { type: 'result' },
636
+ ]);
637
+
638
+ await service.continueConversation('make it blue', []);
639
+
640
+ const options = getLastOptions();
641
+ expect(options?.resume).toBe('empty-context-session');
642
+ });
643
+ });
644
+
645
+ describe('resetConversation', () => {
646
+ it('should clear session so continueConversation fails', async () => {
647
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
648
+
649
+ // First establish a session
650
+ setMockMessages([
651
+ { type: 'system', subtype: 'init', session_id: 'some-session' },
652
+ {
653
+ type: 'assistant',
654
+ message: { content: [{ type: 'text', text: 'What do you want?' }] },
655
+ },
656
+ { type: 'result' },
657
+ ]);
658
+
659
+ await service.editText({
660
+ mode: 'selection',
661
+ selectedText: 'test',
662
+ instruction: 'fix',
663
+ notePath: 'test.md',
664
+ });
665
+
666
+ // Reset should clear the session
667
+ service.resetConversation();
668
+
669
+ // continueConversation should fail since session is cleared
670
+ const result = await service.continueConversation('more details');
671
+ expect(result.success).toBe(false);
672
+ expect(result.error).toContain('No active conversation');
673
+ });
674
+ });
675
+
676
+ describe('cancel', () => {
677
+ it('should abort ongoing request', async () => {
678
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
679
+
680
+ setMockMessages([
681
+ { type: 'system', subtype: 'init', session_id: 'test-session' },
682
+ {
683
+ type: 'assistant',
684
+ message: { content: [{ type: 'text', text: '<replacement>fixed</replacement>' }] },
685
+ },
686
+ ]);
687
+
688
+ const editPromise = service.editText({
689
+ mode: 'selection',
690
+ selectedText: 'test',
691
+ instruction: 'fix',
692
+ notePath: 'test.md',
693
+ });
694
+
695
+ // Cancel immediately
696
+ service.cancel();
697
+
698
+ const result = await editPromise;
699
+ expect(result.success).toBe(false);
700
+ expect(result.error).toBe('Cancelled');
701
+ });
702
+
703
+ it('should handle cancel when no request is running', () => {
704
+ expect(() => service.cancel()).not.toThrow();
705
+ });
706
+ });
707
+
708
+ describe('read-only hook enforcement', () => {
709
+ it('should create hook that allows read-only tools', () => {
710
+ const hook = createReadOnlyHook();
711
+
712
+ expect(hook.hooks).toHaveLength(1);
713
+ });
714
+
715
+ it('should allow Read tool through hook', async () => {
716
+ const hook = createReadOnlyHook();
717
+ const result = await callHook(hook.hooks[0], { tool_name: 'Read', tool_input: { file_path: 'test.md' } });
718
+
719
+ expect(result.continue).toBe(true);
720
+ });
721
+
722
+ it('should allow Grep tool through hook', async () => {
723
+ const hook = createReadOnlyHook();
724
+ const result = await callHook(hook.hooks[0], { tool_name: 'Grep', tool_input: { pattern: 'test' } });
725
+
726
+ expect(result.continue).toBe(true);
727
+ });
728
+
729
+ it('should allow WebSearch tool through hook', async () => {
730
+ const hook = createReadOnlyHook();
731
+ const result = await callHook(hook.hooks[0], { tool_name: 'WebSearch', tool_input: { query: 'test' } });
732
+
733
+ expect(result.continue).toBe(true);
734
+ });
735
+
736
+ it('should block Write tool through hook', async () => {
737
+ const hook = createReadOnlyHook();
738
+ const result = await callHook(hook.hooks[0], { tool_name: 'Write', tool_input: { file_path: 'test.md' } });
739
+
740
+ expect(result.continue).toBe(false);
741
+ expect(result.hookSpecificOutput.permissionDecision).toBe('deny');
742
+ expect(result.hookSpecificOutput.permissionDecisionReason).toContain('not allowed');
743
+ });
744
+
745
+ it('should block Bash tool through hook', async () => {
746
+ const hook = createReadOnlyHook();
747
+ const result = await callHook(hook.hooks[0], { tool_name: 'Bash', tool_input: { command: 'rm -rf /' } });
748
+
749
+ expect(result.continue).toBe(false);
750
+ expect(result.hookSpecificOutput.permissionDecision).toBe('deny');
751
+ });
752
+
753
+ it('should block Edit tool through hook', async () => {
754
+ const hook = createReadOnlyHook();
755
+ const result = await callHook(hook.hooks[0], { tool_name: 'Edit', tool_input: { file_path: 'test.md' } });
756
+
757
+ expect(result.continue).toBe(false);
758
+ });
759
+ });
760
+
761
+ describe('error handling', () => {
762
+ beforeEach(() => {
763
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
764
+ });
765
+
766
+ it('should surface SDK query errors', async () => {
767
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
768
+ const sdk = require('@anthropic-ai/claude-agent-sdk');
769
+ const spy = jest.spyOn(sdk, 'query').mockImplementation(() => {
770
+ throw new Error('boom');
771
+ });
772
+
773
+ try {
774
+ const result = await service.editText({
775
+ mode: 'selection',
776
+ selectedText: 'text',
777
+ instruction: 'edit',
778
+ notePath: 'note.md',
779
+ });
780
+
781
+ expect(result.success).toBe(false);
782
+ expect(result.error).toBe('boom');
783
+ } finally {
784
+ spy.mockRestore();
785
+ }
786
+ });
787
+
788
+ it('returns null path for unknown tool input', () => {
789
+ expect(getPathFromToolInput('Unknown', {})).toBeNull();
790
+ });
791
+
792
+ it('extracts LS path from tool input', () => {
793
+ expect(getPathFromToolInput('LS', { path: 'notes' })).toBe('notes');
794
+ });
795
+ });
796
+
797
+ describe('buildCursorContext', () => {
798
+ // Helper to create mock getLine function from array of lines
799
+ const createGetLine = (lines: string[]) => (line: number) => lines[line] ?? '';
800
+
801
+ describe('inline mode detection', () => {
802
+ it('should detect inline when cursor is in middle of text', () => {
803
+ const lines = ['Hello world'];
804
+ const ctx = buildCursorContext(createGetLine(lines), 1, 0, 6);
805
+
806
+ expect(ctx.isInbetween).toBe(false);
807
+ expect(ctx.beforeCursor).toBe('Hello ');
808
+ expect(ctx.afterCursor).toBe('world');
809
+ expect(ctx.line).toBe(0);
810
+ expect(ctx.column).toBe(6);
811
+ });
812
+
813
+ it('should detect inline when cursor is at line start with text after', () => {
814
+ const lines = ['Hello world'];
815
+ const ctx = buildCursorContext(createGetLine(lines), 1, 0, 0);
816
+
817
+ expect(ctx.isInbetween).toBe(false);
818
+ expect(ctx.beforeCursor).toBe('');
819
+ expect(ctx.afterCursor).toBe('Hello world');
820
+ });
821
+
822
+ it('should detect inline when cursor is at line end with text before', () => {
823
+ const lines = ['Hello world'];
824
+ const ctx = buildCursorContext(createGetLine(lines), 1, 0, 11);
825
+
826
+ expect(ctx.isInbetween).toBe(false);
827
+ expect(ctx.beforeCursor).toBe('Hello world');
828
+ expect(ctx.afterCursor).toBe('');
829
+ });
830
+
831
+ it('should preserve whitespace around cursor', () => {
832
+ const lines = [' hello world '];
833
+ const ctx = buildCursorContext(createGetLine(lines), 1, 0, 9);
834
+
835
+ expect(ctx.isInbetween).toBe(false);
836
+ expect(ctx.beforeCursor).toBe(' hello ');
837
+ expect(ctx.afterCursor).toBe(' world ');
838
+ });
839
+ });
840
+
841
+ describe('inbetween mode detection', () => {
842
+ it('should detect inbetween on empty line', () => {
843
+ const lines = ['First paragraph', '', 'Second paragraph'];
844
+ const ctx = buildCursorContext(createGetLine(lines), 3, 1, 0);
845
+
846
+ expect(ctx.isInbetween).toBe(true);
847
+ expect(ctx.beforeCursor).toBe('First paragraph');
848
+ expect(ctx.afterCursor).toBe('Second paragraph');
849
+ });
850
+
851
+ it('should detect inbetween on whitespace-only line', () => {
852
+ const lines = ['First paragraph', ' ', 'Second paragraph'];
853
+ const ctx = buildCursorContext(createGetLine(lines), 3, 1, 2);
854
+
855
+ expect(ctx.isInbetween).toBe(true);
856
+ expect(ctx.beforeCursor).toBe('First paragraph');
857
+ expect(ctx.afterCursor).toBe('Second paragraph');
858
+ });
859
+
860
+ it('should detect inbetween when cursor on line with only whitespace before and after', () => {
861
+ const lines = ['Content', ' \t ', 'More content'];
862
+ const ctx = buildCursorContext(createGetLine(lines), 3, 1, 2);
863
+
864
+ expect(ctx.isInbetween).toBe(true);
865
+ });
866
+
867
+ it('should find nearest non-empty line before cursor', () => {
868
+ const lines = ['Content', '', '', '', 'More'];
869
+ const ctx = buildCursorContext(createGetLine(lines), 5, 2, 0);
870
+
871
+ expect(ctx.isInbetween).toBe(true);
872
+ expect(ctx.beforeCursor).toBe('Content');
873
+ });
874
+
875
+ it('should find nearest non-empty line after cursor', () => {
876
+ const lines = ['Content', '', '', '', 'More'];
877
+ const ctx = buildCursorContext(createGetLine(lines), 5, 2, 0);
878
+
879
+ expect(ctx.isInbetween).toBe(true);
880
+ expect(ctx.afterCursor).toBe('More');
881
+ });
882
+
883
+ it('should handle cursor at document start (empty first line)', () => {
884
+ const lines = ['', 'First content'];
885
+ const ctx = buildCursorContext(createGetLine(lines), 2, 0, 0);
886
+
887
+ expect(ctx.isInbetween).toBe(true);
888
+ expect(ctx.beforeCursor).toBe('');
889
+ expect(ctx.afterCursor).toBe('First content');
890
+ });
891
+
892
+ it('should handle cursor at document end (empty last line)', () => {
893
+ const lines = ['Last content', ''];
894
+ const ctx = buildCursorContext(createGetLine(lines), 2, 1, 0);
895
+
896
+ expect(ctx.isInbetween).toBe(true);
897
+ expect(ctx.beforeCursor).toBe('Last content');
898
+ expect(ctx.afterCursor).toBe('');
899
+ });
900
+
901
+ it('should handle multiple consecutive empty lines', () => {
902
+ const lines = ['Para A', '', '', '', 'Para B'];
903
+ const ctx = buildCursorContext(createGetLine(lines), 5, 2, 0);
904
+
905
+ expect(ctx.isInbetween).toBe(true);
906
+ expect(ctx.beforeCursor).toBe('Para A');
907
+ expect(ctx.afterCursor).toBe('Para B');
908
+ });
909
+ });
910
+
911
+ describe('edge cases', () => {
912
+ it('should handle single line document with cursor', () => {
913
+ const lines = ['Only line'];
914
+ const ctx = buildCursorContext(createGetLine(lines), 1, 0, 5);
915
+
916
+ expect(ctx.isInbetween).toBe(false);
917
+ expect(ctx.beforeCursor).toBe('Only ');
918
+ expect(ctx.afterCursor).toBe('line');
919
+ });
920
+
921
+ it('should handle empty document', () => {
922
+ const lines = [''];
923
+ const ctx = buildCursorContext(createGetLine(lines), 1, 0, 0);
924
+
925
+ expect(ctx.isInbetween).toBe(true);
926
+ expect(ctx.beforeCursor).toBe('');
927
+ expect(ctx.afterCursor).toBe('');
928
+ });
929
+
930
+ it('should preserve line and column in context', () => {
931
+ const lines = ['Line 0', 'Line 1', 'Line 2'];
932
+ const ctx = buildCursorContext(createGetLine(lines), 3, 1, 3);
933
+
934
+ expect(ctx.line).toBe(1);
935
+ expect(ctx.column).toBe(3);
936
+ });
937
+ });
938
+ });
939
+
940
+ describe('buildCursorPrompt', () => {
941
+ it('should build inline cursor prompt correctly', () => {
942
+ const request: InlineEditRequest = {
943
+ mode: 'cursor',
944
+ instruction: 'add missing word',
945
+ notePath: 'notes/test.md',
946
+ cursorContext: {
947
+ beforeCursor: 'The quick brown ',
948
+ afterCursor: ' jumps over',
949
+ isInbetween: false,
950
+ line: 5,
951
+ column: 16,
952
+ },
953
+ };
954
+
955
+ const prompt = buildInlineEditPrompt(request);
956
+
957
+ // New format: instruction first, then XML context (no <query> wrapper)
958
+ expect(prompt).toContain('add missing word');
959
+ expect(prompt).toContain('<editor_cursor path="notes/test.md" line="6">');
960
+ expect(prompt).toContain('The quick brown | jumps over #inline');
961
+ expect(prompt).toContain('</editor_cursor>');
962
+ // Verify instruction comes before cursor context
963
+ expect(prompt.indexOf('add missing word')).toBeLessThan(prompt.indexOf('<editor_cursor'));
964
+ });
965
+
966
+ it('should build inbetween cursor prompt with surrounding context', () => {
967
+ const request: InlineEditRequest = {
968
+ mode: 'cursor',
969
+ instruction: 'add a new section',
970
+ notePath: 'docs/readme.md',
971
+ cursorContext: {
972
+ beforeCursor: '# Introduction',
973
+ afterCursor: '## Features',
974
+ isInbetween: true,
975
+ line: 3,
976
+ column: 0,
977
+ },
978
+ };
979
+
980
+ const prompt = buildInlineEditPrompt(request);
981
+
982
+ // New format: instruction first, then XML context (no <query> wrapper)
983
+ expect(prompt).toContain('add a new section');
984
+ expect(prompt).toContain('<editor_cursor path="docs/readme.md" line="4">');
985
+ expect(prompt).toContain('# Introduction');
986
+ expect(prompt).toContain('| #inbetween');
987
+ expect(prompt).toContain('## Features');
988
+ expect(prompt).toContain('</editor_cursor>');
989
+ // Verify instruction comes before cursor context
990
+ expect(prompt.indexOf('add a new section')).toBeLessThan(prompt.indexOf('<editor_cursor'));
991
+ });
992
+
993
+ it('should handle inbetween with no content before cursor', () => {
994
+ const request: InlineEditRequest = {
995
+ mode: 'cursor',
996
+ instruction: 'add header',
997
+ notePath: 'empty.md',
998
+ cursorContext: {
999
+ beforeCursor: '',
1000
+ afterCursor: 'First paragraph',
1001
+ isInbetween: true,
1002
+ line: 0,
1003
+ column: 0,
1004
+ },
1005
+ };
1006
+
1007
+ const prompt = buildInlineEditPrompt(request);
1008
+
1009
+ expect(prompt).toContain('| #inbetween');
1010
+ expect(prompt).toContain('First paragraph');
1011
+ expect(prompt).not.toMatch(/\n\n\| #inbetween/); // No double newline before marker
1012
+ });
1013
+
1014
+ it('should handle inbetween with no content after cursor', () => {
1015
+ const request: InlineEditRequest = {
1016
+ mode: 'cursor',
1017
+ instruction: 'add footer',
1018
+ notePath: 'doc.md',
1019
+ cursorContext: {
1020
+ beforeCursor: 'Last paragraph',
1021
+ afterCursor: '',
1022
+ isInbetween: true,
1023
+ line: 10,
1024
+ column: 0,
1025
+ },
1026
+ };
1027
+
1028
+ const prompt = buildInlineEditPrompt(request);
1029
+
1030
+ expect(prompt).toContain('Last paragraph');
1031
+ expect(prompt).toContain('| #inbetween');
1032
+ });
1033
+ });
1034
+
1035
+ describe('buildPrompt mode dispatch', () => {
1036
+ it('should dispatch to selection prompt for selection mode', () => {
1037
+ const request: InlineEditRequest = {
1038
+ mode: 'selection',
1039
+ instruction: 'fix this',
1040
+ notePath: 'test.md',
1041
+ selectedText: 'selected text here',
1042
+ };
1043
+
1044
+ const prompt = buildInlineEditPrompt(request);
1045
+
1046
+ expect(prompt).toContain('selected text here');
1047
+ expect(prompt).not.toContain('#inline');
1048
+ expect(prompt).not.toContain('#inbetween');
1049
+ });
1050
+
1051
+ it('should dispatch to cursor prompt for cursor mode', () => {
1052
+ const request: InlineEditRequest = {
1053
+ mode: 'cursor',
1054
+ instruction: 'insert here',
1055
+ notePath: 'test.md',
1056
+ cursorContext: {
1057
+ beforeCursor: 'before',
1058
+ afterCursor: 'after',
1059
+ isInbetween: false,
1060
+ line: 0,
1061
+ column: 6,
1062
+ },
1063
+ };
1064
+
1065
+ const prompt = buildInlineEditPrompt(request);
1066
+
1067
+ expect(prompt).toContain('before|after #inline');
1068
+ });
1069
+ });
1070
+
1071
+ describe('parseResponse with insertion tags', () => {
1072
+ it('should extract text from insertion tags', () => {
1073
+ const response = 'Here is the content:\n<insertion>inserted text here</insertion>';
1074
+
1075
+ const result = parseInlineEditResponse(response);
1076
+
1077
+ expect(result.success).toBe(true);
1078
+ expect(result.insertedText).toBe('inserted text here');
1079
+ expect(result.editedText).toBeUndefined();
1080
+ });
1081
+
1082
+ it('should handle multiline insertion content', () => {
1083
+ const response = '<insertion>Line 1\nLine 2\nLine 3</insertion>';
1084
+
1085
+ const result = parseInlineEditResponse(response);
1086
+
1087
+ expect(result.success).toBe(true);
1088
+ expect(result.insertedText).toBe('Line 1\nLine 2\nLine 3');
1089
+ });
1090
+
1091
+ it('should prefer replacement tags over insertion tags', () => {
1092
+ const response = '<replacement>replaced</replacement><insertion>inserted</insertion>';
1093
+
1094
+ const result = parseInlineEditResponse(response);
1095
+
1096
+ expect(result.success).toBe(true);
1097
+ expect(result.editedText).toBe('replaced');
1098
+ expect(result.insertedText).toBeUndefined();
1099
+ });
1100
+
1101
+ it('should handle insertion tags with leading/trailing newlines', () => {
1102
+ const response = '<insertion>\n## New Section\n\nContent here\n</insertion>';
1103
+
1104
+ const result = parseInlineEditResponse(response);
1105
+
1106
+ expect(result.success).toBe(true);
1107
+ expect(result.insertedText).toBe('\n## New Section\n\nContent here\n');
1108
+ });
1109
+
1110
+ it('should handle empty insertion tags', () => {
1111
+ const response = '<insertion></insertion>';
1112
+
1113
+ const result = parseInlineEditResponse(response);
1114
+
1115
+ expect(result.success).toBe(true);
1116
+ expect(result.insertedText).toBe('');
1117
+ });
1118
+
1119
+ it('should handle insertion with special characters', () => {
1120
+ const response = '<insertion>const x = a < b && c > d;</insertion>';
1121
+
1122
+ const result = parseInlineEditResponse(response);
1123
+
1124
+ expect(result.success).toBe(true);
1125
+ expect(result.insertedText).toBe('const x = a < b && c > d;');
1126
+ });
1127
+
1128
+ it('should return clarification when no tags present', () => {
1129
+ const response = 'What would you like me to insert?';
1130
+
1131
+ const result = parseInlineEditResponse(response);
1132
+
1133
+ expect(result.success).toBe(true);
1134
+ expect(result.clarification).toBe('What would you like me to insert?');
1135
+ expect(result.insertedText).toBeUndefined();
1136
+ expect(result.editedText).toBeUndefined();
1137
+ });
1138
+ });
1139
+
1140
+ describe('editText with cursor mode', () => {
1141
+ beforeEach(() => {
1142
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
1143
+ });
1144
+
1145
+ it('should handle cursor mode request', async () => {
1146
+ setMockMessages([
1147
+ { type: 'system', subtype: 'init', session_id: 'cursor-session' },
1148
+ {
1149
+ type: 'assistant',
1150
+ message: { content: [{ type: 'text', text: '<insertion>fox</insertion>' }] },
1151
+ },
1152
+ { type: 'result' },
1153
+ ]);
1154
+
1155
+ const result = await service.editText({
1156
+ mode: 'cursor',
1157
+ instruction: 'what animal?',
1158
+ notePath: 'test.md',
1159
+ cursorContext: {
1160
+ beforeCursor: 'The quick brown ',
1161
+ afterCursor: ' jumps over',
1162
+ isInbetween: false,
1163
+ line: 0,
1164
+ column: 16,
1165
+ },
1166
+ });
1167
+
1168
+ expect(result.success).toBe(true);
1169
+ expect(result.insertedText).toBe('fox');
1170
+ });
1171
+
1172
+ it('should handle inbetween mode request', async () => {
1173
+ setMockMessages([
1174
+ { type: 'system', subtype: 'init', session_id: 'inbetween-session' },
1175
+ {
1176
+ type: 'assistant',
1177
+ message: { content: [{ type: 'text', text: '<insertion>## Description\n\nNew section content</insertion>' }] },
1178
+ },
1179
+ { type: 'result' },
1180
+ ]);
1181
+
1182
+ const result = await service.editText({
1183
+ mode: 'cursor',
1184
+ instruction: 'add description section',
1185
+ notePath: 'readme.md',
1186
+ cursorContext: {
1187
+ beforeCursor: '# Title',
1188
+ afterCursor: '## Features',
1189
+ isInbetween: true,
1190
+ line: 2,
1191
+ column: 0,
1192
+ },
1193
+ });
1194
+
1195
+ expect(result.success).toBe(true);
1196
+ expect(result.insertedText).toContain('## Description');
1197
+ });
1198
+ });
1199
+ });