@lardexx/arenacode 2.7.0 → 2.8.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.
package/README.md CHANGED
@@ -148,11 +148,17 @@ from the same bridge request the desktop app answers:
148
148
  | `ui apps` | the Start menu (Windows), `/Applications` (macOS), `.desktop` entries (Linux) |
149
149
  | `ui multi-select`, `ui multi-edit` | built from the primitives above |
150
150
 
151
- No extra install on **Windows** (PowerShell + `user32.dll`). **macOS**: grant Accessibility to your terminal, and
152
- `brew install cliclick` for the mouse. **Linux** (X11): `sudo apt install xdotool wmctrl imagemagick`; on Wayland
153
- `xdotool` cannot inject input, so Computer Agent needs XWayland. What this client does *not* have is the desktop's UI
154
- Automation tree: `ui state` returns windows and pixels, with `interactive_elements` empty, and `--annotated`
155
- screenshots are plain screenshots.
151
+ No extra install on **Windows**: PowerShell + `user32.dll` + the .NET UI Automation assemblies. The desktop client
152
+ ships a `windows-use.dll` (exports `wu_call_json`, `wu_free`, `wu_version`) for the same job; the terminal client
153
+ reproduces its behaviour instead of depending on it — the same JSON keys (`bounding_box`, `center`, `control_type`,
154
+ `interactive_elements_text`, `scrollable_elements`, `screenshot_png_base64`), the same ideas: the UI Automation tree of
155
+ the focused window for `ui state`, **unicode typing through `SendInput`** so the clipboard is untouched, extended-key
156
+ flags for arrows/navigation, fuzzy window matching (`ui app --name chrome` finds *"Google Chrome — New Tab"*), and
157
+ numbered labels drawn on the elements for `ui shot --annotated`.
158
+
159
+ **macOS**: grant Accessibility to your terminal, and `brew install cliclick` for the mouse. **Linux** (X11):
160
+ `sudo apt install xdotool wmctrl imagemagick`; on Wayland `xdotool` cannot inject input, so Computer Agent needs
161
+ XWayland. On macOS/Linux the accessibility tree is not read: `ui state` returns windows and geometry as elements.
156
162
 
157
163
  **Ask and Proceed / Proceed.** The desktop's Settings switch, `ctrl+x a` here (also `/ask`, `/proceed`,
158
164
  `--proceed`, and `arenacode config set approval proceed`). The default, **Ask and Proceed**, asks before commands,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lardexx/arenacode",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Terminal client for ArenaCode: talks to an arena-bridge, streams the Agent Mode timeline, and runs local commands and file operations on your PC. Node port of the ArenaCode desktop client.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
package/src/automation.js CHANGED
@@ -55,6 +55,8 @@ $ErrorActionPreference = 'Stop'
55
55
  $req = ([Console]::In.ReadToEnd() | ConvertFrom-Json)
56
56
  Add-Type -AssemblyName System.Drawing
57
57
  Add-Type -AssemblyName System.Windows.Forms
58
+ Add-Type -AssemblyName UIAutomationClient
59
+ Add-Type -AssemblyName UIAutomationTypes
58
60
  Add-Type -TypeDefinition @'
59
61
  using System;
60
62
  using System.Text;
