UI Automation on Microsoft Windows was designed invisible. Terminator paints it.
Microsoft UI Automation is the COM accessibility surface that powers Narrator, Inspect.exe, FlaUI, pywinauto, and WinAppDriver. None of them tell a human what the bot is doing in real time. Terminator does, through a translucent click-through overlay wrapped in an RAII guard. File: crates/terminator/src/platforms/windows/action_overlay.rs, 564 lines, MIT.
The absent primitive: visible automation
Read the UI Automation docs on learn.microsoft.com and you will find the same shape repeated across every page. AutomationElement, control patterns, TreeWalker, PropertyCondition. The entire API is built around letting a silent process read the UI and invoke it without the user noticing. That was intentional. UIA was designed for assistive technology, where the sighted user is already operating the computer and the screen reader works in the background.
The moment you repurpose UIA for automation, that design assumption becomes a bug. You want to hand a bot control of a desktop and then watch what it does. You want an operator next to the machine to see which button is about to be clicked before the click lands. You want QA to record a run and play back the visible decision points. The official surface gives you none of that. It gives you invoke() and a bool.
Terminator fills that gap in the Windows adapter. The file you want is crates/terminator/src/platforms/windows/action_overlay.rs. It is a 564-line layered-window implementation that draws what the framework is about to do, every time.
The constants that shape the overlay
Four numbers at the top of action_overlay.rs. Nothing else matters.
Alpha 77 out of 255 is roughly 30 percent opaque. The background is filled with pure white via FillRect(hdc, &rect, CreateSolidBrush(COLORREF(0xFFFFFF))), so the screen ends up reading as frosted grey with a centered 800-by-400 pixel black-bordered box showing the action name in 32px Segoe UI bold and the element description in 20px Segoe UI regular. The 300 ms minimum display time is the interesting one. Most UIA invokes finish in 20 to 80 ms, faster than a human can focus on a message, so hide_action_overlay sleeps the shortfall before actually removing the window.
“SetLayeredWindowAttributes(hwnd, COLORREF(0), 77, LWA_ALPHA). That one call is the difference between a debugger breakpoint and a translucent status bar you can read across the room.”
action_overlay.rs line 347
The RAII guard, and why it cannot be forgotten
If the overlay were a pair of plain functions (show, hide) the framework would eventually ship an action verb that forgot to call hide. Rust has a better pattern, and ActionOverlayGuard uses it. You construct the guard at the top of every action function. It goes out of scope when the function returns, whether that is a normal return, an early error return, or a panic. The Drop implementation calls hide. The guard cannot be skipped.
Eleven call sites, one pattern
Every UIA action verb in element.rs opens with the same four lines. Grep the file for ActionOverlayGuard::new and you get the list below.
show_action_overlay()
Spawns a layered HWND across the virtual screen. Stores the handle in a global RwLock<OverlayState>. Checks the 100 ms anti-spam cooldown before painting.
hide_action_overlay()
Reads OVERLAY_SHOWN_AT, sleeps the remainder of MINIMUM_DISPLAY_MS if the action finished too fast, then signals the overlay thread through an AtomicBool and posts WM_CLOSE.
ActionOverlayGuard
RAII value held on the stack for the duration of a UIA action. new(action, element_info) paints. Drop hides. Four lines at the top of every action verb.
OVERLAY_STATE
once_cell::sync::Lazy<RwLock<OverlayState>> holding is_visible, message, sub_message, and the current HWND as an isize. Std sync, not tokio, so it works from Win32 WndProc callbacks.
TERMINATOR_ACTION_OVERLAY
Env var kill switch. 0 / false / off disables at startup. The check runs once via Lazy. set_action_overlay_enabled(false) toggles at runtime.
update_action_overlay_message()
Updates the centered text while the HWND stays alive. Useful when one UIA action streams sub-steps (type, then press Enter). Calls InvalidateRect to force a repaint.
How show and hide flow through the system
The ordering matters because UIA actions can be faster than the overlay lifecycle. Here is the full path from guard construction to the released HWND.
overlay lifecycle on a single UIA invoke
The five window styles that make it click-through
This is the whole reason Terminator can draw a full-screen overlay without breaking the automation. Five extended styles, set once at window creation, and the overlay becomes a passive painter that ignores input and refuses focus.
what each style buys you
- WS_EX_TOPMOST keeps the overlay above the target app's windows without being owned by them.
- WS_EX_LAYERED is the prerequisite for SetLayeredWindowAttributes and alpha blending.
- WS_EX_TRANSPARENT forwards mouse clicks to whatever is underneath, so the overlay never intercepts input.
- WS_EX_TOOLWINDOW omits the overlay from Alt-Tab. Humans do not see a second task entry while a bot runs.
- WS_EX_NOACTIVATE means the overlay never steals focus from the app Terminator is automating.
Actionability pre-checks, then overlay, then click
The overlay is the visible half. The invisible half is a Playwright-style actionability gate that runs before Terminator ever moves the cursor. validate_clickable rejects the action if the element is not visible, not enabled, or outside the viewport. determine_click_coordinates then prefers UIA's own GetClickablePoint over a bounding-box center, because Microsoft's UIA guidance is that GetClickablePoint is the correct point for hit-testing custom controls.
one UIA invoke, two visible layers
What it looks like when it runs
The overlay paints the action name in bold 32-pixel Segoe UI and the element description in 20-pixel Segoe UI regular. Here is the sequence as it prints from the Rust tracing crate at info level when Terminator clicks a Save button.
Turn it off for CI
The overlay is for humans. For CI, set the env var. The check runs once on first access through once_cell::sync::Lazy and flips the global AtomicBool. Runtime toggles are atomic too.
How this compares to other UI Automation clients
| Feature | Typical UIA client | Terminator |
|---|---|---|
| visible feedback when an action fires | silent. UIA is programmatic-only | TerminatorActionOverlay paints every action |
| pattern covers every action verb without reminders | developer opts in to logging per call site | ActionOverlayGuard RAII, 11 call sites, impossible to forget |
| overlay blocks user input | not applicable | no. WS_EX_TRANSPARENT forwards mouse, WS_EX_NOACTIVATE never steals focus |
| minimum visible time for fast actions | not applicable | MINIMUM_DISPLAY_MS = 300 so 40 ms invokes still flash |
| anti-strobe for rapid actions | not applicable | OVERLAY_CHANGE_COOLDOWN_MS = 100 |
| kill switch for CI | not applicable | TERMINATOR_ACTION_OVERLAY=0 env var or set_action_overlay_enabled(false) |
| Playwright-style actionability pre-checks | developer checks IsEnabled manually | validate_clickable: visible, enabled, in-viewport, in that order |
| auditable MIT source | COM surface only, no impl visibility | action_overlay.rs 564 lines, element.rs call sites visible on GitHub |
What you touch on top of IUIAutomation
from SDK call to on-screen overlay
Your code: locator().click()
Whether you call this from Rust, Node through @mediar-ai/terminator, Python via terminator-py, or through the MCP server, the trace ends in the same Windows adapter function.
element.rs: click()
Constructs ActionOverlayGuard::new('Clicking', Some(&element_info)). The guard paints the overlay and records OVERLAY_SHOWN_AT.
validate_clickable()
Three UIA reads: is_visible, is_enabled, ensure_in_viewport. If any fails, the function returns an AutomationError and the guard's Drop hides the overlay cleanly.
determine_click_coordinates()
IUIAutomationElement::GetClickablePoint first, BoundsCenter fallback. Returns (x, y, source, api) so the trace attributes which UIA call produced the point.
input.rs: send_mouse_click()
Win32 SendInput pathway. This is the only part of the pipeline that Microsoft UIA does not own. Input is synthesized outside the accessibility surface so it works even on controls that do not implement InvokePattern.
Drop(_overlay_guard)
hide_action_overlay runs. Enforces MINIMUM_DISPLAY_MS if the click finished under 300 ms, then posts WM_CLOSE and DestroyWindow.
Why this matters for agent-style automation
When an LLM is driving your desktop through an MCP tool, a human operator needs a fast way to know that the agent is about to click the wrong thing. Staring at the mouse pointer is not enough, because UIA invokes frequently do not move the pointer at all (Invoke pattern dispatches a WM_COMMAND, not a mouse event). Terminator's overlay gives you a half second to yank Ctrl-C before Invoking is followed by the next verb. That is the loop the TERMINATOR_ACTION_OVERLAY env var exists to preserve in CI and disable in production agents that no human is watching.
Microsoft UI Automation surfaces the overlay covers
Frames of the overlay life
Worth seeing end to end. The animation below walks through one single-click from guard construction to WM_CLOSE.
life of one action_overlay call
t = 0 ms
ActionOverlayGuard::new("Clicking", Some(info)) on the stack. Global OVERLAY_STATE.is_visible flips to true.
Bring Microsoft UI Automation out of the shadows for your team
30 minutes on what the overlay shows, how to wire it to your agents, and whether Terminator fits your Windows automation workloads.
Microsoft UI Automation FAQ
What is UI Automation on Microsoft Windows?
Microsoft UI Automation (UIA) is the COM-based accessibility framework that ships with Windows as the successor to Microsoft Active Accessibility. It exposes the entire desktop as a tree of IUIAutomationElement nodes rooted at the desktop, with typed control patterns like Invoke, Value, Toggle, ExpandCollapse, Window, Selection, Scroll, and RangeValue. Screen readers (Narrator, NVDA, JAWS), inspection tools (Inspect.exe, AccEvent, FlaUInspect), and automation frameworks (FlaUI, pywinauto, WinAppDriver, Terminator) all drive this same COM surface through IUIAutomation.
Why does every UI Automation client feel headless even when it is not?
Because the UIA surface was designed for assistive technology, not for operators watching a bot. Every interface on learn.microsoft.com for UIA documents programmatic access: walk a tree, read a property, invoke a pattern. There is no IUIAutomation method for 'tell the human what I just did'. The framework presumes the consumer is a screen reader that speaks to a user, or a test harness running in CI. So when you drive an app with raw UIA, IAccessible, WinAppDriver, or FlaUI, nothing on screen changes to reflect that automation is in progress. Actions happen silently.
What does Terminator add that other Microsoft UI Automation tools do not?
A full-screen click-through overlay that paints the current action on top of the virtual screen every time the framework invokes a UIA pattern. It is implemented in crates/terminator/src/platforms/windows/action_overlay.rs. The window is registered under class name TerminatorActionOverlay, created across the entire virtual screen with extended styles WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, set to alpha 77 of 255 (roughly 30 percent opaque) via SetLayeredWindowAttributes, and rendered with a centered 800 by 400 pixel message box. WS_EX_TRANSPARENT means mouse and keyboard input pass straight through to whatever you are automating. The overlay is ambient status, not a blocker.
How is the overlay enforced across every action in Terminator?
Through an RAII guard called ActionOverlayGuard. You construct it with ActionOverlayGuard::new(action_name, Some(element_description)), which calls show_action_overlay. When the guard goes out of scope, Drop calls hide_action_overlay. Because every action verb in crates/terminator/src/platforms/windows/element.rs owns a local _overlay_guard, a function cannot return without hiding the overlay. There are 11 call sites in element.rs covering click, double_click, right_click, invoke, perform_action for named actions, type_text, press_key, set_value, scroll, and set_toggled_with_state. You cannot add a new action verb and forget to draw the overlay, because the idiom is the same four lines every time.
What exact constants govern the overlay timing?
Four constants live at the top of action_overlay.rs. OVERLAY_CHANGE_COOLDOWN_MS is 100 milliseconds; any second show call inside that window is dropped so rapid-fire clicks do not strobe. MINIMUM_DISPLAY_MS is 300 milliseconds; if a UIA action completes faster than that, hide_action_overlay sleeps the remainder so a 40 millisecond invoke still flashes long enough for a human to read. WINDOW_CREATION_DELAY_MS is 50 milliseconds, the small pause after spawning the overlay thread so the HWND exists before the action proceeds. The alpha is 77 out of 255, set via SetLayeredWindowAttributes(hwnd, COLORREF(0), 77, LWA_ALPHA).
Can I turn the overlay off for CI or for workflow recording?
Yes. Set the environment variable TERMINATOR_ACTION_OVERLAY to 0, false, or off before the process starts and every show_action_overlay call becomes a no-op. The check happens once on first use through a once_cell::sync::Lazy initializer. You can also toggle it at runtime with terminator::platforms::windows::action_overlay::set_action_overlay_enabled(false), which atomically swaps an AtomicBool and hides any active overlay. The workflow recorder (crates/terminator-workflow-recorder) sets recording mode globally via the RECORDING_MODE atomic in highlighting.rs so the passive inspector does not bloom overlays while the user is clicking around.
How does the overlay coexist with real keyboard and mouse input?
By being click-through. The overlay window is created with WS_EX_TRANSPARENT, which tells Windows to forward mouse messages to whatever is underneath. It is also created with WS_EX_NOACTIVATE so it never steals focus from the target application, and with WS_EX_TOOLWINDOW so it does not appear in the Alt-Tab switcher. Keyboard input to the application being automated, human input, and Terminator's own synthesized input (from crates/terminator/src/platforms/windows/input.rs) all pass through the overlay unchanged. The human sees a translucent status; the app sees unmodified input.
What Playwright-style pre-checks run before a UIA action?
validate_clickable in element.rs runs three checks before any click. First, is_visible, which includes a multi-monitor bounds check so off-screen elements are rejected. Second, is_enabled, which reads IsEnabled from UIA so disabled buttons never receive a click. Third, ensure_in_viewport, which searches up to 7 ancestors for an IUIAutomationScrollPattern and scrolls if needed. Only after those pass does determine_click_coordinates run, and it prefers UIA GetClickablePoint over BoundsCenter because GetClickablePoint is what Microsoft's own pattern documentation recommends for click hit testing. Between the pre-checks and the overlay, every click is both validated and visible.
Does this overlay pattern work on Win32, WPF, WinUI 3, and UWP?
Yes. The overlay is a top-level layered HWND owned by the Terminator process, which means it renders on top of any Windows surface regardless of framework. The UIA side reads IUIAutomationElement from IAccessible-bridged Win32 controls, from native WPF providers, from XAML-generated UWP and WinUI 3 providers, and from Microsoft Edge WebView2 accessibility trees. The overlay paints the same way whether Terminator is clicking a WPF Button, an old Win32 edit control, a UWP AppBarButton, or an HTML input inside Edge. The COM boundary the overlay lives above is the same one UIA lives above.
Where is the source and what is the license?
Terminator is MIT licensed. The overlay implementation is in crates/terminator/src/platforms/windows/action_overlay.rs, 564 lines. ActionOverlayGuard and the global OVERLAY_STATE RwLock are at the top of that file. The call sites are in crates/terminator/src/platforms/windows/element.rs: every action verb has a matching let _overlay_guard = ActionOverlayGuard::new(...) at the top of its body. Install the framework from npm as @mediar-ai/terminator, from pip as terminator-py, or run the MCP server with npx -y terminator-mcp-agent@latest.
Related guides
Microsoft UI Automation: the five spatial selectors
rightof:, leftof:, above:, below:, near: with a 50px threshold. What IUIAutomation does not do.
Microsoft UI Automation tool: subtree caching
One IUIAutomationCacheRequest, seven UIProperty fields, TreeScope::Subtree. The whole subtree in one COM call.
Coded UI Automation: what replaced it
Microsoft deprecated Coded UI in Visual Studio 2019. Here is the modern UIA toolchain.