@yemi33/minions 0.1.1972 → 0.1.1974
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/js/command-center.js +89 -3
- package/dashboard/styles.css +5 -2
- package/engine/cli.js +8 -1
- package/engine/spawn-agent.js +36 -16
- package/package.json +1 -1
|
@@ -485,17 +485,103 @@ function ccRenderTabBar() {
|
|
|
485
485
|
for (var i = 0; i < _ccTabs.length; i++) {
|
|
486
486
|
var t = _ccTabs[i];
|
|
487
487
|
var isActive = t.id === _ccActiveTabId;
|
|
488
|
-
|
|
488
|
+
// draggable="true" + DnD handlers enable click-and-drag reorder of tabs.
|
|
489
|
+
// The handlers below splice _ccTabs in place (preserving per-tab in-flight
|
|
490
|
+
// state: _sending, _queue, _abortController, _streamedText, _toolsUsed)
|
|
491
|
+
// and persist via ccSaveState. The close X / new-tab + opt out of drag
|
|
492
|
+
// with draggable="false" + ondragstart preventDefault so the affordances
|
|
493
|
+
// don't accidentally start a drag.
|
|
494
|
+
html += '<div class="cc-tab' + (isActive ? ' active' : '') + (t._sending ? ' working' : '') + '" draggable="true"'
|
|
495
|
+
+ ' ondragstart="ccTabDragStart(event, \'' + t.id + '\')"'
|
|
496
|
+
+ ' ondragover="ccTabDragOver(event, \'' + t.id + '\')"'
|
|
497
|
+
+ ' ondragleave="ccTabDragLeave(event)"'
|
|
498
|
+
+ ' ondrop="ccTabDrop(event, \'' + t.id + '\')"'
|
|
499
|
+
+ ' ondragend="ccTabDragEnd(event)"'
|
|
500
|
+
+ ' onclick="ccSwitchTab(\'' + t.id + '\')" title="' + escHtml(t.title) + '">';
|
|
489
501
|
html += '<span class="cc-tab-text">' + escHtml(t.title) + '</span>';
|
|
490
502
|
if (t._unread) html += '<span class="notif-badge done"></span>';
|
|
491
|
-
html += '<span class="cc-tab-close" onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\')">×</span>';
|
|
503
|
+
html += '<span class="cc-tab-close" draggable="false" ondragstart="event.preventDefault();event.stopPropagation();" onmousedown="event.stopPropagation();" onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\')">×</span>';
|
|
492
504
|
html += '</div>';
|
|
493
505
|
}
|
|
494
|
-
html += '<div class="cc-tab cc-tab-new" onclick="ccNewTab()" title="New tab">+</div>';
|
|
506
|
+
html += '<div class="cc-tab cc-tab-new" draggable="false" ondragstart="event.preventDefault();event.stopPropagation();" onclick="ccNewTab()" title="New tab">+</div>';
|
|
495
507
|
html += '</div>';
|
|
496
508
|
bar.innerHTML = html;
|
|
497
509
|
}
|
|
498
510
|
|
|
511
|
+
// ── Tab drag-to-reorder ─────────────────────────────────────────────────────
|
|
512
|
+
// Native HTML5 DnD: dragstart records the source tab id, dragover on a peer
|
|
513
|
+
// tab calls preventDefault to allow drop and adds a drop-target indicator,
|
|
514
|
+
// drop splices _ccTabs in place so per-tab references (and their in-flight
|
|
515
|
+
// state) are preserved, and dragend clears any leftover visual state.
|
|
516
|
+
var _ccDragSourceId = null;
|
|
517
|
+
|
|
518
|
+
function ccTabDragStart(ev, id) {
|
|
519
|
+
_ccDragSourceId = id;
|
|
520
|
+
try {
|
|
521
|
+
ev.dataTransfer.effectAllowed = 'move';
|
|
522
|
+
// setData is required for Firefox to actually start the drag.
|
|
523
|
+
ev.dataTransfer.setData('text/plain', id);
|
|
524
|
+
} catch {}
|
|
525
|
+
var el = ev.currentTarget;
|
|
526
|
+
if (el && el.classList) el.classList.add('cc-tab-dragging');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function ccTabDragOver(ev, id) {
|
|
530
|
+
if (!_ccDragSourceId) return;
|
|
531
|
+
// preventDefault must fire on every dragover (including the source) so the
|
|
532
|
+
// drop event actually fires — required to detect "released on same tab".
|
|
533
|
+
ev.preventDefault();
|
|
534
|
+
try { ev.dataTransfer.dropEffect = 'move'; } catch {}
|
|
535
|
+
if (_ccDragSourceId === id) return;
|
|
536
|
+
var el = ev.currentTarget;
|
|
537
|
+
if (el && el.classList) el.classList.add('cc-tab-drop-target');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function ccTabDragLeave(ev) {
|
|
541
|
+
var el = ev.currentTarget;
|
|
542
|
+
if (el && el.classList) el.classList.remove('cc-tab-drop-target');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function ccTabDrop(ev, targetId) {
|
|
546
|
+
ev.preventDefault();
|
|
547
|
+
var srcId = _ccDragSourceId;
|
|
548
|
+
// Clear all visual indicators before any early-return.
|
|
549
|
+
var nodes = document.querySelectorAll('.cc-tab-drop-target, .cc-tab-dragging');
|
|
550
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
551
|
+
nodes[i].classList.remove('cc-tab-drop-target');
|
|
552
|
+
nodes[i].classList.remove('cc-tab-dragging');
|
|
553
|
+
}
|
|
554
|
+
// Released on the source tab (or no source): treat as a click — activate
|
|
555
|
+
// the tab and skip any reorder/state churn. ccSwitchTab is a no-op when
|
|
556
|
+
// the target is already active.
|
|
557
|
+
if (!srcId || srcId === targetId) {
|
|
558
|
+
if (targetId) ccSwitchTab(targetId);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
var fromIdx = _ccTabs.findIndex(function(t) { return t.id === srcId; });
|
|
562
|
+
var toIdx = _ccTabs.findIndex(function(t) { return t.id === targetId; });
|
|
563
|
+
if (fromIdx === -1 || toIdx === -1) return;
|
|
564
|
+
// Splice the existing tab reference so per-tab in-flight state survives
|
|
565
|
+
// (_sending, _queue, _abortController, _streamedText, _toolsUsed,
|
|
566
|
+
// _retryRequests, _sendStartedAt, etc.). Do NOT clone/replace the object.
|
|
567
|
+
var moved = _ccTabs.splice(fromIdx, 1)[0];
|
|
568
|
+
_ccTabs.splice(toIdx, 0, moved);
|
|
569
|
+
// _ccActiveTabId is intentionally unchanged: the same tab stays active,
|
|
570
|
+
// just at a new position.
|
|
571
|
+
ccRenderTabBar();
|
|
572
|
+
ccSaveState();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function ccTabDragEnd(_ev) {
|
|
576
|
+
_ccDragSourceId = null;
|
|
577
|
+
// Defensive cleanup: dragend always fires, even if drop didn't.
|
|
578
|
+
var nodes = document.querySelectorAll('.cc-tab-dragging, .cc-tab-drop-target');
|
|
579
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
580
|
+
nodes[i].classList.remove('cc-tab-dragging');
|
|
581
|
+
nodes[i].classList.remove('cc-tab-drop-target');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
499
585
|
function ccRestoreMessages() {
|
|
500
586
|
var el = document.getElementById('cc-messages');
|
|
501
587
|
var tab = _ccActiveTab();
|
package/dashboard/styles.css
CHANGED
|
@@ -654,8 +654,11 @@
|
|
|
654
654
|
|
|
655
655
|
/* Command Center tab bar */
|
|
656
656
|
.cc-tab-scroll { display: flex; gap: 4px; align-items: center; overflow-x: auto; overflow-y: hidden; flex: 1 1 auto; min-width: 0; scrollbar-width: thin; }
|
|
657
|
-
.cc-tab { padding: 4px 10px; font-size: 10px; border: 1px solid var(--border); border-bottom: none; border-radius: 6px 6px 0 0; background: var(--surface2); color: var(--muted); cursor:
|
|
658
|
-
.cc-tab
|
|
657
|
+
.cc-tab { padding: 4px 10px; font-size: 10px; border: 1px solid var(--border); border-bottom: none; border-radius: 6px 6px 0 0; background: var(--surface2); color: var(--muted); cursor: grab; white-space: nowrap; max-width: 140px; display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; margin-bottom: -1px; position: relative; }
|
|
658
|
+
.cc-tab:active { cursor: grabbing; }
|
|
659
|
+
.cc-tab.cc-tab-dragging { opacity: 0.5; cursor: grabbing; }
|
|
660
|
+
.cc-tab.cc-tab-drop-target { box-shadow: inset 2px 0 0 0 var(--blue); }
|
|
661
|
+
.cc-tab-new { color: var(--muted); padding: 4px 8px; cursor: pointer; }
|
|
659
662
|
.cc-tab-text { overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
|
|
660
663
|
.cc-tab.active { color: var(--text); background: var(--bg); border-color: var(--border); z-index: 1; font-weight: 600; }
|
|
661
664
|
.cc-tab.working { border-color: var(--blue); }
|
package/engine/cli.js
CHANGED
|
@@ -521,7 +521,14 @@ const commands = {
|
|
|
521
521
|
const savedBranch = normalizeSessionBranch(sj?.branch);
|
|
522
522
|
if (sj?.sessionId && (!expectedBranch || savedBranch === expectedBranch)) {
|
|
523
523
|
sessionId = sj.sessionId;
|
|
524
|
-
} else if (sj?.sessionId && expectedBranch) {
|
|
524
|
+
} else if (sj?.sessionId && expectedBranch && sj?.dispatchId === item.id) {
|
|
525
|
+
// Only warn when the saved session is for THIS dispatch but on the
|
|
526
|
+
// wrong branch — that's a true anomaly worth flagging. The common
|
|
527
|
+
// case — leftover session.json from a previous (now-completed)
|
|
528
|
+
// dispatch on a different branch — is expected and silent, since
|
|
529
|
+
// the engine writes session.json on completion of each dispatch
|
|
530
|
+
// and a fresh dispatch may run on a different branch before
|
|
531
|
+
// saveSession overwrites it (W-mpbn93ou000611b3).
|
|
525
532
|
shared.log('warn', `Reattach: ignoring session for ${agentId} on branch ${savedBranch || 'unknown'}; expected ${expectedBranch}`);
|
|
526
533
|
}
|
|
527
534
|
} catch {}
|
package/engine/spawn-agent.js
CHANGED
|
@@ -529,9 +529,44 @@ function main() {
|
|
|
529
529
|
clearTimeout(startupTimer);
|
|
530
530
|
clearTimeout(initialSnapshotTimer);
|
|
531
531
|
clearInterval(descTimer);
|
|
532
|
+
|
|
533
|
+
// Compute the exit code and write the [process-exit] sentinel FIRST,
|
|
534
|
+
// before the descendant snapshot/reap. The engine's orphan reaper uses
|
|
535
|
+
// the sentinel as the single signal that "the runtime exited cleanly
|
|
536
|
+
// with code N"; if we delay it behind `snapshotDescendants()` (which
|
|
537
|
+
// shells out to `Get-CimInstance` on Windows and can block 1-5+s),
|
|
538
|
+
// there's a window where the runtime PID we track is already dead but
|
|
539
|
+
// no sentinel exists yet. After an engine restart that path triggers
|
|
540
|
+
// `canReapDeadProcess` and the dispatch gets auto-retried as orphaned
|
|
541
|
+
// even though it completed normally. See W-mpbn93ou000611b3 / the
|
|
542
|
+
// 2026-05-18 ripley-explore regression.
|
|
543
|
+
//
|
|
544
|
+
// Prefer the 'exit' event's code/signal when present (Node's 'close'
|
|
545
|
+
// event can report code=0 on Windows when the OS-level exit was
|
|
546
|
+
// non-zero — see the long-form note above the exit handler).
|
|
547
|
+
const effectiveCode = (realExitFromEvent != null) ? realExitFromEvent : code;
|
|
548
|
+
const effectiveSignal = realSignalFromEvent || signal;
|
|
549
|
+
const exitCode = normalizeRuntimeExit(effectiveCode, effectiveSignal);
|
|
550
|
+
if (sentinelWritten) {
|
|
551
|
+
// Defense-in-depth: never write a duplicate sentinel. We observed pairs
|
|
552
|
+
// of [process-exit] code=0 lines in live-output.log across many failed
|
|
553
|
+
// runs, which suggests close has fired twice in some edge cases (e.g.,
|
|
554
|
+
// shim re-launch on Windows). One sentinel per spawn is the contract.
|
|
555
|
+
// Skip descendant reap on the duplicate close too — the first close
|
|
556
|
+
// already handled it (reaping the same PIDs again is a no-op at best,
|
|
557
|
+
// but skipping is faster and matches the prior early-return contract).
|
|
558
|
+
fs.appendFileSync(debugPath, `EXIT (duplicate close, skipping sentinel): code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''}\n`);
|
|
559
|
+
process.exit(exitCode);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
sentinelWritten = true;
|
|
563
|
+
const sentinelResult = writeProcessExitSentinel({ exitCode, signal: effectiveSignal });
|
|
564
|
+
|
|
532
565
|
// Final snapshot + reap, but only when the runtime actually spawned
|
|
533
566
|
// children. Read-only / very short agents (exit before the 3s initial
|
|
534
|
-
// snapshot fires) skip the wmic shell-out entirely.
|
|
567
|
+
// snapshot fires) skip the wmic shell-out entirely. Runs AFTER the
|
|
568
|
+
// sentinel write so a slow Get-CimInstance call can't gate completion
|
|
569
|
+
// detection — see the hoist note above.
|
|
535
570
|
if (trackedDescendants.size || gotFirstOutput) {
|
|
536
571
|
snapshotDescendants();
|
|
537
572
|
if (trackedDescendants.size) {
|
|
@@ -580,21 +615,6 @@ function main() {
|
|
|
580
615
|
try { fs.appendFileSync(debugPath, `DESCENDANTS reaped=${reaped}/${toKillPids.length} kept=${kept.length}\n`); } catch {}
|
|
581
616
|
}
|
|
582
617
|
}
|
|
583
|
-
// Prefer the 'exit' event's code/signal when present — see note above.
|
|
584
|
-
const effectiveCode = (realExitFromEvent != null) ? realExitFromEvent : code;
|
|
585
|
-
const effectiveSignal = realSignalFromEvent || signal;
|
|
586
|
-
const exitCode = normalizeRuntimeExit(effectiveCode, effectiveSignal);
|
|
587
|
-
if (sentinelWritten) {
|
|
588
|
-
// Defense-in-depth: never write a duplicate sentinel. We observed pairs
|
|
589
|
-
// of [process-exit] code=0 lines in live-output.log across many failed
|
|
590
|
-
// runs, which suggests close has fired twice in some edge cases (e.g.,
|
|
591
|
-
// shim re-launch on Windows). One sentinel per spawn is the contract.
|
|
592
|
-
fs.appendFileSync(debugPath, `EXIT (duplicate close, skipping sentinel): code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''}\n`);
|
|
593
|
-
process.exit(exitCode);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
sentinelWritten = true;
|
|
597
|
-
const sentinelResult = writeProcessExitSentinel({ exitCode, signal: effectiveSignal });
|
|
598
618
|
fs.appendFileSync(debugPath, `EXIT: code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''} (close=${code} exit=${realExitFromEvent})\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
|
|
599
619
|
if (!sentinelResult.fileWritten) {
|
|
600
620
|
fs.appendFileSync(debugPath, `EXIT SENTINEL: file write failed for ${process.env.MINIONS_LIVE_OUTPUT_PATH}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1974",
|
|
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"
|