From b632854cd46497a96ad0fd4da24029f035435bb0 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 29 Mar 2025 18:40:17 +0100 Subject: [PATCH] Start the plugin system (#372) --- .../Assistants/ERI/AssistantERI.razor | 36 ++-- .../Components/PreviewAlpha.razor | 2 +- .../Components/PreviewAlpha.razor.cs | 8 +- .../Components/PreviewBeta.razor | 2 +- .../Components/PreviewBeta.razor.cs | 8 +- .../Components/PreviewExperimental.razor | 2 +- .../Components/PreviewExperimental.razor.cs | 8 +- .../Components/PreviewPrototype.razor | 2 +- .../Components/PreviewPrototype.razor.cs | 8 +- .../Components/PreviewReleaseCandidate.razor | 2 +- .../PreviewReleaseCandidate.razor.cs | 8 +- .../Settings/SettingsPanelEmbeddings.razor | 24 +-- .../Settings/SettingsPanelProfiles.razor | 18 +- .../Settings/SettingsPanelProviders.razor | 24 +-- .../Settings/SettingsDialogDataSources.razor | 20 ++- .../Layout/MainLayout.razor.cs | 62 +++---- app/MindWork AI Studio/Pages/Plugins.razor | 77 +++++++++ app/MindWork AI Studio/Pages/Plugins.razor.cs | 56 +++++++ app/MindWork AI Studio/Pages/Writer.razor | 5 +- .../icon.lua | 11 ++ .../plugin.lua | 10 +- .../icon.lua | 12 ++ .../plugin.lua | 8 +- app/MindWork AI Studio/Routes.razor.cs | 1 + .../Settings/DataModel/Data.cs | 5 + .../Settings/SettingsManager.cs | 3 + .../Tools/PluginSystem/IPluginMetadata.cs | 74 +++++++++ .../Tools/PluginSystem/NoPlugin.cs | 2 +- .../Tools/PluginSystem/PluginBase.Icon.cs | 45 +++++ .../Tools/PluginSystem/PluginBase.cs | 92 ++++------- .../PluginSystem/PluginFactory.Internal.cs | 79 +++++++++ .../Tools/PluginSystem/PluginFactory.cs | 156 ++++++++++-------- .../Tools/PluginSystem/PluginLanguage.cs | 2 +- .../Tools/PluginSystem/PluginMetadata.cs | 50 ++++++ app/MindWork AI Studio/wwwroot/app.css | 14 ++ .../wwwroot/changelog/v0.9.39.md | 2 + 36 files changed, 705 insertions(+), 233 deletions(-) create mode 100644 app/MindWork AI Studio/Pages/Plugins.razor create mode 100644 app/MindWork AI Studio/Pages/Plugins.razor.cs create mode 100644 app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua create mode 100644 app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor index 83eda3ec..86dfd241 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor @@ -228,18 +228,20 @@ else Name Type - Actions + Actions @context.EmbeddingName @context.EmbeddingType - - - Edit - - - Delete - + + + + Edit + + + Delete + + @@ -274,17 +276,19 @@ else Name - Actions + Actions @context.Name - - - Edit - - - Delete - + + + + Edit + + + Delete + + diff --git a/app/MindWork AI Studio/Components/PreviewAlpha.razor b/app/MindWork AI Studio/Components/PreviewAlpha.razor index 99f9d844..b1b629d8 100644 --- a/app/MindWork AI Studio/Components/PreviewAlpha.razor +++ b/app/MindWork AI Studio/Components/PreviewAlpha.razor @@ -1,4 +1,4 @@ - + Alpha diff --git a/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs b/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs index 62466852..deb962c4 100644 --- a/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs +++ b/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs @@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components; namespace AIStudio.Components; -public partial class PreviewAlpha : ComponentBase; \ No newline at end of file +public partial class PreviewAlpha : ComponentBase +{ + [Parameter] + public bool ApplyInnerScrollingFix { get; set; } + + private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/PreviewBeta.razor b/app/MindWork AI Studio/Components/PreviewBeta.razor index cd6b3c65..dd54225e 100644 --- a/app/MindWork AI Studio/Components/PreviewBeta.razor +++ b/app/MindWork AI Studio/Components/PreviewBeta.razor @@ -1,4 +1,4 @@ - + Beta diff --git a/app/MindWork AI Studio/Components/PreviewBeta.razor.cs b/app/MindWork AI Studio/Components/PreviewBeta.razor.cs index a5064b60..d8fee758 100644 --- a/app/MindWork AI Studio/Components/PreviewBeta.razor.cs +++ b/app/MindWork AI Studio/Components/PreviewBeta.razor.cs @@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components; namespace AIStudio.Components; -public partial class PreviewBeta : ComponentBase; \ No newline at end of file +public partial class PreviewBeta : ComponentBase +{ + [Parameter] + public bool ApplyInnerScrollingFix { get; set; } + + private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/PreviewExperimental.razor b/app/MindWork AI Studio/Components/PreviewExperimental.razor index 59e6651a..bedc9e4f 100644 --- a/app/MindWork AI Studio/Components/PreviewExperimental.razor +++ b/app/MindWork AI Studio/Components/PreviewExperimental.razor @@ -1,4 +1,4 @@ - + Experimental diff --git a/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs b/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs index c66fa730..0588d489 100644 --- a/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs +++ b/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs @@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components; namespace AIStudio.Components; -public partial class PreviewExperimental : ComponentBase; \ No newline at end of file +public partial class PreviewExperimental : ComponentBase +{ + [Parameter] + public bool ApplyInnerScrollingFix { get; set; } + + private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/PreviewPrototype.razor b/app/MindWork AI Studio/Components/PreviewPrototype.razor index f645e0ca..9aa8bbc0 100644 --- a/app/MindWork AI Studio/Components/PreviewPrototype.razor +++ b/app/MindWork AI Studio/Components/PreviewPrototype.razor @@ -1,4 +1,4 @@ - + Prototype diff --git a/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs b/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs index 573e2fd0..3ceab4d1 100644 --- a/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs +++ b/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs @@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components; namespace AIStudio.Components; -public partial class PreviewPrototype : ComponentBase; \ No newline at end of file +public partial class PreviewPrototype : ComponentBase +{ + [Parameter] + public bool ApplyInnerScrollingFix { get; set; } + + private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor index 44b51084..86954ddd 100644 --- a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor +++ b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor @@ -1,4 +1,4 @@ - + Release Candidate diff --git a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs index 1d22d17e..249f1f35 100644 --- a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs +++ b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs @@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components; namespace AIStudio.Components; -public partial class PreviewReleaseCandidate : ComponentBase; \ No newline at end of file +public partial class PreviewReleaseCandidate : ComponentBase +{ + [Parameter] + public bool ApplyInnerScrollingFix { get; set; } + + private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor index ea82ce7a..7203ff78 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor @@ -36,7 +36,7 @@ Name Provider Model - Actions + Actions @context.Num @@ -44,16 +44,18 @@ @context.UsedLLMProvider @this.GetEmbeddingProviderModelName(context) - - - Open Dashboard - - - Edit - - - Delete - + + + + Open Dashboard + + + Edit + + + Delete + + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor index 924eb773..e49390f0 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor @@ -23,18 +23,20 @@ # Profile Name - Actions + Actions @context.Num @context.Name - - - Edit - - - Delete - + + + + Edit + + + Delete + + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index cd1b4ffb..51db26b4 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -24,7 +24,7 @@ Instance Name Provider Model - Actions + Actions @context.Num @@ -44,16 +44,18 @@ @("as selected by provider") } - - - Open Dashboard - - - Edit - - - Delete - + + + + Open Dashboard + + + Edit + + + Delete + + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor index 5ec517a7..17c05fc2 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor @@ -29,7 +29,7 @@ Name Type Embedding - Actions + Actions @context.Num @@ -37,14 +37,16 @@ @context.Type.GetDisplayName() @this.GetEmbeddingName(context) - - - - Edit - - - Delete - + + + + + Edit + + + Delete + + diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 32d9e981..b689569b 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -82,8 +82,19 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis // Ensure that all settings are loaded: await this.SettingsManager.LoadSettings(); - // Ensure that all internal plugins are present: - await PluginFactory.EnsureInternalPlugins(); + // + // We cannot process the plugins before the settings are loaded, + // and we know our data directory. + // + if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager)) + { + // Ensure that all internal plugins are present: + await PluginFactory.EnsureInternalPlugins(); + + // Load (but not start) all plugins, without waiting for them: + var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + _ = PluginFactory.LoadAll(pluginLoadingTimeout.Token); + } // Register this component with the message bus: this.MessageBus.RegisterComponent(this); @@ -106,33 +117,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis private void LoadNavItems() { - var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager); - var isWriterModePreviewEnabled = PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager); - if (!isWriterModePreviewEnabled) - { - this.navItems = new List - { - new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true), - new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false), - new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false), - new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false), - new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false), - new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false), - }; - } - else - { - this.navItems = new List - { - new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true), - new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false), - new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false), - new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false), - new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false), - new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false), - new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false), - }; - } + this.navItems = new List(this.GetNavItems()); } #endregion @@ -185,6 +170,25 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis } #endregion + + private IEnumerable GetNavItems() + { + var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager); + + yield return new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true); + yield return new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false); + yield return new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false); + + if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager)) + yield return new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false); + + if (PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager)) + yield return new("Plugins", Icons.Material.TwoTone.Extension, palette.DarkLighten, palette.GrayLight, Routes.PLUGINS, false); + + yield return new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false); + yield return new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false); + yield return new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false); + } private async Task DismissUpdate() { diff --git a/app/MindWork AI Studio/Pages/Plugins.razor b/app/MindWork AI Studio/Pages/Plugins.razor new file mode 100644 index 00000000..6d5bf8ce --- /dev/null +++ b/app/MindWork AI Studio/Pages/Plugins.razor @@ -0,0 +1,77 @@ +@using AIStudio.Tools.PluginSystem +@attribute [Route(Routes.PLUGINS)] + +
+ + Plugins + + + + + + + + + + + + + + Plugins + Actions + + + + @switch (context.Key) + { + case GROUP_ENABLED: + + Enabled Plugins + + break; + + case GROUP_DISABLED: + + Disabled Plugins + + break; + + case GROUP_INTERNAL: + + Internal Plugins + + break; + } + + + + + +
+ @((MarkupString)context.IconSVG) +
+
+
+ + + + @context.Name + + + @context.Description + + + + + @if (!context.IsInternal) + { + var isEnabled = this.SettingsManager.IsPluginEnabled(context); + + + + } + +
+
+
+
\ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs new file mode 100644 index 00000000..f2dc83d9 --- /dev/null +++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs @@ -0,0 +1,56 @@ +using AIStudio.Settings; +using AIStudio.Tools.PluginSystem; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Pages; + +public partial class Plugins : ComponentBase +{ + private const string GROUP_ENABLED = "Enabled"; + private const string GROUP_DISABLED = "Disabled"; + private const string GROUP_INTERNAL = "Internal"; + + [Inject] + private MessageBus MessageBus { get; init; } = null!; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + private TableGroupDefinition groupConfig = null!; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + this.groupConfig = new TableGroupDefinition + { + Expandable = true, + IsInitiallyExpanded = true, + Selector = pluginMeta => + { + if (pluginMeta.IsInternal) + return GROUP_INTERNAL; + + return this.SettingsManager.IsPluginEnabled(pluginMeta) + ? GROUP_ENABLED + : GROUP_DISABLED; + } + }; + + await base.OnInitializedAsync(); + } + + #endregion + + private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta) + { + if (this.SettingsManager.IsPluginEnabled(pluginMeta)) + this.SettingsManager.ConfigurationData.EnabledPlugins.Remove(pluginMeta.Id); + else + this.SettingsManager.ConfigurationData.EnabledPlugins.Add(pluginMeta.Id); + + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Writer.razor b/app/MindWork AI Studio/Pages/Writer.razor index 6108647b..790e3e07 100644 --- a/app/MindWork AI Studio/Pages/Writer.razor +++ b/app/MindWork AI Studio/Pages/Writer.razor @@ -2,11 +2,10 @@ @inherits MSGComponentBase
- + Writer - - + diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua new file mode 100644 index 00000000..c10dd294 --- /dev/null +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua @@ -0,0 +1,11 @@ +SVG = [[ + + + + + + + + + + ]] \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 4801b782..ab05f0bc 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1,6 +1,12 @@ +require("contentHome") +require("icon") + -- The ID for this plugin: ID = "43065dbc-78d0-45b7-92be-f14c2926e2dc" +-- The icon for the plugin: +ICON_SVG = SVG + -- The name of the plugin: NAME = "MindWork AI Studio - German / Deutsch" @@ -32,8 +38,8 @@ TARGET_GROUPS = { "EVERYONE" } IS_MAINTAINED = true -- When the plugin is deprecated, this message will be shown to users: -DEPRECATION_MESSAGE = nil +DEPRECATION_MESSAGE = "" UI_TEXT_CONTENT = { HOME = CONTENT_HOME, -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua new file mode 100644 index 00000000..75320473 --- /dev/null +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua @@ -0,0 +1,12 @@ +SVG = [[ + + + + + + + + + + + ]] \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 6e2fea50..d6c98d49 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1,8 +1,12 @@ require("contentHome") +require("icon") -- The ID for this plugin: ID = "97dfb1ba-50c4-4440-8dfa-6575daf543c8" +-- The icon for the plugin: +ICON_SVG = SVG + -- The name of the plugin: NAME = "MindWork AI Studio - US English" @@ -34,8 +38,8 @@ TARGET_GROUPS = { "EVERYONE" } IS_MAINTAINED = true -- When the plugin is deprecated, this message will be shown to users: -DEPRECATION_MESSAGE = nil +DEPRECATION_MESSAGE = "" UI_TEXT_CONTENT = { HOME = CONTENT_HOME, -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs index 98326821..b6318820 100644 --- a/app/MindWork AI Studio/Routes.razor.cs +++ b/app/MindWork AI Studio/Routes.razor.cs @@ -9,6 +9,7 @@ public sealed partial class Routes public const string SETTINGS = "/settings"; public const string SUPPORTERS = "/supporters"; public const string WRITER = "/writer"; + public const string PLUGINS = "/plugins"; // ReSharper disable InconsistentNaming public const string ASSISTANT_TRANSLATION = "/assistant/translation"; diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index b7139c72..b47eba49 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -36,6 +36,11 @@ public sealed class Data /// public List Profiles { get; init; } = []; + /// + /// List of enabled plugins. + /// + public List EnabledPlugins { get; set; } = []; + /// /// The next provider number to use. /// diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 0930c9a5..ec74cab8 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; using AIStudio.Provider; using AIStudio.Settings.DataModel; +using AIStudio.Tools.PluginSystem; // ReSharper disable NotAccessedPositionalProperty.Local @@ -142,6 +143,8 @@ public sealed class SettingsManager(ILogger logger) return minimumLevel; } + public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id); + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false) { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs new file mode 100644 index 00000000..95d26b34 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs @@ -0,0 +1,74 @@ +namespace AIStudio.Tools.PluginSystem; + +public interface IPluginMetadata +{ + /// + /// The icon of this plugin. + /// + public string IconSVG { get; } + + /// + /// The type of this plugin. + /// + public PluginType Type { get; } + + /// + /// The ID of this plugin. + /// + public Guid Id { get; } + + /// + /// The name of this plugin. + /// + public string Name { get; } + + /// + /// The description of this plugin. + /// + public string Description { get; } + + /// + /// The version of this plugin. + /// + public PluginVersion Version { get; } + + /// + /// The authors of this plugin. + /// + public string[] Authors { get; } + + /// + /// The support contact for this plugin. + /// + public string SupportContact { get; } + + /// + /// The source URL of this plugin. + /// + public string SourceURL { get; } + + /// + /// The categories of this plugin. + /// + public PluginCategory[] Categories { get; } + + /// + /// The target groups of this plugin. + /// + public PluginTargetGroup[] TargetGroups { get; } + + /// + /// True, when the plugin is maintained. + /// + public bool IsMaintained { get; } + + /// + /// The message that should be displayed when the plugin is deprecated. + /// + public string DeprecationMessage { get; } + + /// + /// True, when the plugin is AI Studio internal. + /// + public bool IsInternal { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs index a21ab819..3d9b74d1 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs @@ -6,4 +6,4 @@ namespace AIStudio.Tools.PluginSystem; /// Represents a plugin that could not be loaded. /// /// The error message that occurred while parsing the plugin. -public sealed class NoPlugin(string parsingError) : PluginBase(string.Empty, LuaState.Create(), PluginType.NONE, parsingError); \ No newline at end of file +public sealed class NoPlugin(string parsingError) : PluginBase(false, LuaState.Create(), PluginType.NONE, parsingError); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs new file mode 100644 index 00000000..5c6140c8 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs @@ -0,0 +1,45 @@ +namespace AIStudio.Tools.PluginSystem; + +public abstract partial class PluginBase +{ + private const string DEFAULT_ICON_SVG = + """ + + """; + + #region Initialization-related methods + + /// + /// Tries to initialize the icon of the plugin. + /// + /// + /// When no icon is specified, the default icon will be used. + /// + /// The error message, when the icon could not be read. + /// The read icon as SVG. + /// True, when the icon could be read successfully. + + // ReSharper disable once OutParameterValueIsAlwaysDiscarded.Local + // ReSharper disable once UnusedMethodReturnValue.Local + private bool TryInitIconSVG(out string message, out string iconSVG) + { + if (!this.state.Environment["ICON_SVG"].TryRead(out iconSVG)) + { + iconSVG = DEFAULT_ICON_SVG; + message = "The field ICON_SVG does not exist or is not a valid string."; + return true; + } + + if (string.IsNullOrWhiteSpace(iconSVG)) + { + iconSVG = DEFAULT_ICON_SVG; + message = "The field ICON_SVG is empty. The icon must be a non-empty string."; + return true; + } + + message = string.Empty; + return true; + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs index 4f719d18..2674d4db 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs @@ -1,5 +1,4 @@ using Lua; -using Lua.Standard; // ReSharper disable MemberCanBePrivate.Global namespace AIStudio.Tools.PluginSystem; @@ -7,73 +6,55 @@ namespace AIStudio.Tools.PluginSystem; /// /// Represents the base of any AI Studio plugin. /// -public abstract class PluginBase +public abstract partial class PluginBase : IPluginMetadata { private readonly IReadOnlyCollection baseIssues; protected readonly LuaState state; protected readonly List pluginIssues = []; + + /// + public string IconSVG { get; } - /// - /// The type of this plugin. - /// + /// public PluginType Type { get; } - /// - /// The ID of this plugin. - /// + /// public Guid Id { get; } - /// - /// The name of this plugin. - /// + /// public string Name { get; } = string.Empty; - /// - /// The description of this plugin. - /// + /// public string Description { get; } = string.Empty; - /// - /// The version of this plugin. - /// + /// public PluginVersion Version { get; } - /// - /// The authors of this plugin. - /// + /// public string[] Authors { get; } = []; - /// - /// The support contact for this plugin. - /// + /// public string SupportContact { get; } = string.Empty; - /// - /// The source URL of this plugin. - /// + /// public string SourceURL { get; } = string.Empty; - /// - /// The categories of this plugin. - /// + /// public PluginCategory[] Categories { get; } = []; - /// - /// The target groups of this plugin. - /// + /// public PluginTargetGroup[] TargetGroups { get; } = []; - /// - /// True, when the plugin is maintained. - /// + /// public bool IsMaintained { get; } - /// - /// The message that should be displayed when the plugin is deprecated. - /// - public string? DeprecationMessage { get; } - + /// + public string DeprecationMessage { get; } = string.Empty; + + /// + public bool IsInternal { get; } + /// /// The issues that occurred during the initialization of this plugin. /// @@ -88,31 +69,24 @@ public abstract class PluginBase /// public bool IsValid => this is not NoPlugin && this.baseIssues.Count == 0 && this.pluginIssues.Count == 0; - protected PluginBase(string path, LuaState state, PluginType type, string parseError = "") + protected PluginBase(bool isInternal, LuaState state, PluginType type, string parseError = "") { this.state = state; this.Type = type; - - // For security reasons, we don't want to allow the plugin to load modules: - this.state.ModuleLoader = new NoModuleLoader(); - - // Add some useful libraries: - this.state.OpenModuleLibrary(); - this.state.OpenStringLibrary(); - this.state.OpenTableLibrary(); - this.state.OpenMathLibrary(); - this.state.OpenBitwiseLibrary(); - this.state.OpenCoroutineLibrary(); - - // Add the module loader so that the plugin can load other Lua modules: - this.state.ModuleLoader = new PluginLoader(path); var issues = new List(); if(!string.IsNullOrWhiteSpace(parseError)) issues.Add(parseError); + + // Notice: when no icon is specified, the default icon will be used. + this.TryInitIconSVG(out _, out var iconSVG); + this.IconSVG = iconSVG; if(this.TryInitId(out var issue, out var id)) + { this.Id = id; + this.IsInternal = isInternal; + } else if(this is not NoPlugin) issues.Add(issue); @@ -456,19 +430,19 @@ public abstract class PluginBase /// The error message, when the deprecation message could not be read. /// The read deprecation message. /// True, when the deprecation message could be read successfully. - private bool TryInitDeprecationMessage(out string message, out string? deprecationMessage) + private bool TryInitDeprecationMessage(out string message, out string deprecationMessage) { if (!this.state.Environment["DEPRECATION_MESSAGE"].TryRead(out deprecationMessage)) { - deprecationMessage = null; - message = "The field DEPRECATION_MESSAGE does not exist, is not a valid string. This field is optional: use nil to indicate that the plugin is not deprecated."; + deprecationMessage = string.Empty; + message = "The field DEPRECATION_MESSAGE does not exist, is not a valid string. This message is optional: use an empty string to indicate that the plugin is not deprecated."; return false; } message = string.Empty; return true; } - + /// /// Tries to initialize the UI text content of the plugin. /// diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs new file mode 100644 index 00000000..316376f6 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs @@ -0,0 +1,79 @@ +using System.Reflection; + +using Microsoft.Extensions.FileProviders; + +namespace AIStudio.Tools.PluginSystem; + +public static partial class PluginFactory +{ + public static async Task EnsureInternalPlugins() + { + LOG.LogInformation("Start ensuring internal plugins."); + foreach (var plugin in Enum.GetValues()) + { + LOG.LogInformation($"Ensure plugin: {plugin}"); + await EnsurePlugin(plugin); + } + } + + private static async Task EnsurePlugin(InternalPlugin plugin) + { + try + { + #if DEBUG + var basePath = Path.Join(Environment.CurrentDirectory, "Plugins"); + var resourceFileProvider = new PhysicalFileProvider(basePath); + #else + var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Plugins"); + #endif + + var metaData = plugin.MetaData(); + var mainResourcePath = $"{metaData.ResourcePath}/plugin.lua"; + var resourceInfo = resourceFileProvider.GetFileInfo(mainResourcePath); + + if(!resourceInfo.Exists) + { + LOG.LogError($"The plugin {plugin} does not exist. This should not happen, since the plugin is an integral part of AI Studio."); + return; + } + + // Ensure that the additional resources exist: + foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath)) + { + if(content.IsDirectory) + { + LOG.LogError("The plugin contains a directory. This is not allowed."); + continue; + } + + await CopyInternalPluginFile(content, metaData); + } + } + catch + { + LOG.LogError($"Was not able to ensure the plugin: {plugin}"); + } + } + + private static async Task CopyInternalPluginFile(IFileInfo resourceInfo, InternalPluginData metaData) + { + await using var inputStream = resourceInfo.CreateReadStream(); + + var pluginTypeBasePath = Path.Join(INTERNAL_PLUGINS_ROOT, metaData.Type.GetDirectory()); + + if (!Directory.Exists(INTERNAL_PLUGINS_ROOT)) + Directory.CreateDirectory(INTERNAL_PLUGINS_ROOT); + + if (!Directory.Exists(pluginTypeBasePath)) + Directory.CreateDirectory(pluginTypeBasePath); + + var pluginPath = Path.Join(pluginTypeBasePath, metaData.ResourceName); + if (!Directory.Exists(pluginPath)) + Directory.CreateDirectory(pluginPath); + + var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name); + + await using var outputStream = File.Create(pluginFilePath); + await inputStream.CopyToAsync(outputStream); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index e20d80f4..81837f73 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -1,101 +1,110 @@ -using System.Reflection; +using System.Text; using AIStudio.Settings; using Lua; - -using Microsoft.Extensions.FileProviders; +using Lua.Standard; namespace AIStudio.Tools.PluginSystem; -public static class PluginFactory +public static partial class PluginFactory { private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory"); + private static readonly string DATA_DIR = SettingsManager.DataDirectory!; - public static async Task EnsureInternalPlugins() - { - LOG.LogInformation("Start ensuring internal plugins."); - foreach (var plugin in Enum.GetValues()) - { - LOG.LogInformation($"Ensure plugin: {plugin}"); - await EnsurePlugin(plugin); - } - } + private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); - private static async Task EnsurePlugin(InternalPlugin plugin) + private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); + + private static readonly List AVAILABLE_PLUGINS = []; + + /// + /// A list of all available plugins. + /// + public static IReadOnlyCollection AvailablePlugins => AVAILABLE_PLUGINS; + + /// + /// Try to load all plugins from the plugins directory. + /// + /// + /// Loading plugins means:
+ /// - Parsing and checking the plugin code
+ /// - Check for forbidden plugins
+ /// - Creating a new instance of the allowed plugin
+ /// - Read the plugin metadata
+ ///
+ /// Loading a plugin does not mean to start the plugin, though. + ///
+ public static async Task LoadAll(CancellationToken cancellationToken = default) { - try + LOG.LogInformation("Start loading plugins."); + if (!Directory.Exists(PLUGINS_ROOT)) { - #if DEBUG - var basePath = Path.Join(Environment.CurrentDirectory, "Plugins"); - var resourceFileProvider = new PhysicalFileProvider(basePath); - #else - var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Plugins"); - #endif + LOG.LogInformation("No plugins found."); + return; + } + + // + // The easiest way to load all plugins is to find all `plugin.lua` files and load them. + // By convention, each plugin is enforced to have a `plugin.lua` file. + // + var pluginMainFiles = Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories); + foreach (var pluginMainFile in pluginMainFiles) + { + if (cancellationToken.IsCancellationRequested) + break; - var metaData = plugin.MetaData(); - var mainResourcePath = $"{metaData.ResourcePath}/plugin.lua"; - var resourceInfo = resourceFileProvider.GetFileInfo(mainResourcePath); + LOG.LogInformation($"Try to load plugin: {pluginMainFile}"); + var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken); + var pluginPath = Path.GetDirectoryName(pluginMainFile)!; + var plugin = await Load(pluginPath, code, cancellationToken); - if(!resourceInfo.Exists) + switch (plugin) { - LOG.LogError($"The plugin {plugin} does not exist. This should not happen, since the plugin is an integral part of AI Studio."); - return; - } - - // Ensure that the additional resources exist: - foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath)) - { - if(content.IsDirectory) - { - LOG.LogError("The plugin contains a directory. This is not allowed."); + case NoPlugin noPlugin when noPlugin.Issues.Any(): + LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: {noPlugin.Issues.First()}"); continue; - } - await CopyPluginFile(content, metaData); + case NoPlugin: + LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: Unknown."); + continue; + + case { IsValid: false }: + LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix."); + #if DEBUG + foreach (var pluginIssue in plugin.Issues) + LOG.LogError($"Plugin issue: {pluginIssue}"); + #endif + continue; + + case { IsMaintained: false }: + LOG.LogWarning($"The plugin '{pluginMainFile}' is not maintained anymore. Please consider to disable it."); + break; } - } - catch - { - LOG.LogError($"Was not able to ensure the plugin: {plugin}"); - } - } - - private static async Task CopyPluginFile(IFileInfo resourceInfo, InternalPluginData metaData) - { - await using var inputStream = resourceInfo.CreateReadStream(); - - var pluginsRoot = Path.Join(DATA_DIR, "plugins"); - var pluginTypeBasePath = Path.Join(pluginsRoot, metaData.Type.GetDirectory()); - - if (!Directory.Exists(pluginsRoot)) - Directory.CreateDirectory(pluginsRoot); - - if (!Directory.Exists(pluginTypeBasePath)) - Directory.CreateDirectory(pluginTypeBasePath); - - var pluginPath = Path.Join(pluginTypeBasePath, metaData.ResourceName); - if (!Directory.Exists(pluginPath)) - Directory.CreateDirectory(pluginPath); - - var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name); - await using var outputStream = File.Create(pluginFilePath); - await inputStream.CopyToAsync(outputStream); + LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')"); + AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin)); + } } - public static async Task LoadAll() - { - - } - - public static async Task Load(string path, string code, CancellationToken cancellationToken = default) + private static async Task Load(string pluginPath, string code, CancellationToken cancellationToken = default) { if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState) return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}"); var state = LuaState.Create(); + + // Add the module loader so that the plugin can load other Lua modules: + state.ModuleLoader = new PluginLoader(pluginPath); + + // Add some useful libraries: + state.OpenModuleLibrary(); + state.OpenStringLibrary(); + state.OpenTableLibrary(); + state.OpenMathLibrary(); + state.OpenBitwiseLibrary(); + state.OpenCoroutineLibrary(); try { @@ -105,6 +114,10 @@ public static class PluginFactory { return new NoPlugin($"Was not able to parse the plugin: {e.Message}"); } + catch (LuaRuntimeException e) + { + return new NoPlugin($"Was not able to run the plugin: {e.Message}"); + } if (!state.Environment["TYPE"].TryRead(out var typeText)) return new NoPlugin("TYPE does not exist or is not a valid string."); @@ -115,9 +128,10 @@ public static class PluginFactory if(type is PluginType.NONE) return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}"); + var isInternal = pluginPath.StartsWith(INTERNAL_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase); return type switch { - PluginType.LANGUAGE => new PluginLanguage(path, state, type), + PluginType.LANGUAGE => new PluginLanguage(isInternal, state, type), _ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.") }; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs index e1676e49..4c8cf30a 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs @@ -8,7 +8,7 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin private ILanguagePlugin? baseLanguage; - public PluginLanguage(string path, LuaState state, PluginType type) : base(path, state, type) + public PluginLanguage(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type) { if (this.TryInitUITextContent(out var issue, out var readContent)) this.content = readContent; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs new file mode 100644 index 00000000..2bfdab1e --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs @@ -0,0 +1,50 @@ +namespace AIStudio.Tools.PluginSystem; + +public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata +{ + #region Implementation of IPluginMetadata + + /// + public string IconSVG { get; } = plugin.IconSVG; + + /// + public PluginType Type { get; } = plugin.Type; + + /// + public Guid Id { get; } = plugin.Id; + + /// + public string Name { get; } = plugin.Name; + + /// + public string Description { get; } = plugin.Description; + + /// + public PluginVersion Version { get; } = plugin.Version; + + /// + public string[] Authors { get; } = plugin.Authors; + + /// + public string SupportContact { get; } = plugin.SupportContact; + + /// + public string SourceURL { get; } = plugin.SourceURL; + + /// + public PluginCategory[] Categories { get; } = plugin.Categories; + + /// + public PluginTargetGroup[] TargetGroups { get; } = plugin.TargetGroups; + + /// + public bool IsMaintained { get; } = plugin.IsMaintained; + + /// + public string DeprecationMessage { get; } = plugin.DeprecationMessage; + + /// + public bool IsInternal { get; } = plugin.IsInternal; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index e62b4f83..af0e139a 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -35,6 +35,20 @@ margin-top: 4px; } +.plugin-icon-container { + width: var(--mud-icon-size-large); + height: var(--mud-icon-size-large); +} + +.plugin-icon-container svg { + width: 100%; + height: 100%; +} + +.mud-popover-open.InnerScrollingFix { + left: 0 !important; +} + :root { --confidence-color: #000000; } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md index a3f49df3..94f9958e 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md @@ -1,4 +1,6 @@ # v0.9.39, build 214 (2025-03-xx xx:xx UTC) - Added a feature flag for the plugin system. This flag is disabled by default and can be enabled inside the app settings. Please note that this feature is still in development; there are no plugins available yet. - Added the Lua library we use for the plugin system to the about page. +- Added the plugin overview page. This page shows all installed plugins and allows you to enable or disable them. It is only available when the plugin preview feature is enabled. +- Fixed the preview tooltip component not showing the correct position when used inside a scrollable container. - Upgraded to Rust 1.85.1 \ No newline at end of file