fix(tui): persist selected model mode
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user