Merge remote-tracking branch 'origin/main' into stable

This commit is contained in:
Kai Tao
2025-11-13 10:39:32 +08:00
37 changed files with 917 additions and 288 deletions

View File

@@ -147,6 +147,18 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.AdvancedPasteSemanticKernelFormatEvent</td> <td>Microsoft.PowerToys.AdvancedPasteSemanticKernelFormatEvent</td>
<td>Triggered when Advanced Paste leverages the Semantic Kernel.</td> <td>Triggered when Advanced Paste leverages the Semantic Kernel.</td>
</tr> </tr>
<tr>
<td>Microsoft.PowerToys.AdvancedPasteSemanticKernelErrorEvent</td>
<td>Occurs when the Semantic Kernel workflow encounters an error.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.AdvancedPasteEndpointUsageEvent</td>
<td>Logs the AI provider, model, and processing duration for each endpoint call.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.AdvancedPasteCustomActionErrorEvent</td>
<td>Records provider, model, and status details when a custom action fails.</td>
</tr>
</table> </table>
### Always on Top ### Always on Top

View File

@@ -7,7 +7,9 @@
<h1 align="center"> <h1 align="center">
<span>Microsoft PowerToys</span> <span>Microsoft PowerToys</span>
</h1> </h1>
<p align="center">
<span align="center">Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.</span>
</p>
<h3 align="center"> <h3 align="center">
<a href="#-installation">Installation</a> <a href="#-installation">Installation</a>
<span> · </span> <span> · </span>
@@ -18,8 +20,10 @@
<a href="#-whats-new">Release notes</a> <a href="#-whats-new">Release notes</a>
</h3> </h3>
<br/><br/> <br/><br/>
Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.
<br/><br/> ## 🔨 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 ## 📋 Installation
For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install). For detailed installation instructions and system requirements, 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:
But to get started quickly, choose one of the installation methods below:
<br/><br/>
<details open> <details open>
<summary>Download .exe from GitHub</summary> <summary><strong>Download .exe from GitHub</strong></summary>
<br/>
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. 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.
<!-- items that need to be updated release to release --> <!-- items that need to be updated release to release -->
@@ -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] | | Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] | | Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] | | Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
</details> </details>
<details> <details>
<summary>Microsoft Store</summary> <summary><strong>Microsoft Store</strong></summary>
<br/>
You can easily install PowerToys from the Microsoft Store: You can easily install PowerToys from the Microsoft Store:
<p> <p>
<a style="text-decoration:none" href="https://aka.ms/getPowertoys"> <a style="text-decoration:none" href="https://aka.ms/getPowertoys">
@@ -82,10 +79,9 @@ You can easily install PowerToys from the Microsoft Store:
</p> </p>
</details> </details>
<details> <details>
<summary>WinGet</summary> <summary><strong>WinGet</strong></summary>
<br/>
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: 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]* *User scope installer [default]*
@@ -100,8 +96,8 @@ winget install --scope machine Microsoft.PowerToys -s winget
</details> </details>
<details> <details>
<summary>Other methods</summary> <summary><strong>Other methods</strong></summary>
<br/>
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. 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.
</details> </details>

View File

@@ -49,6 +49,8 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
public bool CloseAfterLosingFocus => false; public bool CloseAfterLosingFocus => false;
public bool EnableClipboardPreview => true;
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions; public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions; public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;

View File

@@ -112,11 +112,7 @@ namespace AdvancedPaste
/// Invoked when the application is launched. /// Invoked when the application is launched.
/// </summary> /// </summary>
/// <param name="args">Details about the launch request and process.</param> /// <param name="args">Details about the launch request and process.</param>
#if DEBUG
protected async override void OnLaunched(LaunchActivatedEventArgs args)
#else
protected override void OnLaunched(LaunchActivatedEventArgs args) protected override void OnLaunched(LaunchActivatedEventArgs args)
#endif
{ {
var cmdArgs = Environment.GetCommandLineArgs(); var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1) if (cmdArgs?.Length > 1)
@@ -138,10 +134,6 @@ namespace AdvancedPaste
{ {
ProcessNamedPipe(cmdArgs[2]); ProcessNamedPipe(cmdArgs[2]);
} }
#if DEBUG
await ShowWindow(); // This allows for direct access without using PowerToys Runner, not all functionality might work
#endif
} }
private void ProcessNamedPipe(string pipeName) private void ProcessNamedPipe(string pipeName)

View File

@@ -611,10 +611,10 @@
CornerRadius="{StaticResource ControlCornerRadius}" CornerRadius="{StaticResource ControlCornerRadius}"
Visibility="{x:Bind IsLocalModel, Mode=OneWay}"> Visibility="{x:Bind IsLocalModel, Mode=OneWay}">
<TextBlock <TextBlock
x:Uid="LocalModelBadge"
AutomationProperties.AccessibilityView="Raw" AutomationProperties.AccessibilityView="Raw"
FontSize="10" FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
Text="Local" />
</Border> </Border>
<!--<Border <!--<Border
Grid.Column="2" Grid.Column="2"

View File

@@ -156,7 +156,7 @@
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="20" CornerRadius="20"
Visibility="{x:Bind ViewModel.ClipboardHasData, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"> Visibility="{x:Bind ViewModel.ShowClipboardPreview, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -168,7 +168,8 @@
Margin="0,0,4,0" Margin="0,0,4,0"
VerticalAlignment="Center" VerticalAlignment="Center"
IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}" IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}"
Style="{StaticResource SubtleButtonStyle}"> Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind ViewModel.ShowClipboardHistoryButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ToolTipService.ToolTip> <ToolTipService.ToolTip>
<TextBlock x:Uid="ClipboardHistoryButtonToolTip" /> <TextBlock x:Uid="ClipboardHistoryButtonToolTip" />
</ToolTipService.ToolTip> </ToolTipService.ToolTip>
@@ -270,9 +271,9 @@
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock TextWrapping="Wrap"> <TextBlock TextWrapping="Wrap">
<Run x:Uid="AIMistakeNote" /><LineBreak /><Run <Run x:Uid="AIMistakeNote" /><LineBreak /><Run
x:Uid="CustomEndpointWarning"
FontSize="12" FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
Text="You are using a custom endpoint. Verify all answers." />
</TextBlock> </TextBlock>
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<HyperlinkButton <HyperlinkButton

View File

@@ -18,6 +18,7 @@ namespace AdvancedPaste.Helpers
PromptTokens = semanticKernelFormatEvent.PromptTokens; PromptTokens = semanticKernelFormatEvent.PromptTokens;
CompletionTokens = semanticKernelFormatEvent.CompletionTokens; CompletionTokens = semanticKernelFormatEvent.CompletionTokens;
ModelName = semanticKernelFormatEvent.ModelName; ModelName = semanticKernelFormatEvent.ModelName;
ProviderType = semanticKernelFormatEvent.ProviderType;
ActionChain = semanticKernelFormatEvent.ActionChain; ActionChain = semanticKernelFormatEvent.ActionChain;
} }
@@ -38,6 +39,8 @@ namespace AdvancedPaste.Helpers
public string ModelName { get; set; } public string ModelName { get; set; }
public string ProviderType { get; set; }
public string ActionChain { get; set; } public string ActionChain { get; set; }
public string ToJsonString() => JsonSerializer.Serialize(this, SourceGenerationContext.Default.AIServiceFormatEvent); public string ToJsonString() => JsonSerializer.Serialize(this, SourceGenerationContext.Default.AIServiceFormatEvent);

View File

@@ -19,6 +19,8 @@ namespace AdvancedPaste.Settings
public bool CloseAfterLosingFocus { get; } public bool CloseAfterLosingFocus { get; }
public bool EnableClipboardPreview { get; }
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions { get; } public IReadOnlyList<AdvancedPasteCustomAction> CustomActions { get; }
public IReadOnlyList<PasteFormats> AdditionalActions { get; } public IReadOnlyList<PasteFormats> AdditionalActions { get; }

View File

@@ -40,6 +40,8 @@ namespace AdvancedPaste.Settings
public bool CloseAfterLosingFocus { get; private set; } public bool CloseAfterLosingFocus { get; private set; }
public bool EnableClipboardPreview { get; private set; }
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions; public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions; public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
@@ -53,6 +55,7 @@ namespace AdvancedPaste.Settings
IsAIEnabled = false; IsAIEnabled = false;
ShowCustomPreview = true; ShowCustomPreview = true;
CloseAfterLosingFocus = false; CloseAfterLosingFocus = false;
EnableClipboardPreview = true;
PasteAIConfiguration = new PasteAIConfiguration(); PasteAIConfiguration = new PasteAIConfiguration();
_additionalActions = []; _additionalActions = [];
_customActions = []; _customActions = [];
@@ -107,6 +110,7 @@ namespace AdvancedPaste.Settings
IsAIEnabled = properties.IsAIEnabled; IsAIEnabled = properties.IsAIEnabled;
ShowCustomPreview = properties.ShowCustomPreview; ShowCustomPreview = properties.ShowCustomPreview;
CloseAfterLosingFocus = properties.CloseAfterLosingFocus; CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
EnableClipboardPreview = properties.EnableClipboardPreview;
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration(); PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
var sourceAdditionalActions = properties.AdditionalActions; var sourceAdditionalActions = properties.AdditionalActions;
@@ -163,28 +167,143 @@ namespace AdvancedPaste.Settings
return false; 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; return false;
} }
settings.Properties.IsAIEnabled = true; bool configurationUpdated = false;
return true;
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 try
{ {
PasswordVault vault = new(); 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) 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) public async Task SetActiveAIProviderAsync(string providerId)
{ {
if (string.IsNullOrWhiteSpace(providerId)) if (string.IsNullOrWhiteSpace(providerId))

View File

@@ -214,7 +214,7 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
var serviceType = GetRuntimeConfiguration().ServiceType; var serviceType = GetRuntimeConfiguration().ServiceType;
return new OpenAIPromptExecutionSettings return new OpenAIPromptExecutionSettings
{ {
FunctionChoiceBehavior = FunctionChoiceBehavior.Required(), FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
Temperature = 0.01, Temperature = 0.01,
}; };
} }

View File

@@ -83,32 +83,39 @@ namespace AdvancedPaste.Services.CustomActions
SystemPrompt = systemPrompt, SystemPrompt = systemPrompt,
}; };
var operationStart = DateTime.UtcNow;
var providerContent = await provider.ProcessPasteAsync( var providerContent = await provider.ProcessPasteAsync(
request, request,
cancellationToken, cancellationToken,
progress); progress);
var durationMs = (int)Math.Round((DateTime.UtcNow - operationStart).TotalMilliseconds);
var usage = request.Usage; var usage = request.Usage;
var content = providerContent ?? string.Empty; var content = providerContent ?? string.Empty;
// Log endpoint usage // Log endpoint usage (custom action pipeline is not the advanced SK flow)
var endpointEvent = new AdvancedPasteEndpointUsageEvent(providerConfig.ProviderType); var endpointEvent = new AdvancedPasteEndpointUsageEvent(providerConfig.ProviderType, providerConfig.Model ?? string.Empty, isAdvanced: false, durationMs: durationMs);
PowerToysTelemetry.Log.WriteEvent(endpointEvent); PowerToysTelemetry.Log.WriteEvent(endpointEvent);
Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}"); Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}, DurationMs={durationMs}");
return new CustomActionTransformResult(content, usage); return new CustomActionTransformResult(content, usage);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} failed", ex); Logger.LogError($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} failed", ex);
var statusCode = ExtractStatusCode(ex);
var modelName = providerConfig.Model ?? string.Empty;
AdvancedPasteCustomActionErrorEvent errorEvent = new(providerConfig.ProviderType, modelName, statusCode, ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
PowerToysTelemetry.Log.WriteEvent(errorEvent);
if (ex is PasteActionException or OperationCanceledException) if (ex is PasteActionException or OperationCanceledException)
{ {
throw; throw;
} }
var statusCode = ExtractStatusCode(ex);
var failureMessage = providerConfig.ProviderType switch var failureMessage = providerConfig.ProviderType switch
{ {
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => ErrorHelpers.TranslateErrorText(statusCode), AIServiceType.OpenAI or AIServiceType.AzureOpenAI => ErrorHelpers.TranslateErrorText(statusCode),

View File

@@ -186,12 +186,20 @@ public abstract class KernelServiceBase(
private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable<ActionChainItem> actionChain, AIServiceUsage usage) private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable<ActionChainItem> actionChain, AIServiceUsage usage)
{ {
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, AdvancedAIModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain)); var runtimeConfig = GetRuntimeConfiguration();
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(
cacheUsed,
isSavedQuery,
usage.PromptTokens,
usage.CompletionTokens,
AdvancedAIModelName,
runtimeConfig.ServiceType.ToString(),
AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
PowerToysTelemetry.Log.WriteEvent(telemetryEvent); PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
// Log endpoint usage // Log endpoint usage
var runtimeConfig = GetRuntimeConfiguration(); var endpointEvent = new AdvancedPasteEndpointUsageEvent(runtimeConfig.ServiceType, AdvancedAIModelName, isAdvanced: true);
var endpointEvent = new AdvancedPasteEndpointUsageEvent(runtimeConfig.ServiceType);
PowerToysTelemetry.Log.WriteEvent(endpointEvent); PowerToysTelemetry.Log.WriteEvent(endpointEvent);
var logEvent = new AIServiceFormatEvent(telemetryEvent); var logEvent = new AIServiceFormatEvent(telemetryEvent);

View File

@@ -359,6 +359,13 @@
</data> </data>
<data name="Relative_Date_TimeFormat" xml:space="preserve"> <data name="Relative_Date_TimeFormat" xml:space="preserve">
<value>{0}, {1}</value> <value>{0}, {1}</value>
<comment>(e.g., 10/20/2025, 17:05 in the users locale)</comment> <comment>(e.g., "10/20/2025, 17:05" in the user's locale)</comment>
</data>
<data name="CustomEndpointWarning" xml:space="preserve">
<value>You are using a custom endpoint. Verify all answers.</value>
</data>
<data name="LocalModelBadge" xml:space="preserve">
<value>Local</value>
<comment>Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally</comment>
</data> </data>
</root> </root>

View File

@@ -0,0 +1,34 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace AdvancedPaste.Telemetry;
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public sealed class AdvancedPasteCustomActionErrorEvent : EventBase, IEvent
{
public AdvancedPasteCustomActionErrorEvent(AIServiceType providerType, string modelName, int statusCode, string error)
{
ProviderType = providerType.ToString();
ModelName = modelName;
StatusCode = statusCode;
Error = error;
}
public string ProviderType { get; set; }
public string ModelName { get; set; }
public int StatusCode { get; set; }
public string Error { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}

View File

@@ -19,9 +19,27 @@ public class AdvancedPasteEndpointUsageEvent : EventBase, IEvent
/// </summary> /// </summary>
public string ProviderType { get; set; } public string ProviderType { get; set; }
public AdvancedPasteEndpointUsageEvent(AIServiceType providerType) /// <summary>
/// Gets or sets the configured model name.
/// </summary>
public string ModelName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the advanced AI pipeline was used.
/// </summary>
public bool IsAdvanced { get; set; }
/// <summary>
/// Gets or sets the total duration in milliseconds, or -1 if unavailable.
/// </summary>
public int DurationMs { get; set; }
public AdvancedPasteEndpointUsageEvent(AIServiceType providerType, string modelName, bool isAdvanced, int durationMs = -1)
{ {
ProviderType = providerType.ToString(); ProviderType = providerType.ToString();
ModelName = modelName;
IsAdvanced = isAdvanced;
DurationMs = durationMs;
} }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;

View File

@@ -14,7 +14,7 @@ namespace AdvancedPaste.Telemetry;
[EventData] [EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string actionChain) : EventBase, IEvent public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string providerType, string actionChain) : EventBase, IEvent
{ {
public static string FormatActionChain(IEnumerable<ActionChainItem> actionChain) => FormatActionChain(actionChain.Select(item => item.Format)); public static string FormatActionChain(IEnumerable<ActionChainItem> actionChain) => FormatActionChain(actionChain.Select(item => item.Format));
@@ -30,6 +30,8 @@ public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSaved
public string ModelName { get; set; } = modelName; public string ModelName { get; set; } = modelName;
public string ProviderType { get; set; } = providerType;
/// <summary> /// <summary>
/// Gets or sets a comma-separated list of paste formats used - in the same order they were executed. /// Gets or sets a comma-separated list of paste formats used - in the same order they were executed.
/// Conceptually an array but formatted this way to work around https://github.com/dotnet/runtime/issues/10428 /// Conceptually an array but formatted this way to work around https://github.com/dotnet/runtime/issues/10428

View File

@@ -63,6 +63,7 @@ namespace AdvancedPaste.ViewModels
private ClipboardFormat _availableClipboardFormats; private ClipboardFormat _availableClipboardFormats;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowClipboardHistoryButton))]
private bool _clipboardHistoryEnabled; private bool _clipboardHistoryEnabled;
[ObservableProperty] [ObservableProperty]
@@ -225,6 +226,10 @@ namespace AdvancedPaste.ViewModels
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats); public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
public bool ShowClipboardPreview => _userSettings.EnableClipboardPreview;
public bool ShowClipboardHistoryButton => ClipboardHistoryEnabled;
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress); public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
private PasteFormats CustomAIFormat => private PasteFormats CustomAIFormat =>
@@ -310,6 +315,7 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(IsAdvancedAIEnabled)); OnPropertyChanged(nameof(IsAdvancedAIEnabled));
OnPropertyChanged(nameof(AIProviders)); OnPropertyChanged(nameof(AIProviders));
OnPropertyChanged(nameof(AllowedAIProviders)); OnPropertyChanged(nameof(AllowedAIProviders));
OnPropertyChanged(nameof(ShowClipboardPreview));
NotifyActiveProviderChanged(); NotifyActiveProviderChanged();

