CmdPal: GEH per partes; part 1: error report builder, sanitizer and internals tools setting page (#44140)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR adds three parts of the original big bad global error handler
(error report builder, sanitization and internal tools UI).

### Error Report Generation

- `ErrorReportBuilder`: Produces a detailed, technical report with
system context.
- Comprehensive data: OS version, architecture, culture, app version,
elevation status, etc.
- Exception analysis: Coalesces nested exception messages and HRESULT
details for clearer diagnostics.

<details><summary>Example</summary>
<pre>

This is an error report generated by Windows Command Palette.
If you are seeing this, it means something went a little sideways in the
app.
You can help us fix it by filing a report at
https://aka.ms/powerToysReportBug.

(While you’re at it, give the details below a quick skim — just to make
sure there’s nothing personal you’d prefer not to share. It’s rare, but
sometimes little surprises sneak in.)
============================================================
Summary:
  Message:               Test exception; thrown from the UI thread
  Type:                  System.NotImplementedException
  Source:                Microsoft.CmdPal.UI
  Time:                  2025-08-25 18:54:44.3854569
  HRESULT:               0x80004001 (-2147467263)
  Context:               MainThreadException

Application:
  App version:           0.0.1.0
  Is elevated:           no

Environment:
  OS version:            Microsoft Windows 10.0.26120
  OS architecture:       X64
  Runtime identifier:    win-x64
  Framework:             .NET 9.0.8
  Process architecture:  X64
  Culture:               cs-CZ
  UI culture:            en-US

Stack Trace:
at
Microsoft.CmdPal.UI.Settings.InternalPage.ThrowPlainMainThreadException_Click(Object
sender, RoutedEventArgs e)
at
WinRT._EventSource_global__Microsoft_UI_Xaml_RoutedEventHandler.EventState.<GetEventInvoke>b__1_0(Object
sender, RoutedEventArgs e)
at ABI.Microsoft.UI.Xaml.RoutedEventHandler.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr e)

------------------ Full Exception Details ------------------
System.NotImplementedException: Test exception; thrown from the UI
thread
at
Microsoft.CmdPal.UI.Settings.InternalPage.ThrowPlainMainThreadException_Click(Object
sender, RoutedEventArgs e)
at
WinRT._EventSource_global__Microsoft_UI_Xaml_RoutedEventHandler.EventState.<GetEventInvoke>b__1_0(Object
sender, RoutedEventArgs e)
at ABI.Microsoft.UI.Xaml.RoutedEventHandler.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr e)

============================================================

</pre>
</details> 

Real-world example: #41362

### PII Sanitization Framework

- `ErrorReportSanitizer`: Multi-layer sanitization pipeline for
sensitive data.
- Nine specialized rule providers:
- `PiiRuleProvider`: Personally identifiable information (emails, phone
numbers, SSNs).
- `ProfilePathAndUsernameRuleProvider`: Windows user profiles and
usernames.
- `NetworkRuleProvider`: IP addresses, MAC addresses, network
identifiers.
- `SecretKeyValueRulesProvider`: API keys, tokens, passwords in
key/value formats.
  - `FilenameMaskRuleProvider`: Sensitive file paths and extensions.
  - `UrlRuleProvider`: URLs and web addresses.
  - `TokenRuleProvider`: JWT and other auth tokens.
  - `ConnectionStringRuleProvider`: Database connection strings.
- `EnvironmentPropertiesRuleProvider`: Environment variables and system
properties.

### Internals Tools Page

A page in settings available in non-CI-builds:

<img width="1305" height="745" alt="image"
src="https://github.com/user-attachments/assets/3145ecfd-997f-491d-8c8a-6096634b6045"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-01-29 04:09:37 +01:00
committed by GitHub
parent f82afdf384
commit 8ec530c65e
50 changed files with 2529 additions and 35 deletions

View File

@@ -41,7 +41,7 @@ namespace Microsoft.CmdPal.UI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
public partial class App : Application, IDisposable
{
private readonly GlobalErrorHandler _globalErrorHandler = new();
@@ -67,7 +67,7 @@ public partial class App : Application
public App()
{
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
_globalErrorHandler.Register(this);
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default);
#endif
Services = ConfigureServices();
@@ -203,4 +203,11 @@ public partial class App : Application
services.AddSingleton<ShellViewModel>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
}
public void Dispose()
{
_globalErrorHandler.Dispose();
EtwTrace.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -128,7 +128,7 @@ public sealed partial class CommandBar : UserControl,
private void SettingsIcon_Clicked(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage());
}
private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e)

View File

@@ -203,6 +203,12 @@
</Grid>
</Border>
<!-- More section -->
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="More" />
<Border>
<Button Command="{x:Bind ViewModel.OpenInternalToolsCommand}" Content="Open internal tools" />
</Border>
</StackPanel>
<!-- Footer -->

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Services.Reports;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
@@ -15,14 +15,22 @@ namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// Global error handler for Command Palette.
/// </summary>
internal sealed partial class GlobalErrorHandler
internal sealed partial class GlobalErrorHandler : IDisposable
{
private readonly ErrorReportBuilder _errorReportBuilder = new();
private Options? _options;
private App? _app;
// GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
internal void Register(App app)
internal void Register(App app, Options options)
{
ArgumentNullException.ThrowIfNull(app);
ArgumentNullException.ThrowIfNull(options);
app.UnhandledException += App_UnhandledException;
_options = options;
_app = app;
_app.UnhandledException += App_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
}
@@ -54,21 +62,15 @@ internal sealed partial class GlobalErrorHandler
HandleException(e.Exception, Context.UnobservedTaskException);
}
private static void HandleException(Exception ex, Context context)
private void HandleException(Exception ex, Context context)
{
Logger.LogError($"Unhandled exception detected ({context})", ex);
if (context == Context.MainThreadException)
{
var error = DiagnosticsHelper.BuildExceptionMessage(ex, null);
var report = $"""
This is an error report generated by Windows Command Palette.
If you are seeing this message, it means the application has encountered an unexpected issue.
You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
{error}
""";
var report = _errorReportBuilder.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true);
StoreReport(report, storeOnDesktop: false);
StoreReport(report, storeOnDesktop: _options?.StoreReportOnUserDesktop == true);
string message;
string caption;
@@ -138,6 +140,13 @@ internal sealed partial class GlobalErrorHandler
}
}
public void Dispose()
{
_app?.UnhandledException -= App_UnhandledException;
TaskScheduler.UnobservedTaskException -= TaskScheduler_UnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
}
private enum Context
{
Unknown = 0,
@@ -146,4 +155,26 @@ internal sealed partial class GlobalErrorHandler
UnobservedTaskException,
AppDomainUnhandledException,
}
/// <summary>
/// Configuration options controlling how <see cref="GlobalErrorHandler"/> reacts to exceptions
/// (what to log, what to show to the user, and where to store reports).
/// </summary>
internal sealed record Options
{
/// <summary>
/// Gets the default configuration.
/// </summary>
public static Options Default { get; } = new();
/// <summary>
/// Gets a value indicating whether Personally Identifiable Information (PII) should be redacted in error reports.
/// </summary>
public bool RedactPii { get; init; } = true;
/// <summary>
/// Gets a value indicating whether to store the error report on the user's desktop in addition to the log directory.
/// </summary>
public bool StoreReportOnUserDesktop { get; init; }
}
}

View File

