agentleFS
Sign inSign up

microsoft-ui-reactor

microsoft/microsoft-ui-reactor/AGENTS.md

Reactor is a declarative, component-based C# framework for building WinUI 3 desktop apps. It renders real WinUI controls via a virtual element tree and reconciler — similar to React's programming model but targeting native Windows UI. Every suite runs on Microsoft.Testing.Platform (MTP): global.json pins test.runner, xunit.v3 v4 is MTP-only, and the MSTest projects opt in with EnableMSTestRunner. Practical consequences: the project is still passed positionally (dotnet test tests/Reactor.Tests), but prefer --filter-class / --filter-method / --filter-not-class / --filter-trait on the xUnit…

AGENTS.md654 starsChanged 37 days ago
# Copilot Instructions — Microsoft.UI.Reactor

Reactor is a declarative, component-based C# framework for building WinUI 3 desktop apps. It renders real WinUI controls via a virtual element tree and reconciler — similar to React's programming model but targeting native Windows UI.

## Build, Test, Lint

```bash
# Build (platform defaults to machine arch for apps; libraries are AnyCPU)
dotnet build Reactor.slnx

# Unit tests — xUnit, headless, fast (13,000+ tests incl. 590 Yoga fixtures)
dotnet test tests/Reactor.Tests

# Single test class (xUnit suites use the MTP filter flags, not `--filter`)
dotnet test tests/Reactor.Tests --filter-class "*ReconcilerMountUpdateTests*"

# Selftests — real WinUI window, in-process (~10s)
dotnet test tests/Reactor.SelfTests

# Packaged selftests — same fixtures under MSIX identity (needs Developer Mode)
dotnet test tests/Reactor.PackagedTests -p:Platform=x64

# Raw TAP output (faster iteration, supports --filter prefix)
dotnet run --project tests/Reactor.AppTests.Host -- --self-test --filter "Flex"

# E2E — winapp ui CLI (install: winget install Microsoft.WinAppCli, or run ./bootstrap.ps1)
dotnet test tests/Reactor.AppTests

# Single E2E class (MSTest suites keep the VSTest-style `--filter` expression)
dotnet test tests/Reactor.AppTests --filter "ClassName=Microsoft.UI.Reactor.AppTests.Tests.AccessibilityTests"
```

Every suite runs on **Microsoft.Testing.Platform** (MTP): `global.json` pins
`test.runner`, xunit.v3 v4 is MTP-only, and the MSTest projects opt in with
`EnableMSTestRunner`. Practical consequences: the project is still passed
positionally (`dotnet test tests/Reactor.Tests`), but prefer `--filter-class` /
`--filter-method` / `--filter-not-class` / `--filter-trait`
on the xUnit suites (they also still accept the VSTest `--filter` expression;
MSTest accepts **only** `--filter` and rejects `--filter-class` with exit 5),
`--logger "trx;…"` becomes `--report-trx`, and `--blame-hang-*` becomes
`--hangdump …`.

CI runs unit tests + selftests + packaged selftests + full solution build on every PR. .NET 10 SDK, `windows-latest` runner.

Full testing guide — tier selection, NativeAOT runs, code coverage — in [`TESTING.md`](TESTING.md).

## Architecture

### Virtual DOM model

UI is described as **immutable C# records** (`Element` subclasses), not WinUI controls. The reconciler diffs old vs. new element trees and patches only what changed on real controls.

```
Component.Render() → Element tree (records)
                        ↓
                   Reconciler
                   ├── Mount  → creates WinUI controls
                   └── Update → diffs & patches controls
```

### Reconciler is split across partial classes

- `Reconciler.cs` — orchestration, child reconciliation, unmount, helpers
- `Reconciler.Mount.cs` — mount dispatch + composition-primitive handlers (controls mount via their registered `ControlDescriptor`/`IElementHandler`)
- `Reconciler.Update.cs` — update dispatch + composition-primitive handlers (controls update via the same registered descriptors/handlers)

### Hooks follow React rules

