pantheon-opencode 1.3.2 → 1.3.4

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 (57) hide show
  1. package/README.md +154 -1
  2. package/bin/pantheon-init.mjs +160 -120
  3. package/opencode.json +0 -1
  4. package/package.json +10 -5
  5. package/scripts/cost.mjs +69 -0
  6. package/scripts/doctor.mjs +56 -22
  7. package/scripts/hooks/on-subagent-delegation-stop.sh +22 -5
  8. package/scripts/install/opencode.mjs +132 -32
  9. package/scripts/install/plugin.mjs +112 -28
  10. package/scripts/install-mcp.mjs +1 -1
  11. package/scripts/redaction-gate.mjs +69 -0
  12. package/scripts/release-notes.mjs +317 -0
  13. package/scripts/test-context-compression.sh +5 -2
  14. package/scripts/validate-package.mjs +56 -0
  15. package/scripts/versioning.mjs +117 -18
  16. package/src/agents/zeus.md +80 -0
  17. package/src/pantheon/background-job-board.ts +115 -40
  18. package/src/pantheon/chat-reminders.ts +79 -0
  19. package/src/pantheon/compaction-assert.ts +160 -0
  20. package/src/pantheon/cost-command.ts +208 -0
  21. package/src/pantheon/cost-tracker.ts +108 -0
  22. package/src/pantheon/delegation-compaction.ts +237 -0
  23. package/src/pantheon/delegation-enforce.ts +144 -0
  24. package/src/pantheon/delegation-finalize.ts +277 -0
  25. package/src/pantheon/delegation-notify.ts +100 -0
  26. package/src/pantheon/delegation.ts +690 -0
  27. package/src/pantheon/dispatch-guard.ts +107 -0
  28. package/src/pantheon/file-persistence.ts +10 -6
  29. package/src/pantheon/goal-loop.ts +300 -0
  30. package/src/pantheon/goal-store.ts +97 -0
  31. package/src/pantheon/hashline/core.ts +91 -0
  32. package/src/pantheon/hashline/read-enhancer.ts +66 -0
  33. package/src/pantheon/hashline/tool.ts +260 -0
  34. package/src/pantheon/hashline/xxhash.ts +53 -0
  35. package/src/pantheon/idle-continuation.ts +54 -0
  36. package/src/pantheon/logger.ts +117 -0
  37. package/src/pantheon/plugin-once.ts +76 -0
  38. package/src/pantheon/preemptive-compact.ts +76 -0
  39. package/src/pantheon/presets.d.mts +31 -0
  40. package/src/pantheon/presets.mjs +105 -3
  41. package/src/pantheon/todo-enforcer.ts +421 -0
  42. package/src/pantheon/todo-preserve.ts +295 -0
  43. package/src/plugin.ts +406 -13
  44. package/src/plugins/pantheon-hooks.ts +52 -30
  45. package/src/plugins/tui/dist/server.d.ts +17 -0
  46. package/src/plugins/tui/dist/server.d.ts.map +1 -0
  47. package/src/plugins/tui/dist/server.js +20 -0
  48. package/src/plugins/tui/dist/server.js.map +1 -0
  49. package/src/plugins/tui/dist/tui.d.ts +303 -1
  50. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  51. package/src/plugins/tui/dist/tui.js +977 -350
  52. package/src/plugins/tui/dist/tui.tsx +1427 -150
  53. package/src/plugins/tui/package.json +4 -3
  54. package/src/plugins/tui/src/index.tsx +1427 -150
  55. package/src/plugins/tui/src/server.ts +15 -0
  56. package/src/plugins/tui/tsdown.config.ts +1 -1
  57. package/src/routing.yml +35 -2
@@ -1,10 +1,10 @@
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, writeFile } from "node:fs/promises";
3
+ import { 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";
7
- import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js";
7
+ import { For, Show, createEffect, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js";
8
8
  //#region src/index.tsx
9
9
  /** @jsxImportSource @opentui/solid */
10
10
  /**
@@ -16,7 +16,7 @@ import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount
16
16
  *
17
17
  * Slots:
18
18
  * - sidebar_content (order 900) — Pantheon sidebar (header/version/
19
- * branch, Sessions, Commands, Agents, Config, Memory).
19
+ * branch, Sessions, real-time Delegations panel).
20
20
  * - app_bottom (order 60) — AI subscription usage gauges
21
21
  * (Anthropic/OpenAI quotas, OpenCode Go/Zen dollar limits + provider
22
22
  * status incidents).
@@ -49,130 +49,9 @@ import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount
49
49
  * (the old ContextBar was removed) and we deliberately do NOT hack the
50
50
  * native footer. Track upstream: opencode statusline config.
51
51
  */
52
- const COMMANDS = [
53
- {
54
- name: "/pantheon",
55
- desc: "Council synthesis"
56
- },
57
- {
58
- name: "/pantheon-status",
59
- desc: "System status"
60
- },
61
- {
62
- name: "/pantheon-audit",
63
- desc: "Full audit"
64
- },
65
- {
66
- name: "/pantheon-bg",
67
- desc: "List background tasks"
68
- },
69
- {
70
- name: "/pantheon-cancel",
71
- desc: "Cancel task"
72
- },
73
- {
74
- name: "/pantheon-deepwork",
75
- desc: "Deep work mode"
76
- },
77
- {
78
- name: "/pantheon-focus",
79
- desc: "Focus on scope"
80
- },
81
- {
82
- name: "/pantheon-remember",
83
- desc: "Memory store/recall"
84
- },
85
- {
86
- name: "/pantheon-search",
87
- desc: "Memory search"
88
- },
89
- {
90
- name: "/pantheon-consolidate",
91
- desc: "Merge memories"
92
- },
93
- {
94
- name: "/pantheon-forget",
95
- desc: "Compress memories"
96
- }
97
- ];
98
52
  /** How often the sidebar re-reads .pantheon/active-preset.json so `set-tier`
99
53
  * changes made while opencode is open show up within ~30s. */
100
54
  const PRESET_REFRESH_MS = 3e4;
101
- const AGENTS = [
102
- {
103
- name: "zeus",
104
- tier: "default",
105
- role: "Orchestrator"
106
- },
107
- {
108
- name: "athena",
109
- tier: "premium",
110
- role: "Strategic planner"
111
- },
112
- {
113
- name: "apollo",
114
- tier: "fast",
115
- role: "Codebase discovery"
116
- },
117
- {
118
- name: "hermes",
119
- tier: "default",
120
- role: "Backend"
121
- },
122
- {
123
- name: "aphrodite",
124
- tier: "default",
125
- role: "Frontend"
126
- },
127
- {
128
- name: "demeter",
129
- tier: "default",
130
- role: "Database"
131
- },
132
- {
133
- name: "themis",
134
- tier: "premium",
135
- role: "Quality & security"
136
- },
137
- {
138
- name: "prometheus",
139
- tier: "default",
140
- role: "Infrastructure"
141
- },
142
- {
143
- name: "hephaestus",
144
- tier: "default",
145
- role: "AI pipelines"
146
- },
147
- {
148
- name: "nyx",
149
- tier: "fast",
150
- role: "Observability"
151
- },
152
- {
153
- name: "gaia",
154
- tier: "fast",
155
- role: "Remote sensing"
156
- },
157
- {
158
- name: "iris",
159
- tier: "fast",
160
- role: "GitHub operations"
161
- },
162
- {
163
- name: "mnemosyne",
164
- tier: "fast",
165
- role: "Memory bank"
166
- },
167
- {
168
- name: "talos",
169
- tier: "fast",
170
- role: "Hotfixes"
171
- }
172
- ];
173
- function fmtInt(n) {
174
- return Intl.NumberFormat("en-US").format(Math.max(0, Math.round(n)));
175
- }
176
55
  /** Read .pantheon/active-preset.json — mirrors the presets.mjs file leg:
177
56
  * first existing candidate wins; malformed JSON or a `preset` that is
178
57
  * missing, empty or "none" → null (no fall-through to lower candidates). */
@@ -795,9 +674,685 @@ async function setupUsageBar(api) {
795
674
  } }
796
675
  });
797
676
  }
