Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
5ee9620
Add lvt Viewer: a WPF graphical live visual tree GUI
asklar Aug 21, 2026
a475c29
Wire lvt Viewer into the CMake build behind LVT_BUILD_VIEWER
asklar Aug 21, 2026
5608da2
Document lvt Viewer in the root README
asklar Aug 21, 2026
9c024a4
Update viewer README: crosshair-drag verified live with a real mouse
asklar Aug 21, 2026
915cea5
Fix lvt Viewer: copy TAP DLLs and plugins alongside lvt.exe, not just…
asklar Aug 21, 2026
80f45fa
Fix real UI-thread hang and cross-window content bug in XAML diagnostics
asklar Aug 21, 2026
56dd92c
Broaden XAML property capture beyond the curated text/state list
asklar Aug 21, 2026
6b68501
Add viewer features: highlight, point-to-select, tree labels, type fi…
asklar Aug 21, 2026
a7cacf2
Fix viewer crashing on every launch (Owner set before window shown)
asklar Aug 21, 2026
d475609
Hide the highlight overlay when the watched target disappears
asklar Aug 21, 2026
f45a452
Add a slow-connect status hint for large/complex UI trees
asklar Aug 22, 2026
b44f6b7
Fix the real cause of watch appearing stuck: two compounding bugs
asklar Aug 22, 2026
619d8d4
Fix point-to-select hit-testing and add an element search bar
asklar Aug 22, 2026
a7a5e04
Chunk XAML property/bounds collection to stop monopolizing the target…
asklar Aug 22, 2026
48b3c91
Add Find Previous, hide highlights on minimize, clarify disabled cros…
asklar Aug 22, 2026
c70fc61
Fix highlight overlay landing in the wrong place on scaled displays
asklar Aug 22, 2026
f98a313
Fix highlight DPI conversion applying to the wrong coordinate source
asklar Aug 22, 2026
aa1d25c
Fix highlight not visually following the target until the viewer rega…
asklar Aug 22, 2026
0f72a50
Shrink durable element keys by dropping the type/className redundancy
asklar Aug 22, 2026
ded1b46
Add millisecond timestamps and phase checkpoints to the TAP DLL debug…
asklar Aug 22, 2026
360dea7
Add --fast: skip the XAML/WinUI3 property-chain walk for a much quick…
asklar Aug 22, 2026
0d5ffde
Wire --fast through MCP get_visual_tree and the viewer's live tree
asklar Aug 22, 2026
1a89d93
Add file-based diagnostic logging to the viewer
asklar Aug 22, 2026
ee1fe4a
Fix tree navigation resetting: TreeViewItem.IsExpanded was OneWay
asklar Aug 22, 2026
0e1613c
Fix watch exiting (disabling the crosshair) on one transient connect …
asklar Aug 22, 2026
4ccab5b
Fix the real cause of "the tree refreshes as I navigate": one rebuild…
asklar Aug 22, 2026
4827ad6
Make LiveTree fully incremental — never recompute the whole hierarchy
asklar Aug 22, 2026
a66c794
Fix crosshair window picking: Z-order/visibility-aware hit test + Esc…
asklar Aug 24, 2026
0252208
Fix "the tree rebuilds while navigating": watch had its own, divergen…
asklar Aug 24, 2026
03b07f6
Fix persistent tree churn: watch now reconciles identity across ticks
asklar Aug 24, 2026
091c32d
Retry XAML injection per-tick, not just on watch's first connect
asklar Aug 24, 2026
7acbaab
Hold last-known-good XAML content across a transient injection outage
asklar Aug 24, 2026
438e2c3
Fix the actual root cause of tree data loss: a too-short connect timeout
asklar Aug 25, 2026
9025364
Replace per-tick XAML/WinUI3 reinjection with a persistent connection
asklar Aug 25, 2026
758c2bf
docs: update TAP DLL design and architecture docs for persistent conn…
asklar Aug 25, 2026
a8f102b
Add optional plugin ABI v2: persistent connections for framework plugins
asklar Aug 25, 2026
5447ac2
Add incremental push: TAP proactively streams Add/Remove CHANGE events
asklar Aug 25, 2026
790c29b
Fix WinUI3 C++/WinRT projection detection for newer NuGet layouts
asklar Aug 25, 2026
c3ddac5
Reuse one UIA automation client across watch and MCP sessions
asklar Aug 25, 2026
578748e
Refresh dead framework connections in watch/MCP instead of falling ba…
asklar Aug 25, 2026
39caa4c
Make viewer highlight overlay an owned window instead of Topmost
asklar Aug 25, 2026
a2063a6
Stop watch/MCP from silently falling back to one-shot reinjection
asklar Aug 25, 2026
27eac1e
Ignore stale watch-process Exited notifications during reconnect
asklar Aug 26, 2026
fdffd22
Fix highlight overlay z-order with occlusion polling + SWP_NOOWNERZORDER
asklar Aug 26, 2026
71e613e
Retry a framework connection whose very first acquire() attempt failed
asklar Aug 26, 2026
63b7763
Fix crosshair picking an unrelated large window instead of our own UI
asklar Aug 26, 2026
53820c6
Ignore the click-through overlay during crosshair hit testing
asklar Aug 27, 2026
82365e4
Show connection progress and add Ctrl+F in the viewer
asklar Aug 27, 2026
7c216a4
Add standard find navigation shortcuts
asklar Aug 27, 2026
cd63989
Remove redundant XAML startup collection
asklar Aug 27, 2026
8effdcc
Use compact XAML instance-handle keys
asklar Aug 27, 2026
d8122ff
Decode lvt subprocess output as UTF-8
asklar Aug 27, 2026
f6cc278
Add lvt Viewer application icon
asklar Aug 27, 2026
a18fd90
Wire the viewer application icon
asklar Aug 27, 2026
55400c5
Reset TAP snapshot data before each tree refresh
asklar Aug 27, 2026
8d1200d
Show private-use glyphs as Unicode code points
asklar Aug 27, 2026
4c92211
Fix installed headers and compact MCP reference classification
asklar Aug 27, 2026
1049b6c
Support system XAML desktop islands
asklar Aug 27, 2026
68245f6
Harden persistent connection teardown and plugin contracts
asklar Aug 27, 2026
ec89db6
Publish the viewer as a separate release asset
asklar Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/skills/lvt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,10 @@ Pattern state is only emitted where the pattern is supported, so the presence of
2. **Run `lvt --name <app> --format xml`** to get a quick overview of the UI tree
3. **Take a screenshot** with `lvt screenshot --name <app> --output ui.png` to see the visual layout with element IDs
4. **Drill into a subtree** with `--element <id> --depth <n>` if the tree is large
5. **Use element IDs and bounds** to plan any UI interactions (clicks, keyboard input)
5. **Add `--fast`** on a rich XAML/WinUI3 app if `dump`/`watch` feels slow — it
skips the full property-chain walk in favor of cheap bounds/Text/Content/
basic-state reads, at the cost of not reporting arbitrary custom properties
6. **Use element IDs and bounds** to plan any UI interactions (clicks, keyboard input)

