opencode-subagent-magazine 1.7.1 → 1.7.3

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 (53) hide show
  1. package/dist/_version.d.ts +1 -1
  2. package/dist/panel/entry-map.d.ts +29 -0
  3. package/dist/tui.js +166 -19
  4. package/dist/v2.js +145 -22
  5. package/package.json +17 -4
  6. package/dist/_version.js +0 -2
  7. package/dist/clipboard.js +0 -85
  8. package/dist/core/color.js +0 -68
  9. package/dist/core/format.js +0 -113
  10. package/dist/core/index.js +0 -6
  11. package/dist/core/kv.js +0 -64
  12. package/dist/core/state-machine.js +0 -27
  13. package/dist/core/types.js +0 -9
  14. package/dist/core/usage.js +0 -1
  15. package/dist/i18n.js +0 -353
  16. package/dist/index.js +0 -528
  17. package/dist/panel/SubAgentPanel.js +0 -1644
  18. package/dist/panel/entry-map.js +0 -99
  19. package/dist/panel/panel-api.js +0 -1
  20. package/dist/panel/session-routing.js +0 -4
  21. package/dist/panel/store.js +0 -5
  22. package/dist/server.js +0 -14
  23. package/dist/v2/commands.js +0 -434
  24. package/dist/v2/db.js +0 -341
  25. package/dist/v2/index.js +0 -85
  26. package/dist/v2/settings-menu.js +0 -242
  27. package/dist/v2/theme.js +0 -12
  28. package/dist/v2/types.js +0 -6
  29. package/dist/v2/v2-panel-api.js +0 -623
  30. package/src/_version.ts +0 -2
  31. package/src/clipboard.ts +0 -134
  32. package/src/core/color.ts +0 -70
  33. package/src/core/format.ts +0 -105
  34. package/src/core/index.ts +0 -6
  35. package/src/core/kv.ts +0 -78
  36. package/src/core/state-machine.ts +0 -46
  37. package/src/core/types.ts +0 -85
  38. package/src/core/usage.ts +0 -16
  39. package/src/i18n.ts +0 -365
  40. package/src/index.tsx +0 -584
  41. package/src/panel/SubAgentPanel.tsx +0 -1877
  42. package/src/panel/entry-map.ts +0 -116
  43. package/src/panel/panel-api.ts +0 -100
  44. package/src/panel/session-routing.ts +0 -10
  45. package/src/panel/store.ts +0 -8
  46. package/src/server.ts +0 -19
  47. package/src/v2/commands.ts +0 -422
  48. package/src/v2/db.ts +0 -346
  49. package/src/v2/index.tsx +0 -149
  50. package/src/v2/settings-menu.ts +0 -259
  51. package/src/v2/theme.ts +0 -14
  52. package/src/v2/types.ts +0 -167
  53. package/src/v2/v2-panel-api.ts +0 -520
