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 +8 -3
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +88 -19
- package/dist/tui.js +1859 -0
- package/package.json +6 -3
- package/src/_version.ts +1 -1
- package/src/index.tsx +123 -28
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.3";
|
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.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
|
-
//
|
|
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
|
-
//
|
|
277
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
671
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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 = {
|