Skip to content

Replay ancestor Context when virtualized hosts realize rows - #1387

Open
Alexandre Zollinger Chohfi (azchohfi) wants to merge 11 commits into
mainfrom
azchohfi-lazy-row-context-probe
Open

Alexandre Zollinger Chohfi (azchohfi) wants to merge 11 commits into
mainfrom
azchohfi-lazy-row-context-probe

Conversation

@azchohfi

@azchohfi Alexandre Zollinger Chohfi (azchohfi) commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Virtualized hosts (LazyVStack/ItemsRepeater, ItemsView, typed and untyped ListView/GridView, typed TreeView) build rows from WinUI layout callbacks (IElementFactory.GetElement, ContainerContentChanging). Those callbacks run outside the reconciler's tree walk: either after it has finished (first display, scrolling), or in the middle of an unrelated update, when something calls UpdateLayout() synchronously. Rows built there had two problems.

1. Rows couldn't see their ancestors' Context. UseContext returned the default and child-mode UseNavigation<T>() threw.

  • Fix: each host saves its provider values (a ContextCapture on its ReactorState) whenever it is reconciled. Row building swaps those values in, replacing the live stack rather than pushing on top, so a row never sees whatever subtree happens to be mid-update.
  • Keeping the saved values fresh. A list counts as out of date when the live providers no longer match the start of its saved values, or when the reconciler's provider generation has moved. The generation goes up whenever any element's provided values change, including additions and removals. Removals can't be detected by the match alone, and a list nested in a scrolled-off row may only be reached in a later update, so it has to rely on the generation.
  • An out-of-date list declines the "unchanged element" skip and re-saves its values.
  • Replaying an out-of-date snapshot uses a one-off generation token, so nothing captured from it ever looks current, even across repeated rebuilds. That includes an inner list mounted inside a rebuilt row. A provider change inside a rebuilt row also advances that replay's generation.
  • The skip path re-saves at the list's own depth. Only the element that registered the snapshot may refresh it, so a decorator sharing the control (Flyout, …) can't overwrite it.
  • While a row is being rebuilt from saved values, context checks stay on even if those values are empty.
  • ElementFactory<T> takes its own snapshot when no host snapshot is attached, so a custom handler built on it is also covered.
  • Pooling or retiring a host empties its saved values, so the factory that shares them stops keeping old provider objects alive.

2. A row built mid-update joined that update's bookkeeping.

  • If the update was mounting an ErrorBoundary whose child failed, the rollback also tore down those rows (unrelated, still on screen) and cleaned up their effects.
  • A row that threw was reported to that unrelated boundary instead of showing its own error.

Fix: row building now runs inside a RealizationFrame. While the row is built it:

  • sets aside the update's boundary rollback lists and depth, its provider- and theme-change depths, the element being reconciled, the stagger state, and the diagnostic owner;
  • swaps in the saved providers;
  • picks the theme for ThemeRef brushes, as below;

and restores everything afterwards.

How the theme is picked:

  • During the update that last reached the list, the row uses the theme that update is applying to the list's subtree, because the native RequestedTheme is only applied after children.
  • Otherwise it reads the list control's live ActualTheme, so an ancestor theme change while the list is skipped is also picked up.
  • An element whose own RequestedTheme changed makes the lists below it decline the skip once. A theme generation tracks this, mirroring the provider generation, so a skipped list re-records the pending theme before a later sibling can force layout in the same update.

Linked issue / spec

Fixes #1386

Related: microsoft/windows-rs#5085, where windows-reactor hits the same synchronous-realization constraint.

