@yemi33/minions 0.1.1583 → 0.1.1585

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 CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1583 (2026-04-28)
3
+ ## 0.1.1585 (2026-04-28)
4
+
5
+ ### Fixes
6
+ - optimistic UI for planApprove cascades to linked PRD
7
+ - stack chain-of-thought above progress block (was side-by-side)
4
8
 
5
9
  ### Other
6
10
  - Keep doc chat collapsed while streaming
@@ -27,8 +27,8 @@ let _qaProcessing = false; // true while waiting for response
27
27
  let _qaAbortController = null;
28
28
  let _qaQueue = []; // queued messages while processing
29
29
  const QA_QUEUE_CAP = 10; // max queued messages
30
+ const QA_STREAM_STALL_MS = 90000; // abort doc-chat if heartbeats continue but no chunk/tool progress arrives
30
31
  let _qaSessionKey = ''; // key for current conversation (title or filePath)
31
- let _qaThreadCollapsed = false; // sticky while streaming so collapse doesn't bounce open on updates
32
32
 
33
33
  function _renderQaUserMessage(thread, message, selection) {
34
34
  let qHtml = '<div class="modal-qa-q">' + escHtml(message);
@@ -38,7 +38,7 @@ function _renderQaUserMessage(thread, message, selection) {
38
38
  qHtml += '</div>';
39
39
  thread.insertAdjacentHTML('beforeend', qHtml);
40
40
  thread.scrollTop = thread.scrollHeight;
41
- _showThreadWrap(true);
41
+ _showThreadWrap();
42
42
  }
43
43
  const _qaSessions = new Map(); // persist conversations across modal open/close {key → {history, threadHtml}}
44
44
  const _qaRuntime = new Map(); // key → {history, processing, abortController, queue}
@@ -199,28 +199,29 @@ function _qaBuildAssistantHtml(text, opts) {
199
199
 
200
200
  function _qaBuildLiveProgressHtml(loadingId, label, elapsedSeconds, streamedText, toolsUsed, queueCount) {
201
201
  const qaQueueBadge = queueCount > 0 ? ' <span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + queueCount + ' queued</span>' : '';
202
- let html = '';
202
+ // Wrap in a column-flex container so chain-of-thought (tool calls) stack
203
+ // vertically on top and the progress block sits at the bottom. Overrides the
204
+ // parent .modal-qa-loading row-flex (which is right for the simple
205
+ // "Thinking..." initial state but wrong once tools/streamed text appear).
206
+ let html = '<div style="display:flex;flex-direction:column;align-items:stretch;gap:6px;width:100%">';
203
207
  if (toolsUsed && toolsUsed.length > 0) {
204
- html += '<div style="margin-bottom:6px">';
208
+ html += '<div style="display:flex;flex-direction:column;gap:2px">';
205
209
  toolsUsed.forEach(function(t) {
206
210
  const name = typeof t === 'string' ? t : t.name;
207
211
  const input = typeof t === 'string' ? {} : (t.input || {});
208
- html += '<div style="color:var(--muted);font-size:10px;font-family:monospace"><span style="flex-shrink:0">&#9679;</span> ' + formatToolSummary(name, input) + '</div>';
212
+ html += '<div style="color:var(--muted);font-size:10px;font-family:monospace;display:flex;align-items:flex-start;gap:6px"><span style="flex-shrink:0">&#9679;</span><span style="word-break:break-all">' + formatToolSummary(name, input) + '</span></div>';
209
213
  });
210
214
  html += '</div>';
211
215
  }
212
- if (streamedText) html += '<div style="margin-bottom:6px">' + renderMd(streamedText) + '</div>';
213
- html += '<div style="display:flex;flex-direction:column;align-items:flex-start;gap:6px">' +
214
- '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">' +
215
- '<span class="dot-pulse"><span></span><span></span><span></span></span> ' +
216
- '<span id="' + loadingId + '-text">' + escHtml(label) + '</span>' +
217
- '</div>' +
218
- '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">' +
219
- '<span id="' + loadingId + '-time" style="font-size:10px;color:var(--muted)">' + elapsedSeconds + 's</span>' +
220
- '<button onclick="qaAbort()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Stop</button>' +
221
- qaQueueBadge +
222
- '</div>' +
223
- '</div>';
216
+ if (streamedText) html += '<div>' + renderMd(streamedText) + '</div>';
217
+ html += '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
218
+ '<span class="dot-pulse"><span></span><span></span><span></span></span>' +
219
+ '<span id="' + loadingId + '-text">' + escHtml(label) + '</span>' +
220
+ '<span id="' + loadingId + '-time" style="font-size:10px;color:var(--muted)">' + elapsedSeconds + 's</span>' +
221
+ '<button onclick="qaAbort()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Stop</button>' +
222
+ qaQueueBadge +
223
+ '</div>';
224
+ html += '</div>';
224
225
  return html;
225
226
  }
226
227
 
@@ -291,7 +292,6 @@ function _initQaSession() {
291
292
  if (!key || _qaSessionKey === key) return;
292
293
  if (_qaSessionKey && _qaSessionKey !== key) _qaSaveActiveSessionState();
293
294
  _qaSessionKey = key;
294
- _qaThreadCollapsed = false;
295
295
  const card = findCardForFile(_modalFilePath);
296
296
  if (card) clearNotifBadge(card);
297
297
  var prior = _qaSessions.get(key);
@@ -307,7 +307,7 @@ function _initQaSession() {
307
307
  });
308
308
  }
309
309
  if (prior.filePath) _modalFilePath = prior.filePath;
310
- _showThreadWrap(true);
310
+ _showThreadWrap();
311
311
  requestAnimationFrame(function() {
312
312
  var thread = document.getElementById('modal-qa-thread');
313
313
  if (thread) thread.scrollTop = thread.scrollHeight;
@@ -333,7 +333,6 @@ function clearQaConversation() {
333
333
  _qaQueue = [];
334
334
  _qaProcessing = false;
335
335
  _qaAbortController = null;
336
- _qaThreadCollapsed = false;
337
336
  document.getElementById('modal-qa-thread').innerHTML = '';
338
337
  var wrap = document.getElementById('modal-qa-thread-wrap');
339
338
  var expandBar = document.getElementById('qa-expand-bar');
@@ -433,6 +432,21 @@ async function _processQaMessage(message, selection, opts) {
433
432
  : [[0,'Thinking...'],[3000,'Reading document...'],[8000,'Analyzing...'],[20000,'Still working...'],[60000,'Taking a while...']];
434
433
  let streamedText = '';
435
434
  let toolsUsed = [];
435
+ let _qaStreamStalled = false;
436
+ let _qaStallTimer = null;
437
+ function _clearQaStreamWatchdog() {
438
+ if (_qaStallTimer) {
439
+ clearTimeout(_qaStallTimer);
440
+ _qaStallTimer = null;
441
+ }
442
+ }
443
+ function _resetQaStreamWatchdog() {
444
+ _clearQaStreamWatchdog();
445
+ _qaStallTimer = setTimeout(() => {
446
+ _qaStreamStalled = true;
447
+ try { abortController.abort(); } catch {}
448
+ }, QA_STREAM_STALL_MS);
449
+ }
436
450
  function _qaProgressLabel(elapsed) {
437
451
  let label = qaPhases[0][1];
438
452
  for (let i = qaPhases.length - 1; i >= 0; i--) {
@@ -471,6 +485,7 @@ async function _processQaMessage(message, selection, opts) {
471
485
  _qaRenderProgress(false);
472
486
  }, 500);
473
487
  _qaRenderProgress(false);
488
+ _resetQaStreamWatchdog();
474
489
 
475
490
  try {
476
491
  const res = await fetch('/api/doc-chat/stream', {
@@ -507,11 +522,13 @@ async function _processQaMessage(message, selection, opts) {
507
522
  if (!evt || !evt.type) return;
508
523
  if (evt.type === 'heartbeat') return;
509
524
  if (evt.type === 'chunk') {
525
+ _resetQaStreamWatchdog();
510
526
  streamedText = evt.text || '';
511
527
  _qaRenderProgress(true);
512
528
  return;
513
529
  }
514
530
  if (evt.type === 'tool') {
531
+ _resetQaStreamWatchdog();
515
532
  toolsUsed.push({ name: evt.name, input: evt.input || {} });
516
533
  _qaRenderProgress(true);
517
534
  return;
@@ -519,6 +536,7 @@ async function _processQaMessage(message, selection, opts) {
519
536
  if (evt.type === 'done') {
520
537
  terminalEventSeen = true;
521
538
  clearInterval(qaTimer);
539
+ _clearQaStreamWatchdog();
522
540
  const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
523
541
  const borderColor = evt.edited ? 'var(--green)' : 'var(--blue)';
524
542
  const suffix = evt.edited ? '\n\n\u2713 Document saved.' : '';
@@ -565,7 +583,10 @@ async function _processQaMessage(message, selection, opts) {
565
583
  });
566
584
  return;
567
585
  }
568
- if (evt.type === 'error') throw new Error(evt.error || 'Failed');
586
+ if (evt.type === 'error') {
587
+ _clearQaStreamWatchdog();
588
+ throw new Error(evt.error || 'Failed');
589
+ }
569
590
  }
570
591
 
571
592
  while (true) {
@@ -591,8 +612,14 @@ async function _processQaMessage(message, selection, opts) {
591
612
  if (!terminalEventSeen) throw new Error('The response stream ended before completion.');
592
613
  } catch (e) {
593
614
  clearInterval(qaTimer);
615
+ _clearQaStreamWatchdog();
594
616
  const qaElapsedExc = Math.round((Date.now() - qaStartTime) / 1000);
595
- const messageHtml = e.name === 'AbortError'
617
+ const stallMessage = 'Doc chat stalled with no tool or text progress for 90s.';
618
+ const messageHtml = _qaStreamStalled
619
+ ? (streamedText
620
+ ? _qaBuildAssistantHtml(streamedText + '\n\nError: ' + stallMessage, { borderColor: 'var(--red)', elapsed: qaElapsedExc })
621
+ : _qaBuildAssistantHtml('Error: ' + stallMessage, { color: 'var(--red)', isError: true, elapsed: qaElapsedExc }))
622
+ : e.name === 'AbortError'
596
623
  ? (streamedText
597
624
  ? _qaBuildAssistantHtml(streamedText + '\n\n_Stopped._', { borderColor: 'var(--muted)', elapsed: qaElapsedExc })
598
625
  : _qaBuildAssistantHtml('Stopped', { color: 'var(--muted)', isError: true, elapsed: qaElapsedExc }))
@@ -674,18 +701,15 @@ function toggleDocChat() {
674
701
  var expandBar = document.getElementById('qa-expand-bar');
675
702
  if (!wrap) return;
676
703
  var visible = wrap.style.display !== 'none';
677
- var nextVisible = !visible;
678
- _qaThreadCollapsed = !nextVisible;
679
- wrap.style.display = nextVisible ? '' : 'none';
680
- if (expandBar) expandBar.style.display = nextVisible ? 'none' : '';
704
+ wrap.style.display = visible ? 'none' : '';
705
+ if (expandBar) expandBar.style.display = visible ? '' : 'none';
681
706
  }
682
707
 
683
- function _showThreadWrap(forceExpand) {
708
+ function _showThreadWrap() {
684
709
  var wrap = document.getElementById('modal-qa-thread-wrap');
685
710
  var expandBar = document.getElementById('qa-expand-bar');
686
- if (forceExpand) _qaThreadCollapsed = false;
687
- if (wrap) wrap.style.display = _qaThreadCollapsed ? 'none' : '';
688
- if (expandBar) expandBar.style.display = _qaThreadCollapsed ? '' : 'none';
711
+ if (wrap) wrap.style.display = '';
712
+ if (expandBar) expandBar.style.display = 'none';
689
713
  }
690
714
 
691
715
  // ── Drag-to-resize doc chat thread ──────────────────────────────────────────
@@ -559,6 +559,32 @@ async function planView(file) {
559
559
  async function planApprove(file, btn) {
560
560
  if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Approving...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
561
561
  showToast('cmd-toast', 'Plan approved — work will begin on next engine tick', true);
562
+
563
+ // Optimistic UI: mutate cached state and re-render so both the plan card and
564
+ // PRD view reflect 'approved' immediately. The .md card's display status
565
+ // follows the linked PRD's status via lookup in renderPlanCard, so updating
566
+ // the PRD entry covers both. Refresh after fetch reconciles with truth.
567
+ const isPrd = typeof file === 'string' && file.endsWith('.json');
568
+ if (Array.isArray(window._lastPlans)) {
569
+ for (const p of window._lastPlans) {
570
+ if (p.file === file) {
571
+ p.status = 'approved';
572
+ p.planStale = false;
573
+ delete p.pausedAt;
574
+ }
575
+ }
576
+ try { renderPlans(window._lastPlans); } catch { /* render is best-effort */ }
577
+ }
578
+ if (isPrd && window._lastStatus?.prdProgress?.items) {
579
+ for (const item of window._lastStatus.prdProgress.items) {
580
+ if (item.source === file) {
581
+ item.planStatus = 'approved';
582
+ item.planStale = false;
583
+ }
584
+ }
585
+ if (typeof rerenderPrdFromCache === 'function') rerenderPrdFromCache();
586
+ }
587
+
562
588
  try {
563
589
  const res = await fetch('/api/plans/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
564
590
  if (res.ok) {
@@ -568,8 +594,14 @@ async function planApprove(file, btn) {
568
594
  if (btn) { btn.textContent = btn.dataset.origText || 'Approve'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
569
595
  const d = await res.json().catch(() => ({}));
570
596
  showToast('cmd-toast', 'Approve failed: ' + (d.error || 'unknown'), false);
597
+ // Refresh to revert optimistic state from server truth
598
+ refresh();
571
599
  }
572
- } catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Approve'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } showToast('cmd-toast', 'Error: ' + e.message, false); }
600
+ } catch (e) {
601
+ if (btn) { btn.textContent = btn.dataset.origText || 'Approve'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
602
+ showToast('cmd-toast', 'Error: ' + e.message, false);
603
+ refresh();
604
+ }
573
605
  }
574
606
 
575
607
  async function planDelete(file) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1583",
3
+ "version": "0.1.1585",
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"