@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,2202 @@
1
+ import * as path from 'path';
2
+
3
+ import {
4
+ deriveCodexMemoriesDirFromSessionsRoot,
5
+ deriveCodexSessionsRootFromSessionPath,
6
+ parseCodexSessionContent,
7
+ parseCodexSessionFile,
8
+ parseCodexSessionTurns,
9
+ } from '@/providers/codex/history/CodexHistoryStore';
10
+
11
+ const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures');
12
+
13
+ describe('CodexHistoryStore', () => {
14
+ describe('path helpers', () => {
15
+ it('derives transcript and memories roots from POSIX session paths', () => {
16
+ const sessionFilePath = '/home/user/.codex/sessions/2026/04/14/rollout-thread.jsonl';
17
+
18
+ expect(deriveCodexSessionsRootFromSessionPath(sessionFilePath)).toBe('/home/user/.codex/sessions');
19
+ expect(deriveCodexMemoriesDirFromSessionsRoot('/home/user/.codex/sessions')).toBe(
20
+ '/home/user/.codex/memories',
21
+ );
22
+ });
23
+
24
+ it('derives transcript and memories roots from WSL UNC session paths', () => {
25
+ const sessionFilePath = '\\\\wsl$\\Ubuntu\\home\\user\\.codex\\sessions\\2026\\04\\14\\rollout-thread.jsonl';
26
+
27
+ expect(deriveCodexSessionsRootFromSessionPath(sessionFilePath)).toBe(
28
+ '\\\\wsl$\\Ubuntu\\home\\user\\.codex\\sessions',
29
+ );
30
+ expect(deriveCodexMemoriesDirFromSessionsRoot('\\\\wsl$\\Ubuntu\\home\\user\\.codex\\sessions')).toBe(
31
+ '\\\\wsl$\\Ubuntu\\home\\user\\.codex\\memories',
32
+ );
33
+ });
34
+ });
35
+
36
+ describe('parseCodexSessionFile - simple session', () => {
37
+ it('should parse a simple session with reasoning and agent message', () => {
38
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-simple.jsonl');
39
+ const messages = parseCodexSessionFile(filePath);
40
+
41
+ expect(messages).toHaveLength(1);
42
+ expect(messages[0].role).toBe('assistant');
43
+ expect(messages[0].content).toBe('Hello! I can help you with that.');
44
+
45
+ // Should have thinking content block
46
+ const thinkingBlock = messages[0].contentBlocks?.find(b => b.type === 'thinking');
47
+ expect(thinkingBlock).toBeDefined();
48
+ expect(thinkingBlock).toMatchObject({
49
+ type: 'thinking',
50
+ content: 'Let me think about this request carefully.',
51
+ });
52
+
53
+ // Should have text content block
54
+ const textBlock = messages[0].contentBlocks?.find(b => b.type === 'text');
55
+ expect(textBlock).toBeDefined();
56
+ });
57
+
58
+ it('should rebuild thinking text from persisted reasoning content blocks', () => {
59
+ const content = [
60
+ JSON.stringify({
61
+ timestamp: '2026-03-29T00:00:00.000Z',
62
+ type: 'response_item',
63
+ payload: {
64
+ type: 'message',
65
+ role: 'user',
66
+ content: [{ type: 'input_text', text: 'Explain this.' }],
67
+ },
68
+ }),
69
+ JSON.stringify({
70
+ timestamp: '2026-03-29T00:00:00.000Z',
71
+ type: 'response_item',
72
+ payload: {
73
+ type: 'reasoning',
74
+ summary: [],
75
+ content: ['First thought', ' second thought'],
76
+ },
77
+ }),
78
+ JSON.stringify({
79
+ timestamp: '2026-03-29T00:00:00.002Z',
80
+ type: 'response_item',
81
+ payload: {
82
+ type: 'message',
83
+ role: 'assistant',
84
+ content: [{ type: 'output_text', text: 'Done.' }],
85
+ },
86
+ }),
87
+ ].join('\n');
88
+
89
+ const messages = parseCodexSessionContent(content);
90
+
91
+ expect(messages).toHaveLength(2);
92
+ expect(messages[1].contentBlocks).toEqual([
93
+ { type: 'thinking', content: 'First thought\n\nsecond thought' },
94
+ { type: 'text', content: 'Done.' },
95
+ ]);
96
+ });
97
+ });
98
+
99
+ describe('parseCodexSessionFile - tools session', () => {
100
+ it('should parse a session with command execution and file changes', () => {
101
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-tools.jsonl');
102
+ const messages = parseCodexSessionFile(filePath);
103
+
104
+ expect(messages).toHaveLength(1);
105
+
106
+ const msg = messages[0];
107
+ expect(msg.toolCalls).toBeDefined();
108
+ expect(msg.toolCalls!.length).toBeGreaterThanOrEqual(2);
109
+
110
+ // Check command execution
111
+ const bashTool = msg.toolCalls!.find(tc => tc.name === 'Bash');
112
+ expect(bashTool).toBeDefined();
113
+ expect(bashTool!.input.command).toBe('cat src/main.ts');
114
+ expect(bashTool!.status).toBe('completed');
115
+
116
+ // Check file change
117
+ const patchTool = msg.toolCalls!.find(tc => tc.name === 'apply_patch');
118
+ expect(patchTool).toBeDefined();
119
+ expect(patchTool!.status).toBe('completed');
120
+ });
121
+
122
+ it('should preserve content blocks order', () => {
123
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-tools.jsonl');
124
+ const messages = parseCodexSessionFile(filePath);
125
+
126
+ const blocks = messages[0].contentBlocks;
127
+ expect(blocks).toBeDefined();
128
+ expect(blocks!.length).toBeGreaterThanOrEqual(3);
129
+
130
+ // First block should be text (from initial agent message)
131
+ expect(blocks![0].type).toBe('text');
132
+ // Then tool_use blocks
133
+ const toolBlocks = blocks!.filter(b => b.type === 'tool_use');
134
+ expect(toolBlocks.length).toBeGreaterThanOrEqual(2);
135
+ });
136
+ });
137
+
138
+ describe('parseCodexSessionFile - abort session', () => {
139
+ it('should handle turn.failed and mark as interrupted', () => {
140
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-abort.jsonl');
141
+ const messages = parseCodexSessionFile(filePath);
142
+
143
+ // Should have two messages: one interrupted, one successful
144
+ expect(messages).toHaveLength(2);
145
+ expect(messages[0].isInterrupt).toBe(true);
146
+ expect(messages[1].isInterrupt).toBeUndefined();
147
+ expect(messages[1].content).toBe('OK, what would you like me to do instead?');
148
+ });
149
+
150
+ it('keeps the latest streamed content for interrupted turns', () => {
151
+ const content = [
152
+ JSON.stringify({ type: 'event', event: { type: 'turn.started' } }),
153
+ JSON.stringify({ type: 'event', event: { type: 'item.started', item: { id: 'item_1', type: 'agent_message', text: '' } } }),
154
+ JSON.stringify({ type: 'event', event: { type: 'item.updated', item: { id: 'item_1', type: 'agent_message', text: 'Hello' } } }),
155
+ JSON.stringify({ type: 'event', event: { type: 'item.updated', item: { id: 'item_1', type: 'agent_message', text: 'Hello world' } } }),
156
+ JSON.stringify({ type: 'event', event: { type: 'turn.failed', error: { message: 'Cancelled' } } }),
157
+ ].join('\n');
158
+
159
+ const messages = parseCodexSessionContent(content);
160
+
161
+ expect(messages).toHaveLength(1);
162
+ expect(messages[0]).toMatchObject({
163
+ role: 'assistant',
164
+ content: 'Hello world',
165
+ isInterrupt: true,
166
+ });
167
+ });
168
+ });
169
+
170
+ describe('parseCodexSessionFile - web search session', () => {
171
+ it('should parse web search items', () => {
172
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-websearch.jsonl');
173
+ const messages = parseCodexSessionFile(filePath);
174
+
175
+ expect(messages).toHaveLength(1);
176
+
177
+ const msg = messages[0];
178
+ expect(msg.toolCalls).toBeDefined();
179
+
180
+ const searchTool = msg.toolCalls!.find(tc => tc.name === 'WebSearch');
181
+ expect(searchTool).toBeDefined();
182
+ expect(searchTool!.input.query).toBe('obsidian plugin API documentation');
183
+ expect(searchTool!.status).toBe('completed');
184
+ });
185
+ });
186
+
187
+ describe('parseCodexSessionFile - non-existent file', () => {
188
+ it('should return empty array for missing files', () => {
189
+ const messages = parseCodexSessionFile('/nonexistent/path.jsonl');
190
+ expect(messages).toEqual([]);
191
+ });
192
+ });
193
+
194
+ describe('parseCodexSessionContent - persisted response items', () => {
195
+ it('reconstructs user and assistant turns from response_item logs', () => {
196
+ const content = [
197
+ JSON.stringify({
198
+ timestamp: '2026-03-27T00:00:00.000Z',
199
+ type: 'response_item',
200
+ payload: {
201
+ type: 'message',
202
+ role: 'user',
203
+ content: [{ type: 'input_text', text: 'Review this diff.' }],
204
+ },
205
+ }),
206
+ JSON.stringify({
207
+ timestamp: '2026-03-27T00:00:01.000Z',
208
+ type: 'event_msg',
209
+ payload: {
210
+ type: 'agent_reasoning',
211
+ text: 'Thinking through the changes.',
212
+ },
213
+ }),
214
+ JSON.stringify({
215
+ timestamp: '2026-03-27T00:00:02.000Z',
216
+ type: 'response_item',
217
+ payload: {
218
+ type: 'function_call',
219
+ name: 'shell_command',
220
+ arguments: '{"command":"git diff --stat"}',
221
+ call_id: 'call_1',
222
+ },
223
+ }),
224
+ JSON.stringify({
225
+ timestamp: '2026-03-27T00:00:03.000Z',
226
+ type: 'response_item',
227
+ payload: {
228
+ type: 'function_call_output',
229
+ call_id: 'call_1',
230
+ output: 'Exit code: 0\nOutput:\n src/main.ts | 2 +-',
231
+ },
232
+ }),
233
+ JSON.stringify({
234
+ timestamp: '2026-03-27T00:00:04.000Z',
235
+ type: 'response_item',
236
+ payload: {
237
+ type: 'message',
238
+ role: 'assistant',
239
+ content: [{ type: 'output_text', text: 'The diff looks good.' }],
240
+ },
241
+ }),
242
+ ].join('\n');
243
+
244
+ const messages = parseCodexSessionContent(content);
245
+
246
+ expect(messages).toHaveLength(2);
247
+ expect(messages[0]).toMatchObject({
248
+ role: 'user',
249
+ content: 'Review this diff.',
250
+ });
251
+ expect(messages[1]).toMatchObject({
252
+ role: 'assistant',
253
+ content: 'The diff looks good.',
254
+ });
255
+
256
+ expect(messages[1].toolCalls).toEqual([
257
+ expect.objectContaining({
258
+ id: 'call_1',
259
+ name: 'Bash',
260
+ input: { command: 'git diff --stat' },
261
+ status: 'completed',
262
+ }),
263
+ ]);
264
+
265
+ // Result should be normalized (Output:\n stripped)
266
+ expect(messages[1].toolCalls![0].result).toBe(' src/main.ts | 2 +-');
267
+
268
+ expect(messages[1].contentBlocks).toEqual([
269
+ { type: 'thinking', content: 'Thinking through the changes.' },
270
+ { type: 'tool_use', toolId: 'call_1' },
271
+ { type: 'text', content: 'The diff looks good.' },
272
+ ]);
273
+ });
274
+
275
+ it('deduplicates the same user message when both response_item and event_msg are persisted', () => {
276
+ const content = [
277
+ JSON.stringify({
278
+ timestamp: '2026-03-27T00:00:00.000Z',
279
+ type: 'response_item',
280
+ payload: {
281
+ type: 'message',
282
+ role: 'user',
283
+ content: [{ type: 'input_text', text: 'hi' }],
284
+ },
285
+ }),
286
+ JSON.stringify({
287
+ timestamp: '2026-03-27T00:00:00.001Z',
288
+ type: 'event_msg',
289
+ payload: {
290
+ type: 'user_message',
291
+ message: 'hi',
292
+ },
293
+ }),
294
+ JSON.stringify({
295
+ timestamp: '2026-03-27T00:00:01.000Z',
296
+ type: 'event_msg',
297
+ payload: {
298
+ type: 'agent_message',
299
+ message: 'Hello there.',
300
+ },
301
+ }),
302
+ JSON.stringify({
303
+ timestamp: '2026-03-27T00:00:01.001Z',
304
+ type: 'response_item',
305
+ payload: {
306
+ type: 'message',
307
+ role: 'assistant',
308
+ content: [{ type: 'output_text', text: 'Hello there.' }],
309
+ },
310
+ }),
311
+ ].join('\n');
312
+
313
+ const messages = parseCodexSessionContent(content);
314
+
315
+ expect(messages).toEqual([
316
+ expect.objectContaining({
317
+ role: 'user',
318
+ content: 'hi',
319
+ }),
320
+ expect.objectContaining({
321
+ role: 'assistant',
322
+ content: 'Hello there.',
323
+ }),
324
+ ]);
325
+ });
326
+ });
327
+
328
+ describe('parseCodexSessionFile - persisted tools', () => {
329
+ it('restores exec_command as Bash with normalized result', () => {
330
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-persisted-tools.jsonl');
331
+ const messages = parseCodexSessionFile(filePath);
332
+
333
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
334
+ expect(assistantMsg).toBeDefined();
335
+
336
+ const bashTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'Bash');
337
+ expect(bashTool).toBeDefined();
338
+ expect(bashTool!.id).toBe('call_exec_1');
339
+ expect(bashTool!.input).toEqual({ command: 'cat src/main.ts' });
340
+ expect(bashTool!.status).toBe('completed');
341
+ // Result should be normalized: "Output:\n" prefix stripped
342
+ expect(bashTool!.result).toBe("import { Plugin } from 'obsidian';");
343
+ });
344
+
345
+ it('restores custom_tool_call apply_patch as native apply_patch', () => {
346
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-persisted-tools.jsonl');
347
+ const messages = parseCodexSessionFile(filePath);
348
+
349
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
350
+ const patchTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'apply_patch');
351
+ expect(patchTool).toBeDefined();
352
+ expect(patchTool!.id).toBe('call_patch_1');
353
+ expect(patchTool!.input.patch).toContain('Update File: src/main.ts');
354
+ expect(patchTool!.status).toBe('completed');
355
+ });
356
+
357
+ it('restores raw custom_tool_call apply_patch input as patch text', () => {
358
+ const content = [
359
+ JSON.stringify({
360
+ timestamp: '2026-03-27T00:00:00.000Z',
361
+ type: 'event_msg',
362
+ payload: { type: 'task_started' },
363
+ }),
364
+ JSON.stringify({
365
+ timestamp: '2026-03-27T00:00:01.000Z',
366
+ type: 'response_item',
367
+ payload: {
368
+ type: 'custom_tool_call',
369
+ name: 'apply_patch',
370
+ call_id: 'call_patch_raw',
371
+ input: '*** Begin Patch\n*** Update File: src/main.ts\n*** End Patch',
372
+ },
373
+ }),
374
+ ].join('\n');
375
+
376
+ const messages = parseCodexSessionContent(content);
377
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
378
+ const patchTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'apply_patch');
379
+ expect(patchTool).toBeDefined();
380
+ expect(patchTool!.input.patch).toBe('*** Begin Patch\n*** Update File: src/main.ts\n*** End Patch');
381
+ });
382
+
383
+ it('restores update_plan as TodoWrite', () => {
384
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-persisted-tools.jsonl');
385
+ const messages = parseCodexSessionFile(filePath);
386
+
387
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
388
+ const todoTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'TodoWrite');
389
+ expect(todoTool).toBeDefined();
390
+ expect(todoTool!.id).toBe('call_plan_1');
391
+ expect(todoTool!.input.todos).toEqual([
392
+ expect.objectContaining({ content: 'Fix the bug', status: 'completed' }),
393
+ expect.objectContaining({ content: 'Run tests', status: 'in_progress' }),
394
+ ]);
395
+ });
396
+
397
+ it('restores request_user_input as AskUserQuestion', () => {
398
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-persisted-tools.jsonl');
399
+ const messages = parseCodexSessionFile(filePath);
400
+
401
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
402
+ const askTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'AskUserQuestion');
403
+ expect(askTool).toBeDefined();
404
+ expect(askTool!.id).toBe('call_ask_1');
405
+ expect(askTool!.input.questions).toEqual([
406
+ expect.objectContaining({ question: 'Should I also update the tests?', id: 'q1' }),
407
+ ]);
408
+ });
409
+
410
+ it('restores request_user_input options and multi-select metadata', () => {
411
+ const content = [
412
+ JSON.stringify({
413
+ timestamp: '2026-03-27T00:00:00.000Z',
414
+ type: 'response_item',
415
+ payload: {
416
+ type: 'function_call',
417
+ name: 'request_user_input',
418
+ call_id: 'call_ask_opts',
419
+ arguments: JSON.stringify({
420
+ questions: [{
421
+ id: 'title_generation_timing',
422
+ question: 'When should I generate the title?',
423
+ options: [
424
+ { label: 'Non-blocking', description: 'Generate it later.' },
425
+ { label: 'Blocking', description: 'Wait before continuing.' },
426
+ ],
427
+ multi_select: true,
428
+ }],
429
+ }),
430
+ },
431
+ }),
432
+ ].join('\n');
433
+
434
+ const messages = parseCodexSessionContent(content);
435
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
436
+ const askTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'AskUserQuestion');
437
+
438
+ expect(askTool!.input.questions).toEqual([
439
+ {
440
+ id: 'title_generation_timing',
441
+ question: 'When should I generate the title?',
442
+ header: 'Q1',
443
+ options: [
444
+ { label: 'Non-blocking', description: 'Generate it later.' },
445
+ { label: 'Blocking', description: 'Wait before continuing.' },
446
+ ],
447
+ multiSelect: true,
448
+ },
449
+ ]);
450
+ });
451
+
452
+ it('restores view_image as Read', () => {
453
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-persisted-tools.jsonl');
454
+ const messages = parseCodexSessionFile(filePath);
455
+
456
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
457
+ const readTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'Read');
458
+ expect(readTool).toBeDefined();
459
+ expect(readTool!.id).toBe('call_img_1');
460
+ expect(readTool!.input.file_path).toBe('/tmp/screenshot.png');
461
+ });
462
+
463
+ it('restores non-empty write_stdin as native write_stdin', () => {
464
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-persisted-tools.jsonl');
465
+ const messages = parseCodexSessionFile(filePath);
466
+
467
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
468
+ const stdinTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'write_stdin');
469
+ expect(stdinTool).toBeDefined();
470
+ expect(stdinTool!.id).toBe('call_stdin_1');
471
+ expect(stdinTool!.input.session_id).toBe('sess_1');
472
+ expect(stdinTool!.input.chars).toBe('y\n');
473
+ });
474
+
475
+ it('suppresses standalone empty write_stdin polling calls', () => {
476
+ const content = [
477
+ JSON.stringify({
478
+ timestamp: '2026-03-27T00:00:00.000Z',
479
+ type: 'response_item',
480
+ payload: {
481
+ type: 'function_call',
482
+ name: 'write_stdin',
483
+ arguments: '{"session_id":2404,"chars":"","yield_time_ms":1000}',
484
+ call_id: 'call_poll',
485
+ },
486
+ }),
487
+ JSON.stringify({
488
+ timestamp: '2026-03-27T00:00:01.000Z',
489
+ type: 'response_item',
490
+ payload: {
491
+ type: 'function_call_output',
492
+ call_id: 'call_poll',
493
+ output: 'Input sent.',
494
+ },
495
+ }),
496
+ JSON.stringify({
497
+ timestamp: '2026-03-27T00:00:02.000Z',
498
+ type: 'response_item',
499
+ payload: {
500
+ type: 'message',
501
+ role: 'assistant',
502
+ content: [{ type: 'output_text', text: 'Done.' }],
503
+ },
504
+ }),
505
+ ].join('\n');
506
+
507
+ const messages = parseCodexSessionContent(content);
508
+ const assistantMsg = messages.find(m => m.role === 'assistant');
509
+
510
+ expect(assistantMsg!.toolCalls).toBeUndefined();
511
+ });
512
+
513
+ it('maps long-running write_stdin polling output back to the parent Bash tool', () => {
514
+ const content = [
515
+ JSON.stringify({
516
+ timestamp: '2026-03-27T00:00:00.000Z',
517
+ type: 'response_item',
518
+ payload: {
519
+ type: 'message',
520
+ role: 'user',
521
+ content: [{ type: 'input_text', text: 'Run checks.' }],
522
+ },
523
+ }),
524
+ JSON.stringify({
525
+ timestamp: '2026-03-27T00:00:01.000Z',
526
+ type: 'response_item',
527
+ payload: {
528
+ type: 'function_call',
529
+ name: 'exec_command',
530
+ arguments: '{"cmd":"bun run check"}',
531
+ call_id: 'call_cmd',
532
+ },
533
+ }),
534
+ JSON.stringify({
535
+ timestamp: '2026-03-27T00:00:02.000Z',
536
+ type: 'response_item',
537
+ payload: {
538
+ type: 'function_call_output',
539
+ call_id: 'call_cmd',
540
+ output: 'Chunk ID: aaa\nWall time: 0.0000 seconds\nProcess running with session ID 2404\nOriginal token count: 3\nOutput:\n$ bun run check\n',
541
+ },
542
+ }),
543
+ JSON.stringify({
544
+ timestamp: '2026-03-27T00:00:03.000Z',
545
+ type: 'response_item',
546
+ payload: {
547
+ type: 'function_call',
548
+ name: 'write_stdin',
549
+ arguments: '{"session_id":2404,"chars":"","yield_time_ms":1000}',
550
+ call_id: 'call_poll',
551
+ },
552
+ }),
553
+ JSON.stringify({
554
+ timestamp: '2026-03-27T00:00:04.000Z',
555
+ type: 'response_item',
556
+ payload: {
557
+ type: 'function_call_output',
558
+ call_id: 'call_poll',
559
+ output: 'Chunk ID: bbb\nWall time: 1.0000 seconds\nProcess exited with code 0\nOriginal token count: 2\nOutput:\nall good\n',
560
+ },
561
+ }),
562
+ JSON.stringify({
563
+ timestamp: '2026-03-27T00:00:05.000Z',
564
+ type: 'response_item',
565
+ payload: {
566
+ type: 'message',
567
+ role: 'assistant',
568
+ content: [{ type: 'output_text', text: 'Done.' }],
569
+ },
570
+ }),
571
+ ].join('\n');
572
+
573
+ const messages = parseCodexSessionContent(content);
574
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
575
+
576
+ expect(assistantMsg!.toolCalls!.map(tc => tc.name)).not.toContain('write_stdin');
577
+ const bashTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'Bash');
578
+ expect(bashTool).toMatchObject({
579
+ id: 'call_cmd',
580
+ status: 'completed',
581
+ result: '$ bun run check\nall good\n',
582
+ });
583
+ });
584
+
585
+ it('keeps non-empty write_stdin separate from the parent Bash tool', () => {
586
+ const content = [
587
+ JSON.stringify({
588
+ timestamp: '2026-03-27T00:00:00.000Z',
589
+ type: 'response_item',
590
+ payload: {
591
+ type: 'message',
592
+ role: 'user',
593
+ content: [{ type: 'input_text', text: 'Confirm the prompt.' }],
594
+ },
595
+ }),
596
+ JSON.stringify({
597
+ timestamp: '2026-03-27T00:00:01.000Z',
598
+ type: 'response_item',
599
+ payload: {
600
+ type: 'function_call',
601
+ name: 'exec_command',
602
+ arguments: '{"cmd":"npm init"}',
603
+ call_id: 'call_cmd',
604
+ },
605
+ }),
606
+ JSON.stringify({
607
+ timestamp: '2026-03-27T00:00:02.000Z',
608
+ type: 'response_item',
609
+ payload: {
610
+ type: 'function_call_output',
611
+ call_id: 'call_cmd',
612
+ output: 'Chunk ID: aaa\nWall time: 0.0000 seconds\nProcess running with session ID 2404\nOriginal token count: 2\nOutput:\nProceed? [y/N]\n',
613
+ },
614
+ }),
615
+ JSON.stringify({
616
+ timestamp: '2026-03-27T00:00:03.000Z',
617
+ type: 'response_item',
618
+ payload: {
619
+ type: 'function_call',
620
+ name: 'write_stdin',
621
+ arguments: '{"session_id":2404,"chars":"y\\n"}',
622
+ call_id: 'call_stdin',
623
+ },
624
+ }),
625
+ JSON.stringify({
626
+ timestamp: '2026-03-27T00:00:04.000Z',
627
+ type: 'response_item',
628
+ payload: {
629
+ type: 'function_call_output',
630
+ call_id: 'call_stdin',
631
+ output: 'Input sent.',
632
+ },
633
+ }),
634
+ ].join('\n');
635
+
636
+ const messages = parseCodexSessionContent(content);
637
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
638
+ const bashTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'Bash');
639
+ const stdinTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'write_stdin');
640
+
641
+ expect(bashTool).toMatchObject({
642
+ id: 'call_cmd',
643
+ status: 'running',
644
+ result: 'Proceed? [y/N]\n',
645
+ });
646
+ expect(stdinTool).toMatchObject({
647
+ id: 'call_stdin',
648
+ status: 'completed',
649
+ input: { session_id: 2404, chars: 'y\n' },
650
+ result: 'Input sent.',
651
+ });
652
+ });
653
+
654
+ it('drops orphan custom_tool_call_output rows instead of rendering generic tool cards', () => {
655
+ const content = [
656
+ JSON.stringify({
657
+ timestamp: '2026-03-27T00:00:00.000Z',
658
+ type: 'response_item',
659
+ payload: {
660
+ type: 'custom_tool_call_output',
661
+ call_id: 'call_patch_orphan',
662
+ output: 'Success. Updated the following files:\nM /tmp/a.ts\n',
663
+ },
664
+ }),
665
+ JSON.stringify({
666
+ timestamp: '2026-03-27T00:00:01.000Z',
667
+ type: 'response_item',
668
+ payload: {
669
+ type: 'message',
670
+ role: 'assistant',
671
+ content: [{ type: 'output_text', text: 'Done.' }],
672
+ },
673
+ }),
674
+ ].join('\n');
675
+
676
+ const messages = parseCodexSessionContent(content);
677
+
678
+ expect(messages).toHaveLength(1);
679
+ expect(messages[0].role).toBe('assistant');
680
+ expect(messages[0].toolCalls).toBeUndefined();
681
+ expect(messages[0].content).toBe('Done.');
682
+ });
683
+ });
684
+
685
+ describe('parseCodexSessionFile - agent lifecycle', () => {
686
+ it('restores agent lifecycle tools with native names', () => {
687
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-agent-lifecycle.jsonl');
688
+ const messages = parseCodexSessionFile(filePath);
689
+
690
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
691
+ expect(assistantMsg).toBeDefined();
692
+ const toolNames = assistantMsg!.toolCalls!.map(tc => tc.name);
693
+
694
+ expect(toolNames).toContain('spawn_agent');
695
+ expect(toolNames).toContain('send_input');
696
+ expect(toolNames).toContain('wait');
697
+ expect(toolNames).toContain('resume_agent');
698
+ expect(toolNames).toContain('close_agent');
699
+
700
+ // Should NOT be mapped to Agent/Task
701
+ expect(toolNames).not.toContain('Agent');
702
+ expect(toolNames).not.toContain('Task');
703
+ });
704
+
705
+ it('preserves spawn_agent input fields', () => {
706
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-agent-lifecycle.jsonl');
707
+ const messages = parseCodexSessionFile(filePath);
708
+
709
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
710
+ const spawnTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'spawn_agent');
711
+ expect(spawnTool!.input).toEqual({
712
+ message: 'Update the imports in utils.ts',
713
+ agent_type: 'code-writer',
714
+ });
715
+ });
716
+
717
+ it('preserves wait input fields', () => {
718
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-agent-lifecycle.jsonl');
719
+ const messages = parseCodexSessionFile(filePath);
720
+
721
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
722
+ const waitTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'wait');
723
+ expect(waitTool!.input).toEqual({
724
+ ids: ['agent_001'],
725
+ timeout_ms: 30000,
726
+ });
727
+ });
728
+ });
729
+
730
+ describe('parseCodexSessionContent - system-injected user messages', () => {
731
+ it('should skip AGENTS.md instructions injected as user message', () => {
732
+ const content = [
733
+ JSON.stringify({ type: 'session_meta', id: 'test-session' }),
734
+ JSON.stringify({
735
+ type: 'response_item',
736
+ payload: {
737
+ type: 'message',
738
+ role: 'developer',
739
+ content: [{ type: 'input_text', text: '<permissions instructions>\nSandbox mode...\n</permissions instructions>' }],
740
+ },
741
+ }),
742
+ JSON.stringify({
743
+ type: 'response_item',
744
+ payload: {
745
+ type: 'message',
746
+ role: 'user',
747
+ content: [
748
+ { type: 'input_text', text: '# AGENTS.md instructions for /Users/test/project\n\n<INSTRUCTIONS>\nDo good work.\n</INSTRUCTIONS>' },
749
+ { type: 'input_text', text: '<environment_context>\n <cwd>/Users/test/project</cwd>\n</environment_context>' },
750
+ ],
751
+ },
752
+ }),
753
+ JSON.stringify({ type: 'event_msg', payload: { type: 'task_started' } }),
754
+ JSON.stringify({
755
+ timestamp: '2026-03-27T00:00:00.000Z',
756
+ type: 'response_item',
757
+ payload: {
758
+ type: 'message',
759
+ role: 'user',
760
+ content: [{ type: 'input_text', text: 'Fix the bug in main.ts' }],
761
+ },
762
+ }),
763
+ JSON.stringify({
764
+ timestamp: '2026-03-27T00:00:01.000Z',
765
+ type: 'response_item',
766
+ payload: {
767
+ type: 'message',
768
+ role: 'assistant',
769
+ content: [{ type: 'output_text', text: 'Done.' }],
770
+ },
771
+ }),
772
+ ].join('\n');
773
+
774
+ const messages = parseCodexSessionContent(content);
775
+
776
+ // AGENTS.md message should be filtered out; only real user + assistant remain
777
+ expect(messages).toHaveLength(2);
778
+ expect(messages[0]).toMatchObject({ role: 'user', content: 'Fix the bug in main.ts' });
779
+ expect(messages[1]).toMatchObject({ role: 'assistant', content: 'Done.' });
780
+ });
781
+
782
+ it('should skip standalone <environment_context> user message', () => {
783
+ const content = [
784
+ JSON.stringify({
785
+ type: 'response_item',
786
+ payload: {
787
+ type: 'message',
788
+ role: 'user',
789
+ content: [{ type: 'input_text', text: '<environment_context>\n <cwd>/Users/test</cwd>\n</environment_context>' }],
790
+ },
791
+ }),
792
+ JSON.stringify({
793
+ timestamp: '2026-03-27T00:00:00.000Z',
794
+ type: 'response_item',
795
+ payload: {
796
+ type: 'message',
797
+ role: 'assistant',
798
+ content: [{ type: 'output_text', text: 'Ready.' }],
799
+ },
800
+ }),
801
+ ].join('\n');
802
+
803
+ const messages = parseCodexSessionContent(content);
804
+
805
+ expect(messages).toHaveLength(1);
806
+ expect(messages[0]).toMatchObject({ role: 'assistant', content: 'Ready.' });
807
+ });
808
+
809
+ it('should skip Conductor interrupt control envelope persisted as user message', () => {
810
+ const conductorControlText = [
811
+ '<system_instruction>',
812
+ 'You are working inside Conductor, a Mac app that lets the user run many coding agents in parallel.',
813
+ '</system_instruction>',
814
+ '',
815
+ '<turn_aborted>',
816
+ 'The user interrupted the previous turn on purpose.',
817
+ '</turn_aborted>',
818
+ '',
819
+ '<user-preferences>',
820
+ 'dev notes and throwaway scripts should be placed inside .context/',
821
+ '</user-preferences>',
822
+ ].join('\n');
823
+
824
+ const content = [
825
+ JSON.stringify({
826
+ type: 'response_item',
827
+ payload: {
828
+ type: 'message',
829
+ role: 'user',
830
+ content: [{ type: 'input_text', text: conductorControlText }],
831
+ },
832
+ }),
833
+ JSON.stringify({
834
+ timestamp: '2026-03-27T00:00:00.000Z',
835
+ type: 'response_item',
836
+ payload: {
837
+ type: 'message',
838
+ role: 'user',
839
+ content: [{ type: 'input_text', text: 'Fix the interrupt rendering bug.' }],
840
+ },
841
+ }),
842
+ JSON.stringify({
843
+ timestamp: '2026-03-27T00:00:01.000Z',
844
+ type: 'response_item',
845
+ payload: {
846
+ type: 'message',
847
+ role: 'assistant',
848
+ content: [{ type: 'output_text', text: 'Fixed.' }],
849
+ },
850
+ }),
851
+ ].join('\n');
852
+
853
+ const messages = parseCodexSessionContent(content);
854
+
855
+ expect(messages).toHaveLength(2);
856
+ expect(messages[0]).toMatchObject({ role: 'user', content: 'Fix the interrupt rendering bug.' });
857
+ expect(messages[1]).toMatchObject({ role: 'assistant', content: 'Fixed.' });
858
+ });
859
+
860
+ it('should strip leading Conductor control blocks and keep visible user text', () => {
861
+ const content = [
862
+ JSON.stringify({
863
+ timestamp: '2026-03-27T00:00:00.000Z',
864
+ type: 'response_item',
865
+ payload: {
866
+ type: 'message',
867
+ role: 'user',
868
+ content: [{
869
+ type: 'input_text',
870
+ text: [
871
+ '<system_instruction>',
872
+ 'You are working inside Conductor.',
873
+ '</system_instruction>',
874
+ '',
875
+ '<turn_aborted>',
876
+ 'The user interrupted the previous turn on purpose.',
877
+ '</turn_aborted>',
878
+ '',
879
+ 'Hide the interrupt envelope, but keep this prompt.',
880
+ ].join('\n'),
881
+ }],
882
+ },
883
+ }),
884
+ JSON.stringify({
885
+ timestamp: '2026-03-27T00:00:01.000Z',
886
+ type: 'response_item',
887
+ payload: {
888
+ type: 'message',
889
+ role: 'assistant',
890
+ content: [{ type: 'output_text', text: 'Done.' }],
891
+ },
892
+ }),
893
+ ].join('\n');
894
+
895
+ const messages = parseCodexSessionContent(content);
896
+
897
+ expect(messages).toHaveLength(2);
898
+ expect(messages[0]).toMatchObject({
899
+ role: 'user',
900
+ content: 'Hide the interrupt envelope, but keep this prompt.',
901
+ });
902
+ expect(messages[1]).toMatchObject({ role: 'assistant', content: 'Done.' });
903
+ });
904
+
905
+ it('should set displayContent stripping bracket context from user messages', () => {
906
+ const content = [
907
+ JSON.stringify({
908
+ timestamp: '2026-03-27T00:00:00.000Z',
909
+ type: 'response_item',
910
+ payload: {
911
+ type: 'message',
912
+ role: 'user',
913
+ content: [{ type: 'input_text', text: 'Fix the bug\n[Current note: notes/bug.md]' }],
914
+ },
915
+ }),
916
+ JSON.stringify({
917
+ timestamp: '2026-03-27T00:00:01.000Z',
918
+ type: 'response_item',
919
+ payload: {
920
+ type: 'message',
921
+ role: 'assistant',
922
+ content: [{ type: 'output_text', text: 'Done.' }],
923
+ },
924
+ }),
925
+ ].join('\n');
926
+
927
+ const messages = parseCodexSessionContent(content);
928
+
929
+ expect(messages).toHaveLength(2);
930
+ expect(messages[0]).toMatchObject({
931
+ role: 'user',
932
+ content: 'Fix the bug\n[Current note: notes/bug.md]',
933
+ displayContent: 'Fix the bug',
934
+ });
935
+ });
936
+
937
+ it('should set displayContent stripping editor selection context', () => {
938
+ const content = [
939
+ JSON.stringify({
940
+ timestamp: '2026-03-27T00:00:00.000Z',
941
+ type: 'response_item',
942
+ payload: {
943
+ type: 'message',
944
+ role: 'user',
945
+ content: [{ type: 'input_text', text: 'Explain this\n[Editor selection from notes/code.md:\nconst x = 1;\n]' }],
946
+ },
947
+ }),
948
+ JSON.stringify({
949
+ timestamp: '2026-03-27T00:00:01.000Z',
950
+ type: 'response_item',
951
+ payload: {
952
+ type: 'message',
953
+ role: 'assistant',
954
+ content: [{ type: 'output_text', text: 'It declares a variable.' }],
955
+ },
956
+ }),
957
+ ].join('\n');
958
+
959
+ const messages = parseCodexSessionContent(content);
960
+
961
+ expect(messages[0]).toMatchObject({
962
+ role: 'user',
963
+ displayContent: 'Explain this',
964
+ });
965
+ });
966
+
967
+ it('should not set displayContent on plain user messages', () => {
968
+ const content = [
969
+ JSON.stringify({
970
+ timestamp: '2026-03-27T00:00:00.000Z',
971
+ type: 'response_item',
972
+ payload: {
973
+ type: 'message',
974
+ role: 'user',
975
+ content: [{ type: 'input_text', text: 'What does main.ts do?' }],
976
+ },
977
+ }),
978
+ JSON.stringify({
979
+ timestamp: '2026-03-27T00:00:01.000Z',
980
+ type: 'response_item',
981
+ payload: {
982
+ type: 'message',
983
+ role: 'assistant',
984
+ content: [{ type: 'output_text', text: 'It initializes the plugin.' }],
985
+ },
986
+ }),
987
+ ].join('\n');
988
+
989
+ const messages = parseCodexSessionContent(content);
990
+
991
+ expect(messages[0]).toMatchObject({ role: 'user', content: 'What does main.ts do?' });
992
+ expect(messages[0].displayContent).toBeUndefined();
993
+ });
994
+
995
+ it('should filter out skill wrapper user messages as system-injected', () => {
996
+ const skillText = [
997
+ '<skill>',
998
+ '<name>test</name>',
999
+ '<path>/Users/me/.codex/skills/test/SKILL.md</path>',
1000
+ '---',
1001
+ 'description: testing',
1002
+ '---',
1003
+ '',
1004
+ '## Task',
1005
+ '',
1006
+ 'tell me a joke',
1007
+ '',
1008
+ '</skill>',
1009
+ ].join('\n');
1010
+
1011
+ const content = [
1012
+ JSON.stringify({
1013
+ timestamp: '2026-03-27T00:00:00.000Z',
1014
+ type: 'response_item',
1015
+ payload: {
1016
+ type: 'message',
1017
+ role: 'user',
1018
+ content: [{ type: 'input_text', text: skillText }],
1019
+ },
1020
+ }),
1021
+ JSON.stringify({
1022
+ timestamp: '2026-03-27T00:00:01.000Z',
1023
+ type: 'response_item',
1024
+ payload: {
1025
+ type: 'message',
1026
+ role: 'assistant',
1027
+ content: [{ type: 'output_text', text: 'Why did the skeleton...' }],
1028
+ },
1029
+ }),
1030
+ ].join('\n');
1031
+
1032
+ const messages = parseCodexSessionContent(content);
1033
+
1034
+ // Skill wrapper is system-injected — only the assistant message should remain
1035
+ expect(messages).toHaveLength(1);
1036
+ expect(messages[0]).toMatchObject({ role: 'assistant' });
1037
+ });
1038
+
1039
+ it('should NOT skip real user messages', () => {
1040
+ const content = [
1041
+ JSON.stringify({
1042
+ timestamp: '2026-03-27T00:00:00.000Z',
1043
+ type: 'response_item',
1044
+ payload: {
1045
+ type: 'message',
1046
+ role: 'user',
1047
+ content: [{ type: 'input_text', text: 'What does main.ts do?' }],
1048
+ },
1049
+ }),
1050
+ JSON.stringify({
1051
+ timestamp: '2026-03-27T00:00:01.000Z',
1052
+ type: 'response_item',
1053
+ payload: {
1054
+ type: 'message',
1055
+ role: 'assistant',
1056
+ content: [{ type: 'output_text', text: 'It initializes the plugin.' }],
1057
+ },
1058
+ }),
1059
+ ].join('\n');
1060
+
1061
+ const messages = parseCodexSessionContent(content);
1062
+
1063
+ expect(messages).toHaveLength(2);
1064
+ expect(messages[0]).toMatchObject({ role: 'user', content: 'What does main.ts do?' });
1065
+ });
1066
+ });
1067
+
1068
+ describe('parseCodexSessionFile - persisted web_search_call', () => {
1069
+ it('restores web_search_call as WebSearch', () => {
1070
+ const filePath = path.join(FIXTURES_DIR, 'codex-session-websearch-persisted.jsonl');
1071
+ const messages = parseCodexSessionFile(filePath);
1072
+
1073
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
1074
+ expect(assistantMsg).toBeDefined();
1075
+
1076
+ const searchTool = assistantMsg!.toolCalls!.find(tc => tc.name === 'WebSearch');
1077
+ expect(searchTool).toBeDefined();
1078
+ expect(searchTool!.id).toBe('call_ws_1');
1079
+ expect(searchTool!.input.actionType).toBe('search');
1080
+ expect(searchTool!.input.query).toBe('obsidian plugin API');
1081
+ expect(searchTool!.status).toBe('completed');
1082
+ });
1083
+
1084
+ it('keeps distinct persisted web_search_call entries when call_id is missing', () => {
1085
+ const content = [
1086
+ JSON.stringify({
1087
+ timestamp: '2026-03-27T00:00:00.000Z',
1088
+ type: 'event_msg',
1089
+ payload: { type: 'task_started' },
1090
+ }),
1091
+ JSON.stringify({
1092
+ timestamp: '2026-03-27T00:00:01.000Z',
1093
+ type: 'response_item',
1094
+ payload: {
1095
+ type: 'web_search_call',
1096
+ status: 'completed',
1097
+ action: { type: 'search', query: 'obsidian plugin API' },
1098
+ },
1099
+ }),
1100
+ JSON.stringify({
1101
+ timestamp: '2026-03-27T00:00:02.000Z',
1102
+ type: 'response_item',
1103
+ payload: {
1104
+ type: 'web_search_call',
1105
+ status: 'completed',
1106
+ action: { type: 'open_page', url: 'https://docs.obsidian.md' },
1107
+ },
1108
+ }),
1109
+ ].join('\n');
1110
+
1111
+ const messages = parseCodexSessionContent(content);
1112
+ const assistantMsg = messages.find(m => m.role === 'assistant' && m.toolCalls);
1113
+ expect(assistantMsg).toBeDefined();
1114
+
1115
+ const webSearchTools = assistantMsg!.toolCalls!.filter(tc => tc.name === 'WebSearch');
1116
+ expect(webSearchTools).toHaveLength(2);
1117
+ expect(webSearchTools[0]).toMatchObject({
1118
+ id: 'tail-ws-1',
1119
+ input: { actionType: 'search', query: 'obsidian plugin API' },
1120
+ result: 'Search complete',
1121
+ });
1122
+ expect(webSearchTools[1]).toMatchObject({
1123
+ id: 'tail-ws-2',
1124
+ input: { actionType: 'open_page', url: 'https://docs.obsidian.md' },
1125
+ result: 'Search complete',
1126
+ });
1127
+ });
1128
+ });
1129
+
1130
+ describe('parseCodexSessionContent - event_msg handling', () => {
1131
+ it('task_started + agent_message + task_complete produces proper turn', () => {
1132
+ const content = [
1133
+ JSON.stringify({
1134
+ timestamp: '2026-03-27T00:00:00.000Z',
1135
+ type: 'response_item',
1136
+ payload: {
1137
+ type: 'message',
1138
+ role: 'user',
1139
+ content: [{ type: 'input_text', text: 'Hello' }],
1140
+ },
1141
+ }),
1142
+ JSON.stringify({
1143
+ timestamp: '2026-03-27T00:00:01.000Z',
1144
+ type: 'event_msg',
1145
+ payload: { type: 'task_started' },
1146
+ }),
1147
+ JSON.stringify({
1148
+ timestamp: '2026-03-27T00:00:02.000Z',
1149
+ type: 'event_msg',
1150
+ payload: { type: 'agent_message', message: 'Hi there!' },
1151
+ }),
1152
+ JSON.stringify({
1153
+ timestamp: '2026-03-27T00:00:03.000Z',
1154
+ type: 'event_msg',
1155
+ payload: { type: 'task_complete' },
1156
+ }),
1157
+ ].join('\n');
1158
+
1159
+ const messages = parseCodexSessionContent(content);
1160
+
1161
+ expect(messages).toHaveLength(2);
1162
+ expect(messages[0]).toMatchObject({ role: 'user', content: 'Hello' });
1163
+ expect(messages[1]).toMatchObject({ role: 'assistant', content: 'Hi there!' });
1164
+ });
1165
+
1166
+ it('turn_aborted marks bubble as interrupted', () => {
1167
+ const content = [
1168
+ JSON.stringify({
1169
+ timestamp: '2026-03-27T00:00:00.000Z',
1170
+ type: 'event_msg',
1171
+ payload: { type: 'task_started' },
1172
+ }),
1173
+ JSON.stringify({
1174
+ timestamp: '2026-03-27T00:00:01.000Z',
1175
+ type: 'event_msg',
1176
+ payload: { type: 'agent_message', message: 'Working on it...' },
1177
+ }),
1178
+ JSON.stringify({
1179
+ timestamp: '2026-03-27T00:00:02.000Z',
1180
+ type: 'event_msg',
1181
+ payload: { type: 'turn_aborted' },
1182
+ }),
1183
+ ].join('\n');
1184
+
1185
+ const messages = parseCodexSessionContent(content);
1186
+
1187
+ expect(messages).toHaveLength(1);
1188
+ expect(messages[0]).toMatchObject({
1189
+ role: 'assistant',
1190
+ content: 'Working on it...',
1191
+ isInterrupt: true,
1192
+ });
1193
+ });
1194
+
1195
+ it('user_message reconstructs user ChatMessage from event_msg', () => {
1196
+ const content = [
1197
+ JSON.stringify({
1198
+ timestamp: '2026-03-27T00:00:00.000Z',
1199
+ type: 'event_msg',
1200
+ payload: { type: 'task_started' },
1201
+ }),
1202
+ JSON.stringify({
1203
+ timestamp: '2026-03-27T00:00:00.500Z',
1204
+ type: 'event_msg',
1205
+ payload: { type: 'user_message', message: 'What is 2+2?' },
1206
+ }),
1207
+ JSON.stringify({
1208
+ timestamp: '2026-03-27T00:00:01.000Z',
1209
+ type: 'event_msg',
1210
+ payload: { type: 'agent_message', message: 'The answer is 4.' },
1211
+ }),
1212
+ JSON.stringify({
1213
+ timestamp: '2026-03-27T00:00:02.000Z',
1214
+ type: 'event_msg',
1215
+ payload: { type: 'task_complete' },
1216
+ }),
1217
+ ].join('\n');
1218
+
1219
+ const messages = parseCodexSessionContent(content);
1220
+
1221
+ expect(messages).toHaveLength(2);
1222
+ expect(messages[0]).toMatchObject({
1223
+ role: 'user',
1224
+ content: 'What is 2+2?',
1225
+ });
1226
+ expect(messages[1]).toMatchObject({
1227
+ role: 'assistant',
1228
+ content: 'The answer is 4.',
1229
+ });
1230
+ });
1231
+
1232
+ it('user_message with system content is filtered out', () => {
1233
+ const content = [
1234
+ JSON.stringify({
1235
+ timestamp: '2026-03-27T00:00:00.000Z',
1236
+ type: 'event_msg',
1237
+ payload: { type: 'task_started' },
1238
+ }),
1239
+ JSON.stringify({
1240
+ timestamp: '2026-03-27T00:00:00.500Z',
1241
+ type: 'event_msg',
1242
+ payload: { type: 'user_message', message: '# AGENTS.md instructions for /test\nDo good.' },
1243
+ }),
1244
+ JSON.stringify({
1245
+ timestamp: '2026-03-27T00:00:01.000Z',
1246
+ type: 'event_msg',
1247
+ payload: { type: 'agent_message', message: 'Done.' },
1248
+ }),
1249
+ JSON.stringify({
1250
+ timestamp: '2026-03-27T00:00:02.000Z',
1251
+ type: 'event_msg',
1252
+ payload: { type: 'task_complete' },
1253
+ }),
1254
+ ].join('\n');
1255
+
1256
+ const messages = parseCodexSessionContent(content);
1257
+
1258
+ // System user message should be filtered; only assistant remains
1259
+ const userMessages = messages.filter(m => m.role === 'user');
1260
+ expect(userMessages).toHaveLength(0);
1261
+ expect(messages).toHaveLength(1);
1262
+ expect(messages[0]).toMatchObject({ role: 'assistant', content: 'Done.' });
1263
+ });
1264
+ });
1265
+
1266
+ describe('parseCodexSessionContent - multi-bubble turns', () => {
1267
+ it('assistant text + tool call + tool result + more text in single turn', () => {
1268
+ const content = [
1269
+ JSON.stringify({
1270
+ timestamp: '2026-03-27T00:00:00.000Z',
1271
+ type: 'response_item',
1272
+ payload: {
1273
+ type: 'message',
1274
+ role: 'user',
1275
+ content: [{ type: 'input_text', text: 'Check the file.' }],
1276
+ },
1277
+ }),
1278
+ JSON.stringify({
1279
+ timestamp: '2026-03-27T00:00:01.000Z',
1280
+ type: 'event_msg',
1281
+ payload: { type: 'agent_reasoning', text: 'Let me check.' },
1282
+ }),
1283
+ JSON.stringify({
1284
+ timestamp: '2026-03-27T00:00:02.000Z',
1285
+ type: 'response_item',
1286
+ payload: {
1287
+ type: 'function_call',
1288
+ name: 'exec_command',
1289
+ arguments: '{"command":"cat file.txt"}',
1290
+ call_id: 'call_100',
1291
+ },
1292
+ }),
1293
+ JSON.stringify({
1294
+ timestamp: '2026-03-27T00:00:03.000Z',
1295
+ type: 'response_item',
1296
+ payload: {
1297
+ type: 'function_call_output',
1298
+ call_id: 'call_100',
1299
+ output: 'Exit code: 0\nOutput:\nhello world',
1300
+ },
1301
+ }),
1302
+ JSON.stringify({
1303
+ timestamp: '2026-03-27T00:00:04.000Z',
1304
+ type: 'response_item',
1305
+ payload: {
1306
+ type: 'message',
1307
+ role: 'assistant',
1308
+ content: [{ type: 'output_text', text: 'The file contains hello world.' }],
1309
+ },
1310
+ }),
1311
+ ].join('\n');
1312
+
1313
+ const messages = parseCodexSessionContent(content);
1314
+
1315
+ expect(messages).toHaveLength(2);
1316
+ expect(messages[0]).toMatchObject({ role: 'user', content: 'Check the file.' });
1317
+
1318
+ const assistant = messages[1];
1319
+ expect(assistant.role).toBe('assistant');
1320
+ expect(assistant.content).toBe('The file contains hello world.');
1321
+
1322
+ // Tool calls should be present
1323
+ expect(assistant.toolCalls).toHaveLength(1);
1324
+ expect(assistant.toolCalls![0]).toMatchObject({
1325
+ id: 'call_100',
1326
+ name: 'Bash',
1327
+ status: 'completed',
1328
+ });
1329
+
1330
+ // Content blocks: thinking, tool_use, text
1331
+ expect(assistant.contentBlocks).toBeDefined();
1332
+ const blockTypes = assistant.contentBlocks!.map(b => b.type);
1333
+ expect(blockTypes).toContain('thinking');
1334
+ expect(blockTypes).toContain('tool_use');
1335
+ expect(blockTypes).toContain('text');
1336
+ });
1337
+ });
1338
+
1339
+ describe('parseCodexSessionContent - cross-turn tool output', () => {
1340
+ it('function_call in turn 1, function_call_output in turn 2 links back', () => {
1341
+ const content = [
1342
+ // Turn 1: user + tool call
1343
+ JSON.stringify({
1344
+ timestamp: '2026-03-27T00:00:00.000Z',
1345
+ type: 'response_item',
1346
+ payload: {
1347
+ type: 'message',
1348
+ role: 'user',
1349
+ content: [{ type: 'input_text', text: 'Run the command.' }],
1350
+ },
1351
+ }),
1352
+ JSON.stringify({
1353
+ timestamp: '2026-03-27T00:00:01.000Z',
1354
+ type: 'response_item',
1355
+ payload: {
1356
+ type: 'function_call',
1357
+ name: 'exec_command',
1358
+ arguments: '{"command":"echo hi"}',
1359
+ call_id: 'call_cross_1',
1360
+ },
1361
+ }),
1362
+ // Turn 2: new user message + output for turn 1's call
1363
+ JSON.stringify({
1364
+ timestamp: '2026-03-27T00:00:02.000Z',
1365
+ type: 'response_item',
1366
+ payload: {
1367
+ type: 'message',
1368
+ role: 'user',
1369
+ content: [{ type: 'input_text', text: 'Continue.' }],
1370
+ },
1371
+ }),
1372
+ JSON.stringify({
1373
+ timestamp: '2026-03-27T00:00:03.000Z',
1374
+ type: 'response_item',
1375
+ payload: {
1376
+ type: 'function_call_output',
1377
+ call_id: 'call_cross_1',
1378
+ output: 'Exit code: 0\nOutput:\nhi',
1379
+ },
1380
+ }),
1381
+ JSON.stringify({
1382
+ timestamp: '2026-03-27T00:00:04.000Z',
1383
+ type: 'response_item',
1384
+ payload: {
1385
+ type: 'message',
1386
+ role: 'assistant',
1387
+ content: [{ type: 'output_text', text: 'Done.' }],
1388
+ },
1389
+ }),
1390
+ ].join('\n');
1391
+
1392
+ const messages = parseCodexSessionContent(content);
1393
+
1394
+ // Find the assistant message from turn 1 (has the tool call)
1395
+ const turn1Assistant = messages.find(
1396
+ m => m.role === 'assistant' && m.toolCalls?.some(tc => tc.id === 'call_cross_1'),
1397
+ );
1398
+ expect(turn1Assistant).toBeDefined();
1399
+
1400
+ // The tool output should be resolved back to turn 1
1401
+ const tc = turn1Assistant!.toolCalls!.find(t => t.id === 'call_cross_1');
1402
+ expect(tc!.status).toBe('completed');
1403
+ expect(tc!.result).toBe('hi');
1404
+ });
1405
+ });
1406
+
1407
+ describe('parseCodexSessionContent - non-string tool output', () => {
1408
+ it('does not crash when function_call_output has an array output (e.g. view_image)', () => {
1409
+ const content = [
1410
+ JSON.stringify({
1411
+ timestamp: '2026-03-27T00:00:00.000Z',
1412
+ type: 'event_msg',
1413
+ payload: { type: 'task_started', turn_id: 'turn-img' },
1414
+ }),
1415
+ JSON.stringify({
1416
+ timestamp: '2026-03-27T00:00:01.000Z',
1417
+ type: 'response_item',
1418
+ payload: {
1419
+ type: 'message',
1420
+ role: 'user',
1421
+ content: [{ type: 'input_text', text: 'Show me the image.' }],
1422
+ },
1423
+ }),
1424
+ JSON.stringify({
1425
+ timestamp: '2026-03-27T00:00:02.000Z',
1426
+ type: 'response_item',
1427
+ payload: {
1428
+ type: 'function_call',
1429
+ name: 'view_image',
1430
+ arguments: '{"path":"/tmp/cat.png"}',
1431
+ call_id: 'call_img_1',
1432
+ },
1433
+ }),
1434
+ JSON.stringify({
1435
+ timestamp: '2026-03-27T00:00:03.000Z',
1436
+ type: 'response_item',
1437
+ payload: {
1438
+ type: 'function_call_output',
1439
+ call_id: 'call_img_1',
1440
+ output: [{ type: 'input_image', image_url: 'data:image/jpeg;base64,abc' }],
1441
+ },
1442
+ }),
1443
+ JSON.stringify({
1444
+ timestamp: '2026-03-27T00:00:04.000Z',
1445
+ type: 'response_item',
1446
+ payload: {
1447
+ type: 'message',
1448
+ role: 'assistant',
1449
+ content: [{ type: 'output_text', text: 'Here is the image.' }],
1450
+ },
1451
+ }),
1452
+ JSON.stringify({
1453
+ timestamp: '2026-03-27T00:00:05.000Z',
1454
+ type: 'event_msg',
1455
+ payload: { type: 'task_complete' },
1456
+ }),
1457
+ ].join('\n');
1458
+
1459
+ const messages = parseCodexSessionContent(content);
1460
+
1461
+ // Should not throw and should produce messages
1462
+ expect(messages.length).toBeGreaterThan(0);
1463
+
1464
+ // The tool call should be resolved
1465
+ const assistantMsg = messages.find(
1466
+ m => m.role === 'assistant' && m.toolCalls?.some(tc => tc.id === 'call_img_1'),
1467
+ );
1468
+ expect(assistantMsg).toBeDefined();
1469
+
1470
+ const tc = assistantMsg!.toolCalls!.find(t => t.id === 'call_img_1');
1471
+ expect(tc!.status).toBe('completed');
1472
+ expect(tc!.result).toBe('/tmp/cat.png');
1473
+ });
1474
+ });
1475
+
1476
+ describe('parseCodexSessionContent - interrupted message granularity', () => {
1477
+ it('interrupted bubble with content sets isInterrupt on ChatMessage', () => {
1478
+ const content = [
1479
+ JSON.stringify({
1480
+ timestamp: '2026-03-27T00:00:00.000Z',
1481
+ type: 'event_msg',
1482
+ payload: { type: 'task_started' },
1483
+ }),
1484
+ JSON.stringify({
1485
+ timestamp: '2026-03-27T00:00:01.000Z',
1486
+ type: 'event_msg',
1487
+ payload: { type: 'agent_message', message: 'Starting to work on the feature...' },
1488
+ }),
1489
+ JSON.stringify({
1490
+ timestamp: '2026-03-27T00:00:02.000Z',
1491
+ type: 'event_msg',
1492
+ payload: { type: 'turn_aborted' },
1493
+ }),
1494
+ ].join('\n');
1495
+
1496
+ const messages = parseCodexSessionContent(content);
1497
+
1498
+ expect(messages).toHaveLength(1);
1499
+ expect(messages[0].content).toBe('Starting to work on the feature...');
1500
+ expect(messages[0].isInterrupt).toBe(true);
1501
+ });
1502
+
1503
+ it('interrupted empty bubble sets isInterrupt on bare ChatMessage', () => {
1504
+ const content = [
1505
+ JSON.stringify({
1506
+ timestamp: '2026-03-27T00:00:00.000Z',
1507
+ type: 'event_msg',
1508
+ payload: { type: 'task_started' },
1509
+ }),
1510
+ JSON.stringify({
1511
+ timestamp: '2026-03-27T00:00:01.000Z',
1512
+ type: 'event_msg',
1513
+ payload: { type: 'turn_aborted' },
1514
+ }),
1515
+ ].join('\n');
1516
+
1517
+ const messages = parseCodexSessionContent(content);
1518
+
1519
+ expect(messages).toHaveLength(1);
1520
+ expect(messages[0].content).toBe('');
1521
+ expect(messages[0].isInterrupt).toBe(true);
1522
+ });
1523
+ });
1524
+
1525
+ describe('parseCodexSessionContent - response duration', () => {
1526
+ it('calculates durationSeconds from user timestamp to last assistant event', () => {
1527
+ const content = [
1528
+ JSON.stringify({
1529
+ timestamp: '2026-03-27T00:00:00.000Z',
1530
+ type: 'response_item',
1531
+ payload: {
1532
+ type: 'message',
1533
+ role: 'user',
1534
+ content: [{ type: 'input_text', text: 'Quick question.' }],
1535
+ },
1536
+ }),
1537
+ JSON.stringify({
1538
+ timestamp: '2026-03-27T00:00:05.000Z',
1539
+ type: 'response_item',
1540
+ payload: {
1541
+ type: 'message',
1542
+ role: 'assistant',
1543
+ content: [{ type: 'output_text', text: 'Quick answer.' }],
1544
+ },
1545
+ }),
1546
+ ].join('\n');
1547
+
1548
+ const messages = parseCodexSessionContent(content);
1549
+
1550
+ expect(messages).toHaveLength(2);
1551
+ const assistant = messages.find(m => m.role === 'assistant');
1552
+ expect(assistant!.durationSeconds).toBe(5);
1553
+ });
1554
+
1555
+ it('attaches durationSeconds to the last assistant message of the turn', () => {
1556
+ const content = [
1557
+ JSON.stringify({
1558
+ timestamp: '2026-03-27T00:00:00.000Z',
1559
+ type: 'response_item',
1560
+ payload: {
1561
+ type: 'message',
1562
+ role: 'user',
1563
+ content: [{ type: 'input_text', text: 'Do stuff.' }],
1564
+ },
1565
+ }),
1566
+ JSON.stringify({
1567
+ timestamp: '2026-03-27T00:00:02.000Z',
1568
+ type: 'response_item',
1569
+ payload: {
1570
+ type: 'function_call',
1571
+ name: 'exec_command',
1572
+ arguments: '{"command":"ls"}',
1573
+ call_id: 'call_dur_1',
1574
+ },
1575
+ }),
1576
+ JSON.stringify({
1577
+ timestamp: '2026-03-27T00:00:04.000Z',
1578
+ type: 'response_item',
1579
+ payload: {
1580
+ type: 'function_call_output',
1581
+ call_id: 'call_dur_1',
1582
+ output: 'Exit code: 0\nOutput:\nfile.txt',
1583
+ },
1584
+ }),
1585
+ JSON.stringify({
1586
+ timestamp: '2026-03-27T00:00:10.000Z',
1587
+ type: 'response_item',
1588
+ payload: {
1589
+ type: 'message',
1590
+ role: 'assistant',
1591
+ content: [{ type: 'output_text', text: 'Listed files.' }],
1592
+ },
1593
+ }),
1594
+ ].join('\n');
1595
+
1596
+ const messages = parseCodexSessionContent(content);
1597
+
1598
+ const assistant = messages.find(m => m.role === 'assistant');
1599
+ expect(assistant).toBeDefined();
1600
+ expect(assistant!.durationSeconds).toBe(10);
1601
+ });
1602
+ });
1603
+
1604
+ describe('parseCodexSessionContent - server turn-ID exposure', () => {
1605
+ it('sets userMessageId on parsed user message when task_started has turn_id', () => {
1606
+ const content = [
1607
+ JSON.stringify({
1608
+ timestamp: '2026-03-27T00:00:00.000Z',
1609
+ type: 'event_msg',
1610
+ payload: { type: 'task_started', turn_id: '019d-uuid-turn-1' },
1611
+ }),
1612
+ JSON.stringify({
1613
+ timestamp: '2026-03-27T00:00:00.500Z',
1614
+ type: 'response_item',
1615
+ payload: {
1616
+ type: 'message',
1617
+ role: 'user',
1618
+ content: [{ type: 'input_text', text: 'Hello' }],
1619
+ },
1620
+ }),
1621
+ JSON.stringify({
1622
+ timestamp: '2026-03-27T00:00:01.000Z',
1623
+ type: 'response_item',
1624
+ payload: {
1625
+ type: 'message',
1626
+ role: 'assistant',
1627
+ content: [{ type: 'output_text', text: 'Hi there!' }],
1628
+ },
1629
+ }),
1630
+ JSON.stringify({
1631
+ timestamp: '2026-03-27T00:00:02.000Z',
1632
+ type: 'event_msg',
1633
+ payload: { type: 'task_complete', turn_id: '019d-uuid-turn-1' },
1634
+ }),
1635
+ ].join('\n');
1636
+
1637
+ const messages = parseCodexSessionContent(content);
1638
+
1639
+ expect(messages).toHaveLength(2);
1640
+ expect(messages[0].userMessageId).toBe('019d-uuid-turn-1');
1641
+ });
1642
+
1643
+ it('sets assistantMessageId on the terminal non-interrupt assistant bubble', () => {
1644
+ const content = [
1645
+ JSON.stringify({
1646
+ timestamp: '2026-03-27T00:00:00.000Z',
1647
+ type: 'event_msg',
1648
+ payload: { type: 'task_started', turn_id: '019d-uuid-turn-1' },
1649
+ }),
1650
+ JSON.stringify({
1651
+ timestamp: '2026-03-27T00:00:00.500Z',
1652
+ type: 'response_item',
1653
+ payload: {
1654
+ type: 'message',
1655
+ role: 'user',
1656
+ content: [{ type: 'input_text', text: 'Hello' }],
1657
+ },
1658
+ }),
1659
+ JSON.stringify({
1660
+ timestamp: '2026-03-27T00:00:01.000Z',
1661
+ type: 'response_item',
1662
+ payload: {
1663
+ type: 'message',
1664
+ role: 'assistant',
1665
+ content: [{ type: 'output_text', text: 'Hi there!' }],
1666
+ },
1667
+ }),
1668
+ JSON.stringify({
1669
+ timestamp: '2026-03-27T00:00:02.000Z',
1670
+ type: 'event_msg',
1671
+ payload: { type: 'task_complete', turn_id: '019d-uuid-turn-1' },
1672
+ }),
1673
+ ].join('\n');
1674
+
1675
+ const messages = parseCodexSessionContent(content);
1676
+
1677
+ expect(messages).toHaveLength(2);
1678
+ expect(messages[1].assistantMessageId).toBe('019d-uuid-turn-1');
1679
+ });
1680
+
1681
+ it('does NOT set assistantMessageId on interrupted assistant bubbles', () => {
1682
+ const content = [
1683
+ JSON.stringify({
1684
+ timestamp: '2026-03-27T00:00:00.000Z',
1685
+ type: 'event_msg',
1686
+ payload: { type: 'task_started', turn_id: '019d-uuid-aborted' },
1687
+ }),
1688
+ JSON.stringify({
1689
+ timestamp: '2026-03-27T00:00:01.000Z',
1690
+ type: 'event_msg',
1691
+ payload: { type: 'agent_message', message: 'Starting...' },
1692
+ }),
1693
+ JSON.stringify({
1694
+ timestamp: '2026-03-27T00:00:02.000Z',
1695
+ type: 'event_msg',
1696
+ payload: { type: 'turn_aborted' },
1697
+ }),
1698
+ ].join('\n');
1699
+
1700
+ const messages = parseCodexSessionContent(content);
1701
+
1702
+ expect(messages).toHaveLength(1);
1703
+ expect(messages[0].isInterrupt).toBe(true);
1704
+ expect(messages[0].assistantMessageId).toBeUndefined();
1705
+ });
1706
+
1707
+ it('sets assistantMessageId on the last non-interrupt bubble in a multi-bubble turn', () => {
1708
+ const content = [
1709
+ JSON.stringify({
1710
+ timestamp: '2026-03-27T00:00:00.000Z',
1711
+ type: 'event_msg',
1712
+ payload: { type: 'task_started', turn_id: '019d-uuid-multi' },
1713
+ }),
1714
+ JSON.stringify({
1715
+ timestamp: '2026-03-27T00:00:00.500Z',
1716
+ type: 'response_item',
1717
+ payload: {
1718
+ type: 'message',
1719
+ role: 'user',
1720
+ content: [{ type: 'input_text', text: 'Check files.' }],
1721
+ },
1722
+ }),
1723
+ JSON.stringify({
1724
+ timestamp: '2026-03-27T00:00:01.000Z',
1725
+ type: 'response_item',
1726
+ payload: {
1727
+ type: 'function_call',
1728
+ name: 'exec_command',
1729
+ arguments: '{"command":"ls"}',
1730
+ call_id: 'call_multi_1',
1731
+ },
1732
+ }),
1733
+ JSON.stringify({
1734
+ timestamp: '2026-03-27T00:00:02.000Z',
1735
+ type: 'response_item',
1736
+ payload: {
1737
+ type: 'function_call_output',
1738
+ call_id: 'call_multi_1',
1739
+ output: 'Exit code: 0\nOutput:\nfile.txt',
1740
+ },
1741
+ }),
1742
+ JSON.stringify({
1743
+ timestamp: '2026-03-27T00:00:03.000Z',
1744
+ type: 'response_item',
1745
+ payload: {
1746
+ type: 'message',
1747
+ role: 'assistant',
1748
+ content: [{ type: 'output_text', text: 'Found files.' }],
1749
+ },
1750
+ }),
1751
+ JSON.stringify({
1752
+ timestamp: '2026-03-27T00:00:04.000Z',
1753
+ type: 'event_msg',
1754
+ payload: { type: 'task_complete', turn_id: '019d-uuid-multi' },
1755
+ }),
1756
+ ].join('\n');
1757
+
1758
+ const messages = parseCodexSessionContent(content);
1759
+
1760
+ // user + assistant (single bubble with tool call and text)
1761
+ const userMsg = messages.find(m => m.role === 'user');
1762
+ expect(userMsg!.userMessageId).toBe('019d-uuid-multi');
1763
+
1764
+ // The last assistant message in the turn should get the checkpoint
1765
+ const assistantMsgs = messages.filter(m => m.role === 'assistant');
1766
+ const lastAssistant = assistantMsgs[assistantMsgs.length - 1];
1767
+ expect(lastAssistant.assistantMessageId).toBe('019d-uuid-multi');
1768
+ });
1769
+
1770
+ it('works without task_started (no server turn ID)', () => {
1771
+ const content = [
1772
+ JSON.stringify({
1773
+ timestamp: '2026-03-27T00:00:00.000Z',
1774
+ type: 'response_item',
1775
+ payload: {
1776
+ type: 'message',
1777
+ role: 'user',
1778
+ content: [{ type: 'input_text', text: 'Hello' }],
1779
+ },
1780
+ }),
1781
+ JSON.stringify({
1782
+ timestamp: '2026-03-27T00:00:01.000Z',
1783
+ type: 'response_item',
1784
+ payload: {
1785
+ type: 'message',
1786
+ role: 'assistant',
1787
+ content: [{ type: 'output_text', text: 'Hi!' }],
1788
+ },
1789
+ }),
1790
+ ].join('\n');
1791
+
1792
+ const messages = parseCodexSessionContent(content);
1793
+
1794
+ expect(messages).toHaveLength(2);
1795
+ expect(messages[0].userMessageId).toBeUndefined();
1796
+ expect(messages[1].assistantMessageId).toBeUndefined();
1797
+ });
1798
+ });
1799
+
1800
+ describe('parseCodexSessionTurns - turn-aware parsing', () => {
1801
+ it('returns structured turns with stable turn IDs and messages', () => {
1802
+ const content = [
1803
+ JSON.stringify({
1804
+ timestamp: '2026-03-27T00:00:00.000Z',
1805
+ type: 'event_msg',
1806
+ payload: { type: 'task_started', turn_id: 'uuid-turn-1' },
1807
+ }),
1808
+ JSON.stringify({
1809
+ timestamp: '2026-03-27T00:00:00.500Z',
1810
+ type: 'response_item',
1811
+ payload: {
1812
+ type: 'message',
1813
+ role: 'user',
1814
+ content: [{ type: 'input_text', text: 'First question' }],
1815
+ },
1816
+ }),
1817
+ JSON.stringify({
1818
+ timestamp: '2026-03-27T00:00:01.000Z',
1819
+ type: 'response_item',
1820
+ payload: {
1821
+ type: 'message',
1822
+ role: 'assistant',
1823
+ content: [{ type: 'output_text', text: 'First answer' }],
1824
+ },
1825
+ }),
1826
+ JSON.stringify({
1827
+ timestamp: '2026-03-27T00:00:02.000Z',
1828
+ type: 'event_msg',
1829
+ payload: { type: 'task_complete', turn_id: 'uuid-turn-1' },
1830
+ }),
1831
+ JSON.stringify({
1832
+ timestamp: '2026-03-27T00:00:03.000Z',
1833
+ type: 'event_msg',
1834
+ payload: { type: 'task_started', turn_id: 'uuid-turn-2' },
1835
+ }),
1836
+ JSON.stringify({
1837
+ timestamp: '2026-03-27T00:00:03.500Z',
1838
+ type: 'response_item',
1839
+ payload: {
1840
+ type: 'message',
1841
+ role: 'user',
1842
+ content: [{ type: 'input_text', text: 'Second question' }],
1843
+ },
1844
+ }),
1845
+ JSON.stringify({
1846
+ timestamp: '2026-03-27T00:00:04.000Z',
1847
+ type: 'response_item',
1848
+ payload: {
1849
+ type: 'message',
1850
+ role: 'assistant',
1851
+ content: [{ type: 'output_text', text: 'Second answer' }],
1852
+ },
1853
+ }),
1854
+ JSON.stringify({
1855
+ timestamp: '2026-03-27T00:00:05.000Z',
1856
+ type: 'event_msg',
1857
+ payload: { type: 'task_complete', turn_id: 'uuid-turn-2' },
1858
+ }),
1859
+ ].join('\n');
1860
+
1861
+ const turns = parseCodexSessionTurns(content);
1862
+
1863
+ expect(turns).toHaveLength(2);
1864
+ expect(turns[0].turnId).toBe('uuid-turn-1');
1865
+ expect(turns[0].messages).toHaveLength(2);
1866
+ expect(turns[0].messages[0].role).toBe('user');
1867
+ expect(turns[0].messages[1].role).toBe('assistant');
1868
+
1869
+ expect(turns[1].turnId).toBe('uuid-turn-2');
1870
+ expect(turns[1].messages).toHaveLength(2);
1871
+ });
1872
+
1873
+ it('parseCodexSessionFile still works (uses parseCodexSessionTurns internally)', () => {
1874
+ const content = [
1875
+ JSON.stringify({
1876
+ timestamp: '2026-03-27T00:00:00.000Z',
1877
+ type: 'event_msg',
1878
+ payload: { type: 'task_started', turn_id: 'uuid-turn-flat' },
1879
+ }),
1880
+ JSON.stringify({
1881
+ timestamp: '2026-03-27T00:00:00.500Z',
1882
+ type: 'response_item',
1883
+ payload: {
1884
+ type: 'message',
1885
+ role: 'user',
1886
+ content: [{ type: 'input_text', text: 'Hello' }],
1887
+ },
1888
+ }),
1889
+ JSON.stringify({
1890
+ timestamp: '2026-03-27T00:00:01.000Z',
1891
+ type: 'response_item',
1892
+ payload: {
1893
+ type: 'message',
1894
+ role: 'assistant',
1895
+ content: [{ type: 'output_text', text: 'Hi!' }],
1896
+ },
1897
+ }),
1898
+ JSON.stringify({
1899
+ timestamp: '2026-03-27T00:00:02.000Z',
1900
+ type: 'event_msg',
1901
+ payload: { type: 'task_complete', turn_id: 'uuid-turn-flat' },
1902
+ }),
1903
+ ].join('\n');
1904
+
1905
+ const messages = parseCodexSessionContent(content);
1906
+
1907
+ expect(messages).toHaveLength(2);
1908
+ expect(messages[0].role).toBe('user');
1909
+ expect(messages[1].role).toBe('assistant');
1910
+ });
1911
+
1912
+ it('preserves legacy item content inside mixed modern transcripts', () => {
1913
+ const content = [
1914
+ JSON.stringify({
1915
+ timestamp: '2026-03-27T00:00:00.000Z',
1916
+ type: 'event_msg',
1917
+ payload: { type: 'task_started', turn_id: 'uuid-turn-mixed' },
1918
+ }),
1919
+ JSON.stringify({
1920
+ timestamp: '2026-03-27T00:00:00.100Z',
1921
+ type: 'event',
1922
+ event: {
1923
+ type: 'item.updated',
1924
+ item: {
1925
+ id: 'legacy-msg-1',
1926
+ type: 'agent_message',
1927
+ text: 'Legacy streamed answer',
1928
+ },
1929
+ },
1930
+ }),
1931
+ JSON.stringify({
1932
+ timestamp: '2026-03-27T00:00:00.200Z',
1933
+ type: 'event',
1934
+ event: {
1935
+ type: 'item.started',
1936
+ item: {
1937
+ id: 'legacy-cmd-1',
1938
+ type: 'command_execution',
1939
+ command: 'pwd',
1940
+ },
1941
+ },
1942
+ }),
1943
+ JSON.stringify({
1944
+ timestamp: '2026-03-27T00:00:00.300Z',
1945
+ type: 'event',
1946
+ event: {
1947
+ type: 'item.completed',
1948
+ item: {
1949
+ id: 'legacy-cmd-1',
1950
+ type: 'command_execution',
1951
+ aggregated_output: '/workspace',
1952
+ exit_code: 0,
1953
+ },
1954
+ },
1955
+ }),
1956
+ JSON.stringify({
1957
+ timestamp: '2026-03-27T00:00:01.000Z',
1958
+ type: 'event_msg',
1959
+ payload: { type: 'task_complete', turn_id: 'uuid-turn-mixed' },
1960
+ }),
1961
+ ].join('\n');
1962
+
1963
+ const turns = parseCodexSessionTurns(content);
1964
+
1965
+ expect(turns).toHaveLength(1);
1966
+ expect(turns[0].turnId).toBe('uuid-turn-mixed');
1967
+ expect(turns[0].messages).toHaveLength(1);
1968
+ expect(turns[0].messages[0]).toMatchObject({
1969
+ role: 'assistant',
1970
+ content: 'Legacy streamed answer',
1971
+ });
1972
+ expect(turns[0].messages[0].toolCalls).toEqual([
1973
+ expect.objectContaining({
1974
+ id: 'legacy-cmd-1',
1975
+ name: 'Bash',
1976
+ status: 'completed',
1977
+ result: '/workspace',
1978
+ }),
1979
+ ]);
1980
+ });
1981
+ });
1982
+
1983
+ describe('parseCodexSessionContent - persisted mcp_tool_call', () => {
1984
+ it('restores mcp_tool_call from response_item as mcp__server__tool', () => {
1985
+ const content = [
1986
+ JSON.stringify({
1987
+ timestamp: '2026-03-27T00:00:00.000Z',
1988
+ type: 'response_item',
1989
+ payload: {
1990
+ type: 'message',
1991
+ role: 'user',
1992
+ content: [{ type: 'input_text', text: 'Use MCP tool.' }],
1993
+ },
1994
+ }),
1995
+ JSON.stringify({
1996
+ timestamp: '2026-03-27T00:00:01.000Z',
1997
+ type: 'response_item',
1998
+ payload: {
1999
+ type: 'mcp_tool_call',
2000
+ server: 'myserver',
2001
+ tool: 'mytool',
2002
+ call_id: 'call_mcp_1',
2003
+ status: 'completed',
2004
+ },
2005
+ }),
2006
+ JSON.stringify({
2007
+ timestamp: '2026-03-27T00:00:02.000Z',
2008
+ type: 'response_item',
2009
+ payload: {
2010
+ type: 'message',
2011
+ role: 'assistant',
2012
+ content: [{ type: 'output_text', text: 'MCP tool executed.' }],
2013
+ },
2014
+ }),
2015
+ ].join('\n');
2016
+
2017
+ const messages = parseCodexSessionContent(content);
2018
+
2019
+ const assistant = messages.find(m => m.role === 'assistant' && m.toolCalls);
2020
+ expect(assistant).toBeDefined();
2021
+
2022
+ const mcpTool = assistant!.toolCalls!.find(tc => tc.name === 'mcp__myserver__mytool');
2023
+ expect(mcpTool).toBeDefined();
2024
+ expect(mcpTool!.id).toBe('call_mcp_1');
2025
+ expect(mcpTool!.status).toBe('completed');
2026
+ });
2027
+
2028
+ it('preserves MCP arguments and structured result text', () => {
2029
+ const content = [
2030
+ JSON.stringify({
2031
+ timestamp: '2026-03-27T00:00:00.000Z',
2032
+ type: 'response_item',
2033
+ payload: {
2034
+ type: 'mcp_tool_call',
2035
+ server: 'filesystem',
2036
+ tool: 'read_file',
2037
+ call_id: 'call_mcp_2',
2038
+ status: 'completed',
2039
+ arguments: { path: 'README.md' },
2040
+ result: {
2041
+ content: [
2042
+ { type: 'text', text: 'line 1' },
2043
+ { type: 'text', text: 'line 2' },
2044
+ ],
2045
+ },
2046
+ },
2047
+ }),
2048
+ ].join('\n');
2049
+
2050
+ const messages = parseCodexSessionContent(content);
2051
+ const assistant = messages.find(m => m.role === 'assistant' && m.toolCalls);
2052
+ const mcpTool = assistant!.toolCalls!.find(tc => tc.id === 'call_mcp_2');
2053
+
2054
+ expect(mcpTool).toMatchObject({
2055
+ name: 'mcp__filesystem__read_file',
2056
+ input: { path: 'README.md' },
2057
+ status: 'completed',
2058
+ result: 'line 1\nline 2',
2059
+ });
2060
+ });
2061
+
2062
+ it('preserves MCP error output', () => {
2063
+ const content = [
2064
+ JSON.stringify({
2065
+ timestamp: '2026-03-27T00:00:00.000Z',
2066
+ type: 'response_item',
2067
+ payload: {
2068
+ type: 'mcp_tool_call',
2069
+ server: 'filesystem',
2070
+ tool: 'write_file',
2071
+ call_id: 'call_mcp_3',
2072
+ status: 'failed',
2073
+ arguments: '{"path":"README.md"}',
2074
+ error: 'Permission denied',
2075
+ },
2076
+ }),
2077
+ ].join('\n');
2078
+
2079
+ const messages = parseCodexSessionContent(content);
2080
+ const assistant = messages.find(m => m.role === 'assistant' && m.toolCalls);
2081
+ const mcpTool = assistant!.toolCalls!.find(tc => tc.id === 'call_mcp_3');
2082
+
2083
+ expect(mcpTool).toMatchObject({
2084
+ name: 'mcp__filesystem__write_file',
2085
+ input: { path: 'README.md' },
2086
+ status: 'error',
2087
+ result: 'Permission denied',
2088
+ });
2089
+ });
2090
+ });
2091
+
2092
+ describe('parseCodexSessionContent - context_compacted boundary', () => {
2093
+ it('applies compacted replacement_history before rendering the compact boundary', () => {
2094
+ const content = [
2095
+ JSON.stringify({ timestamp: '2026-03-03T16:00:00.000Z', type: 'event_msg', payload: { type: 'task_started' } }),
2096
+ JSON.stringify({ timestamp: '2026-03-03T16:00:01.000Z', type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hello' }] } }),
2097
+ JSON.stringify({ timestamp: '2026-03-03T16:00:02.000Z', type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Hi there!' }] } }),
2098
+ JSON.stringify({ timestamp: '2026-03-03T16:00:03.000Z', type: 'event_msg', payload: { type: 'task_complete' } }),
2099
+ // Compaction happens here
2100
+ JSON.stringify({
2101
+ timestamp: '2026-03-03T16:00:04.000Z',
2102
+ type: 'compacted',
2103
+ payload: {
2104
+ message: '',
2105
+ replacement_history: [
2106
+ {
2107
+ type: 'message',
2108
+ role: 'user',
2109
+ content: [{ type: 'input_text', text: '<COMPACTION_SUMMARY>\nSummary after compact' }],
2110
+ },
2111
+ {
2112
+ type: 'compaction',
2113
+ encrypted_content: 'encrypted-summary',
2114
+ },
2115
+ ],
2116
+ },
2117
+ }),
2118
+ JSON.stringify({ timestamp: '2026-03-03T16:00:04.000Z', type: 'event_msg', payload: { type: 'context_compacted' } }),
2119
+ // Next turn after compaction
2120
+ JSON.stringify({ timestamp: '2026-03-03T16:00:05.000Z', type: 'event_msg', payload: { type: 'task_started' } }),
2121
+ JSON.stringify({ timestamp: '2026-03-03T16:00:06.000Z', type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'continue' }] } }),
2122
+ JSON.stringify({ timestamp: '2026-03-03T16:00:07.000Z', type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Continuing after compact.' }] } }),
2123
+ JSON.stringify({ timestamp: '2026-03-03T16:00:08.000Z', type: 'event_msg', payload: { type: 'task_complete' } }),
2124
+ ].join('\n');
2125
+
2126
+ const messages = parseCodexSessionContent(content);
2127
+
2128
+ expect(messages.map(m => m.content)).not.toContain('hello');
2129
+ expect(messages.map(m => m.content)).not.toContain('Hi there!');
2130
+ expect(messages[0]).toMatchObject({
2131
+ role: 'user',
2132
+ content: '<COMPACTION_SUMMARY>\nSummary after compact',
2133
+ });
2134
+
2135
+ const compactMsg = messages.find(m =>
2136
+ m.contentBlocks?.some(b => b.type === 'context_compacted'),
2137
+ );
2138
+ expect(compactMsg).toBeDefined();
2139
+ expect(compactMsg!.role).toBe('assistant');
2140
+ expect(compactMsg!.content).toBe('');
2141
+
2142
+ // context_compacted should appear after the compacted replacement history
2143
+ const compactIdx = messages.indexOf(compactMsg!);
2144
+ expect(compactIdx).toBeGreaterThan(0);
2145
+
2146
+ const beforeCompact = messages[compactIdx - 1];
2147
+ expect(beforeCompact.role).toBe('user');
2148
+ expect(beforeCompact.content).toBe('<COMPACTION_SUMMARY>\nSummary after compact');
2149
+
2150
+ const afterCompact = messages[compactIdx + 1];
2151
+ expect(afterCompact.role).toBe('user');
2152
+ expect(afterCompact.content).toContain('continue');
2153
+ });
2154
+
2155
+ it('uses the latest compacted replacement_history when multiple compactions occur', () => {
2156
+ const content = [
2157
+ JSON.stringify({
2158
+ timestamp: '2026-03-03T16:00:00.000Z',
2159
+ type: 'compacted',
2160
+ payload: {
2161
+ message: '',
2162
+ replacement_history: [
2163
+ {
2164
+ type: 'message',
2165
+ role: 'user',
2166
+ content: [{ type: 'input_text', text: 'First summary' }],
2167
+ },
2168
+ ],
2169
+ },
2170
+ }),
2171
+ JSON.stringify({ timestamp: '2026-03-03T16:00:00.000Z', type: 'event_msg', payload: { type: 'context_compacted' } }),
2172
+ JSON.stringify({
2173
+ timestamp: '2026-03-03T16:00:01.000Z',
2174
+ type: 'compacted',
2175
+ payload: {
2176
+ message: '',
2177
+ replacement_history: [
2178
+ {
2179
+ type: 'message',
2180
+ role: 'user',
2181
+ content: [{ type: 'input_text', text: 'Second summary' }],
2182
+ },
2183
+ ],
2184
+ },
2185
+ }),
2186
+ JSON.stringify({ timestamp: '2026-03-03T16:00:01.000Z', type: 'event_msg', payload: { type: 'context_compacted' } }),
2187
+ ].join('\n');
2188
+
2189
+ const messages = parseCodexSessionContent(content);
2190
+
2191
+ expect(messages).toHaveLength(2);
2192
+ expect(messages[0]).toMatchObject({
2193
+ role: 'user',
2194
+ content: 'Second summary',
2195
+ });
2196
+ const compactMessages = messages.filter(m =>
2197
+ m.contentBlocks?.some(b => b.type === 'context_compacted'),
2198
+ );
2199
+ expect(compactMessages).toHaveLength(1);
2200
+ });
2201
+ });
2202
+ });