@yemi33/minions 0.1.748 → 0.1.750

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/CHANGELOG.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.748 (2026-04-09)
3
+ ## 0.1.750 (2026-04-09)
4
+
5
+ ### Fixes
6
+ - replace stale _ccQueue/_ccAbortController references with per-tab state
7
+
8
+ ## 0.1.749 (2026-04-09)
4
9
 
5
10
  ### Features
11
+ - truly parallel CC tabs — per-tab sessions, sending state, abort
6
12
  - CC multi-tab conversations with session resume
7
13
 
8
14
  ### Fixes
@@ -8,9 +8,9 @@ var CC_TITLE_MAX_LENGTH = 40;
8
8
  var _ccTabs = []; // [{id, title, sessionId, messages: [{role, html}]}]
9
9
  var _ccActiveTabId = null;
10
10
  var _ccOpen = false;
11
- var _ccSending = false;
12
- var _ccQueue = [];
13
- var _ccAbortController = null;
11
+ // Per-tab sending state stored on tab objects: tab._sending, tab._queue, tab._abortController
12
+ // Legacy globals for backward compat (badge, drawer close check)
13
+ var _ccSending = false; // true if active tab is sending (UI indicator only)
14
14
  // Clear stale sending state on page load — SSE streams don't survive refresh
15
15
  try { localStorage.removeItem('cc-sending'); } catch {}
16
16
 
@@ -72,9 +72,10 @@ function _ccFindPinTarget(query) {
72
72
  }
73
73
 
