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.
- package/README.md +154 -1
- package/bin/pantheon-init.mjs +160 -120
- package/opencode.json +0 -1
- package/package.json +10 -5
- package/scripts/cost.mjs +69 -0
- package/scripts/doctor.mjs +56 -22
- package/scripts/hooks/on-subagent-delegation-stop.sh +22 -5
- package/scripts/install/opencode.mjs +132 -32
- package/scripts/install/plugin.mjs +112 -28
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/redaction-gate.mjs +69 -0
- package/scripts/release-notes.mjs +317 -0
- package/scripts/test-context-compression.sh +5 -2
- package/scripts/validate-package.mjs +56 -0
- package/scripts/versioning.mjs +117 -18
- package/src/agents/zeus.md +80 -0
- package/src/pantheon/background-job-board.ts +115 -40
- package/src/pantheon/chat-reminders.ts +79 -0
- package/src/pantheon/compaction-assert.ts +160 -0
- package/src/pantheon/cost-command.ts +208 -0
- package/src/pantheon/cost-tracker.ts +108 -0
- package/src/pantheon/delegation-compaction.ts +237 -0
- package/src/pantheon/delegation-enforce.ts +144 -0
- package/src/pantheon/delegation-finalize.ts +277 -0
- package/src/pantheon/delegation-notify.ts +100 -0
- package/src/pantheon/delegation.ts +690 -0
- package/src/pantheon/dispatch-guard.ts +107 -0
- package/src/pantheon/file-persistence.ts +10 -6
- package/src/pantheon/goal-loop.ts +300 -0
- package/src/pantheon/goal-store.ts +97 -0
- package/src/pantheon/hashline/core.ts +91 -0
- package/src/pantheon/hashline/read-enhancer.ts +66 -0
- package/src/pantheon/hashline/tool.ts +260 -0
- package/src/pantheon/hashline/xxhash.ts +53 -0
- package/src/pantheon/idle-continuation.ts +54 -0
- package/src/pantheon/logger.ts +117 -0
- package/src/pantheon/plugin-once.ts +76 -0
- package/src/pantheon/preemptive-compact.ts +76 -0
- package/src/pantheon/presets.d.mts +31 -0
- package/src/pantheon/presets.mjs +105 -3
- package/src/pantheon/todo-enforcer.ts +421 -0
- package/src/pantheon/todo-preserve.ts +295 -0
- package/src/plugin.ts +406 -13
- package/src/plugins/pantheon-hooks.ts +52 -30
- package/src/plugins/tui/dist/server.d.ts +17 -0
- package/src/plugins/tui/dist/server.d.ts.map +1 -0
- package/src/plugins/tui/dist/server.js +20 -0
- package/src/plugins/tui/dist/server.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +303 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +977 -350
- package/src/plugins/tui/dist/tui.tsx +1427 -150
- package/src/plugins/tui/package.json +4 -3
- package/src/plugins/tui/src/index.tsx +1427 -150
- package/src/plugins/tui/src/server.ts +15 -0
- package/src/plugins/tui/tsdown.config.ts +1 -1
- 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,
|
|
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 !==
|
|
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
|
|
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
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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$
|
|
900
|
-
insertNode(_el$
|
|
901
|
-
effect((_$p) => setProp(_el$
|
|
902
|
-
return _el$
|
|
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$
|
|
906
|
-
insertNode(_el$
|
|
907
|
-
insertNode(_el$
|
|
908
|
-
insertNode(_el$
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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$
|
|
922
|
-
insert(_el$
|
|
923
|
-
effect((_$p) => setProp(_el$
|
|
924
|
-
return _el$
|
|
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$
|
|
927
|
-
insert(_el$
|
|
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$
|
|
934
|
-
insertNode(_el$
|
|
935
|
-
setProp(_el$
|
|
936
|
-
setProp(_el$
|
|
937
|
-
insertNode(_el$
|
|
938
|
-
effect((_$p) => setProp(_el$
|
|
939
|
-
return _el$
|
|
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$
|
|
944
|
-
insertNode(_el$
|
|
945
|
-
insertNode(_el$
|
|
946
|
-
insertNode(_el$
|
|
947
|
-
setProp(_el$
|
|
948
|
-
setProp(_el$
|
|
949
|
-
insertNode(_el$
|
|
950
|
-
insert(_el$
|
|
951
|
-
insert(_el$
|
|
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$
|
|
955
|
-
_v$13 !== _p$.t && (_p$.t = setProp(_el$
|
|
956
|
-
_v$14 !== _p$.a && (_p$.a = setProp(_el$
|
|
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$
|
|
1652
|
+
return _el$35;
|
|
964
1653
|
})()
|
|
965
|
-
}), _el$
|
|
966
|
-
insert(_el$
|
|
967
|
-
insertNode(_el$
|
|
968
|
-
insertNode(_el$
|
|
969
|
-
setProp(_el$
|
|
970
|
-
setProp(_el$
|
|
971
|
-
insert(_el$
|
|
972
|
-
insert(_el$
|
|
973
|
-
insert(_el$
|
|
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$
|
|
985
|
-
insertNode(_el$
|
|
986
|
-
setProp(_el$
|
|
987
|
-
insertNode(_el$
|
|
988
|
-
effect((_$p) => setProp(_el$
|
|
989
|
-
return _el$
|
|
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$
|
|
994
|
-
setProp(_el$
|
|
995
|
-
setProp(_el$
|
|
996
|
-
insert(_el$
|
|
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$
|
|
1696
|
+
return _el$26;
|
|
1008
1697
|
}
|
|
1009
1698
|
});
|
|
1010
1699
|
}
|
|
1011
|
-
}), _el$
|
|
1012
|
-
insertNode(_el$
|
|
1013
|
-
insertNode(_el$
|
|
1014
|
-
setProp(_el$
|
|
1015
|
-
setProp(_el$
|
|
1016
|
-
insert(_el$
|
|
1017
|
-
insert(_el$
|
|
1018
|
-
insert(_el$
|
|
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
|
|
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
|
|
1714
|
+
return visibleDelegations().length > 0;
|
|
1110
1715
|
},
|
|
1111
1716
|
get fallback() {
|
|
1112
1717
|
return (() => {
|
|
1113
|
-
var _el$
|
|
1114
|
-
insertNode(_el$
|
|
1115
|
-
setProp(_el$
|
|
1116
|
-
insertNode(_el$
|
|
1117
|
-
effect((_$p) => setProp(_el$
|
|
1118
|
-
return _el$
|
|
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
|
|
1122
|
-
var _el$
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
}),
|
|
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$
|
|
1151
|
-
_v$
|
|
1152
|
-
_v$
|
|
1153
|
-
_v$
|
|
1154
|
-
_v$
|
|
1155
|
-
_v$
|
|
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$
|
|
1767
|
+
return _el$21;
|
|
1173
1768
|
})();
|
|
1174
1769
|
}
|
|
1175
|
-
const tui =
|
|
1176
|
-
const version =
|
|
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
|