@yemi33/minions 0.1.2244 → 0.1.2246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-work-items.js +15 -7
- package/dashboard/slim/body.html +28 -0
- package/dashboard/slim/js/chat.js +146 -16
- package/dashboard/slim/js/command-send.js +100 -77
- package/dashboard/slim/js/modals-tiles.js +23 -0
- package/dashboard/slim/js/plans.js +550 -0
- package/dashboard/slim/js/status.js +7 -0
- package/dashboard/slim/styles.css +4 -3
- package/dashboard-build.js +1 -1
- package/docs/slim-ux/architecture-suggestions.md +10 -0
- package/docs/slim-ux/concepts.md +20 -0
- package/engine/lifecycle.js +52 -0
- package/engine/shared.js +16 -0
- package/engine.js +4 -4
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
var label = action.title || action.id || action.file || action.agent || '';
|
|
5
5
|
return label ? t + ': ' + label : t;
|
|
6
6
|
}
|
|
7
|
-
function renderActionResults(actions, results) {
|
|
7
|
+
function renderActionResults(turnTabId, actions, results) {
|
|
8
8
|
if (!actions || !actions.length) return;
|
|
9
9
|
for (var i = 0; i < actions.length; i++) {
|
|
10
10
|
var action = actions[i];
|
|
@@ -17,22 +17,22 @@
|
|
|
17
17
|
else if (r && r.warning) { severity = 'warn'; symbol = 'ℹ'; detail = ' — ' + r.warning; }
|
|
18
18
|
else if (r && r.duplicate) { severity = 'warn'; symbol = '↺'; detail = ' (already exists)'; }
|
|
19
19
|
var text = symbol + ' ' + label + detail;
|
|
20
|
-
appendActionStatus(severity, text);
|
|
21
|
-
|
|
20
|
+
if (turnTabId === tabId) appendActionStatus(severity, text);
|
|
21
|
+
recordTabMessage(turnTabId, { role: 'action', severity: severity, text: text });
|
|
22
22
|
}
|
|
23
|
-
saveState();
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
// Public entry point for the Send button / Enter key. While a turn is in
|
|
27
|
-
// flight the composer stays interactive (
|
|
28
|
-
// so a second submit QUEUES the message instead of dropping it
|
|
29
|
-
// active stream. The queue drains FIFO, one turn at a time
|
|
26
|
+
// flight the composer stays interactive (the per-tab sending flag no longer
|
|
27
|
+
// disables it), so a second submit QUEUES the message instead of dropping it
|
|
28
|
+
// or racing the active stream. The queue drains FIFO, one turn at a time
|
|
29
|
+
// per tab (W-mqayw3x6).
|
|
30
30
|
function sendMessage() {
|
|
31
31
|
var text = inputEl.value.trim();
|
|
32
32
|
if (!text) return;
|
|
33
33
|
inputEl.value = '';
|
|
34
34
|
inputEl.style.height = 'auto';
|
|
35
|
-
if (
|
|
35
|
+
if (_isTabSending(tabId)) {
|
|
36
36
|
_enqueue(text);
|
|
37
37
|
inputEl.focus();
|
|
38
38
|
return;
|
|
@@ -46,57 +46,74 @@
|
|
|
46
46
|
renderQueue();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Promote the next queued message for the active tab
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// Promote the next queued message for the active tab.
|
|
50
|
+
function _drainQueue() { _drainQueueFor(tabId); }
|
|
51
|
+
|
|
52
|
+
// Promote the next queued message for a SPECIFIC tab, one at a time. No-op
|
|
53
|
+
// while that tab's turn is in flight or its queue is suspended (after an
|
|
54
|
+
// abort/error). Background tabs (W-mqqvba66) drain into their OWN turn, so a
|
|
55
|
+
// tab you switched away from keeps working through its queue.
|
|
56
|
+
function _drainQueueFor(id) {
|
|
57
|
+
if (_isTabSending(id) || queueSuspended[id]) return;
|
|
58
|
+
var q = queues[id];
|
|
59
|
+
if (!q || !q.length) return;
|
|
55
60
|
var next = q.shift();
|
|
56
|
-
renderQueue();
|
|
57
|
-
_performSend(next);
|
|
61
|
+
if (id === tabId) renderQueue();
|
|
62
|
+
_performSend(next, id);
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
// Called from a finished turn's `finally`. On clean completion auto-fire the
|
|
61
|
-
// next queued message; on abort or error suspend auto-drain and
|
|
62
|
-
// queue visible so the user can resend or dismiss explicitly
|
|
66
|
+
// next queued message FOR THAT TAB; on abort or error suspend auto-drain and
|
|
67
|
+
// leave the queue visible so the user can resend or dismiss explicitly
|
|
68
|
+
// (W-mqayw3x6 #5/#6).
|
|
63
69
|
function _afterTurn(turnTabId, outcome) {
|
|
64
70
|
var q = queues[turnTabId];
|
|
65
71
|
if (!q || !q.length) return;
|
|
66
|
-
if (outcome === 'done'
|
|
67
|
-
|
|
72
|
+
if (outcome === 'done') {
|
|
73
|
+
_drainQueueFor(turnTabId);
|
|
68
74
|
} else {
|
|
69
75
|
queueSuspended[turnTabId] = true;
|
|
70
76
|
if (turnTabId === tabId) renderQueue();
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
async function _performSend(text) {
|
|
75
|
-
// Bind this turn to the tab it started on
|
|
76
|
-
//
|
|
77
|
-
// the
|
|
78
|
-
|
|
80
|
+
async function _performSend(text, sendTabId) {
|
|
81
|
+
// Bind this turn to the tab it started on (W-mqqvba66). EVERY append /
|
|
82
|
+
// saveState targets `turnTabId` — never the live-global `tabId`/`messages`
|
|
83
|
+
// — so a mid-turn tab switch keeps the stream running in the background and
|
|
84
|
+
// its partial is recorded on the originating tab, never mis-attributed to
|
|
85
|
+
// whatever tab the user navigated to. `live` is whether that tab is the
|
|
86
|
+
// currently-visible pane (only then do we touch the DOM directly).
|
|
87
|
+
var turnTabId = sendTabId || tabId;
|
|
88
|
+
var st = _streamState(turnTabId);
|
|
79
89
|
var turnOutcome = 'done';
|
|
90
|
+
var live = (turnTabId === tabId);
|
|
80
91
|
|
|
81
92
|
// The tab title is derived from the first user message; only that message
|
|
82
93
|
// changes the chip, so re-render the bar once instead of on every send.
|
|
83
|
-
var wasFirstUser = !
|
|
84
|
-
appendBubble('user', text);
|
|
85
|
-
|
|
86
|
-
saveState();
|
|
94
|
+
var wasFirstUser = !_tabHasUserMessage(turnTabId);
|
|
95
|
+
if (live) appendBubble('user', text);
|
|
96
|
+
recordTabMessage(turnTabId, { role: 'user', text: text });
|
|
87
97
|
if (wasFirstUser) renderTabBar();
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
setSendingFor(turnTabId, true);
|
|
90
100
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
st.streamedText = '';
|
|
102
|
+
st.toolList = [];
|
|
103
|
+
st.userAborted = false;
|
|
104
|
+
// The live bubble exists only while this tab is visible; a background turn
|
|
105
|
+
// accumulates into st.streamedText / st.toolList and re-attaches its bubble
|
|
106
|
+
// on switch-back (_restoreStreamFor). Share st.toolList by reference so
|
|
107
|
+
// restore replays already-streamed tools.
|
|
108
|
+
st.stream = live ? buildStreamBubble(st.streamedText, st.toolList) : null;
|
|
109
|
+
st.abortController = new AbortController();
|
|
95
110
|
var timeoutSignal = null;
|
|
96
111
|
try { timeoutSignal = AbortSignal.timeout(STREAM_TIMEOUT_MS); } catch (_e) { /* old browsers */ }
|
|
97
112
|
var signal = timeoutSignal && AbortSignal.any
|
|
98
|
-
? AbortSignal.any([abortController.signal, timeoutSignal])
|
|
99
|
-
: abortController.signal;
|
|
113
|
+
? AbortSignal.any([st.abortController.signal, timeoutSignal])
|
|
114
|
+
: st.abortController.signal;
|
|
115
|
+
|
|
116
|
+
var turnSessionId = _getTabSessionId(turnTabId);
|
|
100
117
|
|
|
101
118
|
try {
|
|
102
119
|
var res = await fetch('/api/command-center/stream', {
|
|
@@ -106,7 +123,7 @@
|
|
|
106
123
|
// when the indicator reads "Select project"). noProjectSelected lets the
|
|
107
124
|
// server inject a "no project — ask the user" CC preamble instead of
|
|
108
125
|
// silently defaulting; only the slim composer sends it (W-mqayzsj3).
|
|
109
|
-
body: JSON.stringify({ message: text, tabId:
|
|
126
|
+
body: JSON.stringify({ message: text, tabId: turnTabId, sessionId: turnSessionId, currentProject: currentProject || undefined, noProjectSelected: currentProject ? undefined : true }),
|
|
110
127
|
signal: signal,
|
|
111
128
|
});
|
|
112
129
|
if (!res.ok) {
|
|
@@ -135,10 +152,14 @@
|
|
|
135
152
|
var evt;
|
|
136
153
|
try { evt = JSON.parse(line.slice(6)); } catch (_e) { continue; }
|
|
137
154
|
if (evt.type === 'chunk') {
|
|
138
|
-
streamedText = mergeStreamText(streamedText, evt.text || '');
|
|
139
|
-
stream.setText(streamedText);
|
|
155
|
+
st.streamedText = mergeStreamText(st.streamedText, evt.text || '');
|
|
156
|
+
if (st.stream) st.stream.setText(st.streamedText);
|
|
140
157
|
} else if (evt.type === 'tool') {
|
|
141
|
-
stream.addTool
|
|
158
|
+
// st.stream.addTool pushes into the shared st.toolList; when the tab
|
|
159
|
+
// is backgrounded (no bubble) accumulate directly so the tool set is
|
|
160
|
+
// intact for restore / the final message.
|
|
161
|
+
if (st.stream) st.stream.addTool(evt.name, evt.input || {});
|
|
162
|
+
else st.toolList.push({ name: evt.name, input: evt.input || {} });
|
|
142
163
|
} else if (evt.type === 'heartbeat') {
|
|
143
164
|
/* server keep-alive — ignored on purpose */
|
|
144
165
|
} else if (evt.type === 'done') {
|
|
@@ -150,81 +171,82 @@
|
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
if (errorEvt) {
|
|
153
|
-
stream.replaceWithError(errorEvt.error || 'Error');
|
|
154
|
-
|
|
155
|
-
saveState();
|
|
174
|
+
if (st.stream) st.stream.replaceWithError(errorEvt.error || 'Error');
|
|
175
|
+
recordTabMessage(turnTabId, { role: 'error', text: errorEvt.error || 'Error' });
|
|
156
176
|
turnOutcome = 'error';
|
|
157
177
|
return;
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
if (doneEvt) {
|
|
161
|
-
var finalText = mergeStreamText(streamedText, doneEvt.text || '');
|
|
162
|
-
stream.finalize(finalText || '(no response)');
|
|
163
|
-
|
|
164
|
-
if (doneEvt.sessionId !== undefined)
|
|
165
|
-
saveState();
|
|
181
|
+
var finalText = mergeStreamText(st.streamedText, doneEvt.text || '');
|
|
182
|
+
if (st.stream) st.stream.finalize(finalText || '(no response)');
|
|
183
|
+
recordTabMessage(turnTabId, { role: 'assistant', text: finalText || '(no response)', toolCalls: st.toolList });
|
|
184
|
+
if (doneEvt.sessionId !== undefined) _setTabSessionId(turnTabId, doneEvt.sessionId || null);
|
|
166
185
|
if (doneEvt.actionParseError) {
|
|
167
186
|
var pe = '⚠️ Actions block emitted but JSON could not be parsed — no actions executed.';
|
|
168
|
-
appendActionStatus('err', pe);
|
|
169
|
-
|
|
170
|
-
saveState();
|
|
187
|
+
if (turnTabId === tabId) appendActionStatus('err', pe);
|
|
188
|
+
recordTabMessage(turnTabId, { role: 'action', severity: 'err', text: pe });
|
|
171
189
|
}
|
|
172
190
|
if (doneEvt.actions && doneEvt.actions.length > 0) {
|
|
173
|
-
renderActionResults(doneEvt.actions, doneEvt.actionResults || []);
|
|
191
|
+
renderActionResults(turnTabId, doneEvt.actions, doneEvt.actionResults || []);
|
|
174
192
|
}
|
|
175
193
|
// Refresh status + history opportunistically — actions probably
|
|
176
194
|
// changed something.
|
|
177
195
|
scheduleStatusRefresh(800);
|
|
178
196
|
} else {
|
|
179
|
-
stream.finalize(streamedText || '(stream ended before completion)');
|
|
180
|
-
|
|
181
|
-
saveState();
|
|
197
|
+
if (st.stream) st.stream.finalize(st.streamedText || '(stream ended before completion)');
|
|
198
|
+
recordTabMessage(turnTabId, { role: 'assistant', text: st.streamedText || '(stream ended before completion)', toolCalls: st.toolList });
|
|
182
199
|
}
|
|
183
200
|
} catch (e) {
|
|
184
201
|
if (e && (e.name === 'AbortError' || /aborted/i.test(String(e.message || '')))) {
|
|
185
|
-
userAborted = true;
|
|
202
|
+
st.userAborted = true;
|
|
186
203
|
turnOutcome = 'aborted';
|
|
187
|
-
if (streamedText) {
|
|
188
|
-
stream.finalize(streamedText);
|
|
189
|
-
|
|
204
|
+
if (st.streamedText) {
|
|
205
|
+
if (st.stream) st.stream.finalize(st.streamedText);
|
|
206
|
+
recordTabMessage(turnTabId, { role: 'assistant', text: st.streamedText, toolCalls: st.toolList });
|
|
190
207
|
} else {
|
|
191
|
-
stream.replaceWithError('Stopped.');
|
|
192
|
-
|
|
208
|
+
if (st.stream) st.stream.replaceWithError('Stopped.');
|
|
209
|
+
recordTabMessage(turnTabId, { role: 'error', text: 'Stopped.' });
|
|
193
210
|
}
|
|
194
|
-
saveState();
|
|
195
211
|
} else {
|
|
196
212
|
turnOutcome = 'error';
|
|
197
213
|
var msg = 'Send failed: ' + (e && e.message ? e.message : e);
|
|
198
|
-
stream.replaceWithError(msg);
|
|
199
|
-
|
|
200
|
-
saveState();
|
|
214
|
+
if (st.stream) st.stream.replaceWithError(msg);
|
|
215
|
+
recordTabMessage(turnTabId, { role: 'error', text: msg });
|
|
201
216
|
}
|
|
202
217
|
} finally {
|
|
203
|
-
|
|
204
|
-
abortController = null;
|
|
205
|
-
|
|
218
|
+
setSendingFor(turnTabId, false);
|
|
219
|
+
st.abortController = null;
|
|
220
|
+
st.stream = null;
|
|
221
|
+
if (turnTabId === tabId && !st.userAborted) inputEl.focus();
|
|
206
222
|
_afterTurn(turnTabId, turnOutcome);
|
|
207
223
|
}
|
|
208
224
|
}
|
|
209
225
|
|
|
210
|
-
|
|
211
|
-
|
|
226
|
+
// Abort a SPECIFIC tab's in-flight turn — kills the server turn AND aborts the
|
|
227
|
+
// fetch. Wired to ONLY two callers (W-mqqvba66): the explicit Stop button
|
|
228
|
+
// (abortInFlight → active tab) and closeSlimTab (closing a streaming tab).
|
|
229
|
+
// A plain tab SWITCH never calls this.
|
|
230
|
+
function abortTab(id) {
|
|
231
|
+
var st = tabStreams[id];
|
|
232
|
+
if (!st || !st.sending || !st.abortController) return;
|
|
212
233
|
try {
|
|
213
234
|
fetch('/api/command-center/abort', {
|
|
214
235
|
method: 'POST',
|
|
215
236
|
headers: { 'Content-Type': 'application/json' },
|
|
216
|
-
body: JSON.stringify({ tabId:
|
|
237
|
+
body: JSON.stringify({ tabId: id }),
|
|
217
238
|
}).catch(function() {});
|
|
218
239
|
} catch (_e) { /* ignore */ }
|
|
219
|
-
try { abortController.abort(); } catch (_e) { /* ignore */ }
|
|
240
|
+
try { st.abortController.abort(); } catch (_e) { /* ignore */ }
|
|
220
241
|
}
|
|
242
|
+
function abortInFlight() { abortTab(tabId); }
|
|
221
243
|
|
|
222
244
|
async function slimChatNew() {
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
245
|
+
// New chat creates a fresh tab and switches to it. Like switchSlimTab, it
|
|
246
|
+
// does NOT abort the leaving tab's in-flight stream (W-mqqvba66) — that turn
|
|
247
|
+
// keeps streaming + draining its queue in the background. Detach its live
|
|
248
|
+
// bubble first since the transcript is about to be wiped.
|
|
249
|
+
_detachActiveStream();
|
|
228
250
|
// Register a fresh tab in the shared cc-tabs store and switch slim onto
|
|
229
251
|
// it — classic's other tabs are left intact, and the previous session
|
|
230
252
|
// keeps its own server-side cc-sessions.json entry (no DELETE).
|
|
@@ -241,6 +263,7 @@
|
|
|
241
263
|
saveState();
|
|
242
264
|
rerenderHistory();
|
|
243
265
|
renderTabBar();
|
|
266
|
+
_refreshComposerUI();
|
|
244
267
|
inputEl.focus();
|
|
245
268
|
}
|
|
246
269
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
bindModalClose('slim-agent-modal', 'slim-agent-close', _stopAgentDetailRuntime);
|
|
18
18
|
bindModalClose('slim-tools-modal', 'slim-tools-close');
|
|
19
19
|
bindModalClose('slim-tile-modal', 'slim-tile-close');
|
|
20
|
+
bindModalClose('slim-plans-modal', 'slim-plans-close');
|
|
20
21
|
|
|
21
22
|
function tileEmpty(body, text) {
|
|
22
23
|
var d = document.createElement('div');
|
|
@@ -320,6 +321,9 @@
|
|
|
320
321
|
// The Knowledge tile opens the unified Knowledge control panel (Pinned
|
|
321
322
|
// Context + Notes + KB tabs) rather than the read-only tile detail view.
|
|
322
323
|
if (key === 'knowledge') { openKnowledgeModal(); return; }
|
|
324
|
+
// The Plans tile opens the tabbed Plans/PRD control panel (rendered by
|
|
325
|
+
// dashboard/slim/js/plans.js) rather than the read-only tile detail view.
|
|
326
|
+
if (key === 'plans') { openPlansModal(); return; }
|
|
323
327
|
var view = TILE_VIEWS[key];
|
|
324
328
|
if (!view) return;
|
|
325
329
|
var modal = document.getElementById('slim-tile-modal');
|
|
@@ -356,3 +360,22 @@
|
|
|
356
360
|
}
|
|
357
361
|
})();
|
|
358
362
|
|
|
363
|
+
// ── Plans control-panel wiring ─────────────────────────────────────
|
|
364
|
+
// The Plans tile opens #slim-plans-modal (routed in openTileModal above).
|
|
365
|
+
// plans.js is the renderer only; the tab-switch + lazy tile-count wiring
|
|
366
|
+
// lives here, mirroring knowledge.js#bindKnowledgeUi. The plan/PRD count is
|
|
367
|
+
// not part of the /api/status poll, so it is fetched lazily from /api/plans
|
|
368
|
+
// shortly after first paint (and refreshed on each modal open by the renderer).
|
|
369
|
+
(function bindPlansUi() {
|
|
370
|
+
var tabBar = document.getElementById('slim-plans-tabs');
|
|
371
|
+
if (tabBar) {
|
|
372
|
+
tabBar.addEventListener('click', function(ev) {
|
|
373
|
+
var btn = ev.target && ev.target.closest ? ev.target.closest('.kn-tab') : null;
|
|
374
|
+
if (!btn) return;
|
|
375
|
+
var tab = btn.getAttribute('data-plans-tab');
|
|
376
|
+
if (tab && typeof setPlansTab === 'function') setPlansTab(tab);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (typeof loadPlansCounts === 'function') setTimeout(loadPlansCounts, 1500);
|
|
380
|
+
})();
|
|
381
|
+
|