opencode-usage-coach 0.1.1 → 0.1.3

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.
@@ -32,8 +32,10 @@ Default to the loop only when it genuinely adds value. Do not over-engineer smal
32
32
  ## Harness loop (only for substantive work)
33
33
  The user's message is the task source. If it has multiple distinct parts, decompose into discrete tasks (N); if it is one unit, N = 1.
34
34
 
35
+ **Parallel dispatch (independent tasks):** If the decomposed tasks are INDEPENDENT (no data dependency), dispatch them concurrently — issue **multiple `task` calls in the same turn** so they run as parallel subagents. (This is the only reliable parallel path — the deterministic script cannot parallelize due to opencode's single-server model.) Cap concurrency by quota coaching: "big tasks OK" → up to 3-4 parallel; "moderate/small" → 1-2; STOP → don't dispatch. For DEPENDENT tasks (B needs A's output), run sequentially.
36
+
35
37
  1. Call `harness_start(name, N)` to register the run on the panel.
36
- 2. For each task i (1..N):
38
+ 2. For each task i (1..N) — in parallel batches when independent:
37
39
  a. `task_update(i, title, "generating")`.
38
40
  b. **Generate** — delegate to a subagent via the `task` tool (`subagent_type: "general"`):
39
41
  prompt: `"Task: {title}. Perform it for real in the current directory (write/edit files, run commands as needed)."`
package/dist/index.js CHANGED
@@ -1,16 +1,28 @@
1
1
  // src/index.ts
2
2
  import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from "fs";
3
3
  import { spawn } from "child_process";
4
+ import { createHash } from "crypto";
4
5
  import { homedir } from "os";
5
- import { join } from "path";
6
+ import { join, resolve } from "path";
6
7
  import { tool } from "@opencode-ai/plugin";
7
8
  var PLUGIN_NAME = "opencode-usage-coach";
8
- var STATE_DIR = process.env.UC_STATE_DIR ?? join(homedir(), ".cache", "opencode-usage-coach");
9
+ var DEBUG = process.env.UC_DEBUG === "1";
10
+ var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
11
+ var STATE_DIR = join(homedir(), ".cache", "opencode-usage-coach");
9
12
  var STATE_FILE = join(STATE_DIR, "state.json");
10
13
  var HARNESS_FILE = join(STATE_DIR, "harness.json");
11
14
  var LOG_FILE = join(STATE_DIR, "coach.log");
12
- var DEBUG = process.env.UC_DEBUG === "1";
13
- var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
15
+ function projectStateDir(dir) {
16
+ const abs = resolve(dir || ".");
17
+ const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
18
+ return join(homedir(), ".cache", "opencode-usage-coach", "projects", h);
19
+ }
20
+ function setStateDir(dir) {
21
+ STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(dir);
22
+ STATE_FILE = join(STATE_DIR, "state.json");
23
+ HARNESS_FILE = join(STATE_DIR, "harness.json");
24
+ LOG_FILE = join(STATE_DIR, "coach.log");
25
+ }
14
26
  var NOOP_HOOKS = {};
15
27
  function log(msg) {
16
28
  if (DEBUG) try {
@@ -63,14 +75,72 @@ function humanRemaining(iso) {
63
75
  const mins = Math.floor((new Date(iso).getTime() - Date.now()) / 6e4);
64
76
  if (mins < 0) return "resets soon";
65
77
  if (mins < 60) return `resets in ${mins}m`;
66
- if (mins < 1440) return `resets in ${Math.floor(mins / 60)}h`;
67
- return `resets in ${Math.floor(mins / 1440)}d`;
78
+ if (mins < 1440) return `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`;
79
+ return `${Math.floor(mins / 1440)}d left`;
68
80
  } catch {
69
81
  return "";
70
82
  }
71
83
  }
84
+ function captureStdout(args) {
85
+ return new Promise((resolve2) => {
86
+ let out = "";
87
+ let p;
88
+ try {
89
+ p = spawn("codexbar", args, { stdio: ["ignore", "pipe", "ignore"] });
90
+ } catch {
91
+ return resolve2("");
92
+ }
93
+ p.stdout?.on("data", (d) => {
94
+ out += d.toString();
95
+ });
96
+ p.on("error", () => resolve2(""));
97
+ p.on("close", () => resolve2(out));
98
+ });
99
+ }
100
+ async function fetchEnabledProviders() {
101
+ const out = await captureStdout(["config", "providers"]);
102
+ const ids = [];
103
+ for (const line of out.split("\n")) {
104
+ const m = line.match(/^\s*([a-zA-Z0-9_-]+):\s*enabled/);
105
+ if (m) ids.push(m[1]);
106
+ }
107
+ return ids;
108
+ }
109
+ function providerAdvice(h5, wk) {
110
+ const S5H = STOP_5H, SWK = STOP_WK, T5H = THR_5H, TWK = THR_WK;
111
+ if (h5 >= S5H || wk >= SWK) return "STOP \u2014 finish current only";
112
+ if (h5 >= T5H && wk >= TWK) return "small tasks only \u2014 big ones will hit both limits";
113
+ if (h5 >= T5H) return "small tasks only \u2014 5h window nearly full, big tasks after reset";
114
+ if (wk >= TWK) return "small tasks only \u2014 big ones will strain late-week";
115
+ if (h5 >= 50 || wk >= 50) return "moderate tasks OK \u2014 save big ones for headroom";
116
+ return "big tasks OK \u2014 short & long limits comfortable";
117
+ }
118
+ async function fetchProvidersCoach() {
119
+ const ids = await fetchEnabledProviders();
120
+ const results = await Promise.all(ids.map(async (id) => {
121
+ try {
122
+ const out = await captureStdout(["usage", "--provider", id, "--json"]);
123
+ const u = JSON.parse(out)[0]?.usage;
124
+ if (!u) return null;
125
+ const h5 = Math.round(u.tertiary?.usedPercent ?? 0);
126
+ const wk = Math.round(u.primary?.usedPercent ?? 0);
127
+ return {
128
+ id,
129
+ name: id,
130
+ fiveHour: h5,
131
+ weekly: wk,
132
+ fiveHourReset: humanRemaining(u.tertiary?.resetsAt),
133
+ weeklyReset: humanRemaining(u.primary?.resetsAt),
134
+ advice: providerAdvice(h5, wk)
135
+ };
136
+ } catch {
137
+ return null;
138
+ }
139
+ }));
140
+ return results.filter(Boolean);
141
+ }
72
142
  function fetchQuota() {
73
- return new Promise((resolve) => {
143
+ return new Promise((resolve2) => {
74
144
  let out = "";
75
145
  let p;
76
146
  try {
@@ -78,21 +148,21 @@ function fetchQuota() {
78
148
  stdio: ["ignore", "pipe", "ignore"]
79
149
  });
80
150
  } catch {
81
- return resolve(null);
151
+ return resolve2(null);
82
152
  }
83
153
  p.stdout?.on("data", (d) => {
84
154
  out += d.toString();
85
155
  });
86
- p.on("error", () => resolve(null));
156
+ p.on("error", () => resolve2(null));
87
157
  p.on("close", () => {
88
158
  try {
89
159
  const text = out.trim();
90
- if (!text || text === "[]") return resolve(null);
160
+ if (!text || text === "[]") return resolve2(null);
91
161
  const u = JSON.parse(text)[0]?.usage;
92
- if (!u) return resolve(null);
93
- resolve({ weekly: u.primary ?? { usedPercent: 0 }, monthly: u.secondary ?? { usedPercent: 0 }, fiveHour: u.tertiary ?? { usedPercent: 0 } });
162
+ if (!u) return resolve2(null);
163
+ resolve2({ weekly: u.primary ?? { usedPercent: 0 }, monthly: u.secondary ?? { usedPercent: 0 }, fiveHour: u.tertiary ?? { usedPercent: 0 } });
94
164
  } catch {
95
- resolve(null);
165
+ resolve2(null);
96
166
  }
97
167
  });
98
168
  });
@@ -113,6 +183,7 @@ function coach(q) {
113
183
  var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, monthly: -1, fiveHour: -1 };
114
184
  async function UsageCoachPlugin(input) {
115
185
  try {
186
+ setStateDir(input.directory);
116
187
  let last = null;
117
188
  let lastFetchedAt = 0;
118
189
  let refreshing = false;
@@ -121,12 +192,17 @@ async function UsageCoachPlugin(input) {
121
192
  if (refreshing) return;
122
193
  if (last && Date.now() - lastFetchedAt < TTL_MS) return;
123
194
  refreshing = true;
124
- fetchQuota().then((q) => {
195
+ fetchQuota().then(async (q) => {
125
196
  try {
126
197
  last = coach(q);
127
198
  lastFetchedAt = Date.now();
128
- writeState(last);
129
- log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}%`);
199
+ let providers = [];
200
+ try {
201
+ providers = await fetchProvidersCoach();
202
+ } catch {
203
+ }
204
+ writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
205
+ log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
130
206
  } catch (e) {
131
207
  log(`refresh-in-then err: ${String(e)}`);
132
208
  }
package/dist/tui.js CHANGED
@@ -1,14 +1,21 @@
1
1
  // src/tui.tsx
2
- import { setProp as _$setProp } from "@opentui/solid";
3
2
  import { insert as _$insert } from "@opentui/solid";
3
+ import { setProp as _$setProp } from "@opentui/solid";
4
+ import { effect as _$effect } from "@opentui/solid";
4
5
  import { createTextNode as _$createTextNode } from "@opentui/solid";
5
6
  import { insertNode as _$insertNode } from "@opentui/solid";
6
7
  import { createElement as _$createElement } from "@opentui/solid";
7
8
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
9
+ import { createHash } from "crypto";
8
10
  import { homedir } from "os";
9
- import { join } from "path";
11
+ import { join, resolve } from "path";
10
12
  import { createRoot, createSignal, onCleanup } from "solid-js";
11
- var STATE_DIR = process.env.UC_STATE_DIR ?? join(homedir(), ".cache", "opencode-usage-coach");
13
+ function projectStateDir(dir) {
14
+ const abs = resolve(dir || ".");
15
+ const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
16
+ return join(homedir(), ".cache", "opencode-usage-coach", "projects", h);
17
+ }
18
+ var STATE_DIR = join(homedir(), ".cache", "opencode-usage-coach");
12
19
  var STATE_FILE = join(STATE_DIR, "state.json");
13
20
  var HARNESS_FILE = join(STATE_DIR, "harness.json");
14
21
  var MARKER = join(STATE_DIR, "tui-loaded.txt");
@@ -50,11 +57,15 @@ function short(s, n) {
50
57
  return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
51
58
  }
52
59
  function initializeTui(api, disposeRoot) {
60
+ STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
61
+ STATE_FILE = join(STATE_DIR, "state.json");
62
+ HARNESS_FILE = join(STATE_DIR, "harness.json");
63
+ MARKER = join(STATE_DIR, "tui-loaded.txt");
53
64
  try {
54
65
  mkdirSync(STATE_DIR, {
55
66
  recursive: true
56
67
  });
57
- writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()}`);
68
+ writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
58
69
  } catch {
59
70
  }
60
71
  const [getState, setState] = createSignal(readState());
@@ -68,16 +79,49 @@ function initializeTui(api, disposeRoot) {
68
79
  }, 3e3);
69
80
  onCleanup(() => clearInterval(timer));
70
81
  onCleanup(() => disposeRoot());
71
- const statusDot = {
72
- generating: "\u25CF",
73
- grading: "\u25CF",
74
- revising: "\u25CF",
75
- completed: "\u25CF",
76
- failed: "\u25CF",
77
- timed_out: "\u25CF",
78
- halted_quota: "\u25CF"
82
+ const [collapsed, setCollapsed] = createSignal(false);
83
+ let cmdDispose;
84
+ try {
85
+ cmdDispose = api.command?.register?.(() => [{
86
+ title: "Toggle usage-coach panel",
87
+ value: "usage-coach-toggle",
88
+ category: "usage-coach",
89
+ keybind: "alt+h",
90
+ onSelect: () => {
91
+ setCollapsed((c) => !c);
92
+ }
93
+ }]);
94
+ } catch {
95
+ }
96
+ onCleanup(() => {
97
+ try {
98
+ cmdDispose?.();
99
+ } catch {
100
+ }
101
+ });
102
+ const statusKey = {
103
+ generating: "info",
104
+ grading: "accent",
105
+ revising: "warning",
106
+ completed: "success",
107
+ failed: "error",
108
+ timed_out: "error",
109
+ halted_quota: "error"
79
110
  };
80
111
  const panel = (ctx) => {
112
+ const th = ctx.theme?.current ?? {};
113
+ const st = (k) => ({
114
+ fg: th[k]
115
+ });
116
+ if (collapsed()) {
117
+ return (() => {
118
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
119
+ _$insertNode(_el$, _el$2);
120
+ _$insertNode(_el$2, _$createTextNode(`usage-coach (hidden \u2014 Alt+H)`));
121
+ _$effect((_$p) => _$setProp(_el$2, "style", st("textMuted"), _$p));
122
+ return _el$;
123
+ })();
124
+ }
81
125
  let s = null;
82
126
  try {
83
127
  s = getState();
@@ -92,98 +136,148 @@ function initializeTui(api, disposeRoot) {
92
136
  }
93
137
  const nodes = [];
94
138
  if (s) {
139
+ const dKey = s.decision === "GO" ? "success" : s.decision === "THROTTLE" ? "warning" : "error";
95
140
  nodes.push((() => {
96
- var _el$ = _$createElement("text"), _el$2 = _$createTextNode(`usage-coach [`), _el$3 = _$createTextNode(`]`);
97
- _$insertNode(_el$, _el$2);
98
- _$insertNode(_el$, _el$3);
99
- _$insert(_el$, () => TAG[s.decision], _el$3);
100
- return _el$;
101
- })());
102
- nodes.push((() => {
103
- var _el$4 = _$createElement("text"), _el$5 = _$createTextNode(` 5h `), _el$6 = _$createTextNode(` `), _el$7 = _$createTextNode(`%`);
141
+ var _el$4 = _$createElement("text"), _el$5 = _$createTextNode(`usage-coach [`), _el$6 = _$createTextNode(`]`);
104
142
  _$insertNode(_el$4, _el$5);
105
143
  _$insertNode(_el$4, _el$6);
106
- _$insertNode(_el$4, _el$7);
107
- _$insert(_el$4, () => bar(s.fiveHour), _el$6);
108
- _$insert(_el$4, () => s.fiveHour, _el$7);
144
+ _$insert(_el$4, () => TAG[s.decision], _el$6);
145
+ _$effect((_$p) => _$setProp(_el$4, "style", st(dKey), _$p));
109
146
  return _el$4;
110
147
  })());
148
+ if (s.providers && s.providers.length > 0) {
149
+ for (const p of s.providers) {
150
+ nodes.push((() => {
151
+ var _el$7 = _$createElement("text"), _el$8 = _$createTextNode(` `);
152
+ _$insertNode(_el$7, _el$8);
153
+ _$insert(_el$7, () => p.name, null);
154
+ _$effect((_$p) => _$setProp(_el$7, "style", st("textMuted"), _$p));
155
+ return _el$7;
156
+ })());
157
+ const k5 = p.fiveHour >= 70 ? "error" : p.fiveHour >= 40 ? "warning" : "success";
158
+ const kw = p.weekly >= 85 ? "error" : p.weekly >= 60 ? "warning" : "success";
159
+ nodes.push((() => {
160
+ var _el$9 = _$createElement("text"), _el$0 = _$createTextNode(` 5h `), _el$1 = _$createTextNode(` `), _el$10 = _$createTextNode(`% `);
161
+ _$insertNode(_el$9, _el$0);
162
+ _$insertNode(_el$9, _el$1);
163
+ _$insertNode(_el$9, _el$10);
164
+ _$insert(_el$9, () => bar(p.fiveHour), _el$1);
165
+ _$insert(_el$9, () => p.fiveHour, _el$10);
166
+ _$insert(_el$9, () => p.fiveHourReset, null);
167
+ _$effect((_$p) => _$setProp(_el$9, "style", st(k5), _$p));
168
+ return _el$9;
169
+ })());
170
+ nodes.push((() => {
171
+ var _el$11 = _$createElement("text"), _el$12 = _$createTextNode(` wk `), _el$13 = _$createTextNode(` `), _el$14 = _$createTextNode(`% `);
172
+ _$insertNode(_el$11, _el$12);
173
+ _$insertNode(_el$11, _el$13);
174
+ _$insertNode(_el$11, _el$14);
175
+ _$insert(_el$11, () => bar(p.weekly), _el$13);
176
+ _$insert(_el$11, () => p.weekly, _el$14);
177
+ _$insert(_el$11, () => p.weeklyReset, null);
178
+ _$effect((_$p) => _$setProp(_el$11, "style", st(kw), _$p));
179
+ return _el$11;
180
+ })());
181
+ nodes.push((() => {
182
+ var _el$15 = _$createElement("text"), _el$16 = _$createTextNode(` -> `);
183
+ _$insertNode(_el$15, _el$16);
184
+ _$insert(_el$15, () => p.advice, null);
185
+ _$effect((_$p) => _$setProp(_el$15, "style", st(dKey), _$p));
186
+ return _el$15;
187
+ })());
188
+ }
189
+ } else {
190
+ nodes.push((() => {
191
+ var _el$19 = _$createElement("text"), _el$20 = _$createTextNode(` 5h `), _el$21 = _$createTextNode(` `), _el$22 = _$createTextNode(`%`);
192
+ _$insertNode(_el$19, _el$20);
193
+ _$insertNode(_el$19, _el$21);
194
+ _$insertNode(_el$19, _el$22);
195
+ _$insert(_el$19, () => bar(s.fiveHour), _el$21);
196
+ _$insert(_el$19, () => s.fiveHour, _el$22);
197
+ return _el$19;
198
+ })());
199
+ nodes.push((() => {
200
+ var _el$23 = _$createElement("text"), _el$24 = _$createTextNode(` wk `), _el$25 = _$createTextNode(` `), _el$26 = _$createTextNode(`%`);
201
+ _$insertNode(_el$23, _el$24);
202
+ _$insertNode(_el$23, _el$25);
203
+ _$insertNode(_el$23, _el$26);
204
+ _$insert(_el$23, () => bar(s.weekly), _el$25);
205
+ _$insert(_el$23, () => s.weekly, _el$26);
206
+ return _el$23;
207
+ })());
208
+ nodes.push((() => {
209
+ var _el$27 = _$createElement("text"), _el$28 = _$createTextNode(` mo `), _el$29 = _$createTextNode(` `), _el$30 = _$createTextNode(`%`);
210
+ _$insertNode(_el$27, _el$28);
211
+ _$insertNode(_el$27, _el$29);
212
+ _$insertNode(_el$27, _el$30);
213
+ _$insert(_el$27, () => bar(s.monthly), _el$29);
214
+ _$insert(_el$27, () => s.monthly, _el$30);
215
+ return _el$27;
216
+ })());
217
+ }
218
+ } else {
111
219
  nodes.push((() => {
112
- var _el$8 = _$createElement("text"), _el$9 = _$createTextNode(` wk `), _el$0 = _$createTextNode(` `), _el$1 = _$createTextNode(`%`);
113
- _$insertNode(_el$8, _el$9);
114
- _$insertNode(_el$8, _el$0);
115
- _$insertNode(_el$8, _el$1);
116
- _$insert(_el$8, () => bar(s.weekly), _el$0);
117
- _$insert(_el$8, () => s.weekly, _el$1);
118
- return _el$8;
220
+ var _el$31 = _$createElement("text");
221
+ _$insertNode(_el$31, _$createTextNode(`usage-coach: ...`));
222
+ return _el$31;
119
223
  })());
224
+ }
225
+ if (h && h.active !== false && h.tasks.length > 0) {
120
226
  nodes.push((() => {
121
- var _el$10 = _$createElement("text"), _el$11 = _$createTextNode(` mo `), _el$12 = _$createTextNode(` `), _el$13 = _$createTextNode(`%`);
122
- _$insertNode(_el$10, _el$11);
123
- _$insertNode(_el$10, _el$12);
124
- _$insertNode(_el$10, _el$13);
125
- _$insert(_el$10, () => bar(s.monthly), _el$12);
126
- _$insert(_el$10, () => s.monthly, _el$13);
127
- return _el$10;
227
+ var _el$33 = _$createElement("text");
228
+ _$insertNode(_el$33, _$createTextNode(` `));
229
+ return _el$33;
128
230
  })());
129
- } else {
130
231
  nodes.push((() => {
131
- var _el$14 = _$createElement("text");
132
- _$insertNode(_el$14, _$createTextNode(`usage-coach: ...`));
133
- return _el$14;
232
+ var _el$35 = _$createElement("text"), _el$36 = _$createTextNode(`harness: `), _el$37 = _$createTextNode(` `), _el$38 = _$createTextNode(`/`);
233
+ _$insertNode(_el$35, _el$36);
234
+ _$insertNode(_el$35, _el$37);
235
+ _$insertNode(_el$35, _el$38);
236
+ _$insert(_el$35, () => h.name, _el$37);
237
+ _$insert(_el$35, () => h.current, _el$38);
238
+ _$insert(_el$35, () => h.total, null);
239
+ _$effect((_$p) => _$setProp(_el$35, "style", st("textMuted"), _$p));
240
+ return _el$35;
134
241
  })());
135
- }
136
- const hStatus = h && h.tasks.length > 0 ? `${h.name} ${h.current}/${h.total}` : "idle";
137
- nodes.push((() => {
138
- var _el$16 = _$createElement("text");
139
- _$insertNode(_el$16, _$createTextNode(` `));
140
- return _el$16;
141
- })());
142
- nodes.push((() => {
143
- var _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`harness: `);
144
- _$insertNode(_el$18, _el$19);
145
- _$insert(_el$18, hStatus, null);
146
- return _el$18;
147
- })());
148
- if (h && h.tasks.length > 0) {
149
242
  for (const t of h.tasks) {
150
- const dot = statusDot[t.status] ?? "\xB7";
243
+ const sKey = statusKey[t.status] ?? "text";
151
244
  const lbl = TLABEL[t.status] ?? t.status;
152
245
  const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
153
246
  const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
154
247
  nodes.push((() => {
155
- var _el$20 = _$createElement("text"), _el$21 = _$createTextNode(` `), _el$22 = _$createTextNode(` `), _el$23 = _$createTextNode(` `), _el$24 = _$createTextNode(` `);
156
- _$insertNode(_el$20, _el$21);
157
- _$insertNode(_el$20, _el$22);
158
- _$insertNode(_el$20, _el$23);
159
- _$insertNode(_el$20, _el$24);
160
- _$insert(_el$20, dot, _el$22);
161
- _$insert(_el$20, () => t.id, _el$23);
162
- _$insert(_el$20, mdl, _el$23);
163
- _$insert(_el$20, lbl, _el$24);
164
- _$insert(_el$20, rev, _el$24);
165
- _$insert(_el$20, () => short(t.title, 12), null);
166
- return _el$20;
248
+ var _el$39 = _$createElement("text"), _el$40 = _$createTextNode(` \u25CF `), _el$41 = _$createTextNode(` `), _el$42 = _$createTextNode(` `);
249
+ _$insertNode(_el$39, _el$40);
250
+ _$insertNode(_el$39, _el$41);
251
+ _$insertNode(_el$39, _el$42);
252
+ _$insert(_el$39, () => t.id, _el$41);
253
+ _$insert(_el$39, mdl, _el$41);
254
+ _$insert(_el$39, lbl, _el$42);
255
+ _$insert(_el$39, rev, _el$42);
256
+ _$insert(_el$39, () => short(t.title, 12), null);
257
+ _$effect((_$p) => _$setProp(_el$39, "style", st(sKey), _$p));
258
+ return _el$39;
167
259
  })());
168
260
  const pv = t.model ? t.model.startsWith("zai") ? "zai" : (t.model.split("/")[0] ?? "").split("-")[0] : "";
169
261
  const q = pv && h.quotas?.[pv] ? h.quotas[pv] : null;
170
262
  const pct = q ? q.fiveHour : 0;
263
+ const qKey = pct >= 70 ? "error" : pct >= 40 ? "warning" : "success";
171
264
  nodes.push((() => {
172
- var _el$25 = _$createElement("text"), _el$26 = _$createTextNode(` 5h `), _el$27 = _$createTextNode(` `), _el$28 = _$createTextNode(`%`);
173
- _$insertNode(_el$25, _el$26);
174
- _$insertNode(_el$25, _el$27);
175
- _$insertNode(_el$25, _el$28);
176
- _$insert(_el$25, () => bar(pct), _el$27);
177
- _$insert(_el$25, pct, _el$28);
178
- return _el$25;
265
+ var _el$43 = _$createElement("text"), _el$44 = _$createTextNode(` 5h `), _el$45 = _$createTextNode(` `), _el$46 = _$createTextNode(`%`);
266
+ _$insertNode(_el$43, _el$44);
267
+ _$insertNode(_el$43, _el$45);
268
+ _$insertNode(_el$43, _el$46);
269
+ _$insert(_el$43, () => bar(pct), _el$45);
270
+ _$insert(_el$43, pct, _el$46);
271
+ _$effect((_$p) => _$setProp(_el$43, "style", st(qKey), _$p));
272
+ return _el$43;
179
273
  })());
180
274
  }
181
275
  }
182
276
  return (() => {
183
- var _el$29 = _$createElement("box");
184
- _$setProp(_el$29, "flexDirection", "column");
185
- _$insert(_el$29, nodes);
186
- return _el$29;
277
+ var _el$47 = _$createElement("box");
278
+ _$setProp(_el$47, "flexDirection", "column");
279
+ _$insert(_el$47, nodes);
280
+ return _el$47;
187
281
  })();
188
282
  };
189
283
  api.slots.register({
@@ -194,9 +288,9 @@ function initializeTui(api, disposeRoot) {
194
288
  return panel(ctx);
195
289
  } catch {
196
290
  return (() => {
197
- var _el$30 = _$createElement("text");
198
- _$insertNode(_el$30, _$createTextNode(`usage-coach`));
199
- return _el$30;
291
+ var _el$48 = _$createElement("text");
292
+ _$insertNode(_el$48, _$createTextNode(`usage-coach`));
293
+ return _el$48;
200
294
  })();
201
295
  }
202
296
  },
@@ -205,9 +299,9 @@ function initializeTui(api, disposeRoot) {
205
299
  return panel(ctx);
206
300
  } catch {
207
301
  return (() => {
208
- var _el$32 = _$createElement("text");
209
- _$insertNode(_el$32, _$createTextNode(`usage-coach`));
210
- return _el$32;
302
+ var _el$50 = _$createElement("text");
303
+ _$insertNode(_el$50, _$createTextNode(`usage-coach`));
304
+ return _el$50;
211
305
  })();
212
306
  }
213
307
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.1.1",
4
- "description": "opencode closed-loop usage coach quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
3
+ "version": "0.1.3",
4
+ "description": "opencode closed-loop usage coach \u2014 quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -26,7 +26,16 @@
26
26
  "README.md",
27
27
  "LICENSE"
28
28
  ],
29
- "keywords": ["opencode", "opencode-plugin", "quota", "usage", "coach", "loop", "zai", "glm"],
29
+ "keywords": [
30
+ "opencode",
31
+ "opencode-plugin",
32
+ "quota",
33
+ "usage",
34
+ "coach",
35
+ "loop",
36
+ "zai",
37
+ "glm"
38
+ ],
30
39
  "license": "MIT",
31
40
  "peerDependencies": {
32
41
  "@opencode-ai/plugin": ">=1.14",
@@ -43,4 +52,4 @@
43
52
  "tsup": "^8.5",
44
53
  "typescript": "^5"
45
54
  }
46
- }
55
+ }