opencode-subagent-magazine 1.5.2 → 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/LICENSE +21 -21
- package/README.md +192 -192
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/color.d.ts +20 -0
- package/dist/core/color.js +68 -0
- package/dist/core/format.d.ts +5 -0
- package/dist/core/format.js +68 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +6 -0
- package/dist/core/kv.d.ts +20 -0
- package/dist/core/kv.js +30 -0
- package/dist/core/state-machine.d.ts +17 -0
- package/dist/core/state-machine.js +27 -0
- package/dist/core/types.d.ts +51 -0
- package/dist/core/types.js +2 -0
- package/dist/core/usage.d.ts +15 -0
- package/dist/core/usage.js +1 -0
- package/dist/index.js +148 -1484
- package/dist/panel/SubAgentPanel.d.ts +13 -0
- package/dist/panel/SubAgentPanel.js +1237 -0
- package/dist/panel/panel-api.d.ts +67 -0
- package/dist/panel/panel-api.js +1 -0
- package/dist/panel/store.d.ts +5 -0
- package/dist/panel/store.js +5 -0
- package/dist/tui.js +388 -292
- package/dist/v2/commands.d.ts +7 -0
- package/dist/v2/commands.js +263 -0
- package/dist/v2/index.d.ts +8 -0
- package/dist/v2/index.js +62 -0
- package/dist/v2/theme.d.ts +3 -0
- package/dist/v2/theme.js +12 -0
- package/dist/v2/types.d.ts +231 -0
- package/dist/v2/types.js +6 -0
- package/dist/v2/v2-panel-api.d.ts +12 -0
- package/dist/v2/v2-panel-api.js +350 -0
- package/dist/v2.js +3008 -0
- package/install.mjs +103 -103
- package/package.json +64 -63
- package/src/_version.ts +1 -1
- package/src/clipboard.ts +134 -134
- package/src/core/color.ts +70 -0
- package/src/core/format.ts +61 -0
- package/src/core/index.ts +6 -0
- package/src/core/kv.ts +36 -0
- package/src/core/state-machine.ts +46 -0
- package/src/core/types.ts +58 -0
- package/src/core/usage.ts +16 -0
- package/src/i18n.ts +261 -261
- package/src/index.tsx +124 -1750
- package/src/panel/SubAgentPanel.tsx +1465 -0
- package/src/panel/panel-api.ts +72 -0
- package/src/panel/store.ts +8 -0
- package/src/server.ts +10 -10
- package/src/v2/commands.ts +251 -0
- package/src/v2/index.tsx +98 -0
- package/src/v2/theme.ts +14 -0
- package/src/v2/types.ts +165 -0
- package/src/v2/v2-panel-api.ts +306 -0
- package/tui/index.js +11 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
const SUBAGENT_TOOL_V2 = "subagent";
|
|
2
|
+
/** V2 tool metadata → V1 形状(V1 逻辑读 session_id/sessionId——双写保证命中)。 */
|
|
3
|
+
function normalizeMeta(meta) {
|
|
4
|
+
const m = (meta && typeof meta === "object")
|
|
5
|
+
? { ...meta }
|
|
6
|
+
: {};
|
|
7
|
+
if (m.sessionID !== undefined) {
|
|
8
|
+
if (m.session_id === undefined)
|
|
9
|
+
m.session_id = m.sessionID;
|
|
10
|
+
if (m.sessionId === undefined)
|
|
11
|
+
m.sessionId = m.sessionID;
|
|
12
|
+
}
|
|
13
|
+
return m;
|
|
14
|
+
}
|
|
15
|
+
/** V2 content part → V1 Part 形状(scan 的 part() 消费)。 */
|
|
16
|
+
function toV1Part(p) {
|
|
17
|
+
if (p.type === "tool") {
|
|
18
|
+
const st = { ...(p.state ?? {}) };
|
|
19
|
+
if (st.output === undefined && Array.isArray(st.content)) {
|
|
20
|
+
st.output = st.content
|
|
21
|
+
.map((c) => c.type === "text" ? c.text : c.type === "file" ? String(c.path ?? c.filename ?? "") : JSON.stringify(c))
|
|
22
|
+
.filter((t) => typeof t === "string" && t.length > 0)
|
|
23
|
+
.join("\n");
|
|
24
|
+
}
|
|
25
|
+
return { type: "tool", tool: p.tool ?? p.name, state: st, subagent_type: p.subagent_type, metadata: normalizeMeta(p.metadata) };
|
|
26
|
+
}
|
|
27
|
+
if (p.type === "text")
|
|
28
|
+
return { type: "text", text: String(p.text ?? "") };
|
|
29
|
+
if (p.type === "file")
|
|
30
|
+
return { type: "file", source: p.source ?? {} };
|
|
31
|
+
if (p.type === "reasoning")
|
|
32
|
+
return { type: "reasoning", text: String(p.text ?? "") };
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* V2 (opencode2) context → PanelApi 适配实现。
|
|
37
|
+
* 数据流(功能对齐 V1):
|
|
38
|
+
* - usage:messages 反向遍历(同 V1 readSessionTokens/Cost/Model 逻辑);todo 无 V2 对应 → undefined
|
|
39
|
+
* - 事件:tool 事件 → V1 ToolPart(subagent 工具名归一化 + metadata 双写 session_id/sessionId);
|
|
40
|
+
* execution.succeeded/failed → session.idle/error
|
|
41
|
+
* - 取消:session.interrupt(V1 abort 的 V2 对应)
|
|
42
|
+
* - 路由:router.navigate({type:"session"})(V1 route.navigate 的 V2 对应)
|
|
43
|
+
*/
|
|
44
|
+
export function createPanelApi(context, settings) {
|
|
45
|
+
const kvStore = new Map();
|
|
46
|
+
const messageIndex = new Map();
|
|
47
|
+
// 工具信息记忆(key: msgID\u0000id):called 事件可能不带 name——input.started 时记录;
|
|
48
|
+
// progress/success/failed 复用(含 input——防后续事件的空 input 覆盖 title/prompt)
|
|
49
|
+
const toolInfo = new Map();
|
|
50
|
+
const kvGet = (key, fallback) => {
|
|
51
|
+
let entry = kvStore.get(key);
|
|
52
|
+
if (!entry) {
|
|
53
|
+
const created = context.storage.store(`subagent_magazine.${key}`, {
|
|
54
|
+
initial: { value: fallback },
|
|
55
|
+
});
|
|
56
|
+
entry = created;
|
|
57
|
+
kvStore.set(key, entry);
|
|
58
|
+
}
|
|
59
|
+
const v = entry[0].value;
|
|
60
|
+
return v === undefined ? fallback : v;
|
|
61
|
+
};
|
|
62
|
+
const kvSet = (key, value) => {
|
|
63
|
+
let entry = kvStore.get(key);
|
|
64
|
+
if (!entry) {
|
|
65
|
+
const created = context.storage.store(`subagent_magazine.${key}`, {
|
|
66
|
+
initial: { value },
|
|
67
|
+
});
|
|
68
|
+
entry = created;
|
|
69
|
+
kvStore.set(key, entry);
|
|
70
|
+
}
|
|
71
|
+
const [, mutate] = entry;
|
|
72
|
+
return mutate((d) => { d.value = value; });
|
|
73
|
+
};
|
|
74
|
+
const isSubagentTool = (name) => name === SUBAGENT_TOOL_V2 || name === "task" || name === "delegate" || name === "call_omo_agent";
|
|
75
|
+
/** V2 tool 事件 → V1 ToolPart(非 subagent 工具返回 undefined——面板只关心子代理)。
|
|
76
|
+
* 识别规则(对齐 V2 官方 stream-v2.subagent.ts):
|
|
77
|
+
* - input.started/called:工具名在 subagent 集合(subagent/task/delegate/call_omo_agent)
|
|
78
|
+
* - progress:工具名记忆(input.started/called 记录——无记录忽略,防误识别)
|
|
79
|
+
* - success/failed:metadata.sessionID 存在(subagent 工具注入子会话 ID——question 等
|
|
80
|
+
* 普通工具无此字段——忽略)
|
|
81
|
+
* 归一化:subagent → task(V1 SUBAGENT_TOOLS 集合命中);subagent_type 补 input.agent
|
|
82
|
+
* (V1 逻辑读 subagent_type 显示真实 agent 名)。 */
|
|
83
|
+
const toolEventToPart = (event) => {
|
|
84
|
+
const type = event.type;
|
|
85
|
+
const data = event.data;
|
|
86
|
+
if (!data)
|
|
87
|
+
return undefined;
|
|
88
|
+
const key = data.assistantMessageID !== undefined ? `${String(data.assistantMessageID)}\u0000${String(data.id)}` : undefined;
|
|
89
|
+
const info = key ? toolInfo.get(key) : undefined;
|
|
90
|
+
const normName = (n) => (n === SUBAGENT_TOOL_V2 ? "task" : n);
|
|
91
|
+
const agentOf = (input) => {
|
|
92
|
+
const i = (input && typeof input === "object") ? input : {};
|
|
93
|
+
return typeof i.agent === "string" ? i.agent : undefined;
|
|
94
|
+
};
|
|
95
|
+
if (type === "session.tool.input.started") {
|
|
96
|
+
if (!isSubagentTool(data.name))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (key)
|
|
99
|
+
toolInfo.set(key, { name: String(data.name), input: {} });
|
|
100
|
+
return { type: "tool", tool: normName(String(data.name)), id: String(data.id), state: { status: "pending", input: {} } };
|
|
101
|
+
}
|
|
102
|
+
if (type === "session.tool.called") {
|
|
103
|
+
const name = data.name ?? info?.name;
|
|
104
|
+
if (name && !isSubagentTool(name))
|
|
105
|
+
return undefined;
|
|
106
|
+
if (!name)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (key)
|
|
109
|
+
toolInfo.set(key, { name: String(name), input: data.input ?? {} });
|
|
110
|
+
return {
|
|
111
|
+
type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(data.input),
|
|
112
|
+
state: { status: "running", input: data.input ?? {}, metadata: {} },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (type === "session.tool.progress") {
|
|
116
|
+
const name = data.name ?? info?.name;
|
|
117
|
+
if (name && !isSubagentTool(name))
|
|
118
|
+
return undefined;
|
|
119
|
+
if (!name)
|
|
120
|
+
return undefined;
|
|
121
|
+
return {
|
|
122
|
+
type: "tool", tool: normName(String(name)), id: String(data.id), subagent_type: agentOf(info?.input),
|
|
123
|
+
state: { status: "running", input: info?.input ?? {}, metadata: normalizeMeta(data.metadata) },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (type === "session.tool.success" || type === "session.tool.failed") {
|
|
127
|
+
const meta = normalizeMeta(data.metadata);
|
|
128
|
+
if (meta.sessionID === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
// 取消导致的工具失败:metadata.status 是工具被打断时的状态("running")——不是真错误。
|
|
131
|
+
// 忽略——最终状态由 execution.interrupted → settleOnIdle 裁定 cancelled
|
|
132
|
+
// (否则 handlePartUpdated 的 error 分支会把 cancel_requested 覆盖成 error)
|
|
133
|
+
if (type === "session.tool.failed" && meta.status === "running")
|
|
134
|
+
return undefined;
|
|
135
|
+
const name = data.name ?? info?.name;
|
|
136
|
+
if (name && !isSubagentTool(name))
|
|
137
|
+
return undefined;
|
|
138
|
+
const input = info?.input ?? data.input ?? {};
|
|
139
|
+
return {
|
|
140
|
+
type: "tool", tool: normName(String(name ?? "task")), id: String(data.id), subagent_type: agentOf(input),
|
|
141
|
+
state: {
|
|
142
|
+
status: type === "session.tool.failed" ? "error" : "completed",
|
|
143
|
+
input,
|
|
144
|
+
metadata: meta,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
150
|
+
/** 消息归一化(V1 形状:role/tokens/cost/modelID)+ 建立 part 索引。 */
|
|
151
|
+
const normalizeMessages = (sid) => {
|
|
152
|
+
const raw = context.data.session.message.list(sid) ?? [];
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const m of raw) {
|
|
155
|
+
const record = m;
|
|
156
|
+
messageIndex.set(String(m.id), record);
|
|
157
|
+
const model = m.model ?? (typeof m.model === "string" ? { id: m.model } : undefined);
|
|
158
|
+
out.push({
|
|
159
|
+
...m,
|
|
160
|
+
role: m.type === "assistant" ? "assistant" : m.type === "user" ? "user" : m.type,
|
|
161
|
+
modelID: model?.id,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
kv: {
|
|
168
|
+
get: kvGet,
|
|
169
|
+
set: kvSet,
|
|
170
|
+
},
|
|
171
|
+
usage: {
|
|
172
|
+
readSessionTokens: (sid) => {
|
|
173
|
+
if (!sid)
|
|
174
|
+
return undefined;
|
|
175
|
+
try {
|
|
176
|
+
const msgs = context.data.session.message.list(sid);
|
|
177
|
+
if (msgs) {
|
|
178
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
179
|
+
const m = msgs[i];
|
|
180
|
+
if (m.type !== "assistant")
|
|
181
|
+
continue;
|
|
182
|
+
const t = m.tokens;
|
|
183
|
+
if (!t || !(Number(t.output) > 0))
|
|
184
|
+
continue;
|
|
185
|
+
const ctx = (Number(t.input) || 0) +
|
|
186
|
+
(Number(t.output) || 0) +
|
|
187
|
+
(Number(t.reasoning) || 0) +
|
|
188
|
+
(Number(t.cache?.read) || 0) +
|
|
189
|
+
(Number(t.cache?.write) || 0);
|
|
190
|
+
if (ctx > 0)
|
|
191
|
+
return ctx;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
readSessionCost: (sid) => {
|
|
201
|
+
if (!sid)
|
|
202
|
+
return undefined;
|
|
203
|
+
try {
|
|
204
|
+
const session = context.data.session.get(sid);
|
|
205
|
+
if (session?.cost != null && session.cost > 0)
|
|
206
|
+
return session.cost;
|
|
207
|
+
const msgs = context.data.session.message.list(sid);
|
|
208
|
+
if (!msgs)
|
|
209
|
+
return undefined;
|
|
210
|
+
let total = 0;
|
|
211
|
+
for (const m of msgs) {
|
|
212
|
+
if (m.type === "assistant" && typeof m.cost === "number")
|
|
213
|
+
total += m.cost;
|
|
214
|
+
}
|
|
215
|
+
return total > 0 ? total : undefined;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
readSessionModel: (sid) => {
|
|
222
|
+
if (!sid)
|
|
223
|
+
return undefined;
|
|
224
|
+
try {
|
|
225
|
+
const msgs = context.data.session.message.list(sid);
|
|
226
|
+
if (msgs) {
|
|
227
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
228
|
+
const m = msgs[i];
|
|
229
|
+
if (m.type !== "assistant")
|
|
230
|
+
continue;
|
|
231
|
+
const model = m.model;
|
|
232
|
+
if (typeof model === "string")
|
|
233
|
+
return model;
|
|
234
|
+
if (model?.id)
|
|
235
|
+
return String(model.id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
readSessionTodo: () => undefined, // V2 无 todo 数据源
|
|
245
|
+
},
|
|
246
|
+
session: {
|
|
247
|
+
get: (sid) => { try {
|
|
248
|
+
return context.data.session.get(sid);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
} },
|
|
253
|
+
status: (sid) => {
|
|
254
|
+
try {
|
|
255
|
+
const st = context.data.session.status(sid);
|
|
256
|
+
// V2 的 "running" 等价 V1 的 "busy"(cancelEntry 检查 `st.type !== "busy"`——
|
|
257
|
+
// 不映射会跳过取消流程直接标记 done)
|
|
258
|
+
const type = st === "running" ? "busy" : st ?? "";
|
|
259
|
+
return { type };
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
messages: (sid) => { try {
|
|
266
|
+
return normalizeMessages(sid);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
} },
|
|
271
|
+
part: (messageID) => {
|
|
272
|
+
const msg = messageIndex.get(String(messageID));
|
|
273
|
+
if (!msg)
|
|
274
|
+
return undefined;
|
|
275
|
+
if (!Array.isArray(msg.content) || msg.content.length === 0) {
|
|
276
|
+
if (typeof msg.text === "string" && msg.text.length > 0) {
|
|
277
|
+
return [{ type: "text", text: msg.text }];
|
|
278
|
+
}
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
return msg.content.map((p) => toV1Part(p));
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
event: {
|
|
285
|
+
on: (type, cb) => {
|
|
286
|
+
switch (type) {
|
|
287
|
+
case "part.updated": {
|
|
288
|
+
const unsubs = [];
|
|
289
|
+
for (const evt of ["session.tool.input.started", "session.tool.called", "session.tool.progress", "session.tool.success", "session.tool.failed"]) {
|
|
290
|
+
unsubs.push(context.data.on(evt, (e) => {
|
|
291
|
+
const part = toolEventToPart(e);
|
|
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 } });
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
return () => { for (const u of unsubs)
|
|
302
|
+
u(); };
|
|
303
|
+
}
|
|
304
|
+
case "message.updated": {
|
|
305
|
+
const unsubs = [];
|
|
306
|
+
for (const evt of ["session.step.started", "session.step.ended", "session.step.failed"]) {
|
|
307
|
+
unsubs.push(context.data.on(evt, () => cb({ type })));
|
|
308
|
+
}
|
|
309
|
+
return () => { for (const u of unsubs)
|
|
310
|
+
u(); };
|
|
311
|
+
}
|
|
312
|
+
case "session.idle": {
|
|
313
|
+
// V1 的 session.idle = 会话结束(含取消后的 idle——settleOnIdle 裁定 cancelled)
|
|
314
|
+
// V2:execution.succeeded(正常完成)+ execution.interrupted(被中断——取消)都映射到这里
|
|
315
|
+
const unsubs = [];
|
|
316
|
+
for (const evt of ["session.execution.succeeded", "session.execution.interrupted"]) {
|
|
317
|
+
unsubs.push(context.data.on(evt, (e) => {
|
|
318
|
+
const sid = String(e.data?.sessionID ?? "");
|
|
319
|
+
if (sid)
|
|
320
|
+
cb({ type, payload: { sessionID: sid } });
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
return () => { for (const u of unsubs)
|
|
324
|
+
u(); };
|
|
325
|
+
}
|
|
326
|
+
case "session.error":
|
|
327
|
+
return context.data.on("session.execution.failed", (e) => {
|
|
328
|
+
const evt = e.data;
|
|
329
|
+
const sid = String(evt?.sessionID ?? "");
|
|
330
|
+
if (sid)
|
|
331
|
+
cb({ type, payload: { sessionID: sid, error: evt?.error } });
|
|
332
|
+
});
|
|
333
|
+
default:
|
|
334
|
+
return () => { };
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
client: {
|
|
339
|
+
// V2 官方取消途径:context.client.session.interrupt(data.session 无 interrupt)
|
|
340
|
+
abort: ({ sessionID }) => context.client.session.interrupt({ sessionID }).then(() => { }),
|
|
341
|
+
},
|
|
342
|
+
route: {
|
|
343
|
+
navigateSession: (sessionID) => context.ui.router.navigate({ type: "session", sessionID }),
|
|
344
|
+
},
|
|
345
|
+
ui: {
|
|
346
|
+
toast: (message, opts) => context.ui.toast.show({ message, title: opts?.title, variant: opts?.variant }),
|
|
347
|
+
},
|
|
348
|
+
settings,
|
|
349
|
+
};
|
|
350
|
+
}
|