oh-my-agent-web 0.7.0 → 1.0.2

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 (249) hide show
  1. package/README.en.md +394 -248
  2. package/README.md +387 -247
  3. package/dist/server/approval/rules.js +4 -16
  4. package/dist/server/approval/scope.js +3 -3
  5. package/dist/server/cli-config.js +3 -3
  6. package/dist/server/cli-info.js +9 -9
  7. package/dist/server/cli.js +108 -23
  8. package/dist/server/engine/builtin.js +64 -1
  9. package/dist/server/engine/catalog-probe.js +34 -0
  10. package/dist/server/engine/claude-map.js +296 -0
  11. package/dist/server/engine/claude-sessions.js +581 -0
  12. package/dist/server/engine/claude-transport.js +75 -0
  13. package/dist/server/engine/claude.js +414 -0
  14. package/dist/server/engine/cli-version.js +119 -0
  15. package/dist/server/engine/codex-map.js +19 -23
  16. package/dist/server/engine/codex-sessions.js +5 -5
  17. package/dist/server/engine/codex-subagents.js +349 -0
  18. package/dist/server/engine/codex.js +215 -2
  19. package/dist/server/engine/dsh-history.js +1 -1
  20. package/dist/server/engine/dsh-log.js +47 -36
  21. package/dist/server/engine/dsh-map.js +3 -5
  22. package/dist/server/engine/dsh-prompt-patch.js +48 -0
  23. package/dist/server/engine/dsh-sessions.js +10 -11
  24. package/dist/server/engine/dsh-transport.js +2 -2
  25. package/dist/server/engine/dsh.js +45 -2
  26. package/dist/server/engine/engine-versions.js +41 -0
  27. package/dist/server/engine/fake.js +38 -7
  28. package/dist/server/engine/model-registry-reload.js +1 -1
  29. package/dist/server/engine/opencode-map.js +2 -2
  30. package/dist/server/engine/opencode-projects.js +1 -1
  31. package/dist/server/engine/opencode-sessions.js +2 -2
  32. package/dist/server/engine/opencode.js +39 -1
  33. package/dist/server/engine/pi-events.js +4 -4
  34. package/dist/server/engine/pi-plugins.js +8 -6
  35. package/dist/server/engine/pi-sdk.js +179 -5
  36. package/dist/server/engine/pi-subagent-tool.js +2 -2
  37. package/dist/server/engine/pi-subagents.js +2 -2
  38. package/dist/server/engine/pi-transcript.js +1 -1
  39. package/dist/server/engine/registry.js +12 -2
  40. package/dist/server/engine/suggestion-guidance.js +22 -9
  41. package/dist/server/engine/wiring.js +57 -3
  42. package/dist/server/files/content-search.js +187 -0
  43. package/dist/server/files/file-search.js +3 -3
  44. package/dist/server/files/file-service.js +33 -15
  45. package/dist/server/files/files-routes.js +68 -7
  46. package/dist/server/files/git-info.js +51 -11
  47. package/dist/server/files/path-resolve.js +176 -0
  48. package/dist/server/files/pick-dir-routes.js +93 -0
  49. package/dist/server/files/pick-path-routes.js +126 -0
  50. package/dist/server/http/auth-routes.js +71 -4
  51. package/dist/server/http/auth.js +48 -30
  52. package/dist/server/http/aux-routes.js +20 -4
  53. package/dist/server/http/file-headers.js +3 -3
  54. package/dist/server/http/health-and-static.js +1 -1
  55. package/dist/server/http/live-routes.js +4 -4
  56. package/dist/server/http/login-page.js +128 -1
  57. package/dist/server/http/password-store.js +31 -0
  58. package/dist/server/index.js +93 -11
  59. package/dist/server/markers/actions.js +8 -8
  60. package/dist/server/mcp/codex-mcp-store.js +190 -0
  61. package/dist/server/mcp/json-mcp-store.js +276 -0
  62. package/dist/server/mcp/mcp-auth.js +1 -1
  63. package/dist/server/mcp/mcp-command.js +0 -2
  64. package/dist/server/mcp/mcp-engine-shared.js +112 -0
  65. package/dist/server/mcp/mcp-import.js +4 -5
  66. package/dist/server/mcp/mcp-market.js +2 -3
  67. package/dist/server/mcp/mcp-routes.js +4 -3
  68. package/dist/server/mcp/none-mcp-store.js +27 -0
  69. package/dist/server/mcp/pi-mcp-config.js +1 -1
  70. package/dist/server/plugins/engine-plugin-stores.js +265 -0
  71. package/dist/server/plugins/plugin-routes.js +4 -3
  72. package/dist/server/protocol-parse.js +52 -4
  73. package/dist/server/protocol.js +0 -1
  74. package/dist/server/providers/catalog-writes.js +1 -1
  75. package/dist/server/providers/catalog.js +2 -2
  76. package/dist/server/providers/codex-models.js +1 -1
  77. package/dist/server/providers/codex-test.js +1 -1
  78. package/dist/server/providers/enabled-models.js +1 -11
  79. package/dist/server/providers/import-routes.js +2 -2
  80. package/dist/server/providers/probe-routes.js +26 -0
  81. package/dist/server/providers/provider-handlers.js +3 -3
  82. package/dist/server/providers/provider-pool.js +2 -2
  83. package/dist/server/providers/provider-store.js +5 -2
  84. package/dist/server/providers/subscription-routes.js +4 -2
  85. package/dist/server/providers/validate.js +13 -14
  86. package/dist/server/push/push-routes.js +32 -3
  87. package/dist/server/push/push.js +43 -9
  88. package/dist/server/session/client-view.js +2 -2
  89. package/dist/server/session/close-ephemeral.js +23 -0
  90. package/dist/server/session/conversation-create.js +31 -9
  91. package/dist/server/session/delete-session.js +25 -14
  92. package/dist/server/session/engine-switch.js +12 -5
  93. package/dist/server/session/fork-session.js +38 -0
  94. package/dist/server/session/lock.js +4 -4
  95. package/dist/server/session/model-controller.js +14 -2
  96. package/dist/server/session/move-session.js +40 -0
  97. package/dist/server/session/persist-ephemeral.js +48 -0
  98. package/dist/server/session/pi-opener.js +2 -2
  99. package/dist/server/session/pi-refresh.js +1 -1
  100. package/dist/server/session/pi-sessions-provider.js +2 -2
  101. package/dist/server/session/pi-sessions.js +107 -18
  102. package/dist/server/session/runtime.js +144 -9
  103. package/dist/server/session/session-stats.js +2 -2
  104. package/dist/server/session/settings.js +45 -49
  105. package/dist/server/session/subagents.js +4 -1
  106. package/dist/server/session/suggest-title.js +33 -0
  107. package/dist/server/session/title-prompt.js +126 -0
  108. package/dist/server/session/turn-finish.js +2 -2
  109. package/dist/server/session/turn-queue.js +5 -5
  110. package/dist/server/session/turn-watchdog.js +2 -2
  111. package/dist/server/skills/skill-install.js +3 -5
  112. package/dist/server/skills/skill-store.js +3 -3
  113. package/dist/server/skills/skillhub.js +3 -4
  114. package/dist/server/snapshot/messages.js +2 -8
  115. package/dist/server/snapshot/sender.js +1 -1
  116. package/dist/server/snapshot/state.js +3 -5
  117. package/dist/server/subagents/profile-store.js +11 -9
  118. package/dist/server/terminal/terminal-session.js +9 -9
  119. package/dist/server/uploads/upload-routes.js +1 -1
  120. package/dist/server/uploads/upload-store.js +4 -6
  121. package/dist/server/util/env.js +27 -21
  122. package/dist/server/util/ids.js +1 -1
  123. package/dist/server/util/json-file.js +38 -0
  124. package/dist/server/util/log-file.js +3 -3
  125. package/dist/server/util/log.js +4 -4
  126. package/dist/server/util/origin.js +5 -24
  127. package/dist/server/util/paths.js +25 -3
  128. package/dist/server/util/pricing-index.js +2 -6
  129. package/dist/server/util/pricing.js +10 -10
  130. package/dist/server/util/spawn-cli.js +2 -2
  131. package/dist/server/util/upstream-version.js +10 -12
  132. package/dist/server/util/version.js +2 -2
  133. package/dist/server/web-proxy/proxy-routes.js +167 -0
  134. package/dist/server/web-proxy/proxy-url.js +60 -0
  135. package/dist/server/web-proxy/rewrite.js +84 -0
  136. package/dist/server/ws/handle-message.js +21 -2
  137. package/package.json +13 -14
  138. package/scripts/prune-prebuilds.mjs +1 -1
  139. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_abnfDiagram-WEYOIA3C-Dq-2fRb4.js +1 -0
  140. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_architectureDiagram-ZHVYKT53-Dh5CEs4k.js +1 -0
  141. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_c4Diagram-XDPPPE54-Cb19QmmE.js +1 -0
  142. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_cynefinDiagram-WF24UDRP-COk-IL9O.js +1 -0
  143. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-CBK3MWTC-Gp_0nfEJ.js +1 -0
  144. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-ODIL2UCW-rzPf_9IN.js +1 -0
  145. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-PFBEJUXE-B5EnLWwl.js +1 -0
  146. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-TYUNFSNR-DVpFKFwC.js +1 -0
  147. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_ebnfDiagram-V4P3TTPT-CDL1qiPY.js +1 -0
  148. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_ishikawaDiagram-HPEHV2GE-CSkoONzp.js +1 -0
  149. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_mindmap-definition-6OY57UG3-B9sdVB6_.js +1 -0
  150. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_pegDiagram-ECIUJNBX-B6P7baCF.js +1 -0
  151. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_railroadDiagram-Y72NRBL7-W_a8Vy2l.js +1 -0
  152. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_swimlanes-B3ENRKFQ-DvGif-RR.js +1 -0
  153. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_swimlanesDiagram-AYQRV4AK-D0gFG_EI.js +1 -0
  154. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_usecaseDiagram-WPOLBNNX-BjgUBJiR.js +1 -0
  155. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_vennDiagram-Q3Y3IDCW-DnVuWTpF.js +1 -0
  156. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_wardleyDiagram-IVAIDKWA-B2vDAqpl.js +1 -0
  157. package/web/dist/assets/{arc-BTAvbR16.js → arc-Df9SI9yh.js} +1 -1
  158. package/web/dist/assets/{architecture-WOLXFQ4H-fewLD5_7.js → architecture-WOLXFQ4H-iO49E8Y1.js} +1 -1
  159. package/web/dist/assets/{blockDiagram-RJAE7QA3-ClmgyxZl.js → blockDiagram-RJAE7QA3-D8d1ukIs.js} +1 -1
  160. package/web/dist/assets/channel-C2-OT56h.js +1 -0
  161. package/web/dist/assets/{chunk-55XCU76K-DdC5EhZG.js → chunk-55XCU76K-C5wOvqDb.js} +1 -1
  162. package/web/dist/assets/{chunk-7S6XSTFO-CgToWHK6.js → chunk-7S6XSTFO-WIrWcDbG.js} +1 -1
  163. package/web/dist/assets/{chunk-AQDTSKZB-D-rQMnp7.js → chunk-AQDTSKZB-OkzYqFsw.js} +1 -1
  164. package/web/dist/assets/{chunk-DSNLSHJZ-cuZoTGMH.js → chunk-DSNLSHJZ-CHEA9kYd.js} +2 -2
  165. package/web/dist/assets/{chunk-HNE4Q36P-CiKr9iSN.js → chunk-HNE4Q36P-CPsq34CI.js} +1 -1
  166. package/web/dist/assets/{chunk-VQ5W62QI-DbTfXAAT.js → chunk-VQ5W62QI-CamPnEuF.js} +1 -1
  167. package/web/dist/assets/{chunk-WQQL4CDK-DHanHfB5.js → chunk-WQQL4CDK-gNypBVAM.js} +1 -1
  168. package/web/dist/assets/{chunk-ZAUX2LTX-SrjFXpy2.js → chunk-ZAUX2LTX--4WyRBRV.js} +1 -1
  169. package/web/dist/assets/{chunk-ZCHMQSOR-7Wy8xXQ5.js → chunk-ZCHMQSOR-umv3Lkpn.js} +1 -1
  170. package/web/dist/assets/{classDiagram-v2-4MYWR3UA-CBe5ZDvZ.js → classDiagram-v2-4MYWR3UA-C1XldpO1.js} +1 -1
  171. package/web/dist/assets/{cose-bilkent-JH36ORCC-Dljdyvhw.js → cose-bilkent-JH36ORCC-BvugZzFU.js} +1 -1
  172. package/web/dist/assets/{cynefin-EF2NZ3EQ-CcZU7GFP.js → cynefin-EF2NZ3EQ-6qXB70s4.js} +1 -1
  173. package/web/dist/assets/{dagre-NK4GIZVX-Bq_6y8qp.js → dagre-NK4GIZVX-DDdlABYw.js} +1 -1
  174. package/web/dist/assets/{diagram-I5XIIV2D-C2lnnYVG.js → diagram-I5XIIV2D-T8oWy5CY.js} +1 -1
  175. package/web/dist/assets/{diagram-RJA3I4VV-Bqf8Z6wJ.js → diagram-RJA3I4VV-CkdHTtfV.js} +1 -1
  176. package/web/dist/assets/{elk-QONTBCTQ-Cdzq_cJ5.js → elk-QONTBCTQ-BiEUbJJX.js} +1 -1
  177. package/web/dist/assets/{erDiagram-KYQIGQAA-ZJLCjPFC.js → erDiagram-KYQIGQAA-q3WwhyzH.js} +1 -1
  178. package/web/dist/assets/{eventmodeling-K75KTNOO-DHqeIpes.js → eventmodeling-K75KTNOO-BO5gW_NC.js} +1 -1
  179. package/web/dist/assets/{flowDiagram-IUGLLWBU-AVgYvXKN.js → flowDiagram-IUGLLWBU-DubjP6UK.js} +1 -1
  180. package/web/dist/assets/{ganttDiagram-R7GPNTJY-CR1SG1M7.js → ganttDiagram-R7GPNTJY-lKNRH8CI.js} +1 -1
  181. package/web/dist/assets/{gitGraph-VSP46ZUC-CIZQPK0R.js → gitGraph-VSP46ZUC-Cwlv3omT.js} +1 -1
  182. package/web/dist/assets/{gitGraphDiagram-PDYL7XHY-BhOCWEh6.js → gitGraphDiagram-PDYL7XHY-Bo9GpFvW.js} +1 -1
  183. package/web/dist/assets/index-CeYhxqkk.js +113 -0
  184. package/web/dist/assets/index-GJgJwZ23.css +1 -0
  185. package/web/dist/assets/{info-OHQRW6UA-C2wuRxeQ.js → info-OHQRW6UA-g3MbTRmN.js} +1 -1
  186. package/web/dist/assets/{infoDiagram-MVDRKAX5-BP5doAB5.js → infoDiagram-MVDRKAX5-CmILYLoB.js} +1 -1
  187. package/web/dist/assets/{journeyDiagram-BOSHW7O7-BcIDRTPS.js → journeyDiagram-BOSHW7O7-Av8T0U1C.js} +1 -1
  188. package/web/dist/assets/{kanban-definition-FNSA4HC5-CRH_bcoT.js → kanban-definition-FNSA4HC5-B9NyWQYN.js} +1 -1
  189. package/web/dist/assets/{line-iA4GPA_S.js → line-BUrll1My.js} +1 -1
  190. package/web/dist/assets/{linear-BGUKwkTH.js → linear-DjET0phX.js} +1 -1
  191. package/web/dist/assets/{math-DgNW_bVP.js → math-BJAjkhlu.js} +1 -1
  192. package/web/dist/assets/{mermaid-parser.core-CsqmgpEv.js → mermaid-parser.core-CaTdkhkS.js} +3 -3
  193. package/web/dist/assets/{mermaid.core-C1yCGQK6.js → mermaid.core-Cktx937U.js} +4 -4
  194. package/web/dist/assets/{packet-JDAUHWVQ-2ggaoz2g.js → packet-JDAUHWVQ-DhG6l3PG.js} +1 -1
  195. package/web/dist/assets/{pie-XZMESJXO-h_yslQQs.js → pie-XZMESJXO-YPcOx7s0.js} +1 -1
  196. package/web/dist/assets/{pieDiagram-GNMPY74T-6-JZMFyk.js → pieDiagram-GNMPY74T-DesiZKUl.js} +1 -1
  197. package/web/dist/assets/{quadrantDiagram-D57K4MC3-K5r1HOgL.js → quadrantDiagram-D57K4MC3-Cn1kBVgu.js} +1 -1
  198. package/web/dist/assets/{radar-ABXABTNO-6fOJGGgq.js → radar-ABXABTNO-CtuRfYSz.js} +1 -1
  199. package/web/dist/assets/{railroad-I3PHUGI6-B_gUgQ3d.js → railroad-I3PHUGI6-D5Gnyy0R.js} +1 -1
  200. package/web/dist/assets/{railroad-abnf-LNEFI6M7-CJ_eqiXZ.js → railroad-abnf-LNEFI6M7-Kw4pmJEm.js} +1 -1
  201. package/web/dist/assets/{railroad-ebnf-DWYD2UWJ-BTsgjLTU.js → railroad-ebnf-DWYD2UWJ-D8Mk5dIe.js} +1 -1
  202. package/web/dist/assets/{railroad-peg-JR7BVNR7-D91uiLln.js → railroad-peg-JR7BVNR7-R5Jwzs8F.js} +1 -1
  203. package/web/dist/assets/{requirementDiagram-EQYSW5RA-uapnV_OO.js → requirementDiagram-EQYSW5RA-B9RDq-cL.js} +1 -1
  204. package/web/dist/assets/{sankeyDiagram-SY24JHO5-BQsbFgUb.js → sankeyDiagram-SY24JHO5-DoEatOm6.js} +1 -1
  205. package/web/dist/assets/{sequenceDiagram-BLEHNHQU-iRoSYX_e.js → sequenceDiagram-BLEHNHQU-DGE5Z9lT.js} +1 -1
  206. package/web/dist/assets/{src-BBrKcs9r.js → src-BnEL7pZw.js} +1 -1
  207. package/web/dist/assets/{stateDiagram-v2-A6IESKWZ-Bzj9z5KH.js → stateDiagram-v2-A6IESKWZ-_vn8W-k_.js} +1 -1
  208. package/web/dist/assets/{timeline-definition-JXYKGLGB-DLUUuJX0.js → timeline-definition-JXYKGLGB-BaSLTtgR.js} +1 -1
  209. package/web/dist/assets/{treeView-D4JQ5SDB-CphIQ3n5.js → treeView-D4JQ5SDB-DZElyFgL.js} +1 -1
  210. package/web/dist/assets/{treemap-SAJKECNS-DqU3insw.js → treemap-SAJKECNS-DWDO4K4G.js} +1 -1
  211. package/web/dist/assets/{wardley-7MLQ67FV-Ch7d8cmC.js → wardley-7MLQ67FV-DTOUsk6Q.js} +1 -1
  212. package/web/dist/assets/{xychartDiagram-4URNHDU4-DdxpWBeM.js → xychartDiagram-4URNHDU4-DiaCfgAB.js} +1 -1
  213. package/web/dist/index.html +2 -2
  214. package/docs/screenshots/01-chat.png +0 -0
  215. package/docs/screenshots/02-answer.png +0 -0
  216. package/docs/screenshots/03-files-diff.png +0 -0
  217. package/docs/screenshots/04-terminal.png +0 -0
  218. package/docs/screenshots/05-models.png +0 -0
  219. package/docs/screenshots/06-skills.png +0 -0
  220. package/docs/screenshots/07-skills-hub.png +0 -0
  221. package/docs/screenshots/08-mcp.png +0 -0
  222. package/docs/screenshots/09-subagents.png +0 -0
  223. package/docs/screenshots/10-settings.png +0 -0
  224. package/docs/screenshots/11-plugins.png +0 -0
  225. package/docs/screenshots/12-voice.png +0 -0
  226. package/docs/screenshots/13-push.png +0 -0
  227. package/docs/screenshots/README.md +0 -45
  228. package/web/dist/assets/_omaw-mermaid-pruned_chunk_abnfDiagram-WEYOIA3C-CvNbnJ75.js +0 -1
  229. package/web/dist/assets/_omaw-mermaid-pruned_chunk_architectureDiagram-ZHVYKT53-T7NZZ4ao.js +0 -1
  230. package/web/dist/assets/_omaw-mermaid-pruned_chunk_c4Diagram-XDPPPE54-ByuAPg39.js +0 -1
  231. package/web/dist/assets/_omaw-mermaid-pruned_chunk_cynefinDiagram-WF24UDRP-BY5DHttT.js +0 -1
  232. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-CBK3MWTC-D7ZYyrF7.js +0 -1
  233. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-ODIL2UCW-C6kcOZFc.js +0 -1
  234. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-PFBEJUXE-CzFPFq0D.js +0 -1
  235. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-TYUNFSNR-B4G4T8SL.js +0 -1
  236. package/web/dist/assets/_omaw-mermaid-pruned_chunk_ebnfDiagram-V4P3TTPT-BkQ4xwD0.js +0 -1
  237. package/web/dist/assets/_omaw-mermaid-pruned_chunk_ishikawaDiagram-HPEHV2GE-52xqckJv.js +0 -1
  238. package/web/dist/assets/_omaw-mermaid-pruned_chunk_mindmap-definition-6OY57UG3-7cQksbFn.js +0 -1
  239. package/web/dist/assets/_omaw-mermaid-pruned_chunk_pegDiagram-ECIUJNBX-udgtBpwv.js +0 -1
  240. package/web/dist/assets/_omaw-mermaid-pruned_chunk_railroadDiagram-Y72NRBL7-Cd2dcQWz.js +0 -1
  241. package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanes-B3ENRKFQ-p34pL1Yk.js +0 -1
  242. package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanesDiagram-AYQRV4AK-BslfPs4r.js +0 -1
  243. package/web/dist/assets/_omaw-mermaid-pruned_chunk_usecaseDiagram-WPOLBNNX-DJZoN9Xk.js +0 -1
  244. package/web/dist/assets/_omaw-mermaid-pruned_chunk_vennDiagram-Q3Y3IDCW-DPXo67cZ.js +0 -1
  245. package/web/dist/assets/_omaw-mermaid-pruned_chunk_wardleyDiagram-IVAIDKWA-BE6ukwkg.js +0 -1
  246. package/web/dist/assets/channel-DfaYIFFE.js +0 -1
  247. package/web/dist/assets/index-CZQjJbRg.js +0 -104
  248. package/web/dist/assets/index-ChDUWKZX.css +0 -1
  249. /package/web/dist/assets/{_omaw-mermaid-pruned_katex_katex-Zojedrce.js → _oh-my-agent-web-mermaid-pruned_katex_katex-Zojedrce.js} +0 -0
