opencode-subagent-magazine 1.1.0 → 1.1.2

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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Hotakus
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hotakus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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.0";
1
+ export declare const PLUGIN_VERSION = "1.1.2";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.1.0";
2
+ export const PLUGIN_VERSION = "1.1.2";
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,6 +198,9 @@ function safeErrorMsg(err) {
188
198
  // ===================================================================
189
199
  // Sidebar component
190
200
  // ===================================================================
201
+ // 模块级缓存:保留各 session 的最新 entryMap,不随组件内 session 切换而丢失。
202
+ // 当用户在子 session 内部时,handleSessionEnd 可通过此缓存找到父 session 的 entries。
203
+ const globalEntryCache = new Map();
191
204
  function SubAgentPanel(props) {
192
205
  const t = (key) => I18N[props.lang()][key] ?? key;
193
206
  // ── session data (single-key, true deletion on cleanup) ──
@@ -270,7 +283,30 @@ function SubAgentPanel(props) {
270
283
  const setEntryMap = (arg) => {
271
284
  setEntryMapRaw((prev) => {
272
285
  const next = typeof arg === "function" ? arg(prev) : arg;
273
- persistEntries(props.sessionId, next);
286
+ // 检测是否有 entry 从 running 变为 done/error → 立即写 KV
287
+ // 避免 session 切换时 KV 仍是旧状态,导致 scan 用 Date.now() 覆盖正确的 endedAt
288
+ let needsImmediateFlush = false;
289
+ for (const [id, entry] of next) {
290
+ const prevEntry = prev.get(id);
291
+ if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error")) {
292
+ needsImmediateFlush = true;
293
+ break;
294
+ }
295
+ }
296
+ if (needsImmediateFlush) {
297
+ clearTimeout(persistTimer);
298
+ try {
299
+ const data = loadSessionData();
300
+ data[props.sessionId] = { ...data[props.sessionId], ts: Date.now(), entries: [...next.values()] };
301
+ saveSessionData(data);
302
+ }
303
+ catch { }
304
+ }
305
+ else {
306
+ persistEntries(props.sessionId, next);
307
+ }
308
+ // 同步到全局缓存,确保跨 session 查找时数据可用
309
+ globalEntryCache.set(props.sessionId, new Map(next));
274
310
  return next;
275
311
  });
276
312
  };
@@ -288,6 +324,7 @@ function SubAgentPanel(props) {
288
324
  return undefined;
289
325
  } })());
290
326
  const [hoveredOpen, setHoveredOpen] = createSignal(undefined);
327
+ const [hoveredDismiss, setHoveredDismiss] = createSignal(undefined);
291
328
  const [hoveredTop, setHoveredTop] = createSignal(false);
