mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
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:
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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user