feat(web): theme wallpaper support - #6101
Conversation
Add an optional background image behind the app, set from Settings → Appearance. The image is stored with the client settings as a data URL and paints on a fixed root layer under a wash of the active theme's chrome color, so switching or editing a theme restyles the wallpaper for free. The workspace canvas clears so that layer becomes the canvas, and the sidebar goes translucent at the existing glass-opacity preference. Everything above them — cards, bubbles, the composer, popovers — keeps its own paint and stays readable with no component changes.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc048e2a02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } as CSSProperties; | ||
|
|
||
| const chooseWallpaper = async (file: File) => { | ||
| const compressed = await compressImageForStash(file, MAX_WALLPAPER_IMAGE_DATA_URL_CHARS); |
There was a problem hiding this comment.
Reject oversized wallpaper files before reading them
When a user selects a very large file, compressImageForStash first loads the entire blob with arrayBuffer() and builds an even larger base64 string; the existing 50 MB source ceiling is enforced only by compressImageToByteLimit, not this path. Since this picker has no size guard, a large image can freeze or OOM the renderer before it can report an error. Check file.size before invoking the helper or enforce the source ceiling inside the helper.
AGENTS.md reference: AGENTS.md:L15-L17
Useful? React with 👍 / 👎.
| } as CSSProperties; | ||
|
|
||
| const chooseWallpaper = async (file: File) => { | ||
| const compressed = await compressImageForStash(file, MAX_WALLPAPER_IMAGE_DATA_URL_CHARS); |
There was a problem hiding this comment.
Validate small wallpaper files before accepting them
When an under-budget file is corrupt or merely labeled with an image MIME type, compressImageForStash returns it unchanged without ever decoding it, because decoding occurs only during recompression. The picker therefore clears the error and persists a wallpaper that neither the preview nor the CSS background can render; the accept attribute is only a file-picker hint. Decode the image before saving it or handle preview/decode failure as an unreadable image.
Useful? React with 👍 / 👎.
| wallpaperOpacity >= MIN_WALLPAPER_OPACITY && | ||
| wallpaperOpacity <= MAX_WALLPAPER_OPACITY | ||
| ) { | ||
| updateSettings({ wallpaperOpacity }); |
There was a problem hiding this comment.
Persist opacity once rather than on every slider event
With a wallpaper near the 1.3-million-character limit, every slider step persists the entire settings object. In the web client this repeatedly serializes and synchronously writes the large data URL to localStorage; in the inspected desktop path it sends repeated large IPC payloads and starts independent temp-file/rename writes, whose unsequenced completions can leave an older opacity on disk after a drag. Preview locally while dragging and coalesce persistence at drag end, or serialize the writes.
AGENTS.md reference: AGENTS.md:L15-L17
Useful? React with 👍 / 👎.
ApprovabilityVerdict: Needs human review This PR introduces a new user-facing feature (wallpaper support) with new settings, UI controls, and rendering logic. New features introducing user-facing behavior require human review. Multiple unresolved comments also raise performance and correctness concerns. You can customize Macroscope's approvability policy. Learn more. |
Three problems the review surfaced in the wallpaper picker. A picked file was only ever length-checked. `compressImageForStash` stores anything already under budget verbatim, so it never decodes a small file -- and `accept="image/*"` filters the picker, not the file. A few kilobytes of mislabeled text were therefore persisted as a wallpaper that renders as a broken image, with the "could not be read" error never firing. The verbatim path now decodes before it commits. At the other end, nothing stopped a huge file: reading it into a base64 string is itself what takes the tab down, so the source ceiling the re-encoder already enforces is now checked before the file is touched. Compression is async and nothing marked which selection it belonged to, so picking a second image -- or clearing the wallpaper -- while the first was still compressing let the older result land on top. Each pick now carries a token that clearing also bumps, and only the current one commits. Finally, the opacity slider wrote the settings on every step, which drags the wallpaper's whole data URL to storage a dozen times per drag. Dragging now previews off the root element and persists once the slider is released.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 672dffb728
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /> | ||
| ) : null} | ||
| <input | ||
| accept="image/*" |
There was a problem hiding this comment.
Reject or flatten animated wallpaper files
When an animated GIF, WebP, PNG, or SVG is below the storage budget, prepareWallpaperImage preserves it verbatim, and this unrestricted picker then installs it as a full-viewport fixed background. Such a wallpaper continuously repaints while the app is open and can cause sustained GPU usage, so wallpaper input should either be re-encoded to a static frame or reject animated formats.
AGENTS.md reference: AGENTS.md:L142-L142
Useful? React with 👍 / 👎.
| <SettingsRow | ||
| {...searchableSetting("wallpaper")} | ||
| description="Show an image behind the app. The theme color washes over it, and the sidebar and the app's glass surfaces let it ghost through." |
There was a problem hiding this comment.
Add user documentation for wallpaper settings
This adds a user-visible Appearance preference, including image selection, clearing, and opacity behavior, but the reviewed diff contains no corresponding docs/user/ update. Add shipped-product documentation for the new behavior as required for user-noticeable changes.
AGENTS.md reference: AGENTS.md:L75-L75
Useful? React with 👍 / 👎.
| paint and stays readable with no further changes. The chat header is the | ||
| one such surface that is not a direct child of the inset. */ | ||
| html[data-wallpaper] main[data-slot="sidebar-inset"], | ||
| html[data-wallpaper] main[data-slot="sidebar-inset"] > .bg-background, |
There was a problem hiding this comment.
Clear the pull-request workspace canvas
On the pull-request route, the inset's direct child is the unpainted wrapper at _chat.pull-requests.tsx:1113, while the opaque .bg-background workspace is nested inside it at line 1400. This direct-child selector therefore never clears that surface, so the wallpaper is hidden across the PR workspace even though it appears in chat and Settings; target that nested canvas explicitly or give all workspace canvases a shared marker.
AGENTS.md reference: AGENTS.md:L65-L70
Useful? React with 👍 / 👎.
| return; | ||
| } | ||
| setWallpaperError(null); | ||
| updateSettings({ wallpaperImage: prepared.dataUrl }); |
There was a problem hiding this comment.
Avoid storing wallpaper blobs in every settings write
Once this stores a near-limit 1.3-million-character data URL in ClientSettings, every unrelated client-settings update serializes and persists that blob again. Fresh evidence in the current tree is the adjacent glass-opacity handler at SettingsPanels.tsx:1070-1077, which still calls updateSettings for every drag step; this repeatedly blocks the web renderer on synchronous localStorage writes and sends large concurrent desktop IPC writes despite coalescing only the wallpaper-opacity slider. Store the blob separately from frequently updated settings or coalesce every high-frequency writer.
AGENTS.md reference: AGENTS.md:L15-L17
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 672dffb. Configure here.
| // Re-encoding already decoded the file, so only the verbatim path is unproven. | ||
| if (!compressed.image.recompressed && !(await isDecodableImage(file))) { | ||
| return { ok: false, reason: "unreadable" }; | ||
| } |
There was a problem hiding this comment.
SVG wallpapers falsely rejected
Medium Severity
prepareWallpaperImage probes verbatim files with createImageBitmap via isDecodableImage, but wallpapers are painted as CSS background-image (and previewed with img). Browsers reject SVG File/Blob sources in createImageBitmap, so valid under-budget SVGs from accept="image/*" fail as unreadable even though they would render.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 672dffb. Configure here.


