opencode-subagent-magazine 1.6.0-beta.1 → 1.6.0-beta.3
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/panel/SubAgentPanel.js +49 -33
- package/dist/panel/entry-map.d.ts +6 -0
- package/dist/panel/entry-map.js +25 -0
- package/dist/panel/panel-api.d.ts +3 -0
- package/dist/panel/session-routing.d.ts +3 -0
- package/dist/panel/session-routing.js +4 -0
- package/dist/tui.js +68 -35
- package/dist/v2/v2-panel-api.js +5 -3
- package/dist/v2.js +72 -38
- package/package.json +2 -1
- package/src/_version.ts +1 -1
- package/src/panel/SubAgentPanel.tsx +44 -32
- package/src/panel/entry-map.ts +39 -0
- package/src/panel/panel-api.ts +75 -72
- package/src/panel/session-routing.ts +10 -0
- package/src/v2/v2-panel-api.ts +4 -3
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.6.0-beta.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.6.0-beta.3";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION = "1.6.0-beta.
|
|
2
|
+
export const PLUGIN_VERSION = "1.6.0-beta.3";
|
|
@@ -8,6 +8,8 @@ import { visualWidth, truncate, fmtDurationShort, fmtTokens, safeErrorMsg } from
|
|
|
8
8
|
import { rgb, desaturateTo, dimColor, FALLBACK, MAX_SAT } from "../core/color";
|
|
9
9
|
import { KV_PREFIX } from "../core/kv";
|
|
10
10
|
import { globalEntryCache, clearTick } from "./store";
|
|
11
|
+
import { isDirectChildSession } from "./session-routing";
|
|
12
|
+
import { findSubEntryKey, upsertSubEntry } from "./entry-map";
|
|
11
13
|
/** Entry line left prefix: icon + space + status dot + space */
|
|
12
14
|
const LEFT_PAD = 4;
|
|
13
15
|
/** Detail row indent: two spaces */
|
|
@@ -50,15 +52,16 @@ export function SubAgentPanel(props) {
|
|
|
50
52
|
return { parentSid: sid, isChild: false };
|
|
51
53
|
};
|
|
52
54
|
const loadEntries = (sid) => {
|
|
53
|
-
|
|
55
|
+
let m = new Map();
|
|
54
56
|
try {
|
|
55
57
|
const { parentSid, isChild } = resolveParent(sid);
|
|
56
58
|
const rec = loadSessionData()[parentSid];
|
|
57
59
|
if (rec) {
|
|
58
60
|
const source = isChild ? rec.children?.[sid]?.entries : rec.entries;
|
|
59
61
|
if (source) {
|
|
60
|
-
for (const e of source)
|
|
61
|
-
m
|
|
62
|
+
for (const e of source) {
|
|
63
|
+
m = upsertSubEntry(m, e);
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
}
|
|
@@ -231,20 +234,7 @@ export function SubAgentPanel(props) {
|
|
|
231
234
|
let disposed = false;
|
|
232
235
|
// ── upsert ──
|
|
233
236
|
const upsertEntry = (partial) => {
|
|
234
|
-
setEntryMap((prev) =>
|
|
235
|
-
const existing = prev.get(partial.id);
|
|
236
|
-
const next = new Map(prev);
|
|
237
|
-
const nowTs = Date.now();
|
|
238
|
-
const e = partial.status;
|
|
239
|
-
const ended = e === "done" || e === "error" || e === "cancelled";
|
|
240
|
-
next.set(partial.id, {
|
|
241
|
-
...(existing ?? { startedAt: nowTs }),
|
|
242
|
-
...partial,
|
|
243
|
-
startedAt: existing?.startedAt || partial.startedAt || nowTs,
|
|
244
|
-
endedAt: ended ? (existing?.endedAt || nowTs) : undefined,
|
|
245
|
-
});
|
|
246
|
-
return next;
|
|
247
|
-
});
|
|
237
|
+
setEntryMap((prev) => upsertSubEntry(prev, partial));
|
|
248
238
|
};
|
|
249
239
|
// ── cancel helpers ──
|
|
250
240
|
const isDescendantOf = (childId, rootId) => {
|
|
@@ -337,7 +327,7 @@ export function SubAgentPanel(props) {
|
|
|
337
327
|
// V2 事件流是全局的——若 payload 带发起会话 ID,则只归账到正在查看的会话,
|
|
338
328
|
// 避免其他会话的子代理写进当前侧边栏;V1 宿主已按会话 scope,不传 sessionID。
|
|
339
329
|
const eventSid = event.payload?.sessionID !== undefined ? String(event.payload.sessionID) : undefined;
|
|
340
|
-
if (
|
|
330
|
+
if (event.scope === "global" && eventSid !== props.sessionId)
|
|
341
331
|
return;
|
|
342
332
|
// SubtaskPart
|
|
343
333
|
if (part.type === "subtask") {
|
|
@@ -369,9 +359,21 @@ export function SubAgentPanel(props) {
|
|
|
369
359
|
const id = `tool:${String(part.id ?? "")}`;
|
|
370
360
|
if (!part.id)
|
|
371
361
|
return;
|
|
372
|
-
const
|
|
362
|
+
const stMeta = st?.metadata;
|
|
363
|
+
const errorSid = stMeta?.session_id !== undefined ? String(stMeta.session_id)
|
|
364
|
+
: stMeta?.sessionId !== undefined ? String(stMeta.sessionId)
|
|
365
|
+
: undefined;
|
|
366
|
+
const key = findSubEntryKey(entryMap(), id, errorSid);
|
|
367
|
+
const existing = key ? entryMap().get(key) : undefined;
|
|
373
368
|
if (existing) {
|
|
374
|
-
upsertEntry({
|
|
369
|
+
upsertEntry({
|
|
370
|
+
id: existing.id,
|
|
371
|
+
title: existing.title,
|
|
372
|
+
agent: existing.agent,
|
|
373
|
+
prompt: existing.prompt,
|
|
374
|
+
sessionId: existing.sessionId ?? errorSid,
|
|
375
|
+
status: "error",
|
|
376
|
+
});
|
|
375
377
|
}
|
|
376
378
|
return;
|
|
377
379
|
}
|
|
@@ -408,6 +410,17 @@ export function SubAgentPanel(props) {
|
|
|
408
410
|
const sid = String(props_?.sessionID ?? "");
|
|
409
411
|
if (!sid)
|
|
410
412
|
return;
|
|
413
|
+
const isGlobalEvent = event.scope === "global";
|
|
414
|
+
let eventSession;
|
|
415
|
+
if (isGlobalEvent) {
|
|
416
|
+
try {
|
|
417
|
+
eventSession = props.api.session.get(sid);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
eventSession = undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession);
|
|
411
424
|
const sessionTokens = props.api.usage.readSessionTokens(sid);
|
|
412
425
|
const sessionCost = props.api.usage.readSessionCost(sid);
|
|
413
426
|
const sessionModel = props.api.usage.readSessionModel(sid);
|
|
@@ -501,7 +514,7 @@ export function SubAgentPanel(props) {
|
|
|
501
514
|
let changed = false;
|
|
502
515
|
const next = new Map(prev);
|
|
503
516
|
for (const [id, entry] of next) {
|
|
504
|
-
if (entry.sessionId !== sid)
|
|
517
|
+
if (!isCurrentChild || entry.sessionId !== sid)
|
|
505
518
|
continue;
|
|
506
519
|
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested")
|
|
507
520
|
continue;
|
|
@@ -523,7 +536,7 @@ export function SubAgentPanel(props) {
|
|
|
523
536
|
});
|
|
524
537
|
changed = true;
|
|
525
538
|
}
|
|
526
|
-
if (!changed && sessionAgent) {
|
|
539
|
+
if (!changed && sessionAgent && isCurrentChild) {
|
|
527
540
|
const nowTs = Date.now();
|
|
528
541
|
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
529
542
|
const saNorm = normalize(sessionAgent);
|
|
@@ -572,8 +585,7 @@ export function SubAgentPanel(props) {
|
|
|
572
585
|
// 当子代理所属的父 session 与当前视图不同时,通过模块级缓存定位
|
|
573
586
|
// 并更新父 session 的 entry 状态,随后写回 KV。
|
|
574
587
|
try {
|
|
575
|
-
const
|
|
576
|
-
const parentSid = sessionObj?.parentID;
|
|
588
|
+
const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID;
|
|
577
589
|
if (parentSid && parentSid !== props.sessionId) {
|
|
578
590
|
// 优先从模块级缓存获取父 session 的 entries,不受当前视图切换影响
|
|
579
591
|
const parentCache = globalEntryCache.get(parentSid);
|
|
@@ -767,7 +779,12 @@ export function SubAgentPanel(props) {
|
|
|
767
779
|
continue;
|
|
768
780
|
const st = part.state;
|
|
769
781
|
const rawStatus = String(st?.status ?? "");
|
|
770
|
-
const
|
|
782
|
+
const scanStMeta = st?.metadata;
|
|
783
|
+
const scanSubSid = scanStMeta?.session_id !== undefined ? String(scanStMeta.session_id)
|
|
784
|
+
: scanStMeta?.sessionId !== undefined ? String(scanStMeta.sessionId)
|
|
785
|
+
: undefined;
|
|
786
|
+
const existingKey = findSubEntryKey(next, id, scanSubSid);
|
|
787
|
+
const exists = existingKey ? next.get(existingKey) : undefined;
|
|
771
788
|
// 已手动清除的条目:scan 发现但不在内存 → 跳过重建
|
|
772
789
|
if (!exists && clearedIds.has(id))
|
|
773
790
|
continue;
|
|
@@ -778,7 +795,7 @@ export function SubAgentPanel(props) {
|
|
|
778
795
|
// "error": only update existing, never create a new entry
|
|
779
796
|
if (rawStatus === "error") {
|
|
780
797
|
if (exists && exists.status === "running") {
|
|
781
|
-
next.set(id, { ...exists, status: "error", endedAt: Date.now() });
|
|
798
|
+
next.set(existingKey ?? id, { ...exists, status: "error", endedAt: Date.now() });
|
|
782
799
|
}
|
|
783
800
|
continue;
|
|
784
801
|
}
|
|
@@ -794,7 +811,7 @@ export function SubAgentPanel(props) {
|
|
|
794
811
|
status = "running";
|
|
795
812
|
}
|
|
796
813
|
// Already settled → skip
|
|
797
|
-
if (exists && exists.status !== "running" && exists.status !== "cancel_requested")
|
|
814
|
+
if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested")
|
|
798
815
|
continue;
|
|
799
816
|
// Running entry with no explicit status improvement from part:
|
|
800
817
|
// try message-level heuristics first, then time-based fallback.
|
|
@@ -823,15 +840,12 @@ export function SubAgentPanel(props) {
|
|
|
823
840
|
const desc = input?.description !== undefined ? String(input.description) : "";
|
|
824
841
|
const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
|
|
825
842
|
let tokens;
|
|
826
|
-
const scanStMeta2 = st?.metadata;
|
|
827
|
-
const scanSubSid = scanStMeta2?.session_id !== undefined ? String(scanStMeta2.session_id)
|
|
828
|
-
: scanStMeta2?.sessionId !== undefined ? String(scanStMeta2.sessionId)
|
|
829
|
-
: undefined;
|
|
830
843
|
if (scanSubSid)
|
|
831
844
|
tokens = props.api.usage.readSessionTokens(scanSubSid);
|
|
832
845
|
const ended = status === "done"; // "error" handled above, never reaches here
|
|
833
|
-
|
|
834
|
-
|
|
846
|
+
const entryKey = existingKey ?? id;
|
|
847
|
+
next.set(entryKey, {
|
|
848
|
+
id: exists?.id ?? id, title, agent, prompt,
|
|
835
849
|
// Preserve existing values (from handleSessionEnd / KV) — scan must not overwrite
|
|
836
850
|
tokens: exists?.tokens ?? tokens,
|
|
837
851
|
sessionId: exists?.sessionId ?? scanSubSid,
|
|
@@ -839,6 +853,8 @@ export function SubAgentPanel(props) {
|
|
|
839
853
|
startedAt: exists?.startedAt || Date.now(),
|
|
840
854
|
endedAt: ended ? (exists?.endedAt || Date.now()) : undefined,
|
|
841
855
|
});
|
|
856
|
+
if (entryKey !== id)
|
|
857
|
+
next.delete(id);
|
|
842
858
|
}
|
|
843
859
|
}
|
|
844
860
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SubEntry } from "../core/types";
|
|
2
|
+
export type SubEntryPatch = Omit<SubEntry, "startedAt" | "endedAt"> & {
|
|
3
|
+
startedAt?: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function findSubEntryKey(entries: ReadonlyMap<string, SubEntry>, id: string, sessionId?: string): string | undefined;
|
|
6
|
+
export declare function upsertSubEntry(entries: ReadonlyMap<string, SubEntry>, partial: SubEntryPatch, now?: number): Map<string, SubEntry>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function findSubEntryKey(entries, id, sessionId) {
|
|
2
|
+
if (sessionId) {
|
|
3
|
+
for (const [key, entry] of entries) {
|
|
4
|
+
if (entry.sessionId === sessionId)
|
|
5
|
+
return key;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return entries.has(id) ? id : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function upsertSubEntry(entries, partial, now = Date.now()) {
|
|
11
|
+
const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id;
|
|
12
|
+
const existing = entries.get(key);
|
|
13
|
+
const next = new Map(entries);
|
|
14
|
+
if (key !== partial.id)
|
|
15
|
+
next.delete(partial.id);
|
|
16
|
+
const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled";
|
|
17
|
+
next.set(key, {
|
|
18
|
+
...(existing ?? { startedAt: now }),
|
|
19
|
+
...partial,
|
|
20
|
+
id: existing?.id ?? partial.id,
|
|
21
|
+
startedAt: existing?.startedAt || partial.startedAt || now,
|
|
22
|
+
endedAt: ended ? (existing?.endedAt || now) : undefined,
|
|
23
|
+
});
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
@@ -16,8 +16,11 @@ export interface SessionStatusLike {
|
|
|
16
16
|
* into these shapes so the panel component never touches host APIs directly.
|
|
17
17
|
*/
|
|
18
18
|
export type PanelEventType = "part.updated" | "message.updated" | "session.idle" | "session.error";
|
|
19
|
+
export type PanelEventScope = "session" | "global";
|
|
19
20
|
export interface PanelEvent {
|
|
20
21
|
type: PanelEventType;
|
|
22
|
+
/** Global V2 events need explicit session ownership checks. */
|
|
23
|
+
scope?: PanelEventScope;
|
|
21
24
|
/** part payload for part.updated; session properties for idle/error. */
|
|
22
25
|
payload?: Record<string, unknown>;
|
|
23
26
|
}
|
package/dist/tui.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
|
6
6
|
import { createSignal as createSignal3 } from "solid-js";
|
|
7
7
|
|
|
8
8
|
// src/_version.ts
|
|
9
|
-
var PLUGIN_VERSION = "1.6.0-beta.
|
|
9
|
+
var PLUGIN_VERSION = "1.6.0-beta.3";
|
|
10
10
|
|
|
11
11
|
// src/i18n.ts
|
|
12
12
|
var ZH_T = {
|
|
@@ -485,6 +485,36 @@ import { createSignal } from "solid-js";
|
|
|
485
485
|
var globalEntryCache = /* @__PURE__ */ new Map();
|
|
486
486
|
var [clearTick, setClearTick] = createSignal(0);
|
|
487
487
|
|
|
488
|
+
// src/panel/session-routing.ts
|
|
489
|
+
function isDirectChildSession(currentSessionId, eventSessionId, eventSession) {
|
|
490
|
+
return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/panel/entry-map.ts
|
|
494
|
+
function findSubEntryKey(entries, id, sessionId) {
|
|
495
|
+
if (sessionId) {
|
|
496
|
+
for (const [key, entry] of entries) {
|
|
497
|
+
if (entry.sessionId === sessionId) return key;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return entries.has(id) ? id : void 0;
|
|
501
|
+
}
|
|
502
|
+
function upsertSubEntry(entries, partial, now = Date.now()) {
|
|
503
|
+
const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id;
|
|
504
|
+
const existing = entries.get(key);
|
|
505
|
+
const next = new Map(entries);
|
|
506
|
+
if (key !== partial.id) next.delete(partial.id);
|
|
507
|
+
const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled";
|
|
508
|
+
next.set(key, {
|
|
509
|
+
...existing ?? { startedAt: now },
|
|
510
|
+
...partial,
|
|
511
|
+
id: existing?.id ?? partial.id,
|
|
512
|
+
startedAt: existing?.startedAt || partial.startedAt || now,
|
|
513
|
+
endedAt: ended ? existing?.endedAt || now : void 0
|
|
514
|
+
});
|
|
515
|
+
return next;
|
|
516
|
+
}
|
|
517
|
+
|
|
488
518
|
// src/panel/SubAgentPanel.tsx
|
|
489
519
|
var LEFT_PAD = 4;
|
|
490
520
|
var INDENT = 2;
|
|
@@ -524,7 +554,7 @@ function SubAgentPanel(props) {
|
|
|
524
554
|
};
|
|
525
555
|
};
|
|
526
556
|
const loadEntries = (sid) => {
|
|
527
|
-
|
|
557
|
+
let m = /* @__PURE__ */ new Map();
|
|
528
558
|
try {
|
|
529
559
|
const {
|
|
530
560
|
parentSid,
|
|
@@ -534,7 +564,9 @@ function SubAgentPanel(props) {
|
|
|
534
564
|
if (rec) {
|
|
535
565
|
const source = isChild ? rec.children?.[sid]?.entries : rec.entries;
|
|
536
566
|
if (source) {
|
|
537
|
-
for (const e of source)
|
|
567
|
+
for (const e of source) {
|
|
568
|
+
m = upsertSubEntry(m, e);
|
|
569
|
+
}
|
|
538
570
|
}
|
|
539
571
|
}
|
|
540
572
|
} catch {
|
|
@@ -772,22 +804,7 @@ function SubAgentPanel(props) {
|
|
|
772
804
|
let boxEl;
|
|
773
805
|
let disposed = false;
|
|
774
806
|
const upsertEntry = (partial) => {
|
|
775
|
-
setEntryMap((prev) =>
|
|
776
|
-
const existing = prev.get(partial.id);
|
|
777
|
-
const next = new Map(prev);
|
|
778
|
-
const nowTs = Date.now();
|
|
779
|
-
const e = partial.status;
|
|
780
|
-
const ended = e === "done" || e === "error" || e === "cancelled";
|
|
781
|
-
next.set(partial.id, {
|
|
782
|
-
...existing ?? {
|
|
783
|
-
startedAt: nowTs
|
|
784
|
-
},
|
|
785
|
-
...partial,
|
|
786
|
-
startedAt: existing?.startedAt || partial.startedAt || nowTs,
|
|
787
|
-
endedAt: ended ? existing?.endedAt || nowTs : void 0
|
|
788
|
-
});
|
|
789
|
-
return next;
|
|
790
|
-
});
|
|
807
|
+
setEntryMap((prev) => upsertSubEntry(prev, partial));
|
|
791
808
|
};
|
|
792
809
|
const isDescendantOf = (childId, rootId) => {
|
|
793
810
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -902,7 +919,7 @@ function SubAgentPanel(props) {
|
|
|
902
919
|
const part = event.payload?.part;
|
|
903
920
|
if (!part) return;
|
|
904
921
|
const eventSid = event.payload?.sessionID !== void 0 ? String(event.payload.sessionID) : void 0;
|
|
905
|
-
if (
|
|
922
|
+
if (event.scope === "global" && eventSid !== props.sessionId) return;
|
|
906
923
|
if (part.type === "subtask") {
|
|
907
924
|
const agent = String(part.agent ?? "?");
|
|
908
925
|
const prompt = String(part.prompt ?? "");
|
|
@@ -931,13 +948,17 @@ function SubAgentPanel(props) {
|
|
|
931
948
|
if (rawStatus === "error") {
|
|
932
949
|
const id2 = `tool:${String(part.id ?? "")}`;
|
|
933
950
|
if (!part.id) return;
|
|
934
|
-
const
|
|
951
|
+
const stMeta2 = st?.metadata;
|
|
952
|
+
const errorSid = stMeta2?.session_id !== void 0 ? String(stMeta2.session_id) : stMeta2?.sessionId !== void 0 ? String(stMeta2.sessionId) : void 0;
|
|
953
|
+
const key = findSubEntryKey(entryMap(), id2, errorSid);
|
|
954
|
+
const existing = key ? entryMap().get(key) : void 0;
|
|
935
955
|
if (existing) {
|
|
936
956
|
upsertEntry({
|
|
937
|
-
id:
|
|
957
|
+
id: existing.id,
|
|
938
958
|
title: existing.title,
|
|
939
959
|
agent: existing.agent,
|
|
940
960
|
prompt: existing.prompt,
|
|
961
|
+
sessionId: existing.sessionId ?? errorSid,
|
|
941
962
|
status: "error"
|
|
942
963
|
});
|
|
943
964
|
}
|
|
@@ -972,6 +993,16 @@ function SubAgentPanel(props) {
|
|
|
972
993
|
const props_ = event.payload;
|
|
973
994
|
const sid = String(props_?.sessionID ?? "");
|
|
974
995
|
if (!sid) return;
|
|
996
|
+
const isGlobalEvent = event.scope === "global";
|
|
997
|
+
let eventSession;
|
|
998
|
+
if (isGlobalEvent) {
|
|
999
|
+
try {
|
|
1000
|
+
eventSession = props.api.session.get(sid);
|
|
1001
|
+
} catch {
|
|
1002
|
+
eventSession = void 0;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession);
|
|
975
1006
|
const sessionTokens = props.api.usage.readSessionTokens(sid);
|
|
976
1007
|
const sessionCost = props.api.usage.readSessionCost(sid);
|
|
977
1008
|
const sessionModel = props.api.usage.readSessionModel(sid);
|
|
@@ -1060,7 +1091,7 @@ function SubAgentPanel(props) {
|
|
|
1060
1091
|
let changed = false;
|
|
1061
1092
|
const next = new Map(prev);
|
|
1062
1093
|
for (const [id, entry] of next) {
|
|
1063
|
-
if (entry.sessionId !== sid) continue;
|
|
1094
|
+
if (!isCurrentChild || entry.sessionId !== sid) continue;
|
|
1064
1095
|
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested") continue;
|
|
1065
1096
|
if (sid === props.sessionId) continue;
|
|
1066
1097
|
const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested";
|
|
@@ -1080,7 +1111,7 @@ function SubAgentPanel(props) {
|
|
|
1080
1111
|
});
|
|
1081
1112
|
changed = true;
|
|
1082
1113
|
}
|
|
1083
|
-
if (!changed && sessionAgent) {
|
|
1114
|
+
if (!changed && sessionAgent && isCurrentChild) {
|
|
1084
1115
|
const nowTs = Date.now();
|
|
1085
1116
|
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
1086
1117
|
const saNorm = normalize(sessionAgent);
|
|
@@ -1125,8 +1156,7 @@ function SubAgentPanel(props) {
|
|
|
1125
1156
|
return changed ? next : prev;
|
|
1126
1157
|
});
|
|
1127
1158
|
try {
|
|
1128
|
-
const
|
|
1129
|
-
const parentSid = sessionObj?.parentID;
|
|
1159
|
+
const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID;
|
|
1130
1160
|
if (parentSid && parentSid !== props.sessionId) {
|
|
1131
1161
|
const parentCache = globalEntryCache.get(parentSid);
|
|
1132
1162
|
const nowTs = Date.now();
|
|
@@ -1315,12 +1345,15 @@ function SubAgentPanel(props) {
|
|
|
1315
1345
|
if (!part.id) continue;
|
|
1316
1346
|
const st = part.state;
|
|
1317
1347
|
const rawStatus = String(st?.status ?? "");
|
|
1318
|
-
const
|
|
1348
|
+
const scanStMeta = st?.metadata;
|
|
1349
|
+
const scanSubSid = scanStMeta?.session_id !== void 0 ? String(scanStMeta.session_id) : scanStMeta?.sessionId !== void 0 ? String(scanStMeta.sessionId) : void 0;
|
|
1350
|
+
const existingKey = findSubEntryKey(next, id, scanSubSid);
|
|
1351
|
+
const exists = existingKey ? next.get(existingKey) : void 0;
|
|
1319
1352
|
if (!exists && clearedIds.has(id)) continue;
|
|
1320
1353
|
if ((rawStatus === "pending" || rawStatus === "") && !exists) continue;
|
|
1321
1354
|
if (rawStatus === "error") {
|
|
1322
1355
|
if (exists && exists.status === "running") {
|
|
1323
|
-
next.set(id, {
|
|
1356
|
+
next.set(existingKey ?? id, {
|
|
1324
1357
|
...exists,
|
|
1325
1358
|
status: "error",
|
|
1326
1359
|
endedAt: Date.now()
|
|
@@ -1331,11 +1364,11 @@ function SubAgentPanel(props) {
|
|
|
1331
1364
|
let status = "running";
|
|
1332
1365
|
if (rawStatus === "completed") status = "done";
|
|
1333
1366
|
if ((st?.input?.run_in_background === true || st?.input?.background === true) && status === "done") {
|
|
1334
|
-
const
|
|
1335
|
-
const scanHasChild =
|
|
1367
|
+
const scanStMeta2 = st?.metadata;
|
|
1368
|
+
const scanHasChild = scanStMeta2?.session_id !== void 0 || scanStMeta2?.sessionId !== void 0;
|
|
1336
1369
|
if (scanHasChild) status = "running";
|
|
1337
1370
|
}
|
|
1338
|
-
if (exists && exists.status !== "running" && exists.status !== "cancel_requested") continue;
|
|
1371
|
+
if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested") continue;
|
|
1339
1372
|
if (exists && status === "running") {
|
|
1340
1373
|
if (!rawStatus) {
|
|
1341
1374
|
const msgTokens = msg?.tokens;
|
|
@@ -1356,12 +1389,11 @@ function SubAgentPanel(props) {
|
|
|
1356
1389
|
const desc = input?.description !== void 0 ? String(input.description) : "";
|
|
1357
1390
|
const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
|
|
1358
1391
|
let tokens;
|
|
1359
|
-
const scanStMeta2 = st?.metadata;
|
|
1360
|
-
const scanSubSid = scanStMeta2?.session_id !== void 0 ? String(scanStMeta2.session_id) : scanStMeta2?.sessionId !== void 0 ? String(scanStMeta2.sessionId) : void 0;
|
|
1361
1392
|
if (scanSubSid) tokens = props.api.usage.readSessionTokens(scanSubSid);
|
|
1362
1393
|
const ended = status === "done";
|
|
1363
|
-
|
|
1364
|
-
|
|
1394
|
+
const entryKey = existingKey ?? id;
|
|
1395
|
+
next.set(entryKey, {
|
|
1396
|
+
id: exists?.id ?? id,
|
|
1365
1397
|
title,
|
|
1366
1398
|
agent,
|
|
1367
1399
|
prompt,
|
|
@@ -1372,6 +1404,7 @@ function SubAgentPanel(props) {
|
|
|
1372
1404
|
startedAt: exists?.startedAt || Date.now(),
|
|
1373
1405
|
endedAt: ended ? exists?.endedAt || Date.now() : void 0
|
|
1374
1406
|
});
|
|
1407
|
+
if (entryKey !== id) next.delete(id);
|
|
1375
1408
|
}
|
|
1376
1409
|
}
|
|
1377
1410
|
}
|
package/dist/v2/v2-panel-api.js
CHANGED
|
@@ -295,7 +295,9 @@ export function createPanelApi(context, settings) {
|
|
|
295
295
|
// 让面板只归账到正在查看的会话(V1 宿主已按会话 scope,无需该字段)。
|
|
296
296
|
const evt = e.data;
|
|
297
297
|
const sid = evt?.sessionID !== undefined ? String(evt.sessionID) : undefined;
|
|
298
|
-
|
|
298
|
+
if (!sid)
|
|
299
|
+
return;
|
|
300
|
+
cb({ type, scope: "global", payload: { part, sessionID: sid } });
|
|
299
301
|
}));
|
|
300
302
|
}
|
|
301
303
|
return () => { for (const u of unsubs)
|
|
@@ -317,7 +319,7 @@ export function createPanelApi(context, settings) {
|
|
|
317
319
|
unsubs.push(context.data.on(evt, (e) => {
|
|
318
320
|
const sid = String(e.data?.sessionID ?? "");
|
|
319
321
|
if (sid)
|
|
320
|
-
cb({ type, payload: { sessionID: sid } });
|
|
322
|
+
cb({ type, scope: "global", payload: { sessionID: sid } });
|
|
321
323
|
}));
|
|
322
324
|
}
|
|
323
325
|
return () => { for (const u of unsubs)
|
|
@@ -328,7 +330,7 @@ export function createPanelApi(context, settings) {
|
|
|
328
330
|
const evt = e.data;
|
|
329
331
|
const sid = String(evt?.sessionID ?? "");
|
|
330
332
|
if (sid)
|
|
331
|
-
cb({ type, payload: { sessionID: sid, error: evt?.error } });
|
|
333
|
+
cb({ type, scope: "global", payload: { sessionID: sid, error: evt?.error } });
|
|
332
334
|
});
|
|
333
335
|
default:
|
|
334
336
|
return () => { };
|
package/dist/v2.js
CHANGED
|
@@ -242,7 +242,8 @@ function createPanelApi(context, settings) {
|
|
|
242
242
|
if (!part) return;
|
|
243
243
|
const evt2 = e.data;
|
|
244
244
|
const sid = evt2?.sessionID !== void 0 ? String(evt2.sessionID) : void 0;
|
|
245
|
-
|
|
245
|
+
if (!sid) return;
|
|
246
|
+
cb({ type, scope: "global", payload: { part, sessionID: sid } });
|
|
246
247
|
}));
|
|
247
248
|
}
|
|
248
249
|
return () => {
|
|
@@ -263,7 +264,7 @@ function createPanelApi(context, settings) {
|
|
|
263
264
|
for (const evt of ["session.execution.succeeded", "session.execution.interrupted"]) {
|
|
264
265
|
unsubs.push(context.data.on(evt, (e) => {
|
|
265
266
|
const sid = String(e.data?.sessionID ?? "");
|
|
266
|
-
if (sid) cb({ type, payload: { sessionID: sid } });
|
|
267
|
+
if (sid) cb({ type, scope: "global", payload: { sessionID: sid } });
|
|
267
268
|
}));
|
|
268
269
|
}
|
|
269
270
|
return () => {
|
|
@@ -274,7 +275,7 @@ function createPanelApi(context, settings) {
|
|
|
274
275
|
return context.data.on("session.execution.failed", (e) => {
|
|
275
276
|
const evt = e.data;
|
|
276
277
|
const sid = String(evt?.sessionID ?? "");
|
|
277
|
-
if (sid) cb({ type, payload: { sessionID: sid, error: evt?.error } });
|
|
278
|
+
if (sid) cb({ type, scope: "global", payload: { sessionID: sid, error: evt?.error } });
|
|
278
279
|
});
|
|
279
280
|
default:
|
|
280
281
|
return () => {
|
|
@@ -328,7 +329,7 @@ function readTTLDays(kv) {
|
|
|
328
329
|
}
|
|
329
330
|
|
|
330
331
|
// src/_version.ts
|
|
331
|
-
var PLUGIN_VERSION = "1.6.0-beta.
|
|
332
|
+
var PLUGIN_VERSION = "1.6.0-beta.3";
|
|
332
333
|
|
|
333
334
|
// src/i18n.ts
|
|
334
335
|
var ZH_T = {
|
|
@@ -1051,6 +1052,36 @@ var FALLBACK = {
|
|
|
1051
1052
|
};
|
|
1052
1053
|
var MAX_SAT = 0.28;
|
|
1053
1054
|
|
|
1055
|
+
// src/panel/session-routing.ts
|
|
1056
|
+
function isDirectChildSession(currentSessionId, eventSessionId, eventSession) {
|
|
1057
|
+
return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/panel/entry-map.ts
|
|
1061
|
+
function findSubEntryKey(entries, id, sessionId) {
|
|
1062
|
+
if (sessionId) {
|
|
1063
|
+
for (const [key, entry] of entries) {
|
|
1064
|
+
if (entry.sessionId === sessionId) return key;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return entries.has(id) ? id : void 0;
|
|
1068
|
+
}
|
|
1069
|
+
function upsertSubEntry(entries, partial, now = Date.now()) {
|
|
1070
|
+
const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id;
|
|
1071
|
+
const existing = entries.get(key);
|
|
1072
|
+
const next = new Map(entries);
|
|
1073
|
+
if (key !== partial.id) next.delete(partial.id);
|
|
1074
|
+
const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled";
|
|
1075
|
+
next.set(key, {
|
|
1076
|
+
...existing ?? { startedAt: now },
|
|
1077
|
+
...partial,
|
|
1078
|
+
id: existing?.id ?? partial.id,
|
|
1079
|
+
startedAt: existing?.startedAt || partial.startedAt || now,
|
|
1080
|
+
endedAt: ended ? existing?.endedAt || now : void 0
|
|
1081
|
+
});
|
|
1082
|
+
return next;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1054
1085
|
// src/panel/SubAgentPanel.tsx
|
|
1055
1086
|
var LEFT_PAD = 4;
|
|
1056
1087
|
var INDENT = 2;
|
|
@@ -1090,7 +1121,7 @@ function SubAgentPanel(props) {
|
|
|
1090
1121
|
};
|
|
1091
1122
|
};
|
|
1092
1123
|
const loadEntries = (sid) => {
|
|
1093
|
-
|
|
1124
|
+
let m = /* @__PURE__ */ new Map();
|
|
1094
1125
|
try {
|
|
1095
1126
|
const {
|
|
1096
1127
|
parentSid,
|
|
@@ -1100,7 +1131,9 @@ function SubAgentPanel(props) {
|
|
|
1100
1131
|
if (rec) {
|
|
1101
1132
|
const source = isChild ? rec.children?.[sid]?.entries : rec.entries;
|
|
1102
1133
|
if (source) {
|
|
1103
|
-
for (const e of source)
|
|
1134
|
+
for (const e of source) {
|
|
1135
|
+
m = upsertSubEntry(m, e);
|
|
1136
|
+
}
|
|
1104
1137
|
}
|
|
1105
1138
|
}
|
|
1106
1139
|
} catch {
|
|
@@ -1338,22 +1371,7 @@ function SubAgentPanel(props) {
|
|
|
1338
1371
|
let boxEl;
|
|
1339
1372
|
let disposed = false;
|
|
1340
1373
|
const upsertEntry = (partial) => {
|
|
1341
|
-
setEntryMap((prev) =>
|
|
1342
|
-
const existing = prev.get(partial.id);
|
|
1343
|
-
const next = new Map(prev);
|
|
1344
|
-
const nowTs = Date.now();
|
|
1345
|
-
const e = partial.status;
|
|
1346
|
-
const ended = e === "done" || e === "error" || e === "cancelled";
|
|
1347
|
-
next.set(partial.id, {
|
|
1348
|
-
...existing ?? {
|
|
1349
|
-
startedAt: nowTs
|
|
1350
|
-
},
|
|
1351
|
-
...partial,
|
|
1352
|
-
startedAt: existing?.startedAt || partial.startedAt || nowTs,
|
|
1353
|
-
endedAt: ended ? existing?.endedAt || nowTs : void 0
|
|
1354
|
-
});
|
|
1355
|
-
return next;
|
|
1356
|
-
});
|
|
1374
|
+
setEntryMap((prev) => upsertSubEntry(prev, partial));
|
|
1357
1375
|
};
|
|
1358
1376
|
const isDescendantOf = (childId, rootId) => {
|
|
1359
1377
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -1468,7 +1486,7 @@ function SubAgentPanel(props) {
|
|
|
1468
1486
|
const part = event.payload?.part;
|
|
1469
1487
|
if (!part) return;
|
|
1470
1488
|
const eventSid = event.payload?.sessionID !== void 0 ? String(event.payload.sessionID) : void 0;
|
|
1471
|
-
if (
|
|
1489
|
+
if (event.scope === "global" && eventSid !== props.sessionId) return;
|
|
1472
1490
|
if (part.type === "subtask") {
|
|
1473
1491
|
const agent = String(part.agent ?? "?");
|
|
1474
1492
|
const prompt = String(part.prompt ?? "");
|
|
@@ -1497,13 +1515,17 @@ function SubAgentPanel(props) {
|
|
|
1497
1515
|
if (rawStatus === "error") {
|
|
1498
1516
|
const id2 = `tool:${String(part.id ?? "")}`;
|
|
1499
1517
|
if (!part.id) return;
|
|
1500
|
-
const
|
|
1518
|
+
const stMeta2 = st?.metadata;
|
|
1519
|
+
const errorSid = stMeta2?.session_id !== void 0 ? String(stMeta2.session_id) : stMeta2?.sessionId !== void 0 ? String(stMeta2.sessionId) : void 0;
|
|
1520
|
+
const key = findSubEntryKey(entryMap(), id2, errorSid);
|
|
1521
|
+
const existing = key ? entryMap().get(key) : void 0;
|
|
1501
1522
|
if (existing) {
|
|
1502
1523
|
upsertEntry({
|
|
1503
|
-
id:
|
|
1524
|
+
id: existing.id,
|
|
1504
1525
|
title: existing.title,
|
|
1505
1526
|
agent: existing.agent,
|
|
1506
1527
|
prompt: existing.prompt,
|
|
1528
|
+
sessionId: existing.sessionId ?? errorSid,
|
|
1507
1529
|
status: "error"
|
|
1508
1530
|
});
|
|
1509
1531
|
}
|
|
@@ -1538,6 +1560,16 @@ function SubAgentPanel(props) {
|
|
|
1538
1560
|
const props_ = event.payload;
|
|
1539
1561
|
const sid = String(props_?.sessionID ?? "");
|
|
1540
1562
|
if (!sid) return;
|
|
1563
|
+
const isGlobalEvent = event.scope === "global";
|
|
1564
|
+
let eventSession;
|
|
1565
|
+
if (isGlobalEvent) {
|
|
1566
|
+
try {
|
|
1567
|
+
eventSession = props.api.session.get(sid);
|
|
1568
|
+
} catch {
|
|
1569
|
+
eventSession = void 0;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession);
|
|
1541
1573
|
const sessionTokens = props.api.usage.readSessionTokens(sid);
|
|
1542
1574
|
const sessionCost = props.api.usage.readSessionCost(sid);
|
|
1543
1575
|
const sessionModel = props.api.usage.readSessionModel(sid);
|
|
@@ -1626,7 +1658,7 @@ function SubAgentPanel(props) {
|
|
|
1626
1658
|
let changed = false;
|
|
1627
1659
|
const next = new Map(prev);
|
|
1628
1660
|
for (const [id, entry] of next) {
|
|
1629
|
-
if (entry.sessionId !== sid) continue;
|
|
1661
|
+
if (!isCurrentChild || entry.sessionId !== sid) continue;
|
|
1630
1662
|
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested") continue;
|
|
1631
1663
|
if (sid === props.sessionId) continue;
|
|
1632
1664
|
const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested";
|
|
@@ -1646,7 +1678,7 @@ function SubAgentPanel(props) {
|
|
|
1646
1678
|
});
|
|
1647
1679
|
changed = true;
|
|
1648
1680
|
}
|
|
1649
|
-
if (!changed && sessionAgent) {
|
|
1681
|
+
if (!changed && sessionAgent && isCurrentChild) {
|
|
1650
1682
|
const nowTs = Date.now();
|
|
1651
1683
|
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
1652
1684
|
const saNorm = normalize(sessionAgent);
|
|
@@ -1691,8 +1723,7 @@ function SubAgentPanel(props) {
|
|
|
1691
1723
|
return changed ? next : prev;
|
|
1692
1724
|
});
|
|
1693
1725
|
try {
|
|
1694
|
-
const
|
|
1695
|
-
const parentSid = sessionObj?.parentID;
|
|
1726
|
+
const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID;
|
|
1696
1727
|
if (parentSid && parentSid !== props.sessionId) {
|
|
1697
1728
|
const parentCache = globalEntryCache.get(parentSid);
|
|
1698
1729
|
const nowTs = Date.now();
|
|
@@ -1881,12 +1912,15 @@ function SubAgentPanel(props) {
|
|
|
1881
1912
|
if (!part.id) continue;
|
|
1882
1913
|
const st = part.state;
|
|
1883
1914
|
const rawStatus = String(st?.status ?? "");
|
|
1884
|
-
const
|
|
1915
|
+
const scanStMeta = st?.metadata;
|
|
1916
|
+
const scanSubSid = scanStMeta?.session_id !== void 0 ? String(scanStMeta.session_id) : scanStMeta?.sessionId !== void 0 ? String(scanStMeta.sessionId) : void 0;
|
|
1917
|
+
const existingKey = findSubEntryKey(next, id, scanSubSid);
|
|
1918
|
+
const exists = existingKey ? next.get(existingKey) : void 0;
|
|
1885
1919
|
if (!exists && clearedIds.has(id)) continue;
|
|
1886
1920
|
if ((rawStatus === "pending" || rawStatus === "") && !exists) continue;
|
|
1887
1921
|
if (rawStatus === "error") {
|
|
1888
1922
|
if (exists && exists.status === "running") {
|
|
1889
|
-
next.set(id, {
|
|
1923
|
+
next.set(existingKey ?? id, {
|
|
1890
1924
|
...exists,
|
|
1891
1925
|
status: "error",
|
|
1892
1926
|
endedAt: Date.now()
|
|
@@ -1897,11 +1931,11 @@ function SubAgentPanel(props) {
|
|
|
1897
1931
|
let status = "running";
|
|
1898
1932
|
if (rawStatus === "completed") status = "done";
|
|
1899
1933
|
if ((st?.input?.run_in_background === true || st?.input?.background === true) && status === "done") {
|
|
1900
|
-
const
|
|
1901
|
-
const scanHasChild =
|
|
1934
|
+
const scanStMeta2 = st?.metadata;
|
|
1935
|
+
const scanHasChild = scanStMeta2?.session_id !== void 0 || scanStMeta2?.sessionId !== void 0;
|
|
1902
1936
|
if (scanHasChild) status = "running";
|
|
1903
1937
|
}
|
|
1904
|
-
if (exists && exists.status !== "running" && exists.status !== "cancel_requested") continue;
|
|
1938
|
+
if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested") continue;
|
|
1905
1939
|
if (exists && status === "running") {
|
|
1906
1940
|
if (!rawStatus) {
|
|
1907
1941
|
const msgTokens = msg?.tokens;
|
|
@@ -1922,12 +1956,11 @@ function SubAgentPanel(props) {
|
|
|
1922
1956
|
const desc = input?.description !== void 0 ? String(input.description) : "";
|
|
1923
1957
|
const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
|
|
1924
1958
|
let tokens;
|
|
1925
|
-
const scanStMeta2 = st?.metadata;
|
|
1926
|
-
const scanSubSid = scanStMeta2?.session_id !== void 0 ? String(scanStMeta2.session_id) : scanStMeta2?.sessionId !== void 0 ? String(scanStMeta2.sessionId) : void 0;
|
|
1927
1959
|
if (scanSubSid) tokens = props.api.usage.readSessionTokens(scanSubSid);
|
|
1928
1960
|
const ended = status === "done";
|
|
1929
|
-
|
|
1930
|
-
|
|
1961
|
+
const entryKey = existingKey ?? id;
|
|
1962
|
+
next.set(entryKey, {
|
|
1963
|
+
id: exists?.id ?? id,
|
|
1931
1964
|
title,
|
|
1932
1965
|
agent,
|
|
1933
1966
|
prompt,
|
|
@@ -1938,6 +1971,7 @@ function SubAgentPanel(props) {
|
|
|
1938
1971
|
startedAt: exists?.startedAt || Date.now(),
|
|
1939
1972
|
endedAt: ended ? exists?.endedAt || Date.now() : void 0
|
|
1940
1973
|
});
|
|
1974
|
+
if (entryKey !== id) next.delete(id);
|
|
1941
1975
|
}
|
|
1942
1976
|
}
|
|
1943
1977
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-subagent-magazine",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.0-beta.3",
|
|
4
4
|
"description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"build": "tsc && node build.tui.mjs",
|
|
31
31
|
"build:tui": "node build.tui.mjs",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "tsx --test tests/*.test.ts",
|
|
33
34
|
"version": "node -e \"require('fs').writeFileSync('src/_version.ts','// auto-generated\\nexport const PLUGIN_VERSION='+JSON.stringify(require('./package.json').version)+';\\n')\"",
|
|
34
35
|
"prepublishOnly": "tsc"
|
|
35
36
|
},
|
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.6.0-beta.
|
|
2
|
+
export const PLUGIN_VERSION="1.6.0-beta.3";
|
|
@@ -21,6 +21,8 @@ import { rgb, desaturateTo, dimColor, FALLBACK, MAX_SAT } from "../core/color"
|
|
|
21
21
|
import { KV_PREFIX } from "../core/kv"
|
|
22
22
|
import type { PanelApi, PanelEvent } from "./panel-api"
|
|
23
23
|
import { globalEntryCache, clearTick } from "./store"
|
|
24
|
+
import { isDirectChildSession } from "./session-routing"
|
|
25
|
+
import { findSubEntryKey, upsertSubEntry } from "./entry-map"
|
|
24
26
|
|
|
25
27
|
/** Entry line left prefix: icon + space + status dot + space */
|
|
26
28
|
const LEFT_PAD = 4
|
|
@@ -86,14 +88,16 @@ export function SubAgentPanel(props: {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
const loadEntries = (sid: string): Map<string, SubEntry> => {
|
|
89
|
-
|
|
91
|
+
let m = new Map<string, SubEntry>()
|
|
90
92
|
try {
|
|
91
93
|
const { parentSid, isChild } = resolveParent(sid)
|
|
92
94
|
const rec = loadSessionData()[parentSid]
|
|
93
95
|
if (rec) {
|
|
94
96
|
const source = isChild ? rec.children?.[sid]?.entries : rec.entries
|
|
95
97
|
if (source) {
|
|
96
|
-
for (const e of source)
|
|
98
|
+
for (const e of source) {
|
|
99
|
+
m = upsertSubEntry(m, e)
|
|
100
|
+
}
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
} catch {}
|
|
@@ -255,20 +259,7 @@ export function SubAgentPanel(props: {
|
|
|
255
259
|
const upsertEntry = (
|
|
256
260
|
partial: Omit<SubEntry, "startedAt" | "endedAt"> & { startedAt?: number }
|
|
257
261
|
) => {
|
|
258
|
-
setEntryMap((prev) =>
|
|
259
|
-
const existing = prev.get(partial.id)
|
|
260
|
-
const next = new Map(prev)
|
|
261
|
-
const nowTs = Date.now()
|
|
262
|
-
const e = partial.status
|
|
263
|
-
const ended = e === "done" || e === "error" || e === "cancelled"
|
|
264
|
-
next.set(partial.id, {
|
|
265
|
-
...(existing ?? { startedAt: nowTs }),
|
|
266
|
-
...partial,
|
|
267
|
-
startedAt: existing?.startedAt || partial.startedAt || nowTs,
|
|
268
|
-
endedAt: ended ? (existing?.endedAt || nowTs) : undefined,
|
|
269
|
-
})
|
|
270
|
-
return next
|
|
271
|
-
})
|
|
262
|
+
setEntryMap((prev) => upsertSubEntry(prev, partial))
|
|
272
263
|
}
|
|
273
264
|
|
|
274
265
|
// ── cancel helpers ──
|
|
@@ -363,7 +354,7 @@ export function SubAgentPanel(props: {
|
|
|
363
354
|
// V2 事件流是全局的——若 payload 带发起会话 ID,则只归账到正在查看的会话,
|
|
364
355
|
// 避免其他会话的子代理写进当前侧边栏;V1 宿主已按会话 scope,不传 sessionID。
|
|
365
356
|
const eventSid = event.payload?.sessionID !== undefined ? String(event.payload.sessionID) : undefined
|
|
366
|
-
if (
|
|
357
|
+
if (event.scope === "global" && eventSid !== props.sessionId) return
|
|
367
358
|
|
|
368
359
|
// SubtaskPart
|
|
369
360
|
if (part.type === "subtask") {
|
|
@@ -396,9 +387,21 @@ export function SubAgentPanel(props: {
|
|
|
396
387
|
if (rawStatus === "error") {
|
|
397
388
|
const id = `tool:${String(part.id ?? "")}`
|
|
398
389
|
if (!part.id) return
|
|
399
|
-
const
|
|
390
|
+
const stMeta = st?.metadata as Record<string, unknown> | undefined
|
|
391
|
+
const errorSid = stMeta?.session_id !== undefined ? String(stMeta.session_id)
|
|
392
|
+
: stMeta?.sessionId !== undefined ? String(stMeta.sessionId)
|
|
393
|
+
: undefined
|
|
394
|
+
const key = findSubEntryKey(entryMap(), id, errorSid)
|
|
395
|
+
const existing = key ? entryMap().get(key) : undefined
|
|
400
396
|
if (existing) {
|
|
401
|
-
upsertEntry({
|
|
397
|
+
upsertEntry({
|
|
398
|
+
id: existing.id,
|
|
399
|
+
title: existing.title,
|
|
400
|
+
agent: existing.agent,
|
|
401
|
+
prompt: existing.prompt,
|
|
402
|
+
sessionId: existing.sessionId ?? errorSid,
|
|
403
|
+
status: "error",
|
|
404
|
+
})
|
|
402
405
|
}
|
|
403
406
|
return
|
|
404
407
|
}
|
|
@@ -437,6 +440,13 @@ export function SubAgentPanel(props: {
|
|
|
437
440
|
const sid = String(props_?.sessionID ?? "")
|
|
438
441
|
if (!sid) return
|
|
439
442
|
|
|
443
|
+
const isGlobalEvent = event.scope === "global"
|
|
444
|
+
let eventSession: ReturnType<PanelApi["session"]["get"]>
|
|
445
|
+
if (isGlobalEvent) {
|
|
446
|
+
try { eventSession = props.api.session.get(sid) } catch { eventSession = undefined }
|
|
447
|
+
}
|
|
448
|
+
const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession)
|
|
449
|
+
|
|
440
450
|
const sessionTokens = props.api.usage.readSessionTokens(sid)
|
|
441
451
|
const sessionCost = props.api.usage.readSessionCost(sid)
|
|
442
452
|
const sessionModel = props.api.usage.readSessionModel(sid)
|
|
@@ -529,7 +539,7 @@ export function SubAgentPanel(props: {
|
|
|
529
539
|
let changed = false
|
|
530
540
|
const next = new Map(prev)
|
|
531
541
|
for (const [id, entry] of next) {
|
|
532
|
-
if (entry.sessionId !== sid) continue
|
|
542
|
+
if (!isCurrentChild || entry.sessionId !== sid) continue
|
|
533
543
|
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested") continue
|
|
534
544
|
// Skip parent session idle — subagent entries belong to child sessions only
|
|
535
545
|
if (sid === props.sessionId) continue
|
|
@@ -548,7 +558,7 @@ export function SubAgentPanel(props: {
|
|
|
548
558
|
})
|
|
549
559
|
changed = true
|
|
550
560
|
}
|
|
551
|
-
if (!changed && sessionAgent) {
|
|
561
|
+
if (!changed && sessionAgent && isCurrentChild) {
|
|
552
562
|
const nowTs = Date.now()
|
|
553
563
|
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9-]/g, "")
|
|
554
564
|
const saNorm = normalize(sessionAgent)
|
|
@@ -594,8 +604,7 @@ export function SubAgentPanel(props: {
|
|
|
594
604
|
// 当子代理所属的父 session 与当前视图不同时,通过模块级缓存定位
|
|
595
605
|
// 并更新父 session 的 entry 状态,随后写回 KV。
|
|
596
606
|
try {
|
|
597
|
-
const
|
|
598
|
-
const parentSid = sessionObj?.parentID
|
|
607
|
+
const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID
|
|
599
608
|
if (parentSid && parentSid !== props.sessionId) {
|
|
600
609
|
// 优先从模块级缓存获取父 session 的 entries,不受当前视图切换影响
|
|
601
610
|
const parentCache = globalEntryCache.get(parentSid)
|
|
@@ -784,7 +793,12 @@ export function SubAgentPanel(props: {
|
|
|
784
793
|
|
|
785
794
|
const st = (part as any).state as Record<string, unknown> | undefined
|
|
786
795
|
const rawStatus = String(st?.status ?? "")
|
|
787
|
-
const
|
|
796
|
+
const scanStMeta = st?.metadata as Record<string, unknown> | undefined
|
|
797
|
+
const scanSubSid = scanStMeta?.session_id !== undefined ? String(scanStMeta.session_id)
|
|
798
|
+
: scanStMeta?.sessionId !== undefined ? String(scanStMeta.sessionId)
|
|
799
|
+
: undefined
|
|
800
|
+
const existingKey = findSubEntryKey(next, id, scanSubSid)
|
|
801
|
+
const exists = existingKey ? next.get(existingKey) : undefined
|
|
788
802
|
|
|
789
803
|
// 已手动清除的条目:scan 发现但不在内存 → 跳过重建
|
|
790
804
|
if (!exists && clearedIds.has(id)) continue
|
|
@@ -796,7 +810,7 @@ export function SubAgentPanel(props: {
|
|
|
796
810
|
// "error": only update existing, never create a new entry
|
|
797
811
|
if (rawStatus === "error") {
|
|
798
812
|
if (exists && exists.status === "running") {
|
|
799
|
-
next.set(id, { ...exists, status: "error", endedAt: Date.now() })
|
|
813
|
+
next.set(existingKey ?? id, { ...exists, status: "error", endedAt: Date.now() })
|
|
800
814
|
}
|
|
801
815
|
continue
|
|
802
816
|
}
|
|
@@ -812,7 +826,7 @@ export function SubAgentPanel(props: {
|
|
|
812
826
|
}
|
|
813
827
|
|
|
814
828
|
// Already settled → skip
|
|
815
|
-
if (exists && exists.status !== "running" && exists.status !== "cancel_requested") continue
|
|
829
|
+
if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested") continue
|
|
816
830
|
// Running entry with no explicit status improvement from part:
|
|
817
831
|
// try message-level heuristics first, then time-based fallback.
|
|
818
832
|
if (exists && status === "running") {
|
|
@@ -840,15 +854,12 @@ export function SubAgentPanel(props: {
|
|
|
840
854
|
const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40)
|
|
841
855
|
|
|
842
856
|
let tokens: number | undefined
|
|
843
|
-
const scanStMeta2 = st?.metadata as Record<string, unknown> | undefined
|
|
844
|
-
const scanSubSid = scanStMeta2?.session_id !== undefined ? String(scanStMeta2.session_id)
|
|
845
|
-
: scanStMeta2?.sessionId !== undefined ? String(scanStMeta2.sessionId)
|
|
846
|
-
: undefined
|
|
847
857
|
if (scanSubSid) tokens = props.api.usage.readSessionTokens(scanSubSid)
|
|
848
858
|
|
|
849
859
|
const ended = status === "done" // "error" handled above, never reaches here
|
|
850
|
-
|
|
851
|
-
|
|
860
|
+
const entryKey = existingKey ?? id
|
|
861
|
+
next.set(entryKey, {
|
|
862
|
+
id: exists?.id ?? id, title, agent, prompt,
|
|
852
863
|
// Preserve existing values (from handleSessionEnd / KV) — scan must not overwrite
|
|
853
864
|
tokens: exists?.tokens ?? tokens,
|
|
854
865
|
sessionId: exists?.sessionId ?? scanSubSid,
|
|
@@ -856,6 +867,7 @@ export function SubAgentPanel(props: {
|
|
|
856
867
|
startedAt: exists?.startedAt || Date.now(),
|
|
857
868
|
endedAt: ended ? (exists?.endedAt || Date.now()) : undefined,
|
|
858
869
|
})
|
|
870
|
+
if (entryKey !== id) next.delete(id)
|
|
859
871
|
}
|
|
860
872
|
}
|
|
861
873
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SubEntry } from "../core/types"
|
|
2
|
+
|
|
3
|
+
export type SubEntryPatch = Omit<SubEntry, "startedAt" | "endedAt"> & {
|
|
4
|
+
startedAt?: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function findSubEntryKey(
|
|
8
|
+
entries: ReadonlyMap<string, SubEntry>,
|
|
9
|
+
id: string,
|
|
10
|
+
sessionId?: string,
|
|
11
|
+
) {
|
|
12
|
+
if (sessionId) {
|
|
13
|
+
for (const [key, entry] of entries) {
|
|
14
|
+
if (entry.sessionId === sessionId) return key
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return entries.has(id) ? id : undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function upsertSubEntry(
|
|
21
|
+
entries: ReadonlyMap<string, SubEntry>,
|
|
22
|
+
partial: SubEntryPatch,
|
|
23
|
+
now = Date.now(),
|
|
24
|
+
) {
|
|
25
|
+
const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id
|
|
26
|
+
const existing = entries.get(key)
|
|
27
|
+
const next = new Map(entries)
|
|
28
|
+
if (key !== partial.id) next.delete(partial.id)
|
|
29
|
+
|
|
30
|
+
const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled"
|
|
31
|
+
next.set(key, {
|
|
32
|
+
...(existing ?? { startedAt: now }),
|
|
33
|
+
...partial,
|
|
34
|
+
id: existing?.id ?? partial.id,
|
|
35
|
+
startedAt: existing?.startedAt || partial.startedAt || now,
|
|
36
|
+
endedAt: ended ? (existing?.endedAt || now) : undefined,
|
|
37
|
+
})
|
|
38
|
+
return next
|
|
39
|
+
}
|
package/src/panel/panel-api.ts
CHANGED
|
@@ -1,72 +1,75 @@
|
|
|
1
|
-
import type { KVApi } from "../core/kv"
|
|
2
|
-
import type { UsageReader, TodoStats } from "../core/usage"
|
|
3
|
-
import type { Lang, SortOrder, ScrollMode } from "../core/types"
|
|
4
|
-
|
|
5
|
-
/** Minimal session shape the panel reads from the host SDK. */
|
|
6
|
-
export interface SessionLike {
|
|
7
|
-
id?: string
|
|
8
|
-
parentID?: string
|
|
9
|
-
agent?: string
|
|
10
|
-
cost?: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface SessionStatusLike {
|
|
14
|
-
type: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Standardized panel events. Adapter layers (V1/V2) translate host events
|
|
19
|
-
* into these shapes so the panel component never touches host APIs directly.
|
|
20
|
-
*/
|
|
21
|
-
export type PanelEventType = "part.updated" | "message.updated" | "session.idle" | "session.error"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
1
|
+
import type { KVApi } from "../core/kv"
|
|
2
|
+
import type { UsageReader, TodoStats } from "../core/usage"
|
|
3
|
+
import type { Lang, SortOrder, ScrollMode } from "../core/types"
|
|
4
|
+
|
|
5
|
+
/** Minimal session shape the panel reads from the host SDK. */
|
|
6
|
+
export interface SessionLike {
|
|
7
|
+
id?: string
|
|
8
|
+
parentID?: string
|
|
9
|
+
agent?: string
|
|
10
|
+
cost?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionStatusLike {
|
|
14
|
+
type: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Standardized panel events. Adapter layers (V1/V2) translate host events
|
|
19
|
+
* into these shapes so the panel component never touches host APIs directly.
|
|
20
|
+
*/
|
|
21
|
+
export type PanelEventType = "part.updated" | "message.updated" | "session.idle" | "session.error"
|
|
22
|
+
export type PanelEventScope = "session" | "global"
|
|
23
|
+
|
|
24
|
+
export interface PanelEvent {
|
|
25
|
+
type: PanelEventType
|
|
26
|
+
/** Global V2 events need explicit session ownership checks. */
|
|
27
|
+
scope?: PanelEventScope
|
|
28
|
+
/** part payload for part.updated; session properties for idle/error. */
|
|
29
|
+
payload?: Record<string, unknown>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ToastOptions {
|
|
33
|
+
variant?: "success" | "warning" | "error"
|
|
34
|
+
title?: string
|
|
35
|
+
duration?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The full surface the SubAgentPanel component consumes.
|
|
40
|
+
* V1 and V2 each implement this against their host SDK; the panel is agnostic.
|
|
41
|
+
* Theme is slot-scoped in both hosts, so it is passed as a panel prop,
|
|
42
|
+
* not part of the API.
|
|
43
|
+
*/
|
|
44
|
+
export interface PanelApi {
|
|
45
|
+
kv: KVApi
|
|
46
|
+
usage: UsageReader
|
|
47
|
+
session: {
|
|
48
|
+
get(sid: string): SessionLike | undefined
|
|
49
|
+
status(sid: string): SessionStatusLike | undefined
|
|
50
|
+
/** Raw message list for a session (scan / error extraction). */
|
|
51
|
+
messages(sid: string): unknown[] | undefined
|
|
52
|
+
/** Raw parts of a message (scan). */
|
|
53
|
+
part(messageID: string): unknown[] | undefined
|
|
54
|
+
}
|
|
55
|
+
event: {
|
|
56
|
+
on(type: PanelEventType, cb: (e: PanelEvent) => void): () => void
|
|
57
|
+
}
|
|
58
|
+
client: {
|
|
59
|
+
abort(input: { sessionID: string }): Promise<void>
|
|
60
|
+
}
|
|
61
|
+
route: {
|
|
62
|
+
navigateSession(sessionID: string): void
|
|
63
|
+
}
|
|
64
|
+
ui: {
|
|
65
|
+
toast(message: string, opts?: ToastOptions): void
|
|
66
|
+
}
|
|
67
|
+
settings: {
|
|
68
|
+
lang: () => Lang
|
|
69
|
+
maxEntries: () => number
|
|
70
|
+
sortOrder: () => SortOrder
|
|
71
|
+
scrollMode: () => ScrollMode
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type { TodoStats }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SessionLike } from "./panel-api"
|
|
2
|
+
|
|
3
|
+
/** Whether an end event belongs to a child tracked by the current panel. */
|
|
4
|
+
export function isDirectChildSession(
|
|
5
|
+
currentSessionId: string,
|
|
6
|
+
eventSessionId: string,
|
|
7
|
+
eventSession: SessionLike | undefined,
|
|
8
|
+
): boolean {
|
|
9
|
+
return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId
|
|
10
|
+
}
|
package/src/v2/v2-panel-api.ts
CHANGED
|
@@ -256,7 +256,8 @@ export function createPanelApi(context: Context, settings: PanelApi["settings"])
|
|
|
256
256
|
// 让面板只归账到正在查看的会话(V1 宿主已按会话 scope,无需该字段)。
|
|
257
257
|
const evt = (e as Record<string, any>).data as Record<string, any> | undefined
|
|
258
258
|
const sid = evt?.sessionID !== undefined ? String(evt.sessionID) : undefined
|
|
259
|
-
|
|
259
|
+
if (!sid) return
|
|
260
|
+
cb({ type, scope: "global", payload: { part, sessionID: sid } })
|
|
260
261
|
}))
|
|
261
262
|
}
|
|
262
263
|
return () => { for (const u of unsubs) u() }
|
|
@@ -275,7 +276,7 @@ export function createPanelApi(context: Context, settings: PanelApi["settings"])
|
|
|
275
276
|
for (const evt of ["session.execution.succeeded", "session.execution.interrupted"]) {
|
|
276
277
|
unsubs.push(context.data.on(evt, (e) => {
|
|
277
278
|
const sid = String(((e as Record<string, any>).data as Record<string, any> | undefined)?.sessionID ?? "")
|
|
278
|
-
if (sid) cb({ type, payload: { sessionID: sid } })
|
|
279
|
+
if (sid) cb({ type, scope: "global", payload: { sessionID: sid } })
|
|
279
280
|
}))
|
|
280
281
|
}
|
|
281
282
|
return () => { for (const u of unsubs) u() }
|
|
@@ -284,7 +285,7 @@ export function createPanelApi(context: Context, settings: PanelApi["settings"])
|
|
|
284
285
|
return context.data.on("session.execution.failed", (e) => {
|
|
285
286
|
const evt = (e as Record<string, any>).data as Record<string, any> | undefined
|
|
286
287
|
const sid = String(evt?.sessionID ?? "")
|
|
287
|
-
if (sid) cb({ type, payload: { sessionID: sid, error: evt?.error } })
|
|
288
|
+
if (sid) cb({ type, scope: "global", payload: { sessionID: sid, error: evt?.error } })
|
|
288
289
|
})
|
|
289
290
|
default:
|
|
290
291
|
return () => {}
|