pantheon-opencode 1.5.0-beta.17 → 1.5.0-beta.19

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/CHANGELOG.md CHANGED
@@ -18,6 +18,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
18
18
 
19
19
  ## ✅ Closed Issues
20
20
 
21
+ ## [v1.5.0-beta.19] - 2026-09-16
22
+
23
+ <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
24
+ move this section to a versioned entry and reset the template below. -->
25
+
26
+ ## 🆕 What's New
27
+
28
+ - **Improved — painel Delegations**: status ausente de children antigas agora é
29
+ `done` (não `running`); grace 60s para sessões recém-criadas; janela de
30
+ recência 24h (filtra histórico); ordenação newest-first; ceiling esconde
31
+ antigos.
32
+ - **Improved — animação**: spinner ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ agora tem timer
33
+ independente (80ms/frame), separado do poll de dados — fluido, não mais
34
+ "travado".
35
+ ## [v1.5.0-beta.18] - 2026-09-16
36
+
37
+ <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
38
+ move this section to a versioned entry and reset the template below. -->
39
+
40
+ ## 🐞 Fixed
41
+
42
+ - **Painel Delegations voltou a funcionar (crítico):** o canal
43
+ `session.children` da TUI chamava a API com a shape v1 (`{ path: { id } }`)
44
+ num cliente **v2** que espera `{ sessionID }`; o placeholder não era
45
+ substituído (`/session/%7BsessionID%7D/children`), o host rejeitava cada
46
+ poll (~181k erros/s) e o painel ficava vazio ("No delegations"). Corrigido
47
+ `safeSessionPath`/`buildChildrenPath` para `{ sessionID }`, removidos os
48
+ `as any` que escondiam o mismatch, + teste de regressão exato. Auditoria
49
+ confirmou que só a TUI (v2) estava errada (o `plugin.ts` usa SDK v1 de
50
+ propósito).
51
+ - **Higiene de testes:** testes que importam o plugin sem isolamento agora
52
+ fazem `chdir` para um projeto temporário antes do import — não criam mais
53
+ entradas no board real (fim dos estados fantasma em desenvolvimento).
54
+
21
55
  ## [v1.5.0-beta.17] - 2026-09-15
22
56
 
23
57
  <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.5.0-beta.17",
3
+ "version": "1.5.0-beta.19",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon",
3
3
  "publisher": "ils15",
4
- "version": "1.5.0-beta.17",
4
+ "version": "1.5.0-beta.19",
5
5
  "displayName": "Pantheon",
6
6
  "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
7
7
  "agents": "./src/agents",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
4
4
 
5
5
  [project]
6
6
  name = "pantheon"
7
- version = "1.5.0-beta.17"
7
+ version = "1.5.0-beta.19"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
@@ -104,6 +104,24 @@ declare const IDLE_SILENCE_MS: number;
104
104
  * constants only control which rows enter the TUI window. */
105
105
  declare const DELEGATION_DONE_RETENTION_MS: number;
106
106
  declare const DELEGATION_FAILED_RETENTION_MS: number;
107
+ /** Grace window for an ABSENT child status. `api.state.session.status()` only
108
+ * carries the sessions that are currently ALIVE; every finished child of a
109
+ * long-lived session is missing from that map. A child with no status is
110
+ * therefore treated as terminal (done) — treating it as running made every
111
+ * historical child of the session show up as "active" (the "197 children,
112
+ * all active" regression). The one exception is a freshly-spawned child that
113
+ * has not been registered by the status API yet: within this window of its
114
+ * last activity (`time.updated`, falling back to `time.created`) it still
115
+ * reads as running. */
116
+ declare const DELEGATION_CHILD_STATUS_GRACE_MS: number;
117
+ /** Recency window for the children channel. `session.children` returns EVERY
118
+ * child the focused session ever spawned, so a long session accumulates
119
+ * hundreds of historical rows that would inflate the panel. A child whose
120
+ * last activity (its own time, or its matched report's Finalized timestamp)
121
+ * is older than this window is dropped BEFORE it can enter the list. 24h
122
+ * keeps the sidebar scoped to current work while still covering long
123
+ * delegations. A child with no timestamp at all is kept (fail-open). */
124
+ declare const DELEGATION_CHILDREN_RECENCY_MS: number;
107
125
  /** Alias-less NATIVE task() live entries never receive a report alias (the
108
126
  * task tool output carries none), so the 30s alias-less prune in
109
127
  * mergeChildDelegationSources must not apply to them — 5 minutes covers a
@@ -130,9 +148,10 @@ declare function delegationElapsed(entry: DelegationEntry, now: number): string;
130
148
  type DelegationActivity = 'delegating' | 'working' | 'reading' | 'completed' | 'error' | 'cancelled';
131
149
  declare function delegationActivity(entry: DelegationEntry): DelegationActivity;
132
150
  declare function delegationActivityLabel(entry: DelegationEntry): string;
133
- /** Return a deterministic spinner frame. The View ticks this every 1000ms
134
- * (not 140ms — the fast tick flickered without adding information). */
135
- declare function delegationSpinnerFrame(now: number): string;
151
+ /** Return the spinner glyph for the given frame counter. The frame counter
152
+ * is incremented every ~80ms by a dedicated animation timer, decoupled from
153
+ * the data poll cycle. Pure — no side effects. */
154
+ declare function delegationSpinnerFrame(frame: number): string;
136
155
  /** Every state the row knows how to draw: the delegation lifecycle states
137
156
  * derived from the children status + md reports (`completed`, `error`,
138
157
  * `cancelled`, `startup_failed`, `startup_unknown`) plus the TUI-only
@@ -326,25 +345,26 @@ type TuiSessionSources = {
326
345
  * fetch (empty panel, zero errors). Pure — no I/O, no runtime required. */
