import { Component, Show, createMemo, createResource, onMount } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { ButtonV2 } from "@kirincode-ai/ui/v2/button-v2" import { SelectV2 } from "@kirincode-ai/ui/v2/select-v2" import { Switch } from "@kirincode-ai/ui/v2/switch-v2" import { TextInputV2 } from "@kirincode-ai/ui/v2/text-input-v2" import { useTheme, type ColorScheme } from "@kirincode-ai/ui/theme/context" import { useDialog } from "@kirincode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" import { usePermission } from "@/context/permission" import { usePlatform } from "@/context/platform" import { useServerSync } from "@/context/server-sync" import { useServerSDK } from "@/context/server-sdk" import { useUpdaterAction } from "../updater-action" import { monoDefault, monoFontFamily, monoInput, sansDefault, sansFontFamily, sansInput, terminalDefault, terminalFontFamily, terminalInput, useSettings, } from "@/context/settings" import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" import { Link } from "../link" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import "./settings-v2.css" let demoSoundState = { cleanup: undefined as (() => void) | undefined, timeout: undefined as NodeJS.Timeout | undefined, run: 0, } type ThemeOption = { id: string name: string } type ShellOption = { path: string name: string acceptable: boolean } type ShellSelectOption = { id: string value: string label: string } // To prevent audio from overlapping/playing very quickly when navigating the settings menus, // delay the playback by 100ms during quick selection changes and pause existing sounds. const stopDemoSound = () => { demoSoundState.run += 1 if (demoSoundState.cleanup) { demoSoundState.cleanup() } clearTimeout(demoSoundState.timeout) demoSoundState.cleanup = undefined } const playDemoSound = (id: string | undefined) => { stopDemoSound() if (!id) return const run = ++demoSoundState.run demoSoundState.timeout = setTimeout(() => { void playSoundById(id).then((cleanup) => { if (demoSoundState.run !== run) { cleanup?.() return } demoSoundState.cleanup = cleanup }) }, 100) } export const SettingsGeneralV2: Component<{ sessionID?: string }> = (props) => { const theme = useTheme() const language = useLanguage() const permission = usePermission() const platform = usePlatform() const dialog = useDialog() const settings = useSettings() const serverSync = useServerSync() const serverSdk = useServerSDK() const mobile = createMediaQuery("(max-width: 767px)") const updater = useUpdaterAction() const dir = createMemo(() => { if (!props.sessionID) return undefined return serverSync().session.lineage.peek(props.sessionID)?.session.directory }) const accepting = createMemo(() => { const value = dir() if (!value || !props.sessionID) return false return permission.isAutoAccepting(props.sessionID, value) }) const toggleAccept = (checked: boolean) => { const value = dir() if (!value || !props.sessionID) return if (checked) { permission.enableAutoAccept(props.sessionID, value) return } permission.disableAutoAccept(props.sessionID, value) } const desktop = createMemo(() => platform.platform === "desktop") const themeOptions = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) const [shells] = createResource( () => serverSdk() .client.pty.shells() .then((res) => res.data ?? []) .catch(() => [] as ShellOption[]), { initialValue: [] as ShellOption[] }, ) const [pinchZoom, { mutate: setPinchZoom }] = createResource( () => (desktop() && platform.getPinchZoomEnabled ? true : false), () => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false), { initialValue: false }, ) onMount(() => { void theme.loadThemes() }) const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") } const currentShell = createMemo(() => serverSync().data.config.shell ?? "") const shellOptions = createMemo(() => { const list = shells.latest const current = serverSync().data.config.shell const nameCounts = new Map() for (const s of list) { nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1) } const options = [ autoOption, ...list.map((s) => { const ambiguousName = (nameCounts.get(s.name) || 0) > 1 const text = ambiguousName ? s.path : s.name const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})` return { id: s.path, // Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH. value: ambiguousName ? s.path : s.name, label, } }), ] if (current && !options.some((o) => o.value === current)) { options.push({ id: current, value: current, label: current }) } return options }) const onPinchZoomChange = (checked: boolean) => { setPinchZoom(checked) const update = platform.setPinchZoomEnabled?.(checked) if (!update) return void update.catch(() => setPinchZoom(!checked)) } const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [ { value: "system", label: language.t("theme.scheme.system") }, { value: "light", label: language.t("theme.scheme.light") }, { value: "dark", label: language.t("theme.scheme.dark") }, ]) const languageOptions = createMemo(() => language.locales.map((locale) => ({ value: locale, label: language.label(locale), })), ) const noneSound = { id: "none", label: "sound.option.none" } as const const soundOptions = [noneSound, ...SOUND_OPTIONS] const mono = () => monoInput(settings.appearance.font()) const sans = () => sansInput(settings.appearance.uiFont()) const terminal = () => terminalInput(settings.appearance.terminalFont()) const soundSelectProps = ( enabled: () => boolean, current: () => string, setEnabled: (value: boolean) => void, set: (id: string) => void, ) => ({ options: soundOptions, current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound, value: (o: (typeof soundOptions)[number]) => o.id, label: (o: (typeof soundOptions)[number]) => language.t(o.label), onHighlight: (option: (typeof soundOptions)[number] | undefined) => { if (!option) return playDemoSound(option.id === "none" ? undefined : option.id) }, onSelect: (option: (typeof soundOptions)[number] | null) => { if (!option) return if (option.id === "none") { setEnabled(false) stopDemoSound() return } setEnabled(true) set(option.id) playDemoSound(option.id) }, }) const GeneralSection = () => (
o.value === language.locale())} value={(o) => o.value} label={(o) => o.label} onSelect={(option) => option && language.setLocale(option.value)} />
o.value === currentShell()) ?? autoOption} placement="bottom-end" gutter={6} value={(o) => o.id} label={(o) => o.label} onSelect={(option) => { if (!option) return if (option.value === currentShell()) return serverSync().updateConfig({ shell: option.value }) }} />
settings.general.setShowReasoningSummaries(checked)} />
settings.general.setShellToolPartsExpanded(checked)} />
settings.general.setEditToolPartsExpanded(checked)} />
{ settings.general.setNewLayoutDesigns(checked) if (checked) return void import("@/components/dialog-settings").then((module) => { dialog.show(() => ) }) }} />
settings.general.setMobileTitlebarPosition(checked ? "bottom" : "top")} />
) const AdvancedSection = () => (

{language.t("settings.general.section.advanced")}

settings.general.setShowFileTree(checked)} />
settings.general.setShowSearch(checked)} />
settings.general.setShowStatus(checked)} />
settings.general.setShowCustomAgents(checked)} />
) const AppearanceSection = () => (

{language.t("settings.general.section.appearance")}

o.value === theme.colorScheme())} placement="bottom-end" gutter={6} value={(o) => o.value} label={(o) => o.label} onSelect={(option) => option && theme.setColorScheme(option.value)} /> {language.t("settings.general.row.theme.description")}{" "} {language.t("common.learnMore")} } > o.id === theme.themeId())} placement="bottom-end" gutter={6} value={(o) => o.id} label={(o) => o.name} onSelect={(option) => { if (!option) return theme.setTheme(option.id) }} />
settings.appearance.setUIFont(event.currentTarget.value)} placeholder={sansDefault} spellcheck={false} autocorrect="off" autocomplete="off" autocapitalize="off" aria-label={language.t("settings.general.row.uiFont.title")} style={{ "font-family": sansFontFamily(settings.appearance.uiFont()) }} />
settings.appearance.setFont(event.currentTarget.value)} placeholder={monoDefault} spellcheck={false} autocorrect="off" autocomplete="off" autocapitalize="off" aria-label={language.t("settings.general.row.font.title")} style={{ "font-family": monoFontFamily(settings.appearance.font()) }} />
settings.appearance.setTerminalFont(event.currentTarget.value)} placeholder={terminalDefault} spellcheck={false} autocorrect="off" autocomplete="off" autocapitalize="off" aria-label={language.t("settings.general.row.terminalFont.title")} style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }} />
) const NotificationsSection = () => (

{language.t("settings.general.section.notifications")}

settings.notifications.setAgent(checked)} />
settings.notifications.setPermissions(checked)} />
settings.notifications.setErrors(checked)} />
) const SoundsSection = () => (

{language.t("settings.general.section.sounds")}

settings.sounds.agentEnabled(), () => settings.sounds.agent(), (value) => settings.sounds.setAgentEnabled(value), (id) => settings.sounds.setAgent(id), )} placement="bottom-end" gutter={6} /> settings.sounds.permissionsEnabled(), () => settings.sounds.permissions(), (value) => settings.sounds.setPermissionsEnabled(value), (id) => settings.sounds.setPermissions(id), )} placement="bottom-end" gutter={6} /> settings.sounds.errorsEnabled(), () => settings.sounds.errors(), (value) => settings.sounds.setErrorsEnabled(value), (id) => settings.sounds.setErrors(id), )} placement="bottom-end" gutter={6} />
) const UpdatesSection = () => (

{language.t("settings.general.section.updates")}

settings.general.setReleaseNotes(checked)} />
{language.t(updater.action().label)}
) // We can probably remove this, right? const DisplaySection = () => (

{language.t("settings.general.section.display")}

) return ( <>

{language.t("settings.tab.general")}

) }