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.
@@ -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:2px 0 8px;font-size:12px;color:var(--accent);overflow-wrap:anywhere;}
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
- .job-card.reuse .radio-row label{opacity:.55;cursor:not-allowed;}
278
- .startfrom-origin{font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 4px;padding-right:112px;overflow-wrap:anywhere;}
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 &amp; 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 id="details-cap-money-label">$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
709
- <div class="field"><label id="details-cap-time-label">Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
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.state) + '</text>';
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
- childRuns.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
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
- if(rs.length && rs.indexOf(r.glyph) === -1) return false;
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 (hamr 2026-10-02): Resume opens the run's OWN Job tab. There ONLY the money cap and the time cap are
2338
- // editable; every other field is the signed job, shown read-only exactly as the tab already shows it. The click
2339
- // on [Sign & resume] is the signature when a cap changed. A resume is the SAME run: no new card, no new job.
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 (the SAME button as the Ended block's, same Job-tab resume mode).
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
- function reuseWorkflow(runid){
2364
- authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid)).then(function(r){
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(){ openResumeOnJobTab(detail); });
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) + ' &mdash; 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) ? ' &middot; 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 &amp; 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
  " &middot; " + 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.state) + '</b></span>';
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. 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.
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
- 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;
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
- authorPost("/api/settings/providers/row", pvRowBody(env, {
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
- })).then(function(r){
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
- document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = reuseOn || live; });
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
- if(startFrom){
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
- sfOrigin.hidden = true; sfLine.hidden = true; sfNote.hidden = true;
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
- ["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 = ""; });
3759
- document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
3889
+ clearCardBoxes();
3890
+ setVerdict("deterministic");
3760
3891
  document.getElementById("jf-judge").disabled = true;
3761
3892
  modelSelect.selectedIndex = 0;
3762
3893
  msgInput.value = "";
3763
- document.getElementById("jf-cap-note").textContent = "";
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
- checkType: document.querySelector('input[name="jf-verdict"]:checked').value === "rubric" ? "rubric" : "deterministic",
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
- startOk = capOk && !sessionLive && keyOk;
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
- document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === (c.checkType === "rubric" ? "rubric" : "deterministic")); });
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
- document.addEventListener("bareloop-reuse", function(e){
4184
- var d = e.detail || {};
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
- var radios = document.querySelectorAll('input[name="jf-verdict"]');
4191
- radios.forEach(function(r){ r.checked = (r.value === (c.checkType === "rubric" ? "rubric" : "deterministic")); });
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
- sfOrigin.hidden = false;
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