Notify when tool is finished (#27459)

Now if a tool call finishes (or is blocked on confirmation) and the Zed
window is not active, you get a notification popup. You can turn it off
with a setting.

<img width="420" alt="Screenshot 2025-03-25 at 5 19 25 PM"
src="https://github.com/user-attachments/assets/bdf7b6b8-4428-4b46-8b09-e0be140f8a51"
/>
<img width="420 alt="Screenshot 2025-03-25 at 5 18 13 PM"
src="https://github.com/user-attachments/assets/1325e7b8-cd5a-44b9-a82d-5db928ad3cfc"
/>

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Richard Feldman 2025-03-25 20:52:44 -04:00 committed by GitHub
parent a605b66ce1
commit 931a6d6f40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 216 additions and 3 deletions

View file

@ -4,8 +4,9 @@ use crate::thread::{
};
use crate::thread_store::ThreadStore;
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
use crate::ui::ContextPill;
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
use assistant_settings::AssistantSettings;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
@ -13,6 +14,7 @@ use gpui::{
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
WindowHandle,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
@ -42,6 +44,7 @@ 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>>,
_subscriptions: Vec<Subscription>,
}
@ -244,6 +247,7 @@ impl ActiveThread {
}),
editing_message: None,
last_error: None,
pop_ups: Vec::new(),
_subscriptions: subscriptions,
};
@ -370,7 +374,14 @@ impl ActiveThread {
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
self.save_thread(cx);
}
ThreadEvent::DoneStreaming => {}
ThreadEvent::DoneStreaming => {
if !self.thread().read(cx).is_generating() {
self.show_notification("Your changes have been applied.", window, cx);
}
}
ThreadEvent::ToolConfirmationNeeded => {
self.show_notification("There's a tool confirmation needed.", window, cx);
}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
rendered_message.append_text(text, window, cx);
@ -497,6 +508,59 @@ impl ActiveThread {
}
}
fn show_notification(
&mut self,
caption: impl Into<SharedString>,
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
{
let caption = caption.into();
for screen in cx.displays() {
let options = ToolReadyPopUp::window_options(screen, cx);
if let Some(screen_window) = cx
.open_window(options, |_, cx| {
cx.new(|_| ToolReadyPopUp::new(caption.clone()))
})
.log_err()
{
if let Some(pop_up) = screen_window.entity(cx).log_err() {
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
// If there are multiple Zed windows, activate the correct one.
cx.defer(move |cx| {
handle
.update(cx, |_view, window, _cx| {
window.activate_window();
})
.log_err();
});
this.dismiss_notifications(cx);
}
ToolReadyPopupEvent::Dismissed => {
this.dismiss_notifications(cx);
}
}
})
.detach();
self.pop_ups.push(screen_window);
}
}
}
}
}
/// Spawns a task to save the active thread.
///
/// Only one task to save the thread will be in flight at a time.
@ -1635,6 +1699,16 @@ impl ActiveThread {
.into_any()
})
}
fn dismiss_notifications(&mut self, cx: &mut Context<'_, ActiveThread>) {
for window in self.pop_ups.drain(..) {
window
.update(cx, |_, window, _| {
window.remove_window();
})
.ok();
}
}
}
impl Render for ActiveThread {