vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use serde_json::Value;
use vtcode_core::tools::edited_file_monitor::{
    FILE_CONFLICT_DETECTED_FIELD, FILE_CONFLICT_PATH_FIELD,
};
use vtcode_core::tools::tool_intent::is_edited_file_conflict_guarded_call;

use super::status::ToolExecutionStatus;

#[derive(Clone, Debug)]
pub(super) struct PendingFileConflictStatus {
    pub(super) output: Value,
    pub(super) display_path: String,
    pub(super) message: String,
    pub(super) approved_snapshot: Option<Value>,
    pub(super) disk_content: Option<String>,
    pub(super) intended_content: Option<String>,
    pub(super) emit_hitl_notification: bool,
}

impl PendingFileConflictStatus {
    pub(super) fn from_output(output: Value) -> Option<Self> {
        if output
            .get(FILE_CONFLICT_DETECTED_FIELD)
            .and_then(Value::as_bool)
            != Some(true)
        {
            return None;
        }

        let display_path = output
            .get(FILE_CONFLICT_PATH_FIELD)
            .and_then(Value::as_str)?
            .to_string();
        let message = output
            .get("message")
            .and_then(Value::as_str)
            .unwrap_or("File changed on disk since the agent last read it.")
            .to_string();
        let approved_snapshot = output
            .get("disk_snapshot")
            .cloned()
            .filter(Value::is_object);
        let disk_content = output
            .get("disk_content")
            .and_then(Value::as_str)
            .map(ToOwned::to_owned);
        let intended_content = output
            .get("intended_content")
            .and_then(Value::as_str)
            .map(ToOwned::to_owned);
        let emit_hitl_notification = output
            .get("emit_hitl_notification")
            .and_then(Value::as_bool)
            .unwrap_or(false);

        Some(Self {
            output,
            display_path,
            message,
            approved_snapshot,
            disk_content,
            intended_content,
            emit_hitl_notification,
        })
    }
}

#[derive(Debug)]
pub(super) enum RuntimeToolExecution {
    Completed(ToolExecutionStatus),
    PendingFileConflict(PendingFileConflictStatus),
}

pub(super) fn into_runtime_tool_execution(
    name: &str,
    args: &Value,
    status: ToolExecutionStatus,
) -> RuntimeToolExecution {
    if !is_edited_file_conflict_guarded_call(name, args) {
        return RuntimeToolExecution::Completed(status);
    }

    match status {
        ToolExecutionStatus::Success {
            output,
            stdout,
            modified_files,
            command_success,
        } => {
            if output
                .get(FILE_CONFLICT_DETECTED_FIELD)
                .and_then(Value::as_bool)
                == Some(true)
                && let Some(conflict) = PendingFileConflictStatus::from_output(output.clone())
            {
                return RuntimeToolExecution::PendingFileConflict(conflict);
            }

            RuntimeToolExecution::Completed(ToolExecutionStatus::Success {
                output,
                stdout,
                modified_files,
                command_success,
            })
        }
        other => RuntimeToolExecution::Completed(other),
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;
    use vtcode_core::config::constants::tools;

    use super::{RuntimeToolExecution, into_runtime_tool_execution};
    use crate::agent::runloop::unified::tool_pipeline::ToolExecutionStatus;

    fn success(output: serde_json::Value) -> ToolExecutionStatus {
        ToolExecutionStatus::Success {
            output,
            stdout: None,
            modified_files: vec![],
            command_success: true,
        }
    }

    #[test]
    fn unrelated_tools_do_not_enter_file_conflict_flow() {
        let output = json!({
            "success": true,
            "conflict_detected": true,
            "conflict_path": "sample.txt"
        });

        let wrapped = into_runtime_tool_execution(
            tools::READ_FILE,
            &json!({"path": "sample.txt"}),
            success(output.clone()),
        );

        match wrapped {
            RuntimeToolExecution::Completed(ToolExecutionStatus::Success {
                output: observed,
                ..
            }) => assert_eq!(observed, output),
            other => panic!("unexpected runtime execution: {other:?}"),
        }
    }

    #[test]
    fn unified_file_read_bypasses_file_conflict_flow() {
        let output = json!({
            "success": true,
            "conflict_detected": true,
            "conflict_path": "sample.txt"
        });

        let wrapped = into_runtime_tool_execution(
            tools::UNIFIED_FILE,
            &json!({"action": "read", "path": "sample.txt"}),
            success(output.clone()),
        );

        match wrapped {
            RuntimeToolExecution::Completed(ToolExecutionStatus::Success {
                output: observed,
                ..
            }) => assert_eq!(observed, output),
            other => panic!("unexpected runtime execution: {other:?}"),
        }
    }

    #[test]
    fn create_file_conflicts_are_wrapped() {
        let wrapped = into_runtime_tool_execution(
            tools::CREATE_FILE,
            &json!({"path": "sample.txt", "content": "agent"}),
            success(json!({
                "success": true,
                "conflict_detected": true,
                "conflict_path": "sample.txt",
                "disk_content": "external\n",
                "intended_content": "agent\n",
            })),
        );

        match wrapped {
            RuntimeToolExecution::PendingFileConflict(conflict) => {
                assert_eq!(conflict.display_path, "sample.txt");
                assert_eq!(conflict.disk_content.as_deref(), Some("external\n"));
                assert_eq!(conflict.intended_content.as_deref(), Some("agent\n"));
            }
            other => panic!("unexpected runtime execution: {other:?}"),
        }
    }
}