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
2026-08-04 19:57:33 +00:00
committed by copyberry
parent 40d226e398
commit e9a692d53b
5 changed files with 70 additions and 6 deletions
@@ -46,6 +46,7 @@ use crate::config_manager::ConfigManager;
const SESSION_IMPORT_CONCURRENCY: usize = 5;
struct CompletedSessionImport {
cwd: PathBuf,
import: CompletedExternalAgentSessionImport,
connector_attribution: Option<ImportedSessionConnectorAttribution>,
}
@@ -53,6 +54,7 @@ struct CompletedSessionImport {
enum SessionImportOutcome {
Created(CompletedSessionImport),
Appended {
cwd: PathBuf,
source_path: PathBuf,
imported_thread_id: ThreadId,
title: Option<String>,
@@ -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,
@@ -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(),
@@ -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<PathBuf> {
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()?;
// 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)
}
@@ -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");
+11 -1
View File
@@ -146,11 +146,21 @@ impl ExternalAgentConfigImportItemResult {
source: Option<String>,
target: 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.successes.push(ExternalAgentConfigImportSuccess {
item_type: self.item_type,
cwd: self.cwd.clone(),
cwd,
source,
target,
title,