assistant2: Agent notification improvements (#27638)

Agus Zubiaga created

- 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

Change summary

crates/assistant2/src/active_thread.rs         | 180 ++++++++++---------
crates/assistant2/src/thread.rs                |  20 ++
crates/assistant2/src/ui.rs                    |   4 
crates/assistant2/src/ui/agent_notification.rs |  66 +++++--
4 files changed, 159 insertions(+), 111 deletions(-)

Detailed changes

crates/assistant2/src/active_thread.rs 🔗

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

crates/assistant2/src/thread.rs 🔗

@@ -786,6 +786,18 @@ impl Thread {
         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(
         &self,
         request_kind: RequestKind,
@@ -835,6 +847,9 @@ impl Thread {
                 }
                 RequestKind::Summarize => {
                     // 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 {
             role: Role::User,
             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(),
             ],
             cache: false,

crates/assistant2/src/ui.rs 🔗

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

crates/assistant2/src/ui/tool_ready_pop_up.rs → crates/assistant2/src/ui/agent_notification.rs 🔗

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