Skip to content

Add built-in and custom theme palettes#1183

Draft
t3dotgg wants to merge 2 commits intomainfrom
cursor/t3-code-custom-themes-2fa8
Draft

Add built-in and custom theme palettes#1183
t3dotgg wants to merge 2 commits intomainfrom
cursor/t3-code-custom-themes-2fa8

Conversation

@t3dotgg
Copy link
Member

@t3dotgg t3dotgg commented Mar 18, 2026

What Changed

  • adds a first-class palette system on the web app so settings can switch between multiple built-in color palettes
  • loads custom palettes from a watched themes.json file in the user's .t3 state area and applies them across the app via CSS variables
  • exposes the custom theme file path in settings, shows validation issues, and adds targeted server/web tests for the new config flow

Why

  • T3 Code only supported light/dark/system mode with a single hard-coded palette
  • users need a straightforward way to personalize the UI without patching source files
  • file-backed custom palettes match the existing keybindings config model and let themes hot-reload safely from user-managed config

UI Changes

  • Settings > Appearance now includes built-in palette cards plus custom palettes discovered from themes.json
  • the appearance panel now shows the custom theme file path, an example JSON shape, and validation feedback for malformed entries

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
Open in Web Open in Cursor 

Note

Add built-in and custom theme palettes with server-managed themes.json

  • Introduces a ThemePalette system in themePalettes.ts with built-in palettes and support for custom palettes defined in a server-managed themes.json file
  • Rewrites useTheme.ts to persist palette and theme preference (light/dark/system), apply CSS variable tokens to the document root, and react to custom theme injection at runtime
  • Adds a new Themes service that bootstraps, watches, parses, and validates themes.json, exposing a snapshot and change stream to the WebSocket server
  • Extends serverGetConfig responses to include themesConfigPath and customThemes, and adds an updated discriminator array to serverConfigUpdated push payloads
  • Adds a palette selector UI and "Open themes.json" action to the settings page, with validation issue display for malformed custom themes
  • Behavioral Change: serverConfigUpdated payloads now require an updated array field; existing consumers must handle the new schema
📊 Macroscope summarized 8275681. 21 files reviewed, 3 issues evaluated, 1 issue filtered, 1 comment posted

🗂️ Filtered Issues

apps/server/src/wsServer.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 641: The serverConfigUpdated push notifications for keybindings changes (line 633) and themes changes (line 641) each only include their own event.issues in the issues field. However, the serverGetConfig handler (line 905) returns combined issues: [...keybindingsConfig.issues, ...themesConfig.issues]. This means when a keybindings change push arrives, the client receives only keybinding issues and loses any theme issues (and vice versa). Since the issues array is flat and contains no discriminant to tell keybinding issues from theme issues, the client cannot correctly merge partial updates. Before this change there was only one source of issues so this inconsistency did not exist. [ Failed validation ]

cursoragent and others added 2 commits March 18, 2026 06:24
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

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

Run ID: 418f36a5-5d72-4b73-89e9-2c511f297462

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/t3-code-custom-themes-2fa8
📝 Coding Plan
  • Generate coding plan for human review comments

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

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 18, 2026
Comment on lines 277 to +291
let subscribed = false;
const unsubServerConfigUpdated = onServerConfigUpdated((payload) => {
void queryClient.invalidateQueries({ queryKey: serverQueryKeys.config() });
if (!subscribed) return;
const issue = payload.issues.find((entry) => entry.kind.startsWith("keybindings."));
if (!issue) {
toastManager.add({
type: "success",
title: "Keybindings updated",
description: "Keybindings configuration reloaded successfully.",
});
return;
}
const themeIssue = payload.issues.find((entry) => entry.kind.startsWith("themes."));
const keybindingIssue = payload.issues.find((entry) => entry.kind.startsWith("keybindings."));
const themeUpdated = payload.updated.includes("themes");
const keybindingsUpdated = payload.updated.includes("keybindings");

toastManager.add({
type: "warning",
title: "Invalid keybindings configuration",
description: issue.message,
actionProps: {
children: "Open keybindings.json",
onClick: () => {
void queryClient
.ensureQueryData(serverConfigQueryOptions())
.then((config) => {
const editor = resolveAndPersistPreferredEditor(config.availableEditors);
if (!editor) {
throw new Error("No available editors found.");
}
return api.shell.openInEditor(config.keybindingsConfigPath, editor);
})
.catch((error) => {
void syncThemeConfig().then((config) => {
if (!subscribed || !config) {
return;
}

if (themeUpdated) {
if (themeIssue) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Medium routes/__root.tsx:277

The subscribed guard at line 286 is ineffective because onServerConfigUpdated invokes the listener synchronously during subscription, but the callback schedules syncThemeConfig().then(...) which runs as a microtask. By the time the .then() callback executes, line 365 has already set subscribed = true, so the guard !subscribed at line 286 is always false and duplicate toasts are not suppressed for replayed cached values. Consider adding a synchronous flag check before the async work, or restructuring so the guard is checked synchronously when the listener is first invoked.

    let subscribed = false;
+   let skippedFirstReplay = false;
    const unsubServerConfigUpdated = onServerConfigUpdated((payload) => {
+     if (!subscribed) {
+       skippedFirstReplay = true;
+     }
      void queryClient.invalidateQueries({ queryKey: serverQueryKeys.config() });
      const themeIssue = payload.issues.find((entry) => entry.kind.startsWith("themes."));
      const keybindingIssue = payload.issues.find((entry) => entry.kind.startsWith("keybindings."));
      const themeUpdated = payload.updated.includes("themes");
      const keybindingsUpdated = payload.updated.includes("keybindings");

      void syncThemeConfig().then((config) => {
-       if (!subscribed || !config) {
+       if (skippedFirstReplay || !config) {
          return;
        }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/routes/__root.tsx around lines 277-291:

The `subscribed` guard at line 286 is ineffective because `onServerConfigUpdated` invokes the listener synchronously during subscription, but the callback schedules `syncThemeConfig().then(...)` which runs as a microtask. By the time the `.then()` callback executes, line 365 has already set `subscribed = true`, so the guard `!subscribed` at line 286 is always false and duplicate toasts are not suppressed for replayed cached values. Consider adding a synchronous flag check before the async work, or restructuring so the guard is checked synchronously when the listener is first invoked.

Evidence trail:
apps/web/src/routes/__root.tsx lines 274-365 at REVIEWED_COMMIT (guard implementation and `subscribed = true` assignment); apps/web/src/wsNativeApi.ts lines 45-59 at REVIEWED_COMMIT (synchronous listener invocation with `listener(latestConfig)` at line 53); apps/web/src/components/KeybindingsToast.browser.tsx lines 354-366 (test comment confirming the intent that replayed cached value should NOT produce a toast)

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants