@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.
- package/dashboard/js/command-center.js +24 -0
- package/dashboard/js/settings.js +168 -16
- package/dashboard/slim/body.html +22 -0
- package/dashboard/slim/js/members.js +30 -3
- package/dashboard/slim/layout.html +8 -0
- package/dashboard/slim/panel-bootstrap.js +83 -0
- package/dashboard/slim/styles.css +131 -1
- package/dashboard/styles.css +27 -0
- package/dashboard-build.js +29 -3
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/engine/runtimes/copilot.js +54 -8
- package/package.json +1 -1
|
@@ -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 };
|
package/dashboard/js/settings.js
CHANGED
|
@@ -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="
|
|
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="
|
|
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_<NAME>=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)">(' +
|
|
641
|
+
'<span style="font-size:var(--text-sm);color:var(--muted)">(' + displayFeatures.length + ' registered)</span>' +
|
|
614
642
|
'</summary>' +
|
|
615
|
-
(
|
|
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
|
-
|
|
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="
|
|
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
|
-
//
|
|
1026
|
-
|
|
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() {
|
package/dashboard/slim/body.html
CHANGED
|
@@ -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() {
|
|
52
|
+
card.addEventListener('click', function() { openAgentDetailPanel(a); });
|
|
47
53
|
card.addEventListener('keydown', function(ev) {
|
|
48
|
-
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault();
|
|
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); }
|
package/dashboard/styles.css
CHANGED
|
@@ -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);
|
package/dashboard-build.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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:
|
|
771
|
-
* values
|
|
772
|
-
*
|
|
773
|
-
*
|
|
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
|
-
|
|
815
|
-
|
|
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
|
|
826
|
-
if (
|
|
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.
|
|
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"
|