vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{LazyLock, Mutex};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RelaunchPreference {
    PreferPathCommand,
    PreferOriginalExecutable,
}

#[derive(Debug, Clone)]
struct PendingRelaunch {
    preference: RelaunchPreference,
}

#[derive(Debug, Clone, Default)]
struct RuntimeRelaunchContext {
    argv: Vec<OsString>,
    cwd: Option<PathBuf>,
    pending: Option<PendingRelaunch>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct RelaunchAttempt {
    program: OsString,
    args: Vec<OsString>,
}

static RUNTIME_RELAUNCH_CONTEXT: LazyLock<Mutex<RuntimeRelaunchContext>> =
    LazyLock::new(|| Mutex::new(RuntimeRelaunchContext::default()));

fn with_runtime_relaunch_context<R>(f: impl FnOnce(&mut RuntimeRelaunchContext) -> R) -> R {
    match RUNTIME_RELAUNCH_CONTEXT.lock() {
        Ok(mut context) => f(&mut context),
        Err(poisoned) => {
            let mut context = poisoned.into_inner();
            f(&mut context)
        }
    }
}

pub(crate) fn configure_runtime_relaunch_context(argv: Vec<OsString>, cwd: PathBuf) {
    with_runtime_relaunch_context(|context| {
        context.argv = argv;
        context.cwd = Some(cwd);
        context.pending = None;
    });
}

pub(crate) fn queue_runtime_relaunch(preference: RelaunchPreference) {
    with_runtime_relaunch_context(|context| {
        context.pending = Some(PendingRelaunch { preference });
    });
}

pub(crate) fn perform_queued_runtime_relaunch() {
    let Some((pending, argv, cwd)) = with_runtime_relaunch_context(|context| {
        let pending = context.pending.take()?;
        let cwd = context.cwd.clone()?;
        Some((pending, context.argv.clone(), cwd))
    }) else {
        return;
    };

    let current_exe = std::env::current_exe().ok();
    let attempts = relaunch_attempts(&argv, current_exe.as_deref(), pending.preference);
    let manual_command = attempts
        .first()
        .map(format_manual_restart_command)
        .unwrap_or_else(|| "vtcode".to_string());
    let mut last_error: Option<String> = None;

    for attempt in attempts {
        match spawn_relaunch_attempt(&attempt, &cwd) {
            Ok(_) => std::process::exit(0),
            Err(err) => {
                last_error = Some(format!("{}: {}", attempt.program.to_string_lossy(), err));
            }
        }
    }

    eprintln!("warning: VT Code updated but could not restart automatically.");
    eprintln!("warning: restart it manually with `{manual_command}`.");
    if let Some(err) = last_error {
        eprintln!("warning: last relaunch attempt failed: {err}");
    }
}

fn spawn_relaunch_attempt(attempt: &RelaunchAttempt, cwd: &Path) -> std::io::Result<()> {
    let mut command = Command::new(&attempt.program);
    command
        .args(&attempt.args)
        .current_dir(cwd)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit());
    let _child = command.spawn()?;
    Ok(())
}

fn format_manual_restart_command(attempt: &RelaunchAttempt) -> String {
    let mut parts = Vec::with_capacity(attempt.args.len() + 1);
    parts.push(attempt.program.to_string_lossy().to_string());
    parts.extend(
        attempt
            .args
            .iter()
            .map(|arg| arg.to_string_lossy().to_string()),
    );
    parts.join(" ")
}

fn relaunch_attempts(
    argv: &[OsString],
    current_exe: Option<&Path>,
    preference: RelaunchPreference,
) -> Vec<RelaunchAttempt> {
    let original_program = argv.first().cloned();
    let args = argv.get(1..).unwrap_or(&[]).to_vec();
    let current_exe = current_exe.map(|path| path.as_os_str().to_os_string());

    let candidates = match preference {
        RelaunchPreference::PreferPathCommand => vec![
            Some(OsString::from("vtcode")),
            original_program,
            current_exe,
        ],
        RelaunchPreference::PreferOriginalExecutable => vec![
            original_program,
            current_exe,
            Some(OsString::from("vtcode")),
        ],
    };

    let mut attempts = Vec::with_capacity(candidates.len());
    for candidate in candidates.into_iter().flatten() {
        if candidate.is_empty()
            || attempts
                .iter()
                .any(|attempt: &RelaunchAttempt| attempt.program == candidate)
        {
            continue;
        }
        attempts.push(RelaunchAttempt {
            program: candidate,
            args: args.clone(),
        });
    }
    attempts
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn relaunch_attempts_prefer_path_for_managed_installs() {
        let attempts = relaunch_attempts(
            &[
                OsString::from("/usr/local/bin/vtcode"),
                OsString::from("--resume"),
            ],
            Some(Path::new("/tmp/current-vtcode")),
            RelaunchPreference::PreferPathCommand,
        );

        assert_eq!(attempts[0].program, OsString::from("vtcode"));
        assert_eq!(attempts[0].args, vec![OsString::from("--resume")]);
        assert_eq!(attempts[1].program, OsString::from("/usr/local/bin/vtcode"));
    }

    #[test]
    fn relaunch_attempts_prefer_original_binary_for_standalone_installs() {
        let attempts = relaunch_attempts(
            &[OsString::from("/Users/dev/.local/bin/vtcode")],
            Some(Path::new("/tmp/current-vtcode")),
            RelaunchPreference::PreferOriginalExecutable,
        );

        assert_eq!(
            attempts[0].program,
            OsString::from("/Users/dev/.local/bin/vtcode")
        );
        assert_eq!(attempts[1].program, OsString::from("/tmp/current-vtcode"));
    }

    #[test]
    fn manual_restart_command_includes_original_args() {
        let command = format_manual_restart_command(&RelaunchAttempt {
            program: OsString::from("vtcode"),
            args: vec![OsString::from("--resume"), OsString::from("session-1")],
        });

        assert_eq!(command, "vtcode --resume session-1");
    }
}