Skip to main content

pi/core/sessions/
cwd.rs

1//! Missing stored-cwd detection and error helpers.
2//!
3//! Ports `.references/pi/packages/coding-agent/src/core/session-cwd.ts`.
4
5use std::path::Path;
6
7use thiserror::Error;
8
9use super::entries::path_exists;
10
11/// Issue describing a stored session cwd that no longer exists on disk.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct SessionCwdIssue {
14    /// Session file path when known.
15    pub session_file: Option<String>,
16    /// Working directory recorded in the session header.
17    pub session_cwd: String,
18    /// Fallback cwd the caller would continue in.
19    pub fallback_cwd: String,
20}
21
22/// Source of session cwd / file path for missing-cwd checks.
23pub trait SessionCwdSource {
24    /// Resolved session working directory.
25    fn get_cwd(&self) -> &str;
26    /// Session file path, if any.
27    fn get_session_file(&self) -> Option<&str>;
28}
29
30/// Return a missing-cwd issue when the session file is set, the stored cwd is
31/// non-empty, and that path does not exist.
32#[must_use]
33pub fn get_missing_session_cwd_issue(
34    session_manager: &impl SessionCwdSource,
35    fallback_cwd: &str,
36) -> Option<SessionCwdIssue> {
37    let session_file = session_manager.get_session_file()?;
38    let session_cwd = session_manager.get_cwd();
39    if session_cwd.is_empty() || path_exists(Path::new(session_cwd)) {
40        return None;
41    }
42    Some(SessionCwdIssue {
43        session_file: Some(session_file.to_owned()),
44        session_cwd: session_cwd.to_owned(),
45        fallback_cwd: fallback_cwd.to_owned(),
46    })
47}
48
49/// Format the controlled missing-cwd error string (exact TypeScript text).
50#[must_use]
51pub fn format_missing_session_cwd_error(issue: &SessionCwdIssue) -> String {
52    let session_file = issue
53        .session_file
54        .as_ref()
55        .map_or(String::new(), |p| format!("\nSession file: {p}"));
56    format!(
57        "Stored session working directory does not exist: {}{}\nCurrent working directory: {}",
58        issue.session_cwd, session_file, issue.fallback_cwd
59    )
60}
61
62/// Format the interactive missing-cwd prompt (exact TypeScript text).
63#[must_use]
64pub fn format_missing_session_cwd_prompt(issue: &SessionCwdIssue) -> String {
65    format!(
66        "cwd from session file does not exist\n{}\n\ncontinue in current cwd\n{}",
67        issue.session_cwd, issue.fallback_cwd
68    )
69}
70
71/// Error thrown when a stored session cwd is missing on disk.
72///
73/// The error name is `"MissingSessionCwdError"` (via [`std::any::type_name`]
74/// consumers / Display); the message matches TypeScript exactly.
75#[derive(Debug, Error)]
76#[error("{}", format_missing_session_cwd_error(&self.issue))]
77pub struct MissingSessionCwdError {
78    /// The detected missing-cwd issue.
79    pub issue: SessionCwdIssue,
80}
81
82impl MissingSessionCwdError {
83    /// Construct from a [`SessionCwdIssue`].
84    #[must_use]
85    pub fn new(issue: SessionCwdIssue) -> Self {
86        Self { issue }
87    }
88}
89
90/// Assert the session cwd exists; return [`MissingSessionCwdError`] otherwise.
91///
92/// # Errors
93///
94/// Returns [`MissingSessionCwdError`] when the session has a stored file path
95/// and a non-empty cwd that no longer exists on disk.
96pub fn assert_session_cwd_exists(
97    session_manager: &impl SessionCwdSource,
98    fallback_cwd: &str,
99) -> Result<(), MissingSessionCwdError> {
100    if let Some(issue) = get_missing_session_cwd_issue(session_manager, fallback_cwd) {
101        return Err(MissingSessionCwdError::new(issue));
102    }
103    Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    struct FakeSource {
111        cwd: String,
112        file: Option<String>,
113    }
114
115    impl SessionCwdSource for FakeSource {
116        fn get_cwd(&self) -> &str {
117            &self.cwd
118        }
119        fn get_session_file(&self) -> Option<&str> {
120            self.file.as_deref()
121        }
122    }
123
124    #[test]
125    fn detects_missing_cwd() -> Result<(), &'static str> {
126        let src = FakeSource {
127            cwd: "/tmp/pi-oxidized-definitely-missing-cwd-xyz".to_owned(),
128            file: Some("/tmp/session.jsonl".to_owned()),
129        };
130        let issue = get_missing_session_cwd_issue(&src, "/tmp/fallback").ok_or("expected issue")?;
131        assert_eq!(issue.session_cwd, src.cwd);
132        assert_eq!(issue.fallback_cwd, "/tmp/fallback");
133        assert_eq!(issue.session_file.as_deref(), Some("/tmp/session.jsonl"));
134
135        let err = format_missing_session_cwd_error(&issue);
136        assert!(err.contains("Stored session working directory does not exist:"));
137        assert!(err.contains("Session file: /tmp/session.jsonl"));
138        assert!(err.contains("Current working directory: /tmp/fallback"));
139
140        let prompt = format_missing_session_cwd_prompt(&issue);
141        assert!(prompt.contains("cwd from session file does not exist"));
142        assert!(prompt.contains("continue in current cwd"));
143        Ok(())
144    }
145
146    #[test]
147    fn no_issue_when_no_session_file() {
148        let src = FakeSource {
149            cwd: "/tmp/missing".to_owned(),
150            file: None,
151        };
152        assert!(get_missing_session_cwd_issue(&src, "/tmp").is_none());
153    }
154
155    #[test]
156    fn no_issue_when_cwd_exists() {
157        let src = FakeSource {
158            cwd: std::env::temp_dir().to_string_lossy().into_owned(),
159            file: Some("/tmp/session.jsonl".to_owned()),
160        };
161        assert!(get_missing_session_cwd_issue(&src, "/tmp").is_none());
162    }
163
164    #[test]
165    fn assert_throws_missing_cwd_error() -> Result<(), &'static str> {
166        let src = FakeSource {
167            cwd: "/tmp/pi-oxidized-definitely-missing-cwd-xyz".to_owned(),
168            file: Some("/tmp/session.jsonl".to_owned()),
169        };
170        let Err(err) = assert_session_cwd_exists(&src, "/tmp/fallback") else {
171            return Err("expected MissingSessionCwdError");
172        };
173        assert_eq!(err.issue.session_cwd, src.cwd);
174        Ok(())
175    }
176}