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.
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.6.0-beta.1";
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.1";
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
- const m = new Map();
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.set(e.id, e);
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 (eventSid !== undefined && eventSid !== props.sessionId)
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 existing = entryMap().get(id);
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({ id, title: existing.title, agent: existing.agent, prompt: existing.prompt, status: "error" });
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 sessionObj = props.api.session.get(sid);
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 exists = next.get(id);
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
- next.set(id, {
834
- id, title, agent, prompt,
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
  }
@@ -0,0 +1,3 @@
1
+ import type { SessionLike } from "./panel-api";
2
+ /** Whether an end event belongs to a child tracked by the current panel. */
3
+ export declare function isDirectChildSession(currentSessionId: string, eventSessionId: string, eventSession: SessionLike | undefined): boolean;
@@ -0,0 +1,4 @@
1
+ /** Whether an end event belongs to a child tracked by the current panel. */
2
+ export function isDirectChildSession(currentSessionId, eventSessionId, eventSession) {
3
+ return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId;
4
+ }
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.1";
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
- const m = /* @__PURE__ */ new Map();
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) m.set(e.id, e);
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 (eventSid !== void 0 && eventSid !== props.sessionId) return;
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 existing = entryMap().get(id2);
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: id2,
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 sessionObj = props.api.session.get(sid);
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 exists = next.get(id);
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 scanStMeta = st?.metadata;
1335
- const scanHasChild = scanStMeta?.session_id !== void 0 || scanStMeta?.sessionId !== void 0;
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
- next.set(id, {
1364
- id,
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
  }
@@ -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
- cb({ type, payload: { part, sessionID: sid } });
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
- cb({ type, payload: { part, sessionID: sid } });
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.1";
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
- const m = /* @__PURE__ */ new Map();
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) m.set(e.id, e);
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 (eventSid !== void 0 && eventSid !== props.sessionId) return;
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 existing = entryMap().get(id2);
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: id2,
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 sessionObj = props.api.session.get(sid);
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 exists = next.get(id);
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 scanStMeta = st?.metadata;
1901
- const scanHasChild = scanStMeta?.session_id !== void 0 || scanStMeta?.sessionId !== void 0;
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
- next.set(id, {
1930
- id,
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.1",
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.1";
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
- const m = new Map<string, SubEntry>()
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) m.set(e.id, e)
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 (eventSid !== undefined && eventSid !== props.sessionId) return
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 existing = entryMap().get(id)
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({ id, title: existing.title, agent: existing.agent, prompt: existing.prompt, status: "error" })
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 sessionObj = props.api.session.get(sid)
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 exists = next.get(id)
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
- next.set(id, {
851
- id, title, agent, prompt,
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
+ }
@@ -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
- export interface PanelEvent {
24
- type: PanelEventType
25
- /** part payload for part.updated; session properties for idle/error. */
26
- payload?: Record<string, unknown>
27
- }
28
-
29
- export interface ToastOptions {
30
- variant?: "success" | "warning" | "error"
31
- title?: string
32
- duration?: number
33
- }
34
-
35
- /**
36
- * The full surface the SubAgentPanel component consumes.
37
- * V1 and V2 each implement this against their host SDK; the panel is agnostic.
38
- * Theme is slot-scoped in both hosts, so it is passed as a panel prop,
39
- * not part of the API.
40
- */
41
- export interface PanelApi {
42
- kv: KVApi
43
- usage: UsageReader
44
- session: {
45
- get(sid: string): SessionLike | undefined
46
- status(sid: string): SessionStatusLike | undefined
47
- /** Raw message list for a session (scan / error extraction). */
48
- messages(sid: string): unknown[] | undefined
49
- /** Raw parts of a message (scan). */
50
- part(messageID: string): unknown[] | undefined
51
- }
52
- event: {
53
- on(type: PanelEventType, cb: (e: PanelEvent) => void): () => void
54
- }
55
- client: {
56
- abort(input: { sessionID: string }): Promise<void>
57
- }
58
- route: {
59
- navigateSession(sessionID: string): void
60
- }
61
- ui: {
62
- toast(message: string, opts?: ToastOptions): void
63
- }
64
- settings: {
65
- lang: () => Lang
66
- maxEntries: () => number
67
- sortOrder: () => SortOrder
68
- scrollMode: () => ScrollMode
69
- }
70
- }
71
-
72
- export type { TodoStats }
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
+ }
@@ -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
- cb({ type, payload: { part, sessionID: sid } })
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 () => {}