@jeffreyjyz/opencode-command-code 0.3.11 → 0.3.13

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.13",
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
@@ -16,5 +16,18 @@ export declare function periodStart(usage: {
16
16
  * missing or unreadable, so callers can fall back to omitting the row.
17
17
  * Matching is on the canonical key: the session id and the stored modelID can
18
18
  * differ by vendor prefix/punctuation.
19
+ *
20
+ * Given a `sessionID`, the scan is scoped to that conversation and `sinceMs` is
21
+ * ignored — the panel shows "this model in this session" beside the session's
22
+ * own total, and two figures measuring different windows than each other read
23
+ * as a bug (the billing period had just rolled over, so the model row said 129
24
+ * while the session said 2.7K).
19
25
  */
20
- export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string): ModelUsage | null;
26
+ export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string, sessionID?: string): ModelUsage | null;
27
+ /**
28
+ * Requests and cost for one session, so the panel can show what this
29
+ * conversation has spent rather than only the period-to-date figure. Null when
30
+ * the store is missing or unreadable, and callers then omit the row. Rows are
31
+ * keyed by their primary key: message rows get rewritten in place.
32
+ */
33
+ 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";
@@ -1949,20 +2087,14 @@ function usageDbPath() {
1949
2087
  const base = process.env.XDG_DATA_HOME ?? join3(homedir3(), ".local", "share");
1950
2088
  return join3(base, "opencode", "opencode.db");
1951
2089
  }
1952
- function periodStart(usage, now = Date.now()) {
1953
- if (typeof usage.periodStartAt === "number")
1954
- return usage.periodStartAt;
1955
- const end = usage.periodEnd ? Date.parse(`${usage.periodEnd}T23:59:59`) : Number.NaN;
1956
- if (!Number.isFinite(end))
1957
- return now - 30 * 24 * 60 * 60 * 1000;
1958
- const start = new Date(end);
1959
- start.setMonth(start.getMonth() - 1);
1960
- return start.getTime();
1961
- }
1962
2090
  function usage(value) {
1963
2091
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1964
2092
  }
