@yemi33/minions 0.1.2321 → 0.1.2323

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.
@@ -41,7 +41,7 @@ two stay in lockstep.
41
41
  | Token | Value | Common use |
42
42
  | ------------------ | ----- | --------------------------------------------------------- |
43
43
  | `--text-xs` | 10px | Smallest legitimate text; sidebar badges, count pills |
44
- | `--text-sm` | 11px | Status badges, chip / tag labels |
44
+ | `--text-sm` | 12px | Status badges, chip / tag labels |
45
45
  | `--text-base` | 12px | Captions, secondary metadata, dense table cells |
46
46
  | `--text-md` | 13px | Default for slim chat / detail tables / button labels |
47
47
  | `--text-lg` | 14px | Body text, modal textarea, card titles |
@@ -57,6 +57,25 @@ two stay in lockstep.
57
57
  > token that breaks the "all px" rule and is explicitly allowlisted in
58
58
  > the tripwire.
59
59
 
60
+ ### 12px readability floor (W-mr3xgejt0008d3a5)
61
+
62
+ `--text-sm` was raised from an 11px base to **12px** so it clears the 12px
63
+ readability floor in both stylesheets: classic renders it at `12 × 1.05 =
64
+ 12.6px`, slim at `12 × 1 = 12px`. `--text-xs` (10px base) stays below the
65
+ floor and is reserved for the very smallest chrome. To lift the
66
+ highest-signal small-text callsites off `--text-xs`, these selectors were
67
+ reassigned to `--text-sm`:
68
+
69
+ - `.sidebar-count` — primary nav count pill
70
+ - `.dispatch-stuck` — uppercase red "stuck" status badge
71
+ - `.pl-node-meta` — muted plan-node secondary text
72
+ - `.error-details-btn` — uppercase error-details button label
73
+
74
+ The `--text-sm` selectors already on the token (`.table th`, `.pr-table th`,
75
+ `.token-agent-table th`, `.status-badge`, `.sidebar-link`, slim
76
+ `.tile-item-subtle`) now clear the floor automatically via the token bump —
77
+ no per-callsite change needed.
78
+
60
79
  ## Role aliases (preferred for new code)
61
80
 
62
81
  Use the role token when adding new UI. It survives a future size-scale
@@ -70,7 +89,7 @@ redesign without callsite edits.
70
89
  | `--text-body` | `--text-lg` (14px) | Default body text, paragraphs |
71
90
  | `--text-meta` | `--text-md` (13px) | Captions, timestamps, secondary metadata |
72
91
  | `--text-caption` | `--text-base` (12px)| Small labels, table cells |
73
- | `--text-micro` | `--text-sm` (11px) | Chip / tag pill labels |
92
+ | `--text-micro` | `--text-sm` (12px) | Chip / tag pill labels |
74
93
  | `--text-code` | (`0.9em`) | Inline code |
75
94
 
76
95
  ## Utility classes
@@ -69,8 +69,24 @@ function _renderProjectBranch(p) {
69
69
  ? ' <span class="muted">→ ' + mainBranch + '</span>'
70
70
  : '';
71
71
  const detachedSuffix = p.gitDetached ? ' <span class="muted">(detached)</span>' : '';
72
+ // opg-microsoft/minions#664 — the counts are always vs origin/<comparator>
73
+ // (configured mainBranch, falling back to remoteDefaultBranch; see
74
+ // engine/queries.js _probeProjectGitStatus), never vs the checked-out
75
+ // branch's own upstream. When branchDiffersFromMain the bare "↑N ↓M" chip
76
+ // reads as ambiguous (looks like it could be vs the current branch's
77
+ // tracking ref), so append the comparator name to the VISIBLE text too —
78
+ // not just the hover title — so the disambiguation doesn't require a
79
+ // hover. Skip the suffix when the checked-out branch IS the comparator
80
+ // since there's nothing to disambiguate.
81
+ const comparatorBranch = mainBranch || remoteDefault;
82
+ // Deliberately separate from branchDiffersFromMain (which only fires when a
83
+ // configured mainBranch is set, driving the "→ main" suffix): the ahead/
84
+ // behind comparator falls back to remoteDefaultBranch too, so the chip must
85
+ // disambiguate against whichever one actually produced the counts.
86
+ const branchDiffersFromComparator = !!(comparatorBranch && p.gitBranch !== (p.mainBranch || p.remoteDefaultBranch));
87
+ const comparatorSuffix = branchDiffersFromComparator ? ' vs ' + comparatorBranch : '';
72
88
  const aheadBehind = (ahead !== null && behind !== null && (ahead > 0 || behind > 0))
73
- ? ' <span class="project-ab" title="' + escHtml('Ahead ' + ahead + ' / behind ' + behind + ' vs origin/' + (p.mainBranch || p.remoteDefaultBranch)) + '">↑' + ahead + ' ↓' + behind + '</span>'
89
+ ? ' <span class="project-ab" title="' + escHtml('Ahead ' + ahead + ' / behind ' + behind + ' vs origin/' + (p.mainBranch || p.remoteDefaultBranch)) + '">↑' + ahead + ' ↓' + behind + comparatorSuffix + '</span>'
74
90
  : '';
75
91
  // Warning chip surfaces when the configured main disagrees with the remote
76
92
  // default branch (the #2732 case: config=master, origin/HEAD=main) OR when
@@ -49,8 +49,14 @@ async function fetchPullRequestsFromDisk(projects) {
49
49
  out.push(pr);
50
50
  }
51
51
  }
