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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +192 -192
  3. package/dist/_version.d.ts +1 -1
  4. package/dist/_version.js +1 -1
  5. package/dist/core/color.d.ts +20 -0
  6. package/dist/core/color.js +68 -0
  7. package/dist/core/format.d.ts +5 -0
  8. package/dist/core/format.js +68 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.js +6 -0
  11. package/dist/core/kv.d.ts +20 -0
  12. package/dist/core/kv.js +30 -0
  13. package/dist/core/state-machine.d.ts +17 -0
  14. package/dist/core/state-machine.js +27 -0
  15. package/dist/core/types.d.ts +51 -0
  16. package/dist/core/types.js +2 -0
  17. package/dist/core/usage.d.ts +15 -0
  18. package/dist/core/usage.js +1 -0
  19. package/dist/index.js +148 -1484
  20. package/dist/panel/SubAgentPanel.d.ts +13 -0
  21. package/dist/panel/SubAgentPanel.js +1232 -0
  22. package/dist/panel/panel-api.d.ts +67 -0
  23. package/dist/panel/panel-api.js +1 -0
  24. package/dist/panel/store.d.ts +5 -0
  25. package/dist/panel/store.js +5 -0
  26. package/dist/tui.js +383 -289
  27. package/dist/v2/commands.d.ts +7 -0
  28. package/dist/v2/commands.js +263 -0
  29. package/dist/v2/index.d.ts +8 -0
  30. package/dist/v2/index.js +62 -0
  31. package/dist/v2/theme.d.ts +3 -0
  32. package/dist/v2/theme.js +12 -0
  33. package/dist/v2/types.d.ts +231 -0
  34. package/dist/v2/types.js +6 -0
  35. package/dist/v2/v2-panel-api.d.ts +12 -0
  36. package/dist/v2/v2-panel-api.js +345 -0
  37. package/dist/v2.js +3003 -0
  38. package/install.mjs +103 -103
  39. package/package.json +64 -63
  40. package/src/_version.ts +1 -1
  41. package/src/clipboard.ts +134 -134
  42. package/src/core/color.ts +70 -0
  43. package/src/core/format.ts +61 -0
  44. package/src/core/index.ts +6 -0
  45. package/src/core/kv.ts +36 -0
  46. package/src/core/state-machine.ts +46 -0
  47. package/src/core/types.ts +58 -0
  48. package/src/core/usage.ts +16 -0
  49. package/src/i18n.ts +261 -261
  50. package/src/index.tsx +124 -1750
  51. package/src/panel/SubAgentPanel.tsx +1460 -0
  52. package/src/panel/panel-api.ts +72 -0
  53. package/src/panel/store.ts +8 -0
  54. package/src/server.ts +10 -10
  55. package/src/v2/commands.ts +251 -0
  56. package/src/v2/index.tsx +98 -0
  57. package/src/v2/theme.ts +14 -0
  58. package/src/v2/types.ts +165 -0
  59. package/src/v2/v2-panel-api.ts +301 -0
  60. package/tui/index.js +11 -0
@@ -0,0 +1,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) 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
+ }
package/tui/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // tui/index.js——V1/V2 共用入口(双格式)
2
+ // - V1(opencode):exports["./tui"] → 本文件——readV1Plugin 读 tui 字段(V1 面板)
3
+ // - V2(opencode2):exports["./tui"] → 本文件——isPlugin 读 setup 字段(V2 面板)
4
+ import tuiMod from "./../dist/tui.js"
5
+ import v2Mod from "./../dist/v2.js"
6
+
7
+ export default {
8
+ id: "opencode-subagent-magazine",
9
+ tui: tuiMod.tui,
10
+ setup: v2Mod.setup,
11
+ }