View File

@@ -196,10 +196,10 @@ public:
m_enabled = true; m_enabled = true;
Trace::EnableCursorWrap(true); Trace::EnableCursorWrap(true);
if (m_autoActivate) // Always start the mouse hook when the module is enabled
{ // This ensures cursor wrapping is active immediately after enabling
StartMouseHook(); StartMouseHook();
} Logger::info("CursorWrap enabled - mouse hook started");
} }
// Disable the powertoy // Disable the powertoy
@@ -208,6 +208,7 @@ public:
m_enabled = false; m_enabled = false;
Trace::EnableCursorWrap(false); Trace::EnableCursorWrap(false);
StopMouseHook(); StopMouseHook();
Logger::info("CursorWrap disabled - mouse hook stopped");
} }
// Returns if the powertoys is enabled // Returns if the powertoys is enabled

View File

@@ -1812,7 +1812,7 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message,
// Check if GIF is selected by comparing the text // Check if GIF is selected by comparing the text
bool isGifSelected = (wcscmp(selectedText, L"GIF") == 0); 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) { if (isGifSelected) {
g_RecordScaling = g_RecordScalingGIF; g_RecordScaling = g_RecordScalingGIF;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 B

After

Width:  |  Height:  |  Size: 565 B

View File

@@ -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
{
/// <summary>
/// Helper methods for migrating legacy Advanced Paste settings to the updated schema.
/// </summary>
public static class AdvancedPasteMigrationHelper
{
/// <summary>
/// Moves legacy provider configuration snapshots into the strongly-typed providers collection.
/// </summary>
/// <param name="configuration">The configuration instance to migrate.</param>
/// <returns>True if the configuration was modified.</returns>
public static bool MigrateLegacyProviderConfigurations(PasteAIConfiguration configuration)
{
if (configuration is null)
{
return false;
}
configuration.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
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;
}
/// <summary>
/// Ensures an OpenAI provider exists in the configuration, creating one if necessary.
/// </summary>
/// <param name="configuration">The configuration instance.</param>
/// <returns>The ensured provider and a flag indicating whether changes were made.</returns>
public static (PasteAIProviderDefinition Provider, bool Updated) EnsureOpenAIProvider(PasteAIConfiguration configuration)
{
return EnsureProvider(configuration, AIServiceType.OpenAI.ToConfigurationString(), null);
}
/// <summary>
/// Ensures a provider for the supplied service type exists, optionally applying a legacy snapshot.
/// </summary>
/// <param name="configuration">The configuration instance.</param>
/// <param name="serviceTypeKey">The persisted service type key.</param>
/// <param name="snapshot">An optional snapshot containing legacy values.</param>
/// <returns>The ensured provider and whether the configuration was updated.</returns>
public static (PasteAIProviderDefinition Provider, bool Updated) EnsureProvider(PasteAIConfiguration configuration, string serviceTypeKey, AIProviderConfigurationSnapshot snapshot)
{
if (configuration is null)
{
return (null, false);
}
configuration.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
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;
}
}
}

View File

@@ -27,6 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
IsAIEnabled = false; IsAIEnabled = false;
ShowCustomPreview = true; ShowCustomPreview = true;
CloseAfterLosingFocus = false; CloseAfterLosingFocus = false;
EnableClipboardPreview = true;
PasteAIConfiguration = new(); PasteAIConfiguration = new();
} }
@@ -52,10 +53,68 @@ namespace Microsoft.PowerToys.Settings.UI.Library
_extensionData.Remove("IsOpenAIEnabled"); _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<string, JsonElement> _extensionData; private Dictionary<string, JsonElement> _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))] [JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool ShowCustomPreview { get; set; } public bool ShowCustomPreview { get; set; }
@@ -63,6 +122,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonConverter(typeof(BoolPropertyJsonConverter))] [JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool CloseAfterLosingFocus { get; set; } public bool CloseAfterLosingFocus { get; set; }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool EnableClipboardPreview { get; set; }
[JsonPropertyName("advanced-paste-ui-hotkey")] [JsonPropertyName("advanced-paste-ui-hotkey")]
public HotkeySettings AdvancedPasteUIShortcut { get; set; } public HotkeySettings AdvancedPasteUIShortcut { get; set; }

View File

@@ -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
{
/// <summary>
/// Provides default values for Paste AI provider definitions.
/// </summary>
public static class PasteAIProviderDefaults
{
/// <summary>
/// Gets the default model name for a given AI service type.
/// </summary>
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,
};
}
}
}

View File

