pantheon-opencode 1.5.0-beta.9 → 1.5.1

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.
Files changed (84) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +326 -0
  3. package/README.md +77 -29
  4. package/README.pt-BR.md +5 -6
  5. package/commands/pantheon.md +1 -35
  6. package/package.json +2 -3
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +3 -5
  9. package/scripts/doctor.mjs +260 -5
  10. package/scripts/install/plugin.mjs +9 -12
  11. package/scripts/mcp_persistence_server.py +1 -1
  12. package/scripts/sync-tui.mjs +8 -11
  13. package/src/agents/apollo.md +1 -0
  14. package/src/agents/gaia.md +1 -0
  15. package/src/agents/mnemosyne.md +11 -1
  16. package/src/agents/zeus.md +10 -52
  17. package/src/instructions/memory-protocol.instructions.md +6 -3
  18. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  19. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  20. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  21. package/src/mcp/mcp_persistence_server.py +1 -1
  22. package/src/mcp/memory_mcp_server.py +81 -27
  23. package/src/pantheon/background-job-board.ts +4 -4
  24. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  25. package/src/pantheon/cost-command.ts +1 -2
  26. package/src/pantheon/delegation-enforce.ts +59 -14
  27. package/src/pantheon/goal-loop.ts +1 -1
  28. package/src/pantheon/goal-store.ts +1 -1
  29. package/src/pantheon/idle-continuation.ts +4 -6
  30. package/src/pantheon/model-command.ts +1 -2
  31. package/src/pantheon/native-task-status.ts +2 -2
  32. package/src/pantheon/presets.d.mts +0 -20
  33. package/src/pantheon/presets.mjs +0 -64
  34. package/src/pantheon/session-guard.ts +2 -2
  35. package/src/pantheon/session-id.ts +30 -0
  36. package/src/pantheon/task-result-guard.ts +2 -3
  37. package/src/pantheon/todo-enforcer.ts +3 -3
  38. package/src/pantheon/tool-context.ts +23 -0
  39. package/src/pantheon/v2-bridge.ts +1 -4
  40. package/src/plugin-v2.ts +8 -21
  41. package/src/plugin.ts +61 -245
  42. package/src/plugins/pantheon-hooks.ts +2 -2
  43. package/src/plugins/tui/dist/tui.d.ts +229 -103
  44. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  45. package/src/plugins/tui/dist/tui.js +495 -333
  46. package/src/plugins/tui/package-lock.json +5 -2
  47. package/src/plugins/tui/package.json +5 -2
  48. package/src/plugins/tui/src/index.tsx +710 -409
  49. package/src/routing.yml +0 -16
  50. package/src/skills/context-compression/SKILL.md +1 -1
  51. package/scripts/generate-preset-docs.mjs +0 -300
  52. package/scripts/generate-prompts.sh +0 -222
  53. package/scripts/hash_verify.py +0 -192
  54. package/scripts/install/component.mjs +0 -24
  55. package/scripts/install/migrate-config.mjs +0 -302
  56. package/scripts/test-context-compression.sh +0 -169
  57. package/src/mcp/toon_codec.py +0 -491
  58. package/src/pantheon/auto-wake.ts +0 -85
  59. package/src/pantheon/chat-reminders.ts +0 -9
  60. package/src/pantheon/comment-checker.ts +0 -48
  61. package/src/pantheon/compaction-assert.ts +0 -160
  62. package/src/pantheon/compaction-handoff.ts +0 -304
  63. package/src/pantheon/cost-tracker.ts +0 -108
  64. package/src/pantheon/delegate-manager.ts +0 -766
  65. package/src/pantheon/delegation-classifier.ts +0 -307
  66. package/src/pantheon/delegation-finalize.ts +0 -406
  67. package/src/pantheon/delegation-notify.ts +0 -212
  68. package/src/pantheon/delegation-toast.ts +0 -73
  69. package/src/pantheon/delegation.ts +0 -1337
  70. package/src/pantheon/dispatch-guard.ts +0 -109
  71. package/src/pantheon/native-probe.ts +0 -234
  72. package/src/pantheon/permission-eval.ts +0 -284
  73. package/src/pantheon/preemptive-compact.ts +0 -76
  74. package/src/pantheon/provider-capabilities.ts +0 -221
  75. package/src/pantheon/provider-stream-tracker.ts +0 -182
  76. package/src/pantheon/retry-policy.ts +0 -212
  77. package/src/pantheon/step-cap.ts +0 -110
  78. package/src/pantheon/token-opt.ts +0 -689
  79. package/src/pantheon/toon-codec.ts +0 -481
  80. package/src/pantheon/v2-events.ts +0 -148
  81. package/src/pantheon/v2-hooks.ts +0 -287
  82. package/src/pantheon/v2-tool-definitions.ts +0 -331
  83. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
  84. package/src/plugins/tui/dist/tui.tsx +0 -2853
@@ -1,6 +1,6 @@
1
1
  import { createComponent, createElement, createTextNode, effect, insert, insertNode, memo, setProp } from "@opentui/solid";
2
2
  import { Buffer } from "node:buffer";
