@yemi33/minions 0.1.1026 → 0.1.1027

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,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1026 (2026-04-16)
3
+ ## 0.1.1027 (2026-04-16)
4
4
 
5
5
  ### Features
6
6
  - route implement items to dedicated implement playbook (#1115)
@@ -25,6 +25,7 @@
25
25
  - harden audited state transitions
26
26
 
27
27
  ### Other
28
+ - Fix doc chat session isolation
28
29
  - test(pipeline): add unit tests for CRUD, stage execution, run lifecycle (#1162)
29
30
  - chore: test publish after removing stale status check
30
31
  - chore: trigger publish test
@@ -40,6 +40,7 @@ function _renderQaUserMessage(thread, message, selection) {
40
40
  _showThreadWrap();
41
41
  }
42
42
  const _qaSessions = new Map(); // persist conversations across modal open/close {key → {history, threadHtml}}
43
+ const _qaRuntime = new Map(); // key → {history, processing, abortController, queue}
43
44
  // Restore from localStorage
44
45
  try {
45
46
  const saved = JSON.parse(localStorage.getItem('qa-sessions') || '{}');
@@ -55,6 +56,175 @@ function _saveQaSessions() {
55
56
  } catch { /* localStorage might be full */ }
56
57
  }
57
58
 
59
+ function _qaCloneQueue(queue) {
60
+ return Array.isArray(queue) ? queue.map(item => ({ ...item })) : [];
61
+ }
62
+
63
+ function _qaGetRuntime(key) {
64
+ if (!key) return null;
65
+ let runtime = _qaRuntime.get(key);
66
+ if (!runtime) {
67
+ const prior = _qaSessions.get(key);
68
+ runtime = {
69
+ history: Array.isArray(prior?.history) ? prior.history.slice() : [],
70
+ processing: false,
71
+ abortController: null,
72
+ queue: _qaCloneQueue(prior?.queue),
73
+ };
74
+ _qaRuntime.set(key, runtime);
75
+ }
76
+ return runtime;
77
+ }
78
+
79
+ function _qaThreadEl() {
80
+ return document.getElementById('modal-qa-thread');
81
+ }
82
+
83
+ function _qaThreadHtml() {
84
+ return (_qaThreadEl() || {}).innerHTML || '';
85
+ }
86
+
87
+ function _qaIsActiveSession(key) {
88
+ return !!key && _qaSessionKey === key && document.getElementById('modal')?.classList?.contains('open');
89
+ }
90
+
91
+ function _qaSyncActiveRuntime() {
92
+ if (!_qaSessionKey) return;
93
+ const runtime = _qaGetRuntime(_qaSessionKey);
94
+ if (!runtime) return;
95
+ runtime.history = _qaHistory.slice();
96
+ runtime.processing = _qaProcessing;
97
+ runtime.abortController = _qaAbortController || null;
98
+ runtime.queue = _qaCloneQueue(_qaQueue);
99
+ }
100
+
101
+ function _qaPersistSession(key, { threadHtml, docContext, filePath, history, queue } = {}) {
102
+ if (!key) return;
103
+ const runtime = _qaGetRuntime(key);
104
+ const prior = _qaSessions.get(key) || {};
105
+ const persistedHistory = Array.isArray(history)
106
+ ? history.slice()
107
+ : Array.isArray(runtime?.history)
108
+ ? runtime.history.slice()
109
+ : Array.isArray(prior.history)
110
+ ? prior.history.slice()
111
+ : [];
112
+ _qaSessions.set(key, {
113
+ history: persistedHistory,
114
+ threadHtml: threadHtml != null ? threadHtml : (prior.threadHtml || ''),
115
+ docContext: docContext ? { ...docContext } : (prior.docContext ? { ...prior.docContext } : { title: '', content: '', selection: '' }),
116
+ filePath: filePath !== undefined ? filePath : prior.filePath,
117
+ queue: Array.isArray(queue) ? _qaCloneQueue(queue) : _qaCloneQueue(runtime?.queue),
118
+ });
119
+ _saveQaSessions();
120
+ }
121
+
122
+ function _qaSaveActiveSessionState() {
123
+ if (!_qaSessionKey) return;
124
+ _qaSyncActiveRuntime();
125
+ _qaPersistSession(_qaSessionKey, {
126
+ threadHtml: _qaThreadHtml(),
127
+ docContext: { ..._modalDocContext },
128
+ filePath: _modalFilePath,
129
+ history: _qaHistory,
130
+ queue: _qaQueue,
131
+ });
132
+ }
133
+
134
+ function _qaLoadSessionState(key) {
135
+ const prior = _qaSessions.get(key);
136
+ const runtime = _qaGetRuntime(key);
137
+ _qaHistory = Array.isArray(runtime?.history) && runtime.history.length
138
+ ? runtime.history.slice()
139
+ : Array.isArray(prior?.history)
140
+ ? prior.history.slice()
141
+ : [];
142
+ _qaProcessing = !!runtime?.processing;
143
+ _qaAbortController = runtime?.abortController || null;
144
+ _qaQueue = _qaCloneQueue(runtime?.queue);
145
+ const thread = _qaThreadEl();
146
+ if (thread) {
147
+ const tmp = document.createElement('div');
148
+ tmp.innerHTML = prior?.threadHtml || '';
149
+ if (!_qaProcessing) tmp.querySelectorAll('.modal-qa-loading').forEach(el => el.remove());
150
+ thread.innerHTML = tmp.innerHTML;
151
+ }
152
+ }
153
+
154
+ function _qaResetActiveState() {
155
+ _qaHistory = [];
156
+ _qaProcessing = false;
157
+ _qaAbortController = null;
158
+ _qaQueue = [];
159
+ _qaSessionKey = '';
160
+ }
161
+
162
+ function _qaBuildUserMessageHtml(message, selection) {
163
+ let qHtml = '<div class="modal-qa-q">' + escHtml(message);
164
+ if (selection) {
165
+ qHtml += '<span class="selection-ref">Re: "' + escHtml(selection.slice(0, 100)) + ((selection.length > 100) ? '...' : '') + '"</span>';
166
+ }
167
+ qHtml += '</div>';
168
+ return qHtml;
169
+ }
170
+
171
+ function _qaBuildQueuedHtml(message) {
172
+ const preview = escHtml(message.length > 60 ? message.slice(0, 57) + '...' : message);
173
+ return '<div class="qa-queued-item" style="color:var(--muted);font-size:10px;padding:4px 8px">Queued: "' + preview + '"</div>';
174
+ }
175
+
176
+ function _qaBuildLoadingHtml(loadingId, queueCount) {
177
+ 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>' : '';
178
+ return '<div class="modal-qa-loading" id="' + loadingId + '">' +
179
+ '<div class="dot-pulse"><span></span><span></span><span></span></div> ' +
180
+ '<span id="' + loadingId + '-text">Thinking...</span> ' +
181
+ '<span id="' + loadingId + '-time" style="font-size:10px;color:var(--muted)"></span>' +
182
+ ' <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>' +
183
+ qaQueueBadge + '</div>';
184
+ }
185
+
186
+ function _qaBuildAssistantHtml(text, opts) {
187
+ const body = opts?.isError ? escHtml(text) : renderMd(text);
188
+ const style = opts?.isError
189
+ ? 'color:' + (opts?.color || 'var(--red)')
190
+ : 'border-left-color:' + (opts?.borderColor || 'var(--blue)');
191
+ const pad = opts?.isError ? '' : 'padding-right:24px;';
192
+ return '<div class="modal-qa-a" style="' + style + '">' +
193
+ (opts?.isError ? '' : llmCopyBtn()) +
194
+ body +
195
+ '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right;' + pad + '">' + opts.elapsed + 's</div>' +
196
+ '</div>';
197
+ }
198
+
199
+ function _qaMutateThreadHtml(key, mutate) {
200
+ const tmp = document.createElement('div');
201
+ tmp.innerHTML = _qaIsActiveSession(key) ? _qaThreadHtml() : ((_qaSessions.get(key) || {}).threadHtml || '');
202
+ mutate(tmp);
203
+ const html = tmp.innerHTML;
204
+ if (_qaIsActiveSession(key)) {
205
+ const thread = _qaThreadEl();
206
+ if (thread) {
207
+ thread.innerHTML = html;
208
+ thread.scrollTop = thread.scrollHeight;
209
+ }
210
+ _showThreadWrap();
211
+ }
212
+ return html;
213
+ }
214
+
215
+ function _qaResumeQueuedMessages() {
216
+ if (!_qaSessionKey || _qaProcessing || _qaQueue.length === 0) return;
217
+ const next = _qaQueue.shift();
218
+ const thread = _qaThreadEl();
219
+ if (thread) {
220
+ const queuedEl = thread.querySelector('.qa-queued-item');
221
+ if (queuedEl) queuedEl.remove();
222
+ _renderQaUserMessage(thread, next.message, next.selection);
223
+ }
224
+ _qaSaveActiveSessionState();
225
+ _processQaMessage(next.message, next.selection);
226
+ }
227
+
58
228
  function modalAskAboutSelection() {
59
229
  document.getElementById('ask-selection-btn').style.display = 'none';
60
230
 
@@ -91,16 +261,14 @@ function clearQaSelection() {
91
261
  function _initQaSession() {
92
262
  var key = _modalFilePath || _modalDocContext.title || '';
93
263
  if (!key || _qaSessionKey === key) return;
264
+ if (_qaSessionKey && _qaSessionKey !== key) _qaSaveActiveSessionState();
94
265
  _qaSessionKey = key;
95
- // Clear notification badge on the source card when reopening
96
266
  const card = findCardForFile(_modalFilePath);
97
267
  if (card) clearNotifBadge(card);
98
268
  var prior = _qaSessions.get(key);
269
+ _qaLoadSessionState(key);
99
270
  if (prior) {
100
- _qaHistory = prior.history;
101
- document.getElementById('modal-qa-thread').innerHTML = prior.threadHtml;
102
271
  if (prior.docContext) {
103
- // Preserve freshly-fetched content and title — prior session may have stale/empty content
104
272
  const freshContent = _modalDocContext.content;
105
273
  const freshTitle = _modalDocContext.title;
106
274
  _modalDocContext = Object.assign({}, prior.docContext, {
@@ -111,11 +279,13 @@ function _initQaSession() {
111
279
  }
112
280
  if (prior.filePath) _modalFilePath = prior.filePath;
113
281
  _showThreadWrap();
114
- // Defer scroll — container just transitioned from display:none, layout not yet computed
115
282
  requestAnimationFrame(function() {
116
283
  var thread = document.getElementById('modal-qa-thread');
117
284
  if (thread) thread.scrollTop = thread.scrollHeight;
118
285
  });
286
+ if (_qaQueue.length > 0 && !_qaProcessing) {
287
+ setTimeout(_qaResumeQueuedMessages, 0);
288
+ }
119
289
  } else {
120
290
  _qaHistory = [];
121
291
  document.getElementById('modal-qa-thread').innerHTML = '';
@@ -127,9 +297,13 @@ function _initQaSession() {
127
297
  }
128
298
 
129
299
  function clearQaConversation() {
300
+ const runtime = _qaGetRuntime(_qaSessionKey);
301
+ if (_qaAbortController) _qaAbortController.abort();
302
+ if (runtime?.abortController && runtime.abortController !== _qaAbortController) runtime.abortController.abort();
130
303
  _qaHistory = [];
131
304
  _qaQueue = [];
132
305
  _qaProcessing = false;
306
+ _qaAbortController = null;
133
307
  document.getElementById('modal-qa-thread').innerHTML = '';
134
308
  var wrap = document.getElementById('modal-qa-thread-wrap');
135
309
  var expandBar = document.getElementById('qa-expand-bar');
@@ -137,6 +311,7 @@ function clearQaConversation() {
137
311
  if (expandBar) expandBar.style.display = 'none';
138
312
  if (_qaSessionKey) {
139
313
  _qaSessions.delete(_qaSessionKey);
314
+ _qaRuntime.set(_qaSessionKey, { history: [], processing: false, abortController: null, queue: [] });
140
315
  _saveQaSessions();
141
316
  }
142
317
  }
@@ -164,7 +339,6 @@ function modalSend() {
164
339
  var thread = document.getElementById('modal-qa-thread');
165
340
  const selection = _modalDocContext.selection || '';
166
341
 
167
- // Clear input immediately so user can type next message
168
342
  input.value = '';
169
343
  _modalDocContext.selection = '';
170
344
  document.getElementById('modal-qa-pill').style.display = 'none';
@@ -174,63 +348,79 @@ function modalSend() {
174
348
  showToast('cmd-toast', 'Queue full — wait for current response', false);
175
349
  return;
176
350
  }
177
- // Queue the message — show only grey queued indicator (blue bubble shows when processing starts)
178
351
  _qaQueue.push({ message, selection });
179
- const preview = escHtml(message.length > 60 ? message.slice(0, 57) + '...' : message);
180
- thread.insertAdjacentHTML('beforeend', '<div class="qa-queued-item" style="color:var(--muted);font-size:10px;padding:4px 8px">Queued: "' + preview + '"</div>');
352
+ thread.insertAdjacentHTML('beforeend', _qaBuildQueuedHtml(message));
181
353
  thread.scrollTop = thread.scrollHeight;
182
354
  _showThreadWrap();
355
+ _qaSaveActiveSessionState();
183
356
  return;
184
357
  }
185
358
 
186
- // Show message in thread when processing starts (not when queued)
187
359
  _renderQaUserMessage(thread, message, selection);
188
-
360
+ _qaSaveActiveSessionState();
189
361
  _processQaMessage(message, selection);
190
362
  }
191
363
 
192
- async function _processQaMessage(message, selection) {
193
- const thread = document.getElementById('modal-qa-thread');
194
- const btn = document.getElementById('modal-send-btn');
195
- _qaProcessing = true;
196
-
197
- // Capture state now — closeModal may null these while we're awaiting
198
- const capturedFilePath = _modalFilePath;
199
- const capturedDocContext = { ..._modalDocContext };
364
+ async function _processQaMessage(message, selection, opts) {
365
+ const sessionKey = opts?.sessionKey || _qaSessionKey;
366
+ if (!sessionKey) return;
367
+ const runtime = _qaGetRuntime(sessionKey);
368
+ if (!runtime) return;
369
+
370
+ const prior = _qaSessions.get(sessionKey);
371
+ const capturedFilePath = opts?.filePath !== undefined ? opts.filePath : (prior?.filePath || _modalFilePath);
372
+ const capturedDocContext = Object.assign({}, prior?.docContext || {}, opts?.docContext || (_qaIsActiveSession(sessionKey) ? _modalDocContext : {}));
373
+ const isActiveSession = _qaIsActiveSession(sessionKey);
374
+
375
+ runtime.processing = true;
376
+ const abortController = new AbortController();
377
+ runtime.abortController = abortController;
378
+ if (isActiveSession) {
379
+ _qaProcessing = true;
380
+ _qaAbortController = abortController;
381
+ }
200
382
 
201
- // Show processing badge on the source card
202
383
  const sourceCard = findCardForFile(capturedFilePath);
203
384
  if (sourceCard) showNotifBadge(sourceCard, 'processing');
204
385
 
205
386
  const loadingId = 'chat-loading-' + Date.now();
206
- const qaQueueBadge = _qaQueue.length > 0 ? ' <span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + _qaQueue.length + ' queued</span>' : '';
207
- _qaAbortController = new AbortController();
208
- thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-loading" id="' + loadingId + '">' +
209
- '<div class="dot-pulse"><span></span><span></span><span></span></div> ' +
210
- '<span id="' + loadingId + '-text">Thinking...</span> ' +
211
- '<span id="' + loadingId + '-time" style="font-size:10px;color:var(--muted)"></span>' +
212
- ' <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>' +
213
- qaQueueBadge + '</div>');
214
- thread.scrollTop = thread.scrollHeight;
387
+ const loadingHtml = _qaBuildLoadingHtml(loadingId, runtime.queue.length);
388
+ const startThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
389
+ tmp.insertAdjacentHTML('beforeend', loadingHtml);
390
+ });
391
+ _qaPersistSession(sessionKey, {
392
+ threadHtml: startThreadHtml,
393
+ docContext: capturedDocContext,
394
+ filePath: capturedFilePath,
395
+ history: runtime.history,
396
+ queue: runtime.queue,
397
+ });
215
398
 
216
- const isPlanEdit = _modalFilePath && _modalFilePath.match(/^plans\/.*\.md$/);
399
+ const isPlanEdit = capturedFilePath && capturedFilePath.match(/^plans\/.*\.md$/);
217
400
  const qaStartTime = Date.now();
218
401
  const qaPhases = isPlanEdit
219
402
  ? [[0,'Reading plan...'],[3000,'Analyzing structure...'],[8000,'Researching context...'],[15000,'Drafting revisions...'],[30000,'Writing updated plan...'],[60000,'Still working (large document)...'],[120000,'Deep edit in progress...'],[300000,'Almost there...']]
220
403
  : [[0,'Thinking...'],[3000,'Reading document...'],[8000,'Analyzing...'],[20000,'Still working...'],[60000,'Taking a while...']];
221
404
  const qaTimer = setInterval(() => {
222
405
  const elapsed = Date.now() - qaStartTime;
223
- const timeEl = document.getElementById(loadingId + '-time');
224
- const textEl = document.getElementById(loadingId + '-text');
406
+ const timeEl = _qaIsActiveSession(sessionKey) ? document.getElementById(loadingId + '-time') : null;
407
+ const textEl = _qaIsActiveSession(sessionKey) ? document.getElementById(loadingId + '-text') : null;
225
408
  if (timeEl) timeEl.textContent = Math.floor(elapsed / 1000) + 's';
226
- if (textEl) { for (let i = qaPhases.length - 1; i >= 0; i--) { if (elapsed >= qaPhases[i][0]) { textEl.textContent = qaPhases[i][1]; break; } } }
409
+ if (textEl) {
410
+ for (let i = qaPhases.length - 1; i >= 0; i--) {
411
+ if (elapsed >= qaPhases[i][0]) {
412
+ textEl.textContent = qaPhases[i][1];
413
+ break;
414
+ }
415
+ }
416
+ }
227
417
  }, 500);
228
418
 
229
419
  try {
230
420
  const res = await fetch('/api/doc-chat', {
231
421
  method: 'POST',
232
422
  headers: { 'Content-Type': 'application/json' },
233
- signal: _qaAbortController ? _qaAbortController.signal : undefined,
423
+ signal: abortController.signal,
234
424
  body: JSON.stringify({
235
425
  message,
236
426
  document: capturedDocContext.content,
@@ -242,109 +432,141 @@ async function _processQaMessage(message, selection) {
242
432
  });
243
433
  const data = await res.json();
244
434
  clearInterval(qaTimer);
245
- const loadingEl = document.getElementById(loadingId);
246
- if (loadingEl) loadingEl.remove();
435
+ const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
436
+ let sessionDocContext = { ...capturedDocContext };
247
437
 
248
438
  if (data.ok) {
249
439
  const borderColor = data.edited ? 'var(--green)' : 'var(--blue)';
250
440
  const suffix = data.edited ? '\n\n\u2713 Document saved.' : '';
251
- const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
252
- const qaTimeLabel = '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right;padding-right:24px">' + qaElapsed + 's</div>';
253
- thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="border-left-color:' + borderColor + '">' + llmCopyBtn() + renderMd(data.answer + suffix) + qaTimeLabel + '</div>');
441
+ const answerHtml = _qaBuildAssistantHtml(data.answer + suffix, { borderColor, elapsed: qaElapsed });
442
+ const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
443
+ const loadingEl = tmp.querySelector('#' + loadingId);
444
+ if (loadingEl) loadingEl.remove();
445
+ tmp.insertAdjacentHTML('beforeend', answerHtml);
446
+ });
254
447
 
255
- // Track conversation history
256
- _qaHistory.push({ role: 'user', text: message });
257
- _qaHistory.push({ role: 'assistant', text: data.answer });
448
+ runtime.history.push({ role: 'user', text: message });
449
+ runtime.history.push({ role: 'assistant', text: data.answer });
450
+ if (_qaIsActiveSession(sessionKey)) _qaHistory = runtime.history.slice();
258
451
 
259
- // Notify sidebar page link
260
452
  _qaNotifySidebar(capturedFilePath);
261
-
262
- // Execute any CC actions (dispatch, note, etc.)
263
453
  if (data.actions && data.actions.length > 0) {
264
- for (const action of data.actions) { await ccExecuteAction(action); }
454
+ for (const action of data.actions) await ccExecuteAction(action);
265
455
  }
266
456
 
267
- // Refresh modal body if document was edited
268
457
  if (data.edited && data.content) {
269
458
  const display = data.content.replace(/^---[\s\S]*?---\n*/m, '');
270
459
  const isJson = capturedFilePath && capturedFilePath.endsWith('.json');
271
- const body = document.getElementById('modal-body');
272
- if (isJson) {
273
- body.textContent = display;
274
- } else {
275
- body.innerHTML = renderMd(display);
276
- body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
277
- body.style.whiteSpace = 'normal';
460
+ sessionDocContext.content = display;
461
+ sessionDocContext.selection = '';
462
+ if (_qaIsActiveSession(sessionKey)) {
463
+ const body = document.getElementById('modal-body');
464
+ if (isJson) {
465
+ body.textContent = display;
466
+ } else {
467
+ body.innerHTML = renderMd(display);
468
+ body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
469
+ body.style.whiteSpace = 'normal';
470
+ }
471
+ _modalDocContext.content = display;
278
472
  }
279
- _modalDocContext.content = display;
280
473
  }
474
+
475
+ _qaPersistSession(sessionKey, {
476
+ threadHtml: updatedThreadHtml,
477
+ docContext: sessionDocContext,
478
+ filePath: capturedFilePath,
479
+ history: runtime.history,
480
+ queue: runtime.queue,
481
+ });
281
482
  } else {
282
- const qaElapsedErr = Math.round((Date.now() - qaStartTime) / 1000);
283
- thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(data.error || 'Failed') + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsedErr + 's</div></div>');
483
+ const errorHtml = _qaBuildAssistantHtml('Error: ' + (data.error || 'Failed'), { color: 'var(--red)', isError: true, elapsed: qaElapsed });
484
+ const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
485
+ const loadingEl = tmp.querySelector('#' + loadingId);
486
+ if (loadingEl) loadingEl.remove();
487
+ tmp.insertAdjacentHTML('beforeend', errorHtml);
488
+ });
489
+ _qaPersistSession(sessionKey, {
490
+ threadHtml: updatedThreadHtml,
491
+ docContext: sessionDocContext,
492
+ filePath: capturedFilePath,
493
+ history: runtime.history,
494
+ queue: runtime.queue,
495
+ });
284
496
  }
285
497
  } catch (e) {
286
498
  clearInterval(qaTimer);
287
- const loadingEl = document.getElementById(loadingId);
288
- if (loadingEl) loadingEl.remove();
289
499
  const qaElapsedExc = Math.round((Date.now() - qaStartTime) / 1000);
290
- if (e.name === 'AbortError') {
291
- thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="color:var(--muted)">Stopped<div style="font-size:9px;margin-top:4px;text-align:right">' + qaElapsedExc + 's</div></div>');
292
- } else {
293
- thread.insertAdjacentHTML('beforeend', '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(e.message) + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsedExc + 's</div></div>');
294
- }
500
+ const messageHtml = e.name === 'AbortError'
501
+ ? _qaBuildAssistantHtml('Stopped', { color: 'var(--muted)', isError: true, elapsed: qaElapsedExc })
502
+ : _qaBuildAssistantHtml('Error: ' + e.message, { color: 'var(--red)', isError: true, elapsed: qaElapsedExc });
503
+ const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
504
+ const loadingEl = tmp.querySelector('#' + loadingId);
505
+ if (loadingEl) loadingEl.remove();
506
+ tmp.insertAdjacentHTML('beforeend', messageHtml);
507
+ });
508
+ _qaPersistSession(sessionKey, {
509
+ threadHtml: updatedThreadHtml,
510
+ docContext: capturedDocContext,
511
+ filePath: capturedFilePath,
512
+ history: runtime.history,
513
+ queue: runtime.queue,
514
+ });
295
515
  }
296
516
 
297
- _qaProcessing = false;
298
- _qaAbortController = null;
299
- thread.scrollTop = thread.scrollHeight;
300
-
301
- // Clear processing badge on source card (unless more messages queued)
302
- if (_qaQueue.length === 0) {
303
- const doneCard = findCardForFile(capturedFilePath);
304
- if (doneCard) clearNotifBadge(doneCard);
517
+ runtime.processing = false;
518
+ runtime.abortController = null;
519
+ if (_qaIsActiveSession(sessionKey)) {
520
+ _qaProcessing = false;
521
+ _qaAbortController = null;
305
522
  }
306
523
 
307
- // Save session (persists even if modal was closed during processing)
308
- const modalIsOpen = document.getElementById('modal').classList.contains('open');
309
- if (_qaSessionKey) {
310
- // Use captured values if closeModal nulled the globals during processing
311
- const sessionFilePath = _modalFilePath || capturedFilePath;
312
- const sessionDocContext = _modalDocContext.title ? { ..._modalDocContext } : { ...capturedDocContext, ..._modalDocContext, title: capturedDocContext.title };
313
- _qaSessions.set(_qaSessionKey, {
314
- history: _qaHistory,
315
- threadHtml: thread.innerHTML,
316
- docContext: sessionDocContext,
317
- filePath: sessionFilePath,
318
- });
319
- _saveQaSessions();
320
- // Show notification badge on source card when modal was closed during processing
321
- if (!modalIsOpen) {
322
- const card = findCardForFile(sessionFilePath);
323
- if (card) showNotifBadge(card, _qaQueue.length > 0 ? 'processing' : 'done');
324
- _qaSessionKey = '';
524
+ if (runtime.queue.length === 0) {
525
+ const doneCard = findCardForFile(capturedFilePath);
526
+ if (_qaIsActiveSession(sessionKey)) {
527
+ if (doneCard) clearNotifBadge(doneCard);
528
+ } else if (doneCard) {
529
+ showNotifBadge(doneCard, 'done');
325
530
  }
531
+ } else {
532
+ const pendingCard = findCardForFile(capturedFilePath);
533
+ if (pendingCard) showNotifBadge(pendingCard, 'processing');
326
534
  }
327
535
 
328
- // Process next queued message
329
- if (_qaQueue.length > 0) {
330
- const next = _qaQueue.shift();
331
- // Remove the queued indicator and show the blue user bubble now that it's processing
332
- const queuedEl = thread.querySelector('.qa-queued-item');
333
- if (queuedEl) queuedEl.remove();
334
- _renderQaUserMessage(thread, next.message, next.selection);
335
- _processQaMessage(next.message, next.selection);
336
- } else if (modalIsOpen) {
536
+ if (runtime.queue.length > 0) {
537
+ const next = runtime.queue.shift();
538
+ const nextThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
539
+ const queuedEl = tmp.querySelector('.qa-queued-item');
540
+ if (queuedEl) queuedEl.remove();
541
+ tmp.insertAdjacentHTML('beforeend', _qaBuildUserMessageHtml(next.message, next.selection));
542
+ });
543
+ _qaPersistSession(sessionKey, {
544
+ threadHtml: nextThreadHtml,
545
+ docContext: (_qaSessions.get(sessionKey) || {}).docContext || capturedDocContext,
546
+ filePath: capturedFilePath,
547
+ history: runtime.history,
548
+ queue: runtime.queue,
549
+ });
550
+ if (_qaIsActiveSession(sessionKey)) _qaQueue = _qaCloneQueue(runtime.queue);
551
+ _processQaMessage(next.message, next.selection, {
552
+ sessionKey,
553
+ filePath: capturedFilePath,
554
+ docContext: (_qaSessions.get(sessionKey) || {}).docContext || capturedDocContext,
555
+ });
556
+ } else if (_qaIsActiveSession(sessionKey)) {
557
+ _qaQueue = [];
337
558
  document.getElementById('modal-qa-input')?.focus();
338
559
  }
339
560
  }
340
561
 
341
562
  function qaAbort() {
342
- if (_qaAbortController) {
343
- _qaAbortController.abort();
344
- _qaAbortController = null;
563
+ const runtime = _qaGetRuntime(_qaSessionKey);
564
+ if (runtime?.abortController) {
565
+ runtime.abortController.abort();
566
+ runtime.abortController = null;
345
567
  }
346
- // Don't reset _qaProcessing here — the catch block in _processQaMessage handles it,
347
- // avoiding a double-drain race on _qaQueue from a microtask gap.
568
+ if (_qaAbortController) _qaAbortController = null;
569
+ // Don't reset _qaProcessing here the catch block in _processQaMessage handles it.
348
570
  // Don't clear _qaQueue — queued messages auto-process after abort.
349
571
  }
350
572
 
@@ -13,24 +13,17 @@ function closeModal() {
13
13
  const resetBtn = document.getElementById('modal-settings-reset');
14
14
  if (resetBtn) resetBtn.remove();
15
15
  // Save Q&A session for this document (persist across modal open/close)
16
- if (_qaSessionKey && (_qaHistory.length > 0 || _qaQueue.length > 0)) {
17
- _qaSessions.set(_qaSessionKey, {
18
- history: _qaHistory,
19
- threadHtml: (document.getElementById('modal-qa-thread') || {}).innerHTML || '',
20
- docContext: { ..._modalDocContext },
21
- filePath: _modalFilePath,
22
- });
23
- _saveQaSessions();
16
+ if (_qaSessionKey && (_qaHistory.length > 0 || _qaQueue.length > 0 || _qaProcessing)) {
17
+ _qaSaveActiveSessionState();
24
18
  }
25
19
  // If still processing, show animated badge on the source card
26
20
  if (_qaProcessing && _modalFilePath) {
27
21
  const card = findCardForFile(_modalFilePath);
28
22
  if (card) showNotifBadge(card, 'processing');
29
23
  }
30
- // Reset UI state but don't kill processing/queuethey run in background
24
+ // Reset active UI state but don't kill background processing — it is tracked per session
25
+ _qaResetActiveState();
31
26
  _modalDocContext = { title: '', content: '', selection: '' };
32
- // Keep session key alive if processing is in flight — result will save when it completes
33
- if (!_qaProcessing) _qaSessionKey = '';
34
27
  document.getElementById('modal-qa-input').value = '';
35
28
  document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
36
29
  document.getElementById('modal-qa-pill').style.display = 'none';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1026",
3
+ "version": "0.1.1027",
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"