Test plan

  • Unit tests (tests/Reactor.Tests/ContextCaptureUnitTests.cs) for:

    • the match and swap helpers (IsPrefixOf, Matches, Replace/Restore, including nested replay);
    • when a snapshot counts as out of date, including the removed provider, a generation change in an earlier update, provider changes during a replay, and a parked grandchild after a second reuse;
    • Clear, and context checks staying on during an empty replay.
  • Context selftests (VirtualizedRowContextFixtures.cs):

    • rows on first display, while scrolling, and after a parent re-render;
    • provider value changed, only provider removed, and inner provider removed, each while the list itself is unchanged;
    • removing the last provider above a reused component;
    • a list's own provider under a Flyout, both updated and unchanged;
    • a scrolled-off row reused after its provider was removed;
    • child-mode UseNavigation;
    • typed ListView/GridView/ItemsView/TreeView and untyped ListView/GridView.
  • Isolation selftests (RealizationIsolationFixtures.cs):

    • rows built during a failing boundary mount stay alive;
    • a row that throws during a boundary mount shows its own error and doesn't trigger the boundary;
    • a list nested in a reused row picks up a provider removal;
    • a retired host releases the saved values the factory shares;
    • an ancestor theme flipped while the list is skipped, with no Context providers;
    • rows built during the update that flips an ancestor theme use the new theme, both when the list changes and when it is unchanged;
    • an inner list mounted while an out-of-date snapshot is replayed is not treated as current;
    • a reused row that drops its own .Provide around an unchanged inner list refreshes that list.
  • Checked against code without the fix. On e3a320f5e, the isolation checks fail:

    • cleanups=14 of 17 visible rows;
    • boundary tripped by the row;
    • n0.15:removed;
    • entries=1.

    The later checks fail on the commit before their fix:

    • theme: 5d043c3c9 (right=0/27), ef99d20eb (right=0/15) and 4f9856dbb (right=14/25);
    • replay: 0cea32b0f (declines skip) and 7665a9e60 (entries=1).
  • dotnet run --project tests/Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test (full suite, after merging main): 9,197 ok, 1 failure. The failure is ReactorSource_LiveDialog_SourceKeptSkip_ClosedDialogStopsDescribingItsElement, a new main diagnostics test with no virtualized list that waits two 50 ms renders for a ContentDialog to close. It passes 3/3 when run alone.

  • dotnet test tests/Reactor.Tests --no-build -p:Platform=x64 (after merging main): passed, 15,109 succeeded, 0 failed.

  • dotnet build src/Reactor/Reactor.csproj -c Release: succeeded. dotnet build Reactor.slnx --no-restore -c Release (earlier head): 0 compiler or analyzer errors; only the src/vs-reactor projects failed to restore, because the local proxy blocks nuget.org.

  • dotnet run --project tools/Reactor.DocPipeline -- compile --no-screenshots: no changes after the merge; the GetElement snippet in architecture-overview.md is current.

  • Perf (tests/stress_perf/ci/Run-PerfBenchmark.ps1, 3 interleaved runs against the base 20a076b1b, measured at 2cbc3c3b5): every macro workload, the allocation counts and all 17 reconciler micro-benchmarks were within noise. Later commits only add per-row-build field swaps plus one ActualTheme read, and re-save each list's snapshot once after a provider or theme change.

Risk / breaking changes

  • No public API change. All new types and members are internal.

  • Behavior changes, both in the CHANGELOG:

    • Rows now read provided values instead of defaults.
    • A row that throws while being built during another update now shows its own error instead of triggering an unrelated ErrorBoundary.
  • Perf: after a provider or RequestedTheme change, each virtualized list is updated once even if unchanged; nothing changes in steady state.

  • Window hooks in rows: handled by main's Resolve window hooks from the reconciler-supplied owner, not ActiveHostInternal #1399, where window hooks read the reconciler-supplied owner that every component render gets. This PR doesn't touch ReactorApp.ActiveHostInternal.

  • Known follow-ups (not fixed here): two compound edge cases from the final review round. Both need an inner virtualized list, an ancestor RequestedTheme change, synchronous layout forced in the same update, and ThemeRef resource overrides on the inner rows:

    • an earlier sibling forces the outer list to build a row before the update reaches the list; an inner list mounted in that row records the not-yet-applied old theme as current;
    • in an app with no Context providers, a reused row's unchanged inner list isn't re-checked for the theme change while the row is being rebuilt.

    The fix for both is to record whether a replayed theme is current, so anything derived from an outdated one stays stale until the normal update refreshes it. On main these rows were already resolved against whatever theme happened to be active mid-update.

  • Not covered: a custom handler that builds rows itself from ContainerContentChanging, without ElementFactory<T>. It gets neither the saved context nor the isolation (noted in the CHANGELOG).

