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 CHANGED
@@ -1,8 +1,9 @@
1
1
  <div align="center">
2
2
  <strong>
3
3
  <h1>OpenCode SubAgent Magazine</h1>
4
+ 像弹匣一样装填!发射!<br><br>
4
5
  实时子代理监控 · TUI 侧边栏可视化<br>
5
- 自适应主题色 · 中/英双语 · 数据持久化
6
+ 自适应主题色 · 低饱和设计语言 · 中/英双语 · 数据持久化
6
7
  </strong>
7
8
  <br>
8
9
  <br>
@@ -39,13 +40,15 @@
39
40
 
40
41
  - **实时状态**:运行中(呼吸动画)、已完成、错误,彩色圆点一目了然
41
42
  - **Token & 费用追踪**:每个子代理的 Token 消耗和费用汇总,标题栏全局显示
42
- - **展开详情**:点击展开查看 agent 类型、耗时、Token、费用、模型、进度,字段值右对齐
43
+ - **展开详情**:点击展开查看 agent 类型、状态、耗时、Token、费用、模型、进度,字段值右对齐
43
44
  - **一键进入会话**:展开后点 `→ 进入会话` 跳转子代理的完整对话
45
+ - **手动标记完成**:展开区右侧 `- 标记完成` 按钮可手动终结卡住的僵尸条目;`/subagent-clear-running` 批量清理
46
+ - **状态字段显示**:展开区 `状态: 运行中/已完成/错误`,颜色与圆点一致,一目了然
44
47
  - **折叠面板**:标题栏点击折叠/展开,状态持久化,重启后保持
45
48
  - **滚动回顶**:列表底部 `↑ 回顶` 按钮一键回到顶部
46
49
  - **TTL 自动清理**:超过 3 天的旧数据自动从 KV 中清除
47
50
  - **语言适配**:支持 `/subagent-lang` 运行时切换中/英文,偏好持久化
48
- - **斜杠命令**:`/subagent-lang` `/subagent-max` `/subagent-session` `/subagent-version` 动态配置
51
+ - **斜杠命令**:`/subagent-lang` `/subagent-max` `/subagent-session` `/subagent-version` `/subagent-clear-running` 动态配置
49
52
 
50
53
  ---
51
54
 
@@ -98,6 +101,7 @@ npm install -g opencode-subagent-magazine@latest
98
101
  | `/subagent-max` | 调整最大可见条目数 | 输入数字(默认 10),控制面板最多显示多少个子代理条目 |
99
102
  | `/subagent-session` | 查看当前会话 ID | 弹出当前 OpenCode 会话 ID |
100
103
  | `/subagent-version` | 查看插件版本 | 弹出当前插件版本号 |
104
+ | `/subagent-clear-running` | 批量清理僵尸条目 | 一键将所有运行中的条目标记为完成,清理卡住的旧数据 |
101
105
 
102
106
  <div align="center">
103
107
  <img src="https://raw.githubusercontent.com/Hotakus/opencode-subagent-magazine/master/assets/slash_cmds.png" alt="斜杠命令" width="49%"></img>
@@ -113,6 +117,7 @@ npm install -g opencode-subagent-magazine@latest
113
117
  | 滚动列表 | 翻页查看更多子代理 |
114
118
  | 点击 `↑ 回顶` | 跳转到列表顶部 |
115
119
  | 点击 `→ 进入会话` | 跳转子代理完整对话 |
120
+ | 点击 `- 标记完成` | 手动终结该条目(仅运行中时显示) |
116
121
 
117
122
  ### 4.3 状态颜色
