Skip to content
11 changes: 1 addition & 10 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31792,16 +31792,7 @@ struct CMUXCLI {
}

static func codexForkMonitorArguments(environment: [String: String]) -> [String] {
guard let forkParent = environment[CodexForkSessionWatcher.parentSessionEnvironmentKey],
!forkParent.isEmpty else { return [] }
var arguments = ["--fork-parent", forkParent]
if let launchID = environment[CodexForkSessionWatcher.launchIDEnvironmentKey], !launchID.isEmpty {
arguments += ["--fork-launch-id", launchID]
}
if let ownerPID = environment["CMUX_CODEX_PID"], !ownerPID.isEmpty {
arguments += ["--fork-owner-pid", ownerPID]
}
return arguments
CodexForkMonitorArguments().make(environment: environment)
}

/// Watches the Codex rollout until the turn settles.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// Builds the arguments forwarded to a detached Codex fork monitor.
public struct CodexForkMonitorArguments: Sendable {
/// Creates a stateless argument builder.
public init() {}

/// Preserves the parent-session claim and optional launch ownership values.
public func make(environment: [String: String]) -> [String] {
guard let forkParent = environment["CMUX_AGENT_FORK_PARENT_SESSION_ID"],
!forkParent.isEmpty else { return [] }
var arguments = ["--fork-parent", forkParent]
if let launchID = environment["CMUX_AGENT_FORK_LAUNCH_ID"], !launchID.isEmpty {
arguments += ["--fork-launch-id", launchID]
}
if let ownerPID = environment["CMUX_CODEX_PID"], !ownerPID.isEmpty {
arguments += ["--fork-owner-pid", ownerPID]
}
return arguments
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import CMUXAgentLaunch
import Testing

@Suite("CodexForkMonitorArguments")
struct CodexForkMonitorArgumentsTests {
private let builder = CodexForkMonitorArguments()

@Test("forwards fork ownership values")
func forwardsOwnershipValues() {
#expect(builder.make(environment: [
"CMUX_AGENT_FORK_PARENT_SESSION_ID": "parent-session",
"CMUX_AGENT_FORK_LAUNCH_ID": "launch-id",
"CMUX_CODEX_PID": "1234",
]) == [
"--fork-parent", "parent-session",
"--fork-launch-id", "launch-id",
"--fork-owner-pid", "1234",
])
}

