opencode-usage-coach 0.3.4 → 0.3.5

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/README.md CHANGED
@@ -28,6 +28,16 @@ the loop** — and ships a harness agent mode.
28
28
  explicitly ask "run this through the harness" or "use the harness for this". The harness
29
29
  tools are only available when the harness agent mode is active (see Install).
30
30
 
31
+ **Learning from failures (learning loop):**
32
+ - When `grade` returns FAIL, the harness enters a learning cycle: `record_failure` → `investigate` (root-cause analysis) → `verify_diagnosis` → `generalize` (extract a reusable rule).
33
+ - Rules accumulate in `rules.md` → the next `generate` call automatically includes them → the harness avoids repeating the same mistake.
34
+ - Tools: `record_failure`, `investigate`, `verify_diagnosis`, `generalize`.
35
+
36
+ **Domain knowledge base:**
37
+ - `investigate` and `generate` query a local domain DB before running — known facts are injected into the prompt ("Known facts from domain DB: ...").
38
+ - Unknown domains are investigated (webfetch/docs) and stored as graph nodes/edges → accumulates over time → evidence-based judgments instead of speculation.
39
+ - Storage: `nodes.ndjson` + `edges.ndjson` under the project state dir.
40
+
31
41
  ## Requirements
32
42
  - opencode (tested on 1.17.13) with a quota-metered provider configured.
33
43
  - `codexbar` CLI with your provider key wired (e.g. `codexbar config set-api-key --provider zai --stdin`).
@@ -203,9 +213,25 @@ Recurring issues and fixes — mostly learned the hard way during development.
203
213
  - Harness completion sets `active:false` → hidden from the TUI.
204
214
  - Override the state path with `UC_STATE_DIR` (forces global state).
205
215
 
216
+ **Key gotcha — opencode TUI `ctx` does NOT carry `session_id`.**
217
+ The slot context passed to `panel(ctx)` contains only `{ theme }`. There is no `session_id`/`sessionID` field. The current session ID lives in **`api.route.current.params.sessionID`** instead — the panel reads it from there. If you ever see harnesses from other sessions leaking in, the cause is almost certainly that `sid` resolved to empty (→ fallback broad scan).
218
+
219
+ **Debugging session isolation** (if it breaks again):
220
+ 1. Check `~/.cache/opencode-usage-coach/projects/<hash>/tui-debug.log` — is `panel` being called? What `routeSid` value?
221
+ 2. `api.route.current.params.sessionID` — populated? (Empty → panel falls back to scanning all sessions.)
222
+ 3. New TUI code loaded? `tui-loaded.txt` (MARKER) should show `loaded-v2 ...`. If it still says `loaded`, the new dist isn't being picked up.
223
+ 4. `appendFileSync` imported in `src/tui.tsx`? If missing, **all TUI debug logging silently fails** (ReferenceError swallowed by try/catch) — this wasted a lot of debugging time once.
224
+
225
+ **Past issue (fixed v0.3.4):** panel read `ctx.session_id` which was always `undefined` → fallback scanned every session → another session's active harness leaked in. Fixed by reading `api.route.current.params.sessionID`.
226
+
206
227
  ## Status
207
- - ✅ Quota guardian + TUI panel (per-provider coach view, colors, collapsible Alt+H)
208
- - ✅ Harness: agent mode (triage) with generate/grade model-specific tools (1 terminal, multi-model)
228
+ - ✅ Quota guardian + TUI panel (per-provider coach view, 5h/1w gauges, collapsible Alt+H)
229
+ - ✅ Harness: agent mode with generate/grade tools (multi-model, 1 terminal)
230
+ - ✅ Deterministic loop via NEXT directives (parallel PATH A / sequential PATH B)
231
+ - ✅ Quota-aware tools (GO/THROTTLE/STOP drive model selection + concurrency)
232
+ - ✅ Learning loop (record_failure → investigate → verify_diagnosis → generalize → rules.md)
233
+ - ✅ Domain knowledge base (graph store, investigate/generate injection)
234
+ - ✅ Session isolation (api.route, per-session harness state)
209
235
  - ✅ npm packaging (`opencode plugin install opencode-usage-coach`)
210
236
 
211
237
  License: MIT.
package/dist/index.js CHANGED
@@ -1,66 +1,145 @@
1
1
  // src/index.ts
