@yemi33/minions 0.1.744 → 0.1.746
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 +8 -0
- package/dashboard/js/command-center.js +349 -152
- package/dashboard/js/modal-qa.js +15 -27
- package/dashboard/layout.html +2 -1
- package/dashboard/styles.css +6 -0
- package/dashboard.js +45 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,59 @@
|
|
|
1
1
|
// command-center.js — Command center panel functions extracted from dashboard.html
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
// ── Multi-tab state ──────────────────────────────────────────────────────────
|
|
4
|
+
var CC_MAX_TABS = 20;
|
|
5
|
+
var CC_MAX_MESSAGES_PER_TAB = 30;
|
|
6
|
+
var CC_TITLE_MAX_LENGTH = 40;
|
|
7
|
+
|
|
8
|
+
var _ccTabs = []; // [{id, title, sessionId, messages: [{role, html}]}]
|
|
9
|
+
var _ccActiveTabId = null;
|
|
10
|
+
var _ccOpen = false;
|
|
11
|
+
var _ccSending = false;
|
|
12
|
+
var _ccQueue = [];
|
|
13
|
+
var _ccAbortController = null;
|
|
9
14
|
// Clear stale sending state on page load — SSE streams don't survive refresh
|
|
10
15
|
try { localStorage.removeItem('cc-sending'); } catch {}
|
|
11
16
|
|
|
17
|
+
// ── Migration from legacy single-session format ─────────────────────────────
|
|
18
|
+
(function _ccMigrateLegacy() {
|
|
19
|
+
try {
|
|
20
|
+
var legacyTabs = localStorage.getItem('cc-tabs');
|
|
21
|
+
if (legacyTabs) {
|
|
22
|
+
// Already migrated — load tabs
|
|
23
|
+
_ccTabs = JSON.parse(legacyTabs) || [];
|
|
24
|
+
_ccActiveTabId = localStorage.getItem('cc-active-tab') || (_ccTabs.length > 0 ? _ccTabs[0].id : null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
var legacySessionId = localStorage.getItem('cc-session-id');
|
|
28
|
+
var legacyMessages = JSON.parse(localStorage.getItem('cc-messages') || '[]');
|
|
29
|
+
if (legacySessionId || legacyMessages.length > 0) {
|
|
30
|
+
var tabId = 'cc-' + Date.now().toString(36);
|
|
31
|
+
var title = 'New chat';
|
|
32
|
+
if (legacyMessages.length > 0) {
|
|
33
|
+
var firstUser = legacyMessages.find(function(m) { return m.role === 'user'; });
|
|
34
|
+
if (firstUser) {
|
|
35
|
+
var tmp = document.createElement('div');
|
|
36
|
+
tmp.innerHTML = firstUser.html;
|
|
37
|
+
var txt = (tmp.textContent || tmp.innerText || '').trim();
|
|
38
|
+
if (txt.length > 0) title = txt.slice(0, CC_TITLE_MAX_LENGTH);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
_ccTabs = [{ id: tabId, title: title, sessionId: legacySessionId, messages: legacyMessages.slice(-CC_MAX_MESSAGES_PER_TAB) }];
|
|
42
|
+
_ccActiveTabId = tabId;
|
|
43
|
+
// Remove legacy keys
|
|
44
|
+
localStorage.removeItem('cc-session-id');
|
|
45
|
+
localStorage.removeItem('cc-messages');
|
|
46
|
+
ccSaveState();
|
|
47
|
+
}
|
|
48
|
+
} catch { /* ignore migration errors */ }
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
// ── Tab helper: get active tab ──────────────────────────────────────────────
|
|
52
|
+
function _ccActiveTab() {
|
|
53
|
+
if (!_ccActiveTabId || _ccTabs.length === 0) return null;
|
|
54
|
+
return _ccTabs.find(function(t) { return t.id === _ccActiveTabId; }) || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
12
57
|
function _ccFindPinTarget(query) {
|
|
13
58
|
for (var i = 0; i < (inboxData || []).length; i++) {
|
|
14
59
|
if (inboxData[i].name.toLowerCase().includes(query)) {
|
|
@@ -35,13 +80,16 @@ function ccAbort() {
|
|
|
35
80
|
|
|
36
81
|
function toggleCommandCenter() {
|
|
37
82
|
_ccOpen = !_ccOpen;
|
|
38
|
-
|
|
39
|
-
|
|
83
|
+
var drawer = document.getElementById('cc-drawer');
|
|
84
|
+
var overlay = document.getElementById('cc-overlay');
|
|
40
85
|
if (_ccOpen) ccApplySavedWidth();
|
|
41
86
|
drawer.style.display = _ccOpen ? 'flex' : 'none';
|
|
42
87
|
if (overlay) overlay.style.display = _ccOpen ? 'block' : 'none';
|
|
43
88
|
if (_ccOpen) {
|
|
89
|
+
// Ensure at least one tab exists
|
|
90
|
+
if (_ccTabs.length === 0) ccNewTab(true);
|
|
44
91
|
clearNotifBadge(document.getElementById('cc-toggle-btn'));
|
|
92
|
+
ccRenderTabBar();
|
|
45
93
|
document.getElementById('cc-input').focus();
|
|
46
94
|
ccRestoreMessages();
|
|
47
95
|
} else if (_ccSending) {
|
|
@@ -51,43 +99,154 @@ function toggleCommandCenter() {
|
|
|
51
99
|
|
|
52
100
|
|
|
53
101
|
function ccNewSession() {
|
|
54
|
-
|
|
102
|
+
// Legacy wrapper — just creates a new tab
|
|
103
|
+
ccNewTab();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ccNewTab(skipServerReset) {
|
|
107
|
+
if (_ccTabs.length >= CC_MAX_TABS) {
|
|
108
|
+
// Remove oldest tab to make room
|
|
109
|
+
var oldest = _ccTabs.shift();
|
|
110
|
+
if (oldest && oldest.id === _ccActiveTabId) _ccActiveTabId = null;
|
|
111
|
+
}
|
|
112
|
+
var tabId = 'cc-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
113
|
+
var tab = { id: tabId, title: 'New chat', sessionId: null, messages: [] };
|
|
114
|
+
_ccTabs.push(tab);
|
|
115
|
+
_ccActiveTabId = tabId;
|
|
116
|
+
// New tab starts with null sessionId — server creates fresh session on first message
|
|
55
117
|
// Abort any in-flight request
|
|
56
118
|
ccAbort();
|
|
57
119
|
_ccSending = false;
|
|
58
120
|
_ccQueue = [];
|
|
59
121
|
try { localStorage.removeItem('cc-sending'); } catch {}
|
|
60
|
-
_ccSessionId = null;
|
|
61
|
-
_ccMessages = [];
|
|
62
|
-
localStorage.removeItem('cc-session-id');
|
|
63
|
-
localStorage.removeItem('cc-messages');
|
|
64
122
|
document.getElementById('cc-messages').innerHTML = '';
|
|
123
|
+
ccRenderTabBar();
|
|
124
|
+
ccUpdateSessionIndicator();
|
|
125
|
+
ccSaveState();
|
|
126
|
+
var input = document.getElementById('cc-input');
|
|
127
|
+
if (input) input.focus();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ccSwitchTab(id) {
|
|
131
|
+
if (id === _ccActiveTabId) return;
|
|
132
|
+
var tab = _ccTabs.find(function(t) { return t.id === id; });
|
|
133
|
+
if (!tab) return;
|
|
134
|
+
// If there is an active request, keep it running but switch view
|
|
135
|
+
_ccActiveTabId = id;
|
|
136
|
+
var el = document.getElementById('cc-messages');
|
|
137
|
+
el.innerHTML = '';
|
|
138
|
+
// Re-render messages from the tab's data
|
|
139
|
+
for (var i = 0; i < tab.messages.length; i++) {
|
|
140
|
+
ccAddMessage(tab.messages[i].role, tab.messages[i].html, true);
|
|
141
|
+
}
|
|
142
|
+
ccRenderTabBar();
|
|
65
143
|
ccUpdateSessionIndicator();
|
|
144
|
+
ccSaveState();
|
|
145
|
+
var input = document.getElementById('cc-input');
|
|
146
|
+
if (input) input.focus();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function ccCloseTab(id) {
|
|
150
|
+
var idx = _ccTabs.findIndex(function(t) { return t.id === id; });
|
|
151
|
+
if (idx === -1) return;
|
|
152
|
+
// If tab has active request, confirm before closing
|
|
153
|
+
if (_ccSending && _ccActiveTabId === id) {
|
|
154
|
+
if (!confirm('This tab has an active request. Close anyway?')) return;
|
|
155
|
+
ccAbort();
|
|
156
|
+
_ccSending = false;
|
|
157
|
+
_ccQueue = [];
|
|
158
|
+
try { localStorage.removeItem('cc-sending'); } catch {}
|
|
159
|
+
}
|
|
160
|
+
_ccTabs.splice(idx, 1);
|
|
161
|
+
if (_ccActiveTabId === id) {
|
|
162
|
+
// Switch to adjacent tab or create new
|
|
163
|
+
if (_ccTabs.length === 0) {
|
|
164
|
+
ccNewTab(true);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
var newIdx = Math.min(idx, _ccTabs.length - 1);
|
|
168
|
+
ccSwitchTab(_ccTabs[newIdx].id);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
ccRenderTabBar();
|
|
172
|
+
ccSaveState();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ccShowAllConversations() {
|
|
176
|
+
var dropdown = document.getElementById('cc-all-conversations');
|
|
177
|
+
if (dropdown) { dropdown.remove(); return; } // toggle off
|
|
178
|
+
dropdown = document.createElement('div');
|
|
179
|
+
dropdown.id = 'cc-all-conversations';
|
|
180
|
+
dropdown.style.cssText = 'position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-top:none;max-height:300px;overflow-y:auto;z-index:360;box-shadow:0 4px 12px rgba(0,0,0,0.3)';
|
|
181
|
+
var html = '';
|
|
182
|
+
for (var i = _ccTabs.length - 1; i >= 0; i--) {
|
|
183
|
+
var t = _ccTabs[i];
|
|
184
|
+
var isActive = t.id === _ccActiveTabId;
|
|
185
|
+
var preview = '';
|
|
186
|
+
if (t.messages.length > 0) {
|
|
187
|
+
var lastMsg = t.messages[t.messages.length - 1];
|
|
188
|
+
var tmp = document.createElement('div');
|
|
189
|
+
tmp.innerHTML = lastMsg.html;
|
|
190
|
+
preview = (tmp.textContent || tmp.innerText || '').slice(0, 60);
|
|
191
|
+
}
|
|
192
|
+
html += '<div style="padding:8px 12px;border-bottom:1px solid var(--border);cursor:pointer;display:flex;align-items:center;gap:8px' + (isActive ? ';background:rgba(56,139,253,0.1)' : '') + '" onclick="ccSwitchTab(\'' + t.id + '\');document.getElementById(\'cc-all-conversations\')?.remove()">';
|
|
193
|
+
html += '<div style="flex:1;min-width:0"><div style="font-size:11px;font-weight:600;color:' + (isActive ? 'var(--blue)' : 'var(--text)') + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + escHtml(t.title) + '</div>';
|
|
194
|
+
if (preview) html += '<div style="font-size:10px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px">' + escHtml(preview) + '</div>';
|
|
195
|
+
html += '</div>';
|
|
196
|
+
html += '<span style="font-size:10px;color:var(--muted)">' + t.messages.length + ' msg</span>';
|
|
197
|
+
html += '<button onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\');document.getElementById(\'cc-all-conversations\')?.remove();ccShowAllConversations()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:12px;padding:2px 4px">×</button>';
|
|
198
|
+
html += '</div>';
|
|
199
|
+
}
|
|
200
|
+
if (_ccTabs.length === 0) html = '<div style="padding:12px;color:var(--muted);font-size:11px;text-align:center">No conversations yet</div>';
|
|
201
|
+
dropdown.innerHTML = html;
|
|
202
|
+
var tabBar = document.getElementById('cc-tab-bar');
|
|
203
|
+
if (tabBar) { tabBar.style.position = 'relative'; tabBar.appendChild(dropdown); }
|
|
204
|
+
// Close on click outside
|
|
205
|
+
function closeDropdown(e) { if (!dropdown.contains(e.target) && e.target.id !== 'cc-all-btn') { dropdown.remove(); document.removeEventListener('click', closeDropdown); } }
|
|
206
|
+
setTimeout(function() { document.addEventListener('click', closeDropdown); }, 0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ccRenderTabBar() {
|
|
210
|
+
var bar = document.getElementById('cc-tab-bar');
|
|
211
|
+
if (!bar) return;
|
|
212
|
+
var html = '<button onclick="ccNewTab()" style="background:none;border:1px solid var(--border);color:var(--blue);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;white-space:nowrap;flex-shrink:0" title="New tab">+</button>';
|
|
213
|
+
for (var i = 0; i < _ccTabs.length; i++) {
|
|
214
|
+
var t = _ccTabs[i];
|
|
215
|
+
var isActive = t.id === _ccActiveTabId;
|
|
216
|
+
html += '<div class="cc-tab' + (isActive ? ' active' : '') + '" onclick="ccSwitchTab(\'' + t.id + '\')" title="' + escHtml(t.title) + '">';
|
|
217
|
+
html += escHtml(t.title);
|
|
218
|
+
html += '<span class="cc-tab-close" onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\')">×</span>';
|
|
219
|
+
html += '</div>';
|
|
220
|
+
}
|
|
221
|
+
html += '<button id="cc-all-btn" onclick="ccShowAllConversations()" style="background:none;border:1px solid var(--border);color:var(--muted);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;white-space:nowrap;flex-shrink:0;margin-left:auto" title="All conversations">▼</button>';
|
|
222
|
+
bar.innerHTML = html;
|
|
66
223
|
}
|
|
67
224
|
|
|
68
225
|
function ccRestoreMessages() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
226
|
+
var el = document.getElementById('cc-messages');
|
|
227
|
+
var tab = _ccActiveTab();
|
|
228
|
+
if (!tab) return;
|
|
229
|
+
if (el.children.length > 0 || tab.messages.length === 0) return; // Already rendered or nothing to restore
|
|
230
|
+
for (var i = 0; i < tab.messages.length; i++) {
|
|
231
|
+
ccAddMessage(tab.messages[i].role, tab.messages[i].html, true);
|
|
73
232
|
}
|
|
74
233
|
// Restore "thinking" indicator if CC was mid-request when page refreshed
|
|
75
234
|
try {
|
|
76
|
-
|
|
235
|
+
var sendingState = JSON.parse(localStorage.getItem('cc-sending') || 'null');
|
|
77
236
|
// Only restore sending state if very recent (< 10s) — page refresh kills the SSE stream
|
|
78
237
|
if (sendingState?.sending && (Date.now() - sendingState.startedAt) < 10000) {
|
|
79
238
|
_ccSending = true;
|
|
80
|
-
|
|
81
|
-
|
|
239
|
+
var elapsed = Date.now() - sendingState.startedAt;
|
|
240
|
+
var thinking = document.createElement('div');
|
|
82
241
|
thinking.id = 'cc-thinking';
|
|
83
242
|
thinking.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:11px;color:var(--muted);align-self:flex-start;display:flex;align-items:center;gap:8px';
|
|
84
243
|
thinking.innerHTML = '<span class="dot-pulse" style="display:inline-flex;gap:3px"><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.2s"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.4s"></span></span> <span id="cc-thinking-text">Still working...</span> <span id="cc-thinking-time" style="font-size:10px;color:var(--border)">' + Math.floor(elapsed / 1000) + 's</span>' +
|
|
85
|
-
' <button onclick="
|
|
244
|
+
' <button onclick="ccNewTab()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Reset</button>';
|
|
86
245
|
el.appendChild(thinking);
|
|
87
246
|
el.scrollTop = el.scrollHeight;
|
|
88
247
|
// Update timer
|
|
89
|
-
|
|
90
|
-
|
|
248
|
+
var startTime = sendingState.startedAt;
|
|
249
|
+
var restoreTimer = setInterval(function() {
|
|
91
250
|
var timeEl = document.getElementById('cc-thinking-time');
|
|
92
251
|
if (!timeEl || !_ccSending) { clearInterval(restoreTimer); return; }
|
|
93
252
|
timeEl.textContent = Math.floor((Date.now() - startTime) / 1000) + 's';
|
|
@@ -96,28 +255,34 @@ function ccRestoreMessages() {
|
|
|
96
255
|
} catch {}
|
|
97
256
|
}
|
|
98
257
|
|
|
99
|
-
|
|
258
|
+
var _ccSaveDebounce = null;
|
|
100
259
|
function ccSaveState() {
|
|
101
|
-
// Trim
|
|
102
|
-
|
|
260
|
+
// Trim per-tab messages to prevent unbounded growth
|
|
261
|
+
for (var i = 0; i < _ccTabs.length; i++) {
|
|
262
|
+
if (_ccTabs[i].messages.length > 100) _ccTabs[i].messages = _ccTabs[i].messages.slice(-100);
|
|
263
|
+
}
|
|
103
264
|
// Debounce localStorage writes — no need to serialize on every message during streaming
|
|
104
265
|
if (_ccSaveDebounce) return;
|
|
105
|
-
_ccSaveDebounce = setTimeout(()
|
|
266
|
+
_ccSaveDebounce = setTimeout(function() {
|
|
106
267
|
_ccSaveDebounce = null;
|
|
107
268
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
269
|
+
// Save tabs with trimmed messages
|
|
270
|
+
var toSave = _ccTabs.map(function(t) {
|
|
271
|
+
return { id: t.id, title: t.title, sessionId: t.sessionId, messages: t.messages.slice(-CC_MAX_MESSAGES_PER_TAB) };
|
|
272
|
+
});
|
|
273
|
+
localStorage.setItem('cc-tabs', JSON.stringify(toSave));
|
|
274
|
+
if (_ccActiveTabId) localStorage.setItem('cc-active-tab', _ccActiveTabId);
|
|
111
275
|
} catch { /* localStorage might be full */ }
|
|
112
276
|
}, 500);
|
|
113
277
|
}
|
|
114
278
|
|
|
115
279
|
function ccUpdateSessionIndicator() {
|
|
116
|
-
|
|
280
|
+
var el = document.getElementById('cc-session-info');
|
|
117
281
|
if (!el) return;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
282
|
+
var tab = _ccActiveTab();
|
|
283
|
+
if (tab && tab.sessionId) {
|
|
284
|
+
var turns = tab.messages.filter(function(m) { return m.role === 'user'; }).length;
|
|
285
|
+
el.textContent = 'Session: ' + turns + ' turn' + (turns !== 1 ? 's' : '');
|
|
121
286
|
el.style.color = 'var(--green)';
|
|
122
287
|
} else {
|
|
123
288
|
el.textContent = 'Ready';
|
|
@@ -126,26 +291,39 @@ function ccUpdateSessionIndicator() {
|
|
|
126
291
|
}
|
|
127
292
|
|
|
128
293
|
function ccAddMessage(role, html, skipSave) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
294
|
+
var el = document.getElementById('cc-messages');
|
|
295
|
+
var isUser = role === 'user';
|
|
296
|
+
var div = document.createElement('div');
|
|
297
|
+
var isAssistant = !isUser;
|
|
133
298
|
div.className = isAssistant ? 'cc-msg-assistant' : '';
|
|
134
299
|
div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
|
|
135
300
|
(isUser ? 'background:var(--blue);color:#fff;align-self:flex-end' : 'background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--border);position:relative');
|
|
136
301
|
div.innerHTML = (isAssistant && !html.includes('color:var(--red)') && !html.includes('cc-queued-pill') ? llmCopyBtn() : '') + html;
|
|
137
|
-
|
|
302
|
+
var wasNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
|
|
138
303
|
el.appendChild(div);
|
|
139
|
-
if (wasNearBottom || isUser) requestAnimationFrame(()
|
|
304
|
+
if (wasNearBottom || isUser) requestAnimationFrame(function() { el.scrollTop = el.scrollHeight; });
|
|
140
305
|
if (!skipSave) {
|
|
141
|
-
|
|
306
|
+
var tab = _ccActiveTab();
|
|
307
|
+
if (tab) {
|
|
308
|
+
tab.messages.push({ role: role, html: html });
|
|
309
|
+
// Auto-title from first user message
|
|
310
|
+
if (role === 'user' && tab.title === 'New chat') {
|
|
311
|
+
var tmp = document.createElement('div');
|
|
312
|
+
tmp.innerHTML = html;
|
|
313
|
+
var txt = (tmp.textContent || tmp.innerText || '').trim();
|
|
314
|
+
if (txt.length > 0) {
|
|
315
|
+
tab.title = txt.slice(0, CC_TITLE_MAX_LENGTH);
|
|
316
|
+
ccRenderTabBar();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
142
320
|
ccSaveState();
|
|
143
321
|
}
|
|
144
322
|
}
|
|
145
323
|
|
|
146
324
|
async function ccSend() {
|
|
147
|
-
|
|
148
|
-
|
|
325
|
+
var input = document.getElementById('cc-input');
|
|
326
|
+
var message = input.value.trim();
|
|
149
327
|
if (!message) return;
|
|
150
328
|
input.value = '';
|
|
151
329
|
|
|
@@ -155,17 +333,17 @@ async function ccSend() {
|
|
|
155
333
|
_renderQueueIndicator();
|
|
156
334
|
return;
|
|
157
335
|
}
|
|
158
|
-
|
|
336
|
+
var wasAborted = await _ccDoSend(message);
|
|
159
337
|
|
|
160
338
|
// Drain queue — send each queued message in order
|
|
161
339
|
while (_ccQueue.length > 0) {
|
|
162
340
|
// After abort, pause to let server release ccInFlight guard before next send
|
|
163
341
|
if (wasAborted) {
|
|
164
342
|
_ccSending = true; // keep sending flag true so new messages queue instead of bypassing
|
|
165
|
-
await new Promise(r
|
|
343
|
+
await new Promise(function(r) { setTimeout(r, 500); });
|
|
166
344
|
_ccSending = false;
|
|
167
345
|
}
|
|
168
|
-
|
|
346
|
+
var next = _ccQueue.shift();
|
|
169
347
|
_renderQueueIndicator();
|
|
170
348
|
wasAborted = await _ccDoSend(next);
|
|
171
349
|
}
|
|
@@ -210,7 +388,7 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
210
388
|
|
|
211
389
|
_ccSending = true;
|
|
212
390
|
_ccAbortController = new AbortController();
|
|
213
|
-
|
|
391
|
+
var _wasAborted = false;
|
|
214
392
|
try { localStorage.setItem('cc-sending', JSON.stringify({ sending: true, startedAt: Date.now() })); } catch {}
|
|
215
393
|
|
|
216
394
|
if (!skipUserMsg) ccAddMessage('user', escHtml(message));
|
|
@@ -219,8 +397,8 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
219
397
|
var existingQueueEl = document.getElementById('cc-queue-indicator');
|
|
220
398
|
if (existingQueueEl) existingQueueEl.remove();
|
|
221
399
|
|
|
222
|
-
|
|
223
|
-
|
|
400
|
+
var ccStartTime = Date.now();
|
|
401
|
+
var phases = [
|
|
224
402
|
[0, 'Thinking...'],
|
|
225
403
|
[3000, 'Reading minions context...'],
|
|
226
404
|
[8000, 'Analyzing...'],
|
|
@@ -232,14 +410,19 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
232
410
|
];
|
|
233
411
|
|
|
234
412
|
// Streaming state — declared before try so updateStreamDiv works during fetch
|
|
235
|
-
|
|
236
|
-
|
|
413
|
+
var streamedText = '';
|
|
414
|
+
var toolsUsed = [];
|
|
415
|
+
|
|
416
|
+
// Get active tab's sessionId to send with request
|
|
417
|
+
var activeTab = _ccActiveTab();
|
|
418
|
+
var tabSessionId = activeTab ? activeTab.sessionId : null;
|
|
419
|
+
var activeTabId = _ccActiveTabId;
|
|
237
420
|
|
|
238
421
|
// Show thinking immediately — before fetch starts
|
|
239
422
|
ccAddMessage('assistant', '<span style="color:var(--muted);font-size:11px">Thinking...</span>', true);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
423
|
+
var msgs = document.getElementById('cc-messages');
|
|
424
|
+
var streamDiv = msgs.lastElementChild;
|
|
425
|
+
var dotPulse = '<span style="display:inline-flex;gap:3px;margin-left:6px;vertical-align:middle"><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.2s"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.4s"></span></span>';
|
|
243
426
|
function _getThinkingHtml() {
|
|
244
427
|
var elapsed = Date.now() - ccStartTime;
|
|
245
428
|
var label = 'Thinking...';
|
|
@@ -269,39 +452,40 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
269
452
|
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
270
453
|
}
|
|
271
454
|
// Start phase timer immediately so thinking text updates while waiting for SSE
|
|
272
|
-
|
|
455
|
+
var phaseTimer = setInterval(updateStreamDiv, 1000);
|
|
273
456
|
updateStreamDiv(); // render proper layout immediately (not raw "Thinking..." text)
|
|
274
457
|
|
|
275
458
|
try {
|
|
276
459
|
// Stream response via SSE — shows text as it arrives
|
|
277
|
-
|
|
460
|
+
var res = await fetch('/api/command-center/stream', {
|
|
278
461
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
279
|
-
body: JSON.stringify({ message }),
|
|
462
|
+
body: JSON.stringify({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }),
|
|
280
463
|
signal: _ccAbortController ? _ccAbortController.signal : AbortSignal.timeout(960000)
|
|
281
464
|
});
|
|
282
465
|
|
|
283
466
|
if (!res.ok) {
|
|
284
467
|
clearInterval(phaseTimer); streamDiv.remove();
|
|
285
|
-
|
|
468
|
+
var errText = await res.text();
|
|
286
469
|
ccAddMessage('assistant', '<span style="color:var(--red)">' + escHtml(errText || 'CC error') + '</span>' +
|
|
287
|
-
(errText.includes('busy') ? ' <button onclick="
|
|
470
|
+
(errText.includes('busy') ? ' <button onclick="ccNewTab()" style="margin-top:4px;padding:3px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:10px">Reset CC</button>' : ''));
|
|
288
471
|
return;
|
|
289
472
|
}
|
|
290
473
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
474
|
+
var reader = res.body.getReader();
|
|
475
|
+
var decoder = new TextDecoder();
|
|
476
|
+
var buf = '';
|
|
294
477
|
|
|
295
478
|
while (true) {
|
|
296
|
-
|
|
297
|
-
if (done) break;
|
|
298
|
-
buf += decoder.decode(value, { stream: true });
|
|
299
|
-
|
|
479
|
+
var readResult = await reader.read();
|
|
480
|
+
if (readResult.done) break;
|
|
481
|
+
buf += decoder.decode(readResult.value, { stream: true });
|
|
482
|
+
var lines = buf.split('\n');
|
|
300
483
|
buf = lines.pop();
|
|
301
|
-
for (
|
|
484
|
+
for (var li = 0; li < lines.length; li++) {
|
|
485
|
+
var line = lines[li];
|
|
302
486
|
if (!line.startsWith('data: ')) continue;
|
|
303
487
|
try {
|
|
304
|
-
|
|
488
|
+
var evt = JSON.parse(line.slice(6));
|
|
305
489
|
if (evt.type === 'chunk') {
|
|
306
490
|
streamedText = evt.text;
|
|
307
491
|
updateStreamDiv();
|
|
@@ -313,12 +497,16 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
313
497
|
// Replace streaming div with a proper ccAddMessage
|
|
314
498
|
clearInterval(phaseTimer); streamDiv.remove();
|
|
315
499
|
// placeholder was added with skipSave=true — nothing to pop
|
|
316
|
-
|
|
317
|
-
|
|
500
|
+
var ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
|
|
501
|
+
var rendered = renderMd(evt.text || streamedText || '');
|
|
318
502
|
ccAddMessage('assistant', rendered + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed + 's</div>');
|
|
319
|
-
if (evt.sessionId) {
|
|
503
|
+
if (evt.sessionId) {
|
|
504
|
+
var currentTab = _ccActiveTab();
|
|
505
|
+
if (currentTab) { currentTab.sessionId = evt.sessionId; }
|
|
506
|
+
ccSaveState(); ccUpdateSessionIndicator();
|
|
507
|
+
}
|
|
320
508
|
if (evt.actions && evt.actions.length > 0) {
|
|
321
|
-
for (
|
|
509
|
+
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai]); }
|
|
322
510
|
}
|
|
323
511
|
} else if (evt.type === 'error') {
|
|
324
512
|
clearInterval(phaseTimer); streamDiv.remove();
|
|
@@ -330,22 +518,28 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
330
518
|
}
|
|
331
519
|
// Process any remaining buffered data after stream ends
|
|
332
520
|
if (buf.trim()) {
|
|
333
|
-
|
|
334
|
-
|
|
521
|
+
var remainingLines = buf.split('\n');
|
|
522
|
+
for (var ri = 0; ri < remainingLines.length; ri++) {
|
|
523
|
+
var rline = remainingLines[ri];
|
|
524
|
+
if (!rline.startsWith('data: ')) continue;
|
|
335
525
|
try {
|
|
336
|
-
|
|
337
|
-
if (
|
|
526
|
+
var revt = JSON.parse(rline.slice(6));
|
|
527
|
+
if (revt.type === 'done') {
|
|
338
528
|
clearInterval(phaseTimer); streamDiv.remove();
|
|
339
529
|
// placeholder was skipSave — no pop needed
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
ccAddMessage('assistant',
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
530
|
+
var ccElapsed2 = Math.round((Date.now() - ccStartTime) / 1000);
|
|
531
|
+
var rendered2 = renderMd(revt.text || streamedText || '');
|
|
532
|
+
ccAddMessage('assistant', rendered2 + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed2 + 's</div>');
|
|
533
|
+
if (revt.sessionId) {
|
|
534
|
+
var currentTab2 = _ccActiveTab();
|
|
535
|
+
if (currentTab2) { currentTab2.sessionId = revt.sessionId; }
|
|
536
|
+
ccSaveState(); ccUpdateSessionIndicator();
|
|
346
537
|
}
|
|
347
|
-
|
|
348
|
-
|
|
538
|
+
if (revt.actions && revt.actions.length > 0) {
|
|
539
|
+
for (var ai2 = 0; ai2 < revt.actions.length; ai2++) { await ccExecuteAction(revt.actions[ai2]); }
|
|
540
|
+
}
|
|
541
|
+
} else if (revt.type === 'chunk') {
|
|
542
|
+
streamedText = revt.text;
|
|
349
543
|
updateStreamDiv();
|
|
350
544
|
}
|
|
351
545
|
} catch {}
|
|
@@ -355,8 +549,8 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
355
549
|
if (streamDiv.parentNode) {
|
|
356
550
|
clearInterval(phaseTimer); streamDiv.remove();
|
|
357
551
|
if (streamedText) {
|
|
358
|
-
|
|
359
|
-
ccAddMessage('assistant', renderMd(streamedText) + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' +
|
|
552
|
+
var ccElapsed3 = Math.round((Date.now() - ccStartTime) / 1000);
|
|
553
|
+
ccAddMessage('assistant', renderMd(streamedText) + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed3 + 's</div>');
|
|
360
554
|
}
|
|
361
555
|
}
|
|
362
556
|
} catch (e) {
|
|
@@ -364,19 +558,19 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
364
558
|
if (e.name === 'AbortError') {
|
|
365
559
|
_wasAborted = true;
|
|
366
560
|
if (streamedText) {
|
|
367
|
-
|
|
368
|
-
ccAddMessage('assistant', renderMd(streamedText) + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">Stopped after ' +
|
|
561
|
+
var ccElapsed4 = Math.round((Date.now() - ccStartTime) / 1000);
|
|
562
|
+
ccAddMessage('assistant', renderMd(streamedText) + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">Stopped after ' + ccElapsed4 + 's</div>');
|
|
369
563
|
} else {
|
|
370
564
|
ccAddMessage('assistant', '<span style="color:var(--red);font-size:11px">Stopped</span>');
|
|
371
565
|
}
|
|
372
566
|
} else {
|
|
373
|
-
|
|
374
|
-
|
|
567
|
+
var retryId = 'cc-retry-' + Date.now();
|
|
568
|
+
var isNetworkError = e.message === 'Failed to fetch' || e.message.includes('NetworkError');
|
|
375
569
|
ccAddMessage('assistant', '<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>' +
|
|
376
570
|
(isNetworkError ? '<div style="font-size:10px;color:var(--muted);margin-top:4px">Dashboard connection lost. Reload the page to reconnect.</div>' : '') +
|
|
377
571
|
'<button id="' + retryId + '" onclick="ccRetryLast()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:11px">Retry</button>' +
|
|
378
572
|
(isNetworkError ? ' <button onclick="location.reload()" style="margin-top:6px;padding:4px 12px;background:var(--orange);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">Reload Page</button>' : '') +
|
|
379
|
-
' <button onclick="
|
|
573
|
+
' <button onclick="ccNewTab()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer;font-size:11px">New Session</button>');
|
|
380
574
|
}
|
|
381
575
|
} finally {
|
|
382
576
|
_ccSending = false;
|
|
@@ -391,21 +585,23 @@ async function _ccDoSend(message, skipUserMsg) {
|
|
|
391
585
|
|
|
392
586
|
function ccRetryLast() {
|
|
393
587
|
// Find the last user message and resend it
|
|
394
|
-
|
|
588
|
+
var tab = _ccActiveTab();
|
|
589
|
+
if (!tab) return;
|
|
590
|
+
var last = tab.messages.filter(function(m) { return m.role === 'user'; }).pop();
|
|
395
591
|
if (!last) return;
|
|
396
592
|
// Extract text from the HTML (strip tags)
|
|
397
|
-
|
|
593
|
+
var tmp = document.createElement('div');
|
|
398
594
|
tmp.innerHTML = last.html;
|
|
399
|
-
|
|
595
|
+
var text = tmp.textContent || tmp.innerText || '';
|
|
400
596
|
if (!text.trim()) return;
|
|
401
597
|
// Remove the error message (last assistant message)
|
|
402
|
-
|
|
598
|
+
var el = document.getElementById('cc-messages');
|
|
403
599
|
if (el?.lastElementChild) el.lastElementChild.remove();
|
|
404
|
-
|
|
600
|
+
tab.messages = tab.messages.slice(0, -1); // remove error from history
|
|
405
601
|
// Resend, then drain queue
|
|
406
|
-
_ccDoSend(text.trim()).then(async ()
|
|
602
|
+
_ccDoSend(text.trim()).then(async function() {
|
|
407
603
|
while (_ccQueue.length > 0) {
|
|
408
|
-
|
|
604
|
+
var next = _ccQueue.shift();
|
|
409
605
|
_renderQueueIndicator();
|
|
410
606
|
await _ccDoSend(next);
|
|
411
607
|
}
|
|
@@ -413,25 +609,25 @@ function ccRetryLast() {
|
|
|
413
609
|
}
|
|
414
610
|
|
|
415
611
|
async function _ccFetch(url, body) {
|
|
416
|
-
|
|
417
|
-
if (!res.ok) {
|
|
612
|
+
var res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
613
|
+
if (!res.ok) { var d = await res.json().catch(function() { return {}; }); throw new Error(d.error || 'Request failed (' + res.status + ')'); }
|
|
418
614
|
return res;
|
|
419
615
|
}
|
|
420
616
|
|
|
421
617
|
async function ccExecuteAction(action) {
|
|
422
|
-
|
|
423
|
-
|
|
618
|
+
var msgs = document.getElementById('cc-messages');
|
|
619
|
+
var status = document.createElement('div');
|
|
424
620
|
status.style.cssText = 'padding:4px 10px;border-radius:4px;font-size:10px;align-self:flex-start;border:1px dashed var(--border);color:var(--muted)';
|
|
425
621
|
|
|
426
622
|
try {
|
|
427
623
|
switch (action.type) {
|
|
428
624
|
case 'dispatch': {
|
|
429
|
-
|
|
625
|
+
var res = await _ccFetch('/api/work-items', {
|
|
430
626
|
title: action.title, type: action.workType || 'implement',
|
|
431
627
|
priority: action.priority || 'medium', description: action.description || '',
|
|
432
628
|
project: action.project || '', agents: action.agents || [],
|
|
433
629
|
});
|
|
434
|
-
|
|
630
|
+
var d = await res.json();
|
|
435
631
|
status.innerHTML = '✓ Dispatched: <strong>' + escHtml(d.id || action.title) + '</strong>';
|
|
436
632
|
status.style.color = 'var(--green)';
|
|
437
633
|
wakeEngine();
|
|
@@ -465,8 +661,8 @@ async function ccExecuteAction(action) {
|
|
|
465
661
|
break;
|
|
466
662
|
}
|
|
467
663
|
case 'retry': {
|
|
468
|
-
for (
|
|
469
|
-
await _ccFetch('/api/work-items/retry', { id, source: '' });
|
|
664
|
+
for (var ri = 0; ri < (action.ids || []).length; ri++) {
|
|
665
|
+
await _ccFetch('/api/work-items/retry', { id: action.ids[ri], source: '' });
|
|
470
666
|
}
|
|
471
667
|
status.innerHTML = '✓ Retried: <strong>' + escHtml((action.ids || []).join(', ')) + '</strong>';
|
|
472
668
|
status.style.color = 'var(--green)';
|
|
@@ -505,9 +701,9 @@ async function ccExecuteAction(action) {
|
|
|
505
701
|
}
|
|
506
702
|
case 'plan-edit': {
|
|
507
703
|
// Read the plan, send instruction to doc-chat, show version actions
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
704
|
+
var normalizedFile = normalizePlanFile(action.file);
|
|
705
|
+
var planContent = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(function(r) { return r.text(); });
|
|
706
|
+
var res2 = await fetch('/api/doc-chat', {
|
|
511
707
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
512
708
|
body: JSON.stringify({
|
|
513
709
|
message: action.instruction,
|
|
@@ -516,7 +712,7 @@ async function ccExecuteAction(action) {
|
|
|
516
712
|
filePath: 'plans/' + normalizedFile,
|
|
517
713
|
}),
|
|
518
714
|
});
|
|
519
|
-
|
|
715
|
+
var data = await res2.json();
|
|
520
716
|
if (data.ok && data.edited) {
|
|
521
717
|
status.innerHTML = '✓ Plan edited: <strong>' + escHtml(action.file) + '</strong>';
|
|
522
718
|
status.style.color = 'var(--green)';
|
|
@@ -539,7 +735,7 @@ async function ccExecuteAction(action) {
|
|
|
539
735
|
}
|
|
540
736
|
case 'file-edit': {
|
|
541
737
|
// doc-chat reads current content from disk via filePath — pass placeholder for required field
|
|
542
|
-
|
|
738
|
+
var res3 = await fetch('/api/doc-chat', {
|
|
543
739
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
544
740
|
body: JSON.stringify({
|
|
545
741
|
message: action.instruction,
|
|
@@ -548,19 +744,19 @@ async function ccExecuteAction(action) {
|
|
|
548
744
|
filePath: action.file,
|
|
549
745
|
}),
|
|
550
746
|
});
|
|
551
|
-
|
|
552
|
-
if (
|
|
747
|
+
var data3 = await res3.json();
|
|
748
|
+
if (data3.ok && data3.edited) {
|
|
553
749
|
status.innerHTML = '✓ Edited: <strong>' + escHtml(action.file) + '</strong>';
|
|
554
750
|
status.style.color = 'var(--green)';
|
|
555
751
|
} else {
|
|
556
|
-
status.innerHTML =
|
|
557
|
-
status.style.color =
|
|
752
|
+
status.innerHTML = data3.answer ? renderMd(data3.answer) : '✗ Could not edit file';
|
|
753
|
+
status.style.color = data3.answer ? 'var(--muted)' : 'var(--red)';
|
|
558
754
|
}
|
|
559
755
|
break;
|
|
560
756
|
}
|
|
561
757
|
case 'schedule': {
|
|
562
|
-
|
|
563
|
-
|
|
758
|
+
var url = action._update ? '/api/schedules/update' : '/api/schedules';
|
|
759
|
+
var res4 = await fetch(url, {
|
|
564
760
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
565
761
|
body: JSON.stringify({
|
|
566
762
|
id: action.id, title: action.title, cron: action.cron,
|
|
@@ -570,54 +766,55 @@ async function ccExecuteAction(action) {
|
|
|
570
766
|
enabled: action.enabled !== false,
|
|
571
767
|
})
|
|
572
768
|
});
|
|
573
|
-
if (!
|
|
769
|
+
if (!res4.ok) { var d4 = await res4.json().catch(function() { return {}; }); throw new Error(d4.error || 'Schedule create failed'); }
|
|
574
770
|
status.innerHTML = '✓ Schedule ' + (action._update ? 'updated' : 'created') + ': <strong>' + escHtml(action.id) + '</strong>';
|
|
575
771
|
status.style.color = 'var(--green)';
|
|
576
772
|
break;
|
|
577
773
|
}
|
|
578
774
|
case 'delete-schedule': {
|
|
579
|
-
|
|
775
|
+
var res5 = await fetch('/api/schedules/delete', {
|
|
580
776
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
581
777
|
body: JSON.stringify({ id: action.id })
|
|
582
778
|
});
|
|
583
|
-
if (!
|
|
779
|
+
if (!res5.ok) { var d5 = await res5.json().catch(function() { return {}; }); throw new Error(d5.error || 'Schedule delete failed'); }
|
|
584
780
|
status.innerHTML = '✓ Deleted schedule: <strong>' + escHtml(action.id) + '</strong>';
|
|
585
781
|
status.style.color = 'var(--orange)';
|
|
586
782
|
break;
|
|
587
783
|
}
|
|
588
784
|
case 'create-meeting': {
|
|
589
|
-
|
|
785
|
+
var res6 = await fetch('/api/meetings', {
|
|
590
786
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
591
787
|
body: JSON.stringify({ title: action.title, agenda: action.agenda, participants: action.agents, rounds: action.rounds, project: action.project })
|
|
592
788
|
});
|
|
593
|
-
if (!
|
|
594
|
-
|
|
595
|
-
status.innerHTML = '✓ Meeting started: <strong>' + escHtml(action.title) + '</strong>' + (
|
|
789
|
+
if (!res6.ok) { var d6 = await res6.json().catch(function() { return {}; }); throw new Error(d6.error || 'Meeting create failed'); }
|
|
790
|
+
var d6r = await res6.json();
|
|
791
|
+
status.innerHTML = '✓ Meeting started: <strong>' + escHtml(action.title) + '</strong>' + (d6r.id ? ' (' + escHtml(d6r.id) + ')' : '');
|
|
596
792
|
status.style.color = 'var(--green)';
|
|
597
793
|
wakeEngine();
|
|
598
794
|
break;
|
|
599
795
|
}
|
|
600
796
|
case 'set-config': {
|
|
601
|
-
|
|
602
|
-
|
|
797
|
+
var payload = { engine: {} };
|
|
798
|
+
payload.engine[action.setting] = action.value;
|
|
799
|
+
var res7 = await fetch('/api/settings', {
|
|
603
800
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
604
801
|
body: JSON.stringify(payload)
|
|
605
802
|
});
|
|
606
|
-
if (!
|
|
803
|
+
if (!res7.ok) { var d7 = await res7.json().catch(function() { return {}; }); throw new Error(d7.error || 'Config update failed'); }
|
|
607
804
|
status.innerHTML = '✓ Set <strong>' + escHtml(action.setting) + '</strong> = ' + escHtml(String(action.value));
|
|
608
805
|
status.style.color = 'var(--green)';
|
|
609
806
|
break;
|
|
610
807
|
}
|
|
611
808
|
case 'edit-pipeline': {
|
|
612
|
-
|
|
809
|
+
var body = { id: action.id };
|
|
613
810
|
if (action.title) body.title = action.title;
|
|
614
811
|
if (action.stages) body.stages = action.stages;
|
|
615
812
|
if (action.trigger !== undefined) body.trigger = action.trigger;
|
|
616
|
-
|
|
813
|
+
var res8 = await fetch('/api/pipelines/update', {
|
|
617
814
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
618
815
|
body: JSON.stringify(body)
|
|
619
816
|
});
|
|
620
|
-
if (!
|
|
817
|
+
if (!res8.ok) { var d8 = await res8.json().catch(function() { return {}; }); throw new Error(d8.error || 'Pipeline update failed'); }
|
|
621
818
|
status.innerHTML = '✓ Updated pipeline: <strong>' + escHtml(action.id) + '</strong>';
|
|
622
819
|
status.style.color = 'var(--green)';
|
|
623
820
|
break;
|
|
@@ -655,11 +852,11 @@ async function ccExecuteAction(action) {
|
|
|
655
852
|
break;
|
|
656
853
|
}
|
|
657
854
|
case 'trigger-pipeline': {
|
|
658
|
-
var
|
|
855
|
+
var res9 = await fetch('/api/pipelines/trigger', {
|
|
659
856
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
660
857
|
body: JSON.stringify({ id: action.id })
|
|
661
858
|
});
|
|
662
|
-
if (!
|
|
859
|
+
if (!res9.ok) { var d9 = await res9.json().catch(function() { return {}; }); throw new Error(d9.error || 'Pipeline trigger failed'); }
|
|
663
860
|
status.innerHTML = '✓ Pipeline triggered: <strong>' + escHtml(action.id) + '</strong>';
|
|
664
861
|
status.style.color = 'var(--green)';
|
|
665
862
|
wakeEngine();
|
|
@@ -686,10 +883,10 @@ async function ccExecuteAction(action) {
|
|
|
686
883
|
break;
|
|
687
884
|
}
|
|
688
885
|
case 'file-bug': {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
status.innerHTML = '🐛 Bug filed: <a href="' + escHtml(
|
|
886
|
+
var res10 = await _ccFetch('/api/issues/create', { title: action.title, description: action.description, labels: action.labels });
|
|
887
|
+
var d10 = await res10.json();
|
|
888
|
+
if (d10.url) {
|
|
889
|
+
status.innerHTML = '🐛 Bug filed: <a href="' + escHtml(d10.url) + '" target="_blank" style="color:var(--blue)">' + escHtml(action.title) + '</a>';
|
|
693
890
|
} else {
|
|
694
891
|
status.innerHTML = '🐛 Bug filed: <strong>' + escHtml(action.title) + '</strong> — <a href="https://github.com/yemi33/minions/issues" target="_blank" style="color:var(--blue)">view issues</a>';
|
|
695
892
|
}
|
|
@@ -781,33 +978,33 @@ async function ccExecuteAction(action) {
|
|
|
781
978
|
}
|
|
782
979
|
|
|
783
980
|
// --- CC Resize Logic ---
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
981
|
+
var CC_MIN_WIDTH = 320;
|
|
982
|
+
var CC_MAX_WIDTH_RATIO = 0.8; // 80% of viewport
|
|
983
|
+
var CC_DEFAULT_WIDTH = 420;
|
|
984
|
+
var CC_WIDTH_KEY = 'cc-drawer-width';
|
|
788
985
|
|
|
789
986
|
function ccApplySavedWidth() {
|
|
790
|
-
|
|
987
|
+
var drawer = document.getElementById('cc-drawer');
|
|
791
988
|
if (!drawer) return;
|
|
792
|
-
|
|
989
|
+
var saved = parseInt(localStorage.getItem(CC_WIDTH_KEY), 10);
|
|
793
990
|
if (saved && saved >= CC_MIN_WIDTH) {
|
|
794
|
-
|
|
991
|
+
var maxW = Math.floor(window.innerWidth * CC_MAX_WIDTH_RATIO);
|
|
795
992
|
drawer.style.width = Math.min(saved, maxW) + 'px';
|
|
796
993
|
}
|
|
797
994
|
}
|
|
798
995
|
|
|
799
996
|
function ccInitResize() {
|
|
800
|
-
|
|
801
|
-
|
|
997
|
+
var handle = document.getElementById('cc-resize-handle');
|
|
998
|
+
var drawer = document.getElementById('cc-drawer');
|
|
802
999
|
if (!handle || !drawer) return;
|
|
803
1000
|
|
|
804
|
-
|
|
805
|
-
|
|
1001
|
+
var startX = 0;
|
|
1002
|
+
var startW = 0;
|
|
806
1003
|
|
|
807
1004
|
function onMouseMove(e) {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1005
|
+
var maxW = Math.floor(window.innerWidth * CC_MAX_WIDTH_RATIO);
|
|
1006
|
+
var delta = startX - e.clientX; // dragging left = wider
|
|
1007
|
+
var newW = Math.max(CC_MIN_WIDTH, Math.min(startW + delta, maxW));
|
|
811
1008
|
drawer.style.width = newW + 'px';
|
|
812
1009
|
}
|
|
813
1010
|
|
|
@@ -820,7 +1017,7 @@ function ccInitResize() {
|
|
|
820
1017
|
try { localStorage.setItem(CC_WIDTH_KEY, parseInt(drawer.style.width, 10)); } catch {}
|
|
821
1018
|
}
|
|
822
1019
|
|
|
823
|
-
handle.addEventListener('mousedown', (e)
|
|
1020
|
+
handle.addEventListener('mousedown', function(e) {
|
|
824
1021
|
e.preventDefault();
|
|
825
1022
|
startX = e.clientX;
|
|
826
1023
|
startW = drawer.offsetWidth;
|
|
@@ -838,4 +1035,4 @@ if (document.readyState === 'loading') {
|
|
|
838
1035
|
ccInitResize();
|
|
839
1036
|
}
|
|
840
1037
|
|
|
841
|
-
window.MinionsCC = { toggleCommandCenter, ccNewSession, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction };
|
|
1038
|
+
window.MinionsCC = { toggleCommandCenter, ccNewSession, ccNewTab, ccSwitchTab, ccCloseTab, ccShowAllConversations, ccRenderTabBar, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction };
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -99,17 +99,12 @@ function _initQaSession() {
|
|
|
99
99
|
}
|
|
100
100
|
if (prior.filePath) _modalFilePath = prior.filePath;
|
|
101
101
|
_showThreadWrap();
|
|
102
|
-
requestAnimationFrame(function() {
|
|
103
|
-
var thread = document.getElementById('modal-qa-thread');
|
|
104
|
-
if (thread) thread.scrollTop = thread.scrollHeight;
|
|
105
|
-
});
|
|
106
102
|
} else {
|
|
107
103
|
_qaHistory = [];
|
|
108
104
|
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
109
|
-
|
|
105
|
+
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
106
|
+
if (wrap) wrap.style.display = 'none';
|
|
110
107
|
}
|
|
111
|
-
var actionSlot = document.getElementById('qa-action-slot');
|
|
112
|
-
if (actionSlot) actionSlot.innerHTML = '';
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
function clearQaConversation() {
|
|
@@ -117,9 +112,8 @@ function clearQaConversation() {
|
|
|
117
112
|
_qaQueue = [];
|
|
118
113
|
_qaProcessing = false;
|
|
119
114
|
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (actionSlot) actionSlot.innerHTML = '';
|
|
115
|
+
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
116
|
+
if (wrap) wrap.style.display = 'none';
|
|
123
117
|
if (_qaSessionKey) _qaSessions.delete(_qaSessionKey);
|
|
124
118
|
}
|
|
125
119
|
|
|
@@ -274,8 +268,7 @@ async function _processQaMessage(message, selection) {
|
|
|
274
268
|
'<button onclick="planExecute(\'' + esc + '\', \'\', null)" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Re-execute plan</button>' +
|
|
275
269
|
'<button onclick="this.closest(\'div\').innerHTML=\'<span style=color:var(--muted);font-size:11px>Paused. No work dispatched.</span>\'" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer">Keep paused</button>' +
|
|
276
270
|
'<span style="color:var(--muted);font-size:10px;width:100%">Re-execute creates a new PRD from the updated plan.</span>';
|
|
277
|
-
|
|
278
|
-
if (slot) { slot.innerHTML = ''; slot.appendChild(actionDiv); }
|
|
271
|
+
thread.appendChild(actionDiv);
|
|
279
272
|
}
|
|
280
273
|
} else {
|
|
281
274
|
const qaElapsedErr = Math.round((Date.now() - qaStartTime) / 1000);
|
|
@@ -346,25 +339,20 @@ function qaAbort() {
|
|
|
346
339
|
|
|
347
340
|
function toggleDocChat() {
|
|
348
341
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
349
|
-
var expandBar = document.getElementById('qa-expand-bar');
|
|
350
342
|
if (!wrap) return;
|
|
351
|
-
var
|
|
352
|
-
wrap.style.display =
|
|
353
|
-
|
|
343
|
+
var visible = wrap.style.display !== 'none';
|
|
344
|
+
wrap.style.display = visible ? 'none' : '';
|
|
345
|
+
var btn = document.getElementById('qa-toggle-btn');
|
|
346
|
+
if (btn) btn.innerHTML = visible ? '▲ Expand' : '▼ Collapse';
|
|
354
347
|
}
|
|
355
348
|
|
|
356
349
|
function _showThreadWrap() {
|
|
357
350
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
function _hideThreadWrap() {
|
|
364
|
-
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
365
|
-
var expandBar = document.getElementById('qa-expand-bar');
|
|
366
|
-
if (wrap) wrap.style.display = 'none';
|
|
367
|
-
if (expandBar) expandBar.style.display = 'none';
|
|
351
|
+
if (wrap && wrap.style.display === 'none') {
|
|
352
|
+
wrap.style.display = '';
|
|
353
|
+
var btn = document.getElementById('qa-toggle-btn');
|
|
354
|
+
if (btn) btn.innerHTML = '▼ Collapse';
|
|
355
|
+
}
|
|
368
356
|
}
|
|
369
357
|
|
|
370
|
-
window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaAbort, toggleDocChat };
|
|
358
|
+
window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaAbort, toggleDocChat, _showThreadWrap };
|
package/dashboard/layout.html
CHANGED
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
<span id="cc-session-info" style="font-size:10px;color:var(--muted)">New session</span>
|
|
33
33
|
</div>
|
|
34
34
|
<div style="display:flex;align-items:center;gap:8px">
|
|
35
|
-
<button onclick="
|
|
35
|
+
<button onclick="ccNewTab()" style="background:none;border:1px solid var(--border);color:var(--muted);font-size:10px;padding:2px 8px;border-radius:3px;cursor:pointer">New Chat</button>
|
|
36
36
|
<button onclick="toggleCommandCenter()" style="background:none;border:none;color:var(--muted);font-size:16px;cursor:pointer;padding:2px 6px">✕</button>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
39
|
+
<div id="cc-tab-bar" style="display:flex;gap:4px;padding:4px 12px;border-bottom:1px solid var(--border);overflow-x:auto;align-items:center;flex-shrink:0"></div>
|
|
39
40
|
<div id="cc-messages" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:10px"></div>
|
|
40
41
|
<div style="padding:12px 16px;border-top:1px solid var(--border)">
|
|
41
42
|
<div style="display:flex;gap:8px">
|
package/dashboard/styles.css
CHANGED
|
@@ -620,6 +620,12 @@
|
|
|
620
620
|
.notif-badge.processing span:nth-child(3) { animation-delay: 0.4s; }
|
|
621
621
|
@keyframes notifPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
622
622
|
|
|
623
|
+
/* Command Center tab bar */
|
|
624
|
+
.cc-tab { padding: 2px 8px; font-size: 10px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface2); color: var(--muted); cursor: pointer; white-space: nowrap; max-width: 120px; overflow: hidden; text-overflow: ellipsis; display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; }
|
|
625
|
+
.cc-tab.active { color: var(--blue); border-color: var(--blue); background: rgba(56,139,253,0.1); }
|
|
626
|
+
.cc-tab-close { margin-left: 4px; opacity: 0.5; cursor: pointer; font-size: 12px; line-height: 1; }
|
|
627
|
+
.cc-tab-close:hover { opacity: 1; color: var(--red); }
|
|
628
|
+
|
|
623
629
|
/* Command Center resize handle */
|
|
624
630
|
.cc-resize-handle { position: absolute; top: 0; left: -3px; bottom: 0; width: 6px; cursor: col-resize; z-index: 301; }
|
|
625
631
|
.cc-resize-handle::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 2px; height: 32px; background: var(--border); border-radius: 2px; opacity: 0; transition: opacity 0.15s; }
|
package/dashboard.js
CHANGED
|
@@ -577,6 +577,7 @@ function parseCCActions(text) {
|
|
|
577
577
|
// ── Shared LLM call core — used by CC panel and doc modals ──────────────────
|
|
578
578
|
|
|
579
579
|
// Session store for doc modals — keyed by filePath or title, persisted to disk
|
|
580
|
+
const CC_SESSIONS_PATH = path.join(ENGINE_DIR, 'cc-sessions.json');
|
|
580
581
|
const DOC_SESSIONS_PATH = path.join(ENGINE_DIR, 'doc-sessions.json');
|
|
581
582
|
const docSessions = new Map(); // key → { sessionId, lastActiveAt, turnCount }
|
|
582
583
|
|
|
@@ -3254,6 +3255,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3254
3255
|
return jsonReply(res, 200, { ok: true });
|
|
3255
3256
|
}
|
|
3256
3257
|
|
|
3258
|
+
async function handleCCSessionsList(req, res) {
|
|
3259
|
+
const sessions = shared.safeJsonArr(CC_SESSIONS_PATH);
|
|
3260
|
+
return jsonReply(res, 200, { sessions });
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
async function handleCCSessionDelete(req, res, match) {
|
|
3264
|
+
const id = match?.[1];
|
|
3265
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
3266
|
+
const sessions = shared.safeJsonArr(CC_SESSIONS_PATH);
|
|
3267
|
+
const filtered = sessions.filter(s => s.id !== id);
|
|
3268
|
+
safeWrite(CC_SESSIONS_PATH, filtered);
|
|
3269
|
+
return jsonReply(res, 200, { ok: true });
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3257
3272
|
async function handleCommandCenter(req, res) {
|
|
3258
3273
|
if (checkRateLimit('command-center', 10)) return jsonReply(res, 429, { error: 'Rate limited — max 10 requests/minute' });
|
|
3259
3274
|
try {
|
|
@@ -3318,12 +3333,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3318
3333
|
req.on('close', () => { ccInFlight = false; ccInFlightSince = 0; if (_ccStreamAbort) _ccStreamAbort(); });
|
|
3319
3334
|
|
|
3320
3335
|
try {
|
|
3321
|
-
// Session management —
|
|
3322
|
-
|
|
3336
|
+
// Session management — per-tab: use sessionId from request if provided
|
|
3337
|
+
const tabSessionId = body.sessionId || null;
|
|
3338
|
+
if (tabSessionId) {
|
|
3339
|
+
// Resume the tab's specific session
|
|
3340
|
+
ccSession = { sessionId: tabSessionId, createdAt: ccSession.createdAt, lastActiveAt: Date.now(), turnCount: ccSession.turnCount || 0, _promptHash: _ccPromptHash };
|
|
3341
|
+
} else if (!ccSessionValid()) {
|
|
3323
3342
|
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
3324
3343
|
}
|
|
3325
|
-
const wasResume = !!(ccSessionValid() && ccSession.sessionId);
|
|
3326
|
-
const sessionId = wasResume ? ccSession.sessionId : null;
|
|
3344
|
+
const wasResume = !!tabSessionId || !!(ccSessionValid() && ccSession.sessionId);
|
|
3345
|
+
const sessionId = tabSessionId || (wasResume ? ccSession.sessionId : null);
|
|
3327
3346
|
const preamble = wasResume ? '' : buildCCStatePreamble();
|
|
3328
3347
|
const prompt = (preamble ? preamble + '\n\n---\n\n' : '') + body.message;
|
|
3329
3348
|
|
|
@@ -3374,6 +3393,25 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3374
3393
|
safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
|
|
3375
3394
|
}
|
|
3376
3395
|
|
|
3396
|
+
// Persist tab→session mapping if tabId provided
|
|
3397
|
+
const tabId = body.tabId;
|
|
3398
|
+
if (tabId && ccSession.sessionId) {
|
|
3399
|
+
try {
|
|
3400
|
+
const sessions = shared.safeJsonArr(CC_SESSIONS_PATH);
|
|
3401
|
+
const existing = sessions.find(s => s.id === tabId);
|
|
3402
|
+
const preview = (body.message || '').slice(0, 80);
|
|
3403
|
+
if (existing) {
|
|
3404
|
+
existing.sessionId = ccSession.sessionId;
|
|
3405
|
+
existing.lastActiveAt = new Date(now).toISOString();
|
|
3406
|
+
existing.turnCount = ccSession.turnCount;
|
|
3407
|
+
existing.preview = preview;
|
|
3408
|
+
} else {
|
|
3409
|
+
sessions.push({ id: tabId, title: (body.message || 'New chat').slice(0, 40), sessionId: ccSession.sessionId, createdAt: new Date(now).toISOString(), lastActiveAt: new Date(now).toISOString(), turnCount: ccSession.turnCount, preview });
|
|
3410
|
+
}
|
|
3411
|
+
safeWrite(CC_SESSIONS_PATH, sessions);
|
|
3412
|
+
} catch { /* non-critical */ }
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3377
3415
|
// Send final result with actions
|
|
3378
3416
|
const { text: displayText, actions } = parseCCActions(result.text);
|
|
3379
3417
|
res.write('data: ' + JSON.stringify({ type: 'done', text: displayText, actions, sessionId: ccSession.sessionId, newSession: !wasResume }) + '\n\n');
|
|
@@ -4060,7 +4098,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4060
4098
|
// Command Center
|
|
4061
4099
|
{ method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
|
|
4062
4100
|
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
|
|
4063
|
-
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message', handler: handleCommandCenterStream },
|
|
4101
|
+
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
|
|
4102
|
+
{ method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
|
|
4103
|
+
{ method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)$/, desc: 'Delete a CC session by tab ID', handler: handleCCSessionDelete },
|
|
4064
4104
|
|
|
4065
4105
|
// Schedules
|
|
4066
4106
|
{ method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.746",
|
|
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"
|