@yemi33/minions 0.1.2256 → 0.1.2258

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.
@@ -0,0 +1,85 @@
1
+ // dashboard/js/charter-editor.js — shared agent charter editor widget.
2
+ //
3
+ // Backs BOTH the agent detail panel (single instance, scope 'detail') and the
4
+ // Settings Agents table (one instance per agent, scope 'settings-<id>'). All IDs
5
+ // are suffixed with a caller-supplied `scope` so multiple editors coexist on the
6
+ // same page. Raw content is kept in a module-level map (outside the DOM) so a
7
+ // surrounding innerHTML rewrite — e.g. switching detail-panel tabs — never loses
8
+ // the operator's unsaved text. Saves go through the existing
9
+ // POST /api/agents/charter endpoint; do NOT add a second endpoint.
10
+
11
+ const _charterStateByScope = Object.create(null); // scope -> { agentId, raw }
12
+
13
+ // Returns the editor markup as a string. Side effect: records per-scope state
14
+ // (agentId + raw content) so the toggle/cancel/save handlers can find it without
15
+ // threading data through the DOM. `content` is used verbatim (callers pass
16
+ // `detail.charter || ''`), preserving the detail-panel's pre-existing behavior.
17
+ function renderCharterEditor(opts) {
18
+ const agentId = (opts && opts.agentId) || '';
19
+ const content = (opts && opts.content) || '';
20
+ const scope = (opts && opts.scope) || 'detail';
21
+ _charterStateByScope[scope] = { agentId, raw: content };
22
+ const s = escHtml(scope);
23
+ const a = escHtml(agentId);
24
+ return '' +
25
+ '<div style="display:flex;gap:6px;margin-bottom:8px">' +
26
+ '<button class="pr-pager-btn" id="charter-edit-btn-' + s + '" style="font-size:var(--text-sm);padding:2px 10px" onclick="_toggleCharterEdit(\'' + s + '\')">Edit</button>' +
27
+ '<button class="modal-copy is-success" id="charter-save-btn-' + s + '" style="display:none" onclick="_saveCharter(\'' + s + '\',\'' + a + '\')">Save</button>' +
28
+ '<button class="pr-pager-btn" id="charter-cancel-btn-' + s + '" style="font-size:var(--text-sm);padding:2px 10px;display:none" onclick="_cancelCharterEdit(\'' + s + '\')">Cancel</button>' +
29
+ '</div>' +
30
+ '<div id="charter-view-' + s + '" class="section">' + renderMd(content || 'No charter found. Click Edit to create one.') + '</div>' +
31
+ '<textarea id="charter-editor-' + s + '" style="display:none;width:100%;min-height:300px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-family:Consolas,monospace;font-size:var(--text-md);resize:vertical">' + escHtml(content) + '</textarea>';
32
+ }
33
+
34
+ function _toggleCharterEdit(scope) {
35
+ scope = scope || 'detail';
36
+ document.getElementById('charter-view-' + scope).style.display = 'none';
37
+ document.getElementById('charter-editor-' + scope).style.display = '';
38
+ document.getElementById('charter-edit-btn-' + scope).style.display = 'none';
39
+ document.getElementById('charter-save-btn-' + scope).style.display = '';
40
+ document.getElementById('charter-cancel-btn-' + scope).style.display = '';
41
+ document.getElementById('charter-editor-' + scope).focus();
42
+ }
43
+
44
+ function _cancelCharterEdit(scope) {
45
+ scope = scope || 'detail';
46
+ const st = _charterStateByScope[scope] || {};
47
+ document.getElementById('charter-editor-' + scope).value = st.raw || '';
48
+ document.getElementById('charter-view-' + scope).style.display = '';
49
+ document.getElementById('charter-editor-' + scope).style.display = 'none';
50
+ document.getElementById('charter-edit-btn-' + scope).style.display = '';
51
+ document.getElementById('charter-save-btn-' + scope).style.display = 'none';
52
+ document.getElementById('charter-cancel-btn-' + scope).style.display = 'none';
53
+ }
54
+
55
+ async function _saveCharter(scope, agentId) {
56
+ scope = scope || 'detail';
57
+ const st = _charterStateByScope[scope] || {};
58
+ const id = agentId || st.agentId;
59
+ if (!id) { alert('No agent selected'); return; }
60
+ const content = document.getElementById('charter-editor-' + scope).value;
61
+ const btn = document.getElementById('charter-save-btn-' + scope);
62
+ // Optimistic: flip the button to a saving state before the await; revert on error.
63
+ btn.textContent = 'Saving...'; btn.style.pointerEvents = 'none';
64
+ try {
65
+ const res = await fetch('/api/agents/charter', {
66
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({ agent: id, content })
68
+ });
69
+ if (res.ok) {
70
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
71
+ document.getElementById('charter-view-' + scope).innerHTML = renderMd(content);
72
+ _charterStateByScope[scope] = { agentId: id, raw: content };
73
+ _cancelCharterEdit(scope);
74
+ showToast('cmd-toast', 'Charter saved', true);
75
+ } else {
76
+ const d = await res.json().catch(() => ({}));
77
+ alert('Save failed: ' + (d.error || 'unknown'));
78
+ }
79
+ } catch (e) { alert('Save failed: ' + e.message); }
80
+ btn.textContent = 'Save'; btn.style.pointerEvents = '';
81
+ }
82
+
83
+ try {
84
+ window.MinionsCharterEditor = { renderCharterEditor, _toggleCharterEdit, _cancelCharterEdit, _saveCharter };
85
+ } catch (e) { /* non-browser (unit source-inspection) */ }
@@ -1,6 +1,7 @@
1
1
  // dashboard/js/detail-panel.js — Agent detail panel extracted from dashboard.html