@@ -1,520 +0,0 @@
1
- import type { Context } from "./types"
2
- import type { PanelApi, PanelEvent, PanelEventType } from "../panel/panel-api"
3
- import { SETTING_KEYS } from "../core/kv"
4
- import { createSessionDbIndex, modelIdOf, statusOfChild } from "./db"
5
-
6
- const SUBAGENT_TOOL_V2 = "subagent"
7
-
8
- /** V2 tool metadata → V1 形状(V1 逻辑读 session_id/sessionId——双写保证命中)。 */
9
- function normalizeMeta(meta: unknown): Record<string, unknown> {
10
- const m = (meta && typeof meta === "object")
11
- ? { ...(meta as Record<string, unknown>) }
12
- : {}
13
- if (m.sessionID !== undefined) {
14
- if (m.session_id === undefined) m.session_id = m.sessionID
15
- if (m.sessionId === undefined) m.sessionId = m.sessionID
16
- }
17
- return m
18
- }
19
-
20
- /** V2 content part → V1 Part 形状(scan 的 part() 消费)。 */
21
- function toV1Part(p: Record<string, any>): Record<string, any> {
22
- if (p.type === "tool") {
23
- const st = { ...(p.state ?? {}) }
24
- if (st.output === undefined && Array.isArray(st.content)) {
25
- st.output = (st.content as Array<Record<string, any>>)
26
- .map((c) => c.type === "text" ? c.text : c.type === "file" ? String(c.path ?? c.filename ?? "") : JSON.stringify(c))
27
- .filter((t): t is string => typeof t === "string" && t.length > 0)
28
- .join("\n")
29
- }
30
- // TUI 数据层把子代理 metadata 放在 part 级,旧缓存形状可能嵌在 state 里——两处合并后再双写。
31
- const meta = normalizeMeta({
32
- ...(((st as Record<string, unknown>).metadata ?? {}) as Record<string, unknown>),
33
- ...((p.metadata ?? {}) as Record<string, unknown>),
34
- })
35
- return { type: "tool", id: p.id, tool: p.tool ?? p.name, state: st, subagent_type: p.subagent_type, metadata: meta }
36
- }
37
- if (p.type === "text") return { type: "text", text: String(p.text ?? "") }
38
- if (p.type === "file") return { type: "file", source: p.source ?? {} }
39
- if (p.type === "reasoning") return { type: "reasoning", text: String(p.text ?? "") }
40
- return p
41
- }
42
-
43
- /**
44
- * V2 (opencode2) context → PanelApi 适配实现。
45
- * 数据流(功能对齐 V1):
46
- * - usage:messages 反向遍历(同 V1 readSessionTokens/Cost/Model 逻辑);todo 无 V2 对应 → undefined
47
- * - 事件:tool 事件 → V1 ToolPart(subagent 工具名归一化 + metadata 双写 session_id/sessionId);
48
- * execution.succeeded/failed → session.idle/error
49
- * - 取消:session.interrupt(V1 abort 的 V2 对应)
50
- * - 路由:router.navigate({type:"session"})(V1 route.navigate 的 V2 对应)
51
- */
52
- export function createPanelApi(context: Context, settings: PanelApi["settings"]): PanelApi {
53
- const kvStore = new Map<string, [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]>()
54
- const messageIndex = new Map<string, Record<string, any>>()
55
- // 工具信息记忆(key: msgID\u0000id):called 事件可能不带 name——input.started 时记录;
56
- // progress/success/failed 复用(含 input——防后续事件的空 input 覆盖 title/prompt)
57
- const toolInfo = new Map<string, { name?: string; input?: Record<string, unknown> }>()
58
-
59
- const kvGet = <T>(key: string, fallback?: T): T | undefined => {
60
- let entry = kvStore.get(key)
61
- if (!entry) {
62
- const created = context.storage.store<Record<string, any>>(`subagent_magazine.${key}`, {
63
- initial: { value: fallback },
64
- })
65
- entry = created as [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]
66
- kvStore.set(key, entry)
67
- }
68
- const v = entry[0].value
69
- return v === undefined ? fallback : (v as T)
70
- }
71
- const kvSet = (key: string, value: unknown): Promise<void> => {
72
- let entry = kvStore.get(key)
73
- if (!entry) {
74
- const created = context.storage.store<Record<string, any>>(`subagent_magazine.${key}`, {
75
- initial: { value },
76
- })
77
- entry = created as [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]
78
- kvStore.set(key, entry)
79
- }
80
- const [, mutate] = entry
81
- return mutate((d) => { d.value = value })
82
- }
83
- // 原子 read-modify-write:宿主在存储锁内基于最新磁盘值
84
- // 应用 updater,因此并发 TUI 实例会 merge,
85
- // 而不是用过期快照互相覆盖。
86
- const kvUpdate = (key: string, updater: (current: unknown) => unknown): Promise<void> => {
87
- let entry = kvStore.get(key)
88
- if (!entry) {
89
- const created = context.storage.store<Record<string, any>>(`subagent_magazine.${key}`, {
90
- initial: { value: undefined },
91
- })
92
- entry = created as [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]
93
- kvStore.set(key, entry)
94
- }
95
- const [, mutate] = entry
96
- return mutate((d) => { d.value = updater(d.value) })
97
- }
98
-
99
- // 本地 opencode.db 索引:补全前台子代理缺失的 sessionId 与用量字段。
100
- // 数据库不可用、库结构不符或设置项 dbSync 关闭时静默禁用(回退宿主 API)。
101
- const dbIndex = createSessionDbIndex(() => kvGet<boolean>(SETTING_KEYS.dbSync, true) !== false)
102
-
103
- /** 事件 metadata 缺少子会话 id 时用本地库按 call id 回填(前台/后台通用)。 */
104
- const withResolvedSid = (data: Record<string, any>, meta: Record<string, unknown>): Record<string, unknown> => {
105
- if (meta.sessionID !== undefined) return meta
106
- try {
107
- const parent = data.sessionID !== undefined ? String(data.sessionID) : ""
108
- const callId = data.id !== undefined ? String(data.id) : ""
109
- const sid = parent && callId ? dbIndex?.resolveCall(parent, callId) : undefined
110
- if (sid) return normalizeMeta({ ...meta, sessionID: sid })
111
- } catch {}
112
- return meta
113
- }
114
-
115
- const isSubagentTool = (name: string | undefined): boolean =>
116
- name === SUBAGENT_TOOL_V2 || name === "task" || name === "delegate" || name === "call_omo_agent"
117
-
118
- /** 事件未携带工具名/input 时,从宿主消息缓存中恢复。
119
- * V2 的 called/progress/success 事件没有 `name`,且
120
- * input.started 可能被错过(TUI 启动、插件重载、流抖动)——
121
- * 没有这个回退,运行中的子代理要到完成时才会出现。 */
122
- const findToolPart = (data: Record<string, any>): { name?: string; input?: Record<string, unknown> } | undefined => {
123
- const sid = data.sessionID
124
- const msgID = data.assistantMessageID
125
- const partID = data.id
126
- if (sid === undefined || msgID === undefined || partID === undefined) return undefined
127
- try {
128
- const raw = context.data.session.message.list(String(sid))
129
- if (!raw) return undefined
130
- for (let i = raw.length - 1; i >= 0; i--) {
131
- const m = raw[i] as Record<string, any>
132
- if (!m || String(m.id) !== String(msgID)) continue
133
- const content = Array.isArray(m.content) ? m.content : []
134
- for (const p of content) {
135
- const pp = p as Record<string, any>
136
- if (pp && pp.type === "tool" && String(pp.id) === String(partID)) {
137
- const st = (pp.state ?? {}) as Record<string, any>
138
- const input = (st.input && typeof st.input === "object") ? st.input as Record<string, unknown> : undefined
139
- const name = pp.name ?? pp.tool
140
- return { name: name !== undefined ? String(name) : undefined, input }
141
- }
142
- }
143
- break
144
- }
145
- } catch {}
146
- return undefined
147
- }
148
-
149
- /** V2 tool 事件 → V1 ToolPart(非 subagent 工具返回 undefined——面板只关心子代理)。
150
- * 识别规则(对齐 V2 官方 stream-v2.subagent.ts):
151
- * - input.started/called:工具名在 subagent 集合(subagent/task/delegate/call_omo_agent)
152
- * - progress:工具名记忆(input.started/called 记录——无记录忽略,防误识别)
153
- * - success/failed:metadata.sessionID 存在(subagent 工具注入子会话 ID——question 等
154
- * 普通工具无此字段——忽略)
155
- * 归一化:subagent → task(V1 SUBAGENT_TOOLS 集合命中);subagent_type 补 input.agent
156
- * (V1 逻辑读 subagent_type 显示真实 agent 名)。 */
157
- const toolEventToPart = (event: Record<string, any>): Record<string, any> | undefined => {
158
- const type = event.type as string
159
- const data = event.data as Record<string, any> | undefined
160
- if (!data) return undefined
161
- const key = data.assistantMessageID !== undefined ? `${String(data.assistantMessageID)}\u0000${String(data.id)}` : undefined
162
- const info = key ? toolInfo.get(key) : undefined
163
- const normName = (n: string) => (n === SUBAGENT_TOOL_V2 ? "task" : n)
164
- const agentOf = (input: any): string | undefined => {
165
- const i = (input && typeof input === "object") ? input : {}
166
- return typeof i.agent === "string" ? i.agent : undefined
167
- }
168
- if (type === "session.tool.input.started") {
169
- if (!isSubagentTool(data.name)) return undefined
170
- if (key) toolInfo.set(key, { name: String(data.name), input: {} })
171
- return { type: "tool", tool: normName(String(data.name)), id: String(data.id), state: { status: "pending", input: {} } }
172
- }
173
- if (type === "session.tool.called") {
174
- let name: string | undefined = data.name ?? info?.name
175
- let input: Record<string, unknown> | undefined =
176
- (data.input as Record<string, unknown> | undefined) ?? info?.input
177
- if (!name) {
178
- const resolved = findToolPart(data)
179
- if (resolved?.name) {
180
- name = resolved.name
181
- if (input === undefined) input = resolved.input
182
- }
183
- }
184
- if (name && !isSubagentTool(name)) return undefined
185
- if (!name) return undefined
186
- if (key) toolInfo.set(key, { name: String(name), input: input ?? {} })
187
- return {
188
- type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(input),
189
- state: { status: "running", input: input ?? {}, metadata: withResolvedSid(data, {}) },
190
- }
191
- }
192
- if (type === "session.tool.progress") {
193
- let name: string | undefined = data.name ?? info?.name
194
- if (!name) name = findToolPart(data)?.name
195
- if (name && !isSubagentTool(name)) return undefined
196
- if (!name) return undefined
197
- return {
198
- type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(info?.input),
199
- state: { status: "running", input: info?.input ?? {}, metadata: withResolvedSid(data, normalizeMeta(data.metadata)) },
200
- }
201
- }
202
- if (type === "session.tool.success" || type === "session.tool.failed") {
203
- const eventMeta = normalizeMeta(data.metadata)
204
- const meta = withResolvedSid(data, eventMeta)
205
- // 取消导致的工具失败:metadata.status 是工具被打断时的状态("running")——不是真错误。
206
- // 忽略——最终状态由 execution.interrupted → settleOnIdle 裁定 cancelled
207
- // (否则 handlePartUpdated 的 error 分支会把 cancel_requested 覆盖成 error)
208
- if (type === "session.tool.failed" && meta.status === "running") return undefined
209
- let name: string | undefined = data.name ?? info?.name
210
- let input: Record<string, unknown> | undefined =
211
- info?.input ?? (data.input as Record<string, unknown> | undefined)
212
- if (!name || input === undefined) {
213
- const resolved = findToolPart(data)
214
- if (!name && resolved?.name) name = resolved.name
215
- if (input === undefined && resolved?.input) input = resolved.input
216
- }
217
- if (name && !isSubagentTool(name)) return undefined
218
- if (key && name) toolInfo.set(key, { name: String(name), input: input ?? info?.input ?? {} })
219
- const finalInput = input ?? {}
220
- // 后台任务缺少子会话 metadata 时无法判定终态,交给扫描/轮询兜底;
221
- // 前台任务没有 sessionID 也必须回传 completed,否则条目会一直停在 running。
222
- if (meta.sessionID === undefined && (finalInput.background === true || finalInput.run_in_background === true)) {
223
- return undefined
224
- }
225
- // sid 来自本地库(事件没带)时以库里的 time_idle 为准:库里会话仍在运行
226
- // 就不提前收尾,交给周期性 reconcile 按真实状态落定(无时间阈值猜测)。
227
- let status: "completed" | "error" | "running" = type === "session.tool.failed" ? "error" : "completed"
228
- if (meta.sessionID !== undefined && meta.sessionID !== eventMeta.sessionID) {
229
- try {
230
- const childInfo = dbIndex?.info(String(meta.sessionID))
231
- if (childInfo && statusOfChild(childInfo) === undefined) status = "running"
232
- } catch {}
233
- }
234
- return {
235
- type: "tool", tool: normName(String(name ?? "task")), id: String(data.id), subagent_type: agentOf(finalInput),
236
- state: {
237
- status,
238
- input: finalInput,
239
- metadata: meta,
240
- },
241
- }
242
- }
243
- return undefined
244
- }
245
-
246
- /** 消息归一化(V1 形状:role/tokens/cost/modelID)+ 建立 part 索引。 */
247
- const normalizeMessages = (sid: string): any[] => {
248
- const raw = context.data.session.message.list(sid) ?? []
249
- const out: any[] = []
250
- for (const m of raw) {
251
- const record = m as Record<string, any>
252
- messageIndex.set(String(m.id), record)
253
- const model = (m.model as { providerID?: string; id?: string } | undefined) ?? (typeof m.model === "string" ? { id: m.model } : undefined)
254
- out.push({
255
- ...m,
256
- role: m.type === "assistant" ? "assistant" : m.type === "user" ? "user" : m.type,
257
- modelID: model?.id,
258
- })
259
- }
260
- return out
261
- }
262
-
263
- // ── usage enrichment(用量补全)──
264
- // TUI 数据层只为已加载的会话缓存 messages。从 KV 恢复的
265
- // 子会话通常没有缓存,因此读取依次回退到
266
- // 会话级聚合值、最后是(限流的)message sync——
267
- // 结果写入本地缓存,供面板的周期性 enrichment 读取。
268
- const usageCache = new Map<string, { tokens?: number; cost?: number; model?: string; at: number }>()
269
- const syncRequestedAt = new Map<string, number>()
270
- const USAGE_CACHE_TTL_MS = 5000
271
- const SYNC_THROTTLE_MS = 30000
272
-
273
- const rememberUsage = (sid: string, patch: { tokens?: number; cost?: number; model?: string }) => {
274
- const prev = usageCache.get(sid)
275
- usageCache.set(sid, { ...prev, ...patch, at: Date.now() })
276
- }
277
-
278
- const cachedUsage = (sid: string) => {
279
- const hit = usageCache.get(sid)
280
- if (!hit) return undefined
281
- if (Date.now() - hit.at > USAGE_CACHE_TTL_MS) return undefined
282
- return hit
283
- }
284
-
285
- const requestMessageSync = (sid: string) => {
286
- const last = syncRequestedAt.get(sid) ?? 0
287
- if (Date.now() - last < SYNC_THROTTLE_MS) return
288
- syncRequestedAt.set(sid, Date.now())
289
- try {
290
- const sync = context.data.session.message.sync
291
- if (typeof sync === "function") void sync.call(context.data.session.message, sid).catch(() => {})
292
- } catch {}
293
- }
294
-
295
- const listedSession = (sid: string) => {
296
- try { return context.data.session.list()?.find((s) => s.id === sid) } catch { return undefined }
297
- }
298
-
299
- return {
300
- kv: {
301
- get: kvGet,
302
- set: kvSet as (key: string, value: unknown) => void,
303
- update: kvUpdate,
304
- },
305
- usage: {
306
- readSessionTokens: (sid: string): number | undefined => {
307
- if (!sid) return undefined
308
- // 本地库优先:宿主数据层缺少该子会话缓存时也能给出用量。
309
- try {
310
- const dbTokens = dbIndex?.info(sid)?.tokens
311
- if (dbTokens !== undefined) { rememberUsage(sid, { tokens: dbTokens }); return dbTokens }
312
- } catch {}
313
- try {
314
- const msgs = context.data.session.message.list(sid)
315
- if (msgs) {
316
- for (let i = msgs.length - 1; i >= 0; i--) {
317
- const m = msgs[i] as Record<string, any>
318
- if (m.type !== "assistant") continue
319
- const t = m.tokens as Record<string, any> | undefined
320
- if (!t || !(Number(t.output) > 0)) continue
321
- const ctx =
322
- (Number(t.input) || 0) +
323
- (Number(t.output) || 0) +
324
- (Number(t.reasoning) || 0) +
325
- (Number(t.cache?.read) || 0) +
326
- (Number(t.cache?.write) || 0)
327
- if (ctx > 0) { rememberUsage(sid, { tokens: ctx }); return ctx }
328
- }
329
- }
330
- // 本地无消息(子会话尚未加载):使用最近同步的值,
331
- // 否则请数据层同步,稍后重试。
332
- const cached = cachedUsage(sid)?.tokens
333
- if (cached !== undefined) return cached
334
- requestMessageSync(sid)
335
- return undefined
336
- } catch { return undefined }
337
- },
338
- readSessionCost: (sid: string): number | undefined => {
339
- if (!sid) return undefined
340
- try {
341
- const dbCost = dbIndex?.info(sid)?.cost
342
- if (dbCost !== undefined) { rememberUsage(sid, { cost: dbCost }); return dbCost }
343
- } catch {}
344
- try {
345
- const direct = context.data.session.cost(sid)
346
- if (typeof direct === "number" && direct > 0) { rememberUsage(sid, { cost: direct }); return direct }
347
- } catch {}
348
- try {
349
- const session = context.data.session.get(sid)
350
- if (session?.cost != null && session.cost > 0) { rememberUsage(sid, { cost: session.cost }); return session.cost }
351
- const listed = listedSession(sid)
352
- if (listed?.cost != null && listed.cost > 0) { rememberUsage(sid, { cost: listed.cost }); return listed.cost }
353
- const msgs = context.data.session.message.list(sid)
354
- if (Array.isArray(msgs) && msgs.length > 0) {
355
- let total = 0
356
- let sawAssistant = false
357
- for (const m of msgs as any[]) {
358
- if (m.type !== "assistant") continue
359
- sawAssistant = true
360
- if (typeof m.cost === "number") total += m.cost
361
- }
362
- // 已加载的历史是权威来源:assistant 消息 cost 为 0 表示
363
- // 该会话确实免费(如 budget/test 模型),而非未知。
364
- if (sawAssistant) { rememberUsage(sid, { cost: total }); return total }
365
- }
366
- const cached = cachedUsage(sid)?.cost
367
- if (cached !== undefined) return cached
368
- requestMessageSync(sid)
369
- return undefined
370
- } catch { return undefined }
371
- },
372
- readSessionModel: (sid: string): string | undefined => {
373
- if (!sid) return undefined
374
- try {
375
- const dbModel = dbIndex?.info(sid)?.model
376
- if (dbModel !== undefined) { rememberUsage(sid, { model: dbModel }); return dbModel }
377
- } catch {}
378
- try {
379
- const msgs = context.data.session.message.list(sid)
380
- if (msgs) {
381
- for (let i = msgs.length - 1; i >= 0; i--) {
382
- const m = msgs[i] as Record<string, any>
383
- if (m.type !== "assistant") continue
384
- const id = modelIdOf(m.model)
385
- if (id) { rememberUsage(sid, { model: id }); return id }
386
- }
387
- }
388
- const sessionModel = modelIdOf(context.data.session.get(sid)?.model)
389
- if (sessionModel) { rememberUsage(sid, { model: sessionModel }); return sessionModel }
390
- const listedModel = modelIdOf(listedSession(sid)?.model)
391
- if (listedModel) { rememberUsage(sid, { model: listedModel }); return listedModel }
392
- const cached = cachedUsage(sid)?.model
393
- if (cached !== undefined) return cached
394
- requestMessageSync(sid)
395
- return undefined
396
- } catch { return undefined }
397
- },
398
- readSessionTodo: () => undefined, // V2 无 todo 数据源
399
- },
400
- session: {
401
- get: (sid) => {
402
- try {
403
- const direct = context.data.session.get(sid)
404
- if (direct) return direct as any
405
- // 较旧的子会话可能未注入本地缓存;回退到
406
- // 会话列表,保证基于 parentID 的路由继续工作。
407
- const list = context.data.session.list?.()
408
- if (Array.isArray(list)) {
409
- const found = list.find((s) => (s as any)?.id === sid)
410
- if (found) return found as any
411
- }
412
- return undefined
413
- } catch { return undefined }
414
- },
415
- status: (sid) => {
416
- try {
417
- const st = context.data.session.status(sid)
418
- if (st) {
419
- // V2 的 "running" 等价 V1 的 "busy"(cancelEntry 检查 `st.type !== "busy"`——
420
- // 不映射会跳过取消流程直接标记 done)
421
- const type = st === "running" ? "busy" : st
422
- return { type }
423
- }
424
- } catch {}
425
- // 宿主没有该子会话的实时状态(未加载缓存)时用本地库终态兜底:
426
- // time_idle 存在且没有恢复活动即已结束;仍在运行则回报 busy。
427
- try {
428
- const info = dbIndex?.info(sid)
429
- if (info?.active === true) return { type: "busy" }
430
- if (statusOfChild(info) !== undefined) return { type: "idle" }
431
- } catch {}
432
- return undefined
433
- },
434
- messages: (sid) => { try { return normalizeMessages(sid) } catch { return undefined } },
435
- part: (messageID) => {
436
- const msg = messageIndex.get(String(messageID))
437
- if (!msg) return undefined
438
- if (!Array.isArray(msg.content) || msg.content.length === 0) {
439
- if (typeof msg.text === "string" && msg.text.length > 0) {
440
- return [{ type: "text", text: msg.text }]
441
- }
442
- return []
443
- }
444
- return msg.content.map((p) => toV1Part(p as Record<string, any>))
445
- },
446
- resolveChild: (input) => {
447
- try {
448
- const sid = input.callId ? dbIndex?.resolveCall(input.parentId, input.callId) : undefined
449
- if (sid) return sid
450
- return dbIndex?.matchChild(input.parentId, input.agent, input.startedAt)
451
- } catch { return undefined }
452
- },
453
- listChildren: (parentId) => {
454
- try { return dbIndex?.enabled() ? dbIndex.children(parentId) : undefined } catch { return undefined }
455
- },
456
- },
457
- event: {
458
- on: (type: PanelEventType, cb: (e: PanelEvent) => void) => {
459
- switch (type) {
460
- case "part.updated": {
461
- const unsubs: Array<() => void> = []
462
- for (const evt of ["session.tool.input.started", "session.tool.called", "session.tool.progress", "session.tool.success", "session.tool.failed"]) {
463
- unsubs.push(context.data.on(evt, (e) => {
464
- const part = toolEventToPart((e as Record<string, any>))
465
- const evtData = (e as Record<string, any>).data as Record<string, any> | undefined
466
- if (!part) {
467
- return
468
- }
469
- // V2 事件流是全局的(跨所有会话);把发起会话 ID 一并传给面板,
470
- // 让面板只归账到正在查看的会话(V1 宿主已按会话 scope,无需该字段)。
471
- const sid = evtData?.sessionID !== undefined ? String(evtData.sessionID) : undefined
472
- if (!sid) return
473
- cb({ type, scope: "global", payload: { part, sessionID: sid } })
474
- }))
475
- }
476
- return () => { for (const u of unsubs) u() }
477
- }
478
- case "message.updated": {
479
- const unsubs: Array<() => void> = []
480
- for (const evt of ["session.step.started", "session.step.ended", "session.step.failed"]) {
481
- unsubs.push(context.data.on(evt, () => cb({ type })))
482
- }
483
- return () => { for (const u of unsubs) u() }
484
- }
485
- case "session.idle": {
486
- // V1 的 session.idle = 会话结束(含取消后的 idle——settleOnIdle 裁定 cancelled)
487
- // V2:execution.succeeded(正常完成)+ execution.interrupted(被中断——取消)都映射到这里
488
- const unsubs: Array<() => void> = []
489
- for (const evt of ["session.execution.succeeded", "session.execution.interrupted"]) {
490
- unsubs.push(context.data.on(evt, (e) => {
491
- const sid = String(((e as Record<string, any>).data as Record<string, any> | undefined)?.sessionID ?? "")
492
- if (sid) cb({ type, scope: "global", payload: { sessionID: sid } })
493
- }))
494
- }
495
- return () => { for (const u of unsubs) u() }
496
- }
497
- case "session.error":
498
- return context.data.on("session.execution.failed", (e) => {
499
- const evt = (e as Record<string, any>).data as Record<string, any> | undefined
500
- const sid = String(evt?.sessionID ?? "")
501
- if (sid) cb({ type, scope: "global", payload: { sessionID: sid, error: evt?.error } })
502
- })
503
- default:
504
- return () => {}
505
- }
506
- },
507
- },
508
- client: {
509
- // V2 官方取消途径:context.client.session.interrupt(data.session 无 interrupt)
510
- abort: ({ sessionID }) => context.client.session.interrupt({ sessionID }).then(() => {}),
511
- },
512
- route: {
513
- navigateSession: (sessionID) => context.ui.router.navigate({ type: "session", sessionID }),
514
- },
515
- ui: {
516
- toast: (message, opts) => context.ui.toast.show({ message, title: opts?.title, variant: opts?.variant }),
517
- },
518
- settings,
519
- }
520
- }