diff --git a/Cargo.lock b/Cargo.lock index 1b0eda060cb7507434704c6bf09aefbc7aea12af..3f75c3ae59673fff43f04f6dc097ee7678c293d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,6 +491,7 @@ dependencies = [ "prompt_store", "proto", "rand 0.8.5", + "release_channel", "rope", "serde", "serde_json", diff --git a/assets/settings/default.json b/assets/settings/default.json index 252b52c6747ea412fd20d9df3906f83047061742..eca9fa5a6871c50049227f31d82e71ef79f8f475 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -654,7 +654,8 @@ "thinking": true } } - } + }, + "notify_when_agent_waiting": true }, // The settings for slash commands. "slash_commands": { diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 0fb89110233d75bfbbf09b29065401ff9bd583c0..d198bd19ae94aa26cbafeaabe1a5bb78b4e4a7e2 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -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 diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index 82b42b728e8e098b790629d5db54bd48a9bd4290..72a678d085763355b25203f14ebdbad54173a710 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -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, expanded_thinking_segments: HashMap<(MessageId, usize), bool>, last_error: Option, + pop_ups: Vec>, _subscriptions: Vec, } @@ -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, + 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 { diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 5803da85bc0feb0e14fa45bbf806bab41de3b740..0b432def5f5be25c5d6cfd6aca8955ea8be712e1 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -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 { 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 for Thread {} diff --git a/crates/assistant2/src/ui.rs b/crates/assistant2/src/ui.rs index b10c09b3007984ac148bb6c339f27e6aab15ec7d..390a6f8edc93f8bafac4653d66896cef6802e3a3 100644 --- a/crates/assistant2/src/ui.rs +++ b/crates/assistant2/src/ui.rs @@ -1,3 +1,5 @@ mod context_pill; +mod tool_ready_pop_up; pub use context_pill::*; +pub use tool_ready_pop_up::*; diff --git a/crates/assistant2/src/ui/tool_ready_pop_up.rs b/crates/assistant2/src/ui/tool_ready_pop_up.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a43e2911371c7bb0715d7c07e4f81e105fdebf8 --- /dev/null +++ b/crates/assistant2/src/ui/tool_ready_pop_up.rs @@ -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) -> Self { + Self { + caption: caption.into(), + } + } + + pub fn window_options(screen: Rc, 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:: { + 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 for ToolReadyPopUp {} + +impl Render for ToolReadyPopUp { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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); + }) + })), + ) + } +} diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index 552bea6bfc9bdf8823d2e6238d3cff33d3f1cfb8..c937e75fe50f73404709641f1b0744fc358b12c9 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -73,6 +73,7 @@ pub struct AssistantSettings { pub enable_experimental_live_diffs: bool, pub profiles: IndexMap, 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, + /// Whether to show a popup notification when the agent is waiting for user input. + /// + /// Default: true + notify_when_agent_waiting: Option, } #[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, }), ) },