Virtualized hosts (LazyVStack/ItemsRepeater, ItemsView, ListView/GridView,
typed TreeView) build rows from WinUI layout callbacks that run outside the
reconciler traversal, so rows read an empty or unrelated ContextScope:
UseContext returned the default and child-mode UseNavigation threw.

Each host now snapshots the provider stack when it is reconciled, and the
realization callbacks replay that snapshot around the row mount. A host whose
snapshot no longer matches the live providers declines the reference-stable
skip so the snapshot is refreshed before the next realization.

Fixes #1386

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
@azchohfi Alexandre Zollinger Chohfi (azchohfi) added the agent-preparing Agent is addressing feedback or completing required validation and CI label Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build metrics

Artifact sizes for 829a5b5 vs the base branch (5ba98bf).

Packages (compressed .nupkg)

Artifact base PR Δ
Microsoft.UI.Reactor.nupkg 1.90 MB 1.91 MB +6.03 KB (+0.31%) ⚠️
Microsoft.UI.Reactor.Advanced.nupkg 493.3 KB 493.3 KB +3 B (0.00%) ≈
Microsoft.UI.Reactor.Devtools.nupkg 287.9 KB 287.9 KB +8 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor

Artifact base PR Δ
Reactor.Analyzers.dll 410.5 KB 410.5 KB +0 B (0.00%) ≈
Reactor.dll 2.65 MB 2.66 MB +7.50 KB (+0.28%) ⚠️
Reactor.Localization.Generator.dll 16.0 KB 16.0 KB +0 B (0.00%) ≈
Reactor.Wrappers.Abstractions.dll 10.5 KB 10.5 KB +0 B (0.00%) ≈
Reactor.Wrappers.Generator.dll 99.5 KB 99.5 KB +0 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor.Advanced

Artifact base PR Δ
Reactor.Advanced.dll 1.00 MB 1.00 MB +0 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor.Devtools

Artifact base PR Δ
Microsoft.UI.Reactor.Devtools.dll 787.0 KB 787.0 KB +0 B (0.00%) ≈

✅ smaller / ⚠️ larger / ≈ within noise. Sizes come from a Release dotnet pack on the CI runner: packages are the compressed .nupkg download size, assemblies the uncompressed DLL inside it.
workflow run.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

🧪 Merged coverage

Coverage for 829a5b5 vs the base branch (5ba98bf) — unit + selftest merged.

Metric base PR Δ
Line 87.34% 87.39% +0.05 pp ≈
Branch 76.77% (978/1274) 76.57% (977/1276) -0.20 pp ⚠️

✅ higher / ⚠️ lower / ≈ within noise. Δ is in percentage points; coverage is unit + selftest merged (Debug x64) on the CI runner. Cobertura reports attached to the workflow run as artifacts.

- Detect a provider removed above a skipped virtualized host: count
  non-skip Updates whose own ContextValues changed and treat any host
  snapshot (and the context skip gate) below them as stale. Also fixes a
  reference-stable consumer keeping a removed provider's value when the
  last provider is dropped.
- Re-snapshot on the element skip arm so a host sharing its control with
  a decorator (Flyout, ...) keeps a .Provide() on the list element itself.
- ElementFactory<T> captures its own snapshot when no host capture is
  attached, so out-of-tree handlers built on it are covered.
- Refresh reuses the snapshot array when nothing changed.
- Unit tests for ContextScope/ContextCapture primitives; selftests for
  provider removal, decorator-wrapped list, typed GridView and untyped
  ListView/GridView. CHANGELOG entry.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Memoized components and skipped decorators can leave row context incorrect, while fallback captures retain obsolete provider objects.

3 open findings
What changed in this PR

Restores ancestor context for Reactor’s virtualized rows, addressing #1386.

Changes:

  • Captures provider values and replays them during row realization.
  • Refreshes captures during reconciliation and handles provider removal.
  • Adds unit tests, selftests, and release notes.
