vtcode 0.141.10

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::Result;
use tokio::task;
use vtcode_core::core::threads::{SessionQueryScope, list_recent_sessions_in_scope};
use vtcode_core::ui::inline_theme_from_core_styles;
use vtcode_core::ui::theme;
use vtcode_core::utils::ansi::MessageStyle;
use vtcode_ui::tui::app::{InlineListItem, InlineListSelection, TransientSubmission};

use super::{SlashCommandContext, SlashCommandControl};
use crate::agent::runloop::model_picker::{ModelPickerStart, ModelPickerState};
use crate::agent::runloop::slash_commands::SessionPaletteMode;
use crate::agent::runloop::unified::display::{persist_theme_preference, sync_runtime_theme_selection};
use crate::agent::runloop::unified::model_selection::{ModelSwitchCompactionTargets, finalize_model_selection};
use crate::agent::runloop::unified::overlay_prompt::{OverlayWaitOutcome, wait_for_overlay_submission};
use crate::agent::runloop::unified::palettes::{
    ActivePalette, apply_prompt_style, show_mode_palette, show_sessions_palette, show_theme_palette,
};
use crate::agent::runloop::unified::session_setup::{apply_ide_context_snapshot, ide_context_status_label_from_bridge};
use crate::agent::runloop::unified::ui_interaction::PlaceholderSpinner;

#[path = "ui/statusline.rs"]
mod statusline;
#[path = "ui/terminal_title.rs"]
mod terminal_title;

pub(crate) use statusline::handle_start_statusline_setup;
pub(crate) use terminal_title::handle_start_terminal_title_setup;

pub(super) fn ensure_selection_ui_available(ctx: &mut SlashCommandContext<'_>, activity: &str) -> Result<bool> {
    if ctx.model_picker_state.is_some() {
        ctx.renderer
            .line(MessageStyle::Error, &format!("Close the active model picker before {activity}."))?;
        return Ok(false);
    }
    if ctx.palette_state.is_some() {
        ctx.renderer.line(
            MessageStyle::Error,
            "Another selection modal is already open. Press Esc to dismiss it before starting a new one.",
        )?;
        return Ok(false);
    }
    Ok(true)
}

pub(crate) async fn wait_for_list_modal_selection(ctx: &mut SlashCommandContext<'_>) -> Option<InlineListSelection> {
    let outcome: OverlayWaitOutcome<InlineListSelection> =
        wait_for_overlay_submission(ctx.handle, ctx.session, ctx.ctrl_c_state, ctx.ctrl_c_notify, |submission| {
            match submission {
                TransientSubmission::Selection(selection) => Some(selection),
                _ => None,
            }
        })
        .await
        .ok()?;

    close_list_modal(ctx).await;

    match outcome {
        OverlayWaitOutcome::Submitted(selection) => Some(selection),
        OverlayWaitOutcome::Cancelled | OverlayWaitOutcome::Interrupted | OverlayWaitOutcome::Exit => None,
    }
}

pub(crate) async fn close_list_modal(ctx: &mut SlashCommandContext<'_>) {
    ctx.handle.close_modal();
    ctx.handle.force_redraw();
    task::yield_now().await;
}

