use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::session::VisionSessionManager;
pub fn spawn_ghost_writer(
session: Arc<Mutex<VisionSessionManager>>,
) -> Option<tokio::task::JoinHandle<()>> {
let clients = detect_all_memory_dirs();
if clients.is_empty() {
tracing::info!("Ghost Writer (Vision): no AI coding assistants detected. Sync disabled.");
return None;
}
for c in &clients {
tracing::info!("Ghost Writer (Vision): {} detected at {:?}", c.name, c.dir);
}
let client_count = clients.len();
let handle = tokio::spawn(async move {
sync_once(&session, &clients).await;
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
interval.tick().await; loop {
interval.tick().await;
sync_once(&session, &clients).await;
}
});
tracing::info!(
"Ghost Writer (Vision): background sync started ({} clients, 5s interval)",
client_count
);
Some(handle)
}
async fn sync_once(session: &Arc<Mutex<VisionSessionManager>>, clients: &[ClientDir]) {
let markdown = build_vision_context(session).await;
for client in clients {
let target = client.dir.join(&client.filename);
if let Err(e) = atomic_write(&target, markdown.as_bytes()) {
tracing::warn!(
"Ghost Writer (Vision): failed to sync to {:?}: {}",
target,
e
);
}
}
}
async fn build_vision_context(session: &Arc<Mutex<VisionSessionManager>>) -> String {
let session = session.lock().await;
let session_id = session.current_session_id();
let capture_count = session.store().count();
let observations = session.observation_notes();
let tool_log = session.tool_call_log();
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let mut md = String::new();
md.push_str("# AgenticVision Context\n\n");
md.push_str(&format!("> Auto-synced by Ghost Writer at {now}\n\n"));
md.push_str(&format!(
"**Session:** `{session_id}` | **Captures:** {capture_count}\n\n"
));
if !observations.is_empty() {
md.push_str("## Recent Observations\n\n");
for note in observations.iter().rev().take(10) {
md.push_str(&format!(
"- **[{}]** {}",
note.topic.as_deref().unwrap_or("general"),
note.intent
));
if let Some(obs) = ¬e.observation {
let preview = if obs.len() > 150 {
format!("{}...", &obs[..150])
} else {
obs.clone()
};
md.push_str(&format!(" — {preview}"));
}
md.push('\n');
}
md.push('\n');
}
if !tool_log.is_empty() {
md.push_str("## Recent Tool Calls\n\n");
for record in tool_log.iter().rev().take(10) {
let summary = if record.summary.len() > 120 {
format!("{}...", &record.summary[..120])
} else {
record.summary.clone()
};
md.push_str(&format!("- `{}` — {summary}\n", record.tool_name));
}
md.push('\n');
}
md.push_str("---\n");
md.push_str("_Auto-generated by AgenticVision. Do not edit manually._\n");
md
}
struct ClientDir {
name: &'static str,
dir: PathBuf,
filename: String,
}
fn detect_all_memory_dirs() -> Vec<ClientDir> {
let home = match std::env::var("HOME").ok().map(PathBuf::from) {
Some(h) => h,
None => return vec![],
};
let candidates = [
(
"Claude Code",
home.join(".claude").join("memory"),
"VISION_CONTEXT.md",
),
(
"Cursor",
home.join(".cursor").join("memory"),
"agentic-vision.md",
),
(
"Windsurf",
home.join(".windsurf").join("memory"),
"agentic-vision.md",
),
(
"Cody",
home.join(".sourcegraph").join("cody").join("memory"),
"agentic-vision.md",
),
];
let mut dirs = Vec::new();
for (name, memory_dir, filename) in &candidates {
if create_if_parent_exists(memory_dir) {
dirs.push(ClientDir {
name,
dir: memory_dir.clone(),
filename: filename.to_string(),
});
}
}
dirs
}
fn create_if_parent_exists(memory_dir: &Path) -> bool {
if memory_dir.exists() {
return true;
}
if let Some(parent) = memory_dir.parent() {
if parent.exists() {
return std::fs::create_dir_all(memory_dir).is_ok();
}
}
false
}
fn atomic_write(target: &Path, content: &[u8]) -> Result<(), std::io::Error> {
use std::io::Write;
let tmp = target.with_extension("tmp");
let mut f = std::fs::File::create(&tmp)?;
f.write_all(content)?;
f.sync_all()?;
std::fs::rename(&tmp, target)?;
Ok(())
}