Skip to content

feat: follow system light/dark appearance with per-mode theme selection - #650

Open
iamthenuggetman wants to merge 7 commits into
TabularisDB:mainfrom
iamthenuggetman:feature/system-theme-sync
Open

feat: follow system light/dark appearance with per-mode theme selection#650
iamthenuggetman wants to merge 7 commits into
TabularisDB:mainfrom
iamthenuggetman:feature/system-theme-sync

Conversation

@iamthenuggetman

Copy link
Copy Markdown
Contributor

Closes #649

Summary

Users can now pick separate light and dark themes; in Follow System mode the app watches the OS appearance (prefers-color-scheme) and live-applies the theme for the current mode, including the native window chrome via Tauri setTheme. Static mode keeps a single fixed theme (previous behavior, still the default).

Changes

  • Backend (src-tauri/src/config.rs): three new optional AppConfig fields — followSystemTheme, lightThemeId, darkThemeId — persisted to config.json via the existing field-merge save_config. Zero migration: absent fields = static mode, existing users unaffected. Serde round-trip + defaults tests added.
  • Resolution (src/utils/themeManagement.ts"): new pure resolveActiveThemeId(settings, systemIsDark)helper; per-mode picks resolve through the previously deadgetSystemThemeId`.
  • Provider (src/contexts/ThemeProvider.tsx):
    • hydrates the new settings from get_config and persists them via updateSettings (was a no-op);
    • matchMedia change listener now uses the user's per-mode picks (previously hardcoded tabularis-dark/tabularis-light behind a flag nothing could set) with mode-correct preset fallback if a picked theme no longer resolves (e.g. deleted custom theme);
    • toggling Follow System applies the current-mode theme immediately;
    • syncing native window chrome (getCurrentWindow().setTheme) on every theme application — logged and non-fatal on failure;
    • deleting a custom theme resets per-mode picks that referenced it.
  • UI (src/components/settings/AppearanceTab.tsx): Static / Follow System SettingButtonGroup; in follow mode two pickers filtered by theme classification (monacoTheme.base, covers custom themes); static mode picker unchanged.
  • i18n: 6 new keys (themeMode, themeModeDesc, themeModeStatic, themeModeSystem, lightTheme, darkTheme) in all 11 locales.
  • Editor theme is untouched: "Same as App" resolves through currentTheme and follows system switches automatically; explicit editor overrides stay fixed.

Testing

  • pnpm vitest run: 3789 tests pass (new: resolution helper unit tests; provider tests for load hydration, persistence, immediate apply on toggle, bidirectional OS switching, unresolvable-pick fallback, static-mode immunity; AppearanceTab UI tests).
  • pnpm test:rust: 1139/1143 — 4 askpass failures are environmental (socket path under the default macOS TMPDIR; pass with TMPDIR=/tmp), pre-existing and unrelated.
  • pnpm typecheck, pnpm lint: clean.
  • Manually smoke-tested on macOS: mode toggle, OS appearance flip (webview + titlebar), persistence across restart, static-mode immunity.

Comment thread src/contexts/ThemeProvider.tsx Outdated

// If the deleted theme was active, switch to default
if (currentTheme.id === themeId) {
const defaultTheme = themeRegistry.getDefault();

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.

WARNING: Deleting the active per-mode theme in follow-system mode switches to getDefault() (always dark), ignoring the current OS mode.

When follow-system is enabled and the deleted custom theme was the active system-matched pick, the reset block above correctly restores lightThemeId/darkThemeId to the mode-correct preset, but this branch then falls back to themeRegistry.getDefault() regardless of OS. If the OS is currently in light mode, the app ends up showing the dark default and stays there until the OS appearance changes (the matchMedia listener only re-resolves on an OS change, not on this settings/currentTheme update). Consider resolving the replacement through the current OS mode and the just-reset picks (e.g. via getSystemThemeId(window.matchMedia("(prefers-color-scheme: dark)").matches, { lightThemeId, darkThemeId, ... })) instead of getDefault().


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental review of commit d0ca886 ("fix(theme): add final fallback for delete-active replacement"). The new || themeRegistry.getDefault() in the follow-system active-theme replacement branch is a defensive safety net: getPreset() returns Theme | undefined, and the subsequent replacement.id access would throw if the preset were missing. The final fallback guarantees a defined Theme. It does not reintroduce the previously-resolved WARNING — the OS-mode-matched preset (tabularis-light/tabularis-dark) is still resolved first via getSystemThemeId and the mode-correct getPreset, and both presets are always registered. The absolute getDefault() is only reached in the impossible case where the matching preset is absent.

Files Reviewed (1 file)
  • src/contexts/ThemeProvider.tsx
Previous Review Summaries (2 snapshots, latest commit c721476)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit c721476)

Status: No Issues Found | Recommendation: Merge

The previously reported WARNING (deleting an active per-mode theme in follow-system mode falling back to getDefault()) has been resolved. The active-theme branch now resolves the replacement through the current OS mode and the just-reset per-mode picks via getSystemThemeId, with a preset fallback matching the OS mode. The static-mode path still uses themeRegistry.getDefault(). settings.followSystemTheme was correctly added to the deleteCustomTheme dependency array, and a regression test covering the light-OS scenario was added.

Files Reviewed (2 files)
  • src/contexts/ThemeProvider.tsx
  • tests/contexts/ThemeProvider.test.tsx

Previous review (commit b9809a7)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/contexts/ThemeProvider.tsx 317 Deleting the active per-mode theme in follow-system mode falls back to getDefault() (always dark), ignoring the current OS mode and the just-reset per-mode pick.
Files Reviewed (4 substantive files)
  • src-tauri/src/config.rs - 0 issues
  • src/utils/themeManagement.ts - 0 issues
  • src/contexts/ThemeProvider.tsx - 1 issue
  • src/components/settings/AppearanceTab.tsx - 0 issues

i18n locale additions (11 files) and test files (3 files) reviewed for consistency; no issues.

Fix these issues in Kilo Cloud


Reviewed by glm-5.2 · Input: 67.1K · Output: 4.9K · Cached: 216.1K

@kilo-code-bot

kilo-code-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

To use Kilo from GitHub you first need to link your GitHub account to Kilo. Link your Kilo account to continue. After linking, mention me again in this issue or pull request.

…low-system mode

Kilo Code review (TabularisDB#650): in follow-system mode, deleting the active per-mode
pick left the app stuck on themeRegistry.getDefault() (always dark) until
the OS appearance changed. Resolve the replacement through the current OS
mode and the just-reset per-mode picks, mirroring the listener and load
path. Static mode keeps the existing getDefault() fallback.

- ThemeProvider.tsx deleteCustomTheme active branch now branches on
  settings.followSystemTheme; follow-system path uses getSystemThemeId +
  preset fallback by current OS mode.
- New test in system theme sync suite: deletes the active per-mode pick
  in follow-system mode (OS light) and asserts the replacement is the
  light preset, not getDefault().
@iamthenuggetman

Copy link
Copy Markdown
Contributor Author

Addressed the warning at src/contexts/ThemeProvider.tsx:317. New commit c7214768.

In follow-system mode, deleting the active per-mode pick now resolves the replacement through the current OS mode and the just-reset per-mode picks — same fallback chain the listener and load path already use. Static mode keeps getDefault() (unaffected).

New test in the system-theme-sync suite: deletes the active per-mode pick in follow-system mode (OS light, lightThemeId = custom light theme) and asserts the active theme becomes tabularis-light, not getDefault(). Full suite 3790/3790 pass; typecheck + lint clean.

CI build failed: themeRegistry.getPreset returns Theme | undefined, and the
find() || getPreset() chain can resolve to undefined. tsc -b rejects
assigning to a Theme-typed binding. Add themeRegistry.getDefault() as the
final fallback so the assignment is non-undefined, matching the load-path
fallback chain.
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.

[Feature]: Follow system light/dark appearance with per-mode theme selection

1 participant