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 +21 -21
- package/README.md +8 -3
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +203 -8
- package/package.json +5 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +1605 -1379
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
|
|
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
|
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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.
|
|
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.
|
|
2
|
+
export const PLUGIN_VERSION="1.1.2";
|