opencode-usage-coach 0.8.5 → 0.9.0
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 +164 -30
- package/dist/tui.js +174 -126
- package/package.json +6 -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,26 @@ 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
|
+
function fetchQuotaWithRetry(provider, maxRetries = 3) {
|
|
1173
|
+
return new Promise(async (resolve2) => {
|
|
1174
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1175
|
+
const q = await fetchQuota(provider);
|
|
1176
|
+
if (q) return resolve2(q);
|
|
1177
|
+
if (attempt < maxRetries - 1) {
|
|
1178
|
+
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
resolve2(null);
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1130
1184
|
function coach(q, lighter) {
|
|
1131
|
-
if (!q) return { decision: "GO", advice: "quota unavailable \u2014 proceeding cautiously.", weekly: -
|
|
1185
|
+
if (!q) return { decision: "GO", advice: "quota unavailable \u2014 retrying. proceeding cautiously.", weekly: -2, monthly: -2, fiveHour: -2 };
|
|
1132
1186
|
const wk = Math.round(q.weekly?.usedPercent ?? 0), mo = Math.round(q.monthly?.usedPercent ?? 0), h5 = Math.round(q.fiveHour?.usedPercent ?? 0);
|
|
1187
|
+
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
1188
|
const wkR = humanRemaining(q.weekly?.resetsAt), h5R = humanRemaining(q.fiveHour?.resetsAt);
|
|
1134
1189
|
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
1190
|
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 +1208,8 @@ function isFreeModel(model, provider) {
|
|
|
1153
1208
|
}
|
|
1154
1209
|
function providerToCodexbar(provider) {
|
|
1155
1210
|
if (!provider) return "";
|
|
1156
|
-
|
|
1211
|
+
const first = provider.split("-")[0];
|
|
1212
|
+
return first || provider;
|
|
1157
1213
|
}
|
|
1158
1214
|
async function resolveAgent(client, sessionID) {
|
|
1159
1215
|
if (!sessionID) return "";
|
|
@@ -1198,6 +1254,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1198
1254
|
const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
|
|
1199
1255
|
const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
|
|
1200
1256
|
let last = null;
|
|
1257
|
+
let lastKnownQuota = null;
|
|
1201
1258
|
let lastFetchedAt = 0;
|
|
1202
1259
|
let refreshing = false;
|
|
1203
1260
|
const refreshBackground = () => {
|
|
@@ -1215,9 +1272,11 @@ async function UsageCoachPlugin(input) {
|
|
|
1215
1272
|
return;
|
|
1216
1273
|
}
|
|
1217
1274
|
const activeProvider = providerToCodexbar(currentProvider) || PROVIDER;
|
|
1218
|
-
|
|
1275
|
+
fetchQuotaWithRetry(activeProvider).then(async (q) => {
|
|
1219
1276
|
try {
|
|
1220
|
-
|
|
1277
|
+
if (q) lastKnownQuota = q;
|
|
1278
|
+
const effectiveQ = q ?? lastKnownQuota;
|
|
1279
|
+
last = coach(effectiveQ, LIGHTER);
|
|
1221
1280
|
lastFetchedAt = Date.now();
|
|
1222
1281
|
let providers = [];
|
|
1223
1282
|
try {
|
|
@@ -1234,7 +1293,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1234
1293
|
log(`refresh-in-then err: ${String(e)}`);
|
|
1235
1294
|
}
|
|
1236
1295
|
}).catch((e) => {
|
|
1237
|
-
log(`
|
|
1296
|
+
log(`fetchQuotaWithRetry err: ${String(e)}`);
|
|
1238
1297
|
}).finally(() => {
|
|
1239
1298
|
refreshing = false;
|
|
1240
1299
|
});
|
|
@@ -1272,7 +1331,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1272
1331
|
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
1273
1332
|
currentAgent = agent;
|
|
1274
1333
|
refreshBackground();
|
|
1275
|
-
const harnessTools = ["unknown_scan", "generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure", "reverse_interview"];
|
|
1334
|
+
const harnessTools = ["unknown_scan", "question", "generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure", "reverse_interview"];
|
|
1276
1335
|
if (!harnessTools.includes(_input.tool)) return;
|
|
1277
1336
|
if (!isHarnessAgent(agent)) {
|
|
1278
1337
|
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 +1377,11 @@ async function UsageCoachPlugin(input) {
|
|
|
1318
1377
|
\u26A0 DIAGNOSIS GATE \u2014 unknown_scan is REQUIRED before generate/generate_batch.
|
|
1319
1378
|
unknown_scan({ prompt: "<user request>", tasks: [{id:1, title:"..."}, ...] })
|
|
1320
1379
|
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
|
-
|
|
1380
|
+
Review the report: if QUESTIONS are flagged \u2192 call question() to present them
|
|
1381
|
+
to the user BEFORE generate (it is enforced \u2014 generate will block with a
|
|
1382
|
+
warning until answers are recorded). If TASK REFINEMENTS are suggested \u2192
|
|
1383
|
+
apply via task_update. Unknown unknowns found will be automatically injected
|
|
1384
|
+
into generate prompts as context.
|
|
1324
1385
|
|
|
1325
1386
|
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
1387
|
|
|
@@ -1462,6 +1523,42 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
1462
1523
|
return formatReport(result);
|
|
1463
1524
|
}
|
|
1464
1525
|
}),
|
|
1526
|
+
question: tool({
|
|
1527
|
+
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.",
|
|
1528
|
+
args: {
|
|
1529
|
+
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.`)
|
|
1530
|
+
},
|
|
1531
|
+
async execute(args, ctx) {
|
|
1532
|
+
const h = readHarness(ctx.sessionID);
|
|
1533
|
+
if (!h || !h.unknownScan || !h.unknownScan.questions?.length) {
|
|
1534
|
+
return "No questions from unknown_scan. [usage-coach NEXT] proceed to generate.";
|
|
1535
|
+
}
|
|
1536
|
+
const questions = h.unknownScan.questions;
|
|
1537
|
+
if (!args.answers) {
|
|
1538
|
+
const lines2 = [`unknown_scan found ${questions.length} question(s) that need user input before proceeding:
|
|
1539
|
+
`];
|
|
1540
|
+
questions.forEach((q) => {
|
|
1541
|
+
lines2.push(`[${q.id}] ${q.question}`);
|
|
1542
|
+
});
|
|
1543
|
+
lines2.push('\nPresent ALL of these questions to the user. Collect their answers, then call question({ answers: { "' + questions[0].id + '": "..." } }) with ALL answers.');
|
|
1544
|
+
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.");
|
|
1545
|
+
return lines2.join("\n");
|
|
1546
|
+
}
|
|
1547
|
+
h.questionsResolved = true;
|
|
1548
|
+
h.questionAnswers = args.answers;
|
|
1549
|
+
writeHarness(ctx.sessionID, h);
|
|
1550
|
+
const answered = Object.keys(args.answers).length;
|
|
1551
|
+
const lines = [`Questions resolved (${answered}/${questions.length} answered).
|
|
1552
|
+
`];
|
|
1553
|
+
questions.forEach((q) => {
|
|
1554
|
+
const ans = args.answers[q.id];
|
|
1555
|
+
if (ans) lines.push(` ${q.id}: ${q.question}
|
|
1556
|
+
\u2192 ${ans}`);
|
|
1557
|
+
});
|
|
1558
|
+
lines.push("\n[usage-coach NEXT] Answers recorded. Proceed to generate \u2014 they will be injected into the sub-session prompt.");
|
|
1559
|
+
return lines.join("\n");
|
|
1560
|
+
}
|
|
1561
|
+
}),
|
|
1465
1562
|
task_update: tool({
|
|
1466
1563
|
description: "Update a harness task's status on the panel. Call whenever a task transitions to generating/grading/revising/completed/failed.",
|
|
1467
1564
|
args: {
|
|
@@ -1655,7 +1752,14 @@ Origin: ${args.task}
|
|
|
1655
1752
|
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
1753
|
async execute(args, ctx) {
|
|
1657
1754
|
const cfg = readHarnessCfg(ctx.directory);
|
|
1658
|
-
if (!cfg.generator)
|
|
1755
|
+
if (!cfg.generator) {
|
|
1756
|
+
const h = readHarness(ctx.sessionID);
|
|
1757
|
+
if (h) {
|
|
1758
|
+
h.active = false;
|
|
1759
|
+
writeHarness(ctx.sessionID, h);
|
|
1760
|
+
}
|
|
1761
|
+
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.';
|
|
1762
|
+
}
|
|
1659
1763
|
let decision = "GO";
|
|
1660
1764
|
try {
|
|
1661
1765
|
decision = current().decision;
|
|
@@ -1761,7 +1865,14 @@ ${gate.summary}
|
|
|
1761
1865
|
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
1866
|
async execute(args, ctx) {
|
|
1763
1867
|
const cfg = readHarnessCfg(ctx.directory);
|
|
1764
|
-
if (!cfg.generator)
|
|
1868
|
+
if (!cfg.generator) {
|
|
1869
|
+
const h = readHarness(ctx.sessionID);
|
|
1870
|
+
if (h) {
|
|
1871
|
+
h.active = false;
|
|
1872
|
+
writeHarness(ctx.sessionID, h);
|
|
1873
|
+
}
|
|
1874
|
+
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.';
|
|
1875
|
+
}
|
|
1765
1876
|
let decision = "GO";
|
|
1766
1877
|
try {
|
|
1767
1878
|
decision = current().decision;
|
|
@@ -2039,5 +2150,28 @@ If the task is already well-specified with no significant ambiguities, return {"
|
|
|
2039
2150
|
}
|
|
2040
2151
|
}
|
|
2041
2152
|
export {
|
|
2042
|
-
|
|
2153
|
+
buildGapPrompt,
|
|
2154
|
+
buildScanSummary,
|
|
2155
|
+
checkScanGate,
|
|
2156
|
+
clearSubSession,
|
|
2157
|
+
coach,
|
|
2158
|
+
UsageCoachPlugin as default,
|
|
2159
|
+
detectLanguage,
|
|
2160
|
+
extractImplNotes,
|
|
2161
|
+
extractKeywords,
|
|
2162
|
+
findActiveTaskId,
|
|
2163
|
+
formatReport,
|
|
2164
|
+
humanRemaining,
|
|
2165
|
+
isFreeModel,
|
|
2166
|
+
isHarnessAgent,
|
|
2167
|
+
parseFileList,
|
|
2168
|
+
parseGapAnalysis,
|
|
2169
|
+
parseQuotaResponse,
|
|
2170
|
+
providerAdvice,
|
|
2171
|
+
providerToCodexbar,
|
|
2172
|
+
readHarness,
|
|
2173
|
+
readRules,
|
|
2174
|
+
setStateDir,
|
|
2175
|
+
updateSubSession,
|
|
2176
|
+
writeHarness
|
|
2043
2177
|
};
|
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.0",
|
|
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,14 @@
|
|
|
50
53
|
"@opencode-ai/plugin": "*",
|
|
51
54
|
"@opentui/core": ">=0.4.0",
|
|
52
55
|
"@opentui/solid": ">=0.4.0",
|
|
56
|
+
"c8": "^11.0.0",
|
|
53
57
|
"esbuild-plugin-solid": "^0.6.0",
|
|
54
58
|
"eslint": "^10.6.0",
|
|
55
59
|
"eslint-plugin-solid": "^0.14.5",
|
|
56
60
|
"globals": "^17.7.0",
|
|
57
61
|
"solid-js": "^1.9",
|
|
58
62
|
"tsup": "^8.5",
|
|
63
|
+
"tsx": "^4.23.0",
|
|
59
64
|
"typescript": "^5",
|
|
60
65
|
"typescript-eslint": "^8.63.0"
|
|
61
66
|
}
|