Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
66 changes: 0 additions & 66 deletions CLI/CMUXCLI+BrowserEngine.swift

This file was deleted.

136 changes: 26 additions & 110 deletions CLI/cmux.swift

Large diffs are not rendered by default.

48 changes: 1 addition & 47 deletions CLI/cmux_open.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import CryptoKit
import CmuxBrowser
import Darwin
import Foundation

Expand Down Expand Up @@ -141,7 +140,6 @@ extension CMUXCLI {
var window: String?
var surface: String?
var pane: String?
var engine: String?
var focus: String?
var noFocus = false
var targets: [String] = []
Expand Down Expand Up @@ -827,18 +825,6 @@ extension CMUXCLI {
let fileFocus = explicitFocus ?? true

let targets = try parsedArgs.targets.map(resolveOpenTarget)
if parsedArgs.engine != nil,
targets.contains(where: { target in
switch target {
case .url: return false
case .file, .directory: return true
}
}) {
throw CLIError(message: String(
localized: "cli.browser.engine.error.urlOnly",
defaultValue: "--engine is only supported when opening a browser URL"
))
}
var fileCount = 0
var urlCount = 0
var directoryCount = 0
Expand Down Expand Up @@ -890,9 +876,6 @@ extension CMUXCLI {
case .url(let url, let defaultFocus):
try flushPendingFiles()
var params: [String: Any] = ["url": url, "focus": explicitFocus ?? defaultFocus]
if let engine = parsedArgs.engine {
params["engine"] = engine
}
if let windowHandle { params["window_id"] = windowHandle }
if let workspaceHandle { params["workspace_id"] = workspaceHandle }
if let surfaceHandle { params["surface_id"] = surfaceHandle }
Expand Down Expand Up @@ -1287,17 +1270,6 @@ extension CMUXCLI {
parsed.pane = try openOptionValue(commandArgs, index: index, name: arg)
index += 2
continue
case "--engine":
let raw = try openOptionValue(commandArgs, index: index, name: arg)
guard let engine = BrowserEngineKind.parse(raw) else {
throw CLIError(message: String(
localized: "cli.browser.engine.error.invalid",
defaultValue: "--engine requires webkit or chromium"
))
}
parsed.engine = engine.rawValue
index += 2
continue
case "--focus":
parsed.focus = try openOptionValue(commandArgs, index: index, name: arg)
index += 2
Expand All @@ -1307,24 +1279,8 @@ extension CMUXCLI {
index += 1
continue
default:
if arg.hasPrefix("--engine=") {
let raw = String(arg.dropFirst("--engine=".count))
guard let engine = BrowserEngineKind.parse(raw) else {
throw CLIError(message: String(
localized: "cli.browser.engine.error.invalid",
defaultValue: "--engine requires webkit or chromium"
))
}
parsed.engine = engine.rawValue
index += 1
continue
}
if arg.hasPrefix("-") {
let format = String(
localized: "cli.open.error.unknownFlag",
defaultValue: "open: unknown flag '%@'. Usage: cmux open <path-or-url>... [--engine <webkit|chromium>] [--workspace <id|ref|index>] [--surface <id|ref|index>] [--pane <id|ref|index>] [--window <id|ref|index>] [--focus true|false] [--no-focus]"
)
throw CLIError(message: String.localizedStringWithFormat(format, arg))
throw CLIError(message: "open: unknown flag '\(arg)'. Usage: cmux open <path-or-url>... [--workspace <id|ref|index>] [--surface <id|ref|index>] [--pane <id|ref|index>] [--window <id|ref|index>] [--focus true|false] [--no-focus]")
}
}
}
Expand Down Expand Up @@ -7928,7 +7884,6 @@ extension CMUXCLI {
--surface <id|ref|index> Target surface whose pane should receive file tabs (default: $CMUX_SURFACE_ID)
--pane <id|ref|index> Target pane for file tabs
--window <id|ref|index> Target window
--engine <webkit|chromium> Browser engine for URL targets only
--focus <true|false> Focus opened file previews (default: true)
--no-focus Do not focus opened file previews

Expand All @@ -7937,7 +7892,6 @@ extension CMUXCLI {
cmux open image-a.png image-b.jpg
cmux open ~/Downloads/movie.mov --pane pane:1
cmux open https://example.com
cmux open https://example.com --engine chromium
"""
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ public final class BrowserAutomationNavigationCoordinator {
private var allowsSameDocumentCompletion = false
private var downloadPolicyNavigationID: ObjectIdentifier?
private var pendingReplacementNavigationID: ObjectIdentifier?
private var externalNavigationTask: Task<Void, Never>?
private var externalNavigationTicket: BrowserAutomationNavigationTicket?
// Swift tasks cannot force-terminate an engine callback that ignores
// cancellation. Keep one cancelled operation slot owned until that task
// exits, and fail closed while it is occupied so repeated retries cannot
// accumulate permanently retained engine tasks.
private var externalNavigationOperationTask: Task<Void, Never>?
private var externalNavigationOperationID: UUID?

/// Creates a coordinator with a bounded continuous-clock navigation deadline.
public init(navigationTimeout: Duration = .seconds(15)) {
Expand All @@ -49,7 +41,6 @@ public final class BrowserAutomationNavigationCoordinator {
/// Starts observing a WebView instance and supersedes a transaction from an older instance.
public func bind(to instanceID: UUID) {
guard observedInstanceID != instanceID else { return }
cancelExternalNavigation()
if let activeTicket {
finish(activeTicket, with: .superseded)
}
Expand All @@ -69,7 +60,6 @@ public final class BrowserAutomationNavigationCoordinator {
targetURL: URL? = nil,
allowsSameDocumentCompletion: Bool = false
) -> BrowserAutomationNavigationTicket {
cancelExternalNavigation()
if let activeTicket {
finish(activeTicket, with: .superseded)
}
Expand All @@ -88,58 +78,6 @@ public final class BrowserAutomationNavigationCoordinator {
return ticket
}

/// Runs an engine-owned navigation under the same bounded ticket deadline
/// used by WebKit delegate navigations.
///
/// The operation is retained and cancelled when the panel closes, a newer
/// navigation supersedes it, or the caller's wait reaches its deadline.
/// This prevents a stalled CDP target from leaving an unowned task behind.
///
/// - Parameters:
/// - ticket: The active external-navigation ticket.
/// - operation: The engine operation and its terminal load wait.
public func startExternalNavigation(
_ ticket: BrowserAutomationNavigationTicket,
operation: @escaping @MainActor () async throws -> Void
) {
guard activeTicket == ticket else { return }
cancelExternalNavigation()
guard externalNavigationOperationID == nil else {
finish(ticket, with: .timedOut)
return
}
let operationID = UUID()
externalNavigationTicket = ticket
externalNavigationOperationID = operationID
externalNavigationTask = Task { @MainActor [weak self] in
guard let self else { return }
switch await self.runExternalNavigationWithDeadline(operation, operationID: operationID) {
case .committed:
self.finish(ticket, with: .committed)
case .timedOut:
self.finish(ticket, with: .timedOut)
case .cancelled:
self.finish(ticket, with: .cancelled)
case .failed:
self.finish(ticket, with: .failed("Browser operation failed"))
}
}
}

/// Cancels a retained engine operation after its caller has stopped waiting.
public func cancelExternalNavigation(
_ ticket: BrowserAutomationNavigationTicket? = nil
) {
guard ticket == nil || externalNavigationTicket == ticket else { return }
externalNavigationTask?.cancel()
externalNavigationTask = nil
externalNavigationTicket = nil
externalNavigationOperationTask?.cancel()
if externalNavigationOperationTask == nil {
externalNavigationOperationID = nil
}
}

/// Associates the load call's returned navigation identity with its transaction.
public func didStart(
_ ticket: BrowserAutomationNavigationTicket,
Expand Down Expand Up @@ -326,20 +264,6 @@ public final class BrowserAutomationNavigationCoordinator {
finishMatching(instanceID: instanceID, navigationID: navigationID, with: .failed(message))
}

/// Completes a transaction whose navigation identity is owned by an
/// engine adapter rather than a WebKit delegate.
///
/// Chromium reports navigation completion through CDP, so it cannot
/// provide a ``WKNavigation`` identity. The adapter still uses this same
/// coordinator and ticket lifecycle so callers observe identical terminal
/// outcomes across engines.
public func finishExternally(
_ ticket: BrowserAutomationNavigationTicket,
with outcome: BrowserAutomationNavigationOutcome
) {
finish(ticket, with: outcome)
}

/// Records a cancellation only when it belongs to the exact active navigation.
public func didCancel(instanceID: UUID, navigationID: ObjectIdentifier?) {
guard pendingReplacementNavigationID != navigationID else { return }
Expand Down Expand Up @@ -400,78 +324,13 @@ public final class BrowserAutomationNavigationCoordinator {
}

ticket.transaction.discardTerminalOutcome()
if outcome == .timedOut || outcome == .cancelled || Task.isCancelled {
cancelExternalNavigation(ticket)
}
if activeTicket == ticket {
finish(ticket, with: Task.isCancelled ? .cancelled : outcome)
ticket.transaction.discardTerminalOutcome()
}
return Task.isCancelled ? .cancelled : outcome
}

private func runExternalNavigationWithDeadline(
_ operation: @escaping @MainActor () async throws -> Void,
operationID: UUID
) async -> ExternalNavigationRaceResult {
guard externalNavigationOperationID == operationID, !Task.isCancelled else {
return .cancelled
}
let (events, continuation) = AsyncStream.makeStream(
of: ExternalNavigationRaceResult.self,
bufferingPolicy: .bufferingOldest(1)
)
let operationTask = Task { @MainActor [weak self] in
defer { self?.externalNavigationOperationDidFinish(operationID) }
do {
try await operation()
continuation.yield(Task.isCancelled ? .cancelled : .committed)
} catch is CancellationError {
if !Task.isCancelled {
continuation.yield(.cancelled)
}
} catch let error as ChromiumBrowserDiagnostic where error == .navigationTimedOut {
_ = error
continuation.yield(Task.isCancelled ? .cancelled : .timedOut)
} catch {
continuation.yield(Task.isCancelled ? .cancelled : .failed)
}
}
externalNavigationOperationTask = operationTask
let timeoutTask = Task { @MainActor [navigationTimeout, sleep] in
do {
try await sleep(navigationTimeout)
try Task.checkCancellation()
continuation.yield(.timedOut)
} catch is CancellationError {
if !Task.isCancelled {
continuation.yield(.cancelled)
}
} catch {
continuation.yield(Task.isCancelled ? .cancelled : .failed)
}
}
defer {
operationTask.cancel()
timeoutTask.cancel()
continuation.finish()
}
return await withTaskCancellationHandler(operation: {
var iterator = events.makeAsyncIterator()
return await iterator.next() ?? .cancelled
}, onCancel: {
operationTask.cancel()
timeoutTask.cancel()
continuation.finish()
})
}

private func externalNavigationOperationDidFinish(_ operationID: UUID) {
guard externalNavigationOperationID == operationID else { return }
externalNavigationOperationTask = nil
externalNavigationOperationID = nil
}

private func finishMatching(
instanceID: UUID,
navigationID: ObjectIdentifier?,
Expand All @@ -491,9 +350,6 @@ public final class BrowserAutomationNavigationCoordinator {
with outcome: BrowserAutomationNavigationOutcome
) {
guard activeTicket == ticket else { return }
if externalNavigationTicket == ticket {
cancelExternalNavigation(ticket)
}
activeTicket = nil
activeNavigationID = nil
activeTargetURL = nil
Expand Down

This file was deleted.

Loading
Loading