@yemi33/minions 0.1.2058 → 0.1.2060

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.
@@ -4,6 +4,11 @@ function closeModal() {
4
4
  const modalEl = document.querySelector('#modal .modal');
5
5
  if (modalEl) modalEl.classList.remove('modal-wide');
6
6
  document.getElementById('modal').classList.remove('open');
7
+ // Clear the WI-detail auto-refresh tracker so renderWorkItems stops
8
+ // rewriting #modal-body each tick. Safe to do unconditionally — if the
9
+ // closed modal wasn't a WI detail, these vars were already null.
10
+ if (typeof _wiModalOpenId !== 'undefined') { _wiModalOpenId = null; }
11
+ if (typeof _wiModalHydratedFields !== 'undefined') { _wiModalHydratedFields = null; }
7
12
  clearModalBackStack();
8
13
  // Hide Q&A section (only shown for document modals)
9
14
  document.getElementById('modal-qa').style.display = 'none';
@@ -294,20 +294,38 @@ function _processStatusUpdate(data) {
294
294
  window._lastStatus = data;
295
295
 
296
296
 
297
- // Render only changed sections
298
- // Agents is exempt from the _changed gate: real-time status correctness on
299
- // the Minions Members grid (status badge, running timer, Last-run line)
300
- // beats the cost of re-rendering 5 cards every poll tick. The gate was
301
- // causing visible staleness when the ref-eq / JSON-stringify short-circuit
302
- // falsely matched across ticks (W-mpn7keq9000302c9). Still call _changed
303
- // here so the _lastChangedFlags diag ring-buffer keeps recording whether
304
- // the agents payload actually moved this tick.
297
+ // Render every section every tick (W-mpn7keq9000302c9 + this commit).
298
+ // Every `_changed(...)` GATE was producing the same class of bug
299
+ // ref-equality or JSON.stringify short-circuit falsely matching across
300
+ // ticks, leaving the DOM frozen until the user did a hard refresh. The
301
+ // gates were a small CPU win at the cost of real-time correctness on
302
+ // every screen.
303
+ //
304
+ // We KEEP the `_changed(...)` CALLS (their side effect of populating
305
+ // `_lastChangedFlags` is still load-bearing for the diag ring-buffer
306
+ // and for the `_workItemsChanged` / `_prsChanged` cross-slice triggers
307
+ // below — F1 / F3 / W-mpgb0xbh000e3b86) but no longer use the return
308
+ // value to gate the render. Each renderer is a contained DOM rewrite
309
+ // (~ a few KB of HTML); ~10–20 of them per 4s tick is well under one
310
+ // frame's budget.
311
+ //
312
+ // Gates intentionally kept: none on the render path. The render-versions
313
+ // bump path (RENDER_VERSIONS map + _changed's stringify cache) is still
314
+ // useful for the diag flag, just not as a render skip.
305
315
  _changed('agents', data.agents);
306
316
  renderAgents(data.agents);
307
317
  cmdUpdateAgentList(data.agents);
308
- if (_changed('prdProgress', data.prdProgress) || _changed('prdPrs', data.pullRequests?.length)) { renderPrdProgress(data.prdProgress); _cachePrdItems(data.prdProgress); }
309
- if (_changed('inbox', data.inbox)) renderInbox(data.inbox || []);
310
- if (_changed('projects', data.projects)) { cmdUpdateProjectList(data.projects || []); renderProjects(data.projects || []); }
318
+ // prdProgress + prdPrs are captured together so both flags publish to
319
+ // the diag buffer; the renderer + cachePrdItems run unconditionally.
320
+ _changed('prdProgress', data.prdProgress);
321
+ _changed('prdPrs', data.pullRequests?.length);
322
+ renderPrdProgress(data.prdProgress);
323
+ _cachePrdItems(data.prdProgress);
324
+ _changed('inbox', data.inbox);
325
+ renderInbox(data.inbox || []);
326
+ _changed('projects', data.projects);
327
+ cmdUpdateProjectList(data.projects || []);
328
+ renderProjects(data.projects || []);
311
329
  // FRE banner — safe to call every tick (idempotent + cheap). Pass the full
312
330
  // status payload so the runtime-CLI explainer reads autoMode.defaultCli from
313
331
  // THIS tick (window._lastStatus is hoisted above, but renderFre takes the
@@ -315,47 +333,55 @@ function _processStatusUpdate(data) {
315
333
  if (typeof renderFre === 'function') {
316
334
  try { renderFre(data); } catch { /* expected on first load */ }
317
335
  }
318
- if (_changed('notes', data.notes)) renderNotes(data.notes);
319
- if (_changed('prd', [data.prd, data.prdProgress])) renderPrd(data.prd, data.prdProgress);
336
+ _changed('notes', data.notes);
337
+ renderNotes(data.notes);
338
+ _changed('prd', [data.prd, data.prdProgress]);
339
+ renderPrd(data.prd, data.prdProgress);
320
340
  // Capture prs + workItems change signals once — also reused by the cross-slice
321
341
  // render triggers at the bottom of this function (F1/F3, W-mpgb0xbh000e3b86).
322
342
  // _changed mutates _sectionCache so it must be called exactly once per key.
323
343
  var _prsChanged = _changed('prs', data.pullRequests);
324
344
  var _workItemsChanged = _changed('workItems', data.workItems);
325
- if (_prsChanged) renderPrs(data.pullRequests || []);
326
- if (_changed('archivedPrds', data.archivedPrds)) renderArchiveButtons(data.archivedPrds || []);
327
- if (_changed('engine', data.engine)) {
328
- if (data.engine) renderEngineStatus(data.engine);
329
- var qs = document.getElementById('engine-quick-stats');
330
- if (qs && data.engine) {
331
- var wt = data.engine.worktreeCount != null ? data.engine.worktreeCount : '-';
332
- var pid = data.engine.pid || '-';
333
- // W-mpnc4u8c001d9d6c replace the dead "Tick: -" chip (control.json
334
- // never carried a `tick` field) with a live "Next tick in Xs" countdown
335
- // driven by engine.lastTickAt (stamped at the start of every tickInner)
336
- // and engine.tickInterval (config, surfaced in the status payload).
337
- // _updateNextTickChip below ticks the inner span every 1s without
338
- // re-rendering this whole row.
339
- _engineCountdown.lastTickAt = Number(data.engine.lastTickAt) || 0;
340
- _engineCountdown.tickInterval = Number(data.engine.tickInterval) || 0;
341
- _engineCountdown.engineState = data.engine.state || 'stopped';
342
- // Feed the cadence ring buffer so the overshoot label can surface the
343
- // observed tick-to-tick gap (W-mpodheao0006a37a).
344
- _recordEngineTickObservation(_engineCountdown.lastTickAt);
345
- // eslint-disable-next-line no-unsanitized/property -- reason: composed from internal engine metrics (pid, lastTickAt/tickInterval, worktreeCount) and a literal id; no user data flows in
346
- qs.innerHTML = '<span>PID: <b>' + pid + '</b></span>' +
347
- '<span>Next tick in: <b id="engine-next-tick">' + _formatNextTickText() + '</b></span>' +
348
- '<span>Worktrees: <b>' + wt + '</b></span>';
349
- _startNextTickTicker();
350
- }
345
+ renderPrs(data.pullRequests || []);
346
+ _changed('archivedPrds', data.archivedPrds);
347
+ renderArchiveButtons(data.archivedPrds || []);
348
+ _changed('engine', data.engine);
349
+ if (data.engine) renderEngineStatus(data.engine);
350
+ var qs = document.getElementById('engine-quick-stats');
351
+ if (qs && data.engine) {
352
+ var wt = data.engine.worktreeCount != null ? data.engine.worktreeCount : '-';
353
+ var pid = data.engine.pid || '-';
354
+ // W-mpnc4u8c001d9d6c replace the dead "Tick: -" chip (control.json
355
+ // never carried a `tick` field) with a live "Next tick in Xs" countdown
356
+ // driven by engine.lastTickAt (stamped at the start of every tickInner)
357
+ // and engine.tickInterval (config, surfaced in the status payload).
358
+ // _updateNextTickChip below ticks the inner span every 1s without
359
+ // re-rendering this whole row.
360
+ _engineCountdown.lastTickAt = Number(data.engine.lastTickAt) || 0;
361
+ _engineCountdown.tickInterval = Number(data.engine.tickInterval) || 0;
362
+ _engineCountdown.engineState = data.engine.state || 'stopped';
363
+ // Feed the cadence ring buffer so the overshoot label can surface the
364
+ // observed tick-to-tick gap (W-mpodheao0006a37a).
365
+ _recordEngineTickObservation(_engineCountdown.lastTickAt);
366
+ // eslint-disable-next-line no-unsanitized/property -- reason: composed from internal engine metrics (pid, lastTickAt/tickInterval, worktreeCount) and a literal id; no user data flows in
367
+ qs.innerHTML = '<span>PID: <b>' + pid + '</b></span>' +
368
+ '<span>Next tick in: <b id="engine-next-tick">' + _formatNextTickText() + '</b></span>' +
369
+ '<span>Worktrees: <b>' + wt + '</b></span>';
370
+ _startNextTickTicker();
351
371
  }
352
- if (_changed('version', data.version)) renderVersionBanner(data.version);
353
- if (_changed('adoThrottle', data.adoThrottle)) renderAdoThrottleAlert(data.adoThrottle);
354
- if (_changed('ghThrottle', data.ghThrottle)) renderGhThrottleAlert(data.ghThrottle);
355
- if (_changed('dispatch', data.dispatch)) renderDispatch(data.dispatch);
372
+ _changed('version', data.version);
373
+ renderVersionBanner(data.version);
374
+ _changed('adoThrottle', data.adoThrottle);
375
+ renderAdoThrottleAlert(data.adoThrottle);
376
+ _changed('ghThrottle', data.ghThrottle);
377
+ renderGhThrottleAlert(data.ghThrottle);
378
+ _changed('dispatch', data.dispatch);
379
+ renderDispatch(data.dispatch);
356
380
  prunePrdRequeueState(window._lastWorkItems);
357
- if (_changed('engineLog', data.engineLog)) renderEngineLog(data.engineLog || []);
358
- if (_changed('metrics', data.metrics)) renderMetrics(data.metrics || {});
381
+ _changed('engineLog', data.engineLog);
382
+ renderEngineLog(data.engineLog || []);
383
+ _changed('metrics', data.metrics);
384
+ renderMetrics(data.metrics || {});
359
385
  // managed-processes panel — ETag-gated so unchanged ticks return 304 with
360
386
  // no body (P-6e2a8b13). Sequenced BEFORE the keep-processes call below via
361
387
  // .then() so the keep renderer reads a populated managed-PID cache for
@@ -374,14 +400,21 @@ function _processStatusUpdate(data) {
374
400
  .catch(function () { /* keep render even if managed fetch failed — getLastItems() returns the last good cache (or []) */ })
375
401
  .then(function () { try { renderKeepProcesses(); } catch {} });
376
402
  }
377
- if (_workItemsChanged) renderWorkItems(data.workItems || []);
378
- if (_changed('skills', data.skills)) renderSkills(data.skills || []);
379
- if (_changed('mcpServers', data.mcpServers)) renderMcpServers(data.mcpServers || []);
380
- if (_changed('schedules', data.schedules)) renderSchedules(data.schedules || []);
381
- if (_changed('watches', data.watches)) renderWatches(data.watches || []);
382
- if (_changed('meetings', data.meetings)) renderMeetings(data.meetings || []);
383
- if (_changed('pipelines', data.pipelines) && typeof renderPipelines === 'function') renderPipelines(data.pipelines || []);
384
- if (_changed('pinned', data.pinned)) renderPinned(data.pinned || []);
403
+ renderWorkItems(data.workItems || []);
404
+ _changed('skills', data.skills);
405
+ renderSkills(data.skills || []);
406
+ _changed('mcpServers', data.mcpServers);
407
+ renderMcpServers(data.mcpServers || []);
408
+ _changed('schedules', data.schedules);
409
+ renderSchedules(data.schedules || []);
410
+ _changed('watches', data.watches);
411
+ renderWatches(data.watches || []);
412
+ _changed('meetings', data.meetings);
413
+ renderMeetings(data.meetings || []);
414
+ _changed('pipelines', data.pipelines);
415
+ if (typeof renderPipelines === 'function') renderPipelines(data.pipelines || []);
416
+ _changed('pinned', data.pinned);
417
+ renderPinned(data.pinned || []);
385
418
  // Sidebar counts (cheap)
386
419
  const swi = document.getElementById('sidebar-wi');
387
420
  if (swi) swi.textContent = (data.workItems || []).length || '';
@@ -4,6 +4,18 @@ let allWorkItems = [];
4
4
  let wiPage = 0;
5
5
  const WI_PER_PAGE = 20;
6
6
 
7
+ // Track open WI detail modal so renderWorkItems can re-render its body
8
+ // every poll tick. Without this the modal stays frozen on the snapshot
9
+ // from the moment it was opened — status flips / agent assignments /
10
+ // PR link arrival are invisible until the user closes + reopens or
11
+ // hard-refreshes. Mirrors the same fix that drove the section-render
12
+ // gate sweep (8ad48509 / W-mpn7keq9000302c9). `_wiModalHydratedFields`
13
+ // caches the heavy free-text fields (description, acceptanceCriteria,
14
+ // references) loaded once by openWorkItemDetail's GET /api/work-items/<id>
15
+ // hydration call so per-tick re-renders don't lose them.
16
+ let _wiModalOpenId = null;
17
+ let _wiModalHydratedFields = null;
18
+
7
19
  // Track retry state per work item so loading/success/error survives re-renders
8
20
  const _wiRetryState = {}; // { [id]: { status: 'pending'|'done'|'error', message?, until? } }
9
21
  function setWiRetryState(id, state) { _wiRetryState[id] = state; }
@@ -155,6 +167,27 @@ function renderWorkItems(items) {
155
167
  const newWrap = el.querySelector('.pr-table-wrap');
156
168
  if (newWrap) newWrap.scrollLeft = savedScroll;
157
169
  }
170
+ // Refresh the open WI detail modal in-place so its status badge,
171
+ // agent assignment, PR link, etc. reflect the latest /api/status slice.
172
+ // The heavy free-text fields (description, AC, references) live in
173
+ // `_wiModalHydratedFields` from the one-time GET /api/work-items/<id>
174
+ // hydration and survive across these re-renders.
175
+ if (_wiModalOpenId) {
176
+ const slim = items.find(i => i.id === _wiModalOpenId);
177
+ if (slim) {
178
+ const merged = Object.assign({}, _wiModalHydratedFields || {}, slim);
179
+ const body = document.getElementById('modal-body');
180
+ if (body) {
181
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() or renderMd() by _wiRenderDetail()
182
+ body.innerHTML = _wiRenderDetail(merged);
183
+ }
184
+ const title = document.getElementById('modal-title');
185
+ if (title) title.textContent = slim.title || slim.id;
186
+ }
187
+ // If the WI dropped out of the slim slice (deleted/archived), leave
188
+ // the modal as-is — the user will close it normally; we don't want
189
+ // to auto-dismiss in case they're still reading.
190
+ }
158
191
  }
159
192
 
160
193
  async function editWorkItem(id, source) {
@@ -649,6 +682,15 @@ function openWorkItemDetail(id) {
649
682
  (cached.referencesCount > 0 && !Array.isArray(cached.references));
650
683
 
651
684
  const initial = needsHydration ? Object.assign({}, cached, { _descriptionLoading: true }) : cached;
685
+ // Track which WI's modal is open so renderWorkItems can re-render the
686
+ // modal body each poll tick. Reset the hydrated cache; openWorkItemDetail
687
+ // is the only authoritative source of the heavy free-text fields.
688
+ _wiModalOpenId = id;
689
+ _wiModalHydratedFields = needsHydration ? null : {
690
+ description: cached.description,
691
+ acceptanceCriteria: Array.isArray(cached.acceptanceCriteria) ? cached.acceptanceCriteria : undefined,
692
+ references: Array.isArray(cached.references) ? cached.references : undefined,
693
+ };
652
694
  document.getElementById('modal-title').textContent = initial.title || initial.id;
653
695
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() or renderMd() by _wiRenderDetail() (fields: title, description, agent, source, reasons, references, artifacts, PR links)
654
696
  document.getElementById('modal-body').innerHTML = _wiRenderDetail(initial);
@@ -662,18 +704,22 @@ function openWorkItemDetail(id) {
662
704
  .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
663
705
  .then(function(data) {
664
706
  // Guard against modal navigation away from this WI during the fetch.
665
- var title = document.getElementById('modal-title');
666
- if (!title || title.textContent !== (initial.title || initial.id)) return;
707
+ if (_wiModalOpenId !== id) return;
667
708
  var full = data && data.item;
668
709
  if (!full) return;
710
+ // Cache the heavy free-text fields so per-tick re-renders preserve
711
+ // them. We keep them separate from the slim slice (which renderWorkItems
712
+ // refreshes on every tick).
713
+ _wiModalHydratedFields = {
714
+ description: full.description || cached.description || '',
715
+ acceptanceCriteria: Array.isArray(full.acceptanceCriteria) ? full.acceptanceCriteria : undefined,
716
+ references: Array.isArray(full.references) ? full.references : undefined,
717
+ };
669
718
  // Merge: cached cross-slice fields (_pr, _artifacts, etc.) WIN over
670
719
  // the on-disk record so we don't lose engine enrichment that lives
671
720
  // only on the in-memory pass. The full record contributes description,
672
721
  // acceptanceCriteria, and references back to the rendered shape.
673
- var merged = Object.assign({}, full, cached);
674
- merged.description = full.description || cached.description || '';
675
- if (Array.isArray(full.acceptanceCriteria)) merged.acceptanceCriteria = full.acceptanceCriteria;
676
- if (Array.isArray(full.references)) merged.references = full.references;
722
+ var merged = Object.assign({}, full, cached, _wiModalHydratedFields);
677
723
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() or renderMd() by _wiRenderDetail() (fields: title, description, agent, source, reasons, references, artifacts, PR links)
678
724
  document.getElementById('modal-body').innerHTML = _wiRenderDetail(merged);
679
725
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2058",
3
+ "version": "0.1.2060",
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"