Skip to content

fix(vscode): stop theme sync overriding the user's chosen theme (#1053) - #1357

Merged
backnotprop merged 1 commit into
mainfrom
fix/1053-vscode-light-theme
Aug 20, 2026
Merged

backnotprop merged 1 commit into
mainfrom
fix/1053-vscode-light-theme

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

TLDR: Choosing Plannotator's light theme inside a dark VS Code did nothing, because the extension's theme bridge painted the IDE's colors as inline CSS custom properties on <html> and forced the light class to the IDE's theme kind. Inline properties outrank every .theme-* rule, so the user's own choice was unreachable. The bridge now steps aside whenever the user has expressed a preference of their own. Reported by @it-sha in #1053.

Root cause

ThemeProvider.applyThemeClasses (packages/ui/components/ThemeProvider.tsx:65) owns the app's appearance by stamping theme-<palette> and light on <html>, and the palettes are class scoped (.theme-plannotator / .theme-plannotator.light).

The bridge wrote to that same element:

  • apps/vscode-extension/src/vscode-theme.ts:91 (old) set 16 mapped VS Code colors with root.style.setProperty(...). An inline custom property beats any class selector, so whatever palette the user picked was painted over and could never win.
  • apps/vscode-extension/src/vscode-theme.ts:99-102 (old) then ran root.classList.remove("light") and re-added it only for a light IDE, stripping the class the user's Light choice had just produced.

The result for the reported case is the worst of both: dark VS Code tokens under a .light class. It applied once per message and never reconsidered, so a theme change made from Plannotator's settings mid session left the injected colors in place.

Precedence contract

The app owns its own appearance. The bridge only fills in for a user who has expressed no conflicting preference:

  1. A palette the user picked always wins. If <html> carries any palette other than the default, the bridge paints nothing and removes anything it painted earlier.
  2. The mode the app is rendering always wins. VS Code colors are applied only while the app is already showing the same light or dark side the IDE is on. Choosing Light in a dark IDE now yields Plannotator's light theme rather than dark IDE tokens.
  3. System defers to the IDE. Inside VS Code the surrounding system is the editor, so System is the one mode the bridge maps onto the IDE's theme kind.
  4. Every decision is reconciled, not applied once. A MutationObserver on the app's class list re-decides whenever ThemeProvider rewrites it, so switching themes in Settings mid session removes the injected colors immediately, and a theme message that arrives before the app has mounted no longer paints an empty class list.

Failure direction is deliberate: if the default palette id ever drifts from themeRegistry.DEFAULT_COLOR_THEME, the bridge stops syncing and the app renders its own theme, which is safe, rather than painting over it.

Why the cookie seed is part of the fix

Plannotator's own default mode is Dark. With the bridge no longer forcing the mode, a first-time user in a light IDE would have been left with a dark panel, which would have traded one bug for another. So a panel that has never stored a mode now seeds System (applyPanelCookieDefaults in cookie-proxy.ts), which is what "adapts to your VS Code color theme" means here. It is a seed and not a write: an already stored mode is never touched, so it stops applying the moment the user picks a mode. This is the one piece that is a judgement call rather than a straight bug fix, so it is easy to drop if you would rather keep Dark as the in-editor default.

Tests

New apps/vscode-extension/src/vscode-theme.test.ts (8 tests) evaluates the injected listener against an isolated DOM, passing window, document and MutationObserver in as parameters so each test gets its own root element, message listeners and cookie jar, and so the harness can only offer the two DOM surfaces the bridge is allowed to read. vscode-theme.ts previously had no test coverage at all.

Covered: the reported case, sync still working when the modes agree, a chosen palette never being stood in for, overrides being removed when the user switches to Light mid session, System mapping onto the IDE, a pinned Dark surviving a light IDE, and the pre-mount race.

Non-vacuity check: reverting buildThemeListenerScript to its old body and re-running fails 5 to 6 of the 8, including the headline leaves the app's light theme alone in a dark IDE (issue #1053).

Three more tests in cookie-proxy.test.ts cover the seed: absent means System, an existing mode is never overwritten, stored cookies and the auto-close flag survive.

  • bun test apps/vscode-extension/src: 26 pass, 8 skip, 0 fail
  • DOM_TESTS=1 bun test apps/vscode-extension/src/vscode-theme.test.ts: 8 pass
  • bun run typecheck: clean. bun run build:vscode: clean.
  • Registered the DOM gated file in .github/workflows/test.yml alongside the other DOM_TESTS=1 suites.

On the full suite: bun test on this branch reports a few failures beyond the 12 stable baseline ones, all in unrelated server tests (api-404-guard, and a favicon assertion that reads the real ~/.plannotator). This is not from this change. Dropping an empty placeholder test file at the same path on a clean tree reproduces the same class of extra failures, so it is shared process ordering interacting with suite tests that touch the real data directory.

Not done here

No opt-out setting was added. With the precedence fixed, turning sync off is just picking a theme in Plannotator's settings. Say the word if you want an explicit plannotatorWebview.syncTheme toggle as well.

Closes #1053.

AI-assisted (Claude) under maintainer direction.

The theme bridge wrote VS Code's colors as inline custom properties on
<html>, the same element ThemeProvider stamps `theme-<palette>` and
`light` on, and it forced the `light` class to the IDE's theme kind. An
inline property outranks every `.theme-*` rule, so picking Light in a
dark IDE produced dark VS Code tokens sitting under a `.light` class,
and any palette chosen in Plannotator's settings was painted over.

The bridge now reconciles instead of applying once on arrival: VS Code
colors are only painted while the user is on the default palette and the
app is already rendering the IDE's light/dark side, anything it painted
is removed the moment that stops holding, and it no longer writes the
`light` class except to map System onto the IDE's theme kind.

Panels that have never stored a mode seed System, so a first-time user
in a light IDE still gets a light panel now that the bridge does not
force the mode.

Reported by @it-sha.
@backnotprop
backnotprop merged commit 4f80360 into main Aug 20, 2026
28 checks passed
backnotprop added a commit that referenced this pull request Aug 21, 2026
…em (#1362)

#1357 made the panel defer to the app's stored theme mode and seeded
System only when no mode was stored. That helped first-time panels and
nobody else: every panel opened before it already stored `dark`, written
by ThemeProvider on its first mount rather than chosen by anyone, so the
seed never fired and the panel stayed dark in a light IDE. That is issue
#1053 exactly, still broken for the users who reported it.

There is no provenance in the store to read: it is one flat cookie string
in globalState with no timestamps and no per-cookie metadata, and the app
writes the same `plannotator-theme=dark` whether the user picked Dark or
never opened the theme settings. The migration leans on the three signals
that do exist. `light` and `system` are values the auto-seed cannot
produce, so they are choices and are never touched. A new
`plannotator-vscode-seed` marker, written on every load, makes the
re-seed run at most once per store, so a Dark picked afterwards is
permanent. And a mode the user actually picked is recorded server-side in
~/.plannotator/config.json by configStore.set, which configStore.init
applies over the cookie and writes back, so a real choice outranks the
seed and re-asserts itself in the same page load.

What remains is a Dark that exists only as a cookie with nothing in
config.json behind it. That is indistinguishable from the auto-seed and
is reset once: invisible in a dark IDE, and in a light IDE one re-pick
makes it stick for good.

Also fixes the type error #1357 shipped in applyPanelCookieDefaults and
adds the extension's own tsc to CI, which had never run there.

Co-authored-by: Michael Ramos <backnotprop@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Light theme is not working for VS Code plannotator extension in case if IDE has a dark theme.

1 participant