fix(tui): persist selected model mode

This commit is contained in:
Hunter Bown
2026-05-21 09:15:52 +08:00
parent 67fb788110
commit 26c66079ba
6 changed files with 100 additions and 20 deletions
+4
View File
@@ -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
+4
View File
@@ -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
+3 -9
View File
@@ -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;
+19
View File
@@ -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
+12 -11
View File
@@ -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<Usage> {
// `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);
+58
View File
@@ -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();