vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::Result;
use vtcode_core::config::constants::output_limits;
use vtcode_core::hooks::{HookMessage, HookMessageLevel};
use vtcode_core::llm::provider as uni;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};

pub(crate) fn render_hook_messages(
    renderer: &mut AnsiRenderer,
    messages: &[HookMessage],
) -> Result<()> {
    for message in messages {
        let text = message.text.trim();
        if text.is_empty() {
            continue;
        }

        let style = match message.level {
            HookMessageLevel::Info => MessageStyle::Info,
            HookMessageLevel::Warning => MessageStyle::Info,
            HookMessageLevel::Error => MessageStyle::Error,
        };

        renderer.line(style, text)?;
    }

    Ok(())
}

pub(crate) fn append_additional_context(
    history: &mut Vec<uni::Message>,
    additional_context: Vec<String>,
) {
    for context in additional_context {
        if !context.trim().is_empty() {
            history.push(uni::Message::system(context));
        }
    }
}

pub(crate) fn truncate_message_content(content: &str) -> String {
    let mut result =
        String::with_capacity(content.len().min(output_limits::MAX_AGENT_MESSAGES_SIZE));
    let mut truncated = false;

    for line in content.lines() {
        let mut line_bytes = 0;
        let mut end = 0;
        for (idx, ch) in line.char_indices() {
            let ch_len = ch.len_utf8();
            if line_bytes + ch_len > output_limits::MAX_LINE_LENGTH {
                truncated = true;
                break;
            }
            line_bytes += ch_len;
            end = idx + ch_len;
        }
        let trimmed_line = &line[..end];
        if result.len() + trimmed_line.len() + 1 > output_limits::MAX_AGENT_MESSAGES_SIZE {
            truncated = true;
            break;
        }
        result.push_str(trimmed_line);
        result.push('\n');
    }

    if truncated {
        result.push_str("[... content truncated due to size limit ...]");
    }

    result
}

pub(crate) fn enforce_history_limits(history: &mut Vec<uni::Message>) {
    let max_messages = output_limits::DEFAULT_MESSAGE_LIMIT.min(output_limits::MAX_MESSAGE_LIMIT);
    while history.len() > max_messages {
        if !remove_oldest_non_system(history) {
            break;
        }
    }

    loop {
        let total_bytes: usize = history.iter().map(|msg| msg.content.as_text().len()).sum();
        if total_bytes <= output_limits::MAX_ALL_MESSAGES_SIZE {
            break;
        }
        if !remove_oldest_non_system(history) {
            break;
        }
    }
}

fn remove_oldest_non_system(history: &mut Vec<uni::Message>) -> bool {
    if history.is_empty() {
        return false;
    }
    if history[0].role != uni::MessageRole::System {
        history.remove(0);
        return true;
    }
    if history.len() > 1 {
        history.remove(1);
        return true;
    }
    false
}
const UNLIMITED_TOOL_LOOP_BALANCER_WINDOW: usize = 20;

pub(crate) fn should_trigger_turn_balancer(
    step_count: usize,
    max_tool_loops: usize,
    repeated: usize,
    repeat_limit: usize,
) -> bool {
    let loop_window = if max_tool_loops == usize::MAX {
        UNLIMITED_TOOL_LOOP_BALANCER_WINDOW
    } else {
        max_tool_loops
    };
    let step_threshold = loop_window.saturating_mul(3) / 4;
    let effective_repeat_limit = repeat_limit.max(3);
    step_count > step_threshold && repeated >= effective_repeat_limit
}

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

    #[test]
    fn balancer_triggers_after_three_quarters_and_effective_repeat_limit() {
        assert!(should_trigger_turn_balancer(16, 20, 3, 3));
        assert!(!should_trigger_turn_balancer(15, 20, 3, 3));
        assert!(!should_trigger_turn_balancer(16, 20, 2, 3));
        assert!(!should_trigger_turn_balancer(16, 20, 2, 2));
    }

    #[test]
    fn balancer_uses_fallback_window_for_unlimited_loop_limit() {
        assert!(should_trigger_turn_balancer(16, usize::MAX, 3, 3));
        assert!(!should_trigger_turn_balancer(15, usize::MAX, 3, 3));
    }

    #[test]
    fn truncate_message_content_limits_lines_and_size() {
        let long_line = "a".repeat(output_limits::MAX_LINE_LENGTH + 16);
        let truncated = truncate_message_content(&long_line);

        assert!(truncated.contains("content truncated"));
        assert!(truncated.len() <= output_limits::MAX_AGENT_MESSAGES_SIZE);
    }

    #[test]
    fn enforce_history_limits_caps_message_count_and_keeps_system() {
        let mut history = Vec::new();
        history.push(uni::Message::system("system".to_string()));
        for idx in 0..(output_limits::DEFAULT_MESSAGE_LIMIT + 1) {
            history.push(uni::Message::assistant(format!("msg {}", idx)));
        }

        enforce_history_limits(&mut history);

        assert!(history.len() <= output_limits::DEFAULT_MESSAGE_LIMIT);
        assert_eq!(
            history.first().map(|m| m.role.clone()),
            Some(uni::MessageRole::System)
        );
    }

    #[test]
    fn append_additional_context_skips_empty_entries() {
        let mut history = vec![uni::Message::user("prompt".to_string())];

        append_additional_context(
            &mut history,
            vec![
                "keep me".to_string(),
                "   ".to_string(),
                "also keep me".to_string(),
            ],
        );

        assert_eq!(history.len(), 3);
        assert_eq!(history[1].role, uni::MessageRole::System);
        assert_eq!(history[1].get_text_content().as_ref(), "keep me");
        assert_eq!(history[2].role, uni::MessageRole::System);
        assert_eq!(history[2].get_text_content().as_ref(), "also keep me");
    }
}