52
- // Same sort the engine applies: most recent created first, fall back to id.
52
+ // Same sort the engine applies (engine/queries.js#getPullRequests): group
53
+ // terminal (merged/abandoned/closed) PRs to the bottom, then most recent
54
+ // created first within each group, falling back to id.
55
+ const _isTerminalPr = (pr) => pr.status === 'merged' || pr.status === 'abandoned' || pr.status === 'closed';
53
56
  out.sort((a, b) => {
57
+ const at = _isTerminalPr(a) ? 1 : 0;
58
+ const bt = _isTerminalPr(b) ? 1 : 0;
59
+ if (at !== bt) return at - bt;
54
60
  const ac = a.created || '';
55
61
  const bc = b.created || '';
56
62
  if (ac && bc) return bc.localeCompare(ac);
@@ -196,8 +202,13 @@ function prRow(pr) {
196
202
  // clearly-labeled indicator — a still-red build after an unchanged fix
197
203
  // attempt usually means the failure isn't fixable by another agent pass
198
204
  // (flaky infra, external outage, etc.), hence "infra issue suspected".
199
- // No resume action is wired here (clearing this record is an engine-side
200
- // change out of scope for this UI-only follow-up) — informational only.
205
+ // Issue #671 clicking resumes it via /api/pull-requests/clear-build-fix-ineffective
206
+ // (resumeBuildFixIneffective below), which clears _buildFixIneffective so
207
+ // BUILD_FAILURE auto-fix dispatch can resume. A new push already resets
208
+ // this pause automatically (see shared.isBuildFixIneffectivePaused); the
209
+ // click handler exists for the case where an operator has already
210
+ // confirmed (e.g. a fresh CI run queued against a rebased/retried head)
211
+ // and doesn't want to wait for the next poll to notice.
201
212
  var buildFixIneffective = pr._buildFixIneffective;
202
213
  if (buildFixIneffective && typeof buildFixIneffective === 'object' && buildFixIneffective.paused === true) {
203
214
  var bfiTitle = 'Build-fix reported no branch change, and a live CI re-poll confirmed the build is still "'
@@ -205,11 +216,14 @@ function prRow(pr) {
205
216
  + (buildFixIneffective.headSha || 'unknown head') + ') after '
206
217
  + (buildFixIneffective.count || '?') + ' ineffective attempt(s). Reason: '
207
218
  + (buildFixIneffective.reason || 'unknown')
208
- + '. A new push to this branch will reset this pause.';
209
- pausedChips += ' <span class="pr-badge rejected pr-build-fix-ineffective-chip" '
210
- + 'style="font-size:var(--text-xs)" '
211
- + 'title="' + escapeHtml(bfiTitle) + '">'
212
- + ' infra issue suspected ▶</span>';
219
+ + '. A new push to this branch will reset this pause automatically, or click to resume now.';
220
+ pausedChips += ' <button class="pr-badge rejected pr-build-fix-ineffective-chip" '
221
+ + 'style="font-size:var(--text-xs);cursor:pointer;border:none" '
222
+ + 'title="' + escapeHtml(bfiTitle) + '" '
223
+ + 'data-pr-id="' + escapeHtml(String(pr.id || '')) + '" '
224
+ + 'data-pr-head-sha="' + escapeHtml(String(buildFixIneffective.headSha || '')) + '" '
225
+ + 'onclick="event.stopPropagation();resumeBuildFixIneffective(this)">'
226
+ + '⏸ infra issue suspected ▶</button>';
213
227
  }
214
228
  const titleText = pr.title || 'Untitled';
215
229
  // Polish (W-mqv5ccn7): flatten Markdown in the body so the preview shows
@@ -264,15 +278,23 @@ function prRow(pr) {
264
278
  // ellipsis-truncated content reveals the full text. Cell tags stay bare so
265
279
  // the header-to-cell count assertion in test/unit.test.js continues to
266
280
  // balance.
281
+ // First-column secondary project label (W-mr3mowg9) — mirrors the pr-desc
282
+ // secondary-text pattern used in the Title cell so reviewers can see at a
283
+ // glance which project a PR belongs to. `_project` is stamped by the engine
284
+ // enrichment / getAllPrs stamping (see top-of-file note + line ~33).
285
+ const projectName = pr._project ? String(pr._project) : '';
286
+ const projectLabelHtml = projectName
287
+ ? '<div class="pr-desc pr-id-project" title="' + escapeHtml(projectName) + '">' + escapeHtml(projectName) + '</div>'
288
+ : '';
267
289
  return '<tr>' +
268
- '<td><span class="pr-id" title="' + escapeHtml(String(prId)) + '">' + escapeHtml(prShortId(String(prId))) + '</span></td>' +
290
+ '<td><span class="pr-id" title="' + escapeHtml(String(prId)) + '">' + escapeHtml(prShortId(String(prId))) + '</span>' + projectLabelHtml + '</td>' +
269
291
  '<td><a class="pr-title" title="' + escapeHtml(titleText) + '" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(titleText) + '</a>' + followupChip + pausedChips + (descPreview ? '<div class="pr-desc" title="' + escapeHtml(descPreview) + '">' + escapeHtml(descPreview.length > 120 ? descPreview.slice(0, 120) + '...' : descPreview) + '</div>' : '') + '</td>' +
270
- '<td><span class="pr-agent" title="' + escapeHtml(agentText) + '">' + escapeHtml(agentText) + '</span></td>' +
271
- '<td><span class="' + branchClass + '" title="' + escapeHtml(branchError || branchLabel) + '">' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
292
+ '<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
272
293
  '<td><span class="pr-badge ' + reviewClass + '" title="' + escapeHtml(reviewTitle || reviewLabel) + '">' + escapeHtml(reviewLabel) + '</span></td>' +
294
+ '<td><span class="' + branchClass + '" title="' + escapeHtml(branchError || branchLabel) + '">' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
295
+ '<td><span class="pr-agent" title="' + escapeHtml(agentText) + '">' + escapeHtml(agentText) + '</span></td>' +
273
296
  '<td>' + reviewerCell + '</td>' +
274
297
  '<td><span class="pr-badge ' + buildClass + '" title="' + escapeHtml(buildTitle || buildLabel) + '">' + escapeHtml(buildLabel) + '</span></td>' +
275
- '<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
276
298
  '<td>' + (observeBtn || '<span style="color:var(--muted);font-size:var(--text-base)">—</span>') + '</td>' +
277
299
  '<td><span class="pr-date" title="' + escapeHtml(createdLabel) + '">' + escapeHtml(createdLabel) + '</span></td>' +
278
300
  '<td><button class="pr-info-pill" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();openPrDetail(this.dataset.prId)" title="Open PR detail (in-stack modal)">&#x2197;</button>' +
@@ -286,14 +308,14 @@ function prRow(pr) {
286
308
  // the viewport (sticky scrollbar — see styles.css).
287
309
  const PRS_COLGROUP =
288
310
  '<colgroup>' +
289
- '<col style="width:110px">' + // PR id (short label: `gh #481` / `ado #5383607`)
311
+ '<col style="width:130px">' + // PR id (short label: `gh #481` / `ado #5383607`) + project label
290
312
  '<col style="width:320px">' + // Title
291
- '<col style="width:140px">' + // Agent
292
- '<col style="width:200px">' + // Branch
313
+ '<col style="width:110px">' + // Status
293
314
  '<col style="width:130px">' + // Review
315
+ '<col style="width:200px">' + // Branch
316
+ '<col style="width:140px">' + // Agent
294
317
  '<col style="width:140px">' + // Signed Off By
295
318
  '<col style="width:130px">' + // Build
296
- '<col style="width:110px">' + // Status
297
319
  '<col style="width:100px">' + // Observe
298
320
  '<col style="width:130px">' + // Created
299
321
  '<col style="width:80px">' + // Actions (info-pill + remove)
@@ -303,7 +325,7 @@ function prTableHtml(rows) {
303
325
  return '<div class="pr-table-wrap pr-table-wrap--prs"><table class="pr-table pr-table--prs">' +
304
326
  PRS_COLGROUP +
305
327
  '<thead><tr>' +
306
- '<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Observe</th><th>Created</th><th></th>' +
328
+ '<th>PR</th><th>Title</th><th>Status</th><th>Review</th><th>Branch</th><th>Agent</th><th>Signed Off By</th><th>Build</th><th>Observe</th><th>Created</th><th></th>' +
307
329
  '</tr></thead><tbody>' + rows + '</tbody></table></div>';
308
330
  }
309
331
 
@@ -735,4 +757,38 @@ async function resumePausedCause(btn) {
735
757
  }
736
758
  }
737
759
 
738
- window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, openPrDetail, unlinkPr, togglePrObserve, resumePausedCause };
760
+ // Issue #671 resume a paused build-fix-ineffective ("infra issue
761
+ // suspected") pause. Mirrors resumePausedCause above but targets the
762
+ // headSha-keyed _buildFixIneffective record via its own endpoint. Sends the
763
+ // headSha the chip was rendered with so the server can reject the clear as
764
+ // stale if the pause has since moved to a different commit.
765
+ async function resumeBuildFixIneffective(btn) {
766
+ if (!btn || btn.disabled) return;
767
+ const prId = btn.dataset.prId;
768
+ const headSha = btn.dataset.prHeadSha || '';
769
+ if (!prId) return;
770
+ if (!confirm('Resume build-fix automation on ' + prId + '?\n\nThis clears the "infra issue suspected" pause so the engine can re-dispatch build fixes on the next discovery pass.')) return;
771
+ btn.disabled = true;
772
+ const prevDisplay = btn.style.display;
773
+ btn.style.display = 'none';
774
+ showToast('pr-toast', 'Resumed build-fix automation on ' + prId, true);
775
+ try {
776
+ const res = await fetch('/api/pull-requests/clear-build-fix-ineffective', {
777
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
778
+ body: JSON.stringify({ prId, headSha })
779
+ });
780
+ if (!res.ok) {
781
+ const d = await res.json().catch(() => ({}));
782
+ btn.style.display = prevDisplay;
783
+ btn.disabled = false;
784
+ showToast('pr-toast', 'Failed: ' + (d.error || 'unknown'), false);
785
+ return;
786
+ }
787
+ } catch (e) {
788
+ btn.style.display = prevDisplay;
789
+ btn.disabled = false;
790
+ showToast('pr-toast', 'Error: ' + e.message, false);
791
+ }
792
+ }
793
+
794
+ window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, openPrDetail, unlinkPr, togglePrObserve, resumePausedCause, resumeBuildFixIneffective };
@@ -231,7 +231,12 @@
231
231
  the standard TILE_VIEWS path in modals-tiles.js#renderPlansBody. There is no
232
232
  slim-specific Plans/PRD flyout markup anymore (W-mr28ko750010199f / "reuse,
233
233
  don't fork"): the classic /plans screen is the single source of truth for the
234
- Plans + PRD tabs and every lifecycle action. -->
234
+ Plans + PRD tabs and every lifecycle action.
235
+ The "Active PRs" cockpit tile follows the same iframe-embed pattern
236
+ (W-mr3l7y0m0007102a): it opens the LITERAL classic /prs screen in a chrome-off
237
+ iframe (/prs?embed=1) via modals-tiles.js#renderPrsBody, so the full PR
238
+ filter/sort UI + row actions (comment/track/link/pr-action) are the classic
239
+ screen — no slim-specific PR list to maintain. -->
235
240
 
236
241
  <!-- Pin editor — create (Pin Content action / "+ Pin") or edit an existing
237
242
  note. Submits to POST /api/pinned (create) or /api/pinned/update (edit). -->
@@ -81,17 +81,6 @@
81
81
  return card;
82
82
  }
83
83
 
84
- // Parse the org/project slug from a canonical PR id, dropping the host prefix
85
- // and the trailing "#<num>". E.g. "ado:office/iss/constellation#5315603" →
86
- // "office/iss/constellation", "github:yemi33/minions#2911" → "yemi33/minions".
87
- function parsePrSlug(id) {
88
- if (!id || typeof id !== 'string') return '';
89
- var hash = id.indexOf('#');
90
- var head = hash >= 0 ? id.slice(0, hash) : id;
91
- var colon = head.indexOf(':');
92
- return colon >= 0 ? head.slice(colon + 1) : head;
93
- }
94
-
95
84
  // Per-tile body renderers for the detail modal. Each reads the latest status
96
85
  // snapshot and fills the modal body; openTileModal looks them up by key.
97
86
  function renderEngineTileBody(body, data) {
@@ -160,107 +149,23 @@
160
149
  body.appendChild(frame);
161
150
  }
162
151
 
163
- function renderPrTileBody(body, data) {
164
- // Emoji glyphs make build/review status scannable at a glance; the
165
- // accompanying label keeps the row readable when the glyph alone is
166
- // ambiguous (✅ vs ✅, vs ❌, vs 💬). The raw status string also
167
- // stays in the title attribute for hover discoverability.
168
- // Keyed by normalized (lowercased) status; missing/unknown ➖.
169
- var BUILD_STATUS = {
170
- success: { glyph: '✅', label: 'passing' },
171
- succeeded: { glyph: '✅', label: 'passing' },
172
- passing: { glyph: '✅', label: 'passing' },
173
- failing: { glyph: '❌', label: 'failing' },
174
- failed: { glyph: '❌', label: 'failing' },
175
- error: { glyph: '❌', label: 'failing' },
176
- pending: { glyph: '', label: 'pending' },
177
- running: { glyph: '', label: 'running' },
178
- queued: { glyph: '⏳', label: 'queued' },
179
- in_progress: { glyph: '⏳', label: 'in progress' },
180
- };
181
- var REVIEW_STATUS = {
182
- approved: { glyph: '✅', label: 'approved' },
183
- 'changes-requested': { glyph: '❌', label: 'changes requested' },
184
- changes_requested: { glyph: '❌', label: 'changes requested' },
185
- 'requested-changes': { glyph: '❌', label: 'changes requested' },
186
- rejected: { glyph: '❌', label: 'rejected' },
187
- pending: { glyph: '💬', label: 'pending' },
188
- commented: { glyph: '💬', label: 'commented' },
189
- };
190
- function statusSpan(subsystem, raw, table) {
191
- var info = table[String(raw || '').toLowerCase()];
192
- var glyph = info ? info.glyph : '➖';
193
- var label = info ? info.label : (raw ? String(raw) : 'none');
194
- var span = document.createElement('span');
195
- span.className = 'pr-status';
196
- span.textContent = glyph + ' ' + subsystem + ' ' + label;
197
- span.title = subsystem + ': ' + (raw || 'unknown');
198
- return span;
199
- }
200
- var prs = Array.isArray(data.pullRequests) ? data.pullRequests : [];
201
- if (!prs.length) { tileEmpty(body, 'No tracked pull requests.'); return; }
202
- function prCard(p) {
203
- var num = p.prNumber || p.number || p.id;
204
- var build = p.buildStatus || '';
205
- var review = p.reviewStatus || '';
206
- var cls = p.status === 'merged' ? 'green'
207
- : (build === 'failing' ? 'red'
208
- : ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
209
- var statusText = document.createElement('span');
210
- statusText.textContent = p.status || 'unknown';
211
- return tileCard({
212
- title: (num ? '#' + num + ' ' : '') + (p.title || '(untitled PR)'),
213
- // Project slug first (under title), then build/review row below — the
214
- // project is the strongest disambiguator across PRs and benefits from
215
- // being adjacent to the title.
216
- subtle: parsePrSlug(p.id),
217
- subtleBeforeMeta: true,
218
- metaNodes: [statusText, statusSpan('build', build, BUILD_STATUS), statusSpan('review', review, REVIEW_STATUS)],
219
- chip: { text: p.status || '—', cls: cls },
220
- href: p.url || null,
221
- });
222
- }
223
- // Partition once into three fixed buckets (status normalized to lowercase),
224
- // preserving each PR's relative order within its bucket. Mirrors shared
225
- // PR_STATUS (active/merged/abandoned/closed/linked); anything that does not
226
- // match the merged/abandoned buckets defaults to Active so a PR with an
227
- // unexpected status never silently disappears.
228
- var buckets = { active: [], merged: [], abandoned: [] };
229
- prs.forEach(function(p) {
230
- var s = String(p.status || '').toLowerCase();
231
- if (s === 'abandoned') buckets.abandoned.push(p);
232
- else if (s === 'merged' || s === 'completed' || s === 'closed') buckets.merged.push(p);
233
- else buckets.active.push(p);
234
- });
235
- // Fixed section order: Active (open) → Merged / Completed → Abandoned.
236
- // Empty sections are omitted entirely.
237
- var sections = [
238
- { label: 'Active', prs: buckets.active, open: true },
239
- { label: 'Merged / Completed', prs: buckets.merged, open: false },
240
- { label: 'Abandoned', prs: buckets.abandoned, open: false },
241
- ];
242
- sections.forEach(function(section) {
243
- if (!section.prs.length) return;
244
- var details = document.createElement('details');
245
- details.className = 'tile-section';
246
- if (section.open) details.open = true;
247
- var summary = document.createElement('summary');
248
- summary.className = 'tile-section-summary';
249
- var label = document.createElement('span');
250
- label.className = 'tile-section-label';
251
- label.textContent = section.label;
252
- var count = document.createElement('span');
253
- count.className = 'tile-section-count';
254
- count.textContent = '(' + section.prs.length + ')';
255
- summary.appendChild(label);
256
- summary.appendChild(count);
257
- details.appendChild(summary);
258
- var sectionBody = document.createElement('div');
259
- sectionBody.className = 'tile-section-body';
260
- section.prs.forEach(function(p) { sectionBody.appendChild(prCard(p)); });
261
- details.appendChild(sectionBody);
262
- body.appendChild(details);
263
- });
152
+ // Pull-requests tile body reuses the LITERAL classic dashboard /prs screen
153
+ // instead of a slim-specific bucketed PR list, so there is one PRs UI, not two
154
+ // (W-mr3l7y0m0007102a / "reuse, don't fork" mirrors renderQueuedWorkBody for
155
+ // the Work tile and renderPlansBody for the Plans tile). Slim and classic are
156
+ // two separate IIFE bundles with no shared scope, so we embed the real /prs
157
+ // screen in an iframe with the chrome-off ?embed=1 mode. The iframed page IS
158
+ // the classic screen — the full PR list with every filter/sort + row action
159
+ // (comment/track/link/pr-action), backed by the same /api/status pullRequests
160
+ // data + endpoints with zero duplicated rendering logic. Classic is reachable
161
+ // at /prs even with slim-ux ON (only / is taken over). Built with createElement
162
+ // (no innerHTML) to satisfy the dashboard no-unsanitized lint gate.
163
+ function renderPrsBody(body) {
164
+ var frame = document.createElement('iframe');
165
+ frame.className = 'slim-prs-embed';
166
+ frame.src = '/prs?embed=1';
167
+ frame.title = 'Pull Requests';
168
+ body.appendChild(frame);
264
169
  }
265
170
 
266
171
  function renderWatchTileBody(body, data) {
@@ -283,7 +188,7 @@
283
188
  dispatches: { title: 'Active dispatches', render: function(body, data) { renderDispatchTileBody(body, data, true); } },
284
189
  queued: { title: 'Queued work', render: function(body) { renderQueuedWorkBody(body); } },
285
190
  plans: { title: 'Plans', render: function(body) { renderPlansBody(body); } },
286
- prs: { title: 'Pull requests', render: renderPrTileBody },
191
+ prs: { title: 'Pull requests', render: function(body) { renderPrsBody(body); } },
287
192
  watches: { title: 'Watches', render: renderWatchTileBody },
288
193
  };
289
194
 
@@ -304,11 +209,13 @@
304
209
  // The "+ Link PR" header chip only applies to the PR list.
305
210
  var headerChip = document.getElementById('slim-tile-modal-linkpr');
306
211
  if (headerChip) headerChip.style.display = (key === 'prs') ? '' : 'none';
307
- // The queued + plans tiles embed a full classic screen (/work, /plans) in an
308
- // iframe — widen the modal + drop the body padding so the embedded screen
309
- // gets real estate. Both reuse the shared wide-modal treatment.
212
+ // The queued, plans + prs tiles embed a full classic screen (/work, /plans,
213
+ // /prs) in an iframe — widen the modal + drop the body padding so the
214
+ // embedded screen gets real estate. All three reuse the shared wide-modal
215
+ // treatment.
310
216
  modal.classList.toggle('tile-modal--work', key === 'queued');
311
217
  modal.classList.toggle('tile-modal--plans', key === 'plans');
218
+ modal.classList.toggle('tile-modal--prs', key === 'prs');
312
219
  view.render(body, lastStatusData || {});
313
220
  modal.classList.add('open');
314
221
  }
@@ -42,7 +42,7 @@
42
42
  the slim UI at its current text size — the global bump is excluded
43
43
  here by design. Do NOT raise this value to match main. */
44
44
  --minions-text-scale: 1;
45
- --text-xs: calc(10px * var(--minions-text-scale)); --text-sm: calc(11px * var(--minions-text-scale)); --text-base: calc(12px * var(--minions-text-scale));
45
+ --text-xs: calc(10px * var(--minions-text-scale)); --text-sm: calc(12px * var(--minions-text-scale)); --text-base: calc(12px * var(--minions-text-scale));
46
46
  --text-md: calc(13px * var(--minions-text-scale)); --text-lg: calc(14px * var(--minions-text-scale)); --text-xl: calc(16px * var(--minions-text-scale)); --text-2xl: calc(18px * var(--minions-text-scale));
47
47
  --text-stat: calc(22px * var(--minions-text-scale)); --text-stat-lg: calc(28px * var(--minions-text-scale)); --text-display: calc(32px * var(--minions-text-scale));
48
48
  --text-role-display: var(--text-display);
@@ -680,15 +680,19 @@
680
680
  /* Cockpit-tile detail modal: list of dispatches / PRs / watches, or a
681
681
  key/value engine readout (reuses .agent-detail-row). */
682
682
  #slim-tile-modal .modal { width: 720px; max-width: calc(100vw - 32px); }
683
- /* W-mqrdggys000f94a4 / W-mr28ko750010199f the Queued-work + Plans tiles
684
- embed a full classic screen (/work, /plans) in an iframe; widen the modal +
685
- remove body padding so the embedded screen gets the full width/height. */
683
+ /* W-mqrdggys000f94a4 / W-mr28ko750010199f / W-mr3l7y0m0007102a the
684
+ Queued-work + Plans + PRs tiles embed a full classic screen (/work,
685
+ /plans, /prs) in an iframe; widen the modal + remove body padding so the
686
+ embedded screen gets the full width/height. */
686
687
  #slim-tile-modal.tile-modal--work .modal,
687
- #slim-tile-modal.tile-modal--plans .modal { width: 1180px; }
688
+ #slim-tile-modal.tile-modal--plans .modal,
689
+ #slim-tile-modal.tile-modal--prs .modal { width: 1180px; }
688
690
  #slim-tile-modal.tile-modal--work .modal-body,
689
- #slim-tile-modal.tile-modal--plans .modal-body { padding: 0; }
691
+ #slim-tile-modal.tile-modal--plans .modal-body,
692
+ #slim-tile-modal.tile-modal--prs .modal-body { padding: 0; }
690
693
  .slim-work-embed,
691
- .slim-plans-embed {
694
+ .slim-plans-embed,
695
+ .slim-prs-embed {
692
696
  display: block; width: 100%; height: calc(100vh - 160px); min-height: 360px;
693
697
  border: 0; background: var(--bg);
694
698
  }
@@ -711,7 +715,6 @@
711
715
  }
712
716
  .tile-item-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; word-break: break-word; }
713
717
  .tile-item-subtle { font-size: var(--text-sm); color: var(--muted); margin-top: 2px; word-break: break-word; }
714
- .pr-status { display: inline-block; margin: 0 1px; text-decoration: none; }
715
718
  .tile-chip {
716
719
  flex: 0 0 auto;
717
720
  font-size: var(--text-base); font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
@@ -724,26 +727,6 @@
724
727
  .tile-chip.blue { background: rgba(88, 166, 255, 0.12); color: var(--blue); border-color: var(--blue); }
725
728
  .tile-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); }
726
729
 
727
- /* Collapsible PR sections (Active / Merged-Completed / Abandoned). Uses
728
- native <details>/<summary> — no JS wiring needed for the toggle. */
729
- .tile-section { margin-bottom: 10px; }
730
- .tile-section:last-child { margin-bottom: 0; }
731
- .tile-section-summary {
732
- display: flex; align-items: center; gap: 6px;
733
- cursor: pointer; user-select: none;
734
- padding: 4px 2px; margin-bottom: 6px;
735
- list-style: none;
736
- font-size: var(--text-base); color: var(--muted);
737
- }
738
- .tile-section-summary::-webkit-details-marker { display: none; }
739
- .tile-section-summary::before {
740
- content: "▸"; color: var(--muted); font-size: var(--text-sm);
741
- transition: transform 0.15s ease;
742
- }
743
- .tile-section[open] > .tile-section-summary::before { content: "▾"; }
744
- .tile-section-label { font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; color: var(--text); }
745
- .tile-section-count { color: var(--muted); }
746
-
747
730
  /* Pinned-context list rows (Knowledge modal → Pinned Context tab). */
748
731
  .pinned-row {
749
732
  border: 1px solid var(--border);
@@ -28,7 +28,7 @@
28
28
  its own (un-scaled) factor of 1, so the slim UI is excluded from the
29
29
  bump by construction. */
30
30
  --minions-text-scale: 1.05;
31
- --text-xs: calc(10px * var(--minions-text-scale)); --text-sm: calc(11px * var(--minions-text-scale)); --text-base: calc(12px * var(--minions-text-scale));
31
+ --text-xs: calc(10px * var(--minions-text-scale)); --text-sm: calc(12px * var(--minions-text-scale)); --text-base: calc(12px * var(--minions-text-scale));
32
32
  --text-md: calc(13px * var(--minions-text-scale)); --text-lg: calc(14px * var(--minions-text-scale)); --text-xl: calc(16px * var(--minions-text-scale)); --text-2xl: calc(18px * var(--minions-text-scale));
33
33
  --text-stat: calc(22px * var(--minions-text-scale)); --text-stat-lg: calc(28px * var(--minions-text-scale)); --text-display: calc(32px * var(--minions-text-scale));
34
34
 
@@ -45,7 +45,7 @@
45
45
  --text-body: var(--text-lg); /* 14px — default body text */
46
46
  --text-meta: var(--text-md); /* 13px — captions, timestamps, secondary metadata */
47
47
  --text-caption: var(--text-base); /* 12px — small labels, table cells */
48
- --text-micro: var(--text-sm); /* 11px — chip / tag pill labels */
48
+ --text-micro: var(--text-sm); /* 12px — chip / tag pill labels */
49
49
  --text-code: 0.9em; /* inline code; relative so it scales with the surrounding text */
50
50
 
51
51
  /* Border radius */
@@ -107,7 +107,7 @@
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-xs); 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-sm); 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; }
@@ -264,7 +264,7 @@
264
264
  .pl-node-box.condition { border-style: dashed; }
