import { ArchiveIcon, ArchiveX, ChevronDownIcon, InfoIcon, LoaderIcon, PlusIcon, RefreshCwIcon, XIcon,} from "lucide-react";import { useQueryClient } from "@tanstack/react-query";import { type ReactNode, useCallback, useMemo, useRef, useState } from "react";import { PROVIDER_DISPLAY_NAMES, type ScopedThreadRef, type ProviderKind, type ServerProvider, type ServerProviderModel,} from "@t3tools/contracts";import { scopeThreadRef } from "@t3tools/client-runtime";import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";import { normalizeModelSlug } from "@t3tools/shared/model";import { Equal } from "effect";import { APP_VERSION } from "../../branding";import { canCheckForUpdate, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction,} from "../../components/desktopUpdate.logic";import { ProviderModelPicker } from "../chat/ProviderModelPicker";import { TraitsPicker } from "../chat/TraitsPicker";import { resolveAndPersistPreferredEditor } from "../../editorPreferences";import { isElectron } from "../../env";import { useTheme } from "../../hooks/useTheme";import { useSettings, useUpdateSettings } from "../../hooks/useSettings";import { useThreadActions } from "../../hooks/useThreadActions";import { setDesktopUpdateStateQueryData, useDesktopUpdateState,} from "../../lib/desktopUpdateReactQuery";import { MAX_CUSTOM_MODEL_LENGTH, getCustomModelOptionsByProvider, resolveAppModelSelectionState,} from "../../modelSelection";import { ensureLocalApi, readLocalApi } from "../../localApi";import { useShallow } from "zustand/react/shallow";import { selectProjectsAcrossEnvironments, selectThreadShellsAcrossEnvironments, useStore,} from "../../store";import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat";import { cn } from "../../lib/utils";import { Button } from "../ui/button";import { Collapsible, CollapsibleContent } from "../ui/collapsible";import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";import { Input } from "../ui/input";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { Switch } from "../ui/switch";import { toastManager } from "../ui/toast";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import { SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, useRelativeTimeTick,} from "./settingsLayout";import { ProjectFavicon } from "../ProjectFavicon";import { useServerAvailableEditors, useServerKeybindingsConfigPath, useServerObservability, useServerProviders,} from "../../rpc/serverState"; 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; type InstallProviderSettings = { provider: ProviderKind; title: string; binaryPlaceholder: string; binaryDescription: ReactNode; homePathKey?: "codexHomePath"; homePlaceholder?: string; homeDescription?: ReactNode;}; const PROVIDER_SETTINGS: readonly InstallProviderSettings[] = [ { provider: "codex", title: "Codex", binaryPlaceholder: "Codex binary path", binaryDescription: "Path to the Codex binary", homePathKey: "codexHomePath", homePlaceholder: "CODEX_HOME", homeDescription: "Optional custom Codex home and config directory.", }, { provider: "claudeAgent", title: "Claude", binaryPlaceholder: "Claude binary path", binaryDescription: "Path to the Claude binary", },] as const; const PROVIDER_STATUS_STYLES = { disabled: { dot: "bg-amber-400", }, error: { dot: "bg-destructive", }, ready: { dot: "bg-success", }, warning: { dot: "bg-warning", },} as const; function getProviderSummary(provider: ServerProvider | undefined) { if (!provider) { return { headline: "Checking provider status", detail: "Waiting for the server to report installation and authentication details.", }; } if (!provider.enabled) { return { headline: "Disabled", detail: provider.message ?? "This provider is installed but disabled for new sessions in T3 Code.", }; } if (!provider.installed) { return { headline: "Not found", detail: provider.message ?? "CLI not detected on PATH.", }; } if (provider.auth.status === "authenticated") { const authLabel = provider.auth.label ?? provider.auth.type; return { headline: authLabel ? `Authenticated · ${authLabel}` : "Authenticated", detail: provider.message ?? null, }; } if (provider.auth.status === "unauthenticated") { return { headline: "Not authenticated", detail: provider.message ?? null, }; } if (provider.status === "warning") { return { headline: "Needs attention", detail: provider.message ?? "The provider is installed, but the server could not fully verify it.", }; } if (provider.status === "error") { return { headline: "Unavailable", detail: provider.message ?? "The provider failed its startup checks.", }; } return { headline: "Available", detail: provider.message ?? "Installed and ready, but authentication could not be verified.", };} function getProviderVersionLabel(version: string | null | undefined) { if (!version) return null; return version.startsWith("v") ? version : `v${version}`;} 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 queryClient = useQueryClient(); const updateStateQuery = useDesktopUpdateState(); const updateState = updateStateQuery.data ?? null; const handleButtonClick = useCallback(() => { const bridge = window.desktopBridge; if (!bridge) return; const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; if (action === "download") { void bridge .downloadUpdate() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); }) .catch((error: unknown) => { toastManager.add({ 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() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); }) .catch((error: unknown) => { toastManager.add({ 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) => { setDesktopUpdateStateQueryData(queryClient, result.state); if (!result.checked) { toastManager.add({ 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({ type: "error", title: "Could not check for updates", description: error instanceof Error ? error.message : "Update check failed.", }); }); }, [queryClient, 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> } /> );} export function useSettingsRestore(onRestored?: () => void) { const { theme, setTheme } = useTheme(); const settings = useSettings(); const { resetSettings } = useUpdateSettings(); const isGitWritingModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const areProviderSettingsDirty = PROVIDER_SETTINGS.some((providerSettings) => { const currentSettings = settings.providers[providerSettings.provider]; const defaultSettings = DEFAULT_UNIFIED_SETTINGS.providers[providerSettings.provider]; return !Equal.equals(currentSettings, defaultSettings); }); const changedSettingLabels = useMemo( () => [ ...(theme !== "system" ? ["Theme"] : []), ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? ["Diff line wrapping"] : []), ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming ? ["Assistant output"] : []), ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode ? ["New thread mode"] : []), ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive ? ["Archive confirmation"] : []), ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete ? ["Delete confirmation"] : []), ...(isGitWritingModelDirty ? ["Git writing model"] : []), ...(areProviderSettingsDirty ? ["Providers"] : []), ], [ areProviderSettingsDirty, isGitWritingModelDirty, settings.confirmThreadArchive, settings.confirmThreadDelete, settings.defaultThreadEnvMode, settings.diffWordWrap, settings.enableAssistantStreaming, 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"); resetSettings(); onRestored?.(); }, [changedSettingLabels, onRestored, resetSettings, setTheme]); return { changedSettingLabels, restoreDefaults, };} export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); const settings = useSettings(); const { updateSettings } = useUpdateSettings(); const [openingPathByTarget, setOpeningPathByTarget] = useState({ keybindings: false, logsDirectory: false, }); const [openPathErrorByTarget, setOpenPathErrorByTarget] = useState< Partial<Record<"keybindings" | "logsDirectory", string | null>> >({}); const [openProviderDetails, setOpenProviderDetails] = useState<Record<ProviderKind, boolean>>({ codex: Boolean( settings.providers.codex.binaryPath !== DEFAULT_UNIFIED_SETTINGS.providers.codex.binaryPath || settings.providers.codex.homePath !== DEFAULT_UNIFIED_SETTINGS.providers.codex.homePath || settings.providers.codex.customModels.length > 0, ), claudeAgent: Boolean( settings.providers.claudeAgent.binaryPath !== DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath || settings.providers.claudeAgent.customModels.length > 0, ), }); const [customModelInputByProvider, setCustomModelInputByProvider] = useState< Record<ProviderKind, string> >({ codex: "", claudeAgent: "", }); const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< Partial<Record<ProviderKind, string | null>> >({}); const [isRefreshingProviders, setIsRefreshingProviders] = useState(false); const refreshingRef = useRef(false); const modelListRefs = useRef<Partial<Record<ProviderKind, HTMLDivElement | null>>>({}); const refreshProviders = useCallback(() => { if (refreshingRef.current) return; refreshingRef.current = true; setIsRefreshingProviders(true); void ensureLocalApi() .server.refreshProviders() .catch((error: unknown) => { console.warn("Failed to refresh providers", error); }) .finally(() => { refreshingRef.current = false; setIsRefreshingProviders(false); }); }, []); const keybindingsConfigPath = useServerKeybindingsConfigPath(); const availableEditors = useServerAvailableEditors(); const observability = useServerObservability(); const serverProviders = useServerProviders(); const codexHomePath = settings.providers.codex.homePath; const logsDirectoryPath = observability?.logsDirectoryPath ?? null; const diagnosticsDescription = (() => { const exports: string[] = []; if (observability?.otlpTracesEnabled && observability.otlpTracesUrl) { exports.push(`traces to ${observability.otlpTracesUrl}`); } if (observability?.otlpMetricsEnabled && observability.otlpMetricsUrl) { exports.push(`metrics to ${observability.otlpMetricsUrl}`); } const mode = observability?.localTracingEnabled ? "Local trace file" : "Terminal logs only"; return exports.length > 0 ? `${mode}. OTLP exporting ${exports.join(" and ")}.` : `${mode}.`; })(); const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders); const textGenProvider = textGenerationModelSelection.provider; const textGenModel = textGenerationModelSelection.model; const textGenModelOptions = textGenerationModelSelection.options; const gitModelOptionsByProvider = getCustomModelOptionsByProvider( settings, serverProviders, textGenProvider, textGenModel, );Lines 1–500 of 1510. Load more
import { ArchiveIcon, ArchiveX, ChevronDownIcon, InfoIcon, LoaderIcon, PlusIcon, RefreshCwIcon, XIcon, } from "lucide-react"; import { useQueryClient } from "@tanstack/react-query"; import { type ReactNode, useCallback, useMemo, useRef, useState } from "react"; import { PROVIDER_DISPLAY_NAMES, type ScopedThreadRef, type ProviderKind, type ServerProvider, type ServerProviderModel, } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime"; import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; import { normalizeModelSlug } from "@t3tools/shared/model"; import { Equal } from "effect"; import { APP_VERSION } from "../../branding"; import { canCheckForUpdate, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, } from "../../components/desktopUpdate.logic"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { TraitsPicker } from "../chat/TraitsPicker"; import { resolveAndPersistPreferredEditor } from "../../editorPreferences"; import { isElectron } from "../../env"; import { useTheme } from "../../hooks/useTheme"; import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; import { useThreadActions } from "../../hooks/useThreadActions"; import { setDesktopUpdateStateQueryData, useDesktopUpdateState, } from "../../lib/desktopUpdateReactQuery"; import { MAX_CUSTOM_MODEL_LENGTH, getCustomModelOptionsByProvider, resolveAppModelSelectionState, } from "../../modelSelection"; import { ensureLocalApi, readLocalApi } from "../../localApi"; import { useShallow } from "zustand/react/shallow"; import { selectProjectsAcrossEnvironments, selectThreadShellsAcrossEnvironments, useStore, } from "../../store"; import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat"; import { cn } from "../../lib/utils"; import { Button } from "../ui/button"; import { Collapsible, CollapsibleContent } from "../ui/collapsible"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty"; import { Input } from "../ui/input"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { Switch } from "../ui/switch"; import { toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, useRelativeTimeTick, } from "./settingsLayout"; import { ProjectFavicon } from "../ProjectFavicon"; import { useServerAvailableEditors, useServerKeybindingsConfigPath, useServerObservability, useServerProviders, } from "../../rpc/serverState"; 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; type InstallProviderSettings = { provider: ProviderKind; title: string; binaryPlaceholder: string; binaryDescription: ReactNode; homePathKey?: "codexHomePath"; homePlaceholder?: string; homeDescription?: ReactNode; }; const PROVIDER_SETTINGS: readonly InstallProviderSettings[] = [ { provider: "codex", title: "Codex", binaryPlaceholder: "Codex binary path", binaryDescription: "Path to the Codex binary", homePathKey: "codexHomePath", homePlaceholder: "CODEX_HOME", homeDescription: "Optional custom Codex home and config directory.", }, { provider: "claudeAgent", title: "Claude", binaryPlaceholder: "Claude binary path", binaryDescription: "Path to the Claude binary", }, ] as const; const PROVIDER_STATUS_STYLES = { disabled: { dot: "bg-amber-400", }, error: { dot: "bg-destructive", }, ready: { dot: "bg-success", }, warning: { dot: "bg-warning", }, } as const; function getProviderSummary(provider: ServerProvider | undefined) { if (!provider) { return { headline: "Checking provider status", detail: "Waiting for the server to report installation and authentication details.", }; } if (!provider.enabled) { return { headline: "Disabled", detail: provider.message ?? "This provider is installed but disabled for new sessions in T3 Code.", }; } if (!provider.installed) { return { headline: "Not found", detail: provider.message ?? "CLI not detected on PATH.", }; } if (provider.auth.status === "authenticated") { const authLabel = provider.auth.label ?? provider.auth.type; return { headline: authLabel ? `Authenticated · ${authLabel}` : "Authenticated", detail: provider.message ?? null, }; } if (provider.auth.status === "unauthenticated") { return { headline: "Not authenticated", detail: provider.message ?? null, }; } if (provider.status === "warning") { return { headline: "Needs attention", detail: provider.message ?? "The provider is installed, but the server could not fully verify it.", }; } if (provider.status === "error") { return { headline: "Unavailable", detail: provider.message ?? "The provider failed its startup checks.", }; } return { headline: "Available", detail: provider.message ?? "Installed and ready, but authentication could not be verified.", }; } function getProviderVersionLabel(version: string | null | undefined) { if (!version) return null; return version.startsWith("v") ? version : `v${version}`; } 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 queryClient = useQueryClient(); const updateStateQuery = useDesktopUpdateState(); const updateState = updateStateQuery.data ?? null; const handleButtonClick = useCallback(() => { const bridge = window.desktopBridge; if (!bridge) return; const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; if (action === "download") { void bridge .downloadUpdate() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); }) .catch((error: unknown) => { toastManager.add({ 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() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); }) .catch((error: unknown) => { toastManager.add({ 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) => { setDesktopUpdateStateQueryData(queryClient, result.state); if (!result.checked) { toastManager.add({ 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({ type: "error", title: "Could not check for updates", description: error instanceof Error ? error.message : "Update check failed.", }); }); }, [queryClient, 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> } /> ); } export function useSettingsRestore(onRestored?: () => void) { const { theme, setTheme } = useTheme(); const settings = useSettings(); const { resetSettings } = useUpdateSettings(); const isGitWritingModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const areProviderSettingsDirty = PROVIDER_SETTINGS.some((providerSettings) => { const currentSettings = settings.providers[providerSettings.provider]; const defaultSettings = DEFAULT_UNIFIED_SETTINGS.providers[providerSettings.provider]; return !Equal.equals(currentSettings, defaultSettings); }); const changedSettingLabels = useMemo( () => [ ...(theme !== "system" ? ["Theme"] : []), ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap ? ["Diff line wrapping"] : []), ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming ? ["Assistant output"] : []), ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode ? ["New thread mode"] : []), ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive ? ["Archive confirmation"] : []), ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete ? ["Delete confirmation"] : []), ...(isGitWritingModelDirty ? ["Git writing model"] : []), ...(areProviderSettingsDirty ? ["Providers"] : []), ], [ areProviderSettingsDirty, isGitWritingModelDirty, settings.confirmThreadArchive, settings.confirmThreadDelete, settings.defaultThreadEnvMode, settings.diffWordWrap, settings.enableAssistantStreaming, 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"); resetSettings(); onRestored?.(); }, [changedSettingLabels, onRestored, resetSettings, setTheme]); return { changedSettingLabels, restoreDefaults, }; } export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); const settings = useSettings(); const { updateSettings } = useUpdateSettings(); const [openingPathByTarget, setOpeningPathByTarget] = useState({ keybindings: false, logsDirectory: false, }); const [openPathErrorByTarget, setOpenPathErrorByTarget] = useState< Partial<Record<"keybindings" | "logsDirectory", string | null>> >({}); const [openProviderDetails, setOpenProviderDetails] = useState<Record<ProviderKind, boolean>>({ codex: Boolean( settings.providers.codex.binaryPath !== DEFAULT_UNIFIED_SETTINGS.providers.codex.binaryPath || settings.providers.codex.homePath !== DEFAULT_UNIFIED_SETTINGS.providers.codex.homePath || settings.providers.codex.customModels.length > 0, ), claudeAgent: Boolean( settings.providers.claudeAgent.binaryPath !== DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath || settings.providers.claudeAgent.customModels.length > 0, ), }); const [customModelInputByProvider, setCustomModelInputByProvider] = useState< Record<ProviderKind, string> >({ codex: "", claudeAgent: "", }); const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< Partial<Record<ProviderKind, string | null>> >({}); const [isRefreshingProviders, setIsRefreshingProviders] = useState(false); const refreshingRef = useRef(false); const modelListRefs = useRef<Partial<Record<ProviderKind, HTMLDivElement | null>>>({}); const refreshProviders = useCallback(() => { if (refreshingRef.current) return; refreshingRef.current = true; setIsRefreshingProviders(true); void ensureLocalApi() .server.refreshProviders() .catch((error: unknown) => { console.warn("Failed to refresh providers", error); }) .finally(() => { refreshingRef.current = false; setIsRefreshingProviders(false); }); }, []); const keybindingsConfigPath = useServerKeybindingsConfigPath(); const availableEditors = useServerAvailableEditors(); const observability = useServerObservability(); const serverProviders = useServerProviders(); const codexHomePath = settings.providers.codex.homePath; const logsDirectoryPath = observability?.logsDirectoryPath ?? null; const diagnosticsDescription = (() => { const exports: string[] = []; if (observability?.otlpTracesEnabled && observability.otlpTracesUrl) { exports.push(`traces to ${observability.otlpTracesUrl}`); } if (observability?.otlpMetricsEnabled && observability.otlpMetricsUrl) { exports.push(`metrics to ${observability.otlpMetricsUrl}`); } const mode = observability?.localTracingEnabled ? "Local trace file" : "Terminal logs only"; return exports.length > 0 ? `${mode}. OTLP exporting ${exports.join(" and ")}.` : `${mode}.`; })(); const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders); const textGenProvider = textGenerationModelSelection.provider; const textGenModel = textGenerationModelSelection.model; const textGenModelOptions = textGenerationModelSelection.options; const gitModelOptionsByProvider = getCustomModelOptionsByProvider( settings, serverProviders, textGenProvider, textGenModel, );