@yemi33/minions 0.1.2175 → 0.1.2176

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.
@@ -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
- msgsEl.appendChild(div);
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
- msgsEl.appendChild(div);
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
- msgsEl.appendChild(div);
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
- sendBtn.disabled = on;
415
- sendBtn.textContent = on ? 'Sending…' : 'Send';
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 = on;
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
- return;
428
- }
429
- for (var i = 0; i < messages.length; i++) {
430
- var m = messages[i];
431
- if (m.role === 'action') appendActionStatus(m.severity || '', m.text || '');
432
- else appendBubble(m.role, m.text || '', m.toolCalls);
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
- async function sendMessage() {
27
- if (sending) return;
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();
@@ -101,6 +149,7 @@
101
149
  stream.replaceWithError(errorEvt.error || 'Error');
102
150
  messages.push({ role: 'error', text: errorEvt.error || 'Error' });
103
151
  saveState();
152
+ turnOutcome = 'error';
104
153
  return;
105
154
  }
106
155
 
@@ -130,6 +179,7 @@
130
179
  } catch (e) {
131
180
  if (e && (e.name === 'AbortError' || /aborted/i.test(String(e.message || '')))) {
132
181
  userAborted = true;
182
+ turnOutcome = 'aborted';
133
183
  if (streamedText) {
134
184
  stream.finalize(streamedText);
135
185
  messages.push({ role: 'assistant', text: streamedText, toolCalls: stream.getToolList() });
@@ -139,6 +189,7 @@
139
189
  }
140
190
  saveState();
141
191
  } else {
192
+ turnOutcome = 'error';
142
193
  var msg = 'Send failed: ' + (e && e.message ? e.message : e);
143
194
  stream.replaceWithError(msg);
144
195
  messages.push({ role: 'error', text: msg });
@@ -147,7 +198,8 @@
147
198
  } finally {
148
199
  setSending(false);
149
200
  abortController = null;
150
- if (!userAborted) inputEl.focus();
201
+ if (turnTabId === tabId && !userAborted) inputEl.focus();
202
+ _afterTurn(turnTabId, turnOutcome);
151
203
  }
152
204
  }
153
205
 
@@ -165,6 +217,10 @@
165
217
 
166
218
  async function slimChatNew() {
167
219
  if (sending) abortInFlight();
220
+ // New chat = fresh session: drop the leaving tab's pending queue (#7). The
221
+ // aborted in-flight turn's _afterTurn then finds an empty queue and no-ops.
222
+ delete queues[tabId];
223
+ queueSuspended[tabId] = false;
168
224
  // Register a fresh tab in the shared cc-tabs store and switch slim onto
169
225
  // it — classic's other tabs are left intact, and the previous session
170
226
  // keeps its own server-side cc-sessions.json entry (no DELETE).
@@ -360,6 +360,59 @@
360
360
  border: 1px solid var(--red);
361
361
  font-size: var(--text-lg);
362
362
  }
363
+ /* Queued messages (W-mqayw3x6): follow-ups submitted while a turn is still
364
+ streaming. The container is pinned to the bottom of the transcript; each
365
+ item is a greyed-out user bubble that fires FIFO on turn completion. */
366
+ .chat-queue {
367
+ display: flex;
368
+ flex-direction: column;
369
+ gap: 8px;
370
+ align-self: stretch;
371
+ }
372
+ .chat-msg.user.queued {
373
+ align-self: flex-end;
374
+ background: var(--surface2);
375
+ color: var(--muted);
376
+ border: 1px dashed var(--border);
377
+ opacity: 0.8;
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 8px;
381
+ }
382
+ .chat-queue-text { white-space: pre-wrap; word-wrap: break-word; }
383
+ .chat-queue-badge {
384
+ font-size: var(--text-base);
385
+ text-transform: uppercase;
386
+ letter-spacing: 0.5px;
387
+ color: var(--muted);
388
+ border: 1px solid var(--border);
389
+ border-radius: var(--radius);
390
+ padding: 1px 6px;
391
+ flex-shrink: 0;
392
+ }
393
+ .chat-queue-dismiss {
394
+ background: none;
395
+ border: none;
396
+ color: var(--muted);
397
+ cursor: pointer;
398
+ font-size: var(--text-lg);
399
+ line-height: 1;
400
+ padding: 0 2px;
401
+ flex-shrink: 0;
402
+ }
403
+ .chat-queue-dismiss:hover { color: var(--red); }
404
+ .chat-queue-resume {
405
+ align-self: center;
406
+ background: var(--blue);
407
+ color: #fff;
408
+ border: none;
409
+ border-radius: var(--radius);
410
+ padding: 6px 14px;
411
+ font-size: var(--text-md);
412
+ font-weight: 600;
413
+ cursor: pointer;
414
+ }
415
+ .chat-queue-resume:hover { filter: brightness(1.1); }
363
416
  .chat-input-wrap {
364
417
  display: flex;
365
418
  gap: 8px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2175",
3
+ "version": "0.1.2176",
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"