1# Sidebar thread grouping β worktree path canonicalization
2
3## Problem
4
5Threads in the sidebar are grouped by their `folder_paths` (a `PathList` stored
6in the thread metadata database). When a thread is created from a git worktree
7checkout (e.g. `/Users/eric/repo/worktrees/zed/lasalle-lceljoj7/zed`), its
8`folder_paths` records the worktree checkout path. Threads from different
9checkouts of the same repos (different branches) have different raw paths and
10don't group together.
11
12## What we've done
13
14### 1. `PathList` equality fix (PR #52052 β open, awaiting review)
15
16**File:** `crates/util/src/path_list.rs`
17
18Manual `PartialEq`/`Eq`/`Hash` impls that only compare the sorted `paths`
19field, ignoring display order.
20
21### 2. Canonical workspace grouping (this branch)
22
23Replaced the old "absorption" model (worktree workspaces absorbed under main
24repo via index-based tracking) with **canonical-key grouping**: workspaces that
25share the same root repo paths are grouped under a single sidebar header.
26
27#### Architecture
28
29- **`build_worktree_root_mapping()`** β iterates ALL repos from all workspaces
30 to build `HashMap<PathBuf, Arc<Path>>` mapping checkout paths β root repo
31 paths. Uses all repos (not just root repos) for robustness when
32 linked-worktree snapshots are temporarily incomplete.
33
34- **`build_canonical_thread_index()`** β indexes all threads by their
35 canonicalized `folder_paths` (checkout paths mapped to root repo paths).
36
37- **`rebuild_contents()` flow:**
38 1. Group workspaces by canonical key
39 2. For each group: claim threads from canonical index, merge live info from
40 all workspaces in the group, build thread entries with best-workspace
41 selection (raw path match preferred)
42 3. Historical groups: iterate unclaimed threads, group by raw `folder_paths`,
43 create Closed project group sections
44
45- **Worktree chips** β threads from single-root worktree checkouts that differ
46 from the canonical key get a `{worktree-name}` chip via
47 `linked_worktree_short_name`.
48
49- **`Workspace::path_list()`** β moved from free function to method on
50 `Workspace`.
51
52- **`ProjectHeader.workspace`** is `Option<Entity<Workspace>>` to support
53 closed historical group headers.
54
55- **`find_current_workspace_for_path_list` /
56 `find_open_workspace_for_path_list`** β canonicalize both sides before
57 comparing.
58
59- **`activate_archived_thread`** β when no matching workspace is found, saves
60 metadata and sets `focused_thread` instead of opening a new workspace.
61
62- **`prune_stale_worktree_workspaces`** β checks `all_workspace_roots` (from
63 `workspace.root_paths()`) instead of git repo snapshots, so the check works
64 even before git scan completes.
65
66- **`thread_entry_from_metadata`** β extracted helper for building ThreadEntry.
67
68- **`SidebarThreadMetadataStore::all_entries()`** β returns `&[ThreadMetadata]`
69 for reference-based iteration.
70
71## Remaining issues (priority order)
72
73### 1. `save_thread` overwrites `folder_paths` on every thread mutation
74
75**Severity: High β causes data loss**
76
77`NativeAgent::save_thread()` (in `crates/agent/src/agent.rs`) fires on every
78`cx.observe` of the thread entity. It always re-snapshots `folder_paths` from
79the session's project's `visible_worktrees().abs_path()`. When a thread is
80loaded in the wrong workspace (e.g. main repo instead of worktree checkout),
81viewing the thread overwrites its `folder_paths` with the wrong paths,
82permanently losing the worktree association.
83
84**Fix needed:** Fix the loading side β when a thread is loaded (from sidebar
85click, session restore, or archive restore), route it to a workspace whose raw
86paths match its saved `folder_paths`. If no matching workspace exists, create
87one. This way `save_thread` naturally preserves the correct paths.
88
89Affected code paths:
90- **Session restore:** `AgentPanel::load` in `crates/agent_ui/src/agent_panel.rs`
91 (~L907-920) β loads the last active thread into whichever workspace is being
92 restored, regardless of the thread's `work_dirs`
93- **Sidebar click:** `confirm` / `render_thread` β `activate_thread` β loads in
94 the `ThreadEntryWorkspace` which may be the wrong workspace (fallback to
95 first in group)
96- **Archive restore:** `activate_archived_thread` β currently just saves
97 metadata and focuses, but clicking the resulting entry still routes through
98 `open_workspace_and_activate_thread` β `find_existing_workspace`
99
100### 2. Click-to-open from Closed groups goes through `find_existing_workspace`
101
102When a user clicks a thread under a `Closed` historical group header,
103`open_workspace_and_activate_thread` calls `open_paths` β
104`find_existing_workspace`, which routes to an existing workspace that contains
105the path instead of creating a new workspace tab.
106
107**Fix:** Either pass `open_new_workspace: Some(true)` through the call chain,
108or use a direct workspace creation path that bypasses `find_existing_workspace`.
109
110### 3. Best-workspace selection is O(group_size) per thread
111
112`group_workspaces.iter().find(|ws| ws.read(cx).path_list(cx) == row.folder_paths)`
113scans all workspaces in the group for each thread. Should pre-build a
114`HashMap<PathList, Entity<Workspace>>` per group for O(1) lookup.
115
116### 4. Label allocation in historical group sort
117
118`workspace_label_from_path_list` allocates a `SharedString` on every comparison
119during the sort. Should cache labels before sorting.
120
121### 5. Collapse state doesn't transfer between raw and canonical keys
122
123If a user collapses a historical group (keyed by raw `folder_paths`), then opens
124that workspace (which uses the canonical key), the collapse state doesn't
125transfer. Minor UX issue.
126
127### 6. Missing test coverage
128
129- Clicking a thread in a historical (Closed) group
130- The prune fix with `all_workspace_roots` vs snapshot-based check
131- Multiple worktree checkouts grouped under one header (dedicated test)
132
133### 7. Path set mutation (adding/removing folders)
134
135When you add a folder to a project (e.g. adding `ex` to a `zed` workspace),
136existing threads saved with `[zed]` don't match the new `[ex, zed]` path list.
137Design decision still being discussed.
138
139## Key code locations
140
141- **Thread metadata storage:** `crates/agent_ui/src/thread_metadata_store.rs`
142 - `SidebarThreadMetadataStore` β in-memory cache + SQLite DB
143 - `threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>>`
144- **Sidebar rebuild:** `crates/sidebar/src/sidebar.rs`
145 - `rebuild_contents()` β canonical-key grouping + historical groups
146 - `build_worktree_root_mapping()` β worktreeβroot path map
147 - `build_canonical_thread_index()` β threads indexed by canonical path
148 - `canonicalize_path_list()` β maps a PathList through the root mapping
149 - `thread_entry_from_metadata()` β helper for building ThreadEntry
150 - `prune_stale_worktree_workspaces()` β uses `all_workspace_roots`
151- **Thread saving:** `crates/agent/src/agent.rs`
152 - `NativeAgent::save_thread()` β snapshots `folder_paths` on every mutation
153- **Thread loading (session restore):** `crates/agent_ui/src/agent_panel.rs`
154 - `AgentPanel::load` (~L907-920) β deserializes last active thread
155- **Workspace opening:** `crates/workspace/src/workspace.rs`
156 - `find_existing_workspace()` β dedup/routing that swallows worktree checkouts
157 - `Workspace::new_local()` β creates workspace, canonicalizes paths
158 - `Workspace::path_list()` β returns PathList from visible worktrees
159- **Session restore:** `crates/workspace/src/workspace.rs`
160 - `restore_multiworkspace()` β restores workspace tabs from session DB
161- **PathList:** `crates/util/src/path_list.rs`
162
163## Useful debugging queries
164
165```sql
166-- All distinct folder_paths in the sidebar metadata store
167sqlite3 ~/Library/Application\ Support/Zed/db/0-{channel}/db.sqlite \
168 "SELECT folder_paths, COUNT(*) FROM sidebar_threads GROUP BY folder_paths ORDER BY COUNT(*) DESC"
169
170-- Find a specific thread
171sqlite3 ~/Library/Application\ Support/Zed/db/0-{channel}/db.sqlite \
172 "SELECT session_id, title, folder_paths FROM sidebar_threads WHERE title LIKE '%search term%'"
173
174-- Check workspace session bindings
175sqlite3 ~/Library/Application\ Support/Zed/db/0-{channel}/db.sqlite \
176 "SELECT workspace_id, paths, session_id, window_id FROM workspaces WHERE paths LIKE '%search%' ORDER BY timestamp DESC"
177```