Skip to content

feat(web): theme wallpaper support - #6101

Open
pranav100000 wants to merge 2 commits into
pingdotgg:mainfrom
pranav100000:feat/theme-wallpaper-5661
Open

feat(web): theme wallpaper support#6101
pranav100000 wants to merge 2 commits into
pingdotgg:mainfrom
pranav100000:feat/theme-wallpaper-5661

Conversation

@pranav100000

@pranav100000 pranav100000 commented Aug 11, 2026

Copy link
Copy Markdown

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

  • Adds wallpaperImage and wallpaperOpacity fields to ClientSettingsSchema and ClientSettingsPatch, with validation enforcing a max data URL length and opacity clamped to 5–80 (default 15).
  • Introduces a WallpaperAppearanceSync component in the root layout that applies wallpaper preferences to the DOM via CSS variables (--wallpaper-image, --wallpaper-opacity) and a data-wallpaper attribute on the root element.
  • Adds CSS rules in index.css that render the wallpaper as a fixed ::before layer, make the sidebar translucent using glass opacity, and clear canvas backgrounds to reveal the wallpaper.
  • Extends the appearance settings panel with controls to pick, preview, and clear a wallpaper image, and an opacity slider with live preview via previewAppearanceWallpaperOpacity.
  • Adds prepareWallpaperImage to validate and compress picked files, rejecting files that are too large or undecodable with explicit error reasons.
  • Behavioral Change: when a wallpaper is active, syncBrowserChromeTheme resolves the browser chrome color from --app-chrome-background instead 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) and wallpaperOpacity (5–80%, default 15) join client settings schema and patches; desktop client settings tests updated.

Runtime: appearanceWallpaper sets data-wallpaper, --wallpaper-image, and --wallpaper-opacity on the document root (with URL escaping for CSS safety). WallpaperAppearanceSync in 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, isDecodableImage for verbatim files). Opacity slider previews live on the root and commits once per drag to avoid rewriting huge data URLs.

Styling: index.css adds a fixed ::before layer with theme gradient wash; workspace backgrounds go transparent so the wallpaper shows through; desktop sidebar uses glass translucency. syncBrowserChromeTheme uses opaque chrome when data-wallpaper is set so theme-color is 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.

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.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6eedd76f-67b8-45b5-956f-57ad1e3c6908

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Aug 11, 2026
Comment thread apps/web/src/components/settings/SettingsPanels.tsx
Comment thread apps/web/src/components/settings/SettingsPanels.tsx
@pranav100000
pranav100000 marked this pull request as ready for review August 11, 2026 07:53
Comment thread apps/web/src/components/settings/SettingsPanels.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@macroscopeapp

macroscopeapp Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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/*"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +1089 to +1091
<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."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread apps/web/src/index.css
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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" };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 672dffb. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: wallpaper (background image) support in the new theme system

1 participant