Detailed changes
@@ -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
@@ -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,
@@ -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<AgentProfileId, AgentProfileSettings>,
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<LanguageModelParameters>,
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(),
@@ -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,
@@ -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);
}
}
@@ -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;
+}
@@ -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<String, Value>) -> 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(())
+}
@@ -247,6 +247,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
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(
@@ -159,10 +159,10 @@ pub struct AgentSettingsContent {
///
/// Default: "primary_screen"
pub notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
- /// 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<bool>,
+ /// Default: never
+ pub play_sound_when_agent_done: Option<PlaySoundWhenAgentDone>,
/// 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 {
@@ -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| {
@@ -523,6 +523,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::VimInsertModeCursorShape>(render_dropdown)
.add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
.add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
+ .add_basic_renderer::<settings::PlaySoundWhenAgentDone>(render_dropdown)
.add_basic_renderer::<settings::NewThreadLocation>(render_dropdown)
.add_basic_renderer::<settings::ThinkingBlockDisplay>(render_dropdown)
.add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
@@ -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,
@@ -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"
}
}
```