copilot-liku-cli 0.0.3 → 0.0.8

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.
Files changed (46) hide show
  1. package/QUICKSTART.md +24 -0
  2. package/README.md +85 -33
  3. package/package.json +23 -14
  4. package/scripts/postinstall.js +63 -0
  5. package/src/cli/commands/window.js +66 -0
  6. package/src/main/agents/base-agent.js +15 -7
  7. package/src/main/agents/builder.js +211 -0
  8. package/src/main/agents/index.js +7 -4
  9. package/src/main/agents/orchestrator.js +13 -0
  10. package/src/main/agents/producer.js +891 -0
  11. package/src/main/agents/researcher.js +78 -0
  12. package/src/main/agents/state-manager.js +134 -2
  13. package/src/main/agents/verifier.js +201 -0
  14. package/src/main/ai-service.js +349 -35
  15. package/src/main/index.js +702 -113
  16. package/src/main/inspect-service.js +24 -1
  17. package/src/main/python-bridge.js +395 -0
  18. package/src/main/system-automation.js +876 -131
  19. package/src/main/ui-automation/core/ui-provider.js +99 -0
  20. package/src/main/ui-automation/core/uia-host.js +214 -0
  21. package/src/main/ui-automation/index.js +30 -0
  22. package/src/main/ui-automation/interactions/element-click.js +6 -6
  23. package/src/main/ui-automation/interactions/high-level.js +28 -6
  24. package/src/main/ui-automation/interactions/index.js +21 -0
  25. package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
  26. package/src/main/ui-automation/window/index.js +6 -0
  27. package/src/main/ui-automation/window/manager.js +173 -26
  28. package/src/main/ui-watcher.js +401 -58
  29. package/src/main/visual-awareness.js +18 -1
  30. package/src/native/windows-uia/Program.cs +89 -0
  31. package/src/native/windows-uia/build.ps1 +24 -0
  32. package/src/native/windows-uia-dotnet/Program.cs +920 -0
  33. package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
  34. package/src/native/windows-uia-dotnet/build.ps1 +24 -0
  35. package/src/renderer/chat/chat.js +915 -671
  36. package/src/renderer/chat/index.html +2 -4
  37. package/src/renderer/chat/preload.js +8 -1
  38. package/src/renderer/overlay/overlay.js +157 -8
  39. package/src/renderer/overlay/preload.js +4 -0
  40. package/src/shared/inspect-types.js +82 -6
  41. package/ARCHITECTURE.md +0 -411
  42. package/CONFIGURATION.md +0 -302
  43. package/CONTRIBUTING.md +0 -225
  44. package/ELECTRON_README.md +0 -121
  45. package/PROJECT_STATUS.md +0 -229
  46. package/TESTING.md +0 -274
