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.
- package/README.md +86 -447
- package/dist/server/approval/read-only.js +107 -0
- package/dist/server/approval/rules.js +85 -0
- package/dist/server/approval/scope.js +150 -0
- package/dist/server/brand.js +2 -0
- package/dist/server/cli-config.js +52 -0
- package/dist/server/cli-info.js +101 -0
- package/dist/server/cli.js +304 -0
- package/dist/server/engine/builtin.js +61 -0
- package/dist/server/engine/catalog.js +8 -0
- package/dist/server/engine/codex-approvals.js +45 -0
- package/dist/server/engine/codex-map.js +298 -0
- package/dist/server/engine/codex-sessions.js +159 -0
- package/dist/server/engine/codex-transport.js +49 -0
- package/dist/server/engine/codex.js +314 -0
- package/dist/server/engine/dsh-history.js +195 -0
- package/dist/server/engine/dsh-log.js +245 -0
- package/dist/server/engine/dsh-map.js +221 -0
- package/dist/server/engine/dsh-sessions.js +207 -0
- package/dist/server/engine/dsh-transport.js +37 -0
- package/dist/server/engine/dsh.js +284 -0
- package/dist/server/engine/fake.js +231 -0
- package/dist/server/engine/jsonrpc-stdio.js +241 -0
- package/dist/server/engine/model-registry-reload.js +115 -0
- package/dist/server/engine/pi-events.js +148 -0
- package/dist/server/engine/pi-rename.js +14 -0
- package/dist/server/engine/pi-sdk.js +343 -0
- package/dist/server/engine/pi-session-model.js +23 -0
- package/dist/server/engine/pi-thinking.js +34 -0
- package/dist/server/engine/pi-transcript.js +225 -0
- package/dist/server/engine/pi-version.js +11 -0
- package/dist/server/engine/port.js +1 -0
- package/dist/server/engine/registry.js +47 -0
- package/dist/server/engine/scratch.js +13 -0
- package/dist/server/engine/suggestion-guidance.js +9 -0
- package/dist/server/engine/switchable.js +22 -0
- package/dist/server/engine/wiring.js +107 -0
- package/dist/server/files/dir-routes.js +40 -0
- package/dist/server/files/file-search.js +63 -0
- package/dist/server/files/file-service.js +232 -0
- package/dist/server/files/files-routes.js +182 -0
- package/dist/server/files/git-info.js +186 -0
- package/dist/server/http/auth-routes.js +99 -0
- package/dist/server/http/auth.js +191 -0
- package/dist/server/http/aux-routes.js +24 -0
- package/dist/server/http/compress.js +75 -0
- package/dist/server/http/health-and-static.js +33 -0
- package/dist/server/http/live-routes.js +113 -0
- package/dist/server/http/login-page.js +200 -0
- package/dist/server/index.js +342 -520
- package/dist/server/markers/actions.js +262 -0
- package/dist/server/protocol-parse.js +201 -0
- package/dist/server/protocol-settings.js +1 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/providers/catalog-store.js +85 -0
- package/dist/server/providers/catalog-writes.js +197 -0
- package/dist/server/providers/catalog.js +77 -0
- package/dist/server/providers/enabled-models.js +118 -0
- package/dist/server/providers/import-models.js +69 -0
- package/dist/server/providers/import-routes.js +136 -0
- package/dist/server/providers/model-test.js +52 -0
- package/dist/server/providers/probe.js +64 -0
- package/dist/server/providers/provider-handlers.js +203 -0
- package/dist/server/providers/provider-store.js +327 -0
- package/dist/server/providers/request-headers.js +7 -0
- package/dist/server/providers/validate.js +241 -0
- package/dist/server/push/push-routes.js +114 -0
- package/dist/server/push/push.js +120 -0
- package/dist/server/session/approval-bridge.js +103 -0
- package/dist/server/session/attachment-resolve.js +21 -0
- package/dist/server/session/client-view.js +82 -0
- package/dist/server/session/conversation-create.js +65 -0
- package/dist/server/session/delete-session.js +84 -0
- package/dist/server/session/engine-switch.js +108 -0
- package/dist/server/session/lock.js +82 -0
- package/dist/server/session/model-controller.js +111 -0
- package/dist/server/session/notices.js +31 -0
- package/dist/server/session/pi-opener.js +123 -0
- package/dist/server/session/pi-refresh.js +51 -0
- package/dist/server/session/pi-sessions-provider.js +46 -0
- package/dist/server/session/pi-sessions.js +272 -0
- package/dist/server/session/refresh-actions.js +42 -0
- package/dist/server/session/rename-session.js +23 -0
- package/dist/server/session/runtime-services.js +28 -0
- package/dist/server/session/runtime-types.js +1 -0
- package/dist/server/session/runtime.js +358 -0
- package/dist/server/session/session-stats.js +88 -0
- package/dist/server/session/settings.js +367 -0
- package/dist/server/session/snapshot-for.js +7 -0
- package/dist/server/session/stats-refresher.js +50 -0
- package/dist/server/session/switch-conversation.js +86 -0
- package/dist/server/session/throttle.js +14 -0
- package/dist/server/session/turn-events.js +126 -0
- package/dist/server/session/turn-finish.js +79 -0
- package/dist/server/session/turn-markers.js +26 -0
- package/dist/server/session/turn-queue.js +111 -0
- package/dist/server/session/turn-send.js +88 -0
- package/dist/server/session/turn-watchdog.js +77 -0
- package/dist/server/sessions/session-routes.js +17 -0
- package/dist/server/skills/skill-routes.js +28 -0
- package/dist/server/skills/skill-store.js +134 -0
- package/dist/server/snapshot/fan-out.js +10 -0
- package/dist/server/snapshot/messages.js +75 -0
- package/dist/server/snapshot/sender.js +62 -0
- package/dist/server/snapshot/state.js +193 -0
- package/dist/server/terminal/terminal-routes.js +176 -0
- package/dist/server/terminal/terminal-session.js +140 -0
- package/dist/server/uploads/upload-routes.js +64 -0
- package/dist/server/uploads/upload-store.js +115 -0
- package/dist/server/util/data-dir.js +36 -0
- package/dist/server/util/env.js +140 -0
- package/dist/server/util/ids.js +28 -0
- package/dist/server/util/log-file.js +67 -0
- package/dist/server/util/log.js +52 -0
- package/dist/server/util/origin.js +75 -0
- package/dist/server/util/paths.js +60 -0
- package/dist/server/util/pricing-index.js +121 -0
- package/dist/server/util/pricing.js +214 -0
- package/dist/server/util/startup.js +34 -0
- package/dist/server/util/upstream-version.js +140 -0
- package/dist/server/util/usage.js +19 -0
- package/dist/server/util/version.js +50 -0
- package/dist/server/ws/handle-message.js +95 -0
- package/package.json +49 -63
- package/web/dist/apple-touch-icon-precomposed.png +0 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
- package/web/dist/assets/arc-DviL9o3c.js +1 -0
- package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
- package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
- package/web/dist/assets/array-BifhSqXX.js +1 -0
- package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
- package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
- package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
- package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
- package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
- package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
- package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
- package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
- package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
- package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
- package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
- package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
- package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
- package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
- package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
- package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
- package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
- package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
- package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
- package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
- package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
- package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
- package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
- package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
- package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
- package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
- package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
- package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
- package/web/dist/assets/dist-C86cKTuE.js +1 -0
- package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
- package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
- package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
- package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
- package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
- package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
- package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
- package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
- package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/web/dist/assets/index-Bjn56a2i.js +76 -0
- package/web/dist/assets/index-Drp6e5iU.css +1 -0
- package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
- package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
- package/web/dist/assets/init-C-OQMol4.js +1 -0
- package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
- package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
- package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
- package/web/dist/assets/katex-B_JcIwqj.js +257 -0
- package/web/dist/assets/line-CHV9_4lL.js +1 -0
- package/web/dist/assets/linear-C-eZsBTv.js +1 -0
- package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
- package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
- package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
- package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
- package/web/dist/assets/path-fybaL0A-.js +1 -0
- package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
- package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
- package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
- package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
- package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
- package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
- package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
- package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
- package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
- package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
- package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
- package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
- package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
- package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/web/dist/assets/src-Da91fG3g.js +1 -0
- package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
- package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
- package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
- package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
- package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
- package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
- package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
- package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
- package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
- package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
- package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicon.svg +20 -8
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-maskable-512.png +0 -0
- package/web/dist/index.html +21 -27
- package/web/dist/manifest.webmanifest +17 -0
- package/web/dist/maskable.svg +25 -0
- package/web/dist/sw.js +40 -0
- package/bin/oh-my-agent-web-ps.mjs +0 -203
- package/bin/oh-my-agent-web-ws.mjs +0 -593
- package/bin/oh-my-agent-web.mjs +0 -321
- package/bin/update-check.mjs +0 -142
- package/dist/server/codex-client.js +0 -310
- package/dist/server/codex-supervisor.js +0 -446
- package/dist/server/fs-service.js +0 -144
- package/dist/server/request-inspector.js +0 -458
- package/dist/shared/protocol.js +0 -112
- package/scripts/manage-server.mjs +0 -155
- package/scripts/omaw.mjs +0 -74
- package/shared/protocol.ts +0 -228
- package/web/dist/assets/index-B8-VzDf2.css +0 -1
- package/web/dist/assets/index-DeC2eiiq.js +0 -42
- package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export function createPiSessionOpener(options) {
|
|
2
|
+
const pathById = new Map();
|
|
3
|
+
/** 引擎自己的会话 id(codex 的 threadId):跟文件路径并行存,谁有用谁用 */
|
|
4
|
+
const engineIdById = new Map();
|
|
5
|
+
const infoById = new Map();
|
|
6
|
+
const opened = new Set();
|
|
7
|
+
/** workspace → 该项目最近一个会话用的模型(remember 时按 updatedAt 更新) */
|
|
8
|
+
const modelByWorkspace = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* 告诉界面「这个会话现在用哪个模型」。
|
|
11
|
+
* 优先用**这个会话最后一条助手消息**上的模型:那是真的用过的;
|
|
12
|
+
* `currentModel` 只是进程默认模型(历史会话几乎都对不上,会显示成别的模型)。
|
|
13
|
+
*/
|
|
14
|
+
const announceModel = async (conversationId, messages) => {
|
|
15
|
+
// ① 会话文件里的最后一次 model_change:用户在选择器上切过也写在这里,最准
|
|
16
|
+
const saved = (await options.engine.sessionModel?.(conversationId)) ?? null;
|
|
17
|
+
if (saved) {
|
|
18
|
+
options.onModel(saved.provider ? `${saved.provider}/${saved.model}` : saved.model);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// ② 退一步用最后一条助手消息上的模型(它至少是真的用过)
|
|
22
|
+
const used = [...messages].reverse().find((message) => message.role === "assistant" && message.model)?.model;
|
|
23
|
+
if (used) {
|
|
24
|
+
options.onModel(used);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// 新会话还没有回复:这时进程默认模型就是它接下来要用的
|
|
28
|
+
const current = (await options.engine.currentModel?.(conversationId)) ?? null;
|
|
29
|
+
if (current) {
|
|
30
|
+
options.onModel(current.provider ? `${current.provider}/${current.model}` : current.model);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// ③ 最后用侧栏清单里记着的模型(dsh 的历史会话在 resume 之前拿不到 config,
|
|
34
|
+
// 但 projcache 里已经有这条会话最后用的模型)
|
|
35
|
+
const known = infoById.get(conversationId)?.model;
|
|
36
|
+
if (known)
|
|
37
|
+
options.onModel(known);
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
register(input) {
|
|
41
|
+
if (input.sessionPath)
|
|
42
|
+
pathById.set(input.id, input.sessionPath);
|
|
43
|
+
if (input.engineSessionId)
|
|
44
|
+
engineIdById.set(input.id, input.engineSessionId);
|
|
45
|
+
infoById.set(input.id, { title: input.title, workspace: input.workspace, model: input.model ?? null });
|
|
46
|
+
opened.add(input.id); // createSession 已经把它建好了
|
|
47
|
+
},
|
|
48
|
+
remember(projects) {
|
|
49
|
+
for (const project of projects) {
|
|
50
|
+
for (const session of project.sessions) {
|
|
51
|
+
const workspace = session.cwd || project.cwd;
|
|
52
|
+
if (session.sessionPath)
|
|
53
|
+
pathById.set(session.id, session.sessionPath);
|
|
54
|
+
if (session.engineSessionId)
|
|
55
|
+
engineIdById.set(session.id, session.engineSessionId);
|
|
56
|
+
infoById.set(session.id, { title: session.title, workspace, model: session.model ?? null });
|
|
57
|
+
if (!session.model)
|
|
58
|
+
continue;
|
|
59
|
+
// 只记「这个项目里最新那个会话」的模型:它就是项目的当前口味
|
|
60
|
+
const known = modelByWorkspace.get(workspace);
|
|
61
|
+
if (known && known.updatedAt >= session.updatedAt)
|
|
62
|
+
continue;
|
|
63
|
+
modelByWorkspace.set(workspace, { updatedAt: session.updatedAt, model: session.model });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
projectModel: (workspace) => modelByWorkspace.get(workspace)?.model ?? null,
|
|
68
|
+
forget: (conversationId) => {
|
|
69
|
+
pathById.delete(conversationId);
|
|
70
|
+
engineIdById.delete(conversationId);
|
|
71
|
+
infoById.delete(conversationId);
|
|
72
|
+
opened.delete(conversationId);
|
|
73
|
+
// 项目级的「最近用过的模型」不按会话删:它只是新会话的起点,删一条不代表项目口味变了
|
|
74
|
+
},
|
|
75
|
+
clear: () => {
|
|
76
|
+
pathById.clear();
|
|
77
|
+
engineIdById.clear();
|
|
78
|
+
infoById.clear();
|
|
79
|
+
opened.clear();
|
|
80
|
+
modelByWorkspace.clear();
|
|
81
|
+
},
|
|
82
|
+
isOpen: (conversationId) => opened.has(conversationId),
|
|
83
|
+
infoOf: (conversationId) => infoById.get(conversationId) ?? null,
|
|
84
|
+
/** 会话文件路径(统计要读它;web 新建的会话也在这里登记过) */
|
|
85
|
+
pathOf: (conversationId) => pathById.get(conversationId) ?? null,
|
|
86
|
+
handleOf: (conversationId) => engineIdById.get(conversationId) ?? pathById.get(conversationId) ?? null,
|
|
87
|
+
async open(conversationId, refresh) {
|
|
88
|
+
if (opened.has(conversationId)) {
|
|
89
|
+
// 已经打开过也不能跳过「当前模型」:state.model 是全局一份,
|
|
90
|
+
// 切回来时不重读就会停留在别的会话的模型上(选择器显示错乱)。
|
|
91
|
+
const messages = await options.engine.readUiMessages(conversationId);
|
|
92
|
+
await announceModel(conversationId, messages);
|
|
93
|
+
return messages;
|
|
94
|
+
}
|
|
95
|
+
const handle = () => engineIdById.get(conversationId) ?? pathById.get(conversationId) ?? "";
|
|
96
|
+
let found = handle();
|
|
97
|
+
if (found === "") {
|
|
98
|
+
await refresh(true); // 可能是刚建的会话,刷新一次再找
|
|
99
|
+
found = handle();
|
|
100
|
+
}
|
|
101
|
+
if (found === "")
|
|
102
|
+
return null;
|
|
103
|
+
try {
|
|
104
|
+
const engineSessionId = engineIdById.get(conversationId);
|
|
105
|
+
const sessionPath = pathById.get(conversationId);
|
|
106
|
+
await options.engine.openSession({
|
|
107
|
+
sessionId: conversationId,
|
|
108
|
+
...(engineSessionId ? { engineSessionId } : {}),
|
|
109
|
+
...(sessionPath ? { sessionPath } : {}),
|
|
110
|
+
...(infoById.get(conversationId)?.workspace ? { cwd: infoById.get(conversationId)?.workspace } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
options.onError(`打开会话失败:${error instanceof Error ? error.message : String(error)}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
opened.add(conversationId);
|
|
118
|
+
const messages = await options.engine.readUiMessages(conversationId);
|
|
119
|
+
await announceModel(conversationId, messages);
|
|
120
|
+
return messages;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { setPiProjects } from "../snapshot/state.js";
|
|
2
|
+
export function createPiRefresher(options) {
|
|
3
|
+
const minIntervalMs = options.minIntervalMs ?? 10_000;
|
|
4
|
+
let lastScannedAt = Number.NEGATIVE_INFINITY;
|
|
5
|
+
let inFlight = null;
|
|
6
|
+
return {
|
|
7
|
+
async refresh(force = false) {
|
|
8
|
+
const stamp = options.now();
|
|
9
|
+
if (!force && stamp - lastScannedAt < minIntervalMs)
|
|
10
|
+
return;
|
|
11
|
+
if (inFlight)
|
|
12
|
+
return inFlight; // 并发刷新合并成一次
|
|
13
|
+
lastScannedAt = stamp;
|
|
14
|
+
inFlight = (async () => {
|
|
15
|
+
try {
|
|
16
|
+
options.onLoaded(await options.list());
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// 扫不到(pi 没装 / 目录被删)不该影响主流程
|
|
20
|
+
options.onError(`读取历史会话失败:${error instanceof Error ? error.message : String(error)}`);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
inFlight = null;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
return inFlight;
|
|
27
|
+
},
|
|
28
|
+
lastScannedAt: () => lastScannedAt,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 会话清单刷新器(ADR-0021 §1)+ 可替换的「清单来源」。
|
|
33
|
+
*
|
|
34
|
+
* 切引擎时来源要跟着换(pi 扫磁盘 / 未来 codex 问 app-server),但**刷新节奏与缓存逻辑
|
|
35
|
+
* 完全相同**,所以来源放进一个可变槽,refresher 本身只建一次。
|
|
36
|
+
*/
|
|
37
|
+
export function createSessionsRefresher(options) {
|
|
38
|
+
const slot = { current: options.initial };
|
|
39
|
+
const refresher = createPiRefresher({
|
|
40
|
+
list: () => slot.current?.() ?? Promise.resolve([]),
|
|
41
|
+
minIntervalMs: options.minIntervalMs,
|
|
42
|
+
now: options.now,
|
|
43
|
+
onLoaded: (projects) => {
|
|
44
|
+
setPiProjects(options.state, projects);
|
|
45
|
+
options.remember(projects); // 记下每个会话的文件路径(ADR-0021 §2)
|
|
46
|
+
options.onChanged();
|
|
47
|
+
},
|
|
48
|
+
onError: options.onError,
|
|
49
|
+
});
|
|
50
|
+
return { slot, refresh: (force) => refresher.refresh(force) };
|
|
51
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi 会话列表的「扫描 + 缓存」提供者(ADR-0021 §1)——从 index.ts 抽出(400 行护栏)。
|
|
3
|
+
*
|
|
4
|
+
* 首扫要读 247 个文件(实测 ~1s),之后靠 path+mtime 缓存(~7ms);
|
|
5
|
+
* 缓存落在 `<dataDir>/pi-sessions-cache.json`,坏了就重扫(ADR-0005 的同款纪律)。
|
|
6
|
+
*/
|
|
7
|
+
import { PI_AGENT_DIR_ENV, readCacheFile, scanPiSessions, writeCacheFile, } from "./pi-sessions.js";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
/** pi 的配置目录(与 SDK 的 getAgentDir 默认值一致;这里不 import SDK —— 边界守则见 ADR-0003) */
|
|
11
|
+
export function piAgentDir() {
|
|
12
|
+
return process.env[PI_AGENT_DIR_ENV] ?? join(homedir(), ".pi", "agent");
|
|
13
|
+
}
|
|
14
|
+
export function createPiSessionsProvider(options) {
|
|
15
|
+
let cache = { v: 1, entries: {} };
|
|
16
|
+
const cacheFile = join(options.dataDir, "pi-sessions-cache.json");
|
|
17
|
+
let loaded = false;
|
|
18
|
+
return async () => {
|
|
19
|
+
if (!loaded) {
|
|
20
|
+
cache = await readCacheFile(cacheFile);
|
|
21
|
+
loaded = true;
|
|
22
|
+
}
|
|
23
|
+
const result = await scanPiSessions({ sessionsRoot: join(options.agentDir, "sessions"), cache });
|
|
24
|
+
cache = result.cache;
|
|
25
|
+
if (result.read > 0)
|
|
26
|
+
await writeCacheFile(cacheFile, cache).catch(() => { });
|
|
27
|
+
options.logger.info("已读取 pi 已有会话", {
|
|
28
|
+
projects: result.groups.length,
|
|
29
|
+
sessions: result.total,
|
|
30
|
+
read: result.read,
|
|
31
|
+
});
|
|
32
|
+
return result.groups.map((group) => ({
|
|
33
|
+
cwd: group.cwd,
|
|
34
|
+
name: group.name,
|
|
35
|
+
sessions: group.sessions.map((session) => ({
|
|
36
|
+
id: session.id,
|
|
37
|
+
title: session.title,
|
|
38
|
+
startedAt: session.startedAt,
|
|
39
|
+
sessionPath: session.path,
|
|
40
|
+
updatedAt: session.updatedAt,
|
|
41
|
+
cwd: session.cwd,
|
|
42
|
+
model: session.model,
|
|
43
|
+
})),
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读取 pi 已有的会话(ADR-0021 §1):只读 + 缓存。
|
|
3
|
+
*
|
|
4
|
+
* pi 的存储布局(实测 2026-10-16,本机 17 个项目 / 247 个会话):
|
|
5
|
+
* ~/.pi/agent/sessions/<编码后的 cwd>/<ISO 时间>_<uuid>.jsonl
|
|
6
|
+
* 首行:{"type":"session","version":3,"id":…,"timestamp":…,"cwd":"/abs/path"}
|
|
7
|
+
*
|
|
8
|
+
* 关键取舍:**只读首行 header + 前若干行找标题**,不解析整个文件。
|
|
9
|
+
* 247 个文件全量解析会明显变慢,而列表只需要「项目 / 标题 / 时间」。
|
|
10
|
+
*/
|
|
11
|
+
import { readFile, readdir, stat, writeFile, rename, mkdir } from "node:fs/promises";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
export const PI_AGENT_DIR_ENV = "PI_AGENT_DIR";
|
|
14
|
+
export function defaultPiSessionsRoot(agentDir) {
|
|
15
|
+
return join(agentDir, "sessions");
|
|
16
|
+
}
|
|
17
|
+
/** 纯函数:解析首行 header;不是会话文件就返回 null */
|
|
18
|
+
export function parseSessionHeader(line) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(line);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
27
|
+
return null;
|
|
28
|
+
const record = parsed;
|
|
29
|
+
if (record.type !== "session")
|
|
30
|
+
return null;
|
|
31
|
+
const timestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Number.NaN;
|
|
32
|
+
return {
|
|
33
|
+
id: typeof record.id === "string" ? record.id : "",
|
|
34
|
+
cwd: typeof record.cwd === "string" ? record.cwd : "",
|
|
35
|
+
startedAt: Number.isFinite(timestamp) ? timestamp : 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 纯函数:从会话内容行里抠出标题与模型。
|
|
40
|
+
*
|
|
41
|
+
* 标题只看开头(`headLimit` 行):首条消息就够了,全量扫会慢。
|
|
42
|
+
* 模型**从末尾倒着找**最后一条 `model_change`:追加式文件里那就是「现在用哪个」,
|
|
43
|
+
* 只扫开头会把「中途换过模型」的会话报成最初那个(切回去时选择器就显示错了)。
|
|
44
|
+
* 会话名(session_info)也一样倒着找 —— **改名是往文件末尾追加一条**,
|
|
45
|
+
* 只扫开头的话,超过 60 行的会话改完名界面上永远看不到变化("改名改不了"那个 bug)。
|
|
46
|
+
*/
|
|
47
|
+
export function extractListMeta(lines, headLimit = lines.length) {
|
|
48
|
+
let title = "";
|
|
49
|
+
let model = null;
|
|
50
|
+
for (const [index, line] of lines.entries()) {
|
|
51
|
+
// 标题只扫开头;模型 / 会话名在下面单独倒着找
|
|
52
|
+
if (index >= headLimit)
|
|
53
|
+
break;
|
|
54
|
+
let record;
|
|
55
|
+
try {
|
|
56
|
+
record = JSON.parse(line);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (record.type === "model_change" && typeof record.modelId === "string") {
|
|
62
|
+
// 追加式文件:后来的 model_change 覆盖先前的 —— 会话中途换过模型就要取最后一次,
|
|
63
|
+
// 否则切回这个会话时选择器显示的还是它最初那个模型
|
|
64
|
+
const provider = typeof record.provider === "string" ? record.provider : "";
|
|
65
|
+
model = provider ? `${provider}/${record.modelId}` : record.modelId;
|
|
66
|
+
}
|
|
67
|
+
if (title)
|
|
68
|
+
continue;
|
|
69
|
+
if (record.type !== "message")
|
|
70
|
+
continue;
|
|
71
|
+
const message = record.message;
|
|
72
|
+
if (!message || message.role !== "user")
|
|
73
|
+
continue;
|
|
74
|
+
title = textOfMessageContent(message.content);
|
|
75
|
+
}
|
|
76
|
+
/** pi 自己的会话名(session_info.name):有它就以它为准,首条消息只当兜底 */
|
|
77
|
+
const name = lastSessionName(lines);
|
|
78
|
+
// 模型:倒着找最后一条 model_change(只解析含关键字的行,避免整文件 JSON.parse)
|
|
79
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
80
|
+
const line = lines[index] ?? "";
|
|
81
|
+
if (!line.includes("model_change"))
|
|
82
|
+
continue;
|
|
83
|
+
let record;
|
|
84
|
+
try {
|
|
85
|
+
record = JSON.parse(line);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (record.type !== "model_change" || typeof record.modelId !== "string" || record.modelId === "")
|
|
91
|
+
continue;
|
|
92
|
+
const provider = typeof record.provider === "string" ? record.provider : "";
|
|
93
|
+
model = provider ? `${provider}/${record.modelId}` : record.modelId;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
// 列表只显示一行:折叠换行/连续空白(首条消息常是多行任务描述)
|
|
97
|
+
const display = (name || title).replace(/\s+/g, " ").trim();
|
|
98
|
+
return { title: display.slice(0, 60), model };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 最后一条 `session_info.name`(改名 = 往文件末尾再追加一条,所以要从后往前找)。
|
|
102
|
+
* 只对含关键字的行 JSON.parse —— 会话文件动辄几千行,全量解析太贵。
|
|
103
|
+
*/
|
|
104
|
+
function lastSessionName(lines) {
|
|
105
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
106
|
+
const line = lines[index] ?? "";
|
|
107
|
+
if (!line.includes("session_info"))
|
|
108
|
+
continue;
|
|
109
|
+
let record;
|
|
110
|
+
try {
|
|
111
|
+
record = JSON.parse(line);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (record.type !== "session_info")
|
|
117
|
+
continue;
|
|
118
|
+
if (typeof record.name !== "string" || record.name.trim() === "")
|
|
119
|
+
continue;
|
|
120
|
+
return record.name;
|
|
121
|
+
}
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
function textOfMessageContent(content) {
|
|
125
|
+
if (typeof content === "string")
|
|
126
|
+
return content;
|
|
127
|
+
if (!Array.isArray(content))
|
|
128
|
+
return "";
|
|
129
|
+
return content
|
|
130
|
+
.map((part) => {
|
|
131
|
+
const record = typeof part === "object" && part !== null ? part : {};
|
|
132
|
+
return typeof record.text === "string" ? record.text : "";
|
|
133
|
+
})
|
|
134
|
+
.join(" ");
|
|
135
|
+
}
|
|
136
|
+
/** 纯函数:规范化 cwd —— 必须是绝对路径,否则视为「未知」(老会话里出现过 cwd=一句提示词) */
|
|
137
|
+
export function normalizeCwd(cwd) {
|
|
138
|
+
const trimmed = cwd.trim();
|
|
139
|
+
if (trimmed.startsWith("/") || /^[A-Za-z]:[\\/]/.test(trimmed))
|
|
140
|
+
return trimmed;
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
/** 组名:cwd 的最后一段(`/a/b/inke.activity.service` → `inke.activity.service`) */
|
|
144
|
+
export function projectNameOf(cwd) {
|
|
145
|
+
if (cwd === "")
|
|
146
|
+
return "(未知项目)";
|
|
147
|
+
const parts = cwd.split("/").filter(Boolean);
|
|
148
|
+
return parts[parts.length - 1] ?? cwd;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 纯函数:把扁平会话列表按 cwd 分组。
|
|
152
|
+
*
|
|
153
|
+
* 组内按 **startedAt(创建时间)倒序**:新会话在最上面,而**更新一个旧会话不会让它跳位置** ——
|
|
154
|
+
* 按 updatedAt 排的话,你在一个老会话里说一句话它就窜到最上面,整个列表跟着挪,很烦人。
|
|
155
|
+
* 组间仍按最新活动倒序(侧栏会用 projectPrefs 再排一次,这里只保证可预期的初始顺序)。
|
|
156
|
+
*/
|
|
157
|
+
export function groupByProject(sessions) {
|
|
158
|
+
const groups = new Map();
|
|
159
|
+
for (const session of sessions) {
|
|
160
|
+
const key = session.cwd;
|
|
161
|
+
const existing = groups.get(key);
|
|
162
|
+
if (existing) {
|
|
163
|
+
existing.sessions.push(session);
|
|
164
|
+
existing.updatedAt = Math.max(existing.updatedAt, session.updatedAt);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
groups.set(key, {
|
|
168
|
+
cwd: key,
|
|
169
|
+
name: projectNameOf(key),
|
|
170
|
+
sessions: [session],
|
|
171
|
+
updatedAt: session.updatedAt,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const list = [...groups.values()];
|
|
176
|
+
for (const group of list)
|
|
177
|
+
group.sessions.sort((a, b) => b.startedAt - a.startedAt);
|
|
178
|
+
return list.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
179
|
+
}
|
|
180
|
+
export const CACHE_VERSION = 1;
|
|
181
|
+
export function parseCache(text) {
|
|
182
|
+
try {
|
|
183
|
+
const parsed = JSON.parse(text);
|
|
184
|
+
if (parsed && typeof parsed === "object" && parsed.v === CACHE_VERSION && parsed.entries)
|
|
185
|
+
return parsed;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// 坏缓存 = 没有缓存(ADR-0005 的同款纪律:缓存坏了不该致命)
|
|
189
|
+
}
|
|
190
|
+
return { v: CACHE_VERSION, entries: {} };
|
|
191
|
+
}
|
|
192
|
+
export async function scanPiSessions(options) {
|
|
193
|
+
const maxLines = options.maxLinesPerFile ?? 60;
|
|
194
|
+
const cache = { v: CACHE_VERSION, entries: {} };
|
|
195
|
+
const rows = [];
|
|
196
|
+
let read = 0;
|
|
197
|
+
let projectDirs = [];
|
|
198
|
+
try {
|
|
199
|
+
const entries = await readdir(options.sessionsRoot, { withFileTypes: true });
|
|
200
|
+
projectDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join(options.sessionsRoot, entry.name));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// 目录不存在(没装 pi 或没跑过)= 空列表,不是错误
|
|
204
|
+
return { groups: [], cache, read: 0, total: 0 };
|
|
205
|
+
}
|
|
206
|
+
for (const dir of projectDirs) {
|
|
207
|
+
let files = [];
|
|
208
|
+
try {
|
|
209
|
+
files = (await readdir(dir)).filter((name) => name.endsWith(".jsonl"));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
for (const name of files) {
|
|
215
|
+
const path = join(dir, name);
|
|
216
|
+
let mtimeMs = 0;
|
|
217
|
+
try {
|
|
218
|
+
mtimeMs = (await stat(path)).mtimeMs;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
continue; // 文件刚被删(用户在 CLI 里清理过)→ 跳过而不是崩
|
|
222
|
+
}
|
|
223
|
+
const cached = options.cache?.entries[path];
|
|
224
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
225
|
+
cache.entries[path] = cached;
|
|
226
|
+
rows.push(cached.row);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
read += 1;
|
|
230
|
+
let text = "";
|
|
231
|
+
try {
|
|
232
|
+
text = await readFile(path, "utf8");
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const lines = text.split("\n");
|
|
238
|
+
const header = parseSessionHeader(lines[0] ?? "");
|
|
239
|
+
if (!header)
|
|
240
|
+
continue; // 不是会话文件
|
|
241
|
+
const meta = extractListMeta(lines.filter((line, index) => index > 0), maxLines - 1);
|
|
242
|
+
const row = {
|
|
243
|
+
id: header.id || name,
|
|
244
|
+
path,
|
|
245
|
+
// 脏数据防御:老会话里出现过 cwd=一句提示词 → 非绝对路径一律当未知项目,避免侧栏出现「组名是一句话」
|
|
246
|
+
cwd: normalizeCwd(header.cwd),
|
|
247
|
+
startedAt: header.startedAt,
|
|
248
|
+
updatedAt: mtimeMs,
|
|
249
|
+
title: meta.title || new Date(mtimeMs || header.startedAt).toLocaleString(),
|
|
250
|
+
model: meta.model,
|
|
251
|
+
};
|
|
252
|
+
rows.push(row);
|
|
253
|
+
cache.entries[path] = { mtimeMs, row };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const limited = options.limit ? rows.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, options.limit) : rows;
|
|
257
|
+
return { groups: groupByProject(limited), cache, read, total: rows.length };
|
|
258
|
+
}
|
|
259
|
+
export async function readCacheFile(file) {
|
|
260
|
+
try {
|
|
261
|
+
return parseCache(await readFile(file, "utf8"));
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { v: CACHE_VERSION, entries: {} };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export async function writeCacheFile(file, cache) {
|
|
268
|
+
await mkdir(dirname(file), { recursive: true });
|
|
269
|
+
const temporary = `${file}.tmp`;
|
|
270
|
+
await writeFile(temporary, `${JSON.stringify(cache)}\n`, "utf8");
|
|
271
|
+
await rename(temporary, file); // 原子替换(同 ADR-0005 的索引写法)
|
|
272
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { buildRefreshActionsPrompt } from "../markers/actions.js";
|
|
2
|
+
export function createRefreshActions(deps) {
|
|
3
|
+
const { state } = deps;
|
|
4
|
+
const resolveTimeout = () => {
|
|
5
|
+
const value = typeof deps.timeoutMs === "function" ? deps.timeoutMs() : deps.timeoutMs;
|
|
6
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 20_000;
|
|
7
|
+
};
|
|
8
|
+
return async (requested, count) => {
|
|
9
|
+
const conv = requested ?? state.activeId;
|
|
10
|
+
if (!conv)
|
|
11
|
+
return { ok: false, error: { code: "session.none", message: "先打开一个会话再要建议" } };
|
|
12
|
+
if (deps.turns.has(conv)) {
|
|
13
|
+
// 可能是真的在跑一轮,也可能是上一次「下一步」还没回来
|
|
14
|
+
return { ok: false, error: { code: "session.busy", message: "上一轮还在跑,等它结束再点「下一步」" } };
|
|
15
|
+
}
|
|
16
|
+
const labels = state.actions.map((action) => action.label);
|
|
17
|
+
const prompt = buildRefreshActionsPrompt(labels, count);
|
|
18
|
+
state.actions = [];
|
|
19
|
+
deps.pushSnapshot();
|
|
20
|
+
const timeoutMs = resolveTimeout();
|
|
21
|
+
let timedOut = false;
|
|
22
|
+
const timer = timeoutMs > 0
|
|
23
|
+
? setTimeout(() => {
|
|
24
|
+
if (!deps.turns.has(conv))
|
|
25
|
+
return;
|
|
26
|
+
timedOut = true;
|
|
27
|
+
void deps.engine.interrupt(conv).catch(() => undefined);
|
|
28
|
+
}, timeoutMs)
|
|
29
|
+
: null;
|
|
30
|
+
try {
|
|
31
|
+
const result = await deps.sendTurn(prompt, conv);
|
|
32
|
+
if (timedOut)
|
|
33
|
+
deps.notice(`「下一步」${Math.round(timeoutMs / 1000)} 秒没想出来,已取消(可以再点一次)`, "warn");
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (timer !== null)
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
deps.pushSnapshot();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function renameSessionWith(deps) {
|
|
2
|
+
return async (conversationId, title) => {
|
|
3
|
+
const handle = deps.handleOf(conversationId);
|
|
4
|
+
if (!handle) {
|
|
5
|
+
return { ok: false, error: { code: "session.not_found", message: `找不到这条会话:${conversationId}` } };
|
|
6
|
+
}
|
|
7
|
+
if (!deps.engine.renameSession) {
|
|
8
|
+
return { ok: false, error: { code: "session.rename_unsupported", message: "当前引擎不支持改会话名" } };
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
await deps.engine.renameSession(handle, title);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
deps.notice(`重命名失败:${message}`, "error");
|
|
16
|
+
return { ok: false, error: { code: "session.rename_failed", message } };
|
|
17
|
+
}
|
|
18
|
+
deps.onRenamed?.(conversationId, title);
|
|
19
|
+
await deps.refresh();
|
|
20
|
+
deps.pushSnapshot();
|
|
21
|
+
return { ok: true };
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createApprovalBridge } from "./approval-bridge.js";
|
|
2
|
+
import { createNoticeSink } from "./notices.js";
|
|
3
|
+
import { createRuntimeTurnWatchdog } from "./turn-watchdog.js";
|
|
4
|
+
export function createRuntimeServices(options) {
|
|
5
|
+
const sink = createNoticeSink({
|
|
6
|
+
now: options.now,
|
|
7
|
+
nextId: options.nextId,
|
|
8
|
+
dedupeMs: options.options.noticeDedupeMs,
|
|
9
|
+
max: options.options.maxNotices,
|
|
10
|
+
onNotice: options.onNotice,
|
|
11
|
+
});
|
|
12
|
+
const notice = (text, level = "info", key) => void sink.push(text, level, key);
|
|
13
|
+
const approval = createApprovalBridge({
|
|
14
|
+
rules: options.options.rules,
|
|
15
|
+
now: options.now,
|
|
16
|
+
nextId: options.nextId,
|
|
17
|
+
onPending: options.onPending,
|
|
18
|
+
onNotice: (text, level) => notice(text, level),
|
|
19
|
+
});
|
|
20
|
+
options.options.engine.setApprovalHandler(approval.handler);
|
|
21
|
+
const watchdog = createRuntimeTurnWatchdog({
|
|
22
|
+
timeoutMs: options.options.turnTimeoutMs,
|
|
23
|
+
engine: options.options.engine,
|
|
24
|
+
isWaitingForUser: options.isWaitingForUser,
|
|
25
|
+
notice: (text, level) => notice(text, level),
|
|
26
|
+
});
|
|
27
|
+
return { notice, approval, watchdog };
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|