@yemi33/minions 0.1.2389 → 0.1.2391

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.
@@ -33,6 +33,38 @@ function _isCurrentModelLoad(scope, key, token) {
33
33
  return _modelLoadEpochs[scope][key] === token;
34
34
  }
35
35
 
36
+ // Per-open dedup cache for /api/runtimes/<name>/models payloads.
37
+ // initRuntimeFleetUI() fans out one model fetch per agent PLUS default + CC.
38
+ // When the whole fleet shares a runtime (the common case) those are
39
+ // byte-identical requests, and each one makes the server reloadConfig() from
40
+ // disk + re-run model discovery. Collapsing them to one request per distinct
41
+ // runtime per Settings open removes the N+2 → 1 duplicate round-trips (measured
42
+ // 10 → 1 for an 8-agent single-runtime fleet). forceRefresh (the ↻ button)
43
+ // bypasses the cache and repopulates it with the fresh list. Transient
44
+ // failures are NOT cached so a blip can't poison every agent cell for the open.
45
+ let _modelFetchCache = new Map();
46
+ function _resetModelFetchCache() { _modelFetchCache = new Map(); }
47
+ function _fetchRuntimeModelsPayload(runtimeName, forceRefresh) {
48
+ if (!runtimeName) return Promise.resolve({ models: null });
49
+ if (!forceRefresh) {
50
+ const cached = _modelFetchCache.get(runtimeName);
51
+ if (cached) return cached;
52
+ }
53
+ const suffix = forceRefresh ? '/models/refresh' : '/models';
54
+ const p = fetch('/api/runtimes/' + encodeURIComponent(runtimeName) + suffix, forceRefresh ? { method: 'POST' } : undefined)
55
+ .then(res => (res.ok ? res.json() : null))
56
+ .catch(() => null)
57
+ .then(payload => {
58
+ if (!payload || !Array.isArray(payload.models)) {
59
+ if (_modelFetchCache.get(runtimeName) === p) _modelFetchCache.delete(runtimeName);
60
+ return payload || { models: null };
61
+ }
62
+ return payload;
63
+ });
64
+ _modelFetchCache.set(runtimeName, p);
65
+ return p;
66
+ }
67
+
36
68
  async function openSettings() {
37
69
  document.getElementById('modal-title').textContent = 'Settings';
38
70
  document.getElementById('modal-body').innerHTML = '<p style="color:var(--muted)">Loading...</p>';
@@ -719,8 +751,16 @@ async function openSettings() {
719
751
  // hides unmatched rows so operators can see results without tab-hopping.
720
752
  const searchInput = document.getElementById('settings-search');
721
753
  if (searchInput) {
754
+ // Debounce the filter: _applySettingsSearch runs several full-DOM
755
+ // querySelectorAll scans and reads textContent of every [data-search] row
756
+ // (a forced reflow). Firing that per-keystroke made typing janky on the
757
+ // large Settings DOM. The field itself stays instant; only the filter
758
+ // computation is coalesced to the last keystroke in a ~120ms window.
759
+ let _searchDebounce = null;
722
760
  searchInput.addEventListener('input', function() {
723
- _applySettingsSearch(searchInput.value || '');
761
+ const val = searchInput.value || '';
762
+ if (_searchDebounce) clearTimeout(_searchDebounce);
763
+ _searchDebounce = setTimeout(function() { _applySettingsSearch(val); }, 120);
724
764
  });
725
765
  }
726
766
 
@@ -822,6 +862,9 @@ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
822
862
  const ccCliSelect = document.getElementById('set-ccCli');
823
863
  if (!cliSelect || !ccCliSelect) return;
824
864
 
865
+ // Fresh cache per Settings open so a config change between opens is picked up.
866
+ _resetModelFetchCache();
867
+
825
868
  // Fetch the registry; render an empty fallback on failure so the rest of the
826
869
  // settings panel still works.
827
870
  let runtimes = [];
@@ -917,11 +960,7 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue, forceRef
917
960
  return;
918
961
  }
919
962
  let payload = { models: null };
920
- try {
921
- const suffix = forceRefresh ? '/models/refresh' : '/models';
922
- const res = await fetch('/api/runtimes/' + encodeURIComponent(runtimeName) + suffix, forceRefresh ? { method: 'POST' } : undefined);
923
- if (res.ok) payload = await res.json();
924
- } catch { /* fall through to free-text */ }
963
+ payload = await _fetchRuntimeModelsPayload(runtimeName, forceRefresh);
925
964
 
926
965
  if (!_isCurrentModelLoad('runtime', inputId, token)) return;
927
966
  const models = Array.isArray(payload.models) ? payload.models : null;
@@ -962,10 +1001,7 @@ async function loadModelsForAgent(agentId, runtimeName, currentValue) {
962
1001
  return;
963
1002
  }
964
1003
  let payload = { models: null };
965
- try {
966
- const res = await fetch('/api/runtimes/' + encodeURIComponent(runtimeName) + '/models');
967
- if (res.ok) payload = await res.json();
968
- } catch { /* fall through to free-text */ }
1004
+ payload = await _fetchRuntimeModelsPayload(runtimeName, false);
969
1005
 
970
1006
  if (!_isCurrentModelLoad('agent', agentId, token)) return;
971
1007
  const models = Array.isArray(payload.models) ? payload.models : null;
@@ -1445,3 +1481,33 @@ async function removeProject(name) {
1445
1481
  }
1446
1482
 
1447
1483
  window.MinionsSettings = { openSettings, saveSettings, addProject, removeProject, resetSettingsToDefaults, _selectSettingsTab, _applySettingsSearch };
1484
+
1485
+ // ── Settings embed mode (Slim UX parity — W-mrkpke07000i8ed5) ───────────
1486
+ // The Slim UX Settings modal embeds this LITERAL classic Settings surface in
1487
+ // an iframe at /settings?embed=1 (chrome-off) so there is ONE settings UI, not
1488
+ // two (CLAUDE.md "Slim UX vs Classic UX — reuse, don't fork"; mirrors the
1489
+ // Queued-work/Plans/PRs cockpit tiles). Slim and classic are separate IIFE
1490
+ // bundles with no shared runtime scope, so slim can't call openSettings()
1491
+ // in-process — it iframes the real classic screen instead. On boot, when we
1492
+ // ARE the embedded /settings page, auto-open the modal and tag <html> so the
1493
+ // embed CSS renders the settings modal full-bleed inside the iframe. Only the
1494
+ // /settings path triggers this; every other embedded page (/work, /plans,
1495
+ // /prs) is untouched.
1496
+ (function bootSettingsEmbed() {
1497
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
1498
+ try {
1499
+ var isSettingsPath = window.location.pathname.replace(/\/+$/, '') === '/settings';
1500
+ var isEmbed = /[?&]embed=1(?:&|$)/.test(window.location.search);
1501
+ if (!isSettingsPath || !isEmbed) return;
1502
+ } catch (e) { return; }
1503
+ try { document.documentElement.classList.add('embed-settings'); } catch (e) { /* no DOM attr */ }
1504
+ function openWhenReady() {
1505
+ if (typeof openSettings === 'function') { openSettings(); return; }
1506
+ setTimeout(openWhenReady, 30); // wait for the settings module + deps to register
1507
+ }
1508
+ if (document.readyState === 'loading') {
1509
+ document.addEventListener('DOMContentLoaded', function() { setTimeout(openWhenReady, 0); });
1510
+ } else {
1511
+ setTimeout(openWhenReady, 0);
1512
+ }
1513
+ })();
@@ -270,16 +270,19 @@
270
270
  </div>
271
271
  </div>
272
272
 
273
- <!-- Settings dialog (preserved from r2): lists experimental flags so the
274
- user can flip 'slim-ux' off and revert to the original dashboard. -->
273
+ <!-- Settings dialog embeds the LITERAL classic dashboard Settings surface
274
+ in an iframe (/settings?embed=1) so slim shows the FULL settings (engine
275
+ toggles, runtime/model, projects, agents, routing, feature flags incl.
276
+ slim-ux) with zero forked rendering. See dashboard/slim/js/settings.js
277
+ (W-mrkpke07000i8ed5 / "reuse, don't fork"). -->
275
278
  <div class="modal-bg" id="slim-settings-modal">
276
279
  <div class="modal">
277
280
  <div class="modal-header">
278
- <h3>Settings &middot; Experimental flags</h3>
281
+ <h3>Settings</h3>
279
282
  <button id="slim-settings-close" class="icon-btn" title="Close">&times;</button>
280
283
  </div>
281
284
  <div class="modal-body" id="slim-settings-body">
282
- <p>Loading flags&hellip;</p>
285
+ <p>Loading settings&hellip;</p>
283
286
  </div>
284
287
  </div>
285
288
  </div>
@@ -1,80 +1,39 @@
1
1
  // ── Settings dialog ────────────────────────────────────────────
2
- async function openSlimSettings() {
2
+ // Reuses the LITERAL classic dashboard Settings surface instead of a
3
+ // slim-only feature-flag list, so there is ONE settings UI, not two
4
+ // (W-mrkpke07000i8ed5 / CLAUDE.md "Slim UX vs Classic UX — reuse, don't
5
+ // fork"; mirrors the Queued-work/Plans/PRs cockpit tiles). Slim and classic
6
+ // are two separate IIFE bundles with no shared runtime scope, so we can't
7
+ // call the classic openSettings() in-process; instead we embed the real
8
+ // classic /settings screen in an iframe with the chrome-off ?embed=1 mode.
9
+ // The iframed page IS the classic Settings modal — every engine toggle,
10
+ // runtime/model, projects, agents, routing, feature flags (incl. slim-ux) —
11
+ // backed by the same /api/settings + /api/features endpoints, with zero
12
+ // duplicated rendering logic. Classic is reachable at /settings even with
13
+ // slim-ux ON (only / is taken over). Built with createElement (no innerHTML)
14
+ // to satisfy the dashboard no-unsanitized lint gate.
15
+ function openSlimSettings() {
3
16
  var modal = document.getElementById('slim-settings-modal');
4
17
  var body = document.getElementById('slim-settings-body');
5
- body.innerHTML = '<p>Loading flags&hellip;</p>';
18
+ if (!modal || !body) return;
19
+ body.textContent = '';
20
+ var frame = document.createElement('iframe');
21
+ frame.className = 'slim-settings-embed';
22
+ frame.src = '/settings?embed=1';
23
+ frame.title = 'Settings';
24
+ frame.addEventListener('load', function() {
25
+ var frameDocument = frame.contentDocument;
26
+ if (!frameDocument) return;
27
+ // Capture before the classic modal handler so only the parent modal closes.
28
+ frameDocument.addEventListener('keydown', function(ev) {
29
+ if (ev.key !== 'Escape' && ev.keyCode !== 27) return;
30
+ ev.preventDefault();
31
+ ev.stopImmediatePropagation();
32
+ closeSlimSettings();
33
+ }, true);
34
+ });
35
+ body.appendChild(frame);
6
36
  modal.classList.add('open');
7
- try {
8
- var res = await fetch('/api/features');
9
- if (!res.ok) throw new Error('HTTP ' + res.status);
10
- var data = await res.json();
11
- var flags = (data && data.features) || [];
12
-
13
- // Build the body as a document fragment so we avoid .innerHTML for
14
- // the dynamic parts (SEC-03 ratchet — see DYNAMIC_INNERHTML_BASELINE
15
- // in test/unit.test.js). Static intro paragraph uses innerHTML
16
- // because it carries a <code> tag and is a pure string literal
17
- // (exempted by _isStaticInnerHtmlRhs).
18
- var intro = document.createElement('p');
19
- intro.innerHTML = 'Toggle <code>slim-ux</code> off to return to the original dashboard. Advanced settings (agents, projects, runtime) live there for now.';
20
- var frag = document.createDocumentFragment();
21
- frag.appendChild(intro);
22
-
23
- if (flags.length === 0) {
24
- var none = document.createElement('p');
25
- none.innerHTML = 'No experimental flags registered. Add entries to <code>engine/features.js</code>.';
26
- frag.appendChild(none);
27
- } else {
28
- for (var i = 0; i < flags.length; i++) {
29
- var f = flags[i];
30
- var row = document.createElement('div');
31
- row.className = 'flag-row';
32
- var info = document.createElement('div');
33
- var name = document.createElement('div');
34
- name.className = 'flag-name';
35
- name.textContent = f.id;
36
- var desc = document.createElement('div');
37
- desc.className = 'flag-desc';
38
- desc.textContent = f.description || '';
39
- info.appendChild(name);
40
- info.appendChild(desc);
41
- var lbl = document.createElement('label');
42
- lbl.className = 'flag-toggle';
43
- var input = document.createElement('input');
44
- input.type = 'checkbox';
45
- input.setAttribute('data-flag', f.id);
46
- if (f.enabled) input.checked = true;
47
- var stateSpan = document.createElement('span');
48
- stateSpan.textContent = f.enabled ? 'On' : 'Off';
49
- lbl.appendChild(input);
50
- lbl.appendChild(stateSpan);
51
- row.appendChild(info);
52
- row.appendChild(lbl);
53
- frag.appendChild(row);
54
- }
55
- }
56
-
57
- // TEMP: deep-link to the full dashboard's settings page. The slim
58
- // intentionally doesn't reimplement that whole UI — Carlos asked us
59
- // to keep settings sprawl off the main slim view.
60
- var link = document.createElement('a');
61
- link.className = 'settings-link';
62
- link.href = '/?fullDashboard=1';
63
- link.title = 'Open the full dashboard’s settings page';
64
- link.textContent = 'Advanced settings → full dashboard';
65
- frag.appendChild(link);
66
-
67
- body.replaceChildren(frag);
68
- var inputs = body.querySelectorAll('input[type=checkbox][data-flag]');
69
- for (var j = 0; j < inputs.length; j++) {
70
- inputs[j].addEventListener('change', onFlagToggle);
71
- }
72
- } catch (e) {
73
- var err = document.createElement('p');
74
- err.style.color = 'var(--red)';
75
- err.textContent = 'Failed to load flags: ' + (e && e.message ? e.message : 'unknown error');
76
- body.replaceChildren(err);
77
- }
78
37
  }
79
38
 
80
39
  function closeSlimSettings() {
@@ -99,7 +58,7 @@
99
58
  })();
100
59
 
101
60
  // POST a feature-flag toggle; throws on a non-2xx so callers can branch on
102
- // failure. Shared by onFlagToggle() and the one-click returnToClassicDashboard().
61
+ // failure. Used by the one-click returnToClassicDashboard().
103
62
  async function postFeatureToggle(id, enabled) {
104
63
  var res = await fetch('/api/features/toggle', {
105
64
  method: 'POST',
@@ -223,24 +182,6 @@
223
182
  bindModalClose('slim-bug-modal', 'slim-bug-close');
224
183
  })();
225
184
 
226
- async function onFlagToggle(ev) {
227
- var input = ev.currentTarget;
228
- var id = input.getAttribute('data-flag');
229
- var enabled = input.checked;
230
- var label = input.parentElement.querySelector('span');
231
- if (label) label.textContent = enabled ? 'On' : 'Off';
232
- try {
233
- await postFeatureToggle(id, enabled);
234
- if (id === 'slim-ux' && !enabled) {
235
- location.reload();
236
- }
237
- } catch (e) {
238
- input.checked = !enabled;
239
- if (label) label.textContent = !enabled ? 'On' : 'Off';
240
- alert('Toggle failed: ' + e.message);
241
- }
242
- }
243
-
244
185
  // ── Action prompt modal (TEMP: routes through chat) ──────────────
245
186
  // Each of the 4 action buttons opens this modal with prompt text
246
187
  // tailored to the concept. On submit we drop the user's text into the
@@ -690,9 +690,16 @@
690
690
  #slim-tile-modal.tile-modal--work .modal-body,
691
691
  #slim-tile-modal.tile-modal--plans .modal-body,
692
692
  #slim-tile-modal.tile-modal--prs .modal-body { padding: 0; }
693
+ /* W-mrkpke07000i8ed5 — the Settings modal embeds the full classic Settings
694
+ screen (/settings?embed=1) in an iframe; widen the modal + drop the body
695
+ padding so the embedded settings surface (left rail + content) gets the
696
+ full width/height. Mirrors the Queued-work/Plans/PRs tile treatment. */
697
+ #slim-settings-modal .modal { width: 1180px; max-width: calc(100vw - 32px); }
698
+ #slim-settings-modal .modal-body { padding: 0; }
693
699
  .slim-work-embed,
694
700
  .slim-plans-embed,
695
- .slim-prs-embed {
701
+ .slim-prs-embed,
702
+ .slim-settings-embed {
696
703
  display: block; width: 100%; height: calc(100vh - 160px); min-height: 360px;
697
704
  border: 0; background: var(--bg);
698
705
  }
@@ -1705,32 +1712,6 @@
1705
1712
  }
1706
1713
  .btn-primary:hover, .btn-secondary:hover { filter: brightness(1.1); }
1707
1714
 
1708
- .flag-row {
1709
- display: flex;
1710
- align-items: center;
1711
- justify-content: space-between;
1712
- padding: 10px 0;
1713
- border-bottom: 1px solid var(--border);
1714
- }
1715
- .flag-row:last-child { border-bottom: none; }
1716
- .flag-name { font-size: var(--text-lg); font-weight: 600; }
1717
- .flag-desc { font-size: var(--text-base); color: var(--muted); margin-top: 2px; }
1718
- .flag-toggle { display: flex; align-items: center; gap: 8px; font-size: var(--text-md); }
1719
- .flag-toggle input { cursor: pointer; }
1720
- .settings-link {
1721
- display: block;
1722
- margin-top: 14px;
1723
- padding: 10px 12px;
1724
- background: var(--surface2);
1725
- border: 1px solid var(--border);
1726
- border-radius: var(--radius);
1727
- color: var(--blue);
1728
- text-decoration: none;
1729
- font-size: var(--text-md);
1730
- text-align: center;
1731
- }
1732
- .settings-link:hover { filter: brightness(1.1); }
1733
-
1734
1715
  .modal-textarea {
1735
1716
  width: 100%;
1736
1717
  min-height: 96px;
@@ -1645,3 +1645,18 @@
1645
1645
  base .page-layout flex row already gives .page-content full width plus a
1646
1646
  bounded height to scroll within. */
1647
1647
  html.embed .page-content { padding: 12px 16px; overflow-y: auto; }
1648
+
1649
+ /* W-mrkpke07000i8ed5 — settings-embed mode. The Slim UX Settings modal iframes
1650
+ the classic Settings surface at /settings?embed=1; settings.js#bootSettingsEmbed
1651
+ auto-opens the modal and tags <html class="embed-settings">. Render the
1652
+ settings modal full-bleed inside the iframe (no dim backdrop, no rounded
1653
+ card, fill the frame), hide the empty home page-content behind it, and hide
1654
+ the modal close X — the parent slim modal owns closing. */
1655
+ html.embed-settings .page-content { display: none !important; }
1656
+ html.embed-settings #modal.modal-bg { background: var(--bg); }
1657
+ html.embed-settings #modal .modal,
1658
+ html.embed-settings #modal .modal.modal-wide {
1659
+ width: 100%; max-width: none; height: 100%; max-height: none;
1660
+ border: none; border-radius: 0;
1661
+ }
1662
+ html.embed-settings #modal .modal-close { display: none; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2389",
3
+ "version": "0.1.2391",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"