@yemi33/minions 0.1.2049 → 0.1.2051
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/detail-panel.js +1 -0
- package/dashboard/js/live-stream.js +49 -3
- package/dashboard/js/modal.js +4 -0
- package/dashboard/js/refresh.js +5 -0
- package/dashboard/js/render-utils.js +66 -65
- package/dashboard/js/settings.js +377 -188
- package/dashboard/styles.css +35 -0
- package/dashboard.js +67 -3
- package/docs/deprecated.json +50 -5
- package/engine/dispatch-events.js +177 -0
- package/engine/dispatch.js +19 -0
- package/package.json +1 -1
|
@@ -97,6 +97,7 @@ function renderDetailContent(detail, tab) {
|
|
|
97
97
|
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal server timestamp and static live-chat controls (no user data flows in)
|
|
98
98
|
el.innerHTML =
|
|
99
99
|
'<div id="live-chat" style="display:flex;flex-direction:column;height:60vh">' +
|
|
100
|
+
'<div id="live-terminal-banner"></div>' +
|
|
100
101
|
'<div id="live-messages" style="flex:1;overflow-y:auto;padding:8px;font-size:11px;line-height:1.6;display:flex;flex-direction:column"></div>' +
|
|
101
102
|
'<div id="live-status-bar" style="padding:4px 8px;display:flex;align-items:center;gap:8px;border-top:1px solid var(--border)">' +
|
|
102
103
|
'<span class="pulse"></span><span id="live-status-label" style="font-size:11px;color:var(--green)">Streaming live</span>' +
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
let livePollingInterval = null;
|
|
4
4
|
let liveEventSource = null;
|
|
5
|
+
let liveTerminalSource = null;
|
|
5
6
|
let _steerInFlight = false;
|
|
6
7
|
let _lastRenderedText = '';
|
|
7
8
|
let _runtimeTimer = null;
|
|
@@ -45,13 +46,54 @@ function startLiveStream(agentId) {
|
|
|
45
46
|
if (msgEl) msgEl.innerHTML = '';
|
|
46
47
|
_lastRenderedText = '';
|
|
47
48
|
|
|
49
|
+
// W-mpob4nyk0006580e — clear any stale banner from a previous agent.
|
|
50
|
+
const bannerEl = document.getElementById('live-terminal-banner');
|
|
51
|
+
if (bannerEl) bannerEl.innerHTML = '';
|
|
52
|
+
|
|
48
53
|
// Start runtime counter
|
|
49
54
|
_updateRuntimeCounter();
|
|
50
55
|
_runtimeTimer = setInterval(_updateRuntimeCounter, 1000);
|
|
51
56
|
|
|
52
|
-
// Use polling
|
|
53
|
-
//
|
|
57
|
+
// Use polling for log content (avoids HTTP/1.1 connection exhaustion that
|
|
58
|
+
// a full SSE log stream would cause — see W-mpob4nyk0006580e note below).
|
|
54
59
|
startLivePolling();
|
|
60
|
+
|
|
61
|
+
// W-mpob4nyk0006580e — terminal banner is driven by the engine's
|
|
62
|
+
// `dispatch.terminal` SSE event, NOT by parsing the JSONL log tail.
|
|
63
|
+
// The /live-stream endpoint multiplexes log frames (default `data:`
|
|
64
|
+
// messages) and terminal events (named `event: dispatch.terminal`); we
|
|
65
|
+
// open the SSE connection but only consume the terminal event. The
|
|
66
|
+
// stream stays mostly idle (one event per dispatch lifecycle), so this
|
|
67
|
+
// does not regress the connection-exhaustion fix that switched log
|
|
68
|
+
// tailing to polling.
|
|
69
|
+
startTerminalEventSource(agentId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function startTerminalEventSource(agentId) {
|
|
73
|
+
if (typeof window.EventSource !== 'function') return;
|
|
74
|
+
try {
|
|
75
|
+
liveTerminalSource = new EventSource('/api/agent/' + encodeURIComponent(agentId) + '/live-stream?tail=0');
|
|
76
|
+
} catch (e) { console.error('terminal-event source open:', e.message); return; }
|
|
77
|
+
liveTerminalSource.addEventListener('dispatch.terminal', function(ev) {
|
|
78
|
+
let payload;
|
|
79
|
+
try { payload = JSON.parse(ev.data); } catch { return; }
|
|
80
|
+
renderTerminalBannerForAgent(agentId, payload);
|
|
81
|
+
});
|
|
82
|
+
// Ignore default `data:` log frames — log content arrives via polling.
|
|
83
|
+
liveTerminalSource.onerror = function() {
|
|
84
|
+
// EventSource auto-reconnects; nothing to do.
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderTerminalBannerForAgent(agentId, payload) {
|
|
89
|
+
// Guard against late-arriving events for a previous agent (tab switch race).
|
|
90
|
+
if (typeof currentAgentId !== 'undefined' && currentAgentId !== agentId) return;
|
|
91
|
+
if (typeof currentTab !== 'undefined' && currentTab !== 'live') return;
|
|
92
|
+
const bannerEl = document.getElementById('live-terminal-banner');
|
|
93
|
+
if (!bannerEl) return;
|
|
94
|
+
const html = renderTerminalBanner(payload);
|
|
95
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderTerminalBanner() escapes all user-controlled fields (summary, reason, prUrl) before assembling HTML (see dashboard/js/render-utils.js)
|
|
96
|
+
bannerEl.innerHTML = html;
|
|
55
97
|
}
|
|
56
98
|
|
|
57
99
|
function stopLiveStream() {
|
|
@@ -59,6 +101,10 @@ function stopLiveStream() {
|
|
|
59
101
|
liveEventSource.close();
|
|
60
102
|
liveEventSource = null;
|
|
61
103
|
}
|
|
104
|
+
if (liveTerminalSource) {
|
|
105
|
+
try { liveTerminalSource.close(); } catch { /* optional */ }
|
|
106
|
+
liveTerminalSource = null;
|
|
107
|
+
}
|
|
62
108
|
if (_runtimeTimer) { clearInterval(_runtimeTimer); _runtimeTimer = null; }
|
|
63
109
|
stopLivePolling();
|
|
64
110
|
}
|
|
@@ -169,4 +215,4 @@ async function sendSteering() {
|
|
|
169
215
|
}
|
|
170
216
|
}
|
|
171
217
|
|
|
172
|
-
window.MinionsLive = { renderLiveChatMessage, startLiveStream, stopLiveStream, startLivePolling, stopLivePolling, refreshLiveOutput, sendSteering };
|
|
218
|
+
window.MinionsLive = { renderLiveChatMessage, renderTerminalBannerForAgent, startLiveStream, stopLiveStream, startLivePolling, stopLivePolling, refreshLiveOutput, sendSteering };
|
package/dashboard/js/modal.js
CHANGED
|
@@ -7,6 +7,10 @@ function closeModal() {
|
|
|
7
7
|
clearModalBackStack();
|
|
8
8
|
// Hide Q&A section (only shown for document modals)
|
|
9
9
|
document.getElementById('modal-qa').style.display = 'none';
|
|
10
|
+
// Remove settings-body marker so the next modal opens with the default
|
|
11
|
+
// monospace/pre-wrap modal-body styling rather than inheriting the Segoe-UI
|
|
12
|
+
// tabbed-settings overrides (W-mpmwxkcn000646cc).
|
|
13
|
+
document.getElementById('modal-body').classList.remove('settings-body');
|
|
10
14
|
// Remove settings buttons if present
|
|
11
15
|
const settingsBtn = document.getElementById('modal-settings-save');
|
|
12
16
|
if (settingsBtn) settingsBtn.remove();
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -89,6 +89,11 @@ const RENDER_VERSIONS = {
|
|
|
89
89
|
pipelines: 1,
|
|
90
90
|
pinned: 1,
|
|
91
91
|
kbPayload: 2,
|
|
92
|
+
// settings is a modal-driven section, not a tick-driven render — but the
|
|
93
|
+
// cache-bust entry exists so future tick-driven code (or feature-flag-aware
|
|
94
|
+
// renderers) can call `_changed('settings', …)` and bump on visual revamps.
|
|
95
|
+
// Bumped to 2 by W-mpmwxkcn000646cc (left-rail tabbed Settings layout).
|
|
96
|
+
settings: 2,
|
|
92
97
|
};
|
|
93
98
|
const _sectionCache = {};
|
|
94
99
|
const _lastValueByKey = {};
|
|
@@ -189,19 +189,15 @@ function _renderJsonObj(obj, state) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
if (obj.type === 'result') {
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
} else {
|
|
202
|
-
parts.push('<div style="background:rgba(63,185,80,0.1);border:1px solid var(--green);padding:8px 12px;border-radius:8px;margin:8px 0;font-size:12px;color:var(--green)">\u2713 Task complete</div>');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
192
|
+
// W-mpob4nyk0006580e — the terminal banner is no longer derived here.
|
|
193
|
+
// The engine's dispatch.result is the single source of truth and is
|
|
194
|
+
// pushed to the live view via the SSE `dispatch.terminal` channel
|
|
195
|
+
// (engine/dispatch-events.js → dashboard.js handleAgentLiveStream).
|
|
196
|
+
// `result` JSONL lines and the `[process-exit]` sentinel are still
|
|
197
|
+
// parsed for other purposes (e.g. tool deltas above), but they must
|
|
198
|
+
// NOT trigger any banner here — log heuristics can disagree with the
|
|
199
|
+
// engine verdict (CLI crash AFTER successful completion report, exit-0
|
|
200
|
+
// with no result line, timeout kill, …). See renderTerminalBanner().
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
return parts.join('');
|
|
@@ -237,46 +233,12 @@ function renderAgentOutput(text) {
|
|
|
237
233
|
}
|
|
238
234
|
}
|
|
239
235
|
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
// spawn-agent.js writes:
|
|
247
|
-
// "\n[process-exit] code=N\n" on normal close (engine/spawn-agent.js:202)
|
|
248
|
-
// "\n[process-exit] spawn-failed\n" on synchronous spawn() throw
|
|
249
|
-
//
|
|
250
|
-
// We pre-scan to find: (1) whether [process-exit] was emitted at all, (2) its
|
|
251
|
-
// exit code (success vs failure), and (3) the line index of the last result
|
|
252
|
-
// strictly before that sentinel.
|
|
253
|
-
var exitInfo = null; // null = process still running (no banner ever fires)
|
|
254
|
-
var exitLineIdx = -1;
|
|
255
|
-
var lastResultLineIdx = -1;
|
|
256
|
-
var exitRe = /^\[process-exit\]\s+(?:code=)?(-?\d+|spawn-failed)\s*$/;
|
|
257
|
-
for (var k = 0; k < lines.length; k++) {
|
|
258
|
-
var t = lines[k].trim();
|
|
259
|
-
if (!t) continue;
|
|
260
|
-
var em = exitRe.exec(t);
|
|
261
|
-
if (em) {
|
|
262
|
-
var token = em[1];
|
|
263
|
-
var code = token === 'spawn-failed' ? -1 : parseInt(token, 10);
|
|
264
|
-
exitInfo = { code: code, success: code === 0 };
|
|
265
|
-
exitLineIdx = k;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (exitLineIdx !== -1) {
|
|
270
|
-
for (var r = 0; r < exitLineIdx; r++) {
|
|
271
|
-
var rt = lines[r].trim();
|
|
272
|
-
if (!rt || rt.charCodeAt(0) !== 123 /* '{' */) continue;
|
|
273
|
-
try {
|
|
274
|
-
var probe = JSON.parse(rt);
|
|
275
|
-
if (probe && probe.type === 'result') lastResultLineIdx = r;
|
|
276
|
-
} catch (e) { /* ignore parse errors during scan */ }
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
state.exitInfo = exitInfo;
|
|
236
|
+
// W-mpob4nyk0006580e — pre-scan removed. Previously we found the final
|
|
237
|
+
// `"type":"result"` line + `[process-exit]` sentinel to gate a terminal
|
|
238
|
+
// banner here, but the terminal banner is now pushed from the engine via
|
|
239
|
+
// the `dispatch.terminal` SSE channel (see renderTerminalBanner()). The
|
|
240
|
+
// log lines below are still parsed for tool calls / assistant text /
|
|
241
|
+
// reasoning deltas — only the banner-gating heuristic is gone.
|
|
280
242
|
|
|
281
243
|
for (var i = 0; i < lines.length; i++) {
|
|
282
244
|
var trimmed = lines[i].trim();
|
|
@@ -304,26 +266,24 @@ function renderAgentOutput(text) {
|
|
|
304
266
|
continue;
|
|
305
267
|
}
|
|
306
268
|
|
|
307
|
-
// JSON array line
|
|
269
|
+
// JSON array line
|
|
308
270
|
if (trimmed.startsWith('[')) {
|
|
309
271
|
try {
|
|
310
272
|
var arr = JSON.parse(trimmed);
|
|
311
273
|
if (Array.isArray(arr)) {
|
|
312
|
-
state.isFinalResult = false;
|
|
313
274
|
for (var j = 0; j < arr.length; j++) fragments.push(_renderJsonObj(arr[j], state));
|
|
314
275
|
continue;
|
|
315
276
|
}
|
|
316
277
|
} catch (e) { /* fall through */ }
|
|
317
278
|
}
|
|
318
279
|
|
|
319
|
-
// JSON object line
|
|
280
|
+
// JSON object line
|
|
320
281
|
if (trimmed.startsWith('{')) {
|
|
321
282
|
try {
|
|
322
283
|
var obj = JSON.parse(trimmed);
|
|
323
284
|
if (obj.type !== 'assistant.message_delta' && obj.type !== 'assistant.reasoning' && obj.type !== 'assistant.reasoning_delta' && obj.type !== 'assistant.message') {
|
|
324
285
|
flushCopilotPending();
|
|
325
286
|
}
|
|
326
|
-
state.isFinalResult = (i === lastResultLineIdx) && (exitInfo !== null);
|
|
327
287
|
fragments.push(_renderJsonObj(obj, state));
|
|
328
288
|
continue;
|
|
329
289
|
} catch (e) { /* fall through */ }
|
|
@@ -342,16 +302,57 @@ function renderAgentOutput(text) {
|
|
|
342
302
|
|
|
343
303
|
flushCopilotPending();
|
|
344
304
|
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
305
|
+
// W-mpob4nyk0006580e — no log-derived banner here either. The fallback
|
|
306
|
+
// "exit non-zero with no result line" branch used to render a red banner
|
|
307
|
+
// for crashes mid-write; that case is now covered by the engine's
|
|
308
|
+
// `dispatch.result` (set by completeDispatch and pushed via the SSE
|
|
309
|
+
// `dispatch.terminal` channel — see renderTerminalBanner()). Removing
|
|
310
|
+
// this branch is what makes the live view banner agree with the agent
|
|
311
|
+
// row's status in cases like exit-1 AFTER a success completion report.
|
|
352
312
|
return fragments.join('');
|
|
353
313
|
}
|
|
354
314
|
|
|
315
|
+
/**
|
|
316
|
+
* W-mpob4nyk0006580e — render the terminal banner from an authoritative
|
|
317
|
+
* `dispatch.terminal` SSE event. The shape mirrors
|
|
318
|
+
* engine/dispatch-events.js buildTerminalEvent():
|
|
319
|
+
* { type, ts, dispatchId, agentId, result, reason, failureClass, prUrl, summary }
|
|
320
|
+
*
|
|
321
|
+
* The banner state mirrors `dispatch.result` exactly — green for SUCCESS,
|
|
322
|
+
* red otherwise. Optional reason / failure class / PR link / summary first
|
|
323
|
+
* line are surfaced as secondary context.
|
|
324
|
+
*
|
|
325
|
+
* Returns HTML for a single banner div. Callers replace the banner
|
|
326
|
+
* container's innerHTML on each event.
|
|
327
|
+
*/
|
|
328
|
+
function renderTerminalBanner(event) {
|
|
329
|
+
if (!event || !event.result) return '';
|
|
330
|
+
var isSuccess = event.result === 'success';
|
|
331
|
+
var color = isSuccess ? 'var(--green)' : 'var(--red)';
|
|
332
|
+
var bg = isSuccess ? 'rgba(63,185,80,0.1)' : 'rgba(248,81,73,0.1)';
|
|
333
|
+
var glyph = isSuccess ? '\u2713' : '\u2717';
|
|
334
|
+
var headline;
|
|
335
|
+
if (isSuccess) {
|
|
336
|
+
headline = glyph + ' Task complete';
|
|
337
|
+
} else {
|
|
338
|
+
// 'error' / 'timeout' / anything-else → use reason word from the engine
|
|
339
|
+
var reasonWord = event.result === 'timeout' ? 'timeout' : (event.failureClass || 'error');
|
|
340
|
+
headline = glyph + ' Task ended with ' + escHtml(String(reasonWord));
|
|
341
|
+
}
|
|
342
|
+
var extra = '';
|
|
343
|
+
if (event.summary) extra += '<div style="font-size:11px;margin-top:4px;opacity:0.9">' + escHtml(event.summary) + '</div>';
|
|
344
|
+
if (event.reason && event.reason !== event.summary) {
|
|
345
|
+
extra += '<div style="font-size:10px;margin-top:2px;opacity:0.75">' + escHtml(event.reason) + '</div>';
|
|
346
|
+
}
|
|
347
|
+
if (event.prUrl) {
|
|
348
|
+
var safeUrl = encodeURI(String(event.prUrl));
|
|
349
|
+
extra += '<div style="font-size:11px;margin-top:4px"><a href="' + escHtml(safeUrl) + '" target="_blank" rel="noopener" style="color:' + color + '">' + escHtml(event.prUrl) + '</a></div>';
|
|
350
|
+
}
|
|
351
|
+
return '<div style="background:' + bg + ';border:1px solid ' + color + ';padding:8px 12px;border-radius:8px;margin:8px 0;font-size:12px;color:' + color + '">' +
|
|
352
|
+
headline + extra +
|
|
353
|
+
'</div>';
|
|
354
|
+
}
|
|
355
|
+
|
|
355
356
|
/**
|
|
356
357
|
* Standard "Prev / Next" pager HTML for a paginated list. Used by inbox, KB,
|
|
357
358
|
* completed dispatches, and engine log. The caller is responsible for the
|
|
@@ -404,4 +405,4 @@ function pinButton(pinKey, pinned, source, opts) {
|
|
|
404
405
|
(pinned ? 'Unpin' : 'Pin') + '</button>';
|
|
405
406
|
}
|
|
406
407
|
|
|
407
|
-
window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderPager, pinButton };
|
|
408
|
+
window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderTerminalBanner, renderPager, pinButton };
|