pantheon-opencode 1.5.0-beta.18 → 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,20 @@ 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".
21
35
  ## [v1.5.0-beta.18] - 2026-09-16
22
36
 
23
37
  <!-- 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.18",
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.18",
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.18"
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
@@ -364,11 +383,27 @@ type ChildDelegationLike = {
364
383
  updated?: number;
365
384
  };
366
385
  };
367
- /** Map a child status type to a display state. busy/retry → running
368
- * (the child is actively working), idle → completed, unknown → running
369
- * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
370
- * correct it as soon as terminal data exists). */
371
- 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';
372
407
  /** Status-only row model: ONE glyph + ONE identity per row.
373
408
  *
374
409
  * A row is
@@ -386,9 +421,9 @@ declare function delegationRowStatus(state: DelegationDisplayState): DelegationR
386
421
  declare const DELEGATION_ROW_GLYPHS: Readonly<Record<DelegationRowStatus, string>>;
387
422
  declare function delegationRowGlyph(status: DelegationRowStatus): string;
388
423
  /** Row marker (`<glyph> `) — the single state channel. `active` animates
389
- * through the 1s spinner for the given tick; every other kind is static.
390
- * Pure. */
391
- 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;
392
427
  /** ONE identity per row: the short report alias (`apo-1`); a row without a
393
428
  * report alias (native task() child `native-task` / `native-<id>`, or an
394
429
  * alias-less native live row `live-<callID>` kept as children-only) shows
@@ -473,5 +508,5 @@ declare const plugin: TuiPluginModule & {
473
508
  setup: () => Promise<void>;
474
509
  };
475
510
  //#endregion
476
- 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 };
477
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.18";
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
@@ -1451,14 +1470,30 @@ function buildChildrenPath(id) {
1451
1470
  return safeSessionPath(id);
1452
1471
  }
1453
1472
  /** Duck-typed subset of a child Session (+ its live status type). */
1454
- /** Map a child status type to a display state. busy/retry → running
1455
- * (the child is actively working), idle → completed, unknown → running
1456
- * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
1457
- * correct it as soon as terminal data exists). */
1458
- function childStatusToState(status) {
1459
- 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) {
1460
1491
  if (status === "retry") return "retry";
1461
- 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";
1462
1497
  }
1463
1498
  /** Status-only row model: ONE glyph + ONE identity per row.
1464
1499
  *
@@ -1494,11 +1529,11 @@ function delegationRowGlyph(status) {
1494
1529
  return DELEGATION_ROW_GLYPHS[status];
1495
1530
  }
1496
1531
  /** Row marker (`<glyph> `) — the single state channel. `active` animates
1497
- * through the 1s spinner for the given tick; every other kind is static.
1498
- * Pure. */
1499
- 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) {
1500
1535
  const status = delegationRowStatus(state);
1501
- return `${status === "active" ? delegationSpinnerFrame(now) : delegationRowGlyph(status)} `;
1536
+ return `${status === "active" ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)} `;
1502
1537
  }
1503
1538
  /** ONE identity per row: the short report alias (`apo-1`); a row without a
1504
1539
  * report alias (native task() child `native-task` / `native-<id>`, or an
@@ -1637,7 +1672,10 @@ function formatPanelLogLine(children, pantheon, native, md, events) {
1637
1672
  * `parentSessionID` (the focused session id) is stamped on report-less
1638
1673
  * children so the row carries its origin session; omit it and they stay
1639
1674
  * unscoped ('').
1640
- * 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.
1641
1679
  * Pure — no I/O. */
1642
1680
  /** Temporal guard for the children channel (same recycled-alias class as the
1643
1681
  * md merge): an md terminal report matched by taskID may still belong to
@@ -1662,7 +1700,9 @@ function childrenToDelegationEntries(children, md, now = Date.now(), parentSessi
1662
1700
  if (child.id === "" || seen.has(child.id)) continue;
1663
1701
  seen.add(child.id);
1664
1702
  const mdEntry = byTaskID.get(child.id);
1665
- 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);
1666
1706
  out.push({
1667
1707
  alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
1668
1708
  sessionID: mdEntry?.sessionID ?? parentSessionID,
@@ -1771,7 +1811,7 @@ function DelegationRow(props) {
1771
1811
  default: return t.textMuted;
1772
1812
  }
1773
1813
  });
1774
- const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow));
1814
+ const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame));
1775
1815
  const lead = createMemo(() => formatDelegationRowLead(props.job, marker()));
1776
1816
  const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now));
1777
1817
  const description = createMemo(() => truncateDelegationDescription(props.job.description));
@@ -1878,7 +1918,7 @@ function View(props) {
1878
1918
  const panelLog = createTuiLogger(projectRoot());
1879
1919
  const [childDelegations, setChildDelegations] = createSignal([]);
1880
1920
  const [now, setNow] = createSignal(Date.now());
1881
- const [animationNow, setAnimationNow] = createSignal(Date.now());
1921
+ const [frame, setFrame] = createSignal(0);
1882
1922
  let eventRefreshCount = 0;
1883
1923
  let delegationsInflight = null;
1884
1924
  const refreshDelegations = () => {
@@ -1964,7 +2004,7 @@ function View(props) {
1964
2004
  refreshDelegations();
1965
2005
  }, 1e3);
1966
2006
  cleanup.push(() => clearInterval(poll));
1967
- const animation = setInterval(() => setAnimationNow(Date.now()), 1e3);
2007
+ const animation = setInterval(() => setFrame((f) => f + 1), 80);
1968
2008
  cleanup.push(() => clearInterval(animation));
1969
2009
  try {
1970
2010
  const sdk = props.api.state?.session;
@@ -2138,8 +2178,8 @@ function View(props) {
2138
2178
  get now() {
2139
2179
  return now();
2140
2180
  },
2141
- get animationNow() {
2142
- return animationNow();
2181
+ get frame() {
2182
+ return frame();
2143
2183
  }
2144
2184
  })
2145
2185
  }), null);
@@ -2237,6 +2277,6 @@ const plugin = {
2237
2277
  setup: async () => {}
2238
2278
  };
2239
2279
  //#endregion
2240
- 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 };
2241
2281
 
2242
2282
  //# sourceMappingURL=tui.js.map
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
- "version": "1.5.0-beta.18",
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.18",
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.18",
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
 
@@ -2140,14 +2161,36 @@ export type ChildDelegationLike = {
2140
2161
  }
2141
2162
  }
2142
2163
 
2143
- /** Map a child status type to a display state. busy/retry → running
2144
- * (the child is actively working), idle → completed, unknown → running
2145
- * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
2146
- * correct it as soon as terminal data exists). */
2147
- export function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry' {
2148
- 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' {
2149
2187
  if (status === 'retry') return 'retry'
