opencode-subagent-magazine 1.6.0-beta.1 → 1.6.0-beta.2

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.2";
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.2";
@@ -8,6 +8,7 @@ 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";
11
12
  /** Entry line left prefix: icon + space + status dot + space */
12
13
  const LEFT_PAD = 4;
13
14
  /** Detail row indent: two spaces */
@@ -337,7 +338,7 @@ export function SubAgentPanel(props) {
337
338
  // V2 事件流是全局的——若 payload 带发起会话 ID,则只归账到正在查看的会话,
338
339
  // 避免其他会话的子代理写进当前侧边栏;V1 宿主已按会话 scope,不传 sessionID。
339
340
  const eventSid = event.payload?.sessionID !== undefined ? String(event.payload.sessionID) : undefined;
340
- if (eventSid !== undefined && eventSid !== props.sessionId)
341
+ if (event.scope === "global" && eventSid !== props.sessionId)
341
342
  return;
342
343
  // SubtaskPart
343
344
  if (part.type === "subtask") {
@@ -408,6 +409,17 @@ export function SubAgentPanel(props) {
408
409
  const sid = String(props_?.sessionID ?? "");
409
410
  if (!sid)
410
411
  return;
412
+ const isGlobalEvent = event.scope === "global";
413
+ let eventSession;
414
+ if (isGlobalEvent) {
415
+ try {
416
+ eventSession = props.api.session.get(sid);
417
+ }
418
+ catch {
419
+ eventSession = undefined;
420
+ }
421
+ }
422
+ const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession);
411
423
  const sessionTokens = props.api.usage.readSessionTokens(sid);
412
424
  const sessionCost = props.api.usage.readSessionCost(sid);
413
425
  const sessionModel = props.api.usage.readSessionModel(sid);
@@ -501,7 +513,7 @@ export function SubAgentPanel(props) {
501
513
  let changed = false;
502
514
  const next = new Map(prev);
503
515
  for (const [id, entry] of next) {
504
- if (entry.sessionId !== sid)
516
+ if (!isCurrentChild || entry.sessionId !== sid)
505
517
  continue;
506
518
  if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested")
507
519
  continue;
@@ -523,7 +535,7 @@ export function SubAgentPanel(props) {
523
535
  });
524
536
  changed = true;
525
537
  }
526
- if (!changed && sessionAgent) {
538
+ if (!changed && sessionAgent && isCurrentChild) {
527
539
  const nowTs = Date.now();
528
540
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
529
541
  const saNorm = normalize(sessionAgent);
@@ -572,8 +584,7 @@ export function SubAgentPanel(props) {
572
584
  // 当子代理所属的父 session 与当前视图不同时,通过模块级缓存定位
573
585
  // 并更新父 session 的 entry 状态,随后写回 KV。
574
586
  try {
575
- const sessionObj = props.api.session.get(sid);
576
- const parentSid = sessionObj?.parentID;
587
+ const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID;
577
588
  if (parentSid && parentSid !== props.sessionId) {
578
589
  // 优先从模块级缓存获取父 session 的 entries,不受当前视图切换影响
579
590
  const parentCache = globalEntryCache.get(parentSid);
@@ -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.2";
10
10
 
11
11
  // src/i18n.ts
12
12
  var ZH_T = {
@@ -485,6 +485,11 @@ 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
+
488
493
  // src/panel/SubAgentPanel.tsx
489
494
  var LEFT_PAD = 4;
490
495
  var INDENT = 2;
@@ -902,7 +907,7 @@ function SubAgentPanel(props) {
902
907
  const part = event.payload?.part;
903
908
  if (!part) return;
904
909
  const eventSid = event.payload?.sessionID !== void 0 ? String(event.payload.sessionID) : void 0;
905
- if (eventSid !== void 0 && eventSid !== props.sessionId) return;
910
+ if (event.scope === "global" && eventSid !== props.sessionId) return;
906
911
  if (part.type === "subtask") {
907
912
  const agent = String(part.agent ?? "?");
908
913
  const prompt = String(part.prompt ?? "");
@@ -972,6 +977,16 @@ function SubAgentPanel(props) {
972
977
  const props_ = event.payload;
973
978
  const sid = String(props_?.sessionID ?? "");
974
979
  if (!sid) return;
980
+ const isGlobalEvent = event.scope === "global";
981
+ let eventSession;
982
+ if (isGlobalEvent) {
983
+ try {
984
+ eventSession = props.api.session.get(sid);
985
+ } catch {
986
+ eventSession = void 0;
987
+ }
988
+ }
989
+ const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession);
975
990
  const sessionTokens = props.api.usage.readSessionTokens(sid);
976
991
  const sessionCost = props.api.usage.readSessionCost(sid);
977
992
  const sessionModel = props.api.usage.readSessionModel(sid);
@@ -1060,7 +1075,7 @@ function SubAgentPanel(props) {
1060
1075
  let changed = false;
1061
1076
  const next = new Map(prev);
1062
1077
  for (const [id, entry] of next) {
1063
- if (entry.sessionId !== sid) continue;
1078
+ if (!isCurrentChild || entry.sessionId !== sid) continue;
1064
1079
  if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested") continue;
1065
1080
  if (sid === props.sessionId) continue;
1066
1081
  const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested";
@@ -1080,7 +1095,7 @@ function SubAgentPanel(props) {
1080
1095
  });
1081
1096
  changed = true;
1082
1097
  }
1083
- if (!changed && sessionAgent) {
1098
+ if (!changed && sessionAgent && isCurrentChild) {
1084
1099
  const nowTs = Date.now();
1085
1100
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
1086
1101
  const saNorm = normalize(sessionAgent);
@@ -1125,8 +1140,7 @@ function SubAgentPanel(props) {
1125
1140
  return changed ? next : prev;
1126
1141
  });
1127
1142
  try {
1128
- const sessionObj = props.api.session.get(sid);
1129
- const parentSid = sessionObj?.parentID;
1143
+ const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID;
1130
1144
  if (parentSid && parentSid !== props.sessionId) {
1131
1145
  const parentCache = globalEntryCache.get(parentSid);
1132
1146
  const nowTs = Date.now();
@@ -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.2";
332
333
 
333
334
  // src/i18n.ts
334
335
  var ZH_T = {
@@ -1051,6 +1052,11 @@ 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
+
1054
1060
  // src/panel/SubAgentPanel.tsx
1055
1061
  var LEFT_PAD = 4;
1056
1062
  var INDENT = 2;
@@ -1468,7 +1474,7 @@ function SubAgentPanel(props) {
1468
1474
  const part = event.payload?.part;
1469
1475
  if (!part) return;
1470
1476
  const eventSid = event.payload?.sessionID !== void 0 ? String(event.payload.sessionID) : void 0;
1471
- if (eventSid !== void 0 && eventSid !== props.sessionId) return;
1477
+ if (event.scope === "global" && eventSid !== props.sessionId) return;
1472
1478
  if (part.type === "subtask") {
1473
1479
  const agent = String(part.agent ?? "?");
1474
1480
  const prompt = String(part.prompt ?? "");
@@ -1538,6 +1544,16 @@ function SubAgentPanel(props) {
1538
1544
  const props_ = event.payload;
1539
1545
  const sid = String(props_?.sessionID ?? "");
1540
1546
  if (!sid) return;
1547
+ const isGlobalEvent = event.scope === "global";
1548
+ let eventSession;
1549
+ if (isGlobalEvent) {
1550
+ try {
1551
+ eventSession = props.api.session.get(sid);
1552
+ } catch {
1553
+ eventSession = void 0;
1554
+ }
1555
+ }
1556
+ const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession);
1541
1557
  const sessionTokens = props.api.usage.readSessionTokens(sid);
1542
1558
  const sessionCost = props.api.usage.readSessionCost(sid);
1543
1559
  const sessionModel = props.api.usage.readSessionModel(sid);
@@ -1626,7 +1642,7 @@ function SubAgentPanel(props) {
1626
1642
  let changed = false;
1627
1643
  const next = new Map(prev);
1628
1644
  for (const [id, entry] of next) {
1629
- if (entry.sessionId !== sid) continue;
1645
+ if (!isCurrentChild || entry.sessionId !== sid) continue;
1630
1646
  if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested") continue;
1631
1647
  if (sid === props.sessionId) continue;
1632
1648
  const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested";
@@ -1646,7 +1662,7 @@ function SubAgentPanel(props) {
1646
1662
  });
1647
1663
  changed = true;
1648
1664
  }
1649
- if (!changed && sessionAgent) {
1665
+ if (!changed && sessionAgent && isCurrentChild) {
1650
1666
  const nowTs = Date.now();
1651
1667
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
1652
1668
  const saNorm = normalize(sessionAgent);
@@ -1691,8 +1707,7 @@ function SubAgentPanel(props) {
1691
1707
  return changed ? next : prev;
1692
1708
  });
1693
1709
  try {
1694
- const sessionObj = props.api.session.get(sid);
1695
- const parentSid = sessionObj?.parentID;
1710
+ const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID;
1696
1711
  if (parentSid && parentSid !== props.sessionId) {
1697
1712
  const parentCache = globalEntryCache.get(parentSid);
1698
1713
  const nowTs = Date.now();
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.2",
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.2";
@@ -21,6 +21,7 @@ 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"
24
25
 
25
26
  /** Entry line left prefix: icon + space + status dot + space */
26
27
  const LEFT_PAD = 4
@@ -363,7 +364,7 @@ export function SubAgentPanel(props: {
363
364
  // V2 事件流是全局的——若 payload 带发起会话 ID,则只归账到正在查看的会话,
364
365
  // 避免其他会话的子代理写进当前侧边栏;V1 宿主已按会话 scope,不传 sessionID。
365
366
  const eventSid = event.payload?.sessionID !== undefined ? String(event.payload.sessionID) : undefined
366
- if (eventSid !== undefined && eventSid !== props.sessionId) return
367
+ if (event.scope === "global" && eventSid !== props.sessionId) return
367
368
 
368
369
  // SubtaskPart
369
370
  if (part.type === "subtask") {
@@ -437,6 +438,13 @@ export function SubAgentPanel(props: {
437
438
  const sid = String(props_?.sessionID ?? "")
438
439
  if (!sid) return
439
440
 
441
+ const isGlobalEvent = event.scope === "global"
442
+ let eventSession: ReturnType<PanelApi["session"]["get"]>
443
+ if (isGlobalEvent) {
444
+ try { eventSession = props.api.session.get(sid) } catch { eventSession = undefined }
445
+ }
446
+ const isCurrentChild = !isGlobalEvent || isDirectChildSession(props.sessionId, sid, eventSession)
447
+
440
448
  const sessionTokens = props.api.usage.readSessionTokens(sid)
441
449
  const sessionCost = props.api.usage.readSessionCost(sid)
442
450
  const sessionModel = props.api.usage.readSessionModel(sid)
@@ -529,7 +537,7 @@ export function SubAgentPanel(props: {
529
537
  let changed = false
530
538
  const next = new Map(prev)
531
539
  for (const [id, entry] of next) {
532
- if (entry.sessionId !== sid) continue
540
+ if (!isCurrentChild || entry.sessionId !== sid) continue
533
541
  if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested") continue
534
542
  // Skip parent session idle — subagent entries belong to child sessions only
535
543
  if (sid === props.sessionId) continue
@@ -548,7 +556,7 @@ export function SubAgentPanel(props: {
548
556
  })
549
557
  changed = true
550
558
  }
551
- if (!changed && sessionAgent) {
559
+ if (!changed && sessionAgent && isCurrentChild) {
552
560
  const nowTs = Date.now()
553
561
  const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9-]/g, "")
554
562
  const saNorm = normalize(sessionAgent)
@@ -594,8 +602,7 @@ export function SubAgentPanel(props: {
594
602
  // 当子代理所属的父 session 与当前视图不同时,通过模块级缓存定位
595
603
  // 并更新父 session 的 entry 状态,随后写回 KV。
596
604
  try {
597
- const sessionObj = props.api.session.get(sid)
598
- const parentSid = sessionObj?.parentID
605
+ const parentSid = (isGlobalEvent ? eventSession : props.api.session.get(sid))?.parentID
599
606
  if (parentSid && parentSid !== props.sessionId) {
600
607
  // 优先从模块级缓存获取父 session 的 entries,不受当前视图切换影响
601
608
  const parentCache = globalEntryCache.get(parentSid)
@@ -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 () => {}