opencode-subagent-magazine 1.1.1 → 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/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.2";
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.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() {
@@ -314,6 +324,7 @@ function SubAgentPanel(props) {
314
324
  return undefined;
315
325
  } })());
316
326
  const [hoveredOpen, setHoveredOpen] = createSignal(undefined);
327
+ const [hoveredDismiss, setHoveredDismiss] = createSignal(undefined);
317
328
  const [hoveredTop, setHoveredTop] = createSignal(false);
318
329
  const [scrollOffset, setScrollOffset] = createSignal((() => { try {
319
330
  return loadSessionData()[props.sessionId]?.scroll ?? 0;
@@ -827,7 +838,10 @@ function SubAgentPanel(props) {
827
838
  // scan uses setEntryMapRaw — ephemeral data, not persisted to kv.
828
839
  // Only event-driven changes (handlePartUpdated, handleSessionEnd) persist.
829
840
  setEntryMapRaw((prev) => {
830
- 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);
831
845
  try {
832
846
  const msgs = props.api.state.session.messages(sid);
833
847
  if (msgs && msgs.length) {
@@ -1024,7 +1038,7 @@ function SubAgentPanel(props) {
1024
1038
  // ── expanded detail right-align ──
1025
1039
  const expandedMaxLabelW = createMemo(() => {
1026
1040
  const labels = [
1027
- t("agent.label"), t("time.label"), t("tokens.label"),
1041
+ t("agent.label"), t("status.label"), t("time.label"), t("tokens.label"),
1028
1042
  t("error.label"), t("cost.label"), t("model.label"), t("todo.label"),
1029
1043
  ];
1030
1044
  return Math.max(...labels.map(l => visualWidth(l + ": ")));
@@ -1152,11 +1166,20 @@ function SubAgentPanel(props) {
1152
1166
  const pad = Math.max(0, max - visualWidth(truncated));
1153
1167
  return truncated + " ".repeat(pad);
1154
1168
  };
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") })] }) })] })] }));
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
+ })() })] })] }));
1160
1183
  } }), _jsx(Show, { when: hiddenBelow() > 0 || scrollOffset() > 0, children: (() => {
1161
1184
  const showMore = hiddenBelow() > 0;
1162
1185
  const showTop = scrollOffset() > 0;
@@ -1243,6 +1266,54 @@ const tui = async (api) => {
1243
1266
  dialog?.clear();
1244
1267
  },
1245
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
+ },
1246
1317
  ]);
1247
1318
  };