File Description
tests/​Reactor.Tests/​ContextCaptureUnitTests.cs Tests snapshot and replay primitives.
tests/​Reactor.AppTests.Host/​SelfTest/​SelfTestFixtureRegistry.cs Registers new fixtures.
tests/​Reactor.AppTests.Host/​SelfTest/​Fixtures/​VirtualizedRowContextFixtures.cs Tests row context and navigation.
src/​Reactor/​Core/​V1Protocol/​TemplatedListLifecycle.cs Captures typed-list context.
src/​Reactor/​Core/​V1Protocol/​Reconciler.TemplatedTree.cs Replays context for tree nodes.
src/​Reactor/​Core/​V1Protocol/​LazyStackLifecycle.cs Attaches lazy-stack captures.
src/​Reactor/​Core/​V1Protocol/​Handlers/​ListViewHandler.cs Replays untyped-list context.
src/​Reactor/​Core/​V1Protocol/​Handlers/​GridViewHandler.cs Replays untyped-grid context.
src/​Reactor/​Core/​Reconciler.Update.cs Refreshes captures during updates.
src/​Reactor/​Core/​Reconciler.Mount.cs Replays typed-row context.
src/​Reactor/​Core/​Reconciler.KeyedItemsBinding.cs Connects keyed hosts to captures.
src/​Reactor/​Core/​Reconciler.cs Adds capture lifecycle and invalidation.
src/​Reactor/​Core/​IItemsRepeaterFactorySource.cs Defines internal capture contract.
src/​Reactor/​Core/​ElementFactory.cs Adds factory capture and replay.
src/​Reactor/​Core/​ContextScope.cs Implements snapshot comparison and replacement.
CHANGELOG.md Documents behavior changes and limitations.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Reactor/Core/ElementFactory.cs Outdated
Comment thread src/Reactor/Core/Reconciler.cs Outdated
Comment thread src/Reactor/Core/Reconciler.cs Outdated
- Refresh a virtualized host's context snapshot only from the element that
  registered it, so a reference-stable decorator (Flyout, ...) sharing the
  host's control cannot overwrite it at its own shallower depth.
- ElementFactory<T> drops its fallback snapshot once a host capture is
  attached, so it no longer pins mount-time provider values.
- Regenerate docs/guide/architecture-overview.md (GetElement snippet).
- Selftest for a skipped whole decorator; list fixtures wait for
  realization (TreeView realizes over more than one layout wave).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Recycled rows can retain a removed provider’s value when an empty context capture disables context checking.

0 open findings

3 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Recycled rows retain removed provider values after context replay

src/​Reactor/​Core/​Reconciler.cs:1906

A recycled row can retain the last provider's removed value. ElementFactory.RecycleElement removes parked rows from _mountedElements without unmounting them, so the provider-removal update does not refresh those rows. When GetElement later reuses one under the now-empty capture, _providerChangeDepth is zero and this predicate disables context checking. A shallow-equal row root, such as a Border around a reused consumer element, then skips and keeps the removed value instead of reading the default. Keep context checks enabled during captured-context replay even for empty snapshots, with balanced restoration for nested calls and exceptions. Add a regression that recycles a row, removes its only provider, and realizes the same row again.

🧠 Review effort: Balanced

Keep context skip checks enabled while a captured snapshot is replayed,
even when the snapshot is empty because the host's last provider was
removed. A recycled row reused for the same item under that empty
snapshot otherwise shallow-skips its reference-stable consumer and keeps
the removed value. Gated on the scope having ever seen a provider, so
provider-free apps pay nothing during realization.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Nested recycled hosts can retain removed context, and detached factories can retain obsolete provider objects.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Recycled nested hosts retain removed provider values

src/​Reactor/​Core/​ContextScope.cs:164

A recycled row can contain a nested host whose capture still includes a removed ancestor provider. On later GetElement, the outer factory replays its updated snapshot, but _providerChangeDepth is already zero. For a shallow-equal Border(stableNestedList) with no realized context consumers, the shorter replay remains a prefix of the nested capture, so this check allows the Border to skip. The nested host never refreshes, and rows it realizes later read the removed value. Carry removal invalidation into recycled-row replay, and add a nested-host variant of Factory_RecycledRowAfterLastProviderRemoved. This occurs at the element-level skip, without a component memo boundary.

Medium severity Clear factory host capture when retiring nested host state

src/​Reactor/​Core/​Reconciler.cs:1259