Hooks (`UseState`, `UseEffect`, `UseReducer`, `UseMemo`, etc.) are tracked by call order in `RenderContext`. They must be called unconditionally, in the same order every render — no conditional hooks. Pass `threadSafe: true` for cross-thread state updates.

### Echo suppression for value controls

Echo handling is a documented hybrid (spec-047 §8.3). Synchronous, exact-comparable, single-controlled-value round-trips (ComboBox, FlipView, GridView, ListBox, Pivot, PipsPager, RadioButtons, SelectorBar, TabView, TemplatedFlipView, ToggleSwitch, TextBox) use a value-diff arm (`ReactorState.PendingEchoMatch` + `ArmExpectedEcho`/`ShouldSuppressEcho`, opt-in `valueDiffEcho`). `ChangeEchoSuppressor` is **retained** as the suppress-counter fallback for the rest: doubles (Slider/NumberBox value), NumberBox coercion, CalendarView collection diff, deferred/coercion strings (AutoSuggest/Password/RichEdit), Expander, CheckBox path-B, the `ApplySetters` suppression scope, and the public `WriteSuppressed` primitive. Authors keep using the stable `WriteSuppressed` primitive (or declare `.Controlled` / `valueDiffEcho` on a descriptor) — never the suppressor directly.

### Element pooling

