From e026c03d147cbafe67e3960a96cc567dd9a5ebe2 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 12 Aug 2026 19:02:06 +0200 Subject: [PATCH 01/77] Added Hetzner as an experimental LLM provider (#912) --- .../Assistants/I18N/allTexts.lua | 3 + .../Components/ConfidenceInfo.razor | 8 +- .../Plugins/configuration/plugin.lua | 3 +- .../plugin.lua | 3 + .../plugin.lua | 3 + .../Provider/BaseProvider.cs | 54 +++++------ app/MindWork AI Studio/Provider/Confidence.cs | 6 ++ .../Provider/Hetzner/ProviderHetzner.cs | 93 +++++++++++++++++++ .../Provider/LLMProviders.cs | 1 + .../Provider/LLMProvidersExtensions.cs | 17 ++++ .../Settings/ProviderExtensions.OpenSource.cs | 58 +++++++++++- .../Settings/ProviderExtensions.Reasoning.cs | 1 + .../Settings/ProviderExtensions.cs | 1 + .../Settings/SettingsManager.cs | 7 +- .../wwwroot/changelog/v26.8.1.md | 4 +- 15 files changed, 226 insertions(+), 36 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 3c8925f6..9b3f46f6 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -8896,6 +8896,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust leve -- You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2124364471"] = "You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way." +-- The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2930312134"] = "The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training." + -- The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T3010553924"] = "The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**." diff --git a/app/MindWork AI Studio/Components/ConfidenceInfo.razor b/app/MindWork AI Studio/Components/ConfidenceInfo.razor index 0bf2d044..337cc866 100644 --- a/app/MindWork AI Studio/Components/ConfidenceInfo.razor +++ b/app/MindWork AI Studio/Components/ConfidenceInfo.razor @@ -5,11 +5,11 @@ @if (this.Mode is PopoverTriggerMode.ICON) { - + } else { - + @T("Confidence") } @@ -28,7 +28,7 @@ @T("Description") - + @if (this.currentConfidence.Sources.Count > 0) { @@ -61,7 +61,7 @@ - + Close diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 6851b529..f0337abb 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -634,7 +634,7 @@ CONFIG["SETTINGS"] = {} -- Configure a custom confidence scheme. -- This is used when DataConfidence.ConfidenceScheme is set to CUSTOM. -- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD, --- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG +-- PERPLEXITY, OPEN_ROUTER, HETZNER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG -- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH -- -- Replaces, does not merge: a configuration with a higher priority replaces the whole @@ -651,6 +651,7 @@ CONFIG["SETTINGS"] = {} -- ["ALIBABA_CLOUD"] = "LOW", -- ["PERPLEXITY"] = "MODERATE", -- ["OPEN_ROUTER"] = "MODERATE", +-- ["HETZNER"] = "HIGH", -- ["FIREWORKS"] = "MODERATE", -- ["GROQ"] = "MODERATE", -- ["HUGGINGFACE"] = "MODERATE", 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 65f365f1..e2c436b2 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 @@ -8898,6 +8898,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "Das Vertrauens -- You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2124364471"] = "Sie oder ihre Organisation betreiben das LLM lokal oder innerhalb ihres vertrauenswürdigen Netzwerks. In Bezug auf Datenverarbeitung und Sicherheit ist dies die bestmögliche Lösung." +-- The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2930312134"] = "Der Anbieter betreibt seinen Dienst in der EU und unterliegt der **DSGVO** (Datenschutz-Grundverordnung). Er bietet Zugang zu **Open-Source-Modellen**. Der Dienst befindet sich jedoch derzeit in einer **experimentellen** Phase; Leistung und Verfügbarkeit werden nicht garantiert. Uns liegen keine anbieterspezifischen Informationen dazu vor, ob übermittelte Daten für das Training verwendet werden." + -- The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T3010553924"] = "Der Anbieter hat seinen Sitz in der EU und unterliegt der **DSGVO** (Datenschutz-Grundverordnung). Außerdem gibt der Anbieter an, dass **ihre Daten nicht zum Training verwendet werden**." 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 4787b6ae..df250bce 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 @@ -8898,6 +8898,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust leve -- You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2124364471"] = "You or your organization operate the LLM locally or within your trusted network. In terms of data processing and security, this is the best possible way." +-- The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T2930312134"] = "The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training." + -- The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T3010553924"] = "The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**." diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 4ad26580..2167af66 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -269,10 +269,10 @@ public abstract class BaseProvider : IProvider, ISecretId { exception = new(); - if (!line.StartsWith("data: ", StringComparison.InvariantCulture)) + if (!TryGetServerSentEventData(line, out var jsonData)) return false; - var jsonData = line[6..].Trim(); + jsonData = jsonData.Trim(); if (string.IsNullOrWhiteSpace(jsonData) || jsonData is "[DONE]") return false; @@ -304,6 +304,21 @@ public abstract class BaseProvider : IProvider, ISecretId } } + private static bool TryGetServerSentEventData(string line, out string data) + { + const string DATA_PREFIX = "data:"; + data = string.Empty; + + if (!line.StartsWith(DATA_PREFIX, StringComparison.InvariantCulture)) + return false; + + data = line[DATA_PREFIX.Length..]; + if (data.StartsWith(' ')) + data = data[1..]; + + return true; + } + private static bool IsProviderStreamFailure(JsonElement root) { var eventType = TryGetString(root, "type"); @@ -661,13 +676,13 @@ public abstract class BaseProvider : IProvider, ISecretId if (this.TryCreateProviderRequestExceptionFromStreamLine(providerName, line, out var providerRequestException)) throw providerRequestException; - // Skip lines that do not start with "data: ". Regard + // Skip lines that do not start with "data:". According // to the specification, we only want to read the data lines: - if (!line.StartsWith("data: ", StringComparison.InvariantCulture)) + if (!TryGetServerSentEventData(line, out var jsonData)) continue; // Check if the line is the end of the stream: - if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture)) + if (jsonData is "[DONE]") yield break; // @@ -681,10 +696,6 @@ public abstract class BaseProvider : IProvider, ISecretId try { - // We know that the line starts with "data: ". Hence, we can - // skip the first 6 characters to get the JSON data after that. - var jsonData = line[6..]; - // Deserialize the JSON data: providerResponse = JsonSerializer.Deserialize(jsonData, JSON_SERIALIZER_OPTIONS); @@ -713,10 +724,6 @@ public abstract class BaseProvider : IProvider, ISecretId TDelta? providerResponse; try { - // We know that the line starts with "data: ". Hence, we can - // skip the first 6 characters to get the JSON data after that. - var jsonData = line[6..]; - // Deserialize the JSON data: providerResponse = JsonSerializer.Deserialize(jsonData, JSON_SERIALIZER_OPTIONS); @@ -866,20 +873,19 @@ public abstract class BaseProvider : IProvider, ISecretId if (line.StartsWith("event: response.completed", StringComparison.InvariantCulture)) yield break; + if (!TryGetServerSentEventData(line, out var jsonData)) + continue; + // // Find delta lines: // - if (line.StartsWith(""" - data: {"type":"response.output_text.delta" - """, StringComparison.InvariantCulture)) + if (jsonData.StartsWith(""" + {"type":"response.output_text.delta" + """, StringComparison.InvariantCulture)) { TDelta? providerResponse; try { - // We know that the line starts with "data: ". Hence, we can - // skip the first 6 characters to get the JSON data after that. - var jsonData = line[6..]; - // Deserialize the JSON data: providerResponse = JsonSerializer.Deserialize(jsonData, JSON_SERIALIZER_OPTIONS); @@ -903,18 +909,14 @@ public abstract class BaseProvider : IProvider, ISecretId // // Find annotation added lines: // - else if (annotationSupported && line.StartsWith( + else if (annotationSupported && jsonData.StartsWith( """ - data: {"type":"response.output_text.annotation.added" + {"type":"response.output_text.annotation.added" """, StringComparison.InvariantCulture)) { TAnnotation? providerResponse; try { - // We know that the line starts with "data: ". Hence, we can - // skip the first 6 characters to get the JSON data after that. - var jsonData = line[6..]; - // Deserialize the JSON data: providerResponse = JsonSerializer.Deserialize(jsonData, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/Confidence.cs b/app/MindWork AI Studio/Provider/Confidence.cs index a15fd9ed..600a6e5b 100644 --- a/app/MindWork AI Studio/Provider/Confidence.cs +++ b/app/MindWork AI Studio/Provider/Confidence.cs @@ -64,6 +64,12 @@ public sealed record Confidence Level = ConfidenceLevel.MEDIUM, Description = TB("The provider is located in the EU and is subject to the **GDPR** (General Data Protection Regulation). Additionally, the provider states that **your data is not used for training**."), }; + + public static readonly Confidence GDPR_EXPERIMENTAL_OPEN_SOURCE = new() + { + Level = ConfidenceLevel.MEDIUM, + Description = TB("The provider operates its service in the EU and is subject to the **GDPR** (General Data Protection Regulation). It provides access to **open source models**. However, the service is currently **experimental**, and performance and availability are not guaranteed. We have no provider-specific information about whether submitted data is used for training."), + }; public static readonly Confidence SELF_HOSTED = new() { diff --git a/app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs b/app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs new file mode 100644 index 00000000..a2004f23 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Hetzner/ProviderHetzner.cs @@ -0,0 +1,93 @@ +using System.Runtime.CompilerServices; + +using AIStudio.Chat; +using AIStudio.Provider.OpenAI; +using AIStudio.Settings; + +namespace AIStudio.Provider.Hetzner; + +public sealed class ProviderHetzner() : BaseProvider(LLMProviders.HETZNER, new Uri("https://inference.hetzner.com/api/v1/"), ExternalHttpTrustPolicy.SYSTEM_TRUST_ONLY, LOGGER) +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); + + #region Implementation of IProvider + + /// + public override string Id => LLMProviders.HETZNER.ToSecretId(); + + /// + public override string InstanceName { get; set; } = "Hetzner (Experimental)"; + + /// + public override bool HasModelLoadingCapability => true; + + /// + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + { + await foreach (var content in this.StreamOpenAICompatibleChatCompletion( + "Hetzner", + chatModel, + chatThread, + settingsManager, + async (systemPrompt, apiParameters) => + { + var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel); + + return new ChatCompletionAPIRequest + { + Model = chatModel.Id, + Messages = [systemPrompt, ..messages], + Stream = true, + AdditionalApiParameters = apiParameters + }; + }, + token: token)) + yield return content; + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) + { + yield break; + } + #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + /// + public override Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) + { + return Task.FromResult(TranscriptionResult.Failure()); + } + + /// + public override Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>([]); + } + + /// + public override Task GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return this.LoadModelsResponse(SecretStoreType.LLM_PROVIDER, "models", modelResponse => modelResponse.Data, token, apiKeyProvisional); + } + + /// + public override Task GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(ModelLoadResult.FromModels([])); + } + + /// + public override Task GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(ModelLoadResult.FromModels([])); + } + + /// + public override Task GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(ModelLoadResult.FromModels([])); + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/LLMProviders.cs b/app/MindWork AI Studio/Provider/LLMProviders.cs index 6a560036..740b2f4b 100644 --- a/app/MindWork AI Studio/Provider/LLMProviders.cs +++ b/app/MindWork AI Studio/Provider/LLMProviders.cs @@ -16,6 +16,7 @@ public enum LLMProviders ALIBABA_CLOUD = 12, PERPLEXITY = 14, OPEN_ROUTER = 15, + HETZNER = 16, FIREWORKS = 5, GROQ = 6, diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 92a7860d..4cb91ac1 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -6,6 +6,7 @@ using AIStudio.Provider.Google; using AIStudio.Provider.Groq; using AIStudio.Provider.GWDG; using AIStudio.Provider.Helmholtz; +using AIStudio.Provider.Hetzner; using AIStudio.Provider.HuggingFace; using AIStudio.Provider.Mistral; using AIStudio.Provider.OpenAI; @@ -56,6 +57,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", LLMProviders.PERPLEXITY => "Perplexity", LLMProviders.OPEN_ROUTER => "OpenRouter", + LLMProviders.HETZNER => "Hetzner (Experimental)", LLMProviders.GROQ => "Groq", LLMProviders.FIREWORKS => "Fireworks.ai", @@ -91,6 +93,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", LLMProviders.PERPLEXITY => "Perplexity", LLMProviders.OPEN_ROUTER => "OpenRouter", + LLMProviders.HETZNER => "Hetzner", LLMProviders.GROQ => "Groq", LLMProviders.FIREWORKS => "Fireworks.ai", @@ -144,6 +147,12 @@ public static class LLMProvidersExtensions LLMProviders.OPEN_ROUTER => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://openrouter.ai/privacy", "https://openrouter.ai/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.HETZNER => Confidence.GDPR_EXPERIMENTAL_OPEN_SOURCE.WithRegion("Europe, Germany").WithSources( + "https://experiments.hetzner.com/docs/inference", + "https://www.hetzner.com/legal/privacy-policy/", + "https://www.hetzner.com/legal/terms-and-conditions/" + ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), @@ -180,6 +189,7 @@ public static class LLMProvidersExtensions LLMProviders.HUGGINGFACE => false, LLMProviders.PERPLEXITY => false, LLMProviders.OPEN_ROUTER => true, + LLMProviders.HETZNER => false, // // Self-hosted providers are treated as a special case anyway. @@ -209,6 +219,7 @@ public static class LLMProvidersExtensions // Providers that do not support transcription: // LLMProviders.OPEN_ROUTER => false, + LLMProviders.HETZNER => false, LLMProviders.GROQ => false, LLMProviders.ANTHROPIC => false, LLMProviders.X => false, @@ -271,6 +282,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HETZNER => new ProviderHetzner { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, @@ -302,6 +314,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm", LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api", LLMProviders.OPEN_ROUTER => "https://openrouter.ai/keys", + LLMProviders.HETZNER => "https://experiments.hetzner.com", LLMProviders.GROQ => "https://console.groq.com/", LLMProviders.FIREWORKS => "https://fireworks.ai/login", @@ -327,6 +340,7 @@ public static class LLMProvidersExtensions LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api/", LLMProviders.OPEN_ROUTER => "https://openrouter.ai/activity", LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing", + LLMProviders.HETZNER => "https://experiments.hetzner.com", _ => string.Empty, }; @@ -345,6 +359,7 @@ public static class LLMProvidersExtensions LLMProviders.PERPLEXITY => true, LLMProviders.OPEN_ROUTER => true, LLMProviders.HUGGINGFACE => true, + LLMProviders.HETZNER => true, _ => false, }; @@ -422,6 +437,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => true, LLMProviders.PERPLEXITY => true, LLMProviders.OPEN_ROUTER => true, + LLMProviders.HETZNER => true, LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, @@ -445,6 +461,7 @@ public static class LLMProvidersExtensions LLMProviders.ALIBABA_CLOUD => true, LLMProviders.PERPLEXITY => true, LLMProviders.OPEN_ROUTER => true, + LLMProviders.HETZNER => true, LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index 70be5669..2f8aede9 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -76,6 +76,16 @@ public static partial class ProviderExtensions // if (modelName.IndexOf("deepseek") is not -1) { + if ((modelName.IndexOf("deepseek-v4-flash") is not -1 || + modelName.IndexOf("deepseek-v4-pro") is not -1) && + modelName.IndexOf("-base") is -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + if(modelName.IndexOf("deepseek-r1") is not -1 || modelName.IndexOf("deepseek r1") is not -1) return [ @@ -101,6 +111,16 @@ public static partial class ProviderExtensions Capability.ALWAYS_REASONING, Capability.CHAT_COMPLETION_API, ]; + + // Check for the open-weight Qwen 3.8 checkpoint: + if(modelName.IndexOf("qwen3.8-2.4t-a95b") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + + Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; // Check for Qwen 3.5: if(modelName.IndexOf("qwen3.5") is not -1) @@ -117,11 +137,10 @@ public static partial class ProviderExtensions if(modelName.IndexOf("qwen3.6") is not -1) return [ - Capability.TEXT_INPUT, Capability.VIDEO_INPUT, - Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, - Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, + Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; @@ -139,6 +158,29 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; } + + // + // Moonshot AI / Kimi models: + // + if (modelName.IndexOf("kimi-k3") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT, + Capability.TEXT_OUTPUT, + + Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + if (modelName.IndexOf("kimi-k2.7-code") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + + Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; // // Mistral models: @@ -335,6 +377,16 @@ public static partial class ProviderExtensions // if (modelName.IndexOf("glm") is not -1) { + if (modelName.IndexOf("glm-5.2") is not -1) + return + [ + Capability.TEXT_INPUT, + Capability.TEXT_OUTPUT, + + Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + if(modelName.IndexOf("v") is not -1) return [ diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs index ec95ee9b..437aae9b 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Reasoning.cs @@ -90,6 +90,7 @@ public static partial class ProviderExtensions GetQwenReasoningState(parameters)), LLMProviders.OPEN_ROUTER or + LLMProviders.HETZNER or LLMProviders.X or LLMProviders.DEEP_SEEK or LLMProviders.GROQ or diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.cs index 3d18e586..6dc38cff 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.cs @@ -54,6 +54,7 @@ public static partial class ProviderExtensions LLMProviders.ALIBABA_CLOUD => GetModelCapabilitiesAlibaba(model), LLMProviders.PERPLEXITY => GetModelCapabilitiesPerplexity(model), LLMProviders.OPEN_ROUTER => GetModelCapabilitiesOpenRouter(model), + LLMProviders.HETZNER => GetModelCapabilitiesOpenSource(model), LLMProviders.GROQ => GetModelCapabilitiesOpenSource(model), LLMProviders.FIREWORKS => GetModelCapabilitiesOpenSource(model), diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 43d86255..09081ff0 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -599,8 +599,9 @@ public sealed class SettingsManager { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW, + LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW, - _ => ConfidenceLevel.MEDIUM, + _ => ConfidenceLevel.MEDIUM, }; case ConfidenceSchemes.TRUST_USA: @@ -610,7 +611,9 @@ public sealed class SettingsManager LLMProviders.MISTRAL => ConfidenceLevel.LOW, LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW, LLMProviders.GWDG => ConfidenceLevel.LOW, + LLMProviders.HETZNER => ConfidenceLevel.LOW, LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW, + LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.LOW, _ => ConfidenceLevel.MEDIUM, }; @@ -622,6 +625,7 @@ public sealed class SettingsManager LLMProviders.MISTRAL => ConfidenceLevel.MEDIUM, LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM, LLMProviders.GWDG => ConfidenceLevel.MEDIUM, + LLMProviders.HETZNER => ConfidenceLevel.MEDIUM, _ => ConfidenceLevel.LOW, }; @@ -631,6 +635,7 @@ public sealed class SettingsManager { LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH, LLMProviders.DEEP_SEEK => ConfidenceLevel.MEDIUM, + LLMProviders.ALIBABA_CLOUD => ConfidenceLevel.MEDIUM, _ => ConfidenceLevel.LOW, }; diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 9b4473fd..3119a369 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -1,4 +1,6 @@ # v26.8.1, build 251 (2026-08-xx xx:xx UTC) +- Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API. +- Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8. - Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings. - Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant. - Added a share button for assistants, configurations, and language plugins. It uses the native share dialog on Windows and macOS. For Linux, we added an export option, which stores the plugin archive at a location of your choice. When you work on a translation for a new language, you can now hand your current state to testers or to us with one click. @@ -6,7 +8,7 @@ - Added a delete button for assistants, configurations, and language plugins you installed or placed yourself. Until now, such a plugin could only be removed from the data directory by hand, which was especially painful for configurations because they have no on/off switch. Before deleting a configuration, AI Studio lists what disappears with it, such as providers, data sources, and settings that return to their default. When you delete the language plugin you had chosen, AI Studio returns to choosing your language automatically. Plugins shipped with AI Studio and plugins deployed by your IT department cannot be deleted. - Added options for organizations to disable importing, sharing, and exporting plugins, with a separate option for configuration plugins. Organizations can now let people import assistants while keeping configurations to their IT department. - Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself. -- Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions - either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jan Erler (`j-erler`) and marks his first contribution to AI Studio. Thank you, Jan, for this wonderful and useful contribution. +- Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions - either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (`j-erler`) and marks his first contribution to AI Studio. Thank you, Jan, for this wonderful and useful contribution. - Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new `.config-tests` directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure. - Added CSV and TSV files to the file types you can attach. AI Studio was already able to read them, but they could not be selected. - Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed. From 688fea73cb5d7e746f63fbdf0500aa0d470ff0b3 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 12 Aug 2026 19:29:27 +0200 Subject: [PATCH 02/77] Restored the manual integration test documentation that was deleted (#914) --- .../wwwroot/changelog/v26.8.1.md | 2 +- tests/README.md | 16 +++ tests/integration_tests/README.md | 12 ++ .../chat/chat_rendering_regression_tests.md | 120 ++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 tests/README.md create mode 100644 tests/integration_tests/README.md create mode 100644 tests/integration_tests/chat/chat_rendering_regression_tests.md diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 3119a369..08ce282a 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -39,4 +39,4 @@ - Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log. - Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it. - Removed the legacy PowerPoint format (`.ppt`) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern `.pptx` format is not affected. -- Upgraded dependencies to their latest versions to improve security and stability. +- Upgraded dependencies to their latest versions to improve security and stability. \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..1856f217 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,16 @@ +# Test Documentation + +This directory stores manual and automated test definitions for MindWork AI Studio. + +## Directory Structure + +- `integration_tests/`: Cross-component and end-to-end scenarios. + +## Authoring Rules + +- Use US English. +- Keep each feature area in its own Markdown file. +- Prefer stable test IDs (for example: `TC-CHAT-001`). +- Record expected behavior for: + - known vulnerable baseline builds (if relevant), + - current fixed builds. diff --git a/tests/integration_tests/README.md b/tests/integration_tests/README.md new file mode 100644 index 00000000..aa23175e --- /dev/null +++ b/tests/integration_tests/README.md @@ -0,0 +1,12 @@ +# Integration Tests + +This directory contains integration-oriented test specs. + +## Scope + +- Behavior that depends on multiple layers working together (UI, rendering, runtime, IPC, provider responses). +- Regressions that are hard to catch with unit tests only. + +## Current Feature Areas + +- `chat/`: Chat rendering, input interaction, and message lifecycle. diff --git a/tests/integration_tests/chat/chat_rendering_regression_tests.md b/tests/integration_tests/chat/chat_rendering_regression_tests.md new file mode 100644 index 00000000..ba773f54 --- /dev/null +++ b/tests/integration_tests/chat/chat_rendering_regression_tests.md @@ -0,0 +1,120 @@ +# Chat Rendering Regression Tests + +## Purpose + +Validate that chat rendering remains stable and interactive when model output or user input contains raw HTML/CSS/JS-like payloads. + +## Test Type + +Manual regression and integration checks. + +## Preconditions + +1. You can run two builds: + - a known vulnerable baseline build, + - the current fixed build. +2. At least one provider is configured and can answer prompts. +3. Open the Chat page. + +## Execution Flow (for each test case) + +1. Copy the test prompt exactly into the user prompt field. +2. Send the prompt. +3. Observe behavior immediately after send. +4. If the UI is still visible, type additional text in the prompt input. +5. Repeat on both builds. + +## Test Cases + +### TC-CHAT-001 - CSS Kill Switch + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +UI may turn into a white/invisible page immediately after sending. + +**Expected result (fixed build)** +Chat stays visible and usable. Content is rendered as inert text/code, not active page styling. + +--- + +### TC-CHAT-002 - Full White Overlay + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +UI may become fully white and non-interactive immediately after sending. + +**Expected result (fixed build)** +No overlay takes over the app. Chat remains interactive. + +--- + +### TC-CHAT-003 - Inline Event Handler Injection + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +UI may break/blank immediately after sending. + +**Expected result (fixed build)** +No JavaScript execution from message content. Chat remains stable. + +--- + +### TC-CHAT-004 - SVG Onload Injection Attempt + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +May or may not trigger depending on parser/runtime behavior. + +**Expected result (fixed build)** +No script-like execution from content. Chat remains stable and interactive. + +## Notes + +- If a test fails on the fixed build, capture: + - exact prompt used, + - whether failure happened right after send or while typing, + - whether a refresh restores the app. From ede45103b97cb861a3f50f1bc0f894c7e28dbf2e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 12 Aug 2026 20:44:48 +0200 Subject: [PATCH 03/77] Removed provider access rule suppressions outside the settings manager (#915) --- .../DocumentAnalysisAssistant.razor.cs | 9 +- .../VisualBriefingEditorState.cs | 45 +++++- .../ConfigurationProviderSelection.razor.cs | 25 ++-- .../Components/ProviderSelection.razor.cs | 25 +--- .../Settings/SettingsPanelProviders.razor | 4 +- .../Settings/SettingsPanelProviders.razor.cs | 9 +- .../Dialogs/ProviderDialog.razor.cs | 4 +- .../Dialogs/Settings/SettingsDialogBase.cs | 5 +- .../Settings/SettingsManager.cs | 131 +++++++++++++++--- .../Tools/ComponentsExtensions.cs | 69 +++++---- .../UsageAnalyzers/ProviderAccessAnalyzer.cs | 50 ++++++- 11 files changed, 254 insertions(+), 122 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index 22c71381..6da79ad5 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Diagnostics.CodeAnalysis; using AIStudio.Chat; using AIStudio.Dialogs; @@ -371,11 +370,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore(provider.InstanceName, provider.Id)); } @@ -459,7 +457,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore x.Id == this.selectedPolicy.PreselectedProvider); - if (policyProvider is not null && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) + var policyProvider = this.SettingsManager.GetProviderById(this.selectedPolicy.PreselectedProvider); + if (policyProvider != Settings.Provider.NONE && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) { this.ProviderSettings = policyProvider; this.CurrentProfile = this.ResolveProfileSelection(); diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs index 8666bc12..41ead1f6 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs @@ -1,9 +1,8 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Assistants.SlideBuilder; using AIStudio.Chat; using AIStudio.Settings; +using ComponentKind = AIStudio.Tools.Components; using ProviderSettings = AIStudio.Settings.Provider; namespace AIStudio.Assistants.VisualBriefing; @@ -77,7 +76,6 @@ public sealed class VisualBriefingEditorState /// The manifest to read. /// The settings used to resolve the stored provider and profile. /// The editor state for the briefing. - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "A stored briefing references one specific provider and model by id, so it must be looked up directly instead of using the preselection APIs.")] public static VisualBriefingEditorState FromManifest(VisualBriefingManifest briefing, SettingsManager settingsManager) => new() { Name = briefing.Name, @@ -94,8 +92,8 @@ public sealed class VisualBriefingEditorState ProtectionLevel = briefing.Settings.ProtectionLevel, CustomProtectionLevel = briefing.Settings.CustomProtectionLevel, - Provider = settingsManager.ConfigurationData.Providers.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProviderId && candidate.Model.Id == briefing.Settings.ModelId) ?? ProviderSettings.NONE, - Profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProfileId) ?? Profile.NO_PROFILE, + Provider = ResolveProvider(briefing, settingsManager), + Profile = settingsManager.GetProfileById(briefing.Settings.ProfileId), SourceMaterial = [ @@ -112,6 +110,43 @@ public sealed class VisualBriefingEditorState ], }; + /// + /// Resolves the provider a stored briefing refers to. + /// + /// + /// + /// A briefing stores its provider and model as two separate ids, and both must still match: when + /// the user changed the model of that provider, the stored combination no longer exists and the + /// editor starts without a provider. + /// + /// + /// The resolved provider is additionally checked against the minimum confidence level of the + /// visual briefing assistant. This matters because the confidence settings may have become + /// stricter since the briefing was stored: the user may have lowered the confidence of that + /// provider, or may now enforce a global minimum. Without this check, opening an old briefing + /// would silently restore a provider the user no longer trusts, bypassing the filtering that + /// the provider dropdown applies. Note that the component minimum already covers the enforced + /// global minimum as well. + /// + /// + /// The manifest to read. + /// The settings used to resolve the provider. + /// The stored provider, or when it is unavailable or no longer trusted. + private static ProviderSettings ResolveProvider(VisualBriefingManifest briefing, SettingsManager settingsManager) + { + var storedProvider = settingsManager.GetProviderById(briefing.Settings.ProviderId); + if (storedProvider == ProviderSettings.NONE) + return ProviderSettings.NONE; + + if (storedProvider.Model.Id != briefing.Settings.ModelId) + return ProviderSettings.NONE; + + if (!settingsManager.IsProviderConfident(storedProvider, ComponentKind.VISUAL_BRIEFING_ASSISTANT)) + return ProviderSettings.NONE; + + return storedProvider; + } + /// /// Creates the persisted settings for this editor state. /// diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs index 8267219c..722ad15f 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; @@ -35,27 +33,20 @@ public partial class ConfigurationProviderSelection : MSGComponentBase [Parameter] public Func IsLocked { get; set; } = () => false; - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private IEnumerable> FilteredData() { if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS) yield return new(T("Use app default"), string.Empty); - - // Get the minimum confidence level for this component, and/or the enforced global minimum confidence level: - var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component); - - // Apply the explicit minimum confidence level if set and higher than the current minimum level: - if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel) - minimumLevel = this.ExplicitMinimumConfidence; - - // Filter the providers based on the minimum confidence level: + + // + // Filter the providers based on the minimum confidence level of this component, the enforced + // global minimum, and the explicit minimum level when it is higher. Providers which no longer + // exist resolve to `Provider.NONE` and are dropped by the confidence check as well: + // foreach (var providerId in this.Data) { - var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value); - if (provider is null) - continue; - - if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) + var provider = this.SettingsManager.GetProviderById(providerId.Value); + if (this.SettingsManager.IsProviderConfident(provider, this.Component, this.ExplicitMinimumConfidence)) yield return providerId; } } diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs index de7b668c..20313116 100644 --- a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Provider; using AIStudio.Settings; @@ -83,7 +81,6 @@ public partial class ProviderSelection : MSGComponentBase _ => this.T("Uses reasoning (thinking)"), }; - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private IEnumerable GetAvailableProviders() { switch (this.Component) @@ -91,25 +88,17 @@ public partial class ProviderSelection : MSGComponentBase case null: this.Logger.LogError("Component is null! Cannot filter providers based on component settings. Missed CascadingParameter?"); yield break; - + case Tools.Components.NONE: this.Logger.LogError("Component is NONE! Cannot filter providers based on component settings. Used wrong component?"); yield break; - + case { } component: - - // Get the minimum confidence level for this component, and/or the global minimum if enforced: - var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(component); - - // Override with the explicit minimum level if set and higher: - if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel) - minimumLevel = this.ExplicitMinimumConfidence; - - // Filter providers based on the minimum confidence level: - foreach (var provider in this.SettingsManager.ConfigurationData.Providers) - if (provider.UsedLLMProvider != LLMProviders.NONE) - if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) - yield return provider; + + // Filter providers based on the minimum confidence level of this component, the + // enforced global minimum, and the explicit minimum level when it is higher: + foreach (var provider in this.SettingsManager.GetConfidentProviders(component, this.ExplicitMinimumConfidence)) + yield return provider; break; } } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index 5ec93e3e..92030f3f 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -9,7 +9,7 @@ @T("What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.") - + @@ -66,7 +66,7 @@ - @if(this.SettingsManager.ConfigurationData.Providers.Count == 0) + @if(this.SettingsManager.GetAllProviders().Count == 0) { @T("No providers configured yet.") diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index 4e86eed9..9e073563 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -27,7 +27,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase #endregion - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")] private async Task AddLLMProvider() { var dialogParameters = new DialogParameters @@ -50,7 +50,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")] private async Task EditLLMProvider(AIStudio.Settings.Provider provider) { if(provider == AIStudio.Settings.Provider.NONE) @@ -94,7 +94,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")] private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider) { var dialogParameters = new DialogParameters @@ -156,11 +156,10 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName; } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private async Task UpdateProviders() { this.AvailableLLMProviders.Clear(); - foreach (var provider in this.SettingsManager.ConfigurationData.Providers) + foreach (var provider in this.SettingsManager.GetAllProviders()) this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id)); await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders); diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index efa32f91..bd88ba21 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -201,9 +201,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); // Load the used instance names: - #pragma warning disable MWAIS0001 - this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList(); - #pragma warning restore MWAIS0001 + this.UsedInstanceNames = this.SettingsManager.GetAllProviders().Select(x => x.InstanceName.ToLowerInvariant()).ToList(); this.capabilityOverrides = this.DataCapabilityOverrides ?? new(); this.showExpertSettings = !string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters) || this.capabilityOverrides.HasOverrides; diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs index bb214e1f..d93a0263 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Components; using AIStudio.Settings; using AIStudio.Tools.Services; @@ -40,11 +38,10 @@ public abstract class SettingsDialogBase : MSGComponentBase protected void Close() => this.MudDialog.Cancel(); - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private void UpdateProviders() { this.AvailableLLMProviders.Clear(); - foreach (var provider in this.SettingsManager.ConfigurationData.Providers) + foreach (var provider in this.SettingsManager.GetAllProviders()) this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id)); } diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 09081ff0..8eb924bb 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Text.Json; @@ -434,7 +433,6 @@ public sealed class SettingsManager return localeTag[..separatorIndex]; } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false) { var minimumLevel = this.GetMinimumConfidenceLevel(component); @@ -486,15 +484,27 @@ public sealed class SettingsManager return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE; } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] public Provider GetChatProviderForLoadedChat(string? chatProviderId = null) { var minimumLevel = this.GetMinimumConfidenceLevel(Tools.Components.CHAT); - bool IsSelectableProvider(Provider provider) => - provider != Provider.NONE - && provider.UsedLLMProvider != LLMProviders.NONE - && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel; + var chatProvider = FindProviderById(chatProviderId); + if (chatProvider is not null) + return chatProvider; + + var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions + ? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider) + : null; + + if (defaultChatProvider is not null) + return defaultChatProvider; + + var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider); + if (defaultAppProvider is not null) + return defaultAppProvider; + + var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList(); + return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE; Provider? FindProviderById(string? providerId) { @@ -505,22 +515,103 @@ public sealed class SettingsManager return provider is not null && IsSelectableProvider(provider) ? provider : null; } - var chatProvider = FindProviderById(chatProviderId); - if (chatProvider is not null) - return chatProvider; + bool IsSelectableProvider(Provider provider) => + provider != Provider.NONE + && provider.UsedLLMProvider != LLMProviders.NONE + && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel; + } - var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions - ? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider) - : null; - if (defaultChatProvider is not null) - return defaultChatProvider; + /// + /// Returns all configured providers without applying any confidence filtering. + /// + /// + /// + /// This method applies neither the global minimum confidence level (see + /// with EnforceGlobalMinimumConfidence) nor any + /// component-specific minimum. Even when the user enforces a global minimum of, say, + /// , this method still returns every configured provider. + /// That is intentional: this method serves the provider management UI, duplicate-name checks, + /// and the raw select data of provider dropdowns. The dropdowns are filtered afterward by + /// ConfigurationProviderSelection, which calls IsProviderConfident. + /// + /// + /// Whenever a provider is about to be used for an LLM request, do not use this method. Use + /// GetConfidentProviders, GetPreselectedProvider, or GetChatProviderForLoadedChat instead, + /// since they honor the confidence levels. + /// + /// + /// The returned list is the live provider list. It is read-only for callers: adding, editing, + /// or removing providers stays inside the settings UI. + /// + /// + /// All configured providers, unfiltered. + public IReadOnlyList GetAllProviders() => this.ConfigurationData.Providers; - var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider); - if (defaultAppProvider is not null) - return defaultAppProvider; + /// + /// Returns the provider with the given id, without applying any confidence filtering. + /// + /// + /// This method resolves a stored provider reference by its id. It applies neither the global + /// minimum confidence level nor any component-specific minimum, so it returns the requested + /// provider even when the user enforces a higher global minimum. Callers that intend to use the + /// returned provider for an LLM request must check it themselves through + /// IsProviderConfident or fall back to GetPreselectedProvider. + /// + /// The id of the provider to look up. + /// The provider, or when no provider with that id exists. + public Provider GetProviderById(string? providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + return Provider.NONE; - var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList(); - return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE; + if (string.Equals(providerId, Provider.NONE.Id, StringComparison.OrdinalIgnoreCase)) + return Provider.NONE; + + return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id.Equals(providerId, StringComparison.OrdinalIgnoreCase)) ?? Provider.NONE; + } + + /// + /// Determines the minimum confidence level a provider must have for the given component. + /// + /// The component for which the providers get filtered. + /// An explicit minimum level, which is applied when it is higher than the component's minimum. + /// The effective minimum confidence level. + public ConfidenceLevel GetEffectiveMinimumConfidenceLevel(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) + { + var minimumLevel = this.GetMinimumConfidenceLevel(component); + if (explicitMinimum is not ConfidenceLevel.UNKNOWN && explicitMinimum > minimumLevel) + return explicitMinimum; + + return minimumLevel; + } + + /// + /// Checks whether the given provider satisfies the minimum confidence level of the given component. + /// + /// The provider to check. + /// The component for which the provider gets checked. + /// An explicit minimum level, which is applied when it is higher than the component's minimum. + /// True, when the provider may be used by the component, false otherwise. + public bool IsProviderConfident(Provider provider, Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) + { + if (provider.UsedLLMProvider is LLMProviders.NONE) + return false; + + return provider.UsedLLMProvider.GetConfidence(this).Level >= this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum); + } + + /// + /// Returns all providers that satisfy the minimum confidence level of the given component. + /// + /// The component for which the providers get filtered. + /// An explicit minimum level, which is applied when it is higher than the component's minimum. + /// All providers the component may use. + public IEnumerable GetConfidentProviders(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) + { + var minimumLevel = this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum); + foreach (var provider in this.ConfigurationData.Providers) + if (provider.UsedLLMProvider is not LLMProviders.NONE && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) + yield return provider; } public Profile GetPreselectedProfile(Tools.Components component) diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index 1dc1e5c9..a47823ec 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Settings.DataModel; @@ -164,48 +163,42 @@ public static class ComponentsExtensions _ => default, }; - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] - public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) + public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch { - var preselectedProvider = component switch - { - Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : null, - Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : null, - Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : null, - Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : null, - Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : null, - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : null, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : null, - Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : null, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : null, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : null, - Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : null, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : null, - Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : null, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null, - Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null, - Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : null, - Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider), - - // The Document Analysis Assistant does not have a preselected provider at the component level. - // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. - Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE, + Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE, + Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE, + Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE, + Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE, + Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE, + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE, + Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE, + Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE, + Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE, + Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE, + Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider), - Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : null, + // The Document Analysis Assistant does not have a preselected provider at the component level. + // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. + Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE, - Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : null, + Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE, - Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : null, - Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : null, - Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : null, - Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider), + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE, - _ => Settings.Provider.NONE, - }; - - return preselectedProvider ?? Settings.Provider.NONE; - } + Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider), + + _ => Settings.Provider.NONE, + }; public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager) { diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs index a2db69df..2c57bd33 100644 --- a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs +++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs @@ -17,11 +17,16 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer private static readonly string TITLE = "Direct access to `Providers` is not allowed"; - private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetPreselectedProvider`, etc."; + private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetAllProviders`, `GetProviderById`, `GetConfidentProviders`, `GetPreselectedProvider`, or `GetChatProviderForLoadedChat`."; private static readonly string DESCRIPTION = MESSAGE_FORMAT; private const string CATEGORY = "Usage"; + + /// + /// The one type which owns the provider list and is therefore allowed to access it directly. + /// + private const string OWNING_TYPE = "AIStudio.Settings.SettingsManager"; private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION); @@ -29,7 +34,12 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + // + // We analyze generated code as well, because Razor markup ends up in generated files. Without + // this, any `ConfigurationData.Providers` access written directly in a `.razor` file would + // bypass this rule entirely. The Razor compiler maps the diagnostic back to the `.razor` line: + // + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(this.AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); } @@ -42,8 +52,17 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer if (memberAccess.Name.Identifier.Text != "Providers") return; + // + // The settings manager owns the provider list: it implements the very APIs which all other + // code is meant to use, so it must access `Providers` directly. Exempting it here keeps + // those implementations free of suppression attributes, which would otherwise read as if + // suppressing this rule was a normal thing to do: + // + if (IsOwningType(context.ContainingSymbol)) + return; + // Get the full path of the member access: - var fullPath = this.GetFullMemberAccessPath(memberAccess); + var fullPath = GetFullMemberAccessPath(memberAccess); // Check for the forbidden pattern: if (fullPath.EndsWith("ConfigurationData.Providers")) @@ -53,7 +72,30 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer } } - private string GetFullMemberAccessPath(ExpressionSyntax expression) + /// + /// Checks whether the analyzed node sits inside the type which owns the provider list. + /// + /// + /// The containing symbol is the member the node belongs to, e.g. a method or a property. We walk + /// the chain of containing types so that nested types of the owning type are covered as well. + /// + /// The symbol containing the analyzed node, which may be null. + /// True, when the node belongs to the owning type. + private static bool IsOwningType(ISymbol? containingSymbol) + { + var containingType = containingSymbol as INamedTypeSymbol ?? containingSymbol?.ContainingType; + while (containingType != null) + { + if (containingType.ToDisplayString() == OWNING_TYPE) + return true; + + containingType = containingType.ContainingType; + } + + return false; + } + + private static string GetFullMemberAccessPath(ExpressionSyntax expression) { var parts = new List(); while (expression is MemberAccessExpressionSyntax memberAccess) From 98c86b94f51bb15b47a93cfdc1e56fa811c68dea Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 12 Aug 2026 21:57:58 +0200 Subject: [PATCH 04/77] Sort and group model lists by provider (#916) --- .../Assistants/I18N/allTexts.lua | 9 ---- .../Settings/SettingsPanelApp.razor.cs | 2 +- .../Settings/SettingsPanelEmbeddings.razor | 13 +++--- .../Settings/SettingsPanelEmbeddings.razor.cs | 13 +++++- .../Settings/SettingsPanelProviders.razor | 13 +++--- .../Settings/SettingsPanelProviders.razor.cs | 12 +++++ .../Settings/SettingsPanelTranscription.razor | 13 +++--- .../SettingsPanelTranscription.razor.cs | 14 +++++- .../Dialogs/Settings/SettingsDialogBase.cs | 2 +- .../plugin.lua | 9 ---- .../plugin.lua | 9 ---- .../Settings/SettingsManager.cs | 46 +++++++++++++++++-- app/MindWork AI Studio/wwwroot/app.css | 21 +++++++++ .../wwwroot/changelog/v26.8.1.md | 1 + 14 files changed, 120 insertions(+), 57 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 9b3f46f6..498233c5 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4357,9 +4357,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T80509 -- Example text to embed UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Export configuration" @@ -4432,9 +4429,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration" @@ -4495,9 +4489,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78 -- Are you sure you want to delete the transcription provider '{0}'? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T789660305"] = "Are you sure you want to delete the transcription provider '{0}'?" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Export configuration" diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index a05a4e98..cb2a6ef6 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -91,7 +91,7 @@ public partial class SettingsPanelApp : SettingsPanelBase yield return new(T("Disable dictation and transcription"), string.Empty); var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(Tools.Components.APP_SETTINGS); - foreach (var provider in this.SettingsManager.ConfigurationData.TranscriptionProviders) + foreach (var provider in this.SettingsManager.GetAllTranscriptionProviders()) { if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) yield return new(provider.Name, provider.Id); diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor index f89c07d0..71f0e618 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor @@ -17,25 +17,24 @@ @T("This helps AI Studio understand and compare things in a way that's similar to how humans do. When you're working on something, AI Studio can automatically identify related documents and data by comparing their digital fingerprints. For instance, if you're writing about customer service, AI Studio can instantly find other documents in your data that discuss similar topics or experiences, even if they use different words.") - + - - - # @T("Name") - @T("Provider") @T("Model") @T("Actions") + + + @context.Key + + - @context.Num @context.Name - @context.UsedLLMProvider.ToName() @this.GetEmbeddingProviderModelName(context) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index 775b2ad9..f7b88c14 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -11,6 +11,17 @@ namespace AIStudio.Components.Settings; public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase { + /// + /// Groups the table by the used LLM provider. The embedding provider list is already sorted by + /// that provider, so all instances of one LLM provider form a single, coherent group. + /// + private static readonly TableGroupDefinition GROUP_CONFIG = new() + { + Expandable = true, + IsInitiallyExpanded = false, + Selector = provider => provider.UsedLLMProvider.ToName(), + }; + [Parameter] public List> AvailableEmbeddingProviders { get; set; } = new(); @@ -131,7 +142,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase private async Task UpdateEmbeddingProviders() { this.AvailableEmbeddingProviders.Clear(); - foreach (var provider in this.SettingsManager.ConfigurationData.EmbeddingProviders) + foreach (var provider in this.SettingsManager.GetAllEmbeddingProviders()) this.AvailableEmbeddingProviders.Add(new (provider.Name, provider.Id)); await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders); diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index 92030f3f..426929f0 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -9,25 +9,24 @@ @T("What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.") - + - - - # @T("Instance Name") - @T("Provider") @T("Model") @T("Actions") + + + @context.Key + + - @context.Num @context.InstanceName - @context.UsedLLMProvider.ToName() @this.GetLLMProviderModelName(context) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index 9e073563..a308ab5a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using AIStudio.Dialogs; +using AIStudio.Provider; using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -11,6 +12,17 @@ namespace AIStudio.Components.Settings; public partial class SettingsPanelProviders : SettingsPanelProviderBase { + /// + /// Groups the table by the used LLM provider. The provider list is already sorted by that + /// provider, so all instances of one LLM provider form a single, coherent group. + /// + private static readonly TableGroupDefinition GROUP_CONFIG = new() + { + Expandable = true, + IsInitiallyExpanded = false, + Selector = provider => provider.UsedLLMProvider.ToName(), + }; + [Parameter] public List> AvailableLLMProviders { get; set; } = new(); diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor index f0a9c6f2..93d22ba1 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor @@ -13,25 +13,24 @@ @T("With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure providers' section.") - + - - - # @T("Name") - @T("Provider") @T("Model") @T("Actions") + + + @context.Key + + - @context.Num @context.Name - @context.UsedLLMProvider.ToName() @this.GetTranscriptionProviderModelName(context) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs index e143ba82..25e3120b 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Dialogs; +using AIStudio.Provider; using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -9,6 +10,17 @@ namespace AIStudio.Components.Settings; public partial class SettingsPanelTranscription : SettingsPanelProviderBase { + /// + /// Groups the table by the used LLM provider. The transcription provider list is already sorted by + /// that provider, so all instances of one LLM provider form a single, coherent group. + /// + private static readonly TableGroupDefinition GROUP_CONFIG = new() + { + Expandable = true, + IsInitiallyExpanded = false, + Selector = provider => provider.UsedLLMProvider.ToName(), + }; + [Parameter] public List> AvailableTranscriptionProviders { get; set; } = new(); @@ -129,7 +141,7 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase private async Task UpdateTranscriptionProviders() { this.AvailableTranscriptionProviders.Clear(); - foreach (var provider in this.SettingsManager.ConfigurationData.TranscriptionProviders) + foreach (var provider in this.SettingsManager.GetAllTranscriptionProviders()) this.AvailableTranscriptionProviders.Add(new (provider.Name, provider.Id)); await this.AvailableTranscriptionProvidersChanged.InvokeAsync(this.AvailableTranscriptionProviders); diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs index d93a0263..ef4967bc 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs @@ -48,7 +48,7 @@ public abstract class SettingsDialogBase : MSGComponentBase private void UpdateEmbeddingProviders() { this.AvailableEmbeddingProviders.Clear(); - foreach (var provider in this.SettingsManager.ConfigurationData.EmbeddingProviders) + foreach (var provider in this.SettingsManager.GetAllEmbeddingProviders()) this.AvailableEmbeddingProviders.Add(new (provider.Name, provider.Id)); } 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 e2c436b2..f99097b6 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 @@ -4359,9 +4359,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T80509 -- Example text to embed UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Beispieltext zum Einbetten" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Konfiguration exportieren" @@ -4434,9 +4431,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Dashboard öffnen" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Anbieter" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Konfiguration exportieren" @@ -4497,9 +4491,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78 -- Are you sure you want to delete the transcription provider '{0}'? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T789660305"] = "Möchten Sie den Anbieter für Transkriptionen „{0}“ wirklich löschen?" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Anbieter" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Konfiguration exportieren" 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 df250bce..7d01f7fd 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 @@ -4359,9 +4359,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T80509 -- Example text to embed UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T975426229"] = "Export configuration" @@ -4434,9 +4431,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T975426229"] = "Export configuration" @@ -4497,9 +4491,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T78 -- Are you sure you want to delete the transcription provider '{0}'? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T789660305"] = "Are you sure you want to delete the transcription provider '{0}'?" --- Provider -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T900237532"] = "Provider" - -- Export configuration UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T975426229"] = "Export configuration" diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 8eb924bb..286c4fa6 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -540,12 +540,18 @@ public sealed class SettingsManager /// since they honor the confidence levels. /// /// - /// The returned list is the live provider list. It is read-only for callers: adding, editing, - /// or removing providers stays inside the settings UI. + /// The returned list is a sorted copy of the provider list, ordered by the used LLM provider and + /// then by the instance name. This way, all providers of the same LLM provider stay together, and + /// newly added providers appear at their alphabetical position instead of at the end. Callers must + /// not mutate the returned list: adding, editing, or removing providers stays inside the settings UI. /// /// /// All configured providers, unfiltered. - public IReadOnlyList GetAllProviders() => this.ConfigurationData.Providers; + public IReadOnlyList GetAllProviders() => this.ConfigurationData.Providers + .OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase) + .ThenBy(x => x.InstanceName, StringComparer.OrdinalIgnoreCase) + .ThenBy(x => x.Num) + .ToList(); /// /// Returns the provider with the given id, without applying any confidence filtering. @@ -605,15 +611,45 @@ public sealed class SettingsManager /// /// The component for which the providers get filtered. /// An explicit minimum level, which is applied when it is higher than the component's minimum. - /// All providers the component may use. + /// All providers the component may use, in the same order as GetAllProviders. public IEnumerable GetConfidentProviders(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) { var minimumLevel = this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum); - foreach (var provider in this.ConfigurationData.Providers) + foreach (var provider in this.GetAllProviders()) if (provider.UsedLLMProvider is not LLMProviders.NONE && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) yield return provider; } + /// + /// Returns all configured embedding providers. + /// + /// + /// The returned list is a sorted copy of the embedding provider list, ordered by the used LLM + /// provider and then by the name. Callers must not mutate the returned list: adding, editing, or + /// removing embedding providers stays inside the settings UI. + /// + /// All configured embedding providers. + public IReadOnlyList GetAllEmbeddingProviders() => this.ConfigurationData.EmbeddingProviders + .OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase) + .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .ThenBy(x => x.Num) + .ToList(); + + /// + /// Returns all configured transcription providers. + /// + /// + /// The returned list is a sorted copy of the transcription provider list, ordered by the used LLM + /// provider and then by the name. Callers must not mutate the returned list: adding, editing, or + /// removing transcription providers stays inside the settings UI. + /// + /// All configured transcription providers. + public IReadOnlyList GetAllTranscriptionProviders() => this.ConfigurationData.TranscriptionProviders + .OrderBy(x => x.UsedLLMProvider.ToName(), StringComparer.OrdinalIgnoreCase) + .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .ThenBy(x => x.Num) + .ToList(); + public Profile GetPreselectedProfile(Tools.Components component) { var preselection = component.GetProfilePreselection(this); diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index 0a06f9e6..5d97fdea 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -410,4 +410,25 @@ .code-editor .lua-variable { color: var(--mw-code-editor-variable, #267f99); +} + +/* + * Group headers of the provider tables in the settings (LLM providers, embedding providers, + * transcription providers). The rule targets the entire group row, so that the expand button + * gets the same compact padding and shading as the header cell itself. + */ +tr:has(> .provider-group-header) > .mud-table-cell { + padding-top: 0.25em; + padding-bottom: 0.25em; + background-color: var(--mud-palette-background-gray); + border-top: 1px solid var(--mud-palette-lines-default); + border-bottom: 1px solid var(--mud-palette-lines-default); +} + +tr:has(> .provider-group-header) .mud-icon-button { + padding: 0.15em; +} + +.provider-group-header { + font-weight: 600; } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 08ce282a..6afbd4ee 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -14,6 +14,7 @@ - Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed. - Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does. - Improved how Word documents (`.docx`) and OpenDocument text files (`.odt`) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (`nilskruthoff`), who also wrote the library behind it. Thank you, Nils, for this great contribution. +- Improved the order of your models. Every list of models is now sorted by provider and name, so all models of one provider stay together. Until now, models appeared in the order they were created, which meant that a model added later always showed up at the end of the list. This was especially confusing when your organization rolled out new models. In the settings, the tables for LLM providers, embeddings, and transcription are now grouped by provider and start collapsed, so even a long list stays easy to survey. - Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants. - Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example, an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file. - Fixed files that are open in another program being reported as an unrecognized file type. AI Studio now tells you that the file is currently open elsewhere and asks you to close it. This also works for files on shared network drives, where a colleague might have the file open. From 6001cb5f4247b82eebb1a18b38d1532fdd806e64 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 13 Aug 2026 08:10:05 +0200 Subject: [PATCH 05/77] Prepared release v26.8.1 (#917) --- app/Build/Build Script.csproj | 2 +- .../Components/Changelog.Logs.cs | 1 + .../MindWork AI Studio.csproj | 4 +-- app/MindWork AI Studio/packages.lock.json | 36 +++++++++---------- .../wwwroot/changelog/v26.8.1.md | 6 ++-- .../wwwroot/changelog/v26.8.2.md | 1 + metadata.txt | 12 +++---- runtime/Cargo.lock | 2 +- runtime/Cargo.toml | 2 +- runtime/tauri.conf.json | 2 +- 10 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md diff --git a/app/Build/Build Script.csproj b/app/Build/Build Script.csproj index 5a184f2d..0de6dac1 100644 --- a/app/Build/Build Script.csproj +++ b/app/Build/Build Script.csproj @@ -14,7 +14,7 @@ - + diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index cfdd0fd4..a7fe8827 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,6 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ + new (251, "v26.8.1, build 251 (2026-08-13 06:01 UTC)", "v26.8.1.md"), new (250, "v26.7.3, build 250 (2026-07-21 12:45 UTC)", "v26.7.3.md"), new (244, "v26.7.2, build 244 (2026-07-06 18:35 UTC)", "v26.7.2.md"), new (243, "v26.7.1, build 243 (2026-07-05 16:39 UTC)", "v26.7.1.md"), diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index 61b86357..f104b82c 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -53,11 +53,11 @@ - + - + diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index aa38ead2..659e11fa 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -22,21 +22,21 @@ }, "LuaCSharp": { "type": "Direct", - "requested": "[0.5.5, )", - "resolved": "0.5.5", - "contentHash": "IL44DCbMtEafyiy8DzHFd/f+1pXuDUVFJMCJPAu8vQHNfO3ADSoWSOKMg9Py1za/ZE1K0gs0jll1viInoN+19Q==", + "requested": "[0.5.6, )", + "resolved": "0.5.6", + "contentHash": "ncwP3iXeonYM7bILBjsPPIk12mfvCO4tBFnrt9bjnKEZ8Ugdp+MlKskRUGEbo/WHGKU2dRlhxEyuN17L2PHYfg==", "dependencies": { - "LuaCSharp.Annotations": "0.5.5", - "LuaCSharp.SourceGenerator": "0.5.5" + "LuaCSharp.Annotations": "0.5.6", + "LuaCSharp.SourceGenerator": "0.5.6" } }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Direct", - "requested": "[9.0.18, )", - "resolved": "9.0.18", - "contentHash": "+t0Bq5qZZ/zbmO4X70nDMC+anTsNSCxNvjtqXmRiUwh53cNfMoXkB/R95rUO9+yFYhsTR7B302ys9LqXDdIt6g==", + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "Q8pv8Md+VH64ZJM2d3nbLLChTZK7WOq+5Ykp6GpDe11cDSCSYhEiHbw69CigoAT5VYJPNxriZMpPL7dY8UW93Q==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.18" + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.19" } }, "Microsoft.NET.ILLink.Tasks": { @@ -82,13 +82,13 @@ }, "LuaCSharp.Annotations": { "type": "Transitive", - "resolved": "0.5.5", - "contentHash": "5VcwcTNGCY5YXLz2BRko5/Z0YGd6MZqNsnnfPOsGHHpAtqWPFbD0vtOZR4jUqaQLtQUvl2+WRfmIOhp6L2S0rw==" + "resolved": "0.5.6", + "contentHash": "tb8JLViDSSHpmMmBpxqbr+y+4Dpu6v5p3in7RlAUIi+YD7CMi7BEEI7fTc6o5lzoJxSMbIdzzGzjzXXMF6aaSg==" }, "LuaCSharp.SourceGenerator": { "type": "Transitive", - "resolved": "0.5.5", - "contentHash": "2xHKGc1bYXTsmSzZCNmKkuAU6A+1azulNiPY/ICKBSHIgEPMNRQ7JS6PvAClrHe6bk8SKcC/fbba6igtDzDaAw==" + "resolved": "0.5.6", + "contentHash": "IJLlWaIYdpvZ5zO20DidDusqMl5pnoHzrmNmB7UnWMWqOfz1t8LeGfDH8kFTDpGcveFE8YUNeEUa4j6SmWGUPg==" }, "Markdig": { "type": "Transitive", @@ -159,10 +159,10 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.18", - "contentHash": "YqkFlTwnVSMuunsf8IT9b+KySfm6vnMBBM+CKYCfXfjRMQ62uFggVOEu4C2cgR4fXpEO1rZ6utUZC1KoYKgiSg==", + "resolved": "9.0.19", + "contentHash": "raArfuC+4kERkNxWrlCXO7H+QAk5yUtmNxiymr+ItP5HKQfMjLhQ68zdajqmd5kgwIIUiMpPGIhUpf7t9+cC0w==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.18" + "Microsoft.Extensions.Primitives": "9.0.19" } }, "Microsoft.Extensions.Localization": { @@ -200,8 +200,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.18", - "contentHash": "hfHudMC5zDlwMrC0HiHOJesSHMvM+CdqjomjcV/YVzFq5dfSpBRvyRLm1n1Bfh41ZpQnyJzqX+YEo95BAmcDAQ==" + "resolved": "9.0.19", + "contentHash": "9hIg8PQiMnpVFIsEHm25Wi1gBrPa2pS1G75uveyDUVLXrNKqBap9WGwQIgO0s6LUgRAOdsSYnbKbNC5b1G5Pcg==" }, "Microsoft.JSInterop": { "type": "Transitive", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 6afbd4ee..9fdb6cf9 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -1,4 +1,4 @@ -# v26.8.1, build 251 (2026-08-xx xx:xx UTC) +# v26.8.1, build 251 (2026-08-13 06:01 UTC) - Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API. - Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8. - Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings. @@ -8,12 +8,12 @@ - Added a delete button for assistants, configurations, and language plugins you installed or placed yourself. Until now, such a plugin could only be removed from the data directory by hand, which was especially painful for configurations because they have no on/off switch. Before deleting a configuration, AI Studio lists what disappears with it, such as providers, data sources, and settings that return to their default. When you delete the language plugin you had chosen, AI Studio returns to choosing your language automatically. Plugins shipped with AI Studio and plugins deployed by your IT department cannot be deleted. - Added options for organizations to disable importing, sharing, and exporting plugins, with a separate option for configuration plugins. Organizations can now let people import assistants while keeping configurations to their IT department. - Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself. -- Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions - either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (`j-erler`) and marks his first contribution to AI Studio. Thank you, Jan, for this wonderful and useful contribution. +- Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions – either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (`j-erler`) and marks his first contribution to AI Studio. Thank you, Jens, for this wonderful and useful contribution. - Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new `.config-tests` directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure. - Added CSV and TSV files to the file types you can attach. AI Studio was already able to read them, but they could not be selected. - Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed. - Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does. -- Improved how Word documents (`.docx`) and OpenDocument text files (`.odt`) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (`nilskruthoff`), who also wrote the library behind it. Thank you, Nils, for this great contribution. +- Improved how Word documents (`.docx`) and OpenDocument files (`.odt`) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (`nilskruthoff`), who also wrote the library behind it. Thank you, Nils, for this great contribution. - Improved the order of your models. Every list of models is now sorted by provider and name, so all models of one provider stay together. Until now, models appeared in the order they were created, which meant that a model added later always showed up at the end of the list. This was especially confusing when your organization rolled out new models. In the settings, the tables for LLM providers, embeddings, and transcription are now grouped by provider and start collapsed, so even a long list stays easy to survey. - Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants. - Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example, an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file. diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md new file mode 100644 index 00000000..b1c44de3 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -0,0 +1 @@ +# v26.8.2, build 252 (2026-08-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index 7eefd2e1..b8f930ca 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ -26.7.3 -2026-07-21 12:45:10 UTC -250 -9.0.119 (commit 32cc3bdf5e) -9.0.18 (commit d839c41c85) +26.8.1 +2026-08-13 06:01:54 UTC +251 +9.0.120 (commit 3f97250e38) +9.0.19 (commit 8381bdb01f) 1.97.1 (commit 8bab26f4f) 8.15.0 2.11.5 -1e5f07cb010, release +3c789da4654, release osx-arm64 148.0.7763.0 0.7.2 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index efd9001d..a4937e04 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -4260,7 +4260,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mindwork-ai-studio" -version = "26.7.3" +version = "26.8.1" dependencies = [ "aes 0.9.1", "apple-native-keyring-store", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 87a1c55c..c64187ac 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mindwork-ai-studio" -version = "26.7.3" +version = "26.8.1" edition = "2024" description = "MindWork AI Studio" authors = ["Thorsten Sommer"] diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index f3a7bb47..3d9cac39 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -1,7 +1,7 @@ { "productName": "MindWork AI Studio", "mainBinaryName": "MindWork AI Studio", - "version": "26.7.3", + "version": "26.8.1", "identifier": "com.github.mindwork-ai.ai-studio", "build": { From abe450c320e17c7068ed724ac1b90ffae380f49b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 13 Aug 2026 10:43:13 +0200 Subject: [PATCH 06/77] Fixed AppStream metainfo release for Linux Flatpak (#918) --- app/Build/Commands/UpdateMetadataCommands.cs | 201 +++++++++++++++--- .../org.mindworkai.AIStudio.metainfo.xml | 80 ++++++- 2 files changed, 249 insertions(+), 32 deletions(-) diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index 51c5a7e8..1447a64a 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -91,6 +91,40 @@ public sealed partial class UpdateMetadataCommands await this.Build(offline); } + [Command("update-metainfo", Description = "Update the AppStream metainfo entry of one release from its changelog")] + public async Task UpdateMetainfo( + [Option("version", ['v'], Description = "The release version, e.g., 26.1.2. Defaults to the version from the metadata")] string? version = null, + [Option("date", ['d'], Description = "The release date as yyyy-MM-dd. Defaults to the build time from the metadata")] string? date = null) + { + const int APP_VERSION_INDEX = 0; + const int BUILD_TIME_INDEX = 1; + + if(!Environment.IsWorkingDirectoryValid()) + return; + + Console.WriteLine("=============================="); + + try + { + var metadataLines = SplitLines(await File.ReadAllTextAsync(Environment.GetMetadataPath(), Encoding.UTF8)); + var appVersion = string.IsNullOrWhiteSpace(version) ? metadataLines[APP_VERSION_INDEX].Trim() : version.Trim(); + if (!ExactAppVersionRegex().IsMatch(appVersion)) + throw new InvalidOperationException($"The version '{appVersion}' is not a valid app version."); + + DateTime releaseTime; + if (string.IsNullOrWhiteSpace(date)) + releaseTime = ParseMetadataBuildTime(metadataLines[BUILD_TIME_INDEX]); + else if (!DateTime.TryParseExact(date.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out releaseTime)) + throw new InvalidOperationException($"The release date '{date}' is not a valid date in the yyyy-MM-dd format."); + + await WriteMetainfoRelease(appVersion, releaseTime); + } + catch (InvalidOperationException exception) + { + Console.WriteLine($"- Error: {exception.Message}"); + } + } + [Command("update-versions", Description = "The command will update the package versions in the metadata file")] public async Task UpdateVersions() { @@ -154,10 +188,20 @@ public sealed partial class UpdateMetadataCommands var appVersion = await this.UpdateAppVersion(action, version); if (!string.IsNullOrWhiteSpace(appVersion.VersionText)) { + // The changelog is the source for the AppStream description. Check it before we write + // any further metadata, so that a missing changelog cannot leave a half-prepared release: + var changelogPath = GetChangelogPath(appVersion.VersionText); + if (!File.Exists(changelogPath)) + { + Console.WriteLine($"- Error: The changelog file '{Path.GetFileName(changelogPath)}' does not exist."); + return; + } + var buildNumber = await this.IncreaseBuildNumber(); var buildTime = await this.UpdateBuildTime(); await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime); await this.CreateNextChangelog(buildNumber, appVersion); + await WriteMetainfoRelease(appVersion.VersionText, ParseMetadataBuildTime(buildTime)); await this.UpdateProjectCommitHash(); await this.UpdateReleaseDependenciesAndLicence(); Console.WriteLine(); @@ -413,9 +457,7 @@ public sealed partial class UpdateMetadataCommands if (!ExactAppVersionRegex().IsMatch(appVersion)) throw new InvalidOperationException($"The metadata version '{appVersion}' is not a valid app version."); - if (!DateTime.TryParseExact(metadataLines[BUILD_TIME_INDEX].Trim(), "yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var buildTime)) - throw new InvalidOperationException($"The metadata build time '{metadataLines[BUILD_TIME_INDEX]}' is not a valid UTC build time."); - + var buildTime = ParseMetadataBuildTime(metadataLines[BUILD_TIME_INDEX]); if (!int.TryParse(metadataLines[BUILD_NUMBER_INDEX].Trim(), out var buildNumber)) throw new InvalidOperationException($"The metadata build number '{metadataLines[BUILD_NUMBER_INDEX]}' is not a number."); @@ -455,19 +497,15 @@ public sealed partial class UpdateMetadataCommands throw new InvalidOperationException($"Expected exactly one future changelog reserving build {nextChangelogBuildNumber}, but found {nextChangelogCandidates.Count}."); var nextChangelog = nextChangelogCandidates[0]; - var metainfoPath = Path.Combine(Environment.GetRustRuntimeDirectory(), "packaging", "linux", "org.mindworkai.AIStudio.metainfo.xml"); + + // The release entry itself is written by ApplyRebuildReleaseState, which adds it when it is + // missing and moves it to the top otherwise. Here, we only ensure that there is a file to write to: + var metainfoPath = GetMetainfoPath(); if (!File.Exists(metainfoPath)) throw new InvalidOperationException("The AppStream metainfo file does not exist."); - var metainfoContent = await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8); - var releaseTags = ReleaseTagRegex().Matches(metainfoContent).Cast().ToList(); - var matchingReleaseTags = releaseTags.Where(match => ReleaseTagHasVersion(match.Value, appVersion)).ToList(); - if (matchingReleaseTags.Count != 1 || releaseTags.Count == 0 || matchingReleaseTags[0].Index != releaseTags[0].Index) - throw new InvalidOperationException($"The AppStream metainfo must contain v{appVersion} exactly once as its first release."); - - var metainfoReleaseTag = matchingReleaseTags[0].Value; - if (!StableReleaseTypeRegex().IsMatch(metainfoReleaseTag) || !ReleaseDateRegex().IsMatch(metainfoReleaseTag)) - throw new InvalidOperationException($"The AppStream entry for v{appVersion} must be stable and contain a release date."); + if (!ReleasesStartRegex().IsMatch(await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8))) + throw new InvalidOperationException("The AppStream metainfo does not contain a element."); var headCommitHash = (await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD")).Trim(); if (!GitCommitHashRegex().IsMatch(headCommitHash)) @@ -489,9 +527,6 @@ public sealed partial class UpdateMetadataCommands nextChangelog.Content, nextChangelog.Header, nextChangelog.Version, - metainfoPath, - metainfoContent, - metainfoReleaseTag, headCommitHash[..11]); } @@ -530,11 +565,119 @@ public sealed partial class UpdateMetadataCommands await File.WriteAllTextAsync(releaseState.NextChangelogPath, updatedNextChangelog, Environment.UTF8_NO_BOM); Console.WriteLine($"- Reserved build {buildNumber + 1} for '{Path.GetFileName(releaseState.NextChangelogPath)}'."); - var releaseDate = buildTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - var updatedMetainfoReleaseTag = ReleaseDateRegex().Replace(releaseState.MetainfoReleaseTag, $"date=\"{releaseDate}\"", 1); - var updatedMetainfo = ReplaceExactlyOnce(releaseState.MetainfoContent, releaseState.MetainfoReleaseTag, updatedMetainfoReleaseTag); - await File.WriteAllTextAsync(releaseState.MetainfoPath, updatedMetainfo, Environment.UTF8_NO_BOM); - Console.WriteLine($"- Updated the AppStream release date to '{releaseDate}'."); + await WriteMetainfoRelease(releaseState.AppVersion, buildTime); + } + + private static string GetMetainfoPath() => Path.Combine(Environment.GetRustRuntimeDirectory(), "packaging", "linux", "org.mindworkai.AIStudio.metainfo.xml"); + + private static string GetChangelogPath(string appVersion) => Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog", $"v{appVersion}.md"); + + /// + /// Writes the AppStream release entry for the given version, using the changelog of that version as its description. + /// + /// + /// The entry always becomes the first release, and any earlier entry of the same version is replaced. This is what + /// the Flatpak pipeline validates through 'update-metainfo.py --check' before it syncs a release. The release date + /// is derived from the build time, because the pipeline reads it from the second line of the metadata file. + /// + private static async Task WriteMetainfoRelease(string appVersion, DateTime releaseTime) + { + const string RELEASE_INDENT = " "; + + var metainfoPath = GetMetainfoPath(); + if (!File.Exists(metainfoPath)) + throw new InvalidOperationException("The AppStream metainfo file does not exist."); + + var metainfo = await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8); + if (!ReleasesStartRegex().IsMatch(metainfo)) + throw new InvalidOperationException("The AppStream metainfo does not contain a element."); + + var changelogEntries = await ReadChangelogEntries(appVersion); + + // Drop any earlier entry of this version, so that the version stays unique and moves to the top. + // We remove from the back, so that the index of the remaining matches stays valid: + foreach (var previousRelease in ReleaseBlockRegex().Matches(metainfo).Cast().Where(match => ReleaseTagHasVersion(match.Value, appVersion)).Reverse()) + metainfo = metainfo.Remove(previousRelease.Index, previousRelease.Length); + + var lineEnding = metainfo.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n"; + var releaseDate = releaseTime.ToUniversalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + var releaseBlock = new StringBuilder(); + releaseBlock.Append($"{RELEASE_INDENT}{lineEnding}"); + releaseBlock.Append($"{RELEASE_INDENT} {lineEnding}"); + releaseBlock.Append($"{RELEASE_INDENT}
    {lineEnding}"); + + foreach (var changelogEntry in changelogEntries) + releaseBlock.Append($"{RELEASE_INDENT}
  • {changelogEntry}
  • {lineEnding}"); + + releaseBlock.Append($"{RELEASE_INDENT}
{lineEnding}"); + releaseBlock.Append($"{RELEASE_INDENT}
{lineEnding}"); + releaseBlock.Append($"{RELEASE_INDENT}
{lineEnding}"); + + var releasesStart = ReleasesStartRegex().Match(metainfo); + var insertionPoint = releasesStart.Index + releasesStart.Length; + if (metainfo.AsSpan(insertionPoint).StartsWith(lineEnding)) + insertionPoint += lineEnding.Length; + else + releaseBlock.Insert(0, lineEnding); + + metainfo = metainfo.Insert(insertionPoint, releaseBlock.ToString()); + await File.WriteAllTextAsync(metainfoPath, metainfo, Environment.UTF8_NO_BOM); + Console.WriteLine($"- Updated the AppStream metainfo for v{appVersion}, released on {releaseDate}, with {changelogEntries.Count} changelog entries."); + } + + private static async Task> ReadChangelogEntries(string appVersion) + { + var changelogPath = GetChangelogPath(appVersion); + if (!File.Exists(changelogPath)) + throw new InvalidOperationException($"The changelog file '{Path.GetFileName(changelogPath)}' does not exist."); + + // The first line is the changelog header, every other non-empty line must be a changelog entry: + var changelogLines = SplitLines(await File.ReadAllTextAsync(changelogPath, Encoding.UTF8)); + var changelogEntries = new List(); + foreach (var changelogLine in changelogLines.Skip(1)) + { + var changelogEntry = changelogLine.Trim(); + if (changelogEntry.Length is 0) + continue; + + if (!changelogEntry.StartsWith("- ", StringComparison.Ordinal)) + throw new InvalidOperationException($"The changelog '{Path.GetFileName(changelogPath)}' contains a line which is no changelog entry: '{changelogEntry}'."); + + changelogEntries.Add(ConvertChangelogEntryToAppStream(changelogEntry[2..].Trim())); + } + + if (changelogEntries.Count is 0) + throw new InvalidOperationException($"The changelog '{Path.GetFileName(changelogPath)}' does not contain any entry."); + + return changelogEntries; + } + + private static string ConvertChangelogEntryToAppStream(string changelogEntry) + { + var escapedEntry = changelogEntry + .Replace("&", "&", StringComparison.Ordinal) + .Replace("<", "<", StringComparison.Ordinal) + .Replace(">", ">", StringComparison.Ordinal); + + // Markdown code spans become AppStream code elements. Every second segment is inside a code span, + // which requires an even number of markers and therefore an odd number of segments: + var codeSpans = escapedEntry.Split('`'); + if (codeSpans.Length % 2 is 0) + throw new InvalidOperationException($"The changelog entry contains an unbalanced code marker: '{changelogEntry}'."); + + var convertedEntry = new StringBuilder(); + for (var index = 0; index < codeSpans.Length; index++) + convertedEntry.Append(index % 2 is 0 ? codeSpans[index] : $"{codeSpans[index]}"); + + return convertedEntry.ToString(); + } + + private static DateTime ParseMetadataBuildTime(string buildTime) + { + if (!DateTime.TryParseExact(buildTime.Trim(), "yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsedBuildTime)) + throw new InvalidOperationException($"The metadata build time '{buildTime}' is not a valid UTC build time."); + + return parsedBuildTime; } private static string FormatChangelogHeader(string appVersion, int buildNumber, DateTime buildTime) @@ -983,9 +1126,6 @@ public sealed partial class UpdateMetadataCommands string NextChangelogContent, string NextChangelogHeader, string NextChangelogVersion, - string MetainfoPath, - string MetainfoContent, - string MetainfoReleaseTag, string HeadCommitHash); [GeneratedRegex("""(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?[0-9.]+).+Commit:\s+(?[a-zA-Z0-9]+).+Host:\s+Version:\s+(?[0-9.]+).+Commit:\s+(?[a-zA-Z0-9]+)""")] @@ -1015,14 +1155,13 @@ public sealed partial class UpdateMetadataCommands [GeneratedRegex("""^[0-9]+\.[0-9]+\.[0-9]+$""")] private static partial Regex ExactAppVersionRegex(); - [GeneratedRegex("""]*>""")] - private static partial Regex ReleaseTagRegex(); + [GeneratedRegex("""]*>""")] + private static partial Regex ReleasesStartRegex(); - [GeneratedRegex("\\btype=\"stable\"")] - private static partial Regex StableReleaseTypeRegex(); - - [GeneratedRegex("\\bdate=\"[^\"]*\"")] - private static partial Regex ReleaseDateRegex(); + // Matches one entire release element, including its indentation and its trailing line break. The + // self-closing form comes first, so that it is never mistaken for the start of a longer element: + [GeneratedRegex("""(?ms)^[ \t]*]*/>[ \t]*\r?\n?|^[ \t]*]*>.*?[ \t]*\r?\n?""")] + private static partial Regex ReleaseBlockRegex(); [GeneratedRegex("^[0-9a-fA-F]{40,64}$")] private static partial Regex GitCommitHashRegex(); diff --git a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml index bfa60693..346ebc32 100644 --- a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml +++ b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml @@ -102,9 +102,87 @@ + + +
    +
  • Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API.
  • +
  • Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8.
  • +
  • Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings.
  • +
  • Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.
  • +
  • Added a share button for assistants, configurations, and language plugins. It uses the native share dialog on Windows and macOS. For Linux, we added an export option, which stores the plugin archive at a location of your choice. When you work on a translation for a new language, you can now hand your current state to testers or to us with one click.
  • +
  • Added the option to install plugin archives from your files: use the import button on the plugin page or simply drop an archive onto that page. Assistants, configurations, and language plugins are supported, and plugin archives now have their own file extension .mwplugin. Before installing a configuration, AI Studio shows what it sets up: which LLM providers and data sources it adds and where each of them sends your data, plus how many settings it takes control of. A configuration takes effect right away and has no on/off switch, so please install one only when you trust its source. You can remove it again at any time.
  • +
  • Added a delete button for assistants, configurations, and language plugins you installed or placed yourself. Until now, such a plugin could only be removed from the data directory by hand, which was especially painful for configurations because they have no on/off switch. Before deleting a configuration, AI Studio lists what disappears with it, such as providers, data sources, and settings that return to their default. When you delete the language plugin you had chosen, AI Studio returns to choosing your language automatically. Plugins shipped with AI Studio and plugins deployed by your IT department cannot be deleted.
  • +
  • Added options for organizations to disable importing, sharing, and exporting plugins, with a separate option for configuration plugins. Organizations can now let people import assistants while keeping configurations to their IT department.
  • +
  • Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself.
  • +
  • Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions – either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (j-erler) and marks his first contribution to AI Studio. Thank you, Jens, for this wonderful and useful contribution.
  • +
  • Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new .config-tests directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure.
  • +
  • Added CSV and TSV files to the file types you can attach. AI Studio was already able to read them, but they could not be selected.
  • +
  • Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed.
  • +
  • Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does.
  • +
  • Improved how Word documents (.docx) and OpenDocument files (.odt) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (nilskruthoff), who also wrote the library behind it. Thank you, Nils, for this great contribution.
  • +
  • Improved the order of your models. Every list of models is now sorted by provider and name, so all models of one provider stay together. Until now, models appeared in the order they were created, which meant that a model added later always showed up at the end of the list. This was especially confusing when your organization rolled out new models. In the settings, the tables for LLM providers, embeddings, and transcription are now grouped by provider and start collapsed, so even a long list stays easy to survey.
  • +
  • Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants.
  • +
  • Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example, an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file.
  • +
  • Fixed files that are open in another program being reported as an unrecognized file type. AI Studio now tells you that the file is currently open elsewhere and asks you to close it. This also works for files on shared network drives, where a colleague might have the file open.
  • +
  • Fixed files with a wrong file extension being reported as empty. AI Studio now recognizes what a file really is by looking at its content, for example, a PowerPoint presentation that was renamed to .txt, and reads it accordingly. It also points out the wrong extension, so you can correct it.
  • +
  • Fixed files whose content is not text being sent as an empty document. AI Studio now tells you that the file is not readable as text, which usually means it carries a wrong file extension.
  • +
  • Fixed executable programs with a harmless file extension being read as text. They are now recognized by their content and refused.
  • +
  • Fixed a single unreadable page of a PDF silently cutting off the rest of the document. The remaining pages are now used, and AI Studio tells you which pages are missing.
  • +
  • Fixed a single unreadable sheet of a spreadsheet silently dropping all remaining sheets.
  • +
  • Fixed PDFs, text files, spreadsheets, and presentations requiring Pandoc. Only HTML files need Pandoc now, so every other file can be attached and read without it.
  • +
  • Fixed attached files that are temporarily unavailable, disappearing from your message without a word. This could happen when a file was stored on a network drive.
  • +
  • Fixed the file preview showing an empty document when reading the file failed. It now shows what went wrong, so the preview again answers what AI Studio will hand to the AI.
  • +
  • Fixed the file preview looking like an empty file while AI Studio was still reading it. Larger documents and PDFs need a moment to be read, and until now that moment looked like a file without any content. The preview now says that it is still loading and shows the content as soon as it is ready.
  • +
  • Fixed problems while reading files being missing from the log file after the first one. This made exactly those issues hard to track down that only appeared later on.
  • +
  • Fixed reset buttons in assistants. As you may have noticed in the Document Analysis Assistant, resetting it could leave content from the previous analysis visible. Reset buttons now clear previous results completely.
  • +
  • Fixed dropping files after you closed a dialog that accepts files itself. Such a dialog takes over dropped files while it is open, but never handed that role back when you closed it. Afterward, the chat and the assistants silently ignored dropped files until you switched to another page. Each time you opened such a dialog again, the problem got worse.
  • +
  • Fixed configuration-managed settings, remaining active after their configuration plugin was removed.
  • +
  • Fixed settings not returning to your own value after a configuration was removed. When a configuration takes control of a setting, AI Studio now remembers the value you had chosen before and hands it back once no configuration manages that setting anymore. This covers an IT department withdrawing a configuration, deleting one yourself, and an administrator ending a test configuration. When a configuration only suggested a value, and you changed it afterward, your choice stays as it is.
  • +
  • Fixed the integrated code editor to keep errors and other issues in plugin code visible in the footer while scrolling.
  • +
  • Fixed the trusted badge so you can now see at a glance which models are trusted. It is shown consistently for self-hosted models and models from trusted providers.
  • +
  • Fixed approvals for assistant plugins being accepted from any configuration plugin. An approval marks an assistant as safe without a security check, and the app states that your organization approved it. Only configurations your IT department deploys, or that an administrator stages for a test, can do that now; approvals from any other locally placed configuration plugin are ignored and reported in the log.
  • +
  • Fixed withdrawing a configuration your organization deployed. A configuration that declared itself as locally managed stayed on the device even after the IT department stopped deploying it, and it kept every right of an organization configuration, such as approving assistant plugins. Where a configuration is stored now decides this instead of what the configuration says about itself, so withdrawing one always takes effect. This also applies to a device that was offline while the organization changed its policy: the withdrawal is applied when AI Studio starts again.
  • +
  • Fixed preview features contributed by several configuration plugins at once. Only the most recent contribution was recognized as coming from your organization, so features enabled by another configuration looked as if you had switched them on yourself. Each configuration is now tracked separately, which lets your organization enable one preview feature company-wide and another one for a single department.
  • +
  • Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log.
  • +
  • Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it.
  • +
  • Removed the legacy PowerPoint format (.ppt) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern .pptx format is not affected.
  • +
  • Upgraded dependencies to their latest versions to improve security and stability.
  • +
