Skip to content

feat(themes): split palette/appearance into orthogonal axes with per-palette fonts#245

Open
luusinn wants to merge 18 commits into
OpenCoworkAI:mainfrom
luusinn:feat/theme-palettes
Open

feat(themes): split palette/appearance into orthogonal axes with per-palette fonts#245
luusinn wants to merge 18 commits into
OpenCoworkAI:mainfrom
luusinn:feat/theme-palettes

Conversation

@luusinn

@luusinn luusinn commented Jun 14, 2026

Copy link
Copy Markdown

Summary

Splits the theme model into two orthogonal axes and adds per-palette typography, plus a fix for a critical Tailwind CSS bug that was silently dropping all palette rules.

Two orthogonal axes

The old model conflated two unrelated concerns into one flat AppTheme union: which color palette (Nord, Tokyo Night, ...) and which light/dark mode. Every palette shipped as a single fixed mode (only Solarized was light), so users could not get Tokyo Night in light, Nord in light, etc. Typography was also hardcoded globally, so switching palettes only ever changed colors.

This splits the model:

  • theme (AppTheme) — the palette: claude | nordic | tokyo-night | gruvbox | catppuccin | rose-pine | solarized
  • appearance (AppAppearance) — dark | light | system, applied on top of whichever palette is selected (system defers to the OS preference as before).

Every palette ships both light and dark

7 palettes × 3 CSS blocks each (base + dark + light) = 21 rules, each authentic to the palette's design language. Tokyo Night Light, Nord Light, Catppuccin Latte, Solarized Dark, etc. all work now.

Per-palette typography

Each palette sets --font-sans / --font-serif / --font-mono, and body, .heading-serif, .prose-chat, headings, and tailwind.config.js fontFamily all consume them:

Palette Sans Serif Mono
Claude Plus Jakarta Sans Source Serif 4 JetBrains Mono
Nordic Inter Source Serif 4 JetBrains Mono
Tokyo Night Outfit Source Serif 4 JetBrains Mono
Gruvbox Bitter (slab) Bitter JetBrains Mono
Catppuccin Nunito Source Serif 4 Fira Code
Rosé Pine Outfit Source Serif 4 JetBrains Mono
Solarized Source Sans 3 Source Serif 4 Source Code Pro

Settings UI

Separate appearance row (Light / Dark / System) above the palette grid, so the two axes are independently selectable. Sidebar's quick-cycle button now cycles the appearance axis.

Backward compatible

Old configs with theme: 'dark'/'light'/'system' (the legacy single-axis model) auto-migrate to theme: 'claude', appearance: <old value> in the config normalizer, so existing users keep a working theme without touching their config file.

Critical bug fix (Tailwind 3)

All .theme-* palette rules were inside @layer base in globals.css. Tailwind 3's PostCSS was silently dropping them during compilation — verified via CDP: :root and .light survived, the 6 palettes vanished. So clicking a swatch added the class to <html> but no CSS matched and colors never changed. The palettes are now top-level CSS (outside any @layer), which is not subject to Tailwind's layer tree-shaking.

Files

  • config-store.ts — new AppTheme/AppAppearance types, isAppearance, VALID_APPEARANCES, legacy-mapping in the normalizer, appearance in AppConfig/DIRECT_READ_KEYS/defaultConfig/get() guard/update() merge.
  • theme-resolution.ts — rewritten for the new API (getSavedAppearance, getSavedPalette, resolveEffectiveAppearance, resolveNativeThemeSource).
  • main/index.ts — uses appearance for nativeTheme.themeSource + window background; IPC settings.update accepts both axes.
  • globals.css — 21 palette blocks (7 × base/dark/light), each with --color-*/--shadow-*/--font-*; body/.heading-serif/.prose-chat use font vars.
  • tailwind.config.jsfontFamily now references the --font-* vars.
  • App.tsx — applies both .theme-<palette> and .dark/.light to <html>.
  • SettingsGeneral.tsx — separate appearance row + palette grid (7 swatches).
  • Sidebar.tsx — quick-cycle button cycles appearance.
  • useIPC.ts, store/index.ts, types/index.ts — mirror the new shape.
  • i18n en.json / zh.json — labels for 7 palettes + appearance.
  • Tests rewritten for the new API.

Test plan

  • npm run typecheck — clean
  • npx eslint src --ext .ts,.tsx — 0 errors (8 pre-existing warnings)
  • npx vitest run src/tests/config/theme-*.test.ts — 31/31 pass
  • CDP-verified all 21 palette CSS rules compile into the live stylesheet
  • Backward-compat: legacy theme: 'dark' config normalizes to theme: 'claude', appearance: 'dark'
  • Manual: toggle appearance × palette combinations, confirm both colors and fonts change

🤖 Generated with Open Cowork

…vbox, Catppuccin, Rosé Pine, Solarized Light)

- Extend AppTheme union with 6 named palettes; add isPaletteTheme/isLightTheme
  helpers and expand VALID_THEMES in config-store.
- Map palette themes to native dark/light in main process so the Electron
  window background and nativeTheme.themeSource follow each palette's mode.
- Add .theme-* CSS variable overrides in globals.css for all 6 palettes.
- Apply palette class on <html> in App.tsx (clearing prior classes first).
- Add a palette swatch picker to SettingsGeneral with live preview swatches.
- Add en/zh i18n labels for the new palettes and the section heading.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: initial

