@ycheung5/pi-computer-use 0.0.0-stage → 0.1.1

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 (95) hide show
  1. package/GLOSSARY.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -2
  4. package/assets/accessibility.cs +208 -0
  5. package/assets/browser-indicators.cs +37 -0
  6. package/assets/capture.cs +215 -0
  7. package/assets/clipboard.cs +197 -0
  8. package/assets/controls.cs +249 -0
  9. package/assets/indicator-preview.png +0 -0
  10. package/assets/indicators.cs +252 -0
  11. package/assets/input-timing.cs +42 -0
  12. package/assets/node-LICENSE.txt +2732 -0
  13. package/assets/ocr.cs +86 -0
  14. package/assets/ownership.cs +59 -0
  15. package/assets/pointer.cs +241 -0
  16. package/assets/puppeteer-LICENSE.txt +202 -0
  17. package/assets/readme-banner.svg +33 -0
  18. package/assets/stop.cs +197 -0
  19. package/assets/text.cs +279 -0
  20. package/assets/wgc.cs +167 -0
  21. package/assets/window-management.cs +217 -0
  22. package/assets/window-marker.cs +23 -0
  23. package/assets/window-references.cs +80 -0
  24. package/assets/windows.cs +127 -0
  25. package/dist/accessibility.d.ts +29 -0
  26. package/dist/accessibility.js +258 -0
  27. package/dist/browser-actions.d.ts +22 -0
  28. package/dist/browser-actions.js +246 -0
  29. package/dist/browser-indicators.d.ts +23 -0
  30. package/dist/browser-indicators.js +69 -0
  31. package/dist/browser-profiles.d.ts +10 -0
  32. package/dist/browser-profiles.js +174 -0
  33. package/dist/browser.d.ts +11 -0
  34. package/dist/browser.js +2071 -0
  35. package/dist/capture.d.ts +74 -0
  36. package/dist/capture.js +199 -0
  37. package/dist/client.d.ts +39 -0
  38. package/dist/client.js +384 -0
  39. package/dist/desktop.d.ts +20 -0
  40. package/dist/desktop.js +42 -0
  41. package/dist/extension.d.ts +4 -0
  42. package/dist/extension.js +409 -0
  43. package/dist/input-timing.d.ts +24 -0
  44. package/dist/input-timing.js +70 -0
  45. package/dist/launcher.d.ts +6 -0
  46. package/dist/launcher.js +85 -0
  47. package/dist/native.d.ts +11 -0
  48. package/dist/native.js +146 -0
  49. package/dist/ocr.d.ts +52 -0
  50. package/dist/ocr.js +246 -0
  51. package/dist/ownership.d.ts +4 -0
  52. package/dist/ownership.js +24 -0
  53. package/dist/pointer.d.ts +128 -0
  54. package/dist/pointer.js +198 -0
  55. package/dist/protocol.d.ts +15 -0
  56. package/dist/protocol.js +1 -0
  57. package/dist/runtime.d.ts +8 -0
  58. package/dist/runtime.js +72 -0
  59. package/dist/settings.d.ts +85 -0
  60. package/dist/settings.js +449 -0
  61. package/dist/stop.d.ts +24 -0
  62. package/dist/stop.js +156 -0
  63. package/dist/text.d.ts +120 -0
  64. package/dist/text.js +212 -0
  65. package/dist/waits.d.ts +19 -0
  66. package/dist/waits.js +262 -0
  67. package/dist/window-management.d.ts +25 -0
  68. package/dist/window-management.js +167 -0
  69. package/dist/worker.d.ts +1 -0
  70. package/dist/worker.js +553 -0
  71. package/docs/README.md +55 -0
  72. package/docs/adr/0001-preserve-reference-behavior.md +9 -0
  73. package/docs/adr/0002-window-lifetime-references.md +11 -0
  74. package/docs/adr/0003-exclusive-desktop-control.md +11 -0
  75. package/docs/adr/0004-pi-first-acceptance.md +9 -0
  76. package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
  77. package/docs/agents/domain.md +9 -0
  78. package/docs/agents/issue-tracker.md +10 -0
  79. package/docs/agents/triage-labels.md +9 -0
  80. package/docs/browser.md +113 -0
  81. package/docs/implementation-plan.md +153 -0
  82. package/docs/indicators.md +53 -0
  83. package/docs/native-interop-research.md +17 -0
  84. package/docs/ownership.md +25 -0
  85. package/docs/pi-extension-research.md +57 -0
  86. package/docs/pointer.md +77 -0
  87. package/docs/runtime.md +42 -0
  88. package/docs/settings.md +33 -0
  89. package/docs/spec.md +230 -0
  90. package/docs/stopping.md +83 -0
  91. package/docs/text.md +99 -0
  92. package/docs/tickets.md +85 -0
  93. package/docs/validation.md +1420 -0
  94. package/docs/windows.md +171 -0
  95. package/package.json +134 -4
