@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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.746 (2026-04-09)
4
+
5
+ ### Features
6
+ - CC multi-tab conversations with session resume
7
+
8
+ ### Fixes
9
+ - CC tabs use per-tab sessions, not shared global session
10
+
3
11
  ## 0.1.744 (2026-04-09)
4
12
 
5
13
  ### Features
@@ -1,14 +1,59 @@
1
1
  // command-center.js — Command center panel functions extracted from dashboard.html
2
2
 
3
- let _ccSessionId = localStorage.getItem('cc-session-id') || null;
4
- let _ccMessages = JSON.parse(localStorage.getItem('cc-messages') || '[]');
5
- let _ccOpen = false;
6
- let _ccSending = false;
7
- let _ccQueue = [];
8
- let _ccAbortController = null;
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
- const drawer = document.getElementById('cc-drawer');
39
- const overlay = document.getElementById('cc-overlay');
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
- fetch('/api/command-center/new-session', { method: 'POST' }).catch(() => {});
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">&times;</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 + '\')">&times;</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">&#x25BC;</button>';
222
+ bar.innerHTML = html;
66
223
  }
67
224
 
68
225
  function ccRestoreMessages() {
69
- const el = document.getElementById('cc-messages');
70
- if (el.children.length > 0 || _ccMessages.length === 0) return; // Already rendered or nothing to restore
71
- for (const msg of _ccMessages) {
72
- ccAddMessage(msg.role, msg.html, true);
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
- const sendingState = JSON.parse(localStorage.getItem('cc-sending') || 'null');
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
- const elapsed = Date.now() - sendingState.startedAt;
81
- const thinking = document.createElement('div');
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="ccNewSession()" 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>';
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
- const startTime = sendingState.startedAt;
90
- const restoreTimer = setInterval(function() {
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
- let _ccSaveDebounce = null;
258
+ var _ccSaveDebounce = null;
100
259
  function ccSaveState() {
101
- // Trim in-memory array to prevent unbounded growth
102
- if (_ccMessages.length > 100) _ccMessages = _ccMessages.slice(-100);
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
- if (_ccSessionId) localStorage.setItem('cc-session-id', _ccSessionId);
109
- const toSave = _ccMessages.slice(-30);
110
- localStorage.setItem('cc-messages', JSON.stringify(toSave));
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
- const el = document.getElementById('cc-session-info');
280
+ var el = document.getElementById('cc-session-info');
117
281
  if (!el) return;
118
- if (_ccSessionId) {
119
- const turns = _ccMessages.filter(m => m.role === 'user').length;
120
- el.textContent = `Session: ${turns} turn${turns !== 1 ? 's' : ''}`;
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
- const el = document.getElementById('cc-messages');
130
- const isUser = role === 'user';
131
- const div = document.createElement('div');
132
- const isAssistant = !isUser;
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
- const wasNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
302
+ var wasNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
138
303
  el.appendChild(div);
139
- if (wasNearBottom || isUser) requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; });
304
+ if (wasNearBottom || isUser) requestAnimationFrame(function() { el.scrollTop = el.scrollHeight; });
140
305
  if (!skipSave) {
141
- _ccMessages.push({ role, html });
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
- const input = document.getElementById('cc-input');
148
- const message = input.value.trim();
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
- let wasAborted = await _ccDoSend(message);
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 => setTimeout(r, 500));
343
+ await new Promise(function(r) { setTimeout(r, 500); });
166
344
  _ccSending = false;
167
345
  }
168
- const next = _ccQueue.shift();
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
- let _wasAborted = false;
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
- const ccStartTime = Date.now();
223
- const phases = [
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
- let streamedText = '';
236
- let toolsUsed = [];
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
- const msgs = document.getElementById('cc-messages');
241
- const streamDiv = msgs.lastElementChild;
242
- const 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>';
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
- const phaseTimer = setInterval(updateStreamDiv, 1000);
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
- const res = await fetch('/api/command-center/stream', {
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
- const errText = await res.text();
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="ccNewSession()" 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>' : ''));
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
- const reader = res.body.getReader();
292
- const decoder = new TextDecoder();
293
- let buf = '';
474
+ var reader = res.body.getReader();
475
+ var decoder = new TextDecoder();
476
+ var buf = '';
294
477
 
295
478
  while (true) {
296
- const { done, value } = await reader.read();
297
- if (done) break;
298
- buf += decoder.decode(value, { stream: true });
299
- const lines = buf.split('\n');
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 (const line of lines) {
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
- const evt = JSON.parse(line.slice(6));
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
- const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
317
- const rendered = renderMd(evt.text || streamedText || '');
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) { _ccSessionId = evt.sessionId; ccSaveState(); ccUpdateSessionIndicator(); }
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 (const action of evt.actions) { await ccExecuteAction(action); }
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
- for (const line of buf.split('\n')) {
334
- if (!line.startsWith('data: ')) continue;
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
- const evt = JSON.parse(line.slice(6));
337
- if (evt.type === 'done') {
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
- const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
341
- const rendered = renderMd(evt.text || streamedText || '');
342
- 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>');
343
- if (evt.sessionId) { _ccSessionId = evt.sessionId; ccSaveState(); ccUpdateSessionIndicator(); }
344
- if (evt.actions && evt.actions.length > 0) {
345
- for (const action of evt.actions) { await ccExecuteAction(action); }
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
- } else if (evt.type === 'chunk') {
348
- streamedText = evt.text;
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
- const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
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">' + ccElapsed + 's</div>');
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
- const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
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 ' + ccElapsed + 's</div>');
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
- const retryId = 'cc-retry-' + Date.now();
374
- const isNetworkError = e.message === 'Failed to fetch' || e.message.includes('NetworkError');
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="ccNewSession()" 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>');
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
- const last = _ccMessages.filter(m => m.role === 'user').pop();
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
- const tmp = document.createElement('div');
593
+ var tmp = document.createElement('div');
398
594
  tmp.innerHTML = last.html;
399
- const text = tmp.textContent || tmp.innerText || '';
595
+ var text = tmp.textContent || tmp.innerText || '';
400
596
  if (!text.trim()) return;
401
597
  // Remove the error message (last assistant message)
402
- const el = document.getElementById('cc-messages');
598
+ var el = document.getElementById('cc-messages');
403
599
  if (el?.lastElementChild) el.lastElementChild.remove();
404
- _ccMessages = _ccMessages.slice(0, -1); // remove error from history
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
- const next = _ccQueue.shift();
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
- const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
417
- if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Request failed (' + res.status + ')'); }
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
- const msgs = document.getElementById('cc-messages');
423
- const status = document.createElement('div');
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
- const res = await _ccFetch('/api/work-items', {
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
- const d = await res.json();
630
+ var d = await res.json();
435
631
  status.innerHTML = '&#10003; 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 (const id of (action.ids || [])) {
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 = '&#10003; 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
- const normalizedFile = normalizePlanFile(action.file);
509
- const planContent = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(r => r.text());
510
- const res = await fetch('/api/doc-chat', {
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
- const data = await res.json();
715
+ var data = await res2.json();
520
716
  if (data.ok && data.edited) {
521
717
  status.innerHTML = '&#10003; 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
- const res = await fetch('/api/doc-chat', {
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
- const data = await res.json();
552
- if (data.ok && data.edited) {
747
+ var data3 = await res3.json();
748
+ if (data3.ok && data3.edited) {
553
749
  status.innerHTML = '&#10003; Edited: <strong>' + escHtml(action.file) + '</strong>';
554
750
  status.style.color = 'var(--green)';
555
751
  } else {
556
- status.innerHTML = data.answer ? renderMd(data.answer) : '&#10007; Could not edit file';
557
- status.style.color = data.answer ? 'var(--muted)' : 'var(--red)';
752
+ status.innerHTML = data3.answer ? renderMd(data3.answer) : '&#10007; 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
- const url = action._update ? '/api/schedules/update' : '/api/schedules';
563
- const res = await fetch(url, {
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 (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Schedule create failed'); }
769
+ if (!res4.ok) { var d4 = await res4.json().catch(function() { return {}; }); throw new Error(d4.error || 'Schedule create failed'); }
574
770
  status.innerHTML = '&#10003; 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
- const res = await fetch('/api/schedules/delete', {
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 (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Schedule delete failed'); }
779
+ if (!res5.ok) { var d5 = await res5.json().catch(function() { return {}; }); throw new Error(d5.error || 'Schedule delete failed'); }
584
780
  status.innerHTML = '&#10003; 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
- const res = await fetch('/api/meetings', {
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 (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Meeting create failed'); }
594
- const d = await res.json();
595
- status.innerHTML = '&#10003; Meeting started: <strong>' + escHtml(action.title) + '</strong>' + (d.id ? ' (' + escHtml(d.id) + ')' : '');
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 = '&#10003; 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
- const payload = { engine: { [action.setting]: action.value } };
602
- const res = await fetch('/api/settings', {
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 (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Config update failed'); }
803
+ if (!res7.ok) { var d7 = await res7.json().catch(function() { return {}; }); throw new Error(d7.error || 'Config update failed'); }
607
804
  status.innerHTML = '&#10003; 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
- const body = { id: action.id };
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
- const res = await fetch('/api/pipelines/update', {
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 (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Pipeline update failed'); }
817
+ if (!res8.ok) { var d8 = await res8.json().catch(function() { return {}; }); throw new Error(d8.error || 'Pipeline update failed'); }
621
818
  status.innerHTML = '&#10003; 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 res = await fetch('/api/pipelines/trigger', {
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 (!res.ok) { var d = await res.json().catch(function() { return {}; }); throw new Error(d.error || 'Pipeline trigger failed'); }
859
+ if (!res9.ok) { var d9 = await res9.json().catch(function() { return {}; }); throw new Error(d9.error || 'Pipeline trigger failed'); }
663
860
  status.innerHTML = '&#10003; 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
- const res = await _ccFetch('/api/issues/create', { title: action.title, description: action.description, labels: action.labels });
690
- const d = await res.json();
691
- if (d.url) {
692
- status.innerHTML = '&#128027; Bug filed: <a href="' + escHtml(d.url) + '" target="_blank" style="color:var(--blue)">' + escHtml(action.title) + '</a>';
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 = '&#128027; Bug filed: <a href="' + escHtml(d10.url) + '" target="_blank" style="color:var(--blue)">' + escHtml(action.title) + '</a>';
693
890
  } else {
694
891
  status.innerHTML = '&#128027; 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
- const CC_MIN_WIDTH = 320;
785
- const CC_MAX_WIDTH_RATIO = 0.8; // 80% of viewport
786
- const CC_DEFAULT_WIDTH = 420;
787
- const CC_WIDTH_KEY = 'cc-drawer-width';
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
- const drawer = document.getElementById('cc-drawer');
987
+ var drawer = document.getElementById('cc-drawer');
791
988
  if (!drawer) return;
792
- const saved = parseInt(localStorage.getItem(CC_WIDTH_KEY), 10);
989
+ var saved = parseInt(localStorage.getItem(CC_WIDTH_KEY), 10);
793
990
  if (saved && saved >= CC_MIN_WIDTH) {
794
- const maxW = Math.floor(window.innerWidth * CC_MAX_WIDTH_RATIO);
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
- const handle = document.getElementById('cc-resize-handle');
801
- const drawer = document.getElementById('cc-drawer');
997
+ var handle = document.getElementById('cc-resize-handle');
998
+ var drawer = document.getElementById('cc-drawer');
802
999
  if (!handle || !drawer) return;
803
1000
 
804
- let startX = 0;
805
- let startW = 0;
1001
+ var startX = 0;
1002
+ var startW = 0;
806
1003
 
807
1004
  function onMouseMove(e) {
808
- const maxW = Math.floor(window.innerWidth * CC_MAX_WIDTH_RATIO);
809
- const delta = startX - e.clientX; // dragging left = wider
810
- const newW = Math.max(CC_MIN_WIDTH, Math.min(startW + delta, maxW));
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 };
@@ -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
- _hideThreadWrap();
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
- _hideThreadWrap();
121
- var actionSlot = document.getElementById('qa-action-slot');
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
- var slot = document.getElementById('qa-action-slot');
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 isVisible = wrap.style.display !== 'none';
352
- wrap.style.display = isVisible ? 'none' : '';
353
- if (expandBar) expandBar.style.display = isVisible ? '' : 'none';
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 ? '&#x25B2; Expand' : '&#x25BC; Collapse';
354
347
  }
355
348
 
356
349
  function _showThreadWrap() {
357
350
  var wrap = document.getElementById('modal-qa-thread-wrap');
358
- var expandBar = document.getElementById('qa-expand-bar');
359
- if (wrap) wrap.style.display = '';
360
- if (expandBar) expandBar.style.display = 'none';
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 = '&#x25BC; 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 };
@@ -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="ccNewSession()" style="background:none;border:1px solid var(--border);color:var(--muted);font-size:10px;padding:2px 8px;border-radius:3px;cursor:pointer">Clear Chat</button>
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">&#10005;</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">
@@ -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 — same as non-streaming path
3322
- if (body.sessionId && body.sessionId !== ccSession.sessionId) {
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.744",
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"