Skip to content
Open
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ upcoming bare `resume` picker — into a minute-plus silent stall.
method: a first prompt buried past the head budget reports as
`None`, and `line_count` counts non-empty lines rather than
successfully parsed ones.
## The share picker gathers harnesses in parallel — 2026-08-04

The unified session picker (`path share`, and bare `path resume` once
it lands) enumerated the seven providers one after another, so the
slowest scan — a big codex or claude history — stacked on top of all
the others before anything appeared.

- **path-cli** (0.16.2): `gather_artifacts` runs the provider scans in
scoped threads, making pre-picker wall time max-of-providers instead
of sum-of-providers. Claude scans inline on the calling thread (its
chain-index cache is single-threaded); everything else fans out.
Row concatenation keeps the old provider order, so ranking
tie-breaks are unchanged.

## Projected Claude sessions are resumable again — 2026-07-30

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" }
toolpath-pi = { version = "0.6.1", path = "crates/toolpath-pi" }
path-cli = { version = "0.16.1", path = "crates/path-cli" }
path-cli = { version = "0.16.2", path = "crates/path-cli" }
pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" }

reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/path-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "path-cli"
version = "0.16.1"
version = "0.16.2"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
88 changes: 52 additions & 36 deletions crates/path-cli/src/cmd_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,47 +90,63 @@ pub(crate) fn gather_artifacts(
harness_filter: Option<ArtifactType>,
project_filter: Option<&std::path::Path>,
) -> Vec<ArtifactRow> {
let mut rows = Vec::new();
let canonical_cwd = canonicalize_or_self(cwd);
let canonical_project = project_filter.map(canonicalize_or_self);

let want = |h: ArtifactType| harness_filter.is_none_or(|f| f == h);

if want(ArtifactType::Claude)
&& let Some(mgr) = &bundle.claude
{
collect_claude(mgr, &canonical_cwd, canonical_project.as_deref(), &mut rows);
}
if want(ArtifactType::Gemini)
&& let Some(mgr) = &bundle.gemini
{
collect_gemini(mgr, &canonical_cwd, canonical_project.as_deref(), &mut rows);
}
if want(ArtifactType::Pi)
&& let Some(mgr) = &bundle.pi
{
collect_pi(mgr, &canonical_cwd, canonical_project.as_deref(), &mut rows);
}
if want(ArtifactType::Codex)
&& let Some(mgr) = &bundle.codex
{
collect_codex(mgr, &canonical_cwd, canonical_project.as_deref(), &mut rows);
}
if want(ArtifactType::Copilot)
&& let Some(mgr) = &bundle.copilot
{
collect_copilot(mgr, &canonical_cwd, canonical_project.as_deref(), &mut rows);
}
if want(ArtifactType::Opencode)
&& let Some(mgr) = &bundle.opencode
{
collect_opencode(mgr, &canonical_cwd, canonical_project.as_deref(), &mut rows);
}
if want(ArtifactType::Cursor)
&& let Some(mgr) = &bundle.cursor
{
collect_cursor(mgr, &canonical_cwd, canonical_project.as_deref(), &mut rows);
}
// Enumerate providers concurrently: each is an independent
// read-only scan of its own on-disk tree, and the slowest (a big
// codex or claude history) otherwise serializes behind the rest.
// Wall time becomes max-of-providers instead of sum. Claude is the
// one provider that can't cross threads (`ClaudeConvo` caches its
// chain index in a `RefCell`), so it scans inline on this thread
// while the rest run in scoped threads. Concatenation happens in
// the old sequential provider order, so the stable sort's
// tie-breaking matches the previous behavior exactly.
let mut rows = Vec::new();
let cwd_ref = &canonical_cwd;
let project_ref = canonical_project.as_deref();
std::thread::scope(|s| {
let mut handles = Vec::new();

macro_rules! spawn_collect {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a match over a loop of ArtifactType would probably be more immediately readable. While this makes adding a new line much simpler, understanding the actual logic requires a new indirection and reading a macro definition.

This would also make the spawn and join shapes match, decreasing mental overhead for bookending.

e.g.

let mut types = /* all the types here */
if let Some(exclusive_harness) = harness_filter {
    types = Vec::from([exclusive_harness]);
}
for t in types {
    handles.push(s.spawn(match t {
        ArtifactType::Gemini => move || {
            collect_gemini(mgr, canonical_cwd, canonical_project.as_deref()) }
       ....
    }))
}

Remember that collect_gemini, etc. are just helpers for use here and you can make them return an easier to use return type, etc.

($ty:expr, $mgr:expr, $collect:ident) => {
if want($ty)
&& let Some(mgr) = $mgr
{
handles.push(s.spawn(move || {
let mut out = Vec::new();
$collect(mgr, cwd_ref, project_ref, &mut out);
out
}));
}
};
}

spawn_collect!(ArtifactType::Gemini, &bundle.gemini, collect_gemini);
spawn_collect!(ArtifactType::Pi, &bundle.pi, collect_pi);
spawn_collect!(ArtifactType::Codex, &bundle.codex, collect_codex);
spawn_collect!(ArtifactType::Copilot, &bundle.copilot, collect_copilot);
spawn_collect!(ArtifactType::Opencode, &bundle.opencode, collect_opencode);
spawn_collect!(ArtifactType::Cursor, &bundle.cursor, collect_cursor);

if want(ArtifactType::Claude)
&& let Some(mgr) = &bundle.claude
{
collect_claude(mgr, cwd_ref, project_ref, &mut rows);
}

for handle in handles {
match handle.join() {
Ok(out) => rows.extend(out),
// A panicking collector degrades to "that provider is
// missing from the picker", matching how collector-level
// errors already warn-and-continue.
Err(_) => eprintln!("warning: a session collector panicked; its rows are skipped"),
}
}
});

rows.sort_by(|a, b| {
b.matches_cwd
Expand Down
2 changes: 1 addition & 1 deletion site/_data/crates.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
},
{
"name": "path-cli",
"version": "0.16.1",
"version": "0.16.2",
"description": "Unified CLI (binary: path)",
"docs": "https://docs.rs/path-cli",
"crate": "https://crates.io/crates/path-cli",
Expand Down
Loading