@enigmax/dashboard 0.1.97 → 0.1.99

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 +186 -23
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -128,6 +128,21 @@
128
128
  .card .value.accent { color: var(--accent); }
129
129
  .card .value.good { color: var(--good); }
130
130
  .card .foot { position: relative; color: var(--muted); font-size: 12px; margin-top: 5px; }
131
+ /* Gate status strip. Two booleans and a path do not each need a 30px stat card:
132
+ one surface, three inline items (frontend-policy, Visual Hierarchy). */
133
+ .gate-bar {
134
+ display: flex; flex-wrap: wrap; align-items: center; gap: 12px 28px;
135
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
136
+ padding: 13px 18px; margin-bottom: 18px; box-shadow: var(--shadow-1);
137
+ }
138
+ .gate-stat { display: flex; align-items: center; gap: 10px; min-width: 0; }
139
+ .gate-stat-t { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
140
+ .gate-stat-n { font-family: var(--mono); color: var(--muted); font-size: 10.5px; text-transform: uppercase; letter-spacing: .8px; }
141
+ .gate-stat-v { font-family: var(--mono); font-size: 13px; font-weight: 600; color: var(--text); }
142
+ .gate-stat-v.good { color: var(--good); }
143
+ /* Takes the remaining width so the path truncates instead of wrapping the strip. */
144
+ .gate-stat-scope { flex: 1 1 220px; min-width: 0; }
145
+ .gate-stat-f { min-width: 0; font-size: 11.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
131
146
  .panel { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; margin-bottom: 18px; box-shadow: var(--shadow-1); }
132
147
  .panel-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
133
148
  /* Section heading: a mono eyebrow led by a short accent rule (structure as signal). */
@@ -185,8 +200,6 @@
185
200
  .set-cat { margin-bottom: 18px; }
186
201
  .set-cat:last-child { margin-bottom: 0; }
187
202
  .set-cat-h { font-family: var(--mono); color: var(--accent); font-size: 12px; text-transform: uppercase; letter-spacing: 0.7px; margin-bottom: 4px; }
188
- /* A path in a header is a literal: uppercasing it prints something that does not exist. */
189
- .set-cat-h code { text-transform: none; letter-spacing: 0; }
190
203
  .set-cat-b { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
191
204
  .set-row { display: flex; align-items: center; gap: 14px; padding: 9px 0; border-bottom: 1px solid var(--surface2); }
192
205
  .set-row:last-child { border-bottom: none; }
@@ -495,6 +508,11 @@
495
508
  width: 100%; min-height: 300px; font-family: var(--mono); font-size: 12.5px; line-height: 1.55;
496
509
  resize: vertical; white-space: pre; overflow-wrap: normal; overflow-x: auto; tab-size: 2;
497
510
  }
511
+ /* The model row grows a text field and a Save when "Custom" is picked. That cluster does
512
+ not fit a grid column beside its label (`.set-ctl` never shrinks), so the row takes the
513
+ full grid width while it is showing and its controls wrap instead of squeezing the label. */
514
+ #gateModelRow.wide { grid-column: 1 / -1; }
515
+ #gateModelRow .set-ctl { flex-wrap: wrap; justify-content: flex-end; }
498
516
  .gate-run { padding: 13px 0; border-top: 1px solid var(--border-soft); }
499
517
  .gate-run:first-child { border-top: 0; padding-top: 4px; }
500
518
  .gate-run-h { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
@@ -579,6 +597,17 @@
579
597
  }
580
598
  .modal-back.show .modal { transform: none; opacity: 1; }
581
599
  .modal.wide { max-width: 900px; }
600
+ /* Info dialog: the paths and links a panel would otherwise print in its header. */
601
+ .modal.info { max-width: 560px; }
602
+ .info-row { padding: 10px 0; border-top: 1px solid var(--surface2); }
603
+ .info-row:first-of-type { border-top: none; }
604
+ .info-k { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
605
+ .info-v { display: flex; align-items: center; gap: 8px; }
606
+ /* A path is a literal that must stay readable: it wraps rather than overflowing the dialog. */
607
+ .info-v code { flex: 1; min-width: 0; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; }
608
+ .info-links { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 16px; }
609
+ .info-links a { color: var(--accent); font-size: 12px; text-decoration: none; }
610
+ .info-links a:hover { text-decoration: underline; }
582
611
  /* Command palette: the same backdrop and surface as every other dialog, sat in the top
583
612
  third so the result list grows downwards without moving the input under the caret. */
584
613
  /* The palette lives in the markup instead of being created per open, so it needs this:
@@ -1060,18 +1089,22 @@
1060
1089
  <h1>Quality gate</h1>
1061
1090
  <p>The pipeline your agent drives before a change reaches the push target: review, tests, docs, lint, PR and CI. Switch the scope selector to a project to see that repository's runs and its own config.</p>
1062
1091
  </div>
1092
+ <button id="gateInfo" type="button" class="iconbtn" title="Where this is configured" aria-label="Where the gate is configured"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></button>
1063
1093
  <button id="gateRefresh" type="button" class="iconbtn" title="Refresh" aria-label="Refresh the gate view"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
1064
1094
  </div>
1065
- <div class="grid">
1066
- <div class="card">
1067
- <div class="label">Gate</div><div id="gateOn" class="value">-</div>
1068
- <div class="foot" style="display:flex;align-items:center;gap:8px">Auto-driven after a code task<button id="gateOnBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="AI quality gate"></button></div>
1095
+ <div class="gate-bar">
1096
+ <div class="gate-stat">
1097
+ <button id="gateOnBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="AI quality gate" title="Auto-driven after a code task"></button>
1098
+ <div class="gate-stat-t"><span class="gate-stat-n">Gate</span><span id="gateOn" class="gate-stat-v">-</span></div>
1099
+ </div>
1100
+ <div class="gate-stat">
1101
+ <button id="gateDaemonBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Gate daemon" title="Runs the pipeline in the background"></button>
1102
+ <div class="gate-stat-t"><span class="gate-stat-n">Daemon</span><span id="gateDaemon" class="gate-stat-v">-</span></div>
1069
1103
  </div>
1070
- <div class="card">
1071
- <div class="label">Daemon</div><div id="gateDaemon" class="value">-</div>
1072
- <div class="foot" style="display:flex;align-items:center;gap:8px">Runs the pipeline in the background<button id="gateDaemonBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Gate daemon"></button></div>
1104
+ <div class="gate-stat gate-stat-scope">
1105
+ <div class="gate-stat-t"><span class="gate-stat-n">Scope</span><span id="gateRepo" class="gate-stat-v">-</span></div>
1106
+ <span id="gateRepoFoot" class="gate-stat-f"></span>
1073
1107
  </div>
1074
- <div class="card"><div class="label">Repository</div><div id="gateRepo" class="value">-</div><div id="gateRepoFoot" class="foot">Scope</div></div>
1075
1108
  </div>
1076
1109
 
1077
1110
  <div class="panel">
@@ -1080,7 +1113,7 @@
1080
1113
  </div>
1081
1114
 
1082
1115
  <div class="panel" id="gateSetPanel">
1083
- <div class="set-cat-h">Pipeline settings <code id="gateCfgPath"></code></div>
1116
+ <div class="set-cat-h">Pipeline settings</div>
1084
1117
  <div class="sub" style="margin-bottom:10px">What the daemon runs and how hard it tries. Model and auto-fix limits are what a run's duration and token cost actually hang on.</div>
1085
1118
  <div class="set-grid">
1086
1119
  <div class="set-row">
@@ -1089,7 +1122,7 @@
1089
1122
  </div>
1090
1123
  <div class="set-row" id="gateModelRow">
1091
1124
  <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>
1092
- <div class="set-ctl ladd" style="align-items:center"><input id="gateModel" class="linput" style="flex:none;width:210px" spellcheck="false" autocapitalize="none" autocorrect="off" placeholder="agent default"><button id="gateModelSave" type="button" class="toggle on">Save</button></div>
1125
+ <div class="set-ctl ladd" style="align-items:center"><select id="gateModelSel" class="linput" style="flex:none;width:240px"></select><input id="gateModel" class="linput" style="flex:none;width:200px" spellcheck="false" autocapitalize="none" autocorrect="off" placeholder="model id"><button id="gateModelSave" type="button" class="toggle on">Save</button></div>
1093
1126
  </div>
1094
1127
  <div class="set-row">
1095
1128
  <div class="set-meta"><div class="set-name">CI timeout</div><div class="set-hint">how long the CI step babysits an idle PR: 168h, 24h, or unlimited</div></div>
@@ -1110,8 +1143,8 @@
1110
1143
  </div>
1111
1144
 
1112
1145
  <div class="panel">
1113
- <div class="set-cat-h">Global configuration <code class="gate-cfg-path"></code></div>
1114
- <div class="sub" style="margin-bottom:10px">The same file 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>
1146
+ <div class="set-cat-h">Global configuration</div>
1147
+ <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>
1115
1148
  <textarea id="gateCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Global gate configuration"></textarea>
1116
1149
  <div class="ladd" style="gap:8px;align-items:center">
1117
1150
  <button id="gateCfgSave" type="button" class="toggle on" disabled title="No changes to save">Save</button>
@@ -1120,7 +1153,7 @@
1120
1153
  </div>
1121
1154
 
1122
1155
  <div class="panel" id="gateRepoPanel" style="display:none">
1123
- <div class="set-cat-h">Repository configuration <code id="gateRepoCfgPath"></code></div>
1156
+ <div class="set-cat-h">Repository configuration</div>
1124
1157
  <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>
1125
1158
  <textarea id="gateRepoCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Repository gate configuration"></textarea>
1126
1159
  <div class="ladd" style="gap:8px;align-items:center">
@@ -3405,6 +3438,55 @@ export {
3405
3438
  const askIconChoice = (title, iconOptions, value, message) => openModal({ title, message, field: { iconOptions, value } });
3406
3439
  const askConfirm = (title, message, danger) => openModal({ title, message, danger, okLabel: danger ? "Remove" : "OK" }).then(Boolean);
3407
3440
 
3441
+ const COPY_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="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
3442
+ const CHECK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
3443
+ /** Copies `text`, with the button confirming it; falls back to execCommand where the clipboard API is blocked. */
3444
+ function copyToClipboard(text, btn) {
3445
+ const done = () => {
3446
+ toast.success("Copied");
3447
+ if (!btn) return;
3448
+ const before = btn.innerHTML;
3449
+ btn.innerHTML = CHECK_ICON;
3450
+ setTimeout(() => { btn.innerHTML = before; }, 1200);
3451
+ };
3452
+ (navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).then(done).catch(() => {
3453
+ const ta = document.createElement("textarea");
3454
+ ta.value = text; document.body.appendChild(ta); ta.select();
3455
+ try { document.execCommand("copy"); done(); } catch { toast.error("Copy failed"); }
3456
+ document.body.removeChild(ta);
3457
+ });
3458
+ }
3459
+
3460
+ // Read-only dialog behind an info icon: the paths and links a panel header would otherwise
3461
+ // print inline. Rows are labelled literals with a copy action; a row with no value is dropped.
3462
+ function openInfoModal({ title, intro, rows, note, links }) {
3463
+ const back = document.createElement("div");
3464
+ back.className = "modal-back";
3465
+ const list = (rows || []).filter((r) => r && r.value).map((r) =>
3466
+ '<div class="info-row"><div class="info-k">' + esc(r.label) + "</div>"
3467
+ + '<div class="info-v"><code>' + esc(r.value) + "</code>"
3468
+ + '<button type="button" class="iconbtn" data-copy="' + esc(r.value) + '" title="Copy" aria-label="Copy the ' + esc(r.label.toLowerCase()) + '">' + COPY_ICON + "</button></div>"
3469
+ + (r.hint ? '<div class="set-hint">' + esc(r.hint) + "</div>" : "") + "</div>").join("");
3470
+ back.innerHTML = '<div class="modal info" role="dialog" aria-modal="true">'
3471
+ + '<div class="modal-title">' + esc(title) + "</div>"
3472
+ + (intro ? '<div class="modal-msg">' + esc(intro) + "</div>" : "")
3473
+ + list
3474
+ + (note ? '<div class="set-hint" style="margin-top:12px">' + esc(note) + "</div>" : "")
3475
+ + ((links || []).length
3476
+ ? '<div class="info-links">' + links.map((l) => '<a href="' + esc(l.href) + '" target="_blank" rel="noopener noreferrer">' + esc(l.label) + "</a>").join("") + "</div>"
3477
+ : "")
3478
+ + '<div class="modal-acts"><button type="button" class="toggle on modal-ok">Close</button></div></div>';
3479
+ document.body.appendChild(back);
3480
+ requestAnimationFrame(() => back.classList.add("show"));
3481
+ let done = false;
3482
+ const close = () => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); };
3483
+ back.querySelector(".modal-ok").addEventListener("click", close);
3484
+ back.addEventListener("mousedown", (e) => { if (e.target === back) close(); });
3485
+ back.addEventListener("keydown", (e) => { if (e.key === "Escape" || e.key === "Enter") { e.preventDefault(); close(); } });
3486
+ back.querySelectorAll("[data-copy]").forEach((b) => b.addEventListener("click", () => copyToClipboard(b.dataset.copy, b)));
3487
+ back.querySelector(".modal-ok").focus();
3488
+ }
3489
+
3408
3490
  // Turn "(experimental)" and " · measured/experimental" label suffixes into visual badges.
3409
3491
  function labelWithBadges(text) {
3410
3492
  let name = String(text);
@@ -4304,6 +4386,26 @@ export {
4304
4386
  // is Bun-only), so an unavailable history reports itself instead of blanking the page.
4305
4387
  const gateSaved = { global: "", repo: "" };
4306
4388
  let gateWritable = true;
4389
+ // Where the gate keeps its files, kept for the info dialog instead of printed in the headers.
4390
+ let gateWhere = { global: "", repo: "", root: "", note: "" };
4391
+ const GATE_DOCS_URL = "https://fjrg2007.github.io/enigma/docs/concepts/gate";
4392
+ const GATE_COMMANDS_URL = "https://fjrg2007.github.io/enigma/docs/reference/commands";
4393
+ function openGateInfo() {
4394
+ openInfoModal({
4395
+ title: "Gate configuration",
4396
+ intro: "Where the gate reads its settings and keeps its run history.",
4397
+ rows: [
4398
+ { label: "Global config", value: gateWhere.global, hint: "applies to every repository" },
4399
+ { label: "Repository config", value: gateWhere.repo, hint: "committed with the repository, merged over the global config" },
4400
+ { label: "Gate home", value: gateWhere.root, hint: "run database, logs and the daemon pid file" },
4401
+ ],
4402
+ note: gateWhere.note,
4403
+ links: [
4404
+ { label: "Gate documentation", href: GATE_DOCS_URL },
4405
+ { label: "Command reference", href: GATE_COMMANDS_URL },
4406
+ ],
4407
+ });
4408
+ }
4307
4409
  function gateEls(which) {
4308
4410
  return which === "global"
4309
4411
  ? { area: $("gateCfg"), btn: $("gateCfgSave"), note: "gateCfgNote" }
@@ -4375,6 +4477,53 @@ export {
4375
4477
  ];
4376
4478
  // Rendered once per non-live load; the number inputs write on change, so a re-render
4377
4479
  // mid-edit would drop what is being typed.
4480
+ // Model choices per agent. Claude Code's `--model` takes these aliases, so they can be
4481
+ // offered as a real list; codex and opencode take a provider-specific string, and a
4482
+ // guessed list there would be worse than none - they get auto plus Custom.
4483
+ const GATE_MODELS = {
4484
+ claude: [
4485
+ ["claude-opus-5", "claude-opus-5 (most capable)"],
4486
+ ["claude-sonnet-5", "claude-sonnet-5 (faster, cheaper)"],
4487
+ ["claude-haiku-4-5", "claude-haiku-4-5 (fastest)"],
4488
+ ["claude-opus-4-8", "claude-opus-4-8 (previous Opus)"],
4489
+ ],
4490
+ };
4491
+ const GATE_MODEL_CUSTOM = "__custom__";
4492
+ let gateSavedModel = "";
4493
+
4494
+ /**
4495
+ * Rebuilds the model dropdown for `agent` and selects `model` (empty = the agent default).
4496
+ * `agent` must be the RESOLVED agent: the backend reads and writes the model on whatever
4497
+ * `auto` probed to, so keying the list off the declared name left every stored model under
4498
+ * `agent: auto` falling through to "Custom".
4499
+ */
4500
+ function renderGateModel(agent, model) {
4501
+ const sel = $("gateModelSel"), inp = $("gateModel");
4502
+ const opts = [["", "auto (agent default)"]].concat(GATE_MODELS[agent] || [], [[GATE_MODEL_CUSTOM, "Custom model id..."]]);
4503
+ sel.innerHTML = opts.map(([v, label]) => '<option value="' + esc(v) + '">' + esc(label) + "</option>").join("");
4504
+ const listed = opts.some(([v]) => v === model && v !== GATE_MODEL_CUSTOM);
4505
+ sel.value = listed ? model : GATE_MODEL_CUSTOM;
4506
+ syncDD(sel);
4507
+ inp.value = model;
4508
+ gateSavedModel = model;
4509
+ gateModelMode();
4510
+ }
4511
+ /** Shows the free-text field and its Save only while "Custom" is selected. */
4512
+ function gateModelMode() {
4513
+ const custom = $("gateModelSel").value === GATE_MODEL_CUSTOM;
4514
+ $("gateModel").style.display = custom ? "" : "none";
4515
+ $("gateModelSave").style.display = custom ? "" : "none";
4516
+ // Select + field + Save is wider than one settings column; the row takes the full width.
4517
+ $("gateModelRow").classList.toggle("wide", custom);
4518
+ syncGateModelSave();
4519
+ }
4520
+ /** No-op rule: nothing to save while the typed id still equals the stored one. */
4521
+ function syncGateModelSave() {
4522
+ const btn = $("gateModelSave"), dirty = $("gateModel").value.trim() !== gateSavedModel;
4523
+ btn.disabled = !gateWritable || !dirty;
4524
+ btn.title = dirty ? "" : "No changes to save";
4525
+ }
4526
+
4378
4527
  function renderGateSettings(s) {
4379
4528
  const panel = $("gateSetPanel");
4380
4529
  if (!s) { panel.style.display = "none"; return; }
@@ -4391,7 +4540,7 @@ export {
4391
4540
  ? "auto: first agent found on PATH - currently " + s.agentResolved
4392
4541
  : "which coding agent the pipeline drives";
4393
4542
  $("gateModelRow").style.display = s.modelSupported ? "" : "none";
4394
- $("gateModel").value = s.model || "";
4543
+ renderGateModel(s.agentResolved || s.agent, s.model || "");
4395
4544
  $("gateModelHint").textContent = "passed to " + s.agentResolved
4396
4545
  + " on every step; a smaller model is the one change that cuts both run time and tokens";
4397
4546
  $("gateCiTimeout").value = s.ciTimeout || "";
@@ -4403,6 +4552,7 @@ export {
4403
4552
  + '<div class="set-ctl"><input type="number" min="0" max="20" step="1" inputmode="numeric" class="linput" style="flex:none;width:80px" data-gate-af="' + esc(step) + '" value="' + Number(s.autoFix[step] || 0) + '"></div></div>').join("");
4404
4553
  const ctls = panel.querySelectorAll("input, select, button");
4405
4554
  for (const c of ctls) c.disabled = !gateWritable;
4555
+ syncGateModelSave(); // after the sweep, which would otherwise re-enable a clean Save
4406
4556
  }
4407
4557
  // `live` marks a poll refresh: it repaints the run strip and the two switches, and leaves
4408
4558
  // every editor alone so a poll can never overwrite what is being typed.
@@ -4410,14 +4560,15 @@ export {
4410
4560
  g = g || {};
4411
4561
  if (typeof g.serverNow === "number") gateClockSkew = g.serverNow * 1000 - Date.now();
4412
4562
  $("gateOn").textContent = g.on ? "On" : "Off";
4413
- $("gateOn").className = "value " + (g.on ? "good" : "");
4563
+ $("gateOn").className = "gate-stat-v " + (g.on ? "good" : "");
4414
4564
  gateBool($("gateOnBtn"), g.on);
4415
4565
  $("gateDaemon").textContent = g.daemon ? "Running" : "Stopped";
4416
- $("gateDaemon").className = "value " + (g.daemon ? "good" : "");
4566
+ $("gateDaemon").className = "gate-stat-v " + (g.daemon ? "good" : "");
4417
4567
  gateBool($("gateDaemonBtn"), g.daemon);
4418
4568
  const repo = g.repo;
4419
4569
  $("gateRepo").textContent = repo ? (repo.initialized ? "Initialized" : "Not initialized") : "Global";
4420
4570
  $("gateRepoFoot").textContent = repo ? repo.path : "Every active run on this machine";
4571
+ $("gateRepoFoot").title = $("gateRepoFoot").textContent; // the path truncates; keep it recoverable
4421
4572
  $("gateRunsSub").textContent = repo ? "most recent first, this repository" : "active runs across every repository";
4422
4573
  const runs = g.runs || [];
4423
4574
  gateRuns = runs;
@@ -4426,9 +4577,14 @@ export {
4426
4577
  : '<div class="empty" style="padding:12px 0">' + (g.runsAvailable === false ? esc(g.runsNote || "Run history unavailable.") : (repo ? "No runs recorded for this repository yet." : "No run in flight.")) + "</div>";
4427
4578
  gatePoll();
4428
4579
  if (live) return;
4429
- const cfgPath = (g.globalConfig || {}).path || "";
4430
- $("gateCfgPath").textContent = cfgPath;
4431
- for (const el of document.querySelectorAll(".gate-cfg-path")) el.textContent = cfgPath;
4580
+ // The paths are not header decoration: they live behind the info icon, which is the
4581
+ // only place they are worth the room.
4582
+ gateWhere = {
4583
+ global: (g.globalConfig || {}).path || "",
4584
+ repo: repo ? (repo.configPath || "") : "",
4585
+ root: g.root || "",
4586
+ note: g.canWrite === false ? (g.writeNote || "") : "",
4587
+ };
4432
4588
  gateSaved.global = (g.globalConfig || {}).text || "";
4433
4589
  $("gateCfg").value = gateSaved.global;
4434
4590
  // A runtime that cannot parse a gate config must not write one: read-only, with the reason.
@@ -4440,7 +4596,6 @@ export {
4440
4596
  renderGateSettings(g.settings);
4441
4597
  $("gateRepoPanel").style.display = repo ? "" : "none";
4442
4598
  if (repo) {
4443
- $("gateRepoCfgPath").textContent = repo.configPath || "";
4444
4599
  gateSaved.repo = repo.text || "";
4445
4600
  $("gateRepoCfg").value = gateSaved.repo;
4446
4601
  syncGateSave("repo");
@@ -4530,6 +4685,7 @@ export {
4530
4685
  await loadGate();
4531
4686
  }
4532
4687
  $("gateRefresh").addEventListener("click", () => loadGate());
4688
+ $("gateInfo").addEventListener("click", openGateInfo);
4533
4689
  $("gateCfg").addEventListener("input", () => syncGateSave("global"));
4534
4690
  $("gateRepoCfg").addEventListener("input", () => syncGateSave("repo"));
4535
4691
  $("gateCfgSave").addEventListener("click", () => saveGateCfg("global"));
@@ -4539,6 +4695,14 @@ export {
4539
4695
  $("gateAgent").addEventListener("change", (e) => saveGateSetting("agent", e.target.value, "the agent"));
4540
4696
  $("gateLogLevel").addEventListener("change", (e) => saveGateSetting("log_level", e.target.value, "the log level"));
4541
4697
  $("gateIntent").addEventListener("click", () => saveGateSetting("intent.enabled", $("gateIntent").dataset.on !== "1", "intent extraction"));
4698
+ // A listed model commits on pick, like the agent and log-level selects; "Custom" reveals
4699
+ // the field and waits for Save, since a half-typed model id must not be written.
4700
+ $("gateModelSel").addEventListener("change", () => {
4701
+ gateModelMode();
4702
+ if ($("gateModelSel").value === GATE_MODEL_CUSTOM) { $("gateModel").focus(); return; }
4703
+ saveGateSetting("model", $("gateModelSel").value, "the model");
4704
+ });
4705
+ $("gateModel").addEventListener("input", syncGateModelSave);
4542
4706
  $("gateModelSave").addEventListener("click", () => saveGateSetting("model", $("gateModel").value.trim(), "the model"));
4543
4707
  $("gateCiSave").addEventListener("click", () => saveGateSetting("ci_timeout", $("gateCiTimeout").value.trim(), "the CI timeout"));
4544
4708
  // Number inputs commit on change (blur or Enter), so typing "12" is not read as "1" then "12".
@@ -5326,7 +5490,6 @@ export {
5326
5490
  // The button is an icon, so the toast is not the only confirmation: swap the icon for a
5327
5491
  // check on the control itself, then restore the captured original markup.
5328
5492
  const copyIcon = copyBtn.innerHTML;
5329
- const CHECK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
5330
5493
  let copyTimer = null;
5331
5494
  const copied = () => {
5332
5495
  toast.success("Copied curl");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
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": [