Skip to content

Commit 3633cff

Browse files
committed
Merge branch 'dev' into new-setup
2 parents ff6a375 + b0812c8 commit 3633cff

280 files changed

Lines changed: 22445 additions & 6700 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
name: pr-visual-writeup
3+
description: Generate a rich GitHub PR description with dashboard/web-UI screenshots and scrolling animations captured from a running dev server, hosted as a GitHub gist, and pushed to the PR via `gh pr edit`. Use this skill whenever the user asks to "make a PR description with screenshots", "write up a PR with visuals", "add screenshots to my PR", "PR description with GIFs / demo / scroll animations", or anything involving turning a code PR into a visual-heavy writeup. Also triggers on phrases like "ship a PR writeup", "PR body with light and dark mode screenshots", "visual PR review", "generate PR body from dev server". The core value is the parallel capture pipeline — multiple browser sessions running concurrently to produce theme/viewport matrix screenshots in roughly the wall-clock time of a single pass.
4+
---
5+
6+
# pr-visual-writeup
7+
8+
Turn a PR into a visual writeup: inspect the diff, capture screenshots + scroll animations from a local dev server across themes and viewports **in parallel**, host everything in a GitHub gist (PAT-only, no browser cookies), compose a rich markdown body, and set it as the PR description.
9+
10+
## When this triggers
11+
12+
- "make me a pr description with screenshots / gifs / videos"
13+
- "pr writeup with visuals"
14+
- "generate pr body from the running dev server"
15+
- "screenshot all the pages this PR changes and put them in the description"
16+
17+
If the user only wants a text-only PR description, don't use this skill — it's for visual-heavy writeups.
18+
19+
## The shape of the work
20+
21+
Five phases. Phases 2 and 3 are the parallel-heavy ones — lean on subagents there.
22+
23+
1. **Scope** — figure out which PR, which routes, which dev server, which auth
24+
2. **Capture (parallel)** — matrix of {page × theme × viewport}, plus scroll animations
25+
3. **Process (parallel)** — convert scroll videos → GIFs (inline-playable), prep gist
26+
4. **Upload** — one gist, one commit, all files; get raw URLs
27+
5. **Compose + set** — markdown body with tables, then `gh pr edit --body-file`
28+
29+
## Phase 1 — Scope
30+
31+
Before you capture anything, know:
32+
33+
- **PR number + repo**`gh pr view <N> --json baseRefName,headRefName,title,url`
34+
- **Changed UI routes**`gh pr diff <N> --name-only` and filter for page/route files. For Next.js look for `**/page*.tsx` / `**/*page-client.tsx`. Map route files to URL paths based on the app router convention. Ignore changes purely in backend / shared components unless they have an obvious UI surface.
35+
- **Dev server port**`lsof -iTCP -sTCP:LISTEN -P -n | grep node` and `curl -s http://localhost:<port>/ | grep -oE '<title>[^<]+</title>'` to identify which port is the dashboard vs. API vs. docs vs. mock-OAuth.
36+
- **Auth flow** — if the app requires login, inspect the sign-in page for the OAuth provider to use, and ask the user (or infer from context) which dev account to sign in as. Mock OAuth servers typically accept any email.
37+
38+
Record these facts somewhere (a scratchpad file under `/tmp/<skill-workspace>/scope.md` is fine) — the parallel subagents in Phase 2 need them.
39+
40+
## Phase 2 — Parallel capture
41+
42+
This is the skill's core trick. You have N pages × M themes × K viewports of screenshots to take. If you do this in one browser, you navigate sequentially — 9 × 2 × 2 = 36 navigations at ~5s each = 3 minutes. If you fan out, you do it in ~45s.
43+
44+
### Fan-out plan
45+
46+
Spawn one subagent per **(theme, viewport)** combination. Each subagent owns a named `agent-browser` session, authenticates once, captures every page in its assigned theme/viewport, and returns the output directory. Typical combinations:
47+
48+
- `light-standard` (1920×1200, theme=light)
49+
- `dark-standard` (1920×1200, theme=dark)
50+
- `light-wide` (2560×1440, theme=light, a subset of pages)
51+
- `dark-wide` (2560×1440, theme=dark, a subset of pages)
52+
53+
Widescreen captures are usually only worth taking for the "flagship" pages (the 3-5 most important ones). Full matrix on every page is overkill.
54+
55+
**Important:** issue all Agent tool calls for capture subagents **in a single assistant message** so they run concurrently. If you spawn them one at a time across turns, you've lost the parallelism.
56+
57+
The exact subagent prompt pattern lives in `references/capture-patterns.md` — read it before spawning.
58+
59+
### Scroll animations
60+
61+
Tables, long lists, and sticky-header surfaces benefit from a short down-and-back-up scroll clip. Don't do this for every page — pick the 2-3 most representative. Record via **frame-by-frame screenshot then ffmpeg stitch**, not `agent-browser record`, because `record` creates a fresh browser context that loses dev-mode auth state. The recipe is in `references/capture-patterns.md`.
62+
63+
### When fan-out is NOT worth it
64+
65+
- Only 1-2 pages total → just run sequentially in the main conversation
66+
- The dev server can't handle parallel logins (rare, but some mock-OAuth servers serialize)
67+
- The user explicitly asks for a quick single-theme capture
68+
69+
## Phase 3 — Process (parallel)
70+
71+
After capture, you have a pile of PNGs and 2-4 WebM scroll clips. The WebMs need to become GIFs because GitHub only inline-plays `.webm` when it's hosted on `user-attachments/...` (a browser-session-only upload path we're avoiding). Gist-hosted `.webm` becomes a plain download link; gist-hosted `.gif` plays inline.
72+
73+
Run all ffmpeg conversions in parallel using shell `&`:
74+
75+
```bash
76+
for f in *.webm; do
77+
(ffmpeg -y -i "$f" -vf "fps=8,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" "${f%.webm}.gif" >/dev/null 2>&1) &
78+
done
79+
wait
80+
```
81+
82+
`fps=8,scale=960` keeps file sizes reasonable (100-400KB) while still looking smooth.
83+
84+
## Phase 4 — Upload via gist (no browser cookies)
85+
86+
Gist-hosting via `git push` with a PAT is the PAT-only equivalent of `user-attachments`. GitHub's `user-attachments` endpoint requires a browser session cookie (not a PAT) — **don't** use tools like `gh-image` unless the user has explicitly opted in. Gist URLs look like `https://gist.githubusercontent.com/<user>/<gist-id>/raw/<filename>` and render inline as images/GIFs in PR bodies.
87+
88+
Full recipe is in `references/gist-upload.md`. Summary: create a public gist via `gh gist create`, clone it, copy all PNGs + GIFs in, commit, `git push` with a credential-helper trick that feeds the PAT. One push, all files.
89+
90+
## Phase 5 — Compose the body, then `gh pr edit`
91+
92+
Markdown structure template is in `references/pr-body-template.md`. The load-bearing patterns:
93+
94+
- **Summary** paragraph + `Base: → Head:` + scope line (files, +lines)
95+
- **Screenshots** section with one subsection per "flagship" page, each using a 2-col light/dark table, then a widescreen table
96+
- **Other migrated surfaces** compact table for the long tail
97+
- **Scroll behaviour** section with a light/dark GIF table
98+
- Everything after the visual section is the usual PR body: What's new, Notes for reviewers, Test plan
99+
100+
Set it with:
101+
```bash
102+
gh pr edit <N> --body-file <path-to-md>
103+
```
104+
105+
Confirm with the user before pushing if the PR is on a public repo — this is a shared-state action. On a personal fork or draft PR, go ahead.
106+
107+
## A note on trust boundaries
108+
109+
Three distinct credentials touch this workflow. Keep them straight:
110+
111+
- **PAT** (`gh auth token`) — for gist push, `gh pr edit`, `gh pr diff`. Fine to use freely.
112+
- **Dev-server session cookie** — for logging into the local dashboard. Local to the machine, fine.
113+
- **github.com browser session cookie** — what `gh-image` and similar tools extract. **Don't** use this unless the user opts in. It has broader scope than a PAT.
114+
115+
The workflow above deliberately stays in PAT territory.
116+
117+
## Bundled scripts
118+
119+
Use these — don't reinvent them inline. They live at `scripts/` relative to this SKILL.md.
120+
121+
- **`detect_dev_server.sh [min-port] [max-port]`** — lists running node dev servers with their HTML `<title>` so you can pick the right port at a glance.
122+
- **`convert_clips.sh <dir>`** — converts every `.webm` in a directory to `.gif` in parallel (fps=8, 960px wide, ~400KB per clip).
123+
- **`upload_gist.sh <desc> <dir> [<dir> ...]`** — creates a public gist, pushes every file from the input dirs into it in one commit, prints one line per file as `<basename>\t<raw-url>`. Stashes the gist id in `./gist-id.txt`.
124+
125+
## What you bundle in the workspace
126+
127+
Create a `/tmp/pr-<N>-visuals/` workspace to hold everything. After the PR body is set, the PNGs/GIFs live permanently in the gist; the local copies are safe to delete but useful to keep around if the user wants to iterate.
128+
129+
```
130+
/tmp/pr-<N>-visuals/
131+
├── scope.md # phase 1 output
132+
├── shots/ # captured PNGs
133+
├── clips/ # webm + gif scroll animations
134+
├── body.md # composed PR description
135+
├── gist-id.txt # for re-pushing if you add more shots later
136+
└── urls.txt # raw URL per file, for copy-paste
137+
```
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Capture patterns
2+
3+
Reconstructed from SKILL.md hints — edit freely to match your own `agent-browser` or Playwright setup.
4+
5+
## Subagent prompt pattern (per theme/viewport)
6+
7+
Each subagent owns a named browser session and handles the full capture pass for one `(theme, viewport)` combination. Name the session so it's distinct from the main conversation's session and from sibling subagents — e.g. `pr<N>-light-standard`.
8+
9+
Template:
10+
11+
```
12+
You are capturing screenshots for PR #<N> in the <theme> theme at <WxH> viewport.
13+
14+
Scope (from /tmp/pr-<N>-visuals/scope.md):
15+
- Dev server: http://localhost:<port>
16+
- Login: <email> via mock-OAuth at <sign-in path>
17+
- Pages (relative URLs):
18+
- /projects/<projectId>/<route-1>
19+
- /projects/<projectId>/<route-2>
20+
- ...
21+
22+
Do this:
23+
1. Start (or reuse) an agent-browser session named "pr<N>-<theme>-<viewport>".
24+
2. Set viewport to <WxH>.
25+
3. Navigate to the sign-in page and complete the mock-OAuth flow once.
26+
4. Switch the app theme to "<theme>" (usually a dropdown in user settings, or
27+
the `prefers-color-scheme` override in devtools — inspect the app first).
28+
5. For each page in the list: navigate, wait for network-idle + any late-mount
29+
skeletons to settle (~1s extra), then take a full-page screenshot.
30+
Save as /tmp/pr-<N>-visuals/shots/<route-slug>__<theme>__<viewport>.png
31+
where route-slug replaces slashes with dashes.
32+
6. Return the list of PNG paths you produced.
33+
34+
Do NOT:
35+
- Close the browser session at the end (other subagents may be using it, and
36+
re-login is wasteful).
37+
- Use agent-browser's `record` action — it spins up a fresh context that
38+
drops dev-mode auth state. For scroll clips see the frame-stitch recipe.
39+
```
40+
41+
Spawn all subagents (one per theme/viewport combination) in a **single assistant message** with multiple `Agent` tool calls. If you send them across turns, you serialize the work and lose the parallelism that justifies the skill.
42+
43+
## Scroll animation recipe (frame-stitch)
44+
45+
`agent-browser record` opens a new browser context, which doesn't inherit your dev-session cookies — the recording lands on the login page. Work around this by taking a burst of screenshots at the current session instead:
46+
47+
```
48+
In session pr<N>-<theme>-<viewport>:
49+
1. Navigate to the target page, wait for settle.
50+
2. window.scrollTo(0, 0).
51+
3. Loop: take screenshot -> scroll by (viewport_height * 0.8) -> sleep 120ms.
52+
Stop when document.scrollingElement.scrollTop stops increasing.
53+
4. Loop back up symmetrically (optional, nicer).
54+
5. Save frames as /tmp/pr-<N>-visuals/clips/<slug>__<theme>/frame-####.png
55+
```
56+
57+
Stitch with ffmpeg:
58+
59+
```bash
60+
ffmpeg -y -framerate 8 -i /tmp/.../clips/<slug>__<theme>/frame-%04d.png \
61+
-c:v libvpx-vp9 -b:v 0 -crf 40 \
62+
/tmp/.../clips/<slug>__<theme>.webm
63+
```
64+
65+
Then `scripts/convert_clips.sh` turns the webm into a gist-friendly GIF.
66+
67+
## Picking the page matrix
68+
69+
- **Full matrix** (every page × every theme × standard viewport): always — this is the bread-and-butter comparison reviewers actually read.
70+
- **Wide viewport** (2560×1440): only the 3–5 flagship pages. The point of wide is to show layout behavior under extra horizontal space, which most pages handle trivially.
71+
- **Scroll animations**: only pages with meaningful vertical content — tables with many rows, sticky headers, settings with lots of sections. A static screenshot of a 3-field form is fine.
72+
73+
If the PR touches 15+ pages, pick ~5 flagships for the hero section and put the rest in a compact "other migrated surfaces" table.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Gist upload (PAT-only)
2+
3+
The bundled `scripts/upload_gist.sh` handles this end-to-end. This doc explains the mechanism so you can diagnose failures or deviate when you need to.
4+
5+
## Why gists and not `user-attachments`
6+
7+
GitHub renders inline images and GIFs in PR bodies from any `https://` URL. Two common hosts:
8+
9+
| Host | Auth | GIF inline? | WebM inline? |
10+
| --- | --- | --- | --- |
11+
| `user-attachments.githubusercontent.com` | Browser session cookie | yes | **yes** |
12+
| `gist.githubusercontent.com/.../raw/...` | PAT (via git push) | yes | no (download link) |
13+
14+
`user-attachments` is nicer (WebM plays inline, smaller files) but requires the browser session cookie, which has broader scope than a PAT and shouldn't be exfiltrated by a CLI tool without explicit user consent. Stick with gists unless the user says otherwise.
15+
16+
## The push trick
17+
18+
`gh gist create` creates the gist and can seed it with one file via stdin. After that, use git to add more files:
19+
20+
```bash
21+
git clone https://gist.github.com/<gist-id>.git
22+
cp *.png *.gif <clone>/
23+
cd <clone>
24+
git add -A
25+
git commit -m "Add assets"
26+
git push
27+
```
28+
29+
The catch: `git push` to a gist over HTTPS needs credentials. The local git credential helper may be configured to answer with a browser session cookie or nothing useful. Override it inline with a one-shot helper that feeds your PAT:
30+
31+
```bash
32+
USER=$(gh api user --jq .login)
33+
TOKEN=$(gh auth token)
34+
35+
git -c credential.helper= \
36+
-c credential.helper="!f() { echo username=$USER; echo password=$TOKEN; }; f" \
37+
push
38+
```
39+
40+
The `credential.helper=` (empty) clears any inherited helpers; the second `-c` installs a single-use function-based helper that answers with the PAT. This does NOT persist — no config is written.
41+
42+
## Flat namespace
43+
44+
Gists don't support subdirectories. If your local layout is `shots/foo.png` and `clips/bar.gif`, everything gets flattened in the gist. Make sure filenames are unique across your input dirs before pushing (`upload_gist.sh` doesn't dedupe — it just cps, so a later cp wins).
45+
46+
## Raw URL shape
47+
48+
```
49+
https://gist.githubusercontent.com/<user>/<gist-id>/raw/<filename>
50+
```
51+
52+
Note: `raw/` without a revision SHA points to HEAD. If you push new commits to the gist later, old raw URLs serve the newest version of that filename, which is usually what you want for iterative PR body updates.
53+
54+
## Re-pushing to an existing gist
55+
56+
If you saved `gist-id.txt` from a prior run, you can re-push to the same gist instead of creating a new one. This keeps the URL stable across iterations (useful if the PR body has already been approved/reviewed). Swap the "create" step for a clone of the existing gist by ID.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# PR body template
2+
3+
Reconstructed from SKILL.md hints. Treat this as a starting point and adapt to the PR's actual content — don't force-fit sections that don't apply.
4+
5+
## Full structure
6+
7+
```markdown
8+
## Summary
9+
10+
<One-paragraph summary of what the PR does and why. Reviewer-oriented, not
11+
changelog-oriented — answer "what am I about to review and why does it exist".>
12+
13+
**Base:** `<base-branch>`**Head:** `<head-branch>`
14+
**Scope:** <N> files changed · +<added> / -<removed> lines
15+
16+
---
17+
18+
## Screenshots
19+
20+
### <Flagship page 1 name>
21+
22+
<One-line description of the page and what changed.>
23+
24+
| Light | Dark |
25+
| --- | --- |
26+
| ![light](<raw-url>) | ![dark](<raw-url>) |
27+
28+
<Widescreen if captured:>
29+
30+
| Wide (light) | Wide (dark) |
31+
| --- | --- |
32+
| ![wide-light](<raw-url>) | ![wide-dark](<raw-url>) |
33+
34+
### <Flagship page 2 name>
35+
36+
...
37+
38+
---
39+
40+
### Other migrated surfaces
41+
42+
| Page | Light | Dark |
43+
| --- | --- | --- |
44+
| Route A | ![](<raw-url>) | ![](<raw-url>) |
45+
| Route B | ![](<raw-url>) | ![](<raw-url>) |
46+
| ... | ... | ... |
47+
48+
---
49+
50+
## Scroll behaviour
51+
52+
| Light | Dark |
53+
| --- | --- |
54+
| ![scroll-light](<raw-url>) | ![scroll-dark](<raw-url>) |
55+
56+
---
57+
58+
## What's new
59+
60+
- bullet 1
61+
- bullet 2
62+
63+
## Notes for reviewers
64+
65+
- Anything tricky, non-obvious, or worth flagging.
66+
- Known follow-ups or things deliberately out of scope.
67+
68+
## Test plan
69+
70+
- [ ] Check X
71+
- [ ] Check Y
72+
- [ ] Visual sanity — the screenshots above are the canonical reference.
73+
```
74+
75+
## Patterns that pull weight
76+
77+
- **Two-column tables for light/dark**: reviewers can scan both themes at once without scrolling vertically. A single-column list of 10 alternating light/dark shots is much harder to parse.
78+
- **Flagship vs. long tail**: promote 3–5 pages with their own subsection + heading. Everything else goes in a compact table. This gives reviewers a clear "start here" signal.
79+
- **Raw URLs, not markdown image links with titles**: keep `![alt](url)` minimal. Long alt text makes the source unreadable for anyone editing later.
80+
- **Scope line up top**: the `files changed · +x/-y` line answers "how big is this" before the reviewer scrolls.
81+
82+
## Anti-patterns
83+
84+
- Don't embed WebM. Gist-hosted WebM renders as a download link, not a player. Convert to GIF first.
85+
- Don't link to the gist itself — link directly to each raw URL so the asset renders inline.
86+
- Don't include a mega-wall of every page × every theme. If the PR touches 15 pages, put 5 in the hero and 10 in the compact table.
87+
- Don't skip the text sections (What's new / Test plan) just because you have pretty pictures. Reviewers still want the prose.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
# Convert every .webm in a directory to .gif in parallel.
3+
# Usage: convert_clips.sh <dir>
4+
#
5+
# Output: same basename, .gif extension, next to source.
6+
# Config: fps=8, scale=960 — tuned for gist-hosted PR body embeds
7+
# (small enough for <400KB, smooth enough to read).
8+
9+
set -euo pipefail
10+
DIR="${1:?usage: $0 <dir>}"
11+
12+
if ! command -v ffmpeg >/dev/null 2>&1; then
13+
echo "ffmpeg not found. Install with: brew install ffmpeg" >&2
14+
exit 1
15+
fi
16+
17+
shopt -s nullglob
18+
clips=("$DIR"/*.webm)
19+
if [ ${#clips[@]} -eq 0 ]; then
20+
echo "no .webm files in $DIR" >&2
21+
exit 0
22+
fi
23+
24+
echo "converting ${#clips[@]} clips in parallel..."
25+
for f in "${clips[@]}"; do
26+
out="${f%.webm}.gif"
27+
(
28+
ffmpeg -y -i "$f" \
29+
-vf "fps=8,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
30+
"$out" >/dev/null 2>&1 \
31+
&& echo "$(basename "$out") ($(du -h "$out" | cut -f1))"
32+
) &
33+
done
34+
wait
35+
echo "done."

0 commit comments

Comments
 (0)