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.
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/types.js +1 -1
- package/dist/panel/SubAgentPanel.js +7 -2
- package/dist/tui.js +6 -4
- package/dist/v2/v2-panel-api.js +7 -2
- package/dist/v2.js +10 -5
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/core/types.ts +58 -58
- package/src/panel/SubAgentPanel.tsx +7 -2
- package/src/v2/v2-panel-api.ts +306 -301
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.6.0-beta.
|
|
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.
|
|
2
|
+
export const PLUGIN_VERSION = "1.6.0-beta.1";
|
package/dist/core/types.js
CHANGED
|
@@ -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.
|
|
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);
|
package/dist/v2/v2-panel-api.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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.
|
|
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
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.6.0-beta.
|
|
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)
|
package/src/v2/v2-panel-api.ts
CHANGED
|
@@ -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)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}
|