본문으로 건너뛰기
cccskills
무료GitHub에 공개

screenshot-pr

Take responsive screenshots of the current branch's frontend work and embed them into the open PR's Screenshots section. Use when the user wants to capture UI screenshots for a pull request, add screenshots to a PR, or run /screenshot-pr.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SatcherInstitute/health-equity-tracker스타 23(GitHub 23 + cccskills 0)
연동 도구GitHubPlaywright

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md14.5 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

/screenshot-pr

Take responsive screenshots that demonstrate what the PR changed — activated feature shots only, never baseline page views. Screenshots are cropped to the feature element (dialog, popover, drawer, etc.) so reviewers see the change without scrolling through a full-page render. Uploads to a public GCS bucket and embeds into the PR's ## Screenshots section. Re-running always replaces the existing Screenshots section with fresh images.

The user may pass explicit routes/selectors as arguments. If none are given, infer from changed files and confirm before proceeding.


Step 0 — Commit any uncommitted changes first

Before screenshotting, ensure all local code changes are committed and pushed. Screenshots taken from a dirty working tree will not match the code in the PR.

cd /path/to/frontend   # absolute path
npm run cleanup

If cleanup modifies files, stage and commit:

git -C /path/to/repo add -u
git -C /path/to/repo status --porcelain

If any tracked files are modified (lines starting with M or M ):

git -C /path/to/repo add frontend/src frontend/CLAUDE.md .claude/skills
git -C /path/to/repo commit -m "$(cat <<'COMMITMSG'
<short description of changes being screenshotted>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
COMMITMSG
)"
FORK_REMOTE=$(git -C /path/to/repo remote -v | grep -i "github.com[/:]${GH_USER}/" | head -1 | awk '{print $1}')
git -C /path/to/repo push $FORK_REMOTE HEAD

Only continue to Step 1 once the working tree is clean and all changes are pushed.


Step 1 — Get PR context

gh pr view --json number,title,body,headRefName

If no open PR is found: print an error and stop.

Extract: number, headRefName, body.


Step 2 — Derive slugs

  • Feature slug: strip feat/, fix/, feature/, chore/ prefix from branch name; replace / and non-alphanumeric chars with -; lowercase; collapse consecutive -.
    • Example: feat/recent-geo-locations → recent-geo-locations
  • Output dir: /tmp/het-screenshots/pr-{number}/{feature-slug}
  • GCS upload prefix: pr-{number}/{feature-slug}/
  • Public URL base: https://storage.googleapis.com/het-pr-screenshots/pr-{number}/{feature-slug}/

Step 3 — Determine what to capture

The goal is screenshots that show the activated feature, cropped tightly to the changed UI. A page in its default/empty state is never useful.

If the user passed explicit routes or selectors: use those. Skip inference.

If no routes given: read the diff and work out the minimum interaction needed to see the change.

git diff --name-only $(git merge-base HEAD origin/main)
git diff $(git merge-base HEAD origin/main) -- frontend/src/

For each changed file, ask:

  1. What URL loads the page containing this component?
  2. What interaction (click, URL param, seeded localStorage) activates it?
  3. What CSS selector uniquely identifies the feature element to crop to?

Scan Playwright tests for ready-made URLs:

grep -rn "goto(" frontend/playwright-tests/ --include="*.ts" | head -20

Scan for useParamState calls to find URL params that open modals/drawers:

grep -rn "useParamState" frontend/src/ --include="*.tsx" --include="*.ts"

Always confirm the plan with the user — list each capture, the activation steps, and the crop selector — before taking any screenshots.


Step 4 — Ensure dev server is running with the right env

A server started as a bare npx vite still answers 200, so a status check alone is not enough. It skips the env-cmd -f .env.localhost wrapper, leaving VITE_BASE_API_URL unset, and every screenshot comes back with an empty page. Require both a healthy response and evidence the process was launched through the npm script:

curl -s -o /dev/null -w "%{http_code}" http://localhost:3000
pgrep -f "env-cmd -f .env.localhost" > /dev/null && echo "env ok" || echo "env missing"

Restart unless the status is 200 and the env check printed env ok:

