From 65346ca01bd60866f9df660ef289d40e09d58abf Mon Sep 17 00:00:00 2001 From: arzafran Date: Mon, 28 Sep 2026 12:38:07 -0300 Subject: [PATCH 1/2] revert: keep browser socket commands off the main thread The nonisolated socket chain ran every browser method on main, so browser.wait blocked the main thread it was waiting on and timed out. --- Sources/TerminalController.swift | 34 +++++++++++++------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 32a2d4804..d3c83aca0 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -157,7 +157,7 @@ class TerminalController { case failure(path: String, stage: String, errnoCode: Int32) } - private nonisolated static let focusIntentV2Methods: Set = [ + private static let focusIntentV2Methods: Set = [ "window.focus", "workspace.select", "review.open", @@ -578,7 +578,7 @@ class TerminalController { } } - private nonisolated static func socketCommandAllowsInAppFocusMutations(commandKey: String, isV2: Bool) -> Bool { + private static func socketCommandAllowsInAppFocusMutations(commandKey: String, isV2: Bool) -> Bool { // The v1 line protocol is gone; only v2 JSON-RPC methods can carry focus intent. guard isV2 else { return false } return focusIntentV2Methods.contains(commandKey) @@ -601,7 +601,7 @@ class TerminalController { return body() } - private nonisolated func withSocketCommandPolicy(commandKey: String, isV2: Bool, _ body: () -> T) -> T { + private func withSocketCommandPolicy(commandKey: String, isV2: Bool, _ body: () -> T) -> T { let allowsFocusMutation = Self.socketCommandAllowsInAppFocusMutations(commandKey: commandKey, isV2: isV2) Self.socketCommandPolicyLock.lock() Self.socketCommandPolicyDepth += 1 @@ -940,7 +940,7 @@ class TerminalController { } /// Check if `pid` is a descendant of this process by walking the process tree. - nonisolated func isDescendant(_ pid: pid_t) -> Bool { + func isDescendant(_ pid: pid_t) -> Bool { var current = pid // Walk up to 128 levels to avoid infinite loops from kernel bugs for _ in 0..<128 { @@ -960,7 +960,7 @@ class TerminalController { } /// Get the parent PID of a process using sysctl. - private nonisolated func parentPid(of pid: pid_t) -> pid_t { + private func parentPid(of pid: pid_t) -> pid_t { var info = kinfo_proc() var size = MemoryLayout.size var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid] @@ -1488,7 +1488,7 @@ class TerminalController { } } - private nonisolated func passwordAuthRequiredResponse(for command: String) -> String { + private func passwordAuthRequiredResponse(for command: String) -> String { let message = "Authentication required. Send auth first." guard command.hasPrefix("{"), let data = command.data(using: .utf8), @@ -1499,7 +1499,7 @@ class TerminalController { return v2Error(id: id, code: "auth_required", message: message) } - private nonisolated func passwordLoginV2ResponseIfNeeded(for command: String, authenticated: inout Bool) -> String? { + private func passwordLoginV2ResponseIfNeeded(for command: String, authenticated: inout Bool) -> String? { guard command.hasPrefix("{"), let data = command.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { @@ -1535,7 +1535,7 @@ class TerminalController { return v2Ok(id: id, result: ["authenticated": true]) } - private nonisolated func authResponseIfNeeded( + private func authResponseIfNeeded( for command: String, authenticated: inout Bool, requestPolicy: SocketRequestPolicy @@ -1759,7 +1759,7 @@ class TerminalController { /// Unix clients carry the listener policy captured when they were accepted. /// Mobile Bridge sessions are admitted independently by pairing and its method allow-list. - nonisolated func handleClient(_ socket: Int32, peerPid: pid_t? = nil, source: SocketConnectionSource) { + func handleClient(_ socket: Int32, peerPid: pid_t? = nil, source: SocketConnectionSource) { // Owns this connection's writes (both ordinary v2 responses and any #167 subscription // event pushes) and its subscription lifecycle. `teardown()` (which tears down any // attached subscription) must run before the fd is closed -- defers unwind LIFO, so the @@ -1888,7 +1888,7 @@ class TerminalController { } } - private nonisolated func processCommand( + private func processCommand( _ command: String, connection: SocketConnection, requestPolicy: SocketRequestPolicy @@ -1912,7 +1912,7 @@ class TerminalController { // MARK: - V2 JSON Socket Protocol - private nonisolated func processV2Command( + private func processV2Command( _ jsonLine: String, connection: SocketConnection, requestPolicy: SocketRequestPolicy @@ -1952,7 +1952,7 @@ class TerminalController { } return withSocketCommandPolicy(commandKey: method, isV2: true) { - if let result = AgentRPCDispatcher.dispatch(method: method, params: params, controller: self) { + if let result = browserRPCDispatcher.dispatch(method: method, params: params, controller: self) ?? AgentRPCDispatcher.dispatch(method: method, params: params, controller: self) { return v2Result(id: id, result) } switch method { @@ -2194,8 +2194,7 @@ class TerminalController { // Markdown case "markdown.open": - // `v2MarkdownOpen` is main actor-isolated; hop explicitly. - return v2Result(id: id, v2MainSync { self.v2MarkdownOpen(params: params) }) + return v2Result(id: id, self.v2MarkdownOpen(params: params)) // Review (agent diff review panel) case "review.open": @@ -2297,17 +2296,12 @@ class TerminalController { #endif default: - // BrowserRPCDispatcher is @MainActor; only methods that missed every - // nonisolated case above pay this hop, keeping telemetry off main. - if let result = v2MainSync({ browserRPCDispatcher.dispatch(method: method, params: params, controller: self) }) { - return v2Result(id: id, result) - } return v2Error(id: id, code: "method_not_found", message: "Unknown method") } } } - private nonisolated func v2Capabilities(requestPolicy: SocketRequestPolicy) -> [String: Any] { + private func v2Capabilities(requestPolicy: SocketRequestPolicy) -> [String: Any] { return [ "protocol": "cmux-socket", "version": 2, From 60f116d50c10d49faf5f32c45c329366cb21134f Mon Sep 17 00:00:00 2001 From: arzafran Date: Mon, 28 Sep 2026 12:40:26 -0300 Subject: [PATCH 2/2] test: build the collapsed-folder selection state directly in the restore test Selecting a folder child in the live app expands the folder, so the test never reached the collapsed state it asserts on. --- programaTests/TabManagerSessionSnapshotTests.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/programaTests/TabManagerSessionSnapshotTests.swift b/programaTests/TabManagerSessionSnapshotTests.swift index 3615994a5..b663bdc8c 100644 --- a/programaTests/TabManagerSessionSnapshotTests.swift +++ b/programaTests/TabManagerSessionSnapshotTests.swift @@ -189,11 +189,13 @@ final class TabManagerSessionSnapshotTests: XCTestCase { ) grandchild.setCustomTitle("Grandchild") - // Select a folder child so the collapsed-parent selection remap has something to prove. - manager.selectWorkspace(folderChildA) - let encoded = try JSONEncoder().encode(manager.sessionSnapshot(includeScrollback: false)) - let snapshot = try JSONDecoder().decode(SessionTabManagerSnapshot.self, from: encoded) + var snapshot = try JSONDecoder().decode(SessionTabManagerSnapshot.self, from: encoded) + // Selecting a child in the live app expands its folder, so a saved session with a + // collapsed folder and a selected child is built directly to exercise the restore remap. + snapshot.selectedWorkspaceIndex = try XCTUnwrap( + snapshot.workspaces.firstIndex { $0.customTitle == "FolderChildA" } + ) let restored = TabManager() restored.restoreSessionSnapshot(snapshot)