@enigmax/dashboard 0.1.103 → 0.1.105

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.
Files changed (2) hide show
  1. package/assets/index.html +200 -73
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -517,6 +517,24 @@
517
517
  full grid width while it is showing and its controls wrap instead of squeezing the label. */
518
518
  #gateModelRow.wide { grid-column: 1 / -1; }
519
519
  #gateModelRow .set-ctl { flex-wrap: wrap; justify-content: flex-end; }
520
+ /* A row whose control and hint both need the full width of the settings grid. */
521
+ .set-row.span { grid-column: 1 / -1; }
522
+ /* What a run is doing right now: the step in flight and the tail of its log. */
523
+ .gate-act { margin-top: 10px; border-left: 2px solid var(--accent); padding-left: 11px; }
524
+ .gate-act-h { display: flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 12px; color: var(--text); }
525
+ .gate-act-dot {
526
+ width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
527
+ flex: none; animation: gate-pulse 1.4s ease-in-out infinite;
528
+ }
529
+ @keyframes gate-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
530
+ @media (prefers-reduced-motion: reduce) { .gate-act-dot { animation: none; } }
531
+ .gate-act-log {
532
+ margin: 6px 0 0; padding: 8px 10px; max-height: 168px; overflow: auto; overscroll-behavior: contain;
533
+ background: var(--surface2); border-radius: 7px;
534
+ font-family: var(--mono); font-size: 11.5px; line-height: 1.5; color: var(--muted);
535
+ white-space: pre-wrap; overflow-wrap: anywhere;
536
+ }
537
+ .gate-act-empty { margin-top: 6px; font-size: 12px; color: var(--muted); }
520
538
  .gate-run { padding: 13px 0; border-top: 1px solid var(--border-soft); }
521
539
  .gate-run:first-child { border-top: 0; padding-top: 4px; }
