bareloop 0.34.0 → 0.36.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 +58 -0
- package/bareloop.context.md +276 -20
- package/package.json +1 -1
- package/src/authorrun.js +3 -2
- package/src/bundlerun.js +17 -12
- package/src/draftspend.js +197 -0
- package/src/exits.js +10 -1
- package/src/index.js +3 -3
- package/src/interviewrun.js +7 -7
- package/src/job.js +37 -0
- package/src/judged.js +2 -2
- package/src/kinds.js +2 -1
- package/src/ledger.js +101 -5
- package/src/legs.js +171 -0
- package/src/monthly.js +111 -72
- package/src/movefile.js +22 -0
- package/src/npminstall.js +55 -0
- package/src/panel/authorroutes.js +84 -14
- package/src/panel/authorsession.js +490 -125
- package/src/panel/importroutes.js +359 -0
- package/src/panel/importrun.js +203 -0
- package/src/panel/index.html +1194 -213
- package/src/panel/runroutes.js +219 -0
- package/src/panel/server.js +964 -79
- package/src/panel/settingsroutes.js +52 -2
- package/src/panel/status.js +51 -0
- package/src/planrun.js +60 -26
- package/src/providerrows.js +17 -0
- package/src/ralph.js +4 -1
- package/src/replay.js +139 -19
- package/src/replayio.js +11 -4
- package/src/reuse.js +20 -3
- package/src/run.js +6 -3
- package/src/runlist.js +75 -7
- package/src/source.js +43 -4
- package/src/text.js +20 -0
- package/src/u-patient.js +3 -2
- package/src/u-readout.js +9 -1
- package/src/u-watchdog.mjs +4 -2
- package/src/userrun.js +192 -65
- package/src/worktree.js +120 -0
- package/types/draftspend.d.ts +141 -0
- package/types/job.d.ts +25 -0
- package/types/ledger.d.ts +44 -0
- package/types/legs.d.ts +103 -0
- package/types/monthly.d.ts +23 -18
- package/types/movefile.d.ts +9 -0
- package/types/npminstall.d.ts +30 -0
- package/types/panel/authorroutes.d.ts +23 -1
- package/types/panel/authorsession.d.ts +110 -2
- package/types/panel/importroutes.d.ts +129 -0
- package/types/panel/importrun.d.ts +77 -0
- package/types/panel/runroutes.d.ts +32 -0
- package/types/panel/server.d.ts +266 -2
- package/types/panel/settingsroutes.d.ts +6 -1
- package/types/panel/status.d.ts +51 -0
- package/types/planrun.d.ts +13 -21
- package/types/providerrows.d.ts +12 -0
- package/types/ralph.d.ts +19 -0
- package/types/replay.d.ts +25 -1
- package/types/replayio.d.ts +2 -2
- package/types/reuse.d.ts +5 -0
- package/types/run.d.ts +6 -2
- package/types/runlist.d.ts +52 -2
- package/types/source.d.ts +13 -5
- package/types/text.d.ts +19 -0
- package/types/userrun.d.ts +10 -0
- package/types/worktree.d.ts +48 -0
package/src/panel/index.html
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
--magenta:#bb9af7;
|
|
24
24
|
--accent:#7aa2f7;
|
|
25
25
|
--btn-bg:#1f2335; --btn-text:#c0caf5;
|
|
26
|
+
--field-bg:var(--bg); --field-soft-bg:var(--bg); --field-border:var(--text-dim);
|
|
26
27
|
--primary-bg:#7aa2f7; --primary-text:#1a1b26; --primary-border:#7aa2f7;
|
|
27
28
|
font: 14px/1.5 "Courier Prime", "Courier New", Courier, ui-monospace, monospace;
|
|
28
29
|
}
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
--magenta:#9854f1;
|
|
69
70
|
--accent:#2668c2;
|
|
70
71
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
72
|
+
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
71
73
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
72
74
|
}
|
|
73
75
|
}
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
--magenta:#9854f1;
|
|
83
85
|
--accent:#2668c2;
|
|
84
86
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
87
|
+
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
85
88
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
86
89
|
}
|
|
87
90
|
.theme-toggle{padding:4px 9px;min-height:32px;border:1px solid var(--border-strong);background:var(--btn-bg);color:var(--text);}
|
|
@@ -171,6 +174,14 @@
|
|
|
171
174
|
glyph and its own color (magenta), never red or amber. */
|
|
172
175
|
.dot.magenta::before{content:"[?]";color:var(--magenta);}
|
|
173
176
|
|
|
177
|
+
.rp-header-title h2{text-transform:none;letter-spacing:0;}
|
|
178
|
+
.rp-status{font-size:13px;color:var(--text-dim);min-width:0;overflow-wrap:anywhere;}
|
|
179
|
+
.rp-sep{color:var(--text-faint);margin-right:6px;}
|
|
180
|
+
.st-word{font-weight:700;color:var(--text);}
|
|
181
|
+
.wf-ended-inline{flex:1 1 auto;white-space:normal;}
|
|
182
|
+
.wf-compact{flex-wrap:wrap;}
|
|
183
|
+
.wf-runno{color:var(--accent);flex:none;}
|
|
184
|
+
.step-state{display:inline-flex;align-items:center;gap:2px;font-size:12px;}
|
|
174
185
|
.badge{display:inline-block;font-size:11px;padding:1px 4px;color:var(--text-dim);background:none;font-weight:700;}
|
|
175
186
|
.badge::before{content:"[";color:var(--text-faint);}
|
|
176
187
|
.badge::after{content:"]";color:var(--text-faint);}
|
|
@@ -207,6 +218,7 @@
|
|
|
207
218
|
than wrapping onto a 3rd/4th line. */
|
|
208
219
|
.wf-meta-line{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
209
220
|
.wf-meta{font-size:12px;color:var(--text-dim);}
|
|
221
|
+
.wf-ended{display:block;font-size:12px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
210
222
|
.wf-meta + .wf-meta::before{content:" · ";color:var(--text-faint);}
|
|
211
223
|
.wf-row.selected,.hist-row.selected{border:2px solid var(--accent);border-radius:4px;}
|
|
212
224
|
/* item 2 (2026-09-26 merge): a Workflows-view job row expands inline to
|
|
@@ -220,8 +232,6 @@
|
|
|
220
232
|
|
|
221
233
|
.rp-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:2px solid var(--border-strong);background:var(--panel);flex-wrap:wrap;gap:8px;}
|
|
222
234
|
.rp-header h2{margin:0;font-size:15px;}
|
|
223
|
-
.rp-header h2::before{content:"┤ ";color:var(--text-faint);}
|
|
224
|
-
.rp-header h2::after{content:" ├";color:var(--text-faint);}
|
|
225
235
|
.rp-header-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
226
236
|
/* item 4: phone-only jump-back link — hidden on desktop (the two panes
|
|
227
237
|
are already both on screen there, nothing to jump back to). */
|
|
@@ -237,6 +247,47 @@
|
|
|
237
247
|
.counters b,.counters span{color:var(--accent);}
|
|
238
248
|
|
|
239
249
|
.summary-box{position:relative;border:1px solid var(--border-strong);padding:16px 12px 12px;margin-top:16px;background:var(--panel2);font-size:12px;}
|
|
250
|
+
.ended-block{position:relative;border:1px solid var(--border-strong);padding:14px 12px 12px;margin-bottom:14px;background:var(--panel2);font-size:13px;}
|
|
251
|
+
.ended-block.magenta{border-color:var(--magenta);}
|
|
252
|
+
.ended-block.red{border-color:var(--red);}
|
|
253
|
+
.ended-block.green{border-color:var(--green);}
|
|
254
|
+
.ended-row{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;margin-bottom:6px;}
|
|
255
|
+
.ended-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:52px;}
|
|
256
|
+
.ended-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
257
|
+
.ended-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:8px;}
|
|
258
|
+
/* P5 item 4: Import (read only) — the folder browser, the imported rows, the view */
|
|
259
|
+
.import-box{border:1px dashed var(--border-strong);padding:10px;margin-bottom:10px;display:grid;gap:8px;}
|
|
260
|
+
.import-pathrow{display:flex;gap:8px;flex-wrap:wrap;}
|
|
261
|
+
.import-pathrow input{flex:1 1 180px;min-width:0;min-height:36px;background:var(--bg);color:var(--text);border:1px solid var(--border-strong);padding:4px 8px;font:inherit;}
|
|
262
|
+
.import-entries{display:grid;gap:2px;max-height:240px;overflow-y:auto;overflow-x:hidden;padding:2px;}
|
|
263
|
+
.import-entry{display:flex;gap:8px;align-items:baseline;text-align:left;background:none;border:1px solid transparent;color:var(--text);padding:3px 2px;min-height:32px;cursor:pointer;font:inherit;overflow-wrap:anywhere;}
|
|
264
|
+
.import-entry:hover{background:var(--panel);}
|
|
265
|
+
.import-up{font-weight:700;}
|
|
266
|
+
.import-entry.selected{background:var(--panel);border-color:var(--accent);}
|
|
267
|
+
.import-entry .tag{color:var(--accent);font-size:11px;}
|
|
268
|
+
.import-view[hidden]{display:none;}
|
|
269
|
+
.imp-bad{color:var(--red);font-size:12px;font-weight:700;overflow-wrap:anywhere;}
|
|
270
|
+
.wf-imported{color:var(--accent);}
|
|
271
|
+
.import-view{display:grid;gap:10px;font-size:13px;}
|
|
272
|
+
.import-view .imp-row{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;}
|
|
273
|
+
.import-view .imp-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:84px;}
|
|
274
|
+
.import-view .imp-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
275
|
+
.startfrom-line{margin:2px 0 8px;font-size:12px;color:var(--accent);overflow-wrap:anywhere;}
|
|
276
|
+
.job-card .locked{opacity:.55;cursor:not-allowed;}
|
|
277
|
+
.job-card.reuse .radio-row label{opacity:.55;cursor:not-allowed;}
|
|
278
|
+
.startfrom-origin{font-size:12px;font-weight:600;color:var(--text-dim);margin:0 0 4px;padding-right:112px;overflow-wrap:anywhere;}
|
|
279
|
+
.run-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px;}
|
|
280
|
+
.run-actions[hidden]{display:none;}
|
|
281
|
+
.run-actions .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
|
|
282
|
+
.resume-err{color:var(--red);white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
283
|
+
/* P5-R: a resumed run is ONE card; its legs are told apart by a divider line, a dotted map connector, a small tag */
|
|
284
|
+
.wf-resumed{color:var(--amber);}
|
|
285
|
+
.audit-leg-divider{text-align:center;color:var(--text-dim);font-size:12px;margin:10px 0;overflow-wrap:anywhere;}
|
|
286
|
+
.audit-leg-divider-row td{text-align:center;color:var(--text-dim);font-size:12px;background:var(--panel2);}
|
|
287
|
+
/* Resume opens the run's own Job tab: only the two caps turn into inputs, everything else stays read-only */
|
|
288
|
+
.resume-job{border:1px dashed var(--border-strong);margin-top:12px;padding:10px;display:grid;gap:8px;}
|
|
289
|
+
.resume-cap{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
|
|
290
|
+
.resume-cap input{width:7em;max-width:100%;min-height:36px;background:var(--panel);color:var(--text);border:1px solid var(--border-strong);padding:4px 8px;font:inherit;}
|
|
240
291
|
.summary-box.red{border-color:var(--red);}
|
|
241
292
|
.summary-box.amber{border-color:var(--amber);}
|
|
242
293
|
.summary-box.magenta{border-color:var(--magenta);}
|
|
@@ -246,16 +297,20 @@
|
|
|
246
297
|
.sum-label{flex:none;width:66px;color:var(--text-faint);}
|
|
247
298
|
.sum-val{color:var(--text);}
|
|
248
299
|
.summary-box .num{color:var(--accent);font-weight:700;}
|
|
300
|
+
.summary-box.imported-box::before{content:"┤ IMPORTED ├";}
|
|
301
|
+
.imported-box .sum-label{width:104px;}
|
|
302
|
+
.imported-box .summary-row{flex-wrap:wrap;}
|
|
303
|
+
.imported-box .sum-val{flex:1 1 180px;min-width:0;overflow-wrap:anywhere;}
|
|
249
304
|
|
|
250
305
|
.map-box{border:1px solid var(--border-strong);padding:10px;overflow-x:auto;margin-top:16px;margin-bottom:14px;background:var(--panel2);}
|
|
251
306
|
.map-box::before{content:"┤ MAP ├";display:block;font-size:11px;color:var(--text-faint);letter-spacing:0.08em;margin:-2px 0 8px 0;}
|
|
252
307
|
.map-box svg{display:block;max-width:100%;height:auto;}
|
|
253
|
-
.map-
|
|
308
|
+
.map-key{display:flex;gap:14px;font-size:11px;color:var(--text-dim);margin-top:6px;flex-wrap:wrap;}
|
|
254
309
|
|
|
255
310
|
.step-card{position:relative;border:1px solid var(--border);padding:10px;margin-bottom:8px;background:var(--panel2);}
|
|
256
311
|
.step-card.running{border:1px double var(--amber);}
|
|
257
312
|
.step-card .step-head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;}
|
|
258
|
-
.step-card h4{margin:0;font-size:13px;}
|
|
313
|
+
.step-card h4{margin:0;font-size:13px;text-transform:none;letter-spacing:0;}
|
|
259
314
|
.step-meta{font-size:12px;color:var(--text-dim);margin-top:4px;}
|
|
260
315
|
/* build item C: Audit tab attempt/rounds detail (reused by audit-part-toggle expansion) */
|
|
261
316
|
.attempts-list{margin-top:8px;border-top:1px dotted var(--border-strong);padding-top:8px;}
|
|
@@ -307,6 +362,7 @@
|
|
|
307
362
|
|
|
308
363
|
.job-card{position:relative;border:1px solid var(--border-strong);padding:14px 10px 10px;background:var(--panel2);margin-top:10px;margin-bottom:10px;}
|
|
309
364
|
.job-card h4{margin:0 0 8px;font-size:13px;position:absolute;top:-10px;left:10px;background:var(--panel2);padding:0 6px;}
|
|
365
|
+
.job-card .card-clear{position:absolute;top:-13px;right:10px;padding:1px 8px;font-size:11px;line-height:1.4;background:var(--panel2);}
|
|
310
366
|
.job-card h4::before{content:"┤ ";color:var(--text-faint);}
|
|
311
367
|
.job-card h4::after{content:" ├";color:var(--text-faint);}
|
|
312
368
|
.field{margin-bottom:10px;}
|
|
@@ -326,8 +382,6 @@
|
|
|
326
382
|
.job-card.compact label{margin-bottom:1px;font-size:11px;}
|
|
327
383
|
.job-card.compact input,
|
|
328
384
|
.job-card.compact select{padding:5px 7px;}
|
|
329
|
-
.expand-field{text-overflow:ellipsis;transition:height 0.12s ease;}
|
|
330
|
-
.expand-field:focus{height:52px;white-space:normal;}
|
|
331
385
|
.radio-row{display:flex;gap:14px;align-items:center;margin-bottom:10px;}
|
|
332
386
|
.radio-row label{display:flex;align-items:center;gap:5px;font-size:13px;color:var(--text);margin:0;text-transform:none;}
|
|
333
387
|
.cap-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:6px;margin-bottom:6px;}
|
|
@@ -343,15 +397,23 @@
|
|
|
343
397
|
.btn.primary:disabled{background:var(--btn-bg);color:var(--text-faint);border-color:var(--border);}
|
|
344
398
|
.btn.primary:disabled::before,.btn.primary:disabled::after{color:var(--text-faint);opacity:0.4;}
|
|
345
399
|
|
|
346
|
-
/*
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.chat-
|
|
351
|
-
.chat-
|
|
352
|
-
.chat-
|
|
353
|
-
.chat-
|
|
354
|
-
.chat-
|
|
400
|
+
/* the ONE progress list: a step per line — label, then dots while running / check / cross, then its reason */
|
|
401
|
+
.chat-steps{list-style:none;margin:2px 0 10px;padding:0;font-size:12px;}
|
|
402
|
+
.chat-steps .step{padding:1px 0;}
|
|
403
|
+
.chat-steps .step-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 10px;}
|
|
404
|
+
.chat-steps .step-label{color:var(--text);min-width:11em;}
|
|
405
|
+
.chat-steps .step-sign{font-weight:700;}
|
|
406
|
+
.chat-steps .step.run .step-sign{color:var(--amber);}
|
|
407
|
+
.chat-steps .step.ok .step-sign{color:var(--green);}
|
|
408
|
+
.chat-steps .step.bad .step-sign{color:var(--red);}
|
|
409
|
+
.chat-steps .step.wait .step-label{color:var(--amber);}
|
|
410
|
+
.chat-steps .step-detail{display:block;flex-basis:100%;min-width:0;padding-left:1.2em;color:var(--text-faint);word-break:break-word;white-space:pre-wrap;}
|
|
411
|
+
.chat-steps .step.bad .step-detail{color:var(--red);}
|
|
412
|
+
.chat-steps .step.run .step-sign::after{content:"\2026";}
|
|
413
|
+
@keyframes step-dots{0%{content:"";}25%{content:".";}50%{content:"..";}75%{content:"...";}}
|
|
414
|
+
@media (prefers-reduced-motion: no-preference){
|
|
415
|
+
.chat-steps .step.run .step-sign::after{content:"";animation:step-dots 1.2s steps(1,end) infinite;}
|
|
416
|
+
}
|
|
355
417
|
/* build item 7 — the two $0 readiness lines under the Model field. */
|
|
356
418
|
.jf-readiness{font-size:11px;margin-top:2px;}
|
|
357
419
|
.jf-readiness.ok{color:var(--green);}
|
|
@@ -368,12 +430,24 @@
|
|
|
368
430
|
.msg .who{font-size:11px;color:var(--text-faint);margin-bottom:3px;text-transform:none;font-weight:400;}
|
|
369
431
|
.chat-input-row{margin-top:10px;border-top:1px dotted var(--border-strong);padding-top:10px;}
|
|
370
432
|
.chat-input-row input{width:100%;box-sizing:border-box;}
|
|
433
|
+
/* the ask box wraps long text (textarea, hamr 2026-10-05): grows a few rows (fitBox), then scrolls inside; Enter sends, Shift+Enter is a newline */
|
|
434
|
+
textarea[data-testid="chat-msg"]{width:100%;box-sizing:border-box;font:inherit;color:var(--text);resize:vertical;overflow-y:auto;max-height:200px;}
|
|
435
|
+
/* 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 */
|
|
436
|
+
/* same look as the card's one-line inputs (white when editable, ordinary 1px border) — a textarea has its own browser border otherwise */
|
|
437
|
+
.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;}
|
|
438
|
+
/* hamr 2026-10-05: the focused/selected Chat-card field gets a blue border (the accent), no outline ring */
|
|
439
|
+
#panel-chat :is(input,select,textarea):focus{border-color:var(--accent);outline:none;}
|
|
440
|
+
/* hamr 2026-10-05: the ask box is dimmed until something asks for a reply (placeholder stays readable) */
|
|
441
|
+
#chat-msg:disabled{background:var(--bg);opacity:.55;cursor:not-allowed;}
|
|
371
442
|
/* chat message box reads as an unmistakable text box: ~2x the default
|
|
372
443
|
input height, thicker 2px border (ported verbatim from the mockup). */
|
|
373
|
-
#chat-msg{min-height:64px;padding:8px 12px;border:2px solid var(--border-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
444
|
+
#chat-msg{min-height:64px;padding:8px 12px;border:2px solid var(--field-border);background:var(--field-bg);}
|
|
445
|
+
/* hamr 2026-10-05: editable Chat-card fields get the standard soft white (a step below the ask box); disabled/readonly (dimmed) keep the base colour */
|
|
446
|
+
#panel-chat input:not(:disabled):not([readonly]):not(#chat-msg):not([type="radio"]):not([type="checkbox"]),
|
|
447
|
+
#panel-chat select:not(:disabled),
|
|
448
|
+
#panel-chat textarea:not(:disabled):not([readonly]):not(#chat-msg){background:var(--field-soft-bg);}
|
|
449
|
+
/* ONE wide button below the message box (hamr 2026-10-05); its wording comes from mainButtonFor */
|
|
450
|
+
.btn.wide{width:100%;margin-top:10px;white-space:normal;}
|
|
377
451
|
/* Settings view (P4a) — same tokens as the rest of the page; the mockup's settings styles */
|
|
378
452
|
.settings-view{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;}
|
|
379
453
|
.settings-header{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:2px solid var(--border-strong);background:var(--panel);}
|
|
@@ -393,6 +467,7 @@
|
|
|
393
467
|
.pv-table td,.pv-table th{white-space:nowrap;vertical-align:top;}
|
|
394
468
|
.pv-table select,.pv-table input{min-height:32px;max-width:190px;box-sizing:border-box;}
|
|
395
469
|
.pv-table .hint{display:block;margin-top:3px;}
|
|
470
|
+
.pv-table td.pv-price-cell input{width:90px;}
|
|
396
471
|
/* the Test cell keeps ONE width whatever the result says ("testing…", "OK · 312 ms", a long failure note) */
|
|
397
472
|
.pv-table td.pv-test-cell{width:180px;min-width:180px;max-width:180px;}
|
|
398
473
|
.pv-table .pv-test-result{white-space:normal;overflow-wrap:anywhere;}
|
|
@@ -441,10 +516,14 @@
|
|
|
441
516
|
<div class="pane-body" id="left-pane-body">
|
|
442
517
|
|
|
443
518
|
<section id="panel-chat" class="tabpanel active" role="tabpanel" aria-labelledby="tab-chat">
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
<div class="job-card compact" id="job-card" data-testid="job-card"
|
|
519
|
+
<!-- hamr 2026-10-05: the empty card is ALWAYS there when Chat opens (no + New). The top-right button reads
|
|
520
|
+
"Abandon" while a session is live and unsigned, else "Clear"; both leave an empty card. -->
|
|
521
|
+
<div class="job-card compact" id="job-card" data-testid="job-card">
|
|
522
|
+
<div class="startfrom-origin" id="chat-startfrom-origin" data-testid="startfrom-origin" hidden></div>
|
|
447
523
|
<h4 id="job-card-title">Job card — draft</h4>
|
|
524
|
+
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
525
|
+
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
526
|
+
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
448
527
|
|
|
449
528
|
<div class="field">
|
|
450
529
|
<label id="jf-verdict-label">Check type</label>
|
|
@@ -466,27 +545,27 @@
|
|
|
466
545
|
</div>
|
|
467
546
|
<div class="field">
|
|
468
547
|
<label for="jf-goal">Goal</label>
|
|
469
|
-
<
|
|
548
|
+
<textarea class="jf-wrap" id="jf-goal" rows="2"></textarea>
|
|
470
549
|
</div>
|
|
471
550
|
<div class="field">
|
|
472
551
|
<label for="jf-source">Source</label>
|
|
473
|
-
<
|
|
552
|
+
<textarea class="jf-wrap" id="jf-source" rows="1" placeholder="absolute path to a local repo"></textarea>
|
|
474
553
|
</div>
|
|
475
554
|
<div class="field">
|
|
476
555
|
<label for="jf-dest">Destination</label>
|
|
477
|
-
<
|
|
556
|
+
<textarea class="jf-wrap" id="jf-dest" rows="1" placeholder="the write fence — where in the repo this run may change files"></textarea>
|
|
478
557
|
</div>
|
|
479
558
|
<div class="field">
|
|
480
559
|
<label for="jf-success">Success</label>
|
|
481
|
-
<
|
|
560
|
+
<textarea class="jf-wrap" id="jf-success" rows="2"></textarea>
|
|
482
561
|
</div>
|
|
483
562
|
<div class="field">
|
|
484
563
|
<label for="jf-guardrails">Guardrails</label>
|
|
485
|
-
<
|
|
564
|
+
<textarea class="jf-wrap" id="jf-guardrails" rows="2"></textarea>
|
|
486
565
|
</div>
|
|
487
566
|
<div class="field">
|
|
488
567
|
<label for="jf-judge">Judge examples <span class="hint" style="display:inline">(rubric only)</span></label>
|
|
489
|
-
<
|
|
568
|
+
<textarea class="jf-wrap" id="jf-judge" rows="2" disabled></textarea>
|
|
490
569
|
</div>
|
|
491
570
|
|
|
492
571
|
<div class="cap-row" data-testid="cap-row">
|
|
@@ -500,21 +579,15 @@
|
|
|
500
579
|
<input id="jf-cap-time" type="text" placeholder="or blank = no wall">
|
|
501
580
|
</div>
|
|
502
581
|
</div>
|
|
503
|
-
<button class="btn primary" type="button" id="chat-start-btn" data-testid="chat-start" disabled>Start drafting</button>
|
|
504
582
|
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
505
|
-
<div class="hint" id="chat-hash-line" data-testid="chat-hash-line" hidden></div>
|
|
506
|
-
</div>
|
|
507
|
-
|
|
508
|
-
<!-- build item 3 — the label comes FIRST, then the animated
|
|
509
|
-
indicator, so the cycling dots ([progress.] .. [progress....])
|
|
510
|
-
never push the label around. -->
|
|
511
|
-
<div class="chat-progress-row" id="chat-progress-row" data-testid="chat-progress-row" hidden>
|
|
512
|
-
<span class="chat-progress-label" id="chat-progress-label" data-testid="chat-progress-label"></span>
|
|
513
|
-
<span class="chat-progress-glyph" id="chat-progress-glyph" data-testid="chat-progress-glyph"></span>
|
|
514
583
|
</div>
|
|
515
584
|
|
|
516
585
|
<div class="chat-thread" id="chat-thread" data-testid="chat-thread"></div>
|
|
517
586
|
|
|
587
|
+
<!-- ONE progress list (hamr 2026-10-04): every pipeline step is one line that updates in place, running with
|
|
588
|
+
animated dots, then a check or a cross with its code-owned reason; chat turns stay in the thread above it, right over the ask box. -->
|
|
589
|
+
<ol class="chat-steps" id="chat-progress-row" data-testid="chat-steps" aria-live="polite" hidden></ol>
|
|
590
|
+
|
|
518
591
|
<!-- build item 1 — a failed/rejected send, revise, sign-prepare,
|
|
519
592
|
sign or check-deps POST shows here and STAYS shown: it is set
|
|
520
593
|
only by this tab's own action handlers below and is never
|
|
@@ -525,15 +598,10 @@
|
|
|
525
598
|
|
|
526
599
|
<div class="chat-input-row">
|
|
527
600
|
<label for="chat-msg" style="display:none">Message</label>
|
|
528
|
-
<
|
|
529
|
-
</div>
|
|
530
|
-
<button class="btn" type="button" id="chat-check-deps-btn" data-testid="chat-check-deps" hidden>Check again</button>
|
|
531
|
-
|
|
532
|
-
<div class="actions-row">
|
|
533
|
-
<button class="btn" type="button" id="chat-send-btn" data-testid="chat-send" disabled>Send</button>
|
|
534
|
-
<button class="btn primary" type="button" id="chat-sign-btn" data-testid="btn-sign-run" disabled>Sign & run</button>
|
|
535
|
-
<button class="btn" type="button" id="chat-revise-btn" data-testid="btn-revise" disabled>Revise (2 left)</button>
|
|
601
|
+
<textarea id="chat-msg" rows="2" placeholder="Ask for a change, or reply to the plan…" data-testid="chat-msg"></textarea>
|
|
536
602
|
</div>
|
|
603
|
+
<button class="btn primary wide" type="button" id="chat-main-btn" data-testid="chat-main" disabled>Start drafting</button>
|
|
604
|
+
<div class="hint" id="chat-main-hint" data-testid="chat-main-hint"></div>
|
|
537
605
|
<div class="hint">Only your click signs. The chat can't.</div>
|
|
538
606
|
</section>
|
|
539
607
|
|
|
@@ -541,9 +609,25 @@
|
|
|
541
609
|
<div class="chips" role="group" aria-label="Runs view" style="margin-bottom:8px;">
|
|
542
610
|
<button class="chip" type="button" aria-pressed="true" data-runs-view="workflows" data-testid="runs-view-workflows">Workflows</button>
|
|
543
611
|
<button class="chip" type="button" aria-pressed="false" data-runs-view="history" data-testid="runs-view-history">History</button>
|
|
612
|
+
<button class="btn small" type="button" id="wf-import" data-testid="wf-import" title="look at an exported job folder (read only)">Import</button>
|
|
613
|
+
</div>
|
|
614
|
+
<div class="import-box" id="import-box" data-testid="import-box" hidden>
|
|
615
|
+
<label for="import-path">Import a job folder</label>
|
|
616
|
+
<div class="import-pathrow">
|
|
617
|
+
<input id="import-path" data-testid="import-path" type="text" placeholder="/path/to/job.bareloop — or pick below" autocomplete="off">
|
|
618
|
+
<button class="btn small" type="button" id="import-open" data-testid="import-open">Go</button>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="resume-err" id="import-err" data-testid="import-err" hidden></div>
|
|
621
|
+
<div class="hint" id="import-resolved" data-testid="import-resolved"></div>
|
|
622
|
+
<div class="import-entries" id="import-entries" data-testid="import-entries"></div>
|
|
623
|
+
<div class="ended-actions">
|
|
624
|
+
<button class="btn primary" type="button" id="import-go" data-testid="import-go">Import</button>
|
|
625
|
+
<button class="btn" type="button" id="import-cancel" data-testid="import-cancel">Cancel</button>
|
|
626
|
+
</div>
|
|
544
627
|
</div>
|
|
545
628
|
<div id="runs-filterbar-anchor"></div>
|
|
546
629
|
<div id="wf-list" data-testid="wf-list"></div>
|
|
630
|
+
<div id="import-list" data-testid="import-list"></div>
|
|
547
631
|
<div id="history-list" data-testid="history-list" hidden></div>
|
|
548
632
|
</section>
|
|
549
633
|
|
|
@@ -556,8 +640,7 @@
|
|
|
556
640
|
<div class="rp-header-title">
|
|
557
641
|
<span class="dot grey" id="active-wf-dot"></span>
|
|
558
642
|
<h2 id="active-wf-name">(none selected)</h2>
|
|
559
|
-
<span class="
|
|
560
|
-
<span class="badge" id="active-wf-date">—</span>
|
|
643
|
+
<span class="rp-status" id="active-wf-verdict" data-testid="active-wf-verdict"></span>
|
|
561
644
|
</div>
|
|
562
645
|
</div>
|
|
563
646
|
|
|
@@ -570,8 +653,10 @@
|
|
|
570
653
|
<div class="pane-body" id="right-pane-body">
|
|
571
654
|
|
|
572
655
|
<section id="panel-run" class="tabpanel active" role="tabpanel" aria-labelledby="tab-run">
|
|
656
|
+
<div class="import-view" id="import-view" data-testid="import-view" hidden></div>
|
|
573
657
|
<div id="run-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run from Workflows or History</div>
|
|
574
658
|
<div id="run-content" hidden>
|
|
659
|
+
<div class="run-actions" id="run-actions" data-testid="run-actions" hidden></div>
|
|
575
660
|
<div class="counters" id="run-counters" data-testid="live-counters"></div>
|
|
576
661
|
<div class="summary-box" id="run-summary" data-testid="run-summary"></div>
|
|
577
662
|
<div class="map-box" data-testid="step-map" id="map-box"><div id="map-mount"></div></div>
|
|
@@ -582,6 +667,7 @@
|
|
|
582
667
|
<section id="panel-audit" class="tabpanel" role="tabpanel" aria-labelledby="tab-audit" hidden>
|
|
583
668
|
<div id="audit-select-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
584
669
|
<div id="audit-body" hidden>
|
|
670
|
+
<div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
|
|
585
671
|
<div class="audit-toolbar" style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
|
586
672
|
<div class="chips" role="group" aria-label="Filter audit rows" style="margin-bottom:0;">
|
|
587
673
|
<button class="chip" aria-pressed="true" data-filter="all" data-testid="chip-all">All</button>
|
|
@@ -619,8 +705,8 @@
|
|
|
619
705
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
620
706
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
621
707
|
<div class="field-row">
|
|
622
|
-
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
623
|
-
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
708
|
+
<div class="field"><label id="details-cap-money-label">$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
709
|
+
<div class="field"><label id="details-cap-time-label">Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
624
710
|
</div>
|
|
625
711
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
626
712
|
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
@@ -630,6 +716,7 @@
|
|
|
630
716
|
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
631
717
|
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
632
718
|
<div class="hint" id="job-note"></div>
|
|
719
|
+
<div id="resume-job" class="resume-job" data-testid="resume-job" hidden></div>
|
|
633
720
|
</div>
|
|
634
721
|
</section>
|
|
635
722
|
|
|
@@ -656,21 +743,24 @@
|
|
|
656
743
|
<section id="panel-providers" class="tabpanel active" role="tabpanel" aria-labelledby="tab-providers">
|
|
657
744
|
<div class="keyfile-strip" data-testid="keyfile-box">
|
|
658
745
|
<div class="keyfile-strip-main">
|
|
659
|
-
<span>Put <code>NAME=key</code> lines in
|
|
746
|
+
<span>Put <code>NAME=key</code> lines in your keys file (Open keys folder), then Reload keys — every key that has a value gets a row. Only names are listed; keys never reach this page.</span>
|
|
747
|
+
<span class="hint" id="pv-keyfile-path" data-testid="keyfile-path"></span>
|
|
660
748
|
<span class="hint" id="pv-keyfile-note" data-testid="keyfile-warning"></span>
|
|
661
749
|
</div>
|
|
662
750
|
<div class="keyfile-strip-actions">
|
|
751
|
+
<button class="btn small" type="button" id="btn-open-keys-folder" data-testid="btn-open-keys-folder">Open keys folder</button>
|
|
663
752
|
<button class="btn small" type="button" id="btn-reload-keys" data-testid="btn-reload-keys">Reload keys</button>
|
|
664
753
|
</div>
|
|
665
754
|
</div>
|
|
666
755
|
<div class="table-wrap" data-testid="provider-table-wrap">
|
|
667
756
|
<table class="pv-table" data-testid="provider-table">
|
|
668
757
|
<thead>
|
|
669
|
-
<tr><th>Key</th><th>Name</th><th>API shape</th><th>Base URL</th><th>Test</th><th>Tokens used</th><th>Balance</th></tr>
|
|
758
|
+
<tr><th>Key</th><th>Name</th><th>API shape</th><th>Base URL</th><th>Test</th><th>Tokens used</th><th>Balance</th><th>In $/1M</th><th>Out $/1M</th></tr>
|
|
670
759
|
</thead>
|
|
671
760
|
<tbody id="pv-rows"></tbody>
|
|
672
761
|
</table>
|
|
673
762
|
</div>
|
|
763
|
+
<div class="hint" id="pv-price-hint" data-testid="pv-price-hint">If your vendor lists two prices, enter the higher one.</div>
|
|
674
764
|
<div id="pv-shell" data-testid="pv-shell"></div>
|
|
675
765
|
<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>
|
|
676
766
|
</section>
|
|
@@ -720,15 +810,28 @@
|
|
|
720
810
|
var TOKEN = "__BARELOOP_PANEL_TOKEN__";
|
|
721
811
|
document.getElementById("host-line").textContent = "127.0.0.1:" + PORT;
|
|
722
812
|
|
|
813
|
+
// After a panel restart this page's token is dead: the server refuses EVERY call with 403 and the exact reason
|
|
814
|
+
// below (checkHumanGuard, src/panel/authorroutes.js). The ONE place the page makes calls swaps that raw text for
|
|
815
|
+
// one clear line and tells the Chat poll to stop (staleTokenHook, set by the Chat code). No auto-reload.
|
|
816
|
+
var STALE_TOKEN_REFUSAL = "refused \u2014 missing or wrong token";
|
|
817
|
+
var STALE_TOKEN_LINE = "The panel was restarted \u2014 reload this page. A drafting session that was open is gone (nothing more is spent).";
|
|
818
|
+
var staleTokenHook = function(){};
|
|
819
|
+
function staleTokenCheck(j, status){
|
|
820
|
+
if(j && j.ok === false && j.error === STALE_TOKEN_REFUSAL && (status === undefined || status === 403)){
|
|
821
|
+
j.error = STALE_TOKEN_LINE;
|
|
822
|
+
staleTokenHook();
|
|
823
|
+
}
|
|
824
|
+
return j;
|
|
825
|
+
}
|
|
723
826
|
function authorPost(path, body){
|
|
724
827
|
return fetch(path, {
|
|
725
828
|
method: "POST",
|
|
726
829
|
headers: {"content-type": "application/json", "x-bareloop-token": TOKEN},
|
|
727
830
|
body: JSON.stringify(body || {}),
|
|
728
|
-
}).then(function(r){ return r.json().then(function(j){ return {status: r.status, body: j}; }); });
|
|
831
|
+
}).then(function(r){ return r.json().then(function(j){ return {status: r.status, body: staleTokenCheck(j, r.status)}; }); });
|
|
729
832
|
}
|
|
730
833
|
function authorGet(path){
|
|
731
|
-
return fetch(path, {headers: {"x-bareloop-token": TOKEN}}).then(function(r){ return r.json(); });
|
|
834
|
+
return fetch(path, {headers: {"x-bareloop-token": TOKEN}}).then(function(r){ return r.json().then(function(j){ return staleTokenCheck(j, r.status); }); });
|
|
732
835
|
}
|
|
733
836
|
|
|
734
837
|
function wireTabs(tabButtons){
|
|
@@ -832,6 +935,8 @@
|
|
|
832
935
|
var n = 0;
|
|
833
936
|
return steps.map(function(s){
|
|
834
937
|
if(s.noNumber) return -1;
|
|
938
|
+
// P5-R: a step continuing from the leg before is the SAME step — it keeps its number
|
|
939
|
+
if(s.continued && n > 0) return n - 1;
|
|
835
940
|
n++;
|
|
836
941
|
return n - 1;
|
|
837
942
|
});
|
|
@@ -845,8 +950,12 @@
|
|
|
845
950
|
// jump straight to that SAME part's Audit-tab row (build item B's
|
|
846
951
|
// click-through), with no re-lookup needed.
|
|
847
952
|
function partBoxState(part, isLastLivePart){
|
|
953
|
+
// the drafting part happened before the run existed: it is always done
|
|
954
|
+
if(part.kind === "drafting") return "done";
|
|
848
955
|
if(part.outcome === "green" || part.outcome === "satisfied" || part.outcome === "already-green") return "done";
|
|
849
956
|
if(typeof part.outcome === "string") return "stopped";
|
|
957
|
+
// P5-R: its leg's halt cut this part off — the map's existing "stopped" state, never "done"
|
|
958
|
+
if(part.stopReason) return "stopped";
|
|
850
959
|
if(isLastLivePart) return "running";
|
|
851
960
|
return "done"; // scout/plan/replan/judge: no per-part verdict, but the part itself already happened
|
|
852
961
|
}
|
|
@@ -865,6 +974,7 @@
|
|
|
865
974
|
// (a single-attempt step, or the whole "run" box on an old-shape spine)
|
|
866
975
|
// reads the part's own outcome.
|
|
867
976
|
function partResultGlyph(part, box){
|
|
977
|
+
if(part.kind === "drafting") return glyphSpan(attemptGlyph("green", false)); // [✓]: drafting is done once the run exists
|
|
868
978
|
if(partHasNoVerdict(part)) return null;
|
|
869
979
|
var boxIsLive = box.state === "running";
|
|
870
980
|
if(box.attempts.length > 1) return box.attempts.map(function(a){ return glyphSpan(attemptGlyph(a.outcome, boxIsLive)); }).join("");
|
|
@@ -875,7 +985,9 @@
|
|
|
875
985
|
var list = Array.isArray(parts) ? parts : [];
|
|
876
986
|
return list.map(function(part, idx){
|
|
877
987
|
var isLastLivePart = !!isLive && idx === list.length - 1;
|
|
878
|
-
|
|
988
|
+
// "(try N)" is for real in-leg retries only; a step re-entered across a leg boundary is the same step continuing
|
|
989
|
+
var tryN = typeof part.tryNumber === "number" ? part.tryNumber : part.occurrence;
|
|
990
|
+
var occSuffix = part.kind === "step" && part.continued ? " (continued)" : (part.kind === "step" && tryN > 1 ? " (try " + tryN + ")" : "");
|
|
879
991
|
return {
|
|
880
992
|
kind: part.kind,
|
|
881
993
|
partIndex: idx,
|
|
@@ -888,6 +1000,9 @@
|
|
|
888
1000
|
// Audit tab's expand-to-rounds, read off `box.part.attempts` directly).
|
|
889
1001
|
attempts: partHasNoVerdict(part) ? [] : (Array.isArray(part.attempts) ? part.attempts : []),
|
|
890
1002
|
blocked: typeof part.blocked === "number" ? part.blocked : 0,
|
|
1003
|
+
// P5-R: the leg that ran this part ENDED here and the next part is where the next leg picked up
|
|
1004
|
+
resumedNext: part.resumedNext === true,
|
|
1005
|
+
continued: part.continued === true,
|
|
891
1006
|
part: part,
|
|
892
1007
|
};
|
|
893
1008
|
});
|
|
@@ -1031,34 +1146,35 @@
|
|
|
1031
1146
|
'" text-anchor="middle" font-size="10" fill="var(--text-faint)">try ' + retryTry + '</text>';
|
|
1032
1147
|
}
|
|
1033
1148
|
if(idx < n - 1){
|
|
1149
|
+
// P5-R: where a leg ended and the next leg picked up, the connector is DOTTED (and says so) —
|
|
1150
|
+
// one run, one map; the dotted line is the only mark of the resume.
|
|
1151
|
+
var dash = s.resumedNext ? ' stroke-dasharray="2,4" data-resumed="1"' : '';
|
|
1034
1152
|
if(nextRow === row){
|
|
1035
1153
|
var midY = y + boxH / 2;
|
|
1036
1154
|
if(!rtl){
|
|
1037
1155
|
var nx = x + boxW + gap;
|
|
1038
1156
|
body += '<line x1="' + (x + boxW) + '" y1="' + midY + '" x2="' + (nx - 2) + '" y2="' + midY +
|
|
1039
|
-
'" stroke="var(--text-dim)" stroke-width="2" marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1157
|
+
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1040
1158
|
} else {
|
|
1041
1159
|
var px = x - gap - boxW;
|
|
1042
1160
|
body += '<line x1="' + x + '" y1="' + midY + '" x2="' + (px + boxW + 2) + '" y2="' + midY +
|
|
1043
|
-
'" stroke="var(--text-dim)" stroke-width="2" marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1161
|
+
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1044
1162
|
}
|
|
1045
1163
|
} else {
|
|
1046
1164
|
var cx = x + boxW / 2;
|
|
1047
1165
|
body += '<line x1="' + cx + '" y1="' + (y + boxH) + '" x2="' + cx + '" y2="' + (y + rowH - 2) +
|
|
1048
|
-
'" stroke="var(--text-dim)" stroke-width="2" marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1166
|
+
'" stroke="var(--text-dim)" stroke-width="2"' + dash + ' marker-end="url(#' + uid + '-arrow)"></line>';
|
|
1049
1167
|
}
|
|
1050
1168
|
}
|
|
1051
1169
|
});
|
|
1052
1170
|
return '<svg width="' + totalW + '" height="' + totalH + '" viewBox="0 0 ' + totalW + ' ' + totalH + '" role="img" aria-label="step map">' + body + '</svg>';
|
|
1053
1171
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
'<span><span class="dot red"></span>stopped</span>' +
|
|
1059
|
-
'<span><span class="dot grey"></span>waiting</span>' +
|
|
1060
|
-
'<span><span class="dot magenta"></span>died</span>' +
|
|
1172
|
+
// the line-style key under the MAP (hamr 2026-10-04): only the two line styles, never a sign legend
|
|
1173
|
+
function stepMapKeyHTML(steps){
|
|
1174
|
+
var resumed = (steps || []).some(function(b){ return b.resumedNext; });
|
|
1175
|
+
return '<div class="map-key" data-testid="map-key">' +
|
|
1061
1176
|
'<span>⤾ dashed = retry</span>' +
|
|
1177
|
+
(resumed ? '<span>··· dotted = resumed</span>' : '') +
|
|
1062
1178
|
'</div>';
|
|
1063
1179
|
}
|
|
1064
1180
|
var lastSteps = null;
|
|
@@ -1081,7 +1197,7 @@
|
|
|
1081
1197
|
wrapper.hidden = false;
|
|
1082
1198
|
var w = mapAvailWidth(wrapper);
|
|
1083
1199
|
lastWidth = w;
|
|
1084
|
-
mount.innerHTML = buildStepMapSVG(steps, w) +
|
|
1200
|
+
mount.innerHTML = buildStepMapSVG(steps, w) + stepMapKeyHTML(steps);
|
|
1085
1201
|
// build item B: a map box click jumps to the Audit tab, that same part
|
|
1086
1202
|
// expanded and scrolled into view — same click-through as the card list,
|
|
1087
1203
|
// sharing partIndex so the two can never disagree about which row a box
|
|
@@ -1101,7 +1217,7 @@
|
|
|
1101
1217
|
var w = mapAvailWidth(wrapper);
|
|
1102
1218
|
if(w === null || w === lastWidth) return;
|
|
1103
1219
|
lastWidth = w;
|
|
1104
|
-
document.getElementById("map-mount").innerHTML = buildStepMapSVG(lastSteps, w) +
|
|
1220
|
+
document.getElementById("map-mount").innerHTML = buildStepMapSVG(lastSteps, w) + stepMapKeyHTML(lastSteps);
|
|
1105
1221
|
}
|
|
1106
1222
|
if(typeof ResizeObserver !== "undefined"){
|
|
1107
1223
|
var ro = new ResizeObserver(function(){ reflowMap(); });
|
|
@@ -1126,11 +1242,55 @@
|
|
|
1126
1242
|
if(g === "?") return "magenta";
|
|
1127
1243
|
return "grey";
|
|
1128
1244
|
}
|
|
1245
|
+
// ONE spelling of "**word** — reason": the sign and the word come from the server's status table
|
|
1246
|
+
// (src/panel/status.js) — the page never maps an outcome to a word itself. No status = nothing to say.
|
|
1247
|
+
function statusWordHtml(status, reason){
|
|
1248
|
+
if(!status || !status.word) return "";
|
|
1249
|
+
return '<b class="st-word">' + escapeXml(status.word) + '</b>' + (reason ? " \u2014 " + escapeXml(reason) : "");
|
|
1250
|
+
}
|
|
1251
|
+
// local date + time as the header shows it: "10/4/2026, 6:01 PM" ("unknown" when the spine carried no time)
|
|
1252
|
+
function fmtLocalDateTime(iso){
|
|
1253
|
+
var d = iso ? new Date(iso) : null;
|
|
1254
|
+
if(!d || isNaN(d.getTime())) return "unknown";
|
|
1255
|
+
return d.toLocaleString("en-US", {year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "2-digit"});
|
|
1256
|
+
}
|
|
1257
|
+
// the right-side header: `[sign] job (run-N) │ **word** — reason · ended 10/4/2026, 6:01 PM` (no capitals: the job
|
|
1258
|
+
// name is shown as written). `bodyHtml` is already escaped.
|
|
1259
|
+
// a run's NAME for a person (P6 item 7): `job (run-N)`, N from the server (`runNo`, one owner) — the page never counts.
|
|
1260
|
+
// A run with no number (an imported bundle's run is not in the run list) falls back to its id.
|
|
1261
|
+
function runLabel(runNo, runid){ return typeof runNo === "number" ? "run-" + runNo : String(runid || ""); }
|
|
1262
|
+
function runName(job, runNo, runid){ return job + " (" + runLabel(runNo, runid) + ")"; }
|
|
1263
|
+
function setRunHeader(sign, nameText, bodyHtml){
|
|
1264
|
+
document.getElementById("active-wf-name").textContent = nameText;
|
|
1265
|
+
document.getElementById("active-wf-dot").className = "dot " + glyphClass(sign);
|
|
1266
|
+
var st = document.getElementById("active-wf-verdict");
|
|
1267
|
+
st.innerHTML = bodyHtml ? '<span class="rp-sep">\u2502</span>' + bodyHtml : "";
|
|
1268
|
+
st.removeAttribute("title");
|
|
1269
|
+
}
|
|
1270
|
+
// "step 2 of 3" for a live run: the step it is on (first one not yet done) of the real steps
|
|
1271
|
+
function liveStepText(detail){
|
|
1272
|
+
var rs = realSteps(detail.steps || []);
|
|
1273
|
+
if(rs.length === 0) return "";
|
|
1274
|
+
var done = rs.filter(function(s){ return s.state === "done"; }).length;
|
|
1275
|
+
return "step " + Math.min(done + 1, rs.length) + " of " + rs.length;
|
|
1276
|
+
}
|
|
1277
|
+
function runHeaderBody(detail){
|
|
1278
|
+
var live = detail.status && detail.status.word === "running";
|
|
1279
|
+
var reason = detail.ended ? detail.ended.line : (live ? liveStepText(detail) : "");
|
|
1280
|
+
var when = detail.endedAt ? ("ended " + fmtLocalDateTime(detail.endedAt)) : ("started " + fmtLocalDateTime(detail.startedAt || detail.at));
|
|
1281
|
+
return statusWordHtml(detail.status, reason) + " \u00B7 " + escapeXml(when);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// P5-R: a resumed run is the SAME run — one card, never two. This is only a small tag on it.
|
|
1285
|
+
function resumedTagHtml(count, testid){
|
|
1286
|
+
return (typeof count === "number" && count > 0)
|
|
1287
|
+
? '<span class="wf-meta wf-resumed" data-testid="' + escapeXml(testid) + '">resumed ×' + count + '</span>' : '';
|
|
1288
|
+
}
|
|
1129
1289
|
|
|
1130
1290
|
// ---- Runs tab: ONE run-row builder shared by History (flat) and the
|
|
1131
1291
|
// Workflows view's per-job expansion (item 2, 2026-09-26 merge) — the two
|
|
1132
1292
|
// never drift on how a run row reads.
|
|
1133
|
-
function buildRunRowEl(r){
|
|
1293
|
+
function buildRunRowEl(r, compact){
|
|
1134
1294
|
var row = document.createElement("div");
|
|
1135
1295
|
// build item (2026-09-26 fix): compute selected state here, off the
|
|
1136
1296
|
// module's own `currentRunid`, so a row freshly built by a RE-RENDER
|
|
@@ -1145,8 +1305,16 @@
|
|
|
1145
1305
|
row.setAttribute("tabindex", "0");
|
|
1146
1306
|
row.setAttribute("role", "button");
|
|
1147
1307
|
row.setAttribute("aria-pressed", selected ? "true" : "false");
|
|
1148
|
-
var histFullName = r.job
|
|
1149
|
-
if(r.
|
|
1308
|
+
var histFullName = runName(r.job, r.runNo, r.runid);
|
|
1309
|
+
if(r.starting){
|
|
1310
|
+
row.innerHTML =
|
|
1311
|
+
'<span class="wf-line1"><span class="dot amber"></span>' +
|
|
1312
|
+
'<span class="wf-name" title="' + escapeXml(histFullName) + '">' + escapeXml(histFullName) + '</span></span>' +
|
|
1313
|
+
'<span class="wf-meta-line">' +
|
|
1314
|
+
'<span class="wf-meta" data-testid="hist-starting-' + escapeXml(r.runid) + '">starting…</span>' +
|
|
1315
|
+
'<span class="wf-meta">' + escapeXml((r.at || "").slice(0,10) || "unknown") + '</span>' +
|
|
1316
|
+
'</span>';
|
|
1317
|
+
} else if(r.fileMissing){
|
|
1150
1318
|
row.innerHTML =
|
|
1151
1319
|
'<span class="wf-line1"><span class="dot grey"></span>' +
|
|
1152
1320
|
'<span class="wf-name" title="' + escapeXml(histFullName) + '">' + escapeXml(histFullName) + '</span></span>' +
|
|
@@ -1154,15 +1322,31 @@
|
|
|
1154
1322
|
'<span class="wf-meta">file missing</span>' +
|
|
1155
1323
|
'<span class="wf-meta">' + escapeXml((r.at || "").slice(0,10) || "unknown") + '</span>' +
|
|
1156
1324
|
'</span>';
|
|
1325
|
+
} else if(compact){
|
|
1326
|
+
// an EXPANDED per-run row under a workflow (hamr 2026-10-06), 2 lines, left-aligned:
|
|
1327
|
+
// `[sign] (run-N) **word** — reason` / `$ ($drafting) · wall · date`
|
|
1328
|
+
row.innerHTML =
|
|
1329
|
+
'<span class="wf-line1 wf-compact"><span class="dot ' + glyphClass(r.glyph) + '"></span>' +
|
|
1330
|
+
'<span class="wf-name wf-runno" title="' + escapeXml(histFullName) + '" data-testid="hist-runno-' + escapeXml(r.runid) + '">(' + escapeXml(runLabel(r.runNo, r.runid)) + ')</span>' +
|
|
1331
|
+
'<span class="wf-ended wf-ended-inline" data-testid="hist-ended-' + escapeXml(r.runid) + '">' + statusWordHtml(r.status, r.endedLine) + '</span></span>' +
|
|
1332
|
+
'<span class="wf-meta-line">' +
|
|
1333
|
+
'<span class="wf-meta">' + escapeXml(rowSpendText(r, true)) + '</span>' +
|
|
1334
|
+
'<span class="wf-meta">' + escapeXml(rowWallText(r)) + '</span>' +
|
|
1335
|
+
'<span class="wf-meta">' + escapeXml(r.date || "unknown") + '</span>' +
|
|
1336
|
+
resumedTagHtml(r.resumedCount, "hist-resumed-" + r.runid) +
|
|
1337
|
+
'</span>';
|
|
1157
1338
|
} else {
|
|
1339
|
+
// History card: line 1 sign + job (run-N); line 2 **word** — reason; line 3 the facts
|
|
1158
1340
|
row.innerHTML =
|
|
1159
1341
|
'<span class="wf-line1"><span class="dot ' + glyphClass(r.glyph) + '"></span>' +
|
|
1160
1342
|
'<span class="wf-name" title="' + escapeXml(histFullName) + '">' + escapeXml(histFullName) + '</span></span>' +
|
|
1343
|
+
(r.status ? '<span class="wf-ended" data-testid="hist-ended-' + escapeXml(r.runid) + '">' + statusWordHtml(r.status, r.endedLine) + '</span>' : '') +
|
|
1161
1344
|
'<span class="wf-meta-line">' +
|
|
1162
1345
|
'<span class="wf-meta">' + escapeXml(r.checkType) + '</span>' +
|
|
1163
1346
|
'<span class="wf-meta">' + escapeXml(rowSpendText(r)) + '</span>' +
|
|
1164
|
-
'<span class="wf-meta">' + escapeXml(r
|
|
1347
|
+
'<span class="wf-meta">' + escapeXml(rowWallText(r)) + '</span>' +
|
|
1165
1348
|
'<span class="wf-meta">' + escapeXml(r.date || "unknown") + '</span>' +
|
|
1349
|
+
resumedTagHtml(r.resumedCount, "hist-resumed-" + r.runid) +
|
|
1166
1350
|
'</span>';
|
|
1167
1351
|
}
|
|
1168
1352
|
row.addEventListener("click", function(){ selectRun(r.runid, row, ".hist-row"); document.getElementById("tab-run").click(); scrollRunIntoViewMobile(); });
|
|
@@ -1203,12 +1387,16 @@
|
|
|
1203
1387
|
runs: jobRuns,
|
|
1204
1388
|
runCount: jobRuns.length,
|
|
1205
1389
|
lastRunid: last.runid,
|
|
1390
|
+
lastRunNo: last.runNo,
|
|
1206
1391
|
lastAt: last.at,
|
|
1207
1392
|
lastGlyph: last.glyph || "▶",
|
|
1393
|
+
lastStatus: last.status || null,
|
|
1394
|
+
lastEndedLine: last.endedLine || null,
|
|
1395
|
+
lastResumedCount: last.resumedCount || 0,
|
|
1208
1396
|
lastCheckType: last.checkType || "unknown",
|
|
1209
1397
|
lastModel: last.model || null,
|
|
1210
|
-
lastSpend: rowSpendText(last),
|
|
1211
|
-
lastWall: last
|
|
1398
|
+
lastSpend: rowSpendText(last, true),
|
|
1399
|
+
lastWall: rowWallText(last) || "unknown",
|
|
1212
1400
|
lastDate: last.date,
|
|
1213
1401
|
};
|
|
1214
1402
|
});
|
|
@@ -1333,16 +1521,16 @@
|
|
|
1333
1521
|
row.setAttribute("role", "button");
|
|
1334
1522
|
row.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1335
1523
|
row.setAttribute("aria-pressed", isSelected ? "true" : "false");
|
|
1336
|
-
var wfFullName = g.job
|
|
1524
|
+
var wfFullName = runName(g.job, g.lastRunNo, g.lastRunid);
|
|
1337
1525
|
row.innerHTML =
|
|
1338
|
-
'<span class="wf-line1"><span class="wf-caret">' + (hasOtherRuns ? (expanded ? "▼" : "▶") : "") + '</span><span class="dot ' + glyphClass(g.lastGlyph) + '"></span>' +
|
|
1339
|
-
'<span class="wf-name" title="' + escapeXml(wfFullName) + '">' + escapeXml(wfFullName) + '</span
|
|
1526
|
+
'<span class="wf-line1 wf-compact"><span class="wf-caret">' + (hasOtherRuns ? (expanded ? "▼" : "▶") : "") + '</span><span class="dot ' + glyphClass(g.lastGlyph) + '"></span>' +
|
|
1527
|
+
'<span class="wf-name" title="' + escapeXml(wfFullName) + '">' + escapeXml(wfFullName) + '</span>' +
|
|
1528
|
+
(g.lastStatus ? '<span class="wf-ended wf-ended-inline" data-testid="wf-ended-' + escapeXml(g.job) + '">' + statusWordHtml(g.lastStatus, g.lastEndedLine) + '</span>' : '') + '</span>' +
|
|
1340
1529
|
'<span class="wf-meta-line">' +
|
|
1341
1530
|
'<span class="wf-meta">' + escapeXml(g.lastCheckType) + '</span>' +
|
|
1342
1531
|
'<span class="wf-meta">' + escapeXml(g.lastSpend) + '</span>' +
|
|
1343
1532
|
'<span class="wf-meta">' + escapeXml(g.lastWall) + '</span>' +
|
|
1344
1533
|
'<span class="wf-meta">' + escapeXml(g.lastDate || "unknown") + '</span>' +
|
|
1345
|
-
'<span class="wf-meta">' + g.runCount + ' run' + (g.runCount === 1 ? '' : 's') + '</span>' +
|
|
1346
1534
|
'</span>';
|
|
1347
1535
|
row.addEventListener("click", function(){
|
|
1348
1536
|
// a single-run job has nothing to expand — the click still opens
|
|
@@ -1368,7 +1556,7 @@
|
|
|
1368
1556
|
if(expanded){
|
|
1369
1557
|
var runsWrap = document.createElement("div");
|
|
1370
1558
|
runsWrap.className = "wf-runs";
|
|
1371
|
-
childRuns.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r)); });
|
|
1559
|
+
childRuns.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
|
|
1372
1560
|
wrap.appendChild(runsWrap);
|
|
1373
1561
|
}
|
|
1374
1562
|
el.appendChild(wrap);
|
|
@@ -1387,7 +1575,7 @@
|
|
|
1387
1575
|
function filterBarHTML(scope){
|
|
1388
1576
|
return (
|
|
1389
1577
|
'<div class="filterbar" data-scope="' + scope + '" data-testid="' + scope + '-filters">' +
|
|
1390
|
-
'<input type="text" class="search-input" placeholder="search job, run id or model…" ' +
|
|
1578
|
+
'<input type="text" class="search-input" placeholder="search job, run-N, run id or model…" ' +
|
|
1391
1579
|
'id="' + scope + '-filter-search" data-testid="' + scope + '-filter-search" autocomplete="off">' +
|
|
1392
1580
|
'<div class="chips" role="group" aria-label="Filter">' +
|
|
1393
1581
|
'<button class="chip fchip" type="button" aria-pressed="false" data-filter-group="checkType" data-filter-value="deterministic" title="deterministic" data-testid="chip-' + scope + '-ct-deterministic">D</button>' +
|
|
@@ -1416,15 +1604,18 @@
|
|
|
1416
1604
|
// means "no search filter" — everything passes it, same posture as an
|
|
1417
1605
|
// empty chip group). item C (2026-09-26): also matches the run's model
|
|
1418
1606
|
// name (e.g. "deepseek", "sonnet") — `model` is `null`/absent on an older
|
|
1419
|
-
// archived run, same posture as a missing job/runid.
|
|
1420
|
-
|
|
1607
|
+
// archived run, same posture as a missing job/runid. Also the run's shown
|
|
1608
|
+
// name: `run-N` and `job (run-N)` (N is the server's `runNo`, never counted here).
|
|
1609
|
+
function matchesSearch(job, runid, model, query, runNo){
|
|
1421
1610
|
if(!query) return true;
|
|
1422
1611
|
var q = String(query).trim().toLowerCase();
|
|
1423
1612
|
if(!q) return true;
|
|
1424
1613
|
var j = String(job || "").toLowerCase();
|
|
1425
1614
|
var r = String(runid || "").toLowerCase();
|
|
1426
1615
|
var m = String(model || "").toLowerCase();
|
|
1427
|
-
|
|
1616
|
+
var label = typeof runNo === "number" ? "run-" + runNo : "";
|
|
1617
|
+
var shown = label ? j + " (" + label + ")" : "";
|
|
1618
|
+
return j.indexOf(q) !== -1 || r.indexOf(q) !== -1 || m.indexOf(q) !== -1 || (label !== "" && (label.indexOf(q) !== -1 || shown.indexOf(q) !== -1));
|
|
1428
1619
|
}
|
|
1429
1620
|
function filterRuns(runs, filters, nowMs){
|
|
1430
1621
|
nowMs = (typeof nowMs === "number") ? nowMs : Date.now();
|
|
@@ -1435,7 +1626,7 @@
|
|
|
1435
1626
|
return (runs || []).filter(function(r){
|
|
1436
1627
|
if(ct.length && ct.indexOf(r.checkType) === -1) return false;
|
|
1437
1628
|
if(rs.length && rs.indexOf(r.glyph) === -1) return false;
|
|
1438
|
-
if(!matchesSearch(r.job, r.runid, r.model, q)) return false;
|
|
1629
|
+
if(!matchesSearch(r.job, r.runid, r.model, q, r.runNo)) return false;
|
|
1439
1630
|
if(time !== "all"){
|
|
1440
1631
|
var days = time === "7d" ? 7 : 30;
|
|
1441
1632
|
var t = typeof r.at === "string" ? Date.parse(r.at) : NaN;
|
|
@@ -1585,6 +1776,10 @@
|
|
|
1585
1776
|
});
|
|
1586
1777
|
document.getElementById("wf-list").hidden = runsViewMode !== "workflows";
|
|
1587
1778
|
document.getElementById("history-list").hidden = runsViewMode !== "history";
|
|
1779
|
+
// P5 item 4: Import lives on the Workflows view only
|
|
1780
|
+
document.getElementById("wf-import").hidden = runsViewMode !== "workflows";
|
|
1781
|
+
document.getElementById("import-list").hidden = runsViewMode !== "workflows";
|
|
1782
|
+
if(runsViewMode !== "workflows") document.getElementById("import-box").hidden = true;
|
|
1588
1783
|
}
|
|
1589
1784
|
document.querySelectorAll('.chip[data-runs-view]').forEach(function(chip){
|
|
1590
1785
|
chip.addEventListener("click", function(){
|
|
@@ -1622,6 +1817,264 @@
|
|
|
1622
1817
|
document.querySelectorAll(".wf-row, .hist-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
1623
1818
|
}
|
|
1624
1819
|
|
|
1820
|
+
// ---- P5 item 4: IMPORT (read only) — the folder browser, the imported rows, the view ----
|
|
1821
|
+
// The folder listing, the import and every re-read are SERVER routes behind the human guard
|
|
1822
|
+
// (src/panel/importroutes.js): the page only asks and paints. An imported job has ONE button: Reuse workflow.
|
|
1823
|
+
var currentImportId = null;
|
|
1824
|
+
var importBox = document.getElementById("import-box");
|
|
1825
|
+
var importPathEl = document.getElementById("import-path");
|
|
1826
|
+
var importErrEl = document.getElementById("import-err");
|
|
1827
|
+
function importShowErr(msg){ importErrEl.textContent = msg; importErrEl.hidden = !msg; }
|
|
1828
|
+
// the last folder the browser showed — per-viewer convenience only (try/catch, the browser works without it)
|
|
1829
|
+
var IMPORT_DIR_KEY = "bareloop.importDir";
|
|
1830
|
+
function rememberImportDir(path){
|
|
1831
|
+
try{ localStorage.setItem(IMPORT_DIR_KEY, path); }catch(e){ /* best-effort only */ }
|
|
1832
|
+
}
|
|
1833
|
+
function recallImportDir(){
|
|
1834
|
+
try{ var v = localStorage.getItem(IMPORT_DIR_KEY); if(typeof v === "string") return v; }catch(e){ /* per-viewer convenience only */ }
|
|
1835
|
+
return "";
|
|
1836
|
+
}
|
|
1837
|
+
// one directory: its folders (names + a "bundle" tag), a way up, and which folder the server actually chose
|
|
1838
|
+
function importLoadDir(path){
|
|
1839
|
+
importShowErr("");
|
|
1840
|
+
return authorGet("/api/fs/list?path=" + encodeURIComponent(path || "")).then(function(r){
|
|
1841
|
+
if(!r || !r.ok){
|
|
1842
|
+
// a refused path leaves ONLY the error: no stale "showing <folder>" line, no stale list
|
|
1843
|
+
document.getElementById("import-resolved").textContent = "";
|
|
1844
|
+
document.getElementById("import-entries").innerHTML = "";
|
|
1845
|
+
importShowErr((r && r.error) || "could not open that folder");
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
rememberImportDir(r.path);
|
|
1849
|
+
importPathEl.value = r.path;
|
|
1850
|
+
document.getElementById("import-resolved").textContent = "showing " + r.path + (r.requested !== r.path ? " (you asked for " + r.requested + ")" : "") + (r.bundle ? " — this folder is an exported job" : "");
|
|
1851
|
+
var el = document.getElementById("import-entries");
|
|
1852
|
+
el.innerHTML = "";
|
|
1853
|
+
if(r.parent){
|
|
1854
|
+
var up = document.createElement("button");
|
|
1855
|
+
up.type = "button"; up.className = "import-entry import-up"; up.setAttribute("data-testid", "import-up");
|
|
1856
|
+
up.setAttribute("aria-label", "up one folder"); up.setAttribute("title", "up one folder");
|
|
1857
|
+
up.textContent = "..";
|
|
1858
|
+
up.addEventListener("click", function(){ importLoadDir(r.parent); });
|
|
1859
|
+
el.appendChild(up);
|
|
1860
|
+
}
|
|
1861
|
+
r.entries.forEach(function(e){
|
|
1862
|
+
var b = document.createElement("button");
|
|
1863
|
+
b.type = "button"; b.className = "import-entry"; b.setAttribute("data-testid", "import-entry-" + e.name);
|
|
1864
|
+
b.setAttribute("aria-pressed", "false");
|
|
1865
|
+
b.innerHTML = escapeXml(e.name) + ' <span class="tag">(bundle)</span>';
|
|
1866
|
+
b.addEventListener("click", function(){
|
|
1867
|
+
el.querySelectorAll(".import-entry").forEach(function(x){ x.classList.remove("selected"); x.setAttribute("aria-pressed", "false"); });
|
|
1868
|
+
b.classList.add("selected"); b.setAttribute("aria-pressed", "true");
|
|
1869
|
+
importPathEl.value = e.path; importShowErr("");
|
|
1870
|
+
});
|
|
1871
|
+
el.appendChild(b);
|
|
1872
|
+
});
|
|
1873
|
+
if(!r.entries.length) el.insertAdjacentHTML("beforeend", '<div class="hint" data-testid="import-empty">no job bundles here (searched ' + r.depth + ' folders deep)</div>');
|
|
1874
|
+
if(r.stopped) el.insertAdjacentHTML("beforeend", '<div class="hint" data-testid="import-capped">stopped after ' + r.visited + ' folders \u2014 go into a narrower folder</div>');
|
|
1875
|
+
else if(r.truncated) el.insertAdjacentHTML("beforeend", '<div class="hint" data-testid="import-capped">stopped after ' + r.entries.length + ' job bundles \u2014 go into a narrower folder</div>');
|
|
1876
|
+
return true;
|
|
1877
|
+
}).catch(function(x){ importShowErr("could not reach the panel: " + x.message); });
|
|
1878
|
+
}
|
|
1879
|
+
document.getElementById("wf-import").addEventListener("click", function(){
|
|
1880
|
+
importBox.hidden = !importBox.hidden;
|
|
1881
|
+
if(importBox.hidden) return;
|
|
1882
|
+
var want = importPathEl.value || recallImportDir();
|
|
1883
|
+
importLoadDir(want).then(function(ok){ if(ok === false && want && !importPathEl.value) { importShowErr(""); return importLoadDir(""); } });
|
|
1884
|
+
});
|
|
1885
|
+
document.getElementById("import-open").addEventListener("click", function(){ importLoadDir(importPathEl.value); });
|
|
1886
|
+
importPathEl.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); importLoadDir(importPathEl.value); } });
|
|
1887
|
+
document.getElementById("import-cancel").addEventListener("click", function(){ importBox.hidden = true; importShowErr(""); });
|
|
1888
|
+
document.getElementById("import-go").addEventListener("click", function(){
|
|
1889
|
+
importShowErr("");
|
|
1890
|
+
authorPost("/api/imports", {path: importPathEl.value}).then(function(res){
|
|
1891
|
+
if(!res.body || !res.body.ok){ importShowErr((res.body && res.body.error) || ("refused (" + res.status + ")")); return; }
|
|
1892
|
+
importBox.hidden = true;
|
|
1893
|
+
return loadImports().then(function(){ return selectImport(res.body.id); }).then(function(){ revealImportRow(res.body.id); scrollRunIntoViewMobile(); });
|
|
1894
|
+
}).catch(function(x){ importShowErr("could not reach the panel: " + x.message); });
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
// after [Import] the box above the list closes and the left pane shrinks: bring the job just opened into view
|
|
1898
|
+
function revealImportRow(id){
|
|
1899
|
+
var row = document.querySelector('[data-testid="import-row-' + id + '"]');
|
|
1900
|
+
if(row && row.scrollIntoView) row.scrollIntoView({block: "nearest"});
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// the imported rows (Workflows view): every bundle is re-read by the server each time this list is fetched
|
|
1904
|
+
function loadImports(){
|
|
1905
|
+
return authorGet("/api/imports").then(function(r){ renderImports((r && r.imports) || []); }).catch(function(){ /* keep what is shown */ });
|
|
1906
|
+
}
|
|
1907
|
+
function renderImports(list){
|
|
1908
|
+
var el = document.getElementById("import-list");
|
|
1909
|
+
el.innerHTML = "";
|
|
1910
|
+
list.forEach(function(imp){
|
|
1911
|
+
var row = document.createElement("div");
|
|
1912
|
+
row.className = "wf-row" + (imp.id === currentImportId ? " selected" : "");
|
|
1913
|
+
row.setAttribute("data-testid", "import-row-" + imp.id);
|
|
1914
|
+
row.setAttribute("tabindex", "0");
|
|
1915
|
+
row.setAttribute("role", "button");
|
|
1916
|
+
row.setAttribute("aria-pressed", imp.id === currentImportId ? "true" : "false");
|
|
1917
|
+
row.innerHTML =
|
|
1918
|
+
'<span class="wf-line1"><span class="wf-caret"></span><span class="dot ' + (imp.runStatus ? glyphClass(imp.runStatus.sign) : "grey") + '"></span>' +
|
|
1919
|
+
'<span class="wf-name" title="' + escapeXml(imp.dir) + '">' + escapeXml(imp.job) + '</span></span>' +
|
|
1920
|
+
(imp.runStatus ? '<span class="wf-ended" data-testid="import-ended-' + escapeXml(imp.id) + '">' + statusWordHtml(imp.runStatus, imp.runReason) + '</span>' : '') +
|
|
1921
|
+
'<span class="wf-meta-line"><span class="wf-meta wf-imported" data-testid="import-tag-' + escapeXml(imp.id) + '">imported · view only</span></span>' +
|
|
1922
|
+
(imp.status !== "ok" ? '<span class="imp-bad" data-testid="import-changed-' + escapeXml(imp.id) + '">' + escapeXml(imp.statusText || "changed since import") + '</span>' : "");
|
|
1923
|
+
row.addEventListener("click", function(){ selectImport(imp.id); scrollRunIntoViewMobile(); });
|
|
1924
|
+
row.addEventListener("keydown", function(e){ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); row.click(); } });
|
|
1925
|
+
el.appendChild(row);
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
function hideImportView(){
|
|
1929
|
+
currentImportId = null;
|
|
1930
|
+
document.getElementById("import-view").hidden = true;
|
|
1931
|
+
document.getElementById("run-counters").hidden = false;
|
|
1932
|
+
document.getElementById("run-empty").textContent = "select a run from Workflows or History";
|
|
1933
|
+
document.getElementById("audit-select-empty").textContent = "select a run first";
|
|
1934
|
+
document.querySelectorAll("#import-list .wf-row").forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed", "false"); });
|
|
1935
|
+
}
|
|
1936
|
+
function selectImport(id){
|
|
1937
|
+
clearSelection();
|
|
1938
|
+
currentRunid = null;
|
|
1939
|
+
runIsLive = false;
|
|
1940
|
+
resumeMode = null;
|
|
1941
|
+
currentImportId = id;
|
|
1942
|
+
document.querySelectorAll("#import-list .wf-row").forEach(function(r){
|
|
1943
|
+
var on = r.getAttribute("data-testid") === "import-row-" + id;
|
|
1944
|
+
r.classList.toggle("selected", on); r.setAttribute("aria-pressed", on ? "true" : "false");
|
|
1945
|
+
});
|
|
1946
|
+
return authorGet("/api/imports/" + encodeURIComponent(id)).then(function(v){
|
|
1947
|
+
if(currentImportId !== id) return;
|
|
1948
|
+
renderImportView(v);
|
|
1949
|
+
}).catch(function(x){ if(currentImportId === id) renderImportView({ok: false, error: x.message}); });
|
|
1950
|
+
}
|
|
1951
|
+
// An imported job reads like a normal run page: the SAME Run / Audit / Job tabs. [Reuse workflow] sits in the top
|
|
1952
|
+
// action row, then the IMPORTED box (what the folder says about itself), then the latest GREEN run in the bundle
|
|
1953
|
+
// painted by the very same renderRun a run uses — its SUMMARY and MAP, and its Audit when the bundle carries the
|
|
1954
|
+
// run's log; a bundle with only a bridge paints the bridge's numbers through that same renderer ("not recorded"
|
|
1955
|
+
// where the bridge is silent); a bundle with no green says so. The Job tab carries the spec. Nothing here writes.
|
|
1956
|
+
function importedHeader(v){
|
|
1957
|
+
var tag = "imported \u00B7 view only";
|
|
1958
|
+
if(v.runStatus) setRunHeader(v.runStatus.sign, v.job, statusWordHtml(v.runStatus, v.runReason) + " \u00B7 " + tag);
|
|
1959
|
+
else setRunHeader("\u00B7", v.job, tag);
|
|
1960
|
+
}
|
|
1961
|
+
function importedNoLog(){
|
|
1962
|
+
document.getElementById("audit-body").hidden = true;
|
|
1963
|
+
var auditEmpty = document.getElementById("audit-select-empty");
|
|
1964
|
+
auditEmpty.hidden = false;
|
|
1965
|
+
auditEmpty.textContent = "no log — this job ran on another machine";
|
|
1966
|
+
}
|
|
1967
|
+
// the green run below the IMPORTED box: spine (read through the ordinary run routes), bridge, or nothing
|
|
1968
|
+
function paintImportedRun(v){
|
|
1969
|
+
var run = v.run || {kind: "none"};
|
|
1970
|
+
var content = document.getElementById("run-content");
|
|
1971
|
+
var guard = function(){ return currentImportId === v.id; };
|
|
1972
|
+
document.getElementById("step-list").innerHTML = "";
|
|
1973
|
+
if(run.kind === "spine"){
|
|
1974
|
+
currentRunid = run.id;
|
|
1975
|
+
getJSON("/api/runs/" + encodeURIComponent(run.id)).then(function(d){
|
|
1976
|
+
if(!guard()) return;
|
|
1977
|
+
d.ended = null; d.resume = null; d.live = false; d.stopping = false;
|
|
1978
|
+
renderRun(d);
|
|
1979
|
+
importedHeader(v);
|
|
1980
|
+
document.getElementById("run-actions").hidden = true;
|
|
1981
|
+
document.getElementById("run-counters").hidden = true; // the SUMMARY headline already says it
|
|
1982
|
+
content.hidden = false;
|
|
1983
|
+
}).catch(function(e){ if(guard()) paintImportedRunPlain(v, "could not read this run: " + e.message); });
|
|
1984
|
+
getJSON("/api/runs/" + encodeURIComponent(run.id) + "/audit").then(function(result){
|
|
1985
|
+
if(guard()) renderAudit(result);
|
|
1986
|
+
}).catch(function(){ if(guard()) renderAudit(null); });
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
importedNoLog();
|
|
1990
|
+
if(run.kind === "bridge" && run.detail){
|
|
1991
|
+
renderRun(run.detail);
|
|
1992
|
+
importedHeader(v);
|
|
1993
|
+
importedNoLog();
|
|
1994
|
+
document.getElementById("run-actions").hidden = true;
|
|
1995
|
+
document.getElementById("run-counters").hidden = true;
|
|
1996
|
+
document.getElementById("step-list").innerHTML = "";
|
|
1997
|
+
if(!run.detail.parts || run.detail.parts.length === 0){
|
|
1998
|
+
document.getElementById("map-box").hidden = false;
|
|
1999
|
+
document.getElementById("map-mount").innerHTML = '<div class="hint" data-testid="import-no-steps">no steps recorded</div>';
|
|
2000
|
+
}
|
|
2001
|
+
content.hidden = false;
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
paintImportedRunPlain(v, "no green run in this bundle");
|
|
2005
|
+
}
|
|
2006
|
+
// a plain sentence in the SUMMARY box and nothing else (no counters, no map, no cards)
|
|
2007
|
+
function paintImportedRunPlain(v, text){
|
|
2008
|
+
var box = document.getElementById("run-summary");
|
|
2009
|
+
box.className = "summary-box";
|
|
2010
|
+
box.innerHTML = '<div class="summary-row"><span class="sum-val" data-testid="import-no-green">' + escapeXml(text) + '</span></div>';
|
|
2011
|
+
document.getElementById("run-counters").hidden = true;
|
|
2012
|
+
document.getElementById("map-box").hidden = true;
|
|
2013
|
+
document.getElementById("step-list").innerHTML = "";
|
|
2014
|
+
document.getElementById("run-actions").hidden = true;
|
|
2015
|
+
importedNoLog();
|
|
2016
|
+
document.getElementById("run-content").hidden = false;
|
|
2017
|
+
}
|
|
2018
|
+
function renderImportView(v){
|
|
2019
|
+
var el = document.getElementById("import-view");
|
|
2020
|
+
el.hidden = false;
|
|
2021
|
+
document.getElementById("run-empty").hidden = true;
|
|
2022
|
+
document.getElementById("run-content").hidden = true;
|
|
2023
|
+
importedNoLog();
|
|
2024
|
+
if(!v || !v.ok){
|
|
2025
|
+
setRunHeader("\u00B7", "imported job", "imported \u00B7 view only");
|
|
2026
|
+
el.innerHTML = '<div class="imp-bad">' + escapeXml((v && v.error) || "could not read this imported job") + '</div>';
|
|
2027
|
+
renderJob(null);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
importedHeader(v);
|
|
2031
|
+
var h = v.history || {greens: 0, reds: 0, total: 0, recent: []};
|
|
2032
|
+
var row = function(label, text, testid){
|
|
2033
|
+
return '<div class="summary-row"><span class="sum-label">' + label + '</span><span class="sum-val" data-testid="' + testid + '">' + escapeXml(text) + '</span></div>';
|
|
2034
|
+
};
|
|
2035
|
+
var recent = (h.recent || []).map(function(r){
|
|
2036
|
+
var line = (r.at || "").slice(0, 10) || "—";
|
|
2037
|
+
line += " · " + (r.outcome || "?") + (typeof r.costUsd === "number" ? " · " + panelMoney(r.costUsd) : "") + (r.runid ? " · run " + r.runid : "");
|
|
2038
|
+
return '<div class="summary-row"><span class="sum-label"></span><span class="sum-val">' + escapeXml(line) + '</span></div>';
|
|
2039
|
+
}).join("");
|
|
2040
|
+
var run = v.run || {kind: "none"};
|
|
2041
|
+
var runsHere = typeof v.runsHere === "number" ? v.runsHere : 0;
|
|
2042
|
+
var shown = run.kind === "spine" ? ("latest green run here, " + (run.runid || "")) :
|
|
2043
|
+
(run.kind === "bridge" ? ("the exported green run" + (run.runid ? " " + run.runid : "") + (run.at ? ", " + run.at.slice(0, 10) : "")) : "no green run");
|
|
2044
|
+
el.innerHTML =
|
|
2045
|
+
'<div class="run-actions" data-testid="run-actions-import"><button class="btn" type="button" data-testid="btn-reuse-import">Reuse workflow</button></div>' +
|
|
2046
|
+
'<div class="summary-box imported-box" data-testid="import-box">' +
|
|
2047
|
+
(v.status !== "ok" ? '<div class="imp-bad" data-testid="import-view-changed">' + escapeXml(v.changedLine || v.statusText || "changed since import") + '</div>' : "") +
|
|
2048
|
+
row("runs here:", runsHere === 0 ? "none yet" : (runsHere + (runsHere === 1 ? " run" : " runs")), "import-no-runs") +
|
|
2049
|
+
row("exported:", h.greens + " green · " + h.reds + " not green", "import-history") +
|
|
2050
|
+
recent +
|
|
2051
|
+
row("this machine:", v.approvedText || "unknown", "import-approved") +
|
|
2052
|
+
row("shown below:", shown, "import-shown") +
|
|
2053
|
+
'</div>';
|
|
2054
|
+
el.querySelector('[data-testid="btn-reuse-import"]').addEventListener("click", function(){ reuseImportedWorkflow(v.id); });
|
|
2055
|
+
var nr = "not recorded";
|
|
2056
|
+
renderJob({
|
|
2057
|
+
job: v.job, resolved: false, checkType: v.checkType || nr, model: v.model || nr,
|
|
2058
|
+
goal: v.goal || nr, success: v.success || nr, guardrails: v.guardrails || nr,
|
|
2059
|
+
source: nr, destination: nr, tools: nr,
|
|
2060
|
+
budgetUsd: (typeof v.budgetUsd === "number") ? v.budgetUsd : null,
|
|
2061
|
+
maxWallMs: (typeof v.maxWallMs === "number") ? v.maxWallMs : null,
|
|
2062
|
+
note: "imported · view only — the exported job's own spec",
|
|
2063
|
+
});
|
|
2064
|
+
paintImportedRun(v);
|
|
2065
|
+
document.getElementById("tab-run").click();
|
|
2066
|
+
}
|
|
2067
|
+
// Reuse workflow, on an IMPORTED job: the server reads the bundle again now and prefills from its signed spec.
|
|
2068
|
+
function reuseImportedWorkflow(id){
|
|
2069
|
+
authorGet("/api/author/start-from?import=" + encodeURIComponent(id)).then(function(r){
|
|
2070
|
+
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2071
|
+
document.getElementById("tab-chat").click();
|
|
2072
|
+
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: null, importId: id, prefill: r}}));
|
|
2073
|
+
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2074
|
+
}
|
|
2075
|
+
loadImports();
|
|
2076
|
+
document.getElementById("tab-runs").addEventListener("click", function(){ loadImports(); });
|
|
2077
|
+
|
|
1625
2078
|
// ---- Run tab ----
|
|
1626
2079
|
// panelMoney — hamr's ruling 2026-09-28 ("panel money 2-decimals"): every
|
|
1627
2080
|
// money display in THIS page (Run tab summary/counters, Runs list rows,
|
|
@@ -1667,18 +2120,30 @@
|
|
|
1667
2120
|
* complete run whose drafting share is itself incomplete — hamr's ruling:
|
|
1668
2121
|
* a reader summing the two must never read the leading number as exact
|
|
1669
2122
|
* when the bracket next to it isn't). `draftSpendComplete === false` marks
|
|
1670
|
-
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX".
|
|
2123
|
+
* the bracket itself a floor. `budgetUsd` (when given) appends " of $X.XX". `shortForm` drops the word
|
|
2124
|
+
* "drafting" (the Runs list cards only — "$5.00 ($0.08)").
|
|
1671
2125
|
*/
|
|
1672
|
-
function panelMoneyWithDraft(spendCoreVal, spendCoreFloor, draftSpentUsd, draftSpendComplete, budgetUsd){
|
|
2126
|
+
function panelMoneyWithDraft(spendCoreVal, spendCoreFloor, draftSpentUsd, draftSpendComplete, budgetUsd, shortForm){
|
|
1673
2127
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
1674
2128
|
var draftFloor = hasDraft && draftSpendComplete === false;
|
|
1675
2129
|
var core = (spendCoreVal === null || spendCoreVal === undefined)
|
|
1676
2130
|
? "unknown"
|
|
1677
2131
|
: ((spendCoreFloor || draftFloor) ? "at least " + panelMoney(spendCoreVal) : panelMoney(spendCoreVal));
|
|
1678
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + " drafting)" : "";
|
|
2132
|
+
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
1679
2133
|
var capPart = (budgetUsd === undefined) ? "" : (" of " + panelMoney(budgetUsd));
|
|
1680
2134
|
return core + draftPart + capPart;
|
|
1681
2135
|
}
|
|
2136
|
+
// P5 item 6: a LIVE row (no job-end yet, runner alive) reads the same running floors the
|
|
2137
|
+
// right pane reads — `liveSpendText`/`liveWallPhrase` are the one spelling — and a row whose
|
|
2138
|
+
// spine is not written yet reads "starting…", never "file missing" or "unknown".
|
|
2139
|
+
function rowIsLive(r){
|
|
2140
|
+
return r.glyph === "▶" && !r.died && !r.starting && (r.spentUsd === null || r.spentUsd === undefined);
|
|
2141
|
+
}
|
|
2142
|
+
function rowWallText(r){
|
|
2143
|
+
if(r.starting) return "just started";
|
|
2144
|
+
if(rowIsLive(r)) return liveWallPhrase(r.wallFloorMs);
|
|
2145
|
+
return r.wall;
|
|
2146
|
+
}
|
|
1682
2147
|
/**
|
|
1683
2148
|
* The Runs list row / Workflows grouping's own spend text — off the
|
|
1684
2149
|
* numeric fields `/api/runs`' row now carries (never `r.spend`, which is
|
|
@@ -1689,11 +2154,13 @@
|
|
|
1689
2154
|
* is a floor only when its own drafting share is incomplete.
|
|
1690
2155
|
* @param {any} r a `/api/runs` row (or a Workflows group's `last` row)
|
|
1691
2156
|
*/
|
|
1692
|
-
function rowSpendText(r){
|
|
2157
|
+
function rowSpendText(r, shortForm){
|
|
2158
|
+
if(r.starting) return "starting…";
|
|
2159
|
+
if(rowIsLive(r)) return liveSpendText(r.spendFloorUsd, r.draftSpentUsd, r.draftSpendComplete, shortForm);
|
|
1693
2160
|
var draftIncomplete = typeof r.draftSpentUsd === "number" && r.draftSpentUsd !== null && r.draftSpendComplete === false;
|
|
1694
2161
|
var coreVal = r.died ? r.spendFloorUsd : r.spentUsd;
|
|
1695
2162
|
var coreFloor = !!r.died || draftIncomplete;
|
|
1696
|
-
return panelMoneyWithDraft(coreVal, coreFloor, r.draftSpentUsd, r.draftSpendComplete);
|
|
2163
|
+
return panelMoneyWithDraft(coreVal, coreFloor, r.draftSpentUsd, r.draftSpendComplete, undefined, shortForm);
|
|
1697
2164
|
}
|
|
1698
2165
|
function duration(ms){
|
|
1699
2166
|
if(ms === null || ms === undefined) return "unknown";
|
|
@@ -1716,11 +2183,11 @@
|
|
|
1716
2183
|
* @param {number|null} spendFloorUsd @param {number|null} draftSpentUsd
|
|
1717
2184
|
* @param {boolean|null} draftSpendComplete @returns {string}
|
|
1718
2185
|
*/
|
|
1719
|
-
function liveSpendText(spendFloorUsd, draftSpentUsd, draftSpendComplete){
|
|
2186
|
+
function liveSpendText(spendFloorUsd, draftSpentUsd, draftSpendComplete, shortForm){
|
|
1720
2187
|
if(spendFloorUsd === null || spendFloorUsd === undefined) return "unknown";
|
|
1721
2188
|
var hasDraft = typeof draftSpentUsd === "number" && draftSpentUsd !== null;
|
|
1722
2189
|
var draftFloor = hasDraft && draftSpendComplete === false;
|
|
1723
|
-
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + " drafting)" : "";
|
|
2190
|
+
var draftPart = hasDraft ? " (" + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
|
|
1724
2191
|
return panelMoney(spendFloorUsd) + " so far" + draftPart;
|
|
1725
2192
|
}
|
|
1726
2193
|
/** build item 6 — the matching wall-clock phrase: "running <duration>",
|
|
@@ -1807,6 +2274,11 @@
|
|
|
1807
2274
|
if(!detail.model) return "not recorded";
|
|
1808
2275
|
return detail.provider ? (detail.model + " (" + detail.provider + ")") : detail.model;
|
|
1809
2276
|
}
|
|
2277
|
+
// an imported job's bridge names the tools its green run used, with no counts
|
|
2278
|
+
function bridgeToolsLine(list){
|
|
2279
|
+
if(!Array.isArray(list) || list.length === 0) return "not recorded";
|
|
2280
|
+
return list.map(function(k){ return toolDisplayLabel(k); }).join(" \u00b7 ");
|
|
2281
|
+
}
|
|
1810
2282
|
function toolsLine(behaviour){
|
|
1811
2283
|
if(!behaviour) return "unknown (no log saved)";
|
|
1812
2284
|
if(behaviour.totalCalls === 0) return "0 calls";
|
|
@@ -1858,36 +2330,199 @@
|
|
|
1858
2330
|
else { row.hidden = true; val.textContent = ""; }
|
|
1859
2331
|
}
|
|
1860
2332
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2333
|
+
// ---- P5: the ENDED block (Audit tab, first block, every finished run) and Resume ----
|
|
2334
|
+
// The sentences are CODE-OWNED on the server (`endedFor`, src/panel/server.js) — this only
|
|
2335
|
+
// paints them. `detail.ended` is null while a run is live (no block while running).
|
|
2336
|
+
var lastEndedSig = null;
|
|
2337
|
+
// P5-R (hamr 2026-10-02): Resume opens the run's OWN Job tab. There ONLY the money cap and the time cap are
|
|
2338
|
+
// editable; every other field is the signed job, shown read-only exactly as the tab already shows it. The click
|
|
2339
|
+
// on [Sign & resume] is the signature when a cap changed. A resume is the SAME run: no new card, no new job.
|
|
2340
|
+
var resumeMode = null;
|
|
2341
|
+
var lastJobShown = null;
|
|
2342
|
+
function renderEnded(detail){
|
|
2343
|
+
var box = document.getElementById("ended-block");
|
|
2344
|
+
var e = detail.ended;
|
|
2345
|
+
if(!e){ box.hidden = true; box.innerHTML = ""; lastEndedSig = null; return; }
|
|
2346
|
+
// rebuilt only when what it shows changed
|
|
2347
|
+
var sig2 = detail.runid + "|" + JSON.stringify(e) + "|" + JSON.stringify(detail.resume);
|
|
2348
|
+
if(sig2 === lastEndedSig) return;
|
|
2349
|
+
lastEndedSig = sig2;
|
|
2350
|
+
box.hidden = false;
|
|
2351
|
+
box.className = "ended-block " + (detail.died ? "magenta" : (detail.glyph === "✓" ? "green" : "red"));
|
|
2352
|
+
// text only: the ONE place for [Stop] / [Resume] / [Reuse workflow] is the Run tab's top action row (renderRunActions)
|
|
2353
|
+
box.innerHTML =
|
|
2354
|
+
'<div class="ended-row"><span class="ended-label">ENDED</span><span class="ended-text" data-testid="ended-reason">' + escapeXml(e.reason) + '</span></div>' +
|
|
2355
|
+
'<div class="ended-row"><span class="ended-label">NEXT</span><span class="ended-text" data-testid="ended-next">' + escapeXml(e.next) + '</span></div>';
|
|
2356
|
+
}
|
|
2357
|
+
// ---- P5 item 5: the run's ACTION AREA (Run tab, above the counters) — Stop while the run is live, Resume once
|
|
2358
|
+
// it ended in a way the engine would accept (the SAME button as the Ended block's, same Job-tab resume mode).
|
|
2359
|
+
// The Ended block (Audit tab) is text only and carries no buttons.
|
|
2360
|
+
var stopAsked = {};
|
|
2361
|
+
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
2362
|
+
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2363
|
+
function reuseWorkflow(runid){
|
|
2364
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid)).then(function(r){
|
|
2365
|
+
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2366
|
+
document.getElementById("tab-chat").click();
|
|
2367
|
+
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
2368
|
+
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2369
|
+
}
|
|
2370
|
+
function renderRunActions(detail){
|
|
2371
|
+
var bar = document.getElementById("run-actions");
|
|
2372
|
+
var html = "";
|
|
2373
|
+
var live = !!detail.live && !detail.died && !detail.ended;
|
|
2374
|
+
if(live){
|
|
2375
|
+
if(detail.stopping || stopAsked[detail.runid]){
|
|
2376
|
+
html = '<span class="stopping" data-testid="stopping-note">stopping after this turn…</span>';
|
|
2377
|
+
} else {
|
|
2378
|
+
html = '<button class="btn" type="button" data-testid="btn-stop">Stop</button>';
|
|
2379
|
+
}
|
|
2380
|
+
} else {
|
|
2381
|
+
delete stopAsked[detail.runid];
|
|
2382
|
+
var canResume = !!detail.resume && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "resume"; });
|
|
2383
|
+
if(canResume) html = '<button class="btn primary" type="button" data-testid="btn-resume-run">Resume</button>';
|
|
2384
|
+
}
|
|
2385
|
+
// Reuse workflow: on a GREEN run only (the server's Ended block says so) — a new run of the same signed job
|
|
2386
|
+
if(!live && !detail.died && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "reuse"; })){
|
|
2387
|
+
html += '<button class="btn" type="button" data-testid="btn-reuse">Reuse workflow</button>';
|
|
2388
|
+
}
|
|
2389
|
+
bar.innerHTML = html;
|
|
2390
|
+
bar.hidden = html === "";
|
|
2391
|
+
var reuseBtn = bar.querySelector('[data-testid="btn-reuse"]');
|
|
2392
|
+
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid); });
|
|
2393
|
+
var stopBtn = bar.querySelector('[data-testid="btn-stop"]');
|
|
2394
|
+
if(stopBtn){
|
|
2395
|
+
stopBtn.addEventListener("click", function(){
|
|
2396
|
+
stopBtn.disabled = true;
|
|
2397
|
+
authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/stop", {}).then(function(res){
|
|
2398
|
+
if(res.body && res.body.ok){ stopAsked[detail.runid] = true; renderRunActions(detail); }
|
|
2399
|
+
else { stopBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="stop-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>'); }
|
|
2400
|
+
}).catch(function(x){ stopBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="stop-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2404
|
+
if(resBtn) resBtn.addEventListener("click", function(){ openResumeOnJobTab(detail); });
|
|
2405
|
+
}
|
|
2406
|
+
function openResumeOnJobTab(detail){
|
|
2407
|
+
var r = detail.resume || {};
|
|
2408
|
+
resumeMode = {
|
|
2409
|
+
runid: detail.runid, spentUsd: r.spentUsd, spendComplete: r.spendComplete,
|
|
2410
|
+
wallUsedMs: r.wallUsedMs,
|
|
2411
|
+
budget: (r.budgetUsd === null || r.budgetUsd === undefined) ? "" : String(r.budgetUsd),
|
|
2412
|
+
wall: typeof r.maxWallMin === "number" ? String(r.maxWallMin) : null,
|
|
2413
|
+
painted: false,
|
|
2414
|
+
};
|
|
2415
|
+
document.getElementById("tab-details").click();
|
|
2416
|
+
paintResumeMode();
|
|
2417
|
+
}
|
|
2418
|
+
// Turns the Job tab's two cap values into inputs (and shows the Sign box) while a resume is open on this run;
|
|
2419
|
+
// called again after every Job re-render so a re-render never drops the typed numbers.
|
|
2420
|
+
function paintResumeMode(){
|
|
2421
|
+
var box = document.getElementById("resume-job");
|
|
2422
|
+
var moneyLabel = document.getElementById("details-cap-money-label");
|
|
2423
|
+
var timeLabel = document.getElementById("details-cap-time-label");
|
|
2424
|
+
if(!resumeMode || resumeMode.runid !== currentRunid){
|
|
2425
|
+
box.hidden = true; box.innerHTML = "";
|
|
2426
|
+
moneyLabel.textContent = "$ cap"; timeLabel.textContent = "Time cap";
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
var m = resumeMode;
|
|
2430
|
+
// the unit lives in the caption, never as loose text around the input
|
|
2431
|
+
moneyLabel.textContent = "Money cap ($)";
|
|
2432
|
+
timeLabel.textContent = "Time cap (min)";
|
|
2433
|
+
document.getElementById("details-cap-money").innerHTML =
|
|
2434
|
+
'<span class="resume-cap"><input type="number" min="0" step="0.01" id="resume-budget" data-testid="resume-budget" value="' + escapeXml(m.budget) + '"></span>';
|
|
2435
|
+
if(m.wall !== null){
|
|
2436
|
+
document.getElementById("details-cap-time").innerHTML =
|
|
2437
|
+
'<span class="resume-cap"><input type="number" min="1" step="1" id="resume-wall" data-testid="resume-wall" value="' + escapeXml(m.wall) + '"></span>';
|
|
2438
|
+
}
|
|
2439
|
+
var budgetEl = document.getElementById("resume-budget");
|
|
2440
|
+
budgetEl.addEventListener("input", function(){ m.budget = budgetEl.value; });
|
|
2441
|
+
var wallEl = document.getElementById("resume-wall");
|
|
2442
|
+
if(wallEl) wallEl.addEventListener("input", function(){ m.wall = wallEl.value; });
|
|
2443
|
+
box.hidden = false;
|
|
2444
|
+
if(m.painted) return;
|
|
2445
|
+
m.painted = true;
|
|
2446
|
+
var spent = (m.spentUsd === null || m.spentUsd === undefined) ? "unknown" : ((m.spendComplete ? "" : "at least ") + panelMoney(m.spentUsd));
|
|
2447
|
+
box.innerHTML =
|
|
2448
|
+
'<div data-testid="resume-intro">Resume run ' + escapeXml(m.runid) + ' — the same run, not a new one. Only the money cap and the time cap can change; everything else is the signed job.</div>' +
|
|
2449
|
+
'<div class="hint" data-testid="resume-spent">spent so far ' + escapeXml(spent) +
|
|
2450
|
+
(typeof m.wallUsedMs === "number" && isFinite(m.wallUsedMs) ? ' · time used so far ' + Math.ceil(m.wallUsedMs / 60000) + ' min' : '') + '</div>' +
|
|
2451
|
+
'<div class="ended-actions"><button class="btn primary" type="button" data-testid="btn-sign-resume">Sign & resume</button> ' +
|
|
2452
|
+
'<button class="btn" type="button" data-testid="btn-resume-cancel">Cancel</button></div>' +
|
|
2453
|
+
'<div class="resume-err" id="resume-err" data-testid="resume-err" hidden></div>';
|
|
2454
|
+
var resumeSignBtn = box.querySelector('[data-testid="btn-sign-resume"]');
|
|
2455
|
+
box.querySelector('[data-testid="btn-resume-cancel"]').addEventListener("click", function(){
|
|
2456
|
+
resumeMode = null;
|
|
2457
|
+
renderJob(lastJobShown);
|
|
2458
|
+
});
|
|
2459
|
+
resumeSignBtn.addEventListener("click", function(){
|
|
2460
|
+
var err = document.getElementById("resume-err");
|
|
2461
|
+
err.hidden = true;
|
|
2462
|
+
resumeSignBtn.disabled = true;
|
|
2463
|
+
var body = {budgetUsd: document.getElementById("resume-budget").value};
|
|
2464
|
+
var wall = document.getElementById("resume-wall");
|
|
2465
|
+
if(wall){ body.maxWallMin = wall.value; }
|
|
2466
|
+
return authorPost("/api/runs/" + encodeURIComponent(m.runid) + "/resume", body).then(function(res){
|
|
2467
|
+
if(res.body && res.body.ok){
|
|
2468
|
+
// the SAME run goes on: back to its Run tab, which follows it until it is live again
|
|
2469
|
+
resumeMode = null;
|
|
2470
|
+
renderJob(lastJobShown);
|
|
2471
|
+
document.getElementById("tab-run").click();
|
|
2472
|
+
afterResumeRefresh(m.runid, 8);
|
|
2473
|
+
} else {
|
|
2474
|
+
resumeSignBtn.disabled = false;
|
|
2475
|
+
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
2476
|
+
err.hidden = false;
|
|
2477
|
+
}
|
|
2478
|
+
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
// After a resume starts, the open run's own page follows it: re-select once the run reads live again (the spine's
|
|
2482
|
+
// new leg appears a moment after the engine starts), a few tries, never forever.
|
|
2483
|
+
function afterResumeRefresh(runid, tries){
|
|
2484
|
+
refreshRunsList(true);
|
|
2485
|
+
setTimeout(function(){
|
|
2486
|
+
if(currentRunid !== runid) return;
|
|
2487
|
+
getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(d){
|
|
2488
|
+
if(currentRunid !== runid) return;
|
|
2489
|
+
if(d.glyph === "▶" && !d.died){ selectRun(runid, null, ".hist-row"); return; }
|
|
2490
|
+
if(tries > 0) afterResumeRefresh(runid, tries - 1);
|
|
2491
|
+
}).catch(function(){ if(tries > 0) afterResumeRefresh(runid, tries - 1); });
|
|
2492
|
+
}, 1500);
|
|
2493
|
+
}
|
|
1866
2494
|
|
|
2495
|
+
function renderRun(detail){
|
|
1867
2496
|
if(detail.fileMissing){
|
|
1868
|
-
|
|
1869
|
-
dot.className = "dot grey";
|
|
1870
|
-
verdict.textContent = "unknown";
|
|
1871
|
-
date.textContent = "file missing";
|
|
2497
|
+
setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
|
|
1872
2498
|
document.getElementById("run-empty").hidden = false;
|
|
1873
2499
|
document.getElementById("run-empty").textContent = "spine file no longer on disk";
|
|
1874
2500
|
document.getElementById("run-content").hidden = true;
|
|
1875
2501
|
document.getElementById("audit-select-empty").hidden = false;
|
|
2502
|
+
document.getElementById("audit-select-empty").textContent = "select a run first";
|
|
1876
2503
|
document.getElementById("audit-body").hidden = true;
|
|
1877
2504
|
return;
|
|
1878
2505
|
}
|
|
1879
2506
|
|
|
2507
|
+
if(detail.starting){
|
|
2508
|
+
setRunHeader("\u25B6", runName(detail.job, detail.runNo, detail.runid), statusWordHtml(detail.status, "starting\u2026") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
|
|
2509
|
+
document.getElementById("run-empty").hidden = false;
|
|
2510
|
+
document.getElementById("run-empty").textContent = "starting… the run has not written its log yet";
|
|
2511
|
+
document.getElementById("run-content").hidden = true;
|
|
2512
|
+
document.getElementById("audit-select-empty").hidden = false;
|
|
2513
|
+
document.getElementById("audit-select-empty").textContent = "starting… the run has not written its log yet";
|
|
2514
|
+
document.getElementById("audit-body").hidden = true;
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
1880
2517
|
document.getElementById("audit-select-empty").hidden = true;
|
|
1881
2518
|
document.getElementById("audit-body").hidden = false;
|
|
2519
|
+
renderEnded(detail);
|
|
1882
2520
|
|
|
1883
|
-
|
|
1884
|
-
dot.className = "dot " + glyphClass(detail.glyph);
|
|
1885
|
-
verdict.textContent = detail.checkType;
|
|
1886
|
-
if(detail.checkTypeTitle){ verdict.title = detail.checkTypeTitle; } else { verdict.removeAttribute("title"); }
|
|
1887
|
-
date.textContent = detail.date || "unknown";
|
|
2521
|
+
setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), runHeaderBody(detail));
|
|
1888
2522
|
|
|
1889
2523
|
document.getElementById("run-empty").hidden = true;
|
|
1890
2524
|
document.getElementById("run-content").hidden = false;
|
|
2525
|
+
renderRunActions(detail);
|
|
1891
2526
|
|
|
1892
2527
|
// died: the two REAL figures the panel has (spentUsd/wallMs, both null —
|
|
1893
2528
|
// no job-end means no final totals were ever recorded) are replaced with
|
|
@@ -1904,11 +2539,12 @@
|
|
|
1904
2539
|
// false and spentUsd is still null) shows the RUNNING floor instead of
|
|
1905
2540
|
// "unknown" — the server now derives the same spendFloorUsd/wallFloorMs
|
|
1906
2541
|
// for a still-running spine that it already derived for a died one.
|
|
1907
|
-
var isLiveNoEnd = !detail.died && detail.spentUsd === null;
|
|
1908
|
-
var spendText, wallPhrase;
|
|
2542
|
+
var isLiveNoEnd = !detail.died && detail.spentUsd === null && detail.fromBridge !== true;
|
|
2543
|
+
var spendText, wallPhrase, wallText;
|
|
1909
2544
|
if(isLiveNoEnd){
|
|
1910
2545
|
spendText = liveSpendText(detail.spendFloorUsd, detail.draftSpentUsd, detail.draftSpendComplete);
|
|
1911
2546
|
wallPhrase = liveWallPhrase(detail.wallFloorMs);
|
|
2547
|
+
wallText = wallPhrase; // the summary headline reads wallText in BOTH branches
|
|
1912
2548
|
} else {
|
|
1913
2549
|
var draftIncomplete = typeof detail.draftSpentUsd === "number" && detail.draftSpentUsd !== null && detail.draftSpendComplete === false;
|
|
1914
2550
|
var spendCoreVal = detail.died ? detail.spendFloorUsd : detail.spentUsd;
|
|
@@ -1921,10 +2557,16 @@
|
|
|
1921
2557
|
// still in progress: a live [▶] run (glyph "▶" and not died).
|
|
1922
2558
|
var isLive = detail.glyph === "▶" && !detail.died;
|
|
1923
2559
|
wallPhrase = isLive ? (wallText + " elapsed") : ("took " + wallText);
|
|
2560
|
+
// an imported job's green run read from its bridge: only what the bridge carries; the rest says so
|
|
2561
|
+
if(detail.fromBridge === true){
|
|
2562
|
+
spendText = typeof detail.spentUsd === "number" ? ((detail.spendComplete === false ? "at least " : "") + panelMoney(detail.spentUsd)) : "not recorded";
|
|
2563
|
+
wallText = typeof detail.wallMs === "number" ? duration(detail.wallMs) : "not recorded";
|
|
2564
|
+
wallPhrase = typeof detail.wallMs === "number" ? ("took " + wallText) : "time not recorded";
|
|
2565
|
+
}
|
|
1924
2566
|
}
|
|
1925
2567
|
|
|
1926
2568
|
document.getElementById("run-counters").innerHTML =
|
|
1927
|
-
spendText + " of " + panelMoney(detail.budgetUsd) +
|
|
2569
|
+
spendText + ((detail.fromBridge === true && typeof detail.budgetUsd !== "number") ? "" : (" of " + panelMoney(detail.budgetUsd))) +
|
|
1928
2570
|
" · " + wallPhrase;
|
|
1929
2571
|
|
|
1930
2572
|
var box = document.getElementById("run-summary");
|
|
@@ -1935,9 +2577,9 @@
|
|
|
1935
2577
|
// "0 of 1 done".
|
|
1936
2578
|
var rSteps = realSteps(detail.steps);
|
|
1937
2579
|
var stepsDone = rSteps.filter(function(s){ return s.state === "done"; }).length;
|
|
1938
|
-
var stepsText = rSteps.length === 0 ? "none started" : ('<span class="num">' + stepsDone + '</span> of <span class="num">' + rSteps.length + '</span> done');
|
|
2580
|
+
var stepsText = rSteps.length === 0 ? (detail.fromBridge === true ? "not recorded" : "none started") : ('<span class="num">' + stepsDone + '</span> of <span class="num">' + rSteps.length + '</span> done');
|
|
1939
2581
|
box.innerHTML =
|
|
1940
|
-
'<div class="summary-headline"><span class="dot ' + glyphClass(detail.glyph) + '"></span
|
|
2582
|
+
'<div class="summary-headline"><span class="dot ' + glyphClass(detail.glyph) + '"></span>' + (detail.status && detail.status.word ? '<b class="st-word" data-testid="summary-status">' + escapeXml(detail.status.word) + '</b> · ' : '') + '<b>' + escapeXml(detail.checkType) + '</b> · ' +
|
|
1941
2583
|
'<span class="num">' + spendText + '</span> · <span class="num">' + wallText + '</span></div>' +
|
|
1942
2584
|
'<div class="summary-row" data-testid="summary-model"><span class="sum-label">model:</span><span class="sum-val">' + escapeXml(modelLine(detail)) + '</span></div>' +
|
|
1943
2585
|
(detail.judgeModel ? '<div class="summary-row" data-testid="summary-judge"><span class="sum-label">judge:</span><span class="sum-val">' + escapeXml(detail.judgeModel) + '</span></div>' : '') +
|
|
@@ -1945,7 +2587,7 @@
|
|
|
1945
2587
|
(detail.replans ? ' · ' + detail.replans + ' replan' + (detail.replans === 1 ? '' : 's') : '') + '</span></div>' +
|
|
1946
2588
|
'<div class="summary-row"><span class="sum-label">why:</span><span class="sum-val">' + escapeXml(detail.stopReason || (detail.glyph === "✓" ? "run ended clean" : "not recorded")) + '</span></div>' +
|
|
1947
2589
|
'<div class="summary-row"><span class="sum-label">close:</span><span class="sum-val">' + escapeXml(detail.close && detail.close.verdict ? detail.close.verdict : "none — the run ended before any close ran") + '</span></div>' +
|
|
1948
|
-
'<div class="summary-row" data-testid="summary-tools"><span class="sum-label">tools:</span><span class="sum-val">' + escapeXml(toolsLine(detail.behaviour)) + '</span></div>' +
|
|
2590
|
+
'<div class="summary-row" data-testid="summary-tools"><span class="sum-label">tools:</span><span class="sum-val">' + escapeXml(detail.fromBridge === true ? bridgeToolsLine(detail.toolsUsed) : toolsLine(detail.behaviour)) + '</span></div>' +
|
|
1949
2591
|
'<div class="summary-row" data-testid="summary-offered" id="summary-offered-row" hidden><span class="sum-label">offered:</span><span class="sum-val" id="summary-offered-val"></span></div>' +
|
|
1950
2592
|
'<div class="summary-row" data-testid="summary-cache"><span class="sum-label">cache:</span><span class="sum-val">' + escapeXml(cacheLine(detail.memoryCache)) + '</span></div>';
|
|
1951
2593
|
|
|
@@ -1968,7 +2610,6 @@
|
|
|
1968
2610
|
listEl.innerHTML = "";
|
|
1969
2611
|
orderedBoxes.forEach(function(box, idx){
|
|
1970
2612
|
var part = box.part;
|
|
1971
|
-
var badgeClass = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
1972
2613
|
var titleText = box.kind === "step" ? ((cardStepNumbers[idx] + 1) + ' · ' + escapeXml(box.title)) : escapeXml(box.title);
|
|
1973
2614
|
var resultGlyph = partResultGlyph(part, box);
|
|
1974
2615
|
// build item 4: ONE owner (partLine1Text, shared with the Audit tab's
|
|
@@ -1983,11 +2624,11 @@
|
|
|
1983
2624
|
card.setAttribute("role", "button");
|
|
1984
2625
|
card.innerHTML =
|
|
1985
2626
|
'<div class="step-head"><h4>' + titleText + '</h4>' +
|
|
1986
|
-
|
|
2627
|
+
stepStateHTML(box, 'part-state-' + idx) +
|
|
1987
2628
|
(box.blocked > 0 ? '<span class="badge red" data-testid="part-blocked-badge">' + box.blocked + ' blocked</span>' : '') +
|
|
1988
2629
|
'</div>' +
|
|
1989
2630
|
'<div class="step-meta" title="a part is one piece of the run (scout, plan, a step, replan, fix, judge); a call is one model round">' + line1Bits.join(" · ") + '</div>' +
|
|
1990
|
-
'<div class="step-meta">' + escapeXml(
|
|
2631
|
+
'<div class="step-meta">' + escapeXml(partLine2Text(box.part)) + '</div>';
|
|
1991
2632
|
listEl.appendChild(card);
|
|
1992
2633
|
var jumpToAudit = function(){
|
|
1993
2634
|
document.getElementById("tab-audit").click();
|
|
@@ -2002,9 +2643,15 @@
|
|
|
2002
2643
|
// for the map+cards, never a second independently-fetched data source
|
|
2003
2644
|
// (the flat one-row-per-call table still comes from its own `/audit`
|
|
2004
2645
|
// fetch, wired separately in selectRun below).
|
|
2646
|
+
lastLegDividers = Array.isArray(detail.legDividers) ? detail.legDividers : [];
|
|
2005
2647
|
renderAuditGroups(detail);
|
|
2006
2648
|
}
|
|
2007
2649
|
|
|
2650
|
+
// ONE spelling of a step's state: its dot + the bold state word — read by the Run tab's step cards AND the Audit tab's grouped rows
|
|
2651
|
+
function stepStateHTML(box, testid){
|
|
2652
|
+
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2653
|
+
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(box.state) + '</b></span>';
|
|
2654
|
+
}
|
|
2008
2655
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
2009
2656
|
// lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
|
|
2010
2657
|
function buildAttemptsList(container, partIndex, attempts, boxIsLive){
|
|
@@ -2142,6 +2789,46 @@
|
|
|
2142
2789
|
// shows — expand -> attempts (parts with >1, e.g. a multi-attempt step or
|
|
2143
2790
|
// the fix loop) or straight to a rounds table (every other part kind,
|
|
2144
2791
|
// which carries exactly one synthetic whole-part attempt).
|
|
2792
|
+
// The DRAFTING part (a panel-authored run's first part; src/draftspend.js `draftingPartFor`): ONE owner of its
|
|
2793
|
+
// money wording, line 1, line 2 and its Audit table. Money is the spine's own drafting figure ("at least" when it
|
|
2794
|
+
// is a floor); a call with no price reads "unknown", never $0.
|
|
2795
|
+
function draftingMoneyText(part){
|
|
2796
|
+
return (part.spendComplete === false ? "at least " : "") + panelMoney(part.spentUsd);
|
|
2797
|
+
}
|
|
2798
|
+
function draftingLine1Text(part){
|
|
2799
|
+
var n = typeof part.rounds === "number" ? part.rounds : 0;
|
|
2800
|
+
return (part.wallMs === null || part.wallMs === undefined ? "–" : duration(part.wallMs)) + " · " + draftingMoneyText(part) + " · " + n + " call" + (n === 1 ? "" : "s");
|
|
2801
|
+
}
|
|
2802
|
+
// line 2 of a part's card / Audit body: the drafting step names, or a worker part's tool breakdown
|
|
2803
|
+
function partLine2Text(part){
|
|
2804
|
+
if(part && part.kind === "drafting") return (part.draftSteps || []).map(function(x){ return x.label; }).join(" · ");
|
|
2805
|
+
return toolBreakdownLine(part.byTool);
|
|
2806
|
+
}
|
|
2807
|
+
function draftingCallCost(c){
|
|
2808
|
+
if(c.costUsd === null || c.costUsd === undefined) return "unknown";
|
|
2809
|
+
if(c.unpricedRounds > 0) return "unknown (" + c.unpricedRounds + " unpriced round" + (c.unpricedRounds === 1 ? "" : "s") + ")";
|
|
2810
|
+
return panelMoney(c.costUsd);
|
|
2811
|
+
}
|
|
2812
|
+
// one row per drafting step ("–" where it made no call); a step's further calls get their own row named by the
|
|
2813
|
+
// call's own label (confirm#2, revise-1…); the last row is the total, equal to the header's money
|
|
2814
|
+
function draftingTableHtml(part){
|
|
2815
|
+
var rows = "";
|
|
2816
|
+
(part.draftSteps || []).forEach(function(st){
|
|
2817
|
+
var wall = st.wallMs === null || st.wallMs === undefined ? "–" : duration(st.wallMs);
|
|
2818
|
+
var calls = Array.isArray(st.calls) ? st.calls : [];
|
|
2819
|
+
var first = calls[0];
|
|
2820
|
+
rows += "<tr><td>" + escapeXml(st.label) + "</td><td>" + wall + "</td><td>" + (first ? "1" : "–") + "</td><td>" +
|
|
2821
|
+
(first && first.model ? escapeXml(first.model) : "–") + "</td><td>" + (first ? draftingCallCost(first) : "–") + "</td></tr>";
|
|
2822
|
+
calls.slice(1).forEach(function(c){
|
|
2823
|
+
rows += "<tr><td>" + escapeXml(st.label) + " (" + escapeXml(c.label) + ")</td><td>–</td><td>1</td><td>" +
|
|
2824
|
+
(c.model ? escapeXml(c.model) : "–") + "</td><td>" + draftingCallCost(c) + "</td></tr>";
|
|
2825
|
+
});
|
|
2826
|
+
});
|
|
2827
|
+
rows += '<tr class="drafting-total"><td><b>total</b></td><td>' + (part.wallMs === null || part.wallMs === undefined ? "–" : duration(part.wallMs)) +
|
|
2828
|
+
"</td><td>" + (typeof part.rounds === "number" ? part.rounds : 0) + "</td><td>–</td><td>" + draftingMoneyText(part) + "</td></tr>";
|
|
2829
|
+
return '<div class="audit-table-scroll"><table class="rounds-table drafting-table" data-testid="drafting-table">' +
|
|
2830
|
+
"<thead><tr><th>Step</th><th>Time</th><th>Calls</th><th>Model</th><th>Cost</th></tr></thead><tbody>" + rows + "</tbody></table></div>";
|
|
2831
|
+
}
|
|
2145
2832
|
function partHasWrites(part){
|
|
2146
2833
|
var bt = part.byTool;
|
|
2147
2834
|
if(!bt) return false;
|
|
@@ -2171,6 +2858,7 @@
|
|
|
2171
2858
|
* @returns {string}
|
|
2172
2859
|
*/
|
|
2173
2860
|
function partLine1Text(part, detail){
|
|
2861
|
+
if(part && part.kind === "drafting") return draftingLine1Text(part);
|
|
2174
2862
|
var isLiveRunPart = part && part.kind === "run" && detail && !detail.died && detail.spentUsd === null;
|
|
2175
2863
|
var wallText = (isLiveRunPart && part.wallMs === null) ? liveWallPhrase(detail.wallFloorMs) : duration(part.wallMs);
|
|
2176
2864
|
var spendText = (isLiveRunPart && part.spentUsd === null)
|
|
@@ -2189,6 +2877,8 @@
|
|
|
2189
2877
|
var latestAttempt = Array.isArray(part.attempts) && part.attempts.length ? part.attempts[part.attempts.length - 1] : null;
|
|
2190
2878
|
var headline = latestAttempt ? attemptChecksLine(latestAttempt) : "";
|
|
2191
2879
|
if(headline) bits.push(headline);
|
|
2880
|
+
// P5-R: a part its leg's halt cut off says why, in words
|
|
2881
|
+
if(part && part.stopReason) bits.push("stopped — " + escapeXml(part.stopReason));
|
|
2192
2882
|
return bits.join(" · ");
|
|
2193
2883
|
}
|
|
2194
2884
|
/**
|
|
@@ -2355,10 +3045,12 @@
|
|
|
2355
3045
|
var isLiveRun = detail.glyph === "▶" && !detail.died;
|
|
2356
3046
|
var boxes = buildOrderedBoxes(parts, isLiveRun);
|
|
2357
3047
|
var numberMap = stepNumberIndices(boxes);
|
|
3048
|
+
// P5-R: a resumed run is ONE run — its legs are told apart by a divider line (code-owned text from the server)
|
|
3049
|
+
var dividers = Array.isArray(detail.legDividers) ? detail.legDividers : [];
|
|
2358
3050
|
boxes.forEach(function(box, idx){
|
|
3051
|
+
dividers.forEach(function(dv){ if(dv.beforePart === idx) container.appendChild(legDividerEl(dv)); });
|
|
2359
3052
|
var part = box.part;
|
|
2360
3053
|
var titleText = box.kind === "step" ? ((numberMap[idx] + 1) + ' · ' + escapeXml(box.title)) : escapeXml(box.title);
|
|
2361
|
-
var badgeClass = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2362
3054
|
var rowWrap = document.createElement("div");
|
|
2363
3055
|
rowWrap.className = "audit-part";
|
|
2364
3056
|
rowWrap.setAttribute("data-testid", "audit-part-" + idx);
|
|
@@ -2372,7 +3064,7 @@
|
|
|
2372
3064
|
headBtn.setAttribute("data-testid", "audit-part-toggle-" + idx);
|
|
2373
3065
|
var groupResultGlyph = partResultGlyph(part, box);
|
|
2374
3066
|
headBtn.innerHTML =
|
|
2375
|
-
|
|
3067
|
+
stepStateHTML(box, 'audit-part-state-' + idx) + ' ' + titleText + ' · ' + partLine1Text(part, detail) +
|
|
2376
3068
|
(groupResultGlyph !== null ? ' · ' + groupResultGlyph : '') +
|
|
2377
3069
|
(box.blocked > 0 ? ' <span class="badge red" data-testid="audit-part-blocked-' + idx + '">' + box.blocked + ' blocked</span>' : '');
|
|
2378
3070
|
var body = document.createElement("div");
|
|
@@ -2385,9 +3077,15 @@
|
|
|
2385
3077
|
built = true;
|
|
2386
3078
|
var toolsHeader = document.createElement("div");
|
|
2387
3079
|
toolsHeader.className = "hint";
|
|
2388
|
-
toolsHeader.textContent =
|
|
3080
|
+
toolsHeader.textContent = partLine2Text(part);
|
|
2389
3081
|
body.appendChild(toolsHeader);
|
|
2390
|
-
if(
|
|
3082
|
+
if(part.kind === "drafting"){
|
|
3083
|
+
// no tool calls and no rounds: one row per drafting step instead (the drafting part is index 0 of the
|
|
3084
|
+
// shown list; the rounds endpoint has nothing to serve for it)
|
|
3085
|
+
var draftingEl = document.createElement("div");
|
|
3086
|
+
draftingEl.innerHTML = draftingTableHtml(part);
|
|
3087
|
+
body.appendChild(draftingEl);
|
|
3088
|
+
} else if(attempts.length > 1){
|
|
2391
3089
|
// parts WITH an attempt sub-loop (a multi-attempt step, or the fix
|
|
2392
3090
|
// loop) expand to attempt rows first — each attempt expands to its
|
|
2393
3091
|
// own rounds table on its own click, same as the Run tab used to.
|
|
@@ -2415,8 +3113,17 @@
|
|
|
2415
3113
|
container.appendChild(rowWrap);
|
|
2416
3114
|
if(startExpanded && !built) buildBody(); // a live run's currently-running part opens by default
|
|
2417
3115
|
});
|
|
3116
|
+
// a leg that has not started a part yet (or died before one) closes the list
|
|
3117
|
+
dividers.forEach(function(dv){ if(dv.beforePart >= boxes.length) container.appendChild(legDividerEl(dv)); });
|
|
2418
3118
|
applyAuditFilter(currentAuditFilter);
|
|
2419
3119
|
}
|
|
3120
|
+
function legDividerEl(dv){
|
|
3121
|
+
var d = document.createElement("div");
|
|
3122
|
+
d.className = "audit-leg-divider";
|
|
3123
|
+
d.setAttribute("data-testid", "audit-leg-divider-" + dv.leg);
|
|
3124
|
+
d.textContent = "\u2500\u2500 " + dv.text + " \u2500\u2500";
|
|
3125
|
+
return d;
|
|
3126
|
+
}
|
|
2420
3127
|
// build item B: a Run-tab card (or map box) click switches to the Audit
|
|
2421
3128
|
// tab, that same part's row expanded and scrolled into view — sharing
|
|
2422
3129
|
// partIndex so the two tabs can never disagree about which row a click maps to.
|
|
@@ -2439,6 +3146,9 @@
|
|
|
2439
3146
|
|
|
2440
3147
|
// ---- Audit tab: FLAT view (the old one-row-per-call time-ordered table) ----
|
|
2441
3148
|
var auditRowEls = [];
|
|
3149
|
+
// the open run's leg dividers (set by renderRun); the flat table places them by time
|
|
3150
|
+
var lastLegDividers = [];
|
|
3151
|
+
function dividerAtMs(dv){ var t = Date.parse(dv.at); return Number.isFinite(t) ? t : Infinity; }
|
|
2442
3152
|
function renderAudit(result){
|
|
2443
3153
|
var empty = document.getElementById("audit-empty");
|
|
2444
3154
|
var content = document.getElementById("audit-content");
|
|
@@ -2455,7 +3165,18 @@
|
|
|
2455
3165
|
content.hidden = false;
|
|
2456
3166
|
var tbody = document.getElementById("audit-tbody");
|
|
2457
3167
|
tbody.innerHTML = "";
|
|
3168
|
+
// P5-R: the flat table carries the same leg dividers, placed by time (a divider sits before the first call
|
|
3169
|
+
// made at or after the moment that leg picked up; any that remain close the table)
|
|
3170
|
+
var pendingDividers = Array.isArray(lastLegDividers) ? lastLegDividers.slice() : [];
|
|
3171
|
+
function flatDividerRow(dv){
|
|
3172
|
+
var dividerRow = document.createElement("tr");
|
|
3173
|
+
dividerRow.className = "audit-leg-divider-row";
|
|
3174
|
+
dividerRow.setAttribute("data-testid", "audit-leg-divider-row-" + dv.leg);
|
|
3175
|
+
dividerRow.innerHTML = '<td colspan="6">\u2500\u2500 ' + escapeXml(dv.text) + ' \u2500\u2500</td>';
|
|
3176
|
+
return dividerRow;
|
|
3177
|
+
}
|
|
2458
3178
|
result.rows.forEach(function(r){
|
|
3179
|
+
while(pendingDividers.length && dividerAtMs(pendingDividers[0]) <= Date.parse(r.time)){ tbody.appendChild(flatDividerRow(pendingDividers.shift())); }
|
|
2459
3180
|
var tr = document.createElement("tr");
|
|
2460
3181
|
var rowCls = auditRowClassName(r);
|
|
2461
3182
|
if(rowCls) tr.className = rowCls;
|
|
@@ -2486,8 +3207,9 @@
|
|
|
2486
3207
|
"<td>" + auditTimeCellHtml(r) + "</td>";
|
|
2487
3208
|
tbody.appendChild(tr);
|
|
2488
3209
|
});
|
|
3210
|
+
pendingDividers.forEach(function(dv){ tbody.appendChild(flatDividerRow(dv)); });
|
|
2489
3211
|
document.getElementById("audit-rawlog").textContent = result.raw || "";
|
|
2490
|
-
auditRowEls = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
|
|
3212
|
+
auditRowEls = Array.prototype.slice.call(tbody.querySelectorAll("tr:not(.audit-leg-divider-row)"));
|
|
2491
3213
|
// Scoped to [data-filter] — the audit tab's OWN chips (chip-all/writes/
|
|
2492
3214
|
// blocked). A bare ".chip" here also matched the History filter bar's
|
|
2493
3215
|
// chips (same class, different data- attributes), binding a second
|
|
@@ -2517,6 +3239,7 @@
|
|
|
2517
3239
|
function renderJob(job){
|
|
2518
3240
|
var empty = document.getElementById("job-empty");
|
|
2519
3241
|
var card = document.getElementById("job-card-readonly");
|
|
3242
|
+
lastJobShown = job;
|
|
2520
3243
|
if(!job){ empty.hidden = false; card.hidden = true; return; }
|
|
2521
3244
|
empty.hidden = true;
|
|
2522
3245
|
card.hidden = false;
|
|
@@ -2539,6 +3262,7 @@
|
|
|
2539
3262
|
document.getElementById("job-note").textContent = job.note || "";
|
|
2540
3263
|
lastJobToolsList = Array.isArray(job.toolsList) ? job.toolsList : null;
|
|
2541
3264
|
paintOfferedRow();
|
|
3265
|
+
paintResumeMode();
|
|
2542
3266
|
}
|
|
2543
3267
|
|
|
2544
3268
|
// ---- selection + loading ----
|
|
@@ -2553,10 +3277,13 @@
|
|
|
2553
3277
|
// uses for `isLive` — never re-derived a second way).
|
|
2554
3278
|
var runIsLive = false;
|
|
2555
3279
|
function selectRun(runid, rowEl, siblingSelector){
|
|
3280
|
+
hideImportView();
|
|
2556
3281
|
clearSelection();
|
|
2557
3282
|
document.querySelectorAll(siblingSelector).forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
2558
3283
|
if(rowEl){ rowEl.classList.add("selected"); rowEl.setAttribute("aria-pressed","true"); }
|
|
2559
3284
|
currentRunid = runid;
|
|
3285
|
+
resumeMode = null;
|
|
3286
|
+
lastLegDividers = [];
|
|
2560
3287
|
lastJobToolsList = null;
|
|
2561
3288
|
lastRunDetailSig = null;
|
|
2562
3289
|
runIsLive = false;
|
|
@@ -2818,9 +3545,9 @@
|
|
|
2818
3545
|
}
|
|
2819
3546
|
function loadProviders(){
|
|
2820
3547
|
authorGet("/api/settings/providers").then(function(r){
|
|
2821
|
-
if(!r || !r.ok){ pvNote.textContent = "could not read providers"; return; }
|
|
3548
|
+
if(!r || !r.ok){ pvNote.textContent = "could not read providers"; document.getElementById("pv-keyfile-path").textContent = "the keys file (path unknown — could not read providers)"; return; }
|
|
2822
3549
|
var kf = r.keysFile;
|
|
2823
|
-
document.getElementById("pv-keyfile-path").textContent =
|
|
3550
|
+
document.getElementById("pv-keyfile-path").textContent = "";
|
|
2824
3551
|
document.getElementById("pv-keyfile-note").textContent = kf.warning ? ("⚠ " + kf.warning) : "";
|
|
2825
3552
|
pvRows.innerHTML = r.rows.length ? r.rows.map(function(row){
|
|
2826
3553
|
var e = escapeXml(row.envName);
|
|
@@ -2840,8 +3567,10 @@
|
|
|
2840
3567
|
? '<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>'
|
|
2841
3568
|
: '<span class="hint">not available</span>') + "</td>"
|
|
2842
3569
|
+ '<td data-testid="pv-tokens-' + e + '">' + escapeXml(tokensText(row.tokens)) + "</td>"
|
|
2843
|
-
+ "<td>" + bal + "</td
|
|
2844
|
-
|
|
3570
|
+
+ "<td>" + bal + "</td>"
|
|
3571
|
+
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-in" data-testid="pv-price-in-' + e + '" placeholder="not set" value="' + (row.priceInPerM === null ? "" : escapeXml(String(row.priceInPerM))) + '"></td>'
|
|
3572
|
+
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-out" data-testid="pv-price-out-' + e + '" placeholder="not set" value="' + (row.priceOutPerM === null ? "" : escapeXml(String(row.priceOutPerM))) + '"></td>';
|
|
3573
|
+
}).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>';
|
|
2845
3574
|
pvRows.querySelectorAll(".pv-balance-fetch").forEach(function(fetchEl){
|
|
2846
3575
|
authorGet("/api/settings/providers/balance?env=" + encodeURIComponent(fetchEl.getAttribute("data-env"))).then(function(b){
|
|
2847
3576
|
fetchEl.textContent = b && b.text ? b.text : ("— " + ((b && b.note) || "unavailable"));
|
|
@@ -2855,6 +3584,16 @@
|
|
|
2855
3584
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
2856
3585
|
}).catch(function(){ pvNote.textContent = "could not read providers"; });
|
|
2857
3586
|
}
|
|
3587
|
+
// The row's POST body. A price is both boxes or neither (the server refuses a half-set one), so while exactly one
|
|
3588
|
+
// is filled — the normal middle of typing a price — the price fields are LEFT OUT (the server keeps what is saved)
|
|
3589
|
+
// and the other boxes still save; the price posts once both are filled, or both are cleared.
|
|
3590
|
+
function pvRowBody(env, v){
|
|
3591
|
+
var pin = String(v.priceIn).trim();
|
|
3592
|
+
var pout = String(v.priceOut).trim();
|
|
3593
|
+
var body = { envName: env, name: v.name, shape: v.shape, baseUrl: v.baseUrl };
|
|
3594
|
+
if((pin === "") === (pout === "")){ body.priceInPerM = pin; body.priceOutPerM = pout; }
|
|
3595
|
+
return body;
|
|
3596
|
+
}
|
|
2858
3597
|
pvRows.addEventListener("change", function(e){
|
|
2859
3598
|
var t = e.target;
|
|
2860
3599
|
var tr = t.closest("tr");
|
|
@@ -2865,14 +3604,15 @@
|
|
|
2865
3604
|
}).catch(function(){ pvNote.textContent = "could not save"; });
|
|
2866
3605
|
return;
|
|
2867
3606
|
}
|
|
2868
|
-
if(!(t.classList.contains("pv-name") || t.classList.contains("pv-shape") || t.classList.contains("pv-url"))) return;
|
|
3607
|
+
if(!(t.classList.contains("pv-name") || t.classList.contains("pv-shape") || t.classList.contains("pv-url") || t.classList.contains("pv-price-in") || t.classList.contains("pv-price-out"))) return;
|
|
2869
3608
|
var env = tr.getAttribute("data-env");
|
|
2870
|
-
authorPost("/api/settings/providers/row", {
|
|
2871
|
-
envName: env,
|
|
3609
|
+
authorPost("/api/settings/providers/row", pvRowBody(env, {
|
|
2872
3610
|
name: tr.querySelector(".pv-name").value,
|
|
2873
3611
|
shape: tr.querySelector(".pv-shape").value,
|
|
2874
3612
|
baseUrl: tr.querySelector(".pv-url").value,
|
|
2875
|
-
|
|
3613
|
+
priceIn: tr.querySelector(".pv-price-in").value,
|
|
3614
|
+
priceOut: tr.querySelector(".pv-price-out").value,
|
|
3615
|
+
})).then(function(r){
|
|
2876
3616
|
pvNote.textContent = r.body && r.body.ok ? ("Saved — " + env + ".") : ((r.body && r.body.error) || "could not save");
|
|
2877
3617
|
// no re-render: it would steal focus mid-edit; Chat's Model menu just re-reads
|
|
2878
3618
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
@@ -2889,6 +3629,14 @@
|
|
|
2889
3629
|
}).catch(function(){ out.textContent = "failed — no answer"; }).then(function(){ t.disabled = false; });
|
|
2890
3630
|
});
|
|
2891
3631
|
document.getElementById("btn-reload-keys").addEventListener("click", loadProviders);
|
|
3632
|
+
// Open keys folder: the server opens the FOLDER in the file manager; if it cannot, the path shows as text
|
|
3633
|
+
document.getElementById("btn-open-keys-folder").addEventListener("click", function(){
|
|
3634
|
+
var out = document.getElementById("pv-keyfile-path");
|
|
3635
|
+
authorPost("/api/settings/open-keys-folder", {}).then(function(r){
|
|
3636
|
+
var b = r.body || {};
|
|
3637
|
+
out.textContent = b.ok && b.opened ? "" : ("Could not open it here. Your keys folder: " + (b.path || "(unknown)"));
|
|
3638
|
+
}).catch(function(){ out.textContent = "Could not open it here — the keys folder is ~/.config/bareloop"; });
|
|
3639
|
+
});
|
|
2892
3640
|
wireTabs([document.getElementById("tab-providers"), document.getElementById("tab-money")]);
|
|
2893
3641
|
document.getElementById("tab-providers").addEventListener("click", loadProviders);
|
|
2894
3642
|
// Auto-save on a human edit (change = blur or Enter): POST carries the per-start token; the chat has no route here
|
|
@@ -2910,24 +3658,21 @@
|
|
|
2910
3658
|
|
|
2911
3659
|
// ---- Chat tab: job-card authoring (PANEL-BUILD.md P3) ----
|
|
2912
3660
|
(function(){
|
|
2913
|
-
var
|
|
3661
|
+
var clearBtn = document.getElementById("chat-clear-btn");
|
|
2914
3662
|
var card = document.getElementById("job-card");
|
|
2915
3663
|
var thread = document.getElementById("chat-thread");
|
|
2916
3664
|
var msgInput = document.getElementById("chat-msg");
|
|
2917
|
-
var
|
|
2918
|
-
var
|
|
2919
|
-
var
|
|
2920
|
-
var
|
|
2921
|
-
var
|
|
3665
|
+
var mainBtn = document.getElementById("chat-main-btn");
|
|
3666
|
+
var mainHint = document.getElementById("chat-main-hint");
|
|
3667
|
+
var mainAction = "none";
|
|
3668
|
+
var lastState = null;
|
|
3669
|
+
var startOk = false;
|
|
2922
3670
|
var errEl = document.getElementById("chat-card-error");
|
|
2923
3671
|
var actionErrEl = document.getElementById("chat-action-error");
|
|
2924
|
-
var hashLine = document.getElementById("chat-hash-line");
|
|
2925
3672
|
var moneyCapInput = document.getElementById("jf-cap-money");
|
|
2926
3673
|
var modelSelect = document.getElementById("jf-model");
|
|
2927
3674
|
var modelStatusEl = document.getElementById("jf-model-status");
|
|
2928
3675
|
var progressRow = document.getElementById("chat-progress-row");
|
|
2929
|
-
var progressGlyph = document.getElementById("chat-progress-glyph");
|
|
2930
|
-
var progressLabelEl = document.getElementById("chat-progress-label");
|
|
2931
3676
|
|
|
2932
3677
|
var sessionId = null;
|
|
2933
3678
|
var pollTimer = null;
|
|
@@ -2939,26 +3684,98 @@
|
|
|
2939
3684
|
// src/panel/authorroutes.js, mirrored here so Start never even offers a
|
|
2940
3685
|
// click the server would 409 anyway).
|
|
2941
3686
|
var CLIENT_TERMINAL_PHASES = ["refused", "abandoned", "error", "signed", "signing-failed"];
|
|
2942
|
-
var DONE_PHASES = ["prepared", "signed"];
|
|
2943
3687
|
var sessionLive = false;
|
|
2944
3688
|
// build item 7 — Start also stays disabled while the chosen model's key
|
|
2945
3689
|
// isn't usable; updated by refreshModelStatus() below.
|
|
2946
3690
|
var keyOk = false;
|
|
2947
3691
|
|
|
2948
|
-
|
|
3692
|
+
// Reuse workflow — set when the card was opened by [Reuse workflow]: the run (or imported job) it came from.
|
|
3693
|
+
// Only Source, Destination and the two caps are open on that card; the SERVER refuses any other change.
|
|
3694
|
+
var startFrom = null;
|
|
3695
|
+
var reuseSession = false;
|
|
3696
|
+
var autoSigned = false;
|
|
3697
|
+
var sfOrigin = document.getElementById("chat-startfrom-origin");
|
|
3698
|
+
var sfLine = document.getElementById("chat-startfrom-line");
|
|
3699
|
+
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3700
|
+
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3701
|
+
var LOCKED_IDS = ["jf-name", "jf-goal", "jf-success", "jf-guardrails", "jf-judge"];
|
|
3702
|
+
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3703
|
+
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3704
|
+
var OPEN_IDS = ["jf-source", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3705
|
+
var reuseOn = false;
|
|
3706
|
+
function syncCardLock(){
|
|
3707
|
+
var live = sessionLive;
|
|
3708
|
+
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3709
|
+
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live); });
|
|
3710
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live); });
|
|
3711
|
+
document.getElementById("jf-model").disabled = live;
|
|
3712
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = reuseOn || live; });
|
|
3713
|
+
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3714
|
+
if(!reuseOn && !live){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3715
|
+
}
|
|
3716
|
+
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3717
|
+
function clearStartFrom(){
|
|
3718
|
+
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes
|
|
3719
|
+
if(startFrom){
|
|
3720
|
+
["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"].forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3721
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3722
|
+
}
|
|
3723
|
+
startFrom = null; reuseSession = false; autoSigned = false;
|
|
3724
|
+
sfOrigin.hidden = true; sfLine.hidden = true; sfNote.hidden = true;
|
|
3725
|
+
setReuseLocked(false);
|
|
3726
|
+
renderMain();
|
|
3727
|
+
}
|
|
3728
|
+
function openNewCard(){
|
|
2949
3729
|
card.hidden = false;
|
|
2950
|
-
newBtn.hidden = true;
|
|
2951
3730
|
thread.innerHTML = "";
|
|
2952
3731
|
errEl.textContent = "";
|
|
2953
3732
|
actionErrEl.textContent = "";
|
|
2954
3733
|
sessionId = null;
|
|
2955
3734
|
sessionLive = false;
|
|
2956
3735
|
signClickedOnce = false;
|
|
3736
|
+
reuseSession = false;
|
|
3737
|
+
autoSigned = false;
|
|
2957
3738
|
if(pollTimer){ clearInterval(pollTimer); pollTimer = null; }
|
|
2958
|
-
stopProgressDots();
|
|
2959
3739
|
progressRow.hidden = true;
|
|
2960
|
-
|
|
3740
|
+
progressRow.innerHTML = "";
|
|
3741
|
+
lastState = null;
|
|
2961
3742
|
refreshStartEnabled();
|
|
3743
|
+
}
|
|
3744
|
+
// hamr's ruling 2026-10-05: ONE button, two states. A live, unsigned session -> "Abandon" (the server ends it and
|
|
3745
|
+
// releases the one-at-a-time lock; money already spent stays booked). Anything else (signed and running, terminal,
|
|
3746
|
+
// nothing started) -> "Clear", which empties the card only and never stops a run (Stop lives on the Run tab).
|
|
3747
|
+
function clearButtonFor(live, hasId){
|
|
3748
|
+
return live ? {text: "Abandon", disabled: !hasId} : {text: "Clear", disabled: false};
|
|
3749
|
+
}
|
|
3750
|
+
function refreshClearBtn(){
|
|
3751
|
+
var b = clearButtonFor(sessionLive, !!sessionId);
|
|
3752
|
+
clearBtn.textContent = b.text;
|
|
3753
|
+
clearBtn.disabled = b.disabled;
|
|
3754
|
+
}
|
|
3755
|
+
// back to a fresh empty card: every box, check type deterministic, the default (first) Model, origin/estimate/notes
|
|
3756
|
+
// hidden, progress list and thread cleared, reuse lock lifted
|
|
3757
|
+
function resetCard(){
|
|
3758
|
+
["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"].forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3759
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3760
|
+
document.getElementById("jf-judge").disabled = true;
|
|
3761
|
+
modelSelect.selectedIndex = 0;
|
|
3762
|
+
msgInput.value = "";
|
|
3763
|
+
document.getElementById("jf-cap-note").textContent = "";
|
|
3764
|
+
lastPhase = null;
|
|
3765
|
+
clearStartFrom();
|
|
3766
|
+
openNewCard();
|
|
3767
|
+
refreshModelStatus();
|
|
3768
|
+
}
|
|
3769
|
+
clearBtn.addEventListener("click", function(){
|
|
3770
|
+
if(clearBtn.textContent === "Abandon" && sessionId){
|
|
3771
|
+
authorPost("/api/author/" + sessionId + "/abandon", {}).then(function(r){
|
|
3772
|
+
var o = chatPostOutcome(r);
|
|
3773
|
+
if(!o.ok){ chatActionFailed(o.error); return; }
|
|
3774
|
+
resetCard();
|
|
3775
|
+
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not abandoned"); });
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
resetCard();
|
|
2962
3779
|
});
|
|
2963
3780
|
|
|
2964
3781
|
function currentCard(){
|
|
@@ -2989,7 +3806,10 @@
|
|
|
2989
3806
|
function refreshStartEnabled(){
|
|
2990
3807
|
var v = Number(moneyCapInput.value);
|
|
2991
3808
|
var capOk = moneyCapInput.value.trim() !== "" && Number.isFinite(v) && v > 0;
|
|
2992
|
-
|
|
3809
|
+
startOk = capOk && !sessionLive && keyOk;
|
|
3810
|
+
syncCardLock();
|
|
3811
|
+
renderMain();
|
|
3812
|
+
refreshClearBtn();
|
|
2993
3813
|
}
|
|
2994
3814
|
moneyCapInput.addEventListener("input", refreshStartEnabled);
|
|
2995
3815
|
// P4a — the monthly-limit note sits right under the cap, as the person types.
|
|
@@ -3080,6 +3900,16 @@
|
|
|
3080
3900
|
});
|
|
3081
3901
|
}
|
|
3082
3902
|
modelSelect.addEventListener("change", refreshModelStatus);
|
|
3903
|
+
// Model is open on a reuse card: the estimate line counts only runs on the worker the box names right now
|
|
3904
|
+
function refreshReuseLine(){
|
|
3905
|
+
if(!startFrom) return;
|
|
3906
|
+
var q = (startFrom.importId ? "import=" + encodeURIComponent(startFrom.importId) : "runid=" + encodeURIComponent(startFrom.runid)) +
|
|
3907
|
+
"&model=" + encodeURIComponent(modelSelect.value);
|
|
3908
|
+
authorGet("/api/author/start-from?" + q).then(function(r){
|
|
3909
|
+
if(startFrom && r && r.ok){ sfLine.textContent = r.line || ""; }
|
|
3910
|
+
}).catch(function(){ /* the line keeps what it had */ });
|
|
3911
|
+
}
|
|
3912
|
+
modelSelect.addEventListener("change", refreshReuseLine);
|
|
3083
3913
|
// P4b — the Model menu IS the Settings rows: every Name, in keys-file order (a blank Name
|
|
3084
3914
|
// is never offered). Re-read whenever Settings loads or saves, keeping the pick if it survives.
|
|
3085
3915
|
function loadModels(){
|
|
@@ -3119,25 +3949,6 @@
|
|
|
3119
3949
|
thread.scrollTop = thread.scrollHeight;
|
|
3120
3950
|
}
|
|
3121
3951
|
|
|
3122
|
-
// build item 3 — the single live progress line: the LABEL sits first in
|
|
3123
|
-
// the markup, then the animated indicator (never the other way — the
|
|
3124
|
-
// dots must not push the label around as they cycle 1..4).
|
|
3125
|
-
var progressDotTimer = null;
|
|
3126
|
-
var progressDotCount = 1;
|
|
3127
|
-
var reducedMotion = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
3128
|
-
function stopProgressDots(){
|
|
3129
|
-
if(progressDotTimer){ clearInterval(progressDotTimer); progressDotTimer = null; }
|
|
3130
|
-
}
|
|
3131
|
-
function startProgressDots(){
|
|
3132
|
-
if(reducedMotion){ progressGlyph.textContent = "[progress....]"; return; }
|
|
3133
|
-
if(progressDotTimer) return;
|
|
3134
|
-
progressDotCount = 1;
|
|
3135
|
-
progressGlyph.textContent = "[progress.]";
|
|
3136
|
-
progressDotTimer = setInterval(function(){
|
|
3137
|
-
progressDotCount = (progressDotCount % 4) + 1;
|
|
3138
|
-
progressGlyph.textContent = "[progress" + new Array(progressDotCount + 1).join(".") + "]";
|
|
3139
|
-
}, 500);
|
|
3140
|
-
}
|
|
3141
3952
|
// build item 3 — pure: what the label must say for a given state. A
|
|
3142
3953
|
// person waiting on their OWN next action (an ask is pending) must never
|
|
3143
3954
|
// see a stale machine step name (observed: "checking prior behaviour"
|
|
@@ -3150,75 +3961,153 @@
|
|
|
3150
3961
|
if(pending) return "waiting for your answer";
|
|
3151
3962
|
return (state && state.progressLabel) || "working";
|
|
3152
3963
|
}
|
|
3964
|
+
// the ONE progress list: a step per line, in the server's order (STEP_LABELS in src/panel/authorsession.js owns
|
|
3965
|
+
// the ids and words). While a step runs its dots are CSS (still under prefers-reduced-motion); it ends as a check
|
|
3966
|
+
// or a cross, a cross carrying the code-owned reason on the same line. A pending ask for the person replaces the
|
|
3967
|
+
// running step's label with what is being waited on, never an animation.
|
|
3968
|
+
// Returns true when the list's content actually changed (a new line, or the latest line changed) so the caller can
|
|
3969
|
+
// scroll only then; an identical 2s poll tick returns false and leaves the person's scroll position alone.
|
|
3153
3970
|
function renderProgress(state){
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
progressLabelEl.textContent = progressLabelFor(state);
|
|
3168
|
-
startProgressDots();
|
|
3169
|
-
}
|
|
3971
|
+
var steps = (state && state.steps) || [];
|
|
3972
|
+
var before = progressRow.innerHTML;
|
|
3973
|
+
progressRow.hidden = steps.length === 0;
|
|
3974
|
+
progressRow.innerHTML = steps.map(function(st, i){
|
|
3975
|
+
var waiting = st.status === "running" && i === steps.length - 1 && state.pendingAsk;
|
|
3976
|
+
var cls = waiting ? "wait" : (st.status === "running" ? "run" : (st.status === "failed" ? "bad" : "ok"));
|
|
3977
|
+
var sign = cls === "ok" ? "\u2713" : (cls === "bad" ? "\u2717" : "");
|
|
3978
|
+
var label = waiting ? progressLabelFor(state) : st.label;
|
|
3979
|
+
return '<li class="step ' + cls + '" data-step="' + escapeXml(st.id) + '"><div class="step-line"><span class="step-label">' + escapeXml(label) + '</span>' +
|
|
3980
|
+
'<span class="step-sign">' + sign + '</span></div>' +
|
|
3981
|
+
(st.detail ? '<div class="step-detail">' + escapeXml("> " + st.detail) + '</div>' : "") + '</li>';
|
|
3982
|
+
}).join("");
|
|
3983
|
+
return progressRow.innerHTML !== before;
|
|
3170
3984
|
}
|
|
3171
3985
|
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
signBtn.disabled = false;
|
|
3183
|
-
signBtn.textContent = "Sign & run";
|
|
3184
|
-
} else if(state.phase === "prepared" && signClickedOnce){
|
|
3185
|
-
signBtn.disabled = false;
|
|
3186
|
-
signBtn.textContent = "Sign " + (state.specHash || "").slice(0, 8) + " & run";
|
|
3187
|
-
} else {
|
|
3188
|
-
signBtn.disabled = !atMenu;
|
|
3189
|
-
signBtn.textContent = "Sign & run";
|
|
3986
|
+
// hamr's ruling 2026-10-05: ONE wide button below the message box. Its wording, action and enabled state are ONE
|
|
3987
|
+
// pure function of (state, typed-text-empty, reuse); state = {session, startOk, phase, pendingAsk, revisesLeft,
|
|
3988
|
+
// signClicked, specHash}. Actions: start | check-deps | send | revise | sign-prepare | sign | none.
|
|
3989
|
+
function mainButtonFor(st, textEmpty, reuse){
|
|
3990
|
+
var ask = st.pendingAsk;
|
|
3991
|
+
var atMenu = !!ask && ask.kind === "menu";
|
|
3992
|
+
var left = st.revisesLeft || 0;
|
|
3993
|
+
var hint = atMenu ? left + (left === 1 ? " change left" : " changes left") : "";
|
|
3994
|
+
if(!st.session){
|
|
3995
|
+
return {text: reuse ? "Sign & run" : "Start drafting", action: "start", disabled: !st.startOk, hint: ""};
|
|
3190
3996
|
}
|
|
3191
|
-
if(
|
|
3192
|
-
|
|
3193
|
-
hashLine.textContent = "spec hash: " + state.specHash;
|
|
3997
|
+
if(ask && ask.kind === "install-needed"){
|
|
3998
|
+
return {text: "Check again", action: "check-deps", disabled: false, hint: ""};
|
|
3194
3999
|
}
|
|
3195
|
-
if(
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
4000
|
+
if(ask && !textEmpty){
|
|
4001
|
+
return atMenu
|
|
4002
|
+
? {text: "Send", action: "revise", disabled: left <= 0, hint: hint}
|
|
4003
|
+
: {text: "Send", action: "send", disabled: false, hint: ""};
|
|
4004
|
+
}
|
|
4005
|
+
if(ask && !atMenu){
|
|
4006
|
+
return {text: "Send", action: "send", disabled: true, hint: ""};
|
|
4007
|
+
}
|
|
4008
|
+
if(atMenu || st.phase === "prepared"){
|
|
4009
|
+
if(st.phase === "prepared" && st.signClicked){
|
|
4010
|
+
return {text: "Sign " + (st.specHash || "").slice(0, 8) + " & run", action: "sign", disabled: false, hint: hint};
|
|
4011
|
+
}
|
|
4012
|
+
return {text: "Sign & run", action: "sign-prepare", disabled: false, hint: hint};
|
|
3199
4013
|
}
|
|
3200
|
-
|
|
4014
|
+
return {text: "Sign & run", action: "none", disabled: true, hint: ""};
|
|
4015
|
+
}
|
|
4016
|
+
// pure: the ask box takes typed text only when the session has a pending ask that accepts it (the plan menu -> a
|
|
4017
|
+
// change request, or any question); not with no session, no ask, or while waiting on an install
|
|
4018
|
+
function askBoxOpenFor(st){
|
|
4019
|
+
return !!st.session && !!st.pendingAsk && st.pendingAsk.kind !== "install-needed";
|
|
4020
|
+
}
|
|
4021
|
+
function renderMain(){
|
|
4022
|
+
var st = {session: sessionLive, startOk: startOk, signClicked: signClickedOnce};
|
|
4023
|
+
if(sessionLive && lastState){
|
|
4024
|
+
st.phase = lastState.phase; st.pendingAsk = lastState.pendingAsk;
|
|
4025
|
+
st.revisesLeft = lastState.revisesLeft; st.specHash = lastState.specHash;
|
|
4026
|
+
}
|
|
4027
|
+
// hamr 2026-10-05: the ask box is open only while something asks for a reply; otherwise dimmed and emptied
|
|
4028
|
+
var open = askBoxOpenFor(st);
|
|
4029
|
+
msgInput.disabled = !open;
|
|
4030
|
+
if(!open) msgInput.value = "";
|
|
4031
|
+
var b = mainButtonFor(st, !msgInput.value.trim(), !!startFrom);
|
|
4032
|
+
mainBtn.textContent = b.text;
|
|
4033
|
+
mainBtn.disabled = b.disabled;
|
|
4034
|
+
mainAction = b.action;
|
|
4035
|
+
mainHint.textContent = b.hint;
|
|
4036
|
+
fitBox(msgInput);
|
|
4037
|
+
fitCardBoxes();
|
|
4038
|
+
}
|
|
4039
|
+
msgInput.addEventListener("input", renderMain);
|
|
4040
|
+
// grow a text box with its text (a few rows, then it scrolls inside); ONE function for the ask box and the card boxes.
|
|
4041
|
+
// A box that is hidden (or not laid out) measures 0 and gets no inline height.
|
|
4042
|
+
function fitBox(el){
|
|
4043
|
+
el.style.height = "auto";
|
|
4044
|
+
if(!el.scrollHeight){ el.style.height = ""; return; }
|
|
4045
|
+
var border = el.offsetHeight - el.clientHeight;
|
|
4046
|
+
el.style.height = Math.min(el.scrollHeight + border, 200) + "px";
|
|
4047
|
+
}
|
|
4048
|
+
function fitCardBoxes(){
|
|
4049
|
+
["jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge"].forEach(function(id){ fitBox(document.getElementById(id)); });
|
|
4050
|
+
}
|
|
4051
|
+
// live growth: each card box re-fits on its own input (typing and Enter), through the same fitBox
|
|
4052
|
+
["jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge"].forEach(function(id){
|
|
4053
|
+
var box = document.getElementById(id);
|
|
4054
|
+
box.addEventListener("input", function(){ fitBox(box); });
|
|
4055
|
+
});
|
|
4056
|
+
window.addEventListener("resize", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4057
|
+
document.getElementById("tab-chat").addEventListener("click", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4058
|
+
// Enter sends through the one main button; Shift+Enter (or IME composition) keeps its newline
|
|
4059
|
+
msgInput.addEventListener("keydown", function(e){
|
|
4060
|
+
if(e.key === "Enter" && !e.shiftKey && !e.isComposing){
|
|
4061
|
+
e.preventDefault();
|
|
4062
|
+
if(mainAction !== "start") mainBtn.click(); // never starts a (paid) draft from a stray Enter
|
|
4063
|
+
}
|
|
4064
|
+
});
|
|
4065
|
+
|
|
4066
|
+
function renderActions(state){
|
|
4067
|
+
lastState = state;
|
|
4068
|
+
// a session's stop is shown ONCE, in the progress line (renderProgress); #chat-card-error carries only a
|
|
4069
|
+
// refused START (its own handler writes it) — echoing state.error here made the same refusal appear twice
|
|
4070
|
+
errEl.textContent = "";
|
|
4071
|
+
var progressChanged = renderProgress(state);
|
|
3201
4072
|
// build item 4 — re-enable Start only once the session has settled
|
|
3202
4073
|
// into a terminal phase (a fresh Start becomes possible again).
|
|
3203
4074
|
sessionLive = CLIENT_TERMINAL_PHASES.indexOf(state.phase) === -1;
|
|
3204
4075
|
refreshStartEnabled();
|
|
4076
|
+
// hamr 2026-10-05: while the session is live and the list changed, bring the newest progress line into view.
|
|
4077
|
+
// Never moves keyboard focus, and an unchanged poll tick never scrolls.
|
|
4078
|
+
if(progressChanged && sessionLive && progressRow.lastElementChild){
|
|
4079
|
+
progressRow.lastElementChild.scrollIntoView({block: "nearest"});
|
|
4080
|
+
}
|
|
3205
4081
|
}
|
|
3206
4082
|
|
|
3207
4083
|
function poll(){
|
|
3208
4084
|
if(!sessionId) return;
|
|
4085
|
+
var polledId = sessionId;
|
|
3209
4086
|
authorGet("/api/author/" + sessionId).then(function(j){
|
|
3210
|
-
if(!j || !j.state) return;
|
|
4087
|
+
if(!j || !j.state || polledId !== sessionId) return;
|
|
3211
4088
|
renderMessages(j.state);
|
|
3212
4089
|
renderActions(j.state);
|
|
4090
|
+
if(reuseSession && !autoSigned && j.state.phase === "prepared" && j.state.specHash){
|
|
4091
|
+
autoSigned = true;
|
|
4092
|
+
authorPost("/api/author/" + sessionId + "/sign", {specHash: j.state.specHash}).then(function(r){
|
|
4093
|
+
var o = chatPostOutcome(r);
|
|
4094
|
+
if(o.ok){ chatActionOk(); } else { autoSigned = false; chatActionFailed(o.error); }
|
|
4095
|
+
poll();
|
|
4096
|
+
}).catch(function(){ autoSigned = false; chatActionFailed("network error — could not reach the panel server, not signed"); });
|
|
4097
|
+
}
|
|
3213
4098
|
if(j.state.phase === "signed"){
|
|
3214
4099
|
clearInterval(pollTimer);
|
|
3215
4100
|
document.getElementById("tab-runs").click();
|
|
4101
|
+
// hamr's ruling 2026-10-05: the run has started, so the Chat card goes back to the empty card (the one reset
|
|
4102
|
+
// Clear uses) — it must not keep showing a signed job that invites a rerun. The run itself is untouched.
|
|
4103
|
+
resetCard();
|
|
4104
|
+
return;
|
|
3216
4105
|
}
|
|
3217
4106
|
lastPhase = j.state.phase;
|
|
3218
4107
|
}).catch(function(){ /* transient — next poll tries again */ });
|
|
3219
4108
|
}
|
|
3220
4109
|
|
|
3221
|
-
|
|
4110
|
+
function doStart(){
|
|
3222
4111
|
errEl.textContent = "";
|
|
3223
4112
|
actionErrEl.textContent = "";
|
|
3224
4113
|
// build item 4 — disable the moment it's clicked, not just once the
|
|
@@ -3226,6 +4115,7 @@
|
|
|
3226
4115
|
sessionLive = true;
|
|
3227
4116
|
refreshStartEnabled();
|
|
3228
4117
|
var body = currentCard();
|
|
4118
|
+
if(startFrom) body.startFrom = startFrom.importId ? {importId: startFrom.importId} : startFrom.runid;
|
|
3229
4119
|
authorPost("/api/author/start", body).then(function(r){
|
|
3230
4120
|
if(r.status !== 200 || !r.body.ok){
|
|
3231
4121
|
errEl.textContent = (r.body && r.body.error) || "refused";
|
|
@@ -3234,12 +4124,89 @@
|
|
|
3234
4124
|
return;
|
|
3235
4125
|
}
|
|
3236
4126
|
sessionId = r.body.sessionId;
|
|
3237
|
-
|
|
4127
|
+
// a same-job session has no confirm turn: it goes straight to `prepared`, and THIS click (Sign & run) is the
|
|
4128
|
+
// signature — the page signs the hash the server prepared as soon as it is ready (the server re-checks it)
|
|
4129
|
+
reuseSession = r.body.reuse === true;
|
|
4130
|
+
autoSigned = false;
|
|
4131
|
+
signClickedOnce = reuseSession;
|
|
3238
4132
|
renderMessages(r.body.state);
|
|
3239
4133
|
renderActions(r.body.state);
|
|
3240
4134
|
if(pollTimer) clearInterval(pollTimer);
|
|
3241
4135
|
pollTimer = setInterval(poll, 2000);
|
|
3242
4136
|
});
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// Re-attach after a page load/refresh: the server holds the one live (non-terminal) authoring session, so ask it.
|
|
4140
|
+
// If one exists, this tab takes it over exactly as if it had started it: the card filled from the session's own
|
|
4141
|
+
// copy, the poll loop, progress list, thread, ask box, main button and Abandon. A signed session is terminal, so it
|
|
4142
|
+
// never comes back (the card stays empty). Nothing is stored page-side.
|
|
4143
|
+
function attachSession(j){
|
|
4144
|
+
var st = j.state, c = st.card || {};
|
|
4145
|
+
openNewCard();
|
|
4146
|
+
sessionId = j.sessionId;
|
|
4147
|
+
sessionLive = true;
|
|
4148
|
+
reuseSession = st.reuse === true;
|
|
4149
|
+
autoSigned = false;
|
|
4150
|
+
signClickedOnce = reuseSession;
|
|
4151
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === (c.checkType === "rubric" ? "rubric" : "deterministic")); });
|
|
4152
|
+
if(c.model){
|
|
4153
|
+
if(!Array.prototype.some.call(modelSelect.options, function(o){ return o.value === c.model; })){
|
|
4154
|
+
modelSelect.insertAdjacentHTML("beforeend", '<option value="' + escapeXml(c.model) + '">' + escapeXml(c.model) + "</option>");
|
|
4155
|
+
}
|
|
4156
|
+
modelSelect.value = c.model;
|
|
4157
|
+
}
|
|
4158
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4159
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4160
|
+
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4161
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4162
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4163
|
+
if(reuseSession) setReuseLocked(true);
|
|
4164
|
+
fitCardBoxes();
|
|
4165
|
+
renderMessages(st);
|
|
4166
|
+
renderActions(st);
|
|
4167
|
+
if(pollTimer) clearInterval(pollTimer);
|
|
4168
|
+
pollTimer = setInterval(poll, 2000);
|
|
4169
|
+
poll();
|
|
4170
|
+
}
|
|
4171
|
+
function reattachLive(){
|
|
4172
|
+
if(sessionId || sessionLive) return;
|
|
4173
|
+
authorGet("/api/author/live").then(function(j){
|
|
4174
|
+
if(sessionId || sessionLive || !j || !j.ok || !j.sessionId || !j.state) return;
|
|
4175
|
+
attachSession(j);
|
|
4176
|
+
}).catch(function(){ /* nothing to attach to — the empty card stays */ });
|
|
4177
|
+
}
|
|
4178
|
+
document.getElementById("tab-chat").addEventListener("click", reattachLive);
|
|
4179
|
+
reattachLive();
|
|
4180
|
+
|
|
4181
|
+
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4182
|
+
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4183
|
+
document.addEventListener("bareloop-reuse", function(e){
|
|
4184
|
+
var d = e.detail || {};
|
|
4185
|
+
var pre = d.prefill || {};
|
|
4186
|
+
var c = pre.card || {};
|
|
4187
|
+
clearStartFrom();
|
|
4188
|
+
openNewCard();
|
|
4189
|
+
startFrom = d.importId ? {importId: d.importId} : {runid: d.runid};
|
|
4190
|
+
var radios = document.querySelectorAll('input[name="jf-verdict"]');
|
|
4191
|
+
radios.forEach(function(r){ r.checked = (r.value === (c.checkType === "rubric" ? "rubric" : "deterministic")); });
|
|
4192
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4193
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4194
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4195
|
+
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4196
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4197
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4198
|
+
setReuseLocked(true);
|
|
4199
|
+
sfOrigin.hidden = false;
|
|
4200
|
+
sfOrigin.textContent = "Reuse: " + ((pre.origin && pre.origin.job) || "job") + (d.runid ? " (run " + d.runid + ")" : " (imported)");
|
|
4201
|
+
sfLine.hidden = false;
|
|
4202
|
+
sfLine.textContent = pre.line || "";
|
|
4203
|
+
sfLine.className = "startfrom-line";
|
|
4204
|
+
sfNote.hidden = !pre.note;
|
|
4205
|
+
sfNote.textContent = pre.note || "";
|
|
4206
|
+
refreshModelStatus();
|
|
4207
|
+
refreshReuseLine();
|
|
4208
|
+
refreshCapNote();
|
|
4209
|
+
refreshStartEnabled();
|
|
3243
4210
|
});
|
|
3244
4211
|
|
|
3245
4212
|
// build item 1 — the "lost answer" fix. ROOT CAUSE: `renderActions`
|
|
@@ -3275,18 +4242,23 @@
|
|
|
3275
4242
|
function chatActionOk(){
|
|
3276
4243
|
actionErrEl.textContent = "";
|
|
3277
4244
|
}
|
|
4245
|
+
// a dead token (panel restarted): show the one line where Chat actions show errors and stop polling
|
|
4246
|
+
staleTokenHook = function(){
|
|
4247
|
+
actionErrEl.textContent = STALE_TOKEN_LINE;
|
|
4248
|
+
if(pollTimer){ clearInterval(pollTimer); pollTimer = null; }
|
|
4249
|
+
};
|
|
3278
4250
|
|
|
3279
|
-
|
|
4251
|
+
function doCheckDeps(){
|
|
3280
4252
|
if(!sessionId) return;
|
|
3281
|
-
|
|
4253
|
+
mainBtn.disabled = true;
|
|
3282
4254
|
authorPost("/api/author/" + sessionId + "/check-deps", {}).then(function(r){
|
|
3283
4255
|
var o = chatPostOutcome(r);
|
|
3284
4256
|
if(o.ok) chatActionOk(); else chatActionFailed(o.error);
|
|
3285
4257
|
poll();
|
|
3286
4258
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, nothing was sent"); });
|
|
3287
|
-
}
|
|
4259
|
+
}
|
|
3288
4260
|
|
|
3289
|
-
|
|
4261
|
+
function doSend(){
|
|
3290
4262
|
if(!sessionId) return;
|
|
3291
4263
|
var text = msgInput.value;
|
|
3292
4264
|
if(!text.trim()) return;
|
|
@@ -3302,9 +4274,9 @@
|
|
|
3302
4274
|
}
|
|
3303
4275
|
poll();
|
|
3304
4276
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
3305
|
-
}
|
|
4277
|
+
}
|
|
3306
4278
|
|
|
3307
|
-
|
|
4279
|
+
function doRevise(){
|
|
3308
4280
|
if(!sessionId) return;
|
|
3309
4281
|
var text = msgInput.value;
|
|
3310
4282
|
authorPost("/api/author/" + sessionId + "/revise", {text: text}).then(function(r){
|
|
@@ -3317,9 +4289,9 @@
|
|
|
3317
4289
|
}
|
|
3318
4290
|
poll();
|
|
3319
4291
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, your text was NOT sent"); });
|
|
3320
|
-
}
|
|
4292
|
+
}
|
|
3321
4293
|
|
|
3322
|
-
|
|
4294
|
+
function doSign(){
|
|
3323
4295
|
if(!sessionId) return;
|
|
3324
4296
|
if(!signClickedOnce){
|
|
3325
4297
|
// click 1 — the confirm turn's own "confirm" pick. Never signs.
|
|
@@ -3342,6 +4314,15 @@
|
|
|
3342
4314
|
poll();
|
|
3343
4315
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not signed"); });
|
|
3344
4316
|
});
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
mainBtn.addEventListener("click", function(){
|
|
4320
|
+
if(mainBtn.disabled) return;
|
|
4321
|
+
if(mainAction === "start") doStart();
|
|
4322
|
+
else if(mainAction === "check-deps") doCheckDeps();
|
|
4323
|
+
else if(mainAction === "send") doSend();
|
|
4324
|
+
else if(mainAction === "revise") doRevise();
|
|
4325
|
+
else if(mainAction === "sign-prepare" || mainAction === "sign") doSign();
|
|
3345
4326
|
});
|
|
3346
4327
|
|
|
3347
4328
|
refreshStartEnabled();
|