Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
dbba283
test: reproduce sidebar snapshots surviving workspace replacement
austinywang Sep 16, 2026
fee5924
fix: isolate sidebar discovery and bound workspace snapshot lifetime
austinywang Sep 16, 2026
ad3672a
fix: retain the FSEvents receiver through native registration
austinywang Sep 16, 2026
ccfe1cd
Merge remote-tracking branch 'origin/main' into issue-12791-sidebar-r…
austinywang Sep 16, 2026
d5464d0
test: verify native callback lifetime through queued teardown
austinywang Sep 16, 2026
f165b33
test: split nested Cloud assertion to unblock hosted suites
austinywang Sep 17, 2026
50b7233
fix: restore pairing preparation recovery lost in upstream merge
austinywang Sep 17, 2026
8d16430
Merge remote-tracking branch 'origin/main' into issue-12791-sidebar-r…
austinywang Sep 17, 2026
2e683ce
test: import Cloud fixture remote configuration from its owning module
austinywang Sep 17, 2026
0bffb5f
fix: construct sidebar snapshots only at lifecycle and event boundaries
austinywang Sep 17, 2026
9ee3bb1
test: preserve Git metadata monitoring when watcher replacement fails
austinywang Sep 19, 2026
79e6a1a
fix: continue issue 12791 rendering stall repairs
austinywang Sep 19, 2026
f9f67f4
merge origin/main into issue-12791-sidebar-rendering-stalls
austinywang Sep 19, 2026
642c86a
fix: scope browser import automation helper
austinywang Sep 19, 2026
d790f65
test: reproduce installed watcher loss on descriptor failure
austinywang Sep 19, 2026
3ee91b2
fix: preserve watcher on failed discovery and browser automation cont…
austinywang Sep 19, 2026
bb48a38
test: inject browser import coordinator in wizard coverage
austinywang Sep 20, 2026
c9de46c
merge origin/main into issue-12791-sidebar-rendering-stalls
austinywang Sep 20, 2026
683ca09
fix: close browser import review findings
austinywang Sep 20, 2026
511d198
fix: import process identity from CmuxFoundation
austinywang Sep 20, 2026
4a6a7df
test: isolate shared watcher lifecycle fixture
austinywang Sep 20, 2026
a73b116
merge origin/main into issue-12791-sidebar-rendering-stalls
austinywang Sep 21, 2026
4d4c8ea
refactor: move browser destination resolution into CmuxBrowser
austinywang Sep 21, 2026
110d737
fix: expose Foundation UUID in browser resolver API
austinywang Sep 21, 2026
1d9afef
fix: instantiate browser destination resolver adapter
austinywang Sep 21, 2026
fa8e093
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 21, 2026
de85594
fix: handle blank browser import identifiers
austinywang Sep 21, 2026
4092d91
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 21, 2026
13d30fa
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 21, 2026
d386ce0
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 21, 2026
8662152
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 21, 2026
264c7da
test: pump common run loop modes for sidebar harness
austinywang Sep 21, 2026
6734ee9
merge: sync latest main
austinywang Sep 21, 2026
414e47f
chore: normalize merged source whitespace
austinywang Sep 21, 2026
a71b3dc
fix: retain initial sidebar snapshot observation
austinywang Sep 21, 2026
634509f
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 21, 2026
ec6a650
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 22, 2026
460950d
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 22, 2026
45e344c
merge: preserve import coordinator while syncing current main
austinywang Sep 25, 2026
c473752
test: keep sidebar regression on the standard run loop
austinywang Sep 25, 2026
91d432e
ci: execute sidebar Git watcher regression tests
austinywang Sep 25, 2026
6a90db3
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 25, 2026
5b4b943
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 25, 2026
da813a5
Wire AgentChatProseStreamWakeDriver.swift into the app target
teamleaderleo Sep 25, 2026
1d9676d
test: align remote Git metadata with directory trust policy
austinywang Sep 25, 2026
d4ca913
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 25, 2026
e5441eb
fix: preserve sidebar cache app target wiring
austinywang Sep 25, 2026
465ccf3
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 25, 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
1 change: 1 addition & 0 deletions .github/workflows/ci-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,7 @@ jobs:
CmuxNotifications
CmuxSettings
CmuxSettingsUI
CmuxSidebarGit
CmuxSurfaceCatalogModel
CmuxSudoBroker
CmuxSudoBrokerUI
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
internal import Dispatch