+
+
-

Update

+
    +
  • Added support for OpenAI GPT-5.6 Sol, Terra, and Luna; Anthropic Claude Fable 5 and Mythos 5; and Google Gemini 3 Flash, Gemini 3.1 Flash-Lite, Gemini 3.1 Pro, and Gemini 3.5 Flash.
  • +
  • Added support for OpenDocument presentations (.odp) when attaching and reading presentation files.
  • +
  • Added a log viewer assistant that shows AI Studio log files in a read-only view with search, log filters, highlighting, and auto-refresh.
  • +
  • Added audio and video transcription for chats and assistants. AI Studio now prepares supported media locally, sends only normalized audio to the configured transcription provider, and attaches the resulting transcript instead of the original media.
  • +
  • Added AI-assisted editing and revision for assistants created with the Assistant Builder. Thanks, Nils Kruthoff (nilskruthoff), for this contribution.
  • +
  • Added options to view and edit the code of AI-generated assistants and to delete your own generated assistants. Thanks, Nils Kruthoff (nilskruthoff), for this contribution.
  • +
  • Added enterprise configuration options to hide the last changelog and vision panels on the welcome page. Thanks, Dominic Neuburg (donework), for the contribution.
  • +
  • Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
  • +
  • Improved presentation imports so AI Studio can include speaker notes, slide comments, and presentation metadata in the extracted content.
  • +
  • Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
  • +
  • Improved secure API-key storage diagnostics on Linux. AI Studio now provides specific guidance when the default password collection is missing or locked, a password-manager prompt is dismissed, or no compatible Secret Service is available.
  • +
  • Improved the file dialogs to prevent opening multiple times when you click "Open" or "Save" multiple times in a row.
  • +
  • Improved assistant plugins so they clearly indicate when content from a document has been loaded and clear the indicator when the assistant is reset.
  • +
  • Improved the Assistant Builder security check so it can use the selected provider when no dedicated security audit agent provider is configured.
  • +
  • Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. Yes, we know this was an annoying bug, and we apologize for the inconvenience.
  • +
  • Fixed connections to internal HTTPS services and enterprise configuration servers that use organization-provided root certificates on Linux.
  • +
  • Fixed enterprise configuration plugins from Windows-created ZIP files may not load correctly on Linux when the ZIP contained plugin files inside a folder.
  • +
  • Fixed the voice recording shortcut on Linux so it works globally on supported desktops and while AI Studio is focused on other Linux desktops.
  • +
  • Fixed voice recording and transcription on Linux.
  • +
  • Fixed copied content from AI Studio not remaining available on the clipboard on Linux.
  • +
  • Fixed dragging and dropping files from the home folder into the Linux Flatpak version.
  • +
  • Fixed being able to switch document analysis policies while an analysis or media transcription was still in progress.
  • +
  • Fixed file extension handling so files are recognized correctly regardless of uppercase or lowercase letters in their extensions. Thanks, Paul Schweiß, for reporting this issue.
  • +
  • Fixed AI Studio failing to start on Linux systems & showing an outdated version on the Flatpak page.
  • +
  • Upgraded Rust to v1.97.1.
  • +
  • Upgraded .NET to v9.0.18.
  • +
  • Upgraded Tauri to v2.11.5.
  • +
  • Upgraded common dependencies.
  • +
  • Upgraded runtime dependencies.
  • +
