Introduced additive configuration handling for managed preview features (#667)
Some checks are pending
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Peer Schütt 2026-02-20 12:40:38 +01:00 committed by GitHub
parent ed8bd9d25c
commit 6f76c845f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 260 additions and 40 deletions

View File

@ -14,8 +14,22 @@
SelectedValuesChanged="@this.OptionChanged"> SelectedValuesChanged="@this.OptionChanged">
@foreach (var data in this.Data) @foreach (var data in this.Data)
{ {
<MudSelectItemExtended Value="@data.Value"> var isLockedValue = this.IsLockedValue(data.Value);
<MudSelectItemExtended Value="@data.Value" Disabled="@isLockedValue" Style="@(isLockedValue ? "pointer-events:auto !important;" : null)">
@if (isLockedValue)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.FlexStart" Wrap="Wrap.NoWrap">
@* MudTooltip.RootStyle is set as a workaround for issue -> https://github.com/MudBlazor/MudBlazor/issues/10882 *@
<MudTooltip Text="@this.LockedTooltip()" Arrow="true" Placement="Placement.Right" RootStyle="display:inline-flex;">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" Class="mr-1"/>
</MudTooltip>
@data.Name @data.Name
</MudStack>
}
else
{
@data.Name
}
</MudSelectItemExtended> </MudSelectItemExtended>
} }
</MudSelectExtended> </MudSelectExtended>

View File

@ -28,6 +28,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
[Parameter] [Parameter]
public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { }; public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { };
/// <summary>
/// Determines whether a specific item is locked by a configuration plugin.
/// </summary>
[Parameter]
public Func<TData, bool> IsItemLocked { get; set; } = _ => false;
#region Overrides of ConfigurationBase #region Overrides of ConfigurationBase
/// <inheritdoc /> /// <inheritdoc />
@ -62,4 +68,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
return string.Format(T("You have selected {0} preview features."), selectedValues.Count); return string.Format(T("You have selected {0} preview features."), selectedValues.Count);
} }
private bool IsLockedValue(TData value) => this.IsItemLocked(value);
private string LockedTooltip() =>
this.T(
"This feature is managed by your organization and has therefore been disabled.",
typeof(ConfigurationBase).Namespace,
nameof(ConfigurationBase));
} }

View File

@ -25,7 +25,7 @@
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList(); var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
if (availablePreviewFeatures.Count > 0) if (availablePreviewFeatures.Count > 0)
{ {
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet())" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="@T("Which preview features would you like to enable?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/> <ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@this.GetSelectedPreviewFeatures" Data="@availablePreviewFeatures" SelectionUpdate="@this.UpdateEnabledPreviewFeatures" OptionHelp="@T("Which preview features would you like to enable?")" IsItemLocked="@this.IsPluginContributedPreviewFeature" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
} }
} }

View File

@ -27,7 +27,41 @@ public partial class SettingsPanelApp : SettingsPanelBase
private void UpdatePreviewFeatures(PreviewVisibility previewVisibility) private void UpdatePreviewFeatures(PreviewVisibility previewVisibility)
{ {
this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility; this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility;
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures); var filtered = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures);
filtered.UnionWith(this.GetPluginContributedPreviewFeatures());
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = filtered;
}
private HashSet<PreviewFeatures> GetPluginContributedPreviewFeatures()
{
if (ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.HasPluginContribution)
return meta.PluginContribution.Where(x => !x.IsReleased()).ToHashSet();
return [];
}
private bool IsPluginContributedPreviewFeature(PreviewFeatures feature)
{
if (feature.IsReleased())
return false;
if (!ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) || !meta.HasPluginContribution)
return false;
return meta.PluginContribution.Contains(feature);
}
private HashSet<PreviewFeatures> GetSelectedPreviewFeatures()
{
var enabled = this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet();
enabled.UnionWith(this.GetPluginContributedPreviewFeatures());
return enabled;
}
private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures)
{
selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures());
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures;
} }
private async Task UpdateLangBehaviour(LangBehavior behavior) private async Task UpdateLangBehaviour(LangBehavior behavior)

View File

