@yemi33/minions 0.1.2424 → 0.1.2426

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.
Files changed (185) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +7 -5
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/index.html +9 -2
  55. package/docs/internal-install.md +212 -0
  56. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  57. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  58. package/docs/kb-sweep.md +2 -2
  59. package/docs/keep-processes.md +8 -1
  60. package/docs/live-checkout-mode.md +1 -1
  61. package/docs/managed-spawn.md +21 -3
  62. package/docs/named-agents.md +3 -2
  63. package/docs/onboarding.md +26 -0
  64. package/docs/plan-lifecycle.md +32 -32
  65. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  69. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  70. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  71. package/docs/qa-runbook-lifecycle.md +67 -51
  72. package/docs/qa-runbooks.md +2 -2
  73. package/docs/runtime-adapters.md +101 -50
  74. package/docs/security.md +28 -0
  75. package/docs/self-improvement.md +47 -13
  76. package/docs/skills.md +29 -0
  77. package/docs/slim-ux/concepts.md +2 -2
  78. package/docs/specs/agent-configurability.md +43 -41
  79. package/docs/specs/agent-rename.md +13 -13
  80. package/docs/team-memory.md +283 -14
  81. package/docs/tutorials/01-install-and-connect.md +23 -0
  82. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  83. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  84. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  85. package/docs/tutorials/README.md +4 -3
  86. package/docs/visual-evidence-ci.md +103 -0
  87. package/docs/watches.md +35 -30
  88. package/docs/worktree-lifecycle.md +151 -13
  89. package/engine/acp-transport.js +1 -0
  90. package/engine/ado.js +80 -33
  91. package/engine/agent-api-validation.js +571 -0
  92. package/engine/agent-worker-pool.js +7 -6
  93. package/engine/api-contracts/agent-content.js +761 -0
  94. package/engine/api-contracts/cc-ops.js +219 -0
  95. package/engine/api-contracts/config-runtime.js +419 -0
  96. package/engine/api-contracts/core.js +138 -0
  97. package/engine/api-contracts/index.js +506 -0
  98. package/engine/api-contracts/orchestration.js +602 -0
  99. package/engine/api-contracts/pull-requests.js +323 -0
  100. package/engine/api-contracts/qa-process.js +764 -0
  101. package/engine/api-contracts/work-plan-prd.js +494 -0
  102. package/engine/api-validation.js +543 -0
  103. package/engine/cc-api-validation.js +586 -0
  104. package/engine/cc-worker-pool.js +9 -1
  105. package/engine/cleanup.js +393 -271
  106. package/engine/cli.js +65 -7
  107. package/engine/comment-classifier.js +70 -5
  108. package/engine/consolidation.js +123 -5
  109. package/engine/content-api-validation.js +606 -0
  110. package/engine/create-pr-worktree.js +22 -1
  111. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  112. package/engine/dispatch.js +33 -0
  113. package/engine/distribution.js +189 -0
  114. package/engine/execution-model.js +59 -0
  115. package/engine/features.js +12 -0
  116. package/engine/github.js +67 -25
  117. package/engine/harness-context.js +313 -0
  118. package/engine/inbox-store.js +1 -1
  119. package/engine/kb-sweep.js +161 -38
  120. package/engine/keep-process-sweep.js +43 -3
  121. package/engine/lifecycle.js +1066 -165
  122. package/engine/llm.js +4 -0
  123. package/engine/managed-spawn.js +463 -48
  124. package/engine/meeting.js +197 -34
  125. package/engine/memory-retrieval.js +280 -0
  126. package/engine/memory-store.js +821 -38
  127. package/engine/model-discovery.js +17 -3
  128. package/engine/pipeline.js +802 -14
  129. package/engine/plan-prd-validation.js +745 -0
  130. package/engine/playbook.js +206 -17
  131. package/engine/pooled-agent-process.js +6 -1
  132. package/engine/pr-action.js +1 -1
  133. package/engine/pr-issue-validation.js +653 -0
  134. package/engine/pr-resolve.js +75 -13
  135. package/engine/prd-store.js +39 -6
  136. package/engine/preflight.js +7 -2
  137. package/engine/process-utils.js +79 -31
  138. package/engine/projects.js +198 -12
  139. package/engine/promotion.js +371 -0
  140. package/engine/qa-from-prd.js +3 -0
  141. package/engine/qa-process-validation.js +572 -0
  142. package/engine/qa-runbooks.js +41 -37
  143. package/engine/qa-runners/maestro.js +11 -7
  144. package/engine/qa-runners/playwright.js +37 -14
  145. package/engine/qa-runners.js +9 -10
  146. package/engine/qa-runs.js +124 -11
  147. package/engine/qa-sessions.js +139 -40
  148. package/engine/quarantine-refs.js +81 -11
  149. package/engine/queries.js +163 -9
  150. package/engine/review-learning-backfill.js +482 -0
  151. package/engine/review-learning.js +1236 -0
  152. package/engine/runtimes/claude.js +60 -2
  153. package/engine/runtimes/codex.js +57 -16
  154. package/engine/runtimes/contract.js +21 -0
  155. package/engine/runtimes/copilot.js +51 -3
  156. package/engine/runtimes/index.js +1 -0
  157. package/engine/scheduler.js +201 -32
  158. package/engine/settings-validation.js +909 -0
  159. package/engine/shared.js +705 -208
  160. package/engine/spawn-agent.js +48 -21
  161. package/engine/steering-constraints.js +31 -0
  162. package/engine/steering-store.js +10 -2
  163. package/engine/steering.js +14 -2
  164. package/engine/timeout.js +404 -111
  165. package/engine/untrusted-fence.js +24 -10
  166. package/engine/watch-actions.js +7 -2
  167. package/engine/watches.js +353 -51
  168. package/engine/work-item-validation.js +561 -0
  169. package/engine/work-items-store.js +61 -1
  170. package/engine/worktree-gc.js +656 -37
  171. package/engine/worktree-preflight.js +154 -0
  172. package/engine.js +2068 -342
  173. package/package.json +1 -1
  174. package/playbooks/_pr-description-audit.md +88 -41
  175. package/playbooks/build-fix-complex.md +3 -3
  176. package/playbooks/docs.md +2 -2
  177. package/playbooks/fix.md +18 -18
  178. package/playbooks/implement.md +18 -18
  179. package/playbooks/qa-session-draft.md +10 -12
  180. package/playbooks/qa-session-execute.md +16 -12
  181. package/playbooks/qa-validate.md +8 -7
  182. package/playbooks/shared-rules.md +45 -0
  183. package/playbooks/test.md +4 -0
  184. package/prompts/cc-system.md +1 -1
  185. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -136,7 +136,8 @@ function _detectPageChanges(data) {
136
136
  // reload path below — RENDER_VERSIONS handles the within-process case.
137
137
  const RENDER_VERSIONS = {
138
138
  // Bumped 2→3 for client-ticked relative last-action timestamps.
139
- agents: 3,
139
+ // Bumped 3→4 to show the runtime-reported model when no model is pinned.
140
+ agents: 4,
140
141
  prdProgress: 2,
141
142
  prdPrs: 1,
142
143
  inbox: 2,
@@ -164,13 +165,25 @@ const RENDER_VERSIONS = {
164
165
  // Bumped 10→11: (prior).
165
166
  // Bumped 11→12: WI detail modal surfaces the cancellation reason + cancelledAt
166
167
  // for CANCELLED items (W-mrtdet41000h935a).
167
- workItems: 12,
168
+ // Bumped 12→13: pending branch holds name the operator checkout and branch.
169
+ // Bumped 13→15: trailing action-button cell right-aligned so the delete ✕ aligns
170
+ // across rows (jumps over main's independent 13→14 workItems bump).
171
+ // Bumped 15→16: Archive row-action button outline now uses border-color
172
+ // var(--muted) to match its muted icon (visible outline, family convention).
173
+ // Bumped 16→17: merge of this branch's needs-attention renderer change with
174
+ // main's independent workItems→16 bump — both touch the row render, so force
175
+ // a client reload past both.
176
+ // Bumped 17→18: WI detail modal shows the effective model (persisted dispatch
177
+ // evidence, else the current-config projection) — W-ms0ytfdd000d5a26.
178
+ workItems: 18,
168
179
  skills: 1,
169
180
  commands: 1,
170
181
  mcpServers: 1,
171
182
  // Bumped to 2 by W-mqptgmcn000k5c90: renderHarnessDiagnostics now stamps each
172
183
  // <details> with a stable data-hkey and the render is gated behind _changed().
173
- harnessDiag: 2,
184
+ // Bumped 2→3 (W-ms0plsji000h0f9d): compact card padding/margins + Suppressed
185
+ // assets now defaults collapsed, so force a client re-render past cached state.
186
+ harnessDiag: 3,
174
187
  schedules: 1,
175
188
  watches: 3,
176
189
  meetings: 1,
@@ -185,7 +198,9 @@ const RENDER_VERSIONS = {
185
198
  // Bumped to 4 by W-mqrdavob (Agents table: per-agent charter editor column).
186
199
  // Bumped to 5 by W-mr0qs0vw (Worker Pool: CC session idle-timeout field).
187
200
  // Bumped to 6 for the fleet ACP pool opt-in default and warning copy.
188
- settings: 7,
201
+ // Bumped to 7 on origin/main.
202
+ // Bumped to 8 for the Auto-fix pane tile-grid revamp (W-mryyzhug000454b7).
203
+ settings: 8,
189
204
  };
190
205
  const _sectionCache = {};
191
206
  const _lastValueByKey = {};
@@ -267,15 +282,15 @@ function _condFetchJson(url) {
267
282
  function _formatCcPowerLabel(autoMode) {
268
283
  var runtime = autoMode && autoMode.ccCli ? String(autoMode.ccCli) : 'claude';
269
284
  var runtimeLabel = runtime.charAt(0).toUpperCase() + runtime.slice(1);
270
- var model = autoMode && autoMode.ccModel ? String(autoMode.ccModel) : '';
271
- return 'Ask anything, dispatch work, manage plans — powered by ' + runtimeLabel + (model ? ' (' + model + ')' : '');
285
+ var model = MinionsModelDisplay.label(autoMode && (autoMode.ccModelDisplay || autoMode.ccModel));
286
+ return 'Ask anything, dispatch work, manage plans — powered by ' + runtimeLabel + ' (' + model + ')';
272
287
  }
273
288
 
274
289
  function _formatCcDrawerLabel(autoMode) {
275
290
  var runtime = autoMode && autoMode.ccCli ? String(autoMode.ccCli) : 'claude';
276
291
  var runtimeLabel = runtime.charAt(0).toUpperCase() + runtime.slice(1);
277
- var model = autoMode && autoMode.ccModel ? String(autoMode.ccModel) : '';
278
- return runtimeLabel + (model ? ' (' + model + ')' : '') + '-powered. Full minions context. Enter to send, Shift+Enter for newline.';
292
+ var model = MinionsModelDisplay.label(autoMode && (autoMode.ccModelDisplay || autoMode.ccModel));
293
+ return runtimeLabel + ' (' + model + ')-powered. Full minions context. Enter to send, Shift+Enter for newline.';
279
294
  }
280
295
 
281
296
  // W-mpnc4u8c001d9d6c / W-mpodheao0006a37a — #engine-quick-stats "Next tick in"
@@ -500,7 +515,7 @@ function _processStatusUpdate(data, opts) {
500
515
  const autoEl = document.getElementById('auto-approve-badge');
501
516
  // eslint-disable-next-line no-unsanitized/property -- reason: composed from compile-time constants (no user data flows in)
502
517
  if (autoEl) autoEl.innerHTML = data.autoMode?.approvePlans
503
- ? '<span style="font-size:var(--text-xs);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3)">AUTO-APPROVE</span>'
518
+ ? '<span style="font-size:var(--text-sm);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3)">AUTO-APPROVE</span>'
504
519
  : '';
505
520
  const ccLabelEl = document.getElementById('cmd-powered-by');
506
521
  if (ccLabelEl) ccLabelEl.textContent = _formatCcPowerLabel(data.autoMode);
@@ -824,14 +839,13 @@ function _processStatusUpdate(data, opts) {
824
839
  // prunePrdRequeueState moved into the /api/work-items .then handler so it
825
840
  // operates on the fresh fetched list rather than the synchronously-empty
826
841
  // window._lastWorkItems. (Review finding #5.)
827
- // Engine log (last 50 entries) comes from /state/engine/log.json with a
828
- // client-side .slice(-50) to mirror queries.js getEngineLog (issue #2949).
842
+ // Engine log (last 50 entries) comes from the SQL-backed /api/engine-log.
829
843
  // Scroll-preservation opts (preserveEngineLogScroll) are threaded through
830
844
  // to renderEngineLog so the log pane keeps its scroll position across
831
845
  // refreshes (W-mpp75wkg000b3018).
832
846
  _safeRender('engineLog', function() {
833
847
  const logOpts = { preserveEngineLogScroll: opts.preserveEngineLogScroll };
834
- Promise.resolve(fetchEngineLogFromDisk())
848
+ Promise.resolve(fetchEngineLog())
835
849
  .then(function (fresh) {
836
850
  const list = fresh && fresh.length ? fresh : (window._lastEngineLog || []);
837
851
  window._lastEngineLog = list;
@@ -995,17 +1009,22 @@ function _processStatusUpdate(data, opts) {
995
1009
  if (Array.isArray(window._lastSchedules)) _safeRender('schedules', function() { renderSchedules(window._lastSchedules); });
996
1010
  });
997
1011
  });
998
- // Watches array comes from /state/engine/watches.json (issue #2949).
1012
+ // Watches come from GET /api/watches the canonical SQL-backed read
1013
+ // surface (engine/watches-store.js). The retired engine/watches.json mirror
1014
+ // is never written, so /state/ has nothing to serve.
999
1015
  _safeRender('watches', function() {
1000
- // Seq guard — watches.json is mutated every 3 ticks by engine/watches.js
1001
- // (mutateWatches on watch-fire / pause). (Round-4 #10.)
1016
+ // Seq guard — the watches table is mutated every 3 ticks by
1017
+ // engine/watches.js (watch-fire / pause), so a late response from a prior
1018
+ // tick must not clobber fresher state. (Round-4 #10.)
1002
1019
  const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
1003
1020
  window._lastRequestedSeq = window._lastRequestedSeq || {};
1004
1021
  window._lastRequestedSeq.watches = seq;
1005
- Promise.resolve(fetchWatchesFromDisk())
1022
+ MinionsWatchesSource.fetchWatches()
1006
1023
  .then(function (fresh) {
1007
1024
  if (seq < (window._lastRequestedSeq.watches || 0)) return;
1008
- const list = fresh && fresh.length ? fresh : (window._lastWatches || []);
1025
+ // null = request failed / unusable payload keep the prior slice.
1026
+ // A genuine empty array renders as empty (the list really is empty).
1027
+ const list = Array.isArray(fresh) ? fresh : (window._lastWatches || []);
1009
1028
  window._lastWatches = list;
1010
1029
  _safeRender('watches', function() { renderWatches(list); });
1011
1030
  })
@@ -1446,7 +1465,7 @@ async function refresh(opts) {
1446
1465
  refresh();
1447
1466
  _syncPinsFromServer(); // Load server-side pins on startup
1448
1467
  // Gate the "Try new Slim UX" promo button behind the slim-ux-promo feature
1449
- // flag (off by default). The button ships hidden in layout.html; reveal it
1468
+ // flag (on by default). The button ships hidden in layout.html; reveal it
1450
1469
  // only when the flag is on. The classic HTML (and its window.MINIONS_FEATURES
1451
1470
  // bootstrap) is built once at dashboard startup, so a runtime flag toggle
1452
1471
  // wouldn't be reflected there — read the LIVE flag state from /api/features
@@ -32,16 +32,16 @@ function _runtimeTagHtml(runtime) {
32
32
  }
33
33
  // Unknown runtime — fall back to a small text pill so the user still sees something
34
34
  const fallback = runtime || 'unknown';
35
- return '<span class="agent-runtime-tag" title="Runtime: ' + escapeHtml(fallback) + '" style="font-size:var(--text-xs);font-weight:600;letter-spacing:0.4px;text-transform:uppercase;padding:1px 5px;margin-left:6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted);background:transparent">' + escapeHtml(fallback) + '</span>';
35
+ return '<span class="agent-runtime-tag" title="Runtime: ' + escapeHtml(fallback) + '" style="font-size:var(--text-sm);font-weight:600;letter-spacing:0.4px;text-transform:uppercase;padding:1px 5px;margin-left:6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted);background:transparent">' + escapeHtml(fallback) + '</span>';
36
36
  }
37
37
 
38
38
  // W-mpmwxk4y00053271 — compact text chip showing the resolved model next to
39
- // the runtime tag. Returns '' when the model is unknown so the row degrades
40
- // gracefully instead of rendering "null" / "undefined". Caleb feedback:
39
+ // the runtime tag. For unpinned agents, the API supplies the concrete model
40
+ // reported by the latest matching runtime session. Caleb feedback:
41
41
  // "The model is really critical piece of information to me as a user."
42
42
  function _modelChipHtml(model) {
43
- if (!model || typeof model !== 'string') return '';
44
- return '<span class="agent-model-tag" title="Model: ' + escapeHtml(model) + '" style="font-size:var(--text-xs);font-weight:600;letter-spacing:0.4px;padding:1px 5px;margin-left:4px;border:1px solid var(--muted);border-radius:3px;color:var(--muted);background:transparent">' + escapeHtml(model) + '</span>';
43
+ const label = MinionsModelDisplay.label(model);
44
+ return '<span class="agent-model-tag" title="Model: ' + escapeHtml(label) + '" style="font-size:var(--text-sm);font-weight:600;letter-spacing:0.4px;padding:1px 5px;margin-left:4px;border:1px solid var(--muted);border-radius:3px;color:var(--muted);background:transparent">' + escapeHtml(label) + '</span>';
45
45
  }
46
46
 
47
47
  function _agentHasTimedAction(agent) {
@@ -80,15 +80,15 @@ function renderAgents(agents) {
80
80
  grid.innerHTML = agents.map(a => `
81
81
  <div class="agent-card ${statusColor(a.status)}" data-agent-id="${escapeHtml(a.id)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail(this.dataset.agentId)">
82
82
  <div class="agent-card-header">
83
- <span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${escapeHtml(a.name)}${_runtimeTagHtml(a.runtime)}${_modelChipHtml(a.model)}</span>
83
+ <span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${escapeHtml(a.name)}${_runtimeTagHtml(a.runtime)}${_modelChipHtml(a.displayModel || a.model)}</span>
84
84
  <span class="status-badge ${escapeHtml(a.status)}">${escapeHtml(a.status)}</span>
85
85
  </div>
86
86
  <div class="agent-role">${escapeHtml(a.role)}</div>
87
87
  ${_agentActionHtml(a)}
88
88
  ${(function() {
89
89
  var s = a.started_at, c = a.completed_at;
90
- if (s && c) { var d = new Date(c) - new Date(s); if (d > 0) { var sec = Math.floor(d/1000)%60, min = Math.floor(d/60000)%60, hr = Math.floor(d/3600000); return '<div style="font-size:var(--text-xs);color:var(--muted)">Last run: ' + (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's</div>'; } }
91
- if (s && a.status === 'working') return '<div class="agent-runtime-tick" data-started="' + s + '" style="font-size:var(--text-xs);color:var(--yellow)"></div>';
90
+ if (s && c) { var d = new Date(c) - new Date(s); if (d > 0) { var sec = Math.floor(d/1000)%60, min = Math.floor(d/60000)%60, hr = Math.floor(d/3600000); return '<div style="font-size:var(--text-sm);color:var(--muted)">Last run: ' + (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's</div>'; } }
91
+ if (s && a.status === 'working') return '<div class="agent-runtime-tick" data-started="' + s + '" style="font-size:var(--text-sm);color:var(--yellow)"></div>';
92
92
  return '';
93
93
  })()}
94
94
  ${a._blockingToolCall ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:var(--text-sm);color:var(--muted)">&#x23F3; Blocking tool call (${escapeHtml(a._blockingToolCall.tool)}) &mdash; silent ${Math.round(a._blockingToolCall.silentMs/60000)}min, timeout in ${Math.round(a._blockingToolCall.remainingMs/60000)}min</div>` : ''}
@@ -151,21 +151,17 @@ async function openAgentDetail(id) {
151
151
  // W-mpmwxk4y00053271 — mirror the model chip the card shows so the detail
152
152
  // header stays in sync. textContent path keeps the model string from being
153
153
  // interpreted as HTML.
154
- const modelSpan = (agent.model && typeof agent.model === 'string')
155
- ? (() => {
156
- const s = document.createElement('span');
157
- s.title = 'Model: ' + agent.model;
158
- s.style.cssText = 'display:inline-block;margin-left:6px;font-size:var(--text-sm);font-weight:600;letter-spacing:0.4px;padding:2px 6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted)';
159
- s.textContent = agent.model;
160
- return s;
161
- })()
162
- : null;
154
+ const modelLabel = MinionsModelDisplay.label(agent.displayModel || agent.model);
155
+ const modelSpan = document.createElement('span');
156
+ modelSpan.title = 'Model: ' + modelLabel;
157
+ modelSpan.style.cssText = 'display:inline-block;margin-left:6px;font-size:var(--text-sm);font-weight:600;letter-spacing:0.4px;padding:2px 6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted)';
158
+ modelSpan.textContent = modelLabel;
163
159
  const children = [
164
160
  emojiSpan,
165
161
  document.createTextNode(' ' + (agent.name || '') + ' \u2014 ' + (agent.role || '')),
166
162
  runtimeSpan,
167
163
  ];
168
- if (modelSpan) children.push(modelSpan);
164
+ children.push(modelSpan);
169
165
  nameEl.replaceChildren(...children);
170
166
 
171
167
  const badgeClass = agent.status;
@@ -5,64 +5,9 @@ const LOG_PER_PAGE = 50;
5
5
  let _completedPage = 0;
6
6
  let _logPage = 0;
7
7
 
8
- // Fetch engine/dispatch.json straight off disk through the /state/<path>
9
- // static-file passthrough so dispatch state flips (queued→active, active→
10
- // completed, skipReason changes on pending entries) surface within one 4 s
11
- // poll regardless of /api/status outer-cache state (issue #2949).
12
- //
13
- // completedTotal isn't on disk — it's metrics-derived (sum across
14
- // agents.tasksCompleted + agents.tasksErrored). The renderer already falls
15
- // back to completed.length when completedTotal is missing, so we just
16
- // overlay the cached value from the previous /api/status snapshot for
17
- // continuity. Engine-side _withCompletionReportSummary enrichment (which
18
- // reads per-dispatch sidecar files) is preserved on the cached snapshot
19
- // too, so the modal "view report" links keep working — we only overlay the
20
- // fresh status/timestamp fields onto the cached row when ids match.
21
- async function fetchDispatchFromDisk(cachedDispatch) {
22
- try {
23
- const r = await fetch('/state/engine/dispatch.json');
24
- if (!r.ok) return null;
25
- const fresh = await r.json();
26
- if (!fresh || typeof fresh !== 'object') return null;
27
- // Cap completed to last 20 to match engine/queries.js getDispatchQueue
28
- // — render-dispatch only paginates within this window, and dispatch.json
29
- // on disk is already capped to 100 by the engine, so this is the same
30
- // shape the renderer has always seen.
31
- if (Array.isArray(fresh.completed) && fresh.completed.length > 20) {
32
- fresh.completed = fresh.completed.slice(-20);
33
- }
34
- // Overlay completion-report summaries from the cached snapshot keyed by
35
- // dispatch id. They live in sidecar files the client can't read, but
36
- // they're slow to change so the cached overlay is fine.
37
- if (cachedDispatch) {
38
- const cachedById = new Map();
39
- for (const list of ['pending', 'active', 'completed']) {
40
- for (const d of (cachedDispatch[list] || [])) {
41
- if (d && d.id) cachedById.set(d.id, d);
42
- }
43
- }
44
- for (const list of ['pending', 'active', 'completed']) {
45
- if (!Array.isArray(fresh[list])) continue;
46
- fresh[list] = fresh[list].map((d) => {
47
- if (!d || !d.id) return d;
48
- const cached = cachedById.get(d.id);
49
- if (!cached) return d;
50
- const merged = Object.assign({}, d);
51
- if (cached._completionReport !== undefined && merged._completionReport === undefined) {
52
- merged._completionReport = cached._completionReport;
53
- }
54
- return merged;
55
- });
56
- }
57
- if (fresh.completedTotal == null && cachedDispatch.completedTotal != null) {
58
- fresh.completedTotal = cachedDispatch.completedTotal;
59
- }
60
- }
61
- return fresh;
62
- } catch {
63
- return null;
64
- }
65
- }
8
+ // Dispatch state is served fresh by GET /api/dispatch (see refresh.js) the
9
+ // engine/dispatch.json mirror it used to read was retired by the SQL-only
10
+ // cutover, and completion-report sidecars are loaded server-side now.
66
11
 
67
12
  function _completedPrev() { if (_completedPage > 0) { _completedPage--; refresh({ preserveCompletedScroll: false }); } }
68
13
  function _completedNext() { _completedPage++; refresh({ preserveCompletedScroll: false }); } // clamped in renderDispatch
@@ -382,7 +327,7 @@ function renderDispatch(dispatch, opts) {
382
327
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: dispatch type, agent, task, skip reason)
383
328
  pendingEl.innerHTML = '<div style="font-size:var(--text-base);color:var(--yellow);margin:8px 0 6px;font-weight:600">PENDING</div><div class="dispatch-list">' +
384
329
  dispatch.pending.map(d => dispatchItemHtml(d,
385
- d.skipReason ? '<span style="font-size:var(--text-xs);color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : ''
330
+ d.skipReason ? '<span style="font-size:var(--text-sm);color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : ''
386
331
  )).join('') + '</div>';
387
332
  } else {
388
333
  pendingEl.innerHTML = '';
@@ -434,19 +379,14 @@ function renderDispatch(dispatch, opts) {
434
379
  restoreDashboardScrollState(completedEl, completedScroll);
435
380
  }
436
381
 
437
- // Pull the engine log straight off disk through /state/engine/log.json.
438
- // On-disk file is the full append-only array; engine/queries.js getEngineLog
439
- // slices to the last 50 for /api/status. We replicate that slice client-side
440
- // after the disk fetch. ETag/304 keeps the steady state cheap; a new log
441
- // line forces a fresh download (file is ~500 KB at high engine traffic, but
442
- // it changes only on engine writes — not on every 4 s poll).
443
- async function fetchEngineLogFromDisk() {
382
+ // Pull the latest engine log entries from the SQL-backed API. Event-version
383
+ // ETags keep steady-state polls cheap while each new log batch invalidates it.
384
+ async function fetchEngineLog() {
444
385
  try {
445
- const r = await fetch('/state/engine/log.json');
386
+ const r = await fetch('/api/engine-log');
446
387
  if (!r.ok) return null;
447
- const raw = await r.json();
448
- const arr = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.entries) ? raw.entries : []);
449
- return arr.slice(-50);
388
+ const entries = await r.json();
389
+ return Array.isArray(entries) ? entries.slice(-50) : [];
450
390
  } catch {
451
391
  return null;
452
392
  }
@@ -548,7 +488,7 @@ function renderVersionBanner(version) {
548
488
 
549
489
  const v = version.dashboardRunning || version.running || version.disk || '?';
550
490
  const commitLabel = version.dashboardRunningCommit ? ' (' + version.dashboardRunningCommit + ')' : '';
551
- const warnStyle = 'font-size:var(--text-xs);padding:2px 8px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:4px;color:var(--yellow);cursor:help';
491
+ const warnStyle = 'font-size:var(--text-sm);padding:2px 8px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:4px;color:var(--yellow);cursor:help';
552
492
 
553
493
  // During the post-restart grace window the old engine's reported codeVersion
554
494
  // can still be in the cached payload — silently swallow the engineStale flag
@@ -570,11 +510,11 @@ function renderVersionBanner(version) {
570
510
  el.textContent = '\u26A0 Dashboard running v' + (version.dashboardRunning || '?') + ' — disk has v' + (version.disk || '?') + '. Run: minions restart';
571
511
  el.title = 'The dashboard process is running older code. Run: minions restart';
572
512
  } else if (version.updateAvailable) {
573
- el.style.cssText = 'font-size:var(--text-xs);padding:2px 8px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:4px;color:var(--green);cursor:help';
574
- el.textContent = 'v' + v + commitLabel + ' — v' + version.latest + ' available. Run: minions update or npm install -g @yemi33/minions@latest';
513
+ el.style.cssText = 'font-size:var(--text-sm);padding:2px 8px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:4px;color:var(--green);cursor:help';
514
+ el.textContent = 'v' + v + commitLabel + ' — v' + version.latest + ' available. Run: minions update or npm install -g ' + (version.packageName || '@yemi33/minions') + '@latest';
575
515
  el.title = 'A newer version is available on npm. Run minions update to upgrade and restart.';
576
516
  } else {
577
- el.style.cssText = 'font-size:var(--text-xs);color:var(--muted)';
517
+ el.style.cssText = 'font-size:var(--text-sm);color:var(--muted)';
578
518
  el.textContent = 'v' + v + commitLabel;
579
519
  el.title = 'Minions v' + v + (version.latest ? ' (latest)' : '');
580
520
  }
@@ -140,9 +140,9 @@ function renderInbox(inbox, opts) {
140
140
  <div class="inbox-preview" onclick="openModal(${idx})" style="cursor:pointer">${escapeHtml(item.content.slice(0,200))}</div>
141
141
  <div style="display:flex;gap:6px;margin-top:6px;align-items:center">
142
142
  ${pinButton(pk, pinned, 'inbox')}
143
- <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();promoteToKB(this.dataset.inboxName)">Add to Knowledge Base</button>
144
- <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();openInboxInExplorer(this.dataset.inboxName)">Open in Explorer</button>
145
- <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red)" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();deleteInboxItem(this.dataset.inboxName)">Delete</button>
143
+ <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();promoteToKB(this.dataset.inboxName)">Add to Knowledge Base</button>
144
+ <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();openInboxInExplorer(this.dataset.inboxName)">Open in Explorer</button>
145
+ <button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--red)" data-inbox-name="${escapeHtml(item.name)}" onclick="event.stopPropagation();deleteInboxItem(this.dataset.inboxName)">Delete</button>
146
146
  </div>
147
147
  </div>`;
148
148
  }).join('');
@@ -121,8 +121,8 @@ function renderMeetings(meetings, opts) {
121
121
  '<div style="display:flex;align-items:center;gap:8px">' +
122
122
  '<span style="color:' + statusColor + ';font-size:var(--text-base);font-weight:600">' + statusLabel + '</span>' +
123
123
  (m.status === 'archived'
124
- ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px" onclick="event.stopPropagation();_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>'
125
- : (m.status === 'completed' ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px" onclick="event.stopPropagation();_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' : '')) +
124
+ ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px" onclick="event.stopPropagation();_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>'
125
+ : (m.status === 'completed' ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px" onclick="event.stopPropagation();_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' : '')) +
126
126
  '<button class="btn-destructive" onclick="event.stopPropagation();_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
127
127
  '</div>' +
128
128
  '</div>' +
@@ -275,28 +275,28 @@ function _renderMeetingDetail(m) {
275
275
  // Actions
276
276
  if (m.status === 'archived') {
277
277
  html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
278
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>' +
278
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" onclick="_unarchiveMeeting(\'' + escHtml(m.id) + '\')">Unarchive</button>' +
279
279
  '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
280
280
  '</div>';
281
281
  } else if (m.status === 'completed') {
282
282
  const linkedPlan = _findLinkedPlan(m);
283
283
  html += '<div style="display:flex;gap:8px;flex-wrap:wrap;border-top:1px solid var(--border);padding-top:8px">' +
284
284
  (linkedPlan
285
- ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--blue);border-color:var(--blue)" onclick="_viewPlanWithBack(\'' + escHtml(linkedPlan.file) + '\',\'' + escHtml(m.id) + '\')">View Plan</button>' +
285
+ ? '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--blue);border-color:var(--blue)" onclick="_viewPlanWithBack(\'' + escHtml(linkedPlan.file) + '\',\'' + escHtml(m.id) + '\')">View Plan</button>' +
286
286
  '<button class="btn-action btn-action-md" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">New Plan</button>'
287
287
  : '<button class="btn-action btn-action-md" onclick="_createPlanFromMeeting(\'' + escHtml(m.id) + '\',this)">Create Plan from Meeting</button>') +
288
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px" onclick="_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' +
288
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px" onclick="_archiveMeeting(\'' + escHtml(m.id) + '\')">Archive</button>' +
289
289
  '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
290
290
  '</div>' +
291
291
  '<div id="meeting-plan-toast" class="cmd-toast cmd-toast-inline" style="margin-top:6px"></div>' +
292
- '<div style="font-size:var(--text-xs);color:var(--muted);margin-top:4px">Use the Q&amp;A below to discuss action items' + (linkedPlan ? '' : ', then create a plan to execute them') + '.</div>';
292
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-top:4px">Use the Q&amp;A below to discuss action items' + (linkedPlan ? '' : ', then create a plan to execute them') + '.</div>';
293
293
  } else {
294
294
  html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
295
295
  '<input id="meeting-note-input" type="text" placeholder="Add context for all agents..." style="flex:1;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md)" onkeydown="if(event.key===\'Enter\')_submitMeetingNote(\'' + escHtml(m.id) + '\')">' +
296
296
  '<button onclick="_submitMeetingNote(\'' + escHtml(m.id) + '\',this)" style="padding:6px 12px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:var(--text-base)">Add Note</button>' +
297
297
  '</div>' +
298
298
  '<div style="display:flex;gap:8px;margin-top:4px">' +
299
- '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_advanceMeeting(\'' + escHtml(m.id) + '\',this)">Skip to Next Round</button>' +
299
+ '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_advanceMeeting(\'' + escHtml(m.id) + '\',this)">Skip to Next Round</button>' +
300
300
  '<button class="btn-destructive btn-destructive-md" onclick="_endMeeting(\'' + escHtml(m.id) + '\',this)">End Meeting</button>' +
301
301
  '<button class="btn-destructive btn-destructive-md" onclick="_deleteMeeting(\'' + escHtml(m.id) + '\')">Delete</button>' +
302
302
  '</div>';
@@ -498,7 +498,7 @@ function _viewPlanWithBack(file, meetingId) {
498
498
  if (title && !title.querySelector('.mtg-back-btn')) {
499
499
  const back = document.createElement('button');
500
500
  back.className = 'pr-pager-btn mtg-back-btn';
501
- back.style.cssText = 'font-size:var(--text-xs);padding:2px 8px;margin-right:8px;vertical-align:middle';
501
+ back.style.cssText = 'font-size:var(--text-sm);padding:2px 8px;margin-right:8px;vertical-align:middle';
502
502
  back.textContent = '\u2190 Back to Meeting';
503
503
  back.onclick = function() { openMeetingDetail(meetingId); };
504
504
  title.prepend(back);
@@ -690,6 +690,20 @@ function _aggregateEngineUsageForTokenTile(engine) {
690
690
  return { cost, input, output, cache, calls };
691
691
  }
692
692
 
693
+ function _formatTrackedCost(value, available) {
694
+ if (available === false) return 'N/A';
695
+ return '$' + (Number(value) || 0).toFixed(2);
696
+ }
697
+
698
+ function _usageExists(m) {
699
+ return !!((m.totalCostUsd || 0)
700
+ || (m.totalInputTokens || 0)
701
+ || (m.totalOutputTokens || 0)
702
+ || (m.totalCacheRead || 0)
703
+ || (m.tasksCompleted || 0)
704
+ || (m.tasksErrored || 0));
705
+ }
706
+
693
707
  function renderTokenUsage(metrics) {
694
708
  const el = document.getElementById('token-usage-content');
695
709
  const agents = Object.entries(metrics).filter(([k]) => !k.startsWith('_'));
@@ -723,10 +737,11 @@ function renderTokenUsage(metrics) {
723
737
 
724
738
  const fmtTokens = (n) => n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? (n / 1000).toFixed(0) + 'K' : String(n);
725
739
  const fmtCost = (n) => '$' + n.toFixed(2);
740
+ const hasUnreportedAgentCost = agents.some(([, m]) => m.costTracking === false && _usageExists(m));
726
741
 
727
742
  // Summary tiles
728
743
  let html = '<div class="token-tiles">';
729
- html += '<div class="token-tile"><div class="token-tile-label">Total Cost</div><div class="token-tile-value">' + fmtCost(totalCost) + '</div></div>';
744
+ html += '<div class="token-tile"><div class="token-tile-label">Reported Cost</div><div class="token-tile-value">' + fmtCost(totalCost) + '</div><div class="token-tile-sub">excludes runtimes without USD telemetry</div></div>';
730
745
  html += '<div class="token-tile"><div class="token-tile-label">Input Tokens</div><div class="token-tile-value">' + fmtTokens(totalInput) + '</div></div>';
731
746
  html += '<div class="token-tile"><div class="token-tile-label">Output Tokens</div><div class="token-tile-value">' + fmtTokens(totalOutput) + '</div></div>';
732
747
  html += '<div class="token-tile"><div class="token-tile-label">Cache Reads</div><div class="token-tile-value">' + fmtTokens(totalCache) + '</div></div>';
@@ -735,7 +750,8 @@ function renderTokenUsage(metrics) {
735
750
  const today = new Date().toISOString().slice(0, 10);
736
751
  const todayData = daily[today];
737
752
  if (todayData) {
738
- html += '<div class="token-tile"><div class="token-tile-label">Today</div><div class="token-tile-value">' + fmtCost(todayData.costUsd) + '</div><div class="token-tile-sub">' + (todayData.tasks || 0) + ' tasks</div></div>';
753
+ const todayCostAvailable = (todayData.costUsd || 0) > 0 || !hasUnreportedAgentCost;
754
+ html += '<div class="token-tile"><div class="token-tile-label">Today</div><div class="token-tile-value">' + _formatTrackedCost(todayData.costUsd, todayCostAvailable) + '</div><div class="token-tile-sub">' + (todayData.tasks || 0) + ' tasks' + (todayCostAvailable ? '' : ' · USD telemetry unavailable') + '</div></div>';
739
755
  }
740
756
  html += '</div>';
741
757
 
@@ -743,12 +759,13 @@ function renderTokenUsage(metrics) {
743
759
  const days = Object.keys(daily).sort().slice(-14);
744
760
  if (days.length > 1) {
745
761
  const maxCost = Math.max(...days.map(d => daily[d].costUsd || 0), 0.01);
746
- html += '<div style="font-size:var(--text-sm);color:var(--muted);margin:8px 0 4px">Daily Cost (last ' + days.length + ' days)</div>';
762
+ html += '<div style="font-size:var(--text-sm);color:var(--muted);margin:8px 0 4px">Daily Reported Cost (last ' + days.length + ' days)</div>';
747
763
  html += '<div class="token-chart">';
748
764
  for (const day of days) {
749
765
  const d = daily[day];
766
+ const costAvailable = (d.costUsd || 0) > 0 || !hasUnreportedAgentCost;
750
767
  const pct = Math.max(((d.costUsd || 0) / maxCost) * 100, 2);
751
- html += '<div class="token-bar" style="height:' + pct + '%"><div class="token-bar-tip">' + day.slice(5) + ': ' + fmtCost(d.costUsd) + ' / ' + (d.tasks || 0) + ' tasks</div></div>';
768
+ html += '<div class="token-bar" style="height:' + pct + '%"><div class="token-bar-tip">' + day.slice(5) + ': ' + _formatTrackedCost(d.costUsd, costAvailable) + ' / ' + (d.tasks || 0) + ' tasks</div></div>';
752
769
  }
753
770
  html += '</div>';
754
771
  html += '<div class="token-chart-labels">';
@@ -763,7 +780,7 @@ function renderTokenUsage(metrics) {
763
780
  var tempAgents = agents.filter(function(a) { return a[0].startsWith('temp-'); });
764
781
  var tokenRows = permAgents.slice();
765
782
  if (tempAgents.length > 0) {
766
- var tempMerged = { totalCostUsd: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheRead: 0, tasksCompleted: 0, tasksErrored: 0, model: '' };
783
+ var tempMerged = { totalCostUsd: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheRead: 0, tasksCompleted: 0, tasksErrored: 0, model: '', costTracking: true };
767
784
  for (var ti = 0; ti < tempAgents.length; ti++) {
768
785
  var tm = tempAgents[ti][1];
769
786
  tempMerged.totalCostUsd += tm.totalCostUsd || 0;
@@ -772,22 +789,24 @@ function renderTokenUsage(metrics) {
772
789
  tempMerged.totalCacheRead += tm.totalCacheRead || 0;
773
790
  tempMerged.tasksCompleted += tm.tasksCompleted || 0;
774
791
  tempMerged.tasksErrored += tm.tasksErrored || 0;
792
+ if (tm.costTracking === false) tempMerged.costTracking = false;
775
793
  if (!tempMerged.model && tm.model) tempMerged.model = tm.model;
776
794
  }
777
795
  tokenRows.push(['Temp Agents (' + tempAgents.length + ')', tempMerged]);
778
796
  }
779
- var agentsWithUsage = tokenRows.filter(function(a) { return (a[1].totalCostUsd || 0) > 0; });
797
+ var agentsWithUsage = tokenRows.filter(function(a) { return _usageExists(a[1]); });
780
798
  if (agentsWithUsage.length > 0) {
781
799
  html += '<div style="font-size:var(--text-sm);color:var(--muted);margin:12px 0 4px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Agent Usage</div>';
782
800
  html += '<table class="token-agent-table"><thead><tr><th style="width:20%">Agent</th><th style="width:10%">Model</th><th style="width:14%">Cost</th><th style="width:14%">Input</th><th style="width:14%">Output</th><th style="width:14%">Cache</th><th style="width:14%">$/task</th></tr></thead><tbody>';
783
801
  for (const [id, m] of agentsWithUsage.sort((a, b) => (b[1].totalCostUsd || 0) - (a[1].totalCostUsd || 0))) {
784
802
  const tasks = (m.tasksCompleted || 0) + (m.tasksErrored || 0);
785
- const perTask = tasks > 0 ? fmtCost((m.totalCostUsd || 0) / tasks) : '-';
803
+ const costAvailable = m.costTracking !== false;
804
+ const perTask = tasks > 0 ? _formatTrackedCost((m.totalCostUsd || 0) / tasks, costAvailable) : '-';
786
805
  const modelLabel = (m.model || '').replace(/^claude-/, '').replace(/\[.*\]$/, '') || '-';
787
806
  html += '<tr>' +
788
807
  '<td style="font-weight:600">' + escHtml(id) + '</td>' +
789
808
  '<td style="color:var(--muted);font-size:var(--text-sm)">' + escHtml(modelLabel) + '</td>' +
790
- '<td>' + fmtCost(m.totalCostUsd || 0) + '</td>' +
809
+ '<td>' + _formatTrackedCost(m.totalCostUsd, costAvailable) + '</td>' +
791
810
  '<td>' + fmtTokens(m.totalInputTokens || 0) + '</td>' +
792
811
  '<td>' + fmtTokens(m.totalOutputTokens || 0) + '</td>' +
793
812
  '<td>' + fmtTokens(m.totalCacheRead || 0) + '</td>' +
@@ -804,11 +823,12 @@ function renderTokenUsage(metrics) {
804
823
  html += '<div style="font-size:var(--text-sm);color:var(--muted);margin:12px 0 4px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Engine Usage</div>';
805
824
  html += '<table class="token-agent-table"><thead><tr><th style="width:20%">Operation</th><th style="width:10%">Calls</th><th style="width:14%">Cost</th><th style="width:14%">Input</th><th style="width:14%">Output</th><th style="width:14%">Cache</th><th style="width:14%">$/call</th></tr></thead><tbody>';
806
825
  for (const [cat, e] of engineEntries.sort((a, b) => (b[1].costUsd || 0) - (a[1].costUsd || 0))) {
807
- const perCall = (e.calls || 0) > 0 ? fmtCost((e.costUsd || 0) / e.calls) : '-';
826
+ const costAvailable = (e.costUsd || 0) > 0;
827
+ const perCall = (e.calls || 0) > 0 ? _formatTrackedCost((e.costUsd || 0) / e.calls, costAvailable) : '-';
808
828
  html += '<tr>' +
809
829
  '<td style="font-weight:600">' + escHtml(labels[cat] || cat) + '</td>' +
810
830
  '<td>' + (e.calls || 0) + '</td>' +
811
- '<td>' + fmtCost(e.costUsd || 0) + '</td>' +
831
+ '<td>' + _formatTrackedCost(e.costUsd, costAvailable) + '</td>' +
812
832
  '<td>' + fmtTokens(e.inputTokens || 0) + '</td>' +
813
833
  '<td>' + fmtTokens(e.outputTokens || 0) + '</td>' +
814
834
  '<td>' + fmtTokens(e.cacheRead || 0) + '</td>' +
@@ -868,13 +888,13 @@ async function _runProjectScan() {
868
888
  html += '<div style="max-height:400px;overflow-y:auto;display:flex;flex-direction:column;gap:4px">';
869
889
  repos.forEach(function(r, i) {
870
890
  var linked = r.linked;
871
- var hostBadge = '<span style="font-size:var(--text-xs);padding:1px 5px;border-radius:3px;background:' +
891
+ var hostBadge = '<span style="font-size:var(--text-sm);padding:1px 5px;border-radius:3px;background:' +
872
892
  (r.host === 'GitHub' ? 'rgba(88,166,255,0.15);color:var(--blue)' : r.host === 'ADO' ? 'rgba(188,140,255,0.15);color:var(--purple)' : 'var(--surface2);color:var(--muted)') +
873
893
  '">' + escHtml(r.host) + '</span>';
874
894
  html += '<label style="display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;cursor:' + (linked ? 'default' : 'pointer') + ';opacity:' + (linked ? '0.5' : '1') + '">' +
875
895
  '<input type="checkbox" data-scan-idx="' + i + '" ' + (linked ? 'disabled checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px">' +
876
896
  '<div style="flex:1;min-width:0">' +
877
- '<div style="font-weight:600;font-size:var(--text-md)">' + escHtml(r.name) + ' ' + hostBadge + (linked ? ' <span style="font-size:var(--text-xs);color:var(--green)">linked</span>' : '') + '</div>' +
897
+ '<div style="font-weight:600;font-size:var(--text-md)">' + escHtml(r.name) + ' ' + hostBadge + (linked ? ' <span style="font-size:var(--text-sm);color:var(--green)">linked</span>' : '') + '</div>' +
878
898
  '<div style="font-size:var(--text-sm);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(r.path) + '</div>' +
879
899
  (r.description ? '<div style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(r.description) + '</div>' : '') +
880
900
  '</div>' +
@@ -982,7 +1002,7 @@ function _scopeBadge(scope, projectName, pluginName) {
982
1002
  else if (scope === 'claude-code' || scope === 'copilot' || scope === 'codex') {
983
1003
  label = 'user:' + scope;
984
1004
  }
985
- return '<span style="font-size:var(--text-xs);color:' + color + ';margin-left:6px;font-weight:500">[' + escHtml(label) + ']</span>';
1005
+ return '<span style="font-size:var(--text-sm);color:' + color + ';margin-left:6px;font-weight:500">[' + escHtml(label) + ']</span>';
986
1006
  }
987
1007
 
988
1008
  function renderCommands(commands) {
@@ -1028,7 +1048,7 @@ function _harnessRowsBlock(label, rows) {
1028
1048
  }
1029
1049
  const body = rows.map(r => {
1030
1050
  const marker = r.exists ? '<span style="color:var(--green)">✓</span>' : '<span style="color:var(--orange)">⚠</span>';
1031
- const missing = r.exists ? '' : ' <span style="color:var(--orange);font-size:var(--text-xs)">(missing on disk)</span>';
1051
+ const missing = r.exists ? '' : ' <span style="color:var(--orange);font-size:var(--text-sm)">(missing on disk)</span>';
1032
1052
  return '<div style="font-family:monospace;font-size:var(--text-sm);margin-left:18px;display:flex;gap:8px;align-items:baseline">' +
1033
1053
  marker + ' <span>' + escHtml(r.path) + '</span> <span style="color:var(--muted)">[' + escHtml(r.scope) + ']</span>' + missing +
1034
1054
  '</div>';
@@ -1043,12 +1063,16 @@ function renderHarnessDiagnostics(diag) {
1043
1063
  el.innerHTML = '<p class="empty">Harness diagnostics unavailable.</p>';
1044
1064
  return;
1045
1065
  }
1066
+ // Shared compact card style for every collapsed disclosure row so the section
1067
+ // reads as a tight stack instead of tall bordered boxes (W-ms0plsji000h0f9d).
1068
+ const cardStyle = 'margin:3px 0;border:1px solid var(--border);border-radius:6px;padding:4px 10px;background:var(--surface2)';
1069
+ const summaryStyle = 'font-weight:600;cursor:pointer;font-size:var(--text-sm)';
1046
1070
  const parts = [];
1047
- parts.push('<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:8px">Fleet-default runtime: <code style="color:var(--blue)">' + escHtml(diag.fleetDefaultCli || '?') + '</code>. Adapter inventory only: each CLI performs native discovery from its cwd and user configuration.</div>');
1071
+ parts.push('<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:4px">Fleet-default runtime: <code style="color:var(--blue)">' + escHtml(diag.fleetDefaultCli || '?') + '</code>. Adapter inventory only: each CLI performs native discovery from its cwd and user configuration.</div>');
1048
1072
 
1049
1073
  // Per-runtime adapter rows
1050
1074
  for (const r of diag.runtimes || []) {
1051
- parts.push('<details data-hkey="rt:' + escHtml(r.name) + '" style="margin:6px 0;border:1px solid var(--border);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer">Runtime: ' + escHtml(r.name) + '</summary>' +
1075
+ parts.push('<details data-hkey="rt:' + escHtml(r.name) + '" style="' + cardStyle + '"><summary style="' + summaryStyle + '">Runtime: ' + escHtml(r.name) + '</summary>' +
1052
1076
  _harnessRowsBlock('User asset dirs (adapter inventory)', r.userAssetDirs) +
1053
1077
  _harnessRowsBlock('Skill roots (CLI native discovery)', r.skillRoots) +
1054
1078
  _harnessRowsBlock('Skill write targets', r.skillWriteTargets) +
@@ -1057,10 +1081,11 @@ function renderHarnessDiagnostics(diag) {
1057
1081
  '</details>');
1058
1082
  }
1059
1083
 
1060
- // Suppressed assets
1084
+ // Suppressed assets — default collapsed like the other cards; operator toggles
1085
+ // are still preserved across refreshes via the data-hkey open-state snapshot.
1061
1086
  const suppressed = diag.suppressed || [];
1062
1087
  if (suppressed.length > 0) {
1063
- parts.push('<details data-hkey="suppressed" open style="margin:6px 0;border:1px solid var(--border);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer">Suppressed assets <span style="color:var(--muted);font-weight:400">(' + suppressed.length + ')</span></summary>' +
1088
+ parts.push('<details data-hkey="suppressed" style="' + cardStyle + '"><summary style="' + summaryStyle + '">Suppressed assets <span style="color:var(--muted);font-weight:400">(' + suppressed.length + ')</span></summary>' +
1064
1089
  suppressed.map(s =>
1065
1090
  '<div style="margin:6px 0 0 12px"><code style="color:var(--orange)">' + escHtml(s.flag) + '</code> <span style="color:var(--muted);font-size:var(--text-sm)">(' + escHtml(String(s.value)) + ')</span>' +
1066
1091
  '<div style="font-size:var(--text-sm);color:var(--muted);margin-left:18px">' + escHtml(s.effect || '') + '</div></div>'
@@ -1071,7 +1096,7 @@ function renderHarnessDiagnostics(diag) {
1071
1096
  // Missing dirs summary (aggregate)
1072
1097
  const missing = diag.missingDirs || [];
1073
1098
  if (missing.length > 0) {
1074
- parts.push('<details data-hkey="missingDirs" style="margin:6px 0;border:1px solid var(--border);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer">Missing on-disk dirs <span style="color:var(--muted);font-weight:400">(' + missing.length + ')</span></summary>' +
1099
+ parts.push('<details data-hkey="missingDirs" style="' + cardStyle + '"><summary style="' + summaryStyle + '">Missing on-disk dirs <span style="color:var(--muted);font-weight:400">(' + missing.length + ')</span></summary>' +
1075
1100
  '<div style="font-size:var(--text-sm);color:var(--muted);margin:4px 0 6px 12px">Adapters declare these paths but they do not exist on this host. Warnings, not failures — create them if the runtime should look there.</div>' +
1076
1101
  missing.map(m =>
1077
1102
  '<div style="font-family:monospace;font-size:var(--text-sm);margin-left:18px"><span style="color:var(--orange)">⚠</span> ' + escHtml(m.path) + ' <span style="color:var(--muted)">[' + escHtml(m.scope) + ' · ' + escHtml(m.kind) + ' · ' + escHtml(m.runtime) + ']</span></div>'
@@ -1097,7 +1122,7 @@ function renderHarnessDiagnostics(diag) {
1097
1122
  }
1098
1123
  }
1099
1124
 
1100
- window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderCommands, renderHarnessDiagnostics, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, openScanProjectsModal };
1125
+ window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderCommands, renderHarnessDiagnostics, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, _formatTrackedCost, openScanProjectsModal };
1101
1126
 
1102
1127
  // ─── keep_processes panel (W-mp68q6ke0010de68) ─────────────────────────────
1103
1128
  // Polls /api/keep-processes every refresh (engine page only) and renders a