>
)
}
// Keys a range input treats as a step, so the peek can flash the live window
// for keyboard adjustment the way a pointer drag holds it open.
const SLIDER_STEP_KEYS = new Set([
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'End',
'Home',
'PageDown',
'PageUp'
])
interface TranslucencySliderProps {
label: string
onChange: (value: number) => void
value: number
}
/**
* One 0–100 lever, used up to twice: Clear's window opacity, and under Glass
* the tint plus an optional native fade.
*
* Peek while the hand is on it — the overlay (scrim + near-opaque card) ghosts
* so the window behind IS the live preview. The pointer pair covers
* mouse/touch drags; the keyboard path pulses per step instead, and blur ends
* any residual hold.
*/
function TranslucencySlider({ label, onChange, value }: TranslucencySliderProps) {
return (
<>
{
triggerHaptic('selection')
onChange(Number(event.target.value))
}}
onKeyDown={event => {
if (SLIDER_STEP_KEYS.has(event.key)) {
pulseTranslucencyPeek()
}
}}
onLostPointerCapture={endTranslucencyPeek}
onPointerDown={beginTranslucencyPeek}
onPointerUp={endTranslucencyPeek}
step={TRANSLUCENCY_STEP}
style={{ accentColor: 'var(--dt-primary)' }}
type="range"
value={value}
/>
{value}%
>
)
}
interface GlassRowProps {
children: React.ReactNode
label: string
}
/** A labelled control in the Glass sub-panel: tint, fade, frost, area. */
function GlassRow({ children, label }: GlassRowProps) {
return (
{label}
{children}
)
}
export function AppearanceSettings() {
const { t, isSavingLocale } = useI18n()
const { themeName, mode, resolvedMode, availableThemes, setTheme, setMode } = useTheme()
const toolViewMode = useStore($toolViewMode)
const reasoningCollapsedByDefault = useStore($reasoningCollapsedByDefault)
const sessionListDensity = useStore($sessionListDensity)
const tabStripDefault = useStore($tabStripDefault)
const zoomPercent = useStore($zoomPercent)
const embedMode = useStore($embedMode)
const embedAllowed = useStore($embedAllowed)
const composerPopoutGesturesEnabled = useStore($composerPopoutGesturesEnabled)
const translucency = useStore($translucency)
const glassMode = translucency.mode === 'glass' && GLASS_SUPPORTED
const reactionsEnabled = useStore($reactionsEnabled)
const tipsEnabled = useStore($tipsEnabled)
const toursEnabled = useStore($toursEnabled)
const retiredTips = useStore($retiredTips)
const vibeHeartsEnabled = useStore($vibeHeartsEnabled)
const backdrop = useStore($backdrop)
const introSplash = useStore($introSplash)
const installs = useStore($marketplaceInstalls)
const profiles = useStore($profiles)
const activeProfileKey = normalizeProfileKey(useStore($activeGatewayProfile))
const a = t.settings.appearance
// A pointer held on the intensity slider when this overlay closes (Escape
// mid-drag) never delivers its pointerup here, which would strand the peek
// counter above zero and ghost the NEXT settings overlay. Unmount drops
// every outstanding hold.
useEffect(() => resetTranslucencyPeek, [])
// Shared by the mode/frost/area pickers: apply the choice, then show it
// through the overlay it just altered (a pulse, not a hold — see the peek
// notes on the slider itself).
const pickTranslucency =
(set: (value: T) => void) =>
(value: T) => {
triggerHaptic('selection')
set(value)
if (translucency.intensity > 0) {
pulseTranslucencyPeek()
}
}
const [query, setQuery] = useState('')
useDeepLinkHighlight({
elementId: appearanceSettingElementId,
param: 'setting',
ready: id => APPEARANCE_SEARCH_TARGETS.has(id)
})
// One box does double duty: filter installed themes live (below), and run a
// name search against the VS Code Marketplace (the Cmd-K "Install theme…"
// backend) for anything not already installed.
const needle = normalize(query)
const filteredThemes = availableThemes
.filter(
theme =>
!needle ||
theme.label.toLowerCase().includes(needle) ||
theme.name.toLowerCase().includes(needle) ||
theme.description.toLowerCase().includes(needle)
)
// Active theme first; stable sort keeps the rest in their original order.
.sort((a, b) => Number(b.name === themeName) - Number(a.name === themeName))
// Themes save per profile. Surface that only when the user actually has more
// than one profile (single-profile installs never see the distinction).
const showProfileNote = profiles.length > 1
const activeProfileName =
profiles.find(profile => normalizeProfileKey(profile.name) === activeProfileKey)?.name ?? activeProfileKey
const modeOptions = MODE_OPTIONS.map(({ id, icon }) => ({ icon, id, label: t.settings.modeOptions[id].label }))
const toolOptions = [
{ id: 'product', label: a.product },
{ id: 'technical', label: a.technical }
] as const
const sessionDensityOptions = [
{ id: 'compact', label: a.sessionDensityCompact },
{ id: 'comfortable', label: a.sessionDensityComfortable },
{ id: 'detailed', label: a.sessionDensityDetailed }
] as const satisfies readonly { id: SessionListDensity; label: string }[]
const tabStripOptions = [
{ id: 'auto', label: a.tabStripAuto },
{ id: 'always', label: a.tabStripAlways },
{ id: 'never', label: a.tabStripNever }
] as const satisfies readonly { id: TabStripDefault; label: string }[]
const embedOptions = [
{ id: 'ask', label: a.embedsAsk },
{ id: 'always', label: a.embedsAlways },
{ id: 'off', label: a.embedsOff }
] as const satisfies readonly { id: EmbedMode; label: string }[]
const uiScaleOptions = UI_SCALE_PRESETS.map(preset => ({ id: preset, label: `${preset}%` }))
const matchedScalePreset = matchUiScalePreset(zoomPercent)
return (
{a.intro}
}
description={isSavingLocale ? t.language.saving : t.language.description}
id={appearanceSettingElementId(APPEARANCE_SETTING_IDS.language)}
title={t.language.label}
/>
{/* One search box: filters your installed themes (the grid)
and live-searches the VS Code Marketplace below. */}
}
wide
/>
{
triggerHaptic('selection')
setZoomPercent(Number(id))
}}
options={uiScaleOptions}
value={matchedScalePreset ?? ('' as UiScalePreset)}
/>
}
description={a.uiScaleDesc(zoomPercent)}
id={appearanceSettingElementId(APPEARANCE_SETTING_IDS.uiScale)}
title={a.uiScaleTitle}
/>
{
triggerHaptic('selection')
setSessionListDensity(id)
}}
options={sessionDensityOptions}
value={sessionListDensity}
/>
}
description={a.sessionDensityDesc}
title={a.sessionDensityTitle}
/>
{
triggerHaptic('selection')
setTabStripDefault(id)
}}
options={tabStripOptions}
value={tabStripDefault}
/>
}
description={a.tabStripDesc}
title={a.tabStripTitle}
/>
{/* Linux has neither half of this setting (see TRANSLUCENCY_SUPPORTED),
so the row is absent there rather than offering a dead lever. */}
{TRANSLUCENCY_SUPPORTED && (
{GLASS_SUPPORTED && (
)}
{/* Clear has one lever and it belongs beside the mode. Glass
has four controls, so they move into the labelled panel
below rather than crowding this line with an unlabelled
slider that means something different. */}
{!glassMode && (
)}