From 8226e6094719d5a9d69081b37c756978dc3580f1 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 31 Jul 2026 19:21:54 +0530 Subject: [PATCH 1/2] persist Nova thread settings --- apps/web/components/chat/index.tsx | 98 ++++++++++++++++++++--- apps/web/lib/chat-thread-settings.test.ts | 33 ++++++++ apps/web/lib/chat-thread-settings.ts | 42 ++++++++++ 3 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 apps/web/lib/chat-thread-settings.test.ts create mode 100644 apps/web/lib/chat-thread-settings.ts diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index c78df7f7b..90eb452ff 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -73,6 +73,10 @@ import { } from "./attachments" import { cacheFileBlob, removeCachedFile } from "@/lib/file-cache" import { ReasoningSelector } from "./reasoning-selector" +import { + type ChatThreadSettings, + readChatThreadSettings, +} from "@/lib/chat-thread-settings" type ChatMessageSendSource = "typed" | "suggested" | "highlight" | "home" @@ -284,7 +288,8 @@ export function ChatSidebar({ const [chatSpaceProjects, setChatSpaceProjects] = useState([ initialChatProject ?? AUTO_CHAT_SPACE_ID, ]) - const chatProject = chatSpaceProjects[0] ?? selectedProject + const chatProject = + chatSpaceProjects[0] ?? selectedProject ?? AUTO_CHAT_SPACE_ID const { allProjects } = useContainerTags() const selectedProjectRef = useRef(chatProject) selectedProjectRef.current = chatProject @@ -348,6 +353,28 @@ export function ChatSidebar({ ) const chatApiBase = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + const persistThreadSettings = useCallback( + async (settings: ChatThreadSettings) => { + if (!threadId) return + try { + const response = await fetch( + `${chatApiBase}/chat/threads/${threadId}`, + { + method: "PATCH", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ settings }), + }, + ) + if (!response.ok) { + console.error("Failed to persist chat settings", response.status) + } + } catch (error) { + console.error("Failed to persist chat settings", error) + } + }, + [chatApiBase, threadId], + ) const chatTransport = useMemo( () => @@ -471,11 +498,47 @@ export function ChatSidebar({ const handleModelChange = useCallback( (modelId: ModelId) => { + const nextReasoningEffort = getDefaultReasoningEffort(modelId) setSelectedModel(modelId) - setReasoningEffort(getDefaultReasoningEffort(modelId)) + setReasoningEffort(nextReasoningEffort) clearError() + void persistThreadSettings({ + model: modelId, + projectId: selectedProjectRef.current, + reasoningEffort: nextReasoningEffort, + spaceMode: + selectedProjectRef.current === AUTO_CHAT_SPACE_ID ? "auto" : "manual", + }) + }, + [clearError, persistThreadSettings], + ) + + const handleReasoningEffortChange = useCallback( + (nextReasoningEffort: ReasoningEffort) => { + setReasoningEffort(nextReasoningEffort) + void persistThreadSettings({ + model: selectedModelRef.current, + projectId: selectedProjectRef.current, + reasoningEffort: nextReasoningEffort, + spaceMode: + selectedProjectRef.current === AUTO_CHAT_SPACE_ID ? "auto" : "manual", + }) + }, + [persistThreadSettings], + ) + + const handleChatSpaceProjectsChange = useCallback( + (nextProjects: string[]) => { + const nextProject = nextProjects[0] ?? AUTO_CHAT_SPACE_ID + setChatSpaceProjects([nextProject]) + void persistThreadSettings({ + model: selectedModelRef.current, + projectId: nextProject, + reasoningEffort: reasoningEffortRef.current, + spaceMode: nextProject === AUTO_CHAT_SPACE_ID ? "auto" : "manual", + }) }, - [clearError], + [persistThreadSettings], ) const setAttachmentDraftState = useCallback( @@ -1139,6 +1202,12 @@ export function ChatSidebar({ }) if (response.ok) { const data = await response.json() + const restoredSettings = readChatThreadSettings( + data.thread?.settings, + data.thread?.space?.containerTag ?? + selectedProject ?? + AUTO_CHAT_SPACE_ID, + ) const uiMessages = data.messages.map( (m: { id: string @@ -1165,6 +1234,9 @@ export function ChatSidebar({ pendingResponseModelsRef.current = [] seenAssistantMessageIdsRef.current = new Set() setResponseModelByMessageId({}) + setSelectedModel(restoredSettings.model) + setReasoningEffort(restoredSettings.reasoningEffort) + setChatSpaceProjects([restoredSettings.projectId]) setThreadId(id) setPendingThreadLoad({ id, messages: uiMessages }) setMessageQueue([]) @@ -1178,7 +1250,7 @@ export function ChatSidebar({ console.error("Failed to load thread:", error) } }, - [chatApiBase, setThreadId], + [chatApiBase, selectedProject, setThreadId], ) // Auto-restore thread from URL on mount (e.g. reload or direct link) @@ -1612,11 +1684,11 @@ export function ChatSidebar({ ] for (const t of filtered) { const ts = new Date(t.updatedAt).getTime() - if (ts >= startOfToday) buckets[0].items.push(t) - else if (ts >= startOfToday - day) buckets[1].items.push(t) - else if (ts >= startOfToday - 7 * day) buckets[2].items.push(t) - else if (ts >= startOfToday - 30 * day) buckets[3].items.push(t) - else buckets[4].items.push(t) + if (ts >= startOfToday) buckets[0]?.items.push(t) + else if (ts >= startOfToday - day) buckets[1]?.items.push(t) + else if (ts >= startOfToday - 7 * day) buckets[2]?.items.push(t) + else if (ts >= startOfToday - 30 * day) buckets[3]?.items.push(t) + else buckets[4]?.items.push(t) } return buckets.filter((b) => b.items.length > 0) }, [threads, historySearch]) @@ -1894,11 +1966,11 @@ export function ChatSidebar({ /> ) : undefined } @@ -2120,7 +2192,7 @@ export function ChatSidebar({ isStackedInput ? ( { + it("restores Auto mode independently of the physical default space", () => { + expect( + readChatThreadSettings( + { + model: "gemini-2.5-pro", + projectId: AUTO_CHAT_SPACE_ID, + reasoningEffort: "thinking", + spaceMode: "auto", + }, + "sm_project_default", + ), + ).toEqual({ + model: "gemini-2.5-pro", + projectId: AUTO_CHAT_SPACE_ID, + reasoningEffort: "thinking", + spaceMode: "auto", + }) + }) + + it("falls back safely for a legacy manual-space thread", () => { + expect(readChatThreadSettings(undefined, "project_legacy")).toEqual({ + model: "grok-4.3", + projectId: "project_legacy", + reasoningEffort: "instant", + spaceMode: "manual", + }) + }) +}) diff --git a/apps/web/lib/chat-thread-settings.ts b/apps/web/lib/chat-thread-settings.ts new file mode 100644 index 000000000..acb3987b6 --- /dev/null +++ b/apps/web/lib/chat-thread-settings.ts @@ -0,0 +1,42 @@ +import { AUTO_CHAT_SPACE_ID } from "./chat-auto-space" +import { + getDefaultReasoningEffort, + modelNames, + type ModelId, + type ReasoningEffort, +} from "./models" + +export type ChatThreadSettings = { + model: ModelId + reasoningEffort: ReasoningEffort + spaceMode: "auto" | "manual" + projectId: string +} + +function isModelId(value: unknown): value is ModelId { + return typeof value === "string" && value in modelNames +} + +export function readChatThreadSettings( + value: unknown, + fallbackProjectId: string, +): ChatThreadSettings { + const settings = + typeof value === "object" && value !== null + ? (value as Record) + : {} + const model = isModelId(settings.model) ? settings.model : "grok-4.3" + const reasoningEffort = + settings.reasoningEffort === "instant" || + settings.reasoningEffort === "thinking" + ? settings.reasoningEffort + : getDefaultReasoningEffort(model) + const spaceMode = settings.spaceMode === "auto" ? "auto" : "manual" + const storedProjectId = + typeof settings.projectId === "string" && settings.projectId.length > 0 + ? settings.projectId + : fallbackProjectId + const projectId = spaceMode === "auto" ? AUTO_CHAT_SPACE_ID : storedProjectId + + return { model, reasoningEffort, spaceMode, projectId } +} From ebd9c87affa539d0a433ad8b00b464ebb4d9c9ff Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Mon, 3 Aug 2026 19:57:17 +0530 Subject: [PATCH 2/2] Delete apps/web/lib/chat-thread-settings.test.ts --- apps/web/lib/chat-thread-settings.test.ts | 33 ----------------------- 1 file changed, 33 deletions(-) delete mode 100644 apps/web/lib/chat-thread-settings.test.ts diff --git a/apps/web/lib/chat-thread-settings.test.ts b/apps/web/lib/chat-thread-settings.test.ts deleted file mode 100644 index 47a6e099c..000000000 --- a/apps/web/lib/chat-thread-settings.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from "vitest" -import { AUTO_CHAT_SPACE_ID } from "./chat-auto-space" -import { readChatThreadSettings } from "./chat-thread-settings" - -describe("chat thread settings", () => { - it("restores Auto mode independently of the physical default space", () => { - expect( - readChatThreadSettings( - { - model: "gemini-2.5-pro", - projectId: AUTO_CHAT_SPACE_ID, - reasoningEffort: "thinking", - spaceMode: "auto", - }, - "sm_project_default", - ), - ).toEqual({ - model: "gemini-2.5-pro", - projectId: AUTO_CHAT_SPACE_ID, - reasoningEffort: "thinking", - spaceMode: "auto", - }) - }) - - it("falls back safely for a legacy manual-space thread", () => { - expect(readChatThreadSettings(undefined, "project_legacy")).toEqual({ - model: "grok-4.3", - projectId: "project_legacy", - reasoningEffort: "instant", - spaceMode: "manual", - }) - }) -})