Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c2c6f8f
test: reproduce cross-client Cloud display membership gap
austinywang Sep 29, 2026
971f372
fix: persist Cloud display workspace membership
austinywang Sep 29, 2026
a75f0e1
fix: retain Cloud terminal replay before pane binding
austinywang Sep 29, 2026
d9f3915
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 12226…
austinywang Sep 29, 2026
78664ad
fix: import Cloud projection end reason
austinywang Sep 29, 2026
837e8a2
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 12226…
austinywang Sep 29, 2026
167227b
fix: preserve Cloud display placement identities
austinywang Sep 29, 2026
4e36b68
style: keep Cloud snapshot arguments ordered
austinywang Sep 29, 2026
80146da
fix: materialize Cloud display memberships across clients
austinywang Sep 30, 2026
c9a599e
Merge main (40138c68cec9) into 12226-cloud-workspace-defaults
austinywang Sep 30, 2026
3b5a7c7
fix: reconcile Cloud display membership from fresh snapshots
austinywang Sep 30, 2026
b1d2959
Merge main (d5c10c52558c) into 12226-cloud-workspace-defaults
austinywang Sep 30, 2026
1c27920
fix: keep catalog projection mutation within owner file
austinywang Sep 30, 2026
85392bf
test: wire Cloud display projection fixtures
austinywang Sep 30, 2026
e7ffb8f
fix: deduplicate Cloud display presentation resources
austinywang Sep 30, 2026
d9059bc
fix: avoid duplicate Cloud display placement sync
austinywang Sep 30, 2026
8926fa6
fix: preserve every Cloud display membership token
austinywang Sep 30, 2026
819b132
fix: avoid replaying Cloud placement after materialization
austinywang Sep 30, 2026
8bc50d3
Merge origin/main into 12226-cloud-workspace-defaults
austinywang Sep 30, 2026
b5f4226
fix: restore shared sizing request tracking access
austinywang Sep 30, 2026
c256e62
Merge green main into cloud workspace defaults
austinywang Sep 30, 2026
3a22aa9
Merge main (2761cc95e705) into 12226-cloud-workspace-defaults
austinywang Sep 30, 2026
4ef2787
Merge main (666c77f0c3d7) into 12226-cloud-workspace-defaults
austinywang Sep 30, 2026
fe408bb
style: keep Cloud mirror session within file budget
austinywang Sep 30, 2026
92c1e56
Merge main (e30de3da7f3a) into 12226-cloud-workspace-defaults
austinywang Sep 30, 2026
16973dd
fix: model Cloud display memberships in desktop fixture
austinywang Sep 30, 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
Expand Up @@ -10,6 +10,7 @@ public struct CloudWorkspaceProjectionPlan {
public init(desired: [SurfaceResourcePlacement], existing: [SurfaceProjection]) {
let wanted = Set(desired)
var seen = Set<SurfaceResourcePlacement>()
var satisfied = Set<SurfaceResourcePlacement>()
var obsolete: [SurfaceProjection] = []
for projection in existing.sorted(by: { $0.panelID.uuidString < $1.panelID.uuidString }) {
let placement = SurfaceResourcePlacement(
Expand All @@ -22,13 +23,19 @@ public struct CloudWorkspaceProjectionPlan {
// coordinates were cleared by an authoritative remote deletion has
// neither coordinate and must still be retired.
if projection.isLocalWorkspaceView && projection.remoteWorkspaceID != nil {
seen.insert(placement)
for matching in desired where
matching.cloudDisplayMembershipViewID != nil
&& matching.resource == projection.resource
&& matching.remoteWorkspaceID == projection.remoteWorkspaceID {
satisfied.insert(matching)
seen.insert(matching)
}
continue
}
if !wanted.contains(placement) || !seen.insert(placement).inserted { obsolete.append(projection) }
}
var missingSeen = Set<SurfaceResourcePlacement>()
missing = desired.filter { !seen.contains($0) && missingSeen.insert($0).inserted }
missing = desired.filter { !satisfied.contains($0) && missingSeen.insert($0).inserted }
self.obsolete = obsolete
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ extension SurfaceProjection {
/// their bound workspace; availability in the machine resource pool never does.
/// A daemon placement of the same resource already supplies that workspace row.
public static func localWorkspaceMembers(resources: [SurfaceResource], projections: [SurfaceProjection]) -> [(resource: SurfaceResource, workspaceID: String)] {
let previews = Dictionary(uniqueKeysWithValues: resources.filter { $0.kind == .display || $0.id.isForwardedPort }.map { ($0.id, $0) })
let previews = Dictionary(
resources.filter { $0.kind == .display || $0.id.isForwardedPort }.map { ($0.id, $0) },
uniquingKeysWith: { first, _ in first }
)
var seen: [SurfaceResourceID: Set<String>] = [:]
return projections.compactMap { projection in
guard projection.isLocalWorkspaceView,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@ struct CloudWorkspaceProjectionPlanTests {
#expect(plan.missing == [desired])
}

@Test("A local Desktop preview satisfies an exact frontend membership view")
func localDisplayPreviewSatisfiesMembership() {
let display = SurfaceResourceID(machine: machine, kind: .display, key: "display:1")
let preview = SurfaceProjection(
resource: display,
workspaceID: UUID(),
panelID: UUID(),
remoteWorkspaceID: "remote-workspace"
)
let remoteWorkspace = SurfaceRemoteWorkspace(
id: "remote-workspace", name: "Cloud", index: 0, focused: true
)
let desired = SurfaceResourcePlacement(
resource: display,
remoteView: SurfaceRemoteView(
tabID: SurfaceRemoteView.cloudDisplayMembershipViewPrefix + "view-a",
workspace: remoteWorkspace
)
)
let secondView = SurfaceResourcePlacement(
resource: display,
remoteView: SurfaceRemoteView(
tabID: SurfaceRemoteView.cloudDisplayMembershipViewPrefix + "view-b",
workspace: remoteWorkspace
)
)

let plan = CloudWorkspaceProjectionPlan(desired: [desired, secondView], existing: [preview])

#expect(plan.obsolete.isEmpty)
#expect(plan.missing.isEmpty)
}

@Test("A preview whose remote placement was deleted is retired")
func deletedRemotePlacementIsNotMistakenForPreview() {
let display = SurfaceResourceID(machine: machine, kind: .display, key: "display:1")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

public extension CloudTuiRequests {
/// Updates one durable Cloud display-membership projection.
static func putCloudDisplayMembershipProjection(
projectionID: String,
frontendID: String,
windowID: String,
generation: String,
projection: [String: Any],
expectedProjectionRevision: UInt64?,
idempotencyKey: String
) -> CloudTuiRequest {
var fields: [String: Any] = [
"frontend_projection": projectionID,
"frontend_id": frontendID,
"window_id": windowID,
"generation": generation,
"projection": projection,
]
if let expectedProjectionRevision {
fields["expected_projection_revision"] = String(expectedProjectionRevision)
}
return CloudTuiRequest("frontend_projection.put", fields, mutation: true, key: idempotencyKey)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Foundation

/// One durable display view attached to a Cloud workspace.
///
/// The daemon stores these values in a frontend projection because a VNC display
/// is a machine resource rather than a cmux-tui tab. `machine`, `workspaceID`,
/// and `displayID` are the ownership identity; `clientID` and `viewID` let
/// independent clients remove only the view they created.
public struct CloudVMDisplayMembership: Hashable, Codable, Sendable {
/// Schema stored in the daemon's frontend projection record.
public static let projectionSchema = "cmux.cloud.workspace-displays.v1"
/// Frontend identity authorized to author this projection.
public static let projectionFrontendID = "cmux-macos-cloud-layout-v1"
/// Projection generation used to reject stale frontend implementations.
public static let projectionGeneration = "cmux-cloud-display-layout-v1"

/// Stable window subject used to scope one machine workspace projection.
public static func projectionWindowID(machine: SurfaceMachineID, workspaceID: String) -> String {
"cloud-workspace:\(machine.rawValue):\(workspaceID)"
}

public let machine: SurfaceMachineID
public let workspaceID: String
public let displayID: String
public let clientID: String
public let viewID: String

public init(
machine: SurfaceMachineID,
workspaceID: String,
displayID: String,
clientID: String,
viewID: String
) {
self.machine = machine
self.workspaceID = workspaceID
self.displayID = displayID
self.clientID = clientID
self.viewID = viewID
}
}

extension CloudVMState {
/// Reads only validated display memberships from the accepted frontend
/// projection rows. Unknown machines, workspaces, displays, and malformed
/// view tokens are ignored so stale or foreign provenance cannot enter the
/// catalog projection.
public var displayMemberships: [CloudVMDisplayMembership] {
let workspaceIDs = Set(workspaces.map(\.id))
var result = Set<CloudVMDisplayMembership>()
let expectedSessionID = (document.value(forKey: "session") as? [String: Any])?["id"] as? String
for row in document.objects(forCollectionKey: "frontend_projections") ?? [] {
guard row["frontend_id"] as? String == CloudVMDisplayMembership.projectionFrontendID,
row["generation"] as? String == CloudVMDisplayMembership.projectionGeneration else { continue }
if let expectedSessionID, row["session_id"] as? String != expectedSessionID { continue }
guard let projection = row["projection"] as? [String: Any],
projection["schema"] as? String == CloudVMDisplayMembership.projectionSchema,
projection["machine_id"] as? String == machine.rawValue,
let workspaceID = projection["workspace_id"] as? String,
workspaceIDs.contains(workspaceID),
row["window_id"] as? String == CloudVMDisplayMembership.projectionWindowID(machine: machine, workspaceID: workspaceID),
let memberships = projection["memberships"] as? [[String: Any]] else { continue }
for membership in memberships {
guard let displayID = membership["display_id"] as? String,
displayID.hasPrefix("display:"),
displayID.dropFirst("display:".count).isEmpty == false,
let clientID = membership["client_id"] as? String,
!clientID.isEmpty,
let viewID = membership["view_id"] as? String,
!viewID.isEmpty else { continue }
result.insert(CloudVMDisplayMembership(
machine: machine,
workspaceID: workspaceID,
displayID: displayID,
clientID: clientID,
viewID: viewID
))
}
}
return result.sorted {
($0.workspaceID, $0.displayID, $0.clientID, $0.viewID)
< ($1.workspaceID, $1.displayID, $1.clientID, $1.viewID)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

extension SurfaceCatalogSnapshot {
/// Returns the machine pool plus one presentation copy for every accepted
/// display/workspace membership. The copies retain the same resource id and
/// are used only by workspace rows and groups; the pool remains one row per
/// discovered display resource. A copy contains only its frontend views, so
/// an existing daemon display tab is not emitted a second time.
public func cloudWorkspaceResources(on machine: SurfaceMachineID) -> [SurfaceResource] {
let base = resources(on: machine)
let byID = Dictionary(base.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first })
let workspaces = Dictionary(
(machines.first { $0.id == machine }?.remoteWorkspaces ?? []).map { ($0.id, $0) },
uniquingKeysWith: { first, _ in first }
)
var result = base
let membershipsByDisplayID = Dictionary(grouping: cloudDisplayMemberships.filter { $0.machine == machine }, by: \.displayID)
for displayID in membershipsByDisplayID.keys.sorted() {
let id = SurfaceResourceID(machine: machine, kind: .display, key: displayID)
guard let baseResource = byID[id] else { continue }
var placed = baseResource
let memberships = (membershipsByDisplayID[displayID] ?? [])
.sorted { ($0.workspaceID, $0.clientID, $0.viewID) < ($1.workspaceID, $1.clientID, $1.viewID) }
let views = memberships.enumerated().compactMap { index, membership -> SurfaceRemoteView? in
guard let workspace = workspaces[membership.workspaceID] else { return nil }
return SurfaceRemoteView(
tabID: SurfaceRemoteView.cloudDisplayMembershipViewPrefix + membership.viewID,
workspace: workspace,
name: nil,
index: index,
focused: false
)
}
guard !views.isEmpty else { continue }
placed.remoteViews = views
placed.remoteWorkspace = views.first?.workspace
result.append(placed)
}
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import Foundation
/// The catalog as one value: what the sidebar renders, what `surface.catalog` and
/// `cmux vm tree --json` print. Machines are ordered local first, then by name.
public struct SurfaceCatalogSnapshot: Hashable, Codable, Sendable {
/// Workspaces admitted for deletion but not yet confirmed by the daemon,
/// per machine. Nil when nothing is pending, so socket readers on older
/// builds keep decoding the same document.
/// Pending native workspace identities, keyed by machine and daemon workspace.
public var pendingWorkspaceCreations: [SurfaceMachineID: [String: UUID]]? = nil
public var pendingWorkspaceDeletions: [SurfaceMachineID: Set<String>]? = nil
Expand All @@ -14,6 +11,7 @@ public struct SurfaceCatalogSnapshot: Hashable, Codable, Sendable {
public var projections: [SurfaceProjection]
public var staleMachineIDs: Set<SurfaceMachineID> = []
public var displayCreationMachines: Set<SurfaceMachineID>? = nil
public var cloudDisplayMemberships: [CloudVMDisplayMembership] = []

public static let empty = SurfaceCatalogSnapshot(machines: [], resources: [], projections: [])

Expand All @@ -37,7 +35,8 @@ public struct SurfaceCatalogSnapshot: Hashable, Codable, Sendable {
resources: [SurfaceResource],
projections: [SurfaceProjection],
staleMachineIDs: Set<SurfaceMachineID> = [],
displayCreationMachines: Set<SurfaceMachineID>? = nil
displayCreationMachines: Set<SurfaceMachineID>? = nil,
cloudDisplayMemberships: [CloudVMDisplayMembership] = []
) {
self.pendingWorkspaceCreations = pendingWorkspaceCreations
self.pendingWorkspaceDeletions = pendingWorkspaceDeletions
Expand All @@ -46,12 +45,13 @@ public struct SurfaceCatalogSnapshot: Hashable, Codable, Sendable {
self.projections = projections
self.staleMachineIDs = staleMachineIDs
self.displayCreationMachines = displayCreationMachines
self.cloudDisplayMemberships = cloudDisplayMemberships
}
}

extension SurfaceCatalogSnapshot {
private enum CodingKeys: String, CodingKey {
case pendingWorkspaceCreations, pendingWorkspaceDeletions, machines, resources, projections, staleMachineIDs, displayCreationMachines
case pendingWorkspaceCreations, pendingWorkspaceDeletions, machines, resources, projections, staleMachineIDs, displayCreationMachines, cloudDisplayMemberships
}

public init(from decoder: Decoder) throws {
Expand All @@ -63,5 +63,6 @@ extension SurfaceCatalogSnapshot {
projections = try values.decode([SurfaceProjection].self, forKey: .projections)
staleMachineIDs = try values.decodeIfPresent(Set<SurfaceMachineID>.self, forKey: .staleMachineIDs) ?? []
displayCreationMachines = try values.decodeIfPresent(Set<SurfaceMachineID>.self, forKey: .displayCreationMachines)
cloudDisplayMemberships = try values.decodeIfPresent([CloudVMDisplayMembership].self, forKey: .cloudDisplayMemberships) ?? []
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

extension SurfaceRemoteView {
/// Synthetic tab identity used only to order a display view persisted by
/// the Cloud frontend projection. It is never sent to a daemon tab API.
public static let cloudDisplayMembershipViewPrefix = "cloud-display-view:"

public var isCloudDisplayMembershipView: Bool {
tabID.hasPrefix(Self.cloudDisplayMembershipViewPrefix)
}

/// The durable frontend view token carried by a synthetic display view.
/// It is intentionally separate from daemon tab ids: a display membership
/// is persisted outside the daemon layout, but still needs an exact identity
/// when two clients attach the same display to one workspace.
public var cloudDisplayMembershipViewID: String? {
guard isCloudDisplayMembershipView else { return nil }
let value = String(tabID.dropFirst(Self.cloudDisplayMembershipViewPrefix.count))
return value.isEmpty ? nil : value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ public struct SurfaceResourcePlacement: Hashable, Codable, Sendable {
public let resource: SurfaceResourceID
public let remoteWorkspaceID: String?
public let remoteTabID: String?
/// Exact frontend display membership identity, when this placement came
/// from the Cloud workspace projection rather than a daemon tab.
public let cloudDisplayMembershipViewID: String?

public init(
resource: SurfaceResourceID,
remoteView: SurfaceRemoteView? = nil,
remoteWorkspaceID: String? = nil,
remoteTabID: String? = nil
remoteTabID: String? = nil,
cloudDisplayMembershipViewID: String? = nil
) {
self.resource = resource
self.remoteWorkspaceID = remoteView?.workspace.id ?? remoteWorkspaceID
self.remoteTabID = remoteView?.tabID ?? remoteTabID
self.remoteTabID = remoteView?.isCloudDisplayMembershipView == true ? nil : (remoteView?.tabID ?? remoteTabID)
self.cloudDisplayMembershipViewID = remoteView?.cloudDisplayMembershipViewID ?? cloudDisplayMembershipViewID
}
}
12 changes: 6 additions & 6 deletions Sources/Cloud/CloudTreeNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ final class CloudTreeNode: NSObject {
var resourceSection: CloudTreeMachineResourceSection?
/// For workspace rows: everything the workspace holds, in the order it opens.
private var explicitDragGroup: SurfaceResourceGroup?

init(id: String, kind: Kind, children: [CloudTreeNode] = [], dragGroup: SurfaceResourceGroup? = nil, isPinned: Bool = false) {
self.id = id
self.kind = kind
Expand Down Expand Up @@ -422,20 +421,22 @@ enum CloudTreeNodeBuilder {
for resource in snapshot.resources where resource.remoteViews?.count == 1 {
singleViewResources.insert(resource.id)
}

for projection in snapshot.projections {
openResources.insert(projection.resource)
workspaceCountsByResource[projection.resource, default: [:]][projection.workspaceID, default: 0] += 1
if let remoteWorkspaceID = projection.remoteWorkspaceID {
if let remoteTabID = projection.remoteTabID, !remoteTabID.isEmpty {
if CloudTreeNodeBuilder.hasCloudDisplayMembershipProjection(projection, snapshot: snapshot, workspaceID: remoteWorkspaceID) {
workspaceOnly[RemoteWorkspaceIdentity(resource: projection.resource, workspaceID: remoteWorkspaceID), default: []].append(projection.workspaceID)
} else if let remoteTabID = projection.remoteTabID, !remoteTabID.isEmpty {
let identity = RemotePlacementIdentity(
resource: projection.resource,
workspaceID: remoteWorkspaceID,
tabID: remoteTabID
)
openPlacements.insert(identity)
exact[identity, default: []].append(projection.workspaceID)
} else if let views = resourceByID[projection.resource]?.remoteViews {
} else if let views = resourceByID[projection.resource]?.remoteViews,
!views.allSatisfy(\.isCloudDisplayMembershipView) {
// Intermediate builds persisted the workspace id before
// they persisted tab ids. Recover the tab only when the
// current graph has one unambiguous view in that
Expand Down Expand Up @@ -486,7 +487,6 @@ enum CloudTreeNodeBuilder {
}
}
}

func isOpen(_ resource: SurfaceResourceID, remoteView: SurfaceRemoteView?) -> Bool {
guard let remoteView else { return openResources.contains(resource) }
let identity = RemotePlacementIdentity(
Expand Down Expand Up @@ -876,7 +876,7 @@ enum CloudTreeNodeBuilder {
for workspace in info.remoteWorkspaces ?? [] {
byWorkspace[workspace.id] = RemoteWorkspaceRows(workspace: workspace)
}
for resource in resources {
for resource in snapshot.cloudWorkspaceResources(on: machine) {
for placement in remotePlacements(of: resource) {
var rows = byWorkspace[placement.workspace.id] ?? RemoteWorkspaceRows(workspace: placement.workspace)
switch resource.kind {
Expand Down
Loading
Loading