@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,1672 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ import type { ChatMessage, ContentBlock, ToolCallInfo } from '../../../core/types';
6
+ import { extractUserDisplayContent } from '../../../utils/context';
7
+ import {
8
+ isCodexToolOutputError,
9
+ normalizeCodexMcpToolInput,
10
+ normalizeCodexMcpToolName,
11
+ normalizeCodexMcpToolState,
12
+ normalizeCodexToolInput,
13
+ normalizeCodexToolName,
14
+ normalizeCodexToolResult,
15
+ parseCodexArguments,
16
+ } from '../normalization/codexToolNormalization';
17
+
18
+ interface CodexEvent {
19
+ type: string;
20
+ thread_id?: string;
21
+ item?: CodexItem;
22
+ usage?: { input_tokens: number; cached_input_tokens: number; output_tokens: number };
23
+ error?: { message: string };
24
+ message?: string;
25
+ }
26
+
27
+ interface CodexItem {
28
+ id: string;
29
+ type: string;
30
+ text?: string;
31
+ command?: string;
32
+ aggregated_output?: string;
33
+ exit_code?: number;
34
+ status?: string;
35
+ changes?: Array<{ path: string; kind: string }>;
36
+ query?: string;
37
+ message?: string;
38
+ server?: string;
39
+ tool?: string;
40
+ }
41
+
42
+ interface PersistedMessagePart {
43
+ type?: string;
44
+ text?: string;
45
+ }
46
+
47
+ interface PersistedMessagePayload {
48
+ type: 'message';
49
+ role?: string;
50
+ content?: PersistedMessagePart[];
51
+ }
52
+
53
+ interface PersistedReasoningPayload {
54
+ type: 'reasoning';
55
+ summary?: Array<{ type?: string; text?: string } | string>;
56
+ content?: Array<{ type?: string; text?: string } | string>;
57
+ text?: string;
58
+ }
59
+
60
+ interface PersistedToolCallPayload {
61
+ type: 'function_call' | 'custom_tool_call';
62
+ name?: string;
63
+ arguments?: string;
64
+ call_id?: string;
65
+ input?: string;
66
+ }
67
+
68
+ interface PersistedToolCallOutputPayload {
69
+ type: 'function_call_output' | 'custom_tool_call_output';
70
+ call_id?: string;
71
+ output?: string | unknown[];
72
+ }
73
+
74
+ interface PersistedWebSearchCallPayload {
75
+ type: 'web_search_call';
76
+ action?: {
77
+ type?: string;
78
+ query?: string;
79
+ queries?: string[];
80
+ url?: string;
81
+ pattern?: string;
82
+ };
83
+ status?: string;
84
+ call_id?: string;
85
+ }
86
+
87
+ interface PersistedMcpToolCallPayload {
88
+ type: 'mcp_tool_call';
89
+ server?: string;
90
+ tool?: string;
91
+ call_id?: string;
92
+ status?: string;
93
+ arguments?: string | Record<string, unknown>;
94
+ result?: { content?: Array<{ type?: string; text?: string }> } | null;
95
+ error?: string | null;
96
+ duration_ms?: number | null;
97
+ }
98
+
99
+ interface PersistedEventPayload {
100
+ type?: string;
101
+ text?: string;
102
+ message?: string;
103
+ }
104
+
105
+ interface PersistedCompactionPayload {
106
+ type: 'compaction';
107
+ encrypted_content?: string;
108
+ }
109
+
110
+ interface PersistedCompactedPayload {
111
+ message?: string;
112
+ replacement_history?: PersistedPayload[];
113
+ }
114
+
115
+ interface ParsedSessionRecord {
116
+ timestamp: number;
117
+ type?: string;
118
+ event?: CodexEvent;
119
+ payload?: PersistedPayload;
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Multi-bubble turn model
124
+ // ---------------------------------------------------------------------------
125
+
126
+ interface CodexAssistantBubble {
127
+ contentChunks: string[];
128
+ thinkingChunks: string[];
129
+ toolCalls: ToolCallInfo[];
130
+ toolIndexesById: Map<string, number>;
131
+ contentBlocks: ContentBlock[];
132
+ startedAt: number;
133
+ lastEventAt: number;
134
+ interrupted: boolean;
135
+ }
136
+
137
+ interface CodexTurnState {
138
+ id: string;
139
+ serverTurnId?: string;
140
+ startedAt: number;
141
+ completedAt?: number;
142
+ completed?: boolean;
143
+ lastEventAt: number;
144
+ userTimestamp?: number;
145
+ userChunks: string[];
146
+ assistantBubbles: CodexAssistantBubble[];
147
+ activeBubbleIndex: number | null;
148
+ }
149
+
150
+ type PersistedPayload =
151
+ | PersistedMessagePayload
152
+ | PersistedReasoningPayload
153
+ | PersistedToolCallPayload
154
+ | PersistedToolCallOutputPayload
155
+ | PersistedWebSearchCallPayload
156
+ | PersistedMcpToolCallPayload
157
+ | PersistedCompactionPayload
158
+ | PersistedEventPayload
159
+ | undefined;
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Turn/bubble lifecycle helpers
163
+ // ---------------------------------------------------------------------------
164
+
165
+ function newBubble(timestamp: number): CodexAssistantBubble {
166
+ return {
167
+ contentChunks: [],
168
+ thinkingChunks: [],
169
+ toolCalls: [],
170
+ toolIndexesById: new Map(),
171
+ contentBlocks: [],
172
+ startedAt: timestamp,
173
+ lastEventAt: timestamp,
174
+ interrupted: false,
175
+ };
176
+ }
177
+
178
+ function newTurnState(id: string, timestamp: number): CodexTurnState {
179
+ return {
180
+ id,
181
+ startedAt: timestamp,
182
+ lastEventAt: timestamp,
183
+ userChunks: [],
184
+ assistantBubbles: [],
185
+ activeBubbleIndex: null,
186
+ };
187
+ }
188
+
189
+ function createPersistedParseContext(): PersistedParseContext {
190
+ return {
191
+ turns: new Map(),
192
+ turnOrder: [],
193
+ currentTurnId: null,
194
+ toolCallToTurn: new Map(),
195
+ suppressedToolOutputIds: new Set(),
196
+ terminalSessionToCommandId: new Map(),
197
+ stdinCallToCommandId: new Map(),
198
+ turnCounter: 0,
199
+ };
200
+ }
201
+
202
+ function ensureTurn(
203
+ turns: Map<string, CodexTurnState>,
204
+ turnOrder: string[],
205
+ preferredTurnId: string,
206
+ currentTurnId: string | null,
207
+ timestamp: number,
208
+ ): CodexTurnState {
209
+ const id = currentTurnId ?? preferredTurnId;
210
+ const existing = turns.get(id);
211
+ if (existing) {
212
+ if (timestamp > 0 && timestamp > existing.lastEventAt) {
213
+ existing.lastEventAt = timestamp;
214
+ }
215
+ return existing;
216
+ }
217
+
218
+ const turn = newTurnState(id, timestamp);
219
+ turns.set(id, turn);
220
+ turnOrder.push(id);
221
+ return turn;
222
+ }
223
+
224
+ function ensureAssistantBubble(turn: CodexTurnState, timestamp: number): CodexAssistantBubble {
225
+ if (turn.activeBubbleIndex !== null) {
226
+ const bubble = turn.assistantBubbles[turn.activeBubbleIndex];
227
+ if (timestamp > 0 && timestamp > bubble.lastEventAt) {
228
+ bubble.lastEventAt = timestamp;
229
+ }
230
+ return bubble;
231
+ }
232
+
233
+ const bubble = newBubble(timestamp);
234
+ turn.assistantBubbles.push(bubble);
235
+ turn.activeBubbleIndex = turn.assistantBubbles.length - 1;
236
+ return bubble;
237
+ }
238
+
239
+ function closeAssistantBubble(turn: CodexTurnState): void {
240
+ turn.activeBubbleIndex = null;
241
+ }
242
+
243
+ function pushToolInvocation(bubble: CodexAssistantBubble, toolCall: ToolCallInfo): void {
244
+ const existingIndex = bubble.toolIndexesById.get(toolCall.id);
245
+ if (existingIndex !== undefined) {
246
+ bubble.toolCalls[existingIndex] = toolCall;
247
+ return;
248
+ }
249
+
250
+ bubble.toolIndexesById.set(toolCall.id, bubble.toolCalls.length);
251
+ bubble.toolCalls.push(toolCall);
252
+ bubble.contentBlocks.push({ type: 'tool_use', toolId: toolCall.id });
253
+ }
254
+
255
+ function appendUniqueChunk(chunks: string[], value: string): void {
256
+ const trimmed = value.trim();
257
+ if (!trimmed) return;
258
+ if (chunks[chunks.length - 1] === trimmed) return;
259
+ chunks.push(trimmed);
260
+ }
261
+
262
+ function replaceLatestChunk(chunks: string[], value: string): void {
263
+ const trimmed = value.trim();
264
+ if (!trimmed) return;
265
+ chunks.length = 0;
266
+ chunks.push(trimmed);
267
+ }
268
+
269
+ function appendUserChunk(turn: CodexTurnState, value: string, timestamp: number): void {
270
+ const chunkCountBefore = turn.userChunks.length;
271
+ appendUniqueChunk(turn.userChunks, value);
272
+
273
+ if (turn.userChunks.length > chunkCountBefore && !turn.userTimestamp && timestamp > 0) {
274
+ turn.userTimestamp = timestamp;
275
+ }
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Legacy TurnAccumulator — kept for the `event` wrapper format
280
+ // ---------------------------------------------------------------------------
281
+
282
+ interface TurnAccumulator {
283
+ assistantText: string;
284
+ thinkingText: string;
285
+ toolCalls: ToolCallInfo[];
286
+ contentBlocks: ContentBlock[];
287
+ interrupted: boolean;
288
+ timestamp: number;
289
+ }
290
+
291
+ function newTurn(timestamp = 0): TurnAccumulator {
292
+ return {
293
+ assistantText: '',
294
+ thinkingText: '',
295
+ toolCalls: [],
296
+ contentBlocks: [],
297
+ interrupted: false,
298
+ timestamp,
299
+ };
300
+ }
301
+
302
+ function flushTurn(turn: TurnAccumulator, messages: ChatMessage[], msgIndex: number): number {
303
+ if (
304
+ !turn.assistantText &&
305
+ !turn.thinkingText &&
306
+ turn.toolCalls.length === 0
307
+ ) {
308
+ return msgIndex;
309
+ }
310
+
311
+ const msg: ChatMessage = {
312
+ id: `codex-msg-${msgIndex}`,
313
+ role: 'assistant',
314
+ content: turn.assistantText,
315
+ timestamp: turn.timestamp || Date.now(),
316
+ toolCalls: turn.toolCalls.length > 0 ? turn.toolCalls : undefined,
317
+ contentBlocks: turn.contentBlocks.length > 0 ? turn.contentBlocks : undefined,
318
+ };
319
+
320
+ if (turn.interrupted) {
321
+ msg.isInterrupt = true;
322
+ }
323
+
324
+ messages.push(msg);
325
+ return msgIndex + 1;
326
+ }
327
+
328
+ function setTextBlock(turn: TurnAccumulator, content: string): void {
329
+ const index = turn.contentBlocks.findIndex(block => block.type === 'text');
330
+ if (index === -1) {
331
+ turn.contentBlocks.push({ type: 'text', content });
332
+ return;
333
+ }
334
+
335
+ turn.contentBlocks[index] = { type: 'text', content };
336
+ }
337
+
338
+ function setThinkingBlock(turn: TurnAccumulator, content: string): void {
339
+ const normalized = content.trim();
340
+ if (!normalized) {
341
+ return;
342
+ }
343
+
344
+ turn.thinkingText = normalized;
345
+
346
+ const index = turn.contentBlocks.findIndex(block => block.type === 'thinking');
347
+ if (index === -1) {
348
+ turn.contentBlocks.push({ type: 'thinking', content: normalized });
349
+ return;
350
+ }
351
+
352
+ turn.contentBlocks[index] = { type: 'thinking', content: normalized };
353
+ }
354
+
355
+ // ---------------------------------------------------------------------------
356
+ // Shared helpers
357
+ // ---------------------------------------------------------------------------
358
+
359
+ function parseTimestamp(value: unknown): number {
360
+ if (typeof value !== 'string') {
361
+ return 0;
362
+ }
363
+
364
+ const parsed = Date.parse(value);
365
+ return Number.isFinite(parsed) ? parsed : 0;
366
+ }
367
+
368
+ function parseSessionRecord(line: string): ParsedSessionRecord | null {
369
+ let parsed: {
370
+ timestamp?: string;
371
+ type?: string;
372
+ event?: CodexEvent;
373
+ payload?: PersistedPayload;
374
+ };
375
+
376
+ try {
377
+ parsed = JSON.parse(line) as typeof parsed;
378
+ } catch {
379
+ return null;
380
+ }
381
+
382
+ return {
383
+ timestamp: parseTimestamp(parsed.timestamp),
384
+ type: parsed.type,
385
+ event: parsed.event,
386
+ payload: parsed.payload,
387
+ };
388
+ }
389
+
390
+ const CODEX_SYSTEM_MESSAGE_PREFIXES = [
391
+ '# AGENTS.md instructions',
392
+ ];
393
+
394
+ const CODEX_CONTROL_BLOCK_TAGS = [
395
+ 'system_instruction',
396
+ 'environment_context',
397
+ 'turn_aborted',
398
+ 'user-preferences',
399
+ 'subagent_notification',
400
+ 'skill',
401
+ ];
402
+
403
+ function stripLeadingTaggedBlock(text: string, tagName: string): string | null {
404
+ const openTag = `<${tagName}>`;
405
+ if (!text.startsWith(openTag)) {
406
+ return null;
407
+ }
408
+
409
+ const closeTag = `</${tagName}>`;
410
+ const closeIndex = text.indexOf(closeTag, openTag.length);
411
+ if (closeIndex === -1) {
412
+ return '';
413
+ }
414
+
415
+ return text.slice(closeIndex + closeTag.length);
416
+ }
417
+
418
+ function stripLeadingCodexControlBlocks(text: string): string {
419
+ let remaining = text.trimStart();
420
+ let stripped = true;
421
+
422
+ while (stripped) {
423
+ stripped = false;
424
+
425
+ for (const tagName of CODEX_CONTROL_BLOCK_TAGS) {
426
+ const next = stripLeadingTaggedBlock(remaining, tagName);
427
+ if (next === null) {
428
+ continue;
429
+ }
430
+
431
+ remaining = next.trimStart();
432
+ stripped = true;
433
+ break;
434
+ }
435
+ }
436
+
437
+ return remaining;
438
+ }
439
+
440
+ function extractCodexUserVisibleText(text: string): string | null {
441
+ const trimmed = text.trimStart();
442
+ if (!trimmed) {
443
+ return null;
444
+ }
445
+
446
+ if (CODEX_SYSTEM_MESSAGE_PREFIXES.some(prefix => trimmed.startsWith(prefix))) {
447
+ return null;
448
+ }
449
+
450
+ const visible = stripLeadingCodexControlBlocks(trimmed).trim();
451
+ return visible ? visible : null;
452
+ }
453
+
454
+ function extractMessageText(content: PersistedMessagePart[] | undefined): string {
455
+ if (!Array.isArray(content)) {
456
+ return '';
457
+ }
458
+
459
+ return content
460
+ .map(part => (typeof part?.text === 'string' ? part.text : ''))
461
+ .join('');
462
+ }
463
+
464
+ function joinTextParts(parts: Array<{ text?: string } | string>): string {
465
+ return parts
466
+ .map((part) => {
467
+ if (typeof part === 'string') return part;
468
+ return typeof part?.text === 'string' ? part.text : '';
469
+ })
470
+ .map(part => part.trim())
471
+ .filter(Boolean)
472
+ .join('\n\n')
473
+ .trim();
474
+ }
475
+
476
+ function extractReasoningText(payload: PersistedReasoningPayload | PersistedEventPayload): string {
477
+ if ('summary' in payload && Array.isArray(payload.summary) && payload.summary.length > 0) {
478
+ return joinTextParts(payload.summary);
479
+ }
480
+
481
+ if ('content' in payload && Array.isArray(payload.content) && payload.content.length > 0) {
482
+ return joinTextParts(payload.content);
483
+ }
484
+
485
+ return typeof payload.text === 'string' ? payload.text.trim() : '';
486
+ }
487
+
488
+ // ---------------------------------------------------------------------------
489
+ // Legacy event wrapper processing (kept as-is)
490
+ // ---------------------------------------------------------------------------
491
+
492
+ function processLegacyItem(
493
+ eventType: string,
494
+ item: CodexItem,
495
+ turn: TurnAccumulator,
496
+ ): void {
497
+ switch (item.type) {
498
+ case 'agent_message':
499
+ if (eventType === 'item.completed' || eventType === 'item.updated') {
500
+ if (item.text) {
501
+ turn.assistantText = item.text;
502
+ setTextBlock(turn, item.text);
503
+ }
504
+ }
505
+ break;
506
+
507
+ case 'reasoning':
508
+ if (eventType === 'item.completed' || eventType === 'item.updated') {
509
+ if (item.text) {
510
+ setThinkingBlock(turn, item.text);
511
+ }
512
+ }
513
+ break;
514
+
515
+ case 'command_execution':
516
+ if (eventType === 'item.started') {
517
+ turn.toolCalls.push({
518
+ id: item.id,
519
+ name: normalizeCodexToolName(item.type),
520
+ input: normalizeCodexToolInput(item.type, { command: item.command ?? '' }),
521
+ status: 'running',
522
+ });
523
+ turn.contentBlocks.push({ type: 'tool_use', toolId: item.id });
524
+ } else if (eventType === 'item.completed') {
525
+ const tc = turn.toolCalls.find(tool => tool.id === item.id);
526
+ if (tc) {
527
+ const rawOutput = item.aggregated_output ?? '';
528
+ tc.result = normalizeCodexToolResult(tc.name, rawOutput);
529
+ tc.status = item.exit_code === 0 ? 'completed' : 'error';
530
+ }
531
+ }
532
+ break;
533
+
534
+ case 'file_change': {
535
+ const changes = item.changes ?? [];
536
+ if (eventType === 'item.started' || eventType === 'item.completed') {
537
+ const existing = turn.toolCalls.find(tool => tool.id === item.id);
538
+ if (!existing) {
539
+ const paths = changes.map(change => `${change.kind}: ${change.path}`).join(', ');
540
+ turn.toolCalls.push({
541
+ id: item.id,
542
+ name: normalizeCodexToolName('file_change'),
543
+ input: { changes },
544
+ status: item.status === 'completed' ? 'completed' : 'error',
545
+ result: paths ? `Applied: ${paths}` : 'Applied',
546
+ });
547
+ turn.contentBlocks.push({ type: 'tool_use', toolId: item.id });
548
+ } else if (eventType === 'item.completed') {
549
+ existing.status = item.status === 'completed' ? 'completed' : 'error';
550
+ }
551
+ }
552
+ break;
553
+ }
554
+
555
+ case 'web_search':
556
+ if (eventType === 'item.started') {
557
+ turn.toolCalls.push({
558
+ id: item.id,
559
+ name: normalizeCodexToolName(item.type),
560
+ input: normalizeCodexToolInput(item.type, { query: item.query ?? '' }),
561
+ status: 'running',
562
+ });
563
+ turn.contentBlocks.push({ type: 'tool_use', toolId: item.id });
564
+ } else if (eventType === 'item.completed') {
565
+ const tc = turn.toolCalls.find(tool => tool.id === item.id);
566
+ if (tc) {
567
+ tc.result = 'Search complete';
568
+ tc.status = 'completed';
569
+ }
570
+ }
571
+ break;
572
+
573
+ case 'mcp_tool_call':
574
+ if (eventType === 'item.started') {
575
+ const server = item.server ?? '';
576
+ const tool = item.tool ?? '';
577
+ turn.toolCalls.push({
578
+ id: item.id,
579
+ name: `mcp__${server}__${tool}`,
580
+ input: {},
581
+ status: 'running',
582
+ });
583
+ turn.contentBlocks.push({ type: 'tool_use', toolId: item.id });
584
+ } else if (eventType === 'item.completed') {
585
+ const tc = turn.toolCalls.find(tool => tool.id === item.id);
586
+ if (tc) {
587
+ tc.status = item.status === 'completed' ? 'completed' : 'error';
588
+ tc.result = item.status === 'completed' ? 'Completed' : 'Failed';
589
+ }
590
+ }
591
+ break;
592
+
593
+ default:
594
+ break;
595
+ }
596
+ }
597
+
598
+ // ---------------------------------------------------------------------------
599
+ // Persisted-format (response_item) processing — with bubble model
600
+ // ---------------------------------------------------------------------------
601
+
602
+ interface PersistedParseContext {
603
+ turns: Map<string, CodexTurnState>;
604
+ turnOrder: string[];
605
+ currentTurnId: string | null;
606
+ toolCallToTurn: Map<string, { turnId: string; bubbleIndex: number }>;
607
+ suppressedToolOutputIds: Set<string>;
608
+ terminalSessionToCommandId: Map<string, string>;
609
+ stdinCallToCommandId: Map<string, string>;
610
+ turnCounter: number;
611
+ }
612
+
613
+ function nextTurnId(ctx: PersistedParseContext): string {
614
+ ctx.turnCounter += 1;
615
+ return `turn-${ctx.turnCounter}`;
616
+ }
617
+
618
+ function processPersistedToolCall(
619
+ payload: PersistedToolCallPayload,
620
+ timestamp: number,
621
+ ctx: PersistedParseContext,
622
+ ): void {
623
+ const callId = payload.call_id;
624
+ if (!callId) return;
625
+
626
+ if (payload.name === 'write_stdin') {
627
+ const parsedArgs = parseCodexArguments(payload.arguments ?? payload.input);
628
+ if (isSilentWriteStdinInput(parsedArgs)) {
629
+ const terminalSessionId = readTerminalSessionIdArgument(parsedArgs);
630
+ const parentCallId = terminalSessionId
631
+ ? ctx.terminalSessionToCommandId.get(terminalSessionId)
632
+ : undefined;
633
+ if (parentCallId) {
634
+ ctx.stdinCallToCommandId.set(callId, parentCallId);
635
+ }
636
+ ctx.suppressedToolOutputIds.add(callId);
637
+ return;
638
+ }
639
+ }
640
+
641
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
642
+ const bubble = ensureAssistantBubble(turn, timestamp);
643
+
644
+ const rawArgs = payload.arguments ?? payload.input;
645
+ const parsedArgs = parseCodexArguments(rawArgs);
646
+ const normalizedName = normalizeCodexToolName(payload.name);
647
+ const normalizedInput = normalizeCodexToolInput(payload.name, parsedArgs);
648
+
649
+ const toolCall: ToolCallInfo = {
650
+ id: callId,
651
+ name: normalizedName,
652
+ input: normalizedInput,
653
+ status: 'running',
654
+ };
655
+
656
+ pushToolInvocation(bubble, toolCall);
657
+
658
+ ctx.toolCallToTurn.set(callId, {
659
+ turnId: turn.id,
660
+ bubbleIndex: turn.activeBubbleIndex!,
661
+ });
662
+ }
663
+
664
+ function processPersistedToolOutput(
665
+ payload: PersistedToolCallOutputPayload,
666
+ timestamp: number,
667
+ ctx: PersistedParseContext,
668
+ ): void {
669
+ const callId = payload.call_id;
670
+ if (!callId) return;
671
+
672
+ // output can be a string or an array (e.g. view_image returns image objects)
673
+ const rawOutput = typeof payload.output === 'string'
674
+ ? payload.output
675
+ : Array.isArray(payload.output)
676
+ ? JSON.stringify(payload.output)
677
+ : '';
678
+
679
+ const parentCommandId = ctx.stdinCallToCommandId.get(callId);
680
+ if (parentCommandId) {
681
+ const parentToolCall = findPersistedToolCallById(ctx, parentCommandId);
682
+ if (parentToolCall) {
683
+ applyPersistedToolOutput(parentToolCall, payload.output, rawOutput, ctx, {
684
+ allowImplicitCommandCompletion: false,
685
+ });
686
+ }
687
+ ctx.stdinCallToCommandId.delete(callId);
688
+ ctx.suppressedToolOutputIds.delete(callId);
689
+ return;
690
+ }
691
+
692
+ if (ctx.suppressedToolOutputIds.delete(callId)) {
693
+ return;
694
+ }
695
+
696
+ // Cross-turn resolution: look up where the tool call was originally pushed
697
+ const origin = ctx.toolCallToTurn.get(callId);
698
+ if (origin) {
699
+ const originTurn = ctx.turns.get(origin.turnId);
700
+ if (originTurn && origin.bubbleIndex < originTurn.assistantBubbles.length) {
701
+ const originBubble = originTurn.assistantBubbles[origin.bubbleIndex];
702
+ const existing = originBubble.toolCalls.find(tool => tool.id === callId);
703
+ if (existing) {
704
+ applyPersistedToolOutput(existing, payload.output, rawOutput, ctx);
705
+ return;
706
+ }
707
+ }
708
+ }
709
+
710
+ if (payload.type === 'custom_tool_call_output') {
711
+ return;
712
+ }
713
+
714
+ // Fallback: push orphan entry into current turn
715
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
716
+ const bubble = ensureAssistantBubble(turn, timestamp);
717
+ const normalizedResult = normalizeCodexToolResult('tool', rawOutput);
718
+
719
+ pushToolInvocation(bubble, {
720
+ id: callId,
721
+ name: 'tool',
722
+ input: {},
723
+ status: isCodexToolOutputError(rawOutput) ? 'error' : 'completed',
724
+ result: normalizedResult,
725
+ });
726
+ }
727
+
728
+ function findPersistedToolCallById(ctx: PersistedParseContext, callId: string): ToolCallInfo | null {
729
+ const origin = ctx.toolCallToTurn.get(callId);
730
+ if (!origin) {
731
+ return null;
732
+ }
733
+
734
+ const turn = ctx.turns.get(origin.turnId);
735
+ if (!turn || origin.bubbleIndex >= turn.assistantBubbles.length) {
736
+ return null;
737
+ }
738
+
739
+ return turn.assistantBubbles[origin.bubbleIndex].toolCalls.find(tool => tool.id === callId) ?? null;
740
+ }
741
+
742
+ function readTerminalSessionIdArgument(input: Record<string, unknown>): string | undefined {
743
+ const value = input.session_id ?? input.sessionId;
744
+ if (typeof value === 'string' && value) return value;
745
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
746
+ return undefined;
747
+ }
748
+
749
+ function isSilentWriteStdinInput(input: Record<string, unknown>): boolean {
750
+ return typeof input.chars !== 'string' || input.chars.length === 0;
751
+ }
752
+
753
+ function appendCommandOutput(previous: string | undefined, next: string): string {
754
+ if (!next) return previous ?? '';
755
+ if (!previous) return next;
756
+ if (previous.endsWith('\n') || next.startsWith('\n')) return previous + next;
757
+ return `${previous}\n${next}`;
758
+ }
759
+
760
+ function readPersistedCommandToolResult(rawOutputText: string): {
761
+ output: string;
762
+ status: 'running' | 'completed' | 'unknown';
763
+ exitCode?: number;
764
+ terminalSessionId?: string;
765
+ } {
766
+ const output = normalizeCodexToolResult('Bash', rawOutputText);
767
+ const exitCodeMatch = rawOutputText.match(/(?:Exit code:|Process exited with code)\s*(-?\d+)/i);
768
+ const runningMatch = rawOutputText.match(/Process running with session ID\s*([^\n]+)/i);
769
+
770
+ return {
771
+ output,
772
+ status: exitCodeMatch ? 'completed' : runningMatch ? 'running' : 'unknown',
773
+ ...(exitCodeMatch ? { exitCode: Number(exitCodeMatch[1] ?? 0) } : {}),
774
+ ...(runningMatch ? { terminalSessionId: (runningMatch[1] ?? '').trim() } : {}),
775
+ };
776
+ }
777
+
778
+ function applyPersistedToolOutput(
779
+ toolCall: ToolCallInfo,
780
+ rawOutputValue: string | unknown[] | undefined,
781
+ rawOutputText: string,
782
+ ctx: PersistedParseContext,
783
+ options: { allowImplicitCommandCompletion?: boolean } = {},
784
+ ): void {
785
+ if (toolCall.name === 'Bash') {
786
+ const commandResult = readPersistedCommandToolResult(rawOutputText);
787
+ toolCall.result = appendCommandOutput(toolCall.result, commandResult.output);
788
+ if (commandResult.terminalSessionId) {
789
+ ctx.terminalSessionToCommandId.set(commandResult.terminalSessionId, toolCall.id);
790
+ }
791
+ if (commandResult.status === 'running') {
792
+ toolCall.status = 'running';
793
+ return;
794
+ }
795
+ if (commandResult.status === 'unknown' && options.allowImplicitCommandCompletion === false) {
796
+ return;
797
+ }
798
+ toolCall.status = commandResult.exitCode !== undefined
799
+ ? commandResult.exitCode === 0 ? 'completed' : 'error'
800
+ : isCodexToolOutputError(rawOutputText) ? 'error' : 'completed';
801
+ return;
802
+ }
803
+
804
+ toolCall.result = normalizePersistedToolOutput(toolCall, rawOutputValue, rawOutputText);
805
+ toolCall.status = isCodexToolOutputError(rawOutputText) ? 'error' : 'completed';
806
+ }
807
+
808
+ function normalizePersistedToolOutput(
809
+ toolCall: ToolCallInfo,
810
+ rawOutputValue: string | unknown[] | undefined,
811
+ rawOutputText: string,
812
+ ): string {
813
+ if (Array.isArray(rawOutputValue) && toolCall.name === 'Read') {
814
+ const filePath = toolCall.input.file_path;
815
+ if (typeof filePath === 'string' && filePath) {
816
+ return filePath;
817
+ }
818
+ }
819
+
820
+ return normalizeCodexToolResult(toolCall.name, rawOutputText);
821
+ }
822
+
823
+ function processPersistedWebSearchCall(
824
+ payload: PersistedWebSearchCallPayload,
825
+ timestamp: number,
826
+ lineIndex: number,
827
+ ctx: PersistedParseContext,
828
+ ): void {
829
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
830
+ const bubble = ensureAssistantBubble(turn, timestamp);
831
+
832
+ // Persisted web_search_call entries commonly omit call_id. Use transcript line index
833
+ // so live tailing and history reload reconstruct the same visible tool sequence.
834
+ const callId = payload.call_id || `tail-ws-${lineIndex}`;
835
+
836
+ if (bubble.toolIndexesById.has(callId)) return;
837
+
838
+ const input = normalizeCodexToolInput('web_search_call', {
839
+ action: payload.action ?? {},
840
+ });
841
+
842
+ const isTerminal = payload.status === 'completed' || payload.status === 'failed'
843
+ || payload.status === 'error' || payload.status === 'cancelled';
844
+
845
+ const toolCall: ToolCallInfo = {
846
+ id: callId,
847
+ name: 'WebSearch',
848
+ input,
849
+ status: isTerminal ? (payload.status === 'completed' ? 'completed' : 'error') : 'running',
850
+ ...(isTerminal ? { result: 'Search complete' } : {}),
851
+ };
852
+
853
+ pushToolInvocation(bubble, toolCall);
854
+
855
+ ctx.toolCallToTurn.set(callId, {
856
+ turnId: turn.id,
857
+ bubbleIndex: turn.assistantBubbles.indexOf(bubble),
858
+ });
859
+ }
860
+
861
+ function processPersistedMcpToolCall(
862
+ payload: PersistedMcpToolCallPayload,
863
+ timestamp: number,
864
+ ctx: PersistedParseContext,
865
+ ): void {
866
+ const callId = payload.call_id;
867
+ if (!callId) return;
868
+
869
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
870
+ const bubble = ensureAssistantBubble(turn, timestamp);
871
+
872
+ if (bubble.toolIndexesById.has(callId)) return;
873
+
874
+ const normalizedInput = normalizeCodexMcpToolInput(payload.arguments);
875
+ const normalizedState = normalizeCodexMcpToolState(payload.status, payload.result, payload.error);
876
+
877
+ const toolCall: ToolCallInfo = {
878
+ id: callId,
879
+ name: normalizeCodexMcpToolName(payload.server, payload.tool),
880
+ input: normalizedInput,
881
+ status: normalizedState.status,
882
+ ...(normalizedState.result ? { result: normalizedState.result } : {}),
883
+ };
884
+
885
+ pushToolInvocation(bubble, toolCall);
886
+
887
+ ctx.toolCallToTurn.set(callId, {
888
+ turnId: turn.id,
889
+ bubbleIndex: turn.activeBubbleIndex!,
890
+ });
891
+ }
892
+
893
+ function processPersistedPayload(
894
+ payload: PersistedPayload,
895
+ timestamp: number,
896
+ lineIndex: number,
897
+ ctx: PersistedParseContext,
898
+ ): void {
899
+ if (!payload?.type) {
900
+ return;
901
+ }
902
+
903
+ switch (payload.type) {
904
+ case 'message': {
905
+ const messagePayload = payload as PersistedMessagePayload;
906
+ const text = extractMessageText(messagePayload.content);
907
+
908
+ if (messagePayload.role === 'user') {
909
+ const visibleText = extractCodexUserVisibleText(text);
910
+ if (visibleText === null) break;
911
+
912
+ // Close any active bubble in the current turn before starting user content
913
+ if (ctx.currentTurnId) {
914
+ const prevTurn = ctx.turns.get(ctx.currentTurnId);
915
+ if (prevTurn) closeAssistantBubble(prevTurn);
916
+ }
917
+
918
+ // User message opens a new turn
919
+ ctx.currentTurnId = null;
920
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), null, timestamp);
921
+ ctx.currentTurnId = turn.id;
922
+ appendUserChunk(turn, visibleText, timestamp);
923
+ } else if (messagePayload.role === 'assistant') {
924
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
925
+ const bubble = ensureAssistantBubble(turn, timestamp);
926
+ if (text) {
927
+ appendUniqueChunk(bubble.contentChunks, text);
928
+ }
929
+ }
930
+ break;
931
+ }
932
+
933
+ case 'reasoning': {
934
+ const reasoningPayload = payload as PersistedReasoningPayload;
935
+ const text = extractReasoningText(reasoningPayload);
936
+ if (!text) break;
937
+
938
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
939
+ const bubble = ensureAssistantBubble(turn, timestamp);
940
+ appendUniqueChunk(bubble.thinkingChunks, text);
941
+ break;
942
+ }
943
+
944
+ case 'function_call':
945
+ case 'custom_tool_call':
946
+ processPersistedToolCall(payload as PersistedToolCallPayload, timestamp, ctx);
947
+ break;
948
+
949
+ case 'function_call_output':
950
+ case 'custom_tool_call_output':
951
+ processPersistedToolOutput(payload as PersistedToolCallOutputPayload, timestamp, ctx);
952
+ break;
953
+
954
+ case 'web_search_call':
955
+ processPersistedWebSearchCall(payload as PersistedWebSearchCallPayload, timestamp, lineIndex, ctx);
956
+ break;
957
+
958
+ case 'mcp_tool_call':
959
+ processPersistedMcpToolCall(payload as PersistedMcpToolCallPayload, timestamp, ctx);
960
+ break;
961
+
962
+ case 'compaction':
963
+ break;
964
+
965
+ default:
966
+ break;
967
+ }
968
+ }
969
+
970
+ function applyCompactedReplacementHistory(
971
+ payload: PersistedCompactedPayload | undefined,
972
+ timestamp: number,
973
+ ctx: PersistedParseContext,
974
+ ): void {
975
+ ctx.turns.clear();
976
+ ctx.turnOrder.length = 0;
977
+ ctx.currentTurnId = null;
978
+ ctx.toolCallToTurn.clear();
979
+ ctx.suppressedToolOutputIds.clear();
980
+ ctx.terminalSessionToCommandId.clear();
981
+ ctx.stdinCallToCommandId.clear();
982
+ ctx.turnCounter = 0;
983
+
984
+ const replacementHistory = Array.isArray(payload?.replacement_history)
985
+ ? payload.replacement_history
986
+ : [];
987
+
988
+ for (const [index, item] of replacementHistory.entries()) {
989
+ processPersistedPayload(item, timestamp + index, index, ctx);
990
+ }
991
+
992
+ if (ctx.currentTurnId) {
993
+ const turn = ctx.turns.get(ctx.currentTurnId);
994
+ if (turn) {
995
+ closeAssistantBubble(turn);
996
+ }
997
+ ctx.currentTurnId = null;
998
+ }
999
+ }
1000
+
1001
+ // ---------------------------------------------------------------------------
1002
+ // event_msg processing
1003
+ // ---------------------------------------------------------------------------
1004
+
1005
+ function extractServerTurnId(payload: PersistedEventPayload): string | undefined {
1006
+ const turnId = (payload as Record<string, unknown>).turn_id;
1007
+ return typeof turnId === 'string' ? turnId : undefined;
1008
+ }
1009
+
1010
+ function processEventMsg(
1011
+ payload: PersistedEventPayload,
1012
+ timestamp: number,
1013
+ ctx: PersistedParseContext,
1014
+ ): void {
1015
+ if (!payload?.type) return;
1016
+
1017
+ switch (payload.type) {
1018
+ case 'task_started': {
1019
+ const serverTurnId = extractServerTurnId(payload);
1020
+ const id = nextTurnId(ctx);
1021
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, id, null, timestamp);
1022
+ turn.startedAt = timestamp;
1023
+ if (serverTurnId) turn.serverTurnId = serverTurnId;
1024
+ ctx.currentTurnId = turn.id;
1025
+ break;
1026
+ }
1027
+
1028
+ case 'task_complete': {
1029
+ if (ctx.currentTurnId) {
1030
+ const turn = ctx.turns.get(ctx.currentTurnId);
1031
+ if (turn) {
1032
+ turn.completedAt = timestamp;
1033
+ turn.completed = true;
1034
+ closeAssistantBubble(turn);
1035
+ const serverTurnId = extractServerTurnId(payload);
1036
+ if (serverTurnId && !turn.serverTurnId) turn.serverTurnId = serverTurnId;
1037
+ }
1038
+ }
1039
+ ctx.currentTurnId = null;
1040
+ break;
1041
+ }
1042
+
1043
+ case 'turn_aborted': {
1044
+ if (ctx.currentTurnId) {
1045
+ const turn = ctx.turns.get(ctx.currentTurnId);
1046
+ if (turn) {
1047
+ const bubble = ensureAssistantBubble(turn, timestamp);
1048
+ bubble.interrupted = true;
1049
+ closeAssistantBubble(turn);
1050
+ turn.completedAt = timestamp;
1051
+ }
1052
+ }
1053
+ ctx.currentTurnId = null;
1054
+ break;
1055
+ }
1056
+
1057
+ case 'user_message': {
1058
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
1059
+ const msg = payload.message;
1060
+ if (typeof msg === 'string') {
1061
+ const visibleText = extractCodexUserVisibleText(msg);
1062
+ if (visibleText !== null) {
1063
+ appendUserChunk(turn, visibleText, timestamp);
1064
+ }
1065
+ }
1066
+ break;
1067
+ }
1068
+
1069
+ case 'agent_message': {
1070
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
1071
+ const bubble = ensureAssistantBubble(turn, timestamp);
1072
+ const msg = payload.message;
1073
+ if (typeof msg === 'string') {
1074
+ appendUniqueChunk(bubble.contentChunks, msg);
1075
+ }
1076
+ break;
1077
+ }
1078
+
1079
+ case 'agent_reasoning': {
1080
+ const text = extractReasoningText(payload);
1081
+ if (!text) break;
1082
+
1083
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
1084
+ const bubble = ensureAssistantBubble(turn, timestamp);
1085
+ appendUniqueChunk(bubble.thinkingChunks, text);
1086
+ break;
1087
+ }
1088
+
1089
+ case 'context_compacted': {
1090
+ // Close any active bubble so the boundary stays standalone
1091
+ if (ctx.currentTurnId) {
1092
+ const prevTurn = ctx.turns.get(ctx.currentTurnId);
1093
+ if (prevTurn) closeAssistantBubble(prevTurn);
1094
+ }
1095
+
1096
+ // Create a dedicated turn for the compact boundary
1097
+ const id = nextTurnId(ctx);
1098
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, id, null, timestamp);
1099
+ const bubble = ensureAssistantBubble(turn, timestamp);
1100
+ bubble.contentBlocks.push({ type: 'context_compacted' });
1101
+ closeAssistantBubble(turn);
1102
+ ctx.currentTurnId = null;
1103
+ break;
1104
+ }
1105
+
1106
+ default:
1107
+ break;
1108
+ }
1109
+ }
1110
+
1111
+ // ---------------------------------------------------------------------------
1112
+ // Flush multi-bubble turns to ChatMessage[]
1113
+ // ---------------------------------------------------------------------------
1114
+
1115
+ function flushBubbleTurnMessages(
1116
+ turn: CodexTurnState,
1117
+ msgIndex: number,
1118
+ ): { messages: ChatMessage[]; nextMsgIndex: number } {
1119
+ const messages: ChatMessage[] = [];
1120
+
1121
+ const visibleUserText = extractCodexUserVisibleText(turn.userChunks.join('\n'));
1122
+ if (visibleUserText) {
1123
+ const displayContent = extractUserDisplayContent(visibleUserText);
1124
+ messages.push({
1125
+ id: `codex-msg-${msgIndex}`,
1126
+ role: 'user',
1127
+ content: visibleUserText,
1128
+ ...(displayContent !== undefined ? { displayContent } : {}),
1129
+ ...(turn.serverTurnId ? { userMessageId: turn.serverTurnId } : {}),
1130
+ timestamp: turn.userTimestamp || turn.startedAt || Date.now(),
1131
+ });
1132
+ msgIndex += 1;
1133
+ }
1134
+
1135
+ let lastAssistantTimestamp = 0;
1136
+ const assistantMessages: ChatMessage[] = [];
1137
+
1138
+ for (const bubble of turn.assistantBubbles) {
1139
+ const contentText = bubble.contentChunks.join('\n\n');
1140
+ const thinkingText = bubble.thinkingChunks.join('\n\n');
1141
+ const hasContent = contentText.trim().length > 0;
1142
+ const hasThinking = thinkingText.trim().length > 0;
1143
+ const hasToolCalls = bubble.toolCalls.length > 0;
1144
+ const hasCompactBoundary = bubble.contentBlocks.some(b => b.type === 'context_compacted');
1145
+
1146
+ if (!hasContent && !hasThinking && !hasToolCalls && !hasCompactBoundary) {
1147
+ if (bubble.interrupted) {
1148
+ messages.push({
1149
+ id: `codex-msg-${msgIndex}`,
1150
+ role: 'assistant',
1151
+ content: '',
1152
+ timestamp: bubble.startedAt || turn.startedAt || Date.now(),
1153
+ isInterrupt: true,
1154
+ });
1155
+ msgIndex += 1;
1156
+ }
1157
+ continue;
1158
+ }
1159
+
1160
+ const contentBlocks: ContentBlock[] = [];
1161
+ if (hasThinking) {
1162
+ contentBlocks.push({ type: 'thinking', content: thinkingText.trim() });
1163
+ }
1164
+ contentBlocks.push(...bubble.contentBlocks);
1165
+ if (hasContent) {
1166
+ contentBlocks.push({ type: 'text', content: contentText.trim() });
1167
+ }
1168
+
1169
+ const msg: ChatMessage = {
1170
+ id: `codex-msg-${msgIndex}`,
1171
+ role: 'assistant',
1172
+ content: contentText.trim(),
1173
+ timestamp: bubble.startedAt || turn.startedAt || Date.now(),
1174
+ toolCalls: hasToolCalls ? bubble.toolCalls : undefined,
1175
+ contentBlocks: contentBlocks.length > 0 ? contentBlocks : undefined,
1176
+ };
1177
+
1178
+ if (bubble.interrupted) {
1179
+ msg.isInterrupt = true;
1180
+ }
1181
+
1182
+ if (bubble.lastEventAt > lastAssistantTimestamp) {
1183
+ lastAssistantTimestamp = bubble.lastEventAt;
1184
+ }
1185
+
1186
+ assistantMessages.push(msg);
1187
+ messages.push(msg);
1188
+ msgIndex += 1;
1189
+ }
1190
+
1191
+ if (assistantMessages.length > 0 && turn.userTimestamp && lastAssistantTimestamp > turn.userTimestamp) {
1192
+ const durationMs = lastAssistantTimestamp - turn.userTimestamp;
1193
+ const lastMsg = assistantMessages[assistantMessages.length - 1];
1194
+ lastMsg.durationSeconds = Math.round(durationMs / 1000);
1195
+ }
1196
+
1197
+ if (turn.serverTurnId && turn.completed && assistantMessages.length > 0) {
1198
+ const lastNonInterrupt = [...assistantMessages].reverse().find(m => !m.isInterrupt);
1199
+ if (lastNonInterrupt) {
1200
+ lastNonInterrupt.assistantMessageId = turn.serverTurnId;
1201
+ }
1202
+ }
1203
+
1204
+ return { messages, nextMsgIndex: msgIndex };
1205
+ }
1206
+
1207
+ // ---------------------------------------------------------------------------
1208
+ // Session file discovery
1209
+ // ---------------------------------------------------------------------------
1210
+
1211
+ const SAFE_SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
1212
+
1213
+ function getPathModuleForSessionPath(sessionPath: string): typeof path.posix {
1214
+ return sessionPath.includes('\\') || /^[A-Za-z]:/.test(sessionPath)
1215
+ ? path.win32
1216
+ : path.posix;
1217
+ }
1218
+
1219
+ export function deriveCodexSessionsRootFromSessionPath(
1220
+ sessionFilePath: string | null | undefined,
1221
+ ): string | null {
1222
+ if (!sessionFilePath) {
1223
+ return null;
1224
+ }
1225
+
1226
+ const pathModule = getPathModuleForSessionPath(sessionFilePath);
1227
+ let current = pathModule.dirname(pathModule.normalize(sessionFilePath));
1228
+ let previous: string | null = null;
1229
+
1230
+ while (current && current !== previous) {
1231
+ if (pathModule.basename(current).toLowerCase() === 'sessions') {
1232
+ return current;
1233
+ }
1234
+ previous = current;
1235
+ current = pathModule.dirname(current);
1236
+ }
1237
+
1238
+ return null;
1239
+ }
1240
+
1241
+ export function deriveCodexMemoriesDirFromSessionsRoot(
1242
+ sessionsDir: string | null | undefined,
1243
+ ): string | null {
1244
+ if (!sessionsDir) {
1245
+ return null;
1246
+ }
1247
+
1248
+ const pathModule = getPathModuleForSessionPath(sessionsDir);
1249
+ return pathModule.join(pathModule.dirname(sessionsDir), 'memories');
1250
+ }
1251
+
1252
+ export function findCodexSessionFile(
1253
+ threadId: string,
1254
+ root: string = path.join(os.homedir(), '.codex', 'sessions'),
1255
+ ): string | null {
1256
+ if (!threadId || !SAFE_SESSION_ID_PATTERN.test(threadId) || !fs.existsSync(root)) {
1257
+ return null;
1258
+ }
1259
+
1260
+ const directPath = path.join(root, `${threadId}.jsonl`);
1261
+ if (fs.existsSync(directPath)) {
1262
+ return directPath;
1263
+ }
1264
+
1265
+ const stack = [root];
1266
+ while (stack.length > 0) {
1267
+ const current = stack.pop();
1268
+ if (!current) {
1269
+ continue;
1270
+ }
1271
+
1272
+ let entries: fs.Dirent[];
1273
+ try {
1274
+ entries = fs.readdirSync(current, { withFileTypes: true });
1275
+ } catch {
1276
+ continue;
1277
+ }
1278
+
1279
+ for (const entry of entries) {
1280
+ const fullPath = path.join(current, entry.name);
1281
+ if (entry.isDirectory()) {
1282
+ stack.push(fullPath);
1283
+ continue;
1284
+ }
1285
+
1286
+ if (entry.isFile() && entry.name.endsWith(`-${threadId}.jsonl`)) {
1287
+ return fullPath;
1288
+ }
1289
+ }
1290
+ }
1291
+
1292
+ return null;
1293
+ }
1294
+
1295
+ export function parseCodexSessionFile(filePath: string): ChatMessage[] {
1296
+ let content: string;
1297
+ try {
1298
+ content = fs.readFileSync(filePath, 'utf-8');
1299
+ } catch {
1300
+ return [];
1301
+ }
1302
+
1303
+ return parseCodexSessionContent(content);
1304
+ }
1305
+
1306
+ export interface CodexParsedTurn {
1307
+ turnId: string | null;
1308
+ messages: ChatMessage[];
1309
+ }
1310
+
1311
+ export function parseCodexSessionContent(content: string): ChatMessage[] {
1312
+ const turns = parseCodexSessionTurns(content);
1313
+ return turns.flatMap(t => t.messages);
1314
+ }
1315
+
1316
+ export function parseCodexSessionTurns(content: string): CodexParsedTurn[] {
1317
+ const records = content
1318
+ .split('\n')
1319
+ .filter(line => line.trim())
1320
+ .map(parseSessionRecord)
1321
+ .filter((record): record is ParsedSessionRecord => record !== null);
1322
+
1323
+ // Detect format: legacy uses type=event, modern uses event_msg/response_item
1324
+ let hasLegacy = false;
1325
+ let hasModern = false;
1326
+ for (const record of records) {
1327
+ if (record.type === 'event') hasLegacy = true;
1328
+ else if (record.type === 'event_msg' || record.type === 'response_item' || record.type === 'compacted') hasModern = true;
1329
+ if (hasLegacy && hasModern) break;
1330
+ }
1331
+
1332
+ // Pure legacy sessions use the old flat accumulator (no turn-level structure)
1333
+ if (hasLegacy && !hasModern) {
1334
+ const messages = parseLegacySession(records);
1335
+ return messages.length > 0 ? [{ turnId: null, messages }] : [];
1336
+ }
1337
+
1338
+ // Modern or mixed sessions use the bubble model with turn-level grouping
1339
+ return parseModernSessionTurns(records);
1340
+ }
1341
+
1342
+ // ---------------------------------------------------------------------------
1343
+ // Legacy (event wrapper) parser — preserved for backward compat
1344
+ // ---------------------------------------------------------------------------
1345
+
1346
+ function parseLegacySession(records: ParsedSessionRecord[]): ChatMessage[] {
1347
+ const messages: ChatMessage[] = [];
1348
+ let turn = newTurn();
1349
+ let msgIndex = 0;
1350
+
1351
+ for (const parsed of records) {
1352
+ if (parsed.type === 'event' && parsed.event) {
1353
+ const event = parsed.event;
1354
+
1355
+ switch (event.type) {
1356
+ case 'turn.started':
1357
+ if (turn.assistantText || turn.thinkingText || turn.toolCalls.length > 0) {
1358
+ msgIndex = flushTurn(turn, messages, msgIndex);
1359
+ }
1360
+ turn = newTurn();
1361
+ break;
1362
+
1363
+ case 'item.started':
1364
+ case 'item.updated':
1365
+ case 'item.completed':
1366
+ if (event.item) {
1367
+ processLegacyItem(event.type, event.item, turn);
1368
+ }
1369
+ break;
1370
+
1371
+ case 'turn.completed':
1372
+ msgIndex = flushTurn(turn, messages, msgIndex);
1373
+ turn = newTurn();
1374
+ break;
1375
+
1376
+ case 'turn.failed':
1377
+ turn.interrupted = true;
1378
+ msgIndex = flushTurn(turn, messages, msgIndex);
1379
+ turn = newTurn();
1380
+ break;
1381
+
1382
+ default:
1383
+ break;
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ flushTurn(turn, messages, msgIndex);
1389
+ return messages;
1390
+ }
1391
+
1392
+ // ---------------------------------------------------------------------------
1393
+ // Modern (response_item + event_msg) parser — bubble model
1394
+ // ---------------------------------------------------------------------------
1395
+
1396
+ function parseModernSessionTurns(records: ParsedSessionRecord[]): CodexParsedTurn[] {
1397
+ const ctx = createPersistedParseContext();
1398
+
1399
+ for (const [lineIndex, parsed] of records.entries()) {
1400
+ const timestamp = parsed.timestamp;
1401
+
1402
+ // Legacy event records can appear in mixed sessions
1403
+ if (parsed.type === 'event' && parsed.event) {
1404
+ processLegacyEventInModernContext(parsed.event, timestamp, ctx);
1405
+ continue;
1406
+ }
1407
+
1408
+ if (parsed.type === 'event_msg') {
1409
+ processEventMsg(parsed.payload as PersistedEventPayload, timestamp, ctx);
1410
+ continue;
1411
+ }
1412
+
1413
+ if (parsed.type === 'compacted') {
1414
+ applyCompactedReplacementHistory(parsed.payload as PersistedCompactedPayload | undefined, timestamp, ctx);
1415
+ continue;
1416
+ }
1417
+
1418
+ if (parsed.type === 'response_item') {
1419
+ processPersistedPayload(parsed.payload, timestamp, lineIndex, ctx);
1420
+ }
1421
+ }
1422
+
1423
+ return flushBubbleTurnsGrouped(ctx.turns, ctx.turnOrder);
1424
+ }
1425
+
1426
+ function flushBubbleTurnsGrouped(
1427
+ turns: Map<string, CodexTurnState>,
1428
+ turnOrder: string[],
1429
+ ): CodexParsedTurn[] {
1430
+ const result: CodexParsedTurn[] = [];
1431
+ let messageOffset = 0;
1432
+
1433
+ for (const turnId of turnOrder) {
1434
+ const turn = turns.get(turnId);
1435
+ if (!turn) continue;
1436
+ const { messages: turnMessages, nextMsgIndex } = flushBubbleTurnMessages(turn, messageOffset);
1437
+ if (turnMessages.length === 0) continue;
1438
+ messageOffset = nextMsgIndex;
1439
+
1440
+ result.push({
1441
+ turnId: turn.serverTurnId ?? null,
1442
+ messages: turnMessages,
1443
+ });
1444
+ }
1445
+
1446
+ return result;
1447
+ }
1448
+
1449
+ function findToolCallOrigin(
1450
+ ctx: PersistedParseContext,
1451
+ callId: string,
1452
+ ): ToolCallInfo | null {
1453
+ const origin = ctx.toolCallToTurn.get(callId);
1454
+ if (!origin) {
1455
+ return null;
1456
+ }
1457
+
1458
+ const turn = ctx.turns.get(origin.turnId);
1459
+ if (!turn || origin.bubbleIndex >= turn.assistantBubbles.length) {
1460
+ return null;
1461
+ }
1462
+
1463
+ return turn.assistantBubbles[origin.bubbleIndex].toolCalls.find(tool => tool.id === callId) ?? null;
1464
+ }
1465
+
1466
+ function trackToolCallOrigin(
1467
+ ctx: PersistedParseContext,
1468
+ callId: string,
1469
+ turn: CodexTurnState,
1470
+ ): void {
1471
+ ctx.toolCallToTurn.set(callId, {
1472
+ turnId: turn.id,
1473
+ bubbleIndex: turn.activeBubbleIndex!,
1474
+ });
1475
+ }
1476
+
1477
+ function ensureModernLegacyToolCall(
1478
+ ctx: PersistedParseContext,
1479
+ timestamp: number,
1480
+ item: CodexItem,
1481
+ build: () => ToolCallInfo,
1482
+ ): ToolCallInfo {
1483
+ const existing = findToolCallOrigin(ctx, item.id);
1484
+ if (existing) {
1485
+ return existing;
1486
+ }
1487
+
1488
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
1489
+ const bubble = ensureAssistantBubble(turn, timestamp);
1490
+ const toolCall = build();
1491
+ pushToolInvocation(bubble, toolCall);
1492
+ trackToolCallOrigin(ctx, item.id, turn);
1493
+ return toolCall;
1494
+ }
1495
+
1496
+ function processLegacyItemInModernContext(
1497
+ eventType: string,
1498
+ item: CodexItem,
1499
+ timestamp: number,
1500
+ ctx: PersistedParseContext,
1501
+ ): void {
1502
+ switch (item.type) {
1503
+ case 'agent_message': {
1504
+ if ((eventType === 'item.updated' || eventType === 'item.completed') && item.text) {
1505
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
1506
+ const bubble = ensureAssistantBubble(turn, timestamp);
1507
+ replaceLatestChunk(bubble.contentChunks, item.text);
1508
+ }
1509
+ break;
1510
+ }
1511
+
1512
+ case 'reasoning': {
1513
+ if ((eventType === 'item.updated' || eventType === 'item.completed') && item.text) {
1514
+ const turn = ensureTurn(ctx.turns, ctx.turnOrder, nextTurnId(ctx), ctx.currentTurnId, timestamp);
1515
+ const bubble = ensureAssistantBubble(turn, timestamp);
1516
+ replaceLatestChunk(bubble.thinkingChunks, item.text);
1517
+ }
1518
+ break;
1519
+ }
1520
+
1521
+ case 'command_execution': {
1522
+ if (eventType === 'item.started') {
1523
+ ensureModernLegacyToolCall(ctx, timestamp, item, () => ({
1524
+ id: item.id,
1525
+ name: normalizeCodexToolName(item.type),
1526
+ input: normalizeCodexToolInput(item.type, { command: item.command ?? '' }),
1527
+ status: 'running',
1528
+ }));
1529
+ break;
1530
+ }
1531
+
1532
+ if (eventType === 'item.completed') {
1533
+ const toolCall = ensureModernLegacyToolCall(ctx, timestamp, item, () => ({
1534
+ id: item.id,
1535
+ name: normalizeCodexToolName(item.type),
1536
+ input: normalizeCodexToolInput(item.type, { command: item.command ?? '' }),
1537
+ status: 'running',
1538
+ }));
1539
+ const rawOutput = item.aggregated_output ?? '';
1540
+ toolCall.result = normalizeCodexToolResult(toolCall.name, rawOutput);
1541
+ toolCall.status = item.exit_code === 0 ? 'completed' : 'error';
1542
+ }
1543
+ break;
1544
+ }
1545
+
1546
+ case 'file_change': {
1547
+ if (eventType !== 'item.started' && eventType !== 'item.completed') {
1548
+ break;
1549
+ }
1550
+
1551
+ const changes = item.changes ?? [];
1552
+ const toolCall = ensureModernLegacyToolCall(ctx, timestamp, item, () => ({
1553
+ id: item.id,
1554
+ name: normalizeCodexToolName('file_change'),
1555
+ input: { changes },
1556
+ status: 'running',
1557
+ }));
1558
+
1559
+ if (eventType === 'item.completed') {
1560
+ const paths = changes.map(change => `${change.kind}: ${change.path}`).join(', ');
1561
+ toolCall.result = paths ? `Applied: ${paths}` : 'Applied';
1562
+ toolCall.status = item.status === 'completed' ? 'completed' : 'error';
1563
+ }
1564
+ break;
1565
+ }
1566
+
1567
+ case 'web_search': {
1568
+ if (eventType === 'item.started') {
1569
+ ensureModernLegacyToolCall(ctx, timestamp, item, () => ({
1570
+ id: item.id,
1571
+ name: normalizeCodexToolName(item.type),
1572
+ input: normalizeCodexToolInput(item.type, { query: item.query ?? '' }),
1573
+ status: 'running',
1574
+ }));
1575
+ break;
1576
+ }
1577
+
1578
+ if (eventType === 'item.completed') {
1579
+ const toolCall = ensureModernLegacyToolCall(ctx, timestamp, item, () => ({
1580
+ id: item.id,
1581
+ name: normalizeCodexToolName(item.type),
1582
+ input: normalizeCodexToolInput(item.type, { query: item.query ?? '' }),
1583
+ status: 'running',
1584
+ }));
1585
+ toolCall.result = 'Search complete';
1586
+ toolCall.status = 'completed';
1587
+ }
1588
+ break;
1589
+ }
1590
+
1591
+ case 'mcp_tool_call': {
1592
+ if (eventType === 'item.started') {
1593
+ ensureModernLegacyToolCall(ctx, timestamp, item, () => ({
1594
+ id: item.id,
1595
+ name: `mcp__${item.server ?? ''}__${item.tool ?? ''}`,
1596
+ input: {},
1597
+ status: 'running',
1598
+ }));
1599
+ break;
1600
+ }
1601
+
1602
+ if (eventType === 'item.completed') {
1603
+ const toolCall = ensureModernLegacyToolCall(ctx, timestamp, item, () => ({
1604
+ id: item.id,
1605
+ name: `mcp__${item.server ?? ''}__${item.tool ?? ''}`,
1606
+ input: {},
1607
+ status: 'running',
1608
+ }));
1609
+ toolCall.status = item.status === 'completed' ? 'completed' : 'error';
1610
+ toolCall.result = item.status === 'completed' ? 'Completed' : 'Failed';
1611
+ }
1612
+ break;
1613
+ }
1614
+
1615
+ default:
1616
+ break;
1617
+ }
1618
+ }
1619
+
1620
+ function processLegacyEventInModernContext(
1621
+ event: CodexEvent,
1622
+ timestamp: number,
1623
+ ctx: PersistedParseContext,
1624
+ ): void {
1625
+ switch (event.type) {
1626
+ case 'turn.started': {
1627
+ if (ctx.currentTurnId) {
1628
+ const previousTurn = ctx.turns.get(ctx.currentTurnId);
1629
+ if (previousTurn) {
1630
+ closeAssistantBubble(previousTurn);
1631
+ }
1632
+ }
1633
+ const id = nextTurnId(ctx);
1634
+ ensureTurn(ctx.turns, ctx.turnOrder, id, null, timestamp);
1635
+ ctx.currentTurnId = id;
1636
+ break;
1637
+ }
1638
+
1639
+ case 'turn.completed': {
1640
+ if (ctx.currentTurnId) {
1641
+ const turn = ctx.turns.get(ctx.currentTurnId);
1642
+ if (turn) closeAssistantBubble(turn);
1643
+ }
1644
+ ctx.currentTurnId = null;
1645
+ break;
1646
+ }
1647
+
1648
+ case 'turn.failed': {
1649
+ if (ctx.currentTurnId) {
1650
+ const turn = ctx.turns.get(ctx.currentTurnId);
1651
+ if (turn) {
1652
+ const bubble = ensureAssistantBubble(turn, timestamp);
1653
+ bubble.interrupted = true;
1654
+ closeAssistantBubble(turn);
1655
+ }
1656
+ }
1657
+ ctx.currentTurnId = null;
1658
+ break;
1659
+ }
1660
+
1661
+ case 'item.started':
1662
+ case 'item.updated':
1663
+ case 'item.completed':
1664
+ if (event.item) {
1665
+ processLegacyItemInModernContext(event.type, event.item, timestamp, ctx);
1666
+ }
1667
+ break;
1668
+
1669
+ default:
1670
+ break;
1671
+ }
1672
+ }