diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index c3b56efe..082b9486 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5750,9 +5750,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent "expectedLatestWindowId": window1.windowId.uuidString, "expectedLatestTabId": tabId1.uuidString, ], at: path) - // Leave the initial window's terminal focused so UI tests can type shell - // commands while still keeping the second window configured for notifications. - window1.window?.makeKeyAndOrderFront(nil) self.publishMultiWindowNotificationSocketStateIfNeeded(at: path) } } @@ -5821,7 +5818,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent "socketPathExists": health.socketPathExists ? "1" : "0", "socketFailureSignals": failureSignals, ], at: dataPath) - guard !isTimedOut else { return } + guard !isTimedOut, !isReady else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { publish() } diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index b536d0e7..ca04b758 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -1247,6 +1247,9 @@ class TerminalController { case "send_key": return sendKey(args) + case "send_workspace": + return sendInputToWorkspace(args) + case "send_surface": return sendInputToSurface(args) @@ -9267,6 +9270,7 @@ class TerminalController { Input commands: send - Send text to current terminal send_key - Send special key (ctrl-c, ctrl-d, enter, tab, escape) + send_workspace - Send text to a workspace's focused terminal send_surface - Send text to a specific terminal send_key_surface - Send special key to a specific terminal read_screen [id|idx] [--scrollback] [--lines N] - Read terminal text (plain text) @@ -11594,6 +11598,49 @@ class TerminalController { return success ? "OK" : "ERROR: Failed to send input" } + private func sendInputToWorkspace(_ args: String) -> String { + guard let tabManager else { return "ERROR: TabManager not available" } + let parts = args.split(separator: " ", maxSplits: 1).map(String.init) + guard parts.count == 2 else { return "ERROR: Usage: send_workspace " } + + let workspaceArg = parts[0].trimmingCharacters(in: .whitespacesAndNewlines) + let text = parts[1] + guard let workspaceId = UUID(uuidString: workspaceArg) else { + return "ERROR: Invalid workspace ID" + } + + var success = false + var error: String? + DispatchQueue.main.sync { + guard let targetManager = AppDelegate.shared?.tabManagerFor(tabId: workspaceId) + ?? (tabManager.tabs.contains(where: { $0.id == workspaceId }) ? tabManager : nil) else { + error = "ERROR: Workspace not found" + return + } + guard let tab = targetManager.tabs.first(where: { $0.id == workspaceId }), + let terminalPanel = tab.focusedTerminalPanel else { + error = "ERROR: No focused terminal in workspace" + return + } + + let unescaped = text + .replacingOccurrences(of: "\\n", with: "\r") + .replacingOccurrences(of: "\\r", with: "\r") + .replacingOccurrences(of: "\\t", with: "\t") + + if let surface = terminalPanel.surface.surface { + sendSocketText(unescaped, surface: surface) + } else { + terminalPanel.sendText(unescaped) + terminalPanel.surface.requestBackgroundSurfaceStartIfNeeded() + } + success = true + } + + if let error { return error } + return success ? "OK" : "ERROR: Failed to send input" + } + private func sendInputToSurface(_ args: String) -> String { guard let tabManager = tabManager else { return "ERROR: TabManager not available" } let parts = args.split(separator: " ", maxSplits: 1).map(String.init) diff --git a/cmuxUITests/MultiWindowNotificationsUITests.swift b/cmuxUITests/MultiWindowNotificationsUITests.swift index 245d423e..5753173f 100644 --- a/cmuxUITests/MultiWindowNotificationsUITests.swift +++ b/cmuxUITests/MultiWindowNotificationsUITests.swift @@ -224,6 +224,10 @@ final class MultiWindowNotificationsUITests: XCTestCase { XCTFail("Missing setup workspace id") return } + guard let tabId1 = setup["tabId1"], !tabId1.isEmpty else { + XCTFail("Missing source workspace id") + return + } if let expectedSocketPath = setup["socketExpectedPath"], !expectedSocketPath.isEmpty { socketPath = expectedSocketPath } @@ -270,11 +274,6 @@ final class MultiWindowNotificationsUITests: XCTestCase { return } - XCTAssertTrue(app.windows.element(boundBy: 0).waitForExistence(timeout: 4.0), "Expected at least one window before typing notify command") - app.windows.element(boundBy: 0) - .coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) - .click() - let notifyCommand = [ "rm -f \(shellSingleQuote(commandStatusPath)) \(shellSingleQuote(commandStdoutPath)) \(shellSingleQuote(commandStderrPath));", "(sleep 1;", @@ -290,7 +289,10 @@ final class MultiWindowNotificationsUITests: XCTestCase { "2>\(shellSingleQuote(commandStderrPath));", "printf '%s' $? >\(shellSingleQuote(commandStatusPath))) >/dev/null 2>&1 &" ].joined(separator: " ") - app.typeText(notifyCommand + "\n") + guard socketCommand("send_workspace \(tabId1) \(notifyCommand)\\n") == "OK" else { + XCTFail("Failed to inject delayed bundled `cmux notify` command into source workspace \(tabId1)") + return + } let finder = XCUIApplication(bundleIdentifier: "com.apple.finder") finder.activate()