lsof -ti :3000 | xargs kill -9 2>/dev/null; sleep 1
cd /path/to/frontend
npm run dev > /tmp/het-dev-server.log 2>&1 &
DEV_PID=$!
TIMEOUT=60
until curl -s http://localhost:3000 > /dev/null 2>&1; do
  sleep 1; TIMEOUT=$((TIMEOUT - 1))
  [ $TIMEOUT -le 0 ] && echo "Dev server failed to start" && kill $DEV_PID 2>/dev/null && exit 1
done
echo "Server ready"

Track $DEV_PID for cleanup in Step 9.


Step 5 — Feature screenshots (element-cropped)

Write a temporary Playwright script at frontend/scripts/_screenshots.ts. For each capture:

  1. Navigate to the URL with { waitUntil: 'networkidle' }
  2. Disable animations via an injected style tag
  3. Perform the activation (click button, seed localStorage, set URL param, etc.)
  4. Wait for the feature element to appear
  5. Crop to the feature element using locator.screenshot() — this captures only the element's bounding box, not the full page

Cropping approach — always use boundingBox + clip with padding:

Show the feature element with enough surrounding context that the reviewer can see its boundary, shadow, and relationship to its parent/neighbors. Never crop flush to the element edge.

const PADDING = 80  // pixels of surrounding context on each side
const el = page.locator(CROP_SELECTOR).first()
await el.waitFor({ timeout: 10000 })
const box = await el.boundingBox()
if (box) {
  const clipX = Math.max(0, box.x - PADDING)
  const clipY = Math.max(0, box.y - PADDING)
  const clipRight = Math.min(vp.width, box.x + box.width + PADDING)
  const clipBottom = Math.min(vp.height, box.y + box.height + PADDING)
  await page.screenshot({
    path: join(OUT, filename),
    clip: { x: clipX, y: clipY, width: clipRight - clipX, height: clipBottom - clipY },
  })
}

Adjust PADDING per feature — 80px is the default. Use 120px+ for modals where you want the dimmed backdrop edge visible.

Common crop selectors:

  • MUI Popover/Menu paper: '.MuiPopover-paper'
  • MUI Dialog: '[role="dialog"]'
  • MUI Drawer: '.MuiDrawer-paper'
  • Custom panels/cards: use the outermost container class or data-testid

Viewport heights: 844 mobile, 1024 tablet, 900 desktop.

Key considerations:

  • Use { waitUntil: 'networkidle' } — the dev server fetches live data from GCP.
  • Disable animations before activating the feature: page.addStyleTag({ content: '*, *::before, *::after { animation: none !important; transition: none !important; }' })
  • Create a fresh browser context per viewport so state doesn't bleed.
  • If a feature only renders below a breakpoint, add a maxWidth guard and log a skip message.
  • If the feature element never appears, log a warning and continue — do not crash.

Example script structure:

import { chromium } from '@playwright/test'
import { mkdirSync } from 'node:fs'
import { join } from 'node:path'

const BASE = 'http://localhost:3000'
const OUT = '/tmp/het-screenshots/pr-{number}/{feature-slug}'
const CROP_SELECTOR = '.MuiPopover-paper'  // adjust per feature

const VIEWPORTS = [
  { width: 375,  height: 844,  label: 'mobile'  },
  { width: 768,  height: 1024, label: 'tablet'  },
  { width: 1280, height: 900,  label: 'desktop' },
]

mkdirSync(OUT, { recursive: true })
const browser = await chromium.launch()

for (const vp of VIEWPORTS) {
  const ctx  = await browser.newContext({ viewport: { width: vp.width, height: vp.height } })
  const page = await ctx.newPage()

  // Seed any required state before navigation
  await page.goto(BASE, { waitUntil: 'domcontentloaded' })
  await page.evaluate(() => {
    localStorage.setItem('my-key', JSON.stringify({ /* seed data */ }))
  })

  await page.goto(`${BASE}/exploredata?mls=1.hiv-3.06&mlp=disparity`, { waitUntil: 'networkidle' })
  await page.addStyleTag({ content: '*, *::before, *::after { animation: none !important; transition: none !important; }' })

  // Activate the feature
  await page.getByRole('button', { name: /location name/i }).first().click()
  await page.waitForTimeout(400)

  // Crop to feature element with surrounding context
  const PADDING = 80
  const el = page.locator(CROP_SELECTOR).first()
  try {
    await el.waitFor({ timeout: 10000 })
    const box = await el.boundingBox()
    if (!box) throw new Error('no bounding box')
    const clipX = Math.max(0, box.x - PADDING)
    const clipY = Math.max(0, box.y - PADDING)
    const clipRight = Math.min(vp.width, box.x + box.width + PADDING)
    const clipBottom = Math.min(vp.height, box.y + box.height + PADDING)
    const filename = `feature-name-${vp.label}.png`
    await page.screenshot({
      path: join(OUT, filename),
      clip: { x: clipX, y: clipY, width: clipRight - clipX, height: clipBottom - clipY },
    })
    console.info(`  [${vp.label}] ${filename}`)
  } catch {
    console.warn(`  [warn] feature element not found @ ${vp.width}px`)
  }

  await ctx.close()
}

