vtcode 0.99.1

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

use anyhow::{Context, Result};
use vtcode_core::config::loader::{ConfigManager, VTCodeConfig};
use vtcode_core::config::models::Provider;
use vtcode_core::config::types::ReasoningEffortLevel;
use vtcode_core::models_manager::{ModelPreset, builtin_model_presets};
use vtcode_core::utils::ansi::MessageStyle;
use vtcode_tui::app::{InlineListItem, InlineListSelection};

use crate::agent::runloop::ui::build_inline_header_context;

use super::{SlashCommandContext, SlashCommandControl};

pub(crate) async fn handle_set_effort(
    mut ctx: SlashCommandContext<'_>,
    level: Option<ReasoningEffortLevel>,
    persist: bool,
) -> Result<SlashCommandControl> {
    let supported = supported_effort_levels(&ctx);
    if supported.is_empty() {
        ctx.renderer.line(
            MessageStyle::Info,
            &format!(
                "The current model '{}' does not support configurable effort.",
                ctx.config.model
            ),
        )?;
        return Ok(SlashCommandControl::Continue);
    }

    let chosen = if let Some(level) = level {
        if !supported.contains(&level) {
            ctx.renderer.line(
                MessageStyle::Error,
                &format!(
                    "Effort level '{}' is not supported by '{}'.",
                    level.as_str(),
                    ctx.config.model
                ),
            )?;
            ctx.renderer.line(
                MessageStyle::Info,
                &format!(
                    "Supported levels: {}.",
                    supported
                        .iter()
                        .map(|value| value.as_str())
                        .collect::<Vec<_>>()
                        .join(", ")
                ),
            )?;
            return Ok(SlashCommandControl::Continue);
        }
        level
    } else {
        if !ctx.renderer.supports_inline_ui() {
            ctx.renderer.line(
                MessageStyle::Info,
                &format!(
                    "Current effort level: {}. Supported levels for '{}': {}.",
                    ctx.config.reasoning_effort.as_str(),
                    ctx.config.model,
                    supported
                        .iter()
                        .map(|value| value.as_str())
                        .collect::<Vec<_>>()
                        .join(", ")
                ),
            )?;
            ctx.renderer.line(
                MessageStyle::Info,
                "Usage: /effort [--persist] [none|minimal|low|medium|high|xhigh|max]",
            )?;
            return Ok(SlashCommandControl::Continue);
        }

        if !super::ui::ensure_selection_ui_available(&mut ctx, "choosing an effort level")? {
            return Ok(SlashCommandControl::Continue);
        }

        ctx.handle.show_list_modal(
            "Effort level".to_string(),
            vec![
                format!("Set effort for '{}' in this conversation.", ctx.config.model),
                if persist {
                    "The selected level will also be written to vtcode.toml.".to_string()
                } else {
                    "The selected level only affects the active conversation unless you use --persist.".to_string()
                },
            ],
            effort_items(&supported, ctx.config.reasoning_effort, ctx.config.model.as_str()),
            Some(InlineListSelection::ConfigAction(format!(
                "effort:{}",
                ctx.config.reasoning_effort.as_str()
            ))),
            None,
        );

        let Some(selection) = super::ui::wait_for_list_modal_selection(&mut ctx).await else {
            ctx.renderer
                .line(MessageStyle::Info, "Effort selection cancelled.")?;
            return Ok(SlashCommandControl::Continue);
        };

        let InlineListSelection::ConfigAction(action) = selection else {
            ctx.renderer.line(
                MessageStyle::Error,
                "Unsupported effort selection received from inline UI.",
            )?;
            return Ok(SlashCommandControl::Continue);
        };

        let Some(value) = action.strip_prefix("effort:") else {
            ctx.renderer.line(
                MessageStyle::Error,
                "Unsupported effort selection received from inline UI.",
            )?;
            return Ok(SlashCommandControl::Continue);
        };

        let Some(chosen) = ReasoningEffortLevel::parse(value) else {
            ctx.renderer.line(
                MessageStyle::Error,
                "Invalid effort level returned by inline UI.",
            )?;
            return Ok(SlashCommandControl::Continue);
        };

        chosen
    };

    apply_effort_change(&mut ctx, chosen, persist).await?;
    Ok(SlashCommandControl::Continue)
}