@@ -42,8 +42,9 @@
HorizontalAlignment="Center" /> HorizontalAlignment="Center" />
<TextBlock <TextBlock
x:Name="LoadingStatusTextBlock" x:Name="LoadingStatusTextBlock"
x:Uid="AdvancedPaste_FL_LoadingStatus"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Loading Foundry Local status..."
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
@@ -56,7 +57,6 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel <StackPanel
x:Name="NoModelsPanel" x:Name="NoModelsPanel"
Grid.Row="0" Grid.Row="0"
@@ -64,24 +64,28 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
Orientation="Vertical" Orientation="Vertical"
Spacing="4"> Spacing="4">
<FontIcon FontSize="24" Glyph="&#xE74E;" /> <FontIcon
AutomationProperties.AccessibilityView="Raw"
FontSize="24"
Glyph="&#xF158;" />
<TextBlock <TextBlock
x:Uid="AdvancedPaste_FL_NoModelsDownloaded."
HorizontalAlignment="Center" HorizontalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" Style="{StaticResource BodyStrongTextBlockStyle}"
Text="No models downloaded"
TextAlignment="Center" /> TextAlignment="Center" />
<TextBlock <TextBlock
x:Uid="AdvancedPaste_FL_RunFoundryLocalText.Text"
HorizontalAlignment="Center" HorizontalAlignment="Center"
FontSize="12" FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Run Foundry Local to download or add a local model below."
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<Button <Button
x:Name="LaunchFoundryModelListButton" x:Name="LaunchFoundryModelListButton"
x:Uid="AdvancedPaste_FL_OpenFoundryModelList"
Margin="0,8,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Click="LaunchFoundryModelListButton_Click" Click="LaunchFoundryModelListButton_Click"
Content="Open Foundry model list"
Style="{StaticResource AccentButtonStyle}" /> Style="{StaticResource AccentButtonStyle}" />
</StackPanel> </StackPanel>
@@ -101,10 +105,10 @@
SelectionChanged="CachedModelsComboBox_SelectionChanged"> SelectionChanged="CachedModelsComboBox_SelectionChanged">
<ComboBox.Header> <ComboBox.Header>
<TextBlock> <TextBlock>
<Run Text="Foundry Local model" /><LineBreak /><Run <Run x:Uid="AdvancedPaste_FL_LocalModel" /><LineBreak /><Run
x:Uid="AdvancedPaste_FL_UseCLIToDownloadModels"
FontSize="12" FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
Text="Use the Foundry Local CLI to download models that run locally on-device. They'll appear here." />
</TextBlock> </TextBlock>
</ComboBox.Header> </ComboBox.Header>
</ComboBox> </ComboBox>
@@ -114,9 +118,11 @@
MinHeight="32" MinHeight="32"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Click="RefreshModelsButton_Click" Click="RefreshModelsButton_Click"
Style="{StaticResource SubtleButtonStyle}" Style="{StaticResource SubtleButtonStyle}">
ToolTipService.ToolTip="Refresh model list">
<FontIcon FontSize="16" Glyph="&#xE72C;" /> <FontIcon FontSize="16" Glyph="&#xE72C;" />
<ToolTipService.ToolTip>
<TextBlock x:Uid="AdvancedPaste_FL_RefreshModelList" />
</ToolTipService.ToolTip>
</Button> </Button>
</Grid> </Grid>
<StackPanel <StackPanel
@@ -146,24 +152,28 @@
Spacing="8"> Spacing="8">
<Image Width="36" Source="ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg" /> <Image Width="36" Source="ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg" />
<TextBlock <TextBlock
x:Uid="AdvancedPaste_FL_FLNotavailableYet"
HorizontalAlignment="Center"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="Foundry Local is not available on this device yet."
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
x:Uid="AdvancedPaste_FL_StartService"
HorizontalAlignment="Center"
FontSize="12" FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True" IsTextSelectionEnabled="True"
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap"> TextWrapping="Wrap" />
<Run Text="Start the Foundry Local service before returning to PowerToys." /> <HyperlinkButton
</TextBlock> x:Uid="AdvancedPaste_FL_CLIGuide"
<HyperlinkButton Content="Follow the Foundry Local CLI guide" NavigateUri="https://learn.microsoft.com/en-us/azure/ai-foundry/foundry-local/get-started" /> HorizontalAlignment="Center"
NavigateUri="https://learn.microsoft.com/azure/ai-foundry/foundry-local/get-started" />
<TextBlock <TextBlock
x:Uid="FoundryLocal_RestartRequiredNote" x:Uid="FoundryLocal_RestartRequiredNote"
HorizontalAlignment="Center"
FontSize="12" FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Note: After installing the Foundry Local CLI, restart PowerToys to use it."
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>

View File

@@ -103,10 +103,7 @@
</StackPanel> </StackPanel>
</tkcontrols:SettingsExpander.Description> </tkcontrols:SettingsExpander.Description>
<tkcontrols:SettingsExpander.ItemsHeader> <tkcontrols:SettingsExpander.ItemsHeader>
<tkcontrols:SettingsCard <tkcontrols:SettingsCard x:Uid="AdvancedPaste_ModelProviders" Style="{StaticResource DefaultSettingsExpanderItemStyle}">
Description="Add online or local models"
Header="Model providers"
Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<Button Content="Add model" Style="{StaticResource AccentButtonStyle}"> <Button Content="Add model" Style="{StaticResource AccentButtonStyle}">
<Button.Flyout> <Button.Flyout>
<MenuFlyout x:Name="AddProviderMenuFlyout" Opening="AddProviderMenuFlyout_Opening" /> <MenuFlyout x:Name="AddProviderMenuFlyout" Opening="AddProviderMenuFlyout_Opening" />
@@ -130,16 +127,16 @@
<Button.Flyout> <Button.Flyout>
<MenuFlyout> <MenuFlyout>
<MenuFlyoutItem <MenuFlyoutItem
x:Uid="AdvancedPaste_Edit"
Click="EditPasteAIProviderButton_Click" Click="EditPasteAIProviderButton_Click"
Icon="{ui:FontIcon Glyph=&#xE70F;}" Icon="{ui:FontIcon Glyph=&#xE70F;}"
Tag="{x:Bind}" Tag="{x:Bind}" />
Text="Edit" />
<MenuFlyoutSeparator /> <MenuFlyoutSeparator />
<MenuFlyoutItem <MenuFlyoutItem
x:Uid="AdvancedPaste_Remove"
Click="RemovePasteAIProviderButton_Click" Click="RemovePasteAIProviderButton_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;}" Icon="{ui:FontIcon Glyph=&#xE74D;}"
Tag="{x:Bind}" Tag="{x:Bind}" />
Text="Remove" />
</MenuFlyout> </MenuFlyout>
</Button.Flyout> </Button.Flyout>
</Button> </Button>
@@ -180,6 +177,9 @@
<tkcontrols:SettingsCard Name="AdvancedPasteCloseAfterLosingFocus" ContentAlignment="Left"> <tkcontrols:SettingsCard Name="AdvancedPasteCloseAfterLosingFocus" ContentAlignment="Left">
<CheckBox x:Uid="AdvancedPaste_CloseAfterLosingFocus" IsChecked="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" /> <CheckBox x:Uid="AdvancedPaste_CloseAfterLosingFocus" IsChecked="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="AdvancedPasteEnableClipboardPreview" ContentAlignment="Left">
<CheckBox x:Uid="AdvancedPaste_EnableClipboardPreview" IsChecked="{x:Bind ViewModel.EnableClipboardPreview, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="AdvancedPasteShowCustomPreviewSettingsCard" ContentAlignment="Left"> <tkcontrols:SettingsCard Name="AdvancedPasteShowCustomPreviewSettingsCard" ContentAlignment="Left">
<controls:CheckBoxWithDescriptionControl x:Uid="AdvancedPaste_ShowCustomPreviewSettingsCard" IsChecked="{x:Bind ViewModel.ShowCustomPreview, Mode=TwoWay}" /> <controls:CheckBoxWithDescriptionControl x:Uid="AdvancedPaste_ShowCustomPreviewSettingsCard" IsChecked="{x:Bind ViewModel.ShowCustomPreview, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
@@ -496,46 +496,44 @@
Spacing="16"> Spacing="16">
<TextBox <TextBox
x:Name="PasteAIModelNameTextBox" x:Name="PasteAIModelNameTextBox"
x:Uid="AdvancedPaste_ModelName"
MinWidth="200" MinWidth="200"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Header="Model name" PlaceholderText="gpt-4o"
PlaceholderText="gpt-4"
Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelName, Mode=TwoWay}" /> Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelName, Mode=TwoWay}" />
<TextBox <TextBox
x:Name="PasteAIEndpointUrlTextBox" x:Name="PasteAIEndpointUrlTextBox"
x:Uid="AdvancedPaste_EndpointURL"
MinWidth="200" MinWidth="200"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Header="Endpoint URL"
PlaceholderText="https://your-resource.openai.azure.com/" PlaceholderText="https://your-resource.openai.azure.com/"
Text="{x:Bind ViewModel.PasteAIProviderDraft.EndpointUrl, Mode=TwoWay}" /> Text="{x:Bind ViewModel.PasteAIProviderDraft.EndpointUrl, Mode=TwoWay}" />
<PasswordBox <PasswordBox
x:Name="PasteAIApiKeyPasswordBox" x:Name="PasteAIApiKeyPasswordBox"
MinWidth="200" x:Uid="AdvancedPaste_APIKey"
Header="API key" MinWidth="200" />
PlaceholderText="Enter API Key" />
<TextBox <TextBox
x:Name="PasteAIApiVersionTextBox" x:Name="PasteAIApiVersionTextBox"
x:Uid="AdvancedPaste_APIVersion"
MinWidth="200" MinWidth="200"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Header="API version"
PlaceholderText="2024-10-01" PlaceholderText="2024-10-01"
Text="{x:Bind ViewModel.PasteAIProviderDraft.ApiVersion, Mode=TwoWay}" Text="{x:Bind ViewModel.PasteAIProviderDraft.ApiVersion, Mode=TwoWay}"
Visibility="Collapsed" /> Visibility="Collapsed" />
<TextBox <TextBox
x:Name="PasteAIDeploymentNameTextBox" x:Name="PasteAIDeploymentNameTextBox"
x:Uid="AdvancedPaste_DeploymentName"
MinWidth="200" MinWidth="200"
Header="Deployment name" PlaceholderText="gpt-4o"
PlaceholderText="gpt-4"
Text="{x:Bind ViewModel.PasteAIProviderDraft.DeploymentName, Mode=TwoWay}" /> Text="{x:Bind ViewModel.PasteAIProviderDraft.DeploymentName, Mode=TwoWay}" />
<TextBox <TextBox
x:Name="PasteAISystemPromptTextBox" x:Name="PasteAISystemPromptTextBox"
x:Uid="AdvancedPaste_SystemPrompt"
MinWidth="200" MinWidth="200"
MinHeight="76" MinHeight="76"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
AcceptsReturn="True" AcceptsReturn="True"
Header="System prompt" Header="System prompt"
PlaceholderText="You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. Do not output anything else besides the reformatted clipboard content."
Text="{x:Bind ViewModel.PasteAIProviderDraft.SystemPrompt, Mode=TwoWay}" Text="{x:Bind ViewModel.PasteAIProviderDraft.SystemPrompt, Mode=TwoWay}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<Grid <Grid
@@ -575,13 +573,10 @@
IsOn="{x:Bind ViewModel.PasteAIProviderDraft.EnableAdvancedAI, Mode=TwoWay}" IsOn="{x:Bind ViewModel.PasteAIProviderDraft.EnableAdvancedAI, Mode=TwoWay}"
Toggled="PasteAIEnableAdvancedAICheckBox_Toggled" Toggled="PasteAIEnableAdvancedAICheckBox_Toggled"
Visibility="Collapsed"> Visibility="Collapsed">
<ToolTipService.ToolTip>
<TextBlock Text="" TextWrapping="Wrap" />
</ToolTipService.ToolTip>
<ToggleSwitch.Header> <ToggleSwitch.Header>
<TextBlock> <TextBlock>
<Run Text="Enable Advanced AI" /> <LineBreak /> <Run x:Uid="AdvancedPaste_EnableAdvancedAI" /> <LineBreak />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Use built-in functions to handle complex tasks. Token consumption may increase." /> <Run x:Uid="AdvancedPaste_EnableAdvancedAIDescription" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock> </TextBlock>
</ToggleSwitch.Header> </ToggleSwitch.Header>
</ToggleSwitch> </ToggleSwitch>