2150
- 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'
2151
2194
  }
2152
2195
 
2153
2196
  /** Status-only row model: ONE glyph + ONE identity per row.
@@ -2193,11 +2236,11 @@ export function delegationRowGlyph(status: DelegationRowStatus): string {
2193
2236
  }
2194
2237
 
2195
2238
  /** Row marker (`<glyph> `) — the single state channel. `active` animates
2196
- * through the 1s spinner for the given tick; every other kind is static.
2197
- * Pure. */
2198
- 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 {
2199
2242
  const status = delegationRowStatus(state)
2200
- const glyph = status === 'active' ? delegationSpinnerFrame(now) : delegationRowGlyph(status)
2243
+ const glyph = status === 'active' ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)
2201
2244
  return `${glyph} `
2202
2245
  }
2203
2246
 
@@ -2375,7 +2418,10 @@ export function formatPanelLogLine(
2375
2418
  * `parentSessionID` (the focused session id) is stamped on report-less
2376
2419
  * children so the row carries its origin session; omit it and they stay
2377
2420
  * unscoped ('').
2378
- * 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.
2379
2425
  * Pure — no I/O. */
2380
2426
  /** Temporal guard for the children channel (same recycled-alias class as the
2381
2427
  * md merge): an md terminal report matched by taskID may still belong to
@@ -2409,10 +2455,22 @@ export function childrenToDelegationEntries(
2409
2455
  if (child.id === '' || seen.has(child.id)) continue
2410
2456
  seen.add(child.id)
2411
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
2412
2470
  const state =
2413
2471
  mdEntry !== undefined && mdEntry.state !== 'running' && mdTerminalCoversChild(mdEntry, child)
2414
2472
  ? mdEntry.state
2415
- : childStatusToState(child.status)
2473
+ : childStatusToState(child.status, child.time, now)
2416
2474
  out.push({
2417
2475
  alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
2418
2476
  // A report-less child IS a child of the focused session
@@ -2593,7 +2651,7 @@ function DelegationRow(props: {
2593
2651
  api: TuiPluginApi
2594
2652
  job: DelegationEntry
2595
2653
  now: number
2596
- animationNow: number
2654
+ frame: number
2597
2655
  }) {
2598
2656
  const theme = () => props.api.theme.current
2599
2657
 
@@ -2615,7 +2673,7 @@ function DelegationRow(props: {
2615
2673
  }
2616
2674
  })
2617
2675
 
2618
- const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow))
2676
+ const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame))
2619
2677
  const lead = createMemo(() => formatDelegationRowLead(props.job, marker()))
2620
2678
  const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now))
2621
2679
  const description = createMemo(() => truncateDelegationDescription(props.job.description))
@@ -2775,9 +2833,9 @@ function View(props: {
2775
2833
  // (oh-my-opencode-slim pattern): re-fetch children + re-read the md every
2776
2834
  // second so the panel updates even when NO event ever fires.
2777
2835
  const [now, setNow] = createSignal(Date.now())
2778
- // Separate fast ticker for the spinner. The elapsed label intentionally
2779
- // remains on the 1s ticker to avoid unnecessary work.
2780
- 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)
2781
2839
  // Cumulative count of event-triggered refreshes — logged with every fetch
2782
2840
  // ("panel: children=N(pantheon=M native=K) md=N events=N") so the (0)
2783
2841
  // symptom is diagnosable: children>0 means the source works; events=0
@@ -2952,7 +3010,9 @@ function View(props: {
2952
3010
  void refreshDelegations()
2953
3011
  }, 1_000)
2954
3012
  cleanup.push(() => clearInterval(poll))
2955
- 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)
2956
3016
  cleanup.push(() => clearInterval(animation))
2957
3017
 
2958
3018
  // Compaction recovery: `message.part.removed` clears live entries while
@@ -3064,14 +3124,7 @@ function View(props: {
3064
3124
  >
3065
3125
  <box marginLeft={1} flexDirection="column">
3066
3126
  <For each={delegationCeiling().visible}>
3067
- {(job) => (
3068
- <DelegationRow
3069
- api={props.api}
3070
- job={job}
3071
- now={now()}
3072
- animationNow={animationNow()}
3073
- />
3074
- )}
3127
+ {(job) => <DelegationRow api={props.api} job={job} now={now()} frame={frame()} />}
3075
3128
  </For>
3076
3129
  <Show when={delegationCeiling().hidden > 0}>
3077
3130
  <text fg={theme().textMuted}>