| name | reactor-devtools |
| description | Driving a running Reactor app via `mur devtools` — screenshot, inspect visual tree, click/type/scroll, read hook state. Use when diagnosing visible bugs (layout, contrast) or verifying a UI change landed. |
Reactor Devtools — CLI-driven UI automation
The Reactor devtools let you look at a running debug-build app the way a
user does (screenshots, rendered text, layout bounds) and drive it the way
a user does (click, type, toggle, scroll). Always prefer the mur devtools
CLI — every action composes with shell pipes and jq, each invocation is
a complete audit record, and no MCP client setup is needed.
Under the hood the CLI talks JSON-RPC to a loopback HTTP endpoint the app
exposes. The MCP endpoint is still available at http://127.0.0.1:PORT/mcp
as a parity escape hatch, but reach for it only when the CLI can't express
what you need (structured args the CLI flattens, or another MCP client
that's already wired up).
Loopback-only — DEBUG builds only, never ship it. Auth is a per-launch
bearer token written into the lockfile; the CLI applies it transparently
during lockfile discovery, so you don't see it. Don't pass --endpoint
for tool calls — without the token the server returns 401. --endpoint
is only useful when you're crafting the Authorization: Bearer … header
yourself (curl, custom client).
Getting mur on your PATH
mur resolves from PATH in both modes:
- Skill kit (deployed):
install-skill-kit.ps1 (shipped with the kit zip)
prepends <install>/bin/<arch> to your user PATH.
- Cloned repo (selfhost):
dotnet build of src/Reactor.Cli automatically
mirrors the output to <repo>/bin/<arch>/ (architecture is determined from
RuntimeIdentifier). Add that directory to your PATH once.
If mur --version doesn't resolve, neither path was set up; commands below assume it does.
Attaching to a running app
The app author enables devtools capability in the app project, usually only for Debug builds:
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<RuntimeHostConfigurationOption Include="Reactor.DevtoolsSupport"
Value="true" Trim="true" />
</ItemGroup>
ReactorApp.Run takes no devtools: or preview: argument.
Prefer attaching to an app that's already running. Any devtools-enabled
app writes a lockfile to %TEMP%/reactor-devtools/ on first render, and
mur devtools <verb> discovers it automatically — no port, no config,
no parsing stdout.
mur devtools session list --pretty
Exit 0 with a row means you're good to go; every other verb will find it.
Exit 4 means no live session — you need to launch one yourself (next
section).
Launching an app yourself
Two launch modes, from simplest to most featured:
Plain dotnet run (the default)
dotnet run --project path/to/App.csproj -- --devtools run \
> /tmp/app-stdout.log 2>&1 &
This is what you want in almost every AI session: spawn the app, drive it
with mur devtools <verb>, call mur devtools shutdown when you're done.
No supervisor machinery, no pinned port — the CLI finds the session via
the lockfile regardless.
mur devtools <project> — the supervisor
Reach for this only when you need a stable MCP endpoint across
reload cycles:
mur devtools path/to/App.csproj --mcp-port 54931
The supervisor pins the port (reload-proof) and catches the child's exit
code 42 to rebuild and relaunch. Useful if you've wired an external MCP
client to http://127.0.0.1:54931/mcp and want it to survive code
edits. Overkill for one-shot automation.
Discovering the tool surface
mur devtools call tools/list --pretty
mur devtools call tools/list | jq '.tools[].name'
mur devtools --help lists every named verb with one-line descriptions.
CLI verb catalog
Each verb attaches to the running session via lockfile discovery; no flags
needed when only one session is active. Pass --pretty to any verb for
indented JSON.
| Verb | What it does |
|---|
version | Build tag + pid + port — confirm the app is the one you expect. |
components | Class names of every Component subclass; marks which is mounted. |
switch <name> | Swap the root component by class name. Invalidates all node ids. |
reload [--component N] | Rebuild + relaunch via the supervisor sentinel. Old ids dead. |
shutdown | Close the app cleanly (supervisor exits 0). Releases the build output file lock. |
windows | Active window ids, titles, bounds, currently-mounted component. |
windows.list | Spec 036 §10. Per-window id, key, title, DIP size, DPI, state, isMain. Use this when you care about DPI / DIP size or the key column for UseOpenWindow-keyed lookups. |
windows.activate <id> | Activate (focus) a window. Returns { ok, id }. |
windows.close <id> | Close a window. Honors UseClosingGuard / Closing subscribers — returns { ok: false, cancelled: true, id } when a guard vetoed the close. |
windows.open <Component> [--title T] [--width W] [--height H] [--key K] | Open a new top-level window mounting an allowlisted Component. The component name is gated by the same allowlist as switchComponent; rejected names return unknown-component with the available list. |
tree [--selector S] [--window W] [--view summary|full] | Dump the visual tree as JSON. full adds layout/automation/visual fields. |
screenshot [--selector S] [--out path] | PNG of the window (or selector-cropped region). --out path.png writes to file; --out - streams bytes to stdout. |
click <selector> | UIA click. Prefers Invoke → Toggle → SelectionItem; returns . |
Reference-graph overlay
The references tool (Spec 057) returns the app's reactive reference
graph — descriptor.Reference/.ReferenceList, the binding.Reference
bridge, and modifier edges like .LabeledBy/.DescribedBy/.FlowsTo/
.XYFocus* — as {from, to, label, slot, kind, resolved} edges keyed to
the same node ids tree uses, plus diagnostics for cycles and
unresolved (perpetually-null) references. Reach it through the
generic passthrough:
mur devtools call references --pretty
mur devtools call references --args '{"selector":"#login-form"}'
Use it to confirm an accessibility relationship (LabeledBy/DescribedBy)
or an XYFocus* ring actually resolved to the control you expect, or to
spot a reference that never resolved. Cycles are a supported topology and
are reported informationally, not as errors. The same overlay also backs
the References toggle in the VS Code live-preview panel.
Session management
mur devtools session list
mur devtools session list --pretty
mur devtools session clean
mur devtools session clean --dry-run
Shared flags (before any verb)
--endpoint <url> — skip lockfile discovery and talk to this endpoint.
Drops the bearer token — the CLI has no --token flag, so verbs hit
the endpoint unauthenticated and get 401. Only useful with curl plus a
hand-built Authorization header, not for mur devtools <verb> calls.
--pretty — indent JSON output.
--auto — loopback port scan (slow; use only when lockfile discovery fails).
Exit codes
| Code | Meaning |
|---|
| 0 | Success. |
| 1 | Usage error (unknown flag, missing argument). |
| 2 | Transport error (endpoint unreachable, timeout). |
| 3 | Another devtools session is already active for this project. |
| 4 | No live devtools session found. |
| 5 | Tool returned a JSON-RPC error. stdout has the error envelope. |
Selector grammar (5 forms)
- Node id —
r:main/CounterDemo.SubmitButton — copy from tree. Stable within a window's lifetime; invalidated by switch / reload.
- AutomationId —
#btn-inc. Matches AutomationProperties.AutomationId exactly.
- AutomationName —
[name='Increment'] or [name="+ 1"]. Matches AutomationProperties.Name OR the visible caption of Buttons / TextBlocks / TextBoxes / ContentControls.
- TypePath —
Button, Button[2], StackPanel > Button. Type name is GetType().Name. Index disambiguates.
- Reactor source —
{component:'CounterDemo',line:42}. Reserved (Phase 3).
[name=…] cannot be indexed — if it matches multiple, error is ambiguous-selector with all candidate ids listed; pick one by node id or prefix with a TypePath step.
Typical workflows
"Does the app look right?" — visual diagnosis
mur devtools screenshot --out /tmp/shot.png
mur devtools screenshot --selector "[name='Submit']" --out /tmp/btn.png
mur devtools tree --selector "#login-form" --view full --pretty
Full-view tree nodes carry bounds, actualSize, desiredSize,
layout.margin, layout.padding, isVisible, isEnabled,
automationControlType. That's usually enough to spot a margin collapse
or a zero-size child without running the app in a debugger.
Diagnosing a layout issue
mur devtools screenshot --out /tmp/shot.png — confirm what's actually on screen.
mur devtools tree --selector "<container>" --view full | jq '.nodes[] | select(.actualSize.width == 0)' — find zero-sized children. A child with actualSize:{width:0,height:0} under a parent that's sized means the child isn't getting space (missing Flex(grow:1) on a ScrollView inside a FlexColumn is a classic).
- Edit the Reactor code, then
mur devtools reload (rebuild + relaunch) and re-screenshot.
mur devtools wait "<selector>" --visible --timeout 2000 if the state is async — don't screenshot before mount.
Diagnosing a contrast / color issue
mur devtools screenshot --selector "<element>" --out /tmp/el.png — pull the cropped PNG.
- Decode the PNG and sample foreground/background pixels. Compute WCAG 2.1 ratio =
(L1 + 0.05) / (L2 + 0.05) where L is relative luminance. Target ≥ 4.5:1 for body text, ≥ 3:1 for large text / UI chrome.
- If low, check whether the color came from a Theme token (correct — rebind to the right token for the surface) or a hardcoded hex (wrong — replace with a
Theme.* token; see skills/design.md).
- Edit,
mur devtools reload, re-screenshot, re-measure.
Verifying a state-driven change
mur devtools click "[name='+ 1']"
mur devtools wait "[name='Current count: 1']" --timeout 1000
mur devtools state
state is particularly useful when the UI text doesn't obviously encode
the value (e.g. a slider position or a theme toggle).
Reading debug output while the app runs
mur devtools logs --tail 50
mur devtools logs --source debug --pretty
mur devtools logs --filter "Nav.*cache" --tail 20
mur devtools logs --follow
mur devtools logs --since 42
Pair with state and tree to diagnose state-driven bugs: paste a
Debug.WriteLine in the render path, reload, reproduce, then drain logs.
The buffer (4 MB default) is installed before component reflection so
it catches startup output too — attach late and you still see what booted.
dropped in the response reports ring overflow; bump via
--devtools-logs-capacity <MB> on the app's launch if you're losing
entries. Set --devtools-logs off on launch to disable capture entirely.
Inspecting properties and styles
mur devtools properties "#my-button" --pretty
mur devtools properties "#my-button" --name Margin
mur devtools properties "#my-button" --name Grid.Row
mur devtools set-property "#my-button" Background "#FF0000"
mur devtools set-property "#my-button" Margin "10,5,10,5"
mur devtools set-property "#my-button" Visibility Collapsed
mur devtools styles "#my-button" --pretty
properties reports isLocal: true when a value was set directly (via
code or XAML attribute) vs inherited from style/template/default. Use
set-property to hot-patch layout or color at runtime without a rebuild.
Browsing resources and themes
mur devtools resources --pretty
mur devtools resources --filter "Color|Brush" --pretty
mur devtools resources --selector "#my-panel" --scope element
mur devtools set-resource ButtonBackground "#00FF00"
Resources are listed with their scope (element, ancestor:Grid, app,
app/merged, app/theme:Light, etc.) so you can trace where a value
came from. set-resource at a higher scope may shadow a value
defined in a merged/theme dictionary — check the replaced flag in the
response.
Understanding the visual tree hierarchy
mur devtools ancestors "#my-button" --pretty
Useful for understanding layout containers, resource scoping, and which
parent element is providing inherited property values.
Driving the app from a script
For launch → drive → shutdown loops (stress tests, batch automation, CI):
- Start the app yourself (
dotnet run … -- --devtools run & or the built exe).
- Poll
mur devtools session list until exit 0 — that's the only signal
that lockfile discovery + the auth probe both succeed. Don't grep stdout
for MCP_ENDPOINT=; that banner fires before the lockfile is written and
before the server can authorize you.
- Issue verbs without
--endpoint so lockfile discovery applies the bearer
token.
mur devtools shutdown — the child exits with code 0 within a second.
Wait for the OS process to exit before relaunching against the same
project, or the single-instance check (exit 3) will trip on the next iteration.
- If a previous iteration was killed without
shutdown, run
mur devtools session clean to GC the stale lockfile.
Cleaning up when done
mur devtools shutdown
A running Reactor app holds open handles on its own build output, which
blocks dotnet build from overwriting the DLLs. Use shutdown between
rebuilds when you're iterating on the app's source. If the app was started
under mur devtools <project> (supervisor mode), shutdown also makes the
supervisor return 0 so the mur.exe binary frees up.
Gotchas
- Both the build-time switch and a devtools launch are required. The MCP server only starts if the app was compiled with
<RuntimeHostConfigurationOption Include="Reactor.DevtoolsSupport" Value="true" Trim="true" /> (samples use a Debug-conditional shared props convention so Release/AOT stays trimmed) and launched with either mur devtools … or dotnet run -- --devtools run directly. Either launch mode works — the supervisor is optional. Miss both legs (switch + launch flag) and the app boots normally with no MCP port, no lockfile, no banner. If mur devtools session list reports exit 4 indefinitely, check the csproj/props switch first, then the launch command.
switch and reload invalidate every node id. Re-walk the tree after them; do not cache r:… ids across swaps.
- Popups aren't in the main visual tree.
tree walks window.Content — ComboBox dropdown items, flyouts, and context menus live in separate popup roots and won't show up. select auto-expands the container but item resolution through the main tree will still miss them. Prefer ISelectionItemProvider via a node-id that tree emitted while the popup was open, or switch to a selector that targets the SelectorItem ancestor directly.
fire only sees declared methods. Inline lambdas (Button("+1", () => setCount(...))) are unreachable. Hoist the handler to a method on the Component class when you need fire access.
- Scroll percent read-back can lag one call. Right after
mur devtools scroll … --by 0,50, the next verb may report scrollPercent.vertical:0 before the engine settles. If the offset matters, call scroll … --by 0,0 once more to read the settled value, or use --to <item-selector> (ScrollIntoView) which is deterministic.
- Use
wait before asserting on async UI. Many demos render on a dispatcher hop; a screenshot immediately after click may capture the pre-state.
- Devtools log is authoritative. Every call lands in
%LOCALAPPDATA%\Reactor\devtools\{pid}.log (tab-separated: ts, tool, selector, latency, ok/err, rpc code). Tail it to reconstruct a failed run.
- Two against the same is a hard error (exit 3). Use or close the window to release the lockfile before starting another.
Raw MCP (escape hatch)
If you have to talk MCP directly — another MCP client, an existing script,
or a structured argument shape the CLI flattens — the endpoint is
discoverable from the lockfile:
ENDPOINT=$(mur devtools session list | jq -r '.endpoint')
curl -s $ENDPOINT -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"click","arguments":{"selector":"[name=\"Save\"]"}}}'
GET $ENDPOINT returns the self-describing schema document (protocol
version, selector grammar, tool inventory). Prefer the CLI for everything
else.
Spec + source pointers
- Specs:
docs/specs/024-ai-agent-devtools.md, docs/specs/025-devtools-cli-parity.md, docs/specs/036-window-design.md (multi-window MCP surface §10)
- Server:
src/Reactor/Hosting/Devtools/DevtoolsMcpServer.cs
- Tool registration:
DevtoolsTools.cs, DevtoolsUiaTools.cs, DevtoolsFireTool.cs, DevtoolsStateTool.cs, DevtoolsLogsTool.cs, DevtoolsPropertyTools.cs
- Log capture:
LogCaptureBuffer.cs, LogCaptureInstall.cs
- CLI verbs:
src/Reactor.Cli/Devtools/DevtoolsVerbs.cs
- Selector grammar / parser:
SelectorParser.cs