Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
826 changes: 826 additions & 0 deletions Resources/Localizable.xcstrings

Large diffs are not rendered by default.

54 changes: 34 additions & 20 deletions Sources/Cloud/CloudMachineSurfacePresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,43 @@ struct CloudMachineSurfacePresentation {
}

static func emptyPorts(info: SurfaceMachineInfo) -> CloudTreeNode {
let text: String
let style: CloudTreePlaceholder.Style
switch info.linkState {
case .connecting:
text = String(localized: "cloudTree.ports.loading", defaultValue: "Discovering ports…")
style = .connecting
case .error:
text = info.linkError ?? String(localized: "cloudTree.ports.failed", defaultValue: "Couldn’t discover ports. Refresh to retry.")
style = .error
case .asleep:
text = String(localized: "cloudTree.ports.asleep", defaultValue: "Open the machine to discover ports")
style = .dimmed
case .unavailable:
text = String(localized: "cloudTree.ports.unavailable", defaultValue: "Port discovery unavailable. Refresh to retry.")
style = .dimmed
case .connected, .notApplicable:
text = String(localized: "cloudTree.ports.empty", defaultValue: "No reachable ports")
style = .dimmed
}
let presentation = CloudPortsStatusPresentation.make(info: info)
let style: CloudTreePlaceholder.Style = {
switch info.portDiscoveryState {
case .loading:
return .connecting
case .notRequested:
switch info.linkState {
case .connecting: return .connecting
case .error: return .error
default: return .dimmed
}
case .unavailable, .stale:
return .error
case .empty, .unsupported, .available:
return .dimmed
}
}()
return CloudTreeNode(
id: "machine:\(info.id.rawValue)/ports/status",
kind: .placeholder(machine: info.id, CloudTreePlaceholder(
text: presentation.title,
style: style,
opensMachine: info.linkState == .asleep || presentation.action == .openMachine,
portStatus: presentation
))
)
}

static func portStatus(info: SurfaceMachineInfo) -> CloudTreeNode? {
guard info.portDiscoveryState.keepsStatusAlongsideRows else { return nil }
let presentation = CloudPortsStatusPresentation.make(info: info)
return CloudTreeNode(
id: "machine:\(info.id.rawValue)/ports/status",
kind: .placeholder(machine: info.id, CloudTreePlaceholder(
text: text, style: style, opensMachine: info.linkState == .asleep
text: presentation.title,
style: .error,
portStatus: presentation
))
)
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/Cloud/CloudPortsStatusAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

/// The action exposed by an actionable Ports status callout.
enum CloudPortsStatusAction: Equatable {
case none
case refresh
case openMachine
case openShell
}
106 changes: 106 additions & 0 deletions Sources/Cloud/CloudPortsStatusContent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import AppKit

/// Native, wrapping Ports guidance hosted above the outline's passthrough SwiftUI row.
@MainActor
final class CloudPortsStatusContent: NSView {
private let titleLabel = NSTextField(wrappingLabelWithString: "")
private let messageLabel = NSTextField(wrappingLabelWithString: "")
private let actionButton = NSButton(title: "", target: nil, action: nil)
private var presentation: CloudPortsStatusPresentation?
private var style = CloudTreeStyle.defaultStyle
private var actionHandler: (() -> Void)?

override var isFlipped: Bool { true }

override func hitTest(_ point: NSPoint) -> NSView? {
guard !actionButton.isHidden, actionButton.frame.contains(point) else { return nil }
return actionButton.hitTest(convert(point, to: actionButton))
}

override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
titleLabel.font = .systemFont(ofSize: 11, weight: .semibold)
titleLabel.maximumNumberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
messageLabel.maximumNumberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textColor = .secondaryLabelColor
actionButton.bezelStyle = .inline
actionButton.target = self
actionButton.action = #selector(performAction)
actionButton.setAccessibilityRole(.button)
addSubview(titleLabel)
addSubview(messageLabel)
addSubview(actionButton)
setAccessibilityRole(.group)
setAccessibilityIdentifier("CloudPortsStatus")
}

@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

func configure(
presentation: CloudPortsStatusPresentation,
style: CloudTreeStyle,
action: @escaping () -> Void
) {
self.presentation = presentation
self.style = style
actionHandler = action
titleLabel.stringValue = presentation.title
messageLabel.stringValue = presentation.message
actionButton.title = presentation.actionTitle ?? ""
actionButton.isHidden = presentation.action == .none || presentation.actionTitle == nil
actionButton.setAccessibilityLabel(presentation.actionTitle ?? presentation.title)
let fontSize = GlobalFontMagnification.scaledSize(max(10, style.detailSize))
titleLabel.font = .systemFont(ofSize: fontSize, weight: .semibold)
messageLabel.font = style.monospacedText
? .monospacedSystemFont(ofSize: fontSize, weight: .regular)
: .systemFont(ofSize: fontSize)
setAccessibilityLabel("\(presentation.title), \(presentation.message)")
needsLayout = true
}

override func layout() {
super.layout()
let width = max(1, bounds.width)
let inset: CGFloat = 2
let titleHeight = Self.textHeight(titleLabel.stringValue, font: titleLabel.font ?? .systemFont(ofSize: 11), width: width - inset * 2)
titleLabel.frame = NSRect(x: inset, y: 2, width: width - inset * 2, height: titleHeight)
let messageY = titleLabel.frame.maxY + 2
let messageHeight = Self.textHeight(messageLabel.stringValue, font: messageLabel.font ?? .systemFont(ofSize: 11), width: width - inset * 2)
messageLabel.frame = NSRect(x: inset, y: messageY, width: width - inset * 2, height: messageHeight)
if actionButton.isHidden {
actionButton.frame = .zero
} else {
actionButton.frame = NSRect(x: inset, y: messageLabel.frame.maxY + 4, width: actionButton.fittingSize.width, height: 22)
}
}

override var intrinsicContentSize: NSSize {
guard let presentation else { return NSSize(width: NSView.noIntrinsicMetric, height: 0) }
return NSSize(width: NSView.noIntrinsicMetric, height: Self.height(width: max(180, bounds.width), presentation: presentation, style: style))
}

static func height(width: CGFloat, presentation: CloudPortsStatusPresentation, style: CloudTreeStyle) -> CGFloat {
let fontSize = GlobalFontMagnification.scaledSize(max(10, style.detailSize))
let titleFont = NSFont.systemFont(ofSize: fontSize, weight: .semibold)
let messageFont = style.monospacedText
? NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
: NSFont.systemFont(ofSize: fontSize)
let contentWidth = max(1, width - 4)
let title = textHeight(presentation.title, font: titleFont, width: contentWidth)
let message = textHeight(presentation.message, font: messageFont, width: contentWidth)
let button = presentation.action == .none ? 0 : 26
return ceil(title + message + button + 10)
}

private static func textHeight(_ text: String, font: NSFont, width: CGFloat) -> CGFloat {
ceil(NSAttributedString(string: text, attributes: [.font: font]).boundingRect(
with: NSSize(width: max(1, width), height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading]
).height)
}

@objc private func performAction() { actionHandler?() }
}
159 changes: 159 additions & 0 deletions Sources/Cloud/CloudPortsStatusPresentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import Foundation

