diff --git a/NuGet.config b/NuGet.config index 128d95e59..04d645b5f 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,10 +3,16 @@ + + + + + + diff --git a/SalmonEgg/Directory.Packages.props b/SalmonEgg/Directory.Packages.props index 0fec63a73..e3a7018cd 100644 --- a/SalmonEgg/Directory.Packages.props +++ b/SalmonEgg/Directory.Packages.props @@ -38,6 +38,9 @@ + + diff --git a/SalmonEgg/SalmonEgg/MainPage.xaml.cs b/SalmonEgg/SalmonEgg/MainPage.xaml.cs index cc5d464dd..282a79f9e 100644 --- a/SalmonEgg/SalmonEgg/MainPage.xaml.cs +++ b/SalmonEgg/SalmonEgg/MainPage.xaml.cs @@ -779,6 +779,10 @@ private async void OnMainPageLoaded(object sender, RoutedEventArgs e) // sampling remain owned by the independent probe; the page only exposes the live shell root. NumberBoxThemeProbeDriver.TryStart(App.ServiceProvider, this); + // Diagnostics-only Markdown ThemeListener lifecycle probe. The seeded mixed conversation + // owns its content; the probe only exercises load, theme, reactivation, unload and reload. + MarkdownThemeProbeDriver.TryStart(App.ServiceProvider, this); + // The opt-in setup probe owns navigation and sampling; the shell only supplies its live root. AcpSetupRuntimePathProbeDriver.TryStart(App.ServiceProvider, this); diff --git a/SalmonEgg/SalmonEgg/Presentation/Diagnostics/MarkdownThemeProbeDriver.cs b/SalmonEgg/SalmonEgg/Presentation/Diagnostics/MarkdownThemeProbeDriver.cs new file mode 100644 index 000000000..9a02425c7 --- /dev/null +++ b/SalmonEgg/SalmonEgg/Presentation/Diagnostics/MarkdownThemeProbeDriver.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using SalmonEgg.Controls; +using SalmonEgg.Presentation.ViewModels.Navigation; + +namespace SalmonEgg.Presentation.Diagnostics; + +/// +/// Diagnostics-only runtime probe for the Skia Markdown ThemeListener lifecycle regression. +/// +/// +/// The Toolkit ThemeListener historically dereferenced Window.Current.CoreWindow, which does +/// not exist on Skia desktop windows; loading or disposing a Markdown control could then raise a +/// dispatcher exception. The driver exercises the real seeded Markdown conversation end to end — +/// materialized presenter, theme toggle, native window reactivation, authoritative selection switch +/// away (unload) and back (reload) — and reports only observable UI facts. It is compiled out of +/// Release behavior and inert unless SALMONEGG_MARKDOWN_THEME_PROBE=1. +/// +internal static class MarkdownThemeProbeDriver +{ + private const string EnableVariable = "SALMONEGG_MARKDOWN_THEME_PROBE"; + + // Mirrors SkiaDesktopGuiSeedWriter's constants; the app cannot reference the test-support + // assembly, and the shell gate hardcodes the same ids when seeding. + private const string MixedConversationId = "skia-mixed-session-01"; + private const string PlainConversationId = "skia-plain-session-02"; + private const string ProjectId = "project-1"; + + private const int StepTimeoutMilliseconds = 15000; + private const int PollDelayMilliseconds = 50; + +#if DEBUG && __UNO_SKIA__ + private static int _started; +#endif + + public static void TryStart(IServiceProvider services, DependencyObject shellRoot) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(shellRoot); + +#if DEBUG && __UNO_SKIA__ + if (!string.Equals(Environment.GetEnvironmentVariable(EnableVariable), "1", StringComparison.Ordinal) + || Interlocked.Exchange(ref _started, 1) != 0) + { + return; + } + + var navigation = services.GetRequiredService(); + _ = RunAsync(navigation, shellRoot); +#endif + } + +#if DEBUG && __UNO_SKIA__ + private static async Task RunAsync(MainNavigationViewModel navigation, DependencyObject shellRoot) + { + var loaded = false; + var themeToggled = false; + var reactivated = false; + var unloaded = false; + var reloaded = false; + + try + { + App.BootLog("MarkdownThemeProbe: started"); + + // The NumberBox probe drives the content frame into settings and back; it owns the + // frame until it completes. Wait for it instead of racing it for navigation. + await NumberBoxThemeProbeDriver.Completion.WaitAsync(TimeSpan.FromSeconds(120)).ConfigureAwait(true); + + // Re-assert the seeded conversation through the authoritative selection so the chat + // transcript is on screen regardless of where the frame was left. + if (!await navigation.ActivateSessionAsync(MixedConversationId, ProjectId).ConfigureAwait(true)) + { + throw new InvalidOperationException( + $"The markdown conversation '{MixedConversationId}' did not activate."); + } + + var firstPresenter = await WaitUntilAsync( + () => FindRenderedPresenter(shellRoot), StepTimeoutMilliseconds).ConfigureAwait(true); + loaded = firstPresenter is not null; + App.BootLog($"MarkdownThemeProbe: step=loaded rendered={loaded}"); + + if (loaded) + { + themeToggled = await ToggleThemeAsync().ConfigureAwait(true); + App.BootLog($"MarkdownThemeProbe: step=theme toggled={themeToggled}"); + + reactivated = ReactivateWindow(); + App.BootLog($"MarkdownThemeProbe: step=reactivate activated={reactivated}"); + + // Switch the authoritative selection away: the plain sibling conversation has no + // markdown rows, so the presenter must unload and dispose with its ThemeListener. + if (!await navigation + .ActivateSessionAsync(PlainConversationId, ProjectId).ConfigureAwait(true)) + { + throw new InvalidOperationException( + $"The plain sibling conversation '{PlainConversationId}' did not activate."); + } + + var gone = await WaitUntilAsync( + () => FindRenderedPresenter(shellRoot) is null, StepTimeoutMilliseconds).ConfigureAwait(true); + unloaded = gone; + App.BootLog($"MarkdownThemeProbe: step=unload gone={unloaded}"); + + if (!await navigation + .ActivateSessionAsync(MixedConversationId, ProjectId).ConfigureAwait(true)) + { + throw new InvalidOperationException( + $"The markdown conversation '{MixedConversationId}' did not re-activate."); + } + + var secondPresenter = await WaitUntilAsync( + () => + { + var candidate = FindRenderedPresenter(shellRoot); + return candidate is not null && !ReferenceEquals(candidate, firstPresenter) + ? candidate + : null; + }, + StepTimeoutMilliseconds).ConfigureAwait(true); + reloaded = secondPresenter is not null; + App.BootLog($"MarkdownThemeProbe: step=reload recreated={reloaded}"); + } + + var passed = loaded && themeToggled && reactivated && unloaded && reloaded; + App.BootLog( + $"MarkdownThemeProbe: complete loaded={loaded} themeToggled={themeToggled}" + + $" reactivated={reactivated} unloaded={unloaded} reloaded={reloaded} passed={passed}"); + } + catch (Exception ex) + { + // A dispatcher or layout fault anywhere in the lifecycle is the regression signature + // the gate exists to catch; the absence of the complete marker fails the gate. + App.BootLog($"MarkdownThemeProbe: faulted loaded={loaded} themeToggled={themeToggled}" + + $" reactivated={reactivated} unloaded={unloaded} reloaded={reloaded} exception={ex}"); + throw; + } + } + + private static MarkdownTextPresenter? FindRenderedPresenter(DependencyObject root) + => Find(root, static presenter => + presenter.ActualHeight > 0 && presenter.Visibility == Visibility.Visible); + + private static async Task ToggleThemeAsync() + { + if (App.MainWindowInstance?.Content is not FrameworkElement root) + { + return false; + } + + var original = root.RequestedTheme; + var opposite = original is ElementTheme.Light ? ElementTheme.Dark : ElementTheme.Light; + root.RequestedTheme = opposite; + await AwaitNextFrameAsync().ConfigureAwait(true); + + // The ThemeListener reacts on its own dispatcher subscription; give the theme change a + // chance to propagate before restoring the preference. + await Task.Delay(150).ConfigureAwait(true); + var toggled = root.RequestedTheme == opposite; + + if (root.RequestedTheme != original) + { + root.RequestedTheme = original; + await AwaitNextFrameAsync().ConfigureAwait(true); + } + + return toggled; + } + + private static bool ReactivateWindow() + { + if (App.MainWindowInstance is not { } window) return false; + window.Activate(); + return true; + } + + private static async Task AwaitNextFrameAsync() + { + var rendered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + void OnRendered(object? sender, object args) => rendered.TrySetResult(); + CompositionTarget.Rendering += OnRendered; + try + { + await rendered.Task.ConfigureAwait(true); + } + finally + { + CompositionTarget.Rendering -= OnRendered; + } + } + + private static async Task WaitUntilAsync(Func probe, int timeoutMilliseconds) + where T : class + { + using var cancellation = new CancellationTokenSource(timeoutMilliseconds); + while (true) + { + if (probe() is { } result) return result; + await Task.Delay(PollDelayMilliseconds, cancellation.Token).ConfigureAwait(true); + } + } + + private static async Task WaitUntilAsync(Func condition, int timeoutMilliseconds) + { + using var cancellation = new CancellationTokenSource(timeoutMilliseconds); + while (!condition()) + { + await Task.Delay(PollDelayMilliseconds, cancellation.Token).ConfigureAwait(true); + } + + return true; + } + + private static T? Find(DependencyObject root, Func? predicate = null) + where T : class, DependencyObject + { + predicate ??= static _ => true; + if (root is T match && predicate(match)) return match; + for (var index = 0; index < VisualTreeHelper.GetChildrenCount(root); index++) + { + if (Find(VisualTreeHelper.GetChild(root, index), predicate) is { } descendant) return descendant; + } + + return null; + } +#endif +} diff --git a/SalmonEgg/SalmonEgg/Presentation/Diagnostics/NumberBoxThemeProbeDriver.cs b/SalmonEgg/SalmonEgg/Presentation/Diagnostics/NumberBoxThemeProbeDriver.cs index 4d8cd2eb4..9facd0ff6 100644 --- a/SalmonEgg/SalmonEgg/Presentation/Diagnostics/NumberBoxThemeProbeDriver.cs +++ b/SalmonEgg/SalmonEgg/Presentation/Diagnostics/NumberBoxThemeProbeDriver.cs @@ -47,6 +47,11 @@ internal static class NumberBoxThemeProbeDriver private const double MinimumContrastRatio = 4.5; #if DEBUG && __UNO_SKIA__ private static int _started; + private static readonly TaskCompletionSource s_completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + + /// Completes when the probe run finishes, succeeded or faulted; lets sibling probes + /// that drive navigation wait instead of racing this one for the content frame. + internal static Task Completion => s_completion.Task; #endif public static void TryStart(IServiceProvider services, DependencyObject shellRoot) @@ -90,6 +95,7 @@ private static async Task RunAsync(MainNavigationViewModel navigation, Dependenc $"NumberBoxThemeProbe: complete samples={result.CompletedSamples}" + $" valueUnchanged={result.ValueUnchanged} passed={result.Passed}" + $" reason={result.FailureReason}"); + s_completion.TrySetResult(); } } @@ -369,6 +375,11 @@ private static async Task TryCaptureRenderedColorsAsync( /// reason. A bare null cannot distinguish an empty Skia frame from an off-viewport rect, /// a translucent offscreen composition or an ambiguous cluster, so every exit reports its cause. /// + // Uno0001: the analyzer's not-implemented listing marks the whole RenderTargetBitmap surface, + // yet the Skia runtime demonstrably renders and reports real pixels here (the probe's inliers + // capture and passing contrast samples are real pixels), so the listing is stale for this + // target. Silence it for this capture method rather than weakening the sampler. +#pragma warning disable Uno0001 private static async Task TryCaptureRenderedBackgroundAsync( Border borderElement, ScrollViewer? scrollHost) @@ -441,6 +452,7 @@ private static async Task TryCaptureRenderedBackgroundAsync( bounds.Y * scaleY, bounds.Width * scaleX, bounds.Height * scaleY)); +#pragma warning restore Uno0001 } /// diff --git a/SalmonEgg/SalmonEgg/SalmonEgg.csproj b/SalmonEgg/SalmonEgg/SalmonEgg.csproj index 8d709980a..2fbb15b35 100644 --- a/SalmonEgg/SalmonEgg/SalmonEgg.csproj +++ b/SalmonEgg/SalmonEgg/SalmonEgg.csproj @@ -159,6 +159,7 @@ + diff --git a/scripts/gates/run-skia-desktop-gui-smoke-gates.sh b/scripts/gates/run-skia-desktop-gui-smoke-gates.sh index dab3914aa..68ac2f994 100755 --- a/scripts/gates/run-skia-desktop-gui-smoke-gates.sh +++ b/scripts/gates/run-skia-desktop-gui-smoke-gates.sh @@ -18,6 +18,7 @@ WINDOW_CREATED_TIMEOUT_SECONDS=120 TRANSCRIPT_SEED_CONVERSATION_ID="skia-mixed-session-01" TRANSCRIPT_SEED_MARKER="SKIA_MD_MARKER_7f3a" NUMBERBOX_PROBE_COMPLETE_MARKER="NumberBoxThemeProbe: complete" +MARKDOWN_PROBE_COMPLETE_MARKER="MarkdownThemeProbe: complete" DOTNET_BIN="${DOTNET_BIN:-$(command -v dotnet || true)}" GIT_BIN="${GIT_BIN:-$(command -v git || true)}" @@ -185,10 +186,10 @@ case "${OS_NAME}" in if [ -z "${SMOKE_DISPLAY}" ]; then start_xvfb "${XVFB_BIN}" fi - LAUNCH_COMMAND=(env DISPLAY="${SMOKE_DISPLAY}" SALMONEGG_GUI=1 SALMONEGG_NUMBERBOX_THEME_PROBE=1 SALMONEGG_APPDATA_ROOT="${APPDATA_ROOT}" "${APP_PATH}") + LAUNCH_COMMAND=(env DISPLAY="${SMOKE_DISPLAY}" SALMONEGG_GUI=1 SALMONEGG_NUMBERBOX_THEME_PROBE=1 SALMONEGG_MARKDOWN_THEME_PROBE=1 SALMONEGG_APPDATA_ROOT="${APPDATA_ROOT}" "${APP_PATH}") ;; Darwin) - LAUNCH_COMMAND=(env SALMONEGG_GUI=1 SALMONEGG_NUMBERBOX_THEME_PROBE=1 SALMONEGG_APPDATA_ROOT="${APPDATA_ROOT}" "${APP_PATH}") + LAUNCH_COMMAND=(env SALMONEGG_GUI=1 SALMONEGG_NUMBERBOX_THEME_PROBE=1 SALMONEGG_MARKDOWN_THEME_PROBE=1 SALMONEGG_APPDATA_ROOT="${APPDATA_ROOT}" "${APP_PATH}") ;; *) echo "Skia Desktop GUI smoke supports Linux and macOS. Use scripts/gates/run-gui-smoke-gates.ps1 for Windows WinUI/FlaUI." >&2 @@ -263,6 +264,7 @@ deadline=$((SECONDS + 60)) shell_ready=0 transcript_ready=0 numberbox_probe_complete=0 +markdown_probe_complete=0 while [ "${SECONDS}" -lt "${deadline}" ]; do if ! kill -0 "${APP_PID}" 2>/dev/null; then cat "${STDOUT_LOG}" >&2 @@ -287,11 +289,17 @@ while [ "${SECONDS}" -lt "${deadline}" ]; do && grep -Fq "${NUMBERBOX_PROBE_COMPLETE_MARKER}" "${BOOT_LOG}"; then numberbox_probe_complete=1 fi + + if [ "${markdown_probe_complete}" -eq 0 ] \ + && grep -Fq "${MARKDOWN_PROBE_COMPLETE_MARKER}" "${BOOT_LOG}"; then + markdown_probe_complete=1 + fi fi if [ "${shell_ready}" -eq 1 ] \ && [ "${transcript_ready}" -eq 1 ] \ - && [ "${numberbox_probe_complete}" -eq 1 ]; then + && [ "${numberbox_probe_complete}" -eq 1 ] \ + && [ "${markdown_probe_complete}" -eq 1 ]; then break fi @@ -325,6 +333,23 @@ if [ "${numberbox_probe_complete}" -ne 1 ]; then exit 1 fi +if [ "${markdown_probe_complete}" -ne 1 ]; then + cat "${STDOUT_LOG}" >&2 + if [ -f "${BOOT_LOG}" ]; then + cat "${BOOT_LOG}" >&2 + fi + echo "Skia Desktop GUI smoke did not complete the Markdown ThemeListener lifecycle probe." >&2 + exit 1 +fi + +# The probe logs its complete line even when a lifecycle step failed; a bare marker grep would +# pass on "passed=False". Require the passing verdict on the complete line itself. +if ! grep -F "MarkdownThemeProbe: complete" "${BOOT_LOG}" | grep -Fq "passed=True"; then + cat "${BOOT_LOG}" >&2 + echo "Skia Desktop GUI smoke markdown lifecycle probe completed without a passing verdict." >&2 + exit 1 +fi + numberbox_sample_count="$(grep -acE 'NumberBoxThemeProbe: sample=[0-9]+' "${BOOT_LOG}" || true)" if [ "${numberbox_sample_count}" -lt 3 ]; then cat "${BOOT_LOG}" >&2 diff --git a/tests/SalmonEgg.Presentation.Core.Tests/Build/SkiaDesktopGuiSeedWriterTests.cs b/tests/SalmonEgg.Presentation.Core.Tests/Build/SkiaDesktopGuiSeedWriterTests.cs index bb2e8a723..50707e7cb 100644 --- a/tests/SalmonEgg.Presentation.Core.Tests/Build/SkiaDesktopGuiSeedWriterTests.cs +++ b/tests/SalmonEgg.Presentation.Core.Tests/Build/SkiaDesktopGuiSeedWriterTests.cs @@ -32,10 +32,21 @@ public void WriteMixedTranscriptSeed_WritesRealProductionAppDataLayout() SkiaDesktopGuiSeedWriter.ConversationId, rootElement.GetProperty("lastActiveConversationId").GetString()); - var conversation = Assert.Single(rootElement.GetProperty("conversations").EnumerateArray()); + var conversations = rootElement.GetProperty("conversations").EnumerateArray().ToArray(); + Assert.Equal(2, conversations.Length); + + var conversation = Assert.Single( + conversations, + item => item.GetProperty("conversationId").GetString() + == SkiaDesktopGuiSeedWriter.ConversationId); + var plainConversation = Assert.Single( + conversations, + item => item.GetProperty("conversationId").GetString() + == SkiaDesktopGuiSeedWriter.PlainConversationId); + var plainMessage = Assert.Single(plainConversation.GetProperty("messages").EnumerateArray()); Assert.Equal( - SkiaDesktopGuiSeedWriter.ConversationId, - conversation.GetProperty("conversationId").GetString()); + "text", + plainMessage.GetProperty("contentType").GetString()); var contentTypes = conversation.GetProperty("messages") .EnumerateArray() diff --git a/tests/SalmonEgg.TestSupport/SkiaDesktopGuiSeedWriter.cs b/tests/SalmonEgg.TestSupport/SkiaDesktopGuiSeedWriter.cs index 109533718..2691a076f 100644 --- a/tests/SalmonEgg.TestSupport/SkiaDesktopGuiSeedWriter.cs +++ b/tests/SalmonEgg.TestSupport/SkiaDesktopGuiSeedWriter.cs @@ -15,6 +15,7 @@ public static class SkiaDesktopGuiSeedWriter public const string ToolCallId = "skia-tool-call-1"; public const string ToolCallTitle = "Read config"; public const string ProjectId = "project-1"; + public const string PlainConversationId = "skia-plain-session-02"; public static SeedPaths WriteMixedTranscriptSeed(string appDataRoot) { @@ -112,6 +113,30 @@ public static string BuildMixedTranscriptConversationsJson(string projectRootPat isOutgoing = false } } + }, + // A plain-text sibling conversation in the same project so the markdown lifecycle + // probe can switch the authoritative selection away and back without synthesizing + // conversations at runtime. No markdown rows: switching to it must unload the + // seeded MarkdownTextPresenter, switching back must re-materialize a new one. + new + { + conversationId = PlainConversationId, + displayName = "Skia Plain Transcript 02", + createdAt = timestamp.AddMinutes(1), + lastUpdatedAt = timestamp.AddMinutes(1), + lastAccessedAt = timestamp.AddMinutes(1), + cwd = projectRootPath, + messages = new object[] + { + new + { + id = "skia-plain-user-1", + timestamp = timestamp.AddMinutes(1), + contentType = "text", + textContent = "Plain sibling conversation without markdown.", + isOutgoing = true + } + } } } }; diff --git a/tools/vendor/uno-toolkit-theme-listener/LICENSE.md b/tools/vendor/uno-toolkit-theme-listener/LICENSE.md new file mode 100644 index 000000000..5aefa46c6 --- /dev/null +++ b/tools/vendor/uno-toolkit-theme-listener/LICENSE.md @@ -0,0 +1,13 @@ +# Windows Community Toolkit + +Copyright © .NET Foundation and Contributors + +All rights reserved. + +## MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tools/vendor/uno-toolkit-theme-listener/README.md b/tools/vendor/uno-toolkit-theme-listener/README.md new file mode 100644 index 000000000..e63fc54bc --- /dev/null +++ b/tools/vendor/uno-toolkit-theme-listener/README.md @@ -0,0 +1,29 @@ +# Uno Toolkit ThemeListener compatibility packages + +Toolkit 7.1.206's `ThemeListener` dereferences `Window.Current.CoreWindow`, which is absent +on Skia. Rendering an ordinary Markdown response can therefore raise a native dispatcher +exception. [Upstream PR 242](https://github.com/unoplatform/Uno.WindowsCommunityToolkit/pull/242) +uses the native `Window.Activated` event instead. + +These three packages are unchanged outputs from the upstream public +[build 233351](https://dev.azure.com/uno-platform/1dd81cbd-cb35-41de-a570-b0df3571a196/_build/results?buildId=233351), +commit `15b1194ecbf27fa8279647cdc7b8dad8bc14eb62`, artifact `WCT-Packages`. +They were downloaded again from that build and compared with the recorded SHA-256 hashes. +`LICENSE.md` is the upstream MIT license from the same commit. Package archives also include +their own license and third-party notices. + +PR 242 is still open; these are development CI artifacts, not a published stable release. + +The dependency override is limited to non-Windows application targets and these exact three +package IDs. All other packages use nuget.org. The existing Markdown package remains at 7.1.206; +the direct UI reference supplies its fixed transitive implementation and matching dependencies. + +Delete this directory, its NuGet source mapping and the direct UI version pin once a published +Toolkit version includes PR 242. Upgrade the Markdown dependency to that release, then verify +ordinary Markdown rendering in Desktop and BrowserWasm with no `ThemeListener` exception. + +| Package | SHA-256 | +| --- | --- | +| Uno.CommunityToolkit.Common.7.1.207-dev.4.g15b1194ecb.nupkg | 88e90eff30c53940e94d6d2daaccdd2a44ec52ac226dfa8b3508af1cf1743720 | +| Uno.CommunityToolkit.WinUI.7.1.207-dev.4.g15b1194ecb.nupkg | 918efc2ce167b28c7af2db0da67fa7bc4aa7a442e162e08f54fe027817dff01a | +| Uno.CommunityToolkit.WinUI.UI.7.1.207-dev.4.g15b1194ecb.nupkg | 76211e5cc4838e7232dc915d4b4b79c0c1de4d4b2d14c4b9ae37b35deb64091c | diff --git a/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.Common.7.1.207-dev.4.g15b1194ecb.nupkg b/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.Common.7.1.207-dev.4.g15b1194ecb.nupkg new file mode 100644 index 000000000..4db934749 Binary files /dev/null and b/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.Common.7.1.207-dev.4.g15b1194ecb.nupkg differ diff --git a/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.WinUI.7.1.207-dev.4.g15b1194ecb.nupkg b/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.WinUI.7.1.207-dev.4.g15b1194ecb.nupkg new file mode 100644 index 000000000..13119e6e4 Binary files /dev/null and b/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.WinUI.7.1.207-dev.4.g15b1194ecb.nupkg differ diff --git a/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.WinUI.UI.7.1.207-dev.4.g15b1194ecb.nupkg b/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.WinUI.UI.7.1.207-dev.4.g15b1194ecb.nupkg new file mode 100644 index 000000000..e73b0a2d3 Binary files /dev/null and b/tools/vendor/uno-toolkit-theme-listener/Uno.CommunityToolkit.WinUI.UI.7.1.207-dev.4.g15b1194ecb.nupkg differ