use anyhow::{Context, Result};
use std::path::Path;
use crate::component;
use crate::snapshot;
fn signal_editor_refresh(file: &Path) {
let canonical = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
if let Some(root) = snapshot::find_project_root(&canonical) {
if crate::ipc_socket::is_listener_active(&root)
&& crate::ipc_socket::send_vcs_refresh(&root).unwrap_or(false)
{
return;
}
let signal = root.join(".agent-doc/patches/vcs-refresh.signal");
if signal.parent().is_some_and(|p| p.exists()) {
let _ = std::fs::write(&signal, "boundary-refresh");
}
}
}
pub const BOUNDARY_PREFIX: &str = "<!-- agent:boundary:";
pub const BOUNDARY_SUFFIX: &str = " -->";
pub fn new_id() -> String {
agent_doc::new_boundary_id()
}
pub fn format_marker(id: &str) -> String {
format!("{}{}{}", BOUNDARY_PREFIX, id, BOUNDARY_SUFFIX)
}
#[allow(dead_code)]
pub fn extract_id(marker: &str) -> Option<&str> {
let trimmed = marker.trim();
trimmed
.strip_prefix(BOUNDARY_PREFIX)
.and_then(|rest| rest.strip_suffix(BOUNDARY_SUFFIX))
.map(|id| id.trim())
}
#[allow(dead_code)]
pub fn find_in_component(doc: &str, comp: &component::Component, boundary_id: &str) -> Option<(usize, usize)> {
let content_region = &doc[comp.open_end..comp.close_start];
let marker = format_marker(boundary_id);
if let Some(rel_pos) = content_region.find(&marker) {
let abs_pos = comp.open_end + rel_pos;
let line_start = doc[..abs_pos]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(comp.open_end)
.max(comp.open_end);
let marker_end = abs_pos + marker.len();
let line_end = if marker_end < comp.close_start && doc.as_bytes().get(marker_end) == Some(&b'\n') {
marker_end + 1
} else {
marker_end
};
Some((line_start, line_end.min(comp.close_start)))
} else {
None
}
}
#[allow(dead_code)]
pub fn find_boundary_id_in_component(doc: &str, comp: &component::Component) -> Option<String> {
let content_region = &doc[comp.open_end..comp.close_start];
let code_ranges = component::find_code_ranges(doc);
let mut search_from = 0;
while let Some(start) = content_region[search_from..].find(BOUNDARY_PREFIX) {
let abs_start = comp.open_end + search_from + start;
if code_ranges.iter().any(|&(cs, ce)| abs_start >= cs && abs_start < ce) {
search_from += start + BOUNDARY_PREFIX.len();
continue;
}
let after_prefix = &content_region[search_from + start + BOUNDARY_PREFIX.len()..];
if let Some(end) = after_prefix.find(BOUNDARY_SUFFIX) {
let id = &after_prefix[..end];
return Some(id.trim().to_string());
}
break;
}
None
}
pub fn insert(doc: &str, component_name: &str) -> Result<(String, String)> {
let cleaned = remove_all(doc);
let stale_count = doc.matches(BOUNDARY_PREFIX).count();
if stale_count > 0 {
eprintln!(
"[boundary] removed {} stale boundary marker(s) before inserting new one",
stale_count
);
}
let components = component::parse(&cleaned)?;
let comp = components
.iter()
.find(|c| c.name == component_name)
.ok_or_else(|| anyhow::anyhow!("component '{}' not found", component_name))?;
let id = new_id();
let marker = format_marker(&id);
let mut result = String::with_capacity(cleaned.len() + marker.len() + 2);
let content = &cleaned[comp.open_end..comp.close_start];
result.push_str(&cleaned[..comp.open_end]);
result.push_str(content.trim_end());
result.push('\n');
result.push_str(&marker);
result.push('\n');
result.push_str(&cleaned[comp.close_start..]);
Ok((id, result))
}
#[allow(dead_code)]
pub fn remove(doc: &str, boundary_id: &str) -> String {
let marker_line = format!("{}\n", format_marker(boundary_id));
doc.replace(&marker_line, "")
}
pub fn remove_all(doc: &str) -> String {
let mut result = String::with_capacity(doc.len());
for line in doc.lines() {
if line.trim().starts_with(BOUNDARY_PREFIX) && line.trim().ends_with(BOUNDARY_SUFFIX) {
continue;
}
result.push_str(line);
result.push('\n');
}
if !doc.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
result
}
pub fn run(file: &Path, component: Option<&str>) -> Result<()> {
let component_name = component.unwrap_or("exchange");
let content = std::fs::read_to_string(file)
.with_context(|| format!("failed to read {}", file.display()))?;
let (id, updated) = insert(&content, component_name)?;
let tmp = file.with_extension("boundary.tmp");
std::fs::write(&tmp, &updated)
.with_context(|| format!("failed to write temp file {}", tmp.display()))?;
std::fs::rename(&tmp, file)
.with_context(|| format!("failed to rename {} to {}", tmp.display(), file.display()))?;
snapshot::save(file, &updated)?;
signal_editor_refresh(file);
println!("{}", id);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_and_extract() {
let id = "abc-123";
let marker = format_marker(id);
assert_eq!(marker, "<!-- agent:boundary:abc-123 -->");
assert_eq!(extract_id(&marker), Some("abc-123"));
}
#[test]
fn insert_at_end_of_component() {
let doc = "before\n<!-- agent:exchange -->\nsome content\n<!-- /agent:exchange -->\nafter";
let (id, result) = insert(doc, "exchange").unwrap();
let marker = format_marker(&id);
assert!(result.contains(&marker));
assert!(result.contains("some content"));
let marker_pos = result.find(&marker).unwrap();
let close_pos = result.find("<!-- /agent:exchange -->").unwrap();
assert!(marker_pos < close_pos);
}
#[test]
fn find_boundary_in_component() {
let id = "test-uuid";
let doc = format!(
"<!-- agent:exchange -->\ncontent\n{}\n<!-- /agent:exchange -->",
format_marker(id)
);
let components = component::parse(&doc).unwrap();
let comp = &components[0];
let result = find_in_component(&doc, comp, id);
assert!(result.is_some());
}
#[test]
fn remove_boundary() {
let id = "test-uuid";
let doc = format!(
"<!-- agent:exchange -->\ncontent\n{}\n<!-- /agent:exchange -->",
format_marker(id)
);
let cleaned = remove(&doc, id);
assert!(!cleaned.contains("agent:boundary"));
assert!(cleaned.contains("content"));
}
#[test]
fn remove_all_boundaries() {
let doc = "line1\n<!-- agent:boundary:aaa -->\nline2\n<!-- agent:boundary:bbb -->\nline3\n";
let cleaned = remove_all(doc);
assert_eq!(cleaned, "line1\nline2\nline3\n");
}
#[test]
fn insert_cleans_stale_markers() {
let doc = concat!(
"<!-- agent:exchange -->\n",
"some content\n",
"<!-- agent:boundary:stale-1 -->\n",
"more content\n",
"<!-- agent:boundary:stale-2 -->\n",
"<!-- /agent:exchange -->\n",
);
let (new_id, result) = insert(doc, "exchange").unwrap();
assert!(!result.contains("stale-1"), "stale marker 1 should be removed");
assert!(!result.contains("stale-2"), "stale marker 2 should be removed");
let new_marker = format_marker(&new_id);
assert!(result.contains(&new_marker), "new marker should be present");
assert!(result.contains("some content"));
assert!(result.contains("more content"));
let marker_count = result.matches(BOUNDARY_PREFIX).count();
assert_eq!(marker_count, 1, "should have exactly one boundary marker");
}
}