@giamme/forge 0.9.0 → 0.11.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 +56 -0
- package/SKILL.md +22 -3
- package/VERSION +1 -1
- package/docs/artifacts.md +28 -2
- package/docs/commands.md +55 -0
- package/docs/dataset.md +22 -0
- package/docs/decomposed-runs.md +54 -2
- package/docs/forge-ui.md +48 -7
- package/docs/memory.md +4 -2
- package/docs/reviews.md +87 -2
- package/forge +31 -1
- package/package.json +1 -1
- package/references/dataset.md +103 -4
- package/references/decompose.md +41 -5
- package/references/harnesses.md +43 -1
- package/references/ui.md +31 -6
- package/scripts/forge-config.py +350 -0
- package/scripts/forge-control.py +649 -0
- package/scripts/forge-dispatch.sh +235 -121
- package/scripts/forge-issues.py +1134 -0
- package/scripts/forge-lib-plans.sh +6 -2
- package/scripts/forge-lib-review.sh +158 -3
- package/scripts/forge-lib-task.sh +358 -41
- package/scripts/forge-parallel-options.sh +3 -1
- package/scripts/forge-parallel.sh +99 -13
- package/scripts/forge-prompt.py +61 -4
- package/scripts/forge-route.py +245 -28
- package/scripts/forge-runs-rm.py +840 -0
- package/scripts/forge-runs.py +19 -1
- package/scripts/forge-runtime.py +47 -1
- package/scripts/forge-schedule.py +227 -11
- package/scripts/forge-ui.py +85 -19
- package/scripts/forge_dataset/__init__.py +84 -8
- package/scripts/forge_dataset/capture.py +5 -2
- package/scripts/forge_dataset/cli.py +44 -1
- package/scripts/forge_dataset/export.py +36 -3
- package/scripts/forge_dataset/forecast.py +355 -0
- package/scripts/forge_dataset/harvest.py +45 -2
- package/scripts/forge_dataset/importer.py +431 -17
- package/scripts/forge_dataset/reviewers.py +145 -0
- package/scripts/forge_dataset/routing.py +1 -0
- package/scripts/forge_jev/__init__.py +7 -3
- package/scripts/forge_registry.py +183 -26
- package/ui/README.md +41 -7
- package/ui/docs/api.md +259 -21
- package/ui/forge_ui/__main__.py +10 -2
- package/ui/forge_ui/control.py +587 -0
- package/ui/forge_ui/export.py +17 -3
- package/ui/forge_ui/reader.py +210 -12
- package/ui/forge_ui/server.py +125 -4
- package/ui/forge_ui/static/app.js +104 -18
- package/ui/forge_ui/static/cloud3d.js +4 -1
- package/ui/forge_ui/static/cloudmodel.js +1 -1
- package/ui/forge_ui/static/cloudui.js +13 -4
- package/ui/forge_ui/static/controls.js +619 -0
- package/ui/forge_ui/static/findingsview.js +438 -0
- package/ui/forge_ui/static/graph.js +6 -5
- package/ui/forge_ui/static/index.html +7 -1
- package/ui/forge_ui/static/leaderboard.js +18 -0
- package/ui/forge_ui/static/modelstats.js +19 -1
- package/ui/forge_ui/static/panel.js +76 -14
- package/ui/forge_ui/static/rundelete.js +388 -0
- package/ui/forge_ui/static/runmodel.js +128 -13
- package/ui/forge_ui/static/scene3d.js +32 -15
- package/ui/forge_ui/static/settings.js +399 -0
- package/ui/forge_ui/static/style.css +184 -5
- package/ui/forge_ui/static/uikit.js +161 -0
|
@@ -91,13 +91,14 @@
|
|
|
91
91
|
:root[data-theme=dark] { color-scheme: dark; }
|
|
92
92
|
|
|
93
93
|
/* status: the lamp behind each forge state. Five lamps, one meaning each (--st; --st-lit on the 3D plate);
|
|
94
|
-
a hollow lamp is that lamp's family but not its plain case: passed without a merge, stopped, failed outside the code
|
|
94
|
+
a hollow lamp is that lamp's family but not its plain case: passed without a merge, stopped, failed outside the code,
|
|
95
|
+
an outdated review */
|
|
95
96
|
.st-merged, .st-pass, .st-accepted, .st-nochanges { --st: var(--lamp-green); --st-lit: var(--lit-green); }
|
|
96
97
|
.st-running, .st-interrupted { --st: var(--lamp-amber); --st-lit: var(--lit-amber); }
|
|
97
|
-
.st-pending, .st-none, .st-unknown, .st-invalidated { --st: var(--lamp-off); --st-lit: var(--lamp-off); }
|
|
98
|
+
.st-pending, .st-none, .st-unknown, .st-invalidated, .st-outdated { --st: var(--lamp-off); --st-lit: var(--lamp-off); }
|
|
98
99
|
.st-fail, .st-error, .st-conflict, .st-infra, .st-timeout, .st-blocked { --st: var(--lamp-red); --st-lit: var(--lit-red); }
|
|
99
100
|
[class^="st-"], [class*=" st-"] { --st-fill: var(--st); --st-ring: var(--lamp-edge); }
|
|
100
|
-
.st-accepted, .st-nochanges, .st-interrupted, .st-infra, .st-timeout, .st-blocked { --st-fill: transparent; --st-ring: var(--st); }
|
|
101
|
+
.st-accepted, .st-nochanges, .st-interrupted, .st-infra, .st-timeout, .st-blocked, .st-outdated { --st-fill: transparent; --st-ring: var(--st); }
|
|
101
102
|
/* review severity is a shape (P0 spike, P1 diamond, P2 cone, P3 pin) on a ramp of its own, never a status lamp */
|
|
102
103
|
.sev-P0 { --sev: var(--sev-p0); --sev-shape: polygon(50% 0, 100% 100%, 0 100%); }
|
|
103
104
|
.sev-P1 { --sev: var(--sev-p1); --sev-shape: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
|
|
@@ -152,6 +153,7 @@ kbd { font-family: var(--mono); font-size: var(--t-xs); padding: 0 5px; border:
|
|
|
152
153
|
.chip { display: inline-flex; align-items: center; gap: 5px; padding: 1px 7px 0 6px; border: 1px solid var(--rule); border-radius: var(--r-small); background: var(--key); color: var(--ink); white-space: nowrap;
|
|
153
154
|
font-family: var(--font-plate); font-weight: 700; font-size: .857rem; letter-spacing: .05em; text-transform: uppercase; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
|
154
155
|
.chip::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--st-fill, var(--lamp-off)); box-shadow: inset 0 0 0 1.5px var(--st-ring, transparent); flex: none; }
|
|
156
|
+
a.chip, a.chip:hover { color: inherit; text-decoration: none; }
|
|
155
157
|
.chips { display: flex; gap: var(--s-1); flex-wrap: wrap; }
|
|
156
158
|
.sev-chip { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: var(--t-xs); white-space: nowrap; }
|
|
157
159
|
.sev-chip::before { content: ''; width: 9px; height: 9px; clip-path: var(--sev-shape, circle(30%)); background: var(--sev, var(--muted)); flex: none; }
|
|
@@ -320,7 +322,7 @@ body.no-tasks .tasks-module { display: none; }
|
|
|
320
322
|
body[data-shell=workbench].no-tasks .run-view { grid-template-rows: auto minmax(0, 1fr) auto 0; }
|
|
321
323
|
body[data-shell=immersive] .sidebar { position: absolute; z-index: 4; left: var(--gap); top: var(--gap); bottom: var(--gap); width: var(--runs-w); box-shadow: var(--ply-1); }
|
|
322
324
|
body[data-shell=immersive][data-runs=closed] .sidebar { display: none; }
|
|
323
|
-
body[data-shell=immersive]:is([data-view=models], [data-view=waiting], [data-view=loading], [data-view=first-run]) #main { left: calc(var(--L) - var(--gap)); }
|
|
325
|
+
body[data-shell=immersive]:is([data-view=models], [data-view=findings], [data-view=settings], [data-view=waiting], [data-view=loading], [data-view=first-run]) #main { left: calc(var(--L) - var(--gap)); }
|
|
324
326
|
body[data-shell=immersive] .stage-row, body[data-shell=immersive] .stage-col { position: absolute; inset: 0; }
|
|
325
327
|
body[data-shell=immersive] .stage-col .stage-slot { position: absolute; inset: 0; }
|
|
326
328
|
body[data-shell=immersive] .insp-module { position: absolute; z-index: 3; right: var(--gap); top: var(--gap); bottom: var(--gap); width: var(--insp-w); box-shadow: var(--ply-1); }
|
|
@@ -375,7 +377,7 @@ body[data-shell=workbench] .map-cards-host { overflow: auto; }
|
|
|
375
377
|
body[data-shell=workbench] .map-cards { padding: var(--s-4); }
|
|
376
378
|
|
|
377
379
|
/* content views: the leaderboard and the waiting pages */
|
|
378
|
-
body:is([data-view=models], [data-view=waiting], [data-view=loading], [data-view=first-run]) #main { padding: var(--s-6) var(--s-6) var(--s-8); }
|
|
380
|
+
body:is([data-view=models], [data-view=findings], [data-view=settings], [data-view=waiting], [data-view=loading], [data-view=first-run]) #main { padding: var(--s-6) var(--s-6) var(--s-8); }
|
|
379
381
|
|
|
380
382
|
/* ---------- the stage: the panel the run is inlaid on ---------- */
|
|
381
383
|
.stage-slot { position: relative; background: var(--stage-bg); }
|
|
@@ -583,6 +585,52 @@ pre.cmd { max-height: none; }
|
|
|
583
585
|
.ft-d1 { padding-left: 16px; } .ft-d2 { padding-left: 32px; } .ft-d3 { padding-left: 48px; } .ft-d4 { padding-left: 64px; } .ft-d5 { padding-left: 80px; } .ft-d6 { padding-left: 96px; }
|
|
584
586
|
.ft-node:not(.ft-d0) { border-left: 1px solid var(--rule-2); margin-left: 4px; }
|
|
585
587
|
|
|
588
|
+
/* ---------- write helpers (uikit.js) ---------- */
|
|
589
|
+
/* a result line: a lamp and a sentence; a green lamp for done, a red one for refused or failed */
|
|
590
|
+
p.ui-outcome { display: flex; align-items: flex-start; gap: 7px; margin: 6px 0 0; font-size: var(--t-sm); overflow-wrap: anywhere; }
|
|
591
|
+
.ui-outcome .lamp { margin-top: .42em; }
|
|
592
|
+
/* an inline confirm: a strip with Confirm and Cancel keys, its lamp the route lamp (red for a destructive change) */
|
|
593
|
+
.ui-confirm { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin: var(--s-2) 0 0; padding: 6px var(--s-2); border: 1px solid var(--route); border-radius: var(--r-key); background: var(--key); }
|
|
594
|
+
.ui-confirm.danger { border-color: var(--lamp-red); }
|
|
595
|
+
.ui-confirm-text { flex: 1 1 14em; min-width: 0; font-size: var(--t-sm); overflow-wrap: anywhere; }
|
|
596
|
+
.ui-confirm button { min-height: 28px; }
|
|
597
|
+
button.ui-danger { background: var(--lamp-red); border-color: var(--lamp-red); color: light-dark(#fff, #111613); }
|
|
598
|
+
button.ui-danger:hover:not(:disabled) { background: var(--lamp-red); color: light-dark(#fff, #111613); opacity: .9; }
|
|
599
|
+
input:disabled { opacity: .45; cursor: default; }
|
|
600
|
+
|
|
601
|
+
/* ---------- the inspector's controls (controls.js) ---------- */
|
|
602
|
+
/* each control is a tonal well on the module: its caption, the keys or field, what it means, and its result */
|
|
603
|
+
.ctl-body { display: grid; gap: var(--s-2); }
|
|
604
|
+
.ctl-row { display: grid; gap: 6px; padding: var(--s-2) 10px; border: 1px solid var(--rule); border-radius: var(--r-key); background: var(--module-2); }
|
|
605
|
+
.ctl-status:empty, .ctl-notes:empty, .ctl-confirm:empty, .ctl-out:empty { display: none; }
|
|
606
|
+
.ctl-label { font-family: var(--font-plate); font-weight: 700; font-size: .786rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); cursor: default; }
|
|
607
|
+
label.ctl-label { cursor: pointer; }
|
|
608
|
+
p.ctl-live { display: flex; align-items: center; gap: var(--s-2); margin: 0 0 var(--s-1); }
|
|
609
|
+
p.ctl-note { margin: 0; }
|
|
610
|
+
p.ctl-help, p.ctl-why { margin: 0; font-size: var(--t-xs); color: var(--muted); }
|
|
611
|
+
p.ctl-why { display: flex; align-items: center; gap: 6px; }
|
|
612
|
+
p.ctl-why::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--lamp-off); flex: none; }
|
|
613
|
+
.ctl-bank { flex-wrap: wrap; max-width: 100%; gap: 2px; justify-self: start; }
|
|
614
|
+
.ctl-bank button.is-draft { box-shadow: inset 0 0 0 1px var(--route); }
|
|
615
|
+
.ctl-set, .ctl-pin { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
616
|
+
.ctl-set input { width: 6.5em; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
617
|
+
.ctl-pin { align-items: end; }
|
|
618
|
+
.ctl-pin .ctl-alias, .ctl-pin .ctl-harness { flex: 1 1 9em; min-width: 0; }
|
|
619
|
+
.ctl-pin .ctl-effort { flex: 1 1 100%; }
|
|
620
|
+
.ctl-pin input { min-width: 0; }
|
|
621
|
+
.ctl-unit { color: var(--muted); font-size: var(--t-xs); }
|
|
622
|
+
.ctl-set button, .ctl-pin > button { min-height: 28px; }
|
|
623
|
+
.ctl-set button[aria-pressed=true] { background: var(--key); box-shadow: inset 0 0 0 1px var(--rule-2); color: var(--muted); }
|
|
624
|
+
.ctl-pin-now { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
|
|
625
|
+
p.ctl-stop { display: flex; align-items: center; gap: 7px; margin: 0; padding: 4px var(--s-2); border-radius: var(--r-small); background: var(--warn-wash); color: var(--warn-ink); font-weight: 600; }
|
|
626
|
+
.ctl-log { margin-top: var(--s-1); }
|
|
627
|
+
.ctl-changes { list-style: none; margin: 4px 0 0; padding: 0; display: grid; gap: 4px; font-size: var(--t-xs); }
|
|
628
|
+
.ctl-changes li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; }
|
|
629
|
+
.ctl-what { color: var(--ink); font-weight: 600; }
|
|
630
|
+
.ctl[aria-busy=true] .ctl-row { opacity: .7; }
|
|
631
|
+
/* who decided an escalation, on the line that reports it */
|
|
632
|
+
.src-plate { font-size: .714rem; padding: 0 5px; vertical-align: 1px; }
|
|
633
|
+
|
|
586
634
|
/* ---------- tables ---------- */
|
|
587
635
|
.table-scroll { overflow: auto; max-width: 100%; border: 1px solid var(--rule); border-radius: var(--r-key); background: var(--module); }
|
|
588
636
|
table { width: 100%; border-collapse: separate; border-spacing: 0; text-align: left; font-variant-numeric: tabular-nums; }
|
|
@@ -637,6 +685,7 @@ th:hover .th-sort, .th-sort.on { color: var(--ink); }
|
|
|
637
685
|
.fg-bar { fill: var(--st, var(--lamp-off)); }
|
|
638
686
|
.fg-verdict { stroke: var(--lamp-red); }
|
|
639
687
|
.fg-verdict.fg-pass { stroke: var(--st, var(--lamp-green)); }
|
|
688
|
+
.fg-node.st-accepted .fg-box { fill: transparent; }
|
|
640
689
|
.fg-empty { color: var(--muted); padding: var(--s-3) 0; }
|
|
641
690
|
.gantt, .strip { display: block; font-family: var(--mono); }
|
|
642
691
|
.gt-lane { fill: var(--module-2); }
|
|
@@ -647,6 +696,7 @@ th:hover .th-sort, .th-sort.on { color: var(--ink); }
|
|
|
647
696
|
.gt-tick-label { fill: var(--muted); font-size: 10px; }
|
|
648
697
|
.gt-item { cursor: pointer; }
|
|
649
698
|
.gt-box { fill: var(--st, var(--lamp-off)); fill-opacity: .2; stroke: var(--st, var(--lamp-off)); stroke-width: 1.2; }
|
|
699
|
+
.gt-item.st-accepted .gt-box { fill: transparent; }
|
|
650
700
|
.gt-item:hover .gt-box, .gt-item:focus .gt-box { fill-opacity: .35; }
|
|
651
701
|
.gt-item:focus { outline: none; }
|
|
652
702
|
.gt-item.gt-sel .gt-box { stroke: var(--route); stroke-width: 2.5; }
|
|
@@ -1071,3 +1121,132 @@ dialog.export-dialog h2 { margin-bottom: var(--s-2); }
|
|
|
1071
1121
|
@media (prefers-reduced-motion: reduce) {
|
|
1072
1122
|
*, *::before, *::after { animation: none !important; transition-duration: 0ms !important; }
|
|
1073
1123
|
}
|
|
1124
|
+
|
|
1125
|
+
/* ---------- open findings (#/runs/<key>/findings) ---------- */
|
|
1126
|
+
.findings-view { display: grid; gap: var(--s-3); max-width: 1400px; }
|
|
1127
|
+
.fnd-head { display: flex; align-items: baseline; gap: var(--s-4); flex-wrap: wrap; }
|
|
1128
|
+
.fnd-lede { margin: 0; }
|
|
1129
|
+
.fnd-scope { justify-self: start; }
|
|
1130
|
+
.fnd-lamp { display: flex; align-items: center; gap: 7px; margin: 0; font-size: var(--t-sm); overflow-wrap: anywhere; }
|
|
1131
|
+
.fnd-amber { --st-fill: var(--lamp-amber); }
|
|
1132
|
+
.fnd-notes p { margin: 0; font-size: var(--t-sm); }
|
|
1133
|
+
.fnd-filters, .fnd-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); }
|
|
1134
|
+
.fnd-field { display: inline-flex; align-items: center; gap: var(--s-2); }
|
|
1135
|
+
.fnd-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-sm); }
|
|
1136
|
+
.fnd-actions { padding: 6px var(--s-2); border: 1px solid var(--rule); border-radius: var(--r-key); background: var(--key); }
|
|
1137
|
+
.fnd-actions button:disabled { opacity: .5; cursor: not-allowed; }
|
|
1138
|
+
.fnd-note { font-size: var(--t-sm); flex: 1 1 12em; }
|
|
1139
|
+
.fnd-reason { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
|
|
1140
|
+
.fnd-reason:empty { display: none; }
|
|
1141
|
+
.fnd-reason-field input { min-width: 22em; }
|
|
1142
|
+
.fnd-status { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
1143
|
+
.fnd-summary-line { display: flex; align-items: baseline; gap: 6px; }
|
|
1144
|
+
.fnd-more { padding: 0 4px; min-height: 22px; border: 1px solid var(--rule); border-radius: var(--r-small); background: var(--key); color: var(--ink); cursor: pointer; }
|
|
1145
|
+
.fnd-more:focus-visible, .fnd-back:focus-visible, .fnd-grid input:focus-visible { outline: 2px solid var(--route); outline-offset: 2px; }
|
|
1146
|
+
.fnd-detail { margin-top: var(--s-2); }
|
|
1147
|
+
.fnd-text, .fnd-body { margin: 0; padding: var(--s-2); max-height: 22em; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; border: 1px solid var(--rule); border-radius: var(--r-small); font-family: var(--mono); font-size: var(--t-xs); }
|
|
1148
|
+
.fnd-meta { display: grid; grid-template-columns: max-content 1fr; gap: 2px var(--s-3); margin: var(--s-2) 0 0; font-size: var(--t-xs); }
|
|
1149
|
+
.fnd-meta dt { color: var(--muted); }
|
|
1150
|
+
.fnd-meta dd { margin: 0; font-family: var(--mono); overflow-wrap: anywhere; }
|
|
1151
|
+
dialog.fnd-dialog { width: min(760px, calc(100vw - 24px)); max-height: 86vh; overflow: auto; padding: 18px 20px; border: 1px solid var(--rule); border-radius: var(--r-module); background: var(--module); color: var(--ink); box-shadow: var(--ply-2); }
|
|
1152
|
+
dialog.fnd-dialog::backdrop { background: light-dark(rgb(17 22 19 / .35), rgb(0 0 0 / .55)); }
|
|
1153
|
+
.fnd-warn, .fnd-same { display: flex; align-items: flex-start; gap: 7px; margin: var(--s-2) 0; }
|
|
1154
|
+
.fnd-warn .lamp, .fnd-same .lamp, .fnd-created .lamp { margin-top: .42em; }
|
|
1155
|
+
.fnd-draft { margin: var(--s-3) 0; }
|
|
1156
|
+
.fnd-draft-title { display: flex; align-items: center; gap: var(--s-2); margin: 0 0 var(--s-2); overflow-wrap: anywhere; }
|
|
1157
|
+
.fnd-dialog-actions { display: flex; justify-content: flex-end; gap: var(--s-2); margin-top: var(--s-3); }
|
|
1158
|
+
.fnd-created { list-style: none; margin: var(--s-2) 0 0; padding: 0; }
|
|
1159
|
+
.fnd-created li { display: flex; align-items: flex-start; gap: 7px; overflow-wrap: anywhere; }
|
|
1160
|
+
.findings-link .findings-count:not(:empty) { font-family: var(--mono); }
|
|
1161
|
+
|
|
1162
|
+
/* ---------- delete a run ---------- */
|
|
1163
|
+
.del-gap { align-self: stretch; width: 1px; margin: 0 6px; background: var(--rule-2); }
|
|
1164
|
+
button.del-key { border-color: var(--lamp-red); color: var(--lamp-red); }
|
|
1165
|
+
button.del-key:hover:not(:disabled) { background: var(--lamp-red); border-color: var(--lamp-red); color: light-dark(#fff, #111613); }
|
|
1166
|
+
button.del-key:focus-visible, button.del-go:focus-visible { outline: 2px solid var(--lamp-red); outline-offset: 2px; }
|
|
1167
|
+
dialog.del-dialog { width: min(680px, calc(100vw - 24px)); max-height: 90vh; overflow: auto; padding: 18px 20px; border: 1px solid var(--lamp-red); border-radius: var(--r-module); background: var(--module); color: var(--ink); box-shadow: var(--ply-2); }
|
|
1168
|
+
dialog.del-dialog::backdrop { background: light-dark(rgb(17 22 19 / .35), rgb(0 0 0 / .55)); }
|
|
1169
|
+
.del-title { margin: 0 0 var(--s-2); }
|
|
1170
|
+
.del-ident { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); margin: 0 0 var(--s-3); overflow-wrap: anywhere; }
|
|
1171
|
+
.del-id { padding: 0 5px; border: 1px solid var(--rule-2); border-radius: var(--r-small); background: var(--field); font-family: var(--mono); }
|
|
1172
|
+
.del-status { display: flex; align-items: flex-start; gap: 7px; margin: 0 0 var(--s-3); font-size: var(--t-sm); overflow-wrap: anywhere; }
|
|
1173
|
+
.del-status .lamp, .del-warning .lamp, .del-outcome-head .lamp { margin-top: .42em; }
|
|
1174
|
+
.del-section { margin: 0 0 var(--s-3); }
|
|
1175
|
+
.del-section > h3 { margin: 0 0 var(--s-2); }
|
|
1176
|
+
.del-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px var(--s-3); margin: 0; font-size: var(--t-sm); }
|
|
1177
|
+
.del-facts dt { color: var(--muted); }
|
|
1178
|
+
.del-facts dd { margin: 0; overflow-wrap: anywhere; }
|
|
1179
|
+
.del-steps, .del-list, .del-warnings { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
|
|
1180
|
+
.del-step, .del-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--s-2); font-size: var(--t-sm); overflow-wrap: anywhere; }
|
|
1181
|
+
.del-step .lamp { align-self: center; }
|
|
1182
|
+
.del-step-fail { color: var(--lamp-red); font-weight: 600; }
|
|
1183
|
+
.del-row-text { min-width: 0; overflow-wrap: anywhere; }
|
|
1184
|
+
.del-plate-foreign { background: var(--ink-2); }
|
|
1185
|
+
.del-plate-dirty, .del-plate-unmerged { background: var(--lamp-red); color: light-dark(#fff, #111613); }
|
|
1186
|
+
.del-options { margin: 0 0 var(--s-3); padding: 0; border: 0; min-width: 0; }
|
|
1187
|
+
.del-options legend { margin-bottom: var(--s-2); }
|
|
1188
|
+
.del-opt { display: flex; gap: 10px; align-items: flex-start; margin: var(--s-2) 0; }
|
|
1189
|
+
.del-opt input { margin-top: 4px; }
|
|
1190
|
+
.del-opt span { display: grid; gap: 2px; }
|
|
1191
|
+
.del-opt .muted { font-size: var(--t-sm); }
|
|
1192
|
+
.del-warnings:empty { display: none; }
|
|
1193
|
+
.del-warnings { margin: 0 0 var(--s-3); }
|
|
1194
|
+
.del-warning { display: flex; align-items: flex-start; gap: 7px; padding: 6px var(--s-2); border: 1px solid var(--rule-2); border-radius: var(--r-key); background: var(--warn-wash); color: var(--warn-ink); font-size: var(--t-sm); overflow-wrap: anywhere; }
|
|
1195
|
+
.del-warning-warn { border-color: var(--lamp-red); }
|
|
1196
|
+
.del-confirm { display: grid; gap: 6px; margin: 0 0 var(--s-3); }
|
|
1197
|
+
.del-confirm-label { font-size: var(--t-sm); }
|
|
1198
|
+
.del-confirm-input { width: 100%; min-height: 32px; padding: 0 var(--s-2); border: 1px solid var(--rule-2); border-radius: var(--r-key); background: var(--field); font-family: var(--mono); }
|
|
1199
|
+
.del-confirm-input:focus-visible { outline: 2px solid var(--lamp-red); outline-offset: 1px; }
|
|
1200
|
+
.del-confirm-input:disabled { opacity: .45; }
|
|
1201
|
+
.del-outcome { margin: 0 0 var(--s-3); padding: var(--s-2); border: 1px solid var(--rule-2); border-radius: var(--r-key); }
|
|
1202
|
+
.del-outcome-head { display: flex; align-items: flex-start; gap: 7px; margin: 0 0 var(--s-2); }
|
|
1203
|
+
.del-guidance { margin: var(--s-2) 0 0; font-weight: 600; overflow-wrap: anywhere; }
|
|
1204
|
+
.del-archive { margin: var(--s-2) 0 0; font-size: var(--t-xs); overflow-wrap: anywhere; }
|
|
1205
|
+
.del-actions { display: flex; justify-content: flex-end; gap: var(--s-2); margin-top: var(--s-3); }
|
|
1206
|
+
|
|
1207
|
+
/* ---------- settings (#/settings): registry, Jev and the repository's .forge files ---------- */
|
|
1208
|
+
.settings-view { display: grid; gap: var(--s-4); max-width: 1100px; }
|
|
1209
|
+
.set-head { display: flex; align-items: baseline; gap: var(--s-4); flex-wrap: wrap; }
|
|
1210
|
+
.set-lede { margin: 0; font-size: var(--t-sm); }
|
|
1211
|
+
.set-picker-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-3); }
|
|
1212
|
+
.set-picker select { min-width: 22em; max-width: 100%; }
|
|
1213
|
+
.set-field { display: inline-flex; align-items: center; }
|
|
1214
|
+
.set-repo-path, .set-why, .set-state-line, .set-file-status { display: flex; align-items: flex-start; gap: 7px; margin: var(--s-2) 0 0; font-size: var(--t-sm); overflow-wrap: anywhere; }
|
|
1215
|
+
.set-repo-path .lamp, .set-why .lamp, .set-state-line .lamp, .set-file-status .lamp { margin-top: .42em; }
|
|
1216
|
+
.set-amber { --st-fill: var(--lamp-amber); }
|
|
1217
|
+
.set-body { display: grid; gap: var(--s-4); }
|
|
1218
|
+
.set-section { display: grid; gap: var(--s-2); justify-items: start; padding: var(--s-4); border: 1px solid var(--rule); border-radius: var(--r-key); background: var(--module); }
|
|
1219
|
+
.set-section > * { max-width: 100%; margin-block: 0; }
|
|
1220
|
+
.set-path { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); }
|
|
1221
|
+
.set-path-label { font-family: var(--font-plate); font-weight: 700; font-size: .857rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
|
|
1222
|
+
.set-path .mono { overflow-wrap: anywhere; }
|
|
1223
|
+
.set-table-wrap { width: 100%; overflow-x: auto; border: 1px solid var(--rule); border-radius: var(--r-key); }
|
|
1224
|
+
.set-table th.set-alias { position: static; background: none; font-family: var(--mono); font-size: var(--t-sm); letter-spacing: 0; text-transform: none; color: var(--ink); }
|
|
1225
|
+
.set-switch-row { display: flex; align-items: center; gap: var(--s-3); }
|
|
1226
|
+
.set-state { font-weight: 600; }
|
|
1227
|
+
button.set-switch { gap: 8px; padding: 0 10px 0 4px; }
|
|
1228
|
+
.set-switch-knob { width: 26px; height: 14px; border-radius: 7px; background: var(--lamp-off); box-shadow: inset 0 0 0 1.5px var(--lamp-edge); position: relative; transition: background-color var(--relay); }
|
|
1229
|
+
.set-switch-knob::after { content: ''; position: absolute; top: 2px; left: 2px; width: 10px; height: 10px; border-radius: 50%; background: var(--key); box-shadow: 0 0 0 1px var(--lamp-edge); transition: translate var(--relay); }
|
|
1230
|
+
button.set-switch[aria-checked=true] .set-switch-knob { background: var(--lamp-green); }
|
|
1231
|
+
button.set-switch[aria-checked=true] .set-switch-knob::after { translate: 12px 0; }
|
|
1232
|
+
button.set-switch:focus-visible, .set-editor:focus-visible, .set-picker select:focus-visible { outline: 2px solid var(--route); outline-offset: 2px; }
|
|
1233
|
+
.set-file { display: grid; gap: var(--s-2); width: 100%; padding: var(--s-3); border: 1px solid var(--rule); border-radius: var(--r-key); background: var(--module-2); }
|
|
1234
|
+
.set-file > * { margin-block: 0; }
|
|
1235
|
+
.set-file-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; }
|
|
1236
|
+
.set-file-name { margin: 0; font-family: var(--mono); font-size: var(--t-base); }
|
|
1237
|
+
.set-file-status { margin: 0; }
|
|
1238
|
+
.set-file-blurb, .set-file-note { font-size: var(--t-sm); }
|
|
1239
|
+
.set-editor { width: 100%; padding: 8px 10px; min-height: 4.5em; resize: vertical; font-family: var(--mono); font-size: var(--t-sm); line-height: 1.5; tab-size: 4; white-space: pre; overflow: auto; }
|
|
1240
|
+
.set-editor[aria-invalid=true] { border-color: var(--lamp-red); }
|
|
1241
|
+
.set-problem { font-size: var(--t-sm); }
|
|
1242
|
+
.set-keys { display: flex; gap: var(--s-2); }
|
|
1243
|
+
button.set-remove { border-color: var(--lamp-red); color: var(--lamp-red); }
|
|
1244
|
+
button.set-remove:hover:not(:disabled) { background: var(--lamp-red); border-color: var(--lamp-red); color: light-dark(#fff, #111613); }
|
|
1245
|
+
button.set-remove:focus-visible { outline: 2px solid var(--lamp-red); outline-offset: 2px; }
|
|
1246
|
+
.set-pre { max-height: 220px; }
|
|
1247
|
+
.set-result:empty { display: none; }
|
|
1248
|
+
.set-result { width: 100%; }
|
|
1249
|
+
.set-env { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px var(--s-4); margin: 0; width: 100%; }
|
|
1250
|
+
.set-env dt, .set-env dd { margin: 0; overflow-wrap: anywhere; }
|
|
1251
|
+
.set-env dt { color: var(--ink-2); }
|
|
1252
|
+
.set-readonly { margin: 0; }
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* forge-ui shared write helpers: window.ForgeUI, and a CommonJS module under node:test. Every page that changes a run
|
|
2
|
+
uses these and none re-implements them.
|
|
3
|
+
el(tag, text?, className?) a DOM element; text enters as text
|
|
4
|
+
token() the write token from <meta name="forge-ui-token">, or null
|
|
5
|
+
controlsEnabled() Promise<bool>: a token AND /api/health says controls:true; asked once per page load
|
|
6
|
+
get(path) / post(path, body) Promise<{ok, status, data, error, detail, result}>; post sends the token and never retries
|
|
7
|
+
outcome(host, res, okText) an inline result line (a lamp and a sentence) in an aria-live region
|
|
8
|
+
confirmInline(host, text, {danger}) Promise<bool>: an inline Confirm / Cancel strip, Escape cancels
|
|
9
|
+
The write API is described in ui/docs/api.md, "Controls (write endpoints)". All API values enter the DOM as text. */
|
|
10
|
+
(function (root, factory) {
|
|
11
|
+
'use strict';
|
|
12
|
+
const api = factory();
|
|
13
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
14
|
+
else root.ForgeUI = api.create(root);
|
|
15
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const TOKEN_HEADER = 'X-Forge-UI-Token';
|
|
19
|
+
const TIMED_OUT = 'timed out: it may have partly completed. Reload to see the current state.';
|
|
20
|
+
|
|
21
|
+
// The fetch options for a request. A POST carries the token and a JSON body; nothing else ever does.
|
|
22
|
+
function requestInit(method, body, token) {
|
|
23
|
+
const init = { method, cache: 'no-store', credentials: 'same-origin', headers: {} };
|
|
24
|
+
if (method === 'POST') {
|
|
25
|
+
init.headers[TOKEN_HEADER] = token;
|
|
26
|
+
init.headers['Content-Type'] = 'application/json';
|
|
27
|
+
init.body = JSON.stringify(body === undefined ? {} : body);
|
|
28
|
+
}
|
|
29
|
+
return init;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const object = v => v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
33
|
+
|
|
34
|
+
// An HTTP answer as {ok, status, data, error, detail, result}. `status` 0 is "no answer" (the network, or no token).
|
|
35
|
+
// `result` is what the server passes through from the forge CLI's own stdout (an error body may hold it).
|
|
36
|
+
function mapResponse(status, data) {
|
|
37
|
+
const body = object(data);
|
|
38
|
+
if (status >= 200 && status < 300) {
|
|
39
|
+
if (body) return { ok: true, status, data: body, error: null, detail: null, result: object(body.result) };
|
|
40
|
+
return { ok: false, status, data: null, error: 'bad response', detail: 'the server answered without a JSON object', result: null };
|
|
41
|
+
}
|
|
42
|
+
const error = body && typeof body.error === 'string' ? body.error : status === 0 ? 'network' : 'http ' + status;
|
|
43
|
+
const detail = body && typeof body.detail === 'string' ? body.detail : status === 0 ? 'could not reach forge-ui' : null;
|
|
44
|
+
return { ok: false, status, data: body, error, detail, result: body ? object(body.result) : null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// What an inline result line says: {tone: 'ok' | 'bad', text}.
|
|
48
|
+
function outcomeText(res, okText) {
|
|
49
|
+
if (res && res.ok) return { tone: 'ok', text: okText || 'done' };
|
|
50
|
+
const r = res || {};
|
|
51
|
+
if (r.status === 409 && r.error === 'busy') return { tone: 'bad', text: 'busy, try again' };
|
|
52
|
+
if (r.status === 504 || r.error === 'timeout') return { tone: 'bad', text: TIMED_OUT };
|
|
53
|
+
if (r.status === 403) return { tone: 'bad', text: 'not allowed: reload the page (forge-ui may have restarted)' };
|
|
54
|
+
if (r.status === 405) return { tone: 'bad', text: 'this server is read-only' };
|
|
55
|
+
return { tone: 'bad', text: r.detail || r.error || 'failed' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The helpers over one environment ({document, fetch}); the page makes one over `window`, a test one over fakes.
|
|
59
|
+
function create(env) {
|
|
60
|
+
let enabled = null; // Promise<bool>, kept for the page load
|
|
61
|
+
const open = new WeakMap(); // host → the cancel of the confirm strip it holds
|
|
62
|
+
|
|
63
|
+
function el(tag, text, className) {
|
|
64
|
+
const node = env.document.createElement(tag);
|
|
65
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
66
|
+
if (className) node.className = className;
|
|
67
|
+
return node;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function token() {
|
|
71
|
+
const meta = env.document && env.document.querySelector('meta[name="forge-ui-token"]');
|
|
72
|
+
const value = meta && meta.getAttribute('content');
|
|
73
|
+
return value ? value : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function request(path, init) {
|
|
77
|
+
let response;
|
|
78
|
+
try { response = await env.fetch(path, init); } catch (_) { return mapResponse(0, null); }
|
|
79
|
+
let data = null;
|
|
80
|
+
try { data = await response.json(); } catch (_) { /* an empty or non-JSON body maps to the status alone */ }
|
|
81
|
+
return mapResponse(response.status, data);
|
|
82
|
+
}
|
|
83
|
+
const get = path => request(path, requestInit('GET'));
|
|
84
|
+
// Never retried: a write that timed out may have happened, and the caller says what to do next.
|
|
85
|
+
function post(path, body) {
|
|
86
|
+
const t = token();
|
|
87
|
+
if (!t) return Promise.resolve(mapResponse(0, { error: 'no token', detail: 'this page is read-only: it has no write token' }));
|
|
88
|
+
return request(path, requestInit('POST', body, t));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function controlsEnabled() {
|
|
92
|
+
if (enabled) return enabled;
|
|
93
|
+
if (!token()) return (enabled = Promise.resolve(false));
|
|
94
|
+
const asked = get('/api/health').then(res => {
|
|
95
|
+
if (res.status === 0) enabled = null; // the server did not answer: ask again next time
|
|
96
|
+
return res.ok && res.data.controls === true;
|
|
97
|
+
});
|
|
98
|
+
return (enabled = asked);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function paint(line, o, silent) {
|
|
102
|
+
line.className = 'ui-outcome ' + o.tone;
|
|
103
|
+
line.setAttribute('aria-live', silent ? 'off' : 'polite');
|
|
104
|
+
line.replaceChildren(el('i', undefined, 'lamp st-' + (o.tone === 'ok' ? 'pass' : 'fail')), el('span', o.text));
|
|
105
|
+
}
|
|
106
|
+
// A live region has to exist before its text changes to be announced, so a new one is filled on the next tick.
|
|
107
|
+
// `opts.silent` draws a result again without announcing it twice.
|
|
108
|
+
function outcome(host, res, okText, opts) {
|
|
109
|
+
const o = outcomeText(res, okText), silent = !!(opts && opts.silent);
|
|
110
|
+
let line = Array.from(host.children).find(c => c.classList.contains('ui-outcome'));
|
|
111
|
+
if (line) { paint(line, o, silent); return line; }
|
|
112
|
+
line = el('p', undefined, 'ui-outcome');
|
|
113
|
+
line.setAttribute('role', 'status');
|
|
114
|
+
line.setAttribute('aria-atomic', 'true');
|
|
115
|
+
line.setAttribute('aria-live', silent ? 'off' : 'polite');
|
|
116
|
+
host.append(line);
|
|
117
|
+
if (silent) paint(line, o, true); else setTimeout(() => paint(line, o, false), 0);
|
|
118
|
+
return line;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// One strip per host: asking again cancels the first. Escape and Cancel answer false, Confirm true. Focus goes to
|
|
122
|
+
// Confirm (to Cancel when `danger`, so a stray Enter cannot delete) and returns to where it was.
|
|
123
|
+
function confirmInline(host, text, opts) {
|
|
124
|
+
const danger = !!(opts && opts.danger);
|
|
125
|
+
if (open.has(host)) open.get(host)(false);
|
|
126
|
+
return new Promise(resolve => {
|
|
127
|
+
const returnTo = env.document.activeElement;
|
|
128
|
+
const strip = el('div', undefined, 'ui-confirm' + (danger ? ' danger' : ''));
|
|
129
|
+
strip.setAttribute('role', 'group');
|
|
130
|
+
strip.setAttribute('aria-label', String(text));
|
|
131
|
+
const yes = el('button', 'Confirm', danger ? 'ui-danger' : 'primary'), no = el('button', 'Cancel');
|
|
132
|
+
yes.type = 'button'; no.type = 'button';
|
|
133
|
+
strip.append(el('span', undefined, 'ui-confirm-lamp lamp ' + (danger ? 'st-fail' : 'lamp-route')), el('span', text, 'ui-confirm-text'), yes, no);
|
|
134
|
+
let done = false;
|
|
135
|
+
const finish = answer => {
|
|
136
|
+
if (done) return;
|
|
137
|
+
done = true;
|
|
138
|
+
open.delete(host);
|
|
139
|
+
const inside = strip.contains(env.document.activeElement);
|
|
140
|
+
strip.remove();
|
|
141
|
+
if (inside && returnTo && returnTo.isConnected && returnTo.focus) returnTo.focus({ preventScroll: true });
|
|
142
|
+
resolve(answer);
|
|
143
|
+
};
|
|
144
|
+
open.set(host, finish);
|
|
145
|
+
yes.addEventListener('click', () => finish(true));
|
|
146
|
+
no.addEventListener('click', () => finish(false));
|
|
147
|
+
strip.addEventListener('keydown', e => {
|
|
148
|
+
if (e.key !== 'Escape') return;
|
|
149
|
+
e.preventDefault(); e.stopPropagation(); // the page's own Escape handlers are not for this
|
|
150
|
+
finish(false);
|
|
151
|
+
});
|
|
152
|
+
host.append(strip);
|
|
153
|
+
(danger ? no : yes).focus({ preventScroll: true });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { el, token, controlsEnabled, get, post, outcome, confirmInline, requestInit, mapResponse, outcomeText };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { create, requestInit, mapResponse, outcomeText, TOKEN_HEADER };
|
|
161
|
+
});
|