Skip to content

Commit 88efc86

Browse files
authored
Split settings into subroutes with shared shell (#423)
- Add dedicated style subroute and shared settings navigation shell - Consolidate settings reset state and restore-defaults handling - Keep the sidebar active on nested settings pages
1 parent 1c85544 commit 88efc86

8 files changed

Lines changed: 3462 additions & 3160 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ export default function Sidebar() {
559559
const navigate = useNavigate();
560560
const pathname = useLocation({ select: (loc) => loc.pathname });
561561
const isOnSubPage =
562-
pathname === "/settings" ||
562+
pathname.startsWith("/settings") ||
563563
pathname === "/pr-review" ||
564564
pathname === "/merge-conflicts" ||
565565
pathname === "/sme-chat";
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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

Comments
 (0)