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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-subagent-magazine",
3
- "version": "1.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": "./src/index.tsx",
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.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
- // 模块级缓存:保留各 session 的最新 entryMap,不随组件内 session 切换而丢失。
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
- // 检测是否有 entry 从 running 变为 done/error 立即写 KV
341
- // 避免 session 切换时 KV 仍是旧状态,导致 scan 用 Date.now() 覆盖正确的 endedAt
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
- // 同步到全局缓存,确保跨 session 查找时数据可用
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
- // session 完成事件:用户正在查看子 session 内部时,其他子代理 done。
716
- // 上面的 setEntryMap 在子 session 的 entries 中找不到父 session 的 entry
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
- // 优先全局缓存——切换 session 时不会被替换,始终保留父 session 的最新 entries
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(极端情况:session 切换前缓存未建立)
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
- // 回退命中了也要写回 KV,并回填缓存
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
- // KV 同步:以全局缓存(最新的内存状态)为准写回 KV
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
- const next = switched ? loadEntries(sid) : new Map(prev)
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
- <Show when={entry.sessionId}>
1329
- <text
1330
- onMouseOver={() => setHoveredOpen(entry.id)}
1331
- onMouseOut={() => setHoveredOpen(undefined)}
1332
- onMouseUp={() => {
1333
- if (entry.sessionId) {
1334
- props.api.route.navigate("session", { sessionID: entry.sessionId })
1335
- }
1336
- }}
1337
- >
1338
- {" "}
1339
- <span style={{ fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }}>{"\u2192 "}</span>
1340
- <span style={{ fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }}>{t("open.label")}</span>
1341
- </text>
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