bareloop 0.35.0 → 0.36.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/CHANGELOG.md +27 -0
- package/bareloop.context.md +36 -8
- package/package.json +1 -1
- package/src/authorrun.js +3 -2
- package/src/bundlerun.js +4 -8
- package/src/draftspend.js +116 -1
- package/src/index.js +3 -3
- package/src/legs.js +1 -1
- package/src/movefile.js +22 -0
- package/src/npminstall.js +55 -0
- package/src/panel/authorsession.js +87 -17
- package/src/panel/index.html +137 -43
- package/src/panel/server.js +130 -14
- package/src/panel/settingsroutes.js +52 -2
- package/src/ralph.js +1 -1
- package/src/source.js +43 -4
- package/src/u-patient.js +3 -2
- package/src/userrun.js +56 -10
- package/src/worktree.js +120 -0
- package/types/draftspend.d.ts +88 -0
- package/types/legs.d.ts +1 -1
- package/types/movefile.d.ts +9 -0
- package/types/npminstall.d.ts +30 -0
- package/types/panel/authorsession.d.ts +2 -1
- package/types/panel/server.d.ts +13 -2
- package/types/panel/settingsroutes.d.ts +6 -1
- package/types/ralph.d.ts +19 -0
- package/types/source.d.ts +13 -5
- package/types/userrun.d.ts +10 -0
- package/types/worktree.d.ts +48 -0
package/src/panel/index.html
CHANGED
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
.st-word{font-weight:700;color:var(--text);}
|
|
181
181
|
.wf-ended-inline{flex:1 1 auto;white-space:normal;}
|
|
182
182
|
.wf-compact{flex-wrap:wrap;}
|
|
183
|
-
.wf-
|
|
183
|
+
.wf-runno{color:var(--accent);flex:none;}
|
|
184
184
|
.step-state{display:inline-flex;align-items:center;gap:2px;font-size:12px;}
|
|
185
185
|
.badge{display:inline-block;font-size:11px;padding:1px 4px;color:var(--text-dim);background:none;font-weight:700;}
|
|
186
186
|
.badge::before{content:"[";color:var(--text-faint);}
|
|
@@ -467,6 +467,7 @@
|
|
|
467
467
|
.pv-table td,.pv-table th{white-space:nowrap;vertical-align:top;}
|
|
468
468
|
.pv-table select,.pv-table input{min-height:32px;max-width:190px;box-sizing:border-box;}
|
|
469
469
|
.pv-table .hint{display:block;margin-top:3px;}
|
|
470
|
+
.pv-table td.pv-price-cell input{width:90px;}
|
|
470
471
|
/* the Test cell keeps ONE width whatever the result says ("testing…", "OK · 312 ms", a long failure note) */
|
|
471
472
|
.pv-table td.pv-test-cell{width:180px;min-width:180px;max-width:180px;}
|
|
472
473
|
.pv-table .pv-test-result{white-space:normal;overflow-wrap:anywhere;}
|
|
@@ -742,21 +743,24 @@
|
|
|
742
743
|
<section id="panel-providers" class="tabpanel active" role="tabpanel" aria-labelledby="tab-providers">
|
|
743
744
|
<div class="keyfile-strip" data-testid="keyfile-box">
|
|
744
745
|
<div class="keyfile-strip-main">
|
|
745
|
-
<span>Put <code>NAME=key</code> lines in
|
|
746
|
+
<span>Put <code>NAME=key</code> lines in your keys file (Open keys folder), then Reload keys — every key that has a value gets a row. Only names are listed; keys never reach this page.</span>
|
|
747
|
+
<span class="hint" id="pv-keyfile-path" data-testid="keyfile-path"></span>
|
|
746
748
|
<span class="hint" id="pv-keyfile-note" data-testid="keyfile-warning"></span>
|
|
747
749
|
</div>
|
|
748
750
|
<div class="keyfile-strip-actions">
|
|
751
|
+
<button class="btn small" type="button" id="btn-open-keys-folder" data-testid="btn-open-keys-folder">Open keys folder</button>
|
|
749
752
|
<button class="btn small" type="button" id="btn-reload-keys" data-testid="btn-reload-keys">Reload keys</button>
|
|
750
753
|
</div>
|
|
751
754
|
</div>
|
|
752
755
|
<div class="table-wrap" data-testid="provider-table-wrap">
|
|
753
756
|
<table class="pv-table" data-testid="provider-table">
|
|
754
757
|
<thead>
|
|
755
|
-
<tr><th>Key</th><th>Name</th><th>API shape</th><th>Base URL</th><th>Test</th><th>Tokens used</th><th>Balance</th></tr>
|
|
758
|
+
<tr><th>Key</th><th>Name</th><th>API shape</th><th>Base URL</th><th>Test</th><th>Tokens used</th><th>Balance</th><th>In $/1M</th><th>Out $/1M</th></tr>
|
|
756
759
|
</thead>
|
|
757
760
|
<tbody id="pv-rows"></tbody>
|
|
758
761
|
</table>
|
|
759
762
|
</div>
|
|
763
|
+
<div class="hint" id="pv-price-hint" data-testid="pv-price-hint">If your vendor lists two prices, enter the higher one.</div>
|
|
760
764
|
<div id="pv-shell" data-testid="pv-shell"></div>
|
|
761
765
|
<div class="hint" id="pv-note" data-testid="pv-note" style="margin-top:6px;">Name is the model id Chat uses. Base URL blank = the API shape's own host. Test asks the provider for its model list — it costs nothing.</div>
|
|
762
766
|
</section>
|
|
@@ -946,6 +950,8 @@
|
|
|
946
950
|
// jump straight to that SAME part's Audit-tab row (build item B's
|
|
947
951
|
// click-through), with no re-lookup needed.
|
|
948
952
|
function partBoxState(part, isLastLivePart){
|
|
953
|
+
// the drafting part happened before the run existed: it is always done
|
|
954
|
+
if(part.kind === "drafting") return "done";
|
|
949
955
|
if(part.outcome === "green" || part.outcome === "satisfied" || part.outcome === "already-green") return "done";
|
|
950
956
|
if(typeof part.outcome === "string") return "stopped";
|
|
951
957
|
// P5-R: its leg's halt cut this part off — the map's existing "stopped" state, never "done"
|
|
@@ -968,6 +974,7 @@
|
|
|
968
974
|
// (a single-attempt step, or the whole "run" box on an old-shape spine)
|
|
969
975
|
// reads the part's own outcome.
|
|
970
976
|
function partResultGlyph(part, box){
|
|
977
|
+
if(part.kind === "drafting") return glyphSpan(attemptGlyph("green", false)); // [✓]: drafting is done once the run exists
|
|
971
978
|
if(partHasNoVerdict(part)) return null;
|
|
972
979
|
var boxIsLive = box.state === "running";
|
|
973
980
|
if(box.attempts.length > 1) return box.attempts.map(function(a){ return glyphSpan(attemptGlyph(a.outcome, boxIsLive)); }).join("");
|
|
@@ -1247,8 +1254,12 @@
|
|
|
1247
1254
|
if(!d || isNaN(d.getTime())) return "unknown";
|
|
1248
1255
|
return d.toLocaleString("en-US", {year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "2-digit"});
|
|
1249
1256
|
}
|
|
1250
|
-
// the right-side header: `[sign] job (
|
|
1257
|
+
// the right-side header: `[sign] job (run-N) │ **word** — reason · ended 10/4/2026, 6:01 PM` (no capitals: the job
|
|
1251
1258
|
// name is shown as written). `bodyHtml` is already escaped.
|
|
1259
|
+
// a run's NAME for a person (P6 item 7): `job (run-N)`, N from the server (`runNo`, one owner) — the page never counts.
|
|
1260
|
+
// A run with no number (an imported bundle's run is not in the run list) falls back to its id.
|
|
1261
|
+
function runLabel(runNo, runid){ return typeof runNo === "number" ? "run-" + runNo : String(runid || ""); }
|
|
1262
|
+
function runName(job, runNo, runid){ return job + " (" + runLabel(runNo, runid) + ")"; }
|
|
1252
1263
|
function setRunHeader(sign, nameText, bodyHtml){
|
|
1253
1264
|
document.getElementById("active-wf-name").textContent = nameText;
|
|
1254
1265
|
document.getElementById("active-wf-dot").className = "dot " + glyphClass(sign);
|
|
@@ -1294,7 +1305,7 @@
|
|
|
1294
1305
|
row.setAttribute("tabindex", "0");
|
|
1295
1306
|
row.setAttribute("role", "button");
|
|
1296
1307
|
row.setAttribute("aria-pressed", selected ? "true" : "false");
|
|
1297
|
-
var histFullName = r.job
|
|
1308
|
+
var histFullName = runName(r.job, r.runNo, r.runid);
|
|
1298
1309
|
if(r.starting){
|
|
1299
1310
|
row.innerHTML =
|
|
1300
1311
|
'<span class="wf-line1"><span class="dot amber"></span>' +
|
|
@@ -1312,14 +1323,20 @@
|
|
|
1312
1323
|
'<span class="wf-meta">' + escapeXml((r.at || "").slice(0,10) || "unknown") + '</span>' +
|
|
1313
1324
|
'</span>';
|
|
1314
1325
|
} else if(compact){
|
|
1315
|
-
// an EXPANDED per-run row under a workflow
|
|
1326
|
+
// an EXPANDED per-run row under a workflow (hamr 2026-10-06), 2 lines, left-aligned:
|
|
1327
|
+
// `[sign] (run-N) **word** — reason` / `$ ($drafting) · wall · date`
|
|
1316
1328
|
row.innerHTML =
|
|
1317
1329
|
'<span class="wf-line1 wf-compact"><span class="dot ' + glyphClass(r.glyph) + '"></span>' +
|
|
1318
|
-
'<span class="wf-
|
|
1319
|
-
'<span class="wf-
|
|
1320
|
-
|
|
1330
|
+
'<span class="wf-name wf-runno" title="' + escapeXml(histFullName) + '" data-testid="hist-runno-' + escapeXml(r.runid) + '">(' + escapeXml(runLabel(r.runNo, r.runid)) + ')</span>' +
|
|
1331
|
+
'<span class="wf-ended wf-ended-inline" data-testid="hist-ended-' + escapeXml(r.runid) + '">' + statusWordHtml(r.status, r.endedLine) + '</span></span>' +
|
|
1332
|
+
'<span class="wf-meta-line">' +
|
|
1333
|
+
'<span class="wf-meta">' + escapeXml(rowSpendText(r, true)) + '</span>' +
|
|
1334
|
+
'<span class="wf-meta">' + escapeXml(rowWallText(r)) + '</span>' +
|
|
1335
|
+
'<span class="wf-meta">' + escapeXml(r.date || "unknown") + '</span>' +
|
|
1336
|
+
resumedTagHtml(r.resumedCount, "hist-resumed-" + r.runid) +
|
|
1337
|
+
'</span>';
|
|
1321
1338
|
} else {
|
|
1322
|
-
// History card: line 1 sign + job (
|
|
1339
|
+
// History card: line 1 sign + job (run-N); line 2 **word** — reason; line 3 the facts
|
|
1323
1340
|
row.innerHTML =
|
|
1324
1341
|
'<span class="wf-line1"><span class="dot ' + glyphClass(r.glyph) + '"></span>' +
|
|
1325
1342
|
'<span class="wf-name" title="' + escapeXml(histFullName) + '">' + escapeXml(histFullName) + '</span></span>' +
|
|
@@ -1370,6 +1387,7 @@
|
|
|
1370
1387
|
runs: jobRuns,
|
|
1371
1388
|
runCount: jobRuns.length,
|
|
1372
1389
|
lastRunid: last.runid,
|
|
1390
|
+
lastRunNo: last.runNo,
|
|
1373
1391
|
lastAt: last.at,
|
|
1374
1392
|
lastGlyph: last.glyph || "▶",
|
|
1375
1393
|
lastStatus: last.status || null,
|
|
@@ -1377,7 +1395,7 @@
|
|
|
1377
1395
|
lastResumedCount: last.resumedCount || 0,
|
|
1378
1396
|
lastCheckType: last.checkType || "unknown",
|
|
1379
1397
|
lastModel: last.model || null,
|
|
1380
|
-
lastSpend: rowSpendText(last),
|
|
1398
|
+
lastSpend: rowSpendText(last, true),
|
|
1381
1399
|
lastWall: rowWallText(last) || "unknown",
|
|
1382
1400
|
lastDate: last.date,
|
|
1383
1401
|
};
|
|
@@ -1503,18 +1521,16 @@
|
|
|
1503
1521
|
row.setAttribute("role", "button");
|
|
1504
1522
|
row.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1505
1523
|
row.setAttribute("aria-pressed", isSelected ? "true" : "false");
|
|
1506
|
-
var wfFullName = g.job
|
|
1524
|
+
var wfFullName = runName(g.job, g.lastRunNo, g.lastRunid);
|
|
1507
1525
|
row.innerHTML =
|
|
1508
|
-
'<span class="wf-line1"><span class="wf-caret">' + (hasOtherRuns ? (expanded ? "▼" : "▶") : "") + '</span><span class="dot ' + glyphClass(g.lastGlyph) + '"></span>' +
|
|
1509
|
-
'<span class="wf-name" title="' + escapeXml(wfFullName) + '">' + escapeXml(wfFullName) + '</span
|
|
1510
|
-
(g.lastStatus ? '<span class="wf-ended" data-testid="wf-ended-' + escapeXml(g.job) + '">' + statusWordHtml(g.lastStatus, g.lastEndedLine) + '</span>' : '') +
|
|
1526
|
+
'<span class="wf-line1 wf-compact"><span class="wf-caret">' + (hasOtherRuns ? (expanded ? "▼" : "▶") : "") + '</span><span class="dot ' + glyphClass(g.lastGlyph) + '"></span>' +
|
|
1527
|
+
'<span class="wf-name" title="' + escapeXml(wfFullName) + '">' + escapeXml(wfFullName) + '</span>' +
|
|
1528
|
+
(g.lastStatus ? '<span class="wf-ended wf-ended-inline" data-testid="wf-ended-' + escapeXml(g.job) + '">' + statusWordHtml(g.lastStatus, g.lastEndedLine) + '</span>' : '') + '</span>' +
|
|
1511
1529
|
'<span class="wf-meta-line">' +
|
|
1512
1530
|
'<span class="wf-meta">' + escapeXml(g.lastCheckType) + '</span>' +
|
|
1513
1531
|
'<span class="wf-meta">' + escapeXml(g.lastSpend) + '</span>' +
|
|
1514
1532
|
'<span class="wf-meta">' + escapeXml(g.lastWall) + '</span>' +
|
|
1515
1533
|
'<span class="wf-meta">' + escapeXml(g.lastDate || "unknown") + '</span>' +
|
|
1516
|
-
resumedTagHtml(g.lastResumedCount, "wf-resumed-" + g.job) +
|
|
1517
|
-
'<span class="wf-meta">' + g.runCount + ' run' + (g.runCount === 1 ? '' : 's') + '</span>' +
|
|
1518
1534
|
'</span>';
|
|
1519
1535
|
row.addEventListener("click", function(){
|
|
1520
1536
|
// a single-run job has nothing to expand — the click still opens
|
|
@@ -1559,7 +1575,7 @@
|
|
|
1559
1575
|
function filterBarHTML(scope){
|
|
1560
1576
|
return (
|
|
1561
1577
|
'<div class="filterbar" data-scope="' + scope + '" data-testid="' + scope + '-filters">' +
|
|
1562
|
-
'<input type="text" class="search-input" placeholder="search job, run id or model…" ' +
|
|
1578
|
+
'<input type="text" class="search-input" placeholder="search job, run-N, run id or model…" ' +
|
|
1563
1579
|
'id="' + scope + '-filter-search" data-testid="' + scope + '-filter-search" autocomplete="off">' +
|
|
1564
1580
|
'<div class="chips" role="group" aria-label="Filter">' +
|
|
1565
1581
|
'<button class="chip fchip" type="button" aria-pressed="false" data-filter-group="checkType" data-filter-value="deterministic" title="deterministic" data-testid="chip-' + scope + '-ct-deterministic">D</button>' +
|
|
@@ -1588,15 +1604,18 @@
|
|
|
1588
1604
|
// means "no search filter" — everything passes it, same posture as an
|
|
1589
1605
|
// empty chip group). item C (2026-09-26): also matches the run's model
|
|
1590
1606
|
// name (e.g. "deepseek", "sonnet") — `model` is `null`/absent on an older
|
|
1591
|
-
// archived run, same posture as a missing job/runid.
|
|
1592
|
-
|
|
1607
|
+
// archived run, same posture as a missing job/runid. Also the run's shown
|
|
1608
|
+
// name: `run-N` and `job (run-N)` (N is the server's `runNo`, never counted here).
|
|
1609
|
+
function matchesSearch(job, runid, model, query, runNo){
|
|
1593
1610
|
if(!query) return true;
|
|
1594
1611
|
var q = String(query).trim().toLowerCase();
|
|
1595
1612
|
if(!q) return true;
|
|
1596
1613
|
var j = String(job || "").toLowerCase();
|
|
1597
1614
|
var r = String(runid || "").toLowerCase();
|
|
1598
1615
|
var m = String(model || "").toLowerCase();
|
|
1599
|
-
|
|
1616
|
+
var label = typeof runNo === "number" ? "run-" + runNo : "";
|
|
1617
|
+
var shown = label ? j + " (" + label + ")" : "";
|
|
1618
|
+
return j.indexOf(q) !== -1 || r.indexOf(q) !== -1 || m.indexOf(q) !== -1 || (label !== "" && (label.indexOf(q) !== -1 || shown.indexOf(q) !== -1));
|
|
1600
1619
|
}
|
|
1601
1620
|
function filterRuns(runs, filters, nowMs){
|
|
1602
1621
|
nowMs = (typeof nowMs === "number") ? nowMs : Date.now();
|
|
@@ -1607,7 +1626,7 @@
|
|
|
1607
1626
|
return (runs || []).filter(function(r){
|
|
1608
1627
|
if(ct.length && ct.indexOf(r.checkType) === -1) return false;
|
|
1609
1628
|
if(rs.length && rs.indexOf(r.glyph) === -1) return false;
|
|
1610
|
-
if(!matchesSearch(r.job, r.runid, r.model, q)) return false;
|
|
1629
|
+
if(!matchesSearch(r.job, r.runid, r.model, q, r.runNo)) return false;
|
|
1611
1630
|
if(time !== "all"){
|
|
1612
1631
|
var days = time === "7d" ? 7 : 30;
|
|
1613
1632
|
var t = typeof r.at === "string" ? Date.parse(r.at) : NaN;
|
|
@@ -2101,15 +2120,16 @@
|
|
|
2101
2120
|
* complete run whose drafting share is itself incomplete — hamr's ruling:
|
|
2102
2121
|
* a reader summing the two must never read the leading number as exact
|
|
2103
2122
|
* when the bracket next to it isn't). `draftSpendComplete === false` marks
|
|
2104
|
-
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX".
|
|
2123
|
+
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX". `shortForm` drops the word
|
|
2124
|
+
* "drafting" (the Runs list cards only — "$5.00 ($0.08)").
|
|
2105
2125
|
*/
|
|
2106
|
-
function panelMoneyWithDraft(spendCoreVal, spendCoreFloor, draftSpentUsd, draftSpendComplete, budgetUsd){
|
|
2126
|
+
function panelMoneyWithDraft(spendCoreVal, spendCoreFloor, draftSpentUsd, draftSpendComplete, budgetUsd, shortForm){
|
|
2107
2127
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
2108
2128
|
var draftFloor = hasDraft && draftSpendComplete === false;
|
|
2109
2129
|
var core = (spendCoreVal === null || spendCoreVal === undefined)
|
|
2110
2130
|
? "unknown"
|
|
2111
2131
|
: ((spendCoreFloor || draftFloor) ? "at least " + panelMoney(spendCoreVal) : panelMoney(spendCoreVal));
|
|
2112
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + " drafting)" : "";
|
|
2132
|
+
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2113
2133
|
var capPart = (budgetUsd === undefined) ? "" : (" of " + panelMoney(budgetUsd));
|
|
2114
2134
|
return core + draftPart + capPart;
|
|
2115
2135
|
}
|
|
@@ -2134,13 +2154,13 @@
|
|
|
2134
2154
|
* is a floor only when its own drafting share is incomplete.
|
|
2135
2155
|
* @param {any} r a `/api/runs` row (or a Workflows group's `last` row)
|
|
2136
2156
|
*/
|
|
2137
|
-
function rowSpendText(r){
|
|
2157
|
+
function rowSpendText(r, shortForm){
|
|
2138
2158
|
if(r.starting) return "starting…";
|
|
2139
|
-
if(rowIsLive(r)) return liveSpendText(r.spendFloorUsd, r.draftSpentUsd, r.draftSpendComplete);
|
|
2159
|
+
if(rowIsLive(r)) return liveSpendText(r.spendFloorUsd, r.draftSpentUsd, r.draftSpendComplete, shortForm);
|
|
2140
2160
|
var draftIncomplete = typeof r.draftSpentUsd === "number" && r.draftSpentUsd !== null && r.draftSpendComplete === false;
|
|
2141
2161
|
var coreVal = r.died ? r.spendFloorUsd : r.spentUsd;
|
|
2142
2162
|
var coreFloor = !!r.died || draftIncomplete;
|
|
2143
|
-
return panelMoneyWithDraft(coreVal, coreFloor, r.draftSpentUsd, r.draftSpendComplete);
|
|
2163
|
+
return panelMoneyWithDraft(coreVal, coreFloor, r.draftSpentUsd, r.draftSpendComplete, undefined, shortForm);
|
|
2144
2164
|
}
|
|
2145
2165
|
function duration(ms){
|
|
2146
2166
|
if(ms === null || ms === undefined) return "unknown";
|
|
@@ -2163,11 +2183,11 @@
|
|
|
2163
2183
|
* @param {number|null} spendFloorUsd @param {number|null} draftSpentUsd
|
|
2164
2184
|
* @param {boolean|null} draftSpendComplete @returns {string}
|
|
2165
2185
|
*/
|
|
2166
|
-
function liveSpendText(spendFloorUsd, draftSpentUsd, draftSpendComplete){
|
|
2186
|
+
function liveSpendText(spendFloorUsd, draftSpentUsd, draftSpendComplete, shortForm){
|
|
2167
2187
|
if(spendFloorUsd === null || spendFloorUsd === undefined) return "unknown";
|
|
2168
2188
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
2169
2189
|
var draftFloor = hasDraft && draftSpendComplete === false;
|
|
2170
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + " drafting)" : "";
|
|
2190
|
+
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
2171
2191
|
return panelMoney(spendFloorUsd) + " so far" + draftPart;
|
|
2172
2192
|
}
|
|
2173
2193
|
/** build item 6 — the matching wall-clock phrase: "running <duration>",
|
|
@@ -2474,7 +2494,7 @@
|
|
|
2474
2494
|
|
|
2475
2495
|
function renderRun(detail){
|
|
2476
2496
|
if(detail.fileMissing){
|
|
2477
|
-
setRunHeader("\u00B7", detail.job
|
|
2497
|
+
setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
|
|
2478
2498
|
document.getElementById("run-empty").hidden = false;
|
|
2479
2499
|
document.getElementById("run-empty").textContent = "spine file no longer on disk";
|
|
2480
2500
|
document.getElementById("run-content").hidden = true;
|
|
@@ -2485,7 +2505,7 @@
|
|
|
2485
2505
|
}
|
|
2486
2506
|
|
|
2487
2507
|
if(detail.starting){
|
|
2488
|
-
setRunHeader("\u25B6", detail.job
|
|
2508
|
+
setRunHeader("\u25B6", runName(detail.job, detail.runNo, detail.runid), statusWordHtml(detail.status, "starting\u2026") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
|
|
2489
2509
|
document.getElementById("run-empty").hidden = false;
|
|
2490
2510
|
document.getElementById("run-empty").textContent = "starting… the run has not written its log yet";
|
|
2491
2511
|
document.getElementById("run-content").hidden = true;
|
|
@@ -2498,7 +2518,7 @@
|
|
|
2498
2518
|
document.getElementById("audit-body").hidden = false;
|
|
2499
2519
|
renderEnded(detail);
|
|
2500
2520
|
|
|
2501
|
-
setRunHeader(detail.glyph, detail.job
|
|
2521
|
+
setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), runHeaderBody(detail));
|
|
2502
2522
|
|
|
2503
2523
|
document.getElementById("run-empty").hidden = true;
|
|
2504
2524
|
document.getElementById("run-content").hidden = false;
|
|
@@ -2608,7 +2628,7 @@
|
|
|
2608
2628
|
(box.blocked > 0 ? '<span class="badge red" data-testid="part-blocked-badge">' + box.blocked + ' blocked</span>' : '') +
|
|
2609
2629
|
'</div>' +
|
|
2610
2630
|
'<div class="step-meta" title="a part is one piece of the run (scout, plan, a step, replan, fix, judge); a call is one model round">' + line1Bits.join(" · ") + '</div>' +
|
|
2611
|
-
'<div class="step-meta">' + escapeXml(
|
|
2631
|
+
'<div class="step-meta">' + escapeXml(partLine2Text(box.part)) + '</div>';
|
|
2612
2632
|
listEl.appendChild(card);
|
|
2613
2633
|
var jumpToAudit = function(){
|
|
2614
2634
|
document.getElementById("tab-audit").click();
|
|
@@ -2769,6 +2789,46 @@
|
|
|
2769
2789
|
// shows — expand -> attempts (parts with >1, e.g. a multi-attempt step or
|
|
2770
2790
|
// the fix loop) or straight to a rounds table (every other part kind,
|
|
2771
2791
|
// which carries exactly one synthetic whole-part attempt).
|
|
2792
|
+
// The DRAFTING part (a panel-authored run's first part; src/draftspend.js `draftingPartFor`): ONE owner of its
|
|
2793
|
+
// money wording, line 1, line 2 and its Audit table. Money is the spine's own drafting figure ("at least" when it
|
|
2794
|
+
// is a floor); a call with no price reads "unknown", never $0.
|
|
2795
|
+
function draftingMoneyText(part){
|
|
2796
|
+
return (part.spendComplete === false ? "at least " : "") + panelMoney(part.spentUsd);
|
|
2797
|
+
}
|
|
2798
|
+
function draftingLine1Text(part){
|
|
2799
|
+
var n = typeof part.rounds === "number" ? part.rounds : 0;
|
|
2800
|
+
return (part.wallMs === null || part.wallMs === undefined ? "–" : duration(part.wallMs)) + " · " + draftingMoneyText(part) + " · " + n + " call" + (n === 1 ? "" : "s");
|
|
2801
|
+
}
|
|
2802
|
+
// line 2 of a part's card / Audit body: the drafting step names, or a worker part's tool breakdown
|
|
2803
|
+
function partLine2Text(part){
|
|
2804
|
+
if(part && part.kind === "drafting") return (part.draftSteps || []).map(function(x){ return x.label; }).join(" · ");
|
|
2805
|
+
return toolBreakdownLine(part.byTool);
|
|
2806
|
+
}
|
|
2807
|
+
function draftingCallCost(c){
|
|
2808
|
+
if(c.costUsd === null || c.costUsd === undefined) return "unknown";
|
|
2809
|
+
if(c.unpricedRounds > 0) return "unknown (" + c.unpricedRounds + " unpriced round" + (c.unpricedRounds === 1 ? "" : "s") + ")";
|
|
2810
|
+
return panelMoney(c.costUsd);
|
|
2811
|
+
}
|
|
2812
|
+
// one row per drafting step ("–" where it made no call); a step's further calls get their own row named by the
|
|
2813
|
+
// call's own label (confirm#2, revise-1…); the last row is the total, equal to the header's money
|
|
2814
|
+
function draftingTableHtml(part){
|
|
2815
|
+
var rows = "";
|
|
2816
|
+
(part.draftSteps || []).forEach(function(st){
|
|
2817
|
+
var wall = st.wallMs === null || st.wallMs === undefined ? "–" : duration(st.wallMs);
|
|
2818
|
+
var calls = Array.isArray(st.calls) ? st.calls : [];
|
|
2819
|
+
var first = calls[0];
|
|
2820
|
+
rows += "<tr><td>" + escapeXml(st.label) + "</td><td>" + wall + "</td><td>" + (first ? "1" : "–") + "</td><td>" +
|
|
2821
|
+
(first && first.model ? escapeXml(first.model) : "–") + "</td><td>" + (first ? draftingCallCost(first) : "–") + "</td></tr>";
|
|
2822
|
+
calls.slice(1).forEach(function(c){
|
|
2823
|
+
rows += "<tr><td>" + escapeXml(st.label) + " (" + escapeXml(c.label) + ")</td><td>–</td><td>1</td><td>" +
|
|
2824
|
+
(c.model ? escapeXml(c.model) : "–") + "</td><td>" + draftingCallCost(c) + "</td></tr>";
|
|
2825
|
+
});
|
|
2826
|
+
});
|
|
2827
|
+
rows += '<tr class="drafting-total"><td><b>total</b></td><td>' + (part.wallMs === null || part.wallMs === undefined ? "–" : duration(part.wallMs)) +
|
|
2828
|
+
"</td><td>" + (typeof part.rounds === "number" ? part.rounds : 0) + "</td><td>–</td><td>" + draftingMoneyText(part) + "</td></tr>";
|
|
2829
|
+
return '<div class="audit-table-scroll"><table class="rounds-table drafting-table" data-testid="drafting-table">' +
|
|
2830
|
+
"<thead><tr><th>Step</th><th>Time</th><th>Calls</th><th>Model</th><th>Cost</th></tr></thead><tbody>" + rows + "</tbody></table></div>";
|
|
2831
|
+
}
|
|
2772
2832
|
function partHasWrites(part){
|
|
2773
2833
|
var bt = part.byTool;
|
|
2774
2834
|
if(!bt) return false;
|
|
@@ -2798,6 +2858,7 @@
|
|
|
2798
2858
|
* @returns {string}
|
|
2799
2859
|
*/
|
|
2800
2860
|
function partLine1Text(part, detail){
|
|
2861
|
+
if(part && part.kind === "drafting") return draftingLine1Text(part);
|
|
2801
2862
|
var isLiveRunPart = part && part.kind === "run" && detail && !detail.died && detail.spentUsd === null;
|
|
2802
2863
|
var wallText = (isLiveRunPart && part.wallMs === null) ? liveWallPhrase(detail.wallFloorMs) : duration(part.wallMs);
|
|
2803
2864
|
var spendText = (isLiveRunPart && part.spentUsd === null)
|
|
@@ -3016,9 +3077,15 @@
|
|
|
3016
3077
|
built = true;
|
|
3017
3078
|
var toolsHeader = document.createElement("div");
|
|
3018
3079
|
toolsHeader.className = "hint";
|
|
3019
|
-
toolsHeader.textContent =
|
|
3080
|
+
toolsHeader.textContent = partLine2Text(part);
|
|
3020
3081
|
body.appendChild(toolsHeader);
|
|
3021
|
-
if(
|
|
3082
|
+
if(part.kind === "drafting"){
|
|
3083
|
+
// no tool calls and no rounds: one row per drafting step instead (the drafting part is index 0 of the
|
|
3084
|
+
// shown list; the rounds endpoint has nothing to serve for it)
|
|
3085
|
+
var draftingEl = document.createElement("div");
|
|
3086
|
+
draftingEl.innerHTML = draftingTableHtml(part);
|
|
3087
|
+
body.appendChild(draftingEl);
|
|
3088
|
+
} else if(attempts.length > 1){
|
|
3022
3089
|
// parts WITH an attempt sub-loop (a multi-attempt step, or the fix
|
|
3023
3090
|
// loop) expand to attempt rows first — each attempt expands to its
|
|
3024
3091
|
// own rounds table on its own click, same as the Run tab used to.
|
|
@@ -3480,7 +3547,7 @@
|
|
|
3480
3547
|
authorGet("/api/settings/providers").then(function(r){
|
|
3481
3548
|
if(!r || !r.ok){ pvNote.textContent = "could not read providers"; document.getElementById("pv-keyfile-path").textContent = "the keys file (path unknown — could not read providers)"; return; }
|
|
3482
3549
|
var kf = r.keysFile;
|
|
3483
|
-
document.getElementById("pv-keyfile-path").textContent =
|
|
3550
|
+
document.getElementById("pv-keyfile-path").textContent = "";
|
|
3484
3551
|
document.getElementById("pv-keyfile-note").textContent = kf.warning ? ("⚠ " + kf.warning) : "";
|
|
3485
3552
|
pvRows.innerHTML = r.rows.length ? r.rows.map(function(row){
|
|
3486
3553
|
var e = escapeXml(row.envName);
|
|
@@ -3500,8 +3567,10 @@
|
|
|
3500
3567
|
? '<button class="btn small pv-test" type="button" data-testid="test-connection-' + e + '">Test</button><div class="hint pv-test-result" data-testid="test-result-' + e + '"></div>'
|
|
3501
3568
|
: '<span class="hint">not available</span>') + "</td>"
|
|
3502
3569
|
+ '<td data-testid="pv-tokens-' + e + '">' + escapeXml(tokensText(row.tokens)) + "</td>"
|
|
3503
|
-
+ "<td>" + bal + "</td
|
|
3504
|
-
|
|
3570
|
+
+ "<td>" + bal + "</td>"
|
|
3571
|
+
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-in" data-testid="pv-price-in-' + e + '" placeholder="not set" value="' + (row.priceInPerM === null ? "" : escapeXml(String(row.priceInPerM))) + '"></td>'
|
|
3572
|
+
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-out" data-testid="pv-price-out-' + e + '" placeholder="not set" value="' + (row.priceOutPerM === null ? "" : escapeXml(String(row.priceOutPerM))) + '"></td>';
|
|
3573
|
+
}).join("") : '<tr><td colspan="9" class="hint" data-testid="pv-empty">no keys with a value yet — add NAME=key to the keys file, then Reload keys</td></tr>';
|
|
3505
3574
|
pvRows.querySelectorAll(".pv-balance-fetch").forEach(function(fetchEl){
|
|
3506
3575
|
authorGet("/api/settings/providers/balance?env=" + encodeURIComponent(fetchEl.getAttribute("data-env"))).then(function(b){
|
|
3507
3576
|
fetchEl.textContent = b && b.text ? b.text : ("— " + ((b && b.note) || "unavailable"));
|
|
@@ -3515,6 +3584,16 @@
|
|
|
3515
3584
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
3516
3585
|
}).catch(function(){ pvNote.textContent = "could not read providers"; });
|
|
3517
3586
|
}
|
|
3587
|
+
// The row's POST body. A price is both boxes or neither (the server refuses a half-set one), so while exactly one
|
|
3588
|
+
// is filled — the normal middle of typing a price — the price fields are LEFT OUT (the server keeps what is saved)
|
|
3589
|
+
// and the other boxes still save; the price posts once both are filled, or both are cleared.
|
|
3590
|
+
function pvRowBody(env, v){
|
|
3591
|
+
var pin = String(v.priceIn).trim();
|
|
3592
|
+
var pout = String(v.priceOut).trim();
|
|
3593
|
+
var body = { envName: env, name: v.name, shape: v.shape, baseUrl: v.baseUrl };
|
|
3594
|
+
if((pin === "") === (pout === "")){ body.priceInPerM = pin; body.priceOutPerM = pout; }
|
|
3595
|
+
return body;
|
|
3596
|
+
}
|
|
3518
3597
|
pvRows.addEventListener("change", function(e){
|
|
3519
3598
|
var t = e.target;
|
|
3520
3599
|
var tr = t.closest("tr");
|
|
@@ -3525,14 +3604,15 @@
|
|
|
3525
3604
|
}).catch(function(){ pvNote.textContent = "could not save"; });
|
|
3526
3605
|
return;
|
|
3527
3606
|
}
|
|
3528
|
-
if(!(t.classList.contains("pv-name") || t.classList.contains("pv-shape") || t.classList.contains("pv-url"))) return;
|
|
3607
|
+
if(!(t.classList.contains("pv-name") || t.classList.contains("pv-shape") || t.classList.contains("pv-url") || t.classList.contains("pv-price-in") || t.classList.contains("pv-price-out"))) return;
|
|
3529
3608
|
var env = tr.getAttribute("data-env");
|
|
3530
|
-
authorPost("/api/settings/providers/row", {
|
|
3531
|
-
envName: env,
|
|
3609
|
+
authorPost("/api/settings/providers/row", pvRowBody(env, {
|
|
3532
3610
|
name: tr.querySelector(".pv-name").value,
|
|
3533
3611
|
shape: tr.querySelector(".pv-shape").value,
|
|
3534
3612
|
baseUrl: tr.querySelector(".pv-url").value,
|
|
3535
|
-
|
|
3613
|
+
priceIn: tr.querySelector(".pv-price-in").value,
|
|
3614
|
+
priceOut: tr.querySelector(".pv-price-out").value,
|
|
3615
|
+
})).then(function(r){
|
|
3536
3616
|
pvNote.textContent = r.body && r.body.ok ? ("Saved — " + env + ".") : ((r.body && r.body.error) || "could not save");
|
|
3537
3617
|
// no re-render: it would steal focus mid-edit; Chat's Model menu just re-reads
|
|
3538
3618
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
@@ -3549,6 +3629,14 @@
|
|
|
3549
3629
|
}).catch(function(){ out.textContent = "failed — no answer"; }).then(function(){ t.disabled = false; });
|
|
3550
3630
|
});
|
|
3551
3631
|
document.getElementById("btn-reload-keys").addEventListener("click", loadProviders);
|
|
3632
|
+
// Open keys folder: the server opens the FOLDER in the file manager; if it cannot, the path shows as text
|
|
3633
|
+
document.getElementById("btn-open-keys-folder").addEventListener("click", function(){
|
|
3634
|
+
var out = document.getElementById("pv-keyfile-path");
|
|
3635
|
+
authorPost("/api/settings/open-keys-folder", {}).then(function(r){
|
|
3636
|
+
var b = r.body || {};
|
|
3637
|
+
out.textContent = b.ok && b.opened ? "" : ("Could not open it here. Your keys folder: " + (b.path || "(unknown)"));
|
|
3638
|
+
}).catch(function(){ out.textContent = "Could not open it here — the keys folder is ~/.config/bareloop"; });
|
|
3639
|
+
});
|
|
3552
3640
|
wireTabs([document.getElementById("tab-providers"), document.getElementById("tab-money")]);
|
|
3553
3641
|
document.getElementById("tab-providers").addEventListener("click", loadProviders);
|
|
3554
3642
|
// Auto-save on a human edit (change = blur or Enter): POST carries the per-start token; the chat has no route here
|
|
@@ -3960,6 +4048,11 @@
|
|
|
3960
4048
|
function fitCardBoxes(){
|
|
3961
4049
|
["jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge"].forEach(function(id){ fitBox(document.getElementById(id)); });
|
|
3962
4050
|
}
|
|
4051
|
+
// live growth: each card box re-fits on its own input (typing and Enter), through the same fitBox
|
|
4052
|
+
["jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge"].forEach(function(id){
|
|
4053
|
+
var box = document.getElementById(id);
|
|
4054
|
+
box.addEventListener("input", function(){ fitBox(box); });
|
|
4055
|
+
});
|
|
3963
4056
|
window.addEventListener("resize", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
3964
4057
|
document.getElementById("tab-chat").addEventListener("click", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
3965
4058
|
// Enter sends through the one main button; Shift+Enter (or IME composition) keeps its newline
|
|
@@ -4068,6 +4161,7 @@
|
|
|
4068
4161
|
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4069
4162
|
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4070
4163
|
if(reuseSession) setReuseLocked(true);
|
|
4164
|
+
fitCardBoxes();
|
|
4071
4165
|
renderMessages(st);
|
|
4072
4166
|
renderActions(st);
|
|
4073
4167
|
if(pollTimer) clearInterval(pollTimer);
|