opencode-subagent-magazine 1.4.2 → 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.
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.4.2";
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.4.2";
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
- entry.status = targetStatus;
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
- best.entry.status = targetStatus;
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: "done", endedAt: Date.now(),
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 (!isRunning)
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 = () => isRunning ? pal().warning : isError ? pal().error : pal().muted;
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(), isRunning)
1504
+ ? fmtDurationShort(elapsed(), isActiveRunning)
1366
1505
  : "";
1367
1506
  const suffixW = () => {
1368
1507
  let w = 0;
@@ -1382,30 +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: 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: (() => {
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: (() => {
1386
1525
  const cost = entry.cost;
1387
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)] })] }));
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
- if (entry.sessionId) {
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) {
1390
1533
  props.api.ui.toast({
1534
+ variant: "success",
1391
1535
  title: entry.title || entry.agent,
1392
- message: `${entry.sessionId}\n\n${t("session.toast.copy")}`,
1393
- duration: 8000,
1536
+ message: t("session.toast.copied"),
1537
+ duration: 2500,
1394
1538
  });
1539
+ return;
1395
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
+ });
1396
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: (() => {
1397
- const dismissLabel = () => `- ${t("dismiss.label")}`;
1398
1548
  const openPrefix = () => " \u2192 ";
1399
1549
  const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : "";
1400
1550
  const openW = () => entry.sessionId ? visualWidth(openFull()) : 0;
1401
- const spacerW = () => Math.max(1, panelWidth() - openW() - visualWidth(dismissLabel()) - 2 /* indent */);
1402
- 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: () => {
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: () => {
1403
1556
  if (entry.sessionId) {
1404
1557
  props.api.route.navigate("session", { sessionID: entry.sessionId });
1405
1558
  }
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: _jsxs(_Fragment, { children: [_jsx("text", { style: { fg: pal().muted }, children: " ".repeat(spacerW()) }), _jsx("text", { onMouseOver: () => setHoveredDismiss(entry.id), onMouseOut: () => setHoveredDismiss(undefined), onMouseUp: () => {
1407
- upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" });
1408
- }, children: _jsx("span", { style: { fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }, children: dismissLabel() }) })] }) })] }));
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() }) }) })] }));
1409
1562
  })() })] })] }));
1410
1563
  } }), _jsx(Show, { when: hiddenBelow() > 0 || (props.sortOrder() === "desc" ? scrollOffset() > 0 : entryList().length > max() && clampedOffset() < entryList().length - max()), children: (() => {
1411
1564
  const showMore = hiddenBelow() > 0;
@@ -1580,7 +1733,7 @@ const tui = async (api) => {
1580
1733
  }
1581
1734
  let count = 0;
1582
1735
  for (const [, entry] of entries) {
1583
- if (entry.status === "running") {
1736
+ if (entry.status === "running" || entry.status === "cancel_requested") {
1584
1737
  entry.status = "done";
1585
1738
  entry.endedAt = Date.now();
1586
1739
  count++;