From 6464a6723018e8c44dd8019800ca9dd42012e05a Mon Sep 17 00:00:00 2001 From: Henry Li Date: Fri, 16 Jan 2026 23:03:39 +0800 Subject: [PATCH] feat: remember sidebar state --- .../app/workspace/chats/[thread_id]/page.tsx | 8 ++-- frontend/src/app/workspace/layout.tsx | 16 +++++++ frontend/src/core/settings/hooks.ts | 44 ++++++++++++------- frontend/src/core/settings/local.ts | 25 +++++------ 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index f9d0c4c..66ef937 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -26,7 +26,7 @@ export default function ChatPage() { [threadIdFromPath], ); const [threadId, setThreadId] = useState(null); - const { threadContext, setThreadContext } = useLocalSettings(); + const [settings, setSettings] = useLocalSettings(); useEffect(() => { if (threadIdFromPath !== "new") { @@ -43,7 +43,7 @@ export default function ChatPage() { isNewThread, threadId, thread, - threadContext, + threadContext: settings.context, afterSubmit() { router.push(pathOfThread(threadId!)); }, @@ -71,8 +71,8 @@ export default function ChatPage() { className="w-full max-w-(--container-width-md)" autoFocus={isNewThread} status={thread.isLoading ? "streaming" : "ready"} - context={threadContext} - onContextChange={setThreadContext} + context={settings.context} + onContextChange={(context) => setSettings("context", context)} onSubmit={handleSubmit} onStop={handleStop} /> diff --git a/frontend/src/app/workspace/layout.tsx b/frontend/src/app/workspace/layout.tsx index bf770cb..80f3bd9 100644 --- a/frontend/src/app/workspace/layout.tsx +++ b/frontend/src/app/workspace/layout.tsx @@ -1,16 +1,30 @@ "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useCallback, useEffect, useState } from "react"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { Overscroll } from "@/components/workspace/overscroll"; import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar"; +import { useLocalSettings } from "@/core/settings"; const queryClient = new QueryClient(); export default function WorkspaceLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + const [settings, setSettings] = useLocalSettings(); + const [open, setOpen] = useState(false); + useEffect(() => { + setOpen(!settings.layout.sidebar_collapsed); + }, [settings.layout.sidebar_collapsed]); + const handleOpenChange = useCallback( + (open: boolean) => { + setOpen(open); + setSettings("layout", { sidebar_collapsed: !open }); + }, + [setSettings], + ); return ( diff --git a/frontend/src/core/settings/hooks.ts b/frontend/src/core/settings/hooks.ts index 7984763..07606ea 100644 --- a/frontend/src/core/settings/hooks.ts +++ b/frontend/src/core/settings/hooks.ts @@ -1,34 +1,46 @@ import { useCallback, useState } from "react"; import { useEffect } from "react"; -import type { AgentThreadContext } from "../threads"; - import { DEFAULT_LOCAL_SETTINGS, getLocalSettings, - updateContextOfLocalSettings, + saveLocalSettings, + type LocalSettings, } from "./local"; -export function useLocalSettings() { +export function useLocalSettings(): [ + LocalSettings, + ( + key: keyof LocalSettings, + value: Partial, + ) => void, +] { const [mounted, setMounted] = useState(false); - const [threadContextState, setThreadContextState] = useState< - Omit - >(DEFAULT_LOCAL_SETTINGS.context); + const [state, setState] = useState(DEFAULT_LOCAL_SETTINGS); useEffect(() => { if (!mounted) { - setThreadContextState(getLocalSettings().context); + setState(getLocalSettings()); } setMounted(true); }, [mounted]); - const setThreadContext = useCallback( - (context: Omit) => { - setThreadContextState(context); - updateContextOfLocalSettings(context); + const setter = useCallback( + ( + key: keyof LocalSettings, + value: Partial, + ) => { + setState((prev) => { + const newState = { + ...prev, + [key]: { + ...prev[key], + ...value, + }, + }; + saveLocalSettings(newState); + return newState; + }); }, [], ); - return { - threadContext: threadContextState, - setThreadContext, - }; + return [state, setter]; } diff --git a/frontend/src/core/settings/local.ts b/frontend/src/core/settings/local.ts index 419ba9d..41312c8 100644 --- a/frontend/src/core/settings/local.ts +++ b/frontend/src/core/settings/local.ts @@ -5,12 +5,18 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = { model_name: "deepseek-v3.2", thinking_enabled: true, }, + layout: { + sidebar_collapsed: false, + }, }; const LOCAL_SETTINGS_KEY = "deerflow.local-settings"; export interface LocalSettings { context: Omit; + layout: { + sidebar_collapsed: boolean; + }; } export function getLocalSettings(): LocalSettings { @@ -20,7 +26,11 @@ export function getLocalSettings(): LocalSettings { const json = localStorage.getItem(LOCAL_SETTINGS_KEY); try { if (json) { - return JSON.parse(json); + const settings = JSON.parse(json); + return { + ...DEFAULT_LOCAL_SETTINGS, + ...settings, + }; } } catch {} return DEFAULT_LOCAL_SETTINGS; @@ -29,16 +39,3 @@ export function getLocalSettings(): LocalSettings { export function saveLocalSettings(settings: LocalSettings) { localStorage.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settings)); } - -export function updateContextOfLocalSettings( - context: LocalSettings["context"], -) { - const settings = getLocalSettings(); - saveLocalSettings({ - ...settings, - context: { - ...settings.context, - ...context, - }, - }); -}