## MCP server mode

Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@ jobs:
preset: default
build_dir: build
build_tests: 'ON'
build_viewer: 'ON'
rust_target: x86_64-pc-windows-msvc
- arch: x86
msvc_arch: amd64_x86
preset: x86
build_dir: build-x86
build_tests: 'OFF'
build_viewer: 'OFF'
rust_target: i686-pc-windows-msvc
- arch: arm64
msvc_arch: amd64_arm64
preset: arm64
build_dir: build-arm64
build_tests: 'OFF'
build_viewer: 'OFF'
rust_target: aarch64-pc-windows-msvc

steps:
Expand All @@ -64,13 +67,20 @@ jobs:
- name: Set up Rust (for the MCP server)
run: rustup target add ${{ matrix.rust_target }}

- name: Set up .NET 10 (for the x64 viewer)
if: matrix.build_viewer == 'ON'
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

# Tests only run on x64, and gtest now sits behind a vcpkg manifest
# feature, so the other legs can skip building it entirely.
- name: Configure
run: >
cmake --preset ${{ matrix.preset }}
-DCMAKE_BUILD_TYPE=Release
-DLVT_BUILD_TESTS=${{ matrix.build_tests }}
-DLVT_BUILD_VIEWER=${{ matrix.build_viewer }}
-DLVT_ENABLE_MCP=ON

- name: Build managed WPF assembly
Expand Down Expand Up @@ -128,12 +138,40 @@ jobs:
Copy-Item .github\skills\lvt\SKILL.md release\skills\lvt\
Compress-Archive -Path release\* -DestinationPath lvt-${{ env.RELEASE_TAG }}-${{ matrix.arch }}.zip

