bareloop 0.39.0 → 0.41.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 +60 -0
- package/bareloop.context.md +137 -112
- package/package.json +2 -2
- package/src/authorflow.js +333 -58
- package/src/authoring.js +65 -2
- package/src/authorjob.js +57 -9
- package/src/authorreadout.js +50 -2
- package/src/authorrun.js +30 -27
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/bundle.js +7 -4
- package/src/bundlerun.js +4 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +4 -1
- package/src/cli.js +1 -1
- package/src/config.js +129 -29
- package/src/declaredclose.js +4 -3
- package/src/draftspend.js +6 -1
- package/src/index.js +2 -4
- package/src/interviewrun.js +73 -52
- package/src/job.js +50 -11
- package/src/jobblock.js +140 -0
- package/src/judged.js +12 -44
- package/src/keysfile.js +15 -7
- package/src/kinds.js +2 -2
- package/src/ledger.js +1 -1
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/panel/authorroutes.js +34 -19
- package/src/panel/authorsession.js +187 -72
- package/src/panel/index.html +699 -215
- package/src/panel/jobplan.js +107 -0
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +102 -28
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +6 -6
- package/src/promptregisters.js +1 -1
- package/src/providerrows.js +226 -141
- package/src/providers.js +58 -250
- package/src/providershapes.js +18 -0
- package/src/replay.js +1 -1
- 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/thinking.js +130 -0
- package/src/transport.js +1 -1
- package/src/userrun.js +53 -76
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +7 -1
- package/types/authorjob.d.ts +35 -3
- package/types/authorreadout.d.ts +17 -1
- package/types/authorscout.d.ts +5 -3
- package/types/config.d.ts +23 -6
- package/types/declaredclose.d.ts +4 -1
- package/types/draftspend.d.ts +8 -2
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +6 -6
- package/types/jobblock.d.ts +102 -0
- package/types/judged.d.ts +5 -36
- package/types/keysfile.d.ts +15 -4
- package/types/modellist.d.ts +50 -0
- package/types/monthly.d.ts +4 -3
- package/types/panel/authorsession.d.ts +44 -15
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +34 -4
- package/types/planrun.d.ts +1 -1
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +35 -116
- package/types/providershapes.d.ts +10 -0
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +6 -1
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;}
|
|
@@ -466,6 +469,41 @@
|
|
|
466
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 */
|
|
467
470
|
/* same look as the card's one-line inputs (white when editable, ordinary 1px border) — a textarea has its own browser border otherwise */
|
|
468
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;}
|
|
469
507
|
/* hamr 2026-10-05: the focused/selected Chat-card field gets a blue border (the accent), no outline ring */
|
|
470
508
|
#panel-chat :is(input,select,textarea):focus{border-color:var(--accent);outline:none;}
|
|
471
509
|
/* hamr 2026-10-05: the ask box is dimmed until something asks for a reply (placeholder stays readable) */
|
|
@@ -492,19 +530,92 @@
|
|
|
492
530
|
.money-item input{width:100%;height:44px;box-sizing:border-box;}
|
|
493
531
|
.keyfile-strip{position:relative;border:1px solid var(--border-strong);padding:12px 12px 8px;background:var(--panel2);margin:14px 0;font-size:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;}
|
|
494
532
|
.keyfile-strip::before{content:"┤ KEYS ├";position:absolute;top:-9px;left:10px;background:var(--panel2);padding:0 4px;font-size:11px;color:var(--text-faint);letter-spacing:0.08em;}
|
|
495
|
-
.keyfile-strip-main{display:flex;flex-direction:column;gap:3px;flex:1;min-width:
|
|
496
|
-
.keyfile-strip-actions{display:flex;gap:6px;flex:none;}
|
|
533
|
+
.keyfile-strip-main{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0;overflow-wrap:anywhere;}
|
|
534
|
+
.keyfile-strip-actions{display:flex;flex-wrap:wrap;gap:6px;flex:none;max-width:100%;}
|
|
497
535
|
.keyfile-strip code{background:var(--bg);border:1px solid var(--border-strong);padding:1px 5px;font-size:11px;color:var(--amber);overflow-wrap:anywhere;}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
.pv-
|
|
504
|
-
.pv-
|
|
536
|
+
/* M4d Providers layout F (hamr approved 2026-10-11): ONE shared grid, so a key row, the model sub-head and
|
|
537
|
+
every model row sit on the same column edges. Key band: KEY | API SHAPE | BASE URL (two columns) | OLD MAX_TOKENS | TOKENS | refresh.
|
|
538
|
+
Model row: [USE 44px][MODEL] under KEY, IN + OUT side by side under BASE URL, TOKENS on the key band's TOKENS edge. A key row has a
|
|
539
|
+
solid 1px line, a model row a dotted 1px hairline. Below 1280px every row collapses to stacked label : value lines. */
|
|
540
|
+
.pv-grid{--pv-cols:minmax(0,2.8fr) minmax(0,1.5fr) minmax(0,.95fr) minmax(0,.95fr) minmax(0,.9fr) minmax(0,.6fr) 272px;font-size:13px;}
|
|
541
|
+
.pv-head,.pv-krow,.pv-mhead,.pv-mrow{display:grid;grid-template-columns:var(--pv-cols);column-gap:8px;align-items:center;padding:0 8px;min-width:0;}
|
|
542
|
+
.pv-head > *,.pv-krow > *,.pv-mhead > *,.pv-mrow > *{min-width:0;}
|
|
543
|
+
.pv-head,.pv-mhead{font-size:11px;font-weight:700;color:var(--text-dim);letter-spacing:0.04em;min-height:26px;}
|
|
544
|
+
.pv-head{background:var(--panel2);border-bottom:1px solid var(--border-strong);}
|
|
545
|
+
.pv-mhead{background:var(--bg);border-bottom:1px dotted var(--border-strong);}
|
|
546
|
+
.pv-key{border-top:1px solid var(--border-strong);}
|
|
547
|
+
.pv-krow{min-height:40px;padding-top:4px;padding-bottom:4px;}
|
|
548
|
+
.pv-c-url{grid-column:3 / span 2;}
|
|
549
|
+
.pv-lbl{display:none;}
|
|
550
|
+
.pv-name{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;min-width:0;overflow-wrap:anywhere;}
|
|
551
|
+
.pv-name code{font-weight:700;}
|
|
552
|
+
.pv-ok{color:var(--green);font-weight:700;}
|
|
553
|
+
.pv-fold{border:0;background:none;color:var(--text);font:inherit;padding:0 4px 0 0;min-width:22px;min-height:24px;cursor:pointer;text-align:left;}
|
|
554
|
+
.pv-krow select,.pv-krow input[type=text]{width:100%;max-width:100%;min-height:28px;box-sizing:border-box;}
|
|
555
|
+
.pv-grid select,.pv-grid input[type=text]{font-size:12px;height:28px;padding:2px 6px;}
|
|
556
|
+
.pv-legacy{display:inline-flex;align-items:center;gap:6px;cursor:pointer;min-height:28px;text-transform:none;letter-spacing:0;font-weight:400;font-size:12px;}
|
|
557
|
+
.pv-legacy input,.pv-m-use input{width:16px;height:16px;margin:0;accent-color:var(--accent);cursor:pointer;}
|
|
558
|
+
.pv-tok{text-align:right;font-variant-numeric:tabular-nums;}
|
|
559
|
+
.pv-refcell{display:flex;justify-content:flex-end;}
|
|
560
|
+
.pv-refbox{display:flex;flex-direction:column;align-items:flex-start;max-width:100%;}
|
|
561
|
+
.pv-refbtns{display:flex;gap:6px;}
|
|
562
|
+
.pv-refbox .pv-msg{width:0;min-width:100%;overflow-wrap:anywhere;margin:2px 0 0;}
|
|
563
|
+
.pv-refbox .pv-msg:empty{display:none;}
|
|
564
|
+
.pv-refbtn{white-space:nowrap;flex:0 0 auto;}
|
|
565
|
+
.pv-refbtn:disabled{opacity:.5;cursor:not-allowed;}
|
|
566
|
+
.pv-sum{color:var(--text-faint);font-size:11px;font-weight:400;}
|
|
567
|
+
.pv-key[data-open="1"] .pv-sum{display:none;}
|
|
568
|
+
.pv-knote{padding:0 8px 4px 34px;margin:0;}
|
|
569
|
+
.pv-knote:empty{display:none;}
|
|
570
|
+
.pv-knote > .hint:empty{display:none;}
|
|
571
|
+
.pv-key[data-open="0"] .pv-models{display:none;}
|
|
572
|
+
.pv-mrow{border-top:1px dotted var(--border-strong);min-height:32px;}
|
|
573
|
+
.pv-mrow:hover,.pv-mrow:focus-within{background:var(--panel2);}
|
|
574
|
+
.pv-um{grid-column:1;display:flex;align-items:center;gap:0;min-width:0;}
|
|
575
|
+
.pv-uh,.pv-m-use{flex:0 0 44px;width:44px;display:flex;align-items:center;justify-content:center;}
|
|
576
|
+
.pv-m-use{min-height:32px;}
|
|
577
|
+
.pv-m-name{min-width:0;overflow-wrap:anywhere;font-size:12px;}
|
|
578
|
+
.pv-pair{grid-column:3 / span 2;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);column-gap:8px;align-items:center;min-width:0;}
|
|
579
|
+
.pv-pair > *{min-width:0;}
|
|
580
|
+
.pv-pair input[type=text]{width:100%;text-align:right;}
|
|
581
|
+
.pv-mhead .pv-tok{text-align:right;}
|
|
582
|
+
.pv-mhead .pv-tok,.pv-m-tok{grid-column:6;}
|
|
583
|
+
.pv-m-msg{grid-column:1 / -1;padding:2px 8px 2px 52px;margin:0;}
|
|
584
|
+
.pv-m-msg:empty{display:none;}
|
|
585
|
+
.pv-more{padding:3px 8px 3px 34px;border-top:1px dotted var(--border-strong);}
|
|
586
|
+
.pv-more:empty{display:none;}
|
|
587
|
+
.pv-more .pv-add{border:0;background:none;color:var(--accent);font:inherit;font-size:12px;padding:2px 0;min-height:24px;cursor:pointer;text-decoration:underline;}
|
|
588
|
+
.pv-more.pv-failed{color:var(--amber);background:var(--amber-bg);font-size:12px;}
|
|
589
|
+
.pv-typed{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-top:4px;}
|
|
590
|
+
.pv-typed input{flex:1 1 160px;min-width:0;max-width:260px;min-height:28px;box-sizing:border-box;}
|
|
591
|
+
.pv-typed .hint{flex:1 1 100%;margin:0;}
|
|
592
|
+
.hint.bad{color:var(--red);}
|
|
593
|
+
.field label.jf-thinking{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:6px;margin-top:6px;font-size:12px;text-transform:none;letter-spacing:0;}
|
|
594
|
+
.field label.jf-thinking input{width:auto;min-height:0;margin:0;}
|
|
505
595
|
.table-wrap{overflow-x:auto;border:1px solid var(--border-strong);background:var(--panel2);max-width:100%;}
|
|
506
596
|
@media (max-width: 899px){
|
|
507
597
|
.money-strip{grid-template-columns:1fr 1fr;}
|
|
598
|
+
.keyfile-strip-main{flex:1 1 100%;}
|
|
599
|
+
}
|
|
600
|
+
@media (max-width: 1279px){
|
|
601
|
+
.pv-head,.pv-mhead{display:none;}
|
|
602
|
+
.pv-krow,.pv-mrow{display:block;padding:2px 8px;}
|
|
603
|
+
.pv-cell{display:grid;grid-template-columns:104px minmax(0,1fr);gap:8px;align-items:center;min-height:44px;border-top:1px dotted var(--border);}
|
|
604
|
+
.pv-cell:first-child{border-top:none;}
|
|
605
|
+
.pv-lbl{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-dim);font-weight:700;}
|
|
606
|
+
.pv-cell.nolbl{display:flex;}
|
|
607
|
+
.pv-tok{text-align:left;}
|
|
608
|
+
.pv-grid select,.pv-grid input[type=text]{height:44px;font-size:14px;}
|
|
609
|
+
.pv-krow select,.pv-krow input[type=text]{min-height:44px;}
|
|
610
|
+
.pv-legacy{min-height:44px;gap:10px;}
|
|
611
|
+
.pv-legacy input,.pv-m-use input{width:24px;height:24px;}
|
|
612
|
+
.pv-um{min-height:44px;}
|
|
613
|
+
.pv-uh,.pv-m-use{min-height:44px;}
|
|
614
|
+
.pv-cell.pv-pair{display:grid;grid-template-columns:104px minmax(0,1fr) minmax(0,1fr);}
|
|
615
|
+
.pv-cell.pv-pair::before{content:"In / Out $/1M";font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-dim);font-weight:700;}
|
|
616
|
+
.pv-fold,.pv-refbtn,.pv-more .pv-add{min-height:44px;min-width:44px;}
|
|
617
|
+
.pv-knote,.pv-more{padding-left:8px;}
|
|
618
|
+
.pv-m-msg{padding-left:8px;}
|
|
508
619
|
}
|
|
509
620
|
@media (max-width: 899px){
|
|
510
621
|
.main{flex-direction:column;}
|
|
@@ -577,35 +688,32 @@
|
|
|
577
688
|
<!-- build item 5 (2026-09-28) — ONE line under Model, not two:
|
|
578
689
|
key + reachability collapsed into a single readiness read. -->
|
|
579
690
|
<div class="jf-readiness unknown" id="jf-model-status" data-testid="jf-model-status"></div>
|
|
691
|
+
<!-- P8: the job's "Thinking on" tick (drafting + the rubric judge; never the worker). The SERVER's per-line `thinking.canChange` decides. -->
|
|
692
|
+
<label class="jf-thinking" id="jf-thinking-label"><input type="checkbox" id="jf-thinking" data-testid="jf-thinking"> Thinking on</label>
|
|
693
|
+
<div class="hint" id="jf-thinking-note" data-testid="jf-thinking-note"></div>
|
|
580
694
|
</div>
|
|
581
695
|
<div class="field">
|
|
582
696
|
<label for="jf-name">Job name</label>
|
|
583
697
|
<input id="jf-name" type="text" placeholder="kebab-case, unique">
|
|
584
698
|
</div>
|
|
585
699
|
<div class="field">
|
|
586
|
-
<label for="jf-
|
|
587
|
-
<
|
|
700
|
+
<label for="jf-job">The job</label>
|
|
701
|
+
<div class="nbox" data-testid="nbox-job">
|
|
702
|
+
<div class="gut" id="jf-job-gut" aria-hidden="true"></div>
|
|
703
|
+
<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>
|
|
704
|
+
</div>
|
|
588
705
|
</div>
|
|
589
706
|
<div class="field">
|
|
590
|
-
<label for="jf-
|
|
591
|
-
<
|
|
707
|
+
<label for="jf-inputs">Inputs</label>
|
|
708
|
+
<div class="nbox" data-testid="nbox-inputs">
|
|
709
|
+
<div class="gut" id="jf-inputs-gut" aria-hidden="true"></div>
|
|
710
|
+
<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>
|
|
711
|
+
</div>
|
|
592
712
|
</div>
|
|
593
713
|
<div class="field">
|
|
594
714
|
<label for="jf-dest">Destination</label>
|
|
595
715
|
<textarea class="jf-wrap" id="jf-dest" rows="1" placeholder="the write fence — where in the repo this run may change files"></textarea>
|
|
596
716
|
</div>
|
|
597
|
-
<div class="field">
|
|
598
|
-
<label for="jf-success">Success</label>
|
|
599
|
-
<textarea class="jf-wrap" id="jf-success" rows="2"></textarea>
|
|
600
|
-
</div>
|
|
601
|
-
<div class="field">
|
|
602
|
-
<label for="jf-guardrails">Guardrails</label>
|
|
603
|
-
<textarea class="jf-wrap" id="jf-guardrails" rows="2"></textarea>
|
|
604
|
-
</div>
|
|
605
|
-
<div class="field">
|
|
606
|
-
<label for="jf-judge">Judge examples <span class="hint" style="display:inline">(rubric only)</span></label>
|
|
607
|
-
<textarea class="jf-wrap" id="jf-judge" rows="2" disabled></textarea>
|
|
608
|
-
</div>
|
|
609
717
|
|
|
610
718
|
<div class="cap-row" data-testid="cap-row">
|
|
611
719
|
<div class="cap-field">
|
|
@@ -750,17 +858,16 @@
|
|
|
750
858
|
<h4 id="details-title">Job card — signed</h4>
|
|
751
859
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
752
860
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
861
|
+
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
862
|
+
<div class="field"><label>The job</label><div class="ro-value" id="details-plan" data-testid="details-plan">—</div></div>
|
|
863
|
+
<div class="field"><label>Inputs</label><div class="ro-value" id="details-inputs" data-testid="details-inputs">—</div></div>
|
|
864
|
+
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
865
|
+
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
753
866
|
<div class="field-row">
|
|
754
867
|
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
755
868
|
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
756
869
|
</div>
|
|
757
|
-
<div class="field"
|
|
758
|
-
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
759
|
-
<div class="field"><label>Success</label><div class="ro-value" id="details-success">—</div></div>
|
|
760
|
-
<div class="field"><label>Guardrails</label><div class="ro-value" id="details-guardrails">—</div></div>
|
|
761
|
-
<div class="field"><label>Source</label><div class="ro-value" id="details-source">—</div></div>
|
|
762
|
-
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
763
|
-
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
870
|
+
<div class="field"><label>Signed</label><div class="ro-value" id="details-signed" data-testid="details-signed">—</div></div>
|
|
764
871
|
<div class="hint" id="job-note"></div>
|
|
765
872
|
</div>
|
|
766
873
|
</section>
|
|
@@ -784,30 +891,26 @@
|
|
|
784
891
|
</div>
|
|
785
892
|
|
|
786
893
|
<div class="pane-body">
|
|
787
|
-
<!-- PROVIDERS TAB (
|
|
894
|
+
<!-- PROVIDERS TAB (P8: one row per key in the keys file that has a value; layout rewritten from fwdloop src/panel/index.html@f9449e8) -->
|
|
788
895
|
<section id="panel-providers" class="tabpanel active" role="tabpanel" aria-labelledby="tab-providers">
|
|
789
|
-
<div class="keyfile-strip" data-testid="
|
|
896
|
+
<div class="keyfile-strip" data-testid="keys-strip">
|
|
790
897
|
<div class="keyfile-strip-main">
|
|
791
|
-
<span>Put <code>NAME=key</code> lines in
|
|
792
|
-
<span class="hint" id="
|
|
793
|
-
<span class="hint" id="
|
|
898
|
+
<span>Put <code>NAME=key</code> lines in <code id="keys-path" data-testid="keys-path">~/.config/bareloop/.env</code>, then Reload keys — each row below says whether its key is set. Only names are listed; keys never reach this page.</span>
|
|
899
|
+
<span class="hint bad" id="keys-refusal" data-testid="keys-refusal"></span>
|
|
900
|
+
<span class="hint" id="keys-open-note" data-testid="keys-open-note"></span>
|
|
794
901
|
</div>
|
|
795
902
|
<div class="keyfile-strip-actions">
|
|
796
|
-
<button class="btn small"
|
|
797
|
-
<button class="btn small"
|
|
903
|
+
<button class="btn small" id="open-keys-folder" type="button" data-testid="open-keys-folder" aria-label="Open keys folder">Open keys folder</button>
|
|
904
|
+
<button class="btn small" id="reload-keys" type="button" data-testid="reload-keys">Reload keys</button>
|
|
798
905
|
</div>
|
|
799
906
|
</div>
|
|
800
907
|
<div class="table-wrap" data-testid="provider-table-wrap">
|
|
801
|
-
<
|
|
802
|
-
<
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
<tbody id="pv-rows"></tbody>
|
|
806
|
-
</table>
|
|
908
|
+
<div class="pv-grid" data-testid="provider-table">
|
|
909
|
+
<div class="pv-head" aria-hidden="true"><span>KEY</span><span>API SHAPE</span><span class="pv-c-url">BASE URL</span><span>OLD MAX_TOKENS</span><span class="pv-tok">TOKENS</span><span></span></div>
|
|
910
|
+
<div id="pv-rows"></div>
|
|
911
|
+
</div>
|
|
807
912
|
</div>
|
|
808
|
-
<div class="hint" id="pv-
|
|
809
|
-
<div id="pv-shell" data-testid="pv-shell"></div>
|
|
810
|
-
<div class="hint" id="pv-note" data-testid="pv-note" style="margin-top:6px;">Name is the model id Chat uses. Base URL blank = the API shape's own host. Test asks the provider for its model list — it costs nothing.</div>
|
|
913
|
+
<div class="hint" id="pv-note" data-testid="pv-note" style="margin-top:6px;">⟳ refresh models (free) · old max_tokens: tick if the server wants the old field name</div>
|
|
811
914
|
</section>
|
|
812
915
|
|
|
813
916
|
<!-- MONEY & LIMITS TAB -->
|
|
@@ -1991,8 +2094,8 @@
|
|
|
1991
2094
|
var nr = "not recorded";
|
|
1992
2095
|
renderJob({
|
|
1993
2096
|
job: v.job, resolved: false, checkType: v.checkType || nr, model: v.model || nr,
|
|
1994
|
-
goal: v.goal || nr,
|
|
1995
|
-
|
|
2097
|
+
goal: v.goal || nr, plan: v.plan || null, inputs: v.inputs || null, signed: null,
|
|
2098
|
+
destination: nr, tools: nr,
|
|
1996
2099
|
budgetUsd: (typeof v.budgetUsd === "number") ? v.budgetUsd : null,
|
|
1997
2100
|
maxWallMs: (typeof v.maxWallMs === "number") ? v.maxWallMs : null,
|
|
1998
2101
|
note: "imported · view only — the exported job's own spec",
|
|
@@ -2296,7 +2399,7 @@
|
|
|
2296
2399
|
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2297
2400
|
// `model` (optional) = the Chat card's Model box, when the picker asks: the estimate line then counts only that worker's runs.
|
|
2298
2401
|
function reuseWorkflow(runid, model){
|
|
2299
|
-
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&
|
|
2402
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&" + model : "")).then(function(r){
|
|
2300
2403
|
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2301
2404
|
document.getElementById("tab-chat").click();
|
|
2302
2405
|
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
@@ -3128,6 +3231,35 @@
|
|
|
3128
3231
|
});
|
|
3129
3232
|
});
|
|
3130
3233
|
|
|
3234
|
+
// ---- the ONE job plan drawing (P7): used by the chat's plan bubble AND the Job tab. `plan` is the server's model
|
|
3235
|
+
// (src/panel/jobplan.js): numbered lines with their rule, the checks under the line each serves (folded), the AI's own
|
|
3236
|
+
// "Not checked", the always-on guards. Only the marker is blue (--plan). Returns HTML (every text is escaped).
|
|
3237
|
+
function renderJobPlan(plan){
|
|
3238
|
+
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>' +
|
|
3239
|
+
(plan.goal && !plan.older ? '<div class="jp-sum" data-testid="plan-goal"><span class="lbl">AI\'s summary</span>' + escapeXml(plan.goal) + "</div>" : "");
|
|
3240
|
+
var checkLine = function(c){
|
|
3241
|
+
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>";
|
|
3242
|
+
};
|
|
3243
|
+
(plan.lines || []).forEach(function(l){
|
|
3244
|
+
h += '<div class="jl" data-testid="plan-line"><span class="num">' + escapeXml(String(l.n)) + "</span>" + escapeXml(l.text);
|
|
3245
|
+
if(l.rule) h += '<div class="sub">~ ' + escapeXml(l.rule) + "</div>";
|
|
3246
|
+
(l.checks || []).forEach(function(c){ h += checkLine(c); });
|
|
3247
|
+
(l.refused || []).forEach(function(r){ h += '<div class="fold" data-testid="plan-refused"><span class="mk">\u203A</span> not checked \u00B7 ' + escapeXml(r) + "</div>"; });
|
|
3248
|
+
h += "</div>";
|
|
3249
|
+
});
|
|
3250
|
+
if((plan.loose || []).length){
|
|
3251
|
+
h += '<div class="jl"><span class="num"></span>other checks' + plan.loose.map(checkLine).join("") + "</div>";
|
|
3252
|
+
}
|
|
3253
|
+
var fold = function(title, items, testid){
|
|
3254
|
+
if(!items || !items.length) return "";
|
|
3255
|
+
return '<details class="fold" data-testid="' + testid + '"><summary>' + escapeXml(title) + " \u00B7 " + items.length + "</summary><ul>" +
|
|
3256
|
+
items.map(function(x){ return "<li>" + escapeXml(x) + "</li>"; }).join("") + "</ul></details>";
|
|
3257
|
+
};
|
|
3258
|
+
h += fold("Not checked (the AI's own reading)", plan.notChecked, "plan-notchecked");
|
|
3259
|
+
h += fold("Always on", plan.alwaysOn, "plan-alwayson");
|
|
3260
|
+
return h + "</div>";
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3131
3263
|
// ---- Job tab ----
|
|
3132
3264
|
function renderJob(job){
|
|
3133
3265
|
var empty = document.getElementById("job-empty");
|
|
@@ -3143,10 +3275,14 @@
|
|
|
3143
3275
|
var descField = document.getElementById("details-description-field");
|
|
3144
3276
|
if(job.description){ descField.hidden = false; document.getElementById("details-description").textContent = job.description; }
|
|
3145
3277
|
else { descField.hidden = true; }
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
document.getElementById("details-
|
|
3149
|
-
|
|
3278
|
+
// P7: the numbered job lines, their rules and checks — the SAME drawing the chat's plan bubble uses (renderJobPlan)
|
|
3279
|
+
var plan = job.plan || {lines: [{n: 1, text: job.goal || "not recorded", rule: "", checks: [], refused: []}], loose: [], notChecked: [], alwaysOn: [], older: true};
|
|
3280
|
+
document.getElementById("details-plan").innerHTML = renderJobPlan(plan);
|
|
3281
|
+
var ins = Array.isArray(job.inputs) ? job.inputs : [];
|
|
3282
|
+
document.getElementById("details-inputs").innerHTML = ins.length
|
|
3283
|
+
? ins.map(function(i){ return '<div><span class="num-in">' + escapeXml(String(i.n)) + "</span> " + escapeXml(i.label + ": " + i.value) + "</div>"; }).join("")
|
|
3284
|
+
: "not recorded";
|
|
3285
|
+
document.getElementById("details-signed").textContent = job.signed || "not recorded";
|
|
3150
3286
|
document.getElementById("details-dest").textContent = job.destination;
|
|
3151
3287
|
document.getElementById("details-tools").textContent = job.tools;
|
|
3152
3288
|
document.getElementById("details-cap-money").textContent = panelMoney(job.budgetUsd);
|
|
@@ -3218,7 +3354,7 @@
|
|
|
3218
3354
|
// drives both fetches — never two independent timers that could race
|
|
3219
3355
|
// each other. ----
|
|
3220
3356
|
//
|
|
3221
|
-
// F195 (docs/
|
|
3357
|
+
// F195 (docs/wiki/FINDINGS.md): /api/runs re-replays every spine on
|
|
3222
3358
|
// every call (~457ms CPU at 250 rows). hamr's ruling B, 2026-09-27:
|
|
3223
3359
|
// "refresh less often, up to 30s at most, choose the lesser when
|
|
3224
3360
|
// possible" — the Runs list polls every 10s (RUNS_LIST_POLL_MS) instead
|
|
@@ -3414,7 +3550,7 @@
|
|
|
3414
3550
|
document.getElementById("btn-settings").addEventListener("click", function(){
|
|
3415
3551
|
mainView.hidden = true;
|
|
3416
3552
|
settingsView.hidden = false;
|
|
3417
|
-
loadProviders();
|
|
3553
|
+
loadProviders(true);
|
|
3418
3554
|
loadMoney();
|
|
3419
3555
|
});
|
|
3420
3556
|
document.getElementById("settings-back").addEventListener("click", function(){
|
|
@@ -3423,7 +3559,7 @@
|
|
|
3423
3559
|
});
|
|
3424
3560
|
document.getElementById("tab-money").addEventListener("click", loadMoney);
|
|
3425
3561
|
|
|
3426
|
-
// ---- Providers tab (
|
|
3562
|
+
// ---- Providers tab (P8): one block per key in the keys file that has a value; rewritten from fwdloop src/panel/index.html@f9449e8 ----
|
|
3427
3563
|
var pvRows = document.getElementById("pv-rows");
|
|
3428
3564
|
var pvNote = document.getElementById("pv-note");
|
|
3429
3565
|
var PV_NOTE_DEFAULT = pvNote.textContent;
|
|
@@ -3433,104 +3569,316 @@
|
|
|
3433
3569
|
if(n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
|
3434
3570
|
return String(n);
|
|
3435
3571
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
+ '<td><input type="text" class="pv-name" data-testid="pv-name-' + e + '" placeholder="model id" value="' + escapeXml(row.name) + '"></td>'
|
|
3454
|
-
+ '<td><select class="pv-shape" data-testid="pv-shape-' + e + '">' + shapeOpts + "</select></td>"
|
|
3455
|
-
+ '<td><input type="text" class="pv-url" data-testid="pv-url-' + e + '" placeholder="' + escapeXml(row.placeholder || "the shape\'s own host") + '" value="' + escapeXml(row.baseUrl) + '"></td>'
|
|
3456
|
-
+ '<td class="pv-test-cell">' + (row.canTest
|
|
3457
|
-
? '<button class="btn small pv-test" type="button" data-testid="test-connection-' + e + '">Test</button><div class="hint pv-test-result" data-testid="test-result-' + e + '"></div>'
|
|
3458
|
-
: '<span class="hint">not available</span>') + "</td>"
|
|
3459
|
-
+ '<td data-testid="pv-tokens-' + e + '">' + escapeXml(tokensText(row.tokens)) + "</td>"
|
|
3460
|
-
+ "<td>" + bal + "</td>"
|
|
3461
|
-
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-in" data-testid="pv-price-in-' + e + '" placeholder="not set" value="' + (row.priceInPerM === null ? "" : escapeXml(String(row.priceInPerM))) + '"></td>'
|
|
3462
|
-
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-out" data-testid="pv-price-out-' + e + '" placeholder="not set" value="' + (row.priceOutPerM === null ? "" : escapeXml(String(row.priceOutPerM))) + '"></td></tr>';
|
|
3463
|
-
}).join("") : '<tr><td colspan="9" class="hint" data-testid="pv-empty">no keys with a value yet — add NAME=key to the keys file, then Reload keys</td></tr>';
|
|
3464
|
-
pvRows.querySelectorAll(".pv-balance-fetch").forEach(function(fetchEl){
|
|
3465
|
-
authorGet("/api/settings/providers/balance?env=" + encodeURIComponent(fetchEl.getAttribute("data-env"))).then(function(b){
|
|
3466
|
-
fetchEl.textContent = b && b.text ? b.text : ("— " + ((b && b.note) || "unavailable"));
|
|
3467
|
-
}).catch(function(){ fetchEl.textContent = "— unavailable"; });
|
|
3468
|
-
});
|
|
3469
|
-
// a key exported only in the shell has no row: say so, by NAME, once per name
|
|
3470
|
-
document.getElementById("pv-shell").innerHTML = (r.shellOnly || []).map(function(n){
|
|
3471
|
-
return '<div class="hint" data-testid="pv-shell-' + escapeXml(n) + '">' + escapeXml(n) + " is set in your shell but not in ~/.config/bareloop/.env — add it there to use it here and in Chat.</div>";
|
|
3472
|
-
}).join("");
|
|
3473
|
-
pvNote.textContent = r.configProblem ? r.configProblem : PV_NOTE_DEFAULT;
|
|
3572
|
+
// A hint line: plain text, red when it is a refusal.
|
|
3573
|
+
function setNote(el, bad, text){
|
|
3574
|
+
el.className = "hint" + (bad ? " bad" : "");
|
|
3575
|
+
el.textContent = text || "";
|
|
3576
|
+
}
|
|
3577
|
+
// The server's own plain sentence (`say`); the page never shows a code or an HTTP status.
|
|
3578
|
+
function settingsRefusal(r){
|
|
3579
|
+
var b = r && r.body;
|
|
3580
|
+
if(b && typeof b.say === "string" && b.say) return b.say;
|
|
3581
|
+
return "Settings cannot do that. Keys are typed into your keys file by hand.";
|
|
3582
|
+
}
|
|
3583
|
+
function networkText(){ return "Could not reach the panel, so nothing is confirmed. Check it is still running."; }
|
|
3584
|
+
// `list` true = also ask each provider for its model list (free): on page open and on Reload keys. A save repaints without asking again.
|
|
3585
|
+
function loadProviders(list){
|
|
3586
|
+
return authorGet("/api/settings/providers").then(function(d){
|
|
3587
|
+
if(!d || !d.ok){ setNote(document.getElementById("keys-refusal"), true, "Could not read your providers. Reload the page."); return; }
|
|
3588
|
+
paintProviders(d, list === true);
|
|
3474
3589
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
3475
|
-
}
|
|
3590
|
+
}, function(){ setNote(document.getElementById("keys-refusal"), true, networkText()); });
|
|
3591
|
+
}
|
|
3592
|
+
// Open keys folder: the server opens the FOLDER in the file manager; if it cannot, the path shows as text
|
|
3593
|
+
document.getElementById("open-keys-folder").addEventListener("click", function(){
|
|
3594
|
+
var out = document.getElementById("keys-open-note");
|
|
3595
|
+
out.textContent = "";
|
|
3596
|
+
authorPost("/api/settings/open-keys-folder", {}).then(function(r){
|
|
3597
|
+
var b = r.body || {};
|
|
3598
|
+
if(b.ok === true) out.textContent = b.opened ? "" : "Could not open it here. Your keys folder: " + (b.path || "(unknown)");
|
|
3599
|
+
else out.textContent = settingsRefusal(r);
|
|
3600
|
+
}, function(){ out.textContent = networkText(); });
|
|
3601
|
+
});
|
|
3602
|
+
document.getElementById("reload-keys").addEventListener("click", function(){ loadProviders(true); });
|
|
3603
|
+
|
|
3604
|
+
// M4d Providers layout F. One parent block per key (.pv-key): its key band (name, API shape select, Base URL box, the legacy tick, tokens,
|
|
3605
|
+
// the Refresh button), a folded summary, and its model lines as child rows (.pv-mrow). The key row owns shape, Base URL and the legacy
|
|
3606
|
+
// tick, so a model line can never drift from them. Shape and Base URL are always editable; a change of either saves both (judged together).
|
|
3607
|
+
var folds = {}; // slot -> true (unfolded) / false (folded): set once, on first paint (a key with a ticked model starts unfolded), then only the person changes it
|
|
3608
|
+
var typedOpen = {}; // slot -> true while "+ type a model name" is open
|
|
3609
|
+
function buildProviderRow(row){
|
|
3610
|
+
var key = document.createElement("div");
|
|
3611
|
+
key.className = "pv-key";
|
|
3612
|
+
key.setAttribute("data-slot", row.slot);
|
|
3613
|
+
key.setAttribute("data-testid", "provider-row-" + row.slot);
|
|
3614
|
+
key.innerHTML =
|
|
3615
|
+
'<div class="pv-krow">' +
|
|
3616
|
+
'<div class="pv-cell pv-name nolbl"><button class="pv-fold" type="button" data-act="fold" data-testid="pv-fold"></button><code></code><span class="pv-status" data-f="status" data-testid="pv-status-' + escapeXml(row.slot) + '"></span><span class="pv-sum" data-f="sum" data-testid="pv-sum"></span></div>' +
|
|
3617
|
+
'<div class="pv-cell"><span class="pv-lbl">API shape</span><select data-prov="shape" data-testid="prov-shape" aria-label="' + escapeXml(row.label + " API shape") + '"></select></div>' +
|
|
3618
|
+
'<div class="pv-cell pv-c-url"><span class="pv-lbl">Base URL</span><input type="text" data-prov="baseUrl" data-testid="prov-baseurl" aria-label="' + escapeXml(row.label + " Base URL") + '" autocomplete="off" spellcheck="false" placeholder="type the Base URL"></div>' +
|
|
3619
|
+
'<div class="pv-cell"><span class="pv-lbl">Old max_tokens</span><label class="pv-legacy"><input type="checkbox" data-prov="legacyMaxTokens" data-testid="prov-legacy" aria-label="' + escapeXml(row.label + " legacy max_tokens") + '"><span data-f="legacy-word">off</span></label></div>' +
|
|
3620
|
+
'<div class="pv-cell pv-tok pv-ktok"><span class="pv-lbl">Tokens</span><span data-f="tokens" data-testid="pv-tokens-' + escapeXml(row.slot) + '"></span></div>' +
|
|
3621
|
+
'<div class="pv-cell pv-refcell nolbl"><div class="pv-refbox"><div class="pv-refbtns"><button class="btn small pv-refbtn" type="button" data-act="test-key" data-testid="prov-test" aria-label="' + escapeXml("Test " + row.envVar) + '">Test</button><button class="btn small pv-refbtn" type="button" data-act="refresh-models" data-testid="prov-refresh" aria-label="' + escapeXml("Refresh models for " + row.label) + '">⟳ Refresh</button></div><div class="hint pv-msg" data-f="msg-test" data-testid="prov-msg-test" role="status"></div></div></div>' +
|
|
3622
|
+
'</div>' +
|
|
3623
|
+
'<div class="pv-knote"><div class="hint bad pv-msg" data-f="msg-edit" data-testid="prov-msg-edit"></div><div class="hint pv-msg" data-f="msg-legacyMaxTokens" data-testid="prov-msg-legacyMaxTokens"></div></div>' +
|
|
3624
|
+
'<div class="pv-models" data-models-for="' + escapeXml(row.slot) + '" data-testid="models-row-' + escapeXml(row.slot) + '">' +
|
|
3625
|
+
'<div class="pv-mhead" aria-hidden="true"><span class="pv-um"><span class="pv-uh">USE</span><span>MODEL</span></span><span class="pv-pair"><span>IN $/1M</span><span>OUT $/1M</span></span><span class="pv-tok">TOKENS</span></div>' +
|
|
3626
|
+
'<div data-f="models"></div>' +
|
|
3627
|
+
'<div class="pv-more"><button class="pv-add" type="button" data-act="typed-open" data-testid="model-typed-open">+ type a model name</button>' +
|
|
3628
|
+
'<div class="pv-typed" data-f="typed" hidden><input type="text" data-typed="1" data-testid="model-typed" autocomplete="off" spellcheck="false" aria-label="' + escapeXml(row.label + " model name") + '" placeholder="type a model name"> ' +
|
|
3629
|
+
'<button class="btn small" type="button" data-act="add-model" data-testid="model-add">Add</button><div class="hint pv-msg" data-f="typed-msg" data-testid="model-typed-msg"></div></div></div>' +
|
|
3630
|
+
'</div>';
|
|
3631
|
+
key.querySelector("code").textContent = row.envVar;
|
|
3632
|
+
key.querySelectorAll("input").forEach(function(inp){
|
|
3633
|
+
inp.addEventListener("input", function(){ inp.setAttribute("data-dirty", "1"); });
|
|
3634
|
+
});
|
|
3635
|
+
key.querySelector("select").addEventListener("change", function(ev){ ev.target.setAttribute("data-dirty", "1"); });
|
|
3636
|
+
return key;
|
|
3476
3637
|
}
|
|
3477
|
-
//
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3638
|
+
// One child row per model: [Use][name], In + Out side by side, Tokens. The price pair is both-or-neither; the server's one rule says why not.
|
|
3639
|
+
function buildModelLine(slot, m){
|
|
3640
|
+
var d = document.createElement("div");
|
|
3641
|
+
d.className = "pv-mrow";
|
|
3642
|
+
d.setAttribute("data-model", m.model);
|
|
3643
|
+
d.setAttribute("data-testid", "model-line");
|
|
3644
|
+
d.innerHTML =
|
|
3645
|
+
'<div class="pv-cell pv-um nolbl"><span class="pv-m-use"><input type="checkbox" data-mf="use" data-testid="model-use"></span><code class="pv-m-name"></code></div>' +
|
|
3646
|
+
'<div class="pv-cell pv-pair"><label><input type="text" inputmode="decimal" data-mf="inPerM" data-testid="model-in" autocomplete="off"></label>' +
|
|
3647
|
+
'<label><input type="text" inputmode="decimal" data-mf="outPerM" data-testid="model-out" autocomplete="off"></label></div>' +
|
|
3648
|
+
'<div class="pv-cell pv-tok pv-m-tok"><span class="pv-lbl">Tokens</span><span data-f="model-tokens" data-testid="model-tokens"></span></div>' +
|
|
3649
|
+
'<div class="hint pv-msg pv-m-msg" data-f="msg-model" data-testid="model-msg"></div>';
|
|
3650
|
+
d.querySelector(".pv-m-name").textContent = m.model;
|
|
3651
|
+
d.querySelector("[data-mf=use]").setAttribute("aria-label", slot + " " + m.model + " use");
|
|
3652
|
+
d.querySelector("[data-mf=inPerM]").setAttribute("aria-label", m.model + " price in per 1M tokens");
|
|
3653
|
+
d.querySelector("[data-mf=outPerM]").setAttribute("aria-label", m.model + " price out per 1M tokens");
|
|
3654
|
+
d.querySelectorAll("input[type=text]").forEach(function(inp){
|
|
3655
|
+
inp.addEventListener("input", function(){ inp.setAttribute("data-dirty", "1"); });
|
|
3656
|
+
});
|
|
3657
|
+
return d;
|
|
3486
3658
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3659
|
+
// The provider's own model list per key (asked on page open, Reload keys, Refresh models): { models: [ids] } or { why }. Not saved: only ticked or typed lines are.
|
|
3660
|
+
var listed = {};
|
|
3661
|
+
var lastData = null;
|
|
3662
|
+
function linesFor(row){
|
|
3663
|
+
var l = listed[row.slot];
|
|
3664
|
+
var have = {};
|
|
3665
|
+
row.models.forEach(function(m){ have[m.model] = true; });
|
|
3666
|
+
var extra = (l && l.models ? l.models : []).filter(function(id){ return !have[id]; }).map(function(id){ return { model: id, use: false }; });
|
|
3667
|
+
return row.models.concat(extra);
|
|
3668
|
+
}
|
|
3669
|
+
// "3 models · none ticked", "2 models · 1 ticked"; a list that failed with nothing saved says so.
|
|
3670
|
+
function sumText(row){
|
|
3671
|
+
var l = listed[row.slot];
|
|
3672
|
+
var lines = linesFor(row);
|
|
3673
|
+
if(l && l.why && lines.length === 0) return "failed — type a name";
|
|
3674
|
+
var ticked = lines.filter(function(m){ return m.use === true; }).length;
|
|
3675
|
+
return lines.length + (lines.length === 1 ? " model" : " models") + " · " + (ticked === 0 ? "none ticked" : ticked + " ticked");
|
|
3676
|
+
}
|
|
3677
|
+
function paintModels(row, mr){
|
|
3678
|
+
var box = mr.querySelector("[data-f=models]");
|
|
3679
|
+
var lines = linesFor(row);
|
|
3680
|
+
var l = listed[row.slot];
|
|
3681
|
+
var typed = mr.querySelector("[data-f=typed]");
|
|
3682
|
+
typed.hidden = typedOpen[row.slot] !== true;
|
|
3683
|
+
var wanted = lines.map(function(m){ return m.model; });
|
|
3684
|
+
Array.prototype.slice.call(box.querySelectorAll(".pv-mrow")).forEach(function(el){
|
|
3685
|
+
if(wanted.indexOf(el.getAttribute("data-model")) < 0) box.removeChild(el);
|
|
3686
|
+
});
|
|
3687
|
+
var note = box.querySelector("[data-f=models-note]");
|
|
3688
|
+
lines.forEach(function(m){
|
|
3689
|
+
var line = Array.prototype.find.call(box.querySelectorAll(".pv-mrow"), function(el){ return el.getAttribute("data-model") === m.model; });
|
|
3690
|
+
if(!line){ line = buildModelLine(row.slot, m); box.insertBefore(line, note); }
|
|
3691
|
+
var use = line.querySelector("[data-mf=use]");
|
|
3692
|
+
use.checked = m.use === true;
|
|
3693
|
+
["inPerM", "outPerM"].forEach(function(f){
|
|
3694
|
+
var inp = line.querySelector('[data-mf="' + f + '"]');
|
|
3695
|
+
inp.placeholder = "—";
|
|
3696
|
+
if(inp.getAttribute("data-dirty") !== "1") inp.value = typeof m[f] === "number" ? String(m[f]) : "";
|
|
3697
|
+
});
|
|
3698
|
+
line.querySelector("[data-f=model-tokens]").textContent = typeof m.tokens === "number" ? tokensText(m.tokens) : "—";
|
|
3699
|
+
});
|
|
3700
|
+
note = box.querySelector("[data-f=models-note]");
|
|
3701
|
+
if(!note){ note = document.createElement("div"); note.className = "hint pv-more"; note.setAttribute("data-f", "models-note"); note.setAttribute("data-testid", "models-note"); box.appendChild(note); }
|
|
3702
|
+
setNote(note, false, l && l.why ? l.why + " Type the model name below." : (lines.length === 0 ? "No models yet. Press ⟳ to refresh models, or type a name." : ""));
|
|
3703
|
+
note.className += " pv-more" + (l && l.why ? " pv-failed" : "");
|
|
3704
|
+
box.parentNode.querySelector(".pv-mhead").hidden = lines.length === 0;
|
|
3705
|
+
}
|
|
3706
|
+
var STATUS_WORD = { "set in file": "set in file", "found in shell": "found in shell", "not set": "not set", "refused": "refused — see above" };
|
|
3707
|
+
// The Refresh button: "[ ⟳ Refresh ]", disabled "[ ⟳ refreshing… ]" while the free list call is out.
|
|
3708
|
+
function setRefreshBtn(key, pending){
|
|
3709
|
+
var b = key.querySelector("[data-act=refresh-models]");
|
|
3710
|
+
b.disabled = pending === true;
|
|
3711
|
+
b.innerHTML = pending === true ? "⟳ refreshing…" : "⟳ Refresh";
|
|
3712
|
+
}
|
|
3713
|
+
// The Test button: "[ Test ]", disabled "[ testing… ]" while the free call is out. The route says ok + ms or a plain-words why; written on the key row's own note.
|
|
3714
|
+
function testKey(slot, key){
|
|
3715
|
+
var b = key.querySelector("[data-act=test-key]");
|
|
3716
|
+
var msg = key.querySelector("[data-f=msg-test]");
|
|
3717
|
+
var row = lastData && lastData.rows.filter(function(r){ return r.slot === slot; })[0];
|
|
3718
|
+
if(row && row.baseUrl === ""){
|
|
3719
|
+
setNote(msg, true, "Type a Base URL for " + row.envVar + " in Settings.");
|
|
3495
3720
|
return;
|
|
3496
3721
|
}
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3722
|
+
b.disabled = true;
|
|
3723
|
+
b.innerHTML = "testing…";
|
|
3724
|
+
setNote(msg, false, "");
|
|
3725
|
+
authorPost("/api/settings/test", { slot: slot }).then(function(r){
|
|
3726
|
+
var d = r.body;
|
|
3727
|
+
if(d && d.ok === true) setNote(msg, false, "OK \u00b7 " + d.ms + " ms");
|
|
3728
|
+
else setNote(msg, true, d && d.why ? d.why : settingsRefusal(r));
|
|
3729
|
+
}, function(){ setNote(msg, true, networkText()); }).then(function(){
|
|
3730
|
+
b.disabled = false;
|
|
3731
|
+
b.innerHTML = "Test";
|
|
3505
3732
|
});
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3733
|
+
}
|
|
3734
|
+
function refreshModels(slot, tr){
|
|
3735
|
+
listed[slot] = { pending: true };
|
|
3736
|
+
setRefreshBtn(tr, true);
|
|
3737
|
+
return authorPost("/api/settings/models", { slot: slot }).then(function(r){
|
|
3738
|
+
var b = r.body;
|
|
3739
|
+
listed[slot] = b && b.ok ? { models: b.models } : { why: (b && b.why) ? b.why : settingsRefusal(r) };
|
|
3740
|
+
}, function(){ listed[slot] = { why: networkText() }; }).then(function(){
|
|
3741
|
+
if(lastData) paintProviders(lastData, false);
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
function paintProviders(data, list){
|
|
3745
|
+
lastData = data;
|
|
3746
|
+
var path = data.keysFile && data.keysFile.path ? data.keysFile.path : "~/.config/bareloop/.env";
|
|
3747
|
+
document.getElementById("keys-path").textContent = path;
|
|
3748
|
+
setNote(document.getElementById("keys-refusal"), true, data.keysFile && data.keysFile.refusal);
|
|
3749
|
+
setNote(pvNote, !!data.configProblem, data.configProblem || PV_NOTE_DEFAULT);
|
|
3750
|
+
data.rows.forEach(function(row){
|
|
3751
|
+
var key = pvRows.querySelector('.pv-key[data-slot="' + row.slot + '"]');
|
|
3752
|
+
if(!key){ key = buildProviderRow(row); pvRows.appendChild(key); }
|
|
3753
|
+
if(folds[row.slot] === undefined) folds[row.slot] = row.models.some(function(m){ return m.use === true; });
|
|
3754
|
+
key.setAttribute("data-open", folds[row.slot] ? "1" : "0");
|
|
3755
|
+
var fold = key.querySelector("[data-act=fold]");
|
|
3756
|
+
fold.textContent = folds[row.slot] ? "▾" : "▸";
|
|
3757
|
+
fold.setAttribute("aria-expanded", folds[row.slot] ? "true" : "false");
|
|
3758
|
+
fold.setAttribute("aria-label", (folds[row.slot] ? "Fold " : "Unfold ") + row.label + " models");
|
|
3759
|
+
paintModels(row, key.querySelector(".pv-models"));
|
|
3760
|
+
setRefreshBtn(key, !!(listed[row.slot] && listed[row.slot].pending));
|
|
3761
|
+
key.querySelector("[data-f=sum]").textContent = sumText(row);
|
|
3762
|
+
var st = key.querySelector("[data-f=status]");
|
|
3763
|
+
var word = STATUS_WORD[row.keyStatus] || row.keyStatus;
|
|
3764
|
+
st.className = row.keyStatus === "set in file" || row.keyStatus === "found in shell" ? "pv-status pv-ok" : "pv-status hint";
|
|
3765
|
+
st.textContent = st.className.indexOf("pv-ok") >= 0 ? "✓" : word;
|
|
3766
|
+
st.setAttribute("title", word);
|
|
3767
|
+
if(st.className.indexOf("pv-ok") >= 0) st.setAttribute("aria-label", word); else st.removeAttribute("aria-label");
|
|
3768
|
+
var legacyIn = key.querySelector('[data-prov=legacyMaxTokens]');
|
|
3769
|
+
if(document.activeElement !== legacyIn) legacyIn.checked = row.legacyMaxTokens === true;
|
|
3770
|
+
key.querySelector("[data-f=legacy-word]").textContent = legacyIn.checked ? "on" : "off";
|
|
3771
|
+
// shape + Base URL: the key row's own boxes, written from the server's row unless the person has typed in them (a refused value stays typed, with its sentence)
|
|
3772
|
+
var sel = key.querySelector("[data-prov=shape]");
|
|
3773
|
+
if(sel.options.length !== data.shapes.length || data.shapes.some(function(sh, i){ return sel.options[i].value !== sh.id; })){
|
|
3774
|
+
sel.textContent = "";
|
|
3775
|
+
data.shapes.forEach(function(sh){ var o = document.createElement("option"); o.value = sh.id; o.textContent = sh.label; sel.appendChild(o); });
|
|
3776
|
+
}
|
|
3777
|
+
if(sel.getAttribute("data-dirty") !== "1") sel.value = row.shape;
|
|
3778
|
+
var urlIn = key.querySelector("[data-prov=baseUrl]");
|
|
3779
|
+
if(urlIn.getAttribute("data-dirty") !== "1" && document.activeElement !== urlIn) urlIn.value = row.baseUrl;
|
|
3780
|
+
key.querySelector("[data-f=tokens]").textContent = tokensText(row.tokens);
|
|
3781
|
+
});
|
|
3782
|
+
// rows that left the keys file leave the page
|
|
3783
|
+
Array.prototype.slice.call(pvRows.querySelectorAll(".pv-key")).forEach(function(key){
|
|
3784
|
+
if(!data.rows.some(function(r){ return r.slot === key.getAttribute("data-slot"); })) pvRows.removeChild(key);
|
|
3785
|
+
});
|
|
3786
|
+
if(list) data.rows.forEach(function(row){
|
|
3787
|
+
var key = pvRows.querySelector('.pv-key[data-slot="' + row.slot + '"]');
|
|
3788
|
+
if(row.baseUrl === ""){ listed[row.slot] = { why: "Type a Base URL for " + row.slot + " in Settings." }; paintProviders(lastData, false); }
|
|
3789
|
+
else refreshModels(row.slot, key);
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
// Shape and Base URL save together, on change of either (blur / Enter / pick); the server judges them as a pair and says which sentence on
|
|
3793
|
+
// a refusal (nothing saved, the typed value stays so the person can fix it).
|
|
3794
|
+
function saveEndpoint(key){
|
|
3795
|
+
var msg = key.querySelector("[data-f=msg-edit]");
|
|
3796
|
+
var payload = { slot: key.getAttribute("data-slot"), shape: key.querySelector("[data-prov=shape]").value, baseUrl: key.querySelector("[data-prov=baseUrl]").value.trim() };
|
|
3797
|
+
authorPost("/api/settings/provider", payload).then(function(r){
|
|
3798
|
+
if(r.body && r.body.ok){
|
|
3799
|
+
key.querySelector("[data-prov=shape]").removeAttribute("data-dirty");
|
|
3800
|
+
key.querySelector("[data-prov=baseUrl]").removeAttribute("data-dirty");
|
|
3801
|
+
setNote(msg, true, "");
|
|
3802
|
+
loadProviders();
|
|
3803
|
+
} else setNote(msg, true, settingsRefusal(r));
|
|
3804
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3805
|
+
}
|
|
3806
|
+
pvRows.addEventListener("click", function(ev){
|
|
3807
|
+
var b = ev.target.closest && ev.target.closest("button[data-act]");
|
|
3808
|
+
if(!b) return;
|
|
3809
|
+
var key = b.closest(".pv-key");
|
|
3810
|
+
var slot = key.getAttribute("data-slot");
|
|
3811
|
+
var act = b.getAttribute("data-act");
|
|
3812
|
+
if(act === "refresh-models") refreshModels(slot, key);
|
|
3813
|
+
else if(act === "test-key") testKey(slot, key);
|
|
3814
|
+
else if(act === "fold"){ folds[slot] = !folds[slot]; paintProviders(lastData, false); }
|
|
3815
|
+
else if(act === "typed-open"){ typedOpen[slot] = !typedOpen[slot]; paintProviders(lastData, false); }
|
|
3816
|
+
else if(act === "add-model") addTypedModel(slot, b.closest("[data-f=typed]"));
|
|
3522
3817
|
});
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3818
|
+
// Saved like bareloop saves a row field: on change (blur or Enter), no Save button. The legacy tick saves alone; shape and Base URL save as a pair.
|
|
3819
|
+
pvRows.addEventListener("change", function(ev){
|
|
3820
|
+
var inp = ev.target;
|
|
3821
|
+
if(!inp.getAttribute) return;
|
|
3822
|
+
if(inp.getAttribute("data-prov") === "legacyMaxTokens"){ saveProvider(inp.closest(".pv-key"), inp); return; }
|
|
3823
|
+
if(inp.getAttribute("data-prov") === "shape" || inp.getAttribute("data-prov") === "baseUrl"){ saveEndpoint(inp.closest(".pv-key")); return; }
|
|
3824
|
+
if(!inp.getAttribute("data-mf")) return;
|
|
3825
|
+
saveModel(inp.closest(".pv-models") ? inp.closest(".pv-models").getAttribute("data-models-for") : "", inp.closest(".pv-mrow"));
|
|
3531
3826
|
});
|
|
3827
|
+
// A box the person typed: empty = back to the default (null); a number is sent as a number; anything else is
|
|
3828
|
+
// sent as the text typed, so the server refuses it by naming the box.
|
|
3829
|
+
function priceValue(text){
|
|
3830
|
+
var t = String(text).trim();
|
|
3831
|
+
if(t === "") return null;
|
|
3832
|
+
var n = Number(t);
|
|
3833
|
+
return isFinite(n) ? n : t;
|
|
3834
|
+
}
|
|
3835
|
+
// One model line per save: {slot, model, use, inPerM, outPerM}. A refusal is the line's hint (the server's one rule),
|
|
3836
|
+
// and nothing was saved: the repaint puts the tick back.
|
|
3837
|
+
function saveModel(slot, line){
|
|
3838
|
+
var msg = line.querySelector("[data-f=msg-model]");
|
|
3839
|
+
var payload = {
|
|
3840
|
+
slot: slot,
|
|
3841
|
+
model: line.getAttribute("data-model"),
|
|
3842
|
+
use: line.querySelector("[data-mf=use]").checked,
|
|
3843
|
+
inPerM: priceValue(line.querySelector("[data-mf=inPerM]").value),
|
|
3844
|
+
outPerM: priceValue(line.querySelector("[data-mf=outPerM]").value)
|
|
3845
|
+
};
|
|
3846
|
+
authorPost("/api/settings/model", payload).then(function(r){
|
|
3847
|
+
if(r.body && r.body.ok){
|
|
3848
|
+
line.querySelectorAll("input[type=text]").forEach(function(inp){ inp.removeAttribute("data-dirty"); });
|
|
3849
|
+
loadProviders();
|
|
3850
|
+
setNote(msg, false, "saved — applies from the next model call");
|
|
3851
|
+
} else { setNote(msg, true, settingsRefusal(r)); loadProviders(); }
|
|
3852
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// A model name typed because the list failed: saved as an unticked, empty line (typed lines are saved); the person then fills the prices and ticks it.
|
|
3856
|
+
function addTypedModel(slot, box){
|
|
3857
|
+
var inp = box.querySelector("input");
|
|
3858
|
+
var msg = box.querySelector("[data-f=typed-msg]");
|
|
3859
|
+
authorPost("/api/settings/model", { slot: slot, model: inp.value.trim(), use: false }).then(function(r){
|
|
3860
|
+
if(r.body && r.body.ok){ inp.value = ""; setNote(msg, false, "added"); loadProviders(); }
|
|
3861
|
+
else setNote(msg, true, settingsRefusal(r));
|
|
3862
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
// The legacy tick saves alone: {slot, legacyMaxTokens}. A refusal is the cell's hint line.
|
|
3866
|
+
function saveProvider(key, inp){
|
|
3867
|
+
var field = inp.getAttribute("data-prov");
|
|
3868
|
+
var msg = key.querySelector("[data-f=msg-" + field + "]");
|
|
3869
|
+
var payload = { slot: key.getAttribute("data-slot") };
|
|
3870
|
+
payload[field] = inp.checked;
|
|
3871
|
+
authorPost("/api/settings/provider", payload).then(function(r){
|
|
3872
|
+
if(r.body && r.body.ok){
|
|
3873
|
+
inp.removeAttribute("data-dirty");
|
|
3874
|
+
loadProviders();
|
|
3875
|
+
setNote(msg, false, "saved — applies from the next model call");
|
|
3876
|
+
} else setNote(msg, true, settingsRefusal(r));
|
|
3877
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
document.getElementById("tab-providers").addEventListener("click", function(){ loadProviders(); });
|
|
3532
3881
|
wireTabs([document.getElementById("tab-providers"), document.getElementById("tab-money")]);
|
|
3533
|
-
document.getElementById("tab-providers").addEventListener("click", loadProviders);
|
|
3534
3882
|
// Auto-save on a human edit (change = blur or Enter): POST carries the per-start token; the chat has no route here
|
|
3535
3883
|
limitInput.addEventListener("change", function(){
|
|
3536
3884
|
var text = limitInput.value.trim();
|
|
@@ -3564,6 +3912,9 @@
|
|
|
3564
3912
|
var actionErrEl = document.getElementById("chat-action-error");
|
|
3565
3913
|
var moneyCapInput = document.getElementById("jf-cap-money");
|
|
3566
3914
|
var modelSelect = document.getElementById("jf-model");
|
|
3915
|
+
var thinkingBox = document.getElementById("jf-thinking");
|
|
3916
|
+
var thinkingNote = document.getElementById("jf-thinking-note");
|
|
3917
|
+
var lineThinking = {}; // option value -> the server's {support, canChange, say} for that Model line
|
|
3567
3918
|
var modelStatusEl = document.getElementById("jf-model-status");
|
|
3568
3919
|
var progressRow = document.getElementById("chat-progress-row");
|
|
3569
3920
|
|
|
@@ -3594,10 +3945,10 @@
|
|
|
3594
3945
|
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3595
3946
|
var originLine = document.getElementById("chat-origin-line");
|
|
3596
3947
|
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3597
|
-
var LOCKED_IDS = ["jf-name", "jf-
|
|
3948
|
+
var LOCKED_IDS = ["jf-name", "jf-job"];
|
|
3598
3949
|
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3599
3950
|
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3600
|
-
var OPEN_IDS = ["jf-
|
|
3951
|
+
var OPEN_IDS = ["jf-inputs", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3601
3952
|
var reuseOn = false;
|
|
3602
3953
|
// Resume mode (hamr 2026-10-07): the card shows the signed job of one run, every box locked except the two caps.
|
|
3603
3954
|
// `resumeRun` = {runid, noTime} while it is on, else null. Never set together with a drafting session or a reuse pick.
|
|
@@ -3608,19 +3959,22 @@
|
|
|
3608
3959
|
var resuming = resumeRun !== null;
|
|
3609
3960
|
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3610
3961
|
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live || resuming); });
|
|
3611
|
-
|
|
3612
|
-
|
|
3962
|
+
// the one exception on a resume card: an older spec that names no model leaves the Model box open (picked at resume)
|
|
3963
|
+
var modelOpen = resuming && resumeRun.pickModel === true;
|
|
3964
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1 && !(modelOpen && id === "jf-model"))); });
|
|
3965
|
+
document.getElementById("jf-model").disabled = live || (resuming && !modelOpen);
|
|
3613
3966
|
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3614
3967
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3615
3968
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3616
|
-
|
|
3969
|
+
syncThinking();
|
|
3617
3970
|
}
|
|
3618
3971
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3619
3972
|
function clearStartFrom(keepBoxes){
|
|
3620
3973
|
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes — except when a Check type
|
|
3621
3974
|
// radio click leaves reuse (keepBoxes): a radio never blanks what the person has on the card
|
|
3622
3975
|
if(startFrom && !keepBoxes){
|
|
3623
|
-
|
|
3976
|
+
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3977
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
3624
3978
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3625
3979
|
}
|
|
3626
3980
|
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
@@ -3632,7 +3986,7 @@
|
|
|
3632
3986
|
// ---- Reuse workflow picker (hamr 2026-10-06): Check type is ONE radio group of three; "Reuse workflow" shows a search
|
|
3633
3987
|
// box over the jobs that have a green local run (GET /api/author/reuse-jobs), a pick fills the card through the SAME
|
|
3634
3988
|
// path the Run tab's button uses (reuseWorkflow -> bareloop-reuse -> applyReuse), then a picked box with `change`.
|
|
3635
|
-
var CARD_BOX_IDS = ["jf-name", "jf-
|
|
3989
|
+
var CARD_BOX_IDS = ["jf-name", "jf-job", "jf-inputs", "jf-dest", "jf-cap-money", "jf-cap-time"];
|
|
3636
3990
|
var rwBox = document.getElementById("rw");
|
|
3637
3991
|
var rwSearch = document.getElementById("rw-search");
|
|
3638
3992
|
var rwQ = document.getElementById("rw-q");
|
|
@@ -3649,9 +4003,11 @@
|
|
|
3649
4003
|
}
|
|
3650
4004
|
function setVerdict(v){
|
|
3651
4005
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === v); });
|
|
4006
|
+
setJobPlaceholders(v);
|
|
3652
4007
|
}
|
|
3653
4008
|
function clearCardBoxes(){
|
|
3654
4009
|
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
4010
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
3655
4011
|
document.getElementById("jf-cap-note").textContent = "";
|
|
3656
4012
|
}
|
|
3657
4013
|
// ONE function shows/hides the picker: visible only while the radio is on Reuse workflow; the search box before a
|
|
@@ -3688,7 +4044,7 @@
|
|
|
3688
4044
|
}
|
|
3689
4045
|
}
|
|
3690
4046
|
function loadReuseJobs(){
|
|
3691
|
-
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?
|
|
4047
|
+
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?" + pickQuery(modelSelect.value) : "")).then(function(r){
|
|
3692
4048
|
rwFailed = !(r && r.ok);
|
|
3693
4049
|
rwJobs = rwFailed ? null : r.jobs;
|
|
3694
4050
|
renderReuseList();
|
|
@@ -3699,14 +4055,13 @@
|
|
|
3699
4055
|
function openPicker(){
|
|
3700
4056
|
rwQ.value = ""; rwHl = 0;
|
|
3701
4057
|
rwJobs = null; rwFailed = false; // a fresh open shows "Loading…", never the previous list
|
|
3702
|
-
document.getElementById("jf-judge").disabled = true;
|
|
3703
4058
|
renderPicker();
|
|
3704
4059
|
renderReuseList();
|
|
3705
4060
|
loadReuseJobs();
|
|
3706
4061
|
refreshStartEnabled();
|
|
3707
4062
|
rwQ.focus();
|
|
3708
4063
|
}
|
|
3709
|
-
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, modelSelect.value); }
|
|
4064
|
+
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, pickQuery(modelSelect.value)); }
|
|
3710
4065
|
rwQ.addEventListener("input", function(){ rwHl = 0; renderReuseList(); });
|
|
3711
4066
|
rwQ.addEventListener("keydown", function(e){
|
|
3712
4067
|
if(e.key === "ArrowDown"){ rwHl = Math.min(rwHl + 1, rwShown.length - 1); renderReuseList(); e.preventDefault(); }
|
|
@@ -3726,7 +4081,9 @@
|
|
|
3726
4081
|
document.getElementById("rw-change").addEventListener("click", unpickReuse);
|
|
3727
4082
|
// leaving resume mode: the title, the hidden drafting chrome, the time box and the resume box all come back
|
|
3728
4083
|
function exitResumeMode(){
|
|
4084
|
+
var filtered = resumeRun !== null && !!resumeRun.worker;
|
|
3729
4085
|
resumeRun = null;
|
|
4086
|
+
if(filtered){ renderModelOptions(); }
|
|
3730
4087
|
originLine.classList.remove("res");
|
|
3731
4088
|
document.getElementById("job-card-title").textContent = "Job card \u2014 draft";
|
|
3732
4089
|
document.getElementById("chat-resume-box").hidden = true;
|
|
@@ -3740,7 +4097,7 @@
|
|
|
3740
4097
|
card.hidden = false;
|
|
3741
4098
|
originLine.hidden = true;
|
|
3742
4099
|
exitResumeMode();
|
|
3743
|
-
thread.innerHTML = "";
|
|
4100
|
+
thread.innerHTML = ""; lastMsgSig = null;
|
|
3744
4101
|
errEl.textContent = "";
|
|
3745
4102
|
actionErrEl.textContent = "";
|
|
3746
4103
|
sessionId = null;
|
|
@@ -3770,8 +4127,8 @@
|
|
|
3770
4127
|
function resetCard(){
|
|
3771
4128
|
clearCardBoxes();
|
|
3772
4129
|
setVerdict("deterministic");
|
|
3773
|
-
document.getElementById("jf-judge").disabled = true;
|
|
3774
4130
|
modelSelect.selectedIndex = 0;
|
|
4131
|
+
setThinkingTick(false);
|
|
3775
4132
|
msgInput.value = "";
|
|
3776
4133
|
rwQ.value = "";
|
|
3777
4134
|
lastPhase = null;
|
|
@@ -3797,14 +4154,14 @@
|
|
|
3797
4154
|
return {
|
|
3798
4155
|
// on "Reuse workflow" the type is the picked job's own, never the radio's
|
|
3799
4156
|
checkType: (verdictValue() === "reuse" ? reuseCheckType : verdictValue()) === "rubric" ? "rubric" : "deterministic",
|
|
3800
|
-
|
|
4157
|
+
provider: pickParts(document.getElementById("jf-model").value).provider,
|
|
4158
|
+
model: pickParts(document.getElementById("jf-model").value).model,
|
|
3801
4159
|
jobName: document.getElementById("jf-name").value.trim(),
|
|
3802
|
-
|
|
3803
|
-
|
|
4160
|
+
jobText: document.getElementById("jf-job").value.trim(),
|
|
4161
|
+
inputs: document.getElementById("jf-inputs").value.trim(),
|
|
3804
4162
|
destination: document.getElementById("jf-dest").value.trim(),
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
judgeExamples: document.getElementById("jf-judge").value.trim(),
|
|
4163
|
+
// the "Thinking on" tick: sent as a boolean; the server judges it per line and signs it only when ticked
|
|
4164
|
+
thinking: thinkingBox.checked === true,
|
|
3808
4165
|
capUsd: moneyCap,
|
|
3809
4166
|
maxWallMs: timeCapMin === "" ? undefined : Math.round(Number(timeCapMin) * 60000),
|
|
3810
4167
|
};
|
|
@@ -3822,6 +4179,7 @@
|
|
|
3822
4179
|
var capOk = moneyCapInput.value.trim() !== "" && Number.isFinite(v) && v > 0;
|
|
3823
4180
|
// on Reuse workflow nothing starts before a job is picked
|
|
3824
4181
|
startOk = capOk && !sessionLive && keyOk && (verdictValue() !== "reuse" || startFrom !== null);
|
|
4182
|
+
if(typeof syncResumeSign === "function"){ syncResumeSign(); }
|
|
3825
4183
|
syncCardLock();
|
|
3826
4184
|
renderMain();
|
|
3827
4185
|
refreshClearBtn();
|
|
@@ -3853,7 +4211,7 @@
|
|
|
3853
4211
|
setVerdict(v);
|
|
3854
4212
|
renderPicker();
|
|
3855
4213
|
}
|
|
3856
|
-
|
|
4214
|
+
setJobPlaceholders(v);
|
|
3857
4215
|
refreshStartEnabled();
|
|
3858
4216
|
}
|
|
3859
4217
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){
|
|
@@ -3902,21 +4260,30 @@
|
|
|
3902
4260
|
return { keyOk: true, ok: false, text: reachStatusText(reach.status) };
|
|
3903
4261
|
}
|
|
3904
4262
|
function refreshModelStatus(){
|
|
3905
|
-
var modelId = modelSelect.value;
|
|
3906
|
-
if(
|
|
4263
|
+
var modelId = pickParts(modelSelect.value).model;
|
|
4264
|
+
if(modelSelect.value === ""){
|
|
3907
4265
|
keyOk = false;
|
|
3908
4266
|
modelStatusEl.className = "jf-readiness bad";
|
|
3909
|
-
modelStatusEl.textContent = "[✗] no model —
|
|
4267
|
+
modelStatusEl.textContent = "[✗] no model — Tick a model in Settings > Providers.";
|
|
3910
4268
|
refreshStartEnabled();
|
|
3911
4269
|
return;
|
|
3912
4270
|
}
|
|
3913
|
-
|
|
4271
|
+
// the key state of the NEW pick is unknown until its check returns: blocked at once, and a late answer for a line
|
|
4272
|
+
// no longer selected is ignored (it would otherwise unblock Start / Sign & resume for the wrong key)
|
|
4273
|
+
var asked = modelSelect.value;
|
|
4274
|
+
keyOk = false;
|
|
4275
|
+
modelStatusEl.className = "jf-readiness unknown";
|
|
4276
|
+
modelStatusEl.textContent = "checking the key\u2026";
|
|
4277
|
+
refreshStartEnabled();
|
|
4278
|
+
authorGet("/api/author/model-check?" + pickQuery(modelSelect.value)).then(function(r){
|
|
4279
|
+
if(modelSelect.value !== asked) return;
|
|
3914
4280
|
var line = modelStatusLine(r, modelId);
|
|
3915
4281
|
keyOk = line.keyOk;
|
|
3916
4282
|
modelStatusEl.className = "jf-readiness " + (line.ok ? "ok" : "bad");
|
|
3917
4283
|
modelStatusEl.textContent = line.text;
|
|
3918
4284
|
refreshStartEnabled();
|
|
3919
4285
|
}).catch(function(){
|
|
4286
|
+
if(modelSelect.value !== asked) return;
|
|
3920
4287
|
keyOk = false;
|
|
3921
4288
|
modelStatusEl.className = "jf-readiness bad";
|
|
3922
4289
|
modelStatusEl.textContent = "[✗] could not check the key";
|
|
@@ -3928,25 +4295,89 @@
|
|
|
3928
4295
|
function refreshReuseLine(){
|
|
3929
4296
|
if(!startFrom) return;
|
|
3930
4297
|
var q = (startFrom.importId ? "import=" + encodeURIComponent(startFrom.importId) : "runid=" + encodeURIComponent(startFrom.runid)) +
|
|
3931
|
-
"&
|
|
4298
|
+
"&" + pickQuery(modelSelect.value);
|
|
3932
4299
|
authorGet("/api/author/start-from?" + q).then(function(r){
|
|
3933
4300
|
if(startFrom && r && r.ok){ sfLine.textContent = r.line ? rwMeta({checkType: reuseCheckType, line: r.line}) : ""; }
|
|
3934
4301
|
}).catch(function(){ /* the line keeps what it had */ });
|
|
3935
4302
|
}
|
|
3936
4303
|
modelSelect.addEventListener("change", refreshReuseLine);
|
|
3937
4304
|
modelSelect.addEventListener("change", function(){ if(!rwBox.hidden && startFrom === null && !reuseSession) loadReuseJobs(); });
|
|
3938
|
-
//
|
|
3939
|
-
//
|
|
4305
|
+
// P8 — the Model menu IS the Settings rows: every TICKED, PRICED line, labelled "<KEY> · <model>", in keys-file order. An option's
|
|
4306
|
+
// value carries the key AND the model ("KEY|model"): two keys can tick the same model id, and the card names which one.
|
|
4307
|
+
// Re-read whenever Settings loads or saves, keeping the pick if it survives.
|
|
4308
|
+
function pickValue(slot, model){ return (slot || "") + "|" + model; }
|
|
4309
|
+
function pickParts(v){
|
|
4310
|
+
var i = String(v).indexOf("|");
|
|
4311
|
+
return i < 0 ? {provider: "", model: String(v)} : {provider: v.slice(0, i), model: v.slice(i + 1)};
|
|
4312
|
+
}
|
|
4313
|
+
function pickQuery(v){ var p = pickParts(v); return "model=" + encodeURIComponent(p.model) + "&provider=" + encodeURIComponent(p.provider); }
|
|
4314
|
+
// restore a saved card's pick: its key and model; an older card names only the model (the first option holding it)
|
|
4315
|
+
function setPick(c){
|
|
4316
|
+
if(!c || !c.model) return;
|
|
4317
|
+
var want = c.provider ? pickValue(c.provider, c.model) : null;
|
|
4318
|
+
var opts = Array.prototype.slice.call(modelSelect.options);
|
|
4319
|
+
var hit = opts.filter(function(o){ return want ? o.value === want : pickParts(o.value).model === c.model; })[0];
|
|
4320
|
+
if(!hit){
|
|
4321
|
+
hit = document.createElement("option");
|
|
4322
|
+
hit.value = want || pickValue("", c.model);
|
|
4323
|
+
hit.textContent = (c.provider ? c.provider + " \u00b7 " : "") + c.model;
|
|
4324
|
+
modelSelect.appendChild(hit);
|
|
4325
|
+
}
|
|
4326
|
+
modelSelect.value = hit.value;
|
|
4327
|
+
syncThinking();
|
|
4328
|
+
}
|
|
4329
|
+
// The "Thinking on" box follows the SERVER's answer for the picked line (`thinking.canChange`): false greys AND unticks it with the
|
|
4330
|
+
// server's words; 'unverified' leaves it enabled with no note. The tick is the person's; the page decides nothing.
|
|
4331
|
+
function syncThinking(){
|
|
4332
|
+
var t = lineThinking[modelSelect.value];
|
|
4333
|
+
var blocked = !!(t && t.canChange === false);
|
|
4334
|
+
if(blocked) thinkingBox.checked = false;
|
|
4335
|
+
thinkingBox.disabled = blocked || sessionLive || resumeRun !== null || reuseOn;
|
|
4336
|
+
thinkingNote.textContent = blocked ? (t.say || "") : "";
|
|
4337
|
+
document.getElementById("jf-thinking-label").hidden = reuseOn;
|
|
4338
|
+
}
|
|
4339
|
+
function setThinkingTick(on){ thinkingBox.checked = on === true; syncThinking(); }
|
|
4340
|
+
modelSelect.addEventListener("change", syncThinking);
|
|
4341
|
+
var modelReads = 0;
|
|
4342
|
+
var modelLines = [];
|
|
4343
|
+
var modelSay = "";
|
|
4344
|
+
// A Resume card that must PICK a model (old spec naming none) offers only the lines resolveWorker (src/providerrows.js) accepts
|
|
4345
|
+
// for that spec: the same shape, and the same endpoint when the spec names one (`resumeRun.worker`, from start-from, endpoint
|
|
4346
|
+
// already normalised by the server). With none, the sentence names the shape to tick (label from providershapes).
|
|
4347
|
+
function renderModelOptions(){
|
|
4348
|
+
var w = resumeRun !== null && resumeRun.pickModel === true ? resumeRun.worker : null;
|
|
4349
|
+
var lines = modelLines.filter(function(m){ return !w || (m.shape === w.provider && (w.endpoint === "" || m.endpoint === w.endpoint)); });
|
|
4350
|
+
var was = modelSelect.value;
|
|
4351
|
+
lineThinking = {};
|
|
4352
|
+
lines.forEach(function(m){ lineThinking[pickValue(m.slot, m.model)] = m.thinking || null; });
|
|
4353
|
+
var none = (w && modelLines.length) ? "Tick a " + w.label + " model in Settings > Providers." : (modelSay || "Tick a model in Settings > Providers.");
|
|
4354
|
+
modelSelect.innerHTML = lines.length ? lines.map(function(m){
|
|
4355
|
+
return '<option value="' + escapeXml(pickValue(m.slot, m.model)) + '">' + escapeXml(m.label) + "</option>";
|
|
4356
|
+
}).join("") : '<option value="">' + escapeXml(none) + "</option>";
|
|
4357
|
+
if(lines.some(function(m){ return pickValue(m.slot, m.model) === was; })) modelSelect.value = was;
|
|
4358
|
+
syncThinking();
|
|
4359
|
+
}
|
|
3940
4360
|
function loadModels(){
|
|
4361
|
+
if(modelSelect.options.length === 0){
|
|
4362
|
+
modelSelect.innerHTML = '<option value="">Reading your models\u2026</option>';
|
|
4363
|
+
}
|
|
3941
4364
|
authorGet("/api/author/models").then(function(r){
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
4365
|
+
if(!r || !r.ok) throw new Error("read");
|
|
4366
|
+
modelReads = 0;
|
|
4367
|
+
modelLines = r.lines || [];
|
|
4368
|
+
modelSay = r.say || "";
|
|
4369
|
+
renderModelOptions();
|
|
4370
|
+
refreshModelStatus();
|
|
4371
|
+
}).catch(function(){
|
|
4372
|
+
modelReads += 1;
|
|
4373
|
+
if(modelReads <= 5){
|
|
4374
|
+
modelSelect.innerHTML = '<option value="">Could not read your models. Trying again\u2026</option>';
|
|
4375
|
+
setTimeout(loadModels, 1500);
|
|
4376
|
+
} else {
|
|
4377
|
+
modelSelect.innerHTML = '<option value="">Could not read your models. Reload the page.</option>';
|
|
4378
|
+
}
|
|
3948
4379
|
refreshModelStatus();
|
|
3949
|
-
})
|
|
4380
|
+
});
|
|
3950
4381
|
}
|
|
3951
4382
|
document.addEventListener("bareloop-providers-changed", loadModels);
|
|
3952
4383
|
loadModels();
|
|
@@ -3964,11 +4395,23 @@
|
|
|
3964
4395
|
if(role === "system") return "";
|
|
3965
4396
|
return "bareloop";
|
|
3966
4397
|
}
|
|
4398
|
+
// the thread is redrawn only when its messages changed: a 2 s poll tick must not close a fold the person just opened
|
|
4399
|
+
var lastMsgSig = null;
|
|
3967
4400
|
function renderMessages(state){
|
|
4401
|
+
var sig = JSON.stringify(state.messages);
|
|
4402
|
+
if(sig === lastMsgSig) return;
|
|
4403
|
+
lastMsgSig = sig;
|
|
3968
4404
|
thread.innerHTML = state.messages.map(function(m){
|
|
3969
4405
|
var cls = m.role === "you" ? "you" : (m.role === "system" ? "system" : "bot");
|
|
3970
4406
|
var who = chatWhoLabel(m.role);
|
|
3971
4407
|
var whoHtml = who ? ('<div class="who">' + escapeXml(who) + '</div>') : "";
|
|
4408
|
+
if(m.kind === "plan" && m.plan){
|
|
4409
|
+
// P7: the confirm turn's plan is drawn by the one job-plan renderer (the Job tab uses the same)
|
|
4410
|
+
var qs = (m.plan.questions || []).length
|
|
4411
|
+
? '<div class="jp-qs" data-testid="plan-questions"><span class="lbl">AI\'s open questions</span><ul>' +
|
|
4412
|
+
m.plan.questions.map(function(q){ return "<li>" + escapeXml(q) + "</li>"; }).join("") + "</ul><b>Revise if you need to.</b></div>" : "";
|
|
4413
|
+
return '<div class="msg ' + cls + '" data-testid="plan-bubble">' + whoHtml + renderJobPlan(m.plan) + qs + '<div class="jp-goal">' + escapeXml(m.text) + "</div></div>";
|
|
4414
|
+
}
|
|
3972
4415
|
return '<div class="msg ' + cls + '">' + whoHtml + escapeXml(m.text) + '</div>';
|
|
3973
4416
|
}).join("");
|
|
3974
4417
|
thread.scrollTop = thread.scrollHeight;
|
|
@@ -4062,22 +4505,61 @@
|
|
|
4062
4505
|
fitCardBoxes();
|
|
4063
4506
|
}
|
|
4064
4507
|
msgInput.addEventListener("input", renderMain);
|
|
4508
|
+
// 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
|
|
4509
|
+
// number; Inputs numbers every line; empty lines have none. A hidden mirror (same font/width/padding) measures each logical
|
|
4510
|
+
// line's wrapped height so every number stays level with its own line — the same rules src/jobblock.js parses by.
|
|
4511
|
+
var NUMBERED_IDS = ["jf-job", "jf-inputs"];
|
|
4512
|
+
var FIT_IDS = ["jf-job", "jf-inputs", "jf-dest"];
|
|
4513
|
+
function gutterSync(id){
|
|
4514
|
+
var ta = document.getElementById(id), gut = document.getElementById(id + "-gut"), mir = document.getElementById(id + "-mir");
|
|
4515
|
+
if(!ta || !gut || !mir) return;
|
|
4516
|
+
var lines = ta.value.split("\n");
|
|
4517
|
+
var isJob = id === "jf-job";
|
|
4518
|
+
var n = 0;
|
|
4519
|
+
var nums = lines.map(function(l){
|
|
4520
|
+
var t = l.trim();
|
|
4521
|
+
if(t === "" || (isJob && t.charAt(0) === "~")) return "";
|
|
4522
|
+
n += 1; return String(n);
|
|
4523
|
+
});
|
|
4524
|
+
mir.style.width = ta.clientWidth ? ta.clientWidth + "px" : "";
|
|
4525
|
+
mir.innerHTML = lines.map(function(l){ return "<div>" + (l === "" ? "\u200b" : escapeXml(l)) + "</div>"; }).join("");
|
|
4526
|
+
var rows = mir.children;
|
|
4527
|
+
gut.innerHTML = nums.map(function(t, i){
|
|
4528
|
+
var hgt = rows[i] ? rows[i].offsetHeight : 0;
|
|
4529
|
+
return '<div' + (hgt ? ' style="height:' + hgt + 'px"' : "") + ">" + t + "</div>";
|
|
4530
|
+
}).join("");
|
|
4531
|
+
gut.scrollTop = ta.scrollTop;
|
|
4532
|
+
}
|
|
4533
|
+
// the empty-box examples (grey placeholders that vanish on typing) change with the Check type radio
|
|
4534
|
+
var JOB_PLACEHOLDERS = {
|
|
4535
|
+
deterministic: "Fix the failing date tests in src/date.js\n~ don't edit any test file\nMake npm test pass",
|
|
4536
|
+
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",
|
|
4537
|
+
};
|
|
4538
|
+
var INPUTS_PLACEHOLDER = "repo: /home/me/myrepo\nspec: docs/spec.md";
|
|
4539
|
+
function setJobPlaceholders(v){
|
|
4540
|
+
document.getElementById("jf-job").setAttribute("placeholder", JOB_PLACEHOLDERS[v === "rubric" ? "rubric" : "deterministic"]);
|
|
4541
|
+
document.getElementById("jf-inputs").setAttribute("placeholder", INPUTS_PLACEHOLDER);
|
|
4542
|
+
}
|
|
4543
|
+
setJobPlaceholders("deterministic");
|
|
4065
4544
|
// grow a text box with its text (a few rows, then it scrolls inside); ONE function for the ask box and the card boxes.
|
|
4066
4545
|
// A box that is hidden (or not laid out) measures 0 and gets no inline height.
|
|
4067
4546
|
function fitBox(el){
|
|
4068
4547
|
el.style.height = "auto";
|
|
4069
4548
|
if(!el.scrollHeight){ el.style.height = ""; return; }
|
|
4070
4549
|
var border = el.offsetHeight - el.clientHeight;
|
|
4071
|
-
|
|
4550
|
+
var cap = parseFloat(window.getComputedStyle(el).maxHeight);
|
|
4551
|
+
el.style.height = Math.min(el.scrollHeight + border, cap > 0 ? cap : 200) + "px";
|
|
4072
4552
|
}
|
|
4073
4553
|
function fitCardBoxes(){
|
|
4074
|
-
|
|
4554
|
+
FIT_IDS.forEach(function(id){ fitBox(document.getElementById(id)); });
|
|
4555
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
4075
4556
|
}
|
|
4076
4557
|
// live growth: each card box re-fits on its own input (typing and Enter), through the same fitBox
|
|
4077
|
-
|
|
4558
|
+
FIT_IDS.forEach(function(id){
|
|
4078
4559
|
var box = document.getElementById(id);
|
|
4079
|
-
box.addEventListener("input", function(){ fitBox(box); });
|
|
4560
|
+
box.addEventListener("input", function(){ fitBox(box); if(NUMBERED_IDS.indexOf(id) !== -1) gutterSync(id); });
|
|
4080
4561
|
});
|
|
4562
|
+
NUMBERED_IDS.forEach(function(id){ var box = document.getElementById(id); box.addEventListener("scroll", function(){ document.getElementById(id + "-gut").scrollTop = box.scrollTop; }); });
|
|
4081
4563
|
window.addEventListener("resize", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4082
4564
|
document.getElementById("tab-chat").addEventListener("click", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4083
4565
|
// Enter sends through the one main button; Shift+Enter (or IME composition) keeps its newline
|
|
@@ -4177,16 +4659,10 @@
|
|
|
4177
4659
|
reusePickedName = c.jobName || "job";
|
|
4178
4660
|
setVerdict(reuseSession ? "reuse" : reuseCheckType);
|
|
4179
4661
|
if(c.model){
|
|
4180
|
-
|
|
4181
|
-
modelSelect.insertAdjacentHTML("beforeend", '<option value="' + escapeXml(c.model) + '">' + escapeXml(c.model) + "</option>");
|
|
4182
|
-
}
|
|
4183
|
-
modelSelect.value = c.model;
|
|
4662
|
+
setPick(c);
|
|
4184
4663
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4188
|
-
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4189
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4664
|
+
fillCard(c);
|
|
4665
|
+
|
|
4190
4666
|
if(reuseSession) setReuseLocked(true);
|
|
4191
4667
|
renderPicker();
|
|
4192
4668
|
fitCardBoxes();
|
|
@@ -4208,6 +4684,15 @@
|
|
|
4208
4684
|
|
|
4209
4685
|
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4210
4686
|
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4687
|
+
// ONE owner of filling the card from a prefill (a re-attached session, Reuse, Edit in chat, Resume): the six boxes P7 left
|
|
4688
|
+
// (the old Goal/Source/Success/Guardrails/Judge boxes are gone), then every box re-fits and its number gutter re-measures.
|
|
4689
|
+
function fillCard(c){
|
|
4690
|
+
setVal("jf-name", c.jobName); setVal("jf-job", c.jobText); setVal("jf-inputs", c.inputs);
|
|
4691
|
+
setVal("jf-dest", c.destination); setVal("jf-cap-money", c.capUsd);
|
|
4692
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4693
|
+
setThinkingTick(c.thinking === true);
|
|
4694
|
+
fitCardBoxes();
|
|
4695
|
+
}
|
|
4211
4696
|
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
4212
4697
|
// `bareloop-reuse` event), so the card ends up in the same picked state whichever door was used.
|
|
4213
4698
|
function applyReuse(d){
|
|
@@ -4219,12 +4704,9 @@
|
|
|
4219
4704
|
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4220
4705
|
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4221
4706
|
setVerdict("reuse");
|
|
4222
|
-
if(c.model){
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4226
|
-
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4227
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4707
|
+
if(c.model){ setPick(c); }
|
|
4708
|
+
fillCard(c);
|
|
4709
|
+
|
|
4228
4710
|
setReuseLocked(true);
|
|
4229
4711
|
renderPicker();
|
|
4230
4712
|
sfLine.hidden = false;
|
|
@@ -4248,12 +4730,9 @@
|
|
|
4248
4730
|
clearStartFrom();
|
|
4249
4731
|
openNewCard();
|
|
4250
4732
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4251
|
-
if(c.model){
|
|
4252
|
-
|
|
4253
|
-
|
|
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";
|
|
4733
|
+
if(c.model){ setPick(c); }
|
|
4734
|
+
fillCard(c);
|
|
4735
|
+
|
|
4257
4736
|
originLine.textContent = d.origin || "";
|
|
4258
4737
|
originLine.hidden = !d.origin;
|
|
4259
4738
|
renderPicker();
|
|
@@ -4272,17 +4751,16 @@
|
|
|
4272
4751
|
var r = d.resume || {};
|
|
4273
4752
|
clearStartFrom();
|
|
4274
4753
|
openNewCard();
|
|
4754
|
+
// hamr 2026-10-11 Q2(b): a spec that names no model (the server says so) opens the Model box; the pick rides the POST
|
|
4275
4755
|
resumeRun = {runid: d.runid, noTime: typeof r.maxWallMin !== "number"};
|
|
4756
|
+
if((d.prefill || {}).specNamesModel === false){ resumeRun.pickModel = true; if(d.prefill.specWorker){ resumeRun.worker = d.prefill.specWorker; } renderModelOptions(); }
|
|
4276
4757
|
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4277
4758
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4278
|
-
if(c.model){
|
|
4279
|
-
|
|
4280
|
-
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4281
|
-
setVal("jf-judge", c.judgeExamples);
|
|
4759
|
+
if(c.model && !resumeRun.pickModel){ setPick(c); }
|
|
4760
|
+
fillCard(c);
|
|
4282
4761
|
setVal("jf-cap-money", r.budgetUsd);
|
|
4283
4762
|
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
4284
4763
|
document.getElementById("jf-cap-time").parentNode.hidden = resumeRun.noTime;
|
|
4285
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4286
4764
|
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
4765
|
originLine.classList.add("res");
|
|
4288
4766
|
originLine.hidden = false;
|
|
@@ -4300,16 +4778,21 @@
|
|
|
4300
4778
|
document.addEventListener("bareloop-resume", function(e){ applyResume(e.detail || {}); });
|
|
4301
4779
|
document.getElementById("chat-resume-cancel").addEventListener("click", function(){ resetCard(); });
|
|
4302
4780
|
var resumeSignBtn = document.getElementById("chat-resume-sign");
|
|
4781
|
+
// a Model pick that is needed (older spec) blocks Sign & resume until a ticked line is picked and its key is usable
|
|
4782
|
+
function resumeSignBlocked(){ return resumeRun !== null && resumeRun.pickModel === true && (modelSelect.value === "" || !keyOk); }
|
|
4783
|
+
function syncResumeSign(){ resumeSignBtn.disabled = resumeSignBlocked(); }
|
|
4303
4784
|
function signResume(){
|
|
4304
4785
|
var run = resumeRun;
|
|
4305
4786
|
if(run === null) return;
|
|
4787
|
+
if(resumeSignBlocked()) return;
|
|
4306
4788
|
var err = document.getElementById("chat-resume-err");
|
|
4307
4789
|
err.hidden = true;
|
|
4308
4790
|
resumeSignBtn.disabled = true;
|
|
4309
4791
|
var body = {budgetUsd: document.getElementById("jf-cap-money").value};
|
|
4310
4792
|
if(!run.noTime){ body.maxWallMin = document.getElementById("jf-cap-time").value; }
|
|
4793
|
+
if(run.pickModel){ var pk = pickParts(modelSelect.value); body.model = pk.model; if(pk.provider !== ""){ body.provider = pk.provider; } }
|
|
4311
4794
|
return authorPost("/api/runs/" + encodeURIComponent(run.runid) + "/resume", body).then(function(res){
|
|
4312
|
-
resumeSignBtn.disabled =
|
|
4795
|
+
resumeSignBtn.disabled = resumeSignBlocked();
|
|
4313
4796
|
if(res.body && res.body.ok){
|
|
4314
4797
|
// 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
4798
|
resetCard();
|
|
@@ -4319,7 +4802,7 @@
|
|
|
4319
4802
|
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
4320
4803
|
err.hidden = false;
|
|
4321
4804
|
}
|
|
4322
|
-
}).catch(function(x){ resumeSignBtn.disabled =
|
|
4805
|
+
}).catch(function(x){ resumeSignBtn.disabled = resumeSignBlocked(); err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
4323
4806
|
}
|
|
4324
4807
|
resumeSignBtn.addEventListener("click", signResume);
|
|
4325
4808
|
|
|
@@ -4400,6 +4883,7 @@
|
|
|
4400
4883
|
if(!o.ok){ chatActionFailed(o.error); return; }
|
|
4401
4884
|
openNewCard();
|
|
4402
4885
|
chatActionOk();
|
|
4886
|
+
lastMsgSig = null;
|
|
4403
4887
|
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
4888
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not revised"); });
|
|
4405
4889
|
}
|