feat(ui): centralize interaction preferences

Expose interaction defaults in Settings and reuse the same registry for command palette actions.
This commit is contained in:
Shantur Rathore
2026-03-11 10:53:28 +00:00
parent 0d9da40102
commit 627ff2d42b
11 changed files with 850 additions and 183 deletions

View File

@@ -1,7 +1,10 @@
import type { Component } from "solid-js"
import { Check, Laptop, Moon, Sun } from "lucide-solid"
import { Select } from "@kobalte/core/select"
import { createEffect, createMemo, createSignal, For, type Component } from "solid-js"
import { Check, ChevronDown, Laptop, Moon, Sun } from "lucide-solid"
import { useI18n } from "../../lib/i18n"
import { useTheme, type ThemeMode } from "../../lib/theme"
import { useConfig } from "../../stores/preferences"
import { getBehaviorSettings, type BehaviorSetting } from "../../lib/settings/behavior-registry"
const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [
{ value: "system", icon: Laptop },
@@ -12,6 +15,200 @@ const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [
export const AppearanceSettingsSection: Component = () => {
const { t } = useI18n()
const { themeMode, setThemeMode } = useTheme()
const {
preferences,
updatePreferences,
toggleShowThinkingBlocks,
toggleKeyboardShortcutHints,
toggleShowTimelineTools,
toggleUsageMetrics,
toggleAutoCleanupBlankSessions,
togglePromptSubmitOnEnter,
setDiffViewMode,
setToolOutputExpansion,
setDiagnosticsExpansion,
setThinkingBlocksExpansion,
setToolInputsVisibility,
} = useConfig()
const behaviorSettings = createMemo(() =>
getBehaviorSettings({
preferences,
updatePreferences,
toggleShowThinkingBlocks,
toggleKeyboardShortcutHints,
toggleShowTimelineTools,
toggleUsageMetrics,
toggleAutoCleanupBlankSessions,
togglePromptSubmitOnEnter,
setDiffViewMode,
setToolOutputExpansion,
setDiagnosticsExpansion,
setThinkingBlocksExpansion,
setToolInputsVisibility,
}),
)
const [overrides, setOverrides] = createSignal<Map<string, unknown>>(new Map())
const setOverride = (id: string, value: unknown) => {
setOverrides((prev) => {
const next = new Map(prev)
next.set(id, value)
return next
})
}
createEffect(() => {
const current = overrides()
if (current.size === 0) return
const prefs = preferences()
const settings = behaviorSettings()
let changed = false
const next = new Map(current)
for (const setting of settings) {
if (!next.has(setting.id)) continue
const overrideValue = next.get(setting.id)
const actualValue = setting.get(prefs)
if (Object.is(actualValue, overrideValue)) {
next.delete(setting.id)
changed = true
}
}
if (changed) {
setOverrides(next)
}
})
const readSettingValue = (setting: BehaviorSetting) => {
const current = overrides()
if (current.has(setting.id)) return current.get(setting.id)
return setting.get(preferences())
}
type SelectOption = { value: string; label: string }
const BehaviorRow: Component<{ setting: BehaviorSetting }> = (props) => {
const setting = props.setting
const disabled = createMemo(() => (setting.disabled ? Boolean(setting.disabled()) : false))
if (setting.kind === "toggle") {
const options = createMemo<SelectOption[]>(() => [
{ value: "true", label: t("settings.common.enabled") },
{ value: "false", label: t("settings.common.disabled") },
])
const currentValue = createMemo(() => String(Boolean(readSettingValue(setting))))
const selectedOption = createMemo(() => options().find((opt) => opt.value === currentValue()))
return (
<div class={`settings-toggle-row ${disabled() ? "opacity-60" : ""}`}>
<div>
<div class="settings-toggle-title">{t(setting.titleKey)}</div>
<div class="settings-toggle-caption">{t(setting.subtitleKey)}</div>
</div>
<Select<SelectOption>
value={selectedOption()}
onChange={(opt) => {
if (!opt) return
const next = opt.value === "true"
setOverride(setting.id, next)
setting.set(next)
}}
options={options()}
optionValue="value"
optionTextValue="label"
disabled={disabled()}
itemComponent={(itemProps) => (
<Select.Item item={itemProps.item} class="selector-option">
<Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}</Select.ItemLabel>
</Select.Item>
)}
>
<Select.Trigger class="selector-trigger" aria-label={t(setting.titleKey)}>
<div class="flex-1 min-w-0">
<Select.Value<SelectOption>>
{(state) => (
<span class="selector-trigger-primary selector-trigger-primary--align-left">
{state.selectedOption()?.label}
</span>
)}
</Select.Value>
</div>
<Select.Icon class="selector-trigger-icon">
<ChevronDown class="w-3 h-3" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content class="selector-popover">
<Select.Listbox class="selector-listbox" />
</Select.Content>
</Select.Portal>
</Select>
</div>
)
}
const enumSetting = setting as Extract<BehaviorSetting, { kind: "enum" }>
const options = createMemo<SelectOption[]>(() =>
enumSetting.options.map((opt: { value: string; labelKey: string }) => ({
value: String(opt.value),
label: t(opt.labelKey),
})),
)
const currentValue = createMemo(() => String(readSettingValue(setting) ?? ""))
const selectedOption = createMemo(() => options().find((opt) => opt.value === currentValue()))
return (
<div class={`settings-toggle-row ${disabled() ? "opacity-60" : ""}`}>
<div>
<div class="settings-toggle-title">{t(setting.titleKey)}</div>
<div class="settings-toggle-caption">{t(setting.subtitleKey)}</div>
</div>
<Select<SelectOption>
value={selectedOption()}
onChange={(opt) => {
if (!opt) return
setOverride(setting.id, opt.value)
enumSetting.set(opt.value as any)
}}
options={options()}
optionValue="value"
optionTextValue="label"
disabled={disabled()}
itemComponent={(itemProps) => (
<Select.Item item={itemProps.item} class="selector-option">
<Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}</Select.ItemLabel>
</Select.Item>
)}
>
<Select.Trigger class="selector-trigger" aria-label={t(setting.titleKey)}>
<div class="flex-1 min-w-0">
<Select.Value<SelectOption>>
{(state) => (
<span class="selector-trigger-primary selector-trigger-primary--align-left">
{state.selectedOption()?.label}
</span>
)}
</Select.Value>
</div>
<Select.Icon class="selector-trigger-icon">
<ChevronDown class="w-3 h-3" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content class="selector-popover">
<Select.Listbox class="selector-listbox" />
</Select.Content>
</Select.Portal>
</Select>
</div>
)
}
const modeLabel = (mode: ThemeMode) => {
if (mode === "system") return t("theme.mode.system")
@@ -54,6 +251,20 @@ export const AppearanceSettingsSection: Component = () => {
})}
</div>
</div>
<div class="settings-card">
<div class="settings-card-header">
<div>
<h3 class="settings-card-title">{t("settings.appearance.behavior.title")}</h3>
<p class="settings-card-subtitle">{t("settings.appearance.behavior.subtitle")}</p>
</div>
<span class="settings-scope-badge">{t("settings.scope.device")}</span>
</div>
<div class="settings-stack">
<For each={behaviorSettings()}>{(setting) => <BehaviorRow setting={setting} />}</For>
</div>
</div>
</div>
)
}