@yemi33/minions 0.1.2410 → 0.1.2412

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.
@@ -2388,4 +2388,28 @@ if (document.readyState === 'loading') {
2388
2388
  ccInitResize();
2389
2389
  }
2390
2390
 
2391
+ // W-mrz5yjdw00052b6e — Esc closes the Command Center drawer. Routes through
2392
+ // toggleCommandCenter() so the open/close path stays the single source of truth
2393
+ // (same path as the ✕ button and the overlay click). Bubble phase, and guarded
2394
+ // so it only fires when the drawer is actually open — Esc is not stolen when CC
2395
+ // is hidden. If a modal (#modal.open) or the QA review modal is layered on top,
2396
+ // we early-out so the topmost overlay closes first (modal.js / qa.js own that
2397
+ // Esc). Closing while mid-typing in cc-input IS desired, so we do NOT exempt
2398
+ // TEXTAREA here — but the top command bar's @/# autocomplete popup gets first
2399
+ // dibs on Esc (first Esc dismisses the typeahead, second closes the panel).
2400
+ if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
2401
+ document.addEventListener('keydown', function(ev) {
2402
+ if (ev.key !== 'Escape' && ev.keyCode !== 27) return;
2403
+ if (!_ccOpen) return;
2404
+ // Let a modal / QA review modal layered on top close first.
2405
+ if (typeof _qaModalState !== 'undefined' && _qaModalState) return;
2406
+ var modalEl = document.getElementById('modal');
2407
+ if (modalEl && modalEl.classList.contains('open')) return;
2408
+ // First Esc dismisses the command-bar autocomplete popup, not the drawer.
2409
+ var popup = document.getElementById('cmd-mention-popup');
2410
+ if (popup && popup.classList.contains('visible')) return;
2411
+ toggleCommandCenter();
2412
+ });
2413
+ }
2414
+
2391
2415
  window.MinionsCC = { toggleCommandCenter, ccNewSession, ccNewTab, ccSwitchTab, ccCloseTab, ccRenderTabBar, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction, ccPrActionFollowup, ccAddImageFiles, ccRemoveAttachment, ccHandlePaste, ccHandleDragOver, ccHandleDragLeave, ccHandleDrop };