3
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -10,9 +10,10 @@ import { For, Show, createEffect, createMemo, createResource, createSignal, onCl
10
10
  /**
11
11
  * pantheon-tui — Pantheon TUI plugin for opencode.
12
12
  *
13
- * Single self-contained entry file: `tsdown && cp src/index.tsx dist/tui.tsx`
14
- * keeps the no-build load path working (dist/tui.tsx is raw source, transpiled
15
- * on the fly by opencode). All feature code lives in this one file.
13
+ * Bundled entry: `tsdown` emits `dist/tui.js`, and the package `exports`
14
+ * (`./tui` → ./dist/tui.js, `./server` → ./dist/server.js) is the ONLY load
15
+ * path — no raw TSX copy is shipped. Because the bundle inlines relative
16
+ * imports, this entry may be split into modules without breaking the loader.
16
17
  *
17
18
  * Slots:
18
19
  * - sidebar_content (order 900) — Pantheon sidebar (header/version/
@@ -164,7 +165,7 @@ async function detectVersion(api) {
164
165
  if (ver) return ver;
165
166
  }
166
167
  } catch {}
167
- return "1.5.0-beta.9";
168
+ return "1.5.1";
168
169
  }
169
170
  /**
170
171
  * usage-bar — AI subscription usage gauge for the opencode TUI.
@@ -831,25 +832,31 @@ async function readDelegationEntries(dir) {
831
832
  }
832
833
  /** Read delegation reports from EVERY session under
833
834
  * `<root>/.pantheon/delegations/<sessionID>/<alias>.md` — the panel's
834
- * HISTORY channel. Unlike the children/live channels it does NOT depend on a
835
- * resolved sessionID: with no focused session (null/placeholder), the panel
836
- * still shows the reports from all past sessions (running first, Finalized
837
- * desc — the sort applied by readDelegationEntries). Fail-open: a
838
- * missing/unreadable directory yields []. */
835
+ * ENRICHMENT source. The read is deliberately unfiltered (each entry carries
836
+ * its directory's sessionID, so a taskID match can enrich any channel);
837
+ * callers MUST scope the merged result with {@link filterDelegationsToSession}
838
+ * before rendering — only the active session's reports are shown. Entries are
839
+ * running first, Finalized desc (the sort applied by readDelegationEntries).
840
+ * Fail-open: a missing/unreadable directory yields []. */
839
841
  async function readAllDelegationEntries(root) {
840
842
  return readDelegationEntries(join(root, ".pantheon", "delegations"));
841
843
  }
842
- /** Resolve the directory where the job board writes delegation md reports.
843
- * The board writes `.pantheon/delegations` RELATIVE to the server cwd,
844
- * which the TUI exposes as `TuiState.path.directory`. `project` does NOT
845
- * exist on `TuiState.path` (the old `state?.project ?? state?.worktree`
846
- * resolution was always undefined for the first term) and `worktree` is
847
- * `/` when there is no git (e.g. the sandbox test project) — a root of
848
- * `''` or `'/'` must fall back to `process.cwd()`. */
849
- function resolveDelegationsDir(state, cwd = process.cwd()) {
844
+ /** Resolve the PROJECT ROOT used by every pantheon file channel. `directory`
845
+ * wins over `worktree` (the old `resolveDelegationsDir` already did this);
846
+ * an absent/empty root or `/` (no git — e.g. the sandbox test project) falls
847
+ * back to cwd. Standardised here so the delegations md and the panel logger
848
+ * all read the SAME root (audit finding: the channels
849
+ * resolved the root independently). Pure — no I/O. */
850
+ function resolvePantheonRoot(state, cwd = process.cwd()) {
850
851
  const root = state?.directory ?? state?.worktree ?? "";
851
- if (root === "" || root === "/") return join(cwd, ".pantheon", "delegations");
852
- return join(root, ".pantheon", "delegations");
852
+ if (root === "" || root === "/") return cwd;
853
+ return root;
854
+ }
855
+ /** Resolve the directory where delegation md reports are written.
856
+ * The finalizer writes `.pantheon/delegations` RELATIVE to the server cwd,
857
+ * which the TUI exposes as `TuiState.path.directory`. */
858
+ function resolveDelegationsDir(state, cwd = process.cwd()) {
859
+ return join(resolvePantheonRoot(state, cwd), ".pantheon", "delegations");
853
860
  }
854
861
  /** Project root derived from the delegations dir: `<root>/.pantheon/delegations`
855
862
  * → `<root>`. Used to point the panel logger at the REAL hooks.log — passing
@@ -875,21 +882,24 @@ function compareDelegationEntries(a, b) {
875
882
  return (b.updatedAt ?? b.startedAt) - (a.updatedAt ?? a.startedAt);
876
883
  }
877
884
  /** Split the panel list: active jobs (running/retry, stale-marked) first,
878
- * then the most recent terminal reports, then the archived tail (paginated
879
- * in the View). Pure — so the history-only panel (no sessionID) is testable
885
+ * then the most recent terminal reports; the remaining tail is collapsed
886
+ * by the View into a single "… +N more" line. Pure — so the history-only panel (no sessionID) is testable
880
887
  * without the TUI runtime. */
881
888
  function splitDelegationList(all, maxRecent = 8, now = Date.now(), staleThresholdMs = 1800 * 1e3) {
882
- const active = all.filter((d) => d.state === "running" || d.state === "retry").map((d) => markStaleIfRunning(d, now, staleThresholdMs));
883
- const terminal = all.filter((d) => d.state !== "running" && d.state !== "retry");
889
+ const isActiveState = (st) => st === "running" || st === "retry" || st === "stale-running";
884
890
  return {
885
- active,
886
- recent: terminal.slice(0, maxRecent),
887
- archived: terminal.slice(maxRecent)
891
+ active: all.filter((d) => isActiveState(d.state)).map((d) => markStaleIfRunning(d, now, staleThresholdMs)).sort(compareDelegationEntries),
892
+ recent: all.filter((d) => !isActiveState(d.state)).filter((d) => {
893
+ if (d.updatedAt === null || !Number.isFinite(d.updatedAt)) return true;
894
+ const age = now - d.updatedAt;
895
+ if (age < 0) return true;
896
+ return age < (delegationRowStatus(d.state) === "failed" ? DELEGATION_FAILED_RETENTION_MS : DELEGATION_DONE_RETENTION_MS);
897
+ }).sort(compareDelegationEntries).slice(0, maxRecent)
888
898
  };
889
899
  }
890
- /** The list the panel actually renders (kept for the header count and
891
- * existing tests): active jobs first, then the most recent terminal
892
- * reports (capped) — the archived tail is rendered separately. Pure. */
900
+ /** Live-first window used by {@link ceilingDelegationList} (kept for the
901
+ * ceiling helper and existing tests): active jobs first, then the most
902
+ * recent terminal reports (capped). Pure. */
893
903
  function visibleDelegationList(all, maxTerminal = 8, now = Date.now(), staleThresholdMs = 1800 * 1e3) {
894
904
  const { active, recent } = splitDelegationList(all, maxTerminal, now, staleThresholdMs);
895
905
  return [...active, ...recent];
@@ -900,10 +910,37 @@ const STALE_RUNNING_THRESHOLD_MS = 1800 * 1e3;
900
910
  * considered stale. Combined with the stale-running threshold to produce the
901
911
  * display-only `stale-running` state. */
902
912
  const IDLE_SILENCE_MS = 60 * 1e3;
913
+ /** Visual-only terminal retention windows. Reports remain on disk; these
914
+ * constants only control which rows enter the TUI window. */
915
+ const DELEGATION_DONE_RETENTION_MS = 120 * 1e3;
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;
935
+ /** Alias-less NATIVE task() live entries never receive a report alias (the
936
+ * task tool output carries none), so the 30s alias-less prune in
937
+ * mergeChildDelegationSources must not apply to them — 5 minutes covers a
938
+ * slow child listing while still bounding the live map. */
939
+ const NATIVE_LIVE_ALIASLESS_TTL_MS = 300 * 1e3;
903
940
  /**
904
941
  * Mark a running entry as `stale-running` if it has been running longer than
905
942
  * the threshold AND has no recent activity (no `updatedAt` change in the last
906
- * `IDLE_SILENCE_MS`). This is DISPLAY-ONLY — the board state is unchanged.
943
+ * `IDLE_SILENCE_MS`). This is DISPLAY-ONLY — the persisted state is unchanged.
907
944
  *
908
945
  * A `stale-running` entry renders with a warning indicator but the underlying
909
946
  * delegation is still treated as running by the backend.
@@ -917,7 +954,7 @@ function markStaleIfRunning(entry, now, thresholdMs = STALE_RUNNING_THRESHOLD_MS
917
954
  state: "stale-running"
918
955
  };
919
956
  }
920
- /** Compact elapsed-time label: "5m 12s", "1h 30m", "2d 4h" — ticks every
957
+ /** Compact elapsed-time label, single unit only: "12s"/"3m"/"1h"/"2d" — ticks every
921
958
  * second for running jobs. */
922
959
  function fmtElapsed(ms) {
923
960
  const total = Math.max(0, Math.floor(ms / 1e3));
@@ -925,15 +962,16 @@ function fmtElapsed(ms) {
925
962
  const hours = Math.floor(total % 86400 / 3600);
926
963
  const minutes = Math.floor(total % 3600 / 60);
927
964
  const seconds = total % 60;
928
- if (days > 0) return `${days}d ${hours}h`;
929
- if (hours > 0) return `${hours}h ${minutes}m`;
930
- if (minutes > 0) return `${minutes}m ${seconds}s`;
965
+ if (days > 0) return `${days}d`;
966
+ if (hours > 0) return `${hours}h`;
967
+ if (minutes > 0) return `${minutes}m`;
931
968
  return `${seconds}s`;
932
969
  }
933
- /** Elapsed label for one entry: running → ticks `now - startedAt`, terminal
934
- * → fixed `updatedAt - startedAt` (em dash when no finalized timestamp). */
970
+ /** Elapsed label for one entry: an ACTIVE entry (running/retry/stale-running)
971
+ * ticks `now - startedAt`; a terminal one is fixed at
972
+ * `updatedAt - startedAt` (em dash when no finalized timestamp). */
935
973
  function delegationElapsed(entry, now) {
936
- if (entry.state === "running" || entry.state === "stale-running") return fmtElapsed(now - entry.startedAt);
974
+ if (entry.state === "running" || entry.state === "retry" || entry.state === "stale-running") return fmtElapsed(now - entry.startedAt);
937
975
  return entry.updatedAt !== null ? fmtElapsed(entry.updatedAt - entry.startedAt) : "—";
938
976
  }
939
977
  /** The activity labels shown by the animated row. Keeping this pure makes the
@@ -942,7 +980,6 @@ function delegationActivity(entry) {
942
980
  if (entry.state === "completed") return "completed";
943
981
  if (entry.state === "error" || entry.state === "startup_failed" || entry.state === "startup_unknown") return "error";
944
982
  if (entry.state === "cancelled") return "cancelled";
945
- if (entry.read) return "reading";
946
983
  if (entry.alias.startsWith("live-") && entry.taskID === void 0) return "delegating";
947
984
  return "working";
948
985
  }
@@ -951,7 +988,6 @@ function delegationActivityLabel(entry) {
951
988
  switch (delegationActivity(entry)) {
952
989
  case "delegating": return "DELEGATING";
953
990
  case "working": return "WORKING";
954
- case "reading": return "READING RESULT";
955
991
  case "completed": return entry.timedOut ? "DONE (TIMED OUT)" : "DONE";
956
992
  case "error": return "ERROR";
957
993
  default: return "CANCELLED";
@@ -969,18 +1005,42 @@ const DELEGATION_SPINNER_FRAMES = [
969
1005
  "⠇",
970
1006
  "⠏"
971
1007
  ];
972
- /** Return a deterministic spinner frame. The View ticks this every 140ms. */
973
- function delegationSpinnerFrame(now) {
974
- const index = Math.floor(Math.max(0, now) / 140) % DELEGATION_SPINNER_FRAMES.length;
1008
+ /** Return the spinner glyph for the given frame counter. The frame counter
1009
+ * is incremented every ~80ms by a dedicated animation timer, decoupled from
1010
+ * the data poll cycle. Pure — no side effects. */
1011
+ function delegationSpinnerFrame(frame) {
1012
+ const index = Math.abs(Math.floor(frame)) % DELEGATION_SPINNER_FRAMES.length;
975
1013
  return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0];
976
1014
  }
1015
+ /** Every state the row knows how to draw: the delegation lifecycle states
1016
+ * derived from the children status + md reports (`completed`, `error`,
1017
+ * `cancelled`, `startup_failed`, `startup_unknown`) plus the TUI-only
1018
+ * states (`retry`, `stale-running`). Fase 1 deliberately omits speculative
1019
+ * blocked/paused/scheduled/skipped. There is no `pending` display state: a
1020
+ * pre-dispatch tool part maps to `running` in {@link reduceDelegationToolPart}. */
1021
+ /** Semantic tone mapped to the TUI theme at the row ({@link DelegationRow}).
1022
+ * Kept separate + pure so the color channel is testable without booting the
1023
+ * renderer. Every display state resolves to one of the three status colors;
1024
+ * the row paints its whole content with it (see {@link DelegationRow}). */
1025
+ /** Status → color mapping for the whole-row tone. Red = failure, green =
1026
+ * terminal, yellow = in flight. The glyph remains a redundant channel so the
1027
+ * state stays legible in monochrome. Display-only; no behavior change. */
1028
+ function delegationStateTone(state) {
1029
+ switch (state) {
1030
+ case "running":
1031
+ case "retry":
1032
+ case "startup_unknown":
1033
+ case "stale-running": return "warning";
1034
+ case "error":
1035
+ case "startup_failed": return "error";
1036
+ case "completed":
1037
+ case "cancelled": return "success";
1038
+ }
1039
+ }
977
1040
  /** Bounded tracker: child session id → latest running tool call. */
978
1041
  const latestToolActivity = /* @__PURE__ */ new Map();
979
1042
  const MAX_TOOL_ACTIVITY_ENTRIES = 200;
980
1043
  const TOOL_ACTIVITY_TTL_MS = 300 * 1e3;
981
- /** Archived-section pagination state — module-level so it survives sidebar
982
- * remounts (route changes) without leaking into the component tree. */
983
- const archivedUiState = { page: 0 };
984
1044
  /** First usable summary string from common tool input fields. */
985
1045
  function summarizeToolInput(input) {
986
1046
  if (!input) return "";
@@ -1049,8 +1109,11 @@ function mergeChildDelegationSources(children, live) {
1049
1109
  bySessionAlias.set(key(entry.sessionID, entry.alias), index);
1050
1110
  });
1051
1111
  for (const liveEntry of live) {
1052
- if (liveEntry.alias === null && liveEntry.taskID === null && Date.now() - liveEntry.startedAt > 3e4) continue;
1112
+ const aliaslessTTL = liveEntry.tool === "task" ? NATIVE_LIVE_ALIASLESS_TTL_MS : 3e4;
1113
+ if (liveEntry.alias === null && liveEntry.taskID === null && Date.now() - liveEntry.startedAt > aliaslessTTL) continue;
1053
1114
  const incoming = toDelegationEntry(liveEntry);
1115
+ const isNativeLive = liveEntry.tool === "task" && liveEntry.alias === null;
1116
+ if (isNativeLive) incoming.source = "children-only";
1054
1117
  const index = (incoming.taskID !== void 0 ? byTask.get(incoming.taskID) : void 0) ?? (incoming.alias !== "" ? bySessionAlias.get(key(incoming.sessionID, incoming.alias)) : void 0);
1055
1118
  if (index === void 0) {
1056
1119
  result.push(incoming);
@@ -1064,7 +1127,6 @@ function mergeChildDelegationSources(children, live) {
1064
1127
  if (existing.state !== "running") {
1065
1128
  if (liveEntry.alias !== null && existing.alias !== incoming.alias) existing.alias = incoming.alias;
1066
1129
  if (incoming.agent !== "agent" && existing.agent !== incoming.agent) existing.agent = incoming.agent;
1067
- if (incoming.read && !existing.read) existing.read = true;
1068
1130
  continue;
1069
1131
  }
1070
1132
  result[index] = {
@@ -1077,30 +1139,26 @@ function mergeChildDelegationSources(children, live) {
1077
1139
  state: incoming.state,
1078
1140
  startedAt: Math.min(existing.startedAt, incoming.startedAt),
1079
1141
  updatedAt: incoming.updatedAt,
1080
- read: incoming.read,
1081
- source: "live"
1142
+ source: isNativeLive ? existing.source : "live"
1082
1143
  };
1083
1144
  }
1084
1145
  result.sort(compareDelegationEntries);
1085
1146
  return result;
1086
1147
  }
1087
1148
  /** Duck-typed subset of a tool part (SDK v2 `ToolPart` / `ToolState`). */
1088
- /** One live delegation tracked in-memory, keyed by the delegate callID. */
1149
+ /** One live delegation tracked in-memory, keyed by the task callID. */
1089
1150
  /** Result of parsing one tool part into lifecycle-relevant fields. */
1090
- /** Alias in the delegate output: "Delegated to apollo: [apo-1] (task …)". */
1151
+ /** Alias in the task output: "Delegated to apollo: [apo-1] (task …)". */
1091
1152
  const DELEGATE_ALIAS_PATTERN = /\[([a-z]{2,8}-\d+)\]/i;
1092
- /** Child task id in the delegate output: "(task ses_child_9)". */
1153
+ /** Child task id in the task output: "(task ses_child_9)". */
1093
1154
  const DELEGATE_TASKID_PATTERN = /\(task\s+([a-z0-9_]+)\)/i;
1094
- /** Plain alias, as passed to pantheon_delegation_read input.id: "apo-1". */
1095
- const READ_ALIAS_PATTERN = /^[a-z]{2,8}-\d+$/i;
1096
1155
  /** Extract the tool name + args from a `message.part.updated` part and
1097
1156
  * reduce it to what the panel needs. Returns null for anything that is
1098
- * not a pantheon delegation tool part, the native `task` subagent tool
1099
- * (same parentID === caller mechanism — its children render `[native]`),
1100
- * or is missing its callID. */
1157
+ * not the native `task` subagent tool (parentID === caller mechanism —
1158
+ * its children render `nat:`), or is missing its callID. */
1101
1159
  function parseDelegationToolPart(part, now = Date.now()) {
1102
1160
  if (part.type !== "tool") return null;
1103
- if (part.tool !== "pantheon_delegate" && part.tool !== "pantheon_delegation_read" && part.tool !== "task") return null;
1161
+ if (part.tool !== "task") return null;
1104
1162
  const callID = part.callID;
1105
1163
  if (callID === void 0 || callID === "") return null;
1106
1164
  const sessionID = part.sessionID ?? "";
@@ -1110,28 +1168,6 @@ function parseDelegationToolPart(part, now = Date.now()) {
1110
1168
  const input = state.input ?? {};
1111
1169
  const startedAt = state.time?.start ?? now;
1112
1170
  const endAt = state.time?.end ?? null;
1113
- if (part.tool === "pantheon_delegation_read") {
1114
- const target = typeof input.id === "string" ? input.id : null;
1115
- let alias = null;
1116
- let taskID = null;
1117
- if (target !== null) {
1118
- if (READ_ALIAS_PATTERN.test(target)) alias = target;
1119
- else if (target.startsWith("ses_")) taskID = target;
1120
- }
1121
- return {
1122
- callID,
1123
- partID: part.id ?? "",
1124
- sessionID,
1125
- tool: "pantheon_delegation_read",
1126
- agent: null,
1127
- description: "",
1128
- status,
1129
- alias,
1130
- taskID,
1131
- startedAt,
1132
- endAt
1133
- };
1134
- }
1135
1171
  const agent = typeof input.agent === "string" ? input.agent : typeof input.subagent_type === "string" ? input.subagent_type : "agent";
1136
1172
  const description = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt.slice(0, 120) : "";
1137
1173
  let alias = null;
@@ -1145,7 +1181,7 @@ function parseDelegationToolPart(part, now = Date.now()) {
1145
1181
  callID,
1146
1182
  partID: part.id ?? "",
1147
1183
  sessionID,
1148
- tool: part.tool === "task" ? "task" : "pantheon_delegate",
1184
+ tool: "task",
1149
1185
  agent,
1150
1186
  description,
1151
1187
  status,
@@ -1155,38 +1191,11 @@ function parseDelegationToolPart(part, now = Date.now()) {
1155
1191
  endAt
1156
1192
  };
1157
1193
  }
1158
- /** Find a live entry by alias or taskID (read parts resolve by id). */
1159
- function findLiveByTarget(map, alias, taskID) {
1160
- if (alias === null && taskID === null) return void 0;
1161
- for (const entry of map.values()) {
1162
- if (alias !== null && entry.alias === alias) return entry;
1163
- if (taskID !== null && entry.taskID === taskID) return entry;
1164
- }
1165
- }
1166
1194
  /** Apply one tool part to the live map. Returns true when the map changed.
1167
1195
  * Pure w.r.t. I/O — only mutates `map`. */
1168
1196
  function reduceDelegationToolPart(map, part, now = Date.now()) {
1169
1197
  const parsed = parseDelegationToolPart(part, now);
1170
1198
  if (parsed === null) return false;
1171
- if (parsed.tool === "pantheon_delegation_read") {
1172
- const target = findLiveByTarget(map, parsed.alias, parsed.taskID);
1173
- if (target === void 0) return false;
1174
- let changed = false;
1175
- if (!target.read) {
1176
- target.read = true;
1177
- changed = true;
1178
- }
1179
- if (parsed.status === "completed" && target.state === "running") {
1180
- target.state = "completed";
1181
- target.updatedAt = parsed.endAt ?? now;
1182
- changed = true;
1183
- } else if (parsed.status === "error" && target.state === "running") {
1184
- target.state = "error";
1185
- target.updatedAt = parsed.endAt ?? now;
1186
- changed = true;
1187
- }
1188
- return changed;
1189
- }
1190
1199
  const existing = map.get(parsed.callID);
1191
1200
  if (parsed.status === "error") {
1192
1201
  if (existing !== void 0 && existing.state === "error" && existing.updatedAt === parsed.endAt) return false;
@@ -1195,14 +1204,13 @@ function reduceDelegationToolPart(map, part, now = Date.now()) {
1195
1204
  partID: parsed.partID,
1196
1205
  sessionID: parsed.sessionID,
1197
1206
  tool: parsed.tool,
1198
- agent: parsed.agent ?? "agent",
1207
+ agent: parsed.agent,
1199
1208
  description: parsed.description,
1200
1209
  alias: existing?.alias ?? null,
1201
1210
  taskID: existing?.taskID ?? null,
1202
1211
  state: "error",
1203
1212
  startedAt: existing?.startedAt ?? parsed.startedAt,
1204
- updatedAt: parsed.endAt ?? now,
1205
- read: existing?.read ?? false
1213
+ updatedAt: parsed.endAt ?? now
1206
1214
  });
1207
1215
  return true;
1208
1216
  }
@@ -1212,14 +1220,13 @@ function reduceDelegationToolPart(map, part, now = Date.now()) {
1212
1220
  partID: parsed.partID,
1213
1221
  sessionID: parsed.sessionID,
1214
1222
  tool: parsed.tool,
1215
- agent: parsed.agent ?? "agent",
1223
+ agent: parsed.agent,
1216
1224
  description: parsed.description,
1217
1225
  alias: parsed.alias,
1218
1226
  taskID: parsed.taskID,
1219
1227
  state: "running",
1220
1228
  startedAt: parsed.startedAt,
1221
- updatedAt: null,
1222
- read: false
1229
+ updatedAt: null
1223
1230
  });
1224
1231
  return true;
1225
1232
  }
@@ -1256,14 +1263,14 @@ function removeDelegationEntry(map, partIDOrCallID) {
1256
1263
  }
1257
1264
  return false;
1258
1265
  }
1259
- /** Collect pantheon delegation + native task tool parts from a session's messages.
1266
+ /** Collect native task tool parts from a session's messages.
1260
1267
  * Messages may carry their parts inline (duck-typed `msg.parts`); when
1261
1268
  * they don't, the optional `getParts(messageID)` callback is used (the TUI
1262
1269
  * SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
1263
- * child session with parentID = caller — the same mechanism as
1264
- * pantheon_delegate — so its parts feed the live-map as the native signal
1265
- * (rows render `[native]` via the children channel). Pure w.r.t. I/O — used
1266
- * by the mount re-scan to re-seed the live map after compaction/attach. */
1270
+ * child session with parentID = caller, so its parts feed the live-map as
1271
+ * the refresh signal (rows come from the children channel). Pure w.r.t.
1272
+ * I/O — used by the mount re-scan to re-seed the live map after
1273
+ * compaction/attach. */
1267
1274
  function collectDelegationToolParts(messages, getParts) {
1268
1275
  const out = [];
1269
1276
  for (const msg of messages ?? []) {
@@ -1273,7 +1280,7 @@ function collectDelegationToolParts(messages, getParts) {
1273
1280
  if (!parts) continue;
1274
1281
  for (const raw of parts) {
1275
1282
  const part = raw;
1276
- if (part?.type === "tool" && (part.tool === "pantheon_delegate" || part.tool === "pantheon_delegation_read" || part.tool === "task")) out.push(part);
1283
+ if (part?.type === "tool" && part.tool === "task") out.push(part);
1277
1284
  }
1278
1285
  }
1279
1286
  return out;
@@ -1288,7 +1295,7 @@ function seedLiveDelegationMap(map, parts, now = Date.now()) {
1288
1295
  return changed;
1289
1296
  }
1290
1297
  /** Convert a live entry into the shared display shape. Alias falls back to
1291
- * a `live-<callID>` prefix while the delegate tool has not completed yet. */
1298
+ * a `live-<callID>` prefix while the task call has not completed yet. */
1292
1299
  function toDelegationEntry(live) {
1293
1300
  return {
1294
1301
  alias: live.alias ?? `live-${live.callID.slice(0, 8)}`,
@@ -1300,7 +1307,6 @@ function toDelegationEntry(live) {
1300
1307
  updatedAt: live.updatedAt,
1301
1308
  timedOut: false,
1302
1309
  description: live.description,
1303
- read: live.read,
1304
1310
  source: "live"
1305
1311
  };
1306
1312
  }
@@ -1333,6 +1339,26 @@ function mergeDelegationSources(live, md) {
1333
1339
  all.sort(compareDelegationEntries);
1334
1340
  return all;
1335
1341
  }
1342
+ /** Scope a fully-merged display list to the ACTIVE session only.
1343
+ *
1344
+ * The panel is session-scoped: rows from other sessions (md history under
1345
+ * `.pantheon/delegations/<other-session>/`) and rows with no
1346
+ * attributable session (empty sessionID) are DROPPED. The previous
1347
+ * cross-session behavior rendered those rows and clicking them led to
1348
+ * "Session not found" — there is no cross-session channel anymore.
1349
+ *
1350
+ * The md channel still feeds the merge UNFILTERED so it can ENRICH an
1351
+ * active-session row with state/alias/agent (dedup by taskID), but it can
1352
+ * never introduce a row for another session: this filter is applied
1353
+ * ONCE, after every merge (children + live + md).
1354
+ *
1355
+ * Returns [] when no active session resolves (null/placeholder) — there is no
1356
+ * scope to show. Native children always carry the active session id (stamped
1357
+ * by `childrenToDelegationEntries`), so they survive the filter. Pure. */
1358
+ function filterDelegationsToSession(entries, activeSessionID) {
1359
+ if (!isValidSessionId(activeSessionID)) return [];
1360
+ return entries.filter((entry) => entry.sessionID === activeSessionID);
1361
+ }
1336
1362
  /** Server-aligned session id validity: opencode rejects anything not starting
1337
1363
  * with "ses" (SchemaError). This deliberately mirrors that exact contract —
1338
1364
  * nothing stricter, nothing looser — so a template placeholder ("{sessionID}"),
@@ -1362,45 +1388,202 @@ function resolveCurrentSessionID(sources) {
1362
1388
  return null;
1363
1389
  }
1364
1390
  /** THE single choke point for every `session.children` / session-API path.
1365
- * Returns `{ path: { id } }` ONLY for a server-valid session id; returns
1366
- * null for anything else (placeholder, empty, foreign id) so the caller
1367
- * skips the call entirely instead of sending an unsubstituted placeholder
1368
- * (the "%7BsessionID%7D" regression). Every session-API call site MUST go
1369
- * through this function (enforced by the source-scan test in
1370
- * tests/pantheon/tui-delegations.test.ts). */
1391
+ * Returns the v2 SDK parameter shape `{ sessionID }` ONLY for a
1392
+ * server-valid session id; returns null for anything else (placeholder,
1393
+ * empty, foreign id) so the caller skips the call entirely instead of
1394
+ * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression).
1395
+ * The TUI client is `@opencode-ai/sdk/v2`, whose session methods take a
1396
+ * FLAT parameter object (`{ sessionID }`), NOT the v1 `{ path: { id } }`
1397
+ * envelope — passing the v1 shape left the v2 `{sessionID}` URL template
1398
+ * unsubstituted (`/session/%7BsessionID%7D/children`). Every session-API
1399
+ * call site MUST go through this function (enforced by the source-scan
1400
+ * test in tests/pantheon/tui-delegations.test.ts). */
1371
1401
  function safeSessionPath(id) {
1372
1402
  if (!isValidSessionId(id)) return null;
1373
- return { path: { id } };
1403
+ return { sessionID: id };
1374
1404
  }
1375
- /** Build the `session.children` path ONLY from a validated session id.
1405
+ /** Build the `session.children` parameters ONLY from a validated session id.
1376
1406
  * Delegates to {@link safeSessionPath} — the single choke point. Returns
1377
- * null for null/invalid ids so the caller skips the fetch instead of
1378
- * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression). */
1407
+ * the v2 SDK shape `{ sessionID }`; null for null/invalid ids so the caller
1408
+ * skips the fetch instead of sending an unsubstituted placeholder (the
1409
+ * "%7BsessionID%7D" regression). */
1379
1410
  function buildChildrenPath(id) {
1380
1411
  return safeSessionPath(id);
1381
1412
  }
1382
1413
  /** Duck-typed subset of a child Session (+ its live status type). */
1383
- /** Map a child status type to a display state. busy/retry → running
1384
- * (the child is actively working), idle → completed, unknown → running
1385
- * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
1386
- * correct it as soon as terminal data exists). */
1387
- function childStatusToState(status) {
1388
- if (status === "idle") return "completed";
1414
+ /** Map a child status type to a display state.
1415
+ *
1416
+ * Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is
1417
+ * any other/unknown status — a status the panel does not recognise is not
1418
+ * evidence of activity. An ABSENT status is the historical case: the status
1419
+ * map (`api.state.session.status`) contains only currently-alive sessions, so
1420
+ * every finished child of a long session is missing from it. Defaulting that
1421
+ * to running made the whole session history render as "active" (the
1422
+ * "197 children, all active" regression), so an absent status is terminal
1423
+ * (done) UNLESS the child was active within `graceMs` — a just-spawned child
1424
+ * that the status API has not registered yet must still render as running.
1425
+ *
1426
+ * @param status the `api.state.session.status(id)?.type`, or undefined
1427
+ * @param time the child session time fields (`created`/`updated`)
1428
+ * @param now current wall-clock ms (injectable for tests)
1429
+ * @param graceMs recency window for the absent-status running assumption
1430
+ * Pure — no I/O. */
1431
+ function childStatusToState(status, time, now = Date.now(), graceMs = DELEGATION_CHILD_STATUS_GRACE_MS) {
1389
1432
  if (status === "retry") return "retry";
1390
- return "running";
1391
- }
1392
- /** Row tag for a delegation entry: `[native]` for native task() children
1393
- * (source 'children-only' — no board report), `[pantheon:<alias>]` for board
1394
- * rows ([pantheon:apo-1]) — the same tags the manager prints in
1395
- * pantheon_delegation_list, so panel rows and CLI list lines match. The
1396
- * panel renders the tag with a distinct style so native task() children are
1397
- * visually separable from pantheon_delegate jobs. */
1398
- function delegationTag(entry) {
1399
- return entry.source === "children-only" ? "[native]" : `[pantheon:${entry.alias}]`;
1400
- }
1401
- /** Split a display list into native task() rows vs pantheon_delegate rows.
1402
- * Native = source 'children-only' (no board report); everything else counts
1403
- * as pantheon. Pure — powers the header breakdown + the hooks.log line. */
1433
+ if (status === "busy") {
1434
+ if (time) {
1435
+ const lastActivity = Math.max(time.updated ?? 0, time.created ?? 0);
1436
+ if (lastActivity > 0 && now - lastActivity > 18e5) return "completed";
1437
+ }
1438
+ return "running";
1439
+ }
1440
+ if (status !== void 0) return "completed";
1441
+ const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0);
1442
+ if (lastActivity > 0 && now - lastActivity < graceMs) return "running";
1443
+ return "completed";
1444
+ }
1445
+ /** Status-only row model: ONE glyph + ONE identity per row.
1446
+ *
1447
+ * A row is
1448
+ * `{glyph} {alias:7} {elapsed:>5}` plus a muted description line. The short
1449
+ * alias (`apo-1`) is the single identity; rows without a report alias show
1450
+ * the agent instead (never both). */
1451
+ /** Map every FSM/display state to one of the 4 row kinds. reconciled never
1452
+ * reaches the panel (the md parser drops it) but reads as done;
1453
+ * cancelled reads as done; startup_failed reads as failed while
1454
+ * startup_unknown (no error known) reads as retry. Pure. */
1455
+ function delegationRowStatus(state) {
1456
+ switch (state) {
1457
+ case "running":
1458
+ case "stale-running": return "active";
1459
+ case "retry":
1460
+ case "startup_unknown": return "retry";
1461
+ case "completed":
1462
+ case "cancelled": return "done";
1463
+ case "error":
1464
+ case "startup_failed": return "failed";
1465
+ }
1466
+ }
1467
+ /** The only 4 glyphs a row may show: animated active, done, failed, retry.
1468
+ * Shape is redundant with color (never the only signal). Pure. */
1469
+ const DELEGATION_ROW_GLYPHS = {
1470
+ active: "⠋",
1471
+ done: "✓",
1472
+ failed: "✕",
1473
+ retry: "⟳"
1474
+ };
1475
+ function delegationRowGlyph(status) {
1476
+ return DELEGATION_ROW_GLYPHS[status];
1477
+ }
1478
+ /** Row marker (`<glyph> `) — the single state channel. `active` animates
1479
+ * through the spinner for the given frame counter (incremented every ~80ms);
1480
+ * every other kind is static. Pure. */
1481
+ function delegationRowMarker(state, frame = 0) {
1482
+ const status = delegationRowStatus(state);
1483
+ return `${status === "active" ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)} `;
1484
+ }
1485
+ /** ONE identity per row: the short report alias (`apo-1`); a row without a
1486
+ * report alias (native task() child `native-task` / `native-<id>`, or an
1487
+ * alias-less native live row `live-<callID>` kept as children-only) shows
1488
+ * the agent instead — never alias + agent stacked. Pure. */
1489
+ function delegationRowIdentity(entry) {
1490
+ if (entry.agent === "") return entry.alias;
1491
+ if (entry.alias === "native-task" || entry.alias.startsWith("native-")) return entry.agent;
1492
+ if (entry.alias.startsWith("live-") && entry.source === "children-only") return entry.agent;
1493
+ return entry.alias;
1494
+ }
1495
+ /** Fixed alias cell, pad-right + hard-truncate to `width` (default 7) so
1496
+ * identities line up across rows. Pure. */
1497
+ const DELEGATION_ALIAS_WIDTH = 7;
1498
+ function formatDelegationAlias(identity, width = 7) {
1499
+ if (identity.length >= width) return identity.slice(0, width);
1500
+ return `${identity}${" ".repeat(width - identity.length)}`;
1501
+ }
1502
+ /** Max description width on the row detail line — the old 180-char slice
1503
+ * wrapped the sidebar; 44 keeps one readable line. */
1504
+ const DELEGATION_DESCRIPTION_MAX = 44;
1505
+ /** Lazily-built grapheme segmenter. `Intl.Segmenter` keeps ZWJ emoji families
1506
+ * (and skin-tone/variation sequences) intact; `Array.from` (code points) is
1507
+ * the fallback for runtimes without it. */
1508
+ let graphemeSegmenter;
1509
+ /** Split `text` into display graphemes so truncation never cuts a multi-unit
1510
+ * emoji in half (surrogate pair → mojibake). */
1511
+ function splitGraphemes(text) {
1512
+ if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
1513
+ graphemeSegmenter ??= new Intl.Segmenter("pt", { granularity: "grapheme" });
1514
+ return Array.from(graphemeSegmenter.segment(text), (s) => s.segment);
1515
+ }
1516
+ return Array.from(text);
1517
+ }
1518
+ /** Truncate a description to `max` graphemes appending `…` when cut. Exact
1519
+ * `max`-length text is left untouched. Grapheme-granular, so an emoji made of
1520
+ * several code points is never split into mojibake at the boundary. Pure. */
1521
+ function truncateDelegationDescription(text, max = 44) {
1522
+ const graphemes = splitGraphemes(text);
1523
+ if (graphemes.length <= max) return text;
1524
+ return `${graphemes.slice(0, Math.max(0, max - 1)).join("")}\u2026`;
1525
+ }
1526
+ /** Fixed elapsed-time box: right-aligned to `width` (default 5) so elapsed
1527
+ * values line up across rows, e.g. ` 12s`. A label that already fills
1528
+ * the box is returned untouched. Pure. */
1529
+ const DELEGATION_ELAPSED_WIDTH = 5;
1530
+ function formatDelegationElapsed(entry, now, width = 5) {
1531
+ const label = delegationElapsed(entry, now);
1532
+ if (label.length >= width) return label;
1533
+ return `${" ".repeat(width - label.length)}${label}`;
1534
+ }
1535
+ /** Colored left half of a row line: `<marker><alias:7> ` — the state glyph
1536
+ * plus the single identity, padded so the muted elapsed column aligns. The
1537
+ * row renders this colored lead separately from the muted elapsed tail. Pure. */
1538
+ function formatDelegationRowLead(entry, marker) {
1539
+ return `${marker}${formatDelegationAlias(delegationRowIdentity(entry))} `;
1540
+ }
1541
+ /** Visible-row ceiling for the panel: the remainder collapses into a single
1542
+ * "… +N more" line. Live rows render first, so a running job is never hidden
1543
+ * by the cap. */
1544
+ const DELEGATION_VISIBLE_CEILING = 8;
1545
+ /** Header summary: `(N active · M done)` plus `· K failed` only when K > 0.
1546
+ * Active counts running/retry AND display-only stale-running (a stale row is
1547
+ * still a live job — it must never read as done); failed counts
1548
+ * error/startup_failed; cancelled reads as done. Pure. */
1549
+ function formatDelegationHeader(entries) {
1550
+ let active = 0;
1551
+ let done = 0;
1552
+ let failed = 0;
1553
+ for (const e of entries) switch (delegationRowStatus(e.state)) {
1554
+ case "active":
1555
+ case "retry":
1556
+ active++;
1557
+ break;
1558
+ case "failed":
1559
+ failed++;
1560
+ break;
1561
+ case "done":
1562
+ done++;
1563
+ break;
1564
+ }
1565
+ const tail = failed > 0 ? ` · ${failed} failed` : "";
1566
+ return `(${active} active · ${done} done${tail})`;
1567
+ }
1568
+ /** Cap the panel: live rows first, then most-recent retained terminal rows, at
1569
+ * most `maxVisible` total. Hidden counts describe the retained render list,
1570
+ * not expired history. */
1571
+ function ceilingDelegationList(all, maxVisible = 8, now = Date.now()) {
1572
+ const { active, recent } = splitDelegationList(all, all.length, now);
1573
+ const visibleActive = active.slice(0, maxVisible);
1574
+ const visible = [...visibleActive, ...recent.slice(0, Math.max(0, maxVisible - visibleActive.length))];
1575
+ const hiddenActive = Math.max(0, active.length - visibleActive.length);
1576
+ const hiddenTerminal = Math.max(0, recent.length - (visible.length - visibleActive.length));
1577
+ return {
1578
+ visible,
1579
+ hidden: hiddenActive + hiddenTerminal,
1580
+ hiddenActive,
1581
+ hiddenTerminal
1582
+ };
1583
+ }
1584
+ /** Split a display list into native task() rows vs pantheon report rows.
1585
+ * Native = source 'children-only' (no delegate report); everything else counts
1586
+ * as pantheon. Pure — powers the hooks.log line. */
1404
1587
  function countDelegationSources(entries) {
1405
1588
  let native = 0;
1406
1589
  for (const e of entries) if (e.source === "children-only") native++;
@@ -1411,7 +1594,7 @@ function countDelegationSources(entries) {
1411
1594
  };
1412
1595
  }
1413
1596
  /** Diagnostic hooks.log line for a panel re-fetch, with the children
1414
- * breakdown (pantheon = children WITH a board report, native = children
1597
+ * breakdown (pantheon = children WITH a delegate report, native = children
1415
1598
  * WITHOUT one). Pure — the View logs the returned string verbatim. */
1416
1599
  function formatPanelLogLine(children, pantheon, native, md, events) {
1417
1600
  return `panel: children=${children}(pantheon=${pantheon} native=${native}) md=${md} events=${events}`;
@@ -1423,17 +1606,39 @@ function formatPanelLogLine(children, pantheon, native, md, events) {
1423
1606
  * report still renders: description from its title, agent from the child
1424
1607
  * itself (fallback 'agent'), state derived from its status, startedAt from
1425
1608
  * time.created. A report-less child is a NATIVE task() child (every
1426
- * child of the current session — pantheon_delegate OR the native `task()`
1427
- * tool — carries parentID = caller), so it gets source 'children-only',
1428
- * the internal alias 'native-task' and the `[native]` tag instead of a
1429
- * board alias. The 'task nativa' description fallback keeps the row
1430
- * non-empty when the child carries no title.
1431
- * Terminal md state wins over the derived state; a running md defers to
1609
+ * child of the current session — the native `task()` tool — carries
1610
+ * parentID = caller), so it gets source 'children-only', a
1611
+ * per-child alias 'native-<last4 of the child id>' (one identity per native
1612
+ * row) instead of a report alias. The 'task nativa' description fallback
1613
+ * keeps the row non-empty when the child carries no title.
1614
+ * A FRESH terminal md state wins over the derived state (a stale md report
1615
+ * from a previous incarnation never flips a live child — see
1616
+ * mdTerminalCoversChild); a running md defers to
1432
1617
  * the child's live status. A running child is NEVER archived — it always
1433
1618
  * lands in the active split (splitDelegationList active = running/retry).
1434
- * Sorted running-first (compareDelegationEntries).
1619
+ * `parentSessionID` (the focused session id) is stamped on report-less
1620
+ * children so the row carries its origin session; omit it and they stay
1621
+ * unscoped ('').
1622
+ * Children outside {@link DELEGATION_CHILDREN_RECENCY_MS} are dropped before
1623
+ * they can enter the list (the per-session child list grows for the whole
1624
+ * session lifetime); the survivors are sorted running-first, most recent
1625
+ * first (compareDelegationEntries), so a ceiling hides the OLDEST rows.
1626
+ * Pure — no I/O. */
1627
+ /** Temporal guard for the children channel (same recycled-alias class as the
1628
+ * md merge): an md terminal report matched by taskID may still belong to
1629
+ * a PREVIOUS job incarnation. Accept it only when its Finalized timestamp
1630
+ * covers the child's latest activity, when either side has no timestamp to
1631
+ * compare, or when both startedAt agree on a single job incarnation.
1435
1632
  * Pure — no I/O. */
1436
- function childrenToDelegationEntries(children, md, now = Date.now()) {
1633
+ function mdTerminalCoversChild(md, child) {
1634
+ const childUpdated = child.time?.updated;
1635
+ if (md.updatedAt !== null && childUpdated !== void 0) {
1636
+ if (md.updatedAt >= childUpdated) return true;
1637
+ return md.startedAt === child.time?.created;
1638
+ }
1639
+ return true;
1640
+ }
1641
+ function childrenToDelegationEntries(children, md, now = Date.now(), parentSessionID = "") {
1437
1642
  const byTaskID = /* @__PURE__ */ new Map();
1438
1643
  for (const m of md) if (m.taskID !== void 0 && !byTaskID.has(m.taskID)) byTaskID.set(m.taskID, m);
1439
1644
  const out = [];
@@ -1442,10 +1647,12 @@ function childrenToDelegationEntries(children, md, now = Date.now()) {
1442
1647
  if (child.id === "" || seen.has(child.id)) continue;
1443
1648
  seen.add(child.id);
1444
1649
  const mdEntry = byTaskID.get(child.id);
1445
- const state = mdEntry !== void 0 && mdEntry.state !== "running" ? mdEntry.state : childStatusToState(child.status);
1650
+ const lastActivity = Math.max(child.time?.updated ?? 0, child.time?.created ?? 0, mdEntry?.updatedAt ?? 0);
1651
+ if (lastActivity > 0 && now - lastActivity > 864e5) continue;
1652
+ const state = mdEntry !== void 0 && mdEntry.state !== "running" && mdTerminalCoversChild(mdEntry, child) ? mdEntry.state : childStatusToState(child.status, child.time, now);
1446
1653
  out.push({
1447
- alias: mdEntry?.alias ?? "native-task",
1448
- sessionID: mdEntry?.sessionID ?? "",
1654
+ alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
1655
+ sessionID: mdEntry?.sessionID ?? parentSessionID,
1449
1656
  taskID: child.id,
1450
1657
  agent: mdEntry?.agent ?? child.agent ?? "agent",
1451
1658
  state,
@@ -1466,8 +1673,19 @@ function childrenToDelegationEntries(children, md, now = Date.now()) {
1466
1673
  * unsubstituted "{sessionID}" placeholder can never be routed. */
1467
1674
  function navigateToDelegationSession(route, taskID) {
1468
1675
  if (typeof route?.navigate !== "function" || !isValidSessionId(taskID)) return false;
1469
- route.navigate("session", { sessionID: taskID });
1470
- return true;
1676
+ try {
1677
+ const navigation = route.navigate("session", { sessionID: taskID });
1678
+ if (navigation !== void 0) Promise.resolve(navigation).catch(() => void 0);
1679
+ return true;
1680
+ } catch {
1681
+ return false;
1682
+ }
1683
+ }
1684
+ /** Build the mouse handler used by each delegation row. */
1685
+ function createDelegationRowOpenHandler(route, taskID) {
1686
+ return () => {
1687
+ navigateToDelegationSession(route, taskID);
1688
+ };
1471
1689
  }
1472
1690
  /** Silence-by-default panel logger.
1473
1691
  * @param projectRoot the PROJECT ROOT — the logger appends to
@@ -1495,7 +1713,11 @@ function createTuiLogger(projectRoot, module = "pantheon-tui") {
1495
1713
  await mkdir(dirname(logPath), { recursive: true });
1496
1714
  const stamp = (/* @__PURE__ */ new Date()).toISOString();
1497
1715
  await appendFile(logPath, `${lines.map((l) => `[${stamp}] [${module}] ${l}`).join("\n")}\n`, "utf8");
1498
- } catch {}
1716
+ } catch (err) {
1717
+ if (echo) try {
1718
+ process.stderr.write(`[${module}] file-log failed: ${formatArg(err)}\n`);
1719
+ } catch {}
1720
+ }
1499
1721
  })();
1500
1722
  if (echo) for (const line of lines) console.log(`[${module}] ${line}`);
1501
1723
  };
@@ -1527,72 +1749,61 @@ function SessionRow(props) {
1527
1749
  }
1528
1750
  function DelegationRow(props) {
1529
1751
  const theme = () => props.api.theme.current;
1530
- const color = createMemo(() => {
1752
+ const tone = createMemo(() => {
1531
1753
  const t = theme();
1532
- switch (props.job.state) {
1533
- case "running": return t.warning;
1534
- case "retry": return t.warning;
1535
- case "stale-running": return t.error;
1536
- case "completed": return t.success;
1754
+ switch (delegationStateTone(props.job.state)) {
1755
+ case "warning": return t.warning;
1537
1756
  case "error": return t.error;
1757
+ case "success": return t.success;
1538
1758
  default: return t.textMuted;
1539
1759
  }
1540
1760
  });
1541
- const marker = createMemo(() => {
1542
- if (props.job.state === "running") return `${delegationSpinnerFrame(props.animationNow)} `;
1543
- if (props.job.state === "retry") return "⟳ ";
1544
- if (props.job.state === "stale-running") return "⚠ ";
1545
- switch (props.job.state) {
1546
- case "completed": return "✓ ";
1547
- case "error": return "✕ ";
1548
- default: return "○ ";
1549
- }
1550
- });
1551
- const tagColor = createMemo(() => props.job.source === "children-only" ? theme().info : color());
1552
- const detail = createMemo(() => {
1553
- const line = `${delegationElapsed(props.job, props.now)}${props.job.description !== "" ? ` \u2014 ${props.job.description}` : ""}`;
1554
- return line.length > 180 ? `${line.slice(0, 177)}\u2026` : line;
1555
- });
1761
+ const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame));
1762
+ const lead = createMemo(() => formatDelegationRowLead(props.job, marker()));
1763
+ const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now));
1764
+ const description = createMemo(() => truncateDelegationDescription(props.job.description));
1556
1765
  const activity = createMemo(() => latestToolActivityFor(latestToolActivity, props.job.taskID, props.now));
1557
- const open = () => {
1558
- navigateToDelegationSession(props.api.route, props.job.taskID);
1559
- };
1766
+ const open = createDelegationRowOpenHandler(props.api.route, props.job.taskID);
1560
1767
  return (() => {
1561
- var _el$13 = createElement("box"), _el$14 = createElement("box"), _el$15 = createElement("text"), _el$16 = createElement("text"), _el$17 = createElement("text"), _el$18 = createElement("text");
1768
+ var _el$13 = createElement("box"), _el$14 = createElement("box"), _el$15 = createElement("text"), _el$16 = createElement("span"), _el$17 = createElement("span");
1562
1769
  insertNode(_el$13, _el$14);
1563
- insertNode(_el$13, _el$18);
1564
1770
  setProp(_el$13, "onMouseDown", open);
1565
1771
  insertNode(_el$14, _el$15);
1566
- insertNode(_el$14, _el$16);
1567
- insertNode(_el$14, _el$17);
1568
1772
  setProp(_el$14, "flexDirection", "row");
1569
- insert(_el$15, marker);
1570
- insert(_el$16, () => delegationTag(props.job));
1571
- insert(_el$17, () => ` ${props.job.agent} \u2014 ${delegationActivityLabel(props.job)}`);
1572
- insert(_el$18, detail);
1773
+ insertNode(_el$15, _el$16);
1774
+ insertNode(_el$15, _el$17);
1775
+ insert(_el$16, lead);
1776
+ insert(_el$17, elapsed);
1777
+ insert(_el$13, createComponent(Show, {
1778
+ get when() {
1779
+ return description() !== "";
1780
+ },
1781
+ get children() {
1782
+ var _el$18 = createElement("text");
1783
+ insert(_el$18, () => ` ${description()}`);
1784
+ effect((_$p) => setProp(_el$18, "fg", tone(), _$p));
1785
+ return _el$18;
1786
+ }
1787
+ }), null);
1573
1788
  insert(_el$13, createComponent(Show, {
1574
1789
  get when() {
1575
1790
  return activity();
1576
1791
  },
1577
1792
  children: (a) => (() => {
1578
1793
  var _el$19 = createElement("text");
1579
- insert(_el$19, () => ` \u21b3 ${a().tool}${a().summary !== "" ? ` ${a().summary}` : ""}`);
1580
- effect((_$p) => setProp(_el$19, "fg", theme().textMuted, _$p));
1794
+ insert(_el$19, () => ` ↳ ${truncateDelegationDescription(`${a().tool}${a().summary !== "" ? ` ${a().summary}` : ""}`)}`);
1795
+ effect((_$p) => setProp(_el$19, "fg", tone(), _$p));
1581
1796
  return _el$19;
1582
1797
  })()
1583
1798
  }), null);
1584
1799
  effect((_p$) => {
1585
- var _v$5 = color(), _v$6 = tagColor(), _v$7 = color(), _v$8 = theme().textMuted;
1586
- _v$5 !== _p$.e && (_p$.e = setProp(_el$15, "fg", _v$5, _p$.e));
1587
- _v$6 !== _p$.t && (_p$.t = setProp(_el$16, "fg", _v$6, _p$.t));
1588
- _v$7 !== _p$.a && (_p$.a = setProp(_el$17, "fg", _v$7, _p$.a));
1589
- _v$8 !== _p$.o && (_p$.o = setProp(_el$18, "fg", _v$8, _p$.o));
1800
+ var _v$5 = tone(), _v$6 = tone();
1801
+ _v$5 !== _p$.e && (_p$.e = setProp(_el$16, "fg", _v$5, _p$.e));
1802
+ _v$6 !== _p$.t && (_p$.t = setProp(_el$17, "fg", _v$6, _p$.t));
1590
1803
  return _p$;
1591
1804
  }, {
1592
1805
  e: void 0,
1593
- t: void 0,
1594
- a: void 0,
1595
- o: void 0
1806
+ t: void 0
1596
1807
  });
1597
1808
  return _el$13;
1598
1809
  })();
@@ -1650,11 +1861,11 @@ function View(props) {
1650
1861
  return (b.time?.updated ?? b.updated ?? 0) - ta;
1651
1862
  }).slice(0, 8);
1652
1863
  });
1653
- const delegationsDir = createMemo(() => resolveDelegationsDir(props.api.state.path));
1654
- const panelLog = createTuiLogger(panelLogDir(delegationsDir()));
1864
+ const projectRoot = createMemo(() => resolvePantheonRoot(props.api.state.path));
1865
+ const panelLog = createTuiLogger(projectRoot());
1655
1866
  const [childDelegations, setChildDelegations] = createSignal([]);
1656
1867
  const [now, setNow] = createSignal(Date.now());
1657
- const [animationNow, setAnimationNow] = createSignal(Date.now());
1868
+ const [frame, setFrame] = createSignal(0);
1658
1869
  let eventRefreshCount = 0;
1659
1870
  let delegationsInflight = null;
1660
1871
  const refreshDelegations = () => {
@@ -1664,7 +1875,7 @@ function View(props) {
1664
1875
  const state = props.api.state;
1665
1876
  let md = [];
1666
1877
  try {
1667
- md = await readAllDelegationEntries(panelLogDir(delegationsDir()));
1878
+ md = await readAllDelegationEntries(projectRoot());
1668
1879
  } catch {
1669
1880
  md = [];
1670
1881
  }
@@ -1673,13 +1884,14 @@ function View(props) {
1673
1884
  api: props.api
1674
1885
  });
1675
1886
  if (sessionID === null) {
1676
- setChildDelegations(md);
1677
- panelLog.info(`${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — history shown)`);
1887
+ setChildDelegations([]);
1888
+ panelLog.info(`${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — inactive panel)`);
1678
1889
  return;
1679
1890
  }
1680
1891
  let children = [];
1681
1892
  try {
1682
- const result = await props.api.client?.session?.children?.(buildChildrenPath(sessionID));
1893
+ const childrenPath = buildChildrenPath(sessionID);
1894
+ const result = childrenPath ? await props.api.client.session.children(childrenPath) : void 0;
1683
1895
  const data = result?.data ?? result;
1684
1896
  children = Array.isArray(data) ? data : [];
1685
1897
  } catch (err) {
@@ -1689,7 +1901,7 @@ function View(props) {
1689
1901
  const resolveStatus = (childID) => {
1690
1902
  if (!isValidSessionId(childID)) return void 0;
1691
1903
  try {
1692
- return state?.session?.status?.(childID)?.type;
1904
+ return state.session.status(childID)?.type;
1693
1905
  } catch {
1694
1906
  return;
1695
1907
  }
@@ -1697,10 +1909,10 @@ function View(props) {
1697
1909
  const childEntries = childrenToDelegationEntries(children.map((c) => ({
1698
1910
  ...c,
1699
1911
  status: resolveStatus(c.id)
1700
- })), md, now());
1912
+ })), md, now(), sessionID);
1701
1913
  for (const [k, v] of props.liveStore.map) if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 3e4) props.liveStore.map.delete(k);
1702
1914
  const liveEntries = [...props.liveStore.map.values()].filter((entry) => entry.sessionID === sessionID);
1703
- setChildDelegations(mergeChildDelegationSources(childEntries, liveEntries));
1915
+ setChildDelegations(filterDelegationsToSession(mergeChildDelegationSources(childEntries, liveEntries), sessionID));
1704
1916
  const counts = countDelegationSources(childEntries);
1705
1917
  panelLog.info(formatPanelLogLine(children.length, counts.pantheon, counts.native, md.length, eventRefreshCount));
1706
1918
  } finally {
@@ -1709,15 +1921,8 @@ function View(props) {
1709
1921
  })();
1710
1922
  return delegationsInflight;
1711
1923
  };
1712
- const delegationSplit = createMemo(() => splitDelegationList(childDelegations()));
1713
- const visibleDelegations = createMemo(() => [...delegationSplit().active, ...delegationSplit().recent]);
1714
- const delegationCounts = createMemo(() => countDelegationSources(visibleDelegations()));
1715
- const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page);
1716
- const clampArchivedPage = (page, pages) => {
1717
- const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)));
1718
- archivedUiState.page = clamped;
1719
- setArchivedPage(clamped);
1720
- };
1924
+ const delegationCeiling = createMemo(() => ceilingDelegationList(childDelegations(), 8, now()));
1925
+ const delegationHeader = createMemo(() => formatDelegationHeader(delegationCeiling().visible));
1721
1926
  onMount(() => {
1722
1927
  const cleanup = [];
1723
1928
  try {
@@ -1746,7 +1951,7 @@ function View(props) {
1746
1951
  refreshDelegations();
1747
1952
  }, 1e3);
1748
1953
  cleanup.push(() => clearInterval(poll));
1749
- const animation = setInterval(() => setAnimationNow(Date.now()), 140);
1954
+ const animation = setInterval(() => setFrame((f) => f + 1), 80);
1750
1955
  cleanup.push(() => clearInterval(animation));
1751
1956
  try {
1752
1957
  const sdk = props.api.state?.session;
@@ -1785,10 +1990,10 @@ function View(props) {
1785
1990
  return branch();
1786
1991
  },
1787
1992
  children: (b) => (() => {
1788
- var _el$32 = createElement("text");
1789
- insert(_el$32, b);
1790
- effect((_$p) => setProp(_el$32, "fg", theme().textMuted, _$p));
1791
- return _el$32;
1993
+ var _el$33 = createElement("text");
1994
+ insert(_el$33, b);
1995
+ effect((_$p) => setProp(_el$33, "fg", theme().textMuted, _$p));
1996
+ return _el$33;
1792
1997
  })()
1793
1998
  }), _el$24);
