pantheon-opencode 1.5.2 → 1.6.0-beta.10
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/.pantheon/code-mode/README.md +4 -2
- package/.pantheon/code-mode/checkpoint_session.py +138 -102
- package/.pantheon/code-mode/compress-inline.py +102 -57
- package/.pantheon/code-mode/manifest.json +2 -8
- package/AGENTS.global.md +182 -0
- package/AGENTS.md +10 -891
- package/CHANGELOG.md +343 -0
- package/README.md +577 -79
- package/README.pt-BR.md +345 -60
- package/bin/pantheon-init.mjs +26 -3
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-deepwork.md +14 -3
- package/commands/pantheon-focus.md +17 -0
- package/commands/pantheon-optimize.md +2 -2
- package/commands/pantheon-vision.md +61 -12
- package/package.json +30 -11
- package/plugin.json +2 -2
- package/pyproject.toml +11 -16
- package/scripts/build-agents-md.mjs +376 -58
- package/scripts/changelog-merge.mjs +161 -0
- package/scripts/check-plugin-v2-coverage.mjs +213 -0
- package/scripts/ci-validate-yaml.py +7 -5
- package/scripts/{code_mode_server.py → code_mode.py} +119 -27
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +236 -29
- package/scripts/install/agents-md.mjs +4 -4
- package/scripts/install/cli-ui.mjs +1 -1
- package/scripts/install/config-migration.mjs +319 -6
- package/scripts/install/health-check.mjs +26 -37
- package/scripts/install/interactive.mjs +7 -7
- package/scripts/install/migrate.mjs +1 -1
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +627 -179
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/shared.mjs +120 -12
- package/scripts/install/vision-prompt.mjs +161 -0
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +372 -198
- package/scripts/postinstall.mjs +0 -7
- package/scripts/probe-context-rehydrate.mjs +36 -13
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/prune.mjs +2 -2
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +26 -13
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +1913 -0
- package/scripts/validate-routing.mjs +43 -3
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/scripts/versioning.mjs +13 -1
- package/scripts/vision-status.mjs +682 -0
- package/skills-lock.json +15 -16
- package/src/agents/aphrodite.md +20 -37
- package/src/agents/apollo.md +7 -3
- package/src/agents/athena.md +63 -35
- package/src/agents/demeter.md +8 -28
- package/src/agents/gaia.md +1 -1
- package/src/agents/hephaestus.md +10 -21
- package/src/agents/hermes.md +15 -38
- package/src/agents/iris.md +1 -1
- package/src/agents/mnemosyne.md +23 -60
- package/src/agents/nyx.md +1 -1
- package/src/agents/prometheus.md +16 -20
- package/src/agents/talos.md +10 -2
- package/src/agents/themis.md +5 -5
- package/src/agents/zeus.md +99 -130
- package/src/instructions/agent-return-format.instructions.md +18 -3
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +11 -110
- package/src/instructions/zeus-anti-stall.instructions.md +14 -61
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
- package/src/instructions/zeus-memory-operations.instructions.md +26 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
- package/src/mcp/{code_mode_server.py → code_mode.py} +119 -27
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/mcp_codemap_module.py +559 -129
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +373 -250
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +396 -40
- package/src/mcp/requirements-mcp.txt +10 -6
- package/src/mcp/requirements-vision.txt +4 -2
- package/src/pantheon/cost-command.ts +399 -101
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/model-command.ts +18 -7
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +5 -32
- package/src/pantheon/todo-enforcer.ts +1 -1
- package/src/pantheon/v2-bounded-read.ts +640 -0
- package/src/pantheon/v2-delegation-enforce.ts +214 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-tools.ts +429 -0
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +90 -0
- package/src/pantheon/v2-vision.ts +622 -0
- package/src/pantheon/vision-local.ts +531 -0
- package/src/pantheon/vision.ts +1161 -116
- package/src/plugin-v2.ts +1244 -288
- package/src/plugin.ts +12 -9
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +659 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +85 -82
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +72 -59
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5504 -1341
- package/src/plugins/tui/package.json +25 -15
- package/src/plugins/tui/src/index-v2.tsx +734 -0
- package/src/plugins/tui/src/index.tsx +106 -85
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsconfig.typecheck.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +22 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +11 -22
- package/src/skills/README.md +34 -39
- package/src/skills/agent-coordination/SKILL.md +7 -2
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +54 -95
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/skills/spec-driven-development/SKILL.md +3 -1
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- package/.pantheon/code-mode/eval-llm-judge.py +0 -255
- package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
- package/.pantheon/code-mode/eval-run.py +0 -197
- package/.pantheon/code-mode/eval-static.py +0 -321
- package/.pantheon/code-mode/session-end-save.py +0 -186
- package/.pantheon/code-mode/session-end-save.sh +0 -18
- package/opencode.json +0 -58
- package/scripts/recover-zenodo-deposition.mjs +0 -146
- package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
- package/scripts/validate-zenodo-release.mjs +0 -154
- package/scripts/zenodo-artifact.mjs +0 -94
- package/scripts/zenodo-release-state.mjs +0 -53
- package/src/skills/orchestration-workflow/SKILL.md +0 -311
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { createComponent, createElement, createTextNode, effect, insert, insertNode, memo, setProp } from "@opentui/solid";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
|
6
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
7
|
+
//#region src/index-v2.tsx
|
|
8
|
+
/** @jsxImportSource @opentui/solid */
|
|
9
|
+
const SINGLETON = Symbol.for("pantheon.tui.v2.loaded");
|
|
10
|
+
/** How often the sidebar re-reads .pantheon/active-preset.json (V1 parity) so
|
|
11
|
+
* `set-tier` changes made while opencode is open show up within ~30s. */
|
|
12
|
+
const PRESET_REFRESH_MS = 3e4;
|
|
13
|
+
/** Running delegations rendered before the "… +N more" line (V1 parity). */
|
|
14
|
+
const DELEGATIONS_VISIBLE_CEILING = 8;
|
|
15
|
+
/** Running-row spinner cadence — matches V1's animation tick. */
|
|
16
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
17
|
+
/** Bounded warm-up: sync at most this many sessions at plugin load. */
|
|
18
|
+
const WARMUP_SESSION_LIMIT = 24;
|
|
19
|
+
/** Sidebar body shown while the host has not published a session yet. */
|
|
20
|
+
const SIDEBAR_WAITING_LABEL = "Aguardando sessão…";
|
|
21
|
+
/** Delegate-completion toast title. */
|
|
22
|
+
const TOAST_TITLE = "Pantheon";
|
|
23
|
+
/** One event name the host publishes on `data.on` — the OpenCode V2 union. */
|
|
24
|
+
/** V2 events that refresh the sidebar. V1 also listened to `session.updated`,
|
|
25
|
+
* but that name is absent from the OpenCode V2 event union (verified against
|
|
26
|
+
* `@opencode/client`'s `OpenCodeEvent`), so the valid events that carry the
|
|
27
|
+
* same state changes are used instead: `session.renamed` /
|
|
28
|
+
* `session.metadata.updated` / `session.moved` (the mutations `session.updated`
|
|
29
|
+
* used to stand for) and `session.idle` (a child finishing). `satisfies` keeps
|
|
30
|
+
* the literals while making a name outside the union a type error (surfaced by
|
|
31
|
+
* `tsc -p tsconfig.build.json`), and the sidebar test asserts the same names
|
|
32
|
+
* against the installed union at runtime. */
|
|
33
|
+
const SIDEBAR_REFRESH_EVENTS = [
|
|
34
|
+
"session.created",
|
|
35
|
+
"session.renamed",
|
|
36
|
+
"session.metadata.updated",
|
|
37
|
+
"session.moved",
|
|
38
|
+
"session.idle",
|
|
39
|
+
"session.status",
|
|
40
|
+
"session.deleted"
|
|
41
|
+
];
|
|
42
|
+
/** Read .pantheon/active-preset.json — first existing candidate wins; malformed
|
|
43
|
+
* JSON or a `preset` that is missing, empty or "none" → null (no fall-through
|
|
44
|
+
* to lower candidates). */
|
|
45
|
+
async function readActivePresetFile(cwd) {
|
|
46
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
47
|
+
const candidates = [
|
|
48
|
+
join(cwd, ".pantheon", "active-preset.json"),
|
|
49
|
+
join(xdgConfig, "opencode", ".pantheon", "active-preset.json"),
|
|
50
|
+
join(homedir(), ".opencode", ".pantheon", "active-preset.json")
|
|
51
|
+
];
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = await readFile(candidate, "utf8");
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
const name = parsed && typeof parsed === "object" ? parsed.preset : void 0;
|
|
62
|
+
if (typeof name !== "string" || name.length === 0 || name === "none") return null;
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
source: "file"
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** Env leg of resolution: PANTHEON_MODEL_PRESET set and !== 'none' wins. */
|
|
74
|
+
function presetFromEnv(env) {
|
|
75
|
+
const name = env.PANTHEON_MODEL_PRESET;
|
|
76
|
+
if (name !== void 0 && name !== "" && name !== "none") return {
|
|
77
|
+
name,
|
|
78
|
+
source: "env"
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
name: null,
|
|
82
|
+
source: null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Resolve the active preset for the sidebar: env > file > default. */
|
|
86
|
+
async function resolvePresetForTui(env, cwd) {
|
|
87
|
+
const envPreset = presetFromEnv(env);
|
|
88
|
+
if (envPreset.source === "env") return envPreset;
|
|
89
|
+
return await readActivePresetFile(cwd) ?? {
|
|
90
|
+
name: null,
|
|
91
|
+
source: null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** `SessionInfo.id` is always a `ses…` handle. Guarded so a placeholder or a
|
|
95
|
+
* foreign id never reaches the status/navigate APIs. */
|
|
96
|
+
function isValidSessionID(id) {
|
|
97
|
+
return typeof id === "string" && id.startsWith("ses");
|
|
98
|
+
}
|
|
99
|
+
function numberOrNull(value) {
|
|
100
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
101
|
+
}
|
|
102
|
+
/** Grace window for a child that has not started yet. V2's `session.status()`
|
|
103
|
+
* returns `idle` both for "created but not started" and for "finished", so an
|
|
104
|
+
* idle/unknown/absent child whose own last activity (`time.updated`, falling
|
|
105
|
+
* back to `time.created`) is this recent still reads as running — otherwise a
|
|
106
|
+
* just-spawned child renders a premature ✓. Trade-off (V1 parity: it had the
|
|
107
|
+
* same window for its absent statuses): a child that finished within the window
|
|
108
|
+
* and reports no `outcome` reads as running until the window closes, at which
|
|
109
|
+
* point it turns terminal and its completion toast fires. A child that reports
|
|
110
|
+
* a terminal `outcome` is never held back by the window. */
|
|
111
|
+
const DELEGATION_CHILD_STATUS_GRACE_MS = 6e4;
|
|
112
|
+
/** One delegated child job as the sidebar renders it. */
|
|
113
|
+
/** Map the host's live session status to a display state. V2 exposes only
|
|
114
|
+
* `"idle" | "running"` through the TUI context (the client union adds `retry`
|
|
115
|
+
* and `busy`); `error`/`failed` are handled defensively for host drift.
|
|
116
|
+
* `idle`, `retry` and unknown/absent statuses are terminal only once the
|
|
117
|
+
* child's own activity is older than the grace window — within it the child
|
|
118
|
+
* reads as running (see `DELEGATION_CHILD_STATUS_GRACE_MS`). An interruption is
|
|
119
|
+
* not part of this union: it arrives as a terminal `outcome`. Pure. */
|
|
120
|
+
function delegationStateFromStatus(status, time, now = Date.now(), graceMs = DELEGATION_CHILD_STATUS_GRACE_MS) {
|
|
121
|
+
switch (status) {
|
|
122
|
+
case "running":
|
|
123
|
+
case "busy": return "running";
|
|
124
|
+
case "error":
|
|
125
|
+
case "failed": return "failed";
|
|
126
|
+
default: {
|
|
127
|
+
const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0);
|
|
128
|
+
if (lastActivity > 0 && now - lastActivity < graceMs) return "running";
|
|
129
|
+
return "completed";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Map a V2 child `outcome` to a display state. `interrupted` (user abort,
|
|
134
|
+
* shutdown, superseded, inactivity) is distinct from both success and failure,
|
|
135
|
+
* so an aborted child never renders the ✓ of a completed one. Returns null for
|
|
136
|
+
* an absent/unrecognised outcome so the live status decides. Pure. */
|
|
137
|
+
function delegationStateFromOutcome(outcome) {
|
|
138
|
+
switch (outcome) {
|
|
139
|
+
case "succeeded": return "completed";
|
|
140
|
+
case "failed": return "failed";
|
|
141
|
+
case "interrupted": return "interrupted";
|
|
142
|
+
default: return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Build a delegation row from a child session + its live status. A terminal
|
|
146
|
+
* `outcome` wins when present (the only V2 primitive that surfaces a
|
|
147
|
+
* delegation failure/interruption once the status is back to idle); otherwise
|
|
148
|
+
* the live status decides, with the absent/idle grace window applied. Pure. */
|
|
149
|
+
function toDelegationView(child, status, now = Date.now()) {
|
|
150
|
+
const id = typeof child?.id === "string" ? child.id : "";
|
|
151
|
+
const title = typeof child?.title === "string" && child.title !== "" ? child.title : "";
|
|
152
|
+
const agent = typeof child?.agent === "string" && child.agent !== "" ? child.agent : "";
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
label: title !== "" ? title : agent !== "" ? agent : id.slice(0, 8),
|
|
156
|
+
state: delegationStateFromOutcome(child?.outcome) ?? delegationStateFromStatus(status, child?.time, now),
|
|
157
|
+
startedAt: numberOrNull(child?.time?.created),
|
|
158
|
+
updatedAt: numberOrNull(child?.time?.updated)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const DELEGATION_TONES = {
|
|
162
|
+
running: "warning",
|
|
163
|
+
completed: "success",
|
|
164
|
+
failed: "error",
|
|
165
|
+
interrupted: "warning"
|
|
166
|
+
};
|
|
167
|
+
/** Whole-row tone: red = failed, green = terminal, yellow = in flight. The
|
|
168
|
+
* glyph stays a redundant channel so the state reads in monochrome. Pure. */
|
|
169
|
+
function delegationStateTone(state) {
|
|
170
|
+
return DELEGATION_TONES[state];
|
|
171
|
+
}
|
|
172
|
+
const SPINNER_FRAMES = [
|
|
173
|
+
"⠋",
|
|
174
|
+
"⠙",
|
|
175
|
+
"⠹",
|
|
176
|
+
"⠸",
|
|
177
|
+
"⠼",
|
|
178
|
+
"⠴",
|
|
179
|
+
"⠦",
|
|
180
|
+
"⠧",
|
|
181
|
+
"⠇",
|
|
182
|
+
"⠏"
|
|
183
|
+
];
|
|
184
|
+
/** Spinner glyph for a frame counter (incremented every ~80ms while any job is
|
|
185
|
+
* running). Pure. */
|
|
186
|
+
function spinnerFrame(frame) {
|
|
187
|
+
const index = Math.abs(Math.floor(frame)) % SPINNER_FRAMES.length;
|
|
188
|
+
return SPINNER_FRAMES[index] ?? SPINNER_FRAMES[0];
|
|
189
|
+
}
|
|
190
|
+
/** Row marker (`<glyph> `): the animated spinner while running, ✓ when done,
|
|
191
|
+
* ⊘ when interrupted, ✕ on failure. Pure. */
|
|
192
|
+
function delegationRowMarker(state, frame = 0) {
|
|
193
|
+
if (state === "running") return `${spinnerFrame(frame)} `;
|
|
194
|
+
if (state === "failed") return "✕ ";
|
|
195
|
+
if (state === "interrupted") return "⊘ ";
|
|
196
|
+
return "✓ ";
|
|
197
|
+
}
|
|
198
|
+
/** Compact single-unit elapsed label: "12s"/"3m"/"1h"/"2d". Pure. */
|
|
199
|
+
function fmtElapsed(ms) {
|
|
200
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
201
|
+
const days = Math.floor(total / 86400);
|
|
202
|
+
const hours = Math.floor(total % 86400 / 3600);
|
|
203
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
204
|
+
const seconds = total % 60;
|
|
205
|
+
if (days > 0) return `${days}d`;
|
|
206
|
+
if (hours > 0) return `${hours}h`;
|
|
207
|
+
if (minutes > 0) return `${minutes}m`;
|
|
208
|
+
return `${seconds}s`;
|
|
209
|
+
}
|
|
210
|
+
/** Elapsed label for one row: a running job ticks `now - startedAt`; a terminal
|
|
211
|
+
* one is fixed at `updatedAt - startedAt` (em dash when a timestamp is
|
|
212
|
+
* missing). Pure. */
|
|
213
|
+
function delegationElapsed(view, now) {
|
|
214
|
+
if (view.state === "running") return view.startedAt === null ? "—" : fmtElapsed(now - view.startedAt);
|
|
215
|
+
if (view.startedAt !== null && view.updatedAt !== null) return fmtElapsed(view.updatedAt - view.startedAt);
|
|
216
|
+
return "—";
|
|
217
|
+
}
|
|
218
|
+
/** Live rows first (a running job never falls behind the cap), then by most
|
|
219
|
+
* recent activity. Pure. */
|
|
220
|
+
function orderDelegations(views) {
|
|
221
|
+
return [...views].sort((a, b) => {
|
|
222
|
+
const activeA = a.state === "running" ? 1 : 0;
|
|
223
|
+
const activeB = b.state === "running" ? 1 : 0;
|
|
224
|
+
if (activeA !== activeB) return activeB - activeA;
|
|
225
|
+
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** Cap the panel: at most `maxVisible` rows, the remainder collapsing into the
|
|
229
|
+
* "… +N more" line. Pure. */
|
|
230
|
+
function ceilingDelegations(views, maxVisible = 8) {
|
|
231
|
+
const limit = Math.max(0, maxVisible);
|
|
232
|
+
const visible = views.slice(0, limit);
|
|
233
|
+
return {
|
|
234
|
+
visible,
|
|
235
|
+
hidden: Math.max(0, views.length - visible.length)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** Session ids to eagerly sync at load (deduped, bounded). Pure. */
|
|
239
|
+
function collectWarmupTargets(sessions, limit = WARMUP_SESSION_LIMIT) {
|
|
240
|
+
const ids = [];
|
|
241
|
+
for (const session of sessions) {
|
|
242
|
+
if (!isValidSessionID(session?.id) || ids.includes(session.id)) continue;
|
|
243
|
+
ids.push(session.id);
|
|
244
|
+
if (ids.length >= limit) break;
|
|
245
|
+
}
|
|
246
|
+
return ids;
|
|
247
|
+
}
|
|
248
|
+
/** Body a sidebar instance renders when the host has not published a real
|
|
249
|
+
* session id yet. OpenCode V2 scopes the `sidebar.content` slot to a session
|
|
250
|
+
* (`SlotMap["sidebar.content"] = { sessionID: string }`), so before the first
|
|
251
|
+
* prompt the slot may not mount at all — see the `home.footer.status`
|
|
252
|
+
* contribution for the pre-prompt degradation. Pure. */
|
|
253
|
+
function sidebarPlaceholder(sessionID) {
|
|
254
|
+
return isValidSessionID(sessionID) ? null : SIDEBAR_WAITING_LABEL;
|
|
255
|
+
}
|
|
256
|
+
/** Navigate to a session through the V2 router. Fail-open: an absent router or
|
|
257
|
+
* a removed session must never break the sidebar. Returns whether a valid
|
|
258
|
+
* navigation was dispatched. Pure. */
|
|
259
|
+
function navigateToSession(router, sessionID) {
|
|
260
|
+
if (typeof router?.navigate !== "function" || !isValidSessionID(sessionID)) return false;
|
|
261
|
+
try {
|
|
262
|
+
router.navigate({
|
|
263
|
+
type: "session",
|
|
264
|
+
sessionID
|
|
265
|
+
});
|
|
266
|
+
return true;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Diff two delegation observations: entries that were `running` and are no
|
|
272
|
+
* longer running count as completions, fired once per transition. Returns the
|
|
273
|
+
* next snapshot so callers can keep a single tracker. First observation never
|
|
274
|
+
* reports a completion (no toast spam on mount). Pure. */
|
|
275
|
+
function detectDelegationCompletions(previous, current) {
|
|
276
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
277
|
+
const completed = [];
|
|
278
|
+
for (const entry of current) {
|
|
279
|
+
snapshot.set(entry.id, entry.state);
|
|
280
|
+
if (previous.get(entry.id) === "running" && entry.state !== "running") completed.push(entry);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
completed,
|
|
284
|
+
snapshot
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function resolveLocation(context) {
|
|
288
|
+
try {
|
|
289
|
+
return context?.location ?? context?.data?.location?.default();
|
|
290
|
+
} catch {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function resolveSidebarCwd(context) {
|
|
295
|
+
const location = resolveLocation(context);
|
|
296
|
+
if (location && typeof location.directory === "string" && location.directory !== "") return location.directory;
|
|
297
|
+
return process.cwd();
|
|
298
|
+
}
|
|
299
|
+
function sessionStatusOf(context, sessionID) {
|
|
300
|
+
try {
|
|
301
|
+
const status = context?.data?.session?.status?.(sessionID);
|
|
302
|
+
return typeof status === "string" ? status : void 0;
|
|
303
|
+
} catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Populate the cached data at load so the sidebar has content before the first
|
|
308
|
+
* prompt (Known Issue guard). Fire-and-forget: a missing API or a failed sync
|
|
309
|
+
* degrades silently and the sidebar still renders its static header. */
|
|
310
|
+
function warmUpSidebarData(context, location) {
|
|
311
|
+
try {
|
|
312
|
+
Promise.resolve(context?.data?.location?.vcs?.sync?.(location)).catch(() => {});
|
|
313
|
+
} catch {}
|
|
314
|
+
let listed = [];
|
|
315
|
+
try {
|
|
316
|
+
const result = context?.data?.session?.list?.();
|
|
317
|
+
if (Array.isArray(result)) listed = result;
|
|
318
|
+
} catch {}
|
|
319
|
+
for (const id of collectWarmupTargets(listed)) try {
|
|
320
|
+
Promise.resolve(context?.data?.session?.sync?.(id)).catch(() => {});
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
function showDelegationToast(context, done) {
|
|
324
|
+
const variant = done.state === "failed" ? "error" : done.state === "interrupted" ? "warning" : "success";
|
|
325
|
+
const verb = done.state === "failed" ? "falhou" : done.state === "interrupted" ? "interrompida" : "concluída";
|
|
326
|
+
try {
|
|
327
|
+
context?.ui?.toast?.show?.({
|
|
328
|
+
title: TOAST_TITLE,
|
|
329
|
+
message: `${done.label} ${verb}`,
|
|
330
|
+
variant,
|
|
331
|
+
sessionID: done.id
|
|
332
|
+
});
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
/** Active preset signal with mount + 30s refresh. Shared by the sidebar and the
|
|
336
|
+
* home-screen status contribution. */
|
|
337
|
+
function useActivePreset(context) {
|
|
338
|
+
const [preset, setPreset] = createSignal(presetFromEnv(process.env));
|
|
339
|
+
onMount(() => {
|
|
340
|
+
const cwd = resolveSidebarCwd(context);
|
|
341
|
+
let cancelled = false;
|
|
342
|
+
const refresh = async () => {
|
|
343
|
+
try {
|
|
344
|
+
const info = await resolvePresetForTui(process.env, cwd);
|
|
345
|
+
if (!cancelled) setPreset(info);
|
|
346
|
+
} catch {}
|
|
347
|
+
};
|
|
348
|
+
refresh();
|
|
349
|
+
const timer = setInterval(() => void refresh(), PRESET_REFRESH_MS);
|
|
350
|
+
onCleanup(() => {
|
|
351
|
+
cancelled = true;
|
|
352
|
+
clearInterval(timer);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
return preset;
|
|
356
|
+
}
|
|
357
|
+
/** Reactive state the sidebar view renders. Accessors keep the view reactive
|
|
358
|
+
* while the model itself stays free of JSX, so its effects are exercisable
|
|
359
|
+
* without a TUI renderer. */
|
|
360
|
+
/** Reactive wiring behind the sidebar: refresh subscription, warm syncs, the
|
|
361
|
+
* running-only spinner and the completion-toast effect. `context` stays loosely
|
|
362
|
+
* typed (the host API only exists at runtime) but the refresh event names are
|
|
363
|
+
* narrowed to the V2 union at the subscription site. */
|
|
364
|
+
function createSidebarModel(context, sessionID) {
|
|
365
|
+
const [revision, setRevision] = createSignal(0);
|
|
366
|
+
const [frame, setFrame] = createSignal(0);
|
|
367
|
+
const location = resolveLocation(context);
|
|
368
|
+
const preset = useActivePreset(context);
|
|
369
|
+
let observedStates = /* @__PURE__ */ new Map();
|
|
370
|
+
const refresh = () => setRevision((value) => value + 1);
|
|
371
|
+
const activeSessionID = () => {
|
|
372
|
+
const id = sessionID();
|
|
373
|
+
return isValidSessionID(id) ? id : "";
|
|
374
|
+
};
|
|
375
|
+
const session = createMemo(() => {
|
|
376
|
+
revision();
|
|
377
|
+
const id = activeSessionID();
|
|
378
|
+
if (id === "") return void 0;
|
|
379
|
+
try {
|
|
380
|
+
return context.data.session.get(id);
|
|
381
|
+
} catch {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
const branch = createMemo(() => {
|
|
386
|
+
revision();
|
|
387
|
+
try {
|
|
388
|
+
return context.data.location.vcs.info(location)?.branch?.current ?? null;
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
const allDelegations = createMemo(() => {
|
|
394
|
+
revision();
|
|
395
|
+
const id = activeSessionID();
|
|
396
|
+
if (id === "") return [];
|
|
397
|
+
let family = [];
|
|
398
|
+
try {
|
|
399
|
+
const result = context.data.session.family?.(id);
|
|
400
|
+
if (Array.isArray(result)) family = result;
|
|
401
|
+
} catch {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const views = [];
|
|
406
|
+
for (const childID of family) {
|
|
407
|
+
if (childID === id || !isValidSessionID(childID)) continue;
|
|
408
|
+
const child = context.data.session.get(childID);
|
|
409
|
+
if (child?.parentID !== id) continue;
|
|
410
|
+
views.push(toDelegationView(child, sessionStatusOf(context, childID), now));
|
|
411
|
+
}
|
|
412
|
+
return orderDelegations(views);
|
|
413
|
+
});
|
|
414
|
+
const delegations = createMemo(() => allDelegations().filter((view) => view.state === "running"));
|
|
415
|
+
const ceiling = createMemo(() => ceilingDelegations(delegations(), 8));
|
|
416
|
+
const hasRunning = createMemo(() => delegations().length > 0);
|
|
417
|
+
onMount(() => {
|
|
418
|
+
const stop = [];
|
|
419
|
+
const data = context?.data;
|
|
420
|
+
if (typeof data?.on === "function") for (const type of SIDEBAR_REFRESH_EVENTS) try {
|
|
421
|
+
const unsubscribe = data.on(type, refresh);
|
|
422
|
+
if (typeof unsubscribe === "function") stop.push(unsubscribe);
|
|
423
|
+
} catch {}
|
|
424
|
+
const id = activeSessionID();
|
|
425
|
+
if (id !== "") try {
|
|
426
|
+
Promise.resolve(context.data.session.sync(id)).then(refresh);
|
|
427
|
+
} catch {}
|
|
428
|
+
try {
|
|
429
|
+
Promise.resolve(context.data.location.vcs.sync(location)).then(refresh);
|
|
430
|
+
} catch {}
|
|
431
|
+
createEffect(() => {
|
|
432
|
+
if (!hasRunning()) return;
|
|
433
|
+
const spinner = setInterval(() => setFrame((value) => value + 1), SPINNER_INTERVAL_MS);
|
|
434
|
+
onCleanup(() => clearInterval(spinner));
|
|
435
|
+
});
|
|
436
|
+
onCleanup(() => {
|
|
437
|
+
for (const unsubscribe of stop) if (typeof unsubscribe === "function") unsubscribe();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
createEffect(() => {
|
|
441
|
+
const views = allDelegations();
|
|
442
|
+
if (activeSessionID() === "") {
|
|
443
|
+
observedStates = /* @__PURE__ */ new Map();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const { completed, snapshot } = detectDelegationCompletions(observedStates, views.map((view) => ({
|
|
447
|
+
id: view.id,
|
|
448
|
+
label: view.label,
|
|
449
|
+
state: view.state
|
|
450
|
+
})));
|
|
451
|
+
observedStates = snapshot;
|
|
452
|
+
for (const done of completed) showDelegationToast(context, done);
|
|
453
|
+
});
|
|
454
|
+
return {
|
|
455
|
+
activeSessionID,
|
|
456
|
+
session,
|
|
457
|
+
branch,
|
|
458
|
+
delegations,
|
|
459
|
+
ceiling,
|
|
460
|
+
hasRunning,
|
|
461
|
+
frame,
|
|
462
|
+
preset
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function Sidebar(props) {
|
|
466
|
+
const { context } = props;
|
|
467
|
+
const model = createSidebarModel(context, () => props.sessionID);
|
|
468
|
+
const text = () => context.theme.text?.base;
|
|
469
|
+
const muted = () => context.theme.text?.muted ?? text();
|
|
470
|
+
const tone = (value) => context.theme?.text?.feedback?.[value]?.base ?? text();
|
|
471
|
+
const open = (sessionID) => () => navigateToSession(context.ui?.router, sessionID);
|
|
472
|
+
return (() => {
|
|
473
|
+
var _el$ = createElement("box"), _el$2 = createElement("text"), _el$3 = createElement("text");
|
|
474
|
+
insertNode(_el$, _el$2);
|
|
475
|
+
insertNode(_el$, _el$3);
|
|
476
|
+
setProp(_el$, "flexDirection", "column");
|
|
477
|
+
setProp(_el$, "width", "100%");
|
|
478
|
+
setProp(_el$, "padding", 1);
|
|
479
|
+
setProp(_el$2, "attributes", 1);
|
|
480
|
+
insert(_el$2, `Pantheon v1.6.0-beta.10`);
|
|
481
|
+
insertNode(_el$3, createTextNode(`OpenCode V2 · plugin TUI ativo`));
|
|
482
|
+
insert(_el$, createComponent(Show, {
|
|
483
|
+
get when() {
|
|
484
|
+
return model.branch();
|
|
485
|
+
},
|
|
486
|
+
children: (value) => (() => {
|
|
487
|
+
var _el$8 = createElement("text");
|
|
488
|
+
insert(_el$8, () => `Branch: ${value()}`);
|
|
489
|
+
effect((_$p) => setProp(_el$8, "fg", muted(), _$p));
|
|
490
|
+
return _el$8;
|
|
491
|
+
})()
|
|
492
|
+
}), null);
|
|
493
|
+
insert(_el$, createComponent(Show, {
|
|
494
|
+
get when() {
|
|
495
|
+
return model.preset().name;
|
|
496
|
+
},
|
|
497
|
+
get fallback() {
|
|
498
|
+
return (() => {
|
|
499
|
+
var _el$9 = createElement("box"), _el$0 = createElement("text");
|
|
500
|
+
insertNode(_el$9, _el$0);
|
|
501
|
+
setProp(_el$9, "flexDirection", "row");
|
|
502
|
+
insertNode(_el$0, createTextNode(`Preset: default`));
|
|
503
|
+
effect((_$p) => setProp(_el$0, "fg", muted(), _$p));
|
|
504
|
+
return _el$9;
|
|
505
|
+
})();
|
|
506
|
+
},
|
|
507
|
+
children: (name) => (() => {
|
|
508
|
+
var _el$10 = createElement("box"), _el$11 = createElement("text"), _el$13 = createElement("text"), _el$14 = createElement("text");
|
|
509
|
+
insertNode(_el$10, _el$11);
|
|
510
|
+
insertNode(_el$10, _el$13);
|
|
511
|
+
insertNode(_el$10, _el$14);
|
|
512
|
+
setProp(_el$10, "flexDirection", "row");
|
|
513
|
+
insertNode(_el$11, createTextNode(`⚡ Preset: `));
|
|
514
|
+
insert(_el$13, name);
|
|
515
|
+
insert(_el$14, () => ` (${model.preset().source ?? ""})`);
|
|
516
|
+
effect((_p$) => {
|
|
517
|
+
var _v$3 = muted(), _v$4 = text(), _v$5 = muted();
|
|
518
|
+
_v$3 !== _p$.e && (_p$.e = setProp(_el$11, "fg", _v$3, _p$.e));
|
|
519
|
+
_v$4 !== _p$.t && (_p$.t = setProp(_el$13, "fg", _v$4, _p$.t));
|
|
520
|
+
_v$5 !== _p$.a && (_p$.a = setProp(_el$14, "fg", _v$5, _p$.a));
|
|
521
|
+
return _p$;
|
|
522
|
+
}, {
|
|
523
|
+
e: void 0,
|
|
524
|
+
t: void 0,
|
|
525
|
+
a: void 0
|
|
526
|
+
});
|
|
527
|
+
return _el$10;
|
|
528
|
+
})()
|
|
529
|
+
}), null);
|
|
530
|
+
insert(_el$, createComponent(Show, {
|
|
531
|
+
get when() {
|
|
532
|
+
return memo(() => model.activeSessionID() !== "")() && model.session();
|
|
533
|
+
},
|
|
534
|
+
get fallback() {
|
|
535
|
+
return (() => {
|
|
536
|
+
var _el$15 = createElement("text");
|
|
537
|
+
insert(_el$15, () => sidebarPlaceholder(props.sessionID));
|
|
538
|
+
effect((_$p) => setProp(_el$15, "fg", muted(), _$p));
|
|
539
|
+
return _el$15;
|
|
540
|
+
})();
|
|
541
|
+
},
|
|
542
|
+
children: (current) => [(() => {
|
|
543
|
+
var _el$16 = createElement("text");
|
|
544
|
+
insert(_el$16, () => `Sessão: ${current().title || current().id}`);
|
|
545
|
+
effect((_$p) => setProp(_el$16, "fg", text(), _$p));
|
|
546
|
+
return _el$16;
|
|
547
|
+
})(), createComponent(Show, {
|
|
548
|
+
get when() {
|
|
549
|
+
return current().agent;
|
|
550
|
+
},
|
|
551
|
+
get children() {
|
|
552
|
+
var _el$17 = createElement("text");
|
|
553
|
+
insert(_el$17, () => `Agente: ${current().agent}`);
|
|
554
|
+
effect((_$p) => setProp(_el$17, "fg", muted(), _$p));
|
|
555
|
+
return _el$17;
|
|
556
|
+
}
|
|
557
|
+
})]
|
|
558
|
+
}), null);
|
|
559
|
+
insert(_el$, createComponent(Show, {
|
|
560
|
+
get when() {
|
|
561
|
+
return model.delegations().length > 0;
|
|
562
|
+
},
|
|
563
|
+
get children() {
|
|
564
|
+
return [
|
|
565
|
+
(() => {
|
|
566
|
+
var _el$5 = createElement("text");
|
|
567
|
+
insertNode(_el$5, createTextNode(`Delegações`));
|
|
568
|
+
setProp(_el$5, "attributes", 1);
|
|
569
|
+
effect((_$p) => setProp(_el$5, "fg", text(), _$p));
|
|
570
|
+
return _el$5;
|
|
571
|
+
})(),
|
|
572
|
+
createComponent(For, {
|
|
573
|
+
get each() {
|
|
574
|
+
return model.ceiling().visible;
|
|
575
|
+
},
|
|
576
|
+
children: (view) => (() => {
|
|
577
|
+
var _el$18 = createElement("box"), _el$19 = createElement("text");
|
|
578
|
+
insertNode(_el$18, _el$19);
|
|
579
|
+
insert(_el$19, () => `${delegationRowMarker(view.state, model.frame())}${view.label} ${delegationElapsed(view, Date.now())}`);
|
|
580
|
+
effect((_p$) => {
|
|
581
|
+
var _v$6 = open(view.id), _v$7 = tone(delegationStateTone(view.state));
|
|
582
|
+
_v$6 !== _p$.e && (_p$.e = setProp(_el$18, "onMouseDown", _v$6, _p$.e));
|
|
583
|
+
_v$7 !== _p$.t && (_p$.t = setProp(_el$19, "fg", _v$7, _p$.t));
|
|
584
|
+
return _p$;
|
|
585
|
+
}, {
|
|
586
|
+
e: void 0,
|
|
587
|
+
t: void 0
|
|
588
|
+
});
|
|
589
|
+
return _el$18;
|
|
590
|
+
})()
|
|
591
|
+
}),
|
|
592
|
+
createComponent(Show, {
|
|
593
|
+
get when() {
|
|
594
|
+
return model.ceiling().hidden > 0;
|
|
595
|
+
},
|
|
596
|
+
get children() {
|
|
597
|
+
var _el$7 = createElement("text");
|
|
598
|
+
insert(_el$7, () => `… +${model.ceiling().hidden} more`);
|
|
599
|
+
effect((_$p) => setProp(_el$7, "fg", muted(), _$p));
|
|
600
|
+
return _el$7;
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
];
|
|
604
|
+
}
|
|
605
|
+
}), null);
|
|
606
|
+
effect((_p$) => {
|
|
607
|
+
var _v$ = text(), _v$2 = muted();
|
|
608
|
+
_v$ !== _p$.e && (_p$.e = setProp(_el$2, "fg", _v$, _p$.e));
|
|
609
|
+
_v$2 !== _p$.t && (_p$.t = setProp(_el$3, "fg", _v$2, _p$.t));
|
|
610
|
+
return _p$;
|
|
611
|
+
}, {
|
|
612
|
+
e: void 0,
|
|
613
|
+
t: void 0
|
|
614
|
+
});
|
|
615
|
+
return _el$;
|
|
616
|
+
})();
|
|
617
|
+
}
|
|
618
|
+
/** Home-screen status contribution. The sidebar slot is session-scoped, so
|
|
619
|
+
* before the first prompt the sidebar may not mount at all (Known Issue) —
|
|
620
|
+
* this keeps a compact Pantheon line visible on the home footer meanwhile. */
|
|
621
|
+
function HomeStatus(props) {
|
|
622
|
+
const preset = useActivePreset(props.context);
|
|
623
|
+
const muted = () => props.context.theme?.text?.muted ?? props.context.theme?.text?.base;
|
|
624
|
+
return (() => {
|
|
625
|
+
var _el$20 = createElement("text");
|
|
626
|
+
insert(_el$20, () => `Pantheon v1.6.0-beta.10${preset().name ? ` ⚡ ${preset().name}` : ""}`);
|
|
627
|
+
effect((_$p) => setProp(_el$20, "fg", muted(), _$p));
|
|
628
|
+
return _el$20;
|
|
629
|
+
})();
|
|
630
|
+
}
|
|
631
|
+
const plugin = Plugin.define({
|
|
632
|
+
id: "pantheon.tui",
|
|
633
|
+
setup(context) {
|
|
634
|
+
const globalState = globalThis;
|
|
635
|
+
if (globalState[SINGLETON]) return;
|
|
636
|
+
globalState[SINGLETON] = true;
|
|
637
|
+
warmUpSidebarData(context, resolveLocation(context));
|
|
638
|
+
const unregisterSidebar = context.ui.slot({
|
|
639
|
+
append: "sidebar.content",
|
|
640
|
+
render: ({ sessionID }) => createComponent(Sidebar, {
|
|
641
|
+
context,
|
|
642
|
+
sessionID
|
|
643
|
+
})
|
|
644
|
+
});
|
|
645
|
+
const unregisterHome = context.ui.slot({
|
|
646
|
+
append: "home.footer.status",
|
|
647
|
+
render: () => createComponent(HomeStatus, { context })
|
|
648
|
+
});
|
|
649
|
+
return () => {
|
|
650
|
+
if (typeof unregisterSidebar === "function") unregisterSidebar();
|
|
651
|
+
if (typeof unregisterHome === "function") unregisterHome();
|
|
652
|
+
delete globalState[SINGLETON];
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
//#endregion
|
|
657
|
+
export { DELEGATIONS_VISIBLE_CEILING, DELEGATION_CHILD_STATUS_GRACE_MS, SIDEBAR_REFRESH_EVENTS, SIDEBAR_WAITING_LABEL, ceilingDelegations, collectWarmupTargets, createSidebarModel, plugin as default, delegationElapsed, delegationRowMarker, delegationStateFromOutcome, delegationStateFromStatus, delegationStateTone, detectDelegationCompletions, fmtElapsed, isValidSessionID, navigateToSession, orderDelegations, presetFromEnv, resolvePresetForTui, sidebarPlaceholder, spinnerFrame, toDelegationView };
|
|
658
|
+
|
|
659
|
+
//# sourceMappingURL=tui-v2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui-v2.js","names":[],"sources":[],"mappings":""}
|