@Test("omits optional values when absent")
func omitsOptionalValuesWhenAbsent() {
#expect(builder.make(environment: [
"CMUX_AGENT_FORK_PARENT_SESSION_ID": "parent-session",
]) == ["--fork-parent", "parent-session"])
#expect(builder.make(environment: [:]).isEmpty)
}
}
2 changes: 1 addition & 1 deletion Sources/Cloud/CloudTreeNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ final class CloudTreeNode: NSObject {
case .portsGroup: return String(localized: "cloudTree.group.ports", defaultValue: "Ports")
case .resourcesPool: return String(localized: "cloudTree.group.resources", defaultValue: "Resources")
case .resource(_, let row): return row.title
case .port(let resource, let url, _):
case .port(let resource, _, _):
return CloudTreePortPresentation(resource: resource).title
case .placeholder(_, let placeholder): return placeholder.text
case .device(let row): return row.searchableTitle
Expand Down
10 changes: 7 additions & 3 deletions Sources/Cloud/CloudTreeRowContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,20 @@ struct CloudTreeRowContentView: View {
groupRow(title: String(localized: "cloudTree.group.resources", defaultValue: "Resources"))
case .resource(_, let row):
CloudTreeMachineResourceRowContent(row: row, style: style)
case .port(let resource, let url, _):
case .port(let resource, _, _):
let presentation = CloudTreePortPresentation(resource: resource)
CloudTreeLeafRow(
let row = CloudTreeLeafRow(
style: style,
icon: "network",
tint: CloudTreeIconPalette.browser,
title: presentation.title,
detail: presentation.detail
)
.help(presentation.toolTip ?? presentation.title)
if let toolTip = presentation.toolTip {
row.help(toolTip)
} else {
row
}
case .placeholder(_, let placeholder):
CloudTreePlaceholderContent(placeholder: placeholder, style: style)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Cloud/CloudTreeRowToolTip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ enum CloudTreeRowToolTip {
toolTip: joined([title, row.resource.url, CloudTreeBrowserDetail.text(for: row)], beyond: title),
accessibilityLabel: title
)
case .port(let resource, let url, _):
case .port(let resource, _, _):
let presentation = CloudTreePortPresentation(resource: resource)
return .init(
toolTip: presentation.toolTip,
Expand Down
6 changes: 6 additions & 0 deletions Sources/Surfaces/CmuxTuiRemoteRouting.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import Foundation
import CMUXAgentLaunch

/// Pure remote catalog selector and placement resolution shared by the app and CLI.
enum CmuxTuiRemoteRouting {
/// Keeps the app-host test and legacy app-facing alias on the shared package contract.
static func codexForkMonitorArguments(environment: [String: String]) -> [String] {
CodexForkMonitorArguments().make(environment: environment)
}

/// Every `cmux vm agent` option that takes a value, so the alias walk and
/// the help scan skip the value instead of reading it as the first provider
/// argument (or as `--help`).
Expand Down
8 changes: 5 additions & 3 deletions cmuxTests/CloudPortsVPNAffordanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ struct CloudPortsVPNAffordanceTests {
#expect(!children.contains { $0.id.hasSuffix("/ports/vpn-guidance") })
}

@Test("Loopback ports open in cmux without VPN onboarding or an explanatory paragraph")
@Test("Loopback ports show without VPN onboarding or an explanatory paragraph")
func loopbackPortsWithoutVPN() throws {
let machine = SurfaceMachineID.cloud("no-vpn-needed")
let scan = try #require(CloudPortScanResult(socketListing: "LISTEN 0 128 127.0.0.1:33015 0.0.0.0:*"))
Expand All @@ -117,8 +117,10 @@ struct CloudPortsVPNAffordanceTests {
#expect(row.searchableTitle == ":33015")
let cell = CloudTreeCellView(frame: NSRect(x: 0, y: 0, width: 220, height: 24))
cell.configure(node: row, machineActions: machineActions(), nodeActions: nodeActions())
#expect(cell.toolTip == "Open in cmux. No VPN setup needed.")
#expect(cell.accessibilityLabel()?.contains("Open in cmux") == true)
// Port rows carry no inline open action (#16350): a port with no
// process name has no hover text and is labelled by its number.
#expect(cell.toolTip == nil)
#expect(cell.accessibilityLabel() == "Port 33015")
}

/// Each status row explains the whole Ports group, so a second one contradicts it:
Expand Down
15 changes: 8 additions & 7 deletions cmuxTests/CloudTreeRowToolTipTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,15 @@ struct CloudTreeRowToolTipTests {
#expect(toolTip.contains(":1"))
}

@Test("A port row explains its in-app action without suggesting a directly reachable private URL")
@Test("A port row names its process on hover without suggesting a directly reachable private URL")
func portRowHasToolTip() throws {
let node = Self.portNode()
let cell = Self.cell(presence: [])
cell.configure(node: node, machineActions: Self.machineActions(), nodeActions: Self.nodeActions())
let toolTip = try #require(cell.toolTip)
#expect(toolTip == "Open in cmux. No VPN setup needed.")
#expect(cell.accessibilityLabel()?.contains("Port 3000") == true)
#expect(toolTip == "vite")
#expect(!toolTip.contains("://"))
#expect(cell.accessibilityLabel() == "Port 3000, vite")
}

@Test("An untitled browser row is still labelled for assistive technology")
Expand Down Expand Up @@ -287,16 +288,16 @@ struct CloudTreeRowToolTipTests {
#expect(cell.accessibilityLabel()?.isEmpty == false)
}

@Test("A port without a process name still explains that no VPN setup is needed")
func barePortRowExplainsOpenAction() {
@Test("A port without a process name has no hover text and is labelled by its number")
func barePortRowIsLabelledByNumber() {
let cell = Self.cell(presence: [])
cell.configure(
node: Self.barePortNode(),
machineActions: Self.machineActions(),
nodeActions: Self.nodeActions()
)
#expect(cell.toolTip == "Open in cmux. No VPN setup needed.")
#expect(cell.accessibilityLabel() == "Port 3000, Open in cmux")
#expect(cell.toolTip == nil)
#expect(cell.accessibilityLabel() == "Port 3000")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// MARK: - Fixtures
Expand Down
Loading