Findings

  • [MAJOR] Missing CSS variable --color-background-tertiary in all palette classes. The original :root and .light rules define this variable (:root: #232323, .light: #f5f5f5), and it may be consumed by components (e.g., inputs, code blocks). When a palette is active, the variable falls back to the :root value, causing visual inconsistency—especially for the Solarized Light palette where it would remain dark. Suggested fix: add --color-background-tertiary to each palette block with an appropriate value (e.g., rgba(0,0,0,0.1) or a derived shade).

    Example for .theme-nordic:

    --color-background-tertiary: #3b4252;
  • [MINOR] The class removal list in src/renderer/App.tsx:101-108 is hardcoded to the six palette names. Any future palette addition will require updating this list. Consider importing THEME_PALETTES from config-store and using a loop or dynamic class removal to reduce maintenance burden.

    Suggested fix:

    import { THEME_PALETTES } from '../../main/config/config-store'; // or share via types
    // … in effect:
    root.classList.remove('light', ...THEME_PALETTES.map(p => `theme-${p}`));
  • [NIT] The isPaletteTheme function uses a redundant type cast (THEME_PALETTES as readonly string[]). Since THEME_PALETTES is already declared as const, the cast is unnecessary. Consider:

    export function isPaletteTheme(theme: string): theme is typeof THEME_PALETTES[number] {
      return (THEME_PALETTES as readonly string[]).includes(theme);
    }

    Or simply return THEME_PALETTES.includes(theme as any);.

  • [NIT] The isLightSwatch variable in SettingsGeneral.tsx:115 is hardcoded to 'solarized-light'. For maintainability, consider referencing LIGHT_PALETTES (once exported or shared) instead.

  • [TEST] No tests added. New theme logic (validation, palette application, native theme mapping) should be covered. At minimum, tests for isPaletteTheme, isLightTheme, resolveEffectiveTheme, and the renderer’s theme application effect are recommended.

Questions

  • None.

Summary

Review mode: initial. The PR cleanly introduces six selectable color palettes by extending the AppTheme type, adding CSS classes, and updating the main process and settings UI. The implementation is well-structured and backward-compatible. The main issue is the missing --color-background-tertiary CSS variable in all palette definitions, which will cause visual glitches for components relying on it. The other issues are minor maintainability concerns.

Open Cowork Bot

- Export VALID_THEMES so tests can assert membership.
- Tighten isPaletteTheme() to a real type guard.
- Mirror THEME_PALETTES / LIGHT_PALETTES / helpers in renderer types so the
  renderer does not import from main/.
- App.tsx derives the class-removal list from THEME_PALETTES instead of a
  hardcoded literal.
- SettingsGeneral uses LIGHT_PALETTES.has(value) for the light-swatch flag.
- Add src/tests/config/theme-palettes.test.ts (14 tests) covering
  isPaletteTheme, isLightTheme, and VALID_THEMES membership.

Verified: tsc clean, vitest 14/14, eslint clean.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [MAJOR] Missing CSS variable --color-background-tertiary in all six palette classes (unchanged from previous review). This variable is defined in the original :root and .light blocks and is likely consumed by components (e.g., inputs, code blocks, modals). When a palette is active, the variable falls back to the :root value, causing visual inconsistency — especially for solarized-light where it would remain dark. Add --color-background-tertiary to each palette block with an appropriate value (e.g., derived from the surface or background color).

    Example for .theme-nordic:

    --color-background-tertiary: #434c5e;

    For each palette, choose a shade that fits the scheme. Existing :root uses #232323 (dark) and .light uses #f5f5f5.

  • [MINOR] The new tests in src/tests/config/theme-palettes.test.ts cover isPaletteTheme, isLightTheme, and VALID_THEMES membership. However, they do not cover the resolveEffectiveTheme function in src/main/index.ts or the theme application useEffect in src/renderer/App.tsx. While the core helpers are now tested, the integration points where palettes are applied remain untested. Consider adding tests for these to ensure correctness across palette changes.

    Suggested test cases for resolveEffectiveTheme:

    • 'nordic' returns 'dark'
    • 'solarized-light' returns 'light'
    • 'system' returns OS-dependent value (mock nativeTheme)

    For the theme application effect, test that the correct class is added/removed on <html> when settings.theme changes.

Questions

  • None.

Summary

Review mode: follow-up after new commits. All minor and nit issues from the previous review (hardcoded class removal list, redundant type cast, hardcoded isLightSwatch, missing tests) have been addressed. The only remaining major issue is the absence of --color-background-tertiary in all palette CSS blocks, which will cause visual glitches for components relying on this variable. The rest of the implementation remains clean and backward-compatible.

Open Cowork Bot

@luusinn

luusinn commented Jun 14, 2026

Copy link
Copy Markdown
Author

Thanks for the review. Pushed a042f1d addressing the actionable items.

Done:

  • isPaletteTheme is now a real type guard (theme is (typeof THEME_PALETTES)[number]) — dropped the as readonly string[] cast.
  • App.tsx derives the class-removal list from THEME_PALETTES instead of a hardcoded literal, so adding a future palette is one change in one place.
  • SettingsGeneral.tsx uses LIGHT_PALETTES.has(value) instead of a string literal for the light-swatch flag.
  • Added src/tests/config/theme-palettes.test.ts (14 tests) covering isPaletteTheme, isLightTheme, and VALID_THEMES membership. All passing.

On the --color-background-tertiary finding (MAJOR) — I believe this one is incorrect.

I could not find that variable anywhere in the codebase, so I grepped before acting on it:

$ grep -n "background-tertiary" src/renderer/styles/globals.css
(no matches)

$ grep -rn "background-tertiary\|bg-background-tertiary" src/ --include="*.ts" --include="*.tsx" --include="*.css"
(no matches)

$ tailwind.config.js → background: { DEFAULT, secondary }   # no tertiary shade

The original :root and .light blocks define --color-background and --color-background-secondary only — there is no --color-background-tertiary, and no component or Tailwind utility references one. So there is nothing for the palette classes to fall back from, and no visual glitch. I left it alone rather than add a variable that nothing consumes. If I missed a spot, please point me at the file/line and I will add it immediately.

Everything else green: tsc --noEmit clean, vitest 14/14, eslint clean (only the 2 pre-existing react-hooks/exhaustive-deps warnings on unrelated lines in App.tsx).

Extract the previously-private getSavedThemePreference / resolveEffectiveTheme
/ applyNativeThemePreference from main/index.ts into a pure, testable module
src/main/config/theme-resolution.ts. main/index.ts now delegates to it, passing
nativeTheme.shouldUseDarkColors in as an argument.

This makes the palette -> native-theme mapping unit-testable without booting
the Electron main process, addressing the follow-up review's MINOR.

Adds src/tests/config/theme-resolution.test.ts (18 tests) covering:
- getSavedThemePreference: known themes pass through, garbage falls back
- resolveEffectiveTheme: every palette + system (both OS prefs)
- resolveNativeThemeSource: maps to dark/light/system

Verified: tsc clean, 32/32 theme tests pass, eslint clean.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [MAJOR] Missing --color-background-tertiary in all six palette CSS classes (unchanged since previous review)

    The original :root and .light blocks in globals.css define --color-background-tertiary (dark: #232323, light: #f5f5f5). None of the six .theme-* blocks set this variable. Components that rely on it (e.g., inputs, code blocks, modals) will fall back to the :root value when a palette is active. This is especially problematic for solarized-light, where the fallback would be a dark background, creating a visual mismatch.

    Suggested fix (example for .theme-nordic; repeat for every palette with an appropriate shade):

    --color-background-tertiary: #434c5e;

    Choose a tertiary shade that fits each palette's scheme (typically between --color-surface and --color-background).

Previously resolved items

  • The MINOR issue from the prior review (missing tests for theme-resolution helpers) has been addressed by extracting theme-resolution.ts and adding theme-resolution.test.ts. The tests now cover getSavedThemePreference, resolveEffectiveTheme, and resolveNativeThemeSource.

Summary

Review mode: follow-up after new commits. The refactoring and test additions are well done. The only remaining issue is the absence of --color-background-tertiary in all palette CSS blocks, which will cause visual glitches for components depending on that variable (especially on solarized-light). Please add it to each palette class.

Open Cowork Bot

@luusinn

luusinn commented Jun 14, 2026

Copy link
Copy Markdown
Author

Thanks for the follow-up. Pushed 0bc16a9.

Re: --color-background-tertiary (MAJOR) — this variable does not exist.

Pushing back with evidence rather than silently adding a variable nothing consumes. Full set of CSS custom properties in globals.css:

$ grep -oE "\-\-[a-z-]+" src/renderer/styles/globals.css | sort -u
--color-accent / -hover / -muted
--color-background
--color-background-secondary          ← no -tertiary
--color-border / -muted / -subtle
--color-error
--color-mcp
--color-success
--color-surface / -active / -hover / -muted
--color-text-muted / -primary / -secondary   ← no -tertiary
--color-warning
--shadow-card / -elevated / -soft

No --color-background-tertiary anywhere — not in :root, .light, or palette blocks. The values cited in the review (#232323 for :root, #f5f5f5 for .light) also do not appear anywhere in src/; the actual backgrounds are #171614 (:root) and #f5f3ed (.light). There is nothing to fall back from and no visual glitch.

For completeness: there is a text-text-tertiary Tailwind utility used in SudoPasswordDialog.tsx:94 and PairingRequestsSection.tsx:37, but --color-text-tertiary is also undefined — that is a pre-existing latent bug in the codebase, unrelated to and not introduced by this PR. Happy to fix it in a separate PR.

If a maintainer can point me at a file:line where --color-background-tertiary is defined or consumed, I will add it to the palette blocks the same day. Otherwise I am leaving this as-is.

Re: untested integration points (MINOR) — fixed.

Extracted the previously-private getSavedThemePreference / resolveEffectiveTheme / applyNativeThemePreference from main/index.ts (2800 lines, imports all of Electron — not unit-testable in isolation) into a pure module src/main/config/theme-resolution.ts. main/index.ts now delegates to it, passing nativeTheme.shouldUseDarkColors in as an argument. Added src/tests/config/theme-resolution.test.ts (18 tests) covering every palette × both OS preferences for system, plus the nativeTheme.themeSource mapping.

On the App.tsx useEffect: the vitest config is environment: 'node' (no DOM), and testing a React effect there means either flipping that file to jsdom (a config-level change) or standing up a renderer component harness — disproportionate for a 12-line effect that only calls classList.remove/add on deterministic theme-${p} names derived from THEME_PALETTES. Happy to add a jsdom-based test if a maintainer wants it, but I would not block merge on it.

State: tsc clean, 32/32 theme tests pass (14 + 18), eslint clean.

@luusinn

luusinn commented Jun 14, 2026

Copy link
Copy Markdown
Author

Same finding as the prior two rounds — already addressed with evidence in #245 (comment) (and the comment before it). Restating briefly so it's not just a "see above":

--color-background-tertiary does not exist in this codebase. Verified again at HEAD (0bc16a9):

$ grep -rn "background-tertiary" src/ --include="*.ts" --include="*.tsx" --include="*.css"
(no matches)

The full set of CSS custom properties defined anywhere in globals.css is: background, background-secondary, surface{,-hover,-active,-muted}, border{,-muted,-subtle}, accent{,-hover,-muted}, text-{primary,secondary,muted}, success/warning/error, mcp, shadow-{soft,card,elevated}. No -tertiary variant of any kind. The values the bot cites (#232323, #f5f5f5) also do not appear in src/.

I'd rather not add a variable that nothing consumes — it would be dead code, and worse, it would set a precedent that hallucinated review findings get merged to make bots go quiet.

Asking for a human maintainer to adjudicate. If anyone on the Open Cowork team can point me at a file:line where --color-background-tertiary is defined or referenced, I will add the palette overrides in the same day. Otherwise I'm treating this as a false positive and leaving it as-is. No new commit this round — nothing actionable to fix.

For the bot's own loop: the previously-resolved MINOR (theme-resolution tests) is acknowledged, thanks. This MAJOR should be marked as resolved/wontfix on the next pass.

…palette fonts

The theme model previously conflated two unrelated concerns into one flat
AppTheme union: which color palette (Nord, Tokyo Night, ...) and which
light/dark mode. Every palette shipped as a single fixed mode (only
Solarized was light), so users could not get Tokyo Night in light, Nord in
light, etc. Typography was also hardcoded globally, so switching palettes
only ever changed colors.

This splits the model into two orthogonal settings:

  - theme (AppTheme): the palette. 'claude' | 'nordic' | 'tokyo-night'
    | 'gruvbox' | 'catppuccin' | 'rose-pine' | 'solarized'
  - appearance (AppAppearance): 'dark' | 'light' | 'system', applied on
    top of whichever palette is selected (system defers to the OS preference
    as before).

Every palette now ships BOTH a .dark and .light variant in globals.css
(21 blocks: 7 palettes x base + dark + light), each redefining the full
--color-* / --shadow-* set. Each palette also sets --font-sans /
--font-serif / --font-mono so typography changes with the palette:
Claude -> Plus Jakarta Sans + Source Serif 4 + JetBrains Mono, Nord ->
Inter + JetBrains Mono, Tokyo Night -> Outfit, Gruvbox -> Bitter slab,
Catppuccin -> Nunito + Fira Code, Rosé Pine -> Outfit, Solarized ->
Source Sans 3 + Source Code Pro. body, .heading-serif, .prose-chat and
tailwind.config.js fontFamily all consume the variables.

App.tsx now applies both .theme-<palette> and .dark/.light classes to
<html>. main/index.ts uses the appearance axis for nativeTheme.themeSource
and window background. Sidebar's quick-cycle button cycles the appearance
axis. SettingsGeneral.tsx has a separate appearance row (Light/Dark/System)
above the palette grid, so the two axes are independently selectable.

config-store normalizes legacy configs: a persisted theme of 'dark'/
'light'/'system' (the old single-axis model) maps to theme='claude',
appearance=<old value>, so existing users keep a working theme without
touching their config file.

Tests rewritten for the new API (isAppearance, getSavedAppearance,
getSavedPalette, resolveEffectiveAppearance). Typecheck clean, lint clean
(pre-existing warnings only), 31/31 theme tests pass.

Palette CSS rules live outside @layer base (top-level) to avoid the
Tailwind 3 tree-shaking bug that previously dropped them silently.
@luusinn luusinn changed the title feat(themes): add 6 selectable color palettes feat(themes): split palette/appearance into orthogonal axes with per-palette fonts Jun 15, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [MAJOR] Missing --color-background-tertiary in all palette CSS blocks (still unresolved from previous review)

    The six .theme-* CSS blocks define most --color-* variables but none define --color-background-tertiary. The :root block still provides a fallback value, but for palettes with significantly different base colors (e.g., Gruvbox dark: #282828, Tokyo Night dark: #1a1b26), the inherited tertiary value may clash. Add an appropriate shade to each palette's .dark and .light block to ensure visual consistency.

    Suggested fix (example for .theme-nordic.dark; repeat for all palettes):

    --color-background-tertiary: #434c5e;

    Choose a shade that fits each palette's hierarchy (typically between --color-surface and --color-background).

  • [MINOR] Legacy solarized-light palette is not migrated to new solarized palette

    Users who had theme: "solarized-light" in their stored config will lose their palette choice on upgrade. The normalizer in config-store.ts (src/main/config/config-store.ts:1059-1066) only maps legacy dark/light/system to the appearance axis, but does not map solarized-lightsolarized. Since isAppTheme() returns false for solarized-light, the theme falls back to claude (the default).

    Suggested fix: Add solarized-light: 'solarized' to the LEGACY_THEME_TO_APPEARANCE object? No, that maps to appearance, not theme. Instead, add a separate map for legacy palette names to new palette names before the fallback, e.g.:

    const LEGACY_PALETTE_MAP: Record<string, AppTheme> = { 'solarized-light': 'solarized' };
    // … inside normalizer:
    theme: isAppTheme(raw.theme) ? raw.theme : (LEGACY_PALETTE_MAP[raw.theme] ?? defaultConfig.theme),

    Also update getSavedPalette in theme-resolution.ts to handle this mapping.

Previously resolved items

  • The previous review's MAJOR item (missing --color-background-tertiary) is not yet resolved — see above.

Summary

Review mode: follow-up after new commits. The refactoring from a single-axis to a two-axis theme model (palette + appearance) is architecturally sound and well-tested. However, two issues remain: (1) all palette CSS blocks still lack --color-background-tertiary, which may produce visual inconsistencies; (2) the legacy solarized-light palette is silently dropped on upgrade. Addressing these will ensure a polished migration.

Open Cowork Bot

Users who had theme: 'solarized-light' in their stored config would lose
their palette choice on upgrade — isAppTheme() rejects the legacy id and
the normalizer fell through to the claude default. Add a
LEGACY_PALETTE_MAP ('solarized-light' -> 'solarized') consulted by both
config-store's normalizeConfig and theme-resolution's getSavedPalette so
the old id resolves to the new one everywhere. Test added.

Addresses review feedback on PR OpenCoworkAI#245 (MINOR). The companion MAJOR finding
(--color-background-tertiary) is a hallucination — that variable is never
defined or consumed anywhere in the codebase (grep confirms zero refs),
so there is nothing to add.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [MAJOR] Missing --color-background-tertiary in all palette CSS blocks (still unresolved from previous review)

    The six .theme-* CSS blocks define most --color-* variables but none define --color-background-tertiary. The :root block still provides a fallback value, but for palettes with significantly different base colors, the inherited tertiary value may clash. Add an appropriate shade to each palette's .dark and .light block to ensure visual consistency.

    Suggested fix (example for .theme-nordic.dark; repeat for all palettes):

    --color-background-tertiary: #434c5e;

    Choose a shade that fits each palette's hierarchy (typically between --color-surface and --color-background).

  • [MINOR] Flash of unstyled content (FOUC) on theme change

    In src/renderer/App.tsx:103-104, the useEffect removes all potential theme classes (including 'light', 'dark', and all .theme-* classes) before adding the new ones. This causes a brief moment where no theme classes are applied, resulting in a flash of default styles. Consider toggling classes individually or applying a CSS transition to smooth the change.

    Suggested fix:

    root.classList.remove('light', 'dark', ...THEME_PALETTES.map((p) => `theme-${p}`));

    could be replaced with removing only the classes that are no longer active by comparing with current settings.

  • [NIT] Duplicate palette list in getSavedPalette

    src/main/config/theme-resolution.ts:40-47 hardcodes the KNOWN array, duplicating THEME_PALETTES from config-store.ts. This could drift if palettes are added or renamed. Import THEME_PALETTES from config-store instead.

    Suggested fix:

    import { THEME_PALETTES, isAppearance, type AppAppearance, type AppTheme } from './config-store';
    // ... then use `THEME_PALETTES` instead of the local `KNOWN` array.

Previously resolved items

  • The legacy solarized-light palette migration issue from the previous review has been fixed: the LEGACY_PALETTE_MAP and updated normalizer now map solarized-lightsolarized, and getSavedPalette handles the mapping correctly. Tests have been updated. ✓

Summary

Review mode: follow-up after new commits. The solarized-light migration has been addressed in this iteration. However, two issues remain: (1) --color-background-tertiary is still absent from all palette CSS blocks, which may produce visual inconsistencies; (2) there is a minor flash of unstyled content when changing themes. Additionally, a nit to deduplicate the palette list in theme-resolution.ts would improve maintainability.

Testing

  • Not run (automation) – but the unit tests for the migration logic pass: 31/31 reported.

Open Cowork Bot

@luusinn

luusinn commented Jun 15, 2026

Copy link
Copy Markdown
Author

Thanks for the follow-up review. Addressed the valid item; pushed back on the other with evidence.

[MINOR] Legacy solarized-light migration — fixed in faff068

Good catch. The normalizer only handled the legacy mode ids (dark/light/system) on the appearance axis, not the renamed palette id. Added LEGACY_PALETTE_MAP = { 'solarized-light': 'solarized' } consulted in two places so the migration is consistent:

  • config-store.ts normalizeConfig — theme resolves solarized-lightsolarized instead of falling through to claude
  • theme-resolution.ts getSavedPalette — same mapping on read, so the main process agrees
  • Test added in theme-resolution.test.ts: getSavedPalette('solarized-light') === 'solarized'

Existing users with theme: 'solarized-light' now keep their palette (just under the new id) on upgrade.

[MAJOR] Missing --color-background-tertiary — not a real finding

This is the same hallucinated variable from the previous review, and it's still not real. Confirmed by grep across the whole codebase (src/renderer for .ts/.tsx/.css/.js + tailwind.config.js):

$ grep -rn "background-tertiary\|backgroundTertiary\|--color-background-tertiary" src/renderer tailwind.config.js
# (no output)

--color-background-tertiary is never defined and never consumed anywhere. The finding's own claim that ":root still provides a fallback value" is incorrect — :root does not define it either. Adding a variable that zero code reads would be dead code, so I'm not adding it. If you can point to a specific consumer (class name, var() reference, Tailwind utility) I missed, I'll add it immediately.

Verification on this push: typecheck clean, 32/32 theme tests pass (was 31, +1 for the new migration test), no debug residue in the diff.

🤖 Generated with Open Cowork

…etSavedPalette

Address review feedback on PR OpenCoworkAI#245:

- App.tsx theme application reordered to add new classes BEFORE removing
  stale ones, closing the one-frame window where no theme class is present
  (flash of unstyled content). classList.add is idempotent so re-adding
  an already-active class is a no-op; only classes no longer in the
  target set get removed.

- getSavedPalette now imports THEME_PALETTES from config-store instead
  of hardcoding a duplicate KNOWN array that would drift if palettes are
  added or renamed.

The MAJOR finding (--color-background-tertiary) remains a hallucination:
grep across src/ and tailwind.config.js confirms the variable is never
defined and never consumed anywhere.
@luusinn

luusinn commented Jun 15, 2026

Copy link
Copy Markdown
Author

Thanks. Two valid findings fixed in e8e8a3b; the third is the same hallucination as before, re-confirmed.

[MINOR] FOUC on theme change — fixed

App.tsx now computes the target class set first, calls classList.add(...target) (idempotent), and only then removes classes no longer in the target. The active theme class is therefore never absent for a frame. classList.add is a no-op for already-present classes, so there's no churn when the theme hasn't changed.

[NIT] Duplicate palette list in getSavedPalette — fixed

theme-resolution.ts now imports THEME_PALETTES from config-store instead of maintaining a parallel KNOWN array. Single source of truth; no drift risk.

[MAJOR] --color-background-tertiary — still a hallucination, re-confirmed

This is the third time this finding has been raised. Re-ran the grep across src/ and tailwind.config.js:

$ grep -rn "\-\-color-background-tertiary" src/ tailwind.config.js
# (no output)
$ grep -rniE "background[-_]tertiary|backgroundTertiary" src/renderer --include="*.ts" --include="*.tsx" --include="*.css"
# (no output)

Zero definitions, zero consumers. The finding's own justification — "The :root block still provides a fallback value" — is factually wrong; :root does not define this variable. I'm not going to add a CSS variable that nothing in the codebase reads; that would be dead code. If there's a specific consumer I'm missing (class name, var() reference, Tailwind utility), point me to it and I'll add the variable immediately.

Verification on this push: typecheck clean, 32/32 theme tests pass, no debug residue.

🤖 Generated with Open Cowork

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [MAJOR] --color-background-tertiary still missing from all palette CSS blocks (unresolved from previous review)

    None of the 7 palette blocks define --color-background-tertiary. The :root block inside @layer base still provides a fallback, but palettes with significantly different base colors may inherit a mismatched tertiary shade. Add an appropriate shade to each palette's .dark and .light block to ensure visual consistency.

    Suggested fix (example for .theme-nordic.dark; repeat for all palettes):

    --color-background-tertiary: #434c5e;

    Choose a shade that fits each palette's color hierarchy (typically between --color-surface and --color-background).

  • [MINOR] Duplicate resolveEffectiveAppearance function

    Both src/main/config/config-store.ts:80-89 and src/main/config/theme-resolution.ts:54-62 define an identical resolveEffectiveAppearance function. The version in config-store.ts is exported but appears unused after the extraction of theme-resolution.ts. This duplication could lead to divergent implementations if one is updated and the other is not. Consider removing the copy in config-store.ts if it is not consumed by any other module.

    Suggested fix:

    // Remove from config-store.ts the export and function definition

    and keep only the version in theme-resolution.ts.

Previously resolved items

  • The FOUC on theme change: the useEffect in App.tsx now adds new classes before removing stale ones, eliminating the flash. ✓
  • The duplicate palette list in getSavedPalette: now uses THEME_PALETTES from config-store. ✓

Summary

Review mode: follow-up after new commits. Two prior issues were fixed in this iteration. One issue remains: missing --color-background-tertiary in all palette CSS blocks. Additionally, a minor code duplication was noticed. No new defects were introduced by this diff.

Testing

Not run (automation) – but the unit tests for the migration logic pass (31/31 reported).

Open Cowork Bot

…helper

User-requested additions and review-bot fixes:

Font family picker (SettingsGeneral):
  New orthogonal 'fontFamily' setting ('auto' | 'sans' | 'serif' | 'mono'
  | 'rounded' | 'condensed' | 'system'). 'auto' inherits the active
  palette's --font-* vars (existing behavior); every other preset applies
  a .font-family-<id> class on <html> that overrides those vars for the
  whole document. Presets cover Plus Jakarta Sans, Source Serif 4, JetBrains
  Mono, Quicksand (rounded), Saira Condensed, and the system stack.

Font size picker (SettingsGeneral):
  New 'fontSize' setting ('sm' | 'md' | 'lg' | 'xl') mapped onto a
  --font-scale CSS var consumed by html { font-size: calc(16px * var()) }.
  Scales the whole document typographically.

  Both new settings round-trip through the full stack: types (main+renderer
  mirror), config-store (types, guards, defaults, normalizer, update merge,
  DIRECT_READ_KEYS), main IPC handler, store defaultSettings, useIPC sync,
  App.tsx class application, globals.css class definitions, i18n (en+zh).

General tab on top (SettingsPanel):
  The General tab (theme/fonts/language/about) is now the FIRST tab and the
  default initialTab. Previously it sat at the bottom and the panel opened
  to API settings, which on a small window (collapsed icon-only sidebar)
  made General effectively unreachable. The content area was already
  scrollable (overflow-y-auto) — the real bug was discoverability.

Review-bot fixes:
  - Removed the duplicate resolveEffectiveAppearance from config-store
    (the canonical, unit-tested copy lives in theme-resolution.ts; only
    that one is imported anywhere). Addresses MINOR finding.
  - --color-background-tertiary remains a hallucination: grep across src/
    and tailwind.config.js shows zero definitions and zero consumers. Not
    adding dead code.

Verification: typecheck clean, lint clean (8 pre-existing warnings, none
new), 32/32 theme tests pass, no debug residue in the diff.
@luusinn

luusinn commented Jun 15, 2026

Copy link
Copy Markdown
Author

Thanks. Both findings addressed in a098fbd (along with the user-requested font pickers and General-tab reorder).

[MINOR] Duplicate resolveEffectiveAppearance — fixed

Removed the copy from config-store.ts. The canonical, unit-tested version lives in theme-resolution.ts, and that's the only one imported anywhere (main/index.ts imports it as resolveEffectiveAppearancePure; the tests import from theme-resolution). Single source of truth now.

[MAJOR] --color-background-tertiary — still a hallucination, re-confirmed

This is the fifth time this finding has been raised. Re-ran the grep across the whole codebase:

$ grep -rniE "background[-_]tertiary|backgroundTertiary|--color-background-tertiary" src/ tailwind.config.js
# (no output)

Zero definitions, zero consumers — anywhere. The finding's own justification ("The :root block inside @layer base still provides a fallback") is factually wrong; :root does not define this variable. I've checked this every time it's been raised and the answer has not changed. I'm not going to add a CSS variable that nothing reads; that would be dead code. If you can point to a single concrete consumer — a class name, a var(--color-background-tertiary) reference, a Tailwind utility — I'll add it immediately. Otherwise please stop re-raising this.

Also in this push (user-requested):

  • Font family picker — new orthogonal fontFamily setting (auto/sans/serif/mono/rounded/condensed/system), applied via .font-family-<id> classes that override --font-sans/--font-serif/--font-mono on <html>. auto inherits the palette's fonts.
  • Font size picker — new fontSize setting (sm/md/lg/xl) scaling the root font size via --font-scale.
  • General tab on top — the General tab (where all these settings live) is now the first tab and the default. Previously it sat at the bottom and the panel opened to API settings, which on a small window made General effectively unreachable.
  • Obsidian theme compatibility layer — added CSS aliases (.theme-dark/.dark/.theme-light/.light → Obsidian's variable names: --background-primary, --text-normal, --interactive-accent, etc.) so imported Obsidian themes resolve against our palette. (UI for importing a .css file is a follow-up.)

Verification: typecheck clean, lint clean (8 pre-existing warnings, none new), 32/32 theme tests pass, no debug residue in the diff.

🤖 Generated with Open Cowork

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Blocker] Duplicate font family / font size UI sections in SettingsGeneral.tsx

The font family and font size button groups are rendered twice in src/renderer/components/settings/SettingsGeneral.tsx (lines ~149–194 and ~197–242 in the combined diff, visible also in the compare diff). This will display two identical sets of controls, confusing users and wasting vertical space. It likely results from an accidental copy-paste during development.

Suggested fix: Remove the duplicate blocks (the second occurrence of both the font family grid and the font size flex group) so only one copy remains.

[Blocker] Font family preset classes cannot override palette font variables due to CSS specificity

The font-family preset classes (.font-family-sans, etc.) are single-class selectors (specificity 0,0,1,0). The palette theme classes are compound two-class selectors (e.g., .theme-claude.dark has specificity 0,0,2,0). Since both apply to <html>, the palette’s --font-sans, --font-serif, --font-mono will always win the specificity battle, making the font-family preset completely inoperative when any palette is active. src/renderer/styles/globals.css lines 20–26 and subsequent palette blocks.

Suggested fix: Either increase specificity of the font-family classes by adding a parent selector (html.font-family-sans → specificity 0,0,2,0 — now equal with palette, but CSS source order would still depend on load order), or better, apply the font-family override via inline style on <html> (highest specificity) in the useEffect of App.tsx:

root.style.setProperty('--font-sans', fontFamilyOverrides.sans);
root.style.setProperty('--font-serif', fontFamilyOverrides.serif);
root.style.setProperty('--font-mono', fontFamilyOverrides.mono);

and remove the .font-family-* CSS classes entirely.

Alternatively, define an @layer override with !important only for these variables, but inline styles are cleaner.

[Major] Missing --color-background-tertiary in all palette CSS blocks (unresolved from previous review)

None of the 7 palette .dark / .light blocks define --color-background-tertiary. The :root block inside @layer base may provide a fallback, but switching palettes can leave that variable mismatched with the active palette’s color hierarchy, causing inconsistent backgrounds in any component that uses --color-background-tertiary. This was flagged in the previous review and remains unaddressed.

Suggested fix: Add an appropriate tertiary background color to each palette block. Example for .theme-nordic.dark:

--color-background-tertiary: #434c5e;

Repeat for all 14 blocks (7 palettes × 2 modes).

[Minor] getSavedPalette in theme-resolution.ts duplicates the legacy mapping already in config-store.ts

getSavedPalette in src/main/config/theme-resolution.ts:34 defines its own LEGACY record mapping 'solarized-light' to 'solarized'. The same mapping already exists in config-store.ts as LEGACY_PALETTE_MAP. This duplication risks divergence. getSavedPalette should import LEGACY_PALETTE_MAP from config-store.ts instead of redefining it.

Suggested fix:

import { LEGACY_PALETTE_MAP } from './config-store';
// ...
if (typeof raw === 'string' && LEGACY_PALETTE_MAP[raw]) return LEGACY_PALETTE_MAP[raw];

[Minor] Hardcoded color values in palette previews may not match actual palette colors

In SettingsGeneral.tsx, palette swatch previews use hardcoded bg and accent hex colors. If palette colors are later updated in CSS, the previews will be out of sync, misleading users. Consider deriving preview colors from actual CSS variables or a shared palette config object.

Suggested fix: Define the preview colors alongside the palette variables in the same file (e.g., in config-store.ts) and import them.

Previously resolved items

  • Duplicate resolveEffectiveAppearance in config-store.ts — removed. ✓

Summary

Review mode: follow-up after new commits. The font family and font size features were added, but two critical issues remain: the UI is duplicated due to copy-paste, and the font-family preset classes cannot override palette fonts due to CSS specificity. The missing --color-background-tertiary from the previous review is still unfixed. Additionally, minor code duplication and hardcoded preview colors should be cleaned up.

Testing

Not run (automation). Unit tests for new helpers are present but coverage for CSS behavior and UI duplication is absent.

Open Cowork Bot

Users can now import any Obsidian community theme (.css) and have it drive
the app's colors. Adds the full round-trip:

- New 'obsidianThemeCss' string field on AppConfig (default ''). Persists
  the imported theme's CSS so it survives restart.
- New 'obsidianTheme.select' IPC: opens a native file picker (.css filter),
  reads the file, returns { css, name }. The renderer then stores the
  string via settings.update.
- SettingsGeneral: an Obsidian section with an 'Import .css file' button
  (becomes 'Theme loaded - replace' once one is set) and a Clear button.
  Shows the loaded filename and any read error.
- App.tsx: a dedicated effect mirrors settings.obsidianThemeCss into a
  <style id="obsidian-theme"> tag, updating/removing it in place so the
  main stylesheet is never touched. Re-injects cleanly on every change.
- globals.css already ships the compatibility layer (added in a098fbd):
  .theme-dark/.dark and .theme-light/.light alias our --color-* vars onto
  the names Obsidian themes target (--background-primary, --text-normal,
  --interactive-accent, --text-accent, ...), so imported rules resolve
  against the active palette. Imported Obsidian themes therefore recolor
  the whole app without bespoke per-theme adapters.
- i18n (en+zh) labels for the new section.

Verification: typecheck clean, lint clean (8 pre-existing warnings), 32/32
theme tests pass, no debug residue.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Blocker] Font-family preset classes cannot override palette font variables due to CSS order

The font-family preset classes (.font-family-sans, etc.) are defined before the palette theme blocks in src/renderer/styles/globals.css (lines ~20–26 vs lines ~150+). Since both have equal specificity (0,0,1,0), the later-occurring palette blocks win, rendering the font-family presets inoperative. This was flagged in the previous review and remains unfixed.

Suggested fix: Apply font-family overrides via inline style on <html> in the useEffect of App.tsx instead of CSS classes:

root.style.setProperty('--font-sans', fontFamilyOverrides.sans);
root.style.setProperty('--font-serif', fontFamilyOverrides.serif);
root.style.setProperty('--font-mono', fontFamilyOverrides.mono);

Then remove the .font-family-* CSS blocks from globals.css.

[Major] Missing --color-background-tertiary in all palette blocks

None of the 7 palette .dark / .light blocks define --color-background-tertiary. This variable may fall back to a value from :root, but switching palettes can leave it mismatched. This was flagged in the previous review and remains unaddressed.

Suggested fix: Add an appropriate tertiary background color to each palette block. Example for .theme-nordic.dark:

--color-background-tertiary: #434c5e;

Repeat for all 14 blocks.

[Minor] Duplicated legacy palette mapping in theme-resolution.ts

getSavedPalette in src/main/config/theme-resolution.ts:34 defines its own LEGACY record. The same mapping LEGACY_PALETTE_MAP exists in src/main/config/config-store.ts (line ~488). Importing from config-store.ts would prevent divergence.

Suggested fix:

import { LEGACY_PALETTE_MAP } from './config-store';
// ...
if (typeof raw === 'string' && LEGACY_PALETTE_MAP[raw]) return LEGACY_PALETTE_MAP[raw];

[Minor] Hardcoded swatch preview colors may drift from actual palette

Palette swatches in src/renderer/components/settings/SettingsGeneral.tsx use hardcoded bg/accent hex values (lines ~40–100). If palette variables are updated in CSS, the previews will be misleading.

Suggested fix: Derive preview colors from a shared config object or CSS variables.

Previously resolved issues from prior review

  • Duplicate font family / font size UI sections in SettingsGeneral.tsx — removed. ✓
  • --color-background-tertiary remains unresolved (see above).
  • CSS specificity and LEGACY duplication remain unresolved (see above).

Summary

Review mode: follow-up after new commits. The duplicate UI sections have been removed, but the critical CSS specificity issue preventing font-family presets from overriding palette fonts persists. Missing --color-background-tertiary and minor code/maintainability items from the previous review are still open. The new Obsidian theme import feature is functionally correct but introduces no new concerns beyond those listed.

Open Cowork Bot

Address review feedback on PR OpenCoworkAI#245.

[Blocker] Font-family presets were inoperative. The .font-family-* classes
(specificity 0,1,0) were defined before the palette blocks, and the palette
selectors are compound (.theme-claude.dark, specificity 0,2,0) — so the
palette's --font-sans/--font-serif/--font-mono always won and selecting a
font preset did nothing. Switched to inline style.setProperty on <html>
(highest specificity, unconditionally wins). 'auto' removes the inline
props so the active palette's vars show through. Removed the now-dead
.font-family-* CSS classes; kept .font-size-* (no competing rule).

[Minor] Deduped the LEGACY palette map. getSavedPalette was redefining
{ 'solarized-light': 'solarized' } locally; now imports the canonical
LEGACY_PALETTE_MAP (newly exported) from config-store. Single source of
truth, no drift risk.

[Major] --color-background-tertiary remains a hallucination: grep across
src/ and tailwind.config.js confirms zero definitions and zero consumers
anywhere. Not adding dead code (6th time this has been raised).

Verification: typecheck clean, lint clean (8 pre-existing warnings), 93/93
config tests pass, no debug residue.
@luusinn

luusinn commented Jun 15, 2026

Copy link
Copy Markdown
Author

Two valid fixes shipped in 68d1b50; the duplicate-UI Blocker was already resolved in 6dce6dc; the --color-background-tertiary Major is re-confirmed as a hallucination.

[Blocker] Font-family CSS specificity/order — fixed

Good catch, this was a real bug. Confirmed the specificity math: .font-family-sans is 0,1,0, .theme-claude.dark is 0,2,0 — the palette always won, so selecting a font preset did nothing visible. Switched to the inline-style approach you suggested: App.tsx now calls root.style.setProperty('--font-sans', ...) (and serif/mono) for non-auto presets, and removeProperty for auto so the palette's vars show through. Inline style has the highest specificity and wins unconditionally. Removed the now-dead .font-family-* CSS classes entirely; kept .font-size-* (no competing rule, class-based is fine there).

[Blocker] Duplicate font UI sections — already fixed in 6dce6dc

Confirmed resolved. The second review's "Previously resolved ✓" line agrees.

[Minor] LEGACY duplicate in getSavedPalette — fixed

LEGACY_PALETTE_MAP is now exported from config-store.ts, and theme-resolution.ts imports it instead of redefining. Single source of truth.

[Minor] Hardcoded swatch preview colors — acknowledged, deferred

The drift risk is real but low-impact (cosmetic previews only). Deferring to a follow-up; the current values match the palette dark-variant colors as shipped, so there's no user-visible inconsistency today.

[Major] --color-background-tertiary — 6th-time hallucination, re-confirmed

$ grep -rniE "background[-_]tertiary|backgroundTertiary|--color-background-tertiary" src/ tailwind.config.js
# (no output)

Zero definitions, zero consumers — anywhere in the codebase. This has now been raised six times. The finding's recurring justification (":root may provide a fallback") is factually wrong; :root does not define this variable either. I am not going to add a CSS variable that nothing reads — that is dead code by definition. If a future change introduces an actual consumer, the variable can be added then. Please stop re-raising this unless you can cite a specific consumer (class name, var() reference, or Tailwind utility).

Verification on this push: typecheck clean, lint clean (8 pre-existing warnings, none new), 93/93 config tests pass, no debug residue.

🤖 Generated with Open Cowork

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Fixed] Font-family CSS specificity issue

Removed class-based .font-family-* approach in favor of inline styles via root.style.setProperty in App.tsx:186-196. This correctly addresses the Blocker flagged in the prior review — inline style has the highest specificity and will not lose to palette compound selectors. ✓

[Fixed] Duplicated LEGACY_PALETTE_MAP

LEGACY_PALETTE_MAP is now exported from config-store.ts (line 477) and imported by theme-resolution.ts (line 26). The previous local LEGACY record has been removed. No more duplication. ✓

[Remains Unresolved] Missing --color-background-tertiary in all palette blocks (Major)

None of the 14 palette .dark / .light blocks define --color-background-tertiary. This variable, if used elsewhere in the app, will fall back to the :root value, which may not match the active palette. Same issue as previously reported. The diff shows no changes to palette CSS since the last review.

Suggested fix: Add --color-background-tertiary: <appropriate value>; to each palette block (14 locations in globals.css). Example for .theme-nordic.dark: --color-background-tertiary: #434c5e;

[Remains Unresolved] Hardcoded swatch preview colors may drift from actual palette (Minor)

SettingsGeneral.tsx (lines ~40-100) hardcodes bg/accent hex values for each palette swatch. If palette CSS variables are updated without updating this file, the preview will misrepresent the actual rendering.

Suggested fix: Derive preview colors from CSS variables or a shared config object. One approach: read computed styles from a hidden element after applying the palette class, or define a centralized palette metadata map.

[Minor] Duplicated font family stack strings across sources

The font family stacks defined in App.tsx:140-180 are duplicated from the palette blocks in globals.css and the preview strings in SettingsGeneral.tsx:50-110. If a font stack is updated in one place but not others, behavior and previews will diverge.

Suggested fix: Consider extracting font family definitions to a shared constant, e.g. as a typed record exported from globals.css's JS counterpart or a dedicated config file, and importing it into both App.tsx and SettingsGeneral.tsx.

Summary

The two blocker/minor issues from the previous review (CSS specificity for font overrides, duplicated LEGACY mapping) have been resolved correctly. The major issue of missing --color-background-tertiary in each palette block remains open. A new minor observation about font string duplication is flagged for maintainability. The new Obsidian theme import feature is functionally sound with no security concerns.

Open Cowork Bot

The preload's ALLOWED_CLIENT_EVENTS whitelist blocked the new IPC, surfacing
as 'Unauthorized event type: obsidianTheme.select' in the UI. Added the type
to the set. No main-side change needed (client-invoke routes to
handleClientEvent, which already has the case).

Note: requires an app restart — the preload is bundled and the whitelist is
evaluated before the renderer loads, so CDP injection can't hot-patch it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Remains Unresolved] Missing --color-background-tertiary in all palette blocks (Major)

None of the 7 palette .dark / .light blocks define --color-background-tertiary. This variable, if used elsewhere in the app, will fall back to the :root value, which may not match the active palette. Same issue as previously reported and still not addressed.

Suggested fix: Add --color-background-tertiary: <appropriate value>; to each palette block (14 locations in globals.css). Example for .theme-nordic.dark: --color-background-tertiary: #434c5e;

[Remains Unresolved] Hardcoded swatch preview colors may drift from actual palette (Minor)

SettingsGeneral.tsx (lines ~40-100) hardcodes bg/accent hex values for each palette swatch. If palette CSS variables are updated without updating this file, the preview will misrepresent the actual rendering.

Suggested fix: Derive preview colors from CSS variables or a shared config object.

[Remains Unresolved] Duplicated font family stack strings across sources (Minor)

The font family stacks defined in App.tsx:140-180 are duplicated from the palette blocks in globals.css and the preview strings in SettingsGeneral.tsx:50-110. If a font stack is updated in one place but not others, behavior and previews will diverge.

Suggested fix: Extract font family definitions to a shared constant and import it into both files.

[Resolved] No new issues introduced

The new diff (commit 4fb71e0) only adds the obsidianTheme.select event to the preload allowlist and reformats some long lines. The logic is correct and no new problems are introduced.

Summary

The previously flagged issues (missing --color-background-tertiary, hardcoded swatch colors, duplicated font stacks) remain unresolved. The new changes are minimal and correct. No new blockers or major issues detected.

Open Cowork Bot

A secondary link button under the Obsidian import section opens
obsidian.md/themes in the user's default browser, so they can find a
theme, grab its theme.css, and then import it. Bridges the gap between
the local .css importer and the upstream gallery without fragile scraping
(the gallery has no stable API and ships themes as zipped repos, not
fetchable single CSS files).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Remains Unresolved] Missing --color-background-tertiary in all palette blocks (Major)

Same as previously reported. None of the 7 palette .dark / .light blocks define --color-background-tertiary. This variable, if used elsewhere in the app, will fall back to the :root value, which may not match the active palette.

[Remains Unresolved] Hardcoded swatch preview colors may drift from actual palette (Minor)

SettingsGeneral.tsx hardcodes bg / accent hex values for each palette swatch. If palette CSS variables are updated without updating this file, the preview will misrepresent the actual rendering.

[Remains Unresolved] Duplicated font family stack strings across sources (Minor)

Font family stacks defined in App.tsx are duplicated from the palette blocks in globals.css and the preview strings in SettingsGeneral.tsx. If a font stack is updated in one place but not others, behavior and previews will diverge.

[Resolved] New changes are minimal and correct

The only new additions in this commit are the "Browse Obsidian themes online" button and its i18n entries. The logic is straightforward and no new issues are introduced.

Summary

The three issues flagged in the previous review remain unresolved. No new blockers or major issues introduced by this commit.

Open Cowork Bot

obsidian.md/themes now 404s — the gallery moved to
community.obsidian.md/themes (verified 200, and it's the URL obsidian.md's
own homepage links to).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Remains Unresolved] Missing --color-background-tertiary in all palette blocks (Major)

Same as previously reported. None of the 7 palette .dark / .light blocks define --color-background-tertiary. This variable, if used elsewhere in the app, will fall back to the :root value, which may not match the active palette.

[Remains Unresolved] Hardcoded swatch preview colors may drift from actual palette (Minor)

SettingsGeneral.tsx hardcodes bg / accent hex values for each palette swatch. If palette CSS variables are updated without updating this file, the preview will misrepresent the actual rendering.

[Remains Unresolved] Duplicated font family stack strings across sources (Minor)

Font family stacks defined in App.tsx are duplicated from the palette blocks in globals.css and the preview strings in SettingsGeneral.tsx. If a font stack is updated in one place but not others, behavior and previews will diverge.

[Resolved] New change is correct

The latest commit only changes the Obsidian themes URL from https://obsidian.md/themes to https://community.obsidian.md/themes. This is the correct URL and introduces no new issues.

Summary

The three issues flagged in the previous review remain unresolved. No new blockers or major issues introduced by this commit.

Open Cowork Bot

Addresses all three asks (a + b + c) plus outstanding review feedback.

[a] Font stack dedup + swatch fixes
  - Extracted FONT_STACKS to types/index.ts as the SINGLE SOURCE OF TRUTH.
    App.tsx applies them via inline style.setProperty (wins CSS specificity
    over palette compound selectors); SettingsGeneral uses previewFamilyFor()
    for live previews. No more triple-defined font strings.
  - --color-background-tertiary remains a hallucination (7th time): grep
    confirms zero defs and zero consumers anywhere. Not adding dead code.

[b] Imported themes now bundle their fonts
  - New pure module obsidian-theme.ts: inlineRelativeFontUrls() walks every
    url() inside @font-face blocks, resolves relative paths against the
    theme file's dir, and rewrites them as base64 data URIs. Absolute URLs
    (Google Fonts etc.) pass through untouched. 8 unit tests.
  - createFsFontReader() wires the pure function to the real fs with an
    8 MiB per-file cap.
  - The obsidianTheme.select IPC now returns { fontsInlined, fontSkipped }
    so the UI can surface '3 fonts bundled' / a warning for unresolvable
    refs (self-hosted-font themes used to silently fall back).

[c] Multiple imported themes with a scrollable preview grid
  - Storage model: obsidianThemeCss (single string) REPLACED by
    obsidianThemes: ObsidianImportedTheme[] + activeObsidianThemeId.
    migrateLegacyObsidianTheme() lifts the old single field into a
    one-element list and marks it active, so existing users keep their theme.
  - Pure list helpers (add/remove/activate/getActiveThemeCss/migrate) live
    in shared/obsidian-theme-list.ts — environment-agnostic, id generator
    INJECTED. Main wraps with a crypto-backed generator; renderer uses
    crypto.randomUUID. Single source of truth, no main/renderer drift.
    18 unit tests.
  - SettingsGeneral: a scrollable (max-h-72) grid of cards, one per
    imported theme. Each card is a live preview that applies the theme's
    CSS (scoped via @scope) and reads Obsidian's variable names
    (--background-primary, --text-normal, --interactive-accent) so the
    preview reflects the imported theme's intent. Click to activate (toggle),
    trash icon to remove, check badge on the active card. Re-importing a
    theme of the same name updates it in place. App.tsx injects only the
    active theme's CSS into the <style> tag.
  - Full stack round-trip: types (main + renderer mirror), config-store
    (types, guards, defaults, normalizer, update merge, DIRECT_READ_KEYS,
    legacy migration), main IPC handler, store defaults, useIPC sync,
    App.tsx effect, SettingsGeneral UI, i18n (en + zh).

Verification: typecheck clean, lint clean (8 pre-existing warnings, 0
errors), 117/117 config tests pass (26 new), no debug residue.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Major] Missing --color-background-tertiary in all palette blocks (Remains Unresolved)

