Repository navigation
Cloud: open detected ports through private live previews - #30334
Conversation
Connect configured Cloud preview ingress to the existing machine-port actions and browser pane. Preserve the existing proxy route for builds without platform preview ingress. Validation: scoped CloudPorts catalog check passes within existing base debt; XCStrings structure, default parity, test wiring and feature policy pass. Native compile and behavioral tests require the fleet; this lane has no Swift compiler. Agent-Session: https://cmux.com/deeplink/agent?id=aid-a71eddb5268a31d81df5
Move existing Cloud port labels and their catalog out of the app module. Keep preview wiring in existing action and service adapters so the feature does not grow the app module. Agent-Session: https://cmux.com/deeplink/agent?id=aid-a71eddb5268a31d81df5
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Not verified: macOS jobs did not run: compile and app tests were skipped on CI passes on Written by Default changesThis pull request changes 36 defaults at
Signed off on this commit by Merge receipt: merged as Regenerate the snapshot with Shipped in:
|
There was a problem hiding this comment.
6 issues found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="release-notes/next/highlights/cloud-live-previews.md">
<violation number="1" location="release-notes/next/highlights/cloud-live-previews.md:4">
P2: This note does not tell users where to find the preview chooser, so the advertised feature is difficult to discover from What's New. Say that `Cloud Machine Ports` is available in the Cloud machine's action palette, and name the CLI command if it is part of the entry.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudMachinePreviews.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudMachinePreviews.swift:53">
P2: Encode `id` as one path segment before constructing this request. A machine ID containing `/`, `?`, `#`, or percent-encoded separators can otherwise address a different route or inject query/fragment data.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift:53">
P2: `machine-ports` only returns `previews`, `listening`, and `unavailable` when preview ingress is available. Qualify this sentence so agents do not expect those fields from builds using the existing fallback.</violation>
<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift:54">
P2: This sentence incorrectly says `resume-machine` connects the classic-link machine. The ports handler still requires an established daemon connection, while resume may only start the VM and return before attachment; document that the machine must be connected and that paused machines should be resumed first, then wait for the link.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudPortActionCatalog.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudPortActionCatalog.swift:13">
P2: `cloudOpenBrowser` does not always use a private preview URL, and its fallback can still return `pending: true`. Describe the preview as conditional on ingress availability and document the fallback result.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/CloudHandlers+Browser.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/CloudHandlers+Browser.swift:341">
P2: `openPreview` bypasses the Chromium availability guard used by `openBrowser`. When a user selects a still-valid preview while CEF is unavailable, this attempts a pinned CEF tab instead of returning the supported Chromium-unavailable refusal; add the same guard here.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| title: Open Cloud web previews in the browser | ||
| category: improved | ||
|
|
||
| Cloud Machine Ports discovers your running servers and lets you open a private preview in the app's browser. The same preview links are available from the CLI. |
There was a problem hiding this comment.
P2: This note does not tell users where to find the preview chooser, so the advertised feature is difficult to discover from What's New. Say that Cloud Machine Ports is available in the Cloud machine's action palette, and name the CLI command if it is part of the entry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At release-notes/next/highlights/cloud-live-previews.md, line 4:
<comment>This note does not tell users where to find the preview chooser, so the advertised feature is difficult to discover from What's New. Say that `Cloud Machine Ports` is available in the Cloud machine's action palette, and name the CLI command if it is part of the entry.</comment>
<file context>
@@ -0,0 +1,4 @@
+title: Open Cloud web previews in the browser
+category: improved
+
+Cloud Machine Ports discovers your running servers and lets you open a private preview in the app's browser. The same preview links are available from the CLI.
</file context>
| Cloud Machine Ports discovers your running servers and lets you open a private preview in the app's browser. The same preview links are available from the CLI. | |
| From a Cloud machine's action palette, choose Cloud Machine Ports to find running servers and open a private preview in the app's browser. Run `cmux cloud open-browser --target machine:<id> --port <port>` from the CLI. |
| guard !sendsStackSession else { | ||
| throw CloudAPIError.http(status: 501, code: "machine.previews_unsupported", message: nil) | ||
| } | ||
| return try await send("POST", "\(machinePath)/\(id)/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self) |
There was a problem hiding this comment.
P2: Encode id as one path segment before constructing this request. A machine ID containing /, ?, #, or percent-encoded separators can otherwise address a different route or inject query/fragment data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudMachinePreviews.swift, line 53:
<comment>Encode `id` as one path segment before constructing this request. A machine ID containing `/`, `?`, `#`, or percent-encoded separators can otherwise address a different route or inject query/fragment data.</comment>
<file context>
@@ -0,0 +1,55 @@
+ guard !sendsStackSession else {
+ throw CloudAPIError.http(status: 501, code: "machine.previews_unsupported", message: nil)
+ }
+ return try await send("POST", "\(machinePath)/\(id)/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self)
+ }
+}
</file context>
| return try await send("POST", "\(machinePath)/\(id)/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self) | |
| return try await send("POST", "\(machinePath)/\(pathSegment(id))/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self) |
| ports; its menu opens a detected port in the app browser. On the classic link, connect first \ | ||
| with `cmux cloud resume-machine --target machine:<id>`. |
There was a problem hiding this comment.
P2: This sentence incorrectly says resume-machine connects the classic-link machine. The ports handler still requires an established daemon connection, while resume may only start the VM and return before attachment; document that the machine must be connected and that paused machines should be resumed first, then wait for the link.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift, line 54:
<comment>This sentence incorrectly says `resume-machine` connects the classic-link machine. The ports handler still requires an established daemon connection, while resume may only start the VM and return before attachment; document that the machine must be connected and that paused machines should be resumed first, then wait for the link.</comment>
<file context>
@@ -50,8 +50,9 @@ nonisolated enum CloudActionDescriptions {
- ports (comma-separated). Needs the machine connected: resume a paused one first with \
- `cmux cloud resume-machine --target machine:<id>`.
+ ports (comma-separated). New Cloud also returns private previews, listening and unavailable \
+ ports; its menu opens a detected port in the app browser. On the classic link, connect first \
+ with `cmux cloud resume-machine --target machine:<id>`.
""",
</file context>
| ports; its menu opens a detected port in the app browser. On the classic link, connect first \ | |
| with `cmux cloud resume-machine --target machine:<id>`. | |
| ports; its menu opens a detected port in the app browser. On the classic link, the machine must be connected; if paused, run \ | |
| `cmux cloud resume-machine --target machine:<id>` and wait for it to connect. |
| id: "cloudOpenBrowser", | ||
| title: String(localized: "action.cloudOpenBrowser", defaultValue: "Open Machine Port in Browser…", table: "CloudPortActions", bundle: .module), | ||
| description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id; on the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's. A tab not shown within 15 s answers pending: true.", | ||
| description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.", |
There was a problem hiding this comment.
P2: cloudOpenBrowser does not always use a private preview URL, and its fallback can still return pending: true. Describe the preview as conditional on ingress availability and document the fallback result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudPortActionCatalog.swift, line 13:
<comment>`cloudOpenBrowser` does not always use a private preview URL, and its fallback can still return `pending: true`. Describe the preview as conditional on ingress availability and document the fallback result.</comment>
<file context>
@@ -10,7 +10,7 @@ nonisolated enum CloudPortActionCatalog: ActionCatalogGroup {
id: "cloudOpenBrowser",
title: String(localized: "action.cloudOpenBrowser", defaultValue: "Open Machine Port in Browser…", table: "CloudPortActions", bundle: .module),
- description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id; on the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's. A tab not shown within 15 s answers pending: true.",
+ description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.",
keywords: ["vm", "cloud", "browser", "port", "localhost", "preview", "web"], category: .cloud, symbol: "safari",
surfaces: [.palette, .contextMenu], arguments: [CatalogArgument.portInt, CatalogArgument.optionalPathString],
</file context>
| description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.", | |
| description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL when preview ingress is available; otherwise its app-link browser route may return pending: true if the tab is not shown. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.", |
| /// A platform preview is an HTTPS website, independent of the Mac's loopback proxy routes. | ||
| static func openPreview(_ preview: CloudMachinePreview, session: CloudMachineSession, path: String = "/", viewChange: Bool, | ||
| _ context: AppActionContext) async throws -> [String: ActionResultValue] { | ||
| let names = CloudPortNames(session, port: preview.port) |
There was a problem hiding this comment.
P2: openPreview bypasses the Chromium availability guard used by openBrowser. When a user selects a still-valid preview while CEF is unavailable, this attempts a pinned CEF tab instead of returning the supported Chromium-unavailable refusal; add the same guard here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/CloudHandlers+Browser.swift, line 341:
<comment>`openPreview` bypasses the Chromium availability guard used by `openBrowser`. When a user selects a still-valid preview while CEF is unavailable, this attempts a pinned CEF tab instead of returning the supported Chromium-unavailable refusal; add the same guard here.</comment>
<file context>
@@ -264,3 +273,86 @@ extension CloudHandlers {
+ /// A platform preview is an HTTPS website, independent of the Mac's loopback proxy routes.
+ static func openPreview(_ preview: CloudMachinePreview, session: CloudMachineSession, path: String = "/", viewChange: Bool,
+ _ context: AppActionContext) async throws -> [String: ActionResultValue] {
+ let names = CloudPortNames(session, port: preview.port)
+ guard let pane = context.services.windows.active?.focusedPane else { throw ActionFailure(message: CloudPortStrings.noWindow(names)) }
+ let url = try await wrapped(names) { try preview.browserURL(path: path) }
</file context>
| let names = CloudPortNames(session, port: preview.port) | |
| let names = CloudPortNames(session, port: preview.port) | |
| if let reason = context.services.cache.browserTabs.cefUnavailable() { | |
| throw ActionFailure(message: CloudPortStrings.browserFailed(names, detail: BrowserTabService.message(reason))) | |
| } |
| ports (comma-separated). New Cloud also returns private previews, listening and unavailable \ | ||
| ports; its menu opens a detected port in the app browser. On the classic link, connect first \ |
There was a problem hiding this comment.
P2: machine-ports only returns previews, listening, and unavailable when preview ingress is available. Qualify this sentence so agents do not expect those fields from builds using the existing fallback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift, line 53:
<comment>`machine-ports` only returns `previews`, `listening`, and `unavailable` when preview ingress is available. Qualify this sentence so agents do not expect those fields from builds using the existing fallback.</comment>
<file context>
@@ -50,8 +50,9 @@ nonisolated enum CloudActionDescriptions {
Lists the TCP ports listening on the machine (--target machine:<id>) and returns machine_id and \
- ports (comma-separated). Needs the machine connected: resume a paused one first with \
- `cmux cloud resume-machine --target machine:<id>`.
+ ports (comma-separated). New Cloud also returns private previews, listening and unavailable \
+ ports; its menu opens a detected port in the app browser. On the classic link, connect first \
+ with `cmux cloud resume-machine --target machine:<id>`.
</file context>
| ports (comma-separated). New Cloud also returns private previews, listening and unavailable \ | |
| ports; its menu opens a detected port in the app browser. On the classic link, connect first \ | |
| ports (comma-separated). When preview ingress is available, New Cloud also returns private previews, listening and unavailable \ | |
| ports; its menu opens a detected port in the app browser. On the classic link, connect first \ |
|
Review (subagent claude-opus-5-5 medium, for claude-sr2-coord): SHIP at e84d0fa Read the full diff plus
No blocking findings. Follow-ups:
|
|
Review (claude-opus-5-5 medium, cloud-reviewer): SHIP at 924af0f Delta review over the prior SHIP at e84d0fa. The delta is the single commit 924af0f ("Fix preview browser target pane type"). Its parent is e84d0fa, and it changes one line in
No blocking findings. Follow-ups: none new. The prior review's follow-ups 1-6 still stand. |
|
Found 14 test failures on Blacksmith runners: Failures
...and 4 more test failures. View all on Blacksmith
|
… (highlights, capabilities, step coverage, arm64 recorder) (#30624) * base red: tooling-tests, register test_ci_blacksmith_linux_sizes.py as manual and the unregistered tests tooling-tests failed on every feat-cmux-next pull request at "validate_test_execution_registry.py --pr-lanes feat-cmux-next": tests/test_ci_blacksmith_linux_sizes.py: linux-guard lane is not run by any workflow #30360 (d57f2fc) registered the test as linux-guard but added no ci-guards.yml step that runs it. CLAUDE.md forbids new CI tests (Lawrence 2026-10-23), so it becomes a manual entry with how to run it. The registry also warned about six tests with no entry. test_nightly_dedicated_mini.py is already run by ci-guards.yml, so it takes linux-guard; the other five are run by no workflow and enter as manual, each with its own run command. Red at 03423de: python3 scripts/ci/validate_test_execution_registry.py --pr-lanes feat-cmux-next --base-sha HEAD^1 exit 1 Green here: same command, exit 0 (594 tests, manual=31). * base red: Checks job structure, the preflight tests read the preflight step by id tests/test_cmux_next_checks_workflow.py read the preflight script as the last step of cmux-tui-artifacts.yml's tree-preflight job. #30221 (5bdd4c9) added "Record tree owner observation" and an upload step after it, so 14 tests failed with KeyError: 'run' on every pull request. They now take the step by its id (push-head-preflight's `head`, tree-preflight's `inspect`); the assertions do not change. Red at 03423de: python3 tests/test_cmux_next_checks_workflow.py FAILED (errors=14) Green here: same command, 165 tests OK. * base red: Tier routing, an agent pane on a dev server URL loads its own document test_no_suite_runs_page_javascript_on_a_built_bundle flagged AgentPageHibernationTests.swift after #30312 (b16b20c) made it call waitUntilLoaded. That suite's agent pane loads AgentPaneSource.devURLVariable, a loopback port nobody serves (Debug test builds honor it), not the built bundle, so it reads no bundle content. The check now counts AgentPaneSource.devURLVariable as a suite's own document, beside loadHTMLString and the fixture roots. Red at 03423de: python3 tests/test_cmux_next_route.py FAILED (failures=1) Green here: same command, 112 tests OK. * base red: daemon capabilities export lists agent-session-start-no-cwd-v1 #30363 (efd30f5) added the optional capability agent-session-start-no-cwd-v1 to DaemonEndpoint.swift without the export, which reddened "Daemon capabilities export is fresh" and the daemon-capabilities-fresh.test.sh script test. Regenerated with the check's own renderer (--write), as base-autofix does. Red at 03423de: python3 scripts/cmux-next/check-daemon-capabilities-fresh.py exit 1 Green here: same command, exit 0; bash scripts/cmux-next/tests/daemon-capabilities-fresh.test.sh ok. * base red: String tables, store.updates.available has English's plural forms in every language #30288 (e6bfd26) made store.updates.available a plural with `one` and `other`, but ja, km, ko, th, vi, zh-Hans and zh-Hant got only `other`, and check-l10n.sh requires English's forms in every language. Their `one` form is their `other` text, as home.stopAll.couldNotStop does. Also adds the 12 languages #27883 (16b7cc2) left out of action.sidebar.compactList.toggle (style debt CI only warns about), marked needs_review. Red at 03423de: scripts/cmux-next/check-l10n.sh . 7 errors (19 without L10N_STYLE_WARN_MAX) Green here: same command, 0 errors. * base red: release-notary, three highlights link their pull requests scripts/whats-new/test_digest.py needs a try-it action, docs or a pr link on every highlight. chat-title-plain (#29920), cloud-live-previews (#30334) and working-harness-tab-icons (#27905) had none; each now links its pull request, as #29939 did for priority-view-drag. Red at 03423de: python3 scripts/whats-new/test_digest.py FAILED (failures=1) Green here: same command, OK. * base red: release-notary, the static capability check skips agent-session-start-no-cwd-v1 #30363 (efd30f5) added agent-session-start-no-cwd-v1, which cmux-tui's identify_capabilities pushes at runtime (capabilities.rs), so the bundled static manifest never lists it. tests/test_cmux_next_static_capability_runtime_only.py (release-notary, "Validate nightly push throttle") failed, and every nightly's check-daemon-capabilities.sh --static-only would refuse the build. It joins runtime_only, as #30447 did for agent-session-settings-v1. Red at 03423de: python3 tests/test_cmux_next_static_capability_runtime_only.py FAILED (failures=1) Green here: same command, OK. * l10n: compact workspace list uses this table's word for workspace (it, pl, pt-BR, uk, th) * base red: release-notary, nightly step coverage follows #30200's visual verdict steps #30200 (86fd376) removed find-nightly-next-recovery's "Read the visual gate's verdicts" and renamed visual-gate-nightly-verdict's "Fail with the gate when it holds the build" to "Record failed advisory verdict" (same script, visual-gate-fail-when-held.sh). .github/nightly-step-coverage.json kept the old names, so tests/test_nightly_step_coverage.py failed. The stale entry goes and the renamed step keeps its plumbing entry. Red at 03423de: python3 tests/test_nightly_step_coverage.py FAILED (failures=1) Green here: same command, OK. * base red: release-notary, the nightly arm64-only recorder has the cache classifier the Release step sources #30107 (2379dd9) made nightly.yml's "Build nightly app (Release)" source scripts/cmux-next/lib/compilation-cache.sh. tests/test_nightly_next_arm64_only.py runs that step in a scratch tree with recorder executables, and the tree had no such file, so the step exited 1 before xcodebuild's recorder and the test failed with CalledProcessError. The scratch tree now gets the real file, as it does scripts/lib/rust-targets.sh. Every assertion is the same. Red at 03423de: GITHUB_STEP_SUMMARY=/tmp/s python3 tests/test_nightly_next_arm64_only.py CalledProcessError Green here: same command, PASS: nightly-next arm64 only. * base red: release-notary, nightly-next identity reads a YAML-anchored job env as its value #30278 (fe4577e) turned the publish jobs' NIGHTLY_FEEDS into a YAML anchor (`&pub-feeds ${{ needs.decide.outputs.feeds }}`) and aliases (`*pub-feeds`). tests/test_nightly_next_identity.py reads job env as raw text, so test_each_app_reads_the_feed_its_publisher_writes failed on the anchor prefix. job_env now resolves an alias to its one anchor and drops an anchor name, as YAML does; the asserted values are the same. Red at 03423de: python3 tests/test_nightly_next_identity.py 7/8 passed Green here: same command, 8/8 passed.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Cloud Machine Ports discovers listeners on configured NEXT Cloud machines and offers an Open in Browser chooser.
cmux cloud machine-portsreturns the same private preview links, andcloud open-browser --port N --path /docsopens the authenticated preview in the app browser while preserving its sign-in bootstrap.Leo: “yeah we have bits and pieces of this with the computer use preview but i think we can do better” (HQ #5846, item 9).
Preview discovery and ingress belong to cmux-platform. The client calls its new
POST /v1/cloud/machines/{id}/previews/refreshdirectly with the existing person access token and team. Debug builds using the staging Cloud owner select platform staging; an explicit debug HTTPS origin can configure another development preview API. Builds without preview ingress keep their existing browser and port behavior. Classic routes and the computer-use card keep their existing behavior.The existing port strings and catalog move to CmuxNextCloudUI, keeping the app module smaller. Design: leo-workspace
design/cloud.md. Prior art read: #25284 (port/browser actions), #28279 (pending port actions).pr-overlapreported no overlapping open PR.Observed / Not run
924af0f3f43c718748aa7f1a4cfef9a10d2392bb(job47d9a55263d66246d01b72b9, 65 seconds). Fleet native test binary compiled successfully;CloudLivePreviewTests(7 tests) andCloudBrowserActionTests(13 tests) both passed (jobc779c9df80c95f10964ae260, exit 0). The first fleet compile caught a pane-controller/model mismatch; this head fixes it.88080e84e81a83f29ef29495c5a716bd6b588bdd; this PR changes neither package membership nor those catalogs.Risk: the configured staging path now uses private HTTPS ingress; production keeps its existing path. No new timers or observers.
Agent session: agent session