@ai-agent-forge/plugin-desktop-windows-input 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,711 @@
1
+ /**
2
+ * Inlined from agent-forge `src/plugins/desktop/windows-uia-adapter.ts`
3
+ * (D-075 S3 extraction) so this package keeps its dependency closure limited
4
+ * to the plugin SDK and typebox. Semantics are unchanged: the full driver
5
+ * (query + action PowerShell scripts) is carried completely even though this
6
+ * observe package only exposes the read-only capability surface. This file is
7
+ * a deliberate copy shared (not imported) with
8
+ * `plugins/desktop-windows-observe/src/windows-uia-adapter.ts`; keep both
9
+ * copies in sync.
10
+ */
11
+ import { spawnSync as nodeSpawnSync } from "node:child_process";
12
+ import { detachedFreeze } from "@agent-forge/plugin-sdk";
13
+ import { findDesktopReplaySessionMismatch, requiresDesktopUserConfirmation } from "./types.js";
14
+ const READONLY_CAPABILITIES = ["desktop-session", "desktop-uia"];
15
+ const SUPPORTED_ACTIONS = [
16
+ "focus",
17
+ "click",
18
+ "type",
19
+ "key",
20
+ "scroll",
21
+ ];
22
+ const error = (code, message, causeCode) => ({
23
+ version: 1,
24
+ code,
25
+ message,
26
+ ...(causeCode === undefined ? {} : { causeCode }),
27
+ });
28
+ const unsupported = (operationId, reason, causeCode = "platform-unavailable") => ({
29
+ version: 1,
30
+ operationId,
31
+ status: "unsupported",
32
+ summary: reason,
33
+ error: error(causeCode === "action-unsupported" ? "action_unsupported" : "platform_unavailable", reason, causeCode),
34
+ });
35
+ const rectangle = (raw) => ({
36
+ x: Number.isFinite(raw.x) ? Number(raw.x) : 0,
37
+ y: Number.isFinite(raw.y) ? Number(raw.y) : 0,
38
+ width: Number.isFinite(raw.width) ? Number(raw.width) : 0,
39
+ height: Number.isFinite(raw.height) ? Number(raw.height) : 0,
40
+ });
41
+ const QUERY_SCRIPT = `
42
+ try {
43
+ Add-Type -AssemblyName UIAutomationClient -ErrorAction Stop
44
+ Add-Type -AssemblyName UIAutomationTypes -ErrorAction Stop
45
+ function SafeInt($value) {
46
+ $number = [double]$value
47
+ if ([double]::IsNaN($number) -or [double]::IsInfinity($number)) { return 0 }
48
+ return [int][math]::Round($number)
49
+ }
50
+ function Get-ControlId($node, [int]$index) {
51
+ $automationId = [string]$node.Current.AutomationId
52
+ $name = [string]$node.Current.Name
53
+ $role = [string]$node.Current.ControlType.ProgrammaticName
54
+ if ($automationId.Length -gt 0) { return $automationId }
55
+ if ($name.Length -gt 0) { return $role + ':' + $name }
56
+ return $role + ':' + $index
57
+ }
58
+ $root = [System.Windows.Automation.AutomationElement]::RootElement
59
+ $windowWalker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
60
+ $windows = [System.Collections.Generic.List[object]]::new()
61
+ $controls = [System.Collections.Generic.List[object]]::new()
62
+ $focusedMatches = [System.Collections.Generic.List[object]]::new()
63
+ $focused = $null
64
+ try { $focused = [System.Windows.Automation.AutomationElement]::FocusedElement } catch {}
65
+ $maxWindows = 256
66
+ $maxControlsPerWindow = 1000
67
+ $maxControlsTotal = 10000
68
+ $maxDepth = 32
69
+ function Add-Descendants($node, [int64]$windowHandle, [int]$depth, [ref]$count) {
70
+ if ($null -eq $node -or $depth -gt $maxDepth -or $count.Value -ge $maxControlsPerWindow -or $controls.Count -ge $maxControlsTotal) { return }
71
+ $index = $count.Value
72
+ $count.Value = $count.Value + 1
73
+ $childRect = $node.Current.BoundingRectangle
74
+ $controlId = Get-ControlId $node $index
75
+ $value = $null
76
+ try {
77
+ $pattern = $null
78
+ if ($node.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) { $value = [string]$pattern.Current.Value }
79
+ } catch {}
80
+ $name = [string]$node.Current.Name
81
+ $nativeWindowHandle = [int64]$node.Current.NativeWindowHandle
82
+ if ($null -eq $value -and $name.Length -gt 0) { $value = $name }
83
+ $controls.Add([pscustomobject]@{ windowHandle=$windowHandle; nativeWindowHandle=$nativeWindowHandle; controlId=$controlId; name=$name; role=[string]$node.Current.ControlType.ProgrammaticName; enabled=[bool]$node.Current.IsEnabled; value=$value; x=(SafeInt $childRect.X); y=(SafeInt $childRect.Y); width=(SafeInt $childRect.Width); height=(SafeInt $childRect.Height) })
84
+ try {
85
+ if ($null -ne $focused -and $node.Equals($focused)) { $focusedMatches.Add([pscustomobject]@{ windowHandle=$windowHandle; controlId=$controlId }) }
86
+ } catch {}
87
+ $child = $windowWalker.GetFirstChild($node)
88
+ while ($null -ne $child -and $count.Value -lt $maxControlsPerWindow) {
89
+ Add-Descendants $child $windowHandle ($depth + 1) $count
90
+ $child = $windowWalker.GetNextSibling($child)
91
+ }
92
+ }
93
+ $window = $windowWalker.GetFirstChild($root)
94
+ $windowCount = 0
95
+ while ($null -ne $window -and $windowCount -lt $maxWindows) {
96
+ $handle = [int64]$window.Current.NativeWindowHandle
97
+ if ($handle -ne 0) {
98
+ $rect = $window.Current.BoundingRectangle
99
+ $processId = [int]$window.Current.ProcessId
100
+ $windows.Add([pscustomobject]@{ handle=$handle; processId=$processId; name=[string]$window.Current.Name; x=(SafeInt $rect.X); y=(SafeInt $rect.Y); width=(SafeInt $rect.Width); height=(SafeInt $rect.Height) })
101
+ $windowCount++
102
+ $child = $windowWalker.GetFirstChild($window)
103
+ $count = 0
104
+ while ($null -ne $child -and $count -lt $maxControlsPerWindow -and $controls.Count -lt $maxControlsTotal) {
105
+ Add-Descendants $child $handle 1 ([ref]$count)
106
+ $child = $windowWalker.GetNextSibling($child)
107
+ }
108
+ }
109
+ $window = $windowWalker.GetNextSibling($window)
110
+ }
111
+ $focusedControl = if ($focusedMatches.Count -gt 0) { $focusedMatches[0] } else { $null }
112
+ [pscustomobject]@{ windows=@($windows); controls=@($controls); focusedControl=$focusedControl; available=$true } | ConvertTo-Json -Compress -Depth 8
113
+ } catch {
114
+ [pscustomobject]@{ available=$false; error=[string]$_.Exception.Message } | ConvertTo-Json -Compress -Depth 5
115
+ }`;
116
+ const ACTION_SCRIPT = `
117
+ try {
118
+ Add-Type -AssemblyName UIAutomationClient -ErrorAction Stop
119
+ Add-Type -AssemblyName UIAutomationTypes -ErrorAction Stop
120
+ $payload = $env:AGENT_FORGE_UIA_INPUT | ConvertFrom-Json
121
+ $handle = [int64]$payload.target.windowHandle
122
+ $window = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr]$handle)
123
+ if ($null -eq $window) { throw 'Window handle is no longer available' }
124
+ $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
125
+ $target = $window
126
+ $script:AgentForgeTarget = $null
127
+ if ($null -ne $payload.target.controlId) {
128
+ $controlHandle = [int64]$payload.target.controlWindowHandle
129
+ if ($controlHandle -gt 0) {
130
+ try {
131
+ $candidate = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr]$controlHandle)
132
+ if ($null -ne $candidate) {
133
+ $candidateAutomationId = [string]$candidate.Current.AutomationId
134
+ $candidateName = [string]$candidate.Current.Name
135
+ $candidateRole = [string]$candidate.Current.ControlType.ProgrammaticName
136
+ $candidateId = if ($candidateAutomationId.Length -gt 0) { $candidateAutomationId } elseif ($candidateName.Length -gt 0) { $candidateRole + ':' + $candidateName } else { $null }
137
+ if ($candidateId -eq [string]$payload.target.controlId) { $script:AgentForgeTarget = $candidate }
138
+ }
139
+ } catch {}
140
+ }
141
+ $maxDepth = 32
142
+ $maxVisited = 1000
143
+ function Find-Descendant($node, [int]$depth, [ref]$visited) {
144
+ if ($null -eq $node -or $depth -gt $maxDepth -or $visited.Value -ge $maxVisited -or $null -ne $script:AgentForgeTarget) { return }
145
+ $visited.Value = $visited.Value + 1
146
+ $automationId = [string]$node.Current.AutomationId
147
+ $name = [string]$node.Current.Name
148
+ $role = [string]$node.Current.ControlType.ProgrammaticName
149
+ $id = if ($automationId.Length -gt 0) { $automationId } elseif ($name.Length -gt 0) { $role + ':' + $name } else { $role + ':' + ($visited.Value - 1) }
150
+ if ($id -eq [string]$payload.target.controlId) { $script:AgentForgeTarget = $node; return }
151
+ $child = $walker.GetFirstChild($node)
152
+ while ($null -ne $child -and $visited.Value -lt $maxVisited -and $null -eq $script:AgentForgeTarget) {
153
+ Find-Descendant $child ($depth + 1) $visited
154
+ $child = $walker.GetNextSibling($child)
155
+ }
156
+ }
157
+ $visited = 0
158
+ if ($null -eq $script:AgentForgeTarget) {
159
+ $node = $walker.GetFirstChild($window)
160
+ while ($null -ne $node -and $visited -lt $maxVisited -and $null -eq $script:AgentForgeTarget) {
161
+ Find-Descendant $node 1 ([ref]$visited)
162
+ $node = $walker.GetNextSibling($node)
163
+ }
164
+ }
165
+ if ($null -eq $script:AgentForgeTarget) { throw ('Control was not found: ' + [string]$payload.target.controlId) }
166
+ $target = $script:AgentForgeTarget
167
+ }
168
+ if ($payload.action.kind -eq 'focus') { $target.SetFocus() }
169
+ elseif ($payload.action.kind -eq 'click') {
170
+ $pattern = $null
171
+ $invoked = $false
172
+ try {
173
+ if ($target.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$pattern)) {
174
+ $pattern.Invoke()
175
+ $invoked = $true
176
+ }
177
+ } catch {}
178
+ if (-not $invoked) {
179
+ if ($controlHandle -le 0) { throw 'Control does not support InvokePattern' }
180
+ try {
181
+ Add-Type @'
182
+ using System;
183
+ using System.Runtime.InteropServices;
184
+ public static class AgentForgeNativeClick {
185
+ [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
186
+ public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam, uint flags, uint timeout, out IntPtr result);
187
+ }
188
+ '@
189
+ $sendResult = [IntPtr]::Zero
190
+ $callResult = [AgentForgeNativeClick]::SendMessageTimeout([IntPtr]$controlHandle, 0x00F5, [IntPtr]::Zero, [IntPtr]::Zero, 0x0002, 3000, [ref]$sendResult)
191
+ if ($callResult -eq [IntPtr]::Zero) { throw 'Target click failed' }
192
+ } catch { throw 'Target click failed' }
193
+ }
194
+ } elseif ($payload.action.kind -eq 'type') {
195
+ $pattern = $null
196
+ $set = $false
197
+ try {
198
+ if ($target.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) {
199
+ $pattern.SetValue([string]$payload.action.text)
200
+ $set = $true
201
+ }
202
+ } catch {}
203
+ if (-not $set) {
204
+ $legacy = $null
205
+ try {
206
+ if ($target.TryGetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern, [ref]$legacy)) {
207
+ $legacy.SetValue([string]$payload.action.text)
208
+ $set = $true
209
+ }
210
+ } catch {}
211
+ }
212
+ if (-not $set) {
213
+ if ($controlHandle -le 0) { throw 'Control does not support ValuePattern or LegacyIAccessiblePattern' }
214
+ try {
215
+ Add-Type @'
216
+ using System;
217
+ using System.Runtime.InteropServices;
218
+ public static class AgentForgeNativeText {
219
+ [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
220
+ public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, string lParam, uint flags, uint timeout, out IntPtr result);
221
+ }
222
+ '@
223
+ $sendResult = [IntPtr]::Zero
224
+ $callResult = [AgentForgeNativeText]::SendMessageTimeout([IntPtr]$controlHandle, 0x000C, [IntPtr]::Zero, [string]$payload.action.text, 0x0002, 3000, [ref]$sendResult)
225
+ if ($callResult -eq [IntPtr]::Zero -or $sendResult -eq [IntPtr]::Zero) { throw 'Target text update failed' }
226
+ } catch { throw 'Target text update failed' }
227
+ }
228
+ } elseif ($payload.action.kind -eq 'key') {
229
+ $keyText = [string]$payload.action.key
230
+ $hasSendKeysSyntax = $keyText -match '[+%^{}]'
231
+ if (-not $hasSendKeysSyntax -and $controlHandle -gt 0) {
232
+ # Plain text with a real control handle: deliver synchronously through
233
+ # SendMessage WM_CHAR. Queued input (SendInput / WScript SendKeys) never
234
+ # reaches GUI windows in non-interactive or remote desktop sessions, while
235
+ # synchronous SendMessage does — the same path that type (WM_SETTEXT) and
236
+ # click (BM_CLICK) already use. Keyboard focus is not required.
237
+ Add-Type @'
238
+ using System;
239
+ using System.Runtime.InteropServices;
240
+ public static class AgentForgeNativeKey {
241
+ [DllImport("user32.dll", SetLastError=true)] public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
242
+ }
243
+ '@
244
+ foreach ($ch in $keyText.ToCharArray()) {
245
+ [AgentForgeNativeKey]::SendMessage([IntPtr]$controlHandle, 0x0102, [IntPtr][char]$ch, [IntPtr]::Zero) | Out-Null
246
+ }
247
+ } else {
248
+ if ($payload.target.controlId) { $target.SetFocus() }
249
+ $wshell = New-Object -ComObject WScript.Shell
250
+ $wshell.SendKeys($keyText)
251
+ }
252
+ } elseif ($payload.action.kind -eq 'scroll') {
253
+ Add-Type @'
254
+ using System;
255
+ using System.Runtime.InteropServices;
256
+ public static class AgentForgeNativeScroll {
257
+ [DllImport("user32.dll", SetLastError=true)]
258
+ public static extern bool SetCursorPos(int X, int Y);
259
+ [DllImport("user32.dll")]
260
+ public static extern void mouse_event(uint dwFlags, int dx, int dy, int dwData, UIntPtr dwExtraInfo);
261
+ }
262
+ '@
263
+ $direction = [string]$payload.action.text
264
+ $directionMatch = [regex]::Match($direction, '^(up|down)(?::(\\d+))?$')
265
+ if (-not $directionMatch.Success) { throw 'Scroll direction must be up or down with optional :lines' }
266
+ $lines = if ($directionMatch.Groups[2].Value) { [int]$directionMatch.Groups[2].Value } else { 3 }
267
+ if ($lines -lt 1 -or $lines -gt 100) { throw 'Scroll lines must be between 1 and 100' }
268
+ try {
269
+ $rect = $target.Current.BoundingRectangle
270
+ $centerX = [int]($rect.X + $rect.Width / 2)
271
+ $centerY = [int]($rect.Y + $rect.Height / 2)
272
+ [AgentForgeNativeScroll]::SetCursorPos($centerX, $centerY) | Out-Null
273
+ } catch {}
274
+ $wheelDelta = 120 * $lines
275
+ if ($direction.StartsWith('down')) { $wheelDelta = -$wheelDelta }
276
+ [AgentForgeNativeScroll]::mouse_event(0x0800, 0, 0, $wheelDelta, [UIntPtr]::Zero)
277
+ } else { throw ('Action kind is not supported by UI Automation: ' + [string]$payload.action.kind) }
278
+ [pscustomobject]@{ ok=$true } | ConvertTo-Json -Compress
279
+ } catch {
280
+ $message = [string]$_.Exception.Message
281
+ $code = if ($message -match 'Window handle is no longer available|Control was not found') { 'stale_target' } else { 'action_failed' }
282
+ $reason = if ($message -match 'ValuePattern|LegacyIAccessiblePattern') { 'value_pattern_unavailable' } elseif ($message -match 'InvokePattern') { 'invoke_pattern_unavailable' } elseif ($message -match 'SetFocus') { 'focus_failed' } else { 'uia_action_exception' }
283
+ [pscustomobject]@{ ok=$false; code=$code; reason=$reason } | ConvertTo-Json -Compress
284
+ }`;
285
+ const parseJson = (stdout) => {
286
+ try {
287
+ const parsed = JSON.parse(stdout.trim());
288
+ return typeof parsed === "object" && parsed !== null ? parsed : undefined;
289
+ }
290
+ catch {
291
+ return undefined;
292
+ }
293
+ };
294
+ export class WindowsUiAutomationDriver {
295
+ driverId = "windows-uia";
296
+ driverVersion = "1.0.0";
297
+ command;
298
+ timeoutMs;
299
+ platform;
300
+ spawn;
301
+ sessions = new Map();
302
+ targets = new Map();
303
+ sessionTargetKeys = new Map();
304
+ latestObservationIds = new Map();
305
+ observedSessions = new Set();
306
+ nextSessionId = 1;
307
+ nextObservationId = 1;
308
+ constructor(options = {}) {
309
+ this.command = options.command ?? "powershell.exe";
310
+ this.timeoutMs = options.timeoutMs ?? 15_000;
311
+ this.platform = options.platform ?? process.platform;
312
+ this.spawn =
313
+ options.spawnSync ??
314
+ ((command, args, spawnOptions) => nodeSpawnSync(command, args, spawnOptions));
315
+ }
316
+ probe() {
317
+ if (this.platform !== "win32") {
318
+ return {
319
+ available: false,
320
+ platform: this.platform,
321
+ driver: "windows-uia",
322
+ reason: "Windows UI Automation requires win32",
323
+ };
324
+ }
325
+ const result = this.run(QUERY_SCRIPT);
326
+ if (result === undefined)
327
+ return {
328
+ available: false,
329
+ platform: this.platform,
330
+ driver: "windows-uia",
331
+ reason: "PowerShell or UIAutomationClient is unavailable",
332
+ };
333
+ return result.available === true
334
+ ? {
335
+ available: true,
336
+ platform: this.platform,
337
+ driver: "windows-uia",
338
+ capabilities: READONLY_CAPABILITIES,
339
+ }
340
+ : {
341
+ available: false,
342
+ platform: this.platform,
343
+ driver: "windows-uia",
344
+ reason: "UI Automation probe failed",
345
+ };
346
+ }
347
+ openSession(request) {
348
+ const probe = this.probe();
349
+ if (!probe.available)
350
+ return {
351
+ status: "unsupported",
352
+ error: error("platform_unavailable", probe.reason ?? "Windows UI Automation is unavailable", "platform-unavailable"),
353
+ };
354
+ const session = detachedFreeze({
355
+ version: 1,
356
+ sessionId: `desktop-windows-session-${this.nextSessionId++}`,
357
+ isolation: request.isolation,
358
+ status: "ready",
359
+ ...(request.applicationId === undefined ? {} : { applicationId: request.applicationId }),
360
+ });
361
+ this.sessions.set(session.sessionId, session);
362
+ return { status: "succeeded", session };
363
+ }
364
+ observe(sessionId) {
365
+ const session = this.sessions.get(sessionId);
366
+ if (session === undefined)
367
+ return this.failed("observe", "session_not_found", "Session does not exist");
368
+ if (session.status === "stopped")
369
+ return this.cancelled("observe", "Session has been stopped");
370
+ const raw = this.run(QUERY_SCRIPT);
371
+ if (raw === undefined || raw.available !== true) {
372
+ this.invalidateObservation(sessionId);
373
+ return unsupported("observe", "Windows UI Automation observation failed");
374
+ }
375
+ return this.toObservation(session, raw);
376
+ }
377
+ execute(request) {
378
+ const session = this.sessions.get(request.sessionId);
379
+ if (session === undefined)
380
+ return this.failed(request.operationId, "session_not_found", "Session does not exist");
381
+ if (session.status === "stopped")
382
+ return this.cancelled(request.operationId, "Session has been stopped");
383
+ if (requiresDesktopUserConfirmation(request.action.kind))
384
+ return {
385
+ version: 1,
386
+ operationId: request.operationId,
387
+ status: "needs_user",
388
+ summary: `User confirmation required for ${request.action.kind}`,
389
+ error: error("needs_user", `The ${request.action.kind} action is paused for the user`, request.action.kind),
390
+ };
391
+ if (!SUPPORTED_ACTIONS.includes(request.action.kind))
392
+ return unsupported(request.operationId, "The selected Windows UI Automation adapter does not support this action", "action-unsupported");
393
+ if (request.action.target === undefined)
394
+ return this.failed(request.operationId, "target_required", "This desktop action requires an observed target");
395
+ if (request.action.kind === "type" && typeof request.action.text !== "string")
396
+ return this.failed(request.operationId, "text_required", "Type action requires text");
397
+ if (request.action.kind === "key" && typeof request.action.key !== "string")
398
+ return this.failed(request.operationId, "key_required", "Key action requires a key sequence");
399
+ if (request.action.kind === "scroll" && typeof request.action.text !== "string")
400
+ return this.failed(request.operationId, "direction_required", "Scroll action requires an up/down direction");
401
+ const postconditions = request.postconditions ?? [];
402
+ if (postconditions.length > 0 && request.reobserveAfterAction === false)
403
+ return this.failed(request.operationId, "reobserve_required", "Postconditions require an action re-observation");
404
+ const target = this.resolveTarget(request.sessionId, request.action.target);
405
+ if (target === undefined)
406
+ return this.failed(request.operationId, this.observedSessions.has(request.sessionId) ? "stale_target" : "target_not_found", this.observedSessions.has(request.sessionId)
407
+ ? "Target identity is stale; reobserve the desktop before retrying"
408
+ : "Target identity was not observed by UI Automation");
409
+ const raw = this.run(ACTION_SCRIPT, { action: request.action, target: target ?? {} });
410
+ if (raw === undefined)
411
+ return unsupported(request.operationId, "Windows UI Automation action failed");
412
+ if (raw.ok !== true) {
413
+ if (raw.code === "stale_target") {
414
+ this.invalidateObservation(request.sessionId);
415
+ return this.failed(request.operationId, "stale_target", "Target identity is stale; reobserve the desktop before retrying");
416
+ }
417
+ const causeCode = typeof raw.reason === "string" &&
418
+ [
419
+ "value_pattern_unavailable",
420
+ "invoke_pattern_unavailable",
421
+ "focus_failed",
422
+ "uia_action_exception",
423
+ ].includes(raw.reason)
424
+ ? raw.reason
425
+ : "uia_action_exception";
426
+ return this.failed(request.operationId, "action_failed", "UI Automation action failed", causeCode);
427
+ }
428
+ const observation = request.reobserveAfterAction === false ? undefined : this.observe(request.sessionId);
429
+ const verification = this.verifyPostconditions(postconditions, observation);
430
+ if (postconditions.length > 0 && verification.status === "unknown")
431
+ return {
432
+ version: 1,
433
+ operationId: request.operationId,
434
+ status: "failed",
435
+ summary: "Desktop action completed but could not be re-observed",
436
+ verification,
437
+ error: error("verification_unavailable", "The requested desktop state could not be re-observed", "reobserve-failed"),
438
+ };
439
+ if (verification.status === "failed")
440
+ return {
441
+ version: 1,
442
+ operationId: request.operationId,
443
+ status: "failed",
444
+ summary: "Desktop action completed but a postcondition failed",
445
+ ...(observation !== undefined && "observationId" in observation ? { observation } : {}),
446
+ verification,
447
+ error: error("postcondition_failed", "The requested desktop state was not observed", "verification-failed"),
448
+ };
449
+ return {
450
+ version: 1,
451
+ operationId: request.operationId,
452
+ status: "succeeded",
453
+ summary: "Desktop action completed through Windows UI Automation",
454
+ ...(observation !== undefined && "observationId" in observation ? { observation } : {}),
455
+ verification,
456
+ };
457
+ }
458
+ stop(sessionId) {
459
+ const session = this.sessions.get(sessionId);
460
+ if (session === undefined)
461
+ return this.failed("stop", "session_not_found", "Session does not exist");
462
+ const stopped = detachedFreeze({ ...session, status: "stopped" });
463
+ this.sessions.set(sessionId, stopped);
464
+ this.invalidateObservation(sessionId);
465
+ return stopped;
466
+ }
467
+ run(script, payload) {
468
+ if (this.platform !== "win32")
469
+ return undefined;
470
+ const env = {};
471
+ for (const key of ["SystemRoot", "WINDIR", "PATH", "PATHEXT", "ComSpec", "TEMP", "TMP"]) {
472
+ const value = process.env[key];
473
+ if (value !== undefined)
474
+ env[key] = value;
475
+ }
476
+ if (payload !== undefined)
477
+ env.AGENT_FORGE_UIA_INPUT = JSON.stringify(payload);
478
+ const result = this.spawn(this.command, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { encoding: "utf8", timeout: this.timeoutMs, windowsHide: true, env });
479
+ if (result.error !== undefined || result.status !== 0)
480
+ return undefined;
481
+ return parseJson(result.stdout);
482
+ }
483
+ toObservation(session, raw) {
484
+ this.invalidateObservation(session.sessionId);
485
+ const observationId = `desktop-windows-observation-${this.nextObservationId++}`;
486
+ const targetKeys = new Set();
487
+ this.sessionTargetKeys.set(session.sessionId, targetKeys);
488
+ const windows = [];
489
+ for (const item of raw.windows ?? []) {
490
+ if (!Number.isFinite(item.handle))
491
+ continue;
492
+ const handle = Number(item.handle);
493
+ const windowId = `window-${handle}`;
494
+ windows.push({
495
+ version: 1,
496
+ windowId,
497
+ appId: `process:${Number(item.processId ?? 0)}`,
498
+ title: item.name ?? "",
499
+ bounds: rectangle(item),
500
+ });
501
+ }
502
+ const visibleWindows = session.applicationId === undefined
503
+ ? windows
504
+ : windows.filter((window) => window.appId === session.applicationId);
505
+ for (const window of visibleWindows) {
506
+ const handle = Number(/^window-(\d+)$/.exec(window.windowId)?.[1]);
507
+ if (!Number.isFinite(handle))
508
+ continue;
509
+ const key = this.targetKey(session.sessionId, "window", window.windowId);
510
+ this.targets.set(key, { sessionId: session.sessionId, observationId, windowHandle: handle });
511
+ targetKeys.add(key);
512
+ }
513
+ const controls = [];
514
+ for (const item of raw.controls ?? []) {
515
+ if (!Number.isFinite(item.windowHandle))
516
+ continue;
517
+ const windowId = `window-${Number(item.windowHandle)}`;
518
+ if (!visibleWindows.some((window) => window.windowId === windowId))
519
+ continue;
520
+ const controlId = `${windowId}:${item.controlId ?? "control"}`;
521
+ const key = this.targetKey(session.sessionId, "control", controlId);
522
+ this.targets.set(key, {
523
+ sessionId: session.sessionId,
524
+ observationId,
525
+ windowHandle: Number(item.windowHandle),
526
+ ...(Number.isFinite(item.nativeWindowHandle) && Number(item.nativeWindowHandle) > 0
527
+ ? { controlWindowHandle: Number(item.nativeWindowHandle) }
528
+ : {}),
529
+ controlId: item.controlId,
530
+ });
531
+ targetKeys.add(key);
532
+ controls.push({
533
+ version: 1,
534
+ controlId,
535
+ windowId,
536
+ role: item.role ?? "",
537
+ name: item.name ?? "",
538
+ bounds: rectangle(item),
539
+ enabled: item.enabled !== false,
540
+ ...(typeof item.value === "string" && item.value.length > 0 ? { value: item.value } : {}),
541
+ });
542
+ }
543
+ const focusedControl = raw.focusedControl;
544
+ const focusedWindowId = Number.isFinite(focusedControl?.windowHandle)
545
+ ? `window-${Number(focusedControl?.windowHandle)}`
546
+ : undefined;
547
+ const focusedControlId = focusedWindowId === undefined || typeof focusedControl?.controlId !== "string"
548
+ ? undefined
549
+ : `${focusedWindowId}:${focusedControl.controlId}`;
550
+ const observation = detachedFreeze({
551
+ version: 1,
552
+ sessionId: session.sessionId,
553
+ observationId,
554
+ source: "windows-uia",
555
+ windows: visibleWindows,
556
+ controls,
557
+ ...(focusedControlId !== undefined && controls.some((control) => control.controlId === focusedControlId)
558
+ ? { focusedControlId }
559
+ : {}),
560
+ });
561
+ this.latestObservationIds.set(session.sessionId, observationId);
562
+ this.observedSessions.add(session.sessionId);
563
+ return observation;
564
+ }
565
+ resolveTarget(sessionId, target) {
566
+ const identity = target.kind === "control" ? target.controlId : target.windowId;
567
+ const record = this.targets.get(this.targetKey(sessionId, target.kind, identity));
568
+ if (record === undefined ||
569
+ record.sessionId !== sessionId ||
570
+ record.observationId !== this.latestObservationIds.get(sessionId))
571
+ return undefined;
572
+ return {
573
+ windowHandle: record.windowHandle,
574
+ ...(record.controlWindowHandle === undefined ? {} : { controlWindowHandle: record.controlWindowHandle }),
575
+ ...(record.controlId === undefined ? {} : { controlId: record.controlId }),
576
+ };
577
+ }
578
+ targetKey(sessionId, kind, identity) {
579
+ return `${sessionId}:${kind}:${identity}`;
580
+ }
581
+ invalidateObservation(sessionId) {
582
+ for (const key of this.sessionTargetKeys.get(sessionId) ?? [])
583
+ this.targets.delete(key);
584
+ this.sessionTargetKeys.delete(sessionId);
585
+ this.latestObservationIds.delete(sessionId);
586
+ }
587
+ verifyPostconditions(postconditions, observation) {
588
+ if (postconditions.length === 0)
589
+ return { status: "passed", checks: [] };
590
+ if (observation === undefined || !("observationId" in observation))
591
+ return { status: "unknown", checks: ["observation-unavailable"] };
592
+ const checks = [];
593
+ for (const condition of postconditions) {
594
+ const passed = this.checkPostcondition(condition, observation);
595
+ checks.push(`${condition.kind}:${passed ? "passed" : "failed"}`);
596
+ if (!passed)
597
+ return { status: "failed", checks };
598
+ }
599
+ return { status: "passed", checks };
600
+ }
601
+ checkPostcondition(condition, observation) {
602
+ if (condition.kind === "window-exists")
603
+ return observation.windows.some((window) => window.windowId === condition.windowId);
604
+ if (condition.kind === "control-exists")
605
+ return observation.controls.some((control) => control.controlId === condition.controlId);
606
+ if (condition.kind === "focused")
607
+ return observation.focusedControlId === condition.controlId;
608
+ const target = condition.target;
609
+ if (target.kind === "window") {
610
+ const window = observation.windows.find((item) => item.windowId === target.windowId);
611
+ if (window === undefined || condition.property !== "name")
612
+ return false;
613
+ return window.title === condition.expected;
614
+ }
615
+ const control = observation.controls.find((item) => {
616
+ if (item.controlId !== target.controlId)
617
+ return false;
618
+ return target.windowId === undefined || item.windowId === target.windowId;
619
+ });
620
+ if (control === undefined)
621
+ return false;
622
+ if (condition.property === "enabled")
623
+ return control.enabled === condition.expected;
624
+ if (condition.property === "name")
625
+ return control.name === condition.expected;
626
+ return control.value === condition.expected;
627
+ }
628
+ failed(operationId, code, message, causeCode) {
629
+ return { version: 1, operationId, status: "failed", summary: message, error: error(code, message, causeCode) };
630
+ }
631
+ cancelled(operationId, message) {
632
+ return {
633
+ version: 1,
634
+ operationId,
635
+ status: "cancelled",
636
+ summary: message,
637
+ error: error("cancelled", message, "stopped"),
638
+ };
639
+ }
640
+ }
641
+ /** Adapter facade that adds replay and the needs_user gate around a Windows driver. */
642
+ export class DesktopWindowsAdapter {
643
+ adapterId = "desktop-windows-uia";
644
+ adapterVersion = "1.0.0";
645
+ capabilities = READONLY_CAPABILITIES;
646
+ driver;
647
+ constructor(driver = new WindowsUiAutomationDriver()) {
648
+ this.driver = driver;
649
+ }
650
+ probe() {
651
+ return this.driver.probe();
652
+ }
653
+ openSession(request) {
654
+ return this.driver.openSession(request);
655
+ }
656
+ observe(sessionId) {
657
+ return this.driver.observe(sessionId);
658
+ }
659
+ reobserve(sessionId) {
660
+ return this.driver.observe(sessionId);
661
+ }
662
+ execute(request) {
663
+ if (requiresDesktopUserConfirmation(request.action.kind)) {
664
+ return {
665
+ version: 1,
666
+ operationId: request.operationId,
667
+ status: "needs_user",
668
+ summary: `User confirmation required for ${request.action.kind}`,
669
+ error: error("needs_user", `The ${request.action.kind} action is paused for the user`, request.action.kind),
670
+ };
671
+ }
672
+ return this.driver.execute(request);
673
+ }
674
+ stop(sessionId) {
675
+ return this.driver.stop(sessionId);
676
+ }
677
+ replay(request) {
678
+ const mismatchedOperationId = findDesktopReplaySessionMismatch(request);
679
+ if (mismatchedOperationId !== undefined)
680
+ return detachedFreeze({
681
+ version: 1,
682
+ status: "failed",
683
+ results: [
684
+ {
685
+ version: 1,
686
+ operationId: mismatchedOperationId,
687
+ status: "failed",
688
+ summary: "Replay operation session does not match the replay session",
689
+ error: error("session_mismatch", "Replay operation session does not match the replay session", "replay-session"),
690
+ },
691
+ ],
692
+ });
693
+ const results = [];
694
+ for (const operation of request.operations) {
695
+ const result = this.execute(operation);
696
+ results.push(result);
697
+ if (request.stopOnFailure && result.status !== "succeeded")
698
+ return detachedFreeze({
699
+ version: 1,
700
+ status: result.status === "cancelled"
701
+ ? "cancelled"
702
+ : result.status === "needs_user"
703
+ ? "needs_user"
704
+ : "failed",
705
+ results,
706
+ });
707
+ }
708
+ return detachedFreeze({ version: 1, status: "succeeded", results });
709
+ }
710
+ }
711
+ //# sourceMappingURL=windows-uia-adapter.js.map