@@ -0,0 +1,414 @@
1
+ import { CLAUDE_PROVIDER, currentModelId, currentModelOf, friendlyClaudeError, isFinalToolStatus, modelIdFor, modelOptions, permissionApprovalRequest, permissionOptionId, stopReasonToTurnEnd, textEventOf, toolEndEvent, toolFactsOf, toolStartEvent, } from "./claude-map.js";
2
+ import { claudeSessionFile, deleteClaudeSession, lastTurnUsage, readTranscriptFile, sessionModelOf, transcriptStats, transcriptToTranscript, transcriptToUiMessages, } from "./claude-sessions.js";
3
+ import { createClaudeClient, defaultClaudeBin } from "./claude-transport.js";
4
+ import { withProbeCwd } from "./catalog-probe.js";
5
+ import { isScratchCwd } from "./scratch.js";
6
+ const MAX_TOOL_FACTS = 200;
7
+ /** 回合结束后多久内「读统计」要等一下再读:Claude 是边聊边写 JSONL,最后一条可能还没落盘 */
8
+ const SETTLE_MS = 3000;
9
+ export function createClaudeEngine(options = {}) {
10
+ const bin = options.bin ?? defaultClaudeBin();
11
+ const client = options.client ?? createClaudeClient({ bin, args: options.args, log: options.log ?? (() => { }) });
12
+ const bindings = new Map();
13
+ const byEngineSession = new Map();
14
+ const active = new Map();
15
+ /** toolCallId → 工具事实(权限请求兜底用:请求体里只有 title,没有真工具名) */
16
+ const toolFacts = new Map();
17
+ /** conversationId → 最近一次回合结束的时刻(用于 SETTLE_MS 的等待) */
18
+ const settledAt = new Map();
19
+ let approvalHandler = null;
20
+ let lastError = null;
21
+ /**
22
+ * 最近一次拿到的模型表。历史会话在 resume 之前没有 models,但选择器不该是空的 ——
23
+ * 用它兜底;真正切模型时仍会先 resume(拿不到就报可读错误)。
24
+ */
25
+ let lastModels = {};
26
+ /** 目录探针跑过没有(每个实例只探一次;`reloadModels` / `dispose` 重置) */
27
+ let catalogProbed = false;
28
+ const rememberTool = (facts) => {
29
+ const previous = toolFacts.get(facts.toolCallId);
30
+ // 后到的 update 更详细(title 从占位变成真命令),合并而不是覆盖
31
+ toolFacts.set(facts.toolCallId, { ...previous, ...facts });
32
+ if (toolFacts.size > MAX_TOOL_FACTS) {
33
+ const oldest = toolFacts.keys().next().value;
34
+ if (oldest)
35
+ toolFacts.delete(oldest);
36
+ }
37
+ };
38
+ const applyModels = (binding, models) => {
39
+ if (!models)
40
+ return;
41
+ lastModels = models;
42
+ if (binding)
43
+ binding.models = models;
44
+ };
45
+ const bindingOf = (conversationId) => bindings.get(conversationId) ?? null;
46
+ const anyModels = (conversationId) => {
47
+ const binding = conversationId ? bindingOf(conversationId) : undefined;
48
+ if (binding && (binding.models.availableModels?.length ?? 0) > 0)
49
+ return binding.models;
50
+ const found = [...bindings.values()].find((item) => (item.models.availableModels?.length ?? 0) > 0);
51
+ return found?.models ?? lastModels;
52
+ };
53
+ /**
54
+ * 没有会话时把模型表弄进来(原因与代价见 catalog-probe.ts 文件头):
55
+ * Claude 的模型只在 `session/new` 的 `models` 里 → 开个临时会话抄一份,再删掉它的 JSONL。
56
+ * 这套 ACP 面没有 `session/close`(parkSession 也知道这一点),所以收尾只能走文件。
57
+ */
58
+ const warmCatalog = async () => {
59
+ if (catalogProbed || (lastModels.availableModels?.length ?? 0) > 0)
60
+ return;
61
+ catalogProbed = true;
62
+ await withProbeCwd(async (cwd) => {
63
+ const created = await client.request("session/new", { cwd, mcpServers: [] });
64
+ const probeId = created?.sessionId;
65
+ if (!probeId)
66
+ return;
67
+ applyModels(undefined, created.models);
68
+ // 会话文件是**第一次说话时**才写的:找不到文件就是本来没留下东西(不该报警告)
69
+ const file = claudeSessionFile(probeId, { cwd, home: options.home });
70
+ if (file && !deleteClaudeSession(probeId, { home: options.home, cwd, sessionPath: file })) {
71
+ options.log?.(`claude:模型目录探针会话的文件没删掉:${file}`);
72
+ }
73
+ });
74
+ };
75
+ /** 会话文件:宿主给的路径优先,否则按 cwd 猜 / 扫目录 */
76
+ const fileOf = (conversationId) => {
77
+ const binding = bindingOf(conversationId);
78
+ if (!binding)
79
+ return null;
80
+ if (binding.sessionPath)
81
+ return binding.sessionPath;
82
+ const found = claudeSessionFile(binding.engineSessionId, { cwd: binding.cwd, home: options.home });
83
+ if (found)
84
+ binding.sessionPath = found;
85
+ return found;
86
+ };
87
+ const entriesOf = (conversationId) => {
88
+ const file = fileOf(conversationId);
89
+ return file ? readTranscriptFile(file) : [];
90
+ };
91
+ /**
92
+ * 历史会话首次使用时才 resume(ACP 的 resume 会挂一个活的 query)。
93
+ *
94
+ * 代价写在这里:**resume 之前这条会话的模型表是空的**(模型表只能从 `session/new|resume` 拿),
95
+ * 所以刚启动工作台就直接看历史会话时,模型选择器要等第一轮消息(resume)之后才有内容 ——
96
+ * 与 opencode 一致,不是一个可以在不挂活句柄的前提下解的问题。
97
+ *
98
+ * `session/load` 是备用路径:老版本适配器只注册了 load,它会**重放历史** ——
99
+ * 重放发生在 request 返回之前,此时该会话还没有 active turn,事件会被丢掉(不会串到界面里)。
100
+ */
101
+ const ensureLive = async (conversationId) => {
102
+ const binding = bindingOf(conversationId);
103
+ if (!binding)
104
+ return null;
105
+ if (binding.live)
106
+ return binding;
107
+ const params = { sessionId: binding.engineSessionId, cwd: binding.cwd, mcpServers: [] };
108
+ const resume = (method) => client.request(method, params, { timeoutMs: 120_000 }).catch((error) => {
109
+ throw error;
110
+ });
111
+ try {
112
+ applyModels(binding, (await resume("session/resume"))?.models);
113
+ }
114
+ catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ if (!/method not found|not implemented|-32601/i.test(message))
117
+ throw error;
118
+ options.log?.(`claude:session/resume 不可用,退回 session/load(${message.slice(0, 120)})`);
119
+ applyModels(binding, (await resume("session/load"))?.models);
120
+ }
121
+ binding.live = true;
122
+ return binding;
123
+ };
124
+ const handleToolUpdate = (conversationId, update) => {
125
+ const facts = toolFactsOf(update);
126
+ if (!facts)
127
+ return;
128
+ rememberTool(facts);
129
+ const merged = toolFacts.get(facts.toolCallId) ?? facts;
130
+ const turn = active.get(conversationId);
131
+ if (!turn || turn.ended)
132
+ return;
133
+ // Claude 的 tool_call 第一条就带齐了 rawInput,不需要像 opencode 那样等第二条
134
+ if (!turn.started.has(facts.toolCallId)) {
135
+ turn.started.add(facts.toolCallId);
136
+ turn.onEvent(toolStartEvent(merged));
137
+ }
138
+ if (!isFinalToolStatus(update.status) || turn.finished.has(facts.toolCallId))
139
+ return;
140
+ turn.finished.add(facts.toolCallId);
141
+ turn.onEvent(toolEndEvent(update));
142
+ };
143
+ const handleNotification = (notification) => {
144
+ if (notification.method !== "session/update")
145
+ return;
146
+ const params = notification.params;
147
+ const engineSessionId = typeof params.sessionId === "string" ? params.sessionId : "";
148
+ const conversationId = byEngineSession.get(engineSessionId);
149
+ if (!conversationId)
150
+ return;
151
+ const update = (params.update ?? {});
152
+ if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
153
+ handleToolUpdate(conversationId, update);
154
+ return;
155
+ }
156
+ const event = textEventOf(update);
157
+ if (!event)
158
+ return;
159
+ const turn = active.get(conversationId);
160
+ if (turn && !turn.ended)
161
+ turn.onEvent(event);
162
+ };
163
+ const handlePermission = async (id, params) => {
164
+ const toolCallId = params.toolCall?.toolCallId;
165
+ const facts = typeof toolCallId === "string" ? toolFacts.get(toolCallId) : undefined;
166
+ // 带上会话归属:宿主收尾时只收这个会话的审批(不然会误伤并行会话,见 approval-bridge)
167
+ const owner = params.sessionId ? byEngineSession.get(params.sessionId) : undefined;
168
+ const request = { ...permissionApprovalRequest(params, facts), ...(owner ? { conversationId: owner } : {}) };
169
+ let decision = "deny";
170
+ if (approvalHandler) {
171
+ try {
172
+ decision = await approvalHandler(request);
173
+ }
174
+ catch {
175
+ decision = "deny";
176
+ }
177
+ }
178
+ client.respond(id, {
179
+ outcome: { outcome: "selected", optionId: permissionOptionId(params.options, decision) },
180
+ });
181
+ };
182
+ client.onNotification(handleNotification);
183
+ client.onServerRequest((request) => {
184
+ if (request.method === "session/request_permission") {
185
+ void handlePermission(request.id, request.params);
186
+ return;
187
+ }
188
+ // 认不出的服务端请求必须回包,否则 Claude 那一轮会一直等
189
+ client.respondError(request.id, -32601, `agent-web 未实现的服务端请求:${request.method}`);
190
+ });
191
+ const engine = {
192
+ client,
193
+ version: () => client.version(),
194
+ async prewarm() {
195
+ await client.ensureStarted();
196
+ // 顺手把模型表也弄进来:否则切过来、还没开会话时下拉是空的(见 catalog-probe.ts)
197
+ await warmCatalog().catch((error) => {
198
+ options.log?.(`claude:读取模型目录失败:${error instanceof Error ? error.message : String(error)}`);
199
+ });
200
+ },
201
+ /** 重读模型表(改完 Claude 自己的配置后):清掉快照,重新探一次 */
202
+ async reloadModels() {
203
+ lastModels = {};
204
+ catalogProbed = false;
205
+ await warmCatalog().catch((error) => {
206
+ options.log?.(`claude:重读模型目录失败:${error instanceof Error ? error.message : String(error)}`);
207
+ });
208
+ },
209
+ async createSession(sessionOptions) {
210
+ const created = await client
211
+ .request("session/new", { cwd: sessionOptions.workspace, mcpServers: [] }, { timeoutMs: 120_000 })
212
+ .catch((error) => {
213
+ throw new Error(friendlyClaudeError(error));
214
+ });
215
+ const engineSessionId = created?.sessionId;
216
+ if (!engineSessionId)
217
+ throw new Error("Claude Code 没有返回 sessionId");
218
+ const binding = {
219
+ engineSessionId,
220
+ cwd: sessionOptions.workspace,
221
+ live: true,
222
+ sessionPath: null,
223
+ models: created.models ?? {},
224
+ };
225
+ bindings.set(sessionOptions.sessionId, binding);
226
+ byEngineSession.set(engineSessionId, sessionOptions.sessionId);
227
+ applyModels(binding, created.models);
228
+ // 默认模型:只尊重显式配置(AGENT_WEB_CLAUDE_MODEL),否则用 Claude 自己的默认
229
+ const wanted = options.model?.trim();
230
+ if (wanted) {
231
+ const modelId = modelIdFor(binding.models, wanted);
232
+ if (modelId === null) {
233
+ options.log?.(`claude:默认模型不在可选列表里,已忽略:${wanted}`);
234
+ }
235
+ else if (modelId !== currentModelId(binding.models)) {
236
+ await client.request("session/set_model", { sessionId: engineSessionId, modelId }).catch((error) => {
237
+ options.log?.(`claude:设置默认模型失败(${wanted}):${friendlyClaudeError(error)}`);
238
+ });
239
+ applyModels(binding, { ...binding.models, currentModelId: modelId });
240
+ }
241
+ }
242
+ return { sessionPath: null, engineSessionId };
243
+ },
244
+ async openSession(openOptions) {
245
+ const engineSessionId = openOptions.engineSessionId ?? openOptions.sessionPath ?? openOptions.sessionId;
246
+ const binding = {
247
+ engineSessionId,
248
+ cwd: openOptions.cwd ?? "",
249
+ live: false,
250
+ sessionPath: openOptions.sessionPath ?? null,
251
+ models: {},
252
+ };
253
+ bindings.set(openOptions.sessionId, binding);
254
+ byEngineSession.set(engineSessionId, openOptions.sessionId);
255
+ },
256
+ async sendMessage(sessionId, text, onEvent, sendOptions) {
257
+ const binding = await ensureLive(sessionId).catch((error) => {
258
+ throw new Error(friendlyClaudeError(error));
259
+ });
260
+ if (!binding)
261
+ throw new Error("Claude Code 会话还没准备好(先新建一个会话)");
262
+ const turn = { onEvent, ended: false, started: new Set(), finished: new Set() };
263
+ active.set(sessionId, turn);
264
+ const model = currentModelId(binding.models);
265
+ if (model)
266
+ onEvent({ type: "model", model: `${CLAUDE_PROVIDER}/${model}` });
267
+ // ACP 的图片注入:适配器声明了 promptCapabilities.image = true(实测)
268
+ const prompt = [];
269
+ if (text.trim() !== "")
270
+ prompt.push({ type: "text", text });
271
+ for (const image of sendOptions?.images ?? []) {
272
+ prompt.push({ type: "image", data: image.data, mimeType: image.mimeType });
273
+ }
274
+ if (prompt.length === 0) {
275
+ active.delete(sessionId);
276
+ throw new Error("这条消息是空的(没有文本也没有图片)");
277
+ }
278
+ try {
279
+ const result = await client.request("session/prompt", { sessionId: binding.engineSessionId, prompt },
280
+ // 一轮跑很久是正常的(有工具调用):给 30 分钟,别用默认 30 秒掐掉
281
+ { timeoutMs: 30 * 60 * 1000 });
282
+ settledAt.set(sessionId, Date.now());
283
+ // `session/prompt` 不返回 usage:从 JSONL 里把这一轮加起来,界面上的 token 才会动
284
+ const usage = lastTurnUsage(entriesOf(sessionId));
285
+ if (usage && !turn.ended)
286
+ onEvent({ type: "usage", ...usage, final: true });
287
+ if (!turn.ended)
288
+ onEvent({ type: "turn_end", reason: stopReasonToTurnEnd(result?.stopReason) });
289
+ }
290
+ catch (error) {
291
+ throw new Error(friendlyClaudeError(error));
292
+ }
293
+ finally {
294
+ turn.ended = true;
295
+ active.delete(sessionId);
296
+ }
297
+ },
298
+ async interrupt(sessionId) {
299
+ const binding = bindingOf(sessionId);
300
+ if (!binding)
301
+ return;
302
+ // ACP 里 cancel 是**通知**(无 id):发成请求会被当成未知方法丢掉 → 停止按钮没反应
303
+ client.notify("session/cancel", { sessionId: binding.engineSessionId });
304
+ },
305
+ /** 「暂时不看了」:Claude 这套 ACP 面没有 close 方法,只需要停用绑定(resume 很便宜) */
306
+ async parkSession(sessionId) {
307
+ const binding = bindingOf(sessionId);
308
+ if (!binding)
309
+ return;
310
+ binding.live = false;
311
+ },
312
+ async closeSession(sessionId) {
313
+ const binding = bindingOf(sessionId);
314
+ bindings.delete(sessionId);
315
+ if (binding)
316
+ byEngineSession.delete(binding.engineSessionId);
317
+ },
318
+ async dispose() {
319
+ // 注意:**不清 lastModels** —— 它只是「Claude 有哪些模型」的目录快照,
320
+ // 切走再切回时模型选择器不该空着;真正切模型时会先 resume 拿最新模型表。
321
+ bindings.clear();
322
+ byEngineSession.clear();
323
+ active.clear();
324
+ toolFacts.clear();
325
+ settledAt.clear();
326
+ catalogProbed = false; // 探针会话随适配器进程一起没了:下次切回来重新探
327
+ client.close();
328
+ },
329
+ setApprovalHandler(handler) {
330
+ approvalHandler = handler;
331
+ },
332
+ async listModels(sessionId) {
333
+ return modelOptions(anyModels(sessionId));
334
+ },
335
+ async switchModel(sessionId, modelId) {
336
+ lastError = null;
337
+ const binding = await ensureLive(sessionId).catch((error) => {
338
+ lastError = friendlyClaudeError(error);
339
+ return null;
340
+ });
341
+ if (!binding) {
342
+ lastError ??= "先打开一个会话";
343
+ return false;
344
+ }
345
+ const target = modelIdFor(binding.models, modelId);
346
+ if (target === null) {
347
+ lastError = `这个模型不在 Claude Code 的可选列表里:${modelId}`;
348
+ return false;
349
+ }
350
+ try {
351
+ await client.request("session/set_model", { sessionId: binding.engineSessionId, modelId: target });
352
+ applyModels(binding, { ...binding.models, currentModelId: target });
353
+ return true;
354
+ }
355
+ catch (error) {
356
+ lastError = friendlyClaudeError(error);
357
+ return false;
358
+ }
359
+ },
360
+ async currentModel(sessionId) {
361
+ const binding = bindingOf(sessionId);
362
+ if (!binding) {
363
+ // 还没开会话(空状态):用目录快照里的当前值,别让选择器与芯片显示成空的
364
+ return currentModelOf(anyModels());
365
+ }
366
+ if (!binding.live && (binding.models.availableModels?.length ?? 0) === 0) {
367
+ // 历史会话还没 resume:先用文件里最后一次用的模型,别显示成「当前默认」
368
+ return sessionModelOf(entriesOf(sessionId));
369
+ }
370
+ return currentModelOf(binding.models);
371
+ },
372
+ async sessionModel(sessionId) {
373
+ const persisted = sessionModelOf(entriesOf(sessionId));
374
+ if (persisted)
375
+ return persisted;
376
+ return engine.currentModel ? engine.currentModel(sessionId) : null;
377
+ },
378
+ lastError: () => lastError,
379
+ /** 历史回放:直接读 JSONL(ACP 的 load 会重放事件并占一个活句柄,不适合只读) */
380
+ async readUiMessages(sessionId) {
381
+ return transcriptToUiMessages(entriesOf(sessionId));
382
+ },
383
+ async readTranscript(sessionId) {
384
+ return transcriptToTranscript(entriesOf(sessionId));
385
+ },
386
+ /** 用量统计:四桶来自每条 assistant 的 `message.usage`;花费来自 `cost-state` */
387
+ async sessionStats(sessionId) {
388
+ const at = settledAt.get(sessionId);
389
+ const settling = at !== undefined && Date.now() - at < SETTLE_MS;
390
+ const first = transcriptStats(entriesOf(sessionId));
391
+ if (!settling)
392
+ return first;
393
+ // 回合刚结束的一瞬间 JSONL 可能还没写完最后一条:短暂重读几次,别把半截数字写进面板
394
+ let last = first;
395
+ for (let attempt = 0; attempt < 4; attempt += 1) {
396
+ await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 400 : 600));
397
+ last = transcriptStats(entriesOf(sessionId));
398
+ if (last.contextTokens !== 0 || last.assistantMessages !== first.assistantMessages)
399
+ break;
400
+ }
401
+ return last;
402
+ },
403
+ /** Claude 没有删除会话的命令(`/clear` 只是开新的):直接删掉那个 JSONL */
404
+ async deleteSession(handle) {
405
+ const file = claudeSessionFile(handle, { home: options.home });
406
+ if (!deleteClaudeSession(handle, { home: options.home, sessionPath: file })) {
407
+ throw new Error(`找不到这条会话的文件:${handle}`);
408
+ }
409
+ },
410
+ };
411
+ return engine;
412
+ }
413
+ /** 侧栏过滤的判定也对外暴露一份:会话清单与「刚建的会话在不在侧栏」要用同一套规则 */
414
+ export { isScratchCwd };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * 引擎版本探测(切换面板上**每一项**都要显示自己的版本号,不只是当前那个)。
3
+ *
4
+ * 为什么不去问引擎自己:除了 pi(SDK 导出 `VERSION`)和 fake,其余引擎的版本号都是
5
+ * **握手时**给的(ACP 的 `agentInfo.version`、codex 的 `userAgent`)。要拿到握手结果就得
6
+ * 起一个子进程 —— 为了一行小字把 codex app-server / claude-code-acp 全拉起来,代价远大于
7
+ * 收益(codex 还会抢会话写锁、ACP 适配器要读一遍用户配置)。所以这里只做两件便宜事:
8
+ *
9
+ * 1. 跑一次 `<bin> --version`(dsh / opencode / codex 都认),带超时,输出里挑版本号;
10
+ * 2. ACP 适配器(claude-code-acp)**不认** `--version`(它会直接去握手),那就读它
11
+ * bin 旁边的 `package.json` —— 面板上要显示的就是这个适配器版本(实测 0.16.2 与
12
+ * 握手给的 `agentInfo.version` 一致,换引擎前后是同一个数,不会跳)。
13
+ *
14
+ * 拿不到就返回 null:界面少一行字,绝不编一个版本号。
15
+ */
16
+ import { spawn } from "node:child_process";
17
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { findExecutable, killTree, resolveCli } from "../util/spawn-cli.js";
20
+ /** `codex-cli 0.162.1`、`0.2.0-rc.2`、`v1.19.0` 都能挑出来 */
21
+ const VERSION_RE = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
22
+ /** 版本探测超时:拿不到就当没有,不让面板等它 */
23
+ const DEFAULT_TIMEOUT_MS = 4000;
24
+ /** 从一整行输出里挑出版本号本体(`codex-cli 0.162.1` → `0.162.1`);没有就 null */
25
+ function parseVersionText(text) {
26
+ const match = VERSION_RE.exec(text ?? "");
27
+ return match ? match[0] : null;
28
+ }
29
+ /**
30
+ * 读 `<bin>` 所属 npm 包的 `package.json` 版本(同步、不开子进程)。
31
+ *
32
+ * 做法:解析出真实文件路径(`…/bin/claude-code-acp` 是指向
33
+ * `…/lib/node_modules/@zed-industries/claude-code-acp/dist/index.js` 的软链),
34
+ * 然后**从它所在目录往上**找最近的 package.json。找不到返回 null。
35
+ *
36
+ * Windows 上 npm 装出来的 `claude-code-acp.cmd` 是个 shim(不是软链),往上找不到包,
37
+ * 这里就返回 null —— 那种情况界面只显示名字,不显示错版本号。
38
+ */
39
+ export function probePackageVersion(bin, env = process.env, exists = existsSync) {
40
+ const resolved = findExecutable(bin, env, process.platform, exists) ?? bin;
41
+ let file;
42
+ try {
43
+ file = realpathSync(resolved);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ let dir = dirname(file);
49
+ for (let depth = 0; depth < 6; depth += 1) {
50
+ const manifest = join(dir, "package.json");
51
+ if (existsSync(manifest)) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
54
+ if (typeof parsed.version === "string" && parsed.version.trim() !== "")
55
+ return parsed.version.trim();
56
+ }
57
+ catch {
58
+ // 坏 JSON / 读不了:继续往上找,别把整个探测搞挂
59
+ }
60
+ return null; // 最近的 package.json 都没有版本号:再往上只会找到别人的
61
+ }
62
+ const parent = dirname(dir);
63
+ if (parent === dir)
64
+ return null;
65
+ dir = parent;
66
+ }
67
+ return null;
68
+ }
69
+ /**
70
+ * 跑 `<bin> --version` 并挑出版本号;超时 / 没装 / 不认这个参数都返回 null。
71
+ *
72
+ * 杀进程走 `killTree`:Windows 上经 cmd.exe 起的 `.cmd` 会再拉一层 node,只杀直接子进程
73
+ * 会留孤儿(见 util/spawn-cli.ts)。
74
+ */
75
+ export function probeCliVersion(bin, options = {}) {
76
+ const args = options.args ?? ["--version"];
77
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
78
+ const env = options.env ?? process.env;
79
+ return new Promise((resolve) => {
80
+ let timer;
81
+ let settled = false;
82
+ const finish = (value) => {
83
+ if (settled)
84
+ return;
85
+ settled = true;
86
+ if (timer)
87
+ clearTimeout(timer);
88
+ resolve(value);
89
+ };
90
+ let child;
91
+ try {
92
+ const resolved = resolveCli(bin, args, env);
93
+ child = spawn(resolved.file, resolved.args, {
94
+ ...resolved.options,
95
+ // stdin 给 null:`--version` 之后还等输入的实现(claude-code-acp)能立刻退出
96
+ stdio: ["ignore", "pipe", "pipe"],
97
+ });
98
+ }
99
+ catch {
100
+ resolve(null);
101
+ return;
102
+ }
103
+ const timerId = setTimeout(() => {
104
+ killTree(child);
105
+ finish(null);
106
+ }, timeoutMs);
107
+ timer = timerId;
108
+ let output = "";
109
+ child.stdout?.on("data", (chunk) => {
110
+ output += chunk.toString();
111
+ });
112
+ // 有的 CLI 把版本打到 stderr
113
+ child.stderr?.on("data", (chunk) => {
114
+ output += chunk.toString();
115
+ });
116
+ child.on("error", () => finish(null));
117
+ child.on("close", () => finish(parseVersionText(output)));
118
+ });
119
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * codex app-server 的**纯映射**(不碰网络/子进程,单测直接覆盖)。
2
+ * codex app-server 的**纯映射**(不碰网络/子进程)。
3
3
  *
4
4
  * 三件事:
5
5
  * 1. thread items → 工作台的 `UiMessage`(对话区与历史都靠它);
@@ -56,7 +56,12 @@ export function itemsToUiMessages(items, ts) {
56
56
  const text = textOf(item.content);
57
57
  if (text.trim() === "")
58
58
  continue;
59
- messages.push({ id: item.id ?? `codex-user-${ts}-${messages.length}`, role: "user", ts, blocks: [{ kind: "text", text }] });
59
+ messages.push({
60
+ id: item.id ?? `codex-user-${ts}-${messages.length}`,
61
+ role: "user",
62
+ ts,
63
+ blocks: [{ kind: "text", text }],
64
+ });
60
65
  assistant = null;
61
66
  continue;
62
67
  }
@@ -70,7 +75,12 @@ export function itemsToUiMessages(items, ts) {
70
75
  continue;
71
76
  }
72
77
  if (type === "reasoning" || type === "plan") {
73
- const text = type === "plan" ? (item.text ?? "") : [...(item.summary ?? []), ...(Array.isArray(item.content) ? item.content.filter((c) => typeof c === "string") : [])].join("\n");
78
+ const text = type === "plan"
79
+ ? (item.text ?? "")
80
+ : [
81
+ ...(item.summary ?? []),
82
+ ...(Array.isArray(item.content) ? item.content.filter((c) => typeof c === "string") : []),
83
+ ].join("\n");
74
84
  if (text.trim() === "")
75
85
  continue;
76
86
  const target = ensureAssistant();
@@ -138,7 +148,11 @@ export function itemsToUiMessages(items, ts) {
138
148
  }
139
149
  if (type === "imageView" || type === "imageGeneration" || type === "webSearch") {
140
150
  const target = ensureAssistant();
141
- const text = type === "webSearch" ? "(联网搜索)" : type === "imageView" ? `(查看图片:${item.path ?? ""})` : "(生成图片)";
151
+ const text = type === "webSearch"
152
+ ? "(联网搜索)"
153
+ : type === "imageView"
154
+ ? `(查看图片:${item.path ?? ""})`
155
+ : "(生成图片)";
142
156
  target.blocks = [...target.blocks, { kind: "text", text }];
143
157
  }
144
158
  }
@@ -182,7 +196,7 @@ export function thinkingLevelsOf(models, modelId) {
182
196
  .filter((effort) => typeof effort === "string" && effort !== "");
183
197
  }
184
198
  /** 命令行 → 「这类」的作用域键(与 pi 侧的 bash 规则同构,`总是允许这类` 才有意义) */
185
- export function commandScope(command) {
199
+ function commandScope(command) {
186
200
  const first = command.trim().split(/\s+/)[0] ?? "";
187
201
  const clean = first.replace(/^.*\//, "");
188
202
  return { scopeKey: `bash|${clean}`, scopeHint: clean === "" ? command.trim().slice(0, 60) : clean };
@@ -220,24 +234,6 @@ export function approvalDecision(decision, kind) {
220
234
  return "acceptForSession";
221
235
  return kind === "command" ? "accept" : "accept";
222
236
  }
223
- /** 模型下拉里显示的当前模型(codex 的 provider 只有一个,给个稳定标签) */
224
- export function modelDisplay(provider, model) {
225
- if (!model)
226
- return null;
227
- return `${provider ?? "codex"}/${model}`;
228
- }
229
- /** 给 `ModelOption` 用:把引擎模型表转成快照里的形状(保留顺序,去重) */
230
- export function toModelOptions(models) {
231
- const seen = new Set();
232
- const options = [];
233
- for (const model of models) {
234
- if (seen.has(model.id))
235
- continue;
236
- seen.add(model.id);
237
- options.push({ id: model.id, label: model.label });
238
- }
239
- return options;
240
- }
241
237
  export function toolStartOf(item) {
242
238
  const id = item.id ?? `codex-tool-${item.type}`;
243
239
  if (item.type === "commandExecution") {
@@ -32,10 +32,10 @@ function underRoot(cwd, root) {
32
32
  return cwd.startsWith(root.endsWith("/") ? root : `${root}/`);
33
33
  }
34
34
  /**
35
- * 纯分组:projects + threads → 侧栏形状(单测直接覆盖)。
35
+ * 纯分组:projects + threads → 侧栏形状。
36
36
  * 会话按 updatedAt 倒序;项目之间保持 codex 的 position 顺序。
37
37
  */
38
- export function groupThreads(projects, threads) {
38
+ function groupThreads(projects, threads) {
39
39
  const byProjectId = new Map(projects.map((project) => [project.id, project]));
40
40
  const roots = projects.flatMap((project) => project.roots.map((root) => ({ projectId: project.id, path: root.path ?? "" })));
41
41
  const groups = new Map();
@@ -81,7 +81,7 @@ export function groupThreads(projects, threads) {
81
81
  model: thread.model ?? null,
82
82
  });
83
83
  }
84
- return [...groups.values()]
84
+ return ([...groups.values()]
85
85
  .map((group) => ({
86
86
  cwd: group.cwd,
87
87
  name: group.project?.name?.trim() || basename(group.cwd) || "未分项目",
@@ -92,10 +92,10 @@ export function groupThreads(projects, threads) {
92
92
  const leftAt = left.sessions[0]?.updatedAt ?? 0;
93
93
  const rightAt = right.sessions[0]?.updatedAt ?? 0;
94
94
  return rightAt - leftAt;
95
- });
95
+ }));
96
96
  }
97
97
  /** 分页拉全部会话(跟随 nextCursor;上限 5 页,避免超大库把首屏拖死) */
98
- export async function listAllThreads(client, pageSize = 200) {
98
+ async function listAllThreads(client, pageSize = 200) {
99
99
  const rows = [];
100
100
  let cursor = null;
101
101
  for (let page = 0; page < 5; page += 1) {