@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
package/dashboard/js/refresh.js
CHANGED
|
@@ -171,6 +171,14 @@ function wiRow(item) {
|
|
|
171
171
|
: (item.branchStrategy === 'shared-branch' && item.status === 'done')
|
|
172
172
|
? '<span style="font-size:var(--text-xs);color:var(--muted)" title="Part of shared branch — aggregate PR created at verify stage">shared branch</span>'
|
|
173
173
|
: '<span style="color:var(--muted)">—</span>';
|
|
174
|
+
// W-mqrdn6qq — Non-retryable failures surface their red failReason snippet in
|
|
175
|
+
// the PR column (not the Agent column). The engine prefixes failReason with
|
|
176
|
+
// "Non-retryable failure:" and stamps item._failureClass on non-retryable
|
|
177
|
+
// demotion (engine/dispatch.js writeDispatchResult / force-demote path).
|
|
178
|
+
var isNonRetryableFail = !!item._failureClass || /^Non-retryable failure:/i.test(item.failReason || '');
|
|
179
|
+
var failSnippet = item.failReason
|
|
180
|
+
? '<span style="display:block;font-size:var(--text-xs);color:var(--red)" title="' + escapeHtml(item.failReason) + '">' + escapeHtml(item.failReason.slice(0, 30)) + '</span>'
|
|
181
|
+
: '';
|
|
174
182
|
// PR follow-up chip (W-mpej3cox00099466) — surfaced when the WI was spun
|
|
175
183
|
// off from a PR comment via meta.pr_followup. Links to the parent PR.
|
|
176
184
|
var prFollowup = item.meta && item.meta.pr_followup;
|
|
@@ -183,7 +191,7 @@ function wiRow(item) {
|
|
|
183
191
|
}
|
|
184
192
|
return '<tr data-wi-id="' + escapeHtml(item.id) + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\')">' +
|
|
185
193
|
'<td><span class="pr-id">' + escapeHtml(item.id || '') + '</span></td>' +
|
|
186
|
-
'<td style="max-width:
|
|
194
|
+
'<td style="min-width:280px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + followupChip + '</td>' +
|
|
187
195
|
'<td><span style="font-size:var(--text-sm);color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
|
|
188
196
|
(item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:var(--text-xs)">fan-out</span>' : '') + '</td>' +
|
|
189
197
|
'<td>' + typeBadge(item.type) + '</td>' +
|
|
@@ -217,9 +225,9 @@ function wiRow(item) {
|
|
|
217
225
|
(item.completedAgents && item.completedAgents.length > 0
|
|
218
226
|
? '<span class="pr-agent">' + escapeHtml(item.completedAgents.join(', ')) + '</span>'
|
|
219
227
|
: '<span class="pr-agent">' + escapeHtml(item.dispatched_to || item.agent || '—') + '</span>') +
|
|
220
|
-
(
|
|
228
|
+
(failSnippet && !isNonRetryableFail ? failSnippet : '') +
|
|
221
229
|
'</td>' +
|
|
222
|
-
'<td>' + prLink + '</td>' +
|
|
230
|
+
'<td>' + prLink + (failSnippet && isNonRetryableFail ? failSnippet : '') + '</td>' +
|
|
223
231
|
'<td><span class="pr-date">' + escapeHtml((item.created || '').slice(0, 16).replace('T', ' ')) + '</span></td>' +
|
|
224
232
|
'<td style="white-space:nowrap;font-size:var(--text-xs);color:var(--muted)">' +
|
|
225
233
|
(function() {
|
|
@@ -278,7 +286,7 @@ function renderWorkItems(items, opts) {
|
|
|
278
286
|
const start = wiPage * WI_PER_PAGE;
|
|
279
287
|
const pageItems = items.slice(start, start + WI_PER_PAGE);
|
|
280
288
|
|
|
281
|
-
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>
|
|
289
|
+
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>';
|
|
282
290
|
html += pageItems.map(wiRow).join('');
|
|
283
291
|
html += '</tbody></table></div>';
|
|
284
292
|
|
|
@@ -479,7 +487,7 @@ async function toggleWorkItemArchive() {
|
|
|
479
487
|
items.map(function(i) {
|
|
480
488
|
return '<tr style="opacity:0.6">' +
|
|
481
489
|
'<td><span class="pr-id">' + escapeHtml(i.id || '') + '</span></td>' +
|
|
482
|
-
'<td style="max-width:
|
|
490
|
+
'<td style="min-width:240px;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(i.title || '') + '</td>' +
|
|
483
491
|
'<td><span class="dispatch-type ' + (i.type || '') + '">' + escapeHtml(i.type || '') + '</span></td>' +
|
|
484
492
|
'<td style="color:' + (i.status === 'done' ? 'var(--green)' : 'var(--red)') + '">' + escapeHtml(i.status || '') + '</td>' +
|
|
485
493
|
'<td>' + escapeHtml(i.dispatched_to || '—') + '</td>' +
|
|
@@ -746,7 +754,7 @@ function _wiRenderDetail(item) {
|
|
|
746
754
|
}
|
|
747
755
|
html += field('Description', '<div id="wi-detail-desc" style="font-size:var(--text-md);max-height:320px;overflow-y:auto;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)">' + _descHtml + '</div>');
|
|
748
756
|
html += field('Agent', escapeHtml(item.dispatched_to || item.agent || 'Auto'));
|
|
749
|
-
html += field('
|
|
757
|
+
html += field('Project', escapeHtml(item._source || 'central'));
|
|
750
758
|
// P-714ef144 — surface meta.workdir in the detail modal so operators can
|
|
751
759
|
// confirm the agent's actual cwd. Renders nothing when unset (which is
|
|
752
760
|
// the back-compat default = project root). Code-tagged so the path is
|
|
@@ -1053,7 +1061,7 @@ function openWorkItemDetail(id) {
|
|
|
1053
1061
|
|
|
1054
1062
|
function openAllWorkItems() {
|
|
1055
1063
|
document.getElementById('modal-title').textContent = 'All Work Items (' + allWorkItems.length + ')';
|
|
1056
|
-
const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>
|
|
1064
|
+
const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>' +
|
|
1057
1065
|
allWorkItems.map(wiRow).join('') + '</tbody></table></div>';
|
|
1058
1066
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() by wiRow() (fields: work item id/title/source/status/agent/PR links/dates/follow-up metadata)
|
|
1059
1067
|
document.getElementById('modal-body').innerHTML = html;
|
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; }
|