diff --git a/codex-rs/app-server/src/external_agent_migration/session_importer.rs b/codex-rs/app-server/src/external_agent_migration/session_importer.rs index 2fb41f93be..36a08ef8e9 100644 --- a/codex-rs/app-server/src/external_agent_migration/session_importer.rs +++ b/codex-rs/app-server/src/external_agent_migration/session_importer.rs @@ -46,6 +46,7 @@ use crate::config_manager::ConfigManager; const SESSION_IMPORT_CONCURRENCY: usize = 5; struct CompletedSessionImport { + cwd: PathBuf, import: CompletedExternalAgentSessionImport, connector_attribution: Option, } @@ -53,6 +54,7 @@ struct CompletedSessionImport { enum SessionImportOutcome { Created(CompletedSessionImport), Appended { + cwd: PathBuf, source_path: PathBuf, imported_thread_id: ThreadId, title: Option, @@ -129,7 +131,8 @@ impl ExternalAgentSessionImporter { while let Some(result) = import_results.next().await { match result { Ok(Some(SessionImportOutcome::Created(completed_import))) => { - item_result.record_success( + item_result.record_success_with_cwd( + Some(completed_import.cwd.clone()), Some(completed_import.import.source_path.display().to_string()), Some(completed_import.import.imported_thread_id.to_string()), completed_import.import.title.clone(), @@ -137,11 +140,13 @@ impl ExternalAgentSessionImporter { completed_imports.push(completed_import); } Ok(Some(SessionImportOutcome::Appended { + cwd, source_path, imported_thread_id, title, })) => { - item_result.record_success( + item_result.record_success_with_cwd( + Some(cwd), Some(source_path.display().to_string()), Some(imported_thread_id.to_string()), title, @@ -286,6 +291,7 @@ impl ExternalAgentSessionImporter { thread_id, expected_source_content_sha256, } => { + let cwd = session.cwd.clone(); let title = session.title.clone(); let appended = append_existing_session( &self.codex_home, @@ -302,6 +308,7 @@ impl ExternalAgentSessionImporter { ) .await; Ok(appended.then_some(SessionImportOutcome::Appended { + cwd, source_path, imported_thread_id: thread_id, title, @@ -326,6 +333,7 @@ impl ExternalAgentSessionImporter { session_id: session_id.to_string(), server_ids: attributed_mcp_server_ids, }); + let cwd = session.cwd.clone(); let title = session.title.clone(); let imported_thread_id = self.persist_session(session) @@ -337,6 +345,7 @@ impl ExternalAgentSessionImporter { sub_error_type: failure.sub_error_type, })?; Ok(CompletedSessionImport { + cwd, import: CompletedExternalAgentSessionImport { source_path, source_content_sha256, diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index 9ee7953ebb..b7b0d4915a 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -1628,7 +1628,6 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { session_success.item_type, ExternalAgentConfigMigrationItemType::Sessions ); - assert_eq!(session_success.cwd, None); let session_source = std::fs::canonicalize(&session_path)?.display().to_string(); assert_eq!( session_success.source.as_deref(), diff --git a/codex-rs/external-agent-migration/src/detect/sessions/cur.rs b/codex-rs/external-agent-migration/src/detect/sessions/cur.rs index eb1400beee..6de13a4b80 100644 --- a/codex-rs/external-agent-migration/src/detect/sessions/cur.rs +++ b/codex-rs/external-agent-migration/src/detect/sessions/cur.rs @@ -38,7 +38,7 @@ pub(crate) fn detect_recent_cur_sessions_with_limits( if !project_storage.is_dir() { continue; } - let fallback_cwd = cur_project_cwd(&project_storage); + let fallback_cwd = cur_project_cwd(&project_storage, external_agent_home); for path in cur_transcript_files(&project_storage.join("agent-transcripts")) { candidates.push(SessionFileCandidate { path, @@ -79,8 +79,17 @@ fn cur_transcript_files(transcripts_root: &Path) -> Vec { files } -fn cur_project_cwd(project_storage: &Path) -> Option { +fn cur_project_cwd(project_storage: &Path, external_agent_home: &Path) -> Option { let encoded = project_storage.file_name()?.to_str()?; + // Cursor stores projectless chats under this reserved project name. + if encoded == "empty-window" { + let external_agent_home = if external_agent_home.is_absolute() { + external_agent_home.to_path_buf() + } else { + std::env::current_dir().ok()?.join(external_agent_home) + }; + return external_agent_home.parent().map(Path::to_path_buf); + } decode_cur_project_path(encoded) } diff --git a/codex-rs/external-agent-migration/src/detect/sessions/cur_tests.rs b/codex-rs/external-agent-migration/src/detect/sessions/cur_tests.rs index f9f4bb443c..06e15f0c37 100644 --- a/codex-rs/external-agent-migration/src/detect/sessions/cur_tests.rs +++ b/codex-rs/external-agent-migration/src/detect/sessions/cur_tests.rs @@ -34,6 +34,43 @@ fn detects_cur_transcript_with_project_cwd() { ); } +#[test] +fn detects_projectless_cur_transcript_without_embedded_metadata() { + let root = TempDir::new().expect("tempdir"); + let external_agent_home = root.path().join(".cursor"); + let transcript = write_transcript( + &external_agent_home, + "empty-window", + "projectless-session", + "first request", + ); + + let sessions = + detect_recent_cur_sessions(&external_agent_home, root.path()).expect("detect sessions"); + + assert_eq!( + sessions, + vec![ExternalAgentSessionMigration { + path: transcript, + cwd: root.path().to_path_buf(), + title: Some("first request".to_string()), + }] + ); +} + +#[test] +fn resolves_projectless_cur_cwd_from_relative_home() { + let current_dir = std::env::current_dir().expect("current dir"); + + assert_eq!( + cur_project_cwd( + Path::new(".cursor/projects/empty-window"), + Path::new(".cursor"), + ), + Some(current_dir) + ); +} + #[test] fn detects_cur_transcript_with_embedded_unc_cwd() { let root = TempDir::new().expect("tempdir"); diff --git a/codex-rs/external-agent-migration/src/model.rs b/codex-rs/external-agent-migration/src/model.rs index 72fcf95c0e..a41e6170b9 100644 --- a/codex-rs/external-agent-migration/src/model.rs +++ b/codex-rs/external-agent-migration/src/model.rs @@ -146,11 +146,21 @@ impl ExternalAgentConfigImportItemResult { source: Option, target: Option, title: Option, + ) { + self.record_success_with_cwd(self.cwd.clone(), source, target, title); + } + + pub fn record_success_with_cwd( + &mut self, + cwd: Option, + source: Option, + target: Option, + title: Option, ) { self.success_count = self.success_count.saturating_add(1); self.successes.push(ExternalAgentConfigImportSuccess { item_type: self.item_type, - cwd: self.cwd.clone(), + cwd, source, target, title,