@yemi33/minions 0.1.2395 → 0.1.2397

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.
@@ -141,7 +141,8 @@ const RENDER_VERSIONS = {
141
141
  projects: 9,
142
142
  notes: 1,
143
143
  prd: 3,
144
- prs: 4,
144
+ // Bumped 4→5 for shared author/lifecycle/project/review/build filters.
145
+ prs: 5,
145
146
  archivedPrds: 1,
146
147
  engine: 4,
147
148
  version: 1,
@@ -1,8 +1,24 @@
1
1
  // render-prs.js — PR tracker rendering functions extracted from dashboard.html
2
2
 
3
3
  let allPrs = [];
4
+ let filteredPrs = [];
4
5
  let prPage = 0;
6
+ let prFilterOptionsSignature = '';
5
7
  const PR_PER_PAGE = 25;
8
+ const PR_FILTER_CONTROL_IDS = {
9
+ author: 'pr-filter-author',
10
+ lifecycle: 'pr-filter-lifecycle',
11
+ project: 'pr-filter-project',
12
+ review: 'pr-filter-review',
13
+ build: 'pr-filter-build',
14
+ };
15
+ const PR_FILTER_ALL_LABELS = {
16
+ author: 'All authors',
17
+ lifecycle: 'All lifecycle statuses',
18
+ project: 'All projects',
19
+ review: 'All review statuses',
20
+ build: 'All build statuses',
21
+ };
6
22
 
7
23
  // Fetch each project's pull-requests.json straight off disk through the
8
24
  // /state/projects/<name>/pull-requests.json static-file passthrough. This
@@ -373,32 +389,105 @@ function _restorePrScrollState(el, state) {
373
389
  tableWrap.scrollTop = state.top || 0;
374
390
  }
375
391
 
392
+ function _syncPrFilterOptions(prs) {
393
+ const options = getPrFilterOptions(prs);
394
+ const controls = Object.keys(PR_FILTER_CONTROL_IDS).map(function(dimension) {
395
+ return {
396
+ dimension,
397
+ select: document.getElementById(PR_FILTER_CONTROL_IDS[dimension]),
398
+ };
399
+ }).filter(function(entry) { return entry.select; });
400
+ if (!controls.length) return;
401
+ const signature = JSON.stringify(options);
402
+ if (signature === prFilterOptionsSignature) return;
403
+ prFilterOptionsSignature = signature;
404
+ controls.forEach(function(entry) {
405
+ const dimension = entry.dimension;
406
+ const select = entry.select;
407
+ const selected = select.value;
408
+ const all = document.createElement('option');
409
+ all.value = '';
410
+ all.textContent = PR_FILTER_ALL_LABELS[dimension];
411
+ const nodes = [all].concat((options[dimension] || []).map(function(option) {
412
+ const node = document.createElement('option');
413
+ node.value = option.value;
414
+ node.textContent = option.label;
415
+ return node;
416
+ }));
417
+ select.replaceChildren(...nodes);
418
+ if (nodes.some(function(node) { return node.value === selected; })) select.value = selected;
419
+ });
420
+ }
421
+
422
+ function _readPrFilters() {
423
+ const filters = {};
424
+ Object.keys(PR_FILTER_CONTROL_IDS).forEach(function(dimension) {
425
+ const select = document.getElementById(PR_FILTER_CONTROL_IDS[dimension]);
426
+ filters[dimension] = select ? select.value : '';
427
+ });
428
+ return filters;
429
+ }
430
+
431
+ function _hasActivePrFilters(filters) {
432
+ return Object.keys(PR_FILTER_CONTROL_IDS).some(function(dimension) {
433
+ return Boolean(filters[dimension]);
434
+ });
435
+ }
436
+
437
+ function applyPrFilters() {
438
+ prPage = 0;
439
+ renderPrs(allPrs, { preserveScroll: false });
440
+ }
441
+
442
+ function resetPrFilters() {
443
+ Object.keys(PR_FILTER_CONTROL_IDS).forEach(function(dimension) {
444
+ const select = document.getElementById(PR_FILTER_CONTROL_IDS[dimension]);
445
+ if (select) select.value = '';
446
+ });
447
+ applyPrFilters();
448
+ }
449
+
376
450
  function renderPrs(prs, opts) {
377
451
  opts = opts || {};
378
- prs = prs.filter(p => !isDeleted('pr:' + p.id));
452
+ prs = Array.isArray(prs) ? prs.filter(p => p && !isDeleted('pr:' + p.id)) : [];
379
453
  allPrs = prs;
454
+ _syncPrFilterOptions(allPrs);
455
+ const filters = _readPrFilters();
456
+ filteredPrs = filterPullRequests(allPrs, filters);
380
457
  const el = document.getElementById('pr-content');
381
458
  const count = document.getElementById('pr-count');
382
- count.textContent = prs.length;
383
- if (!prs.length) {
459
+ const summary = document.getElementById('pr-filter-summary');
460
+ count.textContent = filteredPrs.length;
461
+ if (summary) {
462
+ summary.textContent = _hasActivePrFilters(filters)
463
+ ? 'Showing ' + filteredPrs.length + ' of ' + allPrs.length
464
+ : '';
465
+ }
466
+ if (!allPrs.length) {
467
+ prPage = 0;
384
468
  el.innerHTML = '<p class="pr-empty">No pull requests yet. PRs created by agents will appear here with review, build, and merge status.</p>';
385
469
  return;
386
470
  }
387
- const totalPages = Math.ceil(prs.length / PR_PER_PAGE);
471
+ if (!filteredPrs.length) {
472
+ prPage = 0;
473
+ el.innerHTML = '<p class="pr-empty">No pull requests match the selected filters.</p>';
474
+ return;
475
+ }
476
+ const totalPages = Math.ceil(filteredPrs.length / PR_PER_PAGE);
388
477
  const previousPage = prPage;
389
478
  if (prPage >= totalPages) prPage = totalPages - 1;
390
479
  const start = prPage * PR_PER_PAGE;
391
- const pagePrs = prs.slice(start, start + PR_PER_PAGE);
480
+ const pagePrs = filteredPrs.slice(start, start + PR_PER_PAGE);
392
481
  const rows = pagePrs.map(prRow).join('');
393
482
 
394
483
  let pager = '';
395
- if (prs.length > PR_PER_PAGE) {
484
+ if (filteredPrs.length > PR_PER_PAGE) {
396
485
  pager = '<div class="pr-pager">' +
397
- '<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+PR_PER_PAGE, prs.length) + ' of ' + prs.length + '</span>' +
486
+ '<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+PR_PER_PAGE, filteredPrs.length) + ' of ' + filteredPrs.length + '</span>' +
398
487
  '<div class="pr-pager-btns">' +
399
488
  '<button class="pr-pager-btn ' + (prPage === 0 ? 'disabled' : '') + '" onclick="prPrev()">Prev</button>' +
400
489
  '<button class="pr-pager-btn ' + (prPage >= totalPages-1 ? 'disabled' : '') + '" onclick="prNext()">Next</button>' +
401
- '<button class="pr-pager-btn see-all" onclick="openAllPrs()">See all ' + prs.length + ' PRs</button>' +
490
+ '<button class="pr-pager-btn see-all" onclick="openAllPrs()">See all ' + filteredPrs.length + ' PRs</button>' +
402
491
  '</div>' +
403
492
  '</div>';
404
493
  }
@@ -412,14 +501,14 @@ function renderPrs(prs, opts) {
412
501
  }
413
502
 
414
503
  function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs, { preserveScroll: false }); } }
415
- function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs, { preserveScroll: false }); } }
504
+ function prNext() { const totalPages = Math.ceil(filteredPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs, { preserveScroll: false }); } }
416
505
 