265
265
  .pl-node-box.pl-node-stop { border-style: dotted; border-color: var(--muted); opacity: 0.7; }
266
266
  .pl-node-arrow { display: flex; align-items: center; padding: 0 2px; color: var(--muted); font-size: var(--text-md); flex-shrink: 0; margin-top: 8px; }
267
- .pl-node-meta { font-size: var(--text-xs); color: var(--muted); margin-top: 2px; text-align: center; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
267
+ .pl-node-meta { font-size: var(--text-sm); color: var(--muted); margin-top: 2px; text-align: center; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
268
268
  .pl-node-loop { font-size: var(--text-sm); color: var(--muted); margin-top: 6px; display: flex; align-items: center; gap: 6px; }
269
269
  .pipeline-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 12px 16px; margin-bottom: var(--space-4); cursor: pointer; transition: border-color var(--transition-fast), transform var(--transition-fast); }
270
270
  .pipeline-card:hover { border-color: var(--blue); transform: translateY(-1px); }
@@ -358,6 +358,7 @@
358
358
  .pr-title:hover { text-decoration: underline; }
359
359
  .pr-desc { color: var(--muted); font-size: var(--text-sm); font-weight: 400; margin-top: 2px; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 400px; }
360
360
  .pr-id { color: var(--muted); font-family: Consolas, monospace; font-size: var(--text-base); }
361
+ .pr-id-project { max-width: 120px; font-size: var(--text-xs); }
361
362
  .pr-agent { font-size: var(--text-base); color: var(--text); }
362
363
  .pr-branch { display: inline-block; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; font-family: Consolas, monospace; font-size: var(--text-sm); color: var(--muted); background: var(--bg); padding: var(--space-1) var(--space-3); border-radius: 3px; border: 1px solid var(--border); }
363
364
  .pr-branch.branch-missing { color: var(--red); border-color: var(--red); background: rgba(248,81,73,0.12); }
@@ -377,7 +378,7 @@
377
378
  .pr-badge.build-escalated { background: rgba(248,81,73,0.25); color: var(--red); border: 2px solid var(--red); font-weight: 600; }
378
379
  /* W-mq08kuog001110a6 — Needs-Attention chip for stuck pending work items. */
379
380
  .pr-badge.needs-attention { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); font-weight: 600; cursor: help; }
