agent: Add sound notification when done generating (#31472)

Danilo Leal created

This PR adds the ability to hear a sound notification when the agent is
done generating and/or needs user input. This setting is turned off by
default and can be used together with the visual notification. The
specific sound I'm using here comes from the [Material Design 2 Sound
Library](https://m2.material.io/design/sound/sound-resources.html#).

Release Notes:

- agent: Added the ability to have a sound notification when the agent
is done generating and/or needs user input.

Change summary

Cargo.lock                                          |  1 
assets/settings/default.json                        |  7 ++
assets/sounds/agent_done.wav                        |  0 
crates/agent/Cargo.toml                             |  1 
crates/agent/src/active_thread.rs                   | 14 ++++
crates/agent/src/agent_configuration.rs             | 40 +++++++++++++++
crates/assistant_settings/src/assistant_settings.rs | 21 +++++++
crates/audio/src/audio.rs                           |  2 
docs/src/ai/agent-panel.md                          | 12 +++
9 files changed, 93 insertions(+), 5 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -60,6 +60,7 @@ dependencies = [
  "assistant_slash_commands",
  "assistant_tool",
  "async-watch",
+ "audio",
  "buffer_diff",
  "chrono",
  "client",

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": {

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

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<SharedString>,

crates/agent/src/agent_configuration.rs 🔗

@@ -327,6 +327,45 @@ impl AgentConfiguration {
             )
     }
 
+    fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> 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::<AssistantSettings>(
+                                fs.clone(),
+                                cx,
+                                move |settings, _| {
+                                    settings.set_play_sound_when_agent_done(allow);
+                                },
+                            );
+                        }
+                    }),
+            )
+    }
+
     fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> 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(

crates/assistant_settings/src/assistant_settings.rs 🔗

@@ -105,6 +105,7 @@ pub struct AssistantSettings {
     pub profiles: IndexMap<AgentProfileId, AgentProfile>,
     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<LanguageModelParameters>,
@@ -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<NotifyWhenAgentWaiting>,
+    /// 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<bool>,
     /// 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,

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",
         }
     }
 }

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}