vtcode 0.170.0

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use std::sync::Arc;

use tokio::sync::{Notify, mpsc};
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::config::types::AgentConfig;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};
use vtcode_ui::tui::app::{InlineEvent, InlineHandle, TransientEvent, TransientSubmission};

use crate::agent::runloop::model_picker::{ModelPickerProgress, ModelPickerStart, ModelPickerState};
use crate::agent::runloop::unified::session_settings::SessionSettingsControl;
use crate::agent::runloop::unified::state::CtrlCState;

enum Picker {
    Model(Box<ModelPickerState>),
}

pub(super) async fn run(
    mut events: mpsc::UnboundedReceiver<InlineEvent>,
    settings: mpsc::UnboundedSender<SessionSettingsControl>,
    handle: InlineHandle,
    config: AgentConfig,
    vt_cfg: Option<VTCodeConfig>,
    ctrl_c_state: Arc<CtrlCState>,
    ctrl_c_notify: Arc<Notify>,
) {
    let mut renderer = AnsiRenderer::with_inline_ui(handle.clone(), Default::default());
    let mut picker = None;
    let mut provider = config.provider;
    let mut model = config.model;
    let mut effort = config.reasoning_effort;
    while let Some(event) = events.recv().await {
        match event {
            InlineEvent::Steer(input) => {
                let command = input.text.trim();
                let (is_model, model_has_args) = match command.strip_prefix("/model") {
                    Some(rest) => (rest.is_empty() || rest.starts_with(char::is_whitespace), !rest.is_empty()),
                    None => (false, false),
                };
                if is_model {
                    if picker.is_some() {
                        let _ =
                            renderer.line(MessageStyle::Error, "Complete or cancel the current settings picker first.");
                        continue;
                    }
                    if model_has_args {
                        let _ = renderer.line(
                            MessageStyle::Info,
                            "The busy-turn model picker takes no arguments; opening the picker.",
                        );
                    }
                    match ModelPickerState::new(
                        &mut renderer,
                        vt_cfg.clone(),
                        effort,
                        vt_cfg.as_ref().and_then(|cfg| cfg.provider.openai.service_tier),
                        Some(config.workspace.clone()),
                        provider.clone(),
                        model.clone(),
                        Some(Arc::clone(&ctrl_c_state)),
                        Some(Arc::clone(&ctrl_c_notify)),
                    )
                    .await
                    {
                        Ok(ModelPickerStart::InProgress(state)) => picker = Some(Picker::Model(Box::new(state))),
                        Ok(ModelPickerStart::Completed { state, selection }) => {
                            provider.clone_from(&selection.provider);
                            model.clone_from(&selection.model);
                            effort = selection.reasoning;
                            let _ = settings.send(SessionSettingsControl::Model { picker: Box::new(state), selection });
                            let _ = renderer.line(MessageStyle::Info, "Model selected; pending the next request.");
                        }
                        Ok(ModelPickerStart::Exit) => {}
                        Err(error) => {
                            let _ =
                                renderer.line(MessageStyle::Error, &format!("Failed to start model picker: {error:#}"));
                        }
                    }
                }
            }
            InlineEvent::Transient(TransientEvent::Submitted(TransientSubmission::Selection(selection))) => {
                match picker.take() {
                    Some(Picker::Model(mut state)) => match state.handle_list_selection(&mut renderer, selection) {
                        Ok(ModelPickerProgress::Completed(selection)) => {
                            provider.clone_from(&selection.provider);
                            model.clone_from(&selection.model);
                            effort = selection.reasoning;
                            let _ = settings.send(SessionSettingsControl::Model { picker: state, selection });
                            let _ = renderer.line(MessageStyle::Info, "Model selected; pending the next request.");
                        }
                        Ok(ModelPickerProgress::NeedsRefresh) => {
                            if let Err(error) = state.refresh_dynamic_models(&mut renderer).await {
                                let _ = renderer
                                    .line(MessageStyle::Error, &format!("Model list refresh failed: {error:#}"));
                            }
                            picker = Some(Picker::Model(state));
                        }
                        Ok(ModelPickerProgress::InProgress) => picker = Some(Picker::Model(state)),
                        Ok(ModelPickerProgress::Cancelled | ModelPickerProgress::Exit) => {}
                        Err(error) => {
                            let _ = renderer.line(MessageStyle::Error, &format!("Invalid model selection: {error:#}"));
                            picker = Some(Picker::Model(state));
                        }
                    },
                    None => {}
                }
            }
            InlineEvent::Transient(TransientEvent::Cancelled) => picker = None,
            _ => {}
        }
    }
}