Skip to content

Add pr-screenshots skill for AI-assisted screenshot capture in PRs#1918

Open
amikofalvy wants to merge 1 commit intomainfrom
feat/pr-screenshots
Open

Add pr-screenshots skill for AI-assisted screenshot capture in PRs#1918
amikofalvy wants to merge 1 commit intomainfrom
feat/pr-screenshots

Conversation

@amikofalvy
Copy link
Collaborator

Summary

  • Adds a new pr-screenshots skill that teaches AI coding agents (Cursor, Codex, Claude Code) how to capture, redact, annotate, and embed screenshots in pull requests
  • Includes three working scripts: Playwright-based screenshot capture with sensitive data masking, Sharp-based image annotation (labels, borders, side-by-side stitching), and pre-upload validation that blocks uploads containing leaked secrets
  • Adds comprehensive affected-routes mapping (component file paths → UI routes) and PR body markdown templates

What's included

Skill: .agents/skills/pr-screenshots/

pr-screenshots/
├── SKILL.md                              # Main skill instructions
├── scripts/
│   ├── capture.ts                        # Playwright screenshot capture
│   ├── annotate.ts                       # Sharp-based image annotation
│   └── validate-sensitive.ts             # Pre-upload sensitive data check
└── references/
    ├── affected-routes.md                # File path → UI route mapping (40+ component directories)
    └── pr-templates.md                   # PR body markdown templates

Key features

capture.ts — Playwright-based screenshot capture

  • Local dev support (--base-url http://localhost:3000)
  • Vercel preview support (--base-url https://agents-git-branch-inkeep.vercel.app)
  • Reusable Playwright server mode (--serve / --connect ws://...)
  • Auto-masks sensitive data before capture: password fields, API key inputs, header values, code blocks with tokens/keys
  • Saves DOM text alongside screenshots for validation

annotate.ts — Sharp-based image annotation

  • Label mode: colored header bar ("Before"/"After") with border
  • Stitch mode: side-by-side before/after comparisons

validate-sensitive.ts — Pre-upload sensitive data validation

  • Critical (exit 1): OpenAI/Anthropic/Inkeep/Stripe/AWS/GitHub keys, PEM keys, connection strings
  • Warning: JWTs, Bearer tokens, generic secrets
  • Blocks upload when critical patterns detected

Example: capture → validate → annotate pipeline

# Capture
npx tsx .cursor/skills/pr-screenshots/scripts/capture.ts \
  --base-url http://localhost:3000 --routes "/" --output-dir ./pr-screenshots

# Validate (blocks if secrets found)
npx tsx .cursor/skills/pr-screenshots/scripts/validate-sensitive.ts --dir ./pr-screenshots

# Annotate
npx tsx .cursor/skills/pr-screenshots/scripts/annotate.ts \
  --input ./pr-screenshots/index.png --label "Home Page" --border "#6366f1" \
  --output ./pr-screenshots/index-labeled.png

Dependency changes

  • Added sharp and playwright to root devDependencies
  • Added /pr-screenshots/ to .gitignore (output directory only)

Test plan

  • capture.ts compiles and runs (tested against local dev server)
  • validate-sensitive.ts detects API keys, JWTs, Bearer tokens (tested with synthetic sensitive data)
  • validate-sensitive.ts passes clean files (tested with normal page content)
  • annotate.ts label mode produces labeled PNG with colored border (tested with generated image)
  • annotate.ts stitch mode produces side-by-side comparison (tested with two generated images)
  • Pre-commit hooks pass (biome formatting, AI artifact validation)

Made with Cursor

Adds a new skill that teaches AI coding agents (Cursor, Codex, Claude Code)
to capture, redact, annotate, and embed screenshots in pull requests.

Includes:
- Playwright-based capture script with sensitive data masking
- Sharp-based annotation script (labels, borders, side-by-side stitching)
- Pre-upload validation script that blocks uploads with leaked secrets
- Affected-routes mapping (component files → UI routes)
- PR body markdown templates

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link

vercel bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Feb 11, 2026 2:31am
agents-docs Ready Ready Preview, Comment Feb 11, 2026 2:31am
agents-manage-ui Ready Ready Preview, Comment Feb 11, 2026 2:31am

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 11, 2026

⚠️ No Changeset found

Latest commit: e78cb32

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

7 Key Findings | Risk: Low

This PR adds a well-structured new skill for AI-assisted screenshot capture. The core implementation is solid, but there are several opportunities to improve error handling and documentation accuracy.

🟠⚠️ Major (3) 🟠⚠️

🟠 1) SKILL.md, capture.ts, .gitignore Path prefix inconsistency (.cursor vs .agents)

Issue: Documentation and comments throughout the skill reference .cursor/skills/pr-screenshots/ instead of the canonical .agents/skills/pr-screenshots/ path. While this works due to symlinks, it creates confusion and fragility.

Why: Users on non-Cursor harnesses (Claude Code, Codex) see Cursor-specific paths. If the symlink structure changes, documented paths break. The canonical source of truth per AGENTS.md is .agents/skills/, not .cursor/skills/.

Fix: Replace all occurrences of .cursor/skills/pr-screenshots/ with .agents/skills/pr-screenshots/ in SKILL.md (lines 50, 56, 62, 89, 92, 111, 150, 154, 158) and capture.ts header comments (lines 8, 14).

Refs:

// Findings posted as inline comments:

  • 🟠 Major: capture.ts:152 Missing viewport input validation
  • 🟠 Major: capture.ts:199-204 Catch block swallows all errors, not just timeouts
  • 🟠 Major: capture.ts:109-115 Missing error handling for server startup

🟡 Minor (4) 🟡

// Findings posted as inline comments:

  • 🟡 Minor: annotate.ts:50 SVG attribute not escaped (defense-in-depth)
  • 🟡 Minor: SKILL.md:120 Documentation lists "Email addresses" but script doesn't check
  • 🟡 Minor: SKILL.md:203 Incorrect dependency source for playwright
  • 🟡 Minor: .gitignore:114 Comment references non-canonical path

💭 Consider (3) 💭

💭 1) annotate.ts:106 SVG injection in stitchImages (hardcoded colors)
Issue: Same SVG escaping pattern as the inline comment on line 50, but the colors array is hardcoded so not currently exploitable.
Fix: Apply escapeXml(colors[i]) for defense-in-depth if colors are ever parameterized.

💭 2) annotate.ts:40 Add file existence check for input images
Issue: sharp(inputPath).metadata() throws cryptic errors for missing/corrupt files.
Fix: Add if (!fs.existsSync(inputPath)) throw new Error(\Input file not found: ${inputPath}`);`

💭 3) validate-sensitive.ts:67 Wrap main() in try-catch
Issue: Unlike the other scripts, this one calls main() directly without .catch(), so file read errors produce raw stack traces.
Fix: Wrap in try { main(); } catch (err) { console.error('Validation failed:', ...); process.exit(1); }


💡 APPROVE WITH SUGGESTIONS

Summary: This is a well-designed skill that follows established patterns. The major findings are all about improving error handling and documentation consistency — they don't block functionality but would improve the developer experience. The path prefix inconsistency is the most widespread issue and should be addressed for maintainability. The error handling improvements in capture.ts would help users debug common failures. Nice work on the sensitive data masking approach! 🎉

Discarded (5)
Location Issue Reason Discarded
package.json:94-95 New devDependencies (playwright, sharp) Informational only — both are justified and version pinning follows conventions
SKILL.md:1-5 Frontmatter validation Positive observation — correctly follows pattern, not an issue
capture.ts:165-172 chromium.connect() error handling Merged with server startup error handling (similar pattern, lower priority)
validate-sensitive.ts scanFile error handling Covered by Consider #3 (main() wrapping addresses this)
annotate.ts:106 SVG injection (colors) Covered by Consider #1 (hardcoded values, not exploitable)
Reviewers (3)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-devops 6 1 0 0 3 0 2
pr-review-standards 4 0 1 0 2 0 1
pr-review-errors 5 0 2 0 2 0 1
Total 15 1 3 0 7 0 4

Note: pr-review-errors finding #1 (catch all errors) merged with pr-review-standards finding #3 (double navigation) as they describe the same issue.

}

const routes = routesStr.split(',').map((r) => r.trim());
const [vw, vh] = viewport.split('x').map(Number);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR: Missing viewport input validation

Issue: The viewport string is split and mapped to Number without validation. If a user passes --viewport invalid or --viewport 1280 (missing height), vw or vh will be NaN, causing a confusing Playwright error downstream.

Why: Input validation at parse time provides clear error messages. Users debugging this would otherwise see cryptic Playwright errors about invalid viewport dimensions.

Fix:

Suggested change
const [vw, vh] = viewport.split('x').map(Number);
const [vw, vh] = viewport.split('x').map(Number);
if (!vw || !vh || Number.isNaN(vw) || Number.isNaN(vh)) {
console.error('Invalid viewport format. Use WxH (e.g., 1280x800)');
process.exit(1);
}

Refs:

Comment on lines +199 to +204
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
} catch {
console.log(' networkidle timed out, proceeding with domcontentloaded...');
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR: Catch block swallows all errors, not just timeouts

Issue: The catch block catches ALL errors from the networkidle navigation but only handles timeouts. Non-timeout failures (DNS errors, connection refused, SSL issues, HTTP 4xx/5xx) are silently discarded. The catch { } syntax with no error parameter means the original error is completely lost.

Why: If the target server is down or returns errors, users see "networkidle timed out" followed by the same failure on retry. The actual cause (e.g., ECONNREFUSED, ERR_CERT_AUTHORITY_INVALID) is lost, making debugging difficult.

Fix: Use waitForLoadState after successful navigation, or check error type before falling back:

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
try {
  await page.waitForLoadState('networkidle', { timeout: 10000 });
} catch {
  console.log('  networkidle timed out, proceeding with current state...');
}

Or catch only timeout errors:

try {
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
} catch (error) {
  const isTimeout = error instanceof Error && 
    (error.name === 'TimeoutError' || error.message.includes('timeout'));
  if (isTimeout) {
    console.log('  networkidle timed out, proceeding with domcontentloaded...');
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
  } else {
    throw error; // Re-throw non-timeout errors
  }
}

Refs:

Comment on lines +109 to +115
async function startServer(port: number) {
const server = await chromium.launchServer({
port,
headless: true,
});
console.log(`Playwright server started at: ${server.wsEndpoint()}`);
console.log('Press Ctrl+C to stop.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR: Missing error handling for server startup

Issue: The startServer function has no error handling for chromium.launchServer(). If the port is in use or Playwright fails to launch, the error bubbles up to main().catch() which outputs "Capture failed:" — misleading because server mode isn't capture mode.

Why: Common failures (port conflict, Chrome not installed, permissions) have no specific guidance. Users see confusing error messages that don't indicate they should try a different port or check their Playwright installation.

Fix: Add specific error handling for server startup:

async function startServer(port: number) {
  let server;
  try {
    server = await chromium.launchServer({
      port,
      headless: true,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    if (message.includes('EADDRINUSE')) {
      console.error(`Port ${port} is already in use. Try a different port with --port <number>`);
    } else {
      console.error(`Failed to start Playwright server: ${message}`);
    }
    process.exit(1);
  }
  console.log(`Playwright server started at: ${server.wsEndpoint()}`);
  // ... rest
}

Refs:


const labelSvg = Buffer.from(`
<svg width="${width}" height="${labelHeight}">
<rect x="0" y="0" width="${width}" height="${labelHeight}" fill="${borderColor}" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR: SVG attribute not escaped (defense-in-depth)

Issue: The borderColor parameter is interpolated directly into the SVG fill attribute without sanitization. While escapeXml is correctly used for label text, the color value could contain quotes or other XML-special characters.

Why: While low-risk (Sharp renders server-side, not a browser), this violates defense-in-depth principles. A malformed color value could break SVG parsing.

Fix:

Suggested change
<rect x="0" y="0" width="${width}" height="${labelHeight}" fill="${borderColor}" />
<rect x="0" y="0" width="${width}" height="${labelHeight}" fill="${escapeXml(borderColor)}" />

Refs:

- Tokens (Bearer, JWT, GitHub PATs)
- PEM private keys
- Connection strings with credentials
- Email addresses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR: Documentation lists "Email addresses" but script doesn't check for them

Issue: The documentation states the validation script checks for "Email addresses" but validate-sensitive.ts has no email pattern in its SENSITIVE_PATTERNS array.

Why: Documentation should accurately reflect what the script does. Users relying on email detection would be surprised to find it's not implemented.

Fix: Either remove "Email addresses" from this list, or add the pattern to validate-sensitive.ts:

{ name: 'Email address', pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, severity: 'warning' },

Refs:


| Package | Source | Notes |
|---|---|---|
| `playwright` | `agents-manage-ui` workspace dep | Browser automation |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR: Incorrect dependency source for playwright

Issue: This table states playwright is an agents-manage-ui workspace dep, but this PR adds it directly to root devDependencies in package.json.

Why: Documentation should accurately reflect where dependencies come from. Users looking here for troubleshooting may be confused.

Fix:

Suggested change
| `playwright` | `agents-manage-ui` workspace dep | Browser automation |
| `playwright` | Root dev dep | Browser automation |

Refs:

.pnpm-store/
.vercel

# PR screenshot captures (generated by .cursor/skills/pr-screenshots/)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR: Comment references non-canonical path

Issue: The comment references .cursor/skills/pr-screenshots/ but the canonical source location is .agents/skills/pr-screenshots/. While both work due to symlinks, the canonical path should be used in documentation.

Why: Consistency with the established convention that .agents/skills/ is the source of truth, with .cursor/, .claude/, .codex/ as symlinks per AGENTS.md.

Fix:

Suggested change
# PR screenshot captures (generated by .cursor/skills/pr-screenshots/)
# PR screenshot captures (generated by .agents/skills/pr-screenshots/)

Refs:

  • AGENTS.md — documents the symlink convention

@github-actions github-actions bot deleted a comment from claude bot Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant