vtcode 0.98.7

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::Result;
use vtcode_core::config::PermissionMode;
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};
use vtcode_tui::ui::interactive_list::SelectionEntry;

use super::common::{prompt_with_placeholder, run_selection};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StartupMode {
    Edit,
    Auto,
    Plan,
}

impl StartupMode {
    pub(crate) const fn label(self) -> &'static str {
        match self {
            Self::Edit => "Edit",
            Self::Auto => "Auto",
            Self::Plan => "Plan",
        }
    }

    const fn description(self) -> &'static str {
        match self {
            Self::Edit => "Standard interactive mode with normal confirmations.",
            Self::Auto => {
                "Classifier-backed autonomous mode inside the normal interactive session."
            }
            Self::Plan => "Read-only planning mode for research, specs, and architecture work.",
        }
    }
}

pub(crate) fn resolve_initial_startup_mode(config: &VTCodeConfig) -> StartupMode {
    if config.permissions.default_mode == PermissionMode::Plan {
        StartupMode::Plan
    } else if config.permissions.default_mode == PermissionMode::Auto {
        StartupMode::Auto
    } else {
        StartupMode::Edit
    }
}

pub(crate) fn prompt_startup_mode(
    renderer: &mut AnsiRenderer,
    default: StartupMode,
) -> Result<StartupMode> {
    renderer.line(
        MessageStyle::Status,
        "Choose the startup mode VT Code should use for new sessions:",
    )?;
    renderer.line(
        MessageStyle::Info,
        "Edit is the standard interactive mode. Auto keeps the normal session flow but routes riskier actions through the classifier.",
    )?;
    renderer.line(
        MessageStyle::Info,
        "Plan starts in read-only planning mode. `--full-auto` is separate and remains an advanced explicit workflow.",
    )?;

    match select_startup_mode_with_ratatui(default) {
        Ok(mode) => Ok(mode),
        Err(error) => {
            renderer.line(
                MessageStyle::Info,
                &format!("Falling back to manual input ({error})."),
            )?;
            prompt_startup_mode_text(renderer, default)
        }
    }
}

fn startup_mode_entries() -> [(StartupMode, SelectionEntry); 3] {
    [
        (
            StartupMode::Edit,
            SelectionEntry::new(
                "Edit (recommended)".to_string(),
                Some(StartupMode::Edit.description().to_string()),
            ),
        ),
        (
            StartupMode::Auto,
            SelectionEntry::new(
                StartupMode::Auto.label().to_string(),
                Some(StartupMode::Auto.description().to_string()),
            ),
        ),
        (
            StartupMode::Plan,
            SelectionEntry::new(
                StartupMode::Plan.label().to_string(),
                Some(StartupMode::Plan.description().to_string()),
            ),
        ),
    ]
}

fn select_startup_mode_with_ratatui(default: StartupMode) -> Result<StartupMode> {
    let entries = startup_mode_entries();
    let default_index = match default {
        StartupMode::Edit => 0,
        StartupMode::Auto => 1,
        StartupMode::Plan => 2,
    };
    let selection_entries: Vec<SelectionEntry> =
        entries.iter().map(|(_mode, entry)| entry.clone()).collect();
    let instructions =
        "Default: Edit. Use ↑/↓ or j/k to choose, Enter to confirm, Esc to keep the default.";
    let selected_index = run_selection(
        "Startup mode",
        instructions,
        &selection_entries,
        default_index,
    )?;
    Ok(entries[selected_index].0)
}

fn prompt_startup_mode_text(
    renderer: &mut AnsiRenderer,
    default: StartupMode,
) -> Result<StartupMode> {
    let entries = startup_mode_entries();
    for (index, (mode, _entry)) in entries.iter().enumerate() {
        renderer.line(
            MessageStyle::Info,
            &format!("  {}) {}{}", index + 1, mode.label(), mode.description()),
        )?;
    }

    loop {
        let input = prompt_with_placeholder(&format!("Startup mode [{}]", default.label()))?;
        let trimmed = input.trim();
        if trimmed.is_empty() {
            return Ok(default);
        }

        if let Ok(index) = trimmed.parse::<usize>()
            && let Some((mode, _entry)) = entries.get(index.saturating_sub(1))
        {
            return Ok(*mode);
        }

        match trimmed.to_ascii_lowercase().as_str() {
            "edit" => return Ok(StartupMode::Edit),
            "auto" => return Ok(StartupMode::Auto),
            "plan" => return Ok(StartupMode::Plan),
            _ => renderer.line(MessageStyle::Error, "Please choose Edit, Auto, or Plan.")?,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use vtcode_core::config::loader::VTCodeConfig;

    #[test]
    fn startup_mode_entries_are_unnumbered() {
        let entries = startup_mode_entries();

        assert_eq!(entries[0].1.title, "Edit (recommended)");
        assert_eq!(entries[1].1.title, "Auto");
        assert_eq!(entries[2].1.title, "Plan");
    }

    #[test]
    fn resolve_initial_startup_mode_prefers_plan() {
        let mut config = VTCodeConfig::default();
        config.permissions.default_mode = PermissionMode::Plan;

        assert_eq!(resolve_initial_startup_mode(&config), StartupMode::Plan);
    }

    #[test]
    fn resolve_initial_startup_mode_maps_auto_defaults() {
        let mut config = VTCodeConfig::default();
        config.permissions.default_mode = PermissionMode::Auto;

        assert_eq!(resolve_initial_startup_mode(&config), StartupMode::Auto);
    }
}