opencode-subagent-magazine 1.7.0 → 1.7.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/core/kv.d.ts +1 -0
- package/dist/core/kv.js +1 -0
- package/dist/core/types.d.ts +8 -0
- package/dist/core/types.js +7 -0
- package/dist/i18n.d.ts +5 -0
- package/dist/i18n.js +20 -0
- package/dist/index.js +19 -2
- package/dist/panel/SubAgentPanel.d.ts +1 -0
- package/dist/panel/SubAgentPanel.js +80 -8
- package/dist/panel/entry-map.d.ts +1 -0
- package/dist/panel/entry-map.js +1 -1
- package/dist/panel/panel-api.d.ts +16 -0
- package/dist/tui.js +385 -218
- package/dist/v2/commands.js +14 -0
- package/dist/v2/db.d.ts +14 -1
- package/dist/v2/db.js +78 -4
- package/dist/v2/index.js +3 -1
- package/dist/v2/settings-menu.js +17 -0
- package/dist/v2/v2-panel-api.js +13 -2
- package/dist/v2.js +473 -223
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/core/kv.ts +1 -0
- package/src/core/types.ts +13 -0
- package/src/i18n.ts +20 -0
- package/src/index.tsx +21 -1
- package/src/panel/SubAgentPanel.tsx +82 -7
- package/src/panel/entry-map.ts +2 -1
- package/src/panel/panel-api.ts +17 -0
- package/src/v2/commands.ts +14 -0
- package/src/v2/db.ts +76 -4
- package/src/v2/index.tsx +5 -0
- package/src/v2/settings-menu.ts +18 -0
- package/src/v2/v2-panel-api.ts +7 -2
package/package.json
CHANGED
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.7.
|
|
2
|
+
export const PLUGIN_VERSION="1.7.1";
|
package/src/core/kv.ts
CHANGED
|
@@ -29,6 +29,7 @@ export const SETTING_KEYS = {
|
|
|
29
29
|
showEntryTime: `${KV_PREFIX}.show_entry_time`,
|
|
30
30
|
showEntryTokens: `${KV_PREFIX}.show_entry_tokens`,
|
|
31
31
|
timeFormat: `${KV_PREFIX}.time_format`,
|
|
32
|
+
showOrigin: `${KV_PREFIX}.show_origin`,
|
|
32
33
|
dbSync: `${KV_PREFIX}.db_sync`,
|
|
33
34
|
} as const
|
|
34
35
|
|
package/src/core/types.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface SubEntry {
|
|
|
20
20
|
cancelRequestedAt?: number
|
|
21
21
|
abortAccepted?: boolean
|
|
22
22
|
cancelReason?: "manual"
|
|
23
|
+
/** 来源:工具调用条目 / 衍生会话条目(spawn)。 */
|
|
24
|
+
origin?: "tool" | "sub"
|
|
25
|
+
/** 工具输入带 background/run_in_background(后台衍生会话)。 */
|
|
26
|
+
background?: boolean
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export type Lang = LangCode
|
|
@@ -31,6 +35,13 @@ export type TimeFormat = "decimal" | "short" | "clock" | "compact" | "seconds"
|
|
|
31
35
|
/** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
|
|
32
36
|
export const SUBAGENT_TOOLS = new Set(["task", "subagent", "delegate", "call_omo_agent"])
|
|
33
37
|
|
|
38
|
+
/** 工具输入是否声明后台执行(V1/V2 两种字段名)。 */
|
|
39
|
+
export function isBackgroundInput(input: unknown): boolean {
|
|
40
|
+
if (!input || typeof input !== "object") return false
|
|
41
|
+
const i = input as Record<string, unknown>
|
|
42
|
+
return i.background === true || i.run_in_background === true
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
export interface ChildRecord {
|
|
35
46
|
scroll: number
|
|
36
47
|
expanded: string
|
|
@@ -66,6 +77,8 @@ export interface SharedSignals {
|
|
|
66
77
|
setShowEntryTokens: (v: boolean) => void
|
|
67
78
|
timeFormat: () => TimeFormat
|
|
68
79
|
setTimeFormat: (f: TimeFormat) => void
|
|
80
|
+
showOrigin: () => boolean
|
|
81
|
+
setShowOrigin: (v: boolean) => void
|
|
69
82
|
dbSync: () => boolean
|
|
70
83
|
setDbSync: (v: boolean) => void
|
|
71
84
|
sessionId: string
|
package/src/i18n.ts
CHANGED
|
@@ -10,6 +10,10 @@ const ZH_T = {
|
|
|
10
10
|
"panel.title": "子代理",
|
|
11
11
|
"status.none": "暂无子代理",
|
|
12
12
|
"agent.label": "代理",
|
|
13
|
+
"origin.label": "来源",
|
|
14
|
+
"origin.tool": "子代理调用",
|
|
15
|
+
"origin.sub": "衍生会话",
|
|
16
|
+
"origin.background": "后台",
|
|
13
17
|
"status.label": "状态",
|
|
14
18
|
"time.label": "耗时",
|
|
15
19
|
"tokens.label": "上下文",
|
|
@@ -71,6 +75,7 @@ const ZH_T = {
|
|
|
71
75
|
"settings.showEntryTime": "列表中显示耗时",
|
|
72
76
|
"settings.showEntryTokens": "列表中显示 Token 数",
|
|
73
77
|
"settings.timeFormat": "时间格式",
|
|
78
|
+
"settings.showOrigin": "列表显示来源标记",
|
|
74
79
|
"settings.dbSync": "本地数据库补全",
|
|
75
80
|
"settings.on": "开",
|
|
76
81
|
"settings.off": "关",
|
|
@@ -86,6 +91,10 @@ const EN_T: Translation = {
|
|
|
86
91
|
"panel.title": "SubAgent",
|
|
87
92
|
"status.none": "No sub-agents yet",
|
|
88
93
|
"agent.label": "agent",
|
|
94
|
+
"origin.label": "origin",
|
|
95
|
+
"origin.tool": "sub-agent call",
|
|
96
|
+
"origin.sub": "spawned session",
|
|
97
|
+
"origin.background": "background",
|
|
89
98
|
"status.label": "status",
|
|
90
99
|
"time.label": "time",
|
|
91
100
|
"tokens.label": "tokens",
|
|
@@ -146,6 +155,7 @@ const EN_T: Translation = {
|
|
|
146
155
|
"settings.showEntryCost": "Cost per sub-agent",
|
|
147
156
|
"settings.showEntryTime": "Elapsed time in list",
|
|
148
157
|
"settings.showEntryTokens": "Token count in list",
|
|
158
|
+
"settings.showOrigin": "Origin indicators in list",
|
|
149
159
|
"settings.timeFormat": "Time format",
|
|
150
160
|
"settings.dbSync": "Local database enrichment",
|
|
151
161
|
"settings.on": "on",
|
|
@@ -159,6 +169,10 @@ const JA_T: Translation = {
|
|
|
159
169
|
"panel.title": "サブエージェント",
|
|
160
170
|
"status.none": "サブエージェントなし",
|
|
161
171
|
"agent.label": "エージェント",
|
|
172
|
+
"origin.label": "発生元",
|
|
173
|
+
"origin.tool": "サブエージェント呼び出し",
|
|
174
|
+
"origin.sub": "生成されたセッション",
|
|
175
|
+
"origin.background": "バックグラウンド",
|
|
162
176
|
"status.label": "状態",
|
|
163
177
|
"time.label": "時間",
|
|
164
178
|
"tokens.label": "コンテキスト",
|
|
@@ -219,6 +233,7 @@ const JA_T: Translation = {
|
|
|
219
233
|
"settings.showEntryCost": "サブエージェント毎の費用",
|
|
220
234
|
"settings.showEntryTime": "一覧に所要時間を表示",
|
|
221
235
|
"settings.showEntryTokens": "一覧にトークン数を表示",
|
|
236
|
+
"settings.showOrigin": "一覧に由来マークを表示",
|
|
222
237
|
"settings.timeFormat": "時間形式",
|
|
223
238
|
"settings.dbSync": "ローカルDB補完",
|
|
224
239
|
"settings.on": "オン",
|
|
@@ -232,6 +247,10 @@ const KO_T: Translation = {
|
|
|
232
247
|
"panel.title": "서브 에이전트",
|
|
233
248
|
"status.none": "서브 에이전트 없음",
|
|
234
249
|
"agent.label": "에이전트",
|
|
250
|
+
"origin.label": "출처",
|
|
251
|
+
"origin.tool": "하위 에이전트 호출",
|
|
252
|
+
"origin.sub": "생성된 세션",
|
|
253
|
+
"origin.background": "백그라운드",
|
|
235
254
|
"status.label": "상태",
|
|
236
255
|
"time.label": "시간",
|
|
237
256
|
"tokens.label": "컨텍스트",
|
|
@@ -292,6 +311,7 @@ const KO_T: Translation = {
|
|
|
292
311
|
"settings.showEntryCost": "하위 에이전트별 비용",
|
|
293
312
|
"settings.showEntryTime": "목록에 소요 시간 표시",
|
|
294
313
|
"settings.showEntryTokens": "목록에 토큰 수 표시",
|
|
314
|
+
"settings.showOrigin": "목록에 출처 아이콘 표시",
|
|
295
315
|
"settings.timeFormat": "시간 형식",
|
|
296
316
|
"settings.dbSync": "로컬 DB 보완",
|
|
297
317
|
"settings.on": "켬",
|
package/src/index.tsx
CHANGED
|
@@ -40,6 +40,7 @@ function createSidebarSlot(api: TuiPluginApi, panelApi: PanelApi, sig: SharedSig
|
|
|
40
40
|
showEntryTime={sig.showEntryTime}
|
|
41
41
|
showEntryTokens={sig.showEntryTokens}
|
|
42
42
|
timeFormat={sig.timeFormat}
|
|
43
|
+
showOrigin={sig.showOrigin}
|
|
43
44
|
sessionId={input.session_id}
|
|
44
45
|
/>
|
|
45
46
|
)
|
|
@@ -79,11 +80,14 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
79
80
|
const [timeFormat, setTimeFormat] = createSignal<TimeFormat>(
|
|
80
81
|
(TIME_FORMATS as readonly string[]).includes(storedTimeFormat) ? (storedTimeFormat as TimeFormat) : "short"
|
|
81
82
|
)
|
|
83
|
+
const [showOrigin, setShowOrigin] = createSignal<boolean>(
|
|
84
|
+
(api.kv.get(`${KV_PREFIX}.show_origin`, false) as boolean) === true
|
|
85
|
+
)
|
|
82
86
|
const [dbSync, setDbSync] = createSignal<boolean>(
|
|
83
87
|
(api.kv.get(`${KV_PREFIX}.db_sync`, true) as boolean) !== false
|
|
84
88
|
)
|
|
85
89
|
|
|
86
|
-
const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible, setBorderVisible, showEntryCost, setShowEntryCost, showEntryTime, setShowEntryTime, showEntryTokens, setShowEntryTokens, timeFormat, setTimeFormat, dbSync, setDbSync, sessionId: "" }
|
|
90
|
+
const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible, setBorderVisible, showEntryCost, setShowEntryCost, showEntryTime, setShowEntryTime, showEntryTokens, setShowEntryTokens, timeFormat, setTimeFormat, showOrigin, setShowOrigin, dbSync, setDbSync, sessionId: "" }
|
|
87
91
|
|
|
88
92
|
// ── V1 PanelApi adapter: wraps the V1 host API into the shared panel contract ──
|
|
89
93
|
const v1Api: PanelApi = {
|
|
@@ -553,6 +557,22 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
553
557
|
))
|
|
554
558
|
},
|
|
555
559
|
},
|
|
560
|
+
{
|
|
561
|
+
title: "SubAgent Magazine: Origin Indicators",
|
|
562
|
+
value: "subagent-origin",
|
|
563
|
+
description: "Show or hide origin markers (⇢/↳) in the sidebar list",
|
|
564
|
+
slash: { name: "subagent-origin" },
|
|
565
|
+
onSelect: (dialog) => {
|
|
566
|
+
const t = createT(() => signals.lang())
|
|
567
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.show_origin`, false))
|
|
568
|
+
api.kv.set(`${KV_PREFIX}.show_origin`, !cur)
|
|
569
|
+
signals.setShowOrigin(!cur)
|
|
570
|
+
api.ui.toast({
|
|
571
|
+
message: `${t("settings.showOrigin")}: ${!cur ? t("settings.on") : t("settings.off")}`,
|
|
572
|
+
})
|
|
573
|
+
dialog?.clear()
|
|
574
|
+
},
|
|
575
|
+
},
|
|
556
576
|
])
|
|
557
577
|
}
|
|
558
578
|
|
|
@@ -15,7 +15,7 @@ import { PLUGIN_VERSION } from "../_version"
|
|
|
15
15
|
import { copyText } from "../clipboard"
|
|
16
16
|
import { createT } from "../i18n"
|
|
17
17
|
import type { Lang, SortOrder, ScrollMode, SubEntry, SubStatus, SessionRecord, TimeFormat } from "../core/types"
|
|
18
|
-
import { SUBAGENT_TOOLS } from "../core/types"
|
|
18
|
+
import { SUBAGENT_TOOLS, isBackgroundInput } from "../core/types"
|
|
19
19
|
import { visualWidth, truncate, fmtDuration, fmtTokens, safeErrorMsg } from "../core/format"
|
|
20
20
|
import { rgb, desaturateTo, dimColor, FALLBACK, MAX_SAT } from "../core/color"
|
|
21
21
|
import { KV_PREFIX, updateSessionData } from "../core/kv"
|
|
@@ -63,6 +63,7 @@ export function SubAgentPanel(props: {
|
|
|
63
63
|
showEntryTime: () => boolean
|
|
64
64
|
showEntryTokens: () => boolean
|
|
65
65
|
timeFormat: () => TimeFormat
|
|
66
|
+
showOrigin: () => boolean
|
|
66
67
|
sessionId: string
|
|
67
68
|
}): JSX.Element {
|
|
68
69
|
const t = createT(() => props.lang())
|
|
@@ -417,7 +418,7 @@ export function SubAgentPanel(props: {
|
|
|
417
418
|
const subSid = part.sessionID !== undefined ? String(part.sessionID) : undefined
|
|
418
419
|
const partModel = part.model as { modelID?: string } | undefined
|
|
419
420
|
const modelId = partModel?.modelID ? String(partModel.modelID) : undefined
|
|
420
|
-
upsertEntry({ id, title, agent, prompt, sessionId: subSid, status: "running", model: modelId })
|
|
421
|
+
upsertEntry({ id, title, agent, prompt, sessionId: subSid, status: "running", model: modelId, origin: "sub" })
|
|
421
422
|
}
|
|
422
423
|
|
|
423
424
|
// ToolPart
|
|
@@ -451,6 +452,8 @@ export function SubAgentPanel(props: {
|
|
|
451
452
|
prompt: existing.prompt,
|
|
452
453
|
sessionId: existing.sessionId ?? errorSid,
|
|
453
454
|
status: "error",
|
|
455
|
+
origin: "tool",
|
|
456
|
+
background: isBackgroundInput(st?.input),
|
|
454
457
|
})
|
|
455
458
|
}
|
|
456
459
|
return
|
|
@@ -463,7 +466,7 @@ export function SubAgentPanel(props: {
|
|
|
463
466
|
// Background tasks: tool completion ≠ agent completion — keep running until session.idle
|
|
464
467
|
// Only keep running if state metadata confirms a child session was spawned;
|
|
465
468
|
// otherwise (failed spawn, invalid agent) mark as done so the entry isn't stuck forever.
|
|
466
|
-
if ((input
|
|
469
|
+
if (isBackgroundInput(input) && status === "done") {
|
|
467
470
|
const stMetaCheck = partMetadata(part as Record<string, any>, st)
|
|
468
471
|
const hasChild = stMetaCheck?.session_id !== undefined || stMetaCheck?.sessionId !== undefined
|
|
469
472
|
if (hasChild) status = "running"
|
|
@@ -481,7 +484,7 @@ export function SubAgentPanel(props: {
|
|
|
481
484
|
const subSid = stMeta?.session_id !== undefined ? String(stMeta.session_id)
|
|
482
485
|
: stMeta?.sessionId !== undefined ? String(stMeta.sessionId)
|
|
483
486
|
: undefined
|
|
484
|
-
upsertEntry({ id, title, agent, prompt, sessionId: subSid, status })
|
|
487
|
+
upsertEntry({ id, title, agent, prompt, sessionId: subSid, status, origin: "tool", background: isBackgroundInput(input) })
|
|
485
488
|
}
|
|
486
489
|
}
|
|
487
490
|
|
|
@@ -869,7 +872,7 @@ export function SubAgentPanel(props: {
|
|
|
869
872
|
if (rawStatus === "completed") status = "done"
|
|
870
873
|
// Background tasks: tool completion ≠ agent completion — keep running until session.idle
|
|
871
874
|
// 仅在 metadata 确认已派生子会话时保持 running。
|
|
872
|
-
if ((scanInput
|
|
875
|
+
if (isBackgroundInput(scanInput) && status === "done") {
|
|
873
876
|
if (scanSubSid !== undefined) status = "running"
|
|
874
877
|
}
|
|
875
878
|
// 子会话存活判定:工具调用已完成但子会话仍 busy,
|
|
@@ -930,6 +933,8 @@ export function SubAgentPanel(props: {
|
|
|
930
933
|
status,
|
|
931
934
|
startedAt: exists?.startedAt || Date.now(),
|
|
932
935
|
endedAt: exists?.endedAt ?? (status === "done" ? Date.now() : undefined),
|
|
936
|
+
origin: "tool",
|
|
937
|
+
background: isBackgroundInput(scanInput) || exists?.background === true,
|
|
933
938
|
})
|
|
934
939
|
if (entryKey !== id) next.delete(id)
|
|
935
940
|
}
|
|
@@ -987,7 +992,7 @@ export function SubAgentPanel(props: {
|
|
|
987
992
|
// 非后台 tool 已完成、条目却仍停在 running:补一次 scan 收尾
|
|
988
993
|
// (success 事件可能因缺少 metadata 被适配层丢弃)。
|
|
989
994
|
const input = st?.input as Record<string, unknown> | undefined
|
|
990
|
-
const isBackground = input
|
|
995
|
+
const isBackground = isBackgroundInput(input)
|
|
991
996
|
const stillRunning = entry.status === "running" || entry.status === "cancel_requested"
|
|
992
997
|
if (rawStatus === "completed" && !isBackground && stillRunning) return true
|
|
993
998
|
}
|
|
@@ -1027,6 +1032,57 @@ export function SubAgentPanel(props: {
|
|
|
1027
1032
|
setEntryMapRaw((prev) => {
|
|
1028
1033
|
let changed = false
|
|
1029
1034
|
const next = new Map(prev)
|
|
1035
|
+
// 每 ~2s 从本地库同步衍生会话(spawn):没有对应工具条目的
|
|
1036
|
+
// 子会话补成 sub: 条目(标题/agent/用量/终态都来自数据库)。
|
|
1037
|
+
if (tick % 4 === 0) {
|
|
1038
|
+
try {
|
|
1039
|
+
const children = props.api.session.listChildren?.(props.sessionId)
|
|
1040
|
+
if (children) {
|
|
1041
|
+
const linked = new Set<string>()
|
|
1042
|
+
for (const e of next.values()) if (e.sessionId) linked.add(e.sessionId)
|
|
1043
|
+
const { parentSid, isChild } = resolveParent(props.sessionId)
|
|
1044
|
+
const rec = loadSessionData()[parentSid]
|
|
1045
|
+
const cleared = new Set<string>(isChild ? rec?.children?.[props.sessionId]?.clearedIds : rec?.clearedIds)
|
|
1046
|
+
for (const child of children) {
|
|
1047
|
+
if (!child.id || linked.has(child.id)) continue
|
|
1048
|
+
const id = `sub:${child.id}`
|
|
1049
|
+
if (cleared.has(id)) continue
|
|
1050
|
+
const finished = child.timeIdle != null && child.active !== true
|
|
1051
|
+
const status: SubStatus = finished
|
|
1052
|
+
? (child.idleOutcome === "interrupted" ? "cancelled" : "done")
|
|
1053
|
+
: "running"
|
|
1054
|
+
next.set(id, {
|
|
1055
|
+
id,
|
|
1056
|
+
title: child.title ?? child.agent ?? "",
|
|
1057
|
+
agent: child.agent ?? "?",
|
|
1058
|
+
prompt: "",
|
|
1059
|
+
status,
|
|
1060
|
+
sessionId: child.id,
|
|
1061
|
+
startedAt: child.timeCreated ?? Date.now(),
|
|
1062
|
+
endedAt: finished ? child.timeIdle : undefined,
|
|
1063
|
+
tokens: child.tokens,
|
|
1064
|
+
cost: child.cost,
|
|
1065
|
+
model: child.model,
|
|
1066
|
+
origin: "sub",
|
|
1067
|
+
})
|
|
1068
|
+
changed = true
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
} catch {}
|
|
1072
|
+
// 恢复运行:终态条目若子会话被 resume(宿主或本地库回报 busy),
|
|
1073
|
+
// 回到 running,等下一次真实 idle 再落定。
|
|
1074
|
+
for (const [id, entry] of next) {
|
|
1075
|
+
if (entry.status !== "done" && entry.status !== "error") continue
|
|
1076
|
+
const sid = entry.sessionId
|
|
1077
|
+
if (!sid) continue
|
|
1078
|
+
try {
|
|
1079
|
+
if (props.api.session.status(sid)?.type === "busy") {
|
|
1080
|
+
next.set(id, { ...entry, status: "running", endedAt: undefined })
|
|
1081
|
+
changed = true
|
|
1082
|
+
}
|
|
1083
|
+
} catch {}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1030
1086
|
// 候选:仍缺 usage 数据的条目。以 round-robin 遍历,
|
|
1031
1087
|
// 避免一串读不到数据的会话(如 free-model 子会话)
|
|
1032
1088
|
// 饿死列表其余部分;每个 tick 只读取少数会话。
|
|
@@ -1535,6 +1591,13 @@ export function SubAgentPanel(props: {
|
|
|
1535
1591
|
!isExpanded() && props.showEntryTime() && (elapsed() >= 2000 || entry.endedAt !== undefined)
|
|
1536
1592
|
? fmtDuration(elapsed(), isActiveRunning, props.timeFormat())
|
|
1537
1593
|
: ""
|
|
1594
|
+
// 来源标记:⇢ = 后台衍生(工具输入 background),↳ = 衍生会话(spawn)。
|
|
1595
|
+
const originMark = () => {
|
|
1596
|
+
if (!props.showOrigin()) return ""
|
|
1597
|
+
if (entry.origin === "sub") return "\u21b3"
|
|
1598
|
+
return entry.background === true ? "\u21e2" : ""
|
|
1599
|
+
}
|
|
1600
|
+
const originW = () => (originMark() ? 2 : 0)
|
|
1538
1601
|
const suffixW = () => {
|
|
1539
1602
|
let w = 0
|
|
1540
1603
|
const t = timeText()
|
|
@@ -1545,7 +1608,7 @@ export function SubAgentPanel(props: {
|
|
|
1545
1608
|
if (c) w += visualWidth(c)
|
|
1546
1609
|
return w
|
|
1547
1610
|
}
|
|
1548
|
-
const labelAvail = () => Math.max(6, panelWidth() - gutter() - LEFT_PAD - suffixW())
|
|
1611
|
+
const labelAvail = () => Math.max(6, panelWidth() - gutter() - LEFT_PAD - originW() - suffixW())
|
|
1549
1612
|
const labelText = () => {
|
|
1550
1613
|
const max = labelAvail()
|
|
1551
1614
|
const text = entry.title || entry.agent
|
|
@@ -1564,6 +1627,7 @@ export function SubAgentPanel(props: {
|
|
|
1564
1627
|
{" "}
|
|
1565
1628
|
<span style={{ fg: statusColor() }}>{statusDot()}</span>
|
|
1566
1629
|
{" "}
|
|
1630
|
+
{originMark() ? <span style={{ fg: pal().muted }}>{originMark() + " "}</span> : null}
|
|
1567
1631
|
<span style={{ fg: pal().text }}>{labelText()}</span>
|
|
1568
1632
|
{timeText() ? (
|
|
1569
1633
|
<>
|
|
@@ -1587,6 +1651,17 @@ export function SubAgentPanel(props: {
|
|
|
1587
1651
|
<span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("agent.label")))}</span>
|
|
1588
1652
|
<span style={{ fg: pal().muted }}>{entry.agent}</span>
|
|
1589
1653
|
</text>
|
|
1654
|
+
<Show when={props.showOrigin()}>
|
|
1655
|
+
<text>
|
|
1656
|
+
{" "}
|
|
1657
|
+
<span style={{ fg: pal().primary }}>{t("origin.label")}: </span>
|
|
1658
|
+
<span style={{ fg: pal().muted }}>{" ".repeat(expandedPad(t("origin.label")))}</span>
|
|
1659
|
+
<span style={{ fg: pal().muted }}>
|
|
1660
|
+
{entry.origin === "sub" ? t("origin.sub") : t("origin.tool")}
|
|
1661
|
+
{entry.background === true ? ` (${t("origin.background")})` : ""}
|
|
1662
|
+
</span>
|
|
1663
|
+
</text>
|
|
1664
|
+
</Show>
|
|
1590
1665
|
<text>
|
|
1591
1666
|
{" "}
|
|
1592
1667
|
<span style={{ fg: pal().primary }}>{t("status.label")}: </span>
|
package/src/panel/entry-map.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { SubEntry, SubStatus } from "../core/types"
|
|
|
2
2
|
|
|
3
3
|
export type SubEntryPatch = Omit<SubEntry, "startedAt" | "endedAt"> & {
|
|
4
4
|
startedAt?: number
|
|
5
|
+
endedAt?: number
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export function findSubEntryKey(
|
|
@@ -33,7 +34,7 @@ export function upsertSubEntry(
|
|
|
33
34
|
...partial,
|
|
34
35
|
id: existing?.id ?? partial.id,
|
|
35
36
|
startedAt: existing?.startedAt || partial.startedAt || now,
|
|
36
|
-
endedAt: ended ? (existing?.endedAt
|
|
37
|
+
endedAt: ended ? (existing?.endedAt ?? partial.endedAt ?? now) : undefined,
|
|
37
38
|
})
|
|
38
39
|
return next
|
|
39
40
|
}
|
package/src/panel/panel-api.ts
CHANGED
|
@@ -10,6 +10,21 @@ export interface SessionLike {
|
|
|
10
10
|
cost?: number
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** Spawned child session summary (V2 reads it from the local database). */
|
|
14
|
+
export interface ChildSessionLike {
|
|
15
|
+
id: string
|
|
16
|
+
agent?: string
|
|
17
|
+
title?: string
|
|
18
|
+
model?: string
|
|
19
|
+
cost?: number
|
|
20
|
+
tokens?: number
|
|
21
|
+
timeCreated?: number
|
|
22
|
+
timeIdle?: number
|
|
23
|
+
idleOutcome?: string
|
|
24
|
+
/** Still running: last message is newer than time_idle (resume keeps the old value). */
|
|
25
|
+
active?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
export interface SessionStatusLike {
|
|
14
29
|
type: string
|
|
15
30
|
}
|
|
@@ -59,6 +74,8 @@ export interface PanelApi {
|
|
|
59
74
|
agent?: string
|
|
60
75
|
startedAt?: number
|
|
61
76
|
}): string | undefined
|
|
77
|
+
/** Optional host-side listing of spawned child sessions. */
|
|
78
|
+
listChildren?(parentId: string): ChildSessionLike[] | undefined
|
|
62
79
|
}
|
|
63
80
|
event: {
|
|
64
81
|
on(type: PanelEventType, cb: (e: PanelEvent) => void): () => void
|
package/src/v2/commands.ts
CHANGED
|
@@ -125,6 +125,7 @@ export function makeCommands(
|
|
|
125
125
|
`${t("settings.showEntryCost")}: ${onOff(signals.showEntryCost())}`,
|
|
126
126
|
`${t("settings.showEntryTime")}: ${onOff(signals.showEntryTime())}`,
|
|
127
127
|
`${t("settings.showEntryTokens")}: ${onOff(signals.showEntryTokens())}`,
|
|
128
|
+
`${t("settings.showOrigin")}: ${onOff(signals.showOrigin())}`,
|
|
128
129
|
`${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[signals.timeFormat()]}`,
|
|
129
130
|
`${t("settings.dbSync")}: ${onOff(signals.dbSync())}`,
|
|
130
131
|
].join("\n")
|
|
@@ -198,6 +199,19 @@ export function makeCommands(
|
|
|
198
199
|
api.ui.toast(`${t("settings.showEntryTokens")}: ${onOff(v)}`)
|
|
199
200
|
},
|
|
200
201
|
},
|
|
202
|
+
{
|
|
203
|
+
id: "opencode-subagent-magazine.subagent.origin",
|
|
204
|
+
title: "SubAgent Magazine: Toggle Origin Indicators",
|
|
205
|
+
description: "Show or hide origin markers (⇢ background / ↳ spawned) in the sidebar list",
|
|
206
|
+
slash: { name: "subagent-origin" },
|
|
207
|
+
palette: true,
|
|
208
|
+
run: () => {
|
|
209
|
+
const v = !signals.showOrigin()
|
|
210
|
+
signals.setShowOrigin(v)
|
|
211
|
+
kv.set(SETTING_KEYS.showOrigin, v)
|
|
212
|
+
api.ui.toast(`${t("settings.showOrigin")}: ${onOff(v)}`)
|
|
213
|
+
},
|
|
214
|
+
},
|
|
201
215
|
{
|
|
202
216
|
id: "opencode-subagent-magazine.subagent.db",
|
|
203
217
|
title: "SubAgent Magazine: Toggle Local DB",
|
package/src/v2/db.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface ChildSessionInfo {
|
|
|
25
25
|
timeCreated?: number
|
|
26
26
|
timeIdle?: number
|
|
27
27
|
idleOutcome?: string
|
|
28
|
+
/** 仍在运行:最后一条消息晚于 time_idle(会话被恢复时 time_idle 不会被清空)。 */
|
|
29
|
+
active?: boolean
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface SessionDbIndex {
|
|
@@ -36,6 +38,8 @@ export interface SessionDbIndex {
|
|
|
36
38
|
matchChild(parentId: string, agent: string | undefined, startedAt: number | undefined): string | undefined
|
|
37
39
|
/** 子会话汇总(model/tokens/cost/time_idle)。 */
|
|
38
40
|
info(sid: string): ChildSessionInfo | undefined
|
|
41
|
+
/** 该会话派生的子会话(spawn),用于补全没有工具条目的衍生会话。 */
|
|
42
|
+
children(parentId: string): ChildSessionInfo[] | undefined
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
type SqlRow = Record<string, unknown>
|
|
@@ -89,10 +93,28 @@ export function contextTokens(tokens: unknown): number | undefined {
|
|
|
89
93
|
return sum > 0 ? sum : undefined
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
/**
|
|
96
|
+
/** 子会话是否仍在运行。
|
|
97
|
+
* 恢复过的会话会保留旧的 time_idle(host 不清空),因此以
|
|
98
|
+
* 「最后一条消息是否晚于 time_idle」为准;最后一条是 idle
|
|
99
|
+
* 消息则视为已停止。 */
|
|
100
|
+
export function isSessionActive(input: {
|
|
101
|
+
timeIdle?: number
|
|
102
|
+
lastMessageType?: string
|
|
103
|
+
lastMessageAt?: number
|
|
104
|
+
}): boolean {
|
|
105
|
+
const { timeIdle, lastMessageType, lastMessageAt } = input
|
|
106
|
+
if (lastMessageType === "idle") return false
|
|
107
|
+
if (lastMessageAt === undefined) return false
|
|
108
|
+
if (timeIdle === undefined) return true
|
|
109
|
+
return lastMessageAt > timeIdle
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 子会话终态:time_idle 存在且没有恢复活动才算结束;interrupted 视为取消。
|
|
93
113
|
* 没有 time_idle 时不作判断(交给宿主实时状态)——避免时间阈值猜测。 */
|
|
94
114
|
export function statusOfChild(info: ChildSessionInfo | undefined): "done" | "cancelled" | undefined {
|
|
95
|
-
if (!info
|
|
115
|
+
if (!info) return undefined
|
|
116
|
+
if (info.active === true) return undefined
|
|
117
|
+
if (info.timeIdle == null) return undefined
|
|
96
118
|
return info.idleOutcome === "interrupted" ? "cancelled" : "done"
|
|
97
119
|
}
|
|
98
120
|
|
|
@@ -136,12 +158,21 @@ const LAST_ASSISTANT_SQL = `
|
|
|
136
158
|
WHERE session_id = ? AND type = 'assistant' AND json_extract(data, '$.tokens.output') > 0
|
|
137
159
|
ORDER BY seq DESC LIMIT 1`
|
|
138
160
|
|
|
161
|
+
const LAST_MESSAGE_SQL = `
|
|
162
|
+
SELECT type, COALESCE(time_updated, time_created) AS at
|
|
163
|
+
FROM session_message WHERE session_id = ? ORDER BY seq DESC LIMIT 1`
|
|
164
|
+
|
|
139
165
|
const ASSISTANT_COST_SQL = `
|
|
140
166
|
SELECT COUNT(*) AS n, SUM(COALESCE(json_extract(data, '$.cost'), 0)) AS total
|
|
141
167
|
FROM session_message WHERE session_id = ? AND type = 'assistant'`
|
|
142
168
|
|
|
143
169
|
const CHILDREN_SQL = `
|
|
144
|
-
SELECT id, agent,
|
|
170
|
+
SELECT id, agent, title, model, cost,
|
|
171
|
+
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
|
172
|
+
time_created, time_idle, idle_outcome
|
|
173
|
+
FROM session_v2
|
|
174
|
+
WHERE parent_id = ? AND (fork_session_id IS NULL OR fork_session_id = '')
|
|
175
|
+
ORDER BY time_created`
|
|
145
176
|
|
|
146
177
|
const num = (v: unknown): number | undefined => {
|
|
147
178
|
const n = Number(v)
|
|
@@ -236,6 +267,7 @@ export function createSessionDbIndex(enabled: () => boolean): SessionDbIndex | u
|
|
|
236
267
|
const row = db!.query(CHILD_INFO_SQL).get(sid)
|
|
237
268
|
if (row) {
|
|
238
269
|
const last = db!.query(LAST_ASSISTANT_SQL).get(sid)
|
|
270
|
+
const lastMsg = db!.query(LAST_MESSAGE_SQL).get(sid)
|
|
239
271
|
let tokens = contextTokens(last ? {
|
|
240
272
|
input: last.input, output: last.output, reasoning: last.reasoning,
|
|
241
273
|
cache: { read: last.cache_read, write: last.cache_write },
|
|
@@ -263,6 +295,11 @@ export function createSessionDbIndex(enabled: () => boolean): SessionDbIndex | u
|
|
|
263
295
|
timeCreated: num(row.time_created),
|
|
264
296
|
timeIdle: num(row.time_idle),
|
|
265
297
|
idleOutcome: str(row.idle_outcome),
|
|
298
|
+
active: isSessionActive({
|
|
299
|
+
timeIdle: num(row.time_idle),
|
|
300
|
+
lastMessageType: str(lastMsg?.type),
|
|
301
|
+
lastMessageAt: num(lastMsg?.at),
|
|
302
|
+
}),
|
|
266
303
|
}
|
|
267
304
|
}
|
|
268
305
|
} catch { result = undefined }
|
|
@@ -270,5 +307,40 @@ export function createSessionDbIndex(enabled: () => boolean): SessionDbIndex | u
|
|
|
270
307
|
return result
|
|
271
308
|
}
|
|
272
309
|
|
|
273
|
-
|
|
310
|
+
const children = (parentId: string): ChildSessionInfo[] | undefined => {
|
|
311
|
+
if (!ready() || !parentId) return undefined
|
|
312
|
+
const now = Date.now()
|
|
313
|
+
let cached = childrenCache.get(parentId)
|
|
314
|
+
if (!cached || now - cached.at > CALL_MAP_TTL_MS) {
|
|
315
|
+
let list: SqlRow[] = []
|
|
316
|
+
try { list = db!.query(CHILDREN_SQL).all(parentId) } catch {}
|
|
317
|
+
cached = { list, at: now }
|
|
318
|
+
childrenCache.set(parentId, cached)
|
|
319
|
+
}
|
|
320
|
+
const out: ChildSessionInfo[] = []
|
|
321
|
+
for (const row of cached.list) {
|
|
322
|
+
const sid = str(row.id)
|
|
323
|
+
if (!sid) continue
|
|
324
|
+
const full = info(sid)
|
|
325
|
+
if (full) { out.push(full); continue }
|
|
326
|
+
// info 缓存未命中时不丢字段:用列表行自身的汇总兜底。
|
|
327
|
+
const agg = (num(row.tokens_input) || 0) + (num(row.tokens_output) || 0) + (num(row.tokens_reasoning) || 0) +
|
|
328
|
+
(num(row.tokens_cache_read) || 0) + (num(row.tokens_cache_write) || 0)
|
|
329
|
+
out.push({
|
|
330
|
+
id: sid,
|
|
331
|
+
parentId,
|
|
332
|
+
agent: str(row.agent),
|
|
333
|
+
title: str(row.title),
|
|
334
|
+
model: modelIdOf(row.model),
|
|
335
|
+
cost: num(row.cost),
|
|
336
|
+
tokens: agg > 0 ? agg : undefined,
|
|
337
|
+
timeCreated: num(row.time_created),
|
|
338
|
+
timeIdle: num(row.time_idle),
|
|
339
|
+
idleOutcome: str(row.idle_outcome),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
return out
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { enabled: ready, resolveCall, matchChild, info, children }
|
|
274
346
|
}
|
package/src/v2/index.tsx
CHANGED
|
@@ -45,6 +45,7 @@ function PluginRoot(props: {
|
|
|
45
45
|
showEntryTime={props.signals.showEntryTime}
|
|
46
46
|
showEntryTokens={props.signals.showEntryTokens}
|
|
47
47
|
timeFormat={props.signals.timeFormat}
|
|
48
|
+
showOrigin={props.signals.showOrigin}
|
|
48
49
|
sessionId={props.sessionID}
|
|
49
50
|
/>
|
|
50
51
|
)
|
|
@@ -95,6 +96,9 @@ const mod: PluginModule & { server: () => Promise<Record<string, never>> } = {
|
|
|
95
96
|
const [timeFormatSignal, setTimeFormat] = createSignal<TimeFormat>(
|
|
96
97
|
(TIME_FORMATS as readonly string[]).includes(storedTimeFormat) ? (storedTimeFormat as TimeFormat) : "short",
|
|
97
98
|
)
|
|
99
|
+
const [showOriginSignal, setShowOrigin] = createSignal<boolean>(
|
|
100
|
+
(api.kv.get(SETTING_KEYS.showOrigin, false) as boolean) === true,
|
|
101
|
+
)
|
|
98
102
|
const [dbSyncSignal, setDbSync] = createSignal<boolean>(
|
|
99
103
|
(api.kv.get(SETTING_KEYS.dbSync, true) as boolean) !== false,
|
|
100
104
|
)
|
|
@@ -109,6 +113,7 @@ const mod: PluginModule & { server: () => Promise<Record<string, never>> } = {
|
|
|
109
113
|
showEntryTime: showEntryTimeSignal, setShowEntryTime,
|
|
110
114
|
showEntryTokens: showEntryTokensSignal, setShowEntryTokens,
|
|
111
115
|
timeFormat: timeFormatSignal, setTimeFormat,
|
|
116
|
+
showOrigin: showOriginSignal, setShowOrigin,
|
|
112
117
|
dbSync: dbSyncSignal, setDbSync,
|
|
113
118
|
sessionId: "",
|
|
114
119
|
}
|
package/src/v2/settings-menu.ts
CHANGED
|
@@ -49,6 +49,7 @@ async function runSettingsMenu(context: Context, api: PanelApi, signals: SharedS
|
|
|
49
49
|
{ title: t("settings.showEntryTime"), value: "time", description: onOff(signals.showEntryTime()), category },
|
|
50
50
|
{ title: t("settings.timeFormat"), value: "timefmt", description: TIME_FORMAT_SAMPLES[signals.timeFormat()], category },
|
|
51
51
|
{ title: t("settings.showEntryTokens"), value: "tokens", description: onOff(signals.showEntryTokens()), category },
|
|
52
|
+
{ title: t("settings.showOrigin"), value: "origin", description: onOff(signals.showOrigin()), category },
|
|
52
53
|
{ title: t("settings.dbSync"), value: "dbsync", description: onOff(signals.dbSync()), category },
|
|
53
54
|
{ title: t("settings.close"), value: "close" },
|
|
54
55
|
],
|
|
@@ -218,6 +219,23 @@ async function runSettingsMenu(context: Context, api: PanelApi, signals: SharedS
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
// ── Origin indicators ──
|
|
223
|
+
if (choice === "origin") {
|
|
224
|
+
const picked = await context.ui.dialog.select<boolean>({
|
|
225
|
+
title: t("settings.showOrigin"),
|
|
226
|
+
options: [
|
|
227
|
+
{ title: t("settings.on"), value: true },
|
|
228
|
+
{ title: t("settings.off"), value: false },
|
|
229
|
+
],
|
|
230
|
+
current: signals.showOrigin(),
|
|
231
|
+
})
|
|
232
|
+
if (picked !== undefined) {
|
|
233
|
+
signals.setShowOrigin(picked)
|
|
234
|
+
kv.set(SETTING_KEYS.showOrigin, picked)
|
|
235
|
+
toast(t("settings.showOrigin"), onOff(picked))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
221
239
|
// ── 本地数据库补全 ──
|
|
222
240
|
if (choice === "dbsync") {
|
|
223
241
|
const picked = await context.ui.dialog.select<boolean>({
|
package/src/v2/v2-panel-api.ts
CHANGED
|
@@ -423,9 +423,11 @@ export function createPanelApi(context: Context, settings: PanelApi["settings"])
|
|
|
423
423
|
}
|
|
424
424
|
} catch {}
|
|
425
425
|
// 宿主没有该子会话的实时状态(未加载缓存)时用本地库终态兜底:
|
|
426
|
-
// time_idle
|
|
426
|
+
// time_idle 存在且没有恢复活动即已结束;仍在运行则回报 busy。
|
|
427
427
|
try {
|
|
428
|
-
|
|
428
|
+
const info = dbIndex?.info(sid)
|
|
429
|
+
if (info?.active === true) return { type: "busy" }
|
|
430
|
+
if (statusOfChild(info) !== undefined) return { type: "idle" }
|
|
429
431
|
} catch {}
|
|
430
432
|
return undefined
|
|
431
433
|
},
|
|
@@ -448,6 +450,9 @@ export function createPanelApi(context: Context, settings: PanelApi["settings"])
|
|
|
448
450
|
return dbIndex?.matchChild(input.parentId, input.agent, input.startedAt)
|
|
449
451
|
} catch { return undefined }
|
|
450
452
|
},
|
|
453
|
+
listChildren: (parentId) => {
|
|
454
|
+
try { return dbIndex?.enabled() ? dbIndex.children(parentId) : undefined } catch { return undefined }
|
|
455
|
+
},
|
|
451
456
|
},
|
|
452
457
|
event: {
|
|
453
458
|
on: (type: PanelEventType, cb: (e: PanelEvent) => void) => {
|