diff --git a/apps/agent-runner/bridges/macos/Sources/AgentMarkBridgeMacos/Dispatcher.swift b/apps/agent-runner/bridges/macos/Sources/AgentMarkBridgeMacos/Dispatcher.swift
index 72a0b6c..fe7a6e5 100644
--- a/apps/agent-runner/bridges/macos/Sources/AgentMarkBridgeMacos/Dispatcher.swift
+++ b/apps/agent-runner/bridges/macos/Sources/AgentMarkBridgeMacos/Dispatcher.swift
@@ -29,10 +29,12 @@ final class Dispatcher {
return try handleCapture(params: request.params)
case "execute":
return try handleExecute(params: request.params)
+ case "execute_batch":
+ return try handleExecuteBatch(params: request.params)
default:
throw RpcError(
code: .methodNotFound,
- message: "Unknown method: \(request.method). Supported: ping, capabilities, list_windows, capture, execute.",
+ message: "Unknown method: \(request.method). Supported: ping, capabilities, list_windows, capture, execute, execute_batch.",
requestId: request.id
)
}
@@ -55,7 +57,7 @@ final class Dispatcher {
return [
"bridge": "agentmark-bridge-macos",
"version": bridgeVersion,
- "methods": ["ping", "capabilities", "list_windows", "capture", "execute"],
+ "methods": ["ping", "capabilities", "list_windows", "capture", "execute", "execute_batch"],
"axapiProvider": "Accessibility (AXAPI)",
"platform": "macos",
"accessibilityGranted": AccessibilityPermission.isGranted,
@@ -83,9 +85,60 @@ final class Dispatcher {
}
private func handleExecute(params: JsonValue?) throws -> Any {
+ guard let p = params else {
+ throw RpcError(code: .invalidParams, message: "execute requires params.")
+ }
+ let req = try buildExecuteRequest(p)
+ return try capturer.execute(req)
+ }
+
+ /// Run N actions in one in-process loop so the entire batch costs one
+ /// stdio round-trip. `onError: stop` (default) aborts on the first
+ /// failure; `onError: continue` runs the full array regardless.
+ private func handleExecuteBatch(params: JsonValue?) throws -> Any {
guard let p = params,
- let elementId = p.string("elementId"), !elementId.isEmpty else {
- throw RpcError(code: .invalidParams, message: "execute requires `elementId`.")
+ let actionsAny = p.dict?["actions"],
+ case .array(let actionArray) = JsonValue.fromAny(actionsAny) else {
+ throw RpcError(code: .invalidParams, message: "execute_batch requires an `actions` array.")
+ }
+
+ let stopOnError = (p.string("onError") ?? "stop") != "continue"
+
+ var results: [[String: Any]] = []
+ results.reserveCapacity(actionArray.count)
+ var allOk = true
+
+ for actionAny in actionArray {
+ let actionParam = JsonValue.fromAny(actionAny)
+ let req: AxapiCapturer.ExecuteRequest
+ do {
+ req = try buildExecuteRequest(actionParam)
+ } catch {
+ results.append(["ok": false, "message": "\(error)"])
+ allOk = false
+ if stopOnError { break }
+ continue
+ }
+ let r = try capturer.execute(req)
+ // `capturer.execute` returns `[String: Any]` with `ok` / `message` / `newValue`.
+ let normalized = r as? [String: Any] ?? [:]
+ results.append(normalized)
+ if let ok = normalized["ok"] as? Bool, !ok {
+ allOk = false
+ if stopOnError { break }
+ }
+ }
+
+ return [
+ "results": results,
+ "allOk": allOk,
+ "executedCount": results.count,
+ ] as [String: Any]
+ }
+
+ private func buildExecuteRequest(_ p: JsonValue) throws -> AxapiCapturer.ExecuteRequest {
+ guard let elementId = p.string("elementId"), !elementId.isEmpty else {
+ throw RpcError(code: .invalidParams, message: "execute action requires `elementId`.")
}
var req = AxapiCapturer.ExecuteRequest()
req.elementId = elementId
@@ -100,6 +153,6 @@ final class Dispatcher {
}
req.clearFirst = p.bool("clearFirst") ?? false
if let i = p.int("timeoutMs") { req.timeoutMs = i }
- return try capturer.execute(req)
+ return req
}
}
diff --git a/apps/agent-runner/bridges/windows/Program.cs b/apps/agent-runner/bridges/windows/Program.cs
index f0af753..72a4981 100644
--- a/apps/agent-runner/bridges/windows/Program.cs
+++ b/apps/agent-runner/bridges/windows/Program.cs
@@ -205,14 +205,15 @@ public RpcDispatcher()
{
return method switch
{
- "ping" => HandlePing(),
- "capabilities" => HandleCapabilities(),
- "list_windows" => HandleListWindows(),
- "capture" => HandleCapture(@params),
- "execute" => HandleExecute(@params),
+ "ping" => HandlePing(),
+ "capabilities" => HandleCapabilities(),
+ "list_windows" => HandleListWindows(),
+ "capture" => HandleCapture(@params),
+ "execute" => HandleExecute(@params),
+ "execute_batch" => HandleExecuteBatch(@params),
_ => throw new RpcException(
RpcError.MethodNotFound,
- $"Unknown method: {method}. Supported: ping, capabilities, list_windows, capture, execute."),
+ $"Unknown method: {method}. Supported: ping, capabilities, list_windows, capture, execute, execute_batch."),
};
}
@@ -247,6 +248,7 @@ public void Dispose()
"list_windows",
"capture",
"execute",
+ "execute_batch",
},
uiaProvider = "FlaUI.UIA3",
platform = "windows",
@@ -288,22 +290,7 @@ private object HandleCapture(JsonElement @params)
private object HandleExecute(JsonElement @params)
{
- var modifiers = ReadStringArray(@params, "modifiers");
-
- var req = new UiaCapturer.ExecuteRequest
- {
- ElementId = ReadString(@params, "elementId")
- ?? throw new RpcException(RpcError.InvalidParams, "execute requires `elementId`."),
- ActionType = ReadString(@params, "actionType") ?? "click",
- Text = ReadString(@params, "text"),
- Value = ReadString(@params, "value"),
- Checked = ReadBool(@params, "checked"),
- Expanded = ReadBool(@params, "expanded"),
- Key = ReadString(@params, "key"),
- Modifiers = modifiers,
- ClearFirst = ReadBool(@params, "clearFirst") ?? false,
- TimeoutMs = ReadInt(@params, "timeoutMs") ?? 5000,
- };
+ var req = BuildExecuteRequest(@params);
try
{
@@ -323,6 +310,82 @@ private object HandleExecute(JsonElement @params)
}
}
+ ///
+ /// Run a sequence of actions inside the same STA invocation so the entire
+ /// batch costs one stdio round-trip. Honours `onError: stop | continue`
+ /// to abort or keep going past failures.
+ ///
+ private object HandleExecuteBatch(JsonElement @params)
+ {
+ if (!@params.TryGetProperty("actions", out var actionsEl) || actionsEl.ValueKind != JsonValueKind.Array)
+ {
+ throw new RpcException(RpcError.InvalidParams, "execute_batch requires an `actions` array.");
+ }
+
+ var stopOnError = (ReadString(@params, "onError") ?? "stop") != "continue";
+
+ var requests = new List(actionsEl.GetArrayLength());
+ foreach (var actionEl in actionsEl.EnumerateArray())
+ {
+ requests.Add(BuildExecuteRequest(actionEl));
+ }
+
+ var results = new List