Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6e6baa3
Replace the focus double blink with one short pulse
teamleaderleo Sep 27, 2026
3d08f3b
Don't flash a pane when typing is what dismissed its notification
teamleaderleo Sep 27, 2026
391819a
Add a Double Blink setting for the pane flash
teamleaderleo Sep 27, 2026
910e506
Merge main (9a5f92679b22) into calm-focus-pulse
teamleaderleo Sep 27, 2026
9df5058
Merge remote-tracking branch 'mf/main' into calm-focus-pulse
teamleaderleo Sep 27, 2026
11d41fb
Merge commit '0c151d1c95d83b1b1f7f784eb5a3669a404b499f' into calm-foc…
teamleaderleo Sep 27, 2026
b7c29c5
Merge main into calm-focus-pulse
teamleaderleo Sep 28, 2026
655601a
Pane flash: a 0.6 s pulse, which won the side-by-side vote over 0.4 s
teamleaderleo Sep 28, 2026
d5323c9
Pane flash: follow the terminal theme foreground by default
teamleaderleo Sep 28, 2026
c9b2b7f
Merge main into calm-focus-pulse
teamleaderleo Sep 28, 2026
dd3c839
Test typing and clicking dismissal against a live surface
teamleaderleo Sep 28, 2026
72ef1fd
Merge main into calm-focus-pulse
teamleaderleo Sep 28, 2026
ceeb317
Merge remote-tracking branch 'origin/main' into calm-focus-pulse
teamleaderleo Sep 30, 2026
02898f0
fix: keep double blink as default focus flash
teamleaderleo Sep 30, 2026
662d005
test: assert double blink is the default
teamleaderleo Sep 30, 2026
a811385
feat: make focus flash color and pattern configurable
teamleaderleo Sep 30, 2026
832235f
fix: keep catalog default double blink and expose theme flash color
teamleaderleo Sep 30, 2026
790ea9d
fix localization coverage for machine and session settings
teamleaderleo Sep 30, 2026
e25a80e
Replace the focus double blink with one short pulse
teamleaderleo Sep 27, 2026
e09a396
Don't flash a pane when typing is what dismissed its notification
teamleaderleo Sep 27, 2026
ed5f4b5
Add a Double Blink setting for the pane flash
teamleaderleo Sep 27, 2026
3e8f0e9
Pane flash: a 0.6 s pulse, which won the side-by-side vote over 0.4 s
teamleaderleo Sep 28, 2026
3945e25
Pane flash: follow the terminal theme foreground by default
teamleaderleo Sep 28, 2026
9b5814a
Test typing and clicking dismissal against a live surface
teamleaderleo Sep 28, 2026
c0e48b3
fix: keep double blink as default focus flash
teamleaderleo Sep 30, 2026
72a9f58
test: assert double blink is the default
teamleaderleo Sep 30, 2026
97bce1a
feat: make focus flash color and pattern configurable
teamleaderleo Sep 30, 2026
b69220a
fix: keep catalog default double blink and expose theme flash color
teamleaderleo Sep 30, 2026
8cb5ce2
feat: make typing pane flash configurable
teamleaderleo Oct 1, 2026
481f927
Merge remote-tracking branch 'upstream/main' into land/14894
teamleaderleo Oct 1, 2026
3f87103
feat: expose typing flash in Settings
teamleaderleo Oct 1, 2026
41ebae2
Merge remote-tracking branch 'upstream/calm-focus-pulse' into land/14894
teamleaderleo Oct 1, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -1163,12 +1163,30 @@ enum CmuxEmbeddedConfigSchema {
"default": true,
"description": "Flash the focused pane when requested."
},
"paneFlashDoubleBlink": {
"x-cmux-scopes": ["global"],
"type": "boolean",
"default": true,
"description": "Blink the pane flash twice instead of one short pulse."
},
"paneFlashOnTyping": {
"x-cmux-scopes": ["global"],
"type": "boolean",
"default": true,
"description": "Flash the pane when terminal typing dismisses its notification."
},
"paneFlashThemeColor": {
"x-cmux-scopes": ["global"],
"type": "boolean",
"default": false,
"description": "Use the terminal theme foreground for flashes when paneFlashColor is unset. Unread rings remain cmux blue."
},
"paneFlashColor": {
"x-cmux-scopes": ["global"],
"$ref": "#/$defs/colorHexOrNull",
"default": null,
"descriptionKey": "schemaDescriptions.notifications.paneFlashColor",
"description": "Override the pane flash and unread ring color. Null keeps the built-in blue."
"description": "Override the pane flash and unread ring color. Null flashes in the terminal theme's foreground and keeps unread rings in the cmux accent."
},
"soundWhenFocused": {
"x-cmux-scopes": ["global"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ public enum NotificationDismissalContext: Sendable {
}
}

/// Whether a dismissal in this context flashes the pane.
///
/// - Parameter flashOnTyping: Whether terminal typing should flash the
/// pane after it dismisses a notification.
public func flashesOnDismiss(flashOnTyping: Bool) -> Bool {
switch self {
case .terminalInteraction:
return flashOnTyping
case .activeFocus, .explicitWorkspaceResume, .directInteraction:
return true
}
}

/// Whether a dismissal in this context flashes the pane using the calmer
/// default for terminal typing.
public var flashesOnDismiss: Bool {
flashesOnDismiss(flashOnTyping: false)
}

/// Whether this context may clear a manually-set unread indicator.
public var canDismissManualUnreadIndicator: Bool {
self == .terminalInteraction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public protocol NotificationDismissalHosting: AnyObject {
/// default) the legacy workspace-visibility withdraw is preserved.
var suppressOnlyFocusedSurface: Bool { get }

/// Whether terminal typing should flash the pane after dismissing its
/// notification. The app setting defaults to the legacy flashing behavior;
/// turning it off gives typing the calmer dismissal feedback.
var paneFlashOnTyping: Bool { get }

// MARK: Workspace indicator reads

/// Whether the panel carries a manually-set unread indicator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public final class NotificationDismissalModel: NotificationDismissing {
host.storeClearFocusedReadIndicator(workspaceId: workspaceId, surfaceId: surfaceId)
}
}
if let targetPanelId {
if let targetPanelId, context.flashesOnDismiss(flashOnTyping: host.paneFlashOnTyping) {
if hasUnreadNotification || hasFocusedIndicator {
host.workspaceTriggerNotificationDismissFlash(workspaceId: workspaceId, panelId: targetPanelId)
} else if didDismissUnreadIndicator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ private final class FakeHost: NotificationDismissalHosting {
var focusedPanelIds: [UUID: UUID] = [:]
var focusedSurfaceIds: [UUID: UUID] = [:]
var suppressOnlyFocusedSurface = false
var paneFlashOnTyping = false
var panelIdsBySurface: [UUID: UUID] = [:]
var manualPanelUnread: Set<UUID> = []
var restoredPanelUnread: Set<UUID> = []
Expand Down Expand Up @@ -250,16 +251,35 @@ struct NotificationDismissalModelTests {
#expect(!model.dismissNotificationOnDirectInteraction(workspaceId: workspaceId, surfaceId: panelId))
#expect(host.log.isEmpty)

// Terminal interaction clears it and triggers the indicator flash.
// Terminal interaction clears it without flashing the pane being typed in.
#expect(model.dismissNotificationOnTerminalInteraction(workspaceId: workspaceId, surfaceId: panelId))
let prefix = String(panelId.uuidString.prefix(4))
#expect(host.log == [
"panelClearManualUnread", "storeClearManualUnread",
"storeClearManualUnread:\(prefix)",
"clearFocusedRead:\(prefix)", "unreadIndicatorFlash",
"clearFocusedRead:\(prefix)",
])
}

@Test func typingDismissesAnUnreadNotificationWithoutFlashing() {
let (model, host, workspaceId, panelId) = makeModel()
host.unreadNotificationSurfaces = [panelId]

#expect(model.dismissNotificationOnTerminalInteraction(workspaceId: workspaceId, surfaceId: panelId))
let prefix = String(panelId.uuidString.prefix(4))
#expect(host.log == ["markRead:\(prefix)", "clearFocusedRead:\(prefix)"])
}

@Test func typingCanFlashWhenEnabled() {
let (model, host, workspaceId, panelId) = makeModel()
host.paneFlashOnTyping = true
host.unreadNotificationSurfaces = [panelId]

#expect(model.dismissNotificationOnTerminalInteraction(workspaceId: workspaceId, surfaceId: panelId))
let prefix = String(panelId.uuidString.prefix(4))
#expect(host.log == ["markRead:\(prefix)", "clearFocusedRead:\(prefix)", "notificationFlash"])
}

@Test func restoredUnreadNotClearedByPlainActiveFocus() {
let (model, host, workspaceId, panelId) = makeModel()
host.restoredPanelUnread = [panelId]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ public struct NotificationsCatalogSection: SettingCatalogSection {
userDefaultsKey: "notificationPaneFlashEnabled"
)

/// Blink the pane flash twice instead of one short pulse.
public let paneFlashDoubleBlink = DefaultsKey<Bool>(
id: "notifications.paneFlashDoubleBlink",
defaultValue: true,
userDefaultsKey: "notificationPaneFlashDoubleBlink"
)

/// Flash the pane when terminal typing dismisses its notification.
public let paneFlashOnTyping = DefaultsKey<Bool>(
id: "notifications.paneFlashOnTyping",
defaultValue: true,
userDefaultsKey: "notificationPaneFlashOnTyping"
)

public let paneFlashThemeColor = DefaultsKey<Bool>(
id: "notifications.paneFlashThemeColor",
defaultValue: false,
userDefaultsKey: "notificationPaneFlashThemeColor"
)

/// Stroke color of the attention ring and pane flash, as a `#RRGGBB` hex.
/// Empty (the default) uses the cmux accent (`app.accentColor`).
public let paneFlashColorHex = DefaultsKey<String>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ extension Array where Element == CuratedSettingEntry {
.init(section: .app, id: "show-menu-bar", title: String(localized: "settings.app.showInMenuBar", defaultValue: "Show in Menu Bar"), synonyms: "Show in Menu Bar notifications.showInMenuBar menubar menu bar status item tray extra"),
.init(section: .app, id: "unread-pane-ring", title: String(localized: "settings.notifications.paneRing.title", defaultValue: "Unread Pane Ring"), synonyms: "Unread Pane Ring notifications.unreadPaneRing blue border unread ring notification pane outline"),
.init(section: .app, id: "pane-flash", title: String(localized: "settings.notifications.paneFlash.title", defaultValue: "Pane Flash"), synonyms: "Pane Flash notifications.paneFlash flash blink highlight pane notification pulse"),
.init(section: .app, id: "pane-flash-double-blink", title: String(localized: "settings.notifications.paneFlashDoubleBlink.title", defaultValue: "Double Blink"), synonyms: "Double Blink notifications.paneFlashDoubleBlink pane flash blink twice pulse"),
.init(section: .app, id: "pane-flash-on-typing", title: String(localized: "settings.notifications.paneFlashOnTyping.title", defaultValue: "Flash While Typing"), synonyms: "Flash While Typing notifications.paneFlashOnTyping pane flash typing terminal interaction notification pulse"),
.init(section: .app, id: "pane-flash-theme-color", title: String(localized: "settings.notifications.paneFlashThemeColor.title", defaultValue: "Use Theme Foreground"), synonyms: "Use Theme Foreground notifications.paneFlashThemeColor pane flash terminal theme foreground"),
.init(
section: .app,
id: "agent-permission-prompt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13338,6 +13338,124 @@
}
}
}
},
"settings.notifications.paneFlashThemeColor.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Use the terminal theme foreground for flashes; unread rings stay cmux blue."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Verwende die Vordergrundfarbe des Terminaldesigns für Blitze; ungelesene Ringe bleiben cmux-blau."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Utiliser le premier plan du thème du terminal pour les flashs ; les anneaux non lus restent bleus cmux."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "استخدم مقدمة سمة الطرفية للومضات؛ تبقى حلقات عدم القراءة باللون الأزرق لـ cmux."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Usa el primer plano del tema de la terminal para los destellos; los anillos no leídos siguen siendo azules de cmux."
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "使用終端主題前景色顯示閃爍;未讀環維持 cmux 藍色。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "使用终端主题前景色显示闪烁;未读环保持 cmux 蓝色。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "플래시에 터미널 테마 전경색을 사용하며 읽지 않은 링은 cmux 파란색으로 유지합니다."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "フラッシュにターミナルテーマの前景色を使い、未読リングは cmux ブルーのままにします。"
}
}
}
},
"settings.notifications.paneFlashThemeColor.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Use Theme Foreground"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Vordergrund des Designs verwenden"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Utiliser le premier plan du thème"
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "استخدام مقدمة السمة"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Usar el primer plano del tema"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "使用主題前景色"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "使用主题前景色"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "테마 전경색 사용"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "テーマの前景色を使う"
}
}
}
}
},
"version": "1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public struct AppSection: View {
@State private var showInMenuBar: DefaultsValueModel<Bool>
@State private var paneRing: DefaultsValueModel<Bool>
@State private var paneFlash: DefaultsValueModel<Bool>
@State private var paneFlashDoubleBlink: DefaultsValueModel<Bool>
@State private var paneFlashOnTyping: DefaultsValueModel<Bool>
@State private var paneFlashThemeColor: DefaultsValueModel<Bool>
@State private var desktopNotifications: DesktopNotificationAuthorizationModel
@State private var agentPermissionPrompt: DefaultsValueModel<Bool>
@State private var agentTurnComplete: DefaultsValueModel<String>
Expand Down Expand Up @@ -124,6 +127,9 @@ public struct AppSection: View {
_showInMenuBar = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.showInMenuBar))
_paneRing = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.unreadPaneRing))
_paneFlash = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.paneFlash))
_paneFlashDoubleBlink = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.paneFlashDoubleBlink))
_paneFlashOnTyping = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.paneFlashOnTyping))
_paneFlashThemeColor = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.paneFlashThemeColor))
_desktopNotifications = State(initialValue: DesktopNotificationAuthorizationModel(hostActions: hostActions))
_agentPermissionPrompt = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.agentPermissionPrompt))
_agentTurnComplete = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.agentTurnComplete))
Expand Down Expand Up @@ -167,7 +173,7 @@ public struct AppSection: View {
AppChannelSwitchCard(hostActions: hostActions)
}
.task {
startSettingsObservation([language, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, focusHistoryIncludesPanesAndTabs, equalizeSplitsOnCreate, fileDrop, preferredEditor, openSupported, openMarkdown, globalFontMagnification, markdownFontSize, markdownFontFamily, markdownMaxWidth, canvasPaneGap, canvasSnapping, fileEditorWordWrap, fileEditorSyntaxHighlighting, fileEditorLineNumbers, fileEditorIndentGuides, fileEditorCurrentLineHighlight, fileEditorTabWidth, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, desktopNotifications, agentPermissionPrompt, agentTurnComplete, agentIdleReminder, soundName, soundWhenFocused, soundCommand, customSoundFile, soundOverrides, telemetry, confirmQuit, warnCloseTab, warnCloseX, warnCloseWorkspace, warnCloseWindow, hideCloseButton, renameSelects, paletteAllSurfaces])
startSettingsObservation([language, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, focusHistoryIncludesPanesAndTabs, equalizeSplitsOnCreate, fileDrop, preferredEditor, openSupported, openMarkdown, globalFontMagnification, markdownFontSize, markdownFontFamily, markdownMaxWidth, canvasPaneGap, canvasSnapping, fileEditorWordWrap, fileEditorSyntaxHighlighting, fileEditorLineNumbers, fileEditorIndentGuides, fileEditorCurrentLineHighlight, fileEditorTabWidth, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, paneFlashDoubleBlink, paneFlashOnTyping, paneFlashThemeColor, desktopNotifications, agentPermissionPrompt, agentTurnComplete, agentIdleReminder, soundName, soundWhenFocused, soundCommand, customSoundFile, soundOverrides, telemetry, confirmQuit, warnCloseTab, warnCloseX, warnCloseWorkspace, warnCloseWindow, hideCloseButton, renameSelects, paletteAllSurfaces])
await soundAgentCache.loadIfNeeded { await hostActions.notificationSoundAgentOptions() }
if languageAtAppear == nil { languageAtAppear = language.current }; if telemetryAtAppear == nil { telemetryAtAppear = telemetry.current }
}
Expand Down Expand Up @@ -706,6 +712,43 @@ public struct AppSection: View {
}
SettingsCardDivider()