await browser.close()
console.info('Done.')

Run it:

npx tsx /absolute/path/to/frontend/scripts/_screenshots.ts

After all screenshots are captured, delete the temp script:

rm /absolute/path/to/frontend/scripts/_screenshots.ts

Step 6 — Upload to GCS

Do not use gsutil or gcloud storage. Both read the gcloud user credential store, which this org expires on a RAPT (ReAuth Proof Token) timer. Refreshing a RAPT requires an interactive terminal, and the Bash tool is not one, so those commands fail with ReauthUnattendedError and no amount of re-running fixes it. Making the user run gcloud auth login only buys a few hours before the same wall reappears.

Application Default Credentials are a separate credential of type authorized_user. Its refresh token exchanges for an access token through a plain POST to Google's OAuth endpoint with no reauth challenge, so it keeps working unattended for weeks. Talk to the GCS JSON API directly with it.

Write a throwaway uploader to /tmp/het_upload.py. It reads the ADC file at runtime from the standard path below. Never copy a token, client secret, or refresh token into this skill or into any committed file.

import json, os, sys, urllib.parse, urllib.request
from pathlib import Path

ADC = os.path.expanduser('~/.config/gcloud/application_default_credentials.json')
BUCKET = 'het-pr-screenshots'
PREFIX = 'pr-{number}/{feature-slug}/'
SRC = '/tmp/het-screenshots/pr-{number}/{feature-slug}'
BOUNDARY = 'het-boundary-7f3a'


def token():
    c = json.load(open(ADC))
    data = urllib.parse.urlencode({
        'client_id': c['client_id'],
        'client_secret': c['client_secret'],
        'refresh_token': c['refresh_token'],
        'grant_type': 'refresh_token',
    }).encode()
    url = 'https://oauth2.googleapis.com/token'
    return json.load(urllib.request.urlopen(url, data))['access_token']


def api(tok, url, method='GET', body=None, headers=None):
    h = {'Authorization': 'Bearer ' + tok}
    h.update(headers or {})
    req = urllib.request.Request(url, data=body, headers=h, method=method)
    return urllib.request.urlopen(req)


tok = token()
base = f'https://storage.googleapis.com/storage/v1/b/{BUCKET}/o'

# Clear stale objects from earlier runs so the prefix matches the new run exactly
listing = json.load(api(tok, f'{base}?prefix={urllib.parse.quote(PREFIX)}'))
for obj in listing.get('items', []):
    api(tok, f'{base}/{urllib.parse.quote(obj["name"], safe="")}', method='DELETE')

for path in sorted(Path(SRC).glob('*.png')):
    name = PREFIX + path.name
    meta = json.dumps({'name': name, 'cacheControl': 'no-cache, no-store'}).encode()
    body = (
        f'--{BOUNDARY}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n'.encode()
        + meta
        + f'\r\n--{BOUNDARY}\r\nContent-Type: image/png\r\n\r\n'.encode()
        + path.read_bytes()
        + f'\r\n--{BOUNDARY}--\r\n'.encode()
    )
    api(
        tok,
        f'https://storage.googleapis.com/upload/storage/v1/b/{BUCKET}/o?uploadType=multipart',
        method='POST',
        body=body,
        headers={'Content-Type': f'multipart/related; boundary={BOUNDARY}'},
    )
    print('uploaded', name)
python3 /tmp/het_upload.py && rm /tmp/het_upload.py

Confirm the count matches the number of PNGs captured. If any file is missing, re-run the script — it is idempotent.

