diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 14de005d463f..78603272dfc1 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -29825,6 +29825,40 @@ } } }, + "contextMenu.muteNotifications": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mute Notifications" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "通知をミュート" + } + } + } + }, + "contextMenu.unmuteNotifications": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Unmute Notifications" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "通知のミュートを解除" + } + } + } + }, "contextMenu.workspaceColor": { "extractionState": "manual", "localizations": { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 9065d5971758..1f28d5321202 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -10973,7 +10973,8 @@ private struct TabItemView: View, Equatable { lhs.showsModifierShortcutHints == rhs.showsModifierShortcutHints && lhs.remoteContextMenuWorkspaceIds == rhs.remoteContextMenuWorkspaceIds && lhs.allRemoteContextMenuTargetsConnecting == rhs.allRemoteContextMenuTargetsConnecting && - lhs.allRemoteContextMenuTargetsDisconnected == rhs.allRemoteContextMenuTargetsDisconnected + lhs.allRemoteContextMenuTargetsDisconnected == rhs.allRemoteContextMenuTargetsDisconnected && + lhs.tab.isMuted == rhs.tab.isMuted } // Use plain references instead of @EnvironmentObject to avoid subscribing @@ -11283,6 +11284,13 @@ private struct TabItemView: View, Equatable { .safeHelp(protectedWorkspaceTooltip) } + if tab.isMuted { + Image(systemName: "bell.slash.fill") + .font(.system(size: 9, weight: .semibold)) + .foregroundColor(activeSecondaryColor(0.8)) + .safeHelp(String(localized: "sidebar.mutedWorkspace.tooltip", defaultValue: "Notifications muted")) + } + Text(tab.title) .font(.system(size: 12.5, weight: titleFontWeight)) .foregroundColor(activePrimaryTextColor) @@ -11634,6 +11642,10 @@ private struct TabItemView: View, Equatable { multi: String(localized: "contextMenu.unpinWorkspaces", defaultValue: "Unpin Workspaces"), single: String(localized: "contextMenu.unpinWorkspace", defaultValue: "Unpin Workspace"), isMulti: isMulti) + let shouldMute = !tab.isMuted + let muteLabel = shouldMute + ? String(localized: "contextMenu.muteNotifications", defaultValue: "Mute Notifications") + : String(localized: "contextMenu.unmuteNotifications", defaultValue: "Unmute Notifications") let closeLabel = contextMenuLabel( multi: String(localized: "contextMenu.closeWorkspaces", defaultValue: "Close Workspaces"), single: String(localized: "contextMenu.closeWorkspace", defaultValue: "Close Workspace"), @@ -11657,6 +11669,14 @@ private struct TabItemView: View, Equatable { syncSelectionAfterMutation() } + Button(muteLabel) { + for id in targetIds { + if let workspace = tabManager.tabs.first(where: { $0.id == id }) { + workspace.isMuted = shouldMute + } + } + } + if let key = renameWorkspaceShortcut.keyEquivalent { Button(String(localized: "contextMenu.renameWorkspace", defaultValue: "Rename Workspace…")) { promptRename() diff --git a/Sources/SessionPersistence.swift b/Sources/SessionPersistence.swift index 188833d40298..a08bb4aad1a3 100644 --- a/Sources/SessionPersistence.swift +++ b/Sources/SessionPersistence.swift @@ -332,6 +332,7 @@ struct SessionWorkspaceSnapshot: Codable, Sendable { var customTitle: String? var customColor: String? var isPinned: Bool + var isMuted: Bool? var currentDirectory: String var focusedPanelId: UUID? var layout: SessionWorkspaceLayoutSnapshot diff --git a/Sources/TerminalNotificationStore.swift b/Sources/TerminalNotificationStore.swift index 19a695bc52cf..69e63c21ecab 100644 --- a/Sources/TerminalNotificationStore.swift +++ b/Sources/TerminalNotificationStore.swift @@ -891,6 +891,12 @@ final class TerminalNotificationStore: ObservableObject { } func addNotification(tabId: UUID, surfaceId: UUID?, title: String, subtitle: String, body: String) { + // Skip all notification activity when the workspace has been muted by the user. + // Use tabManagerFor(tabId:) to search across all windows, not just the key window's tabManager. + if AppDelegate.shared?.tabManagerFor(tabId: tabId)?.tabs.first(where: { $0.id == tabId })?.isMuted == true { + return + } + var updated = notifications var idsToClear: [String] = [] updated.removeAll { existing in @@ -904,8 +910,9 @@ final class TerminalNotificationStore: ObservableObject { focusedReadIndicatorByTabId.removeValue(forKey: tabId) } - let isActiveTab = AppDelegate.shared?.tabManager?.selectedTabId == tabId - let focusedSurfaceId = AppDelegate.shared?.tabManager?.focusedSurfaceId(for: tabId) + let tabManager = AppDelegate.shared?.tabManagerFor(tabId: tabId) + let isActiveTab = tabManager?.selectedTabId == tabId + let focusedSurfaceId = tabManager?.focusedSurfaceId(for: tabId) let isFocusedSurface = surfaceId == nil || focusedSurfaceId == surfaceId let isFocusedPanel = isActiveTab && isFocusedSurface let isAppFocused = AppFocusState.isAppFocused() @@ -915,7 +922,7 @@ final class TerminalNotificationStore: ObservableObject { } if WorkspaceAutoReorderSettings.isEnabled() { - AppDelegate.shared?.tabManager?.moveTabToTopForNotification(tabId) + tabManager?.moveTabToTopForNotification(tabId) } let notification = TerminalNotification( diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 8a231de4d280..90e008619872 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -241,6 +241,7 @@ extension Workspace { customTitle: customTitle, customColor: customColor, isPinned: isPinned, + isMuted: isMuted, currentDirectory: currentDirectory, focusedPanelId: focusedPanelId, layout: layout, @@ -280,6 +281,7 @@ extension Workspace { setCustomTitle(snapshot.customTitle) setCustomColor(snapshot.customColor) isPinned = snapshot.isPinned + isMuted = snapshot.isMuted ?? false // Status entries and agent PIDs are ephemeral runtime state tied to running // processes (e.g. claude_code "Running"). Don't restore them across app @@ -5359,6 +5361,7 @@ final class Workspace: Identifiable, ObservableObject { @Published var title: String @Published var customTitle: String? @Published var isPinned: Bool = false + @Published var isMuted: Bool = false @Published var customColor: String? // hex string, e.g. "#C0392B" @Published var currentDirectory: String private(set) var preferredBrowserProfileID: UUID?