opencode-usage-coach 0.3.2 → 0.3.4
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 +8 -2
- package/dist/index.js +140 -37
- package/dist/tui.js +53 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ Recurring issues and fixes — mostly learned the hard way during development.
|
|
|
166
166
|
- **Export must be `{ tui }`** (an object) — a bare function is misread as a server plugin.
|
|
167
167
|
- **Color prop is `fg`** (not `foreground`): `style={{ fg: theme.current.success }}`.
|
|
168
168
|
- **Call `codexbar` via `spawn`** — the `$` BunShell leaks command output into the TUI.
|
|
169
|
-
- **Harness not visible?** The TUI
|
|
169
|
+
- **Harness not visible?** The TUI shows the **current session's** harness only. A finished harness (`active:false`) is hidden. A harness started in another session won't appear here — switch to that session to see it.
|
|
170
170
|
|
|
171
171
|
### LLM model selection (generate/grade)
|
|
172
172
|
- **`generator` is required in `harness.config.json`** — the tools return a clear error if missing (the old z.ai fallback was removed in v0.2.4).
|
|
@@ -187,13 +187,19 @@ Recurring issues and fixes — mostly learned the hard way during development.
|
|
|
187
187
|
- `(no output)` means the sub-session returned no text. Check `~/.cache/opencode-usage-coach/coach.log` for the runModel trace (requires `UC_DEBUG=1`).
|
|
188
188
|
- **Note**: runModel only terminates on explicit `idle`/`completed` status. An earlier bug broke on `!status` (undefined) immediately — fixed.
|
|
189
189
|
|
|
190
|
+
### generate / generate_batch returns "Tool execution aborted"
|
|
191
|
+
- **Cause:** opencode imposes a tool execution timeout (~60–120s, not configurable). `runModel` polls the sub-session for up to 10 min; if the generator model takes longer than the tool timeout, opencode aborts the call.
|
|
192
|
+
- **This is a platform limit, not a plugin bug** — there is no config key to extend it.
|
|
193
|
+
- **Mitigation:** split large tasks into smaller ones that finish within the timeout. The NEXT directives + revise loop help — a FAIL triggers a focused revise rather than a monolithic retry.
|
|
194
|
+
- The sub-session keeps running in the background after abort; its file writes are preserved. Only the tool's return value is lost.
|
|
195
|
+
|
|
190
196
|
### Multi-session (per-session state isolation)
|
|
191
197
|
- Each opencode session has a unique sessionID; harness state is isolated per-session:
|
|
192
198
|
```
|
|
193
199
|
~/.cache/opencode-usage-coach/projects/<dir-hash>/<sessionID>/harness.json
|
|
194
200
|
```
|
|
195
201
|
- Different working directories get separate project state (keyed by path hash).
|
|
196
|
-
- The TUI
|
|
202
|
+
- The TUI shows the **current session's** harness only (per-session isolation) — no cross-session leakage. A harness running in session B does not appear in session A's panel.
|
|
197
203
|
- Harness completion sets `active:false` → hidden from the TUI.
|
|
198
204
|
- Override the state path with `UC_STATE_DIR` (forces global state).
|
|
199
205
|
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,21 @@ function writeState(c) {
|
|
|
38
38
|
} catch {
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
function rulesFile() {
|
|
42
|
+
return join(STATE_DIR, "rules.md");
|
|
43
|
+
}
|
|
44
|
+
function failuresFile() {
|
|
45
|
+
return join(STATE_DIR, "failures.ndjson");
|
|
46
|
+
}
|
|
47
|
+
function readRules() {
|
|
48
|
+
try {
|
|
49
|
+
const f = rulesFile();
|
|
50
|
+
if (!existsSync(f)) return "";
|
|
51
|
+
return readFileSync(f, "utf8").trim();
|
|
52
|
+
} catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
41
56
|
function harnessFile(sessionID) {
|
|
42
57
|
return join(STATE_DIR, sessionID || "_default", "harness.json");
|
|
43
58
|
}
|
|
@@ -73,56 +88,28 @@ function readHarnessCfg(dir) {
|
|
|
73
88
|
};
|
|
74
89
|
}
|
|
75
90
|
async function runModel(client, model, prompt, directory) {
|
|
76
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
77
91
|
const t0 = Date.now();
|
|
78
92
|
try {
|
|
79
93
|
const slash = model.indexOf("/");
|
|
80
94
|
const providerID = slash >= 0 ? model.slice(0, slash) : model;
|
|
81
95
|
const modelID = slash >= 0 ? model.slice(slash + 1) : "";
|
|
82
96
|
const s = await client.session.create({ body: { title: "uc-harness-sub" }, query: { directory } });
|
|
83
|
-
log(`runModel(${model}): create resp = ${JSON.stringify(s?.data ?? s).slice(0, 500)}`);
|
|
84
97
|
const id = s?.data?.info?.id ?? s?.data?.id ?? s?.id;
|
|
85
98
|
if (!id) return `ERROR: session.create returned no id (response: ${JSON.stringify(s?.data ?? s).slice(0, 200)})`;
|
|
86
|
-
log(`runModel(${model}): session ${id} created, prompt ${prompt.length} chars`);
|
|
87
|
-
await client.session.prompt({
|
|
99
|
+
log(`runModel(${model}): session ${id} created, sending prompt (${prompt.length} chars)`);
|
|
100
|
+
const resp = await client.session.prompt({
|
|
88
101
|
path: { id },
|
|
89
102
|
body: { model: { providerID, modelID }, parts: [{ type: "text", text: prompt }] }
|
|
90
103
|
});
|
|
91
|
-
log(`runModel(${model}): prompt sent to ${id}`);
|
|
92
|
-
let lastMsgCount = -1;
|
|
93
|
-
let timedOut = false;
|
|
94
|
-
let text = "";
|
|
95
|
-
for (let i = 0; i < 600; i++) {
|
|
96
|
-
await sleep(1e3);
|
|
97
|
-
try {
|
|
98
|
-
const msgs = await client.session.messages({ path: { id } });
|
|
99
|
-
const all = msgs?.data ?? msgs ?? [];
|
|
100
|
-
if (all.length !== lastMsgCount) {
|
|
101
|
-
log(`runModel(${model}): poll[${i}] msgs=${all.length}`);
|
|
102
|
-
lastMsgCount = all.length;
|
|
103
|
-
}
|
|
104
|
-
const lastAssistant = all.filter((m) => (m?.info?.role ?? m?.role) === "assistant").pop();
|
|
105
|
-
const parts = lastAssistant?.parts ?? lastAssistant?.info?.parts ?? [];
|
|
106
|
-
const t = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("").trim();
|
|
107
|
-
if (t) {
|
|
108
|
-
text = t;
|
|
109
|
-
log(`runModel(${model}): got assistant text at poll[${i}] (${t.length} chars)`);
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
} catch (e) {
|
|
113
|
-
log(`runModel(${model}): poll[${i}] messages err: ${String(e).slice(0, 120)}`);
|
|
114
|
-
}
|
|
115
|
-
if (i === 599) timedOut = true;
|
|
116
|
-
}
|
|
117
104
|
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
105
|
+
const parts = resp?.data?.parts ?? resp?.parts ?? [];
|
|
106
|
+
const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
|
|
118
107
|
try {
|
|
119
108
|
await client.session.remove?.({ path: { id } });
|
|
120
109
|
} catch {
|
|
121
110
|
}
|
|
122
|
-
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars
|
|
123
|
-
|
|
124
|
-
${text}`;
|
|
125
|
-
return text || `ERROR: no assistant text after ${elapsed}s (${lastMsgCount} messages seen)`;
|
|
111
|
+
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
|
|
112
|
+
return text.trim() || `ERROR: no assistant text in prompt response after ${elapsed}s (parts: ${parts.length}, types: ${parts.map((p) => p?.type).join(",")})`;
|
|
126
113
|
} catch (e) {
|
|
127
114
|
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
128
115
|
log(`runModel err (${model}, ${elapsed}s): ${String(e)}`);
|
|
@@ -376,7 +363,7 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
376
363
|
async execute(args, ctx) {
|
|
377
364
|
const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
|
|
378
365
|
h.tasks = h.tasks.filter((x) => x.id !== args.id);
|
|
379
|
-
h.tasks.push({ id: args.id, title: args.title, status: args.status, model: args.model ?? "", revisions: args.revisions ?? 0, score: args.score ?? null });
|
|
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() });
|
|
380
367
|
if (args.id > h.current) h.current = args.id;
|
|
381
368
|
writeHarness(ctx.sessionID, h);
|
|
382
369
|
return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
|
|
@@ -395,6 +382,110 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
395
382
|
return "Harness complete.";
|
|
396
383
|
}
|
|
397
384
|
}),
|
|
385
|
+
record_failure: tool({
|
|
386
|
+
description: "Stage 1 (RECORD) of the learning loop. Append a failure record to failures.ndjson for later root-cause analysis.",
|
|
387
|
+
args: {
|
|
388
|
+
task: tool.schema.string(),
|
|
389
|
+
prompt: tool.schema.string(),
|
|
390
|
+
gradeResult: tool.schema.string(),
|
|
391
|
+
model: tool.schema.string().optional(),
|
|
392
|
+
revisions: tool.schema.number().optional()
|
|
393
|
+
},
|
|
394
|
+
async execute(args, _ctx) {
|
|
395
|
+
const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
|
|
396
|
+
try {
|
|
397
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
398
|
+
appendFileSync(failuresFile(), JSON.stringify(rec) + "\n");
|
|
399
|
+
} catch (e) {
|
|
400
|
+
log(`record_failure err: ${String(e)}`);
|
|
401
|
+
}
|
|
402
|
+
return `Failure recorded. [usage-coach NEXT] call investigate({failure: ${JSON.stringify(rec)}}) to find the root cause.`;
|
|
403
|
+
}
|
|
404
|
+
}),
|
|
405
|
+
investigate: tool({
|
|
406
|
+
description: "Stage 2 (INVESTIGATE) of the learning loop. Run the generator to analyze the ROOT CAUSE of a failure (not just the symptom).",
|
|
407
|
+
args: {
|
|
408
|
+
task: tool.schema.string(),
|
|
409
|
+
prompt: tool.schema.string(),
|
|
410
|
+
gradeResult: tool.schema.string(),
|
|
411
|
+
model: tool.schema.string().optional()
|
|
412
|
+
},
|
|
413
|
+
async execute(args, ctx) {
|
|
414
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
415
|
+
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
416
|
+
const rcaPrompt = `A task failed. Analyze the ROOT CAUSE (not just the symptom).
|
|
417
|
+
Task: ${args.task}
|
|
418
|
+
What was expected (from grade): ${args.gradeResult}
|
|
419
|
+
Read relevant files in the directory if needed.
|
|
420
|
+
Output a structured root cause:
|
|
421
|
+
category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
|
|
422
|
+
explanation: <why it failed>
|
|
423
|
+
evidence: <file/line or specific quote>`;
|
|
424
|
+
const out = await runModel(input.client, cfg.generator, rcaPrompt, ctx.directory);
|
|
425
|
+
return out + "\n[usage-coach NEXT] call verify_diagnosis with this diagnosis.";
|
|
426
|
+
}
|
|
427
|
+
}),
|
|
428
|
+
verify_diagnosis: tool({
|
|
429
|
+
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.",
|
|
430
|
+
args: {
|
|
431
|
+
diagnosis: tool.schema.string(),
|
|
432
|
+
task: tool.schema.string(),
|
|
433
|
+
gradeResult: tool.schema.string()
|
|
434
|
+
},
|
|
435
|
+
async execute(args, ctx) {
|
|
436
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
437
|
+
const model = cfg.grader ?? cfg.generator;
|
|
438
|
+
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.";
|
|
439
|
+
const verifyPrompt = `Verify this root-cause analysis for a failure.
|
|
440
|
+
Task: ${args.task}
|
|
441
|
+
Grade feedback: ${args.gradeResult}
|
|
442
|
+
Diagnosis: ${args.diagnosis}
|
|
443
|
+
Is the diagnosis CORRECT and ACTIONABLE (leads to a useful rule)?
|
|
444
|
+
Output PASS (the diagnosis is right) or FAIL (re-investigate needed), then reason.`;
|
|
445
|
+
const out = await runModel(input.client, model, verifyPrompt, ctx.directory);
|
|
446
|
+
let verdict = "FAIL";
|
|
447
|
+
if (!out.startsWith("ERROR:")) {
|
|
448
|
+
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
449
|
+
if (/^pass\b/i.test(f)) verdict = "PASS";
|
|
450
|
+
else verdict = "FAIL";
|
|
451
|
+
}
|
|
452
|
+
const next = verdict === "PASS" ? `
|
|
453
|
+
[usage-coach NEXT] call generalize with this verified diagnosis.` : `
|
|
454
|
+
[usage-coach NEXT] FAIL \u2014 re-investigate the root cause.`;
|
|
455
|
+
return out + "\n" + next;
|
|
456
|
+
}
|
|
457
|
+
}),
|
|
458
|
+
generalize: tool({
|
|
459
|
+
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.",
|
|
460
|
+
args: {
|
|
461
|
+
diagnosis: tool.schema.string(),
|
|
462
|
+
task: tool.schema.string()
|
|
463
|
+
},
|
|
464
|
+
async execute(args, ctx) {
|
|
465
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
466
|
+
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
467
|
+
const genPrompt = `Turn this verified root cause into a GENERAL, REUSABLE rule for future tasks of this kind.
|
|
468
|
+
Diagnosis: ${args.diagnosis}
|
|
469
|
+
Failed task: ${args.task}
|
|
470
|
+
Output a single rule in the form: 'For <task-type> tasks, always <check/do X> because <reason>.'
|
|
471
|
+
Keep it concrete and actionable.`;
|
|
472
|
+
const out = await runModel(input.client, cfg.generator, genPrompt, ctx.directory);
|
|
473
|
+
const rule = out;
|
|
474
|
+
try {
|
|
475
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
476
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
477
|
+
appendFileSync(rulesFile(), `## Rule (${date})
|
|
478
|
+
${rule}
|
|
479
|
+
Origin: ${args.task}
|
|
480
|
+
|
|
481
|
+
`);
|
|
482
|
+
} catch (e) {
|
|
483
|
+
log(`generalize err: ${String(e)}`);
|
|
484
|
+
}
|
|
485
|
+
return `${rule}
|
|
486
|
+
[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.`;
|
|
487
|
+
}
|
|
488
|
+
}),
|
|
398
489
|
// Per-role model execution (config-driven, quota-aware, same server, no deadlock).
|
|
399
490
|
// P1: quota decision drives model selection + concurrency.
|
|
400
491
|
generate: tool({
|
|
@@ -410,7 +501,14 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
410
501
|
}
|
|
411
502
|
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
412
503
|
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
413
|
-
const
|
|
504
|
+
const rules = readRules();
|
|
505
|
+
const prefix = rules ? `Lessons learned from previous failures (apply where relevant):
|
|
506
|
+
${rules}
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
` : "";
|
|
511
|
+
const out = await runModel(input.client, model, prefix + args.prompt, ctx.directory);
|
|
414
512
|
return out + (throttle ? `
|
|
415
513
|
[usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
|
|
416
514
|
[usage-coach NEXT] call task_update(i, title, "grading"), then grade to evaluate this work.`;
|
|
@@ -462,7 +560,12 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
462
560
|
}
|
|
463
561
|
const next = verdict === "PASS" ? `
|
|
464
562
|
[usage-coach NEXT] PASS -> call task_update(i, title, "completed", "PASS"), then proceed to next task (or harness_done if last).` : `
|
|
465
|
-
[usage-coach NEXT] FAIL -> if revisions < 2: task_update(i, title, "revising", revisions+1) + generate({prompt: "Apply feedback:\\n{grade result}\\nTask: {title}"}); else:
|
|
563
|
+
[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
|
|
564
|
+
1. record_failure({task, prompt, gradeResult, model, revisions})
|
|
565
|
+
2. investigate({task, prompt, gradeResult}) -> diagnosis
|
|
566
|
+
3. verify_diagnosis({diagnosis, task, gradeResult}) -> if PASS: generalize({diagnosis, task}) (saves rule to rules.md)
|
|
567
|
+
4. task_update(i, title, "failed", "FAIL") -> next task.
|
|
568
|
+
The next generate call will automatically include the new rule.`;
|
|
466
569
|
return out + "\n" + next;
|
|
467
570
|
}
|
|
468
571
|
})
|
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";
|
|
@@ -91,21 +91,43 @@ function barFill(p) {
|
|
|
91
91
|
function barEmpty(p) {
|
|
92
92
|
return "\u2591".repeat(10 - Math.max(0, Math.min(10, Math.round(p / 10))));
|
|
93
93
|
}
|
|
94
|
-
function short(s, n) {
|
|
95
|
-
return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
|
|
96
|
-
}
|
|
97
94
|
function initializeTui(api, disposeRoot) {
|
|
98
95
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
|
|
99
96
|
STATE_FILE = join(STATE_DIR, "state.json");
|
|
100
97
|
HARNESS_FILE = join(STATE_DIR, "harness.json");
|
|
101
98
|
MARKER = join(STATE_DIR, "tui-loaded.txt");
|
|
99
|
+
const TUI_LOG = join(STATE_DIR, "tui-debug.log");
|
|
100
|
+
const tlog = (msg) => {
|
|
101
|
+
try {
|
|
102
|
+
appendFileSync(MARKER, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
103
|
+
`);
|
|
104
|
+
appendFileSync(TUI_LOG, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
105
|
+
`);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
try {
|
|
108
|
+
appendFileSync(MARKER, `TLOG ERR: ${String(e)}
|
|
109
|
+
`);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
102
114
|
try {
|
|
103
115
|
mkdirSync(STATE_DIR, {
|
|
104
116
|
recursive: true
|
|
105
117
|
});
|
|
106
|
-
writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
|
|
118
|
+
writeFileSync(MARKER, `loaded-v2 ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
|
|
107
119
|
} catch {
|
|
108
120
|
}
|
|
121
|
+
tlog(`init start | dir=${api.state.path.directory} | STATE_DIR=${STATE_DIR}`);
|
|
122
|
+
try {
|
|
123
|
+
tlog(`api keys=${Object.keys(api).join(",")}`);
|
|
124
|
+
tlog(`api.state=${JSON.stringify(api.state).slice(0, 400)}`);
|
|
125
|
+
tlog(`api.state.path=${JSON.stringify(api.state?.path).slice(0, 300)}`);
|
|
126
|
+
const r = api.route;
|
|
127
|
+
tlog(`api.route type=${typeof r} keys=${r && typeof r === "object" ? Object.keys(r).join(",") : "?"} val=${JSON.stringify(r).slice(0, 400)}`);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
tlog(`api probe err: ${String(e)}`);
|
|
130
|
+
}
|
|
109
131
|
const [getState, setState] = createSignal(readState());
|
|
110
132
|
const [getHarness, setHarness] = createSignal(readHarness());
|
|
111
133
|
const timer = setInterval(() => {
|
|
@@ -168,12 +190,14 @@ function initializeTui(api, disposeRoot) {
|
|
|
168
190
|
}
|
|
169
191
|
let h = null;
|
|
170
192
|
try {
|
|
171
|
-
const
|
|
193
|
+
const routeSid = api.route?.current?.params?.sessionID ?? "";
|
|
194
|
+
const sid = routeSid || (ctx.session_id ?? "");
|
|
172
195
|
if (sid) {
|
|
173
196
|
const hf = join(STATE_DIR, sid, "harness.json");
|
|
174
197
|
if (existsSync(hf)) h = JSON.parse(readFileSync(hf, "utf8"));
|
|
198
|
+
} else {
|
|
199
|
+
h = readHarness();
|
|
175
200
|
}
|
|
176
|
-
if (!h || h.active === false) h = readHarness();
|
|
177
201
|
} catch {
|
|
178
202
|
h = null;
|
|
179
203
|
}
|
|
@@ -358,6 +382,8 @@ function initializeTui(api, disposeRoot) {
|
|
|
358
382
|
const lbl = TLABEL[t.status] ?? t.status;
|
|
359
383
|
const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
|
|
360
384
|
const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
|
|
385
|
+
const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
|
|
386
|
+
const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
|
|
361
387
|
nodes.push((() => {
|
|
362
388
|
var _el$59 = _$createElement("text"), _el$60 = _$createTextNode(` \u25CF `), _el$61 = _$createTextNode(` `), _el$62 = _$createTextNode(` `);
|
|
363
389
|
_$insertNode(_el$59, _el$60);
|
|
@@ -367,15 +393,18 @@ function initializeTui(api, disposeRoot) {
|
|
|
367
393
|
_$insert(_el$59, mdl, _el$61);
|
|
368
394
|
_$insert(_el$59, lbl, _el$62);
|
|
369
395
|
_$insert(_el$59, rev, _el$62);
|
|
370
|
-
_$insert(_el$59,
|
|
396
|
+
_$insert(_el$59, elapsedStr, _el$62);
|
|
397
|
+
_$insert(_el$59, () => t.title, null);
|
|
371
398
|
_$effect((_$p) => _$setProp(_el$59, "style", st(sKey), _$p));
|
|
372
399
|
return _el$59;
|
|
373
400
|
})());
|
|
374
401
|
const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
375
|
-
const
|
|
376
|
-
const
|
|
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;
|
|
404
|
+
const pct = rawPct < 0 ? 0 : rawPct;
|
|
405
|
+
const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
|
|
377
406
|
nodes.push((() => {
|
|
378
|
-
var _el$63 = _$createElement("box"), _el$64 = _$createElement("text"), _el$66 = _$createElement("text"), _el$67 = _$createElement("text"), _el$68 = _$createElement("text"), _el$69 = _$createTextNode(` `)
|
|
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(` `);
|
|
379
408
|
_$insertNode(_el$63, _el$64);
|
|
380
409
|
_$insertNode(_el$63, _el$66);
|
|
381
410
|
_$insertNode(_el$63, _el$67);
|
|
@@ -385,8 +414,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
385
414
|
_$insert(_el$66, () => barFill(pct));
|
|
386
415
|
_$insert(_el$67, () => barEmpty(pct));
|
|
387
416
|
_$insertNode(_el$68, _el$69);
|
|
388
|
-
_$
|
|
389
|
-
_$insert(_el$68, pct, _el$70);
|
|
417
|
+
_$insert(_el$68, pctLabel, null);
|
|
390
418
|
_$effect((_p$) => {
|
|
391
419
|
var _v$1 = st("text"), _v$10 = st("textMuted");
|
|
392
420
|
_v$1 !== _p$.e && (_p$.e = _$setProp(_el$66, "style", _v$1, _p$.e));
|
|
@@ -401,28 +429,32 @@ function initializeTui(api, disposeRoot) {
|
|
|
401
429
|
}
|
|
402
430
|
}
|
|
403
431
|
return (() => {
|
|
404
|
-
var _el$
|
|
405
|
-
_$setProp(_el$
|
|
406
|
-
_$insert(_el$
|
|
407
|
-
return _el$
|
|
432
|
+
var _el$70 = _$createElement("box");
|
|
433
|
+
_$setProp(_el$70, "flexDirection", "column");
|
|
434
|
+
_$insert(_el$70, nodes);
|
|
435
|
+
return _el$70;
|
|
408
436
|
})();
|
|
409
437
|
};
|
|
438
|
+
tlog("registering slots");
|
|
410
439
|
api.slots.register({
|
|
411
440
|
order: 80,
|
|
412
441
|
slots: {
|
|
413
442
|
sidebar_footer(ctx) {
|
|
443
|
+
tlog("sidebar_footer slot called");
|
|
414
444
|
try {
|
|
415
445
|
return panel(ctx);
|
|
416
|
-
} catch {
|
|
446
|
+
} catch (e) {
|
|
447
|
+
tlog(`sidebar_footer err: ${String(e)}`);
|
|
417
448
|
return (() => {
|
|
418
|
-
var _el$
|
|
419
|
-
_$insertNode(_el$
|
|
420
|
-
return _el$
|
|
449
|
+
var _el$71 = _$createElement("text");
|
|
450
|
+
_$insertNode(_el$71, _$createTextNode(`usage-coach`));
|
|
451
|
+
return _el$71;
|
|
421
452
|
})();
|
|
422
453
|
}
|
|
423
454
|
}
|
|
424
455
|
}
|
|
425
456
|
});
|
|
457
|
+
tlog("slots registered, init complete");
|
|
426
458
|
}
|
|
427
459
|
var tui = async (api) => {
|
|
428
460
|
createRoot((disposeRoot) => initializeTui(api, disposeRoot));
|
package/package.json
CHANGED