vtcode 0.99.1

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

use anyhow::{Context, Result};
use tempfile::Builder as TempFileBuilder;
use toml::Value as TomlValue;
use vtcode_core::config::build_openai_prompt_cache_key;
use vtcode_core::config::loader::ConfigManager;
use vtcode_core::llm::provider::ResponsesCompactionOptions;
use vtcode_core::tools::terminal_app::{EditorLaunchConfig, TerminalAppLauncher};
use vtcode_core::utils::ansi::MessageStyle;

use crate::agent::runloop::slash_commands::CompactConversationCommand;
use crate::agent::runloop::unified::palettes::refresh_runtime_config_from_manager;

use super::apps::run_with_event_loop_suspended;
use super::config_toml::{
    ensure_child_table, load_toml_value, preferred_workspace_config_path, save_toml_value,
};
use super::{SlashCommandContext, SlashCommandControl};

pub(crate) async fn handle_compact_conversation(
    mut ctx: SlashCommandContext<'_>,
    command: CompactConversationCommand,
) -> Result<SlashCommandControl> {
    match command {
        CompactConversationCommand::Run { options } => {
            if !manual_openai_compaction_available(&mut ctx)? {
                return Ok(SlashCommandControl::Continue);
            }
            execute_manual_compaction(&mut ctx, options).await
        }
        CompactConversationCommand::EditDefaultPrompt => {
            edit_default_prompt(&mut ctx).await?;
            Ok(SlashCommandControl::Continue)
        }
        CompactConversationCommand::ResetDefaultPrompt => {
            reset_default_prompt(&mut ctx).await?;
            Ok(SlashCommandControl::Continue)
        }
    }
}

async fn edit_default_prompt(ctx: &mut SlashCommandContext<'_>) -> Result<()> {
    let editor_config = ctx
        .vt_cfg
        .as_ref()
        .map(|config| config.tools.editor.clone())
        .unwrap_or_default();
    if !editor_config.enabled {
        ctx.renderer.line(
            MessageStyle::Warning,
            "External editor is disabled (`tools.editor.enabled = false`).",
        )?;
        return Ok(());
    }

    let initial_prompt = current_default_prompt(ctx).unwrap_or_default();
    let temp_file = TempFileBuilder::new()
        .prefix("vtcode-compact-prompt-")
        .suffix(".md")
        .tempfile_in(&ctx.config.workspace)
        .context("Failed to create temporary prompt file")?;
    fs::write(temp_file.path(), initial_prompt).context("Failed to seed temporary prompt file")?;

    let launcher = TerminalAppLauncher::new(ctx.config.workspace.clone());
    let launch_config = EditorLaunchConfig {
        preferred_editor: if editor_config.preferred_editor.trim().is_empty() {
            None
        } else {
            Some(editor_config.preferred_editor.clone())
        },
        wait_for_editor: true,
    };
    let temp_path = temp_file.path().to_path_buf();

    run_with_event_loop_suspended(ctx.handle, editor_config.suspend_tui, || {
        launcher.launch_editor_with_config(Some(temp_path.clone()), launch_config)
    })
    .await
    .context("Failed to launch editor")?;

    let edited =
        fs::read_to_string(temp_file.path()).context("Failed to read edited compaction prompt")?;
    persist_default_prompt(ctx, trimmed_optional(edited)).await?;
    ctx.renderer.line(
        MessageStyle::Info,
        "Saved workspace default manual compaction prompt.",
    )?;
    Ok(())
}

async fn reset_default_prompt(ctx: &mut SlashCommandContext<'_>) -> Result<()> {
    persist_default_prompt(ctx, None).await?;
    ctx.renderer.line(
        MessageStyle::Info,
        "Reset workspace default manual compaction prompt.",
    )?;
    Ok(())
}

async fn persist_default_prompt(
    ctx: &mut SlashCommandContext<'_>,
    value: Option<String>,
) -> Result<()> {
    let manager = ConfigManager::load_from_workspace(&ctx.config.workspace)
        .context("Failed to load VT Code configuration")?;
    let workspace_config_path = preferred_workspace_config_path(&manager, &ctx.config.workspace);
    let mut root = load_toml_value(&workspace_config_path)?;
    let root_table = root
        .as_table_mut()
        .context("Workspace config root is not a TOML table")?;
    set_manual_compaction_prompt(root_table, value);
    save_toml_value(&workspace_config_path, &root)?;
    refresh_runtime_config_from_manager(
        ctx.renderer,
        ctx.handle,
        ctx.config,
        ctx.vt_cfg,
        ctx.provider_client.as_ref(),
        ctx.session_bootstrap,
        ctx.full_auto,
    )
    .await
}

async fn execute_manual_compaction(
    ctx: &mut SlashCommandContext<'_>,
    options: ResponsesCompactionOptions,
) -> Result<SlashCommandControl> {
    if ctx.conversation_history.is_empty() {
        ctx.renderer
            .line(MessageStyle::Info, "No conversation history to compact.")?;
        return Ok(SlashCommandControl::Continue);
    }

    let resolved_options = resolve_manual_compaction_options(ctx, options);
    let harness_snapshot = ctx.tool_registry.harness_context_snapshot();
    let outcome =
        crate::agent::runloop::unified::turn::compaction::manual_openai_compact_history_in_place(
            crate::agent::runloop::unified::turn::compaction::CompactionContext::new(
                ctx.provider_client.as_ref(),
                &ctx.config.model,
                &harness_snapshot.session_id,
                ctx.thread_id,
                &ctx.config.workspace,
                ctx.vt_cfg.as_ref(),
                ctx.lifecycle_hooks,
                ctx.harness_emitter,
            ),
            crate::agent::runloop::unified::turn::compaction::CompactionState::new(
                ctx.conversation_history,
                ctx.session_stats,
                ctx.context_manager,
            ),
            &resolved_options,
        )
        .await;

    let outcome = match outcome {
        Ok(outcome) => outcome,
        Err(err) => {
            ctx.renderer
                .line(MessageStyle::Error, &format!("Compaction failed: {}", err))?;
            return Ok(SlashCommandControl::Continue);
        }
    };

    let Some(outcome) = outcome else {
        ctx.renderer
            .line(MessageStyle::Info, "Conversation is already compact.")?;
        return Ok(SlashCommandControl::Continue);
    };

    ctx.renderer.line(
        MessageStyle::Info,
        &format!(
            "Compacted conversation history ({} -> {} messages).",
            outcome.original_len, outcome.compacted_len
        ),
    )?;
    Ok(SlashCommandControl::Continue)
}

fn resolve_manual_compaction_options(
    ctx: &SlashCommandContext<'_>,
    options: ResponsesCompactionOptions,
) -> ResponsesCompactionOptions {
    let default_prompt = current_default_prompt(ctx);
    let prompt_cache_key = options
        .prompt_cache_key
        .clone()
        .or_else(|| default_openai_prompt_cache_key(ctx));

    ResponsesCompactionOptions {
        instructions: options
            .instructions
            .and_then(trimmed_optional)
            .or(default_prompt),
        max_output_tokens: options.max_output_tokens,
        reasoning_effort: options.reasoning_effort,
        verbosity: options.verbosity,
        responses_include: options
            .responses_include
            .map(|values| {
                values
                    .into_iter()
                    .filter_map(trimmed_optional)
                    .collect::<Vec<_>>()
            })
            .filter(|values| !values.is_empty()),
        response_store: options.response_store,
        service_tier: options.service_tier.and_then(trimmed_optional),
        prompt_cache_key,
    }
}

fn default_openai_prompt_cache_key(ctx: &SlashCommandContext<'_>) -> Option<String> {
    let prompt_cache = &ctx.config.prompt_cache;
    build_openai_prompt_cache_key(
        prompt_cache.enabled && prompt_cache.providers.openai.enabled,
        &prompt_cache.providers.openai.prompt_cache_key_mode,
        ctx.session_stats.prompt_cache_lineage_id(),
    )
}

fn current_default_prompt(ctx: &SlashCommandContext<'_>) -> Option<String> {
    ctx.vt_cfg
        .as_ref()
        .and_then(|cfg| cfg.provider.openai.manual_compaction.instructions.clone())
        .and_then(trimmed_optional)
}

fn manual_openai_compaction_available(ctx: &mut SlashCommandContext<'_>) -> Result<bool> {
    if ctx
        .provider_client
        .supports_manual_openai_compaction(&ctx.config.model)
    {
        return Ok(true);
    }

    let message = ctx
        .provider_client
        .manual_openai_compaction_unavailable_message(&ctx.config.model);
    ctx.renderer.line(MessageStyle::Error, &message)?;
    Ok(false)
}

fn trimmed_optional(value: String) -> Option<String> {
    let trimmed = value.trim().to_string();
    (!trimmed.is_empty()).then_some(trimmed)
}

fn set_manual_compaction_prompt(
    root_table: &mut toml::map::Map<String, TomlValue>,
    value: Option<String>,
) {
    match value {
        Some(value) => {
            let provider_table = ensure_child_table(root_table, "provider");
            let openai_table = ensure_child_table(provider_table, "openai");
            let manual_table = ensure_child_table(openai_table, "manual_compaction");
            manual_table.insert("instructions".to_string(), TomlValue::String(value));
        }
        None => {
            let remove_openai_table = {
                let provider_table = ensure_child_table(root_table, "provider");
                let openai_table = ensure_child_table(provider_table, "openai");
                let remove_manual_table = {
                    let manual_table = ensure_child_table(openai_table, "manual_compaction");
                    manual_table.remove("instructions");
                    manual_table.is_empty()
                };
                if remove_manual_table {
                    openai_table.remove("manual_compaction");
                }
                openai_table.is_empty()
            };
            if remove_openai_table {
                let remove_provider_table = {
                    let provider_table = ensure_child_table(root_table, "provider");
                    provider_table.remove("openai");
                    provider_table.is_empty()
                };
                if remove_provider_table {
                    root_table.remove("provider");
                }
            }
        }
    }
}