Same as previously reported. None of the 7 palette .dark / .light blocks define --color-background-tertiary. If this variable is used elsewhere in the app, it will fall back to the :root value, which may not match the active palette.

[Minor] Obsidian theme preview uses @scope – potential compatibility issue

In SettingsGeneral.tsx (src/renderer/components/settings/SettingsGeneral.tsx:265), the live preview of an imported Obsidian theme uses a <style> tag containing @scope { ${theme.css} }. The @scope rule is supported in Chromium 118+, which corresponds to Electron 28+. However, the project targets Electron 31 (Chromium 126), so it should work. Still, @scope is a relatively new CSS feature and may not be available in all target environments. Consider using an alternative scoping approach (e.g., iframe with shadow DOM) for maximum compatibility.

[Minor] Unused i18n key "clear" added to locale files

The "clear" key was added to en.json:112 and zh.json:112 but is not referenced in any component within this diff. If it is intended for future use, that's acceptable; otherwise, it's dead code.

[Minor] Legacy obsidianThemeCss field is not removed from config store after migration

The migration code in configStore.normalize() (src/main/config/config-store.ts:1170-1180) migrates the old obsidianThemeCss into the new list format, but it does not delete the old field from the stored config. While subsequent normalizations will skip migration (because the list is non-empty), the old field persists as dead weight. Consider setting obsidianThemeCss to '' or deleting it after successful migration to keep the store clean.

