Problem
Before/after screenshots embedded in the "Visual preview" table are full-page, native-resolution PNGs (up to 10,000px tall / 14.4M pixels / 5MB, per shot.ts's own caps) with NO server-side resizing before storage or embedding. The comment markup (unified-comment-bridge.ts) then displays them with only <img width="360" ...> — no height, no actual resize — so every viewer's browser downloads the full-resolution file (potentially multiple MB) just to render a 360px-wide thumbnail.
The only downscale seam in the codebase (image-downscale.ts) exists purely to shrink an image before an AI vision-model call — it never touches the copy that gets stored in R2/S3 and embedded for human viewers, and even that seam is a no-op passthrough in the hosted Cloudflare Worker path (only active in self-host builds via the esbuild swap).
Separately, the table doesn't match the contributor-facing screenshot contract this same bot enforces on PR authors (see e.g. .claude/skills/metagraphed/SKILL.md Phase B2 in JSONbored/metagraphed, and the equivalent contracts in other gate repos): "each image is a clickable thumbnail... with a one-line caption underneath." ORB's own table has only an invisible alt attribute (before /, after / (mobile), etc.) — no visible caption line naming the route/viewport under each thumbnail.
Area
src/review/visual/shot.ts (capture/storage), src/review/visual/image-downscale.ts (the existing but vision-only downscale seam), src/review/unified-comment-bridge.ts (table markup, width="360" hardcode).
Proposal
- Add a real display-image downscale/re-encode step between capture and storage (or between storage and embed) that produces a reasonably-sized thumbnail-appropriate copy — not necessarily replacing the full-res original (which is still useful behind the "click to open full-size" link), but the copy actually EMBEDDED at
width="360" should be a real ~360-720px-wide encode, not the raw capture. Consider whether this needs its OWN storage key (e.g. a -thumb.png sibling) alongside the existing full-res key so the "click for full-size" behavior is preserved.
- Add a visible one-line caption under each thumbnail in the markup
unified-comment-bridge.ts builds — route + viewport (+ theme, when configured) is already computed for the alt text; render it as visible text too, not just the hidden attribute.
- Confirm the
image-downscale.ts seam's hosted-mode no-op is intentional for the VISION use case (it may well be — Browser Rendering wall-clock is the binding constraint there) but make sure the NEW display-resize step doesn't inherit that same "no-op in hosted mode" limitation, since hosted mode is exactly where this matters most (that's where PUBLIC_API_ORIGIN-served/S3-served images reach real GitHub viewers).
Deliverables
Resources
src/review/visual/shot.ts — capture + MAX_SCREENSHOT_* caps
src/review/visual/image-downscale.ts — existing vision-only downscale seam (precedent for the hosted/self-host swap mechanism, scripts/build-selfhost.mjs's esbuild plugin)
src/review/unified-comment-bridge.ts — table markup (width="360" hardcode, missing caption)
.claude/skills/metagraphed/SKILL.md (JSONbored/metagraphed) — the contributor screenshot contract this table should match the shape of
Boundaries
Scope is the IMAGE pipeline and table markup only — not whether/when capture fires (isVisualPath) or the retry budget. Maintainer-only — assigned JSONbored.
Problem
Before/after screenshots embedded in the "Visual preview" table are full-page, native-resolution PNGs (up to 10,000px tall / 14.4M pixels / 5MB, per
shot.ts's own caps) with NO server-side resizing before storage or embedding. The comment markup (unified-comment-bridge.ts) then displays them with only<img width="360" ...>— noheight, no actual resize — so every viewer's browser downloads the full-resolution file (potentially multiple MB) just to render a 360px-wide thumbnail.The only downscale seam in the codebase (
image-downscale.ts) exists purely to shrink an image before an AI vision-model call — it never touches the copy that gets stored in R2/S3 and embedded for human viewers, and even that seam is a no-op passthrough in the hosted Cloudflare Worker path (only active in self-host builds via the esbuild swap).Separately, the table doesn't match the contributor-facing screenshot contract this same bot enforces on PR authors (see e.g.
.claude/skills/metagraphed/SKILL.mdPhase B2 in JSONbored/metagraphed, and the equivalent contracts in other gate repos): "each image is a clickable thumbnail... with a one-line caption underneath." ORB's own table has only an invisiblealtattribute (before /,after / (mobile), etc.) — no visible caption line naming the route/viewport under each thumbnail.Area
src/review/visual/shot.ts(capture/storage),src/review/visual/image-downscale.ts(the existing but vision-only downscale seam),src/review/unified-comment-bridge.ts(table markup,width="360"hardcode).Proposal
width="360"should be a real ~360-720px-wide encode, not the raw capture. Consider whether this needs its OWN storage key (e.g. a-thumb.pngsibling) alongside the existing full-res key so the "click for full-size" behavior is preserved.unified-comment-bridge.tsbuilds — route + viewport (+ theme, when configured) is already computed for thealttext; render it as visible text too, not just the hidden attribute.image-downscale.tsseam's hosted-mode no-op is intentional for the VISION use case (it may well be — Browser Rendering wall-clock is the binding constraint there) but make sure the NEW display-resize step doesn't inherit that same "no-op in hosted mode" limitation, since hosted mode is exactly where this matters most (that's where PUBLIC_API_ORIGIN-served/S3-served images reach real GitHub viewers).Deliverables
width="360"in the Visual preview table is a real downscaled encode, not the raw full-page capture — meaningfully smaller in bytes for a typical routeResources
src/review/visual/shot.ts— capture +MAX_SCREENSHOT_*capssrc/review/visual/image-downscale.ts— existing vision-only downscale seam (precedent for the hosted/self-host swap mechanism,scripts/build-selfhost.mjs's esbuild plugin)src/review/unified-comment-bridge.ts— table markup (width="360"hardcode, missing caption).claude/skills/metagraphed/SKILL.md(JSONbored/metagraphed) — the contributor screenshot contract this table should match the shape ofBoundaries
Scope is the IMAGE pipeline and table markup only — not whether/when capture fires (
isVisualPath) or the retry budget. Maintainer-only — assigned JSONbored.