vtcode 0.98.2

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::core::interfaces::session::PlanModeEntrySource;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};
use vtcode_core::utils::dot_config::load_workspace_trust_level;
use vtcode_tui::app::{EditingMode, InlineListItem, InlineListSelection};

use crate::agent::runloop::slash_commands::SessionModeCommand;
use crate::agent::runloop::unified::state::{SessionMode, should_enforce_safe_mode_prompts};

use super::{SlashCommandContext, SlashCommandControl};

pub(crate) async fn handle_toggle_plan_mode(
    mut ctx: SlashCommandContext<'_>,
    enable: Option<bool>,
) -> Result<SlashCommandControl> {
    let current = ctx.session_stats.is_plan_mode();
    let new_state = match enable {
        Some(value) => value,
        None => !current,
    };

    if new_state == current {
        sync_workspace_trust_prompt_policy(
            &mut ctx,
            if current {
                SessionMode::Plan
            } else {
                SessionMode::Edit
            },
        )
        .await?;
        ctx.renderer.line(
            MessageStyle::Info,
            if current {
                "Plan Mode is already enabled (strict read-only)."
            } else {
                "Plan Mode is already disabled."
            },
        )?;
        return Ok(SlashCommandControl::Continue);
    }

    if new_state {
        crate::agent::runloop::unified::plan_mode_state::transition_to_plan_mode(
            ctx.tool_registry,
            ctx.session_stats,
            ctx.handle,
            PlanModeEntrySource::UserRequest,
            true,
            true,
        )
        .await;
        sync_workspace_trust_prompt_policy(&mut ctx, SessionMode::Plan).await?;
        ctx.renderer.line(
            MessageStyle::Info,
            "Plan Mode enabled (planner profile active)",
        )?;
        ctx.renderer.line(
            MessageStyle::Output,
            "  The agent will focus on analysis and planning with a structured plan.",
        )?;
        ctx.renderer.line(
            MessageStyle::Output,
            "  Mutating tools are blocked; optional plan-file writes under `.vtcode/plans/` (or an explicit custom plan path) remain allowed.",
        )?;
        ctx.renderer.line(MessageStyle::Output, "")?;
        ctx.renderer.line(
            MessageStyle::Info,
            "Allowed tools: read_file, list_files, grep_file, unified_search, request_user_input",
        )?;
        crate::agent::runloop::unified::plan_mode_state::render_plan_mode_next_step_hint(
            ctx.renderer,
        )?;
    } else {
        crate::agent::runloop::unified::plan_mode_state::transition_to_edit_mode(
            ctx.tool_registry,
            ctx.session_stats,
            ctx.handle,
            true,
        )
        .await;
        sync_workspace_trust_prompt_policy(&mut ctx, SessionMode::Edit).await?;
        ctx.renderer.line(
            MessageStyle::Info,
            "Edit Mode enabled (coder profile active)",
        )?;
        ctx.renderer.line(
            MessageStyle::Output,
            "  Mutating tools (edits, commands, tests) are now allowed, subject to normal permissions.",
        )?;
    }

    persist_mode_preference(
        ctx.renderer,
        ctx.config.workspace.as_path(),
        ctx.vt_cfg,
        Some(if new_state {
            PermissionMode::Plan
        } else {
            PermissionMode::Default
        }),
        "plan mode preference",
    )?;

    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_cycle_mode(ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    let new_mode = match ctx.session_stats.current_mode() {
        SessionMode::Edit => SessionMode::Auto,
        SessionMode::Auto => SessionMode::Plan,
        SessionMode::Plan => SessionMode::Edit,
    };
    apply_session_mode(ctx, new_mode).await
}

pub(crate) async fn handle_set_mode(
    mut ctx: SlashCommandContext<'_>,
    mode: SessionModeCommand,
) -> Result<SlashCommandControl> {
    let requested = match mode {
        SessionModeCommand::Edit => SessionMode::Edit,
        SessionModeCommand::Auto => SessionMode::Auto,
        SessionModeCommand::Plan => SessionMode::Plan,
    };

    if ctx.session_stats.current_mode() == requested {
        sync_workspace_trust_prompt_policy(&mut ctx, requested).await?;
        ctx.renderer
            .line(MessageStyle::Info, already_active_message(requested))?;
        return Ok(SlashCommandControl::Continue);
    }

    apply_session_mode(ctx, requested).await
}

pub(crate) async fn handle_start_mode_selection(
    mut ctx: SlashCommandContext<'_>,
) -> Result<SlashCommandControl> {
    if !ctx.renderer.supports_inline_ui() {
        ctx.renderer.line(
            MessageStyle::Info,
            "Use `/mode edit`, `/mode auto`, `/mode plan`, or `/mode cycle` outside inline UI.",
        )?;
        return Ok(SlashCommandControl::Continue);
    }

    if !super::ui::ensure_selection_ui_available(&mut ctx, "choosing a session mode")? {
        return Ok(SlashCommandControl::Continue);
    }

    let current_mode = ctx.session_stats.current_mode();
    let items = vec![
        mode_item(
            "Edit Mode",
            "Full tool access with standard confirmation prompts",
            "mode:edit",
            "mode edit normal confirmations standard",
            current_mode == SessionMode::Edit,
        ),
        mode_item(
            "Auto Mode",
            "Classifier-backed approvals with deny-and-continue recovery",
            "mode:auto",
            "mode auto classifier approvals autonomous",
            current_mode == SessionMode::Auto,
        ),
        mode_item(
            "Plan Mode",
            "Read-only planning and analysis; mutating tools disabled",
            "mode:plan",
            "mode plan readonly planning analysis",
            current_mode == SessionMode::Plan,
        ),
    ];

    ctx.handle.show_list_modal(
        "Session mode".to_string(),
        vec![
            "Choose how VT Code should run this session.".to_string(),
            "Edit uses normal confirmations, Auto uses background classifier checks, and Plan is read-only.".to_string(),
        ],
        items,
        Some(mode_selection_for(current_mode)),
        None,
    );

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

    let requested = match selection {
        InlineListSelection::ConfigAction(action) if action == "mode:edit" => SessionMode::Edit,
        InlineListSelection::ConfigAction(action) if action == "mode:auto" => SessionMode::Auto,
        InlineListSelection::ConfigAction(action) if action == "mode:plan" => SessionMode::Plan,
        _ => {
            ctx.renderer.line(
                MessageStyle::Error,
                "Unsupported mode selection received from inline UI.",
            )?;
            return Ok(SlashCommandControl::Continue);
        }
    };

    if requested == current_mode {
        ctx.renderer
            .line(MessageStyle::Info, already_active_message(requested))?;
        return Ok(SlashCommandControl::Continue);
    }

    apply_session_mode(ctx, requested).await
}

fn persist_mode_preference(
    renderer: &mut AnsiRenderer,
    workspace: &std::path::Path,
    vt_cfg: &mut Option<VTCodeConfig>,
    permission_mode: Option<PermissionMode>,
    preference_label: &str,
) -> Result<()> {
    if let Err(err) = super::persist_mode_settings(workspace, vt_cfg, permission_mode) {
        renderer.line(
            MessageStyle::Error,
            &format!("Failed to persist {preference_label}: {}", err),
        )?;
    }

    Ok(())
}

async fn apply_session_mode(
    mut ctx: SlashCommandContext<'_>,
    new_mode: SessionMode,
) -> Result<SlashCommandControl> {
    match new_mode {
        SessionMode::Plan => {
            crate::agent::runloop::unified::plan_mode_state::transition_to_plan_mode(
                ctx.tool_registry,
                ctx.session_stats,
                ctx.handle,
                PlanModeEntrySource::UserRequest,
                false,
                false,
            )
            .await;
        }
        SessionMode::Edit | SessionMode::Auto => {
            ctx.tool_registry.disable_plan_mode();
            let plan_state = ctx.tool_registry.plan_mode_state();
            plan_state.disable();
            plan_state.set_plan_file(None).await;
            ctx.session_stats.set_plan_mode(false);
            ctx.session_stats
                .set_autonomous_mode(matches!(new_mode, SessionMode::Auto));
            ctx.handle.set_editing_mode(EditingMode::Edit);
            ctx.handle
                .set_autonomous_mode(matches!(new_mode, SessionMode::Auto));
        }
    }

    sync_workspace_trust_prompt_policy(&mut ctx, new_mode).await?;

    match new_mode {
        SessionMode::Edit => {
            ctx.renderer
                .line(MessageStyle::Info, "Switched to Edit Mode")?;
            ctx.renderer.line(
                MessageStyle::Output,
                "  Full tool access with standard confirmation prompts.",
            )?;
        }
        SessionMode::Auto => {
            ctx.renderer
                .line(MessageStyle::Info, "Switched to Auto Mode")?;
            ctx.renderer.line(
                MessageStyle::Output,
                "  Classifier-backed permission checks run in the background; blocked actions should retry with a safer path.",
            )?;
        }
        SessionMode::Plan => {
            ctx.renderer
                .line(MessageStyle::Info, "Switched to Plan Mode")?;
            ctx.renderer.line(
                MessageStyle::Output,
                "  Read-only mode for analysis and planning. Mutating tools disabled.",
            )?;
            crate::agent::runloop::unified::plan_mode_state::render_plan_mode_next_step_hint(
                ctx.renderer,
            )?;
        }
    }

    persist_mode_preference(
        ctx.renderer,
        ctx.config.workspace.as_path(),
        ctx.vt_cfg,
        match new_mode {
            SessionMode::Auto => Some(PermissionMode::Auto),
            SessionMode::Plan => Some(PermissionMode::Plan),
            SessionMode::Edit => Some(PermissionMode::Default),
        },
        "editing mode preference",
    )?;

    Ok(SlashCommandControl::Continue)
}

async fn sync_workspace_trust_prompt_policy(
    ctx: &mut SlashCommandContext<'_>,
    mode: SessionMode,
) -> Result<()> {
    let workspace_trust_level = match ctx.session_bootstrap.acp_workspace_trust {
        Some(level) => Some(level.to_workspace_trust_level()),
        None => load_workspace_trust_level(&ctx.config.workspace).await?,
    };
    let enforce_safe_mode_prompts = should_enforce_safe_mode_prompts(
        ctx.full_auto,
        matches!(mode, SessionMode::Auto),
        workspace_trust_level,
    );
    ctx.tool_registry
        .set_enforce_safe_mode_prompts(enforce_safe_mode_prompts)
        .await;
    Ok(())
}

fn mode_item(
    title: &str,
    subtitle: &str,
    action: &str,
    search_value: &str,
    current: bool,
) -> InlineListItem {
    InlineListItem {
        title: title.to_string(),
        subtitle: Some(subtitle.to_string()),
        badge: current.then_some("Current".to_string()),
        indent: 0,
        selection: Some(InlineListSelection::ConfigAction(action.to_string())),
        search_value: Some(search_value.to_string()),
    }
}

fn mode_selection_for(mode: SessionMode) -> InlineListSelection {
    match mode {
        SessionMode::Edit => InlineListSelection::ConfigAction("mode:edit".to_string()),
        SessionMode::Auto => InlineListSelection::ConfigAction("mode:auto".to_string()),
        SessionMode::Plan => InlineListSelection::ConfigAction("mode:plan".to_string()),
    }
}

fn already_active_message(mode: SessionMode) -> &'static str {
    match mode {
        SessionMode::Edit => "Edit Mode is already active.",
        SessionMode::Auto => "Auto Mode is already active.",
        SessionMode::Plan => "Plan Mode is already active.",
    }
}