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
@@ -20,11 +20,11 @@
20
20
  * ⚠️ `user/message` 里大量是**系统注入**(runtime-context / skill-catalog / goal …):
21
21
  * 只有 `source.kind === "user"` 才是真的用户发言,其余一律不进对话区、也不拿来取标题。
22
22
  */
23
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
23
+ import { existsSync, readFileSync, readdirSync, rmdirSync, rmSync } from "node:fs";
24
24
  import { join } from "node:path";
25
25
  import { zstdDecompressSync } from "node:zlib";
26
26
  /** projectKey(cwd) —— 镜像 DSH 的会话目录命名:分隔符 → "-",其它非法字符 → ~XXXX(大写 hex) */
27
- export function projectKey(cwd) {
27
+ function projectKey(cwd) {
28
28
  let readable = "";
29
29
  let separatorRun = false;
30
30
  for (let i = 0; i < cwd.length; i += 1) {
@@ -47,7 +47,7 @@ export function projectKey(cwd) {
47
47
  return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
48
48
  }
49
49
  /** 多帧 zstd 解压:按魔数切帧;切错了就向后合并重试(数据体里可能误命中魔数) */
50
- export function zstdDecompressAll(buf) {
50
+ function zstdDecompressAll(buf) {
51
51
  const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
52
52
  const starts = [];
53
53
  for (let i = 0; i + 4 <= buf.length; i += 1) {
@@ -181,39 +181,6 @@ export function firstUserText(log, maxChars = 30) {
181
181
  }
182
182
  return null;
183
183
  }
184
- /** 递归列出目录下的会话日志文件(按 mtime 倒序) */
185
- export function findSessionFiles(root) {
186
- if (!existsSync(root))
187
- return [];
188
- const out = [];
189
- const walk = (dir, depth) => {
190
- if (depth > 3)
191
- return;
192
- let entries;
193
- try {
194
- entries = readdirSync(dir, { withFileTypes: true });
195
- }
196
- catch {
197
- return;
198
- }
199
- for (const entry of entries) {
200
- const full = join(dir, entry.name);
201
- if (entry.isDirectory())
202
- walk(full, depth + 1);
203
- else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl"))
204
- out.push(full);
205
- }
206
- };
207
- walk(root, 0);
208
- return out.sort((left, right) => {
209
- try {
210
- return statSync(right).mtimeMs - statSync(left).mtimeMs;
211
- }
212
- catch {
213
- return 0;
214
- }
215
- });
216
- }
217
184
  /** 按会话 id 找它的日志文件:优先用 cwd 直算目录,否则在 sessions 下一层找 */
218
185
  export function locateSessionFile(sessionRoot, sessionId, cwd) {
219
186
  const candidates = [];
@@ -243,3 +210,47 @@ export function locateSessionFile(sessionRoot, sessionId, cwd) {
243
210
  }
244
211
  return null;
245
212
  }
213
+ /**
214
+ * 删掉一条会话在 DSH 自己存储里的目录(`<sessions>/<projectKey>/<sessionId>/`)。
215
+ *
216
+ * 用途只有一个:**模型目录探针**临时开的那条会话(见 catalog-probe.ts)别留在
217
+ * DSH 的 web / tui 历史里。做法是先按 cwd 直算、再扫一层(与 locateSessionFile 同样的
218
+ * 两条路径)—— 目录命名细节不对也知道要删哪个,删不掉就算了,侧栏还要按临时目录过滤。
219
+ *
220
+ * 返回 true = 真的删掉了。
221
+ */
222
+ export function deleteDshSession(sessionRoot, sessionId, cwd) {
223
+ const groups = [];
224
+ if (cwd)
225
+ groups.push(projectKey(cwd));
226
+ try {
227
+ for (const entry of readdirSync(sessionRoot, { withFileTypes: true })) {
228
+ if (entry.isDirectory() && !groups.includes(entry.name))
229
+ groups.push(entry.name);
230
+ }
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ let removed = false;
236
+ for (const group of groups) {
237
+ const dir = join(sessionRoot, group, sessionId);
238
+ if (!existsSync(dir))
239
+ continue;
240
+ try {
241
+ rmSync(dir, { recursive: true, force: true });
242
+ removed = true;
243
+ // 组目录空了就一起删(DSH 自己也会留一堆空目录,但没必要再添一个)
244
+ try {
245
+ rmdirSync(join(sessionRoot, group));
246
+ }
247
+ catch {
248
+ /* 还有别的会话在里头 */
249
+ }
250
+ }
251
+ catch {
252
+ /* 删不掉就算了 */
253
+ }
254
+ }
255
+ return removed;
256
+ }
@@ -34,7 +34,7 @@ export function parseModelValue(value) {
34
34
  }
35
35
  }
36
36
  /** 我们在界面上用的模型 id:`provider/model`(可读、可回填) */
37
- export function modelIdOf(choice) {
37
+ function modelIdOf(choice) {
38
38
  const parsed = parseModelValue(choice.value);
39
39
  return parsed ? `${parsed.provider}/${parsed.model}` : null;
40
40
  }
@@ -51,7 +51,7 @@ export function modelOptions(config) {
51
51
  models.push({
52
52
  id,
53
53
  label: choice.name ?? id,
54
- ...((current && id === `${current.provider}/${current.model}`) ? { current: true } : {}),
54
+ ...(current && id === `${current.provider}/${current.model}` ? { current: true } : {}),
55
55
  });
56
56
  }
57
57
  return models;
@@ -121,9 +121,7 @@ function textOfContent(update) {
121
121
  if (!content)
122
122
  return "";
123
123
  if (Array.isArray(content)) {
124
- return content
125
- .map((item) => (item?.content?.type === "text" ? (item.content.text ?? "") : ""))
126
- .join("");
124
+ return content.map((item) => (item?.content?.type === "text" ? (item.content.text ?? "") : "")).join("");
127
125
  }
128
126
  return content.type === "text" ? (content.text ?? "") : "";
129
127
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 把「下一步建议」的写法带进 dsh(ADR-0022 的 dsh 版)。
3
+ *
4
+ * 为什么只能走 patch 文件:ACP 是**唯一**入口,而它的 `session/new` 只有
5
+ * `cwd / additionalDirectories / mcpServers / _meta`(schema 实测),没有系统提示字段;
6
+ * dsh 的系统提示由插件栈里的 `@deepseek-ai/dsh-system-prompt` 组装,其中
7
+ * `personaSuffix` 排在系统提示末尾 —— 正好是这段说明该待的位置。
8
+ *
9
+ * 两个必须写清的约束:
10
+ * 1. patch 的 `config` 是**整块替换**(不是深合并):只写 `personaSuffix` 会把 acp 模板
11
+ * 原本的 `personaPrefix` 冲掉(实测 dump 里会整个消失)。所以两句 persona 都带上,
12
+ * 取值照抄 `@deepseek-ai/dsh-acp-app` 的默认值;dsh 改这两句默认值时这里要跟着改。
13
+ * 2. 它是**进程级**的:dsh 子进程启动时才 compose 配置栈,所以改完「想想」条数要
14
+ * 重启工作台(引擎)才生效 —— 不像 pi / codex 那样每条新会话现读。
15
+ */
16
+ import { mkdirSync, writeFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ /** `@deepseek-ai/dsh-acp-app` 给 acp profile 的默认 persona(照抄,别改措辞) */
19
+ const PERSONA_PREFIX = "You are a coding agent powered by the {{model}} model.";
20
+ const PERSONA_SUFFIX = "Your working directory is {{cwd}}.";
21
+ /** YAML 块标量的缩进:`personaSuffix: |` 下的内容比 key 深两级 */
22
+ function indentBlock(text) {
23
+ return text
24
+ .split("\n")
25
+ .map((line) => (line === "" ? "" : ` ${line}`))
26
+ .join("\n");
27
+ }
28
+ /** 纯函数:生成 patch 文件内容(喂给 `dsh --patch <file>`) */
29
+ export function dshSuggestionPatch(guidance) {
30
+ return [
31
+ "# 由 oh-my-agent-web 生成(不要手改):给 dsh 的 system prompt 追加「下一步建议」写法。",
32
+ "# 改条数请改设置里的「想想」,然后重启工作台 —— dsh 的配置栈是进程启动时 compose 的。",
33
+ "- id: system-prompt",
34
+ " config:",
35
+ " personaPrefix: |",
36
+ indentBlock(PERSONA_PREFIX),
37
+ " personaSuffix: |",
38
+ indentBlock(`${PERSONA_SUFFIX}\n\n${guidance}`),
39
+ "",
40
+ ].join("\n");
41
+ }
42
+ /** 写到一个临时目录里(不污染用户的 `dsh-patches` 目录),返回 patch 文件路径 */
43
+ export function writeDshSuggestionPatch(dir, guidance) {
44
+ mkdirSync(dir, { recursive: true });
45
+ const file = join(dir, "dsh-suggestion-patch.yml");
46
+ writeFileSync(file, dshSuggestionPatch(guidance), "utf8");
47
+ return file;
48
+ }
@@ -10,8 +10,8 @@
10
10
  * 拿 header(cwd/createdAt)与 `session/title`,缓存里没有时兜底。
11
11
  *
12
12
  * 为什么要自己扫目录:ACP 的 `session/list` 只回 `{sessionId, cwd}`,**没有标题和时间**;
13
- * 侧栏需要标题/时间/模型,所以直接读 DSH 自己的持久化(与 pi-web-ui 的做法一致,但我们
14
- * 额外用了 workspace.json 与投影缓存,避免每个会话都解压日志)。
13
+ * 侧栏需要标题/时间/模型,所以直接读 DSH 自己的持久化,并额外用 workspace.json 与投影
14
+ * 缓存,避免每个会话都解压日志)。
15
15
  */
16
16
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
17
17
  import { homedir } from "node:os";
@@ -19,7 +19,7 @@ import { basename, join } from "node:path";
19
19
  import { firstUserText, locateSessionFile, readSessionHeader, readSessionLog, sessionTitle } from "./dsh-log.js";
20
20
  import { isScratchCwd } from "./scratch.js";
21
21
  /** 读 workspace.json;没有/坏了都返回空表(侧栏退回按 cwd 分组) */
22
- export function readWorkspaces(home) {
22
+ function readWorkspaces(home) {
23
23
  const file = join(home, "storages", "workspace.json");
24
24
  if (!existsSync(file))
25
25
  return {};
@@ -31,7 +31,7 @@ export function readWorkspaces(home) {
31
31
  }
32
32
  }
33
33
  /** workspace.json → path → 项目名(title 优先,缺了用目录名) */
34
- export function workspaceNames(workspaces) {
34
+ function workspaceNames(workspaces) {
35
35
  const out = new Map();
36
36
  for (const row of Object.values(workspaces.tables?.workspaces ?? {})) {
37
37
  const path = row.path?.trim();
@@ -149,18 +149,17 @@ function summaryOf(home, session) {
149
149
  };
150
150
  }
151
151
  /** 纯分组:会话 → 项目(workspace 的 path 精确匹配;否则按 cwd 单独成组) */
152
- export function groupDshSessions(sessions, names, options = {}) {
152
+ function groupDshSessions(sessions, names, options = {}) {
153
153
  const groups = new Map();
154
154
  for (const session of sessions) {
155
155
  if (options.archived?.has(session.id))
156
156
  continue;
157
157
  const key = session.cwd;
158
- const group = groups.get(key) ??
159
- {
160
- cwd: session.cwd,
161
- name: names.get(session.cwd) ?? basename(session.cwd) ?? session.cwd,
162
- sessions: [],
163
- };
158
+ const group = groups.get(key) ?? {
159
+ cwd: session.cwd,
160
+ name: names.get(session.cwd) ?? basename(session.cwd) ?? session.cwd,
161
+ sessions: [],
162
+ };
164
163
  group.sessions.push({
165
164
  id: session.id,
166
165
  engineSessionId: session.id,
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * 为什么走 ACP(`dsh --profile acp`)而不是 SDK(`--profile sdk`)或 headless:
5
5
  * - SDK 面只有 `initialize / session/prompt / shutdown`,没有会话列表/续接/取消/配置/权限,
6
- * 这些要么自己读文件、要么自己写插件补(pi-web-ui 就是这么干的,代价是 26 个本地依赖 + 重启换模型);
6
+ * 这些要么自己读文件、要么自己写插件补(等于把 26 个本地依赖 + 重启换模型揽进来);
7
7
  * - ACP 原生给了 `session/new|list|resume|close`、`session/set_config_option`(模型/思考档)、
8
8
  * `session/cancel`、`session/request_permission`,我们只需当客户端;
9
9
  * - headless 是一次性进程,没有权限问询。
10
10
  *
11
- * 协议细节见 ADR-0037;映射规则在 dsh-map.ts(纯函数,单测覆盖)。
11
+ * 协议细节见 ADR-0037;映射规则在 dsh-map.ts(纯函数)。
12
12
  */
13
13
  import { createJsonRpcStdioClient } from "./jsonrpc-stdio.js";
14
14
  /** ACP 要求初始化的能力协商;我们不用它的 fs/terminal 桥(工具都在 DSH 进程里跑) */
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { createDshClient } from "./dsh-transport.js";
4
- import { locateSessionFile, readSessionHeader, readSessionLog } from "./dsh-log.js";
4
+ import { withProbeCwd } from "./catalog-probe.js";
5
+ import { deleteDshSession, locateSessionFile, readSessionHeader, readSessionLog } from "./dsh-log.js";
5
6
  import { logToTranscript, logToUiMessages, statsFromLog } from "./dsh-history.js";
6
7
  import { sanitizeHistoricalActions } from "../markers/actions.js";
7
8
  import { currentModelId, currentThinkingLevel, friendlyDshError, modelOptions, modelValueFor, parseModelValue, permissionApprovalRequest, permissionOptionId, pickDefaultModelValue, stopReasonToTurnEnd, thinkingLevels, updateToEvent, } from "./dsh-map.js";
@@ -24,6 +25,8 @@ export function createDshEngine(options = {}) {
24
25
  * 真正切模型时仍会先 resume(拿不到就报可读错误)。
25
26
  */
26
27
  let lastConfig = [];
28
+ /** 目录探针跑过没有(每个实例只探一次;`reloadModels` / `dispose` 重置) */
29
+ let catalogProbed = false;
27
30
  const rememberTool = (id, update) => {
28
31
  toolFacts.set(id, { title: update.title, rawInput: update.rawInput });
29
32
  if (toolFacts.size > MAX_TOOL_FACTS) {
@@ -60,6 +63,30 @@ export function createDshEngine(options = {}) {
60
63
  return binding.config;
61
64
  return [...bindings.values()].find((item) => item.config.length > 0)?.config ?? lastConfig;
62
65
  };
66
+ /**
67
+ * 没有会话时把模型目录弄进来(原因与代价见 catalog-probe.ts 文件头):
68
+ * DSH 的目录只在 `session/new` 的 `configOptions` 里 → 开个临时会话抄一份,
69
+ * 然后 `session/close` 并删掉它在 `~/.dsh` 里的会话目录(不在 DSH 自己的历史里留空会话)。
70
+ */
71
+ const warmCatalog = async () => {
72
+ if (catalogProbed || lastConfig.length > 0)
73
+ return;
74
+ catalogProbed = true;
75
+ await withProbeCwd(async (cwd) => {
76
+ const created = await client.request("session/new", {
77
+ cwd,
78
+ mcpServers: [],
79
+ });
80
+ const probeId = created?.sessionId;
81
+ if (!probeId)
82
+ return;
83
+ applyConfig(undefined, created.configOptions);
84
+ await client.request("session/close", { sessionId: probeId }).catch(() => undefined);
85
+ if (!deleteDshSession(sessionsRoot, probeId, cwd)) {
86
+ options.log?.(`dsh:模型目录探针会话没删掉(${probeId}),侧栏不会显示(临时目录)`);
87
+ }
88
+ });
89
+ };
63
90
  /** 历史会话首次使用时才 resume(ACP 的 resume 会拿到写句柄,所以按需) */
64
91
  const ensureLive = async (conversationId) => {
65
92
  const binding = bindingOf(conversationId);
@@ -147,9 +174,24 @@ export function createDshEngine(options = {}) {
147
174
  version: () => client.version(),
148
175
  async prewarm() {
149
176
  await client.ensureStarted();
177
+ // 顺手把模型目录也弄进来:否则切过来、还没开会话时下拉是空的(见 catalog-probe.ts)
178
+ await warmCatalog().catch((error) => {
179
+ options.log?.(`dsh:读取模型目录失败:${error instanceof Error ? error.message : String(error)}`);
180
+ });
181
+ },
182
+ /** 重读模型表(改完 dsh 自己的 provider / 模型配置后):清掉快照,重新探一次 */
183
+ async reloadModels() {
184
+ lastConfig = [];
185
+ catalogProbed = false;
186
+ await warmCatalog().catch((error) => {
187
+ options.log?.(`dsh:重读模型目录失败:${error instanceof Error ? error.message : String(error)}`);
188
+ });
150
189
  },
151
190
  async createSession(sessionOptions) {
152
- const created = await client.request("session/new", { cwd: sessionOptions.workspace, mcpServers: [] });
191
+ const created = await client.request("session/new", {
192
+ cwd: sessionOptions.workspace,
193
+ mcpServers: [],
194
+ });
153
195
  const sessionId = created?.sessionId;
154
196
  if (!sessionId)
155
197
  throw new Error("dsh 没有返回 sessionId");
@@ -222,6 +264,7 @@ export function createDshEngine(options = {}) {
222
264
  },
223
265
  async dispose() {
224
266
  lastConfig = [];
267
+ catalogProbed = false;
225
268
  bindings.clear();
226
269
  bySession.clear();
227
270
  active.clear();
@@ -0,0 +1,41 @@
1
+ export function createEngineVersionProber(options) {
2
+ const cache = new Map();
3
+ const inflight = new Map();
4
+ const probe = (descriptor) => {
5
+ const known = inflight.get(descriptor.name);
6
+ if (known)
7
+ return known;
8
+ const pending = (async () => {
9
+ try {
10
+ const value = await descriptor.version?.();
11
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
12
+ }
13
+ catch (error) {
14
+ options.onLog?.(`探测 ${descriptor.name} 的版本号失败:${error instanceof Error ? error.message : String(error)}`);
15
+ return null;
16
+ }
17
+ })();
18
+ inflight.set(descriptor.name, pending);
19
+ return pending;
20
+ };
21
+ const snapshot = () => Object.fromEntries(cache);
22
+ return {
23
+ known: snapshot,
24
+ remember(name, version) {
25
+ // 握手给的版本最准:记下来,后面 refresh 也不会把它擦掉
26
+ if (version && version.trim() !== "")
27
+ cache.set(name, version.trim());
28
+ },
29
+ async refresh() {
30
+ await Promise.all(options.descriptors().map(async (descriptor) => {
31
+ if (cache.has(descriptor.name))
32
+ return;
33
+ const value = await probe(descriptor);
34
+ // 期间可能有 remember()(握手)写过更准的值,别覆盖
35
+ if (!cache.has(descriptor.name))
36
+ cache.set(descriptor.name, value);
37
+ }));
38
+ return snapshot();
39
+ },
40
+ };
41
+ }
@@ -1,6 +1,6 @@
1
1
  const BIG_OUTPUT_CHARS = 200 * 1024;
2
2
  /** 触发词 → 脚本。默认行为:把用户输入原样念一遍,证明「逐字说话」。 */
3
- export function planFor(text) {
3
+ function planFor(text) {
4
4
  const key = text.toLowerCase();
5
5
  if (key.includes("boom")) {
6
6
  return [
@@ -80,6 +80,8 @@ export function createFakeEngine(options = {}) {
80
80
  const switchedModels = [];
81
81
  let approvalHandler = null;
82
82
  let toolCallSeq = 0;
83
+ /** 派生会话的 id 序号(真引擎是 pi 的 `createSessionId()`,同样不复用传进来的 id) */
84
+ let forkSeq = 0;
83
85
  async function streamText(sessionId, text, onEvent, slow) {
84
86
  const buffer = sessions.get(sessionId)?.transcript;
85
87
  for (let index = 0; index < text.length; index += chunkChars) {
@@ -95,12 +97,41 @@ export function createFakeEngine(options = {}) {
95
97
  return {
96
98
  version: () => "fake",
97
99
  async createSession(sessionOptions) {
98
- sessions.set(sessionOptions.sessionId, {
99
- workspace: sessionOptions.workspace,
100
- transcript: [],
101
- sessionPath: `fake-sessions/${sessionOptions.sessionId}.jsonl`,
102
- });
103
- return { sessionPath: `fake-sessions/${sessionOptions.sessionId}.jsonl` };
100
+ const ephemeral = sessionOptions.ephemeral === true;
101
+ /** 派生:把源会话的转录拄过来(按 `fake-<会话>-<序号>` 里的序号截断) */
102
+ let transcript = [];
103
+ if (sessionOptions.fork) {
104
+ const source = sessions.get(sessionOptions.fork.sourceSessionId);
105
+ // 与真引擎一致:没落盘的会话不能 fork
106
+ if (!source?.sessionPath)
107
+ throw new Error("这条会话还没存过盘(临时对话),不能从它派生分支");
108
+ const cut = Number(/-(\d+)$/.exec(sessionOptions.fork.leafId)?.[1] ?? Number.NaN);
109
+ transcript = Number.isFinite(cut) ? source.transcript.slice(0, cut + 1) : [...source.transcript];
110
+ // 与真引擎一致(pi 的 `createBranchedSession`):派生会**另生一个会话 id**,
111
+ // 不采纳传进来的那个。宿主必须拿回报的 id 当会话 id,否则发消息就会
112
+ // `engine.no_session`(引擎表里只有新 id)—— 这里必须忠实复现,不然测试盖不住。
113
+ forkSeq += 1;
114
+ const forkedId = `forked-${forkSeq}`;
115
+ const forkedPath = `fake-sessions/${forkedId}.jsonl`;
116
+ sessions.set(forkedId, { workspace: sessionOptions.workspace, transcript, sessionPath: forkedPath });
117
+ return { sessionPath: forkedPath, sessionId: forkedId };
118
+ }
119
+ // 临时会话没有会话文件(与 pi 的 `--no-session` 一致)
120
+ const sessionPath = ephemeral ? null : `fake-sessions/${sessionOptions.sessionId}.jsonl`;
121
+ sessions.set(sessionOptions.sessionId, { workspace: sessionOptions.workspace, transcript, sessionPath });
122
+ return { sessionPath };
123
+ },
124
+ /** 临时会话转正:给它一个文件路径(真引擎是原生导出写盘) */
125
+ async persistSession(sessionId) {
126
+ const session = sessions.get(sessionId);
127
+ if (!session)
128
+ return { sessionPath: null };
129
+ if (session.sessionPath)
130
+ return { sessionPath: session.sessionPath };
131
+ if (session.transcript.length === 0)
132
+ return { sessionPath: null };
133
+ session.sessionPath = `fake-sessions/${sessionId}.jsonl`;
134
+ return { sessionPath: session.sessionPath };
104
135
  },
105
136
  /** 打开已有会话(ADR-0021 §2):假引擎没有真文件,只登记「已打开」 */
106
137
  async openSession(openOptions) {
@@ -22,7 +22,7 @@ async function readJson(path, fallback) {
22
22
  }
23
23
  }
24
24
  /** 把 pi 的两份配置读成「可直接 registerProvider 的形状」 */
25
- export async function readProviderConfigs(agentDir) {
25
+ async function readProviderConfigs(agentDir) {
26
26
  const catalog = await readJson(join(agentDir, "models.json"), {});
27
27
  const store = await readJson(join(agentDir, "models-store.json"), {});
28
28
  const auth = await readJson(join(agentDir, "auth.json"), {});
@@ -1,5 +1,5 @@
1
1
  /**
2
- * opencode ACP 的**纯映射**(不碰网络/子进程,单测直接覆盖)。
2
+ * opencode ACP 的**纯映射**(不碰网络/子进程)。
3
3
  *
4
4
  * 三件事:
5
5
  * 1. `configOptions`(模型 / 思考档)→ 我们的模型下拉与档位;
@@ -90,7 +90,7 @@ export function modelValueFor(config, modelId) {
90
90
  * 思考档那一项:按 `category: "thought_level"` 找(dsh 叫 reasoning_effort,opencode 叫 effort)。
91
91
  * 不支持推理的模型没有这一项 → 返回 undefined,界面自动隐藏控件。
92
92
  */
93
- export function thinkingOptionOf(config) {
93
+ function thinkingOptionOf(config) {
94
94
  return (config ?? []).find((entry) => entry.category === "thought_level" || entry.id === "effort" || entry.id === "reasoning_effort");
95
95
  }
96
96
  export function thinkingLevels(config) {
@@ -17,7 +17,7 @@ import { existsSync } from "node:fs";
17
17
  import { homedir } from "node:os";
18
18
  import { join } from "node:path";
19
19
  /** opencode 的库文件:跟随 XDG(它自己也是这么找数据的) */
20
- export function opencodeDbFile(env = process.env, home = homedir()) {
20
+ function opencodeDbFile(env = process.env, home = homedir()) {
21
21
  const xdg = env.XDG_DATA_HOME?.trim();
22
22
  const dataHome = xdg && xdg !== "" ? xdg : join(home, ".local", "share");
23
23
  return join(dataHome, "opencode", "opencode.db");
@@ -30,13 +30,13 @@ const timestampOf = (value) => {
30
30
  return Number.isFinite(parsed) ? parsed : 0;
31
31
  };
32
32
  /**
33
- * 纯分组:`session/list` 的行 → 侧栏形状(单测直接覆盖)。
33
+ * 纯分组:`session/list` 的行 → 侧栏形状。
34
34
  *
35
35
  * - 临时目录(/tmp、/var/folders…)里的一次性会话丢掉,别塞噪音进侧栏;
36
36
  * - 同 cwd 归一组,组内按更新时间倒序;组之间按「最近一条会话」倒序;
37
37
  * - 没有标题的会话不编名字(opencode 自己会给「New session - …」,照它的原文显示)。
38
38
  */
39
- export function groupOpencodeSessions(rows) {
39
+ function groupOpencodeSessions(rows) {
40
40
  // 逐个项目点名时同一条会话会被问到多次(项目根与它的子目录都解析到同一个项目),先去重:
41
41
  // 同一个 sessionId 只留 updatedAt 最新的一条。
42
42
  const byId = new Map();
@@ -1,4 +1,5 @@
1
1
  import { isScratchCwd } from "./scratch.js";
2
+ import { withProbeCwd } from "./catalog-probe.js";
2
3
  import { currentModelId, currentThinkingLevel, exportStats, exportToTranscript, exportToUiMessages, friendlyOpencodeError, hasPendingUsage, hasUsableToolFacts, isFinalToolStatus, modelOptions, modelValueFor, parseModelValue, permissionApprovalRequest, permissionOptionId, stopReasonToTurnEnd, textEventOf, thinkingConfigId, thinkingLevels, toolEndEvent, toolStartEvent, usageEventOf, } from "./opencode-map.js";
3
4
  import { deleteOpencodeSession, readOpencodeExport } from "./opencode-sessions.js";
4
5
  import { createOpencodeClient } from "./opencode-transport.js";
@@ -16,6 +17,7 @@ const EXPORT_CACHE_MS = 3000;
16
17
  const EXPORT_SETTLE_MS = 3000;
17
18
  export function createOpencodeEngine(options = {}) {
18
19
  const bin = options.bin ?? "opencode";
20
+ const removeSession = options.deleteSession ?? ((engineSessionId) => deleteOpencodeSession(bin, engineSessionId));
19
21
  const client = options.client ?? createOpencodeClient({ bin, args: options.args, log: options.log ?? (() => { }) });
20
22
  const bindings = new Map();
21
23
  const byEngineSession = new Map();
@@ -33,6 +35,8 @@ export function createOpencodeEngine(options = {}) {
33
35
  * 真正切模型时仍会先 resume(拿不到就报可读错误)。
34
36
  */
35
37
  let lastConfig = [];
38
+ /** 目录探针跑过没有(每个实例只探一次;`reloadModels` / `dispose` 重置) */
39
+ let catalogProbed = false;
36
40
  const rememberTool = (facts) => {
37
41
  const previous = toolFacts.get(facts.toolCallId);
38
42
  // 后到的 update 更详细(title 从 "bash" 变成真命令),合并而不是覆盖
@@ -57,6 +61,27 @@ export function createOpencodeEngine(options = {}) {
57
61
  return binding.config;
58
62
  return [...bindings.values()].find((item) => item.config.length > 0)?.config ?? lastConfig;
59
63
  };
64
+ /**
65
+ * 没有会话时把模型目录弄进来(原因与代价见 catalog-probe.ts 文件头)。
66
+ * opencode 的目录只在 `session/new` 的 `configOptions` 里,所以开一个临时会话抄一份,
67
+ * 然后 `session/close` + `opencode session delete` 收掉(不留在 CLI/TUI 的历史里)。
68
+ */
69
+ const warmCatalog = async () => {
70
+ if (catalogProbed || lastConfig.length > 0)
71
+ return;
72
+ catalogProbed = true;
73
+ await withProbeCwd(async (cwd) => {
74
+ const created = await client.request("session/new", { cwd, mcpServers: [] });
75
+ const engineSessionId = created?.sessionId;
76
+ if (!engineSessionId)
77
+ return;
78
+ applyConfig(undefined, created.configOptions);
79
+ await client.request("session/close", { sessionId: engineSessionId }).catch(() => undefined);
80
+ await removeSession(engineSessionId).catch((error) => {
81
+ options.log?.(`opencode:模型目录探针会话没删掉(${engineSessionId}):${error instanceof Error ? error.message : String(error)}`);
82
+ });
83
+ });
84
+ };
60
85
  const invalidateExport = (conversationId) => {
61
86
  const binding = bindingOf(conversationId);
62
87
  if (binding)
@@ -202,6 +227,18 @@ export function createOpencodeEngine(options = {}) {
202
227
  version: () => client.version(),
203
228
  async prewarm() {
204
229
  await client.ensureStarted();
230
+ // 顺手把模型目录也弄进来:否则切过来、还没开会话时下拉是空的(见 catalog-probe.ts)
231
+ await warmCatalog().catch((error) => {
232
+ options.log?.(`opencode:读取模型目录失败:${error instanceof Error ? error.message : String(error)}`);
233
+ });
234
+ },
235
+ /** 重读模型表(改完 opencode 自己的配置后):清掉快照,重新探一次 */
236
+ async reloadModels() {
237
+ lastConfig = [];
238
+ catalogProbed = false;
239
+ await warmCatalog().catch((error) => {
240
+ options.log?.(`opencode:重读模型目录失败:${error instanceof Error ? error.message : String(error)}`);
241
+ });
205
242
  },
206
243
  async createSession(sessionOptions) {
207
244
  const created = await client
@@ -324,6 +361,7 @@ export function createOpencodeEngine(options = {}) {
324
361
  toolFacts.clear();
325
362
  exportCache.clear();
326
363
  settledAt.clear();
364
+ catalogProbed = false; // 探针会话随进程一起没了:下次切回来重新探
327
365
  client.close();
328
366
  },
329
367
  setApprovalHandler(handler) {
@@ -387,7 +425,7 @@ export function createOpencodeEngine(options = {}) {
387
425
  },
388
426
  /** opencode 原生删除(`opencode session delete <sid>`);改名没接(ACP 无此方法) */
389
427
  async deleteSession(handle) {
390
- await deleteOpencodeSession(bin, handle);
428
+ await removeSession(handle);
391
429
  exportCache.delete(handle);
392
430
  },
393
431
  };
@@ -2,7 +2,7 @@
2
2
  * SDK 会话事件 → 端口事件的映射(ADR-0003 的端口实现里最容易出错的一层)。
3
3
  *
4
4
  * 从 `pi-sdk.ts` 拆出(400 行护栏):这是**纯映射**,与「怎么建会话、怎么订阅」无关,
5
- * 所以可以脱离 SDK 用合成事件直接测(单测覆盖整张映射表,不烧 token)。
5
+ * 所以可以脱离 SDK 用合成事件直接驱动(整张映射表都能覆盖,不烧 token)。
6
6
  */
7
7
  import { parseUsage } from "../util/usage.js";
8
8
  function asRecord(value) {
@@ -22,7 +22,7 @@ export function resultText(result) {
22
22
  .join("\n");
23
23
  }
24
24
  /** 把 bash 工具结果的退出码抠出来(不同工具 details 形状不同,取不到就不给) */
25
- export function resultExitCode(result) {
25
+ function resultExitCode(result) {
26
26
  const details = asRecord(asRecord(result).details);
27
27
  const value = details.exitCode ?? asRecord(asRecord(result).structuredContent).exitCode;
28
28
  return typeof value === "number" ? value : undefined;
@@ -30,7 +30,7 @@ export function resultExitCode(result) {
30
30
  /** 一行预览的长度:超过就截(参数再长,抬头也只有一行) */
31
31
  const SUMMARY_CHARS = 200;
32
32
  /** 完整参数的上限:不能让一条快照因参数膨胀(正常命令远小于这个数) */
33
- export const FULL_ARGS_CHARS = 4000;
33
+ const FULL_ARGS_CHARS = 4000;
34
34
  /** 把任意参数变成文本(字符串原样返回:有的引擎直接给命令串,不是 JSON) */
35
35
  function argsText(args) {
36
36
  if (typeof args === "string")
@@ -63,7 +63,7 @@ function providerError(raw) {
63
63
  }
64
64
  /**
65
65
  * 纯函数:把 SDK 会话事件翻译成端口事件。返回 null 表示这个事件不影响 UI。
66
- * (单测用合成事件直接覆盖这张映射表 —— 不需要真的连模型。)
66
+ * (合成事件就能覆盖这张映射表 —— 不需要真的连模型。)
67
67
  */
68
68
  export function mapSessionEvent(event) {
69
69
  const record = asRecord(event);