oh-my-agent-web 0.2.0 → 0.4.0

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 (250) hide show
  1. package/README.md +86 -447
  2. package/dist/server/approval/read-only.js +107 -0
  3. package/dist/server/approval/rules.js +85 -0
  4. package/dist/server/approval/scope.js +150 -0
  5. package/dist/server/brand.js +2 -0
  6. package/dist/server/cli-config.js +52 -0
  7. package/dist/server/cli-info.js +101 -0
  8. package/dist/server/cli.js +304 -0
  9. package/dist/server/engine/builtin.js +61 -0
  10. package/dist/server/engine/catalog.js +8 -0
  11. package/dist/server/engine/codex-approvals.js +45 -0
  12. package/dist/server/engine/codex-map.js +298 -0
  13. package/dist/server/engine/codex-sessions.js +159 -0
  14. package/dist/server/engine/codex-transport.js +49 -0
  15. package/dist/server/engine/codex.js +314 -0
  16. package/dist/server/engine/dsh-history.js +195 -0
  17. package/dist/server/engine/dsh-log.js +245 -0
  18. package/dist/server/engine/dsh-map.js +221 -0
  19. package/dist/server/engine/dsh-sessions.js +207 -0
  20. package/dist/server/engine/dsh-transport.js +37 -0
  21. package/dist/server/engine/dsh.js +284 -0
  22. package/dist/server/engine/fake.js +231 -0
  23. package/dist/server/engine/jsonrpc-stdio.js +241 -0
  24. package/dist/server/engine/model-registry-reload.js +115 -0
  25. package/dist/server/engine/pi-events.js +148 -0
  26. package/dist/server/engine/pi-rename.js +14 -0
  27. package/dist/server/engine/pi-sdk.js +343 -0
  28. package/dist/server/engine/pi-session-model.js +23 -0
  29. package/dist/server/engine/pi-thinking.js +34 -0
  30. package/dist/server/engine/pi-transcript.js +225 -0
  31. package/dist/server/engine/pi-version.js +11 -0
  32. package/dist/server/engine/port.js +1 -0
  33. package/dist/server/engine/registry.js +47 -0
  34. package/dist/server/engine/scratch.js +13 -0
  35. package/dist/server/engine/suggestion-guidance.js +9 -0
  36. package/dist/server/engine/switchable.js +22 -0
  37. package/dist/server/engine/wiring.js +107 -0
  38. package/dist/server/files/dir-routes.js +40 -0
  39. package/dist/server/files/file-search.js +63 -0
  40. package/dist/server/files/file-service.js +232 -0
  41. package/dist/server/files/files-routes.js +182 -0
  42. package/dist/server/files/git-info.js +186 -0
  43. package/dist/server/http/auth-routes.js +99 -0
  44. package/dist/server/http/auth.js +191 -0
  45. package/dist/server/http/aux-routes.js +24 -0
  46. package/dist/server/http/compress.js +75 -0
  47. package/dist/server/http/health-and-static.js +33 -0
  48. package/dist/server/http/live-routes.js +113 -0
  49. package/dist/server/http/login-page.js +200 -0
  50. package/dist/server/index.js +342 -520
  51. package/dist/server/markers/actions.js +262 -0
  52. package/dist/server/protocol-parse.js +201 -0
  53. package/dist/server/protocol-settings.js +1 -0
  54. package/dist/server/protocol.js +6 -0
  55. package/dist/server/providers/catalog-store.js +85 -0
  56. package/dist/server/providers/catalog-writes.js +197 -0
  57. package/dist/server/providers/catalog.js +77 -0
  58. package/dist/server/providers/enabled-models.js +118 -0
  59. package/dist/server/providers/import-models.js +69 -0
  60. package/dist/server/providers/import-routes.js +136 -0
  61. package/dist/server/providers/model-test.js +52 -0
  62. package/dist/server/providers/probe.js +64 -0
  63. package/dist/server/providers/provider-handlers.js +203 -0
  64. package/dist/server/providers/provider-store.js +327 -0
  65. package/dist/server/providers/request-headers.js +7 -0
  66. package/dist/server/providers/validate.js +241 -0
  67. package/dist/server/push/push-routes.js +114 -0
  68. package/dist/server/push/push.js +120 -0
  69. package/dist/server/session/approval-bridge.js +103 -0
  70. package/dist/server/session/attachment-resolve.js +21 -0
  71. package/dist/server/session/client-view.js +82 -0
  72. package/dist/server/session/conversation-create.js +65 -0
  73. package/dist/server/session/delete-session.js +84 -0
  74. package/dist/server/session/engine-switch.js +108 -0
  75. package/dist/server/session/lock.js +82 -0
  76. package/dist/server/session/model-controller.js +111 -0
  77. package/dist/server/session/notices.js +31 -0
  78. package/dist/server/session/pi-opener.js +123 -0
  79. package/dist/server/session/pi-refresh.js +51 -0
  80. package/dist/server/session/pi-sessions-provider.js +46 -0
  81. package/dist/server/session/pi-sessions.js +272 -0
  82. package/dist/server/session/refresh-actions.js +42 -0
  83. package/dist/server/session/rename-session.js +23 -0
  84. package/dist/server/session/runtime-services.js +28 -0
  85. package/dist/server/session/runtime-types.js +1 -0
  86. package/dist/server/session/runtime.js +358 -0
  87. package/dist/server/session/session-stats.js +88 -0
  88. package/dist/server/session/settings.js +367 -0
  89. package/dist/server/session/snapshot-for.js +7 -0
  90. package/dist/server/session/stats-refresher.js +50 -0
  91. package/dist/server/session/switch-conversation.js +86 -0
  92. package/dist/server/session/throttle.js +14 -0
  93. package/dist/server/session/turn-events.js +126 -0
  94. package/dist/server/session/turn-finish.js +79 -0
  95. package/dist/server/session/turn-markers.js +26 -0
  96. package/dist/server/session/turn-queue.js +111 -0
  97. package/dist/server/session/turn-send.js +88 -0
  98. package/dist/server/session/turn-watchdog.js +77 -0
  99. package/dist/server/sessions/session-routes.js +17 -0
  100. package/dist/server/skills/skill-routes.js +28 -0
  101. package/dist/server/skills/skill-store.js +134 -0
  102. package/dist/server/snapshot/fan-out.js +10 -0
  103. package/dist/server/snapshot/messages.js +75 -0
  104. package/dist/server/snapshot/sender.js +62 -0
  105. package/dist/server/snapshot/state.js +193 -0
  106. package/dist/server/terminal/terminal-routes.js +176 -0
  107. package/dist/server/terminal/terminal-session.js +140 -0
  108. package/dist/server/uploads/upload-routes.js +64 -0
  109. package/dist/server/uploads/upload-store.js +115 -0
  110. package/dist/server/util/data-dir.js +36 -0
  111. package/dist/server/util/env.js +140 -0
  112. package/dist/server/util/ids.js +28 -0
  113. package/dist/server/util/log-file.js +67 -0
  114. package/dist/server/util/log.js +52 -0
  115. package/dist/server/util/origin.js +75 -0
  116. package/dist/server/util/paths.js +60 -0
  117. package/dist/server/util/pricing-index.js +121 -0
  118. package/dist/server/util/pricing.js +214 -0
  119. package/dist/server/util/startup.js +34 -0
  120. package/dist/server/util/upstream-version.js +140 -0
  121. package/dist/server/util/usage.js +19 -0
  122. package/dist/server/util/version.js +50 -0
  123. package/dist/server/ws/handle-message.js +95 -0
  124. package/package.json +49 -63
  125. package/web/dist/apple-touch-icon-precomposed.png +0 -0
  126. package/web/dist/apple-touch-icon.png +0 -0
  127. package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
  128. package/web/dist/assets/arc-DviL9o3c.js +1 -0
  129. package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
  130. package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
  131. package/web/dist/assets/array-BifhSqXX.js +1 -0
  132. package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
  133. package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
  134. package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
  135. package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
  136. package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  137. package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
  138. package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
  139. package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  140. package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
  141. package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
  142. package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
  143. package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  144. package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
  145. package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
  146. package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  147. package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  148. package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  149. package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
  150. package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
  151. package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
  152. package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
  153. package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
  154. package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  155. package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
  156. package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
  157. package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
  158. package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
  159. package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
  160. package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
  161. package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
  162. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  163. package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
  164. package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  165. package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
  166. package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
  167. package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
  168. package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
  169. package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
  170. package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
  171. package/web/dist/assets/dist-C86cKTuE.js +1 -0
  172. package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
  173. package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
  174. package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
  175. package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
  176. package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
  177. package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
  178. package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
  179. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
  180. package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  181. package/web/dist/assets/index-Bjn56a2i.js +76 -0
  182. package/web/dist/assets/index-Drp6e5iU.css +1 -0
  183. package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
  184. package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
  185. package/web/dist/assets/init-C-OQMol4.js +1 -0
  186. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
  187. package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
  188. package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
  189. package/web/dist/assets/katex-B_JcIwqj.js +257 -0
  190. package/web/dist/assets/line-CHV9_4lL.js +1 -0
  191. package/web/dist/assets/linear-C-eZsBTv.js +1 -0
  192. package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
  193. package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
  194. package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
  195. package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  196. package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
  197. package/web/dist/assets/path-fybaL0A-.js +1 -0
  198. package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
  199. package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
  200. package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
  201. package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
  202. package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
  203. package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
  204. package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
  205. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
  206. package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
  207. package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
  208. package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
  209. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  210. package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
  211. package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
  212. package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  213. package/web/dist/assets/src-Da91fG3g.js +1 -0
  214. package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
  215. package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
  216. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
  217. package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
  218. package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
  219. package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
  220. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
  221. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
  222. package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
  223. package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
  224. package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
  225. package/web/dist/favicon-32.png +0 -0
  226. package/web/dist/favicon.ico +0 -0
  227. package/web/dist/favicon.svg +20 -8
  228. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  229. package/web/dist/icons/icon-192.png +0 -0
  230. package/web/dist/icons/icon-512.png +0 -0
  231. package/web/dist/icons/icon-maskable-512.png +0 -0
  232. package/web/dist/index.html +21 -27
  233. package/web/dist/manifest.webmanifest +17 -0
  234. package/web/dist/maskable.svg +25 -0
  235. package/web/dist/sw.js +40 -0
  236. package/bin/oh-my-agent-web-ps.mjs +0 -203
  237. package/bin/oh-my-agent-web-ws.mjs +0 -593
  238. package/bin/oh-my-agent-web.mjs +0 -321
  239. package/bin/update-check.mjs +0 -142
  240. package/dist/server/codex-client.js +0 -310
  241. package/dist/server/codex-supervisor.js +0 -446
  242. package/dist/server/fs-service.js +0 -144
  243. package/dist/server/request-inspector.js +0 -458
  244. package/dist/shared/protocol.js +0 -112
  245. package/scripts/manage-server.mjs +0 -155
  246. package/scripts/omaw.mjs +0 -74
  247. package/shared/protocol.ts +0 -228
  248. package/web/dist/assets/index-B8-VzDf2.css +0 -1
  249. package/web/dist/assets/index-DeC2eiiq.js +0 -42
  250. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -0,0 +1,358 @@