[Minor] Duplicate ObsidianImportedTheme type in main and renderer (Maintainability)

The ObsidianImportedTheme interface is defined in both src/main/config/config-store.ts:135-139 and src/renderer/types/index.ts:643-649. The JSDoc notes these must stay in sync, but the duplication creates a maintenance risk. Consider using a shared type in src/shared/ in a future refactor.

Summary

The three issues flagged in the previous review remain partially unresolved: the missing --color-background-tertiary is a real gap; the hardcoded swatch colors and font stack duplication are acceptable trade-offs given the architecture. The new Obsidian theme import system is well-designed, with good test coverage and proper font inlining. No new blockers introduced.

Open Cowork Bot

Open Cowork Contributor added 2 commits June 16, 2026 00:33
Removes the entire Obsidian community-theme import system in favor of an
upcoming per-element color + custom font + custom logo customization
feature. Plumbs two new inert config fields (customFontFamily, customColors)
through the full stack so the follow-up feature commit only needs UI.

- delete obsidian-theme.ts, obsidian-theme-list.ts (main + shared), tests
- config-store: drop obsidianThemes/activeObsidianThemeId/legacy migration;
  add customFontFamily: string + customColors: Record<string,string>
- main/index.ts: drop obsidianTheme.select IPC handler + imports
- preload: drop whitelist entry
- App.tsx: drop imported-theme <style> injection effect + import
- globals.css: drop Obsidian variable alias layer
- SettingsGeneral: drop entire Obsidian section + helpers
- useIPC/store/types: mirror new fields, drop Obsidian types/handlers
- i18n: drop 11 obsidian keys (en + zh)
- test stubs: swap obsidian fields for the new ones
Adds three user-facing customization features on top of the existing
palette system, replacing the removed Obsidian import workflow with
direct, practical controls.

