From d562706c8b08419f6be070a7336c72b3c7a8ed6a Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 2 Aug 2026 08:27:31 +0200 Subject: [PATCH] fix: sync assistant runtime preferences --- .../_components/assistant-configuration.tsx | 1 + .../assistant/assistant-panel.module.css | 4 ++ .../assistant/assistant-preferences.spec.ts | 54 +++++++++++++++++++ .../assistant/assistant-preferences.ts | 16 ++++++ .../assistant/assistant-provider.tsx | 17 ++++-- packages/translation/src/lang/en.json | 12 ++--- 6 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 apps/nextjs/src/components/assistant/assistant-preferences.spec.ts diff --git a/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx b/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx index 2e91251e9..a7b2db536 100644 --- a/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx +++ b/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx @@ -256,6 +256,7 @@ export const AssistantConfiguration = () => { await Promise.all([ utils.assistant.getAdminConfiguration.invalidate(), utils.assistant.getAvailability.invalidate(), + utils.assistant.getRuntimeOptions.invalidate(), ]); showSuccessNotification({ title: t("notification.saved.title"), diff --git a/apps/nextjs/src/components/assistant/assistant-panel.module.css b/apps/nextjs/src/components/assistant/assistant-panel.module.css index ca6c8f105..41bba98c2 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.module.css +++ b/apps/nextjs/src/components/assistant/assistant-panel.module.css @@ -988,6 +988,10 @@ flex: 1 1 auto; } +.modelButton :global(.mantine-Button-label) { + font-size: var(--mantine-font-size-xs); +} + .reasoningButton { min-width: 7.5rem; max-width: 10.5rem; diff --git a/apps/nextjs/src/components/assistant/assistant-preferences.spec.ts b/apps/nextjs/src/components/assistant/assistant-preferences.spec.ts new file mode 100644 index 000000000..846a8e1ae --- /dev/null +++ b/apps/nextjs/src/components/assistant/assistant-preferences.spec.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "vitest"; + +import { resolveAssistantPreferenceModelId } from "./assistant-preferences"; + +const models = [ + { id: "provider/default", name: "Default", inputModalities: ["text"] }, + { id: "provider/alternate", name: "Alternate", inputModalities: ["text"] }, +]; + +describe("resolveAssistantPreferenceModelId", () => { + test("uses the server default when preferences first load", () => { + expect( + resolveAssistantPreferenceModelId({ + currentModelId: null, + previousDefaultModelId: undefined, + defaultModelId: "provider/default", + models, + }), + ).toBe("provider/default"); + }); + + test("adopts a changed server default even when the old model remains available", () => { + expect( + resolveAssistantPreferenceModelId({ + currentModelId: "provider/default", + previousDefaultModelId: "provider/default", + defaultModelId: "provider/alternate", + models, + }), + ).toBe("provider/alternate"); + }); + + test("preserves an available conversation selection while the default is unchanged", () => { + expect( + resolveAssistantPreferenceModelId({ + currentModelId: "provider/alternate", + previousDefaultModelId: "provider/default", + defaultModelId: "provider/default", + models, + }), + ).toBe("provider/alternate"); + }); + + test("falls back to the default when the selected model is no longer available", () => { + expect( + resolveAssistantPreferenceModelId({ + currentModelId: "provider/removed", + previousDefaultModelId: "provider/default", + defaultModelId: "provider/default", + models, + }), + ).toBe("provider/default"); + }); +}); diff --git a/apps/nextjs/src/components/assistant/assistant-preferences.ts b/apps/nextjs/src/components/assistant/assistant-preferences.ts index 35c448552..8da15989e 100644 --- a/apps/nextjs/src/components/assistant/assistant-preferences.ts +++ b/apps/nextjs/src/components/assistant/assistant-preferences.ts @@ -5,3 +5,19 @@ export type AssistantRuntimeModelOption = { name: string; inputModalities: string[]; }; + +export const resolveAssistantPreferenceModelId = ({ + currentModelId, + previousDefaultModelId, + defaultModelId, + models, +}: { + currentModelId: string | null; + previousDefaultModelId: string | null | undefined; + defaultModelId: string | null; + models: readonly AssistantRuntimeModelOption[]; +}) => { + if (previousDefaultModelId !== defaultModelId) return defaultModelId; + if (currentModelId && models.some((model) => model.id === currentModelId)) return currentModelId; + return defaultModelId; +}; diff --git a/apps/nextjs/src/components/assistant/assistant-provider.tsx b/apps/nextjs/src/components/assistant/assistant-provider.tsx index d8d973456..0d678f396 100644 --- a/apps/nextjs/src/components/assistant/assistant-provider.tsx +++ b/apps/nextjs/src/components/assistant/assistant-provider.tsx @@ -30,6 +30,7 @@ import { AssistantAskUserTool, AssistantConfigureAppTool } from "./assistant-hum import { AssistantPanel } from "./assistant-panel"; import { getPendingAssistantAction } from "./assistant-pending-action"; import type { AssistantReasoningMode, AssistantRuntimeModelOption } from "./assistant-preferences"; +import { resolveAssistantPreferenceModelId } from "./assistant-preferences"; import { AssistantRuntimeProviderWithTools } from "./assistant-runtime-provider"; import { sendAssistantPrompt as sendPromptThroughComposer } from "./assistant-send"; import { createAssistantPromptInteraction } from "./assistant-spotlight"; @@ -163,6 +164,7 @@ const AssistantPreferencesProvider = ({ children }: PropsWithChildren) => { }); const [modelId, setModelIdState] = useState(null); const [reasoning, setReasoningState] = useState("auto"); + const previousDefaultModelIdRef = useRef(undefined); const preferencesRef = useRef<{ modelId: string | null; reasoning: AssistantReasoningMode }>({ modelId: null, reasoning: "auto", @@ -190,10 +192,17 @@ const AssistantPreferencesProvider = ({ children }: PropsWithChildren) => { useEffect(() => { if (!data) return; - const currentModelId = preferencesRef.current.modelId; - if (currentModelId && data.models.some((model) => model.id === currentModelId)) return; - setModelId(data.defaultModelId); - }, [data, setModelId]); + const nextModelId = resolveAssistantPreferenceModelId({ + currentModelId: preferencesRef.current.modelId, + previousDefaultModelId: previousDefaultModelIdRef.current, + defaultModelId: data.defaultModelId, + models: data.models, + }); + previousDefaultModelIdRef.current = data.defaultModelId; + if (nextModelId === preferencesRef.current.modelId) return; + preferencesRef.current.modelId = nextModelId; + setModelIdState(nextModelId); + }, [data]); const value = useMemo( () => ({ diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 4e3944fea..9aff59fc2 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1554,12 +1554,12 @@ "noModels": "No models available", "thinking": "Thinking mode", "reasoning": { - "auto": "Thinking: Auto", - "none": "Thinking: Off", - "minimal": "Thinking: Minimal", - "low": "Thinking: Low", - "medium": "Thinking: Medium", - "high": "Thinking: High" + "auto": "Auto", + "none": "Off", + "minimal": "Minimal", + "low": "Low", + "medium": "Medium", + "high": "High" } }, "stop": "Stop response",