use super::*;
impl AgentLoop {
pub(crate) async fn change_dir(&mut self, path: &str) {
let new_path = if path.starts_with('/') {
std::path::PathBuf::from(path)
} else if path.starts_with('~') {
crate::tool::real_home_dir()
.map(|h| h.join(path.strip_prefix("~/").unwrap_or(&path[1..])))
.unwrap_or_else(|| std::path::PathBuf::from(path))
} else {
let wd: PathBuf = self
.turn_runner
.context
.working_dir
.try_read()
.map(|g| g.clone())
.unwrap_or_default();
wd.join(path)
};
let resolved = std::fs::canonicalize(&new_path).unwrap_or(new_path);
if resolved.is_dir() {
{
let mut wd = self.turn_runner.context.working_dir.write().await;
*wd = resolved.clone();
}
self.datalog.set_working_dir(&resolved);
// Clear conversation history — old paths from previous directory will confuse the model
self.conversation.messages.clear();
self.conversation.turn_tracker = crate::conversation::turn::TurnTracker::new();
self.session_files.clear();
// Explicit /cd is a deliberate context switch (cwd, git snapshot,
// skills all change below) → rebuild the frozen system prompt.
self.cached_system_prompt = None;
// Refresh env snapshot for the new directory. The old git
// branch / status belongs to the previous repo; keeping it
// would lie to the model.
self.env_snapshot = crate::ctx::EnvSnapshot::capture(&resolved);
// Reload skills for the new working directory (project-level skills may differ)
if let Ok(mut reg) = self.skill_registry.write() {
// Non-interactive context: warnings would have nowhere to
// render. Drop them; the TUI bootstrap reloads with a
// renderer in scope and will surface anything important.
let _ = reg.reload(&resolved);
}
// Reload code graph for the new project
let graph_path = resolved.join(".atomcode").join("graph.bin");
let new_graph = crate::graph::persist::load(&graph_path);
// Swap graph data (reuse the same Arc, just replace contents)
{
let mut g = self.turn_runner.context.graph.write().await;
*g = new_graph;
}
// Cancel the previous indexer so rapid `/cd` chains don't
// stack parallel parses. Replace the token so the new spawn
// below gets a fresh one; the old spawn cooperatively
// exits at its next cancel check.
self.indexer_cancel.cancel();
self.indexer_cancel = CancellationToken::new();
// Spawn new indexer — but only if the new dir is a real
// project root. `/cd ~/project` (umbrella of many repos)
// and `/cd ~` without markers would otherwise trigger a
// multi-MB tree-sitter walk pegging CPU for minutes.
// `should_index` covers $HOME / `/` / umbrella cases.
if crate::graph::indexer::should_index(&resolved) {
let graph_clone = self.turn_runner.context.graph.clone();
let wd_for_indexer = resolved.clone();
let cancel = self.indexer_cancel.clone();
tokio::spawn(async move {
let mut indexer = crate::graph::indexer::GraphIndexer::new(
graph_clone.clone(),
wd_for_indexer.clone(),
);
indexer.index_all(cancel).await;
let gp = wd_for_indexer.join(".atomcode").join("graph.bin");
if let Ok(g) = graph_clone.try_read() {
let _ = crate::graph::persist::save(&g, &gp);
}
});
} else {
let _ = self.event_tx.send(AgentEvent::TextDelta(
"[skipped code graph index: directory has no project marker \
(.git / Cargo.toml / package.json / pyproject.toml / go.mod / \
pom.xml / build.gradle) and looks like a parent of multiple \
projects. `cd` into a specific project to enable symbol search.]\n"
.to_string(),
));
}
let _ = self.event_tx.send(AgentEvent::WorkingDirChanged(resolved));
}
}
}