async fn apply_effort_change(
    ctx: &mut SlashCommandContext<'_>,
    effort: ReasoningEffortLevel,
    persist: bool,
) -> Result<()> {
    let changed = ctx.config.reasoning_effort != effort;
    ctx.config.reasoning_effort = effort;

    if let Some(cfg) = ctx.vt_cfg.as_mut() {
        cfg.agent.reasoning_effort = effort;
    }

    sync_thread_reasoning_effort(ctx, effort);

    if persist {
        persist_effort_preference(ctx.config.workspace.as_path(), ctx.vt_cfg, effort)?;
    }

    refresh_header_reasoning(ctx).await?;

    let description = effort_description(effort, ctx.config.model.as_str());
    let verb = if changed { "Set" } else { "Kept" };
    let scope = if persist {
        " Updated workspace default for future sessions as well."
    } else {
        ""
    };
    ctx.renderer.line(
        MessageStyle::Info,
        &format!(
            "{} effort level to {}: {}{}",
            verb,
            effort.as_str(),
            description,
            scope
        ),
    )?;

    Ok(())
}

fn effort_items(
    supported: &[ReasoningEffortLevel],
    current: ReasoningEffortLevel,
    model: &str,
) -> Vec<InlineListItem> {
    supported
        .iter()
        .map(|level| InlineListItem {
            title: level.as_str().to_string(),
            subtitle: Some(effort_description(*level, model).to_string()),
            badge: (*level == current).then_some("Current".to_string()),
            indent: 0,
            selection: Some(InlineListSelection::ConfigAction(format!(
                "effort:{}",
                level.as_str()
            ))),
            search_value: Some(format!(
                "{} {}",
                level.as_str(),
                effort_description(*level, model)
            )),
        })
        .collect()
}

fn supported_effort_levels(ctx: &SlashCommandContext<'_>) -> Vec<ReasoningEffortLevel> {
    let Some(provider) = Provider::from_str(ctx.config.provider.as_str()).ok() else {
        return provider_supports_effort(ctx)
            .then(all_effort_levels)
            .unwrap_or_default();
    };

    if let Some(preset) = resolve_model_preset(provider, ctx.config.model.as_str()) {
        return preset
            .supported_reasoning_efforts
            .iter()
            .map(|preset| preset.effort)
            .collect();
    }

    provider_supports_effort(ctx)
        .then(all_effort_levels)
        .unwrap_or_default()
}

fn resolve_model_preset(provider: Provider, model: &str) -> Option<ModelPreset> {
    builtin_model_presets()
        .into_iter()
        .find(|preset| preset.provider == provider && preset.model == model)
}

fn provider_supports_effort(ctx: &SlashCommandContext<'_>) -> bool {
    ctx.provider_client
        .supports_reasoning_effort(&ctx.config.model)
}

fn all_effort_levels() -> Vec<ReasoningEffortLevel> {
    ReasoningEffortLevel::allowed_values()
        .iter()
        .filter_map(|value| ReasoningEffortLevel::parse(value))
        .collect()
}

fn sync_thread_reasoning_effort(ctx: &mut SlashCommandContext<'_>, effort: ReasoningEffortLevel) {
    if let Some(mut metadata) = ctx.thread_handle.metadata() {
        metadata.reasoning_effort = effort.as_str().to_string();
        ctx.thread_handle.replace_metadata(Some(metadata));
    }
}

fn persist_effort_preference(
    workspace: &std::path::Path,
    vt_cfg: &mut Option<VTCodeConfig>,
    effort: ReasoningEffortLevel,
) -> Result<()> {
    let mut manager = ConfigManager::load_from_workspace(workspace).with_context(|| {
        format!(
            "Failed to load vtcode configuration for workspace {}",
            workspace.display()
        )
    })?;
    let mut config = manager.config().clone();
    config.agent.reasoning_effort = effort;
    manager.save_config(&config)?;
    *vt_cfg = Some(config);
    Ok(())
}