1248
1319
  const mod = {
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.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",
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.2";
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
 
@@ -374,6 +384,7 @@ function SubAgentPanel(props: {
374
384
  (() => { try { return loadSessionData()[props.sessionId]?.expanded || undefined } catch { return undefined } })()
375
385
  )
376
386
  const [hoveredOpen, setHoveredOpen] = createSignal<string | undefined>(undefined)
387
+ const [hoveredDismiss, setHoveredDismiss] = createSignal<string | undefined>(undefined)
377
388
  const [hoveredTop, setHoveredTop] = createSignal(false)
378
389
  const [scrollOffset, setScrollOffset] = createSignal(
379
390
  (() => { try { return loadSessionData()[props.sessionId]?.scroll ?? 0 } catch { return 0 } })()
@@ -867,7 +878,10 @@ function SubAgentPanel(props: {
867
878
  // scan uses setEntryMapRaw — ephemeral data, not persisted to kv.
868
879
  // Only event-driven changes (handlePartUpdated, handleSessionEnd) persist.
869
880
  setEntryMapRaw((prev) => {
870
- const next = switched ? loadEntries(sid) : new Map(prev)
881
+ // 优先全局缓存——handleSessionEnd 修复2 已同步更新,无 KV 读写延迟
882
+ const next = switched
883
+ ? new Map(globalEntryCache.get(sid) ?? loadEntries(sid))
884
+ : new Map(prev)
871
885
  try {
872
886
  const msgs = props.api.state.session.messages(sid)
873
887
  if (msgs && (msgs as any[]).length) {
@@ -1060,7 +1074,7 @@ function SubAgentPanel(props: {
1060
1074
  // ── expanded detail right-align ──
1061
1075
  const expandedMaxLabelW = createMemo(() => {
1062
1076
  const labels = [
1063
- t("agent.label"), t("time.label"), t("tokens.label"),
1077
+ t("agent.label"), t("status.label"), t("time.label"), t("tokens.label"),
1064
1078
  t("error.label"), t("cost.label"), t("model.label"), t("todo.label"),
1065
1079
  ]
1066
1080
  return Math.max(...labels.map(l => visualWidth(l + ": ")))
@@ -1275,6 +1289,14 @@ function SubAgentPanel(props: {
1275
1289
  <span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("agent.label")))}</span>
1276
1290
  <span style={{ fg: pal().muted }}>{entry.agent}</span>
1277
1291
  </text>
1292
+ <text>
1293
+ {" "}
1294
+ <span style={{ fg: pal().primary }}>{t("status.label")}: </span>
1295
+ <span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("status.label")))}</span>
1296
+ <span style={{ fg: isRunning ? pal().warning : isError ? pal().error : pal().success }}>
1297
+ {isRunning ? t("status.running") : isError ? t("status.error") : t("status.done")}
1298
+ </span>
1299
+ </text>
1278
1300
  <Show when={elapsed() >= 2000 || entry.endedAt !== undefined}>
1279
1301
  <text>
1280
1302
  {" "}
@@ -1325,20 +1347,49 @@ function SubAgentPanel(props: {
1325
1347
  <span style={{ fg: pal().muted }}>{entry.todoDone}/{entry.todoTotal}</span>
1326
1348
  </text>
1327
1349
  </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>
1350
+ {/* 进入会话 + 标记完成:同排左右两端,空间隔离防误触 */}
1351
+ <Show when={entry.sessionId || isRunning}>
1352
+ {(() => {
1353
+ const dismissLabel = () => `- ${t("dismiss.label")}`
1354
+ const openPrefix = () => " \u2192 "
1355
+ const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : ""
1356
+ const openW = () => entry.sessionId ? visualWidth(openFull()) : 0
1357
+ const spacerW = () => Math.max(1, panelWidth() - openW() - visualWidth(dismissLabel()) - 2 /* indent */)
1358
+ return (
1359
+ <box flexDirection="row">
1360
+ <Show when={entry.sessionId}
1361
+ fallback={<text>{" "}</text>}
1362
+ >
1363
+ <text
1364
+ onMouseOver={() => setHoveredOpen(entry.id)}
1365
+ onMouseOut={() => setHoveredOpen(undefined)}
1366
+ onMouseUp={() => {
1367
+ if (entry.sessionId) {
1368
+ props.api.route.navigate("session", { sessionID: entry.sessionId })
1369
+ }
1370
+ }}
1371
+ >
1372
+ <span style={{ fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }}>{openPrefix()}</span>
1373
+ <span style={{ fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }}>{t("open.label")}</span>
1374
+ </text>
1375
+ </Show>
1376
+ <Show when={isRunning}>
1377
+ <>
1378
+ <text style={{ fg: pal().muted }}>{" ".repeat(spacerW())}</text>
1379
+ <text
1380
+ onMouseOver={() => setHoveredDismiss(entry.id)}
1381
+ onMouseOut={() => setHoveredDismiss(undefined)}
1382
+ onMouseUp={() => {
1383
+ upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" })
1384
+ }}
1385
+ >
1386
+ <span style={{ fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }}>{dismissLabel()}</span>
1387
+ </text>
1388
+ </>
1389
+ </Show>
1390
+ </box>
1391
+ )
1392
+ })()}
1342
1393
  </Show>
1343
1394
  </Show>
1344
1395
  </>
@@ -1498,6 +1549,52 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1498
1549
  dialog?.clear()
1499
1550
  },
1500
1551
  },
1552
+ {
1553
+ title: "SubAgent Magazine: Clear Running",
1554
+ value: "subagent-clear-running",
1555
+ description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
1556
+ slash: { name: "subagent-clear-running" },
1557
+ onSelect: (dialog) => {
1558
+ const entries = globalEntryCache.get(signals.sessionId)
1559
+ if (!entries || entries.size === 0) {
1560
+ const msg = signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found"
1561
+ api.ui.toast({ message: msg })
1562
+ dialog?.clear()
1563
+ return
1564
+ }
1565
+ let count = 0
1566
+ for (const [, entry] of entries) {
1567
+ if (entry.status === "running") {
1568
+ entry.status = "done" as SubStatus
1569
+ entry.endedAt = Date.now()
1570
+ count++
1571
+ }
1572
+ }
1573
+ if (count > 0) {
1574
+ // 立即写 KV
1575
+ try {
1576
+ const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")))
1577
+ data[signals.sessionId] = {
1578
+ ts: Date.now(),
1579
+ entries: [...entries.values()],
1580
+ scroll: data[signals.sessionId]?.scroll ?? 0,
1581
+ expanded: data[signals.sessionId]?.expanded ?? "",
1582
+ }
1583
+ api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
1584
+ } catch {}
1585
+ const msg = signals.lang() === "zh"
1586
+ ? `已标记 ${count} 个运行中的条目为完成`
1587
+ : `Marked ${count} running entries as done`
1588
+ api.ui.toast({ message: msg })
1589
+ } else {
1590
+ const msg = signals.lang() === "zh"
1591
+ ? "没有需要清理的运行中条目"
1592
+ : "No running entries to clear"
1593
+ api.ui.toast({ message: msg })
1594
+ }
1595
+ dialog?.clear()
1596
+ },
1597
+ },
1501
1598
  ])
1502
1599
  }
1503
1600