bareloop 0.37.0 → 0.39.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 +53 -0
- package/bareloop.context.md +48 -26
- package/package.json +3 -3
- package/src/authorflow.js +3 -2
- package/src/authoring.js +62 -3
- package/src/authorjob.js +37 -24
- package/src/authorreadout.js +449 -2
- package/src/authorrun.js +21 -4
- package/src/calibrate.js +84 -10
- package/src/cardauthor.js +4 -1
- package/src/declaredclose.js +10 -4
- package/src/job.js +1 -1
- package/src/judged.js +322 -28
- package/src/kinds.js +13 -1
- package/src/legs.js +15 -0
- package/src/panel/authorroutes.js +9 -6
- package/src/panel/authorsession.js +66 -49
- package/src/panel/index.html +321 -355
- package/src/panel/server.js +33 -13
- package/src/panel/status.js +5 -4
- package/src/planrun.js +4 -3
- package/src/providers.js +27 -0
- package/src/ralph.js +8 -2
- package/src/replay.js +23 -3
- package/src/trend.js +1 -0
- package/types/authoring.d.ts +24 -1
- package/types/authorjob.d.ts +17 -10
- package/types/authorreadout.d.ts +106 -0
- package/types/calibrate.d.ts +4 -1
- package/types/declaredclose.d.ts +2 -1
- package/types/judged.d.ts +21 -2
- package/types/legs.d.ts +12 -0
- package/types/panel/authorsession.d.ts +15 -1
- package/types/panel/server.d.ts +5 -4
- package/types/panel/status.d.ts +1 -1
- package/types/providers.d.ts +7 -0
- package/types/ralph.d.ts +6 -1
- package/types/trend.d.ts +1 -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
|
|
@@ -295,10 +297,6 @@
|
|
|
295
297
|
.wf-resumed{color:var(--amber);}
|
|
296
298
|
.audit-leg-divider{text-align:center;color:var(--text-dim);font-size:12px;margin:10px 0;overflow-wrap:anywhere;}
|
|
297
299
|
.audit-leg-divider-row td{text-align:center;color:var(--text-dim);font-size:12px;background:var(--panel2);}
|
|
298
|
-
/* Resume opens the run's own Job tab: only the two caps turn into inputs, everything else stays read-only */
|
|
299
|
-
.resume-job{border:1px dashed var(--border-strong);margin-top:12px;padding:10px;display:grid;gap:8px;}
|
|
300
|
-
.resume-cap{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
|
|
301
|
-
.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;}
|
|
302
300
|
.summary-box.red{border-color:var(--red);}
|
|
303
301
|
.summary-box.amber{border-color:var(--amber);}
|
|
304
302
|
.summary-box.magenta{border-color:var(--magenta);}
|
|
@@ -313,9 +311,28 @@
|
|
|
313
311
|
.imported-box .summary-row{flex-wrap:wrap;}
|
|
314
312
|
.imported-box .sum-val{flex:1 1 180px;min-width:0;overflow-wrap:anywhere;}
|
|
315
313
|
|
|
316
|
-
.map-box{border:1px solid var(--border-strong);padding:10px;
|
|
314
|
+
.map-box{border:1px solid var(--border-strong);padding:10px;margin-top:16px;margin-bottom:14px;background:var(--panel2);}
|
|
317
315
|
.map-box::before{content:"┤ MAP ├";display:block;font-size:11px;color:var(--text-faint);letter-spacing:0.08em;margin:-2px 0 8px 0;}
|
|
318
|
-
|
|
316
|
+
/* ruling 1A: chips as wide as their own text, side by side, wrapping — a name is never truncated */
|
|
317
|
+
.map-chips{display:flex;flex-wrap:wrap;gap:8px 4px;align-items:flex-start;padding-left:1.4em;}
|
|
318
|
+
/* ruling A: a step + its trailing arrow are ONE unit (the arrow never wraps alone); the lead arrow of a wrapped line
|
|
319
|
+
hangs in the container's left padding (absolute, zero layout impact — toggling it can never move a chip) */
|
|
320
|
+
.map-item{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;}
|
|
321
|
+
.map-arrow{color:var(--text-dim);font-size:13px;flex:none;}
|
|
322
|
+
.map-lead{display:none;position:absolute;left:-1.3em;top:50%;transform:translateY(-50%);color:var(--text-dim);font-size:13px;}
|
|
323
|
+
.map-item.line-start .map-lead{display:block;}
|
|
324
|
+
.chip-retry{color:var(--text-dim);font-size:11px;}
|
|
325
|
+
.map-chip{display:inline-block;width:auto;max-width:100%;min-width:0;box-sizing:border-box;font:inherit;font-size:12px;line-height:1.4;text-align:left;padding:4px 8px;border:2px solid var(--border-strong);border-radius:0;background:var(--panel2);color:var(--text-faint);white-space:normal;overflow-wrap:anywhere;}
|
|
326
|
+
button.map-chip{cursor:pointer;}
|
|
327
|
+
.map-chip.s-done{border-color:var(--green);color:var(--text);} .map-chip.s-done .chip-sign{color:#2e7d32;}
|
|
328
|
+
.map-chip.s-running{border-color:var(--amber);color:var(--text);} .map-chip.s-running .chip-sign{color:#b8860b;}
|
|
329
|
+
.map-chip.s-stopped{border-color:var(--red);color:var(--text);} .map-chip.s-stopped .chip-sign{color:var(--red);}
|
|
330
|
+
.map-chip.s-died{border-color:var(--magenta);color:var(--text);} .map-chip.s-died .chip-sign{color:var(--magenta);}
|
|
331
|
+
.map-chip.retry{border-style:dashed;}
|
|
332
|
+
.map-chip.resumed-next{border-right-style:dotted;}
|
|
333
|
+
.map-chip .chip-blocked{color:var(--red);font-size:10px;}
|
|
334
|
+
.map-chip.s-running .chip-sign{animation:chip-pulse 1.2s infinite;}
|
|
335
|
+
@keyframes chip-pulse{0%,100%{opacity:1}50%{opacity:0.3}}
|
|
319
336
|
.map-key{display:flex;gap:14px;font-size:11px;color:var(--text-dim);margin-top:6px;flex-wrap:wrap;}
|
|
320
337
|
|
|
321
338
|
.step-card{position:relative;border:1px solid var(--border);padding:10px;margin-bottom:8px;background:var(--panel2);}
|
|
@@ -374,6 +391,9 @@
|
|
|
374
391
|
.job-card{position:relative;border:1px solid var(--border-strong);padding:14px 10px 10px;background:var(--panel2);margin-top:10px;margin-bottom:10px;}
|
|
375
392
|
.job-card h4{margin:0 0 8px;font-size:13px;position:absolute;top:-10px;left:10px;background:var(--panel2);padding:0 6px;}
|
|
376
393
|
.job-card .card-clear{position:absolute;top:-13px;right:10px;padding:1px 8px;font-size:11px;line-height:1.4;background:var(--panel2);}
|
|
394
|
+
.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;}
|
|
395
|
+
.job-card .origin-line.res{border-color:var(--accent, var(--text));}
|
|
396
|
+
.job-card .chat-resume-box{display:grid;gap:8px;margin-top:10px;}
|
|
377
397
|
.job-card h4::before{content:"┤ ";color:var(--text-faint);}
|
|
378
398
|
.job-card h4::after{content:" ├";color:var(--text-faint);}
|
|
379
399
|
.field{margin-bottom:10px;}
|
|
@@ -533,6 +553,7 @@
|
|
|
533
553
|
<h4 id="job-card-title">Job card — draft</h4>
|
|
534
554
|
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
535
555
|
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
556
|
+
<div class="origin-line" id="chat-origin-line" data-testid="origin-line" hidden></div>
|
|
536
557
|
|
|
537
558
|
<div class="field">
|
|
538
559
|
<label id="jf-verdict-label">Check type</label>
|
|
@@ -598,6 +619,11 @@
|
|
|
598
619
|
</div>
|
|
599
620
|
</div>
|
|
600
621
|
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
622
|
+
<div id="chat-resume-box" class="chat-resume-box" data-testid="resume-box" hidden>
|
|
623
|
+
<div class="hint" id="chat-resume-spent" data-testid="resume-spent"></div>
|
|
624
|
+
<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>
|
|
625
|
+
<div class="resume-err" id="chat-resume-err" data-testid="resume-err" hidden></div>
|
|
626
|
+
</div>
|
|
601
627
|
</div>
|
|
602
628
|
|
|
603
629
|
<div class="chat-thread" id="chat-thread" data-testid="chat-thread"></div>
|
|
@@ -606,7 +632,7 @@
|
|
|
606
632
|
animated dots, then a check or a cross with its code-owned reason; chat turns stay in the thread above it, right over the ask box. -->
|
|
607
633
|
<ol class="chat-steps" id="chat-progress-row" data-testid="chat-steps" aria-live="polite" hidden></ol>
|
|
608
634
|
|
|
609
|
-
<!-- build item 1 — a failed/rejected send, revise, sign-prepare,
|
|
635
|
+
<!-- build item 1 — a failed/rejected send, revise (reopen), sign-prepare,
|
|
610
636
|
sign or check-deps POST shows here and STAYS shown: it is set
|
|
611
637
|
only by this tab's own action handlers below and is never
|
|
612
638
|
touched by renderActions()/poll() (which own #chat-card-error
|
|
@@ -616,10 +642,12 @@
|
|
|
616
642
|
|
|
617
643
|
<div class="chat-input-row">
|
|
618
644
|
<label for="chat-msg" style="display:none">Message</label>
|
|
619
|
-
<textarea id="chat-msg" rows="2" placeholder="
|
|
645
|
+
<textarea id="chat-msg" rows="2" placeholder="Answer here when the draft asks you a question…" data-testid="chat-msg"></textarea>
|
|
620
646
|
</div>
|
|
621
647
|
<button class="btn primary wide" type="button" id="chat-main-btn" data-testid="chat-main" disabled>Start drafting</button>
|
|
622
648
|
<div class="hint" id="chat-main-hint" data-testid="chat-main-hint"></div>
|
|
649
|
+
<!-- hamr 2026-10-09: Revise = reopen the boxes, filled as drafted, and draft again. Shown only while a plan waits for the OK. -->
|
|
650
|
+
<button class="btn wide" type="button" id="chat-reopen-btn" data-testid="chat-reopen" hidden>Revise</button>
|
|
623
651
|
<div class="hint">Only your click signs. The chat can't.</div>
|
|
624
652
|
</section>
|
|
625
653
|
|
|
@@ -723,8 +751,8 @@
|
|
|
723
751
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
724
752
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
725
753
|
<div class="field-row">
|
|
726
|
-
<div class="field"><label
|
|
727
|
-
<div class="field"><label
|
|
754
|
+
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
755
|
+
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
728
756
|
</div>
|
|
729
757
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
730
758
|
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
@@ -734,7 +762,6 @@
|
|
|
734
762
|
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
735
763
|
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
736
764
|
<div class="hint" id="job-note"></div>
|
|
737
|
-
<div id="resume-job" class="resume-job" data-testid="resume-job" hidden></div>
|
|
738
765
|
</div>
|
|
739
766
|
</section>
|
|
740
767
|
|
|
@@ -863,7 +890,6 @@
|
|
|
863
890
|
btn.setAttribute("aria-selected", "true");
|
|
864
891
|
var panel = document.getElementById(btn.getAttribute("aria-controls"));
|
|
865
892
|
if(panel){ panel.hidden = false; panel.classList.add("active"); }
|
|
866
|
-
if(btn.id === "tab-run"){ reflowMap(); }
|
|
867
893
|
});
|
|
868
894
|
});
|
|
869
895
|
}
|
|
@@ -878,33 +904,16 @@
|
|
|
878
904
|
});
|
|
879
905
|
}
|
|
880
906
|
|
|
881
|
-
// ---- step map renderer (
|
|
882
|
-
// buildStepMapSVG — ONE renderer, no second hand-authored map anywhere on this page) ----
|
|
883
|
-
function stepMapColors(state){
|
|
884
|
-
if(state === "done") return {stroke:"var(--green)", textFill:"#2e7d32"};
|
|
885
|
-
if(state === "running") return {stroke:"var(--amber)", textFill:"#b8860b"};
|
|
886
|
-
if(state === "stopped") return {stroke:"var(--red)", textFill:"var(--red)"};
|
|
887
|
-
// died (hamr's ruling B, 2026-09-25): distinct from stopped/red — no
|
|
888
|
-
// verdict was ever reached, so this box never shares the red look.
|
|
889
|
-
if(state === "died") return {stroke:"var(--magenta)", textFill:"var(--magenta)"};
|
|
890
|
-
return {stroke:"var(--border-strong)", textFill:"var(--text-faint)"};
|
|
891
|
-
}
|
|
907
|
+
// ---- step map renderer (buildStepMapHTML below — ONE renderer, no second hand-authored map anywhere on this page) ----
|
|
892
908
|
function escapeXml(s){
|
|
893
909
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
894
910
|
}
|
|
895
|
-
var stepMapUidCounter = 0;
|
|
896
|
-
var MAP_MARGIN = 10, MAP_GAP = 28;
|
|
897
|
-
var CHAR_W = 7.2;
|
|
898
|
-
var TITLE_HPAD = 16;
|
|
899
|
-
var MIN_BOX_W = 100;
|
|
900
|
-
var BOX_H = 60, BOX_H_2LINE = 78;
|
|
901
|
-
var ROW_GAP = 70;
|
|
902
911
|
// s.noNumber (F196): a synthetic placeholder box (e.g. "died during
|
|
903
912
|
// planning") is a LABEL, never a real step — it renders without the
|
|
904
913
|
// leading "<n> " step number a real step gets.
|
|
905
914
|
// item 4 (2026-09-25): each step box shows its attempts INLINE, e.g.
|
|
906
915
|
// "attempt 1 [✗] · attempt 2 [✓]" — appended to the box's own title text
|
|
907
|
-
// (the
|
|
916
|
+
// (the chip wraps long text inside itself the same way
|
|
908
917
|
// it already does for a long step id; no third renderer, no new box shape).
|
|
909
918
|
// item 1 (2026-09-27, panel P2 defect): an attempt's own outcome is null
|
|
910
919
|
// in exactly two situations — its window is still open on a LIVE run (the
|
|
@@ -929,9 +938,9 @@
|
|
|
929
938
|
if(g === "✗") return '<span class="glyph-red">' + g + '</span>';
|
|
930
939
|
return g;
|
|
931
940
|
}
|
|
932
|
-
// PLAIN TEXT ONLY — feeds the
|
|
933
|
-
//
|
|
934
|
-
//
|
|
941
|
+
// PLAIN TEXT ONLY — feeds the step map chip label (`stepTitleText`, escaped
|
|
942
|
+
// before it goes into the chip); a colour span here would render as literal
|
|
943
|
+
// "<span>" characters inside the map.
|
|
935
944
|
// {@link glyphSpan} (the P3 colour addition) is used only by the DOM
|
|
936
945
|
// part-card renderer below, never by this function.
|
|
937
946
|
function attemptsInlineText(attempts, boxIsLive){
|
|
@@ -1036,224 +1045,102 @@
|
|
|
1036
1045
|
function boxStatusText(box){
|
|
1037
1046
|
return box.status && box.status.word ? "[" + box.status.sign + "] " + boxStatusWord(box) : boxStatusWord(box);
|
|
1038
1047
|
}
|
|
1039
|
-
|
|
1048
|
+
// Ruling 1A (hamr, 2026-10-09): the step map is a row of CHIPS — each box as wide as its own text, flowing
|
|
1049
|
+
// side by side and wrapping to the next line. Plain HTML + CSS (.map-chips / .map-chip): the browser does the
|
|
1050
|
+
// wrapping, so there is no width arithmetic, no resize observer, and no horizontal scroll at phone width. A name
|
|
1051
|
+
// is never truncated (it wraps inside its chip when it is longer than the line).
|
|
1052
|
+
// Marks that were line styles on the old SVG are now border styles: a retried step has a DASHED border, and a step
|
|
1053
|
+
// whose leg ended there (the run resumed after it) has a DOTTED right edge. The sign + word come from
|
|
1054
|
+
// boxStatusText (src/panel/status.js's table) — the chip shows "[sign title]" and keeps the word in its tooltip.
|
|
1055
|
+
function buildStepMapHTML(steps){
|
|
1040
1056
|
var numIdx = stepNumberIndices(steps);
|
|
1041
|
-
var
|
|
1042
|
-
steps.
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
var
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1057
|
+
var last = steps.length - 1;
|
|
1058
|
+
var body = steps.map(function(s, idx){
|
|
1059
|
+
var sign = s.status && s.status.sign ? s.status.sign : "";
|
|
1060
|
+
var tries = boxRetryTry(s);
|
|
1061
|
+
var cls = "map-chip s-" + String(s.state).replace(/[^a-z-]/g, "") +
|
|
1062
|
+
(tries ? " retry" : "") + (s.resumedNext ? " resumed-next" : "");
|
|
1063
|
+
var label = escapeXml(stepTitleText(numIdx[idx], s));
|
|
1064
|
+
var blocked = s.blocked > 0 ? ' <span class="chip-blocked">' + s.blocked + ' blocked</span>' : '';
|
|
1065
|
+
var retry = tries ? '<span class="chip-retry">\u21bb' + tries + '</span> ' : '';
|
|
1066
|
+
var inner = '[' + retry + '<span class="chip-sign">' + escapeXml(sign) + '</span> ' + label + blocked + ']';
|
|
1067
|
+
var tip = ' title="' + escapeXml(boxStatusText(s)) + '"';
|
|
1068
|
+
var attrs = ' class="' + cls + '"' + tip + (s.resumedNext ? ' data-resumed="1"' : '') + (tries ? ' data-retry="' + tries + '"' : '');
|
|
1069
|
+
var chip = typeof s.partIndex === "number"
|
|
1070
|
+
? '<button type="button"' + attrs + ' data-part-index="' + s.partIndex + '">' + inner + '</button>'
|
|
1071
|
+
: '<span' + attrs + '>' + inner + '</span>';
|
|
1072
|
+
return '<span class="map-item" role="listitem">' +
|
|
1073
|
+
(idx > 0 ? '<span class="map-lead" aria-hidden="true">\u2192</span>' : '') +
|
|
1074
|
+
chip + (idx < last ? '<span class="map-arrow" aria-hidden="true">\u2192</span>' : '') + '</span>';
|
|
1075
|
+
}).join("");
|
|
1076
|
+
return '<div class="map-chips" role="list" aria-label="step map">' + body + '</div>';
|
|
1077
|
+
}
|
|
1078
|
+
// Ruling A: the first chip of each WRAPPED line shows a leading arrow. CSS cannot know which chip starts a line, so
|
|
1079
|
+
// after render/resize this compares each item's offsetTop with the previous item's (same line = same top). The lead
|
|
1080
|
+
// arrow is absolutely positioned (no layout impact), so toggling the class can never reflow a chip — no loop.
|
|
1081
|
+
// Takes anything with offsetTop + classList (real elements, or stubs in tests).
|
|
1082
|
+
function markLineStarts(items){
|
|
1083
|
+
for(var i = 0; i < items.length; i++){
|
|
1084
|
+
var starts = i > 0 && items[i].offsetTop > items[i - 1].offsetTop;
|
|
1085
|
+
if(starts) items[i].classList.add("line-start"); else items[i].classList.remove("line-start");
|
|
1066
1086
|
}
|
|
1067
|
-
if(!line1) return [text.slice(0, maxChars), text.slice(maxChars)];
|
|
1068
|
-
var line2 = words.slice(i).join(' ');
|
|
1069
|
-
return line2 ? [line1, line2] : [line1];
|
|
1070
1087
|
}
|
|
1071
|
-
// ONE shared rule for "does this box draw a retry
|
|
1088
|
+
// ONE shared rule for "does this box draw a retry mark, and what try number" — a multi-attempt
|
|
1072
1089
|
// box (a re-run step, or the fix loop; `run` on an old-shape spine falls in here too, naturally,
|
|
1073
|
-
// never special-cased) gets a dashed
|
|
1074
|
-
//
|
|
1090
|
+
// never special-cased) gets a dashed border; anything else (incl. every no-verdict kind, whose
|
|
1091
|
+
// attempts is already forced to [] upstream) gets 0.
|
|
1075
1092
|
function boxRetryTry(box){
|
|
1076
1093
|
var attempts = Array.isArray(box.attempts) ? box.attempts : [];
|
|
1077
1094
|
return attempts.length > 1 ? attempts.length : 0;
|
|
1078
1095
|
}
|
|
1079
|
-
//
|
|
1080
|
-
// start/control/end, and the "try N" label) is kept inside the box that
|
|
1081
|
-
// owns it — MIN_BOX_W (100) is comfortably wider than 2*RETRY_INSET, so
|
|
1082
|
-
// this never inverts the clamp range.
|
|
1083
|
-
var RETRY_INSET = 4;
|
|
1084
|
-
function clampRetryX(v, boxX, boxW){
|
|
1085
|
-
var lo = boxX + RETRY_INSET, hi = boxX + boxW - RETRY_INSET;
|
|
1086
|
-
return Math.min(hi, Math.max(lo, v));
|
|
1087
|
-
}
|
|
1088
|
-
function buildStepMapSVG(steps, availWidth){
|
|
1089
|
-
var n = steps.length;
|
|
1090
|
-
var naturalW = naturalBoxWidth(steps);
|
|
1091
|
-
var layout = computeMapLayout(n, availWidth, naturalW);
|
|
1092
|
-
var perRow = layout.perRow, boxW = layout.boxW, gap = MAP_GAP;
|
|
1093
|
-
var rows = Math.ceil(n / perRow);
|
|
1094
|
-
var margin = MAP_MARGIN;
|
|
1095
|
-
var numIdx = stepNumberIndices(steps);
|
|
1096
|
-
var titleLines = steps.map(function(s, idx){ return wrapTitleLines(stepTitleText(numIdx[idx], s), boxW); });
|
|
1097
|
-
var maxLines = titleLines.reduce(function(m, l){ return Math.max(m, l.length); }, 1);
|
|
1098
|
-
var boxH = maxLines > 1 ? BOX_H_2LINE : BOX_H;
|
|
1099
|
-
var rowH = boxH + ROW_GAP;
|
|
1100
|
-
var totalW = margin * 2 + perRow * boxW + (perRow - 1) * gap;
|
|
1101
|
-
var totalH = rows * rowH;
|
|
1102
|
-
var uid = "sm" + (stepMapUidCounter++);
|
|
1103
|
-
var body =
|
|
1104
|
-
'<defs>' +
|
|
1105
|
-
'<marker id="' + uid + '-arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">' +
|
|
1106
|
-
'<path d="M0,0 L6,3 L0,6 z" fill="var(--text-dim)"></path></marker>' +
|
|
1107
|
-
'<marker id="' + uid + '-arrow-grey" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">' +
|
|
1108
|
-
'<path d="M0,0 L6,3 L0,6 z" fill="var(--text-faint)"></path></marker>' +
|
|
1109
|
-
'</defs>';
|
|
1110
|
-
steps.forEach(function(s, idx){
|
|
1111
|
-
var row = Math.floor(idx / perRow);
|
|
1112
|
-
var col = idx % perRow;
|
|
1113
|
-
var rtl = (row % 2) === 1;
|
|
1114
|
-
var slotCol = rtl ? (perRow - 1 - col) : col;
|
|
1115
|
-
var x = margin + slotCol * (boxW + gap);
|
|
1116
|
-
var y = 30 + row * rowH;
|
|
1117
|
-
var colors = stepMapColors(s.state);
|
|
1118
|
-
var lines = titleLines[idx];
|
|
1119
|
-
var nextRow = idx < n - 1 ? Math.floor((idx + 1) / perRow) : null;
|
|
1120
|
-
var hasDrop = nextRow !== null && nextRow !== row;
|
|
1121
|
-
var stateY = y + boxH - 18;
|
|
1122
|
-
var partIdxAttr = typeof s.partIndex === "number" ? (' data-part-index="' + s.partIndex + '" style="cursor:pointer;"') : '';
|
|
1123
|
-
body += '<rect x="' + x + '" y="' + y + '" width="' + boxW + '" height="' + boxH +
|
|
1124
|
-
'" rx="0" fill="var(--panel2)" stroke="' + colors.stroke + '" stroke-width="2"' + partIdxAttr + '></rect>';
|
|
1125
|
-
if(s.blocked > 0){
|
|
1126
|
-
body += '<text x="' + (x + boxW - 6) + '" y="' + (y + 12) +
|
|
1127
|
-
'" text-anchor="end" font-size="10" fill="var(--red)">' + s.blocked + ' blocked</text>';
|
|
1128
|
-
}
|
|
1129
|
-
if(lines.length === 1){
|
|
1130
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + (y + 25) +
|
|
1131
|
-
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[0]) + '</text>';
|
|
1132
|
-
} else {
|
|
1133
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + (y + 17) +
|
|
1134
|
-
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[0]) + '</text>';
|
|
1135
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + (y + 29) +
|
|
1136
|
-
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[1]) + '</text>';
|
|
1137
|
-
}
|
|
1138
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + stateY +
|
|
1139
|
-
'" text-anchor="middle" font-size="10" fill="' + colors.textFill + '">' + escapeXml(boxStatusText(s)) + '</text>';
|
|
1140
|
-
// P2 (2026-09-27 ruling): a running box gets the mockup's pulsing
|
|
1141
|
-
// amber dot (design/panel-mockup.html ~line 1648) — same fixed color
|
|
1142
|
-
// and animation, never re-derived from the state's own stroke color.
|
|
1143
|
-
if(s.state === "running"){
|
|
1144
|
-
body += '<circle cx="' + (x + 20) + '" cy="' + (y + 12) + '" r="4" fill="#b8860b">' +
|
|
1145
|
-
'<animate attributeName="opacity" values="1;0.3;1" dur="1.2s" repeatCount="indefinite"></animate></circle>';
|
|
1146
|
-
}
|
|
1147
|
-
var retryTry = boxRetryTry(s);
|
|
1148
|
-
if(retryTry){
|
|
1149
|
-
// dashed self-loop under the box, same proportions as the design mockup's curve,
|
|
1150
|
-
// scaled to this box's own width. When this same box also carries the row-to-row
|
|
1151
|
-
// snake drop (which leaves from dead-center, x=cx), shift the loop's start away from
|
|
1152
|
-
// center by a fixed pixel offset so the two never begin at the same point.
|
|
1153
|
-
// item 2 (2026-09-27): at a narrow box width, subtracting that same
|
|
1154
|
-
// fixed pixel shift from proportionally-small x-fractions (e.g. the
|
|
1155
|
-
// curve's own endpoint at 2.8% of boxW) can push the coordinate past
|
|
1156
|
-
// the box's own left edge — the fixed shift was sized for desktop
|
|
1157
|
-
// widths and never scaled down with boxW. clampRetryX is the ONE
|
|
1158
|
-
// place every retry x-coordinate (curve start/control/end AND the
|
|
1159
|
-
// "try N" label) is kept inside [x + RETRY_INSET, x + boxW -
|
|
1160
|
-
// RETRY_INSET] — never re-clamped ad hoc per coordinate — so the
|
|
1161
|
-
// whole loop and its label always stay inside the box that owns it,
|
|
1162
|
-
// at every width, while keeping the mockup's proportions untouched
|
|
1163
|
-
// wherever they already fit (every desktop-width box already fits).
|
|
1164
|
-
var retryShift = hasDrop ? 14 : 0;
|
|
1165
|
-
var bottom = y + boxH;
|
|
1166
|
-
var sx = clampRetryX(x + boxW * 0.50 - retryShift, x, boxW);
|
|
1167
|
-
var sy = bottom + 5;
|
|
1168
|
-
var c1x = clampRetryX(x + boxW * 0.278 - retryShift, x, boxW), c1y = bottom + 30;
|
|
1169
|
-
var c2x = clampRetryX(x + boxW * 0.167 - retryShift, x, boxW), c2y = bottom + 30;
|
|
1170
|
-
var ex = clampRetryX(x + boxW * 0.028 - retryShift, x, boxW), ey = bottom + 2;
|
|
1171
|
-
var labelX = clampRetryX(x + boxW * 0.267 - retryShift, x, boxW);
|
|
1172
|
-
body += '<path d="M ' + sx + ' ' + sy + ' C ' + c1x + ' ' + c1y + ', ' + c2x + ' ' + c2y + ', ' + ex + ' ' + ey +
|
|
1173
|
-
'" fill="none" stroke="var(--text-faint)" stroke-width="1.5" stroke-dasharray="3,3" marker-end="url(#' + uid + '-arrow-grey)"></path>';
|
|
1174
|
-
body += '<text x="' + labelX + '" y="' + (bottom + 32) +
|
|
1175
|
-
'" text-anchor="middle" font-size="10" fill="var(--text-faint)">try ' + retryTry + '</text>';
|
|
1176
|
-
}
|
|
1177
|
-
if(idx < n - 1){
|
|
1178
|
-
// P5-R: where a leg ended and the next leg picked up, the connector is DOTTED (and says so) —
|
|
1179
|
-
// one run, one map; the dotted line is the only mark of the resume.
|
|
1180
|
-
var dash = s.resumedNext ? ' stroke-dasharray="2,4" data-resumed="1"' : '';
|
|
1181
|
-
if(nextRow === row){
|
|
1182
|
-
var midY = y + boxH / 2;
|
|
1183
|
-
if(!rtl){
|
|
1184
|
-
var nx = x + boxW + gap;
|
|
1185
|
-
body += '<line x1="' + (x + boxW) + '" y1="' + midY + '" x2="' + (nx - 2) + '" y2="' + midY +
|
|
1186
|
-
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1187
|
-
} else {
|
|
1188
|
-
var px = x - gap - boxW;
|
|
1189
|
-
body += '<line x1="' + x + '" y1="' + midY + '" x2="' + (px + boxW + 2) + '" y2="' + midY +
|
|
1190
|
-
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1191
|
-
}
|
|
1192
|
-
} else {
|
|
1193
|
-
var cx = x + boxW / 2;
|
|
1194
|
-
body += '<line x1="' + cx + '" y1="' + (y + boxH) + '" x2="' + cx + '" y2="' + (y + rowH - 2) +
|
|
1195
|
-
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
return '<svg width="' + totalW + '" height="' + totalH + '" viewBox="0 0 ' + totalW + ' ' + totalH + '" role="img" aria-label="step map">' + body + '</svg>';
|
|
1200
|
-
}
|
|
1201
|
-
// the line-style key under the MAP (hamr 2026-10-04): only the two line styles, never a sign legend
|
|
1096
|
+
// the line-style key under the MAP (hamr 2026-10-04): only the two border styles, never a sign legend
|
|
1202
1097
|
function stepMapKeyHTML(steps){
|
|
1203
1098
|
var resumed = (steps || []).some(function(b){ return b.resumedNext; });
|
|
1204
1099
|
return '<div class="map-key" data-testid="map-key">' +
|
|
1205
|
-
'<span
|
|
1206
|
-
(resumed ? '<span
|
|
1100
|
+
'<span>\u21bbN = took N tries (dashed box)</span>' +
|
|
1101
|
+
(resumed ? '<span>dotted edge = resumed after</span>' : '') +
|
|
1207
1102
|
'</div>';
|
|
1208
1103
|
}
|
|
1209
|
-
var lastSteps = null;
|
|
1210
|
-
var lastWidth = null;
|
|
1211
|
-
function mapAvailWidth(wrapper){
|
|
1212
|
-
if(!wrapper) return null;
|
|
1213
|
-
var w = wrapper.getBoundingClientRect().width;
|
|
1214
|
-
if(!w) return null;
|
|
1215
|
-
return Math.max(0, w - 20);
|
|
1216
|
-
}
|
|
1217
1104
|
function renderStepMap(steps){
|
|
1218
1105
|
var mount = document.getElementById("map-mount");
|
|
1219
1106
|
var wrapper = document.getElementById("map-box");
|
|
1220
|
-
lastSteps = steps;
|
|
1221
1107
|
if(!steps || !steps.length){
|
|
1222
1108
|
mount.innerHTML = "";
|
|
1223
1109
|
wrapper.hidden = true;
|
|
1224
1110
|
return;
|
|
1225
1111
|
}
|
|
1226
1112
|
wrapper.hidden = false;
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
// build item B: a map box click jumps to the Audit tab, that same part
|
|
1113
|
+
mount.innerHTML = buildStepMapHTML(steps) + stepMapKeyHTML(steps);
|
|
1114
|
+
layoutMap();
|
|
1115
|
+
// build item B: a map chip click jumps to the Audit tab, that same part
|
|
1231
1116
|
// expanded and scrolled into view — same click-through as the card list,
|
|
1232
|
-
// sharing partIndex so the two can never disagree about which row a
|
|
1233
|
-
// maps to.
|
|
1234
|
-
//
|
|
1235
|
-
mount.querySelectorAll("
|
|
1236
|
-
|
|
1237
|
-
var idx = Number(
|
|
1117
|
+
// sharing partIndex so the two can never disagree about which row a chip
|
|
1118
|
+
// maps to. Bound once per render (mount.innerHTML above already wiped any
|
|
1119
|
+
// previous listeners' targets along with the old markup).
|
|
1120
|
+
mount.querySelectorAll(".map-chip[data-part-index]").forEach(function(chip){
|
|
1121
|
+
chip.addEventListener("click", function(){
|
|
1122
|
+
var idx = Number(chip.getAttribute("data-part-index"));
|
|
1238
1123
|
document.getElementById("tab-audit").click();
|
|
1239
1124
|
openAuditPart(idx);
|
|
1240
1125
|
});
|
|
1241
1126
|
});
|
|
1242
1127
|
}
|
|
1243
|
-
function
|
|
1244
|
-
var
|
|
1245
|
-
if(!
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1128
|
+
function layoutMap(){
|
|
1129
|
+
var box = document.getElementById("map-box");
|
|
1130
|
+
if(!box || box.hidden) return;
|
|
1131
|
+
markLineStarts(Array.prototype.slice.call(box.querySelectorAll(".map-item")));
|
|
1132
|
+
}
|
|
1133
|
+
var layoutTimer = null;
|
|
1134
|
+
function layoutMapSoon(){
|
|
1135
|
+
if(layoutTimer) clearTimeout(layoutTimer);
|
|
1136
|
+
layoutTimer = setTimeout(function(){ layoutTimer = null; layoutMap(); }, 60);
|
|
1250
1137
|
}
|
|
1251
1138
|
if(typeof ResizeObserver !== "undefined"){
|
|
1252
|
-
var
|
|
1253
|
-
var
|
|
1254
|
-
if(
|
|
1139
|
+
var mapRo = new ResizeObserver(layoutMapSoon);
|
|
1140
|
+
var mapBoxEl = document.getElementById("map-box");
|
|
1141
|
+
if(mapBoxEl) mapRo.observe(mapBoxEl);
|
|
1255
1142
|
} else {
|
|
1256
|
-
window.addEventListener("resize",
|
|
1143
|
+
window.addEventListener("resize", layoutMapSoon);
|
|
1257
1144
|
}
|
|
1258
1145
|
|
|
1259
1146
|
// F196: a `synthetic` step (e.g. the "died during planning" placeholder
|
|
@@ -1267,6 +1154,7 @@
|
|
|
1267
1154
|
function glyphClass(g){
|
|
1268
1155
|
if(g === "✓") return "green";
|
|
1269
1156
|
if(g === "✗") return "red";
|
|
1157
|
+
if(g === "■") return "stopped";
|
|
1270
1158
|
if(g === "▶") return "amber";
|
|
1271
1159
|
if(g === "?") return "magenta";
|
|
1272
1160
|
return "grey";
|
|
@@ -1499,6 +1387,17 @@
|
|
|
1499
1387
|
// is a normal, expected action.
|
|
1500
1388
|
var wfExpanded = {};
|
|
1501
1389
|
|
|
1390
|
+
// hamr 2026-10-08 (display only, nothing deleted): with NO search/filter active an expanded job shows at most its
|
|
1391
|
+
// latest WF_SUB_CAP other runs (g.runs is latest-first). A selected older run beyond the cap (picked from History) is
|
|
1392
|
+
// appended after them rather than hidden. With search/filters active every matching sub-card shows.
|
|
1393
|
+
var WF_SUB_CAP = 7;
|
|
1394
|
+
function capSubRuns(childRuns, active, selectedRunid){
|
|
1395
|
+
if(active || childRuns.length <= WF_SUB_CAP) return {shown: childRuns, more: false};
|
|
1396
|
+
var shown = childRuns.slice(0, WF_SUB_CAP);
|
|
1397
|
+
var sel = selectedRunid ? childRuns.slice(WF_SUB_CAP).filter(function(r){ return r.runid === selectedRunid; }) : [];
|
|
1398
|
+
return {shown: shown.concat(sel), more: true};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1502
1401
|
function renderWorkflows(jobs){
|
|
1503
1402
|
var el = document.getElementById("wf-list");
|
|
1504
1403
|
el.innerHTML = "";
|
|
@@ -1585,7 +1484,15 @@
|
|
|
1585
1484
|
if(expanded){
|
|
1586
1485
|
var runsWrap = document.createElement("div");
|
|
1587
1486
|
runsWrap.className = "wf-runs";
|
|
1588
|
-
|
|
1487
|
+
var capped = capSubRuns(childRuns, active, olderSelected);
|
|
1488
|
+
capped.shown.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
|
|
1489
|
+
if(capped.more){
|
|
1490
|
+
var olderHint = document.createElement("div");
|
|
1491
|
+
olderHint.className = "hint";
|
|
1492
|
+
olderHint.setAttribute("data-testid", "wf-older-" + g.job);
|
|
1493
|
+
olderHint.textContent = "older: use search.";
|
|
1494
|
+
runsWrap.appendChild(olderHint);
|
|
1495
|
+
}
|
|
1589
1496
|
wrap.appendChild(runsWrap);
|
|
1590
1497
|
}
|
|
1591
1498
|
el.appendChild(wrap);
|
|
@@ -1654,7 +1561,8 @@
|
|
|
1654
1561
|
var q = (filters && filters.search) || "";
|
|
1655
1562
|
return (runs || []).filter(function(r){
|
|
1656
1563
|
if(ct.length && ct.indexOf(r.checkType) === -1) return false;
|
|
1657
|
-
|
|
1564
|
+
// a stopped run ([■]) was a [✗] before hamr's 2026-10-07 sign ruling — the fail chip still finds it
|
|
1565
|
+
if(rs.length && rs.indexOf(r.glyph) === -1 && !(r.glyph === "\u25A0" && rs.indexOf("\u2717") !== -1)) return false;
|
|
1658
1566
|
if(!matchesSearch(r.job, r.runid, r.model, q, r.runNo)) return false;
|
|
1659
1567
|
if(time !== "all"){
|
|
1660
1568
|
var days = time === "7d" ? 7 : 30;
|
|
@@ -1966,7 +1874,6 @@
|
|
|
1966
1874
|
clearSelection();
|
|
1967
1875
|
currentRunid = null;
|
|
1968
1876
|
runIsLive = false;
|
|
1969
|
-
resumeMode = null;
|
|
1970
1877
|
currentImportId = id;
|
|
1971
1878
|
document.querySelectorAll("#import-list .wf-row").forEach(function(r){
|
|
1972
1879
|
var on = r.getAttribute("data-testid") === "import-row-" + id;
|
|
@@ -2363,11 +2270,9 @@
|
|
|
2363
2270
|
// The sentences are CODE-OWNED on the server (`endedFor`, src/panel/server.js) — this only
|
|
2364
2271
|
// paints them. `detail.ended` is null while a run is live (no block while running).
|
|
2365
2272
|
var lastEndedSig = null;
|
|
2366
|
-
// P5-R
|
|
2367
|
-
//
|
|
2368
|
-
//
|
|
2369
|
-
var resumeMode = null;
|
|
2370
|
-
var lastJobShown = null;
|
|
2273
|
+
// 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
|
|
2274
|
+
// in RESUME mode, only the money cap and the time cap open (applyResume / signResume). The Job tab is read-only on
|
|
2275
|
+
// every run — values only, no inputs, no buttons. A resume is the SAME run: no new card, no new job.
|
|
2371
2276
|
function renderEnded(detail){
|
|
2372
2277
|
var box = document.getElementById("ended-block");
|
|
2373
2278
|
var e = detail.ended;
|
|
@@ -2384,7 +2289,7 @@
|
|
|
2384
2289
|
'<div class="ended-row"><span class="ended-label">NEXT</span><span class="ended-text" data-testid="ended-next">' + escapeXml(e.next) + '</span></div>';
|
|
2385
2290
|
}
|
|
2386
2291
|
// ---- P5 item 5: the run's ACTION AREA (Run tab, above the counters) — Stop while the run is live, Resume once
|
|
2387
|
-
// it ended in a way the engine would accept (
|
|
2292
|
+
// it ended in a way the engine would accept (opens the Chat card's resume mode), Edit in chat on every non-green ending.
|
|
2388
2293
|
// The Ended block (Audit tab) is text only and carries no buttons.
|
|
2389
2294
|
var stopAsked = {};
|
|
2390
2295
|
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
@@ -2397,6 +2302,33 @@
|
|
|
2397
2302
|
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
2398
2303
|
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2399
2304
|
}
|
|
2305
|
+
// Edit in chat (hamr 2026-10-07, option A) — the same prefill door as Reuse workflow (`start-from`, the ONE reader of a
|
|
2306
|
+
// run's signed job), but the Chat card opens with EVERY box open and the normal [Start drafting] path: it drafts a new job.
|
|
2307
|
+
// The origin line is code-owned fixed text; run name and status word come from the same helpers the run header uses.
|
|
2308
|
+
function editInChat(detail){
|
|
2309
|
+
var origin = "Copied from " + detail.job + " (" + runLabel(detail.runNo, detail.runid) + ") \u2014 " +
|
|
2310
|
+
((detail.status && detail.status.word) || "ended") +
|
|
2311
|
+
". Every box is open. Change anything and it drafts as a new job; change nothing and it is another run of the same job.";
|
|
2312
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2313
|
+
document.getElementById("tab-chat").click();
|
|
2314
|
+
document.dispatchEvent(new CustomEvent("bareloop-edit", {detail: (r && r.ok) ? {runid: detail.runid, prefill: r, origin: origin} : {error: (r && r.error) || "nothing to copy"}}));
|
|
2315
|
+
}).catch(function(x){
|
|
2316
|
+
document.getElementById("tab-chat").click();
|
|
2317
|
+
document.dispatchEvent(new CustomEvent("bareloop-edit", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
// Resume (hamr 2026-10-07, option B — moved from the run's Job tab): the Chat card opens in RESUME mode, filled from the
|
|
2321
|
+
// SAME prefill (`start-from`), everything locked but the two caps. The card posts /api/runs/<id>/resume; a resume never
|
|
2322
|
+
// drafts and never makes a new card, job or run id (one run = one id).
|
|
2323
|
+
function resumeInChat(detail){
|
|
2324
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2325
|
+
document.getElementById("tab-chat").click();
|
|
2326
|
+
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"}}));
|
|
2327
|
+
}).catch(function(x){
|
|
2328
|
+
document.getElementById("tab-chat").click();
|
|
2329
|
+
document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2400
2332
|
function renderRunActions(detail){
|
|
2401
2333
|
var bar = document.getElementById("run-actions");
|
|
2402
2334
|
var html = "";
|
|
@@ -2412,6 +2344,10 @@
|
|
|
2412
2344
|
var canResume = !!detail.resume && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "resume"; });
|
|
2413
2345
|
if(canResume) html = '<button class="btn primary" type="button" data-testid="btn-resume-run">Resume</button>';
|
|
2414
2346
|
}
|
|
2347
|
+
// Edit in chat: every non-green ending (the server's Ended block says so) — a died run included
|
|
2348
|
+
if(!live && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "edit"; })){
|
|
2349
|
+
html += '<button class="btn" type="button" data-testid="btn-edit-chat">Edit in chat</button>';
|
|
2350
|
+
}
|
|
2415
2351
|
// Reuse workflow: on a GREEN run only (the server's Ended block says so) — a new run of the same signed job
|
|
2416
2352
|
if(!live && !detail.died && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "reuse"; })){
|
|
2417
2353
|
html += '<button class="btn" type="button" data-testid="btn-reuse">Reuse workflow</button>';
|
|
@@ -2420,6 +2356,8 @@
|
|
|
2420
2356
|
bar.hidden = html === "";
|
|
2421
2357
|
var reuseBtn = bar.querySelector('[data-testid="btn-reuse"]');
|
|
2422
2358
|
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid); });
|
|
2359
|
+
var editBtn = bar.querySelector('[data-testid="btn-edit-chat"]');
|
|
2360
|
+
if(editBtn) editBtn.addEventListener("click", function(){ editInChat(detail); });
|
|
2423
2361
|
var stopBtn = bar.querySelector('[data-testid="btn-stop"]');
|
|
2424
2362
|
if(stopBtn){
|
|
2425
2363
|
stopBtn.addEventListener("click", function(){
|
|
@@ -2431,82 +2369,7 @@
|
|
|
2431
2369
|
});
|
|
2432
2370
|
}
|
|
2433
2371
|
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2434
|
-
if(resBtn) resBtn.addEventListener("click", function(){
|
|
2435
|
-
}
|
|
2436
|
-
function openResumeOnJobTab(detail){
|
|
2437
|
-
var r = detail.resume || {};
|
|
2438
|
-
resumeMode = {
|
|
2439
|
-
runid: detail.runid, spentUsd: r.spentUsd, spendComplete: r.spendComplete,
|
|
2440
|
-
wallUsedMs: r.wallUsedMs,
|
|
2441
|
-
budget: (r.budgetUsd === null || r.budgetUsd === undefined) ? "" : String(r.budgetUsd),
|
|
2442
|
-
wall: typeof r.maxWallMin === "number" ? String(r.maxWallMin) : null,
|
|
2443
|
-
painted: false,
|
|
2444
|
-
};
|
|
2445
|
-
document.getElementById("tab-details").click();
|
|
2446
|
-
paintResumeMode();
|
|
2447
|
-
}
|
|
2448
|
-
// Turns the Job tab's two cap values into inputs (and shows the Sign box) while a resume is open on this run;
|
|
2449
|
-
// called again after every Job re-render so a re-render never drops the typed numbers.
|
|
2450
|
-
function paintResumeMode(){
|
|
2451
|
-
var box = document.getElementById("resume-job");
|
|
2452
|
-
var moneyLabel = document.getElementById("details-cap-money-label");
|
|
2453
|
-
var timeLabel = document.getElementById("details-cap-time-label");
|
|
2454
|
-
if(!resumeMode || resumeMode.runid !== currentRunid){
|
|
2455
|
-
box.hidden = true; box.innerHTML = "";
|
|
2456
|
-
moneyLabel.textContent = "$ cap"; timeLabel.textContent = "Time cap";
|
|
2457
|
-
return;
|
|
2458
|
-
}
|
|
2459
|
-
var m = resumeMode;
|
|
2460
|
-
// the unit lives in the caption, never as loose text around the input
|
|
2461
|
-
moneyLabel.textContent = "Money cap ($)";
|
|
2462
|
-
timeLabel.textContent = "Time cap (min)";
|
|
2463
|
-
document.getElementById("details-cap-money").innerHTML =
|
|
2464
|
-
'<span class="resume-cap"><input type="number" min="0" step="0.01" id="resume-budget" data-testid="resume-budget" value="' + escapeXml(m.budget) + '"></span>';
|
|
2465
|
-
if(m.wall !== null){
|
|
2466
|
-
document.getElementById("details-cap-time").innerHTML =
|
|
2467
|
-
'<span class="resume-cap"><input type="number" min="1" step="1" id="resume-wall" data-testid="resume-wall" value="' + escapeXml(m.wall) + '"></span>';
|
|
2468
|
-
}
|
|
2469
|
-
var budgetEl = document.getElementById("resume-budget");
|
|
2470
|
-
budgetEl.addEventListener("input", function(){ m.budget = budgetEl.value; });
|
|
2471
|
-
var wallEl = document.getElementById("resume-wall");
|
|
2472
|
-
if(wallEl) wallEl.addEventListener("input", function(){ m.wall = wallEl.value; });
|
|
2473
|
-
box.hidden = false;
|
|
2474
|
-
if(m.painted) return;
|
|
2475
|
-
m.painted = true;
|
|
2476
|
-
var spent = (m.spentUsd === null || m.spentUsd === undefined) ? "unknown" : ((m.spendComplete ? "" : "at least ") + panelMoney(m.spentUsd));
|
|
2477
|
-
box.innerHTML =
|
|
2478
|
-
'<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>' +
|
|
2479
|
-
'<div class="hint" data-testid="resume-spent">spent so far ' + escapeXml(spent) +
|
|
2480
|
-
(typeof m.wallUsedMs === "number" && isFinite(m.wallUsedMs) ? ' · time used so far ' + Math.ceil(m.wallUsedMs / 60000) + ' min' : '') + '</div>' +
|
|
2481
|
-
'<div class="ended-actions"><button class="btn primary" type="button" data-testid="btn-sign-resume">Sign & resume</button> ' +
|
|
2482
|
-
'<button class="btn" type="button" data-testid="btn-resume-cancel">Cancel</button></div>' +
|
|
2483
|
-
'<div class="resume-err" id="resume-err" data-testid="resume-err" hidden></div>';
|
|
2484
|
-
var resumeSignBtn = box.querySelector('[data-testid="btn-sign-resume"]');
|
|
2485
|
-
box.querySelector('[data-testid="btn-resume-cancel"]').addEventListener("click", function(){
|
|
2486
|
-
resumeMode = null;
|
|
2487
|
-
renderJob(lastJobShown);
|
|
2488
|
-
});
|
|
2489
|
-
resumeSignBtn.addEventListener("click", function(){
|
|
2490
|
-
var err = document.getElementById("resume-err");
|
|
2491
|
-
err.hidden = true;
|
|
2492
|
-
resumeSignBtn.disabled = true;
|
|
2493
|
-
var body = {budgetUsd: document.getElementById("resume-budget").value};
|
|
2494
|
-
var wall = document.getElementById("resume-wall");
|
|
2495
|
-
if(wall){ body.maxWallMin = wall.value; }
|
|
2496
|
-
return authorPost("/api/runs/" + encodeURIComponent(m.runid) + "/resume", body).then(function(res){
|
|
2497
|
-
if(res.body && res.body.ok){
|
|
2498
|
-
// the SAME run goes on: back to its Run tab, which follows it until it is live again
|
|
2499
|
-
resumeMode = null;
|
|
2500
|
-
renderJob(lastJobShown);
|
|
2501
|
-
document.getElementById("tab-run").click();
|
|
2502
|
-
afterResumeRefresh(m.runid, 8);
|
|
2503
|
-
} else {
|
|
2504
|
-
resumeSignBtn.disabled = false;
|
|
2505
|
-
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
2506
|
-
err.hidden = false;
|
|
2507
|
-
}
|
|
2508
|
-
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
2509
|
-
});
|
|
2372
|
+
if(resBtn) resBtn.addEventListener("click", function(){ resumeInChat(detail); });
|
|
2510
2373
|
}
|
|
2511
2374
|
// After a resume starts, the open run's own page follows it: re-select once the run reads live again (the spine's
|
|
2512
2375
|
// new leg appears a moment after the engine starts), a few tries, never forever.
|
|
@@ -2600,7 +2463,7 @@
|
|
|
2600
2463
|
" · " + wallPhrase;
|
|
2601
2464
|
|
|
2602
2465
|
var box = document.getElementById("run-summary");
|
|
2603
|
-
box.className = "summary-box " + (detail.died ? "magenta" : (detail.glyph === "✗" ? "red" : (detail.glyph === "▶" ? "amber" : "")));
|
|
2466
|
+
box.className = "summary-box " + (detail.died ? "magenta" : ((detail.glyph === "✗" || detail.glyph === "■") ? "red" : (detail.glyph === "▶" ? "amber" : "")));
|
|
2604
2467
|
// F196: a synthetic placeholder box (e.g. "died during planning") is a
|
|
2605
2468
|
// LABEL, not a real step — excluded from this count so a run that died
|
|
2606
2469
|
// before any step started reads "0 of 0 done" / "none started", never
|
|
@@ -2679,7 +2542,7 @@
|
|
|
2679
2542
|
|
|
2680
2543
|
// 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
|
|
2681
2544
|
function stepStateHTML(box, testid){
|
|
2682
|
-
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2545
|
+
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")));
|
|
2683
2546
|
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(boxStatusWord(box)) + '</b></span>';
|
|
2684
2547
|
}
|
|
2685
2548
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
@@ -3269,7 +3132,6 @@
|
|
|
3269
3132
|
function renderJob(job){
|
|
3270
3133
|
var empty = document.getElementById("job-empty");
|
|
3271
3134
|
var card = document.getElementById("job-card-readonly");
|
|
3272
|
-
lastJobShown = job;
|
|
3273
3135
|
if(!job){ empty.hidden = false; card.hidden = true; return; }
|
|
3274
3136
|
empty.hidden = true;
|
|
3275
3137
|
card.hidden = false;
|
|
@@ -3292,7 +3154,6 @@
|
|
|
3292
3154
|
document.getElementById("job-note").textContent = job.note || "";
|
|
3293
3155
|
lastJobToolsList = Array.isArray(job.toolsList) ? job.toolsList : null;
|
|
3294
3156
|
paintOfferedRow();
|
|
3295
|
-
paintResumeMode();
|
|
3296
3157
|
}
|
|
3297
3158
|
|
|
3298
3159
|
// ---- selection + loading ----
|
|
@@ -3312,7 +3173,6 @@
|
|
|
3312
3173
|
document.querySelectorAll(siblingSelector).forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
3313
3174
|
if(rowEl){ rowEl.classList.add("selected"); rowEl.setAttribute("aria-pressed","true"); }
|
|
3314
3175
|
currentRunid = runid;
|
|
3315
|
-
resumeMode = null;
|
|
3316
3176
|
lastLegDividers = [];
|
|
3317
3177
|
lastJobToolsList = null;
|
|
3318
3178
|
lastRunDetailSig = null;
|
|
@@ -3696,6 +3556,7 @@
|
|
|
3696
3556
|
var msgInput = document.getElementById("chat-msg");
|
|
3697
3557
|
var mainBtn = document.getElementById("chat-main-btn");
|
|
3698
3558
|
var mainHint = document.getElementById("chat-main-hint");
|
|
3559
|
+
var reopenBtn = document.getElementById("chat-reopen-btn");
|
|
3699
3560
|
var mainAction = "none";
|
|
3700
3561
|
var lastState = null;
|
|
3701
3562
|
var startOk = false;
|
|
@@ -3731,32 +3592,39 @@
|
|
|
3731
3592
|
var autoSigned = false;
|
|
3732
3593
|
var sfLine = document.getElementById("chat-startfrom-line");
|
|
3733
3594
|
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3595
|
+
var originLine = document.getElementById("chat-origin-line");
|
|
3734
3596
|
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3735
3597
|
var LOCKED_IDS = ["jf-name", "jf-goal", "jf-success", "jf-guardrails", "jf-judge"];
|
|
3736
3598
|
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3737
3599
|
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3738
3600
|
var OPEN_IDS = ["jf-source", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3739
3601
|
var reuseOn = false;
|
|
3602
|
+
// Resume mode (hamr 2026-10-07): the card shows the signed job of one run, every box locked except the two caps.
|
|
3603
|
+
// `resumeRun` = {runid, noTime} while it is on, else null. Never set together with a drafting session or a reuse pick.
|
|
3604
|
+
var resumeRun = null;
|
|
3605
|
+
var CAP_IDS = ["jf-cap-money", "jf-cap-time"];
|
|
3740
3606
|
function syncCardLock(){
|
|
3741
3607
|
var live = sessionLive;
|
|
3608
|
+
var resuming = resumeRun !== null;
|
|
3742
3609
|
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3743
|
-
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live); });
|
|
3744
|
-
OPEN_IDS.forEach(function(id){ setLock(id, live); });
|
|
3745
|
-
document.getElementById("jf-model").disabled = live;
|
|
3746
|
-
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse);
|
|
3747
|
-
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live; });
|
|
3610
|
+
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live || resuming); });
|
|
3611
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1)); });
|
|
3612
|
+
document.getElementById("jf-model").disabled = live || resuming;
|
|
3613
|
+
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3614
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3748
3615
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3749
|
-
if(!reuseOn && !live){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3616
|
+
if(!reuseOn && !live && !resuming){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3750
3617
|
}
|
|
3751
3618
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3752
|
-
function clearStartFrom(){
|
|
3753
|
-
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes
|
|
3754
|
-
|
|
3619
|
+
function clearStartFrom(keepBoxes){
|
|
3620
|
+
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes — except when a Check type
|
|
3621
|
+
// radio click leaves reuse (keepBoxes): a radio never blanks what the person has on the card
|
|
3622
|
+
if(startFrom && !keepBoxes){
|
|
3755
3623
|
["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 = ""; });
|
|
3756
3624
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3757
3625
|
}
|
|
3758
3626
|
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
3759
|
-
sfLine.hidden = true; sfNote.hidden = true;
|
|
3627
|
+
sfLine.hidden = true; sfNote.hidden = true; originLine.hidden = true;
|
|
3760
3628
|
setReuseLocked(false);
|
|
3761
3629
|
renderPicker();
|
|
3762
3630
|
renderMain();
|
|
@@ -3807,6 +3675,7 @@
|
|
|
3807
3675
|
function renderReuseList(){
|
|
3808
3676
|
var q = rwQ.value.trim();
|
|
3809
3677
|
var all = rwJobs || [];
|
|
3678
|
+
var loading = rwJobs === null && !rwFailed;
|
|
3810
3679
|
rwShown = all.filter(function(j){ return j.job.toLowerCase().indexOf(q.toLowerCase()) !== -1; });
|
|
3811
3680
|
if(rwHl >= rwShown.length) rwHl = 0;
|
|
3812
3681
|
if(rwShown.length){
|
|
@@ -3815,7 +3684,7 @@
|
|
|
3815
3684
|
}).join("");
|
|
3816
3685
|
} else {
|
|
3817
3686
|
rwList.innerHTML = '<div class="rw-empty" data-testid="rw-empty">' + (rwFailed ? "Could not load the job list."
|
|
3818
|
-
: (all.length === 0 ? "No green jobs yet." : 'No green job matches "' + escapeXml(q) + '".')) + "</div>";
|
|
3687
|
+
: loading ? "Loading\u2026" : (all.length === 0 ? "No green jobs yet." : 'No green job matches "' + escapeXml(q) + '".')) + "</div>";
|
|
3819
3688
|
}
|
|
3820
3689
|
}
|
|
3821
3690
|
function loadReuseJobs(){
|
|
@@ -3826,9 +3695,10 @@
|
|
|
3826
3695
|
}).catch(function(){ rwFailed = true; rwJobs = null; renderReuseList(); });
|
|
3827
3696
|
}
|
|
3828
3697
|
// the empty search state: card emptied, nothing picked, Sign & run disabled until a job is picked
|
|
3698
|
+
// (the boxes are NOT cleared here: clicking the Reuse radio never blanks typing; only a pick fills the card)
|
|
3829
3699
|
function openPicker(){
|
|
3830
|
-
clearCardBoxes();
|
|
3831
3700
|
rwQ.value = ""; rwHl = 0;
|
|
3701
|
+
rwJobs = null; rwFailed = false; // a fresh open shows "Loading…", never the previous list
|
|
3832
3702
|
document.getElementById("jf-judge").disabled = true;
|
|
3833
3703
|
renderPicker();
|
|
3834
3704
|
renderReuseList();
|
|
@@ -3854,8 +3724,22 @@
|
|
|
3854
3724
|
openPicker();
|
|
3855
3725
|
}
|
|
3856
3726
|
document.getElementById("rw-change").addEventListener("click", unpickReuse);
|
|
3727
|
+
// leaving resume mode: the title, the hidden drafting chrome, the time box and the resume box all come back
|
|
3728
|
+
function exitResumeMode(){
|
|
3729
|
+
resumeRun = null;
|
|
3730
|
+
originLine.classList.remove("res");
|
|
3731
|
+
document.getElementById("job-card-title").textContent = "Job card \u2014 draft";
|
|
3732
|
+
document.getElementById("chat-resume-box").hidden = true;
|
|
3733
|
+
document.getElementById("chat-resume-err").hidden = true;
|
|
3734
|
+
document.getElementById("jf-cap-time").parentNode.hidden = false;
|
|
3735
|
+
document.getElementById("chat-clear-btn").hidden = false;
|
|
3736
|
+
["chat-main-btn", "chat-main-hint"].forEach(function(id){ document.getElementById(id).hidden = false; });
|
|
3737
|
+
document.querySelector(".chat-input-row").hidden = false;
|
|
3738
|
+
}
|
|
3857
3739
|
function openNewCard(){
|
|
3858
3740
|
card.hidden = false;
|
|
3741
|
+
originLine.hidden = true;
|
|
3742
|
+
exitResumeMode();
|
|
3859
3743
|
thread.innerHTML = "";
|
|
3860
3744
|
errEl.textContent = "";
|
|
3861
3745
|
actionErrEl.textContent = "";
|
|
@@ -3961,12 +3845,11 @@
|
|
|
3961
3845
|
// the limit changed in Settings: re-read the note for whatever cap is typed
|
|
3962
3846
|
document.addEventListener("bareloop-limit-changed", refreshCapNote);
|
|
3963
3847
|
// Check type: Deterministic / Rubric / Reuse workflow. Reuse opens the picker on an empty card; Deterministic or
|
|
3964
|
-
// Rubric while the picker is showing (picked or not) LEAVES reuse —
|
|
3848
|
+
// Rubric while the picker is showing (picked or not) LEAVES reuse — boxes untouched (clearStartFrom(true)).
|
|
3965
3849
|
function onVerdictChange(v){
|
|
3966
3850
|
if(v === "reuse"){ if(rwBox.hidden) openPicker(); return; }
|
|
3967
3851
|
if(!rwBox.hidden){
|
|
3968
|
-
clearStartFrom();
|
|
3969
|
-
clearCardBoxes();
|
|
3852
|
+
clearStartFrom(true);
|
|
3970
3853
|
setVerdict(v);
|
|
3971
3854
|
renderPicker();
|
|
3972
3855
|
}
|
|
@@ -4126,26 +4009,20 @@
|
|
|
4126
4009
|
}
|
|
4127
4010
|
|
|
4128
4011
|
// hamr's ruling 2026-10-05: ONE wide button below the message box. Its wording, action and enabled state are ONE
|
|
4129
|
-
// pure function of (state, typed-text-empty, reuse); state = {session, startOk, phase, pendingAsk,
|
|
4130
|
-
// signClicked, specHash}. Actions: start | check-deps | send |
|
|
4012
|
+
// pure function of (state, typed-text-empty, reuse); state = {session, startOk, phase, pendingAsk,
|
|
4013
|
+
// signClicked, specHash}. Actions: start | check-deps | send | sign-prepare | sign | none.
|
|
4131
4014
|
function mainButtonFor(st, textEmpty, reuse){
|
|
4132
4015
|
var ask = st.pendingAsk;
|
|
4133
4016
|
var atMenu = !!ask && ask.kind === "menu";
|
|
4134
|
-
var
|
|
4135
|
-
var hint = atMenu ? left + (left === 1 ? " change left" : " changes left") : "";
|
|
4017
|
+
var hint = "";
|
|
4136
4018
|
if(!st.session){
|
|
4137
4019
|
return {text: reuse ? "Sign & run" : "Start drafting", action: "start", disabled: !st.startOk, hint: ""};
|
|
4138
4020
|
}
|
|
4139
4021
|
if(ask && ask.kind === "install-needed"){
|
|
4140
4022
|
return {text: "Check again", action: "check-deps", disabled: false, hint: ""};
|
|
4141
4023
|
}
|
|
4142
|
-
if(ask && !textEmpty){
|
|
4143
|
-
return atMenu
|
|
4144
|
-
? {text: "Send", action: "revise", disabled: left <= 0, hint: hint}
|
|
4145
|
-
: {text: "Send", action: "send", disabled: false, hint: ""};
|
|
4146
|
-
}
|
|
4147
4024
|
if(ask && !atMenu){
|
|
4148
|
-
return {text: "Send", action: "send", disabled:
|
|
4025
|
+
return {text: "Send", action: "send", disabled: textEmpty, hint: ""};
|
|
4149
4026
|
}
|
|
4150
4027
|
if(atMenu || st.phase === "prepared"){
|
|
4151
4028
|
if(st.phase === "prepared" && st.signClicked){
|
|
@@ -4155,16 +4032,21 @@
|
|
|
4155
4032
|
}
|
|
4156
4033
|
return {text: "Sign & run", action: "none", disabled: true, hint: ""};
|
|
4157
4034
|
}
|
|
4158
|
-
// pure: the ask box takes typed text only when the session has a pending ask that accepts it (the plan
|
|
4159
|
-
//
|
|
4035
|
+
// pure: the ask box takes typed text only when the session has a pending ask that accepts it (a question the plan
|
|
4036
|
+
// raised); not with no session, no ask, while the plan waits for the OK (that is Sign & run or Revise, never typed
|
|
4037
|
+
// text), or while waiting on an install
|
|
4160
4038
|
function askBoxOpenFor(st){
|
|
4161
|
-
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind !== "install-needed";
|
|
4039
|
+
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind !== "install-needed" && st.pendingAsk.kind !== "menu";
|
|
4040
|
+
}
|
|
4041
|
+
// pure: Revise shows only while a plan waits for the OK
|
|
4042
|
+
function reviseShownFor(st){
|
|
4043
|
+
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind === "menu";
|
|
4162
4044
|
}
|
|
4163
4045
|
function renderMain(){
|
|
4164
4046
|
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce};
|
|
4165
4047
|
if(sessionLive && lastState){
|
|
4166
4048
|
st.phase = lastState.phase; st.pendingAsk = lastState.pendingAsk;
|
|
4167
|
-
st.
|
|
4049
|
+
st.specHash = lastState.specHash;
|
|
4168
4050
|
}
|
|
4169
4051
|
// hamr 2026-10-05: the ask box is open only while something asks for a reply; otherwise dimmed and emptied
|
|
4170
4052
|
var open = askBoxOpenFor(st);
|
|
@@ -4175,6 +4057,7 @@
|
|
|
4175
4057
|
mainBtn.disabled = b.disabled;
|
|
4176
4058
|
mainAction = b.action;
|
|
4177
4059
|
mainHint.textContent = b.hint;
|
|
4060
|
+
reopenBtn.hidden = !reviseShownFor(st);
|
|
4178
4061
|
fitBox(msgInput);
|
|
4179
4062
|
fitCardBoxes();
|
|
4180
4063
|
}
|
|
@@ -4314,7 +4197,7 @@
|
|
|
4314
4197
|
poll();
|
|
4315
4198
|
}
|
|
4316
4199
|
function reattachLive(){
|
|
4317
|
-
if(sessionId || sessionLive) return;
|
|
4200
|
+
if(sessionId || sessionLive || resumeRun !== null) return;
|
|
4318
4201
|
authorGet("/api/author/live").then(function(j){
|
|
4319
4202
|
if(sessionId || sessionLive || !j || !j.ok || !j.sessionId || !j.state) return;
|
|
4320
4203
|
attachSession(j);
|
|
@@ -4356,13 +4239,97 @@
|
|
|
4356
4239
|
}
|
|
4357
4240
|
document.addEventListener("bareloop-reuse", function(e){ applyReuse(e.detail || {}); });
|
|
4358
4241
|
|
|
4242
|
+
// Edit in chat — the card opens filled from the SIGNED job with EVERY box open; the button stays the normal
|
|
4243
|
+
// [Start drafting] (startFrom / reuse state are never set: no borrowed track record). A live drafting session is
|
|
4244
|
+
// handled exactly as applyReuse does (clearStartFrom + openNewCard). A refused prefill opens nothing, error only.
|
|
4245
|
+
function applyEdit(d){
|
|
4246
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
4247
|
+
var c = (d.prefill || {}).card || {};
|
|
4248
|
+
clearStartFrom();
|
|
4249
|
+
openNewCard();
|
|
4250
|
+
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4251
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4252
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4253
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4254
|
+
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4255
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4256
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4257
|
+
originLine.textContent = d.origin || "";
|
|
4258
|
+
originLine.hidden = !d.origin;
|
|
4259
|
+
renderPicker();
|
|
4260
|
+
refreshModelStatus();
|
|
4261
|
+
refreshCapNote();
|
|
4262
|
+
refreshStartEnabled();
|
|
4263
|
+
}
|
|
4264
|
+
document.addEventListener("bareloop-edit", function(e){ applyEdit(e.detail || {}); });
|
|
4265
|
+
|
|
4266
|
+
// Resume — the card in RESUME mode: the signed job filled and locked, only Cap $ and Time cap open (prefilled from the
|
|
4267
|
+
// engine's own resume plan, the values the Job tab used to show). A live drafting session is handled exactly as
|
|
4268
|
+
// applyReuse does. A refused prefill opens nothing, error only.
|
|
4269
|
+
function applyResume(d){
|
|
4270
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
4271
|
+
var c = (d.prefill || {}).card || {};
|
|
4272
|
+
var r = d.resume || {};
|
|
4273
|
+
clearStartFrom();
|
|
4274
|
+
openNewCard();
|
|
4275
|
+
resumeRun = {runid: d.runid, noTime: typeof r.maxWallMin !== "number"};
|
|
4276
|
+
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4277
|
+
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4278
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4279
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4280
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4281
|
+
setVal("jf-judge", c.judgeExamples);
|
|
4282
|
+
setVal("jf-cap-money", r.budgetUsd);
|
|
4283
|
+
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
4284
|
+
document.getElementById("jf-cap-time").parentNode.hidden = resumeRun.noTime;
|
|
4285
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4286
|
+
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.";
|
|
4287
|
+
originLine.classList.add("res");
|
|
4288
|
+
originLine.hidden = false;
|
|
4289
|
+
var spent = (r.spentUsd === null || r.spentUsd === undefined) ? "unknown" : ((r.spendComplete ? "" : "at least ") + panelMoney(r.spentUsd));
|
|
4290
|
+
document.getElementById("chat-resume-spent").textContent = "spent so far " + spent +
|
|
4291
|
+
(typeof r.wallUsedMs === "number" && isFinite(r.wallUsedMs) ? " \u00B7 time used so far " + Math.ceil(r.wallUsedMs / 60000) + " min" : "");
|
|
4292
|
+
document.getElementById("chat-resume-box").hidden = false;
|
|
4293
|
+
document.getElementById("chat-clear-btn").hidden = true;
|
|
4294
|
+
["chat-main-btn", "chat-main-hint"].forEach(function(id){ document.getElementById(id).hidden = true; });
|
|
4295
|
+
document.querySelector(".chat-input-row").hidden = true;
|
|
4296
|
+
renderPicker();
|
|
4297
|
+
refreshModelStatus();
|
|
4298
|
+
refreshStartEnabled();
|
|
4299
|
+
}
|
|
4300
|
+
document.addEventListener("bareloop-resume", function(e){ applyResume(e.detail || {}); });
|
|
4301
|
+
document.getElementById("chat-resume-cancel").addEventListener("click", function(){ resetCard(); });
|
|
4302
|
+
var resumeSignBtn = document.getElementById("chat-resume-sign");
|
|
4303
|
+
function signResume(){
|
|
4304
|
+
var run = resumeRun;
|
|
4305
|
+
if(run === null) return;
|
|
4306
|
+
var err = document.getElementById("chat-resume-err");
|
|
4307
|
+
err.hidden = true;
|
|
4308
|
+
resumeSignBtn.disabled = true;
|
|
4309
|
+
var body = {budgetUsd: document.getElementById("jf-cap-money").value};
|
|
4310
|
+
if(!run.noTime){ body.maxWallMin = document.getElementById("jf-cap-time").value; }
|
|
4311
|
+
return authorPost("/api/runs/" + encodeURIComponent(run.runid) + "/resume", body).then(function(res){
|
|
4312
|
+
resumeSignBtn.disabled = false;
|
|
4313
|
+
if(res.body && res.body.ok){
|
|
4314
|
+
// 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
|
|
4315
|
+
resetCard();
|
|
4316
|
+
document.getElementById("tab-run").click();
|
|
4317
|
+
afterResumeRefresh(run.runid, 8);
|
|
4318
|
+
} else {
|
|
4319
|
+
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
4320
|
+
err.hidden = false;
|
|
4321
|
+
}
|
|
4322
|
+
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
4323
|
+
}
|
|
4324
|
+
resumeSignBtn.addEventListener("click", signResume);
|
|
4325
|
+
|
|
4359
4326
|
// build item 1 — the "lost answer" fix. ROOT CAUSE: `renderActions`
|
|
4360
4327
|
// (fed by the every-2s `poll()`) unconditionally overwrites
|
|
4361
4328
|
// `#chat-card-error` on every tick (`errEl.textContent = state.error ||
|
|
4362
|
-
// ""`), including the SAME tick the send/
|
|
4329
|
+
// ""`), including the SAME tick the send/sign handlers below
|
|
4363
4330
|
// used to call `poll()` in their own `.then` — so a refused/failed POST's
|
|
4364
4331
|
// error was wiped before a person could read it (often on the very next
|
|
4365
|
-
// line of the same callback). Compounding it, `send
|
|
4332
|
+
// line of the same callback). Compounding it, `send` cleared
|
|
4366
4333
|
// `msgInput.value` unconditionally, even on failure — so a rejected
|
|
4367
4334
|
// answer vanished from the box with no trace at all. Fixed by: (1) a
|
|
4368
4335
|
// dedicated `#chat-action-error` element only these handlers touch,
|
|
@@ -4423,20 +4390,20 @@
|
|
|
4423
4390
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
4424
4391
|
}
|
|
4425
4392
|
|
|
4426
|
-
|
|
4393
|
+
// hamr 2026-10-09 (as fwdloop's Revise): the draft is ended and the New job card opens with the SAME values in every
|
|
4394
|
+
// box, all editable; Start drafting then drafts fresh from whatever the boxes now say. No typed change request goes
|
|
4395
|
+
// to the model. The ended draft is an Abandon: money already spent stays booked.
|
|
4396
|
+
function doReopen(){
|
|
4427
4397
|
if(!sessionId) return;
|
|
4428
|
-
|
|
4429
|
-
authorPost("/api/author/" + sessionId + "/revise", {text: text}).then(function(r){
|
|
4398
|
+
authorPost("/api/author/" + sessionId + "/abandon", {}).then(function(r){
|
|
4430
4399
|
var o = chatPostOutcome(r);
|
|
4431
|
-
if(o.ok){
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
}
|
|
4437
|
-
poll();
|
|
4438
|
-
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
4400
|
+
if(!o.ok){ chatActionFailed(o.error); return; }
|
|
4401
|
+
openNewCard();
|
|
4402
|
+
chatActionOk();
|
|
4403
|
+
thread.innerHTML = '<div class="msg system">' + escapeXml("The boxes are open as you drafted them. Edit what you want, then Start drafting. The first draft's cost stays counted.") + "</div>";
|
|
4404
|
+
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not revised"); });
|
|
4439
4405
|
}
|
|
4406
|
+
reopenBtn.addEventListener("click", doReopen);
|
|
4440
4407
|
|
|
4441
4408
|
function doSign(){
|
|
4442
4409
|
if(!sessionId) return;
|
|
@@ -4468,7 +4435,6 @@
|
|
|
4468
4435
|
if(mainAction === "start") doStart();
|
|
4469
4436
|
else if(mainAction === "check-deps") doCheckDeps();
|
|
4470
4437
|
else if(mainAction === "send") doSend();
|
|
4471
|
-
else if(mainAction === "revise") doRevise();
|
|
4472
4438
|
else if(mainAction === "sign-prepare" || mainAction === "sign") doSign();
|
|
4473
4439
|
});
|
|
4474
4440
|
|