assistant2: Agent notification improvements (#27638)

- Show thread's summary in notification title
- Improve thread's summary prompt so it's more descriptive
- Make whole notification clickable


![image](https://github.com/user-attachments/assets/f29da109-f16e-40af-bb43-0882403535c5)

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga 2025-03-28 13:30:48 -03:00 committed by GitHub
parent 82ce187bfc
commit da47013e56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 156 additions and 108 deletions

View file

@ -4,7 +4,7 @@ use crate::thread::{
}; };
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus}; use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent}; use crate::ui::{AgentNotification, AgentNotificationEvent, ContextPill};
use crate::AssistantPanel; use crate::AssistantPanel;
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use collections::HashMap; use collections::HashMap;
@ -45,9 +45,9 @@ pub struct ActiveThread {
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>, expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
expanded_thinking_segments: HashMap<(MessageId, usize), bool>, expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
last_error: Option<ThreadError>, last_error: Option<ThreadError>,
pop_ups: Vec<WindowHandle<ToolReadyPopUp>>, notifications: Vec<WindowHandle<AgentNotification>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
pop_up_subscriptions: HashMap<WindowHandle<ToolReadyPopUp>, Vec<Subscription>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
} }
struct RenderedMessage { struct RenderedMessage {
@ -252,9 +252,9 @@ impl ActiveThread {
scrollbar_state: ScrollbarState::new(list_state), scrollbar_state: ScrollbarState::new(list_state),
editing_message: None, editing_message: None,
last_error: None, last_error: None,
pop_ups: Vec::new(), notifications: Vec::new(),
_subscriptions: subscriptions, _subscriptions: subscriptions,
pop_up_subscriptions: HashMap::default(), notification_subscriptions: HashMap::default(),
}; };
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() { for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
@ -377,24 +377,23 @@ impl ActiveThread {
self.save_thread(cx); self.save_thread(cx);
} }
ThreadEvent::DoneStreaming => { ThreadEvent::DoneStreaming => {
if !self.thread().read(cx).is_generating() { let thread = self.thread.read(cx);
if !thread.is_generating() {
self.show_notification( self.show_notification(
"The assistant response has concluded.", if thread.used_tools_since_last_user_message() {
IconName::Check, "Finished running tools"
Color::Success, } else {
"New message"
},
IconName::ZedAssistant,
window, window,
cx, cx,
); );
} }
} }
ThreadEvent::ToolConfirmationNeeded => { ThreadEvent::ToolConfirmationNeeded => {
self.show_notification( self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
"There's a tool confirmation needed.",
IconName::Info,
Color::Muted,
window,
cx,
);
} }
ThreadEvent::StreamedAssistantText(message_id, text) => { ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) { if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
@ -526,85 +525,90 @@ impl ActiveThread {
&mut self, &mut self,
caption: impl Into<SharedString>, caption: impl Into<SharedString>,
icon: IconName, icon: IconName,
icon_color: Color,
window: &mut Window, window: &mut Window,
cx: &mut Context<'_, ActiveThread>, cx: &mut Context<'_, ActiveThread>,
) { ) {
if !window.is_window_active() if window.is_window_active()
&& self.pop_ups.is_empty() || !self.notifications.is_empty()
&& AssistantSettings::get_global(cx).notify_when_agent_waiting || !AssistantSettings::get_global(cx).notify_when_agent_waiting
{ {
let caption = caption.into(); return;
}
for screen in cx.displays() { let caption = caption.into();
let options = ToolReadyPopUp::window_options(screen, cx);
if let Some(screen_window) = cx let title = self
.open_window(options, |_, cx| { .thread
cx.new(|_| ToolReadyPopUp::new(caption.clone(), icon, icon_color)) .read(cx)
}) .summary()
.log_err() .unwrap_or("Agent Panel".into());
{
if let Some(pop_up) = screen_window.entity(cx).log_err() {
self.pop_up_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push(cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event {
ToolReadyPopupEvent::Accepted => {
let handle = window.window_handle();
cx.activate(true); // Switch back to the Zed application
let workspace_handle = this.workspace.clone(); for screen in cx.displays() {
let options = AgentNotification::window_options(screen, cx);
// If there are multiple Zed windows, activate the correct one. if let Some(screen_window) = cx
cx.defer(move |cx| { .open_window(options, |_, cx| {
handle cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
.update(cx, |_view, window, _cx| { })
window.activate_window(); .log_err()
{
if let Some(pop_up) = screen_window.entity(cx).log_err() {
self.notification_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push(cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event {
AgentNotificationEvent::Accepted => {
let handle = window.window_handle();
cx.activate(true); // Switch back to the Zed application
if let Some(workspace) = let workspace_handle = this.workspace.clone();
workspace_handle.upgrade()
{ // If there are multiple Zed windows, activate the correct one.
workspace.update(_cx, |workspace, cx| { cx.defer(move |cx| {
workspace handle
.focus_panel::<AssistantPanel>( .update(cx, |_view, window, _cx| {
window, cx, window.activate_window();
);
}); if let Some(workspace) = workspace_handle.upgrade()
} {
}) workspace.update(_cx, |workspace, cx| {
.log_err(); workspace.focus_panel::<AssistantPanel>(
window, cx,
);
});
}
})
.log_err();
});
this.dismiss_notifications(cx);
}
AgentNotificationEvent::Dismissed => {
this.dismiss_notifications(cx);
}
}
}));
self.notifications.push(screen_window);
// If the user manually refocuses the original window, dismiss the popup.
self.notification_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push({
let pop_up_weak = pop_up.downgrade();
cx.observe_window_activation(window, move |_, window, cx| {
if window.is_window_active() {
if let Some(pop_up) = pop_up_weak.upgrade() {
pop_up.update(cx, |_, cx| {
cx.emit(AgentNotificationEvent::Dismissed);
}); });
this.dismiss_notifications(cx);
}
ToolReadyPopupEvent::Dismissed => {
this.dismiss_notifications(cx);
} }
} }
})); })
});
self.pop_ups.push(screen_window);
// If the user manually refocuses the original window, dismiss the popup.
self.pop_up_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push({
let pop_up_weak = pop_up.downgrade();
cx.observe_window_activation(window, move |_, window, cx| {
if window.is_window_active() {
if let Some(pop_up) = pop_up_weak.upgrade() {
pop_up.update(cx, |_, cx| {
cx.emit(ToolReadyPopupEvent::Dismissed);
});
}
}
})
});
}
} }
} }
} }
@ -1764,14 +1768,14 @@ impl ActiveThread {
} }
fn dismiss_notifications(&mut self, cx: &mut Context<'_, ActiveThread>) { fn dismiss_notifications(&mut self, cx: &mut Context<'_, ActiveThread>) {
for window in self.pop_ups.drain(..) { for window in self.notifications.drain(..) {
window window
.update(cx, |_, window, _| { .update(cx, |_, window, _| {
window.remove_window(); window.remove_window();
}) })
.ok(); .ok();
self.pop_up_subscriptions.remove(&window); self.notification_subscriptions.remove(&window);
} }
} }

View file

@ -786,6 +786,18 @@ impl Thread {
self.stream_completion(request, model, cx); self.stream_completion(request, model, cx);
} }
pub fn used_tools_since_last_user_message(&self) -> bool {
for message in self.messages.iter().rev() {
if self.tool_use.message_has_tool_results(message.id) {
return true;
} else if message.role == Role::User {
return false;
}
}
false
}
pub fn to_completion_request( pub fn to_completion_request(
&self, &self,
request_kind: RequestKind, request_kind: RequestKind,
@ -835,6 +847,9 @@ impl Thread {
} }
RequestKind::Summarize => { RequestKind::Summarize => {
// We don't care about tool use during summarization. // We don't care about tool use during summarization.
if self.tool_use.message_has_tool_results(message.id) {
continue;
}
} }
} }
@ -1126,7 +1141,10 @@ impl Thread {
request.messages.push(LanguageModelRequestMessage { request.messages.push(LanguageModelRequestMessage {
role: Role::User, role: Role::User,
content: vec![ content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`" "Generate a concise 3-7 word title for this conversation, omitting punctuation. \
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person."
.into(), .into(),
], ],
cache: false, cache: false,

View file

@ -1,5 +1,5 @@
mod agent_notification;
mod context_pill; mod context_pill;
mod tool_ready_pop_up;
pub use agent_notification::*;
pub use context_pill::*; pub use context_pill::*;
pub use tool_ready_pop_up::*;

View file

@ -1,24 +1,29 @@
use gpui::{ use gpui::{
point, App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window, linear_color_stop, linear_gradient, point, App, Context, EventEmitter, IntoElement,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, PlatformDisplay, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
WindowKind, WindowOptions,
}; };
use release_channel::ReleaseChannel; use release_channel::ReleaseChannel;
use std::rc::Rc; use std::rc::Rc;
use theme; use theme;
use ui::{prelude::*, Render}; use ui::{prelude::*, Render};
pub struct ToolReadyPopUp { pub struct AgentNotification {
title: SharedString,
caption: SharedString, caption: SharedString,
icon: IconName, icon: IconName,
icon_color: Color,
} }
impl ToolReadyPopUp { impl AgentNotification {
pub fn new(caption: impl Into<SharedString>, icon: IconName, icon_color: Color) -> Self { pub fn new(
title: impl Into<SharedString>,
caption: impl Into<SharedString>,
icon: IconName,
) -> Self {
Self { Self {
title: title.into(),
caption: caption.into(), caption: caption.into(),
icon, icon,
icon_color,
} }
} }
@ -58,19 +63,22 @@ impl ToolReadyPopUp {
} }
} }
pub enum ToolReadyPopupEvent { pub enum AgentNotificationEvent {
Accepted, Accepted,
Dismissed, Dismissed,
} }
impl EventEmitter<ToolReadyPopupEvent> for ToolReadyPopUp {} impl EventEmitter<AgentNotificationEvent> for AgentNotification {}
impl Render for ToolReadyPopUp { impl Render for AgentNotification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx); let ui_font = theme::setup_ui_font(window, cx);
let line_height = window.line_height(); let line_height = window.line_height();
let bg = cx.theme().colors().elevated_surface_background;
h_flex() h_flex()
.id("agent-notification")
.size_full() .size_full()
.p_3() .p_3()
.gap_4() .gap_4()
@ -80,14 +88,18 @@ impl Render for ToolReadyPopUp {
.font(ui_font) .font(ui_font)
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.rounded_xl() .rounded_xl()
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(AgentNotificationEvent::Accepted);
}))
.child( .child(
h_flex() h_flex()
.items_start() .items_start()
.gap_2() .gap_2()
.flex_1()
.child( .child(
h_flex().h(line_height).justify_center().child( h_flex().h(line_height).justify_center().child(
Icon::new(self.icon) Icon::new(self.icon)
.color(self.icon_color) .color(Color::Muted)
.size(IconSize::Small), .size(IconSize::Small),
), ),
) )
@ -95,33 +107,47 @@ impl Render for ToolReadyPopUp {
v_flex() v_flex()
.child( .child(
div() div()
.text_size(px(16.)) .text_size(px(14.))
.text_color(cx.theme().colors().text) .text_color(cx.theme().colors().text)
.child("Agent Panel"), .child(self.title.clone()),
) )
.child( .child(
div() div()
.text_size(px(14.)) .text_size(px(12.))
.text_color(cx.theme().colors().text_muted) .text_color(cx.theme().colors().text_muted)
.child(self.caption.clone()), .max_w(px(340.))
.truncate()
.child(self.caption.clone())
.relative()
.child(
div().h_full().absolute().w_8().bottom_0().right_0().bg(
linear_gradient(
90.,
linear_color_stop(bg, 1.),
linear_color_stop(bg.opacity(0.2), 0.),
),
),
),
), ),
), ),
) )
.child( .child(
h_flex() v_flex()
.gap_0p5() .gap_1()
.items_center()
.child( .child(
Button::new("open", "View Panel") Button::new("open", "View Panel")
.style(ButtonStyle::Tinted(ui::TintColor::Accent)) .style(ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.on_click({ .on_click({
cx.listener(move |_this, _event, _, cx| { cx.listener(move |_this, _event, _, cx| {
cx.emit(ToolReadyPopupEvent::Accepted); cx.emit(AgentNotificationEvent::Accepted);
}) })
}), }),
) )
.child(Button::new("dismiss", "Dismiss").on_click({ .child(Button::new("dismiss", "Dismiss").full_width().on_click({
cx.listener(move |_, _event, _, cx| { cx.listener(move |_, _event, _, cx| {
cx.emit(ToolReadyPopupEvent::Dismissed); cx.emit(AgentNotificationEvent::Dismissed);
}) })
})), })),
) )