@yemi33/minions 0.1.2255 → 0.1.2257

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');
@@ -1,9 +1,8 @@
1
1
  // Project context picker: populated from /api/status (which already returns
2
- // the configured project list). One-or-more projects: a <select> dropdown
3
- // whose first option is a "Select project" sentinel (value ""), italicized
4
- // via CSS, so the indicator reads "Select project" until the user explicitly
5
- // picks one and can always be cleared back to it. Zero-project: a hint to
6
- // add one. No auto-default: we never silently fall back to projects[0] /
2
+ // the configured project list). One-or-more projects: a row of selectable pill
3
+ // buttons, single-select, none active until the user explicitly picks one and
4
+ // can always be toggled back off to a "Select project" state. Zero-project: a
5
+ // hint to add one. No auto-default: we never silently fall back to projects[0] /
7
6
  // last-used / constellation, so project-scoped CC actions can't land in the
8
7
  // wrong project (W-mqayzsj3). Selection persists per-browser in localStorage
9
8
  // and is included in every /api/command-center/stream call.
@@ -26,29 +25,33 @@
26
25
  empty.textContent = 'No projects linked yet.';
27
26
  return empty;
28
27
  }
29
- // Strip variant: project <select>. The first option is a "Select project"
30
- // sentinel (value "") that is selected whenever no project is explicitly
31
- // chosen, so the indicator never auto-defaults and the user can always clear
32
- // their choice back to it. Returns the select so the caller can attach a
33
- // change listener.
34
- function makeContextSelect(projects, selected, displayByName) {
35
- var sel = document.createElement('select');
36
- sel.id = 'chat-context-select';
37
- sel.setAttribute('aria-label', 'Working in project');
38
- var placeholder = document.createElement('option');
39
- placeholder.value = '';
40
- placeholder.textContent = 'Select project';
41
- if (!selected) placeholder.selected = true;
42
- sel.appendChild(placeholder);
28
+ // Strip variant: project pills. One selectable pill per project, single-select.
29
+ // No auto-default: when nothing is explicitly chosen, no pill is active and the
30
+ // indicator stays in the "Select project" state, so the picker never silently
31
+ // falls back to projects[0] (W-mqayzsj3) and the user can always toggle their
32
+ // choice back off. Returns a container of pill buttons built as real DOM nodes
33
+ // (no innerHTML — SEC-03 ratchet, see test/unit.test.js DYNAMIC_INNERHTML_BASELINE);
34
+ // the caller attaches a click listener via event delegation.
35
+ function makeContextPills(projects, selected, displayByName) {
36
+ var group = document.createElement('span');
37
+ group.id = 'chat-context-pills';
38
+ group.className = 'context-pills';
39
+ group.setAttribute('role', 'radiogroup');
40
+ group.setAttribute('aria-label', 'Working in project');
43
41
  for (var i = 0; i < projects.length; i++) {
44
42
  var p = projects[i];
45
- var opt = document.createElement('option');
46
- opt.value = p;
47
- opt.textContent = (displayByName && displayByName[p]) || p;
48
- if (p === selected) opt.selected = true;
49
- sel.appendChild(opt);
43
+ var pill = document.createElement('button');
44
+ pill.type = 'button';
45
+ pill.className = 'context-pill';
46
+ pill.dataset.project = p;
47
+ pill.textContent = (displayByName && displayByName[p]) || p;
48
+ pill.setAttribute('role', 'radio');
49
+ var isActive = p === selected;
50
+ pill.classList.toggle('active', isActive);
51
+ pill.setAttribute('aria-checked', isActive ? 'true' : 'false');
52
+ group.appendChild(pill);
50
53
  }
51
- return sel;
54
+ return group;
52
55
  }
53
56
  async function loadProjectsAndRenderContext(opts) {
54
57
  var stripEl = document.getElementById('chat-context-strip');
@@ -77,23 +80,33 @@
77
80
  try { localStorage.removeItem(SLIM_PROJECT_KEY); } catch (_e) {}
78
81
  }
79
82
  // No auto-default: when nothing is explicitly selected, currentProject
80
- // stays null and the picker shows the "Select project" sentinel
81
- // (italicized via CSS). We deliberately do NOT fall back to projects[0]
82
- // here (W-mqayzsj3) so CC turns and project-scoped actions don't
83
- // silently target the wrong project.
84
- var sel = makeContextSelect(projects, currentProject, displayByName);
83
+ // stays null and no pill is active (the "Select project" state). We
84
+ // deliberately do NOT fall back to projects[0] here (W-mqayzsj3) so CC
85
+ // turns and project-scoped actions don't silently target the wrong
86
+ // project.
87
+ var pills = makeContextPills(projects, currentProject, displayByName);
85
88
  stripEl.style.display = '';
86
- controlsEl.replaceChildren(sel, makeContextAddBtn());
87
- sel.addEventListener('change', function(ev) {
88
- var val = ev.target.value || '';
89
- if (val) {
89
+ controlsEl.replaceChildren(pills, makeContextAddBtn());
90
+ pills.addEventListener('click', function(ev) {
91
+ var pill = ev.target.closest('.context-pill');
92
+ if (!pill || !pills.contains(pill)) return;
93
+ var val = pill.dataset.project || '';
94
+ if (val && val !== currentProject) {
95
+ // Select this project.
90
96
  currentProject = val;
91
97
  try { localStorage.setItem(SLIM_PROJECT_KEY, currentProject); } catch (_e) {}
92
98
  } else {
93
- // User cleared back to "Select project" — drop the persisted choice.
99
+ // Re-clicking the active pill toggles back to "Select project" —
100
+ // drop the persisted choice so nothing is selected.
94
101
  currentProject = null;
95
102
  try { localStorage.removeItem(SLIM_PROJECT_KEY); } catch (_e) {}
96
103
  }
104
+ var nodes = pills.querySelectorAll('.context-pill');
105
+ for (var j = 0; j < nodes.length; j++) {
106
+ var active = nodes[j].dataset.project === currentProject;
107
+ nodes[j].classList.toggle('active', active);
108
+ nodes[j].setAttribute('aria-checked', active ? 'true' : 'false');
109
+ }
97
110
  });
98
111
  }
99
112
  var addBtn = document.getElementById('chat-context-add');
@@ -496,27 +496,54 @@
496
496
  font-weight: 700;
497
497
  font-size: var(--text-base);
498
498
  }
499
- .chat-context-strip select {
499
+ /* Pills + "+ Add" lay out in a single row next to the "Working in" label
500
+ (W-mqsdptzy / PR #366). #chat-context-controls is a plain <span>, so
501
+ without an explicit flex the display:flex .context-pills (a block-level
502
+ flex container) pushed the "+ Add" sibling onto its own line below. */
503
+ .chat-context-strip #chat-context-controls {
504
+ display: flex;
505
+ align-items: center;
506
+ gap: var(--space-2);
507
+ flex-wrap: wrap;
508
+ min-width: 0;
509
+ }
510
+ .chat-context-strip .context-pills {
511
+ display: flex;
512
+ align-items: center;
513
+ gap: var(--space-2);
514
+ flex-wrap: wrap;
515
+ }
516
+ /* Project pills: outlined by default, filled/highlighted when active.
517
+ Single-select — exactly one (or zero) carries .active at a time. */
518
+ .chat-context-strip .context-pill {
500
519
  background: var(--bg);
501
520
  color: var(--text);
502
521
  border: 1px solid var(--border);
503
- border-radius: var(--radius);
504
- padding: 4px 8px;
522
+ border-radius: 999px;
523
+ padding: 4px 12px;
505
524
  font-family: inherit;
506
525
  font-size: var(--text-base);
526
+ line-height: 1;
507
527
  cursor: pointer;
528
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
529
+ }
530
+ .chat-context-strip .context-pill:hover {
531
+ border-color: var(--blue);
532
+ background: var(--surface2);
533
+ }
534
+ .chat-context-strip .context-pill:focus-visible {
535
+ outline: none;
536
+ border-color: var(--blue);
537
+ }
538
+ .chat-context-strip .context-pill.active {
539
+ background: var(--blue);
540
+ border-color: var(--blue);
541
+ color: #fff;
542
+ font-weight: 600;
543
+ }
544
+ .chat-context-strip .context-pill.active:hover {
545
+ background: var(--blue);
508
546
  }
509
- .chat-context-strip select:focus { outline: none; border-color: var(--blue); }
510
- /* Italicize the "Select project" placeholder option AND the closed select
511
- widget while the placeholder is the active value. `:has()` is needed
512
- because native <select> rendering ignores child <option> styles for the
513
- displayed (closed) text — we have to style the <select> itself. The
514
- explicit `option { font-style: normal }` reset stops real project names
515
- from inheriting the parent select's italic when the dropdown is opened
516
- while the placeholder is still selected. */
517
- .chat-context-strip select option { font-style: normal; color: var(--text); }
518
- .chat-context-strip select option[value=""] { font-style: italic; color: var(--muted); }
519
- .chat-context-strip select:has(option[value=""]:checked) { font-style: italic; color: var(--muted); }
520
547
  .chat-context-strip .context-static {
521
548
  color: var(--text);
522
549
  font-weight: 500;
@@ -1204,6 +1231,37 @@
1204
1231
  50% { opacity: 0.55; }
1205
1232
  }
1206
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
+
1207
1265
  /* Agent detail modal body */
1208
1266
  .agent-detail-head {
1209
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2255",
3
+ "version": "0.1.2257",
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"