2
- import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from "fs";
2
+ import { mkdirSync as mkdirSync2, writeFileSync, appendFileSync as appendFileSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
3
3
  import { spawn } from "child_process";
4
4
  import { createHash } from "crypto";
5
5
  import { homedir } from "os";
6
- import { join, resolve, dirname } from "path";
6
+ import { join as join2, resolve, dirname } from "path";
7
7
  import { tool } from "@opencode-ai/plugin";
8
+
9
+ // src/domain.ts
10
+ import { mkdirSync, appendFileSync, readFileSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+ var BASE_DIR = "";
13
+ function initDomain(stateDir) {
14
+ BASE_DIR = stateDir;
15
+ }
16
+ var nodesFile = () => join(BASE_DIR, "nodes.ndjson");
17
+ var edgesFile = () => join(BASE_DIR, "edges.ndjson");
18
+ function readNdjson(path) {
19
+ try {
20
+ if (!existsSync(path)) return [];
21
+ return readFileSync(path, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+ function readNodes() {
27
+ return readNdjson(nodesFile());
28
+ }
29
+ function readEdges() {
30
+ return readNdjson(edgesFile());
31
+ }
32
+ function uid(prefix) {
33
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
34
+ }
35
+ function addDomainNode(node) {
36
+ const full = { ...node, id: uid("node"), ts: (/* @__PURE__ */ new Date()).toISOString() };
37
+ try {
38
+ mkdirSync(BASE_DIR, { recursive: true });
39
+ appendFileSync(nodesFile(), JSON.stringify(full) + "\n");
40
+ } catch {
41
+ }
42
+ return full.id;
43
+ }
44
+ function queryDomain(keywords) {
45
+ const lc = keywords.map((k) => k.toLowerCase());
46
+ const nodes = readNodes();
47
+ const matched = nodes.filter((n) => {
48
+ const hay = (n.name + " " + JSON.stringify(n.props)).toLowerCase();
49
+ return lc.some((k) => k && hay.includes(k));
50
+ });
51
+ const ids = new Set(matched.map((n) => n.id));
52
+ const edges = readEdges().filter((e) => ids.has(e.from) || ids.has(e.to));
53
+ return { nodes: matched, edges };
54
+ }
55
+ function saveInvestigationResult(keywords, result, source) {
56
+ try {
57
+ return addDomainNode({
58
+ type: "fact",
59
+ name: keywords.join(" "),
60
+ props: { result },
61
+ source: source || "investigation",
62
+ confidence: 0.7
63
+ });
64
+ } catch {
65
+ return "";
66
+ }
67
+ }
68
+
69
+ // src/index.ts
8
70
  var PLUGIN_NAME = "opencode-usage-coach";
9
71
  var DEBUG = process.env.UC_DEBUG === "1";
10
72
  var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
11
- var STATE_DIR = join(homedir(), ".cache", "opencode-usage-coach");
12
- var STATE_FILE = join(STATE_DIR, "state.json");
13
- var HARNESS_FILE = join(STATE_DIR, "harness.json");
14
- var LOG_FILE = join(STATE_DIR, "coach.log");
73
+ var STATE_DIR = join2(homedir(), ".cache", "opencode-usage-coach");
74
+ var STATE_FILE = join2(STATE_DIR, "state.json");
75
+ var HARNESS_FILE = join2(STATE_DIR, "harness.json");
76
+ var LOG_FILE = join2(STATE_DIR, "coach.log");
15
77
  function projectStateDir(dir) {
16
78
  const abs = resolve(dir || ".");
17
79
  const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
18
- return join(homedir(), ".cache", "opencode-usage-coach", "projects", h);
80
+ return join2(homedir(), ".cache", "opencode-usage-coach", "projects", h);
19
81
  }
20
82
  function setStateDir(dir) {
21
83
  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");
84
+ STATE_FILE = join2(STATE_DIR, "state.json");
85
+ HARNESS_FILE = join2(STATE_DIR, "harness.json");
86
+ LOG_FILE = join2(STATE_DIR, "coach.log");
25
87
  }
26
88
  var NOOP_HOOKS = {};
27
89
  function log(msg) {
28
90
  try {
29
- appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
91
+ appendFileSync2(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
30
92
  `);
31
93
  } catch {
32
94
  }
33
95
  }
34
96
  function writeState(c) {
35
97
  try {
36
- mkdirSync(STATE_DIR, { recursive: true });
98
+ mkdirSync2(STATE_DIR, { recursive: true });
37
99
  writeFileSync(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
38
100
  } catch {
39
101
  }
40
102
  }
41
103
  function rulesFile() {
42
- return join(STATE_DIR, "rules.md");
104
+ return join2(STATE_DIR, "rules.md");
43
105
  }
44
106
  function failuresFile() {
45
- return join(STATE_DIR, "failures.ndjson");
107
+ return join2(STATE_DIR, "failures.ndjson");
46
108
  }
47
109
  function readRules() {
48
110
  try {
49
111
  const f = rulesFile();
50
- if (!existsSync(f)) return "";
51
- return readFileSync(f, "utf8").trim();
112
+ if (!existsSync2(f)) return "";
113
+ return readFileSync2(f, "utf8").trim();
52
114
  } catch {
53
115
  return "";
54
116
  }
55
117
  }
118
+ function extractKeywords(text) {
119
+ try {
120
+ const STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "that", "this", "from", "into", "your", "you", "are", "was", "but", "not", "all", "any", "use", "task", "prompt"]);
121
+ const seen = /* @__PURE__ */ new Set();
122
+ const out = [];
123
+ for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_]+/)) {
124
+ const t = raw.trim();
125
+ if (t.length < 3 || STOP.has(t) || seen.has(t)) continue;
126
+ seen.add(t);
127
+ out.push(t);
128
+ if (out.length >= 16) break;
129
+ }
130
+ return out;
131
+ } catch {
132
+ return [];
133
+ }
134
+ }
56
135
  function harnessFile(sessionID) {
57
- return join(STATE_DIR, sessionID || "_default", "harness.json");
136
+ return join2(STATE_DIR, sessionID || "_default", "harness.json");
58
137
  }
59
138
  function readHarness(sessionID) {
60
139
  try {
61
140
  const f = harnessFile(sessionID);
62
- if (!existsSync(f)) return null;
63
- return JSON.parse(readFileSync(f, "utf8"));
141
+ if (!existsSync2(f)) return null;
142
+ return JSON.parse(readFileSync2(f, "utf8"));
64
143
  } catch {
65
144
  return null;
66
145
  }
@@ -68,7 +147,7 @@ function readHarness(sessionID) {
68
147
  function writeHarness(sessionID, h) {
69
148
  try {
70
149
  const f = harnessFile(sessionID);
71
- mkdirSync(dirname(f), { recursive: true });
150
+ mkdirSync2(dirname(f), { recursive: true });
72
151
  h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
73
152
  writeFileSync(f, JSON.stringify(h, null, 2));
74
153
  } catch {
@@ -77,14 +156,14 @@ function writeHarness(sessionID, h) {
77
156
  function readHarnessCfg(dir) {
78
157
  const tryRead = (p) => {
79
158
  try {
80
- if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
159
+ if (existsSync2(p)) return JSON.parse(readFileSync2(p, "utf8"));
81
160
  } catch {
82
161
  }
83
162
  return {};
84
163
  };
85
164
  return {
86
- ...tryRead(join(homedir(), ".config", "opencode-usage-coach", "harness.config.json")),
87
- ...tryRead(join(dir, "harness.config.json"))
165
+ ...tryRead(join2(homedir(), ".config", "opencode-usage-coach", "harness.config.json")),
166
+ ...tryRead(join2(dir, "harness.config.json"))
88
167
  };
89
168
  }
90
169
  async function runModel(client, model, prompt, directory) {
@@ -105,7 +184,12 @@ async function runModel(client, model, prompt, directory) {
105
184
  const parts = resp?.data?.parts ?? resp?.parts ?? [];
106
185
  const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
107
186
  try {
108
- await client.session.remove?.({ path: { id } });
187
+ const summary = await client.session.summarize?.({ path: { id } });
188
+ log(`runModel(${model}): sub-session summary: ${JSON.stringify(summary?.data ?? summary).slice(0, 300)}`);
189
+ } catch {
190
+ }
191
+ try {
192
+ await client.session.delete?.({ path: { id } });
109
193
  } catch {
110
194
  }
111
195
  log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
@@ -245,6 +329,7 @@ var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, month
245
329
  async function UsageCoachPlugin(input) {
246
330
  try {
247
331
  setStateDir(input.directory);
332
+ initDomain(STATE_DIR);
248
333
  const cfg0 = readHarnessCfg(input.directory);
249
334
  const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
250
335
  const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
@@ -265,6 +350,10 @@ async function UsageCoachPlugin(input) {
265
350
  providers = await fetchProvidersCoach();
266
351
  } catch {
267
352
  }
353
+ if (providers.length > 0 && last.weekly < 0) {
354
+ const p0 = providers[0];
355
+ last = { ...last, weekly: p0.weekly, fiveHour: p0.fiveHour, monthly: p0.weekly >= 0 ? 0 : -1, advice: p0.advice, decision: p0.weekly >= STOP_WK ? "STOP" : p0.weekly >= THR_WK ? "THROTTLE" : "GO" };
356
+ }
268
357
  writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
269
358
  log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
270
359
  } catch (e) {
@@ -295,7 +384,9 @@ async function UsageCoachPlugin(input) {
295
384
  log(`event err: ${String(e)}`);
296
385
  }
297
386
  },
298
- // ACT(1) hard gate: only intentional STOP throws. Our own bugs never block.
387
+ // ACT(1) hard gate ONLY for harness tools (generate/grade/etc.) that consume quota.
388
+ // General tools (read/edit/bash/grep/task) are NEVER blocked — they don't consume model quota.
389
+ // This ensures Agent-Factory-Coordinator and other modes work freely even at STOP.
299
390
  "tool.execute.before": async (_input) => {
300
391
  let decision = "GO";
301
392
  try {
@@ -304,7 +395,10 @@ async function UsageCoachPlugin(input) {
304
395
  decision = "GO";
305
396
  }
306
397
  if (decision === "STOP") {
307
- throw new Error(`[${PLUGIN_NAME}] blocked: quota limit exceeded. ${current().advice}`);
398
+ const harnessTools = ["generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize"];
399
+ if (harnessTools.includes(_input.tool)) {
400
+ throw new Error(`[${PLUGIN_NAME}] blocked: quota limit exceeded. ${current().advice}`);
401
+ }
308
402
  }
309
403
  },
310
404
  // ACT(2) inject coaching into system prompt (double defense). Silent on error.
@@ -361,9 +455,12 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
361
455
  model: tool.schema.string().optional()
362
456
  },
363
457
  async execute(args, ctx) {
458
+ const cfg = readHarnessCfg(ctx.directory);
364
459
  const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
365
460
  h.tasks = h.tasks.filter((x) => x.id !== args.id);
366
- h.tasks.push({ id: args.id, title: args.title, status: args.status, model: args.model ?? "", revisions: args.revisions ?? 0, score: args.score ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
461
+ const model = args.model || cfg.generator || "";
462
+ if (!model) return `ERROR: task ${args.id} has no model and no generator configured. Set "generator" in harness.config.json.`;
463
+ h.tasks.push({ id: args.id, title: args.title, status: args.status, model, revisions: args.revisions ?? 0, score: args.score ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
367
464
  if (args.id > h.current) h.current = args.id;
368
465
  writeHarness(ctx.sessionID, h);
369
466
  return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
@@ -394,8 +491,8 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
394
491
  async execute(args, _ctx) {
395
492
  const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
396
493
  try {
397
- mkdirSync(STATE_DIR, { recursive: true });
398
- appendFileSync(failuresFile(), JSON.stringify(rec) + "\n");
494
+ mkdirSync2(STATE_DIR, { recursive: true });
495
+ appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
399
496
  } catch (e) {
400
497
  log(`record_failure err: ${String(e)}`);
401
498
  }
@@ -413,6 +510,25 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
413
510
  async execute(args, ctx) {
414
511
  const cfg = readHarnessCfg(ctx.directory);
415
512
  if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
513
+ let domainPrefix = "";
514
+ let keywords = [];
515
+ let domainEmpty = true;
516
+ try {
517
+ keywords = extractKeywords(`${args.task} ${args.gradeResult}`);
518
+ if (keywords.length) {
519
+ const { nodes, edges } = queryDomain(keywords);
520
+ if (nodes && nodes.length || edges && edges.length) {
521
+ domainEmpty = false;
522
+ domainPrefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
523
+
524
+ ---
525
+
526
+ `;
527
+ }
528
+ }
529
+ } catch (e) {
530
+ log(`investigate domain query err: ${String(e)}`);
531
+ }
416
532
  const rcaPrompt = `A task failed. Analyze the ROOT CAUSE (not just the symptom).
417
533
  Task: ${args.task}
418
534
  What was expected (from grade): ${args.gradeResult}
@@ -421,7 +537,14 @@ Output a structured root cause:
421
537
  category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
422
538
  explanation: <why it failed>
423
539
  evidence: <file/line or specific quote>`;
424
- const out = await runModel(input.client, cfg.generator, rcaPrompt, ctx.directory);
540
+ const out = await runModel(input.client, cfg.generator, domainPrefix + rcaPrompt, ctx.directory);
541
+ if (domainEmpty && keywords.length) {
542
+ try {
543
+ saveInvestigationResult(keywords, out, "investigate");
544
+ } catch (e) {
545
+ log(`investigate save err: ${String(e)}`);
546
+ }
547
+ }
425
548
  return out + "\n[usage-coach NEXT] call verify_diagnosis with this diagnosis.";
426
549
  }
427
550
  }),
@@ -473,8 +596,8 @@ Keep it concrete and actionable.`;
473
596
  const rule = out;
474
597
  try {
475
598
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
476
- mkdirSync(STATE_DIR, { recursive: true });
477
- appendFileSync(rulesFile(), `## Rule (${date})
599
+ mkdirSync2(STATE_DIR, { recursive: true });
600
+ appendFileSync2(rulesFile(), `## Rule (${date})
478
601
  ${rule}
479
602
  Origin: ${args.task}
480
603
 
@@ -502,13 +625,38 @@ Origin: ${args.task}
502
625
  const throttle = decision === "THROTTLE" && cfg.lighterModel;
503
626
  const model = throttle ? cfg.lighterModel : cfg.generator;
504
627
  const rules = readRules();
505
- const prefix = rules ? `Lessons learned from previous failures (apply where relevant):
628
+ let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
506
629
  ${rules}
507
630
 
508
631
  ---
509
632
 
510
633
  ` : "";
634
+ let keywords = [];
635
+ let domainEmpty = true;
636
+ try {
637
+ keywords = extractKeywords(args.prompt);
638
+ if (keywords.length) {
639
+ const { nodes, edges } = queryDomain(keywords);
640
+ if (nodes && nodes.length || edges && edges.length) {
641
+ domainEmpty = false;
642
+ prefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
643
+
644
+ ---
645
+
646
+ ` + prefix;
647
+ }
648
+ }
649
+ } catch (e) {
650
+ log(`generate domain query err: ${String(e)}`);
651
+ }
511
652
  const out = await runModel(input.client, model, prefix + args.prompt, ctx.directory);