327
346
  declare function resolveCurrentSessionID(sources: TuiSessionSources): string | null;
328
347
  /** THE single choke point for every `session.children` / session-API path.
329
- * Returns `{ path: { id } }` ONLY for a server-valid session id; returns
330
- * null for anything else (placeholder, empty, foreign id) so the caller
331
- * skips the call entirely instead of sending an unsubstituted placeholder
332
- * (the "%7BsessionID%7D" regression). Every session-API call site MUST go
333
- * through this function (enforced by the source-scan test in
334
- * tests/pantheon/tui-delegations.test.ts). */
348
+ * Returns the v2 SDK parameter shape `{ sessionID }` ONLY for a
349
+ * server-valid session id; returns null for anything else (placeholder,
350
+ * empty, foreign id) so the caller skips the call entirely instead of
351
+ * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression).
352
+ * The TUI client is `@opencode-ai/sdk/v2`, whose session methods take a
353
+ * FLAT parameter object (`{ sessionID }`), NOT the v1 `{ path: { id } }`
354
+ * envelope — passing the v1 shape left the v2 `{sessionID}` URL template
355
+ * unsubstituted (`/session/%7BsessionID%7D/children`). Every session-API
356
+ * call site MUST go through this function (enforced by the source-scan
357
+ * test in tests/pantheon/tui-delegations.test.ts). */
335
358
  declare function safeSessionPath(id: unknown): {
336
- path: {
337
- id: string;
338
- };
359
+ sessionID: string;
339
360
  } | null;
340
- /** Build the `session.children` path ONLY from a validated session id.
361
+ /** Build the `session.children` parameters ONLY from a validated session id.
341
362
  * Delegates to {@link safeSessionPath} — the single choke point. Returns
342
- * null for null/invalid ids so the caller skips the fetch instead of
343
- * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression). */
363
+ * the v2 SDK shape `{ sessionID }`; null for null/invalid ids so the caller
364
+ * skips the fetch instead of sending an unsubstituted placeholder (the
365
+ * "%7BsessionID%7D" regression). */
344
366
  declare function buildChildrenPath(id: string | null | undefined): {
345
- path: {
346
- id: string;
347
- };
367
+ sessionID: string;
348
368
  } | null;
349
369
  /** Duck-typed subset of a child Session (+ its live status type). */
350
370
  type ChildDelegationLike = {
@@ -363,11 +383,27 @@ type ChildDelegationLike = {
363
383
  updated?: number;
364
384
  };
365
385
  };
366
- /** Map a child status type to a display state. busy/retry → running
367
- * (the child is actively working), idle → completed, unknown → running
368
- * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
369
- * correct it as soon as terminal data exists). */
370
- declare function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry';
386
+ /** Map a child status type to a display state.
387
+ *
388
+ * Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is
389
+ * any other/unknown status — a status the panel does not recognise is not
390
+ * evidence of activity. An ABSENT status is the historical case: the status
391
+ * map (`api.state.session.status`) contains only currently-alive sessions, so
392
+ * every finished child of a long session is missing from it. Defaulting that
393
+ * to running made the whole session history render as "active" (the
394
+ * "197 children, all active" regression), so an absent status is terminal
395
+ * (done) UNLESS the child was active within `graceMs` — a just-spawned child
396
+ * that the status API has not registered yet must still render as running.
397
+ *
398
+ * @param status the `api.state.session.status(id)?.type`, or undefined
399
+ * @param time the child session time fields (`created`/`updated`)
400
+ * @param now current wall-clock ms (injectable for tests)
401
+ * @param graceMs recency window for the absent-status running assumption
402
+ * Pure — no I/O. */
403
+ declare function childStatusToState(status: string | undefined, time?: {
404
+ created?: number;
405
+ updated?: number;
406
+ }, now?: number, graceMs?: number): 'running' | 'completed' | 'retry';
371
407
  /** Status-only row model: ONE glyph + ONE identity per row.
372
408
  *
373
409
  * A row is
@@ -385,9 +421,9 @@ declare function delegationRowStatus(state: DelegationDisplayState): DelegationR
385
421
  declare const DELEGATION_ROW_GLYPHS: Readonly<Record<DelegationRowStatus, string>>;
386
422
  declare function delegationRowGlyph(status: DelegationRowStatus): string;
387
423
  /** Row marker (`<glyph> `) — the single state channel. `active` animates
388
- * through the 1s spinner for the given tick; every other kind is static.
389
- * Pure. */
390
- declare function delegationRowMarker(state: DelegationDisplayState, now?: number): string;
424
+ * through the spinner for the given frame counter (incremented every ~80ms);
425
+ * every other kind is static. Pure. */
426
+ declare function delegationRowMarker(state: DelegationDisplayState, frame?: number): string;
391
427
  /** ONE identity per row: the short report alias (`apo-1`); a row without a
392
428
  * report alias (native task() child `native-task` / `native-<id>`, or an
393
429
  * alias-less native live row `live-<callID>` kept as children-only) shows
@@ -472,5 +508,5 @@ declare const plugin: TuiPluginModule & {
472
508
  setup: () => Promise<void>;
473
509
  };
474
510
  //#endregion
475
- export { ChildDelegationLike, DELEGATION_ALIAS_WIDTH, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, DelegationActivity, DelegationDisplayState, DelegationEntry, DelegationRowStatus, DelegationStateTone, DelegationToolPart, IDLE_SILENCE_MS, LiveDelegationEntry, LiveDelegationStore, NATIVE_LIVE_ALIASLESS_TTL_MS, ParsedDelegationToolPart, STALE_RUNNING_THRESHOLD_MS, ToolActivity, TuiSessionSources, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
511
+ export { ChildDelegationLike, DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, DelegationActivity, DelegationDisplayState, DelegationEntry, DelegationRowStatus, DelegationStateTone, DelegationToolPart, IDLE_SILENCE_MS, LiveDelegationEntry, LiveDelegationStore, NATIVE_LIVE_ALIASLESS_TTL_MS, ParsedDelegationToolPart, STALE_RUNNING_THRESHOLD_MS, ToolActivity, TuiSessionSources, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
476
512
  //# sourceMappingURL=tui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}
1
+ {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}
@@ -165,7 +165,7 @@ async function detectVersion(api) {
165
165
  if (ver) return ver;
166
166
  }
167
167
  } catch {}
168
- return "1.5.0-beta.17";
168
+ return "1.5.0-beta.19";
169
169
  }
170
170
  /**
171
171
  * usage-bar — AI subscription usage gauge for the opencode TUI.
@@ -914,6 +914,24 @@ const IDLE_SILENCE_MS = 60 * 1e3;
914
914
  * constants only control which rows enter the TUI window. */
