opencode-usage-coach 0.3.3 → 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 +28 -2
- package/dist/index.js +288 -28
- package/dist/tui.js +111 -109
- package/package.json +1 -1
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,
|
|
208
|
-
- ✅ Harness: agent mode
|
|
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,63 +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 =
|
|
12
|
-
var STATE_FILE =
|
|
13
|
-
var HARNESS_FILE =
|
|
14
|
-
var LOG_FILE =
|
|
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
|
|
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 =
|
|
23
|
-
HARNESS_FILE =
|
|
24
|
-
LOG_FILE =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
104
|
+
return join2(STATE_DIR, "rules.md");
|
|
105
|
+
}
|
|
106
|
+
function failuresFile() {
|
|
107
|
+
return join2(STATE_DIR, "failures.ndjson");
|
|
43
108
|
}
|
|
44
109
|
function readRules() {
|
|
45
110
|
try {
|
|
46
111
|
const f = rulesFile();
|
|
47
|
-
if (!
|
|
48
|
-
return
|
|
112
|
+
if (!existsSync2(f)) return "";
|
|
113
|
+
return readFileSync2(f, "utf8").trim();
|
|
49
114
|
} catch {
|
|
50
115
|
return "";
|
|
51
116
|
}
|
|
52
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
|
+
}
|
|
53
135
|
function harnessFile(sessionID) {
|
|
54
|
-
return
|
|
136
|
+
return join2(STATE_DIR, sessionID || "_default", "harness.json");
|
|
55
137
|
}
|
|
56
138
|
function readHarness(sessionID) {
|
|
57
139
|
try {
|
|
58
140
|
const f = harnessFile(sessionID);
|
|
59
|
-
if (!
|
|
60
|
-
return JSON.parse(
|
|
141
|
+
if (!existsSync2(f)) return null;
|
|
142
|
+
return JSON.parse(readFileSync2(f, "utf8"));
|
|
61
143
|
} catch {
|
|
62
144
|
return null;
|
|
63
145
|
}
|
|
@@ -65,7 +147,7 @@ function readHarness(sessionID) {
|
|
|
65
147
|
function writeHarness(sessionID, h) {
|
|
66
148
|
try {
|
|
67
149
|
const f = harnessFile(sessionID);
|
|
68
|
-
|
|
150
|
+
mkdirSync2(dirname(f), { recursive: true });
|
|
69
151
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
70
152
|
writeFileSync(f, JSON.stringify(h, null, 2));
|
|
71
153
|
} catch {
|
|
@@ -74,14 +156,14 @@ function writeHarness(sessionID, h) {
|
|
|
74
156
|
function readHarnessCfg(dir) {
|
|
75
157
|
const tryRead = (p) => {
|
|
76
158
|
try {
|
|
77
|
-
if (
|
|
159
|
+
if (existsSync2(p)) return JSON.parse(readFileSync2(p, "utf8"));
|
|
78
160
|
} catch {
|
|
79
161
|
}
|
|
80
162
|
return {};
|
|
81
163
|
};
|
|
82
164
|
return {
|
|
83
|
-
...tryRead(
|
|
84
|
-
...tryRead(
|
|
165
|
+
...tryRead(join2(homedir(), ".config", "opencode-usage-coach", "harness.config.json")),
|
|
166
|
+
...tryRead(join2(dir, "harness.config.json"))
|
|
85
167
|
};
|
|
86
168
|
}
|
|
87
169
|
async function runModel(client, model, prompt, directory) {
|
|
@@ -102,7 +184,12 @@ async function runModel(client, model, prompt, directory) {
|
|
|
102
184
|
const parts = resp?.data?.parts ?? resp?.parts ?? [];
|
|
103
185
|
const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
|
|
104
186
|
try {
|
|
105
|
-
await client.session.
|
|
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 } });
|
|
106
193
|
} catch {
|
|
107
194
|
}
|
|
108
195
|
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
|
|
@@ -242,6 +329,7 @@ var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, month
|
|
|
242
329
|
async function UsageCoachPlugin(input) {
|
|
243
330
|
try {
|
|
244
331
|
setStateDir(input.directory);
|
|
332
|
+
initDomain(STATE_DIR);
|
|
245
333
|
const cfg0 = readHarnessCfg(input.directory);
|
|
246
334
|
const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
|
|
247
335
|
const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
|
|
@@ -262,6 +350,10 @@ async function UsageCoachPlugin(input) {
|
|
|
262
350
|
providers = await fetchProvidersCoach();
|
|
263
351
|
} catch {
|
|
264
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
|
+
}
|
|
265
357
|
writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
266
358
|
log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
|
|
267
359
|
} catch (e) {
|
|
@@ -292,7 +384,9 @@ async function UsageCoachPlugin(input) {
|
|
|
292
384
|
log(`event err: ${String(e)}`);
|
|
293
385
|
}
|
|
294
386
|
},
|
|
295
|
-
// ACT(1) hard gate
|
|
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.
|
|
296
390
|
"tool.execute.before": async (_input) => {
|
|
297
391
|
let decision = "GO";
|
|
298
392
|
try {
|
|
@@ -301,7 +395,10 @@ async function UsageCoachPlugin(input) {
|
|
|
301
395
|
decision = "GO";
|
|
302
396
|
}
|
|
303
397
|
if (decision === "STOP") {
|
|
304
|
-
|
|
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
|
+
}
|
|
305
402
|
}
|
|
306
403
|
},
|
|
307
404
|
// ACT(2) inject coaching into system prompt (double defense). Silent on error.
|
|
@@ -358,9 +455,12 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
358
455
|
model: tool.schema.string().optional()
|
|
359
456
|
},
|
|
360
457
|
async execute(args, ctx) {
|
|
458
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
361
459
|
const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
|
|
362
460
|
h.tasks = h.tasks.filter((x) => x.id !== args.id);
|
|
363
|
-
|
|
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() });
|
|
364
464
|
if (args.id > h.current) h.current = args.id;
|
|
365
465
|
writeHarness(ctx.sessionID, h);
|
|
366
466
|
return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
|
|
@@ -379,6 +479,136 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
379
479
|
return "Harness complete.";
|
|
380
480
|
}
|
|
381
481
|
}),
|
|
482
|
+
record_failure: tool({
|
|
483
|
+
description: "Stage 1 (RECORD) of the learning loop. Append a failure record to failures.ndjson for later root-cause analysis.",
|
|
484
|
+
args: {
|
|
485
|
+
task: tool.schema.string(),
|
|
486
|
+
prompt: tool.schema.string(),
|
|
487
|
+
gradeResult: tool.schema.string(),
|
|
488
|
+
model: tool.schema.string().optional(),
|
|
489
|
+
revisions: tool.schema.number().optional()
|
|
490
|
+
},
|
|
491
|
+
async execute(args, _ctx) {
|
|
492
|
+
const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
|
|
493
|
+
try {
|
|
494
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
495
|
+
appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
|
|
496
|
+
} catch (e) {
|
|
497
|
+
log(`record_failure err: ${String(e)}`);
|
|
498
|
+
}
|
|
499
|
+
return `Failure recorded. [usage-coach NEXT] call investigate({failure: ${JSON.stringify(rec)}}) to find the root cause.`;
|
|
500
|
+
}
|
|
501
|
+
}),
|
|
502
|
+
investigate: tool({
|
|
503
|
+
description: "Stage 2 (INVESTIGATE) of the learning loop. Run the generator to analyze the ROOT CAUSE of a failure (not just the symptom).",
|
|
504
|
+
args: {
|
|
505
|
+
task: tool.schema.string(),
|
|
506
|
+
prompt: tool.schema.string(),
|
|
507
|
+
gradeResult: tool.schema.string(),
|
|
508
|
+
model: tool.schema.string().optional()
|
|
509
|
+
},
|
|
510
|
+
async execute(args, ctx) {
|
|
511
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
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
|
+
}
|
|
532
|
+
const rcaPrompt = `A task failed. Analyze the ROOT CAUSE (not just the symptom).
|
|
533
|
+
Task: ${args.task}
|
|
534
|
+
What was expected (from grade): ${args.gradeResult}
|
|
535
|
+
Read relevant files in the directory if needed.
|
|
536
|
+
Output a structured root cause:
|
|
537
|
+
category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
|
|
538
|
+
explanation: <why it failed>
|
|
539
|
+
evidence: <file/line or specific quote>`;
|
|
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
|
+
}
|
|
548
|
+
return out + "\n[usage-coach NEXT] call verify_diagnosis with this diagnosis.";
|
|
549
|
+
}
|
|
550
|
+
}),
|
|
551
|
+
verify_diagnosis: tool({
|
|
552
|
+
description: "Stage 3 (VERIFY) of the learning loop. Run the grader to check whether a diagnosis is CORRECT and ACTIONABLE (leads to a useful rule). Returns PASS/FAIL + a [usage-coach NEXT] directive.",
|
|
553
|
+
args: {
|
|
554
|
+
diagnosis: tool.schema.string(),
|
|
555
|
+
task: tool.schema.string(),
|
|
556
|
+
gradeResult: tool.schema.string()
|
|
557
|
+
},
|
|
558
|
+
async execute(args, ctx) {
|
|
559
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
560
|
+
const model = cfg.grader ?? cfg.generator;
|
|
561
|
+
if (!model) return "FAIL\n(ERROR: no grader/generator model configured.)\n[usage-coach NEXT] configure grader in harness.config.json, then retry verify_diagnosis.";
|
|
562
|
+
const verifyPrompt = `Verify this root-cause analysis for a failure.
|
|
563
|
+
Task: ${args.task}
|
|
564
|
+
Grade feedback: ${args.gradeResult}
|
|
565
|
+
Diagnosis: ${args.diagnosis}
|
|
566
|
+
Is the diagnosis CORRECT and ACTIONABLE (leads to a useful rule)?
|
|
567
|
+
Output PASS (the diagnosis is right) or FAIL (re-investigate needed), then reason.`;
|
|
568
|
+
const out = await runModel(input.client, model, verifyPrompt, ctx.directory);
|
|
569
|
+
let verdict = "FAIL";
|
|
570
|
+
if (!out.startsWith("ERROR:")) {
|
|
571
|
+
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
572
|
+
if (/^pass\b/i.test(f)) verdict = "PASS";
|
|
573
|
+
else verdict = "FAIL";
|
|
574
|
+
}
|
|
575
|
+
const next = verdict === "PASS" ? `
|
|
576
|
+
[usage-coach NEXT] call generalize with this verified diagnosis.` : `
|
|
577
|
+
[usage-coach NEXT] FAIL \u2014 re-investigate the root cause.`;
|
|
578
|
+
return out + "\n" + next;
|
|
579
|
+
}
|
|
580
|
+
}),
|
|
581
|
+
generalize: tool({
|
|
582
|
+
description: "Stage 4 (GENERALIZE) of the learning loop. Run the generator to turn a verified root cause into a reusable rule and append it to rules.md, so the next generate call includes it. Returns the rule text and a [usage-coach NEXT] directive.",
|
|
583
|
+
args: {
|
|
584
|
+
diagnosis: tool.schema.string(),
|
|
585
|
+
task: tool.schema.string()
|
|
586
|
+
},
|
|
587
|
+
async execute(args, ctx) {
|
|
588
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
589
|
+
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
590
|
+
const genPrompt = `Turn this verified root cause into a GENERAL, REUSABLE rule for future tasks of this kind.
|
|
591
|
+
Diagnosis: ${args.diagnosis}
|
|
592
|
+
Failed task: ${args.task}
|
|
593
|
+
Output a single rule in the form: 'For <task-type> tasks, always <check/do X> because <reason>.'
|
|
594
|
+
Keep it concrete and actionable.`;
|
|
595
|
+
const out = await runModel(input.client, cfg.generator, genPrompt, ctx.directory);
|
|
596
|
+
const rule = out;
|
|
597
|
+
try {
|
|
598
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
599
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
600
|
+
appendFileSync2(rulesFile(), `## Rule (${date})
|
|
601
|
+
${rule}
|
|
602
|
+
Origin: ${args.task}
|
|
603
|
+
|
|
604
|
+
`);
|
|
605
|
+
} catch (e) {
|
|
606
|
+
log(`generalize err: ${String(e)}`);
|
|
607
|
+
}
|
|
608
|
+
return `${rule}
|
|
609
|
+
[usage-coach NEXT] rule saved to rules.md. The next generate call will include it. Call task_update for the original failed task -> failed, then proceed.`;
|
|
610
|
+
}
|
|
611
|
+
}),
|
|
382
612
|
// Per-role model execution (config-driven, quota-aware, same server, no deadlock).
|
|
383
613
|
// P1: quota decision drives model selection + concurrency.
|
|
384
614
|
generate: tool({
|
|
@@ -395,13 +625,38 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
395
625
|
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
396
626
|
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
397
627
|
const rules = readRules();
|
|
398
|
-
|
|
628
|
+
let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
|
|
399
629
|
${rules}
|
|
400
630
|
|
|
401
631
|
---
|
|
402
632
|
|
|
403
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
|
+
}
|
|
404
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
|
+
}
|
|
405
660
|
return out + (throttle ? `
|
|
406
661
|
[usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
|
|
407
662
|
[usage-coach NEXT] call task_update(i, title, "grading"), then grade to evaluate this work.`;
|
|
@@ -453,7 +708,12 @@ ${rules}
|
|
|
453
708
|
}
|
|
454
709
|
const next = verdict === "PASS" ? `
|
|
455
710
|
[usage-coach NEXT] PASS -> call task_update(i, title, "completed", "PASS"), then proceed to next task (or harness_done if last).` : `
|
|
456
|
-
[usage-coach NEXT] FAIL -> if revisions < 2: task_update(i, title, "revising", revisions+1) + generate({prompt: "Apply feedback:\\n{grade result}\\nTask: {title}"}); else:
|
|
711
|
+
[usage-coach NEXT] FAIL -> if revisions < 2: task_update(i, title, "revising", revisions+1) + generate({prompt: "Apply feedback:\\n{grade result}\\nTask: {title}"}); else: run the learning loop before failing \u2014
|
|
712
|
+
1. record_failure({task, prompt, gradeResult, model, revisions})
|
|
713
|
+
2. investigate({task, prompt, gradeResult}) -> diagnosis
|
|
714
|
+
3. verify_diagnosis({diagnosis, task, gradeResult}) -> if PASS: generalize({diagnosis, task}) (saves rule to rules.md)
|
|
715
|
+
4. task_update(i, title, "failed", "FAIL") -> next task.
|
|
716
|
+
The next generate call will automatically include the new rule.`;
|
|
457
717
|
return out + "\n" + next;
|
|
458
718
|
}
|
|
459
719
|
})
|
package/dist/tui.js
CHANGED
|
@@ -5,7 +5,7 @@ import { effect as _$effect } from "@opentui/solid";
|
|
|
5
5
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
6
6
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
7
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
8
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync, appendFileSync } from "fs";
|
|
9
9
|
import { createHash } from "crypto";
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { join, resolve } from "path";
|
|
@@ -86,26 +86,50 @@ var TLABEL = {
|
|
|
86
86
|
halted_quota: "quota-halt"
|
|
87
87
|
};
|
|
88
88
|
function barFill(p) {
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
function short(s, n) {
|
|
95
|
-
return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
|
|
93
|
+
const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
|
|
94
|
+
return "\u2591".repeat(10 - n);
|
|
96
95
|
}
|
|
97
96
|
function initializeTui(api, disposeRoot) {
|
|
98
97
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
|
|
99
98
|
STATE_FILE = join(STATE_DIR, "state.json");
|
|
100
99
|
HARNESS_FILE = join(STATE_DIR, "harness.json");
|
|
101
100
|
MARKER = join(STATE_DIR, "tui-loaded.txt");
|
|
101
|
+
const TUI_LOG = join(STATE_DIR, "tui-debug.log");
|
|
102
|
+
const tlog = (msg) => {
|
|
103
|
+
try {
|
|
104
|
+
appendFileSync(MARKER, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
105
|
+
`);
|
|
106
|
+
appendFileSync(TUI_LOG, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
107
|
+
`);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
try {
|
|
110
|
+
appendFileSync(MARKER, `TLOG ERR: ${String(e)}
|
|
111
|
+
`);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
102
116
|
try {
|
|
103
117
|
mkdirSync(STATE_DIR, {
|
|
104
118
|
recursive: true
|
|
105
119
|
});
|
|
106
|
-
writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
|
|
120
|
+
writeFileSync(MARKER, `loaded-v2 ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
|
|
107
121
|
} catch {
|
|
108
122
|
}
|
|
123
|
+
tlog(`init start | dir=${api.state.path.directory} | STATE_DIR=${STATE_DIR}`);
|
|
124
|
+
try {
|
|
125
|
+
tlog(`api keys=${Object.keys(api).join(",")}`);
|
|
126
|
+
tlog(`api.state=${JSON.stringify(api.state).slice(0, 400)}`);
|
|
127
|
+
tlog(`api.state.path=${JSON.stringify(api.state?.path).slice(0, 300)}`);
|
|
128
|
+
const r = api.route;
|
|
129
|
+
tlog(`api.route type=${typeof r} keys=${r && typeof r === "object" ? Object.keys(r).join(",") : "?"} val=${JSON.stringify(r).slice(0, 400)}`);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
tlog(`api probe err: ${String(e)}`);
|
|
132
|
+
}
|
|
109
133
|
const [getState, setState] = createSignal(readState());
|
|
110
134
|
const [getHarness, setHarness] = createSignal(readHarness());
|
|
111
135
|
const timer = setInterval(() => {
|
|
@@ -168,7 +192,8 @@ function initializeTui(api, disposeRoot) {
|
|
|
168
192
|
}
|
|
169
193
|
let h = null;
|
|
170
194
|
try {
|
|
171
|
-
const
|
|
195
|
+
const routeSid = api.route?.current?.params?.sessionID ?? "";
|
|
196
|
+
const sid = routeSid || (ctx.session_id ?? "");
|
|
172
197
|
if (sid) {
|
|
173
198
|
const hf = join(STATE_DIR, sid, "harness.json");
|
|
174
199
|
if (existsSync(hf)) h = JSON.parse(readFileSync(hf, "utf8"));
|
|
@@ -213,7 +238,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
213
238
|
_$insert(_el$12, () => p.fiveHour, _el$14);
|
|
214
239
|
_$insert(_el$12, () => p.fiveHourReset, null);
|
|
215
240
|
_$effect((_p$) => {
|
|
216
|
-
var _v$ = st("text"), _v$2 = st("
|
|
241
|
+
var _v$ = st("text"), _v$2 = st("text");
|
|
217
242
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$10, "style", _v$, _p$.e));
|
|
218
243
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$2, _p$.t));
|
|
219
244
|
return _p$;
|
|
@@ -230,7 +255,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
230
255
|
_$insertNode(_el$15, _el$19);
|
|
231
256
|
_$insertNode(_el$15, _el$20);
|
|
232
257
|
_$setProp(_el$15, "flexDirection", "row");
|
|
233
|
-
_$insertNode(_el$16, _$createTextNode(`
|
|
258
|
+
_$insertNode(_el$16, _$createTextNode(` 1w `));
|
|
234
259
|
_$insert(_el$18, () => barFill(p.weekly));
|
|
235
260
|
_$insert(_el$19, () => barEmpty(p.weekly));
|
|
236
261
|
_$insertNode(_el$20, _el$21);
|
|
@@ -238,7 +263,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
238
263
|
_$insert(_el$20, () => p.weekly, _el$22);
|
|
239
264
|
_$insert(_el$20, () => p.weeklyReset, null);
|
|
240
265
|
_$effect((_p$) => {
|
|
241
|
-
var _v$3 = st("text"), _v$4 = st("
|
|
266
|
+
var _v$3 = st("text"), _v$4 = st("text");
|
|
242
267
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "style", _v$3, _p$.e));
|
|
243
268
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$19, "style", _v$4, _p$.t));
|
|
244
269
|
return _p$;
|
|
@@ -258,7 +283,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
258
283
|
}
|
|
259
284
|
} else {
|
|
260
285
|
nodes.push((() => {
|
|
261
|
-
var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text")
|
|
286
|
+
var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text");
|
|
262
287
|
_$insertNode(_el$27, _el$28);
|
|
263
288
|
_$insertNode(_el$27, _el$30);
|
|
264
289
|
_$insertNode(_el$27, _el$31);
|
|
@@ -267,11 +292,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
267
292
|
_$insertNode(_el$28, _$createTextNode(` 5h `));
|
|
268
293
|
_$insert(_el$30, () => barFill(s.fiveHour));
|
|
269
294
|
_$insert(_el$31, () => barEmpty(s.fiveHour));
|
|
270
|
-
_$insertNode(_el$32,
|
|
271
|
-
_$insertNode(_el$32, _el$34);
|
|
272
|
-
_$insert(_el$32, () => s.fiveHour, _el$34);
|
|
295
|
+
_$insertNode(_el$32, _$createTextNode(` 0%`));
|
|
273
296
|
_$effect((_p$) => {
|
|
274
|
-
var _v$5 = st("text"), _v$6 = st("
|
|
297
|
+
var _v$5 = st("text"), _v$6 = st("text");
|
|
275
298
|
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$30, "style", _v$5, _p$.e));
|
|
276
299
|
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$31, "style", _v$6, _p$.t));
|
|
277
300
|
return _p$;
|
|
@@ -282,77 +305,51 @@ function initializeTui(api, disposeRoot) {
|
|
|
282
305
|
return _el$27;
|
|
283
306
|
})());
|
|
284
307
|
nodes.push((() => {
|
|
285
|
-
var _el$
|
|
286
|
-
_$insertNode(_el$
|
|
287
|
-
_$insertNode(_el$
|
|
288
|
-
_$insertNode(_el$
|
|
289
|
-
_$insertNode(_el$
|
|
290
|
-
_$setProp(_el$
|
|
291
|
-
_$insertNode(_el$
|
|
292
|
-
_$insert(_el$
|
|
293
|
-
_$insert(_el$
|
|
294
|
-
_$insertNode(_el$
|
|
295
|
-
_$insertNode(_el$40, _el$42);
|
|
296
|
-
_$insert(_el$40, () => s.weekly, _el$42);
|
|
297
|
-
_$effect((_p$) => {
|
|
298
|
-
var _v$7 = st("text"), _v$8 = st("textMuted");
|
|
299
|
-
_v$7 !== _p$.e && (_p$.e = _$setProp(_el$38, "style", _v$7, _p$.e));
|
|
300
|
-
_v$8 !== _p$.t && (_p$.t = _$setProp(_el$39, "style", _v$8, _p$.t));
|
|
301
|
-
return _p$;
|
|
302
|
-
}, {
|
|
303
|
-
e: void 0,
|
|
304
|
-
t: void 0
|
|
305
|
-
});
|
|
306
|
-
return _el$35;
|
|
307
|
-
})());
|
|
308
|
-
nodes.push((() => {
|
|
309
|
-
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(`%`);
|
|
310
|
-
_$insertNode(_el$43, _el$44);
|
|
311
|
-
_$insertNode(_el$43, _el$46);
|
|
312
|
-
_$insertNode(_el$43, _el$47);
|
|
313
|
-
_$insertNode(_el$43, _el$48);
|
|
314
|
-
_$setProp(_el$43, "flexDirection", "row");
|
|
315
|
-
_$insertNode(_el$44, _$createTextNode(` mo `));
|
|
316
|
-
_$insert(_el$46, () => barFill(s.monthly));
|
|
317
|
-
_$insert(_el$47, () => barEmpty(s.monthly));
|
|
318
|
-
_$insertNode(_el$48, _el$49);
|
|
319
|
-
_$insertNode(_el$48, _el$50);
|
|
320
|
-
_$insert(_el$48, () => s.monthly, _el$50);
|
|
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%`));
|
|
321
318
|
_$effect((_p$) => {
|
|
322
|
-
var _v$
|
|
323
|
-
_v$
|
|
324
|
-
_v$
|
|
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));
|
|
325
322
|
return _p$;
|
|
326
323
|
}, {
|
|
327
324
|
e: void 0,
|
|
328
325
|
t: void 0
|
|
329
326
|
});
|
|
330
|
-
return _el$
|
|
327
|
+
return _el$34;
|
|
331
328
|
})());
|
|
332
329
|
}
|
|
333
330
|
} else {
|
|
334
331
|
nodes.push((() => {
|
|
335
|
-
var _el$
|
|
336
|
-
_$insertNode(_el$
|
|
337
|
-
return _el$
|
|
332
|
+
var _el$41 = _$createElement("text");
|
|
333
|
+
_$insertNode(_el$41, _$createTextNode(`usage-coach: ...`));
|
|
334
|
+
return _el$41;
|
|
338
335
|
})());
|
|
339
336
|
}
|
|
340
337
|
if (h && h.active !== false && h.tasks.length > 0) {
|
|
341
338
|
nodes.push((() => {
|
|
342
|
-
var _el$
|
|
343
|
-
_$insertNode(_el$
|
|
344
|
-
return _el$
|
|
339
|
+
var _el$43 = _$createElement("text");
|
|
340
|
+
_$insertNode(_el$43, _$createTextNode(` `));
|
|
341
|
+
return _el$43;
|
|
345
342
|
})());
|
|
346
343
|
nodes.push((() => {
|
|
347
|
-
var _el$
|
|
348
|
-
_$insertNode(_el$
|
|
349
|
-
_$insertNode(_el$
|
|
350
|
-
_$insertNode(_el$
|
|
351
|
-
_$insert(_el$
|
|
352
|
-
_$insert(_el$
|
|
353
|
-
_$insert(_el$
|
|
354
|
-
_$effect((_$p) => _$setProp(_el$
|
|
355
|
-
return _el$
|
|
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;
|
|
356
353
|
})());
|
|
357
354
|
for (const t of h.tasks) {
|
|
358
355
|
const sKey = statusKey[t.status] ?? "text";
|
|
@@ -362,71 +359,76 @@ function initializeTui(api, disposeRoot) {
|
|
|
362
359
|
const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
|
|
363
360
|
const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
|
|
364
361
|
nodes.push((() => {
|
|
365
|
-
var _el$
|
|
366
|
-
_$insertNode(_el$
|
|
367
|
-
_$insertNode(_el$
|
|
368
|
-
_$insertNode(_el$
|
|
369
|
-
_$insert(_el$
|
|
370
|
-
_$insert(_el$
|
|
371
|
-
_$insert(_el$
|
|
372
|
-
_$insert(_el$
|
|
373
|
-
_$insert(_el$
|
|
374
|
-
_$insert(_el$
|
|
375
|
-
_$effect((_$p) => _$setProp(_el$
|
|
376
|
-
return _el$
|
|
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;
|
|
377
374
|
})());
|
|
378
375
|
const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
379
|
-
const provCoach = s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id));
|
|
380
|
-
const
|
|
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;
|
|
378
|
+
const pct = rawPct < 0 ? 0 : rawPct;
|
|
379
|
+
const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
|
|
381
380
|
nodes.push((() => {
|
|
382
|
-
var _el$
|
|
383
|
-
_$insertNode(_el$
|
|
384
|
-
_$insertNode(_el$
|
|
385
|
-
_$insertNode(_el$
|
|
386
|
-
_$insertNode(_el$
|
|
387
|
-
_$setProp(_el$
|
|
388
|
-
_$insertNode(_el$
|
|
389
|
-
_$insert(_el$
|
|
390
|
-
_$insert(_el$
|
|
391
|
-
_$insertNode(_el$
|
|
392
|
-
_$
|
|
393
|
-
_$insert(_el$68, pct, _el$70);
|
|
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);
|
|
394
392
|
_$effect((_p$) => {
|
|
395
|
-
var _v$
|
|
396
|
-
_v$
|
|
397
|
-
_v$
|
|
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));
|
|
398
396
|
return _p$;
|
|
399
397
|
}, {
|
|
400
398
|
e: void 0,
|
|
401
399
|
t: void 0
|
|
402
400
|
});
|
|
403
|
-
return _el$
|
|
401
|
+
return _el$53;
|
|
404
402
|
})());
|
|
405
403
|
}
|
|
406
404
|
}
|
|
407
405
|
return (() => {
|
|
408
|
-
var _el$
|
|
409
|
-
_$setProp(_el$
|
|
410
|
-
_$insert(_el$
|
|
411
|
-
return _el$
|
|
406
|
+
var _el$60 = _$createElement("box");
|
|
407
|
+
_$setProp(_el$60, "flexDirection", "column");
|
|
408
|
+
_$insert(_el$60, nodes);
|
|
409
|
+
return _el$60;
|
|
412
410
|
})();
|
|
413
411
|
};
|
|
412
|
+
tlog("registering slots");
|
|
414
413
|
api.slots.register({
|
|
415
414
|
order: 80,
|
|
416
415
|
slots: {
|
|
417
416
|
sidebar_footer(ctx) {
|
|
417
|
+
tlog("sidebar_footer slot called");
|
|
418
418
|
try {
|
|
419
419
|
return panel(ctx);
|
|
420
|
-
} catch {
|
|
420
|
+
} catch (e) {
|
|
421
|
+
tlog(`sidebar_footer err: ${String(e)}`);
|
|
421
422
|
return (() => {
|
|
422
|
-
var _el$
|
|
423
|
-
_$insertNode(_el$
|
|
424
|
-
return _el$
|
|
423
|
+
var _el$61 = _$createElement("text");
|
|
424
|
+
_$insertNode(_el$61, _$createTextNode(`usage-coach`));
|
|
425
|
+
return _el$61;
|
|
425
426
|
})();
|
|
426
427
|
}
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
});
|
|
431
|
+
tlog("slots registered, init complete");
|
|
430
432
|
}
|
|
431
433
|
var tui = async (api) => {
|
|
432
434
|
createRoot((disposeRoot) => initializeTui(api, disposeRoot));
|
package/package.json
CHANGED