Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
22 changes: 21 additions & 1 deletion Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let closeLabel = contextMenuLabel(
multi: String(localized: "contextMenu.closeWorkspaces", defaultValue: "Close Workspaces"),
single: String(localized: "contextMenu.closeWorkspace", defaultValue: "Close Workspace"),
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions Sources/SessionPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions Sources/TerminalNotificationStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -915,7 +922,7 @@ final class TerminalNotificationStore: ObservableObject {
}

if WorkspaceAutoReorderSettings.isEnabled() {
AppDelegate.shared?.tabManager?.moveTabToTopForNotification(tabId)
tabManager?.moveTabToTopForNotification(tabId)
}

let notification = TerminalNotification(
Expand Down
3 changes: 3 additions & 0 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ extension Workspace {
customTitle: customTitle,
customColor: customColor,
isPinned: isPinned,
isMuted: isMuted,
currentDirectory: currentDirectory,
focusedPanelId: focusedPanelId,
layout: layout,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down