915
915
  const DELEGATION_DONE_RETENTION_MS = 120 * 1e3;
916
916
  const DELEGATION_FAILED_RETENTION_MS = 600 * 1e3;
917
+ /** Grace window for an ABSENT child status. `api.state.session.status()` only
918
+ * carries the sessions that are currently ALIVE; every finished child of a
919
+ * long-lived session is missing from that map. A child with no status is
920
+ * therefore treated as terminal (done) — treating it as running made every
921
+ * historical child of the session show up as "active" (the "197 children,
922
+ * all active" regression). The one exception is a freshly-spawned child that
923
+ * has not been registered by the status API yet: within this window of its
924
+ * last activity (`time.updated`, falling back to `time.created`) it still
925
+ * reads as running. */
926
+ const DELEGATION_CHILD_STATUS_GRACE_MS = 60 * 1e3;
927
+ /** Recency window for the children channel. `session.children` returns EVERY
928
+ * child the focused session ever spawned, so a long session accumulates
929
+ * hundreds of historical rows that would inflate the panel. A child whose
930
+ * last activity (its own time, or its matched report's Finalized timestamp)
931
+ * is older than this window is dropped BEFORE it can enter the list. 24h
932
+ * keeps the sidebar scoped to current work while still covering long
933
+ * delegations. A child with no timestamp at all is kept (fail-open). */
934
+ const DELEGATION_CHILDREN_RECENCY_MS = 1440 * 60 * 1e3;
917
935
  /** Alias-less NATIVE task() live entries never receive a report alias (the
918
936
  * task tool output carries none), so the 30s alias-less prune in
919
937
  * mergeChildDelegationSources must not apply to them — 5 minutes covers a
@@ -989,10 +1007,11 @@ const DELEGATION_SPINNER_FRAMES = [
989
1007
  "⠇",
990
1008
  "⠏"
991
1009
  ];
992
- /** Return a deterministic spinner frame. The View ticks this every 1000ms
993
- * (not 140ms — the fast tick flickered without adding information). */
994
- function delegationSpinnerFrame(now) {
995
- const index = Math.floor(Math.max(0, now) / 1e3) % DELEGATION_SPINNER_FRAMES.length;
1010
+ /** Return the spinner glyph for the given frame counter. The frame counter
1011
+ * is incremented every ~80ms by a dedicated animation timer, decoupled from
1012
+ * the data poll cycle. Pure — no side effects. */
1013
+ function delegationSpinnerFrame(frame) {
1014
+ const index = Math.abs(Math.floor(frame)) % DELEGATION_SPINNER_FRAMES.length;
996
1015
  return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0];
997
1016
  }
998
1017
  /** Every state the row knows how to draw: the delegation lifecycle states
@@ -1428,32 +1447,53 @@ function resolveCurrentSessionID(sources) {
1428
1447
  return null;
1429
1448
  }
1430
1449
  /** THE single choke point for every `session.children` / session-API path.
1431
- * Returns `{ path: { id } }` ONLY for a server-valid session id; returns
1432
- * null for anything else (placeholder, empty, foreign id) so the caller
1433
- * skips the call entirely instead of sending an unsubstituted placeholder
1434
- * (the "%7BsessionID%7D" regression). Every session-API call site MUST go
1435
- * through this function (enforced by the source-scan test in
1436
- * tests/pantheon/tui-delegations.test.ts). */
1450
+ * Returns the v2 SDK parameter shape `{ sessionID }` ONLY for a
1451
+ * server-valid session id; returns null for anything else (placeholder,
1452
+ * empty, foreign id) so the caller skips the call entirely instead of
1453
+ * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression).
1454
+ * The TUI client is `@opencode-ai/sdk/v2`, whose session methods take a
1455
+ * FLAT parameter object (`{ sessionID }`), NOT the v1 `{ path: { id } }`
1456
+ * envelope — passing the v1 shape left the v2 `{sessionID}` URL template
1457
+ * unsubstituted (`/session/%7BsessionID%7D/children`). Every session-API
1458
+ * call site MUST go through this function (enforced by the source-scan
1459
+ * test in tests/pantheon/tui-delegations.test.ts). */
1437
1460
  function safeSessionPath(id) {
1438
1461
  if (!isValidSessionId(id)) return null;
1439
- return { path: { id } };
1462
+ return { sessionID: id };
1440
1463
  }
