agent_ui: No global thread history (#51362)

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
This commit is contained in:
Bennet Bo Fenner authored and GitHub committed 2026-03-12 09:22:05 +00:00
1 parent 81da953acf
commit 5ebdbe2aac
7 files changed
+332 -194

No files matched your search

+46 -27
View File
@@ -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()))),
+182 -111
View File
@@ -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,
+62 -48
View File
@@ -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,
)
+24 -7
View File
@@ -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
@@ -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<_>>()
+10 -1
View File
@@ -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)
@@ -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() {