opencode-subagent-magazine 1.4.2 → 1.5.1
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/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/clipboard.d.ts +7 -0
- package/dist/clipboard.js +85 -0
- package/dist/i18n.d.ts +75 -0
- package/dist/i18n.js +249 -0
- package/dist/index.js +176 -138
- package/dist/tui.js +564 -172
- package/package.json +2 -1
- package/src/_version.ts +1 -1
- package/src/clipboard.ts +134 -0
- package/src/i18n.ts +261 -0
- package/src/index.tsx +202 -158
package/dist/index.js
CHANGED
|
@@ -1,98 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/solid/jsx-runtime";
|
|
2
2
|
import { createMemo, createSignal, createEffect, onMount, onCleanup, untrack, Show, For, } from "solid-js";
|
|
3
3
|
import { PLUGIN_VERSION } from "./_version";
|
|
4
|
+
import { copyText } from "./clipboard";
|
|
5
|
+
import { LANG_META, createT, detectLang } from "./i18n";
|
|
4
6
|
/** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
|
|
5
7
|
const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"]);
|
|
6
8
|
// ===================================================================
|
|
7
|
-
// i18n
|
|
8
|
-
// ===================================================================
|
|
9
|
-
const I18N = {
|
|
10
|
-
zh: {
|
|
11
|
-
"panel.title": "子代理",
|
|
12
|
-
"status.none": "暂无子代理",
|
|
13
|
-
"agent.label": "代理",
|
|
14
|
-
"status.label": "状态",
|
|
15
|
-
"time.label": "耗时",
|
|
16
|
-
"tokens.label": "上下文",
|
|
17
|
-
"error.label": "错误",
|
|
18
|
-
"model.label": "模型",
|
|
19
|
-
"todo.label": "进度",
|
|
20
|
-
"session.label": "会话 ID",
|
|
21
|
-
"session.toast.copy": "可手动复制上方 ID",
|
|
22
|
-
"open.label": "进入会话",
|
|
23
|
-
"cost.label": "费用",
|
|
24
|
-
"scroll.more": "更多",
|
|
25
|
-
"scroll.top": "回顶",
|
|
26
|
-
"scroll.bottom": "回底",
|
|
27
|
-
"dismiss.label": "标记完成",
|
|
28
|
-
"status.running": "运行中",
|
|
29
|
-
"status.done": "已完成",
|
|
30
|
-
"status.error": "错误",
|
|
31
|
-
"order.desc": "降序(最新在前)",
|
|
32
|
-
"order.asc": "升序(最早在前)",
|
|
33
|
-
"scroll.wheel": "滚轮翻页",
|
|
34
|
-
"scroll.click": "点击翻页",
|
|
35
|
-
"ttl.label": "清理周期",
|
|
36
|
-
"ttl.3d": "3 天",
|
|
37
|
-
"ttl.7d": "7 天",
|
|
38
|
-
"ttl.14d": "14 天",
|
|
39
|
-
"ttl.30d": "30 天",
|
|
40
|
-
"ttl.unlimited": "无期限",
|
|
41
|
-
"ttl.toast": "清理周期已设为 {n} 天",
|
|
42
|
-
"ttl.toast_unlimited": "清理周期已设为无期限",
|
|
43
|
-
"clear.title": "确认清除",
|
|
44
|
-
"clear.prompt": "确定清除当前会话所有子代理记录?此操作不可撤销。",
|
|
45
|
-
"clear.prompt_running": "当前有 {n} 个运行中的子代理,清除后将不可恢复。确定继续?",
|
|
46
|
-
"clear.done": "已清除 {n} 条子代理记录",
|
|
47
|
-
"clear.empty": "当前会话无子代理记录",
|
|
48
|
-
},
|
|
49
|
-
en: {
|
|
50
|
-
"panel.title": "SubAgent",
|
|
51
|
-
"status.none": "No sub-agents yet",
|
|
52
|
-
"agent.label": "agent",
|
|
53
|
-
"status.label": "status",
|
|
54
|
-
"time.label": "time",
|
|
55
|
-
"tokens.label": "tokens",
|
|
56
|
-
"error.label": "error",
|
|
57
|
-
"model.label": "model",
|
|
58
|
-
"todo.label": "todo",
|
|
59
|
-
"session.label": "session ID",
|
|
60
|
-
"session.toast.copy": "Copy the ID above manually",
|
|
61
|
-
"open.label": "Open session",
|
|
62
|
-
"cost.label": "cost",
|
|
63
|
-
"scroll.more": "more",
|
|
64
|
-
"scroll.top": "Top",
|
|
65
|
-
"scroll.bottom": "Bottom",
|
|
66
|
-
"dismiss.label": "dismiss",
|
|
67
|
-
"status.running": "running",
|
|
68
|
-
"status.done": "done",
|
|
69
|
-
"status.error": "error",
|
|
70
|
-
"order.desc": "Desc (newest first)",
|
|
71
|
-
"order.asc": "Asc (oldest first)",
|
|
72
|
-
"scroll.wheel": "Wheel Scroll",
|
|
73
|
-
"scroll.click": "Click Scroll",
|
|
74
|
-
"ttl.label": "TTL (Time to Live)",
|
|
75
|
-
"ttl.3d": "3 days",
|
|
76
|
-
"ttl.7d": "7 days",
|
|
77
|
-
"ttl.14d": "14 days",
|
|
78
|
-
"ttl.30d": "30 days",
|
|
79
|
-
"ttl.unlimited": "Never",
|
|
80
|
-
"ttl.toast": "TTL set to {n} days",
|
|
81
|
-
"ttl.toast_unlimited": "TTL set to Never",
|
|
82
|
-
"clear.title": "Confirm",
|
|
83
|
-
"clear.prompt": "Clear all sub-agent records for this session? This cannot be undone.",
|
|
84
|
-
"clear.prompt_running": "{n} sub-agent(s) are still running. Clearing will discard them permanently. Continue?",
|
|
85
|
-
"clear.done": "Cleared {n} sub-agent record(s)",
|
|
86
|
-
"clear.empty": "No sub-agent records in this session",
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
function detectLang() {
|
|
90
|
-
const env = process?.env?.OPENCODE_LANG ?? process?.env?.LANG ?? "";
|
|
91
|
-
if (env.startsWith("zh"))
|
|
92
|
-
return "zh";
|
|
93
|
-
return "en";
|
|
94
|
-
}
|
|
95
|
-
// ===================================================================
|
|
96
9
|
// Helpers — visual width
|
|
97
10
|
// ===================================================================
|
|
98
11
|
function charColumns(c) {
|
|
@@ -243,7 +156,7 @@ const globalEntryCache = new Map();
|
|
|
243
156
|
// 模块级刷新信号:外部(如斜杠命令)触发清除后 +1,组件 scan 依赖它以重扫。
|
|
244
157
|
const [clearTick, setClearTick] = createSignal(0);
|
|
245
158
|
function SubAgentPanel(props) {
|
|
246
|
-
const t = (
|
|
159
|
+
const t = createT(() => props.lang());
|
|
247
160
|
// ── session data (single-key, true deletion on cleanup) ──
|
|
248
161
|
const SESSION_DATA_KEY = `${KV_PREFIX}.session_data`;
|
|
249
162
|
const ttlDaysRaw = parseInt(String(props.api.kv.get(`${KV_PREFIX}.ttl_days`, "3")), 10);
|
|
@@ -385,7 +298,7 @@ function SubAgentPanel(props) {
|
|
|
385
298
|
let needsImmediateFlush = false;
|
|
386
299
|
for (const [id, entry] of next) {
|
|
387
300
|
const prevEntry = prev.get(id);
|
|
388
|
-
if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error")) {
|
|
301
|
+
if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error" || entry.status === "cancelled")) {
|
|
389
302
|
needsImmediateFlush = true;
|
|
390
303
|
break;
|
|
391
304
|
}
|
|
@@ -438,6 +351,7 @@ function SubAgentPanel(props) {
|
|
|
438
351
|
})());
|
|
439
352
|
const [hoveredOpen, setHoveredOpen] = createSignal(undefined);
|
|
440
353
|
const [hoveredDismiss, setHoveredDismiss] = createSignal(undefined);
|
|
354
|
+
const [hoveredCancel, setHoveredCancel] = createSignal(undefined);
|
|
441
355
|
const [hoveredTop, setHoveredTop] = createSignal(false);
|
|
442
356
|
const [hoveredMoreAbove, setHoveredMoreAbove] = createSignal(false);
|
|
443
357
|
const [hoveredMoreBelow, setHoveredMoreBelow] = createSignal(false);
|
|
@@ -553,7 +467,7 @@ function SubAgentPanel(props) {
|
|
|
553
467
|
const next = new Map(prev);
|
|
554
468
|
const nowTs = Date.now();
|
|
555
469
|
const e = partial.status;
|
|
556
|
-
const ended = e === "done" || e === "error";
|
|
470
|
+
const ended = e === "done" || e === "error" || e === "cancelled";
|
|
557
471
|
next.set(partial.id, {
|
|
558
472
|
...(existing ?? { startedAt: nowTs }),
|
|
559
473
|
...partial,
|
|
@@ -563,6 +477,107 @@ function SubAgentPanel(props) {
|
|
|
563
477
|
return next;
|
|
564
478
|
});
|
|
565
479
|
};
|
|
480
|
+
// ── cancel helpers ──
|
|
481
|
+
const isDescendantOf = (childId, rootId) => {
|
|
482
|
+
const visited = new Set();
|
|
483
|
+
try {
|
|
484
|
+
let current = props.api.state.session.get(childId);
|
|
485
|
+
while (current?.parentID) {
|
|
486
|
+
if (visited.has(current.id))
|
|
487
|
+
return false;
|
|
488
|
+
visited.add(current.id);
|
|
489
|
+
if (current.parentID === rootId)
|
|
490
|
+
return true;
|
|
491
|
+
current = props.api.state.session.get(current.parentID);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch { }
|
|
495
|
+
return false;
|
|
496
|
+
};
|
|
497
|
+
const settleOnIdle = (entry) => {
|
|
498
|
+
if (entry.status === "cancel_requested" && entry.abortAccepted)
|
|
499
|
+
return "cancelled";
|
|
500
|
+
return "done";
|
|
501
|
+
};
|
|
502
|
+
const cancelEntry = async (entry) => {
|
|
503
|
+
const childId = entry.sessionId;
|
|
504
|
+
if (!childId) {
|
|
505
|
+
props.api.ui.toast({
|
|
506
|
+
title: entry.title || entry.agent,
|
|
507
|
+
message: t("cancel.label") + ": " + t("cancel.no_session"),
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const child = props.api.state.session.get(childId);
|
|
513
|
+
if (!child?.parentID) {
|
|
514
|
+
props.api.ui.toast({
|
|
515
|
+
title: entry.title || entry.agent,
|
|
516
|
+
message: t("cancel.label") + ": " + t("cancel.not_child"),
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
props.api.ui.toast({
|
|
523
|
+
title: entry.title || entry.agent,
|
|
524
|
+
message: t("cancel.label") + ": " + t("cancel.read_error"),
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!isDescendantOf(childId, props.sessionId)) {
|
|
529
|
+
props.api.ui.toast({
|
|
530
|
+
title: entry.title || entry.agent,
|
|
531
|
+
message: t("cancel.label") + ": " + t("cancel.outside_tree"),
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const st = props.api.state.session.status(childId);
|
|
537
|
+
if (st?.type !== "busy") {
|
|
538
|
+
const tokens = readSessionTokens(childId);
|
|
539
|
+
const cost = readSessionCost(childId);
|
|
540
|
+
upsertEntry({
|
|
541
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
542
|
+
status: "done", sessionId: entry.sessionId,
|
|
543
|
+
tokens, cost,
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
props.api.ui.toast({
|
|
550
|
+
title: entry.title || entry.agent,
|
|
551
|
+
message: t("cancel.label") + ": " + t("cancel.status_error"),
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
upsertEntry({
|
|
556
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
557
|
+
status: "cancel_requested", sessionId: entry.sessionId,
|
|
558
|
+
cancelRequestedAt: Date.now(), abortAccepted: false, cancelReason: "manual",
|
|
559
|
+
});
|
|
560
|
+
try {
|
|
561
|
+
await props.api.client.session.abort({ sessionID: childId });
|
|
562
|
+
upsertEntry({
|
|
563
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
564
|
+
status: "cancel_requested", sessionId: entry.sessionId,
|
|
565
|
+
abortAccepted: true,
|
|
566
|
+
});
|
|
567
|
+
props.api.ui.toast({ message: t("cancel.label") + ": " + t("cancel.sent") });
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
upsertEntry({
|
|
571
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
572
|
+
status: "error", sessionId: entry.sessionId,
|
|
573
|
+
error: String(err),
|
|
574
|
+
});
|
|
575
|
+
props.api.ui.toast({
|
|
576
|
+
title: entry.title || entry.agent,
|
|
577
|
+
message: t("cancel.label") + ": " + t("cancel.failed"),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
};
|
|
566
581
|
// ── event handlers ──
|
|
567
582
|
const handlePartUpdated = (event) => {
|
|
568
583
|
const e = event;
|
|
@@ -670,10 +685,11 @@ function SubAgentPanel(props) {
|
|
|
670
685
|
// 在给定的 entries Map 中查找并更新匹配的子代理 entry。
|
|
671
686
|
// 返回 true 表示找到并更新了,false 表示未找到。
|
|
672
687
|
const tryMatchAndUpdate = (entriesMap, targetSid, targetStatus, nowTs) => {
|
|
673
|
-
// 精确匹配:sessionId 对得上 + 状态为 running
|
|
688
|
+
// 精确匹配:sessionId 对得上 + 状态为 running / cancel_requested
|
|
674
689
|
for (const [, entry] of entriesMap) {
|
|
675
|
-
if (entry.sessionId === targetSid && entry.status === "running") {
|
|
676
|
-
|
|
690
|
+
if (entry.sessionId === targetSid && (entry.status === "running" || entry.status === "cancel_requested")) {
|
|
691
|
+
const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(entry);
|
|
692
|
+
entry.status = finalStatus;
|
|
677
693
|
entry.endedAt = nowTs;
|
|
678
694
|
entry.tokens = entry.tokens ?? sessionTokens;
|
|
679
695
|
entry.cost = entry.cost ?? sessionCost;
|
|
@@ -684,13 +700,13 @@ function SubAgentPanel(props) {
|
|
|
684
700
|
return true;
|
|
685
701
|
}
|
|
686
702
|
}
|
|
687
|
-
// 回退:sessionId 未关联但 agent 名匹配 + 状态为 running
|
|
703
|
+
// 回退:sessionId 未关联但 agent 名匹配 + 状态为 running / cancel_requested
|
|
688
704
|
if (sessionAgent) {
|
|
689
705
|
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
690
706
|
const saNorm = normalize(sessionAgent);
|
|
691
707
|
let best = null;
|
|
692
708
|
for (const [, entry] of entriesMap) {
|
|
693
|
-
if (entry.status !== "running")
|
|
709
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
694
710
|
continue;
|
|
695
711
|
const eaNorm = normalize(entry.agent);
|
|
696
712
|
if (!eaNorm || !saNorm)
|
|
@@ -703,7 +719,7 @@ function SubAgentPanel(props) {
|
|
|
703
719
|
}
|
|
704
720
|
if (!best) {
|
|
705
721
|
for (const [, entry] of entriesMap) {
|
|
706
|
-
if (entry.status !== "running")
|
|
722
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
707
723
|
continue;
|
|
708
724
|
if (entry.sessionId)
|
|
709
725
|
continue;
|
|
@@ -713,7 +729,8 @@ function SubAgentPanel(props) {
|
|
|
713
729
|
}
|
|
714
730
|
}
|
|
715
731
|
if (best) {
|
|
716
|
-
|
|
732
|
+
const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(best.entry);
|
|
733
|
+
best.entry.status = finalStatus;
|
|
717
734
|
best.entry.endedAt = nowTs;
|
|
718
735
|
best.entry.tokens = best.entry.tokens ?? sessionTokens;
|
|
719
736
|
best.entry.cost = best.entry.cost ?? sessionCost;
|
|
@@ -733,16 +750,17 @@ function SubAgentPanel(props) {
|
|
|
733
750
|
for (const [id, entry] of next) {
|
|
734
751
|
if (entry.sessionId !== sid)
|
|
735
752
|
continue;
|
|
736
|
-
if (entry.status !== "running" && entry.status !== "done")
|
|
753
|
+
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested")
|
|
737
754
|
continue;
|
|
738
755
|
// Skip parent session idle — subagent entries belong to child sessions only
|
|
739
756
|
if (sid === props.sessionId)
|
|
740
757
|
continue;
|
|
741
758
|
// For "done" entries (sync tasks completed before session.idle), only backfill tokens/cost
|
|
742
|
-
const alreadySettled = entry.status !== "running";
|
|
759
|
+
const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested";
|
|
760
|
+
const finalStatus = status === "error" ? "error" : settleOnIdle(entry);
|
|
743
761
|
next.set(id, {
|
|
744
762
|
...entry,
|
|
745
|
-
...(alreadySettled ? {} : { status, endedAt: Date.now() }),
|
|
763
|
+
...(alreadySettled ? {} : { status: finalStatus, endedAt: Date.now() }),
|
|
746
764
|
tokens: entry.tokens ?? sessionTokens,
|
|
747
765
|
cost: entry.cost ?? sessionCost,
|
|
748
766
|
model: entry.model ?? sessionModel,
|
|
@@ -759,7 +777,7 @@ function SubAgentPanel(props) {
|
|
|
759
777
|
let best = null;
|
|
760
778
|
// Phase 1: try matching by agent name(agent 名有交集)
|
|
761
779
|
for (const [id, entry] of next) {
|
|
762
|
-
if (entry.status !== "running")
|
|
780
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
763
781
|
continue;
|
|
764
782
|
const eaNorm = normalize(entry.agent);
|
|
765
783
|
if (!eaNorm || !saNorm)
|
|
@@ -774,7 +792,7 @@ function SubAgentPanel(props) {
|
|
|
774
792
|
// fall back to time proximity for entries that have no sessionId yet
|
|
775
793
|
if (!best) {
|
|
776
794
|
for (const [id, entry] of next) {
|
|
777
|
-
if (entry.status !== "running")
|
|
795
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
778
796
|
continue;
|
|
779
797
|
if (entry.sessionId)
|
|
780
798
|
continue;
|
|
@@ -785,8 +803,9 @@ function SubAgentPanel(props) {
|
|
|
785
803
|
}
|
|
786
804
|
if (best) {
|
|
787
805
|
const entry = next.get(best.id);
|
|
806
|
+
const finalStatus = status === "error" ? "error" : settleOnIdle(entry);
|
|
788
807
|
next.set(best.id, {
|
|
789
|
-
...entry, status, endedAt: nowTs,
|
|
808
|
+
...entry, status: finalStatus, endedAt: nowTs,
|
|
790
809
|
tokens: sessionTokens || entry.tokens,
|
|
791
810
|
cost: sessionCost || entry.cost,
|
|
792
811
|
sessionId: sid,
|
|
@@ -1022,7 +1041,7 @@ function SubAgentPanel(props) {
|
|
|
1022
1041
|
status = "running";
|
|
1023
1042
|
}
|
|
1024
1043
|
// Already settled → skip
|
|
1025
|
-
if (exists && exists.status !== "running")
|
|
1044
|
+
if (exists && exists.status !== "running" && exists.status !== "cancel_requested")
|
|
1026
1045
|
continue;
|
|
1027
1046
|
// Running entry with no explicit status improvement from part:
|
|
1028
1047
|
// try message-level heuristics first, then time-based fallback.
|
|
@@ -1081,7 +1100,7 @@ function SubAgentPanel(props) {
|
|
|
1081
1100
|
let changed = false;
|
|
1082
1101
|
const next = new Map(prev);
|
|
1083
1102
|
for (const [id, entry] of next) {
|
|
1084
|
-
if (entry.status !== "running" || !entry.sessionId)
|
|
1103
|
+
if ((entry.status !== "running" && entry.status !== "cancel_requested") || !entry.sessionId)
|
|
1085
1104
|
continue;
|
|
1086
1105
|
try {
|
|
1087
1106
|
const st = props.api.state.session.status(entry.sessionId);
|
|
@@ -1089,8 +1108,11 @@ function SubAgentPanel(props) {
|
|
|
1089
1108
|
continue;
|
|
1090
1109
|
const tokens = readSessionTokens(entry.sessionId);
|
|
1091
1110
|
const cost = readSessionCost(entry.sessionId);
|
|
1111
|
+
const finalStatus = entry.status === "cancel_requested" && entry.abortAccepted
|
|
1112
|
+
? "cancelled"
|
|
1113
|
+
: "done";
|
|
1092
1114
|
next.set(id, {
|
|
1093
|
-
...entry, status:
|
|
1115
|
+
...entry, status: finalStatus, endedAt: Date.now(),
|
|
1094
1116
|
tokens: tokens ?? entry.tokens,
|
|
1095
1117
|
cost: cost ?? entry.cost,
|
|
1096
1118
|
});
|
|
@@ -1196,8 +1218,8 @@ function SubAgentPanel(props) {
|
|
|
1196
1218
|
elapsed: (e.endedAt ?? nowVal) - e.startedAt,
|
|
1197
1219
|
}));
|
|
1198
1220
|
});
|
|
1199
|
-
const doneCount = createMemo(() => entryList().filter((e) => e.status === "done").length);
|
|
1200
|
-
const runningCount = createMemo(() => entryList().filter((e) => e.status === "running").length);
|
|
1221
|
+
const doneCount = createMemo(() => entryList().filter((e) => e.status === "done" || e.status === "cancelled").length);
|
|
1222
|
+
const runningCount = createMemo(() => entryList().filter((e) => e.status === "running" || e.status === "cancel_requested").length);
|
|
1201
1223
|
const errCount = createMemo(() => entryList().filter((e) => e.status === "error").length);
|
|
1202
1224
|
const anyEntry = () => entryList().length > 0;
|
|
1203
1225
|
const totalTokens = createMemo(() => {
|
|
@@ -1341,11 +1363,16 @@ function SubAgentPanel(props) {
|
|
|
1341
1363
|
}, children: _jsxs("span", { style: { fg: hoveredMoreAbove() ? pal().warning : pal().muted }, children: [" ", "\u2191 ", hiddenAbove(), " ", t("scroll.more")] }) }) }), _jsx(For, { each: visibleList(), children: (entry) => {
|
|
1342
1364
|
const isExpanded = () => expanded() === entry.id;
|
|
1343
1365
|
const isRunning = entry.status === "running";
|
|
1366
|
+
const isCancelRequested = entry.status === "cancel_requested";
|
|
1367
|
+
const isCancelled = entry.status === "cancelled";
|
|
1344
1368
|
const isError = entry.status === "error";
|
|
1369
|
+
const isActiveRunning = isRunning || isCancelRequested;
|
|
1345
1370
|
const elapsed = () => (entry.endedAt ?? now()) - entry.startedAt;
|
|
1346
1371
|
const statusDot = () => "\u25cf";
|
|
1347
1372
|
const statusColor = () => {
|
|
1348
|
-
if (
|
|
1373
|
+
if (isCancelled)
|
|
1374
|
+
return pal().muted;
|
|
1375
|
+
if (!isActiveRunning)
|
|
1349
1376
|
return isError ? pal().error : pal().success;
|
|
1350
1377
|
const t = (Math.sin(((now() % 2000) / 2000) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
|
|
1351
1378
|
const a = rgb(pal().muted), b = rgb(pal().warning);
|
|
@@ -1356,13 +1383,13 @@ function SubAgentPanel(props) {
|
|
|
1356
1383
|
const bl = Math.round(a.b + (b.b - a.b) * t);
|
|
1357
1384
|
return "#" + [r, g, bl].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
1358
1385
|
};
|
|
1359
|
-
const timeColor = () =>
|
|
1386
|
+
const timeColor = () => isActiveRunning ? pal().warning : isError ? pal().error : pal().muted;
|
|
1360
1387
|
// Entry label: collapsed shows title only, expanded shows title only too
|
|
1361
1388
|
const tokenText = () => !isExpanded() && entry.tokens !== undefined && entry.tokens > 0
|
|
1362
1389
|
? ` ${fmtTokens(entry.tokens)}`
|
|
1363
1390
|
: "";
|
|
1364
1391
|
const timeText = () => !isExpanded() && (elapsed() >= 2000 || entry.endedAt !== undefined)
|
|
1365
|
-
? fmtDurationShort(elapsed(),
|
|
1392
|
+
? fmtDurationShort(elapsed(), isActiveRunning)
|
|
1366
1393
|
: "";
|
|
1367
1394
|
const suffixW = () => {
|
|
1368
1395
|
let w = 0;
|
|
@@ -1382,30 +1409,44 @@ function SubAgentPanel(props) {
|
|
|
1382
1409
|
const pad = Math.max(0, max - visualWidth(truncated));
|
|
1383
1410
|
return truncated + " ".repeat(pad);
|
|
1384
1411
|
};
|
|
1385
|
-
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:
|
|
1412
|
+
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: isActiveRunning ? pal().warning : isCancelled ? pal().muted : isError ? pal().error : pal().success }, children: isActiveRunning ? t("status.running") : isCancelled ? t("status.cancelled") : 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(), isActiveRunning) })] }) }), _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: (() => {
|
|
1386
1413
|
const cost = entry.cost;
|
|
1387
1414
|
return (_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: ["$", cost.toFixed(4)] })] }));
|
|
1388
|
-
})() }), _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", { onMouseUp: () => {
|
|
1389
|
-
|
|
1415
|
+
})() }), _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", { onMouseUp: async () => {
|
|
1416
|
+
const sessionId = entry.sessionId;
|
|
1417
|
+
if (!sessionId)
|
|
1418
|
+
return;
|
|
1419
|
+
const result = await copyText(sessionId);
|
|
1420
|
+
if (result.copied) {
|
|
1390
1421
|
props.api.ui.toast({
|
|
1422
|
+
variant: "success",
|
|
1391
1423
|
title: entry.title || entry.agent,
|
|
1392
|
-
message:
|
|
1393
|
-
duration:
|
|
1424
|
+
message: t("session.toast.copied"),
|
|
1425
|
+
duration: 2500,
|
|
1394
1426
|
});
|
|
1427
|
+
return;
|
|
1395
1428
|
}
|
|
1429
|
+
props.api.ui.toast({
|
|
1430
|
+
variant: "warning",
|
|
1431
|
+
title: entry.title || entry.agent,
|
|
1432
|
+
message: `${sessionId}\n\n${t("session.toast.copy_failed")}`,
|
|
1433
|
+
duration: 8000,
|
|
1434
|
+
});
|
|
1396
1435
|
}, children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("session.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("session.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: truncate(entry.sessionId, expandedValAvail() - visualWidth(" ⎘")) }), _jsx("span", { style: { fg: pal().warning }, children: " \u2398" })] }) }), _jsx(Show, { when: entry.sessionId || isRunning, children: (() => {
|
|
1397
|
-
const dismissLabel = () => `- ${t("dismiss.label")}`;
|
|
1398
1436
|
const openPrefix = () => " \u2192 ";
|
|
1399
1437
|
const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : "";
|
|
1400
1438
|
const openW = () => entry.sessionId ? visualWidth(openFull()) : 0;
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1439
|
+
const cancelLabel = () => ` ${t("cancel.label")}`;
|
|
1440
|
+
const dismissLabel = () => ` ${t("dismiss.label")}`;
|
|
1441
|
+
const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0);
|
|
1442
|
+
const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2);
|
|
1443
|
+
return (_jsxs("box", { flexDirection: "row", children: [_jsx(Show, { when: entry.sessionId, children: _jsxs("text", { onMouseOver: () => setHoveredOpen(entry.id), onMouseOut: () => setHoveredOpen(undefined), onMouseUp: () => {
|
|
1403
1444
|
if (entry.sessionId) {
|
|
1404
1445
|
props.api.route.navigate("session", { sessionID: entry.sessionId });
|
|
1405
1446
|
}
|
|
1406
|
-
}, 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:
|
|
1407
|
-
|
|
1408
|
-
|
|
1447
|
+
}, 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("text", { style: { fg: pal().muted }, children: " ".repeat(spacerW()) }), _jsx(Show, { when: isRunning && entry.sessionId, children: _jsx("text", { onMouseOver: () => setHoveredCancel(entry.id), onMouseOut: () => setHoveredCancel(undefined), onMouseUp: () => cancelEntry(entry), children: _jsx("span", { style: { fg: hoveredCancel() === entry.id ? pal().warning : pal().error }, children: cancelLabel() }) }) }), _jsx(Show, { when: isRunning, children: _jsx("text", { onMouseOver: () => setHoveredDismiss(entry.id), onMouseOut: () => setHoveredDismiss(undefined), onMouseUp: () => {
|
|
1448
|
+
upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" });
|
|
1449
|
+
}, children: _jsx("span", { style: { fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }, children: dismissLabel() }) }) })] }));
|
|
1409
1450
|
})() })] })] }));
|
|
1410
1451
|
} }), _jsx(Show, { when: hiddenBelow() > 0 || (props.sortOrder() === "desc" ? scrollOffset() > 0 : entryList().length > max() && clampedOffset() < entryList().length - max()), children: (() => {
|
|
1411
1452
|
const showMore = hiddenBelow() > 0;
|
|
@@ -1458,7 +1499,7 @@ const KV_PREFIX = "subagent_magazine";
|
|
|
1458
1499
|
const tui = async (api) => {
|
|
1459
1500
|
// ── language ──
|
|
1460
1501
|
const stored = String(api.kv.get(`${KV_PREFIX}.lang`, ""));
|
|
1461
|
-
const initialLang =
|
|
1502
|
+
const initialLang = LANG_META.some((m) => m.code === stored) ? stored : detectLang();
|
|
1462
1503
|
const [lang, setLang] = createSignal(initialLang);
|
|
1463
1504
|
const [maxEntries, setMaxEntries] = createSignal(parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10);
|
|
1464
1505
|
const [sortOrder, setSortOrder] = createSignal(String(api.kv.get(`${KV_PREFIX}.order`, "desc")) === "asc" ? "asc" : "desc");
|
|
@@ -1473,15 +1514,12 @@ const tui = async (api) => {
|
|
|
1473
1514
|
description: "Switch display language (中文 / English)",
|
|
1474
1515
|
slash: { name: "subagent-lang" },
|
|
1475
1516
|
onSelect: (dialog) => {
|
|
1476
|
-
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Language / \u8BED\u8A00", options:
|
|
1477
|
-
{ title: "中文", value: "zh" },
|
|
1478
|
-
{ title: "English", value: "en" },
|
|
1479
|
-
], onSelect: (opt) => {
|
|
1517
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Language / \u8BED\u8A00", options: LANG_META.map((m) => ({ title: m.label, value: m.code })), onSelect: (opt) => {
|
|
1480
1518
|
const l = opt.value;
|
|
1481
1519
|
setLang(l);
|
|
1482
1520
|
api.kv.set(`${KV_PREFIX}.lang`, l);
|
|
1483
1521
|
api.ui.toast({
|
|
1484
|
-
message:
|
|
1522
|
+
message: "Language: " + (LANG_META.find((m) => m.code === l)?.label ?? l),
|
|
1485
1523
|
});
|
|
1486
1524
|
dialog?.clear();
|
|
1487
1525
|
} })));
|
|
@@ -1493,7 +1531,7 @@ const tui = async (api) => {
|
|
|
1493
1531
|
description: "Set sub-agent entry sort order (desc / asc)",
|
|
1494
1532
|
slash: { name: "subagent-order" },
|
|
1495
1533
|
onSelect: (dialog) => {
|
|
1496
|
-
const t = (
|
|
1534
|
+
const t = createT(() => lang());
|
|
1497
1535
|
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Sort Order / \u6392\u5E8F\u65B9\u5F0F", options: [
|
|
1498
1536
|
{ title: t("order.desc"), value: "desc" },
|
|
1499
1537
|
{ title: t("order.asc"), value: "asc" },
|
|
@@ -1514,7 +1552,7 @@ const tui = async (api) => {
|
|
|
1514
1552
|
description: "Set scroll mode (wheel / click)",
|
|
1515
1553
|
slash: { name: "subagent-scroll" },
|
|
1516
1554
|
onSelect: (dialog) => {
|
|
1517
|
-
const t = (
|
|
1555
|
+
const t = createT(() => lang());
|
|
1518
1556
|
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Scroll Mode / \u6EDA\u52A8\u6A21\u5F0F", options: [
|
|
1519
1557
|
{ title: t("scroll.wheel"), value: "wheel" },
|
|
1520
1558
|
{ title: t("scroll.click"), value: "click" },
|
|
@@ -1580,7 +1618,7 @@ const tui = async (api) => {
|
|
|
1580
1618
|
}
|
|
1581
1619
|
let count = 0;
|
|
1582
1620
|
for (const [, entry] of entries) {
|
|
1583
|
-
if (entry.status === "running") {
|
|
1621
|
+
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
1584
1622
|
entry.status = "done";
|
|
1585
1623
|
entry.endedAt = Date.now();
|
|
1586
1624
|
count++;
|
|
@@ -1633,7 +1671,7 @@ const tui = async (api) => {
|
|
|
1633
1671
|
description: "Set session data retention period (days before auto-cleanup)",
|
|
1634
1672
|
slash: { name: "subagent-ttl" },
|
|
1635
1673
|
onSelect: (dialog) => {
|
|
1636
|
-
const t = (
|
|
1674
|
+
const t = createT(() => signals.lang());
|
|
1637
1675
|
const curRaw = parseInt(String(api.kv.get(`${KV_PREFIX}.ttl_days`, "3")), 10);
|
|
1638
1676
|
const curDays = Number.isNaN(curRaw) ? 3 : curRaw;
|
|
1639
1677
|
const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`;
|
|
@@ -1646,7 +1684,7 @@ const tui = async (api) => {
|
|
|
1646
1684
|
], onSelect: (opt) => {
|
|
1647
1685
|
const days = parseInt(opt.value, 10);
|
|
1648
1686
|
api.kv.set(`${KV_PREFIX}.ttl_days`, String(days));
|
|
1649
|
-
const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast"
|
|
1687
|
+
const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days });
|
|
1650
1688
|
api.ui.toast({ message: msg });
|
|
1651
1689
|
dialog?.clear();
|
|
1652
1690
|
} })));
|
|
@@ -1658,7 +1696,7 @@ const tui = async (api) => {
|
|
|
1658
1696
|
description: "Delete all sub-agent records for the current session (cannot be undone)",
|
|
1659
1697
|
slash: { name: "subagent-clear-entries" },
|
|
1660
1698
|
onSelect: (dialog) => {
|
|
1661
|
-
const t = (
|
|
1699
|
+
const t = createT(() => signals.lang());
|
|
1662
1700
|
const sid = signals.sessionId;
|
|
1663
1701
|
const sessionObj = api.state.session.get(sid);
|
|
1664
1702
|
const parentID = sessionObj?.parentID;
|
|
@@ -1672,7 +1710,7 @@ const tui = async (api) => {
|
|
|
1672
1710
|
}
|
|
1673
1711
|
}
|
|
1674
1712
|
const msg = runningCount > 0
|
|
1675
|
-
? t("clear.prompt_running"
|
|
1713
|
+
? t("clear.prompt_running", { n: runningCount })
|
|
1676
1714
|
: t("clear.prompt");
|
|
1677
1715
|
dialog?.replace(() => (_jsx(api.ui.DialogConfirm, { title: t("clear.title"), message: msg, onConfirm: () => {
|
|
1678
1716
|
try {
|
|
@@ -1702,7 +1740,7 @@ const tui = async (api) => {
|
|
|
1702
1740
|
api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data));
|
|
1703
1741
|
globalEntryCache.delete(sid);
|
|
1704
1742
|
setClearTick((v) => v + 1);
|
|
1705
|
-
const msg = t("clear.done"
|
|
1743
|
+
const msg = t("clear.done", { n: count });
|
|
1706
1744
|
api.ui.toast({ message: msg });
|
|
1707
1745
|
}
|
|
1708
1746
|
catch { }
|