Dropping this reference leaves the same snapshot reachable through ElementFactory<T>._hostCapture. For example, retiring an outer repeater row containing a nested virtualized host detaches its Reactor state but leaves the native subtree and nested ItemTemplate/factory alive. The factory can therefore retain obsolete ancestor provider objects for the outer list's lifetime. Empty the shared capture before dropping the state reference so factory aliases also release those values, and add a nested-host retirement regression.

Review effort: Balanced

Rows realized by WinUI layout can run re-entrantly inside an unrelated
Reactor update (any synchronous UpdateLayout). They used to join that
update's bookkeeping. Realization now runs in a RealizationFrame that sets
aside the ErrorBoundary rollback journal/roots and boundary depth, the
provider-change depth, the reconciling element and stagger scope, replays
the host's captured providers and ambient theme, and makes the owning
ReactorHost active for window-scoped hooks.

- A failing boundary child no longer tears down live rows in another list.
- A row that throws gets its own in-tree fallback instead of tripping an
  unrelated boundary.
- Rows realized outside a render resolve their own window host.

Round 3 findings:
- Host snapshots record a provider generation bumped on every provider
  change, so a nested host in a recycled row that comes back in a later
  pass still re-snapshots after a removed provider.
- Retiring/pooling a host empties its capture so the aliasing
  ElementFactory no longer keeps old provider values alive.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Ancestor theme changes can leave skipped hosts replaying a stale theme for newly realized rows.

1 open issue

Review effort: Balanced

Comment thread src/Reactor/Core/Reconciler.cs Outdated
- Read the ambient theme for realized rows live from the host control's
  ActualTheme instead of capturing it at reconcile time. A RequestedTheme
  change on an ancestor while the host is skipped no longer leaves rows
  resolving ThemeRef overrides against the old theme (works with no
  Context providers at all).
- Use an epsilon comparison in the nested-host fixture's selector.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Reentrant changes to core reconciler state require human review, and correctness concerns remain unresolved.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Provider changes do not refresh realized ListView or GridView rows

src/​Reactor/​Core/​V1Protocol/​Handlers/​ListViewHandler.cs:40

Refreshing the capture does not refresh already-realized rows in either untyped ListView or GridView. With a stable Items array, changing or removing an ancestor provider now declines the host's skip and refreshes its snapshot, but both handlers leave ItemsSource unchanged and never reconcile existing rows (ListViewHandler.cs:187–192,222; GridViewHandler.cs:165–170). Visible consumers therefore retain the old provided value while newly realized rows read the new value. Reconcile realized containers when their context changes, rather than only updating the snapshot for future realization. Add provider-change and provider-removal regressions for both untyped hosts with a stable Items array.

1 resolved since last review

Review effort: Balanced

Comment thread src/Reactor/Core/Reconciler.cs
Use the pass's pending subtree theme for rows realized re-entrantly in
the same reconcile pass that last reached their host. The native
RequestedTheme modifier is applied only after children are dispatched,
so the host's ActualTheme still shows the previous theme then. Hosts
record the pass id and effective theme at mount, update and skip;
outside that pass the live ActualTheme is still used, so a host skipped
while an ancestor theme changes stays correct.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Nested hosts created during stale context replay can be marked current and retain removed provider values.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Nested snapshots incorrectly mark replayed provider values as current

src/​Reactor/​Core/​ContextScope.cs:172

A nested host can capture removed providers and incorrectly be marked current here. After a provider is removed, an earlier sibling can synchronously realize rows before the outer list refreshes its capture. Those rows replay the old values, but a nested list mounted inside them records the new ProviderGeneration. If its initially realized rows do not consume context, the subsequent traversal can skip the unchanged row subtree: the nested capture's generation matches, and the empty live stack passes IsPrefixOf. Later nested rows then read the removed provider. Preserve the replay source's generation when taking nested snapshots, rather than marking old values current. Add a regression that realizes nested hosts before the outer host refreshes, then scrolls the nested list to context-consuming rows.

1 resolved since last review

Review effort: Balanced

A host snapshot taken while an outdated snapshot is being replayed (a
nested host mounted inside a row realized before the traversal refreshed
the outer host) now records the replayed snapshot's provider generation
instead of the current one, so it is not marked current while holding a
removed provider. ContextScope tracks the generation of each replay;
captures record and compare against SnapshotGeneration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Removing a provider during row reuse can leave nested virtualized hosts serving stale context.

