@yemi33/minions 0.1.1724 → 0.1.1725
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 +2 -1
- package/dashboard/js/command-center.js +25 -6
- 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 +74 -0
- package/engine/copilot-models.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -934,7 +934,13 @@ function ccRetryLast(tabId, retryId) {
|
|
|
934
934
|
|
|
935
935
|
async function _ccFetch(url, body) {
|
|
936
936
|
var res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
937
|
-
if (!res.ok) {
|
|
937
|
+
if (!res.ok) {
|
|
938
|
+
var d = await res.json().catch(function() { return {}; });
|
|
939
|
+
var err = new Error(d.error || 'Request failed (' + res.status + ')');
|
|
940
|
+
err.status = res.status;
|
|
941
|
+
err.data = d;
|
|
942
|
+
throw err;
|
|
943
|
+
}
|
|
938
944
|
return res;
|
|
939
945
|
}
|
|
940
946
|
|
|
@@ -946,6 +952,8 @@ function _tagServerExecuted(actions, actionResults) {
|
|
|
946
952
|
if (r && r.ok) {
|
|
947
953
|
actions[i]._serverExecuted = true;
|
|
948
954
|
if (r.id) actions[i]._serverId = r.id;
|
|
955
|
+
if (r.warning) actions[i]._serverWarning = r.warning;
|
|
956
|
+
if (r.duplicate) actions[i]._serverDuplicate = true;
|
|
949
957
|
} else if (r && r.error) {
|
|
950
958
|
actions[i]._serverExecuted = true;
|
|
951
959
|
actions[i]._serverError = r.error;
|
|
@@ -965,8 +973,10 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
965
973
|
status.style.color = 'var(--red)';
|
|
966
974
|
} else {
|
|
967
975
|
var label = action._serverId ? escHtml(action._serverId) : escHtml(action.title || action.type);
|
|
968
|
-
status.innerHTML = '✓ ' + escHtml(action.type) + ': <strong>' + label + '</strong>'
|
|
969
|
-
|
|
976
|
+
status.innerHTML = '✓ ' + escHtml(action.type) + ': <strong>' + label + '</strong>' +
|
|
977
|
+
(action._serverDuplicate ? ' <span style="color:var(--orange)">already exists</span>' : '') +
|
|
978
|
+
(action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '');
|
|
979
|
+
status.style.color = action._serverDuplicate ? 'var(--orange)' : 'var(--green)';
|
|
970
980
|
}
|
|
971
981
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
972
982
|
if (['dispatch','fix','implement','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
|
|
@@ -1285,9 +1295,18 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1285
1295
|
break;
|
|
1286
1296
|
}
|
|
1287
1297
|
case 'create-pipeline': {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1298
|
+
try {
|
|
1299
|
+
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 });
|
|
1300
|
+
status.innerHTML = '✓ Pipeline created: <strong>' + escHtml(action.id) + '</strong>';
|
|
1301
|
+
status.style.color = 'var(--green)';
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
if (e.status === 409) {
|
|
1304
|
+
status.innerHTML = '✓ Pipeline already exists: <strong>' + escHtml(action.id) + '</strong>';
|
|
1305
|
+
status.style.color = 'var(--orange)';
|
|
1306
|
+
} else {
|
|
1307
|
+
throw e;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1291
1310
|
break;
|
|
1292
1311
|
}
|
|
1293
1312
|
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
|
@@ -1581,6 +1581,70 @@ function meetingParticipantsFromAction(action) {
|
|
|
1581
1581
|
);
|
|
1582
1582
|
}
|
|
1583
1583
|
|
|
1584
|
+
function normalizePipelineForCompare(pipeline) {
|
|
1585
|
+
if (!pipeline || typeof pipeline !== 'object') return null;
|
|
1586
|
+
return {
|
|
1587
|
+
title: pipeline.title || '',
|
|
1588
|
+
stages: Array.isArray(pipeline.stages) ? pipeline.stages : [],
|
|
1589
|
+
trigger: pipeline.trigger && typeof pipeline.trigger === 'object' ? pipeline.trigger : {},
|
|
1590
|
+
enabled: pipeline.enabled !== false,
|
|
1591
|
+
stopWhen: pipeline.stopWhen || null,
|
|
1592
|
+
monitoredResources: Array.isArray(pipeline.monitoredResources) ? pipeline.monitoredResources : [],
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function buildPipelineFromAction(action) {
|
|
1597
|
+
const pipeline = {
|
|
1598
|
+
id: String(action.id || '').trim(),
|
|
1599
|
+
title: String(action.title || '').trim(),
|
|
1600
|
+
stages: action.stages,
|
|
1601
|
+
trigger: action.trigger && typeof action.trigger === 'object' ? action.trigger : {},
|
|
1602
|
+
enabled: action.enabled !== false,
|
|
1603
|
+
};
|
|
1604
|
+
if (action.stopWhen) pipeline.stopWhen = action.stopWhen;
|
|
1605
|
+
if (Array.isArray(action.monitoredResources) && action.monitoredResources.length > 0) {
|
|
1606
|
+
pipeline.monitoredResources = action.monitoredResources;
|
|
1607
|
+
}
|
|
1608
|
+
return pipeline;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function pipelineDefinitionsEqual(a, b) {
|
|
1612
|
+
return JSON.stringify(normalizePipelineForCompare(a)) === JSON.stringify(normalizePipelineForCompare(b));
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function createPipelineFromAction(action) {
|
|
1616
|
+
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
1617
|
+
const pipeline = buildPipelineFromAction(action);
|
|
1618
|
+
const existing = getPipeline(pipeline.id);
|
|
1619
|
+
if (existing) {
|
|
1620
|
+
if (pipelineDefinitionsEqual(existing, pipeline)) {
|
|
1621
|
+
return {
|
|
1622
|
+
type: 'create-pipeline',
|
|
1623
|
+
id: pipeline.id,
|
|
1624
|
+
ok: true,
|
|
1625
|
+
duplicate: true,
|
|
1626
|
+
duplicateOf: pipeline.id,
|
|
1627
|
+
warning: `Pipeline "${pipeline.id}" already exists; no changes made.`,
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
type: 'create-pipeline',
|
|
1632
|
+
id: pipeline.id,
|
|
1633
|
+
error: `Pipeline "${pipeline.id}" already exists with a different definition. Use edit-pipeline to update it.`,
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
savePipeline(pipeline);
|
|
1637
|
+
const persisted = getPipeline(pipeline.id);
|
|
1638
|
+
if (!persisted) {
|
|
1639
|
+
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" was not persisted.` };
|
|
1640
|
+
}
|
|
1641
|
+
if (!pipelineDefinitionsEqual(persisted, pipeline)) {
|
|
1642
|
+
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" persisted with unexpected contents.` };
|
|
1643
|
+
}
|
|
1644
|
+
invalidateStatusCache();
|
|
1645
|
+
return { type: 'create-pipeline', id: pipeline.id, ok: true, created: true };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1584
1648
|
// Required-field validator for CC actions. Returns null when valid, an error string when not.
|
|
1585
1649
|
// Centralises field-required checks so the model can't quietly emit a malformed action and have
|
|
1586
1650
|
// the server silently fall back to placeholder values (e.g. "Untitled"). The handler invokes this
|
|
@@ -1615,6 +1679,11 @@ function _ccValidateAction(action) {
|
|
|
1615
1679
|
if (meetingParticipantsFromAction(action).length < 2) return 'create-meeting action requires at least 2 participants';
|
|
1616
1680
|
return null;
|
|
1617
1681
|
}
|
|
1682
|
+
case 'create-pipeline':
|
|
1683
|
+
if (!action.id || typeof action.id !== 'string' || !action.id.trim()) return 'create-pipeline action missing required field: id';
|
|
1684
|
+
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return 'create-pipeline action missing required field: title';
|
|
1685
|
+
if (!Array.isArray(action.stages) || action.stages.length === 0) return 'create-pipeline action requires non-empty stages array';
|
|
1686
|
+
return null;
|
|
1618
1687
|
default:
|
|
1619
1688
|
return null; // unknown types fall through to existing handler / generic fallback
|
|
1620
1689
|
}
|
|
@@ -1853,6 +1922,10 @@ async function executeCCActions(actions) {
|
|
|
1853
1922
|
results.push({ type: 'create-meeting', id: meeting.id, ok: true });
|
|
1854
1923
|
break;
|
|
1855
1924
|
}
|
|
1925
|
+
case 'create-pipeline': {
|
|
1926
|
+
results.push(createPipelineFromAction(action));
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1856
1929
|
case 'delete-watch': {
|
|
1857
1930
|
const deleted = watchesMod.deleteWatch(action.id);
|
|
1858
1931
|
if (deleted) invalidateStatusCache();
|
|
@@ -6942,6 +7015,7 @@ module.exports = {
|
|
|
6942
7015
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
6943
7016
|
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
6944
7017
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
7018
|
+
_createPipelineFromAction: createPipelineFromAction,
|
|
6945
7019
|
executeCCActions,
|
|
6946
7020
|
};
|
|
6947
7021
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1725",
|
|
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"
|