package/GLOSSARY.md ADDED
@@ -0,0 +1,38 @@
1
+ # Pi Computer Use
2
+
3
+ Computer use lets a Pi agent observe and operate Windows desktop applications and web pages.
4
+
5
+ ## Language
6
+
7
+ **Native Windows host**:
8
+ An environment where Pi runs directly on Windows and controls that Windows desktop.
9
+
10
+ **WSL2 host**:
11
+ An environment where Pi runs inside Windows Subsystem for Linux 2 and controls the associated Windows desktop.
12
+
13
+ **Window lifetime**:
14
+ The period from a native window's creation until its destruction. A replacement window has a different lifetime even when the operating system reuses its numeric handle.
15
+
16
+ **Window reference**:
17
+ An observed identity bound to one window lifetime. A reference to an ended lifetime does not identify its replacement.
18
+
19
+ **Feature parity**:
20
+ A comparison of the capabilities and observable behavior of two systems, as used in the historical DSH comparison records.
21
+
22
+ **Capability coverage**:
23
+ The set of agreed Pi desktop, browser, settings, indicator and stopping behaviors exercised by named checks, including supported cases and explicit limitations.
24
+
25
+ **Desktop ownership**:
26
+ Exclusive permission for one Pi session to operate a Windows desktop during a computer-use operation, shared across native Windows and WSL2 hosts.
27
+
28
+ **Exclusive control**:
29
+ A period in which the agent operates the desktop and ordinary physical keyboard and mouse input is suppressed, while emergency stopping remains available.
30
+
31
+ **Human handback**:
32
+ A pause in automation during which exclusive control has ended and the user can interact normally. Resuming requires an explicit user continuation.
33
+
34
+ **Computer-use operation**:
35
+ A session's related computer-use activity across model turns until processing settles or the operation is stopped. Individual tool calls are steps within the operation.
36
+
37
+ **Emergency stop**:
38
+ A user request to stop active input and reject further actions in the stopped operation; it cannot undo actions already delivered to an application.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pi Computer Use contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,166 @@
1
- # Temporary Holding Version
1
+ # Pi Computer Use
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ![Pi Computer Use: give Pi a Windows desktop, from native Windows or WSL2.](assets/readme-banner.svg)
4
+
5
+ [![Checks](https://github.com/ycheung5/pi-computer-use/actions/workflows/checks.yml/badge.svg)](https://github.com/ycheung5/pi-computer-use/actions/workflows/checks.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ ![Windows 11 + WSL2](https://img.shields.io/badge/Windows_11_%2B_WSL2-0078D4)
8
+
9
+ **Let your Pi agent work with Windows apps and browser pages.** Read controls, take screenshots, enter text and complete tasks from native Windows or WSL2.
10
+
11
+ A standalone Pi extension with **25 computer-use tools**, a blue control border and explicit ways to stop or take back control. It uses your existing Pi model setup.
12
+
13
+ [Install](#install) · [Example tasks](#try-it) · [Capabilities](#what-pi-can-do) · [Documentation](docs/README.md)
14
+
15
+ > **Tested baseline:** Windows 11 x64, WSL2 Ubuntu and Pi SDK/CLI 1.4.0. See the [release status and known limits](docs/validation.md#first-release-preparation) for the exact accepted scope.
16
+
17
+ ## What Pi can do
18
+
19
+ | Capability | What it enables |
20
+ | --- | --- |
21
+ | Windows apps | Discover windows and controls, inspect their state, invoke buttons and manage windows. |
22
+ | Browser pages | Work with forms, tabs, navigation, dialogs and file transfers through a browser protocol channel. |
23
+ | Visual tasks | Capture screenshots and read visible text with Windows OCR. |
24
+ | Desktop input | Move, click, drag, scroll, type Unicode text and send keyboard shortcuts. |
25
+ | Clipboard workflows | Paste text with preservation and cleanup checks; refuse unsupported preservation cases. |
26
+ | Control and preferences | Show a blue border during exclusive control, stop active work and choose which tools Pi loads. |
27
+
28
+ For ordinary app requests, Pi is guided to use Windows defaults and reuse an existing window with a separate tab or workspace when the app supports one. Dedicated browser automation uses a separate profile when that workspace is explicitly selected. [Workspace policy](docs/windows.md#default-apps-and-workspaces)
29
+
30
+ ## Install
31
+
32
+ Use your existing Pi installation. Another computer-use extension is not required.
33
+
34
+ ### From npm
35
+
36
+ Install [@ycheung5/pi-computer-use](https://www.npmjs.com/package/@ycheung5/pi-computer-use) from npm:
37
+
38
+ ```sh
39
+ pi install npm:@ycheung5/pi-computer-use
40
+ ```
41
+
42
+ Use the scoped name above. The unscoped `pi-computer-use` package belongs to a different publisher.
43
+
44
+ ### Build and try this checkout
45
+
46
+ With Node 22.19+ and npm:
47
+
48
+ ```sh
49
+ git clone https://github.com/ycheung5/pi-computer-use.git
50
+ cd pi-computer-use
51
+ npm ci --ignore-scripts
52
+ npm run build
53
+ pi -e .
54
+ ```
55
+
56
+ To retain the built checkout as a local Pi package, run `pi install .` from that directory. Source checkouts do not include compiled output; build before registering the package.
57
+
58
+ ### Install a built archive
59
+
60
+ For a prebuilt `ycheung5-pi-computer-use-0.1.1.tgz` produced by `npm pack`:
61
+
62
+ ```sh
63
+ mkdir pi-computer-use
64
+ tar -xzf ycheung5-pi-computer-use-0.1.1.tgz -C pi-computer-use --strip-components=1
65
+ cd pi-computer-use
66
+ npm install --omit=dev --omit=peer --ignore-scripts
67
+ pi install .
68
+ ```
69
+
70
+ Pi supplies its SDK and TypeBox peers. The archive contains compiled code, so this path needs no TypeScript compiler. Keep the extracted directory while Pi references it.
71
+
72
+ ### Requirements
73
+
74
+ - **Windows 11 x64**, directly or through **WSL2 Ubuntu**, with an interactive desktop. WSL needs Windows executable interop, `cmd.exe` and `wslpath`.
75
+ - **Pi** with your configured model. Tested SDK/CLI: **1.4.0**. A vision-capable model is needed to interpret screenshots; credentials are not bundled.
76
+ - **Windows PowerShell 5.1** and the built-in .NET Framework compiler/desktop assemblies. A separate .NET SDK or Windows Node installation is not required.
77
+ - **Chrome or Edge** for supported browser workflows. Explicitly selected installed Firefox-family browsers are experimental.
78
+ - **Windows OCR language support** for the text you want to recognize.
79
+
80
+ The first computer-use call downloads a pinned, checksum-verified private Windows Node runtime from nodejs.org into `%LOCALAPPDATA%\pi-computer-use\runtime`. Later calls reuse the worker. Initial setup needs HTTPS access to nodejs.org and does not change your global Node installation.
81
+
82
+ ## Try it
83
+
84
+ Start with a read-only connection check:
85
+
86
+ ```text
87
+ Call windows_computer_use_health, then list the open windows.
88
+ Do not activate a window or send any input.
89
+ ```
90
+
91
+ Then give Pi a goal. These are example requests, not promises of support for every app:
92
+
93
+ ```text
94
+ Use Notepad to create a separate test document. Keep any existing documents
95
+ unchanged. Type "Hello from Pi — 中文 café 😀" and verify the editor text.
96
+ Leave the test document open and unsaved.
97
+ ```
98
+
99
+ ```text
100
+ Open https://example.com in my Windows default browser. Reuse an existing
101
+ window with a new tab if possible, then tell me the page title.
102
+ ```
103
+
104
+ ```text
105
+ Find my Downloads window and summarize its visible controls.
106
+ Do not click, type or activate it.
107
+ ```
108
+
109
+ ## Stay in control
110
+
111
+ A **blue gradient border** marks exclusive desktop control. During that period, Windows hooks suppress ordinary physical input so it does not compete with the agent.
112
+
113
+ - Press **Esc** when physical-input protection is armed, or use **`/computer-stop`** in Pi, to interrupt active work.
114
+ - When a task needs your input, the agent should release control and wait for your explicit continuation.
115
+ - Timed-out or cancelled input is not automatically replayed. Unconfirmed cleanup blocks dependent automation.
116
+
117
+ Windows can silently remove timed-out hooks. Suppression and physical stop keys cannot be guaranteed after that OS behavior; detected protection loss stops automation. Read the [control and stopping contract](docs/stopping.md) before relying on unattended desktop work.
118
+
119
+ ## Make it yours
120
+
121
+ ```text
122
+ /computer-settings show
123
+ /computer-settings edit
124
+ /computer-settings set {"toolPreset":"lazy"}
125
+ ```
126
+
127
+ Settings are shared across projects and native Windows/WSL2 Pi in `%LOCALAPPDATA%\pi-computer-use\settings.json`. Choose full, lite, lazy or custom tool loading, plus browser and indicator preferences. The local border remains required during exclusive control. [Settings guide](docs/settings.md)
128
+
129
+ ## Tested scope and known limits
130
+
131
+ The input-timing update passed **214 portable tests**, with one opt-in historical-reference skip, plus native compilation and built-distribution checks. Named native/browser agent workflows and human control/display checks have separate [acceptance receipts](docs/validation.md#capability-coverage). CI is not a claim that every application or desktop state works.
132
+
133
+ - Second-computer WSL installation and desktop actions were user-confirmed. Fresh native-Windows installation, broken-prerequisite handling and shutdown in that clean environment remain accepted, unverified first-release test limitations.
134
+ - Display mapping was exercised at **100%, 150% and 175%**, including a changed negative-origin layout. Other configurations, including 200%, remain unverified.
135
+ - Windows may refuse foreground activation or access to elevated/protected interfaces. The extension reports refusal rather than bypassing Windows protections.
136
+ - Explicit Unicode SendInput defaults to a **50 ms per-unit delay**; explicit native pacing and per-call deadlines are [configurable](docs/text.md#sendinput-pacing-and-deadlines). Clipboard preservation has format-dependent limits; installed Firefox support is experimental.
137
+ - Arm64, genuine same-HWND/PID reuse, verified UIPI denial and additional recorder/remote/capture variants remain unverified.
138
+
139
+ See the [release scope](docs/validation.md#first-release-preparation) for the accepted limits and their evidence.
140
+
141
+ ## Develop and verify
142
+
143
+ ```sh
144
+ npm run check
145
+ npm run test:package
146
+ ```
147
+
148
+ Build with `npm run build`; individual checks are `npm test`, `npm run typecheck` and `npm run lint`. Format with `npm run format` or apply configured fixes with `npm run lint:fix`. Packaging verifies compiled/native assets, licenses, metadata and documentation links.
149
+
150
+ Live Windows and paid-model runners are opt-in. Read their [prerequisites and evidence scopes](docs/validation.md#reproduce-automated-checks) before running them; serialize live WSL checks with `flock -w 120 /tmp/pi-cu-live-tests.lock`. Historical DSH comparisons are optional developer utilities, not release prerequisites.
151
+
152
+ [Documentation index](docs/README.md) · [Requirements](docs/spec.md) · [Implementation plan](docs/implementation-plan.md) · [Report a bug](https://github.com/ycheung5/pi-computer-use/issues)
153
+
154
+ ## Troubleshooting
155
+
156
+ | Symptom | Check |
157
+ | --- | --- |
158
+ | `WORKER_ASSET_MISSING` | Build the checkout or use a built package. |
159
+ | Runtime download or checksum failure | Allow nodejs.org HTTPS. Remove only the indicated corrupt private-runtime cache before retrying on a trusted connection. |
160
+ | `WSL_BOOTSTRAP_FAILED` | Check Windows interop, `cmd.exe` and `wslpath`. A sandbox `UtilBindVsockAnyPort` failure is an execution restriction. |
161
+ | Native compiler or OCR unavailable | Check Windows Framework/desktop components and installed OCR languages. |
162
+ | Desktop busy or stopped | Finish or stop the owning operation, then provide new user input. Do not replay uncertain actions through another tool. |
163
+
164
+ ## License
165
+
166
+ [MIT](LICENSE). Created by **ycheung5**. Bundled notices: [Node](assets/node-LICENSE.txt) and [Puppeteer](assets/puppeteer-LICENSE.txt). Installed dependencies retain their own licenses.
@@ -0,0 +1,208 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Diagnostics;
4
+ using System.Runtime.InteropServices;
5
+ using System.Text;
6
+ using System.Threading;
7
+ using System.Windows;
8
+ using System.Windows.Automation;
9
+
10
+ partial class WindowsBridge {
11
+ [DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr parent, EnumWindow callback, IntPtr parameter);
12
+ [DllImport("user32.dll")] static extern IntPtr GetAncestor(IntPtr handle, uint flags);
13
+ [DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr handle);
14
+ sealed class ObservedElement {
15
+ public AutomationElement Element;
16
+ public IntPtr Window;
17
+ public uint Owner;
18
+ public DateTime Seen;
19
+ public string Runtime,Reference;
20
+ }
21
+ static readonly Dictionary<string, ObservedElement> observed = new Dictionary<string, ObservedElement>();
22
+ static readonly Dictionary<string,string> elementKeys=new Dictionary<string,string>();
23
+ static bool Option(Dictionary<string,object> args, string key, bool fallback) { return args.ContainsKey(key) ? Convert.ToBoolean(args[key]) : fallback; }
24
+ static string TextOption(Dictionary<string,object> args, string key, string fallback) { return args.ContainsKey(key) ? Convert.ToString(args[key]) : fallback; }
25
+ static int NumberOption(Dictionary<string,object> args, string key, int fallback) { return args.ContainsKey(key) ? Convert.ToInt32(args[key]) : fallback; }
26
+ static string WindowTitle(IntPtr handle) { StringBuilder value=new StringBuilder(2048); GetWindowText(handle,value,value.Capacity); return value.ToString(); }
27
+ static uint WindowOwner(IntPtr handle) { uint owner; GetWindowThreadProcessId(handle,out owner); return owner; }
28
+ static IntPtr ResolveObservationWindow(Dictionary<string,object> args) {
29
+ WindowReference bound=ResolveWindowReference(args);
30
+ long explicitHandle=bound!=null?bound.Window.ToInt64():args.ContainsKey("nativeWindowHandle") ? Convert.ToInt64(args["nativeWindowHandle"]) : 0;
31
+ uint owner=args.ContainsKey("processId") ? Convert.ToUInt32(args["processId"]) : 0;
32
+ string title=TextOption(args,"windowTitle","");
33
+ IntPtr handle=IntPtr.Zero;
34
+ if(explicitHandle!=0) {
35
+ handle=new IntPtr(explicitHandle);
36
+ if(!IsWindow(handle) || (owner!=0 && WindowOwner(handle)!=owner) || (title.Length>0 && WindowTitle(handle).IndexOf(title,StringComparison.OrdinalIgnoreCase)<0)) throw new InvalidOperationException("TARGET_IDENTITY_CHANGED: window no longer matches the requested owner/target; observe again");
37
+ } else if(owner!=0 || title.Length>0) {
38
+ List<IntPtr> candidates=new List<IntPtr>();
39
+ EnumWindows(delegate(IntPtr candidate,IntPtr ignored) {
40
+ if(!IsWindowVisible(candidate)) return true;
41
+ if(owner!=0 && WindowOwner(candidate)!=owner) return true;
42
+ if(title.Length>0 && WindowTitle(candidate).IndexOf(title,StringComparison.OrdinalIgnoreCase)<0) return true;
43
+ candidates.Add(candidate); return true;
44
+ },IntPtr.Zero);
45
+ if(candidates.Count==0) throw new InvalidOperationException("TARGET_NOT_FOUND: no window matches the requested target");
46
+ if(candidates.Count>1) throw new InvalidOperationException("AMBIGUOUS_TARGET: multiple windows match; specify nativeWindowHandle");
47
+ handle=candidates[0];
48
+ } else handle=GetForegroundWindow();
49
+ if(handle==IntPtr.Zero || !IsWindow(handle)) throw new InvalidOperationException("TARGET_NOT_FOUND: no usable foreground window");
50
+ CheckPointerTitle(args,handle);
51
+ UIntPtr ignoredResult;
52
+ bool responsive=SendMessageTimeout(handle,0,UIntPtr.Zero,IntPtr.Zero,3,100,out ignoredResult)!=IntPtr.Zero;
53
+ if(!responsive) throw new InvalidOperationException(Marshal.GetLastWin32Error()==5 ? "TARGET_ACCESS_DENIED: Windows refused access" : "TARGET_UNRESPONSIVE: avoid repeated UIA reads; use native discovery/capture");
54
+ if(Option(args,"activate",false) && GetForegroundWindow()!=handle) {
55
+ uint targetOwner=WindowOwner(handle);Dictionary<string,object> diagnostic=new Dictionary<string,object>();
56
+ CheckWindowReference(handle);
57
+ if(!ManagementActivate(handle,targetOwner,diagnostic)) {diagnostic["foregroundAtRefusal"]=ManagementForegroundRelationship(GetForegroundWindow(),handle,targetOwner);throw new ActivationRefusedException(diagnostic);}
58
+ }
59
+ CheckWindowReference(handle);return handle;
60
+ }
61
+ static Dictionary<string,object> UiaBox(System.Windows.Rect bounds) {
62
+ return new Dictionary<string,object> { {"x",bounds.X},{"y",bounds.Y},{"width",bounds.Width},{"height",bounds.Height},{"centerX",bounds.X+bounds.Width/2},{"centerY",bounds.Y+bounds.Height/2} };
63
+ }
64
+ static string RuntimeId(AutomationElement element) { return "uia:rt:"+String.Join("-",element.GetRuntimeId()); }
65
+ static void PruneElements() {
66
+ List<string> remove=new List<string>(); DateTime cutoff=DateTime.UtcNow.AddMinutes(-5);
67
+ foreach(KeyValuePair<string,ObservedElement> item in observed) if(item.Value.Seen<cutoff || !IsWindow(item.Value.Window) || WindowOwner(item.Value.Window)!=item.Value.Owner) remove.Add(item.Key);
68
+ foreach(string key in remove) {ObservedElement item=observed[key];elementKeys.Remove(item.Reference+":"+item.Runtime);observed.Remove(key);}
69
+ if(observed.Count>20000) {observed.Clear();elementKeys.Clear();}
70
+ }
71
+ static IntPtr ElementWindow(AutomationElement element,IntPtr window) {
72
+ if(window!=IntPtr.Zero)return window;
73
+ IntPtr own=new IntPtr(element.Current.NativeWindowHandle);
74
+ if(own!=IntPtr.Zero)return GetAncestor(own,2);
75
+ AutomationElement parent=element;
76
+ for(int level=0;level<128 && parent!=null;level++){int hwnd=parent.Current.NativeWindowHandle;if(hwnd!=0)return GetAncestor(new IntPtr(hwnd),2);parent=TreeWalker.RawViewWalker.GetParent(parent);}
77
+ return IntPtr.Zero;
78
+ }
79
+ static Dictionary<string,object> DescribeElement(AutomationElement element, IntPtr window, bool full, int depth, bool bind=true) {
80
+ AutomationElement.AutomationElementInformation current=element.Current;
81
+ string runtime=RuntimeId(element),id=null;window=ElementWindow(element,window);
82
+ var bindingMetadata=new Dictionary<string,object>();WindowReference binding=bind?ObserveWindowReference(window,bindingMetadata):null;
83
+ if(binding!=null) {
84
+ string key=binding.Id+":"+runtime;ObservedElement existing;
85
+ if(elementKeys.TryGetValue(key,out id) && observed.TryGetValue(id,out existing))existing.Seen=DateTime.UtcNow;
86
+ else {id="u:"+referenceEpoch+":"+(++elementSequence).ToString("x");elementKeys[key]=id;observed[id]=new ObservedElement {Element=element,Window=window,Owner=binding.Owner,Seen=DateTime.UtcNow,Runtime=runtime,Reference=binding.Id};}
87
+ }
88
+ ControlType controlType=current.ControlType;
89
+ bool unknownType=controlType==null || String.IsNullOrEmpty(controlType.ProgrammaticName);
90
+ Dictionary<string,object> info=new Dictionary<string,object> { {"name",current.Name ?? ""},{"controlType",unknownType ? "Unknown" : controlType.ProgrammaticName.Replace("ControlType.","")},{"depth",depth},{"isEnabled",current.IsEnabled},{"isOffscreen",current.IsOffscreen},{"hasKeyboardFocus",current.HasKeyboardFocus},{"isPassword",current.IsPassword} };
91
+ if(id!=null)info["id"]=id;
92
+ if(binding==null && bindingMetadata.ContainsKey("windowRefUnavailable"))info["windowRefUnavailable"]=bindingMetadata["windowRefUnavailable"];
93
+ if(!current.BoundingRectangle.IsEmpty) info["boundingBox"]=UiaBox(current.BoundingRectangle);
94
+ if(unknownType) info["controlTypeUnavailable"]=true;
95
+ if(!String.IsNullOrEmpty(current.AutomationId)) info["automationId"]=current.AutomationId;
96
+ if(!String.IsNullOrEmpty(current.ClassName)) info["className"]=current.ClassName;
97
+ if(full) {
98
+ info["processId"]=current.ProcessId; info["nativeWindowHandle"]=current.NativeWindowHandle; info["runtimeId"]=element.GetRuntimeId(); info["localizedControlType"]=current.LocalizedControlType;
99
+ List<string> patterns=new List<string>(); foreach(AutomationPattern pattern in element.GetSupportedPatterns()) patterns.Add(pattern.ProgrammaticName.Replace("PatternIdentifiers.Pattern","")); info["patterns"]=patterns;
100
+ object value;
101
+ if(!current.IsPassword && element.TryGetCurrentPattern(ValuePattern.Pattern,out value)) { string text=((ValuePattern)value).Current.Value; if(!String.IsNullOrEmpty(text)) info["value"]=text.Length>2000 ? text.Substring(0,2000) : text; }
102
+ }
103
+ return info;
104
+ }
105
+ static AutomationElement ResolveObservedElement(string id, Dictionary<string,object> args, out IntPtr window) {
106
+ ObservedElement entry;
107
+ if(id.StartsWith("uia:hwnd:",StringComparison.Ordinal)) {
108
+ string[] parts=id.Split(':'); long handle; uint pid;
109
+ if(parts.Length!=5 || parts[3]!="pid" || !Int64.TryParse(parts[2],out handle) || !UInt32.TryParse(parts[4],out pid)) throw new InvalidOperationException("INVALID_ELEMENT_ID");
110
+ window=new IntPtr(handle);
111
+ if(!IsWindow(window) || WindowOwner(window)!=pid) throw new InvalidOperationException("STALE_ELEMENT: native window identity changed");
112
+ ValidateElementTarget(window,args); return AutomationElement.FromHandle(window);
113
+ }
114
+ if(!observed.TryGetValue(id,out entry) || entry.Seen<DateTime.UtcNow.AddMinutes(-5)) throw new InvalidOperationException("STALE_ELEMENT: element was not observed or has expired; observe again");
115
+ window=entry.Window;
116
+ if(!IsWindow(window) || WindowOwner(window)!=entry.Owner) { observed.Remove(id); throw new InvalidOperationException("STALE_ELEMENT: owning window identity changed"); }
117
+ WindowReference root=ResolveWindowReference(new Dictionary<string,object>{{"windowRef",entry.Reference}});
118
+ ValidateElementTarget(window,args);
119
+ try { if(RuntimeId(entry.Element)!=entry.Runtime) throw new InvalidOperationException("STALE_ELEMENT: runtime identity changed"); entry.Element.GetCurrentPropertyValue(AutomationElement.ProcessIdProperty); return entry.Element; }
120
+ catch(ElementNotAvailableException) { observed.Remove(id); throw new InvalidOperationException("STALE_ELEMENT: control no longer exists; observe again"); }
121
+ }
122
+ static void ValidateElementTarget(IntPtr window, Dictionary<string,object> args) {
123
+ if(args.ContainsKey("windowRef") || args.ContainsKey("nativeWindowHandle") || args.ContainsKey("processId") || args.ContainsKey("windowTitle")) if(ResolveObservationWindow(args)!=window) throw new InvalidOperationException("ELEMENT_WRONG_OWNER: element belongs to a different target window; observe the target window and element again; do not automatically replay the action");
124
+ }
125
+ sealed class Traversal {
126
+ public Stopwatch Watch=Stopwatch.StartNew(); public int Budget,Maximum,Depth,Count,UnknownControlTypes; public string Reason; public string Error; public bool Offscreen,Full,BrowserLayout; public TreeWalker Walker;
127
+ public bool CanRead() { if(Watch.ElapsedMilliseconds>=Budget) { if(Reason==null) Reason="treeTimeoutMs"; return false; } if(Count>=Maximum) { if(Reason==null) Reason="maxNodes"; return false; } return true; }
128
+ }
129
+ static Dictionary<string,object> ReadTree(AutomationElement element,IntPtr window,int depth,int physicalDepth,Traversal state) {
130
+ if(!state.CanRead()) return null;
131
+ if(physicalDepth>128) { state.Reason="providerDepth"; return null; }
132
+ Dictionary<string,object> node;
133
+ try { node=DescribeElement(element,window,state.Full,depth); }
134
+ catch(ElementNotAvailableException) { state.Reason="providerError"; state.Error="An element disappeared during observation"; return null; }
135
+ state.Count++;
136
+ if(node.ContainsKey("controlTypeUnavailable")) state.UnknownControlTypes++;
137
+ List<object> children=new List<object>();
138
+ AutomationElement child=state.Walker.GetFirstChild(element);
139
+ while(child!=null && state.CanRead()) {
140
+ bool transparent=state.BrowserLayout && child.Current.ControlType==ControlType.Pane && String.IsNullOrEmpty(child.Current.Name);
141
+ int childDepth=depth+(transparent?0:1);
142
+ if(childDepth>state.Depth) { if(state.Reason==null) state.Reason="maxDepth"; }
143
+ else { Dictionary<string,object> item=ReadTree(child,window,childDepth,physicalDepth+1,state); if(item!=null) children.Add(item); }
144
+ if(state.Reason=="maxNodes" || state.Reason=="treeTimeoutMs") break;
145
+ child=state.Walker.GetNextSibling(child);
146
+ }
147
+ if(children.Count>0) node["children"]=children;
148
+ return node;
149
+ }
150
+ static TreeWalker ObservationWalker(string view,bool includeOffscreen) {
151
+ Condition condition=view=="raw"?Automation.RawViewCondition:view=="content"?Automation.ContentViewCondition:Automation.ControlViewCondition;
152
+ // Filter the projected view, rather than prune visible descendants of an offscreen ancestor.
153
+ if(!includeOffscreen)condition=new AndCondition(condition,new PropertyCondition(AutomationElement.IsOffscreenProperty,false));
154
+ return new TreeWalker(condition);
155
+ }
156
+ static Dictionary<string,object> WarmBrowser(IntPtr window,Traversal state) {
157
+ List<IntPtr> renderers=new List<IntPtr>();
158
+ EnumChildWindows(window,delegate(IntPtr child,IntPtr ignored) { StringBuilder name=new StringBuilder(256); GetClassName(child,name,name.Capacity); if(name.ToString()=="Chrome_RenderWidgetHostHWND") renderers.Add(child); return true; },IntPtr.Zero);
159
+ if(renderers.Count==0) return null;
160
+ int ready=0; long start=state.Watch.ElapsedMilliseconds; long budget=Math.Min(1500,state.Budget/2);
161
+ do {
162
+ ready=0;
163
+ foreach(IntPtr renderer in renderers) {
164
+ if(state.Watch.ElapsedMilliseconds>=state.Budget) break;
165
+ try { AutomationElement root=TreeWalker.RawViewWalker.GetFirstChild(AutomationElement.FromHandle(renderer)); if(root!=null) { ControlType type=root.Current.ControlType; if(TreeWalker.RawViewWalker.GetFirstChild(root)!=null) ready++; } } catch(ElementNotAvailableException) {} catch(InvalidOperationException) {}
166
+ }
167
+ if(ready==renderers.Count || state.Watch.ElapsedMilliseconds-start>=budget) break;
168
+ Thread.Sleep(50);
169
+ } while(true);
170
+ return new Dictionary<string,object>{{"renderers",renderers.Count},{"ready",ready},{"waitedMs",state.Watch.ElapsedMilliseconds-start},{"state",ready==renderers.Count ? "ready" : ready>0 ? "partial" : "starting"}};
171
+ }
172
+ static void FindMatches(Dictionary<string,object> node,string query,string control,List<object> matches,int maximum) {
173
+ if(node==null || matches.Count>=maximum) return;
174
+ string text=""; foreach(string field in new string[]{"name","automationId","className","value","controlType"}) if(node.ContainsKey(field)) text+=" "+Convert.ToString(node[field]);
175
+ if(text.IndexOf(query,StringComparison.OrdinalIgnoreCase)>=0 && (control.Length==0 || String.Equals(Convert.ToString(node["controlType"]),control,StringComparison.OrdinalIgnoreCase))) { Dictionary<string,object> match=new Dictionary<string,object>(node); match.Remove("children"); matches.Add(match); }
176
+ if(node.ContainsKey("children")) foreach(object child in (List<object>)node["children"]) { FindMatches((Dictionary<string,object>)child,query,control,matches,maximum); if(matches.Count>=maximum) break; }
177
+ }
178
+ static object ObserveAccessibility(string action, Dictionary<string,object> args) {
179
+ if(args.ContainsKey("windowRef") && TextOption(args,"scope","active_window")=="desktop")throw new InvalidOperationException("WINDOW_REFERENCE_CONFLICT: desktop observation cannot use a window reference");
180
+ PruneElements(); bool full=TextOption(args,"detailLevel","compact")=="full" || action=="find";
181
+ if(action=="element_info") {
182
+ IntPtr owner; AutomationElement element;
183
+ if(args.ContainsKey("elementId")) element=ResolveObservedElement(Convert.ToString(args["elementId"]),args,out owner);
184
+ else {element=AutomationElement.FromPoint(new System.Windows.Point(Convert.ToInt32(args["x"]),Convert.ToInt32(args["y"])));owner=ElementWindow(element,IntPtr.Zero);if(args.ContainsKey("windowRef") || args.ContainsKey("nativeWindowHandle") || args.ContainsKey("processId") || args.ContainsKey("windowTitle")){IntPtr target=ResolveObservationWindow(args);if(owner!=target)throw new InvalidOperationException("ELEMENT_WRONG_OWNER: point is outside target; observe again");}}
185
+ var infoMetadata=new Dictionary<string,object>();WindowReference infoBinding=ObserveWindowReference(owner,infoMetadata);if(infoBinding!=null)currentReference=infoBinding;
186
+ Dictionary<string,object> result=Result(true,action); result["element"]=DescribeElement(element,owner,full,0,infoBinding!=null);foreach(var pair in infoMetadata){result[pair.Key]=pair.Value;if(pair.Key=="windowRefUnavailable")((Dictionary<string,object>)result["element"])[pair.Key]=pair.Value;}if(infoBinding!=null)result["windowRef"]=infoBinding.Id; if(((Dictionary<string,object>)result["element"]).ContainsKey("controlTypeUnavailable")) result["conclusive"]=false; return result;
187
+ }
188
+ bool desktop=TextOption(args,"scope","active_window")=="desktop";
189
+ IntPtr window=desktop ? IntPtr.Zero : ResolveObservationWindow(args);
190
+ var observationMetadata=new Dictionary<string,object>();WindowReference observationBinding=ObserveWindowReference(window,observationMetadata);if(observationBinding!=null){currentReference=observationBinding;observationMetadata["windowRef"]=observationBinding.Id;}
191
+ if(window!=IntPtr.Zero){RememberPointerOrigin(window);RememberPointerTitle(args,window);}
192
+ bool includeOffscreen=Option(args,"includeOffscreen",false);
193
+ Traversal state=new Traversal { Budget=NumberOption(args,"treeTimeoutMs",5000),Maximum=NumberOption(args,"maxNodes",500),Depth=NumberOption(args,"maxDepth",6),Offscreen=includeOffscreen,Full=full,Walker=ObservationWalker(TextOption(args,"viewMode","control"),includeOffscreen) };
194
+ if(window!=IntPtr.Zero) { StringBuilder name=new StringBuilder(256); GetClassName(window,name,name.Capacity); state.BrowserLayout=name.ToString().StartsWith("Chrome_",StringComparison.Ordinal) || name.ToString().StartsWith("Mozilla",StringComparison.Ordinal); }
195
+ Dictionary<string,object> web=window==IntPtr.Zero ? null : WarmBrowser(window,state);
196
+ Dictionary<string,object> root=null;
197
+ try { AutomationElement element=desktop ? AutomationElement.RootElement : AutomationElement.FromHandle(window); root=ReadTree(element,window,0,0,state); }
198
+ catch(Exception error) { state.Reason="providerError"; state.Error=error.Message; }
199
+ Dictionary<string,object> output=Result(root!=null,action); output["scope"]=desktop?"desktop":"active_window"; output["tree"]=root; output["nodeCount"]=state.Count; output["truncated"]=state.Reason!=null || state.UnknownControlTypes>0; output["durationMs"]=state.Watch.ElapsedMilliseconds; output["treeTimeoutMs"]=state.Budget; output["viewMode"]=TextOption(args,"viewMode","control"); output["includeOffscreen"]=state.Offscreen; output["detailLevel"]=TextOption(args,"detailLevel","compact");
200
+ if(window!=IntPtr.Zero) {CheckWindowReference(window);foreach(var pair in observationMetadata)output[pair.Key]=pair.Value; output["nativeWindowHandle"]=window.ToInt64(); output["processId"]=WindowOwner(window); }
201
+ if(web!=null) output["webContent"]=web;
202
+ if(state.UnknownControlTypes>0) { output["unknownControlTypes"]=state.UnknownControlTypes; output["conclusive"]=false; output["truncationReason"]="controlTypeUnavailable"; }
203
+ if(state.Reason!=null) { output["truncationReason"]=state.Reason; output["conclusive"]=false; }
204
+ if(state.Error!=null) { output["providerError"]=state.Error; if(root==null) output["error"]="UIA_PROVIDER_ERROR: "+state.Error; }
205
+ if(action=="find") { List<object> matches=new List<object>(); FindMatches(root,TextOption(args,"query",""),TextOption(args,"controlType",""),matches,NumberOption(args,"maxResults",25)); output.Remove("tree"); output["matches"]=matches; output["query"]=TextOption(args,"query",""); if(matches.Count==0 && Option(args,"ocrFallback",true) && TextOption(args,"controlType","").Length==0 && (state.Count<=20 || state.Reason!=null || state.UnknownControlTypes>0)) output["ocrFallbackPending"]=true; }
206
+ return output;
207
+ }
208
+ }
@@ -0,0 +1,37 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Diagnostics;
4
+ using System.Runtime.InteropServices;
5
+
6
+ partial class WindowsBridge {
7
+ [DllImport("dwmapi.dll",EntryPoint="DwmGetWindowAttribute")] static extern int GetBrowserBorder(IntPtr window,int attribute,out uint value,int bytes);
8
+ [DllImport("dwmapi.dll")] static extern int DwmSetWindowAttribute(IntPtr window,int attribute,ref uint value,int bytes);
9
+ class BrowserBorder { public IntPtr Window;public uint Process,Original; }
10
+ static readonly Dictionary<long,BrowserBorder> browserBorders=new Dictionary<long,BrowserBorder>();
11
+ static object MarkBrowserFrame(Dictionary<string,object> args) {
12
+ uint owner=Convert.ToUInt32(args["processId"]);bool clear=Option(args,"clear",false);int applied=0,refused=0;
13
+ using(Process process=Process.GetProcessById((int)owner)) {
14
+ IntPtr identity=process.Handle;
15
+ if(process.HasExited)throw new InvalidOperationException("BROWSER_PROCESS_EXITED");
16
+ if(clear) {
17
+ foreach(BrowserBorder entry in new List<BrowserBorder>(browserBorders.Values))if(entry.Process==owner) {
18
+ if(IsWindow(entry.Window) && WindowOwner(entry.Window)==owner) {uint original=entry.Original;if(DwmSetWindowAttribute(entry.Window,34,ref original,4)==0)applied++;else refused++;}
19
+ browserBorders.Remove(entry.Window.ToInt64());
20
+ }
21
+ } else EnumWindows(delegate(IntPtr window,IntPtr unused) {
22
+ if(!IsWindowVisible(window) || WindowOwner(window)!=owner)return true;
23
+ uint original;BrowserBorder entry;
24
+ if(!browserBorders.TryGetValue(window.ToInt64(),out entry)) {
25
+ if(GetBrowserBorder(window,34,out original,4)!=0)original=0xFFFFFFFF;
26
+ entry=new BrowserBorder {Window=window,Process=owner,Original=original};
27
+ }
28
+ uint blue=0x00EB841C;
29
+ if(!process.HasExited && WindowOwner(window)==owner && DwmSetWindowAttribute(window,34,ref blue,4)==0) {browserBorders[window.ToInt64()]=entry;applied++;}else refused++;
30
+ return true;
31
+ },IntPtr.Zero);
32
+ }
33
+ Dictionary<string,object> result=Result(true,"browser_frame");result["applied"]=applied;result["refused"]=refused;
34
+ if(applied==0)result["warning"]="No owned visible browser frame accepted DWM border color; verify visual presentation manually";
35
+ return result;
36
+ }
37
+ }