View File

@@ -36,6 +36,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private const string AdvancedAISystemPrompt = "You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. Call function when necessary to help user finish the transformation task. You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best. The user will put in a request to format their clipboard data and you will fulfill it. Do not output anything else besides the reformatted clipboard content."; private const string AdvancedAISystemPrompt = "You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. Call function when necessary to help user finish the transformation task. You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best. The user will put in a request to format their clipboard data and you will fulfill it. Do not output anything else besides the reformatted clipboard content.";
private const string SimpleAISystemPrompt = "You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. Do not output anything else besides the reformatted clipboard content."; private const string SimpleAISystemPrompt = "You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. Do not output anything else besides the reformatted clipboard content.";
private static readonly string AdvancedAISystemPromptNormalized = AdvancedAISystemPrompt.Trim();
private static readonly string SimpleAISystemPromptNormalized = SimpleAISystemPrompt.Trim();
private AdvancedPasteViewModel ViewModel { get; set; } private AdvancedPasteViewModel ViewModel { get; set; }
@@ -804,6 +806,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return; return;
} }
NormalizeSystemPrompt(draft);
string serviceType = draft.ServiceType ?? "OpenAI"; string serviceType = draft.ServiceType ?? "OpenAI";
string apiKey = PasteAIApiKeyPasswordBox.Password; string apiKey = PasteAIApiKeyPasswordBox.Password;
string trimmedApiKey = apiKey?.Trim() ?? string.Empty; string trimmedApiKey = apiKey?.Trim() ?? string.Empty;
@@ -834,22 +837,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return; return;
} }
bool isEmptyOrDefault = string.IsNullOrWhiteSpace(draft.SystemPrompt) || NormalizeSystemPrompt(draft);
draft.SystemPrompt.Trim() == AdvancedAISystemPrompt.Trim() || UpdateSystemPromptPlaceholder();
draft.SystemPrompt.Trim() == SimpleAISystemPrompt.Trim();
if (isEmptyOrDefault)
{
if (!draft.EnableAdvancedAI)
{
// Now we'll switch
draft.SystemPrompt = AdvancedAISystemPrompt;
}
else
{
draft.SystemPrompt = SimpleAISystemPrompt;
}
}
} }
private static bool RequiresApiKeyForService(string serviceType) private static bool RequiresApiKeyForService(string serviceType)
@@ -950,15 +939,47 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private Visibility GetServicePrivacyVisibility(string serviceType) => HasServicePrivacyLink(serviceType) ? Visibility.Visible : Visibility.Collapsed; private Visibility GetServicePrivacyVisibility(string serviceType) => HasServicePrivacyLink(serviceType) ? Visibility.Visible : Visibility.Collapsed;
private void UpdateSystemPromptPlaceholder() private static bool IsPlaceholderSystemPrompt(string prompt)
{ {
var draft = ViewModel?.PasteAIProviderDraft; if (string.IsNullOrWhiteSpace(prompt))
if (draft is null || PasteAISystemPromptTextBox is null) {
return true;
}
string trimmedPrompt = prompt.Trim();
return string.Equals(trimmedPrompt, AdvancedAISystemPromptNormalized, StringComparison.Ordinal)
|| string.Equals(trimmedPrompt, SimpleAISystemPromptNormalized, StringComparison.Ordinal);
}
private static void NormalizeSystemPrompt(PasteAIProviderDefinition draft)
{
if (draft is null)
{ {
return; return;
} }
PasteAISystemPromptTextBox.PlaceholderText = draft.EnableAdvancedAI if (IsPlaceholderSystemPrompt(draft.SystemPrompt))
{
draft.SystemPrompt = string.Empty;
}
}
private void UpdateSystemPromptPlaceholder()
{
var draft = ViewModel?.PasteAIProviderDraft;
if (draft is null)
{
return;
}
NormalizeSystemPrompt(draft);
if (PasteAISystemPromptTextBox is null)
{
return;
}
bool useAdvancedPlaceholder = PasteAIEnableAdvancedAICheckBox?.IsOn ?? draft.EnableAdvancedAI;
PasteAISystemPromptTextBox.PlaceholderText = useAdvancedPlaceholder
? AdvancedAISystemPrompt ? AdvancedAISystemPrompt
: SimpleAISystemPrompt; : SimpleAISystemPrompt;
} }

