diff --git a/native2/Sources/DesktopMac/System.swift b/native2/Sources/DesktopMac/System.swift index 2788e94..1e40bb4 100644 --- a/native2/Sources/DesktopMac/System.swift +++ b/native2/Sources/DesktopMac/System.swift @@ -58,11 +58,13 @@ extension Mac { NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == "com.apple.ScreenSaver.Engine" } } - var layout: JSON? - if let source = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue(), - let id = tisString(source, kTISPropertyInputSourceID) - { - layout = .object(["id": JSON(id), "name": tisString(source, kTISPropertyLocalizedName).map(JSON.string)]) + // Text Input Sources must be read on the main thread: off it, HIToolbox can trap on a queue assertion and take + // the host down mid-call, which agents saw as "host closed the connection" after a desktop_open (#244). + let layout = await MainActor.run { () -> JSON? in + guard let source = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue(), + let id = tisString(source, kTISPropertyInputSourceID) + else { return nil } + return .object(["id": JSON(id), "name": tisString(source, kTISPropertyLocalizedName).map(JSON.string)]) } let pointer = CGEvent(source: nil).map(\.location).map { point -> JSON in .object(["x": JSON(Double(point.x)), "y": JSON(Double(point.y)), "display_index": WindowPlaces().pointerDisplay(point).map { JSON($0) }]) diff --git a/native2/Sources/desktop-tools-mcp/main.swift b/native2/Sources/desktop-tools-mcp/main.swift index 61954b7..dcd3597 100644 --- a/native2/Sources/desktop-tools-mcp/main.swift +++ b/native2/Sources/desktop-tools-mcp/main.swift @@ -203,7 +203,22 @@ func call(_ key: String, tool: String, arguments: JSON) async -> JSON { if case .system("connect", _) = error { return errorResult("The Desktop Tools 2 host isn't serving at \(socketPath). Start it with: native2/.build/debug/desktop-tools-host") } - return errorResult(error.description) + // The request may have reached the host, which can stop or restart mid-call. An action may then have taken + // effect, as a desktop_open whose host stopped after Xcode opened the file did (#244), so it is unknown, never + // a plain error an agent reads as "nothing happened". + if Contract.actions.contains(tool) { + let action: JSON = .object([("action", .object([ + ("outcome", "unknown"), ("action", JSON(tool)), ("retry_safety", "unsafe"), + ("error", .object([("code", "HOST_CONNECTION_LOST"), ("message", JSON( + "\(error.description) The Desktop Tools host stopped while running \(tool), so it may have taken effect. " + + "Observe the target (desktop_windows, desktop_inspect) before deciding whether to act again; don't repeat it blindly."))])), + ]))]) + return .object([ + ("content", [.object([("type", "text"), ("text", "Outcome: unknown.")]), .object([("type", "text"), ("text", JSON(action.text))])]), + ("isError", true), + ]) + } + return errorResult("\(error.description) Nothing was changed by this read; call it again.") } catch { return errorResult(String(describing: error)) }