@ -28,14 +28,14 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
private Expression<Func<TClass, TValue>> PropertyExpression { get; } private Expression<Func<TClass, TValue>> PropertyExpression { get; }
/// <summary> /// <summary>
/// Indicates whether the configuration is managed by a plugin and is therefore locked. /// Indicates whether the configuration is locked by a configuration plugin.
/// </summary> /// </summary>
public bool IsLocked { get; private set; } public bool IsLocked { get; private set; }
/// <summary> /// <summary>
/// The ID of the plugin that manages this configuration. This is set when the configuration is locked. /// The ID of the plugin that locked this configuration.
/// </summary> /// </summary>
public Guid MangedByConfigPluginId { get; private set; } public Guid LockedByConfigPluginId { get; private set; }
/// <summary> /// <summary>
/// The default value for the configuration property. This is used when resetting the property to its default state. /// The default value for the configuration property. This is used when resetting the property to its default state.
@ -43,30 +43,74 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
public required TValue Default { get; init; } public required TValue Default { get; init; }
/// <summary> /// <summary>
/// Locks the configuration state, indicating that it is managed by a specific plugin. /// Indicates whether a plugin contribution is available.
/// </summary> /// </summary>
/// <param name="pluginId">The ID of the plugin that is managing this configuration.</param> public bool HasPluginContribution { get; private set; }
public void LockManagedState(Guid pluginId)
/// <summary>
/// The additive value contribution provided by a configuration plugin.
/// </summary>
public TValue PluginContribution { get; private set; } = default!;
/// <summary>
/// The ID of the plugin that provided the additive value contribution.
/// </summary>
public Guid PluginContributionByConfigPluginId { get; private set; }
/// <summary>
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
/// </summary>
/// <param name="pluginId">The ID of the plugin that is locking this configuration.</param>
public void LockConfiguration(Guid pluginId)
{ {
this.IsLocked = true; this.IsLocked = true;
this.MangedByConfigPluginId = pluginId; this.LockedByConfigPluginId = pluginId;
} }
/// <summary> /// <summary>
/// Resets the managed state of the configuration, allowing it to be modified again. /// Resets the locked state of the configuration, allowing it to be modified again.
/// This will also reset the property to its default value. /// This will also reset the property to its default value.
/// </summary> /// </summary>
public void ResetManagedState() public void ResetLockedConfiguration()
{ {
this.IsLocked = false; this.IsLocked = false;
this.MangedByConfigPluginId = Guid.Empty; this.LockedByConfigPluginId = Guid.Empty;
this.Reset(); this.Reset();
} }
/// <summary>
/// Unlocks the configuration state without changing the current value.
/// </summary>
public void UnlockConfiguration()
{
this.IsLocked = false;
this.LockedByConfigPluginId = Guid.Empty;
}
/// <summary>
/// Stores an additive plugin contribution.
/// </summary>
public void SetPluginContribution(TValue value, Guid pluginId)
{
this.PluginContribution = value;
this.PluginContributionByConfigPluginId = pluginId;
this.HasPluginContribution = true;
}
/// <summary>
/// Clears the additive plugin contribution without changing the current value.
/// </summary>
public void ClearPluginContribution()
{
this.PluginContribution = default!;
this.PluginContributionByConfigPluginId = Guid.Empty;
this.HasPluginContribution = false;
}
/// <summary> /// <summary>
/// Resets the configuration property to its default value. /// Resets the configuration property to its default value.
/// </summary> /// </summary>
public void Reset() private void Reset()
{ {
var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
var memberExpression = this.PropertyExpression.GetMemberExpression(); var memberExpression = this.PropertyExpression.GetMemberExpression();

View File

@ -582,6 +582,90 @@ public static partial class ManagedConfiguration
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
} }
/// <summary>
/// Attempts to process additive plugin contributions for enum set settings from a Lua table.
/// The contributed values are merged into the existing set, and the setting remains unlocked
/// so users can add additional values.
/// </summary>
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
/// <param name="settings">The Lua table containing the settings to process.</param>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the set
/// elements, which must be an enum.</typeparam>
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
public static bool TryProcessConfigurationWithPluginContribution<TClass, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
Guid configPluginId,
LuaTable settings,
bool dryRun)
where TValue : Enum
{
//
// Handle configured enum sets (additive merge)
//
// Check if that configuration was registered:
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = new HashSet<TValue>();
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
configuredLuaList.TryRead<LuaTable>(out var valueTable))
{
// Determine the length of the Lua table and prepare a set to hold the parsed values:
var len = valueTable.ArrayLength;
var set = new HashSet<TValue>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2 -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
{
// Step 3 -- try to parse the string as the target type:
if (Enum.TryParse(typeof(TValue), configuredLuaValueText, true, out var configuredEnum))
set.Add((TValue)configuredEnum);
}
}
configuredValue = set;
successful = true;
}
if (dryRun)
return successful;
if (successful)
{
var configInstance = configSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
var currentValue = propertyExpression.Compile().Invoke(configInstance);
var merged = new HashSet<TValue>(currentValue);
merged.UnionWith(configuredValue);
configMeta.SetValue(merged);
configMeta.SetPluginContribution(new HashSet<TValue>(configuredValue), configPluginId);
}
else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId)
{
configMeta.ClearPluginContribution();
}
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
configMeta.UnlockConfiguration();
return successful;
}
/// <summary> /// <summary>
/// Attempts to process the configuration settings from a Lua table for string set types. /// Attempts to process the configuration settings from a Lua table for string set types.
/// </summary> /// </summary>
@ -744,12 +828,12 @@ public static partial class ManagedConfiguration
// Case: the setting was configured, and we could read the value successfully. // Case: the setting was configured, and we could read the value successfully.
// //
// Set the configured value and lock the managed state: // Set the configured value and lock the configuration:
configMeta.SetValue(configuredValue); configMeta.SetValue(configuredValue);
configMeta.LockManagedState(configPluginId); configMeta.LockConfiguration(configPluginId);
break; break;
case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId: case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId:
// //
// Case: the setting was configured previously, but we could not read the value successfully. // Case: the setting was configured previously, but we could not read the value successfully.
// This happens when the setting was removed from the configuration plugin. We handle that // This happens when the setting was removed from the configuration plugin. We handle that
@ -757,10 +841,10 @@ public static partial class ManagedConfiguration
// //
// The other case, when the setting was locked and managed by a different configuration plugin, // The other case, when the setting was locked and managed by a different configuration plugin,
// is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin // is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin
// is still available. If it is not available, it resets the managed state of the // is still available. If it is not available, it resets the locked state of the
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
// //
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
break; break;
case false: case false:

View File

@ -9,6 +9,7 @@ namespace AIStudio.Settings;
public static partial class ManagedConfiguration public static partial class ManagedConfiguration
{ {
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new(); private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
/// <summary> /// <summary>
/// Attempts to retrieve the configuration metadata for a given configuration selection and /// Attempts to retrieve the configuration metadata for a given configuration selection and
@ -251,13 +252,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@ -272,13 +273,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@ -296,13 +297,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@ -319,13 +320,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }
@ -340,13 +341,38 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true;
}
return false;
}
/// <summary>
/// Checks if a plugin contribution is left over from a configuration plugin that is no longer available.
/// If so, it clears the contribution and returns true.
/// </summary>
public static bool IsPluginContributionLeftOver<TClass, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
IEnumerable<IAvailablePlugin> availablePlugins)
{
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
if (!configMeta.HasPluginContribution || configMeta.PluginContributionByConfigPluginId == Guid.Empty)
return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.PluginContributionByConfigPluginId);
if (plugin is null)
{
configMeta.ClearPluginContribution();
return true; return true;
} }
@ -361,13 +387,13 @@ public static partial class ManagedConfiguration
if (!TryGet(configSelection, propertyExpression, out var configMeta)) if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false; return false;
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false; return false;
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (plugin is null) if (plugin is null)
{ {
configMeta.ResetManagedState(); configMeta.ResetLockedConfiguration();
return true; return true;
} }

View File

@ -121,8 +121,8 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: preview features visibility // Config: preview features visibility
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
// Config: enabled preview features // Config: enabled preview features (plugin contribution; users can enable additional features)
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfigurationWithPluginContribution(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun);
// Config: hide some assistants? // Config: hide some assistants?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun);

View File

@ -219,6 +219,9 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS)) if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
wasConfigurationChanged = true; wasConfigurationChanged = true;
if(ManagedConfiguration.IsPluginContributionLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the transcription provider: // Check for the transcription provider:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UseTranscriptionProvider, AVAILABLE_PLUGINS)) if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UseTranscriptionProvider, AVAILABLE_PLUGINS))
wasConfigurationChanged = true; wasConfigurationChanged = true;

View File

@ -11,6 +11,7 @@
- Improved the workspaces experience by using a different color for the delete button to avoid confusion. - Improved the workspaces experience by using a different color for the delete button to avoid confusion.
- Improved single-input dialogs (e.g., renaming chats) so pressing `Enter` confirmed immediately and the input field focused automatically when the dialog opened. - Improved single-input dialogs (e.g., renaming chats) so pressing `Enter` confirmed immediately and the input field focused automatically when the dialog opened.
- Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition. - Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition.
- Improved the configuration plugins by making `EnabledPreviewFeatures` additive rather than exclusive. Users can now enable additional preview features without being restricted to those selected by the configuration plugin.
- Improved the system language detection for locale values such as `C` and variants like `de_DE.UTF-8`, enabling AI Studio to apply the matching UI language more reliably. - Improved the system language detection for locale values such as `C` and variants like `de_DE.UTF-8`, enabling AI Studio to apply the matching UI language more reliably.
- Fixed an issue where leftover enterprise configuration plugins could remain active after organizational assignment changes during longer absences (for example, vacation), which could lead to configuration conflicts. - Fixed an issue where leftover enterprise configuration plugins could remain active after organizational assignment changes during longer absences (for example, vacation), which could lead to configuration conflicts.
- Fixed an issue where manually saving chats in workspace manual-storage mode could appear unreliable during response streaming. The save button is now disabled while streaming to prevent partial saves. - Fixed an issue where manually saving chats in workspace manual-storage mode could appear unreliable during response streaming. The save button is now disabled while streaming to prevent partial saves.