@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.
@@ -147,7 +147,7 @@ const RENDER_VERSIONS = {
147
147
  dispatch: 2,
148
148
  engineLog: 2,
149
149
  metrics: 1,
150
- workItems: 4,
150
+ workItems: 5,
151
151
  skills: 1,
152
152
  commands: 1,
153
153
  mcpServers: 1,
@@ -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:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + followupChip + '</td>' +
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
- (item.failReason ? '<span style="display:block;font-size:var(--text-xs);color:var(--red)" title="' + escapeHtml(item.failReason) + '">' + escapeHtml(item.failReason.slice(0, 30)) + '</span>' : '') +
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>Source</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>';
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:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(i.title || '') + '</td>' +
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('Source', escapeHtml(item._source || 'central'));
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>Source</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>' +
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;
@@ -112,6 +112,11 @@
112
112
  <div class="cockpit-value dim">0</div>
113
113
  <div class="cockpit-detail">pinned context, notes &amp; 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 &amp; 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">&times;</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
- var sending = false;
31
- var abortController = null;
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 _deriveTitle() {
133
- for (var i = 0; i < messages.length; i++) {
134
- if (messages[i] && messages[i].role === 'user' && messages[i].text) {
135
- return messages[i].text.slice(0, 40);
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, '&amp;')
@@ -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
- var toolList = [];
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
- function setSending(on) {
493
- sending = on;
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
- // Switch the visible pane to another shared tab. Aborts an in-flight stream
565
- // first (the SSE is bound to the current tabId), then re-hydrates from the
566
- // target tab's persisted state.
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
- if (sending) abortInFlight();
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
- if (id === tabId && sending) abortInFlight();
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; }