agent: Move `ActiveThread` and `MessageEditor` into `ActiveView` (#33462)

Agus Zubiaga , Antonio Scandurra , Conrad Irwin , and Smit Barmase created

`ActiveThread` and `MessageEditor` only make sense when `active_view` is
`Thread`, so we moved them in there. This will make it easier to work on
new agent threads.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

crates/agent_ui/src/agent_panel.rs    | 610 +++++++++++++---------------
crates/agent_ui/src/context_picker.rs |   2 
crates/agent_ui/src/context_strip.rs  |   2 
3 files changed, 286 insertions(+), 328 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -120,22 +120,32 @@ pub fn init(cx: &mut App) {
                 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
-                        let thread = panel.read(cx).thread.read(cx).thread().clone();
-                        AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
+                        match &panel.read(cx).active_view {
+                            ActiveView::Thread { thread, .. } => {
+                                let thread = thread.read(cx).thread().clone();
+                                AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
+                            }
+                            ActiveView::TextThread { .. }
+                            | ActiveView::History
+                            | ActiveView::Configuration => {}
+                        }
                     }
                 })
                 .register_action(|workspace, _: &Follow, window, cx| {
                     workspace.follow(CollaboratorId::Agent, window, cx);
                 })
                 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
-                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                        workspace.focus_panel::<AgentPanel>(window, cx);
-                        panel.update(cx, |panel, cx| {
-                            panel.message_editor.update(cx, |editor, cx| {
+                    let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
+                        return;
+                    };
+                    workspace.focus_panel::<AgentPanel>(window, cx);
+                    panel.update(cx, |panel, cx| {
+                        if let Some(message_editor) = panel.active_message_editor() {
+                            message_editor.update(cx, |editor, cx| {
                                 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
                             });
-                        });
-                    }
+                        }
+                    });
                 })
                 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
