@yemi33/minions 0.1.743 → 0.1.745

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,11 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.743 (2026-04-09)
3
+ ## 0.1.745 (2026-04-09)
4
+
5
+ ### Features
6
+ - CC multi-tab conversations with session resume
7
+
8
+ ## 0.1.744 (2026-04-09)
4
9
 
5
10
  ### Features
6
11
  - add prNumber field to pull-requests.json records (#711)
7
12
 
8
13
  ### Fixes
14
+ - handle max_turns exit in dispatch lifecycle cleanup (closes #716) (#735)
9
15
  - link plan files as artifacts on plan/plan-to-prd work items
10
16
  - escalate failed plan items instead of blocking indefinitely (closes #722) (#733)
11
17
  - handle NUL pseudo-file in Windows worktree cleanup (#731)
@@ -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,157 @@ 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
+ // Reset server session for new tab
117
+ if (!skipServerReset) {
118
+ fetch('/api/command-center/new-session', { method: 'POST' }).catch(function() {});
119
+ }
55
120
  // Abort any in-flight request
56
121
  ccAbort();
57
122
  _ccSending = false;
58
123
  _ccQueue = [];
59
124
  try { localStorage.removeItem('cc-sending'); } catch {}
60
- _ccSessionId = null;
61
- _ccMessages = [];
62
- localStorage.removeItem('cc-session-id');
63
- localStorage.removeItem('cc-messages');
64
125
  document.getElementById('cc-messages').innerHTML = '';
126
+ ccRenderTabBar();
127
+ ccUpdateSessionIndicator();
128
+ ccSaveState();
129
+ var input = document.getElementById('cc-input');
130
+ if (input) input.focus();
131
+ }
132
+
133
+ function ccSwitchTab(id) {
134
+ if (id === _ccActiveTabId) return;
135
+ var tab = _ccTabs.find(function(t) { return t.id === id; });
136
+ if (!tab) return;
137
+ // If there is an active request, keep it running but switch view
138
+ _ccActiveTabId = id;
139
+ var el = document.getElementById('cc-messages');
140
+ el.innerHTML = '';
141
+ // Re-render messages from the tab's data
142
+ for (var i = 0; i < tab.messages.length; i++) {
143
+ ccAddMessage(tab.messages[i].role, tab.messages[i].html, true);
144
+ }
145
+ ccRenderTabBar();
65
146
  ccUpdateSessionIndicator();
147
+ ccSaveState();
148
+ var input = document.getElementById('cc-input');
149
+ if (input) input.focus();
150
+ }
151
+
152
+ function ccCloseTab(id) {
153
+ var idx = _ccTabs.findIndex(function(t) { return t.id === id; });
154
+ if (idx === -1) return;
155
+ // If tab has active request, confirm before closing
156
+ if (_ccSending && _ccActiveTabId === id) {
157
+ if (!confirm('This tab has an active request. Close anyway?')) return;
158
+ ccAbort();
159
+ _ccSending = false;
160
+ _ccQueue = [];
161
+ try { localStorage.removeItem('cc-sending'); } catch {}
162
+ }
163
+ _ccTabs.splice(idx, 1);
164
+ if (_ccActiveTabId === id) {
165
+ // Switch to adjacent tab or create new
166
+ if (_ccTabs.length === 0) {
167
+ ccNewTab(true);
168
+ return;
169
+ }
170
+ var newIdx = Math.min(idx, _ccTabs.length - 1);
171
+ ccSwitchTab(_ccTabs[newIdx].id);
172
+ return;
173
+ }
174
+ ccRenderTabBar();
175
+ ccSaveState();
176
+ }
177
+
178
+ function ccShowAllConversations() {
179
+ var dropdown = document.getElementById('cc-all-conversations');
180
+ if (dropdown) { dropdown.remove(); return; } // toggle off
181
+ dropdown = document.createElement('div');
182
+ dropdown.id = 'cc-all-conversations';
183
+ 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)';
184
+ var html = '';
185
+ for (var i = _ccTabs.length - 1; i >= 0; i--) {
186
+ var t = _ccTabs[i];
187
+ var isActive = t.id === _ccActiveTabId;
188
+ var preview = '';
189
+ if (t.messages.length > 0) {
190
+ var lastMsg = t.messages[t.messages.length - 1];
191
+ var tmp = document.createElement('div');
192
+ tmp.innerHTML = lastMsg.html;
193
+ preview = (tmp.textContent || tmp.innerText || '').slice(0, 60);
194
+ }
195
+ 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()">';
196
+ 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>';
197
+ 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>';
198
+ html += '</div>';
199
+ html += '<span style="font-size:10px;color:var(--muted)">' + t.messages.length + ' msg</span>';
200
+ 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>';
201
+ html += '</div>';
202
+ }
203
+ if (_ccTabs.length === 0) html = '<div style="padding:12px;color:var(--muted);font-size:11px;text-align:center">No conversations yet</div>';
204
+ dropdown.innerHTML = html;
205
+ var tabBar = document.getElementById('cc-tab-bar');
206
+ if (tabBar) { tabBar.style.position = 'relative'; tabBar.appendChild(dropdown); }
207
+ // Close on click outside
208
+ function closeDropdown(e) { if (!dropdown.contains(e.target) && e.target.id !== 'cc-all-btn') { dropdown.remove(); document.removeEventListener('click', closeDropdown); } }
209
+ setTimeout(function() { document.addEventListener('click', closeDropdown); }, 0);
210
+ }
211
+
212
+ function ccRenderTabBar() {
213
+ var bar = document.getElementById('cc-tab-bar');
214
+ if (!bar) return;
215
+ 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>';
216
+ for (var i = 0; i < _ccTabs.length; i++) {
217
+ var t = _ccTabs[i];
218
+ var isActive = t.id === _ccActiveTabId;
219
+ html += '<div class="cc-tab' + (isActive ? ' active' : '') + '" onclick="ccSwitchTab(\'' + t.id + '\')" title="' + escHtml(t.title) + '">';
220
+ html += escHtml(t.title);
221
+ html += '<span class="cc-tab-close" onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\')">&times;</span>';
222
+ html += '</div>';
223
+ }
224
+ 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>';
225
+ bar.innerHTML = html;
66
226
  }
67
227
 
68
228
  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);
229
+ var el = document.getElementById('cc-messages');
230
+ var tab = _ccActiveTab();
231
+ if (!tab) return;
232
+ if (el.children.length > 0 || tab.messages.length === 0) return; // Already rendered or nothing to restore
233
+ for (var i = 0; i < tab.messages.length; i++) {
234
+ ccAddMessage(tab.messages[i].role, tab.messages[i].html, true);
73
235
  }
74
236
  // Restore "thinking" indicator if CC was mid-request when page refreshed
75
237
  try {
76
- const sendingState = JSON.parse(localStorage.getItem('cc-sending') || 'null');
238
+ var sendingState = JSON.parse(localStorage.getItem('cc-sending') || 'null');
77
239
  // Only restore sending state if very recent (< 10s) — page refresh kills the SSE stream
78
240
  if (sendingState?.sending && (Date.now() - sendingState.startedAt) < 10000) {
79
241
  _ccSending = true;
80
- const elapsed = Date.now() - sendingState.startedAt;
81
- const thinking = document.createElement('div');
242
+ var elapsed = Date.now() - sendingState.startedAt;
243
+ var thinking = document.createElement('div');
82
244
  thinking.id = 'cc-thinking';
83
245
  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
246
  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>';
247
+ ' <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
248
  el.appendChild(thinking);
87
249
  el.scrollTop = el.scrollHeight;
88
250
  // Update timer
89
- const startTime = sendingState.startedAt;
90
- const restoreTimer = setInterval(function() {
251
+ var startTime = sendingState.startedAt;
252
+ var restoreTimer = setInterval(function() {
91
253
  var timeEl = document.getElementById('cc-thinking-time');
92
254
  if (!timeEl || !_ccSending) { clearInterval(restoreTimer); return; }
93
255
  timeEl.textContent = Math.floor((Date.now() - startTime) / 1000) + 's';
@@ -96,28 +258,34 @@ function ccRestoreMessages() {
96
258
  } catch {}
97
259
  }
98
260
 
99
- let _ccSaveDebounce = null;
261
+ var _ccSaveDebounce = null;
100
262
  function ccSaveState() {
101
- // Trim in-memory array to prevent unbounded growth
102
- if (_ccMessages.length > 100) _ccMessages = _ccMessages.slice(-100);
263
+ // Trim per-tab messages to prevent unbounded growth
264
+ for (var i = 0; i < _ccTabs.length; i++) {
265
+ if (_ccTabs[i].messages.length > 100) _ccTabs[i].messages = _ccTabs[i].messages.slice(-100);
266
+ }
103
267
  // Debounce localStorage writes — no need to serialize on every message during streaming
104
268
  if (_ccSaveDebounce) return;
105
- _ccSaveDebounce = setTimeout(() => {
269
+ _ccSaveDebounce = setTimeout(function() {
106
270
  _ccSaveDebounce = null;
107
271
  try {
108
- if (_ccSessionId) localStorage.setItem('cc-session-id', _ccSessionId);
109
- const toSave = _ccMessages.slice(-30);
110
- localStorage.setItem('cc-messages', JSON.stringify(toSave));
272
+ // Save tabs with trimmed messages
273
+ var toSave = _ccTabs.map(function(t) {
274
+ return { id: t.id, title: t.title, sessionId: t.sessionId, messages: t.messages.slice(-CC_MAX_MESSAGES_PER_TAB) };
275
+ });
276
+ localStorage.setItem('cc-tabs', JSON.stringify(toSave));
277
+ if (_ccActiveTabId) localStorage.setItem('cc-active-tab', _ccActiveTabId);
111
278
  } catch { /* localStorage might be full */ }
112
279
  }, 500);
113
280
  }
114
281
 
115
282
  function ccUpdateSessionIndicator() {
116
- const el = document.getElementById('cc-session-info');
283
+ var el = document.getElementById('cc-session-info');
117
284
  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' : ''}`;
285
+ var tab = _ccActiveTab();
286
+ if (tab && tab.sessionId) {
287
+ var turns = tab.messages.filter(function(m) { return m.role === 'user'; }).length;
288
+ el.textContent = 'Session: ' + turns + ' turn' + (turns !== 1 ? 's' : '');
121
289
  el.style.color = 'var(--green)';
122
290
  } else {
123
291
  el.textContent = 'Ready';
@@ -126,26 +294,39 @@ function ccUpdateSessionIndicator() {
126
294
  }
127
295
 
128
296
  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;
297
+ var el = document.getElementById('cc-messages');
298
+ var isUser = role === 'user';
299
+ var div = document.createElement('div');
300
+ var isAssistant = !isUser;
133
301
  div.className = isAssistant ? 'cc-msg-assistant' : '';
134
302
  div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
135
303
  (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
304
  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;
305
+ var wasNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
138
306
  el.appendChild(div);
139
- if (wasNearBottom || isUser) requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; });
307
+ if (wasNearBottom || isUser) requestAnimationFrame(function() { el.scrollTop = el.scrollHeight; });
140
308
  if (!skipSave) {
141
- _ccMessages.push({ role, html });
309
+ var tab = _ccActiveTab();
310
+ if (tab) {
311
+ tab.messages.push({ role: role, html: html });
312
+ // Auto-title from first user message
313
+ if (role === 'user' && tab.title === 'New chat') {
314
+ var tmp = document.createElement('div');
315
+ tmp.innerHTML = html;
316
+ var txt = (tmp.textContent || tmp.innerText || '').trim();
317
+ if (txt.length > 0) {
318
+ tab.title = txt.slice(0, CC_TITLE_MAX_LENGTH);
319
+ ccRenderTabBar();
320
+ }
321
+ }
322
+ }
142
323
  ccSaveState();
143
324
  }
144
325
  }
145
326
 
146
327
  async function ccSend() {
147
- const input = document.getElementById('cc-input');
148
- const message = input.value.trim();
328
+ var input = document.getElementById('cc-input');
329
+ var message = input.value.trim();
149
330
  if (!message) return;
150
331
  input.value = '';
151
332
 
@@ -155,17 +336,17 @@ async function ccSend() {
155
336
  _renderQueueIndicator();
156
337
  return;
157
338
  }
158
- let wasAborted = await _ccDoSend(message);
339
+ var wasAborted = await _ccDoSend(message);
159
340
 
160
341
  // Drain queue — send each queued message in order
161
342
  while (_ccQueue.length > 0) {
162
343
  // After abort, pause to let server release ccInFlight guard before next send
163
344
  if (wasAborted) {
164
345
  _ccSending = true; // keep sending flag true so new messages queue instead of bypassing
165
- await new Promise(r => setTimeout(r, 500));
346
+ await new Promise(function(r) { setTimeout(r, 500); });
166
347
  _ccSending = false;
167
348
  }
168
- const next = _ccQueue.shift();
349
+ var next = _ccQueue.shift();
169
350
  _renderQueueIndicator();
170
351
  wasAborted = await _ccDoSend(next);
171
352
  }
@@ -210,7 +391,7 @@ async function _ccDoSend(message, skipUserMsg) {
210
391
 
211
392
  _ccSending = true;
212
393
  _ccAbortController = new AbortController();
213
- let _wasAborted = false;
394
+ var _wasAborted = false;
214
395
  try { localStorage.setItem('cc-sending', JSON.stringify({ sending: true, startedAt: Date.now() })); } catch {}
215
396
 
216
397
  if (!skipUserMsg) ccAddMessage('user', escHtml(message));
@@ -219,8 +400,8 @@ async function _ccDoSend(message, skipUserMsg) {
219
400
  var existingQueueEl = document.getElementById('cc-queue-indicator');
220
401
  if (existingQueueEl) existingQueueEl.remove();
221
402
 
222
- const ccStartTime = Date.now();
223
- const phases = [
403
+ var ccStartTime = Date.now();
404
+ var phases = [
224
405
  [0, 'Thinking...'],
225
406
  [3000, 'Reading minions context...'],
226
407
  [8000, 'Analyzing...'],
@@ -232,14 +413,19 @@ async function _ccDoSend(message, skipUserMsg) {
232
413
  ];
233
414
 
234
415
  // Streaming state — declared before try so updateStreamDiv works during fetch
235
- let streamedText = '';
236
- let toolsUsed = [];
416
+ var streamedText = '';
417
+ var toolsUsed = [];
418
+
419
+ // Get active tab's sessionId to send with request
420
+ var activeTab = _ccActiveTab();
421
+ var tabSessionId = activeTab ? activeTab.sessionId : null;
422
+ var activeTabId = _ccActiveTabId;
237
423
 
238
424
  // Show thinking immediately — before fetch starts
239
425
  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>';
426
+ var msgs = document.getElementById('cc-messages');
427
+ var streamDiv = msgs.lastElementChild;
428
+ 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
429
  function _getThinkingHtml() {
244
430
  var elapsed = Date.now() - ccStartTime;
245
431
  var label = 'Thinking...';
@@ -269,39 +455,40 @@ async function _ccDoSend(message, skipUserMsg) {
269
455
  if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
270
456
  }
271
457
  // Start phase timer immediately so thinking text updates while waiting for SSE
272
- const phaseTimer = setInterval(updateStreamDiv, 1000);
458
+ var phaseTimer = setInterval(updateStreamDiv, 1000);
273
459
  updateStreamDiv(); // render proper layout immediately (not raw "Thinking..." text)
274
460
 
275
461
  try {
276
462
  // Stream response via SSE — shows text as it arrives
277
- const res = await fetch('/api/command-center/stream', {
463
+ var res = await fetch('/api/command-center/stream', {
278
464
  method: 'POST', headers: { 'Content-Type': 'application/json' },
279
- body: JSON.stringify({ message }),
465
+ body: JSON.stringify({ message: message, tabId: activeTabId }),
280
466
  signal: _ccAbortController ? _ccAbortController.signal : AbortSignal.timeout(960000)
281
467
  });
282
468
 
283
469
  if (!res.ok) {
284
470
  clearInterval(phaseTimer); streamDiv.remove();
285
- const errText = await res.text();
471
+ var errText = await res.text();
286
472
  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>' : ''));
473
+ (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
474
  return;
289
475
  }
290
476
 
291
- const reader = res.body.getReader();
292
- const decoder = new TextDecoder();
293
- let buf = '';
477
+ var reader = res.body.getReader();
478
+ var decoder = new TextDecoder();
479
+ var buf = '';
294
480
 
295
481
  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');
482
+ var readResult = await reader.read();
483
+ if (readResult.done) break;
484
+ buf += decoder.decode(readResult.value, { stream: true });
485
+ var lines = buf.split('\n');
300
486
  buf = lines.pop();
301
- for (const line of lines) {
487
+ for (var li = 0; li < lines.length; li++) {
488
+ var line = lines[li];
302
489
  if (!line.startsWith('data: ')) continue;
303
490
  try {
304
- const evt = JSON.parse(line.slice(6));
491
+ var evt = JSON.parse(line.slice(6));
305
492
  if (evt.type === 'chunk') {
306
493
  streamedText = evt.text;
307
494
  updateStreamDiv();
@@ -313,12 +500,16 @@ async function _ccDoSend(message, skipUserMsg) {
313
500
  // Replace streaming div with a proper ccAddMessage
314
501
  clearInterval(phaseTimer); streamDiv.remove();
315
502
  // 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 || '');
503
+ var ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
504
+ var rendered = renderMd(evt.text || streamedText || '');
318
505
  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(); }
506
+ if (evt.sessionId) {
507
+ var currentTab = _ccActiveTab();
508
+ if (currentTab) { currentTab.sessionId = evt.sessionId; }
509
+ ccSaveState(); ccUpdateSessionIndicator();
510
+ }
320
511
  if (evt.actions && evt.actions.length > 0) {
321
- for (const action of evt.actions) { await ccExecuteAction(action); }
512
+ for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai]); }
322
513
  }
323
514
  } else if (evt.type === 'error') {
324
515
  clearInterval(phaseTimer); streamDiv.remove();
@@ -330,22 +521,28 @@ async function _ccDoSend(message, skipUserMsg) {
330
521
  }
331
522
  // Process any remaining buffered data after stream ends
332
523
  if (buf.trim()) {
333
- for (const line of buf.split('\n')) {
334
- if (!line.startsWith('data: ')) continue;
524
+ var remainingLines = buf.split('\n');
525
+ for (var ri = 0; ri < remainingLines.length; ri++) {
526
+ var rline = remainingLines[ri];
527
+ if (!rline.startsWith('data: ')) continue;
335
528
  try {
336
- const evt = JSON.parse(line.slice(6));
337
- if (evt.type === 'done') {
529
+ var revt = JSON.parse(rline.slice(6));
530
+ if (revt.type === 'done') {
338
531
  clearInterval(phaseTimer); streamDiv.remove();
339
532
  // 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); }
533
+ var ccElapsed2 = Math.round((Date.now() - ccStartTime) / 1000);
534
+ var rendered2 = renderMd(revt.text || streamedText || '');
535
+ 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>');
536
+ if (revt.sessionId) {
537
+ var currentTab2 = _ccActiveTab();
538
+ if (currentTab2) { currentTab2.sessionId = revt.sessionId; }
539
+ ccSaveState(); ccUpdateSessionIndicator();
346
540
  }
347
- } else if (evt.type === 'chunk') {
348
- streamedText = evt.text;
541
+ if (revt.actions && revt.actions.length > 0) {
542
+ for (var ai2 = 0; ai2 < revt.actions.length; ai2++) { await ccExecuteAction(revt.actions[ai2]); }
543
+ }
544
+ } else if (revt.type === 'chunk') {
545
+ streamedText = revt.text;
349
546
  updateStreamDiv();
350
547
  }
351
548
  } catch {}
@@ -355,8 +552,8 @@ async function _ccDoSend(message, skipUserMsg) {
355
552
  if (streamDiv.parentNode) {
356
553
  clearInterval(phaseTimer); streamDiv.remove();
357
554
  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>');
555
+ var ccElapsed3 = Math.round((Date.now() - ccStartTime) / 1000);
556
+ 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
557
  }
361
558
  }
362
559
  } catch (e) {
@@ -364,19 +561,19 @@ async function _ccDoSend(message, skipUserMsg) {
364
561
  if (e.name === 'AbortError') {
365
562
  _wasAborted = true;
366
563
  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>');
564
+ var ccElapsed4 = Math.round((Date.now() - ccStartTime) / 1000);
565
+ 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
566
  } else {
370
567
  ccAddMessage('assistant', '<span style="color:var(--red);font-size:11px">Stopped</span>');
371
568
  }
372
569
  } else {
373
- const retryId = 'cc-retry-' + Date.now();
374
- const isNetworkError = e.message === 'Failed to fetch' || e.message.includes('NetworkError');
570
+ var retryId = 'cc-retry-' + Date.now();
571
+ var isNetworkError = e.message === 'Failed to fetch' || e.message.includes('NetworkError');
375
572
  ccAddMessage('assistant', '<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>' +
376
573
  (isNetworkError ? '<div style="font-size:10px;color:var(--muted);margin-top:4px">Dashboard connection lost. Reload the page to reconnect.</div>' : '') +
377
574
  '<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
575
  (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>');
576
+ ' <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
577
  }
381
578
  } finally {
382
579
  _ccSending = false;
@@ -391,21 +588,23 @@ async function _ccDoSend(message, skipUserMsg) {
391
588
 
392
589
  function ccRetryLast() {
393
590
  // Find the last user message and resend it
394
- const last = _ccMessages.filter(m => m.role === 'user').pop();
591
+ var tab = _ccActiveTab();
592
+ if (!tab) return;
593
+ var last = tab.messages.filter(function(m) { return m.role === 'user'; }).pop();
395
594
  if (!last) return;
396
595
  // Extract text from the HTML (strip tags)
397
- const tmp = document.createElement('div');
596
+ var tmp = document.createElement('div');
398
597
  tmp.innerHTML = last.html;
399
- const text = tmp.textContent || tmp.innerText || '';
598
+ var text = tmp.textContent || tmp.innerText || '';
400
599
  if (!text.trim()) return;
401
600
  // Remove the error message (last assistant message)
402
- const el = document.getElementById('cc-messages');
601
+ var el = document.getElementById('cc-messages');
403
602
  if (el?.lastElementChild) el.lastElementChild.remove();
404
- _ccMessages = _ccMessages.slice(0, -1); // remove error from history
603
+ tab.messages = tab.messages.slice(0, -1); // remove error from history
405
604
  // Resend, then drain queue
406
- _ccDoSend(text.trim()).then(async () => {
605
+ _ccDoSend(text.trim()).then(async function() {
407
606
  while (_ccQueue.length > 0) {
408
- const next = _ccQueue.shift();
607
+ var next = _ccQueue.shift();
409
608
  _renderQueueIndicator();
410
609
  await _ccDoSend(next);
411
610
  }
@@ -413,25 +612,25 @@ function ccRetryLast() {
413
612
  }
414
613
 
415
614
  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 + ')'); }
615
+ var res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
616
+ if (!res.ok) { var d = await res.json().catch(function() { return {}; }); throw new Error(d.error || 'Request failed (' + res.status + ')'); }
418
617
  return res;
419
618
  }
420
619
 
421
620
  async function ccExecuteAction(action) {
422
- const msgs = document.getElementById('cc-messages');
423
- const status = document.createElement('div');
621
+ var msgs = document.getElementById('cc-messages');
622
+ var status = document.createElement('div');
424
623
  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
624
 
426
625
  try {
427
626
  switch (action.type) {
428
627
  case 'dispatch': {
429
- const res = await _ccFetch('/api/work-items', {
628
+ var res = await _ccFetch('/api/work-items', {
430
629
  title: action.title, type: action.workType || 'implement',
431
630
  priority: action.priority || 'medium', description: action.description || '',
432
631
  project: action.project || '', agents: action.agents || [],
433
632
  });
434
- const d = await res.json();
633
+ var d = await res.json();
435
634
  status.innerHTML = '&#10003; Dispatched: <strong>' + escHtml(d.id || action.title) + '</strong>';
436
635
  status.style.color = 'var(--green)';
437
636
  wakeEngine();
@@ -465,8 +664,8 @@ async function ccExecuteAction(action) {
465
664
  break;
466
665
  }
467
666
  case 'retry': {
468
- for (const id of (action.ids || [])) {
469
- await _ccFetch('/api/work-items/retry', { id, source: '' });
667
+ for (var ri = 0; ri < (action.ids || []).length; ri++) {
668
+ await _ccFetch('/api/work-items/retry', { id: action.ids[ri], source: '' });
470
669
  }
471
670
  status.innerHTML = '&#10003; Retried: <strong>' + escHtml((action.ids || []).join(', ')) + '</strong>';
472
671
  status.style.color = 'var(--green)';
@@ -505,9 +704,9 @@ async function ccExecuteAction(action) {
505
704
  }
506
705
  case 'plan-edit': {
507
706
  // 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', {
707
+ var normalizedFile = normalizePlanFile(action.file);
708
+ var planContent = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(function(r) { return r.text(); });
709
+ var res2 = await fetch('/api/doc-chat', {
511
710
  method: 'POST', headers: { 'Content-Type': 'application/json' },
512
711
  body: JSON.stringify({
513
712
  message: action.instruction,
@@ -516,7 +715,7 @@ async function ccExecuteAction(action) {
516
715
  filePath: 'plans/' + normalizedFile,
517
716
  }),
518
717
  });
519
- const data = await res.json();
718
+ var data = await res2.json();
520
719
  if (data.ok && data.edited) {
521
720
  status.innerHTML = '&#10003; Plan edited: <strong>' + escHtml(action.file) + '</strong>';
522
721
  status.style.color = 'var(--green)';
@@ -539,7 +738,7 @@ async function ccExecuteAction(action) {
539
738
  }
540
739
  case 'file-edit': {
541
740
  // doc-chat reads current content from disk via filePath — pass placeholder for required field
542
- const res = await fetch('/api/doc-chat', {
741
+ var res3 = await fetch('/api/doc-chat', {
543
742
  method: 'POST', headers: { 'Content-Type': 'application/json' },
544
743
  body: JSON.stringify({
545
744
  message: action.instruction,
@@ -548,19 +747,19 @@ async function ccExecuteAction(action) {
548
747
  filePath: action.file,
549
748
  }),
550
749
  });
551
- const data = await res.json();
552
- if (data.ok && data.edited) {
750
+ var data3 = await res3.json();
751
+ if (data3.ok && data3.edited) {
553
752
  status.innerHTML = '&#10003; Edited: <strong>' + escHtml(action.file) + '</strong>';
554
753
  status.style.color = 'var(--green)';
555
754
  } else {
556
- status.innerHTML = data.answer ? renderMd(data.answer) : '&#10007; Could not edit file';
557
- status.style.color = data.answer ? 'var(--muted)' : 'var(--red)';
755
+ status.innerHTML = data3.answer ? renderMd(data3.answer) : '&#10007; Could not edit file';
756
+ status.style.color = data3.answer ? 'var(--muted)' : 'var(--red)';
558
757
  }
559
758
  break;
560
759
  }
561
760
  case 'schedule': {
562
- const url = action._update ? '/api/schedules/update' : '/api/schedules';
563
- const res = await fetch(url, {
761
+ var url = action._update ? '/api/schedules/update' : '/api/schedules';
762
+ var res4 = await fetch(url, {
564
763
  method: 'POST', headers: { 'Content-Type': 'application/json' },
565
764
  body: JSON.stringify({
566
765
  id: action.id, title: action.title, cron: action.cron,
@@ -570,54 +769,55 @@ async function ccExecuteAction(action) {
570
769
  enabled: action.enabled !== false,
571
770
  })
572
771
  });
573
- if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Schedule create failed'); }
772
+ if (!res4.ok) { var d4 = await res4.json().catch(function() { return {}; }); throw new Error(d4.error || 'Schedule create failed'); }
574
773
  status.innerHTML = '&#10003; Schedule ' + (action._update ? 'updated' : 'created') + ': <strong>' + escHtml(action.id) + '</strong>';
575
774
  status.style.color = 'var(--green)';
576
775
  break;
577
776
  }
578
777
  case 'delete-schedule': {
579
- const res = await fetch('/api/schedules/delete', {
778
+ var res5 = await fetch('/api/schedules/delete', {
580
779
  method: 'POST', headers: { 'Content-Type': 'application/json' },
581
780
  body: JSON.stringify({ id: action.id })
582
781
  });
583
- if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Schedule delete failed'); }
782
+ if (!res5.ok) { var d5 = await res5.json().catch(function() { return {}; }); throw new Error(d5.error || 'Schedule delete failed'); }
584
783
  status.innerHTML = '&#10003; Deleted schedule: <strong>' + escHtml(action.id) + '</strong>';
585
784
  status.style.color = 'var(--orange)';
586
785
  break;
587
786
  }
588
787
  case 'create-meeting': {
589
- const res = await fetch('/api/meetings', {
788
+ var res6 = await fetch('/api/meetings', {
590
789
  method: 'POST', headers: { 'Content-Type': 'application/json' },
591
790
  body: JSON.stringify({ title: action.title, agenda: action.agenda, participants: action.agents, rounds: action.rounds, project: action.project })
592
791
  });
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) + ')' : '');
792
+ if (!res6.ok) { var d6 = await res6.json().catch(function() { return {}; }); throw new Error(d6.error || 'Meeting create failed'); }
793
+ var d6r = await res6.json();
794
+ status.innerHTML = '&#10003; Meeting started: <strong>' + escHtml(action.title) + '</strong>' + (d6r.id ? ' (' + escHtml(d6r.id) + ')' : '');
596
795
  status.style.color = 'var(--green)';
597
796
  wakeEngine();
598
797
  break;
599
798
  }
600
799
  case 'set-config': {
601
- const payload = { engine: { [action.setting]: action.value } };
602
- const res = await fetch('/api/settings', {
800
+ var payload = { engine: {} };
801
+ payload.engine[action.setting] = action.value;
802
+ var res7 = await fetch('/api/settings', {
603
803
  method: 'POST', headers: { 'Content-Type': 'application/json' },
604
804
  body: JSON.stringify(payload)
605
805
  });
606
- if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Config update failed'); }
806
+ if (!res7.ok) { var d7 = await res7.json().catch(function() { return {}; }); throw new Error(d7.error || 'Config update failed'); }
607
807
  status.innerHTML = '&#10003; Set <strong>' + escHtml(action.setting) + '</strong> = ' + escHtml(String(action.value));
608
808
  status.style.color = 'var(--green)';
609
809
  break;
610
810
  }
611
811
  case 'edit-pipeline': {
612
- const body = { id: action.id };
812
+ var body = { id: action.id };
613
813
  if (action.title) body.title = action.title;
614
814
  if (action.stages) body.stages = action.stages;
615
815
  if (action.trigger !== undefined) body.trigger = action.trigger;
616
- const res = await fetch('/api/pipelines/update', {
816
+ var res8 = await fetch('/api/pipelines/update', {
617
817
  method: 'POST', headers: { 'Content-Type': 'application/json' },
618
818
  body: JSON.stringify(body)
619
819
  });
620
- if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Pipeline update failed'); }
820
+ if (!res8.ok) { var d8 = await res8.json().catch(function() { return {}; }); throw new Error(d8.error || 'Pipeline update failed'); }
621
821
  status.innerHTML = '&#10003; Updated pipeline: <strong>' + escHtml(action.id) + '</strong>';
622
822
  status.style.color = 'var(--green)';
623
823
  break;
@@ -655,11 +855,11 @@ async function ccExecuteAction(action) {
655
855
  break;
656
856
  }
657
857
  case 'trigger-pipeline': {
658
- var res = await fetch('/api/pipelines/trigger', {
858
+ var res9 = await fetch('/api/pipelines/trigger', {
659
859
  method: 'POST', headers: { 'Content-Type': 'application/json' },
660
860
  body: JSON.stringify({ id: action.id })
661
861
  });
662
- if (!res.ok) { var d = await res.json().catch(function() { return {}; }); throw new Error(d.error || 'Pipeline trigger failed'); }
862
+ if (!res9.ok) { var d9 = await res9.json().catch(function() { return {}; }); throw new Error(d9.error || 'Pipeline trigger failed'); }
663
863
  status.innerHTML = '&#10003; Pipeline triggered: <strong>' + escHtml(action.id) + '</strong>';
664
864
  status.style.color = 'var(--green)';
665
865
  wakeEngine();
@@ -686,10 +886,10 @@ async function ccExecuteAction(action) {
686
886
  break;
687
887
  }
688
888
  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>';
889
+ var res10 = await _ccFetch('/api/issues/create', { title: action.title, description: action.description, labels: action.labels });
890
+ var d10 = await res10.json();
891
+ if (d10.url) {
892
+ status.innerHTML = '&#128027; Bug filed: <a href="' + escHtml(d10.url) + '" target="_blank" style="color:var(--blue)">' + escHtml(action.title) + '</a>';
693
893
  } else {
694
894
  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
895
  }
@@ -781,33 +981,33 @@ async function ccExecuteAction(action) {
781
981
  }
782
982
 
783
983
  // --- 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';
984
+ var CC_MIN_WIDTH = 320;
985
+ var CC_MAX_WIDTH_RATIO = 0.8; // 80% of viewport
986
+ var CC_DEFAULT_WIDTH = 420;
987
+ var CC_WIDTH_KEY = 'cc-drawer-width';
788
988
 
789
989
  function ccApplySavedWidth() {
790
- const drawer = document.getElementById('cc-drawer');
990
+ var drawer = document.getElementById('cc-drawer');
791
991
  if (!drawer) return;
792
- const saved = parseInt(localStorage.getItem(CC_WIDTH_KEY), 10);
992
+ var saved = parseInt(localStorage.getItem(CC_WIDTH_KEY), 10);
793
993
  if (saved && saved >= CC_MIN_WIDTH) {
794
- const maxW = Math.floor(window.innerWidth * CC_MAX_WIDTH_RATIO);
994
+ var maxW = Math.floor(window.innerWidth * CC_MAX_WIDTH_RATIO);
795
995
  drawer.style.width = Math.min(saved, maxW) + 'px';
796
996
  }
797
997
  }
798
998
 
799
999
  function ccInitResize() {
800
- const handle = document.getElementById('cc-resize-handle');
801
- const drawer = document.getElementById('cc-drawer');
1000
+ var handle = document.getElementById('cc-resize-handle');
1001
+ var drawer = document.getElementById('cc-drawer');
802
1002
  if (!handle || !drawer) return;
803
1003
 
804
- let startX = 0;
805
- let startW = 0;
1004
+ var startX = 0;
1005
+ var startW = 0;
806
1006
 
807
1007
  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));
1008
+ var maxW = Math.floor(window.innerWidth * CC_MAX_WIDTH_RATIO);
1009
+ var delta = startX - e.clientX; // dragging left = wider
1010
+ var newW = Math.max(CC_MIN_WIDTH, Math.min(startW + delta, maxW));
811
1011
  drawer.style.width = newW + 'px';
812
1012
  }
813
1013
 
@@ -820,7 +1020,7 @@ function ccInitResize() {
820
1020
  try { localStorage.setItem(CC_WIDTH_KEY, parseInt(drawer.style.width, 10)); } catch {}
821
1021
  }
822
1022
 
823
- handle.addEventListener('mousedown', (e) => {
1023
+ handle.addEventListener('mousedown', function(e) {
824
1024
  e.preventDefault();
825
1025
  startX = e.clientX;
826
1026
  startW = drawer.offsetWidth;
@@ -838,4 +1038,4 @@ if (document.readyState === 'loading') {
838
1038
  ccInitResize();
839
1039
  }
840
1040
 
841
- window.MinionsCC = { toggleCommandCenter, ccNewSession, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction };
1041
+ 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 {
@@ -3374,6 +3389,25 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3374
3389
  safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
3375
3390
  }
3376
3391
 
3392
+ // Persist tab→session mapping if tabId provided
3393
+ const tabId = body.tabId;
3394
+ if (tabId && ccSession.sessionId) {
3395
+ try {
3396
+ const sessions = shared.safeJsonArr(CC_SESSIONS_PATH);
3397
+ const existing = sessions.find(s => s.id === tabId);
3398
+ const preview = (body.message || '').slice(0, 80);
3399
+ if (existing) {
3400
+ existing.sessionId = ccSession.sessionId;
3401
+ existing.lastActiveAt = new Date(now).toISOString();
3402
+ existing.turnCount = ccSession.turnCount;
3403
+ existing.preview = preview;
3404
+ } else {
3405
+ 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 });
3406
+ }
3407
+ safeWrite(CC_SESSIONS_PATH, sessions);
3408
+ } catch { /* non-critical */ }
3409
+ }
3410
+
3377
3411
  // Send final result with actions
3378
3412
  const { text: displayText, actions } = parseCCActions(result.text);
3379
3413
  res.write('data: ' + JSON.stringify({ type: 'done', text: displayText, actions, sessionId: ccSession.sessionId, newSession: !wasResume }) + '\n\n');
@@ -4060,7 +4094,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4060
4094
  // Command Center
4061
4095
  { method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
4062
4096
  { 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 },
4097
+ { method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
4098
+ { method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
4099
+ { method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)$/, desc: 'Delete a CC session by tab ID', handler: handleCCSessionDelete },
4064
4100
 
4065
4101
  // Schedules
4066
4102
  { method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
@@ -1560,7 +1560,7 @@ function classifyFailure(code, stdout = '', stderr = '') {
1560
1560
  }
1561
1561
 
1562
1562
  // Context window / max turns exhausted
1563
- if (/context window|max.*turns.*reached|token limit|conversation.*too long|context.*length.*exceeded/i.test(combined)) {
1563
+ if (/context window|max.*turns.*reached|error_max_turns|terminal_reason.*max_turns|token limit|conversation.*too long|context.*length.*exceeded/i.test(combined)) {
1564
1564
  return FAILURE_CLASS.OUT_OF_CONTEXT;
1565
1565
  }
1566
1566
 
@@ -176,6 +176,10 @@ proc.stdout.once('data', () => { gotFirstOutput = true; clearTimeout(startupTime
176
176
 
177
177
  proc.on('close', (code) => {
178
178
  clearTimeout(startupTimer);
179
+ // Write process-exit sentinel to stdout so the engine can detect completion (#716).
180
+ // This is a backup for cases where Claude CLI crashes without writing a result line.
181
+ // process.stdout.write is synchronous for pipes, so it will be captured by the parent.
182
+ try { process.stdout.write(`\n[process-exit] code=${code}\n`); } catch { /* stdout may be closed */ }
179
183
  fs.appendFileSync(debugPath, `EXIT: code=${code}\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
180
184
  process.exit(code || 0);
181
185
  });
package/engine/timeout.js CHANGED
@@ -177,27 +177,40 @@ function checkTimeouts(config) {
177
177
  const silentMs = Date.now() - lastActivity;
178
178
  const silentSec = Math.round(silentMs / 1000);
179
179
 
180
- // Check if the agent actually completed (result event in live output)
181
- // Optimization: only read file if recent activity (avoids reading stale 1MB logs)
180
+ // Check if the agent actually completed (result event in live output).
181
+ // Read the tail of the log (last 64KB) for efficiency — result JSON is always near the end.
182
+ // No time cap: a stuck dispatch that produced a result must always be detected (#716).
182
183
  let completedViaOutput = false;
183
- if (silentMs <= 600000) try {
184
- const liveLog = safeRead(liveLogPath);
185
- if (liveLog && liveLog.includes('"type":"result"')) {
184
+ try {
185
+ let liveLog;
186
+ try {
187
+ const fd = fs.openSync(liveLogPath, 'r');
188
+ const stat = fs.fstatSync(fd);
189
+ const TAIL_SIZE = 65536; // 64KB
190
+ const tailSize = Math.min(stat.size, TAIL_SIZE);
191
+ const buf = Buffer.alloc(tailSize);
192
+ fs.readSync(fd, buf, 0, tailSize, Math.max(0, stat.size - tailSize));
193
+ fs.closeSync(fd);
194
+ liveLog = buf.toString('utf8');
195
+ } catch { /* ENOENT or read failure — liveLog stays undefined */ }
196
+ if (liveLog && (liveLog.includes('"type":"result"') || liveLog.includes('[process-exit]'))) {
186
197
  completedViaOutput = true;
187
198
  const isSuccess = liveLog.includes('"subtype":"success"');
188
199
  log('info', `Agent ${item.agent} (${item.id}) completed via output detection (${isSuccess ? 'success' : 'error'})`);
189
200
 
190
- // Extract output text for the output.log
201
+ // Extract output text for the output.log — read full file for complete parsing
191
202
  const outputLogPath = path.join(AGENTS_DIR, item.agent, 'output.log');
192
203
  try {
193
- const { text } = shared.parseStreamJsonOutput(liveLog);
204
+ const fullLog = safeRead(liveLogPath) || liveLog;
205
+ const { text } = shared.parseStreamJsonOutput(fullLog);
194
206
  safeWrite(outputLogPath, `# Output for dispatch ${item.id}\n# Exit code: ${isSuccess ? 0 : 1}\n# Completed: ${ts()}\n# Detected via output scan\n\n## Result\n${text || '(no text)'}\n`);
195
207
  } catch (e) { log('warn', 'parse output result: ' + e.message); }
196
208
 
197
209
  completeDispatch(item.id, isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR, 'Completed (detected from output)');
198
210
 
199
211
  // Run post-completion hooks via shared helper (async — fire and forget in timeout context)
200
- runPostCompletionHooks(item, item.agent, isSuccess ? 0 : 1, liveLog, config).catch(e => log('warn', 'post-completion hooks: ' + e.message));
212
+ const fullLogForHooks = safeRead(liveLogPath) || liveLog;
213
+ runPostCompletionHooks(item, item.agent, isSuccess ? 0 : 1, fullLogForHooks, config).catch(e => log('warn', 'post-completion hooks: ' + e.message));
201
214
 
202
215
  if (hasProcess) {
203
216
  shared.killImmediate(activeProcesses.get(item.id)?.proc);
@@ -213,6 +226,8 @@ function checkTimeouts(config) {
213
226
  // Check if agent is in a blocking tool call (TaskOutput block:true, Bash with long timeout, etc.)
214
227
  // These tools produce no stdout for extended periods — don't kill them prematurely
215
228
  // Check for BOTH tracked and untracked processes (orphan case after engine restart)
229
+ // Skip if agent already completed — blocking tool detection on stale tool calls
230
+ // would extend the timeout indefinitely for dead agents (#716).
216
231
  let isBlocking = false;
217
232
  let blockingTimeout = itemHeartbeat;
218
233
  let blockingTool = '';
@@ -220,6 +235,12 @@ function checkTimeouts(config) {
220
235
  try {
221
236
  const liveLog = safeRead(liveLogPath);
222
237
  if (liveLog) {
238
+ // If the output contains a result event or process-exit sentinel, the agent is done.
239
+ // Don't extend timeout for stale blocking tool calls from before the result (#716).
240
+ if (liveLog.includes('"type":"result"') || liveLog.includes('[process-exit]')) {
241
+ // Agent completed but close event didn't fire — let orphan/hung detection handle it.
242
+ // Don't set isBlocking — use base heartbeat timeout.
243
+ } else {
223
244
  // Find the last tool_use call in the output — check if it's a known blocking tool
224
245
  const lines = liveLog.split('\n');
225
246
  for (let i = lines.length - 1; i >= Math.max(0, lines.length - 30); i--) {
@@ -266,7 +287,8 @@ function checkTimeouts(config) {
266
287
  remainingMs: Math.max(0, blockingTimeout - silentMs),
267
288
  });
268
289
  }
269
- }
290
+ } // close else
291
+ } // close if (liveLog)
270
292
  } catch (e) { log('warn', 'blocking tool detection: ' + e.message); }
271
293
  }
272
294
  // Agent recovered from blocking state — clear annotation
package/engine.js CHANGED
@@ -723,7 +723,7 @@ async function spawnAgent(dispatchItem, config) {
723
723
  }
724
724
 
725
725
  // Re-attach to existing tracking
726
- activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId, _steeringAt: Date.now() });
726
+ activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId, _steeringAt: Date.now(), lastRealOutputAt: Date.now() });
727
727
 
728
728
  // Reset output buffers so post-completion parsing only sees the resumed session
729
729
  stdout = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.743",
3
+ "version": "0.1.745",
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"