Repository navigation
Redesign changelog page with feature highlights - #630
Conversation
Major releases now show narrative summaries, feature highlight cards (with image support for screenshots), colored section badges, and contributor avatars. Minor releases stay compact. Adds changelog-media.ts as a supplementary data layer alongside CHANGELOG.md.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a changelog media data module and updates the changelog page to render optional hero/feature images, contributor avatars, and section badges; adds a footer "View Changelog" link, allows remote PNGs from GitHub, and moves documentation prose styles into an Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Greptile SummaryRedesigns the changelog page with visual hierarchy and feature highlights. Major releases now display narrative summaries, 2-column feature cards with optional images, and colored section badges (green for Added, blue for Changed, amber for Fixed, purple for Contributors). Minor releases remain compact bullet lists. Contributor avatars are fetched from GitHub and displayed inline.
The implementation is solid but has two cleanup items: a ternary operator bug in Confidence Score: 4/5
Important Files Changed
Last reviewed commit: 04aa009 |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
web/app/docs/changelog/page.tsx (2)
131-138: Redundant ternary with identical branches.
hasAnyImageis computed but both branches of the ternary produce the same class string:hasAnyImage ? "grid-cols-1 sm:grid-cols-2" : "grid-cols-1 sm:grid-cols-2"Simplify by removing the condition or implement the intended layout variation.
♻️ Proposed simplification
- const hasAnyImage = media.features.some((f) => f.image); - return ( - <div - className={`mt-4 mb-6 grid gap-3 ${ - hasAnyImage ? "grid-cols-1 sm:grid-cols-2" : "grid-cols-1 sm:grid-cols-2" - }`} - > + <div className="mt-4 mb-6 grid gap-3 grid-cols-1 sm:grid-cols-2">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/app/docs/changelog/page.tsx` around lines 131 - 138, The ternary in the className for the div after computing hasAnyImage (via media.features.some((f) => f.image)) uses identical branches, so remove the redundant conditional and simplify the className to the single intended string (or, if different layouts were intended, update the true/false branches to the correct class names); update the JSX where the div's className is set (the element using hasAnyImage) to either a plain string "grid-cols-1 sm:grid-cols-2" or to the correct differing classes for each branch.
108-113:majorvariable is computed but never used for differentiation.The
isMajorReleasefunction returns a boolean that's stored inmajor(line 244), but the conditional at lines 250-252 applies identical empty strings for both cases:className={`... ${major ? "" : ""}`}Either remove the unused
majorcomputation or implement the intended styling difference.♻️ Option 1: Remove unused code
- const major = isMajorRelease(v, media); return ( <article key={v.version} id={`v${v.version}`} - className={`relative border-t border-border py-8 first:border-t-0 ${ - major ? "" : "" - }`} + className="relative border-t border-border py-8 first:border-t-0" >Also applies to: 244-244, 250-252
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/app/docs/changelog/page.tsx` around lines 108 - 113, The variable `major` computed via the `isMajorRelease` function is unused in the className conditional; either remove the `major` computation (and the `isMajorRelease` call) to simplify the code, or update the className template that currently reads `className={\`... ${major ? "" : ""}\`}` to apply distinct classes for major vs non-major releases; locate the `isMajorRelease` function and the `major` variable usage in the component and either delete the `major` variable and related ternary or replace the empty-string branches with the intended CSS class names to effect the visual differentiation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@web/app/docs/changelog/page.tsx`:
- Around line 131-138: The ternary in the className for the div after computing
hasAnyImage (via media.features.some((f) => f.image)) uses identical branches,
so remove the redundant conditional and simplify the className to the single
intended string (or, if different layouts were intended, update the true/false
branches to the correct class names); update the JSX where the div's className
is set (the element using hasAnyImage) to either a plain string "grid-cols-1
sm:grid-cols-2" or to the correct differing classes for each branch.
- Around line 108-113: The variable `major` computed via the `isMajorRelease`
function is unused in the className conditional; either remove the `major`
computation (and the `isMajorRelease` call) to simplify the code, or update the
className template that currently reads `className={\`... ${major ? "" : ""}\`}`
to apply distinct classes for major vs non-major releases; locate the
`isMajorRelease` function and the `major` variable usage in the component and
either delete the `major` variable and related ternary or replace the
empty-string branches with the intended CSS class names to effect the visual
differentiation.
| className={`mt-4 mb-6 grid gap-3 ${ | ||
| hasAnyImage ? "grid-cols-1 sm:grid-cols-2" : "grid-cols-1 sm:grid-cols-2" | ||
| }`} |
There was a problem hiding this comment.
ternary returns identical values in both branches - hasAnyImage is unused
| className={`mt-4 mb-6 grid gap-3 ${ | |
| hasAnyImage ? "grid-cols-1 sm:grid-cols-2" : "grid-cols-1 sm:grid-cols-2" | |
| }`} | |
| className={`mt-4 mb-6 grid gap-3 grid-cols-1 sm:grid-cols-2`} |
| const major = isMajorRelease(v, media); | ||
|
|
||
| return ( | ||
| <article | ||
| key={v.version} | ||
| id={`v${v.version}`} | ||
| className={`relative border-t border-border py-8 first:border-t-0 ${ | ||
| major ? "" : "" | ||
| }`} |
There was a problem hiding this comment.
major variable is computed but never used (ternary on line 251 returns empty string for both branches)
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/app/docs/changelog/page.tsx`:
- Around line 7-14: In pngDimensions, the height is read from the wrong offset
(buf.readUInt32BE(24)); change the second read to buf.readUInt32BE(20) so the
function reads width at offset 16 and height at offset 20 (IHDR bytes 16–23).
Update the comment if needed to reflect the correct byte ranges and keep the
function name pngDimensions and the two buf.readUInt32BE calls as the locations
to modify.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (8)
web/public/changelog/0.60.0-cjk-input.pngis excluded by!**/*.pngweb/public/changelog/0.60.0-devtools.pngis excluded by!**/*.pngweb/public/changelog/0.60.0-tab-context-menu.pngis excluded by!**/*.pngweb/public/changelog/0.61.0-command-palette.pngis excluded by!**/*.pngweb/public/changelog/0.61.0-open-with.pngis excluded by!**/*.pngweb/public/changelog/0.61.0-pin-workspace.pngis excluded by!**/*.pngweb/public/changelog/0.61.0-tab-colors.pngis excluded by!**/*.pngweb/public/changelog/0.61.0-workspace-metadata.pngis excluded by!**/*.png
📒 Files selected for processing (4)
web/app/docs/changelog/changelog-media.tsweb/app/docs/changelog/page.tsxweb/app/page.tsxweb/next.config.ts
| /** Read PNG dimensions from the IHDR chunk (bytes 16-23). */ | ||
| function pngDimensions(filePath: string): { width: number; height: number } { | ||
| const abs = path.join(process.cwd(), "public", filePath); | ||
| const buf = fs.readFileSync(abs); | ||
| return { | ||
| width: buf.readUInt32BE(16), | ||
| height: buf.readUInt32BE(24), | ||
| }; |
There was a problem hiding this comment.
Fix PNG height offset in pngDimensions (currently reading wrong bytes).
Line 13 reads height from byte offset 24, but the IHDR width/height block is bytes 16-23; this yields invalid dimensions and broken image aspect ratios.
🐛 Proposed fix
/** Read PNG dimensions from the IHDR chunk (bytes 16-23). */
function pngDimensions(filePath: string): { width: number; height: number } {
const abs = path.join(process.cwd(), "public", filePath);
const buf = fs.readFileSync(abs);
return {
width: buf.readUInt32BE(16),
- height: buf.readUInt32BE(24),
+ height: buf.readUInt32BE(20),
};
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/app/docs/changelog/page.tsx` around lines 7 - 14, In pngDimensions, the
height is read from the wrong offset (buf.readUInt32BE(24)); change the second
read to buf.readUInt32BE(20) so the function reads width at offset 16 and height
at offset 20 (IHDR bytes 16–23). Update the comment if needed to reflect the
correct byte ranges and keep the function name pngDimensions and the two
buf.readUInt32BE calls as the locations to modify.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
web/app/docs/changelog/page.tsx (1)
7-14:⚠️ Potential issue | 🔴 CriticalFix PNG IHDR height offset in
pngDimensions.Line 13 reads height from byte offset
24; IHDR height is at offset20, so dimensions are incorrect.🐛 Proposed fix
return { width: buf.readUInt32BE(16), - height: buf.readUInt32BE(24), + height: buf.readUInt32BE(20), };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/app/docs/changelog/page.tsx` around lines 7 - 14, The pngDimensions function reads the PNG IHDR chunk incorrectly: change the height offset from 24 to 20 so the IHDR height is read from byte 20; update the return in pngDimensions (which uses buf.readUInt32BE(...)) to read width at 16 and height at 20 to return correct dimensions for the given filePath.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/app/docs/changelog/page.tsx`:
- Around line 8-10: pngDimensions currently reads a file directly and will throw
for missing or non-local/remote image paths, which can 500 the /docs/changelog
page; update pngDimensions to guard against non-local or absent files by first
resolving the path against the public directory and checking existence (e.g.,
with fs.existsSync or a try/catch around fs.readFileSync) and return a safe
fallback (such as default width/height or undefined) when the file is not found
or not local instead of letting the exception propagate; keep the function name
pngDimensions and ensure callers handle the fallback result.
---
Duplicate comments:
In `@web/app/docs/changelog/page.tsx`:
- Around line 7-14: The pngDimensions function reads the PNG IHDR chunk
incorrectly: change the height offset from 24 to 20 so the IHDR height is read
from byte 20; update the return in pngDimensions (which uses
buf.readUInt32BE(...)) to read width at 16 and height at 20 to return correct
dimensions for the given filePath.
| function pngDimensions(filePath: string): { width: number; height: number } { | ||
| const abs = path.join(process.cwd(), "public", filePath); | ||
| const buf = fs.readFileSync(abs); |
There was a problem hiding this comment.
Avoid hard-failing /docs/changelog on missing or remote image sources.
Lines 9-10 throw if the file path is absent/non-local, which can 500 the whole page. Add a safe fallback path in pngDimensions.
🛡️ Proposed hardening
+const FALLBACK_IMAGE_DIMS = { width: 1200, height: 675 };
+
function pngDimensions(filePath: string): { width: number; height: number } {
- const abs = path.join(process.cwd(), "public", filePath);
- const buf = fs.readFileSync(abs);
- return {
- width: buf.readUInt32BE(16),
- height: buf.readUInt32BE(24),
- };
+ if (/^https?:\/\//.test(filePath)) return FALLBACK_IMAGE_DIMS;
+ const rel = filePath.replace(/^\/+/, "");
+ const abs = path.join(process.cwd(), "public", rel);
+ try {
+ const buf = fs.readFileSync(abs);
+ return {
+ width: buf.readUInt32BE(16),
+ height: buf.readUInt32BE(24),
+ };
+ } catch {
+ return FALLBACK_IMAGE_DIMS;
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function pngDimensions(filePath: string): { width: number; height: number } { | |
| const abs = path.join(process.cwd(), "public", filePath); | |
| const buf = fs.readFileSync(abs); | |
| const FALLBACK_IMAGE_DIMS = { width: 1200, height: 675 }; | |
| function pngDimensions(filePath: string): { width: number; height: number } { | |
| if (/^https?:\/\//.test(filePath)) return FALLBACK_IMAGE_DIMS; | |
| const rel = filePath.replace(/^\/+/, ""); | |
| const abs = path.join(process.cwd(), "public", rel); | |
| try { | |
| const buf = fs.readFileSync(abs); | |
| return { | |
| width: buf.readUInt32BE(16), | |
| height: buf.readUInt32BE(24), | |
| }; | |
| } catch { | |
| return FALLBACK_IMAGE_DIMS; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/app/docs/changelog/page.tsx` around lines 8 - 10, pngDimensions currently
reads a file directly and will throw for missing or non-local/remote image
paths, which can 500 the /docs/changelog page; update pngDimensions to guard
against non-local or absent files by first resolving the path against the public
directory and checking existence (e.g., with fs.existsSync or a try/catch around
fs.readFileSync) and return a safe fallback (such as default width/height or
undefined) when the file is not found or not local instead of letting the
exception propagate; keep the function name pngDimensions and ensure callers
handle the fallback result.
Margins were collapsing and conflicting with Tailwind layout utilities in the changelog. Padding doesn't collapse and can't interfere with external spacing set by parent containers.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
web/app/docs/changelog/page.tsx (1)
7-15:⚠️ Potential issue | 🔴 CriticalHarden and correct
pngDimensions(wrong IHDR offset + unhandled file failures).Line 13 reads PNG height from the wrong offset, and the current direct read can throw for missing/non-local assets, breaking
/docs/changelog.🐛 Proposed fix
+const FALLBACK_IMAGE_DIMS = { width: 1200, height: 675 }; + /** Read PNG dimensions from the IHDR chunk (bytes 16-23). */ function pngDimensions(filePath: string): { width: number; height: number } { - const abs = path.join(process.cwd(), "public", filePath); - const buf = fs.readFileSync(abs); - return { - width: buf.readUInt32BE(16), - height: buf.readUInt32BE(24), - }; + if (/^https?:\/\//.test(filePath)) return FALLBACK_IMAGE_DIMS; + const rel = filePath.replace(/^\/+/, ""); + const abs = path.join(process.cwd(), "public", rel); + try { + const buf = fs.readFileSync(abs); + if (buf.length < 24) return FALLBACK_IMAGE_DIMS; + return { + width: buf.readUInt32BE(16), + height: buf.readUInt32BE(20), + }; + } catch { + return FALLBACK_IMAGE_DIMS; + } }#!/bin/bash set -euo pipefail PAGE="$(fd -p 'web/app/docs/changelog/page.tsx' | head -n1)" MEDIA="$(fd -p 'web/app/docs/changelog/changelog-media.ts' | head -n1)" echo "== pngDimensions byte offsets ==" rg -n 'readUInt32BE\(' "$PAGE" echo "== pngDimensions hardening guards ==" rg -n 'FALLBACK_IMAGE_DIMS|https?:\\/\\/|try\\s*\\{|catch' "$PAGE" echo "== Missing local changelog PNG assets ==" python - <<'PY' import re from pathlib import Path media = Path("web/app/docs/changelog/changelog-media.ts") text = media.read_text(encoding="utf-8") pairs = re.findall(r'(?:image|hero):\s*"(/[^"]+\.png)"', text) missing = [p for p in pairs if not Path("web/public" + p).exists()] print("referenced:", len(pairs)) print("missing:", len(missing)) for p in missing: print("-", p) PY🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/app/docs/changelog/page.tsx` around lines 7 - 15, pngDimensions currently reads the PNG IHDR height from the wrong offset and can throw for missing or remote files; update pngDimensions to first guard against non-local paths (e.g., if filePath startsWith 'http' or similar) and missing files by returning a fallback constant (FALLBACK_IMAGE_DIMS), then wrap the file read/parse in try/catch to return the fallback on any error; correct the IHDR offsets to read width = buf.readUInt32BE(16) and height = buf.readUInt32BE(20) (IHDR width at bytes 16-19, height at 20-23), and ensure the function still returns {width:number,height:number} using the fallback on failure.
🧹 Nitpick comments (1)
web/app/docs/changelog/page.tsx (1)
119-131: Avoid settingpriorityon every hero image.
priorityis currently applied to all hero images, which can trigger unnecessary preloads. Limit it to the first/LCP entry.♻️ Proposed refactor
-function HeroImage({ src, version }: { src: string; version: string }) { +function HeroImage({ + src, + version, + priority = false, +}: { + src: string; + version: string; + priority?: boolean; +}) { const { width, height } = pngDimensions(src); return ( <div className="mt-4 mb-6 overflow-hidden rounded-lg border border-border"> <Image src={src} alt={`cmux ${version}`} width={width} height={height} sizes="(max-width: 640px) 100vw, 640px" className="w-full h-auto" - priority + priority={priority} /> </div> ); }- {versions.map((v) => { + {versions.map((v, idx) => { const media = changelogMedia[v.version]; @@ {media?.hero && ( - <HeroImage src={media.hero} version={v.version} /> + <HeroImage src={media.hero} version={v.version} priority={idx === 0} /> )}Also applies to: 246-280
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/app/docs/changelog/page.tsx` around lines 119 - 131, HeroImage currently always sets the Image prop priority which preloads every hero image; change HeroImage({ src, version }) to accept an optional priority boolean (e.g., HeroImage({ src, version, priority }: { src: string; version: string; priority?: boolean }) ) and only pass priority to the next/image component when that prop is true (use priority={priority}). Then update the callers that render the changelog entries (the map that renders the list around lines where entries 246-280 are generated) to pass priority={true} only for the first/LCP entry (use index === 0 or a flag), leaving other calls without the prop so they don’t preload. Ensure you continue to use pngDimensions and Image unchanged except for the conditional priority prop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@web/app/docs/changelog/page.tsx`:
- Around line 7-15: pngDimensions currently reads the PNG IHDR height from the
wrong offset and can throw for missing or remote files; update pngDimensions to
first guard against non-local paths (e.g., if filePath startsWith 'http' or
similar) and missing files by returning a fallback constant
(FALLBACK_IMAGE_DIMS), then wrap the file read/parse in try/catch to return the
fallback on any error; correct the IHDR offsets to read width =
buf.readUInt32BE(16) and height = buf.readUInt32BE(20) (IHDR width at bytes
16-19, height at 20-23), and ensure the function still returns
{width:number,height:number} using the fallback on failure.
---
Nitpick comments:
In `@web/app/docs/changelog/page.tsx`:
- Around line 119-131: HeroImage currently always sets the Image prop priority
which preloads every hero image; change HeroImage({ src, version }) to accept an
optional priority boolean (e.g., HeroImage({ src, version, priority }: { src:
string; version: string; priority?: boolean }) ) and only pass priority to the
next/image component when that prop is true (use priority={priority}). Then
update the callers that render the changelog entries (the map that renders the
list around lines where entries 246-280 are generated) to pass priority={true}
only for the first/LCP entry (use index === 0 or a flag), leaving other calls
without the prop so they don’t preload. Ensure you continue to use pngDimensions
and Image unchanged except for the conditional priority prop.
Block layout was collapsing when articles had media content (h2, feature divs, section divs all rendered at the same position). Switching to display:flex + flex-direction:column on articles and using inline styles for all spacing guarantees proper vertical stacking regardless of docs-content CSS interference.
* Redesign changelog page with feature highlights and visual hierarchy Major releases now show narrative summaries, feature highlight cards (with image support for screenshots), colored section badges, and contributor avatars. Minor releases stay compact. Adds changelog-media.ts as a supplementary data layer alongside CHANGELOG.md. * Use Next.js Image for optimized loading and GitHub avatar remotes * Add screenshots for Open With and Tab Colors features * Add workspace metadata screenshot * Switch to single-column inline layout, add command palette screenshot * Conductor-style titles, narrower body, narrative descriptions, reorder features * Add pin workspace and tab context menu screenshots, remove subtitle * Add View Changelog link to front page, add DevTools to 0.60.0 * Add CJK input screenshot to 0.60.0 * Read real PNG dimensions at build time, add proper sizes attribute * Fix image overflow: wrap in overflow-hidden container, add max-w-full * Fix CSS cascade: move docs-content styles into @layer base Unlayered CSS beats @layer utilities in the cascade, so .docs-content rules (margins, padding, list-style) were overriding Tailwind utilities on ul, li, h2 elements in the changelog page. Moving them into @layer base lets utilities win without needing !important hacks. * Switch docs-content spacing from margin to padding Margins were collapsing and conflicting with Tailwind layout utilities in the changelog. Padding doesn't collapse and can't interfere with external spacing set by parent containers. * Fix changelog layout: use flex column + inline styles for all spacing Block layout was collapsing when articles had media content (h2, feature divs, section divs all rendered at the same position). Switching to display:flex + flex-direction:column on articles and using inline styles for all spacing guarantees proper vertical stacking regardless of docs-content CSS interference. * Remove border from changelog images * Replace devtools screenshot with cmux inspecting cmux.dev
Summary
changelog-media.tsis a supplementary data layer alongsideCHANGELOG.mdfor adding images, summaries, and feature descriptions to major releasesScreenshots needed
Image references are commented out in
changelog-media.ts. To add screenshots, drop PNGs intoweb/public/changelog/and uncomment theimagefields. Planned images:0.61.0-open-with.png(command palette "Open Current Directory in..." results)0.61.0-command-palette.png(general command palette)0.61.0-tab-colors.png(workspace color picker)0.61.0-workspace-metadata.png(sidebar showing PR links, ports, branches)Test plan
/docs/changelog/docs/changelog#v0.61.0)Summary by CodeRabbit