522
540
  .gate-run-h { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
@@ -1126,7 +1144,7 @@
1126
1144
  <div class="set-grid">
1127
1145
  <div class="set-row">
1128
1146
  <div class="set-meta"><div class="set-name">Agent</div><div class="set-hint" id="gateAgentHint">which coding agent the pipeline drives</div></div>
1129
- <div class="set-ctl ladd" style="align-items:center"><select id="gateAgent" class="linput"><option value="auto">auto</option><option value="claude">claude</option><option value="codex">codex</option><option value="opencode">opencode</option><option value="rovodev">rovodev</option><option value="pi">pi</option></select></div>
1147
+ <div class="set-ctl ladd" style="align-items:center"><select id="gateAgent" class="linput" data-dd-icons="agent"><option value="auto">Auto</option><option value="claude">Claude Code</option><option value="codex">Codex</option><option value="opencode">OpenCode</option><option value="rovodev">Rovo Dev</option><option value="pi">Pi</option></select></div>
1130
1148
  </div>
1131
1149
  <div class="set-row" id="gateModelRow">
1132
1150
  <div class="set-meta"><div class="set-name">Model</div><div class="set-hint" id="gateModelHint">the model every step runs on; a smaller one cuts run time and tokens</div></div>
@@ -1145,21 +1163,19 @@
1145
1163
  <div class="set-ctl ladd" style="align-items:center"><button id="gateIntent" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Read intent from transcripts"></button></div>
1146
1164
  </div>
1147
1165
  </div>
1166
+ <div class="set-cat-h" style="margin-top:18px">Findings</div>
1167
+ <div class="sub" style="margin-bottom:10px">Who decides what happens to something the review turns up.</div>
1168
+ <div class="set-grid">
1169
+ <div class="set-row span">
1170
+ <div class="set-meta"><div class="set-name">When the gate finds something</div><div class="set-hint" id="gateFixHint"></div></div>
1171
+ <div class="set-ctl ladd" style="align-items:center"><select id="gateFixPolicy" class="linput" style="flex:none;width:300px"></select></div>
1172
+ </div>
1173
+ </div>
1148
1174
  <div class="set-cat-h" style="margin-top:18px">Auto-fix attempts</div>
1149
1175
  <div class="sub" style="margin-bottom:10px">Follow-up rounds a step gets before it pauses for you. Every round is another full agent pass over the diff, so this is the setting that multiplies a run's cost. 0 pauses immediately.</div>
1150
1176
  <div class="set-grid" id="gateAutoFix"></div>
1151
1177
  </div>
1152
1178
 
1153
- <div class="panel">
1154
- <div class="set-cat-h">Global configuration</div>
1155
- <div class="sub" style="margin-bottom:10px">The global config as raw YAML, for the keys with no control above (agent paths, ACP targets, ignore patterns, evidence). Saved only when it parses, so a typo here never reaches the daemon.</div>
1156
- <textarea id="gateCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Global gate configuration"></textarea>
1157
- <div class="ladd" style="gap:8px;align-items:center">
1158
- <button id="gateCfgSave" type="button" class="toggle on" disabled title="No changes to save">Save</button>
1159
- <span id="gateCfgNote" class="set-hint"></span>
1160
- </div>
1161
- </div>
1162
-
1163
1179
  <div class="panel" id="gateRepoPanel" style="display:none">
1164
1180
  <div class="set-cat-h">Repository configuration</div>
1165
1181
  <div class="sub" style="margin-bottom:10px">Committed with the repository and merged over the global config. Only the fields the gate trusts from a branch take effect.</div>
@@ -3925,14 +3941,34 @@ export {
3925
3941
  claude: '<svg viewBox="0 0 100 100" fill="currentColor" aria-hidden="true"><path d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"/></svg>',
3926
3942
  codex: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>',
3927
3943
  opencode: '<svg viewBox="0 0 240 300" fill="currentColor" aria-hidden="true"><path d="M180 240H60V120H180V240Z" opacity=".55"/><path fill-rule="evenodd" d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z"/></svg>',
3944
+ // Rovo Dev is Atlassian's agent (it is driven through `acli rovodev`), so it carries
3945
+ // the Atlassian mark - there is no separate Rovo Dev glyph to use.
3946
+ rovodev: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.12 11.084a.683.683 0 00-1.16.126L.075 22.974a.703.703 0 00.63 1.018h8.19a.678.678 0 00.63-.39c1.767-3.65.696-9.203-2.406-12.52zM11.434.386a15.515 15.515 0 00-.906 15.317l3.95 7.9a.703.703 0 00.628.388h8.19a.703.703 0 00.63-1.017L12.63.38a.664.664 0 00-1.196.006z"/></svg>',
3947
+ // Pi's own mark (pi.dev), already monochrome upstream - it ships a light/dark pair,
3948
+ // which is exactly what currentColor gives us here.
3949
+ pi: '<svg viewBox="0 0 800 800" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/><path d="M517.36 400H634.72V634.72H517.36Z"/></svg>',
3950
+ };
3951
+ // Agent id -> the name a person would write. A selector must never show the raw id: the
3952
+ // config stores `opencode` and `rovodev`, the reader sees OpenCode and Rovo Dev.
3953
+ const AGENT_LABELS = {
3954
+ auto: "Auto", claude: "Claude Code", codex: "Codex",
3955
+ opencode: "OpenCode", rovodev: "Rovo Dev", pi: "Pi"
3928
3956
  };
3929
- // App label -> internal name. Normalized (substring match) so every label variant maps
3930
- // to an icon: "Claude Code", "Codex"/"OpenAI Codex", "opencode"/"OpenCode".
3957
+ // App label -> internal name. Normalized so every label variant maps to an icon:
3958
+ // "Claude Code", "Codex"/"OpenAI Codex", "opencode"/"OpenCode", "Rovo Dev", "Pi".
3959
+ // Ordered longest-distinctive-first, and `pi` is matched exactly rather than as a
3960
+ // substring, which would otherwise claim anything containing those two letters.
3931
3961
  const agentName = (label) => {
3932
- const s = String(label || "").toLowerCase();
3933
- return s.includes("claude") ? "claude" : s.includes("opencode") ? "opencode" : s.includes("codex") ? "codex" : "";
3962
+ const s = String(label || "").trim().toLowerCase();
3963
+ if (s === "pi") return "pi";
3964
+ return s.includes("claude") ? "claude"
3965
+ : s.includes("opencode") ? "opencode"
3966
+ : s.includes("codex") ? "codex"
3967
+ : s.includes("rovo") ? "rovodev" : "";
3934
3968
  };
3935
3969
  const agentSvg = (name) => AGENT_ICONS[name] || "";
3970
+ /** The display name for an agent id, falling back to the id when it is not one we know. */
3971
+ const agentLabel = (name) => AGENT_LABELS[agentName(name) || name] || String(name || "");
3936
3972
 
3937
3973
  // Compression sources -> a brand icon. Reuses the app icons; the CLI source is enigma itself
3938
3974
  // (the monochrome cube mark, recoloured to currentColor so it sits uniformly in the tables).
@@ -4389,10 +4425,17 @@ export {
4389
4425
  }
4390
4426
 
4391
4427
  // --- quality gate subpage ---------------------------------------------------
4392
- // Two halves with independent availability: the YAML configs are plain files and
4393
- // always render, while the run history needs enigma's own binary (the gate database
4394
- // is Bun-only), so an unavailable history reports itself instead of blanking the page.
4395
- const gateSaved = { global: "", repo: "" };
4428
+ // Two halves with independent availability: the config is a plain file and always
4429
+ // renders, while the run history needs enigma's own binary (the gate database is
4430
+ // Bun-only), so an unavailable history reports itself instead of blanking the page.
4431
+ //
4432
+ // The GLOBAL config has no raw YAML editor: every setting it holds has a control in
4433
+ // the panel above, and a textarea of YAML is not a setting anyone should have to edit
4434
+ // to change how the gate behaves. The keys with no control (agent path overrides, ACP
4435
+ // targets, evidence) stay in the file, whose path the info dialog gives you. The
4436
+ // REPOSITORY config keeps its editor - it is committed with the repo, carries keys the
4437
+ // global one does not, and has no structured equivalent yet.
4438
+ let gateRepoSaved = "";
4396
4439
  let gateWritable = true;
4397
4440
  // Where the gate keeps its files, kept for the info dialog instead of printed in the headers.
4398
4441
  let gateWhere = { global: "", repo: "", root: "", note: "" };
@@ -4401,7 +4444,7 @@ export {
4401
4444
  function openGateInfo() {
4402
4445
  openInfoModal({
4403
4446
  title: "Gate configuration",
4404
- intro: "Where the gate reads its settings and keeps its run history.",
4447
+ intro: "Where the gate reads its settings and keeps its run history. Everything on this page writes to the global config; edit the file directly only for the advanced keys with no control here.",
4405
4448
  rows: [
4406
4449
  { label: "Global config", value: gateWhere.global, hint: "applies to every repository" },
4407
4450
  { label: "Repository config", value: gateWhere.repo, hint: "committed with the repository, merged over the global config" },
@@ -4414,15 +4457,9 @@ export {
4414
4457
  ],
4415
4458
  });
4416
4459
  }
4417
- function gateEls(which) {
4418
- return which === "global"
4419
- ? { area: $("gateCfg"), btn: $("gateCfgSave"), note: "gateCfgNote" }
4420
- : { area: $("gateRepoCfg"), btn: $("gateRepoCfgSave"), note: "gateRepoCfgNote" };
4421
- }
4422
4460
  // Dirty means different from what was loaded, so an edit typed and undone disables Save again.
4423
- function syncGateSave(which) {
4424
- const { area, btn } = gateEls(which);
4425
- const dirty = area.value !== gateSaved[which];
4461
+ function syncGateSave() {
4462
+ const btn = $("gateRepoCfgSave"), dirty = $("gateRepoCfg").value !== gateRepoSaved;
4426
4463
  btn.disabled = !dirty || !gateWritable;
4427
4464
  btn.title = !gateWritable ? "Read-only in this runtime" : dirty ? "Save this configuration" : "No changes to save";
4428
4465
  }
@@ -4452,6 +4489,29 @@ export {
4452
4489
  + (s.findings ? " · " + s.findings : "") + "</span>";
4453
4490
  }).join("");
4454
4491
  }
4492
+ // What the run is doing, in the words the status codes stand for. A step name alone
4493
+ // does not say whether the pipeline is working or waiting, and that is the difference
4494
+ // between "leave it alone" and "it needs you".
4495
+ const GATE_ACT_VERB = {
4496
+ running: "Running", fixing: "Fixing findings in",
4497
+ awaiting_approval: "Waiting for a decision on", fix_review: "Waiting on the fix review of"
4498
+ };
4499
+ /** The live header of a run's activity: what it is on and for how long. */
4500
+ function gateActHead(a) {
4501
+ const el = gateStepDur({ status: a.status, startedAt: a.startedAt, durationMs: null });
4502
+ return '<span class="gate-act-dot"></span>' + esc((GATE_ACT_VERB[a.status] || "Running") + " " + a.step)
4503
+ + (el ? " · " + esc(el) : "");
4504
+ }
4505
+ // The tail of the step's log, which is the only place a run says what it is actually
4506
+ // doing. Refreshed by the poll, not the 1s tick: the file only changes server-side.
4507
+ function gateActivityHtml(a) {
4508
+ if (!a) return "";
4509
+ const tail = (a.tail || []).length
4510
+ ? '<pre class="gate-act-log">' + esc((a.tail || []).join("\n")) + "</pre>"
4511
+ : '<div class="gate-act-empty">No output from this step yet.</div>';
4512
+ return '<div class="gate-act"><div class="gate-act-h">' + gateActHead(a) + "</div>" + tail + "</div>";
4513
+ }
4514
+ const STOP_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
4455
4515
  function gateRunRow(r) {
4456
4516
  const pr = r.prUrl ? ' <a href="' + esc(r.prUrl) + '" target="_blank" rel="noopener noreferrer">PR</a>' : "";
4457
4517
  const steps = gateStepsHtml(r.steps);
@@ -4461,11 +4521,19 @@ export {
4461
4521
  + (f.file ? " · " + esc(f.file) : "") + "<br>" + esc(f.description) + "</div>").join("") + "</div>"
4462
4522
  : "";
4463
4523
  const err = r.error ? '<div class="gate-find-row" style="color:var(--bad)">' + esc(r.error) + "</div>" : "";
4524
+ // Only a run that can still be stopped gets the control; a finished one would
4525
+ // offer an action that does nothing.
4526
+ const stop = r.live
4527
+ ? '<button type="button" class="iconbtn" data-gate-stop="' + esc(r.id) + '" data-gate-branch="' + esc(r.branch) + '"'
4528
+ + ' title="Stop this run" aria-label="Stop the run on ' + esc(r.branch) + '">' + STOP_ICON + "</button>"
4529
+ : "";
4464
4530
  return '<div class="gate-run"><div class="gate-run-h">'
4465
4531
  + '<span class="gate-run-b">' + esc(r.branch) + "</span>"
4466
4532
  + '<span class="cstatus ' + (r.status === "completed" ? "ok" : r.status === "running" ? "orange" : r.status === "failed" ? "bad" : "") + '"><span class="cdot"></span>' + esc(r.status) + (r.awaitingAgent ? " · parked" : "") + "</span>"
4467
4533
  + '<span class="gate-run-meta">' + esc(r.headSha) + pr + "</span>"
4468
- + "</div>" + '<div class="gate-steps">' + steps + "</div>" + findings + err + "</div>";
4534
+ + '<span style="flex:1"></span>' + stop
4535
+ + "</div>" + '<div class="gate-steps">' + steps + "</div>"
4536
+ + gateActivityHtml(r.activity) + findings + err + "</div>";
4469
4537
  }
4470
4538
  // The two on/off switches at the top of the page. Both are optimistic: the switch flips
4471
4539
  // first and rolls back if the server refuses, like every other control here.
@@ -4504,6 +4572,7 @@ export {
4504
4572
  function gateCtlFor(key) {
4505
4573
  if (key.indexOf("auto_fix.") === 0) return document.querySelector('[data-gate-af="' + key.slice("auto_fix.".length) + '"]');
4506
4574
  if (key === "agent") return $("gateAgent");
4575
+ if (key === "fix_policy") return $("gateFixPolicy");
4507
4576
  if (key === "log_level") return $("gateLogLevel");
4508
4577
  if (key === "ci_timeout") return $("gateCiTimeout");
4509
4578
  if (key === "intent.enabled") return $("gateIntent");
@@ -4594,12 +4663,36 @@ export {
4594
4663
  btn.title = dirty ? "" : "No changes to save";
4595
4664
  }
4596
4665
 
4666
+ // Who settles a finding the pipeline parks on. The middle option is the default and the
4667
+ // one worth reading: it is what makes the run stop only for a decision a person has to
4668
+ // make, instead of for every mechanical nit.
4669
+ const GATE_FIX_POLICIES = [
4670
+ ["auto", "Fix without asking", "the agent fixes everything the review turns up and never checks back"],
4671
+ ["assisted", "Fix, and ask about the rest", "the agent fixes what the review found and asks you only when the answer is not already yes: the finding contradicts what you asked for, undoes a deliberate decision, changes agreed behavior, or takes a very large change"],
4672
+ ["ask", "Always ask", "nothing is fixed until you say so - every finding comes back to you first"]
4673
+ ];
4674
+ /** The hint follows the selection: each policy hands the decision to someone different. */
4675
+ function syncGateFixHint() {
4676
+ const cur = $("gateFixPolicy").value;
4677
+ const row = GATE_FIX_POLICIES.find(([v]) => v === cur);
4678
+ const def = gateDefaults && gateDefaults.fixPolicy;
4679
+ const suffix = def && def === cur ? ' <span class="set-def">default</span>' : "";
4680
+ $("gateFixHint").innerHTML = esc(row ? row[2] : "") + suffix;
4681
+ }
4682
+ function renderGateFixPolicy(value) {
4683
+ const sel = $("gateFixPolicy");
4684
+ sel.innerHTML = GATE_FIX_POLICIES.map(([v, label]) => '<option value="' + esc(v) + '">' + esc(label) + "</option>").join("");
4685
+ sel.value = GATE_FIX_POLICIES.some(([v]) => v === value) ? value : "assisted";
4686
+ syncDD(sel);
4687
+ syncGateFixHint();
4688
+ }
4689
+
4597
4690
  function renderGateSettings(s) {
4598
4691
  const panel = $("gateSetPanel");
4599
4692
  if (!s) { panel.style.display = "none"; return; }
4600
4693
  panel.style.display = "";
4601
- // An `acp:<target>` agent set from the raw editor is not one of the fixed options;
4602
- // carry it as one so the select shows the truth instead of going blank.
4694
+ // An `acp:<target>` agent set in the config file by hand is not one of the fixed
4695
+ // options; carry it as one so the select shows the truth instead of going blank.
4603
4696
  const agentSel = $("gateAgent");
4604
4697
  if (!Array.prototype.some.call(agentSel.options, (o) => o.value === s.agent)) {
4605
4698
  agentSel.add(new Option(s.agent, s.agent));
@@ -4616,6 +4709,7 @@ export {
4616
4709
  syncDD($("gateLogLevel"));
4617
4710
  gateBool($("gateIntent"), s.intentEnabled);
4618
4711
  gateDefaults = s.defaults || null;
4712
+ renderGateFixPolicy(s.fixPolicy || "assisted"); // after gateDefaults: the hint reads it
4619
4713
  $("gateAutoFix").innerHTML = GATE_AUTOFIX.map(([step, hint]) => {
4620
4714
  const def = gateDefaults && gateDefaults.autoFix ? Number(gateDefaults.autoFix[step] || 0) : null;
4621
4715
  return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(step) + '</div><div class="set-hint">' + esc(hint)
@@ -4627,6 +4721,7 @@ export {
4627
4721
  // because only the payload knows what each default actually is.
4628
4722
  if (gateDefaults) {
4629
4723
  mountGateReset("agent", "the agent", gateDefaults.agent);
4724
+ mountGateReset("fix_policy", "the findings policy", gateDefaults.fixPolicy);
4630
4725
  mountGateReset("log_level", "the log level", gateDefaults.logLevel);
4631
4726
  mountGateReset("ci_timeout", "the CI timeout", gateDefaults.ciTimeout);
4632
4727
  mountGateReset("intent.enabled", "intent extraction", String(gateDefaults.intentEnabled));
@@ -4667,31 +4762,27 @@ export {
4667
4762
  root: g.root || "",
4668
4763
  note: g.canWrite === false ? (g.writeNote || "") : "",
4669
4764
  };
4670
- gateSaved.global = (g.globalConfig || {}).text || "";
4671
- $("gateCfg").value = gateSaved.global;
4672
4765
  // A runtime that cannot parse a gate config must not write one: read-only, with the reason.
4673
4766
  gateWritable = g.canWrite !== false;
4674
- $("gateCfg").readOnly = !gateWritable;
4675
4767
  $("gateRepoCfg").readOnly = !gateWritable;
4676
- if (!gateWritable) { setNote("gateCfgNote", g.writeNote || "Read-only here.", "err"); setNote("gateRepoCfgNote", g.writeNote || "Read-only here.", "err"); }
4677
- syncGateSave("global");
4768
+ if (!gateWritable) setNote("gateNote", g.writeNote || "Read-only here.", "err");
4678
4769
  renderGateSettings(g.settings);
4679
4770
  $("gateRepoPanel").style.display = repo ? "" : "none";
4680
4771
  if (repo) {
4681
- gateSaved.repo = repo.text || "";
4682
- $("gateRepoCfg").value = gateSaved.repo;
4683
- syncGateSave("repo");
4772
+ gateRepoSaved = repo.text || "";
4773
+ $("gateRepoCfg").value = gateRepoSaved;
4774
+ syncGateSave();
4684
4775
  }
4685
4776
  }
4686
4777
 
4687
4778
  // --- keeping the run strip live -------------------------------------------------
4688
4779
  // A gate run takes tens of minutes and the page has no push channel, so without this the
4689
- // strip shows whatever was true when the page was opened. Two loops: a 5s refetch while
4780
+ // view shows whatever was true when the page was opened. Two loops: a 3s refetch while
4690
4781
  // something is in flight (20s when idle) and a 1s repaint so the active step's elapsed
4691
4782
  // counts up between fetches. Both stop when the tab is hidden or the view is not open.
4692
4783
  let gateRuns = [], gateFetchTimer = null, gateTickTimer = null;
4693
4784
  function gateBusy() {
4694
- return gateRuns.some((r) => r.status === "running" || r.status === "awaiting_approval");
4785
+ return gateRuns.some((r) => r.live);
4695
4786
  }
4696
4787
  function gateStopPoll() {
4697
4788
  clearTimeout(gateFetchTimer); gateFetchTimer = null;
@@ -4703,13 +4794,20 @@ export {
4703
4794
  if (activeView !== "gate" || document.hidden) return;
4704
4795
  const busy = gateBusy();
4705
4796
  $("gateLive").textContent = busy ? "live" : "";
4706
- gateFetchTimer = setTimeout(() => loadGate(true), busy ? 5000 : 20000);
4797
+ gateFetchTimer = setTimeout(() => loadGate(true), busy ? 3000 : 20000);
4707
4798
  if (!busy) return;
4708
- // Repaint only the step strips: replacing whole rows every second would drop a text
4709
- // selection inside the findings of a parked run.
4799
+ // Repaint only the elapsed counters: replacing whole rows every second would drop a
4800
+ // text selection inside a parked run's findings or a scroll position in its log.
4710
4801
  gateTickTimer = setInterval(() => {
4711
- const strips = $("gateRuns").querySelectorAll(".gate-steps");
4712
- strips.forEach((el, i) => { if (gateRuns[i]) el.innerHTML = gateStepsHtml(gateRuns[i].steps); });
4802
+ const rows = $("gateRuns").querySelectorAll(".gate-run");
4803
+ rows.forEach((row, i) => {
4804
+ const r = gateRuns[i];
4805
+ if (!r) return;
4806
+ const strip = row.querySelector(".gate-steps");
4807
+ if (strip) strip.innerHTML = gateStepsHtml(r.steps);
4808
+ const head = row.querySelector(".gate-act-h");
4809
+ if (head && r.activity) head.innerHTML = gateActHead(r.activity);
4810
+ });
4713
4811
  }, 1000);
4714
4812
  }
4715
4813
  async function loadGate(live) {
@@ -4725,56 +4823,80 @@ export {
4725
4823
  } catch { note("Could not reach the server."); }
4726
4824
  }
4727
4825
  document.addEventListener("visibilitychange", () => { if (activeView === "gate") { if (document.hidden) gateStopPoll(); else loadGate(true); } });
4728
- async function saveGateCfg(which) {
4729
- const { area, note } = gateEls(which);
4730
- if (area.value === gateSaved[which]) return; // no-op: nothing changed
4731
- setNote(note, "Saving...");
4732
- const payload = { scope: which === "global" ? "global" : "repo", text: area.value };
4733
- if (which === "repo") payload.path = currentScope;
4826
+ async function saveGateRepoCfg() {
4827
+ const area = $("gateRepoCfg");
4828
+ if (area.value === gateRepoSaved) return; // no-op: nothing changed
4829
+ setNote("gateRepoCfgNote", "Saving...");
4734
4830
  try {
4735
- const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) })).json();
4736
- setNote(note, out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
4737
- if (out.ok) { gateSaved[which] = area.value; syncGateSave(which); }
4738
- } catch { setNote(note, "Could not reach the server.", "err"); }
4831
+ const body = JSON.stringify({ scope: "repo", text: area.value, path: currentScope });
4832
+ const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body })).json();
4833
+ setNote("gateRepoCfgNote", out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
4834
+ if (out.ok) { gateRepoSaved = area.value; syncGateSave(); }
4835
+ } catch { setNote("gateRepoCfgNote", "Could not reach the server.", "err"); }
4836
+ }
4837
+ /**
4838
+ * Stops an in-flight run. Destructive and not undoable - the pipeline drops whatever it
4839
+ * was doing - so it confirms first, naming the branch rather than the run id.
4840
+ */
4841
+ async function stopGateRun(runId, branch) {
4842
+ const ok = await openModal({
4843
+ title: "Stop this run?",
4844
+ message: "The gate run on " + branch + " stops where it is. Nothing it already pushed is undone, and you can start a new run afterwards.",
4845
+ danger: true,
4846
+ okLabel: "Stop run"
4847
+ }).then(Boolean);
4848
+ if (!ok) return;
4849
+ setNote("gateNote", "Stopping the run...");
4850
+ try {
4851
+ const body = JSON.stringify({ action: "abort", run: runId });
4852
+ const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body })).json();
4853
+ setNote("gateNote", out.message || (out.ok ? "Run stopped." : "Not stopped."), out.ok ? "ok" : "err");
4854
+ } catch { setNote("gateNote", "Could not reach the server.", "err"); }
4855
+ await loadGate(true);
4739
4856
  }
4740
4857
  /** One form control -> one key of the global config. Reloads so the file stays the truth. */
4741
4858
  async function saveGateSetting(key, value, label) {
4742
- setNote("gateCfgNote", "Saving " + label + "...");
4859
+ setNote("gateNote", "Saving " + label + "...");
4743
4860
  try {
4744
4861
  const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "setting", key, value }) })).json();