1441
- /** Build the `session.children` path ONLY from a validated session id.
1464
+ /** Build the `session.children` parameters ONLY from a validated session id.
1442
1465
  * Delegates to {@link safeSessionPath} — the single choke point. Returns
1443
- * null for null/invalid ids so the caller skips the fetch instead of
1444
- * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression). */
1466
+ * the v2 SDK shape `{ sessionID }`; null for null/invalid ids so the caller
1467
+ * skips the fetch instead of sending an unsubstituted placeholder (the
1468
+ * "%7BsessionID%7D" regression). */
1445
1469
  function buildChildrenPath(id) {
1446
1470
  return safeSessionPath(id);
1447
1471
  }
1448
1472
  /** Duck-typed subset of a child Session (+ its live status type). */
1449
- /** Map a child status type to a display state. busy/retry → running
1450
- * (the child is actively working), idle → completed, unknown → running
1451
- * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
1452
- * correct it as soon as terminal data exists). */
1453
- function childStatusToState(status) {
1454
- if (status === "idle") return "completed";
1473
+ /** Map a child status type to a display state.
1474
+ *
1475
+ * Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is
1476
+ * any other/unknown status — a status the panel does not recognise is not
1477
+ * evidence of activity. An ABSENT status is the historical case: the status
1478
+ * map (`api.state.session.status`) contains only currently-alive sessions, so
1479
+ * every finished child of a long session is missing from it. Defaulting that
1480
+ * to running made the whole session history render as "active" (the
1481
+ * "197 children, all active" regression), so an absent status is terminal
1482
+ * (done) UNLESS the child was active within `graceMs` — a just-spawned child
1483
+ * that the status API has not registered yet must still render as running.
1484
+ *
1485
+ * @param status the `api.state.session.status(id)?.type`, or undefined
1486
+ * @param time the child session time fields (`created`/`updated`)
1487
+ * @param now current wall-clock ms (injectable for tests)
1488
+ * @param graceMs recency window for the absent-status running assumption
1489
+ * Pure — no I/O. */
1490
+ function childStatusToState(status, time, now = Date.now(), graceMs = DELEGATION_CHILD_STATUS_GRACE_MS) {
1455
1491
  if (status === "retry") return "retry";
1456
- return "running";
1492
+ if (status === "busy") return "running";
1493
+ if (status !== void 0) return "completed";
1494
+ const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0);
1495
+ if (lastActivity > 0 && now - lastActivity < graceMs) return "running";
1496
+ return "completed";
1457
1497
  }
