agent2: New thread from summary (#36578)

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Bennet Bo Fenner 2025-08-20 15:54:00 +02:00 committed by GitHub
parent c5040bd0a4
commit 85865fc950
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 131 additions and 19 deletions

View file

@ -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,

View file

@ -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

View file

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

View file

@ -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,
);

View file

@ -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 {

View file

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