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:
parent
a605b66ce1
commit
931a6d6f40
8 changed files with 216 additions and 3 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -491,6 +491,7 @@ dependencies = [
|
||||||
"prompt_store",
|
"prompt_store",
|
||||||
"proto",
|
"proto",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"release_channel",
|
||||||
"rope",
|
"rope",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -654,7 +654,8 @@
|
||||||
"thinking": true
|
"thinking": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"notify_when_agent_waiting": true
|
||||||
},
|
},
|
||||||
// The settings for slash commands.
|
// The settings for slash commands.
|
||||||
"slash_commands": {
|
"slash_commands": {
|
||||||
|
|
|
@ -61,6 +61,7 @@ project.workspace = true
|
||||||
prompt_library.workspace = true
|
prompt_library.workspace = true
|
||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
|
release_channel.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
|
@ -4,8 +4,9 @@ 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;
|
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
|
||||||
|
|
||||||
|
use assistant_settings::AssistantSettings;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -13,6 +14,7 @@ use gpui::{
|
||||||
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
|
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
|
||||||
Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||||
|
WindowHandle,
|
||||||
};
|
};
|
||||||
use language::{Buffer, LanguageRegistry};
|
use language::{Buffer, LanguageRegistry};
|
||||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||||
|
@ -42,6 +44,7 @@ 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>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,6 +247,7 @@ impl ActiveThread {
|
||||||
}),
|
}),
|
||||||
editing_message: None,
|
editing_message: None,
|
||||||
last_error: None,
|
last_error: None,
|
||||||
|
pop_ups: Vec::new(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -370,7 +374,14 @@ impl ActiveThread {
|
||||||
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
|
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
|
||||||
self.save_thread(cx);
|
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) => {
|
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) {
|
||||||
rendered_message.append_text(text, window, cx);
|
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.
|
/// Spawns a task to save the active thread.
|
||||||
///
|
///
|
||||||
/// Only one task to save the thread will be in flight at a time.
|
/// Only one task to save the thread will be in flight at a time.
|
||||||
|
@ -1635,6 +1699,16 @@ impl ActiveThread {
|
||||||
.into_any()
|
.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 {
|
impl Render for ActiveThread {
|
||||||
|
|
|
@ -352,6 +352,10 @@ impl Thread {
|
||||||
.filter(|tool_use| tool_use.status.needs_confirmation())
|
.filter(|tool_use| tool_use.status.needs_confirmation())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_tool_uses(&self) -> bool {
|
||||||
|
!self.tool_use.pending_tool_uses().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
|
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
|
||||||
self.checkpoints_by_message.get(&id).cloned()
|
self.checkpoints_by_message.get(&id).cloned()
|
||||||
}
|
}
|
||||||
|
@ -1161,6 +1165,7 @@ impl Thread {
|
||||||
messages.clone(),
|
messages.clone(),
|
||||||
tool,
|
tool,
|
||||||
);
|
);
|
||||||
|
cx.emit(ThreadEvent::ToolConfirmationNeeded);
|
||||||
} else {
|
} else {
|
||||||
self.run_tool(
|
self.run_tool(
|
||||||
tool_use.id.clone(),
|
tool_use.id.clone(),
|
||||||
|
@ -1539,6 +1544,7 @@ pub enum ThreadEvent {
|
||||||
canceled: bool,
|
canceled: bool,
|
||||||
},
|
},
|
||||||
CheckpointChanged,
|
CheckpointChanged,
|
||||||
|
ToolConfirmationNeeded,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<ThreadEvent> for Thread {}
|
impl EventEmitter<ThreadEvent> for Thread {}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
mod context_pill;
|
mod context_pill;
|
||||||
|
mod tool_ready_pop_up;
|
||||||
|
|
||||||
pub use context_pill::*;
|
pub use context_pill::*;
|
||||||
|
pub use tool_ready_pop_up::*;
|
||||||
|
|
115
crates/assistant2/src/ui/tool_ready_pop_up.rs
Normal file
115
crates/assistant2/src/ui/tool_ready_pop_up.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
use gpui::{
|
||||||
|
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 {
|
||||||
|
caption: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolReadyPopUp {
|
||||||
|
pub fn new(caption: impl Into<SharedString>) -> Self {
|
||||||
|
Self {
|
||||||
|
caption: caption.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_options(screen: Rc<dyn PlatformDisplay>, cx: &App) -> WindowOptions {
|
||||||
|
let size = Size {
|
||||||
|
width: px(440.),
|
||||||
|
height: px(72.),
|
||||||
|
};
|
||||||
|
|
||||||
|
let notification_margin_width = px(16.);
|
||||||
|
let notification_margin_height = px(-48.);
|
||||||
|
|
||||||
|
let bounds = gpui::Bounds::<Pixels> {
|
||||||
|
origin: screen.bounds().top_right()
|
||||||
|
- point(
|
||||||
|
size.width + notification_margin_width,
|
||||||
|
notification_margin_height,
|
||||||
|
),
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_id = ReleaseChannel::global(cx).app_id();
|
||||||
|
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
titlebar: None,
|
||||||
|
focus: false,
|
||||||
|
show: true,
|
||||||
|
kind: WindowKind::PopUp,
|
||||||
|
is_movable: false,
|
||||||
|
display_id: Some(screen.id()),
|
||||||
|
window_background: WindowBackgroundAppearance::Transparent,
|
||||||
|
app_id: Some(app_id.to_owned()),
|
||||||
|
window_min_size: None,
|
||||||
|
window_decorations: Some(WindowDecorations::Client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ToolReadyPopupEvent {
|
||||||
|
Accepted,
|
||||||
|
Dismissed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ToolReadyPopupEvent> for ToolReadyPopUp {}
|
||||||
|
|
||||||
|
impl Render for ToolReadyPopUp {
|
||||||
|
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();
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.p_3()
|
||||||
|
.gap_4()
|
||||||
|
.justify_between()
|
||||||
|
.elevation_3(cx)
|
||||||
|
.text_ui(cx)
|
||||||
|
.font(ui_font)
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.rounded_xl()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex().h(line_height).justify_center().child(
|
||||||
|
Icon::new(IconName::Info)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.child(Headline::new("Agent Panel").size(HeadlineSize::XSmall))
|
||||||
|
.child(Label::new(self.caption.clone()).color(Color::Muted)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
Button::new("open", "View Panel")
|
||||||
|
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||||
|
.on_click({
|
||||||
|
cx.listener(move |_this, _event, _, cx| {
|
||||||
|
cx.emit(ToolReadyPopupEvent::Accepted);
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(Button::new("dismiss", "Dismiss").on_click({
|
||||||
|
cx.listener(move |_, _event, _, cx| {
|
||||||
|
cx.emit(ToolReadyPopupEvent::Dismissed);
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,6 +73,7 @@ pub struct AssistantSettings {
|
||||||
pub enable_experimental_live_diffs: bool,
|
pub enable_experimental_live_diffs: bool,
|
||||||
pub profiles: IndexMap<Arc<str>, AgentProfile>,
|
pub profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||||
pub always_allow_tool_actions: bool,
|
pub always_allow_tool_actions: bool,
|
||||||
|
pub notify_when_agent_waiting: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssistantSettings {
|
impl AssistantSettings {
|
||||||
|
@ -175,6 +176,7 @@ impl AssistantSettingsContent {
|
||||||
enable_experimental_live_diffs: None,
|
enable_experimental_live_diffs: None,
|
||||||
profiles: None,
|
profiles: None,
|
||||||
always_allow_tool_actions: None,
|
always_allow_tool_actions: None,
|
||||||
|
notify_when_agent_waiting: None,
|
||||||
},
|
},
|
||||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||||
},
|
},
|
||||||
|
@ -198,6 +200,7 @@ impl AssistantSettingsContent {
|
||||||
enable_experimental_live_diffs: None,
|
enable_experimental_live_diffs: None,
|
||||||
profiles: None,
|
profiles: None,
|
||||||
always_allow_tool_actions: None,
|
always_allow_tool_actions: None,
|
||||||
|
notify_when_agent_waiting: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -329,6 +332,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||||
enable_experimental_live_diffs: None,
|
enable_experimental_live_diffs: None,
|
||||||
profiles: None,
|
profiles: None,
|
||||||
always_allow_tool_actions: None,
|
always_allow_tool_actions: None,
|
||||||
|
notify_when_agent_waiting: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -372,6 +376,10 @@ pub struct AssistantSettingsContentV2 {
|
||||||
///
|
///
|
||||||
/// Default: false
|
/// Default: false
|
||||||
always_allow_tool_actions: Option<bool>,
|
always_allow_tool_actions: Option<bool>,
|
||||||
|
/// Whether to show a popup notification when the agent is waiting for user input.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
notify_when_agent_waiting: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
|
@ -519,6 +527,10 @@ impl Settings for AssistantSettings {
|
||||||
&mut settings.always_allow_tool_actions,
|
&mut settings.always_allow_tool_actions,
|
||||||
value.always_allow_tool_actions,
|
value.always_allow_tool_actions,
|
||||||
);
|
);
|
||||||
|
merge(
|
||||||
|
&mut settings.notify_when_agent_waiting,
|
||||||
|
value.notify_when_agent_waiting,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(profiles) = value.profiles {
|
if let Some(profiles) = value.profiles {
|
||||||
settings
|
settings
|
||||||
|
@ -611,6 +623,7 @@ mod tests {
|
||||||
enable_experimental_live_diffs: None,
|
enable_experimental_live_diffs: None,
|
||||||
profiles: None,
|
profiles: None,
|
||||||
always_allow_tool_actions: None,
|
always_allow_tool_actions: None,
|
||||||
|
notify_when_agent_waiting: None,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue