Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
687d128
cloud welcome: introducing cmux cloud window, shown once, five layout…
lucasr1b Oct 4, 2026
88db662
cloud onboarding: glass welcome window, lowercase and machine focus l…
lucasr1b Oct 4, 2026
1c84092
cloud onboarding: one welcome design (machine focus, all lowercase), …
lucasr1b Oct 4, 2026
9b3ba43
cloud onboarding: review fixes (glass behind compiler guard, welcome …
lucasr1b Oct 4, 2026
9b20cd6
cloud welcome: ignore the titlebar safe area (fixes a layout-loop cra…
lucasr1b Oct 4, 2026
0bdbae4
cloud tab intro: title first, no icon
lucasr1b Oct 4, 2026
304b965
cloud tab intro: app icon banner back, lock badge while the plan need…
lucasr1b Oct 4, 2026
3b26270
cloud tab intro: dark app icon in the banner
lucasr1b Oct 4, 2026
2994449
fix(cloud): wait for display helper readiness (#17132)
austinywang Oct 4, 2026
3a7927b
fix(ci): per-runner build roots only on fleet Macs without glaeda (#1…
teamleaderleo Oct 4, 2026
11a3288
fix(ci): repair main's guards, localization parity and cmuxTests comp…
teamleaderleo Oct 4, 2026
805bbb4
ci: require a written merge override for red CI (#17217)
teamleaderleo Oct 4, 2026
ba1f902
Share one vCPU and memory pool across a plan's Cloud VMs (#17238)
lawrencecchen Oct 4, 2026
08f7923
Cloud VM: snapshot create honors Idempotency-Key (#17244)
lawrencecchen Oct 4, 2026
185a35b
Preserve selected tab when closing another surface (#16645)
teamleaderleo Oct 4, 2026
2a4243b
docs: detail tmux help options (#16780)
BlueRaddish Oct 4, 2026
21eb82f
test: guard managed contributor difficulty labels (#16448)
BlueRaddish Oct 4, 2026
161e1cc
fix(cloud): repair main's Cloud app-host failures from #17132 and #17…
teamleaderleo Oct 4, 2026
c707d8a
fix(ci): keep merge-gate diagnostics on the exact PR (#17248)
teamleaderleo Oct 4, 2026
d22f4f5
fix: keep the cmux-cua credential out of the Codex argv (#17252)
lawrencecchen Oct 4, 2026
ebeb9db
fix: keep codex wrapper overrides when the subcommand gets -c (#17257)
lawrencecchen Oct 4, 2026
3b15ee4
Add cmux browser repl: a Playwright-shaped browser REPL for agents (#…
lawrencecchen Oct 4, 2026
01748e3
Cloud: VM file operation routes (port of #16936) (#17254)
lawrencecchen Oct 4, 2026
33637ba
Stop calling the legacy Subrouter during account deletion (#17273)
lawrencecchen Oct 4, 2026
b996f93
ci: remove merge gate and restore exact-head merging (#17275)
teamleaderleo Oct 4, 2026
eadbe50
Cloud: private network routes (port of #16948) (#17255)
lawrencecchen Oct 4, 2026
46fe104
remote-tmux: stop a torn-down control stream from feeding the reconne…
ejc3 Oct 4, 2026
76a372f
remote-tmux: keep a window whose Dock has panels when its mirrors mov…
ejc3 Oct 4, 2026
93585c1
Expose the workspace task-status lane to custom sidebars (#17245)
pmochine Oct 4, 2026
f8db895
Clear restored agent notifications once the agent is gone (#17067)
wowpotato Oct 4, 2026
a6f1af5
Send sidebar links through the external-open rules (#7397)
ejc3 Oct 4, 2026
0cad11f
ci: require merge checks only when their workflows exist (#17284)
teamleaderleo Oct 4, 2026
0589e2c
ci: fall back to ancestor evidence for --main-fix (#17277)
teamleaderleo Oct 4, 2026
7d1a987
fix(session): sweep stale scrollback replay files (#16056)
soyeladice-svg Oct 4, 2026
0a0e3f8
Cover dotted Claude project dir in session directory search (#4939)
gigio1023 Oct 4, 2026
d48fd0d
Answer the tmux session commands Claude Code calls (#13632)
kblok Oct 4, 2026
8305cff
Fix custom-sidebar nil-comparison so optional-guarded views render (#…
xianjianlf2 Oct 4, 2026
186cec7
fix: refresh merge helper and honor neutral checks (#17294)
teamleaderleo Oct 4, 2026
692a633
Merge main (186cec797812) into pr-17230
austinywang Oct 5, 2026
9186e34
test: cover cloud welcome close shortcut ownership
austinywang Oct 5, 2026
a099d3c
fix: route cloud welcome close shortcut to its window
austinywang Oct 5, 2026
1aaf634
Merge updated cloud gate base into cloud onboarding
austinywang Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .github/workflows/ci-guards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ jobs:
if: ${{ matrix.group == 'ci' && steps.fast-guard.outputs.skip != 'true' }}
run: python3 tests/test_ci_guard_attribution.py

- name: Validate main-fix merge evidence
if: ${{ matrix.group == 'ci' && steps.fast-guard.outputs.skip != 'true' }}
run: python3 tests/test_gh_merge_green.py

- name: Validate main compile attribution
if: ${{ matrix.group == 'ci' && steps.fast-guard.outputs.skip != 'true' }}
run: python3 tests/test_main_compile_attribution.py
Expand Down
613 changes: 613 additions & 0 deletions CLI/CMUXCLI+BrowserRepl.swift

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions CLI/CMUXCLI+TaskHelp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ extension CMUXCLI {
browser url|get-url
browser snapshot [--interactive|-i] [--cursor] [--compact] [--max-depth <n>] [--selector <css>]
browser eval <script>
browser repl [--session <name>] [--workspace <id|ref>] [--eval <code>|-] [<code>] (see: browser repl guide)
browser wait [--selector <css>] [--text <text>] [--url-contains <text>] [--load-state <interactive|complete>] [--function <js>] [--timeout-ms <ms>]
browser click|dblclick|hover|focus|check|uncheck|scroll-into-view <selector> [--snapshot-after]
browser type <selector> <text> [--snapshot-after]
Expand Down
12 changes: 6 additions & 6 deletions CLI/CMUXCLI+VMTransfer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,7 @@ extension CMUXCLI {
--machine <id> Skip routing and use this machine.
--new Force a fresh pool machine.
--size <s> Memory preset for a machine this run creates
(4g to 24g on Pro; 32g and 64g need cmux Max).
(4g to 32g on Pro; 64g needs cmux Max).
--timeout <seconds> Command timeout (default \(vmRunDefaultTimeoutSeconds)s, max 15 minutes).
--wait, --output Accepted for symmetry with `vm agent`; `vm run` always
blocks on the command and prints its output.
Expand Down Expand Up @@ -1082,7 +1082,7 @@ extension CMUXCLI {
var memoryMb: Int?
if let sizeOption {
guard let parsed = Self.parseCloudVMSize(sizeOption) else {
throw CLIError(message: "vm run: unknown size '\(sizeOption)'. Sizes: 4g, 8g, 16g, 24g on Pro (32g and 64g need cmux Max), or memory in MB (at least 512).")
throw CLIError(message: "vm run: unknown size '\(sizeOption)'. Sizes: 4g, 8g, 16g, 24g, 32g on Pro (64g needs cmux Max), or memory in MB (at least 512).")
}
memoryMb = parsed
}
Expand Down Expand Up @@ -1689,7 +1689,7 @@ extension CMUXCLI {
--new Ignore the pool and report a fresh machine.
--provision Actually create the machine when routing would.
--size <s> Memory preset for a machine --provision creates
(4g to 24g on Pro; 32g and 64g need cmux Max).
(4g to 32g on Pro; 64g needs cmux Max).
--json {machine, created, reason, would_provision, directory}
"""
}
Expand Down Expand Up @@ -1738,7 +1738,7 @@ extension CMUXCLI {
(exit 1, the agent is not stopped). Default: no limit.
--new Force a fresh pool machine.
--size <s> Memory preset for a machine this call creates
(4g to 24g on Pro; 32g and 64g need cmux Max).
(4g to 32g on Pro; 64g needs cmux Max).

Examples:
cmux vm agent --agent claude --sync -- "run the test suite and fix failures"
Expand Down Expand Up @@ -1841,7 +1841,7 @@ extension CMUXCLI {
var memoryMb: Int?
if let sizeOption {
guard let parsed = Self.parseCloudVMSize(sizeOption) else {
throw CLIError(message: "vm route: unknown size '\(sizeOption)'. Sizes: 4g, 8g, 16g, 24g on Pro (32g and 64g need cmux Max), or memory in MB (at least 512).")
throw CLIError(message: "vm route: unknown size '\(sizeOption)'. Sizes: 4g, 8g, 16g, 24g, 32g on Pro (64g needs cmux Max), or memory in MB (at least 512).")
}
memoryMb = parsed
}
Expand Down Expand Up @@ -1946,7 +1946,7 @@ extension CMUXCLI {
var memoryMb: Int?
if let sizeOption {
guard let parsed = Self.parseCloudVMSize(sizeOption) else {
throw CLIError(message: "vm agent: unknown size '\(sizeOption)'. Sizes: 4g, 8g, 16g, 24g on Pro (32g and 64g need cmux Max), or memory in MB (at least 512).")
throw CLIError(message: "vm agent: unknown size '\(sizeOption)'. Sizes: 4g, 8g, 16g, 24g, 32g on Pro (64g needs cmux Max), or memory in MB (at least 512).")
}
memoryMb = parsed
}
Expand Down
72 changes: 61 additions & 11 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4226,7 +4226,9 @@ struct CMUXCLI {
// `vm_image_config_error`.
/// `--size` spellings → memory in MB. The supported base-image ladder is
/// 4 GB, 8 GB, 16 GB, 24 GB, 32 GB, and 64 GB of RAM, with disk sizes
/// following each image. Each machine has its own resources.
/// following each image. Disk is per machine; vCPUs and memory come from
/// shared vCPU and RAM pools, one per resource, that the plan's active
/// machines draw from (the server enforces both).
private static let cloudVMSizeAliases: [String: Int] = [
"4g": 4096, "4gb": 4096,
"8g": 8192, "8gb": 8192,
Expand Down Expand Up @@ -6022,7 +6024,7 @@ struct CMUXCLI {
vm new: unknown size '\(sizeOpt)'.

Sizes: 4g, 8g, 16g, 24g, 32g, 64g (or memory in MB).
Pro starts 4g to 24g; 32g and 64g need cmux Max. `cmux vm ls` shows your plan.
Pro goes up to 32g; 64g needs cmux Max. Active machines draw from shared vCPU and RAM pools. `cmux vm ls` shows your plan.
""")
}
memoryMb = parsed
Expand All @@ -6037,7 +6039,7 @@ struct CMUXCLI {
vm new: unknown flag '\(unknown)'.

Known flags:
--size <4g|8g|16g|24g|32g|64g> 4g to 24g on Pro; 32g and 64g need cmux Max
--size <4g|8g|16g|24g|32g|64g> 4g to 32g on Pro; 64g needs cmux Max
--network <full|allowlist|none> outbound access (see `cmux vm network --help`)
--network-policy <json> full network policy object
--agent-updates <latest|image> \(String(localized: "cli.vm.new.agentUpdatesFlag", defaultValue: "latest keeps coding agents up to date; image (default) keeps the baked versions"))
Expand Down Expand Up @@ -16654,7 +16656,7 @@ struct CMUXCLI {
var surfaceRaw = surfaceOpt
var args = argsWithoutSurfaceFlag

let verbsWithoutSurface: Set<String> = ["open", "open-split", "new", "identify", "import", "profile", "profiles", "react-grab", "reactgrab", "devtools", "dev-tools", "focus-mode", "design-mode", "zoom", "history"]
let verbsWithoutSurface: Set<String> = ["open", "open-split", "new", "identify", "import", "profile", "profiles", "react-grab", "reactgrab", "devtools", "dev-tools", "focus-mode", "design-mode", "zoom", "history", "repl"]
if surfaceRaw == nil, let first = args.first {
if !first.hasPrefix("-") && !verbsWithoutSurface.contains(first.lowercased()) {
surfaceRaw = first
Expand Down Expand Up @@ -18252,6 +18254,11 @@ struct CMUXCLI {
return
}

if subcommand == "repl" {
try runBrowserRepl(subArgs, client: client, jsonOutput: effectiveJSONOutput)
return
}

if subcommand == "viewport" {
let params = try Self.browserViewportSetParams(subArgs, surfaceID: requireSurface())
let payload = try client.sendV2(method: "browser.viewport.set", params: params)
Expand Down Expand Up @@ -21188,6 +21195,7 @@ struct CMUXCLI {
addinitscript|addscript [--script <js> | <js>]
addstyle [--css <css> | <css>]
\(Self.browserViewportHelp)
\(Self.browserReplHelp)
geolocation|geo <latitude> <longitude>
offline <true|false>
trace <start|stop> [path]
Expand Down Expand Up @@ -27310,8 +27318,30 @@ struct CMUXCLI {
valueFlags: ["-c", "-F", "-n", "-s"],
boolFlags: ["-A", "-d", "-P"]
)
if parsed.hasFlag("-A") {
throw CLIError(message: "new-session -A is not supported in cmux claude-teams mode")
// -A attaches to the named session when it already exists. tmux ignores the
// shell command on that path, and -d keeps the client where it is.
// A missing session falls through to create. A failed lookup does not:
// swallowing it here would invent a workspace the caller did not ask for.
if parsed.hasFlag("-A"), let name = parsed.value("-s") {
do {
let existingWorkspaceId = try tmuxResolveWorkspaceTarget(name, client: client)
if !parsed.hasFlag("-d") {
_ = try client.sendV2(method: "workspace.select", params: [
"workspace_id": existingWorkspaceId
])
}
if parsed.hasFlag("-P") {
let context = try tmuxFormatContext(workspaceId: existingWorkspaceId, client: client)
print(tmuxRenderFormat(
parsed.value("-F"),
context: context,
fallback: "@\(existingWorkspaceId)"
))
}
return
} catch let error as CLIError where Self.isAbsentTmuxSession(error) {
// Create the session below.
}
}
var params: [String: Any] = ["focus": false]
if let cwd = parsed.value("-c") {
Expand Down Expand Up @@ -27521,7 +27551,7 @@ struct CMUXCLI {
print(tmuxRenderFormat(parsed.value("-F"), context: context, fallback: fallback))
}

case "select-window", "selectw":
case "select-window", "selectw", "switch-client":
let parsed = try parseTmuxArguments(rawArgs, valueFlags: ["-t"], boolFlags: [])
let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
_ = try client.sendV2(method: "workspace.select", params: ["workspace_id": workspaceId])
Expand All @@ -27537,8 +27567,14 @@ struct CMUXCLI {
"pane_id": target.paneId
])

case "kill-window", "killw":
// tmux sessions resolve to cmux workspaces, so kill-session closes the same target.
// -a means kill every other session. An unrecognized flag used to be ignored,
// and with no -t the command closed the caller — the session -a must keep.
case "kill-window", "killw", "kill-session":
let parsed = try parseTmuxArguments(rawArgs, valueFlags: ["-t"], boolFlags: [])
if let unsupported = parsed.positional.first(where: { $0.hasPrefix("-") }) {
throw CLIError(message: "Unsupported tmux compatibility command: \(command) \(unsupported)")
}
let workspaceId = try tmuxResolveWorkspaceTarget(parsed.value("-t"), client: client)
_ = try client.sendV2(method: "workspace.close", params: ["workspace_id": workspaceId])
try? tmuxPruneCompatWorkspaceState(workspaceId: workspaceId)
Expand Down Expand Up @@ -27814,10 +27850,9 @@ struct CMUXCLI {
boolFlags: ["-g", "-q", "-s", "-v", "-w"]
)
let optionName = parsed.positional.last ?? ""
guard optionName == "extended-keys" else {
guard let value = Self.tmuxCompatShowOptionValues[optionName] else {
throw CLIError(message: "Unsupported tmux compatibility command: \(command) \(optionName)")
}
let value = "on"
if parsed.hasFlag("-v") {
print(value)
} else {
Expand Down Expand Up @@ -27912,6 +27947,21 @@ struct CMUXCLI {
}
}

/// A session name that resolved to no workspace. Socket and transport failures
/// stay errors so `new-session -A` does not create a workspace for them.
private static func isAbsentTmuxSession(_ error: CLIError) -> Bool {
guard !error.isStructuredProtocolResponse else { return false }
return error.message == "Workspace target not found"
|| error.message.hasPrefix("Workspace target not found:")
}

/// Options cmux answers for tmux-compat callers. cmux runs no tmux server, so each
/// value is what a default tmux client reports.
private static let tmuxCompatShowOptionValues = [
"extended-keys": "on",
"prefix": "C-b"
]

private func tmuxPruneCompatWorkspaceState(workspaceId: String) throws {
try withLockedTmuxCompatStoreIfChanged { store in
let removedLayout = store.mainVerticalLayouts.removeValue(forKey: workspaceId) != nil
Expand Down Expand Up @@ -42653,7 +42703,7 @@ export default {
print()
}

private func resolvedVersionInfo() -> [String: String] {
func resolvedVersionInfo() -> [String: String] {
var info: [String: String] = [:]
if let main = versionInfo(from: Bundle.main.infoDictionary) {
info.merge(main, uniquingKeysWith: { current, _ in current })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ struct BillingPlanCopy: Equatable {
case .pro:
[
L10n.string("mobile.billing.plan.pro.machines", defaultValue: "Up to 5 Cloud VMs"),
L10n.string("mobile.billing.plan.pro.size", defaultValue: "Up to 4 vCPUs and 8 GB RAM per VM"),
L10n.string("mobile.billing.plan.pro.size", defaultValue: "Sharing 20 vCPUs and 40 GB RAM"),
]
case .max:
[
L10n.string("mobile.billing.plan.max.machines", defaultValue: "Up to 5 Cloud VMs"),
L10n.string("mobile.billing.plan.max.size", defaultValue: "Up to 16 vCPUs and 32 GB RAM per VM"),
L10n.string("mobile.billing.plan.max.size", defaultValue: "Sharing 80 vCPUs and 160 GB RAM"),
]
default:
[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ public struct CloudAPIResponseDecoding: Sendable {
let status = rawStatus.flatMap { $0.isEmpty ? nil : $0 } ?? "unknown"
let displayName = (dict["displayName"] as? String).flatMap { $0.isEmpty ? nil : $0 }
let slug = (dict["slug"] as? String).flatMap { $0.isEmpty ? nil : $0 }
return CloudMachine(id: id, provider: provider, status: status, displayName: displayName, slug: slug)
return CloudMachine(
id: id,
provider: provider,
status: status,
displayName: displayName,
slug: slug,
resources: Self.resources(dict["resources"])
)
}
let availableKinds: Set<CloudMachineKind>?
let machineLimits: CloudMachineLimits?
Expand All @@ -52,7 +59,8 @@ public struct CloudAPIResponseDecoding: Sendable {
memoryOptionsMb: Self.intArray(limits["memoryOptionsMb"]),
lockedMemoryOptionsMb: Self.optionalIntArray(limits["lockedMemoryOptionsMb"]),
memoryUpgradePlanID: limits["memoryUpgradePlanId"] as? String,
memoryUpgradePlansByMb: limits["memoryUpgradePlansByMb"] as? [String: String]
memoryUpgradePlansByMb: limits["memoryUpgradePlansByMb"] as? [String: String],
resourcePool: Self.resourcePool(limits)
)
} else {
availableKinds = nil
Expand Down Expand Up @@ -171,6 +179,27 @@ public struct CloudAPIResponseDecoding: Sendable {
return nil
}

/// `vms[].resources: {vcpus, memoryMb}`; nil unless both are positive.
private static func resources(_ value: Any?) -> CloudMachineResources? {
guard let object = value as? [String: Any],
let vcpus = int(object["vcpus"]), vcpus > 0,
let memoryMb = int(object["memoryMb"]), memoryMb > 0 else { return nil }
return CloudMachineResources(vcpus: vcpus, memoryMb: memoryMb)
}

/// The `limits` pool fields; nil unless the plan has a positive pool.
/// Missing usage reads as nothing in use.
private static func resourcePool(_ limits: [String: Any]) -> CloudResourcePool? {
guard let poolVcpus = int(limits["poolVcpus"]), poolVcpus > 0,
let poolMemoryMb = int(limits["poolMemoryMb"]), poolMemoryMb > 0 else { return nil }
return CloudResourcePool(
poolVcpus: poolVcpus,
poolMemoryMb: poolMemoryMb,
usedVcpus: max(0, int(limits["usedVcpus"]) ?? 0),
usedMemoryMb: max(0, int(limits["usedMemoryMb"]) ?? 0)
)
}

private static func intArray(_ value: Any?) -> [Int] {
guard let values = value as? [Any] else { return [] }
return values.compactMap(int).filter { $0 > 0 }
Expand Down
Loading
Loading