// Pane Flash: Double Blink
SettingsCardRow(
configurationReview: .json("notifications.paneFlashDoubleBlink"),
String(localized: "settings.notifications.paneFlashDoubleBlink.title", defaultValue: "Double Blink"),
subtitle: String(localized: "settings.notifications.paneFlashDoubleBlink.subtitle", defaultValue: "Blink the pane flash twice instead of one short pulse.")
) {
Toggle("", isOn: Binding(get: { paneFlashDoubleBlink.current }, set: { paneFlashDoubleBlink.set($0) }))
.labelsHidden()
.controlSize(.small)
.disabled(!paneFlash.current)
}
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("notifications.paneFlashOnTyping"),
String(localized: "settings.notifications.paneFlashOnTyping.title", defaultValue: "Flash While Typing"),
subtitle: String(localized: "settings.notifications.paneFlashOnTyping.subtitle", defaultValue: "Flash the pane when typing dismisses its notification.")
) {
Toggle("", isOn: Binding(get: { paneFlashOnTyping.current }, set: { paneFlashOnTyping.set($0) }))
.labelsHidden()
.controlSize(.small)
.disabled(!paneFlash.current)
}
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("notifications.paneFlashThemeColor"),
String(localized: "settings.notifications.paneFlashThemeColor.title", defaultValue: "Use Theme Foreground"),
subtitle: String(localized: "settings.notifications.paneFlashThemeColor.subtitle", defaultValue: "Use the terminal theme foreground for flashes; unread rings stay cmux blue.")
) {
Toggle("", isOn: Binding(get: { paneFlashThemeColor.current }, set: { paneFlashThemeColor.set($0) }))
.labelsHidden()
.controlSize(.small)
.disabled(!paneFlash.current)
}
SettingsCardDivider()

// Agent: Needs Permission
SettingsCardRow(
configurationReview: .json("notifications.agentPermissionPrompt"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ struct SettingsRowAnchorResolutionTests {
"notifications.command",
"notifications.dockBadge",
"notifications.paneFlash",
"notifications.paneFlashDoubleBlink",
"notifications.paneFlashOnTyping",
"notifications.paneFlashThemeColor",
"notifications.paneFlashColor",
"notifications.showInMenuBar",
"notifications.sound",
Expand Down
Loading
Loading