import { ArchiveIcon, ArchiveX, LoaderIcon, PlusIcon, RefreshCwIcon } from "lucide-react";import { Link } from "@tanstack/react-router";import { useCallback, useMemo, useRef, useState } from "react";import { useAtomValue } from "@effect/atom-react";import { defaultInstanceIdForDriver, type DesktopUpdateChannel, PROVIDER_DISPLAY_NAMES, ProviderDriverKind, type ProviderInstanceConfig, type ProviderInstanceId, type ScopedThreadRef,} from "@t3tools/contracts";import { scopeThreadRef } from "@t3tools/client-runtime/environment";import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";import { isAtomCommandInterrupted, settlePromise, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";import { createModelSelection } from "@t3tools/shared/model";import * as Arr from "effect/Array";import * as Duration from "effect/Duration";import * as Equal from "effect/Equal";import * as Result from "effect/Result";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 { isElectron } from "../../env";import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing";import { useTheme } from "../../hooks/useTheme";import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";import { useThreadActions } from "../../hooks/useThreadActions";import { useDesktopUpdateState } from "../../state/desktopUpdate";import { getCustomModelOptionsByInstance, resolveAppModelSelectionState,} from "../../modelSelection";import { applyProviderInstanceSettings, deriveProviderInstanceEntries, sortProviderInstanceEntries,} from "../../providerInstances";import { ensureLocalApi, readLocalApi } from "../../localApi";import { primaryServerObservabilityAtom, primaryServerProvidersAtom, serverEnvironment,} from "../../state/server";import { usePrimaryEnvironment } from "../../state/environments";import { useProjects } from "../../state/entities";import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat";import { Button } from "../ui/button";import { DraftInput } from "../ui/draft-input";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 { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";import { canOneClickUpdateProviderCandidate, collectProviderUpdateCandidates, hasOneClickUpdateProviderCandidate, isProviderUpdateActive, type ProviderUpdateCandidate,} from "../ProviderUpdateLaunchNotification.logic";import { ProviderInstanceCard } from "./ProviderInstanceCard";import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";import { buildProviderInstanceUpdatePatch, formatDiagnosticsDescription,} from "./SettingsPanels.logic";import { SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, useRelativeTimeTick,} from "./settingsLayout";import { ProjectFavicon } from "../ProjectFavicon";import { useAtomCommand } from "../../state/use-atom-command"; const THEME_OPTIONS = [ { value: "system", label: "System", }, { value: "light", label: "Light", }, { value: "dark", label: "Dark", },] as const; const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", "24-hour": "24-hour",} as const; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); function withoutProviderInstanceKey<V>( record: Readonly<Record<ProviderInstanceId, V>> | undefined, key: ProviderInstanceId,): Record<ProviderInstanceId, V> { const next = { ...record } as Record<ProviderInstanceId, V>; delete next[key]; return next;} function withoutProviderInstanceFavorites( favorites: ReadonlyArray<{ readonly provider: ProviderInstanceId; readonly model: string }>, instanceId: ProviderInstanceId,) { return favorites.filter((favorite) => favorite.provider !== instanceId);} const PROVIDER_SETTINGS = DRIVER_OPTIONS.map((definition) => ({ provider: definition.value,})); function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }) { useRelativeTimeTick(); const lastCheckedRelative = lastCheckedAt ? formatRelativeTime(lastCheckedAt) : null; if (!lastCheckedRelative) { return null; } return ( <span className="text-[11px] text-muted-foreground/60"> {lastCheckedRelative.suffix ? ( <> Checked <span className="font-mono tabular-nums">{lastCheckedRelative.value}</span>{" "} {lastCheckedRelative.suffix} </> ) : ( <>Checked {lastCheckedRelative.value}</> )} </span> );} function AboutVersionTitle() { return ( <span className="inline-flex items-center 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 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(() => { 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") { const confirmed = window.confirm( getDesktopUpdateInstallConfirmationMessage( updateState ?? { availableVersion: null, downloadedVersion: null }, ), ); if (!confirmed) 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.", }), ); }); 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.", }), ); }); }, [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="xs" variant={action === "install" ? "default" : "outline"} disabled={buttonDisabled} onClick={handleButtonClick} > {buttonLabel} </Button> } /> {buttonTooltip ? <TooltipPopup>{buttonTooltip}</TooltipPopup> : null} </Tooltip> } /> {hasDesktopBridge ? ( <SettingsRow title="Update track" description="Stable follows full releases. Nightly follows the nightly desktop channel and can switch back to stable immediately." control={ <Select value={selectedUpdateChannel} onValueChange={(value) => { handleUpdateChannelChange(value as DesktopUpdateChannel); }} > <SelectTrigger 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 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 } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const isGitWritingModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const changedSettingLabels = useMemo( () => [ ...(theme !== "system" ? ["Theme"] : []), ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount ? ["Visible threads"] : []), ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? ["Diff line wrapping"] : []), ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace ? ["Diff whitespace changes"] : []), ...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar ? ["Auto-open task panel"] : []), ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming ? ["Assistant output"] : []), ...(Duration.toMillis(settings.automaticGitFetchInterval) !== Duration.toMillis(DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval) ? ["Automatic Git fetch interval"] : []), ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode ? ["New thread mode"] : []), ...(settings.newWorktreesStartFromOrigin !== DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin ? ["New worktrees start from origin"] : []), ...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory ? ["Add project base directory"] : []), ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive ? ["Archive confirmation"] : []), ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete ? ["Delete confirmation"] : []), ...(isGitWritingModelDirty ? ["Git writing model"] : []), ], [ isGitWritingModelDirty, settings.autoOpenPlanSidebar, settings.confirmThreadArchive, settings.confirmThreadDelete, settings.addProjectBaseDirectory, settings.defaultThreadEnvMode, settings.newWorktreesStartFromOrigin, settings.diffIgnoreWhitespace, settings.diffWordWrap, settings.automaticGitFetchInterval, settings.enableAssistantStreaming, settings.sidebarThreadPreviewCount, settings.timestampFormat, theme, ], ); const restoreDefaults = useCallback(async () => { if (changedSettingLabels.length === 0) return; const api = readLocalApi(); const confirmed = await (api ?? ensureLocalApi()).dialogs.confirm( ["Restore default settings?", `This will reset: ${changedSettingLabels.join(", ")}.`].join( "\n", ), ); if (!confirmed) return; setTheme("system"); updateSettings({ timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap, diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar, enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode, newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin, addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory, confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete, textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, }); onRestored?.(); }, [changedSettingLabels, onRestored, setTheme, updateSettings]); return { changedSettingLabels, restoreDefaults, };} export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const observability = useAtomValue(primaryServerObservabilityAtom); const serverProviders = useAtomValue(primaryServerProvidersAtom); const diagnosticsDescription = formatDiagnosticsDescription({ localTracingEnabled: observability?.localTracingEnabled ?? false, otlpTracesEnabled: observability?.otlpTracesEnabled ?? false, otlpTracesUrl: observability?.otlpTracesUrl, otlpMetricsEnabled: observability?.otlpMetricsEnabled ?? false, otlpMetricsUrl: observability?.otlpMetricsUrl, }); const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders); const textGenInstanceId = textGenerationModelSelection.instanceId; const textGenModel = textGenerationModelSelection.model; const textGenModelOptions = textGenerationModelSelection.options; const gitModelInstanceEntries = sortProviderInstanceEntries( applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), settings),Lines 1–500 of 1646. Load more
import { ArchiveIcon, ArchiveX, LoaderIcon, PlusIcon, RefreshCwIcon } from "lucide-react"; import { Link } from "@tanstack/react-router"; import { useCallback, useMemo, useRef, useState } from "react"; import { useAtomValue } from "@effect/atom-react"; import { defaultInstanceIdForDriver, type DesktopUpdateChannel, PROVIDER_DISPLAY_NAMES, ProviderDriverKind, type ProviderInstanceConfig, type ProviderInstanceId, type ScopedThreadRef, } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; import { isAtomCommandInterrupted, settlePromise, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; import { createModelSelection } from "@t3tools/shared/model"; import * as Arr from "effect/Array"; import * as Duration from "effect/Duration"; import * as Equal from "effect/Equal"; import * as Result from "effect/Result"; 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 { isElectron } from "../../env"; import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; import { useTheme } from "../../hooks/useTheme"; import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { useThreadActions } from "../../hooks/useThreadActions"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { getCustomModelOptionsByInstance, resolveAppModelSelectionState, } from "../../modelSelection"; import { applyProviderInstanceSettings, deriveProviderInstanceEntries, sortProviderInstanceEntries, } from "../../providerInstances"; import { ensureLocalApi, readLocalApi } from "../../localApi"; import { primaryServerObservabilityAtom, primaryServerProvidersAtom, serverEnvironment, } from "../../state/server"; import { usePrimaryEnvironment } from "../../state/environments"; import { useProjects } from "../../state/entities"; import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState"; import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat"; import { Button } from "../ui/button"; import { DraftInput } from "../ui/draft-input"; 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 { AddProviderInstanceDialog } from "./AddProviderInstanceDialog"; import { canOneClickUpdateProviderCandidate, collectProviderUpdateCandidates, hasOneClickUpdateProviderCandidate, isProviderUpdateActive, type ProviderUpdateCandidate, } from "../ProviderUpdateLaunchNotification.logic"; import { ProviderInstanceCard } from "./ProviderInstanceCard"; import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta"; import { buildProviderInstanceUpdatePatch, formatDiagnosticsDescription, } from "./SettingsPanels.logic"; import { SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, useRelativeTimeTick, } from "./settingsLayout"; import { ProjectFavicon } from "../ProjectFavicon"; import { useAtomCommand } from "../../state/use-atom-command"; const THEME_OPTIONS = [ { value: "system", label: "System", }, { value: "light", label: "Light", }, { value: "dark", label: "Dark", }, ] as const; const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", "24-hour": "24-hour", } as const; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); function withoutProviderInstanceKey<V>( record: Readonly<Record<ProviderInstanceId, V>> | undefined, key: ProviderInstanceId, ): Record<ProviderInstanceId, V> { const next = { ...record } as Record<ProviderInstanceId, V>; delete next[key]; return next; } function withoutProviderInstanceFavorites( favorites: ReadonlyArray<{ readonly provider: ProviderInstanceId; readonly model: string }>, instanceId: ProviderInstanceId, ) { return favorites.filter((favorite) => favorite.provider !== instanceId); } const PROVIDER_SETTINGS = DRIVER_OPTIONS.map((definition) => ({ provider: definition.value, })); function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }) { useRelativeTimeTick(); const lastCheckedRelative = lastCheckedAt ? formatRelativeTime(lastCheckedAt) : null; if (!lastCheckedRelative) { return null; } return ( <span className="text-[11px] text-muted-foreground/60"> {lastCheckedRelative.suffix ? ( <> Checked <span className="font-mono tabular-nums">{lastCheckedRelative.value}</span>{" "} {lastCheckedRelative.suffix} </> ) : ( <>Checked {lastCheckedRelative.value}</> )} </span> ); } function AboutVersionTitle() { return ( <span className="inline-flex items-center 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 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(() => { 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") { const confirmed = window.confirm( getDesktopUpdateInstallConfirmationMessage( updateState ?? { availableVersion: null, downloadedVersion: null }, ), ); if (!confirmed) 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.", }), ); }); 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.", }), ); }); }, [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="xs" variant={action === "install" ? "default" : "outline"} disabled={buttonDisabled} onClick={handleButtonClick} > {buttonLabel} </Button> } /> {buttonTooltip ? <TooltipPopup>{buttonTooltip}</TooltipPopup> : null} </Tooltip> } /> {hasDesktopBridge ? ( <SettingsRow title="Update track" description="Stable follows full releases. Nightly follows the nightly desktop channel and can switch back to stable immediately." control={ <Select value={selectedUpdateChannel} onValueChange={(value) => { handleUpdateChannelChange(value as DesktopUpdateChannel); }} > <SelectTrigger 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 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 } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const isGitWritingModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const changedSettingLabels = useMemo( () => [ ...(theme !== "system" ? ["Theme"] : []), ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount ? ["Visible threads"] : []), ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? ["Diff line wrapping"] : []), ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace ? ["Diff whitespace changes"] : []), ...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar ? ["Auto-open task panel"] : []), ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming ? ["Assistant output"] : []), ...(Duration.toMillis(settings.automaticGitFetchInterval) !== Duration.toMillis(DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval) ? ["Automatic Git fetch interval"] : []), ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode ? ["New thread mode"] : []), ...(settings.newWorktreesStartFromOrigin !== DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin ? ["New worktrees start from origin"] : []), ...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory ? ["Add project base directory"] : []), ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive ? ["Archive confirmation"] : []), ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete ? ["Delete confirmation"] : []), ...(isGitWritingModelDirty ? ["Git writing model"] : []), ], [ isGitWritingModelDirty, settings.autoOpenPlanSidebar, settings.confirmThreadArchive, settings.confirmThreadDelete, settings.addProjectBaseDirectory, settings.defaultThreadEnvMode, settings.newWorktreesStartFromOrigin, settings.diffIgnoreWhitespace, settings.diffWordWrap, settings.automaticGitFetchInterval, settings.enableAssistantStreaming, settings.sidebarThreadPreviewCount, settings.timestampFormat, theme, ], ); const restoreDefaults = useCallback(async () => { if (changedSettingLabels.length === 0) return; const api = readLocalApi(); const confirmed = await (api ?? ensureLocalApi()).dialogs.confirm( ["Restore default settings?", `This will reset: ${changedSettingLabels.join(", ")}.`].join( "\n", ), ); if (!confirmed) return; setTheme("system"); updateSettings({ timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap, diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar, enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode, newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin, addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory, confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete, textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, }); onRestored?.(); }, [changedSettingLabels, onRestored, setTheme, updateSettings]); return { changedSettingLabels, restoreDefaults, }; } export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const observability = useAtomValue(primaryServerObservabilityAtom); const serverProviders = useAtomValue(primaryServerProvidersAtom); const diagnosticsDescription = formatDiagnosticsDescription({ localTracingEnabled: observability?.localTracingEnabled ?? false, otlpTracesEnabled: observability?.otlpTracesEnabled ?? false, otlpTracesUrl: observability?.otlpTracesUrl, otlpMetricsEnabled: observability?.otlpMetricsEnabled ?? false, otlpMetricsUrl: observability?.otlpMetricsUrl, }); const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders); const textGenInstanceId = textGenerationModelSelection.instanceId; const textGenModel = textGenerationModelSelection.model; const textGenModelOptions = textGenerationModelSelection.options; const gitModelInstanceEntries = sortProviderInstanceEntries( applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), settings),