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