plan.md

  1# Sidebar v2: Multi-Workspace Project Sidebar
  2
  3## Mental Model
  4
  5The sidebar is **not** a history view. It is the **window's workspace manager** — analogous to editor tabs, but for entire workspaces. Each OS window has its own sidebar with its own set of open projects/workspaces. This is ephemeral session state: started fresh per window, manually managed by the user. No cross-window synchronization.
  6
  7This means:
  8- Opening a window gives you one workspace (your initial project).
  9- You can add more workspaces to the window (open another project, create a new empty workspace, start a new thread in a new worktree).
 10- The sidebar shows all open workspaces grouped by project, with their threads listed underneath.
 11- Recent projects / history are **not** shown in the sidebar. That's a separate concern (file menu, command palette, etc.).
 12
 13## What the Screenshots Show
 14
 15### Left panel — the sidebar itself
 16
 171. **Title bar**: "Threads" label, close button (left), new-thread button (right, `+` with gear ⚙️).
 18
 192. **Project groups**: Threads are grouped by their worktree paths (the "project"). Each group has:
 20   - A **header** showing the project folder names (e.g. `ex`, `ex, zed`, `zed`) with a colored sidebar indicator showing workspace associations.
 21   - **Thread entries** underneath, each showing:
 22     - Agent icon (Zed Agent, Claude, Codex CLI, etc.)
 23     - Thread title (truncated with `...`)
 24     - Optional: author name (e.g. `olivetti`, `rosewood`)
 25     - Optional: diff stats (`+21 -12`)
 26     - Timestamp (e.g. `5:45 PM`, `1d`, `3d`)
 27   - A **"+ View More"** link at the bottom of groups with many threads.
 28
 293. **Project group actions** (visible on hover / right-click):
 30   - "Remove Project" (with keybinding)
 31   - "Collapse Project" (with keybinding)
 32
 334. **New Thread dropdown** (from `+` button):
 34   - "New Thread in..."
 35     - "Current Project"
 36     - "New Worktree"
 37
 385. **Agent picker** (dropdown from agent selector in toolbar):
 39   - "Zed Agent" (default)
 40   - "External Agents" section: Claude Code, Codex CLI, Gemini CLI, OpenCode
 41   - "+ Add More Agents"
 42
 436. **Search bar** at top for filtering threads.
 44
 45---
 46
 47## Current State (what exists today)
 48
 49The current `Sidebar` in `crates/sidebar/src/sidebar.rs` is a flat list using a `Picker` with `WorkspacePickerDelegate`. It has:
 50
 51- **`WorkspaceThreadEntry`**: One entry per workspace, showing worktree label + the active thread's title/status.
 52- **`SidebarEntry`**: Either a separator, a workspace-thread entry, or a recent project.
 53- **Recent projects**: Fetched from disk, shown below active workspaces, time-bucketed (Today, Yesterday, etc.).
 54- **Notifications**: Tracks when background workspaces finish generating.
 55- **Search**: Fuzzy matching across workspace names and recent project names.
 56
 57### Key gaps vs. the target design
 58
 59| Feature | Current | Target |
 60|---------|---------|--------|
 61| Data model | Flat list of workspaces | Flat `ListEntry` enum with project headers + threads |
 62| Threads shown | Only the *active* thread per workspace | *All* threads for each project group |
 63| Recent projects | Shown in sidebar | **Removed** from sidebar |
 64| Grouping | None (flat list with separators) | By worktree paths (project headers in flat list) |
 65| Thread source | `AgentPanel.active_thread_view()` | `ThreadStore.threads_for_paths()` for saved threads + active thread from `AgentPanel` |
 66| Collapsible groups | No | Yes |
 67| "View More" pagination | No | Yes (show N most recent, expand on click) |
 68| Project actions | Remove workspace only | Remove project, collapse project |
 69| New thread flow | Creates empty workspace | "New Thread in Current Project" / "New Worktree" |
 70| Workspace color indicators | None | Colored vertical bars per workspace |
 71| Rendering | `Picker` with `PickerDelegate` | `ListState` + `render_list_entry` (collab_panel pattern) |
 72
 73---
 74
 75## Implementation Plan
 76
 77### Phase 1: New Data Model & List Infrastructure
 78
 79**Goal**: Replace the current `WorkspaceThreadEntry` / `SidebarEntry` / `Picker` model with a flat `ListEntry` enum and `ListState`-based rendering, following the pattern established by `collab_panel.rs`. Remove recent projects from the sidebar entirely.
 80
 81The key insight from `collab_panel` is: **don't model the hierarchy in your data structures — model it in your `update_entries()` function**. You have a flat `Vec<ListEntry>` and a single `update_entries()` method that walks the data sources (workspaces, thread store) and pushes entries in the right order. The grouping is implicit in the push order.
 82
 83#### 1.1 Remove recent projects
 84
 85- Remove `RecentProjectEntry` from the entry enum.
 86- Remove `recent_projects`, `recent_project_thread_titles`, `_fetch_recent_projects` from the sidebar.
 87- Remove `get_recent_projects` dependency and time-bucketing logic (`TimeBucket`, etc.).
 88- Remove `recent_projects` dependency from `Cargo.toml`.
 89
 90#### 1.2 Define a flat `ListEntry` enum
 91
 92Replace `SidebarEntry`, `WorkspaceThreadEntry`, `SidebarMatch`, and the `WorkspacePickerDelegate` with a single flat enum. Each variant carries all the data it needs to render, just like `collab_panel::ListEntry`:
 93
 94```rust
 95enum ListEntry {
 96    /// A project group header (e.g. "ex", "ex, zed", "zed").
 97    /// Not selectable. Clicking toggles collapse.
 98    ProjectHeader {
 99        path_list: PathList,
100        label: SharedString,
101    },
102    /// A thread belonging to the project group above it.
103    Thread {
104        session_id: acp::SessionId,
105        title: SharedString,
106        icon: IconName,
107        status: AgentThreadStatus,
108        updated_at: DateTime<Utc>,
109        diff_stats: Option<(usize, usize)>,
110        /// If this thread is actively running in a workspace, which one.
111        workspace_index: Option<usize>,
112    },
113    /// "+ View More" link at the end of a project group.
114    ViewMore {
115        path_list: PathList,
116        remaining_count: usize,
117    },
118}
119```
120
121Auxiliary state lives on `Sidebar` itself, not in the entries:
122- `collapsed_groups: HashSet<PathList>` — which project groups are collapsed.
123- `expanded_groups: HashSet<PathList>` — which groups have "View More" expanded (default is collapsed to N items).
124- `selection: Option<usize>` — index into `entries`.
125- `entries: Vec<ListEntry>` — the flat list, rebuilt on every change.
126
127#### 1.3 Replace Picker with ListState-based rendering
128
129Drop the `Picker<WorkspacePickerDelegate>` entirely. Instead, follow the collab_panel pattern:
130
131- `Sidebar` owns a `ListState`, a `Vec<ListEntry>`, an optional `selection: Option<usize>`, and a search `Editor`.
132- Render with `list(self.list_state.clone(), cx.processor(Self::render_list_entry)).size_full()`.
133- `render_list_entry(&mut self, ix: usize, window, cx) -> AnyElement` matches on `self.entries[ix]` and dispatches to `render_project_header()`, `render_thread()`, `render_view_more()`.
134- Keyboard nav (`select_next`, `select_previous`, `confirm`) is implemented directly on `Sidebar` via action handlers, same as collab_panel.
135
136This gives us full-width rendering for every item (no picker chrome), collapsible headers, and direct control over the list.
137
138#### 1.4 Build the flat list in `update_entries()`
139
140A single `update_entries(&mut self, cx)` method (called whenever workspaces or threads change) rebuilds `self.entries` from scratch:
141
1421. Gather open workspaces from `MultiWorkspace.workspaces()`. For each, compute its `PathList` from worktree paths.
1432. For each workspace's `PathList`, query `ThreadStore::global(cx).threads_for_paths(&path_list)` to get saved threads for that project.
1443. List workspace groups in workspace creation order (i.e. their order in `MultiWorkspace.workspaces()`).
1454. For each workspace group:
146   - Push `ListEntry::ProjectHeader { path_list, label }` (always visible, even when collapsed).
147   - If not in `collapsed_groups`, push `ListEntry::Thread { ... }` for each thread (active thread from `AgentPanel` merged with saved threads from `threads_for_paths()`, deduped by session ID, sorted by `updated_at` descending).
148   - If there are more than N threads and the group isn't in `expanded_groups`, push only the first N threads then `ListEntry::ViewMore { remaining_count }`.
1495. Update `self.list_state` item count.
150
151This is the same imperative "walk and push" pattern as `collab_panel::update_entries`.
152
153#### 1.5 Subscribe to data sources
154
155Add subscriptions so `update_entries()` is called when data changes:
156- **`MultiWorkspace`** (already exists): workspace added/removed/activated.
157- **`ThreadStore::global(cx)`** (new): threads saved/deleted/reloaded.
158- **Per-workspace `AgentPanel`** (already exists): active thread changes, thread status changes.
159- **Per-workspace `Project`** (already exists): worktree added/removed (changes which group a workspace belongs to).
160
161#### 1.6 Tests for `update_entries()`
162
163Use visual snapshot tests following the `project_panel_tests.rs` pattern. Write a `visible_entries_as_strings` helper that reads `self.entries` and formats each `ListEntry` into a human-readable string, then assert against expected output.
164
165**Helper function**:
166
167The helper should show collapse state on project headers (`>` collapsed, `v` expanded), and selection state (`<== selected`) on any entry — mirroring the project panel pattern:
168
169```rust
170fn visible_entries_as_strings(
171    sidebar: &Entity<Sidebar>,
172    cx: &mut VisualTestContext,
173) -> Vec<String> {
174    sidebar.read_with(cx, |sidebar, _cx| {
175        sidebar.entries.iter().enumerate().map(|(ix, entry)| {
176            let selected = if sidebar.selection == Some(ix) {
177                "  <== selected"
178            } else {
179                ""
180            };
181            match entry {
182                ListEntry::ProjectHeader { label, path_list, .. } => {
183                    let icon = if sidebar.collapsed_groups.contains(path_list) {
184                        ">"
185                    } else {
186                        "v"
187                    };
188                    format!("{} [{}]{}", icon, label, selected)
189                }
190                ListEntry::Thread { title, status, workspace_index, .. } => {
191                    let active = if workspace_index.is_some() { " *" } else { "" };
192                    let status_str = match status {
193                        AgentThreadStatus::Running => " (running)",
194                        AgentThreadStatus::Error => " (error)",
195                        _ => "",
196                    };
197                    format!("  {}{}{}{}", title, active, status_str, selected)
198                }
199                ListEntry::ViewMore { remaining_count, .. } => {
200                    format!("  + View More ({}){}", remaining_count, selected)
201                }
202            }
203        }).collect()
204    })
205}
206```
207
208**Test cases**:
209
2101. **Single workspace, no threads**:
211   ```
212   v [my-project]
213   ```
214
2152. **Single workspace with threads from ThreadStore**:
216   ```
217   v [my-project]
218     Fix crash in project panel
219     Add inline diff view
220     Build a task runner panel
221   ```
222
2233. **Multiple workspaces, each with their own threads**:
224   ```
225   v [project-a]
226     Thread A1 * (running)
227     Thread A2
228   v [project-b]
229     Thread B1
230   ```
231
2324. **View More when threads exceed N**:
233   ```
234   v [my-project]
235     Thread 1
236     Thread 2
237     Thread 3
238     Thread 4
239     Thread 5
240     + View More (7)
241   ```
242
2435. **Active thread from AgentPanel merged with saved threads**: the active thread appears in the list with `*` marker and is deduped against the ThreadStore copy.
244
2456. **Adding a workspace updates entries**: create a second workspace, assert it appears as a new project header with its threads.
246
2477. **Removing a workspace updates entries**: remove a workspace, assert its project header and threads are gone.
248
2498. **Worktree change updates group label**: add a folder to a workspace, assert the project header label updates (e.g. `v [ex]` → `v [ex, zed]`).
250
251### Phase 2: Rendering List Entries
252
253**Goal**: Implement the `render_list_entry` dispatcher and per-variant render methods.
254
255Each method returns a full-width `AnyElement`. No picker chrome, no indentation magic — each entry is a top-level item in the list, same as collab_panel.
256
257#### 2.1 Render project group headers
258
259Each `ProjectHeader` renders as:
260- The group label (derived from folder names, e.g. "ex, zed")
261- A collapse/expand chevron
262- On hover: action buttons (remove, collapse keybindings)
263
264Headers are not selectable (skipped by keyboard nav).
265
266#### 2.2 Render thread items
267
268Each thread renders using the existing `ThreadItem` component, which already supports:
269- Icon, title, timestamp
270- Diff stats (`.added()`, `.removed()`)
271- Running/completed/error status
272- Selected/hovered state
273- Action slot (for context menu or remove button)
274
275New additions needed for `ThreadItem`:
276- Author/branch name display (visible in screenshots as "olivetti", "rosewood")
277
278#### 2.3 Render "View More" items
279
280When a project group has more than N threads (e.g. 5), show only the N most recent and add a "+ View More" item. Clicking it adds the group's `PathList` to `self.expanded_groups` and calls `update_entries()`.
281
282#### 2.4 Implement collapse/expand
283
284Clicking a project group header or using the keybinding toggles membership in `self.collapsed_groups` and calls `update_entries()`. When collapsed, the group's threads and "View More" are not pushed into `entries` at all — the header is still visible.
285
286#### 2.5 Keyboard navigation and selection
287
288Implement directly on `Sidebar` (no Picker):
289- `select_next` / `select_previous` actions: move `self.selection`, skipping `ProjectHeader` entries.
290- `confirm` action: if selection is a `Thread`, activate its workspace or open it. If `ViewMore`, expand the group.
291- Track `selection: Option<usize>` and pass `is_selected` to render methods for highlighting.
292
293#### 2.6 Tests for collapse/expand, View More expansion, and selection
294
295Reuse `visible_entries_as_strings` from 1.6 — the `>` / `v` and `<== selected` markers make these behaviors directly assertable.
296
2971. **Collapsed group hides its threads**:
298   ```
299   > [project-a]
300   v [project-b]
301     Thread B1
302   ```
303
3042. **Expanding a collapsed group shows threads again**:
305   ```
306   v [project-a]
307     Thread A1
308     Thread A2
309   v [project-b]
310     Thread B1
311   ```
312
3133. **Expanding View More shows all threads**: start with `+ View More (7)`, click it, assert all 12 threads appear and "View More" is gone.
314
3154. **Selection skips headers**:
316   ```
317   v [project-a]
318     Thread A1  <== selected
319     Thread A2
320   v [project-b]
321     Thread B1
322   ```
323   After `select_next`:
324   ```
325   v [project-a]
326     Thread A1
327     Thread A2  <== selected
328   v [project-b]
329     Thread B1
330   ```
331   After `select_next` again (jumps over header):
332   ```
333   v [project-a]
334     Thread A1
335     Thread A2
336   v [project-b]
337     Thread B1  <== selected
338   ```
339
3405. **Confirm on selection activates workspace**: select a thread with `workspace_index: Some(1)`, confirm, assert `MultiWorkspace.active_workspace_index()` changed.
341
342### Phase 3: Project Group Actions
343
344**Goal**: Implement the context menu actions visible in the screenshots.
345
346#### 3.1 "Remove Project"
347
348- Removes all workspaces associated with this project group from the `MultiWorkspace`.
349- If there are no open workspaces for the group (it's only showing historical threads), this is a no-op or hides the group.
350- Keybinding: `Shift-Cmd-Backspace` (from screenshot)
351
352#### 3.2 "Collapse Project"
353
354- Toggles the collapsed state of the project group.
355- Keybinding: `Ctrl-Cmd-[` (from screenshot)
356
357#### 3.3 "New Thread" dropdown
358
359The `+` button in the header should show a popover/context menu:
360- **"Current Project"**: Creates a new thread in the currently active workspace's project. This means creating a new agent thread in the existing workspace's `AgentPanel`.
361- **"New Worktree"**: Creates a new empty workspace (existing `create_workspace` behavior) — prompts for a folder to open and starts a thread there.
362
363### Phase 5: Search
364
365**Goal**: The search behavior changes because we now search across thread titles (not just workspace names).
366
367#### 5.1 Update search candidates
368
369The fuzzy search should match against:
370- Thread titles
371- Project group labels (folder names)
372
373When filtering, show matching threads under their group headers. Hide groups with no matching threads.
374
375### Phase 6: Thread Lifecycle Integration
376
377**Goal**: Ensure the sidebar correctly reflects thread state changes in real time.
378
379#### 6.1 Live thread status
380
381For threads that are actively running in a workspace:
382- Subscribe to the workspace's `AgentPanel` events and the active `AcpThread` entity.
383- Update status (Running → Completed → Error) in real time.
384- The notification system (badge on sidebar toggle button) should continue working.
385
386#### 6.2 Thread saving
387
388When a thread is saved (via `ThreadStore.save_thread`), the `ThreadStore` reloads and notifies observers. The sidebar's `ThreadStore` subscription picks this up and rebuilds entries.
389
390#### 6.3 Thread switching within a workspace
391
392When the user switches threads within a workspace's `AgentPanel`, the sidebar should update to reflect which thread is active/selected.
393
394---
395
396## Execution Order & Dependencies
397
398```
399Phase 1 (Data Model & List Infrastructure)
400  ├── 1.1 Remove recent projects (pure deletion)
401  ├── 1.2 Define ListEntry enum
402  ├── 1.3 Replace Picker with ListState (depends on 1.2)
403  ├── 1.4 Build flat list in update_entries() (depends on 1.2)
404  ├── 1.5 Subscribe to data sources (depends on 1.4)
405  └── 1.6 Tests for update_entries() (depends on 1.4, 1.5)
406
407Phase 2 (Rendering) — depends on Phase 1
408  ├── 2.1 Render project headers
409  ├── 2.2 Render thread items
410  ├── 2.3 Render "View More"
411  ├── 2.4 Collapse/expand
412  └── 2.5 Keyboard navigation
413
414Phase 3 (Actions) — depends on Phase 2
415  ├── 3.1 Remove Project
416  ├── 3.2 Collapse Project
417  └── 3.3 New Thread dropdown
418
419Phase 4 (Color Indicators) — depends on Phase 2
420Phase 5 (Search) — depends on Phase 2
421Phase 6 (Thread Lifecycle) — depends on Phase 1
422```
423
424Phases 1 and 6 are the most critical — they determine the data flow. Phases 2-3 are the UI work. Phases 4-5 are polish.
425
426## What's Explicitly Deferred
427
428- **Git worktree integration**: The "New Worktree" option in the new-thread dropdown hints at this, but the full git-worktree-based workflow (create branch → create worktree → open workspace) is a follow-up.
429- **Cross-window sidebar sync**: Explicitly not doing this. Each window manages its own sidebar state.
430- **Recent projects in sidebar**: Removed. Recent projects are accessible via command palette / file menu.
431- **Thread history browsing**: The sidebar shows threads for *open* project groups. Full thread history browsing is a separate feature.
432- **Collaborative features**: The "people" icon in the bottom toolbar is deferred.
433- **Bottom toolbar icons**: Exact functionality of the bottom icon row needs clarification — implement container only.
434
435## Files to Modify
436
437| File | Changes |
438|------|---------|
439| `crates/sidebar/src/sidebar.rs` | Major rewrite: `ListEntry` enum, `ListState` rendering, `update_entries()`, remove Picker |
440| `crates/sidebar/Cargo.toml` | Remove `recent_projects` dep, add `agent` dep (for `ThreadStore`, `DbThreadMetadata`) |
441| `crates/workspace/src/multi_workspace.rs` | Possibly add helper methods for project-group-level operations |
442| `crates/ui/src/components/ai/thread_item.rs` | May need author/branch name field |
443| `crates/agent/src/thread_store.rs` | May need additional query methods (e.g. threads grouped by path_list) |