74
74
  function ccAbort() {
75
- if (_ccAbortController) {
76
- _ccAbortController.abort();
77
- _ccAbortController = null;
75
+ var tab = _ccActiveTab();
76
+ if (tab && tab._abortController) {
77
+ tab._abortController.abort();
78
+ tab._abortController = null;
78
79
  }
79
80
  }
80
81
 
@@ -114,11 +115,6 @@ function ccNewTab(skipServerReset) {
114
115
  _ccTabs.push(tab);
115
116
  _ccActiveTabId = tabId;
116
117
  // New tab starts with null sessionId — server creates fresh session on first message
117
- // Abort any in-flight request
118
- ccAbort();
119
- _ccSending = false;
120
- _ccQueue = [];
121
- try { localStorage.removeItem('cc-sending'); } catch {}
122
118
  document.getElementById('cc-messages').innerHTML = '';
123
119
  ccRenderTabBar();
124
120
  ccUpdateSessionIndicator();
@@ -149,13 +145,13 @@ function ccSwitchTab(id) {
149
145
  function ccCloseTab(id) {
150
146
  var idx = _ccTabs.findIndex(function(t) { return t.id === id; });
151
147
  if (idx === -1) return;
152
- // If tab has active request, confirm before closing
153
- if (_ccSending && _ccActiveTabId === id) {
148
+ var closingTab = _ccTabs.find(function(t) { return t.id === id; });
149
+ if (closingTab && closingTab._sending) {
154
150
  if (!confirm('This tab has an active request. Close anyway?')) return;
155
- ccAbort();
156
- _ccSending = false;
157
- _ccQueue = [];
158
- try { localStorage.removeItem('cc-sending'); } catch {}
151
+ if (closingTab._abortController) { closingTab._abortController.abort(); closingTab._abortController = null; }
152
+ closingTab._sending = false;
153
+ closingTab._queue = [];
154
+ _ccSending = (_ccTabs.some(function(t) { return t._sending; }));
159
155
  }
160
156
  _ccTabs.splice(idx, 1);
161
157
  if (_ccActiveTabId === id) {
@@ -328,23 +324,26 @@ async function ccSend() {
328
324
  if (!message) return;
329
325
  input.value = '';
330
326
 
331
- // If already processing, queue the message
332
- if (_ccSending) {
333
- _ccQueue.push(message);
327
+ var tab = _ccActiveTab();
328
+ if (!tab) return;
329
+ if (!tab._queue) tab._queue = [];
330
+
331
+ // If this tab is already processing, queue the message
332
+ if (tab._sending) {
333
+ tab._queue.push(message);
334
334
  _renderQueueIndicator();
335
335
  return;
336
336
  }
337
337
  var wasAborted = await _ccDoSend(message);
338
338
 
339
- // Drain queue — send each queued message in order
340
- while (_ccQueue.length > 0) {
341
- // After abort, pause to let server release ccInFlight guard before next send
339
+ // Drain this tab's queue — send each queued message in order
340
+ while (tab._queue && tab._queue.length > 0) {
342
341
  if (wasAborted) {
343
- _ccSending = true; // keep sending flag true so new messages queue instead of bypassing
342
+ tab._sending = true;
344
343
  await new Promise(function(r) { setTimeout(r, 500); });
345
- _ccSending = false;
344
+ tab._sending = false;
346
345
  }
347
- var next = _ccQueue.shift();
346
+ var next = tab._queue.shift();
348
347
  _renderQueueIndicator();
349
348
  wasAborted = await _ccDoSend(next);
350
349
  }
@@ -353,10 +352,11 @@ async function ccSend() {
353
352
  function _renderQueueIndicator() {
354
353
  // Remove all existing queue indicators
355
354
  document.querySelectorAll('.cc-queue-item').forEach(function(el) { el.remove(); });
356
- if (_ccQueue.length === 0) return;
355
+ var tab = _ccActiveTab();
356
+ var queue = (tab && tab._queue) || [];
357
+ if (queue.length === 0) return;
357
358
  var msgs = document.getElementById('cc-messages');
358
- // Add each queued message as its own bubble at the bottom
359
- _ccQueue.forEach(function(m) {
359
+ queue.forEach(function(m) {
360
360
  var el = document.createElement('div');
361
361
  el.className = 'cc-queue-item';
362
362
  el.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;align-self:flex-end;background:var(--blue);color:#fff;opacity:0.5;order:9999';
@@ -387,8 +387,12 @@ async function _ccDoSend(message, skipUserMsg) {
387
387
  return;
388
388
  }
389
389
 
390
- _ccSending = true;
391
- _ccAbortController = new AbortController();
390
+ var activeTab = _ccActiveTab();
391
+ var activeTabId = _ccActiveTabId;
392
+ if (!activeTab) return;
393
+ activeTab._sending = true;
394
+ activeTab._abortController = new AbortController();
395
+ _ccSending = true; // UI indicator
392
396
  var _wasAborted = false;
393
397
  try { localStorage.setItem('cc-sending', JSON.stringify({ sending: true, startedAt: Date.now() })); } catch {}
394
398
 
@@ -449,7 +453,7 @@ async function _ccDoSend(message, skipUserMsg) {
449
453
  html += '<div style="margin-top:' + (streamedText ? '6px' : '0') + '">' + _getThinkingHtml() + '</div>';
450
454
  streamDiv.innerHTML = html;
451
455
  // Re-append queue indicators so they stay below the streaming content
452
- if (_ccQueue.length > 0) _renderQueueIndicator();
456
+ if (activeTab._queue && activeTab._queue.length > 0) _renderQueueIndicator();
453
457
  if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
454
458
  }
455
459
  // Start phase timer immediately so thinking text updates while waiting for SSE
@@ -461,7 +465,7 @@ async function _ccDoSend(message, skipUserMsg) {
461
465
  var res = await fetch('/api/command-center/stream', {
462
466
  method: 'POST', headers: { 'Content-Type': 'application/json' },
463
467
  body: JSON.stringify({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }),
464
- signal: _ccAbortController ? _ccAbortController.signal : AbortSignal.timeout(960000)
468
+ signal: activeTab._abortController ? activeTab._abortController.signal : AbortSignal.timeout(960000)
465
469
  });
466
470
 
467
471
  if (!res.ok) {
@@ -574,8 +578,8 @@ async function _ccDoSend(message, skipUserMsg) {
574
578
  ' <button onclick="ccNewTab()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer;font-size:11px">New Session</button>');
575
579
  }
576
580
  } finally {
577
- _ccSending = false;
578
- _ccAbortController = null;
581
+ if (activeTab) { activeTab._sending = false; activeTab._abortController = null; }
582
+ _ccSending = (_ccTabs.some(function(t) { return t._sending; }));
579
583
  try { clearInterval(phaseTimer); } catch { /* may not be defined if error before reader */ }
580
584
  try { localStorage.removeItem('cc-sending'); } catch {}
581
585
  // Show notification badge on CC button if drawer is closed
@@ -601,8 +605,9 @@ function ccRetryLast() {
601
605
  tab.messages = tab.messages.slice(0, -1); // remove error from history
602
606
  // Resend, then drain queue
603
607
  _ccDoSend(text.trim()).then(async function() {
604
- while (_ccQueue.length > 0) {
605
- var next = _ccQueue.shift();
608
+ var retryTab = _ccActiveTab();
609
+ while (retryTab && retryTab._queue && retryTab._queue.length > 0) {
610
+ var next = retryTab._queue.shift();
606
611
  _renderQueueIndicator();
607
612
  await _ccDoSend(next);
608
613
  }
package/dashboard.js CHANGED
@@ -464,8 +464,7 @@ setInterval(() => {
464
464
  const CC_SESSION_EXPIRY_MS = 2 * 60 * 60 * 1000; // 2 hours
465
465
  const CC_SESSION_MAX_TURNS = Infinity;
466
466
  let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
467
- let ccInFlight = false;
468
- let ccInFlightSince = 0; // timestamp — auto-release stuck guard
467
+ const ccInFlightTabs = new Set(); // per-tab in-flight tracking for parallel CC requests
469
468
  const CC_INFLIGHT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes — auto-release if request hangs
470
469
 
471
470
  // _ccPromptHash computed after CC_STATIC_SYSTEM_PROMPT is defined (see below)
@@ -3250,7 +3249,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3250
3249
 
3251
3250
  async function handleCommandCenterNewSession(req, res) {
3252
3251
  ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
3253
- ccInFlight = false; // Reset concurrency guard so a stuck request doesn't block new sessions
3252
+ ccInFlightTabs.clear(); // Reset all in-flight guards
3254
3253
  safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
3255
3254
  return jsonReply(res, 200, { ok: true });
3256
3255
  }
@@ -3275,13 +3274,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3275
3274
  const body = await readBody(req);
3276
3275
  if (!body.message) return jsonReply(res, 400, { error: 'message required' });
3277
3276
 
3278
- // Concurrency guard — only one CC call at a time, with auto-release for stuck requests
3279
- if (ccInFlight && (Date.now() - ccInFlightSince) < CC_INFLIGHT_TIMEOUT_MS) {
3280
- return jsonReply(res, 429, { error: 'Command Center is busy — wait for the current request to finish, or click "New Session" to reset.' });
3277
+ // Per-tab concurrency guard
3278
+ const tabId = body.tabId || 'default';
3279
+ if (ccInFlightTabs.has(tabId)) {
3280
+ return jsonReply(res, 429, { error: 'This tab is already processing — wait or open a new tab.' });
3281
3281
  }
3282
- if (ccInFlight) console.log('[CC] Auto-releasing stuck in-flight guard after timeout');
3283
- ccInFlight = true;
3284
- ccInFlightSince = Date.now();
3282
+ ccInFlightTabs.add(tabId);
3285
3283
 
3286
3284
  try {
3287
3285
  if (body.sessionId && body.sessionId !== ccSession.sessionId) {
@@ -3309,10 +3307,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3309
3307
 
3310
3308
  return jsonReply(res, 200, { ...parseCCActions(result.text), sessionId: ccSession.sessionId, newSession: !wasResume });
3311
3309
  } finally {
3312
- ccInFlight = false;
3313
- ccInFlightSince = 0;
3310
+ ccInFlightTabs.delete(tabId);
3314
3311
  }
3315
- } catch (e) { ccInFlight = false; return jsonReply(res, 500, { error: e.message }); }
3312
+ } catch (e) { ccInFlightTabs.delete(tabId); return jsonReply(res, 500, { error: e.message }); }
3316
3313
  }
3317
3314
 
3318
3315
  async function handleCommandCenterStream(req, res) {
@@ -3320,17 +3317,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3320
3317
  try {
3321
3318
  const body = await readBody(req);
3322
3319
  if (!body.message) { res.statusCode = 400; res.end('message required'); return; }
3323
- if (ccInFlight && (Date.now() - ccInFlightSince) < CC_INFLIGHT_TIMEOUT_MS) {
3324
- res.statusCode = 429; res.end('CC busy'); return;
3320
+ const tabId = body.tabId || 'default';
3321
+ if (ccInFlightTabs.has(tabId)) {
3322
+ res.statusCode = 429; res.end('This tab is already processing'); return;
3325
3323
  }
3326
- if (ccInFlight) console.log('[CC-stream] Auto-releasing stuck guard');
3327
- ccInFlight = true;
3328
- ccInFlightSince = Date.now();
3324
+ ccInFlightTabs.add(tabId);
3329
3325
 
3330
3326
  res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
3331
3327
  let _ccStreamAbort = null;
3332
3328
  // Kill LLM process immediately if client disconnects mid-stream
3333
- req.on('close', () => { ccInFlight = false; ccInFlightSince = 0; if (_ccStreamAbort) _ccStreamAbort(); });
3329
+ req.on('close', () => { ccInFlightTabs.delete(tabId); if (_ccStreamAbort) _ccStreamAbort(); });
3334
3330
 
3335
3331
  try {
3336
3332
  // Session management — per-tab: use sessionId from request if provided
@@ -3417,12 +3413,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3417
3413
  res.write('data: ' + JSON.stringify({ type: 'done', text: displayText, actions, sessionId: ccSession.sessionId, newSession: !wasResume }) + '\n\n');
3418
3414
  res.end();
3419
3415
  } finally {
3420
- ccInFlight = false;
3421
- ccInFlightSince = 0;
3416
+ ccInFlightTabs.delete(tabId);
3422
3417
  }
3423
3418
  } catch (e) {
3424
- ccInFlight = false;
3425
- ccInFlightSince = 0;
3419
+ ccInFlightTabs.delete(tabId);
3426
3420
  try { res.write('data: ' + JSON.stringify({ type: 'error', error: e.message }) + '\n\n'); } catch {}
3427
3421
  try { res.end(); } catch {}
3428
3422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.748",
3
+ "version": "0.1.750",
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"