vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::Result;

use crate::startup::StartupContext;
use vtcode_core::cli::args::Cli;

mod acp;
mod action_resolution;
mod adapters;
mod anthropic_api;
mod app_server;
pub(crate) mod auth;
mod auto;
mod background_subagent;
mod check;
mod checkpoints;
mod config;
mod create_project;
mod dispatch;
mod init;
mod init_project;
mod man;
mod notify;
mod sessions;
pub(crate) mod skills_index;
mod snapshots;
mod trajectory;

pub mod analyze;
pub mod benchmark;
pub mod dependencies;
pub mod exec;
pub mod review;
pub mod schema;
pub mod skills;
pub mod skills_ref;
pub mod update;

mod revert;
mod schedule;

use action_resolution::ResolvedCliAction;
use dispatch::{handle_ask_single_command, handle_chat_command, handle_resume_session_command};

pub(crate) use action_resolution::resolve_action;

/// Skills command options
#[derive(Debug)]
pub struct SkillsCommandOptions {
    pub workspace: std::path::PathBuf,
}

pub async fn dispatch(
    args: &Cli,
    startup: &StartupContext,
    print_mode: Option<String>,
) -> Result<()> {
    let cfg = &startup.config;
    let core_cfg = &startup.agent_config;

    if args.ide
        && args.command.is_none()
        && let Some(ide_target) = crate::main_helpers::detect_available_ide()?
    {
        acp::handle_acp_command(core_cfg, cfg, ide_target).await?;
        return Ok(());
    }

    match resolve_action(args, startup, print_mode)? {
        ResolvedCliAction::Ask { prompt, options } => {
            handle_ask_single_command(
                core_cfg.clone(),
                Some(startup.config.clone()),
                prompt,
                options,
            )
            .await?;
        }
        ResolvedCliAction::FullAuto { prompt } => {
            auto::handle_auto_task_command(core_cfg, cfg, &prompt).await?;
        }
        ResolvedCliAction::Resume { mode } => {
            handle_resume_session_command(
                core_cfg,
                mode,
                startup.resume_show_all,
                startup.custom_session_id.clone(),
                startup.summarize_fork,
                startup.skip_confirmations,
            )
            .await?;
        }
        ResolvedCliAction::Command(command) => {
            dispatch::dispatch_command(args, startup, command).await?;
        }
        ResolvedCliAction::Chat => {
            handle_chat_command(
                core_cfg.clone(),
                startup.config.clone(),
                startup.skip_confirmations,
                startup.full_auto_requested,
                startup.plan_mode_entry_source,
            )
            .await?;
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{ResolvedCliAction, dispatch::handle_resume_session_command, resolve_action};
    use crate::startup::{SessionResumeMode, StartupContext};
    use clap::Parser;
    use std::collections::BTreeMap;
    use std::path::PathBuf;
    use std::time::{SystemTime, UNIX_EPOCH};
    use vtcode_core::cli::args::{Cli, Commands};
    use vtcode_core::config::core::PromptCachingConfig;
    use vtcode_core::config::loader::VTCodeConfig;
    use vtcode_core::config::models::Provider;
    use vtcode_core::config::types::{
        AgentConfig as CoreAgentConfig, ModelSelectionSource, ReasoningEffortLevel,
        UiSurfacePreference,
    };
    use vtcode_core::core::agent::snapshots::{
        DEFAULT_CHECKPOINTS_ENABLED, DEFAULT_MAX_AGE_DAYS, DEFAULT_MAX_SNAPSHOTS,
    };

    fn runtime_config() -> CoreAgentConfig {
        CoreAgentConfig {
            model: vtcode_core::config::constants::models::google::GEMINI_3_FLASH_PREVIEW
                .to_string(),
            api_key: "test-key".to_string(),
            provider: "gemini".to_string(),
            api_key_env: Provider::Gemini.default_api_key_env().to_string(),
            workspace: std::env::current_dir().expect("current_dir"),
            verbose: false,
            quiet: false,
            theme: vtcode_core::ui::theme::DEFAULT_THEME_ID.to_string(),
            reasoning_effort: ReasoningEffortLevel::default(),
            ui_surface: UiSurfacePreference::default(),
            prompt_cache: PromptCachingConfig::default(),
            model_source: ModelSelectionSource::WorkspaceConfig,
            custom_api_keys: BTreeMap::new(),
            checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
            checkpointing_storage_dir: None,
            checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
            checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
            max_conversation_turns: 1000,
            model_behavior: None,
            openai_chatgpt_auth: None,
        }
    }

    fn parse_cli(args: &[&str]) -> Cli {
        Cli::parse_from(args)
    }

    fn startup_context() -> StartupContext {
        StartupContext {
            workspace: PathBuf::from("."),
            config: VTCodeConfig::default(),
            agent_config: runtime_config(),
            skip_confirmations: false,
            full_auto_requested: false,
            automation_prompt: None,
            session_resume: None,
            resume_show_all: false,
            custom_session_id: None,
            summarize_fork: false,
            plan_mode_entry_source:
                vtcode_core::core::interfaces::session::PlanModeEntrySource::None,
        }
    }

    #[test]
    fn resolve_action_prefers_print_mode() {
        let args = parse_cli(&["vtcode", "chat"]);
        let mut startup = startup_context();
        startup.automation_prompt = Some("auto prompt".to_string());
        startup.session_resume = Some(SessionResumeMode::Latest);

        let action = resolve_action(&args, &startup, Some("summarize this".to_string()))
            .expect("print mode should resolve");

        match action {
            ResolvedCliAction::Ask { prompt, .. } => {
                assert_eq!(
                    prompt,
                    Some(
                        crate::main_helpers::build_print_prompt("summarize this".to_string())
                            .expect("print prompt")
                    )
                );
            }
            other => panic!("expected ask action, got {other:?}"),
        }
    }

    #[test]
    fn resolve_action_prefers_auto_over_resume_and_command() {
        let args = parse_cli(&["vtcode", "chat"]);
        let mut startup = startup_context();
        startup.automation_prompt = Some("auto prompt".to_string());
        startup.session_resume = Some(SessionResumeMode::Latest);

        let action = resolve_action(&args, &startup, None).expect("auto should resolve");

        match action {
            ResolvedCliAction::FullAuto { prompt } => assert_eq!(prompt, "auto prompt"),
            other => panic!("expected full-auto action, got {other:?}"),
        }
    }

    #[test]
    fn resolve_action_prefers_resume_over_command() {
        let args = parse_cli(&["vtcode", "chat"]);
        let mut startup = startup_context();
        startup.session_resume = Some(SessionResumeMode::Specific("session-123".to_string()));

        let action = resolve_action(&args, &startup, None).expect("resume should resolve");

        match action {
            ResolvedCliAction::Resume {
                mode: SessionResumeMode::Specific(session_id),
            } => assert_eq!(session_id, "session-123"),
            other => panic!("expected specific resume action, got {other:?}"),
        }
    }

    #[test]
    fn resolve_action_returns_command_when_explicit_subcommand_exists() {
        let args = parse_cli(&["vtcode", "chat"]);
        let startup = startup_context();

        let action = resolve_action(&args, &startup, None).expect("command should resolve");

        match action {
            ResolvedCliAction::Command(Commands::Chat) => {}
            other => panic!("expected chat command action, got {other:?}"),
        }
    }

    #[test]
    fn resolve_action_returns_chat_when_no_special_mode_or_command_exists() {
        let args = parse_cli(&["vtcode"]);
        let startup = startup_context();

        let action = resolve_action(&args, &startup, None).expect("chat should resolve");

        assert!(matches!(action, ResolvedCliAction::Chat));
    }

    #[tokio::test]
    async fn resume_session_command_is_wired_to_sessions_handler() {
        let cfg = runtime_config();
        let unique_suffix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("unix time")
            .as_nanos();
        let fake_id = format!("nonexistent-session-{unique_suffix}");

        let result = handle_resume_session_command(
            &cfg,
            SessionResumeMode::Specific(fake_id),
            false,
            None,
            false,
            true,
        )
        .await;

        let err = result.expect_err("expected missing session error");
        assert!(
            err.to_string().contains("No session with identifier"),
            "unexpected error: {err:#}"
        );
    }
}