@yemi33/minions 0.1.1578 → 0.1.1580
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 +5 -1
- package/dashboard/js/command-center.js +104 -88
- package/dashboard/js/modal-qa.js +166 -71
- package/dashboard.js +471 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1580 (2026-04-28)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- stream doc chat progress
|
|
6
7
|
- hash-dedup, compress+normalize pass, dynamic stale-guard, rich result
|
|
7
8
|
|
|
9
|
+
### Other
|
|
10
|
+
- Keep CC streams reconnectable
|
|
11
|
+
|
|
8
12
|
## 0.1.1577 (2026-04-27)
|
|
9
13
|
|
|
10
14
|
### Fixes
|
|
@@ -94,6 +94,13 @@ function _ccFindPinTarget(query) {
|
|
|
94
94
|
function ccAbort() {
|
|
95
95
|
var tab = _ccActiveTab();
|
|
96
96
|
if (tab && tab._abortController) {
|
|
97
|
+
try {
|
|
98
|
+
fetch('/api/command-center/abort', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ tabId: tab.id })
|
|
102
|
+
}).catch(function() {});
|
|
103
|
+
} catch {}
|
|
97
104
|
tab._abortController.abort();
|
|
98
105
|
tab._abortController = null;
|
|
99
106
|
}
|
|
@@ -211,6 +218,13 @@ function ccCloseTab(id) {
|
|
|
211
218
|
var closingTab = _ccTabs.find(function(t) { return t.id === id; });
|
|
212
219
|
if (closingTab && closingTab._sending) {
|
|
213
220
|
if (!confirm('This tab has an active request. Close anyway?')) return;
|
|
221
|
+
try {
|
|
222
|
+
fetch('/api/command-center/abort', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify({ tabId: id })
|
|
226
|
+
}).catch(function() {});
|
|
227
|
+
} catch {}
|
|
214
228
|
if (closingTab._abortController) { closingTab._abortController.abort(); closingTab._abortController = null; }
|
|
215
229
|
closingTab._sending = false;
|
|
216
230
|
closingTab._queue = [];
|
|
@@ -445,6 +459,8 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
445
459
|
if (existingQueueEl) existingQueueEl.remove();
|
|
446
460
|
|
|
447
461
|
var ccStartTime = Date.now();
|
|
462
|
+
var reconnectAttempts = 0;
|
|
463
|
+
var streamStatusNote = '';
|
|
448
464
|
var phases = [
|
|
449
465
|
[0, 'Thinking...'],
|
|
450
466
|
[3000, 'Reading minions context...'],
|
|
@@ -511,6 +527,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
511
527
|
if (streamedText) {
|
|
512
528
|
html += renderMd(streamedText);
|
|
513
529
|
}
|
|
530
|
+
if (streamStatusNote) {
|
|
531
|
+
html += '<div style="margin-top:6px;font-size:10px;color:var(--muted)">' + escHtml(streamStatusNote) + '</div>';
|
|
532
|
+
}
|
|
514
533
|
html += '<div style="margin-top:' + (streamedText ? '6px' : '0') + '">' + _getThinkingHtml() + '</div>';
|
|
515
534
|
streamDiv.innerHTML = html;
|
|
516
535
|
// Re-append queue indicators so they stay below the streaming content
|
|
@@ -531,35 +550,70 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
531
550
|
var phaseTimer = setInterval(updateStreamDiv, 1000);
|
|
532
551
|
updateStreamDiv(); // render proper layout immediately (not raw "Thinking..." text)
|
|
533
552
|
|
|
534
|
-
|
|
535
|
-
// Stream response via SSE — shows text as it arrives
|
|
553
|
+
async function _ccConsumeStream(requestBody, isReconnect) {
|
|
536
554
|
var res = await fetch('/api/command-center/stream', {
|
|
537
555
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
538
|
-
body: JSON.stringify(
|
|
556
|
+
body: JSON.stringify(requestBody),
|
|
539
557
|
signal: activeTab._abortController ? activeTab._abortController.signal : AbortSignal.timeout(960000)
|
|
540
558
|
});
|
|
541
559
|
|
|
542
560
|
if (!res.ok) {
|
|
543
|
-
|
|
544
|
-
if (res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
|
|
561
|
+
if (!isReconnect && res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
|
|
545
562
|
activeTab._429retries = (activeTab._429retries || 0) + 1;
|
|
546
|
-
_cleanupStreamDiv(); // remove current thinking div — prevents stacking on each retry
|
|
547
563
|
await new Promise(function(r) { setTimeout(r, 1500); });
|
|
548
|
-
return await
|
|
564
|
+
return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }, false);
|
|
549
565
|
}
|
|
550
566
|
activeTab._429retries = 0;
|
|
551
|
-
_cleanupStreamDiv();
|
|
552
567
|
var errText = await res.text();
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return;
|
|
568
|
+
if (isReconnect && res.status === 409) return { interrupted: true, reconnectable: false, reason: errText || 'No live stream' };
|
|
569
|
+
throw new Error(errText || 'CC error');
|
|
556
570
|
}
|
|
557
571
|
|
|
572
|
+
activeTab._429retries = 0;
|
|
573
|
+
streamStatusNote = '';
|
|
574
|
+
updateStreamDiv();
|
|
575
|
+
|
|
558
576
|
var reader = res.body.getReader();
|
|
559
577
|
var decoder = new TextDecoder();
|
|
560
578
|
var buf = '';
|
|
561
579
|
var terminalEventSeen = false;
|
|
562
580
|
|
|
581
|
+
async function _handleEvent(evt) {
|
|
582
|
+
if (evt.type === 'chunk') {
|
|
583
|
+
streamedText = evt.text;
|
|
584
|
+
if (activeTab) activeTab._streamedText = streamedText;
|
|
585
|
+
updateStreamDiv();
|
|
586
|
+
} else if (evt.type === 'heartbeat') {
|
|
587
|
+
return;
|
|
588
|
+
} else if (evt.type === 'tool') {
|
|
589
|
+
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
590
|
+
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
|
591
|
+
updateStreamDiv();
|
|
592
|
+
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
593
|
+
} else if (evt.type === 'done') {
|
|
594
|
+
terminalEventSeen = true;
|
|
595
|
+
_cleanupStreamDiv();
|
|
596
|
+
if (evt.sessionReset) {
|
|
597
|
+
addMsg('system', '<div style="text-align:center;padding:6px 12px;font-size:11px;color:var(--muted);background:var(--surface2);border-radius:6px;margin:4px 0">Minions was updated — started a fresh session with latest context.</div>', false, activeTabId);
|
|
598
|
+
}
|
|
599
|
+
var rendered = renderMd(evt.text || streamedText || '');
|
|
600
|
+
addMsg('assistant', rendered + _ccElapsedFooter('{seconds}s'));
|
|
601
|
+
if (evt.sessionId !== undefined) {
|
|
602
|
+
var originTab = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
603
|
+
if (originTab) { originTab.sessionId = evt.sessionId || null; }
|
|
604
|
+
ccSaveState(); ccUpdateSessionIndicator();
|
|
605
|
+
}
|
|
606
|
+
if (evt.actions && evt.actions.length > 0) {
|
|
607
|
+
_tagServerExecuted(evt.actions, evt.actionResults);
|
|
608
|
+
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
609
|
+
}
|
|
610
|
+
} else if (evt.type === 'error') {
|
|
611
|
+
terminalEventSeen = true;
|
|
612
|
+
_cleanupStreamDiv();
|
|
613
|
+
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(evt.error) + '</span>');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
563
617
|
while (true) {
|
|
564
618
|
var readResult = await reader.read();
|
|
565
619
|
if (readResult.done) break;
|
|
@@ -569,93 +623,55 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
569
623
|
for (var li = 0; li < lines.length; li++) {
|
|
570
624
|
var line = lines[li];
|
|
571
625
|
if (!line.startsWith('data: ')) continue;
|
|
572
|
-
try {
|
|
573
|
-
var evt = JSON.parse(line.slice(6));
|
|
574
|
-
if (evt.type === 'chunk') {
|
|
575
|
-
streamedText = evt.text;
|
|
576
|
-
if (activeTab) activeTab._streamedText = streamedText;
|
|
577
|
-
updateStreamDiv();
|
|
578
|
-
} else if (evt.type === 'heartbeat') {
|
|
579
|
-
continue;
|
|
580
|
-
} else if (evt.type === 'tool') {
|
|
581
|
-
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
582
|
-
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
|
583
|
-
updateStreamDiv();
|
|
584
|
-
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
585
|
-
} else if (evt.type === 'done') {
|
|
586
|
-
terminalEventSeen = true;
|
|
587
|
-
_cleanupStreamDiv();
|
|
588
|
-
// If system prompt changed, show a notice before the response
|
|
589
|
-
if (evt.sessionReset) {
|
|
590
|
-
addMsg('system', '<div style="text-align:center;padding:6px 12px;font-size:11px;color:var(--muted);background:var(--surface2);border-radius:6px;margin:4px 0">Minions was updated — started a fresh session with latest context.</div>', false, activeTabId);
|
|
591
|
-
}
|
|
592
|
-
// placeholder was added with skipSave=true — nothing to pop
|
|
593
|
-
var rendered = renderMd(evt.text || streamedText || '');
|
|
594
|
-
addMsg('assistant', rendered + _ccElapsedFooter('{seconds}s'));
|
|
595
|
-
if (evt.sessionId !== undefined) {
|
|
596
|
-
// Save session to the originating tab, not whichever tab is active now
|
|
597
|
-
var originTab = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
598
|
-
if (originTab) { originTab.sessionId = evt.sessionId || null; }
|
|
599
|
-
ccSaveState(); ccUpdateSessionIndicator();
|
|
600
|
-
}
|
|
601
|
-
if (evt.actions && evt.actions.length > 0) {
|
|
602
|
-
_tagServerExecuted(evt.actions, evt.actionResults);
|
|
603
|
-
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
604
|
-
}
|
|
605
|
-
} else if (evt.type === 'error') {
|
|
606
|
-
terminalEventSeen = true;
|
|
607
|
-
_cleanupStreamDiv();
|
|
608
|
-
// placeholder was skipSave — no pop needed
|
|
609
|
-
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(evt.error) + '</span>');
|
|
610
|
-
}
|
|
611
|
-
} catch { /* incomplete JSON */ }
|
|
626
|
+
try { await _handleEvent(JSON.parse(line.slice(6))); } catch {}
|
|
612
627
|
}
|
|
613
628
|
}
|
|
614
|
-
// Process any remaining buffered data after stream ends
|
|
615
629
|
if (buf.trim()) {
|
|
616
630
|
var remainingLines = buf.split('\n');
|
|
617
631
|
for (var ri = 0; ri < remainingLines.length; ri++) {
|
|
618
632
|
var rline = remainingLines[ri];
|
|
619
633
|
if (!rline.startsWith('data: ')) continue;
|
|
620
|
-
try {
|
|
621
|
-
var revt = JSON.parse(rline.slice(6));
|
|
622
|
-
if (revt.type === 'done') {
|
|
623
|
-
terminalEventSeen = true;
|
|
624
|
-
_cleanupStreamDiv();
|
|
625
|
-
// placeholder was skipSave — no pop needed
|
|
626
|
-
var rendered2 = renderMd(revt.text || streamedText || '');
|
|
627
|
-
addMsg('assistant', rendered2 + _ccElapsedFooter('{seconds}s'));
|
|
628
|
-
if (revt.sessionId !== undefined) {
|
|
629
|
-
var originTab2 = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
630
|
-
if (originTab2) { originTab2.sessionId = revt.sessionId || null; }
|
|
631
|
-
ccSaveState(); ccUpdateSessionIndicator();
|
|
632
|
-
}
|
|
633
|
-
if (revt.actions && revt.actions.length > 0) {
|
|
634
|
-
_tagServerExecuted(revt.actions, revt.actionResults);
|
|
635
|
-
for (var ai2 = 0; ai2 < revt.actions.length; ai2++) { await ccExecuteAction(revt.actions[ai2], activeTabId); }
|
|
636
|
-
}
|
|
637
|
-
} else if (revt.type === 'heartbeat') {
|
|
638
|
-
continue;
|
|
639
|
-
} else if (revt.type === 'error') {
|
|
640
|
-
terminalEventSeen = true;
|
|
641
|
-
_cleanupStreamDiv();
|
|
642
|
-
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(revt.error) + '</span>');
|
|
643
|
-
} else if (revt.type === 'chunk') {
|
|
644
|
-
streamedText = revt.text;
|
|
645
|
-
updateStreamDiv();
|
|
646
|
-
}
|
|
647
|
-
} catch {}
|
|
634
|
+
try { await _handleEvent(JSON.parse(rline.slice(6))); } catch {}
|
|
648
635
|
}
|
|
649
636
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
637
|
+
return { interrupted: !terminalEventSeen, reconnectable: true };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
while (true) {
|
|
642
|
+
var consume = await _ccConsumeStream(
|
|
643
|
+
reconnectAttempts === 0
|
|
644
|
+
? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }
|
|
645
|
+
: { tabId: activeTabId, sessionId: activeTab.sessionId || null, reconnect: true },
|
|
646
|
+
reconnectAttempts > 0
|
|
647
|
+
);
|
|
648
|
+
if (!consume.interrupted) break;
|
|
649
|
+
if (!consume.reconnectable || reconnectAttempts >= 2) {
|
|
650
|
+
_cleanupStreamDiv();
|
|
651
|
+
var streamEndedHint = '<div style="font-size:10px;color:var(--muted);margin-top:4px">The response stream ended before completion. Retry to continue from the last user message.</div>';
|
|
652
|
+
if (streamedText) {
|
|
653
|
+
addMsg('assistant', renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') + _ccRetryControls(streamEndedHint, false));
|
|
654
|
+
} else {
|
|
655
|
+
addMsg('assistant', '<span style="color:var(--red)">The response stream ended before completion.</span>' + _ccRetryControls(streamEndedHint, false));
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
var reconnectHealth = await _ccDashboardHealth();
|
|
660
|
+
if (!reconnectHealth.reachable || reconnectHealth.restarted) {
|
|
661
|
+
_cleanupStreamDiv();
|
|
662
|
+
var reconnectHint = reconnectHealth.restarted
|
|
663
|
+
? '<div style="font-size:10px;color:var(--muted);margin-top:4px">Dashboard restarted while this response was streaming. Reload the page to reconnect to the new instance.</div>'
|
|
664
|
+
: '<div style="font-size:10px;color:var(--muted);margin-top:4px">The request stream was interrupted, but the dashboard is still reachable. Retry or start a new session.</div>';
|
|
665
|
+
addMsg('assistant', (streamedText ? renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') : '') +
|
|
666
|
+
_ccRetryControls(reconnectHint, reconnectHealth.restarted));
|
|
667
|
+
break;
|
|
658
668
|
}
|
|
669
|
+
reconnectAttempts++;
|
|
670
|
+
toolsUsed = [];
|
|
671
|
+
if (activeTab) activeTab._toolsUsed = [];
|
|
672
|
+
streamStatusNote = 'Connection interrupted — reattaching to the live response...';
|
|
673
|
+
updateStreamDiv();
|
|
674
|
+
await new Promise(function(r) { setTimeout(r, 1000 * reconnectAttempts); });
|
|
659
675
|
}
|
|
660
676
|
} catch (e) {
|
|
661
677
|
_cleanupStreamDiv();
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -191,9 +191,36 @@ function _qaBuildAssistantHtml(text, opts) {
|
|
|
191
191
|
const pad = opts?.isError ? '' : 'padding-right:24px;';
|
|
192
192
|
return '<div class="modal-qa-a" style="' + style + '">' +
|
|
193
193
|
(opts?.isError ? '' : llmCopyBtn()) +
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
body +
|
|
195
|
+
'<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right;' + pad + '">' + opts.elapsed + 's</div>' +
|
|
196
|
+
'</div>';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _qaBuildLiveProgressHtml(loadingId, label, elapsedSeconds, streamedText, toolsUsed, queueCount) {
|
|
200
|
+
const qaQueueBadge = queueCount > 0 ? ' <span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + queueCount + ' queued</span>' : '';
|
|
201
|
+
let html = '';
|
|
202
|
+
if (toolsUsed && toolsUsed.length > 0) {
|
|
203
|
+
html += '<div style="margin-bottom:6px">';
|
|
204
|
+
toolsUsed.forEach(function(t) {
|
|
205
|
+
const name = typeof t === 'string' ? t : t.name;
|
|
206
|
+
const input = typeof t === 'string' ? {} : (t.input || {});
|
|
207
|
+
html += '<div style="color:var(--muted);font-size:10px;font-family:monospace"><span style="flex-shrink:0">●</span> ' + formatToolSummary(name, input) + '</div>';
|
|
208
|
+
});
|
|
209
|
+
html += '</div>';
|
|
210
|
+
}
|
|
211
|
+
if (streamedText) html += '<div style="margin-bottom:6px">' + renderMd(streamedText) + '</div>';
|
|
212
|
+
html += '<div style="display:flex;flex-direction:column;align-items:flex-start;gap:6px">' +
|
|
213
|
+
'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">' +
|
|
214
|
+
'<span class="dot-pulse"><span></span><span></span><span></span></span> ' +
|
|
215
|
+
'<span id="' + loadingId + '-text">' + escHtml(label) + '</span>' +
|
|
216
|
+
'</div>' +
|
|
217
|
+
'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">' +
|
|
218
|
+
'<span id="' + loadingId + '-time" style="font-size:10px;color:var(--muted)">' + elapsedSeconds + 's</span>' +
|
|
219
|
+
'<button onclick="qaAbort()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Stop</button>' +
|
|
220
|
+
qaQueueBadge +
|
|
221
|
+
'</div>' +
|
|
196
222
|
'</div>';
|
|
223
|
+
return html;
|
|
197
224
|
}
|
|
198
225
|
|
|
199
226
|
function _qaMutateThreadHtml(key, mutate) {
|
|
@@ -401,23 +428,49 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
401
428
|
const qaPhases = isPlanEdit
|
|
402
429
|
? [[0,'Reading plan...'],[3000,'Analyzing structure...'],[8000,'Researching context...'],[15000,'Drafting revisions...'],[30000,'Writing updated plan...'],[60000,'Still working (large document)...'],[120000,'Deep edit in progress...'],[300000,'Almost there...']]
|
|
403
430
|
: [[0,'Thinking...'],[3000,'Reading document...'],[8000,'Analyzing...'],[20000,'Still working...'],[60000,'Taking a while...']];
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
textEl.textContent = qaPhases[i][1];
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
431
|
+
let streamedText = '';
|
|
432
|
+
let toolsUsed = [];
|
|
433
|
+
function _qaProgressLabel(elapsed) {
|
|
434
|
+
let label = qaPhases[0][1];
|
|
435
|
+
for (let i = qaPhases.length - 1; i >= 0; i--) {
|
|
436
|
+
if (elapsed >= qaPhases[i][0]) {
|
|
437
|
+
label = qaPhases[i][1];
|
|
438
|
+
break;
|
|
415
439
|
}
|
|
416
440
|
}
|
|
441
|
+
return label;
|
|
442
|
+
}
|
|
443
|
+
function _qaRenderProgress(persist) {
|
|
444
|
+
const elapsed = Date.now() - qaStartTime;
|
|
445
|
+
const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
|
|
446
|
+
const loadingEl = tmp.querySelector('#' + loadingId);
|
|
447
|
+
if (!loadingEl) return;
|
|
448
|
+
loadingEl.innerHTML = _qaBuildLiveProgressHtml(
|
|
449
|
+
loadingId,
|
|
450
|
+
_qaProgressLabel(elapsed),
|
|
451
|
+
Math.floor(elapsed / 1000),
|
|
452
|
+
streamedText,
|
|
453
|
+
toolsUsed,
|
|
454
|
+
runtime.queue.length
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
if (persist) {
|
|
458
|
+
_qaPersistSession(sessionKey, {
|
|
459
|
+
threadHtml: updatedThreadHtml,
|
|
460
|
+
docContext: capturedDocContext,
|
|
461
|
+
filePath: capturedFilePath,
|
|
462
|
+
history: runtime.history,
|
|
463
|
+
queue: runtime.queue,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const qaTimer = setInterval(() => {
|
|
468
|
+
_qaRenderProgress(false);
|
|
417
469
|
}, 500);
|
|
470
|
+
_qaRenderProgress(false);
|
|
418
471
|
|
|
419
472
|
try {
|
|
420
|
-
const res = await fetch('/api/doc-chat', {
|
|
473
|
+
const res = await fetch('/api/doc-chat/stream', {
|
|
421
474
|
method: 'POST',
|
|
422
475
|
headers: { 'Content-Type': 'application/json' },
|
|
423
476
|
signal: abortController.signal,
|
|
@@ -431,76 +484,118 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
431
484
|
contentHash: capturedDocContext.content ? capturedDocContext.content.length + ':' + capturedDocContext.content.charCodeAt(0) + ':' + capturedDocContext.content.charCodeAt(capturedDocContext.content.length - 1) : undefined,
|
|
432
485
|
}),
|
|
433
486
|
});
|
|
434
|
-
const data = await res.json();
|
|
435
|
-
clearInterval(qaTimer);
|
|
436
|
-
const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
|
|
437
487
|
let sessionDocContext = { ...capturedDocContext };
|
|
488
|
+
if (!res.ok) {
|
|
489
|
+
const errText = await res.text();
|
|
490
|
+
let errMsg = errText || 'Failed';
|
|
491
|
+
try {
|
|
492
|
+
const parsed = JSON.parse(errText);
|
|
493
|
+
errMsg = parsed.error || errMsg;
|
|
494
|
+
} catch { /* text/plain fallback */ }
|
|
495
|
+
throw new Error(errMsg);
|
|
496
|
+
}
|
|
438
497
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
498
|
+
const reader = res.body.getReader();
|
|
499
|
+
const decoder = new TextDecoder();
|
|
500
|
+
let buf = '';
|
|
501
|
+
let terminalEventSeen = false;
|
|
502
|
+
|
|
503
|
+
async function _qaHandleStreamEvent(evt) {
|
|
504
|
+
if (!evt || !evt.type) return;
|
|
505
|
+
if (evt.type === 'heartbeat') return;
|
|
506
|
+
if (evt.type === 'chunk') {
|
|
507
|
+
streamedText = evt.text || '';
|
|
508
|
+
_qaRenderProgress(true);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (evt.type === 'tool') {
|
|
512
|
+
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
513
|
+
_qaRenderProgress(true);
|
|
514
|
+
return;
|
|
456
515
|
}
|
|
516
|
+
if (evt.type === 'done') {
|
|
517
|
+
terminalEventSeen = true;
|
|
518
|
+
clearInterval(qaTimer);
|
|
519
|
+
const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
|
|
520
|
+
const borderColor = evt.edited ? 'var(--green)' : 'var(--blue)';
|
|
521
|
+
const suffix = evt.edited ? '\n\n\u2713 Document saved.' : '';
|
|
522
|
+
const answerHtml = _qaBuildAssistantHtml((evt.text || '') + suffix, { borderColor, elapsed: qaElapsed });
|
|
523
|
+
const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
|
|
524
|
+
const loadingEl = tmp.querySelector('#' + loadingId);
|
|
525
|
+
if (loadingEl) loadingEl.remove();
|
|
526
|
+
tmp.insertAdjacentHTML('beforeend', answerHtml);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
runtime.history.push({ role: 'user', text: message });
|
|
530
|
+
runtime.history.push({ role: 'assistant', text: evt.text || '' });
|
|
531
|
+
if (_qaIsActiveSession(sessionKey)) _qaHistory = runtime.history.slice();
|
|
532
|
+
|
|
533
|
+
_qaNotifySidebar(capturedFilePath);
|
|
534
|
+
if (evt.actions && evt.actions.length > 0) {
|
|
535
|
+
for (const action of evt.actions) await ccExecuteAction(action);
|
|
536
|
+
}
|
|
457
537
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
538
|
+
if (evt.edited && evt.content) {
|
|
539
|
+
const display = evt.content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
540
|
+
const isJson = capturedFilePath && capturedFilePath.endsWith('.json');
|
|
541
|
+
sessionDocContext.content = display;
|
|
542
|
+
sessionDocContext.selection = '';
|
|
543
|
+
if (_qaIsActiveSession(sessionKey)) {
|
|
544
|
+
const body = document.getElementById('modal-body');
|
|
545
|
+
if (isJson) {
|
|
546
|
+
body.textContent = display;
|
|
547
|
+
} else {
|
|
548
|
+
body.innerHTML = renderMd(display);
|
|
549
|
+
body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
550
|
+
body.style.whiteSpace = 'normal';
|
|
551
|
+
}
|
|
552
|
+
_modalDocContext.content = display;
|
|
471
553
|
}
|
|
472
|
-
_modalDocContext.content = display;
|
|
473
554
|
}
|
|
555
|
+
|
|
556
|
+
_qaPersistSession(sessionKey, {
|
|
557
|
+
threadHtml: updatedThreadHtml,
|
|
558
|
+
docContext: sessionDocContext,
|
|
559
|
+
filePath: capturedFilePath,
|
|
560
|
+
history: runtime.history,
|
|
561
|
+
queue: runtime.queue,
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
474
564
|
}
|
|
565
|
+
if (evt.type === 'error') throw new Error(evt.error || 'Failed');
|
|
566
|
+
}
|
|
475
567
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
queue: runtime.queue,
|
|
496
|
-
});
|
|
568
|
+
while (true) {
|
|
569
|
+
const readResult = await reader.read();
|
|
570
|
+
if (readResult.done) break;
|
|
571
|
+
buf += decoder.decode(readResult.value, { stream: true });
|
|
572
|
+
const lines = buf.split('\n');
|
|
573
|
+
buf = lines.pop();
|
|
574
|
+
for (let i = 0; i < lines.length; i++) {
|
|
575
|
+
const line = lines[i];
|
|
576
|
+
if (!line.startsWith('data: ')) continue;
|
|
577
|
+
await _qaHandleStreamEvent(JSON.parse(line.slice(6)));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (buf.trim()) {
|
|
581
|
+
const trailing = buf.split('\n');
|
|
582
|
+
for (let i = 0; i < trailing.length; i++) {
|
|
583
|
+
const line = trailing[i];
|
|
584
|
+
if (!line.startsWith('data: ')) continue;
|
|
585
|
+
await _qaHandleStreamEvent(JSON.parse(line.slice(6)));
|
|
586
|
+
}
|
|
497
587
|
}
|
|
588
|
+
if (!terminalEventSeen) throw new Error('The response stream ended before completion.');
|
|
498
589
|
} catch (e) {
|
|
499
590
|
clearInterval(qaTimer);
|
|
500
591
|
const qaElapsedExc = Math.round((Date.now() - qaStartTime) / 1000);
|
|
501
592
|
const messageHtml = e.name === 'AbortError'
|
|
502
|
-
?
|
|
503
|
-
|
|
593
|
+
? (streamedText
|
|
594
|
+
? _qaBuildAssistantHtml(streamedText + '\n\n_Stopped._', { borderColor: 'var(--muted)', elapsed: qaElapsedExc })
|
|
595
|
+
: _qaBuildAssistantHtml('Stopped', { color: 'var(--muted)', isError: true, elapsed: qaElapsedExc }))
|
|
596
|
+
: (streamedText
|
|
597
|
+
? _qaBuildAssistantHtml(streamedText + '\n\nError: ' + e.message, { borderColor: 'var(--red)', elapsed: qaElapsedExc })
|
|
598
|
+
: _qaBuildAssistantHtml('Error: ' + e.message, { color: 'var(--red)', isError: true, elapsed: qaElapsedExc }));
|
|
504
599
|
const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
|
|
505
600
|
const loadingEl = tmp.querySelector('#' + loadingId);
|
|
506
601
|
if (loadingEl) loadingEl.remove();
|
package/dashboard.js
CHANGED
|
@@ -544,10 +544,86 @@ const CC_SESSION_TTL_MS = shared.ENGINE_DEFAULTS.ccSessionTtlMs;
|
|
|
544
544
|
let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
545
545
|
const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight tracking for parallel CC requests
|
|
546
546
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
547
|
+
const ccLiveStreams = new Map(); // tabId → buffered live stream state for reconnect-after-disconnect
|
|
547
548
|
const CC_INFLIGHT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes — auto-release if request hangs
|
|
548
549
|
const CC_LOCK_WAIT_MS = 200; // grace period for previous handler's finally to release lock
|
|
549
550
|
const CC_STREAM_HEARTBEAT_MS = 15000; // keep streaming responses alive across proxies/restart races
|
|
551
|
+
const CC_STREAM_REATTACH_GRACE_MS = 60000; // keep CC job alive briefly after disconnect so the UI can reattach
|
|
552
|
+
const CC_STREAM_DONE_RETENTION_MS = 30000; // retain final payload briefly so reconnect can still receive it
|
|
550
553
|
function _releaseCCTab(tabId) { ccInFlightTabs.delete(tabId); ccInFlightAborts.delete(tabId); }
|
|
554
|
+
function _getCcLiveStream(tabId) {
|
|
555
|
+
return ccLiveStreams.get(tabId) || null;
|
|
556
|
+
}
|
|
557
|
+
function _clearCcLiveTimers(tabId) {
|
|
558
|
+
const state = _getCcLiveStream(tabId);
|
|
559
|
+
if (!state) return;
|
|
560
|
+
if (state.abortTimer) {
|
|
561
|
+
clearTimeout(state.abortTimer);
|
|
562
|
+
state.abortTimer = null;
|
|
563
|
+
}
|
|
564
|
+
if (state.cleanupTimer) {
|
|
565
|
+
clearTimeout(state.cleanupTimer);
|
|
566
|
+
state.cleanupTimer = null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function _clearCcLiveStream(tabId) {
|
|
570
|
+
const state = _getCcLiveStream(tabId);
|
|
571
|
+
if (!state) return;
|
|
572
|
+
_clearCcLiveTimers(tabId);
|
|
573
|
+
ccLiveStreams.delete(tabId);
|
|
574
|
+
}
|
|
575
|
+
function _ensureCcLiveStream(tabId) {
|
|
576
|
+
let state = _getCcLiveStream(tabId);
|
|
577
|
+
if (state) return state;
|
|
578
|
+
state = {
|
|
579
|
+
tabId,
|
|
580
|
+
text: '',
|
|
581
|
+
tools: [],
|
|
582
|
+
donePayload: null,
|
|
583
|
+
writer: null,
|
|
584
|
+
endResponse: null,
|
|
585
|
+
abortFn: null,
|
|
586
|
+
abortTimer: null,
|
|
587
|
+
cleanupTimer: null,
|
|
588
|
+
};
|
|
589
|
+
ccLiveStreams.set(tabId, state);
|
|
590
|
+
return state;
|
|
591
|
+
}
|
|
592
|
+
function _attachCcLiveStream(tabId, writer, endResponse) {
|
|
593
|
+
const state = _ensureCcLiveStream(tabId);
|
|
594
|
+
_clearCcLiveTimers(tabId);
|
|
595
|
+
state.writer = writer;
|
|
596
|
+
state.endResponse = endResponse;
|
|
597
|
+
return state;
|
|
598
|
+
}
|
|
599
|
+
function _detachCcLiveStream(tabId, writer) {
|
|
600
|
+
const state = _getCcLiveStream(tabId);
|
|
601
|
+
if (!state) return;
|
|
602
|
+
if (!writer || state.writer === writer) {
|
|
603
|
+
state.writer = null;
|
|
604
|
+
state.endResponse = null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function _scheduleCcLiveAbort(tabId) {
|
|
608
|
+
const state = _getCcLiveStream(tabId);
|
|
609
|
+
if (!state || state.donePayload) return;
|
|
610
|
+
_clearCcLiveTimers(tabId);
|
|
611
|
+
state.abortTimer = setTimeout(() => {
|
|
612
|
+
const live = _getCcLiveStream(tabId);
|
|
613
|
+
if (!live || live.donePayload || live.writer) return;
|
|
614
|
+
try { if (live.abortFn) live.abortFn(); } catch {}
|
|
615
|
+
}, CC_STREAM_REATTACH_GRACE_MS);
|
|
616
|
+
}
|
|
617
|
+
function _scheduleCcLiveCleanup(tabId, delayMs = CC_STREAM_DONE_RETENTION_MS) {
|
|
618
|
+
const state = _getCcLiveStream(tabId);
|
|
619
|
+
if (!state) return;
|
|
620
|
+
if (state.cleanupTimer) clearTimeout(state.cleanupTimer);
|
|
621
|
+
state.cleanupTimer = setTimeout(() => {
|
|
622
|
+
const live = _getCcLiveStream(tabId);
|
|
623
|
+
if (!live || live.writer) return;
|
|
624
|
+
_clearCcLiveStream(tabId);
|
|
625
|
+
}, delayMs);
|
|
626
|
+
}
|
|
551
627
|
function _ccTabIsInFlight(tabId) {
|
|
552
628
|
if (!ccInFlightTabs.has(tabId)) return false;
|
|
553
629
|
// Auto-release stale locks — if a request has been in-flight longer than CC_INFLIGHT_TIMEOUT_MS,
|
|
@@ -1215,12 +1291,111 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
1215
1291
|
return result;
|
|
1216
1292
|
}
|
|
1217
1293
|
|
|
1294
|
+
async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = 900000, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, onChunk, onToolUse } = {}) {
|
|
1295
|
+
if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
1296
|
+
if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
1297
|
+
const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
1298
|
+
const existing = resolveSession(store, sessionKey);
|
|
1299
|
+
let sessionId = existing ? existing.sessionId : null;
|
|
1300
|
+
|
|
1301
|
+
function buildPrompt({ includePreamble = true } = {}) {
|
|
1302
|
+
const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
1303
|
+
if (extraContext) parts.push(extraContext);
|
|
1304
|
+
parts.push(message);
|
|
1305
|
+
return parts.join('\n\n---\n\n');
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
let result;
|
|
1309
|
+
|
|
1310
|
+
if (sessionId && maxTurns > 1) {
|
|
1311
|
+
const p1 = llm.callLLMStreaming(buildPrompt({ includePreamble: false }), '', {
|
|
1312
|
+
timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
|
|
1313
|
+
onChunk,
|
|
1314
|
+
onToolUse,
|
|
1315
|
+
});
|
|
1316
|
+
if (onAbortReady) onAbortReady(p1.abort);
|
|
1317
|
+
result = await p1;
|
|
1318
|
+
llm.trackEngineUsage(label, result.usage);
|
|
1319
|
+
|
|
1320
|
+
if (result.text) {
|
|
1321
|
+
updateSession(store, sessionKey, result.sessionId || sessionId, true);
|
|
1322
|
+
return result;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const sessionStillValid = llm.isResumeSessionStillValid(result);
|
|
1326
|
+
if (sessionStillValid) {
|
|
1327
|
+
console.log(`[${label}] Resume call failed (code=${result.code}, empty=${!result.text}) but session is still valid — preserving session for retry`);
|
|
1328
|
+
updateSession(store, sessionKey, result.sessionId || sessionId, true);
|
|
1329
|
+
return result;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
console.log(`[${label}] Resume failed — session appears dead (code=${result.code}, empty=${!result.text}), retrying fresh...`);
|
|
1333
|
+
sessionId = null;
|
|
1334
|
+
if (store === 'cc') {
|
|
1335
|
+
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
1336
|
+
safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
|
|
1337
|
+
} else if (sessionKey) {
|
|
1338
|
+
docSessions.delete(sessionKey);
|
|
1339
|
+
schedulePersistDocSessions();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const freshPrompt = buildPrompt();
|
|
1344
|
+
const p2 = llm.callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
1345
|
+
timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
|
|
1346
|
+
onChunk,
|
|
1347
|
+
onToolUse,
|
|
1348
|
+
});
|
|
1349
|
+
if (onAbortReady) onAbortReady(p2.abort);
|
|
1350
|
+
result = await p2;
|
|
1351
|
+
llm.trackEngineUsage(label, result.usage);
|
|
1352
|
+
|
|
1353
|
+
if (result.text) {
|
|
1354
|
+
updateSession(store, sessionKey, result.sessionId, false);
|
|
1355
|
+
return result;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (maxTurns <= 1) return result;
|
|
1359
|
+
console.log(`[${label}] Fresh call also failed (code=${result.code}, empty=${!result.text}), retrying once more...`);
|
|
1360
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1361
|
+
const p3 = llm.callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
1362
|
+
timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
|
|
1363
|
+
onChunk,
|
|
1364
|
+
onToolUse,
|
|
1365
|
+
});
|
|
1366
|
+
if (onAbortReady) onAbortReady(p3.abort);
|
|
1367
|
+
result = await p3;
|
|
1368
|
+
llm.trackEngineUsage(label, result.usage);
|
|
1369
|
+
|
|
1370
|
+
if (result.text) {
|
|
1371
|
+
updateSession(store, sessionKey, result.sessionId, false);
|
|
1372
|
+
}
|
|
1373
|
+
return result;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1218
1376
|
// Lightweight content fingerprint — same algorithm used browser-side (no crypto needed)
|
|
1219
1377
|
function contentFingerprint(str) {
|
|
1220
1378
|
if (!str) return '';
|
|
1221
1379
|
return str.length + ':' + str.charCodeAt(0) + ':' + str.charCodeAt(str.length - 1);
|
|
1222
1380
|
}
|
|
1223
1381
|
|
|
1382
|
+
function _parseDocChatResultText(text) {
|
|
1383
|
+
const delimIdx = text.indexOf('---DOCUMENT---');
|
|
1384
|
+
if (delimIdx >= 0) {
|
|
1385
|
+
const answerPart = text.slice(0, delimIdx).trim();
|
|
1386
|
+
const { text: answer, actions } = parseCCActions(answerPart);
|
|
1387
|
+
let content = text.slice(delimIdx + '---DOCUMENT---'.length).trim();
|
|
1388
|
+
content = content.replace(/^```\w*\n?/, '').replace(/\n?```$/, '').trim();
|
|
1389
|
+
return { answer, content, actions };
|
|
1390
|
+
}
|
|
1391
|
+
const { text: stripped, actions } = parseCCActions(text);
|
|
1392
|
+
return { answer: stripped, content: null, actions };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function _docChatDisplayText(text) {
|
|
1396
|
+
return _parseDocChatResultText(text).answer;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1224
1399
|
// Doc-specific wrapper — adds document context, parses ---DOCUMENT---
|
|
1225
1400
|
async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady }) {
|
|
1226
1401
|
const sessionKey = filePath || title;
|
|
@@ -1273,18 +1448,54 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
1273
1448
|
return { answer: 'Failed to process request. Try again.', content: null, actions: [] };
|
|
1274
1449
|
}
|
|
1275
1450
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1451
|
+
return _parseDocChatResultText(result.text);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
async function ccDocCallStreaming({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady, onChunk, onToolUse }) {
|
|
1455
|
+
const sessionKey = filePath || title;
|
|
1456
|
+
const docSlice = document.slice(0, 20000);
|
|
1457
|
+
|
|
1458
|
+
if (freshSession && sessionKey) {
|
|
1459
|
+
docSessions.delete(sessionKey);
|
|
1284
1460
|
}
|
|
1285
1461
|
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1462
|
+
const docHash = require('crypto').createHash('md5').update(docSlice).digest('hex').slice(0, 8);
|
|
1463
|
+
const existing = freshSession ? null : resolveSession('doc', sessionKey);
|
|
1464
|
+
const docUnchanged = existing?.sessionId && existing._docHash === docHash;
|
|
1465
|
+
|
|
1466
|
+
let docContext;
|
|
1467
|
+
if (docUnchanged) {
|
|
1468
|
+
docContext = `## Document: ${title || 'Document'}${filePath ? ' (`' + filePath + '`)' : ''}${selection ? '\n**Selected text:**\n> ' + selection.slice(0, 1500) : ''}${canEdit ? '\nIf editing: respond with your explanation, then \`---DOCUMENT---\` on its own line, then the COMPLETE updated file.' : ''}`;
|
|
1469
|
+
} else {
|
|
1470
|
+
docContext = `## Document Context\n**${title || 'Document'}**${filePath ? ' (`' + filePath + '`)' : ''}${isJson ? ' (JSON)' : ''}\n${selection ? '\n**Selected text:**\n> ' + selection.slice(0, 1500) + '\n' : ''}\n\`\`\`\n${docSlice}\n\`\`\`\n${canEdit ? '\nIf editing: respond with your explanation, then \`---DOCUMENT---\` on its own line, then the COMPLETE updated file.' : '\n(Read-only — answer questions only.)'}`;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const result = await ccCallStreaming(message, {
|
|
1474
|
+
store: 'doc', sessionKey,
|
|
1475
|
+
extraContext: docContext, label: 'doc-chat',
|
|
1476
|
+
allowedTools: canEdit ? 'Read,Write,Edit,Glob,Grep' : 'Read,Glob,Grep',
|
|
1477
|
+
maxTurns: canEdit ? 25 : 10,
|
|
1478
|
+
skipStatePreamble: true,
|
|
1479
|
+
...(model ? { model } : {}),
|
|
1480
|
+
onAbortReady,
|
|
1481
|
+
onChunk: (text) => { if (onChunk) onChunk(_docChatDisplayText(text)); },
|
|
1482
|
+
onToolUse,
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
if (freshSession && sessionKey) {
|
|
1486
|
+
docSessions.delete(sessionKey);
|
|
1487
|
+
schedulePersistDocSessions();
|
|
1488
|
+
} else if (result.code === 0 && result.sessionId) {
|
|
1489
|
+
const session = resolveSession('doc', sessionKey);
|
|
1490
|
+
if (session) session._docHash = docHash;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
if (result.code !== 0 || !result.text) {
|
|
1494
|
+
console.error(`[doc-chat-stream] Failed: code=${result.code}, empty=${!result.text}, filePath=${filePath}, stderr=${(result.stderr || '').slice(0, 200)}`);
|
|
1495
|
+
return { answer: 'Failed to process request. Try again.', content: null, actions: [] };
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
return _parseDocChatResultText(result.text);
|
|
1288
1499
|
}
|
|
1289
1500
|
|
|
1290
1501
|
// -- POST helpers --
|
|
@@ -3353,7 +3564,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3353
3564
|
docChatInFlight.add(docKey);
|
|
3354
3565
|
// Kill LLM process + release guard if client disconnects (abort/navigation)
|
|
3355
3566
|
let _docAbort = null;
|
|
3356
|
-
|
|
3567
|
+
let _docDone = false;
|
|
3568
|
+
req.on('close', () => { if (!_docDone) { docChatInFlight.delete(docKey); if (_docAbort) _docAbort(); } });
|
|
3357
3569
|
|
|
3358
3570
|
try {
|
|
3359
3571
|
const canEdit = !!body.filePath;
|
|
@@ -3402,13 +3614,152 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3402
3614
|
|
|
3403
3615
|
safeWrite(fullPath, content);
|
|
3404
3616
|
|
|
3617
|
+
_docDone = true;
|
|
3405
3618
|
return jsonReply(res, 200, { ok: true, answer, edited: true, content, actions });
|
|
3406
3619
|
}
|
|
3620
|
+
_docDone = true;
|
|
3407
3621
|
return jsonReply(res, 200, { ok: true, answer: answer + '\n\n(Read-only — changes not saved)', edited: false, actions });
|
|
3408
|
-
} finally { _docAbort = null; docChatInFlight.delete(docKey); }
|
|
3622
|
+
} finally { _docAbort = null; _docDone = true; docChatInFlight.delete(docKey); }
|
|
3409
3623
|
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
3410
3624
|
}
|
|
3411
3625
|
|
|
3626
|
+
async function handleDocChatStream(req, res) {
|
|
3627
|
+
let docKey = null;
|
|
3628
|
+
let _docAbort = null;
|
|
3629
|
+
let _docStreamEnded = false;
|
|
3630
|
+
let _docHeartbeatTimer = null;
|
|
3631
|
+
const writeDocEvent = (payload) => {
|
|
3632
|
+
try {
|
|
3633
|
+
res.write('data: ' + JSON.stringify(payload) + '\n\n');
|
|
3634
|
+
return true;
|
|
3635
|
+
} catch {
|
|
3636
|
+
return false;
|
|
3637
|
+
}
|
|
3638
|
+
};
|
|
3639
|
+
const stopDocHeartbeat = () => {
|
|
3640
|
+
if (_docHeartbeatTimer) {
|
|
3641
|
+
clearInterval(_docHeartbeatTimer);
|
|
3642
|
+
_docHeartbeatTimer = null;
|
|
3643
|
+
}
|
|
3644
|
+
};
|
|
3645
|
+
try {
|
|
3646
|
+
const body = await readBody(req);
|
|
3647
|
+
if (!body.message) { res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'message required' })); return; }
|
|
3648
|
+
if (!body.document) { res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'document required' })); return; }
|
|
3649
|
+
|
|
3650
|
+
docKey = body.filePath || body.title || 'default';
|
|
3651
|
+
if (docChatInFlight.has(docKey)) {
|
|
3652
|
+
res.statusCode = 429;
|
|
3653
|
+
res.setHeader('Content-Type', 'application/json');
|
|
3654
|
+
res.end(JSON.stringify({ error: 'This document is already being processed — wait for the current response.' }));
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
docChatInFlight.add(docKey);
|
|
3658
|
+
|
|
3659
|
+
const canEdit = !!body.filePath;
|
|
3660
|
+
const isJson = body.filePath?.endsWith('.json');
|
|
3661
|
+
let currentContent = body.document;
|
|
3662
|
+
let fullPath = null;
|
|
3663
|
+
if (canEdit) {
|
|
3664
|
+
try { shared.sanitizePath(body.filePath, MINIONS_DIR); }
|
|
3665
|
+
catch { docChatInFlight.delete(docKey); res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'path must be under minions directory' })); return; }
|
|
3666
|
+
fullPath = path.resolve(MINIONS_DIR, body.filePath);
|
|
3667
|
+
const diskContent = safeRead(fullPath);
|
|
3668
|
+
if (diskContent !== null) {
|
|
3669
|
+
if (!(body.contentHash && contentFingerprint(diskContent) === body.contentHash)) {
|
|
3670
|
+
currentContent = diskContent;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
3676
|
+
writeDocEvent({ type: 'heartbeat' });
|
|
3677
|
+
_docHeartbeatTimer = setInterval(() => {
|
|
3678
|
+
if (_docStreamEnded) {
|
|
3679
|
+
stopDocHeartbeat();
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
if (!writeDocEvent({ type: 'heartbeat' })) stopDocHeartbeat();
|
|
3683
|
+
}, CC_STREAM_HEARTBEAT_MS);
|
|
3684
|
+
|
|
3685
|
+
req.on('close', () => {
|
|
3686
|
+
if (!_docStreamEnded) {
|
|
3687
|
+
stopDocHeartbeat();
|
|
3688
|
+
docChatInFlight.delete(docKey);
|
|
3689
|
+
if (_docAbort) _docAbort();
|
|
3690
|
+
}
|
|
3691
|
+
});
|
|
3692
|
+
|
|
3693
|
+
try {
|
|
3694
|
+
|
|
3695
|
+
const { answer, content, actions } = await ccDocCallStreaming({
|
|
3696
|
+
message: body.message, document: currentContent, title: body.title,
|
|
3697
|
+
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
3698
|
+
model: body.model || undefined,
|
|
3699
|
+
freshSession: !!body.freshSession,
|
|
3700
|
+
onAbortReady: (abort) => { _docAbort = abort; },
|
|
3701
|
+
onChunk: (text) => { writeDocEvent({ type: 'chunk', text }); },
|
|
3702
|
+
onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
|
|
3703
|
+
});
|
|
3704
|
+
|
|
3705
|
+
if (!content) {
|
|
3706
|
+
writeDocEvent({ type: 'done', text: answer, edited: false, actions });
|
|
3707
|
+
_docStreamEnded = true;
|
|
3708
|
+
res.end();
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
if (isJson) {
|
|
3713
|
+
try { JSON.parse(content); } catch (e) {
|
|
3714
|
+
writeDocEvent({ type: 'done', text: answer + '\n\n(JSON invalid — not saved: ' + e.message + ')', edited: false, actions });
|
|
3715
|
+
_docStreamEnded = true;
|
|
3716
|
+
res.end();
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
if (canEdit && fullPath) {
|
|
3722
|
+
if (body.filePath && /^meetings\//.test(body.filePath) && isJson) {
|
|
3723
|
+
try {
|
|
3724
|
+
const mtg = safeJson(fullPath);
|
|
3725
|
+
if (mtg && (mtg.status === 'completed' || mtg.status === 'archived')) {
|
|
3726
|
+
writeDocEvent({ type: 'done', text: answer, edited: false, actions });
|
|
3727
|
+
_docStreamEnded = true;
|
|
3728
|
+
res.end();
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
} catch { /* proceed with write if can't read */ }
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
safeWrite(fullPath, content);
|
|
3735
|
+
writeDocEvent({ type: 'done', text: answer, edited: true, content, actions });
|
|
3736
|
+
_docStreamEnded = true;
|
|
3737
|
+
res.end();
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
writeDocEvent({ type: 'done', text: answer + '\n\n(Read-only — changes not saved)', edited: false, actions });
|
|
3742
|
+
_docStreamEnded = true;
|
|
3743
|
+
res.end();
|
|
3744
|
+
} finally {
|
|
3745
|
+
stopDocHeartbeat();
|
|
3746
|
+
docChatInFlight.delete(docKey);
|
|
3747
|
+
}
|
|
3748
|
+
} catch (e) {
|
|
3749
|
+
stopDocHeartbeat();
|
|
3750
|
+
if (docKey) docChatInFlight.delete(docKey);
|
|
3751
|
+
if (!res.headersSent) {
|
|
3752
|
+
res.statusCode = e.statusCode || 500;
|
|
3753
|
+
res.setHeader('Content-Type', 'application/json');
|
|
3754
|
+
try { res.end(JSON.stringify({ error: e.message })); } catch {}
|
|
3755
|
+
} else {
|
|
3756
|
+
writeDocEvent({ type: 'error', error: e.message });
|
|
3757
|
+
_docStreamEnded = true;
|
|
3758
|
+
try { res.end(); } catch {}
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3412
3763
|
async function handleInboxPersist(req, res) {
|
|
3413
3764
|
try {
|
|
3414
3765
|
const body = await readBody(req);
|
|
@@ -3900,10 +4251,31 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3900
4251
|
async function handleCommandCenterNewSession(req, res) {
|
|
3901
4252
|
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
3902
4253
|
ccInFlightTabs.clear(); // Reset all in-flight guards
|
|
4254
|
+
for (const [tabId, live] of ccLiveStreams.entries()) {
|
|
4255
|
+
try { if (live.abortFn) live.abortFn(); } catch {}
|
|
4256
|
+
_clearCcLiveStream(tabId);
|
|
4257
|
+
}
|
|
3903
4258
|
safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
|
|
3904
4259
|
return jsonReply(res, 200, { ok: true });
|
|
3905
4260
|
}
|
|
3906
4261
|
|
|
4262
|
+
async function handleCommandCenterAbort(req, res) {
|
|
4263
|
+
try {
|
|
4264
|
+
const body = await readBody(req);
|
|
4265
|
+
const tabId = body.tabId || 'default';
|
|
4266
|
+
const live = _getCcLiveStream(tabId);
|
|
4267
|
+
if (live?.abortFn) {
|
|
4268
|
+
try { live.abortFn(); } catch {}
|
|
4269
|
+
} else {
|
|
4270
|
+
const abort = ccInFlightAborts.get(tabId);
|
|
4271
|
+
if (abort) { try { abort(); } catch {} }
|
|
4272
|
+
}
|
|
4273
|
+
_clearCcLiveStream(tabId);
|
|
4274
|
+
_releaseCCTab(tabId);
|
|
4275
|
+
return jsonReply(res, 200, { ok: true });
|
|
4276
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
4277
|
+
}
|
|
4278
|
+
|
|
3907
4279
|
async function handleCCSessionsList(req, res) {
|
|
3908
4280
|
const sessions = _readCcTabSessions();
|
|
3909
4281
|
return jsonReply(res, 200, { sessions });
|
|
@@ -4037,8 +4409,51 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4037
4409
|
};
|
|
4038
4410
|
try {
|
|
4039
4411
|
const body = await readBody(req);
|
|
4040
|
-
if (!body.message) { res.statusCode = 400; res.end('message required'); return; }
|
|
4412
|
+
if (!body.message && !body.reconnect) { res.statusCode = 400; res.end('message required'); return; }
|
|
4041
4413
|
tabId = body.tabId || 'default';
|
|
4414
|
+
if (body.reconnect) {
|
|
4415
|
+
const live = _getCcLiveStream(tabId);
|
|
4416
|
+
if (!live) { res.statusCode = 409; res.end('No live command-center response to reconnect'); return; }
|
|
4417
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
4418
|
+
writeCcEvent({ type: 'heartbeat' });
|
|
4419
|
+
_ccHeartbeatTimer = setInterval(() => {
|
|
4420
|
+
if (_ccStreamEnded) {
|
|
4421
|
+
stopCcHeartbeat();
|
|
4422
|
+
return;
|
|
4423
|
+
}
|
|
4424
|
+
if (!writeCcEvent({ type: 'heartbeat' })) stopCcHeartbeat();
|
|
4425
|
+
}, CC_STREAM_HEARTBEAT_MS);
|
|
4426
|
+
let reconnectDone;
|
|
4427
|
+
const reconnectDonePromise = new Promise(resolve => { reconnectDone = resolve; });
|
|
4428
|
+
_attachCcLiveStream(tabId, writeCcEvent, () => {
|
|
4429
|
+
if (_ccStreamEnded) return;
|
|
4430
|
+
_ccStreamEnded = true;
|
|
4431
|
+
stopCcHeartbeat();
|
|
4432
|
+
try { res.end(); } catch {}
|
|
4433
|
+
reconnectDone();
|
|
4434
|
+
});
|
|
4435
|
+
req.on('close', () => {
|
|
4436
|
+
if (_ccStreamEnded) return;
|
|
4437
|
+
stopCcHeartbeat();
|
|
4438
|
+
_detachCcLiveStream(tabId, writeCcEvent);
|
|
4439
|
+
_scheduleCcLiveAbort(tabId);
|
|
4440
|
+
reconnectDone();
|
|
4441
|
+
});
|
|
4442
|
+
for (const tool of live.tools || []) {
|
|
4443
|
+
writeCcEvent({ type: 'tool', name: tool.name, input: _lightToolInput(tool.input) });
|
|
4444
|
+
}
|
|
4445
|
+
if (live.text) writeCcEvent({ type: 'chunk', text: live.text });
|
|
4446
|
+
if (live.donePayload) {
|
|
4447
|
+
writeCcEvent(live.donePayload);
|
|
4448
|
+
_ccStreamEnded = true;
|
|
4449
|
+
stopCcHeartbeat();
|
|
4450
|
+
try { res.end(); } catch {}
|
|
4451
|
+
_scheduleCcLiveCleanup(tabId);
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4454
|
+
await reconnectDonePromise;
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4042
4457
|
if (_ccTabIsInFlight(tabId)) {
|
|
4043
4458
|
// Previous request still in-flight — abort its LLM (handles keep-alive abort where close event didn't fire)
|
|
4044
4459
|
const prevAbort = ccInFlightAborts.get(tabId);
|
|
@@ -4049,6 +4464,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4049
4464
|
}
|
|
4050
4465
|
}
|
|
4051
4466
|
ccInFlightTabs.set(tabId, Date.now());
|
|
4467
|
+
_clearCcLiveStream(tabId);
|
|
4468
|
+
const liveState = _attachCcLiveStream(tabId, writeCcEvent, () => {
|
|
4469
|
+
if (_ccStreamEnded) return;
|
|
4470
|
+
_ccStreamEnded = true;
|
|
4471
|
+
stopCcHeartbeat();
|
|
4472
|
+
try { res.end(); } catch {}
|
|
4473
|
+
});
|
|
4052
4474
|
|
|
4053
4475
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
4054
4476
|
writeCcEvent({ type: 'heartbeat' }); // flush headers quickly and keep intermediaries from idling out
|
|
@@ -4060,16 +4482,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4060
4482
|
if (!writeCcEvent({ type: 'heartbeat' })) stopCcHeartbeat();
|
|
4061
4483
|
}, CC_STREAM_HEARTBEAT_MS);
|
|
4062
4484
|
// Kill LLM process immediately if client disconnects mid-stream.
|
|
4063
|
-
//
|
|
4064
|
-
// without the guard, this close event (which fires after res.end) could wipe a new request's lock.
|
|
4485
|
+
// Keep the LLM alive briefly after disconnect so the UI can reattach to the same in-flight turn.
|
|
4065
4486
|
req.on('close', () => {
|
|
4066
4487
|
if (!_ccStreamEnded) {
|
|
4067
4488
|
stopCcHeartbeat();
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
console.log(`[CC-stream] Client disconnected for tab ${tabId} — aborting LLM`);
|
|
4071
|
-
_ccStreamAbort();
|
|
4072
|
-
}
|
|
4489
|
+
_detachCcLiveStream(tabId, writeCcEvent);
|
|
4490
|
+
_scheduleCcLiveAbort(tabId);
|
|
4073
4491
|
}
|
|
4074
4492
|
});
|
|
4075
4493
|
|
|
@@ -4106,14 +4524,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4106
4524
|
onChunk: (text) => {
|
|
4107
4525
|
const actIdx = findCCActionsDelimiter(text);
|
|
4108
4526
|
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
4109
|
-
|
|
4527
|
+
liveState.text = display;
|
|
4528
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4110
4529
|
},
|
|
4111
4530
|
onToolUse: (name, input) => {
|
|
4112
4531
|
toolUses.push({ name, input: input || {} });
|
|
4113
|
-
|
|
4532
|
+
liveState.tools.push({ name, input: input || {} });
|
|
4533
|
+
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4114
4534
|
}
|
|
4115
4535
|
});
|
|
4116
4536
|
_ccStreamAbort = llmPromise.abort;
|
|
4537
|
+
liveState.abortFn = _ccStreamAbort;
|
|
4117
4538
|
ccInFlightAborts.set(tabId, _ccStreamAbort);
|
|
4118
4539
|
const result = await llmPromise;
|
|
4119
4540
|
trackUsage('command-center', result.usage);
|
|
@@ -4127,21 +4548,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4127
4548
|
toolUses = []; // discard stale metadata from the failed resume attempt
|
|
4128
4549
|
const retryPromise = callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
4129
4550
|
timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4551
|
+
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
4552
|
+
effort: streamEffort, direct: true,
|
|
4553
|
+
onChunk: (text) => {
|
|
4554
|
+
const actIdx = findCCActionsDelimiter(text);
|
|
4555
|
+
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
4556
|
+
liveState.text = display;
|
|
4557
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4558
|
+
},
|
|
4559
|
+
onToolUse: (name, input) => {
|
|
4560
|
+
toolUses.push({ name, input: input || {} });
|
|
4561
|
+
liveState.tools.push({ name, input: input || {} });
|
|
4562
|
+
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4563
|
+
}
|
|
4564
|
+
});
|
|
4565
|
+
_ccStreamAbort = retryPromise.abort;
|
|
4566
|
+
liveState.abortFn = _ccStreamAbort;
|
|
4567
|
+
ccInFlightAborts.set(tabId, _ccStreamAbort);
|
|
4568
|
+
const retryResult = await retryPromise;
|
|
4145
4569
|
trackUsage('command-center', retryResult.usage);
|
|
4146
4570
|
if (retryResult.text) {
|
|
4147
4571
|
// Fresh session succeeded — use retryResult from here
|
|
@@ -4154,8 +4578,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4154
4578
|
const stderrTail = (result.stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
|
4155
4579
|
console.error(`[CC-stream] Failed: code=${result.code}, stderr=${(result.stderr || '').slice(0, 500)}, stdout_tail=${(result.raw || '').slice(-500)}`);
|
|
4156
4580
|
const retryHint = 'Send your message again to retry.';
|
|
4157
|
-
|
|
4158
|
-
|
|
4581
|
+
liveState.donePayload = { type: 'done', text: `I had trouble processing that ${debugInfo}. ${stderrTail ? 'Detail: ' + stderrTail : ''}\n\n${retryHint}`, actions: [], sessionId: null };
|
|
4582
|
+
if (liveState.writer) liveState.writer(liveState.donePayload);
|
|
4583
|
+
if (liveState.endResponse) liveState.endResponse();
|
|
4584
|
+
_scheduleCcLiveCleanup(tabId);
|
|
4159
4585
|
return;
|
|
4160
4586
|
}
|
|
4161
4587
|
|
|
@@ -4197,7 +4623,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4197
4623
|
}
|
|
4198
4624
|
const donePayload = { type: 'done', text: displayText, actions, actionResults, sessionId: responseSessionId, newSession: !wasResume };
|
|
4199
4625
|
if (sessionReset) donePayload.sessionReset = true;
|
|
4200
|
-
|
|
4626
|
+
liveState.donePayload = donePayload;
|
|
4627
|
+
if (liveState.writer) liveState.writer(donePayload);
|
|
4201
4628
|
|
|
4202
4629
|
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
4203
4630
|
const _streamTabId = body.tabId || 'default';
|
|
@@ -4205,7 +4632,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4205
4632
|
teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
|
|
4206
4633
|
}
|
|
4207
4634
|
|
|
4208
|
-
|
|
4635
|
+
if (liveState.endResponse) liveState.endResponse();
|
|
4636
|
+
_scheduleCcLiveCleanup(tabId);
|
|
4209
4637
|
} finally {
|
|
4210
4638
|
stopCcHeartbeat();
|
|
4211
4639
|
_releaseCCTab(tabId);
|
|
@@ -5077,6 +5505,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5077
5505
|
|
|
5078
5506
|
// Doc chat
|
|
5079
5507
|
{ method: 'POST', path: '/api/doc-chat', desc: 'Minions-aware doc Q&A + editing via CC session', params: 'message, document, title?, filePath?, selection?, contentHash?', handler: handleDocChat },
|
|
5508
|
+
{ method: 'POST', path: '/api/doc-chat/stream', desc: 'Streaming doc chat — SSE with text chunks and tool progress', params: 'message, document, title?, filePath?, selection?, contentHash?', handler: handleDocChatStream },
|
|
5080
5509
|
|
|
5081
5510
|
// Inbox
|
|
5082
5511
|
{ method: 'POST', path: '/api/inbox/persist', desc: 'Promote an inbox item to team notes', params: 'name', handler: handleInboxPersist },
|
|
@@ -5099,6 +5528,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5099
5528
|
|
|
5100
5529
|
// Command Center
|
|
5101
5530
|
{ method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
|
|
5531
|
+
{ method: 'POST', path: '/api/command-center/abort', desc: 'Abort an in-flight CC request for a tab', params: 'tabId?', handler: handleCommandCenterAbort },
|
|
5102
5532
|
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
|
|
5103
5533
|
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
|
|
5104
5534
|
{ method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1580",
|
|
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"
|