Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d8b09e4
Add text-only component inspection to ReactorDiagnostics
azchohfi Oct 5, 2026
e3da41b
Address review: UIThreadOnly markers, secret text in every value shap…
azchohfi Oct 5, 2026
43dfb00
Address code-quality review: LINQ filters and using declaration
azchohfi Oct 5, 2026
1ba8a42
Docs: snippet-back the component inspection example
azchohfi Oct 5, 2026
f1c49ed
Selftest: accept trimmed props metadata under NativeAOT
azchohfi Oct 5, 2026
0eb124a
Merge remote-tracking branch 'origin/main' into azchohfi-reactor-comp…
azchohfi Oct 5, 2026
0bdcf4c
Diagnostics: host-control content ownership, overlap precedence, thro…
azchohfi Oct 6, 2026
8e98659
Diagnostics: refuse secret writes, undefined flag bits, state-held Re…
azchohfi Oct 6, 2026
8ffaf29
Diagnostics: redact quoted secret labels; refresh UseContext identity…
azchohfi Oct 6, 2026
0b6228d
Diagnostics: redact secret-typed props whole; list only public getters
azchohfi Oct 6, 2026
db8253f
Diagnostics: authored reference-list order, attribution-only dialog t…
azchohfi Oct 6, 2026
7937506
Diagnostics: report Scale/Rotation/Translation/CenterPoint as applied…
azchohfi Oct 6, 2026
4314868
Diagnostics: AutomationId LabeledBy resolves only to a loaded target;…
azchohfi Oct 6, 2026
f662cf5
Diagnostics: clear pending LabeledBy on unmount; empty explicit name …
azchohfi Oct 6, 2026
10e7c4a
Diagnostics: merge decorator-chain modifiers; robust flags bits; prec…
azchohfi Oct 6, 2026
7e6bb28
Diagnostics: array rank in type names; null typed props keep a row; f…
azchohfi Oct 6, 2026
f1285a6
Diagnostics: keep composed-property contributors; redact space-led qu…
azchohfi Oct 6, 2026
5d3dd0f
Diagnostics: no LabeledBy edge rebuilt from an unmounted control's tag
azchohfi Oct 6, 2026
4431482
Diagnostics: resolve host roots against the rendered root; nullable d…
azchohfi Oct 6, 2026
10cd181
Merge remote-tracking branch 'origin/main' into azchohfi-reactor-comp…
azchohfi Oct 6, 2026
8b19505
Merge origin/main into azchohfi-reactor-component-diagnostics
azchohfi Oct 7, 2026
aaf3c9c
Diagnostics: spell nint/nuint as C# does
azchohfi Oct 7, 2026
16ec9aa
Diagnostics: article-free refusal messages; precise render-path cost …
azchohfi Oct 7, 2026
30cef95
Diagnostics: outermost automation name is in effect; CulturedFact for…
azchohfi Oct 7, 2026
7e97bc1
Diagnostics: per-element UI-thread check; quoted truncation; LabeledB…
azchohfi Oct 7, 2026
4d61d1f
Diagnostics: skip reconcilers owned by another UI thread
azchohfi Oct 7, 2026
64d4d30
Diagnostics: a root that renders Empty() still resolves
azchohfi Oct 7, 2026
ffd8960
Diagnostics: explicit ContentTarget ownership for empty roots
azchohfi Oct 7, 2026
bf257ea
Diagnostics: filter foreign-thread reconcilers with Where
azchohfi Oct 7, 2026
155e5c5
Diagnostics: weak, dispose-released ContentTarget ownership; reclaim …
azchohfi Oct 7, 2026
780f2bb
Diagnostics: never call a collection's ToString; ownership follows re…
azchohfi Oct 7, 2026
19d3701
Diagnostics: retargeting drops the earlier claim; public Count getter…
azchohfi Oct 7, 2026
7bafe58
Diagnostics: scalar props stay one row; truncation keeps surrogate pa…
azchohfi Oct 7, 2026
9fc9fa9
Diagnostics: no lookup without a recorded dispatcher; context rows fo…
azchohfi Oct 7, 2026
7e3d6d0
Merge origin/main (#1318 host diagnostics) into azchohfi-reactor-comp…
azchohfi Oct 7, 2026
becda6a
Merge origin/main (#1356) into azchohfi-reactor-component-diagnostics
azchohfi Oct 7, 2026
92238ac
Diagnostics: "1 item"; scope the UI-thread note to the inspection mem…
azchohfi Oct 7, 2026
5a9f872
Diagnostics: round-trip format for DateTime/DateTimeOffset values
azchohfi Oct 7, 2026
b91c113
Diagnostics: props whose own text labels a secret are one redacted row
azchohfi Oct 7, 2026
b95e5c6
Diagnostics: exempt only fully generated record text; accept AOT reda…
azchohfi Oct 7, 2026
6b5a0a7
Diagnostics: resolve a host root against its published container
azchohfi Oct 7, 2026
68c65ed
Diagnostics: quoted secrets starting with punctuation; records with p…
azchohfi Oct 7, 2026
5c9cb08
Test: readonly field in FieldRecord
azchohfi Oct 8, 2026
655c82d
Reactor: a caption change replaces a truncated default automation name
azchohfi Oct 8, 2026
94c0e3c
Reactor: component rerenders use the owning host's dispatcher; window…
azchohfi Oct 8, 2026
48e5eb6
Merge origin/main (#1328 column + ComponentRendered, #1323) into azch…
azchohfi Oct 14, 2026
dae7a67
Diagnostics: describe the rendered-root lifecycle accurately in comments
azchohfi Oct 14, 2026
49c642c
Judge records over hand-written PrintMembers; keep occupied containers
azchohfi Oct 14, 2026
55237b1
Summarise collections by count in applied-property text
azchohfi Oct 14, 2026
ec1692e
Address review: skip redundant claims, document root identity
azchohfi Oct 14, 2026
e174d59
Merge remote-tracking branch 'origin/main' into azchohfi-reactor-comp…
azchohfi Oct 15, 2026
fc240a3
Merge remote-tracking branch 'origin/main' into azchohfi-reactor-comp…
azchohfi Oct 19, 2026
c08e6d3
Publish a root whose render returns null for diagnostics
azchohfi Oct 19, 2026
3f62f75
Keep error fallbacks unattributed on null retries; tolerate byref-lik…
azchohfi Oct 19, 2026
3cdce46
Drop secret redaction from component diagnostics
azchohfi Oct 19, 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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ Conventions for contributors:

### Added

- **Component inspection for devtools: `ReactorDiagnostics.DescribeComponent`,
`TrySetState`, `Rerender`, `GetAppliedProperties`, `GetReferenceEdges`.** An
inspector can go from a realized control to the component behind it: its name,
kind, props, hooks and consumed contexts as a text `ComponentSnapshot`; set a
`UseState` / `UseReducer` / `UsePersisted` value from text (parsed to the hook's
type, refused with a reason otherwise) and re-render it; list the WinUI
properties a control's modifiers set (including the caption-derived default
`AutomationProperties.Name`); and list the references it declares, including
pending ones whose target has not mounted. Component snapshots and property
lists are text — no live component, element or hook object; a resolved
reference edge carries its mounted target control. Reads
are on demand and UI-thread-affine. The render path gains only reference
stores: per component, `UseContext` records the context it read and
reference-list wiring keeps the authored list; per host render, the host
records which root produced the displayed control (and, when it renders into its
`ContentTarget`, that it owns it). With source mapping on, an open
`ContentDialog` is tagged with its element so its chrome resolves to the
`ContentDialog(...)` call site (spec 010, spec 057 §3.1).

- **`REACTOR_DYM_006`: color strings that render gray** (issue #1375, spec 061 §7.3). A new
default-on **Warning** flags compile-time-constant color strings that `BrushHelper` can't
parse and therefore silently fall back to gray: `.Background("orange")`, `.Foreground("#888")`,
Expand Down
29 changes: 29 additions & 0 deletions docs/_pipeline/apps/diagnostics/App.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Diagnostics.Tracing;
using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Reactor.Core.Diagnostics;
using Microsoft.UI.Reactor.Diagnostics;
using Microsoft.UI.Xaml;
using static Microsoft.UI.Reactor.Factories;

ReactorApp.Run<DiagnosticsApp>("Diagnostics Demo");
Expand Down Expand Up @@ -46,3 +48,30 @@ public NavigationOverlay()
public void Dispose() => _subscription.Dispose();
}
// </snippet:navigation-overlay>

public static class ComponentInspector
{
public static void Inspect(UIElement wrapper, UIElement control)
{
// <snippet:inspect-component>
// The Border wrapper every Component<T>() / RenderEachTime / Memo mounts into,
// or a host's root content (window.Content, a ReactorHostControl).
if (ReactorDiagnostics.DescribeComponent(wrapper) is { } component)
{
// component.Name: "Counter", "Memo in MainPage.Render", "render in App.Main"
foreach (var hook in component.State)
Console.WriteLine($"{hook.Index} {hook.Kind} {hook.Type} = {hook.Value}");

// Parsed to the hook's type; same semantics as calling the UseState setter.
if (!ReactorDiagnostics.TrySetState(wrapper, 0, "42", out var error))
Console.WriteLine(error); // "'abc' is not a valid int", "... has kind 'ref'; ..."

ReactorDiagnostics.Rerender(wrapper); // bypasses memoization, like a state change
}

// Which WinUI properties this control's common modifiers set (.Width, .Margin, ...).
var owned = ReactorDiagnostics.GetAppliedProperties(control);
// </snippet:inspect-component>
_ = owned;
}
}
61 changes: 61 additions & 0 deletions docs/_pipeline/templates/source-mapping.md.dt
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,67 @@ small allocation per *component*, never per leaf, and nothing while the flag is
`[ReactorSourceTransparent]`, which is what `Pending(fallback, child)` does;
`intl.RichMessage` cannot, because it is an instance method.

## Inspecting components

`ReactorDiagnostics` (namespace `Microsoft.UI.Reactor.Core.Diagnostics`) lets an
inspector go from a realized control to the component behind it and make
development-time edits. It is a provisional surface for devtools. These inspection
members must be called on the UI thread that owns the element (`GetHosts()` is the
exception: it is safe from any thread). A `ComponentSnapshot` and the property lists are
plain text: they hold no reference into the component, so they can be kept or
sent across a process boundary. The one live object is a resolved reference
edge's `Target`, the mounted control it points at.

```csharp snippet="diagnostics/inspect-component"
```

Values are formatted for display: strings are quoted, collections are summarised
as `List<T> (n items)` from the count they advertise (a lazy sequence is never
enumerated and shows `(count unknown)`), and long values are cut at 200 characters.
Values are not redacted: like other in-process inspectors, Reactor reports what
the app holds, and an inspector that shows or forwards values where a secret must
not appear (a log, a recording, an AI agent) decides what to hide.
`TrySetState` accepts numbers, single characters, `true`/`false`, enum member
names (a `[Flags]` combination only of declared bits), dates, time spans, GUIDs,
plain text and their nullables (`null` clears a
string or nullable); it refuses any other type with a reason rather than guessing.
Hook names are not recorded, so `State` rows have an empty `Name` and are
identified by call-order index and kind. Props are read by reflection: under
NativeAOT or trimming a props type's members can be trimmed away, and `Props` then
holds a single `<Type> (members unavailable)` row instead.

Component lookup does not depend on source mapping. When a root renders a component
directly (`Render() => Component<Shell>()`), its rendered control is also `Shell`'s
wrapper; that element describes `Shell`, and the root is reached through its
`ContentTarget`, its `ReactorHostControl` or the dev-overlay wrapper instead. A host
that installs straight into `Window.Content` has no such anchor, so in that shape its
root is not reachable. `GetAppliedProperties` reads
the element back-pointer, so it needs the control to be tagged — turn source
mapping on for full coverage. It covers common modifiers only; a property set by
the control's own element record (`TextBlock("hi")` → `Text`) is not listed. The
one exception is the `AutomationProperties.Name` Reactor derives from a captioned
control (`Button("Save")`) when you set none: it is reported as
`"DefaultAutomationName"` while the live name still equals it.

An open `ContentDialog` is hosted in its own popup, out of reach of an ancestor
walk from the dialog's buttons, so with source mapping on the realized dialog is
tagged with its `ContentDialog(...)` element too: `ReactorSourceMap.GetSource(dialog)`
names the call site. That tag is for attribution only — the element's modifiers
apply to the collapsed placeholder — so `GetAppliedProperties` and
`GetReferenceEdges` report nothing for the dialog itself, and the tag is cleared
when the dialog closes.

A re-render diffs element against element, not against the live control, so it
does not undo a property you edited directly on the control unless the element's
value for that property changed.

`ReactorDiagnostics.GetReferenceEdges(control)` lists the references a control
declares (`.LabeledBy(ref)`, `.XYFocusRight(ref)`, `.DescribedBy(...)`, a
descriptor's `.Reference(...)`, …): the property, the AutomationId or expected
target type you passed, and whether it has resolved. A pending edge is the case
WinUI cannot show you: until the target mounts, the property itself
(`XYFocusRight`, `AutomationProperties.LabeledBy`) reads null.

## Tips

**For now, lean on the component name.** Wrap chunks of UI in
Expand Down
78 changes: 78 additions & 0 deletions docs/guide/source-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,84 @@ small allocation per *component*, never per leaf, and nothing while the flag is
`[ReactorSourceTransparent]`, which is what `Pending(fallback, child)` does;
`intl.RichMessage` cannot, because it is an instance method.

## Inspecting components

`ReactorDiagnostics` (namespace `Microsoft.UI.Reactor.Core.Diagnostics`) lets an
inspector go from a realized control to the component behind it and make
development-time edits. It is a provisional surface for devtools. These inspection
members must be called on the UI thread that owns the element (`GetHosts()` is the
exception: it is safe from any thread). A `ComponentSnapshot` and the property lists are
plain text: they hold no reference into the component, so they can be kept or
sent across a process boundary. The one live object is a resolved reference
edge's `Target`, the mounted control it points at.

```csharp
// The Border wrapper every Component<T>() / RenderEachTime / Memo mounts into,
// or a host's root content (window.Content, a ReactorHostControl).
if (ReactorDiagnostics.DescribeComponent(wrapper) is { } component)
{
// component.Name: "Counter", "Memo in MainPage.Render", "render in App.Main"
foreach (var hook in component.State)
Console.WriteLine($"{hook.Index} {hook.Kind} {hook.Type} = {hook.Value}");

// Parsed to the hook's type; same semantics as calling the UseState setter.
if (!ReactorDiagnostics.TrySetState(wrapper, 0, "42", out var error))
Console.WriteLine(error); // "'abc' is not a valid int", "... has kind 'ref'; ..."

ReactorDiagnostics.Rerender(wrapper); // bypasses memoization, like a state change
}

// Which WinUI properties this control's common modifiers set (.Width, .Margin, ...).
var owned = ReactorDiagnostics.GetAppliedProperties(control);
```

Values are formatted for display: strings are quoted, collections are summarised
as `List<T> (n items)` from the count they advertise (a lazy sequence is never
enumerated and shows `(count unknown)`), and long values are cut at 200 characters.
Values are not redacted: like other in-process inspectors, Reactor reports what
the app holds, and an inspector that shows or forwards values where a secret must
not appear (a log, a recording, an AI agent) decides what to hide.
`TrySetState` accepts numbers, single characters, `true`/`false`, enum member
names (a `[Flags]` combination only of declared bits), dates, time spans, GUIDs,
plain text and their nullables (`null` clears a
string or nullable); it refuses any other type with a reason rather than guessing.
Hook names are not recorded, so `State` rows have an empty `Name` and are
identified by call-order index and kind. Props are read by reflection: under
NativeAOT or trimming a props type's members can be trimmed away, and `Props` then
holds a single `<Type> (members unavailable)` row instead.

Component lookup does not depend on source mapping. When a root renders a component
directly (`Render() => Component<Shell>()`), its rendered control is also `Shell`'s
wrapper; that element describes `Shell`, and the root is reached through its
`ContentTarget`, its `ReactorHostControl` or the dev-overlay wrapper instead. A host
that installs straight into `Window.Content` has no such anchor, so in that shape its
root is not reachable. `GetAppliedProperties` reads
the element back-pointer, so it needs the control to be tagged — turn source
mapping on for full coverage. It covers common modifiers only; a property set by
the control's own element record (`TextBlock("hi")` → `Text`) is not listed. The
one exception is the `AutomationProperties.Name` Reactor derives from a captioned
control (`Button("Save")`) when you set none: it is reported as
`"DefaultAutomationName"` while the live name still equals it.

An open `ContentDialog` is hosted in its own popup, out of reach of an ancestor
walk from the dialog's buttons, so with source mapping on the realized dialog is
tagged with its `ContentDialog(...)` element too: `ReactorSourceMap.GetSource(dialog)`
names the call site. That tag is for attribution only — the element's modifiers
apply to the collapsed placeholder — so `GetAppliedProperties` and
`GetReferenceEdges` report nothing for the dialog itself, and the tag is cleared
when the dialog closes.

A re-render diffs element against element, not against the live control, so it
does not undo a property you edited directly on the control unless the element's
value for that property changed.

`ReactorDiagnostics.GetReferenceEdges(control)` lists the references a control
declares (`.LabeledBy(ref)`, `.XYFocusRight(ref)`, `.DescribedBy(...)`, a
descriptor's `.Reference(...)`, …): the property, the AutomationId or expected
target type you passed, and whether it has resolved. A pending edge is the case
WinUI cannot show you: until the target mounts, the property itself
(`XYFocusRight`, `AutomationProperties.LabeledBy`) reads null.

## Tips

**For now, lean on the component name.** Wrap chunks of UI in
Expand Down
60 changes: 60 additions & 0 deletions plugins/reactor/skills/reactor-dsl/references/reactor.api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,17 @@ Set<T>(string key, T value) → void
Trim(int targetCount) → int
TryGet<T>(string key, ref T value) → bool

### record AppliedProperty
new(string Modifier, string Property, string Value)
string Modifier { get; init; }
string Property { get; init; }
string Value { get; init; }
Deconstruct(ref string Modifier, ref string Property, ref string Value) → void
Equals(AppliedProperty other) → bool
Equals(object obj) → bool
GetHashCode() → int
ToString() → string

### record ArrayTypeMetadata
new()
Func<Task<object>> CreateElement { get; init; }
Expand Down Expand Up @@ -1731,6 +1742,20 @@ Equals(object obj) → bool
GetHashCode() → int
ToString() → string

### record ComponentSnapshot
new(string Name, string Kind, bool IsRoot, IReadOnlyList<DiagnosticValue> Props, IReadOnlyList<DiagnosticValue> State, IReadOnlyList<DiagnosticValue> Contexts)
IReadOnlyList<DiagnosticValue> Contexts { get; init; }
IReadOnlyList<DiagnosticValue> Props { get; init; }
IReadOnlyList<DiagnosticValue> State { get; init; }
bool IsRoot { get; init; }
string Kind { get; init; }
string Name { get; init; }
Deconstruct(ref string Name, ref string Kind, ref bool IsRoot, ref IReadOnlyList<DiagnosticValue> Props, ref IReadOnlyList<DiagnosticValue> State, ref IReadOnlyList<DiagnosticValue> Contexts) → void
Equals(ComponentSnapshot other) → bool
Equals(object obj) → bool
GetHashCode() → int
ToString() → string

### static class ComponentUseMemoCellsExtensions
Component.UseMemoCells<T>(IReadOnlyList<T> items, Func<T, int, Element> builder, object[] dependencies) → Element[]
Component.UseMemoCellsByIndex<T>(IReadOnlyList<T> items, IReadOnlyList<int> changedIndices, Func<T, int, Element> builder, object[] dependencies) → Element[]
Expand Down Expand Up @@ -1952,6 +1977,21 @@ Equals(object obj) → bool
GetHashCode() → int
ToString() → string

### record DiagnosticValue
new(int Index, string Name, string Kind, string Type, string Value, bool Editable, bool MigratedByHotReload)
bool Editable { get; init; }
bool MigratedByHotReload { get; init; }
int Index { get; init; }
string Kind { get; init; }
string Name { get; init; }
string Type { get; init; }
string Value { get; init; }
Deconstruct(ref int Index, ref string Name, ref string Kind, ref string Type, ref string Value, ref bool Editable, ref bool MigratedByHotReload) → void
Equals(DiagnosticValue other) → bool
Equals(object obj) → bool
GetHashCode() → int
ToString() → string

### record DirectionalTransition
new(Transition enter, Transition exit)
Transition EnterTransition { get; init; }
Expand Down Expand Up @@ -4556,7 +4596,12 @@ Register(IReactorDevtoolsHost host) → void
### static class ReactorDiagnostics
IReadOnlyList<KeyedListDiagnostic> RecentKeyedListWarnings { get; }
ClearRecentKeyedListWarnings() → void
DescribeComponent(UIElement element) → ComponentSnapshot
GetAppliedProperties(UIElement control) → IReadOnlyList<AppliedProperty>
GetHosts() → IReadOnlyList<ReactorHostInfo>
GetReferenceEdges(UIElement control) → IReadOnlyList<ReferenceEdgeSnapshot>
Rerender(UIElement element) → bool
TrySetState(UIElement element, int hookIndex, string text, ref string error) → bool

### static class ReactorDisplay
DisplayInfo Primary { get; }
Expand Down Expand Up @@ -4753,6 +4798,21 @@ ToString() → string
new(T initial)
T Current { get; set; }

### record ReferenceEdgeSnapshot
new(string Property, bool IsList, int Index, string TargetAutomationId, string ExpectedTargetTypeName, bool IsResolved, UIElement Target)
UIElement Target { get; init; }
bool IsList { get; init; }
bool IsResolved { get; init; }
int Index { get; init; }
string ExpectedTargetTypeName { get; init; }
string Property { get; init; }
string TargetAutomationId { get; init; }
Deconstruct(ref string Property, ref bool IsList, ref int Index, ref string TargetAutomationId, ref string ExpectedTargetTypeName, ref bool IsResolved, ref UIElement Target) → void
Equals(ReferenceEdgeSnapshot other) → bool
Equals(object obj) → bool
GetHashCode() → int
ToString() → string

### static class ReflectionTypeMetadataProvider
CreateDescriptor(PropertyInfo property, int defaultOrder) → FieldDescriptor
CreateMetadata(Type type) → TypeMetadata
Expand Down
Loading
Loading