use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionCwdIssue {
pub session_file: Option<PathBuf>,
pub session_cwd: PathBuf,
pub fallback_cwd: PathBuf,
}
pub fn get_missing_session_cwd_issue(
session_file: Option<&Path>,
session_cwd: &Path,
fallback_cwd: &Path,
) -> Option<SessionCwdIssue> {
let session_file = session_file?;
if session_cwd.as_os_str().is_empty() {
return None;
}
if session_cwd.is_dir() {
return None;
}
Some(SessionCwdIssue {
session_file: Some(session_file.to_path_buf()),
session_cwd: session_cwd.to_path_buf(),
fallback_cwd: fallback_cwd.to_path_buf(),
})
}
pub fn resolve_session_cwd(session_cwd: &Path) -> Option<PathBuf> {
if session_cwd.as_os_str().is_empty() {
return None;
}
session_cwd.is_dir().then(|| session_cwd.to_path_buf())
}
pub fn missing_session_cwd_note(recorded: &Path, fallback: &Path) -> String {
format!(
"This session's directory ({}) no longer exists; continuing in {}.",
recorded.display(),
fallback.display()
)
}
pub fn adopted_session_cwd_note(launch: &Path, adopted: &Path) -> String {
format!(
"Continuing this session in {}, where it was created (launched from {}).",
adopted.display(),
launch.display()
)
}
pub fn untrusted_session_cwd_note(recorded: &Path) -> String {
format!(
"This session's directory ({}) is not trusted, so its project settings, \
skills, and context files are not loaded. Run /trust yes (or restart \
with --approve) to allow them.",
recorded.display()
)
}
pub fn format_missing_session_cwd_error(issue: &SessionCwdIssue) -> String {
let session_file = issue
.session_file
.as_ref()
.map(|p| format!("\nSession file: {}", p.display()))
.unwrap_or_default();
format!(
"Stored session working directory does not exist: {}{}\nCurrent working directory: {}",
issue.session_cwd.display(),
session_file,
issue.fallback_cwd.display()
)
}
pub fn format_missing_session_cwd_prompt(issue: &SessionCwdIssue) -> String {
format!(
"cwd from session file does not exist\n{}\n\ncontinue in current cwd\n{}",
issue.session_cwd.display(),
issue.fallback_cwd.display()
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingSessionCwdError {
pub issue: SessionCwdIssue,
}
impl fmt::Display for MissingSessionCwdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&format_missing_session_cwd_error(&self.issue))
}
}
impl std::error::Error for MissingSessionCwdError {}
pub fn assert_session_cwd_exists(
session_file: Option<&Path>,
session_cwd: &Path,
fallback_cwd: &Path,
) -> Result<(), MissingSessionCwdError> {
match get_missing_session_cwd_issue(session_file, session_cwd, fallback_cwd) {
Some(issue) => Err(MissingSessionCwdError { issue }),
None => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn no_session_file_is_ok() {
assert!(
get_missing_session_cwd_issue(None, Path::new("/whatever"), Path::new("/cwd"))
.is_none()
);
}
#[test]
fn existing_dir_is_ok() {
let dir = std::env::temp_dir();
assert!(get_missing_session_cwd_issue(Some(Path::new("/s.jsonl")), &dir, &dir).is_none());
}
#[test]
fn empty_stored_cwd_is_ok() {
assert!(get_missing_session_cwd_issue(
Some(Path::new("/s.jsonl")),
Path::new(""),
Path::new("/cwd")
)
.is_none());
}
#[test]
fn missing_dir_reports_issue() {
let missing = PathBuf::from("/definitely/not/here/xyzzy-1234");
let issue =
get_missing_session_cwd_issue(Some(Path::new("/s.jsonl")), &missing, Path::new("/cwd"))
.expect("issue");
assert_eq!(issue.session_cwd, missing);
assert!(format_missing_session_cwd_error(&issue).contains("does not exist"));
assert!(format_missing_session_cwd_prompt(&issue).contains("continue in current cwd"));
assert!(assert_session_cwd_exists(
Some(Path::new("/s.jsonl")),
&missing,
Path::new("/cwd")
)
.is_err());
}
#[test]
fn resolve_session_cwd_accepts_only_existing_directories() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
resolve_session_cwd(dir.path()),
Some(dir.path().to_path_buf())
);
assert_eq!(resolve_session_cwd(Path::new("")), None);
assert_eq!(
resolve_session_cwd(Path::new("/definitely/not/here/xyzzy-4321")),
None
);
let file = dir.path().join("not-a-dir");
std::fs::write(&file, b"x").unwrap();
assert_eq!(resolve_session_cwd(&file), None);
assert!(
get_missing_session_cwd_issue(Some(Path::new("/s.jsonl")), &file, dir.path()).is_some(),
"a file at the recorded path counts as a missing cwd"
);
}
#[test]
fn missing_and_untrusted_notes_name_the_directories() {
let note = missing_session_cwd_note(Path::new("/gone/project"), Path::new("/here"));
assert!(note.contains("/gone/project"), "{note}");
assert!(note.contains("/here"), "{note}");
assert!(note.contains("no longer exists"), "{note}");
let note = untrusted_session_cwd_note(Path::new("/gone/project"));
assert!(note.contains("/gone/project"), "{note}");
assert!(note.contains("/trust yes"), "{note}");
}
#[test]
fn adopting_a_directory_is_announced_with_both_paths() {
let note =
adopted_session_cwd_note(Path::new("/launch/project"), Path::new("/other/project"));
assert!(note.contains("/other/project"), "{note}");
assert!(note.contains("/launch/project"), "{note}");
assert!(note.contains("where it was created"), "{note}");
}
}