@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 +3 -1
- package/dashboard/js/command-center.js +37 -33
- package/dashboard.js +16 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
-
|
|
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) {
|
|
@@ -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 = '
|
|
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 + '\')">×</span>';
|
|
219
215
|
html += '</div>';
|
|
220
216
|
}
|
|
221
|
-
html += '<
|
|
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">▼</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
|
-
|
|
331
|
-
if (
|
|
332
|
-
|
|
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 (
|
|
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
|
-
|
|
342
|
+
tab._sending = true;
|
|
343
343
|
await new Promise(function(r) { setTimeout(r, 500); });
|
|
344
|
-
|
|
344
|
+
tab._sending = false;
|
|
345
345
|
}
|
|
346
|
-
var next =
|
|
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
|
-
|
|
390
|
-
|
|
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:
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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.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"
|