4745
- setNote("gateCfgNote", out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
4862
+ setNote("gateNote", out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
4746
4863
  await loadGate();
4747
- } catch { setNote("gateCfgNote", "Could not reach the server.", "err"); await loadGate(); }
4864
+ } catch { setNote("gateNote", "Could not reach the server.", "err"); await loadGate(); }
4748
4865
  }
4749
4866
  async function gateDaemonPost(on) {
4750
4867
  gateBool($("gateDaemonBtn"), on); // optimistic; the reload below is the truth
4751
- setNote("gateCfgNote", on ? "Starting the daemon..." : "Stopping the daemon...");
4868
+ setNote("gateNote", on ? "Starting the daemon..." : "Stopping the daemon...");
4752
4869
  try {
4753
4870
  const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "daemon", on }) })).json();
4754
- setNote("gateCfgNote", out.message || (out.ok ? "Done." : "Failed."), out.ok ? "ok" : "err");
4755
- } catch { setNote("gateCfgNote", "Could not reach the server.", "err"); }
4871
+ setNote("gateNote", out.message || (out.ok ? "Done." : "Failed."), out.ok ? "ok" : "err");
4872
+ } catch { setNote("gateNote", "Could not reach the server.", "err"); }
4756
4873
  await loadGate();
4757
4874
  }
