diff --git a/CHANGELOG.md b/CHANGELOG.md index 45af8596..64324028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 stays preloaded in Agent mode, so creating a file no longer stops at the deferred-tool schema hydration message before the normal approval/execution path (#1825, #1841). +- **Saved sessions keep the selected model mode.** Changing from `auto` to a + concrete model now updates existing session metadata, and resumed sessions + recompute the `auto` flag from the saved model instead of falling back to the + startup default. ### Changed diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 45af8596..64324028 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -105,6 +105,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 stays preloaded in Agent mode, so creating a file no longer stops at the deferred-tool schema hydration message before the normal approval/execution path (#1825, #1841). +- **Saved sessions keep the selected model mode.** Changing from `auto` to a + concrete model now updates existing session metadata, and resumed sessions + recompute the `auto` flag from the saved model instead of falling back to the + startup default. ### Changed diff --git a/crates/tui/src/commands/config.rs b/crates/tui/src/commands/config.rs index 435695c8..f18c5746 100644 --- a/crates/tui/src/commands/config.rs +++ b/crates/tui/src/commands/config.rs @@ -370,9 +370,7 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> "model" => { // Support "/model auto" — auto-select model based on request complexity if value.trim().eq_ignore_ascii_case("auto") { - app.auto_model = true; - app.model = "auto".to_string(); - app.last_effective_model = None; + app.set_model_selection("auto".to_string()); app.reasoning_effort = ReasoningEffort::Auto; app.last_effective_reasoning_effort = None; app.update_model_compaction_budget(); @@ -384,15 +382,13 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> ); } // Clear auto mode when a specific model is set - app.auto_model = false; - app.last_effective_model = None; let Some(model) = normalize_model_name_for_provider(app.api_provider, value) else { return CommandResult::error(format!( "Invalid model '{value}'. Expected a DeepSeek model ID. Common models: {}", COMMON_DEEPSEEK_MODELS.join(", ") )); }; - app.model = model.clone(); + app.set_model_selection(model.clone()); app.update_model_compaction_budget(); app.session.last_prompt_tokens = None; app.session.last_completion_tokens = None; @@ -550,9 +546,7 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> } "default_model" => { if let Some(ref model) = settings.default_model { - app.auto_model = model.trim().eq_ignore_ascii_case("auto"); - app.model.clone_from(model); - app.last_effective_model = None; + app.set_model_selection(model.clone()); if app.auto_model { app.reasoning_effort = ReasoningEffort::Auto; app.last_effective_reasoning_effort = None; diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 2e06b2e9..9fcf00d6 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -4150,6 +4150,25 @@ impl App { compaction_threshold_for_model_and_effort(&model, self.reasoning_effort.api_value()); } + pub fn set_model_selection(&mut self, model: String) { + let auto_model = model.trim().eq_ignore_ascii_case("auto"); + self.model = if auto_model { + "auto".to_string() + } else { + model + }; + self.auto_model = auto_model; + self.last_effective_model = None; + } + + pub fn model_selection_for_persistence(&self) -> String { + if self.auto_model || self.model.trim().eq_ignore_ascii_case("auto") { + "auto".to_string() + } else { + self.model.clone() + } + } + pub fn effective_model_for_budget(&self) -> &str { if self.auto_model { return self diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 58066cb0..05893409 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -1497,8 +1497,7 @@ async fn run_event_loop( if app.auto_model { app.last_effective_model = Some(model); } else { - app.model = model; - app.last_effective_model = None; + app.set_model_selection(model); } app.update_model_compaction_budget(); app.workspace = workspace; @@ -3513,6 +3512,7 @@ async fn run_cache_warmup(app: &App, config: &Config) -> Result { // `format_*` chip/message builders moved to `tui/format_helpers.rs`. fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession { + let model = app.model_selection_for_persistence(); if let Some(ref existing_id) = app.current_session_id && let Ok(existing) = manager.load_session(existing_id) { @@ -3522,6 +3522,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession { u64::from(app.session.total_tokens), app.system_prompt.as_ref(), ); + updated.metadata.model = model; updated.metadata.mode = Some(app.mode.as_setting().to_string()); app.sync_cost_to_metadata(&mut updated.metadata); updated.context_references = app.session_context_references.clone(); @@ -3532,7 +3533,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession { create_saved_session_with_id_and_mode( existing_id.clone(), &app.api_messages, - &app.model, + &model, &app.workspace, u64::from(app.session.total_tokens), app.system_prompt.as_ref(), @@ -3541,7 +3542,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession { } else { create_saved_session_with_mode( &app.api_messages, - &app.model, + &model, &app.workspace, u64::from(app.session.total_tokens), app.system_prompt.as_ref(), @@ -4111,16 +4112,16 @@ async fn apply_model_picker_choice( } if model_changed { - app.auto_model = model_is_auto; - app.last_effective_model = None; - app.model = model.clone(); - app.update_model_compaction_budget(); + app.set_model_selection(model.clone()); app.clear_model_scoped_telemetry(); } if effort_changed { app.reasoning_effort = effort; app.last_effective_reasoning_effort = None; } + if model_changed || effort_changed { + app.update_model_compaction_budget(); + } // Best-effort persist; surface a status warning if the settings file // can't be written rather than aborting the in-memory change. @@ -4236,7 +4237,7 @@ async fn switch_provider( let new_model = config.default_model(); let cache_scope_changed = previous_provider != target || previous_model != new_model; app.api_provider = target; - app.model = new_model.clone(); + app.set_model_selection(new_model.clone()); app.update_model_compaction_budget(); if cache_scope_changed { app.clear_model_scoped_telemetry(); @@ -4672,7 +4673,7 @@ async fn apply_command_result( *config = new_config.clone(); app.api_provider = config.api_provider(); let new_model = config.default_model(); - app.model = new_model.clone(); + app.set_model_selection(new_model.clone()); app.update_model_compaction_budget(); app.session.last_prompt_tokens = None; app.session.last_completion_tokens = None; @@ -6255,7 +6256,7 @@ fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession) app.sync_context_references_from_session(&session.context_references, &message_to_cell); app.mark_history_updated(); app.viewport.transcript_selection.clear(); - app.model.clone_from(&session.metadata.model); + app.set_model_selection(session.metadata.model.clone()); app.update_model_compaction_budget(); apply_workspace_runtime_state(app, config, session.metadata.workspace.clone()); app.session.total_tokens = u32::try_from(session.metadata.total_tokens).unwrap_or(u32::MAX); diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index e5e1396b..cd9ef941 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -3912,6 +3912,64 @@ fn first_snapshot_preserves_current_session_id_for_artifact_ownership() { assert_eq!(snapshot.metadata.id, "session-123"); } +#[test] +fn existing_session_snapshot_updates_model_selection() { + let tmp = tempfile::tempdir().expect("tempdir"); + let manager = + crate::session_manager::SessionManager::new(tmp.path().join("sessions")).expect("manager"); + let mut existing = saved_session_with_messages(vec![text_message("user", "hello")]); + existing.metadata.model = "auto".to_string(); + manager + .save_session(&existing) + .expect("save existing session"); + + let mut app = create_test_app(); + app.current_session_id = Some(existing.metadata.id.clone()); + app.api_messages.push(text_message("user", "hello")); + app.set_model_selection("deepseek-v4-flash".to_string()); + + let snapshot = build_session_snapshot(&app, &manager); + + assert_eq!(snapshot.metadata.id, existing.metadata.id); + assert_eq!(snapshot.metadata.model, "deepseek-v4-flash"); +} + +#[test] +fn apply_loaded_session_restores_concrete_model_mode() { + let mut app = create_test_app(); + app.set_model_selection("auto".to_string()); + let mut session = saved_session_with_messages(vec![ + text_message("user", "hello"), + text_message("assistant", "hi"), + ]); + session.metadata.model = "deepseek-v4-flash".to_string(); + + let recovered = apply_loaded_session(&mut app, &Config::default(), &session); + + assert!(!recovered); + assert!(!app.auto_model); + assert_eq!(app.model, "deepseek-v4-flash"); + assert_eq!(app.model_selection_for_persistence(), "deepseek-v4-flash"); +} + +#[test] +fn apply_loaded_session_restores_auto_model_mode() { + let mut app = create_test_app(); + app.set_model_selection("deepseek-v4-pro".to_string()); + let mut session = saved_session_with_messages(vec![ + text_message("user", "hello"), + text_message("assistant", "hi"), + ]); + session.metadata.model = "auto".to_string(); + + let recovered = apply_loaded_session(&mut app, &Config::default(), &session); + + assert!(!recovered); + assert!(app.auto_model); + assert_eq!(app.model, "auto"); + assert_eq!(app.model_selection_for_persistence(), "auto"); +} + #[test] fn apply_loaded_session_restores_artifact_registry() { let mut app = create_test_app();