If the token refresh itself fails (invalid_grant, or the ADC file does not exist), ADC has genuinely expired or was never created. That is the one case where the user must act: ask them to run gcloud auth application-default login, then retry. This should be rare; ADC outlives the user credential by a wide margin.


Step 7 — Build the Screenshots markdown

Group screenshots by feature/section. Append ?v={unix-timestamp} to every URL (same TS=$(date +%s) across the whole run) to bust GitHub's image proxy cache.

### {Feature Name}

**Mobile (375px)**
![{Feature Name} mobile]({url}/{filename}-mobile.png?v={timestamp})

**Tablet (768px)**
![{Feature Name} tablet]({url}/{filename}-tablet.png?v={timestamp})

**Desktop (1280px)**
![{Feature Name} desktop]({url}/{filename}-desktop.png?v={timestamp})

Separate feature groups with ---. Omit the trailing --- after the last block.


Step 8 — Update the PR body (always replaces)

  1. Fetch the current body if not already held.
  2. Strip everything from ## Screenshots to end-of-string (it's always last).
  3. Append the new Screenshots section.
  4. Write to /tmp/het-screenshots/pr-{number}/body.md and apply:
gh pr edit {number} --body-file /tmp/het-screenshots/pr-{number}/body.md

Step 9 — Cleanup and confirm

[ -n "$DEV_PID" ] && kill $DEV_PID 2>/dev/null
rm -rf /tmp/het-screenshots/pr-{number}

Print:

"✓ {count} screenshots embedded in PR #{number}: {PR URL}"


Notes

  • GCS bucket: het-pr-screenshots — public read, 90-day auto-delete lifecycle.
  • Upload auth: ADC only, via the JSON API (Step 6). gsutil / gcloud storage hit an interactive-only reauth challenge and cannot work from a tool call.
  • Breakpoints: 375px mobile, 768px tablet, 1280px desktop.
  • Crop first: always use locator.screenshot() or clip — never fullPage: true.
  • No baseline shots: only capture the activated/changed UI state.
  • Re-running: always replaces the ## Screenshots section — never appends a second copy.
  • If Playwright browsers aren't installed: run npx playwright install chromium from frontend/.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Force-push the current branch to infra-test to trigger a GCP deploy, wait for it to complete, then run each associated DAG pipeline one at a time and wait for each to finish. On full success, updates the PR description with a checklist confirming real GCP infra-test results. Use when the user wants to verify backend changes on the test environment before merging, or run /infra-test.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SatcherInstitute/health-equity-tracker232026년 10월 24일 업데이트

merge

무료

Force-merge an open PR to main (bypassing review requirements), delete the local branch, pull the updated main, and push it to the personal fork remote. Use when the user wants to close out a PR without waiting for review, or run /merge.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SatcherInstitute/health-equity-tracker232026년 10월 24일 업데이트

next

무료

Figure out the best new thing to work on. Pulls open items from the GitHub project board, weighs assignment and labels, then prioritizes cleanly closeable scoped work and issues that would wrap up a milestone. Use when the user wants to know what to work on next, or run /next.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SatcherInstitute/health-equity-tracker232026년 10월 24일 업데이트

pr

무료

Polish an open PR for review. Classify the diff by bucket (frontend / python / go) and run only the relevant checks — Biome+tsc+Playwright for frontend, black+pylint+pytest for python, build+test for go. Address open review comments, update CLAUDE.md docs if stale, rewrite the PR title and description, then watch CI until all checks pass (diagnosing and reporting any failures). Use when the user wants to close out a PR, verify it's ready for review, or run /pr.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SatcherInstitute/health-equity-tracker232026년 10월 24일 업데이트

Summarize what's new on dev since the last release and investigate for footguns — config/infra changes, breaking changes, anything that could bring down production. Use before cutting a release, or run /pre-release.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SatcherInstitute/health-equity-tracker232026년 10월 24일 업데이트

release

무료

Cut a new production release — pre-flight checks, auto-increment tag, publish GitHub release, monitor prod deploy, verify prod loads, targeted Playwright smoke tests, then trigger nightly E2E. Use when the user wants to ship a release or run /release.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SatcherInstitute/health-equity-tracker232026년 10월 24일 업데이트

SatcherInstitute의 스킬 모두 보기

이 스킬의 문제 신고