opencode-subagent-magazine 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +88 -19
- package/dist/tui.js +1859 -0
- package/package.json +6 -3
- package/src/_version.ts +1 -1
- package/src/index.tsx +123 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-subagent-magazine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
},
|
|
13
13
|
"./tui": {
|
|
14
|
-
"import": "./
|
|
14
|
+
"import": "./dist/tui.js",
|
|
15
15
|
"config": {
|
|
16
16
|
"enabled": true
|
|
17
17
|
}
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"opencode-subagent-magazine": "install.mjs"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"build": "tsc",
|
|
30
|
+
"build": "tsc && node build.tui.mjs",
|
|
31
|
+
"build:tui": "node build.tui.mjs",
|
|
31
32
|
"typecheck": "tsc --noEmit",
|
|
32
33
|
"version": "node -e \"require('fs').writeFileSync('src/_version.ts','// auto-generated\\nexport const PLUGIN_VERSION='+JSON.stringify(require('./package.json').version)+';\\n')\"",
|
|
33
34
|
"prepublishOnly": "tsc"
|
|
@@ -54,6 +55,8 @@
|
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@opencode-ai/plugin": "^1.14.50",
|
|
56
57
|
"@opencode-ai/sdk": "^1.14.50",
|
|
58
|
+
"esbuild": "^0.25.0",
|
|
59
|
+
"esbuild-plugin-solid": "^0.5.0",
|
|
57
60
|
"tsx": "^4.22.3",
|
|
58
61
|
"typescript": "^5.8.0"
|
|
59
62
|
}
|
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.1.
|
|
2
|
+
export const PLUGIN_VERSION="1.1.3";
|
package/src/index.tsx
CHANGED
|
@@ -58,6 +58,7 @@ const I18N: Record<Lang, Record<string, string>> = {
|
|
|
58
58
|
"panel.title": "子代理",
|
|
59
59
|
"status.none": "暂无子代理",
|
|
60
60
|
"agent.label": "代理",
|
|
61
|
+
"status.label": "状态",
|
|
61
62
|
"time.label": "耗时",
|
|
62
63
|
"tokens.label": "上下文",
|
|
63
64
|
"error.label": "错误",
|
|
@@ -67,11 +68,16 @@ const I18N: Record<Lang, Record<string, string>> = {
|
|
|
67
68
|
"cost.label": "费用",
|
|
68
69
|
"scroll.more": "更多",
|
|
69
70
|
"scroll.top": "回顶",
|
|
71
|
+
"dismiss.label": "标记完成",
|
|
72
|
+
"status.running": "运行中",
|
|
73
|
+
"status.done": "已完成",
|
|
74
|
+
"status.error": "错误",
|
|
70
75
|
},
|
|
71
76
|
en: {
|
|
72
77
|
"panel.title": "SubAgent",
|
|
73
78
|
"status.none": "No sub-agents yet",
|
|
74
79
|
"agent.label": "agent",
|
|
80
|
+
"status.label": "status",
|
|
75
81
|
"time.label": "time",
|
|
76
82
|
"tokens.label": "tokens",
|
|
77
83
|
"error.label": "error",
|
|
@@ -81,6 +87,10 @@ const I18N: Record<Lang, Record<string, string>> = {
|
|
|
81
87
|
"cost.label": "cost",
|
|
82
88
|
"scroll.more": "more",
|
|
83
89
|
"scroll.top": "Top",
|
|
90
|
+
"dismiss.label": "dismiss",
|
|
91
|
+
"status.running": "running",
|
|
92
|
+
"status.done": "done",
|
|
93
|
+
"status.error": "error",
|
|
84
94
|
},
|
|
85
95
|
}
|
|
86
96
|
|
|
@@ -237,8 +247,7 @@ function safeErrorMsg(err: unknown): string {
|
|
|
237
247
|
// Sidebar component
|
|
238
248
|
// ===================================================================
|
|
239
249
|
|
|
240
|
-
//
|
|
241
|
-
// 当用户在子 session 内部时,handleSessionEnd 可通过此缓存找到父 session 的 entries。
|
|
250
|
+
// 模块级缓存:各 session 的 entry 状态独立存储,不随当前视图切换而清除。
|
|
242
251
|
const globalEntryCache = new Map<string, Map<string, SubEntry>>()
|
|
243
252
|
|
|
244
253
|
function SubAgentPanel(props: {
|
|
@@ -337,8 +346,8 @@ function SubAgentPanel(props: {
|
|
|
337
346
|
setEntryMapRaw((prev) => {
|
|
338
347
|
const next = typeof arg === "function" ? (arg as Function)(prev) : arg
|
|
339
348
|
|
|
340
|
-
//
|
|
341
|
-
//
|
|
349
|
+
// entry 状态落定(done/error)时立即持久化到 KV,跳过常规 debounce,
|
|
350
|
+
// 确保跨视图的状态一致性。
|
|
342
351
|
let needsImmediateFlush = false
|
|
343
352
|
for (const [id, entry] of next) {
|
|
344
353
|
const prevEntry = prev.get(id)
|
|
@@ -359,7 +368,7 @@ function SubAgentPanel(props: {
|
|
|
359
368
|
persistEntries(props.sessionId, next)
|
|
360
369
|
}
|
|
361
370
|
|
|
362
|
-
//
|
|
371
|
+
// 同步到模块级缓存,供其他视图读取当前 session 的最新状态
|
|
363
372
|
globalEntryCache.set(props.sessionId, new Map(next))
|
|
364
373
|
|
|
365
374
|
return next
|
|
@@ -374,6 +383,7 @@ function SubAgentPanel(props: {
|
|
|
374
383
|
(() => { try { return loadSessionData()[props.sessionId]?.expanded || undefined } catch { return undefined } })()
|
|
375
384
|
)
|
|
376
385
|
const [hoveredOpen, setHoveredOpen] = createSignal<string | undefined>(undefined)
|
|
386
|
+
const [hoveredDismiss, setHoveredDismiss] = createSignal<string | undefined>(undefined)
|
|
377
387
|
const [hoveredTop, setHoveredTop] = createSignal(false)
|
|
378
388
|
const [scrollOffset, setScrollOffset] = createSignal(
|
|
379
389
|
(() => { try { return loadSessionData()[props.sessionId]?.scroll ?? 0 } catch { return 0 } })()
|
|
@@ -712,14 +722,13 @@ function SubAgentPanel(props: {
|
|
|
712
722
|
return changed ? next : prev
|
|
713
723
|
})
|
|
714
724
|
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
// 通过全局缓存(切换前保留的父 session entries)查找并更新,再同步回 KV。
|
|
725
|
+
// 当子代理所属的父 session 与当前视图不同时,通过模块级缓存定位
|
|
726
|
+
// 并更新父 session 的 entry 状态,随后写回 KV。
|
|
718
727
|
try {
|
|
719
728
|
const sessionObj = props.api.state.session.get(sid)
|
|
720
729
|
const parentSid = sessionObj?.parentID
|
|
721
730
|
if (parentSid && parentSid !== props.sessionId) {
|
|
722
|
-
//
|
|
731
|
+
// 优先从模块级缓存获取父 session 的 entries,不受当前视图切换影响
|
|
723
732
|
const parentCache = globalEntryCache.get(parentSid)
|
|
724
733
|
const nowTs = Date.now()
|
|
725
734
|
let found = false
|
|
@@ -728,7 +737,7 @@ function SubAgentPanel(props: {
|
|
|
728
737
|
found = tryMatchAndUpdate(parentCache, sid, status, nowTs)
|
|
729
738
|
}
|
|
730
739
|
|
|
731
|
-
// 缓存未命中时回退到 KV
|
|
740
|
+
// 缓存未命中时回退到 KV 读取
|
|
732
741
|
if (!found) {
|
|
733
742
|
const data = loadSessionData()
|
|
734
743
|
const rec = data[parentSid]
|
|
@@ -736,7 +745,7 @@ function SubAgentPanel(props: {
|
|
|
736
745
|
const fallbackMap = new Map(rec.entries.map((e: SubEntry) => [e.id, e]))
|
|
737
746
|
found = tryMatchAndUpdate(fallbackMap, sid, status, nowTs)
|
|
738
747
|
if (found) {
|
|
739
|
-
//
|
|
748
|
+
// 回退命中后写入 KV 并回填缓存
|
|
740
749
|
data[parentSid] = { ...rec, ts: nowTs, entries: [...fallbackMap.values()] }
|
|
741
750
|
saveSessionData(data)
|
|
742
751
|
globalEntryCache.set(parentSid, fallbackMap)
|
|
@@ -744,7 +753,7 @@ function SubAgentPanel(props: {
|
|
|
744
753
|
}
|
|
745
754
|
}
|
|
746
755
|
|
|
747
|
-
//
|
|
756
|
+
// 将模块级缓存中的最新状态同步到 KV
|
|
748
757
|
if (found && parentCache) {
|
|
749
758
|
const data = loadSessionData()
|
|
750
759
|
data[parentSid] = { ...data[parentSid], ts: nowTs, entries: [...parentCache.values()] }
|
|
@@ -867,7 +876,10 @@ function SubAgentPanel(props: {
|
|
|
867
876
|
// scan uses setEntryMapRaw — ephemeral data, not persisted to kv.
|
|
868
877
|
// Only event-driven changes (handlePartUpdated, handleSessionEnd) persist.
|
|
869
878
|
setEntryMapRaw((prev) => {
|
|
870
|
-
|
|
879
|
+
// 优先从模块级缓存加载,KV 仅作缓存未命中时的回退
|
|
880
|
+
const next = switched
|
|
881
|
+
? new Map(globalEntryCache.get(sid) ?? loadEntries(sid))
|
|
882
|
+
: new Map(prev)
|
|
871
883
|
try {
|
|
872
884
|
const msgs = props.api.state.session.messages(sid)
|
|
873
885
|
if (msgs && (msgs as any[]).length) {
|
|
@@ -1060,7 +1072,7 @@ function SubAgentPanel(props: {
|
|
|
1060
1072
|
// ── expanded detail right-align ──
|
|
1061
1073
|
const expandedMaxLabelW = createMemo(() => {
|
|
1062
1074
|
const labels = [
|
|
1063
|
-
t("agent.label"), t("time.label"), t("tokens.label"),
|
|
1075
|
+
t("agent.label"), t("status.label"), t("time.label"), t("tokens.label"),
|
|
1064
1076
|
t("error.label"), t("cost.label"), t("model.label"), t("todo.label"),
|
|
1065
1077
|
]
|
|
1066
1078
|
return Math.max(...labels.map(l => visualWidth(l + ": ")))
|
|
@@ -1275,6 +1287,14 @@ function SubAgentPanel(props: {
|
|
|
1275
1287
|
<span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("agent.label")))}</span>
|
|
1276
1288
|
<span style={{ fg: pal().muted }}>{entry.agent}</span>
|
|
1277
1289
|
</text>
|
|
1290
|
+
<text>
|
|
1291
|
+
{" "}
|
|
1292
|
+
<span style={{ fg: pal().primary }}>{t("status.label")}: </span>
|
|
1293
|
+
<span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("status.label")))}</span>
|
|
1294
|
+
<span style={{ fg: isRunning ? pal().warning : isError ? pal().error : pal().success }}>
|
|
1295
|
+
{isRunning ? t("status.running") : isError ? t("status.error") : t("status.done")}
|
|
1296
|
+
</span>
|
|
1297
|
+
</text>
|
|
1278
1298
|
<Show when={elapsed() >= 2000 || entry.endedAt !== undefined}>
|
|
1279
1299
|
<text>
|
|
1280
1300
|
{" "}
|
|
@@ -1325,20 +1345,49 @@ function SubAgentPanel(props: {
|
|
|
1325
1345
|
<span style={{ fg: pal().muted }}>{entry.todoDone}/{entry.todoTotal}</span>
|
|
1326
1346
|
</text>
|
|
1327
1347
|
</Show>
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1348
|
+
{/* 进入会话 + 标记完成:同排左右两端,空间隔离防误触 */}
|
|
1349
|
+
<Show when={entry.sessionId || isRunning}>
|
|
1350
|
+
{(() => {
|
|
1351
|
+
const dismissLabel = () => `- ${t("dismiss.label")}`
|
|
1352
|
+
const openPrefix = () => " \u2192 "
|
|
1353
|
+
const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : ""
|
|
1354
|
+
const openW = () => entry.sessionId ? visualWidth(openFull()) : 0
|
|
1355
|
+
const spacerW = () => Math.max(1, panelWidth() - openW() - visualWidth(dismissLabel()) - 2 /* indent */)
|
|
1356
|
+
return (
|
|
1357
|
+
<box flexDirection="row">
|
|
1358
|
+
<Show when={entry.sessionId}
|
|
1359
|
+
fallback={<text>{" "}</text>}
|
|
1360
|
+
>
|
|
1361
|
+
<text
|
|
1362
|
+
onMouseOver={() => setHoveredOpen(entry.id)}
|
|
1363
|
+
onMouseOut={() => setHoveredOpen(undefined)}
|
|
1364
|
+
onMouseUp={() => {
|
|
1365
|
+
if (entry.sessionId) {
|
|
1366
|
+
props.api.route.navigate("session", { sessionID: entry.sessionId })
|
|
1367
|
+
}
|
|
1368
|
+
}}
|
|
1369
|
+
>
|
|
1370
|
+
<span style={{ fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }}>{openPrefix()}</span>
|
|
1371
|
+
<span style={{ fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }}>{t("open.label")}</span>
|
|
1372
|
+
</text>
|
|
1373
|
+
</Show>
|
|
1374
|
+
<Show when={isRunning}>
|
|
1375
|
+
<>
|
|
1376
|
+
<text style={{ fg: pal().muted }}>{" ".repeat(spacerW())}</text>
|
|
1377
|
+
<text
|
|
1378
|
+
onMouseOver={() => setHoveredDismiss(entry.id)}
|
|
1379
|
+
onMouseOut={() => setHoveredDismiss(undefined)}
|
|
1380
|
+
onMouseUp={() => {
|
|
1381
|
+
upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" })
|
|
1382
|
+
}}
|
|
1383
|
+
>
|
|
1384
|
+
<span style={{ fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }}>{dismissLabel()}</span>
|
|
1385
|
+
</text>
|
|
1386
|
+
</>
|
|
1387
|
+
</Show>
|
|
1388
|
+
</box>
|
|
1389
|
+
)
|
|
1390
|
+
})()}
|
|
1342
1391
|
</Show>
|
|
1343
1392
|
</Show>
|
|
1344
1393
|
</>
|
|
@@ -1498,6 +1547,52 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1498
1547
|
dialog?.clear()
|
|
1499
1548
|
},
|
|
1500
1549
|
},
|
|
1550
|
+
{
|
|
1551
|
+
title: "SubAgent Magazine: Clear Running",
|
|
1552
|
+
value: "subagent-clear-running",
|
|
1553
|
+
description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
|
|
1554
|
+
slash: { name: "subagent-clear-running" },
|
|
1555
|
+
onSelect: (dialog) => {
|
|
1556
|
+
const entries = globalEntryCache.get(signals.sessionId)
|
|
1557
|
+
if (!entries || entries.size === 0) {
|
|
1558
|
+
const msg = signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found"
|
|
1559
|
+
api.ui.toast({ message: msg })
|
|
1560
|
+
dialog?.clear()
|
|
1561
|
+
return
|
|
1562
|
+
}
|
|
1563
|
+
let count = 0
|
|
1564
|
+
for (const [, entry] of entries) {
|
|
1565
|
+
if (entry.status === "running") {
|
|
1566
|
+
entry.status = "done" as SubStatus
|
|
1567
|
+
entry.endedAt = Date.now()
|
|
1568
|
+
count++
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (count > 0) {
|
|
1572
|
+
// 立即写 KV
|
|
1573
|
+
try {
|
|
1574
|
+
const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")))
|
|
1575
|
+
data[signals.sessionId] = {
|
|
1576
|
+
ts: Date.now(),
|
|
1577
|
+
entries: [...entries.values()],
|
|
1578
|
+
scroll: data[signals.sessionId]?.scroll ?? 0,
|
|
1579
|
+
expanded: data[signals.sessionId]?.expanded ?? "",
|
|
1580
|
+
}
|
|
1581
|
+
api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
|
|
1582
|
+
} catch {}
|
|
1583
|
+
const msg = signals.lang() === "zh"
|
|
1584
|
+
? `已标记 ${count} 个运行中的条目为完成`
|
|
1585
|
+
: `Marked ${count} running entries as done`
|
|
1586
|
+
api.ui.toast({ message: msg })
|
|
1587
|
+
} else {
|
|
1588
|
+
const msg = signals.lang() === "zh"
|
|
1589
|
+
? "没有需要清理的运行中条目"
|
|
1590
|
+
: "No running entries to clear"
|
|
1591
|
+
api.ui.toast({ message: msg })
|
|
1592
|
+
}
|
|
1593
|
+
dialog?.clear()
|
|
1594
|
+
},
|
|
1595
|
+
},
|
|
1501
1596
|
])
|
|
1502
1597
|
}
|
|
1503
1598
|
|