Skip to main content

rpi_cli/
session_cwd.rs

1//! Stored-session working-directory recovery.
2//!
3//! Port of native Pi's `packages/coding-agent/src/core/session-cwd.ts`. A
4//! session file records the cwd it was created in; when that directory has
5//! since been deleted, restoring the session must either fall back to the
6//! current cwd or fail with a clear, actionable message instead of silently
7//! operating in a non-existent directory.
8
9use std::fmt;
10use std::path::{Path, PathBuf};
11
12/// A session whose recorded working directory no longer exists on disk.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SessionCwdIssue {
15    /// The session file that recorded the missing cwd, when known.
16    pub session_file: Option<PathBuf>,
17    /// The cwd stored in the session file (does not exist).
18    pub session_cwd: PathBuf,
19    /// The cwd the process is currently running in (the fallback).
20    pub fallback_cwd: PathBuf,
21}
22
23/// Detect a missing session cwd. Returns `None` when there is no session file,
24/// the stored cwd is empty, or the stored cwd still exists.
25pub fn get_missing_session_cwd_issue(
26    session_file: Option<&Path>,
27    session_cwd: &Path,
28    fallback_cwd: &Path,
29) -> Option<SessionCwdIssue> {
30    let session_file = session_file?;
31    if session_cwd.as_os_str().is_empty() {
32        return None;
33    }
34    if session_cwd.exists() {
35        return None;
36    }
37    Some(SessionCwdIssue {
38        session_file: Some(session_file.to_path_buf()),
39        session_cwd: session_cwd.to_path_buf(),
40        fallback_cwd: fallback_cwd.to_path_buf(),
41    })
42}
43
44/// One-line error message (native `formatMissingSessionCwdError`).
45pub fn format_missing_session_cwd_error(issue: &SessionCwdIssue) -> String {
46    let session_file = issue
47        .session_file
48        .as_ref()
49        .map(|p| format!("\nSession file: {}", p.display()))
50        .unwrap_or_default();
51    format!(
52        "Stored session working directory does not exist: {}{}\nCurrent working directory: {}",
53        issue.session_cwd.display(),
54        session_file,
55        issue.fallback_cwd.display()
56    )
57}
58
59/// Short prompt shown by the startup selector (native
60/// `formatMissingSessionCwdPrompt`).
61pub fn format_missing_session_cwd_prompt(issue: &SessionCwdIssue) -> String {
62    format!(
63        "cwd from session file does not exist\n{}\n\ncontinue in current cwd\n{}",
64        issue.session_cwd.display(),
65        issue.fallback_cwd.display()
66    )
67}
68
69/// Error raised when a session's cwd cannot be resolved and no override was
70/// supplied.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct MissingSessionCwdError {
73    pub issue: SessionCwdIssue,
74}
75
76impl fmt::Display for MissingSessionCwdError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(&format_missing_session_cwd_error(&self.issue))
79    }
80}
81
82impl std::error::Error for MissingSessionCwdError {}
83
84/// Fail unless the session cwd exists (native `assertSessionCwdExists`).
85pub fn assert_session_cwd_exists(
86    session_file: Option<&Path>,
87    session_cwd: &Path,
88    fallback_cwd: &Path,
89) -> Result<(), MissingSessionCwdError> {
90    match get_missing_session_cwd_issue(session_file, session_cwd, fallback_cwd) {
91        Some(issue) => Err(MissingSessionCwdError { issue }),
92        None => Ok(()),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::path::PathBuf;
100
101    #[test]
102    fn no_session_file_is_ok() {
103        assert!(
104            get_missing_session_cwd_issue(None, Path::new("/whatever"), Path::new("/cwd"))
105                .is_none()
106        );
107    }
108
109    #[test]
110    fn existing_dir_is_ok() {
111        let dir = std::env::temp_dir();
112        assert!(get_missing_session_cwd_issue(Some(Path::new("/s.jsonl")), &dir, &dir).is_none());
113    }
114
115    #[test]
116    fn empty_stored_cwd_is_ok() {
117        assert!(get_missing_session_cwd_issue(
118            Some(Path::new("/s.jsonl")),
119            Path::new(""),
120            Path::new("/cwd")
121        )
122        .is_none());
123    }
124
125    #[test]
126    fn missing_dir_reports_issue() {
127        let missing = PathBuf::from("/definitely/not/here/xyzzy-1234");
128        let issue =
129            get_missing_session_cwd_issue(Some(Path::new("/s.jsonl")), &missing, Path::new("/cwd"))
130                .expect("issue");
131        assert_eq!(issue.session_cwd, missing);
132        assert!(format_missing_session_cwd_error(&issue).contains("does not exist"));
133        assert!(format_missing_session_cwd_prompt(&issue).contains("continue in current cwd"));
134        assert!(assert_session_cwd_exists(
135            Some(Path::new("/s.jsonl")),
136            &missing,
137            Path::new("/cwd")
138        )
139        .is_err());
140    }
141}