1965
- function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
2093
+ function hasV2(db) {
2094
+ const row = db.query("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = 'session_message'").get();
2095
+ return Boolean(row);
2096
+ }
2097
+ function loadModelUsage(modelID, sinceMs, path = usageDbPath(), sessionID) {
1966
2098
  let db;
1967
2099
  try {
1968
2100
  db = new Database(path, {
@@ -1972,8 +2104,26 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
1972
2104
  return null;
1973
2105
  }
1974
2106
  try {
2107
+ if (hasV2(db)) {
2108
+ const rows = db.query(`SELECT json_extract(data, '$.model.id') AS model, count(*) AS c
2109
+ FROM session_message
2110
+ WHERE type = 'assistant'
2111
+ ${sessionID ? "AND session_id = ?" : "AND time_created >= ?"}
2112
+ GROUP BY model`).all(sessionID ?? sinceMs);
2113
+ const key = modelKey(modelID);
2114
+ let requests = 0;
2115
+ for (const row of rows) {
2116
+ if (row.model && modelKey(row.model) === key)
2117
+ requests += row.c;
2118
+ }
2119
+ return {
2120
+ requests,
2121
+ cost: 0
2122
+ };
2123
+ }
1975
2124
  const rows = db.query(`SELECT data, time_created FROM message
1976
- WHERE data LIKE '%"tokens"%' AND time_created >= ?`).all(sinceMs);
2125
+ WHERE data LIKE '%"tokens"%'
2126
+ ${sessionID ? "AND session_id = ?" : "AND time_created >= ?"}`).all(sessionID ?? sinceMs);
1977
2127
  const key = modelKey(modelID);
1978
2128
  const seen = new Set;
1979
2129
  let requests = 0;
@@ -2006,13 +2156,77 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
2006
2156
  db.close();
2007
2157
  }
2008
2158
  }
2159
+ function loadSessionUsage(sessionID, path = usageDbPath()) {
2160
+ let db;
2161
+ try {
2162
+ db = new Database(path, {
2163
+ readonly: true
2164
+ });
2165
+ } catch {
2166
+ return null;
2167
+ }
2168
+ try {
2169
+ if (hasV2(db)) {
2170
+ const totals = db.query("SELECT cost FROM session_v2 WHERE id = ?").get(sessionID);
2171
+ const counted = db.query("SELECT count(*) AS c FROM session_message WHERE session_id = ? AND type = 'assistant'").get(sessionID);
2172
+ return {
2173
+ requests: counted.c,
2174
+ cost: typeof totals?.cost === "number" ? totals.cost : 0
2175
+ };
2176
+ }
2177
+ const rows = db.query(`SELECT id, data FROM message
2178
+ WHERE session_id = ? AND data LIKE '%"tokens"%'`).all(sessionID);
2179
+ const seen = new Set;
2180
+ let requests = 0;
2181
+ let cost = 0;
2182
+ for (const row of rows) {
2183
+ if (seen.has(row.id))
2184
+ continue;
2185
+ seen.add(row.id);
2186
+ let data;
2187
+ try {
2188
+ data = JSON.parse(row.data);
2189
+ } catch {
2190
+ continue;
2191
+ }
2192
+ if (data.role !== "assistant" || !data.tokens)
2193
+ continue;
2194
+ requests += 1;
2195
+ cost += usage(data.cost);
2196
+ }
2197
+ return {
2198
+ requests,
2199
+ cost
2200
+ };
2201
+ } catch {
2202
+ return null;
2203
+ } finally {
2204
+ db.close();
2205
+ }
2206
+ }
2009
2207
 
2010
2208
  // src/sidebar/useRows.ts
2011
- var POLL_MS = 60000;
2209
+ var POLL_MS = 20000;
2012
2210
  function sessionKind(providerID) {
2013
2211
  if (!providerID)
2014
2212
  return "unknown";
2015
- return providerID.startsWith("command-code") ? "ours" : "other";
2213
+ if (providerID.startsWith("command-code"))
2214
+ return "ours";
2215
+ if (providerID.startsWith("opencode-go"))
2216
+ return "go";
2217
+ if (providerID === "opencode" || providerID.startsWith("opencode-zen"))
2218
+ return "zen";
2219
+ return "other";
2220
+ }
2221
+ function shows(kind) {
2222
+ return kind === "ours" || kind === "go" || kind === "zen";
2223
+ }
2224
+ function panelTitle(kind) {
2225
+ if (kind === "ours")
2226
+ return "Command Code";
2227
+ if (kind === "zen")
2228
+ return "OpenCode Zen";
2229
+ return "OpenCode Go";
2016
2230
  }
2017
2231
  var lastSnapshot;
2018
2232
  function rememberSnapshot(snapshot) {
@@ -2035,31 +2249,41 @@ function seededModelUsage(id) {
2035
2249
  return lastModelUsage.usage;
2036
2250
  return lastModelUsage.key === modelKey(id) ? lastModelUsage.usage : undefined;
2037
2251
  }
2038
- function useRows(activeModelId, providerID) {
2252
+ function useRows(activeModelId, providerID, sessionID) {
2039
2253
  const [usage, setUsage] = createSignal(seededRows());
2254
+ const [zen, setZen] = createSignal();
2040
2255
  const [meta, setMeta] = createSignal(new Map);
2041
2256
  const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
2257
+ const [sessionUsage, setSessionUsage] = createSignal();
2042
2258
  const [wasOurs, setWasOurs] = createSignal(false);
2043
2259
  loadMeta().then(setMeta).catch(() => {});
2044
- const refreshModelUsage = (id, sinceMs) => {
2260
+ const refreshModelUsage = (id) => {
2045
2261
  if (!id)
2046
2262
  return;
2047
- const fresh = loadModelUsage(id, sinceMs);
2263
+ const fresh = loadModelUsage(id, 0, usageDbPath(), sessionID());
2048
2264
  if (fresh)
2049
2265
  rememberModelUsage(id, fresh);
2050
2266
  setModelUsage(lastModelUsage);
2051
2267
  };
2052
2268
  const refresh = async () => {
2269
+ const kind = sessionKind(providerID());
2270
+ const session = sessionID();
2271
+ if (session)
2272
+ setSessionUsage(loadSessionUsage(session) ?? undefined);
2053
2273
  try {
2054
- const snapshot = await loadUsage();
2055
- rememberSnapshot(snapshot);
2056
- setUsage(usageRows(snapshot));
2057
- refreshModelUsage(activeModelId(), periodStart(snapshot));
2274
+ if (kind === "ours") {
2275
+ const snapshot = await loadUsage();
2276
+ rememberSnapshot(snapshot);
2277
+ setUsage(usageRows(snapshot));
2278
+ refreshModelUsage(activeModelId());
2279
+ } else if (kind === "go" || kind === "zen") {
2280
+ setZen(await loadZen());
2281
+ }
2058
2282
  } catch {}
2059
2283
  };
2060
2284
  createEffect(() => {
2061
2285
  const kind = sessionKind(providerID());
2062
- if (kind !== "ours")
2286
+ if (!shows(kind))
2063
2287
  return;
2064
2288
  setWasOurs(true);
2065
2289
  refresh();
@@ -2068,9 +2292,10 @@ function useRows(activeModelId, providerID) {
2068
2292
  });
2069
2293
  createEffect(() => {
2070
2294
  const id = activeModelId();
2071
- if (sessionKind(providerID()) !== "ours" || !id)
2295
+ const session = sessionID();
2296
+ if (sessionKind(providerID()) !== "ours" || !id || !session)
2072
2297
  return;
2073
- refreshModelUsage(id, periodStart(lastSnapshot ?? {}));
2298
+ refreshModelUsage(id);
2074
2299
  });
2075
2300
  const usageFor = (id) => {
2076
2301
  const seen = modelUsage();
@@ -2081,18 +2306,19 @@ function useRows(activeModelId, providerID) {
2081
2306
  };
2082
2307
  return createMemo(() => {
2083
2308
  const kind = sessionKind(providerID());
2084
- if (kind === "other")
2085
- return [];
2086
- if (kind === "unknown" && !wasOurs())
2309
+ if (!shows(kind) && (kind === "other" || !wasOurs()))
2087
2310
  return [];
2088
2311
  const id = activeModelId();
2089
2312
  const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
2313
+ if (kind === "go" || kind === "zen") {
2314
+ return zenRows(zen(), found, sessionUsage());
2315
+ }
2090
2316
  const model = found ? {
2091
2317
  ...found,
2092
2318
  tier: found.tier ?? (id ? tierFor(id) : undefined),
2093
2319
  minPlan: id ? minPlan(id) : undefined
2094
2320
  } : undefined;
2095
- return [...usage(), separator(), ...modelRows(model, usageFor(id))];
2321
+ return [...usage(), separator(), ...modelRows(model, usageFor(id), sessionUsage())];
2096
2322
  });
2097
2323
  }
2098
2324
 
@@ -2101,8 +2327,9 @@ var ID = "command-code.tui";
2101
2327
  var themed = (colors) => loadPrefs().colors ? colors : mono(colors);
2102
2328
  function PanelV1(props) {
2103
2329
  const current = () => props.api.state.session.get(props.sessionID)?.model;
2104
- const rows = useRows(() => current()?.id, () => current()?.providerID);
2330
+ const rows = useRows(() => current()?.id, () => current()?.providerID, () => props.sessionID);
2105
2331
  return _$createComponent2(Panel, {
2332
+ title: () => panelTitle(sessionKind(current()?.providerID)),
2106
2333
  rows,
2107
2334
  colors: () => themed(legacyColors(props.api.theme.current))
2108
2335
  });
@@ -2127,8 +2354,9 @@ function PanelV2(props) {
2127
2354
  ctx
2128
2355
  } = props;
2129
2356
  const current = () => ctx.data.session.get(props.sessionID)?.model;
2130
- const rows = useRows(() => current()?.id, () => current()?.providerID);
2357
+ const rows = useRows(() => current()?.id, () => current()?.providerID, () => props.sessionID);
2131
2358
  return _$createComponent2(Panel, {
2359
+ title: () => panelTitle(sessionKind(current()?.providerID)),
2132
2360
  rows,
2133
2361
  colors: () => themed(hostColors(ctx.theme))
2134
2362
  });
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.13",
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",