diff --git a/src/settings-ui/Settings.UI/Converters/DateTimeOffsetToStringConverter.cs b/src/settings-ui/Settings.UI/Converters/DateTimeOffsetToStringConverter.cs
new file mode 100644
index 0000000000..a766f33364
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/DateTimeOffsetToStringConverter.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.UI.Xaml.Data;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters
+{
+ public sealed partial class DateTimeOffsetToStringConverter : IValueConverter
+ {
+ ///
+ /// Gets or sets default .NET date format string. Can be overridden in XAML via ConverterParameter.
+ ///
+ public string Format { get; set; } = "MMMM yyyy";
+
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is null)
+ {
+ return string.Empty;
+ }
+
+ var culture = GetCulture(language);
+ var format = parameter as string ?? Format;
+
+ if (value is DateTimeOffset dto)
+ {
+ return dto.ToString(format, culture);
+ }
+
+ if (value is DateTime dt)
+ {
+ return dt.ToString(format, culture);
+ }
+
+ if (value is string s)
+ {
+ // Try to parse strings robustly using the culture; assume unspecified is universal to avoid local offset surprises
+ if (DateTimeOffset.TryParse(s, culture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsedDto))
+ {
+ return parsedDto.ToString(format, culture);
+ }
+
+ if (DateTime.TryParse(s, culture, DateTimeStyles.AssumeLocal, out var parsedDt))
+ {
+ return parsedDt.ToString(format, culture);
+ }
+ }
+
+ return string.Empty;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static CultureInfo GetCulture(string language)
+ {
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(language))
+ {
+ return new CultureInfo(language);
+ }
+ }
+ catch
+ {
+ // ignore and fall back
+ }
+
+ // Prefer UI culture for display
+ return CultureInfo.CurrentUICulture;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Helpers/ReleaseNotesItem.cs b/src/settings-ui/Settings.UI/Helpers/ReleaseNotesItem.cs
new file mode 100644
index 0000000000..e8701d0496
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Helpers/ReleaseNotesItem.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Helpers
+{
+ public sealed class ReleaseNotesItem
+ {
+ public string Title { get; set; }
+
+ public string Markdown { get; set; }
+
+ public DateTimeOffset PublishedDate { get; set; }
+
+ public string VersionGroup { get; set; }
+
+ public string HeaderImageUri { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index eac4332e27..1ad80fc6e4 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -26,6 +26,8 @@
+
+
@@ -156,7 +158,16 @@
Always
-
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
MSBuild:Compile
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml
new file mode 100644
index 0000000000..5c7e85c557
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml.cs
new file mode 100644
index 0000000000..c9836da129
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
+using Microsoft.UI.Xaml.Navigation;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+
+namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
+{
+ public sealed partial class ReleaseNotePage : Page
+ {
+ public ReleaseNotePage()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (e.Parameter is ReleaseNotesItem item)
+ {
+ MarkdownBlock.Text = item.Markdown ?? string.Empty;
+
+ if (!string.IsNullOrWhiteSpace(item.HeaderImageUri) &&
+ Uri.TryCreate(item.HeaderImageUri, UriKind.Absolute, out var uri))
+ {
+ HeaderImage.Source = new BitmapImage(uri);
+ HeaderImage.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ HeaderImage.Source = null;
+ HeaderImage.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ base.OnNavigatedTo(e);
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotesPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotesPage.xaml
new file mode 100644
index 0000000000..05d9e1aad4
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotesPage.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotesPage.xaml.cs
new file mode 100644
index 0000000000..2f73a52bac
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotesPage.xaml.cs
@@ -0,0 +1,303 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using CommunityToolkit.WinUI.Controls;
+using global::PowerToys.GPOWrapper;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
+using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
+using Microsoft.PowerToys.Settings.UI.SerializationContext;
+using Microsoft.PowerToys.Settings.UI.Services;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.UI.Text;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
+{
+ public sealed partial class ReleaseNotesPage : Page
+ {
+ public ReleaseNotesPage()
+ {
+ InitializeComponent();
+ }
+
+ // Your original regex constants
+ private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
+ private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
+ private const RegexOptions RemoveInstallerHashesRegexOptions =
+ RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+
+ // Image extraction regexes (Markdown image + HTML
)
+ private static readonly Regex MdImageRegex =
+ new(
+ @"!\[(?:[^\]]*)\]\((?[^)\s]+)(?:\s+""[^""]*"")?\)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ private static readonly Regex HtmlImageRegex =
+ new(
+ @"
]*\s+src\s*=\s*[""'](?[^""']+)[""'][^>]*>",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ // PR URL normalization:
+ // 1) Markdown links whose *text* is the full PR URL -> make text "#12345"
+ private static readonly Regex MdLinkWithPrUrlTextRegex =
+ new(
+ @"\[(?https?://github\.com/microsoft/PowerToys/pull/(?\d+))\]\(\k\)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ // 2) Bare PR URLs -> turn into "[#12345](url)"
+ private static readonly Regex BarePrUrlRegex =
+ new(
+ @"(?https?://github\.com/microsoft/PowerToys/pull/(?\d+))(?!\))",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ public ObservableCollection ReleaseItems { get; } = new();
+
+ // Fetch, group (by Major.Minor), clean, extract first image, and build items
+ private async Task> GetGroupedReleaseNotesAsync()
+ {
+ // Fetch GitHub releases using system proxy & user-agent
+ using var proxyClientHandler = new HttpClientHandler
+ {
+ DefaultProxyCredentials = CredentialCache.DefaultCredentials,
+ Proxy = WebRequest.GetSystemWebProxy(),
+ PreAuthenticate = true,
+ };
+
+ using var client = new HttpClient(proxyClientHandler);
+ client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
+
+ string json = await client.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases");
+
+ // NOTE: PowerToysReleaseInfo + SourceGenerationContextContext are assumed to exist in your project
+ IList releases =
+ JsonSerializer.Deserialize>(
+ json, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo)!;
+
+ // Prepare hash-removal regexes
+ var removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
+ var removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
+
+ // Parse versions; keep ones that contain x.y.z (handles "v0.93.2" etc.)
+ var parsed = releases
+ .Select(r => new
+ {
+ Release = r,
+ Version = TryParseSemVer(r.TagName ?? r.Name, out var v) ? v : null,
+ })
+ .Where(x => x.Version is not null)
+ .ToList();
+
+ // Group by Major.Minor (e.g., "0.93"), order groups by newest published date
+ var groups = parsed
+ .GroupBy(x => $"{x.Version!.Major}.{x.Version!.Minor}")
+ .OrderByDescending(g => g.Max(x => x.Release.PublishedDate))
+ .ToList();
+
+ var items = new List();
+
+ foreach (var g in groups)
+ {
+ // Order subreleases by version (patch desc), then date desc
+ var ordered = g.OrderByDescending(x => x.Version)
+ .ThenByDescending(x => x.Release.PublishedDate)
+ .ToList();
+
+ // Title is the highest patch tag (e.g., "0.93.2"), trimmed of any leading 'v'
+ var top = ordered.First();
+ var title = TrimLeadingV(top.Release.TagName ?? top.Release.Name);
+
+ var sb = new StringBuilder();
+ int counter = 0;
+ string headerImage = null;
+
+ for (int i = 0; i < ordered.Count; i++)
+ {
+ var r = ordered[i].Release;
+
+ // Clean installer hash sections
+ var cleaned = removeHashRegex.Replace(r.ReleaseNotes ?? string.Empty, "\r\n### Highlights");
+ cleaned = cleaned.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
+ cleaned = removeHotfixHashRegex.Replace(cleaned, string.Empty);
+
+ // Capture & remove FIRST image across the whole group (only once)
+ if (headerImage is null)
+ {
+ var (withoutFirstImage, foundUrl) = RemoveFirstImageAndGetUrl(cleaned);
+ if (!string.IsNullOrWhiteSpace(foundUrl))
+ {
+ headerImage = foundUrl;
+ cleaned = withoutFirstImage;
+ }
+ }
+
+ // Normalize PR links to show "#12345" like GitHub
+ cleaned = NormalizeGitHubPrLinks(cleaned);
+
+ if (i > 0)
+ {
+ // Horizontal rule between subreleases within the same group
+ sb.AppendLine("\r\n---\r\n");
+ }
+
+ // Keep a per-subrelease header for context (optional)
+ var header = $"# {TrimLeadingV(r.TagName ?? r.Name)}";
+ sb.AppendLine(header);
+ sb.AppendLine(cleaned);
+ }
+
+ items.Add(new ReleaseNotesItem
+ {
+ Title = title,
+ VersionGroup = g.Key,
+ PublishedDate = ordered.Max(x => x.Release.PublishedDate),
+ Markdown = sb.ToString(),
+ HeaderImageUri = headerImage,
+ });
+ }
+
+ return items;
+ }
+
+ // Turn "https://github.com/microsoft/PowerToys/pull/41853" into "[#41853](...)".
+ // Also, if the markdown link text equals the full URL, rewrite it to "#41853".
+ private static string NormalizeGitHubPrLinks(string markdown)
+ {
+ if (string.IsNullOrEmpty(markdown))
+ {
+ return markdown;
+ }
+
+ // Case 1: [https://github.com/.../pull/12345](https://github.com/.../pull/12345)
+ markdown = MdLinkWithPrUrlTextRegex.Replace(
+ markdown,
+ m =>
+ {
+ var id = m.Groups["id"].Value;
+ var url = m.Groups["url"].Value;
+ return $"[#{id}]({url})";
+ });
+
+ // Case 2: bare https://github.com/.../pull/12345 (not already inside link markup)
+ markdown = BarePrUrlRegex.Replace(
+ markdown,
+ m =>
+ {
+ var id = m.Groups["id"].Value;
+ var url = m.Groups["url"].Value;
+ return $"[#{id}]({url})";
+ });
+
+ return markdown;
+ }
+
+ // Extract and remove the first image (Markdown or HTML) from a markdown string
+ private static (string Cleaned, string Url) RemoveFirstImageAndGetUrl(string markdown)
+ {
+ if (string.IsNullOrWhiteSpace(markdown))
+ {
+ return (markdown, null);
+ }
+
+ // Markdown image first: 
+ var m = MdImageRegex.Match(markdown);
+ if (m.Success)
+ {
+ var url = m.Groups["url"].Value.Trim();
+ var cleaned = MdImageRegex.Replace(markdown, string.Empty, 1);
+ cleaned = CollapseExtraBlankLines(cleaned);
+ return (cleaned, url);
+ }
+
+ // Fallback: HTML
+ m = HtmlImageRegex.Match(markdown);
+ if (m.Success)
+ {
+ var url = m.Groups["url"].Value.Trim();
+ var cleaned = HtmlImageRegex.Replace(markdown, string.Empty, 1);
+ cleaned = CollapseExtraBlankLines(cleaned);
+ return (cleaned, url);
+ }
+
+ return (markdown, null);
+ }
+
+ private static string CollapseExtraBlankLines(string s)
+ {
+ s = s.Trim();
+ s = Regex.Replace(s, @"(\r?\n){3,}", "\r\n\r\n");
+ return s;
+ }
+
+ // Try to parse the first x.y.z version found in a string (supports leading 'v')
+ private static bool TryParseSemVer(string s, out Version v)
+ {
+ v = null;
+ if (string.IsNullOrWhiteSpace(s))
+ {
+ return false;
+ }
+
+ var m = Regex.Match(s, @"(?
+ string.IsNullOrEmpty(s) ? s : (s.StartsWith("v", StringComparison.OrdinalIgnoreCase) ? s[1..] : s);
+
+ private async void Grid_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (ReleaseItems.Count == 0)
+ {
+ var items = await GetGroupedReleaseNotesAsync();
+ foreach (var item in items)
+ {
+ ReleaseItems.Add(item);
+ }
+
+ if (ReleaseItems.Count > 0 && ReleasesList is not null)
+ {
+ ReleasesList.SelectedIndex = 0;
+ }
+ }
+ }
+
+ private void ReleasesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (ReleasesList.SelectedItem is ReleaseNotesItem item)
+ {
+ ContentFrame.Navigate(typeof(ReleaseNotePage), item);
+ }
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
new file mode 100644
index 0000000000..82b4170618
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml.cs
new file mode 100644
index 0000000000..924914aa04
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using CommunityToolkit.WinUI.Controls;
+using global::PowerToys.GPOWrapper;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
+using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
+using Microsoft.PowerToys.Settings.UI.SerializationContext;
+using Microsoft.PowerToys.Settings.UI.Services;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.UI.Text;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+using WinUIEx;
+
+namespace Microsoft.PowerToys.Settings.UI.SettingsXAML
+{
+ public sealed partial class ScoobeWindow : WindowEx
+ {
+ public ScoobeWindow()
+ {
+ InitializeComponent();
+ this.ExtendsContentIntoTitleBar = true;
+ SetTitleBar(titleBar);
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
index 06f7e222ce..62419505f8 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
@@ -14,6 +14,7 @@ using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Services;
+using Microsoft.PowerToys.Settings.UI.SettingsXAML;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
@@ -373,7 +374,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void WhatIsNewItem_Tapped(object sender, TappedRoutedEventArgs e)
{
- OpenWhatIsNewWindowCallback();
+ // OpenWhatIsNewWindowCallback();
+ ScoobeWindow window = new ScoobeWindow();
+ window.Activate();
}
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)