Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f9587a5
Fix Computer Use companion review findings
lawrencecchen Sep 14, 2026
68840ea
Fix current-main surface provider visibility
lawrencecchen Sep 14, 2026
8a6d5da
fix: restore cloud receipt helpers after recovery revert
austinywang Sep 14, 2026
5d29051
fix: restore Cloud provider ownership after recovery revert
austinywang Sep 14, 2026
aec49e0
Merge current main into Computer Use follow-up
lawrencecchen Sep 14, 2026
b06d37c
Remove duplicate pending mutation helpers
lawrencecchen Sep 14, 2026
7ce2914
fix: stabilize Cloud row view actor isolation
austinywang Sep 14, 2026
c6a685c
Fix Cloud drag test window override
lawrencecchen Sep 14, 2026
dde61c9
Keep Cloud row formatters nonisolated
lawrencecchen Sep 14, 2026
a54a0fd
Fix Cloud hover pointer coordinate conversion
lawrencecchen Sep 14, 2026
10d8f39
Merge latest main into Computer Use follow-up
lawrencecchen Sep 14, 2026
a8ea33d
Merge remote-tracking branch 'origin/main' into fix-computer-use-review
lawrencecchen Sep 15, 2026
9bd572f
test: cover background defaults notification deadlock
lawrencecchen Sep 15, 2026
add1e7c
fix: prevent preference observers from blocking background writers
lawrencecchen Sep 15, 2026
467ea51
test: cover missing window visibility and repeated onboarding layout
lawrencecchen Sep 15, 2026
d036a22
Use main’s scoped Cloud row actor isolation fix
lawrencecchen Sep 15, 2026
b650ddb
fix: preserve offscreen window metadata with raw window IDs
lawrencecchen Sep 15, 2026
5f157e0
test: isolate invalid relay ownership selectors
lawrencecchen Sep 15, 2026
9f68cca
Merge remote-tracking branch 'origin/main' into fix-computer-use-review
lawrencecchen Sep 15, 2026
cd2f743
Merge remote-tracking branch 'origin/main' into fix-computer-use-review
lawrencecchen Sep 15, 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
@@ -0,0 +1,29 @@
public import Foundation

