@yemi33/minions 0.1.1724 → 0.1.1726
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 -0
- package/dashboard/js/command-center.js +27 -7
- package/dashboard/js/command-history.js +1 -1
- package/dashboard/js/detail-panel.js +3 -3
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-dispatch.js +3 -3
- package/dashboard/js/render-inbox.js +1 -2
- package/dashboard/js/render-meetings.js +1 -1
- package/dashboard/js/render-pipelines.js +46 -17
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-schedules.js +2 -2
- package/dashboard/js/render-watches.js +3 -3
- package/dashboard/js/render-work-items.js +3 -3
- package/dashboard/js/utils.js +34 -0
- package/dashboard/styles.css +8 -0
- package/dashboard.js +78 -3
- package/engine/copilot-models.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
var CC_MAX_TABS = 20;
|
|
5
5
|
var CC_MAX_MESSAGES_PER_TAB = 30;
|
|
6
6
|
var CC_TITLE_MAX_LENGTH = 40;
|
|
7
|
+
var CC_STREAM_FETCH_TIMEOUT_MS = (60 * 60 * 1000) + 60000; // backend CC timeout plus 1-minute delivery buffer
|
|
7
8
|
|
|
8
9
|
var _ccTabs = []; // [{id, title, sessionId, messages: [{role, html}]}]
|
|
9
10
|
var _ccActiveTabId = null;
|
|
@@ -711,7 +712,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
711
712
|
var res = await fetch('/api/command-center/stream', {
|
|
712
713
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
713
714
|
body: JSON.stringify(requestBody),
|
|
714
|
-
signal: activeTab._abortController ? activeTab._abortController.signal : AbortSignal.timeout(
|
|
715
|
+
signal: activeTab._abortController ? activeTab._abortController.signal : AbortSignal.timeout(CC_STREAM_FETCH_TIMEOUT_MS)
|
|
715
716
|
});
|
|
716
717
|
|
|
717
718
|
if (!res.ok) {
|
|
@@ -934,7 +935,13 @@ function ccRetryLast(tabId, retryId) {
|
|
|
934
935
|
|
|
935
936
|
async function _ccFetch(url, body) {
|
|
936
937
|
var res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
937
|
-
if (!res.ok) {
|
|
938
|
+
if (!res.ok) {
|
|
939
|
+
var d = await res.json().catch(function() { return {}; });
|
|
940
|
+
var err = new Error(d.error || 'Request failed (' + res.status + ')');
|
|
941
|
+
err.status = res.status;
|
|
942
|
+
err.data = d;
|
|
943
|
+
throw err;
|
|
944
|
+
}
|
|
938
945
|
return res;
|
|
939
946
|
}
|
|
940
947
|
|
|
@@ -946,6 +953,8 @@ function _tagServerExecuted(actions, actionResults) {
|
|
|
946
953
|
if (r && r.ok) {
|
|
947
954
|
actions[i]._serverExecuted = true;
|
|
948
955
|
if (r.id) actions[i]._serverId = r.id;
|
|
956
|
+
if (r.warning) actions[i]._serverWarning = r.warning;
|
|
957
|
+
if (r.duplicate) actions[i]._serverDuplicate = true;
|
|
949
958
|
} else if (r && r.error) {
|
|
950
959
|
actions[i]._serverExecuted = true;
|
|
951
960
|
actions[i]._serverError = r.error;
|
|
@@ -965,8 +974,10 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
965
974
|
status.style.color = 'var(--red)';
|
|
966
975
|
} else {
|
|
967
976
|
var label = action._serverId ? escHtml(action._serverId) : escHtml(action.title || action.type);
|
|
968
|
-
status.innerHTML = '✓ ' + escHtml(action.type) + ': <strong>' + label + '</strong>'
|
|
969
|
-
|
|
977
|
+
status.innerHTML = '✓ ' + escHtml(action.type) + ': <strong>' + label + '</strong>' +
|
|
978
|
+
(action._serverDuplicate ? ' <span style="color:var(--orange)">already exists</span>' : '') +
|
|
979
|
+
(action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '');
|
|
980
|
+
status.style.color = action._serverDuplicate ? 'var(--orange)' : 'var(--green)';
|
|
970
981
|
}
|
|
971
982
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
972
983
|
if (['dispatch','fix','implement','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
|
|
@@ -1285,9 +1296,18 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1285
1296
|
break;
|
|
1286
1297
|
}
|
|
1287
1298
|
case 'create-pipeline': {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1299
|
+
try {
|
|
1300
|
+
await _ccFetch('/api/pipelines', { id: action.id, title: action.title, stages: action.stages || [], trigger: action.trigger || null, stopWhen: action.stopWhen || null, monitoredResources: action.monitoredResources || null });
|
|
1301
|
+
status.innerHTML = '✓ Pipeline created: <strong>' + escHtml(action.id) + '</strong>';
|
|
1302
|
+
status.style.color = 'var(--green)';
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
if (e.status === 409) {
|
|
1305
|
+
status.innerHTML = '✓ Pipeline already exists: <strong>' + escHtml(action.id) + '</strong>';
|
|
1306
|
+
status.style.color = 'var(--orange)';
|
|
1307
|
+
} else {
|
|
1308
|
+
throw e;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1291
1311
|
break;
|
|
1292
1312
|
}
|
|
1293
1313
|
case 'delete-pipeline': {
|
|
@@ -38,7 +38,7 @@ function cmdShowHistory() {
|
|
|
38
38
|
'<div class="cmd-history-item-meta">' +
|
|
39
39
|
'<span class="chip" style="color:' + intentColor + '">' + intentLabel + '</span>' +
|
|
40
40
|
'<span>' + ago + '</span>' +
|
|
41
|
-
'<span>' +
|
|
41
|
+
'<span>' + formatLocalDateTime(date) + '</span>' +
|
|
42
42
|
'</div>' +
|
|
43
43
|
'</div>' +
|
|
44
44
|
'<button class="cmd-history-resubmit" onclick="cmdResubmit(' + i + ')">Resubmit</button>' +
|
|
@@ -42,8 +42,8 @@ function renderDetailContent(detail, tab) {
|
|
|
42
42
|
html += '<h4>Current Status</h4><div class="section">';
|
|
43
43
|
html += 'Status: <span style="color:var(--' + (detail.statusData.status === 'working' ? 'yellow' : detail.statusData.status === 'done' ? 'green' : 'muted') + ')">' + (detail.statusData.status || 'idle').toUpperCase() + '</span>\n';
|
|
44
44
|
if (detail.statusData.task) html += 'Task: ' + escHtml(detail.statusData.task) + '\n';
|
|
45
|
-
if (detail.statusData.started_at) html += 'Started: ' + detail.statusData.started_at + '\n';
|
|
46
|
-
if (detail.statusData.completed_at) html += 'Completed: ' + detail.statusData.completed_at + '\n';
|
|
45
|
+
if (detail.statusData.started_at) html += 'Started: ' + formatLocalDateTime(detail.statusData.started_at) + '\n';
|
|
46
|
+
if (detail.statusData.completed_at) html += 'Completed: ' + formatLocalDateTime(detail.statusData.completed_at) + '\n';
|
|
47
47
|
if (detail.statusData.started_at && detail.statusData.completed_at) {
|
|
48
48
|
var dMs = new Date(detail.statusData.completed_at).getTime() - new Date(detail.statusData.started_at).getTime();
|
|
49
49
|
if (dMs > 0) {
|
|
@@ -132,7 +132,7 @@ function renderDetailContent(detail, tab) {
|
|
|
132
132
|
'<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(d.task) + '">' + escHtml(d.task.slice(0, 80)) + '</td>' +
|
|
133
133
|
'<td><span class="dispatch-type ' + d.type + '">' + escHtml(d.type) + '</span></td>' +
|
|
134
134
|
'<td style="color:' + color + '"' + reason + '>' + escHtml(d.result) + (isError && d.reason ? ' <span style="font-size:10px;color:var(--muted)">(' + escHtml(d.reason.slice(0, 50)) + ')</span>' : '') + '</td>' +
|
|
135
|
-
'<td style="font-size:10px;color:var(--muted)">' + (d.completed_at ?
|
|
135
|
+
'<td style="font-size:10px;color:var(--muted)">' + (d.completed_at ? formatLocalDateTime(d.completed_at) : '') + '</td>' +
|
|
136
136
|
'</tr>';
|
|
137
137
|
});
|
|
138
138
|
html += '</tbody></table>';
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -60,7 +60,7 @@ function _processStatusUpdate(data) {
|
|
|
60
60
|
localStorage.setItem('minions-install-id', data.installId);
|
|
61
61
|
}
|
|
62
62
|
// Always update cheap elements
|
|
63
|
-
document.getElementById('ts').textContent =
|
|
63
|
+
document.getElementById('ts').textContent = formatLocalTime(data.timestamp);
|
|
64
64
|
const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
|
|
65
65
|
document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
|
|
66
66
|
const autoEl = document.getElementById('auto-approve-badge');
|
|
@@ -75,7 +75,7 @@ function renderAdoThrottleAlert(adoThrottle) {
|
|
|
75
75
|
el.innerHTML = '';
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
const resumeTime =
|
|
78
|
+
const resumeTime = formatLocalTime(adoThrottle.retryAfter);
|
|
79
79
|
el.innerHTML =
|
|
80
80
|
'<span class="engine-alert-msg">⚠️ ADO rate-limited — resume ~' + resumeTime + ' (' + adoThrottle.consecutiveHits + ' consecutive hit' + (adoThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
81
81
|
el.style.display = 'flex';
|
|
@@ -89,7 +89,7 @@ function renderGhThrottleAlert(ghThrottle) {
|
|
|
89
89
|
el.innerHTML = '';
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
const resumeTime =
|
|
92
|
+
const resumeTime = formatLocalTime(ghThrottle.retryAfter);
|
|
93
93
|
el.innerHTML =
|
|
94
94
|
'<span class="engine-alert-msg">⚠️ GitHub rate-limited — resume ~' + resumeTime + ' (' + ghThrottle.consecutiveHits + ' consecutive hit' + (ghThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
95
95
|
el.style.display = 'flex';
|
|
@@ -212,7 +212,7 @@ function renderEngineLog(log) {
|
|
|
212
212
|
|
|
213
213
|
function shortTime(t) {
|
|
214
214
|
if (!t) return '';
|
|
215
|
-
|
|
215
|
+
return formatLocalTime(t);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
async function showErrorDetails(agentId, reason, task) {
|
|
@@ -81,8 +81,7 @@ function renderNotes(notes) {
|
|
|
81
81
|
// Show last updated timestamp
|
|
82
82
|
const updatedEl = document.getElementById('notes-updated');
|
|
83
83
|
if (updatedEl && updatedAt) {
|
|
84
|
-
|
|
85
|
-
updatedEl.textContent = 'updated ' + d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
|
84
|
+
updatedEl.textContent = 'updated ' + formatLocalDateTime(updatedAt);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
@@ -46,7 +46,7 @@ function renderMeetings(meetings) {
|
|
|
46
46
|
}).join(' ');
|
|
47
47
|
|
|
48
48
|
const dt = m.completedAt || m.createdAt;
|
|
49
|
-
const timeStr = dt ?
|
|
49
|
+
const timeStr = dt ? formatLocalDateTime(dt) : '';
|
|
50
50
|
|
|
51
51
|
return '<div data-file="meetings/' + escHtml(m.id) + '.json" style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer;position:relative" onclick="if(shouldIgnoreSelectionClick(event))return;openMeetingDetail(\'' + escHtml(m.id) + '\')">' +
|
|
52
52
|
'<div style="display:flex;justify-content:space-between;align-items:center">' +
|
|
@@ -61,6 +61,26 @@ function _renderMonitoredResources(resources, options) {
|
|
|
61
61
|
return '<div style="margin-top:4px;display:flex;flex-wrap:wrap;gap:3px;align-items:center">' + heading + pills.join('') + '</div>';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function _getPipelineActiveRun(pipeline) {
|
|
65
|
+
return ((pipeline && pipeline.runs) || []).find(function(r) { return r.status === 'running' || r.status === 'paused'; });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _getPipelineTriggerLabel(pipeline) {
|
|
69
|
+
return pipeline?.trigger?.cron ? _cronToHuman(pipeline.trigger.cron) : 'Manual trigger';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _getPipelineStageLabel(pipeline) {
|
|
73
|
+
var count = ((pipeline && pipeline.stages) || []).length;
|
|
74
|
+
return count + ' stage' + (count === 1 ? '' : 's');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _getPipelineEmptyRunCopy(pipeline) {
|
|
78
|
+
if (pipeline?.trigger?.cron) {
|
|
79
|
+
return 'No runs yet. Scheduled for ' + _cronToHuman(pipeline.trigger.cron) + ' (' + Intl.DateTimeFormat().resolvedOptions().timeZone + '). Use Run Now to start the first run immediately.';
|
|
80
|
+
}
|
|
81
|
+
return 'No runs yet. Use Run Now to start this pipeline.';
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
/**
|
|
65
85
|
* Render clickable artifact links for a pipeline stage.
|
|
66
86
|
* Each artifact type gets an icon and navigates to the relevant detail view.
|
|
@@ -233,13 +253,14 @@ function _buildNodeChain(stages, run, options) {
|
|
|
233
253
|
|
|
234
254
|
html += '</div>';
|
|
235
255
|
|
|
236
|
-
//
|
|
256
|
+
// Repeat indicator — cron pipelines repeat on schedule; condition/stopWhen pipelines repeat until a terminal condition.
|
|
237
257
|
var hasStopWhen = !!pipeline?.stopWhen;
|
|
238
258
|
var hasConditionStage = (pipeline?.stages || []).some(function(s) { return s.type === 'condition'; });
|
|
239
|
-
|
|
259
|
+
var hasCron = !!pipeline?.trigger?.cron;
|
|
260
|
+
if (hasStopWhen || hasConditionStage || hasCron) {
|
|
240
261
|
var runCount = (pipeline.runs || []).length;
|
|
241
262
|
var cronLabel = pipeline?.trigger?.cron ? _cronToHuman(pipeline.trigger.cron) : 'until condition met';
|
|
242
|
-
html += '<div class="pl-node-loop">\u21BA Loop (' + escHtml(cronLabel) + ')';
|
|
263
|
+
html += '<div class="pl-node-loop">\u21BA ' + (hasCron ? 'Repeats' : 'Loop') + ' (' + escHtml(cronLabel) + ')';
|
|
243
264
|
if (runCount > 0) html += ' \u00b7 Run ' + runCount;
|
|
244
265
|
html += '</div>';
|
|
245
266
|
}
|
|
@@ -261,11 +282,11 @@ function renderPipelines(pipelines) {
|
|
|
261
282
|
countEl.textContent = pipelines.length;
|
|
262
283
|
|
|
263
284
|
el.innerHTML = pipelines.map(function(p) {
|
|
264
|
-
const activeRun = (p
|
|
285
|
+
const activeRun = _getPipelineActiveRun(p);
|
|
265
286
|
const lastRun = (p.runs || []).slice(-1)[0];
|
|
266
287
|
const statusColor = activeRun ? 'var(--blue)' : lastRun?.status === 'completed' ? 'var(--green)' : lastRun?.status === 'failed' ? 'var(--red)' : lastRun?.status === 'stopped' ? 'var(--yellow)' : 'var(--muted)';
|
|
267
|
-
const statusLabel = activeRun ? 'Running' : lastRun ? (lastRun.status === 'completed' ? 'Completed' : lastRun.status === 'failed' ? 'Failed' : lastRun.status === 'stopped' ? 'Stopped' : lastRun.status) : '
|
|
268
|
-
const trigger =
|
|
288
|
+
const statusLabel = activeRun ? (activeRun.status === 'paused' ? 'Paused' : 'Running') : lastRun ? (lastRun.status === 'completed' ? 'Completed' : lastRun.status === 'failed' ? 'Failed' : lastRun.status === 'stopped' ? 'Stopped' : lastRun.status) : 'Not run yet';
|
|
289
|
+
const trigger = _getPipelineTriggerLabel(p);
|
|
269
290
|
|
|
270
291
|
// Build node chain (renders for all pipelines, even never-run)
|
|
271
292
|
var progressHtml = '';
|
|
@@ -278,12 +299,18 @@ function renderPipelines(pipelines) {
|
|
|
278
299
|
var allResources = _collectMonitoredResources(p);
|
|
279
300
|
var resourcesHtml = _renderMonitoredResources(allResources, { compact: true });
|
|
280
301
|
|
|
281
|
-
return '<div
|
|
282
|
-
'<div
|
|
283
|
-
'<
|
|
284
|
-
|
|
285
|
-
'<
|
|
286
|
-
|
|
302
|
+
return '<div class="pipeline-card" onclick="if(shouldIgnoreSelectionClick(event))return;openPipelineDetail(\'' + escHtml(p.id) + '\')">' +
|
|
303
|
+
'<div class="pipeline-card-header">' +
|
|
304
|
+
'<div class="pipeline-card-main">' +
|
|
305
|
+
'<strong class="pipeline-card-title">' + escHtml(p.title || p.id) + '</strong>' +
|
|
306
|
+
'<div class="pipeline-card-meta">' +
|
|
307
|
+
'<span>' + escHtml(p.id) + '</span>' +
|
|
308
|
+
'<span>' + escHtml(_getPipelineStageLabel(p)) + '</span>' +
|
|
309
|
+
'<span>' + escHtml(trigger) + '</span>' +
|
|
310
|
+
'</div>' +
|
|
311
|
+
'</div>' +
|
|
312
|
+
'<div class="pipeline-card-badges">' +
|
|
313
|
+
'<span style="color:' + statusColor + ';font-size:11px;font-weight:600">' + escHtml(statusLabel) + '</span>' +
|
|
287
314
|
(p.stopWhen ? '<span style="font-size:9px;color:var(--yellow)" title="Auto-stops when condition met: ' + escHtml(typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || 'condition')) + '">STOP-WHEN</span>' : '') +
|
|
288
315
|
(p.enabled === false ? '<span style="font-size:9px;color:var(--red)"' + (p._stopReason ? ' title="' + escHtml(p._stopReason) + '"' : '') + '>' + (p._stoppedBy ? 'AUTO-STOPPED' : 'DISABLED') + '</span>' : '') +
|
|
289
316
|
'</div>' +
|
|
@@ -302,9 +329,9 @@ function openPipelineDetail(id) {
|
|
|
302
329
|
var html = '<div style="display:flex;flex-direction:column;gap:12px">';
|
|
303
330
|
|
|
304
331
|
// Status + actions
|
|
305
|
-
var activeRun = (p
|
|
332
|
+
var activeRun = _getPipelineActiveRun(p);
|
|
306
333
|
html += '<div style="display:flex;justify-content:space-between;align-items:center">' +
|
|
307
|
-
'<span style="font-size:10px;color:var(--muted)">' + (p.trigger?.cron ? escHtml(_cronToHuman(p.trigger.cron)) + ' <span style="opacity:0.6">(' + escHtml(p.trigger.cron) + ', ' + escHtml(Intl.DateTimeFormat().resolvedOptions().timeZone) + ')</span>' : 'Manual trigger') + '</span>' +
|
|
334
|
+
'<span style="font-size:10px;color:var(--muted)">' + (p.trigger?.cron ? escHtml(_cronToHuman(p.trigger.cron)) + ' <span style="opacity:0.6">(' + escHtml(p.trigger.cron) + ', ' + escHtml(Intl.DateTimeFormat().resolvedOptions().timeZone) + ')</span>' : 'Manual trigger') + ' · ' + escHtml(_getPipelineStageLabel(p)) + '</span>' +
|
|
308
335
|
'<div style="display:flex;gap:6px">' +
|
|
309
336
|
(activeRun
|
|
310
337
|
? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_abortPipeline(\'' + escHtml(id) + '\',this)">Abort</button>' +
|
|
@@ -335,7 +362,7 @@ function openPipelineDetail(id) {
|
|
|
335
362
|
var swLabel = typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || JSON.stringify(p.stopWhen));
|
|
336
363
|
html += '<div style="border:1px solid color-mix(in srgb, var(--yellow) 30%, transparent);border-radius:6px;padding:4px 10px;background:color-mix(in srgb, var(--yellow) 6%, transparent);font-size:11px">' +
|
|
337
364
|
'<span style="color:var(--yellow);font-weight:600">Stop When:</span> <span style="color:var(--text)">' + escHtml(swLabel) + '</span>' +
|
|
338
|
-
|
|
365
|
+
(p._stoppedBy ? ' <span style="color:var(--green);font-size:10px">\u2714 triggered' + (p._stoppedAt ? ' at ' + escHtml(formatLocalDateTime(p._stoppedAt)) : '') + '</span>' : '') +
|
|
339
366
|
'</div>';
|
|
340
367
|
}
|
|
341
368
|
if (p._stopReason && p.enabled === false) {
|
|
@@ -379,13 +406,15 @@ function openPipelineDetail(id) {
|
|
|
379
406
|
html += '<div style="font-size:10px">' +
|
|
380
407
|
'<div style="display:flex;gap:8px;align-items:center">' +
|
|
381
408
|
'<span style="color:' + color + ';font-weight:600">' + r.status + '</span>' +
|
|
382
|
-
'<span style="color:var(--muted)">' + (r.startedAt ?
|
|
383
|
-
(r.completedAt ? '<span style="color:var(--muted)">\u2192 ' +
|
|
409
|
+
'<span style="color:var(--muted)">' + (r.startedAt ? formatLocalDateTime(r.startedAt) : '') + '</span>' +
|
|
410
|
+
(r.completedAt ? '<span style="color:var(--muted)">\u2192 ' + formatLocalDateTime(r.completedAt) + '</span>' : '') +
|
|
384
411
|
(artifactCount > 0 ? '<span style="color:var(--blue);cursor:pointer;user-select:none" onclick="var el=document.getElementById(\'' + toggleId + '\');el.style.display=el.style.display===\'none\'?\'flex\':\'none\'" title="Toggle artifacts">' + artifactCount + ' artifact' + (artifactCount !== 1 ? 's' : '') + ' ▾</span>' : '') +
|
|
385
412
|
'</div>' +
|
|
386
413
|
(artifactCount > 0 ? '<div id="' + toggleId + '" style="display:none;flex-wrap:wrap;gap:4px;margin-top:4px;margin-left:12px">' + _renderArtifactLinks(runArtifacts.merged, id) + '</div>' : '') +
|
|
387
414
|
'</div>';
|
|
388
415
|
});
|
|
416
|
+
} else {
|
|
417
|
+
html += '<div class="pipeline-empty-runs">' + escHtml(_getPipelineEmptyRunCopy(p)) + '</div>';
|
|
389
418
|
}
|
|
390
419
|
|
|
391
420
|
html += '</div>';
|
|
@@ -498,7 +498,7 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
|
|
|
498
498
|
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planDelete(\'' + escapeHtml(normalizedFile) + '\')">Delete</button>';
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' +
|
|
501
|
+
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + formatLocalDateTime(lastMod) + '</div>' : '';
|
|
502
502
|
const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' + modalActions + '</div>';
|
|
503
503
|
|
|
504
504
|
document.getElementById('modal-title').innerHTML = escapeHtml(title) + (versionLabel ? ' <span style="font-size:11px;font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escapeHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
|
|
@@ -589,7 +589,7 @@ function openArchivedPrdModal() {
|
|
|
589
589
|
html += groups.map((g, i) => {
|
|
590
590
|
const done = g.items.filter(it => it.status === 'done').length;
|
|
591
591
|
const failed = g.items.filter(it => it.status === 'failed').length;
|
|
592
|
-
const completed = g.completedAt ?
|
|
592
|
+
const completed = g.completedAt ? formatLocalDate(g.completedAt) : '';
|
|
593
593
|
return '<div class="plan-card" style="cursor:pointer;margin-bottom:8px" onclick="if(shouldIgnoreSelectionClick(event))return;showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">' +
|
|
594
594
|
'<div class="plan-card-title" style="font-size:13px">' + escHtml(g.summary || g.file) + '</div>' +
|
|
595
595
|
'<div class="plan-card-meta">' +
|
|
@@ -18,7 +18,7 @@ function _cronToHuman(cron) {
|
|
|
18
18
|
const m = parseInt(minute, 10);
|
|
19
19
|
if (isNaN(h) || isNaN(m)) return cron;
|
|
20
20
|
|
|
21
|
-
const timeStr =
|
|
21
|
+
const timeStr = formatLocalClock(h, m);
|
|
22
22
|
|
|
23
23
|
if (dow === '*') return 'Daily at ' + timeStr;
|
|
24
24
|
|
|
@@ -332,7 +332,7 @@ function openScheduleDetail(id) {
|
|
|
332
332
|
const s = (window._lastSchedules || []).find(x => x.id === id);
|
|
333
333
|
if (!s) return;
|
|
334
334
|
const humanCron = _cronToHuman(s.cron || '');
|
|
335
|
-
const lastRun = s._lastRun ?
|
|
335
|
+
const lastRun = s._lastRun ? formatLocalDateTime(s._lastRun) : 'never';
|
|
336
336
|
const enabledLabel = s.enabled ? '<span class="pr-badge approved">enabled</span>' : '<span class="pr-badge rejected">disabled</span>';
|
|
337
337
|
|
|
338
338
|
document.getElementById('modal-title').innerHTML = escHtml(s.title || s.id) +
|
|
@@ -137,9 +137,9 @@ function openWatchDetail(id) {
|
|
|
137
137
|
var w = (window._lastWatches || []).find(function(x) { return x.id === id; });
|
|
138
138
|
if (!w) return;
|
|
139
139
|
var statusBadge = _WATCH_STATUS_BADGES[w.status] || escHtml(w.status || '');
|
|
140
|
-
var lastChecked = w.last_checked ?
|
|
141
|
-
var lastTriggered = w.last_triggered ?
|
|
142
|
-
var createdAt = w.created_at ?
|
|
140
|
+
var lastChecked = w.last_checked ? formatLocalDateTime(w.last_checked) : 'never';
|
|
141
|
+
var lastTriggered = w.last_triggered ? formatLocalDateTime(w.last_triggered) : 'never';
|
|
142
|
+
var createdAt = w.created_at ? formatLocalDateTime(w.created_at) : 'unknown';
|
|
143
143
|
var targetLabel = _WATCH_TARGET_LABELS[w.targetType] || w.targetType;
|
|
144
144
|
var condLabel = _WATCH_CONDITION_LABELS[w.condition] || w.condition;
|
|
145
145
|
|
|
@@ -458,9 +458,9 @@ function openWorkItemDetail(id) {
|
|
|
458
458
|
html += field('Description', '<div style="font-size:12px;max-height:320px;overflow-y:auto;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)">' + renderMd(item.description || item.title || '—') + '</div>');
|
|
459
459
|
html += field('Agent', escapeHtml(item.dispatched_to || item.agent || 'Auto'));
|
|
460
460
|
html += field('Source', escapeHtml(item._source || 'central'));
|
|
461
|
-
if (item.created) html += field('Created', escapeHtml(
|
|
462
|
-
if (item.dispatched_at) html += field('Dispatched', escapeHtml(
|
|
463
|
-
if (item.completedAt) html += field('Completed', escapeHtml(
|
|
461
|
+
if (item.created) html += field('Created', escapeHtml(formatLocalDateTime(item.created)));
|
|
462
|
+
if (item.dispatched_at) html += field('Dispatched', escapeHtml(formatLocalDateTime(item.dispatched_at)) + ' to ' + escapeHtml(item.dispatched_to || '?'));
|
|
463
|
+
if (item.completedAt) html += field('Completed', escapeHtml(formatLocalDateTime(item.completedAt)));
|
|
464
464
|
if (item.failReason) html += field('Failure Reason', '<span style="color:var(--red)">' + escapeHtml(item.failReason) + '</span>');
|
|
465
465
|
if (item._pendingReason && item.status === 'pending') html += field('Pending Reason', item._pendingReason === 'already_dispatched' ? 'Queued — waiting for available agent slot' : escapeHtml(item._pendingReason.replace(/_/g, ' ')));
|
|
466
466
|
if (item._skipReason && item.status === 'pending') html += field('Dispatch Blocked', '<span style="color:var(--yellow)">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + '</span>' + (item._blockedBy ? ' — blocked by <strong>' + escapeHtml(item._blockedBy) + '</strong>' : ''));
|
package/dashboard/js/utils.js
CHANGED
|
@@ -151,6 +151,40 @@ function timeAgo(isoStr) {
|
|
|
151
151
|
return d + 'd ago';
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
const _LOCAL_DATE_TIME_OPTS = { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' };
|
|
155
|
+
const _LOCAL_DATE_OPTS = { month: 'short', day: 'numeric', year: 'numeric' };
|
|
156
|
+
const _LOCAL_TIME_OPTS = { hour: 'numeric', minute: '2-digit' };
|
|
157
|
+
|
|
158
|
+
function _coerceDashboardDate(value) {
|
|
159
|
+
if (!value) return null;
|
|
160
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
161
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatLocalDateTime(value) {
|
|
165
|
+
const date = _coerceDashboardDate(value);
|
|
166
|
+
if (!date) return value ? String(value) : '';
|
|
167
|
+
return date.toLocaleString(undefined, _LOCAL_DATE_TIME_OPTS);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatLocalDate(value) {
|
|
171
|
+
const date = _coerceDashboardDate(value);
|
|
172
|
+
if (!date) return value ? String(value) : '';
|
|
173
|
+
return date.toLocaleDateString(undefined, _LOCAL_DATE_OPTS);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatLocalTime(value) {
|
|
177
|
+
const date = _coerceDashboardDate(value);
|
|
178
|
+
if (!date) return value ? String(value) : '';
|
|
179
|
+
return date.toLocaleTimeString(undefined, _LOCAL_TIME_OPTS);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatLocalClock(hour, minute) {
|
|
183
|
+
const date = new Date();
|
|
184
|
+
date.setHours(Number(hour) || 0, Number(minute) || 0, 0, 0);
|
|
185
|
+
return formatLocalTime(date);
|
|
186
|
+
}
|
|
187
|
+
|
|
154
188
|
function statusColor(s) {
|
|
155
189
|
return s === 'working' ? 'working' : s === 'done' ? 'done' : s === 'error' ? 'error' : '';
|
|
156
190
|
}
|
package/dashboard/styles.css
CHANGED
|
@@ -207,6 +207,14 @@
|
|
|
207
207
|
.pl-node-arrow { display: flex; align-items: center; padding: 0 2px; color: var(--muted); font-size: 12px; flex-shrink: 0; margin-top: 8px; }
|
|
208
208
|
.pl-node-meta { font-size: 9px; color: var(--muted); margin-top: 2px; text-align: center; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
|
|
209
209
|
.pl-node-loop { font-size: 10px; color: var(--muted); margin-top: 6px; display: flex; align-items: center; gap: 6px; }
|
|
210
|
+
.pipeline-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 12px 16px; margin-bottom: var(--space-4); cursor: pointer; transition: border-color var(--transition-fast), transform var(--transition-fast); }
|
|
211
|
+
.pipeline-card:hover { border-color: var(--blue); transform: translateY(-1px); }
|
|
212
|
+
.pipeline-card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-6); }
|
|
213
|
+
.pipeline-card-main { min-width: 0; flex: 1; }
|
|
214
|
+
.pipeline-card-title { display: block; font-size: var(--text-lg); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
215
|
+
.pipeline-card-meta { margin-top: var(--space-2); display: flex; flex-wrap: wrap; gap: var(--space-4); font-size: var(--text-sm); color: var(--muted); }
|
|
216
|
+
.pipeline-card-badges { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: var(--space-4); flex-shrink: 0; }
|
|
217
|
+
.pipeline-empty-runs { border: 1px dashed var(--border); border-radius: var(--radius-md); padding: var(--space-6); color: var(--muted); font-size: var(--text-md); background: rgba(139,148,158,0.04); }
|
|
210
218
|
.prd-items-list { display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; padding: 0 8px; }
|
|
211
219
|
.prd-item-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: var(--radius-sm); font-size: var(--text-base); background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--border); }
|
|
212
220
|
.prd-item-row.st-done { border-left-color: var(--green); }
|
package/dashboard.js
CHANGED
|
@@ -888,6 +888,7 @@ let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCoun
|
|
|
888
888
|
const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight tracking for parallel CC requests
|
|
889
889
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
890
890
|
const ccLiveStreams = new Map(); // tabId → buffered live stream state for reconnect-after-disconnect
|
|
891
|
+
const CC_CALL_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour — long-running CC orchestration can span many tool calls
|
|
891
892
|
const CC_INFLIGHT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes — auto-release if request hangs
|
|
892
893
|
const CC_LOCK_WAIT_MS = 200; // grace period for previous handler's finally to release lock
|
|
893
894
|
const CC_STREAM_HEARTBEAT_MS = 15000; // keep streaming responses alive across proxies/restart races
|
|
@@ -1581,6 +1582,70 @@ function meetingParticipantsFromAction(action) {
|
|
|
1581
1582
|
);
|
|
1582
1583
|
}
|
|
1583
1584
|
|
|
1585
|
+
function normalizePipelineForCompare(pipeline) {
|
|
1586
|
+
if (!pipeline || typeof pipeline !== 'object') return null;
|
|
1587
|
+
return {
|
|
1588
|
+
title: pipeline.title || '',
|
|
1589
|
+
stages: Array.isArray(pipeline.stages) ? pipeline.stages : [],
|
|
1590
|
+
trigger: pipeline.trigger && typeof pipeline.trigger === 'object' ? pipeline.trigger : {},
|
|
1591
|
+
enabled: pipeline.enabled !== false,
|
|
1592
|
+
stopWhen: pipeline.stopWhen || null,
|
|
1593
|
+
monitoredResources: Array.isArray(pipeline.monitoredResources) ? pipeline.monitoredResources : [],
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function buildPipelineFromAction(action) {
|
|
1598
|
+
const pipeline = {
|
|
1599
|
+
id: String(action.id || '').trim(),
|
|
1600
|
+
title: String(action.title || '').trim(),
|
|
1601
|
+
stages: action.stages,
|
|
1602
|
+
trigger: action.trigger && typeof action.trigger === 'object' ? action.trigger : {},
|
|
1603
|
+
enabled: action.enabled !== false,
|
|
1604
|
+
};
|
|
1605
|
+
if (action.stopWhen) pipeline.stopWhen = action.stopWhen;
|
|
1606
|
+
if (Array.isArray(action.monitoredResources) && action.monitoredResources.length > 0) {
|
|
1607
|
+
pipeline.monitoredResources = action.monitoredResources;
|
|
1608
|
+
}
|
|
1609
|
+
return pipeline;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function pipelineDefinitionsEqual(a, b) {
|
|
1613
|
+
return JSON.stringify(normalizePipelineForCompare(a)) === JSON.stringify(normalizePipelineForCompare(b));
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function createPipelineFromAction(action) {
|
|
1617
|
+
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
1618
|
+
const pipeline = buildPipelineFromAction(action);
|
|
1619
|
+
const existing = getPipeline(pipeline.id);
|
|
1620
|
+
if (existing) {
|
|
1621
|
+
if (pipelineDefinitionsEqual(existing, pipeline)) {
|
|
1622
|
+
return {
|
|
1623
|
+
type: 'create-pipeline',
|
|
1624
|
+
id: pipeline.id,
|
|
1625
|
+
ok: true,
|
|
1626
|
+
duplicate: true,
|
|
1627
|
+
duplicateOf: pipeline.id,
|
|
1628
|
+
warning: `Pipeline "${pipeline.id}" already exists; no changes made.`,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
type: 'create-pipeline',
|
|
1633
|
+
id: pipeline.id,
|
|
1634
|
+
error: `Pipeline "${pipeline.id}" already exists with a different definition. Use edit-pipeline to update it.`,
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
savePipeline(pipeline);
|
|
1638
|
+
const persisted = getPipeline(pipeline.id);
|
|
1639
|
+
if (!persisted) {
|
|
1640
|
+
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" was not persisted.` };
|
|
1641
|
+
}
|
|
1642
|
+
if (!pipelineDefinitionsEqual(persisted, pipeline)) {
|
|
1643
|
+
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" persisted with unexpected contents.` };
|
|
1644
|
+
}
|
|
1645
|
+
invalidateStatusCache();
|
|
1646
|
+
return { type: 'create-pipeline', id: pipeline.id, ok: true, created: true };
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1584
1649
|
// Required-field validator for CC actions. Returns null when valid, an error string when not.
|
|
1585
1650
|
// Centralises field-required checks so the model can't quietly emit a malformed action and have
|
|
1586
1651
|
// the server silently fall back to placeholder values (e.g. "Untitled"). The handler invokes this
|
|
@@ -1615,6 +1680,11 @@ function _ccValidateAction(action) {
|
|
|
1615
1680
|
if (meetingParticipantsFromAction(action).length < 2) return 'create-meeting action requires at least 2 participants';
|
|
1616
1681
|
return null;
|
|
1617
1682
|
}
|
|
1683
|
+
case 'create-pipeline':
|
|
1684
|
+
if (!action.id || typeof action.id !== 'string' || !action.id.trim()) return 'create-pipeline action missing required field: id';
|
|
1685
|
+
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return 'create-pipeline action missing required field: title';
|
|
1686
|
+
if (!Array.isArray(action.stages) || action.stages.length === 0) return 'create-pipeline action requires non-empty stages array';
|
|
1687
|
+
return null;
|
|
1618
1688
|
default:
|
|
1619
1689
|
return null; // unknown types fall through to existing handler / generic fallback
|
|
1620
1690
|
}
|
|
@@ -1853,6 +1923,10 @@ async function executeCCActions(actions) {
|
|
|
1853
1923
|
results.push({ type: 'create-meeting', id: meeting.id, ok: true });
|
|
1854
1924
|
break;
|
|
1855
1925
|
}
|
|
1926
|
+
case 'create-pipeline': {
|
|
1927
|
+
results.push(createPipelineFromAction(action));
|
|
1928
|
+
break;
|
|
1929
|
+
}
|
|
1856
1930
|
case 'delete-watch': {
|
|
1857
1931
|
const deleted = watchesMod.deleteWatch(action.id);
|
|
1858
1932
|
if (deleted) invalidateStatusCache();
|
|
@@ -1997,7 +2071,7 @@ function updateSession(store, key, sessionId, existing) {
|
|
|
1997
2071
|
* @param {number} opts.maxTurns - Max tool-use turns
|
|
1998
2072
|
* @param {string} opts.allowedTools - Comma-separated tool list
|
|
1999
2073
|
*/
|
|
2000
|
-
async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout =
|
|
2074
|
+
async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
|
|
2001
2075
|
if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
2002
2076
|
if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
2003
2077
|
const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
@@ -2086,7 +2160,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2086
2160
|
return result;
|
|
2087
2161
|
}
|
|
2088
2162
|
|
|
2089
|
-
async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout =
|
|
2163
|
+
async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, onChunk, onToolUse, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
|
|
2090
2164
|
if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
2091
2165
|
if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
2092
2166
|
const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
|
|
@@ -5169,7 +5243,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5169
5243
|
function _invokeCcStream({ prompt, sessionId, liveState, toolUses, model, effort, maxTurns, engineConfig }) {
|
|
5170
5244
|
const { callLLMStreaming } = require('./engine/llm');
|
|
5171
5245
|
return callLLMStreaming(prompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
5172
|
-
timeout:
|
|
5246
|
+
timeout: CC_CALL_TIMEOUT_MS, label: 'command-center', model, maxTurns,
|
|
5173
5247
|
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
5174
5248
|
sessionId, effort, direct: true,
|
|
5175
5249
|
engineConfig,
|
|
@@ -6942,6 +7016,7 @@ module.exports = {
|
|
|
6942
7016
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
6943
7017
|
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
6944
7018
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
7019
|
+
_createPipelineFromAction: createPipelineFromAction,
|
|
6945
7020
|
executeCCActions,
|
|
6946
7021
|
};
|
|
6947
7022
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1726",
|
|
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"
|