677
+ /** True for `\s` characters (space, tab, newline, CR) — plain char checks so
678
+ * the parser stays regex-free (CodeQL flagged the old `\s*`/`\s+` + `(.+)`
679
+ * patterns as polynomial ReDoS: 12x HIGH). */
680
+ function isWs(ch) {
681
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r";
682
+ }
683
+ /** Em/en dash + hyphen — the separators accepted after the H1 title. */
684
+ const TITLE_SEPARATORS = "—–-";
685
+ /** Parse one delegation report md header into a structured entry.
686
+ * Returns null (skip) when the file is not a recognizable report:
687
+ * missing agent/state/startedAt, an unknown state, or an unparsable
688
+ * Started timestamp. The alias falls back to the file name when the H1
689
+ * title is missing. Pure — no I/O.
690
+ *
691
+ * Linear, single-pass over `raw.split('\n')` with plain string operations
692
+ * (startsWith/indexOf/slice) — zero regex, so worst case is O(bytes) even
693
+ * on adversarial whitespace-heavy input (ReDoS regression, CodeQL 12x HIGH). */
694
+ function parseDelegationMarkdown(raw, fileAlias, sessionID = "") {
695
+ let title;
696
+ let agent;
697
+ let description = "";
698
+ let state;
699
+ let timedOut = false;
700
+ let started;
701
+ let finalized;
702
+ let taskID;
703
+ for (const rawLine of raw.split("\n")) {
704
+ if (rawLine[0] === "#" && isWs(rawLine[1])) {
705
+ let i = 2;
706
+ while (i < rawLine.length && isWs(rawLine[i])) i++;
707
+ if (rawLine.startsWith("Delegation Report", i)) {
708
+ i += 17;
709
+ while (i < rawLine.length && isWs(rawLine[i])) i++;
710
+ if (i < rawLine.length && TITLE_SEPARATORS.includes(rawLine[i])) {
711
+ const rest = rawLine.slice(i + 1).trim();
712
+ if (rest !== "" && title === void 0) title = rest;
713
+ }
714
+ }
715
+ continue;
716
+ }
717
+ if (rawLine[0] !== "-") continue;
718
+ let i = 1;
719
+ while (i < rawLine.length && isWs(rawLine[i])) i++;
720
+ if (!rawLine.startsWith("**", i)) continue;
721
+ const nameStart = i + 2;
722
+ const valueEnd = rawLine.indexOf("**:", nameStart);
723
+ if (valueEnd < 0) continue;
724
+ const name = rawLine.slice(nameStart, valueEnd);
725
+ const value = rawLine.slice(valueEnd + 3).trim();
726
+ switch (name) {
727
+ case "Task ID":
728
+ if (value !== "" && taskID === void 0) taskID = stripTaskIdTicks(value);
729
+ break;
730
+ case "Agent":
731
+ if (value !== "" && agent === void 0) agent = value;
732
+ break;
733
+ case "Description":
734
+ description = value;
735
+ break;
736
+ case "State":
737
+ if (value !== "" && state === void 0) state = value;
738
+ break;
739
+ case "Timed out":
740
+ timedOut = value.startsWith("true");
741
+ break;
742
+ case "Started":
743
+ if (value !== "" && started === void 0) started = value;
744
+ break;
745
+ case "Finalized":
746
+ finalized = value;
747
+ break;
748
+ default: break;
749
+ }
750
+ }
751
+ const startedAt = started !== void 0 ? Date.parse(started) : NaN;
752
+ if (agent === void 0 || state === void 0 || Number.isNaN(startedAt)) return null;
753
+ const normalized = state.toLowerCase();
754
+ if (normalized !== "running" && normalized !== "completed" && normalized !== "error" && normalized !== "cancelled") return null;
755
+ const finalizedAt = finalized !== void 0 ? Date.parse(finalized) : NaN;
756
+ return {
757
+ alias: title ?? (fileAlias !== void 0 ? stripMdSuffix(fileAlias) : "unknown"),
758
+ sessionID,
759
+ ...taskID !== void 0 ? { taskID } : {},
760
+ agent,
761
+ state: normalized,
762
+ startedAt,
763
+ updatedAt: Number.isNaN(finalizedAt) ? null : finalizedAt,
764
+ timedOut,
765
+ description,
766
+ source: "md"
767
+ };
768
+ }
769
+ /** Strip a trailing `.md` (any case) — linear replacement for /\.md$/i. */
770
+ function stripMdSuffix(name) {
771
+ return name.toLowerCase().endsWith(".md") ? name.slice(0, name.length - 3) : name;
772
+ }
773
+ /** Strip surrounding backticks from a `Task ID` value: "`ses_x`" → "ses_x". */
774
+ function stripTaskIdTicks(value) {
775
+ const start = value[0] === "`" ? 1 : 0;
776
+ const end = value.length > start && value[value.length - 1] === "`" ? value.length - 1 : value.length;
777
+ return value.slice(start, end);
778
+ }
779
+ /** Read every delegation report under `<dir>/<sessionID>/<alias>.md`.
780
+ * Fail-open: a missing/unreadable directory yields [], and each unreadable
781
+ * or malformed file is skipped individually. Entries are sorted running
782
+ * first, then terminal by `updatedAt` (most recent first) so the panel can
783
+ * render them in order directly. */
784
+ async function readDelegationEntries(dir) {
785
+ let sessionDirs;
786
+ try {
787
+ sessionDirs = await readdir(dir, { withFileTypes: true });
788
+ } catch {
789
+ return [];
790
+ }
791
+ const entries = [];
792
+ for (const session of sessionDirs) {
793
+ if (!session.isDirectory()) continue;
794
+ let files;
795
+ try {
796
+ files = await readdir(join(dir, session.name), { withFileTypes: true });
797
+ } catch {
798
+ continue;
799
+ }
800
+ for (const file of files) {
801
+ if (!file.isFile() || !file.name.endsWith(".md")) continue;
802
+ try {
803
+ const entry = parseDelegationMarkdown(await readFile(join(dir, session.name, file.name), "utf8"), file.name, session.name);
804
+ if (entry !== null) entries.push(entry);
805
+ } catch {}
806
+ }
807
+ }
808
+ entries.sort(compareDelegationEntries);
809
+ return entries;
810
+ }
811
+ /** Read delegation reports from EVERY session under
812
+ * `<root>/.pantheon/delegations/<sessionID>/<alias>.md` — the panel's
813
+ * HISTORY channel. Unlike the children/live channels it does NOT depend on a
814
+ * resolved sessionID: with no focused session (null/placeholder), the panel
815
+ * still shows the reports from all past sessions (running first, Finalized
816
+ * desc — the sort applied by readDelegationEntries). Fail-open: a
817
+ * missing/unreadable directory yields []. */
818
+ async function readAllDelegationEntries(root) {
819
+ return readDelegationEntries(join(root, ".pantheon", "delegations"));
820
+ }
821
+ /** Resolve the directory where the job board writes delegation md reports.
822
+ * The board writes `.pantheon/delegations` RELATIVE to the server cwd,
823
+ * which the TUI exposes as `TuiState.path.directory`. `project` does NOT
824
+ * exist on `TuiState.path` (the old `state?.project ?? state?.worktree`
825
+ * resolution was always undefined for the first term) and `worktree` is
826
+ * `/` when there is no git (e.g. the sandbox test project) — a root of
827
+ * `''` or `'/'` must fall back to `process.cwd()`. */
828
+ function resolveDelegationsDir(state, cwd = process.cwd()) {
829
+ const root = state?.directory ?? state?.worktree ?? "";
830
+ if (root === "" || root === "/") return join(cwd, ".pantheon", "delegations");
831
+ return join(root, ".pantheon", "delegations");
832
+ }
833
+ /** Project root derived from the delegations dir: `<root>/.pantheon/delegations`
834
+ * → `<root>`. Used to point the panel logger at the REAL hooks.log — passing
835
+ * the delegations dir (or its dirname) directly made createTuiLogger append
836
+ * to `<root>/.pantheon/.pantheon/logs/hooks.log`, a nested empty dir the real
837
+ * log never saw. Pure — no I/O. */
838
+ function panelLogDir(delegationsDir) {
839
+ return dirname(dirname(delegationsDir));
840
+ }
841
+ /** Where the panel logger appends lines: `<projectRoot>/.pantheon/logs/hooks.log`.
842
+ * Pure — testable without the runtime. */
843
+ function tuiLogPath(projectRoot) {
844
+ return join(projectRoot, ".pantheon", "logs", "hooks.log");
845
+ }
846
+ /** Sort delegations: running first, then terminal by recency (updatedAt,
847
+ * falling back to startedAt, descending). Shared by the md reader and
848
+ * mergeDelegationSources. */
849
+ function compareDelegationEntries(a, b) {
850
+ const aRun = a.state === "running" ? 1 : 0;
851
+ const bRun = b.state === "running" ? 1 : 0;
852
+ if (aRun !== bRun) return bRun - aRun;
853
+ return (b.updatedAt ?? b.startedAt) - (a.updatedAt ?? a.startedAt);
854
+ }
855
+ /** The list the panel actually renders: running jobs first, then the most
856
+ * recent terminal reports (capped). Pure — so the history-only panel (no
857
+ * sessionID) is testable without the TUI runtime. The header count uses the
858
+ * same "running + recentes" list. */
859
+ function visibleDelegationList(all, maxTerminal = 8) {
860
+ const running = all.filter((d) => d.state === "running");
861
+ const terminal = all.filter((d) => d.state !== "running").slice(0, maxTerminal);
862
+ return [...running, ...terminal];
863
+ }
864
+ /** Compact elapsed-time label: "5m 12s", "1h 30m", "2d 4h" — ticks every
865
+ * second for running jobs. */
866
+ function fmtElapsed(ms) {
867
+ const total = Math.max(0, Math.floor(ms / 1e3));
868
+ const days = Math.floor(total / 86400);
869
+ const hours = Math.floor(total % 86400 / 3600);
870
+ const minutes = Math.floor(total % 3600 / 60);
871
+ const seconds = total % 60;
872
+ if (days > 0) return `${days}d ${hours}h`;
873
+ if (hours > 0) return `${hours}h ${minutes}m`;
874
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
875
+ return `${seconds}s`;
876
+ }
877
+ /** Elapsed label for one entry: running → ticks `now - startedAt`, terminal
878
+ * → fixed `updatedAt - startedAt` (em dash when no finalized timestamp). */
879
+ function delegationElapsed(entry, now) {
880
+ if (entry.state === "running") return fmtElapsed(now - entry.startedAt);
881
+ return entry.updatedAt !== null ? fmtElapsed(entry.updatedAt - entry.startedAt) : "—";
882
+ }
883
+ /** The activity labels shown by the animated row. Keeping this pure makes the
884
+ * state machine testable without booting OpenCode's renderer. */
885
+ function delegationActivity(entry) {
886
+ if (entry.state === "completed") return "completed";
887
+ if (entry.state === "error") return "error";
888
+ if (entry.state === "cancelled") return "cancelled";
889
+ if (entry.read) return "reading";
890
+ if (entry.alias.startsWith("live-") && entry.taskID === void 0) return "delegating";
891
+ return "working";
892
+ }
893
+ function delegationActivityLabel(entry) {
894
+ switch (delegationActivity(entry)) {
895
+ case "delegating": return "DELEGATING";
896
+ case "working": return "WORKING";
897
+ case "reading": return "READING RESULT";
898
+ case "completed": return entry.timedOut ? "DONE (TIMED OUT)" : "DONE";
899
+ case "error": return "ERROR";
900
+ default: return "CANCELLED";
901
+ }
902
+ }
903
+ const DELEGATION_SPINNER_FRAMES = [
904
+ "⠋",
905
+ "⠙",
906
+ "⠹",
907
+ "⠸",
908
+ "⠼",
909
+ "⠴",
910
+ "⠦",
911
+ "⠧",
912
+ "⠇",
913
+ "⠏"
914
+ ];
915
+ /** Return a deterministic spinner frame. The View ticks this every 140ms. */
916
+ function delegationSpinnerFrame(now) {
917
+ const index = Math.floor(Math.max(0, now) / 140) % DELEGATION_SPINNER_FRAMES.length;
918
+ return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0];
919
+ }
920
+ /** Merge the immediate tool-event channel into the child-session channel.
921
+ *
922
+ * Children remain the durable source, while live entries make a delegation
923
+ * visible before the child API/report catches up. A finalized md entry wins
924
+ * over a stale live entry; a child-only row is upgraded with live agent,
925
+ * alias, phase and timestamps. */
926
+ function mergeChildDelegationSources(children, live) {
927
+ const result = [...children];
928
+ const byTask = /* @__PURE__ */ new Map();
929
+ const bySessionAlias = /* @__PURE__ */ new Map();
930
+ const key = (sessionID, alias) => `${sessionID}\u0000${alias}`;
931
+ result.forEach((entry, index) => {
932
+ if (entry.taskID !== void 0) byTask.set(entry.taskID, index);
933
+ bySessionAlias.set(key(entry.sessionID, entry.alias), index);
934
+ });
935
+ for (const liveEntry of live) {
936
+ const incoming = toDelegationEntry(liveEntry);
937
+ const index = (incoming.taskID !== void 0 ? byTask.get(incoming.taskID) : void 0) ?? (incoming.alias !== "" ? bySessionAlias.get(key(incoming.sessionID, incoming.alias)) : void 0);
938
+ if (index === void 0) {
939
+ result.push(incoming);
940
+ const newIndex = result.length - 1;
941
+ if (incoming.taskID !== void 0) byTask.set(incoming.taskID, newIndex);
942
+ bySessionAlias.set(key(incoming.sessionID, incoming.alias), newIndex);
943
+ continue;
944
+ }
945
+ const existing = result[index];
946
+ if (existing === void 0) continue;
947
+ if (existing.source === "md" && existing.state !== "running") continue;
948
+ result[index] = {
949
+ ...existing,
950
+ alias: liveEntry.alias !== null ? incoming.alias : existing.alias,
951
+ sessionID: existing.sessionID || incoming.sessionID,
952
+ taskID: existing.taskID ?? incoming.taskID,
953
+ agent: incoming.agent !== "agent" ? incoming.agent : existing.agent,
954
+ description: incoming.description !== "" ? incoming.description : existing.description,
955
+ state: incoming.state,
956
+ startedAt: Math.min(existing.startedAt, incoming.startedAt),
957
+ updatedAt: incoming.updatedAt,
958
+ read: incoming.read,
959
+ source: "live"
960
+ };
961
+ }
962
+ result.sort(compareDelegationEntries);
963
+ return result;
964
+ }
965
+ /** Duck-typed subset of a tool part (SDK v2 `ToolPart` / `ToolState`). */
966
+ /** One live delegation tracked in-memory, keyed by the delegate callID. */
967
+ /** Result of parsing one tool part into lifecycle-relevant fields. */
968
+ /** Alias in the delegate output: "Delegated to apollo: [apo-1] (task …)". */
969
+ const DELEGATE_ALIAS_PATTERN = /\[([a-z]{2,8}-\d+)\]/i;
970
+ /** Child task id in the delegate output: "(task ses_child_9)". */
971
+ const DELEGATE_TASKID_PATTERN = /\(task\s+([a-z0-9_]+)\)/i;
972
+ /** Plain alias, as passed to pantheon_delegation_read input.id: "apo-1". */
973
+ const READ_ALIAS_PATTERN = /^[a-z]{2,8}-\d+$/i;
974
+ /** Extract the tool name + args from a `message.part.updated` part and
975
+ * reduce it to what the panel needs. Returns null for anything that is
976
+ * not a pantheon delegation tool part (or is missing its callID). */
977
+ function parseDelegationToolPart(part, now = Date.now()) {
978
+ if (part.type !== "tool") return null;
979
+ if (part.tool !== "pantheon_delegate" && part.tool !== "pantheon_delegation_read") return null;
980
+ const callID = part.callID;
981
+ if (callID === void 0 || callID === "") return null;
982
+ const sessionID = part.sessionID ?? "";
983
+ const state = part.state ?? {};
984
+ const status = state.status ?? "pending";
985
+ if (status !== "pending" && status !== "running" && status !== "completed" && status !== "error") return null;
986
+ const input = state.input ?? {};
987
+ const startedAt = state.time?.start ?? now;
988
+ const endAt = state.time?.end ?? null;
989
+ if (part.tool === "pantheon_delegation_read") {
990
+ const target = typeof input.id === "string" ? input.id : null;
991
+ let alias = null;
992
+ let taskID = null;
993
+ if (target !== null) {
994
+ if (READ_ALIAS_PATTERN.test(target)) alias = target;
995
+ else if (target.startsWith("ses_")) taskID = target;
996
+ }
997
+ return {
998
+ callID,
999
+ partID: part.id ?? "",
1000
+ sessionID,
1001
+ tool: "pantheon_delegation_read",
1002
+ agent: null,
1003
+ description: "",
1004
+ status,
1005
+ alias,
1006
+ taskID,
1007
+ startedAt,
1008
+ endAt
1009
+ };
1010
+ }
1011
+ const agent = typeof input.agent === "string" ? input.agent : "agent";
1012
+ const description = typeof input.description === "string" ? input.description : "";
1013
+ let alias = null;
1014
+ let taskID = null;
1015
+ if (status === "completed") {
1016
+ const output = state.output ?? "";
1017
+ alias = DELEGATE_ALIAS_PATTERN.exec(output)?.[1] ?? null;
1018
+ taskID = DELEGATE_TASKID_PATTERN.exec(output)?.[1] ?? null;
1019
+ }
1020
+ return {
1021
+ callID,
1022
+ partID: part.id ?? "",
1023
+ sessionID,
1024
+ tool: "pantheon_delegate",
1025
+ agent,
1026
+ description,
1027
+ status,
1028
+ alias,
1029
+ taskID,
1030
+ startedAt,
1031
+ endAt
1032
+ };
1033
+ }
1034
+ /** Find a live entry by alias or taskID (read parts resolve by id). */
1035
+ function findLiveByTarget(map, alias, taskID) {
1036
+ if (alias === null && taskID === null) return void 0;
1037
+ for (const entry of map.values()) {
1038
+ if (alias !== null && entry.alias === alias) return entry;
1039
+ if (taskID !== null && entry.taskID === taskID) return entry;
1040
+ }
1041
+ }
1042
+ /** Apply one tool part to the live map. Returns true when the map changed.
1043
+ * Pure w.r.t. I/O — only mutates `map`. */
1044
+ function reduceDelegationToolPart(map, part, now = Date.now()) {
1045
+ const parsed = parseDelegationToolPart(part, now);
1046
+ if (parsed === null) return false;
1047
+ if (parsed.tool === "pantheon_delegation_read") {
1048
+ const target = findLiveByTarget(map, parsed.alias, parsed.taskID);
1049
+ if (target === void 0) return false;
1050
+ let changed = false;
1051
+ if (!target.read) {
1052
+ target.read = true;
1053
+ changed = true;
1054
+ }
1055
+ if (parsed.status === "completed" && target.state === "running") {
1056
+ target.state = "completed";
1057
+ target.updatedAt = parsed.endAt ?? now;
1058
+ changed = true;
1059
+ } else if (parsed.status === "error" && target.state === "running") {
1060
+ target.state = "error";
1061
+ target.updatedAt = parsed.endAt ?? now;
1062
+ changed = true;
1063
+ }
1064
+ return changed;
1065
+ }
1066
+ const existing = map.get(parsed.callID);
1067
+ if (parsed.status === "error") {
1068
+ if (existing !== void 0 && existing.state === "error" && existing.updatedAt === parsed.endAt) return false;
1069
+ map.set(parsed.callID, {
1070
+ callID: parsed.callID,
1071
+ partID: parsed.partID,
1072
+ sessionID: parsed.sessionID,
1073
+ tool: "pantheon_delegate",
1074
+ agent: parsed.agent ?? "agent",
1075
+ description: parsed.description,
1076
+ alias: existing?.alias ?? null,
1077
+ taskID: existing?.taskID ?? null,
1078
+ state: "error",
1079
+ startedAt: existing?.startedAt ?? parsed.startedAt,
1080
+ updatedAt: parsed.endAt ?? now,
1081
+ read: existing?.read ?? false
1082
+ });
1083
+ return true;
1084
+ }
1085
+ if (existing === void 0) {
1086
+ map.set(parsed.callID, {
1087
+ callID: parsed.callID,
1088
+ partID: parsed.partID,
1089
+ sessionID: parsed.sessionID,
1090
+ tool: "pantheon_delegate",
1091
+ agent: parsed.agent ?? "agent",
1092
+ description: parsed.description,
1093
+ alias: parsed.alias,
1094
+ taskID: parsed.taskID,
1095
+ state: "running",
1096
+ startedAt: parsed.startedAt,
1097
+ updatedAt: null,
1098
+ read: false
1099
+ });
1100
+ return true;
1101
+ }
1102
+ let changed = false;
1103
+ if (parsed.alias !== null && existing.alias !== parsed.alias) {
1104
+ existing.alias = parsed.alias;
1105
+ changed = true;
1106
+ }
1107
+ if (parsed.taskID !== null && existing.taskID !== parsed.taskID) {
1108
+ existing.taskID = parsed.taskID;
1109
+ changed = true;
1110
+ }
1111
+ if (parsed.agent !== null && existing.agent !== parsed.agent) {
1112
+ existing.agent = parsed.agent;
1113
+ changed = true;
1114
+ }
1115
+ if (parsed.description !== "" && existing.description !== parsed.description) {
1116
+ existing.description = parsed.description;
1117
+ changed = true;
1118
+ }
1119
+ if (existing.partID === "" && parsed.partID !== "") {
1120
+ existing.partID = parsed.partID;
1121
+ changed = true;
1122
+ }
1123
+ return changed;
1124
+ }
1125
+ /** Remove a live entry by part id (message.part.removed) or call id.
1126
+ * Returns true when something was removed. */
1127
+ function removeDelegationEntry(map, partIDOrCallID) {
1128
+ if (map.delete(partIDOrCallID)) return true;
1129
+ for (const [key, entry] of map) if (entry.partID === partIDOrCallID) {
1130
+ map.delete(key);
1131
+ return true;
1132
+ }
1133
+ return false;
1134
+ }
1135
+ /** Collect pantheon delegation tool parts from a session's messages.
1136
+ * Messages may carry their parts inline (duck-typed `msg.parts`); when
1137
+ * they don't, the optional `getParts(messageID)` callback is used (the TUI
1138
+ * SDK exposes `api.state.part(messageID)`). Pure w.r.t. I/O — used by the
1139
+ * mount re-scan to re-seed the live map after compaction/attach. */
1140
+ function collectDelegationToolParts(messages, getParts) {
1141
+ const out = [];
1142
+ for (const msg of messages ?? []) {
1143
+ let parts;
1144
+ if (Array.isArray(msg?.parts)) parts = msg.parts;
1145
+ else if (msg?.id !== void 0 && typeof getParts === "function") parts = getParts(msg.id);
1146
+ if (!parts) continue;
1147
+ for (const raw of parts) {
1148
+ const part = raw;
1149
+ if (part?.type === "tool" && (part.tool === "pantheon_delegate" || part.tool === "pantheon_delegation_read")) out.push(part);
1150
+ }
1151
+ }
1152
+ return out;
1153
+ }
1154
+ /** Apply a batch of tool parts (in message order) to the live map. Used on
1155
+ * mount to re-seed entries that `message.part.removed` (compaction) wiped,
1156
+ * from the session's existing tool parts. Returns how many parts changed
1157
+ * the map (0 on the second identical seed — idempotent, no extra bumps). */
1158
+ function seedLiveDelegationMap(map, parts, now = Date.now()) {
1159
+ let changed = 0;
1160
+ for (const part of parts) if (reduceDelegationToolPart(map, part, now)) changed++;
1161
+ return changed;
1162
+ }
1163
+ /** Convert a live entry into the shared display shape. Alias falls back to
1164
+ * a `live-<callID>` prefix while the delegate tool has not completed yet. */
1165
+ function toDelegationEntry(live) {
1166
+ return {
1167
+ alias: live.alias ?? `live-${live.callID.slice(0, 8)}`,
1168
+ sessionID: live.sessionID,
1169
+ ...live.taskID !== null ? { taskID: live.taskID } : {},
1170
+ agent: live.agent,
1171
+ state: live.state,
1172
+ startedAt: live.startedAt,
1173
+ updatedAt: live.updatedAt,
1174
+ timedOut: false,
1175
+ description: live.description,
1176
+ read: live.read,
1177
+ source: "live"
1178
+ };
1179
+ }
1180
+ /** Combine the live channel with the md (historical) channel into one
1181
+ * display list. Dedupes by (sessionID, alias) — aliases are per-parent-
1182
+ * session, so the same alias in different sessions stays separate. A
1183
+ * terminal md entry is authoritative over a live running entry for the
1184
+ * same job (it carries Finalized/timedOut/cancelled from finalize). */
1185
+ function mergeDelegationSources(live, md) {
1186
+ const keyOf = (sessionID, alias) => `${sessionID}\u0000${alias}`;
1187
+ const byKey = /* @__PURE__ */ new Map();
1188
+ for (const m of md) {
1189
+ const key = keyOf(m.sessionID, m.alias);
1190
+ const existing = byKey.get(key);
1191
+ if (existing === void 0 || existing.state === "running" && m.state !== "running") byKey.set(key, m);
1192
+ }
1193
+ const aliasless = [];
1194
+ for (const l of live) {
1195
+ const e = toDelegationEntry(l);
1196
+ if (l.alias === null) {
1197
+ aliasless.push(e);
1198
+ continue;
1199
+ }
1200
+ const key = keyOf(l.sessionID, l.alias);
1201
+ const mdEntry = byKey.get(key);
1202
+ if (mdEntry !== void 0 && mdEntry.state !== "running") continue;
1203
+ byKey.set(key, e);
1204
+ }
1205
+ const all = [...aliasless, ...byKey.values()];
1206
+ all.sort(compareDelegationEntries);
1207
+ return all;
1208
+ }
1209
+ /** Server-aligned session id validity: opencode rejects anything not starting
1210
+ * with "ses" (SchemaError). This deliberately mirrors that exact contract —
1211
+ * nothing stricter, nothing looser — so a template placeholder ("{sessionID}"),
1212
+ * its URL-encoded form ("%7BsessionID%7D", what the server reported in the
1213
+ * schema error), an empty/undefined value, or a foreign id (e.g. "wrk_") can
1214
+ * never reach a path and error-spam the log. Confirmed: "{sessionID}" starts
1215
+ * with "{" and "%7BsessionID%7D" with "%" — both fail startsWith("ses"), so
1216
+ * the placeholder is rejected WITHOUT an explicit denylist (covered by tests). */
1217
+ function isValidSessionId(id) {
1218
+ return typeof id === "string" && id.startsWith("ses");
1219
+ }
1220
+ /** Sources the sidebar can resolve the CURRENT session id from. Duck-typed
1221
+ * subsets of TuiPluginApi / TuiState / TuiRouteCurrent so the helper stays
1222
+ * pure and testable without the TUI runtime. */
1223
+ /** Resolve the current session id for the sidebar. Order: slot prop →
1224
+ * api.state.sessionID (runtime superset) → api.route.current.params.sessionID
1225
+ * (typed route). Every source is validated; invalid/absent → next source.
1226
+ * NEVER returns a placeholder or non-ses id. Null → callers MUST skip the
1227
+ * fetch (empty panel, zero errors). Pure — no I/O, no runtime required. */
1228
+ function resolveCurrentSessionID(sources) {
1229
+ const candidates = [
1230
+ sources?.sessionID,
1231
+ sources?.api?.state?.sessionID,
1232
+ sources?.api?.route?.current?.params?.sessionID
1233
+ ];
1234
+ for (const candidate of candidates) if (isValidSessionId(candidate)) return candidate;
1235
+ return null;
1236
+ }
1237
+ /** THE single choke point for every `session.children` / session-API path.
1238
+ * Returns `{ path: { id } }` ONLY for a server-valid session id; returns
1239
+ * null for anything else (placeholder, empty, foreign id) so the caller
1240
+ * skips the call entirely instead of sending an unsubstituted placeholder
1241
+ * (the "%7BsessionID%7D" regression). Every session-API call site MUST go
1242
+ * through this function (enforced by the source-scan test in
1243
+ * tests/pantheon/tui-delegations.test.ts). */
1244
+ function safeSessionPath(id) {
1245
+ if (!isValidSessionId(id)) return null;
1246
+ return { path: { id } };
1247
+ }
1248
+ /** Build the `session.children` path ONLY from a validated session id.
1249
+ * Delegates to {@link safeSessionPath} — the single choke point. Returns
1250
+ * null for null/invalid ids so the caller skips the fetch instead of
1251
+ * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression). */
1252
+ function buildChildrenPath(id) {
1253
+ return safeSessionPath(id);
1254
+ }
1255
+ /** Duck-typed subset of a child Session (+ its live status type). */
1256
+ /** Map a child status type to a display state. busy/retry → running
1257
+ * (the child is actively working), idle → completed, unknown → running
1258
+ * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
1259
+ * correct it as soon as terminal data exists). */
1260
+ function childStatusToState(status) {
1261
+ if (status === "idle") return "completed";
1262
+ return "running";
1263
+ }
1264
+ /** Row tag for a delegation entry: `[task]` for native task() children
1265
+ * (source 'children-only' — no board report), `[<alias>]` for board rows
1266
+ * ([apo-1]). The panel renders the tag with a distinct style so native
1267
+ * task() children are visually separable from pantheon_delegate jobs. */
1268
+ function delegationTag(entry) {
1269
+ return entry.source === "children-only" ? "[task]" : `[${entry.alias}]`;
1270
+ }
1271
+ /** Turn child sessions (PRIMARY) enriched with md reports into the display
1272
+ * list. One entry per child id (duplicates across re-fetches collapse).
1273
+ * The md report is matched by `Task ID` (== child.id) and supplies alias,
1274
+ * agent, description, terminal state and duration. A child without a
1275
+ * report still renders: description from its title, agent from the child
1276
+ * itself (fallback 'agent'), state derived from its status, startedAt from
1277
+ * time.created. A report-less child is a NATIVE task() child (every
1278
+ * child of the current session — pantheon_delegate OR the native `task()`
1279
+ * tool — carries parentID = caller), so it gets source 'children-only'
1280
+ * and the `[task]` tag instead of a board alias.
1281
+ * Terminal md state wins over the derived state; a running md defers to
1282
+ * the child's live status. Sorted running-first (compareDelegationEntries).
1283
+ * Pure — no I/O. */
1284
+ function childrenToDelegationEntries(children, md, now = Date.now()) {
1285
+ const byTaskID = /* @__PURE__ */ new Map();
1286
+ for (const m of md) if (m.taskID !== void 0 && !byTaskID.has(m.taskID)) byTaskID.set(m.taskID, m);
1287
+ const out = [];
1288
+ const seen = /* @__PURE__ */ new Set();
1289
+ for (const child of children ?? []) {
1290
+ if (child.id === "" || seen.has(child.id)) continue;
1291
+ seen.add(child.id);
1292
+ const mdEntry = byTaskID.get(child.id);
1293
+ const state = mdEntry !== void 0 && mdEntry.state !== "running" ? mdEntry.state : childStatusToState(child.status);
1294
+ out.push({
1295
+ alias: mdEntry?.alias ?? "task",
1296
+ sessionID: mdEntry?.sessionID ?? "",
1297
+ taskID: child.id,
1298
+ agent: mdEntry?.agent ?? child.agent ?? "agent",
1299
+ state,
1300
+ startedAt: mdEntry?.startedAt ?? child.time?.created ?? now,
1301
+ updatedAt: mdEntry?.updatedAt ?? (state === "running" ? null : child.time?.updated ?? null),
1302
+ timedOut: mdEntry?.timedOut ?? false,
1303
+ description: mdEntry !== void 0 && mdEntry.description !== "" ? mdEntry.description : child.title ?? "",
1304
+ source: mdEntry !== void 0 ? "md" : "children-only"
1305
+ });
1306
+ }
1307
+ out.sort(compareDelegationEntries);
1308
+ return out;
1309
+ }
1310
+ /** Navigate the TUI to a child session (click/Enter on a delegation row).
1311
+ * Returns false when the route API is unavailable or the target id is
1312
+ * missing/placeholder — the row stays inert instead of crashing. Only a
1313
+ * server-valid session id ("ses...") ever reaches the router, so an
1314
+ * unsubstituted "{sessionID}" placeholder can never be routed. */
1315
+ function navigateToDelegationSession(route, taskID) {
1316
+ if (typeof route?.navigate !== "function" || !isValidSessionId(taskID)) return false;
1317
+ route.navigate("session", { sessionID: taskID });
1318
+ return true;
1319
+ }
1320
+ /** Silence-by-default panel logger.
1321
+ * @param projectRoot the PROJECT ROOT — the logger appends to
1322
+ * `<projectRoot>/.pantheon/logs/hooks.log` (tuiLogPath). Passing anything
1323
+ * deeper (e.g. the delegations dir) used to double-nest the path into
1324
+ * `<root>/.pantheon/.pantheon/logs/hooks.log` and never reach the real log;
1325
+ * derive the root with {@link panelLogDir}. */
1326
+ function createTuiLogger(projectRoot, module = "pantheon-tui") {
1327
+ const echo = (process.env.PANTHEON_HOOKS_LOG ?? "").trim() !== "";
1328
+ const logPath = tuiLogPath(projectRoot ?? process.cwd());
1329
+ const formatArg = (arg) => {
1330
+ if (arg instanceof Error) return arg.stack ?? arg.message;
1331
+ if (typeof arg === "string") return arg;
1332
+ try {
1333
+ return JSON.stringify(arg);
1334
+ } catch {
1335
+ return String(arg);
1336
+ }
1337
+ };
1338
+ const write = (message, args) => {
1339
+ const lines = [message, ...args.map(formatArg)].join(" ").split("\n").map((p) => p.trim()).filter((p) => p !== "");
1340
+ if (lines.length === 0) return;
1341
+ (async () => {
1342
+ try {
1343
+ await mkdir(dirname(logPath), { recursive: true });
1344
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
1345
+ await appendFile(logPath, `${lines.map((l) => `[${stamp}] [${module}] ${l}`).join("\n")}\n`, "utf8");
1346
+ } catch {}
1347
+ })();
1348
+ if (echo) for (const line of lines) console.log(`[${module}] ${line}`);
1349
+ };
1350
+ return { info: (message, ...args) => write(message, args) };
1351
+ }
798
1352
  function SessionRow(props) {
799
1353
  const theme = () => props.api.theme.current;
800
1354
  const status = createMemo(() => {
1355
+ if (!isValidSessionId(props.session?.id)) return null;
801
1356
  try {
802
1357
  const s = props.api.state.session?.status?.(props.session.id);
803
1358
  if (s?.type) return s.type;
@@ -818,11 +1373,69 @@ function SessionRow(props) {
818
1373
  return _el$12;
819
1374
  })();
820
1375
  }
1376
+ function DelegationRow(props) {
1377
+ const theme = () => props.api.theme.current;
1378
+ const color = createMemo(() => {
1379
+ const t = theme();
1380
+ switch (props.job.state) {
1381
+ case "running": return t.warning;
1382
+ case "completed": return t.success;
1383
+ case "error": return t.error;
1384
+ default: return t.textMuted;
1385
+ }
1386
+ });
1387
+ const marker = createMemo(() => {
1388
+ if (props.job.state === "running") return `${delegationSpinnerFrame(props.animationNow)} `;
1389
+ switch (props.job.state) {
1390
+ case "completed": return "✓ ";
1391
+ case "error": return "✕ ";
1392
+ default: return "○ ";
1393
+ }
1394
+ });
1395
+ const tagColor = createMemo(() => props.job.source === "children-only" ? theme().info : color());
1396
+ const detail = createMemo(() => {
1397
+ const line = `${delegationElapsed(props.job, props.now)}${props.job.description !== "" ? ` \u2014 ${props.job.description}` : ""}`;
1398
+ return line.length > 180 ? `${line.slice(0, 177)}\u2026` : line;
1399
+ });
1400
+ const open = () => {
1401
+ navigateToDelegationSession(props.api.route, props.job.taskID);
1402
+ };
1403
+ return (() => {
1404
+ 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");
1405
+ insertNode(_el$13, _el$14);
1406
+ insertNode(_el$13, _el$18);
1407
+ setProp(_el$13, "onMouseDown", open);
1408
+ insertNode(_el$14, _el$15);
1409
+ insertNode(_el$14, _el$16);
1410
+ insertNode(_el$14, _el$17);
1411
+ setProp(_el$14, "flexDirection", "row");
1412
+ insert(_el$15, marker);
1413
+ insert(_el$16, () => delegationTag(props.job));
1414
+ insert(_el$17, () => ` ${props.job.agent} \u2014 ${delegationActivityLabel(props.job)}`);
1415
+ insert(_el$18, detail);
1416
+ effect((_p$) => {
1417
+ var _v$5 = color(), _v$6 = tagColor(), _v$7 = color(), _v$8 = theme().textMuted;
1418
+ _v$5 !== _p$.e && (_p$.e = setProp(_el$15, "fg", _v$5, _p$.e));
1419
+ _v$6 !== _p$.t && (_p$.t = setProp(_el$16, "fg", _v$6, _p$.t));
1420
+ _v$7 !== _p$.a && (_p$.a = setProp(_el$17, "fg", _v$7, _p$.a));
1421
+ _v$8 !== _p$.o && (_p$.o = setProp(_el$18, "fg", _v$8, _p$.o));
1422
+ return _p$;
1423
+ }, {
1424
+ e: void 0,
1425
+ t: void 0,
1426
+ a: void 0,
1427
+ o: void 0
1428
+ });
1429
+ return _el$13;
1430
+ })();
1431
+ }
1432
+ /** Plugin-level live delegation store shared with the event subscriptions
1433
+ * in `tui()`: the map of live entries + a version signal bumped on every
1434
+ * mutation. The View subscribes to the version (in an effect) to refresh the
1435
+ * durable child list and also reads the map as an optimistic live source. */
821
1436
  function View(props) {
822
- const [showCommands, setShowCommands] = createSignal(false);
823
- const [showAgents, setShowAgents] = createSignal(false);
824
- const [showConfig, setShowConfig] = createSignal(false);
825
1437
  const [showSessions, setShowSessions] = createSignal(false);
1438
+ const [showDelegations, setShowDelegations] = createSignal(true);
826
1439
  const theme = () => props.api.theme.current;
827
1440
  const branch = createMemo(() => props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null);
828
1441
  const [preset, setPreset] = createSignal(presetFromEnv(process.env));
@@ -861,12 +1474,72 @@ function View(props) {
861
1474
  if (!result) return [];
862
1475
  const data = result.data ?? result;
863
1476
  if (!Array.isArray(data)) return [];
864
- return data.filter((s) => !s.parentID && s.id !== props.sessionID).sort((a, b) => {
1477
+ return data.filter((s) => !s.parentID && s.id !== resolveCurrentSessionID({
1478
+ sessionID: props.sessionID,
1479
+ api: props.api
1480
+ })).sort((a, b) => {
865
1481
  const ta = a.time?.updated ?? a.updated ?? 0;
866
1482
  return (b.time?.updated ?? b.updated ?? 0) - ta;
867
1483
  }).slice(0, 8);
868
1484
  });
869
- const [memoryCount, setMemoryCount] = createSignal(null);
1485
+ const delegationsDir = createMemo(() => resolveDelegationsDir(props.api.state.path));
1486
+ const panelLog = createTuiLogger(panelLogDir(delegationsDir()));
1487
+ const [childDelegations, setChildDelegations] = createSignal([]);
1488
+ const [now, setNow] = createSignal(Date.now());
1489
+ const [animationNow, setAnimationNow] = createSignal(Date.now());
1490
+ let eventRefreshCount = 0;
1491
+ let delegationsInflight = null;
1492
+ const refreshDelegations = () => {
1493
+ if (delegationsInflight !== null) return delegationsInflight;
1494
+ delegationsInflight = (async () => {
1495
+ try {
1496
+ const state = props.api.state;
1497
+ let md = [];
1498
+ try {
1499
+ md = await readAllDelegationEntries(panelLogDir(delegationsDir()));
1500
+ } catch {
1501
+ md = [];
1502
+ }
1503
+ const sessionID = resolveCurrentSessionID({
1504
+ sessionID: props.sessionID,
1505
+ api: props.api
1506
+ });
1507
+ if (sessionID === null) {
1508
+ setChildDelegations(md);
1509
+ panelLog.info(`panel: children=0 md=${md.length} events=${eventRefreshCount} (no sessionID — history shown)`);
1510
+ return;
1511
+ }
1512
+ let children = [];
1513
+ try {
1514
+ const result = await props.api.client?.session?.children?.(buildChildrenPath(sessionID));
1515
+ const data = result?.data ?? result;
1516
+ children = Array.isArray(data) ? data : [];
1517
+ } catch (err) {
1518
+ children = [];
1519
+ panelLog.info("panel: error children fetch", err);
1520
+ }
1521
+ const resolveStatus = (childID) => {
1522
+ if (!isValidSessionId(childID)) return void 0;
1523
+ try {
1524
+ return state?.session?.status?.(childID)?.type;
1525
+ } catch {
1526
+ return;
1527
+ }
1528
+ };
1529
+ const childEntries = childrenToDelegationEntries(children.map((c) => ({
1530
+ ...c,
1531
+ status: resolveStatus(c.id)
1532
+ })), md, now());
1533
+ const liveEntries = [...props.liveStore.map.values()].filter((entry) => entry.sessionID === sessionID);
1534
+ setChildDelegations(mergeChildDelegationSources(childEntries, liveEntries));
1535
+ panelLog.info(`panel: children=${children.length} md=${md.length} events=${eventRefreshCount}`);
1536
+ } finally {
1537
+ delegationsInflight = null;
1538
+ }
1539
+ })();
1540
+ return delegationsInflight;
1541
+ };
1542
+ const visibleDelegations = createMemo(() => visibleDelegationList(childDelegations()));
870
1543
  onMount(() => {
871
1544
  const cleanup = [];
872
1545
  try {
@@ -876,101 +1549,117 @@ function View(props) {
876
1549
  cleanup.push(props.api.event.on("session.deleted", refetchSessions));
877
1550
  } catch {}
878
1551
  try {
879
- const ev = props.api.event;
880
- if (typeof ev?.on === "function") {
881
- cleanup.push(ev.on("memory.created", () => setMemoryCount((c) => c !== null ? c + 1 : 1)));
882
- cleanup.push(ev.on("memory.deleted", () => setMemoryCount((c) => c !== null ? Math.max(0, c - 1) : 0)));
1552
+ const eventRefresh = () => {
1553
+ eventRefreshCount += 1;
1554
+ refreshDelegations();
1555
+ };
1556
+ cleanup.push(props.api.event.on("session.created", eventRefresh));
1557
+ cleanup.push(props.api.event.on("session.updated", eventRefresh));
1558
+ cleanup.push(props.api.event.on("session.deleted", eventRefresh));
1559
+ cleanup.push(props.api.event.on("session.status", eventRefresh));
1560
+ } catch {}
1561
+ createEffect(() => {
1562
+ props.liveStore.version();
1563
+ eventRefreshCount += 1;
1564
+ refreshDelegations();
1565
+ });
1566
+ const poll = setInterval(() => {
1567
+ setNow(Date.now());
1568
+ refreshDelegations();
1569
+ }, 1e3);
1570
+ cleanup.push(() => clearInterval(poll));
1571
+ const animation = setInterval(() => setAnimationNow(Date.now()), 140);
1572
+ cleanup.push(() => clearInterval(animation));
1573
+ try {
1574
+ const sdk = props.api.state?.session;
1575
+ const mountSessionID = resolveCurrentSessionID({
1576
+ sessionID: props.sessionID,
1577
+ api: props.api
1578
+ });
1579
+ if (mountSessionID !== null && typeof sdk?.messages === "function") {
1580
+ const messages = sdk.messages(mountSessionID) ?? [];
1581
+ const state = props.api.state;
1582
+ const parts = collectDelegationToolParts(messages, typeof state?.part === "function" ? (messageID) => state.part(messageID) : void 0);
1583
+ if (parts.length > 0 && seedLiveDelegationMap(props.liveStore.map, parts) > 0) props.liveStore.bump();
883
1584
  }
884
1585
  } catch {}
885
1586
  onCleanup(() => cleanup.forEach((fn) => {
886
1587
  fn();
887
1588
  }));
888
1589
  });
889
- const configSummary = createMemo(() => {
890
- const cfg = props.api.state.config;
891
- if (!cfg) return null;
892
- return {
893
- mcpCount: cfg.mcp ? Object.keys(cfg.mcp).length : 0,
894
- pluginCount: Array.isArray(cfg.plugin) ? cfg.plugin.length : 0,
895
- autoCompaction: cfg.compaction?.auto === true
896
- };
897
- });
898
1590
  const HR = () => (() => {
899
- var _el$13 = createElement("text");
900
- insertNode(_el$13, createTextNode(`────────────────────────────`));
901
- effect((_$p) => setProp(_el$13, "fg", theme().textMuted, _$p));
902
- return _el$13;
1591
+ var _el$19 = createElement("text");
1592
+ insertNode(_el$19, createTextNode(`────────────────────────────`));
1593
+ effect((_$p) => setProp(_el$19, "fg", theme().textMuted, _$p));
1594
+ return _el$19;
903
1595
  })();
904
1596
  return (() => {
905
- var _el$15 = createElement("box"), _el$16 = createElement("text"), _el$17 = createElement("box"), _el$18 = createElement("text"), _el$19 = createElement("text"), _el$21 = createElement("box"), _el$22 = createElement("text"), _el$23 = createElement("text"), _el$25 = createElement("box"), _el$26 = createElement("text"), _el$27 = createElement("text"), _el$29 = createElement("box"), _el$30 = createElement("text"), _el$31 = createElement("box"), _el$32 = createElement("text");
906
- insertNode(_el$15, _el$16);
907
- insertNode(_el$15, _el$17);
908
- insertNode(_el$15, _el$21);
909
- insertNode(_el$15, _el$25);
910
- insertNode(_el$15, _el$29);
911
- insertNode(_el$15, _el$31);
912
- setProp(_el$15, "flexDirection", "column");
913
- setProp(_el$15, "width", "100%");
914
- setProp(_el$16, "attributes", 1);
915
- insert(_el$16, () => `Pantheon${props.version ? ` v${props.version}` : ""}`);
916
- insert(_el$15, createComponent(Show, {
1597
+ var _el$21 = createElement("box"), _el$22 = createElement("text"), _el$23 = createElement("box"), _el$24 = createElement("text"), _el$25 = createElement("text"), _el$27 = createElement("box"), _el$28 = createElement("text"), _el$29 = createElement("text");
1598
+ insertNode(_el$21, _el$22);
1599
+ insertNode(_el$21, _el$23);
1600
+ insertNode(_el$21, _el$27);
1601
+ setProp(_el$21, "flexDirection", "column");
1602
+ setProp(_el$21, "width", "100%");
1603
+ setProp(_el$22, "attributes", 1);
1604
+ insert(_el$22, () => `Pantheon${props.version ? ` v${props.version}` : ""}`);
1605
+ insert(_el$21, createComponent(Show, {
917
1606
  get when() {
918
1607
  return branch();
919
1608
  },
920
1609
  children: (b) => (() => {
921
- var _el$33 = createElement("text");
922
- insert(_el$33, b);
923
- effect((_$p) => setProp(_el$33, "fg", theme().textMuted, _$p));
924
- return _el$33;
1610
+ var _el$31 = createElement("text");
1611
+ insert(_el$31, b);
1612
+ effect((_$p) => setProp(_el$31, "fg", theme().textMuted, _$p));
1613
+ return _el$31;
925
1614
  })()
926
- }), _el$17);
927
- insert(_el$15, createComponent(Show, {
1615
+ }), _el$23);
1616
+ insert(_el$21, createComponent(Show, {
928
1617
  get when() {
929
1618
  return preset().name;
930
1619
  },
931
1620
  get fallback() {
932
1621
  return (() => {
933
- var _el$34 = createElement("box"), _el$35 = createElement("text");
934
- insertNode(_el$34, _el$35);
935
- setProp(_el$34, "flexDirection", "row");
936
- setProp(_el$34, "gap", 1);
937
- insertNode(_el$35, createTextNode(`Preset: default`));
938
- effect((_$p) => setProp(_el$35, "fg", theme().textMuted, _$p));
939
- return _el$34;
1622
+ var _el$32 = createElement("box"), _el$33 = createElement("text");
1623
+ insertNode(_el$32, _el$33);
1624
+ setProp(_el$32, "flexDirection", "row");
1625
+ setProp(_el$32, "gap", 1);
1626
+ insertNode(_el$33, createTextNode(`Preset: default`));
1627
+ effect((_$p) => setProp(_el$33, "fg", theme().textMuted, _$p));
1628
+ return _el$32;
940
1629
  })();
941
1630
  },
942
1631
  children: (name) => (() => {
943
- var _el$37 = createElement("box"), _el$38 = createElement("text"), _el$40 = createElement("text"), _el$41 = createElement("text");
944
- insertNode(_el$37, _el$38);
945
- insertNode(_el$37, _el$40);
946
- insertNode(_el$37, _el$41);
947
- setProp(_el$37, "flexDirection", "row");
948
- setProp(_el$37, "gap", 1);
949
- insertNode(_el$38, createTextNode(`⚡ Preset:`));
950
- insert(_el$40, name);
951
- insert(_el$41, () => `(${preset().source ?? ""})`);
1632
+ var _el$35 = createElement("box"), _el$36 = createElement("text"), _el$38 = createElement("text"), _el$39 = createElement("text");
1633
+ insertNode(_el$35, _el$36);
1634
+ insertNode(_el$35, _el$38);
1635
+ insertNode(_el$35, _el$39);
1636
+ setProp(_el$35, "flexDirection", "row");
1637
+ setProp(_el$35, "gap", 1);
1638
+ insertNode(_el$36, createTextNode(`⚡ Preset:`));
1639
+ insert(_el$38, name);
1640
+ insert(_el$39, () => `(${preset().source ?? ""})`);
952
1641
  effect((_p$) => {
953
1642
  var _v$12 = theme().textMuted, _v$13 = theme().accent, _v$14 = theme().textMuted;
954
- _v$12 !== _p$.e && (_p$.e = setProp(_el$38, "fg", _v$12, _p$.e));
955
- _v$13 !== _p$.t && (_p$.t = setProp(_el$40, "fg", _v$13, _p$.t));
956
- _v$14 !== _p$.a && (_p$.a = setProp(_el$41, "fg", _v$14, _p$.a));
1643
+ _v$12 !== _p$.e && (_p$.e = setProp(_el$36, "fg", _v$12, _p$.e));
1644
+ _v$13 !== _p$.t && (_p$.t = setProp(_el$38, "fg", _v$13, _p$.t));
1645
+ _v$14 !== _p$.a && (_p$.a = setProp(_el$39, "fg", _v$14, _p$.a));
957
1646
  return _p$;
958
1647
  }, {
959
1648
  e: void 0,
960
1649
  t: void 0,
961
1650
  a: void 0
962
1651
  });
963
- return _el$37;
1652
+ return _el$35;
964
1653
  })()
965
- }), _el$17);
966
- insert(_el$15, createComponent(HR, {}), _el$17);
967
- insertNode(_el$17, _el$18);
968
- insertNode(_el$17, _el$19);
969
- setProp(_el$17, "onMouseDown", () => setShowSessions((x) => !x));
970
- setProp(_el$18, "attributes", 1);
971
- insert(_el$18, () => `${showSessions() ? "▼" : "▶"} Sessions`);
972
- insert(_el$19, () => ` (${String(totalSessions())})`);
973
- insert(_el$15, createComponent(Show, {
1654
+ }), _el$23);
1655
+ insert(_el$21, createComponent(HR, {}), _el$23);
1656
+ insertNode(_el$23, _el$24);
1657
+ insertNode(_el$23, _el$25);
1658
+ setProp(_el$23, "onMouseDown", () => setShowSessions((x) => !x));
1659
+ setProp(_el$24, "attributes", 1);
1660
+ insert(_el$24, () => `${showSessions() ? "▼" : "▶"} Sessions`);
1661
+ insert(_el$25, () => ` (${String(totalSessions())})`);
1662
+ insert(_el$21, createComponent(Show, {
974
1663
  get when() {
975
1664
  return showSessions();
976
1665
  },
@@ -981,19 +1670,19 @@ function View(props) {
981
1670
  },
982
1671
  get fallback() {
983
1672
  return (() => {
984
- var _el$42 = createElement("box"), _el$43 = createElement("text");
985
- insertNode(_el$42, _el$43);
986
- setProp(_el$42, "marginLeft", 1);
987
- insertNode(_el$43, createTextNode(`No recent sessions`));
988
- effect((_$p) => setProp(_el$43, "fg", theme().textMuted, _$p));
989
- return _el$42;
1673
+ var _el$40 = createElement("box"), _el$41 = createElement("text");
1674
+ insertNode(_el$40, _el$41);
1675
+ setProp(_el$40, "marginLeft", 1);
1676
+ insertNode(_el$41, createTextNode(`No recent sessions`));
1677
+ effect((_$p) => setProp(_el$41, "fg", theme().textMuted, _$p));
1678
+ return _el$40;
990
1679
  })();
991
1680
  },
992
1681
  get children() {
993
- var _el$20 = createElement("box");
994
- setProp(_el$20, "marginLeft", 1);
995
- setProp(_el$20, "flexDirection", "column");
996
- insert(_el$20, createComponent(For, {
1682
+ var _el$26 = createElement("box");
1683
+ setProp(_el$26, "marginLeft", 1);
1684
+ setProp(_el$26, "flexDirection", "column");
1685
+ insert(_el$26, createComponent(For, {
997
1686
  get each() {
998
1687
  return recentSessions();
999
1688
  },
@@ -1004,177 +1693,112 @@ function View(props) {
1004
1693
  session: ses
1005
1694
  })
1006
1695
  }));
1007
- return _el$20;
1696
+ return _el$26;
1008
1697
  }
1009
1698
  });
1010
1699
  }
1011
- }), _el$21);
1012
- insertNode(_el$21, _el$22);
1013
- insertNode(_el$21, _el$23);
1014
- setProp(_el$21, "onMouseDown", () => setShowCommands((x) => !x));
1015
- setProp(_el$22, "attributes", 1);
1016
- insert(_el$22, () => `${showCommands() ? "▼" : "▶"} Commands`);
1017
- insert(_el$23, () => ` (${String(COMMANDS.length)})`);
1018
- insert(_el$15, createComponent(Show, {
1700
+ }), _el$27);
1701
+ insertNode(_el$27, _el$28);
1702
+ insertNode(_el$27, _el$29);
1703
+ setProp(_el$27, "onMouseDown", () => setShowDelegations((x) => !x));
1704
+ setProp(_el$28, "attributes", 1);
1705
+ insert(_el$28, () => `${showDelegations() ? "▼" : "▶"} Delegations`);
1706
+ insert(_el$29, () => ` (${String(visibleDelegations().length)})`);
1707
+ insert(_el$21, createComponent(Show, {
1019
1708
  get when() {
1020
- return showCommands();
1021
- },
1022
- get children() {
1023
- var _el$24 = createElement("box");
1024
- setProp(_el$24, "marginLeft", 1);
1025
- setProp(_el$24, "flexDirection", "column");
1026
- insert(_el$24, createComponent(For, {
1027
- each: COMMANDS,
1028
- children: (cmd) => (() => {
1029
- var _el$45 = createElement("box"), _el$46 = createElement("text"), _el$47 = createElement("text");
1030
- insertNode(_el$45, _el$46);
1031
- insertNode(_el$45, _el$47);
1032
- setProp(_el$45, "onMouseDown", (e) => {
1033
- e.stopPropagation();
1034
- try {
1035
- const cmdApi = props.api.command;
1036
- const cmdName = cmd.name.replace("/", "");
1037
- if (cmdApi?.trigger?.(cmdName)) return;
1038
- } catch {}
1039
- props.api.ui?.toast?.({
1040
- title: "Command",
1041
- message: `Type ${cmd.name} in chat`
1042
- });
1043
- });
1044
- insert(_el$46, () => cmd.name);
1045
- insert(_el$47, () => ` \u2014 ${cmd.desc}`);
1046
- effect((_p$) => {
1047
- var _v$15 = cmd.name === "/pantheon" ? theme().accent : theme().textMuted, _v$16 = theme().textMuted;
1048
- _v$15 !== _p$.e && (_p$.e = setProp(_el$46, "fg", _v$15, _p$.e));
1049
- _v$16 !== _p$.t && (_p$.t = setProp(_el$47, "fg", _v$16, _p$.t));
1050
- return _p$;
1051
- }, {
1052
- e: void 0,
1053
- t: void 0
1054
- });
1055
- return _el$45;
1056
- })()
1057
- }));
1058
- return _el$24;
1059
- }
1060
- }), _el$25);
1061
- insertNode(_el$25, _el$26);
1062
- insertNode(_el$25, _el$27);
1063
- setProp(_el$25, "onMouseDown", () => setShowAgents((x) => !x));
1064
- setProp(_el$26, "attributes", 1);
1065
- insert(_el$26, () => `${showAgents() ? "▼" : "▶"} Agents`);
1066
- insert(_el$27, () => ` (${String(AGENTS.length)})`);
1067
- insert(_el$15, createComponent(Show, {
1068
- get when() {
1069
- return showAgents();
1070
- },
1071
- get children() {
1072
- var _el$28 = createElement("box");
1073
- setProp(_el$28, "marginLeft", 1);
1074
- setProp(_el$28, "flexDirection", "column");
1075
- insert(_el$28, createComponent(For, {
1076
- each: AGENTS,
1077
- children: (agent) => (() => {
1078
- var _el$48 = createElement("box"), _el$49 = createElement("text"), _el$50 = createElement("text");
1079
- insertNode(_el$48, _el$49);
1080
- insertNode(_el$48, _el$50);
1081
- insert(_el$49, () => `${agent.tier === "premium" ? "✦ " : "· "}${agent.name}`);
1082
- insert(_el$50, () => ` \u2014 ${agent.role}`);
1083
- effect((_p$) => {
1084
- var _v$17 = agent.tier === "premium" ? theme().accent : theme().textMuted, _v$18 = theme().textMuted;
1085
- _v$17 !== _p$.e && (_p$.e = setProp(_el$49, "fg", _v$17, _p$.e));
1086
- _v$18 !== _p$.t && (_p$.t = setProp(_el$50, "fg", _v$18, _p$.t));
1087
- return _p$;
1088
- }, {
1089
- e: void 0,
1090
- t: void 0
1091
- });
1092
- return _el$48;
1093
- })()
1094
- }));
1095
- return _el$28;
1096
- }
1097
- }), _el$29);
1098
- insertNode(_el$29, _el$30);
1099
- setProp(_el$29, "onMouseDown", () => setShowConfig((x) => !x));
1100
- setProp(_el$30, "attributes", 1);
1101
- insert(_el$30, () => `${showConfig() ? "▼" : "▶"} Config`);
1102
- insert(_el$15, createComponent(Show, {
1103
- get when() {
1104
- return showConfig();
1709
+ return showDelegations();
1105
1710
  },
1106
1711
  get children() {
1107
1712
  return createComponent(Show, {
1108
1713
  get when() {
1109
- return configSummary();
1714
+ return visibleDelegations().length > 0;
1110
1715
  },
1111
1716
  get fallback() {
1112
1717
  return (() => {
1113
- var _el$51 = createElement("box"), _el$52 = createElement("text");
1114
- insertNode(_el$51, _el$52);
1115
- setProp(_el$51, "marginLeft", 1);
1116
- insertNode(_el$52, createTextNode(`No config data`));
1117
- effect((_$p) => setProp(_el$52, "fg", theme().textMuted, _$p));
1118
- return _el$51;
1718
+ var _el$43 = createElement("box"), _el$44 = createElement("text");
1719
+ insertNode(_el$43, _el$44);
1720
+ setProp(_el$43, "marginLeft", 1);
1721
+ insertNode(_el$44, createTextNode(`No delegations`));
1722
+ effect((_$p) => setProp(_el$44, "fg", theme().textMuted, _$p));
1723
+ return _el$43;
1119
1724
  })();
1120
1725
  },
1121
- children: (cfg) => (() => {
1122
- var _el$54 = createElement("box"), _el$55 = createElement("text"), _el$56 = createElement("text");
1123
- insertNode(_el$54, _el$55);
1124
- insertNode(_el$54, _el$56);
1125
- setProp(_el$54, "marginLeft", 1);
1126
- setProp(_el$54, "flexDirection", "column");
1127
- insert(_el$55, () => `MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`);
1128
- insert(_el$56, () => `Auto-compaction: ${cfg().autoCompaction ? "ON" : "OFF"}`);
1129
- effect((_p$) => {
1130
- var _v$19 = theme().textMuted, _v$20 = theme().textMuted;
1131
- _v$19 !== _p$.e && (_p$.e = setProp(_el$55, "fg", _v$19, _p$.e));
1132
- _v$20 !== _p$.t && (_p$.t = setProp(_el$56, "fg", _v$20, _p$.t));
1133
- return _p$;
1134
- }, {
1135
- e: void 0,
1136
- t: void 0
1137
- });
1138
- return _el$54;
1139
- })()
1726
+ get children() {
1727
+ var _el$30 = createElement("box");
1728
+ setProp(_el$30, "marginLeft", 1);
1729
+ setProp(_el$30, "flexDirection", "column");
1730
+ insert(_el$30, createComponent(For, {
1731
+ get each() {
1732
+ return visibleDelegations();
1733
+ },
1734
+ children: (job) => createComponent(DelegationRow, {
1735
+ get api() {
1736
+ return props.api;
1737
+ },
1738
+ job,
1739
+ get now() {
1740
+ return now();
1741
+ },
1742
+ get animationNow() {
1743
+ return animationNow();
1744
+ }
1745
+ })
1746
+ }));
1747
+ return _el$30;
1748
+ }
1140
1749
  });
1141
1750
  }
1142
- }), _el$31);
1143
- insertNode(_el$31, _el$32);
1144
- setProp(_el$31, "marginTop", 1);
1145
- insert(_el$32, (() => {
1146
- var _c$ = memo(() => memoryCount() !== null);
1147
- return () => _c$() ? `Memory: ${fmtInt(memoryCount())} entries` : "Memory: N/A";
1148
- })());
1751
+ }), null);
1149
1752
  effect((_p$) => {
1150
- var _v$5 = theme().accent, _v$6 = theme().text, _v$7 = theme().textMuted, _v$8 = theme().text, _v$9 = theme().textMuted, _v$0 = theme().text, _v$1 = theme().textMuted, _v$10 = theme().text, _v$11 = theme().textMuted;
1151
- _v$5 !== _p$.e && (_p$.e = setProp(_el$16, "fg", _v$5, _p$.e));
1152
- _v$6 !== _p$.t && (_p$.t = setProp(_el$18, "fg", _v$6, _p$.t));
1153
- _v$7 !== _p$.a && (_p$.a = setProp(_el$19, "fg", _v$7, _p$.a));
1154
- _v$8 !== _p$.o && (_p$.o = setProp(_el$22, "fg", _v$8, _p$.o));
1155
- _v$9 !== _p$.i && (_p$.i = setProp(_el$23, "fg", _v$9, _p$.i));
1156
- _v$0 !== _p$.n && (_p$.n = setProp(_el$26, "fg", _v$0, _p$.n));
1157
- _v$1 !== _p$.s && (_p$.s = setProp(_el$27, "fg", _v$1, _p$.s));
1158
- _v$10 !== _p$.h && (_p$.h = setProp(_el$30, "fg", _v$10, _p$.h));
1159
- _v$11 !== _p$.r && (_p$.r = setProp(_el$32, "fg", _v$11, _p$.r));
1753
+ var _v$9 = theme().accent, _v$0 = theme().text, _v$1 = theme().textMuted, _v$10 = theme().text, _v$11 = theme().textMuted;
1754
+ _v$9 !== _p$.e && (_p$.e = setProp(_el$22, "fg", _v$9, _p$.e));
1755
+ _v$0 !== _p$.t && (_p$.t = setProp(_el$24, "fg", _v$0, _p$.t));
1756
+ _v$1 !== _p$.a && (_p$.a = setProp(_el$25, "fg", _v$1, _p$.a));
1757
+ _v$10 !== _p$.o && (_p$.o = setProp(_el$28, "fg", _v$10, _p$.o));
1758
+ _v$11 !== _p$.i && (_p$.i = setProp(_el$29, "fg", _v$11, _p$.i));
1160
1759
  return _p$;
1161
1760
  }, {
1162
1761
  e: void 0,
1163
1762
  t: void 0,
1164
1763
  a: void 0,
1165
1764
  o: void 0,
1166
- i: void 0,
1167
- n: void 0,
1168
- s: void 0,
1169
- h: void 0,
1170
- r: void 0
1765
+ i: void 0
1171
1766
  });
1172
- return _el$15;
1767
+ return _el$21;
1173
1768
  })();
1174
1769
  }
1175
- const tui = async (api, _options, _meta) => {
1176
- const version = await detectVersion(api);
1770
+ const tui = (api, _options, _meta) => {
1771
+ const [version, setVersion] = createSignal(null);
1772
+ detectVersion(api).then((detected) => setVersion(detected)).catch(() => setVersion(null));
1177
1773
  setupUsageBar(api);
1774
+ const [liveVersion, setLiveVersion] = createSignal(0);
1775
+ const liveStore = {
1776
+ map: /* @__PURE__ */ new Map(),
1777
+ version: liveVersion,
1778
+ bump: () => setLiveVersion((v) => v + 1)
1779
+ };
1780
+ const unsubLive = [];
1781
+ try {
1782
+ unsubLive.push(api.event.on("message.part.updated", (event) => {
1783
+ const props = event?.properties ?? {};
1784
+ const part = props.part ?? props.info?.part;
1785
+ if (part === void 0) return;
1786
+ if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump();
1787
+ }));
1788
+ } catch {}
1789
+ try {
1790
+ unsubLive.push(api.event.on("message.part.removed", (event) => {
1791
+ const props = event?.properties ?? {};
1792
+ const partID = props.partID ?? props.part?.id;
1793
+ if (partID !== void 0 && removeDelegationEntry(liveStore.map, partID)) liveStore.bump();
1794
+ }));
1795
+ } catch {}
1796
+ api.lifecycle.onDispose(() => {
1797
+ for (const unsub of unsubLive) try {
1798
+ unsub();
1799
+ } catch {}
1800
+ liveStore.map.clear();
1801
+ });
1178
1802
  api.slots.register({
1179
1803
  order: 900,
1180
1804
  slots: { sidebar_content(_ctx, props) {
@@ -1183,7 +1807,10 @@ const tui = async (api, _options, _meta) => {
1183
1807
  get sessionID() {
1184
1808
  return props.session_id;
1185
1809
  },
1186
- version
1810
+ get version() {
1811
+ return version();
1812
+ },
1813
+ liveStore
1187
1814
  });
1188
1815
  } }
1189
1816
  });
@@ -1193,6 +1820,6 @@ const plugin = {
1193
1820
  tui
1194
1821
  };
1195
1822
  //#endregion
1196
- export { plugin as default };
1823
+ export { buildChildrenPath, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationSpinnerFrame, delegationTag, fmtElapsed, isValidSessionId, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, safeSessionPath, seedLiveDelegationMap, toDelegationEntry, tuiLogPath, visibleDelegationList };
1197
1824
 
1198
1825
  //# sourceMappingURL=tui.js.map