History

Update multi-workspace refactor plan with simplified phased approach

Key changes: - Phase A: Mechanical extract-class refactor (low risk, ship first) - Phase B: Enable second workspace, refactor stores as needed - Defer store complexity until we have test infrastructure - Focus on testability as the driving motivation

Mikayla Maki committed 10 months ago

Commit 00a791c1 parent a7ba4ad6 files · +110 −2964Browse files

Unified · Split

6 files
  1. cross-workspace-threads-plan.md +0 −289
  2. multi-workspace-refactor-prompt.md +110 −125
  3. phase-2-multiworkspace-plan.md +0 −1067
  4. pluggable-shaders-gpui-plan.md +0 −408
  5. threads-sidebar-plan.md +0 −921
  6. utility-pane-removal-plan.md +0 −154
cross-workspace-threads-plan.md+0−289
@@ -1,289 +0,0 @@
1−# Cross-Workspace Thread Handling Plan
2−
3−## Overview
4−
5−This document outlines the design for handling agent threads across multiple workspaces, turning "zombie threads" from a bug into a feature: **background threads**.
6−
7−## Problem Statement
8−
9−Opening the same thread in multiple workspaces currently has significant issues:
10−
11−1. **Flow disruption**: Opening a thread in two windows causes them to get out of sync
12−2. **Zombie threads**: Closing a window does NOT stop the thread from running - it continues processing in the background
13−4. **Split brain**: Each workspace creates its own `AcpThread` entity with no coordination
14−
15−### Current Architecture
16−
17−| Aspect | Current Behavior |
18−|--------|------------------|
19−| Thread data | Persisted to SQLite database, shared across workspaces |
20−| Running state | Per-workspace (no coordination) |
21−| Thread views | Each workspace creates its own `AcpThreadView` and `AcpThread` entity |
22−| Coordination | None between workspaces for the same thread |
23−
24−## Research Findings
25−
26−### AcpThread is Project-Scoped
27−
28−`AcpThread` takes an `Entity<Project>` and uses it for:
29−- **Language registry** - syntax highlighting in markdown/diffs
30−- **Path style** - Windows vs Unix path formatting
31−- **Git store** - checkpoints and restore functionality
32−- **Buffer operations** - `read_text_file`, `write_text_file` via `project.open_buffer()`
33−- **Terminal environment** - directory environment for spawned terminals
34−- **Path resolution** - `project_path_for_absolute_path()`
35−
36−**Implication**: Threads cannot be meaningfully shared across projects. A thread from Project A cannot operate on Project B's files.
37−
38−### No Cancellation on Drop
39−
40−- `AcpThread` has no `Drop` implementation
41−- When `AcpThreadView` is dropped, it only cleans up notification windows
42−- The `send_task` (background LLM work) continues running when entities are dropped
43−- `cancel()` must be called explicitly to stop a running thread
44−
45−### Existing Patterns in Zed
46−
47−- **`BufferStore`**: Per-project store that provides `Entity<Buffer>` instances. Multiple `Editor`s can display the same buffer. Uses `WeakEntity` and deduplicates concurrent loads.
48−- **`ActiveCall`**: Global singleton for call/room state across workspaces. Uses `impl Global` pattern.
49−- **`WorkspaceStore`**: Tracks all open workspaces via `HashSet<WindowHandle<Workspace>>`.
50−
51−## Proposed Solution: AgentThreadStore
52−
53−Create a new **global** `AgentThreadStore` in a new `agent_2` crate that:
54−1. **Owns running threads** - holds strong references to keep them alive
55−2. **Tracks view state** - which window (if any) is viewing each thread
56−3. **Enables background threads** - threads continue running when windows close
57−
58−### Design
59−
60−```rust
61−// crates/agent_2/src/agent_thread_store.rs
62−
63−pub struct AgentThreadStore {
64− /// Thread metadata from database (the "history")
65− threads: Vec<DbThreadMetadata>,
66−
67− /// Running threads - store holds strong reference to keep them alive
68− /// These continue running even if no window is viewing them
69− running_threads: HashMap<acp::SessionId, Entity<AcpThread>>,
70−
71− /// Which window (if any) is currently viewing each running thread
72− /// None = running in background, Some(window) = open in that window
73− thread_views: HashMap<acp::SessionId, WindowHandle<Workspace>>,
74−
75− /// Recently opened for quick-access UI
76− recently_opened: VecDeque<acp::SessionId>,
77−}
78−
79−/// State of a thread for UI display
80−pub enum ThreadState {
81− /// Not running (historical entry only)
82− Idle,
83− /// Running and visible in a window
84− Running { window: WindowHandle<Workspace> },
85− /// Running in background (no window viewing it)
86− Background,
87−}
88−
89−pub enum AgentThreadStoreEvent {
90− /// History list changed (thread added/removed/updated)
91− HistoryChanged,
92− /// A thread started running
93− ThreadStarted(acp::SessionId),
94− /// A thread stopped running
95− ThreadStopped(acp::SessionId),
96− /// A thread's view state changed (attached/detached from window)
97− ThreadViewChanged(acp::SessionId),
98−}
99−
100−/// Result of trying to open a thread
101−pub enum OpenResult {
102− /// Thread opened/attached in this window
103− Opened(Entity<AcpThread>),
104− /// Thread is open in a different window - that window's handle
105− OpenElsewhere(WindowHandle<Workspace>),
106−}
107−
108−impl AgentThreadStore {
109− // --- Global access ---
110− pub fn global(cx: &mut App) -> Entity<Self>;
111−
112− // --- History (read from database) ---
113− pub fn history(&self) -> &[DbThreadMetadata];
114− pub fn recently_opened(&self) -> impl Iterator<Item = &DbThreadMetadata>;
115− pub fn reload(&mut self, cx: &mut Context<Self>) -> Task<()>;
116−
117− // --- Thread state ---
118−
119− /// Get state of a thread for UI display
120− pub fn thread_state(&self, session_id: &acp::SessionId) -> ThreadState;
121−
122− /// Open/attach to a thread in this window.
123− /// - If idle: caller should create thread, then call `register_running`
124− /// - If background: attaches to this window
125− /// - If open elsewhere: returns that window handle
126− pub fn open_thread(
127− &mut self,
128− session_id: &acp::SessionId,
129− window: WindowHandle<Workspace>,
130− cx: &mut Context<Self>,
131− ) -> OpenResult;
132−
133− /// Register a newly created running thread
134− pub fn register_running(
135− &mut self,
136− session_id: acp::SessionId,
137− thread: Entity<AcpThread>,
138− window: WindowHandle<Workspace>,
139− cx: &mut Context<Self>,
140− );
141−
142− /// Detach from a thread (window closing or user explicitly detaching)
143− /// Thread keeps running in background
144− pub fn detach_thread(
145− &mut self,
146− session_id: &acp::SessionId,
147− cx: &mut Context<Self>,
148− );
149−
150− /// Stop a thread completely (user explicitly stopping)
151− pub fn stop_thread(
152− &mut self,
153− session_id: &acp::SessionId,
154− cx: &mut Context<Self>,
155− );
156−
157− // --- Recently opened management ---
158− pub fn mark_recently_opened(
159− &mut self,
160− session_id: acp::SessionId,
161− cx: &mut Context<Self>,
162− );
163−}
164−```
165−
166−### Usage Flow
167−
168−**Opening a thread from history:**
169−```rust
170−fn open_thread(&mut self, session_id: acp::SessionId, window: &mut Window, cx: &mut Context<Self>) {
171− let store = AgentThreadStore::global(cx);
172− let this_window = window.window_handle().downcast::<Workspace>().unwrap();
173−
174− let state = store.read(cx).thread_state(&session_id);
175−
176− match state {
177− ThreadState::Running { window } if window == this_window => {
178− // Already open here, just focus it
179− self.focus_thread_view(cx);
180− }
181− ThreadState::Running { window } => {
182− // Open in another window, activate that
183− cx.activate_window(window);
184− }
185− ThreadState::Background => {
186− // Running in background, attach to this window
187− store.update(cx, |store, cx| {
188− store.open_thread(&session_id, this_window, cx);
189− });
190− self.show_thread_view(session_id, cx);
191− }
192− ThreadState::Idle => {
193− // Not running, need to create/resume
194− let thread = self.create_thread(session_id.clone(), cx);
195− store.update(cx, |store, cx| {
196− store.register_running(session_id, thread, this_window, cx);
197− });
198− }
199− }
200−}
201−```
202−
203−**Window close handling:**
204−```rust
205−fn on_window_close(&mut self, window: WindowHandle<Workspace>, cx: &mut App) {
206− let store = AgentThreadStore::global(cx);
207− store.update(cx, |store, cx| {
208− // Detach all threads viewed in this window - they become background threads
209− for (session_id, view_window) in store.thread_views.iter() {
210− if *view_window == window {
211− store.detach_thread(session_id, cx);
212− }
213− }
214− });
215−}
216−```
217−
218−**History view rendering:**
219−```rust
220−fn render_history_entry(&self, entry: &DbThreadMetadata, cx: &App) -> impl IntoElement {
221− let store = AgentThreadStore::global(cx);
222− let state = store.read(cx).thread_state(&entry.id);
223−
224− let status_indicator = match state {
225− ThreadState::Running { window } if window == self.this_window => {
226− Icon::new(IconName::Play).color(Color::Success) // 🟢 Running here
227− }
228− ThreadState::Running { .. } => {
229− Icon::new(IconName::Play).color(Color::Info) // 🔵 Running elsewhere
230− }
231− ThreadState::Background => {
232− Icon::new(IconName::CloudUpload).color(Color::Warning) // 🟡 Background
233− }
234− ThreadState::Idle => {
235− Icon::new(IconName::Circle).color(Color::Muted) // ⚪ Idle
236− }
237− };
238−
239− div()
240− .child(status_indicator)
241− .child(Label::new(&entry.title))
242−}
243−```
244−
245−### Key Design Decisions
246−
247−1. **Global store owns running threads**: Strong `Entity<AcpThread>` references keep threads alive
248−2. **Background threads are a feature**: Closing a window detaches, doesn't kill
249−3. **Single view per thread**: Only one window can view a thread at a time
250−4. **Redirect, don't duplicate**: If a thread is open elsewhere, redirect to that window
251−
252−## Future Vision
253−
254−This refactor sets the foundation for a deeper architectural change: **threads as the primary object**.
255−
256−In the future, threads will become richer objects that the editor UI is downstream of:
257−- Each thread has its own worktrees
258−- Each thread has its own workspace state
259−- The editor becomes a view into a thread's state
260−- Multiple threads can be running simultaneously, each with their own context
261−
262−This is similar to how modern AI coding tools treat "sessions" or "conversations" as first-class workspace containers.
263−
264−The current refactor provides immediate value by:
265−1. ✅ Fixing the split-brain problem
266−2. ✅ Enabling background threads as a feature
267−3. ✅ Establishing the `AgentThreadStore` as the single source of truth
268−4. ✅ Setting up the ownership model for the future vision
269−
270−## Implementation Plan
271−
272−1. [ ] Create new `agent_2` crate
273−2. [ ] Implement `AgentThreadStore` with global access pattern
274−3. [ ] Implement thread state tracking (`running_threads`, `thread_views`)
275−4. [ ] Add `open_thread` / `detach_thread` / `stop_thread` APIs
276−5. [ ] Wire up `AgentsPanel` (v2) to use `AgentThreadStore`
277−6. [ ] Add window close handling to detach threads
278−7. [ ] Update history view to show thread state (running/background/idle)
279−8. [ ] Add tests for cross-workspace scenarios
280−9. [ ] Add UI affordances for background threads (attach, stop, etc.)
281−
282−## Related Files
283−
284−- `crates/agent_ui_v2/src/agents_panel.rs` - Will consume AgentThreadStore
285−- `crates/agent_ui_v2/src/agent_thread_pane.rs` - Thread pane management
286−- `crates/acp_thread/src/acp_thread.rs` - Thread entity and lifecycle
287−- `crates/agent/src/history_store.rs` - Existing history store (for reference)
288−- `crates/agent/src/db.rs` - Thread database types (DbThreadMetadata, etc.)
289−- `crates/workspace/src/workspace.rs` - Workspace lifecycle, WorkspaceStore pattern
multi-workspace-refactor-prompt.md+110−125
@@ -6,7 +6,7 @@ We are refactoring Zed to support multiple Workspaces that share worktrees. The · 5 unmodified lines
66
77 ### Current Architecture
88 ```
9−Workspace
9+Workspace (being renamed to MultiWorkspace)
1010 └── Entity<Project>
1111 └── Entity<WorktreeStore> // Project creates and owns this
1212 └── Entity<Worktree>...
@@ -16,10 +16,10 @@ Workspace · 3 unmodified lines
1616 └── ... other stores
1717 ```
1818
19−### New Architecture
19+### Target Architecture
2020 ```
21−MultiWorkspace (new top-level container)
22−├── Entity<WorktreeStore> // Shared, contains mix of local AND remote worktrees
21+MultiWorkspace (window-level container)
22+├── Entity<WorktreeStore> // Shared, eventually contains mix of local AND remote worktrees
2323 ├── Entity<BufferStore> // Shared - same file = same buffer
2424 ├── Entity<GitStore> // Shared - git state is per-repo
2525 ├── Entity<LspStore> // Shared - one LSP instance per worktree
@@ -33,6 +33,7 @@ MultiWorkspace (new top-level container) · 7 unmodified lines
3333 │ ├── Entity<DapStore> // Per-workspace (debugging sessions)
3434 │ ├── Entity<TaskStore> // Per-workspace (task execution context)
3535 │ └── (references to shared stores)
36+│ └── Panes, Panels, Docks (UI)
3637 │
3738 └── Workspace B
3839 └── Entity<Project>
@@ -40,40 +41,115 @@ MultiWorkspace (new top-level container) · 1 unmodified line
4041 ├── client_state: ProjectClientState
4142 ├── Entity<DapStore>
4243 └── Entity<TaskStore>
44+ └── Panes, Panels, Docks (UI)
4345 ```
4446
47+## Implementation Strategy
48+
49+The key insight is: **defer store complexity until we actually have two workspaces trying to share them**. This gives us concrete test cases and clearer requirements.
50+
51+### Phase A: Structural Rename (Ship First)
52+
53+**Goal**: Extract `Workspace` from `MultiWorkspace` as a mechanical refactor with no behavior change.
54+
55+**Current State**:
56+- `Workspace` has been renamed to `MultiWorkspace` ✅
57+- Stub `single_workspace::Workspace` module created ✅
58+
59+**What Moves to `Workspace`** (the new inner struct):
60+- `Entity<Project>` - workspace owns its project
61+- `center: PaneGroup` - pane layout
62+- `panes: Vec<Entity<Pane>>` - all panes
63+- `active_pane: Entity<Pane>`
64+- `left_dock`, `right_dock`, `bottom_dock` - dock UI
65+- `status_bar` - per-workspace status
66+- `modal_layer`, `toast_layer` - per-workspace overlays
67+- `notifications` - per-workspace notifications
68+- Most UI rendering logic
69+
70+**What Stays in `MultiWorkspace`**:
71+- `Entity<WorktreeStore>` - shared (passed down to Projects)
72+- `Entity<Workspace>` - just one for now
73+- `weak_self`, `app_state` - window-level concerns
74+- `workspace_actions` - action registration
75+- `database_id`, serialization concerns
76+- `WorkspaceStore` registration
77+- Window title management
78+
79+**This is "extract class" refactoring** - moving fields/methods down without changing behavior. Risk is low because it's mechanical.
80+
81+**Merge Point**: Ship this rename, get it tested in the wild.
82+
83+### Phase B: Enable Second Workspace (Real Refactor)
84+
85+**Goal**: Actually support multiple Workspace instances, refactoring stores as needed.
86+
87+**Why This Order?**: You can't write integration tests for shared state without two workspaces. Once you have:
88+
89+```rust
90+struct MultiWorkspace {
91+ worktree_store: Entity<WorktreeStore>,
92+ workspaces: Vec<Entity<Workspace>>,
93+}
94+```
95+
96+Then you can write tests like:
97+
98+```rust
99+#[gpui::test]
100+async fn test_two_workspaces_share_buffer(cx: &mut TestAppContext) {
101+ let multi = cx.new(|cx| MultiWorkspace::new(...));
102+
103+ // Add a worktree to the shared store
104+ let worktree_id = multi.update(cx, |m, cx| m.add_worktree("/path", cx)).await;
105+
106+ // Create two workspaces viewing the same worktree
107+ let ws_a = multi.update(cx, |m, cx| m.add_workspace(cx));
108+ let ws_b = multi.update(cx, |m, cx| m.add_workspace(cx));
109+
110+ // Open same file in both, verify it's the same buffer
111+ // Edit in one, verify it appears in the other
112+}
113+```
114+
115+**Store Refactors** (driven by test failures):
116+1. `WorktreeStore` - Remove `WorktreeStoreState` enum, push local/remote distinction into individual worktrees
117+2. `BufferStore` - Lift to MultiWorkspace so same file = same buffer
118+3. `GitStore` - Lift to MultiWorkspace (git state is per-repo)
119+4. `LspStore` - Lift to MultiWorkspace (one LSP per worktree)
120+5. Keep `DapStore`, `TaskStore` per-workspace
121+
122+**Project Changes**:
123+- Add `worktree_ids: HashSet<WorktreeId>` to filter which worktrees it "views"
124+- `Project::worktrees()` filters by this set
125+- Project receives shared store references from MultiWorkspace
126+
45127 ## Key Design Decisions
46128
47−### 1. WorktreeStore Contains Mixed Local/Remote Worktrees
129+### 1. WorktreeStore Will Contain Mixed Local/Remote Worktrees
48130
49−The shared WorktreeStore can contain both local and remote worktrees simultaneously:
131+Eventually the shared WorktreeStore can contain both local and remote worktrees:
50132 - `LocalWorktree` for `~/projects/foo` (local filesystem)
51133 - `RemoteWorktree` for `server-a:/home/user/bar` (SSH connection)
52−- `RemoteWorktree` for `server-b:/work/baz` (different SSH connection)
53134
54−This works because **transport lives at the Worktree level**, not WorktreeStore level:
135+This works because **transport lives at the Worktree level**:
55136 - `LocalWorktree` has `fs: Arc<dyn Fs>`
56137 - `RemoteWorktree` has `client: AnyProtoClient` and `project_id: u64`
57138
58−When BufferStore opens a file, it delegates to the Worktree via `worktree.load_file()`, so the Worktree handles its own transport.
59−
60−**Required change**: Remove `WorktreeStoreState` enum (currently Local vs Remote). Instead, provide explicit methods for adding local vs remote worktrees:
139+**Required change** (Phase B): Remove `WorktreeStoreState` enum. Add explicit methods:
61140 ```rust
62141 impl WorktreeStore {
63− pub fn add_local_worktree(&mut self, path: &Path, fs: Arc<dyn Fs>, cx: ...) -> Task<...>
142+ pub fn add_local_worktree(&mut self, path: &Path, fs: Arc<dyn Fs>, ...) -> Task<...>
64143 pub fn add_remote_worktree(&mut self, client: AnyProtoClient, project_id: u64, ...) -> Task<...>
65144 }
66145 ```
67146
68147 ### 2. Project Becomes a "View" Over Shared Worktrees
69148
70−Project tracks which worktrees it contains via `worktree_ids: HashSet<WorktreeId>`:
71−
72149 ```rust
73150 struct Project {
74151 worktree_ids: HashSet<WorktreeId>,
75152 worktree_store: Entity<WorktreeStore>, // Reference to shared store
76− // ...
77153 }
78154
79155 impl Project {
@@ -85,38 +161,28 @@ impl Project { · 5 unmodified lines
85161 }
86162 ```
87163
88−Worktrees are **oblivious to Projects** - they don't track which Projects reference them. This keeps the relationship clean and unidirectional.
164+Worktrees are **oblivious to Projects** - they don't track which Projects reference them.
89165
90166 ### 3. Shared Stores vs Per-Workspace Stores
91167
92−**Shared at MultiWorkspace level** (same file = same entity everywhere):
93−- `BufferStore` - Edits in Workspace A are immediately visible in Workspace B
168+**Eventually Shared** (same file = same entity everywhere):
169+- `BufferStore` - Edits in Workspace A visible in Workspace B
94170 - `GitStore` - Git state is per-repository
95−- `LspStore` - One language server instance per worktree, shared across workspaces
171+- `LspStore` - One language server instance per worktree
96172 - `ImageStore` - Same as BufferStore
97173 - `BreakpointStore` - Breakpoints are on files
98174
99175 **Per-Workspace** (workspace-specific context):
100176 - `DapStore` - Debugging sessions are focused activities
101177 - `TaskStore` - Tasks run in a specific workspace context
102−- `ContextServerStore` - Context servers may be workspace-scoped
103−
104−### 4. Collaboration Model (Option C: Share at Project Level)
105178
106−Collaboration happens at the **Project level**, not WorktreeStore level:
107−- A Project can be shared (gets a `remote_id` in `ProjectClientState::Shared`)
108−- Collaborators join a specific Project and see its worktrees
109−- The same worktree can be in multiple Projects, but...
179+### 4. Collaboration Constraint
110180
111−**Constraint**: A worktree can only be in ONE shared Project at a time.
112−
113−
114−When sharing, validate:
181+A worktree can only be in ONE shared Project at a time. Validate when sharing:
115182 ```rust
116183 impl Project {
117184 pub fn share(&mut self, cx: &mut Context<Self>) -> Task<Result<u64>> {
118185 // Check if any of our worktrees are already in a shared project
119− let multi_workspace = MultiWorkspace::global(cx);
120186 for worktree_id in &self.worktree_ids {
121187 if let Some(other_project) = multi_workspace.find_sharing_project_for_worktree(*worktree_id, cx) {
122188 if other_project != cx.entity() {
@@ -132,100 +198,19 @@ impl Project { · 9 unmodified lines
132198 }
133199 ```
134200
135−### 5. RemoteWorktree.project_id Clarification
136−
137−There are two different IDs to understand:
138−1. **`RemoteWorktree.project_id`** - The upstream source ID (e.g., SSH server's project ID). This is for RPC with the server hosting the files.
139−2. **`Project.client_state.remote_id`** - The collaboration ID when this Project is shared with others.
140−
141−These are orthogonal. A RemoteWorktree connected via SSH has a fixed `project_id` for that connection. That worktree can be part of multiple local Projects with different collaboration states.
142−
143−### 6. Worktree Lifecycle
144−
145−Worktrees are reference-counted in the shared WorktreeStore:
146−- When a Workspace closes, its Project removes its worktree references
147−- The worktree stays alive if another Project still references it
148−- When the last reference is removed, the worktree is dropped
149−
150−## Implementation Plan
151−
152−### Phase 1: Create MultiWorkspace Container
153−
154−1. Create new `MultiWorkspace` struct that holds:
155− - `Entity<WorktreeStore>`
156− - `Entity<BufferStore>`
157− - `Entity<GitStore>`
158− - `Entity<LspStore>`
159− - `Entity<ImageStore>`
160− - `Entity<BreakpointStore>`
161− - `Vec<Entity<Workspace>>` or similar
162−
163−2. Make MultiWorkspace global or per-window (TBD based on window model)
164−
165−3. Update Workspace creation to:
166− - Get/create MultiWorkspace
167− - Create Project with reference to shared WorktreeStore
168− - Pass shared stores to Project
169−
170−### Phase 2: Refactor WorktreeStore
171−
172−1. Remove `WorktreeStoreState` enum
173−2. Add explicit `add_local_worktree` and `add_remote_worktree` methods
174−3. Update `find_or_create_worktree` to take connection info parameter
175−
176−### Phase 3: Refactor Project
177−
178−1. Add `worktree_ids: HashSet<WorktreeId>` field
179−2. Change `worktree_store` from owned to shared reference
180−3. Update all methods that iterate worktrees to filter by `worktree_ids`
181−4. Move DapStore and TaskStore creation into Project (keep them per-workspace)
182−5. Add references to shared stores (BufferStore, GitStore, etc.)
183−
184−### Phase 4: Refactor Store Creation
185−
186−1. Move BufferStore, GitStore, LspStore, ImageStore, BreakpointStore creation to MultiWorkspace
187−2. Update these stores to work with the shared WorktreeStore
188−3. Ensure stores handle worktrees being added/removed dynamically
189−
190−### Phase 5: Update Collaboration
191−
192−1. Add sharing validation (prevent double-sharing of worktrees)
193−2. Update `share_project` to work with the new architecture
194−3. Test collaboration scenarios with multiple workspaces
195−
196−### Phase 6: Update Consumers
197−
198−Many places call `workspace.project()` and expect to access stores. Audit and update:
199−- Panels (AgentPanel, GitPanel, etc.)
200−- Activity indicator
201−- File opening code paths
202−- Search functionality
203−
204−## Key Files to Modify
205−
206−- `crates/project/src/project.rs` - Major refactor
207−- `crates/project/src/worktree_store.rs` - Remove state enum, add explicit methods
208−- `crates/project/src/buffer_store.rs` - May need state simplification
209−- `crates/workspace/src/workspace.rs` - Update to use MultiWorkspace
210−- `crates/call/src/call_impl/room.rs` - Update sharing logic
211−- New file: `crates/workspace/src/multi_workspace.rs` or similar
212−
213−## Testing Considerations
214−
215−1. **Multi-workspace scenarios**: Create tests where two workspaces share worktrees
216−2. **Edit propagation**: Verify edits in one workspace appear in another
217−3. **Collaboration with shared worktrees**: Test sharing when worktrees overlap
218−4. **Mixed local/remote**: Test WorktreeStore with both local and SSH worktrees
219−5. **Worktree lifecycle**: Test closing workspaces and worktree cleanup
220−
221−## Open Questions for Implementation
201+## Key Files
222202
223−1. Should MultiWorkspace be global (one per app) or per-window?
224−2. How should we handle the transition for existing serialized workspaces?
225−3. Should there be UI for managing worktrees across workspaces?
203+- `crates/workspace/src/workspace.rs` - Contains `MultiWorkspace`, will contain `Workspace`
204+- `crates/workspace/src/single_workspace.rs` - Stub module (created)
205+- `crates/project/src/project.rs` - Will add `worktree_ids` filter
206+- `crates/project/src/worktree_store.rs` - Will remove `WorktreeStoreState` enum
226207
227−## References
208+## Current Status
228209
229−- Original detailed plan: `threads-sidebar-plan.md` (some concepts superseded)
230−- Comment in `crates/project/src/project.rs` lines 162-172 showing target architecture
231−- Current Project::local() and Project::remote() constructors show store creation patterns
210+- [x] Rename `Workspace` → `MultiWorkspace` (done)
211+- [x] Create stub `single_workspace::Workspace` module (done)
212+- [ ] Extract UI/pane/panel ownership from `MultiWorkspace` into `Workspace`
213+- [ ] Pass `WorktreeStore` from `MultiWorkspace` down to `Project`
214+- [ ] **MERGE POINT**
215+- [ ] Add second `Workspace` support
216+- [ ] Refactor stores as needed for sharing
phase-2-multiworkspace-plan.md+0−1067
@@ -1,1067 +0,0 @@
1−# Phase 2: MultiWorkspace Implementation Plan
2−
3−This document details Phase 2 of the MultiWorkspace Agent V2 Refactor. Phase 1 (GlobalThreadStore Infrastructure) is a prerequisite.
4−
5−## Overview
6−
7−Enable the workspace to swap between different ThreadWorktrees contexts while keeping state in memory. **Key insight:** Project stays singular per window; we swap which ThreadWorktrees group is active.
8−
9−### Goals
10−
11−1. Create `ThreadWorktrees` as a scoped lens into `Project` for panels
12−2. Implement `WorkspaceState` to store per-thread UI state (panes, panels)
13−3. Enable switching between thread contexts without losing state
14−4. Migrate existing panels from `Entity<Project>` to `Entity<ThreadWorktrees>`
15−
16−### Dependencies
17−
18−- **Phase 1 Complete**: `GlobalThreadStore` with project paths stored per thread
19−- **Feature Flag**: All changes gated behind `agent-v2` feature flag
20−
21−---
22−
23−## 2.1 New Concept: ThreadWorktrees
24−
25−### ThreadWorktreesId
26−
27−The identity that ties a thread to workspace-level state:
28−
29−```rust
30−// New file: crates/project/src/thread_worktrees.rs
31−
32−use gpui::{App, Context, Entity, EventEmitter, Task, WeakEntity};
33−use collections::HashSet;
34−use std::path::PathBuf;
35−use anyhow::{Result, anyhow};
36−
37−/// Identifies a ThreadWorktrees group - glues thread identity to workspace identity
38−#[derive(Clone, Hash, Eq, PartialEq, Debug)]
39−pub struct ThreadWorktreesId {
40− /// None for "default" (no thread active)
41− pub session_id: Option<acp::SessionId>,
42− pub workspace_db_id: WorkspaceId,
43−}
44−
45−impl ThreadWorktreesId {
46− pub fn default_for_workspace(workspace_db_id: WorkspaceId) -> Self {
47− Self {
48− session_id: None,
49− workspace_db_id,
50− }
51− }
52−
53− pub fn for_thread(session_id: acp::SessionId, workspace_db_id: WorkspaceId) -> Self {
54− Self {
55− session_id: Some(session_id),
56− workspace_db_id,
57− }
58− }
59−}
60−```
61−
62−### ThreadWorktrees Struct
63−
64−```rust
65−/// Groups worktrees that belong to a specific thread context.
66−/// Acts as a filtered lens into Project for panels.
67−pub struct ThreadWorktrees {
68− id: ThreadWorktreesId,
69− project: Entity<Project>,
70− worktree_ids: HashSet<WorktreeId>,
71− folder_paths: Vec<PathBuf>, // Absolute paths for persistence
72− _subscriptions: Vec<gpui::Subscription>,
73−}
74−```
75−
76−### Project Additions
77−
78−Modify `crates/project/src/project.rs`:
79−
80−```rust
81−pub struct Project {
82− // EXISTING: all worktrees for all live threads in this window
83− worktree_store: Entity<WorktreeStore>,
84−
85− // NEW: Groups worktrees by thread
86− thread_worktrees: HashMap<ThreadWorktreesId, Entity<ThreadWorktrees>>,
87−
88− // NEW: Currently active thread context
89− active_thread_worktrees_id: Option<ThreadWorktreesId>,
90−
91− // ... existing fields ...
92−}
93−
94−impl Project {
95− /// Get or create ThreadWorktrees for a given ID
96− pub fn thread_worktrees(
97− &mut self,
98− id: &ThreadWorktreesId,
99− cx: &mut Context<Self>,
100− ) -> Entity<ThreadWorktrees> {
101− if let Some(tw) = self.thread_worktrees.get(id) {
102− return tw.clone();
103− }
104−
105− // Create new ThreadWorktrees with worktrees based on thread's folder_paths
106− let worktree_ids = self.worktree_ids_for_thread(id, cx);
107− let thread_worktrees = cx.new(|cx| {
108− ThreadWorktrees::new(id.clone(), cx.entity().clone(), worktree_ids, cx)
109− });
110− self.thread_worktrees.insert(id.clone(), thread_worktrees.clone());
111− thread_worktrees
112− }
113−
114− /// Set the active thread context
115− pub fn set_active_thread_worktrees(
116− &mut self,
117− id: Option<ThreadWorktreesId>,
118− cx: &mut Context<Self>,
119− ) {
120− self.active_thread_worktrees_id = id;
121− cx.notify();
122− }
123−
124− /// Get the currently active ThreadWorktrees
125− pub fn active_thread_worktrees(&self, cx: &App) -> Option<Entity<ThreadWorktrees>> {
126− self.active_thread_worktrees_id
127− .as_ref()
128− .and_then(|id| self.thread_worktrees.get(id).cloned())
129− }
130−}
131−```
132−
133−### Worktree Lifecycle
134−
135−- Worktrees are automatically created/loaded when opening a thread
136−- Live threads (running in background) keep their worktrees loaded
137−- Worktrees shared between threads (e.g., /cloud in threads A and B) exist once in Project but are referenced by multiple ThreadWorktrees
138−
139−---
140−
141−## 2.2 ThreadWorktrees as Panel API Surface
142−
143−Panels currently hold `Entity<Project>` and call methods like `project.read(cx).git_store()`. In multi-workspace, panels need **scoped access** - GitPanel for ThreadWorktrees A shouldn't see repos from ThreadWorktrees B.
144−
145−### Current Panel Pattern (to migrate from)
146−
147−Based on existing code analysis:
148−
149−```rust
150−// crates/git_ui/src/git_panel.rs - Current
151−pub struct GitPanel {
152− pub(crate) project: Entity<Project>,
153− pub(crate) active_repository: Option<Entity<Repository>>,
154− // ...
155−}
156−
157−// crates/project_panel/src/project_panel.rs - Current
158−pub struct ProjectPanel {
159− project: Entity<Project>,
160− fs: Arc<dyn Fs>,
161− // ...
162−}
163−```
164−
165−### ThreadWorktrees API Implementation
166−
167−```rust
168−// crates/project/src/thread_worktrees.rs
169−
170−impl ThreadWorktrees {
171− pub fn new(
172− id: ThreadWorktreesId,
173− project: Entity<Project>,
174− worktree_ids: HashSet<WorktreeId>,
175− cx: &mut Context<Self>,
176− ) -> Self {
177− let git_store = project.read(cx).git_store().clone();
178− let worktree_store = project.read(cx).worktree_store.clone();
179− let mut subscriptions = Vec::new();
180−
181− // Subscribe to git_store and filter events
182− subscriptions.push(cx.subscribe(&git_store, {
183− let worktree_ids = worktree_ids.clone();
184− move |this, _store, event, cx| {
185− this.handle_git_store_event(event, &worktree_ids, cx);
186− }
187− }));
188−
189− // Subscribe to worktree_store for worktree changes
190− subscriptions.push(cx.subscribe(&worktree_store, {
191− let worktree_ids = worktree_ids.clone();
192− move |this, _store, event, cx| {
193− this.handle_worktree_store_event(event, &worktree_ids, cx);
194− }
195− }));
196−
197− Self {
198− id,
199− project,
200− worktree_ids,
201− folder_paths: Vec::new(),
202− _subscriptions: subscriptions,
203− }
204− }
205−
206− // === Identity ===
207−
208− pub fn id(&self) -> &ThreadWorktreesId {
209− &self.id
210− }
211−
212− pub fn project(&self) -> &Entity<Project> {
213− &self.project
214− }
215−
216− // === Scoped Worktree Access ===
217−
218− pub fn visible_worktrees<'a>(&self, cx: &'a App) -> impl Iterator<Item = Entity<Worktree>> + 'a {
219− let owned_ids = self.worktree_ids.clone();
220− self.project
221− .read(cx)
222− .visible_worktrees(cx)
223− .filter(move |wt| owned_ids.contains(&wt.read(cx).id()))
224− }
225−
226− pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option<Entity<Worktree>> {
227− if self.worktree_ids.contains(&id) {
228− self.project.read(cx).worktree_for_id(id, cx)
229− } else {
230− None
231− }
232− }
233−
234− pub fn worktree_count(&self) -> usize {
235− self.worktree_ids.len()
236− }
237−
238− // === Scoped Git Access ===
239−
240− pub fn active_repository(&self, cx: &App) -> Option<Entity<Repository>> {
241− let repo = self.project.read(cx).active_repository(cx)?;
242− let worktree_id = repo.read(cx).worktree_id();
243− self.worktree_ids.contains(&worktree_id).then_some(repo)
244− }
245−
246− /// Returns the shared git_store - events are filtered via subscriptions
247− pub fn git_store(&self, cx: &App) -> Entity<GitStore> {
248− self.project.read(cx).git_store().clone()
249− }
250−
251− fn repo_in_scope(&self, repo_id: RepositoryId, cx: &App) -> bool {
252− let git_store = self.project.read(cx).git_store();
253− git_store
254− .read(cx)
255− .repository(repo_id)
256− .map(|repo| self.worktree_ids.contains(&repo.read(cx).worktree_id()))
257− .unwrap_or(false)
258− }
259−
260− // === Pass-through for Global State ===
261−
262− pub fn is_via_collab(&self, cx: &App) -> bool {
263− self.project.read(cx).is_via_collab()
264− }
265−
266− pub fn is_read_only(&self, cx: &App) -> bool {
267− self.project.read(cx).is_read_only(cx)
268− }
269−
270− pub fn languages(&self, cx: &App) -> Arc<LanguageRegistry> {
271− self.project.read(cx).languages().clone()
272− }
273−
274− pub fn path_style(&self, cx: &App) -> PathStyle {
275− self.project.read(cx).path_style(cx)
276− }
277−
278− pub fn fs(&self, cx: &App) -> Arc<dyn Fs> {
279− self.project.read(cx).fs.clone()
280− }
281−
282− // === Scoped Buffer Operations ===
283−
284− /// Shared buffer_store - buffers are deduplicated across threads
285− pub fn buffer_store(&self, cx: &App) -> Entity<BufferStore> {
286− self.project.read(cx).buffer_store().clone()
287− }
288−
289− /// Open buffer with scope validation (workflow guard, not isolation)
290− pub fn open_buffer(
291− &self,
292− path: ProjectPath,
293− cx: &mut Context<Self>,
294− ) -> Task<Result<Entity<Buffer>>> {
295− if !self.worktree_ids.contains(&path.worktree_id) {
296− return Task::ready(Err(anyhow!("Path not in this ThreadWorktrees scope")));
297− }
298− self.project
299− .update(cx, |project, cx| project.open_buffer(path, cx))
300− }
301−
302− // === Scoped File Operations ===
303−
304− pub fn delete_file(
305− &self,
306− path: ProjectPath,
307− trash: bool,
308− cx: &mut Context<Self>,
309− ) -> Option<Task<Result<()>>> {
310− if !self.worktree_ids.contains(&path.worktree_id) {
311− return None;
312− }
313− self.project
314− .update(cx, |project, cx| project.delete_file(path, trash, cx))
315− }
316−
317− pub fn create_file(
318− &self,
319− path: ProjectPath,
320− is_dir: bool,
321− cx: &mut Context<Self>,
322− ) -> Option<Task<Result<CreatedEntry>>> {
323− if !self.worktree_ids.contains(&path.worktree_id) {
324− return None;
325− }
326− self.project
327− .update(cx, |project, cx| project.create_file(path, is_dir, cx))
328− }
329−
330− pub fn rename_entry(
331− &self,
332− entry_id: ProjectEntryId,
333− new_path: impl Into<Arc<Path>>,
334− cx: &mut Context<Self>,
335− ) -> Option<Task<Result<CreatedEntry>>> {
336− // Verify entry belongs to our worktrees
337− let worktree_id = self.project.read(cx).entry_worktree_id(entry_id)?;
338− if !self.worktree_ids.contains(&worktree_id) {
339− return None;
340− }
341− self.project.update(cx, |project, cx| {
342− project.rename_entry(entry_id, new_path, cx)
343− })
344− }
345−}
346−```
347−
348−### ThreadWorktrees Events
349−
350−```rust
351−#[derive(Clone, Debug)]
352−pub enum ThreadWorktreesEvent {
353− /// Active repository changed (filtered to this scope)
354− ActiveRepositoryChanged(Option<Entity<Repository>>),
355− /// A repository in this scope was updated
356− RepositoryUpdated(RepositoryId, RepositoryEvent),
357− /// A worktree in this scope was added
358− WorktreeAdded(WorktreeId),
359− /// A worktree in this scope was removed
360− WorktreeRemoved(WorktreeId),
361− /// Entries updated in a worktree in this scope
362− WorktreeEntriesUpdated(WorktreeId, UpdatedEntriesSet),
363−}
364−
365−impl EventEmitter<ThreadWorktreesEvent> for ThreadWorktrees {}
366−
367−impl ThreadWorktrees {
368− fn handle_git_store_event(
369− &mut self,
370− event: &GitStoreEvent,
371− worktree_ids: &HashSet<WorktreeId>,
372− cx: &mut Context<Self>,
373− ) {
374− match event {
375− GitStoreEvent::ActiveRepositoryChanged(repo_id) => {
376− // Only emit if repo is in our worktrees
377− let filtered_repo = repo_id.as_ref().and_then(|id| {
378− let git_store = self.project.read(cx).git_store();
379− let repo = git_store.read(cx).repository(*id)?;
380− worktree_ids
381− .contains(&repo.read(cx).worktree_id())
382− .then_some(repo)
383− });
384− cx.emit(ThreadWorktreesEvent::ActiveRepositoryChanged(filtered_repo));
385− }
386− GitStoreEvent::RepositoryUpdated(repo_id, event, _) => {
387− if self.repo_in_scope(*repo_id, cx) {
388− cx.emit(ThreadWorktreesEvent::RepositoryUpdated(*repo_id, event.clone()));
389− }
390− }
391− _ => {}
392− }
393− }
394−
395− fn handle_worktree_store_event(
396− &mut self,
397− event: &WorktreeStoreEvent,
398− worktree_ids: &HashSet<WorktreeId>,
399− cx: &mut Context<Self>,
400− ) {
401− match event {
402− WorktreeStoreEvent::WorktreeAdded(wt) => {
403− let id = wt.read(cx).id();
404− if worktree_ids.contains(&id) {
405− cx.emit(ThreadWorktreesEvent::WorktreeAdded(id));
406− }
407− }
408− WorktreeStoreEvent::WorktreeRemoved(_, id) => {
409− if worktree_ids.contains(id) {
410− cx.emit(ThreadWorktreesEvent::WorktreeRemoved(*id));
411− }
412− }
413− WorktreeStoreEvent::WorktreeEntriesUpdated { worktree_id, entries } => {
414− if worktree_ids.contains(worktree_id) {
415− cx.emit(ThreadWorktreesEvent::WorktreeEntriesUpdated(
416− *worktree_id,
417− entries.clone(),
418− ));
419− }
420− }
421− _ => {}
422− }
423− }
424−}
425−```
426−
427−### API Categories Reference
428−
429−| Category | Examples | ThreadWorktrees Behavior |
430−|----------|----------|-------------------------|
431−| Worktree-scoped | `visible_worktrees()`, `worktree_for_id()` | Filtered to owned worktrees |
432−| Git-scoped | `active_repository()`, `git_init()` | Filtered by worktree ownership |
433−| File operations | `open_buffer()`, `delete_file()` | Validated against scope |
434−| Global/shared | `is_via_collab()`, `languages()`, `path_style()`, `fs()` | Pass-through to Project |
435−| Buffer store | `buffer_store()`, `open_buffer()` | Shared (deduped by ProjectPath) |
436−
437−---
438−
439−## 2.3 WorkspaceState per ThreadWorktrees
440−
441−Modify `crates/workspace/src/workspace.rs`:
442−
443−### WorkspaceState Struct
444−
445−```rust
446−/// State for a specific ThreadWorktrees context.
447−/// Each thread gets its own set of panes and panel instances.
448−pub struct WorkspaceState {
449− thread_worktrees_id: ThreadWorktreesId,
450− thread_worktrees: Entity<ThreadWorktrees>,
451−
452− // Pane state (swapped when switching threads)
453− center: PaneGroup,
454− panes: Vec<Entity<Pane>>,
455− panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
456− active_pane: Entity<Pane>,
457− last_active_center_pane: Option<WeakEntity<Pane>>,
458−
459− // Panel instances - each ThreadWorktrees gets its own
460− panel_instances: HashMap<TypeId, Box<dyn PanelHandle>>,
461−
462− // Serialization
463− last_serialized: Option<Instant>,
464−}
465−
466−impl WorkspaceState {
467− pub fn new(
468− thread_worktrees_id: ThreadWorktreesId,
469− thread_worktrees: Entity<ThreadWorktrees>,
470− window: &mut Window,
471− cx: &mut Context<Workspace>,
472− ) -> Self {
473− let pane = cx.new(|cx| {
474− Pane::new(
475− cx.entity().downgrade(),
476− /* project */ thread_worktrees.read(cx).project().clone(),
477− Default::default(),
478− None,
479− NewFile.boxed_clone(),
480− window,
481− cx,
482− )
483− });
484−
485− Self {
486− thread_worktrees_id,
487− thread_worktrees,
488− center: PaneGroup::new(pane.clone()),
489− panes: vec![pane.clone()],
490− panes_by_item: HashMap::default(),
491− active_pane: pane,
492− last_active_center_pane: None,
493− panel_instances: HashMap::default(),
494− last_serialized: None,
495− }
496− }
497−
498− pub fn thread_worktrees(&self) -> &Entity<ThreadWorktrees> {
499− &self.thread_worktrees
500− }
501−}
502−```
503−
504−### Modified Workspace Struct
505−
506−```rust
507−pub struct Workspace {
508− project: Entity<Project>, // Still singular per window
509−
510− // NEW: State per ThreadWorktrees
511− workspace_states: HashMap<ThreadWorktreesId, WorkspaceState>,
512− active_workspace_state_id: Option<ThreadWorktreesId>,
513−
514− // Docks stay constant - panel contents come from active WorkspaceState
515− left_dock: Entity<Dock>,
516− bottom_dock: Entity<Dock>,
517− right_dock: Entity<Dock>,
518−
519− // These stay constant across thread switches
520− status_bar: Entity<StatusBar>,
521− modal_layer: Entity<ModalLayer>,
522− toast_layer: Entity<ToastLayer>,
523− titlebar_item: Option<AnyView>,
524− notifications: Notifications,
525−
526− // Global panels (not per-thread)
527− global_panel_instances: HashMap<TypeId, Box<dyn PanelHandle>>,
528−
529− // ... existing fields (weak_self, app_state, etc.) ...
530−}
531−```
532−
533−---
534−
535−## 2.4 Switching Active ThreadWorktrees
536−
537−### Implementation
538−
539−```rust
540−impl Workspace {
541− /// Switch to a different thread context
542− pub fn switch_to_thread(
543− &mut self,
544− thread_worktrees_id: ThreadWorktreesId,
545− window: &mut Window,
546− cx: &mut Context<Self>,
547− ) {
548− // 1. Save current state (implicit - already in workspace_states)
549− if let Some(current_id) = &self.active_workspace_state_id {
550− if *current_id == thread_worktrees_id {
551− return; // Already active
552− }
553− }
554−
555− // 2. Ensure ThreadWorktrees exists in Project
556− let thread_worktrees = self.project.update(cx, |project, cx| {
557− project.thread_worktrees(&thread_worktrees_id, cx)
558− });
559−
560− // 3. Update Project's active context
561− self.project.update(cx, |project, cx| {
562− project.set_active_thread_worktrees(Some(thread_worktrees_id.clone()), cx);
563− });
564−
565− // 4. Load or create WorkspaceState
566− if !self.workspace_states.contains_key(&thread_worktrees_id) {
567− let state = WorkspaceState::new(
568− thread_worktrees_id.clone(),
569− thread_worktrees.clone(),
570− window,
571− cx,
572− );
573− self.workspace_states.insert(thread_worktrees_id.clone(), state);
574−
575− // Initialize scoped panels for new state
576− self.initialize_scoped_panels_for_state(&thread_worktrees_id, window, cx);
577− }
578−
579− // 5. Update active state
580− self.active_workspace_state_id = Some(thread_worktrees_id);
581−
582− // 6. Update UI
583− self.update_docks_for_active_state(window, cx);
584− self.update_window_title(window, cx);
585− cx.notify();
586− }
587−
588− /// Get the active WorkspaceState
589− pub fn active_workspace_state(&self) -> Option<&WorkspaceState> {
590− self.active_workspace_state_id
591− .as_ref()
592− .and_then(|id| self.workspace_states.get(id))
593− }
594−
595− pub fn active_workspace_state_mut(&mut self) -> Option<&mut WorkspaceState> {
596− self.active_workspace_state_id
597− .as_ref()
598− .cloned()
599− .and_then(move |id| self.workspace_states.get_mut(&id))
600− }
601−
602− /// Close a thread context and clean up its state
603− pub fn close_thread(
604− &mut self,
605− thread_worktrees_id: &ThreadWorktreesId,
606− window: &mut Window,
607− cx: &mut Context<Self>,
608− ) {
609− // Remove from workspace_states - GPUI reference counting handles cleanup
610− self.workspace_states.remove(thread_worktrees_id);
611−
612− // If this was the active state, switch to default or another thread
613− if self.active_workspace_state_id.as_ref() == Some(thread_worktrees_id) {
614− let fallback = self.workspace_states.keys().next().cloned();
615− if let Some(fallback_id) = fallback {
616− self.switch_to_thread(fallback_id, window, cx);
617− } else {
618− self.active_workspace_state_id = None;
619− cx.notify();
620− }
621− }
622− }
623−
624− fn update_docks_for_active_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
625− let Some(state) = self.active_workspace_state() else {
626− return;
627− };
628−
629− // Update each dock to show panels from the active WorkspaceState
630− for (type_id, panel) in &state.panel_instances {
631− // Find which dock this panel belongs to and update it
632− let position = panel.position(window, cx);
633− let dock = match position {
634− DockPosition::Left => &self.left_dock,
635− DockPosition::Bottom => &self.bottom_dock,
636− DockPosition::Right => &self.right_dock,
637− };
638− // Dock will need API to swap panel instance
639− // dock.update(cx, |dock, cx| dock.set_panel_instance(*type_id, panel.clone(), cx));
640− }
641− }
642−}
643−```
644−
645−---
646−
647−## 2.5 Panel Instance Management
648−
649−### Scoped vs Global Panels
650−
651−**SCOPED (per-ThreadWorktrees)** - stored in `WorkspaceState.panel_instances`:
652−
653−| Panel | Reason |
654−|-------|--------|
655−| `ProjectPanel` | Shows worktree file trees |
656−| `GitPanel` | Shows repos tied to worktrees |
657−| `OutlinePanel` | Shows context for files in worktrees |
658−| `DebugPanel` | Debug sessions are project-specific |
659−| `TerminalPanel` | Terminals have working directories in worktrees |
660−| `AgentPanel` | Thread-specific conversation |
661−
662−**GLOBAL (shared)** - stored on `Workspace.global_panel_instances`:
663−
664−| Panel | Reason |
665−|-------|--------|
666−| `CollabPanel` | Collaborators, channels, contacts - user's social state |
667−| `NotificationPanel` | User's notifications across all projects |
668−
669−### Panel Creation
670−
671−```rust
672−impl WorkspaceState {
673− /// Get or create a scoped panel for this WorkspaceState
674− pub fn get_or_create_panel<P: Panel>(
675− &mut self,
676− thread_worktrees: Entity<ThreadWorktrees>,
677− window: &mut Window,
678− cx: &mut App,
679− ) -> Entity<P>
680− where
681− P: Panel + PanelFromThreadWorktrees,
682− {
683− let type_id = TypeId::of::<P>();
684− if let Some(panel) = self.panel_instances.get(&type_id) {
685− return panel
686− .to_any()
687− .downcast::<P>()
688− .expect("Panel type mismatch");
689− }
690−
691− let panel = cx.new(|cx| P::new(thread_worktrees, window, cx));
692− self.panel_instances.insert(type_id, Box::new(panel.clone()));
693− panel
694− }
695−}
696−
697−/// Trait for panels that can be constructed from ThreadWorktrees
698−pub trait PanelFromThreadWorktrees: Panel {
699− fn new(
700− thread_worktrees: Entity<ThreadWorktrees>,
701− window: &mut Window,
702− cx: &mut Context<Self>,
703− ) -> Self;
704−}
705−```
706−
707−### Initialize Scoped Panels
708−
709−```rust
710−impl Workspace {
711− fn initialize_scoped_panels_for_state(
712− &mut self,
713− thread_worktrees_id: &ThreadWorktreesId,
714− window: &mut Window,
715− cx: &mut Context<Self>,
716− ) {
717− let Some(state) = self.workspace_states.get_mut(thread_worktrees_id) else {
718− return;
719− };
720− let thread_worktrees = state.thread_worktrees.clone();
721−
722− // Initialize all scoped panels
723− // Note: This follows the pattern in zed.rs initialize_panels but per-state
724−
725− // ProjectPanel
726− let project_panel = cx.new(|cx| {
727− ProjectPanel::new_from_thread_worktrees(thread_worktrees.clone(), window, cx)
728− });
729− state.panel_instances.insert(
730− TypeId::of::<ProjectPanel>(),
731− Box::new(project_panel),
732− );
733−
734− // GitPanel
735− let git_panel = cx.new(|cx| {
736− GitPanel::new_from_thread_worktrees(thread_worktrees.clone(), window, cx)
737− });
738− state.panel_instances.insert(
739− TypeId::of::<GitPanel>(),
740− Box::new(git_panel),
741− );
742−
743− // OutlinePanel, DebugPanel, TerminalPanel, AgentPanel...
744− }
745−}
746−```
747−
748−---
749−
750−## 2.6 Panel Migration Pattern
751−
752−### Before/After Comparison
753−
754−```rust
755−// BEFORE: Panel holds Project directly
756−pub struct GitPanel {
757− project: Entity<Project>,
758− active_repository: Option<Entity<Repository>>,
759− // ...
760−}
761−
762−impl GitPanel {
763− fn new(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
764− let project = workspace.project().clone();
765− let git_store = project.read(cx).git_store().clone();
766− let active_repository = project.read(cx).active_repository(cx);
767−
768− cx.new(|cx| {
769− // Subscribe to git_store directly
770− cx.subscribe(&git_store, |this, _, event, cx| {
771− // Handle ALL events, no filtering
772− match event {
773− GitStoreEvent::ActiveRepositoryChanged(repo) => {
774− this.active_repository = repo.clone();
775− cx.notify();
776− }
777− // ...
778− }
779− }).detach();
780−
781− Self { project, active_repository, /* ... */ }
782− })
783− }
784−}
785−```
786−
787−```rust
788−// AFTER: Panel holds ThreadWorktrees
789−pub struct GitPanel {
790− thread_worktrees: Entity<ThreadWorktrees>,
791− active_repository: Option<Entity<Repository>>,
792− // ...
793−}
794−
795−impl GitPanel {
796− fn new_from_thread_worktrees(
797− thread_worktrees: Entity<ThreadWorktrees>,
798− window: &mut Window,
799− cx: &mut Context<Self>,
800− ) -> Self {
801− let active_repository = thread_worktrees.read(cx).active_repository(cx);
802−
803− // Subscribe to ThreadWorktrees events (pre-filtered)
804− cx.subscribe(&thread_worktrees, |this, _, event, cx| {
805− match event {
806− ThreadWorktreesEvent::ActiveRepositoryChanged(repo) => {
807− this.active_repository = repo.clone();
808− cx.notify();
809− }
810− ThreadWorktreesEvent::RepositoryUpdated(repo_id, event) => {
811− // Already filtered to our scope
812− this.handle_repo_update(*repo_id, event, cx);
813− }
814− // ...
815− }
816− }).detach();
817−
818− Self { thread_worktrees, active_repository, /* ... */ }
819− }
820−}
821−
822−// Method migration example
823−impl GitPanel {
824− // BEFORE
825− fn old_get_repos(&self, cx: &App) -> Vec<Entity<Repository>> {
826− self.project.read(cx).visible_worktrees(cx)
827− .filter_map(|wt| /* get repo */)
828− .collect()
829− }
830−
831− // AFTER - same API, scoped results
832− fn get_repos(&self, cx: &App) -> Vec<Entity<Repository>> {
833− self.thread_worktrees.read(cx).visible_worktrees(cx)
834− .filter_map(|wt| /* get repo */)
835− .collect()
836− }
837−}
838−```
839−
840−### Migration Checklist Per Panel
841−
842−For each panel migrating from `Entity<Project>` to `Entity<ThreadWorktrees>`:
843−
844−1. [ ] Replace `project: Entity<Project>` field with `thread_worktrees: Entity<ThreadWorktrees>`
845−2. [ ] Update constructor to take `Entity<ThreadWorktrees>` parameter
846−3. [ ] Replace `cx.subscribe(&git_store, ...)` with `cx.subscribe(&thread_worktrees, ...)`
847−4. [ ] Update event handlers to use `ThreadWorktreesEvent` variants
848−5. [ ] Replace `self.project.read(cx).visible_worktrees(cx)` with `self.thread_worktrees.read(cx).visible_worktrees(cx)`
849−6. [ ] Replace `self.project.read(cx).active_repository(cx)` with `self.thread_worktrees.read(cx).active_repository(cx)`
850−7. [ ] Keep pass-through methods: `is_via_collab()`, `languages()`, `path_style()`, `fs()`
851−8. [ ] Add `impl PanelFromThreadWorktrees for Panel` if needed
852−
853−---
854−
855−## 2.7 Feature-Flagged Actions
856−
857−All Phase 2 functionality is gated behind the `agent-v2` feature flag.
858−
859−### New Actions
860−
861−```rust
862−// crates/workspace/src/workspace.rs
863−
864−actions!(
865− workspace,
866− [
867− /// Switch to a specific thread's workspace state
868− SwitchToThread,
869− /// Open a thread (loads worktrees, creates WorkspaceState if needed)
870− OpenThread,
871− /// Close a thread's workspace state
872− CloseThread,
873− ]
874−);
875−
876−#[derive(Clone, Debug, Deserialize, PartialEq)]
877−pub struct SwitchToThread {
878− pub session_id: String,
879−}
880−
881−impl_actions!(workspace, [SwitchToThread, OpenThread, CloseThread]);
882−```
883−
884−### Action Handlers
885−
886−```rust
887−impl Workspace {
888− pub fn register_thread_actions(cx: &mut App) {
889− if !cx.has_flag::<AgentV2FeatureFlag>() {
890− return;
891− }
892−
893− cx.on_action(|workspace: &mut Workspace, action: &SwitchToThread, window, cx| {
894− let session_id = acp::SessionId::from(action.session_id.clone());
895− let workspace_db_id = workspace.database_id().unwrap_or_default();
896− let thread_worktrees_id = ThreadWorktreesId::for_thread(session_id, workspace_db_id);
897− workspace.switch_to_thread(thread_worktrees_id, window, cx);
898− });
899− }
900−}
901−```
902−
903−---
904−
905−## Cross-Thread File References
906−
907−**Resolution: Center panes are NOT scoped, only panels are**
908−
909−When "Go to Definition" (or similar navigation) targets a file outside the current ThreadWorktrees:
910−
911−- **If the worktree is loaded** (another thread is live) → **Just works**. The file opens in a center tab. ProjectPanel won't show it in its tree (filtered by ThreadWorktrees), but the editor works normally.
912−
913−- **If the worktree is NOT loaded** → **File not available**. Show message: "Definition is in `/path` which is not currently loaded."
914−
915−### Architecture
916−
917−```
918−┌─────────────────────────────────────────────────────────┐
919−│ Window │
920−├──────────┬──────────────────────────────────────────────┤
921−│ Agents │ ┌─────────┬──────────────────┬────────┐ │
922−│ Sidebar │ │ Left │ │ Right │ │
923−│ │ │ Dock │ Center Panes │ Dock │ │
924−│ │ │ │ │ │ │
925−│ │ │ SCOPED │ NOT SCOPED │ SCOPED │ │
926−│ │ │ (shows │ (can open any │ │ │
927−│ │ │ only │ buffer from │ │ │
928−│ │ │ Thread │ any loaded │ │ │
929−│ │ │ A's │ worktree) │ │ │
930−│ │ │ trees) │ │ │ │
931−│ │ └─────────┴──────────────────┴────────┘ │
932−└──────────┴──────────────────────────────────────────────┘
933−```
934−
935−This matches current Zed behavior - you can have open files that aren't in the project panel (via drag-drop, external open, etc.).
936−
937−---
938−
939−## Key Files to Modify
940−
941−| File | Changes |
942−|------|---------|
943−| `crates/project/src/thread_worktrees.rs` | **NEW**: ThreadWorktrees struct, ThreadWorktreesId, event filtering |
944−| `crates/project/src/project.rs` | Add `thread_worktrees` HashMap, `active_thread_worktrees_id`, helper methods |
945−| `crates/workspace/src/workspace.rs` | Add `WorkspaceState`, `workspace_states` HashMap, switch_to_thread logic |
946−| `crates/git_ui/src/git_panel.rs` | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
947−| `crates/project_panel/src/project_panel.rs` | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
948−| `crates/outline_panel/src/outline_panel.rs` | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
949−| `crates/debugger_ui/src/debugger_panel.rs` | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
950−| `crates/terminal_view/src/terminal_panel.rs` | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
951−| `crates/agent_ui/src/agent_panel.rs` | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
952−
953−---
954−
955−## Implementation Order
956−
957−### Step 1: Core Infrastructure
958−1. Create `crates/project/src/thread_worktrees.rs` with `ThreadWorktreesId` and `ThreadWorktrees` struct
959−2. Add basic API methods (scoped worktree access, pass-throughs)
960−3. Add `thread_worktrees` HashMap to `Project`
961−4. Wire up module in `crates/project/src/project.rs`
962−
963−### Step 2: Event Filtering
964−1. Implement `ThreadWorktreesEvent` enum
965−2. Add subscriptions to git_store and worktree_store in `ThreadWorktrees::new`
966−3. Implement `handle_git_store_event` and `handle_worktree_store_event`
967−
968−### Step 3: WorkspaceState
969−1. Create `WorkspaceState` struct in workspace.rs
970−2. Add `workspace_states` HashMap to `Workspace`
971−3. Implement `switch_to_thread` method
972−4. Add feature-flagged actions
973−
974−### Step 4: Panel Migration (one at a time)
975−1. Start with `ProjectPanel` (simplest, most isolated)
976−2. Then `GitPanel` (tests event filtering)
977−3. Then remaining panels
978−
979−### Step 5: Integration Testing
980−1. Test switching between threads
981−2. Test panel state preservation
982−3. Test cross-thread file navigation
983−
984−---
985−
986−## Testing Strategy
987−
988−### Unit Tests
989−
990−```rust
991−#[gpui::test]
992−async fn test_thread_worktrees_scopes_worktrees(cx: &mut TestAppContext) {
993− let fs = FakeFs::new(cx.background_executor.clone());
994− fs.insert_tree("/project_a", json!({"src": {"main.rs": ""}})).await;
995− fs.insert_tree("/project_b", json!({"lib": {"mod.rs": ""}})).await;
996−
997− let project = Project::test(fs.clone(), ["/project_a", "/project_b"], cx).await;
998−
999− // Create ThreadWorktrees with only project_a
1000− let worktree_a_id = project.read_with(cx, |p, cx| {
1001− p.worktrees(cx).next().unwrap().read(cx).id()
1002− });
1003−
1004− let thread_worktrees = cx.new(|cx| {
1005− ThreadWorktrees::new(
1006− ThreadWorktreesId::default_for_workspace(WorkspaceId(1)),
1007− project.clone(),
1008− HashSet::from([worktree_a_id]),
1009− cx,
1010− )
1011− });
1012−
1013− // Should only see project_a worktree
1014− let visible: Vec<_> = thread_worktrees.read_with(cx, |tw, cx| {
1015− tw.visible_worktrees(cx).collect()
1016− });
1017− assert_eq!(visible.len(), 1);
1018−}
1019−```
1020−
1021−### Integration Tests
1022−
1023−```rust
1024−#[gpui::test]
1025−async fn test_switch_between_threads_preserves_state(cx: &mut TestAppContext) {
1026− // Setup workspace with two threads
1027− // Switch to thread A, make changes
1028− // Switch to thread B
1029− // Switch back to thread A
1030− // Verify state is preserved
1031−}
1032−
1033−#[gpui::test]
1034−async fn test_panel_receives_filtered_events(cx: &mut TestAppContext) {
1035− // Setup ThreadWorktrees with subset of worktrees
1036− // Trigger git event on worktree outside scope
1037− // Verify panel doesn't receive event
1038− // Trigger git event on worktree inside scope
1039− // Verify panel receives event
1040−}
1041−```
1042−
1043−---
1044−
1045−## Design Decisions Summary
1046−
1047−1. **Project remains singular** - One `Entity<Project>` per window holds all worktrees. ThreadWorktrees is a filtered lens, not a separate data store.
1048−
1049−2. **ThreadWorktrees as panel API surface** - Provides type safety, centralized event filtering, and clear API boundary.
1050−
1051−3. **Buffer store is shared** - Buffers represent files which are singular. Edits in shared worktrees are visible across all threads immediately.
1052−
1053−4. **Center panes are not scoped** - Only panels are filtered. Users can open any file from any loaded worktree in editor tabs.
1054−
1055−5. **Panel state preserved by design** - Each ThreadWorktrees has its own panel instances stored in WorkspaceState. Switching threads swaps which instances are displayed.
1056−
1057−6. **Reference counting handles cleanup** - No explicit cleanup logic needed. When WorkspaceState is removed, GPUI drops panel instances automatically.
1058−
1059−---
1060−
1061−## Open Questions for Phase 2
1062−
1063−1. **Dock panel swapping mechanism** - How exactly do docks swap panel instances? May need new Dock API.
1064−
1065−2. **Panel initialization timing** - Should panels be lazily initialized on first access or eagerly on thread switch?
1066−
1067−3. **Serialization** - Should WorkspaceState be serialized per-thread for restore on restart? (Likely Phase 3)
pluggable-shaders-gpui-plan.md+0−408
@@ -1,408 +0,0 @@
1−# Pluggable Shaders for GPUI
2−
3−## Overview
4−
5−This document outlines a plan for adding pluggable shader support to GPUI, allowing users to define custom shaders, pipelines, and primitives that integrate with GPUI's rendering system.
6−
7−## Goals
8−
9−- Allow users to define custom WGSL shaders for the Blade backend
10−- Allow users to register custom render pipelines with GPUI
11−- Allow users to define custom primitive types with `#[repr(C)]` structs for GPU data
12−- Integrate custom primitives into GPUI's batching and draw-order system
13−- Provide a clean, type-safe API for painting custom primitives during element rendering
14−
15−## Non-Goals
16−
17−- Supporting Metal or DirectX shader backends initially (focus on Blade/WGSL)
18−- Automatic shader hot-reloading
19−- Providing a shader authoring GUI
20−- Multi-pass rendering for custom primitives (can be added later)
21−
22−## Current State
23−
24−### Rendering Architecture
25−
26−GPUI uses a multi-backend rendering system:
27−- **Blade** (Linux, FreeBSD, macOS with feature flag) - uses WGSL shaders
28−- **Metal** (macOS default) - uses Metal shaders
29−- **DirectX** (Windows) - uses HLSL shaders
30−
31−### Key Components
32−
33−1. **Scene** (`crates/gpui/src/scene.rs:24-36`): Holds all primitives to render
34− - Contains typed vectors for each primitive type (quads, shadows, paths, etc.)
35− - Manages draw ordering via `DrawOrder` (u32)
36− - Provides `batches()` iterator for efficient GPU rendering
37−
38−2. **Primitive Types** (`scene.rs:211-220`): 8 built-in types
39− - Shadow, Quad, Path, Underline
40− - MonochromeSprite, SubpixelSprite, PolychromeSprite
41− - Surface (video surfaces, macOS only)
42−
43−3. **BladeRenderer** (`blade_renderer.rs:369-386`):
44− - Owns pipelines, instance buffers, atlas, textures
45− - `draw()` method iterates batches and dispatches to appropriate pipelines
46−
47−4. **BladePipelines** (`blade_renderer.rs:140-150`):
48− - Struct holding all render pipelines
49− - Created at window initialization with shader source
50−
51−5. **Shader Data** (`blade_renderer.rs:56-123`):
52− - Uses `#[derive(blade_macros::ShaderData)]` for GPU bindings
53− - GlobalParams uniform, storage buffers for instance data
54−
55−### Data Flow
56−
57−```
58−Window::paint_*()
59− → Scene::insert_primitive()
60− → Scene::finish() sorts by order
61− → BladeRenderer::draw()
62− → scene.batches() iterator
63− → For each batch: bind pipeline, upload data, draw
64−```
65−
66−## Proposed Design
67−
68−### Architecture
69−
70−The design adds an extensible primitive system alongside the existing built-in primitives:
71−
72−```
73−┌─────────────────────────────────────────────────────────────┐
74−│ User Code │
75−│ - Define CustomPrimitive struct (#[repr(C)]) │
76−│ - Provide WGSL shader source │
77−│ - Register with GPUI via CustomShaderDescriptor │
78−└─────────────────────────────────────────────────────────────┘
79− │
80− ▼
81−┌─────────────────────────────────────────────────────────────┐
82−│ CustomShaderRegistry │
83−│ - Stores registered shader descriptors │
84−│ - Creates pipelines on renderer initialization │
85−│ - Maps CustomShaderId → pipeline + metadata │
86−└─────────────────────────────────────────────────────────────┘
87− │
88− ▼
89−┌─────────────────────────────────────────────────────────────┐
90−│ Scene (Extended) │
91−│ - Existing primitive vectors │
92−│ - New: custom_primitives: HashMap<CustomShaderId, Vec<u8>> │
93−│ - BatchIterator yields CustomBatch alongside built-ins │
94−└─────────────────────────────────────────────────────────────┘
95− │
96− ▼
97−┌─────────────────────────────────────────────────────────────┐
98−│ BladeRenderer (Extended) │
99−│ - Existing pipelines │
100−│ - New: custom_pipelines: HashMap<CustomShaderId, Pipeline> │
101−│ - draw() handles PrimitiveBatch::Custom │
102−└─────────────────────────────────────────────────────────────┘
103−```
104−
105−### API Surface
106−
107−#### 1. Custom Shader Descriptor
108−
109−```rust
110−/// Describes a custom shader and its pipeline configuration.
111−pub struct CustomShaderDescriptor {
112− /// Unique identifier for this shader
113− pub name: &'static str,
114−
115− /// WGSL shader source code
116− pub shader_source: &'static str,
117−
118− /// Vertex shader entry point name
119− pub vertex_entry: &'static str,
120−
121− /// Fragment shader entry point name
122− pub fragment_entry: &'static str,
123−
124− /// Size of each instance in bytes (must match #[repr(C)] struct)
125− pub instance_size: usize,
126−
127− /// Blend mode for this shader
128− pub blend_mode: CustomBlendMode,
129−
130− /// Primitive topology (typically TriangleStrip for instanced quads)
131− pub topology: PrimitiveTopology,
132−}
133−
134−pub enum CustomBlendMode {
135− /// Standard alpha blending
136− Alpha,
137− /// Premultiplied alpha
138− PremultipliedAlpha,
139− /// Additive blending
140− Additive,
141− /// No blending (opaque)
142− Opaque,
143−}
144−
145−pub enum PrimitiveTopology {
146− TriangleStrip,
147− TriangleList,
148−}
149−```
150−
151−#### 2. Custom Shader Handle
152−
153−```rust
154−/// Opaque handle to a registered custom shader.
155−#[derive(Clone, Copy, PartialEq, Eq, Hash)]
156−pub struct CustomShaderId(u32);
157−```
158−
159−#### 3. Custom Primitive Trait
160−
161−```rust
162−/// Trait for types that can be rendered as custom primitives.
163−///
164−/// # Safety
165−/// The implementing type must be `#[repr(C)]` and match the expected
166−/// layout in the WGSL shader.
167−pub unsafe trait CustomPrimitive: Pod + Zeroable + Clone {
168− /// Returns the bounds of this primitive for clipping/culling.
169− fn bounds(&self) -> Bounds<ScaledPixels>;
170−
171− /// Returns the content mask for clipping.
172− fn content_mask(&self) -> ContentMask<ScaledPixels>;
173−
174− /// Returns the draw order for sorting.
175− fn order(&self) -> DrawOrder;
176−
177− /// Sets the draw order.
178− fn set_order(&mut self, order: DrawOrder);
179−}
180−```
181−
182−#### 4. Registration API (on App)
183−
184−```rust
185−impl App {
186− /// Register a custom shader with GPUI.
187− ///
188− /// This should be called during app initialization before any windows
189− /// are created. The shader will be compiled when windows are opened.
190− pub fn register_custom_shader(
191− &mut self,
192− descriptor: CustomShaderDescriptor,
193− ) -> CustomShaderId;
194−}
195−```
196−
197−#### 5. Painting API (on Window)
198−
199−```rust
200−impl Window {
201− /// Paint a custom primitive into the scene.
202− ///
203− /// The primitive type must match the shader it was registered with.
204− /// This method should only be called during the paint phase.
205− pub fn paint_custom<P: CustomPrimitive>(
206− &mut self,
207− shader_id: CustomShaderId,
208− primitive: P,
209− );
210−
211− /// Paint multiple custom primitives of the same type.
212− ///
213− /// More efficient than calling paint_custom repeatedly.
214− pub fn paint_custom_batch<P: CustomPrimitive>(
215− &mut self,
216− shader_id: CustomShaderId,
217− primitives: &[P],
218− );
219−}
220−```
221−
222−### Example Usage
223−
224−```rust
225−// 1. Define the primitive struct (must match shader layout)
226−#[repr(C)]
227−#[derive(Clone, Copy, Pod, Zeroable)]
228−struct GlowQuad {
229− order: u32,
230− _pad: u32,
231− bounds: Bounds<ScaledPixels>,
232− content_mask: ContentMask<ScaledPixels>,
233− color: Hsla,
234− glow_radius: f32,
235− glow_intensity: f32,
236−}
237−
238−unsafe impl CustomPrimitive for GlowQuad {
239− fn bounds(&self) -> Bounds<ScaledPixels> { self.bounds }
240− fn content_mask(&self) -> ContentMask<ScaledPixels> { self.content_mask }
241− fn order(&self) -> DrawOrder { self.order }
242− fn set_order(&mut self, order: DrawOrder) { self.order = order; }
243−}
244−
245−// 2. Define the shader
246−const GLOW_SHADER: &str = r#"
247−struct GlowQuad {
248− order: u32,
249− pad: u32,
250− bounds: Bounds,
251− content_mask: Bounds,
252− color: Hsla,
253− glow_radius: f32,
254− glow_intensity: f32,
255−}
256−
257−var<storage, read> b_glow_quads: array<GlowQuad>;
258−
259−struct GlowVarying {
260− @builtin(position) position: vec4<f32>,
261− @location(0) @interpolate(flat) color: vec4<f32>,
262− // ... more fields
263−}
264−
265−@vertex
266−fn vs_glow(@builtin(vertex_index) vertex_id: u32,
267− @builtin(instance_index) instance_id: u32) -> GlowVarying {
268− let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
269− let quad = b_glow_quads[instance_id];
270− // ... transform and output
271−}
272−
273−@fragment
274−fn fs_glow(input: GlowVarying) -> @location(0) vec4<f32> {
275− // ... glow effect calculation
276−}
277−"#;
278−
279−// 3. Register at app startup
280−fn main() {
281− App::new().run(|cx| {
282− let glow_shader = cx.register_custom_shader(CustomShaderDescriptor {
283− name: "glow_quad",
284− shader_source: GLOW_SHADER,
285− vertex_entry: "vs_glow",
286− fragment_entry: "fs_glow",
287− instance_size: std::mem::size_of::<GlowQuad>(),
288− blend_mode: CustomBlendMode::PremultipliedAlpha,
289− topology: PrimitiveTopology::TriangleStrip,
290− });
291−
292− // Store shader_id somewhere accessible (e.g., Global)
293− cx.set_global(GlowShader(glow_shader));
294−
295− // ... rest of app setup
296− });
297−}
298−
299−// 4. Use during element painting
300−impl Render for MyElement {
301− fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
302− div()
303− .child("Hello")
304− .on_paint(cx.listener(|this, _bounds, window, cx| {
305− let shader_id = cx.global::<GlowShader>().0;
306− window.paint_custom(shader_id, GlowQuad {
307− order: 0,
308− _pad: 0,
309− bounds: /* ... */,
310− content_mask: window.content_mask().scale(window.scale_factor()),
311− color: hsla(0.5, 1.0, 0.5, 1.0),
312− glow_radius: 10.0,
313− glow_intensity: 0.8,
314− });
315− }))
316− }
317−}
318−```
319−
320−### Implementation Steps
321−
322−1. **[DONE] Add CustomShaderDescriptor and CustomShaderId types** (`crates/gpui/src/custom_shader.rs`)
323− - Define the descriptor struct
324− - Define the ID type
325− - Define CustomPrimitive trait
326− - Added `GPUI_SHADER_HEADER` constant with common shader utilities
327−
328−2. **[DONE] Add CustomShaderRegistry** (`crates/gpui/src/custom_shader.rs`)
329− - Store registered descriptors
330− - Provide lookup by ID
331− - Track which shaders need pipelines created
332−
333−3. **[DONE] Extend App with registration API**
334− - Add `register_custom_shader()` method
335− - Store registry in App state (`custom_shader_registry` field)
336−
337−4. **[DONE] Extend Scene with custom primitive storage** (`crates/gpui/src/scene.rs`)
338− - Add `custom_primitives: HashMap<CustomShaderId, CustomPrimitiveBuffer>`
339− - Add `insert_custom_primitive()` method
340− - Add `custom_batches()` method to yield custom batches separately
341−
342−5. **[DONE] Add PrimitiveBatch::Custom variant** (`crates/gpui/src/scene.rs`)
343− ```rust
344− Custom {
345− shader_id: CustomShaderId,
346− data: &'a [u8],
347− instance_count: u32,
348− vertices_per_instance: u32,
349− }
350− ```
351−
352−6. **[DONE] Extend BladeRenderer** (`crates/gpui/src/platform/blade/blade_renderer.rs`)
353− - Add `custom_pipelines: HashMap<CustomShaderId, CustomPipelineEntry>`
354− - Add `register_custom_shader()` method to create pipelines
355− - Handle `PrimitiveBatch::Custom` in `draw()` via `custom_batches()` iterator
356− - Add `ShaderCustomData` struct for GPU bindings
357−
358−7. **[DONE] Add Window painting API** (`crates/gpui/src/window.rs`)
359− - Add `paint_custom()` method
360−
361−8. **[DONE] Create helper shader functions module**
362− - Export common WGSL functions via `GPUI_SHADER_HEADER` constant
363− - Includes: GlobalParams, Bounds, Hsla, to_device_position, hsla_to_rgba, blend_color, etc.
364−
365−9. **[TODO] Add tests and examples**
366− - Unit tests for registration and painting
367− - Example custom shader (e.g., glow effect, noise pattern)
368−
369−10. **[TODO] Wire up shader registration from App to Renderer**
370− - Currently renderer has `register_custom_shader()` but needs to be called
371− - Need to plumb shader registry to window creation
372−
373−## Open Questions
374−
375−1. **Global vs Per-Window Shader Registration?**
376− - Current design: Global registration, per-window pipeline creation
377− - Alternative: Per-window registration (more flexible, more complex)
378− - **Decision**: Global registration is simpler and matches most use cases
379−
380−2. **How to handle shader compilation errors?**
381− - Option A: Panic on invalid shader (current built-in behavior)
382− - Option B: Return Result from registration
383− - Option C: Log error and skip rendering
384− - **Recommendation**: Return Result, let users handle errors
385−
386−3. **Should custom primitives participate in layer ordering?**
387− - Built-in primitives use `layer_stack` for z-ordering
388− - Custom primitives should integrate with this
389− - **Decision**: Yes, use the same DrawOrder system
390−
391−4. **Texture access for custom shaders?**
392− - Some custom effects need textures (e.g., noise, patterns)
393− - Could expose atlas access or allow custom texture uploads
394− - **Recommendation**: Add optional texture binding support in v2
395−
396−5. **Metal/DirectX support?**
397− - Initial implementation is Blade-only
398− - Could add abstractions for cross-platform shaders later
399− - **Recommendation**: Start with Blade, add others based on demand
400−
401−## References
402−
403−- `crates/gpui/src/scene.rs` - Scene and primitive definitions
404−- `crates/gpui/src/platform/blade/blade_renderer.rs` - Blade rendering implementation
405−- `crates/gpui/src/platform/blade/shaders.wgsl` - Built-in WGSL shaders
406−- `crates/gpui/src/window.rs` - Window painting API
407−- [blade_graphics crate](https://docs.rs/blade-graphics) - GPU abstraction layer
408−- [WGSL specification](https://www.w3.org/TR/WGSL/) - WebGPU Shading Language
threads-sidebar-plan.md+0−921
@@ -1,921 +0,0 @@
1−# MultiWorkspace Agent V2 Refactor
2−
3−
4−
5−
6−
7−
8−
9−This is a multi-phase project to enable Agent V2's vision: a threads sidebar that can switch between projects within a single window, with global thread management and folder ownership.
10−
11−---
12−
13−## Phase 1: GlobalThreadStore Infrastructure
14−
15−Adapt the existing `ThreadStore` to be global and add the data structures for project path associations and folder ownership tracking. **Note:** Association UI is deferred to Phase 3 - all threads will have empty paths initially.
16−
17−### 1.1 Make ThreadStore Global
18−
19−Add Global wrapper pattern (similar to `HistoryManager`):
20−
21−```rust
22−// In crates/agent/src/thread_store.rs
23−struct GlobalThreadStore(Entity<ThreadStore>);
24−impl Global for GlobalThreadStore {}
25−
26−impl ThreadStore {
27− pub fn init(cx: &mut App) { ... }
28− pub fn global(cx: &App) -> Entity<Self> { ... }
29−}
30−```
31−
32−Initialize in [`crates/zed/src/main.rs`](crates/zed/src/main.rs) near other global inits.
33−
34−### 1.2 Add Project Paths to Thread Storage
35−
36−**Database migration** - Add `project_paths` column to threads table in [`crates/agent/src/db.rs`](crates/agent/src/db.rs):
37−
38−```rust
39−// DbThreadMetadata - add project_paths for quick access
40−pub struct DbThreadMetadata {
41− pub id: acp::SessionId,
42− pub title: SharedString,
43− pub updated_at: DateTime<Utc>,
44− pub project_paths: Vec<PathBuf>, // NEW
45−}
46−
47−// DbThread - add with serde default for backwards compat
48−pub struct DbThread {
49− // ... existing fields ...
50− #[serde(default)]
51− pub project_paths: Vec<PathBuf>, // NEW
52−}
53−```
54−
55−### 1.3 Folder Ownership Tracking
56−
57−Add ownership tracking to `ThreadStore`:
58−
59−```rust
60−pub struct ThreadStore {
61− threads: Vec<DbThreadMetadata>,
62− // Tracks which folders are owned by which window
63− folder_owners: HashMap<PathBuf, WeakWindowHandle<Workspace>>,
64− // Tracks which thread is actively open in which window
65− active_threads: HashMap<acp::SessionId, WeakWindowHandle<Workspace>>,
66−}
67−```
68−
69−Key methods:
70−
71−- `claim_thread(session_id, window, project_paths)` - Claims ownership of thread's folders
72−- `release_thread(session_id)` - Releases folder ownership
73−- `can_open_thread(session_id, window)` - Checks if any folders overlap with another window
74−- `thread_available_in_window(session_id, window)` - Returns availability status for UI
75−
76−### 1.4 Update Consumers
77−
78−Update [`crates/agent_ui_v2/src/agents_panel.rs`](crates/agent_ui_v2/src/agents_panel.rs):
79−
80−- Replace `cx.new(|cx| ThreadStore::new(cx))` with `ThreadStore::global(cx)`
81−
82−**Note:** Folder overlap UI indicators are deferred to Phase 3. Since all threads have empty paths initially, there will be no conflicts to show.
83−
84−---
85−
86−## Phase 2: MultiWorkspace
87−
88−Enable the workspace to swap between different ThreadWorktrees contexts while keeping state in memory. **Key insight:** Project stays singular per window; we swap which ThreadWorktrees group is active.
89−
90−### 2.1 New Concept: ThreadWorktrees
91−
92−Add to [`crates/project/src/project.rs`](crates/project/src/project.rs):
93−
94−```rust
95−/// Identifies a ThreadWorktrees group - glues thread identity to workspace identity
96−#[derive(Clone, Hash, Eq, PartialEq)]
97−pub struct ThreadWorktreesId {
98− pub session_id: Option<acp::SessionId>, // None for "default" (no thread)
99− pub workspace_db_id: WorkspaceId,
100−}
101−
102−/// Groups worktrees that belong to a specific thread
103−pub struct ThreadWorktrees {
104− pub id: ThreadWorktreesId,
105− pub worktree_ids: Vec<WorktreeId>,
106− pub folder_paths: Vec<PathBuf>, // Absolute paths for the folders
107−}
108−
109−// Project additions
110−pub struct Project {
111− // Existing: all worktrees for all live threads in this window
112− worktree_store: WorktreeStore,
113−
114− // NEW: Groups worktrees by thread
115− thread_worktrees: HashMap<ThreadWorktreesId, ThreadWorktrees>,
116−
117− // NEW: Currently active thread context
118− active_thread_worktrees: ThreadWorktreesId,
119− // ...
120−}
121−```
122−
123−**Worktree lifecycle:**
124−
125−- Worktrees are automatically created/loaded when opening a thread
126−- Live threads (running in background) keep their worktrees loaded
127−- Worktrees shared between threads (e.g., /cloud in threads A and B) exist once in Project but are referenced by multiple ThreadWorktrees
128−
129−### 2.2 ThreadWorktrees as Panel API Surface
130−
131−Panels currently hold `Entity<Project>` and call methods like `project.read(cx).git_store()`. In multi-workspace, panels need **scoped access** - GitPanel for ThreadWorktrees A shouldn't see repos from ThreadWorktrees B.
132−
133−**ThreadWorktrees becomes the primary API surface for panels**, acting as a filtered lens into Project:
134−
135−```rust
136−pub struct ThreadWorktrees {
137− id: ThreadWorktreesId,
138− project: Entity<Project>,
139− worktree_ids: HashSet<WorktreeId>,
140−}
141−
142−impl ThreadWorktrees {
143− // === Scoped worktree access ===
144− pub fn visible_worktrees<'a>(&self, cx: &'a App) -> impl Iterator<Item = Entity<Worktree>> + 'a {
145− let dominated_ids = self.worktree_ids.clone();
146− self.project.read(cx)
147− .visible_worktrees(cx)
148− .filter(move |wt| dominated_ids.contains(&wt.read(cx).id()))
149− }
150−
151− pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option<Entity<Worktree>> {
152− if self.worktree_ids.contains(&id) {
153− self.project.read(cx).worktree_for_id(id, cx)
154− } else {
155− None
156− }
157− }
158−
159− // === Scoped git access ===
160− pub fn active_repository(&self, cx: &App) -> Option<Entity<Repository>> {
161− let repo = self.project.read(cx).active_repository(cx)?;
162− let worktree_id = repo.read(cx).worktree_id();
163− self.worktree_ids.contains(&worktree_id).then_some(repo)
164− }
165−
166− pub fn git_store(&self, cx: &App) -> Entity<GitStore> {
167− // Returns the shared git_store - events are filtered separately
168− self.project.read(cx).git_store().clone()
169− }
170−
171− // === Pass-through for global state ===
172− pub fn is_via_collab(&self, cx: &App) -> bool {
173− self.project.read(cx).is_via_collab()
174− }
175−
176− pub fn is_read_only(&self, cx: &App) -> bool {
177− self.project.read(cx).is_read_only(cx)
178− }
179−
180− pub fn languages(&self, cx: &App) -> Arc<LanguageRegistry> {
181− self.project.read(cx).languages().clone()
182− }
183−
184− pub fn path_style(&self, cx: &App) -> PathStyle {
185− self.project.read(cx).path_style(cx)
186− }
187−
188− // === Scoped file operations ===
189− pub fn open_buffer(&self, path: ProjectPath, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
190− if !self.worktree_ids.contains(&path.worktree_id) {
191− return Task::ready(Err(anyhow!("Path not in this ThreadWorktrees scope")));
192− }
193− self.project.update(cx, |project, cx| project.open_buffer(path, cx))
194− }
195−
196− pub fn delete_file(&self, path: ProjectPath, trash: bool, cx: &mut Context<Self>) -> Option<Task<Result<()>>> {
197− if !self.worktree_ids.contains(&path.worktree_id) {
198− return None;
199− }
200− self.project.update(cx, |project, cx| project.delete_file(path, trash, cx))
201− }
202−}
203−```
204−
205−**Project API usage categories:**
206−
207−| Category | Examples | ThreadWorktrees behavior |
208−|----------|----------|-------------------------|
209−| Worktree-scoped | `visible_worktrees()`, `worktree_for_id()` | Filtered to owned worktrees |
210−| Git-scoped | `active_repository()`, `git_init()` | Filtered by worktree ownership |
211−| File operations | `open_buffer()`, `delete_file()` | Validated against scope |
212−| Global/shared | `is_via_collab()`, `languages()`, `path_style()` | Pass-through to Project |
213−
214−**Event filtering via ThreadWorktrees:**
215−
216−ThreadWorktrees re-emits filtered events so panels don't need to filter themselves:
217−
218−```rust
219−pub enum ThreadWorktreesEvent {
220− ActiveRepositoryChanged(Option<Entity<Repository>>),
221− RepositoryUpdated(RepositoryId),
222− WorktreeAdded(WorktreeId),
223− WorktreeRemoved(WorktreeId),
224−}
225−
226−impl ThreadWorktrees {
227− fn new(
228− id: ThreadWorktreesId,
229− project: Entity<Project>,
230− worktree_ids: HashSet<WorktreeId>,
231− cx: &mut Context<Self>,
232− ) -> Self {
233− let git_store = project.read(cx).git_store().clone();
234−
235− // Subscribe to git_store and filter events
236− cx.subscribe(&git_store, |this, _store, event, cx| {
237− match event {
238− GitStoreEvent::ActiveRepositoryChanged(repo) => {
239− // Only emit if repo is in our worktrees
240− let dominated_repo = repo.as_ref().and_then(|r| {
241− this.worktree_ids.contains(&r.read(cx).worktree_id())
242− .then(|| r.clone())
243− });
244− cx.emit(ThreadWorktreesEvent::ActiveRepositoryChanged(dominated_repo));
245− }
246− GitStoreEvent::RepositoryUpdated(repo_id, _, _) => {
247− // Check if this repo belongs to our worktrees
248− if this.repo_in_scope(*repo_id, cx) {
249− cx.emit(ThreadWorktreesEvent::RepositoryUpdated(*repo_id));
250− }
251− }
252− // ... filter other events similarly
253− }
254− }).detach();
255−
256− Self { id, project, worktree_ids }
257− }
258−}
259−```
260−
261−**Panel migration pattern:**
262−
263−```rust
264−// Before: Panel holds Project
265−pub struct GitPanel {
266− project: Entity<Project>,
267− // ...
268−}
269−
270−// After: Panel holds ThreadWorktrees
271−pub struct GitPanel {
272− thread_worktrees: Entity<ThreadWorktrees>,
273− // ...
274−}
275−
276−// Before: Direct project access
277−let repos = self.project.read(cx).visible_worktrees(cx);
278−
279−// After: Scoped access via ThreadWorktrees
280−let repos = self.thread_worktrees.read(cx).visible_worktrees(cx);
281−
282−// Before: Subscribe to git_store directly
283−cx.subscribe(&git_store, |this, _, event, cx| { ... });
284−
285−// After: Subscribe to ThreadWorktrees events (pre-filtered)
286−cx.subscribe(&self.thread_worktrees, |this, _, event, cx| {
287− match event {
288− ThreadWorktreesEvent::ActiveRepositoryChanged(repo) => {
289− this.active_repository = repo.clone();
290− this.schedule_update(window, cx);
291− }
292− // ...
293− }
294−});
295−```
296−
297−**Benefits of this approach:**
298−
299−1. **Type safety** - Panels can't accidentally access wrong scope; they only have `ThreadWorktrees`
300−2. **Centralized filtering** - Event filtering happens once in `ThreadWorktrees`, not in every panel
301−3. **Clear API boundary** - Easy to see which operations are scoped vs global
302−4. **Testability** - Can create `ThreadWorktrees` with specific worktree sets for testing
303−
304−### 2.3 WorkspaceState per ThreadWorktrees
305−
306−Modify [`crates/workspace/src/workspace.rs`](crates/workspace/src/workspace.rs):
307−
308−```rust
309−/// State for a specific ThreadWorktrees context
310−struct WorkspaceState {
311− thread_worktrees_id: ThreadWorktreesId,
312− center: PaneGroup,
313− panes: Vec<Entity<Pane>>,
314− panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
315− active_pane: Entity<Pane>,
316− // Panel instances - each ThreadWorktrees gets its own panel entities
317− panel_instances: HashMap<TypeId, Box<dyn PanelHandle>>,
318−}
319−
320−pub struct Workspace {
321− project: Entity<Project>, // Still singular
322−
323− // NEW: State per ThreadWorktrees
324− workspace_states: HashMap<ThreadWorktreesId, WorkspaceState>,
325− active_workspace_state_id: ThreadWorktreesId,
326−
327− // Docks stay constant - but panel contents come from active WorkspaceState
328− left_dock: Entity<Dock>,
329− bottom_dock: Entity<Dock>,
330− right_dock: Entity<Dock>,
331−
332− // These stay constant across switches
333− status_bar: Entity<StatusBar>,
334− modal_layer: Entity<ModalLayer>,
335− toast_layer: Entity<ToastLayer>,
336− titlebar_item: Option<AnyView>,
337− notifications: Notifications,
338− // ...
339−}
340−```
341−
342−### 2.4 Switching Active ThreadWorktrees
343−
344−When switching threads:
345−
346−1. **Save current state** - Store current center/panes in `workspace_states`
347−2. **Update Project** - Set `active_thread_worktrees` to new ThreadWorktreesId
348−3. **Load/create WorkspaceState** - Either restore existing or create new
349−4. **Swap dock panels** - Update docks to show panels from new WorkspaceState
350−5. **Update status bar** - Refresh to reflect new active worktrees
351−```rust
352−impl Workspace {
353− pub fn switch_to_thread(
354− &mut self,
355− thread_worktrees_id: ThreadWorktreesId,
356− window: &mut Window,
357− cx: &mut Context<Self>,
358− ) {
359− // 1. Save current state
360− self.save_active_workspace_state(cx);
361−
362− // 2. Ensure worktrees are loaded in Project
363− self.project.update(cx, |project, cx| {
364− project.ensure_thread_worktrees_loaded(&thread_worktrees_id, cx);
365− });
366−
367− // 3. Load or create WorkspaceState
368− let state = self.workspace_states
369− .entry(thread_worktrees_id.clone())
370− .or_insert_with(|| WorkspaceState::new(thread_worktrees_id.clone(), cx));
371−
372− // 4. Update active state
373− self.active_workspace_state_id = thread_worktrees_id;
374−
375− // 5. Update UI
376− self.update_docks_for_active_state(window, cx);
377− self.update_window_title(window, cx);
378− cx.notify();
379− }
380−}
381−```
382−
383−
384−### 2.5 Panel Instance Management
385−
386−Each ThreadWorktrees gets its own panel instances:
387−
388−```rust
389−impl WorkspaceState {
390− fn get_or_create_panel<P: Panel>(&mut self, cx: &mut App) -> Entity<P> {
391− let type_id = TypeId::of::<P>();
392− if let Some(panel) = self.panel_instances.get(&type_id) {
393− panel.downcast::<P>().unwrap()
394− } else {
395− let panel = cx.new(|cx| P::new(/* ... */));
396− self.panel_instances.insert(type_id, Box::new(panel.clone()));
397− panel
398− }
399− }
400−}
401−```
402−
403−### 2.6 Feature-Flagged Action
404−
405−Add action under `agent-v2` feature flag:
406−
407−- `SwitchToThread(SessionId)` - Switches to a thread's ThreadWorktrees
408−- `OpenThread(SessionId)` - Opens a thread (loads its worktrees, creates WorkspaceState)
409−- Initially test with hardcoded thread data
410−
411−---
412−
413−## Phase 3: Agents Sidebar
414−
415−Create a new sidebar on the far left (outside the workspace/docks), replace AgentsPanel, and remove utility pane concept.
416−
417−### 3.1 Window Layout
418−
419−```
420−┌──────────┬──────────────────────────────────────────────────┐
421−│ │ │
422−│ Agents │ ┌─────────┬──────────────────┬────────┐ │
423−│ Sidebar │ │ Left │ │ Right │ │
424−│ │ │ Dock │ Center Panes │ Dock │ │
425−│ ──────── │ │ │ │ │ │
426−│ Thread │ │ ─────── │ │ │ │
427−│ List │ │ Agent │ │ │ │
428−│ │ │ Panel │ │ │ │
429−│ ──────── │ │ (thread │ │ │ │
430−│ Running │ │ view) │ │ │ │
431−│ Threads │ │ │ │ │ │
432−│ │ └─────────┴──────────────────┴────────┘ │
433−└──────────┴──────────────────────────────────────────────────┘
434−```
435−
436−**AgentsSidebar** (constant, outside workspace):
437−
438−- Far left, before left dock
439−- Resizable (like docks)
440−- Contains thread list/history and running threads section
441−- Does NOT swap with ThreadWorktrees
442−
443−**AgentPanel** (in left dock, swaps with ThreadWorktrees):
444−
445−- Shows single thread view (the thread associated with active ThreadWorktrees)
446−- Each ThreadWorktrees has its own AgentPanel instance
447−- Simpler than current AgentsPanel (no history, just thread view)
448−
449−### 3.2 AgentsSidebar Component
450−
451−Create new [`crates/agent_ui_v2/src/agents_sidebar.rs`](crates/agent_ui_v2/src/agents_sidebar.rs):
452−
453−```rust
454−pub struct AgentsSidebar {
455− width: Pixels,
456− thread_history: Entity<AcpThreadHistory>, // Reuse existing
457− running_threads: Vec<RunningThread>,
458− focus_handle: FocusHandle,
459−}
460−
461−struct RunningThread {
462− session_id: acp::SessionId,
463− thread_worktrees_id: ThreadWorktreesId,
464− thread: Entity<Thread>, // Keeps running in background
465− title: SharedString,
466− status: ThreadStatus, // Running, Idle, Error, etc.
467−}
468−
469−impl AgentsSidebar {
470− /// Called when user clicks a thread in the list
471− fn open_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
472− // 1. Check folder availability via GlobalThreadStore
473− // 2. If available, switch to thread's ThreadWorktrees
474− // 3. Show thread in AgentPanel
475− }
476−
477− /// Called when user clicks a running thread
478− fn switch_to_running_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
479− // Switch to that thread's ThreadWorktrees AND show in AgentPanel
480− // (Multiple threads can share the same ThreadWorktrees)
481− }
482−}
483−```
484−
485−### 3.3 AgentPanel (Simplified)
486−
487−Replace current AgentsPanel with simpler AgentPanel in [`crates/agent_ui_v2/src/agent_panel.rs`](crates/agent_ui_v2/src/agent_panel.rs):
488−
489−```rust
490−/// Panel in left dock that shows the thread view for active ThreadWorktrees
491−pub struct AgentPanel {
492− thread_view: Option<Entity<AcpThreadView>>,
493− thread_worktrees_id: ThreadWorktreesId,
494− // No history - that's in the sidebar now
495−}
496−
497−impl AgentPanel {
498− /// Shows thread associated with the current ThreadWorktrees
499− fn show_thread(&mut self, thread: &AgentSessionInfo, cx: &mut Context<Self>) {
500− // Create/update AcpThreadView
501− }
502−}
503−```
504−
505−**Key difference from current AgentsPanel:**
506−
507−- No thread history (moved to sidebar)
508−- No utility pane management (removed)
509−- One instance per ThreadWorktrees (swaps with workspace state)
510−
511−### 3.4 Remove Utility Pane Concept
512−
513−Delete or deprecate:
514−
515−- `AgentThreadPane` (`crates/agent_ui_v2/src/agent_thread_pane.rs`)
516−- Utility pane infrastructure in workspace
517−
518−### 3.5 Running Threads Section
519−
520−```rust
521−impl AgentsSidebar {
522− fn render_running_threads(&self, cx: &Context<Self>) -> impl IntoElement {
523− v_flex()
524− .child(Label::new("Running Threads").size(LabelSize::Small))
525− .children(self.running_threads.iter().map(|rt| {
526− ListItem::new(rt.session_id.clone())
527− .child(Label::new(rt.title.clone()))
528− .child(ThreadStatusIndicator::new(rt.status))
529− .on_click(cx.listener(move |this, _, _, cx| {
530− this.switch_to_running_thread(&rt.session_id, cx);
531− }))
532− }))
533− }
534−}
535−```
536−
537−### 3.6 Integration with Workspace
538−
539−Modify [`crates/workspace/src/workspace.rs`](crates/workspace/src/workspace.rs) to add sidebar:
540−
541−```rust
542−pub struct Workspace {
543− // NEW: Sidebar lives outside the swappable content
544− agents_sidebar: Option<Entity<AgentsSidebar>>,
545− agents_sidebar_width: Pixels,
546−
547− // Existing (docks swap their panel contents)
548− left_dock: Entity<Dock>,
549− // ...
550−}
551−
552−impl Workspace {
553− fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
554− h_flex()
555− .size_full()
556− // Sidebar first (far left)
557− .when_some(self.agents_sidebar.as_ref(), |this, sidebar| {
558− this.child(sidebar.clone())
559− })
560− // Then the rest of workspace (docks + center)
561− .child(self.render_workspace_content(window, cx))
562− }
563−}
564−```
565−
566−### 3.7 Thread Association UI
567−
568−Add UI for associating threads with project folders (deferred from Phase 1):
569−
570−- **Thread context menu** - Right-click thread in sidebar → "Associate with current project"
571−- **Sidebar action** - Button in thread list header
572−- **New thread prompt** - When creating thread, prompt to associate
573−```rust
574−impl AgentsSidebar {
575− fn associate_thread_with_project(
576− &mut self,
577− session_id: &acp::SessionId,
578− cx: &mut Context<Self>,
579− ) {
580− // Get current window's worktree paths from Project
581− // Update ThreadStore with association
582− // Refresh thread availability indicators
583− }
584−}
585−```
586−
587−
588−---
589−
590−## Architecture Diagram
591−
592−```mermaid
593−graph TB
594− subgraph app [Application]
595− GTS[GlobalThreadStore]
596− DB[(ThreadsDatabase)]
597− end
598−
599− subgraph window [Window - MultiWorkspace]
600− subgraph constant [Constant - Outside Workspace]
601− Sidebar[AgentsSidebar]
602− SidebarHistory[Thread History]
603− SidebarRunning[Running Threads]
604− end
605−
606− subgraph swappable [Swappable - Per ThreadWorktrees]
607− subgraph wsA [WorkspaceState A]
608− CenterA[Center Panes A]
609− AgentPanelA[AgentPanel A]
610− ProjectPanelA[ProjectPanel A]
611− end
612− subgraph wsB [WorkspaceState B]
613− CenterB[Center Panes B]
614− AgentPanelB[AgentPanel B]
615− ProjectPanelB[ProjectPanel B]
616− end
617− end
618−
619− Project[Project - all worktrees]
620− end
621−
622− GTS --> DB
623− Sidebar --> GTS
624− Sidebar --> SidebarHistory
625− Sidebar --> SidebarRunning
626− SidebarRunning --> wsA
627− SidebarRunning --> wsB
628− Project --> swappable
629−```
630−
631−## Window Layout
632−
633−```
634−┌──────────┬──────────────────────────────────────────────────┐
635−│ Agents │ ┌─────────┬──────────────────┬────────┐ │
636−│ Sidebar │ │ Left │ │ Right │ │
637−│ │ │ Dock │ Center Panes │ Dock │ │
638−│ ──────── │ │ │ (swappable) │ │ │
639−│ Thread │ │ Agent │ │ │ │
640−│ History │ │ Panel │ │ │ │
641−│ │ │ (swap) │ │ │ │
642−│ ──────── │ │ │ │ │ │
643−│ Running │ │ Project │ │ │ │
644−│ Threads │ │ Panel │ │ │ │
645−│ │ │ (swap) │ │ │ │
646−│(constant)│ └─────────┴──────────────────┴────────┘ │
647−└──────────┴──────────────────────────────────────────────────┘
648− ↑ ↑
649− Outside Inside Workspace
650− Workspace (swaps with ThreadWorktrees)
651−```
652−
653−## Key Concepts
654−
655−```
656−Window (MultiWorkspace)
657−└── Project (singular)
658− ├── Worktrees: [/zed, /cloud, /zed.dev] ← All loaded worktrees
659− ├── ThreadWorktrees A: [/zed, /cloud] ← Thread A's folders
660− ├── ThreadWorktrees B: [/cloud, /zed.dev] ← Thread B's folders
661− └── active_thread_worktrees: A
662−
663−└── Workspace
664− ├── workspace_states:
665− │ ├── A → WorkspaceState (center panes, panels for Thread A)
666− │ └── B → WorkspaceState (center panes, panels for Thread B)
667− ├── active_workspace_state_id: A
668− └── Docks (constant, panels swapped from active WorkspaceState)
669−```
670−
671−## Folder Ownership Model
672−
673−```
674−Thread A: folders {zed, cloud}
675−Thread B: folders {cloud, zed.dev}
676−
677−Window 1 opens Thread A:
678− → Window 1 owns: {zed, cloud}
679− → Thread B unavailable in Window 2 (shares "cloud")
680− → Thread B available in Window 1 (same window, can open both)
681−```
682−
683−---
684−
685−## Key Files
686−
687−| File | Phase | Changes |
688−|------|-------|---------|
689−| [`crates/agent/src/thread_store.rs`](crates/agent/src/thread_store.rs) | 1 | Global wrapper, ownership tracking |
690−| [`crates/agent/src/db.rs`](crates/agent/src/db.rs) | 1 | Add project_paths to schema |
691−| [`crates/zed/src/main.rs`](crates/zed/src/main.rs) | 1 | ThreadStore::init() |
692−| [`crates/project/src/project.rs`](crates/project/src/project.rs) | 2 | ThreadWorktrees struct, ThreadWorktreesId |
693−| [`crates/project/src/thread_worktrees.rs`](crates/project/src/thread_worktrees.rs) | 2 | NEW: ThreadWorktrees API surface, event filtering |
694−| [`crates/workspace/src/workspace.rs`](crates/workspace/src/workspace.rs) | 2, 3 | WorkspaceState, AgentsSidebar integration |
695−| [`crates/git_ui/src/git_panel.rs`](crates/git_ui/src/git_panel.rs) | 2 | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
696−| [`crates/outline_panel/src/outline_panel.rs`](crates/outline_panel/src/outline_panel.rs) | 2 | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
697−| [`crates/project_panel/src/project_panel.rs`](crates/project_panel/src/project_panel.rs) | 2 | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
698−| [`crates/agent_ui/src/agent_panel.rs`](crates/agent_ui/src/agent_panel.rs) | 2 | Migrate from `Entity<Project>` to `Entity<ThreadWorktrees>` |
699−| [`crates/agent_ui_v2/src/agents_sidebar.rs`](crates/agent_ui_v2/src/agents_sidebar.rs) | 3 | NEW: AgentsSidebar component |
700−| [`crates/agent_ui_v2/src/agent_panel.rs`](crates/agent_ui_v2/src/agent_panel.rs) | 3 | Simplified thread view panel |
701−| [`crates/agent_ui_v2/src/agents_panel.rs`](crates/agent_ui_v2/src/agents_panel.rs) | 3 | DELETE or heavily refactor |
702−| [`crates/agent_ui_v2/src/agent_thread_pane.rs`](crates/agent_ui_v2/src/agent_thread_pane.rs) | 3 | DELETE (utility pane removed) |
703−
704−---
705−
706−## Design Decisions
707−
708−1. **Thread-project association is explicit** - Users must explicitly associate threads with projects (UI in Phase 3). No automatic association on thread creation.
709−
710−2. **Existing threads have empty paths** - Backwards compatible: threads without `project_paths` are treated as available everywhere (no folder ownership conflicts).
711−
712−3. **Missing paths prompt reassociation** - If a thread's associated folders no longer exist on disk, prompt user to re-associate when opening.
713−
714−4. **Association scope** - When associating, all worktrees in the current project are associated.
715−
716−5. **Association UI entry points** (Phase 3):
717− - Thread context menu
718− - Sidebar action
719− - Auto-prompt on new thread creation
720−
721−6. **ThreadWorktrees as panel API surface** - Panels hold `Entity<ThreadWorktrees>` instead of `Entity<Project>`. This provides:
722− - **Type safety**: Panels can't accidentally access wrong scope
723− - **Centralized event filtering**: ThreadWorktrees filters git/worktree events before re-emitting
724− - **Clear API boundary**: Scoped operations (worktrees, git, files) vs pass-through (is_via_collab, languages)
725−
726−7. **Project remains singular** - One `Entity<Project>` per window holds all worktrees. ThreadWorktrees is a filtered lens, not a separate data store. Worktrees shared between threads exist once in Project.
727−
728−8. **Panel migration strategy** - Panels migrate incrementally:
729− - Replace `project: Entity<Project>` with `thread_worktrees: Entity<ThreadWorktrees>`
730− - Replace `cx.subscribe(&git_store, ...)` with `cx.subscribe(&thread_worktrees, ...)`
731− - Scoped methods have same signatures, making migration mechanical
732−
733−---
734−
735−## Open Questions
736−
737−*All questions resolved!*
738−
739−---
740−
741−## Resolved Questions
742−
743−### R1. ThreadWorktrees and buffer_store
744−
745−**Resolution: SHARED (pass-through)**
746−
747−`buffer_store()` is shared across all ThreadWorktrees, not scoped. Rationale:
748−
749−- **Buffers represent files, which are singular** - BufferStore already deduplicates by `ProjectPath`. Opening the same file twice returns the same `Entity<Buffer>`.
750−- **Shared worktrees mean shared files** - If Thread A and B both have `/cloud`, edits to `/cloud/src/main.rs` should be visible in both immediately.
751−- **Scoping is about visibility, not isolation** - ThreadWorktrees controls which worktrees appear in UI, not parallel editing universes.
752−- **LSP and collab depend on shared buffers** - Language servers work per-file, remote collaboration shares buffers by ID.
753−
754−Implementation:
755−```rust
756−impl ThreadWorktrees {
757− // Pass-through - buffers are shared
758− pub fn buffer_store(&self, cx: &App) -> Entity<BufferStore> {
759− self.project.read(cx).buffer_store().clone()
760− }
761−
762− // Convenience with scope validation (workflow guard, not isolation)
763− pub fn open_buffer(&self, path: ProjectPath, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
764− if !self.worktree_ids.contains(&path.worktree_id) {
765− return Task::ready(Err(anyhow!("Path not in this ThreadWorktrees scope")));
766− }
767− self.project.update(cx, |project, cx| project.open_buffer(path, cx))
768− }
769−}
770−```
771−
772−### R2. Cross-thread file references
773−
774−**Resolution: Center panes are NOT scoped, only panels are**
775−
776−When "Go to Definition" (or similar navigation) targets a file outside the current ThreadWorktrees:
777−
778−- **If the worktree is loaded** (another thread is live) → **Just works**. The file opens in a center tab. ProjectPanel won't show it in its tree (filtered by ThreadWorktrees), but the editor works normally.
779−
780−- **If the worktree is NOT loaded** → **File not available**. Show message: "Definition is in `/path` which is not currently loaded."
781−
782−Rationale:
783−- **Project** holds ALL worktrees from ALL live threads
784−- **ThreadWorktrees** is a filtered *view* for panels only
785−- **Center panes** (editors, tabs) can show any buffer from any loaded worktree
786−- This matches current Zed behavior - you can have open files that aren't in the project panel (via drag-drop, external open, etc.)
787−
788−Architecture:
789−```
790−┌─────────────────────────────────────────────────────────┐
791−│ Window │
792−├──────────┬──────────────────────────────────────────────┤
793−│ Agents │ ┌─────────┬──────────────────┬────────┐ │
794−│ Sidebar │ │ Left │ │ Right │ │
795−│ │ │ Dock │ Center Panes │ Dock │ │
796−│ │ │ │ │ │ │
797−│ │ │ SCOPED │ NOT SCOPED │ SCOPED │ │
798−│ │ │ (shows │ (can open any │ │ │
799−│ │ │ only │ buffer from │ │ │
800−│ │ │ Thread │ any loaded │ │ │
801−│ │ │ A's │ worktree) │ │ │
802−│ │ │ trees) │ │ │ │
803−│ │ └─────────┴──────────────────┴────────┘ │
804−└──────────┴──────────────────────────────────────────────┘
805−```
806−
807−### R3. ThreadWorktrees lifecycle
808−
809−**Resolution: Reference counting handles cleanup automatically**
810−
811−GPUI entities are reference counted. No special "closed" state or explicit cleanup logic needed.
812−
813−**Thread states:**
814−- **Active**: Currently shown in UI
815−- **Backgrounded**: Still alive (in `workspace_states` hashmap), not currently shown
816−
817−**Cleanup flow when closing a thread:**
818−1. Remove `WorkspaceState` from `workspace_states` hashmap
819−2. WorkspaceState is dropped, which drops panel instances
820−3. Panels drop their `Entity<ThreadWorktrees>` references
821−4. ThreadWorktrees is automatically cleaned up by GPUI (refcount → 0)
822−5. Worktrees may stay in Project (if referenced by other threads) or get cleaned up similarly
823−
824−No explicit `remove_thread_worktrees()` call needed - just drop the WorkspaceState and reference counting handles the rest.
825−
826−### R4. Panel state preservation
827−
828−**Resolution: Preserved by design**
829−
830−Each ThreadWorktrees has its own panel instances stored in `WorkspaceState.panel_instances`. When switching threads:
831−- Current WorkspaceState (with all panel state) stays in the `workspace_states` hashmap
832−- New ThreadWorktrees' WorkspaceState is restored (or created)
833−- Switch back → original panel state is intact (scroll positions, selections, etc.)
834−
835−No extra work needed - the architecture inherently preserves panel state.
836−
837−### R5. Global vs scoped panels
838−
839−**Resolution: Most panels are scoped, a few stay global**
840−
841−**SCOPED (per-ThreadWorktrees)** - stored in `WorkspaceState.panel_instances`:
842−| Panel | Reason |
843−|-------|--------|
844−| ProjectPanel | Shows worktree file trees |
845−| GitPanel | Shows repos tied to worktrees |
846−| OutlinePanel | Shows context for files in worktrees |
847−| DebugPanel | Debug sessions are project-specific |
848−| TerminalPanel | Terminals have working directories in worktrees |
849−| AgentPanel | Thread-specific conversation |
850−
851−**GLOBAL (shared)** - stored on `Workspace` directly (current behavior):
852−| Panel | Reason |
853−|-------|--------|
854−| CollabPanel | Collaborators, channels, contacts - user's social state |
855−| NotificationPanel | User's notifications across all projects |
856−
857−**SPECIAL:**
858−| Panel | Reason |
859−|-------|--------|
860−| AgentsSidebar | Lives outside workspace/dock system entirely |
861−
862−**Implementation note:** `WorkspaceState` should be an `Entity<WorkspaceState>` so that `zed.rs` can initialize scoped panels using the existing `initialize_panels` pattern. This:
863−- Keeps the initialization flow in one place (zed.rs)
864−- Solves cyclic reference problems (panels reference workspace, workspace references panels)
865−- Matches the existing pattern that already works
866−
867−```rust
868−// In zed.rs - similar to current initialize_panels but per-WorkspaceState
869−fn initialize_scoped_panels(
870− workspace_state: WeakEntity<WorkspaceState>,
871− thread_worktrees: Entity<ThreadWorktrees>,
872− cx: AsyncWindowContext,
873−) {
874− cx.spawn(async move |cx| {
875− let project_panel = ProjectPanel::load(thread_worktrees.clone(), cx.clone());
876− let git_panel = GitPanel::load(thread_worktrees.clone(), cx.clone());
877− // ... other scoped panels
878−
879− futures::join!(
880− add_panel_when_ready(project_panel, workspace_state.clone(), cx.clone()),
881− add_panel_when_ready(git_panel, workspace_state.clone(), cx.clone()),
882− // ...
883− );
884− }).detach();
885−}
886−```
887−
888−Global panels (CollabPanel, NotificationPanel) continue to be initialized once per Workspace as they are today.
889−
890−### R6. Sidebar persistence
891−
892−**Resolution: Serialize alongside MultiWorkspace state**
893−
894−Sidebar state should be persisted to the database alongside the MultiWorkspace state. Rationale:
895−- One MultiWorkspace per Sidebar (per window) - their lifecycles are tied
896−- State should be co-located for consistency
897−- Enables full restore of window state on restart
898−
899−**What to persist:**
900−- Sidebar width
901−- Expanded/collapsed sections (thread history, running threads)
902−- Scroll position in thread list
903−- Any filter/search state
904−
905−**Where to store:**
906−Alongside the existing workspace serialization in the DB, keyed by `WorkspaceId`. The `SerializedWorkspace` struct (or a new sibling) would include sidebar state.
907−
908−```rust
909−struct SerializedMultiWorkspace {
910− // Existing workspace fields...
911−
912− // Sidebar state
913− sidebar_width: Option<Pixels>,
914− sidebar_expanded_sections: Vec<String>,
915− sidebar_scroll_position: Option<f32>,
916−
917− // Per-ThreadWorktrees state
918− thread_workspace_states: HashMap<ThreadWorktreesId, SerializedWorkspaceState>,
919− active_thread_worktrees_id: Option<ThreadWorktreesId>,
920−}
921−```
utility-pane-removal-plan.md+0−154
@@ -1,154 +0,0 @@
1−# Utility Pane Removal Plan
2−
3−## Overview
4−
5−The utility pane is a secondary pane system that sits alongside the main editor panes. It allows panels (specifically `AgentThreadPane`) to "pop out" a detail view next to the center editor area. It supports two slots:
6−
7−- **Left slot**: Adjacent to the left dock
8−- **Right slot**: Adjacent to the right dock
9−
10−According to `threads-sidebar-plan.md`, this removal is part of **Phase 3** of the MultiWorkspace Agent V2 Refactor. The utility pane concept is being replaced by an "Agents Sidebar" that will live outside the workspace/docks structure.
11−
12−---
13−
14−## Files to Modify/Delete
15−
16−### 1. Core Infrastructure (workspace crate)
17−
18−- `crates/workspace/src/utility_pane.rs` — **DELETE**
19− - Contains all utility pane state, types, and impl methods on Workspace
20−
21−- `crates/workspace/src/dock.rs` — **EDIT**
22− - Remove `UtilityPane`, `UtilityPaneHandle`, `UtilityPanePosition`, `MinimizePane`, `ClosePane`
23−
24−- `crates/workspace/src/workspace.rs` — **EDIT**
25− - Remove: `utility_panes` field, imports, `clamp_utility_pane_widths()`, `max_utility_pane_width()`, `UtilityPaneFrame` renders in `fn render()`
26−
27−- `crates/workspace/src/pane.rs` — **EDIT**
28− - Remove utility pane toggle buttons from tab bar rendering
29−
30−### 2. Agent UI V2 (consumer of utility panes)
31−
32−- `crates/agent_ui_v2/src/agent_thread_pane.rs` — **DELETE**
33− - The only implementer of `UtilityPane` trait
34−
35−- `crates/agent_ui_v2/src/agents_panel.rs` — **EDIT**
36− - Remove all utility pane management (`register_utility_pane`, `restore_utility_pane`, subscriptions, etc.)
37−
38−### 3. Settings
39−
40−- `crates/settings/src/settings_content/agent.rs` — **EDIT**
41− - Remove "utility pane" dock setting comment/option
42−
43−---
44−
45−## Types and Traits to Remove
46−
47−### From `crates/workspace/src/dock.rs`
48−
49−- `MinimizePane` (struct)
50−- `ClosePane` (struct)
51−- `UtilityPane` (trait)
52−- `UtilityPaneHandle` (trait)
53−- `UtilityPanePosition` (enum)
54−
55−### From `crates/workspace/src/utility_pane.rs` (entire file)
56−
57−- `UtilityPaneSlot` (enum)
58−- `UtilityPaneSlotState` (struct)
59−- `UtilityPaneState` (struct)
60−- `DraggedUtilityPane` (struct)
61−- `UtilityPaneFrame` (struct)
62−- `utility_slot_for_dock_position()` (function)
63−- `UTILITY_PANE_RESIZE_HANDLE_SIZE` (const)
64−- `UTILITY_PANE_MIN_WIDTH` (const)
65−- All `impl Workspace` methods for utility panes
66−
67−---
68−
69−## Methods on `Workspace` to Remove
70−
71−- `utility_pane()` — Get a utility pane by slot
72−- `toggle_utility_pane()` — Toggle expanded state
73−- `register_utility_pane()` — Register a new utility pane
74−- `clear_utility_pane()` — Clear a utility pane slot
75−- `clear_utility_pane_if_provider()` — Conditionally clear if owned by panel
76−- `resize_utility_pane()` — Resize a utility pane
77−- `reset_utility_pane_width()` — Reset to default width
78−- `clamp_utility_pane_widths()` — Clamp widths when docks resize
79−- `max_utility_pane_width()` — Calculate max allowed width
80−
81−---
82−
83−## Render Changes in `workspace.rs`
84−
85−The `render()` method has **8 places** where `UtilityPaneFrame::new()` is conditionally rendered based on dock positions. All of these need to be removed:
86−
87−- Line ~7244: `UtilityPaneFrame::new(UtilityPaneSlot::Left, ...)`
88−- Line ~7294: `UtilityPaneFrame::new(UtilityPaneSlot::Right, ...)`
89−- Line ~7333: `UtilityPaneFrame::new(UtilityPaneSlot::Left, ...)`
90−- Line ~7368: `UtilityPaneFrame::new(UtilityPaneSlot::Right, ...)`
91−- Line ~7400: `UtilityPaneFrame::new(UtilityPaneSlot::Left, ...)`
92−- Line ~7447: `UtilityPaneFrame::new(UtilityPaneSlot::Right, ...)`
93−- Line ~7474: `UtilityPaneFrame::new(UtilityPaneSlot::Left, ...)`
94−- Line ~7519: `UtilityPaneFrame::new(UtilityPaneSlot::Right, ...)`
95−
96−Also remove:
97−- The `DragMoveEvent<DraggedUtilityPane>` handler (~line 7195)
98−- The `DraggedUtilityPane` import
99−
100−---
101−
102−## Implementation Order
103−
104−### Phase 1: Remove Consumer (agent_ui_v2)
105−
106−1. Delete `crates/agent_ui_v2/src/agent_thread_pane.rs`
107−2. Edit `crates/agent_ui_v2/src/agents_panel.rs`:
108− - Remove imports from `workspace::dock` and `workspace::utility_pane`
109− - Remove `agent_thread_pane` field and related fields
110− - Remove `restore_utility_pane()` method
111− - Remove `handle_utility_pane_event()` method
112− - Remove `handle_close_pane_event()` method
113− - Remove utility pane registration in `open_thread()`
114− - Remove `utility_slot()` method
115− - Update `serialize()` to not include pane state
116−
117−### Phase 2: Remove Infrastructure (workspace)
118−
119−1. Delete `crates/workspace/src/utility_pane.rs`
120−2. Edit `crates/workspace/src/dock.rs`:
121− - Remove `MinimizePane`, `ClosePane` structs
122− - Remove `UtilityPane` trait
123− - Remove `UtilityPaneHandle` trait and impl
124− - Remove `UtilityPanePosition` enum
125−3. Edit `crates/workspace/src/workspace.rs`:
126− - Remove `pub mod utility_pane;` declaration
127− - Remove utility_pane imports
128− - Remove `utility_panes: UtilityPaneState` field from `Workspace` struct
129− - Remove initialization in `Workspace::new()`
130− - Remove `clamp_utility_pane_widths()` calls in dock resize methods
131− - Remove `max_utility_pane_width()` method
132− - Remove `clamp_utility_pane_widths()` method
133− - Remove all `UtilityPaneFrame` renders in `fn render()`
134− - Remove `DragMoveEvent<DraggedUtilityPane>` handler
135− - Remove `clear_utility_pane_if_provider()` call in `remove_panel()`
136−4. Edit `crates/workspace/src/pane.rs`:
137− - Remove `utility_pane::UtilityPaneSlot` import
138− - Remove `open_aside_left` rendering block in `render_tab_bar()`
139− - Remove `open_aside_right` rendering block in `render_tab_bar()`
140−
141−### Phase 3: Clean Up Settings
142−
143−1. Edit `crates/settings/src/settings_content/agent.rs`:
144− - Remove or update the "utility pane" dock setting documentation
145−
146−---
147−
148−## Testing
149−
150−After removal:
151−1. Run `cargo check -p workspace -p agent_ui_v2`
152−2. Run `./script/clippy`
153−3. Verify the workspace renders correctly without utility panes
154−4. Verify the agents panel still functions (without the thread pane popup)