View File

@@ -199,10 +199,8 @@
<ContentDialog <ContentDialog
x:Name="LocationDialog" x:Name="LocationDialog"
x:Uid="LightSwitch_LocationDialog" x:Uid="LightSwitch_LocationDialog"
Closed="LocationDialog_Closed"
IsPrimaryButtonEnabled="False" IsPrimaryButtonEnabled="False"
IsSecondaryButtonEnabled="True" IsSecondaryButtonEnabled="True"
Opened="LocationDialog_Opened"
PrimaryButtonClick="LocationDialog_PrimaryButtonClick" PrimaryButtonClick="LocationDialog_PrimaryButtonClick"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"> PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<Grid RowSpacing="16"> <Grid RowSpacing="16">

View File

@@ -35,9 +35,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private readonly IFileSystemWatcher fileSystemWatcher; private readonly IFileSystemWatcher fileSystemWatcher;
private readonly DispatcherQueue dispatcherQueue; private readonly DispatcherQueue dispatcherQueue;
private bool suppressViewModelUpdates; private bool suppressViewModelUpdates;
private bool suppressLatLonChange = true;
private bool latBoxLoaded;
private bool lonBoxLoaded;
private LightSwitchViewModel ViewModel { get; set; } private LightSwitchViewModel ViewModel { get; set; }
@@ -132,8 +129,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// Since we use this mode, we can remove the selected city data. // Since we use this mode, we can remove the selected city data.
this.ViewModel.SelectedCity = null; this.ViewModel.SelectedCity = null;
this.suppressLatLonChange = false;
// ViewModel.CityTimesText = $"Sunrise: {result.SunriseHour}:{result.SunriseMinute:D2}\n" + $"Sunset: {result.SunsetHour}:{result.SunsetMinute:D2}"; // ViewModel.CityTimesText = $"Sunrise: {result.SunriseHour}:{result.SunriseMinute:D2}\n" + $"Sunset: {result.SunsetHour}:{result.SunsetMinute:D2}";
this.SyncButton.IsEnabled = true; this.SyncButton.IsEnabled = true;
this.SyncLoader.IsActive = false; this.SyncLoader.IsActive = false;
@@ -157,23 +152,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void LatLonBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) private void LatLonBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
{ {
if (this.suppressLatLonChange)
{
return;
}
double latitude = this.LatitudeBox.Value; double latitude = this.LatitudeBox.Value;
double longitude = this.LongitudeBox.Value; double longitude = this.LongitudeBox.Value;
if (double.IsNaN(latitude) || double.IsNaN(longitude)) if (double.IsNaN(latitude) || double.IsNaN(longitude) || (latitude == 0 && longitude == 0))
{
return;
}
double viewModelLatitude = double.TryParse(this.ViewModel.Latitude, out var lat) ? lat : 0.0;
double viewModelLongitude = double.TryParse(this.ViewModel.Longitude, out var lon) ? lon : 0.0;
if (Math.Abs(latitude - viewModelLatitude) < 0.0001 && Math.Abs(longitude - viewModelLongitude) < 0.0001)
{ {
return; return;
} }
@@ -183,7 +165,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute; this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute;
this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute; this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute;
// Show the panel with these values
this.LocationResultPanel.Visibility = Visibility.Visible; this.LocationResultPanel.Visibility = Visibility.Visible;
if (this.LocationDialog != null) if (this.LocationDialog != null)
{ {
@@ -214,37 +195,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
this.SunriseModeChartState(); this.SunriseModeChartState();
} }
private void LocationDialog_Opened(ContentDialog sender, ContentDialogOpenedEventArgs args)
{
this.LatitudeBox.Loaded += LatLonBox_Loaded;
this.LongitudeBox.Loaded += LatLonBox_Loaded;
}
private void LocationDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
this.LatitudeBox.Loaded -= LatLonBox_Loaded;
this.LongitudeBox.Loaded -= LatLonBox_Loaded;
this.latBoxLoaded = false;
this.lonBoxLoaded = false;
}
private void LatLonBox_Loaded(object sender, RoutedEventArgs e)
{
if (sender is NumberBox numberBox && numberBox == this.LatitudeBox && this.LatitudeBox.IsLoaded)
{
this.latBoxLoaded = true;
}
else if (sender is NumberBox numberBox2 && numberBox2 == this.LongitudeBox && this.LongitudeBox.IsLoaded)
{
this.lonBoxLoaded = true;
}
if (this.latBoxLoaded && this.lonBoxLoaded)
{
this.suppressLatLonChange = false;
}
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
if (this.suppressViewModelUpdates) if (this.suppressViewModelUpdates)

View File

@@ -275,41 +275,23 @@
<controls:SettingsGroup x:Uid="MouseUtils_CursorWrap" AutomationProperties.AutomationId="MouseUtils_CursorWrapTestId"> <controls:SettingsGroup x:Uid="MouseUtils_CursorWrap" AutomationProperties.AutomationId="MouseUtils_CursorWrapTestId">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsCursorWrapEnabledGpoConfigured, Mode=OneWay}"> <controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsCursorWrapEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard <tkcontrols:SettingsExpander
Name="MouseUtilsEnableCursorWrap" Name="MouseUtilsEnableCursorWrap"
x:Uid="MouseUtils_Enable_CursorWrap" x:Uid="MouseUtils_Enable_CursorWrap"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CursorWrap.png}" HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CursorWrap.png}"
IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"> IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
IsExpanded="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=TwoWay}" /> <ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard> <tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
<CheckBox x:Uid="MouseUtils_AutoActivate" IsChecked="{x:Bind ViewModel.CursorWrapAutoActivate, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableWrapDuringDrag" IsChecked="{x:Bind ViewModel.CursorWrapDisableWrapDuringDrag, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:GPOInfoControl> </controls:GPOInfoControl>
<tkcontrols:SettingsExpander
Name="MouseUtilsCursorWrapSettingsExpander"
x:Uid="MouseUtils_CursorWrap_ActivationShortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}"
IsExpanded="True">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.CursorWrapActivationShortcut, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="MouseUtils_AutoActivate" IsChecked="{x:Bind ViewModel.CursorWrapAutoActivate, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander
Name="CursorWrapAppearanceBehavior"
x:Uid="Appearance_Behavior"
AutomationProperties.AutomationId="MouseUtils_CursorWrapAppearanceBehaviorId"
HeaderIcon="{ui:FontIcon Glyph=&#xEB3C;}"
IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}"
IsExpanded="False">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableWrapDuringDrag" IsChecked="{x:Bind ViewModel.CursorWrapDisableWrapDuringDrag, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup> </controls:SettingsGroup>
<controls:SettingsGroup x:Uid="MouseUtils_MousePointerCrosshairs" AutomationProperties.AutomationId="MouseUtils_MousePointerCrosshairsTestId"> <controls:SettingsGroup x:Uid="MouseUtils_MousePointerCrosshairs" AutomationProperties.AutomationId="MouseUtils_MousePointerCrosshairsTestId">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsMousePointerCrosshairsEnabledGpoConfigured, Mode=OneWay}"> <controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsMousePointerCrosshairsEnabledGpoConfigured, Mode=OneWay}">

View File