@@ -67,48 +69,66 @@ public class AC {
67
69
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
68
70
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
69
71
  [DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr h, int x, int y, int w, int ht, bool repaint);
70
- [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
71
- [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
72
- [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
73
- [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
74
72
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
73
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
75
74
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
76
- public delegate bool EnumProc(IntPtr h, IntPtr p);
77
75
  [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
78
76
  }
79
77
  '@
80
78
 
79
+ $AC_TYPES = @(
80
+ [System.Windows.Automation.ControlType]::Button,
81
+ [System.Windows.Automation.ControlType]::CheckBox,
82
+ [System.Windows.Automation.ControlType]::ComboBox,
83
+ [System.Windows.Automation.ControlType]::Custom,
84
+ [System.Windows.Automation.ControlType]::DataItem,
85
+ [System.Windows.Automation.ControlType]::Edit,
86
+ [System.Windows.Automation.ControlType]::Hyperlink,
87
+ [System.Windows.Automation.ControlType]::ListItem,
88
+ [System.Windows.Automation.ControlType]::MenuItem,
89
+ [System.Windows.Automation.ControlType]::RadioButton,
90
+ [System.Windows.Automation.ControlType]::ScrollBar,
91
+ [System.Windows.Automation.ControlType]::Slider,
92
+ [System.Windows.Automation.ControlType]::Spinner,
93
+ [System.Windows.Automation.ControlType]::SplitButton,
94
+ [System.Windows.Automation.ControlType]::Tab,
95
+ [System.Windows.Automation.ControlType]::TabItem,
96
+ [System.Windows.Automation.ControlType]::Text,
97
+ [System.Windows.Automation.ControlType]::ToolBar,
98
+ [System.Windows.Automation.ControlType]::TreeItem
99
+ )
100
+
101
+ function Get-ACWindow($h) {
102
+ $hh = [IntPtr][int64]$h
103
+ $r = New-Object AC+RECT
104
+ [void][AC]::GetWindowRect($hh, [ref]$r)
105
+ $sb = New-Object System.Text.StringBuilder 1024
106
+ [void][AC]::GetWindowText($hh, $sb, 1024)
107
+ return [pscustomobject]@{
108
+ name = $sb.ToString()
109
+ handle = $hh.ToInt64()
110
+ hwnd = $hh.ToInt64()
111
+ x = $r.Left; y = $r.Top
112
+ width = [Math]::Max(0, $r.Right - $r.Left); height = [Math]::Max(0, $r.Bottom - $r.Top)
113
+ status = $(if ([AC]::IsIconic($hh)) { 'minimized' } else { 'normal' })
114
+ }
115
+ }
116
+
81
117
  function Get-ACWindows {
82
118
  $list = New-Object System.Collections.ArrayList
83
119
  foreach ($p in (Get-Process | Where-Object { $_.MainWindowHandle -ne 0 })) {
84
120
  try {
85
- $h = [IntPtr]$p.MainWindowHandle
86
- $r = New-Object AC+RECT
87
- [void][AC]::GetWindowRect($h, [ref]$r)
88
121
  $title = [string]$p.MainWindowTitle
89
122
  if ($title.Trim().Length -eq 0) { continue }
90
- [void]$list.Add([pscustomobject]@{
91
- name = $title
92
- app = [string]$p.ProcessName
93
- handle = $h.ToInt64()
94
- x = $r.Left; y = $r.Top
95
- width = [Math]::Max(0, $r.Right - $r.Left); height = [Math]::Max(0, $r.Bottom - $r.Top)
96
- process_id = [int]$p.Id
97
- status = $(if ($p.MainWindowHandle -eq 0) { 'hidden' } else { 'normal' })
98
- })
123
+ $w = Get-ACWindow $p.MainWindowHandle
124
+ $w | Add-Member -NotePropertyName process_id -NotePropertyValue ([int]$p.Id)
125
+ $w | Add-Member -NotePropertyName app -NotePropertyValue ([string]$p.ProcessName)
126
+ [void]$list.Add($w)
99
127
  } catch { }
100
128
  }
101
129
  return $list
102
130
  }
103
131
 
104
- function Find-ACWindow($name) {
105
- if (-not $name) { return $null }
106
- $all = Get-ACWindows
107
- $exact = $all | Where-Object { $_.name -eq $name } | Select-Object -First 1
108
- if ($exact) { return $exact }
109
- return ($all | Where-Object { $_.name -like "*$name*" } | Select-Object -First 1)
110
- }
111
-
112
132
  function Get-ACScreen {
113
133
  $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
114
134
  return @{ width = $b.Width; height = $b.Height }
@@ -116,37 +136,119 @@ function Get-ACScreen {
116
136
 
117
137
  function Send-ACKeys($pairs) {
118
138
  foreach ($k in $pairs) {
119
- if ($k.flags -eq 0) { [AC]::keybd_event([byte]$k.vk, 0, 0, [UIntPtr]::Zero) }
120
- else { [AC]::keybd_event([byte]$k.vk, 0, 2, [UIntPtr]::Zero) }
139
+ $flags = 0
140
+ if ($k.flags -eq 2) { $flags = $flags -bor 2 }
141
+ if ($k.ext) { $flags = $flags -bor 1 }
142
+ [AC]::keybd_event([byte]$k.vk, 0, $flags, [UIntPtr]::Zero)
121
143
  Start-Sleep -Milliseconds 12
122
144
  }
123
145
  }
124
146
 
147
+ # windows-use types through SendInput unicode events: the clipboard is never touched
148
+ function Send-ACUnicode($text) {
149
+ $typed = 0
150
+ foreach ($ch in $text.ToCharArray()) {
151
+ if ($ch -eq [char]10) {
152
+ [AC]::keybd_event(0x0D, 0, 0, [UIntPtr]::Zero)
153
+ [AC]::keybd_event(0x0D, 0, 2, [UIntPtr]::Zero)
154
+ $typed++
155
+ continue
156
+ }
157
+ if ($ch -eq [char]13) { continue }
158
+ [AC]::keybd_event(0, [byte][char]$ch, 4, [UIntPtr]::Zero)
159
+ [AC]::keybd_event(0, [byte][char]$ch, 6, [UIntPtr]::Zero)
160
+ $typed++
161
+ Start-Sleep -Milliseconds 3
162
+ }
163
+ return $typed
164
+ }
165
+
166
+ function Get-ACElements($hwnd, $interactiveOnly) {
167
+ $handle = [IntPtr][int64]$hwnd
168
+ $root = $null
169
+ if ($handle -ne [IntPtr]::Zero) {
170
+ $root = [System.Windows.Automation.AutomationElement]::FromHandle($handle)
171
+ } else {
172
+ $root = [System.Windows.Automation.AutomationElement]::FocusedElement
173
+ while ($root -ne $null -and $root.Current.NativeWindowHandle -eq 0) {
174
+ $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
175
+ $root = $walker.GetParent($root)
176
+ }
177
+ }
178
+ if ($root -eq $null) { return @() }
179
+
180
+ $conditions = @()
181
+ foreach ($ct in $AC_TYPES) {
182
+ $conditions += New-Object System.Windows.Automation.PropertyCondition(
183
+ [System.Windows.Automation.AutomationElement]::ControlTypeProperty, $ct)
184
+ }
185
+ $cond = New-Object System.Windows.Automation.OrCondition $conditions
186
+ $found = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $cond)
187
+
188
+ $out = New-Object System.Collections.ArrayList
189
+ $n = 0
190
+ foreach ($el in $found) {
191
+ if ($n -ge 120) { break }
192
+ try {
193
+ $cur = $el.Current
194
+ if ($cur.IsOffscreen) { continue }
195
+ $rect = $cur.BoundingRectangle
196
+ if ($rect.Width -le 0 -or $rect.Height -le 0) { continue }
197
+ $name = [string]$cur.Name
198
+ $ctype = [string]$cur.ControlType.ProgrammaticName -replace '^ControlType\.', ''
199
+ if ($interactiveOnly -and $ctype -eq 'Text' -and $name.Trim().Length -eq 0) { continue }
200
+ $val = ''
201
+ try {
202
+ $vp = $null
203
+ if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) { $val = [string]$vp.Current.Value }
204
+ } catch { $val = '' }
205
+ $scrollable = $false
206
+ try {
207
+ $sp = $null
208
+ if ($el.TryGetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern, [ref]$sp)) {
209
+ $scrollable = [bool]$sp.Current.VerticallyScrollable -or [bool]$sp.Current.HorizontallyScrollable
210
+ }
211
+ } catch { }
212
+ [void]$out.Add([pscustomobject]@{
213
+ name = $name
214
+ control_type = $ctype
215
+ kind = $ctype
216
+ bounding_box = @{ left = [int]$rect.X; top = [int]$rect.Y; right = [int]($rect.X + $rect.Width); bottom = [int]($rect.Y + $rect.Height) }
217
+ center = @{ x = [int]($rect.X + $rect.Width / 2); y = [int]($rect.Y + $rect.Height / 2) }
218
+ value = $val
219
+ scrollable = $scrollable
220
+ enabled = [bool]$cur.IsEnabled
221
+ automation_id = [string]$cur.AutomationId
222
+ })
223
+ $n++
224
+ } catch { }
225
+ }
226
+ return $out
227
+ }
228
+
125
229
  $out = @{ ok = $true }
126
230
  switch ($req.action) {
127
231
  'screen' { $s = Get-ACScreen; $out.width = $s.width; $out.height = $s.height }
128
- 'windows' { $out.windows = @(Get-ACWindows); $out.active_window = (Get-ACWindows | Select-Object -First 1) }
232
+ 'windows' { $out.windows = @(Get-ACWindows) }
129
233
  'active' {
130
- $h = [AC]::GetForegroundWindow()
131
- $sb = New-Object System.Text.StringBuilder 1024
132
- [void][AC]::GetWindowText($h, $sb, 1024)
133
- $out.name = $sb.ToString()
234
+ $hh = [AC]::GetForegroundWindow()
235
+ $w = Get-ACWindow $hh.ToInt64()
236
+ $out.name = $w.name
237
+ $out.handle = $w.handle
134
238
  }
135
239
  'focus' {
136
- $w = Find-ACWindow $req.name
137
- if ($w) {
138
- [void][AC]::ShowWindow([IntPtr]$w.handle, 9)
139
- [void][AC]::SetForegroundWindow([IntPtr]$w.handle)
140
- Start-Sleep -Milliseconds 150
141
- $out.name = $w.name
142
- } else { $out = @{ ok = $false; error = "window not found: $($req.name)" } }
240
+ $hh = [IntPtr][int64]$req.handle
241
+ [void][AC]::ShowWindow($hh, 9)
242
+ [void][AC]::SetForegroundWindow($hh)
243
+ Start-Sleep -Milliseconds 200
244
+ $w = Get-ACWindow $req.handle
245
+ $out.name = $w.name
143
246
  }
144
247
  'resize' {
145
- $w = Find-ACWindow $req.name
146
- if ($w) {
147
- [void][AC]::MoveWindow([IntPtr]$w.handle, [int]$req.x, [int]$req.y, [int]$req.width, [int]$req.height, $true)
148
- $out.name = $w.name
149
- } else { $out = @{ ok = $false; error = "window not found: $($req.name)" } }
248
+ $hh = [IntPtr][int64]$req.handle
249
+ [void][AC]::MoveWindow($hh, [int]$req.x, [int]$req.y, [int]$req.width, [int]$req.height, $true)
250
+ $w = Get-ACWindow $req.handle
251
+ $out.name = $w.name
150
252
  }
151
253
  'launch' {
152
254
  $name = [string]$req.name
@@ -161,7 +263,7 @@ switch ($req.action) {
161
263
  if ($exe) { Start-Process $exe.Source; $done = $true }
162
264
  }
163
265
  }
164
- if ($done) { Start-Sleep -Milliseconds 700; $out.name = $name } else { $out = @{ ok = $false; error = "app not found: $name" } }
266
+ if ($done) { Start-Sleep -Milliseconds 900; $out.name = $name } else { $out = @{ ok = $false; error = "app not found: $name" } }
165
267
  }
166
268
  'move' { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y) }
167
269
  'click' {
@@ -185,7 +287,7 @@ switch ($req.action) {
185
287
  }
186
288
  'up' {
187
289
  $flag = 4; if ($req.button -eq 'right') { $flag = 16 } elseif ($req.button -eq 'middle') { $flag = 64 }
188
- if ($req.x -ne $null) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y) }
290
+ if ($null -ne $req.x) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y) }
189
291
  [AC]::mouse_event($flag, 0, 0, 0, [UIntPtr]::Zero)
190
292
  }
