use std::path::{Component, Path, PathBuf};
pub fn tmp_plans_dir() -> PathBuf {
std::env::temp_dir().join("vtcode-plans")
}
fn tmp_plans_root_normalized() -> String {
tmp_plans_dir().to_string_lossy().replace('\\', "/")
}
fn path_targets_plan_artifact(path: &str) -> bool {
let normalized = path.trim().replace('\\', "/");
if Path::new(&normalized).components().any(|part| part == Component::ParentDir) {
return false;
}
if normalized == ".vtcode/plans"
|| normalized.starts_with(".vtcode/plans/")
|| normalized.contains("/.vtcode/plans/")
{
return true;
}
let tmp_root = tmp_plans_root_normalized();
normalized == tmp_root
|| normalized.starts_with(&format!("{tmp_root}/"))
|| normalized.contains(&format!("{tmp_root}/"))
}
fn path_is_docs_only(path: &str) -> bool {
let normalized = path.trim().replace('\\', "/");
let lower = normalized.to_ascii_lowercase();
let file_name = lower.rsplit('/').next().unwrap_or_default();
if matches!(file_name, "cmakelists.txt" | "requirements.txt") {
return false;
}
let stem = file_name.split('.').next().unwrap_or_default();
if matches!(stem, "readme" | "changelog" | "license") {
let ext = file_name.rsplit('.').next().unwrap_or_default();
if ext == stem || matches!(ext, "md" | "mdx" | "txt" | "rst") {
return true;
}
}
matches!(file_name.rsplit('.').next(), Some("md" | "mdx" | "txt" | "rst"))
}
fn file_mutation_paths(name: &str, args: &serde_json::Value) -> Vec<PathBuf> {
let mut paths = crate::tools::apply_patch::mutation_target_paths(name, args);
if let Some(extra) = args.get("filePath").and_then(|value| value.as_str()) {
let trimmed = extra.trim();
if !trimmed.is_empty() {
paths.push(PathBuf::from(trimmed));
}
}
paths
}
pub fn is_docs_only_write(name: &str, args: &serde_json::Value) -> bool {
use crate::config::constants::tools as tool_names;
use crate::tools::names::canonical_tool_name;
use crate::tools::tool_intent::file_operation_action;
let canonical = canonical_tool_name(name);
let is_file_write = matches!(
canonical,
tool_names::APPLY_PATCH
| tool_names::WRITE_FILE
| tool_names::EDIT_FILE
| tool_names::CREATE_FILE
| tool_names::SEARCH_REPLACE
| tool_names::DELETE_FILE
| tool_names::MOVE_FILE
| tool_names::COPY_FILE
| tool_names::UNIFIED_FILE
);
if !is_file_write {
return false;
}
if canonical == tool_names::UNIFIED_FILE
&& file_operation_action(args)
.map(|action| action.eq_ignore_ascii_case("read"))
.unwrap_or(false)
{
return false;
}
let paths = file_mutation_paths(canonical, args);
if paths.is_empty() {
return false;
}
paths.iter().all(|path| path.to_str().is_some_and(path_is_docs_only))
}
pub fn is_plan_artifact_write(name: &str, args: &serde_json::Value) -> bool {
use crate::config::constants::tools as tool_names;
use crate::tools::names::canonical_tool_name;
use crate::tools::tool_intent::file_operation_action;
let canonical = canonical_tool_name(name);
if canonical == tool_names::TASK_TRACKER {
return true;
}
if !matches!(
canonical,
tool_names::UNIFIED_FILE
| tool_names::WRITE_FILE
| tool_names::EDIT_FILE
| tool_names::CREATE_FILE
| tool_names::SEARCH_REPLACE
) || (canonical == tool_names::UNIFIED_FILE
&& file_operation_action(args).is_some_and(|action| action.eq_ignore_ascii_case("read")))
{
return false;
}
let paths = file_mutation_paths(canonical, args);
!paths.is_empty() && paths.iter().all(|path| path.to_str().is_some_and(path_targets_plan_artifact))
}