Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
143 commits
Select commit Hold shift + click to select a range
586d493
test: distinguish unreachable Cloud port listeners
austinywang Sep 20, 2026
bf35b44
fix: explain Cloud Ports discovery and VPN state
austinywang Sep 20, 2026
45dff08
fix: address Ports status review findings
austinywang Sep 20, 2026
3f540bc
refactor: split Cloud Ports status types
austinywang Sep 20, 2026
86e060a
fix: preserve Cloud Ports interaction and sleep state
austinywang Sep 20, 2026
9d36260
fix: resolve remaining Ports review blockers
austinywang Sep 20, 2026
dabb3f9
fix: defer Cloud port scans until requested
austinywang Sep 20, 2026
485ef8e
fix: handle default Ports discovery and status tests
austinywang Sep 20, 2026
2f29512
fix: preserve unsupported ports and refresh route metadata
austinywang Sep 20, 2026
6cf2ed3
fix: fence Cloud port retries and sizing
austinywang Sep 20, 2026
fcdf5dd
fix: avoid recursive Ports sizing and fleet retries
austinywang Sep 20, 2026
a572257
fix: finish Ports status layout and interaction
austinywang Sep 20, 2026
1a79922
Merge origin/main into Cloud Ports VPN state
austinywang Sep 20, 2026
e0deb0e
fix: keep Ports action hit testing direct
austinywang Sep 20, 2026
23897d7
fix: keep unsupported provider state through link warnings
austinywang Sep 20, 2026
6c0afd6
style: keep provider state guard within budget
austinywang Sep 20, 2026
8b87c8b
fix: refresh Cloud metadata before port retry
austinywang Sep 20, 2026
8edc9be
fix: scope metadata retry and style status insets
austinywang Sep 20, 2026
9f10463
fix: align Ports discovery with authenticated browser routing
austinywang Sep 20, 2026
0b4817d
fix: use scoped port refresh from the Ports menu
austinywang Sep 20, 2026
b384511
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang Sep 20, 2026
1d2eb9a
fix: match port classification to in-app loopback route
austinywang Sep 20, 2026
f66f4fa
fix: keep Cloud Ports status height type-safe
austinywang Sep 20, 2026
8854ad3
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang Sep 20, 2026
7badfc1
test: evaluate mutating port discovery assertions
austinywang Sep 20, 2026
7b38e40
ci: retrigger hosted Swift verification
austinywang Sep 20, 2026
b1d8dfc
test: match localized no-service guidance
austinywang Sep 20, 2026
f371cc0
test: align port identity and native action assertions
austinywang Sep 20, 2026
ae087d1
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang Sep 20, 2026
3aae903
fix: preserve explicit Cloud port discovery demand
austinywang Sep 20, 2026
886e9dd
fix: invalidate Cloud port rows after width changes
austinywang Sep 20, 2026
af25072
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 13234…
austinywang Sep 21, 2026
7306395
test: align Cloud tree expectations with current sections
austinywang Sep 21, 2026
b495c49
fix: restore Cloud VPN setup affordance in Ports
austinywang Sep 21, 2026
ae1f2f5
fix: clarify VPN state in initial Ports status
austinywang Sep 21, 2026
a67648b
test: require persistent Ports VPN help and independent discovery
austinywang Sep 21, 2026
fbf211c
feat: restore contextual Cloud VPN setup
austinywang Sep 21, 2026
009c4e0
fix: make Cloud VPN setup stateful and shared
austinywang Sep 21, 2026
37282fc
fix: preserve VPN refusal and explain Ports routing
austinywang Sep 21, 2026
254e8c8
test: match initial Ports route guidance
austinywang Sep 21, 2026
bd3cc15
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang Sep 21, 2026
d73e290
chore: preserve main test wiring during merge
austinywang Sep 21, 2026
37d101e
chore: preserve main test wiring during merge
austinywang Sep 21, 2026
52df7f0
chore: wire Cloud port regression tests
austinywang Sep 21, 2026
e612bed
fix: retain current device surface model after main merge
austinywang Sep 21, 2026
179b8d9
fix: retain Cloud Ports target sources after main merge
austinywang Sep 21, 2026
4a480e0
chore: wire current CLI config validation
austinywang Sep 21, 2026
ccbd0c0
fix: merge current main project with Cloud Ports sources
austinywang Sep 21, 2026
7a59839
fix: point merged Cloud source refs at their paths
austinywang Sep 21, 2026
61cb1fb
fix: merge Cloud machine value model with device support
austinywang Sep 21, 2026
51fb7d5
fix: restore task project sources and current test wiring
austinywang Sep 21, 2026
f4f9f56
fix: remove stale main source references after merge
austinywang Sep 21, 2026
dcbd385
fix: keep Cloud model aligned with task target
austinywang Sep 21, 2026
622d5a2
fix: restore Cloud device target wiring
austinywang Sep 22, 2026
9af2192
test: keep VPN setup visible beside discovered ports
austinywang Sep 22, 2026
dc5f6d9
fix: restore visible Cloud VPN guidance for Ports
austinywang Sep 22, 2026
c89213c
fix: pass VPN guidance state in initializer order
austinywang Sep 22, 2026
a164e98
fix: order Cloud tree guidance arguments
austinywang Sep 22, 2026
af21933
merge: integrate origin/main with Cloud VPN Ports guidance
austinywang Sep 25, 2026
35f3cb7
merge: integrate origin/main with Cloud VPN Ports guidance
austinywang Sep 25, 2026
9069c61
fix: wire Cloud port discovery state owner
austinywang Sep 25, 2026
0a965c6
fix: import packaged Cloud port models
austinywang Sep 25, 2026
699053d
fix: import surface catalog model for socket payloads
austinywang Sep 25, 2026
2717e21
fix: import packaged Cloud types in app extensions
austinywang Sep 25, 2026
2ff0e8e
fix: align Cloud port retry with shared link provider
austinywang Sep 25, 2026
0db3b73
fix: import packaged tunnel state in VPN setup model
austinywang Sep 25, 2026
0140292
fix: expose Cloud tree leading geometry to app outline
austinywang Sep 25, 2026
5e11a93
fix: restore explicit Cloud port discovery demand
austinywang Sep 25, 2026
5ca8b63
fix: import packaged tree placeholder in port builder
austinywang Sep 25, 2026
a27826a
fix: defer Cloud Ports scans and activate VPN help
austinywang Sep 25, 2026
6c8b90c
merge: integrate origin/main and remove Ports VPN header help
austinywang Sep 25, 2026
aad1a85
merge: integrate origin/main into Cloud Ports VPN state
austinywang Sep 26, 2026
d387d23
fix: restore Cloud Ports project wiring and VPN setup close shortcut
austinywang Sep 26, 2026
eb9fee8
fix: compile Cloud Ports against current main
austinywang Sep 26, 2026
b2490f1
merge: integrate origin/main into Cloud Ports VPN state
austinywang Sep 26, 2026
ec92fb5
tests: import package modules in Cloud Ports tests
austinywang Sep 26, 2026
988703a
tests: SSH Ports discover over loopback without Cloud VPN guidance
austinywang Sep 26, 2026
aad96b5
fix: route SSH Ports discovery over loopback and settle failed scans
austinywang Sep 26, 2026
67c3261
fix: restore Cloud tree row heights lost in the main merge
austinywang Sep 26, 2026
a0a97d4
fix: install a retried machine's private route on the shared link
austinywang Sep 26, 2026
78af234
fix: drop another machine's ports from an unavailable scan
austinywang Sep 26, 2026
d1124b3
tests: align Ports tests with closed-by-default groups and loopback b…
austinywang Sep 26, 2026
cc4198a
Merge main (f39a1e59c6f5) into 13234-cloud-ports-vpn-state
austinywang Sep 26, 2026
d7d2b15
tests: a Ports rescan keeps its settled inventory and failed links se…
austinywang Sep 26, 2026
c4163e8
fix: fence Ports scans by request so summary polls and cached scans c…
austinywang Sep 26, 2026
36ce5bc
Merge main (5a698502379a) into 13234-cloud-ports-vpn-state
austinywang Sep 26, 2026
20e02ee
tests: a rescan's Ports rows survive a cached refresh that retires it…
austinywang Sep 26, 2026
e319940
fix: publish a rescan's Ports rows on its request fence, not the refr…
austinywang Sep 26, 2026
2a7222b
tests: a snapshot that outlives a rescan keeps the rescan's Ports rows
austinywang Sep 26, 2026
ffec561
fix: publish the Ports inventory accepted during a refresh's snapshot…
austinywang Sep 26, 2026
f678dbf
tests: expect Ports to start collapsed and unchecked in the Cloud tree
austinywang Sep 26, 2026
3383c97
tests: give each Ports recovery fixture its own SSH machine
austinywang Sep 26, 2026
0428218
fix: size the Ports group row like every other Cloud tree group
austinywang Sep 26, 2026
dc3e3cc
fix: keep the checklist popover's detach mark through its animated close
austinywang Sep 26, 2026
e7da910
Merge main (fb665a08d49a) into 13234-cloud-ports-vpn-state
austinywang Sep 26, 2026
4382462
Merge main (cc9065904a55) into 13234-cloud-ports-vpn-state
austinywang Sep 26, 2026
aa44f87
tests: find re-presented popover windows by strong reference
austinywang Sep 26, 2026
9b04bb6
test: Ports actions reach machines under the Cloud Machines section
austinywang Sep 26, 2026
2620606
test: loopback aliases are not other-interface Ports listeners
austinywang Sep 26, 2026
45be5c9
fix: Ports actions find sectioned machines; ignore loopback aliases
austinywang Sep 26, 2026
05e3352
Merge commit '83270f163768380d45fe24e21b1ba5361ffbcee8' into 13234-cl…
austinywang Sep 27, 2026
516caf5
fix: retranslate Cloud VPN setup guidance to match current English copy
austinywang Sep 27, 2026
7db5ff0
test: routine link failure before demand keeps Ports not checked yet
austinywang Sep 27, 2026
94977f2
Merge commit '4c5272e9153eca2033c9f40ac749f0c3a5bcb291' into 13234-cl…
austinywang Sep 27, 2026
d53b4cd
test: pin Cloud outline attention test to 100% magnification
austinywang Sep 27, 2026
c7af924
Localize Cloud Ports status copy for every catalog locale
austinywang Sep 27, 2026
3cf7f98
test: a requested machine gaining a Ports route scans and stays re-re…
austinywang Sep 28, 2026
79518b6
test: cancelled and SSH Ports refreshes settle without stale loading …
austinywang Sep 28, 2026
ad7ffe9
Settle a cancelled Ports request and keep SSH ids out of Cloud metada…
austinywang Sep 28, 2026
80411d8
test: Ports settles after a cancel during display discovery and keeps…
austinywang Sep 28, 2026
9161249
Settle a Ports request cancelled during display discovery without hid…
austinywang Sep 28, 2026
c648233
Merge main (3324f6311893) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
e88d277
Keep Localizable.xcstrings in main's key order
austinywang Sep 28, 2026
44abc14
Merge main (e28555a2ecdf) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
c171271
test: turning Cloud VPN off rebuilds the cached tree with Ports guidance
austinywang Sep 28, 2026
cb48d3d
test: Ports and Settings open one Cloud VPN pane instead of a window
austinywang Sep 28, 2026
c9a04f1
Open Cloud VPN setup as a pane instead of a window
austinywang Sep 28, 2026
cb70bc9
test: Settings brings the main window forward to the Cloud VPN pane
austinywang Sep 28, 2026
3ff7a0d
Bring the main window forward when Settings opens Cloud VPN setup
austinywang Sep 28, 2026
831806c
Merge main (446581e109ef) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
4bac62c
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 13234…
austinywang Sep 28, 2026
875c067
Cover Settings reopening the Cloud VPN pane after it closes
austinywang Sep 28, 2026
aec5cdf
Merge main (f8079082fc46) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
3fd1dcd
Merge main (528c5a870dc2) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
26f380b
Merge commit 'a98c560063fc114d1dd11ad9d7fa8f15c3a8ed4b' into 13234-cl…
austinywang Sep 28, 2026
b7c156b
Merge main (0ebf8d7f4789) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
95744f3
Merge main (356443354a63) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
d453aac
test: Ports status rows fit every wrapped line
austinywang Sep 28, 2026
1289156
Size Ports status rows with the label cell that draws them
austinywang Sep 28, 2026
8883562
Merge main (f7ee3bc64db5) into 13234-cloud-ports-vpn-state
austinywang Sep 28, 2026
7713ad0
test: Ports never pair VPN setup guidance with another status row
austinywang Sep 28, 2026
f8cea99
Show Cloud VPN setup guidance only beside a finished scan's ports
austinywang Sep 28, 2026
f2be9e6
test: clarify Ports when Cloud VPN is not configured
austinywang Sep 29, 2026
724f164
Make Cloud Ports clear without VPN setup
austinywang Sep 29, 2026
39fe8a9
Merge main (6485102d2e2f) into 13234-cloud-ports-vpn-state
austinywang Sep 29, 2026
d89e69d
Update unnamed port tooltip coverage for the no-VPN flow
austinywang Sep 29, 2026
ed1a765
test: exclude container runtime listeners from Cloud app ports
austinywang Sep 29, 2026
6e15922
Exclude container runtime management listeners from Cloud Ports
austinywang Sep 29, 2026
60ff6d5
Read root-owned port metadata through existing guest permissions
austinywang Sep 29, 2026
1d4fe79
Use the existing TcpListener import in the discovery regression
austinywang Sep 29, 2026
b7f085d
Explain the empty Ports state without VPN jargon
austinywang Sep 29, 2026
260e689
Match Cloud VPN setup with the established onboarding page
austinywang Sep 29, 2026
e0e3119
Merge main into Cloud Ports VPN branch
austinywang Sep 29, 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
@@ -0,0 +1,10 @@
import Foundation

