feat: update model save (#3589)
Edit model save to save by default as global or on the profile depending on the session
This commit is contained in:
@@ -42,8 +42,6 @@ pub(crate) struct App {
|
|||||||
/// Config is stored here so we can recreate ChatWidgets as needed.
|
/// Config is stored here so we can recreate ChatWidgets as needed.
|
||||||
pub(crate) config: Config,
|
pub(crate) config: Config,
|
||||||
pub(crate) active_profile: Option<String>,
|
pub(crate) active_profile: Option<String>,
|
||||||
model_saved_to_profile: bool,
|
|
||||||
model_saved_to_global: bool,
|
|
||||||
|
|
||||||
pub(crate) file_search: FileSearchManager,
|
pub(crate) file_search: FileSearchManager,
|
||||||
|
|
||||||
@@ -128,8 +126,6 @@ impl App {
|
|||||||
chat_widget,
|
chat_widget,
|
||||||
config,
|
config,
|
||||||
active_profile,
|
active_profile,
|
||||||
model_saved_to_profile: false,
|
|
||||||
model_saved_to_global: false,
|
|
||||||
file_search,
|
file_search,
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
transcript_cells: Vec::new(),
|
transcript_cells: Vec::new(),
|
||||||
@@ -304,9 +300,38 @@ impl App {
|
|||||||
if let Some(family) = find_family_for_model(&model) {
|
if let Some(family) = find_family_for_model(&model) {
|
||||||
self.config.model_family = family;
|
self.config.model_family = family;
|
||||||
}
|
}
|
||||||
self.model_saved_to_profile = false;
|
}
|
||||||
self.model_saved_to_global = false;
|
AppEvent::PersistModelSelection { model, effort } => {
|
||||||
self.show_model_save_hint();
|
let profile = self.active_profile.as_deref();
|
||||||
|
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {
|
||||||
|
if let Some(profile) = profile {
|
||||||
|
self.chat_widget.add_info_message(
|
||||||
|
format!("Model changed to {model} for {profile} profile"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.chat_widget
|
||||||
|
.add_info_message(format!("Model changed to {model}"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(
|
||||||
|
error = %err,
|
||||||
|
"failed to persist model selection"
|
||||||
|
);
|
||||||
|
if let Some(profile) = profile {
|
||||||
|
self.chat_widget.add_error_message(format!(
|
||||||
|
"Failed to save model for profile `{profile}`: {err}"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.chat_widget
|
||||||
|
.add_error_message(format!("Failed to save default model: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppEvent::UpdateAskForApprovalPolicy(policy) => {
|
AppEvent::UpdateAskForApprovalPolicy(policy) => {
|
||||||
self.chat_widget.set_approval_policy(policy);
|
self.chat_widget.set_approval_policy(policy);
|
||||||
@@ -322,107 +347,9 @@ impl App {
|
|||||||
self.chat_widget.token_usage()
|
self.chat_widget.token_usage()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_model_save_hint(&mut self) {
|
|
||||||
let model = self.config.model.clone();
|
|
||||||
if self.active_profile.is_some() {
|
|
||||||
self.chat_widget.add_info_message(
|
|
||||||
format!("Model changed to {model} for the current session"),
|
|
||||||
Some("(ctrl+s to set as profile default)".to_string()),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.chat_widget.add_info_message(
|
|
||||||
format!("Model changed to {model} for the current session"),
|
|
||||||
Some("(ctrl+s to set as default)".to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_update_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
fn on_update_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||||
let changed = self.config.model_reasoning_effort != effort;
|
|
||||||
self.chat_widget.set_reasoning_effort(effort);
|
self.chat_widget.set_reasoning_effort(effort);
|
||||||
self.config.model_reasoning_effort = effort;
|
self.config.model_reasoning_effort = effort;
|
||||||
if changed {
|
|
||||||
let show_hint = self.model_saved_to_profile || self.model_saved_to_global;
|
|
||||||
self.model_saved_to_profile = false;
|
|
||||||
self.model_saved_to_global = false;
|
|
||||||
if show_hint {
|
|
||||||
self.show_model_save_hint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn persist_model_shortcut(&mut self) {
|
|
||||||
enum SaveScope<'a> {
|
|
||||||
Profile(&'a str),
|
|
||||||
Global,
|
|
||||||
AlreadySaved,
|
|
||||||
}
|
|
||||||
|
|
||||||
let scope = if let Some(profile) = self
|
|
||||||
.active_profile
|
|
||||||
.as_deref()
|
|
||||||
.filter(|_| !self.model_saved_to_profile)
|
|
||||||
{
|
|
||||||
SaveScope::Profile(profile)
|
|
||||||
} else if !self.model_saved_to_global {
|
|
||||||
SaveScope::Global
|
|
||||||
} else {
|
|
||||||
SaveScope::AlreadySaved
|
|
||||||
};
|
|
||||||
|
|
||||||
let model = self.config.model.clone();
|
|
||||||
let effort = self.config.model_reasoning_effort;
|
|
||||||
let codex_home = self.config.codex_home.clone();
|
|
||||||
|
|
||||||
match scope {
|
|
||||||
SaveScope::Profile(profile) => {
|
|
||||||
match persist_model_selection(&codex_home, Some(profile), &model, effort).await {
|
|
||||||
Ok(()) => {
|
|
||||||
self.model_saved_to_profile = true;
|
|
||||||
self.chat_widget.add_info_message(
|
|
||||||
format!("Profile model changed to {model} for all sessions"),
|
|
||||||
Some("(view global config in config.toml)".to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!(
|
|
||||||
error = %err,
|
|
||||||
"failed to persist model selection via shortcut"
|
|
||||||
);
|
|
||||||
self.chat_widget.add_error_message(format!(
|
|
||||||
"Failed to save model preference for profile `{profile}`: {err}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SaveScope::Global => {
|
|
||||||
match persist_model_selection(&codex_home, None, &model, effort).await {
|
|
||||||
Ok(()) => {
|
|
||||||
self.model_saved_to_global = true;
|
|
||||||
self.chat_widget.add_info_message(
|
|
||||||
format!("Default model changed to {model} for all sessions"),
|
|
||||||
Some("(view global config in config.toml)".to_string()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!(
|
|
||||||
error = %err,
|
|
||||||
"failed to persist global model selection via shortcut"
|
|
||||||
);
|
|
||||||
self.chat_widget.add_error_message(format!(
|
|
||||||
"Failed to save global model preference: {err}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SaveScope::AlreadySaved => {
|
|
||||||
self.chat_widget.add_info_message(
|
|
||||||
"Model preference already saved globally; no further action needed."
|
|
||||||
.to_string(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) {
|
async fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) {
|
||||||
@@ -438,14 +365,6 @@ impl App {
|
|||||||
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
|
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
|
||||||
tui.frame_requester().schedule_frame();
|
tui.frame_requester().schedule_frame();
|
||||||
}
|
}
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Char('s'),
|
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.persist_model_shortcut().await;
|
|
||||||
}
|
|
||||||
// Esc primes/advances backtracking only in normal (not working) mode
|
// Esc primes/advances backtracking only in normal (not working) mode
|
||||||
// with an empty composer. In any other state, forward Esc so the
|
// with an empty composer. In any other state, forward Esc so the
|
||||||
// active UI (e.g. status indicator, modals, popups) handles it.
|
// active UI (e.g. status indicator, modals, popups) handles it.
|
||||||
@@ -519,8 +438,6 @@ mod tests {
|
|||||||
chat_widget,
|
chat_widget,
|
||||||
config,
|
config,
|
||||||
active_profile: None,
|
active_profile: None,
|
||||||
model_saved_to_profile: false,
|
|
||||||
model_saved_to_global: false,
|
|
||||||
file_search,
|
file_search,
|
||||||
transcript_cells: Vec::new(),
|
transcript_cells: Vec::new(),
|
||||||
overlay: None,
|
overlay: None,
|
||||||
@@ -533,10 +450,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_reasoning_effort_updates_config_and_resets_flags() {
|
fn update_reasoning_effort_updates_config() {
|
||||||
let mut app = make_test_app();
|
let mut app = make_test_app();
|
||||||
app.model_saved_to_profile = true;
|
|
||||||
app.model_saved_to_global = true;
|
|
||||||
app.config.model_reasoning_effort = Some(ReasoningEffortConfig::Medium);
|
app.config.model_reasoning_effort = Some(ReasoningEffortConfig::Medium);
|
||||||
app.chat_widget
|
app.chat_widget
|
||||||
.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));
|
.set_reasoning_effort(Some(ReasoningEffortConfig::Medium));
|
||||||
@@ -551,7 +466,5 @@ mod tests {
|
|||||||
app.chat_widget.config_ref().model_reasoning_effort,
|
app.chat_widget.config_ref().model_reasoning_effort,
|
||||||
Some(ReasoningEffortConfig::High)
|
Some(ReasoningEffortConfig::High)
|
||||||
);
|
);
|
||||||
assert!(!app.model_saved_to_profile);
|
|
||||||
assert!(!app.model_saved_to_global);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ pub(crate) enum AppEvent {
|
|||||||
/// Update the current model slug in the running app and widget.
|
/// Update the current model slug in the running app and widget.
|
||||||
UpdateModel(String),
|
UpdateModel(String),
|
||||||
|
|
||||||
|
/// Persist the selected model and reasoning effort to the appropriate config.
|
||||||
|
PersistModelSelection {
|
||||||
|
model: String,
|
||||||
|
effort: Option<ReasoningEffort>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Update the current approval policy in the running app and widget.
|
/// Update the current approval policy in the running app and widget.
|
||||||
UpdateAskForApprovalPolicy(AskForApproval),
|
UpdateAskForApprovalPolicy(AskForApproval),
|
||||||
|
|
||||||
|
|||||||
@@ -1192,6 +1192,10 @@ impl ChatWidget {
|
|||||||
}));
|
}));
|
||||||
tx.send(AppEvent::UpdateModel(model_slug.clone()));
|
tx.send(AppEvent::UpdateModel(model_slug.clone()));
|
||||||
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
||||||
|
tx.send(AppEvent::PersistModelSelection {
|
||||||
|
model: model_slug.clone(),
|
||||||
|
effort,
|
||||||
|
});
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"New model: {}, New effort: {}, Current model: {}, Current effort: {}",
|
"New model: {}, New effort: {}, Current model: {}, Current effort: {}",
|
||||||
model_slug.clone(),
|
model_slug.clone(),
|
||||||
@@ -1215,7 +1219,7 @@ impl ChatWidget {
|
|||||||
self.bottom_pane.show_selection_view(
|
self.bottom_pane.show_selection_view(
|
||||||
"Select model and reasoning level".to_string(),
|
"Select model and reasoning level".to_string(),
|
||||||
Some("Switch between OpenAI models for this and future Codex CLI session".to_string()),
|
Some("Switch between OpenAI models for this and future Codex CLI session".to_string()),
|
||||||
Some("Press Enter to confirm, Esc to go back, Ctrl+S to save".to_string()),
|
Some("Press Enter to confirm or Esc to go back".to_string()),
|
||||||
items,
|
items,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user