Preserve working directories when importing external sessions (#36964)

## Why

Cursor stores projectless chats under the reserved `empty-window` project, and
their transcripts may not contain embedded working-directory metadata.

## What changed

- Resolve `empty-window` sessions to the parent of the Cursor home directory,
  including when that home is configured as a relative path.
- Record each created or appended session's working directory in its import
  success result instead of inheriting the migration item's directory.

## Testing

- Cover projectless Cursor transcripts without embedded metadata.
- Cover working-directory resolution from a relative Cursor home.

GitOrigin-RevId: 21cc7ea6acab1ed80cc4016a881e07eca3d0abbd
This commit is contained in:
charlesgong-openai authored and copyberry committed 2026-08-04 19:57:33 +00:00
1 parent 40d226e398
commit e9a692d53b
5 files changed
+70 -6

No files matched your search

@@ -46,6 +46,7 @@ use crate::config_manager::ConfigManager;
const SESSION_IMPORT_CONCURRENCY: usize = 5; const SESSION_IMPORT_CONCURRENCY: usize = 5;
struct CompletedSessionImport { struct CompletedSessionImport {
cwd: PathBuf,
import: CompletedExternalAgentSessionImport, import: CompletedExternalAgentSessionImport,
connector_attribution: Option<ImportedSessionConnectorAttribution>, connector_attribution: Option<ImportedSessionConnectorAttribution>,
} }
@@ -53,6 +54,7 @@ struct CompletedSessionImport {
enum SessionImportOutcome { enum SessionImportOutcome {
Created(CompletedSessionImport), Created(CompletedSessionImport),
Appended { Appended {
cwd: PathBuf,
source_path: PathBuf, source_path: PathBuf,
imported_thread_id: ThreadId, imported_thread_id: ThreadId,
title: Option<String>, title: Option<String>,
@@ -129,7 +131,8 @@ impl ExternalAgentSessionImporter {
while let Some(result) = import_results.next().await { while let Some(result) = import_results.next().await {
match result { match result {
Ok(Some(SessionImportOutcome::Created(completed_import))) => { 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.source_path.display().to_string()),
Some(completed_import.import.imported_thread_id.to_string()), Some(completed_import.import.imported_thread_id.to_string()),
completed_import.import.title.clone(), completed_import.import.title.clone(),
@@ -137,11 +140,13 @@ impl ExternalAgentSessionImporter {
completed_imports.push(completed_import); completed_imports.push(completed_import);
} }
Ok(Some(SessionImportOutcome::Appended { Ok(Some(SessionImportOutcome::Appended {
cwd,
source_path, source_path,
imported_thread_id, imported_thread_id,
title, title,
})) => { })) => {
item_result.record_success( item_result.record_success_with_cwd(
Some(cwd),
Some(source_path.display().to_string()), Some(source_path.display().to_string()),
Some(imported_thread_id.to_string()), Some(imported_thread_id.to_string()),
title, title,
@@ -286,6 +291,7 @@ impl ExternalAgentSessionImporter {
thread_id, thread_id,
expected_source_content_sha256, expected_source_content_sha256,
} => { } => {
let cwd = session.cwd.clone();
let title = session.title.clone(); let title = session.title.clone();
let appended = append_existing_session( let appended = append_existing_session(
&self.codex_home, &self.codex_home,
@@ -302,6 +308,7 @@ impl ExternalAgentSessionImporter {
) )
.await; .await;
Ok(appended.then_some(SessionImportOutcome::Appended { Ok(appended.then_some(SessionImportOutcome::Appended {
cwd,
source_path, source_path,
imported_thread_id: thread_id, imported_thread_id: thread_id,
title, title,
@@ -326,6 +333,7 @@ impl ExternalAgentSessionImporter {
session_id: session_id.to_string(), session_id: session_id.to_string(),
server_ids: attributed_mcp_server_ids, server_ids: attributed_mcp_server_ids,
}); });
let cwd = session.cwd.clone();
let title = session.title.clone(); let title = session.title.clone();
let imported_thread_id = let imported_thread_id =
self.persist_session(session) self.persist_session(session)
@@ -337,6 +345,7 @@ impl ExternalAgentSessionImporter {
sub_error_type: failure.sub_error_type, sub_error_type: failure.sub_error_type,
})?; })?;
Ok(CompletedSessionImport { Ok(CompletedSessionImport {
cwd,
import: CompletedExternalAgentSessionImport { import: CompletedExternalAgentSessionImport {
source_path, source_path,
source_content_sha256, source_content_sha256,
@@ -1628,7 +1628,6 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
session_success.item_type, session_success.item_type,
ExternalAgentConfigMigrationItemType::Sessions ExternalAgentConfigMigrationItemType::Sessions
); );
assert_eq!(session_success.cwd, None);
let session_source = std::fs::canonicalize(&session_path)?.display().to_string(); let session_source = std::fs::canonicalize(&session_path)?.display().to_string();
assert_eq!( assert_eq!(
session_success.source.as_deref(), session_success.source.as_deref(),
@@ -38,7 +38,7 @@ pub(crate) fn detect_recent_cur_sessions_with_limits(
if !project_storage.is_dir() { if !project_storage.is_dir() {
continue; 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")) { for path in cur_transcript_files(&project_storage.join("agent-transcripts")) {
candidates.push(SessionFileCandidate { candidates.push(SessionFileCandidate {
path, path,
@@ -79,8 +79,17 @@ fn cur_transcript_files(transcripts_root: &Path) -> Vec<PathBuf> {
files files
} }
fn cur_project_cwd(project_storage: &Path) -> Option<PathBuf> { fn cur_project_cwd(project_storage: &Path, external_agent_home: &Path) -> Option<PathBuf> {
let encoded = project_storage.file_name()?.to_str()?; 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) decode_cur_project_path(encoded)
} }
@@ -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] #[test]
fn detects_cur_transcript_with_embedded_unc_cwd() { fn detects_cur_transcript_with_embedded_unc_cwd() {
let root = TempDir::new().expect("tempdir"); let root = TempDir::new().expect("tempdir");
+11 -1
View File
@@ -146,11 +146,21 @@ impl ExternalAgentConfigImportItemResult {
source: Option<String>, source: Option<String>,
target: Option<String>, target: Option<String>,
title: Option<String>, title: Option<String>,
) {
self.record_success_with_cwd(self.cwd.clone(), source, target, title);
}
pub fn record_success_with_cwd(
&mut self,
cwd: Option<PathBuf>,
source: Option<String>,
target: Option<String>,
title: Option<String>,
) { ) {
self.success_count = self.success_count.saturating_add(1); self.success_count = self.success_count.saturating_add(1);
self.successes.push(ExternalAgentConfigImportSuccess { self.successes.push(ExternalAgentConfigImportSuccess {
item_type: self.item_type, item_type: self.item_type,
cwd: self.cwd.clone(), cwd,
source, source,
target, target,
title, title,