4758
4875
  // The gate on/off flag is a normal enigma setting, so it goes through the settings route
4759
4876
  // the Settings page uses rather than growing a second writer for the same key.
4760
4877
  async function gateOnPost(on) {
4761
4878
  gateBool($("gateOnBtn"), on);
4762
- setNote("gateCfgNote", on ? "Turning the gate on..." : "Turning the gate off...");
4879
+ setNote("gateNote", on ? "Turning the gate on..." : "Turning the gate off...");
4763
4880
  try {
4764
4881
  const out = await (await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "gate", value: on }) })).json();
4765
- setNote("gateCfgNote", out.ok ? (out.restartNote || "Saved. Restart your agent to apply.") : ("Could not change the gate: " + (out.error || "error")), out.ok ? "warn" : "err");
4766
- } catch { setNote("gateCfgNote", "Could not reach the server.", "err"); }
4882
+ setNote("gateNote", out.ok ? (out.restartNote || "Saved. Restart your agent to apply.") : ("Could not change the gate: " + (out.error || "error")), out.ok ? "warn" : "err");
4883
+ } catch { setNote("gateNote", "Could not reach the server.", "err"); }
4767
4884
  await loadGate();
4768
4885
  }
4769
4886
  $("gateRefresh").addEventListener("click", () => loadGate());