extension NotificationCenter {
/// Observes preference changes on the main actor without blocking background writers.
///
/// Main-thread posts retain synchronous delivery. Background posts schedule
/// the callback and return before it runs. Foundation's `queue: .main`
/// observer blocks the posting thread, which can deadlock a lazy initializer
/// when the main thread is waiting for its value.
///
/// - Parameters:
/// - object: The preferences object to observe, or `nil` for all objects.
/// - handler: The main-actor action that re-reads the current preferences.
/// - Returns: An observer token to remove with `removeObserver(_:)` at teardown.
public func addUserDefaultsObserver(
object: AnyObject? = nil,
using handler: @escaping @MainActor @Sendable () -> Void
) -> any NSObjectProtocol {
// This callback is the Foundation notification boundary. Never make a
// preference writer wait for an operation on the main queue.
addObserver(forName: UserDefaults.didChangeNotification, object: object, queue: nil) { _ in
if Thread.isMainThread {
MainActor.assumeIsolated { handler() }
} else {
Task { @MainActor in handler() }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation
import Testing
@testable import CmuxFoundation

@Suite("Preference notification delivery")
struct UserDefaultsNotificationDeliveryTests {
@Test @MainActor func mainThreadPostsDeliverSynchronously() {
let center = NotificationCenter()
var deliveries = 0
let observer = center.addUserDefaultsObserver { deliveries += 1 }
defer { center.removeObserver(observer) }

center.post(name: UserDefaults.didChangeNotification, object: nil)

#expect(deliveries == 1)
}

@Test(.timeLimit(.minutes(1))) @MainActor
func backgroundWriterReturnsWhileMainThreadIsOccupied() async {
let center = NotificationCenter()
let (deliveries, continuation) = AsyncStream<Void>.makeStream()
let observer = center.addUserDefaultsObserver {
MainActor.preconditionIsolated()
continuation.yield()
}
defer {
center.removeObserver(observer)
continuation.finish()
}
let posted = DispatchSemaphore(value: 0)
Thread.detachNewThread {
center.post(name: UserDefaults.didChangeNotification, object: nil)
posted.signal()
}
// Hold main until the writer returns, with a finite failure deadline.
// A synchronous main-queue observer cannot complete this sequence.
#expect(Self.waitForPostWhileMainThreadIsOccupied(posted))
var iterator = deliveries.makeAsyncIterator()
#expect(await iterator.next() != nil)
}

@MainActor private static func waitForPostWhileMainThreadIsOccupied(_ posted: DispatchSemaphore) -> Bool {
posted.wait(timeout: .now() + 1) == .success
}

@Test @MainActor func objectFilteringAndRemovalRemainEffective() {
let center = NotificationCenter()
let expected = NSObject()
let other = NSObject()
var deliveries = 0
let observer = center.addUserDefaultsObserver(object: expected) { deliveries += 1 }
center.post(name: UserDefaults.didChangeNotification, object: other)
#expect(deliveries == 0)
center.post(name: UserDefaults.didChangeNotification, object: expected)
#expect(deliveries == 1)

center.removeObserver(observer)
center.post(name: UserDefaults.didChangeNotification, object: expected)
#expect(deliveries == 1)
}
}
21 changes: 12 additions & 9 deletions Sources/App/ExternalApplicationWindowTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,14 @@ final class ExternalApplicationWindowTracker {
processIdentifier: pid_t,
primaryScreenMaxY: CGFloat
) -> Snapshot? {
// CGWindowListCreateDescriptionFromArray returns an empty result for a
// valid external window on macOS 26. The including-window query uses
// the same public metadata and returns the requested record reliably.
guard let windowInfo = CGWindowListCopyWindowInfo(
[.optionIncludingWindow, .excludeDesktopElements],
windowID
) as? [[String: Any]] else {
// This API expects pointer-sized window IDs, not boxed CFNumbers.
// Query the tracked ID directly so ordered-out windows retain their
// identity without scanning every window on each sample.
guard windowID != kCGNullWindowID else { return nil }
var rawWindowID = UnsafeRawPointer(bitPattern: UInt(windowID))
guard let windowIDs = CFArrayCreate(kCFAllocatorDefault, &rawWindowID, 1, nil),
let windowInfo = CGWindowListCreateDescriptionFromArray(windowIDs) as? [[String: Any]]
else {
return nil
}
return windowInfo.compactMap {
Expand All @@ -304,7 +305,7 @@ final class ExternalApplicationWindowTracker {
}.first
}

private nonisolated static func snapshot(
nonisolated static func snapshot(
from entry: [String: Any],
expectedWindowID: CGWindowID?,
processIdentifier: pid_t,
Expand Down Expand Up @@ -333,7 +334,9 @@ final class ExternalApplicationWindowTracker {
width: quartzFrame.width,
height: quartzFrame.height
),
isOnScreen: (entry[kCGWindowIsOnscreen as String] as? NSNumber)?.boolValue ?? true
// Missing visibility metadata is ambiguous. Suppress companion
// presentation until WindowServer confirms that the window is on screen.
isOnScreen: (entry[kCGWindowIsOnscreen as String] as? NSNumber)?.boolValue ?? false
Comment thread
lawrencecchen marked this conversation as resolved.
)
}
}
6 changes: 5 additions & 1 deletion Sources/App/ExternalWindowCompanionPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ struct ExternalWindowCompanionPresenter {

func present(_ companionWindow: NSWindow) {
companionWindow.level = .floating
companionWindow.collectionBehavior = [.managed]
// Move to the Space containing System Settings for this first order.
// Clear the transient flag immediately so later app activation cannot
// rehome the companion.
companionWindow.collectionBehavior = [.managed, .moveToActiveSpace]
companionWindow.hidesOnDeactivate = false
orderWindow(companionWindow)
companionWindow.collectionBehavior = [.managed]
}
}
12 changes: 2 additions & 10 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10739,11 +10739,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent

private func installMenuBarVisibilityObserver() {
guard menuBarVisibilityObserver == nil else { return }
menuBarVisibilityObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
menuBarVisibilityObserver = NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
Task { @MainActor [weak self] in
self?.syncApplicationPresentationPreferences()
}
Expand All @@ -10766,11 +10762,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent

private func installMobileHostSettingsObserver() {
guard mobileHostSettingsObserver == nil else { return }
mobileHostSettingsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
mobileHostSettingsObserver = NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
Task { @MainActor [weak self] in
self?.syncMobileHostService()
}
Expand Down
9 changes: 3 additions & 6 deletions Sources/AppearanceSettings.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import SwiftUI
import CmuxTerminalCore
Expand Down Expand Up @@ -272,7 +273,7 @@ enum AppearanceSettings {

final class AppearanceSettingsUserDefaultsObserver {
struct Environment {
let addDefaultsObserver: (@escaping () -> Void) -> NSObjectProtocol
let addDefaultsObserver: (@escaping @MainActor @Sendable () -> Void) -> NSObjectProtocol
let removeObserver: (NSObjectProtocol) -> Void
let currentRawValue: () -> String?
let applyStoredMode: (String?, String) -> AppearanceMode
Expand All @@ -283,11 +284,7 @@ final class AppearanceSettingsUserDefaultsObserver {
) -> Environment {
Environment(
addDefaultsObserver: { handler in
notificationCenter.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { _ in
notificationCenter.addUserDefaultsObserver(object: nil) {
handler()
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxAuthRuntime
import CmuxIrohTransport
import Foundation
Expand Down Expand Up @@ -25,11 +26,7 @@ final class ConnectivityInvalidationSubscriberCoordinator {
func configure(auth: AuthCoordinator) {
self.auth = auth
if defaultsObserver == nil {
defaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: UserDefaults.standard,
queue: .main
) { [weak self] _ in
defaultsObserver = NotificationCenter.default.addUserDefaultsObserver(object: UserDefaults.standard) { [weak self] in
MainActor.assumeIsolated {
self?.evaluate()
}
Expand Down
7 changes: 2 additions & 5 deletions Sources/Cloud/PresenceHeartbeatClient.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import CMUXMobileCore
import CmuxAuthRuntime
import Foundation
Expand Down Expand Up @@ -50,11 +51,7 @@ final class PresenceHeartbeatClient {
// Re-evaluate when the flag or URL flips, so enabling presence in a
// running app starts the loop without a relaunch (and disabling
// stops it and says goodbye).
defaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: UserDefaults.standard,
queue: .main
) { _ in
defaultsObserver = NotificationCenter.default.addUserDefaultsObserver(object: UserDefaults.standard) {
MainActor.assumeIsolated {
PresenceHeartbeatClient.shared.evaluate()
}
Expand Down
6 changes: 1 addition & 5 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11059,11 +11059,7 @@ private final class SidebarTabItemSettingsStore: ObservableObject {
defaults: defaults,
sidebarFontSize: sidebarFontSize
)
defaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
defaultsObserver = NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
Task { @MainActor [weak self] in
self?.refreshSnapshot()
}
Expand Down
7 changes: 2 additions & 5 deletions Sources/KeyboardShortcutSettings.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import Bonsplit
import Carbon
Expand Down Expand Up @@ -1218,11 +1219,7 @@ final class SystemWideHotkeyController {

installHotKeyHandlerIfNeeded()

defaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
defaultsObserver = NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
self?.refreshRegistration()
}
shortcutObserver = NotificationCenter.default.addObserver(
Expand Down
7 changes: 2 additions & 5 deletions Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import Foundation

Expand Down Expand Up @@ -306,11 +307,7 @@ final class BrowserHiddenWebViewDiscardManager {
func installPolicyObserver() {
policyState = BrowserHiddenWebViewDiscardPolicy.resolved(defaults: policyDefaults)
guard policyObserver == nil else { return }
policyObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
policyObserver = NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
Task { @MainActor [weak self] in
self?.handlePolicyDefaultsChanged()
}
Expand Down
6 changes: 1 addition & 5 deletions Sources/Panels/MarkdownPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,7 @@ final class MarkdownPanel: Panel, ObservableObject, FilePreviewTextEditingPanel
/// or a `cmux.json` reload), but only while this viewer still matches the
/// default it was tracking — i.e. the user has not customized it.
private func observeTypographyDefaults() {
typographyDefaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: UserDefaults.standard,
queue: .main
) { [weak self] _ in
typographyDefaultsObserver = NotificationCenter.default.addUserDefaultsObserver(object: UserDefaults.standard) { [weak self] in
Task { @MainActor in
self?.adoptTypographyDefaultsIfFollowing()
}
Expand Down
6 changes: 1 addition & 5 deletions Sources/TabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -726,11 +726,7 @@ class TabManager: ObservableObject {
})

startAgentPIDSweepTimer()
observers.append(NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
observers.append(NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
MainActor.assumeIsolated { [weak self] in
self?.sidebarMetadataSettingsDidChange()
self?.focusHistoryScopeSettingsDidChange()
Expand Down
6 changes: 1 addition & 5 deletions Sources/TerminalNotificationStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -471,11 +471,7 @@ final class TerminalNotificationStore: ObservableObject {
)
}
indexes = Self.buildIndexes(for: notifications)
userDefaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
userDefaultsObserver = NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
Task { @MainActor [weak self] in
self?.refreshDockBadge()
}
Expand Down
18 changes: 3 additions & 15 deletions Sources/Update/UpdateTitlebarAccessory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,7 @@ final class TitlebarControlsLayoutModel {
)

observers.append(
notificationCenter.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
notificationCenter.addUserDefaultsObserver(object: nil) { [weak self] in
MainActor.assumeIsolated {
self?.refreshStyleIfNeeded()
}
Expand Down Expand Up @@ -2040,11 +2036,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
hostingView.layer?.masksToBounds = false
containerView.addSubview(hostingView)

userDefaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
userDefaultsObserver = NotificationCenter.default.addUserDefaultsObserver(object: nil) { [weak self] in
guard let self else { return }
let shouldShow = self.showsWorkspaceTitlebar
let debugSnapshot = MinimalModeTitlebarDebugSettings.snapshot()
Expand Down Expand Up @@ -2865,11 +2857,7 @@ final class UpdateTitlebarAccessoryController {

// Re-evaluate all windows when the presentation mode changes so that
// accessories are removed in minimal mode and re-attached in standard mode.
observers.append(center.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
observers.append(center.addUserDefaultsObserver(object: nil) { [weak self] in
Task { @MainActor [weak self] in
self?.reattachIfPresentationModeChanged()
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/WindowDecorationsController.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import CmuxTestSupport

Expand Down Expand Up @@ -59,7 +60,7 @@ final class WindowDecorationsController {
for name in TitlebarWindowGeometryNotifications.names {
observers.append(center.addObserver(forName: name, object: nil, queue: .main, using: handler))
}
observers.append(center.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { [weak self] _ in
observers.append(center.addUserDefaultsObserver(object: nil) { [weak self] in
self?.applyDefaultsDrivenDecorationChangeIfNeeded()
})
}
Expand Down
6 changes: 1 addition & 5 deletions Sources/WindowToolbarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,7 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate {
}
})

observers.append(center.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
observers.append(center.addUserDefaultsObserver(object: nil) { [weak self] in
Task { @MainActor [weak self] in
self?.updateToolbarVisibilityIfNeeded()
}
Expand Down
Loading