diff --git a/Cargo.lock b/Cargo.lock index 6f36e2ced5..a54d895434 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,7 @@ dependencies = [ "assistant_slash_commands", "assistant_tool", "async-watch", + "audio", "buffer_diff", "chrono", "client", diff --git a/assets/settings/default.json b/assets/settings/default.json index fd280f4535..a839b78dc4 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -822,7 +822,12 @@ // "primary_screen" - Show the notification only on your primary screen (default) // "all_screens" - Show these notifications on all screens // "never" - Never show these notifications - "notify_when_agent_waiting": "primary_screen" + "notify_when_agent_waiting": "primary_screen", + // Whether to play a sound when the agent has either completed + // its response, or needs user input. + + // Default: false + "play_sound_when_agent_done": false }, // The settings for slash commands. "slash_commands": { diff --git a/assets/sounds/agent_done.wav b/assets/sounds/agent_done.wav new file mode 100755 index 0000000000..22c9390c00 Binary files /dev/null and b/assets/sounds/agent_done.wav differ diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index cfb75fcd6d..ced340fca7 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -26,6 +26,7 @@ assistant_slash_command.workspace = true assistant_slash_commands.workspace = true assistant_tool.workspace = true async-watch.workspace = true +audio.workspace = true buffer_diff.workspace = true chrono.workspace = true client.workspace = true diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 094530129e..5f2a532f8e 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -16,6 +16,7 @@ use crate::ui::{ use anyhow::Context as _; use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting}; use assistant_tool::ToolUseStatus; +use audio::{Audio, Sound}; use collections::{HashMap, HashSet}; use editor::actions::{MoveUp, Paste}; use editor::scroll::Autoscroll; @@ -996,9 +997,10 @@ impl ActiveThread { } ThreadEvent::Stopped(reason) => match reason { Ok(StopReason::EndTurn | StopReason::MaxTokens) => { - let thread = self.thread.read(cx); + let used_tools = self.thread.read(cx).used_tools_since_last_user_message(); + self.play_notification_sound(cx); self.show_notification( - if thread.used_tools_since_last_user_message() { + if used_tools { "Finished running tools" } else { "New message" @@ -1011,6 +1013,7 @@ impl ActiveThread { _ => {} }, ThreadEvent::ToolConfirmationNeeded => { + self.play_notification_sound(cx); self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx); } ThreadEvent::StreamedAssistantText(message_id, text) => { @@ -1147,6 +1150,13 @@ impl ActiveThread { cx.notify(); } + fn play_notification_sound(&self, cx: &mut App) { + let settings = AssistantSettings::get_global(cx); + if settings.play_sound_when_agent_done { + Audio::play_sound(Sound::AgentDone, cx); + } + } + fn show_notification( &mut self, caption: impl Into, diff --git a/crates/agent/src/agent_configuration.rs b/crates/agent/src/agent_configuration.rs index 9bc8ad43c0..16c183229e 100644 --- a/crates/agent/src/agent_configuration.rs +++ b/crates/agent/src/agent_configuration.rs @@ -327,6 +327,45 @@ impl AgentConfiguration { ) } + fn render_sound_notification(&mut self, cx: &mut Context) -> impl IntoElement { + let play_sound_when_agent_done = + AssistantSettings::get_global(cx).play_sound_when_agent_done; + + h_flex() + .gap_4() + .justify_between() + .flex_wrap() + .child( + v_flex() + .gap_0p5() + .max_w_5_6() + .child(Label::new("Play sound when finished generating")) + .child( + Label::new( + "Hear a notification sound when the agent is done generating changes or needs your input.", + ) + .color(Color::Muted), + ), + ) + .child( + Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into()) + .color(SwitchColor::Accent) + .on_click({ + let fs = self.fs.clone(); + move |state, _window, cx| { + let allow = state == &ToggleState::Selected; + update_settings_file::( + fs.clone(), + cx, + move |settings, _| { + settings.set_play_sound_when_agent_done(allow); + }, + ); + } + }), + ) + } + fn render_general_settings_section(&mut self, cx: &mut Context) -> impl IntoElement { v_flex() .p(DynamicSpacing::Base16.rems(cx)) @@ -337,6 +376,7 @@ impl AgentConfiguration { .child(Headline::new("General Settings")) .child(self.render_command_permission(cx)) .child(self.render_single_file_review(cx)) + .child(self.render_sound_notification(cx)) } fn render_context_servers_section( diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index 706b20d86c..557cb9897b 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -105,6 +105,7 @@ pub struct AssistantSettings { pub profiles: IndexMap, pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, + pub play_sound_when_agent_done: bool, pub stream_edits: bool, pub single_file_review: bool, pub model_parameters: Vec, @@ -285,6 +286,7 @@ impl AssistantSettingsContent { model_parameters: Vec::new(), preferred_completion_mode: None, enable_feedback: None, + play_sound_when_agent_done: None, }, VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(), }, @@ -317,6 +319,7 @@ impl AssistantSettingsContent { model_parameters: Vec::new(), preferred_completion_mode: None, enable_feedback: None, + play_sound_when_agent_done: None, }, None => AssistantSettingsContentV2::default(), } @@ -517,6 +520,14 @@ impl AssistantSettingsContent { .ok(); } + pub fn set_play_sound_when_agent_done(&mut self, allow: bool) { + self.v2_setting(|setting| { + setting.play_sound_when_agent_done = Some(allow); + Ok(()) + }) + .ok(); + } + pub fn set_single_file_review(&mut self, allow: bool) { self.v2_setting(|setting| { setting.single_file_review = Some(allow); @@ -603,6 +614,7 @@ impl Default for VersionedAssistantSettingsContent { model_parameters: Vec::new(), preferred_completion_mode: None, enable_feedback: None, + play_sound_when_agent_done: None, }) } } @@ -659,6 +671,10 @@ pub struct AssistantSettingsContentV2 { /// /// Default: "primary_screen" notify_when_agent_waiting: Option, + /// Whether to play a sound when the agent has either completed its response, or needs user input. + /// + /// Default: false + play_sound_when_agent_done: Option, /// Whether to stream edits from the agent as they are received. /// /// Default: false @@ -884,6 +900,10 @@ impl Settings for AssistantSettings { &mut settings.notify_when_agent_waiting, value.notify_when_agent_waiting, ); + merge( + &mut settings.play_sound_when_agent_done, + value.play_sound_when_agent_done, + ); merge(&mut settings.stream_edits, value.stream_edits); merge(&mut settings.single_file_review, value.single_file_review); merge(&mut settings.default_profile, value.default_profile); @@ -1027,6 +1047,7 @@ mod tests { default_view: None, profiles: None, always_allow_tool_actions: None, + play_sound_when_agent_done: None, notify_when_agent_waiting: None, stream_edits: None, single_file_review: None, diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index 55f08c38a1..e7b9a59e8f 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -18,6 +18,7 @@ pub enum Sound { Unmute, StartScreenshare, StopScreenshare, + AgentDone, } impl Sound { @@ -29,6 +30,7 @@ impl Sound { Self::Unmute => "unmute", Self::StartScreenshare => "start_screenshare", Self::StopScreenshare => "stop_screenshare", + Self::AgentDone => "agent_done", } } } diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index eb622bbf8e..cc5c66597d 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -39,9 +39,17 @@ To follow the agent reading through your codebase and performing edits, click on ### Get Notified {#get-notified} -If you send a prompt to the Agent and then move elsewhere, thus putting Zed in the background, a notification will pop up at the top right of your screen indicating that the Agent has completed its work. +If you send a prompt to the Agent and then move elsewhere, thus putting Zed in the background, you can be notified of whether its response is finished either via: -You can customize the notification behavior, including the option to turn it off entirely, by using the `agent.notify_when_agent_waiting` settings key. +- a visual notification that appears in the top right of your screen +- or a sound notification + +You can use both notification methods together or just pick one of them. + +For the visual notification, you can customize its behavior, including the option to turn it off entirely, by using the `agent.notify_when_agent_waiting` settings key. +For the sound notification, turn it on or off using the `agent.play_sound_when_agent_done` settings key. + +#### Sound Notification ### Reviewing Changes {#reviewing-changes}