@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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1726 (2026-05-05)
4
+
5
+ ### Other
6
+ - Extend command center timeout
7
+
3
8
  ## 0.1.1724 (2026-05-05)
4
9
 
5
10
  ### Features
@@ -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(960000)
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) { var d = await res.json().catch(function() { return {}; }); throw new Error(d.error || 'Request failed (' + res.status + ')'); }
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 = '&#10003; ' + escHtml(action.type) + ': <strong>' + label + '</strong>';
969
- status.style.color = 'var(--green)';
977
+ status.innerHTML = '&#10003; ' + 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
- 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 });
1289
- status.innerHTML = '&#10003; Pipeline created: <strong>' + escHtml(action.id) + '</strong>';
1290
- status.style.color = 'var(--green)';
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 = '&#10003; Pipeline created: <strong>' + escHtml(action.id) + '</strong>';
1302
+ status.style.color = 'var(--green)';
1303
+ } catch (e) {
1304
+ if (e.status === 409) {
1305
+ status.innerHTML = '&#10003; 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>' + date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) + '</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 ? new Date(d.completed_at).toLocaleString() : '') + '</td>' +
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>';
@@ -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 = new Date(data.timestamp).toLocaleTimeString();
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 = new Date(adoThrottle.retryAfter).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
78
+ const resumeTime = formatLocalTime(adoThrottle.retryAfter);
79
79
  el.innerHTML =
80
80
  '<span class="engine-alert-msg">&#x26A0;&#xFE0F; 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 = new Date(ghThrottle.retryAfter).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
92
+ const resumeTime = formatLocalTime(ghThrottle.retryAfter);
93
93
  el.innerHTML =
94
94
  '<span class="engine-alert-msg">&#x26A0;&#xFE0F; 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
- try { return new Date(t).toLocaleTimeString(); } catch { return t; }
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
- const d = new Date(updatedAt);
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 ? new Date(dt).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';
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
- // Loop indicator — only for pipelines with stopWhen or condition stages (repeat-until pattern)
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
- if (hasStopWhen || hasConditionStage) {
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.runs || []).find(function(r) { return r.status === 'running'; });
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) : 'Never run';
268
- const trigger = p.trigger?.cron ? _cronToHuman(p.trigger.cron) : 'Manual';
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 style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openPipelineDetail(\'' + escHtml(p.id) + '\')">' +
282
- '<div style="display:flex;justify-content:space-between;align-items:center">' +
283
- '<strong style="font-size:13px">' + escHtml(p.title) + '</strong>' +
284
- '<div style="display:flex;align-items:center;gap:8px">' +
285
- '<span style="color:' + statusColor + ';font-size:11px;font-weight:600">' + statusLabel + '</span>' +
286
- '<span style="font-size:10px;color:var(--muted)">' + escHtml(trigger) + '</span>' +
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.runs || []).find(function(r) { return r.status === 'running'; });
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
- (p._stoppedBy ? ' <span style="color:var(--green);font-size:10px">\u2714 triggered' + (p._stoppedAt ? ' at ' + escHtml(p._stoppedAt.slice(0, 16).replace('T', ' ')) : '') + '</span>' : '') +
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 ? new Date(r.startedAt).toLocaleString() : '') + '</span>' +
383
- (r.completedAt ? '<span style="color:var(--muted)">\u2192 ' + new Date(r.completedAt).toLocaleString() + '</span>' : '') +
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: ' + new Date(lastMod).toLocaleString() + '</div>' : '';
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 ? new Date(g.completedAt).toLocaleDateString() : '';
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 = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
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 ? new Date(s._lastRun).toLocaleString() : 'never';
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 ? new Date(w.last_checked).toLocaleString() : 'never';
141
- var lastTriggered = w.last_triggered ? new Date(w.last_triggered).toLocaleString() : 'never';
142
- var createdAt = w.created_at ? new Date(w.created_at).toLocaleString() : 'unknown';
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(new Date(item.created).toLocaleString()));
462
- if (item.dispatched_at) html += field('Dispatched', escapeHtml(new Date(item.dispatched_at).toLocaleString()) + ' to ' + escapeHtml(item.dispatched_to || '?'));
463
- if (item.completedAt) html += field('Completed', escapeHtml(new Date(item.completedAt).toLocaleString()));
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>' : ''));
@@ -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
  }
@@ -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 = 900000, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
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 = 900000, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, onChunk, onToolUse, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
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: 900000, label: 'command-center', model, maxTurns,
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
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-05T09:09:53.466Z"
4
+ "cachedAt": "2026-05-05T09:13:43.248Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1724",
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"