A Windows CLI tool that inspects the visual tree of running applications. Designed for AI agents (e.g. GitHub Copilot) that need a textual representation of an app's UI content.
- Targets any running Windows app by HWND, PID, process name, or window title
- Detects UI frameworks in use: Win32, ComCtl, Windows XAML (UWP), WinUI 3, WPF, Avalonia, Chrome/Edge
- Outputs a unified element tree as JSON or XML markup
- Captures annotated PNG screenshots with element IDs overlaid
- Elements get stable IDs (
e0,e1, …) so AI agents can reference specific parts of the UI
Grab the latest release from GitHub Releases — extract the zip and run lvt.exe from any terminal.
The easiest way to add the lvt skill to GitHub Copilot CLI is to install it as a plugin:
/plugin install asklar/lvt
This gives Copilot the ability to inspect any running Windows app's UI when you ask it to. Verify with /skills list.
- Visual Studio 2022+ (C++ Desktop workload)
- vcpkg with
VCPKG_ROOTenvironment variable set - CMake 3.20+
- x64 Developer Command Prompt
# x64 build (default)
cmake --preset default
cmake --build build
# ARM64 build
cmake --preset arm64
cmake --build build-arm64Produces build/lvt.exe and build/lvt_tap_x64.dll (or build-arm64/lvt.exe and build-arm64/lvt_tap_arm64.dll for ARM64).
Note: lvt.exe must match the target process's architecture. If you target an ARM64 app, use the ARM64 build. A clear error message is shown on mismatch.
Framework support is per-provider, so you can drop pieces you don't need. Win32 and ComCtl are always built — every other provider enriches that base layer.
| Option | Default | Effect |
|---|---|---|
LVT_ENABLE_XAML |
ON | System XAML (UWP / DesktopWindowXamlSource) |
LVT_ENABLE_WINUI3 |
ON | WinUI 3 (Windows App SDK) |
LVT_ENABLE_WPF |
ON | WPF |
LVT_ENABLE_WINFORMS |
ON | WinForms |
LVT_ENABLE_AVALONIA |
ON | Avalonia plugin |
LVT_ENABLE_CHROMIUM |
ON | Chromium (Chrome/Edge DOM) plugin |
LVT_BUILD_TOOL |
ON | Build the lvt CLI as well as the library |
LVT_BUILD_MANAGED |
ON | Build the managed .NET helper assemblies |
LVT_BUILD_TESTS |
ON | Build the test executables |
LVT_BUILD_MANAGED is the only thing that requires the .NET SDK. WPF, WinForms and Avalonia each have a native half that hosts the CLR plus a managed tree-walker assembly; only the latter needs dotnet. With -DLVT_BUILD_MANAGED=OFF the whole native build still works, including those TAP DLLs — you just lose managed enrichment for those three frameworks. XAML and WinUI 3 are pure C++ either way.
The XAML and WinUI 3 providers need C++/WinRT headers. These come from two places:
winrt/base.hand theWindows.*projection — from thecppwinrtvcpkg port- the
Microsoft.*(WinUI 3) projection — generated at configure time intosrc/tap/winui3/from the Windows App SDK NuGet package, which has no vcpkg port
Both halves must be produced by the same cppwinrt version, or the generated headers fail to compile against base.h's macros ("Mismatched C++/WinRT headers"). Taking the generator from the cppwinrt port rather than the Windows SDK keeps them in lockstep — and because vcpkg installs only one version of a port per triplet, a consumer that already depends on cppwinrt picks the version for both.
The generated projection is cached in src/tap/winui3/ (gitignored) and regenerated whenever the generator or the Windows App SDK inputs change, tracked via src/tap/winui3/.cppwinrt-signature. Two escape hatches:
# Use a specific generator or Windows App SDK package
cmake --preset default -DLVT_CPPWINRT_EXE=... -DLVT_WASDK_WINMD_DIR=...
# Force a full regeneration
Remove-Item -Recurse src/tap/winui3lvt installs a CMake package exposing lvt::core, the same library the CLI is built on.
This repository doubles as a vcpkg registry. Add a vcpkg-configuration.json next to your vcpkg.json:
{
"default-registry": {
"kind": "git",
"repository": "https://github.com/microsoft/vcpkg",
"baseline": "<a microsoft/vcpkg commit sha>"
},
"registries": [
{
"kind": "git",
"repository": "https://github.com/asklar/lvt",
"baseline": "<an asklar/lvt commit sha>",
"packages": [ "lvt" ]
}
]
}then depend on it from your vcpkg.json:
{
"dependencies": [
{ "name": "lvt", "features": [ "winui3", "wpf" ] }
]
}Features map one-to-one onto the framework options above: xaml, winui3, wpf, winforms, avalonia, chromium, plus tools for the CLI. All but avalonia are on by default. The port always builds with LVT_BUILD_MANAGED=OFF, because vcpkg builds have neither network access nor a .NET SDK — see ports/lvt/usage.
find_package(lvt CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE lvt::core)
# lvt looks for its injectable TAP DLLs next to the binary it is linked into,
# so copy them beside your executable:
lvt_copy_tap_dlls(my_app)#include <lvt/target.h>
#include <lvt/framework_detector.h>
#include <lvt/tree_builder.h>
#include <lvt/json_serializer.h>
#include <lvt/lvt_config.h> // which frameworks were compiled in
auto matches = lvt::find_by_process_name("notepad");
auto target = lvt::resolve_target(matches[0].hwnd, matches[0].pid);
auto frameworks = lvt::detect_frameworks(target.hwnd, target.pid);
auto tree = lvt::build_tree(target.hwnd, target.pid, frameworks);
lvt::assign_element_ids(tree);Instead of lvt_copy_tap_dlls() you can call lvt::set_tap_directory() at runtime, or point the LVT_TAP_DIR environment variable at the installed tools directory. Plugins are searched for in $LVT_PLUGIN_DIR, then <module dir>/plugins, then %USERPROFILE%\.lvt\plugins.
# Dump Notepad's visual tree as JSON
lvt --name notepad
# XML output
lvt --name notepad --format xml
# Capture annotated screenshot
lvt --pid 1234 --screenshot out.png
# Just detect frameworks
lvt --hwnd 0x1A0B3C --frameworks
# Scope to a subtree
lvt --name myapp --element e5 --depth 3
# Query an element by durable key or eN id
lvt --name myapp --query "win32|Window|MyWindow/win32|Button|Button|Name:OK" text
# Screenshot + tree dump together
lvt --name notepad --screenshot out.png --dump
# Watch for live tree changes as JSON diff events
lvt --name notepad --watch --interval 250| Flag | Description |
|---|---|
--hwnd <handle> |
Target window by HWND (hex) |
--pid <pid> |
Target process by PID |
--name <exe> |
Target by process name (e.g. notepad or notepad.exe) |
--title <text> |
Target by window title substring |
--output <file> |
Write tree to file instead of stdout |
--format <fmt> |
json (default) or xml |
--screenshot <file> |
Capture annotated screenshot to PNG |
--dump |
Output the tree (default unless --screenshot is used) |
--watch |
Emit live JSON tree diff events until Ctrl+C |
--interval <ms> |
Polling interval for --watch (default: 500) |
--element <ref> |
Scope to a specific element subtree by positional eN id or durable key |
--query <ref> [prop] |
Output an element, or one property, by positional eN id or durable key |
--frameworks |
Just list detected frameworks |
--depth <n> |
Max tree traversal depth |
--watch repeatedly rebuilds the target tree and writes newline-delimited JSON
events to stdout. The first tick emits the current tree as added events; later
ticks emit added, removed, and changed events with old/new field values.
Element matching uses stable framework/type/class/path-derived keys instead of
the positional e0, e1, ... ids, so unique moved elements are reported as
changed events with a path field change.
{
"target": { "hwnd": "0x001A0B3C", "pid": 12345, "processName": "Notepad.exe" },
"frameworks": ["win32", "winui3"],
"root": {
"id": "e0",
"type": "Window",
"framework": "win32",
"className": "Notepad",
"text": "Untitled - Notepad",
"bounds": { "x": 100, "y": 100, "width": 800, "height": 600 },
"children": [
{
"id": "e1",
"type": "ContentPresenter",
"framework": "winui3",
"bounds": { "x": 108, "y": 140, "width": 784, "height": 552 }
}
]
}
}<LiveVisualTree hwnd="0x001A0B3C" pid="12345" process="Notepad.exe" frameworks="win32,winui3">
<Window id="e0" framework="win32" className="Notepad" text="Untitled - Notepad" bounds="100,100,800,600">
<ContentPresenter id="e1" framework="winui3" bounds="108,140,784,552" />
</Window>
</LiveVisualTree>The tool uses a 4-stage pipeline:
- Target resolution — resolve HWND/PID/name/title to a target window
- Framework detection — enumerate loaded DLLs to detect UI frameworks
- Tree building — Win32 HWND walk as base, framework providers layer on top
- Serialization — output as JSON/XML, optionally capture screenshot
Framework providers:
- Win32Provider — base HWND tree (always present)
- ComCtlProvider — enriches ComCtl32 controls (ListView items, TreeView nodes, etc.)
- XamlProvider — injects TAP DLL to walk Windows XAML visual trees
- WinUI3Provider — injects TAP DLL to walk WinUI 3 visual trees
- WpfProvider — walks WPF visual trees via managed DLL injection
- Plugins — extensible framework support (e.g. Avalonia) via C ABI plugin interface
See docs/architecture.md for details.
- No UI Automation — uses framework-native APIs directly for speed and accuracy
- Graceful degradation — if a framework provider fails, falls back to HWND-level info
- AI-first — output formats and element IDs designed for machine consumption
- Minimal footprint — single exe + one DLL, no installers, no runtime dependencies
# Run unit tests
build\lvt_unit_tests.exe
# Run integration tests (launches Notepad automatically)
build\lvt_integration_tests.exe
# Via CTest
ctest --test-dir buildlvt supports a plugin architecture for adding new framework providers. Plugins are DLLs that implement a simple C interface and are loaded automatically from %USERPROFILE%\.lvt\plugins\.
See src/plugin.h for the plugin interface.
| Plugin | Framework | Docs |
|---|---|---|
| Avalonia | Avalonia UI desktop apps | docs/avalonia-plugin.md |
| Chromium | Chrome/Edge browser DOM trees | docs/chromium-plugin.md |
These plugins are built from source alongside lvt and deployed to %USERPROFILE%\.lvt\plugins\. See each plugin's documentation for installation and usage details.
- WinForms provider
- WebView2 provider
- MAUI provider
- Element property querying (
--query <id> <property>) - Accessibility tree correlation
- Watch mode for live tree diffing
MIT — see LICENSE.

