@yemi33/minions 0.1.2430 → 0.1.2432

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.
@@ -33,6 +33,13 @@ function renderProjects(projects) {
33
33
  // a warning chip when branchMismatch (configured ≠ origin/HEAD) OR when the
34
34
  // local branch is behind main, and a compact "↑N ↓M" chip when ahead/behind
35
35
  // counts are non-zero.
36
+ //
37
+ // SEMANTICS SOURCE OF TRUTH: the branch/ahead-behind/dirty/main-drift semantics
38
+ // below are mirrored (data-only, no HTML) in dashboard/shared/project-git-summary.js
39
+ // so the Slim UX project-pill hover can render the SAME facts without forking
40
+ // this HTML renderer (CLAUDE.md "no duplicate solutions"). Keep the two in sync;
41
+ // this function stays the canonical HTML renderer (its unit tests extract it
42
+ // standalone, so it deliberately does not call the shared helper).
36
43
  function _renderProjectBranch(p) {
37
44
  if (!p) return '';
38
45
  if (p.gitState === 'missing') return '<span class="project-warn" title="Project localPath does not exist on disk">(path not found)</span>';
@@ -207,12 +207,12 @@ async function openSettings() {
207
207
  '</div>' +
208
208
  '<div class="settings-pane-sub" style="margin-bottom:8px">Per-cause triggers — each tile automatically dispatches an agent for one PR signal (it names the exact discoverFromPrs site it gates). Enabled tiles are highlighted; disabled tiles are dimmed. When <em>Pause ALL</em> is ON every tile is inert; when it is OFF, each tile decides independently whether its site fires.</div>' +
209
209
  '<div class="settings-tile-grid" style="margin-bottom:16px">' +
210
- settingsTile('Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled', '🔨', 'auto-fix builds') +
210
+ settingsTile('Fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled', '🔨', 'auto-fix builds') +
211
211
  settingsTile('Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled', '🔀', 'auto-fix conflicts') +
212
212
  settingsTile('Review PRs', 'set-autoReviewPrs', e.autoReviewPrs !== false, 'Shared dispatch gate: review agent for newly opened agent PRs; also requires that PR provider polling is enabled', '🔍', 'auto-review prs') +
213
213
  settingsTile('Re-review PRs', 'set-autoReReviewPrs', e.autoReReviewPrs !== false, 'Shared dispatch gate: review agent after a fix push is awaiting re-review; also requires that PR provider polling is enabled', '🔁', 'auto-re-review prs') +
214
214
  settingsTile('Review Feedback', 'set-autoFixReviewFeedback', e.autoFixReviewFeedback !== false, 'Shared dispatch gate: fix agent for minions changes-requested verdicts; also requires that PR provider polling is enabled', '✍️', 'auto-fix review feedback') +
215
- settingsTile('Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled', '💬', 'auto-fix human comments') +
215
+ settingsTile('Address Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled', '💬', 'auto-fix human comments') +
216
216
  '</div>' +
217
217
  '<div class="settings-stack" style="margin-bottom:16px">' +
218
218
  settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Gates the review→fix iteration loop only (first review, re-review, review-feedback fix). Does NOT gate build-failure, merge-conflict, or human-comment fixes. Use the emergency stop above to halt everything.') +
@@ -0,0 +1,239 @@
1
+ // dashboard/shared/project-git-summary.js
2
+ //
3
+ // Pure, DOM-free summarizer of a /api/status project object's git + dispatch
4
+ // state. This is the SINGLE source of truth for the *semantics* that the
5
+ // classic Projects bar renders as HTML chips in dashboard/js/render-other.js
6
+ // (_renderProjectBranch / _renderWorktreeModePill / _liveValidationTypesArray).
7
+ // Slim UX loads this helper (it does NOT load render-other.js) to build the
8
+ // project-pill hover panel without forking the classic branching logic
9
+ // (CLAUDE.md "no duplicate solutions — reuse before re-implementing").
10
+ //
11
+ // It intentionally returns plain data only — no HTML, no escaping, no DOM — so
12
+ // each surface renders it its own way (classic keeps its HTML chips; slim renders
13
+ // a hover popover + a title-attribute fallback). Keep the field semantics in
14
+ // lock-step with render-other.js; that file remains the canonical HTML renderer
15
+ // and its unit tests (ahead-behind-badge-comparator-label.test.js,
16
+ // dashboard-worktree-mode-pill.test.js) extract those functions standalone, so
17
+ // they are deliberately NOT refactored to call this helper.
18
+
19
+ // Mirrors render-other.js#_liveValidationTypesArray: normalize the single-string
20
+ // legacy shape OR an array of strings into an array of non-empty strings.
21
+ function _projectLiveValidationTypes(p) {
22
+ var raw = p && p.liveValidationType;
23
+ if (!raw) return [];
24
+ var arr = Array.isArray(raw) ? raw : [raw];
25
+ return arr.filter(function (t) { return typeof t === 'string' && t.length > 0; });
26
+ }
27
+
28
+ // Mirrors the checkout-mode classification in render-other.js#_renderWorktreeModePill.
29
+ // Returns { kind, label, types, autoValidate, detail } where kind is one of
30
+ // 'worktree' | 'live' | 'hybrid' | 'hybrid-invalid'.
31
+ function _projectCheckoutMode(p) {
32
+ if (p && p.checkoutMode === 'live') {
33
+ var types = _projectLiveValidationTypes(p);
34
+ if (p.liveValidationInvalid) {
35
+ var effective = types.length > 0 ? types.join(', ') : 'none';
36
+ return {
37
+ kind: 'hybrid-invalid',
38
+ label: 'Hybrid · invalid',
39
+ types: types,
40
+ autoValidate: false,
41
+ detail: 'Invalid hybrid configuration — unsupported settings are ignored. '
42
+ + 'Effective live work-item types: ' + effective + '.',
43
+ };
44
+ }
45
+ if (types.length > 0) {
46
+ var typesText = types.join(', ');
47
+ var autoDispatch = p.liveValidationAutoDispatch === true;
48
+ return {
49
+ kind: 'hybrid',
50
+ label: 'Hybrid · ' + typesText + (autoDispatch ? ' · auto-validate' : ''),
51
+ types: types,
52
+ autoValidate: autoDispatch,
53
+ detail: 'Hybrid dispatch — only the "' + typesText + '" work-item type(s) run '
54
+ + 'in-place; all others use isolated worktrees. '
55
+ + (autoDispatch
56
+ ? 'A validation work item is dispatched automatically after each eligible coding work item.'
57
+ : 'Auto-validation is OFF — validation must be dispatched manually.'),
58
+ };
59
+ }
60
+ return {
61
+ kind: 'live',
62
+ label: 'Live checkout',
63
+ types: [],
64
+ autoValidate: false,
65
+ detail: 'Live-checkout dispatch — agents run in-place inside the project working '
66
+ + 'tree (no isolated worktree); capped to one mutating dispatch.',
67
+ };
68
+ }
69
+ return {
70
+ kind: 'worktree',
71
+ label: 'Worktrees',
72
+ types: [],
73
+ autoValidate: false,
74
+ detail: 'Worktree dispatch mode (default) — each agent runs in its own git worktree.',
75
+ };
76
+ }
77
+
78
+ // Summarize a single project object from GET /api/status.
79
+ // Returns a plain object:
80
+ // {
81
+ // state, // 'missing'|'non-git'|'probe-timeout'|'stale'|'unknown'|'ok'
82
+ // available, // true only when git branch data is present + fresh
83
+ // refreshing, // true when the cached branch data is known-stale
84
+ // message, // short label for non-ok/unavailable states (or null)
85
+ // branch, mainBranch, remoteDefaultBranch,
86
+ // detached, dirty,
87
+ // branchDiffersFromMain,
88
+ // ahead, behind, comparator, branchDiffersFromComparator, showAheadBehind,
89
+ // warn, // null | { kind:'main-drift'|'behind', text, detail }
90
+ // mode, // { kind, label, types, autoValidate, detail }
91
+ // lines, // [{ label, value }] ready-to-render rows
92
+ // warnings, // [string] visible warning chips
93
+ // titleText, // multi-fact "•"-joined string for a title= fallback
94
+ // }
95
+ function summarizeProjectGit(p) {
96
+ var mode = _projectCheckoutMode(p || {});
97
+ var base = {
98
+ state: 'unknown',
99
+ available: false,
100
+ refreshing: false,
101
+ message: null,
102
+ branch: null,
103
+ mainBranch: null,
104
+ remoteDefaultBranch: null,
105
+ detached: false,
106
+ dirty: false,
107
+ branchDiffersFromMain: false,
108
+ ahead: null,
109
+ behind: null,
110
+ comparator: null,
111
+ branchDiffersFromComparator: false,
112
+ showAheadBehind: false,
113
+ warn: null,
114
+ mode: mode,
115
+ lines: [],
116
+ warnings: [],
117
+ titleText: '',
118
+ };
119
+
120
+ if (!p) return base;
121
+
122
+ // Non-ok git states — mirror the early returns of _renderProjectBranch.
123
+ if (p.gitState === 'missing') {
124
+ base.state = 'missing';
125
+ base.message = '(path not found)';
126
+ base.titleText = 'Project localPath does not exist on disk';
127
+ return base;
128
+ }
129
+ if (p.gitState === 'non-git') {
130
+ base.state = 'non-git';
131
+ base.message = '(not a git repo)';
132
+ base.titleText = 'Project path exists but is not a git repository';
133
+ return base;
134
+ }
135
+ if (p.gitState === 'probe-timeout') {
136
+ base.state = 'probe-timeout';
137
+ base.message = '(checking git…)';
138
+ base.titleText = 'Git status probe timed out (slow filesystem). The repository is valid and will refresh.';
139
+ return base;
140
+ }
141
+ if (p.gitState !== 'ok' || !p.gitBranch) {
142
+ // Nothing to show (classic returns an empty string here).
143
+ base.state = 'unknown';
144
+ return base;
145
+ }
146
+ if (p.gitStale) {
147
+ // Cached branch/dirty is known-stale — show a neutral refreshing state
148
+ // rather than a possibly-wrong branch name (parity with classic).
149
+ base.state = 'stale';
150
+ base.refreshing = true;
151
+ base.message = '(refreshing…)';
152
+ base.titleText = 'Branch changed outside the dashboard — refreshing…';
153
+ return base;
154
+ }
155
+
156
+ var branch = p.gitBranch;
157
+ var mainBranch = p.mainBranch || '';
158
+ var remoteDefault = p.remoteDefaultBranch || '';
159
+ var ahead = Number.isFinite(p.ahead) ? p.ahead : null;
160
+ var behind = Number.isFinite(p.behind) ? p.behind : null;
161
+ var branchDiffersFromMain = !!(mainBranch && branch !== mainBranch);
162
+ var comparator = mainBranch || remoteDefault;
163
+ // Comparator for ahead/behind is origin/<mainBranch || remoteDefaultBranch>
164
+ // (engine/queries.js _probeProjectGitStatus), never the branch's own upstream.
165
+ var comparatorRef = mainBranch || remoteDefault;
166
+ var branchDiffersFromComparator = !!(comparator && branch !== comparatorRef);
167
+ var showAheadBehind = ahead !== null && behind !== null && (ahead > 0 || behind > 0);
168
+
169
+ base.state = 'ok';
170
+ base.available = true;
171
+ base.branch = branch;
172
+ base.mainBranch = mainBranch || null;
173
+ base.remoteDefaultBranch = remoteDefault || null;
174
+ base.detached = !!p.gitDetached;
175
+ base.dirty = !!p.gitDirty;
176
+ base.branchDiffersFromMain = branchDiffersFromMain;
177
+ base.ahead = ahead;
178
+ base.behind = behind;
179
+ base.comparator = comparator || null;
180
+ base.branchDiffersFromComparator = branchDiffersFromComparator;
181
+ base.showAheadBehind = showAheadBehind;
182
+
183
+ // Warning chip — mirror _renderProjectBranch precedence: main drift wins over
184
+ // "behind".
185
+ if (p.branchMismatch) {
186
+ base.warn = {
187
+ kind: 'main-drift',
188
+ text: '⚠ main drift',
189
+ detail: 'Configured mainBranch (' + mainBranch + ') differs from origin/HEAD ('
190
+ + remoteDefault + ') — config is likely stale',
191
+ };
192
+ } else if (behind !== null && behind > 0) {
193
+ base.warn = {
194
+ kind: 'behind',
195
+ text: '⚠ behind',
196
+ detail: 'Local branch is ' + behind + ' commits behind origin/' + comparatorRef,
197
+ };
198
+ }
199
+
200
+ // Ready-to-render rows for a hover panel.
201
+ var branchValue = 'on: ' + branch
202
+ + (branchDiffersFromMain ? ' → ' + mainBranch : '')
203
+ + (base.detached ? ' (detached)' : '');
204
+ base.lines.push({ label: 'Branch', value: branchValue });
205
+ if (showAheadBehind) {
206
+ base.lines.push({
207
+ label: 'Ahead / behind',
208
+ value: '↑' + ahead + ' ↓' + behind
209
+ + (branchDiffersFromComparator ? ' vs ' + comparator : ''),
210
+ });
211
+ }
212
+ if (base.dirty) {
213
+ base.lines.push({ label: 'Working tree', value: '● uncommitted changes' });
214
+ }
215
+ base.lines.push({ label: 'Checkout', value: mode.label });
216
+ if (base.warn) base.warnings.push(base.warn.detail);
217
+
218
+ // "•"-joined multi-fact string for a title= attribute fallback. Mirrors the
219
+ // titleBits composition in _renderProjectBranch, then appends checkout mode.
220
+ var titleBits = ['on: ' + branch];
221
+ if (mainBranch) titleBits.push('configured main: ' + mainBranch);
222
+ if (remoteDefault) titleBits.push('origin/HEAD: ' + remoteDefault);
223
+ if (ahead !== null && behind !== null) titleBits.push('↑' + ahead + ' ↓' + behind + ' vs origin/' + comparatorRef);
224
+ if (base.detached) titleBits.push('(detached)');
225
+ if (base.dirty) titleBits.push('(dirty)');
226
+ titleBits.push('checkout: ' + mode.label);
227
+ if (base.warn) titleBits.push(base.warn.detail);
228
+ base.titleText = titleBits.join(' • ');
229
+
230
+ return base;
231
+ }
232
+
233
+ var MinionsProjectGitSummary = {
234
+ summarize: summarizeProjectGit,
235
+ liveValidationTypes: _projectLiveValidationTypes,
236
+ checkoutMode: _projectCheckoutMode,
237
+ };
238
+
239
+ if (typeof window !== 'undefined') window.MinionsProjectGitSummary = MinionsProjectGitSummary;
@@ -25,6 +25,113 @@
25
25
  empty.textContent = 'No projects linked yet.';
26
26
  return empty;
27
27
  }
28
+ // ── Project-pill hover detail popover ────────────────────────
29
+ // Parity with the CLASSIC Projects bar (dashboard/js/render-other.js
30
+ // _renderProjectBranch / _renderWorktreeModePill): on hover/focus a pill shows
31
+ // the per-project git + dispatch detail — current branch, "→ main" drift,
32
+ // ahead/behind vs origin/main, dirty working tree, main-drift/behind warnings,
33
+ // and the checkout mode (Worktrees / Live checkout / Hybrid + validation types).
34
+ // All the semantics come from the SHARED, DOM-free summarizer
35
+ // window.MinionsProjectGitSummary.summarize (dashboard/shared/project-git-summary.js)
36
+ // so we don't fork the classic branching logic. Content is built with
37
+ // textContent/createElement only (no innerHTML — SEC-03 ratchet).
38
+ var _pillPopoverEl = null;
39
+ var _pillPopoverEscBound = false;
40
+
41
+ function _ensurePillPopover() {
42
+ if (_pillPopoverEl && document.body.contains(_pillPopoverEl)) return _pillPopoverEl;
43
+ var el = document.createElement('div');
44
+ el.className = 'slim-pill-popover';
45
+ el.id = 'slim-project-pill-popover';
46
+ el.setAttribute('role', 'tooltip');
47
+ el.hidden = true;
48
+ document.body.appendChild(el);
49
+ _pillPopoverEl = el;
50
+ if (!_pillPopoverEscBound) {
51
+ // Escape dismisses the popover (accessible non-blocking tooltip).
52
+ document.addEventListener('keydown', function(ev) {
53
+ if (ev.key === 'Escape' && _pillPopoverEl && !_pillPopoverEl.hidden) hidePillPopover();
54
+ });
55
+ _pillPopoverEscBound = true;
56
+ }
57
+ return el;
58
+ }
59
+
60
+ function _appendPopoverRow(parent, label, value, cls) {
61
+ var row = document.createElement('div');
62
+ row.className = 'slim-pill-popover-row' + (cls ? ' ' + cls : '');
63
+ if (label) {
64
+ var l = document.createElement('span');
65
+ l.className = 'slim-pill-popover-label';
66
+ l.textContent = label;
67
+ row.appendChild(l);
68
+ }
69
+ var v = document.createElement('span');
70
+ v.className = 'slim-pill-popover-value';
71
+ v.textContent = value;
72
+ row.appendChild(v);
73
+ parent.appendChild(row);
74
+ }
75
+
76
+ // Populate + position the popover under `pill` for the given status project
77
+ // object. No-op (and hides any open popover) when there is nothing to show.
78
+ function showPillPopover(pill, project) {
79
+ if (!pill || !project) return;
80
+ var summ = (window.MinionsProjectGitSummary && window.MinionsProjectGitSummary.summarize)
81
+ ? window.MinionsProjectGitSummary.summarize(project)
82
+ : null;
83
+ if (!summ) return;
84
+ var el = _ensurePillPopover();
85
+ el.textContent = '';
86
+
87
+ var header = document.createElement('div');
88
+ header.className = 'slim-pill-popover-header';
89
+ header.textContent = String(project.displayName || project.name || '');
90
+ el.appendChild(header);
91
+
92
+ if (summ.available) {
93
+ summ.lines.forEach(function(line) {
94
+ _appendPopoverRow(el, line.label, line.value);
95
+ });
96
+ if (summ.warn) {
97
+ _appendPopoverRow(el, '', summ.warn.text + ' — ' + summ.warn.detail, 'slim-pill-popover-warn');
98
+ }
99
+ } else if (summ.refreshing) {
100
+ _appendPopoverRow(el, '', summ.message || 'Refreshing…', 'slim-pill-popover-muted');
101
+ } else if (summ.message) {
102
+ _appendPopoverRow(el, '', summ.message, 'slim-pill-popover-muted');
103
+ _appendPopoverRow(el, 'Checkout', summ.mode.label);
104
+ } else {
105
+ // No git branch data (e.g. non-project path) — still show checkout mode.
106
+ _appendPopoverRow(el, 'Checkout', summ.mode.label);
107
+ }
108
+
109
+ // Link the pill to the popover for assistive tech while it's visible.
110
+ pill.setAttribute('aria-describedby', el.id);
111
+ el.hidden = false;
112
+
113
+ // Position below the pill, clamped to the viewport.
114
+ var r = pill.getBoundingClientRect();
115
+ var top = r.bottom + 6;
116
+ var left = r.left;
117
+ var maxLeft = window.innerWidth - el.offsetWidth - 8;
118
+ if (left > maxLeft) left = Math.max(8, maxLeft);
119
+ if (left < 8) left = 8;
120
+ el.style.top = top + 'px';
121
+ el.style.left = left + 'px';
122
+ }
123
+
124
+ function hidePillPopover() {
125
+ if (!_pillPopoverEl) return;
126
+ _pillPopoverEl.hidden = true;
127
+ // Drop stale aria-describedby links on all pills.
128
+ var group = document.getElementById('chat-context-pills');
129
+ if (group) {
130
+ var pills = group.querySelectorAll('.context-pill[aria-describedby]');
131
+ for (var i = 0; i < pills.length; i++) pills[i].removeAttribute('aria-describedby');
132
+ }
133
+ }
134
+
28
135
  // Strip variant: project pills. One selectable pill per project, single-select.
29
136
  // No auto-default: when nothing is explicitly chosen, no pill is active and the
30
137
  // indicator stays in the "Select project" state, so the picker never silently
@@ -32,7 +139,11 @@
32
139
  // choice back off. Returns a container of pill buttons built as real DOM nodes
33
140
  // (no innerHTML — SEC-03 ratchet, see test/unit.test.js DYNAMIC_INNERHTML_BASELINE);
34
141
  // the caller attaches a click listener via event delegation.
35
- function makeContextPills(projects, selected, displayByName) {
142
+ //
143
+ // `projectByName` maps project name → the full /api/status project object so
144
+ // each pill can surface its git + checkout detail on hover/focus (see
145
+ // showPillPopover). It is optional — pills still render name-only without it.
146
+ function makeContextPills(projects, selected, displayByName, projectByName) {
36
147
  var group = document.createElement('span');
37
148
  group.id = 'chat-context-pills';
38
149
  group.className = 'context-pills';
@@ -49,15 +160,37 @@
49
160
  var isActive = p === selected;
50
161
  pill.classList.toggle('active', isActive);
51
162
  pill.setAttribute('aria-checked', isActive ? 'true' : 'false');
163
+ var proj = projectByName && projectByName[p];
164
+ if (proj) {
165
+ // title= is the graceful fallback (works with no JS / reduced-motion);
166
+ // the richer popover is layered on via hover/focus below.
167
+ var summ = (window.MinionsProjectGitSummary && window.MinionsProjectGitSummary.summarize)
168
+ ? window.MinionsProjectGitSummary.summarize(proj)
169
+ : null;
170
+ if (summ && summ.titleText) pill.title = summ.titleText;
171
+ _wirePillHover(pill, proj);
172
+ }
52
173
  group.appendChild(pill);
53
174
  }
54
175
  return group;
55
176
  }
177
+
178
+ // Bind hover + keyboard-focus reveal of the detail popover for one pill.
179
+ // Kept in a helper so the closure captures the correct project object per
180
+ // iteration (the loop uses `var`).
181
+ function _wirePillHover(pill, proj) {
182
+ pill.addEventListener('mouseenter', function() { showPillPopover(pill, proj); });
183
+ pill.addEventListener('mouseleave', hidePillPopover);
184
+ pill.addEventListener('focus', function() { showPillPopover(pill, proj); });
185
+ pill.addEventListener('blur', hidePillPopover);
186
+ }
56
187
  async function loadProjectsAndRenderContext(opts) {
57
188
  var stripEl = document.getElementById('chat-context-strip');
58
189
  var controlsEl = document.getElementById('chat-context-controls');
59
190
  if (!stripEl || !controlsEl) return;
60
191
  var preferProject = (opts && opts.preferProject) || null;
192
+ // Any prior hover popover belongs to pills we're about to replace.
193
+ if (typeof hidePillPopover === 'function') hidePillPopover();
61
194
  try {
62
195
  var res = await fetch('/api/status');
63
196
  if (!res.ok) throw new Error('HTTP ' + res.status);
@@ -66,7 +199,12 @@
66
199
  .filter(function(p) { return p && p.name; });
67
200
  var projects = projectList.map(function(p) { return String(p.name); });
68
201
  var displayByName = {};
69
- projectList.forEach(function(p) { displayByName[String(p.name)] = String(p.displayName || p.name); });
202
+ var projectByName = {};
203
+ projectList.forEach(function(p) {
204
+ var key = String(p.name);
205
+ displayByName[key] = String(p.displayName || p.name);
206
+ projectByName[key] = p;
207
+ });
70
208
  if (projects.length === 0) {
71
209
  stripEl.style.display = '';
72
210
  controlsEl.replaceChildren(makeContextEmptyNode(), makeContextAddBtn());
@@ -84,7 +222,7 @@
84
222
  // deliberately do NOT fall back to projects[0] here (W-mqayzsj3) so CC
85
223
  // turns and project-scoped actions don't silently target the wrong
86
224
  // project.
87
- var pills = makeContextPills(projects, currentProject, displayByName);
225
+ var pills = makeContextPills(projects, currentProject, displayByName, projectByName);
88
226
  stripEl.style.display = '';
89
227
  controlsEl.replaceChildren(pills, makeContextAddBtn());
90
228
  pills.addEventListener('click', function(ev) {
@@ -576,6 +576,48 @@
576
576
  .chat-context-strip .context-pill.active:hover {
577
577
  background: var(--blue);
578
578
  }
579
+ /* Project-pill hover detail popover (parity with the classic Projects bar).
580
+ Fixed-position, pointer-events:none so it never traps the pointer or
581
+ focus — it's a passive tooltip dismissed on mouseleave/blur/Escape. */
582
+ .slim-pill-popover {
583
+ position: fixed;
584
+ z-index: 1200;
585
+ max-width: 320px;
586
+ pointer-events: none;
587
+ background: var(--surface2);
588
+ border: 1px solid var(--border);
589
+ border-radius: var(--radius, 8px);
590
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
591
+ padding: var(--space-2) var(--space-3);
592
+ font-size: var(--text-sm, 12px);
593
+ line-height: 1.4;
594
+ color: var(--text);
595
+ }
596
+ .slim-pill-popover[hidden] { display: none; }
597
+ .slim-pill-popover-header {
598
+ font-weight: 700;
599
+ color: var(--blue);
600
+ margin-bottom: 4px;
601
+ white-space: nowrap;
602
+ overflow: hidden;
603
+ text-overflow: ellipsis;
604
+ }
605
+ .slim-pill-popover-row {
606
+ display: flex;
607
+ gap: var(--space-2);
608
+ align-items: baseline;
609
+ }
610
+ .slim-pill-popover-label {
611
+ color: var(--muted);
612
+ flex: 0 0 auto;
613
+ min-width: 84px;
614
+ }
615
+ .slim-pill-popover-value {
616
+ color: var(--text);
617
+ word-break: break-word;
618
+ }
619
+ .slim-pill-popover-muted .slim-pill-popover-value { color: var(--muted); font-style: italic; }
620
+ .slim-pill-popover-warn .slim-pill-popover-value { color: var(--red); }
579
621
  .chat-context-strip .context-static {
580
622
  color: var(--text);
581
623
  font-weight: 500;
@@ -103,11 +103,11 @@
103
103
  /* Sidebar navigation */
104
104
  .page-layout { display: flex; flex: 1; min-height: 0; overflow: hidden; }
105
105
  .sidebar { width: 150px; min-width: 150px; background: var(--surface); border-right: 1px solid var(--border); padding: var(--space-4) 0; overflow-y: auto; position: sticky; top: 0; }
106
- .sidebar-link { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; color: var(--muted); text-decoration: none; font-size: var(--text-sm); border-left: 3px solid transparent; transition: all var(--transition-fast); cursor: pointer; position: relative; }
106
+ .sidebar-link { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; color: var(--muted); text-decoration: none; font-size: var(--text-md); border-left: 3px solid transparent; transition: all var(--transition-fast); cursor: pointer; position: relative; }
107
107
  .sidebar-link .notif-badge.done { top: 50%; right: 6px; transform: translateY(-50%); width: 6px; height: 6px; }
108
108
  .sidebar-link:hover { color: var(--text); background: var(--surface2); }
109
109
  .sidebar-link.active { color: var(--blue); border-left-color: var(--blue); background: var(--surface2); font-weight: 600; }
110
- .sidebar-count { font-size: var(--text-sm); color: var(--muted); background: var(--surface2); padding: 1px 5px; border-radius: 8px; min-width: 16px; text-align: center; }
110
+ .sidebar-count { font-size: var(--text-md); color: var(--muted); background: var(--surface2); padding: 1px 5px; border-radius: 8px; min-width: 16px; text-align: center; }
111
111
  .page-content { flex: 1; overflow-y: auto; min-width: 0; }
112
112
  .page { display: none; }
113
113
  .page.active { display: block; }
@@ -755,9 +755,9 @@
755
755
  .empty-state-add.empty-state-add-muted { color: var(--muted); }
756
756
  .empty-state-add.empty-state-add-muted:hover { background: var(--surface); }
757
757
  /* Blue variant used for the "Scan" affordance that sits next to "+ Add
758
- Project" in the projects empty-state — same dashed chip, but text-sm and
759
- blue text instead of green. */
760
- .empty-state-add.empty-state-add-blue { color: var(--blue); font-size: var(--text-sm); }
758
+ Project" in the projects empty-state — same dashed chip and inherited
759
+ projects-bar text size, but blue text instead of green. */
760
+ .empty-state-add.empty-state-add-blue { color: var(--blue); }
761
761
  .empty-state-add.empty-state-add-blue:hover { background: rgba(88,166,255,0.1); }
762
762
 
763
763
  /* Badge — unified badge/pill base */
@@ -8,7 +8,7 @@ const shared = require('./engine/shared');
8
8
  const { safeRead } = shared;
9
9
 
10
10
  const MINIONS_DIR = __dirname;
11
- const DASHBOARD_SHARED_JS = ['pr-merge-state', 'pr-filters', 'cc-suggestions', 'model-display', 'watches-source', 'welcome-popup'];
11
+ const DASHBOARD_SHARED_JS = ['pr-merge-state', 'pr-filters', 'cc-suggestions', 'model-display', 'watches-source', 'project-git-summary', 'welcome-popup'];
12
12
 
13
13
  function buildDashboardHtml() {
14
14
  const dashDir = path.join(MINIONS_DIR, 'dashboard');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2430",
3
+ "version": "0.1.2432",
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"