@yemi33/minions 0.1.747 → 0.1.749

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,11 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.747 (2026-04-09)
3
+ ## 0.1.749 (2026-04-09)
4
4
 
5
5
  ### Features
6
+ - truly parallel CC tabs — per-tab sessions, sending state, abort
6
7
  - CC multi-tab conversations with session resume
7
8
 
8
9
  ### Fixes
10
+ - move + button after tabs, style as folder tab
9
11
  - clear stale sessionId on resume failure, folder-style tab UI
10
12
  - CC tabs use per-tab sessions, not shared global session
11
13
 
@@ -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) {
@@ -209,7 +205,7 @@ function ccShowAllConversations() {
209
205
  function ccRenderTabBar() {
210
206
  var bar = document.getElementById('cc-tab-bar');
211
207
  if (!bar) return;
212
- var html = '<button onclick="ccNewTab()" style="background:none;border:1px solid var(--border);color:var(--blue);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;white-space:nowrap;flex-shrink:0" title="New tab">+</button>';
208
+ var html = '';
213
209
  for (var i = 0; i < _ccTabs.length; i++) {
214
210
  var t = _ccTabs[i];
215
211
  var isActive = t.id === _ccActiveTabId;
@@ -218,7 +214,8 @@ function ccRenderTabBar() {
218
214
  html += '<span class="cc-tab-close" onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\')">&times;</span>';
219
215
  html += '</div>';
220
216
  }
221
- html += '<button id="cc-all-btn" onclick="ccShowAllConversations()" style="background:none;border:1px solid var(--border);color:var(--muted);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;white-space:nowrap;flex-shrink:0;margin-left:auto" title="All conversations">&#x25BC;</button>';
217
+ html += '<div class="cc-tab" onclick="ccNewTab()" title="New tab" style="color:var(--muted);padding:4px 8px">+</div>';
218
+ html += '<button id="cc-all-btn" onclick="ccShowAllConversations()" style="background:none;border:none;color:var(--muted);font-size:11px;padding:4px 6px;cursor:pointer;white-space:nowrap;flex-shrink:0;margin-left:auto" title="All conversations">&#x25BC;</button>';
222
219
  bar.innerHTML = html;
223
220
  }
224
221
 
@@ -327,23 +324,26 @@ async function ccSend() {
327
324
  if (!message) return;
328
325
  input.value = '';
329
326
 
330
- // If already processing, queue the message
331
- if (_ccSending) {
332
- _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);
333
334
  _renderQueueIndicator();
334
335
  return;
335
336
  }
336
337
  var wasAborted = await _ccDoSend(message);
337
338
 
338
- // Drain queue — send each queued message in order
339
- while (_ccQueue.length > 0) {
340
- // 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) {
341
341
  if (wasAborted) {
342
- _ccSending = true; // keep sending flag true so new messages queue instead of bypassing
342
+ tab._sending = true;
343
343
  await new Promise(function(r) { setTimeout(r, 500); });
344
- _ccSending = false;
344
+ tab._sending = false;
345
345
  }
346
- var next = _ccQueue.shift();
346
+ var next = tab._queue.shift();
347
347
  _renderQueueIndicator();
348
348
  wasAborted = await _ccDoSend(next);
349
349
  }
@@ -386,8 +386,12 @@ async function _ccDoSend(message, skipUserMsg) {
386
386
  return;
387
387
  }
388
388
 
389
- _ccSending = true;
390
- _ccAbortController = new AbortController();
389
+ var activeTab = _ccActiveTab();
390
+ var activeTabId = _ccActiveTabId;
391
+ if (!activeTab) return;
392
+ activeTab._sending = true;
393
+ activeTab._abortController = new AbortController();
394
+ _ccSending = true; // UI indicator
391
395
  var _wasAborted = false;
392
396
  try { localStorage.setItem('cc-sending', JSON.stringify({ sending: true, startedAt: Date.now() })); } catch {}
393
397
 
@@ -460,7 +464,7 @@ async function _ccDoSend(message, skipUserMsg) {
460
464
  var res = await fetch('/api/command-center/stream', {
461
465
  method: 'POST', headers: { 'Content-Type': 'application/json' },
462
466
  body: JSON.stringify({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }),
463
- signal: _ccAbortController ? _ccAbortController.signal : AbortSignal.timeout(960000)
467
+ signal: activeTab._abortController ? activeTab._abortController.signal : AbortSignal.timeout(960000)
464
468
  });
465
469
 
466
470
  if (!res.ok) {
@@ -573,8 +577,8 @@ async function _ccDoSend(message, skipUserMsg) {
573
577
  ' <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>');
574
578
  }
575
579
  } finally {
576
- _ccSending = false;
577
- _ccAbortController = null;
580
+ if (activeTab) { activeTab._sending = false; activeTab._abortController = null; }
581
+ _ccSending = (_ccTabs.some(function(t) { return t._sending; }));
578
582
  try { clearInterval(phaseTimer); } catch { /* may not be defined if error before reader */ }
579
583
  try { localStorage.removeItem('cc-sending'); } catch {}
580
584
  // Show notification badge on CC button if drawer is closed
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.747",
3
+ "version": "0.1.749",
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"