feat(bridge-windows): Phase 0e2 + 0e3 — list_windows, capture, execute (real UIA driving real Windows) - #13
Merged
Merged
Conversation
…alk)
Adds the real UIA-driven methods to the Windows bridge. Builds clean
against FlaUI 5.0.0 on .NET 8 ARM64. Empty-window result confirmed in
SSH non-interactive sessions (Windows session 2 has no UI); validation
against real windows requires running the smoke test from the
interactive Parallels console (Phase 0e2 follow-up task -- arrange
scheduled-task / PsExec for SSH-driven iteration).
New files in apps/agent-runner/bridges/windows/Uia/:
- StaWorker.cs -- single-threaded apartment worker. UIA is COM-STA;
every public capturer call runs on a dedicated
STA thread to avoid COM proxy leaks + deadlocks.
- CaptureDtos.cs -- DTOs (WindowSummaryDto, DesktopCaptureDto,
DesktopElementDto, AriaStateDto, BoundsDto) that
camelCase-serialize directly into the
@thinkfleet/agentmark v0.4 DesktopCapture shape.
- RoleMapper.cs -- UIA ControlType --> normalized DesktopRole vocab.
Handles Window/Pane/ToolBar/Menu/Tab/Tree/List/
Table/Button/Edit/CheckBox/Slider/Hyperlink/etc.
Falls back to InferCustom() for ControlType.Custom
(WPF / WinForms / Electron) before giving up to
"other".
- UiaCapturer.cs -- the real worker.
* ListWindows(): walks top-level UIA windows,
skips chrome-less / off-screen / no-title
ghosts, marks the one containing the focused
element with hasFocus=true.
* Capture(req): resolves target by windowId
(hwnd:0x..), processId, processName, or
windowTitle; falls back to focused window.
Then walks the tree depth-first with depth +
element-count + deadline caps. Extracts value
from Value/RangeValue/Text patterns, selection
from SelectionItem, expansion from
ExpandCollapse, toggle state from Toggle
(mapped onto aria.pressed/checked).
* Everything is defensive -- every property read
is wrapped in try/catch because UIA can return
ElementNotAvailable mid-walk on highly dynamic
UIs (Excel especially).
Program.cs: dispatcher now lazy-instantiates StaWorker + UiaCapturer
(no UIA boot on ping-only smoke tests, ~150ms saved); routes
list_windows + capture through the STA thread; disposes both on shutdown.
scripts/uia-smoke-test.ps1: spawns the bridge, drives three requests
(list_windows, capture by windowId, capture focused). Designed to be
safe to run in any session -- empty results don't fail, they instruct
the user to open a GUI app in the VM console and rerun.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the Windows UIA bridge from "scaffolded" to "actually drives
Windows software." Includes
list_windows,capture, andexecuteplus a robustness fix for the PowerShell-emits-BOM stdin quirk and a
live Notepad demo that proves the full capture → execute → re-capture
loop works against real software.
This is the milestone where AgentMark Desktop stops being a
prototype and starts being real. Validated on Windows 11 ARM64
(Parallels) — bridge captured a live Notepad tree (37 elements, depth
6), wrote "Hello from AgentMark Desktop" into the editor via
ValuePattern.SetValue, and re-captured to verify the typed textlanded. Zero vision/AI involvement; pure structured-tree manipulation
through the protocol.
What's in this PR
Uia/StaWorker.csSingle-threaded apartment worker. UIA is COM-STA; every public
capturer call runs on a dedicated STA thread to avoid COM proxy
leaks + deadlocks.
Uia/CaptureDtos.csDTOs that camelCase-serialize directly into the
@thinkfleet/agentmarkv0.4DesktopCaptureshape — same wireformat the Node-side
WindowsUiaBackend(Phase 0f) will consume.Uia/RoleMapper.csUIA
ControlType→ normalisedDesktopRolevocabulary. HandlesWindow/Pane/ToolBar/Menu/Tab/Tree/List/Table/Button/Edit/CheckBox/
Slider/Hyperlink/etc. Falls back to
InferCustom()forControlType.Custom(WPF / WinForms / Electron) before giving up to"other".Uia/UiaCapturer.csThe real worker. Three public methods:
ListWindows()— walks top-level UIA windows, skipschrome-less / off-screen / no-title ghosts, marks the one
containing the focused element with
hasFocus=true.Capture(req)— resolves target bywindowId(hwnd:0x..),processId,processName, orwindowTitle; falls back to focusedwindow. Walks the tree depth-first with depth + element-count +
deadline caps. Extracts value from Value / RangeValue / Text
patterns, selection from SelectionItem, expansion from
ExpandCollapse, toggle state from Toggle (mapped onto
aria.pressed/aria.checked). Stashes a live element handleper id in a per-capture session so subsequent
executecanresolve back to the live UIA element.
Execute(req)— dispatches an action by element_id throughthe right UIA pattern:
click→InvokePattern.Invoke(with Toggle / SelectionItem/ ExpandCollapse / focus+Space fallbacks)
type→ValuePattern.SetValue(with focus+keystroke fallback,Ctrl+A/Del clear support)
select→SelectionItemPattern.Selecton the target, orexpand + locate-by-name on a Selection container, or
ValuePattern for editable combos
check→TogglePattern.Togglewith state convergenceexpand→ExpandCollapsePattern.Expand/Collapsefocus→AutomationElement.Focusscroll_to→ScrollItemPattern.ScrollIntoViewwith focusfallback
key→Keyboard.Type/TypeSimultaneouslywith modifierparsing (ctrl|alt|shift|meta|win) and friendly key-name
vocabulary
Every UIA call is defensively wrapped — UIA returns
ElementNotAvailablemid-walk on dynamic UIs (Excel especially) andwe don't want one stale element to abort the whole capture or execute.
Program.cslist_windows/capture/executethrough theSTA thread; lazy-instantiates UIA so
ping-only smoke tests stayfast.
the first WriteLine sometimes and System.Text.Json rejects the
malformed request otherwise.
Demos
scripts/uia-smoke-test.ps1— exerciseslist_windows+captureagainst whatever windows are visible. Safe to run anywhere; reports
empty session honestly when run from SSH (session-0 limitation).
scripts/notepad-demo.ps1— full end-to-end loop:list_windows→ find Notepadcapture→ locate text_areaexecutetype → write a messagecapture→ verify the typed text is in element.valueValidated on Win 11 ARM64. PASSED.
Footnote: SSH session limitation
SSH login on Windows runs in a different session than the interactive
Parallels console, so UIA from an SSH-spawned bridge sees an empty
desktop. The bridge logic is correct (
list_windowscleanly returns[]with no error); the limitation is that the SSH session's windowstation has no UI. Iteration-heavy phases run from the VM's
interactive PowerShell for now; a scheduled-task / PsExec trick to
launch the bridge in the interactive session from SSH is queued for
later.
Live demo proof
Next PRs
alignment via NowCerts users)
WindowsUiaBackend— replaces FixtureBackendin the MCP server, spawns this bridge, speaks JSON-RPC. After this
lands, Claude Code / Cursor / Codex on Windows get real desktop
access through
agentmark_desktop_*MCP tools with zeroper-tool integration work.
🤖 Generated with Claude Code