@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 +7 -1
- package/dashboard/js/command-center.js +42 -37
- package/dashboard.js +16 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
var
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
153
|
-
if (
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
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 (
|
|
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
|
-
|
|
342
|
+
tab._sending = true;
|
|
344
343
|
await new Promise(function(r) { setTimeout(r, 500); });
|
|
345
|
-
|
|
344
|
+
tab._sending = false;
|
|
346
345
|
}
|
|
347
|
-
var next =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
578
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
3279
|
-
|
|
3280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3313
|
-
ccInFlightSince = 0;
|
|
3310
|
+
ccInFlightTabs.delete(tabId);
|
|
3314
3311
|
}
|
|
3315
|
-
} catch (e) {
|
|
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
|
-
|
|
3324
|
-
|
|
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
|
-
|
|
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', () => {
|
|
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
|
-
|
|
3421
|
-
ccInFlightSince = 0;
|
|
3416
|
+
ccInFlightTabs.delete(tabId);
|
|
3422
3417
|
}
|
|
3423
3418
|
} catch (e) {
|
|
3424
|
-
|
|
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.
|
|
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"
|