From 70d8d14e26b7ca5290ede713a10a0340bd60a1d0 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 18 Jan 2026 19:11:48 +0100 Subject: [PATCH 1/3] Persist composer model and effort --- src-tauri/src/types.rs | 8 ++++ src/App.tsx | 28 ++++++++++++- src/features/models/hooks/useModels.ts | 40 ++++++++++++++++--- src/features/settings/hooks/useAppSettings.ts | 2 + src/types.ts | 2 + 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index eea4714a2..6a7eba319 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -185,6 +185,10 @@ pub(crate) struct AppSettings { pub(crate) remote_backend_token: Option, #[serde(default = "default_access_mode", rename = "defaultAccessMode")] pub(crate) default_access_mode: String, + #[serde(default, rename = "lastComposerModelId")] + pub(crate) last_composer_model_id: Option, + #[serde(default, rename = "lastComposerReasoningEffort")] + pub(crate) last_composer_reasoning_effort: Option, #[serde(default = "default_ui_scale", rename = "uiScale")] pub(crate) ui_scale: f64, #[serde( @@ -290,6 +294,8 @@ impl Default for AppSettings { remote_backend_host: default_remote_backend_host(), remote_backend_token: None, default_access_mode: "current".to_string(), + last_composer_model_id: None, + last_composer_reasoning_effort: None, ui_scale: 1.0, notification_sounds_enabled: true, experimental_collab_enabled: false, @@ -316,6 +322,8 @@ mod tests { assert_eq!(settings.remote_backend_host, "127.0.0.1:4732"); assert!(settings.remote_backend_token.is_none()); assert_eq!(settings.default_access_mode, "current"); + assert!(settings.last_composer_model_id.is_none()); + assert!(settings.last_composer_reasoning_effort.is_none()); assert!((settings.ui_scale - 1.0).abs() < f64::EPSILON); assert!(settings.notification_sounds_enabled); assert!(!settings.experimental_steer_enabled); diff --git a/src/App.tsx b/src/App.tsx index 22b2092bf..d949a22a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -364,7 +364,12 @@ function MainApp() { reasoningOptions, selectedEffort, setSelectedEffort - } = useModels({ activeWorkspace, onDebug: addDebugEntry }); + } = useModels({ + activeWorkspace, + onDebug: addDebugEntry, + preferredModelId: appSettings.lastComposerModelId, + preferredEffort: appSettings.lastComposerReasoningEffort, + }); const { collaborationModes, selectedCollaborationMode, @@ -477,6 +482,27 @@ function MainApp() { const activeDiffError = diffSource === "pr" ? gitPullRequestDiffsError : diffError; + useEffect(() => { + if (!selectedModelId && selectedEffort === null) { + return; + } + setAppSettings((current) => { + if ( + current.lastComposerModelId === selectedModelId && + current.lastComposerReasoningEffort === selectedEffort + ) { + return current; + } + const nextSettings = { + ...current, + lastComposerModelId: selectedModelId, + lastComposerReasoningEffort: selectedEffort, + }; + void queueSaveSettings(nextSettings); + return nextSettings; + }); + }, [queueSaveSettings, selectedEffort, selectedModelId, setAppSettings]); + useEffect(() => { if (diffSource !== "pr" || centerMode !== "diff") { return; diff --git a/src/features/models/hooks/useModels.ts b/src/features/models/hooks/useModels.ts index 2a83b99f9..16d8dcf17 100644 --- a/src/features/models/hooks/useModels.ts +++ b/src/features/models/hooks/useModels.ts @@ -5,9 +5,16 @@ import { getModelList } from "../../../services/tauri"; type UseModelsOptions = { activeWorkspace: WorkspaceInfo | null; onDebug?: (entry: DebugEntry) => void; + preferredModelId?: string | null; + preferredEffort?: string | null; }; -export function useModels({ activeWorkspace, onDebug }: UseModelsOptions) { +export function useModels({ + activeWorkspace, + onDebug, + preferredModelId = null, + preferredEffort = null, +}: UseModelsOptions) { const [models, setModels] = useState([]); const [selectedModelId, setSelectedModelId] = useState(null); const [selectedEffort, setSelectedEffort] = useState(null); @@ -82,9 +89,24 @@ export function useModels({ activeWorkspace, onDebug }: UseModelsOptions) { data.find((model) => model.model === "gpt-5.2-codex") ?? null; const defaultModel = preferredModel ?? data.find((model) => model.isDefault) ?? data[0] ?? null; - if (defaultModel) { - setSelectedModelId(defaultModel.id); - setSelectedEffort(defaultModel.defaultReasoningEffort ?? null); + const existingSelection = data.find((model) => model.id === selectedModelId) ?? null; + const preferredSelection = data.find((model) => model.id === preferredModelId) ?? null; + const nextSelection = existingSelection ?? preferredSelection ?? defaultModel; + if (nextSelection) { + setSelectedModelId(nextSelection.id); + const nextEffort = + selectedEffort && + nextSelection.supportedReasoningEfforts.some( + (effort) => effort.reasoningEffort === selectedEffort, + ) + ? selectedEffort + : preferredEffort && + nextSelection.supportedReasoningEfforts.some( + (effort) => effort.reasoningEffort === preferredEffort, + ) + ? preferredEffort + : nextSelection.defaultReasoningEffort ?? null; + setSelectedEffort(nextEffort); } } catch (error) { onDebug?.({ @@ -97,7 +119,15 @@ export function useModels({ activeWorkspace, onDebug }: UseModelsOptions) { } finally { inFlight.current = false; } - }, [isConnected, onDebug, workspaceId]); + }, [ + isConnected, + onDebug, + preferredEffort, + preferredModelId, + selectedEffort, + selectedModelId, + workspaceId, + ]); useEffect(() => { if (!workspaceId || !isConnected) { diff --git a/src/features/settings/hooks/useAppSettings.ts b/src/features/settings/hooks/useAppSettings.ts index 3525b4c1c..86c149588 100644 --- a/src/features/settings/hooks/useAppSettings.ts +++ b/src/features/settings/hooks/useAppSettings.ts @@ -9,6 +9,8 @@ const defaultSettings: AppSettings = { remoteBackendHost: "127.0.0.1:4732", remoteBackendToken: null, defaultAccessMode: "current", + lastComposerModelId: null, + lastComposerReasoningEffort: null, uiScale: UI_SCALE_DEFAULT, notificationSoundsEnabled: true, experimentalCollabEnabled: false, diff --git a/src/types.ts b/src/types.ts index 88591adfa..a0164cb53 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,6 +77,8 @@ export type AppSettings = { remoteBackendHost: string; remoteBackendToken: string | null; defaultAccessMode: AccessMode; + lastComposerModelId: string | null; + lastComposerReasoningEffort: string | null; uiScale: number; notificationSoundsEnabled: boolean; experimentalCollabEnabled: boolean; From 98688a1537f3db7e837b09ad5070afefc4e979b0 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 18 Jan 2026 20:14:41 +0100 Subject: [PATCH 2/3] fix: avoid saving composer prefs before settings load --- src/App.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 155b76ad7..d21bb6593 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -96,7 +96,8 @@ function MainApp() { settings: appSettings, setSettings: setAppSettings, saveSettings, - doctor + doctor, + isLoading: appSettingsLoading } = useAppSettings(); const dictationModel = useDictationModel(appSettings.dictationModelId); const { @@ -493,6 +494,9 @@ function MainApp() { diffSource === "pr" ? gitPullRequestDiffsError : diffError; useEffect(() => { + if (appSettingsLoading) { + return; + } if (!selectedModelId && selectedEffort === null) { return; } @@ -511,7 +515,13 @@ function MainApp() { void queueSaveSettings(nextSettings); return nextSettings; }); - }, [queueSaveSettings, selectedEffort, selectedModelId, setAppSettings]); + }, [ + appSettingsLoading, + queueSaveSettings, + selectedEffort, + selectedModelId, + setAppSettings, + ]); useEffect(() => { if (diffSource !== "pr" || centerMode !== "diff") { From 512e60f0eb69ce82cfd26d355b75e2326085a076 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 18 Jan 2026 20:17:54 +0100 Subject: [PATCH 3/3] fix: reapply preferred model after settings load --- src/features/models/hooks/useModels.ts | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/features/models/hooks/useModels.ts b/src/features/models/hooks/useModels.ts index 16d8dcf17..0ae3166fe 100644 --- a/src/features/models/hooks/useModels.ts +++ b/src/features/models/hooks/useModels.ts @@ -154,6 +154,52 @@ export function useModels({ setSelectedEffort(selectedModel.defaultReasoningEffort ?? null); }, [selectedEffort, selectedModel]); + useEffect(() => { + if (!models.length) { + return; + } + const preferredSelection = preferredModelId + ? models.find((model) => model.id === preferredModelId) ?? null + : null; + if (!preferredSelection) { + return; + } + const preferredCodex = + models.find((model) => model.model === "gpt-5.2-codex") ?? null; + const defaultModel = + preferredCodex ?? models.find((model) => model.isDefault) ?? models[0] ?? null; + const shouldApplyPreferredModel = + !selectedModelId || + (defaultModel && + selectedModelId === defaultModel.id && + selectedModelId !== preferredSelection.id); + if (shouldApplyPreferredModel) { + setSelectedModelId(preferredSelection.id); + const nextEffort = + preferredEffort && + preferredSelection.supportedReasoningEfforts.some( + (effort) => effort.reasoningEffort === preferredEffort, + ) + ? preferredEffort + : preferredSelection.defaultReasoningEffort ?? null; + setSelectedEffort(nextEffort); + return; + } + if (selectedModelId !== preferredSelection.id || !preferredEffort) { + return; + } + const preferredEffortSupported = preferredSelection.supportedReasoningEfforts.some( + (effort) => effort.reasoningEffort === preferredEffort, + ); + if (!preferredEffortSupported) { + return; + } + const defaultEffort = preferredSelection.defaultReasoningEffort ?? null; + if (!selectedEffort || selectedEffort === defaultEffort) { + setSelectedEffort(preferredEffort); + } + }, [models, preferredEffort, preferredModelId, selectedEffort, selectedModelId]); + return { models, selectedModel,