|
| 1 | +import { type ReactNode, createContext, useCallback, useContext, useMemo, useState } from "react"; |
| 2 | + |
| 3 | +import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@okcode/contracts"; |
| 4 | + |
| 5 | +import { DEFAULT_PR_REVIEW_REQUEST_CHANGES_TONE, useAppSettings } from "../../appSettings"; |
| 6 | +import { DEFAULT_COLOR_THEME, useTheme } from "../../hooks/useTheme"; |
| 7 | +import { readNativeApi, ensureNativeApi } from "../../nativeApi"; |
| 8 | +import { |
| 9 | + clearFontOverride, |
| 10 | + clearFontSizeOverride, |
| 11 | + clearRadiusOverride, |
| 12 | + clearStoredCustomTheme, |
| 13 | + getStoredFontOverride, |
| 14 | + getStoredFontSizeOverride, |
| 15 | + getStoredRadiusOverride, |
| 16 | + removeCustomTheme, |
| 17 | + setStoredFontOverride, |
| 18 | + setStoredFontSizeOverride, |
| 19 | + setStoredRadiusOverride, |
| 20 | +} from "../../lib/customTheme"; |
| 21 | + |
| 22 | +type ThemeState = ReturnType<typeof useTheme>; |
| 23 | + |
| 24 | +interface SettingsRouteContextValue { |
| 25 | + theme: ThemeState["theme"]; |
| 26 | + setTheme: ThemeState["setTheme"]; |
| 27 | + colorTheme: ThemeState["colorTheme"]; |
| 28 | + setColorTheme: ThemeState["setColorTheme"]; |
| 29 | + fontFamily: ThemeState["fontFamily"]; |
| 30 | + setFontFamily: ThemeState["setFontFamily"]; |
| 31 | + settingsState: ReturnType<typeof useAppSettings>; |
| 32 | + radiusOverride: number | null; |
| 33 | + setRadiusOverride: (value: number | null) => void; |
| 34 | + fontOverride: string; |
| 35 | + setFontOverride: (value: string) => void; |
| 36 | + fontSizeOverride: number | null; |
| 37 | + setFontSizeOverride: (value: number | null) => void; |
| 38 | + changedSettingLabels: readonly string[]; |
| 39 | + restoreDefaults: () => Promise<void>; |
| 40 | +} |
| 41 | + |
| 42 | +const SettingsRouteContext = createContext<SettingsRouteContextValue | null>(null); |
| 43 | + |
| 44 | +export function SettingsRouteContextProvider({ children }: { children: ReactNode }) { |
| 45 | + const { theme, setTheme, colorTheme, setColorTheme, fontFamily, setFontFamily } = useTheme(); |
| 46 | + const settingsState = useAppSettings(); |
| 47 | + const { settings, defaults, resetSettings } = settingsState; |
| 48 | + const [radiusOverrideState, setRadiusOverrideState] = useState<number | null>(() => |
| 49 | + getStoredRadiusOverride(), |
| 50 | + ); |
| 51 | + const [fontOverrideState, setFontOverrideState] = useState<string>( |
| 52 | + () => getStoredFontOverride() ?? "", |
| 53 | + ); |
| 54 | + const [fontSizeOverrideState, setFontSizeOverrideState] = useState<number | null>(() => |
| 55 | + getStoredFontSizeOverride(), |
| 56 | + ); |
| 57 | + |
| 58 | + const setRadiusOverride = useCallback((value: number | null) => { |
| 59 | + setRadiusOverrideState(value); |
| 60 | + if (value === null) { |
| 61 | + clearRadiusOverride(); |
| 62 | + return; |
| 63 | + } |
| 64 | + setStoredRadiusOverride(value); |
| 65 | + }, []); |
| 66 | + |
| 67 | + const setFontOverride = useCallback((value: string) => { |
| 68 | + setFontOverrideState(value); |
| 69 | + if (value.trim()) { |
| 70 | + setStoredFontOverride(value); |
| 71 | + return; |
| 72 | + } |
| 73 | + clearFontOverride(); |
| 74 | + }, []); |
| 75 | + |
| 76 | + const setFontSizeOverride = useCallback((value: number | null) => { |
| 77 | + setFontSizeOverrideState(value); |
| 78 | + if (value === null) { |
| 79 | + clearFontSizeOverride(); |
| 80 | + return; |
| 81 | + } |
| 82 | + setStoredFontSizeOverride(value); |
| 83 | + }, []); |
| 84 | + |
| 85 | + const currentGitTextGenerationModel = |
| 86 | + settings.textGenerationModel ?? DEFAULT_GIT_TEXT_GENERATION_MODEL; |
| 87 | + const defaultGitTextGenerationModel = |
| 88 | + defaults.textGenerationModel ?? DEFAULT_GIT_TEXT_GENERATION_MODEL; |
| 89 | + const isGitTextGenerationModelDirty = |
| 90 | + currentGitTextGenerationModel !== defaultGitTextGenerationModel; |
| 91 | + const isInstallSettingsDirty = |
| 92 | + settings.claudeBinaryPath !== defaults.claudeBinaryPath || |
| 93 | + settings.codexBinaryPath !== defaults.codexBinaryPath || |
| 94 | + settings.codexHomePath !== defaults.codexHomePath; |
| 95 | + const isOpenClawSettingsDirty = |
| 96 | + settings.openclawGatewayUrl !== defaults.openclawGatewayUrl || |
| 97 | + settings.openclawPassword !== defaults.openclawPassword; |
| 98 | + |
| 99 | + const changedSettingLabels = useMemo( |
| 100 | + () => |
| 101 | + [ |
| 102 | + ...(theme !== "system" ? ["Theme"] : []), |
| 103 | + ...(colorTheme !== DEFAULT_COLOR_THEME ? ["Color theme"] : []), |
| 104 | + ...(fontFamily !== "inter" ? ["Font"] : []), |
| 105 | + ...(settings.prReviewRequestChangesTone !== DEFAULT_PR_REVIEW_REQUEST_CHANGES_TONE |
| 106 | + ? ["PR request changes button"] |
| 107 | + : []), |
| 108 | + ...(settings.timestampFormat !== defaults.timestampFormat ? ["Time format"] : []), |
| 109 | + ...(settings.showStitchBorder !== defaults.showStitchBorder ? ["Stitch border"] : []), |
| 110 | + ...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming |
| 111 | + ? ["Assistant output"] |
| 112 | + : []), |
| 113 | + ...(settings.showReasoningContent !== defaults.showReasoningContent |
| 114 | + ? ["Reasoning content"] |
| 115 | + : []), |
| 116 | + ...(settings.showAuthFailuresAsErrors !== defaults.showAuthFailuresAsErrors |
| 117 | + ? ["Auth failure errors"] |
| 118 | + : []), |
| 119 | + ...(settings.showNotificationDetails !== defaults.showNotificationDetails |
| 120 | + ? ["Notification details"] |
| 121 | + : []), |
| 122 | + ...(settings.includeDiagnosticsTipsInCopy !== defaults.includeDiagnosticsTipsInCopy |
| 123 | + ? ["Diagnostics copy tips"] |
| 124 | + : []), |
| 125 | + ...(settings.openLinksExternally !== defaults.openLinksExternally |
| 126 | + ? ["Open links externally"] |
| 127 | + : []), |
| 128 | + ...(settings.codeViewerAutosave !== defaults.codeViewerAutosave |
| 129 | + ? ["Code preview autosave"] |
| 130 | + : []), |
| 131 | + ...(settings.defaultThreadEnvMode !== defaults.defaultThreadEnvMode |
| 132 | + ? ["New thread mode"] |
| 133 | + : []), |
| 134 | + ...(settings.autoUpdateWorktreeBaseBranch !== defaults.autoUpdateWorktreeBaseBranch |
| 135 | + ? ["Worktree base refresh"] |
| 136 | + : []), |
| 137 | + ...(settings.confirmThreadDelete !== defaults.confirmThreadDelete |
| 138 | + ? ["Delete confirmation"] |
| 139 | + : []), |
| 140 | + ...(settings.autoDeleteMergedThreads !== defaults.autoDeleteMergedThreads |
| 141 | + ? ["Auto-delete merged threads"] |
| 142 | + : []), |
| 143 | + ...(settings.autoDeleteMergedThreadsDelayMinutes !== |
| 144 | + defaults.autoDeleteMergedThreadsDelayMinutes |
| 145 | + ? ["Auto-delete delay"] |
| 146 | + : []), |
| 147 | + ...(settings.rebaseBeforeCommit !== defaults.rebaseBeforeCommit |
| 148 | + ? ["Rebase before commit"] |
| 149 | + : []), |
| 150 | + ...(isGitTextGenerationModelDirty ? ["Git writing model"] : []), |
| 151 | + ...(settings.customCodexModels.length > 0 || |
| 152 | + settings.customClaudeModels.length > 0 || |
| 153 | + settings.customOpenClawModels.length > 0 |
| 154 | + ? ["Custom models"] |
| 155 | + : []), |
| 156 | + ...(isInstallSettingsDirty ? ["Provider installs"] : []), |
| 157 | + ...(isOpenClawSettingsDirty ? ["OpenClaw gateway"] : []), |
| 158 | + ...(settings.backgroundImageUrl !== defaults.backgroundImageUrl |
| 159 | + ? ["Background image"] |
| 160 | + : []), |
| 161 | + ...(settings.backgroundImageOpacity !== defaults.backgroundImageOpacity |
| 162 | + ? ["Background opacity"] |
| 163 | + : []), |
| 164 | + ...(settings.sidebarOpacity !== defaults.sidebarOpacity ? ["Sidebar opacity"] : []), |
| 165 | + ...(settings.sidebarProjectRowHeight !== defaults.sidebarProjectRowHeight |
| 166 | + ? ["Project height"] |
| 167 | + : []), |
| 168 | + ...(settings.sidebarThreadRowHeight !== defaults.sidebarThreadRowHeight |
| 169 | + ? ["Thread height"] |
| 170 | + : []), |
| 171 | + ...(settings.sidebarFontSize !== defaults.sidebarFontSize ? ["Sidebar font size"] : []), |
| 172 | + ...(settings.sidebarSpacing !== defaults.sidebarSpacing ? ["Sidebar spacing"] : []), |
| 173 | + ...(radiusOverrideState !== null ? ["Border radius"] : []), |
| 174 | + ...(fontOverrideState ? ["Font family"] : []), |
| 175 | + ...(fontSizeOverrideState !== null ? ["Code font size"] : []), |
| 176 | + ] as const, |
| 177 | + [ |
| 178 | + colorTheme, |
| 179 | + defaults, |
| 180 | + fontFamily, |
| 181 | + fontOverrideState, |
| 182 | + fontSizeOverrideState, |
| 183 | + isGitTextGenerationModelDirty, |
| 184 | + isInstallSettingsDirty, |
| 185 | + isOpenClawSettingsDirty, |
| 186 | + radiusOverrideState, |
| 187 | + settings, |
| 188 | + theme, |
| 189 | + ], |
| 190 | + ); |
| 191 | + |
| 192 | + const restoreDefaults = useCallback(async () => { |
| 193 | + if (changedSettingLabels.length === 0) return; |
| 194 | + |
| 195 | + const api = readNativeApi(); |
| 196 | + const confirmed = await (api ?? ensureNativeApi()).dialogs.confirm( |
| 197 | + ["Restore default settings?", `This will reset: ${changedSettingLabels.join(", ")}.`].join( |
| 198 | + "\n", |
| 199 | + ), |
| 200 | + ); |
| 201 | + if (!confirmed) return; |
| 202 | + |
| 203 | + setTheme("system"); |
| 204 | + setColorTheme(DEFAULT_COLOR_THEME); |
| 205 | + setFontFamily("inter"); |
| 206 | + resetSettings(); |
| 207 | + |
| 208 | + clearStoredCustomTheme(); |
| 209 | + removeCustomTheme(); |
| 210 | + clearRadiusOverride(); |
| 211 | + setRadiusOverrideState(null); |
| 212 | + clearFontOverride(); |
| 213 | + setFontOverrideState(""); |
| 214 | + clearFontSizeOverride(); |
| 215 | + setFontSizeOverrideState(null); |
| 216 | + }, [ |
| 217 | + changedSettingLabels, |
| 218 | + resetSettings, |
| 219 | + setColorTheme, |
| 220 | + setFontFamily, |
| 221 | + setTheme, |
| 222 | + setFontOverrideState, |
| 223 | + setFontSizeOverrideState, |
| 224 | + setRadiusOverrideState, |
| 225 | + ]); |
| 226 | + |
| 227 | + const value = useMemo<SettingsRouteContextValue>( |
| 228 | + () => ({ |
| 229 | + theme, |
| 230 | + setTheme, |
| 231 | + colorTheme, |
| 232 | + setColorTheme, |
| 233 | + fontFamily, |
| 234 | + setFontFamily, |
| 235 | + settingsState, |
| 236 | + radiusOverride: radiusOverrideState, |
| 237 | + setRadiusOverride, |
| 238 | + fontOverride: fontOverrideState, |
| 239 | + setFontOverride, |
| 240 | + fontSizeOverride: fontSizeOverrideState, |
| 241 | + setFontSizeOverride, |
| 242 | + changedSettingLabels, |
| 243 | + restoreDefaults, |
| 244 | + }), |
| 245 | + [ |
| 246 | + changedSettingLabels, |
| 247 | + colorTheme, |
| 248 | + fontFamily, |
| 249 | + fontOverrideState, |
| 250 | + fontSizeOverrideState, |
| 251 | + radiusOverrideState, |
| 252 | + restoreDefaults, |
| 253 | + setColorTheme, |
| 254 | + setFontFamily, |
| 255 | + setFontOverride, |
| 256 | + setFontSizeOverride, |
| 257 | + setRadiusOverride, |
| 258 | + setTheme, |
| 259 | + settingsState, |
| 260 | + theme, |
| 261 | + ], |
| 262 | + ); |
| 263 | + |
| 264 | + return <SettingsRouteContext.Provider value={value}>{children}</SettingsRouteContext.Provider>; |
| 265 | +} |
| 266 | + |
| 267 | +export function useSettingsRouteContext() { |
| 268 | + const value = useContext(SettingsRouteContext); |
| 269 | + if (!value) { |
| 270 | + throw new Error("useSettingsRouteContext must be used within SettingsRouteContextProvider."); |
| 271 | + } |
| 272 | + return value; |
| 273 | +} |
0 commit comments