async fn refresh_header_reasoning(ctx: &mut SlashCommandContext<'_>) -> Result<()> {
    let mode_label = match (ctx.config.ui_surface, ctx.full_auto) {
        (vtcode_core::config::types::UiSurfacePreference::Inline, true) => "auto".to_string(),
        (vtcode_core::config::types::UiSurfacePreference::Inline, false) => "inline".to_string(),
        (vtcode_core::config::types::UiSurfacePreference::Alternate, _) => "alt".to_string(),
        (vtcode_core::config::types::UiSurfacePreference::Auto, true) => "auto".to_string(),
        (vtcode_core::config::types::UiSurfacePreference::Auto, false) => "std".to_string(),
    };
    let next_header_context = build_inline_header_context(
        ctx.config,
        ctx.vt_cfg.as_ref(),
        ctx.session_bootstrap,
        provider_label_for_header(ctx),
        ctx.config.model.clone(),
        ctx.provider_client
            .effective_context_size(&ctx.config.model),
        mode_label,
        ctx.config.reasoning_effort.as_str().to_string(),
    )
    .await?;
    ctx.header_context.clone_from(&next_header_context);
    ctx.handle.set_header_context(next_header_context);
    Ok(())
}

fn provider_label_for_header(ctx: &SlashCommandContext<'_>) -> String {
    if ctx.config.provider.eq_ignore_ascii_case("openai")
        && ctx.config.openai_chatgpt_auth.is_some()
    {
        return "OpenAI (ChatGPT)".to_string();
    }

    let key = ctx.config.provider.trim();
    if key.is_empty() {
        return ctx.provider_client.name().to_string();
    }

    ctx.vt_cfg
        .as_ref()
        .map(|cfg| cfg.provider_display_name(key))
        .filter(|label| !label.trim().is_empty())
        .unwrap_or_else(|| key.to_string())
}

fn effort_description(level: ReasoningEffortLevel, model: &str) -> &'static str {
    match level {
        ReasoningEffortLevel::None => "No additional reasoning overhead for the fastest responses",
        ReasoningEffortLevel::Minimal => "Very light reasoning with minimal extra latency",
        ReasoningEffortLevel::Low => "Quick, straightforward implementation with minimal overhead",
        ReasoningEffortLevel::Medium => {
            "Balanced approach with standard implementation and testing"
        }
        ReasoningEffortLevel::High => {
            "Comprehensive implementation with extensive testing and documentation"
        }
        ReasoningEffortLevel::XHigh if model == "claude-opus-4-7" => {
            "Deeper reasoning than high, just below maximum (Opus 4.7 only)"
        }
        ReasoningEffortLevel::XHigh => "Deeper reasoning for harder, longer-running problems",
        ReasoningEffortLevel::Max => {
            "Maximum adaptive effort for the most demanding tasks on supported Anthropic models"
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{effort_description, resolve_model_preset};
    use vtcode_core::config::models::Provider;
    use vtcode_core::config::types::ReasoningEffortLevel;

    #[test]
    fn opus_preset_exposes_xhigh_and_max() {
        let preset = resolve_model_preset(Provider::Anthropic, "claude-opus-4-7")
            .expect("preset should exist");
        let levels = preset
            .supported_reasoning_efforts
            .iter()
            .map(|value| value.effort)
            .collect::<Vec<_>>();
        assert_eq!(
            levels,
            vec![
                ReasoningEffortLevel::Low,
                ReasoningEffortLevel::Medium,
                ReasoningEffortLevel::High,
                ReasoningEffortLevel::XHigh,
                ReasoningEffortLevel::Max,
            ]
        );
    }

    #[test]
    fn xhigh_description_matches_requested_opus_copy() {
        assert_eq!(
            effort_description(ReasoningEffortLevel::XHigh, "claude-opus-4-7"),
            "Deeper reasoning than high, just below maximum (Opus 4.7 only)"
        );
    }
}