1794
1999
  insert(_el$22, createComponent(Show, {
@@ -1797,37 +2002,37 @@ function View(props) {
1797
2002
  },
1798
2003
  get fallback() {
1799
2004
  return (() => {
1800
- var _el$33 = createElement("box"), _el$34 = createElement("text");
1801
- insertNode(_el$33, _el$34);
1802
- setProp(_el$33, "flexDirection", "row");
1803
- setProp(_el$33, "gap", 1);
1804
- insertNode(_el$34, createTextNode(`Preset: default`));
1805
- effect((_$p) => setProp(_el$34, "fg", theme().textMuted, _$p));
1806
- return _el$33;
2005
+ var _el$34 = createElement("box"), _el$35 = createElement("text");
2006
+ insertNode(_el$34, _el$35);
2007
+ setProp(_el$34, "flexDirection", "row");
2008
+ setProp(_el$34, "gap", 1);
2009
+ insertNode(_el$35, createTextNode(`Preset: default`));
2010
+ effect((_$p) => setProp(_el$35, "fg", theme().textMuted, _$p));
2011
+ return _el$34;
1807
2012
  })();
1808
2013
  },
1809
2014
  children: (name) => (() => {
1810
- var _el$36 = createElement("box"), _el$37 = createElement("text"), _el$39 = createElement("text"), _el$40 = createElement("text");
1811
- insertNode(_el$36, _el$37);
1812
- insertNode(_el$36, _el$39);
1813
- insertNode(_el$36, _el$40);
1814
- setProp(_el$36, "flexDirection", "row");
1815
- setProp(_el$36, "gap", 1);
1816
- insertNode(_el$37, createTextNode(`⚡ Preset:`));
1817
- insert(_el$39, name);
1818
- insert(_el$40, () => `(${preset().source ?? ""})`);
2015
+ var _el$37 = createElement("box"), _el$38 = createElement("text"), _el$40 = createElement("text"), _el$41 = createElement("text");
2016
+ insertNode(_el$37, _el$38);
2017
+ insertNode(_el$37, _el$40);
2018
+ insertNode(_el$37, _el$41);
2019
+ setProp(_el$37, "flexDirection", "row");
2020
+ setProp(_el$37, "gap", 1);
2021
+ insertNode(_el$38, createTextNode(`⚡ Preset:`));
2022
+ insert(_el$40, name);
2023
+ insert(_el$41, () => `(${preset().source ?? ""})`);
1819
2024
  effect((_p$) => {
1820
- var _v$12 = theme().textMuted, _v$13 = theme().accent, _v$14 = theme().textMuted;
1821
- _v$12 !== _p$.e && (_p$.e = setProp(_el$37, "fg", _v$12, _p$.e));
1822
- _v$13 !== _p$.t && (_p$.t = setProp(_el$39, "fg", _v$13, _p$.t));
1823
- _v$14 !== _p$.a && (_p$.a = setProp(_el$40, "fg", _v$14, _p$.a));
2025
+ var _v$10 = theme().textMuted, _v$11 = theme().accent, _v$12 = theme().textMuted;
2026
+ _v$10 !== _p$.e && (_p$.e = setProp(_el$38, "fg", _v$10, _p$.e));
2027
+ _v$11 !== _p$.t && (_p$.t = setProp(_el$40, "fg", _v$11, _p$.t));
2028
+ _v$12 !== _p$.a && (_p$.a = setProp(_el$41, "fg", _v$12, _p$.a));
1824
2029
  return _p$;
1825
2030
  }, {
1826
2031
  e: void 0,
1827
2032
  t: void 0,
1828
2033
  a: void 0
1829
2034
  });
1830
- return _el$36;
2035
+ return _el$37;
1831
2036
  })()
1832
2037
  }), _el$24);
