bareloop 0.36.0 → 0.38.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 +44 -0
- package/bareloop.context.md +52 -22
- package/package.json +1 -1
- package/src/authorrun.js +2 -3
- package/src/draftspend.js +1 -3
- package/src/exits.js +3 -0
- package/src/legs.js +15 -0
- package/src/panel/authorroutes.js +19 -1
- package/src/panel/authorsession.js +14 -16
- package/src/panel/index.html +379 -147
- package/src/panel/server.js +79 -15
- package/src/panel/status.js +27 -4
- package/src/planrun.js +11 -8
- package/src/ralph.js +8 -2
- package/src/replay.js +23 -3
- package/src/source.js +12 -0
- package/types/legs.d.ts +12 -0
- package/types/panel/authorroutes.d.ts +4 -1
- package/types/panel/authorsession.d.ts +2 -7
- package/types/panel/server.d.ts +22 -4
- package/types/panel/status.d.ts +21 -1
- package/types/planrun.d.ts +3 -3
- package/types/ralph.d.ts +6 -1
- package/types/source.d.ts +7 -0
package/src/panel/index.html
CHANGED
|
@@ -140,6 +140,8 @@
|
|
|
140
140
|
.dot::before{content:"[·]";color:var(--grey-dot);}
|
|
141
141
|
.dot.green::before{content:"[✓]";color:var(--green);}
|
|
142
142
|
.dot.red::before{content:"[✗]";color:var(--red);}
|
|
143
|
+
/* stopped (hamr 2026-10-07): the person pressed Stop — its own sign, same red as [✗] */
|
|
144
|
+
.dot.stopped::before{content:"[■]";color:var(--red);}
|
|
143
145
|
/* Panel build P3 addition (2026-09-27, "yes, go"): the part cards below the
|
|
144
146
|
step map (`partResultGlyph`/`attemptGlyph`) and the chat job card render
|
|
145
147
|
their result glyph as PLAIN inline text inside a larger string (never
|
|
@@ -272,10 +274,21 @@
|
|
|
272
274
|
.import-view .imp-row{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;}
|
|
273
275
|
.import-view .imp-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:84px;}
|
|
274
276
|
.import-view .imp-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
275
|
-
.startfrom-line{margin:
|
|
277
|
+
.startfrom-line{margin:4px 0 0;font-size:11px;color:var(--text-dim);overflow-wrap:anywhere;}
|
|
276
278
|
.job-card .locked{opacity:.55;cursor:not-allowed;}
|
|
277
|
-
|
|
278
|
-
.
|
|
279
|
+
/* Reuse workflow picker (hamr 2026-10-06): the third Check type radio; search box + list, then the picked box */
|
|
280
|
+
.radio-row label{white-space:nowrap;}
|
|
281
|
+
.rw-box{position:relative;margin-top:6px;}
|
|
282
|
+
.rw-box input{width:100%;box-sizing:border-box;}
|
|
283
|
+
.rw-list{list-style:none;margin:0;padding:0;border:1px solid var(--border-strong);border-top:none;background:var(--bg);max-height:220px;overflow:auto;}
|
|
284
|
+
.rw-list li{padding:7px 8px;border-bottom:1px dotted var(--border);cursor:pointer;}
|
|
285
|
+
.rw-list li:hover,.rw-list li.hl{background:var(--panel2);}
|
|
286
|
+
.rw-name{font-weight:700;overflow-wrap:anywhere;} .rw-name mark{background:none;color:var(--accent);text-decoration:underline;}
|
|
287
|
+
.rw-meta{font-size:11px;color:var(--text-dim);margin-top:2px;overflow-wrap:anywhere;}
|
|
288
|
+
.rw-empty{padding:7px 8px;font-size:12px;color:var(--text-dim);}
|
|
289
|
+
.rw-picked{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:6px;padding:6px 8px;border:1px solid var(--accent);}
|
|
290
|
+
.rw-picked span{overflow-wrap:anywhere;}
|
|
291
|
+
.rw-picked a{font-size:12px;cursor:pointer;flex:none;}
|
|
279
292
|
.run-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px;}
|
|
280
293
|
.run-actions[hidden]{display:none;}
|
|
281
294
|
.run-actions .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
|
|
@@ -284,10 +297,6 @@
|
|
|
284
297
|
.wf-resumed{color:var(--amber);}
|
|
285
298
|
.audit-leg-divider{text-align:center;color:var(--text-dim);font-size:12px;margin:10px 0;overflow-wrap:anywhere;}
|
|
286
299
|
.audit-leg-divider-row td{text-align:center;color:var(--text-dim);font-size:12px;background:var(--panel2);}
|
|
287
|
-
/* Resume opens the run's own Job tab: only the two caps turn into inputs, everything else stays read-only */
|
|
288
|
-
.resume-job{border:1px dashed var(--border-strong);margin-top:12px;padding:10px;display:grid;gap:8px;}
|
|
289
|
-
.resume-cap{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
|
|
290
|
-
.resume-cap input{width:7em;max-width:100%;min-height:36px;background:var(--panel);color:var(--text);border:1px solid var(--border-strong);padding:4px 8px;font:inherit;}
|
|
291
300
|
.summary-box.red{border-color:var(--red);}
|
|
292
301
|
.summary-box.amber{border-color:var(--amber);}
|
|
293
302
|
.summary-box.magenta{border-color:var(--magenta);}
|
|
@@ -363,6 +372,9 @@
|
|
|
363
372
|
.job-card{position:relative;border:1px solid var(--border-strong);padding:14px 10px 10px;background:var(--panel2);margin-top:10px;margin-bottom:10px;}
|
|
364
373
|
.job-card h4{margin:0 0 8px;font-size:13px;position:absolute;top:-10px;left:10px;background:var(--panel2);padding:0 6px;}
|
|
365
374
|
.job-card .card-clear{position:absolute;top:-13px;right:10px;padding:1px 8px;font-size:11px;line-height:1.4;background:var(--panel2);}
|
|
375
|
+
.job-card .origin-line{font-size:12px;padding:6px 8px;border-left:3px solid var(--amber);background:var(--panel);color:var(--text-dim);margin-bottom:6px;overflow-wrap:anywhere;}
|
|
376
|
+
.job-card .origin-line.res{border-color:var(--accent, var(--text));}
|
|
377
|
+
.job-card .chat-resume-box{display:grid;gap:8px;margin-top:10px;}
|
|
366
378
|
.job-card h4::before{content:"┤ ";color:var(--text-faint);}
|
|
367
379
|
.job-card h4::after{content:" ├";color:var(--text-faint);}
|
|
368
380
|
.field{margin-bottom:10px;}
|
|
@@ -519,17 +531,25 @@
|
|
|
519
531
|
<!-- hamr 2026-10-05: the empty card is ALWAYS there when Chat opens (no + New). The top-right button reads
|
|
520
532
|
"Abandon" while a session is live and unsigned, else "Clear"; both leave an empty card. -->
|
|
521
533
|
<div class="job-card compact" id="job-card" data-testid="job-card">
|
|
522
|
-
<div class="startfrom-origin" id="chat-startfrom-origin" data-testid="startfrom-origin" hidden></div>
|
|
523
534
|
<h4 id="job-card-title">Job card — draft</h4>
|
|
524
535
|
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
525
|
-
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
526
536
|
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
537
|
+
<div class="origin-line" id="chat-origin-line" data-testid="origin-line" hidden></div>
|
|
527
538
|
|
|
528
539
|
<div class="field">
|
|
529
540
|
<label id="jf-verdict-label">Check type</label>
|
|
530
541
|
<div class="radio-row" role="radiogroup" aria-labelledby="jf-verdict-label">
|
|
531
542
|
<label><input type="radio" name="jf-verdict" value="deterministic" checked> Deterministic</label>
|
|
532
543
|
<label><input type="radio" name="jf-verdict" value="rubric"> Rubric</label>
|
|
544
|
+
<label><input type="radio" name="jf-verdict" value="reuse" data-testid="jf-verdict-reuse"> Reuse workflow</label>
|
|
545
|
+
</div>
|
|
546
|
+
<div id="rw" data-testid="rw" hidden>
|
|
547
|
+
<div class="rw-box" id="rw-search" data-testid="rw-search">
|
|
548
|
+
<input id="rw-q" data-testid="rw-q" type="text" placeholder="type a job name…" autocomplete="off">
|
|
549
|
+
<ul class="rw-list" id="rw-list" data-testid="rw-list"></ul>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="rw-picked" id="rw-picked" data-testid="rw-picked" hidden><span id="rw-picked-txt" data-testid="rw-picked-txt"></span><a id="rw-change" data-testid="rw-change" role="button" tabindex="0">change</a></div>
|
|
552
|
+
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
533
553
|
</div>
|
|
534
554
|
</div>
|
|
535
555
|
<div class="field">
|
|
@@ -580,6 +600,11 @@
|
|
|
580
600
|
</div>
|
|
581
601
|
</div>
|
|
582
602
|
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
603
|
+
<div id="chat-resume-box" class="chat-resume-box" data-testid="resume-box" hidden>
|
|
604
|
+
<div class="hint" id="chat-resume-spent" data-testid="resume-spent"></div>
|
|
605
|
+
<div class="ended-actions"><button class="btn primary" type="button" id="chat-resume-sign" data-testid="btn-sign-resume">Sign & resume</button> <button class="btn" type="button" id="chat-resume-cancel" data-testid="btn-resume-cancel">Cancel</button></div>
|
|
606
|
+
<div class="resume-err" id="chat-resume-err" data-testid="resume-err" hidden></div>
|
|
607
|
+
</div>
|
|
583
608
|
</div>
|
|
584
609
|
|
|
585
610
|
<div class="chat-thread" id="chat-thread" data-testid="chat-thread"></div>
|
|
@@ -705,8 +730,8 @@
|
|
|
705
730
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
706
731
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
707
732
|
<div class="field-row">
|
|
708
|
-
<div class="field"><label
|
|
709
|
-
<div class="field"><label
|
|
733
|
+
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
734
|
+
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
710
735
|
</div>
|
|
711
736
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
712
737
|
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
@@ -716,7 +741,6 @@
|
|
|
716
741
|
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
717
742
|
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
718
743
|
<div class="hint" id="job-note"></div>
|
|
719
|
-
<div id="resume-job" class="resume-job" data-testid="resume-job" hidden></div>
|
|
720
744
|
</div>
|
|
721
745
|
</section>
|
|
722
746
|
|
|
@@ -955,6 +979,7 @@
|
|
|
955
979
|
if(part.outcome === "green" || part.outcome === "satisfied" || part.outcome === "already-green") return "done";
|
|
956
980
|
if(typeof part.outcome === "string") return "stopped";
|
|
957
981
|
// P5-R: its leg's halt cut this part off — the map's existing "stopped" state, never "done"
|
|
982
|
+
if(part.status && part.status.key === "died") return "died";
|
|
958
983
|
if(part.stopReason) return "stopped";
|
|
959
984
|
if(isLastLivePart) return "running";
|
|
960
985
|
return "done"; // scout/plan/replan/judge: no per-part verdict, but the part itself already happened
|
|
@@ -993,6 +1018,8 @@
|
|
|
993
1018
|
partIndex: idx,
|
|
994
1019
|
title: part.label + occSuffix,
|
|
995
1020
|
state: partBoxState(part, isLastLivePart),
|
|
1021
|
+
// the word this box wears: the server's table entry for the part (src/panel/status.js partStatusFor), except the live part, which is running
|
|
1022
|
+
status: isLastLivePart && partBoxState(part, true) === "running" ? {sign:"\u25B6", word:"running"} : (part.status || null),
|
|
996
1023
|
noNumber: part.kind !== "step",
|
|
997
1024
|
// display-only attempts (map inline text + card/audit result glyph
|
|
998
1025
|
// fallback) — [] for a no-verdict part kind even though `part.attempts`
|
|
@@ -1007,6 +1034,14 @@
|
|
|
1007
1034
|
};
|
|
1008
1035
|
});
|
|
1009
1036
|
}
|
|
1037
|
+
// ONE spelling of a part's shown state: `[sign] word` from the server's table; the raw state only when no table entry came
|
|
1038
|
+
function boxStatusWord(box){
|
|
1039
|
+
return box.status && box.status.word ? box.status.word : String(box.state);
|
|
1040
|
+
}
|
|
1041
|
+
// the map box adds the sign; step cards and Audit rows show the word only (an attempt glyph already sits beside them)
|
|
1042
|
+
function boxStatusText(box){
|
|
1043
|
+
return box.status && box.status.word ? "[" + box.status.sign + "] " + boxStatusWord(box) : boxStatusWord(box);
|
|
1044
|
+
}
|
|
1010
1045
|
function naturalBoxWidth(steps){
|
|
1011
1046
|
var numIdx = stepNumberIndices(steps);
|
|
1012
1047
|
var maxChars = 0;
|
|
@@ -1107,7 +1142,7 @@
|
|
|
1107
1142
|
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[1]) + '</text>';
|
|
1108
1143
|
}
|
|
1109
1144
|
body += '<text x="' + (x + boxW / 2) + '" y="' + stateY +
|
|
1110
|
-
'" text-anchor="middle" font-size="10" fill="' + colors.textFill + '">' + escapeXml(s
|
|
1145
|
+
'" text-anchor="middle" font-size="10" fill="' + colors.textFill + '">' + escapeXml(boxStatusText(s)) + '</text>';
|
|
1111
1146
|
// P2 (2026-09-27 ruling): a running box gets the mockup's pulsing
|
|
1112
1147
|
// amber dot (design/panel-mockup.html ~line 1648) — same fixed color
|
|
1113
1148
|
// and animation, never re-derived from the state's own stroke color.
|
|
@@ -1238,6 +1273,7 @@
|
|
|
1238
1273
|
function glyphClass(g){
|
|
1239
1274
|
if(g === "✓") return "green";
|
|
1240
1275
|
if(g === "✗") return "red";
|
|
1276
|
+
if(g === "■") return "stopped";
|
|
1241
1277
|
if(g === "▶") return "amber";
|
|
1242
1278
|
if(g === "?") return "magenta";
|
|
1243
1279
|
return "grey";
|
|
@@ -1470,6 +1506,17 @@
|
|
|
1470
1506
|
// is a normal, expected action.
|
|
1471
1507
|
var wfExpanded = {};
|
|
1472
1508
|
|
|
1509
|
+
// hamr 2026-10-08 (display only, nothing deleted): with NO search/filter active an expanded job shows at most its
|
|
1510
|
+
// latest WF_SUB_CAP other runs (g.runs is latest-first). A selected older run beyond the cap (picked from History) is
|
|
1511
|
+
// appended after them rather than hidden. With search/filters active every matching sub-card shows.
|
|
1512
|
+
var WF_SUB_CAP = 7;
|
|
1513
|
+
function capSubRuns(childRuns, active, selectedRunid){
|
|
1514
|
+
if(active || childRuns.length <= WF_SUB_CAP) return {shown: childRuns, more: false};
|
|
1515
|
+
var shown = childRuns.slice(0, WF_SUB_CAP);
|
|
1516
|
+
var sel = selectedRunid ? childRuns.slice(WF_SUB_CAP).filter(function(r){ return r.runid === selectedRunid; }) : [];
|
|
1517
|
+
return {shown: shown.concat(sel), more: true};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1473
1520
|
function renderWorkflows(jobs){
|
|
1474
1521
|
var el = document.getElementById("wf-list");
|
|
1475
1522
|
el.innerHTML = "";
|
|
@@ -1556,7 +1603,15 @@
|
|
|
1556
1603
|
if(expanded){
|
|
1557
1604
|
var runsWrap = document.createElement("div");
|
|
1558
1605
|
runsWrap.className = "wf-runs";
|
|
1559
|
-
|
|
1606
|
+
var capped = capSubRuns(childRuns, active, olderSelected);
|
|
1607
|
+
capped.shown.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
|
|
1608
|
+
if(capped.more){
|
|
1609
|
+
var olderHint = document.createElement("div");
|
|
1610
|
+
olderHint.className = "hint";
|
|
1611
|
+
olderHint.setAttribute("data-testid", "wf-older-" + g.job);
|
|
1612
|
+
olderHint.textContent = "older: use search.";
|
|
1613
|
+
runsWrap.appendChild(olderHint);
|
|
1614
|
+
}
|
|
1560
1615
|
wrap.appendChild(runsWrap);
|
|
1561
1616
|
}
|
|
1562
1617
|
el.appendChild(wrap);
|
|
@@ -1625,7 +1680,8 @@
|
|
|
1625
1680
|
var q = (filters && filters.search) || "";
|
|
1626
1681
|
return (runs || []).filter(function(r){
|
|
1627
1682
|
if(ct.length && ct.indexOf(r.checkType) === -1) return false;
|
|
1628
|
-
|
|
1683
|
+
// a stopped run ([■]) was a [✗] before hamr's 2026-10-07 sign ruling — the fail chip still finds it
|
|
1684
|
+
if(rs.length && rs.indexOf(r.glyph) === -1 && !(r.glyph === "\u25A0" && rs.indexOf("\u2717") !== -1)) return false;
|
|
1629
1685
|
if(!matchesSearch(r.job, r.runid, r.model, q, r.runNo)) return false;
|
|
1630
1686
|
if(time !== "all"){
|
|
1631
1687
|
var days = time === "7d" ? 7 : 30;
|
|
@@ -1937,7 +1993,6 @@
|
|
|
1937
1993
|
clearSelection();
|
|
1938
1994
|
currentRunid = null;
|
|
1939
1995
|
runIsLive = false;
|
|
1940
|
-
resumeMode = null;
|
|
1941
1996
|
currentImportId = id;
|
|
1942
1997
|
document.querySelectorAll("#import-list .wf-row").forEach(function(r){
|
|
1943
1998
|
var on = r.getAttribute("data-testid") === "import-row-" + id;
|
|
@@ -2334,11 +2389,9 @@
|
|
|
2334
2389
|
// The sentences are CODE-OWNED on the server (`endedFor`, src/panel/server.js) — this only
|
|
2335
2390
|
// paints them. `detail.ended` is null while a run is live (no block while running).
|
|
2336
2391
|
var lastEndedSig = null;
|
|
2337
|
-
// P5-R
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
var resumeMode = null;
|
|
2341
|
-
var lastJobShown = null;
|
|
2392
|
+
// Resume (P5-R, hamr 2026-10-02: opened the run's own Job tab) MOVED to the left Chat card (hamr 2026-10-07): the card
|
|
2393
|
+
// in RESUME mode, only the money cap and the time cap open (applyResume / signResume). The Job tab is read-only on
|
|
2394
|
+
// every run — values only, no inputs, no buttons. A resume is the SAME run: no new card, no new job.
|
|
2342
2395
|
function renderEnded(detail){
|
|
2343
2396
|
var box = document.getElementById("ended-block");
|
|
2344
2397
|
var e = detail.ended;
|
|
@@ -2355,18 +2408,46 @@
|
|
|
2355
2408
|
'<div class="ended-row"><span class="ended-label">NEXT</span><span class="ended-text" data-testid="ended-next">' + escapeXml(e.next) + '</span></div>';
|
|
2356
2409
|
}
|
|
2357
2410
|
// ---- P5 item 5: the run's ACTION AREA (Run tab, above the counters) — Stop while the run is live, Resume once
|
|
2358
|
-
// it ended in a way the engine would accept (
|
|
2411
|
+
// it ended in a way the engine would accept (opens the Chat card's resume mode), Edit in chat on every non-green ending.
|
|
2359
2412
|
// The Ended block (Audit tab) is text only and carries no buttons.
|
|
2360
2413
|
var stopAsked = {};
|
|
2361
2414
|
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
2362
2415
|
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2363
|
-
|
|
2364
|
-
|
|
2416
|
+
// `model` (optional) = the Chat card's Model box, when the picker asks: the estimate line then counts only that worker's runs.
|
|
2417
|
+
function reuseWorkflow(runid, model){
|
|
2418
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&model=" + encodeURIComponent(model) : "")).then(function(r){
|
|
2365
2419
|
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2366
2420
|
document.getElementById("tab-chat").click();
|
|
2367
2421
|
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
2368
2422
|
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2369
2423
|
}
|
|
2424
|
+
// Edit in chat (hamr 2026-10-07, option A) — the same prefill door as Reuse workflow (`start-from`, the ONE reader of a
|
|
2425
|
+
// run's signed job), but the Chat card opens with EVERY box open and the normal [Start drafting] path: it drafts a new job.
|
|
2426
|
+
// The origin line is code-owned fixed text; run name and status word come from the same helpers the run header uses.
|
|
2427
|
+
function editInChat(detail){
|
|
2428
|
+
var origin = "Copied from " + detail.job + " (" + runLabel(detail.runNo, detail.runid) + ") \u2014 " +
|
|
2429
|
+
((detail.status && detail.status.word) || "ended") +
|
|
2430
|
+
". Every box is open. Change anything and it drafts as a new job; change nothing and it is another run of the same job.";
|
|
2431
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2432
|
+
document.getElementById("tab-chat").click();
|
|
2433
|
+
document.dispatchEvent(new CustomEvent("bareloop-edit", {detail: (r && r.ok) ? {runid: detail.runid, prefill: r, origin: origin} : {error: (r && r.error) || "nothing to copy"}}));
|
|
2434
|
+
}).catch(function(x){
|
|
2435
|
+
document.getElementById("tab-chat").click();
|
|
2436
|
+
document.dispatchEvent(new CustomEvent("bareloop-edit", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
// Resume (hamr 2026-10-07, option B — moved from the run's Job tab): the Chat card opens in RESUME mode, filled from the
|
|
2440
|
+
// SAME prefill (`start-from`), everything locked but the two caps. The card posts /api/runs/<id>/resume; a resume never
|
|
2441
|
+
// drafts and never makes a new card, job or run id (one run = one id).
|
|
2442
|
+
function resumeInChat(detail){
|
|
2443
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2444
|
+
document.getElementById("tab-chat").click();
|
|
2445
|
+
document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: (r && r.ok) ? {runid: detail.runid, prefill: r, resume: detail.resume || {}, title: detail.job + " (" + runLabel(detail.runNo, detail.runid) + ")"} : {error: (r && r.error) || "nothing to resume"}}));
|
|
2446
|
+
}).catch(function(x){
|
|
2447
|
+
document.getElementById("tab-chat").click();
|
|
2448
|
+
document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2370
2451
|
function renderRunActions(detail){
|
|
2371
2452
|
var bar = document.getElementById("run-actions");
|
|
2372
2453
|
var html = "";
|
|
@@ -2382,6 +2463,10 @@
|
|
|
2382
2463
|
var canResume = !!detail.resume && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "resume"; });
|
|
2383
2464
|
if(canResume) html = '<button class="btn primary" type="button" data-testid="btn-resume-run">Resume</button>';
|
|
2384
2465
|
}
|
|
2466
|
+
// Edit in chat: every non-green ending (the server's Ended block says so) — a died run included
|
|
2467
|
+
if(!live && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "edit"; })){
|
|
2468
|
+
html += '<button class="btn" type="button" data-testid="btn-edit-chat">Edit in chat</button>';
|
|
2469
|
+
}
|
|
2385
2470
|
// Reuse workflow: on a GREEN run only (the server's Ended block says so) — a new run of the same signed job
|
|
2386
2471
|
if(!live && !detail.died && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "reuse"; })){
|
|
2387
2472
|
html += '<button class="btn" type="button" data-testid="btn-reuse">Reuse workflow</button>';
|
|
@@ -2390,6 +2475,8 @@
|
|
|
2390
2475
|
bar.hidden = html === "";
|
|
2391
2476
|
var reuseBtn = bar.querySelector('[data-testid="btn-reuse"]');
|
|
2392
2477
|
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid); });
|
|
2478
|
+
var editBtn = bar.querySelector('[data-testid="btn-edit-chat"]');
|
|
2479
|
+
if(editBtn) editBtn.addEventListener("click", function(){ editInChat(detail); });
|
|
2393
2480
|
var stopBtn = bar.querySelector('[data-testid="btn-stop"]');
|
|
2394
2481
|
if(stopBtn){
|
|
2395
2482
|
stopBtn.addEventListener("click", function(){
|
|
@@ -2401,82 +2488,7 @@
|
|
|
2401
2488
|
});
|
|
2402
2489
|
}
|
|
2403
2490
|
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2404
|
-
if(resBtn) resBtn.addEventListener("click", function(){
|
|
2405
|
-
}
|
|
2406
|
-
function openResumeOnJobTab(detail){
|
|
2407
|
-
var r = detail.resume || {};
|
|
2408
|
-
resumeMode = {
|
|
2409
|
-
runid: detail.runid, spentUsd: r.spentUsd, spendComplete: r.spendComplete,
|
|
2410
|
-
wallUsedMs: r.wallUsedMs,
|
|
2411
|
-
budget: (r.budgetUsd === null || r.budgetUsd === undefined) ? "" : String(r.budgetUsd),
|
|
2412
|
-
wall: typeof r.maxWallMin === "number" ? String(r.maxWallMin) : null,
|
|
2413
|
-
painted: false,
|
|
2414
|
-
};
|
|
2415
|
-
document.getElementById("tab-details").click();
|
|
2416
|
-
paintResumeMode();
|
|
2417
|
-
}
|
|
2418
|
-
// Turns the Job tab's two cap values into inputs (and shows the Sign box) while a resume is open on this run;
|
|
2419
|
-
// called again after every Job re-render so a re-render never drops the typed numbers.
|
|
2420
|
-
function paintResumeMode(){
|
|
2421
|
-
var box = document.getElementById("resume-job");
|
|
2422
|
-
var moneyLabel = document.getElementById("details-cap-money-label");
|
|
2423
|
-
var timeLabel = document.getElementById("details-cap-time-label");
|
|
2424
|
-
if(!resumeMode || resumeMode.runid !== currentRunid){
|
|
2425
|
-
box.hidden = true; box.innerHTML = "";
|
|
2426
|
-
moneyLabel.textContent = "$ cap"; timeLabel.textContent = "Time cap";
|
|
2427
|
-
return;
|
|
2428
|
-
}
|
|
2429
|
-
var m = resumeMode;
|
|
2430
|
-
// the unit lives in the caption, never as loose text around the input
|
|
2431
|
-
moneyLabel.textContent = "Money cap ($)";
|
|
2432
|
-
timeLabel.textContent = "Time cap (min)";
|
|
2433
|
-
document.getElementById("details-cap-money").innerHTML =
|
|
2434
|
-
'<span class="resume-cap"><input type="number" min="0" step="0.01" id="resume-budget" data-testid="resume-budget" value="' + escapeXml(m.budget) + '"></span>';
|
|
2435
|
-
if(m.wall !== null){
|
|
2436
|
-
document.getElementById("details-cap-time").innerHTML =
|
|
2437
|
-
'<span class="resume-cap"><input type="number" min="1" step="1" id="resume-wall" data-testid="resume-wall" value="' + escapeXml(m.wall) + '"></span>';
|
|
2438
|
-
}
|
|
2439
|
-
var budgetEl = document.getElementById("resume-budget");
|
|
2440
|
-
budgetEl.addEventListener("input", function(){ m.budget = budgetEl.value; });
|
|
2441
|
-
var wallEl = document.getElementById("resume-wall");
|
|
2442
|
-
if(wallEl) wallEl.addEventListener("input", function(){ m.wall = wallEl.value; });
|
|
2443
|
-
box.hidden = false;
|
|
2444
|
-
if(m.painted) return;
|
|
2445
|
-
m.painted = true;
|
|
2446
|
-
var spent = (m.spentUsd === null || m.spentUsd === undefined) ? "unknown" : ((m.spendComplete ? "" : "at least ") + panelMoney(m.spentUsd));
|
|
2447
|
-
box.innerHTML =
|
|
2448
|
-
'<div data-testid="resume-intro">Resume run ' + escapeXml(m.runid) + ' — the same run, not a new one. Only the money cap and the time cap can change; everything else is the signed job.</div>' +
|
|
2449
|
-
'<div class="hint" data-testid="resume-spent">spent so far ' + escapeXml(spent) +
|
|
2450
|
-
(typeof m.wallUsedMs === "number" && isFinite(m.wallUsedMs) ? ' · time used so far ' + Math.ceil(m.wallUsedMs / 60000) + ' min' : '') + '</div>' +
|
|
2451
|
-
'<div class="ended-actions"><button class="btn primary" type="button" data-testid="btn-sign-resume">Sign & resume</button> ' +
|
|
2452
|
-
'<button class="btn" type="button" data-testid="btn-resume-cancel">Cancel</button></div>' +
|
|
2453
|
-
'<div class="resume-err" id="resume-err" data-testid="resume-err" hidden></div>';
|
|
2454
|
-
var resumeSignBtn = box.querySelector('[data-testid="btn-sign-resume"]');
|
|
2455
|
-
box.querySelector('[data-testid="btn-resume-cancel"]').addEventListener("click", function(){
|
|
2456
|
-
resumeMode = null;
|
|
2457
|
-
renderJob(lastJobShown);
|
|
2458
|
-
});
|
|
2459
|
-
resumeSignBtn.addEventListener("click", function(){
|
|
2460
|
-
var err = document.getElementById("resume-err");
|
|
2461
|
-
err.hidden = true;
|
|
2462
|
-
resumeSignBtn.disabled = true;
|
|
2463
|
-
var body = {budgetUsd: document.getElementById("resume-budget").value};
|
|
2464
|
-
var wall = document.getElementById("resume-wall");
|
|
2465
|
-
if(wall){ body.maxWallMin = wall.value; }
|
|
2466
|
-
return authorPost("/api/runs/" + encodeURIComponent(m.runid) + "/resume", body).then(function(res){
|
|
2467
|
-
if(res.body && res.body.ok){
|
|
2468
|
-
// the SAME run goes on: back to its Run tab, which follows it until it is live again
|
|
2469
|
-
resumeMode = null;
|
|
2470
|
-
renderJob(lastJobShown);
|
|
2471
|
-
document.getElementById("tab-run").click();
|
|
2472
|
-
afterResumeRefresh(m.runid, 8);
|
|
2473
|
-
} else {
|
|
2474
|
-
resumeSignBtn.disabled = false;
|
|
2475
|
-
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
2476
|
-
err.hidden = false;
|
|
2477
|
-
}
|
|
2478
|
-
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
2479
|
-
});
|
|
2491
|
+
if(resBtn) resBtn.addEventListener("click", function(){ resumeInChat(detail); });
|
|
2480
2492
|
}
|
|
2481
2493
|
// After a resume starts, the open run's own page follows it: re-select once the run reads live again (the spine's
|
|
2482
2494
|
// new leg appears a moment after the engine starts), a few tries, never forever.
|
|
@@ -2570,7 +2582,7 @@
|
|
|
2570
2582
|
" · " + wallPhrase;
|
|
2571
2583
|
|
|
2572
2584
|
var box = document.getElementById("run-summary");
|
|
2573
|
-
box.className = "summary-box " + (detail.died ? "magenta" : (detail.glyph === "✗" ? "red" : (detail.glyph === "▶" ? "amber" : "")));
|
|
2585
|
+
box.className = "summary-box " + (detail.died ? "magenta" : ((detail.glyph === "✗" || detail.glyph === "■") ? "red" : (detail.glyph === "▶" ? "amber" : "")));
|
|
2574
2586
|
// F196: a synthetic placeholder box (e.g. "died during planning") is a
|
|
2575
2587
|
// LABEL, not a real step — excluded from this count so a run that died
|
|
2576
2588
|
// before any step started reads "0 of 0 done" / "none started", never
|
|
@@ -2649,8 +2661,8 @@
|
|
|
2649
2661
|
|
|
2650
2662
|
// ONE spelling of a step's state: its dot + the bold state word — read by the Run tab's step cards AND the Audit tab's grouped rows
|
|
2651
2663
|
function stepStateHTML(box, testid){
|
|
2652
|
-
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2653
|
-
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(box
|
|
2664
|
+
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? (box.status && box.status.sign === "\u25A0" ? "stopped" : "red") : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2665
|
+
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(boxStatusWord(box)) + '</b></span>';
|
|
2654
2666
|
}
|
|
2655
2667
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
2656
2668
|
// lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
|
|
@@ -3239,7 +3251,6 @@
|
|
|
3239
3251
|
function renderJob(job){
|
|
3240
3252
|
var empty = document.getElementById("job-empty");
|
|
3241
3253
|
var card = document.getElementById("job-card-readonly");
|
|
3242
|
-
lastJobShown = job;
|
|
3243
3254
|
if(!job){ empty.hidden = false; card.hidden = true; return; }
|
|
3244
3255
|
empty.hidden = true;
|
|
3245
3256
|
card.hidden = false;
|
|
@@ -3262,7 +3273,6 @@
|
|
|
3262
3273
|
document.getElementById("job-note").textContent = job.note || "";
|
|
3263
3274
|
lastJobToolsList = Array.isArray(job.toolsList) ? job.toolsList : null;
|
|
3264
3275
|
paintOfferedRow();
|
|
3265
|
-
paintResumeMode();
|
|
3266
3276
|
}
|
|
3267
3277
|
|
|
3268
3278
|
// ---- selection + loading ----
|
|
@@ -3282,7 +3292,6 @@
|
|
|
3282
3292
|
document.querySelectorAll(siblingSelector).forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
3283
3293
|
if(rowEl){ rowEl.classList.add("selected"); rowEl.setAttribute("aria-pressed","true"); }
|
|
3284
3294
|
currentRunid = runid;
|
|
3285
|
-
resumeMode = null;
|
|
3286
3295
|
lastLegDividers = [];
|
|
3287
3296
|
lastJobToolsList = null;
|
|
3288
3297
|
lastRunDetailSig = null;
|
|
@@ -3569,7 +3578,7 @@
|
|
|
3569
3578
|
+ '<td data-testid="pv-tokens-' + e + '">' + escapeXml(tokensText(row.tokens)) + "</td>"
|
|
3570
3579
|
+ "<td>" + bal + "</td>"
|
|
3571
3580
|
+ '<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>';
|
|
3581
|
+
+ '<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></tr>';
|
|
3573
3582
|
}).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>';
|
|
3574
3583
|
pvRows.querySelectorAll(".pv-balance-fetch").forEach(function(fetchEl){
|
|
3575
3584
|
authorGet("/api/settings/providers/balance?env=" + encodeURIComponent(fetchEl.getAttribute("data-env"))).then(function(b){
|
|
@@ -3584,15 +3593,15 @@
|
|
|
3584
3593
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
3585
3594
|
}).catch(function(){ pvNote.textContent = "could not read providers"; });
|
|
3586
3595
|
}
|
|
3587
|
-
// The row's POST body
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3596
|
+
// The row's POST body, or null when exactly ONE price box is filled: a price is both boxes or neither (the server
|
|
3597
|
+
// refuses a half-set one), so nothing in that row saves. This runs only when a save would happen (the row's
|
|
3598
|
+
// change event), so typing the first box never flashes the message; it shows when the person leaves that box.
|
|
3599
|
+
var PV_HALF_PRICE = "Fill both price boxes or clear both.";
|
|
3590
3600
|
function pvRowBody(env, v){
|
|
3591
3601
|
var pin = String(v.priceIn).trim();
|
|
3592
3602
|
var pout = String(v.priceOut).trim();
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
return body;
|
|
3603
|
+
if((pin === "") !== (pout === "")) return null;
|
|
3604
|
+
return { envName: env, name: v.name, shape: v.shape, baseUrl: v.baseUrl, priceInPerM: pin, priceOutPerM: pout };
|
|
3596
3605
|
}
|
|
3597
3606
|
pvRows.addEventListener("change", function(e){
|
|
3598
3607
|
var t = e.target;
|
|
@@ -3606,13 +3615,15 @@
|
|
|
3606
3615
|
}
|
|
3607
3616
|
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;
|
|
3608
3617
|
var env = tr.getAttribute("data-env");
|
|
3609
|
-
|
|
3618
|
+
var rowBody = pvRowBody(env, {
|
|
3610
3619
|
name: tr.querySelector(".pv-name").value,
|
|
3611
3620
|
shape: tr.querySelector(".pv-shape").value,
|
|
3612
3621
|
baseUrl: tr.querySelector(".pv-url").value,
|
|
3613
3622
|
priceIn: tr.querySelector(".pv-price-in").value,
|
|
3614
3623
|
priceOut: tr.querySelector(".pv-price-out").value,
|
|
3615
|
-
})
|
|
3624
|
+
});
|
|
3625
|
+
if(!rowBody){ pvNote.textContent = PV_HALF_PRICE; return; }
|
|
3626
|
+
authorPost("/api/settings/providers/row", rowBody).then(function(r){
|
|
3616
3627
|
pvNote.textContent = r.body && r.body.ok ? ("Saved — " + env + ".") : ((r.body && r.body.error) || "could not save");
|
|
3617
3628
|
// no re-render: it would steal focus mid-edit; Chat's Model menu just re-reads
|
|
3618
3629
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
@@ -3692,41 +3703,161 @@
|
|
|
3692
3703
|
// Reuse workflow — set when the card was opened by [Reuse workflow]: the run (or imported job) it came from.
|
|
3693
3704
|
// Only Source, Destination and the two caps are open on that card; the SERVER refuses any other change.
|
|
3694
3705
|
var startFrom = null;
|
|
3706
|
+
// the picked job's OWN check type: with the Check type radio on "Reuse workflow" the card's checkType comes from here, never from the radio
|
|
3707
|
+
var reuseCheckType = "deterministic";
|
|
3708
|
+
var reusePickedName = "";
|
|
3695
3709
|
var reuseSession = false;
|
|
3696
3710
|
var autoSigned = false;
|
|
3697
|
-
var sfOrigin = document.getElementById("chat-startfrom-origin");
|
|
3698
3711
|
var sfLine = document.getElementById("chat-startfrom-line");
|
|
3699
3712
|
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3713
|
+
var originLine = document.getElementById("chat-origin-line");
|
|
3700
3714
|
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3701
3715
|
var LOCKED_IDS = ["jf-name", "jf-goal", "jf-success", "jf-guardrails", "jf-judge"];
|
|
3702
3716
|
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3703
3717
|
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3704
3718
|
var OPEN_IDS = ["jf-source", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3705
3719
|
var reuseOn = false;
|
|
3720
|
+
// Resume mode (hamr 2026-10-07): the card shows the signed job of one run, every box locked except the two caps.
|
|
3721
|
+
// `resumeRun` = {runid, noTime} while it is on, else null. Never set together with a drafting session or a reuse pick.
|
|
3722
|
+
var resumeRun = null;
|
|
3723
|
+
var CAP_IDS = ["jf-cap-money", "jf-cap-time"];
|
|
3706
3724
|
function syncCardLock(){
|
|
3707
3725
|
var live = sessionLive;
|
|
3726
|
+
var resuming = resumeRun !== null;
|
|
3708
3727
|
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3709
|
-
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live); });
|
|
3710
|
-
OPEN_IDS.forEach(function(id){ setLock(id, live); });
|
|
3711
|
-
document.getElementById("jf-model").disabled = live;
|
|
3712
|
-
|
|
3728
|
+
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live || resuming); });
|
|
3729
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1)); });
|
|
3730
|
+
document.getElementById("jf-model").disabled = live || resuming;
|
|
3731
|
+
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3732
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3713
3733
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3714
|
-
if(!reuseOn && !live){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3734
|
+
if(!reuseOn && !live && !resuming){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3715
3735
|
}
|
|
3716
3736
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3717
|
-
function clearStartFrom(){
|
|
3718
|
-
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes
|
|
3719
|
-
|
|
3737
|
+
function clearStartFrom(keepBoxes){
|
|
3738
|
+
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes — except when a Check type
|
|
3739
|
+
// radio click leaves reuse (keepBoxes): a radio never blanks what the person has on the card
|
|
3740
|
+
if(startFrom && !keepBoxes){
|
|
3720
3741
|
["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"].forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3721
3742
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3722
3743
|
}
|
|
3723
|
-
startFrom = null; reuseSession = false; autoSigned = false;
|
|
3724
|
-
|
|
3744
|
+
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
3745
|
+
sfLine.hidden = true; sfNote.hidden = true; originLine.hidden = true;
|
|
3725
3746
|
setReuseLocked(false);
|
|
3747
|
+
renderPicker();
|
|
3726
3748
|
renderMain();
|
|
3727
3749
|
}
|
|
3750
|
+
// ---- Reuse workflow picker (hamr 2026-10-06): Check type is ONE radio group of three; "Reuse workflow" shows a search
|
|
3751
|
+
// box over the jobs that have a green local run (GET /api/author/reuse-jobs), a pick fills the card through the SAME
|
|
3752
|
+
// path the Run tab's button uses (reuseWorkflow -> bareloop-reuse -> applyReuse), then a picked box with `change`.
|
|
3753
|
+
var CARD_BOX_IDS = ["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"];
|
|
3754
|
+
var rwBox = document.getElementById("rw");
|
|
3755
|
+
var rwSearch = document.getElementById("rw-search");
|
|
3756
|
+
var rwQ = document.getElementById("rw-q");
|
|
3757
|
+
var rwList = document.getElementById("rw-list");
|
|
3758
|
+
var rwPicked = document.getElementById("rw-picked");
|
|
3759
|
+
var rwPickedTxt = document.getElementById("rw-picked-txt");
|
|
3760
|
+
var rwJobs = null;
|
|
3761
|
+
var rwFailed = false;
|
|
3762
|
+
var rwShown = [];
|
|
3763
|
+
var rwHl = 0;
|
|
3764
|
+
function verdictValue(){
|
|
3765
|
+
var ck = document.querySelector('input[name="jf-verdict"]:checked');
|
|
3766
|
+
return ck ? ck.value : "deterministic";
|
|
3767
|
+
}
|
|
3768
|
+
function setVerdict(v){
|
|
3769
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === v); });
|
|
3770
|
+
}
|
|
3771
|
+
function clearCardBoxes(){
|
|
3772
|
+
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3773
|
+
document.getElementById("jf-cap-note").textContent = "";
|
|
3774
|
+
}
|
|
3775
|
+
// ONE function shows/hides the picker: visible only while the radio is on Reuse workflow; the search box before a
|
|
3776
|
+
// pick, the picked box (+ the estimate line under it) after. A re-attached reuse session shows as picked too.
|
|
3777
|
+
function renderPicker(){
|
|
3778
|
+
var on = verdictValue() === "reuse";
|
|
3779
|
+
var picked = on && (startFrom !== null || reuseSession);
|
|
3780
|
+
rwBox.hidden = !on;
|
|
3781
|
+
rwSearch.hidden = picked;
|
|
3782
|
+
rwPicked.hidden = !picked;
|
|
3783
|
+
rwPickedTxt.innerHTML = picked ? "<b>" + escapeXml(reusePickedName) + "</b>" : "";
|
|
3784
|
+
if(!picked) sfLine.hidden = true;
|
|
3785
|
+
}
|
|
3786
|
+
function rwMark(name, q){
|
|
3787
|
+
var i = q ? name.toLowerCase().indexOf(q.toLowerCase()) : -1;
|
|
3788
|
+
if(i < 0) return escapeXml(name);
|
|
3789
|
+
return escapeXml(name.slice(0, i)) + "<mark>" + escapeXml(name.slice(i, i + q.length)) + "</mark>" + escapeXml(name.slice(i + q.length));
|
|
3790
|
+
}
|
|
3791
|
+
// the server's estimate line, prefixed with the job's check type (the line itself does not carry it)
|
|
3792
|
+
function rwMeta(j){ return j.checkType + " · " + String(j.line || "").replace(/^Same job — /, ""); }
|
|
3793
|
+
function renderReuseList(){
|
|
3794
|
+
var q = rwQ.value.trim();
|
|
3795
|
+
var all = rwJobs || [];
|
|
3796
|
+
var loading = rwJobs === null && !rwFailed;
|
|
3797
|
+
rwShown = all.filter(function(j){ return j.job.toLowerCase().indexOf(q.toLowerCase()) !== -1; });
|
|
3798
|
+
if(rwHl >= rwShown.length) rwHl = 0;
|
|
3799
|
+
if(rwShown.length){
|
|
3800
|
+
rwList.innerHTML = rwShown.map(function(j, i){
|
|
3801
|
+
return '<li data-i="' + i + '" data-testid="rw-item" class="' + (i === rwHl ? "hl" : "") + '"><div class="rw-name">' + rwMark(j.job, q) + '</div><div class="rw-meta">' + escapeXml(rwMeta(j)) + "</div></li>";
|
|
3802
|
+
}).join("");
|
|
3803
|
+
} else {
|
|
3804
|
+
rwList.innerHTML = '<div class="rw-empty" data-testid="rw-empty">' + (rwFailed ? "Could not load the job list."
|
|
3805
|
+
: loading ? "Loading\u2026" : (all.length === 0 ? "No green jobs yet." : 'No green job matches "' + escapeXml(q) + '".')) + "</div>";
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
function loadReuseJobs(){
|
|
3809
|
+
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?model=" + encodeURIComponent(modelSelect.value) : "")).then(function(r){
|
|
3810
|
+
rwFailed = !(r && r.ok);
|
|
3811
|
+
rwJobs = rwFailed ? null : r.jobs;
|
|
3812
|
+
renderReuseList();
|
|
3813
|
+
}).catch(function(){ rwFailed = true; rwJobs = null; renderReuseList(); });
|
|
3814
|
+
}
|
|
3815
|
+
// the empty search state: card emptied, nothing picked, Sign & run disabled until a job is picked
|
|
3816
|
+
// (the boxes are NOT cleared here: clicking the Reuse radio never blanks typing; only a pick fills the card)
|
|
3817
|
+
function openPicker(){
|
|
3818
|
+
rwQ.value = ""; rwHl = 0;
|
|
3819
|
+
rwJobs = null; rwFailed = false; // a fresh open shows "Loading…", never the previous list
|
|
3820
|
+
document.getElementById("jf-judge").disabled = true;
|
|
3821
|
+
renderPicker();
|
|
3822
|
+
renderReuseList();
|
|
3823
|
+
loadReuseJobs();
|
|
3824
|
+
refreshStartEnabled();
|
|
3825
|
+
rwQ.focus();
|
|
3826
|
+
}
|
|
3827
|
+
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, modelSelect.value); }
|
|
3828
|
+
rwQ.addEventListener("input", function(){ rwHl = 0; renderReuseList(); });
|
|
3829
|
+
rwQ.addEventListener("keydown", function(e){
|
|
3830
|
+
if(e.key === "ArrowDown"){ rwHl = Math.min(rwHl + 1, rwShown.length - 1); renderReuseList(); e.preventDefault(); }
|
|
3831
|
+
else if(e.key === "ArrowUp"){ rwHl = Math.max(rwHl - 1, 0); renderReuseList(); e.preventDefault(); }
|
|
3832
|
+
else if(e.key === "Enter" && rwShown[rwHl]){ e.preventDefault(); pickReuseJob(rwShown[rwHl]); }
|
|
3833
|
+
});
|
|
3834
|
+
rwList.addEventListener("click", function(e){
|
|
3835
|
+
var li = e.target.closest ? e.target.closest("li") : null;
|
|
3836
|
+
if(li) pickReuseJob(rwShown[Number(li.getAttribute("data-i"))]);
|
|
3837
|
+
});
|
|
3838
|
+
// `change` = back to the empty search state; the radio stays on Reuse workflow
|
|
3839
|
+
function unpickReuse(){
|
|
3840
|
+
clearStartFrom();
|
|
3841
|
+
setVerdict("reuse");
|
|
3842
|
+
openPicker();
|
|
3843
|
+
}
|
|
3844
|
+
document.getElementById("rw-change").addEventListener("click", unpickReuse);
|
|
3845
|
+
// leaving resume mode: the title, the hidden drafting chrome, the time box and the resume box all come back
|
|
3846
|
+
function exitResumeMode(){
|
|
3847
|
+
resumeRun = null;
|
|
3848
|
+
originLine.classList.remove("res");
|
|
3849
|
+
document.getElementById("job-card-title").textContent = "Job card \u2014 draft";
|
|
3850
|
+
document.getElementById("chat-resume-box").hidden = true;
|
|
3851
|
+
document.getElementById("chat-resume-err").hidden = true;
|
|
3852
|
+
document.getElementById("jf-cap-time").parentNode.hidden = false;
|
|
3853
|
+
document.getElementById("chat-clear-btn").hidden = false;
|
|
3854
|
+
["chat-main-btn", "chat-main-hint"].forEach(function(id){ document.getElementById(id).hidden = false; });
|
|
3855
|
+
document.querySelector(".chat-input-row").hidden = false;
|
|
3856
|
+
}
|
|
3728
3857
|
function openNewCard(){
|
|
3729
3858
|
card.hidden = false;
|
|
3859
|
+
originLine.hidden = true;
|
|
3860
|
+
exitResumeMode();
|
|
3730
3861
|
thread.innerHTML = "";
|
|
3731
3862
|
errEl.textContent = "";
|
|
3732
3863
|
actionErrEl.textContent = "";
|
|
@@ -3755,12 +3886,12 @@
|
|
|
3755
3886
|
// back to a fresh empty card: every box, check type deterministic, the default (first) Model, origin/estimate/notes
|
|
3756
3887
|
// hidden, progress list and thread cleared, reuse lock lifted
|
|
3757
3888
|
function resetCard(){
|
|
3758
|
-
|
|
3759
|
-
|
|
3889
|
+
clearCardBoxes();
|
|
3890
|
+
setVerdict("deterministic");
|
|
3760
3891
|
document.getElementById("jf-judge").disabled = true;
|
|
3761
3892
|
modelSelect.selectedIndex = 0;
|
|
3762
3893
|
msgInput.value = "";
|
|
3763
|
-
|
|
3894
|
+
rwQ.value = "";
|
|
3764
3895
|
lastPhase = null;
|
|
3765
3896
|
clearStartFrom();
|
|
3766
3897
|
openNewCard();
|
|
@@ -3782,7 +3913,8 @@
|
|
|
3782
3913
|
var moneyCap = Number(document.getElementById("jf-cap-money").value);
|
|
3783
3914
|
var timeCapMin = document.getElementById("jf-cap-time").value.trim();
|
|
3784
3915
|
return {
|
|
3785
|
-
|
|
3916
|
+
// on "Reuse workflow" the type is the picked job's own, never the radio's
|
|
3917
|
+
checkType: (verdictValue() === "reuse" ? reuseCheckType : verdictValue()) === "rubric" ? "rubric" : "deterministic",
|
|
3786
3918
|
model: document.getElementById("jf-model").value,
|
|
3787
3919
|
jobName: document.getElementById("jf-name").value.trim(),
|
|
3788
3920
|
goal: document.getElementById("jf-goal").value.trim(),
|
|
@@ -3806,7 +3938,8 @@
|
|
|
3806
3938
|
function refreshStartEnabled(){
|
|
3807
3939
|
var v = Number(moneyCapInput.value);
|
|
3808
3940
|
var capOk = moneyCapInput.value.trim() !== "" && Number.isFinite(v) && v > 0;
|
|
3809
|
-
|
|
3941
|
+
// on Reuse workflow nothing starts before a job is picked
|
|
3942
|
+
startOk = capOk && !sessionLive && keyOk && (verdictValue() !== "reuse" || startFrom !== null);
|
|
3810
3943
|
syncCardLock();
|
|
3811
3944
|
renderMain();
|
|
3812
3945
|
refreshClearBtn();
|
|
@@ -3829,11 +3962,20 @@
|
|
|
3829
3962
|
});
|
|
3830
3963
|
// the limit changed in Settings: re-read the note for whatever cap is typed
|
|
3831
3964
|
document.addEventListener("bareloop-limit-changed", refreshCapNote);
|
|
3965
|
+
// Check type: Deterministic / Rubric / Reuse workflow. Reuse opens the picker on an empty card; Deterministic or
|
|
3966
|
+
// Rubric while the picker is showing (picked or not) LEAVES reuse — boxes untouched (clearStartFrom(true)).
|
|
3967
|
+
function onVerdictChange(v){
|
|
3968
|
+
if(v === "reuse"){ if(rwBox.hidden) openPicker(); return; }
|
|
3969
|
+
if(!rwBox.hidden){
|
|
3970
|
+
clearStartFrom(true);
|
|
3971
|
+
setVerdict(v);
|
|
3972
|
+
renderPicker();
|
|
3973
|
+
}
|
|
3974
|
+
document.getElementById("jf-judge").disabled = v !== "rubric";
|
|
3975
|
+
refreshStartEnabled();
|
|
3976
|
+
}
|
|
3832
3977
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){
|
|
3833
|
-
r.addEventListener("change", function(){
|
|
3834
|
-
var isRubric = document.querySelector('input[name="jf-verdict"]:checked').value === "rubric";
|
|
3835
|
-
document.getElementById("jf-judge").disabled = !isRubric;
|
|
3836
|
-
});
|
|
3978
|
+
r.addEventListener("change", function(){ onVerdictChange(r.value); });
|
|
3837
3979
|
});
|
|
3838
3980
|
|
|
3839
3981
|
// build item 7 / build item 5 (2026-09-28) — key + reachability, $0,
|
|
@@ -3906,10 +4048,11 @@
|
|
|
3906
4048
|
var q = (startFrom.importId ? "import=" + encodeURIComponent(startFrom.importId) : "runid=" + encodeURIComponent(startFrom.runid)) +
|
|
3907
4049
|
"&model=" + encodeURIComponent(modelSelect.value);
|
|
3908
4050
|
authorGet("/api/author/start-from?" + q).then(function(r){
|
|
3909
|
-
if(startFrom && r && r.ok){ sfLine.textContent = r.line
|
|
4051
|
+
if(startFrom && r && r.ok){ sfLine.textContent = r.line ? rwMeta({checkType: reuseCheckType, line: r.line}) : ""; }
|
|
3910
4052
|
}).catch(function(){ /* the line keeps what it had */ });
|
|
3911
4053
|
}
|
|
3912
4054
|
modelSelect.addEventListener("change", refreshReuseLine);
|
|
4055
|
+
modelSelect.addEventListener("change", function(){ if(!rwBox.hidden && startFrom === null && !reuseSession) loadReuseJobs(); });
|
|
3913
4056
|
// P4b — the Model menu IS the Settings rows: every Name, in keys-file order (a blank Name
|
|
3914
4057
|
// is never offered). Re-read whenever Settings loads or saves, keeping the pick if it survives.
|
|
3915
4058
|
function loadModels(){
|
|
@@ -4028,7 +4171,7 @@
|
|
|
4028
4171
|
var open = askBoxOpenFor(st);
|
|
4029
4172
|
msgInput.disabled = !open;
|
|
4030
4173
|
if(!open) msgInput.value = "";
|
|
4031
|
-
var b = mainButtonFor(st, !msgInput.value.trim(), !!startFrom);
|
|
4174
|
+
var b = mainButtonFor(st, !msgInput.value.trim(), !!startFrom || verdictValue() === "reuse");
|
|
4032
4175
|
mainBtn.textContent = b.text;
|
|
4033
4176
|
mainBtn.disabled = b.disabled;
|
|
4034
4177
|
mainAction = b.action;
|
|
@@ -4148,7 +4291,9 @@
|
|
|
4148
4291
|
reuseSession = st.reuse === true;
|
|
4149
4292
|
autoSigned = false;
|
|
4150
4293
|
signClickedOnce = reuseSession;
|
|
4151
|
-
|
|
4294
|
+
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4295
|
+
reusePickedName = c.jobName || "job";
|
|
4296
|
+
setVerdict(reuseSession ? "reuse" : reuseCheckType);
|
|
4152
4297
|
if(c.model){
|
|
4153
4298
|
if(!Array.prototype.some.call(modelSelect.options, function(o){ return o.value === c.model; })){
|
|
4154
4299
|
modelSelect.insertAdjacentHTML("beforeend", '<option value="' + escapeXml(c.model) + '">' + escapeXml(c.model) + "</option>");
|
|
@@ -4161,6 +4306,7 @@
|
|
|
4161
4306
|
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4162
4307
|
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4163
4308
|
if(reuseSession) setReuseLocked(true);
|
|
4309
|
+
renderPicker();
|
|
4164
4310
|
fitCardBoxes();
|
|
4165
4311
|
renderMessages(st);
|
|
4166
4312
|
renderActions(st);
|
|
@@ -4169,7 +4315,7 @@
|
|
|
4169
4315
|
poll();
|
|
4170
4316
|
}
|
|
4171
4317
|
function reattachLive(){
|
|
4172
|
-
if(sessionId || sessionLive) return;
|
|
4318
|
+
if(sessionId || sessionLive || resumeRun !== null) return;
|
|
4173
4319
|
authorGet("/api/author/live").then(function(j){
|
|
4174
4320
|
if(sessionId || sessionLive || !j || !j.ok || !j.sessionId || !j.state) return;
|
|
4175
4321
|
attachSession(j);
|
|
@@ -4180,15 +4326,17 @@
|
|
|
4180
4326
|
|
|
4181
4327
|
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4182
4328
|
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4183
|
-
|
|
4184
|
-
|
|
4329
|
+
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
4330
|
+
// `bareloop-reuse` event), so the card ends up in the same picked state whichever door was used.
|
|
4331
|
+
function applyReuse(d){
|
|
4185
4332
|
var pre = d.prefill || {};
|
|
4186
4333
|
var c = pre.card || {};
|
|
4187
4334
|
clearStartFrom();
|
|
4188
4335
|
openNewCard();
|
|
4189
4336
|
startFrom = d.importId ? {importId: d.importId} : {runid: d.runid};
|
|
4190
|
-
|
|
4191
|
-
|
|
4337
|
+
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4338
|
+
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4339
|
+
setVerdict("reuse");
|
|
4192
4340
|
if(c.model){ modelSelect.value = c.model; }
|
|
4193
4341
|
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4194
4342
|
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
@@ -4196,10 +4344,9 @@
|
|
|
4196
4344
|
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4197
4345
|
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4198
4346
|
setReuseLocked(true);
|
|
4199
|
-
|
|
4200
|
-
sfOrigin.textContent = "Reuse: " + ((pre.origin && pre.origin.job) || "job") + (d.runid ? " (run " + d.runid + ")" : " (imported)");
|
|
4347
|
+
renderPicker();
|
|
4201
4348
|
sfLine.hidden = false;
|
|
4202
|
-
sfLine.textContent = pre.line
|
|
4349
|
+
sfLine.textContent = pre.line ? rwMeta({checkType: reuseCheckType, line: pre.line}) : "";
|
|
4203
4350
|
sfLine.className = "startfrom-line";
|
|
4204
4351
|
sfNote.hidden = !pre.note;
|
|
4205
4352
|
sfNote.textContent = pre.note || "";
|
|
@@ -4207,7 +4354,92 @@
|
|
|
4207
4354
|
refreshReuseLine();
|
|
4208
4355
|
refreshCapNote();
|
|
4209
4356
|
refreshStartEnabled();
|
|
4210
|
-
}
|
|
4357
|
+
}
|
|
4358
|
+
document.addEventListener("bareloop-reuse", function(e){ applyReuse(e.detail || {}); });
|
|
4359
|
+
|
|
4360
|
+
// Edit in chat — the card opens filled from the SIGNED job with EVERY box open; the button stays the normal
|
|
4361
|
+
// [Start drafting] (startFrom / reuse state are never set: no borrowed track record). A live drafting session is
|
|
4362
|
+
// handled exactly as applyReuse does (clearStartFrom + openNewCard). A refused prefill opens nothing, error only.
|
|
4363
|
+
function applyEdit(d){
|
|
4364
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
4365
|
+
var c = (d.prefill || {}).card || {};
|
|
4366
|
+
clearStartFrom();
|
|
4367
|
+
openNewCard();
|
|
4368
|
+
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4369
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4370
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4371
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4372
|
+
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4373
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4374
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4375
|
+
originLine.textContent = d.origin || "";
|
|
4376
|
+
originLine.hidden = !d.origin;
|
|
4377
|
+
renderPicker();
|
|
4378
|
+
refreshModelStatus();
|
|
4379
|
+
refreshCapNote();
|
|
4380
|
+
refreshStartEnabled();
|
|
4381
|
+
}
|
|
4382
|
+
document.addEventListener("bareloop-edit", function(e){ applyEdit(e.detail || {}); });
|
|
4383
|
+
|
|
4384
|
+
// Resume — the card in RESUME mode: the signed job filled and locked, only Cap $ and Time cap open (prefilled from the
|
|
4385
|
+
// engine's own resume plan, the values the Job tab used to show). A live drafting session is handled exactly as
|
|
4386
|
+
// applyReuse does. A refused prefill opens nothing, error only.
|
|
4387
|
+
function applyResume(d){
|
|
4388
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
4389
|
+
var c = (d.prefill || {}).card || {};
|
|
4390
|
+
var r = d.resume || {};
|
|
4391
|
+
clearStartFrom();
|
|
4392
|
+
openNewCard();
|
|
4393
|
+
resumeRun = {runid: d.runid, noTime: typeof r.maxWallMin !== "number"};
|
|
4394
|
+
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4395
|
+
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4396
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4397
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4398
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4399
|
+
setVal("jf-judge", c.judgeExamples);
|
|
4400
|
+
setVal("jf-cap-money", r.budgetUsd);
|
|
4401
|
+
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
4402
|
+
document.getElementById("jf-cap-time").parentNode.hidden = resumeRun.noTime;
|
|
4403
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4404
|
+
originLine.textContent = "The same run goes on, not a new one. Only the money cap and the time cap can change; everything else is the signed job.";
|
|
4405
|
+
originLine.classList.add("res");
|
|
4406
|
+
originLine.hidden = false;
|
|
4407
|
+
var spent = (r.spentUsd === null || r.spentUsd === undefined) ? "unknown" : ((r.spendComplete ? "" : "at least ") + panelMoney(r.spentUsd));
|
|
4408
|
+
document.getElementById("chat-resume-spent").textContent = "spent so far " + spent +
|
|
4409
|
+
(typeof r.wallUsedMs === "number" && isFinite(r.wallUsedMs) ? " \u00B7 time used so far " + Math.ceil(r.wallUsedMs / 60000) + " min" : "");
|
|
4410
|
+
document.getElementById("chat-resume-box").hidden = false;
|
|
4411
|
+
document.getElementById("chat-clear-btn").hidden = true;
|
|
4412
|
+
["chat-main-btn", "chat-main-hint"].forEach(function(id){ document.getElementById(id).hidden = true; });
|
|
4413
|
+
document.querySelector(".chat-input-row").hidden = true;
|
|
4414
|
+
renderPicker();
|
|
4415
|
+
refreshModelStatus();
|
|
4416
|
+
refreshStartEnabled();
|
|
4417
|
+
}
|
|
4418
|
+
document.addEventListener("bareloop-resume", function(e){ applyResume(e.detail || {}); });
|
|
4419
|
+
document.getElementById("chat-resume-cancel").addEventListener("click", function(){ resetCard(); });
|
|
4420
|
+
var resumeSignBtn = document.getElementById("chat-resume-sign");
|
|
4421
|
+
function signResume(){
|
|
4422
|
+
var run = resumeRun;
|
|
4423
|
+
if(run === null) return;
|
|
4424
|
+
var err = document.getElementById("chat-resume-err");
|
|
4425
|
+
err.hidden = true;
|
|
4426
|
+
resumeSignBtn.disabled = true;
|
|
4427
|
+
var body = {budgetUsd: document.getElementById("jf-cap-money").value};
|
|
4428
|
+
if(!run.noTime){ body.maxWallMin = document.getElementById("jf-cap-time").value; }
|
|
4429
|
+
return authorPost("/api/runs/" + encodeURIComponent(run.runid) + "/resume", body).then(function(res){
|
|
4430
|
+
resumeSignBtn.disabled = false;
|
|
4431
|
+
if(res.body && res.body.ok){
|
|
4432
|
+
// the SAME run goes on: the card goes back to empty, the right side to its Run tab, which follows it until it is live again
|
|
4433
|
+
resetCard();
|
|
4434
|
+
document.getElementById("tab-run").click();
|
|
4435
|
+
afterResumeRefresh(run.runid, 8);
|
|
4436
|
+
} else {
|
|
4437
|
+
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
4438
|
+
err.hidden = false;
|
|
4439
|
+
}
|
|
4440
|
+
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
4441
|
+
}
|
|
4442
|
+
resumeSignBtn.addEventListener("click", signResume);
|
|
4211
4443
|
|
|
4212
4444
|
// build item 1 — the "lost answer" fix. ROOT CAUSE: `renderActions`
|
|
4213
4445
|
// (fed by the every-2s `poll()`) unconditionally overwrites
|