1 open issue

Review effort: Balanced

Comment thread src/Reactor/Core/ContextScope.cs
A provider change inside a replayed subtree (a reused row dropping its
own .Provide around a reference-stable nested host) now advances the
replay's generation: to the new live generation if the replay was
current, or to a unique never-current value if it was already stale.
Nested snapshots taken under the replay before the change are stale, so
the nested host declines the skip and re-snapshots.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Same-pass theme handling for skipped lists and generation tracking across repeated replays still have correctness gaps.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Prevent repeated capture replay from restoring invalidated providers

src/​Reactor/​Core/​ContextScope.cs:168

Replaying the same capture again can undo the new provider invalidation. An outer factory can retain generation 0 while a reused row drops .Provide: the first reuse advances the generation to 1 and refreshes an inner list to empty/gen1, but a parked grandchild list still holds the removed provider/gen0. A second reuse replays gen0 here, and refreshing the inner list stamps its empty capture with gen0 again. When that inner list realizes the parked grandchild, IsStale returns false: the generations match and the empty stack is a prefix of the old capture. Later rows can therefore read the removed provider. Preserve invalidation across repeated replays, for example by assigning stale replay invocations distinct non-live generation tokens. Extend the regression with a second recycle/reuse and a parked grandchild.

Medium severity Propagate pending theme through child skips during same-pass realization

src/​Reactor/​Core/​Reconciler.cs:2079

The pending-theme fix misses child-level skips. ChildReconciler.UpdateCommonChild (lines 221–248) can return without either capture-refresh helper. With no Context providers, keep a list reference-stable, change its ancestor's RequestedTheme, and scroll/force layout from a later sibling's effect in the same update. The capture still has the previous pass ID, so this reads the old ActualTheme before the ancestor's modifier is applied. Newly realized rows then retain incorrectly resolved ThemeRef resource overrides. The current in-pass fixture changes the list's items, while the stable-list fixture scrolls after the update; neither covers this combination. Propagate the pending theme through child/container skips, respecting host-local overrides, and add a stable-list, same-pass realization regression.

1 resolved since last review

Review effort: Balanced

- Every replay of an outdated host snapshot gets a fresh generation token
  that no live generation or other replay can equal, so snapshots taken
  from it are never treated as current - including across repeated
  replays of the same outdated snapshot (parked grandchild after a second
  reuse).
- Track RequestedTheme changes like provider changes: a non-skip Update
  whose own RequestedTheme changed bumps a theme generation and keeps the
  context skip check active below it. A virtualized host whose recorded
  theme generation is behind declines the skip once, so its Update
  records the pending subtree theme before a later sibling forces layout
  in the same pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-cutting re-entrant reconciliation changes and unresolved nested-theme freshness issues require final human review.

2 open issues

Review effort: Balanced

Comment thread src/Reactor/Core/Reconciler.cs
Comment thread src/Reactor/Core/Reconciler.cs
@azchohfi Alexandre Zollinger Chohfi (azchohfi) added ready-for-review Agent work and technical checks complete; awaiting review, not approval or merge and removed agent-preparing Agent is addressing feedback or completing required validation and CI labels Oct 19, 2026
Resolve the CHANGELOG conflict by keeping both Fixed entries.

Main's #1399 resolves window hooks from the reconciler-supplied owner,
which every component render receives (including virtualized rows), so
drop this branch's ActiveHostInternal swap in RealizationFrame, the
ReactorHost.OwnerHost wiring and its fixture; main's
WindowHookOwnerFixtures.VirtualizedRow covers that case. RealizationFrame
now also sets aside main's per-render diagnostic owner, so a row built
re-entrantly is not attributed to the component being rendered.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
@azchohfi Alexandre Zollinger Chohfi (azchohfi) added agent-preparing Agent is addressing feedback or completing required validation and CI ready-for-review Agent work and technical checks complete; awaiting review, not approval or merge and removed ready-for-review Agent work and technical checks complete; awaiting review, not approval or merge agent-preparing Agent is addressing feedback or completing required validation and CI labels Oct 22, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review Agent work and technical checks complete; awaiting review, not approval or merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Virtualized list rows don't see ancestor Context (UseContext returns default; UseNavigation throws)

2 participants