417
506
  function openAllPrs() {
418
507
  const modalEl = document.querySelector('#modal .modal');
419
508
  if (modalEl) modalEl.classList.add('modal-wide');
420
- document.getElementById('modal-title').textContent = 'All Pull Requests (' + allPrs.length + ')';
509
+ document.getElementById('modal-title').textContent = 'All Pull Requests (' + filteredPrs.length + ')';
421
510
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all PR user data wrapped in escapeHtml() by prRow() (fields: PR id, title, description, agent, branch, review/build/status labels)
422
- document.getElementById('modal-body').innerHTML = prTableHtml(allPrs.map(prRow).join(''));
511
+ document.getElementById('modal-body').innerHTML = prTableHtml(filteredPrs.map(prRow).join(''));
423
512
  document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
424
513
  document.getElementById('modal-body').style.whiteSpace = 'normal';
425
514
  document.getElementById('modal').classList.add('open');
@@ -862,6 +951,8 @@ window.MinionsPrs = {
862
951
  prRow,
863
952
  prTableHtml,
864
953
  renderPrs,
954
+ applyPrFilters,
955
+ resetPrFilters,
865
956
  renderPrDetail: _renderPrDetail, // exported for testing
866
957
  prPrev,
867
958
  prNext,
@@ -4,5 +4,34 @@
4
4
  <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">created by agents, tracked with review + build status</span>
5
5
  </h2>
6
6
  <div id="pr-toast" class="cmd-toast cmd-toast-inline" style="margin:6px 0"></div>
7
+ <div class="pr-filters" role="group" aria-label="Pull request filters">
8
+ <label class="pr-filter">Author
9
+ <select id="pr-filter-author" onchange="applyPrFilters()">
10
+ <option value="">All authors</option>
11
+ </select>
12
+ </label>
13
+ <label class="pr-filter">Lifecycle
14
+ <select id="pr-filter-lifecycle" onchange="applyPrFilters()">
15
+ <option value="">All lifecycle statuses</option>
16
+ </select>
17
+ </label>
18
+ <label class="pr-filter">Project
19
+ <select id="pr-filter-project" onchange="applyPrFilters()">
20
+ <option value="">All projects</option>
21
+ </select>
22
+ </label>
23
+ <label class="pr-filter">Review
24
+ <select id="pr-filter-review" onchange="applyPrFilters()">
25
+ <option value="">All review statuses</option>
26
+ </select>
27
+ </label>
28
+ <label class="pr-filter">Build
29
+ <select id="pr-filter-build" onchange="applyPrFilters()">
30
+ <option value="">All build statuses</option>
31
+ </select>
32
+ </label>
33
+ <button class="pr-filter-reset" type="button" onclick="resetPrFilters()">Reset filters</button>
34
+ <span class="pr-filter-summary" id="pr-filter-summary" aria-live="polite"></span>
35
+ </div>
7
36
  <div id="pr-content"><p class="pr-empty">No pull requests yet.</p></div>
8
37
  </section>
@@ -0,0 +1,166 @@
1
+ // Shared PR filter semantics. Slim embeds the classic /prs screen, and both
2
+ // bundles load this helper so lifecycle and legacy-field normalization stay
3
+ // identical across surfaces.
4
+ var PR_FILTER_DIMENSIONS = ['author', 'lifecycle', 'project', 'review', 'build'];
5
+ var PR_LIFECYCLE_FILTER_LABELS = {
6
+ open: 'Open / active',
7
+ merged: 'Merged',
8
+ closed: 'Closed',
9
+ };
10
+ var PR_FILTER_OPTION_ORDER = {
11
+ lifecycle: ['open', 'merged', 'closed'],
12
+ review: ['approved', 'changes-requested', 'waiting', 'pending'],
13
+ build: ['passing', 'failing', 'running', 'none'],
14
+ };
15
+
16
+ function _prFilterText(value) {
17
+ return typeof value === 'string' ? value.trim() : '';
18
+ }
19
+
20
+ function _prFilterName(value) {
21
+ var direct = _prFilterText(value);
22
+ if (direct) return direct;
23
+ if (!value || typeof value !== 'object') return '';
24
+ return _prFilterText(value.login)
25
+ || _prFilterText(value.displayName)
26
+ || _prFilterText(value.uniqueName)
27
+ || _prFilterText(value.name);
28
+ }
29
+
30
+ function _prFilterKey(value) {
31
+ return _prFilterText(value).toLowerCase();
32
+ }
33
+
34
+ function normalizePrAuthor(pr) {
35
+ if (!pr || typeof pr !== 'object') return '';
36
+ return _prFilterName(pr.author)
37
+ || _prFilterName(pr.createdBy)
38
+ || _prFilterName(pr.created_by)
39
+ || _prFilterName(pr.agent);
40
+ }
41
+
42
+ function normalizePrLifecycleStatus(pr) {
43
+ if (!pr || typeof pr !== 'object') return '';
44
+ var status = _prFilterKey(pr.status || pr.state);
45
+ if (pr.merged === true || pr.isMerged === true || pr.mergedAt || pr.merged_at
46
+ || status === 'merged' || status === 'completed') {
47
+ return 'merged';
48
+ }
49
+ if (status === 'active' || status === 'open' || status === 'linked') return 'open';
50
+ if (status === 'closed' || status === 'abandoned' || status === 'declined') return 'closed';
51
+ return status;
52
+ }
53
+
54
+ function normalizePrProject(pr) {
55
+ if (!pr || typeof pr !== 'object') return '';
56
+ return _prFilterText(pr._project)
57
+ || _prFilterText(pr.project)
58
+ || _prFilterText(pr.sourceProject);
59
+ }
60
+
61
+ function normalizePrReviewStatus(pr) {
62
+ if (!pr || typeof pr !== 'object') return '';
63
+ var minionsStatus = pr.minionsReview && _prFilterText(pr.minionsReview.status);
64
+ var status = _prFilterKey(minionsStatus || pr.reviewStatus || pr.review_status);
65
+ if (!minionsStatus && status === 'waiting') {
66
+ if (normalizePrLifecycleStatus(pr) === 'merged') return 'approved';
67
+ if (_prFilterKey(pr.status) === 'abandoned') return 'pending';
68
+ }
69
+ if (status === 'approve') return 'approved';
70
+ if (status === 'rejected' || status === 'changes_requested'
71
+ || status === 'requested-changes' || status === 'changesrequired') {
72
+ return 'changes-requested';
73
+ }
74
+ if (status === 'reviewing') return 'waiting';
75
+ return status;
76
+ }
77
+
78
+ function normalizePrBuildStatus(pr) {
79
+ if (!pr || typeof pr !== 'object') return '';
80
+ var status = _prFilterKey(pr.buildStatus || pr.build_status);
81
+ if (status === 'success' || status === 'succeeded') return 'passing';
82
+ if (status === 'failed' || status === 'error') return 'failing';
83
+ if (status === 'in-progress' || status === 'in_progress' || status === 'queued') return 'running';
84
+ if (status === 'not-run' || status === 'not_run') return 'none';
85
+ return status;
86
+ }
87
+
88
+ function _prFilterEntry(pr, dimension) {
89
+ var label = '';
90
+ var value = '';
91
+ if (dimension === 'author') {
92
+ label = normalizePrAuthor(pr);
93
+ value = _prFilterKey(label);
94
+ } else if (dimension === 'lifecycle') {
95
+ value = normalizePrLifecycleStatus(pr);
96
+ label = PR_LIFECYCLE_FILTER_LABELS[value] || _formatPrFilterLabel(value);
97
+ } else if (dimension === 'project') {
98
+ label = normalizePrProject(pr);
99
+ value = _prFilterKey(label);
100
+ } else if (dimension === 'review') {
101
+ value = normalizePrReviewStatus(pr);
102
+ label = _formatPrFilterLabel(value);
103
+ } else if (dimension === 'build') {
104
+ value = normalizePrBuildStatus(pr);
105
+ label = _formatPrFilterLabel(value);
106
+ }
107
+ return value ? { value: value, label: label || value } : null;
108
+ }
109
+
110
+ function _formatPrFilterLabel(value) {
111
+ var text = _prFilterText(value).replace(/[-_]+/g, ' ');
112
+ return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
113
+ }
114
+
115
+ function getPrFilterValue(pr, dimension) {
116
+ var entry = _prFilterEntry(pr, dimension);
117
+ return entry ? entry.value : '';
118
+ }
119
+
120
+ function matchesPullRequestFilters(pr, filters) {
121
+ filters = filters || {};
122
+ for (var i = 0; i < PR_FILTER_DIMENSIONS.length; i++) {
123
+ var dimension = PR_FILTER_DIMENSIONS[i];
124
+ var selected = _prFilterKey(filters[dimension]);
125
+ if (selected && getPrFilterValue(pr, dimension) !== selected) return false;
126
+ }
127
+ return true;
128
+ }
129
+
130
+ function filterPullRequests(prs, filters) {
131
+ if (!Array.isArray(prs)) return [];
132
+ return prs.filter(function(pr) { return matchesPullRequestFilters(pr, filters); });
133
+ }
134
+
135
+ function getPrFilterOptions(prs) {
136
+ var result = {};
137
+ var records = Array.isArray(prs) ? prs : [];
138
+ PR_FILTER_DIMENSIONS.forEach(function(dimension) {
139
+ var seen = new Map();
140
+ records.forEach(function(pr) {
141
+ var entry = _prFilterEntry(pr, dimension);
142
+ if (entry && !seen.has(entry.value)) seen.set(entry.value, entry);
143
+ });
144
+ var order = PR_FILTER_OPTION_ORDER[dimension] || [];
145
+ result[dimension] = Array.from(seen.values()).sort(function(a, b) {
146
+ var ai = order.indexOf(a.value);
147
+ var bi = order.indexOf(b.value);
148
+ if (ai !== -1 || bi !== -1) {
149
+ if (ai === -1) return 1;
150
+ if (bi === -1) return -1;
151
+ if (ai !== bi) return ai - bi;
152
+ }
153
+ return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
154
+ });
155
+ });
156
+ return result;
157
+ }
158
+
159
+ window.MinionsPrFilters = {
160
+ author: normalizePrAuthor,
161
+ normalizeLifecycle: normalizePrLifecycleStatus,
162
+ value: getPrFilterValue,
163
+ matches: matchesPullRequestFilters,
164
+ filter: filterPullRequests,
165
+ options: getPrFilterOptions,
166
+ };
@@ -322,6 +322,34 @@
322
322
  .prd-pending { color: var(--yellow); font-style: italic; }
323
323
 
324
324
  /* PR Tracker */
325
+ .pr-filters {
326
+ display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-4);
327
+ padding: var(--space-4) 0 var(--space-6);
328
+ }
329
+ .pr-filter {
330
+ display: grid; gap: var(--space-1); min-width: 140px;
331
+ color: var(--muted); font-size: var(--text-sm); font-weight: 600;
332
+ }
333
+ .pr-filter select {
334
+ min-height: 32px; padding: var(--space-2) var(--space-4);
335
+ color: var(--text); background: var(--surface2);
336
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
337
+ font: inherit; font-weight: 400;
338
+ }
339
+ .pr-filter select:focus-visible, .pr-filter-reset:focus-visible {
340
+ outline: 2px solid var(--blue); outline-offset: 2px;
341
+ }
342
+ .pr-filter-reset {
343
+ min-height: 32px; padding: var(--space-2) var(--space-5);
344
+ color: var(--blue); background: transparent;
345
+ border: 1px solid var(--blue); border-radius: var(--radius-sm);
346
+ cursor: pointer; font-size: var(--text-base);
347
+ }
348
+ .pr-filter-reset:hover { background: rgba(88,166,255,0.1); }
349
+ .pr-filter-summary {
350
+ min-height: 32px; display: inline-flex; align-items: center;
351
+ color: var(--muted); font-size: var(--text-base);
352
+ }
325
353
  .pr-table-wrap { overflow-x: auto; }
326
354
  .pr-table { width: 100%; border-collapse: collapse; font-size: var(--text-md); table-layout: auto; }
327
355
  .pr-table th:last-child, .pr-table td:last-child { width: 36px; min-width: 36px; text-align: center; }
@@ -348,7 +376,7 @@
348
376
  wrap) stay visible while the user is on this page. The modal "see all"
