Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/services/src/import/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,9 @@ fn collect_files(
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Not followed through symlinks: a link back to an ancestor would
// make the walk branch without end.
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
visit(&path, predicate, files);
} else if predicate(&path) {
files.push(path);
Expand Down
26 changes: 12 additions & 14 deletions crates/services/src/import/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,22 @@ pub fn scan_recent_dirs(roots: &ExternalRoots, exclude_roots: &[PathBuf]) -> Vec

let mut groups = HashMap::<PathBuf, Vec<ExternalThread>>::new();
for (cwd, thread) in found {
if !cwd.is_dir()
|| exclude_roots
.iter()
.any(|excluded| same_path(&cwd, excluded))
{
continue;
}
groups.entry(cwd).or_default().push(thread);
}
// Filesystem checks run once per directory, not per thread: each one can
// block on a slow or unreachable mount.
let excluded: Vec<_> = exclude_roots
.iter()
.map(|root| root.canonicalize().unwrap_or_else(|_| root.clone()))
.collect();
let mut dirs: Vec<_> = groups
.into_iter()
.filter(|(cwd, _)| {
cwd.is_dir() && {
let cwd = cwd.canonicalize().unwrap_or_else(|_| cwd.clone());
!excluded.contains(&cwd)
}
})
.map(|(path, mut threads)| {
threads.sort_by_key(|thread| std::cmp::Reverse(thread.last_active_ms));
RecentDir {
Expand Down Expand Up @@ -209,10 +214,3 @@ fn desktop_metadata(root: &Path) -> HashMap<String, DesktopMeta> {
}
metadata
}

fn same_path(left: &Path, right: &Path) -> bool {
match (left.canonicalize(), right.canonicalize()) {
(Ok(left), Ok(right)) => left == right,
_ => left == right,
}
}
8 changes: 8 additions & 0 deletions crates/services/src/import/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ fn scanner_groups_attributes_orders_and_excludes() {
})],
);

// A link back to an ancestor must not list the sessions below it again.
#[cfg(unix)]
std::os::unix::fs::symlink(
roots.codex_session_roots[0].join("2026"),
roots.codex_session_roots[0].join("2026/01/loop"),
)
.unwrap();

let recent = scan_recent_dirs(&roots, std::slice::from_ref(&excluded_cwd));
assert_eq!(recent.len(), 2);
assert_eq!(recent[0].path, desktop_cwd);
Expand Down
41 changes: 28 additions & 13 deletions crates/ui/src/add_project_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ use tcode_protocol::{CommandResponse, ExternalImportState, ExternalThread, Recen
const RECENT_LIMIT: usize = 15;
const RECENT_ROW_HEIGHT_ESTIMATE: f32 = 64.;
const RECENT_VIEWPORT_MAX_HEIGHT: f32 = 390.;
/// Everything above the recents viewport inside the dialog: title, the path row
/// and the footer. Subtracted so the list scrolls instead of pushing the Open
/// button off a short window.
const RECENT_VIEWPORT_CHROME: f32 = 260.;
/// Everything around the recents viewport inside the dialog: title, the path
/// row, the recents heading and the footer. Subtracted so the list scrolls
/// instead of pushing the Open button off a short window.
const RECENT_VIEWPORT_CHROME: f32 = 280.;

enum RecentState {
Loading,
Expand Down Expand Up @@ -364,12 +364,6 @@ impl Render for AddProjectDialog {
let can_browse = self.can_browse(cx);
v_flex()
.gap_4()
.child(
v_flex()
.gap_2()
.child(div().text_size(px(13.)).font_semibold().child(recent_label))
.child(self.render_recent(window, cx)),
)
.child(
v_flex()
.gap_1()
Expand Down Expand Up @@ -410,6 +404,22 @@ impl Render for AddProjectDialog {
)
}),
)
.child(
v_flex()
.gap_2()
.child(
v_flex()
.gap_0p5()
.child(div().text_size(px(13.)).font_semibold().child(recent_label))
.child(
div()
.text_size(px(11.))
.text_color(cx.theme().muted_foreground)
.child(crate::tr!("sidebar.recent_import_hint")),
),
)
.child(self.render_recent(window, cx)),
)
}
}

Expand Down Expand Up @@ -583,9 +593,14 @@ fn tool_counts(threads: &[ExternalThread]) -> String {
]
.into_iter()
.filter_map(|source| {
counts
.get(&source)
.map(|count| format!("{} ×{count}", source.display_name()))
counts.get(&source).map(|&count| {
let tool = source.display_name();
if count == 1 {
crate::tr!("sidebar.recent_tool_threads_one", tool = tool).into_owned()
} else {
crate::tr!("sidebar.recent_tool_threads", tool = tool, count = count).into_owned()
}
})
})
.collect::<Vec<_>>()
.join(" · ")
Expand Down
5 changes: 4 additions & 1 deletion locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,12 @@ sidebar:
add_project: "Add project"
select_project: "Select project directory"
recent_activity: "Recently active"
open_recent: "Open recent project %{name}"
open_recent: "Open recent project %{name} and import its conversations"
recent_loading: "Looking for recent activity…"
recent_empty: "No recent activity found"
recent_import_hint: "Choosing a project here also imports the conversations listed under it."
recent_tool_threads_one: "%{tool}: 1 conversation"
recent_tool_threads: "%{tool}: %{count} conversations"
path_placeholder: "Enter an absolute directory path"
host_path_placeholder: "Absolute path on %{host}"
host_path_hint: "This path is on %{host}, not on this device."
Expand Down
5 changes: 4 additions & 1 deletion locales/zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,12 @@ sidebar:
add_project: "添加项目"
select_project: "选择项目目录"
recent_activity: "最近活跃"
open_recent: "打开最近项目 %{name}"
open_recent: "打开最近项目 %{name} 并导入其对话"
recent_loading: "正在查找最近活跃的目录…"
recent_empty: "未发现最近活跃的目录"
recent_import_hint: "点击此处的项目会同时导入其下列出的对话。"
recent_tool_threads_one: "%{tool}:1 条对话"
recent_tool_threads: "%{tool}:%{count} 条对话"
path_placeholder: "输入绝对目录路径"
host_path_placeholder: "%{host} 上的绝对路径"
host_path_hint: "该路径在 %{host} 上解析,而不是这台设备。"
Expand Down
Loading