@@ -285,6 +285,9 @@
AutomationProperties.AutomationId="InputOutputNavItem" AutomationProperties.AutomationId="InputOutputNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/InputOutput.png}" Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/InputOutput.png}"
SelectsOnInvoked="False"> SelectsOnInvoked="False">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
<NavigationViewItem.MenuItems> <NavigationViewItem.MenuItems>
<NavigationViewItem <NavigationViewItem
x:Name="KeyboardManagerNavigationItem" x:Name="KeyboardManagerNavigationItem"
@@ -299,7 +302,11 @@
x:Uid="Shell_MouseUtilities" x:Uid="Shell_MouseUtilities"
helpers:NavHelper.NavigateTo="views:MouseUtilsPage" helpers:NavHelper.NavigateTo="views:MouseUtilsPage"
AutomationProperties.AutomationId="MouseUtilitiesNavItem" AutomationProperties.AutomationId="MouseUtilitiesNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseUtils.png}" /> Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseUtils.png}">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem>
<NavigationViewItem <NavigationViewItem
x:Name="MouseWithoutBordersNavigationItem" x:Name="MouseWithoutBordersNavigationItem"
x:Uid="Shell_MouseWithoutBorders" x:Uid="Shell_MouseWithoutBorders"

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -644,14 +644,11 @@ Please review the placeholder content that represents the final terms and usage
<data name="AdvancedPaste_EnablePasteAIModerationToggle.Header" xml:space="preserve"> <data name="AdvancedPaste_EnablePasteAIModerationToggle.Header" xml:space="preserve">
<value>Enable OpenAI content moderation</value> <value>Enable OpenAI content moderation</value>
</data> </data>
<data name="AdvancedPaste_EnableAdvancedAI_SettingsCard.Header" xml:space="preserve"> <data name="AdvancedPaste_EnableAdvancedAIDescription.Text" xml:space="preserve">
<value>Enable Advanced AI</value>
</data>
<data name="AdvancedPaste_EnableAdvancedAI_SettingsCard.Description" xml:space="preserve">
<value>Use built-in functions to handle complex tasks. Token consumption may increase.</value> <value>Use built-in functions to handle complex tasks. Token consumption may increase.</value>
</data> </data>
<data name="AdvancedPaste_Clipboard_History_Enabled_SettingsCard.Header" xml:space="preserve"> <data name="AdvancedPaste_Clipboard_History_Enabled_SettingsCard.Header" xml:space="preserve">
<value>Access Clipboard History</value> <value>Show what's currently on your Clipboard and access your Clipboard history</value>
</data> </data>
<data name="AdvancedPaste_Clipboard_History_Enabled_SettingsCard.Description" xml:space="preserve"> <data name="AdvancedPaste_Clipboard_History_Enabled_SettingsCard.Description" xml:space="preserve">
<value>Clipboard History shows a list of previously copied items.</value> <value>Clipboard History shows a list of previously copied items.</value>
@@ -4058,11 +4055,8 @@ Activate by holding the key for the character you want to add an accent to, then
<data name="AdvancedPaste_ShowCustomPreviewSettingsCard.Description" xml:space="preserve"> <data name="AdvancedPaste_ShowCustomPreviewSettingsCard.Description" xml:space="preserve">
<value>Preview the output of AI formats and Image to text before pasting</value> <value>Preview the output of AI formats and Image to text before pasting</value>
</data> </data>
<data name="AdvancedPaste_EnableAdvancedAI.Header" xml:space="preserve"> <data name="AdvancedPaste_EnableAdvancedAI.Text" xml:space="preserve">
<value>Advanced AI</value> <value>Enable Advanced AI</value>
</data>
<data name="AdvancedPaste_EnableAdvancedAI.Description" xml:space="preserve">
<value>Supports advanced workflows by chaining transformations and working with files and images. May use additional API credits.</value>
</data> </data>
<data name="Oobe_AdvancedPaste.Description" xml:space="preserve"> <data name="Oobe_AdvancedPaste.Description" xml:space="preserve">
<value>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.</value> <value>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.</value>
@@ -4604,9 +4598,13 @@ Activate by holding the key for the character you want to add an accent to, then
<value>If you do not have credits you will see an 'API key quota exceeded' error</value> <value>If you do not have credits you will see an 'API key quota exceeded' error</value>
</data> </data>
<data name="AdvancedPaste_CloseAfterLosingFocus.Content" xml:space="preserve"> <data name="AdvancedPaste_CloseAfterLosingFocus.Content" xml:space="preserve">
<value>Automatically close the Advanced Paste window after it loses focus</value> <value>Automatically close the window after it loses focus</value>
<comment>Advanced Paste is a product name, do not loc</comment> <comment>Advanced Paste is a product name, do not loc</comment>
</data> </data>
<data name="AdvancedPaste_EnableClipboardPreview.Content" xml:space="preserve">
<value>Show clipboard preview</value>
<comment>Enables display of clipboard contents preview in the Advanced Paste window</comment>
</data>
<data name="GPO_CommandNotFound_ForceDisabled.Title" xml:space="preserve"> <data name="GPO_CommandNotFound_ForceDisabled.Title" xml:space="preserve">
<value>The Command Not Found module is disabled by your organization.</value> <value>The Command Not Found module is disabled by your organization.</value>
<comment>"Command Not Found" is a product name</comment> <comment>"Command Not Found" is a product name</comment>
@@ -5679,4 +5677,74 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="LightSwitch_SetLocationButton.Content" xml:space="preserve"> <data name="LightSwitch_SetLocationButton.Content" xml:space="preserve">
<value>Set Location</value> <value>Set Location</value>
</data> </data>
<data name="AdvancedPaste_FL_OpenFoundryModelList.Content" xml:space="preserve">
<value>Open Foundry Local model list</value>
<comment>Do not localize "Foundry Local", it's a product name</comment>
</data>
<data name="AdvancedPaste_FL_RunFoundryLocalText.Text" xml:space="preserve">
<value>Run Foundry Local to download or add a local model</value>
<comment>Do not localize "Foundry Local", it's a product name</comment>
</data>
<data name="AdvancedPaste_FL_NoModelsDownloaded.Text" xml:space="preserve">
<value>No models downloaded</value>
</data>
<data name="AdvancedPaste_FL_LoadingStatus.Text" xml:space="preserve">
<value>Loading Foundry Local status..</value>
<comment>Do not localize "Foundry Local", it's a product name</comment>
</data>
<data name="AdvancedPaste_FL_LocalModel.Text" xml:space="preserve">
<value>Foundry Local model</value>
<comment>Do not localize "Foundry Local", it's a product name</comment>
</data>
<data name="AdvancedPaste_FL_UseCLIToDownloadModels.Text" xml:space="preserve">
<value>Use the Foundry Local CLI to download models that run locally on-device. They'll appear here.</value>
<comment>Do not localize "Foundry Local", it's a product name</comment>
</data>
<data name="AdvancedPaste_FL_RefreshModelList.Text" xml:space="preserve">
<value>Refresh model list</value>
</data>
<data name="AdvancedPaste_FL_FLNotavailableYet.Text" xml:space="preserve">
<value>Foundry Local is not available on this device yet.</value>
<comment>Do not localize "Foundry Local", it's a product name</comment>
</data>
<data name="AdvancedPaste_FL_StartService.Text" xml:space="preserve">
<value>Start the Foundry Local service before returning to PowerToys.</value>
</data>
<data name="AdvancedPaste_FL_CLIGuide.Content" xml:space="preserve">
<value>Follow the Foundry Local CLI guide</value>
<comment>Do not localize "Foundry Local", it's a product name</comment>
</data>
<data name="AdvancedPaste_ModelProviders.Header" xml:space="preserve">
<value>Model providers</value>
</data>
<data name="AdvancedPaste_ModelProviders.Description" xml:space="preserve">
<value>Add online or local models</value>
</data>
<data name="AdvancedPaste_Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="AdvancedPaste_Remove.Text" xml:space="preserve">
<value>Remove</value>
</data>
<data name="AdvancedPaste_ModelName.Header" xml:space="preserve">
<value>Model name</value>
</data>
<data name="AdvancedPaste_EndpointURL.Header" xml:space="preserve">
<value>Endpoint URL</value>
</data>
<data name="AdvancedPaste_APIKey.Header" xml:space="preserve">
<value>API key</value>
</data>
<data name="AdvancedPaste_APIKey.PlaceholderText" xml:space="preserve">
<value>Enter API key</value>
</data>
<data name="AdvancedPaste_APIVersion.Header" xml:space="preserve">
<value>API version</value>
</data>
<data name="AdvancedPaste_DeploymentName.Header" xml:space="preserve">
<value>Deployment name</value>
</data>
<data name="AdvancedPaste_SystemPrompt.Header" xml:space="preserve">
<value>System prompt</value>
</data>
</root> </root>

