Skip to main content

mj_controller/import/
native.rs

1use super::*;
2
3/// One native session located on disk, normalized across harnesses: the id
4/// `session/load` takes and the file or directory its transcript is read from.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct LocatedNativeSession {
7    pub native_session_id: String,
8    pub source_path: PathBuf,
9}
10
11/// One native session as a picker lists it, normalized across harnesses.
12#[derive(Debug, Clone)]
13pub struct NativeSessionListing {
14    pub native_session_id: String,
15    pub title: String,
16    pub modified_at: SystemTime,
17    pub git_branch: String,
18    pub size_bytes: u64,
19    pub cwd: PathBuf,
20    /// Why this session cannot be imported, when it cannot be.
21    pub unavailable_reason: Option<&'static str>,
22    /// Archived inside the harness itself. Only Codex reports this today.
23    pub natively_archived: bool,
24}
25
26/// Locate one native session for any harness.
27pub fn locate_native_session(
28    harness: HarnessKind,
29    home: &Path,
30    selection: &ClaudeSessionSelection,
31) -> Result<LocatedNativeSession> {
32    let (native_session_id, source_path) = match harness {
33        HarnessKind::Muse => {
34            return muse::locate(&mj_checkpoint::native::muse_sessions_root(home)?, selection);
35        }
36        HarnessKind::Codex => {
37            let located = locate_codex_session(home, selection)?;
38            (located.native_session_id, located.jsonl_path)
39        }
40        HarnessKind::Claude => {
41            let located = locate_claude_session(home, selection)?;
42            (located.native_session_id, located.jsonl_path)
43        }
44        HarnessKind::Kimi => {
45            let located = locate_kimi_session(home, selection)?;
46            (located.native_session_id, located.session_path)
47        }
48        HarnessKind::Grok => {
49            let located = locate_grok_session(home, selection)?;
50            (located.native_session_id, located.session_path)
51        }
52    };
53    Ok(LocatedNativeSession {
54        native_session_id,
55        source_path,
56    })
57}
58
59/// Project one native session into the canonical transcript, for any harness.
60pub fn read_native_transcript(
61    harness: HarnessKind,
62    source_path: &Path,
63) -> Result<ClaudeTranscript> {
64    match harness {
65        HarnessKind::Muse => muse::read_transcript(source_path),
66        HarnessKind::Codex => read_codex_transcript(source_path),
67        HarnessKind::Claude => read_claude_transcript(source_path),
68        HarnessKind::Kimi => read_kimi_transcript(source_path),
69        HarnessKind::Grok => read_grok_transcript(source_path),
70    }
71}
72
73/// Scan a harness home newest first, reporting after every candidate.
74pub fn scan_native_sessions(
75    harness: HarnessKind,
76    home: &Path,
77    mut report: impl FnMut(SessionScanProgress<NativeSessionListing>),
78) -> Result<()> {
79    let mut forward = |scanned, total, session| {
80        report(SessionScanProgress {
81            scanned,
82            total,
83            session,
84        });
85    };
86    match harness {
87        HarnessKind::Muse => muse::scan(
88            &mj_checkpoint::native::muse_sessions_root(home)?,
89            |progress| {
90                forward(progress.scanned, progress.total, progress.session);
91            },
92        ),
93        HarnessKind::Codex => scan_codex_sessions(home, |progress| {
94            let session = progress.session.map(|session| NativeSessionListing {
95                unavailable_reason: session.history_mode.import_issue(),
96                native_session_id: session.native_session_id,
97                title: session.title,
98                modified_at: session.modified_at,
99                git_branch: session.git_branch,
100                size_bytes: session.size_bytes,
101                cwd: session.cwd,
102                natively_archived: session.natively_archived,
103            });
104            forward(progress.scanned, progress.total, session);
105        }),
106        HarnessKind::Claude => scan_claude_sessions(home, |progress| {
107            let session = progress.session.map(|session| NativeSessionListing {
108                native_session_id: session.native_session_id,
109                title: session.title,
110                modified_at: session.modified_at,
111                git_branch: session.git_branch,
112                size_bytes: session.size_bytes,
113                cwd: session.cwd,
114                unavailable_reason: None,
115                natively_archived: false,
116            });
117            forward(progress.scanned, progress.total, session);
118        }),
119        HarnessKind::Kimi => scan_kimi_sessions(home, |progress| {
120            let session = progress.session.map(|session| NativeSessionListing {
121                native_session_id: session.native_session_id,
122                title: session.title,
123                modified_at: session.modified_at,
124                git_branch: session.git_branch,
125                size_bytes: session.size_bytes,
126                cwd: session.cwd,
127                unavailable_reason: None,
128                natively_archived: false,
129            });
130            forward(progress.scanned, progress.total, session);
131        }),
132        HarnessKind::Grok => scan_grok_sessions(home, |progress| {
133            let session = progress.session.map(|session| NativeSessionListing {
134                native_session_id: session.native_session_id,
135                title: session.title,
136                modified_at: session.modified_at,
137                git_branch: session.git_branch,
138                size_bytes: session.size_bytes,
139                cwd: session.cwd,
140                unavailable_reason: None,
141                natively_archived: false,
142            });
143            forward(progress.scanned, progress.total, session);
144        }),
145    }
146}