From 85865fc9509d7c336325a0825f990a2c6d3267ca Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 20 Aug 2025 15:54:00 +0200 Subject: [PATCH] agent2: New thread from summary (#36578) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga Co-authored-by: Cole Miller --- crates/agent2/src/history_store.rs | 4 ++ crates/agent_ui/src/acp/message_editor.rs | 30 ++++++++ crates/agent_ui/src/acp/thread_view.rs | 25 +++++-- crates/agent_ui/src/agent_panel.rs | 83 +++++++++++++++++++---- crates/agent_ui/src/agent_ui.rs | 7 ++ crates/zed/src/zed.rs | 1 + 6 files changed, 131 insertions(+), 19 deletions(-) diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 3df4eddde4..870c2607c4 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -111,6 +111,10 @@ impl HistoryStore { } } + pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> { + self.threads.iter().find(|thread| &thread.id == session_id) + } + pub fn delete_thread( &mut self, id: acp::SessionId, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index b5282bf891..a50e33dc31 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -163,6 +163,36 @@ impl MessageEditor { } } + pub fn insert_thread_summary( + &mut self, + thread: agent2::DbThreadMetadata, + window: &mut Window, + cx: &mut Context, + ) { + let start = self.editor.update(cx, |editor, cx| { + editor.set_text(format!("{}\n", thread.title), window, cx); + editor + .buffer() + .read(cx) + .snapshot(cx) + .anchor_before(Point::zero()) + .text_anchor + }); + + self.confirm_completion( + thread.title.clone(), + start, + thread.title.len(), + MentionUri::Thread { + id: thread.id.clone(), + name: thread.title.to_string(), + }, + window, + cx, + ) + .detach(); + } + #[cfg(test)] pub(crate) fn editor(&self) -> &Entity { &self.editor diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f89198c84b..8d7f9c53ca 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -155,6 +155,7 @@ impl AcpThreadView { pub fn new( agent: Rc, resume_thread: Option, + summarize_thread: Option, workspace: WeakEntity, project: Entity, history_store: Entity, @@ -164,7 +165,7 @@ impl AcpThreadView { ) -> Self { let prevent_slash_commands = agent.clone().downcast::().is_some(); let message_editor = cx.new(|cx| { - MessageEditor::new( + let mut editor = MessageEditor::new( workspace.clone(), project.clone(), history_store.clone(), @@ -177,7 +178,11 @@ impl AcpThreadView { }, window, cx, - ) + ); + if let Some(entry) = summarize_thread { + editor.insert_thread_summary(entry, window, cx); + } + editor }); let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); @@ -3636,8 +3641,18 @@ impl AcpThreadView { .child( Button::new("start-new-thread", "Start New Thread") .label_size(LabelSize::Small) - .on_click(cx.listener(|_this, _, _window, _cx| { - // todo: Once thread summarization is implemented, start a new thread from a summary. + .on_click(cx.listener(|this, _, window, cx| { + let Some(thread) = this.thread() else { + return; + }; + let session_id = thread.read(cx).session_id().clone(); + window.dispatch_action( + crate::NewNativeAgentThreadFromSummary { + from_session_id: session_id, + } + .boxed_clone(), + cx, + ); })), ) .when(burn_mode_available, |this| { @@ -4320,6 +4335,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(agent), None, + None, workspace.downgrade(), project, history_store, @@ -4526,6 +4542,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), None, + None, workspace.downgrade(), project.clone(), history_store.clone(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 286d3b1c26..e2c4acb1ce 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -30,7 +30,7 @@ use crate::{ thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; -use crate::{ExternalAgent, NewExternalAgentThread}; +use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, @@ -98,6 +98,16 @@ pub fn init(cx: &mut App) { workspace.focus_panel::(window, cx); } }) + .register_action( + |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.new_native_agent_thread_from_summary(action, window, cx) + }); + workspace.focus_panel::(window, cx); + } + }, + ) .register_action(|workspace, _: &OpenHistory, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); @@ -120,7 +130,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent, None, window, cx) + panel.external_thread(action.agent, None, None, window, cx) }); } }) @@ -670,6 +680,7 @@ impl AgentPanel { this.external_thread( Some(crate::ExternalAgent::NativeAgent), Some(thread.clone()), + None, window, cx, ); @@ -974,6 +985,29 @@ impl AgentPanel { AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } + fn new_native_agent_thread_from_summary( + &mut self, + action: &NewNativeAgentThreadFromSummary, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self + .acp_history_store + .read(cx) + .thread_from_session_id(&action.from_session_id) + else { + return; + }; + + self.external_thread( + Some(ExternalAgent::NativeAgent), + None, + Some(thread.clone()), + window, + cx, + ); + } + fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { let context = self .context_store @@ -1015,6 +1049,7 @@ impl AgentPanel { &mut self, agent_choice: Option, resume_thread: Option, + summarize_thread: Option, window: &mut Window, cx: &mut Context, ) { @@ -1083,6 +1118,7 @@ impl AgentPanel { crate::acp::AcpThreadView::new( server, resume_thread, + summarize_thread, workspace.clone(), project, this.acp_history_store.clone(), @@ -1754,6 +1790,7 @@ impl AgentPanel { agent2::HistoryEntry::AcpThread(entry) => this.external_thread( Some(ExternalAgent::NativeAgent), Some(entry.clone()), + None, window, cx, ), @@ -1823,15 +1860,23 @@ impl AgentPanel { AgentType::TextThread => { window.dispatch_action(NewTextThread.boxed_clone(), cx); } - AgentType::NativeAgent => { - self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) - } + AgentType::NativeAgent => self.external_thread( + Some(crate::ExternalAgent::NativeAgent), + None, + None, + window, + cx, + ), AgentType::Gemini => { - self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) - } - AgentType::ClaudeCode => { - self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx) + self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx) } + AgentType::ClaudeCode => self.external_thread( + Some(crate::ExternalAgent::ClaudeCode), + None, + None, + window, + cx, + ), } } @@ -1841,7 +1886,13 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); + self.external_thread( + Some(ExternalAgent::NativeAgent), + Some(thread), + None, + window, + cx, + ); } } @@ -2358,8 +2409,10 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } + ActiveView::ExternalAgentThread { thread_view } => { + thread_view.read(cx).as_native_thread(cx) + } + ActiveView::Thread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, @@ -2396,15 +2449,15 @@ impl AgentPanel { let thread = active_thread.read(cx); if !thread.is_empty() { - let thread_id = thread.id().clone(); + let session_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), + Box::new(NewNativeAgentThreadFromSummary { + from_session_id: session_id.clone(), }), cx, ); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 01a248994d..7b6557245f 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -146,6 +146,13 @@ pub struct NewExternalAgentThread { agent: Option, } +#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] +#[action(namespace = agent)] +#[serde(deny_unknown_fields)] +pub struct NewNativeAgentThreadFromSummary { + from_session_id: agent_client_protocol::SessionId, +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0972973b89..0f6d236c65 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4362,6 +4362,7 @@ mod tests { | "workspace::MoveItemToPaneInDirection" | "workspace::OpenTerminal" | "workspace::SendKeystrokes" + | "agent::NewNativeAgentThreadFromSummary" | "zed::OpenBrowser" | "zed::OpenZedUrl" => {} _ => {