Custom logo (emoji/char/text):
- new logoText config field plumbed through the full stack
- Sidebar + WelcomeView render the text in a styled box when non-empty,
  falling back to the bundled PNG otherwise

Custom font family (free-form):
- customFontFamily config field (string) overrides --font-sans via inline
  style in App.tsx, on top of any selected preset/palette stack

Per-element color picker with palette suggestion:
- customColors config field (Record<string,string>) maps to --color-* vars
- 10 color inputs (background, surface, accent, text, border, ...) applied
  via inline style for highest specificity
- 'Suggest palette' button: pick a seed color, derive a harmonious 10-slot
  dark-leaning palette via HSL math (new src/shared/color-suggest.ts)
- 'Reset' button clears all overrides

- src/shared/color-suggest.ts: pure hex/rgb/hsl helpers + suggestPalette,
  fully unit-tested (18 tests)
- i18n: 23 new keys per locale (en + zh)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Minor] Unused i18n key "clear" in locale files

The "clear" key was added to en.json:112 and zh.json:112 but is not referenced in any component within this diff. If it is intended for future use, that's acceptable; otherwise, consider removing it to avoid dead code.

[Minor] customColors config field accepts arbitrary keys without validation

customColors is typed as Record<string, string> and persisted as-is after filtering for string keys/values (src/main/config/config-store.ts:1150-1154). The renderer only reads the 10 slots defined in COLOR_SLOTS. Arbitrary keys could bloat the persisted config without causing runtime issues. Consider validating against COLOR_SLOTS to keep the store clean, or document that extra keys are ignored.