653
+ if (domainEmpty && keywords.length) {
654
+ try {
655
+ saveInvestigationResult(keywords, out, "generate");
656
+ } catch (e) {
657
+ log(`generate save err: ${String(e)}`);
658
+ }
659
+ }
512
660
  return out + (throttle ? `
513
661
  [usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
514
662
  [usage-coach NEXT] call task_update(i, title, "grading"), then grade to evaluate this work.`;
package/dist/tui.js CHANGED
@@ -86,10 +86,12 @@ var TLABEL = {
86
86
  halted_quota: "quota-halt"
87
87
  };
88
88
  function barFill(p) {
89
- return "\u2588".repeat(Math.max(0, Math.min(10, Math.round(p / 10))));
89
+ const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
90
+ return "\u2588".repeat(n);
90
91
  }
91
92
  function barEmpty(p) {
92
- return "\u2591".repeat(10 - Math.max(0, Math.min(10, Math.round(p / 10))));
93
+ const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
94
+ return "\u2591".repeat(10 - n);
93
95
  }
94
96
  function initializeTui(api, disposeRoot) {
95
97
  STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
@@ -236,7 +238,7 @@ function initializeTui(api, disposeRoot) {
236
238
  _$insert(_el$12, () => p.fiveHour, _el$14);
237
239
  _$insert(_el$12, () => p.fiveHourReset, null);
238
240
  _$effect((_p$) => {
239
- var _v$ = st("text"), _v$2 = st("textMuted");
241
+ var _v$ = st("text"), _v$2 = st("text");
240
242
  _v$ !== _p$.e && (_p$.e = _$setProp(_el$10, "style", _v$, _p$.e));
241
243
  _v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$2, _p$.t));
242
244
  return _p$;
@@ -253,7 +255,7 @@ function initializeTui(api, disposeRoot) {
253
255
  _$insertNode(_el$15, _el$19);
254
256
  _$insertNode(_el$15, _el$20);
255
257
  _$setProp(_el$15, "flexDirection", "row");
256
- _$insertNode(_el$16, _$createTextNode(` wk `));
258
+ _$insertNode(_el$16, _$createTextNode(` 1w `));
257
259
  _$insert(_el$18, () => barFill(p.weekly));
258
260
  _$insert(_el$19, () => barEmpty(p.weekly));
259
261
  _$insertNode(_el$20, _el$21);
@@ -261,7 +263,7 @@ function initializeTui(api, disposeRoot) {
261
263
  _$insert(_el$20, () => p.weekly, _el$22);
262
264
  _$insert(_el$20, () => p.weeklyReset, null);
263
265
  _$effect((_p$) => {
264
- var _v$3 = st("text"), _v$4 = st("textMuted");
266
+ var _v$3 = st("text"), _v$4 = st("text");
265
267
  _v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "style", _v$3, _p$.e));
266
268
  _v$4 !== _p$.t && (_p$.t = _$setProp(_el$19, "style", _v$4, _p$.t));
267
269
  return _p$;
@@ -281,7 +283,7 @@ function initializeTui(api, disposeRoot) {
281
283
  }
282
284
  } else {
283
285
  nodes.push((() => {
284
- var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text"), _el$33 = _$createTextNode(` `), _el$34 = _$createTextNode(`%`);
286
+ var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text");
285
287
  _$insertNode(_el$27, _el$28);
286
288
  _$insertNode(_el$27, _el$30);
287
289
  _$insertNode(_el$27, _el$31);
@@ -290,11 +292,9 @@ function initializeTui(api, disposeRoot) {
290
292
  _$insertNode(_el$28, _$createTextNode(` 5h `));
291
293
  _$insert(_el$30, () => barFill(s.fiveHour));
292
294
  _$insert(_el$31, () => barEmpty(s.fiveHour));
293
- _$insertNode(_el$32, _el$33);
294
- _$insertNode(_el$32, _el$34);
295
- _$insert(_el$32, () => s.fiveHour, _el$34);
295
+ _$insertNode(_el$32, _$createTextNode(` 0%`));
296
296
  _$effect((_p$) => {
297
- var _v$5 = st("text"), _v$6 = st("textMuted");
297
+ var _v$5 = st("text"), _v$6 = st("text");
298
298
  _v$5 !== _p$.e && (_p$.e = _$setProp(_el$30, "style", _v$5, _p$.e));
299
299
  _v$6 !== _p$.t && (_p$.t = _$setProp(_el$31, "style", _v$6, _p$.t));
300
300
  return _p$;
@@ -305,77 +305,51 @@ function initializeTui(api, disposeRoot) {
305
305
  return _el$27;
306
306
  })());
307
307
  nodes.push((() => {
308
- var _el$35 = _$createElement("box"), _el$36 = _$createElement("text"), _el$38 = _$createElement("text"), _el$39 = _$createElement("text"), _el$40 = _$createElement("text"), _el$41 = _$createTextNode(` `), _el$42 = _$createTextNode(`%`);
309
- _$insertNode(_el$35, _el$36);
310
- _$insertNode(_el$35, _el$38);
311
- _$insertNode(_el$35, _el$39);
312
- _$insertNode(_el$35, _el$40);
313
- _$setProp(_el$35, "flexDirection", "row");
314
- _$insertNode(_el$36, _$createTextNode(` wk `));
315
- _$insert(_el$38, () => barFill(s.weekly));
316
- _$insert(_el$39, () => barEmpty(s.weekly));
317
- _$insertNode(_el$40, _el$41);
318
- _$insertNode(_el$40, _el$42);
319
- _$insert(_el$40, () => s.weekly, _el$42);
308
+ var _el$34 = _$createElement("box"), _el$35 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createElement("text"), _el$39 = _$createElement("text");
309
+ _$insertNode(_el$34, _el$35);
310
+ _$insertNode(_el$34, _el$37);
311
+ _$insertNode(_el$34, _el$38);
312
+ _$insertNode(_el$34, _el$39);
313
+ _$setProp(_el$34, "flexDirection", "row");
314
+ _$insertNode(_el$35, _$createTextNode(` 1w `));
315
+ _$insert(_el$37, () => barFill(s.weekly));
316
+ _$insert(_el$38, () => barEmpty(s.weekly));
317
+ _$insertNode(_el$39, _$createTextNode(` 0%`));
320
318
  _$effect((_p$) => {
321
- var _v$7 = st("text"), _v$8 = st("textMuted");
322
- _v$7 !== _p$.e && (_p$.e = _$setProp(_el$38, "style", _v$7, _p$.e));
323
- _v$8 !== _p$.t && (_p$.t = _$setProp(_el$39, "style", _v$8, _p$.t));
319
+ var _v$7 = st("text"), _v$8 = st("text");
320
+ _v$7 !== _p$.e && (_p$.e = _$setProp(_el$37, "style", _v$7, _p$.e));
321
+ _v$8 !== _p$.t && (_p$.t = _$setProp(_el$38, "style", _v$8, _p$.t));
324
322
  return _p$;
325
323
  }, {
326
324
  e: void 0,
327
325
  t: void 0
328
326
  });
329
- return _el$35;
330
- })());
331
- nodes.push((() => {
332
- var _el$43 = _$createElement("box"), _el$44 = _$createElement("text"), _el$46 = _$createElement("text"), _el$47 = _$createElement("text"), _el$48 = _$createElement("text"), _el$49 = _$createTextNode(` `), _el$50 = _$createTextNode(`%`);
333
- _$insertNode(_el$43, _el$44);
334
- _$insertNode(_el$43, _el$46);
335
- _$insertNode(_el$43, _el$47);
336
- _$insertNode(_el$43, _el$48);
337
- _$setProp(_el$43, "flexDirection", "row");
338
- _$insertNode(_el$44, _$createTextNode(` mo `));
339
- _$insert(_el$46, () => barFill(s.monthly));
340
- _$insert(_el$47, () => barEmpty(s.monthly));
341
- _$insertNode(_el$48, _el$49);
342
- _$insertNode(_el$48, _el$50);
343
- _$insert(_el$48, () => s.monthly, _el$50);
344
- _$effect((_p$) => {
345
- var _v$9 = st("text"), _v$0 = st("textMuted");
346
- _v$9 !== _p$.e && (_p$.e = _$setProp(_el$46, "style", _v$9, _p$.e));
347
- _v$0 !== _p$.t && (_p$.t = _$setProp(_el$47, "style", _v$0, _p$.t));
348
- return _p$;
349
- }, {
350
- e: void 0,
351
- t: void 0
352
- });
353
- return _el$43;
327
+ return _el$34;
354
328
  })());
355
329
  }
356
330
  } else {
357
331
  nodes.push((() => {
358
- var _el$51 = _$createElement("text");
359
- _$insertNode(_el$51, _$createTextNode(`usage-coach: ...`));
360
- return _el$51;
332
+ var _el$41 = _$createElement("text");
333
+ _$insertNode(_el$41, _$createTextNode(`usage-coach: ...`));
334
+ return _el$41;
361
335
  })());
362
336
  }
363
337
  if (h && h.active !== false && h.tasks.length > 0) {
364
338
  nodes.push((() => {
365
- var _el$53 = _$createElement("text");
366
- _$insertNode(_el$53, _$createTextNode(` `));
367
- return _el$53;
339
+ var _el$43 = _$createElement("text");
340
+ _$insertNode(_el$43, _$createTextNode(` `));
341
+ return _el$43;
368
342
  })());
369
343
  nodes.push((() => {
370
- var _el$55 = _$createElement("text"), _el$56 = _$createTextNode(`harness: `), _el$57 = _$createTextNode(` `), _el$58 = _$createTextNode(`/`);
371
- _$insertNode(_el$55, _el$56);
372
- _$insertNode(_el$55, _el$57);
373
- _$insertNode(_el$55, _el$58);
374
- _$insert(_el$55, () => h.name, _el$57);
375
- _$insert(_el$55, () => h.current, _el$58);
376
- _$insert(_el$55, () => h.total, null);
377
- _$effect((_$p) => _$setProp(_el$55, "style", st("textMuted"), _$p));
378
- return _el$55;
344
+ var _el$45 = _$createElement("text"), _el$46 = _$createTextNode(`harness: `), _el$47 = _$createTextNode(` `), _el$48 = _$createTextNode(`/`);
345
+ _$insertNode(_el$45, _el$46);
346
+ _$insertNode(_el$45, _el$47);
347
+ _$insertNode(_el$45, _el$48);
348
+ _$insert(_el$45, () => h.name, _el$47);
349
+ _$insert(_el$45, () => h.current, _el$48);
350
+ _$insert(_el$45, () => h.total, null);
351
+ _$effect((_$p) => _$setProp(_el$45, "style", st("textMuted"), _$p));
352
+ return _el$45;
379
353
  })());
380
354
  for (const t of h.tasks) {
381
355
  const sKey = statusKey[t.status] ?? "text";
@@ -385,54 +359,54 @@ function initializeTui(api, disposeRoot) {
385
359
  const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
386
360
  const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
387
361
  nodes.push((() => {
388
- var _el$59 = _$createElement("text"), _el$60 = _$createTextNode(` \u25CF `), _el$61 = _$createTextNode(` `), _el$62 = _$createTextNode(` `);
389
- _$insertNode(_el$59, _el$60);
390
- _$insertNode(_el$59, _el$61);
391
- _$insertNode(_el$59, _el$62);
392
- _$insert(_el$59, () => t.id, _el$61);
393
- _$insert(_el$59, mdl, _el$61);
394
- _$insert(_el$59, lbl, _el$62);
395
- _$insert(_el$59, rev, _el$62);
396
- _$insert(_el$59, elapsedStr, _el$62);
397
- _$insert(_el$59, () => t.title, null);
398
- _$effect((_$p) => _$setProp(_el$59, "style", st(sKey), _$p));
399
- return _el$59;
362
+ var _el$49 = _$createElement("text"), _el$50 = _$createTextNode(` \u25CF `), _el$51 = _$createTextNode(` `), _el$52 = _$createTextNode(` `);
363
+ _$insertNode(_el$49, _el$50);
364
+ _$insertNode(_el$49, _el$51);
365
+ _$insertNode(_el$49, _el$52);
366
+ _$insert(_el$49, () => t.id, _el$51);
367
+ _$insert(_el$49, mdl, _el$51);
368
+ _$insert(_el$49, lbl, _el$52);
369
+ _$insert(_el$49, rev, _el$52);
370
+ _$insert(_el$49, elapsedStr, _el$52);
371
+ _$insert(_el$49, () => t.title, null);
372
+ _$effect((_$p) => _$setProp(_el$49, "style", st(sKey), _$p));
373
+ return _el$49;
400
374
  })());
401
375
  const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
402
- const provCoach = s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id));
403
- const rawPct = provCoach ? provCoach.fiveHour : s?.fiveHour ?? 0;
376
+ const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
377
+ const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
404
378
  const pct = rawPct < 0 ? 0 : rawPct;
405
379
  const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
406
380
  nodes.push((() => {
407
- var _el$63 = _$createElement("box"), _el$64 = _$createElement("text"), _el$66 = _$createElement("text"), _el$67 = _$createElement("text"), _el$68 = _$createElement("text"), _el$69 = _$createTextNode(` `);
408
- _$insertNode(_el$63, _el$64);
409
- _$insertNode(_el$63, _el$66);
410
- _$insertNode(_el$63, _el$67);
411
- _$insertNode(_el$63, _el$68);
412
- _$setProp(_el$63, "flexDirection", "row");
413
- _$insertNode(_el$64, _$createTextNode(` 5h `));
414
- _$insert(_el$66, () => barFill(pct));
415
- _$insert(_el$67, () => barEmpty(pct));
416
- _$insertNode(_el$68, _el$69);
417
- _$insert(_el$68, pctLabel, null);
381
+ var _el$53 = _$createElement("box"), _el$54 = _$createElement("text"), _el$56 = _$createElement("text"), _el$57 = _$createElement("text"), _el$58 = _$createElement("text"), _el$59 = _$createTextNode(` `);
382
+ _$insertNode(_el$53, _el$54);
383
+ _$insertNode(_el$53, _el$56);
384
+ _$insertNode(_el$53, _el$57);
385
+ _$insertNode(_el$53, _el$58);
386
+ _$setProp(_el$53, "flexDirection", "row");
387
+ _$insertNode(_el$54, _$createTextNode(` 5h `));
388
+ _$insert(_el$56, () => barFill(pct));
389
+ _$insert(_el$57, () => barEmpty(pct));
390
+ _$insertNode(_el$58, _el$59);
391
+ _$insert(_el$58, pctLabel, null);
418
392
  _$effect((_p$) => {
419
- var _v$1 = st("text"), _v$10 = st("textMuted");
420
- _v$1 !== _p$.e && (_p$.e = _$setProp(_el$66, "style", _v$1, _p$.e));
421
- _v$10 !== _p$.t && (_p$.t = _$setProp(_el$67, "style", _v$10, _p$.t));
393
+ var _v$9 = st("text"), _v$0 = st("text");
394
+ _v$9 !== _p$.e && (_p$.e = _$setProp(_el$56, "style", _v$9, _p$.e));
395
+ _v$0 !== _p$.t && (_p$.t = _$setProp(_el$57, "style", _v$0, _p$.t));
422
396
  return _p$;
423
397
  }, {
424
398
  e: void 0,
425
399
  t: void 0
426
400
  });
427
- return _el$63;
401
+ return _el$53;
428
402
  })());
429
403
  }
430
404
  }
431
405
  return (() => {
432
- var _el$70 = _$createElement("box");
433
- _$setProp(_el$70, "flexDirection", "column");
434
- _$insert(_el$70, nodes);
435
- return _el$70;
406
+ var _el$60 = _$createElement("box");
407
+ _$setProp(_el$60, "flexDirection", "column");
408
+ _$insert(_el$60, nodes);
409
+ return _el$60;
436
410
  })();
437
411
  };
438
412
  tlog("registering slots");
@@ -446,9 +420,9 @@ function initializeTui(api, disposeRoot) {
446
420
  } catch (e) {
447
421
  tlog(`sidebar_footer err: ${String(e)}`);
448
422
  return (() => {
449
- var _el$71 = _$createElement("text");
450
- _$insertNode(_el$71, _$createTextNode(`usage-coach`));
451
- return _el$71;
423
+ var _el$61 = _$createElement("text");
424
+ _$insertNode(_el$61, _$createTextNode(`usage-coach`));
425
+ return _el$61;
452
426
  })();
453
427
  }
454
428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
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",