pantheon-opencode 1.2.0 → 1.2.2

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