import { Spinner } from "~/components/ui/spinner";import { ArchiveIcon, ArchiveX, ChevronRightIcon, SettingsIcon } from "lucide-react";import { Link, useNavigate } from "@tanstack/react-router";import type { CSSProperties, ReactNode } from "react";import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { useAtomValue } from "@effect/atom-react";import { type BackgroundActivityProfile, type DesktopUpdateChannel, ProviderDriverKind, type ProviderInstanceId, type ScopedThreadRef, type SidebarProjectGroupingMode,} from "@t3tools/contracts";import { scopeThreadRef } from "@t3tools/client-runtime/environment";import { isAtomCommandInterrupted, settlePromise, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, DEFAULT_UNIFIED_SETTINGS, type DiffLayout, type EnvironmentIdentificationMode, MAX_APPEARANCE_CONTRAST, MAX_CODE_FONT_SIZE, MAX_GLASS_OPACITY, MAX_INTERFACE_FONT_SIZE, MAX_PANEL_ANIMATION_DURATION_MS, MAX_PROMPT_FONT_SIZE, MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, MAX_TERMINAL_FONT_SIZE, MIN_CODE_FONT_SIZE, MIN_APPEARANCE_CONTRAST, MIN_GLASS_OPACITY, MIN_INTERFACE_FONT_SIZE, MIN_PANEL_ANIMATION_DURATION_MS, MIN_PROMPT_FONT_SIZE, MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, MIN_TERMINAL_FONT_SIZE, type QuitConfirmationMode,} from "@t3tools/contracts/settings";import { resolveServerBackgroundActivitySettings } from "@t3tools/shared/backgroundActivitySettings";import { createModelSelection } from "@t3tools/shared/model";import * as Duration from "effect/Duration";import * as Equal from "effect/Equal";import * as Schema from "effect/Schema";import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding";import { canCheckForUpdate, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction,} from "../../components/desktopUpdate.logic";import { ProviderModelPicker } from "../chat/ProviderModelPicker";import { TraitsPicker } from "../chat/TraitsPicker";import { resolveEnvironmentIdentificationPillLabel, useEnvironmentStageLabel,} from "../SidebarStageBackdrop";import { isElectron } from "../../env";import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing";import { useCustomThemes } from "../../hooks/useCustomThemes";import { readAppearanceModePreference, readThemeHalves, readThemePreference, useTheme,} from "../../hooks/useTheme";import { useLocalStorage } from "../../hooks/useLocalStorage";import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";import { useThreadActions } from "../../hooks/useThreadActions";import { useDesktopUpdateState } from "../../state/desktopUpdate";import { getCustomModelOptionsByInstance, resolveAppModelSelectionState, withoutPlanAgentSelection,} from "../../modelSelection";import { applyProviderInstanceSettings, deriveProviderInstanceEntries, sortProviderInstanceEntries,} from "../../providerInstances";import { ensureLocalApi, readLocalApi } from "../../localApi";import { isMacPlatform } from "../../lib/utils";import { primaryServerConfigAtom, primaryServerObservabilityAtom, primaryServerProvidersAtom,} from "../../state/server";import { useProjects } from "../../state/entities";import { usePrimaryEnvironmentId } from "../../state/environments";import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";import { formatRelativeTimeLabel } from "../../timestampFormat";import { Button } from "../ui/button";import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle,} from "../ui/dialog";import { DraftInput } from "../ui/draft-input";import { Input } from "../ui/input";import { DEFAULT_CODE_FONT_STACK, DEFAULT_SANS_FONT_STACK, isFontFamilyAvailable, isMonospaceFamily, resolveDefaultFamilyLabel, resolveTerminalFontPreference, resolveTerminalFontSizePreference, TYPOGRAPHY_ADVANCED_STORAGE_KEY,} from "../../appearanceFonts";import { CodeFontPreview, PromptFontPreview, TerminalFontPreview } from "./SettingsFontPreviews";import { SharedSettingsMismatchAlert } from "./SharedSettingsMismatchAlert";import { discoverInstalledFonts, FontFamilyPicker, useFontEnumeration } from "./FontFamilyPicker";import { NumberField, NumberFieldDecrement, NumberFieldGroup, NumberFieldIncrement, NumberFieldInput,} from "../ui/number-field";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { Switch } from "../ui/switch";import { stackedThreadToast, toastManager } from "../ui/toast";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import { ThemeLibrary } from "./ThemeSettings";import { backgroundActivityOverrideSettings, backgroundActivitySharedPolicySettings, durationToSeconds, formatDiagnosticsDescription, getChangedBrowserSettingLabels, getChangedTypographySettingLabels, normalizeIntervalSeconds, PROVIDER_HEALTH_INTERVAL_STEP_SECONDS, hasChangedBackgroundActivitySettings, isProjectGroupingEnabled, projectGroupingModeFromToggle, readLastEnabledProjectGroupingMode, rememberEnabledProjectGroupingMode, resolveBackgroundActivityProfileOption,} from "./SettingsPanels.logic";import { PolicyTooltip, SETTINGS_PICKER_TRIGGER_CLASSNAME, SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, useSettingsSearchTarget, useSettingsSearchTargetId,} from "./settingsLayout";import { searchableSetting } from "./settingsSearch";import { ProjectFavicon } from "../ProjectFavicon";import { PanelAnimationsPreview } from "./PanelAnimationsPreview"; const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = { artwork: "Artwork", pill: "Version pill", none: "None",}; const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", "24-hour": "24-hour",} as const; const DIFF_LAYOUT_LABELS: Record<DiffLayout, string> = { stacked: "Stacked", split: "Split",}; const QUIT_CONFIRMATION_MODE_LABELS: Record<QuitConfirmationMode, string> = { direct: "Direct", hold: "Hold", "double-click": "Double press",}; const BACKGROUND_ACTIVITY_PROFILE_LABELS: Record<BackgroundActivityProfile, string> = { balanced: "Balanced", performance: "Performance", "battery-saver": "Battery saver",}; type BackgroundActivityProfileOption = BackgroundActivityProfile | "advanced"; const BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS: Record<BackgroundActivityProfileOption, string> = { ...BACKGROUND_ACTIVITY_PROFILE_LABELS, advanced: "Advanced",}; const BACKGROUND_ACTIVITY_PROFILE_DESCRIPTIONS: Record<BackgroundActivityProfile, string> = { balanced: "Pauses probes for idle clients, locked hosts, or low power mode.", performance: "Allows scoped background probes while any subscribed client remains connected.", "battery-saver": "Also pauses background probes when the host or client is on battery.",}; const ADVANCED_BACKGROUND_ACTIVITY_DESCRIPTION = "Uses custom intervals."; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex");const BACKGROUND_ACTIVITY_BOOLEAN_OVERRIDES: ReadonlyArray<{ readonly key: | "pauseWhenHostLocked" | "pauseWhenHostLowPower" | "pauseWhenClientLowPower" | "pauseWhenOnBattery"; readonly label: string;}> = [ { key: "pauseWhenHostLocked", label: "Pause when host is locked" }, { key: "pauseWhenHostLowPower", label: "Pause on host low power" }, { key: "pauseWhenClientLowPower", label: "Pause on client low power" }, { key: "pauseWhenOnBattery", label: "Pause on battery" },]; function resetBackgroundActivitySettings() { return { backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity, };} function backgroundActivityProfileSettings(profile: BackgroundActivityProfile) { return { backgroundActivity: { schemaVersion: 1 as const, profile, overrides: {}, }, };} function AboutVersionTitle() { return ( <span className="inline-flex items-baseline gap-2"> <span>Version</span> <code className="text-[11px] font-medium text-muted-foreground">{APP_VERSION}</code> </span> );} function AboutVersionSection() { const updateState = useDesktopUpdateState(); const [isChangingUpdateChannel, setIsChangingUpdateChannel] = useState(false); const [isUpdateActionPending, setIsUpdateActionPending] = useState(false); const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge); const selectedUpdateChannel = updateState?.channel ?? "latest"; const selectedHostedAppChannel = hasDesktopBridge ? null : HOSTED_APP_CHANNEL; const handleUpdateChannelChange = useCallback( (channel: DesktopUpdateChannel) => { const bridge = window.desktopBridge; if ( !bridge || typeof bridge.setUpdateChannel !== "function" || channel === selectedUpdateChannel ) { return; } setIsChangingUpdateChannel(true); void bridge .setUpdateChannel(channel) .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not change update track", description: error instanceof Error ? error.message : "Update track change failed.", }), ); }) .finally(() => { setIsChangingUpdateChannel(false); }); }, [selectedUpdateChannel], ); const handleButtonClick = useCallback(async () => { const bridge = window.desktopBridge; if (!bridge) return; const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; if (action === "download") { void bridge.downloadUpdate().catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not download update", description: error instanceof Error ? error.message : "Download failed.", }), ); }); return; } if (action === "install") { if (isUpdateActionPending) return; setIsUpdateActionPending(true); let confirmed = false; try { confirmed = await ensureLocalApi().dialogs.confirm( getDesktopUpdateInstallConfirmationMessage( updateState ?? { availableVersion: null, downloadedVersion: null }, ), ); } catch (error) { setIsUpdateActionPending(false); toastManager.add( stackedThreadToast({ type: "error", title: "Could not confirm update", description: error instanceof Error ? error.message : "Update confirmation failed.", }), ); return; } if (!confirmed) { setIsUpdateActionPending(false); return; } void bridge .installUpdate() .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not install update", description: error instanceof Error ? error.message : "Install failed.", }), ); }) .finally(() => setIsUpdateActionPending(false)); return; } if (typeof bridge.checkForUpdate !== "function") return; void bridge .checkForUpdate() .then((result) => { if (!result.checked) { toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: result.state.message ?? "Automatic updates are not available in this build.", }), ); } }) .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: error instanceof Error ? error.message : "Update check failed.", }), ); }); }, [isUpdateActionPending, updateState]); const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; const buttonTooltip = updateState ? getDesktopUpdateButtonTooltip(updateState) : null; const buttonDisabled = action === "none" ? !canCheckForUpdate(updateState) : isDesktopUpdateButtonDisabled(updateState); const actionLabel: Record<string, string> = { download: "Download", install: "Install" }; const statusLabel: Record<string, string> = { checking: "Checking…", downloading: "Downloading…", "up-to-date": "Up to Date", }; const buttonLabel = actionLabel[action] ?? statusLabel[updateState?.status ?? ""] ?? "Check for Updates"; const description = action === "download" || action === "install" ? "Update available." : "Current version of the application."; return ( <> <SettingsRow title={<AboutVersionTitle />} description={description} control={ <Tooltip> <TooltipTrigger render={ <Button size="sm" variant="outline" disabled={buttonDisabled || isUpdateActionPending} onClick={handleButtonClick} > {buttonLabel} </Button> } /> {buttonTooltip ? <TooltipPopup>{buttonTooltip}</TooltipPopup> : null} </Tooltip> } /> {hasDesktopBridge ? ( <SettingsRow title="Update track" description="Use stable releases or nightly builds. Switch back anytime." control={ <Select value={selectedUpdateChannel} onValueChange={(value) => { handleUpdateChannelChange(value as DesktopUpdateChannel); }} > <SelectTrigger size="sm" className="w-full sm:w-40" aria-label="Update track" disabled={isChangingUpdateChannel} > <SelectValue> {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"} </SelectValue> </SelectTrigger> <SelectPopup align="end" alignItemWithTrigger={false}> <SelectItem hideIndicator value="latest"> Stable </SelectItem> <SelectItem hideIndicator value="nightly"> Nightly </SelectItem> </SelectPopup> </Select> } /> ) : selectedHostedAppChannel ? ( <SettingsRow title="Update track" description="Switches the hosted app release channel." control={ <Select value={selectedHostedAppChannel} onValueChange={(value) => { if (value === selectedHostedAppChannel) return; window.location.assign( buildHostedChannelSelectionUrl({ channel: value as HostedAppChannel }), ); }} > <SelectTrigger size="sm" className="w-full sm:w-40" aria-label="Update track"> <SelectValue>{HOSTED_APP_CHANNEL_LABEL}</SelectValue> </SelectTrigger> <SelectPopup align="end" alignItemWithTrigger={false}> <SelectItem hideIndicator value="latest"> Latest </SelectItem> <SelectItem hideIndicator value="nightly"> Nightly </SelectItem> </SelectPopup> </Select> } /> ) : null} </> );} export function useSettingsRestore(onRestored?: () => void) { const { theme, setTheme, followSystem, setFollowSystem, setThemeHalf, clearThemeHalves, themeHalves, } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const isTextGenerationModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const isBackgroundActivityDirty = hasChangedBackgroundActivitySettings(settings); const changedSettingLabels = useMemo(Lines 1–500 of 3105. Load more
import { Spinner } from "~/components/ui/spinner"; import { ArchiveIcon, ArchiveX, ChevronRightIcon, SettingsIcon } from "lucide-react"; import { Link, useNavigate } from "@tanstack/react-router"; import type { CSSProperties, ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useAtomValue } from "@effect/atom-react"; import { type BackgroundActivityProfile, type DesktopUpdateChannel, ProviderDriverKind, type ProviderInstanceId, type ScopedThreadRef, type SidebarProjectGroupingMode, } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { isAtomCommandInterrupted, settlePromise, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, DEFAULT_UNIFIED_SETTINGS, type DiffLayout, type EnvironmentIdentificationMode, MAX_APPEARANCE_CONTRAST, MAX_CODE_FONT_SIZE, MAX_GLASS_OPACITY, MAX_INTERFACE_FONT_SIZE, MAX_PANEL_ANIMATION_DURATION_MS, MAX_PROMPT_FONT_SIZE, MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, MAX_TERMINAL_FONT_SIZE, MIN_CODE_FONT_SIZE, MIN_APPEARANCE_CONTRAST, MIN_GLASS_OPACITY, MIN_INTERFACE_FONT_SIZE, MIN_PANEL_ANIMATION_DURATION_MS, MIN_PROMPT_FONT_SIZE, MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, MIN_TERMINAL_FONT_SIZE, type QuitConfirmationMode, } from "@t3tools/contracts/settings"; import { resolveServerBackgroundActivitySettings } from "@t3tools/shared/backgroundActivitySettings"; import { createModelSelection } from "@t3tools/shared/model"; import * as Duration from "effect/Duration"; import * as Equal from "effect/Equal"; import * as Schema from "effect/Schema"; import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; import { canCheckForUpdate, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, } from "../../components/desktopUpdate.logic"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { TraitsPicker } from "../chat/TraitsPicker"; import { resolveEnvironmentIdentificationPillLabel, useEnvironmentStageLabel, } from "../SidebarStageBackdrop"; import { isElectron } from "../../env"; import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; import { useCustomThemes } from "../../hooks/useCustomThemes"; import { readAppearanceModePreference, readThemeHalves, readThemePreference, useTheme, } from "../../hooks/useTheme"; import { useLocalStorage } from "../../hooks/useLocalStorage"; import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { useThreadActions } from "../../hooks/useThreadActions"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { getCustomModelOptionsByInstance, resolveAppModelSelectionState, withoutPlanAgentSelection, } from "../../modelSelection"; import { applyProviderInstanceSettings, deriveProviderInstanceEntries, sortProviderInstanceEntries, } from "../../providerInstances"; import { ensureLocalApi, readLocalApi } from "../../localApi"; import { isMacPlatform } from "../../lib/utils"; import { primaryServerConfigAtom, primaryServerObservabilityAtom, primaryServerProvidersAtom, } from "../../state/server"; import { useProjects } from "../../state/entities"; import { usePrimaryEnvironmentId } from "../../state/environments"; import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState"; import { formatRelativeTimeLabel } from "../../timestampFormat"; import { Button } from "../ui/button"; import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible"; import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "../ui/dialog"; import { DraftInput } from "../ui/draft-input"; import { Input } from "../ui/input"; import { DEFAULT_CODE_FONT_STACK, DEFAULT_SANS_FONT_STACK, isFontFamilyAvailable, isMonospaceFamily, resolveDefaultFamilyLabel, resolveTerminalFontPreference, resolveTerminalFontSizePreference, TYPOGRAPHY_ADVANCED_STORAGE_KEY, } from "../../appearanceFonts"; import { CodeFontPreview, PromptFontPreview, TerminalFontPreview } from "./SettingsFontPreviews"; import { SharedSettingsMismatchAlert } from "./SharedSettingsMismatchAlert"; import { discoverInstalledFonts, FontFamilyPicker, useFontEnumeration } from "./FontFamilyPicker"; import { NumberField, NumberFieldDecrement, NumberFieldGroup, NumberFieldIncrement, NumberFieldInput, } from "../ui/number-field"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { Switch } from "../ui/switch"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { ThemeLibrary } from "./ThemeSettings"; import { backgroundActivityOverrideSettings, backgroundActivitySharedPolicySettings, durationToSeconds, formatDiagnosticsDescription, getChangedBrowserSettingLabels, getChangedTypographySettingLabels, normalizeIntervalSeconds, PROVIDER_HEALTH_INTERVAL_STEP_SECONDS, hasChangedBackgroundActivitySettings, isProjectGroupingEnabled, projectGroupingModeFromToggle, readLastEnabledProjectGroupingMode, rememberEnabledProjectGroupingMode, resolveBackgroundActivityProfileOption, } from "./SettingsPanels.logic"; import { PolicyTooltip, SETTINGS_PICKER_TRIGGER_CLASSNAME, SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, useSettingsSearchTarget, useSettingsSearchTargetId, } from "./settingsLayout"; import { searchableSetting } from "./settingsSearch"; import { ProjectFavicon } from "../ProjectFavicon"; import { PanelAnimationsPreview } from "./PanelAnimationsPreview"; const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = { artwork: "Artwork", pill: "Version pill", none: "None", }; const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", "24-hour": "24-hour", } as const; const DIFF_LAYOUT_LABELS: Record<DiffLayout, string> = { stacked: "Stacked", split: "Split", }; const QUIT_CONFIRMATION_MODE_LABELS: Record<QuitConfirmationMode, string> = { direct: "Direct", hold: "Hold", "double-click": "Double press", }; const BACKGROUND_ACTIVITY_PROFILE_LABELS: Record<BackgroundActivityProfile, string> = { balanced: "Balanced", performance: "Performance", "battery-saver": "Battery saver", }; type BackgroundActivityProfileOption = BackgroundActivityProfile | "advanced"; const BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS: Record<BackgroundActivityProfileOption, string> = { ...BACKGROUND_ACTIVITY_PROFILE_LABELS, advanced: "Advanced", }; const BACKGROUND_ACTIVITY_PROFILE_DESCRIPTIONS: Record<BackgroundActivityProfile, string> = { balanced: "Pauses probes for idle clients, locked hosts, or low power mode.", performance: "Allows scoped background probes while any subscribed client remains connected.", "battery-saver": "Also pauses background probes when the host or client is on battery.", }; const ADVANCED_BACKGROUND_ACTIVITY_DESCRIPTION = "Uses custom intervals."; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); const BACKGROUND_ACTIVITY_BOOLEAN_OVERRIDES: ReadonlyArray<{ readonly key: | "pauseWhenHostLocked" | "pauseWhenHostLowPower" | "pauseWhenClientLowPower" | "pauseWhenOnBattery"; readonly label: string; }> = [ { key: "pauseWhenHostLocked", label: "Pause when host is locked" }, { key: "pauseWhenHostLowPower", label: "Pause on host low power" }, { key: "pauseWhenClientLowPower", label: "Pause on client low power" }, { key: "pauseWhenOnBattery", label: "Pause on battery" }, ]; function resetBackgroundActivitySettings() { return { backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity, }; } function backgroundActivityProfileSettings(profile: BackgroundActivityProfile) { return { backgroundActivity: { schemaVersion: 1 as const, profile, overrides: {}, }, }; } function AboutVersionTitle() { return ( <span className="inline-flex items-baseline gap-2"> <span>Version</span> <code className="text-[11px] font-medium text-muted-foreground">{APP_VERSION}</code> </span> ); } function AboutVersionSection() { const updateState = useDesktopUpdateState(); const [isChangingUpdateChannel, setIsChangingUpdateChannel] = useState(false); const [isUpdateActionPending, setIsUpdateActionPending] = useState(false); const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge); const selectedUpdateChannel = updateState?.channel ?? "latest"; const selectedHostedAppChannel = hasDesktopBridge ? null : HOSTED_APP_CHANNEL; const handleUpdateChannelChange = useCallback( (channel: DesktopUpdateChannel) => { const bridge = window.desktopBridge; if ( !bridge || typeof bridge.setUpdateChannel !== "function" || channel === selectedUpdateChannel ) { return; } setIsChangingUpdateChannel(true); void bridge .setUpdateChannel(channel) .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not change update track", description: error instanceof Error ? error.message : "Update track change failed.", }), ); }) .finally(() => { setIsChangingUpdateChannel(false); }); }, [selectedUpdateChannel], ); const handleButtonClick = useCallback(async () => { const bridge = window.desktopBridge; if (!bridge) return; const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; if (action === "download") { void bridge.downloadUpdate().catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not download update", description: error instanceof Error ? error.message : "Download failed.", }), ); }); return; } if (action === "install") { if (isUpdateActionPending) return; setIsUpdateActionPending(true); let confirmed = false; try { confirmed = await ensureLocalApi().dialogs.confirm( getDesktopUpdateInstallConfirmationMessage( updateState ?? { availableVersion: null, downloadedVersion: null }, ), ); } catch (error) { setIsUpdateActionPending(false); toastManager.add( stackedThreadToast({ type: "error", title: "Could not confirm update", description: error instanceof Error ? error.message : "Update confirmation failed.", }), ); return; } if (!confirmed) { setIsUpdateActionPending(false); return; } void bridge .installUpdate() .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not install update", description: error instanceof Error ? error.message : "Install failed.", }), ); }) .finally(() => setIsUpdateActionPending(false)); return; } if (typeof bridge.checkForUpdate !== "function") return; void bridge .checkForUpdate() .then((result) => { if (!result.checked) { toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: result.state.message ?? "Automatic updates are not available in this build.", }), ); } }) .catch((error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: error instanceof Error ? error.message : "Update check failed.", }), ); }); }, [isUpdateActionPending, updateState]); const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; const buttonTooltip = updateState ? getDesktopUpdateButtonTooltip(updateState) : null; const buttonDisabled = action === "none" ? !canCheckForUpdate(updateState) : isDesktopUpdateButtonDisabled(updateState); const actionLabel: Record<string, string> = { download: "Download", install: "Install" }; const statusLabel: Record<string, string> = { checking: "Checking…", downloading: "Downloading…", "up-to-date": "Up to Date", }; const buttonLabel = actionLabel[action] ?? statusLabel[updateState?.status ?? ""] ?? "Check for Updates"; const description = action === "download" || action === "install" ? "Update available." : "Current version of the application."; return ( <> <SettingsRow title={<AboutVersionTitle />} description={description} control={ <Tooltip> <TooltipTrigger render={ <Button size="sm" variant="outline" disabled={buttonDisabled || isUpdateActionPending} onClick={handleButtonClick} > {buttonLabel} </Button> } /> {buttonTooltip ? <TooltipPopup>{buttonTooltip}</TooltipPopup> : null} </Tooltip> } /> {hasDesktopBridge ? ( <SettingsRow title="Update track" description="Use stable releases or nightly builds. Switch back anytime." control={ <Select value={selectedUpdateChannel} onValueChange={(value) => { handleUpdateChannelChange(value as DesktopUpdateChannel); }} > <SelectTrigger size="sm" className="w-full sm:w-40" aria-label="Update track" disabled={isChangingUpdateChannel} > <SelectValue> {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"} </SelectValue> </SelectTrigger> <SelectPopup align="end" alignItemWithTrigger={false}> <SelectItem hideIndicator value="latest"> Stable </SelectItem> <SelectItem hideIndicator value="nightly"> Nightly </SelectItem> </SelectPopup> </Select> } /> ) : selectedHostedAppChannel ? ( <SettingsRow title="Update track" description="Switches the hosted app release channel." control={ <Select value={selectedHostedAppChannel} onValueChange={(value) => { if (value === selectedHostedAppChannel) return; window.location.assign( buildHostedChannelSelectionUrl({ channel: value as HostedAppChannel }), ); }} > <SelectTrigger size="sm" className="w-full sm:w-40" aria-label="Update track"> <SelectValue>{HOSTED_APP_CHANNEL_LABEL}</SelectValue> </SelectTrigger> <SelectPopup align="end" alignItemWithTrigger={false}> <SelectItem hideIndicator value="latest"> Latest </SelectItem> <SelectItem hideIndicator value="nightly"> Nightly </SelectItem> </SelectPopup> </Select> } /> ) : null} </> ); } export function useSettingsRestore(onRestored?: () => void) { const { theme, setTheme, followSystem, setFollowSystem, setThemeHalf, clearThemeHalves, themeHalves, } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const isTextGenerationModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const isBackgroundActivityDirty = hasChangedBackgroundActivitySettings(settings); const changedSettingLabels = useMemo(