# Keep the normal CLI archive lean. The viewer has a .NET Desktop
# Runtime dependency and is published as a separate, version-matched
# x64 asset. It still includes the complete matching lvt runtime so
# users never have to combine two archives by hand.
- name: Package viewer
if: matrix.build_viewer == 'ON'
run: |
if (!(Test-Path ${{ matrix.build_dir }}\viewer\LvtViewer.exe)) {
throw "The viewer build output was not produced."
}
New-Item -ItemType Directory -Path viewer-release -Force
Copy-Item -Recurse ${{ matrix.build_dir }}\viewer\* viewer-release\
Copy-Item src\viewer\RELEASE-README.txt viewer-release\README.txt
Copy-Item LICENSE viewer-release\
# Debug symbols and incremental-link files are useful CI artifacts,
# but not runtime dependencies and add substantial release weight.
Get-ChildItem viewer-release -Recurse -File -Include *.pdb,*.ilk,*.locked* |
Remove-Item -Force
Compress-Archive -Path viewer-release\* `
-DestinationPath lvt-viewer-${{ env.RELEASE_TAG }}-x64.zip

- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.arch }}
path: lvt-${{ env.RELEASE_TAG }}-${{ matrix.arch }}.zip

- name: Upload viewer release artifact
if: matrix.build_viewer == 'ON'
uses: actions/upload-artifact@v4
with:
name: release-viewer-x64
path: lvt-viewer-${{ env.RELEASE_TAG }}-x64.zip

publish:
runs-on: ubuntu-latest
needs: release
Expand Down
239 changes: 234 additions & 5 deletions CMakeLists.txt

Large diffs are not rendered by default.

40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ A Windows CLI tool that inspects the visual tree of running applications. Design

### Download

Grab the latest release from **[GitHub Releases](https://github.com/asklar/lvt/releases/latest)** — extract the zip and run `lvt.exe` from any terminal.
Grab the latest release from **[GitHub Releases](https://github.com/asklar/lvt/releases/latest)**.
The `lvt-vX.Y.Z-<arch>.zip` assets are the lean command-line packages; extract
one and run `lvt.exe` from any terminal. The graphical viewer is published
separately as `lvt-viewer-vX.Y.Z-x64.zip`.

### Install the Copilot skill

Expand Down Expand Up @@ -186,7 +189,7 @@ lvt frameworks --hwnd 0x1A0B3C
lvt dump --name myapp --element e5 --depth 3

# Query an element by durable key or eN id
lvt query "win32|Window|MyWindow/win32|Button|Button|Name:OK" text --name myapp
lvt query "win32|MyWindow/win32|Button|Name:OK" text --name myapp

# Watch for live tree changes as JSON diff events
lvt watch --name notepad --interval 250
Expand Down Expand Up @@ -222,6 +225,7 @@ lvt wait-for e9 --wait-prop IsEnabled=true --name myapp
| `--output <file>` | Write to a file instead of stdout, or the PNG path for `screenshot` |
| `--format <fmt>` | `json` (default) or `xml` |
| `--interval <ms>` | Polling interval for `watch` (default: 500) |
| `--fast` | Skip the XAML/WinUI3 property-chain walk (`GetPropertyValuesChain`) in favor of cheap direct property reads. Much faster on a rich tree — still reports bounds, `Text`, `Content`, and basic state, but not arbitrary custom properties. Default is off (today's exhaustive collection) |
| `--element <ref>` | Scope to a specific element subtree by positional `eN` id, durable key, or `uia:<RuntimeId>` |
| `--uia` | Use the UI Automation tree instead of the visual tree |
| `--uia-view <view>` | UIA tree view: `control` (default), `raw`, or `content` |
Expand Down Expand Up @@ -400,6 +404,33 @@ for the full tool reference and the security model.
Building it from source needs a Rust toolchain and is opt-in
(`-DLVT_ENABLE_MCP=ON`); released binaries have it built in.

## lvt Viewer

A graphical, live element-tree browser for Windows — think Visual Studio's
Live Visual Tree or the Windows SDK's Inspect.exe. Drag a crosshair onto a
window to target it; a tree on one side and a property panel on the other
both update live as the target's UI changes.

Download `lvt-viewer-vX.Y.Z-x64.zip` from the matching
[GitHub release](https://github.com/asklar/lvt/releases/latest), extract the
whole archive, and run `LvtViewer.exe`. The archive contains the matching x64
CLI, TAP DLLs, managed walkers, and plugins; it requires the
[.NET 10 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/10.0).

To build it from source instead:

```powershell
cmake --preset default -DLVT_BUILD_VIEWER=ON
cmake --build build
.\build\viewer\LvtViewer.exe
```

It's a separate WPF (.NET) app that drives `lvt.exe` as a subprocess (`watch`
for live updates, `toggle`/`set-value` for editing) rather than linking
`lvt_core`. See **[src/viewer/README.md](src/viewer/README.md)** for the
architecture, why `watch` was chosen over MCP for live updates, and how to
build/run it.

## Output format

### Watch mode
Expand All @@ -411,6 +442,11 @@ 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.

`--fast` applies to `watch` too: every tick collects the cheaper property set
instead of the full XAML/WinUI3 property chain, so `changed` events on an
arbitrary custom property outside bounds/Text/Content/basic state won't be
reported — only those properties are tracked and diffed in fast mode.

### JSON

```json
Expand Down
8 changes: 7 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ flowchart BT

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 via named pipe, and graft XAML subtrees into matching `DesktopChildSiteBridge` elements in the Win32 tree.
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.