View File

@@ -172,19 +172,90 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private void MigrateLegacyAIEnablement() private void MigrateLegacyAIEnablement()
{ {
if (_advancedPasteSettings.Properties.IsAIEnabled || IsOnlineAIModelsDisallowedByGPO) var properties = _advancedPasteSettings?.Properties;
if (properties is null)
{ {
return; 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; return;
} }
_advancedPasteSettings.Properties.IsAIEnabled = true; bool configurationUpdated = false;
SaveAndNotifySettings();
OnPropertyChanged(nameof(IsAIEnabled)); 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 public bool IsEnabled
@@ -229,34 +300,30 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO; public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO;
private bool LegacyOpenAIKeyExists() private PasswordCredential TryGetLegacyOpenAICredential()
{ {
try try
{ {
PasswordVault vault = new(); PasswordVault vault = new();
var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
// return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null; credential?.RetrievePassword();
var legacyOpenAIKey = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); return credential;
if (legacyOpenAIKey != null) }
{ catch (Exception)
string credentialResource = GetAICredentialResource("OpenAI"); {
var targetProvider = PasteAIConfiguration?.ActiveProvider ?? PasteAIConfiguration?.Providers?.FirstOrDefault(); return null;
string providerId = targetProvider?.Id ?? string.Empty; }
string serviceType = targetProvider?.ServiceType ?? "OpenAI"; }
string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType);
PasswordCredential cred = new(credentialResource, credentialUserName, legacyOpenAIKey.Password); private void RemoveLegacyOpenAICredential()
vault.Add(cred); {
try
// delete old key {
TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); PasswordVault vault = new();
return true; TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
}
return false;
} }
catch (Exception) catch (Exception)
{ {
return false;
} }
} }
@@ -479,6 +546,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public bool EnableClipboardPreview
{
get => _advancedPasteSettings.Properties.EnableClipboardPreview;
set
{
if (value != _advancedPasteSettings.Properties.EnableClipboardPreview)
{
_advancedPasteSettings.Properties.EnableClipboardPreview = value;
NotifySettingsChanged();
}
}
}
public bool IsConflictingCopyShortcut => public bool IsConflictingCopyShortcut =>
_customActions.Select(customAction => customAction.Shortcut) _customActions.Select(customAction => customAction.Shortcut)
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut]) .Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
@@ -519,7 +599,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var provider = new PasteAIProviderDefinition var provider = new PasteAIProviderDefinition
{ {
ServiceType = persistedServiceType, ServiceType = persistedServiceType,
ModelName = GetDefaultModelName(normalizedServiceType), ModelName = PasteAIProviderDefaults.GetDefaultModelName(normalizedServiceType),
EndpointUrl = string.Empty, EndpointUrl = string.Empty,
ApiVersion = string.Empty, ApiVersion = string.Empty,
DeploymentName = string.Empty, DeploymentName = string.Empty,
@@ -559,20 +639,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return serviceTypeKind; 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) public bool IsServiceTypeAllowedByGPO(AIServiceType serviceType)
{ {
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType); var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
@@ -1151,6 +1217,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(CloseAfterLosingFocus)); OnPropertyChanged(nameof(CloseAfterLosingFocus));
} }
if (target.EnableClipboardPreview != source.EnableClipboardPreview)
{
target.EnableClipboardPreview = source.EnableClipboardPreview;
OnPropertyChanged(nameof(EnableClipboardPreview));
}
var incomingConfig = source.PasteAIConfiguration ?? new PasteAIConfiguration(); var incomingConfig = source.PasteAIConfiguration ?? new PasteAIConfiguration();
if (ShouldReplacePasteAIConfiguration(target.PasteAIConfiguration, incomingConfig)) if (ShouldReplacePasteAIConfiguration(target.PasteAIConfiguration, incomingConfig))
{ {
@@ -1352,7 +1424,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>(); pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
bool configurationUpdated = AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(pasteConfig);
SubscribeToPasteAIProviders(pasteConfig); SubscribeToPasteAIProviders(pasteConfig);
if (configurationUpdated)
{
SaveAndNotifySettings();
OnPropertyChanged(nameof(PasteAIConfiguration));
}
} }
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName) private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)

View File

@@ -145,6 +145,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
IsEnabled = gpo == GpoRuleConfigured.Enabled || (gpo != GpoRuleConfigured.Disabled && ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType)), IsEnabled = gpo == GpoRuleConfigured.Enabled || (gpo != GpoRuleConfigured.Disabled && ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType)),
IsLocked = gpo == GpoRuleConfigured.Enabled || gpo == GpoRuleConfigured.Disabled, IsLocked = gpo == GpoRuleConfigured.Enabled || gpo == GpoRuleConfigured.Disabled,
Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType), Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType),
IsNew = moduleType == ModuleType.CursorWrap,
DashboardModuleItems = GetModuleItems(moduleType), DashboardModuleItems = GetModuleItems(moduleType),
}; };
newItem.EnabledChangedCallback = EnabledChangedOnUI; newItem.EnabledChangedCallback = EnabledChangedOnUI;

View File

@@ -562,7 +562,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
SyncButtonInformation = SelectedCity != null SyncButtonInformation = SelectedCity != null
? SelectedCity.City ? SelectedCity.City
: $"{Latitude},{Longitude}"; : $"{Latitude}°,{Longitude}°";
double lat = double.Parse(ModuleSettings.Properties.Latitude.Value, CultureInfo.InvariantCulture); double lat = double.Parse(ModuleSettings.Properties.Latitude.Value, CultureInfo.InvariantCulture);
double lon = double.Parse(ModuleSettings.Properties.Longitude.Value, CultureInfo.InvariantCulture); double lon = double.Parse(ModuleSettings.Properties.Longitude.Value, CultureInfo.InvariantCulture);

View File

@@ -1000,6 +1000,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GeneralSettingsConfig.Enabled.CursorWrap = value; GeneralSettingsConfig.Enabled.CursorWrap = value;
OnPropertyChanged(nameof(IsCursorWrapEnabled)); 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); OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString()); SendConfigMSG(outgoing.ToString());

View File

@@ -24,6 +24,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public class ZoomItViewModel : Observable public class ZoomItViewModel : Observable
{ {
private const string FormatGif = "GIF";
private const string FormatMp4 = "MP4";
private ISettingsUtils SettingsUtils { get; set; } private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
@@ -656,12 +659,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
get get
{ {
if (_zoomItSettings.Properties.RecordFormat.Value == "GIF") if (_zoomItSettings.Properties.RecordFormat.Value == FormatGif)
{ {
return 0; return 0;
} }
if (_zoomItSettings.Properties.RecordFormat.Value == "MP4") if (_zoomItSettings.Properties.RecordFormat.Value == FormatMp4)
{ {
return 1; return 1;
} }
@@ -672,19 +675,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
set set
{ {
int format = 0; int format = 0;
if (_zoomItSettings.Properties.RecordFormat.Value == "GIF") if (_zoomItSettings.Properties.RecordFormat.Value == FormatGif)
{ {
format = 0; format = 0;
} }
if (_zoomItSettings.Properties.RecordFormat.Value == "MP4") if (_zoomItSettings.Properties.RecordFormat.Value == FormatMp4)
{ {
format = 1; format = 1;
} }
if (format != value) if (format != value)
{ {
_zoomItSettings.Properties.RecordFormat.Value = value == 0 ? "GIF" : "MP4"; _zoomItSettings.Properties.RecordFormat.Value = value == 0 ? FormatGif : FormatMp4;
OnPropertyChanged(nameof(RecordFormatIndex)); OnPropertyChanged(nameof(RecordFormatIndex));
NotifySettingsChanged(); NotifySettingsChanged();