use anyhow::Result;
use crate::sessions::{self, Tmux};
use crate::sync;
pub fn run() -> Result<()> {
run_with_tmux(&Tmux::default_server())
}
pub fn run_with_tmux(tmux: &Tmux) -> Result<()> {
let pane_id = match sessions::current_pane() {
Ok(p) => p,
Err(_) => {
return Ok(());
}
};
let mut registry = sessions::load()?;
let all_claimed: Vec<(String, sessions::SessionEntry)> = registry
.iter()
.filter(|(_, entry)| entry.pane == pane_id)
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if all_claimed.is_empty() {
eprintln!("[autoclaim] No files claimed for pane {}", pane_id);
return Ok(());
}
let mut stale_ids: Vec<String> = Vec::new();
let mut claimed: Vec<(String, sessions::SessionEntry)> = Vec::new();
for (session_id, entry) in all_claimed {
if std::path::Path::new(&entry.file).exists() {
claimed.push((session_id, entry));
} else {
eprintln!(
"[autoclaim] Pruning stale claim: {} (file no longer exists)",
entry.file
);
stale_ids.push(session_id);
}
}
if !stale_ids.is_empty() {
for id in &stale_ids {
registry.remove(id);
}
if let Err(e) = sessions::save(®istry) {
eprintln!("[autoclaim] Failed to save pruned registry: {}", e);
}
}
if claimed.is_empty() {
eprintln!("[autoclaim] All claims for pane {} were stale (files moved/deleted)", pane_id);
return Ok(());
}
for (session_id, entry) in &claimed {
eprintln!(
"[autoclaim] Pane {} has file {} (session {})",
pane_id,
entry.file,
&session_id[..8.min(session_id.len())]
);
}
if let Err(e) = tmux.select_pane(&pane_id) {
eprintln!("[autoclaim] Failed to focus pane {}: {}", pane_id, e);
}
let claimed_refs: Vec<(&String, &sessions::SessionEntry)> = claimed
.iter()
.map(|(k, v)| (k, v))
.collect();
sync_after_autoclaim(tmux, &pane_id, &claimed_refs);
for (_, entry) in &claimed {
println!(
"This pane ({}) has an active agent-doc claim on: {}",
pane_id, entry.file
);
println!(
"To re-establish the claim, run: /agent-doc claim {}",
entry.file
);
}
Ok(())
}
fn sync_after_autoclaim(
tmux: &Tmux,
pane_id: &str,
_claimed: &[(&String, &sessions::SessionEntry)],
) {
let window_id = match tmux.pane_window(pane_id) {
Ok(w) => w,
Err(_) => return,
};
let registry = match sessions::load() {
Ok(r) => r,
Err(_) => return,
};
let window_files: Vec<String> = registry
.values()
.filter(|entry| {
!entry.pane.is_empty()
&& tmux.pane_alive(&entry.pane)
&& tmux.pane_window(&entry.pane).ok().as_deref() == Some(&window_id)
&& !entry.file.is_empty()
})
.map(|entry| entry.file.clone())
.collect();
if window_files.len() < 2 {
return; }
let file_count = window_files.len();
let col_args: Vec<String> = window_files;
if let Err(e) = sync::run_with_tmux(&col_args, Some(&window_id), None, tmux) {
eprintln!("[autoclaim] warning: post-claim sync failed: {}", e);
} else {
eprintln!(
"[autoclaim] Auto-synced {} files in window {}",
file_count,
window_id
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sessions::{IsolatedTmux, SessionEntry, SessionRegistry};
use tempfile::TempDir;
fn setup_registry(dir: &std::path::Path, pane_id: &str) {
let mut reg = SessionRegistry::new();
reg.insert(
"test-session-1234".to_string(),
SessionEntry {
pane: pane_id.to_string(),
pid: std::process::id(),
cwd: dir.to_string_lossy().to_string(),
started: "2026-01-01T00:00:00Z".to_string(),
file: "tasks/test.md".to_string(),
window: String::new(),
},
);
let sessions_dir = dir.join(".agent-doc");
std::fs::create_dir_all(&sessions_dir).unwrap();
let sessions_path = sessions_dir.join("sessions.json");
let content = serde_json::to_string_pretty(®).unwrap();
std::fs::write(sessions_path, content).unwrap();
}
#[test]
#[ignore] fn autoclaim_focuses_pane_with_claim() {
let iso = IsolatedTmux::new("agent-doc-test-autoclaim-focus");
let dir = TempDir::new().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
let pane_id = iso.new_session("test", dir.path()).unwrap();
setup_registry(dir.path(), &pane_id);
unsafe { std::env::set_var("TMUX_PANE", &pane_id) };
let pane2 = iso.new_window("test", dir.path()).unwrap();
iso.select_pane(&pane2).unwrap();
let result = run_with_tmux(&iso);
assert!(result.is_ok(), "autoclaim should succeed: {:?}", result);
let active = iso.active_pane("test").expect("should have active pane");
assert_eq!(active, pane_id, "autoclaim should have focused the claimed pane");
unsafe { std::env::remove_var("TMUX_PANE") };
}
fn setup_multi_file_registry(
dir: &std::path::Path,
entries: &[(&str, &str, &str)], ) {
let mut reg = SessionRegistry::new();
for (session_id, pane_id, file) in entries {
reg.insert(
session_id.to_string(),
SessionEntry {
pane: pane_id.to_string(),
pid: std::process::id(),
cwd: dir.to_string_lossy().to_string(),
started: "2026-01-01T00:00:00Z".to_string(),
file: file.to_string(),
window: String::new(),
},
);
}
let sessions_dir = dir.join(".agent-doc");
std::fs::create_dir_all(&sessions_dir).unwrap();
let sessions_path = sessions_dir.join("sessions.json");
let content = serde_json::to_string_pretty(®).unwrap();
std::fs::write(sessions_path, content).unwrap();
}
#[test]
#[ignore] fn autoclaim_syncs_layout_with_multiple_files() {
let iso = IsolatedTmux::new("agent-doc-test-autoclaim-sync");
let dir = TempDir::new().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
let doc1 = dir.path().join("tasks/test1.md");
let doc2 = dir.path().join("tasks/test2.md");
std::fs::create_dir_all(dir.path().join("tasks")).unwrap();
std::fs::write(&doc1, "---\nagent_doc_session: session-1\nagent_doc_mode: template\n---\n# Doc 1\n").unwrap();
std::fs::write(&doc2, "---\nagent_doc_session: session-2\nagent_doc_mode: template\n---\n# Doc 2\n").unwrap();
let pane1 = iso.new_session("test", dir.path()).unwrap();
let pane2 = iso.new_window("test", dir.path()).unwrap();
iso.join_pane(&pane2, &pane1, "-dh").unwrap();
setup_multi_file_registry(
dir.path(),
&[
("session-1", &pane1, "tasks/test1.md"),
("session-2", &pane2, "tasks/test2.md"),
],
);
unsafe { std::env::set_var("TMUX_PANE", &pane1) };
let result = run_with_tmux(&iso);
assert!(result.is_ok(), "autoclaim should succeed: {:?}", result);
assert!(iso.pane_alive(&pane1), "pane1 should be alive after sync");
assert!(iso.pane_alive(&pane2), "pane2 should be alive after sync");
unsafe { std::env::remove_var("TMUX_PANE") };
}
#[test]
#[ignore] fn autoclaim_no_claim_skips_focus() {
let dir = TempDir::new().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
let sessions_dir = dir.path().join(".agent-doc");
std::fs::create_dir_all(&sessions_dir).unwrap();
std::fs::write(sessions_dir.join("sessions.json"), "{}").unwrap();
unsafe { std::env::set_var("TMUX_PANE", "%99999") };
let result = run_with_tmux(&Tmux::default_server());
assert!(result.is_ok());
unsafe { std::env::remove_var("TMUX_PANE") };
}
}