191
293
  'drag' {
@@ -202,7 +304,7 @@ switch ($req.action) {
202
304
  [AC]::mouse_event(4, 0, 0, 0, [UIntPtr]::Zero)
203
305
  }
204
306
  'scroll' {
205
- if ($req.x -ne $null) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y); Start-Sleep -Milliseconds 40 }
307
+ if ($null -ne $req.x -and $null -ne $req.y) { [void][AC]::SetCursorPos([int]$req.x, [int]$req.y); Start-Sleep -Milliseconds 40 }
206
308
  $amount = [int]$req.amount
207
309
  $dir = 120; if ($amount -lt 0) { $dir = -120 }
208
310
  for ($i = 0; $i -lt [Math]::Abs($amount); $i++) {
@@ -212,24 +314,38 @@ switch ($req.action) {
212
314
  }
213
315
  'keys' { Send-ACKeys $req.sequence }
214
316
  'type' {
215
- if ($req.x -ne $null) {
317
+ if ($null -ne $req.x -and $null -ne $req.y) {
216
318
  [void][AC]::SetCursorPos([int]$req.x, [int]$req.y)
217
319
  Start-Sleep -Milliseconds 60
218
320
  [AC]::mouse_event(2, 0, 0, 0, [UIntPtr]::Zero)
219
321
  [AC]::mouse_event(4, 0, 0, 0, [UIntPtr]::Zero)
220
- Start-Sleep -Milliseconds 120
322
+ Start-Sleep -Milliseconds 150
221
323
  }
222
- Set-Clipboard -Value ([string]$req.text)
223
- Start-Sleep -Milliseconds 80
224
- [System.Windows.Forms.SendKeys]::SendWait('^v')
225
- Start-Sleep -Milliseconds 150
226
- $out.typed = ([string]$req.text).Length
324
+ $out.typed = (Send-ACUnicode ([string]$req.text))
227
325
  }
228
326
  'shot' {
229
327
  $s = Get-ACScreen
230
328
  $bmp = New-Object System.Drawing.Bitmap $s.width, $s.height
231
329
  $g = [System.Drawing.Graphics]::FromImage($bmp)
232
330
  $g.CopyFromScreen(0, 0, 0, 0, $bmp.Size)
331
+ if ($req.annotate) {
332
+ $fg = $null
333
+ try { $fg = [AC]::GetForegroundWindow().ToInt64() } catch { $fg = 0 }
334
+ $els = @(Get-ACElements $fg $true)
335
+ $font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
336
+ $brush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(230, 250, 178, 131))
337
+ $pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(200, 250, 178, 131)), 1
338
+ $i = 1
339
+ foreach ($e in $els) {
340
+ $bb = $e.bounding_box
341
+ $g.DrawRectangle($pen, $bb.left, $bb.top, [Math]::Max(1, $bb.right - $bb.left), [Math]::Max(1, $bb.bottom - $bb.top))
342
+ $g.FillRectangle($brush, $bb.left, [Math]::Max(0, $bb.top - 14), 18, 14)
343
+ $g.DrawString([string]$i, $font, (New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::Black)), $bb.left + 2, [Math]::Max(0, $bb.top - 14))
344
+ $i++
345
+ }
346
+ $pen.Dispose(); $brush.Dispose(); $font.Dispose()
347
+ $out.elements = $els.Count
348
+ }
233
349
  $bmp.Save($req.path, [System.Drawing.Imaging.ImageFormat]::Png)
