@yemi33/minions 0.1.2245 → 0.1.2247
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/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
package/dashboard/slim/body.html
CHANGED
|
@@ -112,6 +112,11 @@
|
|
|
112
112
|
<div class="cockpit-value dim">0</div>
|
|
113
113
|
<div class="cockpit-detail">pinned context, notes & KB</div>
|
|
114
114
|
</div>
|
|
115
|
+
<div class="cockpit-tile" data-tile="plans">
|
|
116
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Plans</div>
|
|
117
|
+
<div class="cockpit-value dim">0</div>
|
|
118
|
+
<div class="cockpit-detail">drafts & materialized PRDs</div>
|
|
119
|
+
</div>
|
|
115
120
|
</div>
|
|
116
121
|
</div>
|
|
117
122
|
</div>
|
|
@@ -212,6 +217,29 @@
|
|
|
212
217
|
</div>
|
|
213
218
|
</div>
|
|
214
219
|
|
|
220
|
+
<!-- Plans + PRD control panel — opened from the "Plans" status tile. Two tabs:
|
|
221
|
+
Plans (.md plan drafts) and PRD (materialized PRDs). Each tab is rendered
|
|
222
|
+
lazily by dashboard/slim/js/plans.js into #slim-plans-body. Backed by the
|
|
223
|
+
existing endpoints (no new server routes):
|
|
224
|
+
Plans — /api/plans (list), /api/plans/:file (read), and the lifecycle
|
|
225
|
+
POSTs (/api/plans/{approve,execute,reject,pause,regenerate,
|
|
226
|
+
archive,delete,unarchive}).
|
|
227
|
+
PRD — the .json entries from /api/plans plus /api/prd and the
|
|
228
|
+
read-only subset of /api/prd-items. -->
|
|
229
|
+
<div class="modal-bg" id="slim-plans-modal">
|
|
230
|
+
<div class="modal slim-plans-modal-inner">
|
|
231
|
+
<div class="modal-header">
|
|
232
|
+
<h3>Plans</h3>
|
|
233
|
+
<div class="kn-tabs" id="slim-plans-tabs" role="tablist">
|
|
234
|
+
<button class="kn-tab active" id="slim-plans-tab-plans" data-plans-tab="plans" type="button" role="tab" aria-selected="true" aria-controls="slim-plans-body">Plans</button>
|
|
235
|
+
<button class="kn-tab" id="slim-plans-tab-prd" data-plans-tab="prd" type="button" role="tab" aria-selected="false" aria-controls="slim-plans-body">PRD</button>
|
|
236
|
+
</div>
|
|
237
|
+
<button id="slim-plans-close" class="icon-btn" title="Close" style="margin-left:auto">×</button>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="modal-body" id="slim-plans-body" role="tabpanel" tabindex="0" aria-labelledby="slim-plans-tab-plans"></div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
215
243
|
<!-- Pin editor — create (Pin Content action / "+ Pin") or edit an existing
|
|
216
244
|
note. Submits to POST /api/pinned (create) or /api/pinned/update (edit). -->
|
|
217
245
|
<div class="modal-bg" id="slim-pin-edit-modal">
|
|
@@ -27,8 +27,27 @@
|
|
|
27
27
|
|
|
28
28
|
var sessionId = null;
|
|
29
29
|
var messages = [];
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Per-tab streaming runtime (W-mqqvba66). Stream state lives here, keyed by
|
|
31
|
+
// tabId, so an in-flight turn keeps running AND accumulating when the user
|
|
32
|
+
// switches to another tab — mirroring the classic dashboard's per-tab
|
|
33
|
+
// tab._sending / _abortController / _segments model (command-center.js). The
|
|
34
|
+
// old module-global `sending` / `abortController` (one turn at a time) caused
|
|
35
|
+
// a mid-stream tab switch to abort the server turn and push the captured
|
|
36
|
+
// partial onto the wrong (now-active) tab. Each entry:
|
|
37
|
+
// { sending, abortController, streamedText, toolList, stream, userAborted }
|
|
38
|
+
// `stream` is the live DOM bubble handle — only set while that tab is the
|
|
39
|
+
// visible pane (null on background tabs; re-created on switch-back).
|
|
40
|
+
var tabStreams = {};
|
|
41
|
+
function _streamState(id) {
|
|
42
|
+
return tabStreams[id] || (tabStreams[id] = {
|
|
43
|
+
sending: false, abortController: null, streamedText: '',
|
|
44
|
+
toolList: [], stream: null, userAborted: false,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function _isTabSending(id) {
|
|
48
|
+
var st = tabStreams[id];
|
|
49
|
+
return !!(st && st.sending);
|
|
50
|
+
}
|
|
32
51
|
// Per-tab queue of follow-up messages submitted while a turn is streaming
|
|
33
52
|
// (W-mqayw3x6). Keyed by tabId so each chat tab keeps its own queue across
|
|
34
53
|
// tab switches. In-memory only — a hard refresh drops the queue by design.
|
|
@@ -129,14 +148,19 @@
|
|
|
129
148
|
if (m.severity) out.severity = m.severity;
|
|
130
149
|
return out;
|
|
131
150
|
}
|
|
132
|
-
function
|
|
133
|
-
for (var i = 0; i <
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
function _deriveTitleFrom(msgs) {
|
|
152
|
+
for (var i = 0; i < (msgs || []).length; i++) {
|
|
153
|
+
var m = msgs[i];
|
|
154
|
+
if (m && m.role === 'user') {
|
|
155
|
+
var t = (typeof m.text === 'string') ? m.text : _htmlToText(m.html);
|
|
156
|
+
if (t) return t.slice(0, 40);
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
return 'New chat';
|
|
139
160
|
}
|
|
161
|
+
function _deriveTitle() {
|
|
162
|
+
return _deriveTitleFrom(messages);
|
|
163
|
+
}
|
|
140
164
|
|
|
141
165
|
function loadState() {
|
|
142
166
|
try {
|
|
@@ -168,6 +192,70 @@
|
|
|
168
192
|
}, 300);
|
|
169
193
|
}
|
|
170
194
|
|
|
195
|
+
// Append a message to a SPECIFIC tab (W-mqqvba66). For the active (visible)
|
|
196
|
+
// tab we reuse the in-memory `messages` array + debounced saveState; for a
|
|
197
|
+
// background tab — one whose turn is still streaming while the user views a
|
|
198
|
+
// different tab — we write straight through to the shared cc-tabs store so the
|
|
199
|
+
// response is recorded on its OWN tab and never mis-attributed to the live
|
|
200
|
+
// `messages`/`tabId`. The originating tabId is captured at send time and
|
|
201
|
+
// threaded through every append (see command-send.js _performSend).
|
|
202
|
+
function recordTabMessage(id, msg) {
|
|
203
|
+
if (id === tabId) {
|
|
204
|
+
messages.push(msg);
|
|
205
|
+
saveState();
|
|
206
|
+
} else {
|
|
207
|
+
_persistBackgroundMessage(id, msg);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function _persistBackgroundMessage(id, msg) {
|
|
211
|
+
try {
|
|
212
|
+
var tabs = _readCcTabs();
|
|
213
|
+
var idx = _findTabIdx(tabs, id);
|
|
214
|
+
if (idx < 0) return; // tab was closed/removed — drop rather than resurrect it
|
|
215
|
+
var tab = tabs[idx];
|
|
216
|
+
var msgs = Array.isArray(tab.messages) ? tab.messages : [];
|
|
217
|
+
msgs.push(_slimToStored(msg));
|
|
218
|
+
tab.messages = msgs.slice(-SLIM_MAX_MESSAGES);
|
|
219
|
+
tab.title = _deriveTitleFrom(tab.messages);
|
|
220
|
+
tabs[idx] = tab;
|
|
221
|
+
_writeCcTabs(tabs);
|
|
222
|
+
} catch (_e) { /* localStorage full */ }
|
|
223
|
+
}
|
|
224
|
+
// Resolve / persist a tab's server session id, targeting the right store entry
|
|
225
|
+
// regardless of which tab is currently visible.
|
|
226
|
+
function _getTabSessionId(id) {
|
|
227
|
+
if (id === tabId) return sessionId;
|
|
228
|
+
try {
|
|
229
|
+
var tabs = _readCcTabs();
|
|
230
|
+
var idx = _findTabIdx(tabs, id);
|
|
231
|
+
return idx >= 0 ? (tabs[idx].sessionId || null) : null;
|
|
232
|
+
} catch (_e) { return null; }
|
|
233
|
+
}
|
|
234
|
+
function _setTabSessionId(id, sid) {
|
|
235
|
+
if (id === tabId) { sessionId = sid; saveState(); return; }
|
|
236
|
+
try {
|
|
237
|
+
var tabs = _readCcTabs();
|
|
238
|
+
var idx = _findTabIdx(tabs, id);
|
|
239
|
+
if (idx < 0) return;
|
|
240
|
+
tabs[idx].sessionId = sid;
|
|
241
|
+
_writeCcTabs(tabs);
|
|
242
|
+
} catch (_e) { /* localStorage full */ }
|
|
243
|
+
}
|
|
244
|
+
// True if a given tab already has at least one user message (drives the
|
|
245
|
+
// first-message title-chip refresh), targeting the active in-memory array or
|
|
246
|
+
// the persisted store for a background tab.
|
|
247
|
+
function _tabHasUserMessage(id) {
|
|
248
|
+
if (id === tabId) {
|
|
249
|
+
return messages.some(function(m) { return m && m.role === 'user'; });
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
var tabs = _readCcTabs();
|
|
253
|
+
var idx = _findTabIdx(tabs, id);
|
|
254
|
+
if (idx < 0) return false;
|
|
255
|
+
return (tabs[idx].messages || []).some(function(m) { return m && m.role === 'user'; });
|
|
256
|
+
} catch (_e) { return false; }
|
|
257
|
+
}
|
|
258
|
+
|
|
171
259
|
function escHtmlChat(s) {
|
|
172
260
|
return String(s == null ? '' : s)
|
|
173
261
|
.replace(/&/g, '&')
|
|
@@ -425,7 +513,7 @@
|
|
|
425
513
|
return { panel: panel, scroll: scroll };
|
|
426
514
|
}
|
|
427
515
|
|
|
428
|
-
function buildStreamBubble() {
|
|
516
|
+
function buildStreamBubble(initialText, initialTools) {
|
|
429
517
|
clearEmpty();
|
|
430
518
|
var div = document.createElement('div');
|
|
431
519
|
div.className = 'chat-msg assistant streaming';
|
|
@@ -445,11 +533,20 @@
|
|
|
445
533
|
thinking.innerHTML = 'Thinking<span class="chat-thinking-dots"><span></span><span></span><span></span></span>';
|
|
446
534
|
toolbar.appendChild(thinking);
|
|
447
535
|
|
|
448
|
-
|
|
536
|
+
// Reuse the caller's backing tool list (shared by reference) when restoring
|
|
537
|
+
// a live bubble after a tab switch, so already-streamed tools replay and new
|
|
538
|
+
// ones keep appending into the SAME per-tab array (W-mqqvba66).
|
|
539
|
+
var toolList = Array.isArray(initialTools) ? initialTools : [];
|
|
449
540
|
var tp = buildToolsPanel(toolList);
|
|
450
|
-
tp.panel.style.display = 'none';
|
|
541
|
+
tp.panel.style.display = toolList.length ? '' : 'none';
|
|
451
542
|
toolbar.appendChild(tp.panel);
|
|
452
543
|
|
|
544
|
+
// Seed the partial text accumulated so far (restore-on-switch-back path).
|
|
545
|
+
if (initialText) {
|
|
546
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMarkdown() escapes input via escHtmlChat() first; later substitutions only re-insert pre-escaped <pre><code>/<a>/<strong>/<em> spans with scheme-validated hrefs
|
|
547
|
+
body.innerHTML = renderMarkdown(initialText);
|
|
548
|
+
}
|
|
549
|
+
|
|
453
550
|
_appendMsgEl(div);
|
|
454
551
|
scrollToBottom();
|
|
455
552
|
|
|
@@ -489,8 +586,15 @@
|
|
|
489
586
|
};
|
|
490
587
|
}
|
|
491
588
|
|
|
492
|
-
|
|
493
|
-
|
|
589
|
+
// Stream state is per-tab now (W-mqqvba66): toggle the originating tab's
|
|
590
|
+
// sending flag, and only refresh the composer chrome when that tab is the
|
|
591
|
+
// visible one (the Send/Stop buttons always reflect the ACTIVE tab).
|
|
592
|
+
function setSendingFor(id, on) {
|
|
593
|
+
_streamState(id).sending = on;
|
|
594
|
+
if (id === tabId) _refreshComposerUI();
|
|
595
|
+
}
|
|
596
|
+
function _refreshComposerUI() {
|
|
597
|
+
var on = _isTabSending(tabId);
|
|
494
598
|
// Composer stays interactive while a turn streams so the user can keep
|
|
495
599
|
// typing and queue follow-up messages (W-mqayw3x6 #1). The Send button
|
|
496
600
|
// stays enabled — submitting mid-turn enqueues — and only its label
|
|
@@ -561,30 +665,56 @@
|
|
|
561
665
|
tabsEl.appendChild(add);
|
|
562
666
|
}
|
|
563
667
|
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
668
|
+
// Detach the active tab's live stream-bubble handle before its transcript DOM
|
|
669
|
+
// is wiped (rerenderHistory). The background fetch loop checks `st.stream`
|
|
670
|
+
// every chunk, so nulling it makes the still-running turn stop touching the
|
|
671
|
+
// DOM while keeping its partial accumulating into st.streamedText / saveState.
|
|
672
|
+
function _detachActiveStream() {
|
|
673
|
+
var st = tabStreams[tabId];
|
|
674
|
+
if (st) st.stream = null;
|
|
675
|
+
}
|
|
676
|
+
// Re-attach a live streaming bubble for a tab the user is switching BACK to
|
|
677
|
+
// while its turn is still in flight, seeded from the tab's accumulated partial
|
|
678
|
+
// + tool list (mirror classic's _restoreStreamHtml). The running fetch loop
|
|
679
|
+
// resumes updating st.stream as new chunks arrive; slim has no elapsed-timer
|
|
680
|
+
// chrome, so unlike classic no 1s restore interval is needed.
|
|
681
|
+
function _restoreStreamFor(id) {
|
|
682
|
+
var st = tabStreams[id];
|
|
683
|
+
if (!st || !st.sending) return;
|
|
684
|
+
st.stream = buildStreamBubble(st.streamedText, st.toolList);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Switch the visible pane to another shared tab. The previous tab's in-flight
|
|
688
|
+
// stream is KEPT RUNNING in the background (W-mqqvba66) — we never abort on a
|
|
689
|
+
// switch — and the target tab is re-hydrated from its own persisted state,
|
|
690
|
+
// restoring its live streaming bubble if it is mid-turn.
|
|
567
691
|
function switchSlimTab(id) {
|
|
568
692
|
if (!id || id === tabId) return;
|
|
569
|
-
|
|
693
|
+
_detachActiveStream();
|
|
570
694
|
tabId = id;
|
|
571
695
|
try { localStorage.setItem(CC_ACTIVE_KEY, id); } catch (_e) { /* private mode */ }
|
|
572
696
|
sessionId = null;
|
|
573
697
|
messages = [];
|
|
574
698
|
loadState();
|
|
575
699
|
rerenderHistory();
|
|
700
|
+
_restoreStreamFor(id);
|
|
576
701
|
renderTabBar();
|
|
702
|
+
_refreshComposerUI();
|
|
577
703
|
inputEl.focus();
|
|
578
704
|
}
|
|
579
705
|
|
|
580
706
|
function closeSlimTab(id) {
|
|
581
707
|
if (!id) return;
|
|
582
|
-
|
|
708
|
+
// Closing a streaming tab DOES abort + delete its server session (one of the
|
|
709
|
+
// only two abort paths left, alongside the explicit Stop button).
|
|
710
|
+
if (_isTabSending(id)) abortTab(id);
|
|
583
711
|
var tabs = _readCcTabs();
|
|
584
712
|
var idx = _findTabIdx(tabs, id);
|
|
585
713
|
if (idx < 0) return;
|
|
586
714
|
tabs.splice(idx, 1);
|
|
587
715
|
_writeCcTabs(tabs);
|
|
716
|
+
delete queues[id];
|
|
717
|
+
delete queueSuspended[id];
|
|
588
718
|
// Evict the server-side session for the closed tab (mirrors classic).
|
|
589
719
|
try { fetch('/api/cc-sessions/' + encodeURIComponent(id), { method: 'DELETE' }).catch(function() {}); } catch (_e) { /* ignore */ }
|
|
590
720
|
if (id !== tabId) { renderTabBar(); return; }
|
|
@@ -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
|
+
|