292
329
  const [scrollOffset, setScrollOffset] = createSignal((() => { try {
293
330
  return loadSessionData()[props.sessionId]?.scroll ?? 0;
@@ -511,6 +548,66 @@ function SubAgentPanel(props) {
511
548
  }
512
549
  }
513
550
  catch { }
551
+ // 在给定的 entries Map 中查找并更新匹配的子代理 entry。
552
+ // 返回 true 表示找到并更新了,false 表示未找到。
553
+ const tryMatchAndUpdate = (entriesMap, targetSid, targetStatus, nowTs) => {
554
+ // 精确匹配:sessionId 对得上 + 状态为 running
555
+ for (const [, entry] of entriesMap) {
556
+ if (entry.sessionId === targetSid && entry.status === "running") {
557
+ entry.status = targetStatus;
558
+ entry.endedAt = nowTs;
559
+ entry.tokens = entry.tokens ?? sessionTokens;
560
+ entry.cost = entry.cost ?? sessionCost;
561
+ entry.model = entry.model ?? sessionModel;
562
+ entry.todoTotal = entry.todoTotal ?? sessionTodo?.total;
563
+ entry.todoDone = entry.todoDone ?? sessionTodo?.done;
564
+ entry.error = errorMsg || entry.error;
565
+ return true;
566
+ }
567
+ }
568
+ // 回退:sessionId 未关联但 agent 名匹配 + 状态为 running
569
+ if (sessionAgent) {
570
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
571
+ const saNorm = normalize(sessionAgent);
572
+ let best = null;
573
+ for (const [, entry] of entriesMap) {
574
+ if (entry.status !== "running")
575
+ continue;
576
+ const eaNorm = normalize(entry.agent);
577
+ if (!eaNorm || !saNorm)
578
+ continue;
579
+ if (!eaNorm.includes(saNorm) && !saNorm.includes(eaNorm))
580
+ continue;
581
+ const gap = nowTs - (entry.startedAt || 0);
582
+ if (!best || gap > best.gap)
583
+ best = { entry, gap };
584
+ }
585
+ if (!best) {
586
+ for (const [, entry] of entriesMap) {
587
+ if (entry.status !== "running")
588
+ continue;
589
+ if (entry.sessionId)
590
+ continue;
591
+ const gap = nowTs - (entry.startedAt || 0);
592
+ if (!best || gap > best.gap)
593
+ best = { entry, gap };
594
+ }
595
+ }
596
+ if (best) {
597
+ best.entry.status = targetStatus;
598
+ best.entry.endedAt = nowTs;
599
+ best.entry.tokens = best.entry.tokens ?? sessionTokens;
600
+ best.entry.cost = best.entry.cost ?? sessionCost;
601
+ best.entry.model = best.entry.model ?? sessionModel;
602
+ best.entry.todoTotal = best.entry.todoTotal ?? sessionTodo?.total;
603
+ best.entry.todoDone = best.entry.todoDone ?? sessionTodo?.done;
604
+ best.entry.sessionId = targetSid;
605
+ best.entry.error = errorMsg || best.entry.error;
606
+ return true;
607
+ }
608
+ }
609
+ return false;
610
+ };
514
611
  setEntryMap((prev) => {
515
612
  let changed = false;
516
613
  const next = new Map(prev);
@@ -581,6 +678,44 @@ function SubAgentPanel(props) {
581
678
  }
582
679
  return changed ? next : prev;
583
680
  });
681
+ // 跨 session 完成事件:用户正在查看子 session 内部时,其他子代理 done。
682
+ // 上面的 setEntryMap 在子 session 的 entries 中找不到父 session 的 entry。
683
+ // 通过全局缓存(切换前保留的父 session entries)查找并更新,再同步回 KV。
684
+ try {
685
+ const sessionObj = props.api.state.session.get(sid);
686
+ const parentSid = sessionObj?.parentID;
687
+ if (parentSid && parentSid !== props.sessionId) {
688
+ // 优先全局缓存——切换 session 时不会被替换,始终保留父 session 的最新 entries
689
+ const parentCache = globalEntryCache.get(parentSid);
690
+ const nowTs = Date.now();
691
+ let found = false;
692
+ if (parentCache) {
693
+ found = tryMatchAndUpdate(parentCache, sid, status, nowTs);
694
+ }
695
+ // 缓存未命中时回退到 KV(极端情况:session 切换前缓存未建立)
696
+ if (!found) {
697
+ const data = loadSessionData();
698
+ const rec = data[parentSid];
699
+ if (rec?.entries) {
700
+ const fallbackMap = new Map(rec.entries.map((e) => [e.id, e]));
701
+ found = tryMatchAndUpdate(fallbackMap, sid, status, nowTs);
702
+ if (found) {
703
+ // 回退命中了也要写回 KV,并回填缓存
704
+ data[parentSid] = { ...rec, ts: nowTs, entries: [...fallbackMap.values()] };
705
+ saveSessionData(data);
706
+ globalEntryCache.set(parentSid, fallbackMap);
707
+ }
708
+ }
709
+ }
710
+ // KV 同步:以全局缓存(最新的内存状态)为准写回 KV
711
+ if (found && parentCache) {
712
+ const data = loadSessionData();
713
+ data[parentSid] = { ...data[parentSid], ts: nowTs, entries: [...parentCache.values()] };
714
+ saveSessionData(data);
715
+ }
716
+ }
717
+ }
718
+ catch { }
584
719
  // Delayed backfill: re-read data after state sync catches up, to capture the final
585
720
  // token/cost values that may not have been available when session.idle fired.
586
721
  setTimeout(() => {
@@ -703,7 +838,10 @@ function SubAgentPanel(props) {
703
838
  // scan uses setEntryMapRaw — ephemeral data, not persisted to kv.
704
839
  // Only event-driven changes (handlePartUpdated, handleSessionEnd) persist.
705
840
  setEntryMapRaw((prev) => {
706
- const next = switched ? loadEntries(sid) : new Map(prev);
841
+ // 优先全局缓存——handleSessionEnd 修复2 已同步更新,无 KV 读写延迟
842
+ const next = switched
843
+ ? new Map(globalEntryCache.get(sid) ?? loadEntries(sid))
844
+ : new Map(prev);
707
845
  try {
708
846
  const msgs = props.api.state.session.messages(sid);
709
847
  if (msgs && msgs.length) {
@@ -900,7 +1038,7 @@ function SubAgentPanel(props) {
900
1038
  // ── expanded detail right-align ──
901
1039
  const expandedMaxLabelW = createMemo(() => {
902
1040
  const labels = [
903
- t("agent.label"), t("time.label"), t("tokens.label"),
1041
+ t("agent.label"), t("status.label"), t("time.label"), t("tokens.label"),
904
1042
  t("error.label"), t("cost.label"), t("model.label"), t("todo.label"),
905
1043
  ];
906
1044
  return Math.max(...labels.map(l => visualWidth(l + ": ")));
@@ -1028,11 +1166,20 @@ function SubAgentPanel(props) {
1028
1166
  const pad = Math.max(0, max - visualWidth(truncated));
1029
1167
  return truncated + " ".repeat(pad);
1030
1168
  };
1031
- 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: () => {
1032
- if (entry.sessionId) {
1033
- props.api.route.navigate("session", { sessionID: entry.sessionId });
1034
- }
1035
- }, 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") })] }) })] })] }));
1169
+ 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: (() => {
1170
+ const dismissLabel = () => `- ${t("dismiss.label")}`;
1171
+ const openPrefix = () => " \u2192 ";
1172
+ const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : "";
1173
+ const openW = () => entry.sessionId ? visualWidth(openFull()) : 0;
1174
+ const spacerW = () => Math.max(1, panelWidth() - openW() - visualWidth(dismissLabel()) - 2 /* indent */);
1175
+ 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: () => {
1176
+ if (entry.sessionId) {
1177
+ props.api.route.navigate("session", { sessionID: entry.sessionId });
1178
+ }
1179
+ }, 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: () => {
1180
+ upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" });
1181
+ }, children: _jsx("span", { style: { fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }, children: dismissLabel() }) })] }) })] }));
1182
+ })() })] })] }));
1036
1183
  } }), _jsx(Show, { when: hiddenBelow() > 0 || scrollOffset() > 0, children: (() => {
1037
1184
  const showMore = hiddenBelow() > 0;
1038
1185
  const showTop = scrollOffset() > 0;
@@ -1119,6 +1266,54 @@ const tui = async (api) => {
1119
1266
  dialog?.clear();
1120
1267
  },
1121
1268
  },
1269
+ {
1270
+ title: "SubAgent Magazine: Clear Running",
1271
+ value: "subagent-clear-running",
1272
+ description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
1273
+ slash: { name: "subagent-clear-running" },
1274
+ onSelect: (dialog) => {
1275
+ const entries = globalEntryCache.get(signals.sessionId);
1276
+ if (!entries || entries.size === 0) {
1277
+ const msg = signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found";
1278
+ api.ui.toast({ message: msg });
1279
+ dialog?.clear();
1280
+ return;
1281
+ }
1282
+ let count = 0;
1283
+ for (const [, entry] of entries) {
1284
+ if (entry.status === "running") {
1285
+ entry.status = "done";
1286
+ entry.endedAt = Date.now();
1287
+ count++;
1288
+ }
1289
+ }
1290
+ if (count > 0) {
1291
+ // 立即写 KV
1292
+ try {
1293
+ const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")));
1294
+ data[signals.sessionId] = {
1295
+ ts: Date.now(),
1296
+ entries: [...entries.values()],
1297
+ scroll: data[signals.sessionId]?.scroll ?? 0,
1298
+ expanded: data[signals.sessionId]?.expanded ?? "",
1299
+ };
1300
+ api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data));
1301
+ }
1302
+ catch { }
1303
+ const msg = signals.lang() === "zh"
1304
+ ? `已标记 ${count} 个运行中的条目为完成`
1305
+ : `Marked ${count} running entries as done`;
1306
+ api.ui.toast({ message: msg });
1307
+ }
1308
+ else {
1309
+ const msg = signals.lang() === "zh"
1310
+ ? "没有需要清理的运行中条目"
1311
+ : "No running entries to clear";
1312
+ api.ui.toast({ message: msg });
1313
+ }
1314
+ dialog?.clear();
1315
+ },
1316
+ },
1122
1317
  ]);
1123
1318
  };
1124
1319
  const mod = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-subagent-magazine",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,6 +39,10 @@
39
39
  "subagent",
40
40
  "subtask"
41
41
  ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/Hotakus/opencode-subagent-magazine.git"
45
+ },
42
46
  "license": "MIT",
43
47
  "peerDependencies": {
44
48
  "@opencode-ai/plugin": ">=1.14.0",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.1.0";
2
+ export const PLUGIN_VERSION="1.1.2";