Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
de3d294
test: cover Cloud display ownership and readiness gaps
austinywang Sep 20, 2026
eb3edd7
Enforce Cloud display provenance and independent guest displays
austinywang Sep 20, 2026
fb79df1
Merge origin/main into 13192-cloud-display-ownership
austinywang Sep 20, 2026
2b23bbc
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 20, 2026
329897a
Keep display creation compatible with baked Cloud images
austinywang Sep 20, 2026
d11a13b
Merge origin/main into 13192-cloud-display-ownership
austinywang Sep 20, 2026
0f8e410
Fix guest display target wiring and session supervision
austinywang Sep 20, 2026
a7f0e3b
Harden embedded display helper and Dock restore ownership
austinywang Sep 20, 2026
8c5a84b
Merge origin/main into 13192-cloud-display-ownership
austinywang Sep 20, 2026
0a7fad0
Close Cloud display lifecycle gaps
austinywang Sep 20, 2026
87abadc
Harden display discovery and helper restart recovery
austinywang Sep 20, 2026
5db9dc3
Finish Cloud display build and readiness guards
austinywang Sep 20, 2026
21478f4
Merge origin/main into 13192-cloud-display-ownership
austinywang Sep 20, 2026
91d6bdb
Preserve display state and bind guest listeners privately
austinywang Sep 20, 2026
657285f
Invalidate display catalogs when VM state changes
austinywang Sep 20, 2026
a440be5
Preserve Dock display duplication identity
austinywang Sep 20, 2026
27a7fe8
Preserve Cloud display refresh and browser locations
austinywang Sep 20, 2026
c92eb8a
Run guest display service as the desktop user
austinywang Sep 20, 2026
44e6a17
Align Ghostty submodule with current main
austinywang Sep 20, 2026
fe68113
Invalidate terminal Cloud navigation callbacks
austinywang Sep 20, 2026
3aeafaa
Keep guest display ports out of forwarded resources
austinywang Sep 20, 2026
89be278
Complete additional display recovery paths
austinywang Sep 20, 2026
220c027
Synchronize guest profile and slow-route readiness
austinywang Sep 20, 2026
d7a3aaf
Finish guest display startup and restore routing
austinywang Sep 20, 2026
49e5899
Keep unresolved display restores retryable
austinywang Sep 20, 2026
26e71ed
Fence guest discovery to provider lifetime
austinywang Sep 20, 2026
a6c7dea
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 20, 2026
c0f7bcd
test: cover display transport recovery and duplication state
austinywang Sep 20, 2026
81bd61b
fix: preserve Cloud displays during transport recovery
austinywang Sep 20, 2026
7ebdd4f
test: preserve display identity across browser reconfiguration
austinywang Sep 20, 2026
292fd36
fix: retain display identity across route reconfiguration
austinywang Sep 20, 2026
b1f53ae
test: cover route observation after display reconfiguration
austinywang Sep 20, 2026
54fcdfb
fix: retain Cloud restore lifecycle state
austinywang Sep 20, 2026
1c64ed8
test: drop Cloud provenance after external navigation
austinywang Sep 20, 2026
7cda189
fix: clear Cloud provenance on external browser navigation
austinywang Sep 20, 2026
1d18589
test: cover display catalog and readiness cancellation
austinywang Sep 20, 2026
fbef08c
fix: fence display catalog and readiness lifecycles
austinywang Sep 20, 2026
3a5ea98
test: cover delayed display restore and scoped helpers
austinywang Sep 20, 2026
c3c5c8c
fix: complete display restore and supervisor isolation
austinywang Sep 20, 2026
9928173
fix: require discovered guest display resources
austinywang Sep 20, 2026
0cb8b98
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 20, 2026
a9a948a
test: cover guest component recovery
austinywang Sep 20, 2026
bed6163
fix: preserve Cloud provenance and supervise displays
austinywang Sep 20, 2026
30e94d5
test: reject failed display catalog responses
austinywang Sep 20, 2026
10c6557
fix: fence display discovery by response and auth
austinywang Sep 20, 2026
46ce029
test: filter untrusted display restore targets
austinywang Sep 20, 2026
6dba3ec
fix: fence display restore targets and VM kind
austinywang Sep 20, 2026
0c0319f
test: fence browser Cloud service identity
austinywang Sep 20, 2026
f58c2c6
test: exercise recovered display supervision
austinywang Sep 20, 2026
fff1196
fix: recover display supervisors and port identity
austinywang Sep 20, 2026
f96bad4
test: recover scoped display process commands
austinywang Sep 20, 2026
e2fb4e3
fix: recover scoped display processes by command
austinywang Sep 20, 2026
c6f86de
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
c1cb5d9
test: cover Cloud restore and destination comment fixes
austinywang Sep 21, 2026
178d35e
fix: address Cloud display review comments
austinywang Sep 21, 2026
4795f0f
test: cover display port ownership and recovery
austinywang Sep 21, 2026
cbe4863
fix: harden Cloud display supervisor and route lifecycle
austinywang Sep 21, 2026
3d27a16
test: stay within Swift file budget
austinywang Sep 21, 2026
f7d469d
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
92ed22a
fix: sanitize display errors and readiness probes
austinywang Sep 21, 2026
60c550c
fix: restore Cloud resources in Dock scopes
austinywang Sep 21, 2026
3df7c56
fix: recover global Dock projections and daemon readiness
austinywang Sep 21, 2026
4d657c1
fix: preserve Dock connections across Cloud route changes
austinywang Sep 21, 2026
812b4ef
fix: bound display startup and preserve duplicate URLs
austinywang Sep 21, 2026
75fdecb
fix: defer Cloud activation for hidden restores
austinywang Sep 21, 2026
6db7593
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
42f1ab4
fix: preserve ownership checks across latest main merge
austinywang Sep 21, 2026
eb39f9e
fix: remove duplicate projection query declarations
austinywang Sep 21, 2026
23c807b
fix: restore provider display lifecycle after main merge
austinywang Sep 21, 2026
d8333af
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
1a127d2
fix: use merged hostname route API
austinywang Sep 21, 2026
210df4c
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
e18669f
fix: restore New Display hover button after main merge
austinywang Sep 21, 2026
7856fa7
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
1eda271
fix: keep SurfaceCatalog within its line budget
austinywang Sep 21, 2026
2bc5ca3
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
21a652e
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 21, 2026
3d82498
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang Sep 22, 2026
345e9ef
fix: route cloud desktop clicks through portal
austinywang Sep 22, 2026
019fd05
chore: keep cloud fix within file budgets
austinywang Sep 22, 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
590 changes: 590 additions & 0 deletions Resources/Localizable.xcstrings

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Sources/AppDelegate+DockSurfaceMove.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ extension AppDelegate {

/// Whether a live surface can leave its current owner and be driven from
/// `destinationDock`.
func canMoveSurfaceIntoDock(sourceTabId: UUID, destinationDock _: DockSplitStore) -> Bool {
func canMoveSurfaceIntoDock(sourceTabId: UUID, destinationDock: DockSplitStore) -> Bool {
guard let source = locateContainerSurface(tabId: sourceTabId) else { return false }
return canMoveSurfaceIntoDock(source)
return destinationDock.surfaceOwnershipPolicy.rejection(for: machineOwningBonsplitTab(sourceTabId)) == nil && canMoveSurfaceIntoDock(source)
}

/// Whether the right sidebar (Files / Find / Dock) currently owns input
Expand Down Expand Up @@ -93,7 +93,7 @@ extension AppDelegate {
destination: BonsplitController.ExternalTabDropRequest.Destination
) -> Bool {
guard let source = locateContainerSurface(tabId: sourceTabId) else { return false }
guard canMoveSurfaceIntoDock(source) else { return false }
guard canMoveSurfaceIntoDock(sourceTabId: sourceTabId, destinationDock: destinationDock) else { return false }
let shouldPreserveSourceWorkspace = shouldPreserveSourceWorkspaceAfterDockMove(
source,
destinationDock: destinationDock
Expand Down
2 changes: 1 addition & 1 deletion Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4027,7 +4027,7 @@ final class WindowBrowserPortal: NSObject {
for subview in hostView.subviews.reversed() {
guard let container = subview as? WindowBrowserSlotView else { continue }
guard !container.isHidden else { continue }
guard container.frame.contains(point) else { continue }
let containsPoint = container.frame.contains(point) || container.convert(container.bounds, to: nil).contains(windowPoint); guard containsPoint else { continue }
guard let webView = entriesByWebViewId
.first(where: { _, entry in entry.containerView === container })?
.value
Expand Down
123 changes: 123 additions & 0 deletions Sources/Cloud/CloudDisplayCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import Foundation
import Observation

/// Explicit display discovery/creation through the existing VM-authorized exec
/// route. Routine terminal catalog refreshes never execute guest commands.
@MainActor
@Observable
final class CloudDisplayCoordinator {
private let execute: @MainActor (String, Int) async throws -> VMExecResult
private(set) var snapshot: CloudGuestDisplaySnapshot?
private(set) var lastValidatedSnapshot: CloudGuestDisplaySnapshot?
private(set) var isAvailable = false
private var generation: UInt64 = 0
private var requestID: UUID?
private var refreshTask: Task<Void, Never>?
private var creation: Task<CloudGuestDisplaySnapshot, Error>?
Comment thread
austinywang marked this conversation as resolved.

init(execute: @escaping @MainActor (String, Int) async throws -> VMExecResult) {
self.execute = execute
}

var canCreate: Bool { isAvailable && (snapshot?.canCreate == true || requestID != nil) && creation == nil }
var displaySnapshot: CloudGuestDisplaySnapshot? { snapshot ?? lastValidatedSnapshot }

func refresh() async {
guard creation == nil else { return }
refreshTask?.cancel()
generation &+= 1
let token = generation
let task = Task { [weak self, execute] in
do {
var response: VMExecResult?
var lastError: (any Error)?
for attempt in 0..<3 {
do {
let candidate = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000)
if candidate.exitCode == 0 {
response = candidate
break
}
lastError = SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage)
} catch {
lastError = error
}
if attempt < 2 { try await Task.sleep(for: .milliseconds(100)) }
Comment thread
austinywang marked this conversation as resolved.
}
guard let response else { throw lastError ?? SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) }
guard response.exitCode == 0 else {
throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage)
}
let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8))
guard let self, token == self.generation, !Task.isCancelled else { return }
self.snapshot = snapshot
self.lastValidatedSnapshot = snapshot
self.isAvailable = true
} catch {
guard let self, token == self.generation else { return }
self.snapshot = nil
self.isAvailable = false
}
}
refreshTask = task
await task.value
if refreshTask != nil, token == generation { refreshTask = nil }
}

func create() async throws -> CloudGuestDisplaySnapshot {
if let creation {
return try await withTaskCancellationHandler {
try await creation.value
} onCancel: {
creation.cancel()
}
}
guard isAvailable, snapshot?.canCreate == true || requestID != nil else {
throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage)
}
generation &+= 1
let token = generation
let request = requestID ?? UUID()
requestID = request
// The UUID is generated here and retained after failures. A retry cannot
// create a second guest display when the first receipt was lost.
let task = Task { [weak self, execute] in
try Task.checkCancellation()
let response = try await execute(CloudGuestDisplayScript.command(action: "create", requestID: request), 65_000)
let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8))
try Task.checkCancellation()
guard let self, self.generation == token else { throw CancellationError() }
self.snapshot = snapshot
self.lastValidatedSnapshot = snapshot
guard response.exitCode == 0, snapshot.error == nil, snapshot.created != nil else {
throw SurfaceCatalogError.unsupported(String(localized: "cloud.display.creationFailed", defaultValue: "The new display could not start. Refresh Displays, then retry. Existing displays are unchanged."))
}
self.requestID = nil
return snapshot
}
creation = task
defer { if generation == token { creation = nil } }
return try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}

func stop() {
invalidate()
}

/// Drops guest state when the provider identity or account scope changes.
func invalidate() {
generation &+= 1
refreshTask?.cancel()
refreshTask = nil
creation?.cancel()
creation = nil
snapshot = nil
lastValidatedSnapshot = nil
requestID = nil
isAvailable = false
}
}
20 changes: 20 additions & 0 deletions Sources/Cloud/CloudGuestDisplay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

/// One guest-issued display resource with a stable slot and noVNC target.
struct CloudGuestDisplay: Decodable, Sendable {
let id: String
let number: Int
let port: Int
let state: SurfaceLifecycle

func resource(on machine: SurfaceMachineID, address: String?) -> SurfaceResource {
SurfaceResource(
id: SurfaceResourceID(machine: machine, kind: .display, key: id),
title: number == 1
? String(localized: "cloudTree.node.desktop", defaultValue: "Desktop")
: String(format: String(localized: "cloud.display.numberedTitle", defaultValue: "Desktop %d"), number),
detail: "noVNC", lifecycle: state, agent: nil, remoteWorkspace: nil,
port: port, url: address.map { CmuxTuiSurfaceProvider.privateDesktopURL(privateAddress: $0, port: port) }
)
}
}
Loading
Loading