pantheon-opencode 1.1.1 → 1.2.0-beta.9.7bef03e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +74 -1
  2. package/bin/pantheon-init.mjs +79 -0
  3. package/commands/pantheon-audit.md +25 -0
  4. package/commands/pantheon-consolidate.md +11 -0
  5. package/commands/pantheon-deepwork.md +79 -0
  6. package/commands/pantheon-optimize.md +79 -0
  7. package/commands/pantheon-vision.md +67 -0
  8. package/commands/pantheon.md +49 -0
  9. package/opencode.json +12 -41
  10. package/package.json +5 -1
  11. package/scripts/doctor.mjs +4 -2
  12. package/scripts/install/health-check.mjs +101 -1
  13. package/scripts/install/model-picker.mjs +118 -0
  14. package/scripts/install/opencode.mjs +98 -17
  15. package/scripts/install-mcp.mjs +98 -13
  16. package/scripts/mcp_persistence_server.py +331 -1
  17. package/scripts/secret-scan.mjs +76 -0
  18. package/scripts/validate-routing.mjs +15 -0
  19. package/src/agents/aphrodite.md +7 -0
  20. package/src/agents/demeter.md +7 -0
  21. package/src/agents/hephaestus.md +7 -0
  22. package/src/agents/hermes.md +7 -0
  23. package/src/agents/zeus.md +31 -18
  24. package/src/instructions/agent-return-format.instructions.md +1 -0
  25. package/src/instructions/zeus-anti-stall.instructions.md +43 -5
  26. package/src/instructions/zeus-timeout-retry.instructions.md +47 -10
  27. package/src/mcp/install-mcp.mjs +6 -913
  28. package/src/mcp/mcp_persistence_server.py +332 -0
  29. package/src/mcp/pantheon_vision_server.py +495 -0
  30. package/src/mcp/requirements-vision.txt +3 -0
  31. package/src/pantheon/auto-wake.ts +91 -0
  32. package/src/pantheon/background-job-board.ts +568 -0
  33. package/src/pantheon/file-persistence.ts +95 -0
  34. package/src/pantheon/presets.d.mts +99 -0
  35. package/src/pantheon/presets.mjs +491 -0
  36. package/src/pantheon/vision.ts +1340 -0
  37. package/src/plugin.ts +40 -25
  38. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  39. package/src/plugins/tui/dist/tui.js +806 -222
  40. package/src/plugins/tui/dist/tui.tsx +866 -121
  41. package/src/plugins/tui/package.json +2 -2
  42. package/src/plugins/tui/src/index.tsx +866 -121
  43. package/src/routing.yml +179 -0
  44. package/commands/pantheon-reflect.md +0 -16
  45. package/commands/pantheon-verify.md +0 -17
@@ -1,8 +1,53 @@
1
1
  import { createComponent, createElement, createTextNode, effect, insert, insertNode, memo, setProp } from "@opentui/solid";
2
+ import { Buffer } from "node:buffer";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
2
6
  import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js";
3
7
  //#region src/index.tsx
4
8
  /** @jsxImportSource @opentui/solid */
