PLAN.md

  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```