Let users set a background wallpaper behind the app shell, wired into the existing theme system with a Settings control and light/dark handling. Persisted as a theme setting. Tests included (54 passing).
Closes #5661.
Note
Add theme wallpaper support with opacity controls to appearance settings
wallpaperImageandwallpaperOpacityfields toClientSettingsSchemaandClientSettingsPatch, with validation enforcing a max data URL length and opacity clamped to 5–80 (default 15).WallpaperAppearanceSynccomponent in the root layout that applies wallpaper preferences to the DOM via CSS variables (--wallpaper-image,--wallpaper-opacity) and adata-wallpaperattribute on the root element.::beforelayer, make the sidebar translucent using glass opacity, and clear canvas backgrounds to reveal the wallpaper.previewAppearanceWallpaperOpacity.prepareWallpaperImageto validate and compress picked files, rejecting files that are too large or undecodable with explicit error reasons.syncBrowserChromeThemeresolves the browser chrome color from--app-chrome-backgroundinstead of probing translucent surfaces.Macroscope summarized 672dffb.
Note
Medium Risk
Large data URLs in client settings and global CSS/DOM appearance changes affect the whole shell; mitigations include size limits, escaping, and decode checks.
Overview
Adds user-configurable wallpapers in Settings → Appearance: pick an image, adjust wash strength, clear or reset with restore-defaults.
Persistence:
wallpaperImage(data URL, capped) andwallpaperOpacity(5–80%, default 15) join client settings schema and patches; desktop client settings tests updated.Runtime:
appearanceWallpapersetsdata-wallpaper,--wallpaper-image, and--wallpaper-opacityon the document root (with URL escaping for CSS safety).WallpaperAppearanceSyncin the app root applies prefs and re-runs browser chrome theme sync when wallpaper toggles.UI: Image pick goes through
prepareWallpaperImage(size ceiling before read, stash compression budget,isDecodableImagefor verbatim files). Opacity slider previews live on the root and commits once per drag to avoid rewriting huge data URLs.Styling:
index.cssadds a fixed::beforelayer with theme gradient wash; workspace backgrounds go transparent so the wallpaper shows through; desktop sidebar uses glass translucency.syncBrowserChromeThemeuses opaque chrome whendata-wallpaperis set sotheme-coloris not sampled from translucent surfaces.Tests: Contracts, appearance helpers, and wallpaper prep logic covered.
Reviewed by Cursor Bugbot for commit 672dffb. Bugbot is set up for automated code reviews on this repo. Configure here.