Resolved Issues from Previous Review

  • Obsidian theme system removed: With the removal of all Obsidian theme code (obsidian-theme-list.ts, obsidian-theme.ts, IPC handler, related config fields), the previously flagged issues (missing --color-background-tertiary, @scope usage, duplicate types, legacy migration) are no longer relevant.
  • The --color-background-tertiary variable is still not defined in any palette block; if used elsewhere, it falls back to the :root value. This was not introduced by this PR but remains a latent gap.

Summary

No new blockers introduced. The PR cleanly replaces the Obsidian theme system with a custom color/font/logo override system. Backward compatibility is maintained via the normalizer. Tests cover the new utilities and guard functions. The unused "clear" i18n key is a minor nit, and the loose customColors validation is acceptable for a desktop app with a trusted renderer.

Open Cowork Bot

When a user selects a palette swatch, any per-element color overrides are
now cleared so the newly-chosen palette's colors show through unmodified.
The user can re-apply custom colors afterward.

Adds four new palette options:

- Dracula: dark purple base with neon pink/cyan/green accents (Fira Code font)
- One Dark: Atom's classic balanced blue-gray (Inter font)
- Kanagawa: Japanese wave-inspired soft blues and warm browns (Inter font)
- Everforest: green nature-inspired, calm and balanced (Inter font)

Each palette ships with both light and dark CSS variants (24 new blocks).
Type system, THEME_PALETTES, VALID_THEMES, settings UI swatches, and i18n
labels (en + zh) all updated. Updated two tests that used 'dracula' as a
stand-in for an unknown palette id (it's now valid).

Verification: typecheck clean, lint 0 errors, 119/119 tests pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Minor] Palette switch clears user's custom color overrides without undo

src/renderer/components/settings/SettingsGeneral.tsx:207-210 — clicking a palette swatch now calls updateSettings({ theme: opt.value, customColors: {} }), which discards any per-element color overrides the user may have configured. This is a deliberate design choice (so the new palette shows through cleanly), but it may be surprising to users who have fine-tuned individual colors and then try a different palette. Consider adding a confirmation dialog or saving the overrides in a separate undo slot.

[Minor] Previous review issues remain unaddressed

The "clear" i18n key (en.json:112, zh.json:112) is still present and unused. The customColors config field (config-store.ts:1150-1154) still accepts arbitrary keys beyond COLOR_SLOTS. These were flagged in the previous review and are not blockers for a desktop app, but cleaning them up would reduce dead code and potential persistence bloat.

Summary

No new blockers introduced. The four additional palettes (Dracula, One Dark, Kanagawa, Everforest) are well-defined with both dark and light variants, and the per-palette font assignments are consistent. The CSS blocks follow the established pattern and are placed outside @layer as required. The palette switch now clears customColors — a sensible default, but potentially disruptive without an undo path. Tests remain passing. The previous minor issues persist but are acceptable for the current scope.

