feat(desktop): one theme list in the palette, with a mode toggle inside it

The picker split every palette across a Light and a Dark group, so a
built-in appeared twice and picking one silently set the mode too. It now
mirrors Appearance settings: light/dark/system rows, then every theme once,
applied on top of whichever mode is selected.

Mode rows preview on highlight like theme rows already did — system resolves
through the live prefers-color-scheme query, so it previews what committing
it would actually give you.
This commit is contained in:
Brooklyn Nicholson
2026-08-20 00:05:45 -05:00
committed by brooklyn!
parent cefeed4ca8
commit 83fac2cf2d
+67 -19
View File
@@ -18,6 +18,7 @@ import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from '@
import { HighlightMatches } from '@/components/ui/highlight-matches'
import { KbdCombo } from '@/components/ui/kbd'
import { getHermesConfigRecord, listAllProfileSessions } from '@/hermes'
import { useMediaQuery } from '@/hooks/use-media-query'
import { useI18n } from '@/i18n'
import { sessionTitle } from '@/lib/chat-runtime'
import {
@@ -248,8 +249,8 @@ const rankGroups = (groups: PaletteGroup[], search: string): PaletteGroup[] => {
.map(entry => entry.group)
}
// cmdk selection values must be unique; labels alone can repeat (the same
// theme lists under both Light and Dark). The id suffix disambiguates.
// cmdk selection values must be unique; labels alone can repeat (a settings
// field and a session can share a title). The id suffix disambiguates.
const paletteValue = (item: PaletteItem): string => `${item.label}\u0001${item.id}`
const EMPTY_GROUPS: PaletteGroup[] = []
@@ -559,6 +560,15 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
const { availableThemes, clearThemePreview, mode, previewTheme, resolvedMode, setMode, setTheme, themeName } =
useTheme()
// Mode rows preview like theme rows do: paint the committed skin at the
// highlighted brightness. `system` has to be resolved here — previewTheme
// paints a concrete light/dark.
const systemDark = useMediaQuery('(prefers-color-scheme: dark)')
const resolveThemeMode = useCallback(
(target: ThemeMode): 'light' | 'dark' => (target === 'system' ? (systemDark ? 'dark' : 'light') : target),
[systemDark]
)
const [search, setSearch] = useState('')
const [page, setPage] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
@@ -1148,6 +1158,7 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
keepOpen: true,
keywords: ['appearance', 'color mode', 'brightness', entry.mode, t.settings.modeOptions[entry.mode].label],
label: t.settings.modeOptions[entry.mode].label,
onHighlight: () => previewTheme(themeName, resolveThemeMode(entry.mode)),
run: () => setMode(entry.mode)
}))
})
@@ -1232,6 +1243,7 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
mode,
previewTheme,
resolvedMode,
resolveThemeMode,
search,
sessions,
setMode,
@@ -1325,27 +1337,51 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
}
]
},
// Built-ins and imported families list under the mode(s) they support;
// picking sets skin + mode at once. A multi-variant import (GitHub,
// Solarized) appears in both groups and switches variants with the mode.
...(['light', 'dark'] as const).map(groupMode => ({
heading: groupMode === 'light' ? t.settings.modeOptions.light.label : t.settings.modeOptions.dark.label,
items: availableThemes
.filter(theme => themeSupportsMode(theme.name, groupMode))
.map(theme => ({
active: themeName === theme.name && resolvedMode === groupMode,
icon: groupMode === 'light' ? Sun : Moon,
id: `theme-${theme.name}-${groupMode}`,
// Brightness lives with the palettes: one mode toggle for the whole
// list instead of splitting every theme across a Light and a Dark group.
{
heading: t.settings.appearance.colorMode,
items: THEME_MODES.map(entry => ({
active: mode === entry.mode,
icon: entry.icon,
id: `theme-mode-${entry.mode}`,
keepOpen: true,
keywords: ['appearance', 'brightness', 'color mode', t.settings.modeOptions[entry.mode].label],
label: t.settings.modeOptions[entry.mode].label,
onHighlight: () => previewTheme(themeName, resolveThemeMode(entry.mode)),
run: () => setMode(entry.mode)
}))
},
// Every palette once, applied on top of the selected mode. An import
// that only ships one variant (Dracula) flips the mode to the side it
// can actually render.
{
heading: t.settings.appearance.themeTitle,
items: availableThemes.map(theme => {
const previewMode = themeSupportsMode(theme.name, resolvedMode)
? resolvedMode
: resolvedMode === 'dark'
? 'light'
: 'dark'
return {
active: themeName === theme.name,
icon: Palette,
id: `theme-${theme.name}`,
keepOpen: true,
keywords: ['theme', 'appearance', 'palette', groupMode, theme.label, theme.description ?? ''],
keywords: ['theme', 'appearance', 'palette', theme.label, theme.description ?? ''],
label: theme.label,
onHighlight: () => previewTheme(theme.name, groupMode),
onHighlight: () => previewTheme(theme.name, previewMode),
run: () => {
setTheme(theme.name)
setMode(groupMode)
if (previewMode !== resolvedMode) {
setMode(previewMode)
}
}
}))
}))
}
})
}
]
},
'color-mode': {
@@ -1361,6 +1397,7 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
keepOpen: true,
keywords: ['appearance', 'brightness', t.settings.modeOptions[entry.mode].label],
label: t.settings.modeOptions[entry.mode].label,
onHighlight: () => previewTheme(themeName, resolveThemeMode(entry.mode)),
run: () => setMode(entry.mode)
}))
}
@@ -1387,7 +1424,18 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
groups: settingsPageGroups
}
}),
[availableThemes, mode, previewTheme, resolvedMode, setMode, setTheme, settingsPageGroups, t, themeName]
[
availableThemes,
mode,
previewTheme,
resolvedMode,
resolveThemeMode,
setMode,
setTheme,
settingsPageGroups,
t,
themeName
]
)
const activePage = page ? subPages[page] : null