@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.
- package/dashboard/js/render-other.js +7 -0
- package/dashboard/js/settings.js +2 -2
- package/dashboard/shared/project-git-summary.js +239 -0
- package/dashboard/slim/js/projects.js +141 -3
- package/dashboard/slim/styles.css +42 -0
- package/dashboard/styles.css +5 -5
- package/dashboard-build.js +1 -1
- package/package.json +1 -1
|
@@ -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>';
|
package/dashboard/js/settings.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/dashboard/styles.css
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
|
759
|
-
blue text instead of green. */
|
|
760
|
-
.empty-state-add.empty-state-add-blue { color: var(--blue);
|
|
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 */
|
package/dashboard-build.js
CHANGED
|
@@ -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.
|
|
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"
|