380
- .error-details-btn { font-size: var(--text-xs); padding: var(--space-1) var(--space-3); margin-left: var(--space-2); background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); border-radius: var(--radius-lg); cursor: pointer; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
381
+ .error-details-btn { font-size: var(--text-sm); padding: var(--space-1) var(--space-3); margin-left: var(--space-2); background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); border-radius: var(--radius-lg); cursor: pointer; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
381
382
  .error-details-btn:hover { background: rgba(248,81,73,0.3); }
382
383
  .pr-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); padding: var(--space-6) 0; }
383
384
  .pr-pager { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
@@ -670,7 +671,8 @@
670
671
  in render-other.js, but with a dashed border + green text to read as
671
672
  a placeholder/call-to-action. */
672
673
  .empty-state-add {
673
- display: inline-block;
674
+ display: inline-flex; align-items: center; justify-content: center;
675
+ min-height: 24px; min-width: 24px;
674
676
  background: var(--surface2);
675
677
  border: 1px dashed var(--border);
676
678
  color: var(--green);
@@ -786,7 +788,15 @@
786
788
  .cmd-chip.priority-medium { color: var(--yellow); border-color: rgba(210,153,34,0.3); background: rgba(210,153,34,0.08); }
787
789
  .cmd-chip.priority-low { color: var(--muted); }
788
790
  .cmd-chip.project-chip { color: var(--green); border-color: rgba(63,185,80,0.3); background: rgba(63,185,80,0.08); }
789
- .cmd-chip .chip-x { cursor: pointer; opacity: 0.5; margin-left: 2px; font-size: var(--text-lg); }
791
+ /* Close/remove affordance on command chips. Sized as a >=24x24px hit target
792
+ (W-mr3xgo9a a11y sweep) via inline-flex box + min dimensions rather than
793
+ relying on the bare glyph; the glyph stays centered. Negative vertical
794
+ margins keep the enlarged tap box from inflating the parent chip's height. */
795
+ .cmd-chip .chip-x {
796
+ display: inline-flex; align-items: center; justify-content: center;
797
+ min-width: 24px; min-height: 24px; margin: -4px -4px -4px 2px;
798
+ cursor: pointer; opacity: 0.5; font-size: var(--text-lg);
799
+ }
790
800
  .cmd-chip .chip-x:hover { opacity: 1; }
791
801
  .cmd-chip select {
792
802
  background: transparent; border: none; color: inherit; font-size: var(--text-base);
@@ -978,7 +988,7 @@
978
988
  .dispatch-agent { font-weight: 600; color: var(--text); }
979
989
  .dispatch-task { flex: 1; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
980
990
  .dispatch-time { font-size: var(--text-sm); color: var(--border); }
981
- .dispatch-stuck { font-size: var(--text-xs); font-weight: 700; padding: var(--space-1) var(--space-3); border-radius: var(--radius-sm); text-transform: uppercase; letter-spacing: 0.3px; background: rgba(248,81,73,0.18); color: var(--red); border: 1px dashed var(--red); margin-left: var(--space-2); animation: pulse 1.5s infinite; cursor: help; }
991
+ .dispatch-stuck { font-size: var(--text-sm); font-weight: 700; padding: var(--space-1) var(--space-3); border-radius: var(--radius-sm); text-transform: uppercase; letter-spacing: 0.3px; background: rgba(248,81,73,0.18); color: var(--red); border: 1px dashed var(--red); margin-left: var(--space-2); animation: pulse 1.5s infinite; cursor: help; }
982
992
 
983
993
  /* Engine Log */
984
994
  .log-list { max-height: 280px; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-1); }