magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
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();
        // fire-and-forget: model catalog load is short-lived; completion returns through TUI channel with request id.
        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,
                },
            );
        });
    }
}