opencode-subagent-magazine 1.6.0-beta.0 → 1.6.0-beta.1

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.
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.6.0-beta.0";
1
+ export declare const PLUGIN_VERSION = "1.6.0-beta.1";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.6.0-beta.0";
2
+ export const PLUGIN_VERSION = "1.6.0-beta.1";
@@ -1,2 +1,2 @@
1
1
  /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
2
- export const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"]);
2
+ export const SUBAGENT_TOOLS = new Set(["task", "subagent", "delegate", "call_omo_agent"]);
@@ -334,6 +334,11 @@ export function SubAgentPanel(props) {
334
334
  const part = event.payload?.part;
335
335
  if (!part)
336
336
  return;
337
+ // V2 事件流是全局的——若 payload 带发起会话 ID,则只归账到正在查看的会话,
338
+ // 避免其他会话的子代理写进当前侧边栏;V1 宿主已按会话 scope,不传 sessionID。
339
+ const eventSid = event.payload?.sessionID !== undefined ? String(event.payload.sessionID) : undefined;
340
+ if (eventSid !== undefined && eventSid !== props.sessionId)
341
+ return;
337
342
  // SubtaskPart
338
343
  if (part.type === "subtask") {
339
344
  const agent = String(part.agent ?? "?");
@@ -384,7 +389,7 @@ export function SubAgentPanel(props) {
384
389
  if (hasChild)
385
390
  status = "running";
386
391
  }
387
- const agent = String(part.subagent_type ?? input?.subagent_type ?? input?.category ?? tool);
392
+ const agent = String(part.subagent_type ?? input?.agent ?? input?.subagent_type ?? input?.category ?? tool);
388
393
  const prompt = String(input?.prompt ?? part.description ?? "");
389
394
  const desc = input?.description !== undefined ? String(input.description) : "";
390
395
  const title = desc || truncate(prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim(), 40);
@@ -813,7 +818,7 @@ export function SubAgentPanel(props) {
813
818
  // If already tracked as running but tool state says completed/error → update
814
819
  // If not tracked → add fresh
815
820
  const input = st?.input;
816
- const agent = String(part.subagent_type ?? input?.subagent_type ?? tool);
821
+ const agent = String(part.subagent_type ?? input?.agent ?? input?.subagent_type ?? tool);
817
822
  const prompt = String(input?.prompt ?? part.description ?? "");
818
823
  const desc = input?.description !== undefined ? String(input.description) : "";
819
824
  const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
package/dist/tui.js CHANGED
@@ -6,7 +6,7 @@ import { createComponent as _$createComponent2 } from "@opentui/solid";
6
6
  import { createSignal as createSignal3 } from "solid-js";
7
7
 
8
8
  // src/_version.ts
9
- var PLUGIN_VERSION = "1.6.0-beta.0";
9
+ var PLUGIN_VERSION = "1.6.0-beta.1";
10
10
 
11
11
  // src/i18n.ts
12
12
  var ZH_T = {
@@ -363,7 +363,7 @@ async function copyText(text) {
363
363
  }
364
364
 
365
365
  // src/core/types.ts
366
- var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["task", "delegate", "call_omo_agent"]);
366
+ var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["task", "subagent", "delegate", "call_omo_agent"]);
367
367
 
368
368
  // src/core/format.ts
369
369
  function charColumns(c) {
@@ -901,6 +901,8 @@ function SubAgentPanel(props) {
901
901
  const handlePartUpdated = (event) => {
902
902
  const part = event.payload?.part;
903
903
  if (!part) return;
904
+ const eventSid = event.payload?.sessionID !== void 0 ? String(event.payload.sessionID) : void 0;
905
+ if (eventSid !== void 0 && eventSid !== props.sessionId) return;
904
906
  if (part.type === "subtask") {
905
907
  const agent = String(part.agent ?? "?");
906
908
  const prompt = String(part.prompt ?? "");
@@ -949,7 +951,7 @@ function SubAgentPanel(props) {
949
951
  const hasChild = stMetaCheck?.session_id !== void 0 || stMetaCheck?.sessionId !== void 0;
950
952
  if (hasChild) status = "running";
951
953
  }
952
- const agent = String(part.subagent_type ?? input?.subagent_type ?? input?.category ?? tool);
954
+ const agent = String(part.subagent_type ?? input?.agent ?? input?.subagent_type ?? input?.category ?? tool);
953
955
  const prompt = String(input?.prompt ?? part.description ?? "");
954
956
  const desc = input?.description !== void 0 ? String(input.description) : "";
955
957
  const title = desc || truncate(prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim(), 40);
@@ -1349,7 +1351,7 @@ function SubAgentPanel(props) {
1349
1351
  }
1350
1352
  }
1351
1353
  const input = st?.input;
1352
- const agent = String(part.subagent_type ?? input?.subagent_type ?? tool);
1354
+ const agent = String(part.subagent_type ?? input?.agent ?? input?.subagent_type ?? tool);
1353
1355
  const prompt = String(input?.prompt ?? part.description ?? "");
1354
1356
  const desc = input?.description !== void 0 ? String(input.description) : "";
1355
1357
  const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
@@ -289,8 +289,13 @@ export function createPanelApi(context, settings) {
289
289
  for (const evt of ["session.tool.input.started", "session.tool.called", "session.tool.progress", "session.tool.success", "session.tool.failed"]) {
290
290
  unsubs.push(context.data.on(evt, (e) => {
291
291
  const part = toolEventToPart(e);
292
- if (part)
293
- cb({ type, payload: { part } });
292
+ if (!part)
293
+ return;
294
+ // V2 事件流是全局的(跨所有会话);把发起会话 ID 一并传给面板,
295
+ // 让面板只归账到正在查看的会话(V1 宿主已按会话 scope,无需该字段)。
296
+ const evt = e.data;
297
+ const sid = evt?.sessionID !== undefined ? String(evt.sessionID) : undefined;
298
+ cb({ type, payload: { part, sessionID: sid } });
294
299
  }));
295
300
  }
296
301
  return () => { for (const u of unsubs)
package/dist/v2.js CHANGED
@@ -239,7 +239,10 @@ function createPanelApi(context, settings) {
239
239
  for (const evt of ["session.tool.input.started", "session.tool.called", "session.tool.progress", "session.tool.success", "session.tool.failed"]) {
240
240
  unsubs.push(context.data.on(evt, (e) => {
241
241
  const part = toolEventToPart(e);
242
- if (part) cb({ type, payload: { part } });
242
+ if (!part) return;
243
+ const evt2 = e.data;
244
+ const sid = evt2?.sessionID !== void 0 ? String(evt2.sessionID) : void 0;
245
+ cb({ type, payload: { part, sessionID: sid } });
243
246
  }));
244
247
  }
245
248
  return () => {
@@ -325,7 +328,7 @@ function readTTLDays(kv) {
325
328
  }
326
329
 
327
330
  // src/_version.ts
328
- var PLUGIN_VERSION = "1.6.0-beta.0";
331
+ var PLUGIN_VERSION = "1.6.0-beta.1";
329
332
 
330
333
  // src/i18n.ts
331
334
  var ZH_T = {
@@ -931,7 +934,7 @@ async function copyText(text) {
931
934
  }
932
935
 
933
936
  // src/core/types.ts
934
- var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["task", "delegate", "call_omo_agent"]);
937
+ var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["task", "subagent", "delegate", "call_omo_agent"]);
935
938
 
936
939
  // src/core/format.ts
937
940
  function charColumns(c) {
@@ -1464,6 +1467,8 @@ function SubAgentPanel(props) {
1464
1467
  const handlePartUpdated = (event) => {
1465
1468
  const part = event.payload?.part;
1466
1469
  if (!part) return;
1470
+ const eventSid = event.payload?.sessionID !== void 0 ? String(event.payload.sessionID) : void 0;
1471
+ if (eventSid !== void 0 && eventSid !== props.sessionId) return;
1467
1472
  if (part.type === "subtask") {
1468
1473
  const agent = String(part.agent ?? "?");
1469
1474
  const prompt = String(part.prompt ?? "");
@@ -1512,7 +1517,7 @@ function SubAgentPanel(props) {
1512
1517
  const hasChild = stMetaCheck?.session_id !== void 0 || stMetaCheck?.sessionId !== void 0;
1513
1518
  if (hasChild) status = "running";
1514
1519
  }
1515
- const agent = String(part.subagent_type ?? input?.subagent_type ?? input?.category ?? tool);
1520
+ const agent = String(part.subagent_type ?? input?.agent ?? input?.subagent_type ?? input?.category ?? tool);
1516
1521
  const prompt = String(input?.prompt ?? part.description ?? "");
1517
1522
  const desc = input?.description !== void 0 ? String(input.description) : "";
1518
1523
  const title = desc || truncate(prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim(), 40);
@@ -1912,7 +1917,7 @@ function SubAgentPanel(props) {
1912
1917
  }
1913
1918
  }
1914
1919
  const input = st?.input;
1915
- const agent = String(part.subagent_type ?? input?.subagent_type ?? tool);
1920
+ const agent = String(part.subagent_type ?? input?.agent ?? input?.subagent_type ?? tool);
1916
1921
  const prompt = String(input?.prompt ?? part.description ?? "");
1917
1922
  const desc = input?.description !== void 0 ? String(input.description) : "";
1918
1923
  const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-subagent-magazine",
3
- "version": "1.6.0-beta.0",
3
+ "version": "1.6.0-beta.1",
4
4
  "description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.6.0-beta.0";
2
+ export const PLUGIN_VERSION="1.6.0-beta.1";
package/src/core/types.ts CHANGED
@@ -1,58 +1,58 @@
1
- import type { LangCode } from "../i18n"
2
-
3
- export type SubStatus = "running" | "done" | "error" | "cancel_requested" | "cancelled"
4
-
5
- export interface SubEntry {
6
- id: string
7
- title: string
8
- agent: string
9
- prompt: string
10
- error?: string
11
- tokens?: number
12
- cost?: number
13
- status: SubStatus
14
- sessionId?: string
15
- startedAt: number
16
- endedAt?: number
17
- model?: string
18
- todoTotal?: number
19
- todoDone?: number
20
- cancelRequestedAt?: number
21
- abortAccepted?: boolean
22
- cancelReason?: "manual"
23
- }
24
-
25
- export type Lang = LangCode
26
- export type SortOrder = "desc" | "asc"
27
- export type ScrollMode = "wheel" | "click"
28
-
29
- /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
30
- export const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
31
-
32
- export interface ChildRecord {
33
- scroll: number
34
- expanded: string
35
- entries: SubEntry[]
36
- clearedIds?: string[]
37
- }
38
-
39
- export interface SessionRecord {
40
- ts: number
41
- entries: SubEntry[]
42
- scroll: number
43
- expanded: string
44
- children: Record<string, ChildRecord>
45
- clearedIds?: string[]
46
- }
47
-
48
- export interface SharedSignals {
49
- lang: () => Lang
50
- setLang: (l: Lang) => void
51
- maxEntries: () => number
52
- setMaxEntries: (n: number) => void
53
- sortOrder: () => SortOrder
54
- setSortOrder: (o: SortOrder) => void
55
- scrollMode: () => ScrollMode
56
- setScrollMode: (m: ScrollMode) => void
57
- sessionId: string
58
- }
1
+ import type { LangCode } from "../i18n"
2
+
3
+ export type SubStatus = "running" | "done" | "error" | "cancel_requested" | "cancelled"
4
+
5
+ export interface SubEntry {
6
+ id: string
7
+ title: string
8
+ agent: string
9
+ prompt: string
10
+ error?: string
11
+ tokens?: number
12
+ cost?: number
13
+ status: SubStatus
14
+ sessionId?: string
15
+ startedAt: number
16
+ endedAt?: number
17
+ model?: string
18
+ todoTotal?: number
19
+ todoDone?: number
20
+ cancelRequestedAt?: number
21
+ abortAccepted?: boolean
22
+ cancelReason?: "manual"
23
+ }
24
+
25
+ export type Lang = LangCode
26
+ export type SortOrder = "desc" | "asc"
27
+ export type ScrollMode = "wheel" | "click"
28
+
29
+ /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
30
+ export const SUBAGENT_TOOLS = new Set(["task", "subagent", "delegate", "call_omo_agent"])
31
+
32
+ export interface ChildRecord {
33
+ scroll: number
34
+ expanded: string
35
+ entries: SubEntry[]
36
+ clearedIds?: string[]
37
+ }
38
+
39
+ export interface SessionRecord {
40
+ ts: number
41
+ entries: SubEntry[]
42
+ scroll: number
43
+ expanded: string
44
+ children: Record<string, ChildRecord>
45
+ clearedIds?: string[]
46
+ }
47
+
48
+ export interface SharedSignals {
49
+ lang: () => Lang
50
+ setLang: (l: Lang) => void
51
+ maxEntries: () => number
52
+ setMaxEntries: (n: number) => void
53
+ sortOrder: () => SortOrder
54
+ setSortOrder: (o: SortOrder) => void
55
+ scrollMode: () => ScrollMode
56
+ setScrollMode: (m: ScrollMode) => void
57
+ sessionId: string
58
+ }
@@ -360,6 +360,11 @@ export function SubAgentPanel(props: {
360
360
  const part = event.payload?.part as Record<string, unknown> | undefined
361
361
  if (!part) return
362
362
 
363
+ // V2 事件流是全局的——若 payload 带发起会话 ID,则只归账到正在查看的会话,
364
+ // 避免其他会话的子代理写进当前侧边栏;V1 宿主已按会话 scope,不传 sessionID。
365
+ const eventSid = event.payload?.sessionID !== undefined ? String(event.payload.sessionID) : undefined
366
+ if (eventSid !== undefined && eventSid !== props.sessionId) return
367
+
363
368
  // SubtaskPart
364
369
  if (part.type === "subtask") {
365
370
  const agent = String(part.agent ?? "?")
@@ -411,7 +416,7 @@ export function SubAgentPanel(props: {
411
416
  if (hasChild) status = "running"
412
417
  }
413
418
 
414
- const agent = String((part as any).subagent_type ?? input?.subagent_type ?? input?.category ?? tool)
419
+ const agent = String((part as any).subagent_type ?? input?.agent ?? input?.subagent_type ?? input?.category ?? tool)
415
420
  const prompt = String(input?.prompt ?? (part as any).description ?? "")
416
421
  const desc = input?.description !== undefined ? String(input.description) : ""
417
422
  const title = desc || truncate(prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim(), 40)
@@ -829,7 +834,7 @@ export function SubAgentPanel(props: {
829
834
  // If not tracked → add fresh
830
835
 
831
836
  const input = st?.input as Record<string, unknown> | undefined
832
- const agent = String((part as any).subagent_type ?? input?.subagent_type ?? tool)
837
+ const agent = String((part as any).subagent_type ?? input?.agent ?? input?.subagent_type ?? tool)
833
838
  const prompt = String(input?.prompt ?? (part as any).description ?? "")
834
839
  const desc = input?.description !== undefined ? String(input.description) : ""
835
840
  const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40)
@@ -1,301 +1,306 @@
1
- import type { Context } from "./types"
2
- import type { PanelApi, PanelEvent, PanelEventType } from "../panel/panel-api"
3
-
4
- const SUBAGENT_TOOL_V2 = "subagent"
5
-
6
- /** V2 tool metadata → V1 形状(V1 逻辑读 session_id/sessionId——双写保证命中)。 */
7
- function normalizeMeta(meta: unknown): Record<string, unknown> {
8
- const m = (meta && typeof meta === "object")
9
- ? { ...(meta as Record<string, unknown>) }
10
- : {}
11
- if (m.sessionID !== undefined) {
12
- if (m.session_id === undefined) m.session_id = m.sessionID
13
- if (m.sessionId === undefined) m.sessionId = m.sessionID
14
- }
15
- return m
16
- }
17
-
18
- /** V2 content part → V1 Part 形状(scan 的 part() 消费)。 */
19
- function toV1Part(p: Record<string, any>): Record<string, any> {
20
- if (p.type === "tool") {
21
- const st = { ...(p.state ?? {}) }
22
- if (st.output === undefined && Array.isArray(st.content)) {
23
- st.output = (st.content as Array<Record<string, any>>)
24
- .map((c) => c.type === "text" ? c.text : c.type === "file" ? String(c.path ?? c.filename ?? "") : JSON.stringify(c))
25
- .filter((t): t is string => typeof t === "string" && t.length > 0)
26
- .join("\n")
27
- }
28
- return { type: "tool", tool: p.tool ?? p.name, state: st, subagent_type: p.subagent_type, metadata: normalizeMeta(p.metadata) }
29
- }
30
- if (p.type === "text") return { type: "text", text: String(p.text ?? "") }
31
- if (p.type === "file") return { type: "file", source: p.source ?? {} }
32
- if (p.type === "reasoning") return { type: "reasoning", text: String(p.text ?? "") }
33
- return p
34
- }
35
-
36
- /**
37
- * V2 (opencode2) context → PanelApi 适配实现。
38
- * 数据流(功能对齐 V1):
39
- * - usage:messages 反向遍历(同 V1 readSessionTokens/Cost/Model 逻辑);todo 无 V2 对应 → undefined
40
- * - 事件:tool 事件 → V1 ToolPart(subagent 工具名归一化 + metadata 双写 session_id/sessionId);
41
- * execution.succeeded/failed → session.idle/error
42
- * - 取消:session.interrupt(V1 abort 的 V2 对应)
43
- * - 路由:router.navigate({type:"session"})(V1 route.navigate 的 V2 对应)
44
- */
45
- export function createPanelApi(context: Context, settings: PanelApi["settings"]): PanelApi {
46
- const kvStore = new Map<string, [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]>()
47
- const messageIndex = new Map<string, Record<string, any>>()
48
- // 工具信息记忆(key: msgID\u0000id):called 事件可能不带 name——input.started 时记录;
49
- // progress/success/failed 复用(含 input——防后续事件的空 input 覆盖 title/prompt)
50
- const toolInfo = new Map<string, { name?: string; input?: Record<string, unknown> }>()
51
-
52
- const kvGet = <T>(key: string, fallback?: T): T | undefined => {
53
- let entry = kvStore.get(key)
54
- if (!entry) {
55
- const created = context.storage.store<Record<string, any>>(`subagent_magazine.${key}`, {
56
- initial: { value: fallback },
57
- })
58
- entry = created as [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]
59
- kvStore.set(key, entry)
60
- }
61
- const v = entry[0].value
62
- return v === undefined ? fallback : (v as T)
63
- }
64
- const kvSet = (key: string, value: unknown): Promise<void> => {
65
- let entry = kvStore.get(key)
66
- if (!entry) {
67
- const created = context.storage.store<Record<string, any>>(`subagent_magazine.${key}`, {
68
- initial: { value },
69
- })
70
- entry = created as [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]
71
- kvStore.set(key, entry)
72
- }
73
- const [, mutate] = entry
74
- return mutate((d) => { d.value = value })
75
- }
76
-
77
- const isSubagentTool = (name: string | undefined): boolean =>
78
- name === SUBAGENT_TOOL_V2 || name === "task" || name === "delegate" || name === "call_omo_agent"
79
-
80
- /** V2 tool 事件 → V1 ToolPart(非 subagent 工具返回 undefined——面板只关心子代理)。
81
- * 识别规则(对齐 V2 官方 stream-v2.subagent.ts):
82
- * - input.started/called:工具名在 subagent 集合(subagent/task/delegate/call_omo_agent)
83
- * - progress:工具名记忆(input.started/called 记录——无记录忽略,防误识别)
84
- * - success/failed:metadata.sessionID 存在(subagent 工具注入子会话 ID——question 等
85
- * 普通工具无此字段——忽略)
86
- * 归一化:subagent → task(V1 SUBAGENT_TOOLS 集合命中);subagent_type 补 input.agent
87
- * (V1 逻辑读 subagent_type 显示真实 agent 名)。 */
88
- const toolEventToPart = (event: Record<string, any>): Record<string, any> | undefined => {
89
- const type = event.type as string
90
- const data = event.data as Record<string, any> | undefined
91
- if (!data) return undefined
92
- const key = data.assistantMessageID !== undefined ? `${String(data.assistantMessageID)}\u0000${String(data.id)}` : undefined
93
- const info = key ? toolInfo.get(key) : undefined
94
- const normName = (n: string) => (n === SUBAGENT_TOOL_V2 ? "task" : n)
95
- const agentOf = (input: any): string | undefined => {
96
- const i = (input && typeof input === "object") ? input : {}
97
- return typeof i.agent === "string" ? i.agent : undefined
98
- }
99
- if (type === "session.tool.input.started") {
100
- if (!isSubagentTool(data.name)) return undefined
101
- if (key) toolInfo.set(key, { name: String(data.name), input: {} })
102
- return { type: "tool", tool: normName(String(data.name)), id: String(data.id), state: { status: "pending", input: {} } }
103
- }
104
- if (type === "session.tool.called") {
105
- const name = data.name ?? info?.name
106
- if (name && !isSubagentTool(name)) return undefined
107
- if (!name) return undefined
108
- if (key) toolInfo.set(key, { name: String(name), input: data.input ?? {} })
109
- return {
110
- type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(data.input),
111
- state: { status: "running", input: data.input ?? {}, metadata: {} },
112
- }
113
- }
114
- if (type === "session.tool.progress") {
115
- const name = data.name ?? info?.name
116
- if (name && !isSubagentTool(name)) return undefined
117
- if (!name) return undefined
118
- return {
119
- type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(info?.input),
120
- state: { status: "running", input: info?.input ?? {}, metadata: normalizeMeta(data.metadata) },
121
- }
122
- }
123
- if (type === "session.tool.success" || type === "session.tool.failed") {
124
- const meta = normalizeMeta(data.metadata)
125
- if (meta.sessionID === undefined) return undefined
126
- // 取消导致的工具失败:metadata.status 是工具被打断时的状态("running")——不是真错误。
127
- // 忽略——最终状态由 execution.interrupted → settleOnIdle 裁定 cancelled
128
- // (否则 handlePartUpdated 的 error 分支会把 cancel_requested 覆盖成 error)
129
- if (type === "session.tool.failed" && meta.status === "running") return undefined
130
- const name = data.name ?? info?.name
131
- if (name && !isSubagentTool(name)) return undefined
132
- const input = info?.input ?? data.input ?? {}
133
- return {
134
- type: "tool", tool: normName(String(name ?? "task")), id: String(data.id), subagent_type: agentOf(input),
135
- state: {
136
- status: type === "session.tool.failed" ? "error" : "completed",
137
- input,
138
- metadata: meta,
139
- },
140
- }
141
- }
142
- return undefined
143
- }
144
-
145
- /** 消息归一化(V1 形状:role/tokens/cost/modelID)+ 建立 part 索引。 */
146
- const normalizeMessages = (sid: string): any[] => {
147
- const raw = context.data.session.message.list(sid) ?? []
148
- const out: any[] = []
149
- for (const m of raw) {
150
- const record = m as Record<string, any>
151
- messageIndex.set(String(m.id), record)
152
- const model = (m.model as { providerID?: string; id?: string } | undefined) ?? (typeof m.model === "string" ? { id: m.model } : undefined)
153
- out.push({
154
- ...m,
155
- role: m.type === "assistant" ? "assistant" : m.type === "user" ? "user" : m.type,
156
- modelID: model?.id,
157
- })
158
- }
159
- return out
160
- }
161
-
162
- return {
163
- kv: {
164
- get: kvGet,
165
- set: kvSet as (key: string, value: unknown) => void,
166
- },
167
- usage: {
168
- readSessionTokens: (sid: string): number | undefined => {
169
- if (!sid) return undefined
170
- try {
171
- const msgs = context.data.session.message.list(sid)
172
- if (msgs) {
173
- for (let i = msgs.length - 1; i >= 0; i--) {
174
- const m = msgs[i] as Record<string, any>
175
- if (m.type !== "assistant") continue
176
- const t = m.tokens as Record<string, any> | undefined
177
- if (!t || !(Number(t.output) > 0)) continue
178
- const ctx =
179
- (Number(t.input) || 0) +
180
- (Number(t.output) || 0) +
181
- (Number(t.reasoning) || 0) +
182
- (Number(t.cache?.read) || 0) +
183
- (Number(t.cache?.write) || 0)
184
- if (ctx > 0) return ctx
185
- }
186
- }
187
- return undefined
188
- } catch { return undefined }
189
- },
190
- readSessionCost: (sid: string): number | undefined => {
191
- if (!sid) return undefined
192
- try {
193
- const session = context.data.session.get(sid)
194
- if (session?.cost != null && session.cost > 0) return session.cost
195
- const msgs = context.data.session.message.list(sid)
196
- if (!msgs) return undefined
197
- let total = 0
198
- for (const m of msgs as any[]) {
199
- if (m.type === "assistant" && typeof m.cost === "number") total += m.cost
200
- }
201
- return total > 0 ? total : undefined
202
- } catch { return undefined }
203
- },
204
- readSessionModel: (sid: string): string | undefined => {
205
- if (!sid) return undefined
206
- try {
207
- const msgs = context.data.session.message.list(sid)
208
- if (msgs) {
209
- for (let i = msgs.length - 1; i >= 0; i--) {
210
- const m = msgs[i] as Record<string, any>
211
- if (m.type !== "assistant") continue
212
- const model = m.model as { id?: string } | string | undefined
213
- if (typeof model === "string") return model
214
- if (model?.id) return String(model.id)
215
- }
216
- }
217
- return undefined
218
- } catch { return undefined }
219
- },
220
- readSessionTodo: () => undefined, // V2 无 todo 数据源
221
- },
222
- session: {
223
- get: (sid) => { try { return context.data.session.get(sid) as any } catch { return undefined } },
224
- status: (sid) => {
225
- try {
226
- const st = context.data.session.status(sid)
227
- // V2 的 "running" 等价 V1 的 "busy"(cancelEntry 检查 `st.type !== "busy"`——
228
- // 不映射会跳过取消流程直接标记 done)
229
- const type = st === "running" ? "busy" : st ?? ""
230
- return { type }
231
- } catch { return undefined }
232
- },
233
- messages: (sid) => { try { return normalizeMessages(sid) } catch { return undefined } },
234
- part: (messageID) => {
235
- const msg = messageIndex.get(String(messageID))
236
- if (!msg) return undefined
237
- if (!Array.isArray(msg.content) || msg.content.length === 0) {
238
- if (typeof msg.text === "string" && msg.text.length > 0) {
239
- return [{ type: "text", text: msg.text }]
240
- }
241
- return []
242
- }
243
- return msg.content.map((p) => toV1Part(p as Record<string, any>))
244
- },
245
- },
246
- event: {
247
- on: (type: PanelEventType, cb: (e: PanelEvent) => void) => {
248
- switch (type) {
249
- case "part.updated": {
250
- const unsubs: Array<() => void> = []
251
- for (const evt of ["session.tool.input.started", "session.tool.called", "session.tool.progress", "session.tool.success", "session.tool.failed"]) {
252
- unsubs.push(context.data.on(evt, (e) => {
253
- const part = toolEventToPart((e as Record<string, any>))
254
- if (part) cb({ type, payload: { part } })
255
- }))
256
- }
257
- return () => { for (const u of unsubs) u() }
258
- }
259
- case "message.updated": {
260
- const unsubs: Array<() => void> = []
261
- for (const evt of ["session.step.started", "session.step.ended", "session.step.failed"]) {
262
- unsubs.push(context.data.on(evt, () => cb({ type })))
263
- }
264
- return () => { for (const u of unsubs) u() }
265
- }
266
- case "session.idle": {
267
- // V1 的 session.idle = 会话结束(含取消后的 idle——settleOnIdle 裁定 cancelled)
268
- // V2:execution.succeeded(正常完成)+ execution.interrupted(被中断——取消)都映射到这里
269
- const unsubs: Array<() => void> = []
270
- for (const evt of ["session.execution.succeeded", "session.execution.interrupted"]) {
271
- unsubs.push(context.data.on(evt, (e) => {
272
- const sid = String(((e as Record<string, any>).data as Record<string, any> | undefined)?.sessionID ?? "")
273
- if (sid) cb({ type, payload: { sessionID: sid } })
274
- }))
275
- }
276
- return () => { for (const u of unsubs) u() }
277
- }
278
- case "session.error":
279
- return context.data.on("session.execution.failed", (e) => {
280
- const evt = (e as Record<string, any>).data as Record<string, any> | undefined
281
- const sid = String(evt?.sessionID ?? "")
282
- if (sid) cb({ type, payload: { sessionID: sid, error: evt?.error } })
283
- })
284
- default:
285
- return () => {}
286
- }
287
- },
288
- },
289
- client: {
290
- // V2 官方取消途径:context.client.session.interrupt(data.session 无 interrupt)
291
- abort: ({ sessionID }) => context.client.session.interrupt({ sessionID }).then(() => {}),
292
- },
293
- route: {
294
- navigateSession: (sessionID) => context.ui.router.navigate({ type: "session", sessionID }),
295
- },
296
- ui: {
297
- toast: (message, opts) => context.ui.toast.show({ message, title: opts?.title, variant: opts?.variant }),
298
- },
299
- settings,
300
- }
301
- }
1
+ import type { Context } from "./types"
2
+ import type { PanelApi, PanelEvent, PanelEventType } from "../panel/panel-api"
3
+
4
+ const SUBAGENT_TOOL_V2 = "subagent"
5
+
6
+ /** V2 tool metadata → V1 形状(V1 逻辑读 session_id/sessionId——双写保证命中)。 */
7
+ function normalizeMeta(meta: unknown): Record<string, unknown> {
8
+ const m = (meta && typeof meta === "object")
9
+ ? { ...(meta as Record<string, unknown>) }
10
+ : {}
11
+ if (m.sessionID !== undefined) {
12
+ if (m.session_id === undefined) m.session_id = m.sessionID
13
+ if (m.sessionId === undefined) m.sessionId = m.sessionID
14
+ }
15
+ return m
16
+ }
17
+
18
+ /** V2 content part → V1 Part 形状(scan 的 part() 消费)。 */
19
+ function toV1Part(p: Record<string, any>): Record<string, any> {
20
+ if (p.type === "tool") {
21
+ const st = { ...(p.state ?? {}) }
22
+ if (st.output === undefined && Array.isArray(st.content)) {
23
+ st.output = (st.content as Array<Record<string, any>>)
24
+ .map((c) => c.type === "text" ? c.text : c.type === "file" ? String(c.path ?? c.filename ?? "") : JSON.stringify(c))
25
+ .filter((t): t is string => typeof t === "string" && t.length > 0)
26
+ .join("\n")
27
+ }
28
+ return { type: "tool", tool: p.tool ?? p.name, state: st, subagent_type: p.subagent_type, metadata: normalizeMeta(p.metadata) }
29
+ }
30
+ if (p.type === "text") return { type: "text", text: String(p.text ?? "") }
31
+ if (p.type === "file") return { type: "file", source: p.source ?? {} }
32
+ if (p.type === "reasoning") return { type: "reasoning", text: String(p.text ?? "") }
33
+ return p
34
+ }
35
+
36
+ /**
37
+ * V2 (opencode2) context → PanelApi 适配实现。
38
+ * 数据流(功能对齐 V1):
39
+ * - usage:messages 反向遍历(同 V1 readSessionTokens/Cost/Model 逻辑);todo 无 V2 对应 → undefined
40
+ * - 事件:tool 事件 → V1 ToolPart(subagent 工具名归一化 + metadata 双写 session_id/sessionId);
41
+ * execution.succeeded/failed → session.idle/error
42
+ * - 取消:session.interrupt(V1 abort 的 V2 对应)
43
+ * - 路由:router.navigate({type:"session"})(V1 route.navigate 的 V2 对应)
44
+ */
45
+ export function createPanelApi(context: Context, settings: PanelApi["settings"]): PanelApi {
46
+ const kvStore = new Map<string, [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]>()
47
+ const messageIndex = new Map<string, Record<string, any>>()
48
+ // 工具信息记忆(key: msgID\u0000id):called 事件可能不带 name——input.started 时记录;
49
+ // progress/success/failed 复用(含 input——防后续事件的空 input 覆盖 title/prompt)
50
+ const toolInfo = new Map<string, { name?: string; input?: Record<string, unknown> }>()
51
+
52
+ const kvGet = <T>(key: string, fallback?: T): T | undefined => {
53
+ let entry = kvStore.get(key)
54
+ if (!entry) {
55
+ const created = context.storage.store<Record<string, any>>(`subagent_magazine.${key}`, {
56
+ initial: { value: fallback },
57
+ })
58
+ entry = created as [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]
59
+ kvStore.set(key, entry)
60
+ }
61
+ const v = entry[0].value
62
+ return v === undefined ? fallback : (v as T)
63
+ }
64
+ const kvSet = (key: string, value: unknown): Promise<void> => {
65
+ let entry = kvStore.get(key)
66
+ if (!entry) {
67
+ const created = context.storage.store<Record<string, any>>(`subagent_magazine.${key}`, {
68
+ initial: { value },
69
+ })
70
+ entry = created as [Record<string, any>, (fn: (d: Record<string, any>) => void) => Promise<void>]
71
+ kvStore.set(key, entry)
72
+ }
73
+ const [, mutate] = entry
74
+ return mutate((d) => { d.value = value })
75
+ }
76
+
77
+ const isSubagentTool = (name: string | undefined): boolean =>
78
+ name === SUBAGENT_TOOL_V2 || name === "task" || name === "delegate" || name === "call_omo_agent"
79
+
80
+ /** V2 tool 事件 → V1 ToolPart(非 subagent 工具返回 undefined——面板只关心子代理)。
81
+ * 识别规则(对齐 V2 官方 stream-v2.subagent.ts):
82
+ * - input.started/called:工具名在 subagent 集合(subagent/task/delegate/call_omo_agent)
83
+ * - progress:工具名记忆(input.started/called 记录——无记录忽略,防误识别)
84
+ * - success/failed:metadata.sessionID 存在(subagent 工具注入子会话 ID——question 等
85
+ * 普通工具无此字段——忽略)
86
+ * 归一化:subagent → task(V1 SUBAGENT_TOOLS 集合命中);subagent_type 补 input.agent
87
+ * (V1 逻辑读 subagent_type 显示真实 agent 名)。 */
88
+ const toolEventToPart = (event: Record<string, any>): Record<string, any> | undefined => {
89
+ const type = event.type as string
90
+ const data = event.data as Record<string, any> | undefined
91
+ if (!data) return undefined
92
+ const key = data.assistantMessageID !== undefined ? `${String(data.assistantMessageID)}\u0000${String(data.id)}` : undefined
93
+ const info = key ? toolInfo.get(key) : undefined
94
+ const normName = (n: string) => (n === SUBAGENT_TOOL_V2 ? "task" : n)
95
+ const agentOf = (input: any): string | undefined => {
96
+ const i = (input && typeof input === "object") ? input : {}
97
+ return typeof i.agent === "string" ? i.agent : undefined
98
+ }
99
+ if (type === "session.tool.input.started") {
100
+ if (!isSubagentTool(data.name)) return undefined
101
+ if (key) toolInfo.set(key, { name: String(data.name), input: {} })
102
+ return { type: "tool", tool: normName(String(data.name)), id: String(data.id), state: { status: "pending", input: {} } }
103
+ }
104
+ if (type === "session.tool.called") {
105
+ const name = data.name ?? info?.name
106
+ if (name && !isSubagentTool(name)) return undefined
107
+ if (!name) return undefined
108
+ if (key) toolInfo.set(key, { name: String(name), input: data.input ?? {} })
109
+ return {
110
+ type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(data.input),
111
+ state: { status: "running", input: data.input ?? {}, metadata: {} },
112
+ }
113
+ }
114
+ if (type === "session.tool.progress") {
115
+ const name = data.name ?? info?.name
116
+ if (name && !isSubagentTool(name)) return undefined
117
+ if (!name) return undefined
118
+ return {
119
+ type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(info?.input),
120
+ state: { status: "running", input: info?.input ?? {}, metadata: normalizeMeta(data.metadata) },
121
+ }
122
+ }
123
+ if (type === "session.tool.success" || type === "session.tool.failed") {
124
+ const meta = normalizeMeta(data.metadata)
125
+ if (meta.sessionID === undefined) return undefined
126
+ // 取消导致的工具失败:metadata.status 是工具被打断时的状态("running")——不是真错误。
127
+ // 忽略——最终状态由 execution.interrupted → settleOnIdle 裁定 cancelled
128
+ // (否则 handlePartUpdated 的 error 分支会把 cancel_requested 覆盖成 error)
129
+ if (type === "session.tool.failed" && meta.status === "running") return undefined
130
+ const name = data.name ?? info?.name
131
+ if (name && !isSubagentTool(name)) return undefined
132
+ const input = info?.input ?? data.input ?? {}
133
+ return {
134
+ type: "tool", tool: normName(String(name ?? "task")), id: String(data.id), subagent_type: agentOf(input),
135
+ state: {
136
+ status: type === "session.tool.failed" ? "error" : "completed",
137
+ input,
138
+ metadata: meta,
139
+ },
140
+ }
141
+ }
142
+ return undefined
143
+ }
144
+
145
+ /** 消息归一化(V1 形状:role/tokens/cost/modelID)+ 建立 part 索引。 */
146
+ const normalizeMessages = (sid: string): any[] => {
147
+ const raw = context.data.session.message.list(sid) ?? []
148
+ const out: any[] = []
149
+ for (const m of raw) {
150
+ const record = m as Record<string, any>
151
+ messageIndex.set(String(m.id), record)
152
+ const model = (m.model as { providerID?: string; id?: string } | undefined) ?? (typeof m.model === "string" ? { id: m.model } : undefined)
153
+ out.push({
154
+ ...m,
155
+ role: m.type === "assistant" ? "assistant" : m.type === "user" ? "user" : m.type,
156
+ modelID: model?.id,
157
+ })
158
+ }
159
+ return out
160
+ }
161
+
162
+ return {
163
+ kv: {
164
+ get: kvGet,
165
+ set: kvSet as (key: string, value: unknown) => void,
166
+ },
167
+ usage: {
168
+ readSessionTokens: (sid: string): number | undefined => {
169
+ if (!sid) return undefined
170
+ try {
171
+ const msgs = context.data.session.message.list(sid)
172
+ if (msgs) {
173
+ for (let i = msgs.length - 1; i >= 0; i--) {
174
+ const m = msgs[i] as Record<string, any>
175
+ if (m.type !== "assistant") continue
176
+ const t = m.tokens as Record<string, any> | undefined
177
+ if (!t || !(Number(t.output) > 0)) continue
178
+ const ctx =
179
+ (Number(t.input) || 0) +
180
+ (Number(t.output) || 0) +
181
+ (Number(t.reasoning) || 0) +
182
+ (Number(t.cache?.read) || 0) +
183
+ (Number(t.cache?.write) || 0)
184
+ if (ctx > 0) return ctx
185
+ }
186
+ }
187
+ return undefined
188
+ } catch { return undefined }
189
+ },
190
+ readSessionCost: (sid: string): number | undefined => {
191
+ if (!sid) return undefined
192
+ try {
193
+ const session = context.data.session.get(sid)
194
+ if (session?.cost != null && session.cost > 0) return session.cost
195
+ const msgs = context.data.session.message.list(sid)
196
+ if (!msgs) return undefined
197
+ let total = 0
198
+ for (const m of msgs as any[]) {
199
+ if (m.type === "assistant" && typeof m.cost === "number") total += m.cost
200
+ }
201
+ return total > 0 ? total : undefined
202
+ } catch { return undefined }
203
+ },
204
+ readSessionModel: (sid: string): string | undefined => {
205
+ if (!sid) return undefined
206
+ try {
207
+ const msgs = context.data.session.message.list(sid)
208
+ if (msgs) {
209
+ for (let i = msgs.length - 1; i >= 0; i--) {
210
+ const m = msgs[i] as Record<string, any>
211
+ if (m.type !== "assistant") continue
212
+ const model = m.model as { id?: string } | string | undefined
213
+ if (typeof model === "string") return model
214
+ if (model?.id) return String(model.id)
215
+ }
216
+ }
217
+ return undefined
218
+ } catch { return undefined }
219
+ },
220
+ readSessionTodo: () => undefined, // V2 无 todo 数据源
221
+ },
222
+ session: {
223
+ get: (sid) => { try { return context.data.session.get(sid) as any } catch { return undefined } },
224
+ status: (sid) => {
225
+ try {
226
+ const st = context.data.session.status(sid)
227
+ // V2 的 "running" 等价 V1 的 "busy"(cancelEntry 检查 `st.type !== "busy"`——
228
+ // 不映射会跳过取消流程直接标记 done)
229
+ const type = st === "running" ? "busy" : st ?? ""
230
+ return { type }
231
+ } catch { return undefined }
232
+ },
233
+ messages: (sid) => { try { return normalizeMessages(sid) } catch { return undefined } },
234
+ part: (messageID) => {
235
+ const msg = messageIndex.get(String(messageID))
236
+ if (!msg) return undefined
237
+ if (!Array.isArray(msg.content) || msg.content.length === 0) {
238
+ if (typeof msg.text === "string" && msg.text.length > 0) {
239
+ return [{ type: "text", text: msg.text }]
240
+ }
241
+ return []
242
+ }
243
+ return msg.content.map((p) => toV1Part(p as Record<string, any>))
244
+ },
245
+ },
246
+ event: {
247
+ on: (type: PanelEventType, cb: (e: PanelEvent) => void) => {
248
+ switch (type) {
249
+ case "part.updated": {
250
+ const unsubs: Array<() => void> = []
251
+ for (const evt of ["session.tool.input.started", "session.tool.called", "session.tool.progress", "session.tool.success", "session.tool.failed"]) {
252
+ unsubs.push(context.data.on(evt, (e) => {
253
+ const part = toolEventToPart((e as Record<string, any>))
254
+ if (!part) return
255
+ // V2 事件流是全局的(跨所有会话);把发起会话 ID 一并传给面板,
256
+ // 让面板只归账到正在查看的会话(V1 宿主已按会话 scope,无需该字段)。
257
+ const evt = (e as Record<string, any>).data as Record<string, any> | undefined
258
+ const sid = evt?.sessionID !== undefined ? String(evt.sessionID) : undefined
259
+ cb({ type, payload: { part, sessionID: sid } })
260
+ }))
261
+ }
262
+ return () => { for (const u of unsubs) u() }
263
+ }
264
+ case "message.updated": {
265
+ const unsubs: Array<() => void> = []
266
+ for (const evt of ["session.step.started", "session.step.ended", "session.step.failed"]) {
267
+ unsubs.push(context.data.on(evt, () => cb({ type })))
268
+ }
269
+ return () => { for (const u of unsubs) u() }
270
+ }
271
+ case "session.idle": {
272
+ // V1 session.idle = 会话结束(含取消后的 idle——settleOnIdle 裁定 cancelled)
273
+ // V2:execution.succeeded(正常完成)+ execution.interrupted(被中断——取消)都映射到这里
274
+ const unsubs: Array<() => void> = []
275
+ for (const evt of ["session.execution.succeeded", "session.execution.interrupted"]) {
276
+ unsubs.push(context.data.on(evt, (e) => {
277
+ const sid = String(((e as Record<string, any>).data as Record<string, any> | undefined)?.sessionID ?? "")
278
+ if (sid) cb({ type, payload: { sessionID: sid } })
279
+ }))
280
+ }
281
+ return () => { for (const u of unsubs) u() }
282
+ }
283
+ case "session.error":
284
+ return context.data.on("session.execution.failed", (e) => {
285
+ const evt = (e as Record<string, any>).data as Record<string, any> | undefined
286
+ const sid = String(evt?.sessionID ?? "")
287
+ if (sid) cb({ type, payload: { sessionID: sid, error: evt?.error } })
288
+ })
289
+ default:
290
+ return () => {}
291
+ }
292
+ },
293
+ },
294
+ client: {
295
+ // V2 官方取消途径:context.client.session.interrupt(data.session 无 interrupt)
296
+ abort: ({ sessionID }) => context.client.session.interrupt({ sessionID }).then(() => {}),
297
+ },
298
+ route: {
299
+ navigateSession: (sessionID) => context.ui.router.navigate({ type: "session", sessionID }),
300
+ },
301
+ ui: {
302
+ toast: (message, opts) => context.ui.toast.show({ message, title: opts?.title, variant: opts?.variant }),
303
+ },
304
+ settings,
305
+ }
306
+ }