Skip to content

Commit e3a8fc5

Browse files
authored
fix(frontend): unify local settings runtime state and remove sidebar layout from LocalSettings (bytedance#1879)
* fix(frontend): resolve layout flickering by migrating workspace sidebar state to cookie * fix(frontend): unify local settings runtime state to fix state drift * fix(frontend): only persist thread model on explicit context model updates
1 parent e86067d commit e3a8fc5

6 files changed

Lines changed: 235 additions & 105 deletions

File tree

frontend/src/app/workspace/layout.tsx

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,30 @@
1-
"use client";
2-
3-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4-
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
1+
import { cookies } from "next/headers";
52
import { Toaster } from "sonner";
63

4+
import { QueryClientProvider } from "@/components/query-client-provider";
75
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
86
import { CommandPalette } from "@/components/workspace/command-palette";
97
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
10-
import { getLocalSettings, useLocalSettings } from "@/core/settings";
118

12-
const queryClient = new QueryClient();
9+
function parseSidebarOpenCookie(
10+
value: string | undefined,
11+
): boolean | undefined {
12+
if (value === "true") return true;
13+
if (value === "false") return false;
14+
return undefined;
15+
}
1316

14-
export default function WorkspaceLayout({
17+
export default async function WorkspaceLayout({
1518
children,
1619
}: Readonly<{ children: React.ReactNode }>) {
17-
const [settings, setSettings] = useLocalSettings();
18-
const [open, setOpen] = useState(false); // SSR default: open (matches server render)
19-
useLayoutEffect(() => {
20-
// Runs synchronously before first paint on the client — no visual flash
21-
setOpen(!getLocalSettings().layout.sidebar_collapsed);
22-
}, []);
23-
useEffect(() => {
24-
setOpen(!settings.layout.sidebar_collapsed);
25-
}, [settings.layout.sidebar_collapsed]);
26-
const handleOpenChange = useCallback(
27-
(open: boolean) => {
28-
setOpen(open);
29-
setSettings("layout", { sidebar_collapsed: !open });
30-
},
31-
[setSettings],
20+
const cookieStore = await cookies();
21+
const initialSidebarOpen = parseSidebarOpenCookie(
22+
cookieStore.get("sidebar_state")?.value,
3223
);
24+
3325
return (
34-
<QueryClientProvider client={queryClient}>
35-
<SidebarProvider
36-
className="h-screen"
37-
open={open}
38-
onOpenChange={handleOpenChange}
39-
>
26+
<QueryClientProvider>
27+
<SidebarProvider className="h-screen" defaultOpen={initialSidebarOpen}>
4028
<WorkspaceSidebar />
4129
<SidebarInset className="min-w-0">{children}</SidebarInset>
4230
</SidebarProvider>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use client";
2+
3+
import {
4+
QueryClient,
5+
QueryClientProvider as TanStackQueryClientProvider,
6+
} from "@tanstack/react-query";
7+
8+
const queryClient = new QueryClient();
9+
10+
export function QueryClientProvider({
11+
children,
12+
}: Readonly<{
13+
children: React.ReactNode;
14+
}>) {
15+
return (
16+
<TanStackQueryClientProvider client={queryClient}>
17+
{children}
18+
</TanStackQueryClientProvider>
19+
);
20+
}
Lines changed: 43 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,59 @@
1-
import { useCallback, useLayoutEffect, useState } from "react";
1+
import { useCallback, useMemo, useSyncExternalStore } from "react";
22

33
import {
44
DEFAULT_LOCAL_SETTINGS,
5-
getLocalSettings,
6-
getThreadLocalSettings,
7-
saveLocalSettings,
8-
saveThreadLocalSettings,
5+
applyThreadModelOverride,
96
type LocalSettings,
107
} from "./local";
8+
import {
9+
getBaseSettingsSnapshot,
10+
getThreadModelSnapshot,
11+
subscribe,
12+
updateLocalSettings,
13+
updateThreadSettings,
14+
type LocalSettingsSetter,
15+
} from "./store";
1116

12-
type LocalSettingsSetter = (
13-
key: keyof LocalSettings,
14-
value: Partial<LocalSettings[keyof LocalSettings]>,
15-
) => void;
16-
17-
function useSettingsState(
18-
getSettings: () => LocalSettings,
19-
saveSettings: (settings: LocalSettings) => void,
20-
): [LocalSettings, LocalSettingsSetter] {
21-
const [state, setState] = useState<LocalSettings>(DEFAULT_LOCAL_SETTINGS);
22-
23-
const [mounted, setMounted] = useState(false);
24-
useLayoutEffect(() => {
25-
setState(getSettings());
26-
setMounted(true);
27-
}, [getSettings]);
28-
29-
const setter = useCallback<LocalSettingsSetter>(
30-
(key, value) => {
31-
if (!mounted) return;
32-
setState((prev) => {
33-
const newState: LocalSettings = {
34-
...prev,
35-
[key]: {
36-
...prev[key],
37-
...value,
38-
},
39-
};
40-
saveSettings(newState);
41-
return newState;
42-
});
43-
},
44-
[mounted, saveSettings],
17+
export function useLocalSettings(): [LocalSettings, LocalSettingsSetter] {
18+
const settings = useSyncExternalStore(
19+
subscribe,
20+
getBaseSettingsSnapshot,
21+
() => DEFAULT_LOCAL_SETTINGS,
4522
);
4623

47-
return [state, setter];
48-
}
24+
const setSettings = useCallback<LocalSettingsSetter>((key, value) => {
25+
updateLocalSettings(key, value);
26+
}, []);
4927

50-
export function useLocalSettings(): [LocalSettings, LocalSettingsSetter] {
51-
return useSettingsState(getLocalSettings, saveLocalSettings);
28+
return [settings, setSettings];
5229
}
5330

5431
export function useThreadSettings(
5532
threadId: string,
5633
): [LocalSettings, LocalSettingsSetter] {
57-
return useSettingsState(
58-
useCallback(() => getThreadLocalSettings(threadId), [threadId]),
59-
useCallback(
60-
(settings: LocalSettings) => saveThreadLocalSettings(threadId, settings),
61-
[threadId],
62-
),
34+
const baseSettings = useSyncExternalStore(
35+
subscribe,
36+
getBaseSettingsSnapshot,
37+
() => DEFAULT_LOCAL_SETTINGS,
6338
);
39+
40+
const threadModelName = useSyncExternalStore(
41+
subscribe,
42+
() => getThreadModelSnapshot(threadId),
43+
() => undefined,
44+
);
45+
46+
const settings = useMemo(
47+
() => applyThreadModelOverride(baseSettings, threadModelName),
48+
[baseSettings, threadModelName],
49+
);
50+
51+
const setSettings = useCallback<LocalSettingsSetter>(
52+
(key, value) => {
53+
updateThreadSettings(threadId, key, value);
54+
},
55+
[threadId],
56+
);
57+
58+
return [settings, setSettings];
6459
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from "./hooks";
2-
export * from "./local";
1+
export { useLocalSettings, useThreadSettings } from "./hooks";
2+
export type { LocalSettings } from "./local";

frontend/src/core/settings/local.ts

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
99
mode: undefined,
1010
reasoning_effort: undefined,
1111
},
12-
layout: {
13-
sidebar_collapsed: false,
14-
},
1512
};
1613

17-
const LOCAL_SETTINGS_KEY = "deerflow.local-settings";
18-
const THREAD_MODEL_KEY_PREFIX = "deerflow.thread-model.";
14+
export const LOCAL_SETTINGS_KEY = "deerflow.local-settings";
15+
export const THREAD_MODEL_KEY_PREFIX = "deerflow.thread-model.";
1916

2017
function isBrowser(): boolean {
2118
return typeof window !== "undefined";
@@ -38,9 +35,6 @@ export interface LocalSettings {
3835
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
3936
reasoning_effort?: "minimal" | "low" | "medium" | "high";
4037
};
41-
layout: {
42-
sidebar_collapsed: boolean;
43-
};
4438
}
4539

4640
function mergeLocalSettings(settings?: Partial<LocalSettings>): LocalSettings {
@@ -50,10 +44,6 @@ function mergeLocalSettings(settings?: Partial<LocalSettings>): LocalSettings {
5044
...DEFAULT_LOCAL_SETTINGS.context,
5145
...settings?.context,
5246
},
53-
layout: {
54-
...DEFAULT_LOCAL_SETTINGS.layout,
55-
...settings?.layout,
56-
},
5747
notification: {
5848
...DEFAULT_LOCAL_SETTINGS.notification,
5949
...settings?.notification,
@@ -87,11 +77,10 @@ export function saveThreadModelName(
8777
localStorage.setItem(key, modelName);
8878
}
8979

90-
function applyThreadModelOverride(
80+
export function applyThreadModelOverride(
9181
settings: LocalSettings,
92-
threadId?: string,
82+
threadModelName: string | undefined,
9383
): LocalSettings {
94-
const threadModelName = threadId ? getThreadModelName(threadId) : undefined;
9584
if (!threadModelName) {
9685
return settings;
9786
}
@@ -118,21 +107,9 @@ export function getLocalSettings(): LocalSettings {
118107
return DEFAULT_LOCAL_SETTINGS;
119108
}
120109

121-
export function getThreadLocalSettings(threadId: string): LocalSettings {
122-
return applyThreadModelOverride(getLocalSettings(), threadId);
123-
}
124-
125110
export function saveLocalSettings(settings: LocalSettings) {
126111
if (!isBrowser()) {
127112
return;
128113
}
129114
localStorage.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settings));
130115
}
131-
132-
export function saveThreadLocalSettings(
133-
threadId: string,
134-
settings: LocalSettings,
135-
) {
136-
saveLocalSettings(settings);
137-
saveThreadModelName(threadId, settings.context.model_name);
138-
}

0 commit comments

Comments
 (0)