opencode-usage-coach 0.8.5 → 0.9.1
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 +1 -1
- package/dist/index.js +162 -30
- package/dist/tui.js +174 -126
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A closed-loop usage coach and harness for [OpenCode](https://opencode.ai). Built for flat-rate / quota-metered coding plans — it **senses quota → coaches → stops/advances the loop**. Provider-agnostic, configurable via `harness.config.json`.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/opencode-usage-coach) [](./LICENSE) [](https://www.npmjs.com/package/opencode-usage-coach) [](./LICENSE) [](./coverage-badge.json) [](https://ko-fi.com/lhjnano) [](https://github.com/sponsors/lhjnano)
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
package/dist/index.js
CHANGED
|
@@ -184,6 +184,7 @@ var PLUGIN_NAME = "opencode-usage-coach";
|
|
|
184
184
|
var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
|
|
185
185
|
var DEFAULT_MAX_STEPS = Number(process.env.UC_MAX_STEPS ?? 30) || 30;
|
|
186
186
|
var WATCHDOG_POLL_MS = Math.max(1e3, Number(process.env.UC_WATCHDOG_POLL_MS ?? 3e3) || 3e3);
|
|
187
|
+
var WALL_TIMEOUT_MS = Math.max(1, Number(process.env.UC_WALL_TIMEOUT_MIN ?? 30) || 30) * 60 * 1e3;
|
|
187
188
|
var DEFAULT_MAX_QUESTIONS = Math.max(1, Math.round(Number(process.env.UC_MAX_QUESTIONS ?? 7)) || 7);
|
|
188
189
|
var PIPE_LOG = join2(homedir(), ".cache", "opencode-usage-coach", "pipeline.log");
|
|
189
190
|
function pipeLog(msg) {
|
|
@@ -827,7 +828,7 @@ Questions for the user (${r.questions.length}):`);
|
|
|
827
828
|
if (r.rawAnalysis) L.push(`
|
|
828
829
|
Raw analysis: ${r.rawAnalysis.slice(0, 200)}`);
|
|
829
830
|
L.push("\n[usage-coach NEXT] unknowns reviewed:");
|
|
830
|
-
L.push(" - If questions are flagged,
|
|
831
|
+
L.push(" - If questions are flagged, call question() to present them to the user.");
|
|
831
832
|
L.push(" - If task splits are suggested, adjust via task_update.");
|
|
832
833
|
L.push(" - Then proceed to generate/generate_batch.");
|
|
833
834
|
return L.join("\n");
|
|
@@ -870,11 +871,27 @@ function checkScanGate(sessionID) {
|
|
|
870
871
|
try {
|
|
871
872
|
const h = readHarness(sessionID);
|
|
872
873
|
if (!h || !h.scanRequired) return { warning: null, summary: null };
|
|
873
|
-
if (h.scanDone)
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
874
|
+
if (!h.scanDone) {
|
|
875
|
+
return {
|
|
876
|
+
warning: `\u26A0 DIAGNOSIS GATE: unknown_scan was NOT called before this generate. You are generating without pre-flight gap analysis. Blind spots (unknown unknowns) may cause wrong assumptions and waste steps. Call unknown_scan first, OR proceed consciously accepting the risk.`,
|
|
877
|
+
summary: null
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
if (h.unknownScan?.questions?.length && !h.questionsResolved) {
|
|
881
|
+
return {
|
|
882
|
+
warning: `\u26A0 UNRESOLVED QUESTIONS: unknown_scan found ${h.unknownScan.questions.length} question(s) for the user, but the question tool was not called. You MUST call question() to present these to the user before generating. The answers materially affect the approach. Do NOT proceed without asking.`,
|
|
883
|
+
summary: h.scanSummary ?? null
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
let summary = h.scanSummary ?? null;
|
|
887
|
+
if (h.questionsResolved && h.questionAnswers) {
|
|
888
|
+
const answers = Object.entries(h.questionAnswers).map(([id, ans]) => ` ${id}: ${ans}`).join("\n");
|
|
889
|
+
summary = summary ? `${summary}
|
|
890
|
+
User answers to scan questions:
|
|
891
|
+
${answers}` : `User answers to scan questions:
|
|
892
|
+
${answers}`;
|
|
893
|
+
}
|
|
894
|
+
return { warning: null, summary };
|
|
878
895
|
} catch {
|
|
879
896
|
return { warning: null, summary: null };
|
|
880
897
|
}
|
|
@@ -896,8 +913,10 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
896
913
|
const t0 = Date.now();
|
|
897
914
|
const subStart = Date.now();
|
|
898
915
|
let poller = null;
|
|
916
|
+
let wallTimer = null;
|
|
899
917
|
let subId = null;
|
|
900
918
|
let timedOut = false;
|
|
919
|
+
let pollerDone = false;
|
|
901
920
|
let signalTimeout;
|
|
902
921
|
const timeoutSignal = new Promise((resolve2) => {
|
|
903
922
|
signalTimeout = resolve2;
|
|
@@ -912,12 +931,13 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
912
931
|
subId = id;
|
|
913
932
|
log(`runModel(${model}): session ${id} created, sending prompt (${prompt.length} chars), max_steps=${maxSteps}`);
|
|
914
933
|
poller = setInterval(async () => {
|
|
915
|
-
if (timedOut) return;
|
|
934
|
+
if (timedOut || pollerDone) return;
|
|
916
935
|
try {
|
|
917
936
|
let step = 0;
|
|
918
937
|
let lastTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
919
938
|
try {
|
|
920
939
|
const msgs = await client.session.messages?.({ path: { id } });
|
|
940
|
+
if (pollerDone) return;
|
|
921
941
|
const msgList = Array.isArray(msgs?.data) ? msgs.data : Array.isArray(msgs) ? msgs : [];
|
|
922
942
|
if (msgList.length) {
|
|
923
943
|
step = msgList.filter((m) => {
|
|
@@ -940,7 +960,7 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
940
960
|
signalTimeout();
|
|
941
961
|
return;
|
|
942
962
|
}
|
|
943
|
-
if (track) {
|
|
963
|
+
if (track && !pollerDone) {
|
|
944
964
|
const elapsed2 = Math.round((Date.now() - subStart) / 1e3);
|
|
945
965
|
updateSubSession(track.sessionID, track.taskId, {
|
|
946
966
|
subSessionId: id,
|
|
@@ -953,6 +973,17 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
953
973
|
log(`runModel poller err: ${String(e)}`);
|
|
954
974
|
}
|
|
955
975
|
}, WATCHDOG_POLL_MS);
|
|
976
|
+
wallTimer = setTimeout(() => {
|
|
977
|
+
if (!timedOut) {
|
|
978
|
+
timedOut = true;
|
|
979
|
+
log(`runModel(${model}): WALL-CLOCK timeout after ${WALL_TIMEOUT_MS / 1e3}s, aborting session ${id}`);
|
|
980
|
+
try {
|
|
981
|
+
client.session.abort?.({ path: { id } });
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
signalTimeout();
|
|
985
|
+
}
|
|
986
|
+
}, WALL_TIMEOUT_MS);
|
|
956
987
|
const promptP = client.session.prompt({
|
|
957
988
|
path: { id },
|
|
958
989
|
body: { model: { providerID, modelID }, parts: [{ type: "text", text: prompt }] }
|
|
@@ -997,7 +1028,9 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
997
1028
|
log(`runModel err (${model}, ${elapsed}s): ${String(e)}`);
|
|
998
1029
|
return `ERROR: runModel exception after ${elapsed}s: ${String(e)}`;
|
|
999
1030
|
} finally {
|
|
1031
|
+
pollerDone = true;
|
|
1000
1032
|
if (poller) clearInterval(poller);
|
|
1033
|
+
if (wallTimer) clearTimeout(wallTimer);
|
|
1001
1034
|
if (track) {
|
|
1002
1035
|
try {
|
|
1003
1036
|
clearSubSession(track.sessionID, track.taskId);
|
|
@@ -1031,7 +1064,9 @@ var WORM_MAX_NODES = num("UC_WORM_MAX_NODES", 1e5);
|
|
|
1031
1064
|
function humanRemaining(iso) {
|
|
1032
1065
|
try {
|
|
1033
1066
|
if (!iso) return "";
|
|
1034
|
-
const
|
|
1067
|
+
const ms = new Date(iso).getTime();
|
|
1068
|
+
if (Number.isNaN(ms)) return "";
|
|
1069
|
+
const mins = Math.floor((ms - Date.now()) / 6e4);
|
|
1035
1070
|
if (mins < 0) return "resets soon";
|
|
1036
1071
|
if (mins < 60) return `resets in ${mins}m`;
|
|
1037
1072
|
if (mins < 1440) return `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
@@ -1098,6 +1133,21 @@ async function fetchProvidersCoach() {
|
|
|
1098
1133
|
}));
|
|
1099
1134
|
return results.filter(Boolean);
|
|
1100
1135
|
}
|
|
1136
|
+
function parseQuotaResponse(rawText) {
|
|
1137
|
+
try {
|
|
1138
|
+
const text = (rawText || "").trim();
|
|
1139
|
+
if (!text || text === "[]") return null;
|
|
1140
|
+
const u = JSON.parse(text)[0]?.usage;
|
|
1141
|
+
if (!u) return null;
|
|
1142
|
+
return {
|
|
1143
|
+
weekly: u.primary ?? { usedPercent: 0 },
|
|
1144
|
+
monthly: u.secondary ?? { usedPercent: 0 },
|
|
1145
|
+
fiveHour: u.tertiary ?? { usedPercent: 0 }
|
|
1146
|
+
};
|
|
1147
|
+
} catch {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1101
1151
|
function fetchQuota(provider) {
|
|
1102
1152
|
return new Promise((resolve2) => {
|
|
1103
1153
|
let out = "";
|
|
@@ -1115,21 +1165,24 @@ function fetchQuota(provider) {
|
|
|
1115
1165
|
});
|
|
1116
1166
|
p.on("error", () => resolve2(null));
|
|
1117
1167
|
p.on("close", () => {
|
|
1118
|
-
|
|
1119
|
-
const text = out.trim();
|
|
1120
|
-
if (!text || text === "[]") return resolve2(null);
|
|
1121
|
-
const u = JSON.parse(text)[0]?.usage;
|
|
1122
|
-
if (!u) return resolve2(null);
|
|
1123
|
-
resolve2({ weekly: u.primary ?? { usedPercent: 0 }, monthly: u.secondary ?? { usedPercent: 0 }, fiveHour: u.tertiary ?? { usedPercent: 0 } });
|
|
1124
|
-
} catch {
|
|
1125
|
-
resolve2(null);
|
|
1126
|
-
}
|
|
1168
|
+
resolve2(parseQuotaResponse(out));
|
|
1127
1169
|
});
|
|
1128
1170
|
});
|
|
1129
1171
|
}
|
|
1172
|
+
async function fetchQuotaWithRetry(provider, maxRetries = 3) {
|
|
1173
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1174
|
+
const q = await fetchQuota(provider);
|
|
1175
|
+
if (q) return q;
|
|
1176
|
+
if (attempt < maxRetries - 1) {
|
|
1177
|
+
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1130
1182
|
function coach(q, lighter) {
|
|
1131
|
-
if (!q) return { decision: "GO", advice: "quota unavailable \u2014 proceeding cautiously.", weekly: -
|
|
1183
|
+
if (!q) return { decision: "GO", advice: "quota unavailable \u2014 retrying. proceeding cautiously.", weekly: -2, monthly: -2, fiveHour: -2 };
|
|
1132
1184
|
const wk = Math.round(q.weekly?.usedPercent ?? 0), mo = Math.round(q.monthly?.usedPercent ?? 0), h5 = Math.round(q.fiveHour?.usedPercent ?? 0);
|
|
1185
|
+
if (Number.isNaN(wk) || Number.isNaN(mo) || Number.isNaN(h5)) return { decision: "THROTTLE", advice: "invalid quota data \u2014 proceeding with caution. switch to lighter model if available.", weekly: Number.isNaN(wk) ? 0 : wk, monthly: Number.isNaN(mo) ? 0 : mo, fiveHour: Number.isNaN(h5) ? 0 : h5 };
|
|
1133
1186
|
const wkR = humanRemaining(q.weekly?.resetsAt), h5R = humanRemaining(q.fiveHour?.resetsAt);
|
|
1134
1187
|
const stop = (r) => ({ decision: "STOP", advice: `STOP recommend \u2014 ${r}. window nearly exhausted. stop now or it will be force-blocked.`, weekly: wk, monthly: mo, fiveHour: h5 });
|
|
1135
1188
|
const thr = (r) => ({ decision: "THROTTLE", advice: `Throttle recommend \u2014 ${r}. switch to lighter model (${lighter}) or wait for window reset.`, weekly: wk, monthly: mo, fiveHour: h5 });
|
|
@@ -1153,7 +1206,8 @@ function isFreeModel(model, provider) {
|
|
|
1153
1206
|
}
|
|
1154
1207
|
function providerToCodexbar(provider) {
|
|
1155
1208
|
if (!provider) return "";
|
|
1156
|
-
|
|
1209
|
+
const first = provider.split("-")[0];
|
|
1210
|
+
return first || provider;
|
|
1157
1211
|
}
|
|
1158
1212
|
async function resolveAgent(client, sessionID) {
|
|
1159
1213
|
if (!sessionID) return "";
|
|
@@ -1198,6 +1252,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1198
1252
|
const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
|
|
1199
1253
|
const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
|
|
1200
1254
|
let last = null;
|
|
1255
|
+
let lastKnownQuota = null;
|
|
1201
1256
|
let lastFetchedAt = 0;
|
|
1202
1257
|
let refreshing = false;
|
|
1203
1258
|
const refreshBackground = () => {
|
|
@@ -1215,9 +1270,11 @@ async function UsageCoachPlugin(input) {
|
|
|
1215
1270
|
return;
|
|
1216
1271
|
}
|
|
1217
1272
|
const activeProvider = providerToCodexbar(currentProvider) || PROVIDER;
|
|
1218
|
-
|
|
1273
|
+
fetchQuotaWithRetry(activeProvider).then(async (q) => {
|
|
1219
1274
|
try {
|
|
1220
|
-
|
|
1275
|
+
if (q) lastKnownQuota = q;
|
|
1276
|
+
const effectiveQ = q ?? lastKnownQuota;
|
|
1277
|
+
last = coach(effectiveQ, LIGHTER);
|
|
1221
1278
|
lastFetchedAt = Date.now();
|
|
1222
1279
|
let providers = [];
|
|
1223
1280
|
try {
|
|
@@ -1234,7 +1291,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1234
1291
|
log(`refresh-in-then err: ${String(e)}`);
|
|
1235
1292
|
}
|
|
1236
1293
|
}).catch((e) => {
|
|
1237
|
-
log(`
|
|
1294
|
+
log(`fetchQuotaWithRetry err: ${String(e)}`);
|
|
1238
1295
|
}).finally(() => {
|
|
1239
1296
|
refreshing = false;
|
|
1240
1297
|
});
|
|
@@ -1272,7 +1329,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1272
1329
|
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
1273
1330
|
currentAgent = agent;
|
|
1274
1331
|
refreshBackground();
|
|
1275
|
-
const harnessTools = ["unknown_scan", "generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure", "reverse_interview"];
|
|
1332
|
+
const harnessTools = ["unknown_scan", "question", "generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure", "reverse_interview"];
|
|
1276
1333
|
if (!harnessTools.includes(_input.tool)) return;
|
|
1277
1334
|
if (!isHarnessAgent(agent)) {
|
|
1278
1335
|
throw new Error(`[${PLUGIN_NAME}] '${_input.tool}' is restricted to agent mode ${JSON.stringify(HARNESS_AGENTS)} (current: ${JSON.stringify(agent || "unknown")}). Switch to that agent mode to use it.`);
|
|
@@ -1318,9 +1375,11 @@ async function UsageCoachPlugin(input) {
|
|
|
1318
1375
|
\u26A0 DIAGNOSIS GATE \u2014 unknown_scan is REQUIRED before generate/generate_batch.
|
|
1319
1376
|
unknown_scan({ prompt: "<user request>", tasks: [{id:1, title:"..."}, ...] })
|
|
1320
1377
|
If you skip it, generate will inject a \u26A0 warning into the sub-session prompt.
|
|
1321
|
-
Review the report: if QUESTIONS are flagged \u2192
|
|
1322
|
-
|
|
1323
|
-
|
|
1378
|
+
Review the report: if QUESTIONS are flagged \u2192 call question() to present them
|
|
1379
|
+
to the user BEFORE generate (it is enforced \u2014 generate will block with a
|
|
1380
|
+
warning until answers are recorded). If TASK REFINEMENTS are suggested \u2192
|
|
1381
|
+
apply via task_update. Unknown unknowns found will be automatically injected
|
|
1382
|
+
into generate prompts as context.
|
|
1324
1383
|
|
|
1325
1384
|
STEP LIMIT (default ${DEFAULT_MAX_STEPS}): each generate call creates a sub-session that is automatically aborted if it exceeds ${DEFAULT_MAX_STEPS} assistant steps. Before starting the loop, review each task: can it be completed in a focused, single-pass effort? If a task seems too broad (multiple files, multiple features, open-ended research), SPLIT it now into 2-3 smaller subtasks. A timeout wastes quota \u2014 split upfront.
|
|
1326
1385
|
|
|
@@ -1462,6 +1521,42 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
1462
1521
|
return formatReport(result);
|
|
1463
1522
|
}
|
|
1464
1523
|
}),
|
|
1524
|
+
question: tool({
|
|
1525
|
+
description: "Present unknown_scan questions to the user. REQUIRED after unknown_scan finds questions \u2014 call BEFORE generate. First call (no answers) returns the questions formatted for presentation. Present them to the user, then call again with their answers.",
|
|
1526
|
+
args: {
|
|
1527
|
+
answers: tool.schema.record(tool.schema.string(), tool.schema.string()).optional().describe(`User's answers keyed by question ID (e.g. {"Q1": "yes", "Q2": "node"}). Omit on first call to get the questions.`)
|
|
1528
|
+
},
|
|
1529
|
+
async execute(args, ctx) {
|
|
1530
|
+
const h = readHarness(ctx.sessionID);
|
|
1531
|
+
if (!h || !h.unknownScan || !h.unknownScan.questions?.length) {
|
|
1532
|
+
return "No questions from unknown_scan. [usage-coach NEXT] proceed to generate.";
|
|
1533
|
+
}
|
|
1534
|
+
const questions = h.unknownScan.questions;
|
|
1535
|
+
if (!args.answers) {
|
|
1536
|
+
const lines2 = [`unknown_scan found ${questions.length} question(s) that need user input before proceeding:
|
|
1537
|
+
`];
|
|
1538
|
+
questions.forEach((q) => {
|
|
1539
|
+
lines2.push(`[${q.id}] ${q.question}`);
|
|
1540
|
+
});
|
|
1541
|
+
lines2.push('\nPresent ALL of these questions to the user. Collect their answers, then call question({ answers: { "' + questions[0].id + '": "..." } }) with ALL answers.');
|
|
1542
|
+
lines2.push("[usage-coach NEXT] Present the questions above to the user verbatim. After they respond, call question({answers:{...}}) to record answers, then proceed to generate.");
|
|
1543
|
+
return lines2.join("\n");
|
|
1544
|
+
}
|
|
1545
|
+
h.questionsResolved = true;
|
|
1546
|
+
h.questionAnswers = args.answers;
|
|
1547
|
+
writeHarness(ctx.sessionID, h);
|
|
1548
|
+
const answered = Object.keys(args.answers).length;
|
|
1549
|
+
const lines = [`Questions resolved (${answered}/${questions.length} answered).
|
|
1550
|
+
`];
|
|
1551
|
+
questions.forEach((q) => {
|
|
1552
|
+
const ans = args.answers[q.id];
|
|
1553
|
+
if (ans) lines.push(` ${q.id}: ${q.question}
|
|
1554
|
+
\u2192 ${ans}`);
|
|
1555
|
+
});
|
|
1556
|
+
lines.push("\n[usage-coach NEXT] Answers recorded. Proceed to generate \u2014 they will be injected into the sub-session prompt.");
|
|
1557
|
+
return lines.join("\n");
|
|
1558
|
+
}
|
|
1559
|
+
}),
|
|
1465
1560
|
task_update: tool({
|
|
1466
1561
|
description: "Update a harness task's status on the panel. Call whenever a task transitions to generating/grading/revising/completed/failed.",
|
|
1467
1562
|
args: {
|
|
@@ -1655,7 +1750,14 @@ Origin: ${args.task}
|
|
|
1655
1750
|
args: { prompt: tool.schema.string(), max_steps: tool.schema.number().optional().describe("Maximum sub-session steps before timeout (default 30). Increase for complex tasks, decrease to fail fast on scope creep.") },
|
|
1656
1751
|
async execute(args, ctx) {
|
|
1657
1752
|
const cfg = readHarnessCfg(ctx.directory);
|
|
1658
|
-
if (!cfg.generator)
|
|
1753
|
+
if (!cfg.generator) {
|
|
1754
|
+
const h = readHarness(ctx.sessionID);
|
|
1755
|
+
if (h) {
|
|
1756
|
+
h.active = false;
|
|
1757
|
+
writeHarness(ctx.sessionID, h);
|
|
1758
|
+
}
|
|
1759
|
+
return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json). HARNESS TERMINATED \u2014 configure a generator model, then restart the harness.';
|
|
1760
|
+
}
|
|
1659
1761
|
let decision = "GO";
|
|
1660
1762
|
try {
|
|
1661
1763
|
decision = current().decision;
|
|
@@ -1761,7 +1863,14 @@ ${gate.summary}
|
|
|
1761
1863
|
args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })), max_steps: tool.schema.number().optional().describe("Maximum sub-session steps per task before timeout (default 30).") },
|
|
1762
1864
|
async execute(args, ctx) {
|
|
1763
1865
|
const cfg = readHarnessCfg(ctx.directory);
|
|
1764
|
-
if (!cfg.generator)
|
|
1866
|
+
if (!cfg.generator) {
|
|
1867
|
+
const h = readHarness(ctx.sessionID);
|
|
1868
|
+
if (h) {
|
|
1869
|
+
h.active = false;
|
|
1870
|
+
writeHarness(ctx.sessionID, h);
|
|
1871
|
+
}
|
|
1872
|
+
return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json). HARNESS TERMINATED \u2014 configure a generator model, then restart the harness.';
|
|
1873
|
+
}
|
|
1765
1874
|
let decision = "GO";
|
|
1766
1875
|
try {
|
|
1767
1876
|
decision = current().decision;
|
|
@@ -2039,5 +2148,28 @@ If the task is already well-specified with no significant ambiguities, return {"
|
|
|
2039
2148
|
}
|
|
2040
2149
|
}
|
|
2041
2150
|
export {
|
|
2042
|
-
|
|
2151
|
+
buildGapPrompt,
|
|
2152
|
+
buildScanSummary,
|
|
2153
|
+
checkScanGate,
|
|
2154
|
+
clearSubSession,
|
|
2155
|
+
coach,
|
|
2156
|
+
UsageCoachPlugin as default,
|
|
2157
|
+
detectLanguage,
|
|
2158
|
+
extractImplNotes,
|
|
2159
|
+
extractKeywords,
|
|
2160
|
+
findActiveTaskId,
|
|
2161
|
+
formatReport,
|
|
2162
|
+
humanRemaining,
|
|
2163
|
+
isFreeModel,
|
|
2164
|
+
isHarnessAgent,
|
|
2165
|
+
parseFileList,
|
|
2166
|
+
parseGapAnalysis,
|
|
2167
|
+
parseQuotaResponse,
|
|
2168
|
+
providerAdvice,
|
|
2169
|
+
providerToCodexbar,
|
|
2170
|
+
readHarness,
|
|
2171
|
+
readRules,
|
|
2172
|
+
setStateDir,
|
|
2173
|
+
updateSubSession,
|
|
2174
|
+
writeHarness
|
|
2043
2175
|
};
|
package/dist/tui.js
CHANGED
|
@@ -10,6 +10,101 @@ import { createHash } from "crypto";
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { join, resolve } from "path";
|
|
12
12
|
import { createRoot, createSignal, onCleanup } from "solid-js";
|
|
13
|
+
|
|
14
|
+
// src/tui-logic.ts
|
|
15
|
+
var STALE_MS = 5 * 6e4;
|
|
16
|
+
var HIDE_MS = 30 * 6e4;
|
|
17
|
+
var TAG = {
|
|
18
|
+
GO: "ok",
|
|
19
|
+
THROTTLE: "slow",
|
|
20
|
+
STOP: "STOP"
|
|
21
|
+
};
|
|
22
|
+
var TLABEL = {
|
|
23
|
+
generating: "gen",
|
|
24
|
+
grading: "grade",
|
|
25
|
+
revising: "revise",
|
|
26
|
+
completed: "done",
|
|
27
|
+
failed: "fail",
|
|
28
|
+
timed_out: "timeout",
|
|
29
|
+
halted_quota: "quota-halt",
|
|
30
|
+
stale: "STALE"
|
|
31
|
+
};
|
|
32
|
+
var STATUS_KEY = {
|
|
33
|
+
generating: "info",
|
|
34
|
+
grading: "accent",
|
|
35
|
+
revising: "warning",
|
|
36
|
+
completed: "success",
|
|
37
|
+
failed: "error",
|
|
38
|
+
timed_out: "error",
|
|
39
|
+
halted_quota: "error"
|
|
40
|
+
};
|
|
41
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
42
|
+
"completed",
|
|
43
|
+
"failed",
|
|
44
|
+
"timed_out",
|
|
45
|
+
"halted_quota"
|
|
46
|
+
]);
|
|
47
|
+
function barFill(p) {
|
|
48
|
+
const n = !Number.isFinite(p) || p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
|
|
49
|
+
return "\u2588".repeat(n);
|
|
50
|
+
}
|
|
51
|
+
function barEmpty(p) {
|
|
52
|
+
const n = !Number.isFinite(p) || p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
|
|
53
|
+
return "\u2591".repeat(10 - n);
|
|
54
|
+
}
|
|
55
|
+
function computeStaleness(h, now = Date.now()) {
|
|
56
|
+
const hAge = h.updatedAt ? now - new Date(h.updatedAt).getTime() : 0;
|
|
57
|
+
const hasActiveSub = h.tasks.some((t) => !!t.subSessionId);
|
|
58
|
+
const isStale = !hasActiveSub && hAge > STALE_MS;
|
|
59
|
+
const shouldHide = hAge > HIDE_MS && !hasActiveSub;
|
|
60
|
+
return { hAge, hasActiveSub, isStale, shouldHide };
|
|
61
|
+
}
|
|
62
|
+
function isHarnessVisible(h, staleness) {
|
|
63
|
+
if (!h) return false;
|
|
64
|
+
if (h.tasks.length === 0) return false;
|
|
65
|
+
if (h.active !== true) return false;
|
|
66
|
+
if (staleness.shouldHide) return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function computeTaskDisplay(t, isStale, now = Date.now()) {
|
|
70
|
+
const displayStatus = isStale && !TERMINAL_STATUSES.has(t.status) ? "stale" : t.status;
|
|
71
|
+
const sKey = STATUS_KEY[displayStatus] ?? "text";
|
|
72
|
+
const lbl = TLABEL[displayStatus] ?? displayStatus;
|
|
73
|
+
const rev = (t.revisions ?? 0) > 0 && t.status === "revising" ? `(${t.revisions})` : "";
|
|
74
|
+
const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
|
|
75
|
+
const hasSub = !!t.subSessionId;
|
|
76
|
+
const subStepStr = hasSub && t.subStep !== void 0 && t.subStep > 0 ? ` step:${t.subStep}` : "";
|
|
77
|
+
const subEl = hasSub && t.subElapsed !== void 0 ? ` ${t.subElapsed}s` : "";
|
|
78
|
+
const subWarn = hasSub && (t.subElapsed ?? 0) > 300;
|
|
79
|
+
const elapsed = t.startedAt ? Math.max(0, Math.round((now - new Date(t.startedAt).getTime()) / 1e3)) : 0;
|
|
80
|
+
const taskEl = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
|
|
81
|
+
const displayEl = hasSub ? subEl : taskEl;
|
|
82
|
+
const lineKey = subWarn ? "warning" : sKey;
|
|
83
|
+
return {
|
|
84
|
+
displayStatus,
|
|
85
|
+
themeKey: lineKey,
|
|
86
|
+
label: lbl,
|
|
87
|
+
revSuffix: rev,
|
|
88
|
+
modelStr: mdl,
|
|
89
|
+
stepStr: subStepStr,
|
|
90
|
+
elapsedStr: displayEl,
|
|
91
|
+
hasSub,
|
|
92
|
+
subWarn
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function decisionThemeKey(decision) {
|
|
96
|
+
return decision === "GO" ? "success" : decision === "THROTTLE" ? "warning" : "error";
|
|
97
|
+
}
|
|
98
|
+
function taskQuotaPct(t, s) {
|
|
99
|
+
const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
100
|
+
const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
|
|
101
|
+
const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
|
|
102
|
+
const pct = rawPct < 0 ? 0 : rawPct;
|
|
103
|
+
const label = rawPct === -1 ? "\u2026" : rawPct < 0 ? "retry" : `${rawPct}%`;
|
|
104
|
+
return { pct, label };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/tui.tsx
|
|
13
108
|
function projectStateDir(dir) {
|
|
14
109
|
const abs = resolve(dir || ".");
|
|
15
110
|
const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
@@ -71,31 +166,6 @@ function readHarness() {
|
|
|
71
166
|
return null;
|
|
72
167
|
}
|
|
73
168
|
}
|
|
74
|
-
var TAG = {
|
|
75
|
-
GO: "ok",
|
|
76
|
-
THROTTLE: "slow",
|
|
77
|
-
STOP: "STOP"
|
|
78
|
-
};
|
|
79
|
-
var TLABEL = {
|
|
80
|
-
generating: "gen",
|
|
81
|
-
grading: "grade",
|
|
82
|
-
revising: "revise",
|
|
83
|
-
completed: "done",
|
|
84
|
-
failed: "fail",
|
|
85
|
-
timed_out: "timeout",
|
|
86
|
-
halted_quota: "quota-halt",
|
|
87
|
-
stale: "STALE"
|
|
88
|
-
};
|
|
89
|
-
var STALE_MS = 5 * 6e4;
|
|
90
|
-
var HIDE_MS = 30 * 6e4;
|
|
91
|
-
function barFill(p) {
|
|
92
|
-
const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
|
|
93
|
-
return "\u2588".repeat(n);
|
|
94
|
-
}
|
|
95
|
-
function barEmpty(p) {
|
|
96
|
-
const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
|
|
97
|
-
return "\u2591".repeat(10 - n);
|
|
98
|
-
}
|
|
99
169
|
function initializeTui(api, disposeRoot) {
|
|
100
170
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
|
|
101
171
|
STATE_FILE = join(STATE_DIR, "state.json");
|
|
@@ -162,15 +232,6 @@ function initializeTui(api, disposeRoot) {
|
|
|
162
232
|
} catch {
|
|
163
233
|
}
|
|
164
234
|
});
|
|
165
|
-
const statusKey = {
|
|
166
|
-
generating: "info",
|
|
167
|
-
grading: "accent",
|
|
168
|
-
revising: "warning",
|
|
169
|
-
completed: "success",
|
|
170
|
-
failed: "error",
|
|
171
|
-
timed_out: "error",
|
|
172
|
-
halted_quota: "error"
|
|
173
|
-
};
|
|
174
235
|
const panel = (ctx) => {
|
|
175
236
|
const th = ctx.theme?.current ?? {};
|
|
176
237
|
const st = (k) => ({
|
|
@@ -206,7 +267,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
206
267
|
}
|
|
207
268
|
const nodes = [];
|
|
208
269
|
if (s) {
|
|
209
|
-
const dKey = s.decision
|
|
270
|
+
const dKey = decisionThemeKey(s.decision);
|
|
210
271
|
const modelShort = s.model ? s.model.split("/").pop() ?? s.model : "";
|
|
211
272
|
if (s.isFree) {
|
|
212
273
|
nodes.push((() => {
|
|
@@ -316,7 +377,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
316
377
|
}
|
|
317
378
|
} else {
|
|
318
379
|
nodes.push((() => {
|
|
319
|
-
var _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$35 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createElement("text");
|
|
380
|
+
var _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$35 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createTextNode(` `), _el$39 = _$createTextNode(`%`);
|
|
320
381
|
_$insertNode(_el$32, _el$33);
|
|
321
382
|
_$insertNode(_el$32, _el$35);
|
|
322
383
|
_$insertNode(_el$32, _el$36);
|
|
@@ -325,7 +386,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
325
386
|
_$insertNode(_el$33, _$createTextNode(` 5h `));
|
|
326
387
|
_$insert(_el$35, () => barFill(s.fiveHour));
|
|
327
388
|
_$insert(_el$36, () => barEmpty(s.fiveHour));
|
|
328
|
-
_$insertNode(_el$37,
|
|
389
|
+
_$insertNode(_el$37, _el$38);
|
|
390
|
+
_$insertNode(_el$37, _el$39);
|
|
391
|
+
_$insert(_el$37, () => s.fiveHour, _el$39);
|
|
329
392
|
_$effect((_p$) => {
|
|
330
393
|
var _v$9 = st("text"), _v$0 = st("text");
|
|
331
394
|
_v$9 !== _p$.e && (_p$.e = _$setProp(_el$35, "style", _v$9, _p$.e));
|
|
@@ -338,126 +401,111 @@ function initializeTui(api, disposeRoot) {
|
|
|
338
401
|
return _el$32;
|
|
339
402
|
})());
|
|
340
403
|
nodes.push((() => {
|
|
341
|
-
var _el$
|
|
342
|
-
_$insertNode(_el$
|
|
343
|
-
_$insertNode(_el$
|
|
344
|
-
_$insertNode(_el$
|
|
345
|
-
_$insertNode(_el$
|
|
346
|
-
_$setProp(_el$
|
|
347
|
-
_$insertNode(_el$
|
|
348
|
-
_$insert(_el$
|
|
349
|
-
_$insert(_el$
|
|
350
|
-
_$insertNode(_el$
|
|
404
|
+
var _el$40 = _$createElement("box"), _el$41 = _$createElement("text"), _el$43 = _$createElement("text"), _el$44 = _$createElement("text"), _el$45 = _$createElement("text"), _el$46 = _$createTextNode(` `), _el$47 = _$createTextNode(`%`);
|
|
405
|
+
_$insertNode(_el$40, _el$41);
|
|
406
|
+
_$insertNode(_el$40, _el$43);
|
|
407
|
+
_$insertNode(_el$40, _el$44);
|
|
408
|
+
_$insertNode(_el$40, _el$45);
|
|
409
|
+
_$setProp(_el$40, "flexDirection", "row");
|
|
410
|
+
_$insertNode(_el$41, _$createTextNode(` 1w `));
|
|
411
|
+
_$insert(_el$43, () => barFill(s.weekly));
|
|
412
|
+
_$insert(_el$44, () => barEmpty(s.weekly));
|
|
413
|
+
_$insertNode(_el$45, _el$46);
|
|
414
|
+
_$insertNode(_el$45, _el$47);
|
|
415
|
+
_$insert(_el$45, () => s.weekly, _el$47);
|
|
351
416
|
_$effect((_p$) => {
|
|
352
417
|
var _v$1 = st("text"), _v$10 = st("text");
|
|
353
|
-
_v$1 !== _p$.e && (_p$.e = _$setProp(_el$
|
|
354
|
-
_v$10 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
418
|
+
_v$1 !== _p$.e && (_p$.e = _$setProp(_el$43, "style", _v$1, _p$.e));
|
|
419
|
+
_v$10 !== _p$.t && (_p$.t = _$setProp(_el$44, "style", _v$10, _p$.t));
|
|
355
420
|
return _p$;
|
|
356
421
|
}, {
|
|
357
422
|
e: void 0,
|
|
358
423
|
t: void 0
|
|
359
424
|
});
|
|
360
|
-
return _el$
|
|
425
|
+
return _el$40;
|
|
361
426
|
})());
|
|
362
427
|
}
|
|
363
428
|
}
|
|
364
429
|
} else {
|
|
365
430
|
nodes.push((() => {
|
|
366
|
-
var _el$
|
|
367
|
-
_$insertNode(_el$
|
|
368
|
-
return _el$
|
|
431
|
+
var _el$48 = _$createElement("text");
|
|
432
|
+
_$insertNode(_el$48, _$createTextNode(`usage-coach: ...`));
|
|
433
|
+
return _el$48;
|
|
369
434
|
})());
|
|
370
435
|
}
|
|
371
|
-
if (h
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const shouldHide = hAge > HIDE_MS && !hasActiveSub;
|
|
376
|
-
if (shouldHide) {
|
|
377
|
-
} else {
|
|
436
|
+
if (h) {
|
|
437
|
+
const staleness = computeStaleness(h);
|
|
438
|
+
if (isHarnessVisible(h, staleness)) {
|
|
439
|
+
const isStale = staleness.isStale;
|
|
378
440
|
nodes.push((() => {
|
|
379
|
-
var _el$
|
|
380
|
-
_$insertNode(_el$
|
|
381
|
-
return _el$
|
|
441
|
+
var _el$50 = _$createElement("text");
|
|
442
|
+
_$insertNode(_el$50, _$createTextNode(` `));
|
|
443
|
+
return _el$50;
|
|
382
444
|
})());
|
|
383
445
|
nodes.push((() => {
|
|
384
|
-
var _el$
|
|
385
|
-
_$insertNode(_el$
|
|
386
|
-
_$insertNode(_el$
|
|
387
|
-
_$insertNode(_el$
|
|
388
|
-
_$insert(_el$
|
|
389
|
-
_$insert(_el$
|
|
390
|
-
_$insert(_el$
|
|
391
|
-
_$insert(_el$
|
|
392
|
-
_$effect((_$p) => _$setProp(_el$
|
|
393
|
-
return _el$
|
|
446
|
+
var _el$52 = _$createElement("text"), _el$53 = _$createTextNode(`harness: `), _el$54 = _$createTextNode(` `), _el$55 = _$createTextNode(`/`);
|
|
447
|
+
_$insertNode(_el$52, _el$53);
|
|
448
|
+
_$insertNode(_el$52, _el$54);
|
|
449
|
+
_$insertNode(_el$52, _el$55);
|
|
450
|
+
_$insert(_el$52, () => h.name, _el$54);
|
|
451
|
+
_$insert(_el$52, () => h.current, _el$55);
|
|
452
|
+
_$insert(_el$52, () => h.total, null);
|
|
453
|
+
_$insert(_el$52, isStale ? " (stale)" : "", null);
|
|
454
|
+
_$effect((_$p) => _$setProp(_el$52, "style", st("textMuted"), _$p));
|
|
455
|
+
return _el$52;
|
|
394
456
|
})());
|
|
395
457
|
for (const t of h.tasks) {
|
|
396
|
-
const
|
|
397
|
-
const displayStatus = isStale && !TERMINAL.has(t.status) ? "stale" : t.status;
|
|
398
|
-
const sKey = statusKey[displayStatus] ?? "text";
|
|
399
|
-
const lbl = TLABEL[displayStatus] ?? displayStatus;
|
|
400
|
-
const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
|
|
401
|
-
const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
|
|
402
|
-
const hasSub = !!t.subSessionId;
|
|
403
|
-
const subStepStr = hasSub && t.subStep !== void 0 && t.subStep > 0 ? ` step:${t.subStep}` : "";
|
|
404
|
-
const subEl = hasSub && t.subElapsed !== void 0 ? ` ${t.subElapsed}s` : "";
|
|
405
|
-
const subWarn = hasSub && (t.subElapsed ?? 0) > 300;
|
|
406
|
-
const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
|
|
407
|
-
const taskEl = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
|
|
408
|
-
const displayEl = hasSub ? subEl : taskEl;
|
|
409
|
-
const lineKey = subWarn ? "warning" : sKey;
|
|
458
|
+
const td = computeTaskDisplay(t, isStale);
|
|
410
459
|
nodes.push((() => {
|
|
411
|
-
var _el$
|
|
412
|
-
_$insertNode(_el$
|
|
413
|
-
_$insertNode(_el$
|
|
414
|
-
_$insertNode(_el$
|
|
415
|
-
_$insert(_el$
|
|
416
|
-
_$insert(_el$
|
|
417
|
-
_$insert(_el$
|
|
418
|
-
_$insert(_el$
|
|
419
|
-
_$insert(_el$
|
|
420
|
-
_$insert(_el$
|
|
421
|
-
_$insert(_el$
|
|
422
|
-
_$effect((_$p) => _$setProp(_el$
|
|
423
|
-
return _el$
|
|
460
|
+
var _el$56 = _$createElement("text"), _el$57 = _$createTextNode(` \u25CF `), _el$58 = _$createTextNode(` `), _el$59 = _$createTextNode(` `);
|
|
461
|
+
_$insertNode(_el$56, _el$57);
|
|
462
|
+
_$insertNode(_el$56, _el$58);
|
|
463
|
+
_$insertNode(_el$56, _el$59);
|
|
464
|
+
_$insert(_el$56, () => t.id, _el$58);
|
|
465
|
+
_$insert(_el$56, () => td.modelStr, _el$58);
|
|
466
|
+
_$insert(_el$56, () => td.label, _el$59);
|
|
467
|
+
_$insert(_el$56, () => td.revSuffix, _el$59);
|
|
468
|
+
_$insert(_el$56, () => td.stepStr, _el$59);
|
|
469
|
+
_$insert(_el$56, () => td.elapsedStr, _el$59);
|
|
470
|
+
_$insert(_el$56, () => t.title, null);
|
|
471
|
+
_$effect((_$p) => _$setProp(_el$56, "style", st(td.themeKey), _$p));
|
|
472
|
+
return _el$56;
|
|
424
473
|
})());
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
|
|
474
|
+
const {
|
|
475
|
+
pct,
|
|
476
|
+
label: pctLabel
|
|
477
|
+
} = taskQuotaPct(t, s);
|
|
430
478
|
nodes.push((() => {
|
|
431
|
-
var _el$
|
|
432
|
-
_$insertNode(_el$
|
|
433
|
-
_$insertNode(_el$
|
|
434
|
-
_$insertNode(_el$
|
|
435
|
-
_$insertNode(_el$
|
|
436
|
-
_$setProp(_el$
|
|
437
|
-
_$insertNode(_el$
|
|
438
|
-
_$insert(_el$
|
|
439
|
-
_$insert(_el$
|
|
440
|
-
_$insertNode(_el$
|
|
441
|
-
_$insert(_el$
|
|
479
|
+
var _el$60 = _$createElement("box"), _el$61 = _$createElement("text"), _el$63 = _$createElement("text"), _el$64 = _$createElement("text"), _el$65 = _$createElement("text"), _el$66 = _$createTextNode(` `);
|
|
480
|
+
_$insertNode(_el$60, _el$61);
|
|
481
|
+
_$insertNode(_el$60, _el$63);
|
|
482
|
+
_$insertNode(_el$60, _el$64);
|
|
483
|
+
_$insertNode(_el$60, _el$65);
|
|
484
|
+
_$setProp(_el$60, "flexDirection", "row");
|
|
485
|
+
_$insertNode(_el$61, _$createTextNode(` 5h `));
|
|
486
|
+
_$insert(_el$63, () => barFill(pct));
|
|
487
|
+
_$insert(_el$64, () => barEmpty(pct));
|
|
488
|
+
_$insertNode(_el$65, _el$66);
|
|
489
|
+
_$insert(_el$65, pctLabel, null);
|
|
442
490
|
_$effect((_p$) => {
|
|
443
491
|
var _v$11 = st("text"), _v$12 = st("text");
|
|
444
|
-
_v$11 !== _p$.e && (_p$.e = _$setProp(_el$
|
|
445
|
-
_v$12 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
492
|
+
_v$11 !== _p$.e && (_p$.e = _$setProp(_el$63, "style", _v$11, _p$.e));
|
|
493
|
+
_v$12 !== _p$.t && (_p$.t = _$setProp(_el$64, "style", _v$12, _p$.t));
|
|
446
494
|
return _p$;
|
|
447
495
|
}, {
|
|
448
496
|
e: void 0,
|
|
449
497
|
t: void 0
|
|
450
498
|
});
|
|
451
|
-
return _el$
|
|
499
|
+
return _el$60;
|
|
452
500
|
})());
|
|
453
501
|
}
|
|
454
502
|
}
|
|
455
503
|
}
|
|
456
504
|
return (() => {
|
|
457
|
-
var _el$
|
|
458
|
-
_$setProp(_el$
|
|
459
|
-
_$insert(_el$
|
|
460
|
-
return _el$
|
|
505
|
+
var _el$67 = _$createElement("box");
|
|
506
|
+
_$setProp(_el$67, "flexDirection", "column");
|
|
507
|
+
_$insert(_el$67, nodes);
|
|
508
|
+
return _el$67;
|
|
461
509
|
})();
|
|
462
510
|
};
|
|
463
511
|
tlog("registering slots");
|
|
@@ -472,9 +520,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
472
520
|
} catch (e) {
|
|
473
521
|
tlog(`sidebar_footer err: ${String(e)}`);
|
|
474
522
|
result = (() => {
|
|
475
|
-
var _el$
|
|
476
|
-
_$insertNode(_el$
|
|
477
|
-
return _el$
|
|
523
|
+
var _el$68 = _$createElement("text");
|
|
524
|
+
_$insertNode(_el$68, _$createTextNode(`usage-coach`));
|
|
525
|
+
return _el$68;
|
|
478
526
|
})();
|
|
479
527
|
}
|
|
480
528
|
return result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-coach",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "opencode closed-loop usage coach — quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
21
|
"lint": "eslint .",
|
|
22
22
|
"lint:fix": "eslint . --fix",
|
|
23
|
+
"test": "node --import tsx --test test/**/*.test.ts",
|
|
24
|
+
"test:coverage": "c8 --reporter=text --reporter=lcov --reporter=json-summary node --import tsx --test test/**/*.test.ts",
|
|
25
|
+
"coverage:badge": "node scripts/coverage-badge.mjs",
|
|
23
26
|
"prepack": "tsup"
|
|
24
27
|
},
|
|
25
28
|
"files": [
|
|
@@ -50,12 +53,15 @@
|
|
|
50
53
|
"@opencode-ai/plugin": "*",
|
|
51
54
|
"@opentui/core": ">=0.4.0",
|
|
52
55
|
"@opentui/solid": ">=0.4.0",
|
|
56
|
+
"@types/node": "^26.1.1",
|
|
57
|
+
"c8": "^11.0.0",
|
|
53
58
|
"esbuild-plugin-solid": "^0.6.0",
|
|
54
59
|
"eslint": "^10.6.0",
|
|
55
60
|
"eslint-plugin-solid": "^0.14.5",
|
|
56
61
|
"globals": "^17.7.0",
|
|
57
62
|
"solid-js": "^1.9",
|
|
58
63
|
"tsup": "^8.5",
|
|
64
|
+
"tsx": "^4.23.0",
|
|
59
65
|
"typescript": "^5",
|
|
60
66
|
"typescript-eslint": "^8.63.0"
|
|
61
67
|
}
|