@yemi33/minions 0.1.2188 → 0.1.2190
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/dashboard/js/command-center.js +47 -41
- package/dashboard/js/modal-qa.js +31 -29
- package/dashboard/js/render-utils.js +138 -0
- package/dashboard/js/utils.js +58 -25
- package/dashboard/styles.css +29 -0
- package/dashboard.js +35 -18
- package/engine/llm.js +20 -3
- package/engine.js +11 -0
- package/package.json +1 -1
|
@@ -549,12 +549,12 @@ function ccSwitchTab(id) {
|
|
|
549
549
|
var phases = [[0,'Thinking...'],[3000,'Reading minions context...'],[8000,'Analyzing...'],[15000,'Using tools to dig deeper...'],[30000,'Still working (multi-turn)...'],[60000,'Deep research in progress...']];
|
|
550
550
|
function _restoreStreamHtml() {
|
|
551
551
|
var html = '';
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
var text = tab.
|
|
557
|
-
|
|
552
|
+
// Restore the same interleaved text↔tool layout the live stream renders,
|
|
553
|
+
// so chips and partial text keep their chronological order (and chip
|
|
554
|
+
// status styling) after a tab switch / reload. renderToolChip is reached
|
|
555
|
+
// via ccSegmentsRender.
|
|
556
|
+
var text = ccSegmentsText(tab._segments || []);
|
|
557
|
+
html += ccSegmentsRender(tab._segments || [], { keyPrefix: tab.id });
|
|
558
558
|
var ms = Date.now() - restoreStart;
|
|
559
559
|
var label = 'Thinking...';
|
|
560
560
|
for (var pi = phases.length - 1; pi >= 0; pi--) { if (ms >= phases[pi][0]) { label = phases[pi][1]; break; } }
|
|
@@ -633,7 +633,7 @@ function ccRenderTabBar() {
|
|
|
633
633
|
var isActive = t.id === _ccActiveTabId;
|
|
634
634
|
// draggable="true" + DnD handlers enable click-and-drag reorder of tabs.
|
|
635
635
|
// The handlers below splice _ccTabs in place (preserving per-tab in-flight
|
|
636
|
-
// state: _sending, _queue, _abortController,
|
|
636
|
+
// state: _sending, _queue, _abortController, _segments)
|
|
637
637
|
// and persist via ccSaveState. The close X / new-tab + opt out of drag
|
|
638
638
|
// with draggable="false" + ondragstart preventDefault so the affordances
|
|
639
639
|
// don't accidentally start a drag.
|
|
@@ -709,7 +709,7 @@ function ccTabDrop(ev, targetId) {
|
|
|
709
709
|
var toIdx = _ccTabs.findIndex(function(t) { return t.id === targetId; });
|
|
710
710
|
if (fromIdx === -1 || toIdx === -1) return;
|
|
711
711
|
// Splice the existing tab reference so per-tab in-flight state survives
|
|
712
|
-
// (_sending, _queue, _abortController,
|
|
712
|
+
// (_sending, _queue, _abortController, _segments,
|
|
713
713
|
// _retryRequests, _sendStartedAt, etc.). Do NOT clone/replace the object.
|
|
714
714
|
var moved = _ccTabs.splice(fromIdx, 1)[0];
|
|
715
715
|
_ccTabs.splice(toIdx, 0, moved);
|
|
@@ -979,9 +979,12 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
979
979
|
|
|
980
980
|
// Streaming state — declared before try so updateStreamDiv works during fetch
|
|
981
981
|
// Also saved on tab for restore when switching back
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
982
|
+
// Ordered text↔tool segments for this turn (replaces the old flat
|
|
983
|
+
// streamedText + toolsUsed buckets). Rendered in arrival order so prose and
|
|
984
|
+
// tool activity stay interleaved instead of welded into one blob. See
|
|
985
|
+
// render-utils.js ccSegments* helpers.
|
|
986
|
+
var segments = [];
|
|
987
|
+
if (activeTab) activeTab._segments = segments;
|
|
985
988
|
|
|
986
989
|
// Get active tab's sessionId to send with request
|
|
987
990
|
var tabSessionId = activeTab ? activeTab.sessionId : null;
|
|
@@ -1020,16 +1023,15 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1020
1023
|
if (re) { streamDiv = re; re.removeAttribute('id'); } else return;
|
|
1021
1024
|
}
|
|
1022
1025
|
var html = '';
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1026
|
+
// ccSegmentsRender interleaves tool chips and text blocks in arrival order
|
|
1027
|
+
// (delegates each chip to renderToolChip). keyPrefix=tab id keeps any
|
|
1028
|
+
// expanded tool group open across the per-frame re-render.
|
|
1029
|
+
var bodyHtml = ccSegmentsRender(segments, { keyPrefix: activeTabId });
|
|
1030
|
+
if (bodyHtml) html += bodyHtml;
|
|
1029
1031
|
if (streamStatusNote) {
|
|
1030
1032
|
html += '<div style="margin-top:6px;font-size:var(--text-sm);color:var(--muted)">' + escHtml(streamStatusNote) + '</div>';
|
|
1031
1033
|
}
|
|
1032
|
-
html += '<div style="margin-top:' + (
|
|
1034
|
+
html += '<div style="margin-top:' + (bodyHtml ? '6px' : '0') + '">' + _getThinkingHtml() + '</div>';
|
|
1033
1035
|
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() and renderToolChip() escape streamed text/tool fields before assembling live stream HTML
|
|
1034
1036
|
streamDiv.innerHTML = html;
|
|
1035
1037
|
// Re-append queue indicators so they stay below the streaming content
|
|
@@ -1098,24 +1100,22 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1098
1100
|
|
|
1099
1101
|
async function _handleEvent(evt) {
|
|
1100
1102
|
if (evt.type === 'chunk') {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
+
// evt.segmentId (from engine/llm.js) marks distinct assistant text
|
|
1104
|
+
// blocks; ccSegmentsApplyChunk falls back to a heuristic when it's
|
|
1105
|
+
// absent (Copilot pool path / live replay).
|
|
1106
|
+
ccSegmentsApplyChunk(segments, evt.text || '', evt.segmentId);
|
|
1107
|
+
if (activeTab) activeTab._segments = segments;
|
|
1103
1108
|
updateStreamDiv();
|
|
1104
1109
|
} else if (evt.type === 'heartbeat') {
|
|
1105
1110
|
return;
|
|
1106
1111
|
} else if (evt.type === 'tool') {
|
|
1107
|
-
|
|
1108
|
-
if (activeTab) activeTab.
|
|
1112
|
+
ccSegmentsApplyTool(segments, { name: evt.name, input: evt.input || {}, id: evt.id || null });
|
|
1113
|
+
if (activeTab) activeTab._segments = segments;
|
|
1109
1114
|
updateStreamDiv();
|
|
1110
1115
|
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
1111
1116
|
} else if (evt.type === 'tool-update') {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
toolsUsed[ti].status = evt.status;
|
|
1115
|
-
break;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
|
1117
|
+
ccSegmentsApplyToolUpdate(segments, evt.id, evt.status);
|
|
1118
|
+
if (activeTab) activeTab._segments = segments;
|
|
1119
1119
|
updateStreamDiv();
|
|
1120
1120
|
} else if (evt.type === 'done') {
|
|
1121
1121
|
terminalEventSeen = true;
|
|
@@ -1131,10 +1131,13 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1131
1131
|
}
|
|
1132
1132
|
addMsg('system', '<div style="text-align:center;padding:6px 12px;font-size:var(--text-base);color:var(--muted);background:var(--surface2);border-radius:6px;margin:4px 0">' + resetText + '</div>', false, activeTabId);
|
|
1133
1133
|
}
|
|
1134
|
-
|
|
1134
|
+
// Reconcile streamed segments with the authoritative terminal text
|
|
1135
|
+
// (Claude's `result` — the last assistant message), preserving earlier
|
|
1136
|
+
// interleaved text/tool segments, then render the whole turn in order.
|
|
1137
|
+
ccSegmentsFinalize(segments, evt.text || '');
|
|
1135
1138
|
if (evt.actions && evt.actions.length > 0) _tagServerExecuted(evt.actions, evt.actionResults);
|
|
1136
|
-
var rendered = renderMd(finalText || streamedText || '');
|
|
1137
1139
|
var assistantMessageId = _ccNewMessageId('cc-turn');
|
|
1140
|
+
var rendered = ccSegmentsRender(segments, { keyPrefix: assistantMessageId });
|
|
1138
1141
|
addMsg('assistant', rendered + _ccElapsedFooter('{seconds}s'), false, { messageId: assistantMessageId });
|
|
1139
1142
|
// Surface each server-executed action as a standalone chip OUTSIDE the
|
|
1140
1143
|
// assistant bubble — matches the pattern used by ccExecuteAction for
|
|
@@ -1267,8 +1270,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1267
1270
|
_cleanupStreamDiv();
|
|
1268
1271
|
var streamEndedHint = '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">The response stream ended before completion. Retry to resend the interrupted message.</div>';
|
|
1269
1272
|
var streamEndedRetry = _ccStoreRetryRequest(activeTab, activeTabId, message);
|
|
1270
|
-
|
|
1271
|
-
|
|
1273
|
+
var streamEndedPartial = ccSegmentsRender(segments, { keyPrefix: activeTabId });
|
|
1274
|
+
if (streamEndedPartial) {
|
|
1275
|
+
addMsg('assistant', streamEndedPartial + _ccElapsedFooter('Stream interrupted after {seconds}s') + _ccRetryControls(streamEndedRetry, streamEndedHint, false), false, { retryId: streamEndedRetry.id });
|
|
1272
1276
|
} else {
|
|
1273
1277
|
addMsg('assistant', '<span style="color:var(--red)">The response stream ended before completion.</span>' + _ccRetryControls(streamEndedRetry, streamEndedHint, false), false, { retryId: streamEndedRetry.id });
|
|
1274
1278
|
}
|
|
@@ -1281,14 +1285,14 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1281
1285
|
? '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">Dashboard restarted while this response was streaming. Restart Minions to reconnect to the new instance.</div>'
|
|
1282
1286
|
: '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">The request stream was interrupted, but the dashboard is still reachable. Retry or start a new session.</div>';
|
|
1283
1287
|
var reconnectRetry = _ccStoreRetryRequest(activeTab, activeTabId, message);
|
|
1284
|
-
|
|
1288
|
+
var reconnectPartial = ccSegmentsRender(segments, { keyPrefix: activeTabId });
|
|
1289
|
+
addMsg('assistant', (reconnectPartial ? reconnectPartial + _ccElapsedFooter('Stream interrupted after {seconds}s') : '') +
|
|
1285
1290
|
_ccRetryControls(reconnectRetry, reconnectHint, reconnectHealth.restarted), false, { retryId: reconnectRetry.id });
|
|
1286
1291
|
break;
|
|
1287
1292
|
}
|
|
1288
1293
|
reconnectAttempts++;
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
if (activeTab) { activeTab._streamedText = ''; activeTab._toolsUsed = []; }
|
|
1294
|
+
segments = [];
|
|
1295
|
+
if (activeTab) activeTab._segments = segments;
|
|
1292
1296
|
streamStatusNote = 'Connection interrupted — reattaching to the live response...';
|
|
1293
1297
|
updateStreamDiv();
|
|
1294
1298
|
await new Promise(function(r) { setTimeout(r, 1000 * reconnectAttempts); });
|
|
@@ -1297,8 +1301,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1297
1301
|
_cleanupStreamDiv();
|
|
1298
1302
|
if (activeTab && activeTab._userAborted) {
|
|
1299
1303
|
_wasAborted = true;
|
|
1300
|
-
|
|
1301
|
-
|
|
1304
|
+
var abortedPartial = ccSegmentsRender(segments, { keyPrefix: activeTabId });
|
|
1305
|
+
if (abortedPartial) {
|
|
1306
|
+
addMsg('assistant', abortedPartial + _ccElapsedFooter('Stopped after {seconds}s'));
|
|
1302
1307
|
} else {
|
|
1303
1308
|
addMsg('assistant', '<span style="color:var(--red);font-size:var(--text-base)">Stopped</span>');
|
|
1304
1309
|
}
|
|
@@ -1332,12 +1337,13 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
1332
1337
|
} else {
|
|
1333
1338
|
errorRendered = '<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>';
|
|
1334
1339
|
}
|
|
1335
|
-
|
|
1340
|
+
var errorPartial = ccSegmentsRender(segments, { keyPrefix: activeTabId });
|
|
1341
|
+
addMsg('assistant', (errorPartial ? errorPartial + _ccElapsedFooter('Stream interrupted after {seconds}s') : '') +
|
|
1336
1342
|
errorRendered +
|
|
1337
1343
|
_ccRetryControls(errorRetry, connectionHint, isNetworkError && (!dashboardHealth.reachable || dashboardHealth.restarted)), false, { retryId: errorRetry.id });
|
|
1338
1344
|
}
|
|
1339
1345
|
} finally {
|
|
1340
|
-
if (activeTab) { activeTab._sending = false; activeTab._abortController = null; activeTab._429retries = 0; delete activeTab.
|
|
1346
|
+
if (activeTab) { activeTab._sending = false; activeTab._abortController = null; activeTab._429retries = 0; delete activeTab._segments; delete activeTab._sendStartedAt; delete activeTab._userAborted; }
|
|
1341
1347
|
_ccSending = (_ccTabs.some(function(t) { return t._sending; }));
|
|
1342
1348
|
// Mark tab unread if response completed on a background tab or while drawer is closed
|
|
1343
1349
|
if (activeTab && !_wasAborted && (activeTab.id !== _ccActiveTabId || !_ccOpen)) activeTab._unread = true;
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -378,23 +378,16 @@ function _qaBuildActionFeedbackHtml(actionFeedback) {
|
|
|
378
378
|
}).join('');
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
function _qaBuildLiveProgressHtml(loadingId, label, elapsedSeconds,
|
|
381
|
+
function _qaBuildLiveProgressHtml(loadingId, label, elapsedSeconds, segments, queueCount) {
|
|
382
382
|
const qaQueueBadge = queueCount > 0 ? ' <span style="font-size:var(--text-xs);color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + queueCount + ' queued</span>' : '';
|
|
383
|
-
// Wrap in a column-flex container so chain-of-thought (tool
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// "Thinking..."
|
|
383
|
+
// Wrap in a column-flex container so the interleaved chain-of-thought (tool
|
|
384
|
+
// calls + partial text in arrival order, via ccSegmentsRender) stacks on top
|
|
385
|
+
// and the progress block sits at the bottom. Overrides the parent
|
|
386
|
+
// .modal-qa-loading row-flex (right for the simple "Thinking..." state but
|
|
387
|
+
// wrong once tools/streamed text appear).
|
|
387
388
|
let html = '<div style="display:flex;flex-direction:column;align-items:stretch;gap:6px;width:100%">';
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
toolsUsed.forEach(function(t) {
|
|
391
|
-
const name = typeof t === 'string' ? t : t.name;
|
|
392
|
-
const input = typeof t === 'string' ? {} : (t.input || {});
|
|
393
|
-
html += '<div style="color:var(--muted);font-size:var(--text-sm);font-family:monospace;display:flex;align-items:flex-start;gap:6px"><span style="flex-shrink:0">●</span><span style="word-break:break-all">' + formatToolSummary(name, input) + '</span></div>';
|
|
394
|
-
});
|
|
395
|
-
html += '</div>';
|
|
396
|
-
}
|
|
397
|
-
if (streamedText) html += '<div>' + renderMd(streamedText) + '</div>';
|
|
389
|
+
const bodyHtml = ccSegmentsRender(segments || [], { keyPrefix: loadingId });
|
|
390
|
+
if (bodyHtml) html += bodyHtml;
|
|
398
391
|
html += '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
|
|
399
392
|
'<span class="dot-pulse"><span></span><span></span><span></span></span>' +
|
|
400
393
|
'<span id="' + loadingId + '-text">' + escHtml(label) + '</span>' +
|
|
@@ -629,8 +622,11 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
629
622
|
const qaPhases = isPlanEdit
|
|
630
623
|
? [[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...']]
|
|
631
624
|
: [[0,'Thinking...'],[3000,'Reading document...'],[8000,'Analyzing...'],[20000,'Still working...'],[60000,'Taking a while...']];
|
|
632
|
-
|
|
633
|
-
|
|
625
|
+
// Ordered text↔tool segments for the live progress view (replaces the old
|
|
626
|
+
// flat streamedText + toolsUsed buckets). Rendered interleaved via
|
|
627
|
+
// ccSegmentsRender; see render-utils.js. The final answer bubble still
|
|
628
|
+
// collapses to the authoritative evt.text (doc-chat returns one answer).
|
|
629
|
+
let segments = [];
|
|
634
630
|
let _qaStreamStalled = false;
|
|
635
631
|
let _qaStallTimer = null;
|
|
636
632
|
function _clearQaStreamWatchdog() {
|
|
@@ -666,8 +662,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
666
662
|
loadingId,
|
|
667
663
|
_qaProgressLabel(elapsed),
|
|
668
664
|
Math.floor(elapsed / 1000),
|
|
669
|
-
|
|
670
|
-
toolsUsed,
|
|
665
|
+
segments,
|
|
671
666
|
runtime.queue.length
|
|
672
667
|
);
|
|
673
668
|
});
|
|
@@ -730,13 +725,19 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
730
725
|
}
|
|
731
726
|
if (evt.type === 'chunk') {
|
|
732
727
|
_resetQaStreamWatchdog();
|
|
733
|
-
|
|
728
|
+
ccSegmentsApplyChunk(segments, evt.text || '', evt.segmentId);
|
|
734
729
|
_qaRenderProgress(true);
|
|
735
730
|
return;
|
|
736
731
|
}
|
|
737
732
|
if (evt.type === 'tool') {
|
|
738
733
|
_resetQaStreamWatchdog();
|
|
739
|
-
|
|
734
|
+
ccSegmentsApplyTool(segments, { name: evt.name, input: evt.input || {}, id: evt.id || null });
|
|
735
|
+
_qaRenderProgress(true);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (evt.type === 'tool-update') {
|
|
739
|
+
_resetQaStreamWatchdog();
|
|
740
|
+
ccSegmentsApplyToolUpdate(segments, evt.id, evt.status);
|
|
740
741
|
_qaRenderProgress(true);
|
|
741
742
|
return;
|
|
742
743
|
}
|
|
@@ -760,7 +761,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
760
761
|
const suffix = evt.edited ? '\n\n\u2713 Document saved.' : '';
|
|
761
762
|
// Fall back to the live-streamed text when the backend produced no final
|
|
762
763
|
// answer \u2014 covers the "stream had visible chunks then returned empty" case.
|
|
763
|
-
const finalText = (evt.text && evt.text.trim()) ? evt.text : (
|
|
764
|
+
const finalText = (evt.text && evt.text.trim()) ? evt.text : ccSegmentsText(segments);
|
|
764
765
|
let bodyText = finalText + suffix;
|
|
765
766
|
if (evt.partial && evt.warning) {
|
|
766
767
|
bodyText += '\n\n_' + evt.warning + '_';
|
|
@@ -872,23 +873,24 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
872
873
|
_clearQaStreamWatchdog();
|
|
873
874
|
const qaElapsedExc = Math.round((Date.now() - qaStartTime) / 1000);
|
|
874
875
|
const stallMessage = 'Doc chat stalled with no tool or text progress for 6 minutes.';
|
|
876
|
+
const qaPartialText = ccSegmentsText(segments);
|
|
875
877
|
const messageHtml = _qaStreamStalled
|
|
876
|
-
? (
|
|
877
|
-
? _qaBuildAssistantHtml(
|
|
878
|
+
? (qaPartialText
|
|
879
|
+
? _qaBuildAssistantHtml(qaPartialText + '\n\nError: ' + stallMessage, { borderColor: 'var(--red)', elapsed: qaElapsedExc })
|
|
878
880
|
: _qaBuildAssistantHtml('Error: ' + stallMessage, { color: 'var(--red)', isError: true, elapsed: qaElapsedExc }))
|
|
879
881
|
: e.name === 'AbortError'
|
|
880
|
-
? (
|
|
881
|
-
? _qaBuildAssistantHtml(
|
|
882
|
+
? (qaPartialText
|
|
883
|
+
? _qaBuildAssistantHtml(qaPartialText + '\n\n_Stopped._', { borderColor: 'var(--muted)', elapsed: qaElapsedExc })
|
|
882
884
|
: _qaBuildAssistantHtml('Stopped', { color: 'var(--muted)', isError: true, elapsed: qaElapsedExc }))
|
|
883
|
-
: (
|
|
884
|
-
? _qaBuildAssistantHtml(
|
|
885
|
+
: (qaPartialText
|
|
886
|
+
? _qaBuildAssistantHtml(qaPartialText + '\n\nError: ' + e.message, { borderColor: 'var(--red)', elapsed: qaElapsedExc })
|
|
885
887
|
: _qaBuildAssistantHtml('Error: ' + e.message, { color: 'var(--red)', isError: true, elapsed: qaElapsedExc }));
|
|
886
888
|
const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
|
|
887
889
|
const loadingEl = tmp.querySelector('#' + loadingId);
|
|
888
890
|
if (loadingEl) loadingEl.remove();
|
|
889
891
|
_qaInsertBeforeQueued(tmp, messageHtml);
|
|
890
892
|
});
|
|
891
|
-
if (e.name === 'AbortError' && _qaRecordAbortedPartial(runtime, message,
|
|
893
|
+
if (e.name === 'AbortError' && _qaRecordAbortedPartial(runtime, message, qaPartialText) && _qaIsActiveSession(sessionKey)) {
|
|
892
894
|
_qaHistory = runtime.history.slice();
|
|
893
895
|
}
|
|
894
896
|
_qaFlushPersistDebounce(sessionKey);
|
|
@@ -71,6 +71,144 @@ function formatToolSummary(name, input) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// ─── Ordered text↔tool segments (CC + doc-chat live streams) ───────────────
|
|
75
|
+
//
|
|
76
|
+
// A single assistant turn can interleave many text blocks with tool calls
|
|
77
|
+
// (text → tool → text → tool → final text, up to ccMaxTurns). The old model
|
|
78
|
+
// flattened that into "all tool chips first, then one merged text blob", which
|
|
79
|
+
// destroyed the chronological structure and welded back-to-back assistant
|
|
80
|
+
// messages together. The segment model preserves arrival order: each entry is
|
|
81
|
+
// a { kind:'text', id, text } or { kind:'tool', id, name, input, status }
|
|
82
|
+
// record, rendered in sequence so prose and tool activity stay interleaved.
|
|
83
|
+
//
|
|
84
|
+
// Text-segment boundaries come from the server `segmentId` on chunk events
|
|
85
|
+
// (engine/llm.js increments it on a tool boundary or a non-extending text
|
|
86
|
+
// push). When segmentId is absent (Copilot pool path, live-stream replay,
|
|
87
|
+
// older engine) ccSegmentsApplyChunk falls back to a heuristic: a chunk that
|
|
88
|
+
// arrives after a tool starts a new text segment; a chunk that extends the
|
|
89
|
+
// current text segment updates it in place. Boundaries are never inferred from
|
|
90
|
+
// character overlap, so unrelated chunks can never weld (cf. W-mq1jwwqo).
|
|
91
|
+
|
|
92
|
+
function _ccSegMergeText(prev, next) {
|
|
93
|
+
if (!prev) return next || '';
|
|
94
|
+
if (!next) return prev;
|
|
95
|
+
if (next === prev) return prev;
|
|
96
|
+
if (next.indexOf(prev) === 0) return next; // accumulated buffer grew
|
|
97
|
+
if (prev.indexOf(next) === 0) return prev; // stale/short repaint of same text
|
|
98
|
+
return prev + '\n\n' + next; // distinct text — separate, never weld
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The trailing text segment of the current run, or null if a tool closed the
|
|
102
|
+
// run (so the next text belongs to a fresh segment).
|
|
103
|
+
function ccSegmentsLastText(segments) {
|
|
104
|
+
for (var i = segments.length - 1; i >= 0; i--) {
|
|
105
|
+
if (segments[i].kind === 'text') return segments[i];
|
|
106
|
+
if (segments[i].kind === 'tool') return null;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ccSegmentsApplyChunk(segments, text, segmentId) {
|
|
112
|
+
if (!text) return segments;
|
|
113
|
+
var hasId = segmentId !== undefined && segmentId !== null;
|
|
114
|
+
var last = segments.length ? segments[segments.length - 1] : null;
|
|
115
|
+
if (hasId) {
|
|
116
|
+
if (last && last.kind === 'text' && last.id === segmentId) { last.text = text; return segments; }
|
|
117
|
+
segments.push({ kind: 'text', id: segmentId, text: text });
|
|
118
|
+
return segments;
|
|
119
|
+
}
|
|
120
|
+
// Heuristic fallback: extend the open text segment, else open a new one.
|
|
121
|
+
if (last && last.kind === 'text') { last.text = _ccSegMergeText(last.text, text); return segments; }
|
|
122
|
+
segments.push({ kind: 'text', id: null, text: text });
|
|
123
|
+
return segments;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ccSegmentsApplyTool(segments, tool) {
|
|
127
|
+
segments.push({
|
|
128
|
+
kind: 'tool',
|
|
129
|
+
id: (tool && tool.id) || null,
|
|
130
|
+
name: tool && tool.name,
|
|
131
|
+
input: (tool && tool.input) || {},
|
|
132
|
+
status: tool && (tool.id ? (tool.status || 'pending') : (tool.status || null)),
|
|
133
|
+
});
|
|
134
|
+
return segments;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ccSegmentsApplyToolUpdate(segments, id, status) {
|
|
138
|
+
for (var i = segments.length - 1; i >= 0; i--) {
|
|
139
|
+
if (segments[i].kind === 'tool' && segments[i].id === id) { segments[i].status = status; break; }
|
|
140
|
+
}
|
|
141
|
+
return segments;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Reconcile streamed segments with the authoritative final text (Claude's
|
|
145
|
+
// `result` event carries the LAST assistant message and arrives via setText
|
|
146
|
+
// with no chunk). Ensures the trailing text segment matches the final answer
|
|
147
|
+
// without discarding earlier interleaved segments.
|
|
148
|
+
function ccSegmentsFinalize(segments, finalText) {
|
|
149
|
+
if (!finalText) return segments;
|
|
150
|
+
var last = ccSegmentsLastText(segments);
|
|
151
|
+
if (last) last.text = _ccSegMergeText(last.text, finalText);
|
|
152
|
+
else segments.push({ kind: 'text', id: null, text: finalText });
|
|
153
|
+
return segments;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Expanded-state for collapsible tool groups, keyed by a caller-supplied stable
|
|
157
|
+
// key (tab id / message id + group ordinal). Persisted here — outside the DOM —
|
|
158
|
+
// so the live stream's full re-render every frame doesn't reset a group the user
|
|
159
|
+
// expanded mid-stream. Native <details> drives the toggle; this just remembers.
|
|
160
|
+
var _ccToolGroupOpen = Object.create(null);
|
|
161
|
+
function ccToolGroupToggle(el, key) {
|
|
162
|
+
if (!el) return;
|
|
163
|
+
if (el.open) _ccToolGroupOpen[key] = true; else delete _ccToolGroupOpen[key];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Render segments in chronological order. Each text segment becomes its own
|
|
167
|
+
// markdown block. A run of >1 tool chips collapses into a <details> showing only
|
|
168
|
+
// the first chip (+ "N more"); expanding reveals the rest. opts.keyPrefix gives
|
|
169
|
+
// each group a stable key so the expanded state survives live-stream re-renders.
|
|
170
|
+
function ccSegmentsRender(segments, opts) {
|
|
171
|
+
if (!segments || !segments.length) return '';
|
|
172
|
+
var keyPrefix = (opts && opts.keyPrefix) || '';
|
|
173
|
+
var html = '';
|
|
174
|
+
var i = 0;
|
|
175
|
+
var groupOrdinal = 0;
|
|
176
|
+
while (i < segments.length) {
|
|
177
|
+
if (segments[i].kind === 'tool') {
|
|
178
|
+
var chips = [];
|
|
179
|
+
while (i < segments.length && segments[i].kind === 'tool') { chips.push(renderToolChip(segments[i])); i++; }
|
|
180
|
+
if (chips.length <= 1) {
|
|
181
|
+
html += '<div class="cc-tool-group">' + chips.join('') + '</div>';
|
|
182
|
+
} else {
|
|
183
|
+
var groupKey = keyPrefix + ':tg' + groupOrdinal;
|
|
184
|
+
var openAttr = _ccToolGroupOpen[groupKey] ? ' open' : '';
|
|
185
|
+
html += '<details class="cc-tool-group cc-tool-collapsible"' + openAttr +
|
|
186
|
+
' ontoggle="ccToolGroupToggle(this,\'' + groupKey + '\')">' +
|
|
187
|
+
'<summary class="cc-tool-summary">' + chips[0] +
|
|
188
|
+
'<span class="cc-tool-more">+' + (chips.length - 1) + ' more</span>' +
|
|
189
|
+
'<span class="cc-tool-less">show less</span></summary>' +
|
|
190
|
+
'<div class="cc-tool-rest">' + chips.slice(1).join('') + '</div>' +
|
|
191
|
+
'</details>';
|
|
192
|
+
}
|
|
193
|
+
groupOrdinal++;
|
|
194
|
+
} else {
|
|
195
|
+
if (segments[i].text) html += '<div class="cc-text-segment">' + renderMd(segments[i].text) + '</div>';
|
|
196
|
+
i++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return html;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Plain-text join of all text segments — for tab titles, copy, transcripts.
|
|
203
|
+
function ccSegmentsText(segments) {
|
|
204
|
+
if (!segments || !segments.length) return '';
|
|
205
|
+
var parts = [];
|
|
206
|
+
for (var i = 0; i < segments.length; i++) {
|
|
207
|
+
if (segments[i].kind === 'text' && segments[i].text) parts.push(segments[i].text);
|
|
208
|
+
}
|
|
209
|
+
return parts.join('\n\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
74
212
|
/**
|
|
75
213
|
* Internal helper: renders a single parsed JSON object into an HTML fragment.
|
|
76
214
|
* @param {object} obj - Parsed JSON object from agent JSONL output
|
package/dashboard/js/utils.js
CHANGED
|
@@ -301,10 +301,47 @@ function _renderMdCore(s) {
|
|
|
301
301
|
// 3. Block-level processing (line by line)
|
|
302
302
|
var lines = html.split('\n');
|
|
303
303
|
var out = [];
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
// Nested-list state: a stack of { tag:'ul'|'ol', indent } frames, each owning
|
|
305
|
+
// one currently-open <li>. Deeper-indented items nest inside the parent <li>;
|
|
306
|
+
// shallower items pop levels. Single-level lists behave exactly as before.
|
|
307
|
+
var listStack = [];
|
|
308
|
+
// Paragraph buffer: consecutive plain-text lines coalesce into one <p>-style
|
|
309
|
+
// block (soft line breaks via <br>) so paragraphs get real vertical spacing
|
|
310
|
+
// instead of each wrapped line becoming its own gap-less <div>.
|
|
311
|
+
var paraBuf = [];
|
|
312
|
+
|
|
313
|
+
function flushPara() {
|
|
314
|
+
if (!paraBuf.length) return;
|
|
315
|
+
out.push('<div class="md-p">' + paraBuf.join('<br>') + '</div>');
|
|
316
|
+
paraBuf = [];
|
|
317
|
+
}
|
|
318
|
+
function closeLevel() {
|
|
319
|
+
var top = listStack.pop();
|
|
320
|
+
out.push('</li>');
|
|
321
|
+
out.push(top.tag === 'ol' ? '</ol>' : '</ul>');
|
|
322
|
+
}
|
|
323
|
+
function closeList() { while (listStack.length) closeLevel(); }
|
|
324
|
+
function _openListTag(tag, isCb) {
|
|
325
|
+
var style = tag === 'ol'
|
|
326
|
+
? 'margin:2px 0 2px 20px;padding:0'
|
|
327
|
+
: (isCb ? 'margin:2px 0 2px 16px;padding:0;list-style:none' : 'margin:2px 0 2px 16px;padding:0');
|
|
328
|
+
return '<' + tag + ' style="' + style + '">';
|
|
329
|
+
}
|
|
330
|
+
// Append a list item at the given indent, opening/closing/nesting lists as
|
|
331
|
+
// needed. `isCb` only affects styling of a freshly opened <ul>.
|
|
332
|
+
function pushListItem(indent, tag, liInner, isCb) {
|
|
333
|
+
flushPara();
|
|
334
|
+
while (listStack.length && listStack[listStack.length - 1].indent > indent) closeLevel();
|
|
335
|
+
if (listStack.length && listStack[listStack.length - 1].indent === indent) {
|
|
336
|
+
if (listStack[listStack.length - 1].tag === tag) {
|
|
337
|
+
out.push('</li>'); out.push('<li>' + liInner); return;
|
|
338
|
+
}
|
|
339
|
+
closeLevel(); // same indent, different list type → switch
|
|
340
|
+
}
|
|
341
|
+
out.push(_openListTag(tag, isCb));
|
|
342
|
+
listStack.push({ tag: tag, indent: indent });
|
|
343
|
+
out.push('<li>' + liInner);
|
|
344
|
+
}
|
|
308
345
|
function nextNonEmptyLine(startIdx) {
|
|
309
346
|
for (var ni = startIdx; ni < lines.length; ni++) {
|
|
310
347
|
if (lines[ni].trim()) return lines[ni];
|
|
@@ -312,28 +349,22 @@ function _renderMdCore(s) {
|
|
|
312
349
|
return '';
|
|
313
350
|
}
|
|
314
351
|
function continuesCurrentList(nextLine) {
|
|
315
|
-
if (!
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
function openList(type) {
|
|
320
|
-
if (inList && listType !== type) closeList();
|
|
321
|
-
if (!inList) {
|
|
322
|
-
var style = type === 'ol' ? 'margin:2px 0 2px 20px;padding:0' : type === 'cb' ? 'margin:2px 0 2px 16px;padding:0;list-style:none' : 'margin:2px 0 2px 16px;padding:0';
|
|
323
|
-
out.push('<' + (type === 'ol' ? 'ol' : 'ul') + ' style="' + style + '">');
|
|
324
|
-
inList = true; listType = type === 'cb' ? 'ul' : type;
|
|
325
|
-
}
|
|
352
|
+
if (!listStack.length || !nextLine) return false;
|
|
353
|
+
// Any list item (of any type/indent) keeps the list region open across a
|
|
354
|
+
// blank line; the item handler manages level transitions.
|
|
355
|
+
return /^(\s*)\d+\.\s+(.+)/.test(nextLine) || /^(\s*)[-*]\s+(.+)/.test(nextLine);
|
|
326
356
|
}
|
|
327
357
|
|
|
328
358
|
for (var i = 0; i < lines.length; i++) {
|
|
329
359
|
var line = lines[i];
|
|
330
360
|
|
|
331
361
|
// Code block placeholder — pass through as-is
|
|
332
|
-
if (line.match(/^\x00CB\d+\x00$/)) { closeList(); out.push(line); continue; }
|
|
362
|
+
if (line.match(/^\x00CB\d+\x00$/)) { flushPara(); closeList(); out.push(line); continue; }
|
|
333
363
|
|
|
334
364
|
// Headings
|
|
335
365
|
var headMatch = line.match(/^(#{1,4})\s+(.+)/);
|
|
336
366
|
if (headMatch) {
|
|
367
|
+
flushPara();
|
|
337
368
|
closeList();
|
|
338
369
|
var sizes = { 1: '16px', 2: '14px', 3: '13px', 4: '12px' };
|
|
339
370
|
out.push('<div style="font-weight:600;font-size:' + sizes[headMatch[1].length] + ';margin:8px 0 4px">' + headMatch[2] + '</div>');
|
|
@@ -342,6 +373,7 @@ function _renderMdCore(s) {
|
|
|
342
373
|
|
|
343
374
|
// Horizontal rule (only bare ---, ***, ___ lines)
|
|
344
375
|
if (/^[-*_]{3,}\s*$/.test(line) && !/\S/.test(line.replace(/[-*_]/g, ''))) {
|
|
376
|
+
flushPara();
|
|
345
377
|
closeList();
|
|
346
378
|
out.push('<hr style="border:none;border-top:1px solid var(--border);margin:8px 0">');
|
|
347
379
|
continue;
|
|
@@ -349,6 +381,7 @@ function _renderMdCore(s) {
|
|
|
349
381
|
|
|
350
382
|
// Blockquote
|
|
351
383
|
if (line.match(/^>\s?/)) {
|
|
384
|
+
flushPara();
|
|
352
385
|
closeList();
|
|
353
386
|
out.push('<div style="border-left:3px solid var(--border);padding-left:8px;color:var(--muted);margin:2px 0">' + line.replace(/^(>\s?)+/, '') + '</div>');
|
|
354
387
|
continue;
|
|
@@ -356,6 +389,7 @@ function _renderMdCore(s) {
|
|
|
356
389
|
|
|
357
390
|
// Table: detect | col | col | rows, consume until non-table line
|
|
358
391
|
if (line.match(/^\|.+\|/)) {
|
|
392
|
+
flushPara();
|
|
359
393
|
closeList();
|
|
360
394
|
var tableRows = [];
|
|
361
395
|
var sepIdx = -1;
|
|
@@ -384,40 +418,39 @@ function _renderMdCore(s) {
|
|
|
384
418
|
// Checkbox list (must come before UL — both start with - )
|
|
385
419
|
var cbMatch = line.match(/^(\s*)[-*]\s\[([ xX])\]\s(.+)/);
|
|
386
420
|
if (cbMatch) {
|
|
387
|
-
|
|
388
|
-
out.push('<li>' + (cbMatch[2] !== ' ' ? '\u2611' : '\u2610') + ' ' + cbMatch[3] + '</li>');
|
|
421
|
+
pushListItem(cbMatch[1].length, 'ul', (cbMatch[2] !== ' ' ? '\u2611' : '\u2610') + ' ' + cbMatch[3], true);
|
|
389
422
|
continue;
|
|
390
423
|
}
|
|
391
424
|
|
|
392
425
|
// Unordered list (- or * followed by space and content, not bare --- or ***)
|
|
393
426
|
var ulMatch = line.match(/^(\s*)[-*]\s+(.+)/);
|
|
394
427
|
if (ulMatch) {
|
|
395
|
-
|
|
396
|
-
out.push('<li>' + ulMatch[2] + '</li>');
|
|
428
|
+
pushListItem(ulMatch[1].length, 'ul', ulMatch[2], false);
|
|
397
429
|
continue;
|
|
398
430
|
}
|
|
399
431
|
|
|
400
432
|
// Ordered list
|
|
401
433
|
var olMatch = line.match(/^(\s*)\d+\.\s+(.+)/);
|
|
402
434
|
if (olMatch) {
|
|
403
|
-
|
|
404
|
-
out.push('<li>' + olMatch[2] + '</li>');
|
|
435
|
+
pushListItem(olMatch[1].length, 'ol', olMatch[2], false);
|
|
405
436
|
continue;
|
|
406
437
|
}
|
|
407
438
|
|
|
408
439
|
// Blank line → spacer
|
|
409
440
|
if (!line.trim()) {
|
|
441
|
+
flushPara();
|
|
410
442
|
if (continuesCurrentList(nextNonEmptyLine(i + 1))) continue;
|
|
411
443
|
closeList();
|
|
412
444
|
out.push('<div style="height:4px"></div>');
|
|
413
445
|
continue;
|
|
414
446
|
}
|
|
415
447
|
|
|
416
|
-
// Non-list line — close any open list
|
|
448
|
+
// Non-list text line — close any open list, then buffer into the current
|
|
449
|
+
// paragraph (flushed at the next block boundary / blank line).
|
|
417
450
|
closeList();
|
|
418
|
-
|
|
419
|
-
out.push('<div>' + line + '</div>');
|
|
451
|
+
paraBuf.push(line);
|
|
420
452
|
}
|
|
453
|
+
flushPara();
|
|
421
454
|
closeList();
|
|
422
455
|
html = out.join('\n');
|
|
423
456
|
|
package/dashboard/styles.css
CHANGED
|
@@ -995,6 +995,35 @@
|
|
|
995
995
|
.md-content th, .md-content td { padding: 4px 8px; border: 1px solid var(--border); text-align: left; font-size: var(--text-base); white-space: nowrap; }
|
|
996
996
|
.md-content td { white-space: normal; min-width: 60px; }
|
|
997
997
|
.md-content th { background: var(--surface); font-weight: 600; }
|
|
998
|
+
/* Real paragraph spacing (md-p groups consecutive text lines) + tightened
|
|
999
|
+
nested-list margins so multi-level lists read cleanly in the narrow drawer. */
|
|
1000
|
+
.md-content .md-p { margin: 0 0 6px; }
|
|
1001
|
+
.md-content .md-p:last-child { margin-bottom: 0; }
|
|
1002
|
+
.md-content li { margin: 1px 0; }
|
|
1003
|
+
.md-content ul ul, .md-content ul ol, .md-content ol ul, .md-content ol ol { margin-top: 2px; margin-bottom: 2px; }
|
|
1004
|
+
.md-content pre { max-width: 100%; }
|
|
1005
|
+
/* Interleaved CC / doc-chat segments: prose is the primary content; tool
|
|
1006
|
+
activity is grouped, dimmed and indented behind a rule so it recedes. */
|
|
1007
|
+
.cc-tool-group { margin: 4px 0; padding-left: 8px; border-left: 2px solid var(--border); opacity: 0.85; display: flex; flex-direction: column; gap: 2px; }
|
|
1008
|
+
.cc-tool-group > div { line-height: 1.4; }
|
|
1009
|
+
/* Collapsible tool group: <details> shows only the first chip; expanding
|
|
1010
|
+
reveals the rest. Native <details> drives the toggle so the final (static)
|
|
1011
|
+
message needs no JS; the live stream persists open state via ccToolGroupToggle. */
|
|
1012
|
+
details.cc-tool-group { display: block; }
|
|
1013
|
+
.cc-tool-summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px; line-height: 1.4; padding: 5px 6px; margin: -3px -6px; border-radius: 6px; min-height: 28px; -webkit-tap-highlight-color: transparent; }
|
|
1014
|
+
.cc-tool-summary:hover { background: var(--surface2); }
|
|
1015
|
+
.cc-tool-summary::-webkit-details-marker { display: none; }
|
|
1016
|
+
/* Bigger tap targets — the hints are small text, so pad them into pill-sized
|
|
1017
|
+
hit areas (the whole summary row is clickable too). */
|
|
1018
|
+
.cc-tool-more, .cc-tool-less { font-size: var(--text-sm); white-space: nowrap; padding: 3px 9px; border-radius: 11px; border: 1px solid var(--border); background: var(--surface2); }
|
|
1019
|
+
.cc-tool-more { color: var(--muted); }
|
|
1020
|
+
.cc-tool-less { display: none; color: var(--blue); }
|
|
1021
|
+
details.cc-tool-group[open] .cc-tool-more { display: none; }
|
|
1022
|
+
details.cc-tool-group[open] .cc-tool-less { display: inline-block; }
|
|
1023
|
+
.cc-tool-rest { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }
|
|
1024
|
+
.cc-text-segment { margin: 2px 0; }
|
|
1025
|
+
.cc-text-segment:first-child { margin-top: 0; }
|
|
1026
|
+
.cc-text-segment + .cc-tool-group, .cc-tool-group + .cc-text-segment { margin-top: 6px; }
|
|
998
1027
|
.status-line { display: flex; align-items: center; gap: var(--space-5); padding: var(--space-5) var(--space-7); background: var(--bg); border-bottom: 1px solid var(--border); font-size: var(--text-md); }
|
|
999
1028
|
|
|
1000
1029
|
/* Modal for inbox detail */
|
package/dashboard.js
CHANGED
|
@@ -5058,10 +5058,10 @@ function _finalizeDocChatEdit({ filePath, fullPath, isJson, canEdit, originalCon
|
|
|
5058
5058
|
function _makeDocChatStreamStripper(onChunk) {
|
|
5059
5059
|
if (!onChunk) return undefined;
|
|
5060
5060
|
let lastSent;
|
|
5061
|
-
return (text) => {
|
|
5061
|
+
return (text, segmentId) => {
|
|
5062
5062
|
if (text === lastSent) return;
|
|
5063
5063
|
lastSent = text;
|
|
5064
|
-
onChunk(text);
|
|
5064
|
+
onChunk(text, segmentId);
|
|
5065
5065
|
};
|
|
5066
5066
|
}
|
|
5067
5067
|
|
|
@@ -8160,12 +8160,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8160
8160
|
}
|
|
8161
8161
|
let wire;
|
|
8162
8162
|
try {
|
|
8163
|
-
// W-mpmwxni2000c25c7-d — terminal error frames go
|
|
8164
|
-
//
|
|
8165
|
-
//
|
|
8166
|
-
//
|
|
8167
|
-
|
|
8168
|
-
wire =
|
|
8163
|
+
// W-mpmwxni2000c25c7-d / W-mqevl09s000i9989 — terminal error frames go
|
|
8164
|
+
// out as a named SSE `event: error` frame so consumers using
|
|
8165
|
+
// addEventListener('error', …) can target them, mirroring the
|
|
8166
|
+
// handleCommandCenterStream contract. The JSON payload still carries
|
|
8167
|
+
// `type: 'error'` for the data-line parser in modal-qa.js.
|
|
8168
|
+
wire = (type === 'error')
|
|
8169
|
+
? `event: error\ndata: ${JSON.stringify(payload)}\n\n`
|
|
8170
|
+
: `data: ${JSON.stringify(payload)}\n\n`;
|
|
8169
8171
|
} catch {
|
|
8170
8172
|
_logFail('json-serialize-failed');
|
|
8171
8173
|
return false;
|
|
@@ -8286,7 +8288,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8286
8288
|
freshSession: !!body.freshSession,
|
|
8287
8289
|
transcript: body.transcript,
|
|
8288
8290
|
onAbortReady: (abort) => { _docAbort = abort; },
|
|
8289
|
-
onChunk: (text) => { bumpTimer(); writeDocEvent({ type: 'chunk', text }); },
|
|
8291
|
+
onChunk: (text, segmentId) => { bumpTimer(); writeDocEvent({ type: 'chunk', text, segmentId }); },
|
|
8290
8292
|
onToolUse: (name, input) => { bumpTimer(); writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
|
|
8291
8293
|
onRetry: (attempt) => { bumpTimer(); writeDocEvent({ type: 'progress', attempt }); },
|
|
8292
8294
|
systemPrompt: turnSystemPrompt,
|
|
@@ -9063,10 +9065,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9063
9065
|
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
9064
9066
|
sessionId, effort, direct: true,
|
|
9065
9067
|
engineConfig,
|
|
9066
|
-
onChunk: (text) => {
|
|
9068
|
+
onChunk: (text, segmentId) => {
|
|
9067
9069
|
_touchCcLiveStream(liveState);
|
|
9068
9070
|
liveState.text = text;
|
|
9069
|
-
if (liveState.writer) liveState.writer({ type: 'chunk', text });
|
|
9071
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text, segmentId });
|
|
9070
9072
|
},
|
|
9071
9073
|
onToolUse: (name, input) => {
|
|
9072
9074
|
_touchCcLiveStream(liveState);
|
|
@@ -13560,13 +13562,28 @@ if (require.main === module) {
|
|
|
13560
13562
|
// (engine/shared.js#openUrlInBrowser) now owns the env-var check and
|
|
13561
13563
|
// emits a debug-level SUPPRESSED log entry so we can prove the kill-
|
|
13562
13564
|
// switch is firing.
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13565
|
+
//
|
|
13566
|
+
// W-mqef-dashboard-tty — only self-open for an interactive human run
|
|
13567
|
+
// (`node dashboard.js` in a terminal). The CLI spawns the dashboard
|
|
13568
|
+
// DETACHED + non-TTY with MINIONS_NO_AUTO_OPEN=1 and orchestrates the open
|
|
13569
|
+
// itself, so it never relied on this branch. But the dashboard integration
|
|
13570
|
+
// tests — and any agent running `npm test` — spawn dashboard.js with piped
|
|
13571
|
+
// stdio and NO env guard, so this fired for real on every test run, popping
|
|
13572
|
+
// a browser tab to a random localhost port that the test then tore down
|
|
13573
|
+
// (blank window on the operator's desktop). stdout.isTTY is false for every
|
|
13574
|
+
// programmatic spawn (tests, agents, CI, the detached CLI dashboard) and
|
|
13575
|
+
// true only for a real terminal session, so it's the correct discriminator.
|
|
13576
|
+
if (process.stdout.isTTY) {
|
|
13577
|
+
const result = shared.openUrlInBrowser(`http://localhost:${PORT}`, {
|
|
13578
|
+
reason: 'dashboard-self-open',
|
|
13579
|
+
callerHint: 'dashboard.js:13124',
|
|
13580
|
+
});
|
|
13581
|
+
if (!result.ok && !result.suppressed) {
|
|
13582
|
+
console.log(` Could not auto-open browser: ${result.error}`);
|
|
13583
|
+
console.log(` Please open http://localhost:${PORT} manually.`);
|
|
13584
|
+
}
|
|
13585
|
+
} else {
|
|
13586
|
+
console.log(` Open http://localhost:${PORT} in your browser.`);
|
|
13570
13587
|
}
|
|
13571
13588
|
|
|
13572
13589
|
// Warm the CC runtime binary cache off the request path so the first CC /
|
package/engine/llm.js
CHANGED
|
@@ -478,6 +478,14 @@ function _createStreamAccumulator({
|
|
|
478
478
|
let taskCompleteFired = false;
|
|
479
479
|
let terminalResultFired = false;
|
|
480
480
|
let lastTaskCompleteSummary = '';
|
|
481
|
+
// Ordered text-segment index threaded to onChunk so the client can interleave
|
|
482
|
+
// distinct assistant text blocks with tool calls instead of welding them into
|
|
483
|
+
// one bubble. Bumped when a tool ran since the last text push (text after a
|
|
484
|
+
// tool is a new block) or when a push doesn't extend the current segment
|
|
485
|
+
// (a fresh assistant message after a stream reset). Consumed by the CC /
|
|
486
|
+
// doc-chat segment renderer (dashboard/js/render-utils.js ccSegments*).
|
|
487
|
+
let segmentSeq = 0;
|
|
488
|
+
let sawToolSinceText = false;
|
|
481
489
|
const toolUses = [];
|
|
482
490
|
|
|
483
491
|
function _streamText(value) {
|
|
@@ -492,8 +500,12 @@ function _createStreamAccumulator({
|
|
|
492
500
|
const next = _streamText(value);
|
|
493
501
|
text = next;
|
|
494
502
|
if (onChunk && next !== lastTextSent) {
|
|
503
|
+
// New segment when a tool ran since the last text, or this push is not a
|
|
504
|
+
// continuation of the current segment (a fresh message after a reset).
|
|
505
|
+
if (lastTextSent && (sawToolSinceText || !next.startsWith(lastTextSent))) segmentSeq++;
|
|
506
|
+
sawToolSinceText = false;
|
|
495
507
|
lastTextSent = next;
|
|
496
|
-
onChunk(next);
|
|
508
|
+
onChunk(next, segmentSeq);
|
|
497
509
|
}
|
|
498
510
|
},
|
|
499
511
|
setText(value) {
|
|
@@ -518,6 +530,7 @@ function _createStreamAccumulator({
|
|
|
518
530
|
if (!name) return;
|
|
519
531
|
const toolUse = { name, input: input || {} };
|
|
520
532
|
toolUses.push(toolUse);
|
|
533
|
+
sawToolSinceText = true;
|
|
521
534
|
if (onToolUse) onToolUse(toolUse.name, toolUse.input);
|
|
522
535
|
},
|
|
523
536
|
toolUseAlreadySeen(name, input) {
|
|
@@ -539,8 +552,10 @@ function _createStreamAccumulator({
|
|
|
539
552
|
if (!text) {
|
|
540
553
|
text = finalSummary;
|
|
541
554
|
if (onChunk && finalSummary !== lastTextSent) {
|
|
555
|
+
if (lastTextSent && (sawToolSinceText || !finalSummary.startsWith(lastTextSent))) segmentSeq++;
|
|
556
|
+
sawToolSinceText = false;
|
|
542
557
|
lastTextSent = finalSummary;
|
|
543
|
-
onChunk(finalSummary);
|
|
558
|
+
onChunk(finalSummary, segmentSeq);
|
|
544
559
|
}
|
|
545
560
|
}
|
|
546
561
|
if (!alreadySeen && onTaskComplete) {
|
|
@@ -795,7 +810,9 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
795
810
|
/**
|
|
796
811
|
* Streaming variant of callLLM — emits text chunks via onChunk callback.
|
|
797
812
|
* Returns the same result object as callLLM when the process completes.
|
|
798
|
-
* onChunk(text) is called for each assistant text block as it
|
|
813
|
+
* onChunk(text, segmentIndex) is called for each assistant text block as it
|
|
814
|
+
* arrives; segmentIndex increments across distinct text blocks so consumers can
|
|
815
|
+
* keep them visually separate instead of welding them together.
|
|
799
816
|
*/
|
|
800
817
|
function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
801
818
|
const {
|
package/engine.js
CHANGED
|
@@ -3306,6 +3306,14 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3306
3306
|
// them unconditionally regardless of repo host.
|
|
3307
3307
|
childEnv.GIT_TERMINAL_PROMPT = '0';
|
|
3308
3308
|
childEnv.GCM_INTERACTIVE = 'never';
|
|
3309
|
+
// W-mqef-dashboard-tty — agents run headless and must never pop a browser on
|
|
3310
|
+
// the operator's desktop. Stamp the suppression env so anything an agent runs
|
|
3311
|
+
// that funnels through shared.openUrlInBrowser — notably the dashboard a
|
|
3312
|
+
// `npm test` run boots — inherits the guard. The engine's own process env did
|
|
3313
|
+
// NOT carry this flag, so agent-spawned dashboards were self-opening to a
|
|
3314
|
+
// random localhost port (blank window). Belt-and-suspenders with dashboard.js's
|
|
3315
|
+
// stdout.isTTY gate.
|
|
3316
|
+
childEnv.MINIONS_NO_AUTO_OPEN = '1';
|
|
3309
3317
|
|
|
3310
3318
|
if (getRepoHost(project) === 'ado') {
|
|
3311
3319
|
// Inject cached ADO token so ADO agents skip re-authentication (#998).
|
|
@@ -3756,6 +3764,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3756
3764
|
// credential dialogs on `git push` against stale PATs.
|
|
3757
3765
|
childEnv.GIT_TERMINAL_PROMPT = '0';
|
|
3758
3766
|
childEnv.GCM_INTERACTIVE = 'never';
|
|
3767
|
+
// W-mqef-dashboard-tty — same browser-popup suppression on steering resume
|
|
3768
|
+
// (see the initial spawn site). Agents must never auto-open a browser.
|
|
3769
|
+
childEnv.MINIONS_NO_AUTO_OPEN = '1';
|
|
3759
3770
|
if (getRepoHost(project) === 'ado') {
|
|
3760
3771
|
// Inject cached ADO token for steering session too (#998)
|
|
3761
3772
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2190",
|
|
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"
|