# Architecture ## Overview lvt is structured as a 4-stage pipeline that transforms a target window identifier into a structured element tree. ```mermaid flowchart TB A["Target\nResolution\ntarget.cpp"] --> B["Framework\nDetection\nframework_detector.cpp"] B --> C["Tree\nBuilding\ntree_builder.cpp\nproviders/*"] C --> D["Serialization\n& Output\njson_serializer.cpp\nscreenshot.cpp"] ``` ## Stage 1: Target Resolution (`target.cpp`) Accepts one of four target specifiers and resolves to an `HWND` + `PID`: | Input | Resolution method | |-------|-------------------| | `--hwnd` | Used directly; PID via `GetWindowThreadProcessId` | | `--pid` | `EnumWindows` → filter by PID → select largest visible window | | `--name` | Enumerate all top-level windows, match process name (case-insensitive) | | `--title` | Enumerate all top-level windows, match title substring | For `--name` and `--title`, if multiple windows match, lvt prints all matches and exits (lets the user refine with `--hwnd`). ## Stage 2: Framework Detection (`framework_detector.cpp`) Enumerates modules loaded in the target process via `EnumProcessModules` and checks for known DLLs: | Framework | Detection signal | Version source | |-----------|-----------------|----------------| | Win32 | Always present | N/A | | ComCtl | Child window with known ComCtl class name | `comctl32.dll` file version | | XAML | `Windows.UI.Xaml.dll` loaded | DLL file version | | WinUI3 | `Microsoft.UI.Xaml.dll` loaded | DLL file version | | WPF | `PresentationFramework.dll` or `wpfgfx_*.dll` loaded | DLL file version | | Avalonia | `Avalonia.Base.dll` loaded (via plugin) | DLL file version | ComCtl detection uses `EnumChildWindows` and checks against a list of known class names (`SysListView32`, `SysTreeView32`, `ToolbarWindow32`, etc.). ## Stage 3: Tree Building (`tree_builder.cpp` + providers) ### Layered provider model The tree is always rooted in the Win32 HWND hierarchy. Framework-specific providers layer additional detail on top: ```mermaid flowchart BT Win32["Win32Provider\n(base HWND tree)"] ComCtl["ComCtlProvider\n(enrich controls)"] XAML["XamlProvider\n(inject TAP DLL)"] WinUI3["WinUI3Provider\n(inject TAP DLL)"] WPF["WpfProvider\n(persistent managed TAP)"] WinForms["WinFormsProvider\n(persistent managed TAP)"] Avalonia["Avalonia plugin\n(managed TAP)"] Tree["Unified Element Tree"] Win32 --> ComCtl & XAML & WinUI3 & WPF & WinForms & Avalonia ComCtl & XAML & WinUI3 & WPF & WinForms & Avalonia --> Tree ``` 1. **Win32Provider** builds the base tree by recursively enumerating child windows (`EnumChildWindows`). Each HWND becomes an `Element` with class name, text, bounds, styles. 2. **ComCtlProvider** walks the existing tree and enriches known ComCtl controls. For example, a `SysListView32` element gets child elements for its items, columns, and headers via control-specific messages (`LVM_GETITEMCOUNT`, `LVM_GETITEMTEXT`, etc.). 3. **XamlProvider / WinUI3Provider** inject the TAP DLL into the target process, receive the XAML visual tree as JSON over a persistent named pipe, and graft XAML subtrees into matching `DesktopChildSiteBridge` elements in the Win32 tree. 4. **WpfProvider / WinFormsProvider** inject architecture-matched native CLR hosts once per session. The managed tree walker connects one duplex pipe and serves correlated tree and typed-property commands until `DISCONNECT` or target/pipe failure. WPF dependency-property operations run on `Application.Dispatcher`; WinForms `TypeDescriptor` operations run through the control's owning Form. Managed object IDs are weakly assigned and the per-connection reverse map is rebuilt on every snapshot, so refreshes preserve identity without retaining dead controls. See [Managed TAP connections](managed-tap-connections.md). 5. **Avalonia plugin** injects an architecture-matched native TAP (`x86`, `x64`, or `arm64`) and loads one AnyCPU managed walker through CoreCLR's default component entry point. Its current ABI-v1 path is one-shot: each enrichment unloads the native TAP before a later refresh reconnects. Per-target injection ownership remains held until the remote `LoadLibraryW` thread completes or the target exits, even after the diagnostic timeout, preventing a retry from racing a delayed load. CoreCLR 8 is the supported floor. ### Reusable connections (`providers/framework_connection.h`, `connection_registry.h`) Injecting the TAP DLL and calling `AdviseVisualTreeChange` is meant to happen **once** per debugging session, not on every tree refresh — see `docs/tap-dll-design.md`'s connection lifecycle section. `IFrameworkConnection` is the generic interface a provider can implement to expose that as "connect once, `get_tree()` many times"; `ConnectionRegistry` is a per-process, refcounted registry (keyed by `pid` + framework label) that lets a long-running consumer — `watch`'s tick loop, an MCP session — acquire one via a move-only `ConnectionHandle` and reuse it for its own lifetime, instead of each tree refresh re-injecting from scratch. `tree_builder.h`'s `build_tree` takes an optional `ConnectionLookup` callback for this; a caller that doesn't supply one (a one-shot `dump`/`query`/`screenshot`) sees no behavior change — providers fall back to their original one-shot `enrich()`. XamlProvider, WinUI3Provider, WpfProvider, and WinFormsProvider use persistent diagnostics connections. One-shot WPF/WinForms operations use the same path as long-running sessions — open connection, `GET_TREE`, `DISCONNECT` — so injection and teardown have one implementation. Win32Provider and ComCtlProvider use lightweight per-session connections, not for tree collection, but to register opaque native element identities and cache provider-owned typed-property schemas. The Win32 connection also owns one root-scoped WinEvent subscription that covers the native HWND subtree and common-control logical-item notifications. UI Automation sessions own one `IUIAutomation` client and one exact-HWND event subscription on a persistent MTA thread; registry keys include the root HWND so two top-level windows in one process never share callbacks or identity state. Plugins can adopt the interface without changing how callers acquire or use them. ### Native WinEvent refresh hints (`native_win_event.*`) Long-running visual sessions install one `SetWinEventHook` subscription on the Win32 connection for `EVENT_OBJECT_CREATE` through `EVENT_OBJECT_PARENTCHANGE`, filtered by the target PID. The hook is `WINEVENT_OUTOFCONTEXT`; targets outside lvt also use `WINEVENT_SKIPOWNPROCESS`. A dedicated thread installs the hook, owns the required message loop, and calls `UnhookWinEvent` exactly once on that same thread when the last connection-generation reference is released or the target process exits. Out-of-context callbacks route through thread-local state, so simultaneous sessions have no global hook-to-connection map that could collide. The callback does not send messages, inspect HWNDs, take locks, or allocate. It only copies a small event record into a fixed 256-entry single-producer queue; reentrancy or overflow sets a bounded reset marker. `poll_events()` drains off-callback, filters records against the root and the HWNDs from the last published snapshot, and coalesces any relevant native/control-item activity to one provider-neutral `snapshotRequired` event. Destroy notifications are matched only against stored handle values and never dereference or query the destroyed HWND. Current HWND subtree checks for create/reparent events happen only during the later drain. Native eventing is an optimization signal, never a correctness source. Both `watch` and MCP still build complete periodic snapshots and derive public added/removed/changed output only from `diff_trees`; they drain pushed events after that authoritative refresh instead of also emitting native structural deltas. Missing, delayed, coalesced, or overflowed WinEvents therefore cannot hide text, bounds, state, or structure changes and cannot create duplicate add/remove storms. The current schedulers do not wait on a provider event handle: `watch` latency remains bounded by `--interval`, and subscribed MCP resources by their approximately 500 ms polling cadence. Key-scoped watch resolves the requested `--element` only for its initial snapshot. It records the resolved key, parent, full-tree path, base identity, and provider identity, then builds and reconciles complete trees on every later tick before deriving the reported scoped snapshot. An authoritative durable-identity replacement at the anchored parent/path is surfaced as a Removed subtree followed by an Added subtree with the fresh key. A moved sibling merely occupying the old slot is not adopted: replacement requires continuity from a durable identity, supported native/provider handle, normalized UIA RuntimeId, or an explicit provider mapping whose parent cardinality is unchanged. A positional/ambiguous scope has no such token, so a sibling-set structural change removes it and later slot occupants are ignored until the caller rescopes. Removal with a retained token keeps the watch alive until that anchored identity validly reappears. Full-tree reconciliation preserves process-wide XAML/WinUI reparent matching, while incomplete provider markers continue to preserve the previous snapshot rather than reporting false removals. ### UI Automation refresh hints (`uia_provider.*`) Each persistent `UiaConnection` creates its `IUIAutomation` client, root element, cache request, and event handlers on one connection-owned MTA thread. The subscriptions use `TreeScope_Subtree` from the exact `ElementFromHandle` root. `UiaTargetIdentity` treats the resolver/registry PID as authoritative: connection creation rejects a numeric HWND currently owned by any other process before starting the worker. Target resolution also captures the process creation timestamp and the root UIA `RuntimeId`. Persistent connections retain an open handle to that original process, while one-shot tree/action fallbacks carry the same immutable PID, creation identity, and root token captured by the CLI command or MCP session. Because a same-process HWND can eventually recycle to the exact same numeric value and therefore reproduce an HWND-derived UIA `RuntimeId`, identity also owns a non-recyclable window-lifetime sentinel. lvt normally installs a GUID-named window property whose unique value exists only for that window generation and disappears when it is destroyed. The last identity owner removes the property only if the original process is still live and the property still matches. If `SetProp` is denied (for example by an integrity boundary), lvt keeps support by falling back to an exact-root destroy WinEvent subscription; validation synchronizes with that hook before accepting the window. Immediately after every `ElementFromHandle`, on the owning MTA, lvt rechecks the current HWND owner, retained process identity, window-lifetime sentinel, UIA root `ProcessId`, and root `RuntimeId` before reading or acting. A stale registry key therefore cannot attach to a replacement process after Windows reuses a PID/HWND, and same-process HWND reuse cannot silently switch to a different automation root. No desktop/global UIA subscription is installed. Structure changes, UIA property changes affecting tree shape/identity/bounds/enabled/offscreen state, pattern availability and Value/RangeValue/Toggle/ExpandCollapse/Selection/Scroll state, plus relevant automation events all feed the same provider-neutral event flow. The COM callback object is free-threaded and deliberately does no UIA work: it takes no lock, performs no tree walk or provider call, and allocates nothing. It atomically coalesces any burst to one bounded `snapshotRequired` bit. `refresh_events()` provides an MTA barrier and `poll_events()` exchanges that bit on the owning MTA. Watch and MCP consume the bit only after a successful authoritative UIA snapshot. MCP drain scopes are tree-specific: UIA snapshots poll only the `uia` connection, while visual snapshots poll injected, managed, and plugin connections but exclude UIA; native hints retain their pre-visual build phase. Thus correlation performs exactly one visual-provider drain and one UIA drain, a failed walk cannot lose the wake-up needed by its retry, and a UIA hint arriving after the last UIA snapshot stays queued across unrelated visual requests. The MTA worker checks the exact HWND/PID every 100 ms. On disconnect, target exit, or final release it first stops accepting callback hints, calls `RemoveAllEventHandlers` on the same `IUIAutomation` instance/thread, releases all COM references there, and only then reports the connection dead. UIA remains architecture-neutral because no target-side code or pointer-sized payload is injected. Persistent tree failures may still fall back to a one-shot UIA client for ordinary transient provider contention, but the fallback receives the session's captured `UiaTargetIdentity`; it never re-derives ownership from the current numeric HWND. Identity mismatch returns `ownershipLost` and stops the walk/action rather than exposing a replacement process or root. Synthetic UIA fallbacks repeat the full identity check after foreground activation and again immediately before every `SendInput` dispatch (mouse, wheel, text, or each key chord). Ownership failures set the structured action code `ownershipLost`, including elementless type/press-key requests, so CLI and MCP clients can distinguish a security boundary from an ordinary action error. Every dispatch also verifies that the target root is still the foreground window. Multi-batch operations (focus-click then text, select-all then text, or multi-chord key sequences) re-activate and revalidate between batches if foreground moved; otherwise they refuse the remaining input. UIA cache and pattern boundaries are fenced too. Every `BuildUpdatedCache` result is followed by a fresh original-token/root validation before cached data is traversed or returned, including when the provider call failed. Mutation patterns, `Realize`, and `SetFocus` validate immediately before the provider call and again afterwards; a provider failure is never classified until that post-call validation has ruled out target replacement. Visual MCP sessions capture the same `UiaTargetIdentity` even though their tree comes from framework-native providers. Geometry-based input and Win32 window commands validate that original identity before acting, after foreground activation, and immediately before each dispatch. Visual and UIA `wait-gone` still succeed when the original target disappears, while `wait-for` reports structured `ownershipLost`. The initial visual activation is unconditional: even an already-foreground root still passes through `bring_to_foreground`, whose contract also restores a minimized window and raises it in Z order. Subsequent batches re-activate only when foreground moved. If Windows temporarily reports no foreground window, `bring_to_foreground` creates the caller's message queue, restores and raises only the intended root, and retries activation twice before returning the system-observed result. It never focuses an unrelated window as an intermediate step. ### Native typed-property safety (`native_message.*`, `native_property_connection.*`) The Win32/Common Controls property adapter is intentionally curated. Its descriptor ids map to internal semantic operations such as `Text`, `SelectedIndex`, or `Expanded`; they cannot encode a message id or native pointer. All cross-process messages go through one `SendMessageTimeoutW` wrapper with `SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT`. Common-control messages at or above `WM_USER` do not marshal caller pointers, so structures and strings for those messages live in RAII `VirtualAllocEx` buffers. The owning process handle uses only `PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE`; pointer message lifetime uses a separate `SYNCHRONIZE`-only process handle. System messages such as `WM_GETTEXT` and `EM_GETSEL` use User32's documented marshalling but remain disabled across architectures in the conservative first implementation. Because a timed-out send can return before the target WndProc has finished, pointer-message buffers are not released on caller timeout. A bounded retirement manager holds them until target-process exit; exhausting the bound refuses new pointer sends rather than accumulating unbounded remote memory. Toolbar text avoids the no-capacity `TB_GETBUTTONTEXT` copy: it uses that message only for the documented length query, then retrieves through `TB_GETBUTTONINFO` with `cchText` and bounded re-query/retry. Every mutation validates that the HWND still exists and has the same PID and class. Item operations additionally revalidate the current index and identity (or toolbar command id), range-constrained values are checked against live state, and every successful write is read back. Owner-data or ambiguous list-view/tab items and ABI-sensitive cross-bitness operations remain visible but read-only with a reason. Native connections accept only HWNDs registered from their root/descendant tree; compact handles do not authorize arbitrary same-process top-level windows, and every operation repeats the root/descendant check so reparented controls stop being actionable. Publishing a new visual snapshot replaces, rather than accumulates, the session allow-list. Connection lookup validates session liveness before and after connection-map work, never while holding the connection-map lock, so disconnect cannot recreate an erased entry and authorization publication cannot deadlock against property connection acquisition. Native durable keys are deliberately independent of those mutation handles. Every HWND-backed Win32/common-control node uses the same compact `win32:0x…`/`comctl:0x…` key in one-shot and persistent trees, derived from its HWND plus an architecture-neutral window-lifetime sentinel. Exact numeric HWND reuse receives a fresh key. When UIPI prevents sentinel installation, the node remains inspectable but receives no mutable provider handle. Logical common-control children use public-safe identities scoped to that lifetime: unique list/tab text fingerprints, a fingerprint of the documented tree-item handle, and unique toolbar command ids. Duplicate/unsafe identities, toolbar separators, and status-bar parts fall back to parent-local structural slots; their mutable indices are never promoted to durable identity. The provider's opaque session handle remains in `Element::providerHandle` only for property routing and is never substituted into the public key. Parsing an HWND key identifies a candidate; the connection's published-root allow-list still authorizes it. List and toolbar uniqueness scans cover the complete control, not only the first 50 children emitted in a dump. The scan is safety-bounded at 256 items; failure, timeout, or a larger control suppresses the durable logical identity and keeps property mutation read-only rather than claiming false uniqueness. Tabs apply the same bound while scanning every label. Tree item handles are intrinsically unique within their owning control, so output truncation does not weaken their identity proof. Status-part mutation safety remains separately guarded by index/count/text snapshot validation despite its structural key. Watch reconciliation treats a provider-supplied `durableIdentity` as authoritative. It is inherited only when equal and unique in both snapshots; change, loss, or ambiguity emits removal of the old key and addition of the freshly assigned key (including the corresponding subtree), rather than falling back to shape or position and silently retaining stale identity. List-view, tree-view, and tab text reads grow their capacity and retry up to the provider safety limit; setters reject values beyond that same readback limit before mutation. Tab reads request `TCIF_TEXT` only: `TCIF_PARAM` is intentionally avoided because `TCM_SETITEMEXTRA` replaces the `TCITEM` ABI with an application-defined `TCITEMHEADER`-based structure. Toolbar buttons require both a stable index and a unique command id. Status parts inspect the returned `SBT_*` type before any copy; `SBT_OWNERDRAW` is item data, never a string, so those parts are unavailable/read-only and their flag is never combined with a temporary text pointer. ### Persistent plugin connections Plugin ABI v2 maps its optional `lvt_connection_open` / `lvt_connection_get_tree` / `lvt_connection_close` exports onto the same interface. Core enables that path only when the complete lifetime group and `lvt_plugin_free` are present; event polling is a separate complete pair (`lvt_connection_poll_events` + `lvt_connection_events_free`). Framework detection carries an opaque token for the exact `LoadedPlugin` that reported it, so an alias differing from plugin metadata — or two plugins reporting the same framework name — cannot be resolved to the wrong DLL later. Registry keys include that plugin's loaded module and source path plus the target HWND, preventing collisions with built-ins, other plugin instances, and another window in the same process. Provider options such as Chromium's tab selector are forwarded on every persistent `get_tree`. An explicitly persistent lookup never falls back to one-shot injection when its connection is missing or dies. The refresh is marked incomplete, the old generation is released, and watch/MCP reacquire through `ConnectionRegistry`; generation checks keep stale handles from releasing a replacement. Watch re-runs framework detection before every snapshot, adding newly available v2 plugin slots and releasing obsolete ones before building that tick. MCP disconnect takes the per-target guard before erasing the session, so a read either finishes with its retained handle snapshot or observes a disconnected session — it cannot cross the boundary and one-shot inject. Plugins that expose only ABI v1, or an incomplete v2 group, remain on `lvt_enrich_tree` for every refresh and therefore stay backward-compatible. Both watch and MCP drain the provider-neutral event queue after successful snapshots; a persistent plugin without the optional poll pair still refreshes normally through `get_tree`. Plugin tree JSON is schema-checked before grafting and bounded by nesting/node limits. Event arrays are count-bounded, each record's `struct_size` is checked before reading it, and plugin-owned tree/event allocations are released on every success and failure path. Invalid persistent output marks that provider refresh incomplete and triggers normal reconnection rather than being accepted as an empty tree. ### Element ID assignment After the full tree is built, `assign_element_ids()` walks the tree in depth-first order and assigns IDs: `e0`, `e1`, `e2`, …. These IDs are: - Stable within a single invocation - Deterministic (same tree structure → same IDs) - Used by `--element` for subtree scoping and by screenshot annotations Durable `key` identity is separate. XAML/WinUI instance handles and managed WPF/WinForms handles use compact framework-qualified keys (`wpf:0x…`, `winforms:0x…`, and so on), remaining stable across refreshes on the same live target. Native HWND keys are also compact and connection-independent; only XAML/WinUI identities opt into process-wide reparent reconciliation. ### Bridge-to-XAML matching WinUI 3 apps use `DesktopChildSiteBridge` windows to host XAML content inside Win32 HWNDs. The XAML provider: 1. Collects all bridge elements from the Win32 tree (in enumeration order) 2. Collects XAML root elements from the TAP DLL output (in order) 3. Matches them 1:1 by position 4. Grafts each XAML subtree as children of the corresponding bridge element ## Stage 3 (alternate): UI Automation tree (`providers/uia_provider.cpp`) `--uia` swaps out Stage 3 entirely. Instead of the layered Win32-plus-providers build, it walks the target's UI Automation tree with an `IUIAutomation` client and returns the same `lvt::Element` type, so Stages 1 and 4 are untouched. ```mermaid flowchart LR A["Target"] --> B{"--uia?"} B -- no --> C["Framework detection\n+ layered providers"] B -- yes --> D["UiaProvider\nIUIAutomation client"] C --> E["Element tree"] D --> E E --> F["Serialization"] ``` Differences that matter: | | Visual tree | UIA tree | |---|---|---| | Mechanism | Native APIs + DLL injection | `IUIAutomation` client, no injection | | Architecture | Must match for injected providers; native-only cross-architecture is scalar/partial | Cross-architecture | | Identity | Class names, x:Name | `AutomationId`, `RuntimeId` | | Actionability | none | `SupportedPatterns` + pattern state | Three implementation constraints shape the provider: 1. **Everything goes through one cache request.** Each UIA property read is a cross-process call, so the provider builds an `IUIAutomationCacheRequest` with `TreeScope_Subtree` and every property and pattern it wants, calls `BuildUpdatedCache` once, then walks with `GetCachedChildren()` / `GetCachedPropertyValue()`. This is the difference between one round trip and thousands. 2. **Pattern-backed properties are read with the `Ex` accessor.** UIA's `GetCachedPropertyValue` substitutes the property type's *default* when an element does not support the owning pattern, so a `Window` answers `Toggle.ToggleState` with `2` (`ToggleState_Indeterminate`) exactly as a real checkbox answers `1`. Properties named `Pattern.Member` are therefore read with `GetCachedPropertyValueEx(id, ignoreDefaultValue=TRUE, …)`, whose reserved "not supported" return is the provider-authoritative signal — no property-to-pattern table needed. Core properties keep the plain accessor, because for them "not supported" also fires whenever a provider simply did not set the value, which would drop useful state such as `IsControlElement`. Separately, `uia_props.cpp` suppresses framework-specific "unset" sentinels (Win32 uses `0` where XAML uses `-1` for an unset `Level`). 3. **It runs in the MTA.** UIA clients want an MTA; `screenshot.cpp` initializes an STA on the calling thread. A thread cannot be both, so one-shot walks use `run_on_mta()`. Long-running watch/MCP sessions instead keep one dedicated MTA worker for the lifetime of `UiaConnection`; every reused-client tree read, typed-property mutation, event registration/drain, and COM release is dispatched to that same worker. The walk is bounded by `--uia-timeout`, which drives UIA's transaction timeout — the thing that actually bounds a wedged target, since every cross-process call happens inside the single `BuildUpdatedCache`. The per-element deadline check additionally limits the in-process traversal of the materialised cache; when it fires, the root carries a `Truncated` property so a consumer reading only the document can tell the tree is incomplete. It caps each provider response rather than total wall time, since one walk involves many responses. The 10s default sits comfortably above a real app's walk — the heaviest measured, a WebView2 host with ~380 elements, takes about 1.3s — while staying well under UIA's own 20s default. ## MCP server (`lvt_api.cpp` + `mcp/`) `lvt mcp` serves the Model Context Protocol over stdio from inside `lvt.exe`. It is not a fifth pipeline stage but a second front end: it drives the same target resolution, tree building and action machinery the CLI does, only with a session held open between requests instead of one process per command. It is split across a language boundary: ``` mcp/src/server.rs tool schemas, MCP protocol (Rust, rmcp) mcp/src/ffi.rs the only unsafe code (Rust) ↕ extern "C" — one function, JSON in, JSON out src/lvt_api.cpp sessions, dispatch, all logic (C++) src/providers/… unchanged ``` **Why Rust.** `lvt mcp` has to be served by `lvt.exe` itself — a second binary or a DLL would defeat the point of a single-executable release. That rules out running the MCP SDK as a separate process, and of the options that link into an existing MSVC binary, a Rust `staticlib` is the mainstream one. The alternative considered, C# NativeAOT with `NativeLib=Static`, is documented by Microsoft as implemented but unsupported. **Why the seam is one function.** `lvt_api_call(method, params_json, &result)` is the entire ABI. Adding a tool never changes it, the FFI surface is small enough to audit at a glance, and if the Rust layer ever needed replacing the C++ side would not move. **Memory ownership.** Rust always links the release CRT while lvt may be built against the debug one, so the two can genuinely have different heaps. The rule is therefore absolute: each side frees only what it allocated. `result_json` is malloc'd by lvt and released only through `lvt_api_free`, which on the Rust side is enforced by a `Drop` impl rather than by remembering to call it. **Neither runtime unwinds into the other.** `lvt_api_call` wraps its body so no C++ exception escapes; the Rust entry point uses `catch_unwind` and the crate is built with `panic = "abort"`. **stdout is the protocol.** Every diagnostic goes to stderr — this is why the WIL logging callback was made stderr-only before any of this was written. **Input gating** happens at registration, not at call time: `--allow-input` selects which tool routers are composed, so a withheld tool is absent from `tools/list` and not merely refused. **Sessions** live in `lvt_api.cpp` behind a mutex, keyed `s1`, `s2`, …. Each holds a resolved HWND, pid and architecture, so later calls skip target resolution — which is ambiguous when several windows match a process name. ## Stage 4: Serialization (`json_serializer.cpp`, `screenshot.cpp`) ### JSON output Standard JSON with `target` metadata, `frameworks` array, and `root` element tree. Uses nlohmann/json for serialization. ### XML output XML markup where each element's type becomes the tag name. Attributes include `id`, `framework`, `className`, `text`, `bounds`, and any framework-specific properties. ### Screenshot capture Uses `Windows.Graphics.Capture` APIs: 1. Create a `GraphicsCaptureItem` from the target HWND 2. Capture a frame via `Direct3D11CaptureFramePool` 3. Convert to a CPU-accessible bitmap 4. Annotate with bounding boxes and element ID labels using GDI 5. Fix alpha channel (GDI doesn't set alpha; must post-process to set all alpha bytes to 255) 6. Encode to PNG via `WICBitmapEncoder` Annotation skips elements with zero bounds. XAML element bounds are computed from the bridge window's screen position plus per-element offsets and dimensions. ## Element model (`element.h`) ```cpp struct Bounds { int x = 0, y = 0, width = 0, height = 0; }; struct Element { std::string id; // "e0", "e1", ... std::string type; // Friendly name ("Button", "StackPanel") std::string framework; // "win32", "comctl", "xaml", "winui3" std::string className; // Full class/type name std::string text; // Visible text or accessible name Bounds bounds; // Screen coordinates std::map properties; std::vector children; uintptr_t nativeHandle; // Pointer-sized native identity (e.g. HWND) uint64_t providerHandle; // Fixed-width provider object identity }; ``` ## Dependencies | Dependency | Purpose | Source | |-----------|---------|--------| | WIL | Smart pointers, error handling | vcpkg | | nlohmann/json | JSON serialization | vcpkg | | GoogleTest | Unit and integration tests | vcpkg | | Windows SDK | Win32 APIs, XAML Diagnostics, Graphics.Capture | System | | C++/WinRT | WinRT APIs (Graphics.Capture, BitmapEncoder) | Windows SDK |