@@ -0,0 +1,920 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.Linq;
5
+ using System.Runtime.InteropServices;
6
+ using System.Text.Json;
7
+ using System.Threading;
8
+ using System.Timers;
9
+ using System.Windows;
10
+ using System.Windows.Automation;
11
+
12
+ namespace UIAWrapper
13
+ {
14
+ class Program
15
+ {
16
+ [DllImport("user32.dll")]
17
+ static extern IntPtr GetForegroundWindow();
18
+
19
+ static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = false };
20
+
21
+ // ── Thread-safe output (Phase 4) ─────────────────────────────────────
22
+ static readonly object _writeLock = new object();
23
+
24
+ // ── Event subscription state (Phase 4) ──────────────────────────────
25
+ static bool _eventsSubscribed = false;
26
+ static AutomationElement? _subscribedWindow = null;
27
+ static int _subscribedWindowHandle = 0;
28
+ static readonly int MaxWalkElements = 300;
29
+
30
+ // Debounce timers
31
+ static System.Timers.Timer? _structureDebounce = null;
32
+ static System.Timers.Timer? _propertyDebounce = null;
33
+ static readonly List<Dictionary<string, object?>> _pendingPropertyChanges = new();
34
+ static readonly object _propLock = new object();
35
+
36
+ // Adaptive backoff: if >10 structure events in 1s, increase debounce
37
+ static int _structureEventBurst = 0;
38
+ static DateTime _structureBurstWindowStart = DateTime.UtcNow;
39
+ static int _structureDebounceMs = 100;
40
+
41
+ // Event handler references (for removal)
42
+ static AutomationFocusChangedEventHandler? _focusHandler = null;
43
+ static StructureChangedEventHandler? _structureHandler = null;
44
+ static AutomationPropertyChangedEventHandler? _propertyHandler = null;
45
+
46
+ static void Main(string[] args)
47
+ {
48
+ // Legacy one-shot mode: no args → dump foreground tree and exit
49
+ if (!Console.IsInputRedirected && args.Length == 0)
50
+ {
51
+ IntPtr handle = GetForegroundWindow();
52
+ if (handle == IntPtr.Zero) return;
53
+ AutomationElement root = AutomationElement.FromHandle(handle);
54
+ var node = BuildTree(root);
55
+ Console.WriteLine(JsonSerializer.Serialize(node, new JsonSerializerOptions { WriteIndented = true }));
56
+ return;
57
+ }
58
+
59
+ // Persistent command-loop mode (JSONL over stdin/stdout)
60
+ string? line;
61
+ while ((line = Console.ReadLine()) != null)
62
+ {
63
+ if (string.IsNullOrWhiteSpace(line)) continue;
64
+ try
65
+ {
66
+ using var doc = JsonDocument.Parse(line);
67
+ var root = doc.RootElement;
68
+ var cmd = root.GetProperty("cmd").GetString() ?? "";
69
+
70
+ switch (cmd)
71
+ {
72
+ case "getTree":
73
+ HandleGetTree();
74
+ break;
75
+ case "elementFromPoint":
76
+ HandleElementFromPoint(root);
77
+ break;
78
+ case "setValue":
79
+ HandleSetValue(root);
80
+ break;
81
+ case "scroll":
82
+ HandleScroll(root);
83
+ break;
84
+ case "expandCollapse":
85
+ HandleExpandCollapse(root);
86
+ break;
87
+ case "getText":
88
+ HandleGetText(root);
89
+ break;
90
+ case "subscribeEvents":
91
+ HandleSubscribeEvents();
92
+ break;
93
+ case "unsubscribeEvents":
94
+ HandleUnsubscribeEvents();
95
+ break;
96
+ case "exit":
97
+ Reply(new { ok = true, cmd = "exit" });
98
+ return;
99
+ default:
100
+ Reply(new { ok = false, error = $"Unknown command: {cmd}" });
101
+ break;
102
+ }
103
+ }
104
+ catch (Exception ex)
105
+ {
106
+ Reply(new { ok = false, error = ex.Message });
107
+ }
108
+ }
109
+ }
110
+
111
+ static void Reply(object obj)
112
+ {
113
+ lock (_writeLock)
114
+ {
115
+ Console.WriteLine(JsonSerializer.Serialize(obj, JsonOpts));
116
+ Console.Out.Flush();
117
+ }
118
+ }
119
+
120
+ // ── getTree ──────────────────────────────────────────────────────────
121
+ static void HandleGetTree()
122
+ {
123
+ IntPtr handle = GetForegroundWindow();
124
+ if (handle == IntPtr.Zero)
125
+ {
126
+ Reply(new { ok = false, error = "No foreground window" });
127
+ return;
128
+ }
129
+ AutomationElement root = AutomationElement.FromHandle(handle);
130
+ var node = BuildTree(root);
131
+ Reply(new { ok = true, cmd = "getTree", tree = node });
132
+ }
133
+
134
+ // ── elementFromPoint ─────────────────────────────────────────────────
135
+ static void HandleElementFromPoint(JsonElement root)
136
+ {
137
+ double x = root.GetProperty("x").GetDouble();
138
+ double y = root.GetProperty("y").GetDouble();
139
+
140
+ AutomationElement element;
141
+ try
142
+ {
143
+ element = AutomationElement.FromPoint(new Point(x, y));
144
+ }
145
+ catch (Exception ex)
146
+ {
147
+ Reply(new { ok = false, error = $"FromPoint failed: {ex.Message}" });
148
+ return;
149
+ }
150
+
151
+ if (element == null)
152
+ {
153
+ Reply(new { ok = false, error = "No element at point" });
154
+ return;
155
+ }
156
+
157
+ var payload = BuildRichElement(element);
158
+ payload["queryPoint"] = new Dictionary<string, double> { ["x"] = x, ["y"] = y };
159
+ Reply(new { ok = true, cmd = "elementFromPoint", element = payload });
160
+ }
161
+
162
+ // ── Helper: resolve element at x,y ───────────────────────────────────
163
+ static AutomationElement? ResolveElement(JsonElement root, out double x, out double y)
164
+ {
165
+ x = root.GetProperty("x").GetDouble();
166
+ y = root.GetProperty("y").GetDouble();
167
+ return AutomationElement.FromPoint(new Point(x, y));
168
+ }
169
+
170
+ // ── setValue (Phase 3) ───────────────────────────────────────────────
171
+ static void HandleSetValue(JsonElement root)
172
+ {
173
+ try
174
+ {
175
+ var el = ResolveElement(root, out double x, out double y);
176
+ if (el == null) { Reply(new { ok = false, cmd = "setValue", error = "No element at point" }); return; }
177
+
178
+ string value = root.GetProperty("value").GetString() ?? "";
179
+
180
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty))
181
+ {
182
+ var vp = (ValuePattern)el.GetCurrentPattern(ValuePattern.Pattern);
183
+ vp.SetValue(value);
184
+ Reply(new { ok = true, cmd = "setValue", method = "ValuePattern", element = BuildRichElement(el) });
185
+ }
186
+ else
187
+ {
188
+ Reply(new { ok = false, cmd = "setValue", error = "ValuePattern not supported", patterns = GetPatternNames(el) });
189
+ }
190
+ }
191
+ catch (Exception ex) { Reply(new { ok = false, cmd = "setValue", error = ex.Message }); }
192
+ }
193
+
194
+ // ── scroll (Phase 3) ─────────────────────────────────────────────────
195
+ static void HandleScroll(JsonElement root)
196
+ {
197
+ try
198
+ {
199
+ var el = ResolveElement(root, out double x, out double y);
200
+ if (el == null) { Reply(new { ok = false, cmd = "scroll", error = "No element at point" }); return; }
201
+
202
+ string direction = root.TryGetProperty("direction", out var dirProp) ? dirProp.GetString() ?? "down" : "down";
203
+ double amount = root.TryGetProperty("amount", out var amtProp) ? amtProp.GetDouble() : -1;
204
+
205
+ if (!(bool)el.GetCurrentPropertyValue(AutomationElement.IsScrollPatternAvailableProperty))
206
+ {
207
+ Reply(new { ok = false, cmd = "scroll", error = "ScrollPattern not supported", patterns = GetPatternNames(el) });
208
+ return;
209
+ }
210
+
211
+ var sp = (ScrollPattern)el.GetCurrentPattern(ScrollPattern.Pattern);
212
+
213
+ if (amount >= 0)
214
+ {
215
+ // SetScrollPercent mode
216
+ double hPct = sp.Current.HorizontalScrollPercent;
217
+ double vPct = sp.Current.VerticalScrollPercent;
218
+ switch (direction)
219
+ {
220
+ case "left": hPct = Math.Max(0, amount); break;
221
+ case "right": hPct = Math.Min(100, amount); break;
222
+ case "up": vPct = Math.Max(0, amount); break;
223
+ default: vPct = Math.Min(100, amount); break; // down
224
+ }
225
+ sp.SetScrollPercent(hPct, vPct);
226
+ }
227
+ else
228
+ {
229
+ // Scroll by amount (SmallIncrement)
230
+ switch (direction)
231
+ {
232
+ case "up": sp.ScrollVertical(ScrollAmount.SmallDecrement); break;
233
+ case "down": sp.ScrollVertical(ScrollAmount.SmallIncrement); break;
234
+ case "left": sp.ScrollHorizontal(ScrollAmount.SmallDecrement); break;
235
+ case "right": sp.ScrollHorizontal(ScrollAmount.SmallIncrement); break;
236
+ }
237
+ }
238
+
239
+ Reply(new
240
+ {
241
+ ok = true,
242
+ cmd = "scroll",
243
+ method = "ScrollPattern",
244
+ direction,
245
+ scrollInfo = new
246
+ {
247
+ horizontalPercent = sp.Current.HorizontalScrollPercent,
248
+ verticalPercent = sp.Current.VerticalScrollPercent,
249
+ horizontalViewSize = sp.Current.HorizontalViewSize,
250
+ verticalViewSize = sp.Current.VerticalViewSize
251
+ }
252
+ });
253
+ }
254
+ catch (Exception ex) { Reply(new { ok = false, cmd = "scroll", error = ex.Message }); }
255
+ }
256
+
257
+ // ── expandCollapse (Phase 3) ─────────────────────────────────────────
258
+ static void HandleExpandCollapse(JsonElement root)
259
+ {
260
+ try
261
+ {
262
+ var el = ResolveElement(root, out double x, out double y);
263
+ if (el == null) { Reply(new { ok = false, cmd = "expandCollapse", error = "No element at point" }); return; }
264
+
265
+ string action = root.TryGetProperty("action", out var actProp) ? actProp.GetString() ?? "toggle" : "toggle";
266
+
267
+ if (!(bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty))
268
+ {
269
+ Reply(new { ok = false, cmd = "expandCollapse", error = "ExpandCollapsePattern not supported", patterns = GetPatternNames(el) });
270
+ return;
271
+ }
272
+
273
+ var ecp = (ExpandCollapsePattern)el.GetCurrentPattern(ExpandCollapsePattern.Pattern);
274
+ var stateBefore = ecp.Current.ExpandCollapseState.ToString();
275
+
276
+ switch (action)
277
+ {
278
+ case "expand": ecp.Expand(); break;
279
+ case "collapse": ecp.Collapse(); break;
280
+ default: // toggle
281
+ if (ecp.Current.ExpandCollapseState == ExpandCollapseState.Collapsed)
282
+ ecp.Expand();
283
+ else
284
+ ecp.Collapse();
285
+ break;
286
+ }
287
+
288
+ Reply(new
289
+ {
290
+ ok = true,
291
+ cmd = "expandCollapse",
292
+ method = "ExpandCollapsePattern",
293
+ action,
294
+ stateBefore,
295
+ stateAfter = ecp.Current.ExpandCollapseState.ToString()
296
+ });
297
+ }
298
+ catch (Exception ex) { Reply(new { ok = false, cmd = "expandCollapse", error = ex.Message }); }
299
+ }
300
+
301
+ // ── getText (Phase 3) ────────────────────────────────────────────────
302
+ static void HandleGetText(JsonElement root)
303
+ {
304
+ try
305
+ {
306
+ var el = ResolveElement(root, out double x, out double y);
307
+ if (el == null) { Reply(new { ok = false, cmd = "getText", error = "No element at point" }); return; }
308
+
309
+ // Try TextPattern first
310
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTextPatternAvailableProperty))
311
+ {
312
+ var tp = (TextPattern)el.GetCurrentPattern(TextPattern.Pattern);
313
+ string text = tp.DocumentRange.GetText(-1);
314
+ Reply(new { ok = true, cmd = "getText", method = "TextPattern", text, element = BuildRichElement(el) });
315
+ return;
316
+ }
317
+
318
+ // Fallback: try ValuePattern
319
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty))
320
+ {
321
+ var vp = (ValuePattern)el.GetCurrentPattern(ValuePattern.Pattern);
322
+ string text = vp.Current.Value;
323
+ Reply(new { ok = true, cmd = "getText", method = "ValuePattern", text, element = BuildRichElement(el) });
324
+ return;
325
+ }
326
+
327
+ // Fallback: Name property
328
+ string name = el.Current.Name;
329
+ if (!string.IsNullOrEmpty(name))
330
+ {
331
+ Reply(new { ok = true, cmd = "getText", method = "Name", text = name, element = BuildRichElement(el) });
332
+ return;
333
+ }
334
+
335
+ Reply(new { ok = false, cmd = "getText", error = "No text source available", patterns = GetPatternNames(el) });
336
+ }
337
+ catch (Exception ex) { Reply(new { ok = false, cmd = "getText", error = ex.Message }); }
338
+ }
339
+
340
+ // ── Helper: get pattern short names ──────────────────────────────────
341
+
342
+ // ── Phase 4: Event streaming ─────────────────────────────────────────
343
+
344
+ static void HandleSubscribeEvents()
345
+ {
346
+ if (_eventsSubscribed)
347
+ {
348
+ Reply(new { ok = true, cmd = "subscribeEvents", note = "already subscribed" });
349
+ return;
350
+ }
351
+
352
+ _eventsSubscribed = true;
353
+
354
+ // Register system-wide focus changed handler
355
+ _focusHandler = new AutomationFocusChangedEventHandler(OnFocusChanged);
356
+ Automation.AddAutomationFocusChangedEventHandler(_focusHandler);
357
+
358
+ // Set up debounce timers
359
+ _structureDebounce = new System.Timers.Timer(_structureDebounceMs) { AutoReset = false };
360
+ _structureDebounce.Elapsed += OnStructureDebounceElapsed;
361
+
362
+ _propertyDebounce = new System.Timers.Timer(50) { AutoReset = false };
363
+ _propertyDebounce.Elapsed += OnPropertyDebounceElapsed;
364
+
365
+ // Immediately attach to current foreground window
366
+ try
367
+ {
368
+ IntPtr fgHwnd = GetForegroundWindow();
369
+ if (fgHwnd != IntPtr.Zero)
370
+ {
371
+ var win = AutomationElement.FromHandle(fgHwnd);
372
+ AttachToWindow(win);
373
+ }
374
+ }
375
+ catch { /* ignore — will pick up on next focus change */ }
376
+
377
+ // Return initial snapshot
378
+ var initialElements = WalkFocusedWindowElements();
379
+ var activeWindow = GetActiveWindowInfo();
380
+ Reply(new
381
+ {
382
+ ok = true,
383
+ cmd = "subscribeEvents",
384
+ initial = new { activeWindow, elements = initialElements }
385
+ });
386
+ }
387
+
388
+ static void HandleUnsubscribeEvents()
389
+ {
390
+ if (!_eventsSubscribed)
391
+ {
392
+ Reply(new { ok = true, cmd = "unsubscribeEvents", note = "not subscribed" });
393
+ return;
394
+ }
395
+
396
+ DetachFromWindow();
397
+
398
+ if (_focusHandler != null)
399
+ {
400
+ try { Automation.RemoveAutomationFocusChangedEventHandler(_focusHandler); } catch { }
401
+ _focusHandler = null;
402
+ }
403
+
404
+ _structureDebounce?.Stop();
405
+ _structureDebounce?.Dispose();
406
+ _structureDebounce = null;
407
+
408
+ _propertyDebounce?.Stop();
409
+ _propertyDebounce?.Dispose();
410
+ _propertyDebounce = null;
411
+
412
+ lock (_propLock) { _pendingPropertyChanges.Clear(); }
413
+
414
+ _eventsSubscribed = false;
415
+ _structureDebounceMs = 100;
416
+ _structureEventBurst = 0;
417
+
418
+ Reply(new { ok = true, cmd = "unsubscribeEvents" });
419
+ }
420
+
421
+ static void OnFocusChanged(object sender, AutomationFocusChangedEventArgs e)
422
+ {
423
+ if (!_eventsSubscribed) return;
424
+
425
+ try
426
+ {
427
+ var focused = sender as AutomationElement;
428
+ if (focused == null) return;
429
+
430
+ // Walk up to find the top-level window
431
+ var topWindow = FindTopLevelWindow(focused);
432
+ if (topWindow == null) return;
433
+
434
+ int hwnd = topWindow.Current.NativeWindowHandle;
435
+
436
+ // Skip if same window
437
+ if (hwnd == _subscribedWindowHandle && hwnd != 0) return;
438
+
439
+ // Switch windows
440
+ DetachFromWindow();
441
+ AttachToWindow(topWindow);
442
+
443
+ // Emit focus changed event with active window info
444
+ var winInfo = BuildWindowInfo(topWindow);
445
+ Reply(new
446
+ {
447
+ type = "event",
448
+ @event = "focusChanged",
449
+ ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
450
+ data = new { activeWindow = winInfo }
451
+ });
452
+
453
+ // Also trigger a structure snapshot for the new window
454
+ FireStructureDebounce();
455
+ }
456
+ catch (ElementNotAvailableException) { /* element vanished, ignore */ }
457
+ catch { /* defensive */ }
458
+ }
459
+
460
+ static void OnStructureChanged(object sender, StructureChangedEventArgs e)
461
+ {
462
+ if (!_eventsSubscribed) return;
463
+ FireStructureDebounce();
464
+ }
465
+
466
+ static void OnPropertyChanged(object sender, AutomationPropertyChangedEventArgs e)
467
+ {
468
+ if (!_eventsSubscribed) return;
469
+
470
+ try
471
+ {
472
+ var el = sender as AutomationElement;
473
+ if (el == null) return;
474
+
475
+ var light = BuildLightElement(el, _subscribedWindowHandle);
476
+ if (light == null) return;
477
+
478
+ lock (_propLock)
479
+ {
480
+ _pendingPropertyChanges.Add(light);
481
+ }
482
+
483
+ // Reset the 50ms debounce timer
484
+ _propertyDebounce?.Stop();
485
+ _propertyDebounce?.Start();
486
+ }
487
+ catch (ElementNotAvailableException) { /* vanished */ }
488
+ catch { /* defensive */ }
489
+ }
490
+
491
+ static void FireStructureDebounce()
492
+ {
493
+ // Adaptive backoff: track burst rate
494
+ var now = DateTime.UtcNow;
495
+ if ((now - _structureBurstWindowStart).TotalMilliseconds > 1000)
496
+ {
497
+ // New 1-second window
498
+ if (_structureEventBurst > 10)
499
+ {
500
+ // Too many events last second — increase debounce for 5 seconds
501
+ _structureDebounceMs = 200;
502
+ }
503
+ else if (_structureDebounceMs > 100)
504
+ {
505
+ // Cool down back to normal
506
+ _structureDebounceMs = 100;
507
+ }
508
+ _structureEventBurst = 0;
509
+ _structureBurstWindowStart = now;
510
+ }
511
+ _structureEventBurst++;
512
+
513
+ if (_structureDebounce != null)
514
+ {
515
+ _structureDebounce.Interval = _structureDebounceMs;
516
+ _structureDebounce.Stop();
517
+ _structureDebounce.Start();
518
+ }
519
+ }
520
+
521
+ static void OnStructureDebounceElapsed(object? sender, ElapsedEventArgs e)
522
+ {
523
+ if (!_eventsSubscribed) return;
524
+
525
+ try
526
+ {
527
+ var elements = WalkFocusedWindowElements();
528
+ Reply(new
529
+ {
530
+ type = "event",
531
+ @event = "structureChanged",
532
+ ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
533
+ data = new { elements }
534
+ });
535
+ }
536
+ catch (Exception ex)
537
+ {
538
+ // Window may have vanished
539
+ Reply(new
540
+ {
541
+ type = "event",
542
+ @event = "error",
543
+ ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
544
+ data = new { error = ex.Message }
545
+ });
546
+ }
547
+ }
548
+
549
+ static void OnPropertyDebounceElapsed(object? sender, ElapsedEventArgs e)
550
+ {
551
+ if (!_eventsSubscribed) return;
552
+
553
+ List<Dictionary<string, object?>> batch;
554
+ lock (_propLock)
555
+ {
556
+ if (_pendingPropertyChanges.Count == 0) return;
557
+ batch = new List<Dictionary<string, object?>>(_pendingPropertyChanges);
558
+ _pendingPropertyChanges.Clear();
559
+ }
560
+
561
+ // Deduplicate by id (keep latest)
562
+ var deduped = new Dictionary<string, Dictionary<string, object?>>();
563
+ foreach (var el in batch)
564
+ {
565
+ var id = el["id"]?.ToString() ?? "";
566
+ deduped[id] = el; // last wins
567
+ }
568
+
569
+ Reply(new
570
+ {
571
+ type = "event",
572
+ @event = "propertyChanged",
573
+ ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
574
+ data = new { elements = deduped.Values.ToList() }
575
+ });
576
+ }
577
+
578
+ static void AttachToWindow(AutomationElement window)
579
+ {
580
+ _subscribedWindow = window;
581
+ try { _subscribedWindowHandle = window.Current.NativeWindowHandle; } catch { _subscribedWindowHandle = 0; }
582
+
583
+ _structureHandler = new StructureChangedEventHandler(OnStructureChanged);
584
+ _propertyHandler = new AutomationPropertyChangedEventHandler(OnPropertyChanged);
585
+
586
+ try
587
+ {
588
+ Automation.AddStructureChangedEventHandler(
589
+ window, TreeScope.Subtree, _structureHandler);
590
+ }
591
+ catch { /* element may have vanished */ }
592
+
593
+ try
594
+ {
595
+ Automation.AddAutomationPropertyChangedEventHandler(
596
+ window, TreeScope.Subtree, _propertyHandler,
597
+ AutomationElement.BoundingRectangleProperty,
598
+ AutomationElement.NameProperty,
599
+ AutomationElement.IsEnabledProperty,
600
+ AutomationElement.IsOffscreenProperty);
601
+ }
602
+ catch { /* element may have vanished */ }
603
+ }
604
+
605
+ static void DetachFromWindow()
606
+ {
607
+ if (_subscribedWindow == null) return;
608
+
609
+ if (_structureHandler != null)
610
+ {
611
+ try { Automation.RemoveStructureChangedEventHandler(_subscribedWindow, _structureHandler); } catch { }
612
+ _structureHandler = null;
613
+ }
614
+ if (_propertyHandler != null)
615
+ {
616
+ try { Automation.RemoveAutomationPropertyChangedEventHandler(_subscribedWindow, _propertyHandler); } catch { }
617
+ _propertyHandler = null;
618
+ }
619
+
620
+ _subscribedWindow = null;
621
+ _subscribedWindowHandle = 0;
622
+ }
623
+
624
+ static AutomationElement? FindTopLevelWindow(AutomationElement element)
625
+ {
626
+ try
627
+ {
628
+ var walker = TreeWalker.ControlViewWalker;
629
+ var current = element;
630
+ AutomationElement? lastWindow = null;
631
+
632
+ while (current != null && !Automation.Compare(current, AutomationElement.RootElement))
633
+ {
634
+ try
635
+ {
636
+ if (current.Current.ControlType == ControlType.Window)
637
+ lastWindow = current;
638
+ }
639
+ catch (ElementNotAvailableException) { break; }
640
+
641
+ current = walker.GetParent(current);
642
+ }
643
+
644
+ return lastWindow;
645
+ }
646
+ catch { return null; }
647
+ }
648
+
649
+ static Dictionary<string, object?> BuildWindowInfo(AutomationElement window)
650
+ {
651
+ try
652
+ {
653
+ var rect = window.Current.BoundingRectangle;
654
+ return new Dictionary<string, object?>
655
+ {
656
+ ["hwnd"] = window.Current.NativeWindowHandle,
657
+ ["title"] = window.Current.Name,
658
+ ["processId"] = window.Current.ProcessId,
659
+ ["bounds"] = new Dictionary<string, double>
660
+ {
661
+ ["x"] = SafeNumber(rect.X),
662
+ ["y"] = SafeNumber(rect.Y),
663
+ ["width"] = SafeNumber(rect.Width),
664
+ ["height"] = SafeNumber(rect.Height)
665
+ }
666
+ };
667
+ }
668
+ catch
669
+ {
670
+ return new Dictionary<string, object?> { ["hwnd"] = 0, ["title"] = "", ["bounds"] = null };
671
+ }
672
+ }
673
+
674
+ /// <summary>
675
+ /// Walk the focused window tree, returning elements in the same shape
676
+ /// as the PowerShell UIWatcher (id, name, type, automationId, className,
677
+ /// windowHandle, bounds, center, isEnabled).
678
+ /// </summary>
679
+ static List<Dictionary<string, object?>> WalkFocusedWindowElements()
680
+ {
681
+ var results = new List<Dictionary<string, object?>>();
682
+
683
+ AutomationElement? win = _subscribedWindow;
684
+ if (win == null)
685
+ {
686
+ try
687
+ {
688
+ IntPtr fgHwnd = GetForegroundWindow();
689
+ if (fgHwnd != IntPtr.Zero)
690
+ win = AutomationElement.FromHandle(fgHwnd);
691
+ }
692
+ catch { return results; }
693
+ }
694
+ if (win == null) return results;
695
+
696
+ int rootHwnd = 0;
697
+ try { rootHwnd = win.Current.NativeWindowHandle; } catch { }
698
+
699
+ try
700
+ {
701
+ var all = win.FindAll(TreeScope.Descendants, System.Windows.Automation.Condition.TrueCondition);
702
+ int count = 0;
703
+ foreach (AutomationElement el in all)
704
+ {
705
+ if (count >= MaxWalkElements) break;
706
+ var light = BuildLightElement(el, rootHwnd);
707
+ if (light != null) { results.Add(light); count++; }
708
+ }
709
+ }
710
+ catch (ElementNotAvailableException) { /* window vanished */ }
711
+
712
+ return results;
713
+ }
714
+
715
+ /// <summary>
716
+ /// Build a lightweight element matching the PowerShell UIWatcher format exactly.
717
+ /// Returns null for elements with no useful info or zero-size bounds.
718
+ /// </summary>
719
+ static Dictionary<string, object?>? BuildLightElement(AutomationElement el, int rootHwnd)
720
+ {
721
+ try
722
+ {
723
+ var rect = el.Current.BoundingRectangle;
724
+ if (rect.Width <= 0 || rect.Height <= 0) return null;
725
+ if (rect.X < -10000 || rect.Y < -10000) return null;
726
+
727
+ string name = el.Current.Name ?? "";
728
+ name = name.Replace("\r", " ").Replace("\n", " ").Replace("\t", " ");
729
+
730
+ string ctrlType = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", "");
731
+ string autoId = el.Current.AutomationId ?? "";
732
+ autoId = autoId.Replace("\r", " ").Replace("\n", " ").Replace("\t", " ");
733
+
734
+ // Skip elements with no useful identifying info (same filter as PS watcher)
735
+ if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(autoId)) return null;
736
+
737
+ int x = (int)rect.X, y = (int)rect.Y;
738
+ int w = (int)rect.Width, h = (int)rect.Height;
739
+
740
+ return new Dictionary<string, object?>
741
+ {
742
+ ["id"] = $"{ctrlType}|{name}|{autoId}|{x}|{y}",
743
+ ["name"] = name,
744
+ ["type"] = ctrlType,
745
+ ["automationId"] = autoId,
746
+ ["className"] = el.Current.ClassName,
747
+ ["windowHandle"] = rootHwnd,
748
+ ["bounds"] = new Dictionary<string, int> { ["x"] = x, ["y"] = y, ["width"] = w, ["height"] = h },
749
+ ["center"] = new Dictionary<string, int> { ["x"] = x + w / 2, ["y"] = y + h / 2 },
750
+ ["isEnabled"] = el.Current.IsEnabled
751
+ };
752
+ }
753
+ catch (ElementNotAvailableException) { return null; }
754
+ catch { return null; }
755
+ }
756
+
757
+ static Dictionary<string, object?>? GetActiveWindowInfo()
758
+ {
759
+ try
760
+ {
761
+ IntPtr hwnd = GetForegroundWindow();
762
+ if (hwnd == IntPtr.Zero) return null;
763
+ var win = AutomationElement.FromHandle(hwnd);
764
+ return BuildWindowInfo(win);
765
+ }
766
+ catch { return null; }
767
+ }
768
+
769
+ // ── End Phase 4 ─────────────────────────────────────────────────────
770
+ static List<string> GetPatternNames(AutomationElement el)
771
+ {
772
+ var patterns = new List<string>();
773
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) patterns.Add("Invoke");
774
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) patterns.Add("Value");
775
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) patterns.Add("Toggle");
776
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsSelectionItemPatternAvailableProperty)) patterns.Add("SelectionItem");
777
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) patterns.Add("ExpandCollapse");
778
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsScrollPatternAvailableProperty)) patterns.Add("Scroll");
779
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTextPatternAvailableProperty)) patterns.Add("Text");
780
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsWindowPatternAvailableProperty)) patterns.Add("Window");
781
+ return patterns;
782
+ }
783
+
784
+ // ── Rich element payload (Phase 2) ───────────────────────────────────
785
+ static Dictionary<string, object?> BuildRichElement(AutomationElement el)
786
+ {
787
+ var rect = el.Current.BoundingRectangle;
788
+ var result = new Dictionary<string, object?>
789
+ {
790
+ ["name"] = el.Current.Name,
791
+ ["automationId"] = el.Current.AutomationId,
792
+ ["className"] = el.Current.ClassName,
793
+ ["role"] = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""),
794
+ ["bounds"] = new Dictionary<string, double>
795
+ {
796
+ ["x"] = SafeNumber(rect.X),
797
+ ["y"] = SafeNumber(rect.Y),
798
+ ["width"] = SafeNumber(rect.Width),
799
+ ["height"] = SafeNumber(rect.Height)
800
+ },
801
+ ["isEnabled"] = el.Current.IsEnabled,
802
+ ["isOffscreen"] = el.Current.IsOffscreen,
803
+ ["hasKeyboardFocus"] = el.Current.HasKeyboardFocus,
804
+ ["nativeWindowHandle"] = el.Current.NativeWindowHandle
805
+ };
806
+
807
+ // RuntimeId — session-scoped stable identity
808
+ try
809
+ {
810
+ int[] rid = el.GetRuntimeId();
811
+ result["runtimeId"] = rid;
812
+ }
813
+ catch { result["runtimeId"] = null; }
814
+
815
+ // TryGetClickablePoint — preferred click target
816
+ try
817
+ {
818
+ if (el.TryGetClickablePoint(out Point pt))
819
+ {
820
+ result["clickPoint"] = new Dictionary<string, double>
821
+ {
822
+ ["x"] = pt.X,
823
+ ["y"] = pt.Y
824
+ };
825
+ }
826
+ else
827
+ {
828
+ result["clickPoint"] = null;
829
+ }
830
+ }
831
+ catch { result["clickPoint"] = null; }
832
+
833
+ // Value (if available)
834
+ try
835
+ {
836
+ object val = el.GetCurrentPropertyValue(ValuePattern.ValueProperty);
837
+ result["value"] = val?.ToString();
838
+ }
839
+ catch { result["value"] = null; }
840
+
841
+ // Supported patterns (names only — avoids expensive GetSupportedPatterns())
842
+ var patterns = new List<string>();
843
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) patterns.Add("Invoke");
844
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) patterns.Add("Value");
845
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) patterns.Add("Toggle");
846
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsSelectionItemPatternAvailableProperty)) patterns.Add("SelectionItem");
847
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) patterns.Add("ExpandCollapse");
848
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsScrollPatternAvailableProperty)) patterns.Add("Scroll");
849
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTextPatternAvailableProperty)) patterns.Add("Text");
850
+ if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsWindowPatternAvailableProperty)) patterns.Add("Window");
851
+ result["patterns"] = patterns;
852
+
853
+ return result;
854
+ }
855
+
856
+ // ── Tree builder (legacy path, unchanged shape) ──────────────────────
857
+ static UIANode BuildTree(AutomationElement element)
858
+ {
859
+ var rectangle = element.Current.BoundingRectangle;
860
+ var node = new UIANode
861
+ {
862
+ id = element.Current.AutomationId,
863
+ name = element.Current.Name,
864
+ role = element.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""),
865
+ bounds = new Bounds
866
+ {
867
+ x = SafeNumber(rectangle.X),
868
+ y = SafeNumber(rectangle.Y),
869
+ width = SafeNumber(rectangle.Width),
870
+ height = SafeNumber(rectangle.Height)
871
+ },
872
+ isClickable = (bool)element.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty) || element.Current.IsKeyboardFocusable,
873
+ isFocusable = element.Current.IsKeyboardFocusable,
874
+ children = new List<UIANode>()
875
+ };
876
+
877
+ var walker = TreeWalker.ControlViewWalker;
878
+ var child = walker.GetFirstChild(element);
879
+ while (child != null)
880
+ {
881
+ try
882
+ {
883
+ if (!child.Current.IsOffscreen)
884
+ {
885
+ node.children.Add(BuildTree(child));
886
+ }
887
+ }
888
+ catch (ElementNotAvailableException) { }
889
+
890
+ child = walker.GetNextSibling(child);
891
+ }
892
+
893
+ return node;
894
+ }
895
+
896
+ static double SafeNumber(double value)
897
+ {
898
+ return double.IsFinite(value) ? value : 0;
899
+ }
900
+ }
901
+
902
+ class UIANode
903
+ {
904
+ public string id { get; set; } = "";
905
+ public string name { get; set; } = "";
906
+ public string role { get; set; } = "";
907
+ public Bounds bounds { get; set; } = new();
908
+ public bool isClickable { get; set; }
909
+ public bool isFocusable { get; set; }
910
+ public List<UIANode> children { get; set; } = new();
911
+ }
912
+
913
+ class Bounds
914
+ {
915
+ public double x { get; set; }
916
+ public double y { get; set; }
917
+ public double width { get; set; }
918
+ public double height { get; set; }
919
+ }
920
+ }