diff --git a/crates/agent_ui/src/agent_connection_store.rs b/crates/agent_ui/src/agent_connection_store.rs index c0c4519bcc64d53690dd782a55e6b9da4f498fe0..936b9b7a2de984f20f59c8f050ecb3bff1386595 100644 --- a/crates/agent_ui/src/agent_connection_store.rs +++ b/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, LoadError>>>, - }, - Connected { - connection: Rc, + connect_task: Shared>>, }, + Connected(AgentConnectedState), Error { error: LoadError, }, } -impl ConnectionEntry { - pub fn wait_for_connection(&self) -> Shared, LoadError>>> { +#[derive(Clone)] +pub struct AgentConnectedState { + pub connection: Rc, + pub history: Entity, +} + +impl AgentConnectionEntry { + pub fn wait_for_connection(&self) -> Shared>> { 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> { + match self { + AgentConnectionEntry::Connected(state) => Some(&state.history), + _ => None, } } } -pub enum ConnectionEntryEvent { +pub enum AgentConnectionEntryEvent { NewVersionAvailable(SharedString), } -impl EventEmitter for ConnectionEntry {} +impl EventEmitter for AgentConnectionEntry {} pub struct AgentConnectionStore { project: Entity, - entries: HashMap>, + entries: HashMap>, _subscriptions: Vec, } @@ -59,17 +68,21 @@ impl AgentConnectionStore { } } + pub fn entry(&self, key: &ExternalAgent) -> Option<&Entity> { + self.entries.get(key) + } + pub fn request_connection( &mut self, key: ExternalAgent, server: Rc, cx: &mut Context, - ) -> Entity { + ) -> Entity { 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, ) -> ( Receiver>, - Task, LoadError>>, + Task>, ) { let (new_version_tx, new_version_rx) = watch::channel::>(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::() { Ok(load_error) => Err(load_error), Err(err) => Err(LoadError::Other(SharedString::from(err.to_string()))), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 1aefc99c020409a764ad2c44fe8477665f73c4bc..741e995c8f1b2e44677ec7c7de7bef22a3421f3c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/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 }, TextThreads, } @@ -639,7 +639,7 @@ enum ActiveView { _subscriptions: Vec, }, History { - kind: HistoryKind, + history: History, }, Configuration, } @@ -870,8 +870,6 @@ pub struct AgentPanel { project: Entity, fs: Arc, language_registry: Arc, - acp_history: Entity, - acp_history_view: Entity, text_thread_history: Entity, thread_store: Entity, text_thread_store: Entity, @@ -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 { - &self.acp_history + pub fn connection_store(&self) -> &Entity { + &self.connection_store } pub fn open_thread( @@ -1353,27 +1335,41 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - 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) { @@ -1554,13 +1550,52 @@ impl AgentPanel { }) } - fn history_kind_for_selected_agent(&self, cx: &App) -> Option { - 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, + ) -> Option { + 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, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + 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) { - 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, ) { - 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, - kind: HistoryKind, + history: History, cx: &mut Context, ) -> 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::(); 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::(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, diff --git a/crates/agent_ui/src/connection_view.rs b/crates/agent_ui/src/connection_view.rs index b562688a83b75b75a1b95c065b14d0484daef055..8aeacbd61ad404f94c39efbd14a846a3b52150d9 100644 --- a/crates/agent_ui/src/connection_view.rs +++ b/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>, prompt_store: Option>, server_state: ServerState, - history: Entity, focus_handle: FocusHandle, notifications: Vec>, notification_subscriptions: HashMap, Vec>, @@ -418,6 +419,7 @@ pub struct ConnectedServerState { active_id: Option, threads: HashMap>, connection: Rc, + history: Entity, conversation: Entity, _connection_entry_subscription: Subscription, } @@ -484,7 +486,6 @@ impl ConnectionView { project: Entity, thread_store: Option>, prompt_store: Option>, - history: Entity, window: &mut Window, cx: &mut Context, ) -> 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, resumed_without_history: bool, initial_content: Option, + history: Entity, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -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> { + self.as_connected().map(|c| &c.history) + } + pub fn delete_history_entry(&mut self, session_id: &acp::SessionId, cx: &mut Context) { - 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, &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, &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, - Entity, + Option>, &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, ) diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 2aee2b4601e126b25a977cf92d314970049026da..8fde876183db385c019e6ccb1f2e5a0d4b121892 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/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| 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::(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 diff --git a/crates/agent_ui/src/text_thread_history.rs b/crates/agent_ui/src/text_thread_history.rs index c19f64bc3503ab38c83dc9534d64fae5c23cc21c..7a2a4ff91ddae0531df200118b55151a8dbb4499 100644 --- a/crates/agent_ui/src/text_thread_history.rs +++ b/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) { let entries = self.text_thread_store.update(cx, |store, _| { store.ordered_text_threads().cloned().collect::>() diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 5e66d4468767e7002b8b5f6c79ffe8aaecf77127..1ca763cb6a64f1d1b680e31c1ac55a4717762157 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/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>, cx: &mut Context, + ) { + self.set_session_list_impl(session_list, cx); + } + + fn set_session_list_impl( + &mut self, + session_list: Option>, + cx: &mut Context, ) { if let (Some(current), Some(next)) = (&self.session_list, &session_list) && Rc::ptr_eq(current, next) diff --git a/crates/agent_ui/src/thread_history_view.rs b/crates/agent_ui/src/thread_history_view.rs index 1756fc46ed48e86dc4bf9c78f2c2ef79618ed43b..4e43748911ba0559485e7a4d991e5dc9d2d4c524 100644 --- a/crates/agent_ui/src/thread_history_view.rs +++ b/crates/agent_ui/src/thread_history_view.rs @@ -117,6 +117,10 @@ impl ThreadHistoryView { this } + pub fn history(&self) -> &Entity { + &self.history + } + fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { let entries = self.history.read(cx).sessions().to_vec(); let new_list_items = if self.search_query.is_empty() {