### 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()`.

Only XamlProvider and WinUI3Provider implement this today (they are the only frameworks with a real `AdviseVisualTreeChange`-equivalent API); other providers/plugins can adopt the same interface later without changing how callers acquire or use it.

### Element ID assignment

Expand Down
14 changes: 14 additions & 0 deletions docs/avalonia-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ $ lvt --name AvaloniaTestApp --format xml --depth 3
- Target process must match lvt's architecture (x64 or ARM64)
- The .NET runtime (`hostfxr.dll`) must be installed on the system

## Persistent connections (optional)

`src/plugin.h`'s plugin ABI (v2) has a persistent lifetime group:
`lvt_connection_open`, `lvt_connection_get_tree`, and
`lvt_connection_close` (plus the existing `lvt_plugin_free`) must all be
implemented before lvt enables the connection path. The optional
`lvt_connection_poll_events`/`lvt_connection_events_free` pair adds push
event draining. These functions let
`watch` and MCP sessions reuse one connection across many tree refreshes instead of
re-injecting every time — see that header's "Persistent connections" section and
`docs/tap-dll-design.md`'s connection lifecycle for the pattern the built-in XAML/WinUI3
providers already follow. This plugin does not implement them yet; it still uses the
original one-shot `lvt_enrich_tree` path, which continues to work unchanged either way.

## Test app

A simple Avalonia test application is included in `tests/avalonia_test_app/`:
Expand Down
11 changes: 11 additions & 0 deletions docs/chromium-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,14 @@ lvt --name chrome
- WebView2 support (Chrome embedded in Win32 apps)
- Lazy loading for very large DOM trees
- Chrome Web Store / Edge Add-ons publication
- Persistent connections: `src/plugin.h`'s plugin ABI (v2) enables this path
only when `lvt_connection_open`, `lvt_connection_get_tree`,
`lvt_connection_close`, and the existing `lvt_plugin_free` are all
implemented. The optional `lvt_connection_poll_events`/
`lvt_connection_events_free` pair adds event draining. These let `watch` and MCP
sessions reuse one connection across many refreshes instead of
re-establishing the extension/native-messaging channel every time — see
that header and `docs/tap-dll-design.md`'s connection lifecycle for the
pattern the built-in XAML/WinUI3 providers already follow. This plugin
does not implement them yet; it still uses the one-shot `lvt_enrich_tree`
path, which continues to work unchanged either way.
21 changes: 17 additions & 4 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ input at where an element is. Because it works by injecting into the target it
needs lvt and the target to share an architecture; when they do not, it says so
and names the right binary.

On a rich XAML/WinUI3 tree, `get_visual_tree` walks every element's entire
property inheritance chain by default (`IVisualTreeService::
GetPropertyValuesChain`) — measured at ~4.5ms/element on a real app, which adds
up on a tree of hundreds or thousands of elements. Pass `fast: true` to skip
that walk and collect bounds/`Text`/`Content`/basic state the cheaper way
instead (a few direct property reads per element, no property-chain walk).
This is enough to browse or search a tree by, and to hit-test/highlight
elements, but it will not report arbitrary custom properties the way the
default (`fast: false`) walk does — use `get_element_properties` for a single
element's exhaustive property set regardless of which mode built the tree.

## Addressing elements

Every tool that takes an element accepts these forms:
Expand All @@ -127,8 +138,10 @@ Every tool that takes an element accepts these forms:
so it can be checked rather than assumed.
- **`e12`** — the element's position in the tree you fetched, read against the
session's own tree.
- **A durable key** — a path-based identifier that survives more change. Also
self-describing: it names the framework that produced it.
- **A durable key** — a framework-native identifier that survives more change.
XAML/WinUI3 use compact diagnostics handles (`xaml:0x…`, `winui3:0x…`);
providers without a process-wide handle use a structural path. Both forms are
self-describing.
- **`uia:<RuntimeId>`** — the UIA runtime identifier.

**A session only accepts references from its own tree.** The other tree's are
Expand Down Expand Up @@ -287,8 +300,8 @@ the other's references** rather than guessing what you meant. If you want to
work the other way round, open a second session — they are independent and cheap.

Durable keys are self-describing — they name the framework that produced them
(`wpf|…`, `uia|…`) — so they need no qualifier, and they are refused by the
wrong session just as `eN` refs are.
(`winui3:0x…`, `wpf|…`, `uia|…`) — so they need no qualifier, and they are
refused by the wrong session just as `eN` refs are.

## Prefer patterns over synthetic input

Expand Down
Loading
Loading