@yemi33/minions 0.1.2438 → 0.1.2440
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 +27 -1
- package/dashboard/js/command-input.js +20 -3
- package/dashboard/js/render-work-items.js +43 -14
- package/dashboard/js/settings.js +21 -0
- package/dashboard/js/utils.js +29 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/cc-queue-store.js +71 -0
- package/dashboard/slim/js/chat.js +20 -1
- package/dashboard/slim/js/command-send.js +2 -0
- package/dashboard/slim/js/modals-tiles.js +117 -19
- package/dashboard/slim/js/settings.js +82 -8
- package/dashboard/slim/js/status.js +15 -6
- package/dashboard/slim/styles.css +13 -0
- package/dashboard-build.js +1 -1
- package/dashboard.js +56 -0
- package/engine/api-contracts/cc-ops.js +56 -0
- package/engine/cc-api-validation.js +54 -0
- package/engine/db/migrations/028-cc-queued-messages.js +29 -0
- package/engine/small-state-store.js +88 -0
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +14 -0
- package/playbooks/build-fix-complex.md +1 -1
- package/playbooks/fix.md +2 -0
- package/playbooks/implement.md +2 -0
- package/playbooks/shared-rules.md +1 -0
|
@@ -760,6 +760,8 @@ function ccSwitchTab(id) {
|
|
|
760
760
|
ccRenderTabBar();
|
|
761
761
|
ccUpdateSessionIndicator();
|
|
762
762
|
ccSaveState();
|
|
763
|
+
_ccRestoreQueue(tab); // load persisted queue once (W-ms4o3gtm005z91c9)
|
|
764
|
+
_renderQueueIndicator(); // surface any in-memory queued bubbles for this tab
|
|
763
765
|
var input = document.getElementById('cc-input');
|
|
764
766
|
if (input) input.focus();
|
|
765
767
|
}
|
|
@@ -914,6 +916,7 @@ function ccRestoreMessages() {
|
|
|
914
916
|
var el = document.getElementById('cc-messages');
|
|
915
917
|
var tab = _ccActiveTab();
|
|
916
918
|
if (!tab) return;
|
|
919
|
+
_ccRestoreQueue(tab);
|
|
917
920
|
if (el.children.length > 0) return;
|
|
918
921
|
if (tab.messages.length === 0) {
|
|
919
922
|
_ccRenderSuggestedPrompts();
|
|
@@ -1072,6 +1075,7 @@ async function ccSend(options) {
|
|
|
1072
1075
|
if (tab._sending) {
|
|
1073
1076
|
tab._queue.push({ message: message, intentMetadata: intentMetadata });
|
|
1074
1077
|
_renderQueueIndicator();
|
|
1078
|
+
ccQueueSave(originTabId, tab._queue); // persist so the queue survives a refresh (W-ms4o3gtm005z91c9)
|
|
1075
1079
|
return;
|
|
1076
1080
|
}
|
|
1077
1081
|
var wasAborted = await _ccDoSend(message, false, originTabId, intentMetadata);
|
|
@@ -1085,6 +1089,7 @@ async function ccSend(options) {
|
|
|
1085
1089
|
var nextMessage = typeof next === 'string' ? next : next.message;
|
|
1086
1090
|
var nextIntentMetadata = typeof next === 'string' ? null : (next.intentMetadata || null);
|
|
1087
1091
|
_renderQueueIndicator();
|
|
1092
|
+
ccQueueSave(originTabId, tab._queue); // persist the post-drain remainder (W-ms4o3gtm005z91c9)
|
|
1088
1093
|
wasAborted = await _ccDoSend(nextMessage, false, originTabId, nextIntentMetadata);
|
|
1089
1094
|
}
|
|
1090
1095
|
}
|
|
@@ -1107,6 +1112,24 @@ function _renderQueueIndicator() {
|
|
|
1107
1112
|
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
1108
1113
|
}
|
|
1109
1114
|
|
|
1115
|
+
// Restore a tab's persisted queued messages after a hard refresh / new session
|
|
1116
|
+
// (W-ms4o3gtm005z91c9). Loaded once per tab (guarded by _queueRestored). The
|
|
1117
|
+
// queue is restored as greyed 'queued' bubbles but is NOT auto-drained on load:
|
|
1118
|
+
// the existing drain loop only runs off the user's next ccSend, so restored
|
|
1119
|
+
// bubbles sit idle until the user acts (never silently auto-firing on reload).
|
|
1120
|
+
// No-op when the tab already has an in-memory queue (a live session).
|
|
1121
|
+
function _ccRestoreQueue(tab) {
|
|
1122
|
+
if (!tab || !tab.id || tab._queueRestored) return;
|
|
1123
|
+
tab._queueRestored = true;
|
|
1124
|
+
ccQueueLoad(tab.id).then(function(loaded) {
|
|
1125
|
+
if (!loaded || !loaded.length) return;
|
|
1126
|
+
if (!tab._queue) tab._queue = [];
|
|
1127
|
+
if (tab._queue.length) return;
|
|
1128
|
+
for (var i = 0; i < loaded.length; i++) tab._queue.push(loaded[i]);
|
|
1129
|
+
if (tab.id === _ccActiveTabId) _renderQueueIndicator();
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1110
1133
|
async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
1111
1134
|
// Client-side /pin and /unpin — no LLM round-trip needed
|
|
1112
1135
|
var pinMatch = message.match(/^\/(pin|unpin)\s+(.+)/i);
|
|
@@ -1618,7 +1641,10 @@ function ccRetryLast(tabId, retryId) {
|
|
|
1618
1641
|
while (retryTab && retryTab._queue && retryTab._queue.length > 0) {
|
|
1619
1642
|
var next = retryTab._queue.shift();
|
|
1620
1643
|
_renderQueueIndicator();
|
|
1621
|
-
|
|
1644
|
+
ccQueueSave(retryTab.id, retryTab._queue); // persist the post-drain remainder (W-ms4o3gtm005z91c9)
|
|
1645
|
+
var retryMsg = typeof next === 'string' ? next : next.message;
|
|
1646
|
+
var retryMeta = typeof next === 'string' ? null : (next.intentMetadata || null);
|
|
1647
|
+
await _ccDoSend(retryMsg, false, retryTab.id, retryMeta);
|
|
1622
1648
|
}
|
|
1623
1649
|
});
|
|
1624
1650
|
}
|
|
@@ -74,7 +74,7 @@ function cmdKeyDown(e) {
|
|
|
74
74
|
items[cmdMentionIdx]?.scrollIntoView({ block: 'nearest' });
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
if (e.key === 'Enter' && !e
|
|
77
|
+
if (e.key === 'Enter' && !isSendShortcutEvent(e)) {
|
|
78
78
|
e.preventDefault();
|
|
79
79
|
e.stopPropagation();
|
|
80
80
|
const active = items[cmdMentionIdx >= 0 ? cmdMentionIdx : 0];
|
|
@@ -124,13 +124,30 @@ function cmdKeyDown(e) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
// Ctrl+Enter
|
|
128
|
-
if (e
|
|
127
|
+
// Platform submit chord — Command+Enter on macOS, Ctrl+Enter elsewhere
|
|
128
|
+
if (isSendShortcutEvent(e)) {
|
|
129
129
|
e.preventDefault();
|
|
130
130
|
cmdSubmit();
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Paint the send button's visible hint + accessible name with the platform
|
|
135
|
+
// chord. The markup ships the Ctrl+Enter default so non-JS/pre-boot renders
|
|
136
|
+
// still read correctly; this only rewrites it on macOS.
|
|
137
|
+
function cmdApplySendShortcutHint() {
|
|
138
|
+
const btn = document.getElementById('cmd-send-btn');
|
|
139
|
+
if (!btn) return;
|
|
140
|
+
const kbd = document.getElementById('cmd-send-kbd');
|
|
141
|
+
if (kbd) kbd.textContent = sendShortcutLabel();
|
|
142
|
+
btn.setAttribute('aria-label', 'Send (' + sendShortcutAriaLabel() + ')');
|
|
143
|
+
btn.setAttribute('aria-keyshortcuts', sendShortcutAriaKeys());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof document !== 'undefined' && document.addEventListener) {
|
|
147
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', cmdApplySendShortcutHint);
|
|
148
|
+
else cmdApplySendShortcutHint();
|
|
149
|
+
}
|
|
150
|
+
|
|
134
151
|
async function cmdSubmit() {
|
|
135
152
|
const input = document.getElementById('cmd-input');
|
|
136
153
|
const raw = input.value.trim();
|
|
@@ -432,30 +432,59 @@ async function archiveWorkItem(id, source) {
|
|
|
432
432
|
} catch (e) { clearDeleted('wi:' + id); showToast('cmd-toast', 'Archive error: ' + e.message, false); refresh(); }
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
// The dashboard is an app-shell layout: html/body/.page-layout are all
|
|
436
|
+
// overflow:hidden at a fixed height, so window.scrollY never moves and the only
|
|
437
|
+
// scrollable ancestor is #page-content. "See Archive" therefore used to reveal a
|
|
438
|
+
// panel that, on any work list taller than the fold, landed outside the scroll
|
|
439
|
+
// viewport (measured at 1440x900: panel top 1072 in a 900px viewport) while the
|
|
440
|
+
// trigger label stayed "See Archive". The handler ran, the API returned 200 and
|
|
441
|
+
// the DOM really did change — but nothing a human could perceive did, so the
|
|
442
|
+
// control looked dead.
|
|
443
|
+
//
|
|
444
|
+
// The repair is the disclosure contract itself, not a special case: the trigger
|
|
445
|
+
// reports its own state, and revealing the panel brings it into view.
|
|
446
|
+
function _setWiArchiveToggleState(expanded) {
|
|
447
|
+
const btn = document.getElementById('work-archive-toggle');
|
|
448
|
+
if (!btn) return;
|
|
449
|
+
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
450
|
+
btn.textContent = expanded ? 'Hide Archive' : 'See Archive';
|
|
451
|
+
}
|
|
452
|
+
|
|
435
453
|
let wiArchiveVisible = false;
|
|
436
454
|
async function toggleWorkItemArchive() {
|
|
437
455
|
const el = document.getElementById('work-items-archive');
|
|
456
|
+
if (!el) return;
|
|
438
457
|
wiArchiveVisible = !wiArchiveVisible;
|
|
458
|
+
_setWiArchiveToggleState(wiArchiveVisible);
|
|
439
459
|
if (!wiArchiveVisible) { el.style.display = 'none'; return; }
|
|
440
460
|
el.style.display = 'block';
|
|
441
461
|
el.innerHTML = '<p class="empty">Loading archive...</p>';
|
|
442
462
|
try {
|
|
443
463
|
const items = await fetch('/api/work-items/archive').then(r => r.json());
|
|
444
|
-
if (!items.length) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
'<
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
464
|
+
if (!items.length) {
|
|
465
|
+
el.innerHTML = '<p class="empty">No archived work items.</p>';
|
|
466
|
+
} else {
|
|
467
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: archived work item id/title/type/status/agent)
|
|
468
|
+
el.innerHTML = '<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Archived (' + items.length + ')</div>' +
|
|
469
|
+
'<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Agent</th><th>Archived</th></tr></thead><tbody>' +
|
|
470
|
+
items.map(function(i) {
|
|
471
|
+
return '<tr style="opacity:0.6">' +
|
|
472
|
+
'<td><span class="pr-id">' + escapeHtml(i.id || '') + '</span></td>' +
|
|
473
|
+
'<td style="min-width:240px;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(i.title || '') + '</td>' +
|
|
474
|
+
'<td><span class="dispatch-type ' + (i.type || '') + '">' + escapeHtml(i.type || '') + '</span></td>' +
|
|
475
|
+
'<td style="color:' + (i.status === 'done' ? 'var(--green)' : 'var(--red)') + '">' + escapeHtml(i.status || '') + '</td>' +
|
|
476
|
+
'<td>' + escapeHtml(i.dispatched_to || '—') + '</td>' +
|
|
477
|
+
'<td class="pr-date">' + shortTime(i.archivedAt) + '</td>' +
|
|
478
|
+
'</tr>';
|
|
479
|
+
}).join('') + '</tbody></table></div>';
|
|
480
|
+
}
|
|
458
481
|
} catch (e) { el.innerHTML = '<p class="empty">Failed to load archive.</p>'; }
|
|
482
|
+
// Scroll only after the panel has its final height, so a tall archive lands
|
|
483
|
+
// properly instead of being nudged into view while still showing "Loading".
|
|
484
|
+
// block:'nearest' scrolls the nearest scrollable ancestor (#page-content here)
|
|
485
|
+
// and is a no-op when the panel is already on screen, so tall viewports and
|
|
486
|
+
// short work lists are unaffected.
|
|
487
|
+
try { el.scrollIntoView({ block: 'nearest' }); } catch { /* older browsers */ }
|
|
459
488
|
}
|
|
460
489
|
|
|
461
490
|
async function retryWorkItem(id, source) {
|
package/dashboard/js/settings.js
CHANGED
|
@@ -1960,4 +1960,25 @@ window.MinionsSettings = { openSettings, saveSettings, addProject, removeProject
|
|
|
1960
1960
|
} else {
|
|
1961
1961
|
setTimeout(openWhenReady, 0);
|
|
1962
1962
|
}
|
|
1963
|
+
// FRESHNESS ON REOPEN (W-ms4ogdc800aa6931): the slim parent now CACHES this
|
|
1964
|
+
// embedded /settings frame (created once, shown/hidden) instead of cold-booting
|
|
1965
|
+
// it per open, so the iframe `load` — and thus the one-shot openSettings() boot
|
|
1966
|
+
// above — fires only ONCE. Re-pull /api/settings + /api/features whenever the
|
|
1967
|
+
// parent re-shows us (visible:true) so a value changed elsewhere isn't stale. A
|
|
1968
|
+
// full openSettings() re-render is cheap here (the SPA is already booted — just
|
|
1969
|
+
// two localhost fetches + a DOM render, no cold boot), so we prefer it over
|
|
1970
|
+
// ad-hoc partial refresh. Reuses the SAME {source:'minions-slim-embed',
|
|
1971
|
+
// type:'visibility'} channel refresh.js already honors (no second postMessage
|
|
1972
|
+
// bus). Embed-mode gated by the isSettingsPath+isEmbed early return above, so a
|
|
1973
|
+
// standalone /settings visit is completely unaffected.
|
|
1974
|
+
try {
|
|
1975
|
+
window.addEventListener('message', function(ev) {
|
|
1976
|
+
if (!ev || ev.origin !== window.location.origin) return;
|
|
1977
|
+
var d = ev.data;
|
|
1978
|
+
if (!d || d.source !== 'minions-slim-embed' || d.type !== 'visibility') return;
|
|
1979
|
+
if (d.visible && typeof openSettings === 'function') {
|
|
1980
|
+
try { openSettings(); } catch (e) { /* transient — next show retries */ }
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
} catch (e) { /* defensive — never block the embed boot on this hook */ }
|
|
1963
1984
|
})();
|
package/dashboard/js/utils.js
CHANGED
|
@@ -902,4 +902,32 @@ async function submitBugReport() {
|
|
|
902
902
|
}
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
-
|
|
905
|
+
// ── Send-shortcut helpers (Classic UX) ──────────────────────────────────
|
|
906
|
+
// macOS submits with Command+Enter; Windows/Linux keep Control+Enter. These are
|
|
907
|
+
// the single source of truth for both the key handler and the visible hint so
|
|
908
|
+
// the two can never drift apart.
|
|
909
|
+
|
|
910
|
+
function isMacPlatform() {
|
|
911
|
+
try {
|
|
912
|
+
if (typeof navigator === 'undefined' || !navigator) return false;
|
|
913
|
+
var uaData = navigator.userAgentData;
|
|
914
|
+
var platform = String((uaData && uaData.platform) || navigator.platform || '');
|
|
915
|
+
if (/mac/i.test(platform)) return true;
|
|
916
|
+
return /Mac OS X|Macintosh/i.test(String(navigator.userAgent || ''));
|
|
917
|
+
} catch { return false; }
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// True only for the platform submit chord. Shift/Alt and the non-platform
|
|
921
|
+
// modifier are rejected so unrelated combinations never trigger a send.
|
|
922
|
+
function isSendShortcutEvent(e) {
|
|
923
|
+
if (!e || e.key !== 'Enter') return false;
|
|
924
|
+
if (e.shiftKey || e.altKey) return false;
|
|
925
|
+
return isMacPlatform() ? (!!e.metaKey && !e.ctrlKey) : (!!e.ctrlKey && !e.metaKey);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function sendShortcutLabel() { return isMacPlatform() ? '⌘+Enter' : 'Ctrl+Enter'; }
|
|
929
|
+
function sendShortcutAriaLabel() { return isMacPlatform() ? 'Command+Enter' : 'Control+Enter'; }
|
|
930
|
+
// aria-keyshortcuts uses the DOM modifier names, so macOS reports "Meta".
|
|
931
|
+
function sendShortcutAriaKeys() { return isMacPlatform() ? 'Meta+Enter' : 'Control+Enter'; }
|
|
932
|
+
|
|
933
|
+
window.MinionsUtils = { wakeEngine, markDeleted, clearDeleted, escapeHtml, escHtml, renderMd, normalizePlanFile, timeAgo, statusColor, shouldIgnoreSelectionClick, llmCopyBtn, copyLlmText, openBugReport, submitBugReport, isMacPlatform, isSendShortcutEvent, sendShortcutLabel, sendShortcutAriaLabel, sendShortcutAriaKeys };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="cmd-highlight-layer" id="cmd-highlight" aria-hidden="true"></div>
|
|
5
5
|
<textarea id="cmd-input" rows="1" placeholder='What do you need? e.g. "Fix the auth bug @dallas", "explain the dispatch flow", or "/note always use feature flags"'
|
|
6
6
|
oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)" onscroll="syncHighlightScroll()" onpaste="cmdHandlePaste(event)"></textarea>
|
|
7
|
-
<button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()">Send <kbd>Ctrl+Enter</kbd></button>
|
|
7
|
+
<button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()" aria-label="Send (Control+Enter)" aria-keyshortcuts="Control+Enter">Send <kbd id="cmd-send-kbd">Ctrl+Enter</kbd></button>
|
|
8
8
|
</div>
|
|
9
9
|
<div class="cmd-mention-popup" id="cmd-mention-popup"></div>
|
|
10
10
|
<div class="cmd-meta" id="cmd-meta" style="display:none"></div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<section id="work-items-section" style="overflow:visible">
|
|
2
2
|
<h2>Work Items <span class="count" id="wi-count">0</span>
|
|
3
3
|
<button class="btn-add" style="margin-left:8px" onclick="openCreateWorkItemModal()">+ New</button>
|
|
4
|
-
<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;margin-left:4px" onclick="toggleWorkItemArchive()">See Archive</button>
|
|
4
|
+
<button id="work-archive-toggle" type="button" class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;margin-left:4px" onclick="toggleWorkItemArchive()" aria-expanded="false" aria-controls="work-items-archive">See Archive</button>
|
|
5
5
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">tasks dispatched to agents — auto-created from PRDs or added manually</span>
|
|
6
6
|
</h2>
|
|
7
7
|
<div id="work-items-content"><p class="empty">No work items yet.</p></div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// dashboard/shared/cc-queue-store.js
|
|
2
|
+
//
|
|
3
|
+
// Shared client helper to persist a Command Center tab's undrained queued
|
|
4
|
+
// (follow-up) messages to SQLite so they survive a hard refresh / new browser
|
|
5
|
+
// session (W-ms4o3gtm005z91c9). Used by BOTH the classic dashboard
|
|
6
|
+
// (dashboard/js/command-center.js) and Slim (dashboard/slim/js/chat.js +
|
|
7
|
+
// command-send.js) — do not fork this logic.
|
|
8
|
+
//
|
|
9
|
+
// The in-memory queue differs per surface: classic stores
|
|
10
|
+
// { message, intentMetadata } objects on tab._queue; slim stores plain message
|
|
11
|
+
// strings in queues[tabId]. Both normalize to { message, intentMetadata } for
|
|
12
|
+
// the wire, and ccQueueLoad always hands back that object shape so each surface
|
|
13
|
+
// can re-hydrate its own representation.
|
|
14
|
+
//
|
|
15
|
+
// Every helper is fire-and-forget and never throws: persistence is a
|
|
16
|
+
// best-effort convenience, so a failed request must never break the live queue.
|
|
17
|
+
|
|
18
|
+
function _ccQueueNormalizeEntry(entry) {
|
|
19
|
+
if (typeof entry === 'string') {
|
|
20
|
+
return entry ? { message: entry, intentMetadata: null } : null;
|
|
21
|
+
}
|
|
22
|
+
if (entry && typeof entry === 'object' && typeof entry.message === 'string' && entry.message) {
|
|
23
|
+
var meta = (entry.intentMetadata && typeof entry.intentMetadata === 'object'
|
|
24
|
+
&& !Array.isArray(entry.intentMetadata)) ? entry.intentMetadata : null;
|
|
25
|
+
return { message: entry.message, intentMetadata: meta };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _ccQueueUrl(tabId) {
|
|
31
|
+
return '/api/cc-sessions/' + encodeURIComponent(tabId) + '/queue';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Replace the persisted queue for a tab with the current in-memory queue.
|
|
35
|
+
// Accepts an array of strings and/or { message, intentMetadata } objects.
|
|
36
|
+
function ccQueueSave(tabId, queue) {
|
|
37
|
+
if (!tabId) return Promise.resolve();
|
|
38
|
+
var messages = (Array.isArray(queue) ? queue : [])
|
|
39
|
+
.map(_ccQueueNormalizeEntry)
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
try {
|
|
42
|
+
return fetch(_ccQueueUrl(tabId), {
|
|
43
|
+
method: 'PUT',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ messages: messages }),
|
|
46
|
+
}).catch(function() {});
|
|
47
|
+
} catch (_e) { return Promise.resolve(); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load the persisted queue for a tab. Resolves to an array of
|
|
51
|
+
// { message, intentMetadata } objects (empty array on any failure).
|
|
52
|
+
function ccQueueLoad(tabId) {
|
|
53
|
+
if (!tabId) return Promise.resolve([]);
|
|
54
|
+
try {
|
|
55
|
+
return fetch(_ccQueueUrl(tabId))
|
|
56
|
+
.then(function(res) { return (res && res.ok) ? res.json() : null; })
|
|
57
|
+
.then(function(data) {
|
|
58
|
+
var arr = data && Array.isArray(data.messages) ? data.messages : [];
|
|
59
|
+
return arr.map(_ccQueueNormalizeEntry).filter(Boolean);
|
|
60
|
+
})
|
|
61
|
+
.catch(function() { return []; });
|
|
62
|
+
} catch (_e) { return Promise.resolve([]); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Clear the persisted queue for a tab.
|
|
66
|
+
function ccQueueClear(tabId) {
|
|
67
|
+
if (!tabId) return Promise.resolve();
|
|
68
|
+
try {
|
|
69
|
+
return fetch(_ccQueueUrl(tabId), { method: 'DELETE' }).catch(function() {});
|
|
70
|
+
} catch (_e) { return Promise.resolve(); }
|
|
71
|
+
}
|
|
@@ -435,6 +435,7 @@
|
|
|
435
435
|
var qq = _getQueue();
|
|
436
436
|
qq.splice(i, 1);
|
|
437
437
|
renderQueue();
|
|
438
|
+
ccQueueSave(tabId, qq); // persist the shrunk queue (W-ms4o3gtm005z91c9)
|
|
438
439
|
});
|
|
439
440
|
row.appendChild(dismiss);
|
|
440
441
|
queueEl.appendChild(row);
|
|
@@ -442,7 +443,23 @@
|
|
|
442
443
|
scrollToBottom();
|
|
443
444
|
}
|
|
444
445
|
|
|
445
|
-
//
|
|
446
|
+
// Restore a tab's persisted queued messages after a hard refresh / new
|
|
447
|
+
// session (W-ms4o3gtm005z91c9). The queue is restored as greyed 'queued'
|
|
448
|
+
// bubbles but auto-drain is SUSPENDED — the user resumes explicitly via the
|
|
449
|
+
// "Send queued (N)" control (mirrors the post-abort state), so a restored
|
|
450
|
+
// queue never silently auto-fires on page load. No-op when the tab already
|
|
451
|
+
// has an in-memory queue (a live session we must not clobber or duplicate).
|
|
452
|
+
function _restoreQueueFor(id) {
|
|
453
|
+
if (!id) return;
|
|
454
|
+
ccQueueLoad(id).then(function(loaded) {
|
|
455
|
+
if (!loaded || !loaded.length) return;
|
|
456
|
+
var q = queues[id] || (queues[id] = []);
|
|
457
|
+
if (q.length) return;
|
|
458
|
+
loaded.forEach(function(entry) { q.push(entry.message); });
|
|
459
|
+
queueSuspended[id] = true;
|
|
460
|
+
if (id === tabId) renderQueue();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
446
463
|
// command/args (used by the tool-calls modal); otherwise long values are
|
|
447
464
|
// clipped for the collapsed 3-line panel.
|
|
448
465
|
function formatTool(name, input, full) {
|
|
@@ -723,6 +740,7 @@
|
|
|
723
740
|
renderTabBar();
|
|
724
741
|
_refreshComposerUI();
|
|
725
742
|
inputEl.focus();
|
|
743
|
+
_restoreQueueFor(id);
|
|
726
744
|
}
|
|
727
745
|
|
|
728
746
|
function closeSlimTab(id) {
|
|
@@ -755,3 +773,4 @@
|
|
|
755
773
|
loadState();
|
|
756
774
|
rerenderHistory();
|
|
757
775
|
renderTabBar();
|
|
776
|
+
_restoreQueueFor(tabId);
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
function _enqueue(text) {
|
|
45
45
|
_getQueue().push(text);
|
|
46
46
|
renderQueue();
|
|
47
|
+
ccQueueSave(tabId, _getQueue()); // persist so the queue survives a refresh (W-ms4o3gtm005z91c9)
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// Promote the next queued message for the active tab.
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
var q = queues[id];
|
|
59
60
|
if (!q || !q.length) return;
|
|
60
61
|
var next = q.shift();
|
|
62
|
+
ccQueueSave(id, q); // persist the post-drain remainder (W-ms4o3gtm005z91c9)
|
|
61
63
|
if (id === tabId) renderQueue();
|
|
62
64
|
_performSend(next, id);
|
|
63
65
|
}
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
// card container. Guarded with typeof so the source-extracted unit-test copy
|
|
20
20
|
// of this fn (which lacks the embed helpers/host) stays a safe no-op.
|
|
21
21
|
if (typeof _pauseActiveEmbed === 'function') _pauseActiveEmbed();
|
|
22
|
+
// The Automation composite is likewise cached + kept mounted (W-ms4r1t5q…);
|
|
23
|
+
// hide it and suspend its tab iframes' pollers rather than tearing it down.
|
|
24
|
+
if (typeof _hideAutomationComposite === 'function') _hideAutomationComposite();
|
|
22
25
|
var transient = document.getElementById('slim-tile-transient');
|
|
23
26
|
if (transient) transient.textContent = '';
|
|
24
27
|
}
|
|
@@ -133,6 +136,22 @@
|
|
|
133
136
|
} catch (e) { /* defensive — same-origin means this should never throw */ }
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
// Re-post a cached embed iframe's LIVE visibility once it has actually
|
|
140
|
+
// loaded. A visibility message posted BEFORE the iframe navigates lands on
|
|
141
|
+
// its throwaway about:blank document and is dropped when the real document
|
|
142
|
+
// replaces it — and the embedded classic SPA boots with its poll UNSUSPENDED
|
|
143
|
+
// (refresh.js `_embedPollSuspended = false`), so a frame that was hidden
|
|
144
|
+
// while still loading would poll /api/status every 4s forever in the
|
|
145
|
+
// background. `isVisible` is evaluated at LOAD time, not bind time, so the
|
|
146
|
+
// frame lands in whatever state the tile is in by then. Shared by every
|
|
147
|
+
// cached-iframe path (single-iframe embed tiles + Automation tabs).
|
|
148
|
+
function _bindEmbedVisibilityOnLoad(frame, isVisible) {
|
|
149
|
+
if (!frame || typeof frame.addEventListener !== 'function') return;
|
|
150
|
+
frame.addEventListener('load', function() {
|
|
151
|
+
_postEmbedVisibility(frame, !!isVisible());
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
136
155
|
// Ensure the persistent host + transient containers exist inside the tile
|
|
137
156
|
// modal body (created once, never wiped). Returns { host, transient }.
|
|
138
157
|
function _ensureTileContainers() {
|
|
@@ -170,9 +189,7 @@
|
|
|
170
189
|
frame.src = spec[0];
|
|
171
190
|
frame.title = spec[1];
|
|
172
191
|
frame.hidden = true;
|
|
173
|
-
frame
|
|
174
|
-
_postEmbedVisibility(frame, _activeEmbedKey === key);
|
|
175
|
-
});
|
|
192
|
+
_bindEmbedVisibilityOnLoad(frame, function() { return _activeEmbedKey === key; });
|
|
176
193
|
containers.host.appendChild(frame);
|
|
177
194
|
_embedFrames[key] = frame;
|
|
178
195
|
return frame;
|
|
@@ -295,22 +312,58 @@
|
|
|
295
312
|
_showReusableEmbed('prs');
|
|
296
313
|
}
|
|
297
314
|
|
|
298
|
-
// Automation
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
315
|
+
// Persistent Automation composite (W-ms4r1t5q003h341a): the tabbed
|
|
316
|
+
// Watches·Schedules·Pipelines composite + its lazy iframes are built ONCE into
|
|
317
|
+
// #slim-tile-automation-host and shown/hidden across opens — mirroring the
|
|
318
|
+
// _embedFrames cache used by the single-iframe tiles — so reopening the
|
|
319
|
+
// Automation tile is an instant show, NOT a rebuild that cold-reloads all three
|
|
320
|
+
// classic screens into the wiped transient container. { activate, frames,
|
|
321
|
+
// activeKey } or null until first open.
|
|
322
|
+
var _automationComposite = null;
|
|
323
|
+
|
|
324
|
+
// Ensure the persistent Automation composite host exists inside the tile modal
|
|
325
|
+
// body (created once, never wiped). Sibling of #slim-tile-embed-host and
|
|
326
|
+
// #slim-tile-transient; shown only while the Automation tile is open.
|
|
327
|
+
function _ensureAutomationHost() {
|
|
328
|
+
var body = document.getElementById('slim-tile-body');
|
|
329
|
+
if (!body) return null;
|
|
330
|
+
var host = document.getElementById('slim-tile-automation-host');
|
|
331
|
+
if (!host) {
|
|
332
|
+
host = document.createElement('div');
|
|
333
|
+
host.id = 'slim-tile-automation-host';
|
|
334
|
+
host.className = 'slim-tile-automation-host';
|
|
335
|
+
host.hidden = true;
|
|
336
|
+
body.appendChild(host);
|
|
337
|
+
}
|
|
338
|
+
return host;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Hide the cached Automation composite and suspend all its embedded tab
|
|
342
|
+
// pollers. A hidden (display:none) iframe still reports visibilityState
|
|
343
|
+
// 'visible', so without this the cached /watches·/schedule·/pipelines screens
|
|
344
|
+
// would keep polling /api/status in the background forever (refresh.js honors
|
|
345
|
+
// the visibility message). Called when another tile opens and on modal close.
|
|
346
|
+
// Guarded/no-op before the composite is built.
|
|
347
|
+
function _hideAutomationComposite() {
|
|
348
|
+
var host = document.getElementById('slim-tile-automation-host');
|
|
349
|
+
if (host) host.hidden = true;
|
|
350
|
+
if (_automationComposite && _automationComposite.frames) {
|
|
351
|
+
Object.keys(_automationComposite.frames).forEach(function(k) {
|
|
352
|
+
if (typeof _postEmbedVisibility === 'function') _postEmbedVisibility(_automationComposite.frames[k], false);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Build the tabbed Watches·Schedules·Pipelines composite ONCE into the
|
|
358
|
+
// persistent host. Returns { activate, frames, activeKey }. Each tab's iframe
|
|
359
|
+
// is LAZY (mounted only on first activation) and CACHED (kept mounted so
|
|
360
|
+
// reopening never rebuilds/reloads it). Hidden tabs' pollers are suspended via
|
|
361
|
+
// the same visibility message the single-iframe embeds use, so a cached-but-
|
|
362
|
+
// inactive tab isn't polling in the background. Built with createElement (no
|
|
310
363
|
// innerHTML) to satisfy the dashboard no-unsanitized lint gate; tab buttons
|
|
311
364
|
// reuse the shared .kn-tab pill primitive and carry role="tab"/aria-selected
|
|
312
365
|
// for keyboard/AT parity with the Knowledge modal tabs.
|
|
313
|
-
function
|
|
366
|
+
function _buildAutomationComposite(host) {
|
|
314
367
|
var TABS = [
|
|
315
368
|
{ key: 'watches', label: 'Watches', src: '/watches?embed=1' },
|
|
316
369
|
{ key: 'schedule', label: 'Schedules', src: '/schedule?embed=1' },
|
|
@@ -327,6 +380,7 @@
|
|
|
327
380
|
var btns = {};
|
|
328
381
|
var panels = {};
|
|
329
382
|
var frames = {};
|
|
383
|
+
var composite = { activate: null, frames: frames, activeKey: null };
|
|
330
384
|
|
|
331
385
|
function activate(key) {
|
|
332
386
|
TABS.forEach(function(t) {
|
|
@@ -343,11 +397,24 @@
|
|
|
343
397
|
frame.className = 'slim-automation-embed';
|
|
344
398
|
frame.src = t.src;
|
|
345
399
|
frame.title = t.label;
|
|
400
|
+
// A visibility message posted before this frame navigates is dropped
|
|
401
|
+
// on about:blank, so re-post the live state once it has loaded —
|
|
402
|
+
// otherwise a tab hidden mid-load becomes a permanent background
|
|
403
|
+
// /api/status poller. Same contract as _getOrCreateEmbedFrame.
|
|
404
|
+
_bindEmbedVisibilityOnLoad(frame, function() {
|
|
405
|
+
return composite.activeKey === t.key && !host.hidden;
|
|
406
|
+
});
|
|
346
407
|
panel.appendChild(frame);
|
|
347
408
|
frames[t.key] = frame;
|
|
348
409
|
}
|
|
410
|
+
// Suspend hidden cached frames' pollers; resume the shown one.
|
|
411
|
+
if (frames[t.key] && typeof _postEmbedVisibility === 'function') {
|
|
412
|
+
_postEmbedVisibility(frames[t.key], isActive);
|
|
413
|
+
}
|
|
349
414
|
});
|
|
415
|
+
composite.activeKey = key;
|
|
350
416
|
}
|
|
417
|
+
composite.activate = activate;
|
|
351
418
|
|
|
352
419
|
TABS.forEach(function(t) {
|
|
353
420
|
var btn = document.createElement('button');
|
|
@@ -371,9 +438,36 @@
|
|
|
371
438
|
panes.appendChild(panel);
|
|
372
439
|
});
|
|
373
440
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
441
|
+
host.appendChild(tablist);
|
|
442
|
+
host.appendChild(panes);
|
|
443
|
+
return composite;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Automation tile body — a tabbed composite (Watches · Schedules · Pipelines)
|
|
447
|
+
// that reuses the LITERAL classic dashboard screens (/watches, /schedule,
|
|
448
|
+
// /pipelines) instead of a slim-specific list, so there is one Automation UI,
|
|
449
|
+
// not two (W-mrz0x2a400030f90 / "reuse, don't fork" — mirrors renderPlansBody
|
|
450
|
+
// for the Plans tile and renderPrsBody for the PRs tile). Each tab embeds the
|
|
451
|
+
// real classic screen in an iframe with the chrome-off ?embed=1 mode; the
|
|
452
|
+
// iframed page IS the classic screen — full create/edit/pause/run + row
|
|
453
|
+
// actions, backed by the same /api/{watches,schedules,pipelines} endpoints —
|
|
454
|
+
// with zero duplicated rendering logic. Classic is reachable at each route even
|
|
455
|
+
// with slim-ux ON (only / is taken over). The composite + its lazy iframes are
|
|
456
|
+
// CACHED in the persistent #slim-tile-automation-host and SHOWN/HIDDEN across
|
|
457
|
+
// opens (W-ms4r1t5q003h341a) rather than rebuilt into the wiped transient
|
|
458
|
+
// container — so reopening the tile is an instant show, not a rebuild that
|
|
459
|
+
// cold-reloads all three classic screens. Mirrors the _embedFrames reuse the
|
|
460
|
+
// single-iframe tiles already use. The `body` arg (transient) is intentionally
|
|
461
|
+
// ignored; the composite lives in its own persistent host.
|
|
462
|
+
function renderAutomationTileBody(/* body */) {
|
|
463
|
+
var host = _ensureAutomationHost();
|
|
464
|
+
if (!host) return;
|
|
465
|
+
if (!_automationComposite) {
|
|
466
|
+
_automationComposite = _buildAutomationComposite(host);
|
|
467
|
+
}
|
|
468
|
+
host.hidden = false;
|
|
469
|
+
// Reopen on the last-active tab (default Watches on first open).
|
|
470
|
+
_automationComposite.activate(_automationComposite.activeKey || 'watches');
|
|
377
471
|
}
|
|
378
472
|
|
|
379
473
|
// tile key -> { title, render }. Adding a tile is a one-line table edit.
|
|
@@ -423,6 +517,10 @@
|
|
|
423
517
|
// into the transient container, so pause/hide any shown embed behind them.
|
|
424
518
|
var isReusableEmbed = Object.prototype.hasOwnProperty.call(_EMBED_TILES, key);
|
|
425
519
|
if (!isReusableEmbed) _pauseActiveEmbed();
|
|
520
|
+
// The Automation composite lives in its own persistent, cached host; hide it
|
|
521
|
+
// (suspending its tab iframes' pollers) whenever a different tile is shown.
|
|
522
|
+
// renderAutomationTileBody unhides + reactivates it when Automation opens.
|
|
523
|
+
if (key !== 'automation') _hideAutomationComposite();
|
|
426
524
|
view.render(target, lastStatusData || {});
|
|
427
525
|
modal.classList.add('open');
|
|
428
526
|
}
|
|
@@ -18,16 +18,44 @@
|
|
|
18
18
|
// — two stacked "Settings" bars was the duplicate-menu-bar bug
|
|
19
19
|
// (W-mrpbzke4000ie609). Closing is driven from the embedded modal's own close
|
|
20
20
|
// X (wired below to the parent), plus backdrop-click and Escape.
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// CACHED settings iframe (W-ms4ogdc800aa6931). The old code discarded the
|
|
22
|
+
// frame on close and appended a BRAND-NEW `/settings?embed=1` iframe on every
|
|
23
|
+
// open, so each open cold-booted the entire classic SPA + full Settings render
|
|
24
|
+
// (/api/settings + /api/features + per-agent runtime dropdowns + projects +
|
|
25
|
+
// routing, plus the SPA's 4s /api/status poll) — the multi-second stall. We
|
|
26
|
+
// now mirror the cockpit-tile reuse contract (modals-tiles.js
|
|
27
|
+
// #_getOrCreateEmbedFrame / _showReusableEmbed): create the frame ONCE, keep
|
|
28
|
+
// it mounted (hidden), and show/hide it. A single background boot replaces N
|
|
29
|
+
// per-open cold boots; while hidden the embedded SPA's poll is suspended via
|
|
30
|
+
// the shared {source:'minions-slim-embed',type:'visibility'} message
|
|
31
|
+
// (refresh.js honors it in embed mode) so the cached frame doesn't poll
|
|
32
|
+
// /api/status forever off-screen.
|
|
33
|
+
var _slimSettingsFrame = null;
|
|
34
|
+
|
|
35
|
+
// Create (once) the cached settings iframe and return it. Built with
|
|
36
|
+
// createElement (no innerHTML) to satisfy the dashboard no-unsanitized lint
|
|
37
|
+
// gate. Starts hidden; a load-time visibility message suspends its poll until
|
|
38
|
+
// it is shown. Because the frame is cached, its `load` handler fires exactly
|
|
39
|
+
// ONCE — so the close/Escape wiring is installed here and persists across all
|
|
40
|
+
// hide/show cycles.
|
|
41
|
+
function _getOrCreateSettingsFrame() {
|
|
42
|
+
if (_slimSettingsFrame) return _slimSettingsFrame;
|
|
23
43
|
var body = document.getElementById('slim-settings-body');
|
|
24
|
-
if (!
|
|
25
|
-
body.textContent = '';
|
|
44
|
+
if (!body) return null;
|
|
45
|
+
body.textContent = ''; // drop the "Loading settings…" placeholder (once)
|
|
26
46
|
var frame = document.createElement('iframe');
|
|
27
47
|
frame.className = 'slim-settings-embed';
|
|
28
48
|
frame.src = '/settings?embed=1';
|
|
29
49
|
frame.title = 'Settings';
|
|
50
|
+
frame.hidden = true;
|
|
30
51
|
frame.addEventListener('load', function() {
|
|
52
|
+
// Suspend/resume the embedded SPA poll to match current visibility. On a
|
|
53
|
+
// visible:true the embedded classic Settings also re-pulls /api/settings +
|
|
54
|
+
// /api/features (see dashboard/js/settings.js#bootSettingsEmbed) — the
|
|
55
|
+
// same single postMessage channel, no second bus.
|
|
56
|
+
var modal = document.getElementById('slim-settings-modal');
|
|
57
|
+
var shown = !!(modal && modal.classList.contains('open'));
|
|
58
|
+
if (typeof _postEmbedVisibility === 'function') _postEmbedVisibility(frame, shown);
|
|
31
59
|
var frameDocument = frame.contentDocument;
|
|
32
60
|
if (!frameDocument) return;
|
|
33
61
|
// Capture before the classic modal handler so only the parent modal closes.
|
|
@@ -53,20 +81,50 @@
|
|
|
53
81
|
}
|
|
54
82
|
});
|
|
55
83
|
body.appendChild(frame);
|
|
84
|
+
_slimSettingsFrame = frame;
|
|
85
|
+
return frame;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function openSlimSettings() {
|
|
89
|
+
var modal = document.getElementById('slim-settings-modal');
|
|
90
|
+
var frame = _getOrCreateSettingsFrame();
|
|
91
|
+
if (!modal || !frame) return;
|
|
92
|
+
frame.hidden = false;
|
|
93
|
+
// Resume the embedded SPA poll + trigger a fresh /api/settings+/api/features
|
|
94
|
+
// re-pull (freshness on reopen — the cached frame's `load` fired only once,
|
|
95
|
+
// so without this a value changed elsewhere would show stale).
|
|
96
|
+
if (typeof _postEmbedVisibility === 'function') _postEmbedVisibility(frame, true);
|
|
56
97
|
modal.classList.add('open');
|
|
57
98
|
}
|
|
58
99
|
|
|
59
100
|
function closeSlimSettings() {
|
|
60
|
-
document.getElementById('slim-settings-modal')
|
|
101
|
+
var modal = document.getElementById('slim-settings-modal');
|
|
102
|
+
if (!modal) return;
|
|
103
|
+
// Keep the frame MOUNTED for an instant reopen; just hide it and suspend its
|
|
104
|
+
// poll (so the cached frame stops polling /api/status in the background).
|
|
105
|
+
if (_slimSettingsFrame) {
|
|
106
|
+
if (typeof _postEmbedVisibility === 'function') _postEmbedVisibility(_slimSettingsFrame, false);
|
|
107
|
+
_slimSettingsFrame.hidden = true;
|
|
108
|
+
}
|
|
109
|
+
modal.classList.remove('open');
|
|
61
110
|
}
|
|
62
111
|
|
|
63
112
|
(function bindSettingsUi() {
|
|
64
113
|
var btn = document.getElementById('slim-settings-btn');
|
|
65
114
|
var backdrop = document.getElementById('slim-settings-modal');
|
|
66
|
-
if (btn)
|
|
115
|
+
if (btn) {
|
|
116
|
+
btn.addEventListener('click', openSlimSettings);
|
|
117
|
+
// PREDICTIVE prewarm: the first hover / focus / press of the gear boots the
|
|
118
|
+
// cached settings frame before the click lands, so the open is an instant
|
|
119
|
+
// show. Fires at most once (the frame is cached thereafter).
|
|
120
|
+
var predictivePrewarm = function() { _getOrCreateSettingsFrame(); };
|
|
121
|
+
btn.addEventListener('pointerenter', predictivePrewarm, { once: true });
|
|
122
|
+
btn.addEventListener('pointerdown', predictivePrewarm, { once: true });
|
|
123
|
+
btn.addEventListener('focus', predictivePrewarm, { once: true });
|
|
124
|
+
}
|
|
67
125
|
// No outer close button: the embedded classic Settings modal's own close X
|
|
68
|
-
// (wired to the parent in
|
|
69
|
-
// alongside backdrop-click and Escape below (W-mrpbzke4000ie609).
|
|
126
|
+
// (wired to the parent in _getOrCreateSettingsFrame) is the single close
|
|
127
|
+
// affordance, alongside backdrop-click and Escape below (W-mrpbzke4000ie609).
|
|
70
128
|
if (backdrop) {
|
|
71
129
|
backdrop.addEventListener('click', function(ev) {
|
|
72
130
|
if (ev.target === backdrop) closeSlimSettings();
|
|
@@ -78,6 +136,22 @@
|
|
|
78
136
|
});
|
|
79
137
|
})();
|
|
80
138
|
|
|
139
|
+
// Deferred prewarm: boot the cached settings frame AFTER the cockpit's own hot
|
|
140
|
+
// embed prewarm (modals-tiles.js schedules that at ~800ms) so it never competes
|
|
141
|
+
// with slim cockpit initial paint. Idle-callback when available, else a delayed
|
|
142
|
+
// timeout — either way it runs well after first paint. First open then becomes
|
|
143
|
+
// an instant show instead of a cold boot.
|
|
144
|
+
(function scheduleSettingsPrewarm() {
|
|
145
|
+
var warm = function() { try { _getOrCreateSettingsFrame(); } catch (e) { /* skip prewarm */ } };
|
|
146
|
+
try {
|
|
147
|
+
if (typeof requestIdleCallback === 'function') {
|
|
148
|
+
requestIdleCallback(function() { setTimeout(warm, 1200); }, { timeout: 4000 });
|
|
149
|
+
} else {
|
|
150
|
+
setTimeout(warm, 1600);
|
|
151
|
+
}
|
|
152
|
+
} catch (e) { /* no timers available — skip prewarm, first open cold-boots */ }
|
|
153
|
+
})();
|
|
154
|
+
|
|
81
155
|
// POST a feature-flag toggle; throws on a non-2xx so callers can branch on
|
|
82
156
|
// failure. Used by the one-click returnToClassicDashboard().
|
|
83
157
|
async function postFeatureToggle(id, enabled) {
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
// — only deltas from a previously-seen number trigger the fade, so opening
|
|
22
22
|
// the dashboard doesn't animate every tile.
|
|
23
23
|
var lastTileValues = Object.create(null);
|
|
24
|
+
// Last (count|detail|lit) signature painted onto the Automation cockpit tile,
|
|
25
|
+
// so applyStatus can skip a redundant updateTile on every 5s status poll when
|
|
26
|
+
// nothing changed (W-ms4r1t5q003h341a).
|
|
27
|
+
var _automationTileSig = null;
|
|
24
28
|
|
|
25
29
|
// Apply lit-state and value/detail text to a tile.
|
|
26
30
|
function updateTile(key, value, detail, lit) {
|
|
@@ -230,14 +234,19 @@
|
|
|
230
234
|
// counts are not carried by the status poll, and the task explicitly
|
|
231
235
|
// says not to invent new API calls just for the tile count. The detail
|
|
232
236
|
// modal (renderAutomationTileBody) embeds all three classic screens.
|
|
237
|
+
// This runs on EVERY 5s status poll, so gate the updateTile DOM work
|
|
238
|
+
// (querySelectors + textContent + classList churn) behind a change-check —
|
|
239
|
+
// repaint only when the count / detail / lit-state actually changed
|
|
240
|
+
// (render-cache-busting parity with the classic dashboard's _changed()).
|
|
233
241
|
var watches = Array.isArray(data.watches) ? data.watches : [];
|
|
234
242
|
var activeWatches = watches.filter(function(w) { return w && w.status === 'active'; });
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
var autoDetail = activeWatches.length ? 'monitoring' : 'no automation set';
|
|
244
|
+
var autoLit = activeWatches.length ? 'blue' : null;
|
|
245
|
+
var autoSig = activeWatches.length + '|' + autoDetail + '|' + (autoLit || '');
|
|
246
|
+
if (_automationTileSig !== autoSig) {
|
|
247
|
+
_automationTileSig = autoSig;
|
|
248
|
+
updateTile('automation', activeWatches.length, autoDetail, autoLit);
|
|
249
|
+
}
|
|
241
250
|
|
|
242
251
|
// ── Knowledge tile (Pinned Context + Notes + KB) ──────────────
|
|
243
252
|
// Pinned count is live from the poll; notes/KB counts are fetched lazily
|
|
@@ -797,11 +797,24 @@
|
|
|
797
797
|
outranks the class rules (no !important needed). */
|
|
798
798
|
.slim-tile-embed-host, .slim-tile-transient { display: block; }
|
|
799
799
|
#slim-tile-embed-host iframe[hidden] { display: none; }
|
|
800
|
+
/* Cached slim Settings frame (W-ms4ogdc800aa6931): like the tile embeds it is
|
|
801
|
+
created once and shown/hidden rather than cold-booted per open. The
|
|
802
|
+
.slim-settings-embed class rule above sets display:block, which outranks
|
|
803
|
+
[hidden]'s UA display:none — so a hidden cached frame must be forced back
|
|
804
|
+
to none. The id+attr selector outranks the class rule (no !important). */
|
|
805
|
+
#slim-settings-body iframe[hidden] { display: none; }
|
|
800
806
|
/* Automation tile (Watches · Schedules · Pipelines) — a tabbed composite
|
|
801
807
|
inside the shared wide tile-modal. The tab bar sits above the embedded
|
|
802
808
|
classic screen, so the iframe height leaves room for it (vs the padless
|
|
803
809
|
full-height embeds above). Reuses the .kn-tab pill primitive. */
|
|
804
810
|
.slim-automation-tabs { padding: 12px 16px 0; }
|
|
811
|
+
/* Persistent Automation composite host (W-ms4r1t5q003h341a): like the single-
|
|
812
|
+
iframe embed host above, the tabbed Watches·Schedules·Pipelines composite +
|
|
813
|
+
its lazy iframes are built once and shown/hidden across opens rather than
|
|
814
|
+
rebuilt (which would cold-reload all three classic screens on every open).
|
|
815
|
+
Hidden via the [hidden] attribute when another tile is shown. */
|
|
816
|
+
#slim-tile-automation-host[hidden] { display: none; }
|
|
817
|
+
#slim-tile-automation-host .slim-automation-panel[hidden] { display: none; }
|
|
805
818
|
.slim-automation-embed {
|
|
806
819
|
display: block; width: 100%; height: calc(100vh - 148px); min-height: 320px;
|
|
807
820
|
border: 0; background: var(--bg);
|
package/dashboard-build.js
CHANGED
|
@@ -8,7 +8,7 @@ const shared = require('./engine/shared');
|
|
|
8
8
|
const { safeRead } = shared;
|
|
9
9
|
|
|
10
10
|
const MINIONS_DIR = __dirname;
|
|
11
|
-
const DASHBOARD_SHARED_JS = ['pr-merge-state', 'pr-filters', 'cc-suggestions', 'model-display', 'watches-source', 'project-git-summary', 'welcome-popup'];
|
|
11
|
+
const DASHBOARD_SHARED_JS = ['pr-merge-state', 'pr-filters', 'cc-suggestions', 'cc-queue-store', 'model-display', 'watches-source', 'project-git-summary', 'welcome-popup'];
|
|
12
12
|
|
|
13
13
|
// ── Canonical classic-dashboard assembly manifest ──────────────────────────
|
|
14
14
|
// Single source of truth, consumed by BOTH assemblers: buildDashboardHtml()
|
package/dashboard.js
CHANGED
|
@@ -11087,6 +11087,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11087
11087
|
const sessions = _filterCcTabSessions(raw);
|
|
11088
11088
|
return sessions.filter(s => s.id !== id);
|
|
11089
11089
|
});
|
|
11090
|
+
// Evict any persisted queued messages for the closed tab (mirrors the
|
|
11091
|
+
// in-memory tab._queue / queues[id] cleanup on both surfaces).
|
|
11092
|
+
try { smallStateStore.clearCcQueue(id); } catch { /* SQL unavailable — best effort */ }
|
|
11090
11093
|
// Sub-task C of W-mp2w003600196c51: tear down the persistent ACP worker
|
|
11091
11094
|
// for this tab so we don't leak a Copilot process after the user closes
|
|
11092
11095
|
// the tab. closeTab is a no-op when the pool has no entry for the tabId,
|
|
@@ -11095,6 +11098,56 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11095
11098
|
return jsonReply(res, 200, { ok: true });
|
|
11096
11099
|
}
|
|
11097
11100
|
|
|
11101
|
+
// GET /api/cc-sessions/:id/queue — load a tab's persisted queued messages so
|
|
11102
|
+
// the greyed-out 'queued' bubbles can be restored after a hard refresh
|
|
11103
|
+
// (W-ms4o3gtm005z91c9). Read-only; returns { messages: [{message, intentMetadata}] }.
|
|
11104
|
+
async function handleCCSessionQueueGet(req, res, match) {
|
|
11105
|
+
const id = match?.[1];
|
|
11106
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
11107
|
+
try {
|
|
11108
|
+
ccApiValidation.validateCcSessionPathId(id);
|
|
11109
|
+
const messages = smallStateStore.readCcQueue(id);
|
|
11110
|
+
return jsonReply(res, 200, { messages });
|
|
11111
|
+
} catch (e) {
|
|
11112
|
+
if (apiValidation.isApiInputError(e)) return apiErrorReply(res, e, req);
|
|
11113
|
+
return jsonReply(res, e.statusCode || 500, { error: e.message });
|
|
11114
|
+
}
|
|
11115
|
+
}
|
|
11116
|
+
|
|
11117
|
+
// PUT /api/cc-sessions/:id/queue — replace a tab's persisted queued messages.
|
|
11118
|
+
// The client re-sends the full queue on every mutation (enqueue, dismiss,
|
|
11119
|
+
// drain) so the persisted state always mirrors the live in-memory queue.
|
|
11120
|
+
async function handleCCSessionQueuePut(req, res, match) {
|
|
11121
|
+
const id = match?.[1];
|
|
11122
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
11123
|
+
try {
|
|
11124
|
+
ccApiValidation.validateCcSessionPathId(id);
|
|
11125
|
+
const body = await readBody(req, { maxBytes: CC_IMAGE_REQUEST_MAX_BYTES });
|
|
11126
|
+
ccApiValidation.validateCcQueueReplaceRequest(body);
|
|
11127
|
+
const result = smallStateStore.replaceCcQueue(id, body.messages);
|
|
11128
|
+
return jsonReply(res, 200, { ok: true, count: result.count });
|
|
11129
|
+
} catch (e) {
|
|
11130
|
+
if (apiValidation.isApiInputError(e)) return apiErrorReply(res, e, req);
|
|
11131
|
+
return jsonReply(res, e.statusCode || 500, { error: e.message });
|
|
11132
|
+
}
|
|
11133
|
+
}
|
|
11134
|
+
|
|
11135
|
+
// DELETE /api/cc-sessions/:id/queue — clear a tab's persisted queued messages
|
|
11136
|
+
// (e.g. after the queue fully drains). Bodyless.
|
|
11137
|
+
async function handleCCSessionQueueDelete(req, res, match) {
|
|
11138
|
+
const id = match?.[1];
|
|
11139
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
11140
|
+
try {
|
|
11141
|
+
ccApiValidation.validateCcSessionPathId(id);
|
|
11142
|
+
ccApiValidation.validateNoRequestBody(req?.[REQUEST_BODY_CACHE]?.value);
|
|
11143
|
+
smallStateStore.clearCcQueue(id);
|
|
11144
|
+
return jsonReply(res, 200, { ok: true });
|
|
11145
|
+
} catch (e) {
|
|
11146
|
+
if (apiValidation.isApiInputError(e)) return apiErrorReply(res, e, req);
|
|
11147
|
+
return jsonReply(res, e.statusCode || 500, { error: e.message });
|
|
11148
|
+
}
|
|
11149
|
+
}
|
|
11150
|
+
|
|
11098
11151
|
// Trigger a process-spawn + initialize + session/new (including MCP init)
|
|
11099
11152
|
// in the background so the user's first message skips the ~18-21 s Copilot
|
|
11100
11153
|
// cold-spawn. Runtime-gated: a no-op (200 skipped) when the pool is off, so
|
|
@@ -15931,6 +15984,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
15931
15984
|
{ method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
|
|
15932
15985
|
{ method: 'POST', path: '/api/cc-sessions/warm', desc: 'Pre-warm the worker pool for a CC tab (process + MCP init, no LLM call). No-op when pool is off.', params: 'tabId', handler: handleCcSessionWarm },
|
|
15933
15986
|
{ method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)$/, template: '/api/cc-sessions/:id', desc: 'Delete a CC session by tab ID', handler: handleCCSessionDelete },
|
|
15987
|
+
{ method: 'GET', path: /^\/api\/cc-sessions\/([\w-]+)\/queue$/, template: '/api/cc-sessions/:id/queue', desc: 'Load a CC tab\'s persisted queued (undrained) messages', handler: handleCCSessionQueueGet },
|
|
15988
|
+
{ method: 'PUT', path: /^\/api\/cc-sessions\/([\w-]+)\/queue$/, template: '/api/cc-sessions/:id/queue', desc: 'Replace a CC tab\'s persisted queued (undrained) messages', params: 'messages[]', handler: handleCCSessionQueuePut },
|
|
15989
|
+
{ method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)\/queue$/, template: '/api/cc-sessions/:id/queue', desc: 'Clear a CC tab\'s persisted queued (undrained) messages', handler: handleCCSessionQueueDelete },
|
|
15934
15990
|
|
|
15935
15991
|
// Schedules
|
|
15936
15992
|
{ method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
|
|
@@ -98,10 +98,14 @@ module.exports = {
|
|
|
98
98
|
'GET /api/cc-sessions',
|
|
99
99
|
'POST /api/cc-sessions/warm',
|
|
100
100
|
'DELETE /api/cc-sessions/<id>',
|
|
101
|
+
'GET /api/cc-sessions/<id>/queue',
|
|
102
|
+
'PUT /api/cc-sessions/<id>/queue',
|
|
103
|
+
'DELETE /api/cc-sessions/<id>/queue',
|
|
101
104
|
],
|
|
102
105
|
bodyless: [
|
|
103
106
|
'POST /api/command-center/new-session',
|
|
104
107
|
'DELETE /api/cc-sessions/<id>',
|
|
108
|
+
'DELETE /api/cc-sessions/<id>/queue',
|
|
105
109
|
],
|
|
106
110
|
overrides: {
|
|
107
111
|
'GET /api/cc-sessions': { noInput: true },
|
|
@@ -215,5 +219,57 @@ module.exports = {
|
|
|
215
219
|
{ strategy: 'send-unexpected-body', expectedStatus: 400 },
|
|
216
220
|
],
|
|
217
221
|
},
|
|
222
|
+
'GET /api/cc-sessions/<id>/queue': {
|
|
223
|
+
audit: 'audited',
|
|
224
|
+
path: {
|
|
225
|
+
policy: 'required',
|
|
226
|
+
fields: [
|
|
227
|
+
{ name: 'id', type: 'string', required: true, maxLength: CC_API_LIMITS.tabIdMaxChars },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
query: { policy: 'none', fields: [] },
|
|
231
|
+
headers: { policy: 'none', fields: [] },
|
|
232
|
+
},
|
|
233
|
+
'PUT /api/cc-sessions/<id>/queue': {
|
|
234
|
+
audit: 'audited',
|
|
235
|
+
path: {
|
|
236
|
+
policy: 'required',
|
|
237
|
+
fields: [
|
|
238
|
+
{ name: 'id', type: 'string', required: true, maxLength: CC_API_LIMITS.tabIdMaxChars },
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
body: {
|
|
242
|
+
policy: 'required',
|
|
243
|
+
fields: [
|
|
244
|
+
{
|
|
245
|
+
name: 'messages',
|
|
246
|
+
type: 'array',
|
|
247
|
+
required: true,
|
|
248
|
+
maxItems: CC_API_LIMITS.queueMaxMessages,
|
|
249
|
+
itemType: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
fields: [
|
|
252
|
+
{ name: 'message', type: 'string', required: true, maxLength: CC_API_LIMITS.messageMaxChars },
|
|
253
|
+
{ name: 'intentMetadata', type: 'object', required: false, maxBytes: CC_API_LIMITS.metadataMaxBytes },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
negativeTests: commonNegativeTests,
|
|
260
|
+
},
|
|
261
|
+
'DELETE /api/cc-sessions/<id>/queue': {
|
|
262
|
+
audit: 'audited',
|
|
263
|
+
path: {
|
|
264
|
+
policy: 'required',
|
|
265
|
+
fields: [
|
|
266
|
+
{ name: 'id', type: 'string', required: true, maxLength: CC_API_LIMITS.tabIdMaxChars },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
negativeTests: [
|
|
270
|
+
{ strategy: 'use-oversized-path-id', expectedStatus: 400 },
|
|
271
|
+
{ strategy: 'send-unexpected-body', expectedStatus: 400 },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
218
274
|
},
|
|
219
275
|
};
|
|
@@ -26,6 +26,7 @@ const CC_API_LIMITS = Object.freeze({
|
|
|
26
26
|
imageFilenameMaxChars: 255,
|
|
27
27
|
imageMaxCount: 4,
|
|
28
28
|
imageMaxDecodedBytes: 5 * 1024 * 1024,
|
|
29
|
+
queueMaxMessages: 50,
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
const TRANSCRIPT_ROLES = Object.freeze(['user', 'assistant', 'action', 'system']);
|
|
@@ -346,6 +347,58 @@ function validateCcSessionPathId(id) {
|
|
|
346
347
|
return validateTabId(id, { required: true, path: ['path', 'id'] });
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
// PUT /api/cc-sessions/:id/queue — replace a tab's persisted queued messages.
|
|
351
|
+
// Body shape: { messages: [{ message: string, intentMetadata?: object }, ...] }.
|
|
352
|
+
// The queue is bounded so a client cannot persist an unbounded blob.
|
|
353
|
+
function validateCcQueueReplaceRequest(body) {
|
|
354
|
+
validateBody(body);
|
|
355
|
+
apiValidation.validateArray(body.messages, {
|
|
356
|
+
field: 'messages',
|
|
357
|
+
path: ['body', 'messages'],
|
|
358
|
+
maxLength: CC_API_LIMITS.queueMaxMessages,
|
|
359
|
+
});
|
|
360
|
+
body.messages.forEach((entry, index) => {
|
|
361
|
+
const entryPath = ['body', 'messages', index];
|
|
362
|
+
apiValidation.validatePlainObject(entry, {
|
|
363
|
+
field: 'messages',
|
|
364
|
+
path: entryPath,
|
|
365
|
+
});
|
|
366
|
+
validateNonBlankString(entry.message, {
|
|
367
|
+
field: 'message',
|
|
368
|
+
path: [...entryPath, 'message'],
|
|
369
|
+
maxLength: CC_API_LIMITS.messageMaxChars,
|
|
370
|
+
});
|
|
371
|
+
if (entry.intentMetadata !== undefined && entry.intentMetadata !== null) {
|
|
372
|
+
apiValidation.validatePlainObject(entry.intentMetadata, {
|
|
373
|
+
field: 'intentMetadata',
|
|
374
|
+
path: [...entryPath, 'intentMetadata'],
|
|
375
|
+
});
|
|
376
|
+
let encoded;
|
|
377
|
+
try {
|
|
378
|
+
encoded = JSON.stringify(entry.intentMetadata);
|
|
379
|
+
} catch {
|
|
380
|
+
throwConstraint('intentMetadata must be JSON-serializable', {
|
|
381
|
+
field: 'intentMetadata',
|
|
382
|
+
path: [...entryPath, 'intentMetadata'],
|
|
383
|
+
rejectedValue: entry.intentMetadata,
|
|
384
|
+
expected: 'JSON-serializable plain object',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (Object.keys(entry.intentMetadata).length > CC_API_LIMITS.metadataMaxKeys
|
|
388
|
+
|| Buffer.byteLength(encoded || '', 'utf8') > CC_API_LIMITS.metadataMaxBytes) {
|
|
389
|
+
throwConstraint('intentMetadata is too large', {
|
|
390
|
+
field: 'intentMetadata',
|
|
391
|
+
path: [...entryPath, 'intentMetadata'],
|
|
392
|
+
rejectedValue: entry.intentMetadata,
|
|
393
|
+
expected: `plain object with at most ${CC_API_LIMITS.metadataMaxKeys} keys and ${CC_API_LIMITS.metadataMaxBytes} encoded bytes`,
|
|
394
|
+
extra: { max: CC_API_LIMITS.metadataMaxBytes },
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
return body;
|
|
400
|
+
}
|
|
401
|
+
|
|
349
402
|
function realPath(targetPath) {
|
|
350
403
|
return fs.realpathSync.native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath);
|
|
351
404
|
}
|
|
@@ -577,6 +630,7 @@ module.exports = {
|
|
|
577
630
|
validateAbortRequest,
|
|
578
631
|
validateCcWarmRequest,
|
|
579
632
|
validateCcSessionPathId,
|
|
633
|
+
validateCcQueueReplaceRequest,
|
|
580
634
|
validateDocChatRequest,
|
|
581
635
|
validateDocChatWarmRequest,
|
|
582
636
|
validateBrowserPresenceRequest,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// engine/db/migrations/028-cc-queued-messages.js
|
|
2
|
+
//
|
|
3
|
+
// Persist each Command Center tab's undrained queued messages so they survive a
|
|
4
|
+
// hard refresh / new browser session (W-ms4o3gtm005z91c9). The queue was
|
|
5
|
+
// previously in-memory only (classic tab._queue, slim `queues[tabId]`).
|
|
6
|
+
//
|
|
7
|
+
// One row per queued message, keyed by (tab_id, position). `position` preserves
|
|
8
|
+
// FIFO order; `intent_metadata` carries the optional classic {intentMetadata}
|
|
9
|
+
// blob (slim entries are plain strings and store NULL). No backfill — there is
|
|
10
|
+
// no legacy on-disk source for an in-memory-only queue.
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
version: 28,
|
|
14
|
+
description: 'cc_queued_messages — persist per-tab CC queued messages',
|
|
15
|
+
up(db) {
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE cc_queued_messages (
|
|
18
|
+
tab_id TEXT NOT NULL,
|
|
19
|
+
position INTEGER NOT NULL,
|
|
20
|
+
message TEXT NOT NULL,
|
|
21
|
+
intent_metadata TEXT,
|
|
22
|
+
created_at INTEGER NOT NULL,
|
|
23
|
+
PRIMARY KEY (tab_id, position)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX idx_cc_queued_messages_tab ON cc_queued_messages(tab_id);
|
|
27
|
+
`);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -746,6 +746,90 @@ function applyCcGlobalSessionMutation(mutator) {
|
|
|
746
746
|
});
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
+
// ─── cc_queued_messages ──────────────────────────────────────────────────────
|
|
750
|
+
// Persist a CC tab's undrained queued follow-up messages so they survive a hard
|
|
751
|
+
// refresh (W-ms4o3gtm005z91c9). One row per message keyed by (tab_id, position);
|
|
752
|
+
// `position` preserves FIFO order. Entry shape: { message, intentMetadata }
|
|
753
|
+
// (slim enqueues plain strings → intentMetadata null).
|
|
754
|
+
|
|
755
|
+
function _normalizeCcQueueEntry(entry) {
|
|
756
|
+
if (typeof entry === 'string') {
|
|
757
|
+
const message = entry;
|
|
758
|
+
return message ? { message, intentMetadata: null } : null;
|
|
759
|
+
}
|
|
760
|
+
if (entry && typeof entry === 'object' && typeof entry.message === 'string' && entry.message) {
|
|
761
|
+
const intentMetadata = (entry.intentMetadata && typeof entry.intentMetadata === 'object'
|
|
762
|
+
&& !Array.isArray(entry.intentMetadata))
|
|
763
|
+
? entry.intentMetadata
|
|
764
|
+
: null;
|
|
765
|
+
return { message: entry.message, intentMetadata };
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function readCcQueue(tabId) {
|
|
771
|
+
if (!tabId) return [];
|
|
772
|
+
const { getDb } = require('./db');
|
|
773
|
+
const rows = getDb()
|
|
774
|
+
.prepare('SELECT message, intent_metadata FROM cc_queued_messages WHERE tab_id = ? ORDER BY position')
|
|
775
|
+
.all(String(tabId));
|
|
776
|
+
const out = [];
|
|
777
|
+
for (const row of rows) {
|
|
778
|
+
let intentMetadata = null;
|
|
779
|
+
if (row.intent_metadata != null) {
|
|
780
|
+
try {
|
|
781
|
+
const parsed = JSON.parse(row.intent_metadata);
|
|
782
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) intentMetadata = parsed;
|
|
783
|
+
} catch { /* malformed — drop metadata, keep message */ }
|
|
784
|
+
}
|
|
785
|
+
out.push({ message: row.message, intentMetadata });
|
|
786
|
+
}
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function replaceCcQueue(tabId, messages) {
|
|
791
|
+
if (!tabId) return { wrote: false, count: 0 };
|
|
792
|
+
const { getDb, withTransaction } = require('./db');
|
|
793
|
+
let db;
|
|
794
|
+
try { db = getDb(); }
|
|
795
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
796
|
+
|
|
797
|
+
const normalized = (Array.isArray(messages) ? messages : [])
|
|
798
|
+
.map(_normalizeCcQueueEntry)
|
|
799
|
+
.filter(Boolean);
|
|
800
|
+
|
|
801
|
+
return withTransaction(db, () => {
|
|
802
|
+
db.prepare('DELETE FROM cc_queued_messages WHERE tab_id = ?').run(String(tabId));
|
|
803
|
+
const ins = db.prepare(`
|
|
804
|
+
INSERT INTO cc_queued_messages (tab_id, position, message, intent_metadata, created_at)
|
|
805
|
+
VALUES (?, ?, ?, ?, ?)
|
|
806
|
+
`);
|
|
807
|
+
const now = Date.now();
|
|
808
|
+
normalized.forEach((entry, position) => {
|
|
809
|
+
ins.run(
|
|
810
|
+
String(tabId),
|
|
811
|
+
position,
|
|
812
|
+
entry.message,
|
|
813
|
+
entry.intentMetadata ? JSON.stringify(entry.intentMetadata) : null,
|
|
814
|
+
now,
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
return { wrote: true, count: normalized.length };
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function clearCcQueue(tabId) {
|
|
822
|
+
if (!tabId) return { wrote: false };
|
|
823
|
+
const { getDb, withTransaction } = require('./db');
|
|
824
|
+
let db;
|
|
825
|
+
try { db = getDb(); }
|
|
826
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
827
|
+
return withTransaction(db, () => {
|
|
828
|
+
const info = db.prepare('DELETE FROM cc_queued_messages WHERE tab_id = ?').run(String(tabId));
|
|
829
|
+
return { wrote: (info && info.changes > 0) || false };
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
749
833
|
// ─── doc_sessions ──────────────────────────────────────────────────────────
|
|
750
834
|
// Shape: { [filePath]: { sessionId, lastActiveAt, turnCount, ... } }
|
|
751
835
|
// SQL: row per filePath key.
|
|
@@ -837,6 +921,10 @@ module.exports = {
|
|
|
837
921
|
applyCcSessionsMutation,
|
|
838
922
|
readCcGlobalSession,
|
|
839
923
|
applyCcGlobalSessionMutation,
|
|
924
|
+
// cc_queued_messages
|
|
925
|
+
readCcQueue,
|
|
926
|
+
replaceCcQueue,
|
|
927
|
+
clearCcQueue,
|
|
840
928
|
// doc_sessions
|
|
841
929
|
readDocSessions,
|
|
842
930
|
applyDocSessionsMutation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2440",
|
|
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"
|
|
@@ -73,6 +73,20 @@ on the OPG `Visual evidence capture` CI bot (that bot stays as-is; this is
|
|
|
73
73
|
additive PR-body evidence). When the existing description already embeds
|
|
74
74
|
`` refs for a view this dispatch touched, refresh them the same way.
|
|
75
75
|
|
|
76
|
+
**MANDATORY GATE — UI-surface diffs REQUIRE screenshots.** Screenshot capture +
|
|
77
|
+
embed is a **required completion gate**, not a nicety, for any dispatch whose diff
|
|
78
|
+
modifies a **rendered UI surface** — e.g. files under `dashboard/`,
|
|
79
|
+
`dashboard/slim/`, `dashboard/pages/`, `dashboard/js/`, `dashboard/styles.css`, or
|
|
80
|
+
ANY change that alters what a dashboard page/route renders
|
|
81
|
+
(layout/CSS/markup/new UI). For such a diff you **MUST** capture and embed AFTER
|
|
82
|
+
screenshots (BEFORE/AFTER for visual FIXES, AFTER-only for NEW UI) for the PR to be
|
|
83
|
+
considered complete. Omitting screenshots on a UI-surface diff **without a recorded
|
|
84
|
+
hard-failure reason makes the PR incomplete** — "I judged it not visual enough" is
|
|
85
|
+
**NOT** a valid skip when the diff touches those paths. The ONLY legitimate skips are
|
|
86
|
+
genuine hard failures: the dev server will not start after a real attempt, Playwright
|
|
87
|
+
MCP is unavailable, the route 404s, or upload/attachment auth fails. Record any such
|
|
88
|
+
skip in `meta.descriptionAudit.result` as `screenshots-skipped (<specific-reason>)`.
|
|
89
|
+
|
|
76
90
|
**Scope guard — which changes get screenshots.** Only meaningful visual/UI
|
|
77
91
|
changes: dashboard pages, slim-ux, Settings, work-item/PR/plan views, badges,
|
|
78
92
|
layout/CSS. Skip backend-only, engine-logic, text/prose-only, analysis, and
|
|
@@ -135,7 +135,7 @@ After you push commits to the PR's source branch and BEFORE you mark the work it
|
|
|
135
135
|
|
|
136
136
|
**Out-of-scope guardrails**
|
|
137
137
|
|
|
138
|
-
- Only screenshot **meaningful visual/UI changes** (dashboard pages, slim-ux, Settings, work-item/PR/plan views, badges, layout/CSS); skip backend/engine/text/analysis/docs changes.
|
|
138
|
+
- Only screenshot **meaningful visual/UI changes** (dashboard pages, slim-ux, Settings, work-item/PR/plan views, badges, layout/CSS); skip backend/engine/text/analysis/docs changes. **MANDATORY GATE:** when a build fix's diff touches a **rendered UI surface** (files under `dashboard/`, `dashboard/slim/`, `dashboard/pages/`, `dashboard/js/`, `dashboard/styles.css`, or anything that alters what a dashboard page/route renders), you **MUST** capture + embed AFTER screenshots (BEFORE/AFTER for fixes, AFTER-only for new UI) via the release-asset recipe in `_pr-description-audit.md` for the PR to be complete — "not visual enough" is NOT a valid skip on those paths; only a recorded hard failure (dev server won't start, Playwright MCP unavailable, route 404s, upload auth fails) justifies skipping, recorded in `meta.descriptionAudit.result` as `screenshots-skipped (<specific-reason>)`.
|
|
139
139
|
- Don't rewrite description prose beyond the targeted stale edits.
|
|
140
140
|
- Don't modify the PR title; don't toggle draft state, close/reopen the PR.
|
|
141
141
|
- Don't post a separate PR comment summarizing the description change.
|
package/playbooks/fix.md
CHANGED
|
@@ -172,6 +172,8 @@ After you push commits to the PR's source branch and BEFORE you mark the work it
|
|
|
172
172
|
|
|
173
173
|
For a PR that contains a **meaningful visual/UI change**, proactively CAPTURE screenshots and EMBED them directly in the PR description — do NOT rely only on the OPG `Visual evidence capture` CI bot (it stays as-is; this is additive PR-body evidence). When the description already embeds `` refs for a view this dispatch touched, refresh them the same way.
|
|
174
174
|
|
|
175
|
+
**MANDATORY GATE — UI-surface diffs REQUIRE screenshots.** Screenshot capture + embed is a **required completion gate**, not a nicety, for any dispatch whose diff modifies a **rendered UI surface** — e.g. files under `dashboard/`, `dashboard/slim/`, `dashboard/pages/`, `dashboard/js/`, `dashboard/styles.css`, or ANY change that alters what a dashboard page/route renders (layout/CSS/markup/new UI). For such a diff you **MUST** capture and embed AFTER screenshots (BEFORE/AFTER for visual FIXES, AFTER-only for NEW UI) for the PR to be considered complete. Omitting screenshots on a UI-surface diff **without a recorded hard-failure reason makes the PR incomplete** — "I judged it not visual enough" is **NOT** a valid skip when the diff touches those paths. The ONLY legitimate skips are genuine hard failures: the dev server will not start after a real attempt, Playwright MCP is unavailable, the route 404s, or upload/attachment auth fails. Record any such skip in `meta.descriptionAudit.result` as `screenshots-skipped (<specific-reason>)`.
|
|
176
|
+
|
|
175
177
|
**Scope guard — which changes get screenshots.** Only meaningful visual/UI changes: dashboard pages, slim-ux, Settings, work-item/PR/plan views, badges, layout/CSS. Skip backend-only, engine-logic, text/prose-only, analysis, and docs changes — those get NO screenshots (they add no signal). BEFORE/AFTER pair for layout/visual FIXES; AFTER-only for NEW UI.
|
|
176
178
|
|
|
177
179
|
1. If the project has a runnable dev server (detect via `package.json` scripts named `dev`, `start`, or `serve`) AND Playwright MCP is available, spin it up with a detached handoff per `shared-rules.md` → "Long-Running Commands" (record PID + log path + URL + stop command).
|
package/playbooks/implement.md
CHANGED
|
@@ -144,6 +144,8 @@ After you push commits to the PR's source branch and BEFORE you mark the work it
|
|
|
144
144
|
|
|
145
145
|
For a PR that contains a **meaningful visual/UI change**, proactively CAPTURE screenshots and EMBED them directly in the PR description — do NOT rely only on the OPG `Visual evidence capture` CI bot (it stays as-is; this is additive PR-body evidence). When the description already embeds `` refs for a view this dispatch touched, refresh them the same way.
|
|
146
146
|
|
|
147
|
+
**MANDATORY GATE — UI-surface diffs REQUIRE screenshots.** Screenshot capture + embed is a **required completion gate**, not a nicety, for any dispatch whose diff modifies a **rendered UI surface** — e.g. files under `dashboard/`, `dashboard/slim/`, `dashboard/pages/`, `dashboard/js/`, `dashboard/styles.css`, or ANY change that alters what a dashboard page/route renders (layout/CSS/markup/new UI). For such a diff you **MUST** capture and embed AFTER screenshots (BEFORE/AFTER for visual FIXES, AFTER-only for NEW UI) for the PR to be considered complete. Omitting screenshots on a UI-surface diff **without a recorded hard-failure reason makes the PR incomplete** — "I judged it not visual enough" is **NOT** a valid skip when the diff touches those paths. The ONLY legitimate skips are genuine hard failures: the dev server will not start after a real attempt, Playwright MCP is unavailable, the route 404s, or upload/attachment auth fails. Record any such skip in `meta.descriptionAudit.result` as `screenshots-skipped (<specific-reason>)`.
|
|
148
|
+
|
|
147
149
|
**Scope guard — which changes get screenshots.** Only meaningful visual/UI changes: dashboard pages, slim-ux, Settings, work-item/PR/plan views, badges, layout/CSS. Skip backend-only, engine-logic, text/prose-only, analysis, and docs changes — those get NO screenshots (they add no signal). BEFORE/AFTER pair for layout/visual FIXES; AFTER-only for NEW UI.
|
|
148
150
|
|
|
149
151
|
1. If the project has a runnable dev server (detect via `package.json` scripts named `dev`, `start`, or `serve`) AND Playwright MCP is available, spin it up with a detached handoff per `shared-rules.md` → "Long-Running Commands" (record PID + log path + URL + stop command).
|
|
@@ -208,6 +208,7 @@ Concretely:
|
|
|
208
208
|
- Do not sleep or busy-wait for `mergeStatus`, `buildStatus`, or any ADO/GitHub API to flip from `running` to `passing`.
|
|
209
209
|
- If you skipped local validation, say so in the completion JSON (e.g. `tests: skipped — relying on PR pipeline`) and still exit.
|
|
210
210
|
- Holding a slot to watch a pipeline is wasted capacity; the engine has its own pipeline-monitoring path.
|
|
211
|
+
- **Screenshots are a MANDATORY completion gate for UI-surface diffs.** Any PR whose diff changes a rendered dashboard/UI surface (files under `dashboard/`, `dashboard/slim/`, `dashboard/pages/`, `dashboard/js/`, `dashboard/styles.css`, or anything that alters what a page/route renders) MUST embed AFTER screenshots in the PR description (full recipe: `_pr-description-audit.md`) — skips are allowed only on a recorded hard failure (dev server won't start, Playwright MCP unavailable, route 404s, upload auth fails). Backend/engine/text/analysis/docs diffs get NO screenshots.
|
|
211
212
|
|
|
212
213
|
## Resolving Review Threads — No Silent Closures
|
|
213
214
|
|