agent2: New thread from summary (#36578)

Bennet Bo Fenner , Agus Zubiaga , and Cole Miller created

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

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(-)

Detailed changes

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,

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<Self>,
+    ) {
+        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<Editor> {
         &self.editor

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -155,6 +155,7 @@ impl AcpThreadView {
     pub fn new(
         agent: Rc<dyn AgentServer>,
         resume_thread: Option<DbThreadMetadata>,
+        summarize_thread: Option<DbThreadMetadata>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
         history_store: Entity<HistoryStore>,
@@ -164,7 +165,7 @@ impl AcpThreadView {
     ) -> Self {
         let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().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(),

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::<AgentPanel>(window, cx);
                     }
                 })
+                .register_action(
+                    |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
+                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                            panel.update(cx, |panel, cx| {
+                                panel.new_native_agent_thread_from_summary(action, window, cx)
+                            });
+                            workspace.focus_panel::<AgentPanel>(window, cx);
+                        }
+                    },
+                )
                 .register_action(|workspace, _: &OpenHistory, window, cx| {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
@@ -120,7 +130,7 @@ pub fn init(cx: &mut App) {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(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<Self>,
+    ) {
+        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<Self>) {
         let context = self
             .context_store
@@ -1015,6 +1049,7 @@ impl AgentPanel {
         &mut self,
         agent_choice: Option<crate::ExternalAgent>,
         resume_thread: Option<DbThreadMetadata>,
+        summarize_thread: Option<DbThreadMetadata>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -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>,
     ) {
-        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,
                                                 );

crates/agent_ui/src/agent_ui.rs 🔗

@@ -146,6 +146,13 @@ pub struct NewExternalAgentThread {
     agent: Option<ExternalAgent>,
 }
 
+#[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 {

crates/zed/src/zed.rs 🔗

@@ -4362,6 +4362,7 @@ mod tests {
                     | "workspace::MoveItemToPaneInDirection"
                     | "workspace::OpenTerminal"
                     | "workspace::SendKeystrokes"
+                    | "agent::NewNativeAgentThreadFromSummary"
                     | "zed::OpenBrowser"
                     | "zed::OpenZedUrl" => {}
                     _ => {