From f0c35d89143b6d575358429af627adbb9b2b7c37 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 6 Aug 2026 07:57:09 +0200 Subject: [PATCH] Added a confirmation dialog before importing assistant plugins --- .../Assistants/I18N/allTexts.lua | 66 ++++++++++++++++ .../Dialogs/InformationDialog.razor | 16 ++++ .../Dialogs/InformationDialog.razor.cs | 35 +++++++++ .../Dialogs/PluginImportDialog.razor | 60 +++++++++++++++ .../Dialogs/PluginImportDialog.razor.cs | 47 ++++++++++++ app/MindWork AI Studio/Pages/Plugins.razor.cs | 40 +++++++++- .../plugin.lua | 68 ++++++++++++++++- .../plugin.lua | 68 ++++++++++++++++- .../Services/AssistantPluginCheckResult.cs | 3 + .../Services/AssistantPluginDeleteResult.cs | 3 + .../Services/AssistantPluginInstallResult.cs | 3 + .../Services/AssistantPluginInstallService.cs | 76 ++++++++++--------- .../Services/AssistantPluginUpdateResult.cs | 3 + .../Tools/Services/PluginImportPreview.cs | 17 +++++ 14 files changed, 466 insertions(+), 39 deletions(-) create mode 100644 app/MindWork AI Studio/Dialogs/InformationDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/InformationDialog.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/PluginImportDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/PluginImportDialog.razor.cs create mode 100644 app/MindWork AI Studio/Tools/Services/AssistantPluginCheckResult.cs create mode 100644 app/MindWork AI Studio/Tools/Services/AssistantPluginDeleteResult.cs create mode 100644 app/MindWork AI Studio/Tools/Services/AssistantPluginInstallResult.cs create mode 100644 app/MindWork AI Studio/Tools/Services/AssistantPluginUpdateResult.cs create mode 100644 app/MindWork AI Studio/Tools/Services/PluginImportPreview.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 1ead6621..89bcbfc5 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5359,6 +5359,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embe -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close" +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Close" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." @@ -5449,6 +5452,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T504404155"] = "Accept the ter -- Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking "Accept GPL and archive," you agree to the terms of the GPL license. Software under GPL is free of charge and free to use. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking \"Accept GPL and archive,\" you agree to the terms of the GPL license. Software under GPL is free of charge and free to use." +-- Install plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1525735539"] = "Install plugin" + +-- Version +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1573770551"] = "Version" + +-- Source +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1642243064"] = "Source" + +-- You are about to install a language plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1974491324"] = "You are about to install a language plugin from a file." + +-- Authors +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1985367263"] = "Authors" + +-- Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2053517490"] = "Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source." + +-- You are about to install an assistant plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2063808316"] = "You are about to install an assistant plugin from a file." + +-- You are about to install a configuration plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T21052500"] = "You are about to install a configuration plugin from a file." + +-- You are about to install a theme plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2163853103"] = "You are about to install a theme plugin from a file." + +-- Support contact +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Support contact" + +-- This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3415610475"] = "This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}." + +-- Unknown +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3424652889"] = "Unknown" + +-- Type +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3512062061"] = "Type" + +-- Replace plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Replace plugin" + +-- The authors marked this plugin as deprecated: {0} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "The authors marked this plugin as deprecated: {0}" + +-- You are about to install a plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T841685558"] = "You are about to install a plugin from a file." + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Cancel" + -- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally." @@ -7942,6 +7996,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins" -- Revise Assistant Plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "Revise Assistant Plugin" +-- Import not possible +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3051566124"] = "Import not possible" + -- The assistant plugin '{0}' has been successfully saved. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved." @@ -7975,6 +8032,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions" -- Export plugin archive UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3872669664"] = "Export plugin archive" +-- Install Plugin +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3902690643"] = "Install Plugin" + -- Assistant updated. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T40397082"] = "Assistant updated." @@ -9517,6 +9577,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4 -- Internal assistant plugins cannot be deleted. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Internal assistant plugins cannot be deleted." +-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins." + -- The assistant plugin directory is outside the local assistant plugin directory. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1211881977"] = "The assistant plugin directory is outside the local assistant plugin directory." @@ -9550,6 +9613,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223 -- Unexpected error: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}" +-- Config server managed assistant plugins cannot be replaced. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2594571117"] = "Config server managed assistant plugins cannot be replaced." + -- The assistant plugin has no local directory. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory." diff --git a/app/MindWork AI Studio/Dialogs/InformationDialog.razor b/app/MindWork AI Studio/Dialogs/InformationDialog.razor new file mode 100644 index 00000000..02128ffd --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/InformationDialog.razor @@ -0,0 +1,16 @@ +@inherits MSGComponentBase + + + + + + @this.Message + + + + + + @T("Close") + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/InformationDialog.razor.cs b/app/MindWork AI Studio/Dialogs/InformationDialog.razor.cs new file mode 100644 index 00000000..3d58e676 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/InformationDialog.razor.cs @@ -0,0 +1,35 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +/// +/// A dialog that informs the user about something without asking for a decision. Use it when a +/// message must not be missed, e.g., when an action was refused. +/// +public partial class InformationDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + /// + /// The message shown to the user. + /// + [Parameter] + public string Message { get; set; } = string.Empty; + + /// + /// The icon shown next to the message. + /// + [Parameter] + public string Icon { get; set; } = Icons.Material.Filled.Info; + + /// + /// The color of the icon. + /// + [Parameter] + public Color IconColor { get; set; } = Color.Info; + + private void Close() => this.MudDialog.Close(DialogResult.Ok(true)); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/PluginImportDialog.razor b/app/MindWork AI Studio/Dialogs/PluginImportDialog.razor new file mode 100644 index 00000000..a66315b0 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/PluginImportDialog.razor @@ -0,0 +1,60 @@ +@inherits MSGComponentBase + + + + @this.IntroductionText @T("Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source.") + + + + + @this.Preview.Plugin.Name + + + @this.Preview.Plugin.Description + + + @T("Type"): @this.TypeLabel + + + @T("Version"): @this.Preview.Plugin.Version + + + @T("Authors"): @this.AuthorsLabel + + @if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.SourceURL)) + { + + @T("Source"): @this.Preview.Plugin.SourceURL + + } + @if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.SupportContact)) + { + + @T("Support contact"): @this.Preview.Plugin.SupportContact + + } + + + @if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.DeprecationMessage)) + { + + @string.Format(T("The authors marked this plugin as deprecated: {0}"), this.Preview.Plugin.DeprecationMessage) + + } + + @if (this.Preview.ExistingPlugin is { } existingPlugin) + { + + @string.Format(T("This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}."), existingPlugin.Name, existingPlugin.Version, this.Preview.Plugin.Version) + + } + + + + @T("Cancel") + + + @(this.Preview.ReplacesExisting ? T("Replace plugin") : T("Install plugin")) + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/PluginImportDialog.razor.cs b/app/MindWork AI Studio/Dialogs/PluginImportDialog.razor.cs new file mode 100644 index 00000000..b94f78cc --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/PluginImportDialog.razor.cs @@ -0,0 +1,47 @@ +using AIStudio.Components; +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Services; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +/// +/// Asks the user whether a plugin archive may be installed. It shows the metadata the archive +/// declares about itself, so the user can judge the plugin before its code runs. +/// +public partial class PluginImportDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + /// + /// The metadata of the plugin archive about to be installed. + /// + [Parameter] + public PluginImportPreview Preview { get; set; } = null!; + + /// + /// Names the kind of plugin the user is about to install. Each plugin type gets its own + /// sentence instead of a placeholder because articles and word order differ between languages. + /// + private string IntroductionText => this.Preview.Plugin.Type switch + { + PluginType.LANGUAGE => this.T("You are about to install a language plugin from a file."), + PluginType.ASSISTANT => this.T("You are about to install an assistant plugin from a file."), + PluginType.CONFIGURATION => this.T("You are about to install a configuration plugin from a file."), + PluginType.THEME => this.T("You are about to install a theme plugin from a file."), + + _ => this.T("You are about to install a plugin from a file."), + }; + + private string TypeLabel => this.Preview.Plugin.Type.GetName(); + + private string AuthorsLabel => this.Preview.Plugin.Authors.Length > 0 + ? string.Join(", ", this.Preview.Plugin.Authors) + : this.T("Unknown"); + + private void Cancel() => this.MudDialog.Cancel(); + + private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true)); +} \ 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 index 708fad2a..56235176 100644 --- a/app/MindWork AI Studio/Pages/Plugins.razor.cs +++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs @@ -328,11 +328,17 @@ public partial class Plugins : MSGComponentBase if (selection.UserCancelled) return; - var result = await this.AssistantPluginInstallService.InstallArchiveAsync(selection.SelectedFilePath, CancellationToken.None); + var result = await this.AssistantPluginInstallService.InstallArchiveAsync(selection.SelectedFilePath, this.ConfirmPluginImportAsync, CancellationToken.None); + if (result.Cancelled) + return; + if (!result.Success) { LOG.LogError("Failed to import assistant plugin archive '{ArchivePath}': {Issue}", selection.SelectedFilePath, result.Issue); - await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, string.Format(this.T("The assistant plugin could not be imported: {0}"), result.Issue))); + + // The user actively started this import, so we report the reason in a dialog + // instead of a snackbar. Refused imports must not be missed: + await this.ShowImportRefusedDialogAsync(result.Issue); return; } @@ -350,6 +356,36 @@ public partial class Plugins : MSGComponentBase } } + /// + /// Shows the metadata of a validated plugin archive and asks whether it may be installed. + /// + /// The metadata the archive declares about itself. + /// True when the user confirmed the installation. + private async Task ConfirmPluginImportAsync(PluginImportPreview preview) + { + var dialogParameters = new DialogParameters + { + { x => x.Preview, preview }, + }; + + var dialogReference = await this.DialogService.ShowAsync(this.T("Install Plugin"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + return dialogResult is { Canceled: false }; + } + + private async Task ShowImportRefusedDialogAsync(string issue) + { + var dialogParameters = new DialogParameters + { + { x => x.Message, string.Format(this.T("The assistant plugin could not be imported: {0}"), issue) }, + { x => x.Icon, Icons.Material.Filled.ReportProblem }, + { x => x.IconColor, Color.Error }, + }; + + var dialogReference = await this.DialogService.ShowAsync(this.T("Import not possible"), dialogParameters, DialogOptions.FULLSCREEN); + await dialogReference.Result; + } + private static bool IsSendingMail(string sourceUrl) => sourceUrl.TrimStart().StartsWith("mailto:", StringComparison.OrdinalIgnoreCase); private PluginAssistants? TryGetAssistantPlugin(Guid pluginId) => PluginFactory.RunningPlugins.OfType().FirstOrDefault(x => x.Id == pluginId); 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 4fbcf5ac..d4efa75b 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 @@ -5361,6 +5361,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Einb -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schließen" +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Schließen" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems." @@ -5451,6 +5454,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T504404155"] = "Akzeptieren Si -- Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking "Accept the GPL and download the archive," you agree to the terms of the GPL license. Software under GPL is free of charge and free to use. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc wird unter der GNU General Public License v2 (GPL) vertrieben. Wenn Sie auf „GPL akzeptieren und Archiv herunterladen“ klicken, stimmen Sie den Bedingungen der GPL-Lizenz zu. Software unter der GPL ist kostenlos und frei nutzbar." +-- Install plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1525735539"] = "Plugin installieren" + +-- Version +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1573770551"] = "Version" + +-- Source +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1642243064"] = "Quelle" + +-- You are about to install a language plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1974491324"] = "Sie sind dabei, ein Sprach-Plugin aus einer Datei zu installieren." + +-- Authors +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1985367263"] = "Autor:innen" + +-- Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2053517490"] = "Plugins enthalten Code, der innerhalb von AI Studio ausgeführt wird. Installieren Sie Plugins nur, wenn Sie der Quelle vertrauen." + +-- You are about to install an assistant plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2063808316"] = "Sie sind dabei, ein Assistenten-Plugin aus einer Datei zu installieren." + +-- You are about to install a configuration plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T21052500"] = "Sie sind dabei, ein Konfigurations-Plugin aus einer Datei zu installieren." + +-- You are about to install a theme plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2163853103"] = "Sie sind dabei, ein Design-Plugin aus einer Datei zu installieren." + +-- Support contact +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Supportkontakt" + +-- This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3415610475"] = "Dies ersetzt das bereits installierte Plugin „{0}“. Version {1} wird durch Version {2} ersetzt." + +-- Unknown +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3424652889"] = "Unbekannt" + +-- Type +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3512062061"] = "Typ" + +-- Replace plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Plugin ersetzen" + +-- The authors marked this plugin as deprecated: {0} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "Die Autoren haben dieses Plugin als veraltet gekennzeichnet: {0}" + +-- You are about to install a plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T841685558"] = "Sie sind dabei, ein Plugin aus einer Datei zu installieren." + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Abbrechen" + -- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Teilen Sie der KI mit, was sie machen soll. Was sind ihre Ziele oder was möchten Sie erreichen? Zum Beispiel, dass die KI Sie duzt." @@ -7944,6 +7998,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Aktivierte Plugins" -- Revise Assistant Plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "Assistenten-Plugin überarbeiten" +-- Import not possible +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3051566124"] = "Import nicht möglich" + -- The assistant plugin '{0}' has been successfully saved. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "Das Assistent-Plugin „{0}“ wurde erfolgreich gespeichert." @@ -7959,7 +8016,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3342440765"] = "Ihre Organisation ha -- Share plugin archive UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3355474457"] = "Plugin-Archiv teilen" --- Your organization has disabled sharing plugins. +-- Your organization has disabled sharing plugins UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3379469503"] = "Ihre Organisation hat das Teilen von Plugins deaktiviert" -- Close @@ -7977,6 +8034,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Aktionen" -- Export plugin archive UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3872669664"] = "Plugin-Archiv exportieren" +-- Install Plugin +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3902690643"] = "Plugin installieren" + -- Assistant updated. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T40397082"] = "Assistent aktualisiert." @@ -9519,6 +9579,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4 -- Internal assistant plugins cannot be deleted. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Interne Assistenten-Plugins können nicht gelöscht werden." +-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1138181282"] = "Dieses Plugin-Archiv gibt an, von einem Konfigurationsserver verwaltet zu werden. Nur die IT-Abteilung Ihrer Organisation kann solche Plugins bereitstellen." + -- The assistant plugin directory is outside the local assistant plugin directory. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1211881977"] = "Das Assistenten-Plugin-Verzeichnis befindet sich außerhalb des lokalen Assistenten-Plugin-Verzeichnisses." @@ -9552,6 +9615,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223 -- Unexpected error: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unerwarteter Fehler: {0}" +-- Config server managed assistant plugins cannot be replaced. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2594571117"] = "Vom Konfigurationsserver verwaltete Assistenten-Plugins können nicht ersetzt werden." + -- The assistant plugin has no local directory. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "Das Assistenten-Plugin hat kein lokales Verzeichnis." 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 f3492ec2..a3f4b9fd 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 @@ -5361,6 +5361,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embe -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close" +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Close" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." @@ -5451,6 +5454,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T504404155"] = "Accept the ter -- Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking "Accept the GPL and download the archive," you agree to the terms of the GPL license. Software under GPL is free of charge and free to use. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking \"Accept the GPL and download the archive,\" you agree to the terms of the GPL license. Software under GPL is free of charge and free to use." +-- Install plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1525735539"] = "Install plugin" + +-- Version +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1573770551"] = "Version" + +-- Source +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1642243064"] = "Source" + +-- You are about to install a language plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1974491324"] = "You are about to install a language plugin from a file." + +-- Authors +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1985367263"] = "Authors" + +-- Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2053517490"] = "Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source." + +-- You are about to install an assistant plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2063808316"] = "You are about to install an assistant plugin from a file." + +-- You are about to install a configuration plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T21052500"] = "You are about to install a configuration plugin from a file." + +-- You are about to install a theme plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2163853103"] = "You are about to install a theme plugin from a file." + +-- Support contact +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Support contact" + +-- This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3415610475"] = "This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}." + +-- Unknown +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3424652889"] = "Unknown" + +-- Type +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3512062061"] = "Type" + +-- Replace plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Replace plugin" + +-- The authors marked this plugin as deprecated: {0} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "The authors marked this plugin as deprecated: {0}" + +-- You are about to install a plugin from a file. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T841685558"] = "You are about to install a plugin from a file." + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Cancel" + -- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally." @@ -7944,6 +7998,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins" -- Revise Assistant Plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "Revise Assistant Plugin" +-- Import not possible +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3051566124"] = "Import not possible" + -- The assistant plugin '{0}' has been successfully saved. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved." @@ -7959,7 +8016,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3342440765"] = "Your organization ha -- Share plugin archive UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3355474457"] = "Share plugin archive" --- Your organization has disabled sharing plugins. +-- Your organization has disabled sharing plugins UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3379469503"] = "Your organization has disabled sharing plugins" -- Close @@ -7977,6 +8034,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions" -- Export plugin archive UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3872669664"] = "Export plugin archive" +-- Install Plugin +UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3902690643"] = "Install Plugin" + -- Assistant updated. UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T40397082"] = "Assistant updated." @@ -9519,6 +9579,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4 -- Internal assistant plugins cannot be deleted. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Internal assistant plugins cannot be deleted." +-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins." + -- The assistant plugin directory is outside the local assistant plugin directory. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1211881977"] = "The assistant plugin directory is outside the local assistant plugin directory." @@ -9552,6 +9615,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223 -- Unexpected error: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}" +-- Config server managed assistant plugins cannot be replaced. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2594571117"] = "Config server managed assistant plugins cannot be replaced." + -- The assistant plugin has no local directory. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory." diff --git a/app/MindWork AI Studio/Tools/Services/AssistantPluginCheckResult.cs b/app/MindWork AI Studio/Tools/Services/AssistantPluginCheckResult.cs new file mode 100644 index 00000000..112a7ec8 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/AssistantPluginCheckResult.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Services; + +public sealed record AssistantPluginCheckResult(bool Success, Guid PluginId, string PluginName, string Issue); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/AssistantPluginDeleteResult.cs b/app/MindWork AI Studio/Tools/Services/AssistantPluginDeleteResult.cs new file mode 100644 index 00000000..6f57fa06 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/AssistantPluginDeleteResult.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Services; + +public sealed record AssistantPluginDeleteResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallResult.cs b/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallResult.cs new file mode 100644 index 00000000..31bdbb51 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallResult.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Services; + +public sealed record AssistantPluginInstallResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, bool ReplacedExisting, string Issue, bool Cancelled = false); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs b/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs index d9e914f5..df9d7ebb 100644 --- a/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs +++ b/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs @@ -8,14 +8,6 @@ using AIStudio.Tools.Rust; namespace AIStudio.Tools.Services; -public sealed record AssistantPluginInstallResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, bool ReplacedExisting, string Issue); - -public sealed record AssistantPluginCheckResult(bool Success, Guid PluginId, string PluginName, string Issue); - -public sealed record AssistantPluginDeleteResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue); - -public sealed record AssistantPluginUpdateResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue); - public sealed class AssistantPluginInstallService { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginInstallService).Namespace, nameof(AssistantPluginInstallService)); @@ -33,6 +25,8 @@ public sealed class AssistantPluginInstallService private readonly SemaphoreSlim installSemaphore = new(1, 1); private static AssistantPluginInstallResult Error(string issue) => new(false, Guid.Empty, string.Empty, string.Empty, false, issue); + + private static AssistantPluginInstallResult CancelledByUser() => new(false, Guid.Empty, string.Empty, string.Empty, false, string.Empty, true); private static AssistantPluginCheckResult CheckError(string issue) => new(false, Guid.Empty, string.Empty, issue); @@ -40,11 +34,7 @@ public sealed class AssistantPluginInstallService private static AssistantPluginUpdateResult UpdateError(IPluginMetadata plugin, string pluginDirectory, string issue) => new(false, plugin.Id, plugin.Name, pluginDirectory, issue); - public AssistantPluginInstallService( - ILogger logger, - SettingsManager settingsManager, - AssistantSessionService assistantSessionService, - MediaTranscriptionService mediaTranscriptionService) + public AssistantPluginInstallService(ILogger logger, SettingsManager settingsManager, AssistantSessionService assistantSessionService, MediaTranscriptionService mediaTranscriptionService) { this.logger = logger; this.settingsManager = settingsManager; @@ -129,10 +119,9 @@ public sealed class AssistantPluginInstallService return Error(rootIssue); await this.installSemaphore.WaitAsync(token); - AssistantPluginValidationResult validation; try { - validation = await this.ValidateIntoStagingAsync(lua, token); + var validation = await this.ValidateIntoStagingAsync(lua, token); if (!validation.Success || validation.AssistantPlugin is null) return Error(validation.Issue); @@ -149,9 +138,13 @@ public sealed class AssistantPluginInstallService /// Companion files are validated from and moved with the same staging directory. /// /// The local .mwplugin or .zip archive path. + /// + /// Asks the user whether the validated archive may be installed. It is called after all checks + /// passed and before anything gets written. Returning false aborts the installation. + /// /// Cancellation token for extraction, validation, file IO, and plugin reload. /// Installation result that contains success state, installed plugin metadata, and a user-facing issue when installation failed. - public async Task InstallArchiveAsync(string archivePath, CancellationToken token) + public async Task InstallArchiveAsync(string archivePath, Func> confirmAsync, CancellationToken token) { if (!this.settingsManager.ConfigurationData.App.AllowUserToImportPlugins) return Error(TB("Your organization has disabled importing plugins.")); @@ -184,7 +177,7 @@ public sealed class AssistantPluginInstallService var pluginFile = pluginFiles[0]; var pluginDirectory = Path.GetDirectoryName(pluginFile)!; var pluginCode = await File.ReadAllTextAsync(pluginFile, Encoding.UTF8, token); - var validation = await this.ValidateAssistantPluginCodeAsync( + var validation = await ValidateAssistantPluginCodeAsync( pluginDirectory, pluginCode.Trim(), TB("The imported plugin is not an assistant plugin. Issue: {0}"), @@ -201,6 +194,18 @@ public sealed class AssistantPluginInstallService if (validation.AssistantPlugin.IsManagedByConfigServer) return Error(TB("This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.")); + // The archive would replace an existing plugin: reject it when that plugin belongs + // to the IT department. We check this before asking the user, so that the + // confirmation never offers something we would refuse afterwards anyway: + var replacementIssue = GetAssistantReplacementIssue(validation.AssistantPlugin.Id); + if (!string.IsNullOrEmpty(replacementIssue)) + return Error(replacementIssue); + + // Everything is validated, but nothing was written yet. This is the point where the + // user decides, because the plugin code comes from an untrusted source: + if (!await confirmAsync(CreateImportPreview(validation.AssistantPlugin))) + return CancelledByUser(); + return await this.InstallStagedAssistantAsync(assistantPluginsRoot, validation with { StagingDirectory = pluginDirectory }, token); } catch (Exception e) when (e is not OperationCanceledException) @@ -304,9 +309,10 @@ public sealed class AssistantPluginInstallService Directory.Move(pluginDirectory, backupDirectory); wasEnabled = this.settingsManager.ConfigurationData.EnabledPlugins.Remove(plugin.Id); - removedAudits = this.settingsManager.ConfigurationData.AssistantPluginAudits - .Where(audit => audit.PluginId == plugin.Id) - .ToList(); + removedAudits = + [ + .. this.settingsManager.ConfigurationData.AssistantPluginAudits.Where(audit => audit.PluginId == plugin.Id) + ]; if (removedAudits.Count > 0) this.settingsManager.ConfigurationData.AssistantPluginAudits.RemoveAll(audit => audit.PluginId == plugin.Id); @@ -517,7 +523,7 @@ public sealed class AssistantPluginInstallService var stagedPluginFile = Path.Join(stagingDirectory, PLUGIN_FILE_NAME); await File.WriteAllTextAsync(stagedPluginFile, pluginCode, Encoding.UTF8, token); - var validation = await this.ValidateAssistantPluginCodeAsync( + var validation = await ValidateAssistantPluginCodeAsync( stagingDirectory, pluginCode, TB("The generated plugin is not an assistant plugin. Issue: {0}"), @@ -548,7 +554,7 @@ public sealed class AssistantPluginInstallService try { - return await this.ValidateAssistantPluginCodeAsync( + return await ValidateAssistantPluginCodeAsync( pluginDirectory, lua.Trim(), TB("The edited plugin is not an assistant plugin. Issue: {0}"), @@ -563,13 +569,8 @@ public sealed class AssistantPluginInstallService } } - private async Task ValidateAssistantPluginCodeAsync( - string pluginDirectory, - string pluginCode, - string notAssistantIssue, - string invalidAssistantIssue, - string internalPluginIdIssue, - CancellationToken token) + private static async Task ValidateAssistantPluginCodeAsync(string pluginDirectory, string pluginCode, + string notAssistantIssue, string invalidAssistantIssue, string internalPluginIdIssue, CancellationToken token) { var plugin = await PluginFactory.Load(pluginDirectory, pluginCode, token); if (plugin is not PluginAssistants assistantPlugin) @@ -635,11 +636,8 @@ public sealed class AssistantPluginInstallService : TB("The assistant plugin directory does not exist."); } - private void TryDeleteStagingDirectory(string stagingDirectory) - { - TryDeleteDirectory(stagingDirectory, "assistant plugin staging", this.logger); - } - + private void TryDeleteStagingDirectory(string stagingDirectory) => TryDeleteDirectory(stagingDirectory, "assistant plugin staging", this.logger); + private static string DetermineFinalDirectory(string assistantPluginsRoot, PluginAssistants assistantPlugin) { var existingPlugin = FindReplaceableAssistantPlugin(assistantPlugin.Id); @@ -657,6 +655,14 @@ public sealed class AssistantPluginInstallService .OfType() .FirstOrDefault(plugin => plugin.Type is PluginType.ASSISTANT && plugin.Id == pluginId && !plugin.IsInternal); + /// + /// Collects the metadata an archive declares about itself, together with the information about + /// the installed plugin it would replace. + /// + /// The validated assistant plugin from the archive. + /// The preview shown to the user before the installation starts. + private static PluginImportPreview CreateImportPreview(PluginAssistants assistantPlugin) => new(assistantPlugin, FindReplaceableAssistantPlugin(assistantPlugin.Id)); + /// /// Checks whether an installation may replace the assistant plugin that currently uses the given ID. /// Plugins deployed by a Config Server belong to the organization's IT, so neither an import nor @@ -836,4 +842,4 @@ public sealed class AssistantPluginInstallService { public static AssistantPluginValidationResult Failure(string issue) => new(false, string.Empty, null, issue); } -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/AssistantPluginUpdateResult.cs b/app/MindWork AI Studio/Tools/Services/AssistantPluginUpdateResult.cs new file mode 100644 index 00000000..b4612604 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/AssistantPluginUpdateResult.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Services; + +public sealed record AssistantPluginUpdateResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/PluginImportPreview.cs b/app/MindWork AI Studio/Tools/Services/PluginImportPreview.cs new file mode 100644 index 00000000..0fe7d02e --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/PluginImportPreview.cs @@ -0,0 +1,17 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Tools.Services; + +/// +/// What the user gets to see before a plugin archive is installed. It is not bound to a specific +/// plugin type, so it also serves upcoming import paths for other plugin types. +/// +/// The plugin from the archive, with the metadata it declares about itself. +/// The installed plugin that gets replaced or null when the archive adds a new plugin. +public sealed record PluginImportPreview(IPluginMetadata Plugin, IAvailablePlugin? ExistingPlugin) +{ + /// + /// True when an installed plugin with the same ID gets replaced. + /// + public bool ReplacesExisting => this.ExistingPlugin is not null; +} \ No newline at end of file