349
377
  view uses the same colgroup but is unaffected — modal-body handles its
350
378
  own scrolling. */
351
- #pr-content .pr-table-wrap--prs { max-height: calc(100vh - 200px); overflow: auto; }
379
+ #pr-content .pr-table-wrap--prs { max-height: calc(100vh - 270px); overflow: auto; }
352
380
  #pr-content .pr-table--prs thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
353
381
  .pr-table th { text-align: left; color: var(--muted); font-weight: 500; font-size: var(--text-base); text-transform: uppercase; letter-spacing: 0.5px; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border); }
354
382
  .pr-table td { padding: var(--space-5); border-bottom: 1px solid var(--border); vertical-align: middle; white-space: nowrap; }
@@ -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'];
11
+ const DASHBOARD_SHARED_JS = ['pr-merge-state', 'pr-filters'];
12
12
 
13
13
  function buildDashboardHtml() {
14
14
  const dashDir = path.join(MINIONS_DIR, 'dashboard');
package/dashboard.js CHANGED
@@ -1796,7 +1796,7 @@ function buildDashboardHtml() {
1796
1796
  'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
1797
1797
  ];
1798
1798
  let jsHtml = '';
1799
- for (const f of ['pr-merge-state']) {
1799
+ for (const f of ['pr-merge-state', 'pr-filters']) {
1800
1800
  const content = safeRead(path.join(dashDir, 'shared', f + '.js'));
1801
1801
  jsHtml += `\n// ─── shared/${f}.js ────────────────────────────────────────\n${content}\n`;
1802
1802
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2395",
3
+ "version": "0.1.2397",
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"