From 5884375e9d52eb2a8621898c4e01b97f250097e5 Mon Sep 17 00:00:00 2001 From: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:24:21 +0800 Subject: [PATCH 1/6] Fix advanced paste settings migration issue (#43459) ## Summary of the Pull Request This pull request introduces a robust migration system for legacy Advanced Paste AI provider settings and credentials, refactoring both the migration logic and credential handling to be more maintainable and reliable. It also standardizes the default model names for AI providers and updates related UI placeholders to reflect these changes. The migration logic is now centralized in a new helper class, and legacy credential migration is handled more cleanly in both the settings and view model layers. ## PR Checklist - [x] Closes: #43456 - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --------- Signed-off-by: Shawn Yuan (from Dev Box) Signed-off-by: Shawn Yuan --- .../AdvancedPaste/Helpers/UserSettings.cs | 127 ++++++++++- .../AdvancedPasteMigrationHelper.cs | 205 ++++++++++++++++++ .../AdvancedPasteProperties.cs | 58 +++++ .../PasteAIProviderDefaults.cs | 29 +++ .../SettingsXAML/Views/AdvancedPastePage.xaml | 4 +- .../ViewModels/AdvancedPasteViewModel.cs | 144 ++++++++---- 6 files changed, 518 insertions(+), 49 deletions(-) create mode 100644 src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs create mode 100644 src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs index b6b6c19734..cf93f10796 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs @@ -163,28 +163,143 @@ namespace AdvancedPaste.Settings return false; } - if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists()) + var properties = settings.Properties; + var configuration = properties.PasteAIConfiguration; + + if (configuration is null) + { + configuration = new PasteAIConfiguration(); + properties.PasteAIConfiguration = configuration; + } + + bool hasLegacyProviders = configuration.LegacyProviderConfigurations is { Count: > 0 }; + bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag); + bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag; + PasswordCredential legacyCredential = TryGetLegacyOpenAICredential(); + + if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed) { return false; } - settings.Properties.IsAIEnabled = true; - return true; + bool configurationUpdated = false; + + if (hasLegacyProviders) + { + configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration); + } + + PasteAIProviderDefinition openAIProvider = null; + if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed) + { + var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration); + openAIProvider = ensureResult.Provider; + configurationUpdated |= ensureResult.Updated; + } + + if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled) + { + openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled; + configurationUpdated = true; + } + + if (legacyCredential is not null && openAIProvider is not null) + { + StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password); + RemoveLegacyOpenAICredential(); + } + + bool enabledUpdated = false; + if (!properties.IsAIEnabled && legacyCredential is not null) + { + properties.IsAIEnabled = true; + enabledUpdated = true; + } + + return configurationUpdated || enabledUpdated || legacyAdvancedAIConsumed; } - private static bool LegacyOpenAIKeyExists() + private static PasswordCredential TryGetLegacyOpenAICredential() { try { PasswordVault vault = new(); - return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null; + var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); + credential?.RetrievePassword(); + return credential; } catch (Exception) { - return false; + return null; } } + private static void RemoveLegacyOpenAICredential() + { + try + { + PasswordVault vault = new(); + TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); + } + catch (Exception) + { + } + } + + private static void StoreMigratedOpenAICredential(string providerId, string serviceType, string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + return; + } + + try + { + var serviceKind = serviceType.ToAIServiceType(); + if (serviceKind != AIServiceType.OpenAI) + { + return; + } + + string resource = "https://platform.openai.com/api-keys"; + string username = $"PowerToys_AdvancedPaste_PasteAI_openai_{NormalizeProviderIdentifier(providerId)}"; + + PasswordVault vault = new(); + TryRemoveCredential(vault, resource, username); + + PasswordCredential credential = new(resource, username, password); + vault.Add(credential); + } + catch (Exception ex) + { + Logger.LogError("Failed to migrate legacy OpenAI credential", ex); + } + } + + private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName) + { + try + { + PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName); + vault.Remove(existingCred); + } + catch (Exception) + { + // Credential doesn't exist, which is fine + } + } + + private static string NormalizeProviderIdentifier(string providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + { + return "default"; + } + + var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray()); + return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant(); + } + public async Task SetActiveAIProviderAsync(string providerId) { if (string.IsNullOrWhiteSpace(providerId)) diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs new file mode 100644 index 0000000000..8a606ad83a --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs @@ -0,0 +1,205 @@ +// 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.ObjectModel; +using System.Linq; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Helper methods for migrating legacy Advanced Paste settings to the updated schema. + /// + public static class AdvancedPasteMigrationHelper + { + /// + /// Moves legacy provider configuration snapshots into the strongly-typed providers collection. + /// + /// The configuration instance to migrate. + /// True if the configuration was modified. + public static bool MigrateLegacyProviderConfigurations(PasteAIConfiguration configuration) + { + if (configuration is null) + { + return false; + } + + configuration.Providers ??= new ObservableCollection(); + + bool configurationUpdated = false; + + if (configuration.LegacyProviderConfigurations is { Count: > 0 }) + { + foreach (var entry in configuration.LegacyProviderConfigurations) + { + var result = EnsureProvider(configuration, entry.Key, entry.Value); + configurationUpdated |= result.Updated; + } + + configuration.LegacyProviderConfigurations = null; + } + + configurationUpdated |= EnsureActiveProviderIsValid(configuration); + + return configurationUpdated; + } + + /// + /// Ensures an OpenAI provider exists in the configuration, creating one if necessary. + /// + /// The configuration instance. + /// The ensured provider and a flag indicating whether changes were made. + public static (PasteAIProviderDefinition Provider, bool Updated) EnsureOpenAIProvider(PasteAIConfiguration configuration) + { + return EnsureProvider(configuration, AIServiceType.OpenAI.ToConfigurationString(), null); + } + + /// + /// Ensures a provider for the supplied service type exists, optionally applying a legacy snapshot. + /// + /// The configuration instance. + /// The persisted service type key. + /// An optional snapshot containing legacy values. + /// The ensured provider and whether the configuration was updated. + public static (PasteAIProviderDefinition Provider, bool Updated) EnsureProvider(PasteAIConfiguration configuration, string serviceTypeKey, AIProviderConfigurationSnapshot snapshot) + { + if (configuration is null) + { + return (null, false); + } + + configuration.Providers ??= new ObservableCollection(); + + var normalizedServiceType = NormalizeServiceType(serviceTypeKey); + var existingProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.ServiceType, normalizedServiceType, StringComparison.OrdinalIgnoreCase)); + bool configurationUpdated = false; + + if (existingProvider is null) + { + existingProvider = CreateProvider(normalizedServiceType, snapshot); + configuration.Providers.Add(existingProvider); + configurationUpdated = true; + } + else if (snapshot is not null) + { + configurationUpdated |= ApplySnapshot(existingProvider, snapshot); + } + + configurationUpdated |= EnsureActiveProviderIsValid(configuration, existingProvider); + + return (existingProvider, configurationUpdated); + } + + private static string NormalizeServiceType(string serviceTypeKey) + { + var serviceType = serviceTypeKey.ToAIServiceType(); + return serviceType.ToConfigurationString(); + } + + private static PasteAIProviderDefinition CreateProvider(string serviceTypeKey, AIProviderConfigurationSnapshot snapshot) + { + var serviceType = serviceTypeKey.ToAIServiceType(); + var metadata = AIServiceTypeRegistry.GetMetadata(serviceType); + var provider = new PasteAIProviderDefinition + { + ServiceType = serviceTypeKey, + ModelName = !string.IsNullOrWhiteSpace(snapshot?.ModelName) ? snapshot.ModelName : PasteAIProviderDefaults.GetDefaultModelName(serviceType), + EndpointUrl = snapshot?.EndpointUrl ?? string.Empty, + ApiVersion = snapshot?.ApiVersion ?? string.Empty, + DeploymentName = snapshot?.DeploymentName ?? string.Empty, + ModelPath = snapshot?.ModelPath ?? string.Empty, + SystemPrompt = snapshot?.SystemPrompt ?? string.Empty, + ModerationEnabled = snapshot?.ModerationEnabled ?? true, + IsLocalModel = metadata.IsLocalModel, + }; + + return provider; + } + + private static bool ApplySnapshot(PasteAIProviderDefinition provider, AIProviderConfigurationSnapshot snapshot) + { + bool updated = false; + + if (!string.IsNullOrWhiteSpace(snapshot.ModelName) && !string.Equals(provider.ModelName, snapshot.ModelName, StringComparison.Ordinal)) + { + provider.ModelName = snapshot.ModelName; + updated = true; + } + + if (!string.IsNullOrWhiteSpace(snapshot.EndpointUrl) && !string.Equals(provider.EndpointUrl, snapshot.EndpointUrl, StringComparison.Ordinal)) + { + provider.EndpointUrl = snapshot.EndpointUrl; + updated = true; + } + + if (!string.IsNullOrWhiteSpace(snapshot.ApiVersion) && !string.Equals(provider.ApiVersion, snapshot.ApiVersion, StringComparison.Ordinal)) + { + provider.ApiVersion = snapshot.ApiVersion; + updated = true; + } + + if (!string.IsNullOrWhiteSpace(snapshot.DeploymentName) && !string.Equals(provider.DeploymentName, snapshot.DeploymentName, StringComparison.Ordinal)) + { + provider.DeploymentName = snapshot.DeploymentName; + updated = true; + } + + if (!string.IsNullOrWhiteSpace(snapshot.ModelPath) && !string.Equals(provider.ModelPath, snapshot.ModelPath, StringComparison.Ordinal)) + { + provider.ModelPath = snapshot.ModelPath; + updated = true; + } + + if (!string.IsNullOrWhiteSpace(snapshot.SystemPrompt) && !string.Equals(provider.SystemPrompt, snapshot.SystemPrompt, StringComparison.Ordinal)) + { + provider.SystemPrompt = snapshot.SystemPrompt; + updated = true; + } + + if (provider.ModerationEnabled != snapshot.ModerationEnabled) + { + provider.ModerationEnabled = snapshot.ModerationEnabled; + updated = true; + } + + return updated; + } + + private static bool EnsureActiveProviderIsValid(PasteAIConfiguration configuration, PasteAIProviderDefinition preferredProvider = null) + { + if (configuration?.Providers is null || configuration.Providers.Count == 0) + { + if (!string.IsNullOrWhiteSpace(configuration?.ActiveProviderId)) + { + configuration.ActiveProviderId = string.Empty; + return true; + } + + return false; + } + + bool updated = false; + + var activeProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.Id, configuration.ActiveProviderId, StringComparison.OrdinalIgnoreCase)); + if (activeProvider is null) + { + activeProvider = preferredProvider ?? configuration.Providers.First(); + configuration.ActiveProviderId = activeProvider.Id; + updated = true; + } + + foreach (var provider in configuration.Providers) + { + bool shouldBeActive = string.Equals(provider.Id, configuration.ActiveProviderId, StringComparison.OrdinalIgnoreCase); + if (provider.IsActive != shouldBeActive) + { + provider.IsActive = shouldBeActive; + updated = true; + } + } + + return updated; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs index 200e5e459d..3c377900f8 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs @@ -52,10 +52,68 @@ namespace Microsoft.PowerToys.Settings.UI.Library _extensionData.Remove("IsOpenAIEnabled"); } + + if (_extensionData != null && _extensionData.TryGetValue("IsAdvancedAIEnabled", out var legacyAdvancedElement)) + { + bool? legacyValue = legacyAdvancedElement.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Object when legacyAdvancedElement.TryGetProperty("value", out var advancedValueElement) => advancedValueElement.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + _ => null, + }, + _ => null, + }; + + if (legacyValue.HasValue) + { + LegacyAdvancedAIEnabled = legacyValue.Value; + } + + _extensionData.Remove("IsAdvancedAIEnabled"); + } } } private Dictionary _extensionData; + private bool? _legacyAdvancedAIEnabled; + + [JsonPropertyName("IsAdvancedAIEnabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public BoolProperty LegacyAdvancedAIEnabledProperty + { + get => null; + set + { + if (value is not null) + { + LegacyAdvancedAIEnabled = value.Value; + } + } + } + + [JsonIgnore] + public bool? LegacyAdvancedAIEnabled + { + get => _legacyAdvancedAIEnabled; + private set => _legacyAdvancedAIEnabled = value; + } + + public bool TryConsumeLegacyAdvancedAIEnabled(out bool value) + { + if (_legacyAdvancedAIEnabled is bool flag) + { + value = flag; + _legacyAdvancedAIEnabled = null; + return true; + } + + value = default; + return false; + } [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool ShowCustomPreview { get; set; } diff --git a/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs new file mode 100644 index 0000000000..1ccfa753fa --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs @@ -0,0 +1,29 @@ +// 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.PowerToys.Settings.UI.Library +{ + /// + /// Provides default values for Paste AI provider definitions. + /// + public static class PasteAIProviderDefaults + { + /// + /// Gets the default model name for a given AI service type. + /// + public static string GetDefaultModelName(AIServiceType serviceType) + { + return serviceType switch + { + AIServiceType.OpenAI => "gpt-4o", + AIServiceType.AzureOpenAI => "gpt-4o", + AIServiceType.Mistral => "mistral-large-latest", + AIServiceType.Google => "gemini-1.5-pro", + AIServiceType.AzureAIInference => "gpt-4o-mini", + AIServiceType.Ollama => "llama3", + _ => string.Empty, + }; + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml index 8d6b0afa68..457615bfe5 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml @@ -499,7 +499,7 @@ MinWidth="200" HorizontalAlignment="Stretch" Header="Model name" - PlaceholderText="gpt-4" + PlaceholderText="gpt-4o" Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelName, Mode=TwoWay}" /> 0 }; + PasswordCredential legacyCredential = TryGetLegacyOpenAICredential(); + + if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed) { return; } - _advancedPasteSettings.Properties.IsAIEnabled = true; - SaveAndNotifySettings(); - OnPropertyChanged(nameof(IsAIEnabled)); + bool configurationUpdated = false; + + if (hasLegacyProviders) + { + configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration); + } + + PasteAIProviderDefinition openAIProvider = null; + if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed) + { + var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration); + openAIProvider = ensureResult.Provider; + configurationUpdated |= ensureResult.Updated; + } + + if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled) + { + openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled; + configurationUpdated = true; + } + + if (legacyCredential is not null && openAIProvider is not null) + { + SavePasteAIApiKey(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password); + RemoveLegacyOpenAICredential(); + } + + bool enabledChanged = false; + if (!properties.IsAIEnabled && legacyCredential is not null) + { + properties.IsAIEnabled = true; + enabledChanged = true; + } + + bool shouldPersist = configurationUpdated || enabledChanged || legacyAdvancedAIConsumed; + + if (shouldPersist) + { + SaveAndNotifySettings(); + + if (configurationUpdated) + { + OnPropertyChanged(nameof(PasteAIConfiguration)); + } + + if (enabledChanged) + { + OnPropertyChanged(nameof(IsAIEnabled)); + } + } } public bool IsEnabled @@ -229,34 +300,30 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO; - private bool LegacyOpenAIKeyExists() + private PasswordCredential TryGetLegacyOpenAICredential() { try { PasswordVault vault = new(); - - // return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null; - var legacyOpenAIKey = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); - if (legacyOpenAIKey != null) - { - string credentialResource = GetAICredentialResource("OpenAI"); - var targetProvider = PasteAIConfiguration?.ActiveProvider ?? PasteAIConfiguration?.Providers?.FirstOrDefault(); - string providerId = targetProvider?.Id ?? string.Empty; - string serviceType = targetProvider?.ServiceType ?? "OpenAI"; - string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType); - PasswordCredential cred = new(credentialResource, credentialUserName, legacyOpenAIKey.Password); - vault.Add(cred); - - // delete old key - TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); - return true; - } - - return false; + var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); + credential?.RetrievePassword(); + return credential; + } + catch (Exception) + { + return null; + } + } + + private void RemoveLegacyOpenAICredential() + { + try + { + PasswordVault vault = new(); + TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); } catch (Exception) { - return false; } } @@ -519,7 +586,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels var provider = new PasteAIProviderDefinition { ServiceType = persistedServiceType, - ModelName = GetDefaultModelName(normalizedServiceType), + ModelName = PasteAIProviderDefaults.GetDefaultModelName(normalizedServiceType), EndpointUrl = string.Empty, ApiVersion = string.Empty, DeploymentName = string.Empty, @@ -559,20 +626,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels return serviceTypeKind; } - private static string GetDefaultModelName(AIServiceType serviceType) - { - return serviceType switch - { - AIServiceType.OpenAI => "gpt-4", - AIServiceType.AzureOpenAI => "gpt-4", - AIServiceType.Mistral => "mistral-large-latest", - AIServiceType.Google => "gemini-2.5-pro", - AIServiceType.AzureAIInference => "gpt-4o-mini", - AIServiceType.Ollama => "llama3", - _ => string.Empty, - }; - } - public bool IsServiceTypeAllowedByGPO(AIServiceType serviceType) { var metadata = AIServiceTypeRegistry.GetMetadata(serviceType); @@ -1352,7 +1405,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } pasteConfig.Providers ??= new ObservableCollection(); + + bool configurationUpdated = AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(pasteConfig); + SubscribeToPasteAIProviders(pasteConfig); + + if (configurationUpdated) + { + SaveAndNotifySettings(); + OnPropertyChanged(nameof(PasteAIConfiguration)); + } } private static string RetrieveCredentialValue(string credentialResource, string credentialUserName) From 3bfa0a0cf8f98a6b5d8c753331c0b35dc3f2a41a Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Wed, 12 Nov 2025 10:52:34 +0000 Subject: [PATCH 2/6] Update CursorWrap settings (#43492) ## Summary of the Pull Request Modify CursorWrap settings so that 'Activate on Startup' is enabled when CursorWrap is enabled. Disabling CursorWrap doesn't change the 'Activate on Startup' toggle, this will need to be manually disabled, CursorWrap is active when enabled, the hotkey can be used to temporarily disable the CursorWrap functionality. ## PR Checklist - [ ] Closes: #xxx - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed Manual validation performed on desktop PC and Surface Laptop. --- src/modules/MouseUtils/CursorWrap/dllmain.cpp | 9 +++++---- .../Settings.UI/ViewModels/MouseUtilsViewModel.cs | 7 +++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/modules/MouseUtils/CursorWrap/dllmain.cpp b/src/modules/MouseUtils/CursorWrap/dllmain.cpp index ece1948d01..74524ed9f9 100644 --- a/src/modules/MouseUtils/CursorWrap/dllmain.cpp +++ b/src/modules/MouseUtils/CursorWrap/dllmain.cpp @@ -196,10 +196,10 @@ public: m_enabled = true; Trace::EnableCursorWrap(true); - if (m_autoActivate) - { - StartMouseHook(); - } + // Always start the mouse hook when the module is enabled + // This ensures cursor wrapping is active immediately after enabling + StartMouseHook(); + Logger::info("CursorWrap enabled - mouse hook started"); } // Disable the powertoy @@ -208,6 +208,7 @@ public: m_enabled = false; Trace::EnableCursorWrap(false); StopMouseHook(); + Logger::info("CursorWrap disabled - mouse hook stopped"); } // Returns if the powertoys is enabled diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs index 518b2a6fa4..27695d1037 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs @@ -1000,6 +1000,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels GeneralSettingsConfig.Enabled.CursorWrap = value; OnPropertyChanged(nameof(IsCursorWrapEnabled)); + // Auto-enable the AutoActivate setting when CursorWrap is enabled + // This ensures cursor wrapping is active immediately after enabling + if (value && !_cursorWrapAutoActivate) + { + CursorWrapAutoActivate = true; + } + OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); SendConfigMSG(outgoing.ToString()); From b015d6a778a83646d1b85146645bacbe0075a055 Mon Sep 17 00:00:00 2001 From: Trevor Date: Wed, 12 Nov 2025 11:28:31 -0800 Subject: [PATCH 3/6] Update onenote png icons to new core10 design (#43506) ## Summary of the Pull Request Update the oneNote pngs to October Core 10 icon design MSNews article: https://www.msn.com/en-us/technology/software/microsoft-s-new-office-icons-are-more-curvy-and-colorful/ar-AA1NFYYI ## PR Checklist - [x] Closes: #43507 - [ ] **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 - [x] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../Images/oneNote.dark.png | Bin 550 -> 604 bytes .../Images/oneNote.light.png | Bin 525 -> 565 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png index 4228da1e88fa1c76f55e1e31c94f36cd0fec6d85..7a96d92df10f256583ae8440819dbe055ce68302 100644 GIT binary patch delta 529 zcmV+s0`C2$1l$CWR)6tHL_t(|0qvQMQG-wnfP?1%j({7ufs9Zm5GQCiC{7?wK%Jy@ z1SdczXg7$-5sn;`_5tBX?|pYU;PLK}kdT-60PuJ`o<9X5#w2@^Bih4bW+Wk4LxbLl z;!}{j8WDgatUMhexjd14$kCFJ%vRLP+? zlNun(^Nb#X5mS3$3zhPpB!}94o_+73@Q@vHH${UF)}zc3obN zx>j(a9^0xts@)5vJ=cvbWAb|XO?6wf^o#U(U}=EdORpJViHp8YX_S@4VkwzGGn8hf zODU1ja`K4`;ke&l==Tc!8D2`@nGg~WFe|xEvBNje@qhWFmJ*o?r9~|6yBfgCb10pb zvOCeI7FuCT8k~b`A2H|e1ey~Rhf=v$H2>tK!GXKZFc6E6ct{%D!4itLq42f~vUrgy}%6mK>k2$|^C?DK( T61tcG015yANkvXXu0mjf^eXRl5j z+cOM7Puo3w|qe&*a{mY#$^cU<9~+m)P|L(K6bNi2^F(P)E?Ni5ql2~*%rNU+nk*x|Hk4ulHSMiXur z(Hv!veQ#HLDj#f(m@FVQXMeozV}Uh>XCn)I|6-bAIM}Z&BZTjnOeW*O4T7zgO~{@6C_Mh z7=Z~26SNx?pKL28O&TZusf+wcr$nih`uWbjvjb!@narh83fln(bQ2VLVPUjM3fEI- zS2!FevCA^pAQ3pGXIN^_;7*MRhr4r+*G*_O`oi)4b`JN(e>9Lu(;G>C;Fv>+u%DRB fET73_GKupGKb#q4`F(AC00000NkvXXu0mjfHXq{p delta 449 zcmV;y0Y3h<1dRlcR)3;NL_t(|0qvQMQG+lThTnP);0AVtZXizJ2<-;y1jGr{4eADN zP@KSSV4a}dpnfrMg_H!yFZA-MH;{zW{>WWR)4`XeL^_+87|}Y9&S)Z zp=W1z>lDmw2czXltr{Z(pT*4j2u z`^wO9yL|5-lPL5;I=~+c;wjg~RGm!>;FEvDNHEO<@9?_{y%}o2ZTZV5&oBap8gNg# zE3f$~>@Y%fQGaNK=B9!hGr1_VAU&4XU!(#NG0nt`^a0@u&i1G!1 Date: Thu, 13 Nov 2025 02:32:05 +0100 Subject: [PATCH 4/6] Minor ZoomIt fixes (#43495) ## Summary of the Pull Request Regarding #43265 / #43266, I noticed some minor spelling mistakes. I also refactored some literal strings to string constants. I didn't discuss this with core contributors or @MarioHewardt. Just a quick shot-from-the-hip PR ;-) I hope that isn't a problem... _Note: this is a new Pull Request for this change. I had to close the previous one (#43443), because I had made a commit using a wrong Github account._ ## PR Checklist - [ ] Closes: #xxx - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- src/modules/ZoomIt/ZoomIt/Zoomit.cpp | 2 +- .../Settings.UI/ViewModels/ZoomItViewModel.cs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp index cf0824ada2..bc476155af 100644 --- a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp +++ b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp @@ -1812,7 +1812,7 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, // Check if GIF is selected by comparing the text bool isGifSelected = (wcscmp(selectedText, L"GIF") == 0); - // if gif is selected set the scaling to the g_recordScaleGIF value otherwise to the g_recordScaleMP4 value + // If GIF is selected, set the scaling to the g_RecordScalingGIF value; otherwise to the g_RecordScalingMP4 value if (isGifSelected) { g_RecordScaling = g_RecordScalingGIF; diff --git a/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs index 2d93deef81..e1704e16fb 100644 --- a/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs @@ -24,6 +24,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { public class ZoomItViewModel : Observable { + private const string FormatGif = "GIF"; + private const string FormatMp4 = "MP4"; + private ISettingsUtils SettingsUtils { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; } @@ -656,12 +659,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { get { - if (_zoomItSettings.Properties.RecordFormat.Value == "GIF") + if (_zoomItSettings.Properties.RecordFormat.Value == FormatGif) { return 0; } - if (_zoomItSettings.Properties.RecordFormat.Value == "MP4") + if (_zoomItSettings.Properties.RecordFormat.Value == FormatMp4) { return 1; } @@ -672,19 +675,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels set { int format = 0; - if (_zoomItSettings.Properties.RecordFormat.Value == "GIF") + if (_zoomItSettings.Properties.RecordFormat.Value == FormatGif) { format = 0; } - if (_zoomItSettings.Properties.RecordFormat.Value == "MP4") + if (_zoomItSettings.Properties.RecordFormat.Value == FormatMp4) { format = 1; } if (format != value) { - _zoomItSettings.Properties.RecordFormat.Value = value == 0 ? "GIF" : "MP4"; + _zoomItSettings.Properties.RecordFormat.Value = value == 0 ? FormatGif : FormatMp4; OnPropertyChanged(nameof(RecordFormatIndex)); NotifySettingsChanged(); From 1b742ef817f817f912ae6831f4103fd7da0a4d38 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Thu, 13 Nov 2025 02:32:25 +0100 Subject: [PATCH 5/6] [AP] Settings UI improvements (#43488) ## Summary of the Pull Request ## PR Checklist - [ ] Closes: #xxx - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../ModelPicker/FoundryLocalModelPicker.xaml | 44 +++--- .../SettingsXAML/Views/AdvancedPastePage.xaml | 36 ++--- .../Settings.UI/Strings/en-us/Resources.resw | 140 +++++++++++++----- 3 files changed, 143 insertions(+), 77 deletions(-) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml index f695301e3a..292113dd42 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml @@ -42,8 +42,9 @@ HorizontalAlignment="Center" /> @@ -56,7 +57,6 @@ - - + - - - + TextWrapping="Wrap" /> + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml index 457615bfe5..7f010049ba 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml @@ -103,10 +103,7 @@ - + @@ -496,45 +493,43 @@ Spacing="16"> - + x:Uid="AdvancedPaste_APIKey" + MinWidth="200" /> @@ -575,13 +570,10 @@ IsOn="{x:Bind ViewModel.PasteAIProviderDraft.EnableAdvancedAI, Mode=TwoWay}" Toggled="PasteAIEnableAdvancedAICheckBox_Toggled" Visibility="Collapsed"> - - - - - + + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 650ac06727..d2c66c6a33 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -1,17 +1,17 @@ - @@ -644,14 +644,11 @@ Please review the placeholder content that represents the final terms and usage Enable OpenAI content moderation - - Enable Advanced AI - - + Use built-in functions to handle complex tasks. Token consumption may increase. - Access Clipboard History + Show what's currently on your Clipboard and access your Clipboard history Clipboard History shows a list of previously copied items. @@ -4058,11 +4055,8 @@ Activate by holding the key for the character you want to add an accent to, then Preview the output of AI formats and Image to text before pasting - - Advanced AI - - - Supports advanced workflows by chaining transformations and working with files and images. May use additional API credits. + + Enable Advanced AI Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, markdown, or json directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an AI powered option that is 100% opt-in and requires an Open AI key. Note: this will replace the formatted text in your clipboard with the selected format. @@ -4604,7 +4598,7 @@ Activate by holding the key for the character you want to add an accent to, then If you do not have credits you will see an 'API key quota exceeded' error - Automatically close the Advanced Paste window after it loses focus + Automatically close the window after it loses focus Advanced Paste is a product name, do not loc @@ -5679,4 +5673,74 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Set Location + + Open Foundry Local model list + Do not localize "Foundry Local", it's a product name + + + Run Foundry Local to download or add a local model + Do not localize "Foundry Local", it's a product name + + + No models downloaded + + + Loading Foundry Local status.. + Do not localize "Foundry Local", it's a product name + + + Foundry Local model + Do not localize "Foundry Local", it's a product name + + + Use the Foundry Local CLI to download models that run locally on-device. They'll appear here. + Do not localize "Foundry Local", it's a product name + + + Refresh model list + + + Foundry Local is not available on this device yet. + Do not localize "Foundry Local", it's a product name + + + Start the Foundry Local service before returning to PowerToys. + + + Follow the Foundry Local CLI guide + Do not localize "Foundry Local", it's a product name + + + Model providers + + + Add online or local models + + + Edit + + + Remove + + + Model name + + + Endpoint URL + + + API key + + + Enter API key + + + API version + + + Deployment name + + + System prompt + \ No newline at end of file From b41ed2feb1fdbaf1eae3765afc5bb52cfa8f1b65 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Thu, 13 Nov 2025 02:36:49 +0100 Subject: [PATCH 6/6] Minor styling tweaks in README (#43494) Added a section for utilities and updated installation instructions. ## Summary of the Pull Request ## PR Checklist - [ ] Closes: #xxx - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- README.md | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 067d6d6f50..ab53e53a46 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@

Microsoft PowerToys

- +

+ Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks. +

Installation ยท @@ -18,8 +20,10 @@ Release notes



-Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks. -

+ +## ๐Ÿ”จ Utilities + +PowerToys includes over 25 utilities to help you customize and optimize your Windows experience: | | | | |---|---|---| @@ -37,20 +41,13 @@ Microsoft PowerToys is a collection of utilities that help you customize Windows ## ๐Ÿ“‹ Installation -For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install). - -Before you begin, make sure your device meets the system requirements: - -> [!NOTE] -> - Windows 11 or Windows 10 version 2004 (20H1 / build 19041) or newer -> - 64-bit processor: x64 or ARM64 -> - Latest stable version of [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) is installed via the bootstrapper during setup - -Choose one of the installation methods below: +For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install). +But to get started quickly, choose one of the installation methods below: +

-Download .exe from GitHub - +Download .exe from GitHub +
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer. @@ -67,11 +64,11 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve | Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] | | Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] | | Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] | -
-Microsoft Store +Microsoft Store +
You can easily install PowerToys from the Microsoft Store:

@@ -82,10 +79,9 @@ You can easily install PowerToys from the Microsoft Store:

-
-WinGet - +WinGet +
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell: *User scope installer [default]* @@ -100,8 +96,8 @@ winget install --scope machine Microsoft.PowerToys -s winget
-Other methods - +Other methods +
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.