From a9a37b6bf52253f9d130d78bbb3b4f9ff08fef0e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 13 Aug 2026 13:50:30 +0200 Subject: [PATCH 07/77] Updated README (#920) --- README.md | 3 ++- app/MindWork AI Studio/Assistants/I18N/allTexts.lua | 6 +++--- app/MindWork AI Studio/Pages/Home.razor.cs | 2 +- .../de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua | 6 +++--- .../en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua | 6 +++--- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 17c71ef0..6a5f850d 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Since March 2025: We have started developing the plugin system. There will be la +- v26.8.1: Added Hetzner's EU-hosted inference API as a provider, along with support for the latest open-source models like DeepSeek V4, GLM 5.2, Kimi K2.7 & K3, and Qwen 3.6 & 3.8; added the Visual Briefing Assistant as a preview feature and the Batch Processing Assistant to process entire folders of documents in one run; you can now share, import, and delete plugins; greatly improved working with files, including much better Word and OpenDocument support; expanded enterprise IT support with configuration priorities, test configurations before rollout, and policies for plugin sharing and imports. - v26.7.3: Added support for the latest OpenAI, Anthropic, and Google models; introduced audio and video transcription, a log viewer assistant, and AI-assisted editing and code management in the Assistant Builder; expanded presentation support with OpenDocument files, speaker notes, comments, and metadata; and improved Linux integration, enterprise update controls, and reliability after waking from sleep. - v26.7.1: Added the assistant builder as a beta preview for creating assistant plugins without coding; assistants can now keep running in the background; improved provider capability visibility and expert overrides, expanded enterprise controls for data source behavior and trusted assistant plugins, and made chats, assistants, and source links more reliable. - v26.6.2: Expanded enterprise configuration options with chat defaults, custom introduction panels, trust settings for data security, and managed confidence levels; added auto-backups for app settings & the possibility to view managed profiles and chat templates. @@ -89,7 +90,6 @@ Since March 2025: We have started developing the plugin system. There will be la - v0.10.0: Added support for newer models like Mistral 3 & GPT 5.2, OpenRouter as LLM and embedding provider, the possibility to use file attachments in chats, and support for images as input. - v0.9.51: Added support for [Perplexity](https://www.perplexity.ai/); citations added so that LLMs can provide source references (e.g., some OpenAI models, Perplexity); added support for OpenAI's Responses API so that all text LLMs from OpenAI now work in MindWork AI Studio, including Deep Research models; web searches are now possible (some OpenAI models, Perplexity). - v0.9.50: Added support for self-hosted LLMs using [vLLM](https://blog.vllm.ai/2023/06/20/vllm.html). -- v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation. @@ -115,6 +115,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi - [DeepSeek](https://www.deepseek.com/en) - [Alibaba Cloud](https://www.alibabacloud.com) (Qwen) - [OpenRouter](https://openrouter.ai/) + - [Hetzner](https://experiments.hetzner.com) (experimental inference API running open-source models in the EU) - [Hugging Face](https://huggingface.co/) using their [inference providers](https://huggingface.co/docs/inference-providers/index) such as Cerebras, Nebius, Sambanova, Novita, Hyperbolic, Together AI, Fireworks, Hugging Face - Self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), and [vLLM](https://github.com/vllm-project/vllm) - [Groq](https://groq.com/) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 498233c5..66ee77c3 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -8152,15 +8152,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Cost-effective" -- Flexibility UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibility" --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. -UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3892227145"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." - -- Privacy UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Privacy" -- You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T457410099"] = "You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems." +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T558496815"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." + -- Free of charge UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T617579208"] = "Free of charge" diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index e1851c2b..3072e57b 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -63,7 +63,7 @@ public partial class Home : MSGComponentBase this.itemsAdvantages = [ new(this.T("Free of charge"), this.T("The app is free to use, both for personal and commercial purposes.")), new(this.T("Democratization of AI"), this.T("We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 EUR such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.")), - new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")), + new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")), new(this.T("Assistants"), this.T("You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.")), new(this.T("Unrestricted usage"), this.T("Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.")), new(this.T("Cost-effective"), this.T("You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.")), 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 f99097b6..b3b2c0ed 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 @@ -8154,15 +8154,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Kosteneffizient" -- Flexibility UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibilität" --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. -UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3892227145"] = "Sie sind an keinen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face und selbst gehostete Modelle mit vLLM, llama.cpp, ollama, LM Studio, Groq oder Fireworks. Für Wissenschaftler und Mitarbeiter von Forschungseinrichtungen unterstützen wir auch die KI-Dienste von Helmholtz und GWDG. Diese sind über föderierte Anmeldungen wie eduGAIN für alle 18 Helmholtz-Zentren, die Max-Planck-Gesellschaft, die meisten deutschen und viele internationale Universitäten verfügbar." - -- Privacy UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Datenschutz" -- You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T457410099"] = "Sie können über die Einstellungen zur Anbietervertrauenswürdigkeit steuern, welche Anbieter ihre Daten erhalten. Zum Beispiel können Sie für das Schreiben von E-Mails einen anderen Schutzlevel festlegen als für allgemeine Chats usw. Außerdem garantieren die meisten Anbieter, dass ihre Daten nicht zum Trainieren neuer KI-Systeme verwendet werden." +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T558496815"] = "Sie sind nicht an einen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu Ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT-5, o1 usw.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimentell, in der EU gehostete Open-Source-Modelle), Hugging Face, Groq, Fireworks sowie selbst gehostete Modelle mit vLLM, llama.cpp, ollama oder LM Studio. Für Forschende und Mitarbeitende von Forschungseinrichtungen unterstützen wir außerdem die KI-Dienste von Helmholtz und GWDG. Diese stehen über föderierte Anmeldungen wie eduGAIN allen 18 Helmholtz-Zentren, der Max-Planck-Gesellschaft, den meisten deutschen und vielen internationalen Universitäten zur Verfügung." + -- Free of charge UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T617579208"] = "Kostenlos" 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 7d01f7fd..fa9d9a8c 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 @@ -8154,15 +8154,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3341379752"] = "Cost-effective" -- Flexibility UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3723223888"] = "Flexibility" --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. -UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3892227145"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." - -- Privacy UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3959064551"] = "Privacy" -- You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T457410099"] = "You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems." +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T558496815"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), OpenRouter, Hetzner (experimental, open-source models hosted in the EU), Hugging Face, Groq, Fireworks, and self-hosted models using vLLM, llama.cpp, ollama, or LM Studio. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." + -- Free of charge UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T617579208"] = "Free of charge" From 592c9c76e2a2604ebf7e8d53f2e3c21fabeb344c Mon Sep 17 00:00:00 2001 From: Dominic Neuburg Date: Sat, 15 Aug 2026 11:20:36 +0200 Subject: [PATCH 08/77] Added user-managed API keys for enterprise-configured providers (#919) Co-authored-by: Thorsten Sommer --- .../Assistants/I18N/allTexts.lua | 27 +++++++ .../Settings/SettingsPanelEmbeddings.razor | 11 ++- .../Settings/SettingsPanelEmbeddings.razor.cs | 14 ++++ .../Settings/SettingsPanelProviders.razor | 8 +- .../Settings/SettingsPanelProviders.razor.cs | 23 ++++-- .../Settings/SettingsPanelTranscription.razor | 8 +- .../SettingsPanelTranscription.razor.cs | 18 ++++- .../Dialogs/EmbeddingProviderDialog.razor | 17 ++++- .../Dialogs/EmbeddingProviderDialog.razor.cs | 44 +++++++++-- .../Dialogs/ProviderDialog.razor | 28 +++++-- .../Dialogs/ProviderDialog.razor.cs | 42 ++++++++++- .../Dialogs/TranscriptionProviderDialog.razor | 17 ++++- .../TranscriptionProviderDialog.razor.cs | 44 +++++++++-- .../Plugins/configuration/plugin.lua | 18 +++++ .../plugin.lua | 27 +++++++ .../plugin.lua | 27 +++++++ .../Settings/EmbeddingProvider.cs | 19 ++++- app/MindWork AI Studio/Settings/Provider.cs | 20 ++++- .../Settings/TranscriptionProvider.cs | 19 ++++- .../Tools/PluginSystem/IUserProvidedAPIKey.cs | 15 ++++ .../PluginSystem/PluginConfigurationObject.cs | 7 ++ .../wwwroot/changelog/v26.8.2.md | 2 + documentation/Enterprise IT.md | 74 +++++++++++++++++++ 23 files changed, 479 insertions(+), 50 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/IUserProvidedAPIKey.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 66ee77c3..2f40e79e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4294,6 +4294,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" +-- This embedding provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1931890418"] = "This embedding provider is managed by your organization. You can set your own API key." + -- Add text that should be embedded: UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:" @@ -4426,6 +4429,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T426925 -- This self-hosted provider is trusted for data source security checks. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "This self-hosted provider is trusted for data source security checks." +-- This provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100747"] = "This provider is managed by your organization. You can set your own API key." + -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard" @@ -4480,6 +4486,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T58 -- This transcription provider is trusted by your organization for data source security checks. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "This transcription provider is trusted by your organization for data source security checks." +-- This transcription provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T690752279"] = "This transcription provider is managed by your organization. You can set your own API key." + -- This transcription provider is managed by your organization. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "This transcription provider is managed by your organization." @@ -5776,6 +5785,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Mo -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2331453405"] = "(Optional) API Key" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again." + +-- This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2555207324"] = "This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below." + -- Add UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2646845972"] = "Add" @@ -6076,6 +6091,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T4250996615"] = "P -- Hugging Face Inference Provider UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider" +-- This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1090492389"] = "This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below." + -- Hide Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1108876344"] = "Hide Expert Settings" @@ -6139,6 +6157,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model" -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2331453405"] = "(Optional) API Key" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again." + -- Enabled UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2626085950"] = "Enabled" @@ -7771,6 +7792,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2189814010"] = -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2331453405"] = "(Optional) API Key" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again." + -- Add UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2646845972"] = "Add" @@ -7795,6 +7819,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T504465522"] = -- Host UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T808120719"] = "Host" +-- This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T828088153"] = "This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below." + -- Provider UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T900237532"] = "Provider" diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor index 71f0e618..a4ad15f7 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor @@ -45,12 +45,21 @@
} - @if (context.IsEnterpriseConfiguration) + @if (context.IsEnterpriseConfiguration && !context.AllowUserProvidedAPIKey) { } + else if (context.IsEnterpriseConfiguration && context.AllowUserProvidedAPIKey) + { + + + + + + + } else { diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index f7b88c14..dd74f949 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -73,6 +73,9 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase private async Task EditEmbeddingProvider(EmbeddingProvider embeddingProvider) { + if (embeddingProvider.IsEnterpriseConfiguration && !embeddingProvider.AllowUserProvidedAPIKey) + return; + var dialogParameters = new DialogParameters { { x => x.DataNum, embeddingProvider.Num }, @@ -84,6 +87,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase { x => x.IsSelfHosted, embeddingProvider.IsSelfHosted }, { x => x.IsEditing, true }, { x => x.DataHost, embeddingProvider.Host }, + { x => x.IsEnterpriseConfiguration, embeddingProvider.IsEnterpriseConfiguration }, }; var dialogReference = await this.DialogService.ShowAsync(T("Edit Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN); @@ -91,6 +95,16 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase if (dialogResult is null || dialogResult.Canceled) return; + if (embeddingProvider.IsEnterpriseConfiguration) + { + // Only the API key changed, and the dialog already stored it directly. The provider + // object itself is managed by the configuration plugin and must not be overwritten + // with the dialog's copy -- doing so would let the locked-but-technically-editable + // fields drift from what the organization configured. + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + return; + } + var editedEmbeddingProvider = (EmbeddingProvider)dialogResult.Data!; // Set the provider number if it's not set. This is important for providers diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index 426929f0..c7dea430 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -36,12 +36,18 @@ } - @if (context.IsEnterpriseConfiguration) + @if (context.IsEnterpriseConfiguration && !context.AllowUserProvidedAPIKey) { } + else if (context.IsEnterpriseConfiguration && context.AllowUserProvidedAPIKey) + { + + + + } else { diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index a308ab5a..eef516da 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -67,10 +67,10 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase { if(provider == AIStudio.Settings.Provider.NONE) return; - - if (provider.IsEnterpriseConfiguration) + + if (provider.IsEnterpriseConfiguration && !provider.AllowUserProvidedAPIKey) return; - + var dialogParameters = new DialogParameters { { x => x.DataNum, provider.Num }, @@ -85,6 +85,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase { x => x.HFInferenceProviderId, provider.HFInferenceProvider }, { x => x.AdditionalJsonApiParameters, provider.AdditionalJsonApiParameters }, { x => x.DataCapabilityOverrides, provider.CapabilityOverrides }, + { x => x.IsEnterpriseConfiguration, provider.IsEnterpriseConfiguration }, }; var dialogReference = await this.DialogService.ShowAsync(T("Edit LLM Provider"), dialogParameters, DialogOptions.FULLSCREEN); @@ -92,16 +93,26 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase if (dialogResult is null || dialogResult.Canceled) return; + if (provider.IsEnterpriseConfiguration) + { + // Only the API key changed, and the dialog already stored it directly. The provider + // object itself is managed by the configuration plugin and must not be overwritten + // with the dialog's copy -- doing so would let the locked-but-technically-editable + // fields drift from what the organization configured. + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + return; + } + var editedProvider = (AIStudio.Settings.Provider)dialogResult.Data!; - + // Set the provider number if it's not set. This is important for providers // added before we started saving the provider number. if(editedProvider.Num == 0) editedProvider = editedProvider with { Num = this.SettingsManager.ConfigurationData.NextProviderNum++ }; - + this.SettingsManager.ConfigurationData.Providers[this.SettingsManager.ConfigurationData.Providers.IndexOf(provider)] = editedProvider; await this.UpdateProviders(); - + await this.SettingsManager.StoreSettings(); await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor index 93d22ba1..40ec1a82 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor @@ -41,12 +41,18 @@ } - @if (context.IsEnterpriseConfiguration) + @if (context.IsEnterpriseConfiguration && !context.AllowUserProvidedAPIKey) { } + else if (context.IsEnterpriseConfiguration && context.AllowUserProvidedAPIKey) + { + + + + } else { diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs index 25e3120b..4d8daa15 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs @@ -72,6 +72,9 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase private async Task EditTranscriptionProvider(TranscriptionProvider transcriptionProvider) { + if (transcriptionProvider.IsEnterpriseConfiguration && !transcriptionProvider.AllowUserProvidedAPIKey) + return; + var dialogParameters = new DialogParameters { { x => x.DataNum, transcriptionProvider.Num }, @@ -83,13 +86,24 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase { x => x.IsSelfHosted, transcriptionProvider.IsSelfHosted }, { x => x.IsEditing, true }, { x => x.DataHost, transcriptionProvider.Host }, + { x => x.IsEnterpriseConfiguration, transcriptionProvider.IsEnterpriseConfiguration }, }; - + var dialogReference = await this.DialogService.ShowAsync(T("Edit Transcription Provider"), dialogParameters, DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; if (dialogResult is null || dialogResult.Canceled) return; - + + if (transcriptionProvider.IsEnterpriseConfiguration) + { + // Only the API key changed, and the dialog already stored it directly. The provider + // object itself is managed by the configuration plugin and must not be overwritten + // with the dialog's copy -- doing so would let the locked-but-technically-editable + // fields drift from what the organization configured. + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + return; + } + var editedTranscriptionProvider = (TranscriptionProvider)dialogResult.Data!; // Set the provider number if it's not set. This is important for providers diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index 85e6e6ef..1abb680e 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor @@ -4,10 +4,16 @@ + @if (this.IsEnterpriseConfiguration) + { + + @T("This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.") + + } @* ReSharper disable once CSharpWarnings::CS8974 *@ - + @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { if (provider.ProvideEmbeddingAPI() || provider is LLMProviders.NONE) @@ -38,13 +44,14 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHostname" UserAttributes="@SPELLCHECK_ATTRIBUTES"/> } @if (this.DataLLMProvider.IsHostNeeded()) { - + @foreach (Host host in Enum.GetValues(typeof(Host))) { if (host.IsEmbeddingSupported()) @@ -69,6 +76,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateManuallyModel" UserAttributes="@SPELLCHECK_ATTRIBUTES" HelperText="@T("Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually.")" @@ -76,7 +84,7 @@ } else { - + @T("Load") @if(this.availableModels.Count is 0) @@ -87,7 +95,7 @@ } else { - @@ -121,6 +129,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Lightbulb" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingInstanceName" UserAttributes="@SPELLCHECK_ATTRIBUTES" /> diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index 4f7d39ab..f2fb87f6 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -68,7 +68,14 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public bool IsEditing { get; init; } - + + /// + /// Whether this embedding provider is managed by an enterprise configuration plugin. When true, + /// every field except the API key is locked, matching Settings.EmbeddingProvider.IsEnterpriseConfiguration. + /// + [Parameter] + public bool IsEnterpriseConfiguration { get; set; } + [Inject] private RustService RustService { get; init; } = null!; @@ -85,6 +92,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId private bool dataIsValid; private string[] dataIssues = []; private string dataAPIKey = string.Empty; + private bool dataHadStoredAPIKeyOnLoad; private string dataManuallyModel = string.Empty; private string dataAPIKeyStorageIssue = string.Empty; private string dataEditingPreviousInstanceName = string.Empty; @@ -134,7 +142,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, - IsEnterpriseConfiguration = false, + IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, }; } @@ -174,11 +182,17 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId // Load the API key: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) + { this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption); + this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey); + } else { this.dataAPIKey = string.Empty; - if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED) + + // For an enterprise-managed provider, having no key yet is the expected first-run + // state, not a storage failure -- the user is just about to set their own key: + if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration) { this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue); await this.form.Validate(); @@ -203,8 +217,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId #region Implementation of ISecretId - public string SecretId => this.DataLLMProvider.ToSecretId(); - + // Must mirror Settings.EmbeddingProvider.SecretId exactly: when editing an enterprise-managed + // provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the + // app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would + // silently end up in the wrong keyring row and never be found again. + public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId(); + public string SecretName => this.DataName; #endregion @@ -240,6 +258,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId await this.form.Validate(); return; } + + this.dataHadStoredAPIKeyOnLoad = true; + } + else if (this.dataHadStoredAPIKeyOnLoad) + { + // The user cleared a previously stored key. Without this, the old key would simply + // stay in the OS keyring untouched and keep being used: + var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + if (!deleteResponse.Success) + { + this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue); + await this.form.Validate(); + return; + } + + this.dataHadStoredAPIKeyOnLoad = false; } this.MudDialog.Close(DialogResult.Ok(addedProviderSettings)); diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index 85795de9..b896837e 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -4,6 +4,12 @@ @inherits MSGComponentBase + @if (this.IsEnterpriseConfiguration) + { + + @T("This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.") + + } @* ReSharper disable once CSharpWarnings::CS8974 *@ @@ -15,6 +21,7 @@ OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider"> @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { @@ -27,7 +34,7 @@ @T("Create account") - + @if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost)) { @@ -43,13 +50,14 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHostname" UserAttributes="@SPELLCHECK_ATTRIBUTES"/> } @if (this.DataLLMProvider.IsHostNeeded()) { - + @foreach (Host host in Enum.GetValues(typeof(Host))) { @if (host.IsChatSupported()) @@ -64,7 +72,7 @@ @if (this.DataLLMProvider.IsHFInstanceProviderNeeded()) { - + @foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider))) { @@ -95,6 +103,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateManuallyModel" UserAttributes="@SPELLCHECK_ATTRIBUTES" HelperText="@T("Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually.")" @@ -102,7 +111,7 @@ } else { - + @T("Load models") @if(this.availableModels.Count is 0) @@ -117,7 +126,7 @@ Value="@this.DataModel" ValueChanged="@(async model => await this.OnModelChanged(model))" OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" - Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingModel"> + Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingModel"> @foreach (var model in this.availableModels) { @@ -157,6 +166,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Lightbulb" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingInstanceName" UserAttributes="@SPELLCHECK_ATTRIBUTES" /> @@ -193,12 +203,13 @@ @T("Reset") @@ -216,7 +227,8 @@ Margin="Margin.Dense" OpenIcon="@Icons.Material.Filled.Psychology" AdornmentColor="Color.Info" - Adornment="Adornment.Start"> + Adornment="Adornment.Start" + Disabled="@this.IsEnterpriseConfiguration"> @foreach (var mode in REASONING_OVERRIDE_MODES) { @@ -229,7 +241,7 @@ @string.Format(T("The current model uses the {0}."), this.GetCurrentModelApiLabel()) - + diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index bd88ba21..ef14b7bb 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -90,6 +90,13 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public bool IsEditing { get; init; } + + /// + /// Whether this provider is managed by an enterprise configuration plugin. When true, every + /// field except the API key is locked, matching Settings.Provider.IsEnterpriseConfiguration. + /// + [Parameter] + public bool IsEnterpriseConfiguration { get; set; } [Parameter] public string AdditionalJsonApiParameters { get; set; } = string.Empty; @@ -129,6 +136,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private bool dataIsValid; private string[] dataIssues = []; private string dataAPIKey = string.Empty; + private bool dataHadStoredAPIKeyOnLoad; private string dataManuallyModel = string.Empty; private string dataAPIKeyStorageIssue = string.Empty; private string dataEditingPreviousInstanceName = string.Empty; @@ -170,7 +178,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId UsedLLMProvider = this.DataLLMProvider, Model = this.GetSelectedModel(), IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, - IsEnterpriseConfiguration = false, + IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, HFInferenceProvider = this.HFInferenceProviderId, @@ -228,11 +236,17 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId // Load the API key: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.LLM_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) + { this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption); + this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey); + } else { this.dataAPIKey = string.Empty; - if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED) + + // For an enterprise-managed provider, having no key yet is the expected first-run + // state, not a storage failure -- the user is just about to set their own key: + if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration) { this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue); await this.form.Validate(); @@ -257,8 +271,12 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId #region Implementation of ISecretId - public string SecretId => this.DataLLMProvider.ToSecretId(); - + // Must mirror Settings.Provider.SecretId exactly: when editing an enterprise-managed + // provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the + // app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would + // silently end up in the wrong keyring row and never be found again. + public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId(); + public string SecretName => this.DataInstanceName; #endregion @@ -295,6 +313,22 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId await this.form.Validate(); return; } + + this.dataHadStoredAPIKeyOnLoad = true; + } + else if (this.dataHadStoredAPIKeyOnLoad) + { + // The user cleared a previously stored key. Without this, the old key would simply + // stay in the OS keyring untouched and keep being used: + var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.LLM_PROVIDER); + if (!deleteResponse.Success) + { + this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue); + await this.form.Validate(); + return; + } + + this.dataHadStoredAPIKeyOnLoad = false; } this.MudDialog.Close(DialogResult.Ok(addedProviderSettings)); diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor index 78d2dea2..dd7fb33a 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor @@ -4,10 +4,16 @@ + @if (this.IsEnterpriseConfiguration) + { + + @T("This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.") + + } @* ReSharper disable once CSharpWarnings::CS8974 *@ - + @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { if (provider.ProvideTranscriptionAPI() || provider is LLMProviders.NONE) @@ -38,13 +44,14 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHostname" UserAttributes="@SPELLCHECK_ATTRIBUTES"/> } @if (this.DataLLMProvider.IsHostNeeded()) { - + @foreach (Host host in Enum.GetValues(typeof(Host))) { if (host.IsTranscriptionSupported()) @@ -71,6 +78,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateManuallyModel" UserAttributes="@SPELLCHECK_ATTRIBUTES" HelperText="@T("Currently, we cannot query the transcription models for the selected provider and/or host. Therefore, please enter the model name manually.")" @@ -78,7 +86,7 @@ } else { - + @T("Load") @if(this.availableModels.Count is 0) @@ -89,7 +97,7 @@ } else { - @@ -132,6 +140,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Lightbulb" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingInstanceName" UserAttributes="@SPELLCHECK_ATTRIBUTES" /> diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs index b75ff07d..9132c9e6 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs @@ -68,7 +68,14 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public bool IsEditing { get; init; } - + + /// + /// Whether this transcription provider is managed by an enterprise configuration plugin. When + /// true, every field except the API key is locked, matching Settings.TranscriptionProvider.IsEnterpriseConfiguration. + /// + [Parameter] + public bool IsEnterpriseConfiguration { get; set; } + [Inject] private RustService RustService { get; init; } = null!; @@ -85,6 +92,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId private bool dataIsValid; private string[] dataIssues = []; private string dataAPIKey = string.Empty; + private bool dataHadStoredAPIKeyOnLoad; private string dataManuallyModel = string.Empty; private string dataAPIKeyStorageIssue = string.Empty; private string dataEditingPreviousInstanceName = string.Empty; @@ -149,7 +157,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, - IsEnterpriseConfiguration = false, + IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, }; } @@ -189,11 +197,17 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId // Load the API key: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) + { this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption); + this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey); + } else { this.dataAPIKey = string.Empty; - if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED) + + // For an enterprise-managed provider, having no key yet is the expected first-run + // state, not a storage failure -- the user is just about to set their own key: + if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration) { this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue); await this.form.Validate(); @@ -218,8 +232,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId #region Implementation of ISecretId - public string SecretId => this.DataLLMProvider.ToSecretId(); - + // Must mirror Settings.TranscriptionProvider.SecretId exactly: when editing an enterprise-managed + // provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the + // app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would + // silently end up in the wrong keyring row and never be found again. + public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId(); + public string SecretName => this.DataName; #endregion @@ -255,6 +273,22 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId await this.form.Validate(); return; } + + this.dataHadStoredAPIKeyOnLoad = true; + } + else if (this.dataHadStoredAPIKeyOnLoad) + { + // The user cleared a previously stored key. Without this, the old key would simply + // stay in the OS keyring untouched and keep being used: + var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); + if (!deleteResponse.Success) + { + this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue); + await this.form.Validate(); + return; + } + + this.dataHadStoredAPIKeyOnLoad = false; } this.MudDialog.Close(DialogResult.Ok(addedProviderSettings)); diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index f0337abb..453c980c 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -119,6 +119,14 @@ CONFIG["LLM_PROVIDERS"] = {} -- -- You can export an encrypted API key from an existing provider using the export button in the settings. -- -- ["APIKey"] = "ENC:v1:", -- +-- -- Optional: let each user set their own API key for this otherwise locked provider, +-- -- instead of (or in addition to not) embedding one centrally. Host, model, instance +-- -- name, and every other field stay locked; only the API key becomes editable. +-- -- Mutually exclusive with "APIKey" above: when both are set, the embedded key is +-- -- ignored and a warning is logged. The user's key is preserved in the OS keyring even +-- -- if this configuration is later withdrawn. +-- -- ["AllowUserProvidedAPIKey"] = true, +-- -- ["Model"] = { -- ["Id"] = "", -- ["DisplayName"] = "", @@ -141,6 +149,11 @@ CONFIG["TRANSCRIPTION_PROVIDERS"] = {} -- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details) -- -- ["APIKey"] = "ENC:v1:", -- +-- -- Optional: let each user set their own API key for this otherwise locked transcription +-- -- provider (see LLM_PROVIDERS example for details). Mutually exclusive with "APIKey" +-- -- above: when both are set, the embedded key is ignored and a warning is logged. +-- -- ["AllowUserProvidedAPIKey"] = true, +-- -- ["Model"] = { -- ["Id"] = "", -- ["DisplayName"] = "", @@ -163,6 +176,11 @@ CONFIG["EMBEDDING_PROVIDERS"] = {} -- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details) -- -- ["APIKey"] = "ENC:v1:", -- +-- -- Optional: let each user set their own API key for this otherwise locked embedding +-- -- provider (see LLM_PROVIDERS example for details). Mutually exclusive with "APIKey" +-- -- above: when both are set, the embedded key is ignored and a warning is logged. +-- -- ["AllowUserProvidedAPIKey"] = true, +-- -- ["Model"] = { -- ["Id"] = "", -- ["DisplayName"] = "", 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 b3b2c0ed..9e567b6e 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 @@ -4296,6 +4296,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen" +-- This embedding provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1931890418"] = "Dieser Anbieter für Einbettungen wird von Ihrer Organisation verwaltet. Sie können Ihren eigenen API-Schlüssel festlegen." + -- Add text that should be embedded: UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Text zum Einbetten eingeben:" @@ -4428,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T426925 -- This self-hosted provider is trusted for data source security checks. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "Dieser selbstgehostete Anbieter ist für Sicherheitsprüfungen von Datenquellen vertrauenswürdig." +-- This provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100747"] = "Dieser Anbieter wird von Ihrer Organisation verwaltet. Sie können Ihren eigenen API-Schlüssel einrichten." + -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Dashboard öffnen" @@ -4482,6 +4488,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T58 -- This transcription provider is trusted by your organization for data source security checks. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "Ihre Organisation vertraut diesem Anbieter für Transkriptionen bei der Sicherheitsprüfung von Datenquellen." +-- This transcription provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T690752279"] = "Dieser Anbieter für Transkriptionen wird von Ihrer Organisation verwaltet. Sie können Ihren eigenen API-Schlüssel festlegen." + -- This transcription provider is managed by your organization. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "Dieser Anbieter für Transkriptionen wird von Ihrer Organisation verwaltet." @@ -5778,6 +5787,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Mo -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2331453405"] = "(Optional) API-Schlüssel" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2439094236"] = "Der API-Schlüssel konnte nicht aus dem Betriebssystem entfernt werden. Die Meldung lautete: {0}. Bitte versuchen Sie es erneut." + +-- This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2555207324"] = "Dieser Anbieter für Einbettungen wird von Ihrer Organisation verwaltet. Host, Modell und andere Einstellungen sind gesperrt. Unten können Sie Ihren eigenen API-Schlüssel festlegen." + -- Add UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2646845972"] = "Hinzufügen" @@ -6078,6 +6093,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T4250996615"] = "P -- Hugging Face Inference Provider UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1085481431"] = "Hugging Face Inferenz-Anbieter" +-- This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1090492389"] = "Dieser Provider wird von Ihrer Organisation verwaltet. Host, Modell und andere Einstellungen sind gesperrt. Sie können Ihren eigenen API-Schlüssel unten festlegen." + -- Hide Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1108876344"] = "Experten-Einstellungen ausblenden" @@ -6141,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Modell" -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2331453405"] = "(Optional) API-Schlüssel" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2439094236"] = "Fehler beim Löschen des API-Schlüssels vom Betriebssystem. Die Nachricht war: {0}. Bitte versuchen Sie es erneut." + -- Enabled UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2626085950"] = "Aktiviert" @@ -7773,6 +7794,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2189814010"] = -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2331453405"] = "(Optional) API-Schlüssel" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2439094236"] = "Der API-Schlüssel konnte nicht aus dem Betriebssystem entfernt werden. Die Meldung lautete: {0}. Bitte versuchen Sie es erneut." + -- Add UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2646845972"] = "Hinzufügen" @@ -7797,6 +7821,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T504465522"] = -- Host UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T808120719"] = "Host" +-- This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T828088153"] = "Dieser Anbieter für Transkriptionen wird von Ihrer Organisation verwaltet. Host, Modell und weitere Einstellungen sind gesperrt. Sie können unten Ihren eigenen API-Schlüssel festlegen." + -- Provider UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T900237532"] = "Anbieter" 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 fa9d9a8c..932d6199 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 @@ -4296,6 +4296,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" +-- This embedding provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1931890418"] = "This embedding provider is managed by your organization. You can set your own API key." + -- Add text that should be embedded: UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:" @@ -4428,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T426925 -- This self-hosted provider is trusted for data source security checks. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "This self-hosted provider is trusted for data source security checks." +-- This provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T579100747"] = "This provider is managed by your organization. You can set your own API key." + -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard" @@ -4482,6 +4488,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T58 -- This transcription provider is trusted by your organization for data source security checks. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "This transcription provider is trusted by your organization for data source security checks." +-- This transcription provider is managed by your organization. You can set your own API key. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T690752279"] = "This transcription provider is managed by your organization. You can set your own API key." + -- This transcription provider is managed by your organization. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "This transcription provider is managed by your organization." @@ -5778,6 +5787,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2189814010"] = "Mo -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2331453405"] = "(Optional) API Key" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again." + +-- This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2555207324"] = "This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below." + -- Add UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T2646845972"] = "Add" @@ -6078,6 +6093,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T4250996615"] = "P -- Hugging Face Inference Provider UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1085481431"] = "Hugging Face Inference Provider" +-- This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1090492389"] = "This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below." + -- Hide Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1108876344"] = "Hide Expert Settings" @@ -6141,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model" -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2331453405"] = "(Optional) API Key" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again." + -- Enabled UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2626085950"] = "Enabled" @@ -7773,6 +7794,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2189814010"] = -- (Optional) API Key UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2331453405"] = "(Optional) API Key" +-- Failed to remove the API key from the operating system. The message was: {0}. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2439094236"] = "Failed to remove the API key from the operating system. The message was: {0}. Please try again." + -- Add UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T2646845972"] = "Add" @@ -7797,6 +7821,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T504465522"] = -- Host UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T808120719"] = "Host" +-- This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T828088153"] = "This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below." + -- Provider UI_TEXT_CONTENT["AISTUDIO::DIALOGS::TRANSCRIPTIONPROVIDERDIALOG::T900237532"] = "Provider" diff --git a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs index 8d153f39..b8306d58 100644 --- a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs +++ b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs @@ -20,7 +20,8 @@ public sealed record EmbeddingProvider( bool IsEnterpriseConfiguration = false, Guid EnterpriseConfigurationPluginId = default, string Hostname = "http://localhost:1234", - Host Host = Host.NONE) : ConfigurationBaseObject, ISecretId + Host Host = Host.NONE, + bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -97,6 +98,10 @@ public sealed record EmbeddingProvider( return false; } + var allowUserProvidedApiKey = false; + if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) + allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + provider = new EmbeddingProvider { Num = 0, // will be set later by the PluginConfigurationObject @@ -109,10 +114,18 @@ public sealed record EmbeddingProvider( EnterpriseConfigurationPluginId = configPluginId, Hostname = hostname, Host = host, + AllowUserProvidedAPIKey = allowUserProvidedApiKey, }; - // Handle encrypted API key if present: - if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText)) + // Handle an encrypted API key if present. When the user manages their own key for this + // embedding provider, we must never enqueue an embedded key: doing so would overwrite the + // user's key in the OS keyring on every configuration reload. + if (allowUserProvidedApiKey) + { + if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead(out var ignoredApiKeyText) && !string.IsNullOrWhiteSpace(ignoredApiKeyText)) + LOGGER.LogWarning($"The configured embedding provider {idx} sets both AllowUserProvidedAPIKey and an embedded APIKey. Ignoring the embedded key: the user manages their own key for this provider. (Plugin ID: {configPluginId})"); + } + else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText)) { if (!EnterpriseEncryption.IsEncrypted(apiKeyText)) LOGGER.LogWarning($"The configured embedding provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})"); diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index 4662b1b1..fdf63a34 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -21,6 +21,7 @@ namespace AIStudio.Settings; /// Whether the provider is self-hosted. /// The hostname of the provider. Useful for self-hosted providers. /// The LLM model to use for chat. +/// When set by a configuration plugin, the user may set their own API key for this otherwise locked, enterprise-managed provider. public sealed record Provider( uint Num, string Id, @@ -34,7 +35,8 @@ public sealed record Provider( Host Host = Host.NONE, HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE, string AdditionalJsonApiParameters = "", - ProviderCapabilityOverrides? CapabilityOverrides = null) : ConfigurationBaseObject, ISecretId + ProviderCapabilityOverrides? CapabilityOverrides = null, + bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -155,6 +157,10 @@ public sealed record Provider( var capabilityOverrides = ProviderCapabilityOverrides.TryParseFromLuaTable(idx, table, configPluginId, LOGGER); + var allowUserProvidedApiKey = false; + if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) + allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + provider = new Provider { Num = 0, // will be set later by the PluginConfigurationObject @@ -170,10 +176,18 @@ public sealed record Provider( HFInferenceProvider = hfInferenceProvider, AdditionalJsonApiParameters = additionalJsonApiParameters, CapabilityOverrides = capabilityOverrides, + AllowUserProvidedAPIKey = allowUserProvidedApiKey, }; - // Handle encrypted API key if present: - if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText)) + // Handle an encrypted API key if present. When the user manages their own key for this + // provider, we must never enqueue an embedded key: doing so would overwrite the user's + // key in the OS keyring on every configuration reload. + if (allowUserProvidedApiKey) + { + if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead(out var ignoredApiKeyText) && !string.IsNullOrWhiteSpace(ignoredApiKeyText)) + LOGGER.LogWarning($"The configured provider {idx} sets both AllowUserProvidedAPIKey and an embedded APIKey. Ignoring the embedded key: the user manages their own key for this provider. (Plugin ID: {configPluginId})"); + } + else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText)) { if (!EnterpriseEncryption.IsEncrypted(apiKeyText)) LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})"); diff --git a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs index 973cd138..5afae679 100644 --- a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs +++ b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs @@ -20,7 +20,8 @@ public sealed record TranscriptionProvider( bool IsEnterpriseConfiguration = false, Guid EnterpriseConfigurationPluginId = default, string Hostname = "http://localhost:1234", - Host Host = Host.NONE) : ConfigurationBaseObject, ISecretId + Host Host = Host.NONE, + bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -97,6 +98,10 @@ public sealed record TranscriptionProvider( return false; } + var allowUserProvidedApiKey = false; + if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) + allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + provider = new TranscriptionProvider { Num = 0, // will be set later by the PluginConfigurationObject @@ -109,10 +114,18 @@ public sealed record TranscriptionProvider( EnterpriseConfigurationPluginId = configPluginId, Hostname = hostname, Host = host, + AllowUserProvidedAPIKey = allowUserProvidedApiKey, }; - // Handle encrypted API key if present: - if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText)) + // Handle an encrypted API key if present. When the user manages their own key for this + // transcription provider, we must never enqueue an embedded key: doing so would overwrite + // the user's key in the OS keyring on every configuration reload. + if (allowUserProvidedApiKey) + { + if (table.TryGetValue("APIKey", out var ignoredApiKeyValue) && ignoredApiKeyValue.TryRead(out var ignoredApiKeyText) && !string.IsNullOrWhiteSpace(ignoredApiKeyText)) + LOGGER.LogWarning($"The configured transcription provider {idx} sets both AllowUserProvidedAPIKey and an embedded APIKey. Ignoring the embedded key: the user manages their own key for this provider. (Plugin ID: {configPluginId})"); + } + else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText)) { if (!EnterpriseEncryption.IsEncrypted(apiKeyText)) LOGGER.LogWarning($"The configured transcription provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})"); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IUserProvidedAPIKey.cs b/app/MindWork AI Studio/Tools/PluginSystem/IUserProvidedAPIKey.cs new file mode 100644 index 00000000..db0cb054 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/IUserProvidedAPIKey.cs @@ -0,0 +1,15 @@ +namespace AIStudio.Tools.PluginSystem; + +/// +/// Represents a configuration object whose API key is managed by the user, although the object +/// itself is managed by a configuration plugin. Implemented by all provider kinds which support +/// the "AllowUserProvidedAPIKey" option, i.e., LLM, embedding, and transcription providers. +/// +public interface IUserProvidedAPIKey +{ + /// + /// When set by a configuration plugin, the user may set their own API key for this otherwise + /// locked, enterprise-managed object. + /// + public bool AllowUserProvidedAPIKey { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs index 40f45617..07b4e3e8 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs @@ -407,6 +407,13 @@ public sealed record PluginConfigurationObject else LOG.LogWarning($"Failed to delete secret for removed enterprise object '{item.Name}' from the OS keyring: {deleteResult.Issue}"); } + else if(item is IUserProvidedAPIKey { AllowUserProvidedAPIKey: true }) + { + // The user manages their own key for this provider. Keep it in the OS keyring + // in case the organization's configuration comes back later, instead of forcing + // the user to re-enter it: + LOG.LogInformation($"Preserving the user-provided API key for removed enterprise provider '{item.Name}' in the OS keyring."); + } else if(secretStoreType is not null && item is ISecretId secretId) { var deleteResult = await RustService.DeleteAPIKey(secretId, secretStoreType.Value); diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index b1c44de3..c6b09e24 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1 +1,3 @@ # v26.8.2, build 252 (2026-08-xx xx:xx UTC) +- Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new `AllowUserProvidedAPIKey` option does this, and it works for `LLM_PROVIDERS`, `EMBEDDING_PROVIDERS`, and `TRANSCRIPTION_PROVIDERS` alike. +- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change. \ No newline at end of file diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 8f874ac4..b517f581 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -535,3 +535,77 @@ CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = { ``` The API key will be automatically decrypted when the configuration is loaded and stored securely in the operating system's credential store (Windows Credential Manager / macOS Keychain). + +## Letting users provide their own API key + +Sometimes you want to hand out a preconfigured provider -- a fixed host, model, and instance name +-- without embedding a shared API key for it. Each user then brings their own key, for example +their personal OpenAI or Anthropic account, while everything else about the provider stays exactly +as your organization configured it. + +Set `AllowUserProvidedAPIKey` on the provider: + +```lua +CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = { + ["Id"] = "9072b77d-ca81-40da-be6a-861da525ef7b", + ["InstanceName"] = "Corporate OpenAI GPT-4", + ["UsedLLMProvider"] = "OPEN_AI", + ["Host"] = "NONE", + ["Hostname"] = "", + ["AllowUserProvidedAPIKey"] = true, + ["AdditionalJsonApiParameters"] = "", + ["Model"] = { + ["Id"] = "gpt-4", + ["DisplayName"] = "GPT-4", + } +} +``` + +With `AllowUserProvidedAPIKey` set, the provider still shows up as managed by your organization, +and users still cannot change the host, model, instance name, or any other field. The settings +page shows a key icon instead of the usual lock icon for this provider; opening it only offers the +API key field, with everything else disabled. + +The flag works the same way for embedding and transcription providers: + +```lua +CONFIG["EMBEDDING_PROVIDERS"][#CONFIG["EMBEDDING_PROVIDERS"]+1] = { + ["Id"] = "3f0a4e8c-1d6b-4a91-8f2e-7c5d9b0a4e13", + ["Name"] = "Corporate Embeddings", + ["UsedLLMProvider"] = "OPEN_AI", + ["Host"] = "NONE", + ["Hostname"] = "", + ["AllowUserProvidedAPIKey"] = true, + ["Model"] = { + ["Id"] = "text-embedding-3-large", + ["DisplayName"] = "Text Embedding 3 Large", + } +} + +CONFIG["TRANSCRIPTION_PROVIDERS"][#CONFIG["TRANSCRIPTION_PROVIDERS"]+1] = { + ["Id"] = "b1c7d24f-5e83-4a06-9d1b-2f8e6a3c7d50", + ["Name"] = "Corporate Transcription", + ["UsedLLMProvider"] = "OPEN_AI", + ["Host"] = "NONE", + ["Hostname"] = "", + ["AllowUserProvidedAPIKey"] = true, + ["Model"] = { + ["Id"] = "whisper-1", + ["DisplayName"] = "Whisper", + } +} +``` + +For embedding providers, the settings page keeps the test button available next to the key icon, so +users can verify their own key right after entering it. + +This is mutually exclusive with an embedded `APIKey` on the same provider: if both are present, +AI Studio ignores the embedded key and logs a warning, because the whole point of the flag is that +each user manages their own key. Combine the two across different providers if you need it -- one +provider with a shared, embedded key and another with `AllowUserProvidedAPIKey` -- but not on the +same provider. + +The user's key follows the same "withdrawing a configuration" philosophy as everything else in this +document: if your configuration stops offering this provider, AI Studio removes the provider from +the settings but leaves the user's key in the OS keyring rather than deleting it, in case the same +provider comes back later. See [Withdrawing a configuration](#withdrawing-a-configuration). From a26c4d93a4beb2d1834a0229d4eb4f9a9a7ec8d7 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 15 Aug 2026 11:36:24 +0200 Subject: [PATCH 09/77] Make provider group headers clickable (#921) --- app/MindWork AI Studio/wwwroot/app.css | 4 ++++ app/MindWork AI Studio/wwwroot/app.js | 17 +++++++++++++++++ .../wwwroot/changelog/v26.8.2.md | 1 + 3 files changed, 22 insertions(+) diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index 5d97fdea..3442e246 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -417,6 +417,10 @@ * transcription providers). The rule targets the entire group row, so that the expand button * gets the same compact padding and shading as the header cell itself. */ +tr:has(> .provider-group-header) { + cursor: pointer; +} + tr:has(> .provider-group-header) > .mud-table-cell { padding-top: 0.25em; padding-bottom: 0.25em; diff --git a/app/MindWork AI Studio/wwwroot/app.js b/app/MindWork AI Studio/wwwroot/app.js index 160b5227..ea5b53a5 100644 --- a/app/MindWork AI Studio/wwwroot/app.js +++ b/app/MindWork AI Studio/wwwroot/app.js @@ -35,6 +35,23 @@ window.clearDiv = function (divName) { targetDiv.innerHTML = ''; } +// We add a click handler to the provider group headers so that clicking anywhere on the header expands or collapses the group. +// Right now (August 2026), this is not possible using MudBlazor. +document.addEventListener('click', function (event) { + const target = event.target + if (!(target instanceof Element)) + return + + const groupHeaderRow = target.closest('tr') + if (!groupHeaderRow?.querySelector(':scope > .provider-group-header')) + return + + if (target.closest('.mud-table-row-expander')) + return + + groupHeaderRow.querySelector('.mud-table-row-expander')?.click() +}) + window.scrollToBottom = function(element) { element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' }); } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index c6b09e24..503eeffd 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1,3 +1,4 @@ # v26.8.2, build 252 (2026-08-xx xx:xx UTC) - Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new `AllowUserProvidedAPIKey` option does this, and it works for `LLM_PROVIDERS`, `EMBEDDING_PROVIDERS`, and `TRANSCRIPTION_PROVIDERS` alike. +- Improved the grouped provider tables in the settings. You can now click anywhere on a group header to expand or collapse its LLM, embedding, or transcription providers. - Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change. \ No newline at end of file From 57c0b59fa582ebcbee4da4261aa009db0740ffce Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 15 Aug 2026 14:04:45 +0200 Subject: [PATCH 10/77] Fixed AI Studio installing a second copy of itself next to an existing installation (#922) --- .../Assistants/I18N/allTexts.lua | 42 ++ .../Settings/SettingsPanelApp.razor.cs | 16 +- .../Pages/Information.razor | 1 + .../Pages/Information.razor.cs | 25 + .../Plugins/configuration/plugin.lua | 8 + .../plugin.lua | 42 ++ .../plugin.lua | 42 ++ .../Tools/Rust/InstallationKind.cs | 31 + .../Tools/Rust/RuntimeInfoResponse.cs | 2 +- .../Tools/Services/UpdatePolicy.cs | 13 +- .../Tools/Services/UpdatePolicyMode.cs | 5 +- .../wwwroot/changelog/v26.8.2.md | 3 +- documentation/Enterprise IT.md | 32 +- runtime/src/app_window.rs | 58 +- runtime/src/environment.rs | 532 +++++++++++++++++- 15 files changed, 828 insertions(+), 24 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Rust/InstallationKind.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 2f40e79e..0b867a6f 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4078,6 +4078,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] -- seconds UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds" +-- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own." + -- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled." @@ -4138,6 +4141,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" +-- AI Studio cannot install updates into this installation. Your IT department provides new versions. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio cannot install updates into this installation. Your IT department provides new versions." + +-- Development builds do not install updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Development builds do not install updates." + -- Spellchecking is enabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled" @@ -4180,6 +4189,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] -- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." +-- AI Studio cannot install updates into its current location. Install new versions yourself. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio cannot install updates into its current location. Install new versions yourself." + -- Select a transcription provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider" @@ -4189,6 +4201,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] -- Use additional root certificates for external HTTPS requests? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Use additional root certificates for external HTTPS requests?" +-- AI Studio cannot update itself from its current location, so it does not check for updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] = "AI Studio cannot update itself from its current location, so it does not check for updates." + -- Select a root certificate bundle UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle" @@ -4207,6 +4222,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energy saving is disabled" +-- Development builds do not check for updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T735114866"] = "Development builds do not check for updates." + -- Start page UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page" @@ -8227,6 +8245,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a privat -- Copies the configuration origin to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard" +-- Installation +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1289059917"] = "Installation" + -- Unknown configuration plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin" @@ -8269,6 +8290,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:" +-- in a location AI Studio cannot update itself from +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "in a location AI Studio cannot update itself from" + -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" @@ -8278,6 +8302,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." +-- owned by you, self-updates possible +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "owned by you, self-updates possible" + -- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents." @@ -8311,6 +8338,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" +-- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation." + -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others." @@ -8359,6 +8389,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc -- Configuration origin: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:" +-- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio." + -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:" @@ -8449,6 +8482,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:" +-- managed by someone else, no self-updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "managed by someone else, no self-updates" + -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)." @@ -8503,6 +8539,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compil -- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management." +-- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead." + +-- development build, no self-updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "development build, no self-updates" + -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index cb2a6ef6..24a93ca5 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -15,11 +15,13 @@ public partial class SettingsPanelApp : SettingsPanelBase private UpdatePolicyMode updatePolicyMode; - private UpdateInterval DisplayedUpdateInterval => this.updatePolicyMode is UpdatePolicyMode.FLATPAK + private bool CannotUpdateItself => this.updatePolicyMode is UpdatePolicyMode.FLATPAK or UpdatePolicyMode.MANAGED_INSTALLATION or UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION or UpdatePolicyMode.DEVELOPMENT; + + private UpdateInterval DisplayedUpdateInterval => this.CannotUpdateItself ? UpdateInterval.NO_CHECK : this.SettingsManager.ConfigurationData.App.UpdateInterval; - private UpdateInstallation DisplayedUpdateInstallation => this.updatePolicyMode is UpdatePolicyMode.FLATPAK + private UpdateInstallation DisplayedUpdateInstallation => this.CannotUpdateItself ? UpdateInstallation.MANUAL : this.SettingsManager.ConfigurationData.App.UpdateInstallation; @@ -27,6 +29,9 @@ public partial class SettingsPanelApp : SettingsPanelBase { UpdatePolicyMode.ENTERPRISE_DISABLED => T("Your organization has disabled update checks and installations."), UpdatePolicyMode.FLATPAK => T("AI Studio cannot check for updates when running as a Flatpak. Updates are managed outside the app."), + UpdatePolicyMode.MANAGED_INSTALLATION => T("This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own."), + UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION => T("AI Studio cannot update itself from its current location, so it does not check for updates."), + UpdatePolicyMode.DEVELOPMENT => T("Development builds do not check for updates."), _ => T("How often should we check for app updates?") }; @@ -34,13 +39,16 @@ public partial class SettingsPanelApp : SettingsPanelBase { UpdatePolicyMode.ENTERPRISE_DISABLED => T("This setting has no effect while updates are disabled by your organization."), UpdatePolicyMode.FLATPAK => T("AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution."), + UpdatePolicyMode.MANAGED_INSTALLATION => T("AI Studio cannot install updates into this installation. Your IT department provides new versions."), + UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION => T("AI Studio cannot install updates into its current location. Install new versions yourself."), + UpdatePolicyMode.DEVELOPMENT => T("Development builds do not install updates."), _ => T("Should updates be installed automatically or manually?") }; - private bool IsUpdateIntervalLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED or UpdatePolicyMode.FLATPAK || + private bool IsUpdateIntervalLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED || this.CannotUpdateItself || ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked; - private bool IsUpdateInstallationLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED or UpdatePolicyMode.FLATPAK || + private bool IsUpdateInstallationLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED || this.CannotUpdateItself || ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked; protected override async Task OnInitializedAsync() diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index f0c8b60b..bd66c4ec 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -69,6 +69,7 @@ { } + @switch (HasAnyActiveEnvironment) { diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index de8fb510..7fc7c39f 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -71,6 +71,16 @@ public partial class Information : MSGComponentBase _ => T("not applicable") }; + private string InstallationKind => $"{T("Installation")}: {this.InstallationKindDisplayName}"; + + private string InstallationKindDisplayName => this.RuntimeInfo.InstallationKind switch + { + Tools.Rust.InstallationKind.MANAGED => T("managed by someone else, no self-updates"), + Tools.Rust.InstallationKind.UNSUPPORTED_LOCATION => T("in a location AI Studio cannot update itself from"), + Tools.Rust.InstallationKind.DEVELOPMENT => T("development build, no self-updates"), + _ => T("owned by you, self-updates possible") + }; + private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}"; private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}"; @@ -655,6 +665,21 @@ public partial class Information : MSGComponentBase parameters.Add(x => x.Message, T("AI Studio cannot update itself when installed as a Flatpak. A Flathub listing is planned. Until then, you can find the latest release on GitHub.")); parameters.Add(x => x.ReleaseUrl, "https://github.com/MindWorkAI/AI-Studio/releases/latest"); } + else if (this.updatePolicyMode is UpdatePolicyMode.MANAGED_INSTALLATION) + { + // No release link here: this installation was deployed centrally, and installing a + // second copy from GitHub next to it is exactly what we want to avoid. + parameters.Add(x => x.Message, T("This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio.")); + } + else if (this.updatePolicyMode is UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION) + { + parameters.Add(x => x.Message, T("AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation.")); + parameters.Add(x => x.ReleaseUrl, "https://github.com/MindWorkAI/AI-Studio/releases/latest"); + } + else if (this.updatePolicyMode is UpdatePolicyMode.DEVELOPMENT) + { + parameters.Add(x => x.Message, T("You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead.")); + } else return; diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 453c980c..e2cbd668 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -271,6 +271,14 @@ CONFIG["SETTINGS"] = {} -- of that, their choice outlives your configuration and stays as it is. -- ------ +-- Both update settings below only ever apply to installations AI Studio is able to update. +-- Installations you rolled out yourself, for example, into C:\Program Files or into a location +-- your users cannot write to, never update themselves, no matter what you configure here. You +-- can therefore leave automatic updates enabled for everybody: your deployments ignore them, +-- while installations your colleagues fetched from GitHub keep updating themselves. Place a file +-- named "managed-installation" next to the program file to mark any other installation as one +-- you maintain. The Enterprise IT documentation describes this in detail. + -- Configure the update check interval: -- Allowed values are: NO_CHECK, DISABLE_UPDATES, ONCE_STARTUP, HOURLY, DAILY, WEEKLY -- NO_CHECK disables automatic checks, but users can still check and install updates manually. 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 9e567b6e..e499b666 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 @@ -4080,6 +4080,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] -- seconds UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "Sekunden" +-- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "Diese Kopie von AI Studio wurde von einer anderen Person für Sie installiert und sucht daher nicht selbstständig nach Updates." + -- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Wählen Sie für die Transkription Ihrer Stimme einen Anbieter für Transkriptionen aus. Ohne einen ausgewählten Anbieter wird die Diktier- und Transkriptions-Funktion deaktiviert." @@ -4140,6 +4143,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?" +-- AI Studio cannot install updates into this installation. Your IT department provides new versions. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio kann in dieser Installation keine Updates installieren. Ihre IT-Abteilung stellt neue Versionen bereit." + +-- Development builds do not install updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Entwicklerversionen installieren keine Updates." + -- Spellchecking is enabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Rechtschreibprüfung ist aktiviert" @@ -4182,6 +4191,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] -- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "Wenn aktiviert, ist die Rechtschreibprüfung in allen Eingabefeldern aktiv. Je nach Betriebssystem werden Fehler möglicherweise nicht visuell hervorgehoben, aber ein Rechtsklick kann dennoch Korrekturvorschläge anzeigen." +-- AI Studio cannot install updates into its current location. Install new versions yourself. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio kann Updates nicht am aktuellen Speicherort installieren. Installieren Sie neue Versionen selbst." + -- Select a transcription provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Wählen Sie einen Transkriptionsanbieter aus" @@ -4191,6 +4203,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] -- Use additional root certificates for external HTTPS requests? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Zusätzliche Stammzertifikate für externe HTTPS-Anfragen verwenden?" +-- AI Studio cannot update itself from its current location, so it does not check for updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] = "AI Studio kann sich an seinem aktuellen Speicherort nicht selbst aktualisieren und sucht daher nicht nach Updates." + -- Select a root certificate bundle UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Wählen Sie ein Stammzertifikat-Bundle aus" @@ -4209,6 +4224,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energiesparmodus ist deaktiviert" +-- Development builds do not check for updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T735114866"] = "Entwicklerversionen suchen nicht nach Updates." + -- Start page UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Startseite" @@ -8229,6 +8247,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine pr -- Copies the configuration origin to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Kopiert den Ursprung der Konfiguration in die Zwischenablage" +-- Installation +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1289059917"] = "Installation" + -- Unknown configuration plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unbekanntes Konfigurations-Plugin" @@ -8271,6 +8292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstel -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin-Verzeichnis:" +-- in a location AI Studio cannot update itself from +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "an einem Speicherort, von dem aus AI Studio keine Updates durchführen kann" + -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:" @@ -8280,6 +8304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Kopiert den Pfad -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "Diese Bibliothek wird verwendet, um die Unterschiede zwischen zwei Texten anzuzeigen. Das ist zum Beispiel für den Grammatik- und Rechtschreibassistenten notwendig." +-- owned by you, self-updates possible +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "von Ihnen verwaltet, Updates sind möglich" + -- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "Wenn Sie auf den jeweiligen Pfad klicken, wird dieser in die Zwischenablage kopiert. Sie können diese Dateien mit einem Texteditor öffnen, um ihren Inhalt anzusehen." @@ -8313,6 +8340,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgende -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage" +-- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio kann sich von seinem aktuellen Speicherort aus nicht selbst aktualisieren. Durch die Installation eines Updates würde eine zweite Installation entstehen, anstatt diese zu ersetzen. Um eine neue Version zu erhalten, laden Sie die aktuelle Version herunter und installieren Sie sie über Ihre bestehende Installation." + -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "Das Projekt windows-rs ermöglicht den Zugriff auf Windows-APIs aus Rust. Wir verwenden mehrere Bibliotheken aus diesem Projekt: windows-registry wird verwendet, um die gewünschte Konfiguration in Windows-Unternehmensumgebungen auszulesen. Die Bibliotheken windows und windows-collections werden verwendet, um den nativen Windows-Dialog zum Teilen zu öffnen, zum Beispiel wenn Sie ein Plugin mit anderen teilen." @@ -8361,6 +8391,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installierte Pan -- Configuration origin: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Ursprung der Konfiguration:" +-- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "Diese Kopie von AI Studio wurde von einer anderen Person für Sie installiert, höchstwahrscheinlich von Ihrer IT-Abteilung. Daher kann sie sich nicht selbst aktualisieren. Wenden Sie sich an Ihre IT-Abteilung, wenn Sie Fragen zur Aktualisierung von AI Studio haben." + -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Slot der Konfiguration:" @@ -8451,6 +8484,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vektordatenbank" -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Unternehmenskonfigurations-ID:" +-- managed by someone else, no self-updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "von jemand anderem verwaltet, keine Selbstaktualisierung" + -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Verbinden Sie AI Studio mit den Daten ihrer Organisation über unsere Schnittstelle für externe Datenabfrage (ERI)." @@ -8505,6 +8541,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Verwendeter Rust -- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird mit Unternehmenskonfigurationen unter Verwendung von Konfigurations-Plugins betrieben. Eine zentrale Konfigurationsverwaltung wird nicht eingesetzt." +-- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "Du verwendest eine Entwicklerversion von AI Studio, die sich niemals selbst aktualisiert. Hole stattdessen die neuesten Änderungen und erstelle die App neu." + +-- development build, no self-updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "Entwicklerversion, keine automatischen Updates" + -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!" 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 932d6199..b7fa0dcf 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 @@ -4080,6 +4080,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] -- seconds UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds" +-- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own." + -- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled." @@ -4140,6 +4143,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" +-- AI Studio cannot install updates into this installation. Your IT department provides new versions. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio cannot install updates into this installation. Your IT department provides new versions." + +-- Development builds do not install updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Development builds do not install updates." + -- Spellchecking is enabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled" @@ -4182,6 +4191,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] -- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." +-- AI Studio cannot install updates into its current location. Install new versions yourself. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio cannot install updates into its current location. Install new versions yourself." + -- Select a transcription provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider" @@ -4191,6 +4203,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] -- Use additional root certificates for external HTTPS requests? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Use additional root certificates for external HTTPS requests?" +-- AI Studio cannot update itself from its current location, so it does not check for updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] = "AI Studio cannot update itself from its current location, so it does not check for updates." + -- Select a root certificate bundle UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle" @@ -4209,6 +4224,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energy saving is disabled" +-- Development builds do not check for updates. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T735114866"] = "Development builds do not check for updates." + -- Start page UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page" @@ -8229,6 +8247,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a privat -- Copies the configuration origin to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard" +-- Installation +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1289059917"] = "Installation" + -- Unknown configuration plugin UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin" @@ -8271,6 +8292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:" +-- in a location AI Studio cannot update itself from +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "in a location AI Studio cannot update itself from" + -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" @@ -8280,6 +8304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." +-- owned by you, self-updates possible +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "owned by you, self-updates possible" + -- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents." @@ -8313,6 +8340,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" +-- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation." + -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others." @@ -8361,6 +8391,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc -- Configuration origin: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:" +-- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio." + -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:" @@ -8451,6 +8484,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:" +-- managed by someone else, no self-updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "managed by someone else, no self-updates" + -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)." @@ -8505,6 +8541,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compil -- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management." +-- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead." + +-- development build, no self-updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "development build, no self-updates" + -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" diff --git a/app/MindWork AI Studio/Tools/Rust/InstallationKind.cs b/app/MindWork AI Studio/Tools/Rust/InstallationKind.cs new file mode 100644 index 00000000..58636044 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/InstallationKind.cs @@ -0,0 +1,31 @@ +namespace AIStudio.Tools.Rust; + +/// +/// Tells whether this installation is able to update itself, and if not, why. +/// +public enum InstallationKind +{ + /// + /// An installation the current user owns and which AI Studio may update itself. This is also + /// the fallback when the runtime reports a kind we do not know yet. + /// + USER, + + /// + /// An installation someone else deployed and maintains, for example, an IT department. Whoever + /// deployed it distributes new versions instead. + /// + MANAGED, + + /// + /// An installation the current user owns, but which the updater cannot replace. Its owner has + /// to install a new version themselves. + /// + UNSUPPORTED_LOCATION, + + /// + /// Not an installation at all, but a development build started from a build directory or an + /// IDE. There is nothing here the updater could replace. + /// + DEVELOPMENT, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs b/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs index 435e89c1..c9e2f769 100644 --- a/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs +++ b/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs @@ -1,3 +1,3 @@ namespace AIStudio.Tools.Rust; -public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, string LinuxPackageType); \ No newline at end of file +public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, string LinuxPackageType, InstallationKind InstallationKind); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs b/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs index 265b63ec..c41713f1 100644 --- a/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs +++ b/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs @@ -11,7 +11,18 @@ public sealed class UpdatePolicy(SettingsManager settingsManager, RuntimeInfoRes : runtimeInfo.LinuxPackageType switch { "flatpak" => UpdatePolicyMode.FLATPAK, - _ => UpdatePolicyMode.SELF_UPDATE + _ => runtimeInfo.InstallationKind switch + { + // + // The runtime already refuses to update these installations. We mirror its decision + // here so that the UI explains the situation instead of offering update actions that + // would silently do nothing: + // + InstallationKind.MANAGED => UpdatePolicyMode.MANAGED_INSTALLATION, + InstallationKind.UNSUPPORTED_LOCATION => UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION, + InstallationKind.DEVELOPMENT => UpdatePolicyMode.DEVELOPMENT, + _ => UpdatePolicyMode.SELF_UPDATE + } }; public bool AllowsManualChecks => this.CurrentMode is UpdatePolicyMode.SELF_UPDATE; diff --git a/app/MindWork AI Studio/Tools/Services/UpdatePolicyMode.cs b/app/MindWork AI Studio/Tools/Services/UpdatePolicyMode.cs index 021e1a6e..776deb7f 100644 --- a/app/MindWork AI Studio/Tools/Services/UpdatePolicyMode.cs +++ b/app/MindWork AI Studio/Tools/Services/UpdatePolicyMode.cs @@ -4,5 +4,8 @@ public enum UpdatePolicyMode { SELF_UPDATE, FLATPAK, - ENTERPRISE_DISABLED + ENTERPRISE_DISABLED, + MANAGED_INSTALLATION, + UNSUPPORTED_INSTALLATION_LOCATION, + DEVELOPMENT, } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 503eeffd..9342abb9 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1,4 +1,5 @@ # v26.8.2, build 252 (2026-08-xx xx:xx UTC) - Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new `AllowUserProvidedAPIKey` option does this, and it works for `LLM_PROVIDERS`, `EMBEDDING_PROVIDERS`, and `TRANSCRIPTION_PROVIDERS` alike. - Improved the grouped provider tables in the settings. You can now click anywhere on a group header to expand or collapse its LLM, embedding, or transcription providers. -- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change. \ No newline at end of file +- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change. +- Fixed AI Studio installing a second copy of itself next to an existing installation. Its updater always installs into your personal user folder, so an installation elsewhere, such as one your IT department rolled out, was never replaced. AI Studio now recognizes those installations and leaves them alone. The information page tells you which case applies to yours. For IT departments: automatic updates can now stay enabled for everybody, and the Enterprise IT documentation explains the rest. \ No newline at end of file diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index b517f581..2fc35995 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -4,7 +4,7 @@ Do you want to manage MindWork AI Studio in a corporate environment or within an organization? This documentation explains what you need to do and how it works. First, here's an overview of the entire process: - You can distribute MindWork AI Studio to employees' devices using tools like Microsoft System Center Configuration Manager (SCCM). -- Employees can get updates through the built-in update feature. Enterprise configuration can disable automatic checks or the entire built-in update feature so that the IT department controls which version gets distributed. +- Employees can get updates through the built-in update feature. Enterprise configuration can disable automatic checks or the entire built-in update feature so that the IT department controls which version gets distributed. Installations you rolled out yourself never update themselves anyway, so the two kinds can coexist on one device. - AI Studio checks about every 16 minutes to see where and which configuration it should load. This information is loaded from the local system. On Windows, you might use the registry, for example. - If it finds the necessary metadata, AI Studio downloads the configuration as a ZIP file from the specified server. - The configuration is an AI Studio plugin written in Lua. @@ -21,6 +21,36 @@ Set `CONFIG["SETTINGS"]["DataApp.UpdateInterval"]` in the configuration plugin t Use `DISABLE_UPDATES` when your organization distributes approved versions through its own software-management process. +### Installations that never update themselves + +AI Studio recognizes installations its updater cannot replace and never updates those, no matter what `DataApp.UpdateInterval` and `DataApp.UpdateInstallation` say. You can therefore leave automatic updates enabled for your whole organization: the installations you rolled out ignore them and receive their versions from you, while installations your colleagues fetched from GitHub keep updating themselves. + +This matters most on Windows. The installer we publish installs per user below `%LOCALAPPDATA%`, and the updater runs exactly that installer. Updating an installation that sits anywhere else therefore does not replace it: a second installation appears below `%LOCALAPPDATA%` while yours stays untouched, and from then on it is a matter of chance which one a colleague starts. Loosening the permissions of your deployment does not change this — the updater never writes into the current location to begin with. + +AI Studio recognizes these cases: + +| Case | How AI Studio recognizes it | What users are told | +|---|---|---| +| Marker file | A file named `managed-installation` next to the program file | Updates come from their IT department | +| Machine-wide program directory | The program file sits below `%ProgramFiles%`, `%ProgramFiles(x86)%`, or `%ProgramW6432%` | Updates come from their IT department | +| Location the user cannot write to | The directory that would have to be replaced is not writable for the current user, e.g. `/Applications` on a device managed through MDM, or `/opt` on Linux | Updates come from their IT department | +| Self-chosen directory (Windows only) | Everything else outside `%LOCALAPPDATA%`, e.g. `D:\Tools\MindWork AI Studio` | They have to install a new version themselves, with a link to the latest release | +| Flatpak | Running inside a Flatpak sandbox | Updates come from their Flatpak distribution | + +The information page reports which case applies, so a support request can start from that instead of guesswork. + +#### The marker file + +Place an empty file named `managed-installation` next to the program file, in the same directory as `MindWork AI Studio.exe` on Windows or as the executable on Linux. Its content is ignored; only its existence matters. The marker applies to that one installation, so a colleague who installed AI Studio from GitHub on the same device is not affected by it. + +Use the marker when the other cases do not cover your deployment, for example, when you roll out our regular per-user installer through Intune, or when you install into a directory of your own such as `D:\Program Files\MindWork AI Studio`. + +On macOS there is no marker file: any additional file inside the app bundle would break its code signature. A bundle in a location your users cannot write to is recognized anyway. If you want AI Studio to name your organization explicitly on macOS, set `DataApp.UpdateInterval` to `DISABLE_UPDATES`, which takes precedence over all of this. + +#### Existing double installations + +This recognition prevents new double installations; it does not clean up ones that already exist. On affected devices, remove the second installation below `%LOCALAPPDATA%\MindWork AI Studio\` together with its uninstall entry under `HKEY_CURRENT_USER`, and make sure that shortcuts point at your deployment again. + ## Configure the devices So that MindWork AI Studio knows where to load which configuration, this information must be provided as metadata on employees' devices. Currently, the following options are available: diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 3fa86892..7c01962e 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -23,7 +23,8 @@ use crate::api_token::APIToken; use crate::clipboard::shutdown_clipboard; use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_server}; use crate::environment::{ - is_prod, is_dev, is_flatpak, CONFIG_DIRECTORY, DATA_DIRECTORY, FLATPAK_LIBRARY_DIRECTORY, + installation_kind, is_prod, is_dev, is_flatpak, InstallationKind, CONFIG_DIRECTORY, + DATA_DIRECTORY, FLATPAK_LIBRARY_DIRECTORY, }; use crate::log::switch_to_file_logging; use crate::pdfium::PDFIUM_LIB_PATH; @@ -514,8 +515,7 @@ pub async fn change_location_to(url: &str) { /// Checks for updates. pub async fn check_for_update(_token: APIToken) -> Json { - if !self_update_allowed(is_dev(), is_flatpak()) { - let reason = if is_flatpak() { "Flatpak installations are updated externally" } else { "the app is running in development mode" }; + if let Some(reason) = self_update_blocked_reason(is_flatpak(), installation_kind()) { warn!(Source = "Updater"; "Skipping update check because {reason}."); return Json(CheckUpdateResponse { update_is_available: false, @@ -600,8 +600,7 @@ pub struct CheckUpdateResponse { /// Installs the update. pub async fn install_update(_token: APIToken) { - if !self_update_allowed(is_dev(), is_flatpak()) { - let reason = if is_flatpak() { "Flatpak installations are updated externally" } else { "the app is running in development mode" }; + if let Some(reason) = self_update_blocked_reason(is_flatpak(), installation_kind()) { warn!(Source = "Updater"; "Skipping update installation because {reason}."); return; } @@ -660,8 +659,18 @@ pub async fn install_update(_token: APIToken) { } } -fn self_update_allowed(development: bool, flatpak: bool) -> bool { - !development && !flatpak +/// Returns why this installation cannot update itself, or `None` when it can. +fn self_update_blocked_reason(flatpak: bool, installation_kind: InstallationKind) -> Option<&'static str> { + if flatpak { + return Some("Flatpak installations are updated externally"); + } + + match installation_kind { + InstallationKind::User => None, + InstallationKind::Managed => Some("this installation is centrally managed"), + InstallationKind::UnsupportedLocation => Some("this installation is in a location the updater cannot replace"), + InstallationKind::Development => Some("the app is running in development mode"), + } } /// Response for application exit requests. @@ -895,18 +904,47 @@ mod tests { #[test] fn self_update_is_disabled_in_development() { - assert!(!self_update_allowed(true, false)); + assert!(self_update_blocked_reason(false, InstallationKind::Development).is_some()); } #[test] fn self_update_is_disabled_for_flatpak() { - assert!(!self_update_allowed(false, true)); + assert!(self_update_blocked_reason(true, InstallationKind::User).is_some()); + } + + #[test] + fn self_update_is_disabled_for_managed_installations() { + assert!(self_update_blocked_reason(false, InstallationKind::Managed).is_some()); + } + + #[test] + fn self_update_is_disabled_for_unsupported_installation_locations() { + assert!(self_update_blocked_reason(false, InstallationKind::UnsupportedLocation).is_some()); + } + + #[test] + fn every_blocked_installation_kind_has_its_own_reason() { + let reasons = [ + self_update_blocked_reason(false, InstallationKind::Managed), + self_update_blocked_reason(false, InstallationKind::UnsupportedLocation), + self_update_blocked_reason(false, InstallationKind::Development), + ]; + + for (index, reason) in reasons.iter().enumerate() { + assert!(reason.is_some(), "expected a reason at index {index}"); + assert_eq!( + reasons.iter().filter(|other| *other == reason).count(), + 1, + "expected the reason at index {index} to be unique" + ); + } } #[test] fn self_update_is_enabled_for_normal_production_installations() { - assert!(self_update_allowed(false, false)); + assert!(self_update_blocked_reason(false, InstallationKind::User).is_none()); } + #[test] fn pdfium_library_directory_prefers_resources_libraries() { let temp_dir = tempfile::tempdir().unwrap(); diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 6f10b1c9..28e777da 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -23,6 +23,12 @@ const ENTERPRISE_REGISTRY_KEY_PATH: &str = r"Software\github\MindWork AI Studio\ const ENTERPRISE_POLICY_SECRET_FILE_NAME: &str = "config_encryption_secret.yaml"; const EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME: &str = "external_http_custom_root_certificates.yaml"; +/// Marker file an IT department may place next to the executable to declare this installation +/// as centrally managed. It is not used on macOS, because any additional file inside the app +/// bundle would break its code signature. +#[cfg(any(target_os = "windows", target_os = "linux", test))] +const MANAGED_INSTALLATION_MARKER_FILE_NAME: &str = "managed-installation"; + pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_POLICY_CONFIGURED"; pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED"; pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH"; @@ -47,6 +53,9 @@ pub static CONFIG_DIRECTORY: OnceLock = OnceLock::new(); /// The user language cached once per runtime process. static USER_LANGUAGE: OnceLock = OnceLock::new(); +/// The installation kind cached once per runtime process. +static INSTALLATION_KIND: OnceLock = OnceLock::new(); + /// Returns the config directory. pub async fn get_config_directory(_token: APIToken) -> String { match CONFIG_DIRECTORY.get() { @@ -71,11 +80,36 @@ pub async fn read_user_name(_token: APIToken) -> String { }) } +/// Tells whether this installation is able to update itself, and if not, why. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +pub enum InstallationKind { + /// An installation the current user owns and which the app may update itself. + User, + + /// An installation someone else deployed and maintains: it sits in a machine-wide program + /// directory, the current user cannot modify it, or it was declared as centrally maintained + /// through the marker file or by shipping it as a Flatpak. Whoever deployed it distributes new + /// versions instead. + Managed, + + /// An installation the current user owns, but which the updater still cannot replace. This only + /// happens on Windows: the NSIS updater ignores where the app currently sits and always + /// installs below the local app data directory, so updating a self-chosen directory such as + /// `D:\Tools\MindWork AI Studio` would leave a second installation behind. Nobody else + /// maintains this installation, so its owner has to install a new version themselves. + UnsupportedLocation, + + /// Not an installation at all, but a development build started from a build directory or an + /// IDE. There is nothing here the updater could replace. + Development, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct RuntimeInfo { pub working_directory: String, pub executable_path: String, pub linux_package_type: String, + pub installation_kind: InstallationKind, } pub async fn get_runtime_info(_token: APIToken) -> Json { @@ -87,6 +121,7 @@ pub async fn get_runtime_info(_token: APIToken) -> Json { .map(|path| path.to_string_lossy().into_owned()) .unwrap_or_default(), linux_package_type: detect_linux_package_type().to_string(), + installation_kind: installation_kind(), }) } @@ -129,6 +164,268 @@ fn env_var_has_value(key: &str) -> bool { env::var(key).is_ok_and(|value| !value.trim().is_empty()) } +/// Returns the kind of this installation, cached for the lifetime of the process. +/// +/// Installations outside the per-user location cannot be replaced by the Tauri updater: on Windows +/// it runs the NSIS setup with its per-user defaults and creates a second installation below the +/// local app data directory instead of updating the existing one. That happens for an enterprise +/// deployment into `C:\Program Files` just as much as for a user who chose their own directory. +/// +/// Whenever the kind cannot be determined, we report a user installation. Wrongly reporting that an +/// installation cannot update itself would cut regular users off from every future update, +/// including security updates, which is far worse than a second installation. +pub(crate) fn installation_kind() -> InstallationKind { + *INSTALLATION_KIND.get_or_init(|| { + // A development build lives in a build directory, which is perfectly writable and would + // therefore look like a regular user installation. We check it up front so that the + // platform-specific detection below only ever deals with real installations: + let kind = if is_dev() { + InstallationKind::Development + } else { + detect_installation_kind() + }; + + info!(Source = "Updater"; "Detected a {kind:?} installation of AI Studio."); + kind + }) +} + +#[cfg(target_os = "windows")] +fn detect_installation_kind() -> InstallationKind { + let executable_path = match env::current_exe() { + Ok(path) => path, + Err(e) => { + warn!(Source = "Updater"; "Cannot read the current executable path: {e}. Assuming a user installation."); + return InstallationKind::User; + } + }; + + if has_managed_installation_marker(&executable_path) { + return InstallationKind::Managed; + } + + if is_windows_machine_wide_installation(&executable_path, &windows_program_files_directories()) { + return InstallationKind::Managed; + } + + if is_windows_per_user_installation(&executable_path, dirs::data_local_dir().as_deref()) { + return InstallationKind::User; + } + + // The installation sits neither in the location the NSIS updater targets nor in a machine-wide + // program directory, so an update would create a second installation next to it. Who put it + // there decides how the app words that: a directory the current user cannot write to was set up + // by an administrator, while a writable one is a directory the user chose in the installer. + let Some(install_directory) = executable_path.parent() else { + return InstallationKind::UnsupportedLocation; + }; + + match directory_is_writable(install_directory) { + Some(false) => InstallationKind::Managed, + _ => InstallationKind::UnsupportedLocation, + } +} + +#[cfg(target_os = "macos")] +fn detect_installation_kind() -> InstallationKind { + let executable_path = match env::current_exe() { + Ok(path) => path, + Err(e) => { + warn!(Source = "Updater"; "Cannot read the current executable path: {e}. Assuming a user installation."); + return InstallationKind::User; + } + }; + + // The updater replaces the entire app bundle, so it needs to write into the directory that + // contains the bundle. On a device managed through an MDM solution like Jamf, the bundle sits + // in a location the user cannot write to. We deliberately do not look for a marker file here: + // any additional file inside the bundle would break its code signature. As a consequence, a + // macOS installation is never reported as managed, only as an unsupported location. An + // organization that wants AI Studio to name it explicitly sets DataApp.UpdateInterval to + // DISABLE_UPDATES in its enterprise configuration, which takes precedence anyway. + match macos_app_bundle_directory(&executable_path) { + Some(bundle_directory) => update_target_installation_kind(&bundle_directory), + None => InstallationKind::User, + } +} + +#[cfg(target_os = "linux")] +fn detect_installation_kind() -> InstallationKind { + // Flatpak installations are always updated from outside the app: + if is_flatpak() { + return InstallationKind::Managed; + } + + let executable_path = match env::current_exe() { + Ok(path) => path, + Err(e) => { + warn!(Source = "Updater"; "Cannot read the current executable path: {e}. Assuming a user installation."); + return InstallationKind::User; + } + }; + + if has_managed_installation_marker(&executable_path) { + return InstallationKind::Managed; + } + + // For AppImages, the updater replaces the AppImage file itself. Everything else is replaced + // in place as well. A deployment into a system-wide location such as /opt is therefore not + // updatable by the app: + let update_target = env::var("APPIMAGE") + .map(PathBuf::from) + .unwrap_or(executable_path); + + update_target_installation_kind(&update_target) +} + +/// Returns whether the executable sits in the per-user location the NSIS updater targets, which is +/// the only Windows location it can actually replace. Everywhere else an update installs below the +/// local app data directory and leaves the existing installation behind. +#[cfg(any(target_os = "windows", test))] +fn is_windows_per_user_installation(executable_path: &Path, local_app_data_directory: Option<&Path>) -> bool { + let Some(local_app_data_directory) = local_app_data_directory else { + warn!(Source = "Updater"; "Cannot read the local app data directory. Assuming a user installation."); + return true; + }; + + path_is_below(executable_path, local_app_data_directory) +} + +/// Returns whether the executable sits in one of the machine-wide program directories. The NSIS +/// installer we ship installs per user and never picks such a directory on its own, so whatever +/// runs from there was packaged and deployed by an IT department. +/// +/// The permissions of that directory deliberately play no role here. Some organizations make their +/// deployment writable for users, hoping the updater would then replace it in place. It never does: +/// it runs our per-user setup, which installs below the local app data directory regardless of the +/// current location and leaves a second installation behind. +#[cfg(any(target_os = "windows", test))] +fn is_windows_machine_wide_installation(executable_path: &Path, program_files_directories: &[PathBuf]) -> bool { + program_files_directories + .iter() + .any(|program_files_directory| path_is_below(executable_path, program_files_directory)) +} + +/// Returns the machine-wide program directories of this Windows system. A 32-bit process sees +/// `ProgramFiles` as `C:\Program Files (x86)` and reaches the 64-bit directory only through +/// `ProgramW6432`, so we read all of them instead of assuming one layout or a fixed drive. +#[cfg(target_os = "windows")] +fn windows_program_files_directories() -> Vec { + ["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"] + .iter() + .filter_map(|variable_name| env::var(variable_name).ok()) + .filter(|value| !value.trim().is_empty()) + .map(PathBuf::from) + .collect() +} + +/// Returns whether the given path sits inside the given directory. +/// +/// Both paths must be compared in the same form. Canonicalization resolves junctions, symbolic +/// links, and 8.3 short names such as PROGRA~1, but it also prepends the \\?\ verbatim prefix on +/// Windows. Applying it to only one of the two paths would make even a regular per-user +/// installation look like it sits somewhere else. Therefore, we either use both canonicalized paths +/// or neither of them. +#[cfg(any(target_os = "windows", test))] +fn path_is_below(path: &Path, directory: &Path) -> bool { + let (path, directory) = match (fs::canonicalize(path), fs::canonicalize(directory)) { + (Ok(canonical_path), Ok(canonical_directory)) => (canonical_path, canonical_directory), + _ => (path.to_path_buf(), directory.to_path_buf()), + }; + + path_starts_with_ignoring_case(&path, &directory) +} + +/// Compares the path components case-insensitively, because Windows paths are not case-sensitive. +/// A plain string prefix check is not enough either: it would treat `C:\Users\Alice-Backup` as +/// being below `C:\Users\Alice`. +#[cfg(any(target_os = "windows", test))] +fn path_starts_with_ignoring_case(path: &Path, prefix: &Path) -> bool { + let mut path_components = path.components(); + for prefix_component in prefix.components() { + let Some(path_component) = path_components.next() else { + return false; + }; + + let path_text = path_component.as_os_str().to_string_lossy(); + let prefix_text = prefix_component.as_os_str().to_string_lossy(); + if !path_text.eq_ignore_ascii_case(&prefix_text) { + return false; + } + } + + true +} + +/// Derives the app bundle root from the executable path, e.g. +/// `/Applications/MindWork AI Studio.app/Contents/MacOS/MindWork AI Studio` becomes +/// `/Applications/MindWork AI Studio.app`. +#[cfg(any(target_os = "macos", test))] +fn macos_app_bundle_directory(executable_path: &Path) -> Option { + let macos_directory = executable_path.parent()?; + if macos_directory.file_name()? != "MacOS" { + return None; + } + + let contents_directory = macos_directory.parent()?; + if contents_directory.file_name()? != "Contents" { + return None; + } + + let bundle_directory = contents_directory.parent()?; + if !bundle_directory.extension().is_some_and(|extension| extension.eq_ignore_ascii_case("app")) { + return None; + } + + Some(bundle_directory.to_path_buf()) +} + +/// Decides the installation kind for the platforms whose updater replaces the given target in +/// place. It writes the replacement into the directory that contains the target, so that is the +/// directory we test: whoever may write there may update the app. +#[cfg(any(target_os = "macos", target_os = "linux", test))] +fn update_target_installation_kind(update_target: &Path) -> InstallationKind { + let Some(directory) = update_target.parent() else { + return InstallationKind::User; + }; + + // A directory the current user cannot write to was set up by an administrator or an IT + // department, and they are the ones distributing new versions. There is no unsupported location + // on these platforms: an in-place replacement works wherever the user may write: + match directory_is_writable(directory) { + Some(false) => InstallationKind::Managed, + _ => InstallationKind::User, + } +} + +/// Tests whether the current user may write into the given directory by actually creating a +/// temporary file there. Permission bits alone are not reliable: ACLs, read-only mounts, and +/// managed-device restrictions do not show up in them. +/// +/// Returns `None` when the test itself could not be carried out, so that callers can fall back to +/// treating the installation as updatable instead of locking the user out on an inconclusive probe. +fn directory_is_writable(directory: &Path) -> Option { + match tempfile::Builder::new().prefix(".ai-studio-write-test").tempfile_in(directory) { + Ok(_) => Some(true), + Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => Some(false), + Err(e) => { + warn!(Source = "Updater"; "Cannot test whether '{}' is writable: {e}.", directory.display()); + None + } + } +} + +/// Returns whether an IT department declared this installation as centrally managed by placing a +/// marker file next to the executable. This covers deployments the path check cannot recognize, +/// for example, when an organization rolls out the regular per-user installer through Intune. +#[cfg(any(target_os = "windows", target_os = "linux", test))] +fn has_managed_installation_marker(executable_path: &Path) -> bool { + match executable_path.parent() { + Some(directory) => directory.join(MANAGED_INSTALLATION_MARKER_FILE_NAME).is_file(), + None => false, + } +} + /// Returns true if the application is running in development mode. pub fn is_dev() -> bool { cfg!(debug_assertions) @@ -1055,17 +1352,21 @@ fn normalize_enterprise_config_id(value: &str) -> Option { #[cfg(test)] mod tests { use super::{ - enterprise_environment_key_name, enterprise_policy_file_slot_suffix, + directory_is_writable, enterprise_environment_key_name, + enterprise_policy_file_slot_suffix, has_managed_installation_marker, + is_windows_machine_wide_installation, is_windows_per_user_installation, load_external_http_custom_root_certificate_policy_from_directories, linux_policy_directories_from_xdg, load_policy_values_from_directories, - normalize_locale_tag, parse_enterprise_source_values, - select_effective_enterprise_config_source, select_effective_enterprise_secret_source, + macos_app_bundle_directory, normalize_locale_tag, parse_enterprise_source_values, + path_starts_with_ignoring_case, select_effective_enterprise_config_source, + select_effective_enterprise_secret_source, update_target_installation_kind, EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues, - ExternalHttpCustomRootCertificatePolicy, + ExternalHttpCustomRootCertificatePolicy, InstallationKind, + MANAGED_INSTALLATION_MARKER_FILE_NAME, }; use std::collections::HashMap; use std::fs; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use tempfile::tempdir; const TEST_ID_A: &str = "9072B77D-CA81-40DA-BE6A-861DA525EF7B"; @@ -1454,6 +1755,227 @@ mod tests { ); } + /// Builds a path from its components using the separator of the current platform. Windows + /// paths written with backslashes would be a single component on Unix, so the tests below + /// could not exercise the component comparison there. + fn path_of(components: &[&str]) -> PathBuf { + components.iter().collect() + } + + #[test] + fn windows_per_user_installations_may_update_themselves() { + let local_app_data = path_of(&["/", "Users", "Alice", "AppData", "Local"]); + let executable = path_of(&["/", "Users", "Alice", "AppData", "Local", "MindWork AI Studio", "MindWork AI Studio.exe"]); + + assert!(is_windows_per_user_installation(&executable, Some(&local_app_data))); + } + + #[test] + fn windows_installations_outside_the_local_app_data_directory_cannot_update_themselves() { + let local_app_data = path_of(&["/", "Users", "Alice", "AppData", "Local"]); + + for install_directory in [ + vec!["/", "Program Files", "MindWork AI Studio"], + vec!["/", "Program Files (x86)", "MindWork AI Studio"], + vec!["/", "Apps", "MindWork AI Studio"], + vec!["/", "Users", "Alice", "AppData", "Roaming", "MindWork AI Studio"], + ] { + let mut components = install_directory.clone(); + components.push("MindWork AI Studio.exe"); + let executable = path_of(&components); + + assert!( + !is_windows_per_user_installation(&executable, Some(&local_app_data)), + "expected '{}' to sit outside the per-user installation location", + executable.display() + ); + } + } + + #[test] + fn windows_program_directories_are_managed_regardless_of_their_permissions() { + let program_files_directories = vec![ + path_of(&["/", "Program Files"]), + path_of(&["/", "Program Files (x86)"]), + ]; + + for install_directory in [ + vec!["/", "Program Files", "MindWork AI Studio"], + vec!["/", "Program Files (x86)", "MindWork AI Studio"], + ] { + let mut components = install_directory.clone(); + components.push("MindWork AI Studio.exe"); + let executable = path_of(&components); + + assert!( + is_windows_machine_wide_installation(&executable, &program_files_directories), + "expected '{}' to be a machine-wide installation", + executable.display() + ); + } + } + + #[test] + fn windows_directories_next_to_the_program_directories_are_not_machine_wide() { + let program_files_directories = vec![path_of(&["/", "Program Files"])]; + + // 'Program Files (x86)' is not configured here, and a plain string prefix check would still + // match it against 'Program Files'. The same holds for a self-chosen directory: + for executable in [ + path_of(&["/", "Program Files (x86)", "MindWork AI Studio", "MindWork AI Studio.exe"]), + path_of(&["/", "Apps", "MindWork AI Studio", "MindWork AI Studio.exe"]), + ] { + assert!( + !is_windows_machine_wide_installation(&executable, &program_files_directories), + "expected '{}' not to be a machine-wide installation", + executable.display() + ); + } + } + + #[test] + fn windows_installations_are_not_machine_wide_without_program_directories() { + let executable = path_of(&["/", "Program Files", "MindWork AI Studio", "MindWork AI Studio.exe"]); + + assert!(!is_windows_machine_wide_installation(&executable, &[])); + } + + #[test] + fn windows_installation_kind_ignores_case_but_respects_component_boundaries() { + let local_app_data = path_of(&["/", "Users", "Alice", "AppData", "Local"]); + + // Windows paths are not case-sensitive: + let differently_cased = path_of(&["/", "users", "alice", "appdata", "local", "MindWork AI Studio", "MindWork AI Studio.exe"]); + assert!(is_windows_per_user_installation(&differently_cased, Some(&local_app_data))); + + // A plain string prefix check would wrongly accept this one: + let sibling_directory = path_of(&["/", "Users", "Alice", "AppData", "LocalBackup", "MindWork AI Studio", "MindWork AI Studio.exe"]); + assert!(!is_windows_per_user_installation(&sibling_directory, Some(&local_app_data))); + } + + #[test] + fn windows_installation_kind_falls_back_to_user_without_local_app_data() { + let executable = path_of(&["/", "Program Files", "MindWork AI Studio", "MindWork AI Studio.exe"]); + + assert!(is_windows_per_user_installation(&executable, None)); + } + + #[test] + fn windows_installation_kind_does_not_mix_canonical_and_raw_paths() { + // The local app data directory exists and can be canonicalized, while the executable below + // it does not. Canonicalizing only one of the two would compare different path forms, for + // example '/private/var/...' against '/var/...' or '\\?\C:\...' against 'C:\...', and would + // reject a perfectly regular per-user installation: + let local_app_data = tempdir().unwrap(); + let executable = local_app_data + .path() + .join("MindWork AI Studio") + .join("MindWork AI Studio.exe"); + + assert!(is_windows_per_user_installation(&executable, Some(local_app_data.path()))); + } + + #[test] + fn path_starts_with_ignoring_case_compares_whole_components() { + assert!(path_starts_with_ignoring_case( + Path::new("/Applications/Some App.app/Contents"), + Path::new("/applications/some app.app") + )); + + assert!(!path_starts_with_ignoring_case( + Path::new("/Applications"), + Path::new("/Applications/Some App.app") + )); + + assert!(!path_starts_with_ignoring_case( + Path::new("/Applications-Backup/Some App.app"), + Path::new("/Applications") + )); + } + + #[test] + fn macos_app_bundle_directory_resolves_the_bundle_root() { + assert_eq!( + macos_app_bundle_directory(Path::new( + "/Applications/MindWork AI Studio.app/Contents/MacOS/MindWork AI Studio" + )), + Some(PathBuf::from("/Applications/MindWork AI Studio.app")) + ); + } + + #[test] + fn macos_app_bundle_directory_rejects_paths_outside_a_bundle() { + assert_eq!( + macos_app_bundle_directory(Path::new("/usr/local/bin/mindwork-ai-studio")), + None + ); + + assert_eq!( + macos_app_bundle_directory(Path::new( + "/Applications/MindWork AI Studio/Contents/MacOS/MindWork AI Studio" + )), + None + ); + } + + #[test] + fn writable_update_targets_are_user_installations() { + let directory = tempdir().unwrap(); + assert_eq!(directory_is_writable(directory.path()), Some(true)); + + // An AppImage may sit anywhere as long as its directory is writable: + let update_target = directory.path().join("MindWork AI Studio.AppImage"); + assert_eq!( + update_target_installation_kind(&update_target), + InstallationKind::User + ); + } + + #[cfg(unix)] + #[test] + fn read_only_update_targets_are_managed_installations() { + use std::os::unix::fs::PermissionsExt; + + let directory = tempdir().unwrap(); + let read_only_directory = directory.path().join("read-only"); + fs::create_dir(&read_only_directory).unwrap(); + fs::set_permissions(&read_only_directory, fs::Permissions::from_mode(0o500)).unwrap(); + + // Permissions do not apply to root, so the assertions below would fail there. In that case, + // we skip them instead of asserting something the environment cannot provide: + let running_as_root = fs::write(read_only_directory.join("root-probe"), "").is_ok(); + if !running_as_root { + assert_eq!(directory_is_writable(&read_only_directory), Some(false)); + + // Whoever set up a directory the user cannot write to also distributes the updates: + let update_target = read_only_directory.join("MindWork AI Studio.AppImage"); + assert_eq!( + update_target_installation_kind(&update_target), + InstallationKind::Managed + ); + } + + // Restore the permissions so that the temporary directory can be cleaned up: + fs::set_permissions(&read_only_directory, fs::Permissions::from_mode(0o700)).unwrap(); + } + + #[test] + fn the_marker_file_declares_a_managed_installation() { + let directory = tempdir().unwrap(); + let executable = directory.path().join("MindWork AI Studio"); + fs::write(&executable, "").unwrap(); + + assert!(!has_managed_installation_marker(&executable)); + + fs::write( + directory.path().join(MANAGED_INSTALLATION_MARKER_FILE_NAME), + "", + ) + .unwrap(); + + assert!(has_managed_installation_marker(&executable)); + } + #[test] fn load_policy_values_from_directories_uses_first_directory_wins() { let directory_a = tempdir().unwrap(); From 01f25b2bc08b0d9fc58219ce3c194c738de568ce Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 15 Aug 2026 14:26:00 +0200 Subject: [PATCH 11/77] Prepared release v26.8.1 (#923) --- app/MindWork AI Studio/Components/Changelog.Logs.cs | 2 +- app/MindWork AI Studio/packages.lock.json | 6 +++--- app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md | 5 ++++- app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md | 6 +----- metadata.txt | 6 +++--- .../packaging/linux/org.mindworkai.AIStudio.metainfo.xml | 5 ++++- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index a7fe8827..07951293 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,7 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ - new (251, "v26.8.1, build 251 (2026-08-13 06:01 UTC)", "v26.8.1.md"), + new (252, "v26.8.1, build 252 (2026-08-15 12:21 UTC)", "v26.8.1.md"), new (250, "v26.7.3, build 250 (2026-07-21 12:45 UTC)", "v26.7.3.md"), new (244, "v26.7.2, build 244 (2026-07-06 18:35 UTC)", "v26.7.2.md"), new (243, "v26.7.1, build 243 (2026-07-05 16:39 UTC)", "v26.7.1.md"), diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 659e11fa..3bd053e7 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -41,9 +41,9 @@ }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.18, )", - "resolved": "9.0.18", - "contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg==" + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "I9GkKrCVjzxGU1hsKSurOW6P/ABPPHARfc/MTnzIgDb8YjJ/votxKN2z7K+J3DvlQXGH0O7KqdtBseRj8j7eNQ==" }, "MudBlazor": { "type": "Direct", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 9fdb6cf9..ce1893dd 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -1,4 +1,4 @@ -# v26.8.1, build 251 (2026-08-13 06:01 UTC) +# v26.8.1, build 252 (2026-08-15 12:21 UTC) - Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API. - Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8. - Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings. @@ -10,6 +10,7 @@ - Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself. - Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions – either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (`j-erler`) and marks his first contribution to AI Studio. Thank you, Jens, for this wonderful and useful contribution. - Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new `.config-tests` directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure. +- Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new `AllowUserProvidedAPIKey` option does this, and it works for `LLM_PROVIDERS`, `EMBEDDING_PROVIDERS`, and `TRANSCRIPTION_PROVIDERS` alike. - Added CSV and TSV files to the file types you can attach. AI Studio was already able to read them, but they could not be selected. - Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed. - Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does. @@ -39,5 +40,7 @@ - Fixed preview features contributed by several configuration plugins at once. Only the most recent contribution was recognized as coming from your organization, so features enabled by another configuration looked as if you had switched them on yourself. Each configuration is now tracked separately, which lets your organization enable one preview feature company-wide and another one for a single department. - Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log. - Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it. +- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change. +- Fixed AI Studio installing a second copy of itself next to an existing installation. Its updater always installs into your personal user folder, so an installation elsewhere, such as one your IT department rolled out, was never replaced. AI Studio now recognizes those installations and leaves them alone. The information page tells you which case applies to yours. For IT departments: automatic updates can now stay enabled for everybody, and the Enterprise IT documentation explains the rest. - Removed the legacy PowerPoint format (`.ppt`) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern `.pptx` format is not affected. - Upgraded dependencies to their latest versions to improve security and stability. \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 9342abb9..f375356b 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1,5 +1 @@ -# v26.8.2, build 252 (2026-08-xx xx:xx UTC) -- Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new `AllowUserProvidedAPIKey` option does this, and it works for `LLM_PROVIDERS`, `EMBEDDING_PROVIDERS`, and `TRANSCRIPTION_PROVIDERS` alike. -- Improved the grouped provider tables in the settings. You can now click anywhere on a group header to expand or collapse its LLM, embedding, or transcription providers. -- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change. -- Fixed AI Studio installing a second copy of itself next to an existing installation. Its updater always installs into your personal user folder, so an installation elsewhere, such as one your IT department rolled out, was never replaced. AI Studio now recognizes those installations and leaves them alone. The information page tells you which case applies to yours. For IT departments: automatic updates can now stay enabled for everybody, and the Enterprise IT documentation explains the rest. \ No newline at end of file +# v26.8.2, build 253 (2026-08-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index b8f930ca..e54904fe 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ 26.8.1 -2026-08-13 06:01:54 UTC -251 +2026-08-15 12:21:19 UTC +252 9.0.120 (commit 3f97250e38) 9.0.19 (commit 8381bdb01f) 1.97.1 (commit 8bab26f4f) 8.15.0 2.11.5 -3c789da4654, release +1bf592994d3, release osx-arm64 148.0.7763.0 0.7.2 \ No newline at end of file diff --git a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml index 346ebc32..5b9983cc 100644 --- a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml +++ b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml @@ -102,7 +102,7 @@ - +
  • Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API.
  • @@ -116,6 +116,7 @@
  • Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself.
  • Added the Batch Processing Assistant: process all documents of a folder in one run. Each document is sent to the AI along with your instructions – either a free prompt, one of your document analysis policies, or instructions you import from a file. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table, which you can name yourself. Every run writes a log that lists each document with its processing time, the model, the status, and the reason for any error. When you start another run on the same output folder, we ask whether you want to continue it: documents that failed or are missing from the log are processed again, which is helpful after a crash or when documents exceeded the context window of your model. A single failing document never stops the run, and you can cancel at any time. The assistant was contributed by Jens Erler (j-erler) and marks his first contribution to AI Studio. Thank you, Jens, for this wonderful and useful contribution.
  • Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new .config-tests directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure.
  • +
  • Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new AllowUserProvidedAPIKey option does this, and it works for LLM_PROVIDERS, EMBEDDING_PROVIDERS, and TRANSCRIPTION_PROVIDERS alike.
  • Added CSV and TSV files to the file types you can attach. AI Studio was already able to read them, but they could not be selected.
  • Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed.
  • Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does.
  • @@ -145,6 +146,8 @@
  • Fixed preview features contributed by several configuration plugins at once. Only the most recent contribution was recognized as coming from your organization, so features enabled by another configuration looked as if you had switched them on yourself. Each configuration is now tracked separately, which lets your organization enable one preview feature company-wide and another one for a single department.
  • Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log.
  • Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it.
  • +
  • Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change.
  • +
  • Fixed AI Studio installing a second copy of itself next to an existing installation. Its updater always installs into your personal user folder, so an installation elsewhere, such as one your IT department rolled out, was never replaced. AI Studio now recognizes those installations and leaves them alone. The information page tells you which case applies to yours. For IT departments: automatic updates can now stay enabled for everybody, and the Enterprise IT documentation explains the rest.
  • Removed the legacy PowerPoint format (.ppt) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern .pptx format is not affected.
  • Upgraded dependencies to their latest versions to improve security and stability.
From 8148b66876e44def94188be236ecf71a2deb3b1d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 15 Aug 2026 19:55:42 +0200 Subject: [PATCH 12/77] Fixed plugin installation and endless plugin reloading on Linux (#924) --- .../Assistants/Builder/AssistantBuilder.razor | 8 +- .../Settings/SettingsManager.cs | 33 +++- .../Assistants/PluginAssistants.cs | 30 +++- .../PluginSystem/PluginFactory.HotReload.cs | 165 ++++++++++++------ .../Tools/PluginSystem/PluginFactory.cs | 56 +++++- .../PluginInstallService.AssistantBuilder.cs | 8 +- .../Services/PluginInstallService.Delete.cs | 8 +- .../Services/PluginInstallService.Editing.cs | 4 + .../PluginInstallService.FileSystem.cs | 124 +++++++++++++ .../Services/PluginInstallService.Import.cs | 5 +- .../PluginInstallService.Installation.cs | 18 +- .../Tools/Services/PluginInstallService.cs | 13 ++ .../Tools/Services/PluginShareService.cs | 17 +- app/MindWork AI Studio/packages.lock.json | 6 +- .../wwwroot/changelog/v26.8.1.md | 2 + 15 files changed, 402 insertions(+), 95 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/Builder/AssistantBuilder.razor b/app/MindWork AI Studio/Assistants/Builder/AssistantBuilder.razor index c2951401..6ff85253 100644 --- a/app/MindWork AI Studio/Assistants/Builder/AssistantBuilder.razor +++ b/app/MindWork AI Studio/Assistants/Builder/AssistantBuilder.razor @@ -111,7 +111,7 @@ else @T("The generated assistant could not be checked.") @if (!string.IsNullOrWhiteSpace(this.installFlowIssue)) { - @string.Format(T("Issue: {0}"), this.installFlowIssue) + @string.Format(T("Issue: {0}"), this.installFlowIssue) } } @@ -143,7 +143,7 @@ else @T("The assistant could not be installed.") @if (!string.IsNullOrWhiteSpace(this.installFlowIssue)) { - @string.Format(T("Issue: {0}"), this.installFlowIssue) + @string.Format(T("Issue: {0}"), this.installFlowIssue) } } @@ -177,7 +177,7 @@ else @T("The security audit could not be completed.") @if (!string.IsNullOrWhiteSpace(this.installFlowIssue)) { - @string.Format(T("Issue: {0}"), this.installFlowIssue) + @string.Format(T("Issue: {0}"), this.installFlowIssue) } } @@ -209,7 +209,7 @@ else @T("The assistant cannot be enabled.") @if (!string.IsNullOrWhiteSpace(this.installFlowIssue)) { - @string.Format(T("Issue: {0}"), this.installFlowIssue) + @string.Format(T("Issue: {0}"), this.installFlowIssue) } } diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 286c4fa6..1242c78e 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -334,10 +334,35 @@ public sealed class SettingsManager } var settingsJson = JsonSerializer.Serialize(settingsData, JSON_OPTIONS); - var tempFile = Path.GetTempFileName(); - await File.WriteAllTextAsync(tempFile, settingsJson); - - File.Move(tempFile, settingsPath, true); + + // + // We write the new settings next to the previous ones and replace them afterwards, so that + // no crash can leave a half-written settings file behind. The temporary file has to live in + // the configuration directory for that: replacing a file is a rename, and a rename across a + // file system boundary falls back to copying, which is exactly what we want to avoid. The + // temporary directory of the operating system is such another file system under Flatpak. + // + var tempFile = $"{settingsPath}.tmp-{Guid.NewGuid():N}"; + try + { + await File.WriteAllTextAsync(tempFile, settingsJson); + File.Move(tempFile, settingsPath, true); + } + catch + { + try + { + if (File.Exists(tempFile)) + File.Delete(tempFile); + } + catch (Exception cleanupException) + { + this.logger.LogWarning(cleanupException, $"Failed to delete the temporary settings file '{tempFile}'."); + } + + throw; + } + this.logger.LogInformation($"Stored the settings to '{settingsPath}'."); } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs index 9c610c85..1db864d3 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -301,12 +301,40 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType return fileMap.ToImmutable(); } + /// + /// The audit hash of this plugin, together with the directory it was computed for. + /// + /// + /// One record instead of two fields, so that a reader always sees a directory and a hash which + /// belong together. Recomputing the same hash twice costs nothing but time, mixing up a hash + /// with the wrong directory would show a wrong security state. + /// + private sealed record AuditHashCache(string PluginPath, string Hash); + + private AuditHashCache? auditHashCache; + /// /// Computes a stable audit hash across all Lua files by hashing a canonical /// sequence of relative path length, relative path, content length, and content /// for each file in ordinal path order. /// - public string ComputeAuditHash() => AssistantPluginHash.Compute(this.PluginPath); + /// + /// The result is kept, because computing it reads every Lua file of the plugin, and the plugins + /// page as well as the assistants page ask for it on every render. That is safe: the files of + /// one plugin instance never change. Whenever something in the plugins directory changes, the + /// plugin factory reloads and creates new instances, cf. PluginFactory.Starting.RestartAllPlugins. + /// The plugin directory is assigned after the instance was created, so the cache remembers which + /// directory it belongs to. + /// + public string ComputeAuditHash() + { + if (this.auditHashCache is { } cache && string.Equals(cache.PluginPath, this.PluginPath, StringComparison.Ordinal)) + return cache.Hash; + + var hash = AssistantPluginHash.Compute(this.PluginPath); + this.auditHashCache = new(this.PluginPath, hash); + return hash; + } private static string BuildSecureSystemPrompt(string pluginSystemPrompt) { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs index 0505787c..3791cdc9 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs @@ -1,9 +1,26 @@ +using Timer = System.Timers.Timer; + namespace AIStudio.Tools.PluginSystem; public static partial class PluginFactory { private static readonly SemaphoreSlim HOT_RELOAD_SEMAPHORE = new(1, 1); - + + /// + /// How long the plugins directory has to stay quiet before we reload. + /// + /// + /// One change never arrives as one event: writing a single file produces several, and moving an + /// entire plugin directory into place produces dozens. Reloading on each of them would restart + /// every plugin over and over. + /// + private static readonly TimeSpan HOT_RELOAD_DEBOUNCE_INTERVAL = TimeSpan.FromSeconds(1); + + private static readonly Timer HOT_RELOAD_DEBOUNCE_TIMER = new(HOT_RELOAD_DEBOUNCE_INTERVAL) + { + AutoReset = false, + }; + public static void SetUpHotReloading() { if (!IsInitialized) @@ -11,18 +28,26 @@ public static partial class PluginFactory LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it."); return; } - + LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'."); try { + HOT_RELOAD_DEBOUNCE_TIMER.Elapsed += (_, _) => _ = ReloadPluginsAsync(); + HOT_RELOAD_WATCHER.IncludeSubdirectories = true; - HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.CreationTime - | NotifyFilters.DirectoryName + + // + // We watch for plugins appearing, disappearing, and changing. We do not watch access + // times: reading a plugin is not a change, and on Linux our own reads would be + // reported back to us. Loading the plugins and computing the audit hash of an + // assistant plugin both read every Lua file in this directory, so such a filter + // makes each reload cause the next one: + // + HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName - | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Size; - + HOT_RELOAD_WATCHER.Changed += HotReloadEventHandler; HOT_RELOAD_WATCHER.Deleted += HotReloadEventHandler; HOT_RELOAD_WATCHER.Created += HotReloadEventHandler; @@ -42,64 +67,96 @@ public static partial class PluginFactory LOG.LogInformation("Hot reloading plugins set up."); } } - - private static async void HotReloadEventHandler(object _, FileSystemEventArgs args) + + private static void HotReloadEventHandler(object _, FileSystemEventArgs args) { try { - var changeType = args.ChangeType.ToString().ToLowerInvariant(); - if (!await HOT_RELOAD_SEMAPHORE.WaitAsync(0)) - { - LOG.LogInformation($"File changed '{args.FullPath}' (event={changeType}). Already processing another change."); + // + // Our own lock file lives in the watched directory. Writing and removing it are not + // plugin changes, and reacting to them would turn every locked operation into a + // reload of its own: + // + if (IsHotReloadLockFile(args.FullPath)) return; - } - try - { - LOG.LogInformation($"File changed '{args.FullPath}' (event={changeType}). Reloading plugins..."); - if (File.Exists(HOT_RELOAD_LOCK_FILE)) - { - LOG.LogInformation("Hot reload lock file exists. Waiting for it to be released before proceeding with the reload."); + var changeType = args.ChangeType.ToString().ToLowerInvariant(); + LOG.LogInformation($"File changed '{args.FullPath}' (event={changeType}). Scheduling a plugin reload."); - var lockFileCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - var token = lockFileCancellationTokenSource.Token; - var waitTime = TimeSpan.FromSeconds(1); - while (File.Exists(HOT_RELOAD_LOCK_FILE) && !token.IsCancellationRequested) - { - try - { - LOG.LogDebug("Waiting for hot reload lock to be released..."); - await Task.Delay(waitTime, token); - waitTime = TimeSpan.FromSeconds(Math.Min(waitTime.TotalSeconds * 2, 120)); // Exponential backoff with a cap - } - catch (TaskCanceledException) - { - // Case: The cancellation token was triggered, meaning the lock file is still present. - // We expect that something goes wrong. So, we try to delete the lock file: - LOG.LogWarning("Hot reload lock file still exists after 30 seconds. Attempting to delete it..."); - UnlockHotReload(); - break; - } - } - - LOG.LogInformation("Hot reload lock file released. Proceeding with plugin reload."); - } - - await LoadAll(); - await MessageBus.INSTANCE.SendMessage(null, Event.PLUGINS_RELOADED); - } - catch(Exception e) - { - LOG.LogError(e, $"Error while reloading plugins after change in file '{args.FullPath}' with change type '{changeType}'."); - } - finally - { - HOT_RELOAD_SEMAPHORE.Release(); - } + // Restart the debounce window, so that a burst of events results in one reload: + HOT_RELOAD_DEBOUNCE_TIMER.Stop(); + HOT_RELOAD_DEBOUNCE_TIMER.Start(); } catch (Exception e) { LOG.LogError(e, $"Error while handling hot reload event for file '{args.FullPath}' with change type '{args.ChangeType}'."); } } + + private static bool IsHotReloadLockFile(string path) + { + if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(HOT_RELOAD_LOCK_FILE)) + return false; + + return string.Equals(path, HOT_RELOAD_LOCK_FILE, StringComparison.OrdinalIgnoreCase); + } + + private static async Task ReloadPluginsAsync() + { + // + // Reloads must never overlap. When one is still running, we do not drop this one: the + // changes which triggered it might have arrived after the running reload had already read + // them. We try again after another quiet window instead: + // + if (!await HOT_RELOAD_SEMAPHORE.WaitAsync(0)) + { + LOG.LogInformation("A plugin reload is already running. Waiting for it to finish before reloading again."); + HOT_RELOAD_DEBOUNCE_TIMER.Stop(); + HOT_RELOAD_DEBOUNCE_TIMER.Start(); + return; + } + + try + { + LOG.LogInformation("Reloading plugins..."); + if (File.Exists(HOT_RELOAD_LOCK_FILE)) + { + LOG.LogInformation("Hot reload lock file exists. Waiting for it to be released before proceeding with the reload."); + + var lockFileCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var token = lockFileCancellationTokenSource.Token; + var waitTime = TimeSpan.FromSeconds(1); + while (File.Exists(HOT_RELOAD_LOCK_FILE) && !token.IsCancellationRequested) + { + try + { + LOG.LogDebug("Waiting for hot reload lock to be released..."); + await Task.Delay(waitTime, token); + waitTime = TimeSpan.FromSeconds(Math.Min(waitTime.TotalSeconds * 2, 120)); // Exponential backoff with a cap + } + catch (TaskCanceledException) + { + // Case: The cancellation token was triggered, meaning the lock file is still present. + // We expect that something goes wrong. So, we try to delete the lock file: + LOG.LogWarning("Hot reload lock file still exists after 30 seconds. Attempting to delete it..."); + UnlockHotReload(); + break; + } + } + + LOG.LogInformation("Hot reload lock file released. Proceeding with plugin reload."); + } + + // LoadAll announces the reload itself, cf. PluginFactory.Starting.RestartAllPlugins: + await LoadAll(); + } + catch(Exception e) + { + LOG.LogError(e, "Error while reloading plugins after a change in the plugins directory."); + } + finally + { + HOT_RELOAD_SEMAPHORE.Release(); + } + } } \ 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 39689c8b..feeb2434 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -290,7 +290,25 @@ public static partial class PluginFactory return AVAILABLE_PLUGINS.Any(plugin => plugin.Id == configPluginId && plugin.Type is PluginType.CONFIGURATION && IsEnterpriseTestConfigurationPath(plugin.LocalPath)); } - private static async Task LockHotReloadAsync() + /// + /// Counts how many operations currently write to the plugins directory. + /// + /// + /// Downloading an organization's configuration and installing a plugin can run at the same + /// time. Without counting, whichever finishes first would unlock hot reloading while the other + /// is still writing. + /// + private static int HOT_RELOAD_LOCK_COUNT; + private static readonly SemaphoreSlim HOT_RELOAD_LOCK_SEMAPHORE = new(1, 1); + + /// + /// Holds back hot reloading while the caller writes to the plugins directory. + /// + /// + /// Every caller has to release the lock again, so wrap the write in a try-finally block. Hot + /// reloading resumes once the last caller has released it. + /// + public static async Task LockHotReloadAsync() { if (!IsInitialized) { @@ -298,23 +316,28 @@ public static partial class PluginFactory return; } + await HOT_RELOAD_LOCK_SEMAPHORE.WaitAsync(); try { - if (File.Exists(HOT_RELOAD_LOCK_FILE)) - { - LOG.LogWarning("Hot reload lock file already exists."); + if (HOT_RELOAD_LOCK_COUNT++ > 0) return; - } - + await File.WriteAllTextAsync(HOT_RELOAD_LOCK_FILE, DateTime.UtcNow.ToString("o")); } catch (Exception e) { LOG.LogError(e, "An error occurred while trying to lock hot reloading."); } + finally + { + HOT_RELOAD_LOCK_SEMAPHORE.Release(); + } } - private static void UnlockHotReload() + /// + /// Releases the hot reload lock of one caller, see LockHotReloadAsync. + /// + public static void UnlockHotReload() { if (!IsInitialized) { @@ -322,8 +345,20 @@ public static partial class PluginFactory return; } + HOT_RELOAD_LOCK_SEMAPHORE.Wait(); try { + // + // The count can be zero when the reload gave up waiting and removed the lock file + // itself. We must not go negative, because that would keep the next lock from ever + // writing the file again: + // + if (HOT_RELOAD_LOCK_COUNT > 0) + HOT_RELOAD_LOCK_COUNT--; + + if (HOT_RELOAD_LOCK_COUNT > 0) + return; + if(File.Exists(HOT_RELOAD_LOCK_FILE)) File.Delete(HOT_RELOAD_LOCK_FILE); else @@ -333,14 +368,19 @@ public static partial class PluginFactory { LOG.LogError(e, "An error occurred while trying to unlock hot reloading."); } + finally + { + HOT_RELOAD_LOCK_SEMAPHORE.Release(); + } } public static void Dispose() { if(!IsInitialized) return; - + HOT_RELOAD_WATCHER.Dispose(); + HOT_RELOAD_DEBOUNCE_TIMER.Dispose(); } public static IReadOnlyList GetMandatoryInfos() diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.AssistantBuilder.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.AssistantBuilder.cs index 8fb327f8..c345c835 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.AssistantBuilder.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.AssistantBuilder.cs @@ -7,7 +7,7 @@ public sealed partial class PluginInstallService { /// /// Checks whether generated Lua assistant plugin code can be loaded and installed. - /// The plugin is written to a temporary staging directory and validated through the + /// The plugin is written to a staging directory and validated through the /// normal plugin loader, but it is not moved into the user plugin directory. /// /// The full generated plugin.lua content. @@ -44,7 +44,7 @@ public sealed partial class PluginInstallService /// /// Installs generated Lua assistant plugin code into the user plugin directory. - /// Writes the plugin into a temporary staging directory first, validates it through the + /// Writes the plugin into a staging directory first, validates it through the /// normal plugin loader, then moves into data/plugins/assistants. /// If plugin with same ID already exists, the existing directory is moved /// aside as backup and restored when replacement fails. @@ -84,11 +84,11 @@ public sealed partial class PluginInstallService return PluginValidationResult.Failure(TB("The plugin system is not initialized yet.")); var pluginCode = lua.Trim(); - var stagingDirectory = Path.Join(Path.GetTempPath(), $"{ASSISTANT_BUILDER_DIRECTORY_PREFIX}.staging-{Guid.NewGuid():N}"); + if (!this.TryCreateStagingDirectory(ASSISTANT_BUILDER_DIRECTORY_PREFIX, out var stagingDirectory, out var stagingIssue)) + return PluginValidationResult.Failure(stagingIssue); try { - Directory.CreateDirectory(stagingDirectory); var stagedPluginFile = Path.Join(stagingDirectory, PLUGIN_FILE_NAME); await File.WriteAllTextAsync(stagedPluginFile, pluginCode, Encoding.UTF8, token); diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs index 6a08b7e0..215430ff 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs @@ -106,6 +106,9 @@ public sealed partial class PluginInstallService var backupDirectory = string.Empty; var sideEffects = PluginDeleteSideEffects.NONE; + // We reload the plugins ourselves below. Holding back hot reloading keeps the file system + // watcher from starting a second reload while the plugin is being moved away: + await PluginFactory.LockHotReloadAsync(); try { // Check again under the semaphore: another operation might have changed the plugin state @@ -116,7 +119,7 @@ public sealed partial class PluginInstallService backupDirectory = CreateDeleteBackupDirectory(plugin); Directory.CreateDirectory(Path.GetDirectoryName(backupDirectory)!); - Directory.Move(pluginDirectory, backupDirectory); + this.MoveDirectory(pluginDirectory, backupDirectory); sideEffects = this.ApplyDeleteSideEffects(plugin); if (sideEffects.HasChanges) @@ -137,6 +140,7 @@ public sealed partial class PluginInstallService } finally { + PluginFactory.UnlockHotReload(); this.installSemaphore.Release(); } } @@ -241,7 +245,7 @@ public sealed partial class PluginInstallService try { if (!Directory.Exists(pluginDirectory) && Directory.Exists(backupDirectory)) - Directory.Move(backupDirectory, pluginDirectory); + this.MoveDirectory(backupDirectory, pluginDirectory); var configurationData = this.settingsManager.ConfigurationData; if (sideEffects.WasEnabled && !configurationData.EnabledPlugins.Contains(plugin.Id)) diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Editing.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Editing.cs index 6824a4f3..eb20f103 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Editing.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Editing.cs @@ -97,6 +97,9 @@ public sealed partial class PluginInstallService var tempFile = string.Empty; var backupFile = string.Empty; + // We reload the plugins ourselves below. Holding back hot reloading keeps the file system + // watcher from starting a second reload while the plugin file is being replaced: + await PluginFactory.LockHotReloadAsync(); try { var validation = await this.ValidateInPluginDirectoryAsync(lua, pluginDirectory, token); @@ -144,6 +147,7 @@ public sealed partial class PluginInstallService { this.TryDeleteFile(tempFile, "assistant plugin edit temp file"); + PluginFactory.UnlockHotReload(); this.installSemaphore.Release(); } } diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.FileSystem.cs index 2f4e15ac..847daf50 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.FileSystem.cs @@ -1,7 +1,131 @@ +using AIStudio.Settings; + namespace AIStudio.Tools.Services; public sealed partial class PluginInstallService { + /// + /// Creates a staging directory for a plugin that is about to be installed. + /// + /// + /// The staging directory lives below the data directory, never below the temporary directory of + /// the operating system. Installing means moving the staged plugin into the plugins directory, + /// and a directory move cannot cross a file system boundary. Flatpak is the case where this + /// always applies: its temporary directory is a tmpfs inside the sandbox, while the data + /// directory lives in the home directory of the user.

+ /// It lives next to the plugins directory, not inside it: the plugin loader searches the plugins + /// directory recursively, so a half-written plugin there would be loaded while it is still being + /// staged. + ///
+ /// The prefix of the staging directory name, naming the caller. + /// The created staging directory. + /// A user-facing issue when the staging directory could not be created. + /// True when the staging directory exists, false otherwise. + private bool TryCreateStagingDirectory(string prefix, out string stagingDirectory, out string issue) + { + stagingDirectory = string.Empty; + issue = string.Empty; + + var dataDirectory = SettingsManager.DataDirectory; + if (string.IsNullOrWhiteSpace(dataDirectory)) + { + issue = TB("The AI Studio data directory is not initialized yet."); + return false; + } + + var stagingRoot = Path.Join(dataDirectory, STAGING_DIRECTORY); + try + { + Directory.CreateDirectory(stagingRoot); + this.CleanUpExpiredStagingDirectories(stagingRoot); + + stagingDirectory = Path.Join(stagingRoot, $"{prefix}.staging-{Guid.NewGuid():N}"); + Directory.CreateDirectory(stagingDirectory); + return true; + } + catch (Exception e) + { + this.logger.LogError(e, "Failed to create the plugin staging directory below '{StagingRoot}'.", stagingRoot); + stagingDirectory = string.Empty; + issue = string.Format(TB("Unexpected error: {0}"), e.Message); + return false; + } + } + + /// + /// Removes staging directories which an earlier installation left behind, e.g. after a crash. + /// + private void CleanUpExpiredStagingDirectories(string stagingRoot) + { + var expiry = DateTime.UtcNow.AddHours(-STAGING_RETENTION_HOURS); + foreach (var leftOverDirectory in Directory.EnumerateDirectories(stagingRoot, "*", SearchOption.TopDirectoryOnly)) + { + try + { + if (Directory.GetLastWriteTimeUtc(leftOverDirectory) < expiry) + Directory.Delete(leftOverDirectory, true); + } + catch (Exception e) + { + this.logger.LogWarning(e, "Failed to delete the left-over plugin staging directory '{StagingDirectory}'.", leftOverDirectory); + } + } + } + + /// + /// Moves a directory and falls back to copying it when the move crosses a file system boundary. + /// + /// + /// On Unix-like systems, a directory move is a plain rename, which fails as soon as source and + /// destination live on different file systems. A file move falls back to copy and delete in that + /// case, a directory move does not. Everything this service moves stays below the data directory, + /// so the fallback is not expected to run. It keeps installing and deleting plugins working when + /// a setup spreads the data directory across mounts.

+ /// The fallback only applies when the move failed for that reason: when the destination is + /// already taken, the caller has to learn about it instead of getting the two directories merged. + ///

+ /// A failing copy leaves nothing behind: the half-written destination is removed before the + /// error reaches the caller. Every caller rolls back by asking whether the destination exists, + /// so a partial copy would look like a completed move and keep the backup from being restored. + ///
+ /// The directory to move. + /// The directory to move it to. It must not exist yet. + private void MoveDirectory(string sourceDirectory, string destinationDirectory) + { + try + { + Directory.Move(sourceDirectory, destinationDirectory); + return; + } + catch (IOException e) when (Directory.Exists(sourceDirectory) && !Directory.Exists(destinationDirectory)) + { + this.logger.LogWarning(e, "Was not able to move the directory '{SourceDirectory}' to '{DestinationDirectory}'. Falling back to copying it.", sourceDirectory, destinationDirectory); + } + + try + { + CopyDirectory(sourceDirectory, destinationDirectory); + } + catch + { + TryDeleteDirectory(destinationDirectory, "partially copied plugin", this.logger); + throw; + } + + Directory.Delete(sourceDirectory, true); + } + + private static void CopyDirectory(string sourceDirectory, string destinationDirectory) + { + Directory.CreateDirectory(destinationDirectory); + + foreach (var filePath in Directory.EnumerateFiles(sourceDirectory)) + File.Copy(filePath, Path.Join(destinationDirectory, Path.GetFileName(filePath)), true); + + foreach (var subDirectory in Directory.EnumerateDirectories(sourceDirectory)) + CopyDirectory(subDirectory, Path.Join(destinationDirectory, Path.GetFileName(subDirectory))); + } + private static bool IsPathInsideDirectory(string parentDirectory, string path) { var parentPath = Path.GetFullPath(parentDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Import.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Import.cs index e2357f2f..a32455ce 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Import.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Import.cs @@ -38,10 +38,13 @@ public sealed partial class PluginInstallService return Error(TB("The plugin system is not initialized yet.")); await this.installSemaphore.WaitAsync(token); - var stagingDirectory = Path.Join(Path.GetTempPath(), $"plugin-import.staging-{Guid.NewGuid():N}"); + var stagingDirectory = string.Empty; try { token.ThrowIfCancellationRequested(); + if (!this.TryCreateStagingDirectory(PLUGIN_IMPORT_DIRECTORY_PREFIX, out stagingDirectory, out var stagingIssue)) + return Error(stagingIssue); + PluginArchive.Extract(archivePath, stagingDirectory); var pluginFiles = Directory.EnumerateFiles(stagingDirectory, PLUGIN_FILE_NAME, SearchOption.AllDirectories).ToArray(); diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Installation.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Installation.cs index d6862f31..6a4a6852 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Installation.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Installation.cs @@ -16,16 +16,19 @@ public sealed partial class PluginInstallService var replacedExisting = false; var movedIntoPlace = false; + // We reload the plugins ourselves below. Holding back hot reloading keeps the file system + // watcher from starting a second reload while the plugin is being moved into place: + await PluginFactory.LockHotReloadAsync(); try { Directory.CreateDirectory(pluginRoot); finalDirectory = DetermineFinalDirectory(pluginRoot, plugin, pluginType); if (!IsPathInsideDirectory(pluginRoot, finalDirectory)) - return Error(TB("The resolved plugin directory is outside the plugin directory.")); + return Error(plugin, finalDirectory, TB("The resolved plugin directory is outside the plugin directory.")); var replacementIssue = GetReplacementIssue(plugin.Id, pluginType); if (!string.IsNullOrWhiteSpace(replacementIssue)) - return Error(replacementIssue); + return Error(plugin, finalDirectory, replacementIssue); if (Directory.Exists(finalDirectory)) { @@ -36,10 +39,10 @@ public sealed partial class PluginInstallService // would be loaded a second time, next to the version we are installing: backupDirectory = CreateInstallBackupDirectory(plugin); Directory.CreateDirectory(Path.GetDirectoryName(backupDirectory)!); - Directory.Move(finalDirectory, backupDirectory); + this.MoveDirectory(finalDirectory, backupDirectory); } - Directory.Move(stagingDirectory, finalDirectory); + this.MoveDirectory(stagingDirectory, finalDirectory); movedIntoPlace = true; await PluginFactory.LoadAll(token); @@ -51,7 +54,7 @@ public sealed partial class PluginInstallService } catch (Exception e) { - this.logger.LogError(e, "Failed to install plugin."); + this.logger.LogError(e, "Failed to install the {PluginType} plugin '{PluginName}' ({PluginId}) into '{PluginDirectory}'.", pluginType, plugin.Name, plugin.Id, finalDirectory); // Only remove the target directory when this installation actually moved the plugin // there. Otherwise, when moving the previous plugin into the backup directory failed, @@ -63,7 +66,7 @@ public sealed partial class PluginInstallService { try { - Directory.Move(backupDirectory, finalDirectory); + this.MoveDirectory(backupDirectory, finalDirectory); await PluginFactory.LoadAll(CancellationToken.None); } catch (Exception restoreException) @@ -72,11 +75,12 @@ public sealed partial class PluginInstallService } } - return Error(string.Format(TB("Unexpected error: {0}"), e.Message)); + return Error(plugin, finalDirectory ?? string.Empty, string.Format(TB("Unexpected error: {0}"), e.Message)); } finally { this.TryDeleteStagingDirectory(stagingDirectory); + PluginFactory.UnlockHotReload(); } } diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.cs index 44a81d52..81918e1a 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.cs @@ -23,8 +23,11 @@ public sealed partial class PluginInstallService private const string PLUGIN_FILE_NAME = "plugin.lua"; private const string ASSISTANT_BUILDER_DIRECTORY_PREFIX = "assistant-builder"; + private const string PLUGIN_IMPORT_DIRECTORY_PREFIX = "plugin-import"; private const string DELETE_BACKUP_DIRECTORY = ".plugin-delete-backups"; private const string INSTALL_BACKUP_DIRECTORY = ".plugin-install-backups"; + private const string STAGING_DIRECTORY = ".plugin-staging"; + private const int STAGING_RETENTION_HOURS = 24; private const int DIRECTORY_PREFIX_MAX_LEN = 80; private readonly ILogger logger; @@ -35,6 +38,16 @@ public sealed partial class PluginInstallService private static AssistantPluginInstallResult Error(string issue) => new(false, Guid.Empty, string.Empty, string.Empty, false, issue); + /// + /// Reports a failed installation of a plugin we already know. + /// + /// + /// Prefer this over the variant which only takes an issue: the caller logs the plugin and the + /// directory it tried to install into, and both are empty otherwise. Everything that fails + /// before we could read the plugin has to use the other variant. + /// + private static AssistantPluginInstallResult Error(IPluginMetadata plugin, string pluginDirectory, string issue) => new(false, plugin.Id, plugin.Name, pluginDirectory, 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); diff --git a/app/MindWork AI Studio/Tools/Services/PluginShareService.cs b/app/MindWork AI Studio/Tools/Services/PluginShareService.cs index 9b7b2983..1c5f2c05 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginShareService.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginShareService.cs @@ -72,16 +72,19 @@ public sealed class PluginShareService(NativeShareService nativeShareService, Ru try { token.ThrowIfCancellationRequested(); - await Task.Run(() => + await Task.Run(async () => { token.ThrowIfCancellationRequested(); - // The save dialog already asked the user about overwriting an existing file. - // ZipFile.CreateFromDirectory would fail on an existing file, though: - if (File.Exists(archivePath)) - File.Delete(archivePath); - - ZipFile.CreateFromDirectory(pluginRoot, archivePath, CompressionLevel.Optimal, false); + // + // The save dialog already asked the user about overwriting an existing file, so we + // write into the file the user picked instead of removing and recreating it. That + // matters on Linux: inside a Flatpak, the file dialog hands out one single file + // through the desktop portal. We may write that file, but we may not create a new + // one next to it, which is what deleting and recreating would come down to. + // + await using var archiveStream = File.Create(archivePath); + ZipFile.CreateFromDirectory(pluginRoot, archiveStream, CompressionLevel.Optimal, false); }, token); logger.LogInformation("Exported plugin '{PluginName}' ({PluginId}) to the archive '{ArchivePath}'.", plugin.Name, plugin.Id, archivePath); diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 3bd053e7..659e11fa 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -41,9 +41,9 @@ }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "I9GkKrCVjzxGU1hsKSurOW6P/ABPPHARfc/MTnzIgDb8YjJ/votxKN2z7K+J3DvlQXGH0O7KqdtBseRj8j7eNQ==" + "requested": "[9.0.18, )", + "resolved": "9.0.18", + "contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg==" }, "MudBlazor": { "type": "Direct", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index ce1893dd..bdc5d90c 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -42,5 +42,7 @@ - Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it. - Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change. - Fixed AI Studio installing a second copy of itself next to an existing installation. Its updater always installs into your personal user folder, so an installation elsewhere, such as one your IT department rolled out, was never replaced. AI Studio now recognizes those installations and leaves them alone. The information page tells you which case applies to yours. For IT departments: automatic updates can now stay enabled for everybody, and the Enterprise IT documentation explains the rest. +- Fixed installing an assistant on Linux when AI Studio runs as a Flatpak. The Assistant Builder was able to create an assistant, but installing it always ended with an unexpected error. +- Fixed the plugins page and the assistants page reloading again and again on Linux, which started as soon as an assistant was installed. Both pages kept flickering, and nothing on them could be used anymore until you left for another page. - Removed the legacy PowerPoint format (`.ppt`) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern `.pptx` format is not affected. - Upgraded dependencies to their latest versions to improve security and stability. \ No newline at end of file From da68051c61ae462285ef937d62179e37f53491a1 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 16 Aug 2026 12:18:53 +0200 Subject: [PATCH 13/77] Clarify update guidance and strongly type Linux package detection (#925) --- .../Assistants/I18N/allTexts.lua | 61 ++++++++++--------- .../Settings/SettingsPanelApp.razor.cs | 8 +-- .../Pages/Information.razor.cs | 30 +++++---- .../plugin.lua | 61 ++++++++++--------- .../plugin.lua | 61 ++++++++++--------- app/MindWork AI Studio/Program.cs | 2 +- .../Tools/PandocProcessBuilder.cs | 5 +- .../Tools/Rust/LinuxPackageType.cs | 19 ++++++ .../Tools/Rust/RuntimeInfoResponse.cs | 2 +- .../Tools/Services/UpdatePolicy.cs | 2 +- runtime/src/environment.rs | 46 +++++++++++--- 11 files changed, 179 insertions(+), 118 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Rust/LinuxPackageType.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 0b867a6f..c40a386a 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4066,6 +4066,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] -- Root certificate bundle path UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Root certificate bundle path" +-- AI Studio cannot install updates into its current installation location. Install new versions yourself. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T14786838"] = "AI Studio cannot install updates into its current installation location. Install new versions yourself." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar." @@ -4078,9 +4081,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] -- seconds UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds" --- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own." - -- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled." @@ -4102,6 +4102,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] -- Your organization has disabled update checks and installations. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Your organization has disabled update checks and installations." +-- AI Studio cannot install updates when running as a Flatpak. Update it using the Flatpak source or bundle from which you installed it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2009652585"] = "AI Studio cannot install updates when running as a Flatpak. Update it using the Flatpak source or bundle from which you installed it." + -- When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2013281167"] = "When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization." @@ -4120,9 +4123,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] -- Update installation method UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T237706157"] = "Update installation method" --- AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T244540698"] = "AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution." - -- Language UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Language" @@ -4135,15 +4135,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] -- Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"] = "Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox." +-- This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2918560776"] = "This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information." + -- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates." -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" --- AI Studio cannot install updates into this installation. Your IT department provides new versions. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio cannot install updates into this installation. Your IT department provides new versions." - -- Development builds do not install updates. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Development builds do not install updates." @@ -4189,9 +4189,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] -- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." --- AI Studio cannot install updates into its current location. Install new versions yourself. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio cannot install updates into its current location. Install new versions yourself." - -- Select a transcription provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider" @@ -4207,6 +4204,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] -- Select a root certificate bundle UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle" +-- AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T476576809"] = "AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions." + -- Navigation bar behavior UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior" @@ -8290,9 +8290,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:" --- in a location AI Studio cannot update itself from -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "in a location AI Studio cannot update itself from" - -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" @@ -8302,9 +8299,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." --- owned by you, self-updates possible -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "owned by you, self-updates possible" - -- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents." @@ -8338,9 +8332,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" --- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation." - -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others." @@ -8359,9 +8350,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2272122662"] = "Configuration se -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." +-- Flatpak installation, updates are handled outside of AI Studio +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2294279524"] = "Flatpak installation, updates are handled outside of AI Studio" + -- Configuration plugin ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:" +-- AI Studio cannot update itself from its current installation location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2307318338"] = "AI Studio cannot update itself from its current installation location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation." + -- dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2325338322"] = "dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses." @@ -8386,11 +8383,14 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro -- Installed Pandoc version: Pandoc is not installed or not available. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available." +-- current installation location does not support automatic updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2401198677"] = "current installation location does not support automatic updates" + -- Configuration origin: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:" --- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio." +-- This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2444057400"] = "This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions." -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:" @@ -8404,6 +8404,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is -- Used Open Source Projects UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source Projects" +-- development build, no support for automatic updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2582380608"] = "development build, no support for automatic updates" + -- Build time UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" @@ -8449,6 +8452,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation" -- checking availability UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability" +-- managed; updates are handled outside of AI Studio; contact whoever installed it and ask about updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T285730904"] = "managed; updates are handled outside of AI Studio; contact whoever installed it and ask about updates" + -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software." @@ -8482,9 +8488,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:" --- managed by someone else, no self-updates -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "managed by someone else, no self-updates" - -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)." @@ -8542,9 +8545,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w -- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead." --- development build, no self-updates -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "development build, no self-updates" - -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" @@ -8563,6 +8563,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" -- active UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "active" +-- standard; automatic updates supported +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3656709502"] = "standard; automatic updates supported" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index 24a93ca5..ac5d07a0 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -29,7 +29,7 @@ public partial class SettingsPanelApp : SettingsPanelBase { UpdatePolicyMode.ENTERPRISE_DISABLED => T("Your organization has disabled update checks and installations."), UpdatePolicyMode.FLATPAK => T("AI Studio cannot check for updates when running as a Flatpak. Updates are managed outside the app."), - UpdatePolicyMode.MANAGED_INSTALLATION => T("This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own."), + UpdatePolicyMode.MANAGED_INSTALLATION => T("This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information."), UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION => T("AI Studio cannot update itself from its current location, so it does not check for updates."), UpdatePolicyMode.DEVELOPMENT => T("Development builds do not check for updates."), _ => T("How often should we check for app updates?") @@ -38,9 +38,9 @@ public partial class SettingsPanelApp : SettingsPanelBase private string UpdateInstallationHelp => this.updatePolicyMode switch { UpdatePolicyMode.ENTERPRISE_DISABLED => T("This setting has no effect while updates are disabled by your organization."), - UpdatePolicyMode.FLATPAK => T("AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution."), - UpdatePolicyMode.MANAGED_INSTALLATION => T("AI Studio cannot install updates into this installation. Your IT department provides new versions."), - UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION => T("AI Studio cannot install updates into its current location. Install new versions yourself."), + UpdatePolicyMode.FLATPAK => T("AI Studio cannot install updates when running as a Flatpak. Update it using the Flatpak source or bundle from which you installed it."), + UpdatePolicyMode.MANAGED_INSTALLATION => T("AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions."), + UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION => T("AI Studio cannot install updates into its current installation location. Install new versions yourself."), UpdatePolicyMode.DEVELOPMENT => T("Development builds do not install updates."), _ => T("Should updates be installed automatically or manually?") }; diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 7fc7c39f..0afd6652 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -65,20 +65,24 @@ public partial class Information : MSGComponentBase private string LinuxPackageTypeDisplayName => this.RuntimeInfo.LinuxPackageType switch { - "appimage" => "AppImage", - "flatpak" => "Flatpak", - "unknown" => T("unknown"), + Tools.Rust.LinuxPackageType.APP_IMAGE => "AppImage", + Tools.Rust.LinuxPackageType.FLATPAK => "Flatpak", + Tools.Rust.LinuxPackageType.UNKNOWN => T("unknown"), _ => T("not applicable") }; private string InstallationKind => $"{T("Installation")}: {this.InstallationKindDisplayName}"; - private string InstallationKindDisplayName => this.RuntimeInfo.InstallationKind switch + private string InstallationKindDisplayName => this.RuntimeInfo.LinuxPackageType switch { - Tools.Rust.InstallationKind.MANAGED => T("managed by someone else, no self-updates"), - Tools.Rust.InstallationKind.UNSUPPORTED_LOCATION => T("in a location AI Studio cannot update itself from"), - Tools.Rust.InstallationKind.DEVELOPMENT => T("development build, no self-updates"), - _ => T("owned by you, self-updates possible") + Tools.Rust.LinuxPackageType.FLATPAK => T("Flatpak installation, updates are handled outside of AI Studio"), + _ => this.RuntimeInfo.InstallationKind switch + { + Tools.Rust.InstallationKind.MANAGED => T("managed; updates are handled outside of AI Studio; contact whoever installed it and ask about updates"), + Tools.Rust.InstallationKind.UNSUPPORTED_LOCATION => T("current installation location does not support automatic updates"), + Tools.Rust.InstallationKind.DEVELOPMENT => T("development build, no support for automatic updates"), + _ => T("standard; automatic updates supported") + } }; private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}"; @@ -667,13 +671,13 @@ public partial class Information : MSGComponentBase } else if (this.updatePolicyMode is UpdatePolicyMode.MANAGED_INSTALLATION) { - // No release link here: this installation was deployed centrally, and installing a - // second copy from GitHub next to it is exactly what we want to avoid. - parameters.Add(x => x.Message, T("This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio.")); + // No release link here: the app cannot tell how this installation receives updates, + // and installing a second copy from GitHub next to it is exactly what we want to avoid. + parameters.Add(x => x.Message, T("This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions.")); } else if (this.updatePolicyMode is UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION) { - parameters.Add(x => x.Message, T("AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation.")); + parameters.Add(x => x.Message, T("AI Studio cannot update itself from its current installation location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation.")); parameters.Add(x => x.ReleaseUrl, "https://github.com/MindWorkAI/AI-Studio/releases/latest"); } else if (this.updatePolicyMode is UpdatePolicyMode.DEVELOPMENT) @@ -685,4 +689,4 @@ public partial class Information : MSGComponentBase await this.DialogService.ShowAsync(T("How to update"), parameters, DialogOptions.FULLSCREEN); } -} +} \ 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 e499b666..ab802437 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 @@ -4068,6 +4068,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] -- Root certificate bundle path UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Pfad zum Stammzertifikatsbundle" +-- AI Studio cannot install updates into its current installation location. Install new versions yourself. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T14786838"] = "AI Studio kann Updates am aktuellen Installationsort nicht installieren. Installieren Sie neue Versionen bitte selbst." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Wählen Sie das gewünschte Verhalten für die Navigationsleiste aus." @@ -4080,9 +4083,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] -- seconds UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "Sekunden" --- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "Diese Kopie von AI Studio wurde von einer anderen Person für Sie installiert und sucht daher nicht selbstständig nach Updates." - -- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Wählen Sie für die Transkription Ihrer Stimme einen Anbieter für Transkriptionen aus. Ohne einen ausgewählten Anbieter wird die Diktier- und Transkriptions-Funktion deaktiviert." @@ -4104,6 +4104,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] -- Your organization has disabled update checks and installations. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Ihre Organisation hat die Suche nach Updates und deren Installation deaktiviert." +-- AI Studio cannot install updates when running as a Flatpak. Update it using the Flatpak source or bundle from which you installed it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2009652585"] = "AI Studio kann keine Updates installieren, wenn es als Flatpak ausgeführt wird. Aktualisieren Sie es über die Flatpak-Quelle oder das Bundle, über die bzw. das Sie es installiert haben." + -- When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2013281167"] = "Wenn diese Option aktiviert ist, werden zusätzliche Optionen für die Administration angezeigt. Diese Optionen sind für IT-Mitarbeitende vorgesehen, um organisationsweite Einstellungen zu verwalten, z. B. Anbieter für eine gesamte Organisation zu konfigurieren und zu exportieren." @@ -4122,9 +4125,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] -- Update installation method UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T237706157"] = "Installationsmethode für Updates" --- AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T244540698"] = "AI Studio kann keine Updates installieren, wenn es als Flatpak ausgeführt wird. Verwenden Sie die von Ihrer Flatpak-Distribution bereitgestellte Methode zur Aktualisierung." - -- Language UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Sprache" @@ -4137,15 +4137,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] -- Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"] = "Pfad zu einer PEM-Datei mit einem oder mehreren Root-CA-Zertifikaten. Bei Flatpak-Bereitstellungen muss diese Datei an einem Ort abgelegt werden, der innerhalb der Sandbox lesbar ist." +-- This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2918560776"] = "Diese Installation sucht nicht selbst nach Updates. Wenden Sie sich an die Person oder Organisation, die AI Studio installiert hat, um Informationen zu Updates zu erhalten." + -- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Geben Sie pro Zeile ein Hostmuster ein. Exakte Hosts wie data.intra.example.org sowie Wildcards mit einem Label wie *.intra.example.org werden unterstützt. In AI Studio integrierte Endpunkte von Cloud-Anbietern wie OpenAI, Google usw. verwenden diese zusätzlichen Stammzertifikate nicht." -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?" --- AI Studio cannot install updates into this installation. Your IT department provides new versions. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio kann in dieser Installation keine Updates installieren. Ihre IT-Abteilung stellt neue Versionen bereit." - -- Development builds do not install updates. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Entwicklerversionen installieren keine Updates." @@ -4191,9 +4191,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] -- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "Wenn aktiviert, ist die Rechtschreibprüfung in allen Eingabefeldern aktiv. Je nach Betriebssystem werden Fehler möglicherweise nicht visuell hervorgehoben, aber ein Rechtsklick kann dennoch Korrekturvorschläge anzeigen." --- AI Studio cannot install updates into its current location. Install new versions yourself. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio kann Updates nicht am aktuellen Speicherort installieren. Installieren Sie neue Versionen selbst." - -- Select a transcription provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Wählen Sie einen Transkriptionsanbieter aus" @@ -4209,6 +4206,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] -- Select a root certificate bundle UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Wählen Sie ein Stammzertifikat-Bundle aus" +-- AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T476576809"] = "AI Studio kann in dieser Installation keine Updates installieren. Wenden Sie sich für neue Versionen an die Person oder Organisation, die die Installation vorgenommen hat." + -- Navigation bar behavior UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Verhalten der Navigationsleiste" @@ -8292,9 +8292,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstel -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin-Verzeichnis:" --- in a location AI Studio cannot update itself from -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "an einem Speicherort, von dem aus AI Studio keine Updates durchführen kann" - -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:" @@ -8304,9 +8301,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Kopiert den Pfad -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "Diese Bibliothek wird verwendet, um die Unterschiede zwischen zwei Texten anzuzeigen. Das ist zum Beispiel für den Grammatik- und Rechtschreibassistenten notwendig." --- owned by you, self-updates possible -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "von Ihnen verwaltet, Updates sind möglich" - -- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "Wenn Sie auf den jeweiligen Pfad klicken, wird dieser in die Zwischenablage kopiert. Sie können diese Dateien mit einem Texteditor öffnen, um ihren Inhalt anzusehen." @@ -8340,9 +8334,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgende -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage" --- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio kann sich von seinem aktuellen Speicherort aus nicht selbst aktualisieren. Durch die Installation eines Updates würde eine zweite Installation entstehen, anstatt diese zu ersetzen. Um eine neue Version zu erhalten, laden Sie die aktuelle Version herunter und installieren Sie sie über Ihre bestehende Installation." - -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "Das Projekt windows-rs ermöglicht den Zugriff auf Windows-APIs aus Rust. Wir verwenden mehrere Bibliotheken aus diesem Projekt: windows-registry wird verwendet, um die gewünschte Konfiguration in Windows-Unternehmensumgebungen auszulesen. Die Bibliotheken windows und windows-collections werden verwendet, um den nativen Windows-Dialog zum Teilen zu öffnen, zum Beispiel wenn Sie ein Plugin mit anderen teilen." @@ -8361,9 +8352,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2272122662"] = "Konfigurationsse -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "Wir müssen Zufallszahlen erzeugen, z. B. um die Kommunikation zwischen der Benutzeroberfläche und der Laufzeitumgebung abzusichern. Die rand-Bibliothek eignet sich dafür hervorragend." +-- Flatpak installation, updates are handled outside of AI Studio +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2294279524"] = "Flatpak-Installation – Updates werden außerhalb von AI Studio verwaltet." + -- Configuration plugin ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Konfigurations-Plugin-ID:" +-- AI Studio cannot update itself from its current installation location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2307318338"] = "AI Studio kann sich an seinem aktuellen Installationsort nicht selbst aktualisieren. Bei der Installation eines Updates würde eine zweite Installation erstellt, anstatt diese zu ersetzen. Um eine neue Version zu erhalten, laden Sie die neueste Version herunter und installieren Sie sie über Ihrer aktuellen Installation." + -- dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2325338322"] = "dirs bestimmt das plattformspezifische lokale Anwendungsdatenverzeichnis. AI Studio verwendet es, damit das Flatpak-Startprotokoll in dasselbe Verzeichnis geschrieben wird, das auch Tauri verwendet." @@ -8388,11 +8385,14 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "Installation vom -- Installed Pandoc version: Pandoc is not installed or not available. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installierte Pandoc-Version: Pandoc ist nicht installiert oder nicht verfügbar." +-- current installation location does not support automatic updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2401198677"] = "Der aktuelle Installationsort unterstützt keine automatischen Updates." + -- Configuration origin: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Ursprung der Konfiguration:" --- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "Diese Kopie von AI Studio wurde von einer anderen Person für Sie installiert, höchstwahrscheinlich von Ihrer IT-Abteilung. Daher kann sie sich nicht selbst aktualisieren. Wenden Sie sich an Ihre IT-Abteilung, wenn Sie Fragen zur Aktualisierung von AI Studio haben." +-- This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2444057400"] = "Diese Installation kann sich nicht selbst aktualisieren. Wenden Sie sich an die Person oder Organisation, die AI Studio installiert hat, um Informationen zu neuen Versionen zu erhalten." -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Slot der Konfiguration:" @@ -8406,6 +8406,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "Diese Bibliothek -- Used Open Source Projects UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Verwendete Open-Source-Projekte" +-- development build, no support for automatic updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2582380608"] = "Entwicklungsversion, keine Unterstützung für automatische Updates" + -- Build time UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build-Zeit" @@ -8451,6 +8454,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Erklärung" -- checking availability UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "Verfügbarkeit wird geprüft" +-- managed; updates are handled outside of AI Studio; contact whoever installed it and ask about updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T285730904"] = "verwaltet; Updates werden außerhalb von AI Studio durchgeführt; wenden Sie sich an die Person oder Organisation, die AI Studio installiert hat, und erkundigen Sie sich nach Updates." + -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software." @@ -8484,9 +8490,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vektordatenbank" -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Unternehmenskonfigurations-ID:" --- managed by someone else, no self-updates -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "von jemand anderem verwaltet, keine Selbstaktualisierung" - -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Verbinden Sie AI Studio mit den Daten ihrer Organisation über unsere Schnittstelle für externe Datenabfrage (ERI)." @@ -8544,9 +8547,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird m -- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "Du verwendest eine Entwicklerversion von AI Studio, die sich niemals selbst aktualisiert. Hole stattdessen die neuesten Änderungen und erstelle die App neu." --- development build, no self-updates -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "Entwicklerversion, keine automatischen Updates" - -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!" @@ -8565,6 +8565,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar -- active UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "aktiv" +-- standard; automatic updates supported +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3656709502"] = "Standard; automatische Updates werden unterstützt" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." 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 b7fa0dcf..9e5216a6 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 @@ -4068,6 +4068,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] -- Root certificate bundle path UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Root certificate bundle path" +-- AI Studio cannot install updates into its current installation location. Install new versions yourself. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T14786838"] = "AI Studio cannot install updates into its current installation location. Install new versions yourself." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar." @@ -4080,9 +4083,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] -- seconds UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds" --- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own." - -- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled." @@ -4104,6 +4104,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] -- Your organization has disabled update checks and installations. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Your organization has disabled update checks and installations." +-- AI Studio cannot install updates when running as a Flatpak. Update it using the Flatpak source or bundle from which you installed it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2009652585"] = "AI Studio cannot install updates when running as a Flatpak. Update it using the Flatpak source or bundle from which you installed it." + -- When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2013281167"] = "When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization." @@ -4122,9 +4125,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"] -- Update installation method UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T237706157"] = "Update installation method" --- AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T244540698"] = "AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution." - -- Language UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Language" @@ -4137,15 +4137,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] -- Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"] = "Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox." +-- This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2918560776"] = "This installation does not check for updates itself. Contact the person or organization that installed AI Studio for update information." + -- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates." -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" --- AI Studio cannot install updates into this installation. Your IT department provides new versions. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio cannot install updates into this installation. Your IT department provides new versions." - -- Development builds do not install updates. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Development builds do not install updates." @@ -4191,9 +4191,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] -- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." --- AI Studio cannot install updates into its current location. Install new versions yourself. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio cannot install updates into its current location. Install new versions yourself." - -- Select a transcription provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider" @@ -4209,6 +4206,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] -- Select a root certificate bundle UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle" +-- AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T476576809"] = "AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions." + -- Navigation bar behavior UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior" @@ -8292,9 +8292,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:" --- in a location AI Studio cannot update itself from -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "in a location AI Studio cannot update itself from" - -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" @@ -8304,9 +8301,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." --- owned by you, self-updates possible -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "owned by you, self-updates possible" - -- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents." @@ -8340,9 +8334,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" --- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation." - -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others." @@ -8361,9 +8352,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2272122662"] = "Configuration se -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." +-- Flatpak installation, updates are handled outside of AI Studio +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2294279524"] = "Flatpak installation, updates are handled outside of AI Studio" + -- Configuration plugin ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:" +-- AI Studio cannot update itself from its current installation location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2307318338"] = "AI Studio cannot update itself from its current installation location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation." + -- dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2325338322"] = "dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses." @@ -8388,11 +8385,14 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro -- Installed Pandoc version: Pandoc is not installed or not available. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available." +-- current installation location does not support automatic updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2401198677"] = "current installation location does not support automatic updates" + -- Configuration origin: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:" --- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio." +-- This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2444057400"] = "This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions." -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:" @@ -8406,6 +8406,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is -- Used Open Source Projects UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source Projects" +-- development build, no support for automatic updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2582380608"] = "development build, no support for automatic updates" + -- Build time UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" @@ -8451,6 +8454,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation" -- checking availability UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability" +-- managed; updates are handled outside of AI Studio; contact whoever installed it and ask about updates +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T285730904"] = "managed; updates are handled outside of AI Studio; contact whoever installed it and ask about updates" + -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software." @@ -8484,9 +8490,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:" --- managed by someone else, no self-updates -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "managed by someone else, no self-updates" - -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)." @@ -8544,9 +8547,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w -- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead." --- development build, no self-updates -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "development build, no self-updates" - -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" @@ -8565,6 +8565,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" -- active UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "active" +-- standard; automatic updates supported +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3656709502"] = "standard; automatic updates supported" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 85b1bc2a..051aeb9d 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -114,7 +114,7 @@ internal sealed class Program options.FormatterName = TerminalLogger.FORMATTER_NAME; }).AddConsoleFormatter(); - if(runtimeInfo.LinuxPackageType == "flatpak") + if(runtimeInfo.LinuxPackageType is LinuxPackageType.FLATPAK) { try { diff --git a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs index 0eaba5f4..e7711a51 100644 --- a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs +++ b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Reflection; using AIStudio.Tools.Metadata; +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using SharedTools; @@ -256,7 +257,7 @@ public sealed class PandocProcessBuilder ///
public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc"; - private static IEnumerable SystemPandocExecutableCandidates(string executableName, string linuxPackageType) + private static IEnumerable SystemPandocExecutableCandidates(string executableName, LinuxPackageType linuxPackageType) { var candidates = new List(); @@ -275,7 +276,7 @@ public sealed class PandocProcessBuilder break; case RID.LINUX_X64 or RID.LINUX_ARM64: - if (string.Equals(linuxPackageType, "flatpak", StringComparison.OrdinalIgnoreCase)) + if (linuxPackageType is LinuxPackageType.FLATPAK) AddCandidate(candidates, FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY, executableName); AddCandidate(candidates, "/usr/local/bin", executableName); diff --git a/app/MindWork AI Studio/Tools/Rust/LinuxPackageType.cs b/app/MindWork AI Studio/Tools/Rust/LinuxPackageType.cs new file mode 100644 index 00000000..9c819693 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/LinuxPackageType.cs @@ -0,0 +1,19 @@ +namespace AIStudio.Tools.Rust; + +/// +/// Identifies how the Linux build was packaged. +/// +public enum LinuxPackageType +{ + /// An unknown or future Linux package type reported by the runtime. + UNKNOWN, + + /// The app is not running on Linux. + NOT_APPLICABLE, + + /// An AppImage build. + APP_IMAGE, + + /// A Flatpak build. + FLATPAK, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs b/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs index c9e2f769..a8fc2b59 100644 --- a/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs +++ b/app/MindWork AI Studio/Tools/Rust/RuntimeInfoResponse.cs @@ -1,3 +1,3 @@ namespace AIStudio.Tools.Rust; -public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, string LinuxPackageType, InstallationKind InstallationKind); \ No newline at end of file +public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, LinuxPackageType LinuxPackageType, InstallationKind InstallationKind); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs b/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs index c41713f1..59c19afa 100644 --- a/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs +++ b/app/MindWork AI Studio/Tools/Services/UpdatePolicy.cs @@ -10,7 +10,7 @@ public sealed class UpdatePolicy(SettingsManager settingsManager, RuntimeInfoRes ? UpdatePolicyMode.ENTERPRISE_DISABLED : runtimeInfo.LinuxPackageType switch { - "flatpak" => UpdatePolicyMode.FLATPAK, + LinuxPackageType.FLATPAK => UpdatePolicyMode.FLATPAK, _ => runtimeInfo.InstallationKind switch { // diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 28e777da..8c369c5b 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -104,11 +104,27 @@ pub enum InstallationKind { Development, } +/// Identifies how the Linux build was packaged. Non-Linux builds report `NotApplicable`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +pub enum LinuxPackageType { + /// A Linux package type the runtime cannot identify. + Unknown, + + /// The app is not running on Linux. + NotApplicable, + + /// An AppImage build. The explicit name preserves the existing JSON contract. + AppImage, + + /// A Flatpak build. + Flatpak, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct RuntimeInfo { pub working_directory: String, pub executable_path: String, - pub linux_package_type: String, + pub linux_package_type: LinuxPackageType, pub installation_kind: InstallationKind, } @@ -120,25 +136,25 @@ pub async fn get_runtime_info(_token: APIToken) -> Json { executable_path: env::current_exe() .map(|path| path.to_string_lossy().into_owned()) .unwrap_or_default(), - linux_package_type: detect_linux_package_type().to_string(), + linux_package_type: detect_linux_package_type(), installation_kind: installation_kind(), }) } #[cfg(target_os = "linux")] -fn detect_linux_package_type() -> &'static str { +fn detect_linux_package_type() -> LinuxPackageType { if is_flatpak() { - "flatpak" + LinuxPackageType::Flatpak } else if is_appimage() { - "appimage" + LinuxPackageType::AppImage } else { - "unknown" + LinuxPackageType::Unknown } } #[cfg(not(target_os = "linux"))] -fn detect_linux_package_type() -> &'static str { - "not_applicable" +fn detect_linux_package_type() -> LinuxPackageType { + LinuxPackageType::NotApplicable } #[cfg(target_os = "linux")] @@ -1361,7 +1377,7 @@ mod tests { path_starts_with_ignoring_case, select_effective_enterprise_config_source, select_effective_enterprise_secret_source, update_target_installation_kind, EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues, - ExternalHttpCustomRootCertificatePolicy, InstallationKind, + ExternalHttpCustomRootCertificatePolicy, InstallationKind, LinuxPackageType, MANAGED_INSTALLATION_MARKER_FILE_NAME, }; use std::collections::HashMap; @@ -1373,6 +1389,18 @@ mod tests { const TEST_ID_B: &str = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; const TEST_ID_C: &str = "11111111-2222-3333-4444-555555555555"; + #[test] + fn linux_package_type_serialization_preserves_runtime_contract() { + for (package_type, expected) in [ + (LinuxPackageType::Unknown, "\"unknown\""), + (LinuxPackageType::NotApplicable, "\"not_applicable\""), + (LinuxPackageType::AppImage, "\"appimage\""), + (LinuxPackageType::Flatpak, "\"flatpak\""), + ] { + assert_eq!(serde_json::to_string(&package_type).unwrap(), expected); + } + } + fn enterprise_config( id: &str, server_url: &str, From 67a0a39683fe7971a5ea15613a2506974b29614a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 16 Aug 2026 12:35:15 +0200 Subject: [PATCH 14/77] Prepared release v26.8.1 (#926) --- app/MindWork AI Studio/Components/Changelog.Logs.cs | 2 +- app/MindWork AI Studio/packages.lock.json | 6 +++--- app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md | 2 +- app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md | 2 +- metadata.txt | 6 +++--- .../packaging/linux/org.mindworkai.AIStudio.metainfo.xml | 4 +++- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index 07951293..776d9e92 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,7 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ - new (252, "v26.8.1, build 252 (2026-08-15 12:21 UTC)", "v26.8.1.md"), + new (253, "v26.8.1, build 253 (2026-08-16 10:21 UTC)", "v26.8.1.md"), new (250, "v26.7.3, build 250 (2026-07-21 12:45 UTC)", "v26.7.3.md"), new (244, "v26.7.2, build 244 (2026-07-06 18:35 UTC)", "v26.7.2.md"), new (243, "v26.7.1, build 243 (2026-07-05 16:39 UTC)", "v26.7.1.md"), diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 659e11fa..3bd053e7 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -41,9 +41,9 @@ }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.18, )", - "resolved": "9.0.18", - "contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg==" + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "I9GkKrCVjzxGU1hsKSurOW6P/ABPPHARfc/MTnzIgDb8YjJ/votxKN2z7K+J3DvlQXGH0O7KqdtBseRj8j7eNQ==" }, "MudBlazor": { "type": "Direct", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index bdc5d90c..2e153d3d 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -1,4 +1,4 @@ -# v26.8.1, build 252 (2026-08-15 12:21 UTC) +# v26.8.1, build 253 (2026-08-16 10:21 UTC) - Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API. - Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8. - Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings. diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index f375356b..2fb6d13d 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1 +1 @@ -# v26.8.2, build 253 (2026-08-xx xx:xx UTC) +# v26.8.2, build 254 (2026-08-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index e54904fe..7e8a4716 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ 26.8.1 -2026-08-15 12:21:19 UTC -252 +2026-08-16 10:21:20 UTC +253 9.0.120 (commit 3f97250e38) 9.0.19 (commit 8381bdb01f) 1.97.1 (commit 8bab26f4f) 8.15.0 2.11.5 -1bf592994d3, release +da68051c61a, release osx-arm64 148.0.7763.0 0.7.2 \ No newline at end of file diff --git a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml index 5b9983cc..25509642 100644 --- a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml +++ b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml @@ -102,7 +102,7 @@ - +
  • Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API.
  • @@ -148,6 +148,8 @@
  • Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it.
  • Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change.
  • Fixed AI Studio installing a second copy of itself next to an existing installation. Its updater always installs into your personal user folder, so an installation elsewhere, such as one your IT department rolled out, was never replaced. AI Studio now recognizes those installations and leaves them alone. The information page tells you which case applies to yours. For IT departments: automatic updates can now stay enabled for everybody, and the Enterprise IT documentation explains the rest.
  • +
  • Fixed installing an assistant on Linux when AI Studio runs as a Flatpak. The Assistant Builder was able to create an assistant, but installing it always ended with an unexpected error.
  • +
  • Fixed the plugins page and the assistants page reloading again and again on Linux, which started as soon as an assistant was installed. Both pages kept flickering, and nothing on them could be used anymore until you left for another page.
  • Removed the legacy PowerPoint format (.ppt) from the selectable file types. AI Studio has no reader for it, so such a file could be attached but never read. The modern .pptx format is not affected.
  • Upgraded dependencies to their latest versions to improve security and stability.
From ae948a959f11aa5680f0540ad07a6371f61aeeeb Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 18 Aug 2026 11:43:47 +0200 Subject: [PATCH 15/77] Open log file locations from the information page (#928) --- .../Assistants/I18N/allTexts.lua | 25 ++++++++++++- .../Pages/Information.razor | 37 +++++++++++++++++-- .../Pages/Information.razor.cs | 35 +++++++++++++++++- .../plugin.lua | 25 ++++++++++++- .../plugin.lua | 25 ++++++++++++- app/MindWork AI Studio/packages.lock.json | 6 +-- .../wwwroot/changelog/v26.8.2.md | 1 + 7 files changed, 141 insertions(+), 13 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index c40a386a..60bc8a99 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -8299,8 +8299,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." --- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents." +-- Could not open the log file location. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1828231197"] = "Could not open the log file location." -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T185447014"] = "Pandoc Installation" @@ -8392,6 +8392,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration or -- This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2444057400"] = "This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions." +-- Could not open the log file location: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2533784927"] = "Could not open the log file location: {0}" + -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:" @@ -8440,6 +8443,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2787929913"] = "The image crate -- Show Details UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T27924674"] = "Show Details" +-- You can view and filter these files directly in AI Studio with the Log Viewer. Click a path to copy it to the clipboard, or use the folder button to open its location in your file manager. You can also open the files with a text editor. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T280847088"] = "You can view and filter these files directly in AI Studio with the Log Viewer. Click a path to copy it to the clipboard, or use the folder button to open its location in your file manager. You can also open the files with a text editor." + -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2829971158"] = "View our project roadmap and help shape AI Studio's future development." @@ -8545,6 +8551,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w -- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead." +-- Unknown error +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3461425987"] = "Unknown error" + -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" @@ -8566,6 +8575,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "active" -- standard; automatic updates supported UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3656709502"] = "standard; automatic updates supported" +-- The log file path is not available yet. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3686775689"] = "The log file path is not available yet." + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -8608,6 +8620,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Open in folder +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4048746540"] = "Open in folder" + -- Allowed hosts: none configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Allowed hosts: none configured" @@ -8623,6 +8638,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4113556626"] = "Ropus provides t -- Community & Code UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code" +-- Opened the log file location. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4162897654"] = "Opened the log file location." + -- Executable path UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path" @@ -8647,6 +8665,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4291960437"] = "Copies the statu -- Apache ECharts is embedded only in exported visual briefings that use supported data-driven charts. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T485678418"] = "Apache ECharts is embedded only in exported visual briefings that use supported data-driven charts." +-- Open Log Viewer +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T551035563"] = "Open Log Viewer" + -- This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow." diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index bd66c4ec..e1a2f8ef 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -276,21 +276,52 @@ - @T("By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents.") + @T("You can view and filter these files directly in AI Studio with the Log Viewer. Click a path to copy it to the clipboard, or use the folder button to open its location in your file manager. You can also open the files with a text editor.") + + + @T("Open Log Viewer") + @T("Startup log file") - + + + + @this.logPaths.LogStartupPath + + + + + + @T("Usage log file") - + + + + @this.logPaths.LogAppPath + + + + + + diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 0afd6652..7ac6c9bf 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -23,6 +23,9 @@ public partial class Information : MSGComponentBase [Inject] private RustService RustService { get; init; } = null!; + [Inject] + private ILogger Logger { get; init; } = null!; + [Inject] private IDialogService DialogService { get; init; } = null!; @@ -536,6 +539,36 @@ public partial class Information : MSGComponentBase { await this.RustService.CopyText2Clipboard(this.logPaths.LogAppPath); } + + private async Task OpenLogInFileManager(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + await this.MessageBus.SendWarning(new(Icons.Material.Filled.Folder, T("The log file path is not available yet."))); + return; + } + + OpenPathResponse response; + try + { + response = await this.RustService.TryOpenPathInRuntimeFileManager(path); + } + catch (Exception e) + { + this.Logger.LogWarning(e, "Could not open the log file location in the file manager."); + await this.MessageBus.SendError(new(Icons.Material.Filled.Folder, T("Could not open the log file location."))); + return; + } + + if (response.Success) + { + await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FolderOpen, T("Opened the log file location."))); + return; + } + + var issue = string.IsNullOrWhiteSpace(response.Issue) ? T("Unknown error") : response.Issue; + await this.MessageBus.SendError(new(Icons.Material.Filled.Folder, string.Format(T("Could not open the log file location: {0}"), issue))); + } private const string LICENSE = """ # Functional Source License, Version 1.1, MIT Future License @@ -689,4 +722,4 @@ public partial class Information : MSGComponentBase await this.DialogService.ShowAsync(T("How to update"), parameters, DialogOptions.FULLSCREEN); } -} \ 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 ab802437..e5ac2318 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 @@ -8301,8 +8301,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Kopiert den Pfad -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "Diese Bibliothek wird verwendet, um die Unterschiede zwischen zwei Texten anzuzeigen. Das ist zum Beispiel für den Grammatik- und Rechtschreibassistenten notwendig." --- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "Wenn Sie auf den jeweiligen Pfad klicken, wird dieser in die Zwischenablage kopiert. Sie können diese Dateien mit einem Texteditor öffnen, um ihren Inhalt anzusehen." +-- Could not open the log file location. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1828231197"] = "Der Speicherort der Protokolldatei konnte nicht geöffnet werden." -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T185447014"] = "Pandoc-Installation" @@ -8394,6 +8394,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Ursprung der Kon -- This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2444057400"] = "Diese Installation kann sich nicht selbst aktualisieren. Wenden Sie sich an die Person oder Organisation, die AI Studio installiert hat, um Informationen zu neuen Versionen zu erhalten." +-- Could not open the log file location: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2533784927"] = "Der Speicherort der Protokolldatei konnte nicht geöffnet werden: {0}" + -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Slot der Konfiguration:" @@ -8442,6 +8445,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2787929913"] = "Das Crate „ima -- Show Details UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T27924674"] = "Details anzeigen" +-- You can view and filter these files directly in AI Studio with the Log Viewer. Click a path to copy it to the clipboard, or use the folder button to open its location in your file manager. You can also open the files with a text editor. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T280847088"] = "Diese Dateien können Sie direkt in AI Studio mit dem Log-Viewer anzeigen und filtern. Klicken Sie auf einen Pfad, um ihn in die Zwischenablage zu kopieren, oder verwenden Sie den Button rechts, um den Speicherort in Ihrem Dateimanager zu öffnen. Sie können die Dateien auch mit einem Texteditor öffnen." + -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2829971158"] = "Sehen Sie sich unsere Roadmap an und helfen Sie mit, die zukünftige Entwicklung von AI Studio mitzugestalten." @@ -8547,6 +8553,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird m -- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "Du verwendest eine Entwicklerversion von AI Studio, die sich niemals selbst aktualisiert. Hole stattdessen die neuesten Änderungen und erstelle die App neu." +-- Unknown error +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3461425987"] = "Unbekannter Fehler" + -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!" @@ -8568,6 +8577,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "aktiv" -- standard; automatic updates supported UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3656709502"] = "Standard; automatische Updates werden unterstützt" +-- The log file path is not available yet. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3686775689"] = "Der Pfad zur Protokolldatei ist noch nicht verfügbar." + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." @@ -8610,6 +8622,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Pandoc-Installat -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen" +-- Open in folder +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4048746540"] = "Im Ordner öffnen" + -- Allowed hosts: none configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Zulässige Hosts: keine konfiguriert" @@ -8625,6 +8640,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4113556626"] = "Ropus stellt den -- Community & Code UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code" +-- Opened the log file location. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4162897654"] = "Speicherort der Protokolldatei wurde geöffnet." + -- Executable path UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Pfad der ausführbaren Datei" @@ -8649,6 +8667,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4291960437"] = "Kopiert den Stat -- Apache ECharts is embedded only in exported visual briefings that use supported data-driven charts. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T485678418"] = "Apache ECharts ist nur in exportierten visuellen Briefings eingebettet, die unterstützte datengesteuerte Diagramme verwenden." +-- Open Log Viewer +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T551035563"] = "Protokollanzeige öffnen" + -- This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "Dies ist eine Bibliothek, die die Grundlagen für asynchrones Programmieren in Rust bereitstellt. Sie enthält zentrale Trait-Definitionen wie Stream sowie Hilfsfunktionen wie join!, select! und verschiedene Methoden zur Kombination von Futures, die einen ausdrucksstarken asynchronen Kontrollfluss ermöglichen." 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 9e5216a6..606a18dc 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 @@ -8301,8 +8301,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." --- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents." +-- Could not open the log file location. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1828231197"] = "Could not open the log file location." -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T185447014"] = "Pandoc Installation" @@ -8394,6 +8394,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration or -- This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2444057400"] = "This installation cannot update itself. Contact the person or organization that installed AI Studio for information about new versions." +-- Could not open the log file location: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2533784927"] = "Could not open the log file location: {0}" + -- Configuration slot: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:" @@ -8442,6 +8445,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2787929913"] = "The image crate -- Show Details UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T27924674"] = "Show Details" +-- You can view and filter these files directly in AI Studio with the Log Viewer. Click a path to copy it to the clipboard, or use the folder button to open its location in your file manager. You can also open the files with a text editor. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T280847088"] = "You can view and filter these files directly in AI Studio with the Log Viewer. Click a path to copy it to the clipboard, or use the folder button to open its location in your file manager. You can also open the files with a text editor." + -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2829971158"] = "View our project roadmap and help shape AI Studio's future development." @@ -8547,6 +8553,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w -- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead." +-- Unknown error +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3461425987"] = "Unknown error" + -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" @@ -8568,6 +8577,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3648362799"] = "active" -- standard; automatic updates supported UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3656709502"] = "standard; automatic updates supported" +-- The log file path is not available yet. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3686775689"] = "The log file path is not available yet." + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -8610,6 +8622,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Open in folder +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4048746540"] = "Open in folder" + -- Allowed hosts: none configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Allowed hosts: none configured" @@ -8625,6 +8640,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4113556626"] = "Ropus provides t -- Community & Code UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code" +-- Opened the log file location. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4162897654"] = "Opened the log file location." + -- Executable path UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path" @@ -8649,6 +8667,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4291960437"] = "Copies the statu -- Apache ECharts is embedded only in exported visual briefings that use supported data-driven charts. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T485678418"] = "Apache ECharts is embedded only in exported visual briefings that use supported data-driven charts." +-- Open Log Viewer +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T551035563"] = "Open Log Viewer" + -- This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow." diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 3bd053e7..659e11fa 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -41,9 +41,9 @@ }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.19, )", - "resolved": "9.0.19", - "contentHash": "I9GkKrCVjzxGU1hsKSurOW6P/ABPPHARfc/MTnzIgDb8YjJ/votxKN2z7K+J3DvlQXGH0O7KqdtBseRj8j7eNQ==" + "requested": "[9.0.18, )", + "resolved": "9.0.18", + "contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg==" }, "MudBlazor": { "type": "Direct", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 2fb6d13d..7b82bdca 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1 +1,2 @@ # v26.8.2, build 254 (2026-08-xx xx:xx UTC) +- Improved how you access log files from the information page: you can now open their locations in the system file manager or jump directly to the Log Viewer Assistant. \ No newline at end of file From e9ce64cf0cabf6a7b50da5fa3eafd91ee349e9d0 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 18 Aug 2026 18:28:40 +0200 Subject: [PATCH 16/77] Fixed estimated document pages (#929) --- app/MindWork AI Studio/Tools/DocumentManager.cs | 12 +++++++----- runtime/src/file_data.rs | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/MindWork AI Studio/Tools/DocumentManager.cs b/app/MindWork AI Studio/Tools/DocumentManager.cs index 359ac826..018b4509 100644 --- a/app/MindWork AI Studio/Tools/DocumentManager.cs +++ b/app/MindWork AI Studio/Tools/DocumentManager.cs @@ -23,12 +23,14 @@ public sealed class DocumentManager this.currentPageContent = new StringBuilder(); // - // Sections, not pages: a Word or OpenDocument file carries no fixed page layout, so the - // runtime derives these boundaries from page breaks and heuristics. Calling them pages, - // as the PDF reader does with its real ones, would invite the AI to cite page numbers - // which do not exist in the document. + // A Word or OpenDocument file carries no fixed page layout, so the runtime derives these + // boundaries from page breaks and heuristics. We note the estimate as a comment rather + // than as a heading: a heading would sit on the same level as the document's own first + // level headings, leaving the AI unable to tell the structure of the document apart from + // our boundaries. The presentation reader marks its slides the same way. // - this.currentPageContent.AppendLine($"# Section {pageNumber}"); + this.currentPageContent.AppendLine($""); + this.currentPageContent.AppendLine(); this.currentPageContent.Append(content); return completedPage; } diff --git a/runtime/src/file_data.rs b/runtime/src/file_data.rs index 4ca4446f..34981d87 100644 --- a/runtime/src/file_data.rs +++ b/runtime/src/file_data.rs @@ -76,6 +76,7 @@ pub enum Metadata { page_number: Option, image: Option, }, + Image {}, Presentation { From d3163badd9b0c58df9553e947e7b762b66ebc6f0 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 19 Aug 2026 11:53:05 +0200 Subject: [PATCH 17/77] Prepared release v26.8.1 (#930) --- app/MindWork AI Studio/Components/Changelog.Logs.cs | 2 +- .../Settings/ProviderExtensions.OpenSource.cs | 11 +++++++++++ app/MindWork AI Studio/packages.lock.json | 6 +++--- app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md | 3 ++- app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md | 3 +-- metadata.txt | 6 +++--- .../linux/org.mindworkai.AIStudio.metainfo.xml | 3 ++- 7 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index 776d9e92..fc45317b 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,7 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ - new (253, "v26.8.1, build 253 (2026-08-16 10:21 UTC)", "v26.8.1.md"), + new (254, "v26.8.1, build 254 (2026-08-19 09:35 UTC)", "v26.8.1.md"), new (250, "v26.7.3, build 250 (2026-07-21 12:45 UTC)", "v26.7.3.md"), new (244, "v26.7.2, build 244 (2026-07-06 18:35 UTC)", "v26.7.2.md"), new (243, "v26.7.1, build 243 (2026-07-05 16:39 UTC)", "v26.7.1.md"), diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index 2f8aede9..b39b9fc7 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -121,6 +121,17 @@ public static partial class ProviderExtensions Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; + + // Check for the multimodal Qwen 3.8 27B checkpoint: + if(modelName.IndexOf("qwen3.8-27b") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT, + Capability.TEXT_OUTPUT, + + Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; // Check for Qwen 3.5: if(modelName.IndexOf("qwen3.5") is not -1) diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 659e11fa..3bd053e7 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -41,9 +41,9 @@ }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.18, )", - "resolved": "9.0.18", - "contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg==" + "requested": "[9.0.19, )", + "resolved": "9.0.19", + "contentHash": "I9GkKrCVjzxGU1hsKSurOW6P/ABPPHARfc/MTnzIgDb8YjJ/votxKN2z7K+J3DvlQXGH0O7KqdtBseRj8j7eNQ==" }, "MudBlazor": { "type": "Direct", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 2e153d3d..342f8ef7 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -1,4 +1,4 @@ -# v26.8.1, build 253 (2026-08-16 10:21 UTC) +# v26.8.1, build 254 (2026-08-19 09:35 UTC) - Added Hetzner's experimental inference API as an LLM provider. It runs open-source models in the EU and supports text and image chats through its OpenAI-compatible API. - Added support for the new open source models DeepSeek V4 Flash and Pro, GLM 5.2, Kimi K2.7 Code and K3, as well as Qwen 3.6 and 3.8. - Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings. @@ -16,6 +16,7 @@ - Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does. - Improved how Word documents (`.docx`) and OpenDocument files (`.odt`) are read. AI Studio now reads them itself instead of handing them to Pandoc, so these documents no longer need a Pandoc installation. It reads them section by section, which keeps even large documents responsive, and it now picks up more of the document: the title, the author, headers and footers, footnotes, endnotes, and comments. This was contributed by Nils Kruthoff (`nilskruthoff`), who also wrote the library behind it. Thank you, Nils, for this great contribution. - Improved the order of your models. Every list of models is now sorted by provider and name, so all models of one provider stay together. Until now, models appeared in the order they were created, which meant that a model added later always showed up at the end of the list. This was especially confusing when your organization rolled out new models. In the settings, the tables for LLM providers, embeddings, and transcription are now grouped by provider and start collapsed, so even a long list stays easy to survey. +- Improved how you access log files from the information page: you can now open their locations in the system file manager or jump directly to the Log Viewer Assistant. - Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants. - Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example, an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file. - Fixed files that are open in another program being reported as an unrecognized file type. AI Studio now tells you that the file is currently open elsewhere and asks you to close it. This also works for files on shared network drives, where a colleague might have the file open. diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 7b82bdca..ed69559c 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1,2 +1 @@ -# v26.8.2, build 254 (2026-08-xx xx:xx UTC) -- Improved how you access log files from the information page: you can now open their locations in the system file manager or jump directly to the Log Viewer Assistant. \ No newline at end of file +# v26.8.2, build 255 (2026-08-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index 7e8a4716..421260b1 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ 26.8.1 -2026-08-16 10:21:20 UTC -253 +2026-08-19 09:35:26 UTC +254 9.0.120 (commit 3f97250e38) 9.0.19 (commit 8381bdb01f) 1.97.1 (commit 8bab26f4f) 8.15.0 2.11.5 -da68051c61a, release +1aed0f8e19b, release osx-arm64 148.0.7763.0 0.7.2 \ No newline at end of file diff --git a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml index 25509642..ff0dc201 100644 --- a/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml +++ b/runtime/packaging/linux/org.mindworkai.AIStudio.metainfo.xml @@ -102,7 +102,7 @@ - +