/// The action exposed by an actionable Ports status callout.
public enum CloudPortsStatusAction: Equatable, Sendable {
case none
case refresh
case setupVPN
case openMachine
case openShell
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import CmuxSurfaceCatalogModel
import Foundation

/// One immutable Ports reason shared by native status rows and their actions.
public struct CloudPortsStatusPresentation: Equatable, Sendable {
public let state: CloudPortDiscoveryState
public let isVPNGuidance: Bool

public init(state: CloudPortDiscoveryState, isVPNGuidance: Bool = false) {
self.state = state
self.isVPNGuidance = isVPNGuidance
}

public static var vpnGuidance: Self {
Self(state: .available, isVPNGuidance: true)
}

public static func make(info: SurfaceMachineInfo) -> Self {
if info.portDiscoveryState == .notRequested {
switch info.linkState {
case .asleep: return Self(state: .unavailable(.machineAsleep))
case .error: return Self(state: .unavailable(.link))
case .unavailable: return Self(state: .unavailable(.transport))
default: break
}
}
return Self(state: info.portDiscoveryState)
}

public var title: String {
if isVPNGuidance {
return String(localized: "cloud.ports.vpnOff.title", defaultValue: "Optional: Cloud VPN")
}
switch state {
case .notRequested:
return String(localized: "cloudTree.ports.notRequested.title", defaultValue: "Ports not checked yet")
case .loading:
return String(localized: "cloudTree.ports.loading", defaultValue: "Discovering ports…")
case .available:
return String(localized: "cloudTree.ports.available.title", defaultValue: "Listening services")
case .loopbackOnly:
return String(localized: "cloudTree.ports.loopback.title", defaultValue: "Loopback services available")
case .empty(.noListeningService):
return String(localized: "cloudTree.ports.empty", defaultValue: "No ports yet")
case .empty(.otherInterfaceOnly):
return String(localized: "cloudTree.ports.binding.title", defaultValue: "Services use another interface")
case .unavailable(.privateAddress):
return String(localized: "cloudTree.ports.privateAddress", defaultValue: "Private address unavailable")
case .unavailable(.machineAsleep):
return String(localized: "cloudTree.ports.asleep", defaultValue: "Open the machine to discover ports")
case .unavailable(.link):
return String(localized: "cloudTree.ports.link.title", defaultValue: "Cloud link unavailable")
case .unavailable:
return String(localized: "cloudTree.ports.unavailable", defaultValue: "Port discovery unavailable. Refresh to retry.")
case .stale:
return String(localized: "cloudTree.ports.stale", defaultValue: "Port list may be out of date")
case .unsupported:
return String(localized: "cloudTree.ports.unsupported", defaultValue: "Ports are not supported by this provider.")
}
}

public var message: String {
if isVPNGuidance {
return String(
localized: "cloud.ports.vpnOff.explanation",
defaultValue: "Ports open in cmux without setup. To use private addresses in other apps, set up Cloud VPN."
)
}
switch state {
case .notRequested:
return Self.routeNote
case .loading:
return String(localized: "cloudTree.ports.loading.detail", defaultValue: "Checking services through cmux’s authenticated Cloud link. No system VPN is needed.")
case .available:
return Self.routeNote
case .loopbackOnly:
return String(localized: "cloudTree.ports.loopback.detail", defaultValue: "These services open in cmux without Cloud VPN. They listen on the machine’s loopback address, so other apps cannot reach them through its private IP, even with VPN connected.")
case .empty(.noListeningService):
return String(localized: "cloudTree.ports.empty.noService", defaultValue: "Start a web server on this machine, then refresh.")
case .empty(.otherInterfaceOnly):
return String(localized: "cloudTree.ports.binding.detail", defaultValue: "cmux’s browser route connects to 127.0.0.1 on the machine. Bind the service to 127.0.0.1 or 0.0.0.0, then refresh. Turning on Cloud VPN does not change this route.")
case .unavailable(.privateAddress):
return String(localized: "cloudTree.ports.privateAddress.detail", defaultValue: "cmux’s authenticated route needs this machine’s private address. Refresh to retrieve it. Turning on Cloud VPN does not assign a missing address.")
case .unavailable(.machineAsleep):
return String(localized: "cloudTree.ports.asleep.detail", defaultValue: "Wake the machine to check its services. cmux’s in-app forwarding does not need Cloud VPN.")
case .unavailable(.link):
return String(localized: "cloudTree.ports.link.detail", defaultValue: "Port discovery could not connect to this machine. Existing cmux ports may still work. Refresh to reconnect; Cloud VPN is not required.")
case .unavailable(.transport):
return String(localized: "cloudTree.ports.unavailable.detail", defaultValue: "The port scan failed; this does not mean no service is listening. Existing cmux ports may still work. Refresh to retry; Cloud VPN is not required.")
case .unavailable(.hub):
return String(localized: "cloudTree.ports.hub.detail", defaultValue: "This build cannot start cmux’s authenticated Cloud forward. Update cmux, then retry.")
case .stale:
return String(localized: "cloudTree.ports.stale.detail", defaultValue: "The last scan could not be refreshed. Listed services may have changed; refresh to check again. cmux forwarding does not need Cloud VPN.")
case .unsupported:
return String(localized: "cloudTree.ports.unsupported.detail", defaultValue: "This provider has no supported in-app port route. Open a shell to access the service inside the machine. Cloud VPN does not add provider support.")
}
}

public static var routeNote: String {
String(localized: "cloudTree.ports.routeNote", defaultValue: "cmux forwards ports without Cloud VPN. Web browsers and other Mac apps need Cloud VPN to reach services bound to the machine’s private address.")
}

public var action: CloudPortsStatusAction {
if isVPNGuidance { return .setupVPN }
switch state {
case .unavailable(.machineAsleep): return .openMachine
case .unsupported: return .openShell
case .loading, .available, .loopbackOnly: return .none
default: return .refresh
}
}

public var actionTitle: String? {
switch action {
case .none: return nil
case .refresh: return String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
case .setupVPN: return String(localized: "cloudTree.ports.setupVPN", defaultValue: "Set Up VPN…")
case .openMachine: return String(localized: "cloudTree.ports.action.wake", defaultValue: "Wake Machine")
case .openShell: return String(localized: "machines.menu.openShell", defaultValue: "Open Shell")
}
}

public var style: CloudTreePlaceholder.Style {
if isVPNGuidance { return .dimmed }
switch state {
case .loading: return .connecting
case .unavailable(.machineAsleep), .notRequested, .available, .loopbackOnly, .empty, .unsupported: return .dimmed
case .unavailable, .stale: return .error
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CoreGraphics
import CmuxFoundation

/// Geometry shared by the Cloud outline's document and row content.
///
Expand Down Expand Up @@ -36,4 +37,13 @@ public struct CloudTreeLayoutMetrics: Equatable, Sendable {
) -> CGFloat {
max(0, rowWidth - leadingContentWidth - trailingContentWidth - referenceInset)
}
/// Horizontal origin shared by AppKit cell frames and wrapping Ports status measurements.
public func contentLeading(level: Int, style: CloudTreeStyle, magnification: Int = GlobalFontMagnification.storedPercent) -> CGFloat {
GlobalFontMagnification.scaledSize(8 + CGFloat(max(0, level)) * style.indentPerLevel, percent: magnification)
+ GlobalFontMagnification.scaledSize(style.rowGrid.disclosureSlot + style.rowGrid.disclosureGap, percent: magnification)
}

public func portsContentWidth(columnWidth: CGFloat, level: Int, style: CloudTreeStyle) -> CGFloat {
max(1, columnWidth - contentLeading(level: level, style: style) - style.rowGrid.trailingPadding)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ public struct CloudTreePlaceholder: Equatable, Sendable {
public let style: Style
/// Only wake placeholders set this. Empty resource categories remain inert.
public let opensMachine: Bool
public init(text: String, style: Style, opensMachine: Bool = false) {
public let portStatus: CloudPortsStatusPresentation?
public init(text: String, style: Style, opensMachine: Bool = false, portStatus: CloudPortsStatusPresentation? = nil) {
self.text = text
self.style = style
self.opensMachine = opensMachine
self.portStatus = portStatus
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// Snapshot of the caller's Cloud Machines plan for the settings section.
public struct CloudMachinesPlanSummary: Equatable, Sendable {
/// Localized display name of the current plan.
Comment thread
austinywang marked this conversation as resolved.
public let planLabel: String
/// Number of machines currently counted against the plan.
public let activeMachines: Int
/// Active-machine ceiling; nil when the plan has no cap.
public let maxMachines: Int?
/// Whether the plan permits paid machine provisioning.
public let isPaidPlan: Bool

/// Creates a plan summary.
/// - Parameters:
/// - planLabel: Display name of the plan, already localized.
/// - activeMachines: Machines currently counted against the plan.
/// - maxMachines: Active-machine ceiling, or nil when the plan has no cap.
/// - isPaidPlan: Whether the plan is one the backend provisions for.
public init(planLabel: String, activeMachines: Int, maxMachines: Int?, isPaidPlan: Bool) {
self.planLabel = planLabel
self.activeMachines = activeMachines
self.maxMachines = maxMachines
self.isPaidPlan = isPaidPlan
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ public protocol SettingsHostActions: AnyObject {
/// Reveals the right-sidebar Machines panel in the active main window.
func openCloudMachinesPanel()

/// Opens the optional system-wide VPN explanation and its explicit connection controls.
func openCloudVPNSetup()

/// Opens the host's plan management / upgrade flow.
func openCloudMachinesBilling()

Expand Down Expand Up @@ -409,28 +412,6 @@ public struct AutomationRulesStatus: Equatable, Sendable {
}
}

/// Snapshot of the caller's Cloud Machines plan for the settings section.
public struct CloudMachinesPlanSummary: Equatable, Sendable {
public let planLabel: String
public let activeMachines: Int
/// Active-machine ceiling; nil when the plan has no cap.
public let maxMachines: Int?
public let isPaidPlan: Bool

/// Creates a plan summary.
/// - Parameters:
/// - planLabel: Display name of the plan, already localized.
/// - activeMachines: Machines currently counted against the plan.
/// - maxMachines: Active-machine ceiling, or nil when the plan has no cap.
/// - isPaidPlan: Whether the plan is one the backend provisions for.
public init(planLabel: String, activeMachines: Int, maxMachines: Int?, isPaidPlan: Bool) {
self.planLabel = planLabel
self.activeMachines = activeMachines
self.maxMachines = maxMachines
self.isPaidPlan = isPaidPlan
}
}

/// One right-sidebar tab as the Sidebar section's customization card renders
/// it. `id` is the host's stable mode identifier (the mode raw value).
public struct RightSidebarTabSettingsItem: Identifiable, Equatable, Sendable {
Expand Down Expand Up @@ -498,6 +479,7 @@ public extension SettingsHostActions {
var isCloudMachinesAvailable: Bool { false }
func cloudMachinesPlanSummary() async -> CloudMachinesPlanSummary? { nil }
func openCloudMachinesPanel() {}
func openCloudVPNSetup() {}
func openCloudMachinesBilling() {}

/// No release-app switch for previews, tests, and package-only hosts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import SwiftUI

/// **Cloud Machines** section — the plan card for persistent cloud VMs.
/// Deliberately small: machines are managed from the right-sidebar Machines
/// panel; Settings shows the plan meter and the two entry points (open the
/// panel, manage the plan). Renders nothing when the host doesn't expose
/// panel; Settings also exposes the plan and optional system VPN setup.
/// Renders nothing when the host doesn't expose
/// Cloud Machines, so the section is invisible to users outside the flag.
@MainActor
public struct CloudMachinesSection: View {
Expand All @@ -26,11 +26,14 @@ public struct CloudMachinesSection: View {
planRow
Divider().padding(.horizontal, 14)
panelRow
Divider().padding(.horizontal, 14)
vpnRow
}
}
.settingsSearchAnchors([
"setting:cloudMachines:plan",
"setting:cloudMachines:open-panel",
"setting:cloudMachines:vpn",
])
.task {
plan = await hostActions.cloudMachinesPlanSummary()
Expand Down Expand Up @@ -82,6 +85,27 @@ public struct CloudMachinesSection: View {
.id("setting:cloudMachines:open-panel")
}

private var vpnRow: some View {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 2) {
Text(String(localized: "cloud.vpn.setup.entry.subtitle", defaultValue: "Optional private IP access for other apps"))
Text(String(localized: "cloud.vpn.setup.howItWorks.body", defaultValue: "Connect Safari, Chrome, and other apps to your Cloud machines. Each machine keeps its private IP address and original ports. Only traffic to your Cloud network uses this encrypted connection. cmux terminals, Ports, and Desktop work without it."))
.font(.system(size: 11))
.foregroundColor(.secondary)
}
Spacer()
Button(String(localized: "cloudTree.ports.setupVPN", defaultValue: "Set Up VPN…")) {
hostActions.openCloudVPNSetup()
}
.buttonStyle(.bordered)
.controlSize(.small)
.accessibilityIdentifier("SettingsCloudVPNSetup")
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.id("setting:cloudMachines:vpn")
}

private var planSubtitle: String {
guard let plan else {
return hasLoaded
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

/// Why a completed Cloud scan produced no listeners usable by the authenticated browser proxy.
public enum CloudPortDiscoveryEmptyReason: String, Codable, Hashable, Sendable {
case noListeningService = "no_listening_service"
case otherInterfaceOnly = "other_interface_only"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation

/// The authoritative state of a Cloud machine's demand-driven port scan.
public enum CloudPortDiscoveryState: Hashable, Codable, Sendable {
/// Ports have not been requested yet; expanding the Ports group starts a scan.
case notRequested
/// A scan is in flight. Existing port rows remain visible while it runs.
case loading
/// A completed scan found at least one reachable service.
case available
/// The proxy can open these services; a system VPN cannot reach their loopback-only bindings.
case loopbackOnly
/// A completed scan found no service that cmux can reach.
case empty(CloudPortDiscoveryEmptyReason)
/// The scan or the route could not be used to produce a trustworthy result.
case unavailable(CloudPortDiscoveryUnavailableReason)
/// The last graph is retained, but a reconnect or refresh no longer proves it current.
case stale
/// The provider does not advertise Cloud port discovery or preview support.
case unsupported

/// Whether the Ports group should keep a status row after real port rows.
public var keepsStatusAlongsideRows: Bool {
switch self {
case .unavailable, .stale, .unsupported, .loading:
return true
case .notRequested, .available, .loopbackOnly, .empty:
return false
}
}

/// Stable, nonlocalized value used by socket exports and diagnostics.
public var wireValue: String {
switch self {
case .notRequested: return "not_requested"
case .loading: return "loading"
case .available: return "available"
case .loopbackOnly: return "loopback_only"
case .empty(let reason): return "empty_\(reason.rawValue)"
case .unavailable(let reason): return "unavailable_\(reason.rawValue)"
case .stale: return "stale"
case .unsupported: return "unsupported"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

/// Why Cloud port discovery or its in-app route is currently unavailable.
public enum CloudPortDiscoveryUnavailableReason: String, Codable, Hashable, Sendable {
case link
case transport
case privateAddress = "private_address"
case machineAsleep = "machine_asleep"
case hub
}
Loading
Loading