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/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 = (key) => I18N[props.lang()][key] ?? key;
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
- entry.status = targetStatus;
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
- best.entry.status = targetStatus;
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: "done", endedAt: Date.now(),
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 (!isRunning)
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 = () => isRunning ? pal().warning : isError ? pal().error : pal().muted;
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(), isRunning)
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: 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: (() => {
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
- if (entry.sessionId) {
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: `${entry.sessionId}\n\n${t("session.toast.copy")}`,
1393
- duration: 8000,
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 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: () => {
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: _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() }) })] }) })] }));
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 = stored === "zh" || stored === "en" ? stored : detectLang();
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: l === "zh" ? "语言: 中文" : "Language: English",
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 = (k) => I18N[lang()][k] ?? k;
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 = (k) => I18N[lang()][k] ?? k;
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 = (k) => I18N[signals.lang()][k] ?? k;
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").replace("{n}", String(days));
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 = (k) => I18N[signals.lang()][k] ?? k;
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").replace("{n}", String(runningCount))
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").replace("{n}", String(count));
1743
+ const msg = t("clear.done", { n: count });
1706
1744
  api.ui.toast({ message: msg });
1707
1745
  }
1708
1746
  catch { }