Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add built-in and custom theme palettes - #1183

Closed
t3dotgg wants to merge 2 commits into
mainfrom
cursor/t3-code-custom-themes-2fa8
Closed

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

Conversation

@t3dotgg

@t3dotggt3dotgg commented Mar 18, 2026

Copy link
Copy Markdown
Member

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 WebOpen 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 ]

cursoragentand 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
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

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-actionsgithub-actionsBot 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Mediumroutes/__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)

@juliusmarminge

Copy link
Copy Markdown
Member

Closing as part of an open-PR triage — theme/appearance work is being consolidated in #1550.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@t3dotgg@juliusmarminge@cursoragent