Skip to content

feat(themes): Themes settings tab with Light/Dark/System appearance mode#116

Merged
johannesjo merged 4 commits into
johannesjo:mainfrom
brooksc:task/theme2-9c6135
May 16, 2026
Merged

feat(themes): Themes settings tab with Light/Dark/System appearance mode#116
johannesjo merged 4 commits into
johannesjo:mainfrom
brooksc:task/theme2-9c6135

Conversation

@brooksc
Copy link
Copy Markdown
Contributor

@brooksc brooksc commented May 16, 2026

Summary

This PR adds a Themes tab to the Settings dialog that lets users choose their appearance mode (Light / Dark / System) and pick independent theme presets for each mode. It also includes a contrast fix for three built-in themes and a reliability fix for the close-dialog fallback timer.


What's included

Themes settings tab (SettingsDialog.tsx, look.ts, ui.ts, store/)

  • New Themes tab in Settings with three appearance-mode options: Light, Dark, and System (follows the OS preference via window.matchMedia('prefers-color-scheme: dark')).
  • Independent theme preset selectors for the dark slot and the light slot — switching modes instantly applies the preset last chosen for that mode.
  • State is persisted per slot (appearanceMode, darkThemePreset, lightThemePreset, darkThemeCustomId, lightThemeCustomId).
  • Backward-compatible migration: existing users who had the old single themePreset field saved will have it migrated into the correct dark/light slot automatically on first load.

WCAG AA contrast fix (styles.css)

Three built-in themes had accent colours that failed WCAG AA (4.5:1) against their panel backgrounds:

Theme Old colour New colour Old ratio New ratio
classic #4c6fff #4267ff 4.17:1 4.55:1
islands-dark #548af7 #286cf5 3.30:1 4.61:1
islands-light #3574f0 #2c6def 4.27:1 4.60:1

Close-dialog fallback timer fix (window.ts)

When the "Cancel option" close dialog feature was added, WindowCloseHandling (which cancels the backend's 5 s force-destroy fallback) was being sent immediately on every close request — before the async pre-work (captureWindowState, saveState, CountRunningAgents) had completed. A hang in any of those left the window permanently stuck because the safety net was already gone.

Fixed by moving the ack into the preventDefault() callback so the timer stays armed through the pre-work and is only cleared once the renderer has decided to show the interactive dialog.

Tests

  • isLookPreset type guard: valid preset IDs, unknown strings, non-string values.
  • Theme persistence migration: defaults, all three appearance modes, invalid-mode fallback, valid/invalid preset restore, custom ID handling, and all three backward-compat paths for the legacy themePreset field (13 new tests, 475 total passing).

How to test

  1. Open Settings → Themes tab.
  2. Switch between Light / Dark / System — the app should immediately apply the corresponding preset.
  3. Pick a different preset in Dark mode, switch to Light, pick one there, switch back — each mode remembers its own choice.
  4. Quit and relaunch — selections should be restored.
  5. (Migration) Manually set "themePreset": "classic" in the saved state file and relaunch without appearanceMode — should restore dark mode with Classic.

🤖 Generated with Claude Code

brooksc and others added 4 commits May 15, 2026 19:51
- classic:       #4c6fff → #4267ff  (4.17→4.55:1 on #0d1117)
- islands-dark:  #548af7 → #286cf5  (3.30→4.61:1 on #0f1923)
- islands-light: #3574f0 → #2c6def  (4.27→4.60:1 on #f5f7fa)
- isLookPreset: valid presets, unknown string, non-string values
- persistence: defaults, all three appearance modes, invalid mode fallback,
  valid/invalid dark/light preset restore, custom ID handling, and all three
  backward-compat paths for the old single themePreset field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ack was sent immediately on every close request, which canceled the
backend's 5s force-destroy fallback before captureWindowState(), saveState(),
and CountRunningAgents had completed. A hang in any of those left the window
stuck open permanently — the fallback that would have recovered it was gone.

Move the ack into the preventDefault() callback so the backend timer stays
armed through the async pre-work and is only cleared once the renderer has
decided to show the running-terminals dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@brooksc
Copy link
Copy Markdown
Contributor Author

brooksc commented May 16, 2026

image

Shows the Settings on the General Tab

image

When user selects Theme, if they select Light (always light them) it shows the Light themes

image

When user selects Theme, if they select Dark (always dark them) it shows the Dark themes

image

When the user selects Theme, then System they can select both a light and dark theme. Which one is currently enabled is based on their system controls - e.g. on Mac it will transition from Light to Dark in the evening and back to Light in the morning.

@johannesjo
Copy link
Copy Markdown
Owner

Thank you very much for this! <3

@johannesjo johannesjo merged commit ed1557e into johannesjo:main May 16, 2026
2 checks passed
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.

2 participants