1833
2038
  insert(_el$22, createComponent(HR, {}), _el$24);
@@ -1848,12 +2053,12 @@ function View(props) {
1848
2053
  },
1849
2054
  get fallback() {
1850
2055
  return (() => {
1851
- var _el$41 = createElement("box"), _el$42 = createElement("text");
1852
- insertNode(_el$41, _el$42);
1853
- setProp(_el$41, "marginLeft", 1);
1854
- insertNode(_el$42, createTextNode(`No recent sessions`));
1855
- effect((_$p) => setProp(_el$42, "fg", theme().textMuted, _$p));
1856
- return _el$41;
2056
+ var _el$42 = createElement("box"), _el$43 = createElement("text");
2057
+ insertNode(_el$42, _el$43);
2058
+ setProp(_el$42, "marginLeft", 1);
2059
+ insertNode(_el$43, createTextNode(`No recent sessions`));
2060
+ effect((_$p) => setProp(_el$43, "fg", theme().textMuted, _$p));
2061
+ return _el$42;
1857
2062
  })();
1858
2063
  },
1859
2064
  get children() {
@@ -1881,7 +2086,10 @@ function View(props) {
1881
2086
  setProp(_el$28, "onMouseDown", () => setShowDelegations((x) => !x));
1882
2087
  setProp(_el$29, "attributes", 1);
1883
2088
  insert(_el$29, () => `${showDelegations() ? "▼" : "▶"} Delegations`);
1884
- insert(_el$30, () => ` (${String(delegationCounts().native)} native + ${String(delegationCounts().pantheon)} pantheon)`);
2089
+ insert(_el$30, (() => {
2090
+ var _c$ = memo(() => delegationCeiling().visible.length > 0);
2091
+ return () => _c$() ? ` ${delegationHeader()}` : " — idle";
2092
+ })());
1885
2093
  insert(_el$22, createComponent(Show, {
1886
2094
  get when() {
1887
2095
  return showDelegations();
@@ -1889,16 +2097,16 @@ function View(props) {
1889
2097
  get children() {
1890
2098
  return createComponent(Show, {
1891
2099
  get when() {
1892
- return visibleDelegations().length > 0;
2100
+ return delegationCeiling().visible.length > 0;
1893
2101
  },
1894
2102
  get fallback() {
1895
2103
  return (() => {
1896
- var _el$44 = createElement("box"), _el$45 = createElement("text");
1897
- insertNode(_el$44, _el$45);
1898
- setProp(_el$44, "marginLeft", 1);
1899
- insertNode(_el$45, createTextNode(`No delegations`));
1900
- effect((_$p) => setProp(_el$45, "fg", theme().textMuted, _$p));
1901
- return _el$44;
2104
+ var _el$45 = createElement("box"), _el$46 = createElement("text");
2105
+ insertNode(_el$45, _el$46);
2106
+ setProp(_el$45, "marginLeft", 1);
2107
+ insertNode(_el$46, createTextNode(`No delegations`));
2108
+ effect((_$p) => setProp(_el$46, "fg", theme().textMuted, _$p));
2109
+ return _el$45;
1902
2110
  })();
1903
2111
  },
1904
2112
  get children() {
@@ -1907,7 +2115,7 @@ function View(props) {
1907
2115
  setProp(_el$31, "flexDirection", "column");
1908
2116
  insert(_el$31, createComponent(For, {
1909
2117
  get each() {
1910
- return visibleDelegations();
2118
+ return delegationCeiling().visible;
1911
2119
  },
1912
2120
  children: (job) => createComponent(DelegationRow, {
1913
2121
  get api() {
@@ -1917,69 +2125,23 @@ function View(props) {
1917
2125
  get now() {
1918
2126
  return now();
1919
2127
  },
1920
- get animationNow() {
1921
- return animationNow();
2128
+ get frame() {
2129
+ return frame();
1922
2130
  }
1923
2131
  })
1924
2132
  }), null);
1925
2133
  insert(_el$31, createComponent(Show, {
1926
2134
  get when() {
1927
- return delegationSplit().archived.length > 0;
2135
+ return delegationCeiling().hidden > 0;
1928
2136
  },
1929
2137
  get children() {
1930
- return (() => {
1931
- const archived = delegationSplit().archived;
1932
- const pageSize = 8;
1933
- const pages = Math.ceil(archived.length / pageSize);
1934
- const page = Math.min(archivedPage(), pages - 1);
1935
- const slice = archived.slice(page * pageSize, page * pageSize + pageSize);
1936
- return (() => {
1937
- var _el$47 = createElement("box"), _el$48 = createElement("box"), _el$49 = createElement("text"), _el$51 = createElement("text"), _el$52 = createElement("text"), _el$53 = createElement("text");
1938
- insertNode(_el$47, _el$48);
1939
- setProp(_el$47, "flexDirection", "column");
1940
- insertNode(_el$48, _el$49);
1941
- insertNode(_el$48, _el$51);
1942
- insertNode(_el$48, _el$52);
1943
- insertNode(_el$48, _el$53);
1944
- setProp(_el$48, "flexDirection", "row");
1945
- setProp(_el$48, "marginLeft", 0);
1946
- insertNode(_el$49, createTextNode(`‹ `));
1947
- setProp(_el$49, "onMouseDown", () => clampArchivedPage(page - 1, pages));
1948
- insert(_el$51, () => `Archived (${archived.length}) `);
1949
- insert(_el$52, `${page + 1}/${pages}`);
1950
- insertNode(_el$53, createTextNode(` ›`));
1951
- setProp(_el$53, "onMouseDown", () => clampArchivedPage(page + 1, pages));
1952
- insert(_el$47, createComponent(For, {
1953
- each: slice,
1954
- children: (job) => createComponent(DelegationRow, {
1955
- get api() {
1956
- return props.api;
1957
- },
1958
- job,
1959
- get now() {
1960
- return now();
1961
- },
1962
- get animationNow() {
1963
- return animationNow();
1964
- }
1965
- })
1966
- }), null);
1967
- effect((_p$) => {
1968
- var _v$15 = theme().textMuted, _v$16 = theme().textMuted, _v$17 = theme().textMuted, _v$18 = theme().textMuted;
1969
- _v$15 !== _p$.e && (_p$.e = setProp(_el$49, "fg", _v$15, _p$.e));
1970
- _v$16 !== _p$.t && (_p$.t = setProp(_el$51, "fg", _v$16, _p$.t));
1971
- _v$17 !== _p$.a && (_p$.a = setProp(_el$52, "fg", _v$17, _p$.a));
1972
- _v$18 !== _p$.o && (_p$.o = setProp(_el$53, "fg", _v$18, _p$.o));
1973
- return _p$;
1974
- }, {
1975
- e: void 0,
1976
- t: void 0,
1977
- a: void 0,
1978
- o: void 0
1979
- });
1980
- return _el$47;
1981
- })();
1982
- })();
2138
+ var _el$32 = createElement("text");
2139
+ insert(_el$32, (() => {
2140
+ var _c$2 = memo(() => delegationCeiling().hiddenActive > 0);
2141
+ return () => _c$2() ? `… +${delegationCeiling().hiddenActive} active` : `… +${delegationCeiling().hiddenTerminal} more`;
2142
+ })());
2143
+ effect((_$p) => setProp(_el$32, "fg", theme().textMuted, _$p));
2144
+ return _el$32;
1983
2145
  }
1984
2146
  }), null);
1985
2147
  return _el$31;
@@ -1988,12 +2150,12 @@ function View(props) {
1988
2150
  }
1989
2151
  }), null);
1990
2152
  effect((_p$) => {
1991
- var _v$9 = theme().accent, _v$0 = theme().text, _v$1 = theme().textMuted, _v$10 = theme().text, _v$11 = theme().textMuted;
1992
- _v$9 !== _p$.e && (_p$.e = setProp(_el$23, "fg", _v$9, _p$.e));
1993
- _v$0 !== _p$.t && (_p$.t = setProp(_el$25, "fg", _v$0, _p$.t));
1994
- _v$1 !== _p$.a && (_p$.a = setProp(_el$26, "fg", _v$1, _p$.a));
1995
- _v$10 !== _p$.o && (_p$.o = setProp(_el$29, "fg", _v$10, _p$.o));
1996
- _v$11 !== _p$.i && (_p$.i = setProp(_el$30, "fg", _v$11, _p$.i));
2153
+ var _v$7 = theme().accent, _v$8 = theme().text, _v$9 = theme().textMuted, _v$0 = theme().text, _v$1 = theme().textMuted;
2154
+ _v$7 !== _p$.e && (_p$.e = setProp(_el$23, "fg", _v$7, _p$.e));
2155
+ _v$8 !== _p$.t && (_p$.t = setProp(_el$25, "fg", _v$8, _p$.t));
2156
+ _v$9 !== _p$.a && (_p$.a = setProp(_el$26, "fg", _v$9, _p$.a));
2157
+ _v$0 !== _p$.o && (_p$.o = setProp(_el$29, "fg", _v$0, _p$.o));
2158
+ _v$1 !== _p$.i && (_p$.i = setProp(_el$30, "fg", _v$1, _p$.i));
1997
2159
  return _p$;
1998
2160
  }, {
1999
2161
  e: void 0,
@@ -2062,6 +2224,6 @@ const plugin = {
2062
2224
  setup: async () => {}
2063
2225
  };
2064
2226
  //#endregion
2065
- export { IDLE_SILENCE_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationSpinnerFrame, delegationTag, extractToolActivity, fmtElapsed, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, tuiLogPath, visibleDelegationList };
2227
+ 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 };
2066
2228
 
2067
2229
  //# sourceMappingURL=tui.js.map