diff --git a/assets/settings/default.json b/assets/settings/default.json index 2e0ddc2da70af5516d14a2fa8418a759bec62eb1..d8286685b502fea9d531d4f631f06c979c985be0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1102,11 +1102,14 @@ // "all_screens" - Show these notifications on all screens // "never" - Never show these notifications "notify_when_agent_waiting": "primary_screen", - // Whether to play a sound when the agent has either completed + // When to play a sound when the agent has either completed // its response, or needs user input. - - // Default: false - "play_sound_when_agent_done": false, + // "never" - Never play the sound + // "when_hidden" - Only play the sound when the agent panel is not visible + // "always" - Always play the sound + // + // Default: never + "play_sound_when_agent_done": "never", // Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. // // Default: true diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index e74b6e4c5ce34383ad7ea702f1ba3a0cfd028455..c67942e5cd3769f814fad62f7311bf7967f3317a 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -563,7 +563,7 @@ mod tests { use crate::tools::{DeletePathTool, EditFileTool, FetchTool, TerminalTool}; use agent_settings::{AgentProfileId, CompiledRegex, InvalidRegexPattern, ToolRules}; use gpui::px; - use settings::{DockPosition, NotifyWhenAgentWaiting}; + use settings::{DockPosition, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone}; use std::sync::Arc; fn test_agent_settings(tool_permissions: ToolPermissions) -> AgentSettings { @@ -584,7 +584,7 @@ mod tests { default_profile: AgentProfileId::default(), profiles: Default::default(), notify_when_agent_waiting: NotifyWhenAgentWaiting::default(), - play_sound_when_agent_done: false, + play_sound_when_agent_done: PlaySoundWhenAgentDone::default(), single_file_review: false, model_parameters: vec![], enable_feedback: false, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 2ef65fe33641cdeca1a77642251523275511e81f..f0730d39eee17cbd544e5ba8574b30f03963c524 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -13,8 +13,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NewThreadLocation, - NotifyWhenAgentWaiting, RegisterSetting, Settings, SettingsContent, SettingsStore, - SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode, + NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, RegisterSetting, Settings, SettingsContent, + SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode, update_settings_file, }; @@ -165,7 +165,7 @@ pub struct AgentSettings { pub profiles: IndexMap, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, - pub play_sound_when_agent_done: bool, + pub play_sound_when_agent_done: PlaySoundWhenAgentDone, pub single_file_review: bool, pub model_parameters: Vec, pub enable_feedback: bool, @@ -618,7 +618,7 @@ impl Settings for AgentSettings { .collect(), notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), - play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), + play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap_or_default(), single_file_review: agent.single_file_review.unwrap(), model_parameters: agent.model_parameters, enable_feedback: agent.enable_feedback.unwrap(), diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 98715056ccec43fb91cc4dc9307cf41d84719fc0..185a54825d3af18f16f2eb30188ea866c099bf32 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -674,7 +674,9 @@ mod tests { use feature_flags::FeatureFlagAppExt; use gpui::{BorrowAppContext, TestAppContext, px}; use project::DisableAiSettings; - use settings::{DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore}; + use settings::{ + DockPosition, NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, Settings, SettingsStore, + }; #[gpui::test] fn test_agent_command_palette_visibility(cx: &mut TestAppContext) { @@ -705,7 +707,7 @@ mod tests { default_profile: AgentProfileId::default(), profiles: Default::default(), notify_when_agent_waiting: NotifyWhenAgentWaiting::default(), - play_sound_when_agent_done: false, + play_sound_when_agent_done: PlaySoundWhenAgentDone::Never, single_file_review: false, model_parameters: vec![], enable_feedback: false, diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 2231f421bc2af0d8038c002a72c226f551f243cc..9b8b3224a420b32b4f534869ded19b3be821c080 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -2340,7 +2340,7 @@ impl ConversationView { .is_some_and(|workspace| AgentPanel::is_visible(&workspace, cx)) }; #[cfg(feature = "audio")] - if settings.play_sound_when_agent_done && !_visible { + if settings.play_sound_when_agent_done.should_play(_visible) { Audio::play_sound(Sound::AgentDone, cx); } } diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index d554ee1dd887d6048f55a584ed2534db944b3c08..bc779908da7542c0bec34f799482929e96362770 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -316,3 +316,9 @@ pub(crate) mod m_2026_03_23 { pub(crate) use keymap::KEYMAP_PATTERNS; } + +pub(crate) mod m_2026_03_30 { + mod settings; + + pub(crate) use settings::make_play_sound_when_agent_done_an_enum; +} diff --git a/crates/migrator/src/migrations/m_2026_03_30/settings.rs b/crates/migrator/src/migrations/m_2026_03_30/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..598941a6212442a4562814d43df6184e4eb76640 --- /dev/null +++ b/crates/migrator/src/migrations/m_2026_03_30/settings.rs @@ -0,0 +1,29 @@ +use anyhow::Result; +use serde_json::Value; + +use crate::migrations::migrate_settings; + +pub fn make_play_sound_when_agent_done_an_enum(value: &mut Value) -> Result<()> { + migrate_settings(value, &mut migrate_one) +} + +fn migrate_one(obj: &mut serde_json::Map) -> Result<()> { + let Some(play_sound) = obj + .get_mut("agent") + .and_then(|agent| agent.as_object_mut()) + .and_then(|agent| agent.get_mut("play_sound_when_agent_done")) + else { + return Ok(()); + }; + + *play_sound = match play_sound { + Value::Bool(true) => Value::String("always".to_string()), + Value::Bool(false) => Value::String("never".to_string()), + Value::String(s) if s == "never" || s == "when_hidden" || s == "always" => return Ok(()), + _ => { + anyhow::bail!("Expected play_sound_when_agent_done to be a boolean or valid enum value") + } + }; + + Ok(()) +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index ceb6ec2e0e35f0dd3bbd23174637bba00baab6b3..136ace8a12c03c831c3eebed97e2f5915ae6afa3 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -247,6 +247,7 @@ pub fn migrate_settings(text: &str) -> Result> { migrations::m_2026_03_16::SETTINGS_PATTERNS, &SETTINGS_QUERY_2026_03_16, ), + MigrationType::Json(migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum), ]; run_migrations(text, migrations) } @@ -2400,6 +2401,132 @@ mod tests { ); } + #[test] + fn test_make_play_sound_when_agent_done_an_enum() { + assert_migrate_with_migrations( + &[MigrationType::Json( + migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum, + )], + &r#"{ }"#.unindent(), + None, + ); + + assert_migrate_with_migrations( + &[MigrationType::Json( + migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum, + )], + &r#"{ + "agent": { + "play_sound_when_agent_done": true + } + }"# + .unindent(), + Some( + &r#"{ + "agent": { + "play_sound_when_agent_done": "always" + } + }"# + .unindent(), + ), + ); + + assert_migrate_with_migrations( + &[MigrationType::Json( + migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum, + )], + &r#"{ + "agent": { + "play_sound_when_agent_done": false + } + }"# + .unindent(), + Some( + &r#"{ + "agent": { + "play_sound_when_agent_done": "never" + } + }"# + .unindent(), + ), + ); + + assert_migrate_with_migrations( + &[MigrationType::Json( + migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum, + )], + &r#"{ + "agent": { + "play_sound_when_agent_done": "when_hidden" + } + }"# + .unindent(), + None, + ); + + // Platform key: settings nested inside "macos" should be migrated + assert_migrate_with_migrations( + &[MigrationType::Json( + migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum, + )], + &r#" + { + "macos": { + "agent": { + "play_sound_when_agent_done": true + } + } + } + "# + .unindent(), + Some( + &r#" + { + "macos": { + "agent": { + "play_sound_when_agent_done": "always" + } + } + } + "# + .unindent(), + ), + ); + + // Profile: settings nested inside profiles should be migrated + assert_migrate_with_migrations( + &[MigrationType::Json( + migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum, + )], + &r#" + { + "profiles": { + "work": { + "agent": { + "play_sound_when_agent_done": false + } + } + } + } + "# + .unindent(), + Some( + &r#" + { + "profiles": { + "work": { + "agent": { + "play_sound_when_agent_done": "never" + } + } + } + } + "# + .unindent(), + ), + ); + } + #[test] fn test_remove_context_server_source() { assert_migrate_settings( diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index dae5c99b9ef9b5b3892b1201ff9a1686330dc365..7ec6a6b5bbdee57cbe75c13d1abe5277ac4f1825 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -159,10 +159,10 @@ pub struct AgentSettingsContent { /// /// Default: "primary_screen" pub notify_when_agent_waiting: Option, - /// Whether to play a sound when the agent has either completed its response, or needs user input. + /// When to play a sound when the agent has either completed its response, or needs user input. /// - /// Default: false - pub play_sound_when_agent_done: Option, + /// Default: never + pub play_sound_when_agent_done: Option, /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. /// /// Default: true @@ -347,6 +347,37 @@ pub enum NotifyWhenAgentWaiting { Never, } +#[derive( + Copy, + Clone, + Default, + Debug, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + PartialEq, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum PlaySoundWhenAgentDone { + #[default] + Never, + WhenHidden, + Always, +} + +impl PlaySoundWhenAgentDone { + pub fn should_play(&self, visible: bool) -> bool { + match self { + PlaySoundWhenAgentDone::Never => false, + PlaySoundWhenAgentDone::WhenHidden => !visible, + PlaySoundWhenAgentDone::Always => true, + } + } +} + #[with_fallible_options] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] pub struct LanguageModelSelection { diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index b6d10424f4a6cf0710a916410e0e6068d80d6064..8496620f9b4db94f93b2ea65952423b73512e724 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -7278,7 +7278,7 @@ fn ai_page(cx: &App) -> SettingsPage { }), SettingsPageItem::SettingItem(SettingItem { title: "Play Sound When Agent Done", - description: "Whether to play a sound when the agent has either completed its response, or needs user input.", + description: "When to play a sound when the agent has either completed its response, or needs user input.", field: Box::new(SettingField { json_path: Some("agent.play_sound_when_agent_done"), pick: |settings_content| { diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 89268b66f4c2f20411358eb63925187c6c3f382d..70aaaa15412793aae54c7c29fe8a2613854c8adb 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -523,6 +523,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index e5713e90df397a01af850af55338897f9d437e55..17ce65bea4f3354bec1efd9b14d1b0ae08a6263f 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -109,7 +109,7 @@ use { image::RgbaImage, project::{AgentId, Project}, project_panel::ProjectPanel, - settings::{NotifyWhenAgentWaiting, Settings as _}, + settings::{NotifyWhenAgentWaiting, PlaySoundWhenAgentDone, Settings as _}, settings_ui::SettingsWindow, std::{ any::Any, @@ -231,7 +231,7 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()> agent_settings::AgentSettings::override_global( agent_settings::AgentSettings { notify_when_agent_waiting: NotifyWhenAgentWaiting::Never, - play_sound_when_agent_done: false, + play_sound_when_agent_done: PlaySoundWhenAgentDone::Never, ..agent_settings::AgentSettings::get_global(cx).clone() }, cx, diff --git a/docs/src/ai/agent-settings.md b/docs/src/ai/agent-settings.md index e1de9fba5e79d56ef73236b2e07c70c93819a2c7..28ee927e4ab4110e6e46a4a8d551093243d72a09 100644 --- a/docs/src/ai/agent-settings.md +++ b/docs/src/ai/agent-settings.md @@ -292,13 +292,16 @@ The default value is `false`. ### Sound Notification -Control whether to hear a notification sound when the agent is done generating changes or needs your input. -The default value is `false`. +Control whether to hear a notification sound when the agent is done generating changes or needs your input. The default value is `never`. + +- `"never"` (default) — Never play the sound. +- `"when_hidden"` — Only play the sound when the agent panel is not visible. +- `"always"` — Always play the sound on completion. ```json [settings] { "agent": { - "play_sound_when_agent_done": true + "play_sound_when_agent_done": "never" } } ```