Cargo.lock 🔗
@@ -491,6 +491,7 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
+ "release_channel",
"rope",
"serde",
"serde_json",
Richard Feldman , Agus Zubiaga , and Danilo Leal created
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>
Cargo.lock | 1
assets/settings/default.json | 3
crates/assistant2/Cargo.toml | 1
crates/assistant2/src/active_thread.rs | 78 +++++++++
crates/assistant2/src/thread.rs | 6
crates/assistant2/src/ui.rs | 2
crates/assistant2/src/ui/tool_ready_pop_up.rs | 115 +++++++++++++++
crates/assistant_settings/src/assistant_settings.rs | 13 +
8 files changed, 216 insertions(+), 3 deletions(-)
@@ -491,6 +491,7 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
+ "release_channel",
"rope",
"serde",
"serde_json",
@@ -654,7 +654,8 @@
"thinking": true
}
}
- }
+ },
+ "notify_when_agent_waiting": true
},
// The settings for slash commands.
"slash_commands": {
@@ -61,6 +61,7 @@ project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
+release_channel.workspace = true
rope.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -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 {
@@ -352,6 +352,10 @@ impl Thread {
.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> {
self.checkpoints_by_message.get(&id).cloned()
}
@@ -1161,6 +1165,7 @@ impl Thread {
messages.clone(),
tool,
);
+ cx.emit(ThreadEvent::ToolConfirmationNeeded);
} else {
self.run_tool(
tool_use.id.clone(),
@@ -1539,6 +1544,7 @@ pub enum ThreadEvent {
canceled: bool,
},
CheckpointChanged,
+ ToolConfirmationNeeded,
}
impl EventEmitter<ThreadEvent> for Thread {}
@@ -1,3 +1,5 @@
mod context_pill;
+mod tool_ready_pop_up;
pub use context_pill::*;
+pub use tool_ready_pop_up::*;
@@ -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 profiles: IndexMap<Arc<str>, AgentProfile>,
pub always_allow_tool_actions: bool,
+ pub notify_when_agent_waiting: bool,
}
impl AssistantSettings {
@@ -175,6 +176,7 @@ impl AssistantSettingsContent {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
@@ -198,6 +200,7 @@ impl AssistantSettingsContent {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
},
}
}
@@ -329,6 +332,7 @@ impl Default for VersionedAssistantSettingsContent {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
})
}
}
@@ -372,6 +376,10 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: false
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)]
@@ -519,6 +527,10 @@ impl Settings for AssistantSettings {
&mut settings.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 {
settings
@@ -611,6 +623,7 @@ mod tests {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
}),
)
},