@yemi33/minions 0.1.2175 → 0.1.2177
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/utils.js +8 -5
- package/dashboard/slim/body.html +12 -9
- package/dashboard/slim/js/chat.js +98 -12
- package/dashboard/slim/js/command-send.js +66 -6
- package/dashboard/slim/js/modals-tiles.js +89 -7
- package/dashboard/slim/js/projects.js +36 -28
- package/dashboard/slim/js/status.js +52 -4
- package/dashboard/slim/styles.css +218 -20
- package/dashboard.js +8 -1
- package/package.json +1 -1
package/dashboard/js/utils.js
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
// Signal the engine to tick immediately (pick up new work without waiting 60s)
|
|
4
4
|
function wakeEngine() { fetch('/api/engine/wakeup', { method: 'POST' }).catch(() => {}); }
|
|
5
5
|
|
|
6
|
-
// "Try new Slim UX" — flips the slim-ux feature flag ON then
|
|
7
|
-
// root route serves the slim cockpit (the takeover is checked at request
|
|
8
|
-
// so no engine restart is needed).
|
|
9
|
-
// the
|
|
6
|
+
// "Try new Slim UX" — flips the slim-ux feature flag ON then navigates to the
|
|
7
|
+
// root route so it serves the slim cockpit (the takeover is checked at request
|
|
8
|
+
// time, so no engine restart is needed). We navigate to '/' rather than
|
|
9
|
+
// location.reload() because the slim takeover is gated on pathname === '/';
|
|
10
|
+
// reloading from a non-home classic page (/work, /prs, …) preserves the path
|
|
11
|
+
// and silently re-serves the classic dashboard. The first-visit welcome popup
|
|
12
|
+
// is gated on the browser side in dashboard/slim/js/settings.js, not here.
|
|
10
13
|
async function trySlimUx(btn) {
|
|
11
14
|
if (btn) { btn.disabled = true; btn.textContent = 'Switching…'; }
|
|
12
15
|
try {
|
|
@@ -16,7 +19,7 @@ async function trySlimUx(btn) {
|
|
|
16
19
|
body: JSON.stringify({ id: 'slim-ux', enabled: true }),
|
|
17
20
|
});
|
|
18
21
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
19
|
-
location.
|
|
22
|
+
location.href = '/';
|
|
20
23
|
} catch (e) {
|
|
21
24
|
if (btn) { btn.disabled = false; btn.textContent = '✨ Try new Slim UX'; }
|
|
22
25
|
alert('Could not switch to Slim UX: ' + (e && e.message ? e.message : 'unknown error'));
|
package/dashboard/slim/body.html
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
<div class="panel panel-actions">
|
|
22
22
|
<div class="panel-header">
|
|
23
23
|
Actions
|
|
24
|
-
<span class="panel-
|
|
24
|
+
<span class="panel-head-meta">
|
|
25
|
+
<span class="cc-model-badge" id="cc-model-badge" hidden></span>
|
|
26
|
+
<span class="panel-sub">Command Center & quick triggers</span>
|
|
27
|
+
</span>
|
|
25
28
|
</div>
|
|
26
29
|
<div class="actions-chat">
|
|
27
30
|
<div class="chat-tabs" id="chat-tabs"></div>
|
|
@@ -39,7 +42,7 @@
|
|
|
39
42
|
rows="2"
|
|
40
43
|
placeholder="Ask anything or give a command... (Enter to send, Shift+Enter for newline)"
|
|
41
44
|
></textarea>
|
|
42
|
-
<button id="chat-stop" class="chat-stop" type="button" title="Stop the in-flight response"
|
|
45
|
+
<button id="chat-stop" class="chat-stop" type="button" title="Stop the in-flight response" aria-label="Stop agent"><svg class="chat-stop-icon" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"></circle><rect x="8" y="8" width="8" height="8" rx="1" fill="currentColor"></rect></svg></button>
|
|
43
46
|
<button id="chat-send" class="chat-send" type="button">Send</button>
|
|
44
47
|
</div>
|
|
45
48
|
</div>
|
|
@@ -77,23 +80,23 @@
|
|
|
77
80
|
count now lives in the Team cards above, so that tile is removed. -->
|
|
78
81
|
<div class="cockpit-section">
|
|
79
82
|
<div class="cockpit-grid" id="cockpit-grid">
|
|
80
|
-
<div class="cockpit-tile" data-tile="engine">
|
|
83
|
+
<div class="cockpit-tile cockpit-tile--full" data-tile="engine">
|
|
81
84
|
<div class="cockpit-label"><span class="cockpit-dot"></span> Engine</div>
|
|
82
85
|
<div class="cockpit-value dim">—</div>
|
|
83
86
|
<div class="cockpit-detail">checking…</div>
|
|
84
87
|
</div>
|
|
85
|
-
<div class="cockpit-tile" data-tile="dispatches">
|
|
86
|
-
<div class="cockpit-label"><span class="cockpit-dot"></span> Active dispatches</div>
|
|
87
|
-
<div class="cockpit-value dim">0</div>
|
|
88
|
-
<div class="cockpit-detail">none in flight</div>
|
|
89
|
-
</div>
|
|
90
88
|
<div class="cockpit-tile" data-tile="queued">
|
|
91
89
|
<div class="cockpit-label"><span class="cockpit-dot"></span> Queued work</div>
|
|
92
90
|
<div class="cockpit-value dim">0</div>
|
|
93
91
|
<div class="cockpit-detail">queue empty</div>
|
|
94
92
|
</div>
|
|
93
|
+
<div class="cockpit-tile" data-tile="dispatches">
|
|
94
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Active dispatches</div>
|
|
95
|
+
<div class="cockpit-value dim">0</div>
|
|
96
|
+
<div class="cockpit-detail">no agents working</div>
|
|
97
|
+
</div>
|
|
95
98
|
<div class="cockpit-tile" data-tile="prs">
|
|
96
|
-
<button id="slim-tile-linkpr-chip" class="linkpr-chip on-tile" type="button" title="Link a pull request"
|
|
99
|
+
<button id="slim-tile-linkpr-chip" class="linkpr-chip on-tile" type="button" title="Link a pull request" aria-label="Link a pull request">+</button>
|
|
97
100
|
<div class="cockpit-label"><span class="cockpit-dot"></span> Active PRs</div>
|
|
98
101
|
<div class="cockpit-value dim">0</div>
|
|
99
102
|
<div class="cockpit-detail">no open PRs</div>
|
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
var messages = [];
|
|
30
30
|
var sending = false;
|
|
31
31
|
var abortController = null;
|
|
32
|
+
// Per-tab queue of follow-up messages submitted while a turn is streaming
|
|
33
|
+
// (W-mqayw3x6). Keyed by tabId so each chat tab keeps its own queue across
|
|
34
|
+
// tab switches. In-memory only — a hard refresh drops the queue by design.
|
|
35
|
+
var queues = {};
|
|
36
|
+
var queueSuspended = {}; // tabId -> true after an abort/error suspends auto-drain
|
|
37
|
+
var queueEl = null; // bottom-pinned container that holds the queued bubbles
|
|
32
38
|
var currentProject = null;
|
|
33
39
|
try { currentProject = localStorage.getItem(SLIM_PROJECT_KEY) || null; } catch (_e) { /* private mode */ }
|
|
34
40
|
|
|
@@ -241,7 +247,7 @@
|
|
|
241
247
|
} else {
|
|
242
248
|
div.textContent = text;
|
|
243
249
|
}
|
|
244
|
-
|
|
250
|
+
_appendMsgEl(div);
|
|
245
251
|
scrollToBottom();
|
|
246
252
|
return div;
|
|
247
253
|
}
|
|
@@ -249,11 +255,85 @@
|
|
|
249
255
|
var div = document.createElement('div');
|
|
250
256
|
div.className = 'chat-action ' + (severity || '');
|
|
251
257
|
div.textContent = label;
|
|
252
|
-
|
|
258
|
+
_appendMsgEl(div);
|
|
253
259
|
scrollToBottom();
|
|
254
260
|
return div;
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
// ── Queued-message rendering (W-mqayw3x6) ──────────────────────────
|
|
264
|
+
function _getQueue() {
|
|
265
|
+
return queues[tabId] || (queues[tabId] = []);
|
|
266
|
+
}
|
|
267
|
+
// Keep a single bottom-pinned container so queued bubbles always sit below
|
|
268
|
+
// the live transcript (newest content nearest the composer). appendChild on
|
|
269
|
+
// an already-attached node moves it, so this re-pins the container to the end.
|
|
270
|
+
function _ensureQueueEl() {
|
|
271
|
+
if (!queueEl || queueEl.parentNode !== msgsEl) {
|
|
272
|
+
queueEl = document.createElement('div');
|
|
273
|
+
queueEl.className = 'chat-queue';
|
|
274
|
+
}
|
|
275
|
+
msgsEl.appendChild(queueEl);
|
|
276
|
+
return queueEl;
|
|
277
|
+
}
|
|
278
|
+
// Insert a real/stream bubble above the queue container so the pending
|
|
279
|
+
// queued bubbles stay pinned at the bottom of the transcript.
|
|
280
|
+
function _appendMsgEl(el) {
|
|
281
|
+
if (queueEl && queueEl.parentNode === msgsEl) msgsEl.insertBefore(el, queueEl);
|
|
282
|
+
else msgsEl.appendChild(el);
|
|
283
|
+
}
|
|
284
|
+
// Rebuild the queued-message bubbles for the active tab: greyed-out user
|
|
285
|
+
// bubbles with a per-message dismiss. When auto-drain is suspended (after an
|
|
286
|
+
// abort/error) a "Send queued (N)" control lets the user resume explicitly.
|
|
287
|
+
function renderQueue() {
|
|
288
|
+
var q = _getQueue();
|
|
289
|
+
if (!q.length) {
|
|
290
|
+
if (queueEl && queueEl.parentNode) queueEl.parentNode.removeChild(queueEl);
|
|
291
|
+
queueEl = null;
|
|
292
|
+
queueSuspended[tabId] = false;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
_ensureQueueEl();
|
|
296
|
+
queueEl.textContent = '';
|
|
297
|
+
if (queueSuspended[tabId]) {
|
|
298
|
+
var resume = document.createElement('button');
|
|
299
|
+
resume.className = 'chat-queue-resume';
|
|
300
|
+
resume.type = 'button';
|
|
301
|
+
resume.textContent = 'Send queued (' + q.length + ')';
|
|
302
|
+
resume.title = 'Send the queued messages now';
|
|
303
|
+
resume.addEventListener('click', function() {
|
|
304
|
+
queueSuspended[tabId] = false;
|
|
305
|
+
renderQueue();
|
|
306
|
+
_drainQueue();
|
|
307
|
+
});
|
|
308
|
+
queueEl.appendChild(resume);
|
|
309
|
+
}
|
|
310
|
+
q.forEach(function(text, i) {
|
|
311
|
+
var row = document.createElement('div');
|
|
312
|
+
row.className = 'chat-msg user queued';
|
|
313
|
+
var span = document.createElement('span');
|
|
314
|
+
span.className = 'chat-queue-text';
|
|
315
|
+
span.textContent = text;
|
|
316
|
+
row.appendChild(span);
|
|
317
|
+
var badge = document.createElement('span');
|
|
318
|
+
badge.className = 'chat-queue-badge';
|
|
319
|
+
badge.textContent = 'queued';
|
|
320
|
+
row.appendChild(badge);
|
|
321
|
+
var dismiss = document.createElement('button');
|
|
322
|
+
dismiss.className = 'chat-queue-dismiss';
|
|
323
|
+
dismiss.type = 'button';
|
|
324
|
+
dismiss.textContent = '×';
|
|
325
|
+
dismiss.title = 'Remove from queue';
|
|
326
|
+
dismiss.addEventListener('click', function() {
|
|
327
|
+
var qq = _getQueue();
|
|
328
|
+
qq.splice(i, 1);
|
|
329
|
+
renderQueue();
|
|
330
|
+
});
|
|
331
|
+
row.appendChild(dismiss);
|
|
332
|
+
queueEl.appendChild(row);
|
|
333
|
+
});
|
|
334
|
+
scrollToBottom();
|
|
335
|
+
}
|
|
336
|
+
|
|
257
337
|
// Render a tool invocation as a one-line string. `full` keeps the complete
|
|
258
338
|
// command/args (used by the tool-calls modal); otherwise long values are
|
|
259
339
|
// clipped for the collapsed 3-line panel.
|
|
@@ -370,7 +450,7 @@
|
|
|
370
450
|
tp.panel.style.display = 'none';
|
|
371
451
|
toolbar.appendChild(tp.panel);
|
|
372
452
|
|
|
373
|
-
|
|
453
|
+
_appendMsgEl(div);
|
|
374
454
|
scrollToBottom();
|
|
375
455
|
|
|
376
456
|
return {
|
|
@@ -411,26 +491,32 @@
|
|
|
411
491
|
|
|
412
492
|
function setSending(on) {
|
|
413
493
|
sending = on;
|
|
414
|
-
|
|
415
|
-
|
|
494
|
+
// Composer stays interactive while a turn streams so the user can keep
|
|
495
|
+
// typing and queue follow-up messages (W-mqayw3x6 #1). The Send button
|
|
496
|
+
// stays enabled — submitting mid-turn enqueues — and only its label
|
|
497
|
+
// reflects the in-flight state.
|
|
498
|
+
sendBtn.disabled = false;
|
|
499
|
+
sendBtn.textContent = on ? 'Queue' : 'Send';
|
|
416
500
|
stopBtn.style.display = on ? 'block' : 'none';
|
|
417
|
-
inputEl.disabled =
|
|
501
|
+
inputEl.disabled = false;
|
|
418
502
|
}
|
|
419
503
|
|
|
420
504
|
function rerenderHistory() {
|
|
421
505
|
msgsEl.innerHTML = '';
|
|
506
|
+
queueEl = null; // detached by the innerHTML reset; renderQueue rebuilds it
|
|
422
507
|
if (!messages.length) {
|
|
423
508
|
var empty = document.createElement('div');
|
|
424
509
|
empty.className = 'chat-empty';
|
|
425
510
|
empty.textContent = 'No messages yet — say hi to Command Center.';
|
|
426
511
|
msgsEl.appendChild(empty);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
512
|
+
} else {
|
|
513
|
+
for (var i = 0; i < messages.length; i++) {
|
|
514
|
+
var m = messages[i];
|
|
515
|
+
if (m.role === 'action') appendActionStatus(m.severity || '', m.text || '');
|
|
516
|
+
else appendBubble(m.role, m.text || '', m.toolCalls);
|
|
517
|
+
}
|
|
433
518
|
}
|
|
519
|
+
renderQueue();
|
|
434
520
|
}
|
|
435
521
|
// ── Command Center tab bar (shared with the classic dashboard) ──
|
|
436
522
|
// One chip per shared cc-tabs entry, so the same conversations are visible
|
|
@@ -23,10 +23,60 @@
|
|
|
23
23
|
saveState();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Public entry point for the Send button / Enter key. While a turn is in
|
|
27
|
+
// flight the composer stays interactive (setSending no longer disables it),
|
|
28
|
+
// so a second submit QUEUES the message instead of dropping it or racing the
|
|
29
|
+
// active stream. The queue drains FIFO, one turn at a time (W-mqayw3x6).
|
|
30
|
+
function sendMessage() {
|
|
28
31
|
var text = inputEl.value.trim();
|
|
29
32
|
if (!text) return;
|
|
33
|
+
inputEl.value = '';
|
|
34
|
+
inputEl.style.height = 'auto';
|
|
35
|
+
if (sending) {
|
|
36
|
+
_enqueue(text);
|
|
37
|
+
inputEl.focus();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
_performSend(text);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Append to the active tab's pending queue and render the greyed bubbles.
|
|
44
|
+
function _enqueue(text) {
|
|
45
|
+
_getQueue().push(text);
|
|
46
|
+
renderQueue();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Promote the next queued message for the active tab, one at a time. No-op
|
|
50
|
+
// while a turn is in flight or the queue is suspended (after abort/error).
|
|
51
|
+
function _drainQueue() {
|
|
52
|
+
if (sending || queueSuspended[tabId]) return;
|
|
53
|
+
var q = _getQueue();
|
|
54
|
+
if (!q.length) return;
|
|
55
|
+
var next = q.shift();
|
|
56
|
+
renderQueue();
|
|
57
|
+
_performSend(next);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Called from a finished turn's `finally`. On clean completion auto-fire the
|
|
61
|
+
// next queued message; on abort or error suspend auto-drain and leave the
|
|
62
|
+
// queue visible so the user can resend or dismiss explicitly (W-mqayw3x6 #5/#6).
|
|
63
|
+
function _afterTurn(turnTabId, outcome) {
|
|
64
|
+
var q = queues[turnTabId];
|
|
65
|
+
if (!q || !q.length) return;
|
|
66
|
+
if (outcome === 'done' && turnTabId === tabId) {
|
|
67
|
+
_drainQueue();
|
|
68
|
+
} else {
|
|
69
|
+
queueSuspended[turnTabId] = true;
|
|
70
|
+
if (turnTabId === tabId) renderQueue();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function _performSend(text) {
|
|
75
|
+
// Bind this turn to the tab it started on so a mid-turn tab switch (which
|
|
76
|
+
// aborts the stream) settles the queue against the right tab, not the one
|
|
77
|
+
// the user navigated to.
|
|
78
|
+
var turnTabId = tabId;
|
|
79
|
+
var turnOutcome = 'done';
|
|
30
80
|
|
|
31
81
|
// The tab title is derived from the first user message; only that message
|
|
32
82
|
// changes the chip, so re-render the bar once instead of on every send.
|
|
@@ -36,8 +86,6 @@
|
|
|
36
86
|
saveState();
|
|
37
87
|
if (wasFirstUser) renderTabBar();
|
|
38
88
|
|
|
39
|
-
inputEl.value = '';
|
|
40
|
-
inputEl.style.height = 'auto';
|
|
41
89
|
setSending(true);
|
|
42
90
|
|
|
43
91
|
var stream = buildStreamBubble();
|
|
@@ -54,7 +102,11 @@
|
|
|
54
102
|
var res = await fetch('/api/command-center/stream', {
|
|
55
103
|
method: 'POST',
|
|
56
104
|
headers: { 'Content-Type': 'application/json' },
|
|
57
|
-
|
|
105
|
+
// currentProject is the slim project picker's explicit selection (null
|
|
106
|
+
// when the indicator reads "Select project"). noProjectSelected lets the
|
|
107
|
+
// server inject a "no project — ask the user" CC preamble instead of
|
|
108
|
+
// silently defaulting; only the slim composer sends it (W-mqayzsj3).
|
|
109
|
+
body: JSON.stringify({ message: text, tabId: tabId, sessionId: sessionId, currentProject: currentProject || undefined, noProjectSelected: currentProject ? undefined : true }),
|
|
58
110
|
signal: signal,
|
|
59
111
|
});
|
|
60
112
|
if (!res.ok) {
|
|
@@ -101,6 +153,7 @@
|
|
|
101
153
|
stream.replaceWithError(errorEvt.error || 'Error');
|
|
102
154
|
messages.push({ role: 'error', text: errorEvt.error || 'Error' });
|
|
103
155
|
saveState();
|
|
156
|
+
turnOutcome = 'error';
|
|
104
157
|
return;
|
|
105
158
|
}
|
|
106
159
|
|
|
@@ -130,6 +183,7 @@
|
|
|
130
183
|
} catch (e) {
|
|
131
184
|
if (e && (e.name === 'AbortError' || /aborted/i.test(String(e.message || '')))) {
|
|
132
185
|
userAborted = true;
|
|
186
|
+
turnOutcome = 'aborted';
|
|
133
187
|
if (streamedText) {
|
|
134
188
|
stream.finalize(streamedText);
|
|
135
189
|
messages.push({ role: 'assistant', text: streamedText, toolCalls: stream.getToolList() });
|
|
@@ -139,6 +193,7 @@
|
|
|
139
193
|
}
|
|
140
194
|
saveState();
|
|
141
195
|
} else {
|
|
196
|
+
turnOutcome = 'error';
|
|
142
197
|
var msg = 'Send failed: ' + (e && e.message ? e.message : e);
|
|
143
198
|
stream.replaceWithError(msg);
|
|
144
199
|
messages.push({ role: 'error', text: msg });
|
|
@@ -147,7 +202,8 @@
|
|
|
147
202
|
} finally {
|
|
148
203
|
setSending(false);
|
|
149
204
|
abortController = null;
|
|
150
|
-
if (!userAborted) inputEl.focus();
|
|
205
|
+
if (turnTabId === tabId && !userAborted) inputEl.focus();
|
|
206
|
+
_afterTurn(turnTabId, turnOutcome);
|
|
151
207
|
}
|
|
152
208
|
}
|
|
153
209
|
|
|
@@ -165,6 +221,10 @@
|
|
|
165
221
|
|
|
166
222
|
async function slimChatNew() {
|
|
167
223
|
if (sending) abortInFlight();
|
|
224
|
+
// New chat = fresh session: drop the leaving tab's pending queue (#7). The
|
|
225
|
+
// aborted in-flight turn's _afterTurn then finds an empty queue and no-ops.
|
|
226
|
+
delete queues[tabId];
|
|
227
|
+
queueSuspended[tabId] = false;
|
|
168
228
|
// Register a fresh tab in the shared cc-tabs store and switch slim onto
|
|
169
229
|
// it — classic's other tabs are left intact, and the previous session
|
|
170
230
|
// keeps its own server-side cc-sessions.json entry (no DELETE).
|
|
@@ -23,7 +23,15 @@
|
|
|
23
23
|
body.appendChild(d);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// One list row for the tile detail modal.
|
|
26
|
+
// One list row for the tile detail modal.
|
|
27
|
+
// opts: { title, meta, metaNodes, subtle, subtleBeforeMeta, chip:{text,cls}, href }.
|
|
28
|
+
// meta — plain-text meta line.
|
|
29
|
+
// metaNodes — array of Nodes for the meta line, interleaved with
|
|
30
|
+
// ' · ' separators; takes precedence over `meta`.
|
|
31
|
+
// subtle — optional muted second line (e.g. org/project); skipped
|
|
32
|
+
// when empty.
|
|
33
|
+
// subtleBeforeMeta — when true, subtle renders directly under the title
|
|
34
|
+
// (above the meta row); default is below the meta row.
|
|
27
35
|
// Built as an <a> when href is present so PR rows open the PR in a new tab.
|
|
28
36
|
function tileCard(opts) {
|
|
29
37
|
var card = document.createElement(opts.href ? 'a' : 'div');
|
|
@@ -43,15 +51,45 @@
|
|
|
43
51
|
top.appendChild(chip);
|
|
44
52
|
}
|
|
45
53
|
card.appendChild(top);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
function appendSubtle() {
|
|
55
|
+
if (!opts.subtle) return;
|
|
56
|
+
var subtle = document.createElement('div');
|
|
57
|
+
subtle.className = 'tile-item-subtle';
|
|
58
|
+
subtle.textContent = opts.subtle;
|
|
59
|
+
card.appendChild(subtle);
|
|
51
60
|
}
|
|
61
|
+
function appendMeta() {
|
|
62
|
+
if (opts.metaNodes && opts.metaNodes.length) {
|
|
63
|
+
var metaRow = document.createElement('div');
|
|
64
|
+
metaRow.className = 'tile-item-meta';
|
|
65
|
+
opts.metaNodes.forEach(function(node, i) {
|
|
66
|
+
if (i > 0) metaRow.appendChild(document.createTextNode(' · '));
|
|
67
|
+
metaRow.appendChild(node);
|
|
68
|
+
});
|
|
69
|
+
card.appendChild(metaRow);
|
|
70
|
+
} else if (opts.meta) {
|
|
71
|
+
var meta = document.createElement('div');
|
|
72
|
+
meta.className = 'tile-item-meta';
|
|
73
|
+
meta.textContent = opts.meta;
|
|
74
|
+
card.appendChild(meta);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (opts.subtleBeforeMeta) { appendSubtle(); appendMeta(); }
|
|
78
|
+
else { appendMeta(); appendSubtle(); }
|
|
52
79
|
return card;
|
|
53
80
|
}
|
|
54
81
|
|
|
82
|
+
// Parse the org/project slug from a canonical PR id, dropping the host prefix
|
|
83
|
+
// and the trailing "#<num>". E.g. "ado:office/iss/constellation#5315603" →
|
|
84
|
+
// "office/iss/constellation", "github:yemi33/minions#2911" → "yemi33/minions".
|
|
85
|
+
function parsePrSlug(id) {
|
|
86
|
+
if (!id || typeof id !== 'string') return '';
|
|
87
|
+
var hash = id.indexOf('#');
|
|
88
|
+
var head = hash >= 0 ? id.slice(0, hash) : id;
|
|
89
|
+
var colon = head.indexOf(':');
|
|
90
|
+
return colon >= 0 ? head.slice(colon + 1) : head;
|
|
91
|
+
}
|
|
92
|
+
|
|
55
93
|
// Per-tile body renderers for the detail modal. Each reads the latest status
|
|
56
94
|
// snapshot and fills the modal body; openTileModal looks them up by key.
|
|
57
95
|
function renderEngineTileBody(body, data) {
|
|
@@ -83,17 +121,61 @@
|
|
|
83
121
|
}
|
|
84
122
|
|
|
85
123
|
function renderPrTileBody(body, data) {
|
|
124
|
+
// Emoji glyphs make build/review status scannable at a glance; the
|
|
125
|
+
// accompanying label keeps the row readable when the glyph alone is
|
|
126
|
+
// ambiguous (✅ vs ✅, ❌ vs ❌, ⏳ vs 💬). The raw status string also
|
|
127
|
+
// stays in the title attribute for hover discoverability.
|
|
128
|
+
// Keyed by normalized (lowercased) status; missing/unknown → ➖.
|
|
129
|
+
var BUILD_STATUS = {
|
|
130
|
+
success: { glyph: '✅', label: 'passing' },
|
|
131
|
+
succeeded: { glyph: '✅', label: 'passing' },
|
|
132
|
+
passing: { glyph: '✅', label: 'passing' },
|
|
133
|
+
failing: { glyph: '❌', label: 'failing' },
|
|
134
|
+
failed: { glyph: '❌', label: 'failing' },
|
|
135
|
+
error: { glyph: '❌', label: 'failing' },
|
|
136
|
+
pending: { glyph: '⏳', label: 'pending' },
|
|
137
|
+
running: { glyph: '⏳', label: 'running' },
|
|
138
|
+
queued: { glyph: '⏳', label: 'queued' },
|
|
139
|
+
in_progress: { glyph: '⏳', label: 'in progress' },
|
|
140
|
+
};
|
|
141
|
+
var REVIEW_STATUS = {
|
|
142
|
+
approved: { glyph: '✅', label: 'approved' },
|
|
143
|
+
'changes-requested': { glyph: '❌', label: 'changes requested' },
|
|
144
|
+
changes_requested: { glyph: '❌', label: 'changes requested' },
|
|
145
|
+
'requested-changes': { glyph: '❌', label: 'changes requested' },
|
|
146
|
+
rejected: { glyph: '❌', label: 'rejected' },
|
|
147
|
+
pending: { glyph: '💬', label: 'pending' },
|
|
148
|
+
commented: { glyph: '💬', label: 'commented' },
|
|
149
|
+
};
|
|
150
|
+
function statusSpan(subsystem, raw, table) {
|
|
151
|
+
var info = table[String(raw || '').toLowerCase()];
|
|
152
|
+
var glyph = info ? info.glyph : '➖';
|
|
153
|
+
var label = info ? info.label : (raw ? String(raw) : 'none');
|
|
154
|
+
var span = document.createElement('span');
|
|
155
|
+
span.className = 'pr-status';
|
|
156
|
+
span.textContent = glyph + ' ' + subsystem + ' ' + label;
|
|
157
|
+
span.title = subsystem + ': ' + (raw || 'unknown');
|
|
158
|
+
return span;
|
|
159
|
+
}
|
|
86
160
|
var prs = Array.isArray(data.pullRequests) ? data.pullRequests : [];
|
|
87
161
|
if (!prs.length) { tileEmpty(body, 'No tracked pull requests.'); return; }
|
|
88
162
|
prs.forEach(function(p) {
|
|
89
163
|
var num = p.prNumber || p.number || p.id;
|
|
90
164
|
var build = p.buildStatus || '';
|
|
165
|
+
var review = p.reviewStatus || '';
|
|
91
166
|
var cls = p.status === 'merged' ? 'green'
|
|
92
167
|
: (build === 'failing' ? 'red'
|
|
93
168
|
: ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
|
|
169
|
+
var statusText = document.createElement('span');
|
|
170
|
+
statusText.textContent = p.status || 'unknown';
|
|
94
171
|
body.appendChild(tileCard({
|
|
95
172
|
title: (num ? '#' + num + ' ' : '') + (p.title || '(untitled PR)'),
|
|
96
|
-
|
|
173
|
+
// Project slug first (under title), then build/review row below — the
|
|
174
|
+
// project is the strongest disambiguator across PRs and benefits from
|
|
175
|
+
// being adjacent to the title.
|
|
176
|
+
subtle: parsePrSlug(p.id),
|
|
177
|
+
subtleBeforeMeta: true,
|
|
178
|
+
metaNodes: [statusText, statusSpan('build', build, BUILD_STATUS), statusSpan('review', review, REVIEW_STATUS)],
|
|
97
179
|
chip: { text: p.status || '—', cls: cls },
|
|
98
180
|
href: p.url || null,
|
|
99
181
|
}));
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Project context picker: populated from /api/status (which already returns
|
|
2
|
-
// the configured project list).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// the configured project list). One-or-more projects: a <select> dropdown
|
|
3
|
+
// whose first option is a "Select project" sentinel (value ""), italicized
|
|
4
|
+
// via CSS, so the indicator reads "Select project" until the user explicitly
|
|
5
|
+
// picks one and can always be cleared back to it. Zero-project: a hint to
|
|
6
|
+
// add one. No auto-default: we never silently fall back to projects[0] /
|
|
7
|
+
// last-used / constellation, so project-scoped CC actions can't land in the
|
|
8
|
+
// wrong project (W-mqayzsj3). Selection persists per-browser in localStorage
|
|
9
|
+
// and is included in every /api/command-center/stream call.
|
|
6
10
|
// Build the "+ Add" button as a real DOM node (was an HTML string + innerHTML
|
|
7
11
|
// injection — SEC-03 ratchet prefers we avoid that pattern, see
|
|
8
12
|
// test/unit.test.js DYNAMIC_INNERHTML_BASELINE).
|
|
@@ -22,19 +26,20 @@
|
|
|
22
26
|
empty.textContent = 'No projects linked yet.';
|
|
23
27
|
return empty;
|
|
24
28
|
}
|
|
25
|
-
// Strip variant:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return staticSpan;
|
|
31
|
-
}
|
|
32
|
-
// Strip variant: multi-project <select>. Returns the select so the caller
|
|
33
|
-
// can attach a change listener.
|
|
29
|
+
// Strip variant: project <select>. The first option is a "Select project"
|
|
30
|
+
// sentinel (value "") that is selected whenever no project is explicitly
|
|
31
|
+
// chosen, so the indicator never auto-defaults and the user can always clear
|
|
32
|
+
// their choice back to it. Returns the select so the caller can attach a
|
|
33
|
+
// change listener.
|
|
34
34
|
function makeContextSelect(projects, selected) {
|
|
35
35
|
var sel = document.createElement('select');
|
|
36
36
|
sel.id = 'chat-context-select';
|
|
37
37
|
sel.setAttribute('aria-label', 'Working in project');
|
|
38
|
+
var placeholder = document.createElement('option');
|
|
39
|
+
placeholder.value = '';
|
|
40
|
+
placeholder.textContent = 'Select project';
|
|
41
|
+
if (!selected) placeholder.selected = true;
|
|
42
|
+
sel.appendChild(placeholder);
|
|
38
43
|
for (var i = 0; i < projects.length; i++) {
|
|
39
44
|
var p = projects[i];
|
|
40
45
|
var opt = document.createElement('option');
|
|
@@ -69,22 +74,25 @@
|
|
|
69
74
|
currentProject = null;
|
|
70
75
|
try { localStorage.removeItem(SLIM_PROJECT_KEY); } catch (_e) {}
|
|
71
76
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
currentProject = ev.target.value || null;
|
|
77
|
+
// No auto-default: when nothing is explicitly selected, currentProject
|
|
78
|
+
// stays null and the picker shows the "Select project" sentinel
|
|
79
|
+
// (italicized via CSS). We deliberately do NOT fall back to projects[0]
|
|
80
|
+
// here (W-mqayzsj3) so CC turns and project-scoped actions don't
|
|
81
|
+
// silently target the wrong project.
|
|
82
|
+
var sel = makeContextSelect(projects, currentProject);
|
|
83
|
+
stripEl.style.display = '';
|
|
84
|
+
controlsEl.replaceChildren(sel, makeContextAddBtn());
|
|
85
|
+
sel.addEventListener('change', function(ev) {
|
|
86
|
+
var val = ev.target.value || '';
|
|
87
|
+
if (val) {
|
|
88
|
+
currentProject = val;
|
|
85
89
|
try { localStorage.setItem(SLIM_PROJECT_KEY, currentProject); } catch (_e) {}
|
|
86
|
-
}
|
|
87
|
-
|
|
90
|
+
} else {
|
|
91
|
+
// User cleared back to "Select project" — drop the persisted choice.
|
|
92
|
+
currentProject = null;
|
|
93
|
+
try { localStorage.removeItem(SLIM_PROJECT_KEY); } catch (_e) {}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
88
96
|
}
|
|
89
97
|
var addBtn = document.getElementById('chat-context-add');
|
|
90
98
|
if (addBtn) addBtn.addEventListener('click', openAddProjectModal);
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
}, delay || 0);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Tiles whose numeric value should fire a one-shot cockpit-flash animation
|
|
20
|
+
// whenever it changes (W-mqb02bga: status panel). Suppressed on first paint
|
|
21
|
+
// — only deltas from a previously-seen number trigger the flash, so opening
|
|
22
|
+
// the dashboard doesn't pulse every tile.
|
|
23
|
+
var FLASH_ON_CHANGE_KEYS = { dispatches: true, prs: true };
|
|
24
|
+
var lastTileValues = Object.create(null);
|
|
25
|
+
|
|
19
26
|
// Apply lit-state and value/detail text to a tile.
|
|
20
27
|
function updateTile(key, value, detail, lit) {
|
|
21
28
|
var tile = document.querySelector('.cockpit-tile[data-tile="' + key + '"]');
|
|
@@ -27,16 +34,57 @@
|
|
|
27
34
|
valEl.classList.toggle('dim', !lit);
|
|
28
35
|
}
|
|
29
36
|
if (detEl) detEl.textContent = detail || '';
|
|
30
|
-
tile.classList.remove('lit-blue', 'lit-green', 'lit-amber', 'lit-red');
|
|
37
|
+
tile.classList.remove('lit-blue', 'lit-green', 'lit-amber', 'lit-orange', 'lit-red');
|
|
31
38
|
if (lit) tile.classList.add('lit-' + lit);
|
|
39
|
+
// Flash the tile when its number changes. Remove → force reflow → re-add
|
|
40
|
+
// restarts the animation even when the class was already present from the
|
|
41
|
+
// previous tick (otherwise repeated changes would only fire on the first
|
|
42
|
+
// delta after class application). Only numeric values count as a change;
|
|
43
|
+
// ignore null/undefined transitions.
|
|
44
|
+
if (FLASH_ON_CHANGE_KEYS[key] && typeof value === 'number') {
|
|
45
|
+
var prev = lastTileValues[key];
|
|
46
|
+
if (typeof prev === 'number' && prev !== value) {
|
|
47
|
+
tile.classList.remove('flash-change');
|
|
48
|
+
void tile.offsetWidth;
|
|
49
|
+
tile.classList.add('flash-change');
|
|
50
|
+
}
|
|
51
|
+
lastTileValues[key] = value;
|
|
52
|
+
}
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
// Latest /api/status snapshot — read by the cockpit-tile detail modals.
|
|
35
56
|
var lastStatusData = null;
|
|
36
57
|
|
|
58
|
+
// Reflect the Command Center's resolved runtime + model in the Actions panel
|
|
59
|
+
// header (slim-ux only — classic chrome is untouched). Source is the same
|
|
60
|
+
// /api/status payload the cockpit polls: autoMode.ccModel (config.engine
|
|
61
|
+
// ccModel → defaultModel → runtime default) and autoMode.ccCli. When no model
|
|
62
|
+
// is resolvable (runtime picks its own default), the badge stays hidden rather
|
|
63
|
+
// than guessing. textContent keeps it XSS-safe — the values come from config.
|
|
64
|
+
function updateCcModelBadge(model, cli) {
|
|
65
|
+
var badge = document.getElementById('cc-model-badge');
|
|
66
|
+
if (!badge) return;
|
|
67
|
+
var m = (model === null || model === undefined) ? '' : String(model).trim();
|
|
68
|
+
if (!m) {
|
|
69
|
+
badge.hidden = true;
|
|
70
|
+
badge.textContent = '';
|
|
71
|
+
badge.removeAttribute('title');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
var c = (cli === null || cli === undefined) ? '' : String(cli).trim();
|
|
75
|
+
var label = c ? (c + ' · ' + m) : m;
|
|
76
|
+
badge.textContent = label;
|
|
77
|
+
badge.title = 'Command Center model: ' + label;
|
|
78
|
+
badge.hidden = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
37
81
|
function applyStatus(data) {
|
|
38
82
|
if (!data) return;
|
|
39
83
|
lastStatusData = data;
|
|
84
|
+
// ── Command Center model badge (Actions panel header) ──────
|
|
85
|
+
var autoMode = data.autoMode || {};
|
|
86
|
+
updateCcModelBadge(autoMode.ccModel, autoMode.ccCli);
|
|
87
|
+
|
|
40
88
|
// ── Engine tile ────────────────────────────────────────────
|
|
41
89
|
var engine = data.engine || {};
|
|
42
90
|
// /api/status exposes `state` ('running'|'paused'|'stopping'|'stopped')
|
|
@@ -65,14 +113,14 @@
|
|
|
65
113
|
updateTile(
|
|
66
114
|
'dispatches',
|
|
67
115
|
active.length,
|
|
68
|
-
active.length ? '
|
|
69
|
-
active.length ? '
|
|
116
|
+
active.length ? 'agents working' : 'no agents working',
|
|
117
|
+
active.length ? 'amber' : null
|
|
70
118
|
);
|
|
71
119
|
updateTile(
|
|
72
120
|
'queued',
|
|
73
121
|
pending.length,
|
|
74
122
|
pending.length ? 'waiting to spawn' : 'queue empty',
|
|
75
|
-
pending.length ? '
|
|
123
|
+
pending.length ? 'orange' : null
|
|
76
124
|
);
|
|
77
125
|
|
|
78
126
|
// ── PR tile (status snapshot from cached pull-requests) ─────
|
|
@@ -20,9 +20,15 @@
|
|
|
20
20
|
--blue: #58a6ff;
|
|
21
21
|
--green: #3fb950;
|
|
22
22
|
--amber: #d29922;
|
|
23
|
+
--orange: #ea580c;
|
|
23
24
|
--red: #f85149;
|
|
24
25
|
--radius: 6px;
|
|
25
26
|
|
|
27
|
+
/* Spacing scale (W-mqazlkym) — 4/8/12/16/20 rhythm for the chat
|
|
28
|
+
surface. Slim ships its CSS independently, so the scale is declared
|
|
29
|
+
here rather than reusing dashboard/styles.css's --space-* tokens. */
|
|
30
|
+
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px;
|
|
31
|
+
|
|
26
32
|
/* Typography — mirrors dashboard/styles.css :root tokens
|
|
27
33
|
(W-mq1c8og40003a7cf). Slim ships its CSS independently
|
|
28
34
|
(template-baked into slim/layout.html), so we duplicate the
|
|
@@ -221,6 +227,34 @@
|
|
|
221
227
|
color: var(--muted);
|
|
222
228
|
font-size: var(--text-base);
|
|
223
229
|
}
|
|
230
|
+
/* Right-aligned cluster in the Actions panel header: keeps the CC model
|
|
231
|
+
badge tucked next to the "Command Center" sub-label without disturbing
|
|
232
|
+
the header's space-between layout. */
|
|
233
|
+
.panel-header .panel-head-meta {
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
gap: 8px;
|
|
237
|
+
min-width: 0;
|
|
238
|
+
}
|
|
239
|
+
/* Compact pill surfacing the Command Center's resolved runtime/model
|
|
240
|
+
(e.g. "copilot · gpt-5.4"). Muted, monospace, low-key — visible but
|
|
241
|
+
unobtrusive. Populated by status.js#updateCcModelBadge. */
|
|
242
|
+
.cc-model-badge {
|
|
243
|
+
text-transform: none;
|
|
244
|
+
letter-spacing: 0;
|
|
245
|
+
font-weight: 500;
|
|
246
|
+
font-size: var(--text-sm);
|
|
247
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
248
|
+
color: var(--muted);
|
|
249
|
+
background: var(--surface2);
|
|
250
|
+
border: 1px solid var(--border);
|
|
251
|
+
border-radius: 999px;
|
|
252
|
+
padding: 1px 8px;
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
overflow: hidden;
|
|
255
|
+
text-overflow: ellipsis;
|
|
256
|
+
max-width: 220px;
|
|
257
|
+
}
|
|
224
258
|
.panel-body {
|
|
225
259
|
flex: 1;
|
|
226
260
|
overflow-y: auto;
|
|
@@ -252,9 +286,9 @@
|
|
|
252
286
|
.chat-tabs {
|
|
253
287
|
display: flex;
|
|
254
288
|
align-items: center;
|
|
255
|
-
gap:
|
|
289
|
+
gap: var(--space-2);
|
|
256
290
|
flex-wrap: wrap;
|
|
257
|
-
padding:
|
|
291
|
+
padding: var(--space-2) var(--space-3) 0;
|
|
258
292
|
width: 100%;
|
|
259
293
|
max-width: 800px;
|
|
260
294
|
margin-left: auto;
|
|
@@ -310,10 +344,13 @@
|
|
|
310
344
|
.chat-messages {
|
|
311
345
|
flex: 1;
|
|
312
346
|
overflow-y: auto;
|
|
313
|
-
padding
|
|
347
|
+
/* Extra top padding so the first message clears the tab/context chip
|
|
348
|
+
row above it (#10); 12px sides keep the left grid line shared with the
|
|
349
|
+
input row and action toolbar (#3). */
|
|
350
|
+
padding: var(--space-4) var(--space-3) var(--space-3);
|
|
314
351
|
display: flex;
|
|
315
352
|
flex-direction: column;
|
|
316
|
-
gap:
|
|
353
|
+
gap: var(--space-3);
|
|
317
354
|
min-height: 0;
|
|
318
355
|
width: 100%;
|
|
319
356
|
max-width: 800px;
|
|
@@ -329,7 +366,9 @@
|
|
|
329
366
|
}
|
|
330
367
|
.chat-msg {
|
|
331
368
|
max-width: 90%;
|
|
332
|
-
padding
|
|
369
|
+
/* Uniform bubble padding across variants (#2); 20px horizontal gives
|
|
370
|
+
wrapped technical text >=16px right-edge clearance (#8). */
|
|
371
|
+
padding: var(--space-4) var(--space-5);
|
|
333
372
|
border-radius: var(--radius);
|
|
334
373
|
white-space: pre-wrap;
|
|
335
374
|
word-wrap: break-word;
|
|
@@ -360,10 +399,65 @@
|
|
|
360
399
|
border: 1px solid var(--red);
|
|
361
400
|
font-size: var(--text-lg);
|
|
362
401
|
}
|
|
363
|
-
|
|
402
|
+
/* Queued messages (W-mqayw3x6): follow-ups submitted while a turn is still
|
|
403
|
+
streaming. The container is pinned to the bottom of the transcript; each
|
|
404
|
+
item is a greyed-out user bubble that fires FIFO on turn completion. */
|
|
405
|
+
.chat-queue {
|
|
364
406
|
display: flex;
|
|
407
|
+
flex-direction: column;
|
|
365
408
|
gap: 8px;
|
|
366
|
-
|
|
409
|
+
align-self: stretch;
|
|
410
|
+
}
|
|
411
|
+
.chat-msg.user.queued {
|
|
412
|
+
align-self: flex-end;
|
|
413
|
+
background: var(--surface2);
|
|
414
|
+
color: var(--muted);
|
|
415
|
+
border: 1px dashed var(--border);
|
|
416
|
+
opacity: 0.8;
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 8px;
|
|
420
|
+
}
|
|
421
|
+
.chat-queue-text { white-space: pre-wrap; word-wrap: break-word; }
|
|
422
|
+
.chat-queue-badge {
|
|
423
|
+
font-size: var(--text-base);
|
|
424
|
+
text-transform: uppercase;
|
|
425
|
+
letter-spacing: 0.5px;
|
|
426
|
+
color: var(--muted);
|
|
427
|
+
border: 1px solid var(--border);
|
|
428
|
+
border-radius: var(--radius);
|
|
429
|
+
padding: 1px 6px;
|
|
430
|
+
flex-shrink: 0;
|
|
431
|
+
}
|
|
432
|
+
.chat-queue-dismiss {
|
|
433
|
+
background: none;
|
|
434
|
+
border: none;
|
|
435
|
+
color: var(--muted);
|
|
436
|
+
cursor: pointer;
|
|
437
|
+
font-size: var(--text-lg);
|
|
438
|
+
line-height: 1;
|
|
439
|
+
padding: 0 2px;
|
|
440
|
+
flex-shrink: 0;
|
|
441
|
+
}
|
|
442
|
+
.chat-queue-dismiss:hover { color: var(--red); }
|
|
443
|
+
.chat-queue-resume {
|
|
444
|
+
align-self: center;
|
|
445
|
+
background: var(--blue);
|
|
446
|
+
color: #fff;
|
|
447
|
+
border: none;
|
|
448
|
+
border-radius: var(--radius);
|
|
449
|
+
padding: 6px 14px;
|
|
450
|
+
font-size: var(--text-md);
|
|
451
|
+
font-weight: 600;
|
|
452
|
+
cursor: pointer;
|
|
453
|
+
}
|
|
454
|
+
.chat-queue-resume:hover { filter: brightness(1.1); }
|
|
455
|
+
.chat-input-wrap {
|
|
456
|
+
display: flex;
|
|
457
|
+
gap: var(--space-3);
|
|
458
|
+
/* Breathing room around the textarea; the larger gap separates it from
|
|
459
|
+
the Send/Stop buttons (#5). */
|
|
460
|
+
padding: var(--space-3);
|
|
367
461
|
border-top: 1px solid var(--border);
|
|
368
462
|
flex-shrink: 0;
|
|
369
463
|
background: var(--surface);
|
|
@@ -380,8 +474,8 @@
|
|
|
380
474
|
.chat-context-strip {
|
|
381
475
|
display: flex;
|
|
382
476
|
align-items: center;
|
|
383
|
-
gap:
|
|
384
|
-
padding:
|
|
477
|
+
gap: var(--space-3);
|
|
478
|
+
padding: var(--space-2) var(--space-3);
|
|
385
479
|
border-top: 1px solid var(--border);
|
|
386
480
|
flex-shrink: 0;
|
|
387
481
|
background: var(--surface);
|
|
@@ -409,6 +503,16 @@
|
|
|
409
503
|
cursor: pointer;
|
|
410
504
|
}
|
|
411
505
|
.chat-context-strip select:focus { outline: none; border-color: var(--blue); }
|
|
506
|
+
/* Italicize the "Select project" placeholder option AND the closed select
|
|
507
|
+
widget while the placeholder is the active value. `:has()` is needed
|
|
508
|
+
because native <select> rendering ignores child <option> styles for the
|
|
509
|
+
displayed (closed) text — we have to style the <select> itself. The
|
|
510
|
+
explicit `option { font-style: normal }` reset stops real project names
|
|
511
|
+
from inheriting the parent select's italic when the dropdown is opened
|
|
512
|
+
while the placeholder is still selected. */
|
|
513
|
+
.chat-context-strip select option { font-style: normal; color: var(--text); }
|
|
514
|
+
.chat-context-strip select option[value=""] { font-style: italic; color: var(--muted); }
|
|
515
|
+
.chat-context-strip select:has(option[value=""]:checked) { font-style: italic; color: var(--muted); }
|
|
412
516
|
.chat-context-strip .context-static {
|
|
413
517
|
color: var(--text);
|
|
414
518
|
font-weight: 500;
|
|
@@ -461,17 +565,19 @@
|
|
|
461
565
|
.chat-send:hover { filter: brightness(1.1); }
|
|
462
566
|
.chat-send:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
463
567
|
.chat-stop {
|
|
464
|
-
padding:
|
|
568
|
+
padding: 9px;
|
|
465
569
|
background: var(--surface2);
|
|
466
570
|
color: var(--red);
|
|
467
571
|
border: 1px solid var(--border);
|
|
468
572
|
border-radius: var(--radius);
|
|
469
573
|
font-size: var(--text-md);
|
|
574
|
+
line-height: 0;
|
|
470
575
|
cursor: pointer;
|
|
471
576
|
align-self: flex-end;
|
|
472
577
|
display: none;
|
|
473
578
|
}
|
|
474
579
|
.chat-stop:hover { filter: brightness(1.1); }
|
|
580
|
+
.chat-stop .chat-stop-icon { display: block; width: 20px; height: 20px; }
|
|
475
581
|
|
|
476
582
|
/* Tool-call progress panel + thinking indicator. While streaming, the
|
|
477
583
|
thinking indicator sits at the bottom-left of the response card and the
|
|
@@ -481,7 +587,7 @@
|
|
|
481
587
|
display: flex;
|
|
482
588
|
align-items: flex-start;
|
|
483
589
|
gap: 12px;
|
|
484
|
-
margin-top:
|
|
590
|
+
margin-top: var(--space-3);
|
|
485
591
|
}
|
|
486
592
|
/* Collapsed tool panel: at most 3 non-wrapping lines, scrolls (newest at
|
|
487
593
|
the bottom) once there are more. Clicking opens the full list in a modal. */
|
|
@@ -561,6 +667,8 @@
|
|
|
561
667
|
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
562
668
|
}
|
|
563
669
|
.tile-item-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; word-break: break-word; }
|
|
670
|
+
.tile-item-subtle { font-size: var(--text-sm); color: var(--muted); margin-top: 2px; word-break: break-word; }
|
|
671
|
+
.pr-status { display: inline-block; margin: 0 1px; text-decoration: none; }
|
|
564
672
|
.tile-chip {
|
|
565
673
|
flex: 0 0 auto;
|
|
566
674
|
font-size: var(--text-base); font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
|
|
@@ -623,12 +731,26 @@
|
|
|
623
731
|
|
|
624
732
|
.chat-action {
|
|
625
733
|
align-self: flex-start;
|
|
626
|
-
padding:
|
|
734
|
+
padding: var(--space-1) var(--space-3);
|
|
627
735
|
border-radius: 4px;
|
|
628
736
|
font-size: var(--text-base);
|
|
629
737
|
border: 1px dashed var(--border);
|
|
630
738
|
color: var(--muted);
|
|
631
739
|
}
|
|
740
|
+
/* A status line that immediately follows a message reads as that card's
|
|
741
|
+
output (#9): tighten the gap (negative margin trims the 12px column gap
|
|
742
|
+
to ~4px), drop the free-floating dashed box, and tie it to the card
|
|
743
|
+
above with a shared left edge + left-border continuation. */
|
|
744
|
+
.chat-msg + .chat-action {
|
|
745
|
+
margin-top: calc(-1 * var(--space-2));
|
|
746
|
+
border: none;
|
|
747
|
+
border-left: 2px solid var(--border);
|
|
748
|
+
border-radius: 0 var(--radius) var(--radius) 0;
|
|
749
|
+
padding-left: var(--space-3);
|
|
750
|
+
}
|
|
751
|
+
.chat-msg + .chat-action.ok { border-left-color: var(--green); }
|
|
752
|
+
.chat-msg + .chat-action.warn { border-left-color: var(--amber); }
|
|
753
|
+
.chat-msg + .chat-action.err { border-left-color: var(--red); }
|
|
632
754
|
.chat-action.ok { color: var(--green); }
|
|
633
755
|
.chat-action.warn { color: var(--amber); }
|
|
634
756
|
.chat-action.err { color: var(--red); }
|
|
@@ -640,9 +762,10 @@
|
|
|
640
762
|
}
|
|
641
763
|
.chat-msg.assistant code {
|
|
642
764
|
background: var(--bg);
|
|
643
|
-
padding:
|
|
765
|
+
padding: 2px 6px;
|
|
644
766
|
border-radius: 3px;
|
|
645
767
|
font-size: var(--text-code);
|
|
768
|
+
vertical-align: baseline;
|
|
646
769
|
}
|
|
647
770
|
.chat-msg.assistant pre {
|
|
648
771
|
background: var(--bg);
|
|
@@ -656,13 +779,16 @@
|
|
|
656
779
|
.chat-msg.assistant a { color: var(--blue); }
|
|
657
780
|
.chat-msg.assistant strong { color: var(--text); }
|
|
658
781
|
|
|
659
|
-
/* ── Action footer:
|
|
782
|
+
/* ── Action footer: Link PR + Pin Content ──
|
|
783
|
+
De-emphasized (#6, option b): ghost/secondary styling so they no longer
|
|
784
|
+
compete with the primary blue Send button. The 4-column grid keeps each
|
|
785
|
+
button at a quarter of the panel width (low visual weight). */
|
|
660
786
|
.actions-buttons {
|
|
661
787
|
flex-shrink: 0;
|
|
662
788
|
display: grid;
|
|
663
789
|
grid-template-columns: repeat(4, 1fr);
|
|
664
|
-
gap:
|
|
665
|
-
padding:
|
|
790
|
+
gap: var(--space-2);
|
|
791
|
+
padding: var(--space-3);
|
|
666
792
|
border-top: 1px solid var(--border);
|
|
667
793
|
width: 100%;
|
|
668
794
|
max-width: 800px;
|
|
@@ -670,11 +796,11 @@
|
|
|
670
796
|
margin-right: auto;
|
|
671
797
|
}
|
|
672
798
|
.link-pr-btn {
|
|
673
|
-
background:
|
|
674
|
-
color: var(--
|
|
799
|
+
background: transparent;
|
|
800
|
+
color: var(--muted);
|
|
675
801
|
border: 1px solid var(--border);
|
|
676
802
|
border-radius: var(--radius);
|
|
677
|
-
padding:
|
|
803
|
+
padding: var(--space-2) var(--space-3);
|
|
678
804
|
cursor: pointer;
|
|
679
805
|
font-size: var(--text-md);
|
|
680
806
|
font-family: inherit;
|
|
@@ -740,9 +866,16 @@
|
|
|
740
866
|
table inside the IIFE. */
|
|
741
867
|
.cockpit-grid {
|
|
742
868
|
display: grid;
|
|
743
|
-
grid-template-columns: repeat(
|
|
869
|
+
grid-template-columns: repeat(6, 1fr);
|
|
744
870
|
gap: 8px;
|
|
745
871
|
}
|
|
872
|
+
/* Row 2: Queued | Dispatches | PRs — three equal columns (2 of 6 each). */
|
|
873
|
+
.cockpit-grid > [data-tile="queued"],
|
|
874
|
+
.cockpit-grid > [data-tile="dispatches"],
|
|
875
|
+
.cockpit-grid > [data-tile="prs"] { grid-column: span 2; }
|
|
876
|
+
/* Row 3: Watches | Pinned — two equal columns (3 of 6 each). */
|
|
877
|
+
.cockpit-grid > [data-tile="watches"],
|
|
878
|
+
.cockpit-grid > [data-tile="pinned"] { grid-column: span 3; }
|
|
746
879
|
.cockpit-tile {
|
|
747
880
|
background: var(--surface2);
|
|
748
881
|
border: 1px solid var(--border);
|
|
@@ -752,22 +885,37 @@
|
|
|
752
885
|
flex-direction: column;
|
|
753
886
|
gap: 4px;
|
|
754
887
|
transition: border-color 0.2s, background 0.2s;
|
|
888
|
+
/* Per-state flash color (rgb triplet, no rgba wrapper) consumed by
|
|
889
|
+
`cockpit-flash` so the change-flash ring matches the tile's lit state
|
|
890
|
+
(amber for working dispatches, orange for queued, red for failing
|
|
891
|
+
PRs, etc.). Each lit-* class below overrides this. Default is blue
|
|
892
|
+
for unlit / generic tiles. */
|
|
893
|
+
--flash-color: 88, 166, 255;
|
|
755
894
|
}
|
|
756
895
|
.cockpit-tile.lit-blue {
|
|
757
896
|
border-color: rgba(88, 166, 255, 0.6);
|
|
758
897
|
background: rgba(88, 166, 255, 0.06);
|
|
898
|
+
--flash-color: 88, 166, 255;
|
|
759
899
|
}
|
|
760
900
|
.cockpit-tile.lit-green {
|
|
761
901
|
border-color: rgba(63, 185, 80, 0.6);
|
|
762
902
|
background: rgba(63, 185, 80, 0.06);
|
|
903
|
+
--flash-color: 63, 185, 80;
|
|
763
904
|
}
|
|
764
905
|
.cockpit-tile.lit-amber {
|
|
765
906
|
border-color: rgba(210, 153, 34, 0.6);
|
|
766
907
|
background: rgba(210, 153, 34, 0.06);
|
|
908
|
+
--flash-color: 210, 153, 34;
|
|
909
|
+
}
|
|
910
|
+
.cockpit-tile.lit-orange {
|
|
911
|
+
border-color: rgba(234, 88, 12, 0.6);
|
|
912
|
+
background: rgba(234, 88, 12, 0.08);
|
|
913
|
+
--flash-color: 234, 88, 12;
|
|
767
914
|
}
|
|
768
915
|
.cockpit-tile.lit-red {
|
|
769
916
|
border-color: rgba(248, 81, 73, 0.6);
|
|
770
917
|
background: rgba(248, 81, 73, 0.08);
|
|
918
|
+
--flash-color: 248, 81, 73;
|
|
771
919
|
}
|
|
772
920
|
.cockpit-label {
|
|
773
921
|
font-size: var(--text-base);
|
|
@@ -789,6 +937,7 @@
|
|
|
789
937
|
.lit-blue .cockpit-dot { background: var(--blue); box-shadow: 0 0 6px var(--blue); }
|
|
790
938
|
.lit-green .cockpit-dot { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
791
939
|
.lit-amber .cockpit-dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
|
|
940
|
+
.lit-orange .cockpit-dot { background: var(--orange); box-shadow: 0 0 6px var(--orange); }
|
|
792
941
|
.lit-red .cockpit-dot { background: var(--red); box-shadow: 0 0 6px var(--red); }
|
|
793
942
|
.cockpit-value {
|
|
794
943
|
font-size: var(--text-stat);
|
|
@@ -803,10 +952,59 @@
|
|
|
803
952
|
line-height: 1.3;
|
|
804
953
|
min-height: 14px;
|
|
805
954
|
}
|
|
955
|
+
/* Engine tile: full-width, compact single-line horizontal strip. */
|
|
956
|
+
.cockpit-tile--full {
|
|
957
|
+
grid-column: 1 / -1;
|
|
958
|
+
flex-direction: row;
|
|
959
|
+
align-items: center;
|
|
960
|
+
gap: 16px;
|
|
961
|
+
padding: 7px 12px;
|
|
962
|
+
}
|
|
963
|
+
.cockpit-tile--full .cockpit-value { font-size: var(--text-xl); }
|
|
964
|
+
.cockpit-tile--full .cockpit-detail {
|
|
965
|
+
min-height: 0;
|
|
966
|
+
margin-left: auto;
|
|
967
|
+
text-align: right;
|
|
968
|
+
}
|
|
806
969
|
/* Cockpit tiles are clickable -> detail modal. */
|
|
807
970
|
.cockpit-tile { cursor: pointer; position: relative; }
|
|
808
971
|
.cockpit-tile:hover { border-color: var(--blue); }
|
|
809
972
|
.cockpit-tile:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
|
|
973
|
+
/* Active Dispatches tile pulses while there is work in flight — same
|
|
974
|
+
cadence as the per-member "working" badge in the Team section, so the
|
|
975
|
+
eye instantly ties "things are happening" in both regions. The pulse
|
|
976
|
+
only runs in the lit-amber state (dispatches > 0); empty state is
|
|
977
|
+
static. Respects prefers-reduced-motion. */
|
|
978
|
+
.cockpit-tile[data-tile="dispatches"].lit-amber {
|
|
979
|
+
animation: slim-pulse 1.6s ease-in-out infinite;
|
|
980
|
+
}
|
|
981
|
+
/* Change-flash: brief one-shot ring around a tile whose value just
|
|
982
|
+
changed, used by status.js for the `dispatches` and `prs` tiles. Layered
|
|
983
|
+
on a pseudo-element so it composes cleanly with `slim-pulse` above
|
|
984
|
+
(running both on the tile itself would force a single `animation:` rule
|
|
985
|
+
and clobber one of them). The ring expands then fades — fully visible
|
|
986
|
+
even when the underlying tile is already lit. Respects
|
|
987
|
+
prefers-reduced-motion. */
|
|
988
|
+
.cockpit-tile::after {
|
|
989
|
+
content: '';
|
|
990
|
+
position: absolute;
|
|
991
|
+
inset: -1px;
|
|
992
|
+
border-radius: inherit;
|
|
993
|
+
pointer-events: none;
|
|
994
|
+
box-shadow: 0 0 0 0 rgba(var(--flash-color), 0);
|
|
995
|
+
}
|
|
996
|
+
.cockpit-tile.flash-change::after {
|
|
997
|
+
animation: cockpit-flash 0.75s ease-out;
|
|
998
|
+
}
|
|
999
|
+
@keyframes cockpit-flash {
|
|
1000
|
+
0% { box-shadow: 0 0 0 0 rgba(var(--flash-color), 0); }
|
|
1001
|
+
30% { box-shadow: 0 0 0 6px rgba(var(--flash-color), 0.7); }
|
|
1002
|
+
100% { box-shadow: 0 0 0 0 rgba(var(--flash-color), 0); }
|
|
1003
|
+
}
|
|
1004
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1005
|
+
.cockpit-tile[data-tile="dispatches"].lit-amber { animation: none; }
|
|
1006
|
+
.cockpit-tile.flash-change::after { animation: none; }
|
|
1007
|
+
}
|
|
810
1008
|
|
|
811
1009
|
/* ── Status panel sub-divisions: Team cards over System tiles,
|
|
812
1010
|
separated by a divider line (no text headers). ──── */
|
package/dashboard.js
CHANGED
|
@@ -8865,13 +8865,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8865
8865
|
// `currentProject` hint so CC can default action.project without the
|
|
8866
8866
|
// user naming it every turn. Validate against the configured project
|
|
8867
8867
|
// list before injecting — body.currentProject is untrusted input.
|
|
8868
|
+
// When slim has no project explicitly selected (the "Select project"
|
|
8869
|
+
// indicator), it instead sends `noProjectSelected: true` so we inject an
|
|
8870
|
+
// explicit "do not default — ask the user" preamble rather than silently
|
|
8871
|
+
// letting CC pick projects[0] (W-mqayzsj3). The classic dashboard sends
|
|
8872
|
+
// neither field, so its prompt stays byte-identical (empty part).
|
|
8868
8873
|
const _knownProjectNames = (PROJECTS || []).map(p => p && p.name).filter(Boolean);
|
|
8869
8874
|
const _currentProject = (body.currentProject && _knownProjectNames.includes(body.currentProject))
|
|
8870
8875
|
? body.currentProject
|
|
8871
8876
|
: null;
|
|
8872
8877
|
const projectContextPart = _currentProject
|
|
8873
8878
|
? `[Project Context] The user is currently working in project "${_currentProject}". Default any action's "project" field to this project unless the user explicitly names a different one.`
|
|
8874
|
-
:
|
|
8879
|
+
: (body.noProjectSelected
|
|
8880
|
+
? `[Project Context] No project is selected. Do NOT default any action's "project" field — ask the user to pick a project before creating or dispatching project-scoped work.`
|
|
8881
|
+
: '');
|
|
8875
8882
|
const prompt = _joinCcPromptParts(preamble, resumeGuard, carryover, turnHeader, projectContextPart, body.message);
|
|
8876
8883
|
|
|
8877
8884
|
const { trackEngineUsage: trackUsage, trackEngineError: trackErr, withCcTurnTimeout: withTimeout } = require('./engine/llm');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2177",
|
|
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"
|