@@ -173,8 +183,9 @@ pub fn init(cx: &mut App) {
 
 enum ActiveView {
     Thread {
+        thread: Entity<ActiveThread>,
         change_title_editor: Entity<Editor>,
-        thread: WeakEntity<Thread>,
+        message_editor: Entity<MessageEditor>,
         _subscriptions: Vec<gpui::Subscription>,
     },
     TextThread {
@@ -202,8 +213,13 @@ impl ActiveView {
         }
     }
 
-    pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
-        let summary = thread.read(cx).summary().or_default();
+    pub fn thread(
+        active_thread: Entity<ActiveThread>,
+        message_editor: Entity<MessageEditor>,
+        window: &mut Window,
+        cx: &mut Context<AgentPanel>,
+    ) -> Self {
+        let summary = active_thread.read(cx).summary(cx).or_default();
 
         let editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
@@ -212,20 +228,37 @@ impl ActiveView {
         });
 
         let subscriptions = vec![
+            cx.subscribe(&message_editor, |this, _, event, cx| match event {
+                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
+                    cx.notify();
+                }
+                MessageEditorEvent::ScrollThreadToBottom => match &this.active_view {
+                    ActiveView::Thread { thread, .. } => {
+                        thread.update(cx, |thread, cx| {
+                            thread.scroll_to_bottom(cx);
+                        });
+                    }
+                    ActiveView::TextThread { .. }
+                    | ActiveView::History
+                    | ActiveView::Configuration => {}
+                },
+            }),
             window.subscribe(&editor, cx, {
                 {
-                    let thread = thread.clone();
+                    let thread = active_thread.clone();
                     move |editor, event, window, cx| match event {
                         EditorEvent::BufferEdited => {
                             let new_summary = editor.read(cx).text(cx);
 
                             thread.update(cx, |thread, cx| {
-                                thread.set_summary(new_summary, cx);
+                                thread.thread().update(cx, |thread, cx| {
+                                    thread.set_summary(new_summary, cx);
+                                });
                             })
                         }
                         EditorEvent::Blurred => {
                             if editor.read(cx).text(cx).is_empty() {
-                                let summary = thread.read(cx).summary().or_default();
+                                let summary = thread.read(cx).summary(cx).or_default();
 
                                 editor.update(cx, |editor, cx| {
                                     editor.set_text(summary, window, cx);
@@ -236,9 +269,14 @@ impl ActiveView {
                     }
                 }
             }),
-            window.subscribe(&thread, cx, {
+            cx.subscribe(&active_thread, |_, _, event, cx| match &event {
+                ActiveThreadEvent::EditingMessageTokenCountChanged => {
+                    cx.notify();
+                }
+            }),
+            cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, {
                 let editor = editor.clone();
-                move |thread, event, window, cx| match event {
+                move |_, thread, event, window, cx| match event {
                     ThreadEvent::SummaryGenerated => {
                         let summary = thread.read(cx).summary().or_default();
 
@@ -246,6 +284,9 @@ impl ActiveView {
                             editor.set_text(summary, window, cx);
                         })
                     }
+                    ThreadEvent::MessageAdded(_) => {
+                        cx.notify();
+                    }
                     _ => {}
                 }
             }),
@@ -253,7 +294,8 @@ impl ActiveView {
 
         Self::Thread {
             change_title_editor: editor,
-            thread: thread.downgrade(),
+            thread: active_thread,
+            message_editor: message_editor,
             _subscriptions: subscriptions,
         }
     }
@@ -366,9 +408,6 @@ pub struct AgentPanel {
     fs: Arc<dyn Fs>,
     language_registry: Arc<LanguageRegistry>,
     thread_store: Entity<ThreadStore>,
-    thread: Entity<ActiveThread>,
-    message_editor: Entity<MessageEditor>,
-    _active_thread_subscriptions: Vec<Subscription>,
     _default_model_subscription: Subscription,
     context_store: Entity<TextThreadStore>,
     prompt_store: Option<Entity<PromptStore>>,
@@ -513,18 +552,6 @@ impl AgentPanel {
             )
         });
 
-        let message_editor_subscription =
-            cx.subscribe(&message_editor, |this, _, event, cx| match event {
-                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
-                    cx.notify();
-                }
-                MessageEditorEvent::ScrollThreadToBottom => {
-                    this.thread.update(cx, |thread, cx| {
-                        thread.scroll_to_bottom(cx);
-                    });
-                }
-            });
-
         let thread_id = thread.read(cx).id().clone();
         let history_store = cx.new(|cx| {
             HistoryStore::new(
@@ -537,9 +564,22 @@ impl AgentPanel {
 
         cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
 
+        let active_thread = cx.new(|cx| {
+            ActiveThread::new(
+                thread.clone(),
+                thread_store.clone(),
+                context_store.clone(),
+                message_editor_context_store.clone(),
+                language_registry.clone(),
+                workspace.clone(),
+                window,
+                cx,
+            )
+        });
+
         let panel_type = AgentSettings::get_global(cx).default_view;
         let active_view = match panel_type {
-            DefaultView::Thread => ActiveView::thread(thread.clone(), window, cx),
+            DefaultView::Thread => ActiveView::thread(active_thread, message_editor, window, cx),
             DefaultView::TextThread => {
                 let context =
                     context_store.update(cx, |context_store, cx| context_store.create(cx));
@@ -567,33 +607,8 @@ impl AgentPanel {
             }
         };
 
-        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
-            if let ThreadEvent::MessageAdded(_) = &event {
-                // needed to leave empty state
-                cx.notify();
-            }
-        });
-        let active_thread = cx.new(|cx| {
-            ActiveThread::new(
-                thread.clone(),
-                thread_store.clone(),
-                context_store.clone(),
-                message_editor_context_store.clone(),
-                language_registry.clone(),
-                workspace.clone(),
-                window,
-                cx,
-            )
-        });
         AgentDiff::set_active_thread(&workspace, &thread, window, cx);
 
-        let active_thread_subscription =
-            cx.subscribe(&active_thread, |_, _, event, cx| match &event {
-                ActiveThreadEvent::EditingMessageTokenCountChanged => {
-                    cx.notify();
-                }
-            });
-
         let weak_panel = weak_self.clone();
 
         window.defer(cx, move |window, cx| {
@@ -630,13 +645,18 @@ impl AgentPanel {
         let _default_model_subscription = cx.subscribe(
             &LanguageModelRegistry::global(cx),
             |this, _, event: &language_model::Event, cx| match event {
-                language_model::Event::DefaultModelChanged => {
-                    this.thread
-                        .read(cx)
-                        .thread()
-                        .clone()
-                        .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
-                }
+                language_model::Event::DefaultModelChanged => match &this.active_view {
+                    ActiveView::Thread { thread, .. } => {
+                        thread
+                            .read(cx)
+                            .thread()
+                            .clone()
+                            .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
+                    }
+                    ActiveView::TextThread { .. }
+                    | ActiveView::History
+                    | ActiveView::Configuration => {}
+                },
                 _ => {}
             },
         );
@@ -649,13 +669,6 @@ impl AgentPanel {
             fs: fs.clone(),
             language_registry,
             thread_store: thread_store.clone(),
-            thread: active_thread,
-            message_editor,
-            _active_thread_subscriptions: vec![
-                thread_subscription,
-                active_thread_subscription,
-                message_editor_subscription,
-            ],
             _default_model_subscription,
             context_store,
             prompt_store,
@@ -716,24 +729,34 @@ impl AgentPanel {
     }
 
     fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
-        self.thread
-            .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
+        match &self.active_view {
+            ActiveView::Thread { thread, .. } => {
+                thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
+            }
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
+        }
+    }
+
+    fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
+        match &self.active_view {
+            ActiveView::Thread { message_editor, .. } => Some(message_editor),
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
+        }
     }
 
     fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
         // Preserve chat box text when using creating new thread from summary'
-        let preserved_text = action
-            .from_thread_id
-            .is_some()
-            .then(|| self.message_editor.read(cx).get_text(cx).trim().to_string());
+        let preserved_text = if action.from_thread_id.is_some() {
+            self.active_message_editor()
+                .map(|editor| editor.read(cx).get_text(cx).trim().to_string())
+        } else {
+            None
+        };
 
         let thread = self
             .thread_store
             .update(cx, |this, cx| this.create_thread(cx));
 
-        let thread_view = ActiveView::thread(thread.clone(), window, cx);
-        self.set_active_view(thread_view, window, cx);
-
         let context_store = cx.new(|_cx| {
             ContextStore::new(
                 self.project.downgrade(),
@@ -761,14 +784,7 @@ impl AgentPanel {
             .detach_and_log_err(cx);
         }
 
-        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
-            if let ThreadEvent::MessageAdded(_) = &event {
-                // needed to leave empty state
-                cx.notify();
-            }
-        });
-
-        self.thread = cx.new(|cx| {
+        let active_thread = cx.new(|cx| {
             ActiveThread::new(
                 thread.clone(),
                 self.thread_store.clone(),
@@ -780,55 +796,34 @@ impl AgentPanel {
                 cx,
             )
         });
-        AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
 
-        let active_thread_subscription =
-            cx.subscribe(&self.thread, |_, _, event, cx| match &event {
-                ActiveThreadEvent::EditingMessageTokenCountChanged => {
-                    cx.notify();
-                }
-            });
-
-        self.message_editor = cx.new(|cx| {
+        let message_editor = cx.new(|cx| {
             MessageEditor::new(
                 self.fs.clone(),
                 self.workspace.clone(),
                 self.user_store.clone(),
-                context_store,
+                context_store.clone(),
                 self.prompt_store.clone(),
                 self.thread_store.downgrade(),
                 self.context_store.downgrade(),
-                thread,
+                thread.clone(),
                 window,
                 cx,
             )
         });
 
         if let Some(text) = preserved_text {
-            self.message_editor.update(cx, |editor, cx| {
+            message_editor.update(cx, |editor, cx| {
                 editor.set_text(text, window, cx);
             });
         }
 
-        self.message_editor.focus_handle(cx).focus(window);
+        message_editor.focus_handle(cx).focus(window);
 
-        let message_editor_subscription =
-            cx.subscribe(&self.message_editor, |this, _, event, cx| match event {
-                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
-                    cx.notify();
-                }
-                MessageEditorEvent::ScrollThreadToBottom => {
-                    this.thread.update(cx, |thread, cx| {
-                        thread.scroll_to_bottom(cx);
-                    });
-                }
-            });
+        let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
+        self.set_active_view(thread_view, window, cx);
 
-        self._active_thread_subscriptions = vec![
-            thread_subscription,
-            active_thread_subscription,
-            message_editor_subscription,
-        ];
+        AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
     }
 
     fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -980,22 +975,14 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let thread_view = ActiveView::thread(thread.clone(), window, cx);
-        self.set_active_view(thread_view, window, cx);
         let context_store = cx.new(|_cx| {
             ContextStore::new(
                 self.project.downgrade(),
                 Some(self.thread_store.downgrade()),
             )
         });
-        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
-            if let ThreadEvent::MessageAdded(_) = &event {
-                // needed to leave empty state
-                cx.notify();
-            }
-        });
 
-        self.thread = cx.new(|cx| {
+        let active_thread = cx.new(|cx| {
             ActiveThread::new(
                 thread.clone(),
                 self.thread_store.clone(),
@@ -1007,16 +994,7 @@ impl AgentPanel {
                 cx,
             )
         });
-        AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
-
-        let active_thread_subscription =
-            cx.subscribe(&self.thread, |_, _, event, cx| match &event {
-                ActiveThreadEvent::EditingMessageTokenCountChanged => {
-                    cx.notify();
-                }
-            });
-
-        self.message_editor = cx.new(|cx| {
+        let message_editor = cx.new(|cx| {
             MessageEditor::new(
                 self.fs.clone(),
                 self.workspace.clone(),
@@ -1025,30 +1003,16 @@ impl AgentPanel {
                 self.prompt_store.clone(),
                 self.thread_store.downgrade(),
                 self.context_store.downgrade(),
-                thread,
+                thread.clone(),
                 window,
                 cx,
             )
         });
-        self.message_editor.focus_handle(cx).focus(window);
+        message_editor.focus_handle(cx).focus(window);
 
-        let message_editor_subscription =
-            cx.subscribe(&self.message_editor, |this, _, event, cx| match event {
-                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
-                    cx.notify();
-                }
-                MessageEditorEvent::ScrollThreadToBottom => {
-                    this.thread.update(cx, |thread, cx| {
-                        thread.scroll_to_bottom(cx);
-                    });
-                }
-            });
-
-        self._active_thread_subscriptions = vec![
-            thread_subscription,
-            active_thread_subscription,
-            message_editor_subscription,
-        ];
+        let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
+        self.set_active_view(thread_view, window, cx);
+        AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
     }
 
     pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
@@ -1058,18 +1022,14 @@ impl AgentPanel {
                     self.active_view = previous_view;
 
                     match &self.active_view {
-                        ActiveView::Thread { .. } => {
-                            self.message_editor.focus_handle(cx).focus(window);
+                        ActiveView::Thread { message_editor, .. } => {
+                            message_editor.focus_handle(cx).focus(window);
                         }
                         ActiveView::TextThread { context_editor, .. } => {
                             context_editor.focus_handle(cx).focus(window);
                         }
-                        _ => {}
+                        ActiveView::History | ActiveView::Configuration => {}
                     }
-                } else {
-                    self.active_view =
-                        ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
-                    self.message_editor.focus_handle(cx).focus(window);
                 }
                 cx.notify();
             }
@@ -1175,12 +1135,17 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let thread = self.thread.read(cx).thread().clone();
-        self.workspace
-            .update(cx, |workspace, cx| {
-                AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
-            })
-            .log_err();
+        match &self.active_view {
+            ActiveView::Thread { thread, .. } => {
+                let thread = thread.read(cx).thread().clone();
+                self.workspace
+                    .update(cx, |workspace, cx| {
+                        AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
+                    })
+                    .log_err();
+            }
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
+        }
     }
 
     pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -1222,12 +1187,18 @@ impl AgentPanel {
             return;
         };
 
-        let Some(thread) = self.active_thread() else {
-            return;
-        };
-
-        active_thread::open_active_thread_as_markdown(thread, workspace, window, cx)
-            .detach_and_log_err(cx);
+        match &self.active_view {
+            ActiveView::Thread { thread, .. } => {
+                active_thread::open_active_thread_as_markdown(
+                    thread.read(cx).thread().clone(),
+                    workspace,
+                    window,
+                    cx,
+                )
+                .detach_and_log_err(cx);
+            }
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
+        }
     }
 
     fn handle_agent_configuration_event(
@@ -1257,9 +1228,9 @@ impl AgentPanel {
         }
     }
 
-    pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
+    pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
         match &self.active_view {
-            ActiveView::Thread { thread, .. } => thread.upgrade(),
+            ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
             _ => None,
         }
     }
@@ -1273,19 +1244,19 @@ impl AgentPanel {
             .update(cx, |this, cx| this.delete_thread(thread_id, cx))
     }
 
-    pub(crate) fn has_active_thread(&self) -> bool {
-        matches!(self.active_view, ActiveView::Thread { .. })
-    }
-
     fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let thread_state = self.thread.read(cx).thread().read(cx);
+        let ActiveView::Thread { thread, .. } = &self.active_view else {
+            return;
+        };
+
+        let thread_state = thread.read(cx).thread().read(cx);
         if !thread_state.tool_use_limit_reached() {
             return;
         }
 
         let model = thread_state.configured_model().map(|cm| cm.model.clone());
         if let Some(model) = model {
-            self.thread.update(cx, |active_thread, cx| {
+            thread.update(cx, |active_thread, cx| {
                 active_thread.thread().update(cx, |thread, cx| {
                     thread.insert_invisible_continue_message(cx);
                     thread.advance_prompt_id();
@@ -1308,7 +1279,11 @@ impl AgentPanel {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.thread.update(cx, |active_thread, cx| {
+        let ActiveView::Thread { thread, .. } = &self.active_view else {
+            return;
+        };
+
+        thread.update(cx, |active_thread, cx| {
             active_thread.thread().update(cx, |thread, _cx| {
                 let current_mode = thread.completion_mode();
 
@@ -1353,13 +1328,12 @@ impl AgentPanel {
 
         match &self.active_view {
             ActiveView::Thread { thread, .. } => {
-                if let Some(thread) = thread.upgrade() {
-                    if thread.read(cx).is_empty() {
-                        let id = thread.read(cx).id().clone();
-                        self.history_store.update(cx, |store, cx| {
-                            store.remove_recently_opened_thread(id, cx);
-                        });
-                    }
+                let thread = thread.read(cx);
+                if thread.is_empty() {
+                    let id = thread.thread().read(cx).id().clone();
+                    self.history_store.update(cx, |store, cx| {
+                        store.remove_recently_opened_thread(id, cx);
+                    });
                 }
             }
             _ => {}
@@ -1367,10 +1341,8 @@ impl AgentPanel {
 
         match &new_view {
             ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
-                if let Some(thread) = thread.upgrade() {
-                    let id = thread.read(cx).id().clone();
-                    store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
-                }
+                let id = thread.read(cx).thread().read(cx).id().clone();
+                store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
             }),
             ActiveView::TextThread { context_editor, .. } => {
                 self.history_store.update(cx, |store, cx| {
@@ -1464,7 +1436,7 @@ impl AgentPanel {
 impl Focusable for AgentPanel {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         match &self.active_view {
-            ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
+            ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
             ActiveView::History => self.history.focus_handle(cx),
             ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
             ActiveView::Configuration => {
@@ -1573,14 +1545,17 @@ impl AgentPanel {
 
         let content = match &self.active_view {
             ActiveView::Thread {
+                thread: active_thread,
                 change_title_editor,
                 ..
             } => {
-                let active_thread = self.thread.read(cx);
-                let state = if active_thread.is_empty() {
-                    &ThreadSummary::Pending
-                } else {
-                    active_thread.summary(cx)
+                let state = {
+                    let active_thread = active_thread.read(cx);
+                    if active_thread.is_empty() {
+                        &ThreadSummary::Pending
+                    } else {
+                        active_thread.summary(cx)
+                    }
                 };
 
                 match state {
@@ -1600,7 +1575,7 @@ impl AgentPanel {
                         .child(
                             ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
                                 .on_click({
-                                    let active_thread = self.thread.clone();
+                                    let active_thread = active_thread.clone();
                                     move |_, _window, cx| {
                                         active_thread.update(cx, |thread, cx| {
                                             thread.regenerate_summary(cx);
@@ -1681,22 +1656,11 @@ impl AgentPanel {
     }
 
     fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let active_thread = self.thread.read(cx);
         let user_store = self.user_store.read(cx);
-        let thread = active_thread.thread().read(cx);
-        let thread_id = thread.id().clone();
-        let is_empty = active_thread.is_empty();
-        let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
         let usage = user_store.model_request_usage();
 
         let account_url = zed_urls::account_url(cx);
 
-        let show_token_count = match &self.active_view {
-            ActiveView::Thread { .. } => !is_empty || !editor_empty,
-            ActiveView::TextThread { .. } => true,
-            _ => false,
-        };
-
         let focus_handle = self.focus_handle(cx);
 
         let go_back_button = div().child(
@@ -1761,6 +1725,11 @@ impl AgentPanel {
             "Zoom In"
         };
 
+        let active_thread = match &self.active_view {
+            ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
+        };
+
         let agent_extra_menu = PopoverMenu::new("agent-options-menu")
             .trigger_with_tooltip(
                 IconButton::new("agent-options-menu", IconName::Ellipsis)
@@ -1781,17 +1750,23 @@ impl AgentPanel {
             .anchor(Corner::TopRight)
             .with_handle(self.assistant_dropdown_menu_handle.clone())
             .menu(move |window, cx| {
-                Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
+                let active_thread = active_thread.clone();
+                Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
                     menu = menu
                         .action("New Thread", NewThread::default().boxed_clone())
                         .action("New Text Thread", NewTextThread.boxed_clone())
-                        .when(!is_empty, |menu| {
-                            menu.action(
-                                "New From Summary",
-                                Box::new(NewThread {
-                                    from_thread_id: Some(thread_id.clone()),
-                                }),
-                            )
+                        .when_some(active_thread, |this, active_thread| {
+                            let thread = active_thread.read(cx);
+                            if !thread.is_empty() {
+                                this.action(
+                                    "New From Summary",
+                                    Box::new(NewThread {
+                                        from_thread_id: Some(thread.id().clone()),
+                                    }),
+                                )
+                            } else {
+                                this
+                            }
                         })
                         .separator();
 
@@ -1878,9 +1853,7 @@ impl AgentPanel {
                 h_flex()
                     .h_full()
                     .gap_2()
-                    .when(show_token_count, |parent| {
-                        parent.children(self.render_token_count(&thread, cx))
-                    })
+                    .children(self.render_token_count(cx))
                     .child(
                         h_flex()
                             .h_full()
@@ -1913,26 +1886,41 @@ impl AgentPanel {
             )
     }
 
-    fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
-        let is_generating = thread.is_generating();
-        let message_editor = self.message_editor.read(cx);
+    fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
+        let (active_thread, message_editor) = match &self.active_view {
+            ActiveView::Thread {
+                thread,
+                message_editor,
+                ..
+            } => (thread.read(cx), message_editor.read(cx)),
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
+                return None;
+            }
+        };
+
+        let editor_empty = message_editor.is_editor_fully_empty(cx);
 
+        if active_thread.is_empty() && editor_empty {
+            return None;
+        }
+
+        let thread = active_thread.thread().read(cx);
+        let is_generating = thread.is_generating();
         let conversation_token_usage = thread.total_token_usage()?;
 
-        let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
-            self.thread.read(cx).editing_message_id()
-        {
-            let combined = thread
-                .token_usage_up_to_message(editing_message_id)
-                .add(unsent_tokens);
+        let (total_token_usage, is_estimating) =
+            if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() {
+                let combined = thread
+                    .token_usage_up_to_message(editing_message_id)
+                    .add(unsent_tokens);
 
-            (combined, unsent_tokens > 0)
-        } else {
-            let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
-            let combined = conversation_token_usage.add(unsent_tokens);
+                (combined, unsent_tokens > 0)
+            } else {
+                let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
+                let combined = conversation_token_usage.add(unsent_tokens);
 
-            (combined, unsent_tokens > 0)
-        };
+                (combined, unsent_tokens > 0)
+            };
 
         let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
 
@@ -2030,24 +2018,27 @@ impl AgentPanel {
     }
 
     fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
-        if !matches!(self.active_view, ActiveView::Thread { .. }) {
-            return false;
-        }
+        match &self.active_view {
+            ActiveView::Thread { thread, .. } => {
+                let is_using_zed_provider = thread
+                    .read(cx)
+                    .thread()
+                    .read(cx)
+                    .configured_model()
+                    .map_or(false, |model| {
+                        model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
+                    });
 
-        if self.hide_upsell || Upsell::dismissed() {
-            return false;
-        }
+                if !is_using_zed_provider {
+                    return false;
+                }
+            }
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
+                return false;
+            }
+        };
 
-        let is_using_zed_provider = self
-            .thread
-            .read(cx)
-            .thread()
-            .read(cx)
-            .configured_model()
-            .map_or(false, |model| {
-                model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
-            });
-        if !is_using_zed_provider {
+        if self.hide_upsell || Upsell::dismissed() {
             return false;
         }
 
@@ -2352,20 +2343,6 @@ impl AgentPanel {
         )
     }
 
-    fn render_active_thread_or_empty_state(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> AnyElement {
-        if self.thread.read(cx).is_empty() {
-            return self
-                .render_thread_empty_state(window, cx)
-                .into_any_element();
-        }
-
-        self.thread.clone().into_any_element()
-    }
-
     fn render_thread_empty_state(
         &self,
         window: &mut Window,
@@ -2638,23 +2615,21 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<AnyElement> {
-        let tool_use_limit_reached = self
-            .thread
-            .read(cx)
-            .thread()
-            .read(cx)
-            .tool_use_limit_reached();
+        let active_thread = match &self.active_view {
+            ActiveView::Thread { thread, .. } => thread,
+            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
+                return None;
+            }
+        };
+
+        let thread = active_thread.read(cx).thread().read(cx);
+
+        let tool_use_limit_reached = thread.tool_use_limit_reached();
         if !tool_use_limit_reached {
             return None;
         }
 
-        let model = self
-            .thread
-            .read(cx)
-            .thread()
-            .read(cx)
-            .configured_model()?
-            .model;
+        let model = thread.configured_model()?.model;
 
         let focus_handle = self.focus_handle(cx);
 
@@ -2698,14 +2673,17 @@ impl AgentPanel {
                                     .map(|kb| kb.size(rems_from_px(10.))),
                                 )
                                 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
-                                .on_click(cx.listener(|this, _, window, cx| {
-                                    this.thread.update(cx, |active_thread, cx| {
-                                        active_thread.thread().update(cx, |thread, _cx| {
-                                            thread.set_completion_mode(CompletionMode::Burn);
+                                .on_click({
+                                    let active_thread = active_thread.clone();
+                                    cx.listener(move |this, _, window, cx| {
+                                        active_thread.update(cx, |active_thread, cx| {
+                                            active_thread.thread().update(cx, |thread, _cx| {
+                                                thread.set_completion_mode(CompletionMode::Burn);
+                                            });
                                         });
-                                    });
-                                    this.continue_conversation(window, cx);
-                                })),
+                                        this.continue_conversation(window, cx);
+                                    })
+                                }),
                         )
                     }),
             );
@@ -2713,33 +2691,11 @@ impl AgentPanel {
         Some(div().px_2().pb_2().child(banner).into_any_element())
     }
 
-    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
-        let last_error = self.thread.read(cx).last_error()?;
-
-        Some(
-            div()
-                .absolute()
-                .right_3()
-                .bottom_12()
-                .max_w_96()
-                .py_2()
-                .px_3()
-                .elevation_2(cx)
-                .occlude()
-                .child(match last_error {
-                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
-                    ThreadError::ModelRequestLimitReached { plan } => {
-                        self.render_model_request_limit_reached_error(plan, cx)
-                    }
-                    ThreadError::Message { header, message } => {
-                        self.render_error_message(header, message, cx)
-                    }
-                })
-                .into_any(),
-        )
-    }
-
-    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
+    fn render_payment_required_error(
+        &self,
+        thread: &Entity<ActiveThread>,
+        cx: &mut Context<Self>,
+    ) -> AnyElement {
         const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
 
         v_flex()
@@ -2764,25 +2720,27 @@ impl AgentPanel {
                     .mt_1()
                     .gap_1()
                     .child(self.create_copy_button(ERROR_MESSAGE))
-                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
-                        |this, _, _, cx| {
-                            this.thread.update(cx, |this, _cx| {
+                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
+                        let thread = thread.clone();
+                        move |_, _, _, cx| {
+                            thread.update(cx, |this, _cx| {
                                 this.clear_last_error();
                             });
 
                             cx.open_url(&zed_urls::account_url(cx));
                             cx.notify();
-                        },
-                    )))
-                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
-                        |this, _, _, cx| {
-                            this.thread.update(cx, |this, _cx| {
+                        }
+                    })))
+                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
+                        let thread = thread.clone();
+                        move |_, _, _, cx| {
+                            thread.update(cx, |this, _cx| {
                                 this.clear_last_error();
                             });
 
                             cx.notify();
-                        },
-                    ))),
+                        }
+                    }))),
             )
             .into_any()
     }

crates/agent_ui/src/context_picker.rs 🔗

@@ -661,7 +661,7 @@ fn recent_context_picker_entries(
 
     let active_thread_id = workspace
         .panel::<AgentPanel>(cx)
-        .and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
+        .and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
 
     if let Some((thread_store, text_thread_store)) = thread_store
         .and_then(|store| store.upgrade())

crates/agent_ui/src/context_strip.rs 🔗

@@ -161,7 +161,7 @@ impl ContextStrip {
         let workspace = self.workspace.upgrade()?;
         let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
 
-        if let Some(active_thread) = panel.active_thread() {
+        if let Some(active_thread) = panel.active_thread(cx) {
             let weak_active_thread = active_thread.downgrade();
 
             let active_thread = active_thread.read(cx);