@jeffreyjyz/opencode-command-code 0.3.11 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4063,7 +4063,7 @@ var commandCodeV2 = {
4063
4063
  // package.json
4064
4064
  var package_default = {
4065
4065
  name: "@jeffreyjyz/opencode-command-code",
4066
- version: "0.3.11",
4066
+ version: "0.3.12",
4067
4067
  description: "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
4068
4068
  type: "module",
4069
4069
  main: "dist/index.js",
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { type ModelMeta, type Usage } from "./rows";
3
+ import { type ZenUsage } from "./zen";
3
4
  /** `detached` is load-bearing, not tidiness: it starts the child in its own
4
5
  * session, so it has no controlling terminal. cmduse's snapshot() paints a
5
6
  * "fetching usage…" spinner straight to /dev/tty — piping stdout/stderr does
@@ -8,9 +9,19 @@ import { type ModelMeta, type Usage } from "./rows";
8
9
  export declare const SPAWN_OPTIONS: Parameters<typeof spawn>[2];
9
10
  /** Parse `cmduse -1 --json` into the usage snapshot. */
10
11
  export declare function parseUsageJson(text: string): Usage;
11
- /** Parse `mpc --json` into per-model meta, CommandCode side only. */
12
+ /**
13
+ * Parse `mpc --json` into per-model meta: the CommandCode fields at the top
14
+ * level and the OpenCode Go / Zen side under `oc`, so one lookup serves either
15
+ * kind of session. Rows that carry only one side are kept.
16
+ */
12
17
  export declare function parseMpcJson(text: string): Map<string, ModelMeta>;
13
18
  /** mpc's catalog, cached on disk: it scrapes live docs over the network. */
14
19
  export declare function loadMeta(): Promise<Map<string, ModelMeta>>;
15
20
  /** One usage snapshot from cmduse. Throws when cmduse is missing or fails. */
16
21
  export declare function loadUsage(): Promise<Usage>;
22
+ /**
23
+ * One OpenCode Go / Zen snapshot from ocuse, which reads opencode's own store
24
+ * (those providers have no usage API). Throws when ocuse is missing or fails,
25
+ * and the caller then omits the panel.
26
+ */
27
+ export declare function loadZen(): Promise<ZenUsage>;
@@ -45,6 +45,7 @@ export declare function legacyColors(theme: {
45
45
  export declare function mono(colors: PanelColors): PanelColors;
46
46
  /** Bold title, then one line per row. Renders nothing when there are no rows. */
47
47
  export declare function Panel(props: {
48
+ title: () => string;
48
49
  rows: () => SidebarRow[];
49
50
  colors: () => PanelColors;
50
51
  }): import("solid-js").JSX.Element;
@@ -51,11 +51,30 @@ export interface ModelMeta {
51
51
  };
52
52
  intelligence?: number;
53
53
  tps?: number;
54
+ /**
55
+ * The OpenCode Go / Zen side of the same mpc row (0.4.0): the CommandCode
56
+ * fields above are the default, this is what a Go/Zen session reads.
57
+ */
58
+ oc?: {
59
+ provider?: string;
60
+ plan?: string;
61
+ allowance?: number;
62
+ rates?: {
63
+ input: number;
64
+ output: number;
65
+ cacheRead: number;
66
+ cacheWrite?: number | null;
67
+ };
68
+ ability?: number;
69
+ tps?: number;
70
+ };
54
71
  }
55
72
  export interface WindowUsage {
56
73
  cap?: number;
57
74
  used?: number;
58
75
  resetAt?: number;
76
+ /** The account API's own over-cap flag (cmduse passes it through). */
77
+ exceeded?: boolean;
59
78
  }
60
79
  /** One model's usage for the account's current billing period. */
61
80
  export interface ModelUsage {
@@ -85,8 +104,9 @@ export declare function until(epoch: number | undefined, now?: number): string;
85
104
  /** Plan + rolling windows + period totals, from a cmduse snapshot. */
86
105
  export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
87
106
  /** Active model's allowance, rates and benchmarks, from mpc's catalog; its
88
- * period usage (when the store has it) rides directly under the model name. */
89
- export declare function modelRows(meta: ModelMeta | undefined, usage?: ModelUsage): SidebarRow[];
107
+ * period usage (when the store has it) rides directly under the model name, and
108
+ * this session's own totals under that (0.4.0). */
109
+ export declare function modelRows(meta: ModelMeta | undefined, usage?: ModelUsage, session?: ModelUsage): SidebarRow[];
90
110
  /**
91
111
  * Canonical key for a model id, mirroring mpc's normalizeKey: lowercase, drop
92
112
  * the vendor prefix and any parenthetical, strip punctuation. Used to match the
@@ -18,3 +18,10 @@ export declare function periodStart(usage: {
18
18
  * differ by vendor prefix/punctuation.
19
19
  */
20
20
  export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string): ModelUsage | null;
21
+ /**
22
+ * Requests and cost for one session, so the panel can show what this
23
+ * conversation has spent rather than only the period-to-date figure. Null when
24
+ * the store is missing or unreadable, and callers then omit the row. Rows are
25
+ * keyed by their primary key: message rows get rewritten in place.
26
+ */
27
+ export declare function loadSessionUsage(sessionID: string, path?: string): ModelUsage | null;
@@ -1,12 +1,14 @@
1
1
  import { type ModelUsage, type SidebarRow, type Usage } from "./rows";
2
- /** Which provider a session is on, as far as this panel is concerned. */
3
- export type SessionKind = "ours" | "other" | "unknown";
4
2
  /**
5
- * `unknown` is the frame during a session switch, before the host has resolved
6
- * the new session's model. It must not be read as "another provider", or the
7
- * panel blanks and refetches on every tab change.
3
+ * Which family a session's provider belongs to, as far as this panel is
4
+ * concerned. `unknown` is the frame during a session switch, before the host has
5
+ * resolved the new session's model; it must not be read as someone else's, or
6
+ * the panel blanks and refetches on every tab change.
8
7
  */
8
+ export type SessionKind = "ours" | "go" | "zen" | "other" | "unknown";
9
9
  export declare function sessionKind(providerID: string | undefined): SessionKind;
10
+ /** The section's title, per family. */
11
+ export declare function panelTitle(kind: SessionKind): string;
10
12
  /** Keep an account snapshot for the next panel that needs it. */
11
13
  export declare function rememberSnapshot(snapshot: Usage): void;
12
14
  /** Rows to paint before this panel's first fetch of the session completes. */
@@ -22,4 +24,4 @@ export declare function rememberModelUsage(id: string, usage: ModelUsage): void;
22
24
  * beats blanking it, the same call `wasOurs` makes for the account block.
23
25
  */
24
26
  export declare function seededModelUsage(id: string | undefined): ModelUsage | undefined;
25
- export declare function useRows(activeModelId: () => string | undefined, providerID: () => string | undefined): import("solid-js").Accessor<SidebarRow[]>;
27
+ export declare function useRows(activeModelId: () => string | undefined, providerID: () => string | undefined, sessionID: () => string | undefined): import("solid-js").Accessor<SidebarRow[]>;
@@ -14,3 +14,14 @@ export declare function elapsedPct(resetAtMs: number | undefined, durSecs: numbe
14
14
  * Exact zero stays "0%".
15
15
  */
16
16
  export declare function elapsedLabel(resetAtMs: number | undefined, durSecs: number, nowSecs: number): string | undefined;
17
+ /**
18
+ * Seconds until the cap is reached at the window's current burn rate, or
19
+ * undefined when there is nothing honest to warn about: no reset, a window that
20
+ * has not started or is already rolling over, under a tenth elapsed (a flat-rate
21
+ * projection off a few seconds of data is noise), no spend yet, a cap already
22
+ * reached, or a cap that would land after the reset anyway.
23
+ *
24
+ * Mirrors `cmduse_core::pace_eta`; the `paceEta` vectors in the shared
25
+ * `core/conformance.json` are asserted against this in test/conformance.test.ts.
26
+ */
27
+ export declare function paceEtaSecs(resetAtMs: number | undefined, durSecs: number, used: number, cap: number, nowSecs: number): number | undefined;
@@ -0,0 +1,36 @@
1
+ import { type ModelMeta, type ModelUsage, type SidebarRow } from "./rows";
2
+ /** Go's documented per-model window shares of the monthly allowance. */
3
+ export declare const GO_WINDOW_SHARE: {
4
+ readonly fiveHour: 0.2;
5
+ readonly weekly: 0.5;
6
+ readonly month: 1;
7
+ };
8
+ export interface ZenTotals {
9
+ requests: number;
10
+ cost: number;
11
+ tokens: number;
12
+ }
13
+ export interface ZenModel {
14
+ id: string;
15
+ provider: string;
16
+ /** Monthly allowance the docs give this model, when they give one. */
17
+ limit?: number;
18
+ fiveHour: ZenTotals;
19
+ weekly: ZenTotals;
20
+ month: ZenTotals;
21
+ }
22
+ export interface ZenUsage {
23
+ periodStart?: number;
24
+ source?: string;
25
+ totals: {
26
+ fiveHour: ZenTotals;
27
+ weekly: ZenTotals;
28
+ month: ZenTotals;
29
+ };
30
+ /** Keyed by canonical model key, the same normalisation the CC side matches on. */
31
+ models: Map<string, ZenModel>;
32
+ }
33
+ /** Parse `ocuse -1 --json`. Anything malformed reads as "no usage". */
34
+ export declare function parseZenJson(text: string): ZenUsage;
35
+ /** The OpenCode Go / Zen panel: period windows, a rule, then the active model. */
36
+ export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, session?: ModelUsage): SidebarRow[];
package/dist/tui.js CHANGED
@@ -120,7 +120,6 @@ async function runCmduse(arg, opts = {}) {
120
120
 
121
121
  // src/sidebar/panel.tsx
122
122
  import { createComponent as _$createComponent } from "@opentui/solid";
123
- import { createTextNode as _$createTextNode } from "@opentui/solid";
124
123
  import { insertNode as _$insertNode } from "@opentui/solid";
125
124
  import { spread as _$spread } from "@opentui/solid";
126
125
  import { mergeProps as _$mergeProps } from "@opentui/solid";
@@ -469,6 +468,26 @@ function elapsedLabel(resetAtMs, durSecs, nowSecs) {
469
468
  return "<1%";
470
469
  return `${pct}%`;
471
470
  }
471
+ function paceEtaSecs(resetAtMs, durSecs, used, cap, nowSecs) {
472
+ if (typeof resetAtMs !== "number" || !Number.isFinite(resetAtMs))
473
+ return;
474
+ const reset = Math.floor(resetAtMs / 1000);
475
+ if (reset < durSecs)
476
+ return;
477
+ const start = reset - durSecs;
478
+ if (nowSecs <= start || nowSecs >= reset)
479
+ return;
480
+ const elapsed = nowSecs - start;
481
+ if (elapsed / durSecs < 0.1)
482
+ return;
483
+ const rate = used / elapsed;
484
+ if (rate <= 0 || used >= cap)
485
+ return;
486
+ const secsToCap = (cap - used) / rate;
487
+ if (secsToCap >= reset - nowSecs)
488
+ return;
489
+ return secsToCap;
490
+ }
472
491
 
473
492
  // src/sidebar/rows.ts
474
493
  function pctTone(pct) {
@@ -533,10 +552,13 @@ function windowRows(label, w, now, durSecs) {
533
552
  return [];
534
553
  const used = typeof w.used === "number" ? w.used : 0;
535
554
  const pct = Math.round(used / w.cap * 100);
536
- const elapsed = elapsedLabel(w.resetAt, durSecs, Math.floor(now / 1000));
555
+ const nowSecs = Math.floor(now / 1000);
556
+ const elapsed = elapsedLabel(w.resetAt, durSecs, nowSecs);
557
+ const eta = paceEtaSecs(w.resetAt, durSecs, used, w.cap, nowSecs);
537
558
  const reset = until(w.resetAt, now);
538
- const detail = [...elapsed === undefined ? [] : [`${elapsed} elapsed`], ...reset ? [`resets ${reset}`] : []].join(" \xB7 ");
539
- return [[label, `${money(used)}/${money(w.cap)} (${pct}%)`, pctTone(pct)], ...detail ? [["", detail, "muted"]] : []];
559
+ const lead = w.exceeded ? "LIMIT EXCEEDED" : eta === undefined ? elapsed === undefined ? "" : `${elapsed} elapsed` : `cap in ${until(now + eta * 1000, now)}`;
560
+ const detail = [lead, reset ? `resets ${reset}` : ""].filter(Boolean).join(" \xB7 ");
561
+ return [[label, `${money(used)}/${money(w.cap)} (${pct}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : []];
540
562
  }
541
563
  function monthlyDetail(usage, now) {
542
564
  const {
@@ -581,7 +603,7 @@ function usageRows(usage, now = Date.now()) {
581
603
  }
582
604
  return rows;
583
605
  }
584
- function modelRows(meta, usage) {
606
+ function modelRows(meta, usage, session) {
585
607
  if (!meta)
586
608
  return [];
587
609
  const rows = [["Model", clip(meta.name, valueWidth("Model")), "strong", true]];
@@ -589,6 +611,10 @@ function modelRows(meta, usage) {
589
611
  const spent = usage.cost > 0 ? ` \xB7 ${money(usage.cost)}` : "";
590
612
  rows.push(["Usage (this model)", `${count(usage.requests)} req${spent}`, "base"]);
591
613
  }
614
+ if (session) {
615
+ const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
616
+ rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
617
+ }
592
618
  if (meta.tier)
593
619
  rows.push(["Tier", TIER_DISPLAY[meta.tier], "base"]);
594
620
  else if (meta.minPlan)
@@ -704,7 +730,7 @@ function Panel(props) {
704
730
  var _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$9 = _$createElement("b");
705
731
  _$insertNode(_el$7, _el$8);
706
732
  _$insertNode(_el$8, _el$9);
707
- _$insertNode(_el$9, _$createTextNode(`Command Code`));
733
+ _$insert(_el$9, () => props.title());
708
734
  _$insert(_el$7, _$createComponent(For, {
709
735
  get each() {
710
736
  return props.rows();
@@ -1834,8 +1860,107 @@ import { spawn as spawn2 } from "child_process";
1834
1860
  import { mkdir, readFile, writeFile } from "fs/promises";
1835
1861
  import { homedir as homedir2 } from "os";
1836
1862
  import { dirname, join as join2 } from "path";
1863
+
1864
+ // src/sidebar/zen.ts
1865
+ var GO_WINDOW_SHARE = {
1866
+ fiveHour: 0.2,
1867
+ weekly: 0.5,
1868
+ month: 1
1869
+ };
1870
+ function totals(value) {
1871
+ const v = value ?? {};
1872
+ const num = (key) => typeof v[key] === "number" ? v[key] : 0;
1873
+ return {
1874
+ requests: num("requests"),
1875
+ cost: num("cost"),
1876
+ tokens: num("tokens")
1877
+ };
1878
+ }
1879
+ function parseZenJson(text) {
1880
+ let body;
1881
+ try {
1882
+ body = JSON.parse(text);
1883
+ } catch {
1884
+ body = {};
1885
+ }
1886
+ const models = new Map;
1887
+ for (const raw of body.models ?? []) {
1888
+ if (typeof raw.id !== "string")
1889
+ continue;
1890
+ models.set(modelKey(raw.id), {
1891
+ id: raw.id,
1892
+ provider: typeof raw.provider === "string" ? raw.provider : "",
1893
+ limit: typeof raw.limit === "number" ? raw.limit : undefined,
1894
+ fiveHour: totals(raw.fiveHour),
1895
+ weekly: totals(raw.weekly),
1896
+ month: totals(raw.month)
1897
+ });
1898
+ }
1899
+ const top = body.totals ?? {};
1900
+ return {
1901
+ periodStart: typeof body.periodStart === "number" ? body.periodStart : undefined,
1902
+ source: typeof body.source === "string" ? body.source : undefined,
1903
+ totals: {
1904
+ fiveHour: totals(top.fiveHour),
1905
+ weekly: totals(top.weekly),
1906
+ month: totals(top.month)
1907
+ },
1908
+ models
1909
+ };
1910
+ }
1911
+ function windowRows2(label, spent, limit, share) {
1912
+ if (typeof limit !== "number" || limit <= 0) {
1913
+ return [[label, `${money(spent.cost)} \xB7 ${count(spent.requests)} req`, "base"]];
1914
+ }
1915
+ const cap = limit * share;
1916
+ const pct = cap > 0 ? Math.round(spent.cost / cap * 100) : 0;
1917
+ const rows = [[label, `${money(spent.cost)}/${money(cap)} (${pct}%)`, pctTone(pct)]];
1918
+ if (spent.requests > 0)
1919
+ rows.push(["", `${count(spent.requests)} req`, "muted"]);
1920
+ return rows;
1921
+ }
1922
+ function zenRows(usage, meta, session) {
1923
+ if (!usage)
1924
+ return [];
1925
+ const oc = meta?.oc;
1926
+ const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
1927
+ const mine = meta ? usage.models.get(modelKey(meta.name)) : undefined;
1928
+ const limit = oc?.allowance ?? mine?.limit;
1929
+ rows.push(...windowRows2("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, limit, GO_WINDOW_SHARE.fiveHour));
1930
+ rows.push(...windowRows2("Weekly", mine?.weekly ?? usage.totals.weekly, limit, GO_WINDOW_SHARE.weekly));
1931
+ rows.push(...windowRows2("Month", mine?.month ?? usage.totals.month, limit, GO_WINDOW_SHARE.month));
1932
+ rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
1933
+ rows.push(separator());
1934
+ if (!meta)
1935
+ return rows;
1936
+ rows.push(["Model", clip(meta.name, valueWidth("Model")), "strong", true]);
1937
+ if (session) {
1938
+ const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
1939
+ rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
1940
+ }
1941
+ if (typeof limit === "number") {
1942
+ rows.push(["Allowance", `${money(limit)}/mo`, "base"]);
1943
+ }
1944
+ if (oc?.rates) {
1945
+ const cache = [`cache read ${rate(oc.rates.cacheRead)}`];
1946
+ if (typeof oc.rates.cacheWrite === "number" && oc.rates.cacheWrite > 0) {
1947
+ cache.push(`write ${rate(oc.rates.cacheWrite)}`);
1948
+ }
1949
+ rows.push(["Rates", `${rate(oc.rates.input)}/${rate(oc.rates.output)} in/out`, "base"]);
1950
+ rows.push(["", cache.join(" \xB7 "), "muted"]);
1951
+ }
1952
+ if (typeof oc?.ability === "number") {
1953
+ rows.push(["Intelligence", String(oc.ability), "base"]);
1954
+ }
1955
+ if (typeof oc?.tps === "number")
1956
+ rows.push(["Tok/s", String(oc.tps), "base"]);
1957
+ return rows;
1958
+ }
1959
+
1960
+ // src/sidebar/data.ts
1837
1961
  var CMDUSE = cmduseCandidates();
1838
1962
  var MPC = ["mpc", join2(homedir2(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
1963
+ var OCUSE = [process.env.OCUSE_BIN, "ocuse", "/opt/homebrew/bin/ocuse", "/usr/local/bin/ocuse"].filter((bin) => Boolean(bin));
1839
1964
  var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
1840
1965
  var SPAWN_OPTIONS = {
1841
1966
  stdio: ["ignore", "pipe", "pipe"],
@@ -1874,7 +1999,8 @@ function parseUsageJson(text) {
1874
1999
  return {
1875
2000
  cap: typeof w.cap === "number" ? w.cap : undefined,
1876
2001
  used: typeof w.used === "number" ? w.used : undefined,
1877
- resetAt: typeof w.resetAt === "number" ? w.resetAt : undefined
2002
+ resetAt: typeof w.resetAt === "number" ? w.resetAt : undefined,
2003
+ exceeded: typeof w.exceeded === "boolean" ? w.exceeded : undefined
1878
2004
  };
1879
2005
  };
1880
2006
  const summary = raw.summary ?? {};
@@ -1895,16 +2021,25 @@ function parseMpcJson(text) {
1895
2021
  const body = JSON.parse(text);
1896
2022
  const meta = new Map;
1897
2023
  for (const row of body.rows ?? []) {
1898
- const cc = row.cc;
1899
- if (!cc || !row.name)
2024
+ if (!row.name || !row.cc && !row.oc)
1900
2025
  continue;
2026
+ const cc = row.cc;
2027
+ const oc = row.oc;
1901
2028
  const entry = {
1902
2029
  key: row.key ?? modelKey(row.name),
1903
2030
  name: row.name,
1904
- allowance: cc.allowance,
1905
- rates: cc.pricing,
1906
- intelligence: cc.ability ?? undefined,
1907
- tps: cc.tps ?? undefined
2031
+ allowance: cc?.allowance,
2032
+ rates: cc?.pricing,
2033
+ intelligence: cc?.ability ?? undefined,
2034
+ tps: cc?.tps ?? undefined,
2035
+ oc: oc ? {
2036
+ provider: oc.provider,
2037
+ plan: oc.plan,
2038
+ allowance: oc.allowance,
2039
+ rates: oc.pricing,
2040
+ ability: oc.ability ?? undefined,
2041
+ tps: oc.tps ?? undefined
2042
+ } : undefined
1908
2043
  };
1909
2044
  meta.set(entry.key, entry);
1910
2045
  meta.set(modelKey(row.name), entry);
@@ -1938,6 +2073,9 @@ async function loadMeta() {
1938
2073
  async function loadUsage() {
1939
2074
  return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
1940
2075
  }
2076
+ async function loadZen() {
2077
+ return parseZenJson(await runFirst(OCUSE, ["-1", "--json"]));
2078
+ }
1941
2079
 
1942
2080
  // src/sidebar/usageDb.ts
1943
2081
  import { Database } from "bun:sqlite";
@@ -2006,13 +2144,69 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
2006
2144
  db.close();
2007
2145
  }
2008
2146
  }
2147
+ function loadSessionUsage(sessionID, path = usageDbPath()) {
2148
+ let db;
2149
+ try {
2150
+ db = new Database(path, {
2151
+ readonly: true
2152
+ });
2153
+ } catch {
2154
+ return null;
2155
+ }
2156
+ try {
2157
+ const rows = db.query(`SELECT id, data FROM message
2158
+ WHERE session_id = ? AND data LIKE '%"tokens"%'`).all(sessionID);
2159
+ const seen = new Set;
2160
+ let requests = 0;
2161
+ let cost = 0;
2162
+ for (const row of rows) {
2163
+ if (seen.has(row.id))
2164
+ continue;
2165
+ seen.add(row.id);
2166
+ let data;
2167
+ try {
2168
+ data = JSON.parse(row.data);
2169
+ } catch {
2170
+ continue;
2171
+ }
2172
+ if (data.role !== "assistant" || !data.tokens)
2173
+ continue;
2174
+ requests += 1;
2175
+ cost += usage(data.cost);
2176
+ }
2177
+ return {
2178
+ requests,
2179
+ cost
2180
+ };
2181
+ } catch {
2182
+ return null;
2183
+ } finally {
2184
+ db.close();
2185
+ }
2186
+ }
2009
2187
 
2010
2188
  // src/sidebar/useRows.ts
2011
2189
  var POLL_MS = 60000;
2012
2190
  function sessionKind(providerID) {
2013
2191
  if (!providerID)
2014
2192
  return "unknown";
2015
- return providerID.startsWith("command-code") ? "ours" : "other";
2193
+ if (providerID.startsWith("command-code"))
2194
+ return "ours";
2195
+ if (providerID.startsWith("opencode-go"))
2196
+ return "go";
2197
+ if (providerID === "opencode" || providerID.startsWith("opencode-zen"))
2198
+ return "zen";
2199
+ return "other";
2200
+ }
2201
+ function shows(kind) {
2202
+ return kind === "ours" || kind === "go" || kind === "zen";
2203
+ }
2204
+ function panelTitle(kind) {
2205
+ if (kind === "ours")
2206
+ return "Command Code";
2207
+ if (kind === "zen")
2208
+ return "OpenCode Zen";
2209
+ return "OpenCode Go";
2016
2210
  }
2017
2211
  var lastSnapshot;
2018
2212
  function rememberSnapshot(snapshot) {
@@ -2035,10 +2229,12 @@ function seededModelUsage(id) {
2035
2229
  return lastModelUsage.usage;
2036
2230
  return lastModelUsage.key === modelKey(id) ? lastModelUsage.usage : undefined;
2037
2231
  }
2038
- function useRows(activeModelId, providerID) {
2232
+ function useRows(activeModelId, providerID, sessionID) {
2039
2233
  const [usage, setUsage] = createSignal(seededRows());
2234
+ const [zen, setZen] = createSignal();
2040
2235
  const [meta, setMeta] = createSignal(new Map);
2041
2236
  const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
2237
+ const [sessionUsage, setSessionUsage] = createSignal();
2042
2238
  const [wasOurs, setWasOurs] = createSignal(false);
2043
2239
  loadMeta().then(setMeta).catch(() => {});
2044
2240
  const refreshModelUsage = (id, sinceMs) => {
@@ -2050,16 +2246,24 @@ function useRows(activeModelId, providerID) {
2050
2246
  setModelUsage(lastModelUsage);
2051
2247
  };
2052
2248
  const refresh = async () => {
2249
+ const kind = sessionKind(providerID());
2250
+ const session = sessionID();
2251
+ if (session)
2252
+ setSessionUsage(loadSessionUsage(session) ?? undefined);
2053
2253
  try {
2054
- const snapshot = await loadUsage();
2055
- rememberSnapshot(snapshot);
2056
- setUsage(usageRows(snapshot));
2057
- refreshModelUsage(activeModelId(), periodStart(snapshot));
2254
+ if (kind === "ours") {
2255
+ const snapshot = await loadUsage();
2256
+ rememberSnapshot(snapshot);
2257
+ setUsage(usageRows(snapshot));
2258
+ refreshModelUsage(activeModelId(), periodStart(snapshot));
2259
+ } else if (kind === "go" || kind === "zen") {
2260
+ setZen(await loadZen());
2261
+ }
2058
2262
  } catch {}
2059
2263
  };
2060
2264
  createEffect(() => {
2061
2265
  const kind = sessionKind(providerID());
2062
- if (kind !== "ours")
2266
+ if (!shows(kind))
2063
2267
  return;
2064
2268
  setWasOurs(true);
2065
2269
  refresh();
@@ -2081,18 +2285,19 @@ function useRows(activeModelId, providerID) {
2081
2285
  };
2082
2286
  return createMemo(() => {
2083
2287
  const kind = sessionKind(providerID());
2084
- if (kind === "other")
2085
- return [];
2086
- if (kind === "unknown" && !wasOurs())
2288
+ if (!shows(kind) && (kind === "other" || !wasOurs()))
2087
2289
  return [];
2088
2290
  const id = activeModelId();
2089
2291
  const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
2292
+ if (kind === "go" || kind === "zen") {
2293
+ return zenRows(zen(), found, sessionUsage());
2294
+ }
2090
2295
  const model = found ? {
2091
2296
  ...found,
2092
2297
  tier: found.tier ?? (id ? tierFor(id) : undefined),
2093
2298
  minPlan: id ? minPlan(id) : undefined
2094
2299
  } : undefined;
2095
- return [...usage(), separator(), ...modelRows(model, usageFor(id))];
2300
+ return [...usage(), separator(), ...modelRows(model, usageFor(id), sessionUsage())];
2096
2301
  });
2097
2302
  }
2098
2303
 
@@ -2101,8 +2306,9 @@ var ID = "command-code.tui";
2101
2306
  var themed = (colors) => loadPrefs().colors ? colors : mono(colors);
2102
2307
  function PanelV1(props) {
2103
2308
  const current = () => props.api.state.session.get(props.sessionID)?.model;
2104
- const rows = useRows(() => current()?.id, () => current()?.providerID);
2309
+ const rows = useRows(() => current()?.id, () => current()?.providerID, () => props.sessionID);
2105
2310
  return _$createComponent2(Panel, {
2311
+ title: () => panelTitle(sessionKind(current()?.providerID)),
2106
2312
  rows,
2107
2313
  colors: () => themed(legacyColors(props.api.theme.current))
2108
2314
  });
@@ -2127,8 +2333,9 @@ function PanelV2(props) {
2127
2333
  ctx
2128
2334
  } = props;
2129
2335
  const current = () => ctx.data.session.get(props.sessionID)?.model;
2130
- const rows = useRows(() => current()?.id, () => current()?.providerID);
2336
+ const rows = useRows(() => current()?.id, () => current()?.providerID, () => props.sessionID);
2131
2337
  return _$createComponent2(Panel, {
2338
+ title: () => panelTitle(sessionKind(current()?.providerID)),
2132
2339
  rows,
2133
2340
  colors: () => themed(hostColors(ctx.theme))
2134
2341
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",