`ElementPool` recycles WinUI controls. `PoolableTypes` includes interactive controls (`Button`, `TextBox`, `ToggleSwitch`) alongside the non-interactive ones: their event trampolines subscribe **once for the control's lifetime** and read the current element from attached state at invocation time, so a recycled control dispatches to the new element's callbacks. `Reconciler.ReturnControl<T>` therefore deliberately **preserves** `ReactorState.ControlEventState` across rent/return (issue #114) — clearing it would re-allocate on every rent and double-subscribe; the box is dropped only on full detach (`DetachReactorState`). New event wiring on a poolable control must go through that one-time trampoline, never a fresh per-rent subscription. It also keeps a `ConditionalWeakTable<UIElement, object>` (`_compositorTainted`) of elements that have had `GetElementVisual()` called on them — those permanently lose the XAML implicit-transition APIs (`OpacityTransition`, `ScaleTransition`, …), so they are excluded from pooling rather than handed to a future user that might need those APIs.

### Per-element state via attached DP

`ReactorAttached.StateProperty` stores `ReactorState` (Element pointer + `ModifierEventHandlerState` — the routed-input family, lazily allocated — + per-control `ControlEventStateBox` for control-intrinsic events) on native elements — not `FrameworkElement.Tag` or a CWT.

## Key Conventions

### Elements are immutable records

```csharp
public record MyControlElement(string Label, Action? OnClick = null) : Element;
```

Use `with` expressions for variations. Never mutate.

### Factory methods over constructors

The DSL entry point is `using static Microsoft.UI.Reactor.Factories;`. Factory methods return Element records, never WinUI controls:

```csharp
TextBlock("hello")       // not new TextBlockElement("hello")
Button("+", () => ...)   // not new ButtonElement(...)
VStack(child1, child2)   // layout containers
```

`Factories` is `public static partial class` — factory methods can be added from multiple files.

### Fluent modifiers preserve concrete types

Extension methods use `<T> where T : Element` to maintain the concrete type through chains:

```csharp
Text("Hello").Bold().Margin(16).Set(tb => tb.TextWrapping = TextWrapping.Wrap)
// Still TextBlockElement throughout the chain
```

### Adding a new WinUI control

The legacy Element-record + `MountXxx`/`UpdateXxx` dispatch-switch path is gone. The current path:

1. **Element record** in `src/Reactor/Core/Element.cs`
2. **Authoring shape** — a `ControlDescriptor<TElement, TControl>` (the primary path) or a hand-coded `IElementHandler<TElement, TControl>` for irregular controls.
3. **Register** it. Spec 048 §3.4 removed the old bootstrap: built-in handlers now
   self-register **lazily on the first factory call**, via the per-control
   `Reg<>` / `RegDecorator<>` cctor latch in `Dsl.cs`. A `[GenerateReactorWrapper]`
   element gets a static constructor emitting `ControlRegistry.Register` (spec 058).
   To register a third-party control or override a built-in globally, call
   `ControlRegistry.Register<TElement, TControl>` (or `RegisterDecorator`,
   `RegisterForDerivedTypes`) at startup. `ReactorApp.RegisterAllBuiltIns()` is the
   opt-in bulk path for direct-record/AOT callers. Note the two latches differ:
   for a **hand-authored built-in** the `Reg<>` touch lives in the factory body,
   so `new MyElement(...)` alone registers nothing and the factory call is what
   latches it; a **`[GenerateReactorWrapper]`** element registers from its
   generated static constructor, so constructing one is already enough
   (`UnregisteredHandlerAndRegisterAllBuiltInsTests` depends on that, and the
   "unregistered handler" message deliberately never names
   `GenerateReactorWrapper` as a cause).
4. **Selftest fixture** in `tests/Reactor.AppTests.Host/SelfTest/Fixtures/`.

See [`docs/guide/extending-reactor-controls.md`](docs/guide/extending-reactor-controls.md) for the authoring-shape decision tree (prop/engine shapes, children strategies, echo handling, pooling).

Optionally: a factory method in `src/Reactor/Elements/Dsl.cs`, fluent modifiers in `ElementExtensions.cs`, and unit tests in `Reactor.Tests/`.

### Test tier selection

| Testing… | Write a… | Location |
|---|---|---|
| Algorithm, pure function, hook bookkeeping, D3 math | Unit test (xUnit) | `tests/Reactor.Tests/` |
| Element mount/update against real WinUI controls | Selftest fixture | `tests/Reactor.AppTests.Host/SelfTest/Fixtures/` |
| Behaviour that differs under MSIX identity (`ms-appx:`, `Package.Current`, MRT, `PackageRuntime.IsPackaged` branches) | Selftest fixture gated with `PackagedIdentityFixtures.RequirePackagedTier` **and** declared `SelfTestTier.Packaged` | same folder; runs for real in `tests/Reactor.PackagedTests` |
| Real user input, UIA properties, cross-process | E2E test (winapp ui) | `tests/Reactor.AppTests/Tests/` |

Start with unit tests. Use selftests only when you need a live WinUI control. E2E is the slowest tier.

The packaged tier (`tests/Reactor.PackagedTests` + `tests/Reactor.PackagedTests.Host`) exists because
every other tier runs unpackaged and is structurally blind to identity-dependent bugs.
`Reactor.PackagedTests.Host` owns no source; it links every `.cs` from `Reactor.AppTests.Host` and
adds only MSIX properties plus a `Package.appxmanifest`. The whole corpus runs under identity.
**Gotcha worth not re-deriving:** the manifest's `uap5:AppExecutionAlias` is load-bearing — launching
the alias stub inherits stdout while keeping package identity, which AUMID activation cannot do
(it is brokered, so stdout can't be redirected at all).
**Second gotcha:** a packaged fixture needs *two* declarations, not one. The
`RequirePackagedTier` gate decides whether the body asserts; `SelfTestFixtureRegistry.TierRequirements`
decides whether the unpackaged host runs it at all. Skip the second and the fixture self-skips
into the amber skip inventory on every unpackaged run forever (issue #1154). **The gate is not an
independent identity check** — it and the tier filter share one entry-assembly predicate, so inside
the packaged host it always returns true; `Packaged_IdentityGuard` failing its
`PackageRuntime.IsPackaged` / `Package.Current` assertions is what detects a mis-launched packaged
host. Consequence worth knowing: **`--list-fixtures` is tier-dependent**, and both hosts print
`# Total not-applicable fixtures:` / `# Not applicable fixture list:` after `# Total failures:`
so the exclusion is an assertable fact rather than a silent absence.

### Console-mutating tests need collection isolation

Tests that write to `Console.Out`/`Console.Error` must be grouped with `[Collection("ConsoleTests")]` to prevent cross-test interference.

### AOT compatibility

`IsAotCompatible=true` is set for all net10.0+ projects. The core Reactor library promotes IL trimming/AOT warnings to errors — new reflection usage must be annotated before merging. Non-Reactor projects (tests, samples) suppress these warnings.

### WinUI library projects

Class libraries must set `WindowsAppSDKSelfContained=false`. Only app executables own Windows App SDK self-contained packaging.

### No XAML

Everything is C#. No `.xaml` files for UI (except `ReactorApplication.xaml` which loads `XamlControlsResources` for AOT compatibility).

### User guide docs are generated

Docs under `docs/guide/` are compiled from `docs/_pipeline/templates/*.md.dt` via `mur docs compile`. Edit the templates, not the compiled output.

## Project Layout

```
src/Reactor/              Core framework
  Core/                   Reconciler, Component, Element, Hooks, RenderContext
  Elements/               DSL factories (Dsl.cs) + fluent modifiers (ElementExtensions.cs)
  Flex/                   FlexPanel — CSS Flexbox via Yoga
  Yoga/                   Pure C# port of Meta's Yoga layout engine
  Hosting/                ReactorApp entry point, render loop, hot reload
src/Reactor.Cli/          CLI tool (scaffolding, localization, preview)
src/Reactor.Analyzers/    Roslyn analyzers (theming, accessibility)
src/vscode-reactor/       VS Code live preview extension
tests/
  Reactor.Tests/          Unit tests (xUnit, headless)
  Reactor.SelfTests/      Selftest runner (MSTest, wraps TAP subprocess)
  Reactor.AppTests.Host/  Selftest host app + winapp ui fixture navigator
  Reactor.AppTests/       E2E tests (MSTest + winapp ui)
samples/                  Demo apps and samples
docs/
  guide/                  User documentation (generated from templates)
  specs/                  Numbered design specs
  reference/              API and subsystem reference
```

## Field notes (gotchas from past sessions)

Hard-won specifics that repeatedly cost sessions time. Prefer these exact commands.

### Building & running tests

- **Pass `-p:Platform=x64` for any WinUI app/test project build.** AnyCPU builds of
  `Reactor.AppTests`, `Reactor.AppTests.Host`, etc. fail with *"WindowsAppSDKSelfContained
  requires a supported Windows architecture"*. (`dotnet build Reactor.slnx` handles the
  solution defaults; single app/test projects usually do not.)
- **A green Debug build does not clear the `Build solution` CI job.**
  `TreatWarningsAsErrors` is Release-only and CI builds Release, so verify with
  `dotnet restore Reactor.slnx` followed by `dotnet build Reactor.slnx --no-restore -c Release`.
  Keep those two steps split, exactly as the `Restore` and `Build` steps of the
  `build-solution` job in `ci.yml` do. The one-shot
  `dotnet build Reactor.slnx -c Release` also restores under `Configuration=Release`, and NuGet
  honors `TreatWarningsAsErrors` during restore, so on a proxied or offline feed `NU1900`
  ("unable to get package vulnerability data") becomes a hard error — measured on this repo, the
  one-shot form raised NU errors across **127 projects** versus **12** for the split form, burying
  the real result before a single file compiles. On a healthy feed neither form emits `NU1900` and
  the distinction is invisible; it only bites behind a TLS-inspecting proxy or offline. CI is
  immune because it restores without `-c Release`. If `WMC0110` / `WMC1509` follows a C# error,
  treat the markup errors as a likely cascade: fix the earlier error first, then confirm
  they disappear before investigating them independently.
- **Add `-p:SkipSignaturesGen=true` to local `tests/Reactor.Tests` builds** to avoid the
  XAML-markup/SignaturesGen race: `CSC error CS2012: Cannot open '...\obj\...\intermediatexaml\Reactor.dll' ... used by another process`. If it still races under
  parallel WinUI builds, prebuild `src/Reactor` alone first, then escalate **on the build
  command only** — `-m:1 -nodereuse:false` (or `$env:MSBUILDDISABLENODEREUSE='1'`) — and run
  the suite separately with `--no-build`. (`-p:CI=true` also skips SignaturesGen.)
  ```powershell
  dotnet build src/Reactor/Reactor.csproj -p:Platform=x64 -m:1 -nodereuse:false
  dotnet build tests/Reactor.Tests -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false
  dotnet test  tests/Reactor.Tests --no-build -p:Platform=x64
  ```
- **Never append an MSBuild-only switch to `dotnet test`** (issue #1140). `-m:1`,
  `-nodereuse:false`, `--nologo`, `-warnaserror` and friends are *not* `dotnet test`
  options, and anything `dotnet test` does not recognise is forwarded to the **test
  executable**. That forwarding is normal — it is how `--filter-class`, `--parallel` and
  `--report-trx` reach the runner — but an MSBuild-only switch is recognised by *neither*
  layer, so MTP rejects it and exits **5 (`InvalidCommandLine`)** before running anything.
  **The rejected-option message is never surfaced**, so the only signals are the exit code
  and a summary that reads green in prose:
  ```text
  ...\Reactor.Tests.dll (net10.0-windows10.0.22621.0) Zero tests ran
  Test run summary: Zero tests ran
    error: 1
    total: 0   failed: 0   succeeded: 0   skipped: 0
  ```
  The reliable tell is the module label: a run that actually started prints the
  handshake-derived `net10.0|x64`, so the full `net10.0-windows10.0.22621.0` means the test
  host died before its handshake. Exit 5 is *invalid command line*, **not** MTP's zero-tests
  policy (that is exit 8). `-p:…` **is** a real `dotnet test` option, which is why
  `-p:SkipSignaturesGen=true` and `-p:Platform=x64` are safe there; measured on this tree,
  `$env:MSBUILDDISABLENODEREUSE='1'` is also safe, because an environment variable is not an
  argv token. Note `dotnet test --help` is not a reliable safe-list — `-t:`/`-target:` and
  `--property` are accepted but unlisted. See
  [`TESTING.md`](TESTING.md#msbuild-switches-are-not-dotnet-test-switches).
- **Fast selftest loop** (TAP `ok`/`not ok`): `dotnet run --project tests/Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test --filter "<Prefix>"`.
- **Headless unit tests cannot construct any `Microsoft.UI.Xaml` object** — control, brush,
  geometry, `BitmapImage`, or **`AutomationPeer`-derived** type — you get a `COMException`.
  Test only pure-managed logic + WinRT value structs/enums; push anything live to a
  selftest. Internal seams are fair game: `InternalsVisibleTo("Reactor.Tests")` is set, so
  prefer an internal tokenizer/parser (e.g. `PathDataParser.ParseTokens(pathData)`)
  over a public method that builds WinUI objects.
- In the `Microsoft.UI.Reactor.*.Tests` namespaces, `Microsoft.UI.System` shadows `System`
  — use `global::System.IO.Path`, `global::System.IO.File`, etc.
- **E2E input needs an interactive desktop.** `SendInput`/`GetCursorPos` returning
  `ACCESS_DENIED (err 5)` means your session can't inject input — validate the fixture over
  UIA (`winapp ui ... --json -w <hwnd>`) and rely on the CI *"E2E Tests (winapp ui)"* job.
  For stateful E2E/selftest UI use `Component<T>()` fixtures; raw `ctx.UseState` doesn't
  persist (TestHost renders with a fresh `RenderContext`).

### Checks that actually prove something

Applies to xUnit assertions, selftest `H.Check`s, **and the ad-hoc commands you verify
state with**. A broken instrument is trusted by default, so the last one bites hardest.

- **An assertion must fail when its target is broken.** Delete or no-op the code and
  confirm it reddens. Bare non-null, "no throw" on a `void`, and always-emitted shape
  markers are vacuous. Prefer differential oracles, structural counts, or
  corrupt-then-recompute.
- **An oracle proves nothing where its healthy and broken branches coincide.** If a live
  value is `0`, `NaN`, empty, or already equal to what it's compared against, the
  comparison is a tautology regardless of the product. Log the input, not just the verdict
  — mutation testing cannot catch an environment-derived oracle, because the defect is
  upstream of the code it perturbs.
- **A no-match is not a measurement** until a positive control — same probe, same wrapping,
  same file set — shows it can match. Zero from a broken grep and zero from a clean repo are
  the same character on screen. Applies to greps, `gh --jq`, CI queries. Renames make today's
  identifier structurally blind to older revisions.
- **Self-consistency is not currency.** Counts from one stale checkout corroborate each
  other perfectly. Pin measurements to a commit OID and compare against a live remote ref.
- **Green runs corroborate a fix; a mechanism establishes it.** At a 25% failure rate,
  three clean passes happen 42% of the time.

**When to stop.** Validate each instrument once — then stop. Verify the assertion that gates
correctness, not the verifier of the verifier. If you are adding a *second* layer of checking,
or correcting the wording of a comment rather than the behaviour of code, you are past the
point of return: stop and ship.

### Analyzers, CLI checks, docs & public API

- `src/Reactor.Analyzers` targets **`netstandard2.0`** — no `FrozenDictionary`/net8+ APIs,
  and you **cannot reference `src/Reactor.Cli`**; copy shared logic and add parity tests.
  Every new `REACTOR_*` id needs a row in `src/Reactor.Analyzers/AnalyzerReleases.Unshipped.md`
  (else `RS2008`). `mur check` rules are reflection-discovered in `RuleRegistry.cs` — add or
  remove a rule *file*, don't hand-edit a list.
- Docs are generated (see above): compile only the topic you touched and revert unrelated
  snippet churn.
- A new public API surface has **two byte-identical index copies** —
  `skills/reactor.api.txt` and `plugins/reactor/skills/reactor-dsl/references/reactor.api.txt`
  — regenerate via `mur --regen-api`; keep them in sync.
- The **ReactorGallery search index** (`samples/ReactorGallery/reactor-search-index.json`,
  consumed by the external `winui-search` CLI) is generated from the gallery source +
  `tools/Reactor.SearchIndex/editorial.json`. After adding/renaming a gallery control or
  changing **any** of its sample snippets (every clean `SampleCard` is emitted, not just the
  first), regenerate via `dotnet run --project tools/Reactor.SearchIndex` (a `Reactor.Tests`
  gate byte-compares it, so a stale index fails CI). Curate keywords/usings/overrides in
  `editorial.json`, never the generated JSON.
  - **Never bump `SearchIndexGenerator.SchemaVersion`.** The consumer pins it at `1` and treats
    any other value as "nothing I understand" — it returns zero scenarios *without an error*, so
    a bump silently blanks the whole Reactor corpus in `winapp find-ui`. New fields are additive
    and schema-legal; that is why there has never been a reason to bump it. `SchemaVersion_StaysAtOne`
    is the gate.
  - An entry's `details` prose is **lifted verbatim** out of the shipped agent kit by an
    `<!-- index:<control-id> -->` marker in a SKILL.md, so index and skills cannot drift. Edit the
    marked block, not the JSON; an unclosed/nested marker or an id naming no control fails
    generation. Spec 064.
- A new common-element modifier touches every seam: the `ElementModifiers` field, skip
  equality, `Merge`, `ApplyModifiers`, and the fluent extension. Pair a `.HasValue` write
  with `fe.ClearValue(<DP>Property)` on unset unless intentionally matching a no-reset sibling.

### Environment

- Work in a clean worktree, not `main`: `git worktree add -b <branch> <path> origin/main`.
- Don't build under deep or OneDrive-synced paths — WinUI can fail with `MSB3073`/`PRI210`
  or XAML compiler `WMC1006`/`WMC9999`, sometimes naming an unrelated project such as
  `Reactor.AppTests.ThirdPartyControls`. Before concluding your branch broke the build, check
  out the same HEAD at a short path (e.g. `C:\src\probe`); if it passes, the path was the cause.

### Repo skills (`.github/skills/`)

Contributor-facing orchestration skills — read the `SKILL.md` and drive it with your own
tools: `pr-review` (multi-dimensional branch review), `perf-compare` (stress-harness delta
vs `main`), `coverage-uplift` (non-vacuous coverage across tiers), `analyzer-dym`
(did-you-mean / `mur check` authoring). Not shipped to end users.

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.