agent_ui: No global thread history (#51362)

Bennet Bo Fenner created

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_connection_store.rs |  73 +++-
crates/agent_ui/src/agent_panel.rs            | 293 +++++++++++++-------
crates/agent_ui/src/connection_view.rs        | 110 ++++---
crates/agent_ui/src/inline_assistant.rs       |  31 +
crates/agent_ui/src/text_thread_history.rs    |   4 
crates/agent_ui/src/thread_history.rs         |  11 
crates/agent_ui/src/thread_history_view.rs    |   4 
7 files changed, 332 insertions(+), 194 deletions(-)

Detailed changes

crates/agent_ui/src/agent_connection_store.rs 🔗

@@ -9,42 +9,51 @@ use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Subscription
 use project::{AgentServerStore, AgentServersUpdated, Project};
 use watch::Receiver;
 
-use crate::ExternalAgent;
+use crate::{ExternalAgent, ThreadHistory};
 use project::ExternalAgentServerName;
 
-pub enum ConnectionEntry {
+pub enum AgentConnectionEntry {
     Connecting {
-        connect_task: Shared<Task<Result<Rc<dyn AgentConnection>, LoadError>>>,
-    },
-    Connected {
-        connection: Rc<dyn AgentConnection>,
+        connect_task: Shared<Task<Result<AgentConnectedState, LoadError>>>,
     },
+    Connected(AgentConnectedState),
     Error {
         error: LoadError,
     },
 }
 
-impl ConnectionEntry {
-    pub fn wait_for_connection(&self) -> Shared<Task<Result<Rc<dyn AgentConnection>, LoadError>>> {
+#[derive(Clone)]
+pub struct AgentConnectedState {
+    pub connection: Rc<dyn AgentConnection>,
+    pub history: Entity<ThreadHistory>,
+}
+
+impl AgentConnectionEntry {
+    pub fn wait_for_connection(&self) -> Shared<Task<Result<AgentConnectedState, LoadError>>> {
         match self {
-            ConnectionEntry::Connecting { connect_task } => connect_task.clone(),
-            ConnectionEntry::Connected { connection } => {
-                Task::ready(Ok(connection.clone())).shared()
-            }
-            ConnectionEntry::Error { error } => Task::ready(Err(error.clone())).shared(),
+            AgentConnectionEntry::Connecting { connect_task } => connect_task.clone(),
+            AgentConnectionEntry::Connected(state) => Task::ready(Ok(state.clone())).shared(),
+            AgentConnectionEntry::Error { error } => Task::ready(Err(error.clone())).shared(),
+        }
+    }
+
+    pub fn history(&self) -> Option<&Entity<ThreadHistory>> {
+        match self {
+            AgentConnectionEntry::Connected(state) => Some(&state.history),
+            _ => None,
         }
     }
 }
 
-pub enum ConnectionEntryEvent {
+pub enum AgentConnectionEntryEvent {
     NewVersionAvailable(SharedString),
 }
 
-impl EventEmitter<ConnectionEntryEvent> for ConnectionEntry {}
+impl EventEmitter<AgentConnectionEntryEvent> for AgentConnectionEntry {}
 
 pub struct AgentConnectionStore {
     project: Entity<Project>,
-    entries: HashMap<ExternalAgent, Entity<ConnectionEntry>>,
+    entries: HashMap<ExternalAgent, Entity<AgentConnectionEntry>>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -59,17 +68,21 @@ impl AgentConnectionStore {
         }
     }
 
+    pub fn entry(&self, key: &ExternalAgent) -> Option<&Entity<AgentConnectionEntry>> {
+        self.entries.get(key)
+    }
+
     pub fn request_connection(
         &mut self,
         key: ExternalAgent,
         server: Rc<dyn AgentServer>,
         cx: &mut Context<Self>,
-    ) -> Entity<ConnectionEntry> {
+    ) -> Entity<AgentConnectionEntry> {
         self.entries.get(&key).cloned().unwrap_or_else(|| {
             let (mut new_version_rx, connect_task) = self.start_connection(server.clone(), cx);
             let connect_task = connect_task.shared();
 
-            let entry = cx.new(|_cx| ConnectionEntry::Connecting {
+            let entry = cx.new(|_cx| AgentConnectionEntry::Connecting {
                 connect_task: connect_task.clone(),
             });
 
@@ -79,18 +92,18 @@ impl AgentConnectionStore {
                 let key = key.clone();
                 let entry = entry.clone();
                 async move |this, cx| match connect_task.await {
-                    Ok(connection) => {
+                    Ok(connected_state) => {
                         entry.update(cx, |entry, cx| {
-                            if let ConnectionEntry::Connecting { .. } = entry {
-                                *entry = ConnectionEntry::Connected { connection };
+                            if let AgentConnectionEntry::Connecting { .. } = entry {
+                                *entry = AgentConnectionEntry::Connected(connected_state);
                                 cx.notify();
                             }
                         });
                     }
                     Err(error) => {
                         entry.update(cx, |entry, cx| {
-                            if let ConnectionEntry::Connecting { .. } = entry {
-                                *entry = ConnectionEntry::Error { error };
+                            if let AgentConnectionEntry::Connecting { .. } = entry {
+                                *entry = AgentConnectionEntry::Error { error };
                                 cx.notify();
                             }
                         });
@@ -106,7 +119,7 @@ impl AgentConnectionStore {
                     while let Ok(version) = new_version_rx.recv().await {
                         if let Some(version) = version {
                             entry.update(cx, |_entry, cx| {
-                                cx.emit(ConnectionEntryEvent::NewVersionAvailable(
+                                cx.emit(AgentConnectionEntryEvent::NewVersionAvailable(
                                     version.clone().into(),
                                 ));
                             });
@@ -143,7 +156,7 @@ impl AgentConnectionStore {
         cx: &mut Context<Self>,
     ) -> (
         Receiver<Option<String>>,
-        Task<Result<Rc<dyn AgentConnection>, LoadError>>,
+        Task<Result<AgentConnectedState, LoadError>>,
     ) {
         let (new_version_tx, new_version_rx) = watch::channel::<Option<String>>(None);
 
@@ -151,8 +164,14 @@ impl AgentConnectionStore {
         let delegate = AgentServerDelegate::new(agent_server_store, Some(new_version_tx));
 
         let connect_task = server.connect(delegate, cx);
-        let connect_task = cx.spawn(async move |_this, _cx| match connect_task.await {
-            Ok(connection) => Ok(connection),
+        let connect_task = cx.spawn(async move |_this, cx| match connect_task.await {
+            Ok(connection) => cx.update(|cx| {
+                let history = cx.new(|cx| ThreadHistory::new(connection.session_list(cx), cx));
+                Ok(AgentConnectedState {
+                    connection,
+                    history,
+                })
+            }),
             Err(err) => match err.downcast::<LoadError>() {
                 Ok(load_error) => Err(load_error),
                 Err(err) => Err(LoadError::Other(SharedString::from(err.to_string()))),

crates/agent_ui/src/agent_panel.rs 🔗

@@ -29,8 +29,6 @@ use zed_actions::agent::{
     ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff,
 };
 
-use crate::ManageProfiles;
-use crate::agent_connection_store::AgentConnectionStore;
 use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
 use crate::{
     AddContextServer, AgentDiffPane, ConnectionView, CopyThreadToClipboard, Follow,
@@ -48,12 +46,14 @@ use crate::{
     NewNativeAgentThreadFromSummary,
 };
 use crate::{
-    ExpandMessageEditor, ThreadHistory, ThreadHistoryView, ThreadHistoryViewEvent,
+    ExpandMessageEditor, ThreadHistoryView,
     text_thread_history::{TextThreadHistory, TextThreadHistoryEvent},
 };
+use crate::{ManageProfiles, ThreadHistoryViewEvent};
+use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
 use agent_settings::AgentSettings;
 use ai_onboarding::AgentPanelOnboarding;
-use anyhow::{Result, anyhow};
+use anyhow::{Context as _, Result, anyhow};
 use assistant_slash_command::SlashCommandWorkingSet;
 use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
 use client::UserStore;
@@ -621,9 +621,9 @@ fn build_conflicted_files_resolution_prompt(
     content
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-enum HistoryKind {
-    AgentThreads,
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum History {
+    AgentThreads { view: Entity<ThreadHistoryView> },
     TextThreads,
 }
 
@@ -639,7 +639,7 @@ enum ActiveView {
         _subscriptions: Vec<gpui::Subscription>,
     },
     History {
-        kind: HistoryKind,
+        history: History,
     },
     Configuration,
 }
@@ -870,8 +870,6 @@ pub struct AgentPanel {
     project: Entity<Project>,
     fs: Arc<dyn Fs>,
     language_registry: Arc<LanguageRegistry>,
-    acp_history: Entity<ThreadHistory>,
-    acp_history_view: Entity<ThreadHistoryView>,
     text_thread_history: Entity<TextThreadHistory>,
     thread_store: Entity<ThreadStore>,
     text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
@@ -1081,26 +1079,9 @@ impl AgentPanel {
             cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 
         let thread_store = ThreadStore::global(cx);
-        let acp_history = cx.new(|cx| ThreadHistory::new(None, cx));
-        let acp_history_view = cx.new(|cx| ThreadHistoryView::new(acp_history.clone(), window, cx));
         let text_thread_history =
             cx.new(|cx| TextThreadHistory::new(text_thread_store.clone(), window, cx));
-        cx.subscribe_in(
-            &acp_history_view,
-            window,
-            |this, _, event, window, cx| match event {
-                ThreadHistoryViewEvent::Open(thread) => {
-                    this.load_agent_thread(
-                        thread.session_id.clone(),
-                        thread.cwd.clone(),
-                        thread.title.clone(),
-                        window,
-                        cx,
-                    );
-                }
-            },
-        )
-        .detach();
+
         cx.subscribe_in(
             &text_thread_history,
             window,
@@ -1120,15 +1101,18 @@ impl AgentPanel {
         window.defer(cx, move |window, cx| {
             let panel = weak_panel.clone();
             let agent_navigation_menu =
-                ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
+                ContextMenu::build_persistent(window, cx, move |mut menu, window, cx| {
                     if let Some(panel) = panel.upgrade() {
-                        if let Some(kind) = panel.read(cx).history_kind_for_selected_agent(cx) {
-                            menu =
-                                Self::populate_recently_updated_menu_section(menu, panel, kind, cx);
-                            let view_all_label = match kind {
-                                HistoryKind::AgentThreads => "View All",
-                                HistoryKind::TextThreads => "View All Text Threads",
+                        if let Some(history) = panel
+                            .update(cx, |panel, cx| panel.history_for_selected_agent(window, cx))
+                        {
+                            let view_all_label = match history {
+                                History::AgentThreads { .. } => "View All",
+                                History::TextThreads => "View All Text Threads",
                             };
+                            menu = Self::populate_recently_updated_menu_section(
+                                menu, panel, history, cx,
+                            );
                             menu = menu.action(view_all_label, Box::new(OpenHistory));
                         }
                     }
@@ -1222,8 +1206,6 @@ impl AgentPanel {
             zoomed: false,
             pending_serialization: None,
             onboarding,
-            acp_history,
-            acp_history_view,
             text_thread_history,
             thread_store,
             selected_agent: AgentType::default(),
@@ -1288,8 +1270,8 @@ impl AgentPanel {
         &self.thread_store
     }
 
-    pub fn history(&self) -> &Entity<ThreadHistory> {
-        &self.acp_history
+    pub fn connection_store(&self) -> &Entity<AgentConnectionStore> {
+        &self.connection_store
     }
 
     pub fn open_thread(
@@ -1353,27 +1335,41 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(thread) = self
-            .acp_history
-            .read(cx)
-            .session_for_id(&action.from_session_id)
-        else {
-            return;
-        };
+        let agent = ExternalAgent::NativeAgent;
 
-        self.external_thread(
-            Some(ExternalAgent::NativeAgent),
-            None,
-            None,
-            None,
-            Some(AgentInitialContent::ThreadSummary {
-                session_id: thread.session_id,
-                title: thread.title,
-            }),
-            true,
-            window,
-            cx,
-        );
+        let server = agent.server(self.fs.clone(), self.thread_store.clone());
+        let session_id = action.from_session_id.clone();
+
+        let entry = self.connection_store.update(cx, |store, cx| {
+            store.request_connection(agent.clone(), server, cx)
+        });
+        let connect_task = entry.read(cx).wait_for_connection();
+
+        cx.spawn_in(window, async move |this, cx| {
+            let history = connect_task.await?.history;
+            this.update_in(cx, |this, window, cx| {
+                let thread = history
+                    .read(cx)
+                    .session_for_id(&session_id)
+                    .context("Session not found")?;
+
+                this.external_thread(
+                    Some(agent),
+                    None,
+                    None,
+                    None,
+                    Some(AgentInitialContent::ThreadSummary {
+                        session_id: thread.session_id,
+                        title: thread.title,
+                    }),
+                    true,
+                    window,
+                    cx,
+                );
+                anyhow::Ok(())
+            })
+        })
+        .detach_and_log_err(cx);
     }
 
     fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -1554,13 +1550,52 @@ impl AgentPanel {
         })
     }
 
-    fn history_kind_for_selected_agent(&self, cx: &App) -> Option<HistoryKind> {
-        match self.selected_agent {
-            AgentType::NativeAgent => Some(HistoryKind::AgentThreads),
-            AgentType::TextThread => Some(HistoryKind::TextThreads),
-            AgentType::Custom { .. } => {
-                if self.acp_history.read(cx).has_session_list() {
-                    Some(HistoryKind::AgentThreads)
+    fn has_history_for_selected_agent(&self, cx: &App) -> bool {
+        match &self.selected_agent {
+            AgentType::TextThread | AgentType::NativeAgent => true,
+            AgentType::Custom { name } => {
+                let agent = ExternalAgent::Custom { name: name.clone() };
+                self.connection_store
+                    .read(cx)
+                    .entry(&agent)
+                    .map_or(false, |entry| entry.read(cx).history().is_some())
+            }
+        }
+    }
+
+    fn history_for_selected_agent(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<History> {
+        match &self.selected_agent {
+            AgentType::TextThread => Some(History::TextThreads),
+            AgentType::NativeAgent => {
+                let history = self
+                    .connection_store
+                    .read(cx)
+                    .entry(&ExternalAgent::NativeAgent)?
+                    .read(cx)
+                    .history()?
+                    .clone();
+
+                Some(History::AgentThreads {
+                    view: self.create_thread_history_view(history, window, cx),
+                })
+            }
+            AgentType::Custom { name } => {
+                let agent = ExternalAgent::Custom { name: name.clone() };
+                let history = self
+                    .connection_store
+                    .read(cx)
+                    .entry(&agent)?
+                    .read(cx)
+                    .history()?
+                    .clone();
+                if history.read(cx).has_session_list() {
+                    Some(History::AgentThreads {
+                        view: self.create_thread_history_view(history, window, cx),
+                    })
                 } else {
                     None
                 }
@@ -1568,13 +1603,38 @@ impl AgentPanel {
         }
     }
 
+    fn create_thread_history_view(
+        &self,
+        history: Entity<ThreadHistory>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Entity<ThreadHistoryView> {
+        let view = cx.new(|cx| ThreadHistoryView::new(history.clone(), window, cx));
+        cx.subscribe_in(&view, window, |this, _, event, window, cx| match event {
+            ThreadHistoryViewEvent::Open(thread) => {
+                this.load_agent_thread(
+                    thread.session_id.clone(),
+                    thread.cwd.clone(),
+                    thread.title.clone(),
+                    window,
+                    cx,
+                );
+            }
+        })
+        .detach();
+        view
+    }
+
     fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let Some(kind) = self.history_kind_for_selected_agent(cx) else {
+        let Some(history) = self.history_for_selected_agent(window, cx) else {
             return;
         };
 
-        if let ActiveView::History { kind: active_kind } = self.active_view {
-            if active_kind == kind {
+        if let ActiveView::History {
+            history: active_history,
+        } = &self.active_view
+        {
+            if active_history == &history {
                 if let Some(previous_view) = self.previous_view.take() {
                     self.set_active_view(previous_view, true, window, cx);
                 }
@@ -1582,7 +1642,7 @@ impl AgentPanel {
             }
         }
 
-        self.set_active_view(ActiveView::History { kind }, true, window, cx);
+        self.set_active_view(ActiveView::History { history }, true, window, cx);
         cx.notify();
     }
 
@@ -1655,7 +1715,7 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if self.history_kind_for_selected_agent(cx).is_none() {
+        if !self.has_history_for_selected_agent(cx) {
             return;
         }
         self.agent_navigation_menu_handle.toggle(window, cx);
@@ -2096,7 +2156,7 @@ impl AgentPanel {
         let was_in_agent_history = matches!(
             self.active_view,
             ActiveView::History {
-                kind: HistoryKind::AgentThreads
+                history: History::AgentThreads { .. }
             }
         );
         let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
@@ -2154,16 +2214,13 @@ impl AgentPanel {
             }
         };
 
-        let is_in_agent_history = matches!(
-            self.active_view,
-            ActiveView::History {
-                kind: HistoryKind::AgentThreads
+        if let ActiveView::History { history } = &self.active_view {
+            if !was_in_agent_history && let History::AgentThreads { view } = history {
+                view.update(cx, |view, cx| {
+                    view.history()
+                        .update(cx, |history, cx| history.refresh_full_history(cx))
+                });
             }
-        );
-
-        if !was_in_agent_history && is_in_agent_history {
-            self.acp_history
-                .update(cx, |history, cx| history.refresh_full_history(cx));
         }
 
         if focus {
@@ -2175,14 +2232,14 @@ impl AgentPanel {
     fn populate_recently_updated_menu_section(
         mut menu: ContextMenu,
         panel: Entity<Self>,
-        kind: HistoryKind,
+        history: History,
         cx: &mut Context<ContextMenu>,
     ) -> ContextMenu {
-        match kind {
-            HistoryKind::AgentThreads => {
-                let entries = panel
+        match history {
+            History::AgentThreads { view } => {
+                let entries = view
                     .read(cx)
-                    .acp_history
+                    .history()
                     .read(cx)
                     .sessions()
                     .iter()
@@ -2224,7 +2281,7 @@ impl AgentPanel {
                     });
                 }
             }
-            HistoryKind::TextThreads => {
+            History::TextThreads => {
                 let entries = panel
                     .read(cx)
                     .text_thread_store
@@ -2518,7 +2575,6 @@ impl AgentPanel {
                 project,
                 thread_store,
                 self.prompt_store.clone(),
-                self.acp_history.clone(),
                 window,
                 cx,
             )
@@ -3056,9 +3112,9 @@ impl Focusable for AgentPanel {
         match &self.active_view {
             ActiveView::Uninitialized => self.focus_handle.clone(),
             ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx),
-            ActiveView::History { kind } => match kind {
-                HistoryKind::AgentThreads => self.acp_history_view.focus_handle(cx),
-                HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
+            ActiveView::History { history: kind } => match kind {
+                History::AgentThreads { view } => view.read(cx).focus_handle(cx),
+                History::TextThreads => self.text_thread_history.focus_handle(cx),
             },
             ActiveView::TextThread {
                 text_thread_editor, ..
@@ -3292,10 +3348,10 @@ impl AgentPanel {
                         .into_any_element(),
                 }
             }
-            ActiveView::History { kind } => {
+            ActiveView::History { history: kind } => {
                 let title = match kind {
-                    HistoryKind::AgentThreads => "History",
-                    HistoryKind::TextThreads => "Text Thread History",
+                    History::AgentThreads { .. } => "History",
+                    History::TextThreads => "Text Thread History",
                 };
                 Label::new(title).truncate().into_any_element()
             }
@@ -4122,7 +4178,7 @@ impl AgentPanel {
             selected_agent.into_any_element()
         };
 
-        let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
+        let show_history_menu = self.has_history_for_selected_agent(cx);
         let has_v2_flag = cx.has_flag::<AgentV2FeatureFlag>();
         let is_empty_state = !self.active_thread_has_messages(cx);
 
@@ -4402,6 +4458,14 @@ impl AgentPanel {
             return false;
         }
 
+        let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
+            .visible_providers()
+            .iter()
+            .any(|provider| {
+                provider.is_authenticated(cx)
+                    && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
+            });
+
         match &self.active_view {
             ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
                 false
@@ -4411,17 +4475,15 @@ impl AgentPanel {
             {
                 false
             }
-            _ => {
-                let history_is_empty = self.acp_history.read(cx).is_empty();
-
-                let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
-                    .visible_providers()
-                    .iter()
-                    .any(|provider| {
-                        provider.is_authenticated(cx)
-                            && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
-                    });
-
+            ActiveView::AgentThread { server_view } => {
+                let history_is_empty = server_view
+                    .read(cx)
+                    .history()
+                    .is_none_or(|h| h.read(cx).is_empty());
+                history_is_empty || !has_configured_non_zed_providers
+            }
+            ActiveView::TextThread { .. } => {
+                let history_is_empty = self.text_thread_history.read(cx).is_empty();
                 history_is_empty || !has_configured_non_zed_providers
             }
         }
@@ -4803,9 +4865,9 @@ impl Render for AgentPanel {
                     ActiveView::AgentThread { server_view, .. } => parent
                         .child(server_view.clone())
                         .child(self.render_drag_target(cx)),
-                    ActiveView::History { kind } => match kind {
-                        HistoryKind::AgentThreads => parent.child(self.acp_history_view.clone()),
-                        HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
+                    ActiveView::History { history: kind } => match kind {
+                        History::AgentThreads { view } => parent.child(view.clone()),
+                        History::TextThreads => parent.child(self.text_thread_history.clone()),
                     },
                     ActiveView::TextThread {
                         text_thread_editor,
@@ -4910,17 +4972,26 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
             let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
                 return;
             };
+            let Some(history) = panel
+                .read(cx)
+                .connection_store()
+                .read(cx)
+                .entry(&crate::ExternalAgent::NativeAgent)
+                .and_then(|s| s.read(cx).history())
+            else {
+                log::error!("No connection entry found for native agent");
+                return;
+            };
             let project = workspace.read(cx).project().downgrade();
             let panel = panel.read(cx);
             let thread_store = panel.thread_store().clone();
-            let history = panel.history().downgrade();
             assistant.assist(
                 prompt_editor,
                 self.workspace.clone(),
                 project,
                 thread_store,
                 None,
-                history,
+                history.downgrade(),
                 initial_prompt,
                 window,
                 cx,

crates/agent_ui/src/connection_view.rs 🔗

@@ -67,7 +67,9 @@ use super::entry_view_state::EntryViewState;
 use super::thread_history::ThreadHistory;
 use crate::ModeSelector;
 use crate::ModelSelectorPopover;
-use crate::agent_connection_store::{AgentConnectionStore, ConnectionEntryEvent};
+use crate::agent_connection_store::{
+    AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore,
+};
 use crate::agent_diff::AgentDiff;
 use crate::entry_view_state::{EntryViewEvent, ViewEvent};
 use crate::message_editor::{MessageEditor, MessageEditorEvent};
@@ -314,7 +316,6 @@ pub struct ConnectionView {
     thread_store: Option<Entity<ThreadStore>>,
     prompt_store: Option<Entity<PromptStore>>,
     server_state: ServerState,
-    history: Entity<ThreadHistory>,
     focus_handle: FocusHandle,
     notifications: Vec<WindowHandle<AgentNotification>>,
     notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
@@ -418,6 +419,7 @@ pub struct ConnectedServerState {
     active_id: Option<acp::SessionId>,
     threads: HashMap<acp::SessionId, Entity<ThreadView>>,
     connection: Rc<dyn AgentConnection>,
+    history: Entity<ThreadHistory>,
     conversation: Entity<Conversation>,
     _connection_entry_subscription: Subscription,
 }
@@ -484,7 +486,6 @@ impl ConnectionView {
         project: Entity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
         prompt_store: Option<Entity<PromptStore>>,
-        history: Entity<ThreadHistory>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -537,7 +538,6 @@ impl ConnectionView {
             notifications: Vec::new(),
             notification_subscriptions: HashMap::default(),
             auth_task: None,
-            history,
             _subscriptions: subscriptions,
             focus_handle: cx.focus_handle(),
         }
@@ -660,7 +660,7 @@ impl ConnectionView {
 
         let connection_entry_subscription =
             cx.subscribe(&connection_entry, |this, _entry, event, cx| match event {
-                ConnectionEntryEvent::NewVersionAvailable(version) => {
+                AgentConnectionEntryEvent::NewVersionAvailable(version) => {
                     if let Some(thread) = this.active_thread() {
                         thread.update(cx, |thread, cx| {
                             thread.new_server_version_available = Some(version.clone());
@@ -674,8 +674,11 @@ impl ConnectionView {
 
         let load_session_id = resume_session_id.clone();
         let load_task = cx.spawn_in(window, async move |this, cx| {
-            let connection = match connect_result.await {
-                Ok(connection) => connection,
+            let (connection, history) = match connect_result.await {
+                Ok(AgentConnectedState {
+                    connection,
+                    history,
+                }) => (connection, history),
                 Err(err) => {
                     this.update_in(cx, |this, window, cx| {
                         this.handle_load_error(load_session_id.clone(), err, window, cx);
@@ -764,6 +767,7 @@ impl ConnectionView {
                             conversation.clone(),
                             resumed_without_history,
                             initial_content,
+                            history.clone(),
                             window,
                             cx,
                         );
@@ -777,14 +781,6 @@ impl ConnectionView {
                         }
 
                         let id = current.read(cx).thread.read(cx).session_id().clone();
-                        let session_list = if connection.supports_session_history() {
-                            connection.session_list(cx)
-                        } else {
-                            None
-                        };
-                        this.history.update(cx, |history, cx| {
-                            history.set_session_list(session_list, cx);
-                        });
                         this.set_server_state(
                             ServerState::Connected(ConnectedServerState {
                                 connection,
@@ -792,6 +788,7 @@ impl ConnectionView {
                                 active_id: Some(id.clone()),
                                 threads: HashMap::from_iter([(id, current)]),
                                 conversation,
+                                history,
                                 _connection_entry_subscription: connection_entry_subscription,
                             }),
                             cx,
@@ -825,6 +822,7 @@ impl ConnectionView {
         conversation: Entity<Conversation>,
         resumed_without_history: bool,
         initial_content: Option<AgentInitialContent>,
+        history: Entity<ThreadHistory>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Entity<ThreadView> {
@@ -841,7 +839,7 @@ impl ConnectionView {
                 self.workspace.clone(),
                 self.project.downgrade(),
                 self.thread_store.clone(),
-                self.history.downgrade(),
+                history.downgrade(),
                 self.prompt_store.clone(),
                 prompt_capabilities.clone(),
                 available_commands.clone(),
@@ -1008,7 +1006,7 @@ impl ConnectionView {
                 resumed_without_history,
                 self.project.downgrade(),
                 self.thread_store.clone(),
-                self.history.clone(),
+                history,
                 self.prompt_store.clone(),
                 initial_content,
                 subscriptions,
@@ -1090,6 +1088,7 @@ impl ConnectionView {
                         threads: HashMap::default(),
                         connection,
                         conversation: cx.new(|_cx| Conversation::default()),
+                        history: cx.new(|cx| ThreadHistory::new(None, cx)),
                         _connection_entry_subscription: Subscription::new(|| {}),
                     }),
                     cx,
@@ -1694,10 +1693,10 @@ impl ConnectionView {
         cx.spawn_in(window, async move |this, cx| {
             let subagent_thread = subagent_thread_task.await?;
             this.update_in(cx, |this, window, cx| {
-                let conversation = this
+                let Some((conversation, history)) = this
                     .as_connected()
-                    .map(|connected| connected.conversation.clone());
-                let Some(conversation) = conversation else {
+                    .map(|connected| (connected.conversation.clone(), connected.history.clone()))
+                else {
                     return;
                 };
                 conversation.update(cx, |conversation, cx| {
@@ -1709,6 +1708,7 @@ impl ConnectionView {
                     conversation,
                     false,
                     None,
+                    history,
                     window,
                     cx,
                 );
@@ -2215,9 +2215,11 @@ impl ConnectionView {
         let agent_name = self.agent.name();
         let workspace = self.workspace.clone();
         let project = self.project.downgrade();
-        let history = self.history.downgrade();
-
-        let Some(thread) = self.active_thread() else {
+        let Some(connected) = self.as_connected() else {
+            return;
+        };
+        let history = connected.history.downgrade();
+        let Some(thread) = connected.active_view() else {
             return;
         };
         let prompt_capabilities = thread.read(cx).prompt_capabilities.clone();
@@ -2610,8 +2612,16 @@ impl ConnectionView {
         })
     }
 
+    pub fn history(&self) -> Option<&Entity<ThreadHistory>> {
+        self.as_connected().map(|c| &c.history)
+    }
+
     pub fn delete_history_entry(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
-        let task = self
+        let Some(connected) = self.as_connected() else {
+            return;
+        };
+
+        let task = connected
             .history
             .update(cx, |history, cx| history.delete_session(&session_id, cx));
         task.detach_and_log_err(cx);
@@ -2900,8 +2910,6 @@ pub(crate) mod tests {
         let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        // Create history without an initial session list - it will be set after connection
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
@@ -2921,7 +2929,6 @@ pub(crate) mod tests {
                     project,
                     Some(thread_store),
                     None,
-                    history.clone(),
                     window,
                     cx,
                 )
@@ -2931,6 +2938,14 @@ pub(crate) mod tests {
         // Wait for connection to establish
         cx.run_until_parked();
 
+        let history = cx.update(|_window, cx| {
+            thread_view
+                .read(cx)
+                .history()
+                .expect("Missing history")
+                .clone()
+        });
+
         // Initially empty because StubAgentConnection.session_list() returns None
         active_thread(&thread_view, cx).read_with(cx, |view, _cx| {
             assert_eq!(view.recent_history_entries.len(), 0);
@@ -3007,7 +3022,6 @@ pub(crate) mod tests {
         let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
@@ -3027,7 +3041,6 @@ pub(crate) mod tests {
                     project,
                     Some(thread_store),
                     None,
-                    history,
                     window,
                     cx,
                 )
@@ -3066,7 +3079,6 @@ pub(crate) mod tests {
         let captured_cwd = connection.captured_cwd.clone();
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
@@ -3086,7 +3098,6 @@ pub(crate) mod tests {
                     project,
                     Some(thread_store),
                     None,
-                    history,
                     window,
                     cx,
                 )
@@ -3123,7 +3134,6 @@ pub(crate) mod tests {
         let captured_cwd = connection.captured_cwd.clone();
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
@@ -3143,7 +3153,6 @@ pub(crate) mod tests {
                     project,
                     Some(thread_store),
                     None,
-                    history,
                     window,
                     cx,
                 )
@@ -3180,7 +3189,6 @@ pub(crate) mod tests {
         let captured_cwd = connection.captured_cwd.clone();
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
@@ -3200,7 +3208,6 @@ pub(crate) mod tests {
                     project,
                     Some(thread_store),
                     None,
-                    history,
                     window,
                     cx,
                 )
@@ -3498,7 +3505,6 @@ pub(crate) mod tests {
 
         // Set up thread view in workspace 1
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project1.clone(), cx)));
 
@@ -3519,7 +3525,6 @@ pub(crate) mod tests {
                     project1.clone(),
                     Some(thread_store),
                     None,
-                    history,
                     window,
                     cx,
                 )
@@ -3676,7 +3681,8 @@ pub(crate) mod tests {
         agent: impl AgentServer + 'static,
         cx: &mut TestAppContext,
     ) -> (Entity<ConnectionView>, &mut VisualTestContext) {
-        let (thread_view, _history, cx) = setup_thread_view_with_history(agent, cx).await;
+        let (thread_view, _history, cx) =
+            setup_thread_view_with_history_and_initial_content(agent, None, cx).await;
         (thread_view, cx)
     }
 
@@ -3688,7 +3694,9 @@ pub(crate) mod tests {
         Entity<ThreadHistory>,
         &mut VisualTestContext,
     ) {
-        setup_thread_view_with_history_and_initial_content(agent, None, cx).await
+        let (thread_view, history, cx) =
+            setup_thread_view_with_history_and_initial_content(agent, None, cx).await;
+        (thread_view, history.expect("Missing history"), cx)
     }
 
     async fn setup_thread_view_with_initial_content(
@@ -3708,7 +3716,7 @@ pub(crate) mod tests {
         cx: &mut TestAppContext,
     ) -> (
         Entity<ConnectionView>,
-        Entity<ThreadHistory>,
+        Option<Entity<ThreadHistory>>,
         &mut VisualTestContext,
     ) {
         let fs = FakeFs::new(cx.executor());
@@ -3718,18 +3726,19 @@ pub(crate) mod tests {
         let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
+        let agent_key = ExternalAgent::Custom {
+            name: "Test".into(),
+        };
+
         let thread_view = cx.update(|window, cx| {
             cx.new(|cx| {
                 ConnectionView::new(
                     Rc::new(agent),
-                    connection_store,
-                    ExternalAgent::Custom {
-                        name: "Test".into(),
-                    },
+                    connection_store.clone(),
+                    agent_key.clone(),
                     None,
                     None,
                     None,
@@ -3738,13 +3747,20 @@ pub(crate) mod tests {
                     project,
                     Some(thread_store),
                     None,
-                    history.clone(),
                     window,
                     cx,
                 )
             })
         });
         cx.run_until_parked();
+
+        let history = cx.update(|_window, cx| {
+            connection_store
+                .read(cx)
+                .entry(&agent_key)
+                .and_then(|e| e.read(cx).history().cloned())
+        });
+
         (thread_view, history, cx)
     }
 
@@ -4454,7 +4470,6 @@ pub(crate) mod tests {
         let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|_window, cx| cx.new(|cx| ThreadHistory::new(None, cx)));
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
@@ -4475,7 +4490,6 @@ pub(crate) mod tests {
                     project.clone(),
                     Some(thread_store.clone()),
                     None,
-                    history,
                     window,
                     cx,
                 )

crates/agent_ui/src/inline_assistant.rs 🔗

@@ -266,7 +266,7 @@ impl InlineAssistant {
             return;
         };
 
-        let configuration_error = || {
+        let configuration_error = |cx| {
             let model_registry = LanguageModelRegistry::read_global(cx);
             model_registry.configuration_error(model_registry.inline_assistant_model(), cx)
         };
@@ -278,7 +278,15 @@ impl InlineAssistant {
 
         let prompt_store = agent_panel.prompt_store().as_ref().cloned();
         let thread_store = agent_panel.thread_store().clone();
-        let history = agent_panel.history().downgrade();
+        let Some(history) = agent_panel
+            .connection_store()
+            .read(cx)
+            .entry(&crate::ExternalAgent::NativeAgent)
+            .and_then(|s| s.read(cx).history().cloned())
+        else {
+            log::error!("No connection entry found for native agent");
+            return;
+        };
 
         let handle_assist =
             |window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -290,7 +298,7 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             thread_store,
                             prompt_store,
-                            history,
+                            history.downgrade(),
                             action.prompt.clone(),
                             window,
                             cx,
@@ -305,7 +313,7 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             thread_store,
                             prompt_store,
-                            history,
+                            history.downgrade(),
                             action.prompt.clone(),
                             window,
                             cx,
@@ -314,7 +322,7 @@ impl InlineAssistant {
                 }
             };
 
-        if let Some(error) = configuration_error() {
+        if let Some(error) = configuration_error(cx) {
             if let ConfigurationError::ProviderNotAuthenticated(provider) = error {
                 cx.spawn(async move |_, cx| {
                     cx.update(|cx| provider.authenticate(cx)).await?;
@@ -322,7 +330,7 @@ impl InlineAssistant {
                 })
                 .detach_and_log_err(cx);
 
-                if configuration_error().is_none() {
+                if configuration_error(cx).is_none() {
                     handle_assist(window, cx);
                 }
             } else {
@@ -1969,7 +1977,16 @@ impl CodeActionProvider for AssistantCodeActionProvider {
                     .panel::<AgentPanel>(cx)
                     .context("missing agent panel")?
                     .read(cx);
-                anyhow::Ok((panel.thread_store().clone(), panel.history().downgrade()))
+
+                let history = panel
+                    .connection_store()
+                    .read(cx)
+                    .entry(&crate::ExternalAgent::NativeAgent)
+                    .and_then(|e| e.read(cx).history())
+                    .context("no history found for native agent")?
+                    .downgrade();
+
+                anyhow::Ok((panel.thread_store().clone(), history))
             })??;
             let editor = editor.upgrade().context("editor was released")?;
             let range = editor

crates/agent_ui/src/text_thread_history.rs 🔗

@@ -116,6 +116,10 @@ impl TextThreadHistory {
         this
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.visible_items.is_empty()
+    }
+
     fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
         let entries = self.text_thread_store.update(cx, |store, _| {
             store.ordered_text_threads().cloned().collect::<Vec<_>>()

crates/agent_ui/src/thread_history.rs 🔗

@@ -19,14 +19,23 @@ impl ThreadHistory {
             _refresh_task: Task::ready(()),
             _watch_task: None,
         };
-        this.set_session_list(session_list, cx);
+        this.set_session_list_impl(session_list, cx);
         this
     }
 
+    #[cfg(any(test, feature = "test-support"))]
     pub fn set_session_list(
         &mut self,
         session_list: Option<Rc<dyn AgentSessionList>>,
         cx: &mut Context<Self>,
+    ) {
+        self.set_session_list_impl(session_list, cx);
+    }
+
+    fn set_session_list_impl(
+        &mut self,
+        session_list: Option<Rc<dyn AgentSessionList>>,
+        cx: &mut Context<Self>,
     ) {
         if let (Some(current), Some(next)) = (&self.session_list, &session_list)
             && Rc::ptr_eq(current, next)

crates/agent_ui/src/thread_history_view.rs 🔗

@@ -117,6 +117,10 @@ impl ThreadHistoryView {
         this
     }
 
+    pub fn history(&self) -> &Entity<ThreadHistory> {
+        &self.history
+    }
+
     fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
         let entries = self.history.read(cx).sessions().to_vec();
         let new_list_items = if self.search_query.is_empty() {