5
- const BAR_WIDTH = 20;
9
+ /**
10
+ * pantheon-tui — Pantheon TUI plugin for opencode.
11
+ *
12
+ * Single self-contained entry file: `tsdown && cp src/index.tsx dist/tui.tsx`
13
+ * keeps the no-build load path working (dist/tui.tsx is raw source, transpiled
14
+ * on the fly by opencode). All feature code lives in this one file.
15
+ *
16
+ * Slots:
17
+ * - sidebar_content (order 900) — Pantheon sidebar (header/version/
18
+ * branch, Sessions, Commands, Agents, Config, Memory).
19
+ * - app_bottom (order 60) — AI subscription usage gauges
20
+ * (Anthropic/OpenAI quotas, OpenCode Go/Zen dollar limits + provider
21
+ * status incidents).
22
+ *
23
+ * ─────────────────────────────────────────────────────────────────────────────
24
+ * VENDORED FEATURES (MIT) — incorporated with their license headers preserved:
25
+ *
26
+ * • satas20/opencode-usage-bar (MIT)
27
+ * https://github.com/satas20/opencode-usage-bar
28
+ *
29
+ * v1.2.0 added an OpenCode Go/Zen usage provider (`opencodego`) on top of
30
+ * the vendored base — dollar-denominated rolling limits ($12/5h, $30/7d,
31
+ * $60/month) via the same key used for inference. Still MIT-credited to
32
+ * satas20 for the original bar/config/poll code.
33
+ *
34
+ * satas20/opencode-todo-progress was vendored in v1.1.0 alongside the
35
+ * usage-bar but has since been REMOVED as redundant — the native session
36
+ * footer already surfaces todo/context state, so its todo bar slot was
37
+ * dropped entirely. Only usage-bar code remains.
38
+ * ─────────────────────────────────────────────────────────────────────────────
39
+ *
40
+ * LIMITATION — native context/tokens statusline:
41
+ * The opencode session footer natively shows a context/tokens statusline
42
+ * next to the prompt. There is NO config option to hide it: the tui.json
43
+ * schema (opencode.ai/tui.json) only exposes `theme`, `keybinds`,
44
+ * `scroll_speed`, `scroll_acceleration`, `diff_style`, `mouse` and
45
+ * `attention`, and the SDK `Config` type has no statusline/tokens display
46
+ * flag (checked against @opencode-ai/sdk `Config` and the published
47
+ * schema). The Pantheon sidebar therefore no longer duplicates that footer
48
+ * (the old ContextBar was removed) and we deliberately do NOT hack the
49
+ * native footer. Track upstream: opencode statusline config.
50
+ */
6
51
  const COMMANDS = [
7
52
  {
8
53
  name: "/pantheon",
@@ -49,6 +94,9 @@ const COMMANDS = [
49
94
  desc: "Compress memories"
50
95
  }
51
96
  ];
97
+ /** How often the sidebar re-reads .pantheon/active-preset.json so `set-tier`
98
+ * changes made while opencode is open show up within ~30s. */
99
+ const PRESET_REFRESH_MS = 3e4;
52
100
  const AGENTS = [
53
101
  {
54
102
  name: "zeus",
@@ -121,39 +169,67 @@ const AGENTS = [
121
169
  role: "Hotfixes"
122
170
  }
123
171
  ];
124
- function safeNum(v) {
125
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
126
- }
127
172
  function fmtInt(n) {
128
173
  return Intl.NumberFormat("en-US").format(Math.max(0, Math.round(n)));
129
174
  }
130
- function fmtCost(v) {
131
- if (v <= 0) return "";
132
- if (v < .01) return "<$0.01";
133
- return `$${v.toFixed(2)}`;
134
- }
135
- function msgTokens(msg) {
136
- const t = msg?.tokens ?? msg?.info?.tokens ?? {};
137
- return safeNum(t.input) + safeNum(t.output) + safeNum(t.reasoning) + safeNum(t?.cache?.read) + safeNum(t?.cache?.write);
175
+ /** Read .pantheon/active-preset.json — mirrors the presets.mjs file leg:
176
+ * first existing candidate wins; malformed JSON or a `preset` that is
177
+ * missing, empty or "none" → null (no fall-through to lower candidates). */
178
+ async function readActivePresetFile(cwd) {
179
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
180
+ const candidates = [
181
+ join(cwd, ".pantheon", "active-preset.json"),
182
+ join(xdgConfig, "opencode", ".pantheon", "active-preset.json"),
183
+ join(homedir(), ".opencode", ".pantheon", "active-preset.json")
184
+ ];
185
+ for (const candidate of candidates) {
186
+ let raw;
187
+ try {
188
+ raw = await readFile(candidate, "utf8");
189
+ } catch {
190
+ continue;
191
+ }
192
+ try {
193
+ const parsed = JSON.parse(raw);
194
+ const name = parsed && typeof parsed === "object" ? parsed.preset : void 0;
195
+ if (typeof name !== "string" || name.length === 0 || name === "none") return null;
196
+ return {
197
+ name,
198
+ source: "file"
199
+ };
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+ return null;
138
205
  }
139
- function msgCost(src) {
140
- for (const c of [
141
- src?.cost,
142
- src?.info?.cost,
143
- src?.usage?.cost
144
- ]) if (typeof c === "number" && c > 0) return c;
145
- return 0;
206
+ /** Env leg of resolution: PANTHEON_MODEL_PRESET set and !== 'none' wins. */
207
+ function presetFromEnv(env) {
208
+ const name = env.PANTHEON_MODEL_PRESET;
209
+ if (name !== void 0 && name !== "" && name !== "none") return {
210
+ name,
211
+ source: "env"
212
+ };
213
+ return {
214
+ name: null,
215
+ source: null
216
+ };
146
217
  }
147
- function buildBar(pct) {
148
- const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(Math.max(0, Math.min(100, pct)) / 100 * BAR_WIDTH)));
149
- return "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
218
+ /** Resolve the active preset for the sidebar: env > file > default. */
219
+ async function resolvePresetForTui(env, cwd) {
220
+ const envPreset = presetFromEnv(env);
221
+ if (envPreset.source === "env") return envPreset;
222
+ return await readActivePresetFile(cwd) ?? {
223
+ name: null,
224
+ source: null
225
+ };
150
226
  }
151
227
  async function detectVersion(api) {
152
228
  try {
153
229
  const wt = api.state.path?.worktree ?? "";
154
230
  const fp = wt ? `${wt}/package.json` : "package.json";
155
- const result = await api.client.file.read({ query: { path: fp } });
156
- const match = String(result?.content ?? "").match(/"version":\s*"([^"]+)"/);
231
+ const result = await api.client.file.read({ path: fp });
232
+ const match = String(result?.data?.content ?? "").match(/"version":\s*"([^"]+)"/);
157
233
  if (match?.[1]) return match[1];
158
234
  } catch {}
159
235
  try {
@@ -186,74 +262,532 @@ async function detectVersion(api) {
186
262
  } catch {}
187
263
  return null;
188
264
  }
189
- function ContextBar(props) {
190
- const messages = createMemo(() => {
265
+ /**
266
+ * usage-bar — AI subscription usage gauge for the opencode TUI.
267
+ *
268
+ * Renders a compact usage strip in the `app_bottom` slot — a full-width row
269
+ * just below the session footer:
270
+ *
271
+ * ▓▓▓▓░░ 65% · 0h 11m (one window)
272
+ * cld ▓▓▓▓░ 65% · 0h 11m 7d ▓░░░░ 19% · 1d 11h oai ▓░░░░ 12% · 3h 4m
273
+ * ! cld ▓▓▓▓░ 65% · 0h 11m (anthropic incident)
274
+ *
275
+ * Providers:
276
+ * anthropic — Claude Pro/Max via the OAuth token in ~/.claude/.credentials.json
277
+ * openai — ChatGPT Plus/Pro via the Codex CLI login in ~/.codex/auth.json
278
+ * opencodego— OpenCode Go/Zen dollar limits ($12/5h, $30/7d, $60/mo) via
279
+ * PANTHEON_OPENCODE_API_KEY (fallback OPENCODE_API_KEY). No
280
+ * status page — the provider hides silently when unconfigured
281
+ * or when the (not yet public) usage endpoint is unavailable.
282
+ *
283
+ * When a provider's public status page reports an incident, a colored `!`
284
+ * marker appears next to its prefix (red = major/critical, amber = minor,
285
+ * cyan = maintenance). The usage bar itself is unaffected. Disable with
286
+ * `show_status = false` under `[ui]`.
287
+ *
288
+ * Configured via ~/.config/opencode/usage-bar.toml (auto-created with
289
+ * commented defaults on first run; read once at startup). Tokens/keys are
290
+ * only ever sent to their own provider's API host.
291
+ *
292
+ * Loaded via tui.json, e.g.:
293
+ * { "plugin": ["@satas/opencode-usage-bar"] } // published npm package
294
+ * { "plugin": ["/abs/path/to/src/index.tsx"] } // local file (no build)
295
+ */
296
+ const POLL_MS = 12e4;
297
+ const FETCH_TIMEOUT_MS = 1e4;
298
+ const CONFIG_FILE = "usage-bar.toml";
299
+ /** Which quota window a value belongs to; toggled per provider in config. */
300
+ /** Provider health from the vendor's public status page (Statuspage schema). */
301
+ function expandTilde(p) {
302
+ return p === "~" ? homedir() : p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
303
+ }
304
+ function fmtDuration(ms) {
305
+ const totalMinutes = Math.max(0, Math.ceil(ms / 6e4));
306
+ const days = Math.floor(totalMinutes / 1440);
307
+ const hours = Math.floor(totalMinutes % 1440 / 60);
308
+ const minutes = totalMinutes % 60;
309
+ if (days > 0) return `${days}d ${hours}h`;
310
+ return `${hours}h ${minutes}m`;
311
+ }
312
+ /** Decode a JWT's `exp` claim (unix seconds) without verifying. 0 on failure. */
313
+ function jwtExp(token) {
314
+ try {
315
+ const payload = token.split(".")[1];
316
+ if (!payload) return 0;
317
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
318
+ return typeof claims.exp === "number" ? claims.exp : 0;
319
+ } catch {
320
+ return 0;
321
+ }
322
+ }
323
+ async function fetchJson(url, headers) {
324
+ try {
325
+ const res = await fetch(url, {
326
+ headers,
327
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
328
+ });
329
+ if (!res.ok) return null;
330
+ return await res.json();
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+ /** Fetch a vendor's overall status from its public Statuspage JSON endpoint.
336
+ * Returns the `indicator` ("none" when healthy), or `null` when the fetch
337
+ * itself failed — so a network blip never clears a known incident. */
338
+ async function fetchStatus(url) {
339
+ const data = await fetchJson(url, {});
340
+ if (!data?.status) return null;
341
+ const indicator = data.status.indicator;
342
+ return indicator === "minor" || indicator === "major" || indicator === "critical" || indicator === "maintenance" ? indicator : "none";
343
+ }
344
+ /** Set from `api.state.path.state` at startup; default matches opencode's
345
+ * XDG data dir. */
346
+ let opencodeAuthFile = join(homedir(), ".local", "share", "opencode", "auth.json");
347
+ /** Read a provider's entry from opencode's own auth store
348
+ * (`opencode auth login`). Returns null when missing/unreadable. */
349
+ async function opencodeAuth(...ids) {
350
+ try {
351
+ const auth = JSON.parse(await readFile(opencodeAuthFile, "utf8"));
352
+ for (const id of ids) {
353
+ const entry = auth[id];
354
+ if (entry) return entry;
355
+ }
356
+ return null;
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ /** Claude Pro/Max — Anthropic's OAuth usage endpoint (same one Claude Code's
362
+ * `/usage` uses). Token from ~/.claude/.credentials.json, falling back to
363
+ * opencode's own auth store; sent only to api.anthropic.com. */
364
+ const anthropicProvider = {
365
+ id: "anthropic",
366
+ short: "cld",
367
+ statusUrl: "https://status.anthropic.com/api/v2/status.json",
368
+ async fetchUsage(cfg) {
369
+ let token;
191
370
  try {
192
- return props.api.state.session.messages(props.sessionID) ?? [];
193
- } catch {
194
- return [];
371
+ const path = expandTilde(cfg.credentialsPath ?? join(homedir(), ".claude", ".credentials.json"));
372
+ const oauth = JSON.parse(await readFile(path, "utf8")).claudeAiOauth;
373
+ if (oauth?.accessToken && !(oauth.expiresAt && Date.now() >= oauth.expiresAt)) token = oauth.accessToken;
374
+ } catch {}
375
+ if (!token) {
376
+ const entry = await opencodeAuth("anthropic");
377
+ if (entry?.access && !(entry.expires && Date.now() >= entry.expires)) token = entry.access;
195
378
  }
196
- });
197
- const usage = createMemo(() => {
198
- const lastAssistant = [...messages()].reverse().find((m) => {
199
- return (m?.role ?? m?.info?.role) === "assistant" && safeNum(m?.tokens?.output) > 0;
379
+ if (!token) return null;
380
+ const data = await fetchJson("https://api.anthropic.com/api/oauth/usage", {
381
+ authorization: `Bearer ${token}`,
382
+ "anthropic-beta": "oauth-2025-04-20"
200
383
  });
201
- if (!lastAssistant) return null;
202
- const tokens = msgTokens(lastAssistant);
203
- const pid = lastAssistant?.providerID ?? lastAssistant?.info?.providerID;
204
- const mid = lastAssistant?.modelID ?? lastAssistant?.info?.modelID;
205
- const ctxLimit = safeNum((pid ? (props.api.state.provider ?? []).find((p) => p.id === pid) : null)?.models?.[mid ?? ""]?.limit?.context);
206
- return {
207
- tokens,
208
- limit: ctxLimit > 0 ? ctxLimit : 2e5,
209
- percent: ctxLimit > 0 ? Math.round(tokens / ctxLimit * 100) : 0
384
+ if (!data || !Array.isArray(data.limits)) return null;
385
+ const windows = [];
386
+ for (const limit of data.limits) {
387
+ if (!limit || typeof limit.percent !== "number" || !Number.isFinite(limit.percent)) continue;
388
+ if (!limit.kind || !limit.resets_at) continue;
389
+ const resetsAt = Date.parse(limit.resets_at);
390
+ if (Number.isNaN(resetsAt)) continue;
391
+ const category = limit.kind === "session" ? "5h" : limit.kind === "weekly_all" ? "7d" : "model";
392
+ const label = category === "model" ? limit.scope?.model?.display_name ?? "model" : category;
393
+ windows.push({
394
+ category,
395
+ label,
396
+ percent: limit.percent,
397
+ resetsAt
398
+ });
399
+ }
400
+ windows.sort((a, b) => Number(b.category === "5h") - Number(a.category === "5h"));
401
+ return windows.length > 0 ? windows : null;
402
+ }
403
+ };
404
+ /** ChatGPT Plus/Pro (Codex) — reads the Codex CLI login and asks the wham
405
+ * usage endpoint. Read-only: never refreshes/rewrites auth.json; when the
406
+ * token is expired we simply hide (Codex CLI refreshes the file itself). */
407
+ const openaiProvider = {
408
+ id: "openai",
409
+ short: "oai",
410
+ statusUrl: "https://status.openai.com/api/v2/status.json",
411
+ async fetchUsage(cfg) {
412
+ let accessToken;
413
+ let accountId;
414
+ try {
415
+ const path = expandTilde(cfg.codexAuthPath ?? join(homedir(), ".codex", "auth.json"));
416
+ const tokens = JSON.parse(await readFile(path, "utf8")).tokens;
417
+ const exp = tokens?.access_token ? jwtExp(tokens.access_token) : 0;
418
+ if (tokens?.access_token && !(exp > 0 && exp * 1e3 <= Date.now() + 6e4)) {
419
+ accessToken = tokens.access_token;
420
+ accountId = tokens.account_id;
421
+ }
422
+ } catch {}
423
+ if (!accessToken) {
424
+ const entry = await opencodeAuth("openai");
425
+ if (entry?.access && !(entry.expires && Date.now() >= entry.expires)) {
426
+ accessToken = entry.access;
427
+ accountId = entry.accountId;
428
+ }
429
+ }
430
+ if (!accessToken) return null;
431
+ const headers = {
432
+ authorization: `Bearer ${accessToken}`,
433
+ "user-agent": "codex-cli"
210
434
  };
211
- });
212
- const totalCost = createMemo(() => {
213
- const state = props.api.state.session?.get?.(props.sessionID);
214
- const fromState = msgCost(state);
215
- if (fromState > 0) return fromState;
216
- return messages().reduce((sum, m) => sum + msgCost(m), 0);
217
- });
218
- const theme = () => props.api.theme.current;
219
- return createComponent(Show, {
220
- get when() {
221
- return usage();
222
- },
223
- children: (u) => {
224
- const pct = u().percent;
225
- const c = theme();
226
- const barColor = pct >= 90 ? c.error : pct >= 70 ? c.warning : c.accent;
227
- const costStr = fmtCost(totalCost());
228
- return (() => {
229
- var _el$ = createElement("box"), _el$2 = createElement("box"), _el$3 = createElement("text"), _el$5 = createElement("text"), _el$6 = createElement("text"), _el$7 = createElement("text");
230
- insertNode(_el$, _el$2);
231
- insertNode(_el$, _el$7);
232
- setProp(_el$, "flexDirection", "column");
233
- insertNode(_el$2, _el$3);
234
- insertNode(_el$2, _el$5);
235
- insertNode(_el$2, _el$6);
236
- setProp(_el$2, "flexDirection", "row");
237
- setProp(_el$2, "gap", 1);
238
- insertNode(_el$3, createTextNode(`Context`));
239
- setProp(_el$3, "attributes", { bold: true });
240
- setProp(_el$5, "fg", barColor);
241
- insert(_el$5, () => buildBar(pct));
242
- setProp(_el$6, "fg", barColor);
243
- insert(_el$6, `${pct}%`);
244
- insert(_el$7, () => `${fmtInt(u().tokens)} / ${fmtInt(u().limit)} tok${costStr ? ` ${costStr}` : ""}`);
245
- effect((_p$) => {
246
- var _v$ = c.text, _v$2 = c.textMuted;
247
- _v$ !== _p$.e && (_p$.e = setProp(_el$3, "fg", _v$, _p$.e));
248
- _v$2 !== _p$.t && (_p$.t = setProp(_el$7, "fg", _v$2, _p$.t));
249
- return _p$;
250
- }, {
251
- e: void 0,
252
- t: void 0
435
+ if (accountId) headers["chatgpt-account-id"] = accountId;
436
+ const data = await fetchJson("https://chatgpt.com/backend-api/wham/usage", headers);
437
+ if (!data?.rate_limit) return null;
438
+ const windows = [];
439
+ const primary = parseWhamWindow(data.rate_limit.primary_window, "5h");
440
+ if (primary) windows.push(primary);
441
+ const secondary = parseWhamWindow(data.rate_limit.secondary_window, "7d");
442
+ if (secondary) windows.push(secondary);
443
+ windows.sort((a, b) => Number(b.category === "5h") - Number(a.category === "5h"));
444
+ return windows.length > 0 ? windows : null;
445
+ }
446
+ };
447
+ function parseWhamWindow(w, fallback) {
448
+ if (!w || typeof w.used_percent !== "number" || !Number.isFinite(w.used_percent)) return null;
449
+ let resetsAt;
450
+ if (typeof w.reset_at === "number") resetsAt = w.reset_at * 1e3;
451
+ else if (typeof w.reset_after_seconds === "number") resetsAt = Date.now() + w.reset_after_seconds * 1e3;
452
+ if (!resetsAt || !Number.isFinite(resetsAt)) return null;
453
+ const category = typeof w.limit_window_seconds === "number" && w.limit_window_seconds > 0 ? w.limit_window_seconds <= 21600 ? "5h" : "7d" : fallback;
454
+ return {
455
+ category,
456
+ label: category,
457
+ percent: w.used_percent,
458
+ resetsAt
459
+ };
460
+ }
461
+ const providers = [
462
+ anthropicProvider,
463
+ openaiProvider,
464
+ {
465
+ id: "opencodego",
466
+ short: "go",
467
+ async fetchUsage(_cfg) {
468
+ const token = process.env.PANTHEON_OPENCODE_API_KEY ?? process.env.OPENCODE_API_KEY;
469
+ if (!token) return null;
470
+ const data = await fetchJson("https://opencode.ai/zen/go/v1/usage", { authorization: `Bearer ${token}` });
471
+ if (!data) return null;
472
+ const windows = [];
473
+ const push = (w, category) => {
474
+ if (!w) return;
475
+ if (typeof w.usagePercent !== "number" || !Number.isFinite(w.usagePercent)) return;
476
+ if (typeof w.resetInSec !== "number" || !Number.isFinite(w.resetInSec)) return;
477
+ windows.push({
478
+ category,
479
+ label: category,
480
+ percent: Math.round(w.usagePercent),
481
+ resetsAt: Date.now() + w.resetInSec * 1e3
253
482
  });
254
- return _el$;
255
- })();
483
+ };
484
+ push(data.rolling5h, "5h");
485
+ push(data.weekly, "7d");
486
+ push(data.monthly, "1m");
487
+ return windows.length > 0 ? windows : null;
256
488
  }
489
+ }
490
+ ];
491
+ const DEFAULT_TOML = `# opencode-usage-bar configuration
492
+ # Read once at startup — restart opencode after editing.
493
+
494
+ [ui]
495
+ show_bars = true # render ▓▓░░ mini-bars (false = text only)
496
+ show_status = true # show a ! marker next to a provider during incidents
497
+ # bar_width = 6 # override bar width (default: 6 for a single window, 5 otherwise)
498
+
499
+ [anthropic]
500
+ enabled = true # Claude Pro/Max via ~/.claude/.credentials.json
501
+ show_5h = true # rolling 5-hour session window
502
+ show_7d = false # weekly cap across all models
503
+ show_model = false # per-model weekly windows (e.g. Fable)
504
+ # credentials_path = "~/.claude/.credentials.json"
505
+
506
+ [openai]
507
+ enabled = false # ChatGPT Plus/Pro via the Codex CLI login
508
+ show_5h = true
509
+ show_7d = false
510
+ # codex_auth_path = "~/.codex/auth.json"
511
+
512
+ [opencodego]
513
+ enabled = true # OpenCode Go/Zen dollar usage via PANTHEON_OPENCODE_API_KEY
514
+ show_5h = true # rolling 5-hour window ($12)
515
+ show_7d = true # rolling 7-day window ($30)
516
+ show_1m = false # subscription-month window ($60) — opt-in
517
+ # Falls back to OPENCODE_API_KEY when PANTHEON_OPENCODE_API_KEY is unset.
518
+ `;
519
+ function defaultConfig() {
520
+ const show = (over = {}) => ({
521
+ "5h": true,
522
+ "7d": false,
523
+ "1m": false,
524
+ model: false,
525
+ ...over
526
+ });
527
+ return {
528
+ showBars: true,
529
+ showStatus: true,
530
+ providers: {
531
+ anthropic: {
532
+ enabled: true,
533
+ show: show()
534
+ },
535
+ openai: {
536
+ enabled: false,
537
+ show: show()
538
+ },
539
+ opencodego: {
540
+ enabled: true,
541
+ show: show({ "7d": true })
542
+ }
543
+ }
544
+ };
545
+ }
546
+ function asTable(v) {
547
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
548
+ }
549
+ function bool(v, fallback) {
550
+ return typeof v === "boolean" ? v : fallback;
551
+ }
552
+ function str(v) {
553
+ return typeof v === "string" && v.length > 0 ? v : void 0;
554
+ }
555
+ function parseConfig(raw) {
556
+ const toml = globalThis.Bun?.TOML;
557
+ if (!toml?.parse) return defaultConfig();
558
+ const root = asTable(toml.parse(raw));
559
+ const cfg = defaultConfig();
560
+ const ui = asTable(root["ui"]);
561
+ cfg.showBars = bool(ui["show_bars"], cfg.showBars);
562
+ cfg.showStatus = bool(ui["show_status"], cfg.showStatus);
563
+ const rawWidth = ui["bar_width"];
564
+ if (typeof rawWidth === "number" && Number.isFinite(rawWidth) && rawWidth >= 1) cfg.barWidth = Math.min(40, Math.floor(rawWidth));
565
+ for (const id of [
566
+ "anthropic",
567
+ "openai",
568
+ "opencodego"
569
+ ]) {
570
+ const t = asTable(root[id]);
571
+ const p = cfg.providers[id];
572
+ p.enabled = bool(t["enabled"], p.enabled);
573
+ p.show["5h"] = bool(t["show_5h"], p.show["5h"]);
574
+ p.show["7d"] = bool(t["show_7d"], p.show["7d"]);
575
+ p.show["1m"] = bool(t["show_1m"], p.show["1m"]);
576
+ p.show.model = bool(t["show_model"], p.show.model);
577
+ const credentialsPath = str(t["credentials_path"]);
578
+ if (credentialsPath !== void 0) p.credentialsPath = credentialsPath;
579
+ const codexAuthPath = str(t["codex_auth_path"]);
580
+ if (codexAuthPath !== void 0) p.codexAuthPath = codexAuthPath;
581
+ }
582
+ return cfg;
583
+ }
584
+ /** Load `<config dir>/usage-bar.toml`, creating it with commented defaults on
585
+ * first run. Any failure falls back to defaults without touching an existing
586
+ * file. */
587
+ async function loadConfig(configPath) {
588
+ const dir = configPath && !configPath.endsWith(".json") ? configPath : configPath ? dirname(configPath) : join(homedir(), ".config", "opencode");
589
+ const file = join(dir, CONFIG_FILE);
590
+ try {
591
+ const raw = await readFile(file, "utf8");
592
+ try {
593
+ return parseConfig(raw);
594
+ } catch {
595
+ return defaultConfig();
596
+ }
597
+ } catch (err) {
598
+ if (err?.code === "ENOENT") try {
599
+ await mkdir(dir, { recursive: true });
600
+ await writeFile(file, DEFAULT_TOML, { flag: "wx" });
601
+ } catch {}
602
+ return defaultConfig();
603
+ }
604
+ }
605
+ /** Vendored usage-bar plugin init: reads config, seeds the kv cache, starts
606
+ * the poll loop and registers the `app_bottom` slot. Skips everything (and
607
+ * registers nothing) when no provider is enabled — mirrors the standalone
608
+ * plugin's early return. */
609
+ async function setupUsageBar(api) {
610
+ const config = await loadConfig(api.state.path?.config);
611
+ if (api.state.path?.state) opencodeAuthFile = join(api.state.path.state, "auth.json");
612
+ const enabled = providers.filter((p) => config.providers[p.id].enabled);
613
+ if (enabled.length === 0) return;
614
+ const seed = {};
615
+ for (const p of enabled) {
616
+ const cached = api.kv.get(`usage-bar.${p.id}.windows`, void 0);
617
+ if (cached) seed[p.id] = cached;
618
+ }
619
+ const [byProvider, setByProvider] = createSignal(seed);
620
+ const [byStatus, setByStatus] = createSignal({});
621
+ const [now, setNow] = createSignal(Date.now());
622
+ setInterval(() => setNow(Date.now()), 1e3);
623
+ for (const p of enabled) {
624
+ const cfg = config.providers[p.id];
625
+ const poll = async () => {
626
+ const statusP = config.showStatus && p.statusUrl ? fetchStatus(p.statusUrl) : null;
627
+ const [all, status] = await Promise.all([p.fetchUsage(cfg), statusP]);
628
+ if (all) {
629
+ const windows = all.filter((w) => cfg.show[w.category] && w.resetsAt > Date.now());
630
+ setByProvider((prev) => ({
631
+ ...prev,
632
+ [p.id]: windows
633
+ }));
634
+ api.kv.set(`usage-bar.${p.id}.windows`, windows);
635
+ }
636
+ if (status !== null) setByStatus((prev) => ({
637
+ ...prev,
638
+ [p.id]: status
639
+ }));
640
+ setTimeout(poll, all ? POLL_MS : POLL_MS * 3);
641
+ };
642
+ poll();
643
+ }
644
+ api.slots.register({
645
+ order: 60,
646
+ slots: { app_bottom() {
647
+ const theme = () => api.theme.current;
648
+ const groups = createMemo(() => {
649
+ const map = byProvider();
650
+ const statusMap = byStatus();
651
+ const out = [];
652
+ for (const p of enabled) {
653
+ const windows = (map[p.id] ?? []).filter((w) => w.resetsAt > now());
654
+ if (windows.length > 0) out.push({
655
+ short: p.short,
656
+ status: statusMap[p.id] ?? "none",
657
+ windows
658
+ });
659
+ }
660
+ return out;
661
+ });
662
+ const totalWindows = createMemo(() => groups().reduce((sum, g) => sum + g.windows.length, 0));
663
+ const barWidth = createMemo(() => config.barWidth ?? (totalWindows() === 1 ? 6 : 5));
664
+ const multiProvider = createMemo(() => groups().length >= 2);
665
+ const pctOf = (w) => Math.min(100, Math.max(0, Math.round(w.percent)));
666
+ const filledOf = (w) => Math.min(barWidth(), Math.max(0, Math.round(pctOf(w) / 100 * barWidth())));
667
+ const colorOf = (w) => {
668
+ const t = theme();
669
+ const pct = pctOf(w);
670
+ if (pct > 85) return t.error;
671
+ if (pct >= 50) return t.warning;
672
+ return t.success;
673
+ };
674
+ const statusColor = (s) => {
675
+ const t = theme();
676
+ if (s === "critical" || s === "major") return t.error;
677
+ if (s === "minor") return t.warning;
678
+ return t.info;
679
+ };
680
+ return createComponent(Show, {
681
+ get when() {
682
+ return groups().length > 0;
683
+ },
684
+ get children() {
685
+ var _el$ = createElement("box");
686
+ setProp(_el$, "flexDirection", "row");
687
+ setProp(_el$, "gap", 3);
688
+ setProp(_el$, "alignItems", "center");
689
+ setProp(_el$, "width", "100%");
690
+ setProp(_el$, "paddingLeft", 1);
691
+ insert(_el$, createComponent(For, {
692
+ get each() {
693
+ return groups();
694
+ },
695
+ children: (g) => (() => {
696
+ var _el$2 = createElement("box");
697
+ setProp(_el$2, "flexDirection", "row");
698
+ setProp(_el$2, "gap", 2);
699
+ setProp(_el$2, "alignItems", "center");
700
+ setProp(_el$2, "flexShrink", 0);
701
+ insert(_el$2, createComponent(Show, {
702
+ get when() {
703
+ return memo(() => !!config.showStatus)() && g.status !== "none";
704
+ },
705
+ get children() {
706
+ var _el$3 = createElement("text");
707
+ insertNode(_el$3, createTextNode(`!`));
708
+ effect((_$p) => setProp(_el$3, "fg", statusColor(g.status), _$p));
709
+ return _el$3;
710
+ }
711
+ }), null);
712
+ insert(_el$2, createComponent(Show, {
713
+ get when() {
714
+ return multiProvider();
715
+ },
716
+ get children() {
717
+ var _el$5 = createElement("text");
718
+ insert(_el$5, () => g.short);
719
+ effect((_$p) => setProp(_el$5, "fg", theme().textMuted, _$p));
720
+ return _el$5;
721
+ }
722
+ }), null);
723
+ insert(_el$2, createComponent(For, {
724
+ get each() {
725
+ return g.windows;
726
+ },
727
+ children: (w) => (() => {
728
+ var _el$6 = createElement("box"), _el$1 = createElement("text"), _el$10 = createTextNode(`%`), _el$11 = createElement("text");
729
+ insertNode(_el$6, _el$1);
730
+ insertNode(_el$6, _el$11);
731
+ setProp(_el$6, "flexDirection", "row");
732
+ setProp(_el$6, "gap", 1);
733
+ setProp(_el$6, "alignItems", "center");
734
+ setProp(_el$6, "flexShrink", 0);
735
+ insert(_el$6, createComponent(Show, {
736
+ get when() {
737
+ return g.windows.length >= 2;
738
+ },
739
+ get children() {
740
+ var _el$7 = createElement("text");
741
+ insert(_el$7, () => w.label);
742
+ effect((_$p) => setProp(_el$7, "fg", theme().textMuted, _$p));
743
+ return _el$7;
744
+ }
745
+ }), _el$1);
746
+ insert(_el$6, createComponent(Show, {
747
+ get when() {
748
+ return config.showBars;
749
+ },
750
+ get children() {
751
+ var _el$8 = createElement("box"), _el$9 = createElement("text"), _el$0 = createElement("text");
752
+ insertNode(_el$8, _el$9);
753
+ insertNode(_el$8, _el$0);
754
+ setProp(_el$8, "flexDirection", "row");
755
+ insert(_el$9, () => "▓".repeat(filledOf(w)));
756
+ insert(_el$0, () => "░".repeat(barWidth() - filledOf(w)));
757
+ effect((_p$) => {
758
+ var _v$ = colorOf(w), _v$2 = theme().textMuted;
759
+ _v$ !== _p$.e && (_p$.e = setProp(_el$9, "fg", _v$, _p$.e));
760
+ _v$2 !== _p$.t && (_p$.t = setProp(_el$0, "fg", _v$2, _p$.t));
761
+ return _p$;
762
+ }, {
763
+ e: void 0,
764
+ t: void 0
765
+ });
766
+ return _el$8;
767
+ }
768
+ }), _el$1);
769
+ insertNode(_el$1, _el$10);
770
+ insert(_el$1, () => pctOf(w), _el$10);
771
+ insert(_el$11, () => `· ${fmtDuration(w.resetsAt - now())}`);
772
+ effect((_p$) => {
773
+ var _v$3 = theme().text, _v$4 = theme().textMuted;
774
+ _v$3 !== _p$.e && (_p$.e = setProp(_el$1, "fg", _v$3, _p$.e));
775
+ _v$4 !== _p$.t && (_p$.t = setProp(_el$11, "fg", _v$4, _p$.t));
776
+ return _p$;
777
+ }, {
778
+ e: void 0,
779
+ t: void 0
780
+ });
781
+ return _el$6;
782
+ })()
783
+ }), null);
784
+ return _el$2;
785
+ })()
786
+ }));
787
+ return _el$;
788
+ }
789
+ });
790
+ } }
257
791
  });
258
792
  }
259
793
  function SessionRow(props) {
@@ -273,10 +807,10 @@ function SessionRow(props) {
273
807
  }
274
808
  });
275
809
  return (() => {
276
- var _el$8 = createElement("text");
277
- insert(_el$8, () => `${statusIcon()}${props.session?.id?.slice(0, 8) ?? "????"}... ${props.session?.title?.slice(0, 26) ?? "(untitled)"}`);
278
- effect((_$p) => setProp(_el$8, "fg", theme().textMuted, _$p));
279
- return _el$8;
810
+ var _el$12 = createElement("text");
811
+ insert(_el$12, () => `${statusIcon()}${props.session?.id?.slice(0, 8) ?? "????"}... ${props.session?.title?.slice(0, 26) ?? "(untitled)"}`);
812
+ effect((_$p) => setProp(_el$12, "fg", theme().textMuted, _$p));
813
+ return _el$12;
280
814
  })();
281
815
  }
282
816
  function View(props) {
@@ -286,6 +820,23 @@ function View(props) {
286
820
  const [showSessions, setShowSessions] = createSignal(false);
287
821
  const theme = () => props.api.theme.current;
288
822
  const branch = createMemo(() => props.api.state.vcs?.branch ? `\u2387 ${props.api.state.vcs.branch}` : null);
823
+ const [preset, setPreset] = createSignal(presetFromEnv(process.env));
824
+ onMount(() => {
825
+ const cwd = (props.api.state.path?.worktree ?? "") || process.cwd();
826
+ let cancelled = false;
827
+ const refresh = async () => {
828
+ try {
829
+ const info = await resolvePresetForTui(process.env, cwd);
830
+ if (!cancelled) setPreset(info);
831
+ } catch {}
832
+ };
833
+ refresh();
834
+ const timer = setInterval(() => void refresh(), PRESET_REFRESH_MS);
835
+ onCleanup(() => {
836
+ cancelled = true;
837
+ clearInterval(timer);
838
+ });
839
+ });
289
840
  const [sessionList, { refetch: refetchSessions }] = createResource(async () => {
290
841
  try {
291
842
  return await props.api.client?.session?.list?.({ limit: 100 }) ?? { data: [] };
@@ -326,7 +877,9 @@ function View(props) {
326
877
  cleanup.push(ev.on("memory.deleted", () => setMemoryCount((c) => c !== null ? Math.max(0, c - 1) : 0)));
327
878
  }
328
879
  } catch {}
329
- onCleanup(() => cleanup.forEach((fn) => fn()));
880
+ onCleanup(() => cleanup.forEach((fn) => {
881
+ fn();
882
+ }));
330
883
  });
331
884
  const configSummary = createMemo(() => {
332
885
  const cfg = props.api.state.config;
@@ -338,51 +891,81 @@ function View(props) {
338
891
  };
339
892
  });
340
893
  const HR = () => (() => {
341
- var _el$9 = createElement("text");
342
- insertNode(_el$9, createTextNode(`────────────────────────────`));
343
- effect((_$p) => setProp(_el$9, "fg", theme().textMuted, _$p));
344
- return _el$9;
894
+ var _el$13 = createElement("text");
895
+ insertNode(_el$13, createTextNode(`────────────────────────────`));
896
+ effect((_$p) => setProp(_el$13, "fg", theme().textMuted, _$p));
897
+ return _el$13;
345
898
  })();
346
899
  return (() => {
347
- var _el$1 = createElement("box"), _el$10 = createElement("text"), _el$11 = createElement("box"), _el$12 = createElement("text"), _el$13 = createElement("text"), _el$15 = createElement("box"), _el$16 = createElement("text"), _el$17 = createElement("text"), _el$19 = createElement("box"), _el$20 = createElement("text"), _el$21 = createElement("text"), _el$23 = createElement("box"), _el$24 = createElement("text"), _el$25 = createElement("box"), _el$26 = createElement("text");
348
- insertNode(_el$1, _el$10);
349
- insertNode(_el$1, _el$11);
350
- insertNode(_el$1, _el$15);
351
- insertNode(_el$1, _el$19);
352
- insertNode(_el$1, _el$23);
353
- insertNode(_el$1, _el$25);
354
- setProp(_el$1, "flexDirection", "column");
355
- setProp(_el$1, "width", "100%");
356
- setProp(_el$10, "attributes", { bold: true });
357
- insert(_el$10, () => `Pantheon${props.version ? ` v${props.version}` : ""}`);
358
- insert(_el$1, createComponent(Show, {
900
+ var _el$15 = createElement("box"), _el$16 = createElement("text"), _el$17 = createElement("box"), _el$18 = createElement("text"), _el$19 = createElement("text"), _el$21 = createElement("box"), _el$22 = createElement("text"), _el$23 = createElement("text"), _el$25 = createElement("box"), _el$26 = createElement("text"), _el$27 = createElement("text"), _el$29 = createElement("box"), _el$30 = createElement("text"), _el$31 = createElement("box"), _el$32 = createElement("text");
901
+ insertNode(_el$15, _el$16);
902
+ insertNode(_el$15, _el$17);
903
+ insertNode(_el$15, _el$21);
904
+ insertNode(_el$15, _el$25);
905
+ insertNode(_el$15, _el$29);
906
+ insertNode(_el$15, _el$31);
907
+ setProp(_el$15, "flexDirection", "column");
908
+ setProp(_el$15, "width", "100%");
909
+ setProp(_el$16, "attributes", 1);
910
+ insert(_el$16, () => `Pantheon${props.version ? ` v${props.version}` : ""}`);
911
+ insert(_el$15, createComponent(Show, {
359
912
  get when() {
360
913
  return branch();
361
914
  },
362
915
  children: (b) => (() => {
363
- var _el$27 = createElement("text");
364
- insert(_el$27, b);
365
- effect((_$p) => setProp(_el$27, "fg", theme().textMuted, _$p));
366
- return _el$27;
916
+ var _el$33 = createElement("text");
917
+ insert(_el$33, b);
918
+ effect((_$p) => setProp(_el$33, "fg", theme().textMuted, _$p));
919
+ return _el$33;
367
920
  })()
368
- }), _el$11);
369
- insert(_el$1, createComponent(HR, {}), _el$11);
370
- insert(_el$1, createComponent(ContextBar, {
371
- get api() {
372
- return props.api;
921
+ }), _el$17);
922
+ insert(_el$15, createComponent(Show, {
923
+ get when() {
924
+ return preset().name;
373
925
  },
374
- get sessionID() {
375
- return props.sessionID;
376
- }
377
- }), _el$11);
378
- insert(_el$1, createComponent(HR, {}), _el$11);
379
- insertNode(_el$11, _el$12);
380
- insertNode(_el$11, _el$13);
381
- setProp(_el$11, "onMouseDown", () => setShowSessions((x) => !x));
382
- setProp(_el$12, "attributes", { bold: true });
383
- insert(_el$12, () => `${showSessions() ? "▼" : "▶"} Sessions`);
384
- insert(_el$13, () => ` (${String(totalSessions())})`);
385
- insert(_el$1, createComponent(Show, {
926
+ get fallback() {
927
+ return (() => {
928
+ var _el$34 = createElement("box"), _el$35 = createElement("text");
929
+ insertNode(_el$34, _el$35);
930
+ setProp(_el$34, "flexDirection", "row");
931
+ setProp(_el$34, "gap", 1);
932
+ insertNode(_el$35, createTextNode(`Preset: default`));
933
+ effect((_$p) => setProp(_el$35, "fg", theme().textMuted, _$p));
934
+ return _el$34;
935
+ })();
936
+ },
937
+ children: (name) => (() => {
938
+ var _el$37 = createElement("box"), _el$38 = createElement("text"), _el$40 = createElement("text"), _el$41 = createElement("text");
939
+ insertNode(_el$37, _el$38);
940
+ insertNode(_el$37, _el$40);
941
+ insertNode(_el$37, _el$41);
942
+ setProp(_el$37, "flexDirection", "row");
943
+ setProp(_el$37, "gap", 1);
944
+ insertNode(_el$38, createTextNode(`⚡ Preset:`));
945
+ insert(_el$40, name);
946
+ insert(_el$41, () => `(${preset().source ?? ""})`);
947
+ effect((_p$) => {
948
+ var _v$12 = theme().textMuted, _v$13 = theme().accent, _v$14 = theme().textMuted;
949
+ _v$12 !== _p$.e && (_p$.e = setProp(_el$38, "fg", _v$12, _p$.e));
950
+ _v$13 !== _p$.t && (_p$.t = setProp(_el$40, "fg", _v$13, _p$.t));
951
+ _v$14 !== _p$.a && (_p$.a = setProp(_el$41, "fg", _v$14, _p$.a));
952
+ return _p$;
953
+ }, {
954
+ e: void 0,
955
+ t: void 0,
956
+ a: void 0
957
+ });
958
+ return _el$37;
959
+ })()
960
+ }), _el$17);
961
+ insert(_el$15, createComponent(HR, {}), _el$17);
962
+ insertNode(_el$17, _el$18);
963
+ insertNode(_el$17, _el$19);
964
+ setProp(_el$17, "onMouseDown", () => setShowSessions((x) => !x));
965
+ setProp(_el$18, "attributes", 1);
966
+ insert(_el$18, () => `${showSessions() ? "▼" : "▶"} Sessions`);
967
+ insert(_el$19, () => ` (${String(totalSessions())})`);
968
+ insert(_el$15, createComponent(Show, {
386
969
  get when() {
387
970
  return showSessions();
388
971
  },
@@ -393,19 +976,19 @@ function View(props) {
393
976
  },
394
977
  get fallback() {
395
978
  return (() => {
396
- var _el$28 = createElement("box"), _el$29 = createElement("text");
397
- insertNode(_el$28, _el$29);
398
- setProp(_el$28, "marginLeft", 1);
399
- insertNode(_el$29, createTextNode(`No recent sessions`));
400
- effect((_$p) => setProp(_el$29, "fg", theme().textMuted, _$p));
401
- return _el$28;
979
+ var _el$42 = createElement("box"), _el$43 = createElement("text");
980
+ insertNode(_el$42, _el$43);
981
+ setProp(_el$42, "marginLeft", 1);
982
+ insertNode(_el$43, createTextNode(`No recent sessions`));
983
+ effect((_$p) => setProp(_el$43, "fg", theme().textMuted, _$p));
984
+ return _el$42;
402
985
  })();
403
986
  },
404
987
  get children() {
405
- var _el$14 = createElement("box");
406
- setProp(_el$14, "marginLeft", 1);
407
- setProp(_el$14, "flexDirection", "column");
408
- insert(_el$14, createComponent(For, {
988
+ var _el$20 = createElement("box");
989
+ setProp(_el$20, "marginLeft", 1);
990
+ setProp(_el$20, "flexDirection", "column");
991
+ insert(_el$20, createComponent(For, {
409
992
  get each() {
410
993
  return recentSessions();
411
994
  },
@@ -416,32 +999,32 @@ function View(props) {
416
999
  session: ses
417
1000
  })
418
1001
  }));
419
- return _el$14;
1002
+ return _el$20;
420
1003
  }
421
1004
  });
422
1005
  }
423
- }), _el$15);
424
- insertNode(_el$15, _el$16);
425
- insertNode(_el$15, _el$17);
426
- setProp(_el$15, "onMouseDown", () => setShowCommands((x) => !x));
427
- setProp(_el$16, "attributes", { bold: true });
428
- insert(_el$16, () => `${showCommands() ? "▼" : "▶"} Commands`);
429
- insert(_el$17, () => ` (${String(COMMANDS.length)})`);
430
- insert(_el$1, createComponent(Show, {
1006
+ }), _el$21);
1007
+ insertNode(_el$21, _el$22);
1008
+ insertNode(_el$21, _el$23);
1009
+ setProp(_el$21, "onMouseDown", () => setShowCommands((x) => !x));
1010
+ setProp(_el$22, "attributes", 1);
1011
+ insert(_el$22, () => `${showCommands() ? "▼" : "▶"} Commands`);
1012
+ insert(_el$23, () => ` (${String(COMMANDS.length)})`);
1013
+ insert(_el$15, createComponent(Show, {
431
1014
  get when() {
432
1015
  return showCommands();
433
1016
  },
434
1017
  get children() {
435
- var _el$18 = createElement("box");
436
- setProp(_el$18, "marginLeft", 1);
437
- setProp(_el$18, "flexDirection", "column");
438
- insert(_el$18, createComponent(For, {
1018
+ var _el$24 = createElement("box");
1019
+ setProp(_el$24, "marginLeft", 1);
1020
+ setProp(_el$24, "flexDirection", "column");
1021
+ insert(_el$24, createComponent(For, {
439
1022
  each: COMMANDS,
440
1023
  children: (cmd) => (() => {
441
- var _el$31 = createElement("box"), _el$32 = createElement("text"), _el$33 = createElement("text");
442
- insertNode(_el$31, _el$32);
443
- insertNode(_el$31, _el$33);
444
- setProp(_el$31, "onMouseDown", (e) => {
1024
+ var _el$45 = createElement("box"), _el$46 = createElement("text"), _el$47 = createElement("text");
1025
+ insertNode(_el$45, _el$46);
1026
+ insertNode(_el$45, _el$47);
1027
+ setProp(_el$45, "onMouseDown", (e) => {
445
1028
  e.stopPropagation();
446
1029
  try {
447
1030
  const cmdApi = props.api.command;
@@ -453,65 +1036,65 @@ function View(props) {
453
1036
  message: `Type ${cmd.name} in chat`
454
1037
  });
455
1038
  });
456
- insert(_el$32, () => cmd.name);
457
- insert(_el$33, () => ` \u2014 ${cmd.desc}`);
1039
+ insert(_el$46, () => cmd.name);
1040
+ insert(_el$47, () => ` \u2014 ${cmd.desc}`);
458
1041
  effect((_p$) => {
459
- var _v$10 = cmd.name === "/pantheon" ? theme().accent : theme().textMuted, _v$11 = theme().textMuted;
460
- _v$10 !== _p$.e && (_p$.e = setProp(_el$32, "fg", _v$10, _p$.e));
461
- _v$11 !== _p$.t && (_p$.t = setProp(_el$33, "fg", _v$11, _p$.t));
1042
+ var _v$15 = cmd.name === "/pantheon" ? theme().accent : theme().textMuted, _v$16 = theme().textMuted;
1043
+ _v$15 !== _p$.e && (_p$.e = setProp(_el$46, "fg", _v$15, _p$.e));
1044
+ _v$16 !== _p$.t && (_p$.t = setProp(_el$47, "fg", _v$16, _p$.t));
462
1045
  return _p$;
463
1046
  }, {
464
1047
  e: void 0,
465
1048
  t: void 0
466
1049
  });
467
- return _el$31;
1050
+ return _el$45;
468
1051
  })()
469
1052
  }));
470
- return _el$18;
1053
+ return _el$24;
471
1054
  }
472
- }), _el$19);
473
- insertNode(_el$19, _el$20);
474
- insertNode(_el$19, _el$21);
475
- setProp(_el$19, "onMouseDown", () => setShowAgents((x) => !x));
476
- setProp(_el$20, "attributes", { bold: true });
477
- insert(_el$20, () => `${showAgents() ? "▼" : "▶"} Agents`);
478
- insert(_el$21, () => ` (${String(AGENTS.length)})`);
479
- insert(_el$1, createComponent(Show, {
1055
+ }), _el$25);
1056
+ insertNode(_el$25, _el$26);
1057
+ insertNode(_el$25, _el$27);
1058
+ setProp(_el$25, "onMouseDown", () => setShowAgents((x) => !x));
1059
+ setProp(_el$26, "attributes", 1);
1060
+ insert(_el$26, () => `${showAgents() ? "▼" : "▶"} Agents`);
1061
+ insert(_el$27, () => ` (${String(AGENTS.length)})`);
1062
+ insert(_el$15, createComponent(Show, {
480
1063
  get when() {
481
1064
  return showAgents();
482
1065
  },
483
1066
  get children() {
484
- var _el$22 = createElement("box");
485
- setProp(_el$22, "marginLeft", 1);
486
- setProp(_el$22, "flexDirection", "column");
487
- insert(_el$22, createComponent(For, {
1067
+ var _el$28 = createElement("box");
1068
+ setProp(_el$28, "marginLeft", 1);
1069
+ setProp(_el$28, "flexDirection", "column");
1070
+ insert(_el$28, createComponent(For, {
488
1071
  each: AGENTS,
489
1072
  children: (agent) => (() => {
490
- var _el$34 = createElement("box"), _el$35 = createElement("text"), _el$36 = createElement("text");
491
- insertNode(_el$34, _el$35);
492
- insertNode(_el$34, _el$36);
493
- insert(_el$35, () => `${agent.tier === "premium" ? "✦ " : "· "}${agent.name}`);
494
- insert(_el$36, () => ` \u2014 ${agent.role}`);
1073
+ var _el$48 = createElement("box"), _el$49 = createElement("text"), _el$50 = createElement("text");
1074
+ insertNode(_el$48, _el$49);
1075
+ insertNode(_el$48, _el$50);
1076
+ insert(_el$49, () => `${agent.tier === "premium" ? "✦ " : "· "}${agent.name}`);
1077
+ insert(_el$50, () => ` \u2014 ${agent.role}`);
495
1078
  effect((_p$) => {
496
- var _v$12 = agent.tier === "premium" ? theme().accent : theme().textMuted, _v$13 = theme().textMuted;
497
- _v$12 !== _p$.e && (_p$.e = setProp(_el$35, "fg", _v$12, _p$.e));
498
- _v$13 !== _p$.t && (_p$.t = setProp(_el$36, "fg", _v$13, _p$.t));
1079
+ var _v$17 = agent.tier === "premium" ? theme().accent : theme().textMuted, _v$18 = theme().textMuted;
1080
+ _v$17 !== _p$.e && (_p$.e = setProp(_el$49, "fg", _v$17, _p$.e));
1081
+ _v$18 !== _p$.t && (_p$.t = setProp(_el$50, "fg", _v$18, _p$.t));
499
1082
  return _p$;
500
1083
  }, {
501
1084
  e: void 0,
502
1085
  t: void 0
503
1086
  });
504
- return _el$34;
1087
+ return _el$48;
505
1088
  })()
506
1089
  }));
507
- return _el$22;
1090
+ return _el$28;
508
1091
  }
509
- }), _el$23);
510
- insertNode(_el$23, _el$24);
511
- setProp(_el$23, "onMouseDown", () => setShowConfig((x) => !x));
512
- setProp(_el$24, "attributes", { bold: true });
513
- insert(_el$24, () => `${showConfig() ? "▼" : "▶"} Config`);
514
- insert(_el$1, createComponent(Show, {
1092
+ }), _el$29);
1093
+ insertNode(_el$29, _el$30);
1094
+ setProp(_el$29, "onMouseDown", () => setShowConfig((x) => !x));
1095
+ setProp(_el$30, "attributes", 1);
1096
+ insert(_el$30, () => `${showConfig() ? "▼" : "▶"} Config`);
1097
+ insert(_el$15, createComponent(Show, {
515
1098
  get when() {
516
1099
  return showConfig();
517
1100
  },
@@ -522,53 +1105,53 @@ function View(props) {
522
1105
  },
523
1106
  get fallback() {
524
1107
  return (() => {
525
- var _el$37 = createElement("box"), _el$38 = createElement("text");
526
- insertNode(_el$37, _el$38);
527
- setProp(_el$37, "marginLeft", 1);
528
- insertNode(_el$38, createTextNode(`No config data`));
529
- effect((_$p) => setProp(_el$38, "fg", theme().textMuted, _$p));
530
- return _el$37;
1108
+ var _el$51 = createElement("box"), _el$52 = createElement("text");
1109
+ insertNode(_el$51, _el$52);
1110
+ setProp(_el$51, "marginLeft", 1);
1111
+ insertNode(_el$52, createTextNode(`No config data`));
1112
+ effect((_$p) => setProp(_el$52, "fg", theme().textMuted, _$p));
1113
+ return _el$51;
531
1114
  })();
532
1115
  },
533
1116
  children: (cfg) => (() => {
534
- var _el$40 = createElement("box"), _el$41 = createElement("text"), _el$42 = createElement("text");
535
- insertNode(_el$40, _el$41);
536
- insertNode(_el$40, _el$42);
537
- setProp(_el$40, "marginLeft", 1);
538
- setProp(_el$40, "flexDirection", "column");
539
- insert(_el$41, () => `MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`);
540
- insert(_el$42, () => `Auto-compaction: ${cfg().autoCompaction ? "ON" : "OFF"}`);
1117
+ var _el$54 = createElement("box"), _el$55 = createElement("text"), _el$56 = createElement("text");
1118
+ insertNode(_el$54, _el$55);
1119
+ insertNode(_el$54, _el$56);
1120
+ setProp(_el$54, "marginLeft", 1);
1121
+ setProp(_el$54, "flexDirection", "column");
1122
+ insert(_el$55, () => `MCP: ${String(cfg().mcpCount)} Plugins: ${String(cfg().pluginCount)}`);
1123
+ insert(_el$56, () => `Auto-compaction: ${cfg().autoCompaction ? "ON" : "OFF"}`);
541
1124
  effect((_p$) => {
542
- var _v$14 = theme().textMuted, _v$15 = theme().textMuted;
543
- _v$14 !== _p$.e && (_p$.e = setProp(_el$41, "fg", _v$14, _p$.e));
544
- _v$15 !== _p$.t && (_p$.t = setProp(_el$42, "fg", _v$15, _p$.t));
1125
+ var _v$19 = theme().textMuted, _v$20 = theme().textMuted;
1126
+ _v$19 !== _p$.e && (_p$.e = setProp(_el$55, "fg", _v$19, _p$.e));
1127
+ _v$20 !== _p$.t && (_p$.t = setProp(_el$56, "fg", _v$20, _p$.t));
545
1128
  return _p$;
546
1129
  }, {
547
1130
  e: void 0,
548
1131
  t: void 0
549
1132
  });
550
- return _el$40;
1133
+ return _el$54;
551
1134
  })()
552
1135
  });
553
1136
  }
554
- }), _el$25);
555
- insertNode(_el$25, _el$26);
556
- setProp(_el$25, "marginTop", 1);
557
- insert(_el$26, (() => {
1137
+ }), _el$31);
1138
+ insertNode(_el$31, _el$32);
1139
+ setProp(_el$31, "marginTop", 1);
1140
+ insert(_el$32, (() => {
558
1141
  var _c$ = memo(() => memoryCount() !== null);
559
1142
  return () => _c$() ? `Memory: ${fmtInt(memoryCount())} entries` : "Memory: N/A";
560
1143
  })());
561
1144
  effect((_p$) => {
562
- var _v$3 = theme().accent, _v$4 = theme().text, _v$5 = theme().textMuted, _v$6 = theme().text, _v$7 = theme().textMuted, _v$8 = theme().text, _v$9 = theme().textMuted, _v$0 = theme().text, _v$1 = theme().textMuted;
563
- _v$3 !== _p$.e && (_p$.e = setProp(_el$10, "fg", _v$3, _p$.e));
564
- _v$4 !== _p$.t && (_p$.t = setProp(_el$12, "fg", _v$4, _p$.t));
565
- _v$5 !== _p$.a && (_p$.a = setProp(_el$13, "fg", _v$5, _p$.a));
566
- _v$6 !== _p$.o && (_p$.o = setProp(_el$16, "fg", _v$6, _p$.o));
567
- _v$7 !== _p$.i && (_p$.i = setProp(_el$17, "fg", _v$7, _p$.i));
568
- _v$8 !== _p$.n && (_p$.n = setProp(_el$20, "fg", _v$8, _p$.n));
569
- _v$9 !== _p$.s && (_p$.s = setProp(_el$21, "fg", _v$9, _p$.s));
570
- _v$0 !== _p$.h && (_p$.h = setProp(_el$24, "fg", _v$0, _p$.h));
571
- _v$1 !== _p$.r && (_p$.r = setProp(_el$26, "fg", _v$1, _p$.r));
1145
+ var _v$5 = theme().accent, _v$6 = theme().text, _v$7 = theme().textMuted, _v$8 = theme().text, _v$9 = theme().textMuted, _v$0 = theme().text, _v$1 = theme().textMuted, _v$10 = theme().text, _v$11 = theme().textMuted;
1146
+ _v$5 !== _p$.e && (_p$.e = setProp(_el$16, "fg", _v$5, _p$.e));
1147
+ _v$6 !== _p$.t && (_p$.t = setProp(_el$18, "fg", _v$6, _p$.t));
1148
+ _v$7 !== _p$.a && (_p$.a = setProp(_el$19, "fg", _v$7, _p$.a));
1149
+ _v$8 !== _p$.o && (_p$.o = setProp(_el$22, "fg", _v$8, _p$.o));
1150
+ _v$9 !== _p$.i && (_p$.i = setProp(_el$23, "fg", _v$9, _p$.i));
1151
+ _v$0 !== _p$.n && (_p$.n = setProp(_el$26, "fg", _v$0, _p$.n));
1152
+ _v$1 !== _p$.s && (_p$.s = setProp(_el$27, "fg", _v$1, _p$.s));
1153
+ _v$10 !== _p$.h && (_p$.h = setProp(_el$30, "fg", _v$10, _p$.h));
1154
+ _v$11 !== _p$.r && (_p$.r = setProp(_el$32, "fg", _v$11, _p$.r));
572
1155
  return _p$;
573
1156
  }, {
574
1157
  e: void 0,
@@ -581,11 +1164,12 @@ function View(props) {
581
1164
  h: void 0,
582
1165
  r: void 0
583
1166
  });
584
- return _el$1;
1167
+ return _el$15;
585
1168
  })();
586
1169
  }
587
1170
  const tui = async (api, _options, _meta) => {
588
1171
  const version = await detectVersion(api);
1172
+ setupUsageBar(api);
589
1173
  api.slots.register({
590
1174
  order: 900,
591
1175
  slots: { sidebar_content(_ctx, props) {