@@ -6,111 +6,116 @@ Threads in the sidebar are grouped by their `folder_paths` (a `PathList` stored
in the thread metadata database). When a thread is created from a git worktree
checkout (e.g. `/Users/eric/repo/worktrees/zed/lasalle-lceljoj7/zed`), its
`folder_paths` records the worktree path. But the sidebar computes workspace
-groups from `visible_worktrees().abs_path()`, which returns the root repo path
-(e.g. `/Users/eric/repo/zed`). Since `entries_for_path` did exact `PathList`
-equality, threads from worktree checkouts were invisible in the sidebar.
+groups from `visible_worktrees().abs_path()`, which returns the checkout path.
+Threads from different checkouts of the same repos (different branches) have
+different raw paths and don't match.
## What we've done
-### 1. `PathList` equality fix (PR #52052 β ready to merge)
+### 1. `PathList` equality fix (PR #52052 β merged)
**File:** `crates/util/src/path_list.rs`
-`PathList` derived `PartialEq`/`Eq`/`Hash` which included the `order` field
-(display ordering of paths). Two `PathList` values with the same paths in
-different order were considered unequal. This caused thread matching to break
-after worktree reordering in the project panel.
+Manual `PartialEq`/`Eq`/`Hash` impls that only compare the sorted `paths`
+field, ignoring display order.
-**Fix:** Manual `PartialEq`/`Eq`/`Hash` impls that only compare the sorted
-`paths` field.
+### 2. Worktree path canonicalization + historical groups (this branch)
-### 2. Worktree path canonicalization (on this branch, not yet PR'd)
+**Files:** `crates/sidebar/src/sidebar.rs`, `crates/agent_ui/src/thread_metadata_store.rs`
-**File:** `crates/sidebar/src/sidebar.rs`
+#### Core changes:
-Added two functions:
-- `build_worktree_root_mapping()` β iterates all repo snapshots from all open
- workspaces and builds a `HashMap<PathBuf, Arc<Path>>` mapping every known
- worktree checkout path to its root repo path (using `original_repo_abs_path`
- and `linked_worktrees` from `RepositorySnapshot`).
-- `canonicalize_path_list()` β maps each path in a `PathList` through the
- worktree root mapping, producing a canonical `PathList` keyed by root repo
- paths.
+- **`build_worktree_root_mapping()`** β iterates ALL repos from all workspaces
+ (not just root repos) to build a `HashMap<PathBuf, Arc<Path>>` mapping every
+ known worktree checkout path to its root repo path. Robust against snapshot
+ timing where linked-worktree lists may be temporarily incomplete.
-In `rebuild_contents`, instead of querying `entries_for_path(&path_list)` with
-the workspace's literal path list, we now:
-1. Build the worktreeβroot mapping once at the top
-2. Iterate all thread entries and index them by their canonicalized `folder_paths`
-3. Query that canonical index when populating each workspace's thread list
+- **`canonicalize_path_list()`** β maps each path in a `PathList` through the
+ worktree root mapping.
-Also applied the same canonicalization to `find_current_workspace_for_path_list`
-and `find_open_workspace_for_path_list` (used by archive thread restore).
+- **`rebuild_contents()` three-tier thread lookup:**
+ 1. **Raw lookup** (`entries_for_path`) β exact match by workspace's raw paths
+ 2. **Linked worktree loop** (canonical lookup per repo) β finds threads from
+ absorbed worktree checkouts, assigns correct worktree chips
+ 3. **Canonical lookup** β catches threads from different checkouts of the same
+ repos (e.g. thread saved in branch-a, workspace is branch-b)
-**Status:** The core grouping works β threads from worktree checkouts now appear
-under the root repo's sidebar header. But there are remaining issues with the
-archive restore flow and workspace absorption.
+- **Historical groups** β after the workspace loop, iterates all unclaimed
+ threads (tracked via `claimed_session_ids`) and creates `Closed` project
+ group sections. These appear at the bottom of the sidebar.
+
+- **`ProjectHeader.workspace`** is now `Option<Entity<Workspace>>` to support
+ closed historical group headers.
+
+- **`find_current_workspace_for_path_list` / `find_open_workspace_for_path_list`**
+ β canonicalize both sides (thread path and workspace path) before comparing.
+
+- **`activate_archived_thread`** β when no matching workspace is found, saves
+ metadata and sets `focused_thread` instead of opening a new workspace (which
+ would get absorbed via `find_existing_workspace`).
+
+- **`prune_stale_worktree_workspaces`** β doesn't prune a worktree workspace
+ when its main repo workspace is still open (linked-worktree list may be
+ temporarily incomplete during re-scans).
+
+- **`thread_entry_from_metadata`** β extracted helper for building ThreadEntry
+ from ThreadMetadata.
+
+- **`SidebarThreadMetadataStore::all_entries()`** β new method returning
+ `&[ThreadMetadata]` for reference-based iteration.
## Remaining issues
-### Archive thread restore doesn't route correctly
+### Canonical lookup assigns threads to wrong workspace (next up)
-When restoring a thread from the archive, `activate_archived_thread` tries to
-find a matching workspace via `find_current_workspace_for_path_list`. If the
-thread's `folder_paths` is a single worktree path (e.g. `[zed/meteco/zed]`),
-canonicalization maps it to `[/Users/eric/repo/zed]`. But if the current window
-only has an `[ex, zed]` workspace, the canonical `[zed]` doesn't match `[ex,
-zed]` β they're different path sets. So it falls through to
-`open_workspace_and_activate_thread`, which opens the correct worktree but:
-- The new workspace gets **absorbed** under the `ex, zed` header (no separate
- "zed" header appears)
-- The thread activation may not route to the correct agent panel
+When multiple workspaces share the same canonical path (e.g. main repo + worktree
+checkout of the same repos), the canonical lookup assigns threads to whichever
+workspace processes first in the loop. This causes threads to open in the wrong
+workspace context.
-This needs investigation into how absorption interacts with the restore flow,
-and possibly the creation of a dedicated "zed" workspace (without ex) for
-threads that were created in a zed-only context.
+**Fix needed:** Two-pass approach in `rebuild_contents`:
+- **Pass 1:** Raw lookups across all workspaces (priority claims, correct
+ workspace assignment)
+- **Pass 2:** Canonical lookups only for threads not claimed in pass 1
+
+### Click-to-open from Closed groups bypasses `find_existing_workspace`
+
+When a user clicks a thread under a `Closed` historical group header,
+`open_workspace_and_activate_thread` goes through `open_paths` β
+`find_existing_workspace`, which routes to an existing workspace that contains
+the path instead of creating a new workspace tab. Need to either:
+- Pass `open_new_workspace: Some(true)` through the call chain
+- Or use a direct workspace creation path
### Path set mutation (adding/removing folders)
When you add a folder to a project (e.g. adding `ex` to a `zed` workspace),
existing threads saved with `[zed]` don't match the new `[ex, zed]` path list.
-Similarly, removing `ex` leaves threads saved with `[ex, zed]` orphaned.
-
-This is a **design decision** the team is still discussing. Options include:
-- Treat adding/removing a folder as mutating the project group (update all
- thread `folder_paths` to match)
-- Show threads under the closest matching workspace
-- Show "historical" groups for path lists that have threads but no open workspace
+This is a design decision still being discussed.
-### Absorption suppresses workspace headers
+### Pre-existing test failure
-When a worktree workspace is absorbed under a main repo workspace, it doesn't
-get its own sidebar header. This is by design for the common case (you don't
-want `zed` and `zed/meteor-36zvf3d7` as separate headers). But it means that a
-thread from a single-path worktree workspace like `[zed/meteco/zed]` has no
-header to appear under if the main workspace is `[ex, zed]` (different path
-count).
+`test_two_worktree_workspaces_absorbed_when_main_added` fails on `origin/main`
+before our changes. Root cause is a git snapshot timing issue where linked
+worktrees temporarily disappear during re-scans, causing the prune function
+to remove workspaces prematurely.
## Key code locations
- **Thread metadata storage:** `crates/agent_ui/src/thread_metadata_store.rs`
- `SidebarThreadMetadataStore` β in-memory cache + SQLite DB
- `threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>>` β index by literal paths
- - DB location: `~/Library/Application Support/Zed/db/0-{channel}/db.sqlite` table `sidebar_threads`
-- **Old thread storage:** `crates/agent/src/db.rs`
- - `ThreadsDatabase` β the original thread DB (being migrated from)
- - DB location: `~/Library/Application Support/Zed/threads/threads.db`
- **Sidebar rebuild:** `crates/sidebar/src/sidebar.rs`
- - `rebuild_contents()` β the main function that assembles sidebar entries
- - `build_worktree_root_mapping()` β new: builds worktreeβroot path map
- - `canonicalize_path_list()` β new: maps a PathList through the root mapping
- - Absorption logic starts around "Identify absorbed workspaces"
- - Linked worktree query starts around "Load threads from linked git worktrees"
+ - `rebuild_contents()` β three-tier lookup + historical groups
+ - `build_worktree_root_mapping()` β worktreeβroot path map
+ - `canonicalize_path_list()` β maps a PathList through the root mapping
+ - `thread_entry_from_metadata()` β helper for building ThreadEntry
- **Thread saving:** `crates/agent/src/agent.rs`
- - `NativeAgent::save_thread()` β snapshots `folder_paths` from `project.visible_worktrees()` on every save
+ - `NativeAgent::save_thread()` β snapshots `folder_paths` from visible worktrees
- **PathList:** `crates/util/src/path_list.rs`
- - Equality now compares only sorted paths, not display order
+ - Equality compares only sorted paths, not display order
- **Archive restore:** `crates/sidebar/src/sidebar.rs`
- - `activate_archived_thread()` β `find_current_workspace_for_path_list()` β `open_workspace_and_activate_thread()`
+ - `activate_archived_thread()` β saves metadata + focuses thread (no workspace open)
## Useful debugging queries
@@ -119,14 +124,7 @@ count).
sqlite3 ~/Library/Application\ Support/Zed/db/0-nightly/db.sqlite \
"SELECT folder_paths, COUNT(*) FROM sidebar_threads GROUP BY folder_paths ORDER BY COUNT(*) DESC"
--- All distinct folder_paths in the old thread store
-sqlite3 ~/Library/Application\ Support/Zed/threads/threads.db \
- "SELECT folder_paths, COUNT(*) FROM threads WHERE parent_id IS NULL GROUP BY folder_paths ORDER BY COUNT(*) DESC"
-
-- Find a specific thread
sqlite3 ~/Library/Application\ Support/Zed/db/0-nightly/db.sqlite \
"SELECT session_id, title, folder_paths FROM sidebar_threads WHERE title LIKE '%search term%'"
-
--- List all git worktrees for a repo
-git -C /Users/eric/repo/zed worktree list --porcelain
```