1
+ /** 会话运行时:引擎事件 → UI 状态 / 协议事件,并把转录落盘。规则见 ADR。 */
2
+ import { createMarkerStreamFilter } from "../markers/actions.js";
3
+ import { createRefreshActions } from "./refresh-actions.js";
4
+ import { appendText, createMessage } from "../snapshot/messages.js";
5
+ import { createRuntimeServices } from "./runtime-services.js";
6
+ import { createStatsRefresher } from "./stats-refresher.js";
7
+ import { createThrottle } from "./throttle.js";
8
+ import { createRuntimeTurnFinisher, setRunning } from "./turn-finish.js";
9
+ import { cancelQueuedWith, createTurnPlumbing } from "./turn-queue.js";
10
+ import { createConversationFactory } from "./conversation-create.js";
11
+ import { createClientView } from "./client-view.js";
12
+ import { webVersion } from "../util/version.js";
13
+ import { startUpstreamVersionWatch } from "../util/upstream-version.js";
14
+ import { newConversationWith } from "./conversation-create.js";
15
+ import { renameSessionWith } from "./rename-session.js";
16
+ import { createSnapshotFor } from "./snapshot-for.js";
17
+ import { createSwitchConversation } from "./switch-conversation.js";
18
+ import { createModelController, createModelRefresher } from "./model-controller.js";
19
+ import { createPiSessionOpener } from "./pi-opener.js";
20
+ import { createSessionDeleter } from "./delete-session.js";
21
+ import { createTurnSender } from "./turn-send.js";
22
+ import { createSessionsRefresher } from "./pi-refresh.js";
23
+ import { applySettingsPatch, defaultSettings } from "./settings.js";
24
+ import { createTurnContext } from "./turn-events.js";
25
+ import { createEngineSwitcher, engineOptionsFor } from "./engine-switch.js";
26
+ import { createSwitchableEngine } from "../engine/switchable.js";
27
+ import { conversationId as newConversationId, makeIdFactory } from "../util/ids.js";
28
+ import { buildSnapshot, createServerState, emptyRunning, touchConversationIn } from "../snapshot/state.js";
29
+ export function createRuntime(options) {
30
+ const now = options.now ?? (() => Date.now());
31
+ const settings = options.settings;
32
+ const titleChars = options.titleChars ?? 24;
33
+ /** 「下一步」生成建议的超时(默认 20s;测试可调小) */
34
+ const actionsTimeoutMs = options.actionsTimeoutMs ?? 20_000;
35
+ // 引擎槽:下游模块拿到的都是这个稳定代理,切引擎时不用重建任何一个
36
+ const slot = createSwitchableEngine(options.engine);
37
+ const engine = slot.port;
38
+ const state = createServerState(options.workspace, options.engineName ?? "pi");
39
+ if (options.engineCapabilities)
40
+ state.engineCapabilities = { ...options.engineCapabilities };
41
+ state.engines = engineOptionsFor(options.engineName ?? "pi", state.engineCapabilities, options.engines?.descriptors());
42
+ state.files = { root: options.workspace, treeVersion: 0 };
43
+ // 默认真实运行用全局唯一 id(ADR-0019);测试注入确定性 id
44
+ const nextId = options.idSeq ?? makeIdFactory();
45
+ const turns = new Map();
46
+ const clientView = createClientView(); // 每客户端在看哪个会话(ADR-0033)
47
+ state.versions = { web: webVersion(), engine: engine.version?.() ?? null, latest: null }; // 抬头显示的版本号
48
+ /** sendMessage 自引用:队列排空时要能再次调用自己(对象字面量还没构建完成 → 用 ref) */
49
+ let apiRef = null;
50
+ const pushSnapshot = () => options.emit.snapshot(buildSnapshot(state));
51
+ // 上游有没有新版本(抬头那行标红用):后台查,拿到值才推一帧;
52
+ // 离线/超时一直拿不到就一直是 null —— 界面不标红,功能一个不受影响
53
+ startUpstreamVersionWatch((latest) => {
54
+ state.versions = { ...state.versions, latest };
55
+ pushSnapshot();
56
+ });
57
+ // 历史会话清单的刷新节奏(ADR-0021 §1):来源可换(切引擎),节奏与缓存只有一份
58
+ const sessionsRefresher = createSessionsRefresher({
59
+ initial: options.piSessions ?? null,
60
+ state,
61
+ minIntervalMs: options.piSessionsMinIntervalMs,
62
+ now,
63
+ remember: (projects) => piOpener.remember(projects),
64
+ onChanged: pushSnapshot,
65
+ onError: (message) => notice(message, "warn"),
66
+ });
67
+ const piRefresher = { refresh: sessionsRefresher.refresh };
68
+ const services = createRuntimeServices({
69
+ options,
70
+ now,
71
+ nextId,
72
+ onNotice: (entry) => {
73
+ // 按 id upsert:同 key 的提示是「替换」原行,不能当新行追加(否则同一 id 出现两次)
74
+ state.notices = [...state.notices.filter((item) => item.id !== entry.id), entry].slice(-(options.maxNotices ?? 20));
75
+ options.emit.event({ type: "event", event: "notice", notice: entry });
76
+ pushSnapshot();
77
+ },
78
+ onPending: (nextPending) => {
79
+ state.pendingApproval = nextPending;
80
+ state.running.status = nextPending ? "awaiting_approval" : "streaming";
81
+ pushSnapshot();
82
+ },
83
+ isWaitingForUser: () => state.pendingApproval !== null,
84
+ });
85
+ const { notice, approval, watchdog } = services;
86
+ /** 更新会话元数据(只在内存里,不再有自建索引) */
87
+ const touchConversation = (conversationId, patch = {}) => touchConversationIn(state, conversationId, now(), patch);
88
+ const piOpener = createPiSessionOpener({
89
+ engine,
90
+ onModel: (model) => {
91
+ state.model = model;
92
+ pushSnapshot();
93
+ },
94
+ onError: (message) => notice(message, "error"),
95
+ });
96
+ const conversations = createConversationFactory({
97
+ state,
98
+ engine,
99
+ opener: piOpener,
100
+ nextId,
101
+ // 会话 id 与 pi 一致用 UUID;测试注入确定性 id 时仍走 `conv-N`(断言好写)
102
+ newConversationId: options.idSeq ? () => nextId("conv") : newConversationId,
103
+ now,
104
+ titleChars,
105
+ defaultModelFor: (workspace) => state.settings.defaultModels[workspace] ?? piOpener.projectModel(workspace), // 项目默认 > 该项目最近会话,见 conversation-create
106
+ onReady: (id) => {
107
+ void models.refresh(id);
108
+ void refreshStats();
109
+ void models.refreshThinking(id);
110
+ // 新会话可能刚让引擎创建了一个项目(codex):立刻重扫,侧栏不用等下一轮
111
+ void piRefresher.refresh(true);
112
+ },
113
+ });
114
+ const models = createModelController({
115
+ engine,
116
+ state,
117
+ notice: (text, level) => notice(text, level),
118
+ onChanged: pushSnapshot,
119
+ });
120
+ const turnContext = createTurnContext({
121
+ state,
122
+ now,
123
+ pushSnapshot,
124
+ emitEvent: (event) => options.emit.event(event),
125
+ notice,
126
+ onThinkingDelta: createThrottle(250, pushSnapshot), // 思考增量节流
127
+ appendRawText: (chunk, conversationId) => {
128
+ const turn = turns.get(conversationId);
129
+ if (!turn)
130
+ return "";
131
+ return ((turn.rawText += chunk), turn.filter.push(chunk));
132
+ },
133
+ // 收尾全过程都是同步的状态更新(settle 立刻写内存、刷新另起),所以返回的 Promise
134
+ // 不需要再被 await —— 以前留过一条「等落盘」的通道,但它把 Promise 挂在一个已经被
135
+ // 删除的 turns 条目上,永远读不到(死代码,已删)。
136
+ finish: (conversationId, reason) => void finishTurn(conversationId, reason),
137
+ });
138
+ const refreshStats = createStatsRefresher({
139
+ state,
140
+ pathOf: (id) => piOpener.pathOf(id),
141
+ // 引擎自带统计(dsh 的日志);pi / codex 走下面的文件路径兜底
142
+ statsOf: async (id) => (await engine.sessionStats?.(id)) ?? null,
143
+ onChanged: pushSnapshot,
144
+ });
145
+ const { queue, busy } = createTurnPlumbing({
146
+ state,
147
+ engine,
148
+ nextId,
149
+ appendUser: (id, value, steer) => {
150
+ const message = appendText(createMessage(nextId("msg"), "user", now()), value);
151
+ if (steer)
152
+ message.steer = true;
153
+ state.messages[id] = [...(state.messages[id] ?? []), message];
154
+ },
155
+ notice: (text, level) => notice(text, level),
156
+ pushSnapshot,
157
+ });
158
+ const finishTurn = createRuntimeTurnFinisher({
159
+ state,
160
+ turns,
161
+ nextId,
162
+ now,
163
+ shiftQueued: (conversationId) => queue.shift(conversationId),
164
+ sendNext: (message, conversationId) => setTimeout(() => void apiRef?.sendMessage(message.text, message.attachments, "now", conversationId), 0),
165
+ cancelPending: () => approval.cancelPending(),
166
+ onTurnFinished: options.onTurnFinished,
167
+ touchConversation: (conversationId) => touchConversation(conversationId),
168
+ refreshStats: () => void refreshStats(),
169
+ refreshProjects: () => void piRefresher.refresh(true),
170
+ notice: (text, level) => notice(text, level),
171
+ pushSnapshot,
172
+ });
173
+ const switchTo = createSwitchConversation({
174
+ state,
175
+ clientView,
176
+ infoOf: (id) => piOpener.infoOf(id),
177
+ open: (id, refresh) => piOpener.open(id, (force) => refresh(force ?? false)),
178
+ refreshSessions: (force) => piRefresher.refresh(force),
179
+ refreshModels: (id) => models.refresh(id),
180
+ refreshThinking: (id) => models.refreshThinking(id),
181
+ refreshStats: () => void refreshStats(),
182
+ // 切走的那条会话放开引擎写锁(codex):否则 Codex 桌面端打不开它
183
+ parkPrevious: (id) => {
184
+ if (turns.has(id))
185
+ return;
186
+ void engine.parkSession?.(id).catch(() => undefined);
187
+ },
188
+ pushSnapshot,
189
+ report: (text) => options.onStartupInfo?.(text), // 只写日志,不进对话流(切会话慢时靠它定位)
190
+ });
191
+ const refreshModels = createModelRefresher({ engine, state });
192
+ const refreshActions = createRefreshActions({
193
+ state,
194
+ turns,
195
+ engine,
196
+ notice,
197
+ pushSnapshot,
198
+ sendTurn: (prompt, conv) => sendTurn(prompt, undefined, "now", conv, undefined, { hidden: true }),
199
+ // 设置里的「最大超时」优先;没有/非法就退回启动参数(默认 20s)
200
+ timeoutMs: () => {
201
+ const seconds = state.settings.thinkTimeoutSec;
202
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : actionsTimeoutMs;
203
+ },
204
+ });
205
+ const sendTurn = createTurnSender({
206
+ state,
207
+ engine,
208
+ now,
209
+ nextId,
210
+ titleChars,
211
+ turns,
212
+ watchdog,
213
+ turnContext,
214
+ notice,
215
+ pushSnapshot,
216
+ finishTurn: (id, reason) => finishTurn(id, reason),
217
+ busy,
218
+ createConversation: (seed) => conversations.create(seed),
219
+ openConversation: (id, refresh) => piOpener.open(id, refresh),
220
+ isOpen: (id) => piOpener.isOpen(id),
221
+ refreshProjects: (force) => piRefresher.refresh(force),
222
+ refreshModels: (id) => models.refresh(id),
223
+ refreshThinking: (id) => models.refreshThinking(id),
224
+ clientView,
225
+ touchConversation,
226
+ uploadStore: options.uploadStore,
227
+ });
228
+ /**
229
+ * 删除会话后的**本地清场**。
230
+ *
231
+ * 只把磁盘文件改名不够:内存里的会话/消息/排队项、pi-opener 的「已打开」记录、
232
+ * 引擎会话句柄都还在 —— 表现就是「删了还在侧栏、点进去还能读、还能发消息」。
233
+ */
234
+ const discardConversation = (conversationId) => {
235
+ clientView.forget(conversationId);
236
+ piOpener.forget(conversationId);
237
+ state.conversations = state.conversations.filter((conversation) => conversation.id !== conversationId);
238
+ const { [conversationId]: _messages, ...remainingMessages } = state.messages;
239
+ state.messages = remainingMessages;
240
+ state.queued = state.queued.filter((item) => item.conversationId !== conversationId);
241
+ setRunning(state, conversationId, null);
242
+ turns.delete(conversationId);
243
+ if (state.activeId === conversationId) {
244
+ state.activeId = state.conversations[0]?.id ?? null;
245
+ state.running = emptyRunning();
246
+ state.actions = [];
247
+ }
248
+ // 引擎侧的句柄也放开:否则它继续占着会话(还能被写进那个已改名的文件)
249
+ Promise.resolve(engine.closeSession?.(conversationId)).catch(() => undefined);
250
+ };
251
+ const deleteSession = createSessionDeleter({
252
+ engine,
253
+ projects: () => state.piProjects,
254
+ handleOf: (id) => piOpener.handleOf(id),
255
+ discard: discardConversation,
256
+ refresh: () => piRefresher.refresh(true),
257
+ pushSnapshot,
258
+ });
259
+ const switchEngine = createEngineSwitcher({
260
+ state,
261
+ slot,
262
+ engines: options.engines ?? null,
263
+ sessionsSlot: sessionsRefresher.slot,
264
+ clientView,
265
+ clearSessions: () => piOpener.clear(),
266
+ approvalHandler: approval.handler,
267
+ refreshModels,
268
+ refreshSessions: (force) => piRefresher.refresh(force),
269
+ loadProviders: options.loadProviders ?? null,
270
+ isBusy: () => turns.size > 0,
271
+ notice,
272
+ pushSnapshot,
273
+ });
274
+ const api = {
275
+ async init() {
276
+ const ruleCount = await approval.loadRules(); // 「总是允许」规则只记日志,不占对话流
277
+ if (ruleCount > 0)
278
+ options.onStartupInfo?.(`已装载 ${ruleCount} 条审批规则(这些操作不会再询问)`);
279
+ state.settings = settings ? await settings.load() : defaultSettings();
280
+ await piRefresher.refresh(true);
281
+ await apiRef?.refreshProviders();
282
+ pushSnapshot();
283
+ },
284
+ async applySettings(patch) {
285
+ await applySettingsPatch({
286
+ state,
287
+ store: settings,
288
+ patch,
289
+ onError: (message) => notice(message, "warn"),
290
+ });
291
+ pushSnapshot();
292
+ return { ok: true };
293
+ },
294
+ refreshPiSessions: (force) => piRefresher.refresh(force),
295
+ refreshProviders: async () => {
296
+ await refreshModels(); // 模型表挂在会话上:先保证有会话,再重读
297
+ // provider 配置是 pi 独有的(models-store.json):别的引擎不碰这张表
298
+ const ownConfig = options.engines?.get(state.engine)?.descriptor.providerConfig !== false;
299
+ if (options.loadProviders && ownConfig)
300
+ state.providers = await options.loadProviders();
301
+ },
302
+ snapshot: () => buildSnapshot(state),
303
+ snapshotFor: createSnapshotFor({ state, clientView, now }),
304
+ clientView,
305
+ state: () => state,
306
+ isBusy() {
307
+ return turns.size > 0;
308
+ },
309
+ notify(text, level = "info") {
310
+ notice(text, level);
311
+ },
312
+ sendMessage: sendTurn,
313
+ async interrupt(requested) {
314
+ // 优先停用户点的那条(前端会把当前会话带上来),否则用当前会话,最后兜底任意一条
315
+ const fallback = (requested && turns.has(requested) ? requested : undefined) ??
316
+ (state.activeId !== null && turns.has(state.activeId) ? state.activeId : [...turns.keys()][0]);
317
+ const target = fallback ?? null;
318
+ if (!target) {
319
+ return { ok: false, error: { code: "session.idle", message: "当前没有在跑的回合" } };
320
+ }
321
+ await engine.interrupt(target);
322
+ return { ok: true };
323
+ },
324
+ approve(requestId, decision) {
325
+ const result = approval.approve(requestId, decision);
326
+ if (result.ok)
327
+ pushSnapshot();
328
+ return result;
329
+ },
330
+ // 没显式指定条数时用设置里的「一次生成几条」(手机端仍会自己传 3)
331
+ refreshActions: (conversationId, count) => refreshActions(conversationId, count ?? state.settings.thinkCount),
332
+ setModel: (modelId) => models.switchTo(state.activeId, modelId),
333
+ setThinkingLevel: (level) => models.setThinking(state.activeId, level),
334
+ reloadModels: () => models.reload(state.activeId),
335
+ cancelQueued: (queueId) => cancelQueuedWith(queue, queueId, notice),
336
+ pushSnapshot, // 供 provider 等模块「只推帧、不造提示」
337
+ setApprovalMode(mode) {
338
+ approval.setMode(mode);
339
+ state.approvalMode = mode;
340
+ pushSnapshot();
341
+ return { ok: true };
342
+ },
343
+ setEngine: (name) => switchEngine(name),
344
+ newConversation: newConversationWith({ create: conversations.create, clientView, notice, pushSnapshot }),
345
+ deleteSession: (conversationId) => deleteSession(conversationId),
346
+ renameSession: (conversationId, title) => renameSessionWith({
347
+ engine,
348
+ handleOf: (id) => piOpener.handleOf(id),
349
+ refresh: () => piRefresher.refresh(true),
350
+ onRenamed: (id, title) => touchConversation(id, { title }),
351
+ notice,
352
+ pushSnapshot,
353
+ })(conversationId, title),
354
+ switchConversation: (conversationId, clientId) => switchTo(conversationId, clientId),
355
+ };
356
+ apiRef = api; // 队列排空时要能再调 sendMessage
357
+ return api;
358
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * 会话统计(顶部「会话信息」用):直接解析 pi 的会话 JSONL。
3
+ *
4
+ * 为什么从这里算而不是从内存事件累加:会话可能是**在 pi CLI 里开的**(ADR-0021),
5
+ * 或者是上次进程留下的历史。JSONL 是唯一的事实源,内存里没有那些轮的 usage。
6
+ *
7
+ * 只读、有上限(20MB):一个会话文件不该把服务端读卡。
8
+ */
9
+ import { readFile, stat } from "node:fs/promises";
10
+ import { parseUsage } from "../util/usage.js";
11
+ export const MAX_STATS_BYTES = 20 * 1024 * 1024;
12
+ export function emptyStats() {
13
+ return {
14
+ userMessages: 0,
15
+ assistantMessages: 0,
16
+ toolCalls: 0,
17
+ toolResults: 0,
18
+ totalMessages: 0,
19
+ inputTokens: 0,
20
+ outputTokens: 0,
21
+ cacheRead: 0,
22
+ cacheWrite: 0,
23
+ totalTokens: 0,
24
+ contextTokens: 0,
25
+ };
26
+ }
27
+ /** 纯函数:把若干行 JSONL 文本聚合成统计(坏行跳过,绝不抛) */
28
+ export function summarizeSession(lines) {
29
+ const stats = emptyStats();
30
+ for (const line of lines) {
31
+ if (line.trim() === "")
32
+ continue;
33
+ let record;
34
+ try {
35
+ record = JSON.parse(line);
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ const message = (record.message ?? record);
41
+ const role = typeof message.role === "string" ? message.role : "";
42
+ if (role === "user")
43
+ stats.userMessages += 1;
44
+ else if (role === "assistant")
45
+ stats.assistantMessages += 1;
46
+ else if (role === "toolResult" || role === "tool")
47
+ stats.toolResults += 1;
48
+ else
49
+ continue; // header / 其它事件不计
50
+ stats.totalMessages += 1;
51
+ const content = message.content;
52
+ if (Array.isArray(content)) {
53
+ for (const block of content) {
54
+ if (typeof block === "object" && block !== null && block.type === "toolCall")
55
+ stats.toolCalls += 1;
56
+ }
57
+ }
58
+ if (role === "assistant") {
59
+ // 字段名各家不一样 → 统一口径解析(server/util/usage.ts)
60
+ const usage = parseUsage(message.usage);
61
+ if (usage) {
62
+ stats.inputTokens += usage.input;
63
+ stats.outputTokens += usage.output;
64
+ stats.cacheRead += usage.cacheRead;
65
+ stats.cacheWrite += usage.cacheWrite;
66
+ // 最近一条 assistant 的输入侧大小 ≈ 当前上下文占用
67
+ stats.contextTokens = usage.input + usage.cacheRead + usage.cacheWrite;
68
+ }
69
+ }
70
+ }
71
+ stats.totalTokens = stats.inputTokens + stats.outputTokens + stats.cacheRead + stats.cacheWrite;
72
+ return stats;
73
+ }
74
+ /** 读一个会话文件并聚合;文件不存在 / 过大都返回 null(界面显示"未知"即可) */
75
+ export async function readSessionStats(sessionPath) {
76
+ if (!sessionPath)
77
+ return null;
78
+ try {
79
+ const info = await stat(sessionPath);
80
+ if (!info.isFile() || info.size > MAX_STATS_BYTES)
81
+ return null;
82
+ const text = await readFile(sessionPath, "utf8");
83
+ return summarizeSession(text.split("\n"));
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }