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.
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/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/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/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 4228da1e88..7a96d92df1 100644 Binary files a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png and b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png differ diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png index 6c4dcc5ae5..580cf3f609 100644 Binary files a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png and b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png differ 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/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 a4991a1345..96869ce46a 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,40 +493,39 @@ Spacing="16"> - + x:Uid="AdvancedPaste_APIKey" + MinWidth="200" /> - - - - - + + 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 diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs index a7ad7e3632..92a85d96c2 100644 --- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs @@ -172,19 +172,90 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private void MigrateLegacyAIEnablement() { - if (_advancedPasteSettings.Properties.IsAIEnabled || IsOnlineAIModelsDisallowedByGPO) + var properties = _advancedPasteSettings?.Properties; + if (properties is null) { return; } - if (!LegacyOpenAIKeyExists()) + bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag); + bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag; + + if (IsOnlineAIModelsDisallowedByGPO) + { + if (legacyAdvancedAIConsumed) + { + SaveAndNotifySettings(); + } + + return; + } + + var configuration = properties.PasteAIConfiguration; + if (configuration is null) + { + configuration = new PasteAIConfiguration(); + properties.PasteAIConfiguration = configuration; + } + + bool hasLegacyProviders = configuration.LegacyProviderConfigurations is { Count: > 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) 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()); 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();