diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 1920bc5bb039..9c8e3937d1b7 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -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. diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexForkMonitorArguments.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexForkMonitorArguments.swift new file mode 100644 index 000000000000..7642aefaf4bc --- /dev/null +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexForkMonitorArguments.swift @@ -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 + } +} diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/CodexForkMonitorArgumentsTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/CodexForkMonitorArgumentsTests.swift new file mode 100644 index 000000000000..76e3a2769057 --- /dev/null +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/CodexForkMonitorArgumentsTests.swift @@ -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) + } +} diff --git a/Sources/Cloud/CloudTreeNode.swift b/Sources/Cloud/CloudTreeNode.swift index 616baefab6ad..99c60725f0a0 100644 --- a/Sources/Cloud/CloudTreeNode.swift +++ b/Sources/Cloud/CloudTreeNode.swift @@ -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 diff --git a/Sources/Cloud/CloudTreeRowContentView.swift b/Sources/Cloud/CloudTreeRowContentView.swift index 46fc084c1bca..b96ac3654cbf 100644 --- a/Sources/Cloud/CloudTreeRowContentView.swift +++ b/Sources/Cloud/CloudTreeRowContentView.swift @@ -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) } diff --git a/Sources/Cloud/CloudTreeRowToolTip.swift b/Sources/Cloud/CloudTreeRowToolTip.swift index 1c9015174b7f..1f8e6ca13d02 100644 --- a/Sources/Cloud/CloudTreeRowToolTip.swift +++ b/Sources/Cloud/CloudTreeRowToolTip.swift @@ -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, diff --git a/Sources/Surfaces/CmuxTuiRemoteRouting.swift b/Sources/Surfaces/CmuxTuiRemoteRouting.swift index 897870cc1361..9bb26129439c 100644 --- a/Sources/Surfaces/CmuxTuiRemoteRouting.swift +++ b/Sources/Surfaces/CmuxTuiRemoteRouting.swift @@ -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`). diff --git a/cmuxTests/CloudPortsVPNAffordanceTests.swift b/cmuxTests/CloudPortsVPNAffordanceTests.swift index 06deb7d509e8..4d29ff7d2ec3 100644 --- a/cmuxTests/CloudPortsVPNAffordanceTests.swift +++ b/cmuxTests/CloudPortsVPNAffordanceTests.swift @@ -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:*")) @@ -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: diff --git a/cmuxTests/CloudTreeRowToolTipTests.swift b/cmuxTests/CloudTreeRowToolTipTests.swift index 28602bf45203..62bdab3c1d9d 100644 --- a/cmuxTests/CloudTreeRowToolTipTests.swift +++ b/cmuxTests/CloudTreeRowToolTipTests.swift @@ -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") @@ -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") } // MARK: - Fixtures