Repository navigation
Clarify Cloud Ports and use the established VPN onboarding page #13239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 bf35b44
fix: explain Cloud Ports discovery and VPN state
austinywang 45dff08
fix: address Ports status review findings
austinywang 3f540bc
refactor: split Cloud Ports status types
austinywang 86e060a
fix: preserve Cloud Ports interaction and sleep state
austinywang 9d36260
fix: resolve remaining Ports review blockers
austinywang dabb3f9
fix: defer Cloud port scans until requested
austinywang 485ef8e
fix: handle default Ports discovery and status tests
austinywang 2f29512
fix: preserve unsupported ports and refresh route metadata
austinywang 6cf2ed3
fix: fence Cloud port retries and sizing
austinywang fcdf5dd
fix: avoid recursive Ports sizing and fleet retries
austinywang a572257
fix: finish Ports status layout and interaction
austinywang 1a79922
Merge origin/main into Cloud Ports VPN state
austinywang e0deb0e
fix: keep Ports action hit testing direct
austinywang 23897d7
fix: keep unsupported provider state through link warnings
austinywang 6c0afd6
style: keep provider state guard within budget
austinywang 8b87c8b
fix: refresh Cloud metadata before port retry
austinywang 8edc9be
fix: scope metadata retry and style status insets
austinywang 9f10463
fix: align Ports discovery with authenticated browser routing
austinywang 0b4817d
fix: use scoped port refresh from the Ports menu
austinywang b384511
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang 1d2eb9a
fix: match port classification to in-app loopback route
austinywang f66f4fa
fix: keep Cloud Ports status height type-safe
austinywang 8854ad3
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang 7badfc1
test: evaluate mutating port discovery assertions
austinywang 7b38e40
ci: retrigger hosted Swift verification
austinywang b1d8dfc
test: match localized no-service guidance
austinywang f371cc0
test: align port identity and native action assertions
austinywang ae087d1
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang 3aae903
fix: preserve explicit Cloud port discovery demand
austinywang 886e9dd
fix: invalidate Cloud port rows after width changes
austinywang af25072
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 13234…
austinywang 7306395
test: align Cloud tree expectations with current sections
austinywang b495c49
fix: restore Cloud VPN setup affordance in Ports
austinywang ae1f2f5
fix: clarify VPN state in initial Ports status
austinywang a67648b
test: require persistent Ports VPN help and independent discovery
austinywang fbf211c
feat: restore contextual Cloud VPN setup
austinywang 009c4e0
fix: make Cloud VPN setup stateful and shared
austinywang 37282fc
fix: preserve VPN refusal and explain Ports routing
austinywang 254e8c8
test: match initial Ports route guidance
austinywang bd3cc15
Merge remote-tracking branch 'origin/main' into 13234-cloud-ports-vpn…
austinywang d73e290
chore: preserve main test wiring during merge
austinywang 37d101e
chore: preserve main test wiring during merge
austinywang 52df7f0
chore: wire Cloud port regression tests
austinywang e612bed
fix: retain current device surface model after main merge
austinywang 179b8d9
fix: retain Cloud Ports target sources after main merge
austinywang 4a480e0
chore: wire current CLI config validation
austinywang ccbd0c0
fix: merge current main project with Cloud Ports sources
austinywang 7a59839
fix: point merged Cloud source refs at their paths
austinywang 61cb1fb
fix: merge Cloud machine value model with device support
austinywang 51fb7d5
fix: restore task project sources and current test wiring
austinywang f4f9f56
fix: remove stale main source references after merge
austinywang dcbd385
fix: keep Cloud model aligned with task target
austinywang 622d5a2
fix: restore Cloud device target wiring
austinywang 9af2192
test: keep VPN setup visible beside discovered ports
austinywang dc5f6d9
fix: restore visible Cloud VPN guidance for Ports
austinywang c89213c
fix: pass VPN guidance state in initializer order
austinywang a164e98
fix: order Cloud tree guidance arguments
austinywang af21933
merge: integrate origin/main with Cloud VPN Ports guidance
austinywang 35f3cb7
merge: integrate origin/main with Cloud VPN Ports guidance
austinywang 9069c61
fix: wire Cloud port discovery state owner
austinywang 0a965c6
fix: import packaged Cloud port models
austinywang 699053d
fix: import surface catalog model for socket payloads
austinywang 2717e21
fix: import packaged Cloud types in app extensions
austinywang 2ff0e8e
fix: align Cloud port retry with shared link provider
austinywang 0db3b73
fix: import packaged tunnel state in VPN setup model
austinywang 0140292
fix: expose Cloud tree leading geometry to app outline
austinywang 5e11a93
fix: restore explicit Cloud port discovery demand
austinywang 5ca8b63
fix: import packaged tree placeholder in port builder
austinywang a27826a
fix: defer Cloud Ports scans and activate VPN help
austinywang 6c8b90c
merge: integrate origin/main and remove Ports VPN header help
austinywang aad1a85
merge: integrate origin/main into Cloud Ports VPN state
austinywang d387d23
fix: restore Cloud Ports project wiring and VPN setup close shortcut
austinywang eb9fee8
fix: compile Cloud Ports against current main
austinywang b2490f1
merge: integrate origin/main into Cloud Ports VPN state
austinywang ec92fb5
tests: import package modules in Cloud Ports tests
austinywang 988703a
tests: SSH Ports discover over loopback without Cloud VPN guidance
austinywang aad96b5
fix: route SSH Ports discovery over loopback and settle failed scans
austinywang 67c3261
fix: restore Cloud tree row heights lost in the main merge
austinywang a0a97d4
fix: install a retried machine's private route on the shared link
austinywang 78af234
fix: drop another machine's ports from an unavailable scan
austinywang d1124b3
tests: align Ports tests with closed-by-default groups and loopback b…
austinywang cc4198a
Merge main (f39a1e59c6f5) into 13234-cloud-ports-vpn-state
austinywang d7d2b15
tests: a Ports rescan keeps its settled inventory and failed links se…
austinywang c4163e8
fix: fence Ports scans by request so summary polls and cached scans c…
austinywang 36ce5bc
Merge main (5a698502379a) into 13234-cloud-ports-vpn-state
austinywang 20e02ee
tests: a rescan's Ports rows survive a cached refresh that retires it…
austinywang e319940
fix: publish a rescan's Ports rows on its request fence, not the refr…
austinywang 2a7222b
tests: a snapshot that outlives a rescan keeps the rescan's Ports rows
austinywang ffec561
fix: publish the Ports inventory accepted during a refresh's snapshot…
austinywang f678dbf
tests: expect Ports to start collapsed and unchecked in the Cloud tree
austinywang 3383c97
tests: give each Ports recovery fixture its own SSH machine
austinywang 0428218
fix: size the Ports group row like every other Cloud tree group
austinywang dc3e3cc
fix: keep the checklist popover's detach mark through its animated close
austinywang e7da910
Merge main (fb665a08d49a) into 13234-cloud-ports-vpn-state
austinywang 4382462
Merge main (cc9065904a55) into 13234-cloud-ports-vpn-state
austinywang aa44f87
tests: find re-presented popover windows by strong reference
austinywang 9b04bb6
test: Ports actions reach machines under the Cloud Machines section
austinywang 2620606
test: loopback aliases are not other-interface Ports listeners
austinywang 45be5c9
fix: Ports actions find sectioned machines; ignore loopback aliases
austinywang 05e3352
Merge commit '83270f163768380d45fe24e21b1ba5361ffbcee8' into 13234-cl…
austinywang 516caf5
fix: retranslate Cloud VPN setup guidance to match current English copy
austinywang 7db5ff0
test: routine link failure before demand keeps Ports not checked yet
austinywang 94977f2
Merge commit '4c5272e9153eca2033c9f40ac749f0c3a5bcb291' into 13234-cl…
austinywang d53b4cd
test: pin Cloud outline attention test to 100% magnification
austinywang c7af924
Localize Cloud Ports status copy for every catalog locale
austinywang 3cf7f98
test: a requested machine gaining a Ports route scans and stays re-re…
austinywang 79518b6
test: cancelled and SSH Ports refreshes settle without stale loading …
austinywang ad7ffe9
Settle a cancelled Ports request and keep SSH ids out of Cloud metada…
austinywang 80411d8
test: Ports settles after a cancel during display discovery and keeps…
austinywang 9161249
Settle a Ports request cancelled during display discovery without hid…
austinywang c648233
Merge main (3324f6311893) into 13234-cloud-ports-vpn-state
austinywang e88d277
Keep Localizable.xcstrings in main's key order
austinywang 44abc14
Merge main (e28555a2ecdf) into 13234-cloud-ports-vpn-state
austinywang c171271
test: turning Cloud VPN off rebuilds the cached tree with Ports guidance
austinywang cb48d3d
test: Ports and Settings open one Cloud VPN pane instead of a window
austinywang c9a04f1
Open Cloud VPN setup as a pane instead of a window
austinywang cb70bc9
test: Settings brings the main window forward to the Cloud VPN pane
austinywang 3ff7a0d
Bring the main window forward when Settings opens Cloud VPN setup
austinywang 831806c
Merge main (446581e109ef) into 13234-cloud-ports-vpn-state
austinywang 4bac62c
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 13234…
austinywang 875c067
Cover Settings reopening the Cloud VPN pane after it closes
austinywang aec5cdf
Merge main (f8079082fc46) into 13234-cloud-ports-vpn-state
austinywang 3fd1dcd
Merge main (528c5a870dc2) into 13234-cloud-ports-vpn-state
austinywang 26f380b
Merge commit 'a98c560063fc114d1dd11ad9d7fa8f15c3a8ed4b' into 13234-cl…
austinywang b7c156b
Merge main (0ebf8d7f4789) into 13234-cloud-ports-vpn-state
austinywang 95744f3
Merge main (356443354a63) into 13234-cloud-ports-vpn-state
austinywang d453aac
test: Ports status rows fit every wrapped line
austinywang 1289156
Size Ports status rows with the label cell that draws them
austinywang 8883562
Merge main (f7ee3bc64db5) into 13234-cloud-ports-vpn-state
austinywang 7713ad0
test: Ports never pair VPN setup guidance with another status row
austinywang f8cea99
Show Cloud VPN setup guidance only beside a finished scan's ports
austinywang f2be9e6
test: clarify Ports when Cloud VPN is not configured
austinywang 724f164
Make Cloud Ports clear without VPN setup
austinywang 39fe8a9
Merge main (6485102d2e2f) into 13234-cloud-ports-vpn-state
austinywang d89e69d
Update unnamed port tooltip coverage for the no-VPN flow
austinywang ed1a765
test: exclude container runtime listeners from Cloud app ports
austinywang 6e15922
Exclude container runtime management listeners from Cloud Ports
austinywang 60ff6d5
Read root-owned port metadata through existing guest permissions
austinywang 1d4fe79
Use the existing TcpListener import in the discovery regression
austinywang b7f085d
Explain the empty Ports state without VPN jargon
austinywang 260e689
Match Cloud VPN setup with the established onboarding page
austinywang e0e3119
Merge main into Cloud Ports VPN branch
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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?() } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
| 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") | ||
| ) | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.