4770
4887
  $("gateInfo").addEventListener("click", openGateInfo);
4771
- $("gateCfg").addEventListener("input", () => syncGateSave("global"));
4772
- $("gateRepoCfg").addEventListener("input", () => syncGateSave("repo"));
4773
- $("gateCfgSave").addEventListener("click", () => saveGateCfg("global"));
4774
- $("gateRepoCfgSave").addEventListener("click", () => saveGateCfg("repo"));
4888
+ $("gateRepoCfg").addEventListener("input", syncGateSave);
4889
+ $("gateRepoCfgSave").addEventListener("click", saveGateRepoCfg);
4775
4890
  $("gateOnBtn").addEventListener("click", () => gateOnPost($("gateOnBtn").dataset.on !== "1"));
4776
4891
  $("gateDaemonBtn").addEventListener("click", () => gateDaemonPost($("gateDaemonBtn").dataset.on !== "1"));
4892
+ // Delegated: the run rows are rebuilt on every poll, so a per-button listener would be
4893
+ // rebound (or lost) three times a minute.
4894
+ $("gateRuns").addEventListener("click", (e) => {
4895
+ const b = e.target.closest("[data-gate-stop]");
4896
+ if (b) stopGateRun(b.dataset.gateStop, b.dataset.gateBranch);
4897
+ });
4777
4898
  $("gateAgent").addEventListener("change", (e) => saveGateSetting("agent", e.target.value, "the agent"));
