445
}
446
}
447
448
-/// Compare directories the way `session/list` filtering needs: resolve symlinks
449
-/// and `..` when the path exists, otherwise fall back to the path as given.
450
-fn normalize_dir(path: &std::path::Path) -> PathBuf {
451
- std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
448
+/// Do two paths name the same directory, for `session/list` filtering?
449
+///
450
+/// Canonicalizing resolves the symlinks and `..` segments that make a client's
451
+/// `cwd` and a stored one differ while naming the same place (`/tmp` vs
452
+/// `/private/tmp` on macOS). It only works on a directory that still exists,
453
+/// though, so a plain equality check runs first and keeps a session listed
454
+/// after its project directory has been moved or removed.
455
+fn same_dir(a: &std::path::Path, b: &std::path::Path) -> bool {
456
+ if a == b {
457
+ return true;
458
+ }
459
+ match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
460
+ (Ok(a), Ok(b)) => a == b,
461
+ _ => false,
462
+ }
463
}
464
465
/// Rebuild the `session/update` notifications that re-draw a saved conversation
1444
///
1445
/// The sidecar is written here rather than at session start so a thread the
1446
/// user never spoke in doesn't leave one behind, and so a listed session
1436
- /// always has history to import.
1447
+ /// always has history to import. Only the directory the client named on the
1448
+ /// session request is recorded — never the process cwd, which a later
1449
+ /// session in the same process may have moved: a thread listed under the
1450
+ /// wrong project is worse than one that isn't listed at all.
1451
async fn persist_session(&self, session_id: &SessionId, snapshot: &[serde_json::Value]) {
1452
let key = session_id.to_string();
1453
if let Err(error) = session_store::save(&key, snapshot) {
1454
log::warn!("session({session_id}) save failed: {error}");
1455
return;
1456
}
1443
- let cwd = self
1444
- .session_cwd
1445
- .lock()
1446
- .ok()
1447
- .and_then(|guard| guard.clone())
1448
- .or_else(|| std::env::current_dir().ok());
1449
- if let Some(cwd) = cwd {
1450
- session_store::save_meta(&key, &cwd, &workspace::additional_roots());
1457
+ match self.session_cwd.lock().ok().and_then(|guard| guard.clone()) {
1458
+ Some(cwd) => session_store::save_meta(&key, &cwd, &workspace::additional_roots()),
1459
+ None => log::warn!(
1460
+ "session({session_id}) has no recorded cwd — saved, but it won't be listed"
1461
+ ),
1462
}
1463
}
1464
1472
&self,
1473
args: ListSessionsRequest,
1474
) -> agent_client_protocol::Result<ListSessionsResponse> {
1464
- let filter = args.cwd.as_ref().map(|cwd| normalize_dir(cwd));
1475
+ let filter = args.cwd.clone();
1476
let sessions: Vec<SessionInfo> = session_store::list()
1477
.into_iter()
1478
.filter(|entry| entry.message_count > 0)
1479
.filter_map(|entry| {
1480
let cwd = entry.cwd?;
1481
if let Some(filter) = &filter
1471
- && normalize_dir(&cwd) != *filter
1482
+ && !same_dir(&cwd, filter)
1483
{
1484
return None;
1485
}
4264
mod tests {
4265
use super::*;
4266
4267
+ #[test]
4268
+ fn same_dir_matches_through_symlinks_and_survives_a_missing_directory() {
4269
+ let root = std::env::temp_dir().join(format!("sigit_same_dir_{}", std::process::id()));
4270
+ let real = root.join("real");
4271
+ let link = root.join("link");
4272
+ let _ = std::fs::remove_dir_all(&root);
4273
+ std::fs::create_dir_all(&real).unwrap();
4274
+
4275
+ assert!(same_dir(&real, &real));
4276
+ assert!(!same_dir(&real, &root.join("elsewhere")));
4277
+
4278
+ // A stored path that reaches the same directory by another route still
4279
+ // matches the client's.
4280
+ assert!(same_dir(&root.join("real/../real"), &real));
4281
+ #[cfg(unix)]
4282
+ {
4283
+ std::os::unix::fs::symlink(&real, &link).unwrap();
4284
+ assert!(same_dir(&link, &real));
4285
+ }
4286
+
4287
+ // A project that has since been deleted can't be canonicalized; an
4288
+ // exact path still matches, so its threads stay listable.
4289
+ std::fs::remove_dir_all(&root).unwrap();
4290
+ assert!(same_dir(&real, &real));
4291
+ assert!(!same_dir(&link, &real));
4292
+ }
4293
+
4294
#[test]
4295
fn session_context_message_lists_every_root_of_a_multi_root_project() {
4296
let primary = PathBuf::from("/tmp/primary");