234
350
  $g.Dispose(); $bmp.Dispose()
235
351
  $out.width = $s.width; $out.height = $s.height; $out.file = $req.path
@@ -238,6 +354,35 @@ switch ($req.action) {
238
354
  try { $out.apps = @(Get-StartApps | ForEach-Object { @{ name = $_.Name; app_id = $_.AppID } }) }
239
355
  catch { $out.apps = @() }
240
356
  }
357
+ 'state' {
358
+ $hh = $null
359
+ if ($null -ne $req.handle -and [int64]$req.handle -ne 0) { $hh = [int64]$req.handle } else { $hh = [AC]::GetForegroundWindow().ToInt64() }
360
+ $els = @(Get-ACElements $hh $true)
361
+ $interactive = @()
362
+ $scrollable = @()
363
+ $lines = New-Object System.Collections.ArrayList
364
+ $i = 1
365
+ foreach ($e in $els) {
366
+ if ($e.scrollable) { $scrollable += $e } else { $interactive += $e }
367
+ [void]$lines.Add("$i. $($e.control_type) '$($e.name)' at ($($e.center.x), $($e.center.y))")
368
+ $i++
369
+ }
370
+ $out.windows = @(Get-ACWindows)
371
+ $out.active_window = (Get-ACWindow $hh)
372
+ $out.interactive_elements = $interactive
373
+ $out.scrollable_elements = $scrollable
374
+ $out.interactive_elements_text = ($lines -join [Environment]::NewLine)
375
+ $out.scrollable_elements_text = (($scrollable | ForEach-Object { "$($_.control_type) '$($_.name)' at ($($_.center.x), $($_.center.y))" }) -join [Environment]::NewLine)
376
+ $out.system = @{
377
+ windows_version = [string](Get-CimInstance Win32_OperatingSystem).Caption
378
+ os_version = [Environment]::OSVersion.Version.ToString()
379
+ screen_size = (Get-ACScreen)
380
+ user_account_type = 'Local Account'
381
+ default_language = (Get-Culture).Name
382
+ dpi_scaling = 1
383
+ }
384
+ $out.metadata = @{ kind = 'ui-automation'; depth = 'descendants'; all_desktops = $false }
385
+ }
241
386
  'sysinfo' {
242
387
  $out.system = @{
243
388
  windows_version = [string](Get-CimInstance Win32_OperatingSystem).Caption
@@ -250,9 +395,8 @@ switch ($req.action) {
250
395
  }
251
396
  default { $out = @{ ok = $false; error = "unknown action: $($req.action)" } }
252
397
  }
253
- $out | ConvertTo-Json -Depth 6 -Compress
398
+ $out | ConvertTo-Json -Depth 8 -Compress
254
399
  `;
255
-
256
400
  let psEnginePath = null;
257
401
  function psEngine() {
258
402
  if (psEnginePath && fs.existsSync(psEnginePath)) return psEnginePath;
@@ -384,54 +528,103 @@ const IMAGE_TOOLS = [
384
528
  ['spectacle', (p) => ['-b', '-n', '-o', p]],
385
529
  ];
386
530
 
531
+ const VK = {
532
+ ctrl: 0x11, control: 0x11, alt: 0x12, option: 0x12, shift: 0x10, win: 0x5b, super: 0x5b,
533
+ meta: 0x5b, cmd: 0x5b, command: 0x5b,
534
+ enter: 0x0d, return: 0x0d, tab: 0x09, esc: 0x1b, escape: 0x1b, space: 0x20, spacebar: 0x20,
535
+ backspace: 0x08, bs: 0x08, delete: 0x2e, del: 0x2e, insert: 0x2d, ins: 0x2d,
536
+ home: 0x24, end: 0x23, pageup: 0x21, pagedown: 0x22, pgup: 0x21, pgdn: 0x22,
537
+ up: 0x26, down: 0x28, left: 0x25, right: 0x27, arrowup: 0x26, arrowdown: 0x28,
538
+ arrowleft: 0x25, arrowright: 0x27,
539
+ capslock: 0x14, numlock: 0x90, scrolllock: 0x91, printscreen: 0x2c, prtsc: 0x2c,
540
+ pause: 0x13, menu: 0x5d, contextmenu: 0x5d, apps: 0x5d,
541
+ plus: 0xbb, minus: 0xbd, comma: 0xbc, period: 0xbe, slash: 0xbf, backslash: 0xdc,
542
+ semicolon: 0xba, quote: 0xde, bracketleft: 0xdb, bracketright: 0xdd, backtick: 0xc0,
543
+ numpad0: 0x60, numpad1: 0x61, numpad2: 0x62, numpad3: 0x63, numpad4: 0x64,
544
+ numpad5: 0x65, numpad6: 0x66, numpad7: 0x67, numpad8: 0x68, numpad9: 0x69,
545
+ };
546
+ for (let i = 1; i <= 24; i += 1) VK[`f${i}`] = 0x6f + i;
547
+
548
+ // keys that need KEYEVENTF_EXTENDEDKEY, like windows-use's kScExtendedVks
549
+ const EXTENDED = new Set([
550
+ 0x2e, 0x2d, 0x24, 0x23, 0x21, 0x22, 0x26, 0x28, 0x25, 0x27, 0x5b, 0x5d, 0x2c, 0x90, 0x5d,
551
+ 0x6f + 11, 0x6f + 12,
552
+ ]);
553
+ const MODIFIERS = new Set(['ctrl', 'control', 'alt', 'option', 'shift', 'win', 'super', 'meta', 'cmd', 'command']);
554
+
387
555
  function keyboardSequence(combo) {
388
- // "ctrl+shift+s" -> [{vk, flags}] using Windows virtual-key codes, which the
389
- // PowerShell engine sends through keybd_event. Unknown names are ignored.
390
- const VK = {
391
- ctrl: 0x11, control: 0x11, alt: 0x12, shift: 0x10, win: 0x5b, meta: 0x5b, cmd: 0x5b,
392
- enter: 0x0d, return: 0x0d, tab: 0x09, esc: 0x1b, escape: 0x1b, space: 0x20,
393
- backspace: 0x08, delete: 0x2e, del: 0x2e, insert: 0x2d, home: 0x24, end: 0x23,
394
- pageup: 0x21, pagedown: 0x22, pgup: 0x21, pgdn: 0x22, up: 0x26, down: 0x28,
395
- left: 0x25, right: 0x27, printscreen: 0x2c, capslock: 0x14, menu: 0x5d,
396
- };
397
- for (let i = 1; i <= 24; i += 1) VK[`f${i}`] = 0x6f + i;
556
+ // "ctrl+shift+s" -> [{ vk, flags, ext }] for keybd_event, the call windows-use makes too
398
557
  const parts = String(combo || '')
399
558
  .toLowerCase()
400
559
  .split('+')
401
560
  .map((p) => p.trim())
402
561
  .filter(Boolean);
403
562
  if (!parts.length) return [];
404
- const seq = [];
405
- const repeat = parts.filter((p) => p === 'repeat').length;
406
- const keys = parts.filter((p) => p !== 'repeat');
563
+ let repeat = 1;
407
564
  const modVks = [];
408
565
  const mainVks = [];
409
- for (const key of keys) {
410
- if (VK[key]) {
411
- if (['ctrl', 'control', 'alt', 'shift', 'win', 'meta', 'cmd'].includes(key)) modVks.push(VK[key]);
412
- else mainVks.push(VK[key]);
413
- } else if (key.length === 1) {
414
- mainVks.push(key.toUpperCase().charCodeAt(0));
566
+ for (const key of parts) {
567
+ if (key === 'repeat' || /^x\d+$/.test(key)) {
568
+ repeat = key === 'repeat' ? 2 : Number(key.slice(1));
569
+ continue;
415
570
  }
571
+ let vk = VK[key];
572
+ if (vk === undefined && key.length === 1) vk = key.toUpperCase().charCodeAt(0);
573
+ if (vk === undefined) continue;
574
+ if (MODIFIERS.has(key)) modVks.push(vk);
575
+ else mainVks.push(vk);
416
576
  }
417
- const times = Math.max(1, repeat || 1);
577
+ const seq = [];
578
+ const times = Math.max(1, Math.min(50, repeat));
418
579
  for (let i = 0; i < times; i += 1) {
419
580
  if (!mainVks.length && modVks.length) {
420
581
  // "win" on its own: press and release the modifier itself
421
582
  const only = modVks[modVks.length - 1];
422
- seq.push({ vk: only, flags: 0 }, { vk: only, flags: 2 });
583
+ seq.push({ vk: only, flags: 0, ext: EXTENDED.has(only) });
584
+ seq.push({ vk: only, flags: 2, ext: EXTENDED.has(only) });
423
585
  continue;
424
586
  }
425
- for (const vk of modVks) seq.push({ vk, flags: 0 });
587
+ for (const vk of modVks) seq.push({ vk, flags: 0, ext: EXTENDED.has(vk) });
426
588
  for (const vk of mainVks) {
427
- seq.push({ vk, flags: 0 });
428
- seq.push({ vk, flags: 2 });
589
+ seq.push({ vk, flags: 0, ext: EXTENDED.has(vk) });
590
+ seq.push({ vk, flags: 2, ext: EXTENDED.has(vk) });
429
591
  }
430
- for (const vk of [...modVks].reverse()) seq.push({ vk, flags: 2 });
592
+ for (const vk of [...modVks].reverse()) seq.push({ vk, flags: 2, ext: EXTENDED.has(vk) });
431
593
  }
432
594
  return seq;
433
595
  }
434
596
 
597
+ // windows-use matches window names with fuzzy scores (wu::fuzzy::WRatio); the same
598
+ // idea keeps `ui app --name chrome` working when the title is "Google Chrome — …".
599
+ const fuzzyScore = (needle, haystack) => {
600
+ const a = String(needle || '').toLowerCase().trim();
601
+ const b = String(haystack || '').toLowerCase().trim();
602
+ if (!a || !b) return 0;
603
+ if (a === b) return 1;
604
+ if (b === a || b.startsWith(a)) return 0.95;
605
+ if (b.includes(a)) return 0.85;
606
+ const tokens = a.split(/\s+/).filter(Boolean);
607
+ const words = b.split(/\s+/).filter(Boolean);
608
+ const hits = tokens.filter((t) => words.some((w) => w.startsWith(t) || t.startsWith(w))).length;
609
+ if (hits) return 0.6 + (0.2 * hits) / tokens.length;
610
+ // character-level overlap, same spirit as partial_ratio
611
+ const chars = new Set(b.replace(/\s+/g, ''));
612
+ const overlap = [...a.replace(/\s+/g, '')].filter((c) => chars.has(c)).length;
613
+ return 0.4 * (overlap / Math.max(1, a.replace(/\s+/g, '').length));
614
+ };
615
+
616
+ const AVOIDED = [/arenacode/i, /arena code/i];
617
+
618
+ function pickWindow(list, name) {
619
+ const candidates = (list || []).filter((w) => w && w.name && !AVOIDED.some((re) => re.test(w.name)));
620
+ let best = null;
621
+ for (const w of candidates) {
622
+ const score = fuzzyScore(name, w.name);
623
+ if (score >= 0.6 && (!best || score > best.score)) best = { ...w, score };
624
+ }
625
+ return best;
626
+ }
627
+
435
628
  const automation = {
436
629
  platform: process.platform,
437
630
 
@@ -471,16 +664,31 @@ const automation = {
471
664
 
472
665
  windows() {
473
666
  if (isWin) {
474
- const r = winRun({ action: 'windows' });
475
- return r.ok ? { ok: true, windows: r.windows || [], active_window: r.active_window || null } : fail(r.error);
667
+ const r = winRun({ action: 'windows' }, 45000);
668
+ if (!r.ok) return fail(r.error);
669
+ return { ok: true, windows: r.windows || [] };
476
670
  }
477
671
  const list = isMac ? macWindows() : linuxWindows();
478
- return { ok: true, windows: list, active_window: list[0] || null };
672
+ return { ok: true, windows: list };
479
673
  },
480
674
 
481
675
  activeWindow() {
676
+ if (isWin) {
677
+ const r = winRun({ action: 'active' });
678
+ return r.ok ? { ok: true, name: r.name, handle: r.handle } : fail(r.error);
679
+ }
482
680
  const w = this.windows();
483
- return w.ok ? { ok: true, name: (w.active_window && w.active_window.name) || '' } : w;
681
+ const first = w.ok && w.windows.length ? w.windows[0] : null;
682
+ return { ok: true, name: first ? first.name : '', handle: first ? first.handle : null };
683
+ },
684
+
685
+ /** Finds the window the user means, the way windows-use does: best fuzzy match. */
686
+ findWindow(name) {
687
+ const list = this.windows();
688
+ if (!list.ok) return list;
689
+ const hit = pickWindow(list.windows, name);
690
+ if (!hit) return { ok: false, error: `window not found: ${name}` };
691
+ return { ok: true, window: hit };
484
692
  },
485
693
 
486
694
  launchApp(name) {
@@ -501,8 +709,10 @@ const automation = {
501
709
 
502
710
  focusWindow(name) {
503
711
  if (isWin) {
504
- const r = winRun({ action: 'focus', name });
505
- return r.ok ? { ok: true, name: r.name, app: r.name } : fail(r.error);
712
+ const found = this.findWindow(name);
713
+ if (!found.ok) return found;
714
+ const r = winRun({ action: 'focus', handle: found.window.handle });
715
+ return r.ok ? { ok: true, name: r.name, handle: found.window.handle, matched: found.window.name } : fail(r.error);
506
716
  }
507
717
  if (isMac) {
508
718
  const res = osa(`tell application "System Events" to set frontmost of process ${JSON.stringify(name)} to true`);
@@ -514,7 +724,16 @@ const automation = {
514
724
 
515
725
  resizeWindow(name, x, y, width, height) {
516
726
  if (isWin) {
517
- const r = winRun({ action: 'resize', name, x, y, width, height });
727
+ const found = this.findWindow(name);
728
+ if (!found.ok) return found;
729
+ const r = winRun({
730
+ action: 'resize',
731
+ handle: found.window.handle,
732
+ x,
733
+ y,
734
+ width,
735
+ height,
736
+ });
518
737
  return r.ok ? { ok: true, name: r.name, x, y, width, height } : fail(r.error);
519
738
  }
520
739
  if (isMac) {
@@ -527,6 +746,52 @@ const automation = {
527
746
  return res.ok ? { ok: true, name, x, y, width, height } : fail(res.stderr.trim() || 'resize failed');
528
747
  },
529
748
 
749
+ /** The UI Automation tree of the focused window: what the desktop's windows-use returns. */
750
+ state(handle = null) {
751
+ if (isWin) {
752
+ const r = winRun({ action: 'state', handle }, 60000);
753
+ if (!r.ok) return fail(r.error);
754
+ return {
755
+ ok: true,
756
+ windows: r.windows || [],
757
+ active_window: r.active_window || null,
758
+ interactive_elements: r.interactive_elements || [],
759
+ scrollable_elements: r.scrollable_elements || [],
760
+ interactive_elements_text: r.interactive_elements_text || '',
761
+ scrollable_elements_text: r.scrollable_elements_text || '',
762
+ system: r.system || {},
763
+ metadata: r.metadata || {},
764
+ };
765
+ }
766
+ // no accessibility tree on the other platforms: windows, geometry and pixels
767
+ const wins = this.windows();
768
+ const screen = this.screenSize();
769
+ const active = wins.ok && wins.windows.length ? wins.windows[0] : null;
770
+ const elements = (wins.ok ? wins.windows : []).map((w) => ({
771
+ name: w.name,
772
+ control_type: 'Window',
773
+ kind: 'Window',
774
+ bounding_box: { left: w.x, top: w.y, right: w.x + w.width, bottom: w.y + w.height },
775
+ center: { x: w.x + Math.floor(w.width / 2), y: w.y + Math.floor(w.height / 2) },
776
+ value: '',
777
+ enabled: true,
778
+ automation_id: '',
779
+ }));
780
+ return {
781
+ ok: true,
782
+ windows: wins.ok ? wins.windows : [],
783
+ active_window: active,
784
+ interactive_elements: elements,
785
+ scrollable_elements: [],
786
+ interactive_elements_text: elements
787
+ .map((e, i) => `${i + 1}. ${e.control_type} '${e.name}' at (${e.center.x}, ${e.center.y})`)
788
+ .join('\n'),
789
+ scrollable_elements_text: '',
790
+ system: this.sysinfo().system,
791
+ metadata: { kind: 'windows-only-list', depth: 'top-level', all_desktops: false },
792
+ };
793
+ },
794
+
530
795
  click(x, y, { button = 'left', count = 1 } = {}) {
531
796
  if (isWin) {
532
797
  const r = winRun({ action: 'click', x, y, button, count });
@@ -644,11 +909,13 @@ const automation = {
644
909
  return res.ok ? { ok: true, keys: combo } : fail(res.stderr.trim() || 'xdotool key failed');
645
910
  },
646
911
 
647
- screenshot(dest) {
912
+ screenshot(dest, { annotate = false } = {}) {
648
913
  fs.mkdirSync(path.dirname(dest), { recursive: true });
649
914
  if (isWin) {
650
- const r = winRun({ action: 'shot', path: dest }, 60000);
651
- return r.ok ? { ok: true, file: dest, width: r.width, height: r.height, mime: 'image/png' } : fail(r.error);
915
+ const r = winRun({ action: 'shot', path: dest, annotate }, 90000);
916
+ return r.ok
917
+ ? { ok: true, file: dest, width: r.width, height: r.height, mime: 'image/png', annotated: Boolean(annotate), elements: r.elements || 0 }
918
+ : fail(r.error);
652
919
  }
653
920
  if (isMac) {
654
921
  const res = run('screencapture', ['-x', '-t', 'png', dest], { timeout: 30000 });
@@ -723,4 +990,4 @@ const automation = {
723
990
  },
724
991
  };
725
992
 
726
- module.exports = { ...automation, keyboardSequence };
993
+ module.exports = { ...automation, keyboardSequence, fuzzyScore, pickWindow };
package/src/local.js CHANGED
@@ -531,22 +531,10 @@ async function runUiAction(bridge, taskId, request, opts) {
531
531
  };
532
532
 
533
533
  if (op === 'state') {
534
- const wins = automation.windows();
535
- const screen = automation.screenSize();
536
- const info = automation.sysinfo();
537
- if (!wins.ok) return wins;
538
- return {
539
- ok: true,
540
- windows: wins.windows,
541
- active_window: wins.active_window,
542
- interactive_elements: [],
543
- scrollable_elements: [],
544
- interactive_elements_text: '',
545
- system: info.system,
546
- screen: { width: screen.width, height: screen.height },
547
- // the terminal client reads windows and pixels, not the accessibility tree
548
- note: 'interactive_elements are empty: this client does not read the UI Automation tree',
549
- };
534
+ // the desktop's windows-use returns the UI Automation tree; so do we on Windows
535
+ const state = automation.state();
536
+ if (!state.ok) return state;
537
+ return state;
550
538
  }
551
539
 
552
540
  if (op === 'sysinfo') {
@@ -626,7 +614,7 @@ async function runUiAction(bridge, taskId, request, opts) {
626
614
  const dir = path.join(os.tmpdir(), 'arenacode-shots');
627
615
  fs.mkdirSync(dir, { recursive: true });
628
616
  const file = path.join(dir, `shot-${stamp()}.png`);
629
- const shot = automation.screenshot(file);
617
+ const shot = automation.screenshot(file, { annotate: Boolean(request.annotated) });
630
618
  if (!shot.ok) return shot;
631
619
  const buf = fs.readFileSync(shot.file);
632
620
  // the agent runs in the sandbox: the image travels through the bridge workspace
@@ -639,6 +627,7 @@ async function runUiAction(bridge, taskId, request, opts) {
639
627
  height: shot.height,
640
628
  file: remote || shot.file,
641
629
  local_file: shot.file,
630
+ annotated: Boolean(request.annotated),
642
631
  };
643
632
  if (buf.length <= 2 * 1024 * 1024) out.image = buf.toString('base64');
644
633
  return out;