/// Localized, actionable copy for a Ports group that cannot show a trustworthy
/// list or route. The model is independent of AppKit so the state matrix stays
/// testable without constructing an outline view.
struct CloudPortsStatusPresentation: Equatable {
Comment thread
austinywang marked this conversation as resolved.
Resolved
let title: String
let message: String
let action: CloudPortsStatusAction
let actionTitle: String?

static func make(info: SurfaceMachineInfo) -> Self {
if case .notRequested = info.portDiscoveryState {
switch info.linkState {
case .connecting:
return Self(
title: String(localized: "cloudTree.ports.loading", defaultValue: "Discovering ports…"),
message: String(localized: "cloudTree.ports.loading.detail", defaultValue: "cmux is reading the machine’s listening services."),
action: .none,
actionTitle: nil
)
case .error:
return Self(
title: String(localized: "cloudTree.ports.failed", defaultValue: "Cloud link unavailable"),
message: info.linkError ?? String(localized: "cloudTree.ports.failed", defaultValue: "Reconnect the machine or refresh to discover ports again."),
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
)
case .asleep:
return Self(
title: String(localized: "cloudTree.ports.asleep", defaultValue: "Machine is asleep"),
message: String(localized: "cloudTree.ports.asleep.detail", defaultValue: "Wake the machine to discover its listening services."),
action: .openMachine,
actionTitle: String(localized: "cloudTree.ports.action.wake", defaultValue: "Wake Machine")
)
case .unavailable:
return Self(
title: String(localized: "cloudTree.ports.unavailable", defaultValue: "Port discovery unavailable"),
message: String(localized: "cloudTree.ports.unavailable.detail", defaultValue: "Refresh to retry port discovery."),
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
)
case .connected, .notApplicable:
break
}
}
let routeNote = String(
localized: "cloudTree.ports.routeNote",
defaultValue: "cmux’s authenticated in-app forwarding works without the system-wide Cloud VPN. Safari, Chrome, and other Mac apps using the machine’s private address do require Cloud VPN."
)
switch info.portDiscoveryState {
case .notRequested:
return Self(
title: String(localized: "cloudTree.ports.empty", defaultValue: "No reachable ports"),
message: String(localized: "cloudTree.ports.notRequested", defaultValue: "Expand this group to scan the machine for reachable services."),
action: .none,
actionTitle: nil
)
case .loading:
return Self(
title: String(localized: "cloudTree.ports.loading", defaultValue: "Discovering ports…"),
message: String(localized: "cloudTree.ports.loading.detail", defaultValue: "cmux is reading the machine’s listening services."),
action: .none,
actionTitle: nil
)
case .available:
return Self(
title: String(localized: "cloudTree.ports.empty", defaultValue: "No reachable ports"),
message: routeNote,
action: .none,
actionTitle: nil
)
case .empty(let reason):
switch reason {
case .noListeningService:
return Self(
title: String(localized: "cloudTree.ports.empty", defaultValue: "No reachable ports"),
message: String(
format: String(localized: "cloudTree.ports.empty.noService", defaultValue: "No service is listening on a reachable port. Start an HTTP service on the machine, then refresh. %@"),
routeNote
),
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
)
case .loopbackOnly:
return Self(
title: String(localized: "cloudTree.ports.empty", defaultValue: "No reachable ports"),
message: String(
format: String(localized: "cloudTree.ports.empty.loopback", defaultValue: "A service is listening only on loopback. Bind it to the machine network, then refresh. %@"),
routeNote
),
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
)
}
case .unavailable(let reason):
switch reason {
case .privateAddress:
let privateAddressMessage = String(
format: String(localized: "cloudTree.port.noPrivateAddress", defaultValue: "%@ has no private network address yet; refresh the machine list and retry."),
info.id.rawValue
)
return Self(
title: String(localized: "cloudTree.ports.privateAddress", defaultValue: "Private address unavailable"),
message: "\(privateAddressMessage) \(routeNote)",
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
)
case .machineAsleep:
return Self(
title: String(localized: "cloudTree.ports.asleep", defaultValue: "Open the machine to discover ports"),
message: String(localized: "cloudTree.ports.asleep.detail", defaultValue: "Wake the machine to discover its listening services."),
action: .openMachine,
actionTitle: String(localized: "cloudTree.ports.action.wake", defaultValue: "Wake Machine")
)
case .link:
let linkMessage = String(localized: "cloudTree.ports.failed", defaultValue: "Couldn’t discover ports. Refresh to retry.")
return Self(
title: String(localized: "cloudTree.ports.failed", defaultValue: "Couldn’t discover ports. Refresh to retry."),
message: "\(linkMessage) \(routeNote)",
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
)
case .transport:
let transportMessage = String(localized: "cloudTree.ports.unavailable", defaultValue: "Port discovery unavailable. Refresh to retry.")
return Self(
title: String(localized: "cloudTree.ports.unavailable", defaultValue: "Port discovery unavailable. Refresh to retry."),
message: "\(transportMessage) \(routeNote)",
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh Ports")
)
case .hub:
return Self(
title: String(localized: "cloudTree.ports.unavailable", defaultValue: "Port discovery unavailable. Refresh to retry."),
message: String(localized: "cloudTree.ports.hub.detail", defaultValue: "This build cannot start cmux’s authenticated Cloud forward. Update cmux, then retry."),
action: .refresh,
actionTitle: String(localized: "common.retry", defaultValue: "Retry")
)
}
case .stale:
return Self(
title: String(localized: "cloudTree.ports.stale", defaultValue: "Port list may be out of date"),
message: String(
format: String(localized: "cloudTree.ports.stale.detail", defaultValue: "Reconnect or refresh before relying on this list. %@"),
routeNote
),
action: .refresh,
actionTitle: String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")
)
case .unsupported:
return Self(
title: String(localized: "cloudTree.ports.unsupported", defaultValue: "Ports are not supported by this provider."),
message: String(format: String(localized: "cloudTree.port.unsupported", defaultValue: "%@’s provider cannot open machine ports as previews; reach the service from inside the machine with `cmux vm exec %@ -- …`."), info.id.rawValue, info.id.rawValue),
action: .openShell,
actionTitle: String(localized: "machines.menu.openShell", defaultValue: "Open Shell")
)
}
}
}
Loading
Loading