/// Coalesces browser discovery and isolates synchronous system and profile reads.
public actor BrowserInstalledBrowserDetectionService {
private let scan: @Sendable () -> [InstalledBrowserCandidate]
// Foundation/Launch Services discovery has no asynchronous API. This serial
// I/O lane prevents a blocked scan from occupying the UI or cooperative pool.
private let queue = DispatchQueue(label: "com.cmux.browser-discovery", qos: .utility)
private var inFlight: Task<[InstalledBrowserCandidate], Never>?

/// Creates a discovery owner.
///
/// - Parameter scan: Synchronous discovery performed on the I/O lane. Tests
/// provide a fixture-backed scanner; the default scans the current Mac.
public init(scan: @escaping @Sendable () -> [InstalledBrowserCandidate] = {
BrowserInstalledBrowserDetector().detectInstalledBrowsers()
}) {
self.scan = scan
}

/// Returns a fresh discovery snapshot, sharing any scan already in progress.
///
/// Cancellation does not interrupt an in-progress Foundation call. The one
/// scan remains owned until it completes; callers discard canceled results.
/// - Returns: Ranked installed browsers and their importable profiles.
public func detectInstalledBrowsers() async -> [InstalledBrowserCandidate] {
if let inFlight { return await inFlight.value }
let scan = scan
let queue = queue
let task = Task {
await withCheckedContinuation { continuation in
queue.async { continuation.resume(returning: scan()) }
}
}
inFlight = task
let result = await task.value
inFlight = nil
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
public import Foundation

/// Resolves browser-import destination selectors without touching app storage.
public struct BrowserImportDestinationResolver: Sendable {
/// The result of resolving a destination selector.
public enum Resolution: Equatable, Sendable {
/// No destination selector was supplied.
case none
/// An existing profile was selected by its stable identifier.
case matched(UUID)
/// More than one profile matched the display name or slug.
case ambiguous([BrowserProfileDefinition])
/// No profile matched and creation was not requested.
case notFound(String)
/// The UUID-only selector was malformed or unknown.
case invalidIdentifier(String)
/// The caller requested creation after no existing profile matched.
case create(String)
}

/// Creates a stateless destination resolver.
public init() {}

/// Resolves an optional UUID or name/slug selector against profiles.
///
/// - Parameters:
/// - rawSelector: A display name or slug, when supplied.
/// - rawIdentifier: The UUID-only selector, when supplied.
/// - createIfMissing: Whether a missing name may create a profile.
/// - profiles: The current profile definitions.
/// - Returns: A deterministic resolution result; no app state is mutated.
public func resolve(
rawSelector: String?,
rawIdentifier: String?,
createIfMissing: Bool,
profiles: [BrowserProfileDefinition]
) -> Resolution {
let identifier = rawIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines)
if let identifier, !identifier.isEmpty {
guard let id = UUID(uuidString: identifier),
profiles.contains(where: { $0.id == id }) else {
return .invalidIdentifier(identifier)
}
return .matched(id)
}

guard let rawSelector else { return .none }
let selector = rawSelector.trimmingCharacters(in: .whitespacesAndNewlines)
guard !selector.isEmpty else { return .notFound(selector) }
if let id = UUID(uuidString: selector) {
return profiles.contains(where: { $0.id == id })
? .matched(id)
: .invalidIdentifier(selector)
}
let matches = profiles.filter {
$0.displayName.localizedCaseInsensitiveCompare(selector) == .orderedSame
|| $0.slug.localizedCaseInsensitiveCompare(selector) == .orderedSame
}
if matches.count == 1 { return .matched(matches[0].id) }
if matches.count > 1 { return .ambiguous(matches) }
return createIfMissing ? .create(selector) : .notFound(selector)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import Testing
@testable import CmuxBrowser

@Suite("Browser import destination resolver")
struct BrowserImportDestinationResolverTests {
@Test func UUIDSelectionWinsAndAmbiguousNamesFailClosed() throws {
let profiles = [
BrowserProfileDefinition(id: UUID(), displayName: "Shared", createdAt: .distantPast, isBuiltInDefault: false),
BrowserProfileDefinition(id: UUID(), displayName: "shared", createdAt: .distantPast, isBuiltInDefault: false),
]
let resolver = BrowserImportDestinationResolver()
#expect(resolver.resolve(rawSelector: "SHARED", rawIdentifier: nil, createIfMissing: false, profiles: profiles) == .ambiguous(profiles))
#expect(resolver.resolve(rawSelector: "Shared", rawIdentifier: profiles[0].id.uuidString, createIfMissing: false, profiles: profiles) == .matched(profiles[0].id))
#expect(resolver.resolve(rawSelector: "new", rawIdentifier: nil, createIfMissing: true, profiles: profiles) == .create("new"))
}

@Test func blankIdentifierFallsBackToNameSelector() {
let profile = BrowserProfileDefinition(
id: UUID(), displayName: "Work", createdAt: .distantPast, isBuiltInDefault: false
)
let resolver = BrowserImportDestinationResolver()

#expect(resolver.resolve(
rawSelector: "work",
rawIdentifier: " ",
createIfMissing: false,
profiles: [profile]
) == .matched(profile.id))
#expect(resolver.resolve(
rawSelector: nil,
rawIdentifier: "\t",
createIfMissing: false,
profiles: [profile]
) == .none)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Darwin
import Dispatch
import Foundation
import Testing
@testable import CmuxBrowser

@Suite("Browser discovery executor")
struct BrowserInstalledBrowserDetectorAsyncTests {
@MainActor
@Test("real profile discovery leaves the UI actor and preserves injected paths")
func offMainDetectionPreservesResults() async throws {
let home = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-browser-import-async-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager.default.removeItem(at: home) }
let profile = home.appendingPathComponent(
"Library/Application Support/Google/Chrome/Default", isDirectory: true
)
try FileManager.default.createDirectory(at: profile, withIntermediateDirectories: true)
let marker = Data(UUID().uuidString.utf8)
try marker.write(to: profile.appendingPathComponent("History"))

let service = BrowserInstalledBrowserDetectionService {
#expect(pthread_main_np() == 0)
return BrowserInstalledBrowserDetector(
homeDirectoryURL: home,
bundleLookup: { _ in
#expect(pthread_main_np() == 0)
return nil
},
applicationSearchDirectories: []
).detectInstalledBrowsers()
}
let candidates = await service.detectInstalledBrowsers()
let chrome = try #require(candidates.first { $0.id == "google-chrome" })
let foundProfile = try #require(chrome.profiles.first)
#expect(chrome.homeDirectoryURL == home)
#expect(foundProfile.rootURL.lastPathComponent == "Default")
#expect(try Data(contentsOf: foundProfile.rootURL.appendingPathComponent("History")) == marker)
}

@MainActor
@Test("a blocked system scan leaves the main actor runnable")
func slowScanDoesNotBlockMainActor() async {
let (started, continuation) = AsyncStream<Void>.makeStream()
// This semaphore deliberately simulates a blocking system call on the
// I/O lane. It is never waited on by the test's main actor.
let release = DispatchSemaphore(value: 0)
defer { release.signal(); continuation.finish() }
let service = BrowserInstalledBrowserDetectionService {
continuation.yield(())
release.wait()
return []
}
let discovery = Task { await service.detectInstalledBrowsers() }
var iterator = started.makeAsyncIterator()
_ = await iterator.next()
#expect(pthread_main_np() != 0)
release.signal()
#expect(await discovery.value.isEmpty)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import CoreServices
import Foundation

/// Immutable callback context retained by FSEvents until queued teardown completes.
final class FileSystemEventReceiver: Sendable {
let onEvent: @Sendable (FileSystemEventBatch) -> Void

init(onEvent: @escaping @Sendable (FileSystemEventBatch) -> Void) {
self.onEvent = onEvent
}

func makeContext() -> FSEventStreamContext {
FSEventStreamContext(
version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: { pointer in
guard let pointer else { return nil }
_ = Unmanaged<FileSystemEventReceiver>.fromOpaque(pointer).retain()
return pointer
},
release: { pointer in
guard let pointer else { return }
Unmanaged<FileSystemEventReceiver>.fromOpaque(pointer).release()
},
copyDescription: nil
)
}
}
Loading
Loading