1458
1498
  /** Status-only row model: ONE glyph + ONE identity per row.
1459
1499
  *
@@ -1489,11 +1529,11 @@ function delegationRowGlyph(status) {
1489
1529
  return DELEGATION_ROW_GLYPHS[status];
1490
1530
  }
1491
1531
  /** Row marker (`<glyph> `) — the single state channel. `active` animates
1492
- * through the 1s spinner for the given tick; every other kind is static.
1493
- * Pure. */
1494
- function delegationRowMarker(state, now = Date.now()) {
1532
+ * through the spinner for the given frame counter (incremented every ~80ms);
1533
+ * every other kind is static. Pure. */
1534
+ function delegationRowMarker(state, frame = 0) {
1495
1535
  const status = delegationRowStatus(state);
1496
- return `${status === "active" ? delegationSpinnerFrame(now) : delegationRowGlyph(status)} `;
1536
+ return `${status === "active" ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)} `;
1497
1537
  }
1498
1538
  /** ONE identity per row: the short report alias (`apo-1`); a row without a
1499
1539
  * report alias (native task() child `native-task` / `native-<id>`, or an
@@ -1632,7 +1672,10 @@ function formatPanelLogLine(children, pantheon, native, md, events) {
1632
1672
  * `parentSessionID` (the focused session id) is stamped on report-less
1633
1673
  * children so the row carries its origin session; omit it and they stay
1634
1674
  * unscoped ('').
1635
- * Sorted running-first (compareDelegationEntries).
1675
+ * Children outside {@link DELEGATION_CHILDREN_RECENCY_MS} are dropped before
1676
+ * they can enter the list (the per-session child list grows for the whole
1677
+ * session lifetime); the survivors are sorted running-first, most recent
1678
+ * first (compareDelegationEntries), so a ceiling hides the OLDEST rows.
1636
1679
  * Pure — no I/O. */
1637
1680
  /** Temporal guard for the children channel (same recycled-alias class as the
1638
1681
  * md merge): an md terminal report matched by taskID may still belong to
@@ -1657,7 +1700,9 @@ function childrenToDelegationEntries(children, md, now = Date.now(), parentSessi
1657
1700
  if (child.id === "" || seen.has(child.id)) continue;
1658
1701
  seen.add(child.id);
1659
1702
  const mdEntry = byTaskID.get(child.id);
1660
- const state = mdEntry !== void 0 && mdEntry.state !== "running" && mdTerminalCoversChild(mdEntry, child) ? mdEntry.state : childStatusToState(child.status);
1703
+ const lastActivity = Math.max(child.time?.updated ?? 0, child.time?.created ?? 0, mdEntry?.updatedAt ?? 0);
1704
+ if (lastActivity > 0 && now - lastActivity > 864e5) continue;
1705
+ const state = mdEntry !== void 0 && mdEntry.state !== "running" && mdTerminalCoversChild(mdEntry, child) ? mdEntry.state : childStatusToState(child.status, child.time, now);
1661
1706
  out.push({
1662
1707
  alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
1663
1708
  sessionID: mdEntry?.sessionID ?? parentSessionID,
@@ -1766,7 +1811,7 @@ function DelegationRow(props) {
1766
1811
  default: return t.textMuted;
1767
1812
  }
1768
1813
  });
1769
- const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow));
1814
+ const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame));
1770
1815
  const lead = createMemo(() => formatDelegationRowLead(props.job, marker()));
1771
1816
  const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now));
1772
1817
  const description = createMemo(() => truncateDelegationDescription(props.job.description));
@@ -1873,7 +1918,7 @@ function View(props) {
1873
1918
  const panelLog = createTuiLogger(projectRoot());
1874
1919
  const [childDelegations, setChildDelegations] = createSignal([]);
1875
1920
  const [now, setNow] = createSignal(Date.now());
1876
- const [animationNow, setAnimationNow] = createSignal(Date.now());
1921
+ const [frame, setFrame] = createSignal(0);
1877
1922
  let eventRefreshCount = 0;
1878
1923
  let delegationsInflight = null;
1879
1924
  const refreshDelegations = () => {
@@ -1898,7 +1943,8 @@ function View(props) {
1898
1943
  }
1899
1944
  let children = [];
1900
1945
  try {
1901
- const result = await props.api.client?.session?.children?.(buildChildrenPath(sessionID));
1946
+ const childrenPath = buildChildrenPath(sessionID);
1947
+ const result = childrenPath ? await props.api.client.session.children(childrenPath) : void 0;
1902
1948
  const data = result?.data ?? result;
1903
1949
  children = Array.isArray(data) ? data : [];
1904
1950
  } catch (err) {
@@ -1908,7 +1954,7 @@ function View(props) {
1908
1954
  const resolveStatus = (childID) => {
1909
1955
  if (!isValidSessionId(childID)) return void 0;
1910
1956
  try {
1911
- return state?.session?.status?.(childID)?.type;
1957
+ return state.session.status(childID)?.type;
1912
1958
  } catch {
1913
1959
  return;
1914
1960
  }
@@ -1958,7 +2004,7 @@ function View(props) {
1958
2004
  refreshDelegations();
1959
2005
  }, 1e3);
1960
2006
  cleanup.push(() => clearInterval(poll));
1961
- const animation = setInterval(() => setAnimationNow(Date.now()), 1e3);
2007
+ const animation = setInterval(() => setFrame((f) => f + 1), 80);
1962
2008
  cleanup.push(() => clearInterval(animation));
1963
2009
  try {
1964
2010
  const sdk = props.api.state?.session;
@@ -2132,8 +2178,8 @@ function View(props) {
2132
2178
  get now() {
2133
2179
  return now();
2134
2180
  },
2135
- get animationNow() {
2136
- return animationNow();
2181
+ get frame() {
2182
+ return frame();
2137
2183
  }
2138
2184
  })
2139
2185
  }), null);
@@ -2231,6 +2277,6 @@ const plugin = {
2231
2277
  setup: async () => {}
2232
2278
  };
2233
2279
  //#endregion
2234
- export { DELEGATION_ALIAS_WIDTH, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
2280
+ export { DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
2235
2281
 
2236
2282
  //# sourceMappingURL=tui.js.map
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
- "version": "1.5.0-beta.17",
3
+ "version": "1.5.0-beta.19",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pantheon-tui",
9
- "version": "1.5.0-beta.17",
9
+ "version": "1.5.0-beta.19",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@opencode-ai/plugin": ">=1.14.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
3
  "type": "module",
4
- "version": "1.5.0-beta.17",
4
+ "version": "1.5.0-beta.19",
5
5
  "description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -1293,6 +1293,26 @@ export const IDLE_SILENCE_MS = 60 * 1000
1293
1293
  export const DELEGATION_DONE_RETENTION_MS = 2 * 60 * 1000
1294
1294
  export const DELEGATION_FAILED_RETENTION_MS = 10 * 60 * 1000
1295
1295
 
1296
+ /** Grace window for an ABSENT child status. `api.state.session.status()` only
1297
+ * carries the sessions that are currently ALIVE; every finished child of a
1298
+ * long-lived session is missing from that map. A child with no status is
1299
+ * therefore treated as terminal (done) — treating it as running made every
1300
+ * historical child of the session show up as "active" (the "197 children,
1301
+ * all active" regression). The one exception is a freshly-spawned child that
1302
+ * has not been registered by the status API yet: within this window of its
1303
+ * last activity (`time.updated`, falling back to `time.created`) it still
1304
+ * reads as running. */
1305
+ export const DELEGATION_CHILD_STATUS_GRACE_MS = 60 * 1000
1306
+
1307
+ /** Recency window for the children channel. `session.children` returns EVERY
1308
+ * child the focused session ever spawned, so a long session accumulates
1309
+ * hundreds of historical rows that would inflate the panel. A child whose
1310
+ * last activity (its own time, or its matched report's Finalized timestamp)
1311
+ * is older than this window is dropped BEFORE it can enter the list. 24h
1312
+ * keeps the sidebar scoped to current work while still covering long
1313
+ * delegations. A child with no timestamp at all is kept (fail-open). */
1314
+ export const DELEGATION_CHILDREN_RECENCY_MS = 24 * 60 * 60 * 1000
1315
+
1296
1316
  /** Alias-less NATIVE task() live entries never receive a report alias (the
1297
1317
  * task tool output carries none), so the 30s alias-less prune in
1298
1318
  * mergeChildDelegationSources must not apply to them — 5 minutes covers a
@@ -1387,10 +1407,11 @@ export function delegationActivityLabel(entry: DelegationEntry): string {
1387
1407
 
1388
1408
  const DELEGATION_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
1389
1409
 
1390
- /** Return a deterministic spinner frame. The View ticks this every 1000ms
1391
- * (not 140ms — the fast tick flickered without adding information). */
1392
- export function delegationSpinnerFrame(now: number): string {
1393
- const index = Math.floor(Math.max(0, now) / 1_000) % DELEGATION_SPINNER_FRAMES.length
1410
+ /** Return the spinner glyph for the given frame counter. The frame counter
1411
+ * is incremented every ~80ms by a dedicated animation timer, decoupled from
1412
+ * the data poll cycle. Pure — no side effects. */
1413
+ export function delegationSpinnerFrame(frame: number): string {
1414
+ const index = Math.abs(Math.floor(frame)) % DELEGATION_SPINNER_FRAMES.length
1394
1415
  return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0]
1395
1416
  }
1396
1417
 
@@ -2034,7 +2055,7 @@ export function filterDelegationsToSession(
2034
2055
 
2035
2056
  /* ─── Current-session resolution + path guard (placeholder regression) ──
2036
2057
  * The sidebar forwarded `props.session_id` verbatim into
2037
- * `session.children({ path: { id } })`. When the TUI runtime renders the
2058
+ * `session.children({ sessionID })`. When the TUI runtime renders the
2038
2059
  * sidebar without a focused session it leaves the route template
2039
2060
  * UNSUBSTITUTED — the literal "{sessionID}" string — which opencode's server
2040
2061
  * rejected every poll (SchemaError: Expected a string starting with "ses",
@@ -2098,22 +2119,27 @@ export function resolveCurrentSessionID(sources: TuiSessionSources): string | nu
2098
2119
  }
2099
2120
 
2100
2121
  /** THE single choke point for every `session.children` / session-API path.
2101
- * Returns `{ path: { id } }` ONLY for a server-valid session id; returns
2102
- * null for anything else (placeholder, empty, foreign id) so the caller
2103
- * skips the call entirely instead of sending an unsubstituted placeholder
2104
- * (the "%7BsessionID%7D" regression). Every session-API call site MUST go
2105
- * through this function (enforced by the source-scan test in
2106
- * tests/pantheon/tui-delegations.test.ts). */
2107
- export function safeSessionPath(id: unknown): { path: { id: string } } | null {
2122
+ * Returns the v2 SDK parameter shape `{ sessionID }` ONLY for a
2123
+ * server-valid session id; returns null for anything else (placeholder,
2124
+ * empty, foreign id) so the caller skips the call entirely instead of
2125
+ * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression).
2126
+ * The TUI client is `@opencode-ai/sdk/v2`, whose session methods take a
2127
+ * FLAT parameter object (`{ sessionID }`), NOT the v1 `{ path: { id } }`
2128
+ * envelope — passing the v1 shape left the v2 `{sessionID}` URL template
2129
+ * unsubstituted (`/session/%7BsessionID%7D/children`). Every session-API
2130
+ * call site MUST go through this function (enforced by the source-scan
2131
+ * test in tests/pantheon/tui-delegations.test.ts). */
2132
+ export function safeSessionPath(id: unknown): { sessionID: string } | null {
2108
2133
  if (!isValidSessionId(id)) return null
2109
- return { path: { id } }
2134
+ return { sessionID: id }
2110
2135
  }
2111
2136
 
2112
- /** Build the `session.children` path ONLY from a validated session id.
2137
+ /** Build the `session.children` parameters ONLY from a validated session id.
2113
2138
  * Delegates to {@link safeSessionPath} — the single choke point. Returns
2114
- * null for null/invalid ids so the caller skips the fetch instead of
2115
- * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression). */
2116
- export function buildChildrenPath(id: string | null | undefined): { path: { id: string } } | null {
2139
+ * the v2 SDK shape `{ sessionID }`; null for null/invalid ids so the caller
2140
+ * skips the fetch instead of sending an unsubstituted placeholder (the
2141
+ * "%7BsessionID%7D" regression). */
2142
+ export function buildChildrenPath(id: string | null | undefined): { sessionID: string } | null {
2117
2143
  return safeSessionPath(id)
2118
2144
  }
2119
2145
 
@@ -2135,14 +2161,36 @@ export type ChildDelegationLike = {
2135
2161
  }
2136
2162
  }
2137
2163
 
2138
- /** Map a child status type to a display state. busy/retry → running
2139
- * (the child is actively working), idle → completed, unknown → running
2140
- * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
2141
- * correct it as soon as terminal data exists). */
2142
- export function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry' {
2143
- if (status === 'idle') return 'completed'
2164
+ /** Map a child status type to a display state.
2165
+ *
2166
+ * Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is
2167
+ * any other/unknown status — a status the panel does not recognise is not
2168
+ * evidence of activity. An ABSENT status is the historical case: the status
2169
+ * map (`api.state.session.status`) contains only currently-alive sessions, so
2170
+ * every finished child of a long session is missing from it. Defaulting that
2171
+ * to running made the whole session history render as "active" (the
2172
+ * "197 children, all active" regression), so an absent status is terminal
2173
+ * (done) UNLESS the child was active within `graceMs` — a just-spawned child
2174
+ * that the status API has not registered yet must still render as running.
2175
+ *
2176
+ * @param status the `api.state.session.status(id)?.type`, or undefined
2177
+ * @param time the child session time fields (`created`/`updated`)
2178
+ * @param now current wall-clock ms (injectable for tests)
2179
+ * @param graceMs recency window for the absent-status running assumption
2180
+ * Pure — no I/O. */
2181
+ export function childStatusToState(
2182
+ status: string | undefined,
2183
+ time?: { created?: number; updated?: number },
2184
+ now = Date.now(),
2185
+ graceMs = DELEGATION_CHILD_STATUS_GRACE_MS,
2186
+ ): 'running' | 'completed' | 'retry' {
2144
2187
  if (status === 'retry') return 'retry'
2145
- return 'running' // busy or unknown
2188
+ if (status === 'busy') return 'running'
2189
+ if (status !== undefined) return 'completed' // idle + any other explicit terminal
2190
+ // Absent status: running only while the child's last activity is fresh.
2191
+ const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0)
2192
+ if (lastActivity > 0 && now - lastActivity < graceMs) return 'running'
2193
+ return 'completed'
2146
2194
  }
2147
2195
 
2148
2196
  /** Status-only row model: ONE glyph + ONE identity per row.
@@ -2188,11 +2236,11 @@ export function delegationRowGlyph(status: DelegationRowStatus): string {
2188
2236
  }
2189
2237
 
2190
2238
  /** Row marker (`<glyph> `) — the single state channel. `active` animates
2191
- * through the 1s spinner for the given tick; every other kind is static.
2192
- * Pure. */
2193
- export function delegationRowMarker(state: DelegationDisplayState, now = Date.now()): string {
2239
+ * through the spinner for the given frame counter (incremented every ~80ms);
2240
+ * every other kind is static. Pure. */
2241
+ export function delegationRowMarker(state: DelegationDisplayState, frame = 0): string {
2194
2242
  const status = delegationRowStatus(state)
2195
- const glyph = status === 'active' ? delegationSpinnerFrame(now) : delegationRowGlyph(status)
2243
+ const glyph = status === 'active' ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)
2196
2244
  return `${glyph} `
2197
2245
  }
2198
2246
 
@@ -2370,7 +2418,10 @@ export function formatPanelLogLine(
2370
2418
  * `parentSessionID` (the focused session id) is stamped on report-less
2371
2419
  * children so the row carries its origin session; omit it and they stay
2372
2420
  * unscoped ('').
2373
- * Sorted running-first (compareDelegationEntries).
2421
+ * Children outside {@link DELEGATION_CHILDREN_RECENCY_MS} are dropped before
2422
+ * they can enter the list (the per-session child list grows for the whole
2423
+ * session lifetime); the survivors are sorted running-first, most recent
2424
+ * first (compareDelegationEntries), so a ceiling hides the OLDEST rows.
2374
2425
  * Pure — no I/O. */
2375
2426
  /** Temporal guard for the children channel (same recycled-alias class as the
2376
2427
  * md merge): an md terminal report matched by taskID may still belong to
@@ -2404,10 +2455,22 @@ export function childrenToDelegationEntries(
2404
2455
  if (child.id === '' || seen.has(child.id)) continue
2405
2456
  seen.add(child.id)
2406
2457
  const mdEntry = byTaskID.get(child.id)
2458
+ // Recency window (source-level): the children API returns EVERY child the
2459
+ // session ever spawned, so without this the whole session history enters
2460
+ // the list (the "197 children" panel). Effective recency = the latest of
2461
+ // the child's own activity and its matched report's Finalized timestamp;
2462
+ // a child with NO timestamp at all is kept (fail-open — it cannot be
2463
+ // judged). Oldest rows are the ones dropped.
2464
+ const lastActivity = Math.max(
2465
+ child.time?.updated ?? 0,
2466
+ child.time?.created ?? 0,
2467
+ mdEntry?.updatedAt ?? 0,
2468
+ )
2469
+ if (lastActivity > 0 && now - lastActivity > DELEGATION_CHILDREN_RECENCY_MS) continue
2407
2470
  const state =
2408
2471
  mdEntry !== undefined && mdEntry.state !== 'running' && mdTerminalCoversChild(mdEntry, child)
2409
2472
  ? mdEntry.state
2410
- : childStatusToState(child.status)
2473
+ : childStatusToState(child.status, child.time, now)
2411
2474
  out.push({
2412
2475
  alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
2413
2476
  // A report-less child IS a child of the focused session
@@ -2588,7 +2651,7 @@ function DelegationRow(props: {
2588
2651
  api: TuiPluginApi
2589
2652
  job: DelegationEntry
2590
2653
  now: number
2591
- animationNow: number
2654
+ frame: number
2592
2655
  }) {
2593
2656
  const theme = () => props.api.theme.current
2594
2657
 
@@ -2610,7 +2673,7 @@ function DelegationRow(props: {
2610
2673
  }
2611
2674
  })
2612
2675
 
2613
- const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow))
2676
+ const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame))
2614
2677
  const lead = createMemo(() => formatDelegationRowLead(props.job, marker()))
2615
2678
  const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now))
2616
2679
  const description = createMemo(() => truncateDelegationDescription(props.job.description))
@@ -2770,9 +2833,9 @@ function View(props: {
2770
2833
  // (oh-my-opencode-slim pattern): re-fetch children + re-read the md every
2771
2834
  // second so the panel updates even when NO event ever fires.
2772
2835
  const [now, setNow] = createSignal(Date.now())
2773
- // Separate fast ticker for the spinner. The elapsed label intentionally
2774
- // remains on the 1s ticker to avoid unnecessary work.
2775
- const [animationNow, setAnimationNow] = createSignal(Date.now())
2836
+ // Frame counter for the spinner animation — incremented every ~80ms by a
2837
+ // dedicated fast timer, completely decoupled from the data poll cycle.
2838
+ const [frame, setFrame] = createSignal(0)
2776
2839
  // Cumulative count of event-triggered refreshes — logged with every fetch
2777
2840
  // ("panel: children=N(pantheon=M native=K) md=N events=N") so the (0)
2778
2841
  // symptom is diagnosable: children>0 means the source works; events=0
@@ -2786,7 +2849,9 @@ function View(props: {
2786
2849
  if (delegationsInflight !== null) return delegationsInflight
2787
2850
  delegationsInflight = (async () => {
2788
2851
  try {
2789
- const state = props.api.state as any
2852
+ // Typed (no `as any`): TuiState.session.status is a v2 sync accessor
2853
+ // taking the session id directly — same shape as the client call.
2854
+ const state = props.api.state
2790
2855
  // 0. md reports from EVERY session under .pantheon/delegations — the
2791
2856
  // ENRICHMENT channel. Read unfiltered (taskID match supplies alias/
2792
2857
  // agent/description/terminal state); the active-session filter after
@@ -2817,13 +2882,18 @@ function View(props: {
2817
2882
  return
2818
2883
  }
2819
2884
  // 2. children — PRIMARY source (SDK: api.client.session.children).
2885
+ // v2 SDK: `children` takes a FLAT `{ sessionID }` parameter object.
2886
+ // The v1 `{ path: { id } }` envelope is NOT understood by the v2
2887
+ // client — it left the URL template unsubstituted and the server
2888
+ // rejected every poll ("%7BsessionID%7D" regression).
2820
2889
  let children: ChildDelegationLike[] = []
2821
2890
  try {
2822
2891
  // buildChildrenPath re-validates: with sessionID !== null it is
2823
2892
  // never null, so no placeholder can reach the wire.
2824
- const result = await (props.api.client as any)?.session?.children?.(
2825
- buildChildrenPath(sessionID),
2826
- )
2893
+ const childrenPath = buildChildrenPath(sessionID)
2894
+ const result = childrenPath
2895
+ ? await props.api.client.session.children(childrenPath)
2896
+ : undefined
2827
2897
  const data = (result?.data ?? result) as unknown
2828
2898
  children = Array.isArray(data) ? (data as ChildDelegationLike[]) : []
2829
2899
  } catch (err) {
@@ -2837,7 +2907,7 @@ function View(props: {
2837
2907
  // path — "%7BsessionID%7D").
2838
2908
  if (!isValidSessionId(childID)) return undefined
2839
2909
  try {
2840
- return state?.session?.status?.(childID)?.type as string | undefined
2910
+ return state.session.status(childID)?.type
2841
2911
  } catch {
2842
2912
  return undefined // status API unavailable — child state fallback
2843
2913
  }
@@ -2940,7 +3010,9 @@ function View(props: {
2940
3010
  void refreshDelegations()
2941
3011
  }, 1_000)
2942
3012
  cleanup.push(() => clearInterval(poll))
2943
- const animation = setInterval(() => setAnimationNow(Date.now()), 1_000)
3013
+ // Fast spinner animation: 80ms frame counter, decoupled from data poll.
3014
+ // Only the spinner glyph depends on this; elapsed + data use the 1s timer.
3015
+ const animation = setInterval(() => setFrame((f) => f + 1), 80)
2944
3016
  cleanup.push(() => clearInterval(animation))
2945
3017
 
2946
3018
  // Compaction recovery: `message.part.removed` clears live entries while
@@ -3052,14 +3124,7 @@ function View(props: {
3052
3124
  >
3053
3125
  <box marginLeft={1} flexDirection="column">
3054
3126
  <For each={delegationCeiling().visible}>
3055
- {(job) => (
3056
- <DelegationRow
3057
- api={props.api}
3058
- job={job}
3059
- now={now()}
3060
- animationNow={animationNow()}
3061
- />
3062
- )}
3127
+ {(job) => <DelegationRow api={props.api} job={job} now={now()} frame={frame()} />}
3063
3128
  </For>
3064
3129
  <Show when={delegationCeiling().hidden > 0}>
3065
3130
  <text fg={theme().textMuted}>