Skip to content

Commit 4beca2f

Browse files
jamcgrathCopilot
andauthored
Add pr-dashboard skill and plugin (#1444)
* Add pr-dashboard skill and plugin Adds a self-contained PR dashboard skill that generates and opens a rich HTML dashboard in the browser showing GitHub pull requests for a given date range and role filter. - Skill: skills/pr-dashboard/ — bundles pr-dashboard-cli.mjs, dashboard.html, and lib/utils.mjs - Plugin: plugins/pr-dashboard/ — makes it installable via `copilot skill install pr-dashboard@awesome-copilot` Requires GitHub CLI (gh) installed and authenticated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore README.instructions.md to upstream sort order macOS locale sorts Japanese/Korean C# entries differently than Linux CI. Restore to the upstream/staged version since we don't add any instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments - Fix regex character class bug: [-to]+ → (?:-|to) alternation in utils.mjs - Fix 'last week' to return previous calendar week (Mon–Sun) not last 7 days - Remove unused formatHumanDate and buildMarkdown exports from utils.mjs - Fix ghApi error handling: rethrow with helpful message instead of silently returning parsed JSON on failure (prevents silent auth errors) - Add pagination to searchIssues (up to 1000 results across pages) - Add rel="noopener noreferrer" to target=_blank links in generated rows - HTML-escape fallback template content in renderHtml to prevent injection - Move escapeHtml to module level so it's available before renderHtml body - Neutralise dashboard.html template: placeholder title/h1/meta/stats/tbody - Empty __md and filename in template (CLI populates at runtime) - Add aria-label to search input and status/review selects Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * remove newline * Regenerate docs/README.instructions.md * refactor(pr-dashboard): move scripts/assets per skills spec, remove plugin - Move pr-dashboard-cli.mjs and lib/utils.mjs into scripts/ per skills spec - Move dashboard.html into assets/ per skills spec - Update CLI template path and SKILL.md script path reference - Remove plugins/pr-dashboard (redundant now that gh skills install works) - Clean up marketplace.json and docs/README.plugins.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 76ac13a commit 4beca2f

5 files changed

Lines changed: 638 additions & 0 deletions

File tree

docs/README.skills.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
256256
| [power-platform-architect](../skills/power-platform-architect/SKILL.md)<br />`gh skills install github/awesome-copilot power-platform-architect` | Use this skill when the user needs to transform business requirements, use case descriptions, or meeting transcripts into a technical Power Platform solution architecture, including component selection and Mermaid.js diagrams. | None |
257257
| [power-platform-mcp-connector-suite](../skills/power-platform-mcp-connector-suite/SKILL.md)<br />`gh skills install github/awesome-copilot power-platform-mcp-connector-suite` | Generate complete Power Platform custom connector with MCP integration for Copilot Studio - includes schema generation, troubleshooting, and validation | None |
258258
| [powerbi-modeling](../skills/powerbi-modeling/SKILL.md)<br />`gh skills install github/awesome-copilot powerbi-modeling` | Power BI semantic modeling assistant for building optimized data models. Use when working with Power BI semantic models, creating measures, designing star schemas, configuring relationships, implementing RLS, or optimizing model performance. Triggers on queries about DAX calculations, table relationships, dimension/fact table design, naming conventions, model documentation, cardinality, cross-filter direction, calculation groups, and data model best practices. Always connects to the active model first using power-bi-modeling MCP tools to understand the data structure before providing guidance. | `references/MEASURES-DAX.md`<br />`references/PERFORMANCE.md`<br />`references/RELATIONSHIPS.md`<br />`references/RLS.md`<br />`references/STAR-SCHEMA.md` |
259+
| [pr-dashboard](../skills/pr-dashboard/SKILL.md)<br />`gh skills install github/awesome-copilot pr-dashboard` | Open a GitHub PR dashboard in the browser. Use when the user asks to see their pull requests, open the PR dashboard, show PRs for a date range, or check PR status. Trigger phrases include "show my PRs", "open PR dashboard", "pull request dashboard". | `assets/dashboard.html`<br />`scripts/lib`<br />`scripts/pr-dashboard-cli.mjs` |
259260
| [prd](../skills/prd/SKILL.md)<br />`gh skills install github/awesome-copilot prd` | Generate high-quality Product Requirements Documents (PRDs) for software systems and AI-powered features. Includes executive summaries, user stories, technical specifications, and risk analysis. | None |
260261
| [premium-frontend-ui](../skills/premium-frontend-ui/SKILL.md)<br />`gh skills install github/awesome-copilot premium-frontend-ui` | A comprehensive guide for GitHub Copilot to craft immersive, high-performance web experiences with advanced motion, typography, and architectural craftsmanship. | None |
261262
| [project-workflow-analysis-blueprint-generator](../skills/project-workflow-analysis-blueprint-generator/SKILL.md)<br />`gh skills install github/awesome-copilot project-workflow-analysis-blueprint-generator` | Comprehensive technology-agnostic prompt generator for documenting end-to-end application workflows. Automatically detects project architecture patterns, technology stacks, and data flow patterns to generate detailed implementation blueprints covering entry points, service layers, data access, error handling, and testing approaches across multiple technologies including .NET, Java/Spring, React, and microservices architectures. | None |

skills/pr-dashboard/SKILL.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: pr-dashboard
3+
description: 'Open a GitHub PR dashboard in the browser. Use when the user asks to see their pull requests, open the PR dashboard, show PRs for a date range, or check PR status. Trigger phrases include "show my PRs", "open PR dashboard", "pull request dashboard".'
4+
---
5+
6+
# PR Dashboard
7+
8+
Generates and opens a GitHub PR dashboard in the browser for a given date range and role filter.
9+
10+
**Prerequisites:** GitHub CLI (`gh`) must be installed and authenticated (`gh auth login`).
11+
12+
## What to do
13+
14+
Find the CLI script bundled with this skill and run it:
15+
16+
```bash
17+
SKILL_SCRIPT=$(find ~/.copilot -name "pr-dashboard-cli.mjs" -path "*/pr-dashboard/scripts/*" 2>/dev/null | head -1)
18+
node "$SKILL_SCRIPT" "<query>" "<role>"
19+
```
20+
21+
- `<query>`: the date range the user specified (default: `last 7 days`)
22+
- `<role>`: one of `Authored by me`, `Requested reviews`, `Assigned to me`, `All` (default: `Authored by me`)
23+
24+
## Parsing the user's request
25+
26+
Extract the date range and role from the user's message. Examples:
27+
28+
| User says | query | role |
29+
|---|---|---|
30+
| show my PRs | `last 7 days` | `Authored by me` |
31+
| show my PRs last 2 weeks | `last 2 weeks` | `Authored by me` |
32+
| PR dashboard this month reviews | `this month` | `Requested reviews` |
33+
| PR dashboard march 2026 assigned | `march 2026` | `Assigned to me` |
34+
| show all PRs last 30 days | `last 30 days` | `All` |
35+
36+
**Role keyword mapping:**
37+
- "my PRs", "authored", "I wrote" → `Authored by me`
38+
- "reviews", "review requested", "reviewing" → `Requested reviews`
39+
- "assigned" → `Assigned to me`
40+
- "all", "involves me" → `All`
41+
42+
## Supported date range formats
43+
44+
The script understands natural language — pass it through as-is:
45+
- `last 7 days`, `last 2 weeks`, `last 30 days`
46+
- `this week`, `last week`, `this month`, `last month`
47+
- `march 2026`, `feb 2025`
48+
- `2026-01-01 - 2026-03-31`
49+
- `2025` (whole year)
50+
51+
## After running
52+
53+
Tell the user the dashboard is opening in their browser. The script outputs progress to stdout. If it exits with an error, show the error output and suggest they run `gh auth login` if it's an auth issue.
54+
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<!DOCTYPE html>
2+
<html lang="en" data-theme="light">
3+
<head>
4+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5+
<title>PR Dashboard</title>
6+
<style>
7+
:root {
8+
--bg:#ffffff; --bg2:#f6f8fa; --bg3:#eaeef2; --border:#d0d7de;
9+
--text:#1f2328; --muted:#636c76; --link:#0969da;
10+
--hover:#f3f4f6; --thead:#f6f8fa;
11+
}
12+
[data-theme="dark"] {
13+
--bg:#0d1117; --bg2:#161b22; --bg3:#21262d; --border:#30363d;
14+
--text:#e6edf3; --muted:#8b949e; --link:#58a6ff;
15+
--hover:#1c2128; --thead:#21262d;
16+
}
17+
*{box-sizing:border-box;margin:0;padding:0}
18+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:1rem;background:var(--bg);color:var(--text);padding:24px 32px;transition:background .2s,color .2s}
19+
h1{font-size:2rem;font-weight:800;margin-bottom:8px;letter-spacing:-.5px}
20+
.meta{color:var(--muted);font-size:.875rem;margin-bottom:32px}
21+
.stats{display:flex;gap:16px;margin-bottom:32px;flex-wrap:wrap}
22+
.stat{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:20px 28px;text-align:center;transition:all .2s;cursor:default}
23+
.stat:hover{border-color:var(--link);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.08)}
24+
.stat .n{font-size:2.5rem;font-weight:800;line-height:1}.stat .l{font-size:.85rem;color:var(--muted);margin-top:6px;font-weight:500}
25+
.stat.open .n{color:#1a7f37}.stat.merged .n{color:#8250df}.stat.closed .n{color:#cf222e}.stat.draft .n{color:#636c76}
26+
[data-theme="dark"] .stat.open .n{color:#2ea043}[data-theme="dark"] .stat.merged .n{color:#8957e5}
27+
[data-theme="dark"] .stat.closed .n{color:#da3633}[data-theme="dark"] .stat.draft .n{color:#848d97}
28+
.toolbar{display:flex;gap:12px;margin-bottom:20px;align-items:center;flex-wrap:wrap;padding:12px;background:var(--bg2);border-radius:8px}
29+
.toolbar input,.toolbar select{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 12px;color:var(--text);font-size:.95rem;outline:none;transition:border .2s}
30+
.toolbar input{flex:1;min-width:200px}.toolbar input:focus,.toolbar select:focus{border-color:var(--link);box-shadow:0 0 0 3px rgba(9,105,218,.1)}
31+
.visible-count{margin-left:auto;font-size:.875rem;color:var(--muted);font-weight:500}
32+
.theme-toggle{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 14px;color:var(--text);font-size:.875rem;cursor:pointer;white-space:nowrap;transition:all .2s;font-weight:500}
33+
.theme-toggle:hover{background:var(--bg3);border-color:var(--link)}
34+
table{width:100%;border-collapse:collapse;background:var(--bg);border:1px solid var(--border);border-radius:10px;overflow:hidden;font-size:.95rem;box-shadow:0 1px 3px rgba(0,0,0,.05)}
35+
thead{background:var(--thead)}
36+
th{padding:14px 16px;text-align:left;font-size:.8rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
37+
td{padding:16px;border-top:1px solid var(--border);vertical-align:top}
38+
tr:last-child td{border-bottom:none}
39+
tr:hover td{background:var(--hover)}
40+
a{color:var(--link);text-decoration:none;font-weight:500}a:hover{text-decoration:underline}
41+
.title-line{font-weight:600;font-size:1rem;margin-bottom:6px}
42+
.badge{display:inline-block;padding:4px 10px;border-radius:14px;font-size:.75rem;font-weight:700;color:#fff;letter-spacing:.3px;text-transform:uppercase}
43+
.empty{text-align:center;padding:60px 40px;color:var(--muted);font-size:1.1rem}
44+
.controls{display:inline-flex;gap:6px;margin-left:8px}
45+
.mini-btn{background:var(--bg2);border:1px solid var(--border);padding:4px 8px;border-radius:6px;font-size:.75rem;cursor:pointer;transition:all .2s}
46+
.mini-btn:hover{background:var(--bg3);border-color:var(--link)}
47+
48+
49+
</style>
50+
</head>
51+
<body>
52+
<h1>🔀 PR Dashboard</h1>
53+
<div class="meta">PR Dashboard</div>
54+
55+
<div class="stats">
56+
<div class="stat open"><div class="n">0</div><div class="l">Open</div></div>
57+
<div class="stat merged"><div class="n">0</div><div class="l">Merged</div></div>
58+
<div class="stat closed"><div class="n">0</div><div class="l">Closed</div></div>
59+
<div class="stat draft"><div class="n">0</div><div class="l">Draft</div></div>
60+
</div>
61+
62+
<div class="toolbar">
63+
<input type="search" id="q" placeholder="Search title or repo…" aria-label="Search pull requests" oninput="filter()">
64+
<select id="sf" aria-label="Filter by status" onchange="filter()">
65+
<option value="">All statuses</option>
66+
<option>OPEN</option><option>MERGED</option><option>CLOSED</option><option>DRAFT</option>
67+
</select>
68+
<select id="rf" aria-label="Filter by review status" onchange="filter()">
69+
<option value="">All reviews</option>
70+
<option>APPROVED</option><option>CHANGES_REQUESTED</option><option>REVIEW_REQUIRED</option>
71+
</select>
72+
<span class="visible-count" id="vc">0 PRs</span>
73+
<button id="exportMdBtn" class="theme-toggle" onclick="downloadMarkdown()">📄 Export MD</button>
74+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">🌙 Dark</button>
75+
</div>
76+
77+
<table>
78+
<thead><tr><th>Repository</th><th>Title</th><th>Status</th><th>Review</th><th>CI</th><th>Created</th><th>Updated</th></tr></thead>
79+
<tbody id="tb">
80+
<tr>
81+
<td colspan="7" style="text-align:center;color:var(--muted);">No pull request data loaded.</td>
82+
</tr>
83+
</tbody>
84+
</table>
85+
86+
<script>const __md = "";
87+
function filter(){
88+
const q=document.getElementById("q").value.toLowerCase();
89+
const sf=document.getElementById("sf").value;
90+
const rf=document.getElementById("rf").value;
91+
let n=0;
92+
document.querySelectorAll("#tb tr").forEach(r=>{
93+
const t=r.textContent.toLowerCase();
94+
const badges=[...r.querySelectorAll(".badge")].map(b=>b.textContent);
95+
const show=(!q||t.includes(q))&&(!sf||badges[0]===sf)&&(!rf||badges[1]===rf);
96+
r.style.display=show?"":"none";
97+
if(show)n++;
98+
});
99+
document.getElementById("vc").textContent=n+" PR"+(n!==1?"s":"");
100+
}
101+
function toggleTheme(){
102+
const html=document.documentElement;
103+
const isDark=html.getAttribute("data-theme")==="dark";
104+
html.setAttribute("data-theme",isDark?"light":"dark");
105+
document.getElementById("themeBtn").textContent=isDark?"🌙 Dark":"☀️ Light";
106+
localStorage.setItem("pr-dashboard-theme",isDark?"light":"dark");
107+
}
108+
function downloadMarkdown(){
109+
try{
110+
const md = __md || '';
111+
const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
112+
const filename = 'pr-dashboard.md';
113+
const url = URL.createObjectURL(blob);
114+
const a = document.createElement('a');
115+
a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
116+
} catch (e) { console.error(e); }
117+
}
118+
119+
120+
121+
122+
// Restore saved theme preference
123+
const saved=localStorage.getItem("pr-dashboard-theme");
124+
if(saved==="dark"){
125+
document.documentElement.setAttribute("data-theme","dark");
126+
document.getElementById("themeBtn").textContent="☀️ Light";
127+
}
128+
</script>
129+
</body>
130+
</html>
131+
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
export function dateToYMD(d) {
2+
const y = d.getFullYear();
3+
const m = String(d.getMonth() + 1).padStart(2, '0');
4+
const day = String(d.getDate()).padStart(2, '0');
5+
return `${y}-${m}-${day}`;
6+
}
7+
8+
export function parseDateRange(text) {
9+
const now = new Date();
10+
const todayStr = dateToYMD(now);
11+
const lower = (text || '').toLowerCase();
12+
let match;
13+
14+
if ((match = lower.match(/last\s+(\d+)\s+days?/))) {
15+
const n = parseInt(match[1], 10);
16+
const start = new Date();
17+
start.setDate(start.getDate() - (n - 1));
18+
return { start: dateToYMD(start), end: todayStr, label: `Last ${n} days` };
19+
}
20+
21+
if ((match = lower.match(/last\s+(\d+)\s+weeks?/))) {
22+
const n = parseInt(match[1], 10);
23+
const start = new Date();
24+
start.setDate(start.getDate() - (n * 7 - 1));
25+
return { start: dateToYMD(start), end: todayStr, label: `Last ${n} weeks` };
26+
}
27+
28+
if (lower.includes('this week')) {
29+
const d = new Date();
30+
const day = d.getDay();
31+
const diff = (day + 6) % 7;
32+
const start = new Date();
33+
start.setDate(start.getDate() - diff);
34+
return { start: dateToYMD(start), end: todayStr, label: 'This week' };
35+
}
36+
37+
if (lower.includes('last week')) {
38+
const currentWeekStart = new Date();
39+
const day = currentWeekStart.getDay();
40+
const diff = (day + 6) % 7;
41+
currentWeekStart.setDate(currentWeekStart.getDate() - diff);
42+
43+
const end = new Date(currentWeekStart);
44+
end.setDate(end.getDate() - 1);
45+
46+
const start = new Date(end);
47+
start.setDate(start.getDate() - 6);
48+
49+
return { start: dateToYMD(start), end: dateToYMD(end), label: 'Last week' };
50+
}
51+
52+
if (lower.includes('this month')) {
53+
const start = new Date(now.getFullYear(), now.getMonth(), 1);
54+
return { start: dateToYMD(start), end: todayStr, label: 'This month' };
55+
}
56+
57+
if (lower.includes('last month')) {
58+
const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
59+
const end = new Date(now.getFullYear(), now.getMonth(), 0);
60+
return { start: dateToYMD(start), end: dateToYMD(end), label: 'Last month' };
61+
}
62+
63+
if (lower.includes('next month')) {
64+
const start = new Date(now.getFullYear(), now.getMonth() + 1, 1);
65+
const end = new Date(now.getFullYear(), now.getMonth() + 2, 0);
66+
return { start: dateToYMD(start), end: dateToYMD(end), label: 'Next month' };
67+
}
68+
69+
// month-year like "february 2006" or "feb 2006"
70+
if ((match = lower.match(/\b(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{4})\b/))) {
71+
const monthStr = match[1];
72+
const year = parseInt(match[2], 10);
73+
const months = { january:0, jan:0, february:1, feb:1, march:2, mar:2, april:3, apr:3, may:4, june:5, jun:5, july:6, jul:6, august:7, aug:7, september:8, sep:8, sept:8, october:9, oct:9, november:10, nov:10, december:11, dec:11 };
74+
const mIdx = months[monthStr];
75+
if (mIdx !== undefined && !isNaN(year)) {
76+
const start = new Date(year, mIdx, 1);
77+
const end = new Date(year, mIdx + 1, 0);
78+
const label = `${monthStr.charAt(0).toUpperCase()}${monthStr.slice(1)} ${year}`;
79+
return { start: dateToYMD(start), end: dateToYMD(end), label };
80+
}
81+
}
82+
83+
// explicit YYYY-MM-DD - YYYY-MM-DD or YYYY-MM-DD to YYYY-MM-DD
84+
if ((match = text.match(/(\d{4}-\d{2}-\d{2})(?:\s*-\s*|\s+to\s+)(\d{4}-\d{2}-\d{2})/))) {
85+
return { start: match[1], end: match[2], label: `${match[1]}${match[2]}` };
86+
}
87+
88+
// explicit like "Apr 1 - Apr 5" or "Apr 1 to Apr 5"
89+
if ((match = text.match(/([A-Za-z]{3,}\s+\d{1,2}(?:,\s*\d{4})?)(?:\s*-\s*|\s+to\s+)([A-Za-z]{3,}\s+\d{1,2}(?:,\s*\d{4})?)/))) {
90+
const s = new Date(match[1]);
91+
const e = new Date(match[2]);
92+
if (!isNaN(s) && !isNaN(e)) {
93+
return { start: dateToYMD(s), end: dateToYMD(e), label: `${match[1]}${match[2]}` };
94+
}
95+
}
96+
97+
// year-only like "2006"
98+
if ((match = lower.match(/\b(\d{4})\b/))) {
99+
const y = parseInt(match[1], 10);
100+
const start = new Date(y, 0, 1);
101+
const end = new Date(y, 11, 31);
102+
return { start: dateToYMD(start), end: dateToYMD(end), label: `${y}` };
103+
}
104+
105+
// month-only like "february" or "feb" (assume current year)
106+
if ((match = lower.match(/\b(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\b/))) {
107+
const monthStr = match[1];
108+
const months = { january:0, jan:0, february:1, feb:1, march:2, mar:2, april:3, apr:3, may:4, june:5, jun:5, july:6, jul:6, august:7, aug:7, september:8, sep:8, sept:8, october:9, oct:9, november:10, nov:10, december:11, dec:11 };
109+
const mIdx = months[monthStr];
110+
if (mIdx !== undefined) {
111+
const start = new Date(now.getFullYear(), mIdx, 1);
112+
const end = new Date(now.getFullYear(), mIdx + 1, 0);
113+
const label = `${monthStr.charAt(0).toUpperCase()}${monthStr.slice(1)} ${now.getFullYear()}`;
114+
return { start: dateToYMD(start), end: dateToYMD(end), label };
115+
}
116+
}
117+
118+
const start = new Date();
119+
start.setDate(start.getDate() - 6);
120+
return { start: dateToYMD(start), end: todayStr, label: 'Last 7 days' };
121+
}

0 commit comments

Comments
 (0)