bareloop 0.38.0 → 0.40.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 +59 -0
- package/bareloop.context.md +87 -24
- package/package.json +3 -3
- package/src/authorflow.js +335 -59
- package/src/authoring.js +125 -3
- package/src/authorjob.js +93 -32
- package/src/authorreadout.js +498 -3
- package/src/authorrun.js +26 -6
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/calibrate.js +85 -11
- package/src/cardauthor.js +8 -2
- package/src/declaredclose.js +12 -5
- package/src/draftspend.js +6 -1
- package/src/interviewrun.js +56 -27
- package/src/job.js +38 -5
- package/src/jobblock.js +140 -0
- package/src/judged.js +323 -29
- package/src/kinds.js +13 -1
- package/src/ledger.js +1 -1
- package/src/panel/authorroutes.js +17 -11
- package/src/panel/authorsession.js +202 -75
- package/src/panel/index.html +302 -312
- package/src/panel/jobplan.js +107 -0
- package/src/panel/server.js +67 -13
- package/src/planrun.js +6 -5
- package/src/promptregisters.js +1 -1
- package/src/providers.js +111 -2
- package/src/reuse.js +1 -1
- package/src/root.js +1 -1
- package/src/run.js +3 -3
- package/src/selection.js +1 -1
- package/src/source.js +64 -1
- package/src/text.js +13 -0
- package/src/transport.js +1 -1
- package/src/trend.js +1 -0
- package/src/userrun.js +3 -3
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +30 -1
- package/types/authorjob.d.ts +52 -13
- package/types/authorreadout.d.ts +123 -1
- package/types/authorscout.d.ts +5 -3
- package/types/calibrate.d.ts +4 -1
- package/types/declaredclose.d.ts +5 -1
- package/types/draftspend.d.ts +8 -2
- package/types/job.d.ts +1 -1
- package/types/jobblock.d.ts +102 -0
- package/types/judged.d.ts +21 -2
- package/types/panel/authorsession.d.ts +57 -7
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +20 -0
- package/types/planrun.d.ts +1 -1
- package/types/providers.d.ts +52 -1
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
- package/types/trend.d.ts +1 -0
package/src/panel/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
--grey-dot:#878eac; --grey-bg:#20242f;
|
|
22
22
|
--cyan:#7dcfff; --cyan-bg:#182634;
|
|
23
23
|
--magenta:#bb9af7;
|
|
24
|
-
--accent:#7aa2f7;
|
|
24
|
+
--accent:#7aa2f7; --plan:#7aa2f7;
|
|
25
25
|
--btn-bg:#1f2335; --btn-text:#c0caf5;
|
|
26
26
|
--field-bg:var(--bg); --field-soft-bg:var(--bg); --field-border:var(--text-dim);
|
|
27
27
|
--primary-bg:#7aa2f7; --primary-text:#1a1b26; --primary-border:#7aa2f7;
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
--grey-dot:#626887; --grey-bg:#e2e2e8;
|
|
68
68
|
--cyan:#007197; --cyan-bg:#d6ecf1;
|
|
69
69
|
--magenta:#9854f1;
|
|
70
|
-
--accent:#2668c2;
|
|
70
|
+
--accent:#2668c2; --plan:#2668c2;
|
|
71
71
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
72
72
|
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
73
73
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
--grey-dot:#626887; --grey-bg:#e2e2e8;
|
|
83
83
|
--cyan:#007197; --cyan-bg:#d6ecf1;
|
|
84
84
|
--magenta:#9854f1;
|
|
85
|
-
--accent:#2668c2;
|
|
85
|
+
--accent:#2668c2; --plan:#2668c2;
|
|
86
86
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
87
87
|
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
88
88
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
@@ -121,6 +121,9 @@
|
|
|
121
121
|
[hidden]{display:none !important;}
|
|
122
122
|
.main{display:flex;flex:1;min-height:0;}
|
|
123
123
|
.left-pane{width:420px;flex:none;border-right:1px solid var(--border-strong);display:flex;flex-direction:column;background:var(--panel);}
|
|
124
|
+
/* hamr 2026-10-10: the left pane is 630px on wide screens only. 1200px is the floor: the right pane is then >=570px, never
|
|
125
|
+
narrower than the 480px it already had at 900px with the 420px pane (630 at 900 left it 270 and wrapped its tabs) */
|
|
126
|
+
@media (min-width: 1200px){ .left-pane{width:630px;} }
|
|
124
127
|
.right-pane{flex:1;min-width:0;display:flex;flex-direction:column;}
|
|
125
128
|
|
|
126
129
|
.tabrow{display:flex;border-bottom:1px solid var(--border-strong);background:var(--panel2);counter-reset:tabnum;}
|
|
@@ -311,9 +314,28 @@
|
|
|
311
314
|
.imported-box .summary-row{flex-wrap:wrap;}
|
|
312
315
|
.imported-box .sum-val{flex:1 1 180px;min-width:0;overflow-wrap:anywhere;}
|
|
313
316
|
|
|
314
|
-
.map-box{border:1px solid var(--border-strong);padding:10px;
|
|
317
|
+
.map-box{border:1px solid var(--border-strong);padding:10px;margin-top:16px;margin-bottom:14px;background:var(--panel2);}
|
|
315
318
|
.map-box::before{content:"┤ MAP ├";display:block;font-size:11px;color:var(--text-faint);letter-spacing:0.08em;margin:-2px 0 8px 0;}
|
|
316
|
-
|
|
319
|
+
/* ruling 1A: chips as wide as their own text, side by side, wrapping — a name is never truncated */
|
|
320
|
+
.map-chips{display:flex;flex-wrap:wrap;gap:8px 4px;align-items:flex-start;padding-left:1.4em;}
|
|
321
|
+
/* ruling A: a step + its trailing arrow are ONE unit (the arrow never wraps alone); the lead arrow of a wrapped line
|
|
322
|
+
hangs in the container's left padding (absolute, zero layout impact — toggling it can never move a chip) */
|
|
323
|
+
.map-item{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;}
|
|
324
|
+
.map-arrow{color:var(--text-dim);font-size:13px;flex:none;}
|
|
325
|
+
.map-lead{display:none;position:absolute;left:-1.3em;top:50%;transform:translateY(-50%);color:var(--text-dim);font-size:13px;}
|
|
326
|
+
.map-item.line-start .map-lead{display:block;}
|
|
327
|
+
.chip-retry{color:var(--text-dim);font-size:11px;}
|
|
328
|
+
.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;}
|
|
329
|
+
button.map-chip{cursor:pointer;}
|
|
330
|
+
.map-chip.s-done{border-color:var(--green);color:var(--text);} .map-chip.s-done .chip-sign{color:#2e7d32;}
|
|
331
|
+
.map-chip.s-running{border-color:var(--amber);color:var(--text);} .map-chip.s-running .chip-sign{color:#b8860b;}
|
|
332
|
+
.map-chip.s-stopped{border-color:var(--red);color:var(--text);} .map-chip.s-stopped .chip-sign{color:var(--red);}
|
|
333
|
+
.map-chip.s-died{border-color:var(--magenta);color:var(--text);} .map-chip.s-died .chip-sign{color:var(--magenta);}
|
|
334
|
+
.map-chip.retry{border-style:dashed;}
|
|
335
|
+
.map-chip.resumed-next{border-right-style:dotted;}
|
|
336
|
+
.map-chip .chip-blocked{color:var(--red);font-size:10px;}
|
|
337
|
+
.map-chip.s-running .chip-sign{animation:chip-pulse 1.2s infinite;}
|
|
338
|
+
@keyframes chip-pulse{0%,100%{opacity:1}50%{opacity:0.3}}
|
|
317
339
|
.map-key{display:flex;gap:14px;font-size:11px;color:var(--text-dim);margin-top:6px;flex-wrap:wrap;}
|
|
318
340
|
|
|
319
341
|
.step-card{position:relative;border:1px solid var(--border);padding:10px;margin-bottom:8px;background:var(--panel2);}
|
|
@@ -447,6 +469,41 @@
|
|
|
447
469
|
/* hamr 2026-10-05: the card's free-text boxes wrap like the ask box (one shared fitBox); a few rows, then they scroll inside */
|
|
448
470
|
/* same look as the card's one-line inputs (white when editable, ordinary 1px border) — a textarea has its own browser border otherwise */
|
|
449
471
|
.job-card textarea.jf-wrap{width:100%;box-sizing:border-box;font:inherit;padding:6px 8px;border:1px solid var(--border-strong);border-radius:0;background:var(--bg);color:var(--text);resize:vertical;overflow-y:auto;max-height:200px;}
|
|
472
|
+
/* P7 — numbered boxes (The job, Inputs): the number gutter sits OUTSIDE the box, left; a hidden mirror measures each
|
|
473
|
+
logical line's wrapped height so a number stays level with its line (docs/product/PANEL-BUILD.md, signed mock) */
|
|
474
|
+
.nbox{display:grid;grid-template-columns:22px minmax(0,1fr);column-gap:6px;align-items:stretch;}
|
|
475
|
+
/* contain:size keeps the gutter's own content out of the row height (the box alone decides it); its padding mirrors the
|
|
476
|
+
textarea's 6px + 1px border top AND bottom so both scroll the same distance and a number never drifts or falls out */
|
|
477
|
+
.nbox .gut{color:var(--text-faint);text-align:right;padding:7px 0;box-sizing:border-box;overflow:hidden;contain:size;font-size:inherit;line-height:1.5;}
|
|
478
|
+
.nbox .gut div{min-height:1.5em;}
|
|
479
|
+
.nbox-in{position:relative;min-width:0;}
|
|
480
|
+
/* the numbered boxes grow with their text up to 8 lines (8 x 1.5em + 6px padding x2 + 1px border x2), then scroll; fitBox reads this cap */
|
|
481
|
+
.job-card .nbox textarea.jf-wrap{display:block;line-height:1.5;max-height:calc(12em + 14px);}
|
|
482
|
+
.nbox-mir{position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;box-sizing:border-box;padding:6px 8px;font:inherit;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
483
|
+
.nbox-mir div{min-height:1.5em;}
|
|
484
|
+
/* P7 — the ONE job plan drawing (chat plan bubble + Job tab): job lines numbered, rule under, checks folded; only the marker is blue */
|
|
485
|
+
.jp{white-space:normal;font-size:13px;}
|
|
486
|
+
.jp-legend{font-size:12px;color:var(--text-faint);margin:0 0 8px;}
|
|
487
|
+
.jp-legend b,.jp .mk{color:var(--plan);font-weight:700;}
|
|
488
|
+
/* a wrapped job line hangs: its text lines up after the number, not under it (the signed mock's gutter shape) */
|
|
489
|
+
.jp .jl{margin-bottom:8px;overflow-wrap:anywhere;padding-left:30px;text-indent:-30px;}
|
|
490
|
+
.jp .jl .sub,.jp .jl .fold{padding-left:0;text-indent:0;}
|
|
491
|
+
.jp .num{display:inline-block;text-indent:0;width:22px;text-align:right;color:var(--text-faint);margin-right:8px;}
|
|
492
|
+
.jp .sub{color:var(--text-dim);padding-left:30px;}
|
|
493
|
+
.jp .fold{padding-left:30px;color:var(--text);}
|
|
494
|
+
.jp details.fold{padding-left:0;margin:6px 0;}
|
|
495
|
+
.jp details>summary{cursor:pointer;list-style:none;}
|
|
496
|
+
.jp details>summary::-webkit-details-marker{display:none;}
|
|
497
|
+
.jp details>summary::before{content:"\203A";color:var(--plan);font-weight:700;margin-right:6px;}
|
|
498
|
+
.jp details[open]>summary::before{content:"\2304";}
|
|
499
|
+
.jp details ul{margin:4px 0 0;padding-left:26px;color:var(--text-dim);}
|
|
500
|
+
.jp .older{font-size:12px;color:var(--text-faint);margin:0 0 8px;}
|
|
501
|
+
.jp .jp-sum{margin:0 0 8px;overflow-wrap:anywhere;}
|
|
502
|
+
.jp .jp-sum .lbl{display:inline-block;color:var(--text-faint);margin-right:8px;}
|
|
503
|
+
.jp-qs{margin:8px 0;overflow-wrap:anywhere;}
|
|
504
|
+
.jp-qs .lbl{display:inline-block;color:var(--text-faint);margin-right:8px;}
|
|
505
|
+
.jp-qs ul{margin:4px 0;padding-left:26px;}
|
|
506
|
+
.jp-goal{font-size:12px;color:var(--text-faint);margin-top:8px;}
|
|
450
507
|
/* hamr 2026-10-05: the focused/selected Chat-card field gets a blue border (the accent), no outline ring */
|
|
451
508
|
#panel-chat :is(input,select,textarea):focus{border-color:var(--accent);outline:none;}
|
|
452
509
|
/* hamr 2026-10-05: the ask box is dimmed until something asks for a reply (placeholder stays readable) */
|
|
@@ -564,29 +621,23 @@
|
|
|
564
621
|
<input id="jf-name" type="text" placeholder="kebab-case, unique">
|
|
565
622
|
</div>
|
|
566
623
|
<div class="field">
|
|
567
|
-
<label for="jf-
|
|
568
|
-
<
|
|
624
|
+
<label for="jf-job">The job</label>
|
|
625
|
+
<div class="nbox" data-testid="nbox-job">
|
|
626
|
+
<div class="gut" id="jf-job-gut" aria-hidden="true"></div>
|
|
627
|
+
<div class="nbox-in"><textarea class="jf-wrap" id="jf-job" rows="3" data-testid="jf-job" spellcheck="false"></textarea><div class="nbox-mir" id="jf-job-mir" aria-hidden="true"></div></div>
|
|
628
|
+
</div>
|
|
569
629
|
</div>
|
|
570
630
|
<div class="field">
|
|
571
|
-
<label for="jf-
|
|
572
|
-
<
|
|
631
|
+
<label for="jf-inputs">Inputs</label>
|
|
632
|
+
<div class="nbox" data-testid="nbox-inputs">
|
|
633
|
+
<div class="gut" id="jf-inputs-gut" aria-hidden="true"></div>
|
|
634
|
+
<div class="nbox-in"><textarea class="jf-wrap" id="jf-inputs" rows="2" data-testid="jf-inputs" spellcheck="false"></textarea><div class="nbox-mir" id="jf-inputs-mir" aria-hidden="true"></div></div>
|
|
635
|
+
</div>
|
|
573
636
|
</div>
|
|
574
637
|
<div class="field">
|
|
575
638
|
<label for="jf-dest">Destination</label>
|
|
576
639
|
<textarea class="jf-wrap" id="jf-dest" rows="1" placeholder="the write fence — where in the repo this run may change files"></textarea>
|
|
577
640
|
</div>
|
|
578
|
-
<div class="field">
|
|
579
|
-
<label for="jf-success">Success</label>
|
|
580
|
-
<textarea class="jf-wrap" id="jf-success" rows="2"></textarea>
|
|
581
|
-
</div>
|
|
582
|
-
<div class="field">
|
|
583
|
-
<label for="jf-guardrails">Guardrails</label>
|
|
584
|
-
<textarea class="jf-wrap" id="jf-guardrails" rows="2"></textarea>
|
|
585
|
-
</div>
|
|
586
|
-
<div class="field">
|
|
587
|
-
<label for="jf-judge">Judge examples <span class="hint" style="display:inline">(rubric only)</span></label>
|
|
588
|
-
<textarea class="jf-wrap" id="jf-judge" rows="2" disabled></textarea>
|
|
589
|
-
</div>
|
|
590
641
|
|
|
591
642
|
<div class="cap-row" data-testid="cap-row">
|
|
592
643
|
<div class="cap-field">
|
|
@@ -613,7 +664,7 @@
|
|
|
613
664
|
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. -->
|
|
614
665
|
<ol class="chat-steps" id="chat-progress-row" data-testid="chat-steps" aria-live="polite" hidden></ol>
|
|
615
666
|
|
|
616
|
-
<!-- build item 1 — a failed/rejected send, revise, sign-prepare,
|
|
667
|
+
<!-- build item 1 — a failed/rejected send, revise (reopen), sign-prepare,
|
|
617
668
|
sign or check-deps POST shows here and STAYS shown: it is set
|
|
618
669
|
only by this tab's own action handlers below and is never
|
|
619
670
|
touched by renderActions()/poll() (which own #chat-card-error
|
|
@@ -623,10 +674,12 @@
|
|
|
623
674
|
|
|
624
675
|
<div class="chat-input-row">
|
|
625
676
|
<label for="chat-msg" style="display:none">Message</label>
|
|
626
|
-
<textarea id="chat-msg" rows="2" placeholder="
|
|
677
|
+
<textarea id="chat-msg" rows="2" placeholder="Answer here when the draft asks you a question…" data-testid="chat-msg"></textarea>
|
|
627
678
|
</div>
|
|
628
679
|
<button class="btn primary wide" type="button" id="chat-main-btn" data-testid="chat-main" disabled>Start drafting</button>
|
|
629
680
|
<div class="hint" id="chat-main-hint" data-testid="chat-main-hint"></div>
|
|
681
|
+
<!-- hamr 2026-10-09: Revise = reopen the boxes, filled as drafted, and draft again. Shown only while a plan waits for the OK. -->
|
|
682
|
+
<button class="btn wide" type="button" id="chat-reopen-btn" data-testid="chat-reopen" hidden>Revise</button>
|
|
630
683
|
<div class="hint">Only your click signs. The chat can't.</div>
|
|
631
684
|
</section>
|
|
632
685
|
|
|
@@ -729,17 +782,16 @@
|
|
|
729
782
|
<h4 id="details-title">Job card — signed</h4>
|
|
730
783
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
731
784
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
785
|
+
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
786
|
+
<div class="field"><label>The job</label><div class="ro-value" id="details-plan" data-testid="details-plan">—</div></div>
|
|
787
|
+
<div class="field"><label>Inputs</label><div class="ro-value" id="details-inputs" data-testid="details-inputs">—</div></div>
|
|
788
|
+
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
789
|
+
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
732
790
|
<div class="field-row">
|
|
733
791
|
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
734
792
|
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
735
793
|
</div>
|
|
736
|
-
<div class="field"
|
|
737
|
-
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
738
|
-
<div class="field"><label>Success</label><div class="ro-value" id="details-success">—</div></div>
|
|
739
|
-
<div class="field"><label>Guardrails</label><div class="ro-value" id="details-guardrails">—</div></div>
|
|
740
|
-
<div class="field"><label>Source</label><div class="ro-value" id="details-source">—</div></div>
|
|
741
|
-
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
742
|
-
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
794
|
+
<div class="field"><label>Signed</label><div class="ro-value" id="details-signed" data-testid="details-signed">—</div></div>
|
|
743
795
|
<div class="hint" id="job-note"></div>
|
|
744
796
|
</div>
|
|
745
797
|
</section>
|
|
@@ -869,7 +921,6 @@
|
|
|
869
921
|
btn.setAttribute("aria-selected", "true");
|
|
870
922
|
var panel = document.getElementById(btn.getAttribute("aria-controls"));
|
|
871
923
|
if(panel){ panel.hidden = false; panel.classList.add("active"); }
|
|
872
|
-
if(btn.id === "tab-run"){ reflowMap(); }
|
|
873
924
|
});
|
|
874
925
|
});
|
|
875
926
|
}
|
|
@@ -884,33 +935,16 @@
|
|
|
884
935
|
});
|
|
885
936
|
}
|
|
886
937
|
|
|
887
|
-
// ---- step map renderer (
|
|
888
|
-
// buildStepMapSVG — ONE renderer, no second hand-authored map anywhere on this page) ----
|
|
889
|
-
function stepMapColors(state){
|
|
890
|
-
if(state === "done") return {stroke:"var(--green)", textFill:"#2e7d32"};
|
|
891
|
-
if(state === "running") return {stroke:"var(--amber)", textFill:"#b8860b"};
|
|
892
|
-
if(state === "stopped") return {stroke:"var(--red)", textFill:"var(--red)"};
|
|
893
|
-
// died (hamr's ruling B, 2026-09-25): distinct from stopped/red — no
|
|
894
|
-
// verdict was ever reached, so this box never shares the red look.
|
|
895
|
-
if(state === "died") return {stroke:"var(--magenta)", textFill:"var(--magenta)"};
|
|
896
|
-
return {stroke:"var(--border-strong)", textFill:"var(--text-faint)"};
|
|
897
|
-
}
|
|
938
|
+
// ---- step map renderer (buildStepMapHTML below — ONE renderer, no second hand-authored map anywhere on this page) ----
|
|
898
939
|
function escapeXml(s){
|
|
899
940
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
900
941
|
}
|
|
901
|
-
var stepMapUidCounter = 0;
|
|
902
|
-
var MAP_MARGIN = 10, MAP_GAP = 28;
|
|
903
|
-
var CHAR_W = 7.2;
|
|
904
|
-
var TITLE_HPAD = 16;
|
|
905
|
-
var MIN_BOX_W = 100;
|
|
906
|
-
var BOX_H = 60, BOX_H_2LINE = 78;
|
|
907
|
-
var ROW_GAP = 70;
|
|
908
942
|
// s.noNumber (F196): a synthetic placeholder box (e.g. "died during
|
|
909
943
|
// planning") is a LABEL, never a real step — it renders without the
|
|
910
944
|
// leading "<n> " step number a real step gets.
|
|
911
945
|
// item 4 (2026-09-25): each step box shows its attempts INLINE, e.g.
|
|
912
946
|
// "attempt 1 [✗] · attempt 2 [✓]" — appended to the box's own title text
|
|
913
|
-
// (the
|
|
947
|
+
// (the chip wraps long text inside itself the same way
|
|
914
948
|
// it already does for a long step id; no third renderer, no new box shape).
|
|
915
949
|
// item 1 (2026-09-27, panel P2 defect): an attempt's own outcome is null
|
|
916
950
|
// in exactly two situations — its window is still open on a LIVE run (the
|
|
@@ -935,9 +969,9 @@
|
|
|
935
969
|
if(g === "✗") return '<span class="glyph-red">' + g + '</span>';
|
|
936
970
|
return g;
|
|
937
971
|
}
|
|
938
|
-
// PLAIN TEXT ONLY — feeds the
|
|
939
|
-
//
|
|
940
|
-
//
|
|
972
|
+
// PLAIN TEXT ONLY — feeds the step map chip label (`stepTitleText`, escaped
|
|
973
|
+
// before it goes into the chip); a colour span here would render as literal
|
|
974
|
+
// "<span>" characters inside the map.
|
|
941
975
|
// {@link glyphSpan} (the P3 colour addition) is used only by the DOM
|
|
942
976
|
// part-card renderer below, never by this function.
|
|
943
977
|
function attemptsInlineText(attempts, boxIsLive){
|
|
@@ -1042,224 +1076,102 @@
|
|
|
1042
1076
|
function boxStatusText(box){
|
|
1043
1077
|
return box.status && box.status.word ? "[" + box.status.sign + "] " + boxStatusWord(box) : boxStatusWord(box);
|
|
1044
1078
|
}
|
|
1045
|
-
|
|
1079
|
+
// Ruling 1A (hamr, 2026-10-09): the step map is a row of CHIPS — each box as wide as its own text, flowing
|
|
1080
|
+
// side by side and wrapping to the next line. Plain HTML + CSS (.map-chips / .map-chip): the browser does the
|
|
1081
|
+
// wrapping, so there is no width arithmetic, no resize observer, and no horizontal scroll at phone width. A name
|
|
1082
|
+
// is never truncated (it wraps inside its chip when it is longer than the line).
|
|
1083
|
+
// Marks that were line styles on the old SVG are now border styles: a retried step has a DASHED border, and a step
|
|
1084
|
+
// whose leg ended there (the run resumed after it) has a DOTTED right edge. The sign + word come from
|
|
1085
|
+
// boxStatusText (src/panel/status.js's table) — the chip shows "[sign title]" and keeps the word in its tooltip.
|
|
1086
|
+
function buildStepMapHTML(steps){
|
|
1046
1087
|
var numIdx = stepNumberIndices(steps);
|
|
1047
|
-
var
|
|
1048
|
-
steps.
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
var
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1088
|
+
var last = steps.length - 1;
|
|
1089
|
+
var body = steps.map(function(s, idx){
|
|
1090
|
+
var sign = s.status && s.status.sign ? s.status.sign : "";
|
|
1091
|
+
var tries = boxRetryTry(s);
|
|
1092
|
+
var cls = "map-chip s-" + String(s.state).replace(/[^a-z-]/g, "") +
|
|
1093
|
+
(tries ? " retry" : "") + (s.resumedNext ? " resumed-next" : "");
|
|
1094
|
+
var label = escapeXml(stepTitleText(numIdx[idx], s));
|
|
1095
|
+
var blocked = s.blocked > 0 ? ' <span class="chip-blocked">' + s.blocked + ' blocked</span>' : '';
|
|
1096
|
+
var retry = tries ? '<span class="chip-retry">\u21bb' + tries + '</span> ' : '';
|
|
1097
|
+
var inner = '[' + retry + '<span class="chip-sign">' + escapeXml(sign) + '</span> ' + label + blocked + ']';
|
|
1098
|
+
var tip = ' title="' + escapeXml(boxStatusText(s)) + '"';
|
|
1099
|
+
var attrs = ' class="' + cls + '"' + tip + (s.resumedNext ? ' data-resumed="1"' : '') + (tries ? ' data-retry="' + tries + '"' : '');
|
|
1100
|
+
var chip = typeof s.partIndex === "number"
|
|
1101
|
+
? '<button type="button"' + attrs + ' data-part-index="' + s.partIndex + '">' + inner + '</button>'
|
|
1102
|
+
: '<span' + attrs + '>' + inner + '</span>';
|
|
1103
|
+
return '<span class="map-item" role="listitem">' +
|
|
1104
|
+
(idx > 0 ? '<span class="map-lead" aria-hidden="true">\u2192</span>' : '') +
|
|
1105
|
+
chip + (idx < last ? '<span class="map-arrow" aria-hidden="true">\u2192</span>' : '') + '</span>';
|
|
1106
|
+
}).join("");
|
|
1107
|
+
return '<div class="map-chips" role="list" aria-label="step map">' + body + '</div>';
|
|
1108
|
+
}
|
|
1109
|
+
// Ruling A: the first chip of each WRAPPED line shows a leading arrow. CSS cannot know which chip starts a line, so
|
|
1110
|
+
// after render/resize this compares each item's offsetTop with the previous item's (same line = same top). The lead
|
|
1111
|
+
// arrow is absolutely positioned (no layout impact), so toggling the class can never reflow a chip — no loop.
|
|
1112
|
+
// Takes anything with offsetTop + classList (real elements, or stubs in tests).
|
|
1113
|
+
function markLineStarts(items){
|
|
1114
|
+
for(var i = 0; i < items.length; i++){
|
|
1115
|
+
var starts = i > 0 && items[i].offsetTop > items[i - 1].offsetTop;
|
|
1116
|
+
if(starts) items[i].classList.add("line-start"); else items[i].classList.remove("line-start");
|
|
1072
1117
|
}
|
|
1073
|
-
if(!line1) return [text.slice(0, maxChars), text.slice(maxChars)];
|
|
1074
|
-
var line2 = words.slice(i).join(' ');
|
|
1075
|
-
return line2 ? [line1, line2] : [line1];
|
|
1076
1118
|
}
|
|
1077
|
-
// ONE shared rule for "does this box draw a retry
|
|
1119
|
+
// ONE shared rule for "does this box draw a retry mark, and what try number" — a multi-attempt
|
|
1078
1120
|
// box (a re-run step, or the fix loop; `run` on an old-shape spine falls in here too, naturally,
|
|
1079
|
-
// never special-cased) gets a dashed
|
|
1080
|
-
//
|
|
1121
|
+
// never special-cased) gets a dashed border; anything else (incl. every no-verdict kind, whose
|
|
1122
|
+
// attempts is already forced to [] upstream) gets 0.
|
|
1081
1123
|
function boxRetryTry(box){
|
|
1082
1124
|
var attempts = Array.isArray(box.attempts) ? box.attempts : [];
|
|
1083
1125
|
return attempts.length > 1 ? attempts.length : 0;
|
|
1084
1126
|
}
|
|
1085
|
-
//
|
|
1086
|
-
// start/control/end, and the "try N" label) is kept inside the box that
|
|
1087
|
-
// owns it — MIN_BOX_W (100) is comfortably wider than 2*RETRY_INSET, so
|
|
1088
|
-
// this never inverts the clamp range.
|
|
1089
|
-
var RETRY_INSET = 4;
|
|
1090
|
-
function clampRetryX(v, boxX, boxW){
|
|
1091
|
-
var lo = boxX + RETRY_INSET, hi = boxX + boxW - RETRY_INSET;
|
|
1092
|
-
return Math.min(hi, Math.max(lo, v));
|
|
1093
|
-
}
|
|
1094
|
-
function buildStepMapSVG(steps, availWidth){
|
|
1095
|
-
var n = steps.length;
|
|
1096
|
-
var naturalW = naturalBoxWidth(steps);
|
|
1097
|
-
var layout = computeMapLayout(n, availWidth, naturalW);
|
|
1098
|
-
var perRow = layout.perRow, boxW = layout.boxW, gap = MAP_GAP;
|
|
1099
|
-
var rows = Math.ceil(n / perRow);
|
|
1100
|
-
var margin = MAP_MARGIN;
|
|
1101
|
-
var numIdx = stepNumberIndices(steps);
|
|
1102
|
-
var titleLines = steps.map(function(s, idx){ return wrapTitleLines(stepTitleText(numIdx[idx], s), boxW); });
|
|
1103
|
-
var maxLines = titleLines.reduce(function(m, l){ return Math.max(m, l.length); }, 1);
|
|
1104
|
-
var boxH = maxLines > 1 ? BOX_H_2LINE : BOX_H;
|
|
1105
|
-
var rowH = boxH + ROW_GAP;
|
|
1106
|
-
var totalW = margin * 2 + perRow * boxW + (perRow - 1) * gap;
|
|
1107
|
-
var totalH = rows * rowH;
|
|
1108
|
-
var uid = "sm" + (stepMapUidCounter++);
|
|
1109
|
-
var body =
|
|
1110
|
-
'<defs>' +
|
|
1111
|
-
'<marker id="' + uid + '-arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">' +
|
|
1112
|
-
'<path d="M0,0 L6,3 L0,6 z" fill="var(--text-dim)"></path></marker>' +
|
|
1113
|
-
'<marker id="' + uid + '-arrow-grey" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">' +
|
|
1114
|
-
'<path d="M0,0 L6,3 L0,6 z" fill="var(--text-faint)"></path></marker>' +
|
|
1115
|
-
'</defs>';
|
|
1116
|
-
steps.forEach(function(s, idx){
|
|
1117
|
-
var row = Math.floor(idx / perRow);
|
|
1118
|
-
var col = idx % perRow;
|
|
1119
|
-
var rtl = (row % 2) === 1;
|
|
1120
|
-
var slotCol = rtl ? (perRow - 1 - col) : col;
|
|
1121
|
-
var x = margin + slotCol * (boxW + gap);
|
|
1122
|
-
var y = 30 + row * rowH;
|
|
1123
|
-
var colors = stepMapColors(s.state);
|
|
1124
|
-
var lines = titleLines[idx];
|
|
1125
|
-
var nextRow = idx < n - 1 ? Math.floor((idx + 1) / perRow) : null;
|
|
1126
|
-
var hasDrop = nextRow !== null && nextRow !== row;
|
|
1127
|
-
var stateY = y + boxH - 18;
|
|
1128
|
-
var partIdxAttr = typeof s.partIndex === "number" ? (' data-part-index="' + s.partIndex + '" style="cursor:pointer;"') : '';
|
|
1129
|
-
body += '<rect x="' + x + '" y="' + y + '" width="' + boxW + '" height="' + boxH +
|
|
1130
|
-
'" rx="0" fill="var(--panel2)" stroke="' + colors.stroke + '" stroke-width="2"' + partIdxAttr + '></rect>';
|
|
1131
|
-
if(s.blocked > 0){
|
|
1132
|
-
body += '<text x="' + (x + boxW - 6) + '" y="' + (y + 12) +
|
|
1133
|
-
'" text-anchor="end" font-size="10" fill="var(--red)">' + s.blocked + ' blocked</text>';
|
|
1134
|
-
}
|
|
1135
|
-
if(lines.length === 1){
|
|
1136
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + (y + 25) +
|
|
1137
|
-
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[0]) + '</text>';
|
|
1138
|
-
} else {
|
|
1139
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + (y + 17) +
|
|
1140
|
-
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[0]) + '</text>';
|
|
1141
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + (y + 29) +
|
|
1142
|
-
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[1]) + '</text>';
|
|
1143
|
-
}
|
|
1144
|
-
body += '<text x="' + (x + boxW / 2) + '" y="' + stateY +
|
|
1145
|
-
'" text-anchor="middle" font-size="10" fill="' + colors.textFill + '">' + escapeXml(boxStatusText(s)) + '</text>';
|
|
1146
|
-
// P2 (2026-09-27 ruling): a running box gets the mockup's pulsing
|
|
1147
|
-
// amber dot (design/panel-mockup.html ~line 1648) — same fixed color
|
|
1148
|
-
// and animation, never re-derived from the state's own stroke color.
|
|
1149
|
-
if(s.state === "running"){
|
|
1150
|
-
body += '<circle cx="' + (x + 20) + '" cy="' + (y + 12) + '" r="4" fill="#b8860b">' +
|
|
1151
|
-
'<animate attributeName="opacity" values="1;0.3;1" dur="1.2s" repeatCount="indefinite"></animate></circle>';
|
|
1152
|
-
}
|
|
1153
|
-
var retryTry = boxRetryTry(s);
|
|
1154
|
-
if(retryTry){
|
|
1155
|
-
// dashed self-loop under the box, same proportions as the design mockup's curve,
|
|
1156
|
-
// scaled to this box's own width. When this same box also carries the row-to-row
|
|
1157
|
-
// snake drop (which leaves from dead-center, x=cx), shift the loop's start away from
|
|
1158
|
-
// center by a fixed pixel offset so the two never begin at the same point.
|
|
1159
|
-
// item 2 (2026-09-27): at a narrow box width, subtracting that same
|
|
1160
|
-
// fixed pixel shift from proportionally-small x-fractions (e.g. the
|
|
1161
|
-
// curve's own endpoint at 2.8% of boxW) can push the coordinate past
|
|
1162
|
-
// the box's own left edge — the fixed shift was sized for desktop
|
|
1163
|
-
// widths and never scaled down with boxW. clampRetryX is the ONE
|
|
1164
|
-
// place every retry x-coordinate (curve start/control/end AND the
|
|
1165
|
-
// "try N" label) is kept inside [x + RETRY_INSET, x + boxW -
|
|
1166
|
-
// RETRY_INSET] — never re-clamped ad hoc per coordinate — so the
|
|
1167
|
-
// whole loop and its label always stay inside the box that owns it,
|
|
1168
|
-
// at every width, while keeping the mockup's proportions untouched
|
|
1169
|
-
// wherever they already fit (every desktop-width box already fits).
|
|
1170
|
-
var retryShift = hasDrop ? 14 : 0;
|
|
1171
|
-
var bottom = y + boxH;
|
|
1172
|
-
var sx = clampRetryX(x + boxW * 0.50 - retryShift, x, boxW);
|
|
1173
|
-
var sy = bottom + 5;
|
|
1174
|
-
var c1x = clampRetryX(x + boxW * 0.278 - retryShift, x, boxW), c1y = bottom + 30;
|
|
1175
|
-
var c2x = clampRetryX(x + boxW * 0.167 - retryShift, x, boxW), c2y = bottom + 30;
|
|
1176
|
-
var ex = clampRetryX(x + boxW * 0.028 - retryShift, x, boxW), ey = bottom + 2;
|
|
1177
|
-
var labelX = clampRetryX(x + boxW * 0.267 - retryShift, x, boxW);
|
|
1178
|
-
body += '<path d="M ' + sx + ' ' + sy + ' C ' + c1x + ' ' + c1y + ', ' + c2x + ' ' + c2y + ', ' + ex + ' ' + ey +
|
|
1179
|
-
'" fill="none" stroke="var(--text-faint)" stroke-width="1.5" stroke-dasharray="3,3" marker-end="url(#' + uid + '-arrow-grey)"></path>';
|
|
1180
|
-
body += '<text x="' + labelX + '" y="' + (bottom + 32) +
|
|
1181
|
-
'" text-anchor="middle" font-size="10" fill="var(--text-faint)">try ' + retryTry + '</text>';
|
|
1182
|
-
}
|
|
1183
|
-
if(idx < n - 1){
|
|
1184
|
-
// P5-R: where a leg ended and the next leg picked up, the connector is DOTTED (and says so) —
|
|
1185
|
-
// one run, one map; the dotted line is the only mark of the resume.
|
|
1186
|
-
var dash = s.resumedNext ? ' stroke-dasharray="2,4" data-resumed="1"' : '';
|
|
1187
|
-
if(nextRow === row){
|
|
1188
|
-
var midY = y + boxH / 2;
|
|
1189
|
-
if(!rtl){
|
|
1190
|
-
var nx = x + boxW + gap;
|
|
1191
|
-
body += '<line x1="' + (x + boxW) + '" y1="' + midY + '" x2="' + (nx - 2) + '" y2="' + midY +
|
|
1192
|
-
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1193
|
-
} else {
|
|
1194
|
-
var px = x - gap - boxW;
|
|
1195
|
-
body += '<line x1="' + x + '" y1="' + midY + '" x2="' + (px + boxW + 2) + '" y2="' + midY +
|
|
1196
|
-
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1197
|
-
}
|
|
1198
|
-
} else {
|
|
1199
|
-
var cx = x + boxW / 2;
|
|
1200
|
-
body += '<line x1="' + cx + '" y1="' + (y + boxH) + '" x2="' + cx + '" y2="' + (y + rowH - 2) +
|
|
1201
|
-
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
return '<svg width="' + totalW + '" height="' + totalH + '" viewBox="0 0 ' + totalW + ' ' + totalH + '" role="img" aria-label="step map">' + body + '</svg>';
|
|
1206
|
-
}
|
|
1207
|
-
// the line-style key under the MAP (hamr 2026-10-04): only the two line styles, never a sign legend
|
|
1127
|
+
// the line-style key under the MAP (hamr 2026-10-04): only the two border styles, never a sign legend
|
|
1208
1128
|
function stepMapKeyHTML(steps){
|
|
1209
1129
|
var resumed = (steps || []).some(function(b){ return b.resumedNext; });
|
|
1210
1130
|
return '<div class="map-key" data-testid="map-key">' +
|
|
1211
|
-
'<span
|
|
1212
|
-
(resumed ? '<span
|
|
1131
|
+
'<span>\u21bbN = took N tries (dashed box)</span>' +
|
|
1132
|
+
(resumed ? '<span>dotted edge = resumed after</span>' : '') +
|
|
1213
1133
|
'</div>';
|
|
1214
1134
|
}
|
|
1215
|
-
var lastSteps = null;
|
|
1216
|
-
var lastWidth = null;
|
|
1217
|
-
function mapAvailWidth(wrapper){
|
|
1218
|
-
if(!wrapper) return null;
|
|
1219
|
-
var w = wrapper.getBoundingClientRect().width;
|
|
1220
|
-
if(!w) return null;
|
|
1221
|
-
return Math.max(0, w - 20);
|
|
1222
|
-
}
|
|
1223
1135
|
function renderStepMap(steps){
|
|
1224
1136
|
var mount = document.getElementById("map-mount");
|
|
1225
1137
|
var wrapper = document.getElementById("map-box");
|
|
1226
|
-
lastSteps = steps;
|
|
1227
1138
|
if(!steps || !steps.length){
|
|
1228
1139
|
mount.innerHTML = "";
|
|
1229
1140
|
wrapper.hidden = true;
|
|
1230
1141
|
return;
|
|
1231
1142
|
}
|
|
1232
1143
|
wrapper.hidden = false;
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
// build item B: a map box click jumps to the Audit tab, that same part
|
|
1144
|
+
mount.innerHTML = buildStepMapHTML(steps) + stepMapKeyHTML(steps);
|
|
1145
|
+
layoutMap();
|
|
1146
|
+
// build item B: a map chip click jumps to the Audit tab, that same part
|
|
1237
1147
|
// expanded and scrolled into view — same click-through as the card list,
|
|
1238
|
-
// sharing partIndex so the two can never disagree about which row a
|
|
1239
|
-
// maps to.
|
|
1240
|
-
//
|
|
1241
|
-
mount.querySelectorAll("
|
|
1242
|
-
|
|
1243
|
-
var idx = Number(
|
|
1148
|
+
// sharing partIndex so the two can never disagree about which row a chip
|
|
1149
|
+
// maps to. Bound once per render (mount.innerHTML above already wiped any
|
|
1150
|
+
// previous listeners' targets along with the old markup).
|
|
1151
|
+
mount.querySelectorAll(".map-chip[data-part-index]").forEach(function(chip){
|
|
1152
|
+
chip.addEventListener("click", function(){
|
|
1153
|
+
var idx = Number(chip.getAttribute("data-part-index"));
|
|
1244
1154
|
document.getElementById("tab-audit").click();
|
|
1245
1155
|
openAuditPart(idx);
|
|
1246
1156
|
});
|
|
1247
1157
|
});
|
|
1248
1158
|
}
|
|
1249
|
-
function
|
|
1250
|
-
var
|
|
1251
|
-
if(!
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1159
|
+
function layoutMap(){
|
|
1160
|
+
var box = document.getElementById("map-box");
|
|
1161
|
+
if(!box || box.hidden) return;
|
|
1162
|
+
markLineStarts(Array.prototype.slice.call(box.querySelectorAll(".map-item")));
|
|
1163
|
+
}
|
|
1164
|
+
var layoutTimer = null;
|
|
1165
|
+
function layoutMapSoon(){
|
|
1166
|
+
if(layoutTimer) clearTimeout(layoutTimer);
|
|
1167
|
+
layoutTimer = setTimeout(function(){ layoutTimer = null; layoutMap(); }, 60);
|
|
1256
1168
|
}
|
|
1257
1169
|
if(typeof ResizeObserver !== "undefined"){
|
|
1258
|
-
var
|
|
1259
|
-
var
|
|
1260
|
-
if(
|
|
1170
|
+
var mapRo = new ResizeObserver(layoutMapSoon);
|
|
1171
|
+
var mapBoxEl = document.getElementById("map-box");
|
|
1172
|
+
if(mapBoxEl) mapRo.observe(mapBoxEl);
|
|
1261
1173
|
} else {
|
|
1262
|
-
window.addEventListener("resize",
|
|
1174
|
+
window.addEventListener("resize", layoutMapSoon);
|
|
1263
1175
|
}
|
|
1264
1176
|
|
|
1265
1177
|
// F196: a `synthetic` step (e.g. the "died during planning" placeholder
|
|
@@ -2110,8 +2022,8 @@
|
|
|
2110
2022
|
var nr = "not recorded";
|
|
2111
2023
|
renderJob({
|
|
2112
2024
|
job: v.job, resolved: false, checkType: v.checkType || nr, model: v.model || nr,
|
|
2113
|
-
goal: v.goal || nr,
|
|
2114
|
-
|
|
2025
|
+
goal: v.goal || nr, plan: v.plan || null, inputs: v.inputs || null, signed: null,
|
|
2026
|
+
destination: nr, tools: nr,
|
|
2115
2027
|
budgetUsd: (typeof v.budgetUsd === "number") ? v.budgetUsd : null,
|
|
2116
2028
|
maxWallMs: (typeof v.maxWallMs === "number") ? v.maxWallMs : null,
|
|
2117
2029
|
note: "imported · view only — the exported job's own spec",
|
|
@@ -3247,6 +3159,35 @@
|
|
|
3247
3159
|
});
|
|
3248
3160
|
});
|
|
3249
3161
|
|
|
3162
|
+
// ---- the ONE job plan drawing (P7): used by the chat's plan bubble AND the Job tab. `plan` is the server's model
|
|
3163
|
+
// (src/panel/jobplan.js): numbered lines with their rule, the checks under the line each serves (folded), the AI's own
|
|
3164
|
+
// "Not checked", the always-on guards. Only the marker is blue (--plan). Returns HTML (every text is escaped).
|
|
3165
|
+
function renderJobPlan(plan){
|
|
3166
|
+
var h = '<div class="jp" data-testid="job-plan">' + (plan.older ? '<div class="older" data-testid="plan-older">older job \u2014 numbered lines were not saved</div>' : "") + '<div class="jp-legend"><b>\u203A</b> = the approved plan</div>' +
|
|
3167
|
+
(plan.goal && !plan.older ? '<div class="jp-sum" data-testid="plan-goal"><span class="lbl">AI\'s summary</span>' + escapeXml(plan.goal) + "</div>" : "");
|
|
3168
|
+
var checkLine = function(c){
|
|
3169
|
+
return '<div class="fold" data-testid="plan-check"><span class="mk">\u203A</span> check \u00B7 ' + escapeXml(c.name) + (c.cls ? " \u00B7 " + escapeXml(c.cls) : "") + "</div>";
|
|
3170
|
+
};
|
|
3171
|
+
(plan.lines || []).forEach(function(l){
|
|
3172
|
+
h += '<div class="jl" data-testid="plan-line"><span class="num">' + escapeXml(String(l.n)) + "</span>" + escapeXml(l.text);
|
|
3173
|
+
if(l.rule) h += '<div class="sub">~ ' + escapeXml(l.rule) + "</div>";
|
|
3174
|
+
(l.checks || []).forEach(function(c){ h += checkLine(c); });
|
|
3175
|
+
(l.refused || []).forEach(function(r){ h += '<div class="fold" data-testid="plan-refused"><span class="mk">\u203A</span> not checked \u00B7 ' + escapeXml(r) + "</div>"; });
|
|
3176
|
+
h += "</div>";
|
|
3177
|
+
});
|
|
3178
|
+
if((plan.loose || []).length){
|
|
3179
|
+
h += '<div class="jl"><span class="num"></span>other checks' + plan.loose.map(checkLine).join("") + "</div>";
|
|
3180
|
+
}
|
|
3181
|
+
var fold = function(title, items, testid){
|
|
3182
|
+
if(!items || !items.length) return "";
|
|
3183
|
+
return '<details class="fold" data-testid="' + testid + '"><summary>' + escapeXml(title) + " \u00B7 " + items.length + "</summary><ul>" +
|
|
3184
|
+
items.map(function(x){ return "<li>" + escapeXml(x) + "</li>"; }).join("") + "</ul></details>";
|
|
3185
|
+
};
|
|
3186
|
+
h += fold("Not checked (the AI's own reading)", plan.notChecked, "plan-notchecked");
|
|
3187
|
+
h += fold("Always on", plan.alwaysOn, "plan-alwayson");
|
|
3188
|
+
return h + "</div>";
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3250
3191
|
// ---- Job tab ----
|
|
3251
3192
|
function renderJob(job){
|
|
3252
3193
|
var empty = document.getElementById("job-empty");
|
|
@@ -3262,10 +3203,14 @@
|
|
|
3262
3203
|
var descField = document.getElementById("details-description-field");
|
|
3263
3204
|
if(job.description){ descField.hidden = false; document.getElementById("details-description").textContent = job.description; }
|
|
3264
3205
|
else { descField.hidden = true; }
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
document.getElementById("details-
|
|
3268
|
-
|
|
3206
|
+
// P7: the numbered job lines, their rules and checks — the SAME drawing the chat's plan bubble uses (renderJobPlan)
|
|
3207
|
+
var plan = job.plan || {lines: [{n: 1, text: job.goal || "not recorded", rule: "", checks: [], refused: []}], loose: [], notChecked: [], alwaysOn: [], older: true};
|
|
3208
|
+
document.getElementById("details-plan").innerHTML = renderJobPlan(plan);
|
|
3209
|
+
var ins = Array.isArray(job.inputs) ? job.inputs : [];
|
|
3210
|
+
document.getElementById("details-inputs").innerHTML = ins.length
|
|
3211
|
+
? ins.map(function(i){ return '<div><span class="num-in">' + escapeXml(String(i.n)) + "</span> " + escapeXml(i.label + ": " + i.value) + "</div>"; }).join("")
|
|
3212
|
+
: "not recorded";
|
|
3213
|
+
document.getElementById("details-signed").textContent = job.signed || "not recorded";
|
|
3269
3214
|
document.getElementById("details-dest").textContent = job.destination;
|
|
3270
3215
|
document.getElementById("details-tools").textContent = job.tools;
|
|
3271
3216
|
document.getElementById("details-cap-money").textContent = panelMoney(job.budgetUsd);
|
|
@@ -3337,7 +3282,7 @@
|
|
|
3337
3282
|
// drives both fetches — never two independent timers that could race
|
|
3338
3283
|
// each other. ----
|
|
3339
3284
|
//
|
|
3340
|
-
// F195 (docs/
|
|
3285
|
+
// F195 (docs/wiki/FINDINGS.md): /api/runs re-replays every spine on
|
|
3341
3286
|
// every call (~457ms CPU at 250 rows). hamr's ruling B, 2026-09-27:
|
|
3342
3287
|
// "refresh less often, up to 30s at most, choose the lesser when
|
|
3343
3288
|
// possible" — the Runs list polls every 10s (RUNS_LIST_POLL_MS) instead
|
|
@@ -3675,6 +3620,7 @@
|
|
|
3675
3620
|
var msgInput = document.getElementById("chat-msg");
|
|
3676
3621
|
var mainBtn = document.getElementById("chat-main-btn");
|
|
3677
3622
|
var mainHint = document.getElementById("chat-main-hint");
|
|
3623
|
+
var reopenBtn = document.getElementById("chat-reopen-btn");
|
|
3678
3624
|
var mainAction = "none";
|
|
3679
3625
|
var lastState = null;
|
|
3680
3626
|
var startOk = false;
|
|
@@ -3712,10 +3658,10 @@
|
|
|
3712
3658
|
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3713
3659
|
var originLine = document.getElementById("chat-origin-line");
|
|
3714
3660
|
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3715
|
-
var LOCKED_IDS = ["jf-name", "jf-
|
|
3661
|
+
var LOCKED_IDS = ["jf-name", "jf-job"];
|
|
3716
3662
|
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3717
3663
|
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3718
|
-
var OPEN_IDS = ["jf-
|
|
3664
|
+
var OPEN_IDS = ["jf-inputs", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3719
3665
|
var reuseOn = false;
|
|
3720
3666
|
// Resume mode (hamr 2026-10-07): the card shows the signed job of one run, every box locked except the two caps.
|
|
3721
3667
|
// `resumeRun` = {runid, noTime} while it is on, else null. Never set together with a drafting session or a reuse pick.
|
|
@@ -3731,14 +3677,14 @@
|
|
|
3731
3677
|
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3732
3678
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3733
3679
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3734
|
-
if(!reuseOn && !live && !resuming){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3735
3680
|
}
|
|
3736
3681
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3737
3682
|
function clearStartFrom(keepBoxes){
|
|
3738
3683
|
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes — except when a Check type
|
|
3739
3684
|
// radio click leaves reuse (keepBoxes): a radio never blanks what the person has on the card
|
|
3740
3685
|
if(startFrom && !keepBoxes){
|
|
3741
|
-
|
|
3686
|
+
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3687
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
3742
3688
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3743
3689
|
}
|
|
3744
3690
|
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
@@ -3750,7 +3696,7 @@
|
|
|
3750
3696
|
// ---- Reuse workflow picker (hamr 2026-10-06): Check type is ONE radio group of three; "Reuse workflow" shows a search
|
|
3751
3697
|
// box over the jobs that have a green local run (GET /api/author/reuse-jobs), a pick fills the card through the SAME
|
|
3752
3698
|
// 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-
|
|
3699
|
+
var CARD_BOX_IDS = ["jf-name", "jf-job", "jf-inputs", "jf-dest", "jf-cap-money", "jf-cap-time"];
|
|
3754
3700
|
var rwBox = document.getElementById("rw");
|
|
3755
3701
|
var rwSearch = document.getElementById("rw-search");
|
|
3756
3702
|
var rwQ = document.getElementById("rw-q");
|
|
@@ -3767,9 +3713,11 @@
|
|
|
3767
3713
|
}
|
|
3768
3714
|
function setVerdict(v){
|
|
3769
3715
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === v); });
|
|
3716
|
+
setJobPlaceholders(v);
|
|
3770
3717
|
}
|
|
3771
3718
|
function clearCardBoxes(){
|
|
3772
3719
|
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3720
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
3773
3721
|
document.getElementById("jf-cap-note").textContent = "";
|
|
3774
3722
|
}
|
|
3775
3723
|
// ONE function shows/hides the picker: visible only while the radio is on Reuse workflow; the search box before a
|
|
@@ -3817,7 +3765,6 @@
|
|
|
3817
3765
|
function openPicker(){
|
|
3818
3766
|
rwQ.value = ""; rwHl = 0;
|
|
3819
3767
|
rwJobs = null; rwFailed = false; // a fresh open shows "Loading…", never the previous list
|
|
3820
|
-
document.getElementById("jf-judge").disabled = true;
|
|
3821
3768
|
renderPicker();
|
|
3822
3769
|
renderReuseList();
|
|
3823
3770
|
loadReuseJobs();
|
|
@@ -3858,7 +3805,7 @@
|
|
|
3858
3805
|
card.hidden = false;
|
|
3859
3806
|
originLine.hidden = true;
|
|
3860
3807
|
exitResumeMode();
|
|
3861
|
-
thread.innerHTML = "";
|
|
3808
|
+
thread.innerHTML = ""; lastMsgSig = null;
|
|
3862
3809
|
errEl.textContent = "";
|
|
3863
3810
|
actionErrEl.textContent = "";
|
|
3864
3811
|
sessionId = null;
|
|
@@ -3888,7 +3835,6 @@
|
|
|
3888
3835
|
function resetCard(){
|
|
3889
3836
|
clearCardBoxes();
|
|
3890
3837
|
setVerdict("deterministic");
|
|
3891
|
-
document.getElementById("jf-judge").disabled = true;
|
|
3892
3838
|
modelSelect.selectedIndex = 0;
|
|
3893
3839
|
msgInput.value = "";
|
|
3894
3840
|
rwQ.value = "";
|
|
@@ -3917,12 +3863,9 @@
|
|
|
3917
3863
|
checkType: (verdictValue() === "reuse" ? reuseCheckType : verdictValue()) === "rubric" ? "rubric" : "deterministic",
|
|
3918
3864
|
model: document.getElementById("jf-model").value,
|
|
3919
3865
|
jobName: document.getElementById("jf-name").value.trim(),
|
|
3920
|
-
|
|
3921
|
-
|
|
3866
|
+
jobText: document.getElementById("jf-job").value.trim(),
|
|
3867
|
+
inputs: document.getElementById("jf-inputs").value.trim(),
|
|
3922
3868
|
destination: document.getElementById("jf-dest").value.trim(),
|
|
3923
|
-
success: document.getElementById("jf-success").value.trim(),
|
|
3924
|
-
guardrails: document.getElementById("jf-guardrails").value.trim(),
|
|
3925
|
-
judgeExamples: document.getElementById("jf-judge").value.trim(),
|
|
3926
3869
|
capUsd: moneyCap,
|
|
3927
3870
|
maxWallMs: timeCapMin === "" ? undefined : Math.round(Number(timeCapMin) * 60000),
|
|
3928
3871
|
};
|
|
@@ -3971,7 +3914,7 @@
|
|
|
3971
3914
|
setVerdict(v);
|
|
3972
3915
|
renderPicker();
|
|
3973
3916
|
}
|
|
3974
|
-
|
|
3917
|
+
setJobPlaceholders(v);
|
|
3975
3918
|
refreshStartEnabled();
|
|
3976
3919
|
}
|
|
3977
3920
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){
|
|
@@ -4082,11 +4025,23 @@
|
|
|
4082
4025
|
if(role === "system") return "";
|
|
4083
4026
|
return "bareloop";
|
|
4084
4027
|
}
|
|
4028
|
+
// the thread is redrawn only when its messages changed: a 2 s poll tick must not close a fold the person just opened
|
|
4029
|
+
var lastMsgSig = null;
|
|
4085
4030
|
function renderMessages(state){
|
|
4031
|
+
var sig = JSON.stringify(state.messages);
|
|
4032
|
+
if(sig === lastMsgSig) return;
|
|
4033
|
+
lastMsgSig = sig;
|
|
4086
4034
|
thread.innerHTML = state.messages.map(function(m){
|
|
4087
4035
|
var cls = m.role === "you" ? "you" : (m.role === "system" ? "system" : "bot");
|
|
4088
4036
|
var who = chatWhoLabel(m.role);
|
|
4089
4037
|
var whoHtml = who ? ('<div class="who">' + escapeXml(who) + '</div>') : "";
|
|
4038
|
+
if(m.kind === "plan" && m.plan){
|
|
4039
|
+
// P7: the confirm turn's plan is drawn by the one job-plan renderer (the Job tab uses the same)
|
|
4040
|
+
var qs = (m.plan.questions || []).length
|
|
4041
|
+
? '<div class="jp-qs" data-testid="plan-questions"><span class="lbl">AI\'s open questions</span><ul>' +
|
|
4042
|
+
m.plan.questions.map(function(q){ return "<li>" + escapeXml(q) + "</li>"; }).join("") + "</ul><b>Revise if you need to.</b></div>" : "";
|
|
4043
|
+
return '<div class="msg ' + cls + '" data-testid="plan-bubble">' + whoHtml + renderJobPlan(m.plan) + qs + '<div class="jp-goal">' + escapeXml(m.text) + "</div></div>";
|
|
4044
|
+
}
|
|
4090
4045
|
return '<div class="msg ' + cls + '">' + whoHtml + escapeXml(m.text) + '</div>';
|
|
4091
4046
|
}).join("");
|
|
4092
4047
|
thread.scrollTop = thread.scrollHeight;
|
|
@@ -4127,26 +4082,20 @@
|
|
|
4127
4082
|
}
|
|
4128
4083
|
|
|
4129
4084
|
// hamr's ruling 2026-10-05: ONE wide button below the message box. Its wording, action and enabled state are ONE
|
|
4130
|
-
// pure function of (state, typed-text-empty, reuse); state = {session, startOk, phase, pendingAsk,
|
|
4131
|
-
// signClicked, specHash}. Actions: start | check-deps | send |
|
|
4085
|
+
// pure function of (state, typed-text-empty, reuse); state = {session, startOk, phase, pendingAsk,
|
|
4086
|
+
// signClicked, specHash}. Actions: start | check-deps | send | sign-prepare | sign | none.
|
|
4132
4087
|
function mainButtonFor(st, textEmpty, reuse){
|
|
4133
4088
|
var ask = st.pendingAsk;
|
|
4134
4089
|
var atMenu = !!ask && ask.kind === "menu";
|
|
4135
|
-
var
|
|
4136
|
-
var hint = atMenu ? left + (left === 1 ? " change left" : " changes left") : "";
|
|
4090
|
+
var hint = "";
|
|
4137
4091
|
if(!st.session){
|
|
4138
4092
|
return {text: reuse ? "Sign & run" : "Start drafting", action: "start", disabled: !st.startOk, hint: ""};
|
|
4139
4093
|
}
|
|
4140
4094
|
if(ask && ask.kind === "install-needed"){
|
|
4141
4095
|
return {text: "Check again", action: "check-deps", disabled: false, hint: ""};
|
|
4142
4096
|
}
|
|
4143
|
-
if(ask && !textEmpty){
|
|
4144
|
-
return atMenu
|
|
4145
|
-
? {text: "Send", action: "revise", disabled: left <= 0, hint: hint}
|
|
4146
|
-
: {text: "Send", action: "send", disabled: false, hint: ""};
|
|
4147
|
-
}
|
|
4148
4097
|
if(ask && !atMenu){
|
|
4149
|
-
return {text: "Send", action: "send", disabled:
|
|
4098
|
+
return {text: "Send", action: "send", disabled: textEmpty, hint: ""};
|
|
4150
4099
|
}
|
|
4151
4100
|
if(atMenu || st.phase === "prepared"){
|
|
4152
4101
|
if(st.phase === "prepared" && st.signClicked){
|
|
@@ -4156,16 +4105,21 @@
|
|
|
4156
4105
|
}
|
|
4157
4106
|
return {text: "Sign & run", action: "none", disabled: true, hint: ""};
|
|
4158
4107
|
}
|
|
4159
|
-
// pure: the ask box takes typed text only when the session has a pending ask that accepts it (the plan
|
|
4160
|
-
//
|
|
4108
|
+
// pure: the ask box takes typed text only when the session has a pending ask that accepts it (a question the plan
|
|
4109
|
+
// raised); not with no session, no ask, while the plan waits for the OK (that is Sign & run or Revise, never typed
|
|
4110
|
+
// text), or while waiting on an install
|
|
4161
4111
|
function askBoxOpenFor(st){
|
|
4162
|
-
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind !== "install-needed";
|
|
4112
|
+
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind !== "install-needed" && st.pendingAsk.kind !== "menu";
|
|
4113
|
+
}
|
|
4114
|
+
// pure: Revise shows only while a plan waits for the OK
|
|
4115
|
+
function reviseShownFor(st){
|
|
4116
|
+
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind === "menu";
|
|
4163
4117
|
}
|
|
4164
4118
|
function renderMain(){
|
|
4165
4119
|
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce};
|
|
4166
4120
|
if(sessionLive && lastState){
|
|
4167
4121
|
st.phase = lastState.phase; st.pendingAsk = lastState.pendingAsk;
|
|
4168
|
-
st.
|
|
4122
|
+
st.specHash = lastState.specHash;
|
|
4169
4123
|
}
|
|
4170
4124
|
// hamr 2026-10-05: the ask box is open only while something asks for a reply; otherwise dimmed and emptied
|
|
4171
4125
|
var open = askBoxOpenFor(st);
|
|
@@ -4176,26 +4130,66 @@
|
|
|
4176
4130
|
mainBtn.disabled = b.disabled;
|
|
4177
4131
|
mainAction = b.action;
|
|
4178
4132
|
mainHint.textContent = b.hint;
|
|
4133
|
+
reopenBtn.hidden = !reviseShownFor(st);
|
|
4179
4134
|
fitBox(msgInput);
|
|
4180
4135
|
fitCardBoxes();
|
|
4181
4136
|
}
|
|
4182
4137
|
msgInput.addEventListener("input", renderMain);
|
|
4138
|
+
// P7 — the numbered boxes. A plain job line is numbered 1, 2, 3; a line starting `~` is a rule for the line above and has no
|
|
4139
|
+
// number; Inputs numbers every line; empty lines have none. A hidden mirror (same font/width/padding) measures each logical
|
|
4140
|
+
// line's wrapped height so every number stays level with its own line — the same rules src/jobblock.js parses by.
|
|
4141
|
+
var NUMBERED_IDS = ["jf-job", "jf-inputs"];
|
|
4142
|
+
var FIT_IDS = ["jf-job", "jf-inputs", "jf-dest"];
|
|
4143
|
+
function gutterSync(id){
|
|
4144
|
+
var ta = document.getElementById(id), gut = document.getElementById(id + "-gut"), mir = document.getElementById(id + "-mir");
|
|
4145
|
+
if(!ta || !gut || !mir) return;
|
|
4146
|
+
var lines = ta.value.split("\n");
|
|
4147
|
+
var isJob = id === "jf-job";
|
|
4148
|
+
var n = 0;
|
|
4149
|
+
var nums = lines.map(function(l){
|
|
4150
|
+
var t = l.trim();
|
|
4151
|
+
if(t === "" || (isJob && t.charAt(0) === "~")) return "";
|
|
4152
|
+
n += 1; return String(n);
|
|
4153
|
+
});
|
|
4154
|
+
mir.style.width = ta.clientWidth ? ta.clientWidth + "px" : "";
|
|
4155
|
+
mir.innerHTML = lines.map(function(l){ return "<div>" + (l === "" ? "\u200b" : escapeXml(l)) + "</div>"; }).join("");
|
|
4156
|
+
var rows = mir.children;
|
|
4157
|
+
gut.innerHTML = nums.map(function(t, i){
|
|
4158
|
+
var hgt = rows[i] ? rows[i].offsetHeight : 0;
|
|
4159
|
+
return '<div' + (hgt ? ' style="height:' + hgt + 'px"' : "") + ">" + t + "</div>";
|
|
4160
|
+
}).join("");
|
|
4161
|
+
gut.scrollTop = ta.scrollTop;
|
|
4162
|
+
}
|
|
4163
|
+
// the empty-box examples (grey placeholders that vanish on typing) change with the Check type radio
|
|
4164
|
+
var JOB_PLACEHOLDERS = {
|
|
4165
|
+
deterministic: "Fix the failing date tests in src/date.js\n~ don't edit any test file\nMake npm test pass",
|
|
4166
|
+
rubric: "Write a one-page summary of docs/plan.md\n~ PASS: every number is quoted from the file\n~ FAIL: a number that is not in the file\nKeep it under 300 words",
|
|
4167
|
+
};
|
|
4168
|
+
var INPUTS_PLACEHOLDER = "repo: /home/me/myrepo\nspec: docs/spec.md";
|
|
4169
|
+
function setJobPlaceholders(v){
|
|
4170
|
+
document.getElementById("jf-job").setAttribute("placeholder", JOB_PLACEHOLDERS[v === "rubric" ? "rubric" : "deterministic"]);
|
|
4171
|
+
document.getElementById("jf-inputs").setAttribute("placeholder", INPUTS_PLACEHOLDER);
|
|
4172
|
+
}
|
|
4173
|
+
setJobPlaceholders("deterministic");
|
|
4183
4174
|
// grow a text box with its text (a few rows, then it scrolls inside); ONE function for the ask box and the card boxes.
|
|
4184
4175
|
// A box that is hidden (or not laid out) measures 0 and gets no inline height.
|
|
4185
4176
|
function fitBox(el){
|
|
4186
4177
|
el.style.height = "auto";
|
|
4187
4178
|
if(!el.scrollHeight){ el.style.height = ""; return; }
|
|
4188
4179
|
var border = el.offsetHeight - el.clientHeight;
|
|
4189
|
-
|
|
4180
|
+
var cap = parseFloat(window.getComputedStyle(el).maxHeight);
|
|
4181
|
+
el.style.height = Math.min(el.scrollHeight + border, cap > 0 ? cap : 200) + "px";
|
|
4190
4182
|
}
|
|
4191
4183
|
function fitCardBoxes(){
|
|
4192
|
-
|
|
4184
|
+
FIT_IDS.forEach(function(id){ fitBox(document.getElementById(id)); });
|
|
4185
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
4193
4186
|
}
|
|
4194
4187
|
// live growth: each card box re-fits on its own input (typing and Enter), through the same fitBox
|
|
4195
|
-
|
|
4188
|
+
FIT_IDS.forEach(function(id){
|
|
4196
4189
|
var box = document.getElementById(id);
|
|
4197
|
-
box.addEventListener("input", function(){ fitBox(box); });
|
|
4190
|
+
box.addEventListener("input", function(){ fitBox(box); if(NUMBERED_IDS.indexOf(id) !== -1) gutterSync(id); });
|
|
4198
4191
|
});
|
|
4192
|
+
NUMBERED_IDS.forEach(function(id){ var box = document.getElementById(id); box.addEventListener("scroll", function(){ document.getElementById(id + "-gut").scrollTop = box.scrollTop; }); });
|
|
4199
4193
|
window.addEventListener("resize", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4200
4194
|
document.getElementById("tab-chat").addEventListener("click", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4201
4195
|
// Enter sends through the one main button; Shift+Enter (or IME composition) keeps its newline
|
|
@@ -4300,11 +4294,8 @@
|
|
|
4300
4294
|
}
|
|
4301
4295
|
modelSelect.value = c.model;
|
|
4302
4296
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4306
|
-
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4307
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4297
|
+
fillCard(c);
|
|
4298
|
+
|
|
4308
4299
|
if(reuseSession) setReuseLocked(true);
|
|
4309
4300
|
renderPicker();
|
|
4310
4301
|
fitCardBoxes();
|
|
@@ -4326,6 +4317,14 @@
|
|
|
4326
4317
|
|
|
4327
4318
|
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4328
4319
|
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4320
|
+
// ONE owner of filling the card from a prefill (a re-attached session, Reuse, Edit in chat, Resume): the six boxes P7 left
|
|
4321
|
+
// (the old Goal/Source/Success/Guardrails/Judge boxes are gone), then every box re-fits and its number gutter re-measures.
|
|
4322
|
+
function fillCard(c){
|
|
4323
|
+
setVal("jf-name", c.jobName); setVal("jf-job", c.jobText); setVal("jf-inputs", c.inputs);
|
|
4324
|
+
setVal("jf-dest", c.destination); setVal("jf-cap-money", c.capUsd);
|
|
4325
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4326
|
+
fitCardBoxes();
|
|
4327
|
+
}
|
|
4329
4328
|
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
4330
4329
|
// `bareloop-reuse` event), so the card ends up in the same picked state whichever door was used.
|
|
4331
4330
|
function applyReuse(d){
|
|
@@ -4338,11 +4337,8 @@
|
|
|
4338
4337
|
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4339
4338
|
setVerdict("reuse");
|
|
4340
4339
|
if(c.model){ modelSelect.value = c.model; }
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4344
|
-
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4345
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4340
|
+
fillCard(c);
|
|
4341
|
+
|
|
4346
4342
|
setReuseLocked(true);
|
|
4347
4343
|
renderPicker();
|
|
4348
4344
|
sfLine.hidden = false;
|
|
@@ -4367,11 +4363,8 @@
|
|
|
4367
4363
|
openNewCard();
|
|
4368
4364
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4369
4365
|
if(c.model){ modelSelect.value = c.model; }
|
|
4370
|
-
|
|
4371
|
-
|
|
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";
|
|
4366
|
+
fillCard(c);
|
|
4367
|
+
|
|
4375
4368
|
originLine.textContent = d.origin || "";
|
|
4376
4369
|
originLine.hidden = !d.origin;
|
|
4377
4370
|
renderPicker();
|
|
@@ -4394,13 +4387,10 @@
|
|
|
4394
4387
|
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4395
4388
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4396
4389
|
if(c.model){ modelSelect.value = c.model; }
|
|
4397
|
-
|
|
4398
|
-
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4399
|
-
setVal("jf-judge", c.judgeExamples);
|
|
4390
|
+
fillCard(c);
|
|
4400
4391
|
setVal("jf-cap-money", r.budgetUsd);
|
|
4401
4392
|
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
4402
4393
|
document.getElementById("jf-cap-time").parentNode.hidden = resumeRun.noTime;
|
|
4403
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4404
4394
|
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
4395
|
originLine.classList.add("res");
|
|
4406
4396
|
originLine.hidden = false;
|
|
@@ -4444,10 +4434,10 @@
|
|
|
4444
4434
|
// build item 1 — the "lost answer" fix. ROOT CAUSE: `renderActions`
|
|
4445
4435
|
// (fed by the every-2s `poll()`) unconditionally overwrites
|
|
4446
4436
|
// `#chat-card-error` on every tick (`errEl.textContent = state.error ||
|
|
4447
|
-
// ""`), including the SAME tick the send/
|
|
4437
|
+
// ""`), including the SAME tick the send/sign handlers below
|
|
4448
4438
|
// used to call `poll()` in their own `.then` — so a refused/failed POST's
|
|
4449
4439
|
// error was wiped before a person could read it (often on the very next
|
|
4450
|
-
// line of the same callback). Compounding it, `send
|
|
4440
|
+
// line of the same callback). Compounding it, `send` cleared
|
|
4451
4441
|
// `msgInput.value` unconditionally, even on failure — so a rejected
|
|
4452
4442
|
// answer vanished from the box with no trace at all. Fixed by: (1) a
|
|
4453
4443
|
// dedicated `#chat-action-error` element only these handlers touch,
|
|
@@ -4508,20 +4498,21 @@
|
|
|
4508
4498
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
4509
4499
|
}
|
|
4510
4500
|
|
|
4511
|
-
|
|
4501
|
+
// hamr 2026-10-09 (as fwdloop's Revise): the draft is ended and the New job card opens with the SAME values in every
|
|
4502
|
+
// box, all editable; Start drafting then drafts fresh from whatever the boxes now say. No typed change request goes
|
|
4503
|
+
// to the model. The ended draft is an Abandon: money already spent stays booked.
|
|
4504
|
+
function doReopen(){
|
|
4512
4505
|
if(!sessionId) return;
|
|
4513
|
-
|
|
4514
|
-
authorPost("/api/author/" + sessionId + "/revise", {text: text}).then(function(r){
|
|
4506
|
+
authorPost("/api/author/" + sessionId + "/abandon", {}).then(function(r){
|
|
4515
4507
|
var o = chatPostOutcome(r);
|
|
4516
|
-
if(o.ok){
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
poll();
|
|
4523
|
-
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
4508
|
+
if(!o.ok){ chatActionFailed(o.error); return; }
|
|
4509
|
+
openNewCard();
|
|
4510
|
+
chatActionOk();
|
|
4511
|
+
lastMsgSig = null;
|
|
4512
|
+
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>";
|
|
4513
|
+
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not revised"); });
|
|
4524
4514
|
}
|
|
4515
|
+
reopenBtn.addEventListener("click", doReopen);
|
|
4525
4516
|
|
|
4526
4517
|
function doSign(){
|
|
4527
4518
|
if(!sessionId) return;
|
|
@@ -4553,7 +4544,6 @@
|
|
|
4553
4544
|
if(mainAction === "start") doStart();
|
|
4554
4545
|
else if(mainAction === "check-deps") doCheckDeps();
|
|
4555
4546
|
else if(mainAction === "send") doSend();
|
|
4556
|
-
else if(mainAction === "revise") doRevise();
|
|
4557
4547
|
else if(mainAction === "sign-prepare" || mainAction === "sign") doSign();
|
|
4558
4548
|
});
|
|
4559
4549
|
|