use super::super::*;
use super::{MissionControlApp, ModelSelectionWorkerOutcome};
fn run_model_selection_job(
paths: crate::config::McPaths,
auth_state: crate::config::AuthState,
model_id: String,
cancel: &AtomicBool,
) -> Option<Result<TuiModelSelectionResult, String>> {
if cancel.load(Ordering::SeqCst) {
return None;
}
let (provider, model, notice, service_tier) =
match crate::model_catalog::resolve_model_with_fast_service_tier(
&paths,
&auth_state,
&model_id,
) {
Ok(result) => result,
Err(error) => return Some(Err(error.to_string())),
};
if cancel.load(Ordering::SeqCst) {
return None;
}
let config = match crate::config::load_effective_provider_selection(&paths, &provider, &model) {
Ok(config) => config,
Err(error) => return Some(Err(error.to_string())),
};
if cancel.load(Ordering::SeqCst) {
return None;
}
if let Err(error) = crate::model_catalog::persist_selected_model(&paths, &provider, &model) {
return Some(Err(error.to_string()));
}
let thinking_levels = cached_thinking_levels_for_model(
&config.paths,
&provider,
&model,
crate::thinking::capability_scope_for_provider(&config.custom_providers, &provider),
);
Some(Ok(TuiModelSelectionResult {
provider,
model,
notice,
service_tier,
config,
thinking_levels,
}))
}
impl MissionControlApp {
pub(crate) fn select_model_from_text(
&mut self,
model_id: &str,
ui_state: &mut state::MissionControlState,
) {
self.select_model(model_id, ui_state);
}
pub(crate) fn select_model(
&mut self,
model_id: &str,
ui_state: &mut state::MissionControlState,
) {
if self.settings_persistence.is_pending() {
ui_state.status = "settings persistence pending; action blocked".to_string();
return;
}
if self.active_run || self.worker.is_some() {
Self::apply_ui_error(ui_state, "cannot change model while a prompt is running");
return;
}
if self.pending_model_selection.is_some() {
ui_state.status = "model selection already in progress".to_string();
return;
}
self.next_model_selection_request_id =
self.next_model_selection_request_id.saturating_add(1);
let request_id = self.next_model_selection_request_id;
let session_id = self.state.active_session_id().map(ToOwned::to_owned);
let generation = self.session_generation;
let provider = self.state.auth_state.provider().to_string();
let model = self.state.model.clone();
self.pending_model_selection = Some(super::PendingModelSelection {
request_id,
session_id: session_id.clone(),
generation,
provider,
model,
});
ui_state.status = "switching modelā¦".to_string();
let paths = self.config.paths.clone();
let auth_state = self.state.auth_state.clone();
let model_id = model_id.to_string();
let session = self.state.current_session.clone();
let cwd = self.state.cwd.clone();
let sender = self.events.clone();
let cancel = Arc::new(AtomicBool::new(false));
let worker_cancel = Arc::clone(&cancel);
let worker_session_id = session_id.clone();
let handle = thread::Builder::new()
.name("magi-model-selection".to_string())
.spawn(move || {
let Some(result) =
run_model_selection_job(paths, auth_state, model_id, &worker_cancel)
else {
return ModelSelectionWorkerOutcome { result: None };
};
if let Err(message) = &result {
let _ = record_session_event(
session.as_ref(),
&cwd,
SessionEventKind::Diagnostic,
serde_json::json!({"level": "error", "message": message}),
);
}
let _ = send_tui_event(
&sender,
TuiEvent::ModelSelectionFinished {
request_id,
session_id: worker_session_id,
generation,
result: Box::new(result.clone()),
},
);
ModelSelectionWorkerOutcome {
result: Some(result),
}
});
match handle {
Ok(handle) => self
.model_selection_workers
.push(super::ModelSelectionWorker {
request_id,
cancel,
handle,
reconciled: false,
}),
Err(error) => {
self.pending_model_selection = None;
ui_state.status =
format!("model selection failed: could not start worker: {error}");
}
}
}
pub(in crate::tui) fn handle_model_selection_drain(
&mut self,
ui_state: &mut state::MissionControlState,
drain_result: &DrainResult,
) -> bool {
let mut changed = false;
for finished in &drain_result.model_selection_finished {
let request_id = finished.request_id;
let session_id = &finished.session_id;
let generation = finished.generation;
let result = &finished.result;
let Some(pending) = self.pending_model_selection.clone() else {
continue;
};
if pending.request_id != request_id
|| pending.session_id != *session_id
|| pending.generation != generation
{
continue;
}
if let Some(worker) = self
.model_selection_workers
.iter_mut()
.find(|worker| worker.request_id == request_id)
{
worker.reconciled = true;
}
self.pending_model_selection = None;
let state_changed = self.active_run
|| self.worker.is_some()
|| self.session_generation != generation
|| self.state.active_session_id() != session_id.as_deref()
|| self.state.auth_state.provider() != pending.provider
|| self.state.model != pending.model
|| ui_state.provider != pending.provider
|| ui_state.model != pending.model;
if state_changed && result.is_err() {
ui_state.status = "model selection failed: state changed".to_string();
changed = true;
continue;
}
match result {
Ok(result) => {
let TuiModelSelectionResult {
provider,
model,
notice,
service_tier: _service_tier,
config,
thinking_levels,
} = result.clone();
self.state.model = model.clone();
self.state.auth_state = config.auth_state();
self.state.config = Some(config.clone());
self.config.provider = config.provider.clone();
self.config.model = config.model.clone();
self.config.auth = config.auth.clone();
self.config.custom_providers = config.custom_providers.clone();
self.config.thinking_level = config.thinking_level;
self.summarizer.refresh_config(&self.config, &self.settings);
ui_state.provider = provider.clone();
ui_state.provider_ready = self.state.auth_state.is_ready();
ui_state.model = model.clone();
ui_state.refresh_thinking_levels(config.thinking_level, thinking_levels);
ui_state.reset_context_for_model_switch();
self.refresh_fast_mode_state(ui_state);
ui_state.close_model_picker();
ui_state.status = format!("model switched to {provider}/{model}");
if let Some(notice) = notice {
ui_state.status.push_str(&format!(" ({notice})"));
}
}
Err(error) => Self::apply_ui_error(ui_state, error),
}
changed = true;
}
changed
}
pub(in crate::tui) fn reap_model_selection_workers(
&mut self,
ui_state: &mut state::MissionControlState,
) -> bool {
let mut changed = false;
let mut active_workers = Vec::with_capacity(self.model_selection_workers.len());
for worker in std::mem::take(&mut self.model_selection_workers) {
if !worker.handle.is_finished() {
active_workers.push(worker);
continue;
}
match worker.handle.join() {
Ok(outcome) => match outcome.result {
Some(result) if !worker.reconciled => {
let (session_id, generation) = self
.pending_model_selection
.as_ref()
.filter(|pending| pending.request_id == worker.request_id)
.map(|pending| (pending.session_id.clone(), pending.generation))
.unwrap_or((None, self.session_generation));
let mut drain = DrainResult::default();
drain.model_selection_finished.push(ModelSelectionFinished {
request_id: worker.request_id,
session_id,
generation,
result,
});
changed |= self.handle_model_selection_drain(ui_state, &drain);
}
None => {
if self
.pending_model_selection
.as_ref()
.is_some_and(|pending| pending.request_id == worker.request_id)
{
self.pending_model_selection = None;
ui_state.status = "model selection cancelled".to_string();
changed = true;
}
}
Some(_) => {}
},
Err(_) => {
if self
.pending_model_selection
.as_ref()
.is_some_and(|pending| pending.request_id == worker.request_id)
{
self.pending_model_selection = None;
ui_state.status = "model selection worker panicked".to_string();
changed = true;
}
}
}
}
self.model_selection_workers = active_workers;
changed
}
pub(super) fn cancel_model_selection_workers(&self) {
for worker in &self.model_selection_workers {
worker.cancel.store(true, Ordering::SeqCst);
}
}
pub(in crate::tui) fn join_model_selection_workers_for_cleanup(&mut self) -> Vec<String> {
let mut errors = Vec::new();
for worker in std::mem::take(&mut self.model_selection_workers) {
match worker.handle.join() {
Ok(outcome) => match outcome.result {
Some(Err(error)) => errors.push(format!(
"model selection worker {} failed: {}",
worker.request_id, error
)),
Some(Ok(_)) => {}
None => {}
},
Err(_) => errors.push(format!(
"model selection worker {} panicked",
worker.request_id
)),
}
}
self.pending_model_selection = None;
errors
}
pub(crate) fn open_model_picker(
&mut self,
ui_state: &mut state::MissionControlState,
terminal_area: ratatui::layout::Rect,
) {
if self.pending_model_selection.is_some() {
ui_state.status = "model selection already in progress".to_string();
return;
}
if let Some(catalog) = &self.model_catalog_cache
&& catalog.notice.is_none()
{
let disabled = crate::config::disabled_model_ids_from_settings(&self.settings);
ui_state.open_model_picker(
catalog.entries.clone(),
disabled,
catalog.notice.clone(),
model_picker_visible_rows(terminal_area, catalog.notice.is_some()),
);
ui_state.status = "model catalog loaded".to_string();
return;
}
ui_state.status = "loading model catalogsā¦".to_string();
self.model_catalog_loading = true;
self.pending_model_catalog_consumer =
Some(super::PendingModelCatalogConsumer::SetModelPicker);
self.next_model_catalog_request_id += 1;
let request_id = self.next_model_catalog_request_id;
ui_state.pending_model_catalog_request_id = Some(request_id);
let paths = self.config.paths.clone();
let sender = self.events.clone();
thread::spawn(move || {
let agg = crate::model_catalog::load_aggregated_catalog(
&paths,
crate::model_catalog::CachePreference::AllowStale,
);
let notice = if agg.notices.is_empty() {
None
} else {
Some(agg.notices.join("; "))
};
let visible_rows = model_picker_visible_rows(terminal_area, notice.is_some());
let result = Ok(crate::model_catalog::CatalogForUi {
entries: agg.entries,
stale: agg.stale,
notice,
});
let _ = send_critical(
&sender,
TuiEvent::ModelCatalog {
request_id,
result,
visible_rows,
},
);
});
}
}