
Pr Review Canvas
- 1.4k installs
- 2.5k repo stars
- Updated August 5, 2026
- cursor/plugins
pr-review-canvas is a Cursor agent skill that renders visual, canvas-style pull request reviews with spatial change context for developers who need faster comprehension of large diffs.
About
pr-review-canvas is a Cursor plugin skill that turns pull request reviews into a visual canvas layout so developers can see where changes sit in the codebase instead of scrolling endless unified diffs. The skill is published in cursor/plugins on skills.sh and has 440 installs with a catalog rank of 6. It targets PR review sessions in Cursor where spatial context, file relationships, and change clustering matter more than raw line counts. Developers reach for pr-review-canvas when a PR spans many files, touches related modules, or needs a map-like overview before approving, requesting changes, or writing review comments.
- Visual canvas interface for pull request review
- Spatial highlighting of code changes with context
- Agent-compatible review workflow for Cursor
- Reduces review fatigue by replacing linear diff scrolling
- Works on any GitHub PR opened from your workspace
Pr Review Canvas by the numbers
- 1,383 all-time installs (skills.sh)
- +208 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #94 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cursor/plugins --skill pr-review-canvasAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 2.5k |
| Last updated | August 5, 2026 |
| Repository | cursor/plugins ↗ |
How do you review large pull requests with spatial context?
Get visual, canvas-style pull request reviews that highlight changes with spatial context instead of scrolling through endless diffs.
Who is it for?
Developers reviewing multi-file pull requests in Cursor who want spatial diff context instead of scrolling long unified diffs.
Skip if: Teams that only need inline line comments on small single-file patches without any visual overview.
When should I use this skill?
A developer opens or discusses a pull request with many files, cross-module changes, or asks for a visual or canvas-style PR review.
What you get
A canvas-style PR review layout highlighting changed files, spatial relationships, and review-ready change groupings.
- Canvas-style PR review layout
- Spatial change map across modified files
By the numbers
- 440 installs on skills.sh
- Rank 6 in the cursor/plugins catalog on skills.sh
Files
PR Review Canvas
Generate an interactive HTML review of a GitHub PR that reads like a peer walking you through what matters.
Workflow
1. Fetch PR data
Run these gh api calls in parallel:
gh api repos/{owner}/{repo}/pulls/{number} --jq '{title, body, user: .user.login, state, additions, deletions, changed_files, base: .base.ref, head: .head.ref}'
gh api repos/{owner}/{repo}/pulls/{number}/files --paginate --jq '.[] | {filename, status, additions, deletions, patch}'
gh api repos/{owner}/{repo}/pulls/{number}/comments --jq '.[] | {user: .user.login, body, path, line}'2. Analyze the PR and write the body HTML
Read the diffs, understand the PR, and write the <body> content directly as HTML. You have full creative freedom -- the goal is to explain the PR clearly to a reviewer. Use whatever structure best fits the PR.
Typical structure (adapt as needed):
- Header with title, PR number, author, stats
- Summary box explaining what the PR does in plain English
- Core file sections with annotations and diffs
- Mechanical/boilerplate files collapsed by default
- Review checklist at the bottom
But you can also add:
- Pseudocode summaries for verbose code -- show the algorithm in plain English or short pseudocode, with the real diff collapsed below (use a
.bp-sectioncard labeled "Show full implementation"). Great when 150 lines of retry/backoff/error-handling code is really just "fetch with exponential backoff and circuit breaker." - Diagrams (inline SVG, mermaid via CDN, ASCII art in
<pre>) - Flowcharts showing before/after control flow
- Tables comparing old vs new behavior
- Callout boxes for warnings, questions, or gotchas
- Interactive widgets if they help
- Anything else that makes the review clearer
Pseudocode pattern example:
<div class="file-card">
<div class="file-hdr" onclick="toggle(this)">
<span class="fname">retryClient.ts</span>
<div class="fstats"><span class="pill add">+173</span><span class="pill del">−11</span><span class="chev open">▶</span></div>
</div>
<div class="file-body open">
<div class="file-note">
<strong>What this does in plain English:</strong>
<pre style="margin-top:8px;color:var(--text);font-size:12px;line-height:1.6;">
fetch(url):
if circuit breaker is open → fail fast
retry up to N times:
try fetch with timeout
on success → close circuit breaker, return
on retryable error → wait (exponential backoff + jitter)
on non-retryable error → throw
circuit breaker records failure</pre>
</div>
<div class="bp-section" style="margin:0;border:0;border-radius:0;">
<div class="bp-hdr" onclick="toggleBP(this)">
<span>Show full implementation (+173 lines)</span><span class="chev">▶</span>
</div>
<div class="bp-body"><div data-diff="retryClient"></div></div>
</div>
</div>
</div>3. Available CSS classes and JS utilities
Read styles.css and renderer.js from this skill directory. These give you a prebuilt dark-themed toolkit. Inject them into template.html verbatim.
CSS classes you can use:
| Class | Purpose |
|---|---|
.header, .header h1, .header-meta | Page header |
.pill.add, .pill.del, .pill.files | Stat badges (+N, -N, N files) |
.content | Centered content wrapper (max 900px) |
.summary | Summary/TL;DR box |
.section-title | Section heading with bottom border |
.ic | Inline code reference (mono, blue, dark bg) |
.file-card, .file-hdr, .file-body | Collapsible file card (use onclick="toggle(this)" on .file-hdr) |
.file-note | Sticky reviewer annotation inside a file card |
.bp-section, .bp-hdr, .bp-body | Collapsed boilerplate card (use onclick="toggleBP(this)") |
.bp-note | Note inside a boilerplate card |
.verdict | Review checklist box |
JS functions available:
| Function | Usage |
|---|---|
toggle(hdrElement) | Toggle a .file-body open/closed |
toggleBP(hdrElement) | Toggle a .bp-body open/closed |
renderDiff(target, diffInput) | Render a unified diff. target can be a DOM element, string ID, or CSS selector. diffInput can be a raw patch string OR an array of lines -- both work. Automatically filters imports, collapses whitespace-only changes, detects moved code (blue/purple tint). |
esc(string) | HTML-escape a string |
Rendering diffs -- use `data-diff` attributes with auto-discovery. Put <div data-diff="KEY"></div> placeholders in your body HTML wherever you want a diff rendered. The renderer finds them automatically after DOM load and fills them from the <script id="pr-diffs-json" type="application/json"> element in template.html.
CRITICAL: Patch strings can contain `</script>` in addition to newlines, backslashes, and quotes. Even json.dumps(...) is not enough if you paste raw output into executable <script> because HTML parsing can terminate the tag early. Never manually embed patch strings in JS/JSON. Instead, use this safe approach:
1. During the fetch step, save patches to a JSON file using jq (which handles escaping correctly):
gh api repos/{owner}/{repo}/pulls/{number}/files --paginate \
--jq '[.[] | {key: (.filename | gsub("[^a-zA-Z0-9]"; "_")), value: (.patch // "")}] | from_entries' \
> /tmp/pr-patches-{number}.json2. During assembly, use Python to safely inject the JSON into template.html:
python3 <<'PY'
import json
from pathlib import Path
patches = json.loads(Path('/tmp/pr-patches-{number}.json').read_text())
html = Path('/tmp/pr-review-{number}-body.html').read_text()
css = Path('styles.css').read_text()
js = Path('renderer.js').read_text()
tmpl = Path('template.html').read_text()
# Prevent literal </script> from terminating HTML script tags early.
safe_json = json.dumps(patches).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')
out = (
tmpl.replace('/* INJECT_CSS */', css)
.replace('/* INJECT_JS */', js)
.replace('<!-- INJECT_BODY -->', html)
.replace('{"__PR_DIFFS_PLACEHOLDER__":true}', safe_json)
)
Path('/tmp/pr-review-{number}.html').write_text(out)
PYThis guarantees valid JSON and script-safe HTML embedding. The agent writes body HTML to a temp file, then Python assembles everything safely.
The diff data keys should match the data-diff attribute values in the HTML:
<div data-diff="path_to_file_ts"></div>Since renderer.js loads in <head>, you can also call renderDiff(target, lines) directly from inline <script> tags if needed for custom use cases. The function accepts a DOM element, ID string, or CSS selector as target, and a string or array as lines.
You're not limited to these. Add your own inline <style> blocks, <script> blocks, SVGs, diagrams, or anything else. The prebuilt pieces save time but don't constrain you.
4. Assemble and serve
1. Write your body HTML (everything that goes inside <body>) to /tmp/pr-review-{number}-body.html 2. Save patches to /tmp/pr-patches-{number}.json using the jq command from step 3 above 3. Run the Python assembly script from step 3 above (reads styles.css, renderer.js, template.html from this skill directory, injects body + patches safely, writes final HTML) 4. Start a local server on a fixed port:
cd /tmp && python3 -m http.server 8432 --bind 127.0.0.1Run this backgrounded, then navigate the in-app browser to http://127.0.0.1:8432/pr-review-{number}.html.
Why a fixed port and `cd /tmp`: Background shells have no TTY, so Python buffers its startup message ("Serving HTTP on...") indefinitely — using port 0 means you can never read which port was chosen. And --directory /tmp works but cd /tmp is more robust across Python versions. If port 8432 is taken, try 8433, 8434, etc.
Diff features (handled automatically by renderer.js)
- Filters out import-only lines
- Collapses whitespace-only changes into context lines
- Detects moved code blocks (3+ consecutive lines deleted in one place and added identically elsewhere) -- renders in blue/purple instead of red/green
- Near-matches (moved + small edit) get a different purple tint
Style notes
- Dark theme:
#1a1a1abackground, Inter body font, IBM Plex Mono for code - Use
var(--warning)for orange,var(--success)for green,var(--danger)for red,var(--accent)for blue - Sticky file headers (
position: sticky; top: 0) and notes (top: 35px) pin while scrolling - Core files expanded by default (
.file-body.open), mechanical files collapsed
function toggle(hdr) {
var b = hdr.nextElementSibling, c = hdr.querySelector('.chev');
b.classList.toggle('open'); c.classList.toggle('open');
}
function toggleBP(hdr) {
var b = hdr.nextElementSibling, c = hdr.querySelector('.chev');
b.classList.toggle('open'); c.classList.toggle('open');
}
function isImport(line) {
var s = line.replace(/^[+ -]/, '').trim();
return s.startsWith('import ') || s.startsWith('import{') || s.startsWith('} from ');
}
function isWhitespaceOnly(del, add) {
return del.replace(/^-/, '').replace(/\s/g, '') === add.replace(/^\+/, '').replace(/\s/g, '');
}
function esc(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function normWs(s) {
return s.replace(/\s+/g, ' ').trim();
}
function toLines(input) {
if (!input) return [];
if (Array.isArray(input)) return input;
if (typeof input === 'string') return input.split('\n');
return [];
}
function loadPrDiffs() {
var el = document.getElementById('pr-diffs-json');
if (!el) return null;
try {
return JSON.parse(el.textContent || '');
} catch (err) {
console.error('Failed to parse PR diff JSON payload', err);
return null;
}
}
function detectMoves(dels, adds) {
var TH = 3;
var md = {}, ma = {};
for (var di = 0; di < dels.length; di++) {
if (md[di]) continue;
var db = [di];
for (var d2 = di+1; d2 < dels.length && d2-di < 40; d2++) {
if (dels[d2].consecutive && !md[d2]) db.push(d2); else break;
}
if (db.length < TH) continue;
var dn = db.map(function(i){ return normWs(dels[i].code); });
for (var ai = 0; ai < adds.length; ai++) {
if (ma[ai]) continue;
var ab = [ai];
for (var a2 = ai+1; a2 < adds.length && a2-ai < 40; a2++) {
if (adds[a2].consecutive && !ma[a2]) ab.push(a2); else break;
}
if (ab.length < TH) continue;
var an = ab.map(function(i){ return normWs(adds[i].code); });
var ml = Math.min(dn.length, an.length), mc = 0;
for (var m = 0; m < ml; m++) { if (dn[m] === an[m]) mc++; }
if (mc >= TH && mc >= ml * 0.7) {
for (var k = 0; k < ml; k++) {
md[db[k]] = { exact: dn[k] === an[k] };
ma[ab[k]] = { exact: dn[k] === an[k] };
}
break;
}
}
}
return { movedDels: md, movedAdds: ma };
}
/**
* renderDiff(target, diffInput)
* target: DOM element, string ID, or CSS selector
* diffInput: array of diff lines, OR a single string (will be split on \n)
*/
function renderDiff(target, diffInput) {
var el;
if (typeof target === 'string') {
el = document.getElementById(target) || document.querySelector(target);
} else {
el = target;
}
if (!el) return;
var lines = toLines(diffInput);
if (!lines.length) { el.innerHTML = '<div style="padding:12px;color:#777;font-size:12px;">No diff data</div>'; return; }
var filtered = lines.filter(function(l) {
if (l.startsWith('--- ') || l.startsWith('+++ ') || l.startsWith('@@') || l.startsWith('diff ')) return true;
return !isImport(l);
});
var wsOut = [];
for (var wi = 0; wi < filtered.length; wi++) {
if (filtered[wi].startsWith('-')) {
var dr = [filtered[wi]], wj = wi+1;
while (wj < filtered.length && filtered[wj].startsWith('-')) { dr.push(filtered[wj]); wj++; }
var ar = [], wk = wj;
while (wk < filtered.length && filtered[wk].startsWith('+')) { ar.push(filtered[wk]); wk++; }
if (dr.length === ar.length && dr.length > 0) {
var allWs = true;
for (var wc = 0; wc < dr.length; wc++) { if (!isWhitespaceOnly(dr[wc], ar[wc])) { allWs = false; break; } }
if (allWs) { for (var wx = 0; wx < ar.length; wx++) wsOut.push(' ' + ar[wx].slice(1)); wi = wk-1; continue; }
}
}
wsOut.push(filtered[wi]);
}
var dels = [], adds = [], parsed = [];
var oL = 0, nL = 0, pD = false, pA = false;
for (var pi = 0; pi < wsOut.length; pi++) {
var line = wsOut[pi];
if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('diff ')) continue;
if (line.startsWith('@@')) {
var hm = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)/);
if (hm) { oL = parseInt(hm[1]); nL = parseInt(hm[2]); }
parsed.push({ type: 'hunk', text: line }); pD = false; pA = false; continue;
}
if (line.startsWith('+')) {
var ae = { type:'add', code:line.slice(1), newLine:nL, consecutive:pA, idx:parsed.length };
adds.push(ae); parsed.push(ae); nL++; pA = true; pD = false;
} else if (line.startsWith('-')) {
var de = { type:'del', code:line.slice(1), oldLine:oL, consecutive:pD, idx:parsed.length };
dels.push(de); parsed.push(de); oL++; pD = true; pA = false;
} else {
var c = line.startsWith(' ') ? line.slice(1) : line;
parsed.push({ type:'ctx', code:c, oldLine:oL, newLine:nL }); oL++; nL++; pD = false; pA = false;
}
}
var mv = detectMoves(dels, adds);
var rows = [];
for (var ri = 0; ri < parsed.length; ri++) {
var p = parsed[ri];
if (p.type === 'hunk') {
rows.push('<tr class="diff-hunk"><td class="diff-ln"></td><td class="diff-ln"></td><td class="diff-code">' + esc(p.text) + '</td></tr>');
} else if (p.type === 'add') {
var ai2 = -1; for (var fa=0;fa<adds.length;fa++) if(adds[fa].idx===p.idx){ai2=fa;break;}
var cls = (mv.movedAdds[ai2]) ? (mv.movedAdds[ai2].exact ? 'diff-moved-add' : 'diff-moved-add-edited') : 'diff-add';
rows.push('<tr class="'+cls+'"><td class="diff-ln"></td><td class="diff-ln">'+p.newLine+'</td><td class="diff-code">'+esc(p.code)+'</td></tr>');
} else if (p.type === 'del') {
var di2 = -1; for(var fd=0;fd<dels.length;fd++) if(dels[fd].idx===p.idx){di2=fd;break;}
var cls2 = (mv.movedDels[di2]) ? (mv.movedDels[di2].exact ? 'diff-moved-del' : 'diff-moved-del-edited') : 'diff-del';
rows.push('<tr class="'+cls2+'"><td class="diff-ln">'+p.oldLine+'</td><td class="diff-ln"></td><td class="diff-code">'+esc(p.code)+'</td></tr>');
} else {
rows.push('<tr class="diff-ctx"><td class="diff-ln">'+p.oldLine+'</td><td class="diff-ln">'+p.newLine+'</td><td class="diff-code">'+esc(p.code)+'</td></tr>');
}
}
el.innerHTML = '<table class="diff-table"><tbody>' + rows.join('') + '</tbody></table>';
}
/* Auto-discovery: after DOM loads, find all [data-diff] elements and render diffs from pr-diffs-json. */
document.addEventListener('DOMContentLoaded', function() {
var prDiffs = loadPrDiffs();
if (!prDiffs) return;
var els = document.querySelectorAll('[data-diff]');
for (var i = 0; i < els.length; i++) {
var key = els[i].getAttribute('data-diff');
if (key && Object.prototype.hasOwnProperty.call(prDiffs, key)) {
renderDiff(els[i], prDiffs[key]);
}
}
});
:root {
--bg: #1a1a1a;
--surface: #222;
--surface-raised: #2a2a2a;
--border: #333;
--text: #bbb;
--text-muted: #777;
--text-bright: #e5e5e5;
--accent: #7aa2f7;
--warning: #e0a458;
--success: #73b87e;
--danger: #e06060;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Inter', -apple-system, sans-serif;
font-size: 13px;
line-height: 1.5;
}
.header {
border-bottom: 1px solid var(--border);
padding: 24px 32px 20px;
}
.header h1 { font-size: 20px; font-weight: 600; color: var(--text-bright); margin-bottom: 4px; }
.header-meta { font-size: 12px; color: var(--text-muted); display: flex; gap: 16px; align-items: center; margin-top: 8px; }
.pill { display: inline-flex; padding: 1px 8px; border-radius: 10px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; }
.pill.add { background: #73b87e22; color: var(--success); }
.pill.del { background: #e0606022; color: var(--danger); }
.pill.files { background: #7aa2f722; color: var(--accent); }
.content { max-width: 900px; margin: 0 auto; padding: 24px 20px 60px; }
.summary { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 16px 20px; margin-bottom: 28px; font-size: 13px; line-height: 1.6; }
.summary strong { color: var(--text-bright); }
.summary ul { margin-top: 8px; padding-left: 18px; }
.summary li { margin-bottom: 4px; }
.section-title { font-size: 14px; font-weight: 600; color: var(--text-bright); margin: 28px 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
.ic { font-family: 'IBM Plex Mono', monospace; font-size: 11px; background: var(--surface-raised); padding: 1px 5px; border-radius: 3px; color: var(--accent); }
.file-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 12px; }
.file-hdr {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 14px; background: var(--surface-raised); border-bottom: 1px solid var(--border);
cursor: pointer; user-select: none; position: sticky; top: 0; z-index: 12; border-radius: 6px 6px 0 0;
}
.file-hdr:hover { background: #303030; }
.file-hdr .fname { font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: var(--accent); }
.file-hdr .fstats { display: flex; gap: 6px; align-items: center; }
.file-hdr .chev { color: var(--text-muted); transition: transform 0.15s; font-size: 11px; margin-left: 10px; }
.file-hdr .chev.open { transform: rotate(90deg); }
.file-body { display: none; }
.file-body.open { display: block; }
.file-note {
padding: 10px 14px; font-size: 13px; color: var(--text); line-height: 1.55;
border-bottom: 1px solid var(--border); background: var(--surface);
position: sticky; top: 35px; z-index: 11;
}
.file-note strong { color: var(--text-bright); }
.file-note ul { margin-top: 6px; padding-left: 18px; }
.file-note li { margin-bottom: 3px; }
.bp-section { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 8px; }
.bp-hdr { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; cursor: pointer; user-select: none; font-size: 12px; color: var(--text-muted); }
.bp-hdr:hover { background: var(--surface-raised); }
.bp-hdr .chev { color: var(--text-muted); transition: transform 0.15s; font-size: 11px; }
.bp-hdr .chev.open { transform: rotate(90deg); }
.bp-body { display: none; border-top: 1px solid var(--border); }
.bp-body.open { display: block; }
.bp-note { padding: 8px 14px; font-size: 12px; color: var(--text-muted); line-height: 1.5; border-bottom: 1px solid var(--border); background: var(--bg); }
.diff-table { width: 100%; border-collapse: collapse; font-family: 'IBM Plex Mono', monospace; font-size: 12px; line-height: 20px; table-layout: fixed; tab-size: 4; }
.diff-table td { vertical-align: top; white-space: pre; }
.diff-ln { width: 40px; min-width: 40px; max-width: 40px; text-align: right; padding: 0 6px 0 0; color: #555; background: var(--surface); user-select: none; }
.diff-code { padding: 0 12px; color: var(--text); background: var(--bg); overflow-x: auto; }
tr.diff-add .diff-ln { color: #5a8a5a; background: #1a2a1a; }
tr.diff-add .diff-code { background: #1a2a1a; color: #c8e6c8; }
tr.diff-del .diff-ln { color: #8a5a5a; background: #2a1a1a; }
tr.diff-del .diff-code { background: #2a1a1a; color: #e6c8c8; }
tr.diff-ctx .diff-ln { background: var(--surface); }
tr.diff-ctx .diff-code { background: var(--bg); color: var(--text-muted); }
tr.diff-hunk td { background: var(--surface-raised); color: var(--text-muted); font-size: 11px; padding: 3px 12px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
tr.diff-hunk .diff-ln { background: var(--surface-raised); }
tr.diff-moved-del .diff-ln { color: #6a6a8a; background: #1a1a2a; }
tr.diff-moved-del .diff-code { background: #1a1a2a; color: #a8a8d0; }
tr.diff-moved-add .diff-ln { color: #6a6a8a; background: #1a1a2a; }
tr.diff-moved-add .diff-code { background: #1a1a2a; color: #a8a8d0; }
tr.diff-moved-del-edited .diff-ln { color: #7a6a8a; background: #221a2a; }
tr.diff-moved-del-edited .diff-code { background: #221a2a; color: #c0a8d0; }
tr.diff-moved-add-edited .diff-ln { color: #7a6a8a; background: #221a2a; }
tr.diff-moved-add-edited .diff-code { background: #221a2a; color: #c0a8d0; }
.verdict { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 16px 20px; margin-top: 28px; }
.verdict h3 { font-size: 14px; font-weight: 600; color: var(--text-bright); margin-bottom: 8px; }
.verdict ul { padding-left: 18px; }
.verdict li { font-size: 13px; margin-bottom: 6px; line-height: 1.5; }
.verdict li strong { color: var(--text-bright); }
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* INJECT_CSS */
</style>
<script>
/* INJECT_JS */
</script>
</head>
<body>
<!-- INJECT_BODY -->
<script id="pr-diffs-json" type="application/json" data-pr-diffs>{"__PR_DIFFS_PLACEHOLDER__":true}</script>
</body>
</html>
Related skills
How it compares
Choose pr-review-canvas when spatial PR overview matters more than inline line-by-line comment workflows on tiny patches.
FAQ
What does pr-review-canvas do in Cursor?
pr-review-canvas is a Cursor plugin skill that presents pull request changes on a visual canvas so reviewers see spatial relationships between files instead of only scrolling a unified diff. The skill is listed on skills.sh with 440 installs.
When should developers use pr-review-canvas?
pr-review-canvas fits multi-file pull requests where module boundaries, related edits, and change clustering are hard to read in a standard diff view. Developers invoke it during PR review before approving or requesting changes.