opencode-subagent-magazine 1.4.1 → 1.5.0
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/index.js +190 -34
- package/dist/tui.js +419 -152
- package/package.json +2 -1
- package/src/_version.ts +1 -1
- package/src/clipboard.ts +134 -0
- package/src/index.tsx +224 -56
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.5.0";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION = "1.
|
|
2
|
+
export const PLUGIN_VERSION = "1.5.0";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ClipboardMethod = "pbcopy" | "osascript" | "wl-copy" | "xclip" | "xsel" | "powershell" | "osc52" | "none";
|
|
2
|
+
export interface CopyTextResult {
|
|
3
|
+
copied: boolean;
|
|
4
|
+
method: ClipboardMethod;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function copyText(text: string): Promise<CopyTextResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform, release } from "node:os";
|
|
3
|
+
function runWithInput(command, args, input) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
7
|
+
windowsHide: true,
|
|
8
|
+
});
|
|
9
|
+
child.once("error", reject);
|
|
10
|
+
child.once("close", (code) => {
|
|
11
|
+
if (code === 0)
|
|
12
|
+
resolve();
|
|
13
|
+
else
|
|
14
|
+
reject(new Error(`${command} exited with code ${code}`));
|
|
15
|
+
});
|
|
16
|
+
child.stdin?.end(input);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function writeOsc52(text) {
|
|
20
|
+
if (!process.stdout.isTTY)
|
|
21
|
+
return false;
|
|
22
|
+
const payload = Buffer.from(text, "utf8").toString("base64");
|
|
23
|
+
const sequence = `\x1b]52;c;${payload}\x07`;
|
|
24
|
+
const wrapped = process.env.TMUX || process.env.STY
|
|
25
|
+
? `\x1bPtmux;\x1b${sequence}\x1b\\`
|
|
26
|
+
: sequence;
|
|
27
|
+
process.stdout.write(wrapped);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
async function tryCommand(method, command, args, text) {
|
|
31
|
+
try {
|
|
32
|
+
await runWithInput(command, args, text);
|
|
33
|
+
return { copied: true, method };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function copyText(text) {
|
|
40
|
+
if (!text)
|
|
41
|
+
return { copied: false, method: "none", error: "empty_text" };
|
|
42
|
+
const os = platform();
|
|
43
|
+
const isWsl = release().toLowerCase().includes("microsoft");
|
|
44
|
+
const attempts = [];
|
|
45
|
+
if (os === "darwin") {
|
|
46
|
+
attempts.push(() => tryCommand("pbcopy", "pbcopy", [], text));
|
|
47
|
+
attempts.push(() => tryCommand("osascript", "osascript", ["-e", "set the clipboard to (read (POSIX file \"/dev/stdin\") as text)"], text));
|
|
48
|
+
}
|
|
49
|
+
if (os === "linux" && isWsl) {
|
|
50
|
+
attempts.push(() => tryCommand("powershell", "powershell.exe", [
|
|
51
|
+
"-NonInteractive",
|
|
52
|
+
"-NoProfile",
|
|
53
|
+
"-Command",
|
|
54
|
+
"[Console]::InputEncoding=[Text.Encoding]::UTF8; Set-Clipboard ([Console]::In.ReadToEnd())",
|
|
55
|
+
], text));
|
|
56
|
+
}
|
|
57
|
+
else if (os === "linux") {
|
|
58
|
+
if (process.env.WAYLAND_DISPLAY) {
|
|
59
|
+
attempts.push(() => tryCommand("wl-copy", "wl-copy", [], text));
|
|
60
|
+
}
|
|
61
|
+
attempts.push(() => tryCommand("xclip", "xclip", ["-selection", "clipboard"], text));
|
|
62
|
+
attempts.push(() => tryCommand("xsel", "xsel", ["--clipboard", "--input"], text));
|
|
63
|
+
}
|
|
64
|
+
if (os === "win32") {
|
|
65
|
+
attempts.push(() => tryCommand("powershell", "powershell.exe", [
|
|
66
|
+
"-NonInteractive",
|
|
67
|
+
"-NoProfile",
|
|
68
|
+
"-Command",
|
|
69
|
+
"[Console]::InputEncoding=[Text.Encoding]::UTF8; Set-Clipboard ([Console]::In.ReadToEnd())",
|
|
70
|
+
], text));
|
|
71
|
+
}
|
|
72
|
+
for (const attempt of attempts) {
|
|
73
|
+
const result = await attempt();
|
|
74
|
+
if (result)
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
if (writeOsc52(text))
|
|
79
|
+
return { copied: true, method: "osc52" };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Continue to the explicit failure result.
|
|
83
|
+
}
|
|
84
|
+
return { copied: false, method: "none", error: "clipboard_unavailable" };
|
|
85
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
/** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
|
|
5
6
|
const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"]);
|
|
6
7
|
// ===================================================================
|
|
@@ -19,14 +20,18 @@ const I18N = {
|
|
|
19
20
|
"todo.label": "进度",
|
|
20
21
|
"session.label": "会话 ID",
|
|
21
22
|
"session.toast.copy": "可手动复制上方 ID",
|
|
23
|
+
"session.toast.copied": "会话 ID 已复制",
|
|
24
|
+
"session.toast.copy_failed": "无法访问系统剪贴板,请手动复制上方 ID",
|
|
22
25
|
"open.label": "进入会话",
|
|
23
26
|
"cost.label": "费用",
|
|
24
27
|
"scroll.more": "更多",
|
|
25
28
|
"scroll.top": "回顶",
|
|
26
29
|
"scroll.bottom": "回底",
|
|
27
30
|
"dismiss.label": "标记完成",
|
|
31
|
+
"cancel.label": "取消",
|
|
28
32
|
"status.running": "运行中",
|
|
29
33
|
"status.done": "已完成",
|
|
34
|
+
"status.cancelled": "已取消",
|
|
30
35
|
"status.error": "错误",
|
|
31
36
|
"order.desc": "降序(最新在前)",
|
|
32
37
|
"order.asc": "升序(最早在前)",
|
|
@@ -45,6 +50,14 @@ const I18N = {
|
|
|
45
50
|
"clear.prompt_running": "当前有 {n} 个运行中的子代理,清除后将不可恢复。确定继续?",
|
|
46
51
|
"clear.done": "已清除 {n} 条子代理记录",
|
|
47
52
|
"clear.empty": "当前会话无子代理记录",
|
|
53
|
+
"cancel.no_session": "子会话 ID 不可用",
|
|
54
|
+
"cancel.not_child": "目标不是子会话",
|
|
55
|
+
"cancel.read_error": "无法读取会话信息",
|
|
56
|
+
"cancel.outside_tree": "目标不在当前监控会话树中",
|
|
57
|
+
"cancel.already_ended": "会话已结束,无需取消",
|
|
58
|
+
"cancel.status_error": "无法查询会话状态",
|
|
59
|
+
"cancel.sent": "已发送取消指令",
|
|
60
|
+
"cancel.failed": "取消失败",
|
|
48
61
|
},
|
|
49
62
|
en: {
|
|
50
63
|
"panel.title": "SubAgent",
|
|
@@ -58,14 +71,18 @@ const I18N = {
|
|
|
58
71
|
"todo.label": "todo",
|
|
59
72
|
"session.label": "session ID",
|
|
60
73
|
"session.toast.copy": "Copy the ID above manually",
|
|
74
|
+
"session.toast.copied": "Session ID copied",
|
|
75
|
+
"session.toast.copy_failed": "Cannot access the system clipboard; copy the ID above manually",
|
|
61
76
|
"open.label": "Open session",
|
|
62
77
|
"cost.label": "cost",
|
|
63
78
|
"scroll.more": "more",
|
|
64
79
|
"scroll.top": "Top",
|
|
65
80
|
"scroll.bottom": "Bottom",
|
|
66
81
|
"dismiss.label": "dismiss",
|
|
82
|
+
"cancel.label": "Cancel",
|
|
67
83
|
"status.running": "running",
|
|
68
84
|
"status.done": "done",
|
|
85
|
+
"status.cancelled": "cancelled",
|
|
69
86
|
"status.error": "error",
|
|
70
87
|
"order.desc": "Desc (newest first)",
|
|
71
88
|
"order.asc": "Asc (oldest first)",
|
|
@@ -84,6 +101,14 @@ const I18N = {
|
|
|
84
101
|
"clear.prompt_running": "{n} sub-agent(s) are still running. Clearing will discard them permanently. Continue?",
|
|
85
102
|
"clear.done": "Cleared {n} sub-agent record(s)",
|
|
86
103
|
"clear.empty": "No sub-agent records in this session",
|
|
104
|
+
"cancel.no_session": "Child session ID is unavailable",
|
|
105
|
+
"cancel.not_child": "Target is not a child session",
|
|
106
|
+
"cancel.read_error": "Cannot read session info",
|
|
107
|
+
"cancel.outside_tree": "Target is outside the monitored session tree",
|
|
108
|
+
"cancel.already_ended": "Session already ended, no need to cancel",
|
|
109
|
+
"cancel.status_error": "Cannot query session status",
|
|
110
|
+
"cancel.sent": "Cancel instruction sent",
|
|
111
|
+
"cancel.failed": "Cancellation failed",
|
|
87
112
|
},
|
|
88
113
|
};
|
|
89
114
|
function detectLang() {
|
|
@@ -385,7 +410,7 @@ function SubAgentPanel(props) {
|
|
|
385
410
|
let needsImmediateFlush = false;
|
|
386
411
|
for (const [id, entry] of next) {
|
|
387
412
|
const prevEntry = prev.get(id);
|
|
388
|
-
if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error")) {
|
|
413
|
+
if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error" || entry.status === "cancelled")) {
|
|
389
414
|
needsImmediateFlush = true;
|
|
390
415
|
break;
|
|
391
416
|
}
|
|
@@ -438,6 +463,7 @@ function SubAgentPanel(props) {
|
|
|
438
463
|
})());
|
|
439
464
|
const [hoveredOpen, setHoveredOpen] = createSignal(undefined);
|
|
440
465
|
const [hoveredDismiss, setHoveredDismiss] = createSignal(undefined);
|
|
466
|
+
const [hoveredCancel, setHoveredCancel] = createSignal(undefined);
|
|
441
467
|
const [hoveredTop, setHoveredTop] = createSignal(false);
|
|
442
468
|
const [hoveredMoreAbove, setHoveredMoreAbove] = createSignal(false);
|
|
443
469
|
const [hoveredMoreBelow, setHoveredMoreBelow] = createSignal(false);
|
|
@@ -553,7 +579,7 @@ function SubAgentPanel(props) {
|
|
|
553
579
|
const next = new Map(prev);
|
|
554
580
|
const nowTs = Date.now();
|
|
555
581
|
const e = partial.status;
|
|
556
|
-
const ended = e === "done" || e === "error";
|
|
582
|
+
const ended = e === "done" || e === "error" || e === "cancelled";
|
|
557
583
|
next.set(partial.id, {
|
|
558
584
|
...(existing ?? { startedAt: nowTs }),
|
|
559
585
|
...partial,
|
|
@@ -563,6 +589,107 @@ function SubAgentPanel(props) {
|
|
|
563
589
|
return next;
|
|
564
590
|
});
|
|
565
591
|
};
|
|
592
|
+
// ── cancel helpers ──
|
|
593
|
+
const isDescendantOf = (childId, rootId) => {
|
|
594
|
+
const visited = new Set();
|
|
595
|
+
try {
|
|
596
|
+
let current = props.api.state.session.get(childId);
|
|
597
|
+
while (current?.parentID) {
|
|
598
|
+
if (visited.has(current.id))
|
|
599
|
+
return false;
|
|
600
|
+
visited.add(current.id);
|
|
601
|
+
if (current.parentID === rootId)
|
|
602
|
+
return true;
|
|
603
|
+
current = props.api.state.session.get(current.parentID);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch { }
|
|
607
|
+
return false;
|
|
608
|
+
};
|
|
609
|
+
const settleOnIdle = (entry) => {
|
|
610
|
+
if (entry.status === "cancel_requested" && entry.abortAccepted)
|
|
611
|
+
return "cancelled";
|
|
612
|
+
return "done";
|
|
613
|
+
};
|
|
614
|
+
const cancelEntry = async (entry) => {
|
|
615
|
+
const childId = entry.sessionId;
|
|
616
|
+
if (!childId) {
|
|
617
|
+
props.api.ui.toast({
|
|
618
|
+
title: entry.title || entry.agent,
|
|
619
|
+
message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.no_session"] ?? "Child session ID is unavailable"),
|
|
620
|
+
});
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const child = props.api.state.session.get(childId);
|
|
625
|
+
if (!child?.parentID) {
|
|
626
|
+
props.api.ui.toast({
|
|
627
|
+
title: entry.title || entry.agent,
|
|
628
|
+
message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.not_child"] ?? "Target is not a child session"),
|
|
629
|
+
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
props.api.ui.toast({
|
|
635
|
+
title: entry.title || entry.agent,
|
|
636
|
+
message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.read_error"] ?? "Cannot read session info"),
|
|
637
|
+
});
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (!isDescendantOf(childId, props.sessionId)) {
|
|
641
|
+
props.api.ui.toast({
|
|
642
|
+
title: entry.title || entry.agent,
|
|
643
|
+
message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.outside_tree"] ?? "Target is outside the monitored session tree"),
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const st = props.api.state.session.status(childId);
|
|
649
|
+
if (st?.type !== "busy") {
|
|
650
|
+
const tokens = readSessionTokens(childId);
|
|
651
|
+
const cost = readSessionCost(childId);
|
|
652
|
+
upsertEntry({
|
|
653
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
654
|
+
status: "done", sessionId: entry.sessionId,
|
|
655
|
+
tokens, cost,
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
props.api.ui.toast({
|
|
662
|
+
title: entry.title || entry.agent,
|
|
663
|
+
message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.status_error"] ?? "Cannot query session status"),
|
|
664
|
+
});
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
upsertEntry({
|
|
668
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
669
|
+
status: "cancel_requested", sessionId: entry.sessionId,
|
|
670
|
+
cancelRequestedAt: Date.now(), abortAccepted: false, cancelReason: "manual",
|
|
671
|
+
});
|
|
672
|
+
try {
|
|
673
|
+
await props.api.client.session.abort({ sessionID: childId });
|
|
674
|
+
upsertEntry({
|
|
675
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
676
|
+
status: "cancel_requested", sessionId: entry.sessionId,
|
|
677
|
+
abortAccepted: true,
|
|
678
|
+
});
|
|
679
|
+
props.api.ui.toast({ message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.sent"] ?? "Cancel instruction sent") });
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
upsertEntry({
|
|
683
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
684
|
+
status: "error", sessionId: entry.sessionId,
|
|
685
|
+
error: String(err),
|
|
686
|
+
});
|
|
687
|
+
props.api.ui.toast({
|
|
688
|
+
title: entry.title || entry.agent,
|
|
689
|
+
message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.failed"] ?? "Cancellation failed"),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
};
|
|
566
693
|
// ── event handlers ──
|
|
567
694
|
const handlePartUpdated = (event) => {
|
|
568
695
|
const e = event;
|
|
@@ -670,10 +797,11 @@ function SubAgentPanel(props) {
|
|
|
670
797
|
// 在给定的 entries Map 中查找并更新匹配的子代理 entry。
|
|
671
798
|
// 返回 true 表示找到并更新了,false 表示未找到。
|
|
672
799
|
const tryMatchAndUpdate = (entriesMap, targetSid, targetStatus, nowTs) => {
|
|
673
|
-
// 精确匹配:sessionId 对得上 + 状态为 running
|
|
800
|
+
// 精确匹配:sessionId 对得上 + 状态为 running / cancel_requested
|
|
674
801
|
for (const [, entry] of entriesMap) {
|
|
675
|
-
if (entry.sessionId === targetSid && entry.status === "running") {
|
|
676
|
-
|
|
802
|
+
if (entry.sessionId === targetSid && (entry.status === "running" || entry.status === "cancel_requested")) {
|
|
803
|
+
const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(entry);
|
|
804
|
+
entry.status = finalStatus;
|
|
677
805
|
entry.endedAt = nowTs;
|
|
678
806
|
entry.tokens = entry.tokens ?? sessionTokens;
|
|
679
807
|
entry.cost = entry.cost ?? sessionCost;
|
|
@@ -684,13 +812,13 @@ function SubAgentPanel(props) {
|
|
|
684
812
|
return true;
|
|
685
813
|
}
|
|
686
814
|
}
|
|
687
|
-
// 回退:sessionId 未关联但 agent 名匹配 + 状态为 running
|
|
815
|
+
// 回退:sessionId 未关联但 agent 名匹配 + 状态为 running / cancel_requested
|
|
688
816
|
if (sessionAgent) {
|
|
689
817
|
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
690
818
|
const saNorm = normalize(sessionAgent);
|
|
691
819
|
let best = null;
|
|
692
820
|
for (const [, entry] of entriesMap) {
|
|
693
|
-
if (entry.status !== "running")
|
|
821
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
694
822
|
continue;
|
|
695
823
|
const eaNorm = normalize(entry.agent);
|
|
696
824
|
if (!eaNorm || !saNorm)
|
|
@@ -703,7 +831,7 @@ function SubAgentPanel(props) {
|
|
|
703
831
|
}
|
|
704
832
|
if (!best) {
|
|
705
833
|
for (const [, entry] of entriesMap) {
|
|
706
|
-
if (entry.status !== "running")
|
|
834
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
707
835
|
continue;
|
|
708
836
|
if (entry.sessionId)
|
|
709
837
|
continue;
|
|
@@ -713,7 +841,8 @@ function SubAgentPanel(props) {
|
|
|
713
841
|
}
|
|
714
842
|
}
|
|
715
843
|
if (best) {
|
|
716
|
-
|
|
844
|
+
const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(best.entry);
|
|
845
|
+
best.entry.status = finalStatus;
|
|
717
846
|
best.entry.endedAt = nowTs;
|
|
718
847
|
best.entry.tokens = best.entry.tokens ?? sessionTokens;
|
|
719
848
|
best.entry.cost = best.entry.cost ?? sessionCost;
|
|
@@ -733,16 +862,17 @@ function SubAgentPanel(props) {
|
|
|
733
862
|
for (const [id, entry] of next) {
|
|
734
863
|
if (entry.sessionId !== sid)
|
|
735
864
|
continue;
|
|
736
|
-
if (entry.status !== "running" && entry.status !== "done")
|
|
865
|
+
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested")
|
|
737
866
|
continue;
|
|
738
867
|
// Skip parent session idle — subagent entries belong to child sessions only
|
|
739
868
|
if (sid === props.sessionId)
|
|
740
869
|
continue;
|
|
741
870
|
// For "done" entries (sync tasks completed before session.idle), only backfill tokens/cost
|
|
742
|
-
const alreadySettled = entry.status !== "running";
|
|
871
|
+
const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested";
|
|
872
|
+
const finalStatus = status === "error" ? "error" : settleOnIdle(entry);
|
|
743
873
|
next.set(id, {
|
|
744
874
|
...entry,
|
|
745
|
-
...(alreadySettled ? {} : { status, endedAt: Date.now() }),
|
|
875
|
+
...(alreadySettled ? {} : { status: finalStatus, endedAt: Date.now() }),
|
|
746
876
|
tokens: entry.tokens ?? sessionTokens,
|
|
747
877
|
cost: entry.cost ?? sessionCost,
|
|
748
878
|
model: entry.model ?? sessionModel,
|
|
@@ -759,7 +889,7 @@ function SubAgentPanel(props) {
|
|
|
759
889
|
let best = null;
|
|
760
890
|
// Phase 1: try matching by agent name(agent 名有交集)
|
|
761
891
|
for (const [id, entry] of next) {
|
|
762
|
-
if (entry.status !== "running")
|
|
892
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
763
893
|
continue;
|
|
764
894
|
const eaNorm = normalize(entry.agent);
|
|
765
895
|
if (!eaNorm || !saNorm)
|
|
@@ -774,7 +904,7 @@ function SubAgentPanel(props) {
|
|
|
774
904
|
// fall back to time proximity for entries that have no sessionId yet
|
|
775
905
|
if (!best) {
|
|
776
906
|
for (const [id, entry] of next) {
|
|
777
|
-
if (entry.status !== "running")
|
|
907
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
778
908
|
continue;
|
|
779
909
|
if (entry.sessionId)
|
|
780
910
|
continue;
|
|
@@ -785,8 +915,9 @@ function SubAgentPanel(props) {
|
|
|
785
915
|
}
|
|
786
916
|
if (best) {
|
|
787
917
|
const entry = next.get(best.id);
|
|
918
|
+
const finalStatus = status === "error" ? "error" : settleOnIdle(entry);
|
|
788
919
|
next.set(best.id, {
|
|
789
|
-
...entry, status, endedAt: nowTs,
|
|
920
|
+
...entry, status: finalStatus, endedAt: nowTs,
|
|
790
921
|
tokens: sessionTokens || entry.tokens,
|
|
791
922
|
cost: sessionCost || entry.cost,
|
|
792
923
|
sessionId: sid,
|
|
@@ -1022,7 +1153,7 @@ function SubAgentPanel(props) {
|
|
|
1022
1153
|
status = "running";
|
|
1023
1154
|
}
|
|
1024
1155
|
// Already settled → skip
|
|
1025
|
-
if (exists && exists.status !== "running")
|
|
1156
|
+
if (exists && exists.status !== "running" && exists.status !== "cancel_requested")
|
|
1026
1157
|
continue;
|
|
1027
1158
|
// Running entry with no explicit status improvement from part:
|
|
1028
1159
|
// try message-level heuristics first, then time-based fallback.
|
|
@@ -1081,7 +1212,7 @@ function SubAgentPanel(props) {
|
|
|
1081
1212
|
let changed = false;
|
|
1082
1213
|
const next = new Map(prev);
|
|
1083
1214
|
for (const [id, entry] of next) {
|
|
1084
|
-
if (entry.status !== "running" || !entry.sessionId)
|
|
1215
|
+
if ((entry.status !== "running" && entry.status !== "cancel_requested") || !entry.sessionId)
|
|
1085
1216
|
continue;
|
|
1086
1217
|
try {
|
|
1087
1218
|
const st = props.api.state.session.status(entry.sessionId);
|
|
@@ -1089,8 +1220,11 @@ function SubAgentPanel(props) {
|
|
|
1089
1220
|
continue;
|
|
1090
1221
|
const tokens = readSessionTokens(entry.sessionId);
|
|
1091
1222
|
const cost = readSessionCost(entry.sessionId);
|
|
1223
|
+
const finalStatus = entry.status === "cancel_requested" && entry.abortAccepted
|
|
1224
|
+
? "cancelled"
|
|
1225
|
+
: "done";
|
|
1092
1226
|
next.set(id, {
|
|
1093
|
-
...entry, status:
|
|
1227
|
+
...entry, status: finalStatus, endedAt: Date.now(),
|
|
1094
1228
|
tokens: tokens ?? entry.tokens,
|
|
1095
1229
|
cost: cost ?? entry.cost,
|
|
1096
1230
|
});
|
|
@@ -1196,8 +1330,8 @@ function SubAgentPanel(props) {
|
|
|
1196
1330
|
elapsed: (e.endedAt ?? nowVal) - e.startedAt,
|
|
1197
1331
|
}));
|
|
1198
1332
|
});
|
|
1199
|
-
const doneCount = createMemo(() => entryList().filter((e) => e.status === "done").length);
|
|
1200
|
-
const runningCount = createMemo(() => entryList().filter((e) => e.status === "running").length);
|
|
1333
|
+
const doneCount = createMemo(() => entryList().filter((e) => e.status === "done" || e.status === "cancelled").length);
|
|
1334
|
+
const runningCount = createMemo(() => entryList().filter((e) => e.status === "running" || e.status === "cancel_requested").length);
|
|
1201
1335
|
const errCount = createMemo(() => entryList().filter((e) => e.status === "error").length);
|
|
1202
1336
|
const anyEntry = () => entryList().length > 0;
|
|
1203
1337
|
const totalTokens = createMemo(() => {
|
|
@@ -1341,11 +1475,16 @@ function SubAgentPanel(props) {
|
|
|
1341
1475
|
}, children: _jsxs("span", { style: { fg: hoveredMoreAbove() ? pal().warning : pal().muted }, children: [" ", "\u2191 ", hiddenAbove(), " ", t("scroll.more")] }) }) }), _jsx(For, { each: visibleList(), children: (entry) => {
|
|
1342
1476
|
const isExpanded = () => expanded() === entry.id;
|
|
1343
1477
|
const isRunning = entry.status === "running";
|
|
1478
|
+
const isCancelRequested = entry.status === "cancel_requested";
|
|
1479
|
+
const isCancelled = entry.status === "cancelled";
|
|
1344
1480
|
const isError = entry.status === "error";
|
|
1481
|
+
const isActiveRunning = isRunning || isCancelRequested;
|
|
1345
1482
|
const elapsed = () => (entry.endedAt ?? now()) - entry.startedAt;
|
|
1346
1483
|
const statusDot = () => "\u25cf";
|
|
1347
1484
|
const statusColor = () => {
|
|
1348
|
-
if (
|
|
1485
|
+
if (isCancelled)
|
|
1486
|
+
return pal().muted;
|
|
1487
|
+
if (!isActiveRunning)
|
|
1349
1488
|
return isError ? pal().error : pal().success;
|
|
1350
1489
|
const t = (Math.sin(((now() % 2000) / 2000) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
|
|
1351
1490
|
const a = rgb(pal().muted), b = rgb(pal().warning);
|
|
@@ -1356,13 +1495,13 @@ function SubAgentPanel(props) {
|
|
|
1356
1495
|
const bl = Math.round(a.b + (b.b - a.b) * t);
|
|
1357
1496
|
return "#" + [r, g, bl].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
1358
1497
|
};
|
|
1359
|
-
const timeColor = () =>
|
|
1498
|
+
const timeColor = () => isActiveRunning ? pal().warning : isError ? pal().error : pal().muted;
|
|
1360
1499
|
// Entry label: collapsed shows title only, expanded shows title only too
|
|
1361
1500
|
const tokenText = () => !isExpanded() && entry.tokens !== undefined && entry.tokens > 0
|
|
1362
1501
|
? ` ${fmtTokens(entry.tokens)}`
|
|
1363
1502
|
: "";
|
|
1364
1503
|
const timeText = () => !isExpanded() && (elapsed() >= 2000 || entry.endedAt !== undefined)
|
|
1365
|
-
? fmtDurationShort(elapsed(),
|
|
1504
|
+
? fmtDurationShort(elapsed(), isActiveRunning)
|
|
1366
1505
|
: "";
|
|
1367
1506
|
const suffixW = () => {
|
|
1368
1507
|
let w = 0;
|
|
@@ -1382,27 +1521,44 @@ function SubAgentPanel(props) {
|
|
|
1382
1521
|
const pad = Math.max(0, max - visualWidth(truncated));
|
|
1383
1522
|
return truncated + " ".repeat(pad);
|
|
1384
1523
|
};
|
|
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:
|
|
1386
|
-
|
|
1524
|
+
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: (() => {
|
|
1525
|
+
const cost = entry.cost;
|
|
1526
|
+
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)] })] }));
|
|
1527
|
+
})() }), _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 () => {
|
|
1528
|
+
const sessionId = entry.sessionId;
|
|
1529
|
+
if (!sessionId)
|
|
1530
|
+
return;
|
|
1531
|
+
const result = await copyText(sessionId);
|
|
1532
|
+
if (result.copied) {
|
|
1387
1533
|
props.api.ui.toast({
|
|
1534
|
+
variant: "success",
|
|
1388
1535
|
title: entry.title || entry.agent,
|
|
1389
|
-
message:
|
|
1390
|
-
duration:
|
|
1536
|
+
message: t("session.toast.copied"),
|
|
1537
|
+
duration: 2500,
|
|
1391
1538
|
});
|
|
1539
|
+
return;
|
|
1392
1540
|
}
|
|
1541
|
+
props.api.ui.toast({
|
|
1542
|
+
variant: "warning",
|
|
1543
|
+
title: entry.title || entry.agent,
|
|
1544
|
+
message: `${sessionId}\n\n${t("session.toast.copy_failed")}`,
|
|
1545
|
+
duration: 8000,
|
|
1546
|
+
});
|
|
1393
1547
|
}, 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: (() => {
|
|
1394
|
-
const dismissLabel = () => `- ${t("dismiss.label")}`;
|
|
1395
1548
|
const openPrefix = () => " \u2192 ";
|
|
1396
1549
|
const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : "";
|
|
1397
1550
|
const openW = () => entry.sessionId ? visualWidth(openFull()) : 0;
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1551
|
+
const cancelLabel = () => ` ${t("cancel.label")}`;
|
|
1552
|
+
const dismissLabel = () => ` ${t("dismiss.label")}`;
|
|
1553
|
+
const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0);
|
|
1554
|
+
const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2);
|
|
1555
|
+
return (_jsxs("box", { flexDirection: "row", children: [_jsx(Show, { when: entry.sessionId, children: _jsxs("text", { onMouseOver: () => setHoveredOpen(entry.id), onMouseOut: () => setHoveredOpen(undefined), onMouseUp: () => {
|
|
1400
1556
|
if (entry.sessionId) {
|
|
1401
1557
|
props.api.route.navigate("session", { sessionID: entry.sessionId });
|
|
1402
1558
|
}
|
|
1403
|
-
}, 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:
|
|
1404
|
-
|
|
1405
|
-
|
|
1559
|
+
}, 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: () => {
|
|
1560
|
+
upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" });
|
|
1561
|
+
}, children: _jsx("span", { style: { fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }, children: dismissLabel() }) }) })] }));
|
|
1406
1562
|
})() })] })] }));
|
|
1407
1563
|
} }), _jsx(Show, { when: hiddenBelow() > 0 || (props.sortOrder() === "desc" ? scrollOffset() > 0 : entryList().length > max() && clampedOffset() < entryList().length - max()), children: (() => {
|
|
1408
1564
|
const showMore = hiddenBelow() > 0;
|
|
@@ -1577,7 +1733,7 @@ const tui = async (api) => {
|
|
|
1577
1733
|
}
|
|
1578
1734
|
let count = 0;
|
|
1579
1735
|
for (const [, entry] of entries) {
|
|
1580
|
-
if (entry.status === "running") {
|
|
1736
|
+
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
1581
1737
|
entry.status = "done";
|
|
1582
1738
|
entry.endedAt = Date.now();
|
|
1583
1739
|
count++;
|