use std::{env, fs, path::PathBuf};
use crate::Result;
pub(crate) const OPENHANDS_DIR_ENV: &str = "OPENHANDS_DIR";
const DEFAULT_OPENHANDS_ROOTS: [&str; 2] = [".openhands", ".config/openhands"];
const CONVERSATION_DIRS: [&str; 3] = [
"agent-canvas/dev_conversations",
"agent-canvas/conversations",
"conversations",
];
fn candidate_roots() -> Vec<PathBuf> {
if let Some(value) = env::var_os(OPENHANDS_DIR_ENV) {
return match value.to_str() {
Some(list) => list
.split(',')
.map(str::trim)
.filter(|path| !path.is_empty())
.map(PathBuf::from)
.collect(),
None => vec![PathBuf::from(value)],
};
}
crate::home::home_dir()
.into_iter()
.flat_map(|home| {
DEFAULT_OPENHANDS_ROOTS
.into_iter()
.map(move |root| home.join(root))
})
.collect()
}
pub(crate) fn conversation_dirs() -> Result<Vec<PathBuf>> {
let mut conversations = Vec::new();
for root in candidate_roots() {
if root.join("events").is_dir() {
push_unique(&mut conversations, root.join("events"));
continue;
}
for nested in CONVERSATION_DIRS {
let conversations_root = root.join(nested);
if let Ok(entries) = fs::read_dir(&conversations_root) {
for entry in entries.filter_map(std::result::Result::ok) {
let path = entry.path().join("events");
if path.is_dir() {
push_unique(&mut conversations, path);
}
}
}
}
if let Ok(entries) = fs::read_dir(&root) {
for entry in entries.filter_map(std::result::Result::ok) {
if !entry.file_type().is_ok_and(|type_| type_.is_dir()) {
continue;
}
let path = entry.path().join("events");
if path.is_dir() {
push_unique(&mut conversations, path);
}
}
}
}
conversations.sort();
Ok(conversations)
}
fn push_unique(conversations: &mut Vec<PathBuf>, path: PathBuf) {
if !conversations.contains(&path) {
conversations.push(path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_fs::TempDir;
fn prepare(root: &std::path::Path, layout: &[(&str, &str)]) -> Vec<String> {
for (dir, file) in layout {
let events = root.join(dir).join("events");
std::fs::create_dir_all(&events).unwrap();
std::fs::write(events.join(file), "{}").unwrap();
}
unsafe { std::env::set_var(OPENHANDS_DIR_ENV, root) };
let dirs = conversation_dirs().unwrap();
let mut found = dirs
.iter()
.map(|path| {
path.parent()
.unwrap()
.strip_prefix(root)
.unwrap()
.to_string_lossy()
.to_string()
})
.collect::<Vec<_>>();
found.sort();
found
}
#[test]
fn discovers_conversations_in_known_layouts() {
let temp = TempDir::new().unwrap();
let paths = prepare(
temp.path(),
&[
(
"agent-canvas/dev_conversations/conv-a",
"event-00000-a.json",
),
("conversations/conv-b", "event-00000-b.json"),
],
);
assert_eq!(
paths,
vec![
"agent-canvas/dev_conversations/conv-a".to_string(),
"conversations/conv-b".to_string()
]
);
}
#[test]
fn override_may_point_at_a_conversation_directory() {
let temp = TempDir::new().unwrap();
let conv = temp.path().join("agent-canvas/dev_conversations/conv-a");
let events = conv.join("events");
std::fs::create_dir_all(&events).unwrap();
std::fs::write(events.join("event-00000-a.json"), "{}").unwrap();
unsafe { std::env::set_var(OPENHANDS_DIR_ENV, &conv) };
let dirs = conversation_dirs().unwrap();
assert_eq!(dirs, vec![events]);
}
}