@@ -152,7 +152,7 @@ internal sealed partial class TrayIconService
{
if (wParam == PInvoke.WM_USER + 1)
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage());
}
else if (wParam == PInvoke.WM_USER + 2)
{

View File

@@ -83,6 +83,7 @@
<None Remove="Pages\Settings\GeneralPage.xaml" />
<None Remove="SettingsWindow.xaml" />
<None Remove="Settings\AppearancePage.xaml" />
<None Remove="Settings\InternalPage.xaml" />
<None Remove="ShellPage.xaml" />
<None Remove="Styles\Colors.xaml" />
<None Remove="Styles\Settings.xaml" />
@@ -264,6 +265,11 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Settings\InternalPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Colors.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -257,11 +257,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
_ = DispatcherQueue.TryEnqueue(() =>
{
OpenSettings();
OpenSettings(message.SettingsPageTag);
});
}
public void OpenSettings()
public void OpenSettings(string pageTag)
{
if (_settingsWindow is null)
{
@@ -270,6 +270,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
_settingsWindow.Activate();
_settingsWindow.BringToFront();
_settingsWindow.Navigate(pageTag);
}
public void Receive(ShowDetailsMessage message)

View File

@@ -0,0 +1,45 @@
// 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.
namespace Microsoft.CmdPal.UI.Settings;
public partial class InternalPage
{
internal static class SampleData
{
internal static string ExceptionMessageWithPii { get; } =
$"""
Test exception with personal information; thrown from the UI thread
Here is e-mail address <jane.doe@contoso.com>
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
MAC address: 00-14-22-01-23-45
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
IPv6 loopback address: ::1
Password: P@ssw0rd123!
Password=secret
Api key: 1234567890abcdef
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
X-API-key: 1234567890abcdef
Pet-Shop-Subscription-Key: 1234567890abcdef
Here is a user name {Environment.UserName}
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\Pictures
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
Here is machine name {Environment.MachineName}
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
User email john.doe@company.com failed validation
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
Phone number 555-123-4567 is invalid
API key abc123def456ghi789jkl012mno345pqr678 expired
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
Email service error: mailto:admin@internal-company.com?subject=Alert
""";
}
}

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Microsoft.CmdPal.UI.Settings.InternalPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1">
<Grid Padding="16">
<StackPanel
MaxWidth="1000"
HorizontalAlignment="Stretch"
Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="Tools on this page are for internal use only. This page is not visible in CI builds." />
<!-- Exception Handling Section -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Exception Handling" />
<controls:SettingsExpander
Description="Actions for testing global exception handling from the application"
Header="Throw exceptions"
HeaderIcon="{ui:FontIcon Glyph=&#xE783;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread">
<Button Click="ThrowPlainMainThreadException_Click" Content="Throw" />
</controls:SettingsCard>
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread (with PII)">
<Button Click="ThrowPlainMainThreadExceptionPii_Click" Content="Throw" />
</controls:SettingsCard>
<controls:SettingsCard Description="Throw with delay, when the task is collected by the GC" Header="Throw unobserved exception from a task">
<Button Click="ThrowExceptionInUnobservedTask_Click" Content="Throw" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- Diagnostics Section -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Diagnostics" />
<controls:SettingsCard
x:Name="LogsSettingsCard"
Header="Logs folder"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}">
<Button Click="OpenLogsCardClicked" Content="Open folder" />
</controls:SettingsCard>
<controls:SettingsCard
x:Name="CurrentLogFileSettingsCard"
Header="Current log file"
HeaderIcon="{ui:FontIcon Glyph=&#xF7BB;}">
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
</controls:SettingsCard>
<!-- Data Section -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Data and Files" />
<controls:SettingsCard
x:Name="ConfigurationFolderSettingsCard"
Header="Configuration folder"
HeaderIcon="{ui:FontIcon Glyph=&#xF73D;}">
<Button Click="OpenConfigFolderCardClick" Content="Open folder" />
</controls:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,92 @@
// 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 ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Xaml;
using Windows.System;
using Page = Microsoft.UI.Xaml.Controls.Page;
namespace Microsoft.CmdPal.UI.Settings;
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class InternalPage : Page
{
public InternalPage()
{
InitializeComponent();
}
private void ThrowPlainMainThreadException_Click(object sender, RoutedEventArgs e)
{
Logger.LogDebug("Throwing test exception from the UI thread");
throw new NotImplementedException("Test exception; thrown from the UI thread");
}
private void ThrowExceptionInUnobservedTask_Click(object sender, RoutedEventArgs e)
{
Logger.LogDebug("Starting a task that will throw test exception");
Task.Run(() =>
{
Logger.LogDebug("Throwing test exception from a task");
throw new InvalidOperationException("Test exception; thrown from a task");
});
}
private void ThrowPlainMainThreadExceptionPii_Click(object sender, RoutedEventArgs e)
{
Logger.LogDebug("Throwing test exception from the UI thread (PII)");
throw new InvalidOperationException(SampleData.ExceptionMessageWithPii);
}
private async void OpenLogsCardClicked(object sender, RoutedEventArgs e)
{
try
{
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
if (Directory.Exists(logFolderPath))
{
await Launcher.LaunchFolderPathAsync(logFolderPath);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to open directory in Explorer", ex);
}
}
private async void OpenCurrentLogCardClicked(object sender, RoutedEventArgs e)
{
try
{
var logPath = Logger.CurrentLogFile;
if (File.Exists(logPath))
{
await Launcher.LaunchUriAsync(new Uri(logPath));
}
}
catch (Exception ex)
{
Logger.LogError("Failed to open log file", ex);
}
}
private async void OpenConfigFolderCardClick(object sender, RoutedEventArgs e)
{
try
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
if (Directory.Exists(directory))
{
await Launcher.LaunchFolderPathAsync(directory);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to open directory in Explorer", ex);
}
}
}

View File

@@ -72,6 +72,7 @@
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" />
<!-- "Internal Tools" page item is added dynamically from code -->
</NavigationView.MenuItems>
<Grid>
<Grid.RowDefinitions>

View File

@@ -30,6 +30,8 @@ public sealed partial class SettingsWindow : WindowEx,
{
private readonly LocalKeyboardListener _localKeyboardListener;
private readonly NavigationViewItem? _internalNavItem;
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
// Gets or sets optional action invoked after NavigationView is loaded.
@@ -54,6 +56,23 @@ public sealed partial class SettingsWindow : WindowEx,
_localKeyboardListener.Start();
Closed += SettingsWindow_Closed;
RootElement.AddHandler(UIElement.PointerPressedEvent, new PointerEventHandler(RootElement_OnPointerPressed), true);
if (!BuildInfo.IsCiBuild)
{
_internalNavItem = new NavigationViewItem
{
Content = "Internal Tools",
Icon = new FontIcon { Glyph = "\uEC7A" },
Tag = "Internal",
};
NavView.MenuItems.Add(_internalNavItem);
}
else
{
_internalNavItem = null;
}
Navigate("General");
}
private void SettingsWindow_Closed(object sender, WindowEventArgs args)
@@ -68,9 +87,6 @@ public sealed partial class SettingsWindow : WindowEx,
// Delay necessary to ensure NavigationView visual state can match navigation
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
NavView.SelectedItem = NavView.MenuItems[0];
Navigate("General");
if (sender is NavigationView navigationView)
{
// Register for pane open/close changes to announce to screen readers
@@ -96,15 +112,33 @@ public sealed partial class SettingsWindow : WindowEx,
Navigate((selectedItem.Tag as string)!);
}
private void Navigate(string page)
internal void Navigate(string page)
{
var pageType = page switch
Type? pageType;
switch (page)
{
"General" => typeof(GeneralPage),
"Appearance" => typeof(AppearancePage),
"Extensions" => typeof(ExtensionsPage),
_ => null,
};
case "General":
pageType = typeof(GeneralPage);
break;
case "Appearance":
pageType = typeof(AppearancePage);
break;
case "Extensions":
pageType = typeof(ExtensionsPage);
break;
case "Internal":
pageType = typeof(InternalPage);
break;
case "":
// intentional no-op: empty tag means no navigation
pageType = null;
break;
default:
// unknown page, no-op and log
pageType = null;
Logger.LogError($"Unknown settings page tag '{page}'");
break;
}
if (pageType is not null)
{
@@ -268,6 +302,12 @@ public sealed partial class SettingsWindow : WindowEx,
BreadCrumbs.Add(new(extensionsPageType, extensionsPageType));
BreadCrumbs.Add(new(vm.DisplayName, vm));
}
else if (e.SourcePageType == typeof(InternalPage) && _internalNavItem is not null)
{
NavView.SelectedItem = _internalNavItem;
var pageType = "Internal";
BreadCrumbs.Add(new(pageType, pageType));
}
else
{
BreadCrumbs.Add(new($"[{e.SourcePageType?.Name}]", string.Empty));

View File

@@ -8,8 +8,10 @@ using System.Globalization;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.UI;
using Windows.System;
using Windows.UI;
@@ -99,6 +101,12 @@ internal sealed partial class DevRibbonViewModel : ObservableObject
LatestLogs.Clear();
}
[RelayCommand]
private void OpenInternalTools()
{
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Internal"));
}
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
{
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";