From 6382f1fbade3279377b3fe204d5fd8ca959f4af2 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 03:34:21 -0700 Subject: [PATCH 1/5] fix: avoid unsafe main actor assumption in terminal window observers --- Sources/GhosttyTerminalView.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index eca4c9e9b2a8..981b06e9ad13 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -5364,8 +5364,13 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { queue: .main ) { [weak self] notification in guard let occludedWindow = notification.object as? NSWindow else { return } - // Delivered on the main queue (`queue: .main`), which is the main actor. - MainActor.assumeIsolated { + // NotificationCenter's `queue: .main` selects the main operation + // queue, but it does not establish Swift concurrency's main-actor + // executor. AppKit can also post this notification during window + // teardown from a non-actor callback. Hop explicitly instead of + // assuming the executor, which otherwise traps with EXC_BAD_ACCESS + // while a terminal view is being detached. + Task { @MainActor [weak self] in self?.applyRendererWindowVisibility(for: occludedWindow) } } @@ -5379,7 +5384,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { queue: .main ) { [weak self] notification in guard let keyWindow = notification.object as? NSWindow else { return } - MainActor.assumeIsolated { + Task { @MainActor [weak self] in self?.applyRendererWindowVisibility(for: keyWindow) } }) From 11441ee0c32b19d2bae2d142e9efcf1167d547cc Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 04:53:45 -0700 Subject: [PATCH 2/5] fix: restore shared Codex fork monitor arguments --- Sources/Surfaces/CmuxTuiRemoteRouting.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/Surfaces/CmuxTuiRemoteRouting.swift b/Sources/Surfaces/CmuxTuiRemoteRouting.swift index 897870cc1361..cde8a67d688a 100644 --- a/Sources/Surfaces/CmuxTuiRemoteRouting.swift +++ b/Sources/Surfaces/CmuxTuiRemoteRouting.swift @@ -2,6 +2,24 @@ import Foundation /// Pure remote catalog selector and placement resolution shared by the app and CLI. enum CmuxTuiRemoteRouting { + /// Builds the detached Codex monitor flags for a forked session. + /// + /// The app test target aliases its CLI helper name to this shared routing + /// type, so keep this pure argument transformation available here as well + /// as in the executable-only CLI implementation. + static func codexForkMonitorArguments(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 + } + /// 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`). From 90ba3b06a0471ecb31a1eee6886becc5f3b9f975 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:16:27 -0700 Subject: [PATCH 3/5] Fix unused Cloud port bindings --- Sources/Cloud/CloudTreeNode.swift | 2 +- Sources/Cloud/CloudTreeRowContentView.swift | 2 +- Sources/Cloud/CloudTreeRowToolTip.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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..354a827e830e 100644 --- a/Sources/Cloud/CloudTreeRowContentView.swift +++ b/Sources/Cloud/CloudTreeRowContentView.swift @@ -112,7 +112,7 @@ 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( 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, From eda8fb95db63cab62a555efc01368d50181a5689 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:18:26 -0700 Subject: [PATCH 4/5] Serialize shared fork routing and guard window visibility --- CLI/cmux.swift | 15 +-------------- Sources/GhosttyTerminalView.swift | 1 + 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 1920bc5bb039..a9ee96235e0b 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -31762,7 +31762,7 @@ struct CMUXCLI { // the owner process for the child rollout and publish the child hook // binding. Without this, the first fork may render, but a fork of that // child has no durable parent association to discover. - monitorArgs += Self.codexForkMonitorArguments(environment: env) + monitorArgs += CmuxTuiRemoteRouting.codexForkMonitorArguments(environment: env) if let surfaceId, !surfaceId.isEmpty { monitorArgs += ["--surface", surfaceId] } @@ -31791,19 +31791,6 @@ 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 - } - /// Watches the Codex rollout until the turn settles. /// /// Returns the Stop replay for a healthy completion instead of running it, diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 981b06e9ad13..f54d24bafa13 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -9912,6 +9912,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { /// occlusion `.visible` bit is remembered per window so the rule can tell a /// trustworthy occlusion verdict from a virtual display that never sets it. private func applyRendererWindowVisibility(for window: NSWindow) { + guard let currentWindow = self.window, currentWindow === window else { return } let occlusionVisible = window.occlusionState.contains(.visible) if occlusionVisible { Self.windowsThatReportedVisible.add(window) From 65d677b1f917677aea705af11bd660ae18d9fd87 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:05:54 -0700 Subject: [PATCH 5/5] Align Cloud port tests with current presentation --- cmuxTests/CloudPortsVPNAffordanceTests.swift | 4 ++-- cmuxTests/CloudTreeRowToolTipTests.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmuxTests/CloudPortsVPNAffordanceTests.swift b/cmuxTests/CloudPortsVPNAffordanceTests.swift index 06deb7d509e8..f83b5f821dbd 100644 --- a/cmuxTests/CloudPortsVPNAffordanceTests.swift +++ b/cmuxTests/CloudPortsVPNAffordanceTests.swift @@ -117,8 +117,8 @@ 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) + #expect(cell.toolTip == nil) + #expect(cell.accessibilityLabel()?.contains("Open in cmux") == false) } /// 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..de9825aace19 100644 --- a/cmuxTests/CloudTreeRowToolTipTests.swift +++ b/cmuxTests/CloudTreeRowToolTipTests.swift @@ -135,7 +135,7 @@ struct CloudTreeRowToolTipTests { 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(toolTip == "vite") #expect(cell.accessibilityLabel()?.contains("Port 3000") == true) } @@ -295,8 +295,8 @@ struct CloudTreeRowToolTipTests { 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