4899
+ $("gateFixPolicy").addEventListener("change", (e) => { syncGateFixHint(); saveGateSetting("fix_policy", e.target.value, "the findings policy"); });
4778
4900
  $("gateLogLevel").addEventListener("change", (e) => saveGateSetting("log_level", e.target.value, "the log level"));
4779
4901
  $("gateIntent").addEventListener("click", () => saveGateSetting("intent.enabled", $("gateIntent").dataset.on !== "1", "intent extraction"));
4780
4902
  // A listed model commits on pick, like the agent and log-level selects; "Custom" reveals
@@ -4806,6 +4928,7 @@ export {
4806
4928
  if (el) {
4807
4929
  if (key === "intent.enabled") gateBool(el, def === "true");
4808
4930
  else { el.value = def; if (el.tagName === "SELECT") syncDD(el); }
4931
+ if (key === "fix_policy") syncGateFixHint(); // the hint follows the selection
4809
4932
  syncGateResets();
4810
4933
  }
4811
4934
  const value = key.indexOf("auto_fix.") === 0 ? Number(def) : (key === "intent.enabled" ? def === "true" : def);
@@ -5485,16 +5608,20 @@ export {
5485
5608
  async function loadPlayground() {
5486
5609
  let info = { agents: [], models: [], apiPort: 8000 };
5487
5610
  try { info = await (await fetch("/api/playground", { cache: "no-store" })).json(); } catch { /* keep defaults */ }
5488
- // Populate the backend/model dropdown from the installed agents' model lists.
5611
+ // Populate the backend/model dropdown from the installed agents' model lists. The
5612
+ // option VALUE is the model id, so the agent logo has to come from the owning tool
5613
+ // via data-icon - `data-dd-icons="agent"` alone would look the id up and find nothing.
5489
5614
  const sel = $("pgModel");
5490
5615
  const opts = [];
5491
- (info.models || []).forEach((m) => (m.models || []).forEach((id) => opts.push('<option value="' + esc(id) + '">' + esc(id) + " (" + esc(m.tool) + ")</option>")));
5492
- if (!opts.length) opts.push('<option value="claude">claude (default)</option>');
5616
+ (info.models || []).forEach((m) => (m.models || []).forEach((id) => opts.push(
5617
+ '<option value="' + esc(id) + '" data-icon="' + esc(agentName(m.tool)) + '">' + esc(id) + " (" + esc(agentLabel(m.tool)) + ")</option>")));
5618
+ if (!opts.length) opts.push('<option value="claude" data-icon="claude">Claude Code (default)</option>');
5493
5619
  sel.innerHTML = opts.join("");
5494
5620
  syncDD(sel);
5495
5621
  // Context selectors: account (labeled by tool), profile, pack.
5496
5622
  const acc = $("pgAccount");
5497
- acc.innerHTML = '<option value="">(active)</option>' + (info.accounts || []).map((a) => '<option value="' + esc(a.name) + '">' + esc(a.name) + " (" + esc(a.tool) + ")</option>").join("");
5623
+ acc.innerHTML = '<option value="">(active)</option>' + (info.accounts || []).map((a) =>
5624
+ '<option value="' + esc(a.name) + '" data-icon="' + esc(agentName(a.tool)) + '">' + esc(a.name) + " (" + esc(agentLabel(a.tool)) + ")</option>").join("");
5498
5625
  syncDD(acc);
5499
5626
  const prof = $("pgProfile");
5500
5627
  prof.innerHTML = '<option value="">(none)</option>' + (info.profiles || []).map((n) => '<option value="' + esc(n) + '">' + esc(n) + "</option>").join("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.103",
3
+ "version": "0.1.105",
4
4
  "description": "Local browser dashboard UI for enigma: the static page and chart assets enigma serves on its loopback dashboard (savings, real tool usage, in-browser settings). Installed on demand by enigma-cli; not a runtime dependency.",
5
5
  "type": "module",
6
6
  "files": [