Fix project not getting dropped after closing window (#44237)

Bennet Bo Fenner created

Change summary

crates/agent_ui/src/acp/entry_view_state.rs           | 12 +-
crates/agent_ui/src/acp/message_editor.rs             | 44 ++++----
crates/agent_ui/src/acp/thread_view.rs                |  4 
crates/assistant_text_thread/src/text_thread.rs       | 16 +--
crates/assistant_text_thread/src/text_thread_store.rs | 65 +++++++++----
5 files changed, 80 insertions(+), 61 deletions(-)

Detailed changes

crates/agent_ui/src/acp/entry_view_state.rs 🔗

@@ -22,7 +22,7 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
 
 pub struct EntryViewState {
     workspace: WeakEntity<Workspace>,
-    project: Entity<Project>,
+    project: WeakEntity<Project>,
     history_store: Entity<HistoryStore>,
     prompt_store: Option<Entity<PromptStore>>,
     entries: Vec<Entry>,
@@ -34,7 +34,7 @@ pub struct EntryViewState {
 impl EntryViewState {
     pub fn new(
         workspace: WeakEntity<Workspace>,
-        project: Entity<Project>,
+        project: WeakEntity<Project>,
         history_store: Entity<HistoryStore>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
@@ -328,7 +328,7 @@ impl Entry {
 
 fn create_terminal(
     workspace: WeakEntity<Workspace>,
-    project: Entity<Project>,
+    project: WeakEntity<Project>,
     terminal: Entity<acp_thread::Terminal>,
     window: &mut Window,
     cx: &mut App,
@@ -336,9 +336,9 @@ fn create_terminal(
     cx.new(|cx| {
         let mut view = TerminalView::new(
             terminal.read(cx).inner().clone(),
-            workspace.clone(),
+            workspace,
             None,
-            project.downgrade(),
+            project,
             window,
             cx,
         );
@@ -458,7 +458,7 @@ mod tests {
         let view_state = cx.new(|_cx| {
             EntryViewState::new(
                 workspace.downgrade(),
-                project.clone(),
+                project.downgrade(),
                 history_store,
                 None,
                 Default::default(),

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -39,7 +39,6 @@ use zed_actions::agent::Chat;
 pub struct MessageEditor {
     mention_set: Entity<MentionSet>,
     editor: Entity<Editor>,
-    project: Entity<Project>,
     workspace: WeakEntity<Workspace>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
     available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -98,7 +97,7 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
 impl MessageEditor {
     pub fn new(
         workspace: WeakEntity<Workspace>,
-        project: Entity<Project>,
+        project: WeakEntity<Project>,
         history_store: Entity<HistoryStore>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
@@ -135,13 +134,8 @@ impl MessageEditor {
             editor.register_addon(MessageEditorAddon::new());
             editor
         });
-        let mention_set = cx.new(|_cx| {
-            MentionSet::new(
-                project.downgrade(),
-                history_store.clone(),
-                prompt_store.clone(),
-            )
-        });
+        let mention_set =
+            cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
         let completion_provider = Rc::new(PromptCompletionProvider::new(
             cx.entity(),
             editor.downgrade(),
@@ -199,7 +193,6 @@ impl MessageEditor {
 
         Self {
             editor,
-            project,
             mention_set,
             workspace,
             prompt_capabilities,
@@ -572,17 +565,18 @@ impl MessageEditor {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
         };
-        let path_style = self.project.read(cx).path_style(cx);
+        let project = workspace.read(cx).project().clone();
+        let path_style = project.read(cx).path_style(cx);
         let buffer = self.editor.read(cx).buffer().clone();
         let Some(buffer) = buffer.read(cx).as_singleton() else {
             return;
         };
         let mut tasks = Vec::new();
         for path in paths {
-            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
+            let Some(entry) = project.read(cx).entry_for_path(&path, cx) else {
                 continue;
             };
-            let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
+            let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else {
                 continue;
             };
             let abs_path = worktree.read(cx).absolutize(&path.path);
@@ -690,9 +684,13 @@ impl MessageEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
+
         self.clear(window, cx);
 
-        let path_style = self.project.read(cx).path_style(cx);
+        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
         let mut text = String::new();
         let mut mentions = Vec::new();
 
@@ -935,7 +933,7 @@ mod tests {
             cx.new(|cx| {
                 MessageEditor::new(
                     workspace.downgrade(),
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     Default::default(),
@@ -1046,7 +1044,7 @@ mod tests {
             cx.new(|cx| {
                 MessageEditor::new(
                     workspace_handle.clone(),
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     prompt_capabilities.clone(),
@@ -1207,7 +1205,7 @@ mod tests {
             let message_editor = cx.new(|cx| {
                 MessageEditor::new(
                     workspace_handle,
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     prompt_capabilities.clone(),
@@ -1429,7 +1427,7 @@ mod tests {
             let message_editor = cx.new(|cx| {
                 MessageEditor::new(
                     workspace_handle,
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     prompt_capabilities.clone(),
@@ -1920,7 +1918,7 @@ mod tests {
             cx.new(|cx| {
                 let editor = MessageEditor::new(
                     workspace.downgrade(),
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     Default::default(),
@@ -2025,7 +2023,7 @@ mod tests {
             cx.new(|cx| {
                 let mut editor = MessageEditor::new(
                     workspace.downgrade(),
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     Default::default(),
@@ -2094,7 +2092,7 @@ mod tests {
             cx.new(|cx| {
                 MessageEditor::new(
                     workspace.downgrade(),
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     Default::default(),
@@ -2157,7 +2155,7 @@ mod tests {
             let message_editor = cx.new(|cx| {
                 MessageEditor::new(
                     workspace_handle,
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     Default::default(),
@@ -2315,7 +2313,7 @@ mod tests {
             let message_editor = cx.new(|cx| {
                 MessageEditor::new(
                     workspace_handle,
-                    project.clone(),
+                    project.downgrade(),
                     history_store.clone(),
                     None,
                     Default::default(),

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -344,7 +344,7 @@ impl AcpThreadView {
         let message_editor = cx.new(|cx| {
             let mut editor = MessageEditor::new(
                 workspace.clone(),
-                project.clone(),
+                project.downgrade(),
                 history_store.clone(),
                 prompt_store.clone(),
                 prompt_capabilities.clone(),
@@ -369,7 +369,7 @@ impl AcpThreadView {
         let entry_view_state = cx.new(|_| {
             EntryViewState::new(
                 workspace.clone(),
-                project.clone(),
+                project.downgrade(),
                 history_store.clone(),
                 prompt_store.clone(),
                 prompt_capabilities.clone(),

crates/assistant_text_thread/src/text_thread.rs 🔗

@@ -14,7 +14,7 @@ use fs::{Fs, RenameOptions};
 use futures::{FutureExt, StreamExt, future::Shared};
 use gpui::{
     App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
-    Task,
+    Task, WeakEntity,
 };
 use itertools::Itertools as _;
 use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
@@ -688,7 +688,7 @@ pub struct TextThread {
     _subscriptions: Vec<Subscription>,
     telemetry: Option<Arc<Telemetry>>,
     language_registry: Arc<LanguageRegistry>,
-    project: Option<Entity<Project>>,
+    project: Option<WeakEntity<Project>>,
     prompt_builder: Arc<PromptBuilder>,
     completion_mode: agent_settings::CompletionMode,
 }
@@ -708,7 +708,7 @@ impl EventEmitter<TextThreadEvent> for TextThread {}
 impl TextThread {
     pub fn local(
         language_registry: Arc<LanguageRegistry>,
-        project: Option<Entity<Project>>,
+        project: Option<WeakEntity<Project>>,
         telemetry: Option<Arc<Telemetry>>,
         prompt_builder: Arc<PromptBuilder>,
         slash_commands: Arc<SlashCommandWorkingSet>,
@@ -742,7 +742,7 @@ impl TextThread {
         language_registry: Arc<LanguageRegistry>,
         prompt_builder: Arc<PromptBuilder>,
         slash_commands: Arc<SlashCommandWorkingSet>,
-        project: Option<Entity<Project>>,
+        project: Option<WeakEntity<Project>>,
         telemetry: Option<Arc<Telemetry>>,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -873,7 +873,7 @@ impl TextThread {
         language_registry: Arc<LanguageRegistry>,
         prompt_builder: Arc<PromptBuilder>,
         slash_commands: Arc<SlashCommandWorkingSet>,
-        project: Option<Entity<Project>>,
+        project: Option<WeakEntity<Project>>,
         telemetry: Option<Arc<Telemetry>>,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -1167,10 +1167,6 @@ impl TextThread {
         self.language_registry.clone()
     }
 
-    pub fn project(&self) -> Option<Entity<Project>> {
-        self.project.clone()
-    }
-
     pub fn prompt_builder(&self) -> Arc<PromptBuilder> {
         self.prompt_builder.clone()
     }
@@ -2967,7 +2963,7 @@ impl TextThread {
     }
 
     fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut App) {
-        let Some(project) = &self.project else {
+        let Some(project) = self.project.as_ref().and_then(|project| project.upgrade()) else {
             return;
         };
         project.read(cx).user_store().update(cx, |user_store, cx| {

crates/assistant_text_thread/src/text_thread_store.rs 🔗

@@ -51,7 +51,7 @@ pub struct TextThreadStore {
     telemetry: Arc<Telemetry>,
     _watch_updates: Task<Option<()>>,
     client: Arc<Client>,
-    project: Entity<Project>,
+    project: WeakEntity<Project>,
     project_is_shared: bool,
     client_subscription: Option<client::Subscription>,
     _project_subscriptions: Vec<gpui::Subscription>,
@@ -119,10 +119,10 @@ impl TextThreadStore {
                     ],
                     project_is_shared: false,
                     client: project.read(cx).client(),
-                    project: project.clone(),
+                    project: project.downgrade(),
                     prompt_builder,
                 };
-                this.handle_project_shared(project.clone(), cx);
+                this.handle_project_shared(cx);
                 this.synchronize_contexts(cx);
                 this.register_context_server_handlers(cx);
                 this.reload(cx).detach_and_log_err(cx);
@@ -146,7 +146,7 @@ impl TextThreadStore {
             telemetry: project.read(cx).client().telemetry().clone(),
             _watch_updates: Task::ready(None),
             client: project.read(cx).client(),
-            project,
+            project: project.downgrade(),
             project_is_shared: false,
             client_subscription: None,
             _project_subscriptions: Default::default(),
@@ -180,8 +180,10 @@ impl TextThreadStore {
     ) -> Result<proto::OpenContextResponse> {
         let context_id = TextThreadId::from_proto(envelope.payload.context_id);
         let operations = this.update(&mut cx, |this, cx| {
+            let project = this.project.upgrade().context("project not found")?;
+
             anyhow::ensure!(
-                !this.project.read(cx).is_via_collab(),
+                !project.read(cx).is_via_collab(),
                 "only the host contexts can be opened"
             );
 
@@ -211,8 +213,9 @@ impl TextThreadStore {
         mut cx: AsyncApp,
     ) -> Result<proto::CreateContextResponse> {
         let (context_id, operations) = this.update(&mut cx, |this, cx| {
+            let project = this.project.upgrade().context("project not found")?;
             anyhow::ensure!(
-                !this.project.read(cx).is_via_collab(),
+                !project.read(cx).is_via_collab(),
                 "can only create contexts as the host"
             );
 
@@ -255,8 +258,9 @@ impl TextThreadStore {
         mut cx: AsyncApp,
     ) -> Result<proto::SynchronizeContextsResponse> {
         this.update(&mut cx, |this, cx| {
+            let project = this.project.upgrade().context("project not found")?;
             anyhow::ensure!(
-                !this.project.read(cx).is_via_collab(),
+                !project.read(cx).is_via_collab(),
                 "only the host can synchronize contexts"
             );
 
@@ -293,8 +297,12 @@ impl TextThreadStore {
         })?
     }
 
-    fn handle_project_shared(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
-        let is_shared = self.project.read(cx).is_shared();
+    fn handle_project_shared(&mut self, cx: &mut Context<Self>) {
+        let Some(project) = self.project.upgrade() else {
+            return;
+        };
+
+        let is_shared = project.read(cx).is_shared();
         let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
         if is_shared == was_shared {
             return;
@@ -309,7 +317,7 @@ impl TextThreadStore {
                     false
                 }
             });
-            let remote_id = self.project.read(cx).remote_id().unwrap();
+            let remote_id = project.read(cx).remote_id().unwrap();
             self.client_subscription = self
                 .client
                 .subscribe_to_entity(remote_id)
@@ -323,13 +331,13 @@ impl TextThreadStore {
 
     fn handle_project_event(
         &mut self,
-        project: Entity<Project>,
+        _project: Entity<Project>,
         event: &project::Event,
         cx: &mut Context<Self>,
     ) {
         match event {
             project::Event::RemoteIdChanged(_) => {
-                self.handle_project_shared(project, cx);
+                self.handle_project_shared(cx);
             }
             project::Event::Reshared => {
                 self.advertise_contexts(cx);
@@ -382,7 +390,10 @@ impl TextThreadStore {
     }
 
     pub fn create_remote(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<TextThread>>> {
-        let project = self.project.read(cx);
+        let Some(project) = self.project.upgrade() else {
+            return Task::ready(Err(anyhow::anyhow!("project was dropped")));
+        };
+        let project = project.read(cx);
         let Some(project_id) = project.remote_id() else {
             return Task::ready(Err(anyhow::anyhow!("project was not remote")));
         };
@@ -541,7 +552,10 @@ impl TextThreadStore {
         text_thread_id: TextThreadId,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<TextThread>>> {
-        let project = self.project.read(cx);
+        let Some(project) = self.project.upgrade() else {
+            return Task::ready(Err(anyhow::anyhow!("project was dropped")));
+        };
+        let project = project.read(cx);
         let Some(project_id) = project.remote_id() else {
             return Task::ready(Err(anyhow::anyhow!("project was not remote")));
         };
@@ -618,7 +632,10 @@ impl TextThreadStore {
         event: &TextThreadEvent,
         cx: &mut Context<Self>,
     ) {
-        let Some(project_id) = self.project.read(cx).remote_id() else {
+        let Some(project) = self.project.upgrade() else {
+            return;
+        };
+        let Some(project_id) = project.read(cx).remote_id() else {
             return;
         };
 
@@ -652,12 +669,14 @@ impl TextThreadStore {
     }
 
     fn advertise_contexts(&self, cx: &App) {
-        let Some(project_id) = self.project.read(cx).remote_id() else {
+        let Some(project) = self.project.upgrade() else {
+            return;
+        };
+        let Some(project_id) = project.read(cx).remote_id() else {
             return;
         };
-
         // For now, only the host can advertise their open contexts.
-        if self.project.read(cx).is_via_collab() {
+        if project.read(cx).is_via_collab() {
             return;
         }
 
@@ -689,7 +708,10 @@ impl TextThreadStore {
     }
 
     fn synchronize_contexts(&mut self, cx: &mut Context<Self>) {
-        let Some(project_id) = self.project.read(cx).remote_id() else {
+        let Some(project) = self.project.upgrade() else {
+            return;
+        };
+        let Some(project_id) = project.read(cx).remote_id() else {
             return;
         };
 
@@ -828,7 +850,10 @@ impl TextThreadStore {
     }
 
     fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
-        let context_server_store = self.project.read(cx).context_server_store();
+        let Some(project) = self.project.upgrade() else {
+            return;
+        };
+        let context_server_store = project.read(cx).context_server_store();
         cx.subscribe(&context_server_store, Self::handle_context_server_event)
             .detach();