pub(crate) async fn handle_theme_changed(
    ctx: SlashCommandContext<'_>,
    theme_id: String,
) -> Result<SlashCommandControl> {
    sync_runtime_theme_selection(ctx.config, ctx.vt_cfg.as_mut(), &theme_id);
    persist_theme_preference(ctx.renderer, &ctx.config.workspace, &theme_id).await?;
    let styles = theme::active_styles();
    ctx.handle.set_theme(inline_theme_from_core_styles(&styles));
    apply_prompt_style(ctx.handle);
    ctx.handle.force_redraw();
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_start_theme_palette(
    mut ctx: SlashCommandContext<'_>,
    mode: crate::agent::runloop::slash_commands::ThemePaletteMode,
) -> Result<SlashCommandControl> {
    if !ensure_selection_ui_available(&mut ctx, "selecting a theme")? {
        return Ok(SlashCommandControl::Continue);
    }
    if show_theme_palette(ctx.renderer, mode)? {
        *ctx.palette_state = Some(ActivePalette::Theme { mode, original_theme_id: theme::active_theme_id() });
    }
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_start_session_palette(
    mut ctx: SlashCommandContext<'_>,
    mode: SessionPaletteMode,
    limit: usize,
    show_all: bool,
) -> Result<SlashCommandControl> {
    let activity = match mode {
        SessionPaletteMode::Resume => "browsing sessions",
        SessionPaletteMode::Fork => "selecting a session to fork",
    };
    if !ensure_selection_ui_available(&mut ctx, activity)? {
        return Ok(SlashCommandControl::Continue);
    }
    let scope = if show_all {
        SessionQueryScope::All
    } else {
        SessionQueryScope::CurrentWorkspace(ctx.config.workspace.clone())
    };

    match list_recent_sessions_in_scope(limit, &scope).await {
        Ok(listings) => {
            if show_sessions_palette(ctx.renderer, mode, &listings, limit, show_all)? {
                *ctx.palette_state = Some(ActivePalette::Sessions { mode, listings, limit, show_all });
            }
        }
        Err(err) => {
            ctx.renderer
                .line(MessageStyle::Error, &format!("Failed to load session archives: {err}"))?;
        }
    }
    Ok(SlashCommandControl::Continue)
}

pub(crate) fn handle_continue_latest(_ctx: SlashCommandContext<'_>, show_all: bool) -> Result<SlashCommandControl> {
    // Session existence and loading are handled downstream by the
    // ResumeLatest control flow handler, avoiding a redundant query.
    Ok(SlashCommandControl::ResumeLatest { show_all })
}

pub(crate) async fn handle_start_history_picker(mut ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    if !ctx.renderer.supports_inline_ui() {
        ctx.renderer.line(
            MessageStyle::Info,
            "Command history picker is available in inline UI only. Use /resume for archived sessions.",
        )?;
        return Ok(SlashCommandControl::Continue);
    }

    if !ensure_selection_ui_available(&mut ctx, "opening command history")? {
        return Ok(SlashCommandControl::Continue);
    }

    ctx.handle.show_history_picker();
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_start_file_browser(
    mut ctx: SlashCommandContext<'_>,
    initial_filter: Option<String>,
) -> Result<SlashCommandControl> {
    if !ensure_selection_ui_available(&mut ctx, "opening file browser")? {
        return Ok(SlashCommandControl::Continue);
    }
    // Ensure stale inline modal state cannot overlap with the file palette overlay.
    ctx.handle.close_modal();
    ctx.handle.force_redraw();
    if let Some(filter) = initial_filter {
        ctx.handle.set_input(format!("@{filter}"));
    } else {
        ctx.handle.set_input("@".to_string());
    }
    Ok(SlashCommandControl::Continue)
}

fn config_action_item(
    title: &str,
    subtitle: &str,
    badge: &str,
    indent: u8,
    action: impl Into<String>,
    search_value: impl Into<String>,
) -> InlineListItem {
    InlineListItem {
        title: title.to_string(),
        subtitle: Some(subtitle.to_string()),
        badge: Some(badge.to_string()),
        indent,
        selection: Some(InlineListSelection::ConfigAction(action.into())),
        search_value: Some(search_value.into()),
    }
}

pub(crate) async fn handle_start_model_selection(mut ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    if !ensure_selection_ui_available(&mut ctx, "selecting a model")? {
        return Ok(SlashCommandControl::Continue);
    }

    start_model_picker(ctx).await
}

pub(crate) async fn handle_toggle_ide_context(ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    let enabled = ctx.context_manager.toggle_session_ide_context();

    let latest_editor_snapshot = if let Some(bridge) = ctx.ide_context_bridge.as_mut() {
        match bridge.refresh() {
            Ok((snapshot, _)) => snapshot,
            Err(err) => {
                tracing::warn!(error = %err, "Failed to refresh IDE context while toggling /ide");
                bridge.snapshot().cloned()
            }
        }
    } else {
        None
    };

    apply_ide_context_snapshot(
        ctx.context_manager,
        ctx.header_context,
        ctx.handle,
        ctx.config.workspace.as_path(),
        ctx.vt_cfg.as_ref(),
        latest_editor_snapshot.clone(),
    );

    crate::agent::runloop::unified::status_line::update_ide_context_source(
        ctx.input_status_state,
        ide_context_status_label_from_bridge(
            ctx.context_manager,
            ctx.config.workspace.as_path(),
            ctx.vt_cfg.as_ref(),
            ctx.ide_context_bridge.as_ref(),
        ),
    );

    let message = match (enabled, latest_editor_snapshot.is_some()) {
        (true, true) => "IDE context enabled for this session.",
        (true, false) => "IDE context enabled for this session. No IDE snapshot is currently available.",
        (false, _) => "IDE context disabled for this session.",
    };
    ctx.renderer.line(MessageStyle::Info, message)?;

    Ok(SlashCommandControl::Continue)
}

pub(super) async fn start_model_picker(ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    if ctx.model_picker_state.is_some() {
        ctx.renderer.line(
            MessageStyle::Error,
            "A model picker session is already active. Complete or type 'cancel' to exit it before starting another.",
        )?;
        return Ok(SlashCommandControl::Continue);
    }
    let reasoning = ctx
        .vt_cfg
        .as_ref()
        .map(|cfg| cfg.agent.reasoning_effort)
        .unwrap_or(ctx.config.reasoning_effort);
    let service_tier = ctx.vt_cfg.as_ref().and_then(|cfg| cfg.provider.openai.service_tier);
    let workspace_hint = Some(ctx.config.workspace.clone());
    let restore_status_left = ctx.input_status_state.left.clone();
    let restore_status_right = ctx.input_status_state.right.clone();
    let picker_start = {
        let loading_spinner = if ctx.renderer.supports_inline_ui() {
            Some(PlaceholderSpinner::new(
                ctx.handle,
                restore_status_left.clone(),
                restore_status_right.clone(),
                "Loading model lists...",
            ))
        } else {
            ctx.renderer.line(MessageStyle::Info, "Loading model lists...")?;
            None
        };
        let result = ModelPickerState::new(
            ctx.renderer,
            ctx.vt_cfg.clone(),
            reasoning,
            service_tier,
            workspace_hint,
            ctx.config.provider.clone(),
            ctx.config.model.clone(),
            Some(std::sync::Arc::clone(ctx.ctrl_c_state)),
            Some(std::sync::Arc::clone(ctx.ctrl_c_notify)),
        )
        .await;
        drop(loading_spinner);
        result
    };
    match picker_start {
        Ok(ModelPickerStart::InProgress(picker)) => {
            *ctx.model_picker_state = Some(picker);
        }
        Ok(ModelPickerStart::Completed { state, selection }) => {
            let harness_snapshot = ctx.tool_registry.harness_context_snapshot();
            if let Err(err) = finalize_model_selection(
                ctx.renderer,
                &state,
                selection,
                ctx.config,
                ctx.vt_cfg,
                ctx.provider_client,
                ctx.session_bootstrap,
                ctx.handle,
                ctx.header_context,
                ctx.full_auto,
                ModelSwitchCompactionTargets {
                    history: ctx.conversation_history,
                    session_stats: ctx.session_stats,
                    context_manager: ctx.context_manager,
                    session_id: &harness_snapshot.session_id,
                    thread_id: ctx.thread_id,
                    lifecycle_hooks: ctx.lifecycle_hooks,
                    harness_emitter: ctx.harness_emitter,
                },
            )
            .await
            {
                ctx.renderer
                    .line(MessageStyle::Error, &format!("Failed to apply model selection: {err}"))?;
            }
        }
        Ok(ModelPickerStart::Exit) => {
            *ctx.model_picker_state = None;
        }
        Err(err) => {
            ctx.renderer
                .line(MessageStyle::Error, &format!("Failed to start model picker: {err}"))?;
        }
    }
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_start_mode_palette(mut ctx: SlashCommandContext<'_>) -> Result<SlashCommandControl> {
    if !ensure_selection_ui_available(&mut ctx, "selecting an agent mode")? {
        return Ok(SlashCommandControl::Continue);
    }

    let specs = if let Some(controller) = ctx.tool_registry.subagent_controller() {
        controller.effective_specs().await
    } else {
        match vtcode_config::discover_subagents(&vtcode_config::SubagentDiscoveryInput::new(
            ctx.config.workspace.clone(),
        )) {
            Ok(discovered) => discovered.effective,
            Err(err) => {
                ctx.renderer
                    .line(MessageStyle::Error, &format!("Failed to discover agents: {err}"))?;
                return Ok(SlashCommandControl::Continue);
            }
        }
    };

    let current_name = ctx.active_primary_agent.active().identity.name.clone();
    if show_mode_palette(ctx.renderer, &specs, &current_name)? {
        *ctx.palette_state = Some(ActivePalette::Mode);
    }
    Ok(SlashCommandControl::Continue)
}

pub(crate) async fn handle_select_primary_agent_from_slash(
    _ctx: SlashCommandContext<'_>,
    name: &str,
) -> Result<SlashCommandControl> {
    Ok(SlashCommandControl::SelectAgent(name.to_string()))
}