import { useAtomValue } from "@effect/atom-react"; import { NotificationSettings } from "./NotificationSettings"; import { PRIVACY_POLICY_URL } from "../../legalLinks"; import { CheckIcon } from "lucide-react"; import { Link, useNavigate } from "@tanstack/react-router"; import { useRef, useState } from "react"; import { type DesktopUpdateChannel, IDLE_COMPACTION_MODES, globalIdleCompaction, ProviderDriverKind, type ProviderInstanceId, type SidebarProjectGroupingMode, } from "@t3tools/contracts"; import { DEFAULT_UNIFIED_SETTINGS, type DiffLayout, MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, type ResponseStreamingMode, type QuitConfirmationMode, SidebarProjectSortOrder, } from "@t3tools/contracts/settings"; import { desktopUpdateChannelOf } from "@t3tools/shared/cliRelease"; import { createModelSelection } from "@t3tools/shared/model"; 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 { CliCommandSettingsRow } from "./CliCommandSettingsRow"; import { MobileAppSettingsRow } from "./MobileAppSettingsRow"; import { canCheckForUpdate, getDesktopUpdateButtonTooltip, getDesktopUpdateDisabledReason, 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 { readAppearanceModePreference, readThemeHalves, readThemePreference, useTheme, } from "../../hooks/useTheme"; import { useScopedSettings, useScopedSettingsMixed, useUpdateScopedSettings, } from "./useScopedSettings"; import { useScopedModelDisabledReason } from "./useScopedModelAvailability"; import { useOptionalSettingsScope, useSettingsScope } from "./SettingsScopeContext"; import { scopedIdleCompaction } from "./scopedSettings"; import { ProjectDefaultsSettings } from "./ProjectDefaultsSettings"; import { ThreadTitleInstructionsSettings } from "./ThreadTitleInstructionsSettings"; import { SnoozePresetsRow } from "./SnoozePresetsSettings"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { getCustomModelOptionsByInstance, resolveAppModelSelectionState, selectsPlanAgent, } from "../../modelSelection"; import { providerInstanceEntriesFor } from "../../providerInstances"; import { ensureLocalApi, readLocalApi } from "../../localApi"; import { isMacPlatform } from "../../lib/utils"; import { useEnvironmentProviders, useEnvironments } from "../../state/environments"; import { EMPTY_SERVER_PROVIDERS } from "../../state/server"; import { Button } from "../ui/button"; import { DraftInput } from "../ui/draft-input"; import { Input } from "../ui/input"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { Switch } from "../ui/switch"; import { ScopedSwitch } from "./ScopedSwitch"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { getChangedBrowserSettingLabels, getChangedTypographySettingLabels, hasChangedBackgroundActivitySettings, isProjectGroupingEnabled, projectGroupingModeFromToggle, readLastEnabledProjectGroupingMode, rememberEnabledProjectGroupingMode, } from "./SettingsPanels.logic"; import { SETTINGS_PICKER_TRIGGER_CLASSNAME, SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, } from "./settingsLayout"; import { searchableSetting } from "./settingsSearch"; import { BackgroundActivitySettingsRow } from "./BackgroundActivitySettings"; import { AdvancedSettings } from "./AdvancedSettings"; import { PullRequestLinksOpenInAppSetting } from "./IntegrationsSettings"; import { StorageCleanupSections } from "./StorageSettings"; import { UpdateTrackSwitchDialog } from "./UpdateTrackSwitchDialog"; import { environmentIdsKey } from "~/state/environmentAtomList"; import { environmentSessionStatesAtom } from "~/state/session"; import { canUpdateServer } from "../ServerUpdateAction"; import { useUpdateEverythingRun } from "../sidebar/updateEverythingRun"; import { awaitsTrackSwitch, channelSwitchLabel } from "../sidebar/updateEverything.logic"; import { isHostedByThisApp } from "../sidebar/useUpdateEverything"; const RESPONSE_STREAMING_MODE_LABELS: Record = { turn: "Wait for the full response", paragraph: "Show finished paragraphs", }; const RESPONSE_STREAMING_MODE_DESCRIPTIONS: Record = { turn: "Text appears once the agent finishes its turn.", paragraph: "Each paragraph or code block appears as soon as it is complete.", }; const SIDEBAR_PROJECT_SORT_ORDER_LABELS: Record = { updated_at: "Last user message", created_at: "Created at", manual: "Manual", }; const isSidebarProjectSortOrder = Schema.is(SidebarProjectSortOrder); const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", "24-hour": "24-hour", } as const; const DIFF_LAYOUT_LABELS: Record = { stacked: "Stacked", split: "Split", }; const QUIT_CONFIRMATION_MODE_LABELS: Record = { direct: "Direct", hold: "Hold", "double-click": "Double press", }; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); function AboutVersionTitle() { return ( Version {APP_VERSION} ); } function AboutVersionSection() { const updateState = useDesktopUpdateState(); const [switchChannel, setSwitchChannel] = useState(null); const [isUpdateActionPending, setIsUpdateActionPending] = useState(false); const { environments } = useEnvironments(); const sessionStates = useAtomValue( environmentSessionStatesAtom( environmentIdsKey(environments.map(({ environmentId }) => environmentId)), ), ); // A second run would replace the first run's progress and local download. const updateRun = useUpdateEverythingRun(); const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge); const selectedUpdateChannel = updateState?.channel ?? "latest"; const selectedHostedAppChannel = hasDesktopBridge ? null : HOSTED_APP_CHANNEL; // A switch that failed part way leaves this app or a server on the other track. // With its updater off, a switch cannot move this app, so it is not counted. const appOffTrack = updateState?.enabled !== false && desktopUpdateChannelOf(APP_VERSION) !== selectedUpdateChannel; const unswitchedMachines = (appOffTrack ? 1 : 0) + environments.filter( (environment, index) => environment.connection.phase === "connected" && canUpdateServer(sessionStates[index]!) && awaitsTrackSwitch( { serverConfig: environment.serverConfig, hostedByThisApp: isHostedByThisApp(environment), }, selectedUpdateChannel, ), ).length; const handleButtonClick = 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.", }), ); }); }; const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none"; const buttonTooltip = updateState ? getDesktopUpdateButtonTooltip(updateState) : null; const buttonDisabled = action === "none" ? !canCheckForUpdate(updateState) : isDesktopUpdateButtonDisabled(updateState); const actionLabel: Record = { download: "Download", install: "Install" }; const statusLabel: Record = { checking: "Checking…", downloading: "Downloading…", // Off its track, "up to date" only means no switch is pending; Review moves it. ...(appOffTrack ? {} : { "up-to-date": "Up to Date" }), }; const buttonLabel = actionLabel[action] ?? statusLabel[updateState?.status ?? ""] ?? "Check for Updates"; const description = action === "download" || action === "install" ? "Update available." : (getDesktopUpdateDisabledReason(updateState) ?? "Current version of the application."); return ( <> } description={description} control={ {buttonLabel} } /> {buttonTooltip ? {buttonTooltip} : null} } /> {hasDesktopBridge ? ( 0 ? ( <> {unswitchedMachines === 1 ? "1 machine still runs" : `${unswitchedMachines} machines still run`}{" "} {channelSwitchLabel(selectedUpdateChannel === "nightly" ? "latest" : "nightly")}.{" "} {/* The dialog moves what it can and says why it skips the rest. */} ) : ( "Switches this app and every connected machine whose database the target release supports." ) } control={ } /> ) : selectedHostedAppChannel ? ( { if (value === selectedHostedAppChannel) return; window.location.assign( buildHostedChannelSelectionUrl({ channel: value as HostedAppChannel }), ); }} > {HOSTED_APP_CHANNEL_LABEL} Latest Nightly } /> ) : null} {hasDesktopBridge ? : null} {switchChannel === null ? null : ( setSwitchChannel(null)} /> )} ); } export function useSettingsRestore(onRestored?: () => void) { const { theme, setTheme, followSystem, setFollowSystem, setThemeHalf, clearThemeHalves, themeHalves, } = useTheme(); const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); const isTextGenerationModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const isBackgroundActivityDirty = hasChangedBackgroundActivitySettings(settings); const changedSettingLabels = [ ...(theme !== "system" ? ["Theme"] : []), ...(!followSystem ? ["Follow system"] : []), ...(themeHalves !== null ? ["Theme mix"] : []), ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast ? ["Contrast"] : []), ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), ...(settings.diffColorScheme !== DEFAULT_UNIFIED_SETTINGS.diffColorScheme ? ["Diff colors"] : []), ...(settings.chatWidth !== DEFAULT_UNIFIED_SETTINGS.chatWidth ? ["Chat width"] : []), ...(settings.effortDisplay !== DEFAULT_UNIFIED_SETTINGS.effortDisplay ? ["Effort display"] : []), ...(settings.mathRenderingEnabled !== DEFAULT_UNIFIED_SETTINGS.mathRenderingEnabled ? ["Render math"] : []), ...(settings.panelAnimationDurationMs !== DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs ? ["Panel animations"] : []), ...(settings.environmentIdentificationMode !== DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode ? ["Environment identification"] : []), ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), ...(settings.notificationMode !== DEFAULT_UNIFIED_SETTINGS.notificationMode ? ["Thread notifications"] : []), ...(settings.inAppNotificationsEnabled !== DEFAULT_UNIFIED_SETTINGS.inAppNotificationsEnabled ? ["In-app notifications"] : []), ...(settings.sidebarProjectGroupingMode !== DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode ? ["Project Grouping"] : []), ...(settings.sidebarProjectSortOrder !== DEFAULT_UNIFIED_SETTINGS.sidebarProjectSortOrder ? ["Project order"] : []), ...(settings.sidebarWorkingShelfEnabled !== DEFAULT_UNIFIED_SETTINGS.sidebarWorkingShelfEnabled ? ["Working section"] : []), ...(settings.sidebarAutoSettleAfterDays !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ? ["Auto-settle inactive threads"] : []), ...(settings.sidebarAutoSettleKeepPullRequests !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleKeepPullRequests ? ["Keep threads with a PR"] : []), ...(settings.sidebarAutoSettleOnMerge !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge ? ["Auto-settle merged threads"] : []), ...(settings.autoResumeLimitedThreads !== DEFAULT_UNIFIED_SETTINGS.autoResumeLimitedThreads ? ["Resume after usage limits"] : []), ...(settings.snoozeLimitedThreads !== DEFAULT_UNIFIED_SETTINGS.snoozeLimitedThreads ? ["Snooze until limits reset"] : []), ...(!Equal.equals(settings.snoozePresets, DEFAULT_UNIFIED_SETTINGS.snoozePresets) ? ["Snooze options"] : []), ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []), ...(settings.persistComposerContextStrip !== DEFAULT_UNIFIED_SETTINGS.persistComposerContextStrip ? ["Composer context"] : []), ...getChangedTypographySettingLabels(settings), ...(settings.diffFilesCollapsed !== DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed ? ["Default diff file state"] : []), ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace ? ["Diff whitespace changes"] : []), ...(settings.diffLayout !== DEFAULT_UNIFIED_SETTINGS.diffLayout ? ["Diff layout"] : []), ...(settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled ? ["Proactive panels"] : []), ...(settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu ? ["Show skills in slash menu"] : []), ...(settings.composerRichTextEnabled !== DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled ? ["Rich text composer"] : []), ...(settings.sendShortcut !== DEFAULT_UNIFIED_SETTINGS.sendShortcut ? ["Send shortcut"] : []), ...(settings.followUpBehavior !== DEFAULT_UNIFIED_SETTINGS.followUpBehavior ? ["Follow-up behavior"] : []), ...(settings.contextWindowMeterEnabled !== DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled ? ["Context window indicator"] : []), ...(settings.responseStreamingMode !== DEFAULT_UNIFIED_SETTINGS.responseStreamingMode ? ["Response streaming"] : []), ...(settings.enableProviderUpdateChecks !== DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks ? ["Provider update checks"] : []), ...(settings.continueThreadsAfterServerUpdate !== DEFAULT_UNIFIED_SETTINGS.continueThreadsAfterServerUpdate ? ["Continue threads after restarts"] : []), ...(isBackgroundActivityDirty ? ["Background activity"] : []), ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode ? ["New thread mode"] : []), ...(settings.newWorktreesStartFromOrigin !== DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin ? ["New worktrees start from remote"] : []), ...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory ? ["Add project base directory"] : []), ...(settings.confirmThreadUnpin !== DEFAULT_UNIFIED_SETTINGS.confirmThreadUnpin ? ["Unpin confirmation"] : []), ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive ? ["Archive confirmation"] : []), ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete ? ["Delete confirmation"] : []), ...(settings.confirmQuit !== DEFAULT_UNIFIED_SETTINGS.confirmQuit ? ["Quit shortcut"] : []), ...(isTextGenerationModelDirty ? ["Text generation model"] : []), ...(settings.threadTitleInstructions !== DEFAULT_UNIFIED_SETTINGS.threadTitleInstructions ? ["Thread title instructions"] : []), ...getChangedBrowserSettingLabels(settings), ...(settings.enableAgentBrowserAccess !== DEFAULT_UNIFIED_SETTINGS.enableAgentBrowserAccess ? ["Agent browser access"] : []), ]; const restoreDefaults = 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", ), { variant: "destructive" }, ); if (!confirmed) return; // Reset only dirty theme keys so storage failures cannot block other settings; re-read after confirmation // for safe rollback. let previousTheme = theme; try { previousTheme = readThemePreference(); } catch {} const liveHalves = readThemeHalves(); const needsThemeReset = previousTheme !== "system"; const needsMixReset = liveHalves !== null; const needsFollowSystemReset = readAppearanceModePreference(previousTheme) !== "system"; const notifyThemeRestoreFailure = () => { toastManager.add( stackedThreadToast({ type: "error", title: "Couldn’t restore theme settings", description: "Try again.", }), ); }; // Restore the base preference before the mix because restoring the base clears the mix. const previousHalves = liveHalves; const rollbackThemeState = () => { if (needsThemeReset) setTheme(previousTheme); if (previousHalves?.light) setThemeHalf("light", previousHalves.light); if (previousHalves?.dark) setThemeHalf("dark", previousHalves.dark); }; if (needsThemeReset && !setTheme("system")) { notifyThemeRestoreFailure(); return; } if (needsMixReset && !clearThemeHalves()) { rollbackThemeState(); notifyThemeRestoreFailure(); return; } if (needsFollowSystemReset && !setFollowSystem(true)) { rollbackThemeState(); notifyThemeRestoreFailure(); return; } updateSettings({ appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme, chatWidth: DEFAULT_UNIFIED_SETTINGS.chatWidth, chatCustomWidthRem: DEFAULT_UNIFIED_SETTINGS.chatCustomWidthRem, effortDisplay: DEFAULT_UNIFIED_SETTINGS.effortDisplay, mathRenderingEnabled: DEFAULT_UNIFIED_SETTINGS.mathRenderingEnabled, timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode, inAppNotificationsEnabled: DEFAULT_UNIFIED_SETTINGS.inAppNotificationsEnabled, wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, persistComposerContextStrip: DEFAULT_UNIFIED_SETTINGS.persistComposerContextStrip, diffFilesCollapsed: DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed, diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, diffLayout: DEFAULT_UNIFIED_SETTINGS.diffLayout, proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled, showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu, composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled, sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut, followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior, contextWindowMeterEnabled: DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled, environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, panelAnimationDurationMs: DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs, sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, sidebarProjectSortOrder: DEFAULT_UNIFIED_SETTINGS.sidebarProjectSortOrder, sidebarWorkingShelfEnabled: DEFAULT_UNIFIED_SETTINGS.sidebarWorkingShelfEnabled, sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, sidebarAutoSettleKeepPullRequests: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleKeepPullRequests, sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge, autoResumeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.autoResumeLimitedThreads, snoozeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.snoozeLimitedThreads, snoozePresets: DEFAULT_UNIFIED_SETTINGS.snoozePresets, responseStreamingMode: DEFAULT_UNIFIED_SETTINGS.responseStreamingMode, enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, continueThreadsAfterServerUpdate: DEFAULT_UNIFIED_SETTINGS.continueThreadsAfterServerUpdate, backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity, backgroundActivityProfile: DEFAULT_UNIFIED_SETTINGS.backgroundActivityProfile, automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, providerHealthRefreshInterval: DEFAULT_UNIFIED_SETTINGS.providerHealthRefreshInterval, 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, confirmThreadUnpin: DEFAULT_UNIFIED_SETTINGS.confirmThreadUnpin, confirmQuit: DEFAULT_UNIFIED_SETTINGS.confirmQuit, textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, threadTitleInstructions: DEFAULT_UNIFIED_SETTINGS.threadTitleInstructions, fontFamilySans: DEFAULT_UNIFIED_SETTINGS.fontFamilySans, fontFamilyComposer: DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer, fontFamilyCode: DEFAULT_UNIFIED_SETTINGS.fontFamilyCode, fontFamilyTerminal: DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal, fontSizeInterface: DEFAULT_UNIFIED_SETTINGS.fontSizeInterface, fontSizePrompt: DEFAULT_UNIFIED_SETTINGS.fontSizePrompt, fontSizeCode: DEFAULT_UNIFIED_SETTINGS.fontSizeCode, fontSizeTerminal: DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal, browserDefaultViewport: DEFAULT_UNIFIED_SETTINGS.browserDefaultViewport, browserDefaultZoomFactor: DEFAULT_UNIFIED_SETTINGS.browserDefaultZoomFactor, browserDefaultAppearance: DEFAULT_UNIFIED_SETTINGS.browserDefaultAppearance, browserRecordingFrameRate: DEFAULT_UNIFIED_SETTINGS.browserRecordingFrameRate, browserRecordingShowKeyPresses: DEFAULT_UNIFIED_SETTINGS.browserRecordingShowKeyPresses, browserRecordingShowMousePresses: DEFAULT_UNIFIED_SETTINGS.browserRecordingShowMousePresses, browserLinkTarget: DEFAULT_UNIFIED_SETTINGS.browserLinkTarget, browserSearchEngine: DEFAULT_UNIFIED_SETTINGS.browserSearchEngine, browserCustomSearchUrl: DEFAULT_UNIFIED_SETTINGS.browserCustomSearchUrl, browserAutoShowFloatingPreview: DEFAULT_UNIFIED_SETTINGS.browserAutoShowFloatingPreview, pullRequestLinksOpenInApp: DEFAULT_UNIFIED_SETTINGS.pullRequestLinksOpenInApp, enableAgentBrowserAccess: DEFAULT_UNIFIED_SETTINGS.enableAgentBrowserAccess, }); onRestored?.(); }; return { changedSettingLabels, restoreDefaults, }; } const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ?? 3; function AutoSettleDaysInput({ value, onCommit, }: { value: number; onCommit: (days: number) => void; }) { const [draft, setDraft] = useState(String(value)); const [draftSource, setDraftSource] = useState(value); if (draftSource !== value) { setDraftSource(value); setDraft(String(value)); } return ( { setDraft(event.target.value); const parsed = Number(event.target.value); if ( Number.isInteger(parsed) && parsed >= MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS && parsed <= MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS ) { onCommit(parsed); } }} onBlur={() => setDraft(String(value))} aria-label="Days of inactivity before auto-settle" /> ); } export function LegacyFeaturesSection() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); return ( { updateSettings({ planModeEnabled: Boolean(checked) }); }} aria-label="Plan mode (legacy)" /> } /> updateSettings({ contextWindowMeterEnabled: Boolean(checked) }) } aria-label="Context window indicator (legacy)" /> } /> ); } export function GeneralSettingsPanel() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); const lastEnabledProjectGroupingMode = useRef( readLastEnabledProjectGroupingMode(), ); return ( updateSettings({ inAppNotificationsEnabled: checked })} aria-label="In-app notifications" /> } /> updateSettings({ sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, }) } /> ) : null } control={ { if (!checked && settings.sidebarProjectGroupingMode !== "separate") { lastEnabledProjectGroupingMode.current = settings.sidebarProjectGroupingMode; rememberEnabledProjectGroupingMode(settings.sidebarProjectGroupingMode); } updateSettings({ sidebarProjectGroupingMode: projectGroupingModeFromToggle( checked, lastEnabledProjectGroupingMode.current, ), }); }} aria-label="Project grouping" /> } /> updateSettings({ sidebarProjectSortOrder: DEFAULT_UNIFIED_SETTINGS.sidebarProjectSortOrder, }) } /> ) : null } control={ } /> updateSettings({ sidebarWorkingShelfEnabled: DEFAULT_UNIFIED_SETTINGS.sidebarWorkingShelfEnabled, }) } /> ) : null } control={ updateSettings({ sidebarWorkingShelfEnabled: Boolean(checked) }) } aria-label="Working section" /> } /> updateSettings({ confirmThreadUnpin: DEFAULT_UNIFIED_SETTINGS.confirmThreadUnpin, }) } /> ) : null } control={ updateSettings({ confirmThreadUnpin: Boolean(checked) }) } aria-label="Confirm thread unpinning" /> } /> updateSettings({ confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, }) } /> ) : null } control={ updateSettings({ confirmThreadArchive: Boolean(checked) }) } aria-label="Confirm thread archiving" /> } /> updateSettings({ confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete, }) } /> ) : null } control={ updateSettings({ confirmThreadDelete: Boolean(checked) }) } aria-label="Confirm thread deletion" /> } /> {isElectron ? ( updateSettings({ confirmQuit: DEFAULT_UNIFIED_SETTINGS.confirmQuit }) } /> ) : null } control={ } /> ) : null} updateSettings({ timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, }) } /> ) : null } control={ } /> ); } export function ChatSettingsPanel() { const modifierLabel = isMacPlatform(navigator.platform) ? "⌘" : "Ctrl"; const sendShortcutOptions = [ { value: "enter", label: "Enter" }, { value: "mod-enter-multiline", label: `${modifierLabel} + Enter for multiline prompts` }, { value: "mod-enter", label: `${modifierLabel} + Enter always` }, ] as const; const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); return ( updateSettings({ composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled, }) } /> ) : null } control={ updateSettings({ composerRichTextEnabled: Boolean(checked) }) } aria-label="Rich text composer" /> } /> updateSettings({ sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut }) } /> ) : null } control={ } /> updateSettings({ showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu, }) } /> ) : null } control={ updateSettings({ showSkillsInSlashMenu: Boolean(checked) }) } aria-label="Show skills in slash menu" /> } /> updateSettings({ followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior, }) } /> ) : null } control={ } /> ); } function DiffColorsSettingsRow() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); return ( updateSettings({ diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme }) } /> ) : null } control={
} /> ); } export function DiffsSettingsPanel() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); return ( updateSettings({ diffLayout: DEFAULT_UNIFIED_SETTINGS.diffLayout })} /> ) : null } control={ } /> updateSettings({ diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, }) } /> ) : null } control={ updateSettings({ diffIgnoreWhitespace: Boolean(checked) }) } aria-label="Hide whitespace changes by default" /> } /> updateSettings({ diffFilesCollapsed: DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed, }) } /> ) : null } control={ } /> updateSettings({ proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled, }) } /> ) : null } control={ updateSettings({ proactivePanelsEnabled: Boolean(checked) }) } aria-label="Proactive panels" /> } /> ); } /** The Providers panel can render without a settings scope, as its tests do; this row needs one. */ export function CompactIdleThreadsRow() { return useOptionalSettingsScope() === null ? null : ; } function ScopedCompactIdleThreadsRow() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); const { targets } = useSettingsScope(); const idleCompaction = scopedIdleCompaction(targets.length > 0 ? targets : [{ settings }]); return ( updateSettings({ idleCompaction: globalIdleCompaction(DEFAULT_UNIFIED_SETTINGS) }) } /> ) : null } control={ } /> ); } export function ThreadsSettingsPanel() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); const navigate = useNavigate(); const { environment, connectedEnvironments, target, scope } = useSettingsScope(); const isProjectScope = scope.kind === "project" || scope.kind === "checkout"; const environmentId = environment?.environmentId ?? null; const hasServerTargets = connectedEnvironments.length > 0; const mixedResponseStreamingMode = useScopedSettingsMixed(["responseStreamingMode"]); const providersByEnvironment = useEnvironmentProviders(); const serverProviders = (environment?.serverConfig ? providersByEnvironment.get(environment.environmentId) : undefined) ?? EMPTY_SERVER_PROVIDERS; const supportsAutoSettlementScope = connectedEnvironments.every( (target) => target.serverConfig?.environment.capabilities.threadAutoSettlementScope === true, ); const textGenerationProviders = serverProviders.filter( (provider) => provider.supportsTextGeneration !== false, ); const textGenerationModelSelection = resolveAppModelSelectionState( settings, textGenerationProviders, ); const textGenInstanceId = textGenerationModelSelection.instanceId; const textGenModel = textGenerationModelSelection.model; const textGenModelOptions = textGenerationModelSelection.options; const textGenerationModelInstanceEntries = providerInstanceEntriesFor( textGenerationProviders, settings, target?.projectId ?? null, ); const hasTextGenerationProvider = textGenerationModelInstanceEntries.some( (entry) => entry.enabled && entry.isAvailable, ); const textGenInstanceEntry = textGenerationModelInstanceEntries.find( (entry) => entry.instanceId === textGenInstanceId, ); const textGenProvider: ProviderDriverKind = textGenInstanceEntry?.driverKind ?? DEFAULT_DRIVER_KIND; const textGenerationModelOptionsByInstance = getCustomModelOptionsByInstance( settings, textGenerationProviders, textGenInstanceId, textGenModel, ); const isTextGenerationModelDirty = !Equal.equals( settings.textGenerationModelSelection ?? null, DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, ); const textGenerationModelDisabledReason = useScopedModelDisabledReason( settings, textGenerationModelInstanceEntries, ); const mixedAddProjectBaseDirectory = useScopedSettingsMixed(["addProjectBaseDirectory"]); const mixedTextGenerationModel = useScopedSettingsMixed(["textGenerationModelSelection"]); return ( updateSettings({ addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory, }) } /> ) : null } control={ updateSettings({ addProjectBaseDirectory: next })} placeholder={mixedAddProjectBaseDirectory ? "Mixed" : "~/"} spellCheck={false} aria-label="Add project base directory" /> } /> {!isProjectScope ? ( ) : null} updateSettings({ autoResumeLimitedThreads: Boolean(checked) }) } aria-label="Resume after usage limits" /> } /> updateSettings({ snoozeLimitedThreads: Boolean(checked) }) } aria-label="Snooze until limits reset" /> } /> {hasServerTargets ? ( updateSettings({ sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge, }) } /> ) : null } control={ updateSettings({ sidebarAutoSettleOnMerge: Boolean(checked) }) } aria-label="Auto-settle merged threads" /> } /> updateSettings({ sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, }) } /> ) : null } control={ updateSettings({ sidebarAutoSettleAfterDays: checked ? AUTO_SETTLE_DEFAULT_DAYS : null, }) } aria-label="Auto-settle inactive threads" /> } /> {settings.sidebarAutoSettleAfterDays !== null ? ( updateSettings({ sidebarAutoSettleAfterDays: days })} /> } /> ) : null} {settings.sidebarAutoSettleAfterDays !== null && supportsAutoSettlementScope ? ( updateSettings({ sidebarAutoSettleKeepPullRequests: checked }) } aria-label="Keep threads with a PR" /> } /> ) : null} ) : null} updateSettings({ textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, }) } /> ) : null } control={ !hasServerTargets ? ( Connect an environment to choose its text generation model. ) : !hasTextGenerationProvider ? ( No text generation providers available. ) : (
{ void navigate({ to: "/settings/providers", search: { environmentId, instanceId }, }); }, } : {})} onInstanceModelChange={(instanceId, model) => { const reason = textGenerationModelDisabledReason(instanceId, model); if (reason) { toastManager.add({ type: "error", title: "Text generation model not saved", description: reason, }); return; } updateSettings({ textGenerationModelSelection: resolveAppModelSelectionState( { ...settings, textGenerationModelSelection: createModelSelection(instanceId, model), }, textGenerationProviders, ), }); }} /> {textGenInstanceEntry ? ( { updateSettings({ textGenerationModelSelection: resolveAppModelSelectionState( { ...settings, textGenerationModelSelection: createModelSelection( textGenInstanceId, textGenModel, nextOptions, ), }, textGenerationProviders, ), }); }} /> ) : null}
) } /> updateSettings({ responseStreamingMode: DEFAULT_UNIFIED_SETTINGS.responseStreamingMode, }) } /> ) : null } control={ } />
); } export function MaintenanceSettingsPanel() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); const { scope, environment, connectedEnvironments } = useSettingsScope(); const environmentId = environment?.environmentId ?? null; const isEnvironmentScope = scope.environmentIds.length === 1 && environmentId !== null; const hasServerTargets = connectedEnvironments.length > 0; return ( updateSettings({ enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, }) } /> ) : null } control={ updateSettings({ enableProviderUpdateChecks: Boolean(checked) }) } aria-label="Check provider versions" /> } /> updateSettings({ continueThreadsAfterServerUpdate: DEFAULT_UNIFIED_SETTINGS.continueThreadsAfterServerUpdate, }) } /> ) : null } control={ updateSettings({ continueThreadsAfterServerUpdate: Boolean(checked) }) } aria-label="Continue threads after restarts" /> } /> } size="sm" variant="outline" > View diagnostics } /> ); } export function AboutSettingsPanel() { return ( {isElectron || HOSTED_APP_CHANNEL ? ( ) : ( <> } description="Current version of the application." /> )} } size="sm" variant="outline" > View policy } /> } size="sm" variant="outline" > View licenses } /> ); }