118
123
 
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.1.1";
1
+ export declare const PLUGIN_VERSION = "1.1.3";
package/dist/_version.js 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/dist/index.js CHANGED
@@ -11,6 +11,7 @@ const I18N = {
11
11
  "panel.title": "子代理",
12
12
  "status.none": "暂无子代理",
13
13
  "agent.label": "代理",
14
+ "status.label": "状态",
14
15
  "time.label": "耗时",
15
16
  "tokens.label": "上下文",
16
17
  "error.label": "错误",
@@ -20,11 +21,16 @@ const I18N = {
20
21
  "cost.label": "费用",
21
22
  "scroll.more": "更多",
22
23
  "scroll.top": "回顶",
24
+ "dismiss.label": "标记完成",
25
+ "status.running": "运行中",
26
+ "status.done": "已完成",
27
+ "status.error": "错误",
23
28
  },
24
29
  en: {
25
30
  "panel.title": "SubAgent",
26
31
  "status.none": "No sub-agents yet",
27
32
  "agent.label": "agent",
33
+ "status.label": "status",
28
34
  "time.label": "time",
29
35
  "tokens.label": "tokens",
30
36
  "error.label": "error",
@@ -34,6 +40,10 @@ const I18N = {
34
40
  "cost.label": "cost",
35
41
  "scroll.more": "more",
36
42
  "scroll.top": "Top",
43
+ "dismiss.label": "dismiss",
44
+ "status.running": "running",
45
+ "status.done": "done",
46
+ "status.error": "error",
37
47
  },
38
48
  };
39
49
  function detectLang() {
@@ -188,8 +198,7 @@ function safeErrorMsg(err) {
188
198
  // ===================================================================
189
199
  // Sidebar component
190
200
  // ===================================================================
191
- // 模块级缓存:保留各 session 的最新 entryMap,不随组件内 session 切换而丢失。
192
- // 当用户在子 session 内部时,handleSessionEnd 可通过此缓存找到父 session 的 entries。
201
+ // 模块级缓存:各 session entry 状态独立存储,不随当前视图切换而清除。
193
202
  const globalEntryCache = new Map();
194
203
  function SubAgentPanel(props) {
195
204
  const t = (key) => I18N[props.lang()][key] ?? key;
@@ -273,8 +282,8 @@ function SubAgentPanel(props) {
273
282
  const setEntryMap = (arg) => {
274
283
  setEntryMapRaw((prev) => {
275
284
  const next = typeof arg === "function" ? arg(prev) : arg;
276
- // 检测是否有 entry 从 running 变为 done/error 立即写 KV
277
- // 避免 session 切换时 KV 仍是旧状态,导致 scan 用 Date.now() 覆盖正确的 endedAt
285
+ // entry 状态落定(done/error)时立即持久化到 KV,跳过常规 debounce,
286
+ // 确保跨视图的状态一致性。
278
287
  let needsImmediateFlush = false;
279
288
  for (const [id, entry] of next) {
280
289
  const prevEntry = prev.get(id);
@@ -295,7 +304,7 @@ function SubAgentPanel(props) {
295
304
  else {
296
305
  persistEntries(props.sessionId, next);
297
306
  }
298
- // 同步到全局缓存,确保跨 session 查找时数据可用
307
+ // 同步到模块级缓存,供其他视图读取当前 session 的最新状态
299
308
  globalEntryCache.set(props.sessionId, new Map(next));
300
309
  return next;
301
310
  });
@@ -314,6 +323,7 @@ function SubAgentPanel(props) {
314
323
  return undefined;
315
324
  } })());
316
325
  const [hoveredOpen, setHoveredOpen] = createSignal(undefined);
326
+ const [hoveredDismiss, setHoveredDismiss] = createSignal(undefined);
317
327
  const [hoveredTop, setHoveredTop] = createSignal(false);
318
328
  const [scrollOffset, setScrollOffset] = createSignal((() => { try {
319
329
  return loadSessionData()[props.sessionId]?.scroll ?? 0;
@@ -667,21 +677,20 @@ function SubAgentPanel(props) {
667
677
  }
668
678
  return changed ? next : prev;
669
679
  });
670
- // session 完成事件:用户正在查看子 session 内部时,其他子代理 done。
671
- // 上面的 setEntryMap 在子 session 的 entries 中找不到父 session 的 entry
672
- // 通过全局缓存(切换前保留的父 session entries)查找并更新,再同步回 KV。
680
+ // 当子代理所属的父 session 与当前视图不同时,通过模块级缓存定位
681
+ // 并更新父 session 的 entry 状态,随后写回 KV
673
682
  try {
674
683
  const sessionObj = props.api.state.session.get(sid);
675
684
  const parentSid = sessionObj?.parentID;
676
685
  if (parentSid && parentSid !== props.sessionId) {
677
- // 优先全局缓存——切换 session 时不会被替换,始终保留父 session 的最新 entries
686
+ // 优先从模块级缓存获取父 session entries,不受当前视图切换影响
678
687
  const parentCache = globalEntryCache.get(parentSid);
679
688
  const nowTs = Date.now();
680
689
  let found = false;
681
690
  if (parentCache) {
682
691
  found = tryMatchAndUpdate(parentCache, sid, status, nowTs);
683
692
  }
684
- // 缓存未命中时回退到 KV(极端情况:session 切换前缓存未建立)
693
+ // 缓存未命中时回退到 KV 读取
685
694
  if (!found) {
686
695
  const data = loadSessionData();
687
696
  const rec = data[parentSid];
@@ -689,14 +698,14 @@ function SubAgentPanel(props) {
689
698
  const fallbackMap = new Map(rec.entries.map((e) => [e.id, e]));
690
699
  found = tryMatchAndUpdate(fallbackMap, sid, status, nowTs);
691
700
  if (found) {
692
- // 回退命中了也要写回 KV,并回填缓存
701
+ // 回退命中后写入 KV 并回填缓存
693
702
  data[parentSid] = { ...rec, ts: nowTs, entries: [...fallbackMap.values()] };
694
703
  saveSessionData(data);
695
704
  globalEntryCache.set(parentSid, fallbackMap);
696
705
  }
697
706
  }
698
707
  }
699
- // KV 同步:以全局缓存(最新的内存状态)为准写回 KV
708
+ // 将模块级缓存中的最新状态同步到 KV
700
709
  if (found && parentCache) {
701
710
  const data = loadSessionData();
702
711
  data[parentSid] = { ...data[parentSid], ts: nowTs, entries: [...parentCache.values()] };
@@ -827,7 +836,10 @@ function SubAgentPanel(props) {
827
836
  // scan uses setEntryMapRaw — ephemeral data, not persisted to kv.
828
837
  // Only event-driven changes (handlePartUpdated, handleSessionEnd) persist.
829
838
  setEntryMapRaw((prev) => {
830
- const next = switched ? loadEntries(sid) : new Map(prev);
839
+ // 优先从模块级缓存加载,KV 仅作缓存未命中时的回退
840
+ const next = switched
841
+ ? new Map(globalEntryCache.get(sid) ?? loadEntries(sid))
842
+ : new Map(prev);
831
843
  try {
832
844
  const msgs = props.api.state.session.messages(sid);
833
845
  if (msgs && msgs.length) {
@@ -1024,7 +1036,7 @@ function SubAgentPanel(props) {
1024
1036
  // ── expanded detail right-align ──
1025
1037
  const expandedMaxLabelW = createMemo(() => {
1026
1038
  const labels = [
1027
- t("agent.label"), t("time.label"), t("tokens.label"),
1039
+ t("agent.label"), t("status.label"), t("time.label"), t("tokens.label"),
1028
1040
  t("error.label"), t("cost.label"), t("model.label"), t("todo.label"),
1029
1041
  ];
1030
1042
  return Math.max(...labels.map(l => visualWidth(l + ": ")));
@@ -1152,11 +1164,20 @@ function SubAgentPanel(props) {
1152
1164
  const pad = Math.max(0, max - visualWidth(truncated));
1153
1165
  return truncated + " ".repeat(pad);
1154
1166
  };
1155
- return (_jsxs(_Fragment, { children: [_jsxs("text", { onMouseUp: () => toggleExpand(entry.id), children: [_jsx("span", { style: { fg: pal().muted }, children: isExpanded() ? "\u25bc" : "\u25b6" }), " ", _jsx("span", { style: { fg: statusColor() }, children: statusDot() }), " ", _jsx("span", { style: { fg: pal().text }, children: labelText() }), timeText() ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { fg: timeColor() }, children: timeText() })] })) : null, tokenText() ? (_jsx("span", { style: { fg: pal().muted }, children: tokenText() })) : null] }), _jsxs(Show, { when: isExpanded(), children: [_jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("agent.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("agent.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: entry.agent })] }), _jsx(Show, { when: elapsed() >= 2000 || entry.endedAt !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("time.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("time.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: fmtDurationShort(elapsed(), isRunning) })] }) }), _jsx(Show, { when: entry.tokens !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("tokens.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("tokens.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: fmtTokens(entry.tokens) })] }) }), _jsx(Show, { when: entry.error, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().error }, children: [t("error.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("error.label"))) }), _jsx("span", { style: { fg: pal().error }, children: truncate(String(entry.error), expandedValAvail()) })] }) }), _jsx(Show, { when: entry.cost !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("cost.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("cost.label"))) }), _jsxs("span", { style: { fg: pal().muted }, children: ["$", entry.cost.toFixed(4)] })] }) }), _jsx(Show, { when: entry.model, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("model.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("model.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: truncate(entry.model, expandedValAvail()) })] }) }), _jsx(Show, { when: entry.todoTotal !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("todo.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("todo.label"))) }), _jsxs("span", { style: { fg: pal().muted }, children: [entry.todoDone, "/", entry.todoTotal] })] }) }), _jsx(Show, { when: entry.sessionId, children: _jsxs("text", { onMouseOver: () => setHoveredOpen(entry.id), onMouseOut: () => setHoveredOpen(undefined), onMouseUp: () => {
1156
- if (entry.sessionId) {
1157
- props.api.route.navigate("session", { sessionID: entry.sessionId });
1158
- }
1159
- }, children: [" ", _jsx("span", { style: { fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }, children: "\u2192 " }), _jsx("span", { style: { fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }, children: t("open.label") })] }) })] })] }));
1167
+ return (_jsxs(_Fragment, { children: [_jsxs("text", { onMouseUp: () => toggleExpand(entry.id), children: [_jsx("span", { style: { fg: pal().muted }, children: isExpanded() ? "\u25bc" : "\u25b6" }), " ", _jsx("span", { style: { fg: statusColor() }, children: statusDot() }), " ", _jsx("span", { style: { fg: pal().text }, children: labelText() }), timeText() ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { fg: timeColor() }, children: timeText() })] })) : null, tokenText() ? (_jsx("span", { style: { fg: pal().muted }, children: tokenText() })) : null] }), _jsxs(Show, { when: isExpanded(), children: [_jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("agent.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("agent.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: entry.agent })] }), _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("status.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("status.label"))) }), _jsx("span", { style: { fg: isRunning ? pal().warning : isError ? pal().error : pal().success }, children: isRunning ? t("status.running") : isError ? t("status.error") : t("status.done") })] }), _jsx(Show, { when: elapsed() >= 2000 || entry.endedAt !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("time.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("time.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: fmtDurationShort(elapsed(), isRunning) })] }) }), _jsx(Show, { when: entry.tokens !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("tokens.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("tokens.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: fmtTokens(entry.tokens) })] }) }), _jsx(Show, { when: entry.error, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().error }, children: [t("error.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("error.label"))) }), _jsx("span", { style: { fg: pal().error }, children: truncate(String(entry.error), expandedValAvail()) })] }) }), _jsx(Show, { when: entry.cost !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("cost.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("cost.label"))) }), _jsxs("span", { style: { fg: pal().muted }, children: ["$", entry.cost.toFixed(4)] })] }) }), _jsx(Show, { when: entry.model, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("model.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("model.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: truncate(entry.model, expandedValAvail()) })] }) }), _jsx(Show, { when: entry.todoTotal !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("todo.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("todo.label"))) }), _jsxs("span", { style: { fg: pal().muted }, children: [entry.todoDone, "/", entry.todoTotal] })] }) }), _jsx(Show, { when: entry.sessionId || isRunning, children: (() => {
1168
+ const dismissLabel = () => `- ${t("dismiss.label")}`;
1169
+ const openPrefix = () => " \u2192 ";
1170
+ const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : "";
1171
+ const openW = () => entry.sessionId ? visualWidth(openFull()) : 0;
1172
+ const spacerW = () => Math.max(1, panelWidth() - openW() - visualWidth(dismissLabel()) - 2 /* indent */);
1173
+ return (_jsxs("box", { flexDirection: "row", children: [_jsx(Show, { when: entry.sessionId, fallback: _jsx("text", { children: " " }), children: _jsxs("text", { onMouseOver: () => setHoveredOpen(entry.id), onMouseOut: () => setHoveredOpen(undefined), onMouseUp: () => {
1174
+ if (entry.sessionId) {
1175
+ props.api.route.navigate("session", { sessionID: entry.sessionId });
1176
+ }
1177
+ }, children: [_jsx("span", { style: { fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }, children: openPrefix() }), _jsx("span", { style: { fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }, children: t("open.label") })] }) }), _jsx(Show, { when: isRunning, children: _jsxs(_Fragment, { children: [_jsx("text", { style: { fg: pal().muted }, children: " ".repeat(spacerW()) }), _jsx("text", { onMouseOver: () => setHoveredDismiss(entry.id), onMouseOut: () => setHoveredDismiss(undefined), onMouseUp: () => {
1178
+ upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" });
1179
+ }, children: _jsx("span", { style: { fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }, children: dismissLabel() }) })] }) })] }));
1180
+ })() })] })] }));
1160
1181
  } }), _jsx(Show, { when: hiddenBelow() > 0 || scrollOffset() > 0, children: (() => {
1161
1182
  const showMore = hiddenBelow() > 0;
1162
1183
  const showTop = scrollOffset() > 0;
@@ -1243,6 +1264,54 @@ const tui = async (api) => {
1243
1264
  dialog?.clear();
1244
1265
  },
1245
1266
  },
1267
+ {
1268
+ title: "SubAgent Magazine: Clear Running",
1269
+ value: "subagent-clear-running",
1270
+ description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
1271
+ slash: { name: "subagent-clear-running" },
1272
+ onSelect: (dialog) => {
1273
+ const entries = globalEntryCache.get(signals.sessionId);
1274
+ if (!entries || entries.size === 0) {
1275
+ const msg = signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found";
1276
+ api.ui.toast({ message: msg });
1277
+ dialog?.clear();
1278
+ return;
1279
+ }
1280
+ let count = 0;
1281
+ for (const [, entry] of entries) {
1282
+ if (entry.status === "running") {
1283
+ entry.status = "done";
1284
+ entry.endedAt = Date.now();
1285
+ count++;
1286
+ }
1287
+ }
1288
+ if (count > 0) {
1289
+ // 立即写 KV
1290
+ try {
1291
+ const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")));
1292
+ data[signals.sessionId] = {
1293
+ ts: Date.now(),
1294
+ entries: [...entries.values()],
1295
+ scroll: data[signals.sessionId]?.scroll ?? 0,
1296
+ expanded: data[signals.sessionId]?.expanded ?? "",
1297
+ };
1298
+ api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data));
1299
+ }
1300
+ catch { }
1301
+ const msg = signals.lang() === "zh"
1302
+ ? `已标记 ${count} 个运行中的条目为完成`
1303
+ : `Marked ${count} running entries as done`;
1304
+ api.ui.toast({ message: msg });
1305
+ }
1306
+ else {
1307
+ const msg = signals.lang() === "zh"
1308
+ ? "没有需要清理的运行中条目"
1309
+ : "No running entries to clear";
1310
+ api.ui.toast({ message: msg });
1311
+ }
1312
+ dialog?.clear();
1313
+ },
1314
+ },
1246
1315
  ]);
1247
1316
  };
1248
1317
  const mod = {