vtcode 0.136.4

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 file_operation_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:?}"),
        }
    }
}