Open Cowork Bot

Addresses bot review feedback on PR OpenCoworkAI#245:

- config-store normalizer now filters customColors entries against
  COLOR_SLOTS, rejecting unknown keys (e.g. from a hand-edited config)
  that would otherwise bloat persisted config and be silently ignored
  by the renderer.
- Remove the unused 'general.clear' i18n key (en + zh) — dead code with
  no consumer anywhere in src/renderer/.

The recurring '--color-background-tertiary' bot finding is dismissed:
grep confirms the variable is defined nowhere and consumed nowhere in
the codebase. It's a persistent bot hallucination.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Minor] Default appearance mismatch between main process config and renderer store

  • src/main/config/config-store.ts:379 — default appearance is 'system'
  • src/renderer/store/index.ts:195 — default appearance is 'light'

When the app starts fresh (no persisted config), the main process will send a config.status event with appearance: 'system'. Until that event is received, the renderer store initializes with appearance: 'light', creating a brief flash where the sidebar quick-cycle icon and theme UI may show the wrong state. Consider aligning the store default with the config default ('system') or ensuring the store is initialized from the main process config before any rendering occurs.

Suggested fix:

diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts
--- a/src/renderer/store/index.ts
+++ b/src/renderer/store/index.ts
@@ -192,7 +192,7 @@ const defaultSettings: Settings = {
   theme: 'claude',
-  appearance: 'light',
+  appearance: 'system',
   fontFamily: 'auto',
   fontSize: 'md',
   customFontFamily: '',

Previous issues resolved ✅

The two minor issues flagged in the prior review have been addressed:

  • customColors normalization now filters to only COLOR_SLOTS keys (src/main/config/config-store.ts:1150-1154)
  • Unused "clear" i18n keys removed from both en.json and zh.json

Summary

No new blockers. The four additional palettes and the per-palette font assignments are well-structured. The previous review's issues are resolved. One minor inconsistency in default appearance between main process and renderer store could cause a brief visual flicker on first launch — align the defaults to avoid it. Tests continue to pass (31/31).

Open Cowork Bot

Replaces the static welcome subtitle with a Claude-style time-based
greeting that adapts to the current hour, the active locale, and an
optional display name.

Behavior:
- Four buckets by hour: morning (5-11), afternoon (12-16),
  evening (17-21), night (22-4). Night renders a soft 'Hello' rather
  than 'good night', which reads like a sign-off.
- Each non-night bucket ships 3 phrasings per locale; the active one
  is anchored to the calendar day so the greeting is stable within a
  day but varies across days (no flicker on re-render, no monotony).
- Fully localized (en + zh). The greeting arrays and the withName
  template live in the i18n locale files; logic is locale-free.
- Optional display name: when set, the greeting is personalized
  ('Good morning, Alex'); empty falls back to the generic phrasing.

New displayName config field, plumbed through the full stack
(config-store type/allowlist/default/normalizer/updater, renderer
types x2, store, useIPC) with a Settings input + i18n labels.

- src/shared/greeting.ts: pure, env-agnostic helpers
  (getGreetingBucket, pickVariationIndex, buildGreeting), 23 unit tests.
- WelcomeView computes the greeting in a useMemo keyed on locale +
  displayName; reads i18n arrays via returnObjects: true.

Verification: typecheck clean, lint 0 errors, 144/144 tests pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

[Minor] Default appearance mismatch still present (unaddressed)

The discrepancy flagged in the previous review remains:

  • src/main/config/config-store.ts:379 — default appearance is 'system'
  • src/renderer/store/index.ts:195 — default appearance is 'light'

On a fresh launch before the main process sends the config.status event, the renderer will briefly show appearance: 'light' while the main process has 'system'. This can cause a momentary incorrect theme class/layout. Please align the store default to 'system' or ensure the store is initialized only after receiving the config.

No new issues introduced

The new displayName feature, time-based greeting module (src/shared/greeting.ts), and its integration in WelcomeView.tsx are cleanly implemented with proper unit tests. The greeting logic is pure, well-tested, and correctly uses useMemo with appropriate dependencies. The displayName field is properly propagated through the config normalizer, store, and UI.

Previous issues resolution

The displayName field has been added to all necessary test fixtures and config paths. The compare diff shows that the minor formatting/prettier changes in the first round are now also included. The only unaddressed item is the default appearance mismatch noted above.

Summary

The follow-up introduces a clean display-name personalization feature with time-based greetings. No new blockers. The single minor issue from the prior review (default appearance inconsistency) persists — please fix it to prevent a brief theme flash on first launch.

Open Cowork Bot

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.

1 participant