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,367 @@
1
+ /**
2
+ * 客户端设置(ADR-0020 §P1):`<dataDir>/settings.json`。
3
+ *
4
+ * 与 ADR-0005 的索引同一套纪律:**原子写、坏文件不致命、可重建**(重建 = 回默认值)。
5
+ * 归一化放在纯函数里(`normalizeQuickPhrases`),因为「用户能贴进来什么」永远想不到 ——
6
+ * 单条超长、重复、空白、几百条,都必须在上盘前收敛。
7
+ */
8
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
+ import { dirname, join } from "node:path";
10
+ export const SETTINGS_FILE = "settings.json";
11
+ export const SETTINGS_VERSION = 1;
12
+ /** 与 ADR-0020 对齐的上限 */
13
+ export const MAX_QUICK_PHRASES = 30;
14
+ export const MAX_QUICK_PHRASE_CHARS = 200;
15
+ export const DEFAULT_QUICK_PHRASES = [
16
+ "继续",
17
+ "总结一下",
18
+ "详细解释一下",
19
+ "检查并修复问题",
20
+ "补充测试覆盖",
21
+ "提交并跑测试",
22
+ ];
23
+ /**
24
+ * 「想想」参数上下限。
25
+ * 条数上限跟 `MAX_ACTIONS`(5)对齐:再多也只会被截掉,界面就别给出会落空的选择。
26
+ * 超时给到 3 分钟:慢模型 + 长上下文时 20 秒确实会不够。
27
+ */
28
+ export const MIN_THINK_COUNT = 1;
29
+ export const MAX_THINK_COUNT = 5;
30
+ export const DEFAULT_THINK_COUNT = 5;
31
+ export const MIN_THINK_TIMEOUT_SEC = 5;
32
+ export const MAX_THINK_TIMEOUT_SEC = 180;
33
+ export const DEFAULT_THINK_TIMEOUT_SEC = 20;
34
+ export const MAX_TEMPLATES = 40;
35
+ export const MAX_TEMPLATE_NAME_CHARS = 24;
36
+ export const MAX_TEMPLATE_DESC_CHARS = 60;
37
+ export const MAX_TEMPLATE_TEXT_CHARS = 2000;
38
+ /**
39
+ * 默认模板库(P5):都是「开箱即用、点一下就填进输入框」的工作流提示词。
40
+ * 图标用 emoji,避免为一个图标位引入图标库(依赖预算)。
41
+ */
42
+ export const DEFAULT_TEMPLATES = [
43
+ {
44
+ id: "t-spec",
45
+ icon: "🏗",
46
+ name: "规格落地",
47
+ description: "按规格测试优先实现",
48
+ text: "按这份规格实现:先写失败的测试,再实现到测试通过。遇到规格里有歧义的地方先问我,别自己猜。",
49
+ },
50
+ {
51
+ id: "t-review",
52
+ icon: "🧹",
53
+ name: "代码审查",
54
+ description: "对照规格与标准审查 diff",
55
+ text: "审查当前改动:对照规格与仓库既有约定,指出行为差异、边界条件遗漏、以及可以删掉的代码。按严重程度排序。",
56
+ },
57
+ {
58
+ id: "t-map",
59
+ icon: "🗺",
60
+ name: "大任务地图",
61
+ description: "把大工程画成决策地图",
62
+ text: "把这个大任务拆成决策地图:先列出必须现在决定的选择点、各自代价,再给一条最短可交付路径。",
63
+ },
64
+ {
65
+ id: "t-proto",
66
+ icon: "⚗️",
67
+ name: "快速原型",
68
+ description: "用一次性代码验证设计假设",
69
+ text: "用一次性原型验证这个设计假设:给出最小可运行代码、要观察的指标,以及结论(继续 / 换方案)。",
70
+ },
71
+ {
72
+ id: "t-research",
73
+ icon: "📚",
74
+ name: "有据调研",
75
+ description: "读一手来源给出带引用的答案",
76
+ text: "调研这个问题:只读一手来源(官方文档 / 源码 / 规范),给出带链接的结论,不确定的地方明确标注。",
77
+ },
78
+ {
79
+ id: "t-arch",
80
+ icon: "🏛",
81
+ name: "架构体检",
82
+ description: "找出值得重构的模块",
83
+ text: "体检这个项目的架构:找出耦合过紧、职责过载的模块,按「收益 / 风险」排序给重构建议。",
84
+ },
85
+ {
86
+ id: "t-debug",
87
+ icon: "🐛",
88
+ name: "系统化排错",
89
+ description: "从可复现的失败开始诊断",
90
+ text: "系统化排错:先给我一条能稳定复现的命令,再二分定位。不要在没有复现之前猜原因。",
91
+ },
92
+ {
93
+ id: "t-conflict",
94
+ icon: "🔀",
95
+ name: "解决合并冲突",
96
+ description: "逐个 hunk 完成合并/变基",
97
+ text: "解决合并冲突:逐个 hunk 看两侧意图,保留两边语义,冲突解决后跑一遍测试并说明每处取舍。",
98
+ },
99
+ {
100
+ id: "t-ask",
101
+ icon: "🔍",
102
+ name: "需求拷问",
103
+ description: "动手前盘问对齐想法",
104
+ text: "动手之前先拷问我:把需求里模糊的地方列成问题(最多 5 个),每个给出你倾向的默认答案。",
105
+ },
106
+ {
107
+ id: "t-handoff",
108
+ icon: "🤝",
109
+ name: "交接",
110
+ description: "把长会话写成交接文档",
111
+ text: "把这次会话整理成交接文档:已完成 / 未完成 / 关键约束 / 下一步第一步,写给完全不了解背景的人。",
112
+ },
113
+ {
114
+ id: "t-plain",
115
+ icon: "💬",
116
+ name: "说人话",
117
+ description: "用大白话重讲刚才的解释",
118
+ text: "用大白话重讲一遍,把术语替换成日常类比,不超过 10 行。",
119
+ },
120
+ {
121
+ id: "t-tdd",
122
+ icon: "🧪",
123
+ name: "测试驱动",
124
+ description: "红-绿-重构的规则",
125
+ text: "用测试驱动:先写一个失败的测试并让它跑给我看,再实现到绿,最后重构。每一步都要有命令输出。",
126
+ },
127
+ ];
128
+ export const MAX_DEFAULT_MODELS = 200;
129
+ /** 纯函数:归一化「项目默认模型」——只收绝对路径键与非空模型 id,超出上限就截断 */
130
+ export function normalizeDefaultModels(input) {
131
+ const out = {};
132
+ if (typeof input !== "object" || input === null)
133
+ return out;
134
+ for (const [key, value] of Object.entries(input)) {
135
+ if (!key.startsWith("/"))
136
+ continue;
137
+ if (typeof value !== "string")
138
+ continue;
139
+ const modelId = value.trim();
140
+ // 必须是 `provider/model`:只写一半的话服务端没法切
141
+ if (modelId === "" || !modelId.includes("/"))
142
+ continue;
143
+ out[key] = modelId;
144
+ if (Object.keys(out).length >= MAX_DEFAULT_MODELS)
145
+ break;
146
+ }
147
+ return out;
148
+ }
149
+ export const MAX_PROJECT_PREFS = 500;
150
+ export const MAX_ALIAS_CHARS = 40;
151
+ /** 纯函数:归一化项目偏好 —— 只保留绝对路径、去重、上限、别名裁剪 */
152
+ export function normalizeProjectPrefs(input) {
153
+ const empty = { order: [], pinned: [], aliases: {} };
154
+ if (typeof input !== "object" || input === null)
155
+ return empty;
156
+ const record = input;
157
+ const paths = (value) => {
158
+ if (!Array.isArray(value))
159
+ return [];
160
+ const seen = new Set();
161
+ const out = [];
162
+ for (const item of value) {
163
+ if (typeof item !== "string")
164
+ continue;
165
+ const path = item.trim();
166
+ if (!path.startsWith("/") || seen.has(path))
167
+ continue;
168
+ seen.add(path);
169
+ out.push(path);
170
+ if (out.length >= MAX_PROJECT_PREFS)
171
+ break;
172
+ }
173
+ return out;
174
+ };
175
+ const aliases = {};
176
+ if (typeof record.aliases === "object" && record.aliases !== null) {
177
+ for (const [key, value] of Object.entries(record.aliases)) {
178
+ if (!key.startsWith("/") || typeof value !== "string")
179
+ continue;
180
+ const name = value.trim().slice(0, MAX_ALIAS_CHARS);
181
+ if (name !== "")
182
+ aliases[key] = name;
183
+ }
184
+ }
185
+ return { order: paths(record.order), pinned: paths(record.pinned), aliases };
186
+ }
187
+ /** 纯函数:会话偏好的归一化(只保留非空字符串,上限 2000 条;改名不在这里) */
188
+ export function normalizeSessionPrefs(input) {
189
+ const empty = { pinned: [], archived: [] };
190
+ if (typeof input !== "object" || input === null)
191
+ return empty;
192
+ const record = input;
193
+ const ids = (value) => Array.isArray(value)
194
+ ? [...new Set(value.filter((item) => typeof item === "string" && item.trim() !== ""))].slice(0, 2000)
195
+ : [];
196
+ // 旧 settings.json 里可能的 titles 直接忽略(改名的事实源是 pi 的会话文件)
197
+ return { pinned: ids(record.pinned), archived: ids(record.archived) };
198
+ }
199
+ export function defaultSettings() {
200
+ return {
201
+ quickPhrases: [...DEFAULT_QUICK_PHRASES],
202
+ quickPhrasesEnabled: true,
203
+ templates: DEFAULT_TEMPLATES.map((template) => ({ ...template })),
204
+ projectPrefs: { order: [], pinned: [], aliases: {} },
205
+ sessionPrefs: { pinned: [], archived: [] },
206
+ defaultModels: {},
207
+ thinkCount: DEFAULT_THINK_COUNT,
208
+ thinkTimeoutSec: DEFAULT_THINK_TIMEOUT_SEC,
209
+ };
210
+ }
211
+ /**
212
+ * 纯函数:规范化快捷短语列表 —— 去空白、限制单条长度、去重、限制条数。
213
+ * 顺序保持用户给的原样(拖拽排序是后话,见 ADR-0020)。
214
+ */
215
+ export function normalizeQuickPhrases(input) {
216
+ if (!Array.isArray(input))
217
+ return [];
218
+ const seen = new Set();
219
+ const out = [];
220
+ for (const item of input) {
221
+ if (typeof item !== "string")
222
+ continue;
223
+ const text = item.replace(/\s+/g, " ").trim();
224
+ if (text === "")
225
+ continue;
226
+ const clipped = text.length > MAX_QUICK_PHRASE_CHARS ? text.slice(0, MAX_QUICK_PHRASE_CHARS) : text;
227
+ if (seen.has(clipped))
228
+ continue;
229
+ seen.add(clipped);
230
+ out.push(clipped);
231
+ if (out.length >= MAX_QUICK_PHRASES)
232
+ break;
233
+ }
234
+ return out;
235
+ }
236
+ /**
237
+ * 纯函数:规范化模板列表 —— 每项必须像模板,字段裁剪长度、id 去重、条数上限。
238
+ * 顺序保持用户给的原样(用户自己拖过的顺序不该被服务器重排)。
239
+ */
240
+ export function normalizeTemplates(input) {
241
+ if (!Array.isArray(input))
242
+ return [];
243
+ const seen = new Set();
244
+ const out = [];
245
+ for (const item of input) {
246
+ if (typeof item !== "object" || item === null)
247
+ continue;
248
+ const record = item;
249
+ const name = typeof record.name === "string" ? record.name.replace(/\s+/g, " ").trim().slice(0, MAX_TEMPLATE_NAME_CHARS) : "";
250
+ const text = typeof record.text === "string" ? record.text.trim().slice(0, MAX_TEMPLATE_TEXT_CHARS) : "";
251
+ if (name === "" || text === "")
252
+ continue; // 名字和正文是硬要求
253
+ const id = typeof record.id === "string" && record.id.trim() !== "" ? record.id.trim() : `t-${out.length + 1}-${name}`;
254
+ if (seen.has(id))
255
+ continue;
256
+ seen.add(id);
257
+ out.push({
258
+ id,
259
+ name,
260
+ description: typeof record.description === "string"
261
+ ? record.description.replace(/\s+/g, " ").trim().slice(0, MAX_TEMPLATE_DESC_CHARS)
262
+ : "",
263
+ icon: typeof record.icon === "string" ? ([...record.icon][0] ?? "✳️") : "✳️",
264
+ text,
265
+ // 软删除标记:只有明确 true 才带上,缺省即显示
266
+ ...(record.hidden === true ? { hidden: true } : {}),
267
+ });
268
+ if (out.length >= MAX_TEMPLATES)
269
+ break;
270
+ }
271
+ return out;
272
+ }
273
+ /** 纯函数:把一个数字收敛到 [min, max];不是有限数就用默认值(坏值不抛) */
274
+ export function normalizeInt(input, min, max, fallback) {
275
+ const value = typeof input === "number" ? input : typeof input === "string" ? Number(input.trim()) : Number.NaN;
276
+ if (!Number.isFinite(value))
277
+ return fallback;
278
+ return Math.min(max, Math.max(min, Math.round(value)));
279
+ }
280
+ /** 纯函数:把外来的部分设置合并进现有设置(坏值一律忽略,不抛) */
281
+ export function mergeSettings(current, patch) {
282
+ if (typeof patch !== "object" || patch === null)
283
+ return current;
284
+ const record = patch;
285
+ const next = { ...current };
286
+ if ("quickPhrases" in record)
287
+ next.quickPhrases = normalizeQuickPhrases(record.quickPhrases);
288
+ if (typeof record.quickPhrasesEnabled === "boolean")
289
+ next.quickPhrasesEnabled = record.quickPhrasesEnabled;
290
+ // templates 传 null = 「恢复默认模板」(显式手势,不当成坏值忽略)
291
+ if (record.templates === null)
292
+ next.templates = DEFAULT_TEMPLATES.map((template) => ({ ...template }));
293
+ else if ("templates" in record)
294
+ next.templates = normalizeTemplates(record.templates);
295
+ if ("projectPrefs" in record)
296
+ next.projectPrefs = normalizeProjectPrefs(record.projectPrefs);
297
+ if ("sessionPrefs" in record)
298
+ next.sessionPrefs = normalizeSessionPrefs(record.sessionPrefs);
299
+ if ("defaultModels" in record)
300
+ next.defaultModels = normalizeDefaultModels(record.defaultModels);
301
+ if ("thinkCount" in record)
302
+ next.thinkCount = normalizeInt(record.thinkCount, MIN_THINK_COUNT, MAX_THINK_COUNT, DEFAULT_THINK_COUNT);
303
+ if ("thinkTimeoutSec" in record)
304
+ next.thinkTimeoutSec = normalizeInt(record.thinkTimeoutSec, MIN_THINK_TIMEOUT_SEC, MAX_THINK_TIMEOUT_SEC, DEFAULT_THINK_TIMEOUT_SEC);
305
+ return next;
306
+ }
307
+ /** 纯函数:解析设置文件;坏内容/缺字段回默认值 */
308
+ export function parseSettings(text) {
309
+ let parsed;
310
+ try {
311
+ parsed = JSON.parse(text);
312
+ }
313
+ catch {
314
+ return defaultSettings();
315
+ }
316
+ return mergeSettings(defaultSettings(), parsed);
317
+ }
318
+ export function settingsPath(dataDir) {
319
+ return join(dataDir, SETTINGS_FILE);
320
+ }
321
+ export function createFileSettingsStore(dataDir) {
322
+ return {
323
+ async load() {
324
+ try {
325
+ return parseSettings(await readFile(settingsPath(dataDir), "utf8"));
326
+ }
327
+ catch {
328
+ return defaultSettings(); // 文件不存在/读不了 = 用默认值
329
+ }
330
+ },
331
+ async save(settings) {
332
+ const target = settingsPath(dataDir);
333
+ await mkdir(dirname(target), { recursive: true });
334
+ const temporary = `${target}.tmp`;
335
+ const payload = { v: SETTINGS_VERSION, ...settings };
336
+ await writeFile(temporary, `${JSON.stringify(payload, null, "\t")}\n`, "utf8");
337
+ await rename(temporary, target); // 原子替换
338
+ },
339
+ };
340
+ }
341
+ /** 内存版(单测 / 不落盘) */
342
+ export function createMemorySettingsStore(seed = defaultSettings()) {
343
+ let current = { ...seed };
344
+ return {
345
+ async load() {
346
+ return { ...current };
347
+ },
348
+ async save(settings) {
349
+ current = { ...settings };
350
+ },
351
+ };
352
+ }
353
+ /**
354
+ * 把一次设置改动落到 state + 盘上(runtime 侧的粘合,从 runtime 拆出以守住 400 行护栏)。
355
+ * 写盘失败只回调警告:内存里的设置仍然生效(下次启动才会回到旧值)。
356
+ */
357
+ export async function applySettingsPatch(input) {
358
+ const next = mergeSettings(input.state.settings, input.patch);
359
+ input.state.settings = next;
360
+ try {
361
+ await input.store?.save(next);
362
+ }
363
+ catch (error) {
364
+ input.onError(`保存设置失败:${error instanceof Error ? error.message : String(error)}`);
365
+ }
366
+ return next;
367
+ }
@@ -0,0 +1,7 @@
1
+ import { buildSnapshot } from "../snapshot/state.js";
2
+ export function createSnapshotFor(deps) {
3
+ const { state, clientView, now } = deps;
4
+ return (clientId) => buildSnapshot(state, undefined, clientView.activeFor(clientId, state.activeId), {
5
+ includeProjects: clientView.needsProjects(clientId, state.piProjectsVersion, now()),
6
+ });
7
+ }
@@ -0,0 +1,50 @@
1
+ import { emptyStats, readSessionStats } from "./session-stats.js";
2
+ export function createStatsRefresher(options) {
3
+ /** 写回:**按读的时候那个会话写**,不能在回来时再用 state.activeId(期间可能已经切走了) */
4
+ const apply = (stats, conversationId) => {
5
+ const { state } = options;
6
+ // 会话级缓存:别的标签页看自己的会话时不会读到别人那份
7
+ state.statsByConversation[conversationId] = stats;
8
+ // 面板只显示「当前会话」的数字:切走了就别把上一条的统计糊到新会话上
9
+ if (state.activeId === conversationId)
10
+ state.sessionStats = stats;
11
+ };
12
+ /**
13
+ * 读不到统计(新会话还没落盘 / 刚建空文件 / 读失败):把面板**清零**。
14
+ * 以前是 `return` 走人 —— 于是上一个会话的 token 数就赖在那儿,新建的会话看着「有记录」。
15
+ */
16
+ const clear = (conversationId) => {
17
+ const { state } = options;
18
+ const zero = emptyStats();
19
+ state.statsByConversation[conversationId] = zero;
20
+ if (state.activeId === conversationId)
21
+ state.sessionStats = { ...zero };
22
+ };
23
+ return async () => {
24
+ const { state } = options;
25
+ const id = state.activeId;
26
+ if (!id)
27
+ return;
28
+ // 引擎自带统计优先(dsh):它知道自己日志里的四桶用量
29
+ const fromEngine = options.statsOf ? await options.statsOf(id).catch(() => null) : null;
30
+ if (fromEngine) {
31
+ apply(fromEngine, id);
32
+ options.onChanged();
33
+ return;
34
+ }
35
+ // 会话文件路径:opener 登记过的直接用(web 新建的会话也在那儿),没有再扫清单
36
+ const sessionPath = options.pathOf(id) ??
37
+ state.piProjects.flatMap((project) => project.sessions).find((session) => session.id === id)
38
+ ?.sessionPath ??
39
+ null;
40
+ const stats = await readSessionStats(sessionPath);
41
+ // 新会话(文件还没写 / 只有 header)本来就是 0,别把上一个会话的数字留在屏幕上
42
+ if (!stats) {
43
+ clear(id);
44
+ options.onChanged();
45
+ return;
46
+ }
47
+ apply(stats, id);
48
+ options.onChanged();
49
+ };
50
+ }
@@ -0,0 +1,86 @@
1
+ /** 耗时上报:`switch <id> 总 123ms · open=100ms models=10ms …`(只进日志,不进对话流) */
2
+ function report(deps, conversationId, began, laps, clientId) {
3
+ // 带上发起者前缀:多设备时能分清「谁切的」,排查串台全靠它
4
+ const who = clientId ? ` client=${clientId.slice(0, 8)}` : "";
5
+ deps.report(`switch ${conversationId.slice(0, 12)}${who} 总 ${Date.now() - began}ms · ${laps.join(" ")}`);
6
+ }
7
+ export function createSwitchConversation(deps) {
8
+ const { state, clientView } = deps;
9
+ async function switchTo(conversationId, clientId) {
10
+ const began = Date.now();
11
+ const previous = state.activeId;
12
+ const laps = [];
13
+ const lap = (label, from) => laps.push(`${label}=${Date.now() - from}ms`);
14
+ const mine = state.conversations.some((conversation) => conversation.id === conversationId);
15
+ const fromPi = deps.infoOf(conversationId);
16
+ // 先校验再动状态:以前 parkPrevious / 清建议 / 改客户端视图都在校验之前,
17
+ // 点一条已删除的会话会把**当前**会话停泊掉(codex 放开写锁)、建议被清空,
18
+ // 而界面还停在错误提示上 —— 副作用全白做了。
19
+ if (!mine && !fromPi) {
20
+ return { ok: false, error: { code: "session.not_found", message: `会话不存在:${conversationId}` } };
21
+ }
22
+ if (previous && previous !== conversationId) {
23
+ deps.parkPrevious(previous);
24
+ // 建议不跨会话:切走就清空(「想一想」生成的也不保存,下次打开是空的)
25
+ state.actions = [];
26
+ }
27
+ // 每个客户端各记一份视图(互不影响)
28
+ if (clientId)
29
+ clientView.setActive(clientId, conversationId);
30
+ // 统计先换成本会话的缓存(没有就清空):refreshStats 是异步的,不先换的话
31
+ // 这一帧快照里还是**上一个会话**的 token 数(新会话看着像"有记录")。
32
+ state.sessionStats = state.statsByConversation[conversationId] ?? null;
33
+ if (!mine && fromPi) {
34
+ if (state.activeId !== conversationId || !state.messages[conversationId]) {
35
+ const tOpen = Date.now();
36
+ const messages = await deps.open(conversationId, (force) => deps.refreshSessions(force));
37
+ lap("open", tOpen);
38
+ if (messages === null) {
39
+ return { ok: false, error: { code: "session.open_failed", message: `打开会话失败:${conversationId}` } };
40
+ }
41
+ state.messages[conversationId] = messages;
42
+ const tModels = Date.now();
43
+ await deps.refreshModels(conversationId);
44
+ lap("models", tModels);
45
+ const tThinking = Date.now();
46
+ await deps.refreshThinking(conversationId);
47
+ lap("thinking", tThinking);
48
+ state.activeId = conversationId;
49
+ state.files = { root: fromPi.workspace, treeVersion: (state.files?.treeVersion ?? 0) + 1 };
50
+ }
51
+ const tStats = Date.now();
52
+ deps.refreshStats();
53
+ lap("stats", tStats);
54
+ // 无论有没有真的换(已是当前会话也推):客户端靠这份快照把乐观高亮退掉,
55
+ // 否则点一下当前会话就变成「正在打开…」卡在那里 —— 看着就是「点了没反应」。
56
+ deps.pushSnapshot();
57
+ report(deps, conversationId, began, laps, clientId);
58
+ return { ok: true };
59
+ }
60
+ if (state.activeId !== conversationId) {
61
+ state.activeId = conversationId;
62
+ // 自建会话也要给出文件根,否则文件树一直是空的
63
+ const workspace = state.conversations.find((item) => item.id === conversationId)?.workspace;
64
+ if (workspace && state.files?.root !== workspace) {
65
+ state.files = { root: workspace, treeVersion: (state.files?.treeVersion ?? 0) + 1 };
66
+ }
67
+ // 模型选择器是全局一份,切会话必须把「这个会话当前用哪个模型」读回来,
68
+ // 否则会一直显示上一个会话的模型(看着就像「选的模型没生效」)。
69
+ const tModels2 = Date.now();
70
+ await deps.refreshModels(conversationId);
71
+ lap("models", tModels2);
72
+ // 思考强度也是「每个会话一份」:自建会话同样要读回来,否则切来切去显示的是上一个会话的
73
+ const tThinking2 = Date.now();
74
+ await deps.refreshThinking(conversationId);
75
+ lap("thinking", tThinking2);
76
+ }
77
+ const tStats2 = Date.now();
78
+ deps.refreshStats();
79
+ lap("stats", tStats2);
80
+ // 同一个理由:已经是当前会话也要推一份,客户端才知道「切到了」。
81
+ deps.pushSnapshot();
82
+ report(deps, conversationId, began, laps, clientId);
83
+ return { ok: true };
84
+ }
85
+ return switchTo;
86
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 简单节流(thinking 增量推帧用):窗口内只执行一次。
3
+ * 抽成独立模块只是为了 runtime.ts 守住 400 行,逻辑本身没什么特别。
4
+ */
5
+ export function createThrottle(windowMs, run) {
6
+ let last = 0;
7
+ return () => {
8
+ const now = Date.now();
9
+ if (now - last < windowMs)
10
+ return;
11
+ last = now;
12
+ run();
13
+ };
14
+ }
@@ -0,0 +1,126 @@
1
+ import { appendError, appendText, appendThinking, finishTool, startTool } from "../snapshot/messages.js";
2
+ import { scheduledCost } from "../util/pricing-index.js";
3
+ /** 思考计时:messageId → 这一段思考的起点(跨事件累计,最后落成 message.thinkingMs) */
4
+ const thinkingSince = new Map();
5
+ /**
6
+ * 组装「事件 → 状态」的上下文(从 runtime 抽出来,省下十来行装配代码)。
7
+ * runtime 只提供三件事:原始文本累积、事件出口、回合收尾。
8
+ */
9
+ export function createTurnContext(options) {
10
+ return {
11
+ state: options.state,
12
+ now: options.now,
13
+ pushSnapshot: options.pushSnapshot,
14
+ emitEvent: options.emitEvent,
15
+ notice: options.notice,
16
+ onThinkingDelta: options.onThinkingDelta,
17
+ onTextDelta: options.appendRawText,
18
+ onTurnEnd: options.finish,
19
+ };
20
+ }
21
+ export function applyEngineEvent(context, input) {
22
+ const { conversationId, messageId, event } = input;
23
+ const { state } = context;
24
+ const patch = (change) => {
25
+ const list = state.messages[conversationId] ?? [];
26
+ state.messages[conversationId] = list.map((message) => (message.id === messageId ? change(message) : message));
27
+ };
28
+ /** 结束这一段思考计时,把毫秒累到消息上(thinkingMs) */
29
+ const closeThinking = () => {
30
+ const since = thinkingSince.get(messageId);
31
+ if (since === undefined)
32
+ return;
33
+ thinkingSince.delete(messageId);
34
+ const span = Math.max(0, context.now() - since);
35
+ if (span <= 0)
36
+ return;
37
+ patch((message) => ({ ...message, thinkingMs: (message.thinkingMs ?? 0) + span }));
38
+ };
39
+ if (event.type !== "thinking_delta")
40
+ closeThinking();
41
+ switch (event.type) {
42
+ case "text_delta": {
43
+ const visible = context.onTextDelta(event.text, conversationId);
44
+ if (visible === "")
45
+ return; // 抠住了(可能在写标记)
46
+ patch((message) => appendText(message, visible));
47
+ if (conversationId === state.activeId)
48
+ state.running.streamingText += visible;
49
+ context.emitEvent({ type: "event", event: "message_delta", conversationId, messageId, text: visible });
50
+ return;
51
+ }
52
+ case "thinking_delta": {
53
+ if (!thinkingSince.has(messageId))
54
+ thinkingSince.set(messageId, context.now());
55
+ patch((message) => appendThinking(message, event.text));
56
+ context.onThinkingDelta?.(conversationId);
57
+ return;
58
+ }
59
+ case "model": {
60
+ // 每条回复记下自己用的模型(历史/对照时不受「当前选择」影响)
61
+ patch((message) => ({ ...message, model: event.model }));
62
+ return;
63
+ }
64
+ case "usage": {
65
+ // 过程值不算(final:false):同一轮里每调一次模型都会有一份最终值,
66
+ // 全部**累加**才是这一轮的真实开销 —— 以前是覆盖,只剩最后一次调用的数字。
67
+ if (event.final === false)
68
+ return;
69
+ patch((message) => {
70
+ const previous = message.usage;
71
+ // 分时价格(ADR-0026 D 期):模型配了时段表就按「这次调用发生的时刻」算钱,
72
+ // 引擎给的 cost 只是兜底(厂商的分时价 pi 根本不知道,混着算只会更错)。
73
+ // 用增量而不是总额:一轮里跨过时段边界也能各算各的。
74
+ const scheduled = scheduledCost(message.model ?? state.model, { input: event.input, output: event.output, cacheRead: event.cacheRead, cacheWrite: event.cacheWrite }, context.now());
75
+ const delta = scheduled ?? event.cost;
76
+ // 只有真的拿到过价格才给:拿不到就别编一个 0 出来
77
+ const cost = previous?.cost === undefined && delta === undefined ? undefined : (previous?.cost ?? 0) + (delta ?? 0);
78
+ return {
79
+ ...message,
80
+ usage: {
81
+ input: (previous?.input ?? 0) + event.input,
82
+ output: (previous?.output ?? 0) + event.output,
83
+ cacheRead: (previous?.cacheRead ?? 0) + event.cacheRead,
84
+ cacheWrite: (previous?.cacheWrite ?? 0) + event.cacheWrite,
85
+ ...(cost === undefined ? {} : { cost }),
86
+ },
87
+ };
88
+ });
89
+ return;
90
+ }
91
+ case "tool_start": {
92
+ patch((message) => startTool(message, {
93
+ toolCallId: event.toolCallId,
94
+ name: event.name,
95
+ argsSummary: event.argsSummary,
96
+ argsFull: event.argsFull,
97
+ startedAt: context.now(),
98
+ }));
99
+ state.running.activeTool = { name: event.name, argsSummary: event.argsSummary, startedAt: context.now() };
100
+ context.pushSnapshot();
101
+ return;
102
+ }
103
+ case "tool_end": {
104
+ patch((message) => finishTool(message, {
105
+ toolCallId: event.toolCallId,
106
+ status: event.status,
107
+ output: event.output,
108
+ endedAt: context.now(),
109
+ ...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
110
+ }));
111
+ state.running.activeTool = null;
112
+ context.pushSnapshot();
113
+ return;
114
+ }
115
+ case "error": {
116
+ patch((message) => appendError(message, event.code, event.message));
117
+ state.running.status = "error";
118
+ context.notice(`${event.code}: ${event.message}`, "error");
119
+ return;
120
+ }
121
+ case "turn_end": {
122
+ context.onTurnEnd(conversationId, event.reason);
123
+ return;
124
+ }
125
+ }
126
+ }