/ src / settings-ui / Settings.UI / ViewModels / AdvancedPasteViewModel.cs
AdvancedPasteViewModel.cs
   1  // Copyright (c) Microsoft Corporation
   2  // The Microsoft Corporation licenses this file to you under the MIT license.
   3  // See the LICENSE file in the project root for more information.
   4  
   5  using System;
   6  using System.Collections.Generic;
   7  using System.Collections.ObjectModel;
   8  using System.Collections.Specialized;
   9  using System.ComponentModel;
  10  using System.Globalization;
  11  using System.IO.Abstractions;
  12  using System.Linq;
  13  using System.Runtime.Versioning;
  14  using System.Text.Json;
  15  
  16  using global::PowerToys.GPOWrapper;
  17  using Microsoft.PowerToys.Settings.UI.Helpers;
  18  using Microsoft.PowerToys.Settings.UI.Library;
  19  using Microsoft.PowerToys.Settings.UI.Library.Helpers;
  20  using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
  21  using Microsoft.PowerToys.Settings.UI.Library.Utilities;
  22  using Microsoft.PowerToys.Settings.UI.SerializationContext;
  23  using Microsoft.UI.Dispatching;
  24  using Microsoft.Win32;
  25  using Windows.Security.Credentials;
  26  
  27  namespace Microsoft.PowerToys.Settings.UI.ViewModels
  28  {
  29      public partial class AdvancedPasteViewModel : PageViewModelBase
  30      {
  31          private static readonly HashSet<string> WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"];
  32  
  33          private bool _disposed;
  34          private PasteAIProviderDefinition _pasteAIProviderDraft;
  35          private PasteAIProviderDefinition _editingPasteAIProvider;
  36  
  37          protected override string ModuleName => AdvancedPasteSettings.ModuleName;
  38  
  39          private GeneralSettings GeneralSettingsConfig { get; set; }
  40  
  41          private readonly SettingsUtils _settingsUtils;
  42  
  43          private readonly AdvancedPasteSettings _advancedPasteSettings;
  44          private readonly AdvancedPasteAdditionalActions _additionalActions;
  45          private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions;
  46          private readonly DispatcherQueue _dispatcherQueue;
  47          private IFileSystemWatcher _settingsWatcher;
  48          private bool _suppressSave;
  49  
  50          private GpoRuleConfigured _enabledGpoRuleConfiguration;
  51          private bool _enabledStateIsGPOConfigured;
  52          private GpoRuleConfigured _onlineAIModelsGpoRuleConfiguration;
  53          private bool _onlineAIModelsDisallowedByGPO;
  54          private bool _isEnabled;
  55  
  56          private Func<string, int> SendConfigMSG { get; }
  57  
  58          private static readonly HashSet<string> CustomActionNonPersistedProperties = new(StringComparer.Ordinal)
  59          {
  60              nameof(AdvancedPasteCustomAction.CanMoveUp),
  61              nameof(AdvancedPasteCustomAction.CanMoveDown),
  62              nameof(AdvancedPasteCustomAction.IsValid),
  63              nameof(AdvancedPasteCustomAction.HasConflict),
  64              nameof(AdvancedPasteCustomAction.Tooltip),
  65              nameof(AdvancedPasteCustomAction.SubActions),
  66          };
  67  
  68          public AdvancedPasteViewModel(
  69              SettingsUtils settingsUtils,
  70              ISettingsRepository<GeneralSettings> settingsRepository,
  71              ISettingsRepository<AdvancedPasteSettings> advancedPasteSettingsRepository,
  72              Func<string, int> ipcMSGCallBackFunc)
  73          {
  74              // To obtain the general settings configurations of PowerToys Settings.
  75              ArgumentNullException.ThrowIfNull(settingsRepository);
  76  
  77              GeneralSettingsConfig = settingsRepository.SettingsConfig;
  78  
  79              // To obtain the settings configurations of Advanced Paste.
  80              ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
  81  
  82              _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
  83  
  84              _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
  85  
  86              _advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig ?? throw new ArgumentException("SettingsConfig cannot be null", nameof(advancedPasteSettingsRepository));
  87  
  88              if (_advancedPasteSettings.Properties is null)
  89              {
  90                  throw new ArgumentException("AdvancedPasteSettings.Properties cannot be null", nameof(advancedPasteSettingsRepository));
  91              }
  92  
  93              // Ensure AdditionalActions and CustomActions are initialized to prevent null reference exceptions
  94              // This handles legacy settings files that may be missing these properties
  95              _advancedPasteSettings.Properties.AdditionalActions ??= new AdvancedPasteAdditionalActions();
  96              _advancedPasteSettings.Properties.CustomActions ??= new AdvancedPasteCustomActions();
  97  
  98              AttachConfigurationHandlers();
  99  
 100              // set the callback functions value to handle outgoing IPC message.
 101              SendConfigMSG = ipcMSGCallBackFunc;
 102  
 103              _additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
 104              _customActions = _advancedPasteSettings.Properties.CustomActions.Value ?? new ObservableCollection<AdvancedPasteCustomAction>();
 105  
 106              SetupSettingsFileWatcher();
 107  
 108              InitializePasteAIProviderState();
 109  
 110              InitializeEnabledValue();
 111              MigrateLegacyAIEnablement();
 112  
 113              foreach (var action in _additionalActions.GetAllActions())
 114              {
 115                  action.PropertyChanged += OnAdditionalActionPropertyChanged;
 116              }
 117  
 118              foreach (var customAction in _customActions)
 119              {
 120                  customAction.PropertyChanged += OnCustomActionPropertyChanged;
 121              }
 122  
 123              _customActions.CollectionChanged += OnCustomActionsCollectionChanged;
 124              UpdateCustomActionsCanMoveUpDown();
 125          }
 126  
 127          public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
 128          {
 129              var hotkeySettings = new List<HotkeySettings>
 130              {
 131                  PasteAsPlainTextShortcut,
 132                  AdvancedPasteUIShortcut,
 133                  PasteAsMarkdownShortcut,
 134                  PasteAsJsonShortcut,
 135              };
 136  
 137              foreach (var action in _additionalActions.GetAllActions())
 138              {
 139                  if (action is AdvancedPasteAdditionalAction additionalAction)
 140                  {
 141                      hotkeySettings.Add(additionalAction.Shortcut);
 142                  }
 143              }
 144  
 145              // Custom actions do not have localization header, just use the action name.
 146              foreach (var customAction in _customActions)
 147              {
 148                  hotkeySettings.Add(customAction.Shortcut);
 149              }
 150  
 151              return new Dictionary<string, HotkeySettings[]>
 152              {
 153                  [ModuleName] = hotkeySettings.ToArray(),
 154              };
 155          }
 156  
 157          private void InitializeEnabledValue()
 158          {
 159              _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAdvancedPasteEnabledValue();
 160              if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
 161              {
 162                  // Get the enabled state from GPO.
 163                  _enabledStateIsGPOConfigured = true;
 164                  _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
 165              }
 166              else
 167              {
 168                  _isEnabled = GeneralSettingsConfig.Enabled.AdvancedPaste;
 169              }
 170  
 171              _onlineAIModelsGpoRuleConfiguration = GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue();
 172              _onlineAIModelsDisallowedByGPO = _onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled;
 173  
 174              if (_onlineAIModelsDisallowedByGPO)
 175              {
 176                  // disable AI if it was enabled
 177                  DisableAI();
 178              }
 179          }
 180  
 181          private void MigrateLegacyAIEnablement()
 182          {
 183              var properties = _advancedPasteSettings?.Properties;
 184              if (properties is null)
 185              {
 186                  return;
 187              }
 188  
 189              bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
 190              bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
 191  
 192              if (IsOnlineAIModelsDisallowedByGPO)
 193              {
 194                  if (legacyAdvancedAIConsumed)
 195                  {
 196                      SaveAndNotifySettings();
 197                  }
 198  
 199                  return;
 200              }
 201  
 202              PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
 203  
 204              if (legacyCredential is null)
 205              {
 206                  if (legacyAdvancedAIConsumed)
 207                  {
 208                      SaveAndNotifySettings();
 209                  }
 210  
 211                  return;
 212              }
 213  
 214              var configuration = properties.PasteAIConfiguration;
 215              if (configuration is null)
 216              {
 217                  configuration = new PasteAIConfiguration();
 218                  properties.PasteAIConfiguration = configuration;
 219              }
 220  
 221              bool configurationUpdated = false;
 222  
 223              var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
 224              PasteAIProviderDefinition openAIProvider = ensureResult.Provider;
 225              configurationUpdated |= ensureResult.Updated;
 226  
 227              if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
 228              {
 229                  openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled;
 230                  configurationUpdated = true;
 231              }
 232  
 233              if (legacyCredential is not null && openAIProvider is not null)
 234              {
 235                  SavePasteAIApiKey(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
 236                  RemoveLegacyOpenAICredential();
 237              }
 238  
 239              const bool shouldEnableAI = true;
 240              bool enabledChanged = false;
 241              if (properties.IsAIEnabled != shouldEnableAI)
 242              {
 243                  properties.IsAIEnabled = shouldEnableAI;
 244                  enabledChanged = true;
 245              }
 246  
 247              bool shouldPersist = configurationUpdated || enabledChanged || legacyAdvancedAIConsumed;
 248  
 249              if (shouldPersist)
 250              {
 251                  SaveAndNotifySettings();
 252  
 253                  if (configurationUpdated)
 254                  {
 255                      OnPropertyChanged(nameof(PasteAIConfiguration));
 256                  }
 257  
 258                  if (enabledChanged)
 259                  {
 260                      OnPropertyChanged(nameof(IsAIEnabled));
 261                  }
 262              }
 263          }
 264  
 265          public bool IsEnabled
 266          {
 267              get => _isEnabled;
 268              set
 269              {
 270                  if (_enabledStateIsGPOConfigured)
 271                  {
 272                      // If it's GPO configured, shouldn't be able to change this state.
 273                      return;
 274                  }
 275  
 276                  if (_isEnabled != value)
 277                  {
 278                      _isEnabled = value;
 279                      OnPropertyChanged(nameof(IsEnabled));
 280                      OnPropertyChanged(nameof(ShowOnlineAIModelsGpoConfiguredInfoBar));
 281                      OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar));
 282  
 283                      // Set the status of AdvancedPaste in the general settings
 284                      GeneralSettingsConfig.Enabled.AdvancedPaste = value;
 285                      var outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
 286  
 287                      SendConfigMSG(outgoing.ToString());
 288                  }
 289              }
 290          }
 291  
 292          public ObservableCollection<AdvancedPasteCustomAction> CustomActions => _customActions;
 293  
 294          public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
 295  
 296          public static IEnumerable<AIServiceTypeMetadata> AvailableProviders => AIServiceTypeRegistry.GetAvailableServiceTypes();
 297  
 298          /// <summary>
 299          /// Gets available AI providers filtered by GPO policies.
 300          /// Only returns providers that are not explicitly disabled by GPO.
 301          /// </summary>
 302          public IEnumerable<AIServiceTypeMetadata> AvailableProvidersFilteredByGPO =>
 303              AvailableProviders.Where(metadata => IsServiceTypeAllowedByGPO(metadata.ServiceType));
 304  
 305          public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO;
 306  
 307          private PasswordCredential TryGetLegacyOpenAICredential()
 308          {
 309              try
 310              {
 311                  PasswordVault vault = new();
 312                  var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
 313                  credential?.RetrievePassword();
 314                  return credential;
 315              }
 316              catch (Exception)
 317              {
 318                  return null;
 319              }
 320          }
 321  
 322          private void RemoveLegacyOpenAICredential()
 323          {
 324              try
 325              {
 326                  PasswordVault vault = new();
 327                  TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
 328              }
 329              catch (Exception)
 330              {
 331              }
 332          }
 333  
 334          public bool IsEnabledGpoConfigured
 335          {
 336              get => _enabledStateIsGPOConfigured;
 337          }
 338  
 339          public bool IsOnlineAIModelsDisallowedByGPO
 340          {
 341              get => _onlineAIModelsDisallowedByGPO || _enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled;
 342          }
 343  
 344          public bool ShowOnlineAIModelsGpoConfiguredInfoBar
 345          {
 346              get => _onlineAIModelsDisallowedByGPO && _isEnabled;
 347          }
 348  
 349          private bool IsClipboardHistoryEnabled()
 350          {
 351              string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\";
 352              try
 353              {
 354                  int enableClipboardHistory = (int)Registry.GetValue(registryKey, "EnableClipboardHistory", false);
 355                  return enableClipboardHistory != 0;
 356              }
 357              catch (Exception)
 358              {
 359                  return false;
 360              }
 361          }
 362  
 363          private bool IsClipboardHistoryDisabledByGPO()
 364          {
 365              string registryKey = @"HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System\";
 366              try
 367              {
 368                  object allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null);
 369                  if (allowClipboardHistory != null)
 370                  {
 371                      return (int)allowClipboardHistory == 0;
 372                  }
 373                  else
 374                  {
 375                      return false;
 376                  }
 377              }
 378              catch (Exception)
 379              {
 380                  return false;
 381              }
 382          }
 383  
 384          private void SetClipboardHistoryEnabled(bool value)
 385          {
 386              string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\";
 387              try
 388              {
 389                  Registry.SetValue(registryKey, "EnableClipboardHistory", value ? 1 : 0);
 390              }
 391              catch (Exception)
 392              {
 393              }
 394          }
 395  
 396          public bool ClipboardHistoryEnabled
 397          {
 398              get => IsClipboardHistoryEnabled();
 399              set
 400              {
 401                  if (IsClipboardHistoryEnabled() != value)
 402                  {
 403                      SetClipboardHistoryEnabled(value);
 404                  }
 405              }
 406          }
 407  
 408          public bool ClipboardHistoryDisabledByGPO
 409          {
 410              get => IsClipboardHistoryDisabledByGPO();
 411          }
 412  
 413          public bool ShowClipboardHistoryIsGpoConfiguredInfoBar
 414          {
 415              get => IsClipboardHistoryDisabledByGPO() && _isEnabled;
 416          }
 417  
 418          public HotkeySettings AdvancedPasteUIShortcut
 419          {
 420              get => _advancedPasteSettings.Properties.AdvancedPasteUIShortcut;
 421              set
 422              {
 423                  if (_advancedPasteSettings.Properties.AdvancedPasteUIShortcut != value)
 424                  {
 425                      _advancedPasteSettings.Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut;
 426                      OnPropertyChanged(nameof(IsConflictingCopyShortcut));
 427                      OnPropertyChanged(nameof(AdvancedPasteUIShortcut));
 428                      SaveAndNotifySettings();
 429                  }
 430              }
 431          }
 432  
 433          public HotkeySettings PasteAsPlainTextShortcut
 434          {
 435              get => _advancedPasteSettings.Properties.PasteAsPlainTextShortcut;
 436              set
 437              {
 438                  if (_advancedPasteSettings.Properties.PasteAsPlainTextShortcut != value)
 439                  {
 440                      _advancedPasteSettings.Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut;
 441                      OnPropertyChanged(nameof(IsConflictingCopyShortcut));
 442                      OnPropertyChanged(nameof(PasteAsPlainTextShortcut));
 443                      SaveAndNotifySettings();
 444                  }
 445              }
 446          }
 447  
 448          public HotkeySettings PasteAsMarkdownShortcut
 449          {
 450              get => _advancedPasteSettings.Properties.PasteAsMarkdownShortcut;
 451              set
 452              {
 453                  if (_advancedPasteSettings.Properties.PasteAsMarkdownShortcut != value)
 454                  {
 455                      _advancedPasteSettings.Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings();
 456                      OnPropertyChanged(nameof(IsConflictingCopyShortcut));
 457                      OnPropertyChanged(nameof(PasteAsMarkdownShortcut));
 458                      SaveAndNotifySettings();
 459                  }
 460              }
 461          }
 462  
 463          public HotkeySettings PasteAsJsonShortcut
 464          {
 465              get => _advancedPasteSettings.Properties.PasteAsJsonShortcut;
 466              set
 467              {
 468                  if (_advancedPasteSettings.Properties.PasteAsJsonShortcut != value)
 469                  {
 470                      _advancedPasteSettings.Properties.PasteAsJsonShortcut = value ?? new HotkeySettings();
 471                      OnPropertyChanged(nameof(IsConflictingCopyShortcut));
 472                      OnPropertyChanged(nameof(PasteAsJsonShortcut));
 473                      SaveAndNotifySettings();
 474                  }
 475              }
 476          }
 477  
 478          public PasteAIConfiguration PasteAIConfiguration
 479          {
 480              get
 481              {
 482                  // Ensure PasteAIConfiguration is never null for XAML binding
 483                  _advancedPasteSettings.Properties.PasteAIConfiguration ??= new PasteAIConfiguration();
 484                  return _advancedPasteSettings.Properties.PasteAIConfiguration;
 485              }
 486  
 487              set
 488              {
 489                  if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))
 490                  {
 491                      UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration);
 492  
 493                      var newValue = value ?? new PasteAIConfiguration();
 494                      _advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
 495                      SubscribeToPasteAIConfiguration(newValue);
 496  
 497                      OnPropertyChanged(nameof(PasteAIConfiguration));
 498                      SaveAndNotifySettings();
 499                  }
 500              }
 501          }
 502  
 503          public PasteAIProviderDefinition PasteAIProviderDraft
 504          {
 505              get => _pasteAIProviderDraft;
 506              private set
 507              {
 508                  if (!ReferenceEquals(_pasteAIProviderDraft, value))
 509                  {
 510                      _pasteAIProviderDraft = value;
 511                      OnPropertyChanged(nameof(PasteAIProviderDraft));
 512                      OnPropertyChanged(nameof(ShowPasteAIProviderGpoConfiguredInfoBar));
 513                  }
 514              }
 515          }
 516  
 517          public bool ShowPasteAIProviderGpoConfiguredInfoBar
 518          {
 519              get
 520              {
 521                  if (_pasteAIProviderDraft is null)
 522                  {
 523                      return false;
 524                  }
 525  
 526                  var serviceType = _pasteAIProviderDraft.ServiceType.ToAIServiceType();
 527                  return !IsServiceTypeAllowedByGPO(serviceType);
 528              }
 529          }
 530  
 531          public bool IsEditingPasteAIProvider => _editingPasteAIProvider is not null;
 532  
 533          public bool ShowCustomPreview
 534          {
 535              get => _advancedPasteSettings.Properties.ShowCustomPreview;
 536              set
 537              {
 538                  if (value != _advancedPasteSettings.Properties.ShowCustomPreview)
 539                  {
 540                      _advancedPasteSettings.Properties.ShowCustomPreview = value;
 541                      NotifySettingsChanged();
 542                  }
 543              }
 544          }
 545  
 546          public bool CloseAfterLosingFocus
 547          {
 548              get => _advancedPasteSettings.Properties.CloseAfterLosingFocus;
 549              set
 550              {
 551                  if (value != _advancedPasteSettings.Properties.CloseAfterLosingFocus)
 552                  {
 553                      _advancedPasteSettings.Properties.CloseAfterLosingFocus = value;
 554                      NotifySettingsChanged();
 555                  }
 556              }
 557          }
 558  
 559          public bool EnableClipboardPreview
 560          {
 561              get => _advancedPasteSettings.Properties.EnableClipboardPreview;
 562              set
 563              {
 564                  if (value != _advancedPasteSettings.Properties.EnableClipboardPreview)
 565                  {
 566                      _advancedPasteSettings.Properties.EnableClipboardPreview = value;
 567                      NotifySettingsChanged();
 568                  }
 569              }
 570          }
 571  
 572          public bool AutoCopySelectionForCustomActionHotkey
 573          {
 574              get => _advancedPasteSettings.Properties.AutoCopySelectionForCustomActionHotkey;
 575              set
 576              {
 577                  if (value != _advancedPasteSettings.Properties.AutoCopySelectionForCustomActionHotkey)
 578                  {
 579                      _advancedPasteSettings.Properties.AutoCopySelectionForCustomActionHotkey = value;
 580                      NotifySettingsChanged();
 581                  }
 582              }
 583          }
 584  
 585          public bool IsConflictingCopyShortcut =>
 586              _customActions.Select(customAction => customAction.Shortcut)
 587                            .Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
 588                            .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
 589  
 590          public bool IsAdditionalActionConflictingCopyShortcut =>
 591              _additionalActions.GetAllActions()
 592                                .OfType<AdvancedPasteAdditionalAction>()
 593                                .Select(additionalAction => additionalAction.Shortcut)
 594                                .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
 595  
 596          private void NotifySettingsChanged()
 597          {
 598              // Using InvariantCulture as this is an IPC message
 599              SendConfigMSG(
 600                  string.Format(
 601                      CultureInfo.InvariantCulture,
 602                      "{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
 603                      AdvancedPasteSettings.ModuleName,
 604                      JsonSerializer.Serialize(_advancedPasteSettings, SourceGenerationContextContext.Default.AdvancedPasteSettings)));
 605          }
 606  
 607          public void RefreshEnabledState()
 608          {
 609              InitializeEnabledValue();
 610              MigrateLegacyAIEnablement();
 611              OnPropertyChanged(nameof(IsEnabled));
 612              OnPropertyChanged(nameof(ShowOnlineAIModelsGpoConfiguredInfoBar));
 613              OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar));
 614              OnPropertyChanged(nameof(IsAIEnabled));
 615          }
 616  
 617          public void BeginAddPasteAIProvider(string serviceType)
 618          {
 619              var normalizedServiceType = NormalizeServiceType(serviceType, out var persistedServiceType);
 620  
 621              var metadata = AIServiceTypeRegistry.GetMetadata(normalizedServiceType);
 622              var provider = new PasteAIProviderDefinition
 623              {
 624                  ServiceType = persistedServiceType,
 625                  ModelName = PasteAIProviderDefaults.GetDefaultModelName(normalizedServiceType),
 626                  EndpointUrl = string.Empty,
 627                  ApiVersion = string.Empty,
 628                  DeploymentName = string.Empty,
 629                  ModelPath = string.Empty,
 630                  SystemPrompt = string.Empty,
 631                  ModerationEnabled = normalizedServiceType == AIServiceType.OpenAI,
 632                  IsLocalModel = metadata.IsLocalModel,
 633              };
 634  
 635              if (normalizedServiceType is AIServiceType.FoundryLocal or AIServiceType.Onnx or AIServiceType.ML)
 636              {
 637                  provider.ModelName = string.Empty;
 638              }
 639  
 640              _editingPasteAIProvider = null;
 641              PasteAIProviderDraft = provider;
 642          }
 643  
 644          private static AIServiceType NormalizeServiceType(string serviceType, out string persistedServiceType)
 645          {
 646              if (string.IsNullOrWhiteSpace(serviceType))
 647              {
 648                  persistedServiceType = AIServiceType.OpenAI.ToConfigurationString();
 649                  return AIServiceType.OpenAI;
 650              }
 651  
 652              var trimmed = serviceType.Trim();
 653              var serviceTypeKind = trimmed.ToAIServiceType();
 654  
 655              if (serviceTypeKind == AIServiceType.Unknown)
 656              {
 657                  persistedServiceType = AIServiceType.OpenAI.ToConfigurationString();
 658                  return AIServiceType.OpenAI;
 659              }
 660  
 661              persistedServiceType = trimmed;
 662              return serviceTypeKind;
 663          }
 664  
 665          public bool IsServiceTypeAllowedByGPO(AIServiceType serviceType)
 666          {
 667              var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
 668  
 669              // Check if this is an online service
 670              if (metadata.IsOnlineService)
 671              {
 672                  // For online services, first check the global online AI models GPO
 673                  if (_onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled)
 674                  {
 675                      // If global online AI is disabled, all online services are blocked
 676                      return false;
 677                  }
 678  
 679                  // If global online AI is enabled or not configured, check individual endpoint GPO
 680                  var individualGpoRule = serviceType switch
 681                  {
 682                      AIServiceType.OpenAI => GPOWrapper.GetAllowedAdvancedPasteOpenAIValue(),
 683                      AIServiceType.AzureOpenAI => GPOWrapper.GetAllowedAdvancedPasteAzureOpenAIValue(),
 684                      AIServiceType.AzureAIInference => GPOWrapper.GetAllowedAdvancedPasteAzureAIInferenceValue(),
 685                      AIServiceType.Mistral => GPOWrapper.GetAllowedAdvancedPasteMistralValue(),
 686                      AIServiceType.Google => GPOWrapper.GetAllowedAdvancedPasteGoogleValue(),
 687                      _ => GpoRuleConfigured.Unavailable,
 688                  };
 689  
 690                  // If individual GPO is explicitly disabled, block it
 691                  return individualGpoRule != GpoRuleConfigured.Disabled;
 692              }
 693              else
 694              {
 695                  // For local models, only check their individual GPO (not affected by online AI GPO)
 696                  var localGpoRule = serviceType switch
 697                  {
 698                      AIServiceType.Ollama => GPOWrapper.GetAllowedAdvancedPasteOllamaValue(),
 699                      AIServiceType.FoundryLocal => GPOWrapper.GetAllowedAdvancedPasteFoundryLocalValue(),
 700                      _ => GpoRuleConfigured.Unavailable,
 701                  };
 702  
 703                  // If local model GPO is explicitly disabled, block it
 704                  return localGpoRule != GpoRuleConfigured.Disabled;
 705              }
 706          }
 707  
 708          public void BeginEditPasteAIProvider(PasteAIProviderDefinition provider)
 709          {
 710              ArgumentNullException.ThrowIfNull(provider);
 711  
 712              _editingPasteAIProvider = provider;
 713              var draft = provider.Clone();
 714              var storedEndpoint = GetPasteAIEndpoint(draft.Id, draft.ServiceType);
 715              if (!string.IsNullOrWhiteSpace(storedEndpoint))
 716              {
 717                  draft.EndpointUrl = storedEndpoint;
 718              }
 719  
 720              PasteAIProviderDraft = draft;
 721          }
 722  
 723          public void CancelPasteAIProviderDraft()
 724          {
 725              PasteAIProviderDraft = null;
 726              _editingPasteAIProvider = null;
 727          }
 728  
 729          public void CommitPasteAIProviderDraft(string apiKey, string endpoint)
 730          {
 731              if (PasteAIProviderDraft is null)
 732              {
 733                  return;
 734              }
 735  
 736              var config = PasteAIConfiguration ?? new PasteAIConfiguration();
 737              if (_advancedPasteSettings.Properties.PasteAIConfiguration is null)
 738              {
 739                  PasteAIConfiguration = config;
 740              }
 741  
 742              var draft = PasteAIProviderDraft;
 743              draft.EndpointUrl = endpoint?.Trim() ?? string.Empty;
 744  
 745              SavePasteAIApiKey(draft.Id, draft.ServiceType, apiKey);
 746  
 747              if (_editingPasteAIProvider is null)
 748              {
 749                  config.Providers.Add(draft);
 750                  config.ActiveProviderId ??= draft.Id;
 751              }
 752              else
 753              {
 754                  UpdateProviderFromDraft(_editingPasteAIProvider, draft);
 755                  _editingPasteAIProvider = null;
 756              }
 757  
 758              PasteAIProviderDraft = null;
 759              SaveAndNotifySettings();
 760              OnPropertyChanged(nameof(PasteAIConfiguration));
 761          }
 762  
 763          public void RemovePasteAIProvider(PasteAIProviderDefinition provider)
 764          {
 765              if (provider is null)
 766              {
 767                  return;
 768              }
 769  
 770              var config = PasteAIConfiguration;
 771              if (config?.Providers is null)
 772              {
 773                  return;
 774              }
 775  
 776              if (config.Providers.Remove(provider))
 777              {
 778                  RemovePasteAICredentials(provider.Id, provider.ServiceType);
 779                  SaveAndNotifySettings();
 780                  OnPropertyChanged(nameof(PasteAIConfiguration));
 781              }
 782          }
 783  
 784          protected override void Dispose(bool disposing)
 785          {
 786              if (!_disposed)
 787              {
 788                  if (disposing)
 789                  {
 790                      UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings?.Properties.PasteAIConfiguration);
 791  
 792                      foreach (var action in _additionalActions.GetAllActions())
 793                      {
 794                          action.PropertyChanged -= OnAdditionalActionPropertyChanged;
 795                      }
 796  
 797                      foreach (var customAction in _customActions)
 798                      {
 799                          customAction.PropertyChanged -= OnCustomActionPropertyChanged;
 800                      }
 801  
 802                      _customActions.CollectionChanged -= OnCustomActionsCollectionChanged;
 803                      _settingsWatcher?.Dispose();
 804                      _settingsWatcher = null;
 805                  }
 806  
 807                  _disposed = true;
 808              }
 809  
 810              base.Dispose(disposing);
 811          }
 812  
 813          internal void DisableAI()
 814          {
 815              try
 816              {
 817                  bool stateChanged = false;
 818  
 819                  if (_advancedPasteSettings.Properties.IsAIEnabled)
 820                  {
 821                      _advancedPasteSettings.Properties.IsAIEnabled = false;
 822                      stateChanged = true;
 823                  }
 824  
 825                  if (stateChanged)
 826                  {
 827                      SaveAndNotifySettings();
 828                  }
 829                  else
 830                  {
 831                      NotifySettingsChanged();
 832                  }
 833  
 834                  OnPropertyChanged(nameof(IsAIEnabled));
 835              }
 836              catch (Exception)
 837              {
 838              }
 839          }
 840  
 841          internal void EnableAI()
 842          {
 843              try
 844              {
 845                  if (IsOnlineAIModelsDisallowedByGPO)
 846                  {
 847                      return;
 848                  }
 849  
 850                  bool stateChanged = false;
 851  
 852                  if (!_advancedPasteSettings.Properties.IsAIEnabled)
 853                  {
 854                      _advancedPasteSettings.Properties.IsAIEnabled = true;
 855                      stateChanged = true;
 856                  }
 857  
 858                  if (stateChanged)
 859                  {
 860                      SaveAndNotifySettings();
 861                  }
 862                  else
 863                  {
 864                      NotifySettingsChanged();
 865                  }
 866  
 867                  OnPropertyChanged(nameof(IsAIEnabled));
 868              }
 869              catch (Exception)
 870              {
 871              }
 872          }
 873  
 874          internal void SavePasteAIApiKey(string providerId, string serviceType, string apiKey)
 875          {
 876              try
 877              {
 878                  apiKey = apiKey?.Trim() ?? string.Empty;
 879                  serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
 880                  providerId ??= string.Empty;
 881  
 882                  string credentialResource = GetAICredentialResource(serviceType);
 883                  string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType);
 884                  string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(providerId, serviceType);
 885                  PasswordVault vault = new();
 886                  TryRemoveCredential(vault, credentialResource, credentialUserName);
 887                  TryRemoveCredential(vault, credentialResource, endpointCredentialUserName);
 888  
 889                  bool storeApiKey = RequiresCredentialStorage(serviceType) && !string.IsNullOrWhiteSpace(apiKey);
 890                  if (storeApiKey)
 891                  {
 892                      PasswordCredential cred = new(credentialResource, credentialUserName, apiKey);
 893                      vault.Add(cred);
 894                  }
 895  
 896                  OnPropertyChanged(nameof(IsAIEnabled));
 897                  NotifySettingsChanged();
 898              }
 899              catch (Exception)
 900              {
 901              }
 902          }
 903  
 904          internal string GetPasteAIApiKey(string providerId, string serviceType)
 905          {
 906              serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
 907              providerId ??= string.Empty;
 908              return RetrieveCredentialValue(
 909                  GetAICredentialResource(serviceType),
 910                  GetPasteAICredentialUserName(providerId, serviceType));
 911          }
 912  
 913          internal string GetPasteAIEndpoint(string providerId, string serviceType)
 914          {
 915              providerId ??= string.Empty;
 916              var providers = PasteAIConfiguration?.Providers;
 917              if (providers is null)
 918              {
 919                  return string.Empty;
 920              }
 921  
 922              var provider = providers.FirstOrDefault(p => string.Equals(p.Id ?? string.Empty, providerId, StringComparison.OrdinalIgnoreCase));
 923              if (provider is null && !string.IsNullOrWhiteSpace(serviceType))
 924              {
 925                  provider = providers.FirstOrDefault(p => string.Equals(p.ServiceType, serviceType, StringComparison.OrdinalIgnoreCase));
 926              }
 927  
 928              return provider?.EndpointUrl?.Trim() ?? string.Empty;
 929          }
 930  
 931          private string GetAICredentialResource(string serviceType)
 932          {
 933              serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
 934              return serviceType.ToLowerInvariant() switch
 935              {
 936                  "openai" => "https://platform.openai.com/api-keys",
 937                  "azureopenai" => "https://azure.microsoft.com/products/ai-services/openai-service",
 938                  "azureaiinference" => "https://azure.microsoft.com/products/ai-services/ai-inference",
 939                  "mistral" => "https://console.mistral.ai/account/api-keys",
 940                  "google" => "https://ai.google.dev/",
 941                  "ollama" => "https://ollama.com/",
 942                  _ => "https://platform.openai.com/api-keys",
 943              };
 944          }
 945  
 946          private string GetPasteAICredentialUserName(string providerId, string serviceType)
 947          {
 948              serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
 949              providerId ??= string.Empty;
 950  
 951              string service = serviceType.ToLowerInvariant();
 952              string normalizedId = NormalizeProviderIdentifier(providerId);
 953  
 954              return $"PowerToys_AdvancedPaste_PasteAI_{service}_{normalizedId}";
 955          }
 956  
 957          private string GetPasteAIEndpointCredentialUserName(string providerId, string serviceType)
 958          {
 959              return GetPasteAICredentialUserName(providerId, serviceType) + "_Endpoint";
 960          }
 961  
 962          private static void UpdateProviderFromDraft(PasteAIProviderDefinition target, PasteAIProviderDefinition source)
 963          {
 964              if (target is null || source is null)
 965              {
 966                  return;
 967              }
 968  
 969              target.ServiceType = source.ServiceType;
 970              target.ModelName = source.ModelName;
 971              target.EndpointUrl = source.EndpointUrl;
 972              target.ApiVersion = source.ApiVersion;
 973              target.DeploymentName = source.DeploymentName;
 974              target.ModelPath = source.ModelPath;
 975              target.SystemPrompt = source.SystemPrompt;
 976              target.ModerationEnabled = source.ModerationEnabled;
 977              target.EnableAdvancedAI = source.EnableAdvancedAI;
 978              target.IsLocalModel = source.IsLocalModel;
 979          }
 980  
 981          private void RemovePasteAICredentials(string providerId, string serviceType)
 982          {
 983              try
 984              {
 985                  serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
 986                  providerId ??= string.Empty;
 987  
 988                  string credentialResource = GetAICredentialResource(serviceType);
 989                  PasswordVault vault = new();
 990                  TryRemoveCredential(vault, credentialResource, GetPasteAICredentialUserName(providerId, serviceType));
 991                  TryRemoveCredential(vault, credentialResource, GetPasteAIEndpointCredentialUserName(providerId, serviceType));
 992              }
 993              catch (Exception)
 994              {
 995              }
 996          }
 997  
 998          private static string NormalizeProviderIdentifier(string providerId)
 999          {
1000              if (string.IsNullOrWhiteSpace(providerId))
1001              {
1002                  return "default";
1003              }
1004  
1005              var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray());
1006              return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
1007          }
1008  
1009          private static bool RequiresCredentialStorage(string serviceType)
1010          {
1011              var serviceTypeKind = serviceType.ToAIServiceType();
1012  
1013              return serviceTypeKind switch
1014              {
1015                  AIServiceType.Onnx => false,
1016                  AIServiceType.Ollama => false,
1017                  AIServiceType.FoundryLocal => false,
1018                  AIServiceType.ML => false,
1019                  _ => true,
1020              };
1021          }
1022  
1023          private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
1024          {
1025              try
1026              {
1027                  PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
1028                  vault.Remove(existingCred);
1029              }
1030              catch (Exception)
1031              {
1032                  // Credential doesn't exist, which is fine
1033              }
1034          }
1035  
1036          internal AdvancedPasteCustomAction GetNewCustomAction(string namePrefix)
1037          {
1038              ArgumentException.ThrowIfNullOrEmpty(namePrefix);
1039  
1040              var maxUsedPrefix = _customActions.Select(customAction => customAction.Name)
1041                                    .Where(name => name.StartsWith(namePrefix, StringComparison.InvariantCulture))
1042                                    .Select(name => int.TryParse(name.AsSpan(namePrefix.Length), out int number) ? number : 0)
1043                                    .DefaultIfEmpty(0)
1044                                    .Max();
1045  
1046              var maxUsedId = _customActions.Select(customAction => customAction.Id)
1047                                            .DefaultIfEmpty(-1)
1048                                            .Max();
1049              return new()
1050              {
1051                  Id = maxUsedId + 1,
1052                  Name = $"{namePrefix} {maxUsedPrefix + 1}",
1053                  IsShown = true,
1054              };
1055          }
1056  
1057          internal void AddCustomAction(AdvancedPasteCustomAction customAction)
1058          {
1059              if (_customActions.Any(existingCustomAction => existingCustomAction.Id == customAction.Id))
1060              {
1061                  throw new ArgumentException("Duplicate custom action", nameof(customAction));
1062              }
1063  
1064              _customActions.Add(customAction);
1065          }
1066  
1067          internal void DeleteCustomAction(AdvancedPasteCustomAction customAction) => _customActions.Remove(customAction);
1068  
1069          private void SaveCustomActions() => SaveAndNotifySettings();
1070  
1071          private void SaveAndNotifySettings()
1072          {
1073              if (_suppressSave)
1074              {
1075                  return;
1076              }
1077  
1078              _settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName);
1079              NotifySettingsChanged();
1080          }
1081  
1082          private void OnAdditionalActionPropertyChanged(object sender, PropertyChangedEventArgs e)
1083          {
1084              SaveAndNotifySettings();
1085  
1086              if (e.PropertyName == nameof(AdvancedPasteAdditionalAction.Shortcut))
1087              {
1088                  OnPropertyChanged(nameof(IsAdditionalActionConflictingCopyShortcut));
1089              }
1090          }
1091  
1092          private void OnCustomActionPropertyChanged(object sender, PropertyChangedEventArgs e)
1093          {
1094              if (!string.IsNullOrEmpty(e.PropertyName) && !CustomActionNonPersistedProperties.Contains(e.PropertyName))
1095              {
1096                  SaveCustomActions();
1097              }
1098  
1099              if (e.PropertyName == nameof(AdvancedPasteCustomAction.Shortcut))
1100              {
1101                  OnPropertyChanged(nameof(IsConflictingCopyShortcut));
1102              }
1103          }
1104  
1105          private void OnCustomActionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
1106          {
1107              void AddRange(System.Collections.IList items)
1108              {
1109                  foreach (AdvancedPasteCustomAction item in items)
1110                  {
1111                      item.PropertyChanged += OnCustomActionPropertyChanged;
1112                  }
1113              }
1114  
1115              void RemoveRange(System.Collections.IList items)
1116              {
1117                  foreach (AdvancedPasteCustomAction item in items)
1118                  {
1119                      item.PropertyChanged -= OnCustomActionPropertyChanged;
1120                  }
1121              }
1122  
1123              switch (e.Action)
1124              {
1125                  case NotifyCollectionChangedAction.Add:
1126                      AddRange(e.NewItems);
1127                      break;
1128  
1129                  case NotifyCollectionChangedAction.Remove:
1130                      RemoveRange(e.OldItems);
1131                      break;
1132  
1133                  case NotifyCollectionChangedAction.Replace:
1134                      AddRange(e.NewItems);
1135                      RemoveRange(e.OldItems);
1136                      break;
1137  
1138                  case NotifyCollectionChangedAction.Move:
1139                      break;
1140  
1141                  default:
1142                      throw new ArgumentException($"Unsupported {nameof(e.Action)} {e.Action}", nameof(e));
1143              }
1144  
1145              OnPropertyChanged(nameof(IsConflictingCopyShortcut));
1146              UpdateCustomActionsCanMoveUpDown();
1147              SaveCustomActions();
1148          }
1149  
1150          private void AttachConfigurationHandlers()
1151          {
1152              SubscribeToPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration);
1153          }
1154  
1155          private void SetupSettingsFileWatcher()
1156          {
1157              _settingsWatcher = Helper.GetFileWatcher(AdvancedPasteSettings.ModuleName, SettingsUtils.DefaultFileName, OnSettingsFileChanged);
1158          }
1159  
1160          private void OnSettingsFileChanged()
1161          {
1162              if (_disposed)
1163              {
1164                  return;
1165              }
1166  
1167              void Handler()
1168              {
1169                  ApplyExternalSettings();
1170              }
1171  
1172              if (_dispatcherQueue is not null && !_dispatcherQueue.HasThreadAccess)
1173              {
1174                  _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, Handler);
1175              }
1176              else
1177              {
1178                  Handler();
1179              }
1180          }
1181  
1182          private void ApplyExternalSettings()
1183          {
1184              if (_disposed)
1185              {
1186                  return;
1187              }
1188  
1189              AdvancedPasteSettings latestSettings;
1190  
1191              try
1192              {
1193                  latestSettings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteSettings.ModuleName);
1194              }
1195              catch
1196              {
1197                  return;
1198              }
1199  
1200              if (latestSettings?.Properties is null)
1201              {
1202                  return;
1203              }
1204  
1205              try
1206              {
1207                  _suppressSave = true;
1208                  ApplyExternalProperties(latestSettings.Properties);
1209              }
1210              finally
1211              {
1212                  _suppressSave = false;
1213              }
1214          }
1215  
1216          private void ApplyExternalProperties(AdvancedPasteProperties source)
1217          {
1218              var target = _advancedPasteSettings?.Properties;
1219  
1220              if (target is null || source is null)
1221              {
1222                  return;
1223              }
1224  
1225              if (target.IsAIEnabled != source.IsAIEnabled)
1226              {
1227                  target.IsAIEnabled = source.IsAIEnabled;
1228                  OnPropertyChanged(nameof(IsAIEnabled));
1229              }
1230  
1231              if (target.ShowCustomPreview != source.ShowCustomPreview)
1232              {
1233                  target.ShowCustomPreview = source.ShowCustomPreview;
1234                  OnPropertyChanged(nameof(ShowCustomPreview));
1235              }
1236  
1237              if (target.CloseAfterLosingFocus != source.CloseAfterLosingFocus)
1238              {
1239                  target.CloseAfterLosingFocus = source.CloseAfterLosingFocus;
1240                  OnPropertyChanged(nameof(CloseAfterLosingFocus));
1241              }
1242  
1243              if (target.EnableClipboardPreview != source.EnableClipboardPreview)
1244              {
1245                  target.EnableClipboardPreview = source.EnableClipboardPreview;
1246                  OnPropertyChanged(nameof(EnableClipboardPreview));
1247              }
1248  
1249              if (target.AutoCopySelectionForCustomActionHotkey != source.AutoCopySelectionForCustomActionHotkey)
1250              {
1251                  target.AutoCopySelectionForCustomActionHotkey = source.AutoCopySelectionForCustomActionHotkey;
1252                  OnPropertyChanged(nameof(AutoCopySelectionForCustomActionHotkey));
1253              }
1254  
1255              var incomingConfig = source.PasteAIConfiguration ?? new PasteAIConfiguration();
1256              if (ShouldReplacePasteAIConfiguration(target.PasteAIConfiguration, incomingConfig))
1257              {
1258                  PasteAIConfiguration = incomingConfig;
1259              }
1260          }
1261  
1262          private static bool ShouldReplacePasteAIConfiguration(PasteAIConfiguration current, PasteAIConfiguration incoming)
1263          {
1264              if (incoming is null)
1265              {
1266                  return false;
1267              }
1268  
1269              if (current is null)
1270              {
1271                  return true;
1272              }
1273  
1274              if (!string.Equals(current.ActiveProviderId ?? string.Empty, incoming.ActiveProviderId ?? string.Empty, StringComparison.OrdinalIgnoreCase))
1275              {
1276                  return true;
1277              }
1278  
1279              var currentProviders = current.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
1280              var incomingProviders = incoming.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
1281  
1282              if (currentProviders.Count != incomingProviders.Count)
1283              {
1284                  return true;
1285              }
1286  
1287              for (int i = 0; i < currentProviders.Count; i++)
1288              {
1289                  var existing = currentProviders[i];
1290                  var updated = incomingProviders[i];
1291  
1292                  if (!string.Equals(existing?.Id ?? string.Empty, updated?.Id ?? string.Empty, StringComparison.OrdinalIgnoreCase))
1293                  {
1294                      return true;
1295                  }
1296  
1297                  if (!string.Equals(existing?.ServiceType ?? string.Empty, updated?.ServiceType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
1298                  {
1299                      return true;
1300                  }
1301  
1302                  if (!string.Equals(existing?.ModelName ?? string.Empty, updated?.ModelName ?? string.Empty, StringComparison.Ordinal))
1303                  {
1304                      return true;
1305                  }
1306  
1307                  if (!string.Equals(existing?.EndpointUrl ?? string.Empty, updated?.EndpointUrl ?? string.Empty, StringComparison.Ordinal))
1308                  {
1309                      return true;
1310                  }
1311  
1312                  if (!string.Equals(existing?.DeploymentName ?? string.Empty, updated?.DeploymentName ?? string.Empty, StringComparison.Ordinal))
1313                  {
1314                      return true;
1315                  }
1316  
1317                  if (!string.Equals(existing?.ApiVersion ?? string.Empty, updated?.ApiVersion ?? string.Empty, StringComparison.Ordinal))
1318                  {
1319                      return true;
1320                  }
1321  
1322                  if (!string.Equals(existing?.SystemPrompt ?? string.Empty, updated?.SystemPrompt ?? string.Empty, StringComparison.Ordinal))
1323                  {
1324                      return true;
1325                  }
1326  
1327                  if (existing?.ModerationEnabled != updated?.ModerationEnabled || existing?.EnableAdvancedAI != updated?.EnableAdvancedAI || existing?.IsActive != updated?.IsActive)
1328                  {
1329                      return true;
1330                  }
1331              }
1332  
1333              return false;
1334          }
1335  
1336          private void SubscribeToPasteAIConfiguration(PasteAIConfiguration configuration)
1337          {
1338              if (configuration is not null)
1339              {
1340                  configuration.PropertyChanged += OnPasteAIConfigurationPropertyChanged;
1341                  SubscribeToPasteAIProviders(configuration);
1342              }
1343          }
1344  
1345          private void UnsubscribeFromPasteAIConfiguration(PasteAIConfiguration configuration)
1346          {
1347              if (configuration is not null)
1348              {
1349                  configuration.PropertyChanged -= OnPasteAIConfigurationPropertyChanged;
1350                  UnsubscribeFromPasteAIProviders(configuration);
1351              }
1352          }
1353  
1354          private void SubscribeToPasteAIProviders(PasteAIConfiguration configuration)
1355          {
1356              if (configuration?.Providers is null)
1357              {
1358                  return;
1359              }
1360  
1361              configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged;
1362              configuration.Providers.CollectionChanged += OnPasteAIProvidersCollectionChanged;
1363  
1364              foreach (var provider in configuration.Providers)
1365              {
1366                  provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
1367                  provider.PropertyChanged += OnPasteAIProviderPropertyChanged;
1368              }
1369          }
1370  
1371          private void UnsubscribeFromPasteAIProviders(PasteAIConfiguration configuration)
1372          {
1373              if (configuration?.Providers is null)
1374              {
1375                  return;
1376              }
1377  
1378              configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged;
1379  
1380              foreach (var provider in configuration.Providers)
1381              {
1382                  provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
1383              }
1384          }
1385  
1386          private void OnPasteAIProvidersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
1387          {
1388              if (e?.NewItems is not null)
1389              {
1390                  foreach (PasteAIProviderDefinition provider in e.NewItems)
1391                  {
1392                      provider.PropertyChanged += OnPasteAIProviderPropertyChanged;
1393                  }
1394              }
1395  
1396              if (e?.OldItems is not null)
1397              {
1398                  foreach (PasteAIProviderDefinition provider in e.OldItems)
1399                  {
1400                      provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
1401                  }
1402              }
1403  
1404              var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
1405  
1406              OnPropertyChanged(nameof(PasteAIConfiguration));
1407              SaveAndNotifySettings();
1408          }
1409  
1410          private void OnPasteAIProviderPropertyChanged(object sender, PropertyChangedEventArgs e)
1411          {
1412              if (sender is PasteAIProviderDefinition provider)
1413              {
1414                  // When service type changes we may need to update credentials entry names.
1415                  if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.ServiceType), StringComparison.Ordinal))
1416                  {
1417                      SaveAndNotifySettings();
1418                      return;
1419                  }
1420  
1421                  SaveAndNotifySettings();
1422              }
1423          }
1424  
1425          private void OnPasteAIConfigurationPropertyChanged(object sender, PropertyChangedEventArgs e)
1426          {
1427              if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.Providers), StringComparison.Ordinal))
1428              {
1429                  SubscribeToPasteAIProviders(PasteAIConfiguration);
1430                  SaveAndNotifySettings();
1431                  return;
1432              }
1433  
1434              if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal))
1435              {
1436                  SaveAndNotifySettings();
1437              }
1438          }
1439  
1440          private void InitializePasteAIProviderState()
1441          {
1442              var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
1443              if (pasteConfig is null)
1444              {
1445                  _advancedPasteSettings.Properties.PasteAIConfiguration = new PasteAIConfiguration();
1446                  pasteConfig = _advancedPasteSettings.Properties.PasteAIConfiguration;
1447              }
1448  
1449              pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
1450  
1451              SubscribeToPasteAIProviders(pasteConfig);
1452          }
1453  
1454          private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)
1455          {
1456              if (string.IsNullOrWhiteSpace(credentialResource) || string.IsNullOrWhiteSpace(credentialUserName))
1457              {
1458                  return string.Empty;
1459              }
1460  
1461              try
1462              {
1463                  PasswordVault vault = new();
1464                  PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
1465                  existingCred?.RetrievePassword();
1466                  return existingCred?.Password?.Trim() ?? string.Empty;
1467              }
1468              catch (Exception)
1469              {
1470                  return string.Empty;
1471              }
1472          }
1473  
1474          private void UpdateCustomActionsCanMoveUpDown()
1475          {
1476              for (int index = 0; index < _customActions.Count; index++)
1477              {
1478                  var customAction = _customActions[index];
1479                  customAction.CanMoveUp = index != 0;
1480                  customAction.CanMoveDown = index != _customActions.Count - 1;
1481              }
1482          }
1483      }
1484  }