@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 +7 -1
- package/dashboard/js/command-center.js +352 -152
- package/dashboard/js/modal-qa.js +15 -27
- package/dashboard/layout.html +2 -1
- package/dashboard/styles.css +6 -0
- package/dashboard.js +37 -1
- package/engine/lifecycle.js +1 -1
- package/engine/spawn-agent.js +4 -0
- package/engine/timeout.js +31 -9
- package/engine.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
// ── Multi-tab state ──────────────────────────────────────────────────────────
|
|
4
|
+
var CC_MAX_TABS = 20;
|
|
5
|
+
var CC_MAX_MESSAGES_PER_TAB = 30;
|
|
6
|
+
var CC_TITLE_MAX_LENGTH = 40;
|
|
7
|
+
|
|
8
|
+
var _ccTabs = []; // [{id, title, sessionId, messages: [{role, html}]}]
|
|
9
|
+
var _ccActiveTabId = null;
|
|
10
|
+
var _ccOpen = false;
|
|
11
|
+
var _ccSending = false;
|
|
12
|
+
var _ccQueue = [];
|
|
13
|
+
var _ccAbortController = null;
|
|
9
14
|
// Clear stale sending state on page load — SSE streams don't survive refresh
|
|
10
15
|
try { localStorage.removeItem('cc-sending'); } catch {}
|
|
11
16
|
|
|
17
|
+
// ── Migration from legacy single-session format ─────────────────────────────
|
|
18
|
+
(function _ccMigrateLegacy() {
|
|
19
|
+
try {
|
|
20
|
+
var legacyTabs = localStorage.getItem('cc-tabs');
|
|
21
|
+
if (legacyTabs) {
|
|
22
|
+
// Already migrated — load tabs
|
|
23
|
+
_ccTabs = JSON.parse(legacyTabs) || [];
|
|
24
|
+
_ccActiveTabId = localStorage.getItem('cc-active-tab') || (_ccTabs.length > 0 ? _ccTabs[0].id : null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
var legacySessionId = localStorage.getItem('cc-session-id');
|
|
28
|
+
var legacyMessages = JSON.parse(localStorage.getItem('cc-messages') || '[]');
|
|
29
|
+
if (legacySessionId || legacyMessages.length > 0) {
|
|
30
|
+
var tabId = 'cc-' + Date.now().toString(36);
|
|
31
|
+
var title = 'New chat';
|
|
32
|
+
if (legacyMessages.length > 0) {
|
|
33
|
+
var firstUser = legacyMessages.find(function(m) { return m.role === 'user'; });
|
|
34
|
+
if (firstUser) {
|
|
35
|
+
var tmp = document.createElement('div');
|
|
36
|
+
tmp.innerHTML = firstUser.html;
|
|
37
|
+
var txt = (tmp.textContent || tmp.innerText || '').trim();
|
|
38
|
+
if (txt.length > 0) title = txt.slice(0, CC_TITLE_MAX_LENGTH);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
_ccTabs = [{ id: tabId, title: title, sessionId: legacySessionId, messages: legacyMessages.slice(-CC_MAX_MESSAGES_PER_TAB) }];
|
|
42
|
+
_ccActiveTabId = tabId;
|
|
43
|
+
// Remove legacy keys
|
|
44
|
+
localStorage.removeItem('cc-session-id');
|
|
45
|
+
localStorage.removeItem('cc-messages');
|
|
46
|
+
ccSaveState();
|
|
47
|
+
}
|
|
48
|
+
} catch { /* ignore migration errors */ }
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
// ── Tab helper: get active tab ──────────────────────────────────────────────
|
|
52
|
+
function _ccActiveTab() {
|
|
53
|
+
if (!_ccActiveTabId || _ccTabs.length === 0) return null;
|
|
54
|
+
return _ccTabs.find(function(t) { return t.id === _ccActiveTabId; }) || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
12
57
|
function _ccFindPinTarget(query) {
|
|
13
58
|
for (var i = 0; i < (inboxData || []).length; i++) {
|
|
14
59
|
if (inboxData[i].name.toLowerCase().includes(query)) {
|
|
@@ -35,13 +80,16 @@ function ccAbort() {
|
|
|
35
80
|
|
|
36
81
|
function toggleCommandCenter() {
|
|
37
82
|
_ccOpen = !_ccOpen;
|
|
38
|
-
|
|
39
|
-
|
|
83
|
+
var drawer = document.getElementById('cc-drawer');
|
|
84
|
+
var overlay = document.getElementById('cc-overlay');
|
|
40
85
|
if (_ccOpen) ccApplySavedWidth();
|
|
41
86
|
drawer.style.display = _ccOpen ? 'flex' : 'none';
|
|
42
87
|
if (overlay) overlay.style.display = _ccOpen ? 'block' : 'none';
|
|
43
88
|
if (_ccOpen) {
|
|
89
|
+
// Ensure at least one tab exists
|
|
90
|
+
if (_ccTabs.length === 0) ccNewTab(true);
|
|
44
91
|
clearNotifBadge(document.getElementById('cc-toggle-btn'));
|
|
92
|
+
ccRenderTabBar();
|
|
45
93
|
document.getElementById('cc-input').focus();
|
|
46
94
|
ccRestoreMessages();
|
|
47
95
|
} else if (_ccSending) {
|
|
@@ -51,43 +99,157 @@ function toggleCommandCenter() {
|
|
|
51
99
|
|
|
52
100
|
|
|
53
101
|
function ccNewSession() {
|
|
54
|
-
|
|
102
|
+
// Legacy wrapper — just creates a new tab
|
|
103
|
+
ccNewTab();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ccNewTab(skipServerReset) {
|
|
107
|
+
if (_ccTabs.length >= CC_MAX_TABS) {
|
|
108
|
+
// Remove oldest tab to make room
|
|
109
|
+
var oldest = _ccTabs.shift();
|
|
110
|
+
if (oldest && oldest.id === _ccActiveTabId) _ccActiveTabId = null;
|
|
111
|
+
}
|
|
112
|
+
var tabId = 'cc-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
113
|
+
var tab = { id: tabId, title: 'New chat', sessionId: null, messages: [] };
|
|
114
|
+
_ccTabs.push(tab);
|
|
115
|
+
_ccActiveTabId = tabId;
|
|
116
|
+
// 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">×</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 + '\')">×</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">▼</button>';
|
|
225
|
+
bar.innerHTML = html;
|
|
66
226
|
}
|
|
67
227
|
|
|
68
228
|
function ccRestoreMessages() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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="
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
261
|
+
var _ccSaveDebounce = null;
|
|
100
262
|
function ccSaveState() {
|
|
101
|
-
// Trim
|
|
102
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
283
|
+
var el = document.getElementById('cc-session-info');
|
|
117
284
|
if (!el) return;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
305
|
+
var wasNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
|
|
138
306
|
el.appendChild(div);
|
|
139
|
-
if (wasNearBottom || isUser) requestAnimationFrame(()
|
|
307
|
+
if (wasNearBottom || isUser) requestAnimationFrame(function() { el.scrollTop = el.scrollHeight; });
|
|
140
308
|
if (!skipSave) {
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
|
346
|
+
await new Promise(function(r) { setTimeout(r, 500); });
|
|
166
347
|
_ccSending = false;
|
|
167
348
|
}
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
477
|
+
var reader = res.body.getReader();
|
|
478
|
+
var decoder = new TextDecoder();
|
|
479
|
+
var buf = '';
|
|
294
480
|
|
|
295
481
|
while (true) {
|
|
296
|
-
|
|
297
|
-
if (done) break;
|
|
298
|
-
buf += decoder.decode(value, { stream: true });
|
|
299
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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) {
|
|
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 (
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
if (
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
ccAddMessage('assistant',
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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">' +
|
|
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
|
-
|
|
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 ' +
|
|
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
|
-
|
|
374
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
596
|
+
var tmp = document.createElement('div');
|
|
398
597
|
tmp.innerHTML = last.html;
|
|
399
|
-
|
|
598
|
+
var text = tmp.textContent || tmp.innerText || '';
|
|
400
599
|
if (!text.trim()) return;
|
|
401
600
|
// Remove the error message (last assistant message)
|
|
402
|
-
|
|
601
|
+
var el = document.getElementById('cc-messages');
|
|
403
602
|
if (el?.lastElementChild) el.lastElementChild.remove();
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
if (!res.ok) {
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
+
var d = await res.json();
|
|
435
634
|
status.innerHTML = '✓ 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 (
|
|
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 = '✓ 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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
718
|
+
var data = await res2.json();
|
|
520
719
|
if (data.ok && data.edited) {
|
|
521
720
|
status.innerHTML = '✓ 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
|
-
|
|
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
|
-
|
|
552
|
-
if (
|
|
750
|
+
var data3 = await res3.json();
|
|
751
|
+
if (data3.ok && data3.edited) {
|
|
553
752
|
status.innerHTML = '✓ Edited: <strong>' + escHtml(action.file) + '</strong>';
|
|
554
753
|
status.style.color = 'var(--green)';
|
|
555
754
|
} else {
|
|
556
|
-
status.innerHTML =
|
|
557
|
-
status.style.color =
|
|
755
|
+
status.innerHTML = data3.answer ? renderMd(data3.answer) : '✗ 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
|
-
|
|
563
|
-
|
|
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 (!
|
|
772
|
+
if (!res4.ok) { var d4 = await res4.json().catch(function() { return {}; }); throw new Error(d4.error || 'Schedule create failed'); }
|
|
574
773
|
status.innerHTML = '✓ 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
|
-
|
|
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 (!
|
|
782
|
+
if (!res5.ok) { var d5 = await res5.json().catch(function() { return {}; }); throw new Error(d5.error || 'Schedule delete failed'); }
|
|
584
783
|
status.innerHTML = '✓ 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
|
-
|
|
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 (!
|
|
594
|
-
|
|
595
|
-
status.innerHTML = '✓ Meeting started: <strong>' + escHtml(action.title) + '</strong>' + (
|
|
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 = '✓ 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
|
-
|
|
602
|
-
|
|
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 (!
|
|
806
|
+
if (!res7.ok) { var d7 = await res7.json().catch(function() { return {}; }); throw new Error(d7.error || 'Config update failed'); }
|
|
607
807
|
status.innerHTML = '✓ 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
|
-
|
|
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
|
-
|
|
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 (!
|
|
820
|
+
if (!res8.ok) { var d8 = await res8.json().catch(function() { return {}; }); throw new Error(d8.error || 'Pipeline update failed'); }
|
|
621
821
|
status.innerHTML = '✓ 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
|
|
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 (!
|
|
862
|
+
if (!res9.ok) { var d9 = await res9.json().catch(function() { return {}; }); throw new Error(d9.error || 'Pipeline trigger failed'); }
|
|
663
863
|
status.innerHTML = '✓ 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
|
-
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
status.innerHTML = '🐛 Bug filed: <a href="' + escHtml(
|
|
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 = '🐛 Bug filed: <a href="' + escHtml(d10.url) + '" target="_blank" style="color:var(--blue)">' + escHtml(action.title) + '</a>';
|
|
693
893
|
} else {
|
|
694
894
|
status.innerHTML = '🐛 Bug filed: <strong>' + escHtml(action.title) + '</strong> — <a href="https://github.com/yemi33/minions/issues" target="_blank" style="color:var(--blue)">view issues</a>';
|
|
695
895
|
}
|
|
@@ -781,33 +981,33 @@ async function ccExecuteAction(action) {
|
|
|
781
981
|
}
|
|
782
982
|
|
|
783
983
|
// --- CC Resize Logic ---
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
990
|
+
var drawer = document.getElementById('cc-drawer');
|
|
791
991
|
if (!drawer) return;
|
|
792
|
-
|
|
992
|
+
var saved = parseInt(localStorage.getItem(CC_WIDTH_KEY), 10);
|
|
793
993
|
if (saved && saved >= CC_MIN_WIDTH) {
|
|
794
|
-
|
|
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
|
-
|
|
801
|
-
|
|
1000
|
+
var handle = document.getElementById('cc-resize-handle');
|
|
1001
|
+
var drawer = document.getElementById('cc-drawer');
|
|
802
1002
|
if (!handle || !drawer) return;
|
|
803
1003
|
|
|
804
|
-
|
|
805
|
-
|
|
1004
|
+
var startX = 0;
|
|
1005
|
+
var startW = 0;
|
|
806
1006
|
|
|
807
1007
|
function onMouseMove(e) {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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 };
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -99,17 +99,12 @@ function _initQaSession() {
|
|
|
99
99
|
}
|
|
100
100
|
if (prior.filePath) _modalFilePath = prior.filePath;
|
|
101
101
|
_showThreadWrap();
|
|
102
|
-
requestAnimationFrame(function() {
|
|
103
|
-
var thread = document.getElementById('modal-qa-thread');
|
|
104
|
-
if (thread) thread.scrollTop = thread.scrollHeight;
|
|
105
|
-
});
|
|
106
102
|
} else {
|
|
107
103
|
_qaHistory = [];
|
|
108
104
|
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
109
|
-
|
|
105
|
+
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
106
|
+
if (wrap) wrap.style.display = 'none';
|
|
110
107
|
}
|
|
111
|
-
var actionSlot = document.getElementById('qa-action-slot');
|
|
112
|
-
if (actionSlot) actionSlot.innerHTML = '';
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
function clearQaConversation() {
|
|
@@ -117,9 +112,8 @@ function clearQaConversation() {
|
|
|
117
112
|
_qaQueue = [];
|
|
118
113
|
_qaProcessing = false;
|
|
119
114
|
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (actionSlot) actionSlot.innerHTML = '';
|
|
115
|
+
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
116
|
+
if (wrap) wrap.style.display = 'none';
|
|
123
117
|
if (_qaSessionKey) _qaSessions.delete(_qaSessionKey);
|
|
124
118
|
}
|
|
125
119
|
|
|
@@ -274,8 +268,7 @@ async function _processQaMessage(message, selection) {
|
|
|
274
268
|
'<button onclick="planExecute(\'' + esc + '\', \'\', null)" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Re-execute plan</button>' +
|
|
275
269
|
'<button onclick="this.closest(\'div\').innerHTML=\'<span style=color:var(--muted);font-size:11px>Paused. No work dispatched.</span>\'" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer">Keep paused</button>' +
|
|
276
270
|
'<span style="color:var(--muted);font-size:10px;width:100%">Re-execute creates a new PRD from the updated plan.</span>';
|
|
277
|
-
|
|
278
|
-
if (slot) { slot.innerHTML = ''; slot.appendChild(actionDiv); }
|
|
271
|
+
thread.appendChild(actionDiv);
|
|
279
272
|
}
|
|
280
273
|
} else {
|
|
281
274
|
const qaElapsedErr = Math.round((Date.now() - qaStartTime) / 1000);
|
|
@@ -346,25 +339,20 @@ function qaAbort() {
|
|
|
346
339
|
|
|
347
340
|
function toggleDocChat() {
|
|
348
341
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
349
|
-
var expandBar = document.getElementById('qa-expand-bar');
|
|
350
342
|
if (!wrap) return;
|
|
351
|
-
var
|
|
352
|
-
wrap.style.display =
|
|
353
|
-
|
|
343
|
+
var visible = wrap.style.display !== 'none';
|
|
344
|
+
wrap.style.display = visible ? 'none' : '';
|
|
345
|
+
var btn = document.getElementById('qa-toggle-btn');
|
|
346
|
+
if (btn) btn.innerHTML = visible ? '▲ Expand' : '▼ Collapse';
|
|
354
347
|
}
|
|
355
348
|
|
|
356
349
|
function _showThreadWrap() {
|
|
357
350
|
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
function _hideThreadWrap() {
|
|
364
|
-
var wrap = document.getElementById('modal-qa-thread-wrap');
|
|
365
|
-
var expandBar = document.getElementById('qa-expand-bar');
|
|
366
|
-
if (wrap) wrap.style.display = 'none';
|
|
367
|
-
if (expandBar) expandBar.style.display = 'none';
|
|
351
|
+
if (wrap && wrap.style.display === 'none') {
|
|
352
|
+
wrap.style.display = '';
|
|
353
|
+
var btn = document.getElementById('qa-toggle-btn');
|
|
354
|
+
if (btn) btn.innerHTML = '▼ Collapse';
|
|
355
|
+
}
|
|
368
356
|
}
|
|
369
357
|
|
|
370
|
-
window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaAbort, toggleDocChat };
|
|
358
|
+
window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaAbort, toggleDocChat, _showThreadWrap };
|
package/dashboard/layout.html
CHANGED
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
<span id="cc-session-info" style="font-size:10px;color:var(--muted)">New session</span>
|
|
33
33
|
</div>
|
|
34
34
|
<div style="display:flex;align-items:center;gap:8px">
|
|
35
|
-
<button onclick="
|
|
35
|
+
<button onclick="ccNewTab()" style="background:none;border:1px solid var(--border);color:var(--muted);font-size:10px;padding:2px 8px;border-radius:3px;cursor:pointer">New Chat</button>
|
|
36
36
|
<button onclick="toggleCommandCenter()" style="background:none;border:none;color:var(--muted);font-size:16px;cursor:pointer;padding:2px 6px">✕</button>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
39
|
+
<div id="cc-tab-bar" style="display:flex;gap:4px;padding:4px 12px;border-bottom:1px solid var(--border);overflow-x:auto;align-items:center;flex-shrink:0"></div>
|
|
39
40
|
<div id="cc-messages" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:10px"></div>
|
|
40
41
|
<div style="padding:12px 16px;border-top:1px solid var(--border)">
|
|
41
42
|
<div style="display:flex;gap:8px">
|
package/dashboard/styles.css
CHANGED
|
@@ -620,6 +620,12 @@
|
|
|
620
620
|
.notif-badge.processing span:nth-child(3) { animation-delay: 0.4s; }
|
|
621
621
|
@keyframes notifPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
622
622
|
|
|
623
|
+
/* Command Center tab bar */
|
|
624
|
+
.cc-tab { padding: 2px 8px; font-size: 10px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface2); color: var(--muted); cursor: pointer; white-space: nowrap; max-width: 120px; overflow: hidden; text-overflow: ellipsis; display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; }
|
|
625
|
+
.cc-tab.active { color: var(--blue); border-color: var(--blue); background: rgba(56,139,253,0.1); }
|
|
626
|
+
.cc-tab-close { margin-left: 4px; opacity: 0.5; cursor: pointer; font-size: 12px; line-height: 1; }
|
|
627
|
+
.cc-tab-close:hover { opacity: 1; color: var(--red); }
|
|
628
|
+
|
|
623
629
|
/* Command Center resize handle */
|
|
624
630
|
.cc-resize-handle { position: absolute; top: 0; left: -3px; bottom: 0; width: 6px; cursor: col-resize; z-index: 301; }
|
|
625
631
|
.cc-resize-handle::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 2px; height: 32px; background: var(--border); border-radius: 2px; opacity: 0; transition: opacity 0.15s; }
|
package/dashboard.js
CHANGED
|
@@ -577,6 +577,7 @@ function parseCCActions(text) {
|
|
|
577
577
|
// ── Shared LLM call core — used by CC panel and doc modals ──────────────────
|
|
578
578
|
|
|
579
579
|
// Session store for doc modals — keyed by filePath or title, persisted to disk
|
|
580
|
+
const CC_SESSIONS_PATH = path.join(ENGINE_DIR, 'cc-sessions.json');
|
|
580
581
|
const DOC_SESSIONS_PATH = path.join(ENGINE_DIR, 'doc-sessions.json');
|
|
581
582
|
const docSessions = new Map(); // key → { sessionId, lastActiveAt, turnCount }
|
|
582
583
|
|
|
@@ -3254,6 +3255,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3254
3255
|
return jsonReply(res, 200, { ok: true });
|
|
3255
3256
|
}
|
|
3256
3257
|
|
|
3258
|
+
async function handleCCSessionsList(req, res) {
|
|
3259
|
+
const sessions = shared.safeJsonArr(CC_SESSIONS_PATH);
|
|
3260
|
+
return jsonReply(res, 200, { sessions });
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
async function handleCCSessionDelete(req, res, match) {
|
|
3264
|
+
const id = match?.[1];
|
|
3265
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
3266
|
+
const sessions = shared.safeJsonArr(CC_SESSIONS_PATH);
|
|
3267
|
+
const filtered = sessions.filter(s => s.id !== id);
|
|
3268
|
+
safeWrite(CC_SESSIONS_PATH, filtered);
|
|
3269
|
+
return jsonReply(res, 200, { ok: true });
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3257
3272
|
async function handleCommandCenter(req, res) {
|
|
3258
3273
|
if (checkRateLimit('command-center', 10)) return jsonReply(res, 429, { error: 'Rate limited — max 10 requests/minute' });
|
|
3259
3274
|
try {
|
|
@@ -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 },
|
package/engine/lifecycle.js
CHANGED
|
@@ -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
|
|
package/engine/spawn-agent.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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"
|