2
-
3
- let _charterRawCache = ''; // stored outside DOM to survive innerHTML rewrites on tab switch
2
+ // The Charter tab reuses the shared charter editor widget (dashboard/js/charter-editor.js),
3
+ // which also backs the Settings Agents table. Raw content survives innerHTML rewrites
4
+ // on tab switch via the widget's own scope-keyed state map.
4
5
 
5
6
  function closeDetail() {
6
7
  document.getElementById('detail-overlay').classList.remove('open');
@@ -201,17 +202,10 @@ function renderDetailContent(detail, tab) {
201
202
  '</div>';
202
203
  startLiveStream(currentAgentId);
203
204
  } else if (tab === 'charter') {
204
- const charterContent = detail.charter || '';
205
- // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes charter display content and escHtml() escapes textarea content before assembling HTML (see dashboard/js/utils.js)
206
- el.innerHTML =
207
- '<div style="display:flex;gap:6px;margin-bottom:8px">' +
208
- '<button class="pr-pager-btn" id="charter-edit-btn" style="font-size:var(--text-sm);padding:2px 10px" onclick="_toggleCharterEdit()">Edit</button>' +
209
- '<button class="modal-copy is-success" id="charter-save-btn" style="display:none" onclick="_saveCharter()">Save</button>' +
210
- '<button class="pr-pager-btn" id="charter-cancel-btn" style="font-size:var(--text-sm);padding:2px 10px;display:none" onclick="_cancelCharterEdit()">Cancel</button>' +
211
- '</div>' +
212
- '<div id="charter-view" class="section">' + renderMd(charterContent || 'No charter found. Click Edit to create one.') + '</div>' +
213
- '<textarea id="charter-editor" style="display:none;width:100%;min-height:300px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-family:Consolas,monospace;font-size:var(--text-md);resize:vertical">' + escHtml(charterContent) + '</textarea>';
214
- _charterRawCache = charterContent;
205
+ // Shared charter widget (scope 'detail') — same render/save logic the
206
+ // Settings Agents table uses. Pass charter verbatim to preserve behavior.
207
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderCharterEditor() escapes all user-controlled fields (renderMd/escHtml) before assembling HTML (see dashboard/js/charter-editor.js)
208
+ el.innerHTML = renderCharterEditor({ agentId: currentAgentId, content: detail.charter || '', scope: 'detail' });
215
209
  } else if (tab === 'history') {
216
210
  let html = '';
217
211
  // Recent dispatch results
@@ -240,47 +234,4 @@ function renderDetailContent(detail, tab) {
240
234
  }
241
235
  }
242
236
 
243
- function _toggleCharterEdit() {
244
- document.getElementById('charter-view').style.display = 'none';
245
- document.getElementById('charter-editor').style.display = '';
246
- document.getElementById('charter-edit-btn').style.display = 'none';
247
- document.getElementById('charter-save-btn').style.display = '';
248
- document.getElementById('charter-cancel-btn').style.display = '';
249
- document.getElementById('charter-editor').focus();
250
- }
251
-
252
- function _cancelCharterEdit() {
253
- const el = document.getElementById('detail-content');
254
- document.getElementById('charter-editor').value = _charterRawCache || '';
255
- document.getElementById('charter-view').style.display = '';
256
- document.getElementById('charter-editor').style.display = 'none';
257
- document.getElementById('charter-edit-btn').style.display = '';
258
- document.getElementById('charter-save-btn').style.display = 'none';
259
- document.getElementById('charter-cancel-btn').style.display = 'none';
260
- }
261
-
262
- async function _saveCharter() {
263
- if (!currentAgentId) { alert('No agent selected'); return; }
264
- const content = document.getElementById('charter-editor').value;
265
- const btn = document.getElementById('charter-save-btn');
266
- btn.textContent = 'Saving...'; btn.style.pointerEvents = 'none';
267
- try {
268
- const res = await fetch('/api/agents/charter', {
269
- method: 'POST', headers: { 'Content-Type': 'application/json' },
270
- body: JSON.stringify({ agent: currentAgentId, content })
271
- });
272
- if (res.ok) {
273
- // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
274
- document.getElementById('charter-view').innerHTML = renderMd(content);
275
- _charterRawCache = content;
276
- _cancelCharterEdit();
277
- showToast('cmd-toast', 'Charter saved', true);
278
- } else {
279
- const d = await res.json().catch(() => ({}));
280
- alert('Save failed: ' + (d.error || 'unknown'));
281
- }
282
- } catch (e) { alert('Save failed: ' + e.message); }
283
- btn.textContent = 'Save'; btn.style.pointerEvents = '';
284
- }
285
-
286
237
  window.MinionsDetail = { closeDetail, renderDetailTabs, switchTab, renderDetailContent };
@@ -165,7 +165,8 @@ const RENDER_VERSIONS = {
165
165
  // renderers) can call `_changed('settings', …)` and bump on visual revamps.
166
166
  // Bumped to 2 by W-mpmwxkcn000646cc (left-rail tabbed Settings layout).
167
167
  // Bumped to 3 by W-mqk2s8q1 (Advanced → Diagnostics: relocated the diag button).
168
- settings: 3,
168
+ // Bumped to 4 by W-mqrdavob (Agents table: per-agent charter editor column).
169
+ settings: 4,
169
170
  };
170
171
  const _sectionCache = {};
171
172
  const _lastValueByKey = {};
@@ -190,7 +190,6 @@ function wiRow(item) {
190
190
  followupChip = ' <a class="pr-badge draft" style="font-size:var(--text-xs);text-decoration:none" target="_blank" rel="noopener" href="' + escapeHtml(prFollowup.parent_pr_url) + '" title="Follow-up dispatched from ' + escapeHtml(prRef) + (prFollowup.parent_comment_author ? ' by ' + escapeHtml(prFollowup.parent_comment_author) : '') + '" onclick="event.stopPropagation()">&#8617; from ' + escapeHtml(prLabel) + '</a>';
191
191
  }
192
192
  return '<tr data-wi-id="' + escapeHtml(item.id) + '" style="cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;openWorkItemDetail(\'' + escapeHtml(item.id) + '\')">' +
193
- '<td><span class="pr-id">' + escapeHtml(item.id || '') + '</span></td>' +
194
193
  '<td style="min-width:280px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + followupChip + '</td>' +
195
194
  '<td><span style="font-size:var(--text-sm);color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
196
195
  (item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:var(--text-xs)">fan-out</span>' : '') + '</td>' +
@@ -286,7 +285,7 @@ function renderWorkItems(items, opts) {
286
285
  const start = wiPage * WI_PER_PAGE;
287
286
  const pageItems = items.slice(start, start + WI_PER_PAGE);
288
287
 
289
- let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>';
288
+ let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>';
290
289
  html += pageItems.map(wiRow).join('');
291
290
  html += '</tbody></table></div>';
292
291
 
@@ -1061,7 +1060,7 @@ function openWorkItemDetail(id) {
1061
1060
 
1062
1061
  function openAllWorkItems() {
1063
1062
  document.getElementById('modal-title').textContent = 'All Work Items (' + allWorkItems.length + ')';
1064
- const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>' +
1063
+ const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th><th></th></tr></thead><tbody>' +
1065
1064
  allWorkItems.map(wiRow).join('') + '</tbody></table></div>';
1066
1065
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() by wiRow() (fields: work item id/title/source/status/agent/PR links/dates/follow-up metadata)
1067
1066
  document.getElementById('modal-body').innerHTML = html;
@@ -82,7 +82,15 @@ async function openSettings() {
82
82
  '<input value="' + escHtml(a.model || '') + '" placeholder="' + escHtml(fleetModelLabel) + ' (fleet)" disabled style="width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:var(--text-base)">' +
83
83
  '</td>' +
84
84
  '<td><input data-agent="' + escHtml(id) + '" data-field="monthlyBudgetUsd" value="' + escHtml(a.monthlyBudgetUsd != null ? String(a.monthlyBudgetUsd) : '') + '" placeholder="unlimited" style="width:70px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-base);text-align:right"></td>' +
85
- '</tr>';
85
+ // Charter editor opens the SAME shared widget as the agent detail panel
86
+ // (dashboard/js/charter-editor.js); content is lazy-loaded on first open.
87
+ '<td style="text-align:center"><button type="button" class="pr-pager-btn" id="charter-toggle-' + escHtml(id) + '" style="font-size:var(--text-sm);padding:2px 10px" onclick="_toggleSettingsCharter(\'' + escHtml(id) + '\')">Show Charter</button></td>' +
88
+ '</tr>' +
89
+ // Hidden expander row — the shared charter widget is rendered into the cell
90
+ // the first time the operator opens it (see _toggleSettingsCharter).
91
+ '<tr id="charter-row-' + escHtml(id) + '" style="display:none"><td colspan="7" style="padding:8px 4px 16px">' +
92
+ '<div id="charter-cell-' + escHtml(id) + '" style="color:var(--muted)">Loading charter…</div>' +
93
+ '</td></tr>';
86
94
  }).join('');
87
95
 
88
96
  // ── Section bodies — every original control is grouped under one of these
@@ -91,7 +99,7 @@ async function openSettings() {
91
99
  // wrapping the 8 advanced runtime toggles was split into Copilot Tuning and
92
100
  // Claude Tuning tabs so each runtime's knobs live behind its own rail entry.
93
101
  const paneRuntime =
94
- '<h3>Runtime &amp; Models</h3>' +
102
+ '<h3>Agents Runtime &amp; Models</h3>' +
95
103
  '<div class="settings-pane-sub">Single source of truth for which CLI runtime + model the fleet spawns. Per-agent overrides live in the Agents table below.</div>' +
96
104
  '<div id="set-runtime-section" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:16px">' +
97
105
  '<div style="display:grid;grid-template-columns:1fr 2fr;gap:8px;margin-bottom:8px">' +
@@ -152,7 +160,7 @@ async function openSettings() {
152
160
  '<h4>Agents</h4>' +
153
161
  '<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:6px">CLI / Model placeholders show the fleet default each agent will inherit. Pick a value to pin per-agent; clear to re-inherit. Per-agent monthly budget overrides the fleet ceiling.</div>' +
154
162
  '<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:var(--text-base)">' +
155
- '<tr style="text-align:left;color:var(--muted)"><th style="padding:4px">Agent</th><th style="padding:4px">Role</th><th style="padding:4px">Skills</th><th style="padding:4px">CLI</th><th style="padding:4px">Model</th><th style="padding:4px">Budget $/mo</th></tr>' +
163
+ '<tr style="text-align:left;color:var(--muted)"><th style="padding:4px">Agent</th><th style="padding:4px">Role</th><th style="padding:4px">Skills</th><th style="padding:4px">CLI</th><th style="padding:4px">Model</th><th style="padding:4px">Budget $/mo</th><th style="padding:4px">Charter</th></tr>' +
156
164
  agentRows +
157
165
  '</table>';
158
166
 
@@ -447,7 +455,7 @@ async function openSettings() {
447
455
 
448
456
  const paneBudget =
449
457
  '<h3>Budget</h3>' +
450
- '<div class="settings-pane-sub">Fleet-wide spend ceiling. Per-agent monthly caps live in the Agents table on the Runtime &amp; Models tab.</div>' +
458
+ '<div class="settings-pane-sub">Fleet-wide spend ceiling. Per-agent monthly caps live in the Agents table on the Agents Runtime &amp; Models tab.</div>' +
451
459
  '<div class="settings-grid-2">' +
452
460
  settingsField('Max budget (USD)', 'set-maxBudgetUsd', e.maxBudgetUsd != null ? String(e.maxBudgetUsd) : '', '', 'Fleet ceiling for --max-budget-usd. 0 is a valid cap (read-only / dry-run). Empty = no cap. Claude only.') +
453
461
  '</div>';
@@ -543,7 +551,7 @@ async function openSettings() {
543
551
  // adjacent to Auto-fix so operators following the legacy mental model find
544
552
  // the moved flags quickly.
545
553
  const sections = [
546
- { id: 'runtime', label: 'Runtime & Models', featured: true, html: paneRuntime },
554
+ { id: 'runtime', label: 'Agents Runtime & Models', featured: true, html: paneRuntime },
547
555
  { id: 'autofix', label: 'Auto-fix & Review Loop', featured: true, html: paneAutoFix },
548
556
  { id: 'lifecycle', label: 'PR Lifecycle', html: paneLifecycle },
549
557
  { id: 'workflow', label: 'Workflow Defaults', html: paneWorkflow },
@@ -696,6 +704,33 @@ async function openSettings() {
696
704
  });
697
705
  }
698
706
 
707
+ // Lazily open/close the per-agent charter editor in the Settings Agents table.
708
+ // The charter content is NOT included in /api/settings, so the first open fetches
709
+ // it from /api/agent/<id> and renders the shared charter widget (scope
710
+ // 'settings-<id>') into the expander cell — the same widget the detail panel uses.
711
+ async function _toggleSettingsCharter(agentId) {
712
+ const row = document.getElementById('charter-row-' + agentId);
713
+ const cell = document.getElementById('charter-cell-' + agentId);
714
+ if (!row || !cell) return;
715
+ const isOpen = row.style.display !== 'none';
716
+ if (isOpen) { row.style.display = 'none'; return; }
717
+ row.style.display = '';
718
+ if (cell.dataset.loaded) return; // already rendered — just re-expand
719
+ cell.textContent = 'Loading charter…';
720
+ try {
721
+ const res = await fetch('/api/agent/' + encodeURIComponent(agentId));
722
+ if (!res.ok) throw new Error('HTTP ' + res.status);
723
+ const detail = await res.json();
724
+ const html = renderCharterEditor({ agentId, content: detail.charter || '', scope: 'settings-' + agentId });
725
+ cell.textContent = '';
726
+ // eslint-disable-next-line no-unsanitized/method -- reason: renderCharterEditor() escapes all user-controlled fields (renderMd/escHtml) before assembling HTML (see dashboard/js/charter-editor.js)
727
+ cell.insertAdjacentHTML('beforeend', html);
728
+ cell.dataset.loaded = '1';
729
+ } catch (err) {
730
+ cell.textContent = 'Failed to load charter: ' + err.message;
731
+ }
732
+ }
733
+
699
734
  async function initRuntimeFleetUI(engineCfg, agentsCfg) {
700
735
  const cliSelect = document.getElementById('set-defaultCli');
701
736
  const ccCliSelect = document.getElementById('set-ccCli');
@@ -75,6 +75,11 @@
75
75
  <div class="member-grid" id="member-grid">
76
76
  <div class="member-empty">Loading team&hellip;</div>
77
77
  </div>
78
+ <!-- Inline hover details bar: sweeping across the member cards reveals
79
+ this bar with the hovered agent's task + a live working-for
80
+ duration, without opening the click->modal. Populated by
81
+ showAgentHoverBar(); hidden until first hover/focus. -->
82
+ <div class="member-hover-bar" id="member-hover-bar" hidden></div>
78
83
  </div>
79
84
  <!-- System sub-section: cockpit indicator tiles. The per-minion "working"
80
85
  count now lives in the Team cards above, so that tile is removed. -->
@@ -47,6 +47,13 @@
47
47
  card.addEventListener('keydown', function(ev) {
48
48
  if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openAgentDetail(a); }
49
49
  });
50
+ // Hover (and keyboard focus, for tabindex=0 parity) reveals the inline
51
+ // details bar under the grid without opening the click->modal. Listeners
52
+ // are re-attached on every renderMembers since the cards are recreated.
53
+ card.addEventListener('mouseenter', function() { showAgentHoverBar(a); });
54
+ card.addEventListener('mouseleave', _scheduleHideAgentHoverBar);
55
+ card.addEventListener('focus', function() { showAgentHoverBar(a); });
56
+ card.addEventListener('blur', _scheduleHideAgentHoverBar);
50
57
 
51
58
  var emoji = document.createElement('div');
52
59
  emoji.className = 'member-emoji';
@@ -92,6 +99,111 @@
92
99
  if (_agentDetailTimer) { clearInterval(_agentDetailTimer); _agentDetailTimer = null; }
93
100
  }
94
101
 
102
+ // ── Inline hover details bar (member grid) ──────────────────────────
103
+ // Sweeping across .member-card elements reveals an inline bar UNDER the grid
104
+ // showing the hovered agent's task + a live "Working for" duration — without
105
+ // opening the click->modal. Uses a hover-scoped ticker distinct from the
106
+ // modal's _agentDetailTimer so the two never fight over one interval, and is
107
+ // cleared on every enter/leave/swap so no interval leaks when sweeping
108
+ // quickly across many cards.
109
+ var _agentHoverTimer = null;
110
+
111
+ function _tickAgentHoverRuntime() {
112
+ var el = document.getElementById('slim-agent-hover-tick');
113
+ if (!el) { _stopAgentHoverRuntime(); return; }
114
+ el.textContent = _fmtAgentElapsed(Date.now() - new Date(el.dataset.started).getTime());
115
+ }
116
+
117
+ function _stopAgentHoverRuntime() {
118
+ if (_agentHoverTimer) { clearInterval(_agentHoverTimer); _agentHoverTimer = null; }
119
+ }
120
+
121
+ // Dismiss-grace delay: the hover bar is hidden 0.5s after the pointer leaves
122
+ // (or focus blurs) rather than instantly, so brushing past a card or hopping
123
+ // between agents doesn't make the bottom status flicker. Re-entering any card
124
+ // cancels the pending hide (showAgentHoverBar → _cancelPendingHoverHide).
125
+ var AGENT_HOVER_DISMISS_MS = 500;
126
+ var _agentHoverHideTimer = null;
127
+
128
+ function _cancelPendingHoverHide() {
129
+ if (_agentHoverHideTimer) { clearTimeout(_agentHoverHideTimer); _agentHoverHideTimer = null; }
130
+ }
131
+
132
+ function _scheduleHideAgentHoverBar() {
133
+ _cancelPendingHoverHide();
134
+ _agentHoverHideTimer = setTimeout(function() {
135
+ _agentHoverHideTimer = null;
136
+ hideAgentHoverBar();
137
+ }, AGENT_HOVER_DISMISS_MS);
138
+ }
139
+
140
+ // Populate + reveal the hover bar for the given agent object (passed straight
141
+ // from the hovered/focused card). Always clears any prior hover ticker first
142
+ // so swapping between cards can never leak a second interval.
143
+ function showAgentHoverBar(a) {
144
+ var bar = document.getElementById('member-hover-bar');
145
+ if (!a || !bar) return;
146
+ _cancelPendingHoverHide();
147
+ _stopAgentHoverRuntime();
148
+ bar.textContent = '';
149
+
150
+ var name = document.createElement('span');
151
+ name.className = 'member-hover-name';
152
+ name.textContent = a.name || a.id;
153
+ bar.appendChild(name);
154
+
155
+ // Reuse the colored member-status chip rather than re-deriving status copy.
156
+ bar.appendChild(createStatusChip(a.status));
157
+
158
+ var runtime = [a.runtime, a.model].filter(Boolean).join(' · ');
159
+ if (runtime) {
160
+ var rt = document.createElement('span');
161
+ rt.className = 'member-hover-runtime';
162
+ rt.textContent = runtime;
163
+ bar.appendChild(rt);
164
+ }
165
+
166
+ // Only running agents with a start timestamp get the live elapsed segment.
167
+ // Rendered right after the runtime/model so the model name sits next to
168
+ // "Working for"; the work item then takes its own full-width row below.
169
+ if (a.status === 'working' && a.started_at) {
170
+ var work = document.createElement('span');
171
+ work.className = 'member-hover-working';
172
+ var label = document.createElement('span');
173
+ label.className = 'member-hover-working-label';
174
+ label.textContent = 'Working for ';
175
+ var tick = document.createElement('span');
176
+ tick.id = 'slim-agent-hover-tick';
177
+ tick.dataset.started = a.started_at;
178
+ work.appendChild(label);
179
+ work.appendChild(tick);
180
+ bar.appendChild(work);
181
+ _tickAgentHoverRuntime();
182
+ _agentHoverTimer = setInterval(_tickAgentHoverRuntime, 1000);
183
+ }
184
+
185
+ // Work item (current task) appended last so it wraps onto its own
186
+ // full-width row beneath the name/status/model/working-for metadata.
187
+ var task = a.currentTask || a.lastAction || '';
188
+ var taskEl = document.createElement('span');
189
+ taskEl.className = 'member-hover-task' + (task ? '' : ' muted');
190
+ taskEl.textContent = task || 'Idle — nothing in flight';
191
+ bar.appendChild(taskEl);
192
+
193
+ bar.classList.add('open');
194
+ bar.removeAttribute('hidden');
195
+ }
196
+
197
+ // Hide the hover bar and clear its ticker (no leaked intervals on leave/blur).
198
+ function hideAgentHoverBar() {
199
+ _cancelPendingHoverHide();
200
+ _stopAgentHoverRuntime();
201
+ var bar = document.getElementById('member-hover-bar');
202
+ if (!bar) return;
203
+ bar.classList.remove('open');
204
+ bar.setAttribute('hidden', '');
205
+ }
206
+
95
207
  // Append a labelled key/value row to the agent detail modal body.
96
208
  function appendAgentRow(body, key, value, muted) {
97
209
  var row = document.createElement('div');
@@ -1231,6 +1231,37 @@
1231
1231
  50% { opacity: 0.55; }
1232
1232
  }
1233
1233
 
1234
+ /* Inline hover/focus details bar under the member grid. Sweeping across the
1235
+ cards reveals the hovered agent's task + live working-for duration
1236
+ without opening the click->modal; harmonizes with the .member-card and
1237
+ .agent-detail-* surfaces. */
1238
+ .member-hover-bar {
1239
+ margin-top: 8px;
1240
+ display: flex;
1241
+ flex-wrap: wrap;
1242
+ align-items: center;
1243
+ gap: 8px;
1244
+ padding: 8px 10px;
1245
+ background: var(--surface2);
1246
+ border: 1px solid var(--border);
1247
+ border-radius: var(--radius);
1248
+ font-size: var(--text-md);
1249
+ }
1250
+ .member-hover-bar[hidden] { display: none; }
1251
+ .member-hover-name { font-weight: 600; color: var(--text); }
1252
+ .member-hover-task {
1253
+ flex: 1 1 100%;
1254
+ min-width: 0;
1255
+ color: var(--text);
1256
+ overflow: hidden;
1257
+ text-overflow: ellipsis;
1258
+ white-space: nowrap;
1259
+ }
1260
+ .member-hover-task.muted { color: var(--muted); font-style: italic; }
1261
+ .member-hover-runtime { font-size: var(--text-base); color: var(--muted); }
1262
+ .member-hover-working { white-space: nowrap; color: var(--amber); font-weight: 600; }
1263
+ .member-hover-working-label { color: var(--muted); font-weight: 400; }
1264
+
1234
1265
  /* Agent detail modal body */
1235
1266
  .agent-detail-head {
1236
1267
  display: flex;
@@ -44,7 +44,7 @@ function buildDashboardHtml() {
44
44
  }
45
45
 
46
46
  const jsFiles = [
47
- 'utils', 'state', 'features-client', 'render-utils', 'detail-panel', 'live-stream',
47
+ 'utils', 'state', 'features-client', 'render-utils', 'charter-editor', 'detail-panel', 'live-stream',
48
48
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
49
49
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
50
50
  'render-other', 'render-managed', 'memory-panel', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
package/dashboard.js CHANGED
@@ -1798,7 +1798,7 @@ function buildDashboardHtml() {
1798
1798
 
1799
1799
  // Assemble JS modules (order matters: utils → state → renderers → commands → refresh)
1800
1800
  const jsFiles = [
1801
- 'utils', 'state', 'features-client', 'render-utils', 'detail-panel', 'live-stream',
1801
+ 'utils', 'state', 'features-client', 'render-utils', 'charter-editor', 'detail-panel', 'live-stream',
1802
1802
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
1803
1803
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
1804
1804
  'render-other', 'render-managed', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
package/engine/shared.js CHANGED
@@ -7845,8 +7845,8 @@ function mutateWorkItems(filePath, mutator) {
7845
7845
  if (wrote) {
7846
7846
  try { store._mirrorJsonFromSql(scope, filePath); } catch { /* mirror best-effort */ }
7847
7847
  try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
7848
+ try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
7848
7849
  }
7849
- try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
7850
7850
  return result;
7851
7851
  }
7852
7852
 
@@ -7859,9 +7859,9 @@ function mutateWorkItems(filePath, mutator) {
7859
7859
  _skipSqlRouting: true,
7860
7860
  onWrote: () => {
7861
7861
  try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
7862
+ try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
7862
7863
  },
7863
7864
  });
7864
- try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
7865
7865
  return result;
7866
7866
  }
7867
7867
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2256",
3
+ "version": "0.1.2258",
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"