opencode-subagent-magazine 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/clipboard.d.ts +7 -0
- package/dist/clipboard.js +85 -0
- package/dist/i18n.d.ts +75 -0
- package/dist/i18n.js +249 -0
- package/dist/index.js +176 -138
- package/dist/tui.js +564 -172
- package/package.json +2 -1
- package/src/_version.ts +1 -1
- package/src/clipboard.ts +134 -0
- package/src/i18n.ts +261 -0
- package/src/index.tsx +202 -158
package/src/index.tsx
CHANGED
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
For,
|
|
21
21
|
} from "solid-js"
|
|
22
22
|
import { PLUGIN_VERSION } from "./_version"
|
|
23
|
+
import { copyText } from "./clipboard"
|
|
24
|
+
import { LangCode, LANG_META, createT, detectLang } from "./i18n"
|
|
23
25
|
|
|
24
26
|
// ===================================================================
|
|
25
27
|
// Types
|
|
26
28
|
// ===================================================================
|
|
27
29
|
|
|
28
|
-
type SubStatus = "running" | "done" | "error"
|
|
30
|
+
type SubStatus = "running" | "done" | "error" | "cancel_requested" | "cancelled"
|
|
29
31
|
|
|
30
32
|
interface SubEntry {
|
|
31
33
|
id: string
|
|
@@ -42,108 +44,18 @@ interface SubEntry {
|
|
|
42
44
|
model?: string
|
|
43
45
|
todoTotal?: number
|
|
44
46
|
todoDone?: number
|
|
47
|
+
cancelRequestedAt?: number
|
|
48
|
+
abortAccepted?: boolean
|
|
49
|
+
cancelReason?: "manual"
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
type Lang =
|
|
52
|
+
type Lang = LangCode
|
|
48
53
|
type SortOrder = "desc" | "asc"
|
|
49
54
|
type ScrollMode = "wheel" | "click"
|
|
50
55
|
|
|
51
56
|
/** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
|
|
52
57
|
const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
|
|
53
58
|
|
|
54
|
-
// ===================================================================
|
|
55
|
-
// i18n
|
|
56
|
-
// ===================================================================
|
|
57
|
-
|
|
58
|
-
const I18N: Record<Lang, Record<string, string>> = {
|
|
59
|
-
zh: {
|
|
60
|
-
"panel.title": "子代理",
|
|
61
|
-
"status.none": "暂无子代理",
|
|
62
|
-
"agent.label": "代理",
|
|
63
|
-
"status.label": "状态",
|
|
64
|
-
"time.label": "耗时",
|
|
65
|
-
"tokens.label": "上下文",
|
|
66
|
-
"error.label": "错误",
|
|
67
|
-
"model.label": "模型",
|
|
68
|
-
"todo.label": "进度",
|
|
69
|
-
"session.label": "会话 ID",
|
|
70
|
-
"session.toast.copy": "可手动复制上方 ID",
|
|
71
|
-
"open.label": "进入会话",
|
|
72
|
-
"cost.label": "费用",
|
|
73
|
-
"scroll.more": "更多",
|
|
74
|
-
"scroll.top": "回顶",
|
|
75
|
-
"scroll.bottom": "回底",
|
|
76
|
-
"dismiss.label": "标记完成",
|
|
77
|
-
"status.running": "运行中",
|
|
78
|
-
"status.done": "已完成",
|
|
79
|
-
"status.error": "错误",
|
|
80
|
-
"order.desc": "降序(最新在前)",
|
|
81
|
-
"order.asc": "升序(最早在前)",
|
|
82
|
-
"scroll.wheel": "滚轮翻页",
|
|
83
|
-
"scroll.click": "点击翻页",
|
|
84
|
-
"ttl.label": "清理周期",
|
|
85
|
-
"ttl.3d": "3 天",
|
|
86
|
-
"ttl.7d": "7 天",
|
|
87
|
-
"ttl.14d": "14 天",
|
|
88
|
-
"ttl.30d": "30 天",
|
|
89
|
-
"ttl.unlimited": "无期限",
|
|
90
|
-
"ttl.toast": "清理周期已设为 {n} 天",
|
|
91
|
-
"ttl.toast_unlimited": "清理周期已设为无期限",
|
|
92
|
-
"clear.title": "确认清除",
|
|
93
|
-
"clear.prompt": "确定清除当前会话所有子代理记录?此操作不可撤销。",
|
|
94
|
-
"clear.prompt_running": "当前有 {n} 个运行中的子代理,清除后将不可恢复。确定继续?",
|
|
95
|
-
"clear.done": "已清除 {n} 条子代理记录",
|
|
96
|
-
"clear.empty": "当前会话无子代理记录",
|
|
97
|
-
},
|
|
98
|
-
en: {
|
|
99
|
-
"panel.title": "SubAgent",
|
|
100
|
-
"status.none": "No sub-agents yet",
|
|
101
|
-
"agent.label": "agent",
|
|
102
|
-
"status.label": "status",
|
|
103
|
-
"time.label": "time",
|
|
104
|
-
"tokens.label": "tokens",
|
|
105
|
-
"error.label": "error",
|
|
106
|
-
"model.label": "model",
|
|
107
|
-
"todo.label": "todo",
|
|
108
|
-
"session.label": "session ID",
|
|
109
|
-
"session.toast.copy": "Copy the ID above manually",
|
|
110
|
-
"open.label": "Open session",
|
|
111
|
-
"cost.label": "cost",
|
|
112
|
-
"scroll.more": "more",
|
|
113
|
-
"scroll.top": "Top",
|
|
114
|
-
"scroll.bottom": "Bottom",
|
|
115
|
-
"dismiss.label": "dismiss",
|
|
116
|
-
"status.running": "running",
|
|
117
|
-
"status.done": "done",
|
|
118
|
-
"status.error": "error",
|
|
119
|
-
"order.desc": "Desc (newest first)",
|
|
120
|
-
"order.asc": "Asc (oldest first)",
|
|
121
|
-
"scroll.wheel": "Wheel Scroll",
|
|
122
|
-
"scroll.click": "Click Scroll",
|
|
123
|
-
"ttl.label": "TTL (Time to Live)",
|
|
124
|
-
"ttl.3d": "3 days",
|
|
125
|
-
"ttl.7d": "7 days",
|
|
126
|
-
"ttl.14d": "14 days",
|
|
127
|
-
"ttl.30d": "30 days",
|
|
128
|
-
"ttl.unlimited": "Never",
|
|
129
|
-
"ttl.toast": "TTL set to {n} days",
|
|
130
|
-
"ttl.toast_unlimited": "TTL set to Never",
|
|
131
|
-
"clear.title": "Confirm",
|
|
132
|
-
"clear.prompt": "Clear all sub-agent records for this session? This cannot be undone.",
|
|
133
|
-
"clear.prompt_running": "{n} sub-agent(s) are still running. Clearing will discard them permanently. Continue?",
|
|
134
|
-
"clear.done": "Cleared {n} sub-agent record(s)",
|
|
135
|
-
"clear.empty": "No sub-agent records in this session",
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
declare const process: { env: Record<string, string | undefined> } | undefined
|
|
140
|
-
|
|
141
|
-
function detectLang(): Lang {
|
|
142
|
-
const env = process?.env?.OPENCODE_LANG ?? process?.env?.LANG ?? ""
|
|
143
|
-
if (env.startsWith("zh")) return "zh"
|
|
144
|
-
return "en"
|
|
145
|
-
}
|
|
146
|
-
|
|
147
59
|
// ===================================================================
|
|
148
60
|
// Helpers — visual width
|
|
149
61
|
// ===================================================================
|
|
@@ -304,7 +216,7 @@ function SubAgentPanel(props: {
|
|
|
304
216
|
scrollMode: () => ScrollMode
|
|
305
217
|
sessionId: string
|
|
306
218
|
}): JSX.Element {
|
|
307
|
-
const t = (
|
|
219
|
+
const t = createT(() => props.lang())
|
|
308
220
|
|
|
309
221
|
// ── session data (single-key, true deletion on cleanup) ──
|
|
310
222
|
const SESSION_DATA_KEY = `${KV_PREFIX}.session_data`
|
|
@@ -449,7 +361,7 @@ function SubAgentPanel(props: {
|
|
|
449
361
|
let needsImmediateFlush = false
|
|
450
362
|
for (const [id, entry] of next) {
|
|
451
363
|
const prevEntry = prev.get(id)
|
|
452
|
-
if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error")) {
|
|
364
|
+
if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error" || entry.status === "cancelled")) {
|
|
453
365
|
needsImmediateFlush = true
|
|
454
366
|
break
|
|
455
367
|
}
|
|
@@ -497,6 +409,7 @@ function SubAgentPanel(props: {
|
|
|
497
409
|
)
|
|
498
410
|
const [hoveredOpen, setHoveredOpen] = createSignal<string | undefined>(undefined)
|
|
499
411
|
const [hoveredDismiss, setHoveredDismiss] = createSignal<string | undefined>(undefined)
|
|
412
|
+
const [hoveredCancel, setHoveredCancel] = createSignal<string | undefined>(undefined)
|
|
500
413
|
const [hoveredTop, setHoveredTop] = createSignal(false)
|
|
501
414
|
const [hoveredMoreAbove, setHoveredMoreAbove] = createSignal(false)
|
|
502
415
|
const [hoveredMoreBelow, setHoveredMoreBelow] = createSignal(false)
|
|
@@ -602,7 +515,7 @@ function SubAgentPanel(props: {
|
|
|
602
515
|
const next = new Map(prev)
|
|
603
516
|
const nowTs = Date.now()
|
|
604
517
|
const e = partial.status
|
|
605
|
-
const ended = e === "done" || e === "error"
|
|
518
|
+
const ended = e === "done" || e === "error" || e === "cancelled"
|
|
606
519
|
next.set(partial.id, {
|
|
607
520
|
...(existing ?? { startedAt: nowTs }),
|
|
608
521
|
...partial,
|
|
@@ -613,6 +526,108 @@ function SubAgentPanel(props: {
|
|
|
613
526
|
})
|
|
614
527
|
}
|
|
615
528
|
|
|
529
|
+
// ── cancel helpers ──
|
|
530
|
+
const isDescendantOf = (childId: string, rootId: string): boolean => {
|
|
531
|
+
const visited = new Set<string>()
|
|
532
|
+
try {
|
|
533
|
+
let current = props.api.state.session.get(childId) as any
|
|
534
|
+
while (current?.parentID) {
|
|
535
|
+
if (visited.has(current.id)) return false
|
|
536
|
+
visited.add(current.id)
|
|
537
|
+
if (current.parentID === rootId) return true
|
|
538
|
+
current = props.api.state.session.get(current.parentID) as any
|
|
539
|
+
}
|
|
540
|
+
} catch {}
|
|
541
|
+
return false
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const settleOnIdle = (entry: SubEntry): SubStatus => {
|
|
545
|
+
if (entry.status === "cancel_requested" && entry.abortAccepted) return "cancelled"
|
|
546
|
+
return "done"
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const cancelEntry = async (entry: SubEntry) => {
|
|
550
|
+
const childId = entry.sessionId
|
|
551
|
+
if (!childId) {
|
|
552
|
+
props.api.ui.toast({
|
|
553
|
+
title: entry.title || entry.agent,
|
|
554
|
+
message: t("cancel.label") + ": " + t("cancel.no_session"),
|
|
555
|
+
})
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const child = props.api.state.session.get(childId) as any
|
|
561
|
+
if (!child?.parentID) {
|
|
562
|
+
props.api.ui.toast({
|
|
563
|
+
title: entry.title || entry.agent,
|
|
564
|
+
message: t("cancel.label") + ": " + t("cancel.not_child"),
|
|
565
|
+
})
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
} catch {
|
|
569
|
+
props.api.ui.toast({
|
|
570
|
+
title: entry.title || entry.agent,
|
|
571
|
+
message: t("cancel.label") + ": " + t("cancel.read_error"),
|
|
572
|
+
})
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!isDescendantOf(childId, props.sessionId)) {
|
|
577
|
+
props.api.ui.toast({
|
|
578
|
+
title: entry.title || entry.agent,
|
|
579
|
+
message: t("cancel.label") + ": " + t("cancel.outside_tree"),
|
|
580
|
+
})
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const st = props.api.state.session.status(childId)
|
|
586
|
+
if (st?.type !== "busy") {
|
|
587
|
+
const tokens = readSessionTokens(childId)
|
|
588
|
+
const cost = readSessionCost(childId)
|
|
589
|
+
upsertEntry({
|
|
590
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
591
|
+
status: "done", sessionId: entry.sessionId,
|
|
592
|
+
tokens, cost,
|
|
593
|
+
})
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
props.api.ui.toast({
|
|
598
|
+
title: entry.title || entry.agent,
|
|
599
|
+
message: t("cancel.label") + ": " + t("cancel.status_error"),
|
|
600
|
+
})
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
upsertEntry({
|
|
605
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
606
|
+
status: "cancel_requested", sessionId: entry.sessionId,
|
|
607
|
+
cancelRequestedAt: Date.now(), abortAccepted: false, cancelReason: "manual",
|
|
608
|
+
} as any)
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
await (props.api as any).client.session.abort({ sessionID: childId })
|
|
612
|
+
upsertEntry({
|
|
613
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
614
|
+
status: "cancel_requested", sessionId: entry.sessionId,
|
|
615
|
+
abortAccepted: true,
|
|
616
|
+
} as any)
|
|
617
|
+
props.api.ui.toast({ message: t("cancel.label") + ": " + t("cancel.sent") })
|
|
618
|
+
} catch (err) {
|
|
619
|
+
upsertEntry({
|
|
620
|
+
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
621
|
+
status: "error", sessionId: entry.sessionId,
|
|
622
|
+
error: String(err),
|
|
623
|
+
})
|
|
624
|
+
props.api.ui.toast({
|
|
625
|
+
title: entry.title || entry.agent,
|
|
626
|
+
message: t("cancel.label") + ": " + t("cancel.failed"),
|
|
627
|
+
})
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
616
631
|
// ── event handlers ──
|
|
617
632
|
const handlePartUpdated = (event: unknown) => {
|
|
618
633
|
const e = event as Record<string, unknown>
|
|
@@ -728,10 +743,11 @@ function SubAgentPanel(props: {
|
|
|
728
743
|
targetStatus: SubStatus,
|
|
729
744
|
nowTs: number,
|
|
730
745
|
): boolean => {
|
|
731
|
-
// 精确匹配:sessionId 对得上 + 状态为 running
|
|
746
|
+
// 精确匹配:sessionId 对得上 + 状态为 running / cancel_requested
|
|
732
747
|
for (const [, entry] of entriesMap) {
|
|
733
|
-
if (entry.sessionId === targetSid && entry.status === "running") {
|
|
734
|
-
|
|
748
|
+
if (entry.sessionId === targetSid && (entry.status === "running" || entry.status === "cancel_requested")) {
|
|
749
|
+
const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(entry)
|
|
750
|
+
entry.status = finalStatus
|
|
735
751
|
entry.endedAt = nowTs
|
|
736
752
|
entry.tokens = entry.tokens ?? sessionTokens
|
|
737
753
|
entry.cost = entry.cost ?? sessionCost
|
|
@@ -742,13 +758,13 @@ function SubAgentPanel(props: {
|
|
|
742
758
|
return true
|
|
743
759
|
}
|
|
744
760
|
}
|
|
745
|
-
// 回退:sessionId 未关联但 agent 名匹配 + 状态为 running
|
|
761
|
+
// 回退:sessionId 未关联但 agent 名匹配 + 状态为 running / cancel_requested
|
|
746
762
|
if (sessionAgent) {
|
|
747
763
|
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9-]/g, "")
|
|
748
764
|
const saNorm = normalize(sessionAgent)
|
|
749
765
|
let best: { entry: SubEntry; gap: number } | null = null
|
|
750
766
|
for (const [, entry] of entriesMap) {
|
|
751
|
-
if (entry.status !== "running") continue
|
|
767
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested") continue
|
|
752
768
|
const eaNorm = normalize(entry.agent)
|
|
753
769
|
if (!eaNorm || !saNorm) continue
|
|
754
770
|
if (!eaNorm.includes(saNorm) && !saNorm.includes(eaNorm)) continue
|
|
@@ -757,14 +773,15 @@ function SubAgentPanel(props: {
|
|
|
757
773
|
}
|
|
758
774
|
if (!best) {
|
|
759
775
|
for (const [, entry] of entriesMap) {
|
|
760
|
-
if (entry.status !== "running") continue
|
|
776
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested") continue
|
|
761
777
|
if (entry.sessionId) continue
|
|
762
778
|
const gap = nowTs - (entry.startedAt || 0)
|
|
763
779
|
if (!best || gap > best.gap) best = { entry, gap }
|
|
764
780
|
}
|
|
765
781
|
}
|
|
766
782
|
if (best) {
|
|
767
|
-
|
|
783
|
+
const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(best.entry)
|
|
784
|
+
best.entry.status = finalStatus
|
|
768
785
|
best.entry.endedAt = nowTs
|
|
769
786
|
best.entry.tokens = best.entry.tokens ?? sessionTokens
|
|
770
787
|
best.entry.cost = best.entry.cost ?? sessionCost
|
|
@@ -784,14 +801,15 @@ function SubAgentPanel(props: {
|
|
|
784
801
|
const next = new Map(prev)
|
|
785
802
|
for (const [id, entry] of next) {
|
|
786
803
|
if (entry.sessionId !== sid) continue
|
|
787
|
-
if (entry.status !== "running" && entry.status !== "done") continue
|
|
804
|
+
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested") continue
|
|
788
805
|
// Skip parent session idle — subagent entries belong to child sessions only
|
|
789
806
|
if (sid === props.sessionId) continue
|
|
790
807
|
// For "done" entries (sync tasks completed before session.idle), only backfill tokens/cost
|
|
791
|
-
const alreadySettled = entry.status !== "running"
|
|
808
|
+
const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested"
|
|
809
|
+
const finalStatus = status === "error" ? "error" : settleOnIdle(entry)
|
|
792
810
|
next.set(id, {
|
|
793
811
|
...entry,
|
|
794
|
-
...(alreadySettled ? {} : { status, endedAt: Date.now() }),
|
|
812
|
+
...(alreadySettled ? {} : { status: finalStatus, endedAt: Date.now() }),
|
|
795
813
|
tokens: entry.tokens ?? sessionTokens,
|
|
796
814
|
cost: entry.cost ?? sessionCost,
|
|
797
815
|
model: entry.model ?? sessionModel,
|
|
@@ -809,7 +827,7 @@ function SubAgentPanel(props: {
|
|
|
809
827
|
|
|
810
828
|
// Phase 1: try matching by agent name(agent 名有交集)
|
|
811
829
|
for (const [id, entry] of next) {
|
|
812
|
-
if (entry.status !== "running") continue
|
|
830
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested") continue
|
|
813
831
|
const eaNorm = normalize(entry.agent)
|
|
814
832
|
if (!eaNorm || !saNorm) continue
|
|
815
833
|
if (!eaNorm.includes(saNorm) && !saNorm.includes(eaNorm)) continue
|
|
@@ -821,7 +839,7 @@ function SubAgentPanel(props: {
|
|
|
821
839
|
// fall back to time proximity for entries that have no sessionId yet
|
|
822
840
|
if (!best) {
|
|
823
841
|
for (const [id, entry] of next) {
|
|
824
|
-
if (entry.status !== "running") continue
|
|
842
|
+
if (entry.status !== "running" && entry.status !== "cancel_requested") continue
|
|
825
843
|
if (entry.sessionId) continue
|
|
826
844
|
const gap = nowTs - (entry.startedAt || 0)
|
|
827
845
|
if (!best || gap > best.gap) best = { id, gap }
|
|
@@ -830,8 +848,9 @@ function SubAgentPanel(props: {
|
|
|
830
848
|
|
|
831
849
|
if (best) {
|
|
832
850
|
const entry = next.get(best.id)!
|
|
851
|
+
const finalStatus = status === "error" ? "error" : settleOnIdle(entry)
|
|
833
852
|
next.set(best.id, {
|
|
834
|
-
...entry, status, endedAt: nowTs,
|
|
853
|
+
...entry, status: finalStatus, endedAt: nowTs,
|
|
835
854
|
tokens: sessionTokens || entry.tokens,
|
|
836
855
|
cost: sessionCost || entry.cost,
|
|
837
856
|
sessionId: sid,
|
|
@@ -1064,7 +1083,7 @@ function SubAgentPanel(props: {
|
|
|
1064
1083
|
}
|
|
1065
1084
|
|
|
1066
1085
|
// Already settled → skip
|
|
1067
|
-
if (exists && exists.status !== "running") continue
|
|
1086
|
+
if (exists && exists.status !== "running" && exists.status !== "cancel_requested") continue
|
|
1068
1087
|
// Running entry with no explicit status improvement from part:
|
|
1069
1088
|
// try message-level heuristics first, then time-based fallback.
|
|
1070
1089
|
if (exists && status === "running") {
|
|
@@ -1121,14 +1140,17 @@ function SubAgentPanel(props: {
|
|
|
1121
1140
|
let changed = false
|
|
1122
1141
|
const next = new Map(prev)
|
|
1123
1142
|
for (const [id, entry] of next) {
|
|
1124
|
-
if (entry.status !== "running" || !entry.sessionId) continue
|
|
1143
|
+
if ((entry.status !== "running" && entry.status !== "cancel_requested") || !entry.sessionId) continue
|
|
1125
1144
|
try {
|
|
1126
1145
|
const st = props.api.state.session.status(entry.sessionId)
|
|
1127
1146
|
if (!st || st.type !== "idle") continue
|
|
1128
1147
|
const tokens = readSessionTokens(entry.sessionId)
|
|
1129
1148
|
const cost = readSessionCost(entry.sessionId)
|
|
1149
|
+
const finalStatus = entry.status === "cancel_requested" && entry.abortAccepted
|
|
1150
|
+
? "cancelled" as SubStatus
|
|
1151
|
+
: "done" as SubStatus
|
|
1130
1152
|
next.set(id, {
|
|
1131
|
-
...entry, status:
|
|
1153
|
+
...entry, status: finalStatus, endedAt: Date.now(),
|
|
1132
1154
|
tokens: tokens ?? entry.tokens,
|
|
1133
1155
|
cost: cost ?? entry.cost,
|
|
1134
1156
|
})
|
|
@@ -1229,8 +1251,8 @@ function SubAgentPanel(props: {
|
|
|
1229
1251
|
}))
|
|
1230
1252
|
})
|
|
1231
1253
|
|
|
1232
|
-
const doneCount = createMemo(() => entryList().filter((e) => e.status === "done").length)
|
|
1233
|
-
const runningCount = createMemo(() => entryList().filter((e) => e.status === "running").length)
|
|
1254
|
+
const doneCount = createMemo(() => entryList().filter((e) => e.status === "done" || e.status === "cancelled").length)
|
|
1255
|
+
const runningCount = createMemo(() => entryList().filter((e) => e.status === "running" || e.status === "cancel_requested").length)
|
|
1234
1256
|
const errCount = createMemo(() => entryList().filter((e) => e.status === "error").length)
|
|
1235
1257
|
const anyEntry = () => entryList().length > 0
|
|
1236
1258
|
|
|
@@ -1421,12 +1443,16 @@ function SubAgentPanel(props: {
|
|
|
1421
1443
|
{(entry) => {
|
|
1422
1444
|
const isExpanded = () => expanded() === entry.id
|
|
1423
1445
|
const isRunning = entry.status === "running"
|
|
1446
|
+
const isCancelRequested = entry.status === "cancel_requested"
|
|
1447
|
+
const isCancelled = entry.status === "cancelled"
|
|
1424
1448
|
const isError = entry.status === "error"
|
|
1449
|
+
const isActiveRunning = isRunning || isCancelRequested
|
|
1425
1450
|
const elapsed = () => (entry.endedAt ?? now()) - entry.startedAt
|
|
1426
1451
|
|
|
1427
1452
|
const statusDot = () => "\u25cf"
|
|
1428
1453
|
const statusColor = () => {
|
|
1429
|
-
if (
|
|
1454
|
+
if (isCancelled) return pal().muted
|
|
1455
|
+
if (!isActiveRunning) return isError ? pal().error : pal().success
|
|
1430
1456
|
const t = (Math.sin(((now() % 2000) / 2000) * Math.PI * 2 - Math.PI / 2) + 1) / 2
|
|
1431
1457
|
const a = rgb(pal().muted), b = rgb(pal().warning)
|
|
1432
1458
|
if (!a || !b) return pal().warning
|
|
@@ -1437,7 +1463,7 @@ function SubAgentPanel(props: {
|
|
|
1437
1463
|
}
|
|
1438
1464
|
|
|
1439
1465
|
const timeColor = () =>
|
|
1440
|
-
|
|
1466
|
+
isActiveRunning ? pal().warning : isError ? pal().error : pal().muted
|
|
1441
1467
|
|
|
1442
1468
|
// Entry label: collapsed shows title only, expanded shows title only too
|
|
1443
1469
|
const tokenText = () =>
|
|
@@ -1446,7 +1472,7 @@ function SubAgentPanel(props: {
|
|
|
1446
1472
|
: ""
|
|
1447
1473
|
const timeText = () =>
|
|
1448
1474
|
!isExpanded() && (elapsed() >= 2000 || entry.endedAt !== undefined)
|
|
1449
|
-
? fmtDurationShort(elapsed(),
|
|
1475
|
+
? fmtDurationShort(elapsed(), isActiveRunning)
|
|
1450
1476
|
: ""
|
|
1451
1477
|
const suffixW = () => {
|
|
1452
1478
|
let w = 0
|
|
@@ -1499,8 +1525,8 @@ function SubAgentPanel(props: {
|
|
|
1499
1525
|
{" "}
|
|
1500
1526
|
<span style={{ fg: pal().primary }}>{t("status.label")}: </span>
|
|
1501
1527
|
<span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("status.label")))}</span>
|
|
1502
|
-
<span style={{ fg:
|
|
1503
|
-
{
|
|
1528
|
+
<span style={{ fg: isActiveRunning ? pal().warning : isCancelled ? pal().muted : isError ? pal().error : pal().success }}>
|
|
1529
|
+
{isActiveRunning ? t("status.running") : isCancelled ? t("status.cancelled") : isError ? t("status.error") : t("status.done")}
|
|
1504
1530
|
</span>
|
|
1505
1531
|
</text>
|
|
1506
1532
|
<Show when={elapsed() >= 2000 || entry.endedAt !== undefined}>
|
|
@@ -1509,7 +1535,7 @@ function SubAgentPanel(props: {
|
|
|
1509
1535
|
<span style={{ fg: pal().primary }}>{t("time.label")}: </span>
|
|
1510
1536
|
<span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("time.label")))}</span>
|
|
1511
1537
|
<span style={{ fg: pal().muted }}>
|
|
1512
|
-
{fmtDurationShort(elapsed(),
|
|
1538
|
+
{fmtDurationShort(elapsed(), isActiveRunning)}
|
|
1513
1539
|
</span>
|
|
1514
1540
|
</text>
|
|
1515
1541
|
</Show>
|
|
@@ -1560,14 +1586,28 @@ function SubAgentPanel(props: {
|
|
|
1560
1586
|
</Show>
|
|
1561
1587
|
<Show when={entry.sessionId}>
|
|
1562
1588
|
<text
|
|
1563
|
-
onMouseUp={() => {
|
|
1564
|
-
|
|
1589
|
+
onMouseUp={async () => {
|
|
1590
|
+
const sessionId = entry.sessionId
|
|
1591
|
+
if (!sessionId) return
|
|
1592
|
+
|
|
1593
|
+
const result = await copyText(sessionId)
|
|
1594
|
+
|
|
1595
|
+
if (result.copied) {
|
|
1565
1596
|
props.api.ui.toast({
|
|
1597
|
+
variant: "success",
|
|
1566
1598
|
title: entry.title || entry.agent,
|
|
1567
|
-
message:
|
|
1568
|
-
duration:
|
|
1599
|
+
message: t("session.toast.copied"),
|
|
1600
|
+
duration: 2500,
|
|
1569
1601
|
})
|
|
1602
|
+
return
|
|
1570
1603
|
}
|
|
1604
|
+
|
|
1605
|
+
props.api.ui.toast({
|
|
1606
|
+
variant: "warning",
|
|
1607
|
+
title: entry.title || entry.agent,
|
|
1608
|
+
message: `${sessionId}\n\n${t("session.toast.copy_failed")}`,
|
|
1609
|
+
duration: 8000,
|
|
1610
|
+
})
|
|
1571
1611
|
}}
|
|
1572
1612
|
>
|
|
1573
1613
|
{" "}
|
|
@@ -1577,19 +1617,19 @@ function SubAgentPanel(props: {
|
|
|
1577
1617
|
<span style={{ fg: pal().warning }}> ⎘</span>
|
|
1578
1618
|
</text>
|
|
1579
1619
|
</Show>
|
|
1580
|
-
{/* 进入会话 +
|
|
1620
|
+
{/* 进入会话 + 取消任务 + 仅清除显示:同排左右两端 */}
|
|
1581
1621
|
<Show when={entry.sessionId || isRunning}>
|
|
1582
1622
|
{(() => {
|
|
1583
|
-
const dismissLabel = () => `- ${t("dismiss.label")}`
|
|
1584
1623
|
const openPrefix = () => " \u2192 "
|
|
1585
1624
|
const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : ""
|
|
1586
1625
|
const openW = () => entry.sessionId ? visualWidth(openFull()) : 0
|
|
1587
|
-
const
|
|
1626
|
+
const cancelLabel = () => ` ${t("cancel.label")}`
|
|
1627
|
+
const dismissLabel = () => ` ${t("dismiss.label")}`
|
|
1628
|
+
const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0)
|
|
1629
|
+
const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2)
|
|
1588
1630
|
return (
|
|
1589
1631
|
<box flexDirection="row">
|
|
1590
|
-
<Show when={entry.sessionId}
|
|
1591
|
-
fallback={<text>{" "}</text>}
|
|
1592
|
-
>
|
|
1632
|
+
<Show when={entry.sessionId}>
|
|
1593
1633
|
<text
|
|
1594
1634
|
onMouseOver={() => setHoveredOpen(entry.id)}
|
|
1595
1635
|
onMouseOut={() => setHoveredOpen(undefined)}
|
|
@@ -1603,19 +1643,26 @@ function SubAgentPanel(props: {
|
|
|
1603
1643
|
<span style={{ fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }}>{t("open.label")}</span>
|
|
1604
1644
|
</text>
|
|
1605
1645
|
</Show>
|
|
1646
|
+
<text style={{ fg: pal().muted }}>{" ".repeat(spacerW())}</text>
|
|
1647
|
+
<Show when={isRunning && entry.sessionId}>
|
|
1648
|
+
<text
|
|
1649
|
+
onMouseOver={() => setHoveredCancel(entry.id)}
|
|
1650
|
+
onMouseOut={() => setHoveredCancel(undefined)}
|
|
1651
|
+
onMouseUp={() => cancelEntry(entry)}
|
|
1652
|
+
>
|
|
1653
|
+
<span style={{ fg: hoveredCancel() === entry.id ? pal().warning : pal().error }}>{cancelLabel()}</span>
|
|
1654
|
+
</text>
|
|
1655
|
+
</Show>
|
|
1606
1656
|
<Show when={isRunning}>
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
<span style={{ fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }}>{dismissLabel()}</span>
|
|
1617
|
-
</text>
|
|
1618
|
-
</>
|
|
1657
|
+
<text
|
|
1658
|
+
onMouseOver={() => setHoveredDismiss(entry.id)}
|
|
1659
|
+
onMouseOut={() => setHoveredDismiss(undefined)}
|
|
1660
|
+
onMouseUp={() => {
|
|
1661
|
+
upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" })
|
|
1662
|
+
}}
|
|
1663
|
+
>
|
|
1664
|
+
<span style={{ fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }}>{dismissLabel()}</span>
|
|
1665
|
+
</text>
|
|
1619
1666
|
</Show>
|
|
1620
1667
|
</box>
|
|
1621
1668
|
)
|
|
@@ -1732,7 +1779,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1732
1779
|
// ── language ──
|
|
1733
1780
|
const stored = String(api.kv.get(`${KV_PREFIX}.lang`, ""))
|
|
1734
1781
|
const initialLang: Lang =
|
|
1735
|
-
|
|
1782
|
+
LANG_META.some((m) => m.code === stored) ? (stored as Lang) : detectLang()
|
|
1736
1783
|
const [lang, setLang] = createSignal<Lang>(initialLang)
|
|
1737
1784
|
const [maxEntries, setMaxEntries] = createSignal(
|
|
1738
1785
|
parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10
|
|
@@ -1759,16 +1806,13 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1759
1806
|
dialog?.replace(() => (
|
|
1760
1807
|
<api.ui.DialogSelect
|
|
1761
1808
|
title="Language / 语言"
|
|
1762
|
-
options={
|
|
1763
|
-
{ title: "中文", value: "zh" },
|
|
1764
|
-
{ title: "English", value: "en" },
|
|
1765
|
-
]}
|
|
1809
|
+
options={LANG_META.map((m) => ({ title: m.label, value: m.code }))}
|
|
1766
1810
|
onSelect={(opt) => {
|
|
1767
1811
|
const l = opt.value as Lang
|
|
1768
1812
|
setLang(l)
|
|
1769
1813
|
api.kv.set(`${KV_PREFIX}.lang`, l)
|
|
1770
1814
|
api.ui.toast({
|
|
1771
|
-
message:
|
|
1815
|
+
message: "Language: " + (LANG_META.find((m) => m.code === l)?.label ?? l),
|
|
1772
1816
|
})
|
|
1773
1817
|
dialog?.clear()
|
|
1774
1818
|
}}
|
|
@@ -1782,7 +1826,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1782
1826
|
description: "Set sub-agent entry sort order (desc / asc)",
|
|
1783
1827
|
slash: { name: "subagent-order" },
|
|
1784
1828
|
onSelect: (dialog) => {
|
|
1785
|
-
const t = (
|
|
1829
|
+
const t = createT(() => lang())
|
|
1786
1830
|
dialog?.replace(() => (
|
|
1787
1831
|
<api.ui.DialogSelect
|
|
1788
1832
|
title="Sort Order / 排序方式"
|
|
@@ -1809,7 +1853,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1809
1853
|
description: "Set scroll mode (wheel / click)",
|
|
1810
1854
|
slash: { name: "subagent-scroll" },
|
|
1811
1855
|
onSelect: (dialog) => {
|
|
1812
|
-
const t = (
|
|
1856
|
+
const t = createT(() => lang())
|
|
1813
1857
|
dialog?.replace(() => (
|
|
1814
1858
|
<api.ui.DialogSelect
|
|
1815
1859
|
title="Scroll Mode / 滚动模式"
|
|
@@ -1890,7 +1934,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1890
1934
|
}
|
|
1891
1935
|
let count = 0
|
|
1892
1936
|
for (const [, entry] of entries) {
|
|
1893
|
-
if (entry.status === "running") {
|
|
1937
|
+
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
1894
1938
|
entry.status = "done" as SubStatus
|
|
1895
1939
|
entry.endedAt = Date.now()
|
|
1896
1940
|
count++
|
|
@@ -1937,7 +1981,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1937
1981
|
description: "Set session data retention period (days before auto-cleanup)",
|
|
1938
1982
|
slash: { name: "subagent-ttl" },
|
|
1939
1983
|
onSelect: (dialog) => {
|
|
1940
|
-
const t = (
|
|
1984
|
+
const t = createT(() => signals.lang())
|
|
1941
1985
|
const curRaw = parseInt(String(api.kv.get(`${KV_PREFIX}.ttl_days`, "3")), 10)
|
|
1942
1986
|
const curDays = Number.isNaN(curRaw) ? 3 : curRaw
|
|
1943
1987
|
const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`
|
|
@@ -1954,7 +1998,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1954
1998
|
onSelect={(opt) => {
|
|
1955
1999
|
const days = parseInt(opt.value, 10)
|
|
1956
2000
|
api.kv.set(`${KV_PREFIX}.ttl_days`, String(days))
|
|
1957
|
-
const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast"
|
|
2001
|
+
const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days })
|
|
1958
2002
|
api.ui.toast({ message: msg })
|
|
1959
2003
|
dialog?.clear()
|
|
1960
2004
|
}}
|
|
@@ -1968,7 +2012,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1968
2012
|
description: "Delete all sub-agent records for the current session (cannot be undone)",
|
|
1969
2013
|
slash: { name: "subagent-clear-entries" },
|
|
1970
2014
|
onSelect: (dialog) => {
|
|
1971
|
-
const t = (
|
|
2015
|
+
const t = createT(() => signals.lang())
|
|
1972
2016
|
const sid = signals.sessionId
|
|
1973
2017
|
const sessionObj = api.state.session.get(sid)
|
|
1974
2018
|
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
@@ -1979,7 +2023,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1979
2023
|
for (const [, e] of cached) { if (e.status === "running") runningCount++ }
|
|
1980
2024
|
}
|
|
1981
2025
|
const msg = runningCount > 0
|
|
1982
|
-
? t("clear.prompt_running"
|
|
2026
|
+
? t("clear.prompt_running", { n: runningCount })
|
|
1983
2027
|
: t("clear.prompt")
|
|
1984
2028
|
dialog?.replace(() => (
|
|
1985
2029
|
<api.ui.DialogConfirm
|
|
@@ -2012,7 +2056,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
2012
2056
|
api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
|
|
2013
2057
|
globalEntryCache.delete(sid)
|
|
2014
2058
|
setClearTick((v) => v + 1)
|
|
2015
|
-
const msg = t("clear.done"
|
|
2059
|
+
const msg = t("clear.done", { n: count })
|
|
2016
2060
|
api.ui.toast({ message: msg })
|
|
2017
2061
|
} catch {}
|
|
2018
2062
|
dialog?.clear()
|