Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fc602cd
Add configurable browser engine selection
austinywang Jul 15, 2026
29423c6
Merge remote-tracking branch 'origin/main' into feat-browser-engine-c…
austinywang Jul 15, 2026
799dcfc
Bound Chromium engine resource lifecycles
austinywang Jul 15, 2026
2c2700d
Test saturated Chromium input ordering
austinywang Jul 15, 2026
a3efcdb
Complete Chromium engine automation parity
austinywang Jul 15, 2026
8d0d2a0
Test browser engine preference on new tabs
austinywang Jul 15, 2026
1857d3c
Respect browser engine preference for new tabs
austinywang Jul 15, 2026
608a6c0
Test Chromium navigation request rejection
austinywang Jul 15, 2026
c482722
Reject unsupported Chromium navigation requests
austinywang Jul 15, 2026
5a1e9f0
Test Chromium composition and paste input
austinywang Jul 15, 2026
c82056e
Complete Chromium viewport input harness
austinywang Jul 15, 2026
b9b654a
Support Chromium composition and paste input
austinywang Jul 15, 2026
4ae8917
Test Chromium screencast and cleanup safeguards
austinywang Jul 15, 2026
2ebefdb
Bound Chromium viewport resource usage
austinywang Jul 15, 2026
470db8f
Test Chromium topology and drag input
austinywang Jul 15, 2026
5ddc905
Report Chromium topology and drag input correctly
austinywang Jul 15, 2026
e2bd4cb
Test Chromium input backpressure isolation
austinywang Jul 15, 2026
7624c3f
Keep Chromium alive under input backpressure
austinywang Jul 15, 2026
36dabc2
Test Chromium zoom and live title updates
austinywang Jul 15, 2026
2511ed6
Test Chromium URL restoration on engine downgrade
austinywang Jul 15, 2026
016aa3f
Fix Chromium live title and zoom state
austinywang Jul 15, 2026
416e8c2
Merge remote-tracking branch 'origin/main' into feat-browser-engine-c…
austinywang Jul 15, 2026
986bd39
Test WebKit async JavaScript evaluation
austinywang Jul 15, 2026
a673dcd
Await WebKit browser evaluation promises
austinywang Jul 15, 2026
4b92120
Test Chromium engine policy invariants
austinywang Jul 15, 2026
478bc00
Fix Chromium engine policy invariants
austinywang Jul 15, 2026
322a2d9
Test Chromium pre-commit navigation policy
austinywang Jul 15, 2026
67f6e33
Enforce Chromium navigation policy before commit
austinywang Jul 15, 2026
73a1deb
Test Chromium JavaScript exception reporting
austinywang Jul 15, 2026
e4ad3a9
Preserve Chromium JavaScript exception text
austinywang Jul 15, 2026
e87c303
Test Chromium history completion tracking
austinywang Jul 15, 2026
49d45af
Record Chromium history on navigation completion
austinywang Jul 15, 2026
817a820
Test bounded Chromium process termination
austinywang Jul 15, 2026
8311654
Bound Chromium process termination
austinywang Jul 15, 2026
c68f744
Align browser engine code with architecture policy
austinywang Jul 15, 2026
20359a1
Test browser engine event and script regressions
austinywang Jul 15, 2026
b0d6af7
Address browser engine review feedback
austinywang Jul 15, 2026
130f393
Merge origin/main into feat-browser-engine-choice
austinywang Jul 15, 2026
8ec926e
Test bounded Chromium control event pressure
austinywang Jul 15, 2026
d12a9e9
Bound Chromium control event buffering
austinywang Jul 15, 2026
4b934cb
Test Chromium main-frame loading state
austinywang Jul 15, 2026
5bd81fd
Track Chromium main-frame loading state
austinywang Jul 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
2 changes: 2 additions & 0 deletions Packages/macOS/CmuxBrowser/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ let package = Package(
),
],
dependencies: [
.package(path: "../CmuxCore"),
.package(path: "../../../vendor/bonsplit"),
],
targets: [
.target(
name: "CmuxBrowser",
dependencies: [
.product(name: "CmuxCore", package: "CmuxCore"),
.product(name: "Bonsplit", package: "bonsplit"),
],
swiftSettings: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
public import Foundation

/// An installed browser application that can host a Chromium engine process.
public struct BrowserApplication: Equatable, Sendable {
/// The application's bundle identifier.
public let bundleIdentifier: String

/// The URL of the application bundle.
public let bundleURL: URL

/// The executable launched for Chrome DevTools Protocol control.
public let executableURL: URL

/// Creates an installed-browser description.
///
/// - Parameters:
/// - bundleIdentifier: The application's bundle identifier.
/// - bundleURL: The application bundle URL.
/// - executableURL: The executable inside the application bundle.
public init(bundleIdentifier: String, bundleURL: URL, executableURL: URL) {
self.bundleIdentifier = bundleIdentifier
self.bundleURL = bundleURL
self.executableURL = executableURL
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Supplies LaunchServices browser-handler and installed-application information.
@MainActor
public protocol BrowserApplicationProviding: AnyObject {
/// Returns the applications LaunchServices selects for representative HTTPS and HTTP URLs.
func defaultBrowserApplications() -> [BrowserApplication]

/// Returns installed Chromium-family applications that cmux knows how to launch.
func installedChromiumApplications() -> [BrowserApplication]
}
Comment on lines +2 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Make LaunchServices queries asynchronous to avoid blocking the main thread.

LaunchServices queries (such as resolving default browsers or installed applications) involve IPC and can occasionally hang or block. As per coding guidelines, do not add expensive synchronous syscalls to the main actor or latency-sensitive interactive paths.

Consider making these requirements async (and the protocol Sendable/nonisolated) so that implementations can perform the queries on a background actor, preventing potential UI freezes when creating a new browser pane.

♻️ Proposed refactor
-@MainActor
-public protocol BrowserApplicationProviding: AnyObject {
+public protocol BrowserApplicationProviding: AnyObject, Sendable {
     /// Returns the applications LaunchServices selects for representative HTTPS and HTTP URLs.
-    func defaultBrowserApplications() -> [BrowserApplication]
+    func defaultBrowserApplications() async -> [BrowserApplication]
 
     /// Returns installed Chromium-family applications that cmux knows how to launch.
-    func installedChromiumApplications() -> [BrowserApplication]
+    func installedChromiumApplications() async -> [BrowserApplication]
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@MainActor
public protocol BrowserApplicationProviding: AnyObject {
/// Returns the applications LaunchServices selects for representative HTTPS and HTTP URLs.
func defaultBrowserApplications() -> [BrowserApplication]
/// Returns installed Chromium-family applications that cmux knows how to launch.
func installedChromiumApplications() -> [BrowserApplication]
}
public protocol BrowserApplicationProviding: AnyObject, Sendable {
/// Returns the applications LaunchServices selects for representative HTTPS and HTTP URLs.
func defaultBrowserApplications() async -> [BrowserApplication]
/// Returns installed Chromium-family applications that cmux knows how to launch.
func installedChromiumApplications() async -> [BrowserApplication]
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Engine/BrowserApplicationProviding.swift`
around lines 2 - 9, Make the LaunchServices methods defaultBrowserApplications()
and installedChromiumApplications() asynchronous, and update
BrowserApplicationProviding isolation/conformance as needed so implementations
can execute queries off the main actor. Propagate async changes through all
conforming implementations and callers, including browser-pane creation, while
preserving the existing returned application results.

Source: Coding guidelines

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
public import Foundation

/// A browser cookie copied across the engine-neutral automation boundary.
///
/// Use this value when reading or mutating cookies without depending on WebKit
/// or Chrome DevTools Protocol types.
public struct BrowserEngineCookie: Hashable, Sendable {
/// The cookie name.
public let name: String

/// The cookie value.
public let value: String

/// The host or domain scope, including a leading dot when supplied by the engine.
public let domain: String

/// The URL path scope.
public let path: String

/// Whether the cookie is restricted to secure transports.
public let isSecure: Bool

/// Whether page JavaScript is prevented from reading the cookie.
public let isHTTPOnly: Bool

/// The expiration time, or `nil` for a session cookie.
public let expiresDate: Date?

/// Whether the cookie expires with its browser-engine session.
public var isSessionOnly: Bool { expiresDate == nil }

/// Creates an engine-neutral cookie.
///
/// - Parameters:
/// - name: The cookie name.
/// - value: The cookie value.
/// - domain: The host or domain scope.
/// - path: The URL path scope. Defaults to `/`.
/// - isSecure: Whether the cookie requires a secure transport.
/// - isHTTPOnly: Whether page JavaScript is prevented from reading the cookie.
/// - expiresDate: The expiration time, or `nil` for a session cookie.
public init(
name: String,
value: String,
domain: String,
path: String = "/",
isSecure: Bool = false,
isHTTPOnly: Bool = false,
expiresDate: Date? = nil
) {
self.name = name
self.value = value
self.domain = domain
self.path = path
self.isSecure = isSecure
self.isHTTPOnly = isHTTPOnly
self.expiresDate = expiresDate
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// A pre-commit decision for an engine-originated browser navigation.
public enum BrowserEngineNavigationDecision: Sendable {
/// Allow the browser engine to continue the original navigation.
case allow

/// Cancel the browser engine's original navigation.
case cancel
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// The cmux destination requested by an engine-originated browser navigation.
public enum BrowserEngineNavigationDisposition: Equatable, Sendable {
/// Continue navigation in the current browser pane.
case currentTab

/// Route a new-window request into a cmux browser tab.
case newTab
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Evaluates an engine-originated navigation before the browser commits it.
public typealias BrowserEngineNavigationPolicyHandler = @MainActor @Sendable (
BrowserEngineNavigationRequest
) -> BrowserEngineNavigationDecision
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public import Foundation

/// An engine-neutral navigation request awaiting cmux policy evaluation.
public struct BrowserEngineNavigationRequest: Sendable {
/// The original browser request represented by the engine event.
public let request: URLRequest

/// The cmux destination requested by the page.
public let disposition: BrowserEngineNavigationDisposition

/// Creates a navigation request for policy evaluation.
///
/// - Parameters:
/// - request: The original request represented by the browser engine.
/// - disposition: The current-pane or new-tab destination requested by the page.
public init(
request: URLRequest,
disposition: BrowserEngineNavigationDisposition
) {
self.request = request
self.disposition = disposition
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
public import CmuxCore
import Foundation

/// Resolves a browser-engine preference against LaunchServices handler identities.
public struct BrowserEngineResolver: Sendable {
/// Creates an engine resolver.
public init() {}

/// Resolves the effective engine.
///
/// Handler identifiers should be ordered by preference, normally HTTPS then
/// HTTP. Explicit preferences do not inspect the handlers. Automatic mode
/// selects Chromium only for a known Chromium-family bundle identifier;
/// Safari, Firefox-family handlers, and unknown applications use WebKit.
///
/// - Parameters:
/// - preference: The stored user preference.
/// - defaultHandlerBundleIdentifiers: Bundle identifiers returned by
/// LaunchServices for representative HTTPS and HTTP URLs.
/// - Returns: The engine to use for a new browser surface.
public func resolve(
preference: BrowserEnginePreference,
defaultHandlerBundleIdentifiers: [String]
) -> BrowserEngineKind {
switch preference {
case .webKit:
return .webKit
case .chromium:
return .chromium
case .automatic:
guard let preferredHandler = defaultHandlerBundleIdentifiers.first(where: {
!$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}) else {
return .webKit
}
return isChromiumFamilyBundleIdentifier(preferredHandler)
? .chromium
: .webKit
}
}

/// Returns whether a LaunchServices handler identity belongs to a supported
/// Chromium-family application.
///
/// - Parameter bundleIdentifier: An application bundle identifier.
/// - Returns: `true` for Chromium-family applications cmux can drive.
public func isChromiumFamilyBundleIdentifier(_ bundleIdentifier: String) -> Bool {
let normalized = bundleIdentifier.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !normalized.isEmpty else { return false }

if BrowserImportBrowserDescriptor.allBrowserDescriptors.contains(where: { descriptor in
descriptor.family == .chromium && descriptor.bundleIdentifiers.contains {
$0.caseInsensitiveCompare(normalized) == .orderedSame
}
}) {
return true
}

return Self.chromiumBundleIdentifierPrefixes.contains { normalized.hasPrefix($0) }
}

private static let chromiumBundleIdentifierPrefixes = [
"com.google.chrome",
"com.brave.browser",
"com.microsoft.edge",
"com.microsoft.edgemac",
"com.operasoftware.",
"com.vivaldi.vivaldi",
"company.thebrowser.",
"org.chromium.",
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public import CmuxCore

/// The resolved engine and, when needed, the Chromium application that implements it.
public struct BrowserEngineSelection: Equatable, Sendable {
/// The effective engine for the browser surface.
public let kind: BrowserEngineKind

/// The application cmux launches for a Chromium surface.
public let chromiumApplication: BrowserApplication?

/// Creates a resolved browser-engine selection.
///
/// - Parameters:
/// - kind: The effective engine.
/// - chromiumApplication: The Chromium application, required for a usable Chromium surface.
public init(kind: BrowserEngineKind, chromiumApplication: BrowserApplication? = nil) {
self.kind = kind
self.chromiumApplication = chromiumApplication
}

/// A deterministic WebKit selection for direct construction and tests.
public static let webKit = BrowserEngineSelection(kind: .webKit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
public import CmuxCore

/// Resolves stored policy and restored-session intent into a launchable browser engine.
@MainActor
public struct BrowserEngineSelectionService {
private let applicationProvider: any BrowserApplicationProviding
private let resolver: BrowserEngineResolver

/// Creates a browser-engine selection service with injected system boundaries.
///
/// - Parameters:
/// - applicationProvider: Supplies LaunchServices handlers and installed browsers.
/// - resolver: Maps handler identities to an engine family.
public init(
applicationProvider: any BrowserApplicationProviding,
resolver: BrowserEngineResolver = BrowserEngineResolver()
) {
self.applicationProvider = applicationProvider
self.resolver = resolver
}

/// Selects the effective engine for a newly-created or restored surface.
///
/// A restored engine is authoritative. Otherwise the stored preference is
/// resolved against the actual LaunchServices HTTP/HTTPS handlers. Explicit
/// Chromium selection remains Chromium even when no compatible application
/// is installed, allowing the pane to present an actionable error rather
/// than silently changing engines.
///
/// - Parameters:
/// - preference: The user's stored engine preference.
/// - restoredKind: The engine persisted for a restored surface, if any.
/// - Returns: The effective selection and optional Chromium application.
public func select(
preference: BrowserEnginePreference,
restoredKind: BrowserEngineKind? = nil
) -> BrowserEngineSelection {
let defaultApplications = applicationProvider.defaultBrowserApplications()
let kind = restoredKind ?? resolver.resolve(
preference: preference,
defaultHandlerBundleIdentifiers: defaultApplications.map(\.bundleIdentifier)
)
guard kind == .chromium else { return .webKit }

let defaultChromium = defaultApplications.first {
resolver.isChromiumFamilyBundleIdentifier($0.bundleIdentifier)
}
let application = defaultChromium ?? applicationProvider.installedChromiumApplications().first
return BrowserEngineSelection(kind: .chromium, chromiumApplication: application)
}
}
Loading