@@ -144,7 +144,7 @@ async function openSettings() {
144
144
  '</div>' +
145
145
  '<div data-search="default model fleet">' +
146
146
  '<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Default Model</label>' +
147
- '<div id="set-defaultModel-wrap"><input id="set-defaultModel" value="' + escHtml(e.defaultModel || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)"></div>' +
147
+ '<div id="set-defaultModel-wrap"><input id="set-defaultModel" value="' + escHtml(e.defaultModel || '') + '" placeholder="Default (CLI chooses)" style="' + MODEL_FIELD_INPUT_STYLE + '"></div>' +
148
148
  '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = let the runtime pick its own default</div>' +
149
149
  '</div>' +
150
150
  '</div>' +
@@ -168,7 +168,7 @@ async function openSettings() {
168
168
  // swaps THIS element. Without the wrap it would wipe the label
169
169
  // and the hint below, breaking vertical alignment with the
170
170
  // sibling CC CLI / Effort columns.
171
- '<div id="set-ccModel-wrap"><input id="set-ccModel" value="' + escHtml(e.ccModel || '') + '" placeholder="(inherits Default Model)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)"></div>' +
171
+ '<div id="set-ccModel-wrap"><input id="set-ccModel" value="' + escHtml(e.ccModel || '') + '" placeholder="(inherits Default Model)" style="' + MODEL_FIELD_INPUT_STYLE + '"></div>' +
172
172
  '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Empty = inherit Default Model</div>' +
173
173
  '</div>' +
174
174
  '<div data-search="cc effort reasoning">' +
@@ -253,6 +253,29 @@ async function openSettings() {
253
253
  // preferences (font-size scale today; reserved for future theme picks).
254
254
  // Kept narrow + featured near the rail top so the setting is discoverable
255
255
  // rather than buried under generic Engine headers.
256
+ // W-mryz1okr00062b6b — the slim-ux enable/disable control lives in Appearance
257
+ // (a dashboard-wide visual preference) rather than the generic Feature Flags
258
+ // list. It reuses the EXISTING toggle wiring: rendering a checkbox tagged with
259
+ // the data-feature-toggle attribute set to slim-ux means the delegated
260
+ // change-handler below (querySelectorAll('input[data-feature-toggle]'))
261
+ // persists it via POST /api/features/toggle — no new listener, no new
262
+ // endpoint. If the flag is missing from the registry response we hide the
263
+ // control rather than render a broken toggle.
264
+ const slimUxFeature = featuresList.find(function(f) { return f.id === 'slim-ux'; });
265
+ const slimUxEnabled = slimUxFeature
266
+ ? (slimUxFeature.enabled != null ? !!slimUxFeature.enabled : !!slimUxFeature.default)
267
+ : false;
268
+ const slimUxControl = slimUxFeature
269
+ ? '<div data-search="slim ux appearance experimental cockpit">' +
270
+ '<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">' +
271
+ '<input type="checkbox" data-feature-toggle="slim-ux"' + (slimUxEnabled ? ' checked' : '') + ' style="margin-top:3px;accent-color:var(--blue);width:16px;height:16px;cursor:pointer;flex-shrink:0">' +
272
+ '<span style="flex:1">' +
273
+ '<span style="font-size:var(--text-md);color:var(--text);display:block">Slim UX</span>' +
274
+ '<span style="font-size:var(--text-xs);color:var(--muted);display:block;margin-top:1px">Experimental focused single-screen cockpit. Persists immediately.</span>' +
275
+ '</span>' +
276
+ '</label>' +
277
+ '</div>'
278
+ : '';
256
279
  const paneAppearance =
257
280
  '<h3>Appearance</h3>' +
258
281
  '<div class="settings-pane-sub">Dashboard-wide visual preferences. Persisted in browser localStorage + server-side settings so they survive a reload from a cold cache.</div>' +
@@ -270,6 +293,7 @@ async function openSettings() {
270
293
  '</select>' +
271
294
  '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:1px">Scales the whole dashboard. Persisted in browser + server.</div>' +
272
295
  '</div>' +
296
+ slimUxControl +
273
297
  '</div>';
274
298
 
275
299
  // W-mrtdw70o000sb227 — Projects tab now shows ONE project's config at a time,
@@ -605,17 +629,21 @@ async function openSettings() {
605
629
  '</div>';
606
630
 
607
631
  // Toggles persist immediately via POST /api/features/toggle — no Save needed.
632
+ // W-mryz1okr00062b6b — slim-ux is surfaced in the Appearance pane instead, so
633
+ // exclude it here (display-only; the /api/features payload + registry are
634
+ // untouched). The "(N registered)" count below reflects what this list shows.
635
+ const displayFeatures = featuresList.filter(function(f) { return f.id !== 'slim-ux'; });
608
636
  const paneFeatures =
609
637
  '<h3>Feature Flags</h3>' +
610
638
  '<div class="settings-pane-sub">In-progress UX or behavior gates. Toggles persist immediately. Registry: <code>engine/features.js</code>. Env override: <code>MINIONS_FEATURE_&lt;NAME&gt;=1</code>.</div>' +
611
639
  '<details id="settings-features-details" open style="border:1px solid var(--border);border-radius:6px;padding:12px">' +
612
640
  '<summary style="cursor:pointer;font-size:var(--text-md);color:var(--text);user-select:none;margin-bottom:8px">Show experimental flags ' +
613
- '<span style="font-size:var(--text-sm);color:var(--muted)">(' + featuresList.length + ' registered)</span>' +
641
+ '<span style="font-size:var(--text-sm);color:var(--muted)">(' + displayFeatures.length + ' registered)</span>' +
614
642
  '</summary>' +
615
- (featuresList.length === 0
643
+ (displayFeatures.length === 0
616
644
  ? '<div style="font-size:var(--text-base);color:var(--muted);padding:12px;border:1px dashed var(--border);border-radius:4px;text-align:center">No experimental features registered. Add entries to <code>engine/features.js</code> to gate new work.</div>'
617
645
  : '<div style="display:flex;flex-direction:column;gap:8px">' +
618
- featuresList.map(function(f) {
646
+ displayFeatures.map(function(f) {
619
647
  const checked = f.enabled ? ' checked' : '';
620
648
  const expiredBadge = f.expired
621
649
  ? ' <span style="font-size:var(--text-xs);padding:1px 5px;background:rgba(220,80,80,0.15);color:var(--red);border-radius:3px;margin-left:4px">EXPIRED</span>'
@@ -995,6 +1023,133 @@ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
995
1023
  });
996
1024
  }
997
1025
 
1026
+ // ── Model field styling (W-mrvdup4h000fc0a2) ─────────────────────────────
1027
+ // Default Model / CC Model in the Runtime pane are datalist-backed <input>
1028
+ // comboboxes (staged/custom model IDs must stay typeable, so they can't be
1029
+ // closed native <select>s). These shared styles make the input match the box
1030
+ // metrics of the sibling native <select> dropdowns (Default CLI / CC CLI /
1031
+ // Effort / Font Size) and add a chevron so it reads as a dropdown.
1032
+ // box-sizing:border-box keeps the bordered box the same rendered size as a
1033
+ // width:100% <select>; the chevron is drawn just inside the input's right edge.
1034
+ const MODEL_FIELD_INPUT_BOX = 'box-sizing:border-box;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)';
1035
+ const MODEL_FIELD_CHEVRON = "background-image:url('data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2210%22%20height=%226%22%3E%3Cpath%20d=%22M1%201l4%204%204-4%22%20fill=%22none%22%20stroke=%22%238b949e%22%20stroke-width=%221.5%22/%3E%3C/svg%3E');background-repeat:no-repeat;background-position:right 8px center;padding-right:22px";
1036
+ // Full-width variant for the initial (pre-discovery) placeholder inputs.
1037
+ const MODEL_FIELD_INPUT_STYLE = 'width:100%;' + MODEL_FIELD_INPUT_BOX + ';' + MODEL_FIELD_CHEVRON;
1038
+
1039
+ // ── Model-picker dropdown (W-mrvdup4h000fc0a2 / PR #916) ──────────────────
1040
+ // The Default Model / CC Model fields must stay editable <input>s so staged
1041
+ // and custom model IDs remain typeable — they can't be closed native
1042
+ // <select>s. Discovery used to be surfaced through a browser <datalist>, but
1043
+ // the datalist autocomplete popup is browser chrome and reads visibly
1044
+ // different from the native <select> dropdowns of the sibling controls
1045
+ // (Default CLI / CC CLI / Effort) — the mismatch the reviewer flagged: the
1046
+ // "dialog that it launches doesn't match". This wires a dashboard-themed
1047
+ // popover (.model-combo-popup) onto the input so the launched suggestion list
1048
+ // matches those dropdowns while keeping the field free-typeable.
1049
+ //
1050
+ // inputEl : the editable <input> (kept typeable; free text is preserved)
1051
+ // popupEl : the (initially empty) .model-combo-popup container to render into
1052
+ // models : array of { id, name } discovery results (may be empty)
1053
+ //
1054
+ // The popup is positioned with position:fixed at open time (see the CSS) so it
1055
+ // escapes the .settings-content / .settings-body overflow clipping and floats
1056
+ // like a native dropdown. All listeners are element-scoped so they are GC'd
1057
+ // with the input when loadModelsForRuntime re-renders (no global leak).
1058
+ function _wireModelCombobox(inputEl, popupEl, models) {
1059
+ if (!inputEl || !popupEl) return;
1060
+ const options = (models || [])
1061
+ .map(function(m) {
1062
+ const id = m.id || m.name || '';
1063
+ const label = (m.name && m.name !== id) ? m.name : '';
1064
+ return { id: id, label: label };
1065
+ })
1066
+ .filter(function(o) { return o.id; });
1067
+ if (options.length === 0) return;
1068
+ let visible = [];
1069
+ let activeIdx = -1;
1070
+
1071
+ function position() {
1072
+ const r = inputEl.getBoundingClientRect();
1073
+ popupEl.style.left = r.left + 'px';
1074
+ popupEl.style.top = r.bottom + 'px';
1075
+ popupEl.style.width = r.width + 'px';
1076
+ }
1077
+ function render() {
1078
+ const q = (inputEl.value || '').toLowerCase();
1079
+ // A value that exactly equals an option shows the full list, so re-focusing
1080
+ // a filled field still offers every model (matching native <select>).
1081
+ const exact = options.some(function(o) { return o.id.toLowerCase() === q; });
1082
+ visible = (!q || exact) ? options.slice()
1083
+ : options.filter(function(o) {
1084
+ return o.id.toLowerCase().indexOf(q) !== -1 || (o.label && o.label.toLowerCase().indexOf(q) !== -1);
1085
+ });
1086
+ if (visible.length === 0) { hide(); return; }
1087
+ if (activeIdx >= visible.length) activeIdx = -1;
1088
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: model id, model label)
1089
+ popupEl.innerHTML = visible.map(function(o, i) {
1090
+ return '<div class="model-combo-option' + (i === activeIdx ? ' active' : '') + '" data-idx="' + i + '">' +
1091
+ '<span class="model-combo-id">' + escHtml(o.id) + '</span>' +
1092
+ (o.label ? '<span class="model-combo-label">' + escHtml(o.label) + '</span>' : '') +
1093
+ '</div>';
1094
+ }).join('');
1095
+ position();
1096
+ popupEl.classList.add('visible');
1097
+ inputEl.setAttribute('aria-expanded', 'true');
1098
+ }
1099
+ function hide() {
1100
+ popupEl.classList.remove('visible');
1101
+ inputEl.setAttribute('aria-expanded', 'false');
1102
+ activeIdx = -1;
1103
+ }
1104
+ function pick(o) {
1105
+ if (!o) return;
1106
+ inputEl.value = o.id;
1107
+ hide();
1108
+ }
1109
+ function scrollActive() {
1110
+ const el = popupEl.querySelector('.model-combo-option.active');
1111
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
1112
+ }
1113
+
1114
+ inputEl.addEventListener('focus', render);
1115
+ // A click on an already-focused input does not re-fire `focus`, so after a
1116
+ // selection (pick → hide) or Escape (hide) the still-focused input could not
1117
+ // be reopened by clicking — it only stays a dead field until blur+refocus.
1118
+ // Reopen the popup on click whenever it is currently hidden (matches how a
1119
+ // native <select> reopens on click). No-op on the focus-triggering click,
1120
+ // which already opened it, so there is no open/close flicker.
1121
+ inputEl.addEventListener('click', function() {
1122
+ if (!popupEl.classList.contains('visible')) render();
1123
+ });
1124
+ inputEl.addEventListener('input', function() { activeIdx = -1; render(); });
1125
+ inputEl.addEventListener('keydown', function(ev) {
1126
+ const open = popupEl.classList.contains('visible');
1127
+ if (ev.key === 'ArrowDown') {
1128
+ ev.preventDefault();
1129
+ if (!open) { render(); return; }
1130
+ activeIdx = Math.min(activeIdx + 1, visible.length - 1);
1131
+ render(); scrollActive();
1132
+ } else if (ev.key === 'ArrowUp') {
1133
+ if (!open) return;
1134
+ ev.preventDefault();
1135
+ activeIdx = Math.max(activeIdx - 1, 0);
1136
+ render(); scrollActive();
1137
+ } else if (ev.key === 'Enter') {
1138
+ if (open && activeIdx >= 0) { ev.preventDefault(); pick(visible[activeIdx]); }
1139
+ } else if (ev.key === 'Escape') {
1140
+ if (open) { ev.stopPropagation(); hide(); }
1141
+ }
1142
+ });
1143
+ // mousedown (not click) so the pick fires before the input's blur hides it.
1144
+ popupEl.addEventListener('mousedown', function(ev) {
1145
+ const row = ev.target.closest('.model-combo-option');
1146
+ if (!row) return;
1147
+ ev.preventDefault();
1148
+ pick(visible[Number(row.getAttribute('data-idx'))]);
1149
+ });
1150
+ inputEl.addEventListener('blur', function() { setTimeout(hide, 120); });
1151
+ }
1152
+
998
1153
  /**
999
1154
  * Render an editable model field backed by discovered-model suggestions.
1000
1155
  * Discovery is advisory: staged/future/custom model IDs must remain typeable.
@@ -1005,7 +1160,7 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue, forceRef
1005
1160
  const token = _nextModelLoadToken('runtime', inputId);
1006
1161
  if (!runtimeName) {
1007
1162
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: currentValue; inputId is an internal fixed DOM id)
1008
- wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="(no runtime selected)" disabled style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:var(--text-md)">';
1163
+ wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="(no runtime selected)" disabled style="' + MODEL_FIELD_INPUT_STYLE + ';color:var(--muted)">';
1009
1164
  return;
1010
1165
  }
1011
1166
  let payload = { models: null };
@@ -1013,17 +1168,14 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue, forceRef
1013
1168
 
1014
1169
  if (!_isCurrentModelLoad('runtime', inputId, token)) return;
1015
1170
  const models = Array.isArray(payload.models) ? payload.models : null;
1016
- const listId = inputId + '-options';
1017
- let opts = '';
1018
- for (const m of (models || [])) {
1019
- const id = m.id || m.name || '';
1020
- if (!id) continue;
1021
- const label = m.name && m.name !== id ? m.name : '';
1022
- opts += '<option value="' + escHtml(id) + '"' + (label ? ' label="' + escHtml(label) + '"' : '') + '></option>';
1023
- }
1024
1171
  const title = forceRefresh ? 'Model list refreshed' : 'Refresh models from the selected CLI';
1025
- // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml()
1026
- wrap.innerHTML = '<div style="display:flex;gap:4px"><input id="' + inputId + '" list="' + listId + '" value="' + escHtml(currentValue || '') + '" placeholder="Default (CLI chooses)" autocomplete="off" style="min-width:0;flex:1;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)"><button type="button" data-model-refresh title="' + escHtml(title) + '" aria-label="Refresh model list" style="padding:3px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer">↻</button></div><datalist id="' + listId + '">' + opts + '</datalist>';
1172
+ // Editable model input + themed suggestion popover (see _wireModelCombobox).
1173
+ // The field stays free-typeable for staged/custom IDs; discovered models are
1174
+ // offered through .model-combo-popup so the launched list matches the sibling
1175
+ // native <select> dropdowns instead of the browser's <datalist> chrome.
1176
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: currentValue, title; inputId is an internal fixed DOM id)
1177
+ wrap.innerHTML = '<div class="model-combo" style="width:100%"><div style="display:flex;gap:4px;width:100%"><input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="Default (CLI chooses)" autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" style="min-width:0;flex:1;' + MODEL_FIELD_INPUT_BOX + ';' + MODEL_FIELD_CHEVRON + '"><button type="button" data-model-refresh title="' + escHtml(title) + '" aria-label="Refresh model list" style="padding:3px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer">↻</button></div><div class="model-combo-popup" data-model-popup></div></div>';
1178
+ _wireModelCombobox(document.getElementById(inputId), wrap.querySelector('[data-model-popup]'), models || []);
1027
1179
  const refresh = wrap.querySelector('[data-model-refresh]');
1028
1180
  if (refresh) {
1029
1181
  refresh.addEventListener('click', async function() {
@@ -405,3 +405,25 @@
405
405
  </div>
406
406
  </div>
407
407
 
408
+ <!-- Reused CLASSIC agent detail slide-in panel (W-mrz11djx00050594). Opened by
409
+ clicking a Team member card; populated by the reused dashboard/js panel
410
+ modules (openAgentDetail → renderDetailTabs/renderDetailContent) exposed on
411
+ window.MinionsAgents. Structure mirrors dashboard/layout.html so the shared
412
+ JS + CSS resolve unchanged. -->
413
+ <div class="detail-overlay" id="detail-overlay" onclick="closeDetail()"></div>
414
+ <div class="detail-panel" id="detail-panel">
415
+ <div class="detail-header">
416
+ <h3 id="detail-agent-name">—</h3>
417
+ <button class="detail-close" onclick="closeDetail()">ESC / Close</button>
418
+ </div>
419
+ <div class="status-line" id="detail-status-line">—</div>
420
+ <div class="detail-tabs" id="detail-tabs"></div>
421
+ <div class="detail-body">
422
+ <div class="detail-content" id="detail-content"></div>
423
+ </div>
424
+ </div>
425
+
426
+ <!-- Floating toast target for the reused panel's charter-save / steering
427
+ feedback (showToast in panel-bootstrap.js routes here). -->
428
+ <div class="cmd-toast" id="cmd-toast" role="status" aria-live="polite"></div>
429
+
@@ -23,6 +23,12 @@
23
23
  function renderMembers(agents) {
24
24
  var grid = document.getElementById('member-grid');
25
25
  if (!grid) return;
26
+ // Feed the reused CLASSIC detail panel (W-mrz11djx00050594): its
27
+ // openAgentDetail(id) does agentData.find(...), so keep the global
28
+ // agentData (declared in slim/panel-bootstrap.js) in sync with the live
29
+ // /api/status agents. Set before the unchanged-early-return so it stays
30
+ // current even on no-op polls.
31
+ try { window.agentData = agents; } catch { /* non-fatal */ }
26
32
  var json = JSON.stringify(agents);
27
33
  if (json === _lastAgentsJson) return;
28
34
  _lastAgentsJson = json;
@@ -43,9 +49,9 @@
43
49
  card.setAttribute('tabindex', '0');
44
50
  card.title = (a.name || a.id) + (a.role ? ' — ' + a.role : '');
45
51
  // The clicked agent object is captured directly — no id->lookup round-trip.
46
- card.addEventListener('click', function() { openAgentDetail(a); });
52
+ card.addEventListener('click', function() { openAgentDetailPanel(a); });
47
53
  card.addEventListener('keydown', function(ev) {
48
- if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openAgentDetail(a); }
54
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openAgentDetailPanel(a); }
49
55
  });
50
56
  // Hover (and keyboard focus, for tabindex=0 parity) reveals the inline
51
57
  // details bar under the grid without opening the click->modal. Listeners
@@ -219,8 +225,29 @@
219
225
  body.appendChild(row);
220
226
  }
221
227
 
228
+ // Route an agent-card click to the CLASSIC rich detail slide-in panel
229
+ // (W-mrz11djx00050594) — tabs + live-output stream + charter editor + full
230
+ // status — reused verbatim from the classic dashboard and exposed on
231
+ // window.MinionsAgents by dashboard/js/render-agents.js (injected into the
232
+ // slim document as a separate global <script>; see dashboard-build.js).
233
+ // Falls back to the slim mini-modal only if that panel bundle is genuinely
234
+ // unavailable, so a build/regression that drops the panel still shows detail.
235
+ function openAgentDetailPanel(a) {
236
+ if (!a || !a.id) return;
237
+ var MA = window.MinionsAgents;
238
+ if (MA && typeof MA.openAgentDetail === 'function') {
239
+ // openAgentDetail(id) resolves the agent via the global agentData, which
240
+ // renderMembers keeps in sync; pass the id (classic takes an id, not the
241
+ // whole object).
242
+ MA.openAgentDetail(a.id);
243
+ return;
244
+ }
245
+ openAgentDetail(a); // graceful fallback: slim mini-modal
246
+ }
247
+
222
248
  // Populate + open the agent detail modal for the given agent object (passed
223
- // straight from the clicked card).
249
+ // straight from the clicked card). Retained as the fallback for
250
+ // openAgentDetailPanel when the reused classic panel is unavailable.
224
251
  function openAgentDetail(a) {
225
252
  var modal = document.getElementById('slim-agent-modal');
226
253
  var body = document.getElementById('slim-agent-body');
@@ -18,5 +18,13 @@
18
18
  /* __JS__ */
19
19
  })();
20
20
  </script>
21
+ <!-- Reused CLASSIC agent detail slide-in panel (W-mrz11djx00050594). Runs at
22
+ GLOBAL, non-strict scope — the same scope the classic dashboard uses — so
23
+ the reused dashboard/js panel modules behave identically. Kept separate from
24
+ (and after) the slim IIFE above; it exposes window.MinionsAgents.openAgentDetail(id),
25
+ which members.js calls on card click. -->
26
+ <script>
27
+ /* __PANEL_JS__ */
28
+ </script>
21
29
  </body>
22
30
  </html>
@@ -0,0 +1,83 @@
1
+ // dashboard/slim/panel-bootstrap.js — glue for reusing the CLASSIC agent detail
2
+ // slide-in panel inside the Slim-UX document (W-mrz11djx00050594).
3
+ //
4
+ // The classic panel modules (utils, render-utils, charter-editor, live-stream,
5
+ // render-agents, detail-panel) are the SAME real files the classic dashboard
6
+ // ships — dashboard-build.js concatenates them after this bootstrap into one
7
+ // GLOBAL, non-strict <script> so they run in the exact scope they were written
8
+ // for (unlike slim's own strict IIFE). Those files read a handful of primitives
9
+ // that normally live in classic's state.js / command-parser.js, which slim does
10
+ // NOT bundle. Rather than fork the panel, we wire up just those primitives here:
11
+ //
12
+ // - agentData / currentAgentId / currentTab (classic panel module state)
13
+ // - safeFetch (state.js — API fetch w/ timeout)
14
+ // - showToast (command-parser.js — toast feedback)
15
+ // - DASHBOARD_TAB_ID (state.js — per-tab id header)
16
+ //
17
+ // Declared with `var` at the top of the global panel <script> so they become
18
+ // window properties; slim's IIFE (dashboard/slim/js/members.js) sets
19
+ // `window.agentData` before invoking `window.MinionsAgents.openAgentDetail(id)`.
20
+
21
+ // Classic panel module state (mirrors dashboard/js/state.js declarations).
22
+ var agentData = [];
23
+ var currentAgentId = null;
24
+ var currentTab = 'thought-process';
25
+
26
+ // Per-tab id echoed to /api/* via safeFetch, mirroring state.js. Best-effort —
27
+ // sessionStorage may throw in locked-down contexts, so fall back to an ephemeral id.
28
+ var DASHBOARD_TAB_ID = (function() {
29
+ try {
30
+ var existing = sessionStorage.getItem('minions-dashboard-tab-id');
31
+ if (existing) return existing;
32
+ var id = 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
33
+ sessionStorage.setItem('minions-dashboard-tab-id', id);
34
+ return id;
35
+ } catch {
36
+ return 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
37
+ }
38
+ })();
39
+
40
+ // Abort-on-timeout fetch with the dashboard-tab headers — copied verbatim from
41
+ // dashboard/js/state.js so the reused panel modules (openAgentDetail, charter
42
+ // save, live-stream refresh/steer) behave identically to the classic surface.
43
+ function safeFetch(url, opts) {
44
+ var timeout = (opts && opts.timeout) || 15000;
45
+ var controller = new AbortController();
46
+ var timer = setTimeout(function() { controller.abort(); }, timeout);
47
+ var fetchOpts = Object.assign({}, opts, { signal: controller.signal });
48
+ if (typeof url === 'string' && url.indexOf('/api/') === 0) {
49
+ var headers = Object.assign({}, fetchOpts.headers || {});
50
+ headers['X-Minions-Dashboard-Tab'] = DASHBOARD_TAB_ID;
51
+ headers['X-Minions-Dashboard-Url'] = location.pathname + location.search + location.hash;
52
+ headers['X-Minions-Dashboard-Visibility'] = document.visibilityState || '';
53
+ fetchOpts.headers = headers;
54
+ }
55
+ delete fetchOpts.timeout;
56
+ return fetch(url, fetchOpts).finally(function() { clearTimeout(timer); });
57
+ }
58
+
59
+ // Minimal toast used by the reused charter-editor (save) and live-stream
60
+ // (steering) modules. Mirrors the graceful-degradation contract of the classic
61
+ // showToast (dashboard/js/command-parser.js): warn + return when the target
62
+ // element is absent so a missing #cmd-toast can never throw.
63
+ function showToast(id, msg, ok, durationMs) {
64
+ var el = document.getElementById(id);
65
+ if (!el) { try { console.warn('[showToast] no element with id', id, '— message:', msg); } catch {} return; }
66
+ el.classList.add('cmd-toast');
67
+ el.classList.remove('success', 'error');
68
+ el.classList.add(ok ? 'success' : 'error');
69
+ el.textContent = String(msg == null ? '' : msg);
70
+ el.classList.add('open');
71
+ setTimeout(function() { el.classList.remove('success', 'error', 'open'); }, durationMs || 4000);
72
+ }
73
+
74
+ // ESC closes the slide-in panel (parity with the classic "ESC / Close" button).
75
+ // closeDetail() is defined later in this same global script by detail-panel.js;
76
+ // it is only invoked on keydown (runtime), so load-order is fine.
77
+ document.addEventListener('keydown', function(e) {
78
+ if (e.key !== 'Escape') return;
79
+ var panel = document.getElementById('detail-panel');
80
+ if (panel && panel.classList.contains('open') && typeof closeDetail === 'function') {
81
+ closeDetail();
82
+ }
83
+ });
@@ -1750,4 +1750,134 @@
1750
1750
  font-family: inherit;
1751
1751
  resize: vertical;
1752
1752
  }
1753
- .modal-textarea:focus { outline: none; border-color: var(--blue); }
1753
+ .modal-textarea:focus { outline: none; border-color: var(--blue); }
1754
+ /* ===================================================================
1755
+ Reused CLASSIC agent detail slide-in panel (W-mrz11djx00050594)
1756
+ -------------------------------------------------------------------
1757
+ The panel markup + JS are reused verbatim from the classic dashboard
1758
+ (dashboard/layout.html + dashboard/js/{render-agents,detail-panel,
1759
+ live-stream,charter-editor,render-utils,utils}.js). Slim and classic
1760
+ ship SEPARATE top-level stylesheets with no shared-CSS partial, so the
1761
+ panel's presentation rules are mirrored here. The rule bodies are the
1762
+ classic rules unchanged; they reference classic design tokens
1763
+ (--space-N, --radius-*, --yellow, --purple, --transition-*) that slim
1764
+ either lacks or scales differently. Those tokens are scoped to the
1765
+ panel roots below so every panel descendant resolves them to the
1766
+ classic values WITHOUT disturbing slim's global palette.
1767
+ =================================================================== */
1768
+ #detail-panel, #detail-overlay {
1769
+ --yellow: #d29922; --purple: #bc8cff; --orange: #e3b341;
1770
+ --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --radius-xl: 10px; --radius-full: 50%;
1771
+ --transition-fast: 0.15s; --transition-base: 0.2s; --transition-slow: 0.3s;
1772
+ --space-1: 2px; --space-2: 4px; --space-3: 6px; --space-4: 8px; --space-5: 10px;
1773
+ --space-6: 12px; --space-7: 16px; --space-8: 20px; --space-9: 24px;
1774
+ }
1775
+
1776
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
1777
+ .pulse { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--green); display: inline-block; margin-right: var(--space-3); animation: pulse 2s infinite; }
1778
+
1779
+ .detail-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 300; }
1780
+ .detail-overlay.open { display: block; }
1781
+ .detail-panel {
1782
+ position: fixed; top: 0; right: 0; width: 55%; max-width: 750px; height: 100vh;
1783
+ background: var(--surface); border-left: 1px solid var(--border);
1784
+ z-index: 301; display: flex; flex-direction: column;
1785
+ transform: translateX(100%); transition: transform 0.25s ease;
1786
+ }
1787
+ .detail-panel.open { transform: translateX(0); }
1788
+ .detail-header {
1789
+ padding: var(--space-7) var(--space-8); border-bottom: 1px solid var(--border);
1790
+ display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
1791
+ }
1792
+ .detail-header h3 { font-size: var(--text-2xl); color: var(--blue); display: flex; align-items: center; gap: var(--space-4); }
1793
+ .detail-close { background: none; border: 1px solid var(--border); color: var(--muted); font-size: var(--text-md); cursor: pointer; padding: var(--space-2) var(--space-6); border-radius: var(--radius-sm); }
1794
+ .detail-close:hover { color: var(--text); border-color: var(--text); }
1795
+ .detail-body { flex: 1; overflow-y: auto; padding: 0; }
1796
+ .detail-tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; background: var(--bg); }
1797
+ .detail-tab {
1798
+ padding: var(--space-5) var(--space-7); font-size: var(--text-md); font-weight: 500; color: var(--muted);
1799
+ cursor: pointer; border-bottom: 2px solid transparent; transition: all var(--transition-base);
1800
+ }
1801
+ .detail-tab:hover { color: var(--text); }
1802
+ .detail-tab.active { color: var(--blue); border-bottom-color: var(--blue); }
1803
+ .detail-content {
1804
+ padding: var(--space-7) var(--space-8); font-size: var(--text-md); line-height: 1.7;
1805
+ white-space: pre-wrap; word-wrap: break-word; font-family: Consolas, monospace; color: var(--muted);
1806
+ }
1807
+ .detail-content h4 { color: var(--text); font-size: var(--text-lg); margin: var(--space-6) 0 var(--space-3) 0; font-family: 'Segoe UI', sans-serif; }
1808
+ .detail-content .section { margin-bottom: var(--space-7); padding: var(--space-6); background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius-md); }
1809
+ .status-line { display: flex; align-items: center; gap: var(--space-5); padding: var(--space-5) var(--space-7); background: var(--bg); border-bottom: 1px solid var(--border); font-size: var(--text-md); }
1810
+
1811
+ .status-badge { font-size: var(--text-sm); font-weight: 600; padding: var(--space-1) var(--space-4); border-radius: var(--radius-xl); text-transform: uppercase; letter-spacing: 0.5px; }
1812
+ .status-badge.idle { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
1813
+ .status-badge.working { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
1814
+ .status-badge.done { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
1815
+ .status-badge.error { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
1816
+
1817
+ .pr-table { width: 100%; border-collapse: collapse; font-size: var(--text-md); table-layout: auto; }
1818
+ .pr-table th, .pr-table td { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); text-align: left; }
1819
+ .pr-table th { color: var(--muted); font-weight: 600; font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.4px; }
1820
+
1821
+ .dispatch-type { font-size: var(--text-sm); font-weight: 600; padding: var(--space-1) var(--space-4); border-radius: var(--radius-lg); text-transform: uppercase; white-space: nowrap; }
1822
+ .dispatch-type.implement { background: rgba(88,166,255,0.15); color: var(--blue); }
1823
+ .dispatch-type.review { background: rgba(188,140,255,0.15); color: var(--purple); }
1824
+ .dispatch-type.fix { background: rgba(210,153,34,0.15); color: var(--yellow); }
1825
+ .dispatch-type.analyze { background: rgba(63,185,80,0.15); color: var(--green); }
1826
+ .dispatch-type.explore { background: rgba(139,148,158,0.15); color: var(--muted); }
1827
+ .dispatch-type.test { background: rgba(227,179,65,0.15); color: var(--orange); }
1828
+ .dispatch-type.plan { background: rgba(168,85,247,0.15); color: #a855f7; }
1829
+ .dispatch-type.ask { background: rgba(63,185,80,0.15); color: var(--green); }
1830
+ .dispatch-type.verify { background: rgba(63,185,80,0.2); color: var(--green); }
1831
+ .dispatch-type.meeting { background: rgba(88,166,255,0.2); color: var(--blue); }
1832
+ .dispatch-type.decompose { background: rgba(188,140,255,0.2); color: var(--purple); }
1833
+ .dispatch-type.docs { background: rgba(56,189,248,0.15); color: #38bdf8; }
1834
+ .dispatch-type.setup { background: rgba(20,184,166,0.15); color: #14b8a6; }
1835
+
1836
+ .pr-pager-btn {
1837
+ background: var(--surface2); border: 1px solid var(--border); color: var(--muted);
1838
+ font-size: var(--text-base); padding: var(--space-2) var(--space-5); border-radius: var(--radius-sm); cursor: pointer; transition: all var(--transition-base);
1839
+ }
1840
+ .pr-pager-btn:hover { color: var(--text); border-color: var(--text); }
1841
+
1842
+ .btn-primary-lg {
1843
+ background: var(--blue); color: #fff; border: none; border-radius: var(--radius-sm);
1844
+ padding: 6px 16px; font-size: var(--text-md); cursor: pointer; transition: opacity var(--transition-base);
1845
+ }
1846
+ .btn-primary-lg:hover { opacity: 0.9; }
1847
+
1848
+ .modal-copy { background: var(--surface2); border: 1px solid var(--border); color: var(--muted); font-size: var(--text-base); cursor: pointer; padding: var(--space-2) var(--space-5); border-radius: var(--radius-sm); display: flex; align-items: center; gap: var(--space-2); transition: all var(--transition-base); }
1849
+ .modal-copy:hover { color: var(--text); border-color: var(--text); }
1850
+ .modal-copy.is-success { color: var(--green); border-color: var(--green); }
1851
+ .modal-copy.is-danger { color: var(--red); border-color: var(--red); }
1852
+
1853
+ .artifact-chip {
1854
+ display: inline-flex; align-items: center; gap: var(--space-1);
1855
+ padding: var(--space-1) var(--space-4); border-radius: 10px;
1856
+ font-size: var(--text-sm); cursor: pointer;
1857
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text);
1858
+ transition: border-color var(--transition-base), color var(--transition-base);
1859
+ user-select: none;
1860
+ }
1861
+ .artifact-chip:hover { border-color: var(--blue); color: var(--blue); }
1862
+ .artifact-chip-icon { font-size: var(--text-sm); line-height: 1; }
1863
+ .artifact-chip-label { font-size: var(--text-sm); }
1864
+ .artifact-chip.deleted { cursor: not-allowed; text-decoration: line-through; color: var(--muted); opacity: 0.6; }
1865
+ .artifact-chip-unknown { color: var(--muted); }
1866
+
1867
+ /* Markdown content — consistent styling everywhere renderMd() is used inside the panel */
1868
+ .detail-panel .md-content { font-family: 'Segoe UI', system-ui, sans-serif; font-size: var(--text-md); line-height: 1.6; white-space: normal; word-break: break-word; overflow-x: auto; }
1869
+ .detail-panel .md-content pre { font-family: Consolas, 'Courier New', monospace; white-space: pre-wrap; max-width: 100%; }
1870
+ .detail-panel .md-content code { font-family: Consolas, 'Courier New', monospace; }
1871
+ .detail-panel .md-content table { border-collapse: collapse; margin: 6px 0; width: max-content; min-width: 100%; }
1872
+ .detail-panel .md-content th, .detail-panel .md-content td { padding: 4px 8px; border: 1px solid var(--border); text-align: left; font-size: var(--text-base); white-space: nowrap; }
1873
+ .detail-panel .md-content td { white-space: normal; min-width: 60px; }
1874
+ .detail-panel .md-content th { background: var(--surface); font-weight: 600; }
1875
+ .detail-panel .md-content .md-p { margin: 0 0 6px; }
1876
+ .detail-panel .md-content .md-p:last-child { margin-bottom: 0; }
1877
+ .detail-panel .md-content li { margin: 1px 0; }
1878
+
1879
+ /* Floating toast target for the reused panel (charter save / steering feedback). */
1880
+ .cmd-toast { position: fixed; bottom: 16px; right: 16px; z-index: 500; max-width: 360px; padding: 10px 14px; border-radius: 6px; background: var(--surface2); border: 1px solid var(--border); color: var(--text); font-size: var(--text-md); box-shadow: 0 4px 16px rgba(0,0,0,0.4); opacity: 0; pointer-events: none; transition: opacity 0.2s ease; }
1881
+ .cmd-toast.open { opacity: 1; }
1882
+ .cmd-toast.success { border-color: var(--green); }
1883
+ .cmd-toast.error { border-color: var(--red); }
@@ -889,6 +889,33 @@
889
889
  .cmd-mention-item .mention-name { font-weight: 600; color: var(--purple); }
890
890
  .cmd-mention-item .mention-role { color: var(--muted); font-size: var(--text-base); }
891
891
 
892
+ /* Model-picker combobox (Settings → Agents Runtime & Models, PR #916).
893
+ The Default Model / CC Model fields stay editable <input>s (staged/custom
894
+ model IDs must remain typeable) but the launched suggestion list is a
895
+ dashboard-themed popover instead of the browser's native <datalist>
896
+ chrome, so it matches the sibling native <select> dropdowns. Positioned
897
+ with position:fixed (coords set in JS at open time) so it escapes the
898
+ .settings-content / .settings-body overflow clipping and floats like a
899
+ native dropdown. z-index sits above modal content (.modal-bg is 400). */
900
+ .model-combo { position: relative; }
901
+ .model-combo-popup {
902
+ display: none; position: fixed; margin-top: 2px;
903
+ background: var(--surface); border: 1px solid var(--border); border-radius: 4px;
904
+ padding: 2px 0; box-shadow: var(--shadow-lg); z-index: 500;
905
+ max-height: 260px; overflow-y: auto;
906
+ scrollbar-width: thin; scrollbar-color: var(--border) transparent;
907
+ }
908
+ .model-combo-popup.visible { display: block; }
909
+ .model-combo-popup::-webkit-scrollbar { width: 4px; }
910
+ .model-combo-popup::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
911
+ .model-combo-option {
912
+ padding: 4px 8px; font-size: var(--text-md); color: var(--text); cursor: pointer;
913
+ display: flex; align-items: baseline; gap: 6px; white-space: nowrap;
914
+ }
915
+ .model-combo-option:hover { background: var(--surface2); }
916
+ .model-combo-option.active { background: rgba(88,166,255,0.15); }
917
+ .model-combo-label { color: var(--muted); font-size: var(--text-xs); }
918
+
892
919
  /* Hints bar */
893
920
  .cmd-hints {
894
921
  display: flex; gap: var(--space-6); margin-top: var(--space-2); font-size: var(--text-sm); color: var(--muted);
@@ -79,6 +79,18 @@ const SLIM_JS_ORDER = [
79
79
  'command-send', 'status', 'members', 'modals-tiles', 'history', 'pinned', 'knowledge', 'plans',
80
80
  ];
81
81
 
82
+ // W-mrz11djx00050594 — the CLASSIC agent detail slide-in panel, reused verbatim
83
+ // inside Slim-UX. These are the real dashboard/js/*.js modules the classic
84
+ // dashboard ships (no fork); they are concatenated after slim/panel-bootstrap.js
85
+ // into a single GLOBAL, non-strict <script> (the /* __PANEL_JS__ */ slot) so
86
+ // they run in the same global scope they were written for, separate from slim's
87
+ // strict IIFE. Order is load-bearing: helpers before consumers, and
88
+ // charter-editor MUST precede detail-panel (see the "charter-editor registered
89
+ // before detail-panel" test).
90
+ const SLIM_CLASSIC_PANEL_JS = [
91
+ 'utils', 'render-utils', 'charter-editor', 'live-stream', 'render-agents', 'detail-panel',
92
+ ];
93
+
82
94
  // Cache for the assembled slim source fragments (layout/css/body/js). Keyed on
83
95
  // the input file mtimes so dev edits still take effect on the next request
84
96
  // without a server restart, while production serves repeated requests without
@@ -92,8 +104,10 @@ function _slimSourcePaths(slimDir) {
92
104
  path.join(slimDir, 'layout.html'),
93
105
  path.join(slimDir, 'styles.css'),
94
106
  path.join(slimDir, 'body.html'),
107
+ path.join(slimDir, 'panel-bootstrap.js'),
95
108
  ...DASHBOARD_SHARED_JS.map(f => path.join(MINIONS_DIR, 'dashboard', 'shared', f + '.js')),
96
109
  ...SLIM_JS_ORDER.map(f => path.join(slimDir, 'js', f + '.js')),
110
+ ...SLIM_CLASSIC_PANEL_JS.map(f => path.join(MINIONS_DIR, 'dashboard', 'js', f + '.js')),
97
111
  ];
98
112
  }
99
113
 
@@ -112,7 +126,14 @@ function buildSlimHtml(opts) {
112
126
  if (!_slimPartsCache
113
127
  || _slimPartsCache.mtimes.length !== mtimes.length
114
128
  || _slimPartsCache.mtimes.some((m, i) => m !== mtimes[i])) {
115
- const [lp, cp, bp, ...jsPaths] = paths;
129
+ // paths layout: [layout, styles, body, panel-bootstrap,
130
+ // ...DASHBOARD_SHARED_JS, ...SLIM_JS_ORDER, ...SLIM_CLASSIC_PANEL_JS].
131
+ // The slim IIFE gets shared + slim js; the classic panel gets its own
132
+ // global <script> (bootstrap + classic modules) — see /* __PANEL_JS__ */.
133
+ const [lp, cp, bp, bootstrapPath, ...restPaths] = paths;
134
+ const iifeCount = DASHBOARD_SHARED_JS.length + SLIM_JS_ORDER.length;
135
+ const iifePaths = restPaths.slice(0, iifeCount);
136
+ const panelPaths = restPaths.slice(iifeCount);
116
137
  _slimPartsCache = {
117
138
  mtimes,
118
139
  layout: safeRead(lp),
@@ -121,10 +142,14 @@ function buildSlimHtml(opts) {
121
142
  // Join with a newline so a fragment that loses its trailing newline
122
143
  // can't glue two statements together (e.g. `}var foo`). Parts share one
123
144
  // IIFE scope, so order is load-bearing — see SLIM_JS_ORDER.
124
- js: jsPaths.map(p => safeRead(p)).join('\n'),
145
+ js: iifePaths.map(p => safeRead(p)).join('\n'),
146
+ // Reused classic agent-detail panel — bootstrap glue first, then the real
147
+ // classic modules. Emitted into a separate GLOBAL, non-strict <script>
148
+ // (the /* __PANEL_JS__ */ slot) so they run in their original scope.
149
+ panelJs: [bootstrapPath, ...panelPaths].map(p => safeRead(p)).join('\n'),
125
150
  };
126
151
  }
127
- const { layout, css, body, js } = _slimPartsCache;
152
+ const { layout, css, body, js, panelJs } = _slimPartsCache;
128
153
 
129
154
  // Feature-flag bootstrap (window.MINIONS_FEATURES) injected at the top of the
130
155
  // slim IIFE. serveSlimUx passes the live flags as JSON via opts.featuresJson;
@@ -138,6 +163,7 @@ function buildSlimHtml(opts) {
138
163
  // an exact '\n' match would silently no-op and ship an empty body on CRLF).
139
164
  .replace(/<!-- __BODY__ -->\r?\n/, () => body)
140
165
  .replace('{{minions_features}}', () => featuresJson)
166
+ .replace('/* __PANEL_JS__ */', () => panelJs)
141
167
  .replace('/* __JS__ */', () => js);
142
168
  }
143
169
 
@@ -767,10 +767,17 @@ const KNOWN_EVENT_TYPES = new Set([
767
767
  * Returns { text, usage, sessionId, model } — same shape as the Claude adapter
768
768
  * so engine/lifecycle.js can consume both transparently.
769
769
  *
770
- * - text: concatenation of answer `assistant.message.data.content`
771
- * values, plus trailing deltas if the CLI exits before a final
772
- * `assistant.message`. Narration attached to tool requests is
773
- * progress text and is excluded from the final answer.
770
+ * - text: the TRAILING text segment of the turn — answer
771
+ * `assistant.message.data.content` values plus trailing deltas,
772
+ * but only those that arrived AFTER the last real (non-
773
+ * `task_complete`) tool boundary. Narration attached to tool
774
+ * requests is progress text and is excluded from the final
775
+ * answer, and a real tool call resets the accumulators so pre-
776
+ * tool text never welds onto the post-tool answer. This mirrors
777
+ * the stream consumer's `_resetTrailingSegment` and Claude's
778
+ * result-carries-last-message shape, so the terminal
779
+ * reconciliation in engine/llm.js#finalize agrees with the live
780
+ * stream instead of re-welding split blocks (W-mrw87290000b2965).
774
781
  * - usage: mapped from the terminal `result` event. Copilot doesn't
775
782
  * report cost/tokens — those fields are NULL, not 0, so the
776
783
  * dashboard can distinguish "Copilot didn't tell us" from
@@ -809,10 +816,15 @@ function parseOutput(raw, { maxTextLength = 0 } = {}) {
809
816
  const content = obj.data?.content;
810
817
  const toolRequests = obj.data?.toolRequests;
811
818
  const hasToolRequests = Array.isArray(toolRequests) && toolRequests.length > 0;
819
+ let sawRealTool = false;
812
820
  if (hasToolRequests) {
813
821
  for (const tr of toolRequests) {
814
- const summary = tr?.name === 'task_complete' ? tr.arguments?.summary || tr.intentionSummary : '';
815
- if (typeof summary === 'string' && summary) taskCompleteSummary = summary;
822
+ if (tr?.name === 'task_complete') {
823
+ const summary = tr.arguments?.summary || tr.intentionSummary;
824
+ if (typeof summary === 'string' && summary) taskCompleteSummary = summary;
825
+ } else if (tr?.name) {
826
+ sawRealTool = true;
827
+ }
816
828
  }
817
829
  }
818
830
  if (typeof content === 'string') {
@@ -821,9 +833,32 @@ function parseOutput(raw, { maxTextLength = 0 } = {}) {
821
833
  }
822
834
  const ot = obj.data?.outputTokens;
823
835
  if (typeof ot === 'number') outputTokensTotal += ot;
836
+ // A real (non-task_complete) tool call closes the trailing text segment:
837
+ // the final answer is whatever text arrives AFTER it. Reset the trailing
838
+ // accumulators so pre-tool narration — whether it arrived as a finalized
839
+ // assistant.message or as raw deltas — does not weld onto the post-tool
840
+ // answer. Without this, `text = messageContents.join('') + pendingDeltaContent`
841
+ // re-welds `<pre-tool><post-tool>` in the terminal reconciliation
842
+ // (engine/llm.js#finalize -> runtime.parseOutput), and the CC SSE `done`
843
+ // payload re-introduces the weld the stream consumer already split apart
844
+ // (W-mrw87290000b2965). Mirrors the stream consumer's _resetTrailingSegment
845
+ // and Claude, whose result event carries only the last assistant message.
846
+ if (sawRealTool) {
847
+ messageContents.length = 0;
848
+ pendingDeltaContent = '';
849
+ }
824
850
  } else if (type === 'tool.execution_start') {
825
- const summary = obj.data?.toolName === 'task_complete' ? obj.data?.arguments?.summary : '';
826
- if (typeof summary === 'string' && summary) taskCompleteSummary = summary;
851
+ const toolName = obj.data?.toolName;
852
+ if (toolName === 'task_complete') {
853
+ const summary = obj.data?.arguments?.summary;
854
+ if (typeof summary === 'string' && summary) taskCompleteSummary = summary;
855
+ } else if (typeof toolName === 'string' && toolName) {
856
+ // Same real-tool-boundary reset as the toolRequests branch above —
857
+ // `tool.execution_start` is the standalone event shape (delta -> tool
858
+ // -> delta) that produced the original weld repro.
859
+ messageContents.length = 0;
860
+ pendingDeltaContent = '';
861
+ }
827
862
  } else if (type === 'session.task_complete') {
828
863
  const summary = obj.data?.summary;
829
864
  if (typeof summary === 'string' && summary) taskCompleteSummary = summary;
@@ -1196,6 +1231,17 @@ function createStreamConsumer(ctx) {
1196
1231
  function _resetTrailingSegment() {
1197
1232
  trailingText = '';
1198
1233
  _hadPriorTrailingMessage = false;
1234
+ // A real tool call closes the trailing text segment, so the live
1235
+ // `assistant.message_delta` accumulator must reset too — otherwise, when
1236
+ // the pre-tool text arrived ONLY as deltas (no finalizing non-tool
1237
+ // `assistant.message` to zero the buffer, e.g. delta(s) → tool.execution_start
1238
+ // → delta(s)), the post-tool deltas append onto the pre-tool text and
1239
+ // `ctx.pushText` emits the welded whole-turn buffer under the new (post-tool)
1240
+ // segmentId. The dashboard then renders the post-tool block as
1241
+ // `<pre-tool>.<post-tool>` with no separator (W-mrw87290 — CC weld across a
1242
+ // tool call). Claude's adapter resets its accumulator on tool_use for the
1243
+ // same reason (engine/runtimes/claude.js).
1244
+ copilotMessageBuffer = '';
1199
1245
  }
1200
1246
 
1201
1247
  function _captureTaskComplete(summary, success = true, { clearBuffer = false } = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2410",
3
+ "version": "0.1.2412",
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"