From 2ff29b0d4d22e6563cb1d20c2688f563154c0f5c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Jul 2026 13:07:11 +0200 Subject: [PATCH 1/8] Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text (#842) --- .../Assistants/I18N/allTexts.lua | 22 ++-- .../Assistants/MyTasks/AssistantMyTasks.razor | 7 ++ .../MyTasks/AssistantMyTasks.razor.cs | 100 ++++++++++++++++-- .../plugin.lua | 22 ++-- .../plugin.lua | 22 ++-- .../wwwroot/changelog/v26.7.3.md | 1 + 6 files changed, 140 insertions(+), 34 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 5695fea0..3358d2b1 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1630,23 +1630,29 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597 -- Ask your questions UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions" --- Analyze the following text and extract my tasks: -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1349891364"] = "Analyze the following text and extract my tasks:" +-- You can enter text, attach one or more documents, or use both. At least one input is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required." --- Please provide some text as input. For example, an email. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1962809521"] = "Please provide some text as input. For example, an email." +-- Please provide some text or at least one valid document as input. For example, an email. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1635845349"] = "Please provide some text or at least one valid document as input. For example, an email." --- Analyze text -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2268303626"] = "Analyze text" +-- You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1918551346"] = "You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be." -- Target language UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T237828418"] = "Target language" +-- Analyze the following text and/or attached documents and extract my tasks: +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2535924263"] = "Analyze the following text and/or attached documents and extract my tasks:" + -- My Tasks UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3011450657"] = "My Tasks" --- You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] = "You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be." +-- Analyze content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3334965934"] = "Analyze content" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3666048746"] = "Attach documents" -- Custom target language UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Custom target language" diff --git a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor index 18b2d5c2..4a738ef7 100644 --- a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor +++ b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor @@ -3,5 +3,12 @@ +@T("Attach documents") + + @T("You can enter text, attach one or more documents, or use both. At least one input is required.") + +
+ +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs index 18b25880..f66a7bb4 100644 --- a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs +++ b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs @@ -1,3 +1,4 @@ +using AIStudio.Chat; using AIStudio.Dialogs.Settings; using AIStudio.Settings; using AIStudio.Tools.AssistantSessions; @@ -10,34 +11,84 @@ public partial class AssistantMyTasks : AssistantBaseCore protected override string Title => T("My Tasks"); - protected override string Description => T("You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be."); + protected override string Description => T("You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be."); protected override string SystemPrompt => $""" You are a friendly and professional business expert. You receive business emails, protocols, - reports, etc. as input. Additionally, you know the user's role in the organization. The user - wonders if any tasks arise for them in their role based on the text. You now try to give hints - and advice on whether and what the user should do. When you believe there are no tasks for the - user, you tell them this. You consider typical business etiquette in your advice. + reports, etc. as text input and/or attached documents. Additionally, you know the user's role + in the organization. The user wonders if any tasks arise for them in their role based on the + provided content. You now try to give hints and advice on whether and what the user should do. + When you believe there are no tasks for the user, you tell them this. You consider typical + business etiquette in your advice. You write your advice in the following language: {this.SystemPromptLanguage()}. """; protected override IReadOnlyList FooterButtons => []; - protected override string SubmitText => T("Analyze text"); + protected override string SubmitText => T("Analyze content"); protected override Func SubmitAction => this.AnalyzeText; protected override bool ShowProfileSelection => false; - protected override string SendToChatVisibleUserPromptPrefix => T("Analyze the following text and extract my tasks:"); + protected override string SendToChatVisibleUserPromptPrefix => T("Analyze the following text and/or attached documents and extract my tasks:"); protected override string SendToChatVisibleUserPromptContent => this.inputText; + protected override ChatThread ConvertToChatThread + { + get + { + var originalChatThread = this.ChatThread ?? new ChatThread(); + if (string.IsNullOrWhiteSpace(this.SendToChatVisibleUserPromptText)) + { + return originalChatThread with + { + SystemPrompt = SystemPrompts.DEFAULT, + }; + } + + var earliestBlock = originalChatThread.Blocks.MinBy(x => x.Time); + var visiblePromptTime = earliestBlock is null + ? DateTimeOffset.Now + : earliestBlock.Time == DateTimeOffset.MinValue + ? earliestBlock.Time + : earliestBlock.Time.AddTicks(-1); + + var transferredBlocks = originalChatThread.Blocks + .Select(block => block.Role is ChatRole.USER + ? this.CloneHiddenUserBlockWithoutAttachments(block) + : block.DeepClone()) + .ToList(); + + transferredBlocks.Insert(0, new ContentBlock + { + Time = visiblePromptTime, + ContentType = ContentType.TEXT, + HideFromUser = false, + Role = ChatRole.USER, + Content = new ContentText + { + Text = this.SendToChatVisibleUserPromptText, + FileAttachments = this.loadedDocumentPaths.ToList(), + }, + }); + + return originalChatThread with + { + ChatId = Guid.NewGuid(), + SystemPrompt = SystemPrompts.DEFAULT, + Blocks = transferredBlocks, + }; + } + } + protected override void ResetForm() { this.inputText = string.Empty; + this.loadedDocumentPaths.Clear(); if (!this.MightPreselectValues()) { this.selectedTargetLanguage = CommonLanguages.AS_IS; @@ -58,9 +109,11 @@ public partial class AssistantMyTasks : AssistantBaseCore } private string inputText = string.Empty; + private HashSet loadedDocumentPaths = []; private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS; private string customTargetLanguage = string.Empty; private static readonly AssistantSessionStateKey INPUT_TEXT_STATE_KEY = new(nameof(inputText)); + private static readonly AssistantSessionStateKey> LOADED_DOCUMENT_PATHS_STATE_KEY = new(nameof(loadedDocumentPaths)); private static readonly AssistantSessionStateKey SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage)); private static readonly AssistantSessionStateKey CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage)); @@ -68,6 +121,7 @@ public partial class AssistantMyTasks : AssistantBaseCore protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) { state.Set(INPUT_TEXT_STATE_KEY, this.inputText); + state.SetHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths); state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage); state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage); } @@ -76,6 +130,7 @@ public partial class AssistantMyTasks : AssistantBaseCore protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) { state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value); + state.RestoreHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths); state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value); state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value); } @@ -95,12 +150,20 @@ public partial class AssistantMyTasks : AssistantBaseCore private string? ValidatingText(string text) { - if(string.IsNullOrWhiteSpace(text)) - return T("Please provide some text as input. For example, an email."); + if(string.IsNullOrWhiteSpace(text) && !this.HasValidInputDocuments()) + return T("Please provide some text or at least one valid document as input. For example, an email."); return null; } + private bool HasValidInputDocuments() => this.loadedDocumentPaths.Any(n => n is { Exists: true, IsValid: true }); + + private async Task OnDocumentsChanged(HashSet _) + { + if(this.Form is not null) + await this.Form.Validate(); + } + private string? ValidateProfile(Profile profile) { if(profile == Profile.NO_PROFILE) @@ -127,6 +190,23 @@ public partial class AssistantMyTasks : AssistantBaseCore return this.selectedTargetLanguage.Name(); } + + private ContentBlock CloneHiddenUserBlockWithoutAttachments(ContentBlock block) + { + var clone = block.DeepClone(changeHideState: true); + if (clone.Content is ContentText text) + text.FileAttachments = []; + + return clone; + } + + private string BuildUserRequest() + { + if(!string.IsNullOrWhiteSpace(this.inputText)) + return this.inputText; + + return "Analyze the attached document(s) and extract my tasks."; + } private async Task AnalyzeText() { @@ -135,7 +215,7 @@ public partial class AssistantMyTasks : AssistantBaseCore return; this.CreateChatThread(); - var time = this.AddUserRequest(this.inputText); + var time = this.AddUserRequest(this.BuildUserRequest(), false, this.loadedDocumentPaths.ToList()); await this.AddAIResponseAsync(time); } 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 acb8b91b..5af93419 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 @@ -1632,23 +1632,29 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597 -- Ask your questions UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Stellen Sie ihre Fragen" --- Analyze the following text and extract my tasks: -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1349891364"] = "Analysiere den folgenden Text und extrahiere meine Aufgaben:" +-- You can enter text, attach one or more documents, or use both. At least one input is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "Sie können Text eingeben, ein oder mehrere Dokumente anhängen oder beides verwenden. Mindestens eine Eingabe ist erforderlich." --- Please provide some text as input. For example, an email. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1962809521"] = "Bitte geben Sie einen Text ein. Zum Beispiel eine E-Mail." +-- Please provide some text or at least one valid document as input. For example, an email. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1635845349"] = "Bitte geben Sie einen Text oder mindestens ein gültiges Dokument als Eingabe an. Zum Beispiel eine E-Mail." --- Analyze text -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2268303626"] = "Text analysieren" +-- You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1918551346"] = "Sie haben eine kryptische E-Mail oder ein Dokument erhalten, das an viele Empfänger gesendet wurde, und fragen sich nun, ob Sie etwas tun müssen? Kopieren Sie den Text in das Eingabefeld, fügen Sie ein oder mehrere Dokumente an oder nutzen Sie beides. Außerdem müssen Sie ein persönliches Profil auswählen. In diesem Profil sollten Sie Ihre Rolle in der Organisation beschreiben. Die KI wird dann versuchen, Ihnen Hinweise darauf zu geben, welche Aufgaben Sie möglicherweise haben." -- Target language UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T237828418"] = "Zielsprache" +-- Analyze the following text and/or attached documents and extract my tasks: +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2535924263"] = "Analysiere den folgenden Text und/oder die angehängten Dokumente und extrahiere meine Aufgaben:" + -- My Tasks UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3011450657"] = "Meine Aufgaben" --- You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] = "Sie haben eine rätselhafte E-Mail erhalten, die an viele Empfänger verschickt wurde, und fragen sich nun, ob Sie etwas unternehmen müssen? Kopieren Sie die E-Mail in das Eingabefeld. Außerdem müssen Sie ein persönliches Profil auswählen. In diesem Profil sollten Sie ihre Rolle in der Organisation beschreiben. Die KI wird Ihnen dann Hinweise geben, welche Aufgaben für Sie daraus entstehen könnten." +-- Analyze content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3334965934"] = "Inhalt analysieren" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3666048746"] = "Dokumente anhängen" -- Custom target language UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Benutzerdefinierte Zielsprache" 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 77e7094c..39d4bec7 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 @@ -1632,23 +1632,29 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597 -- Ask your questions UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions" --- Analyze the following text and extract my tasks: -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1349891364"] = "Analyze the following text and extract my tasks:" +-- You can enter text, attach one or more documents, or use both. At least one input is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required." --- Please provide some text as input. For example, an email. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1962809521"] = "Please provide some text as input. For example, an email." +-- Please provide some text or at least one valid document as input. For example, an email. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1635845349"] = "Please provide some text or at least one valid document as input. For example, an email." --- Analyze text -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2268303626"] = "Analyze text" +-- You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1918551346"] = "You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be." -- Target language UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T237828418"] = "Target language" +-- Analyze the following text and/or attached documents and extract my tasks: +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2535924263"] = "Analyze the following text and/or attached documents and extract my tasks:" + -- My Tasks UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3011450657"] = "My Tasks" --- You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] = "You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be." +-- Analyze content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3334965934"] = "Analyze content" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3666048746"] = "Attach documents" -- Custom target language UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Custom target language" diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 032a5b1c..29fc7499 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -1 +1,2 @@ # v26.7.3, build 245 (2026-07-xx xx:xx UTC) +- 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. \ No newline at end of file From 0af0330482332f57979e47bdc4a37db65329b4bc Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Jul 2026 14:09:28 +0200 Subject: [PATCH 2/8] Fixed enterprise config ZIP extraction on Linux (#843) --- .../PluginSystem/PluginFactory.Download.cs | 68 ++++++++++++++++++- .../wwwroot/changelog/v26.7.3.md | 3 +- ...26-07-enterprise-config-zip-backslashes.md | 28 ++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs index d1e5507b..89dacd79 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs @@ -65,7 +65,7 @@ public static partial class PluginFactory await response.Content.CopyToAsync(tempFileStream, cancellationToken); } - ZipFile.ExtractToDirectory(tempDownloadFile, stagedDirectory); + ExtractConfigPluginArchive(tempDownloadFile, stagedDirectory); var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString()); if (Directory.Exists(configDirectory)) @@ -129,4 +129,70 @@ public static partial class PluginFactory return wasSuccessful; } + + // Compatibility shim for Windows-created ZIPs with backslashes in entry names (dotnet/runtime#27620). + // See documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md. + private static void ExtractConfigPluginArchive(string sourceArchiveFileName, string destinationDirectory) + { + using var archive = ZipFile.OpenRead(sourceArchiveFileName); + Directory.CreateDirectory(destinationDirectory); + + var destinationDirectoryFullPath = Path.GetFullPath(destinationDirectory); + if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar)) + destinationDirectoryFullPath += Path.DirectorySeparatorChar; + + foreach (var entry in archive.Entries) + { + var normalizedEntryName = NormalizeConfigPluginZipEntryName(entry.FullName); + var destinationPath = GetConfigPluginZipEntryDestinationPath(destinationDirectoryFullPath, normalizedEntryName); + + if (normalizedEntryName.EndsWith('/')) + { + if (entry.Length != 0) + throw new InvalidDataException($"The enterprise configuration plugin archive contains a directory entry with data: '{entry.FullName}'."); + + Directory.CreateDirectory(destinationPath); + continue; + } + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath); + } + + if (!Directory.EnumerateFiles(destinationDirectory, "plugin.lua", SearchOption.AllDirectories).Any()) + throw new InvalidDataException("The enterprise configuration plugin archive does not contain a plugin.lua file."); + } + + private static string NormalizeConfigPluginZipEntryName(string entryName) + { + var normalizedEntryName = entryName.Replace('\\', '/'); + if (string.IsNullOrWhiteSpace(normalizedEntryName)) + throw new InvalidDataException("The enterprise configuration plugin archive contains an empty entry name."); + + if (normalizedEntryName.Contains('\0')) + throw new InvalidDataException($"The enterprise configuration plugin archive contains an invalid entry name: '{entryName}'."); + + if (normalizedEntryName.StartsWith('/')) + throw new InvalidDataException($"The enterprise configuration plugin archive contains a rooted entry name: '{entryName}'."); + + if (normalizedEntryName is [_, ':', ..]) + throw new InvalidDataException($"The enterprise configuration plugin archive contains a drive-qualified entry name: '{entryName}'."); + + var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (pathSegments.Length == 0 || pathSegments.Any(segment => segment is "." or "..")) + throw new InvalidDataException($"The enterprise configuration plugin archive contains an unsafe entry name: '{entryName}'."); + + return normalizedEntryName; + } + + private static string GetConfigPluginZipEntryDestinationPath(string destinationDirectoryFullPath, string normalizedEntryName) + { + var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries); + var relativePath = Path.Combine(pathSegments); + var destinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, relativePath)); + if (!destinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.Ordinal)) + throw new InvalidDataException($"The enterprise configuration plugin archive contains an entry outside the destination directory: '{normalizedEntryName}'."); + + return destinationPath; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 29fc7499..3b08da60 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -1,2 +1,3 @@ # v26.7.3, build 245 (2026-07-xx xx:xx UTC) -- 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. \ No newline at end of file +- 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. +- Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder. \ No newline at end of file diff --git a/documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md b/documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md new file mode 100644 index 00000000..2cfc77f9 --- /dev/null +++ b/documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md @@ -0,0 +1,28 @@ +# Enterprise Configuration ZIP Backslashes + +- Status: Active +- Introduced: 2026-07-09 +- Remove after: when Microsoft fixes dotnet/runtime#27620 and dotnet/runtime#41914 +- Code references: + - `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs` + +## User Impact + +Some enterprise administrators create configuration plugin ZIP files on Windows. Depending on the packaging tool, entries inside the ZIP may use Windows-style backslashes, for example `O\plugin.lua`. + +Without this shim, Unix systems extract those entries as files whose names contain literal backslash characters. The plugin loader then cannot find `plugin.lua`, so the enterprise configuration plugin is not activated. + +## Compatibility Behavior + +AI Studio manually extracts downloaded enterprise configuration plugin ZIP files. During extraction, entry names are normalized so both `/` and `\` are treated as archive path separators. + +The extraction still preserves the archive structure and validates each entry before writing it to disk. Rooted paths, drive-qualified paths, and parent-directory traversal paths are rejected. + +This works around the behavior described in dotnet/runtime#27620. A related upstream context for ZIP entry creation is dotnet/runtime#41914, where the ZIP specification requirement for forward slashes is discussed. + +## Removal Checklist + +- Confirm supported .NET runtimes and administrator packaging guidance no longer require accepting backslashes in enterprise ZIP entry names. +- Replace the manual enterprise configuration plugin ZIP extraction with `ZipFile.ExtractToDirectory(...)`. +- Remove `ExtractConfigPluginArchive(...)`, `NormalizeConfigPluginZipEntryName(...)`, and `GetConfigPluginZipEntryDestinationPath(...)`. +- Update this document's status to `Removed`. From 09fa5bc15b6d5a8964a55cc301af9c382ab5e04e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Jul 2026 20:11:24 +0200 Subject: [PATCH 3/8] Added Flatpak workflow integration (#845) --- .github/workflows/build-and-release.yml | 226 +++++++++++++++++++++++- 1 file changed, 225 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index c39b90e0..375d86a7 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -19,6 +19,8 @@ concurrency: env: RETENTION_INTERMEDIATE_ASSETS: 1 RETENTION_RELEASE_ASSETS: 30 + FLATPAK_REPOSITORY: MindWorkAI/Flatpak + FLATPAK_WORKFLOW: flatpak.yml jobs: determine_run_mode: @@ -165,6 +167,8 @@ jobs: formatted_build_time: ${{ steps.format_metadata.outputs.formatted_build_time }} changelog: ${{ steps.read_changelog.outputs.changelog }} version: ${{ steps.format_metadata.outputs.version }} + source_commit: ${{ steps.format_metadata.outputs.source_commit }} + pdfium_chromium_revision: ${{ steps.format_metadata.outputs.pdfium_chromium_revision }} steps: - name: Checkout repository @@ -176,6 +180,9 @@ jobs: # Read the first two lines of the metadata file: version=$(sed -n '1p' metadata.txt) build_time=$(sed -n '2p' metadata.txt) + pdfium_full_version=$(sed -n '11p' metadata.txt) + pdfium_chromium_revision=$(echo "$pdfium_full_version" | cut -d'.' -f3) + source_commit=$(git rev-parse HEAD) # Format the version: formatted_version="v${version}" @@ -186,12 +193,16 @@ jobs: # Log the formatted metadata: echo "Formatted version: '${formatted_version}'" echo "Formatted build time: '${formatted_build_time}'" + echo "Source commit: '${source_commit}'" + echo "PDFium Chromium revision: '${pdfium_chromium_revision}'" # Set the outputs: echo "formatted_version=${formatted_version}" >> "$GITHUB_OUTPUT" echo "FORMATTED_VERSION=${formatted_version}" >> $GITHUB_ENV echo "formatted_build_time=${formatted_build_time}" >> "$GITHUB_OUTPUT" echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "source_commit=${source_commit}" >> "$GITHUB_OUTPUT" + echo "pdfium_chromium_revision=${pdfium_chromium_revision}" >> "$GITHUB_OUTPUT" - name: Check tag vs. metadata version if: startsWith(github.ref, 'refs/tags/v') @@ -224,6 +235,219 @@ jobs: echo "${changelog}" >> "$GITHUB_OUTPUT" echo "EOOOF" >> "$GITHUB_OUTPUT" + sync_flatpak_repo: + name: Sync Flatpak repo + runs-on: ubuntu-latest + needs: [determine_run_mode, read_metadata] + if: needs.determine_run_mode.outputs.is_release == 'true' + permissions: + contents: read + outputs: + flatpak_commit: ${{ steps.sync.outputs.flatpak_commit }} + + env: + GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} + AI_STUDIO_TAG: ${{ needs.read_metadata.outputs.formatted_version }} + AI_STUDIO_COMMIT: ${{ needs.read_metadata.outputs.source_commit }} + PDFIUM_CHROMIUM_REVISION: ${{ needs.read_metadata.outputs.pdfium_chromium_revision }} + + steps: + - name: Checkout Flatpak repository + uses: actions/checkout@v4 + with: + repository: ${{ env.FLATPAK_REPOSITORY }} + token: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} + ref: main + path: flatpak + fetch-depth: 0 + + - name: Install Flatpak sync tools + run: | + set -euo pipefail + + sudo curl -fsSL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.6/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + "$HOME/.local/bin/uv" --version + yq --version + + - name: Update Flatpak release sources + working-directory: flatpak + env: + PDFIUM_X64_ARCHIVE: ${{ runner.temp }}/pdfium-linux-x64.tgz + PDFIUM_ARM64_ARCHIVE: ${{ runner.temp }}/pdfium-linux-arm64.tgz + run: | + set -euo pipefail + + pdfium_base_url="https://github.com/bblanchon/pdfium-binaries/releases/download/chromium%2F${PDFIUM_CHROMIUM_REVISION}" + pdfium_x64_url="${pdfium_base_url}/pdfium-linux-x64.tgz" + pdfium_arm64_url="${pdfium_base_url}/pdfium-linux-arm64.tgz" + + curl -fsSL -o "$PDFIUM_X64_ARCHIVE" "$pdfium_x64_url" + curl -fsSL -o "$PDFIUM_ARM64_ARCHIVE" "$pdfium_arm64_url" + + export PDFIUM_X64_URL="$pdfium_x64_url" + export PDFIUM_ARM64_URL="$pdfium_arm64_url" + export PDFIUM_X64_SHA256 + export PDFIUM_ARM64_SHA256 + PDFIUM_X64_SHA256=$(sha256sum "$PDFIUM_X64_ARCHIVE" | awk '{print $1}') + PDFIUM_ARM64_SHA256=$(sha256sum "$PDFIUM_ARM64_ARCHIVE" | awk '{print $1}') + + yq -i ' + (.modules[] | select(.name == "mind-work-ai-studio").sources[0].tag) = strenv(AI_STUDIO_TAG) | + (.modules[] | select(.name == "mind-work-ai-studio").sources[0].commit) = strenv(AI_STUDIO_COMMIT) | + (.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").url) = strenv(PDFIUM_X64_URL) | + (.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").sha256) = strenv(PDFIUM_X64_SHA256) | + (.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").url) = strenv(PDFIUM_ARM64_URL) | + (.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").sha256) = strenv(PDFIUM_ARM64_SHA256) + ' org.MindWorkAI.AIStudio.yml + + ./update-dependencies + git diff --stat + + - name: Commit and merge Flatpak sync + id: sync + working-directory: flatpak + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + branch="sync/ai-studio-${AI_STUDIO_TAG}" + git checkout -B "$branch" + git add org.MindWorkAI.AIStudio.yml cargo-sources.json dotnet-sources.json tauri-cli-sources.json + + if git diff --cached --quiet; then + echo "Flatpak repository is already synced for ${AI_STUDIO_TAG}." + flatpak_commit=$(git rev-parse origin/main) + echo "flatpak_commit=${flatpak_commit}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git commit -m "Sync AI Studio ${AI_STUDIO_TAG}" + + remote_branch_sha=$(git ls-remote --heads origin "$branch" | awk '{print $1}') + if [ -n "$remote_branch_sha" ]; then + git push --force-with-lease="refs/heads/${branch}:${remote_branch_sha}" origin "HEAD:${branch}" + else + git push origin "HEAD:${branch}" + fi + + pr_number=$(gh pr list \ + --repo "$FLATPAK_REPOSITORY" \ + --head "$branch" \ + --state open \ + --json number \ + --jq '.[0].number // empty') + + if [ -z "$pr_number" ]; then + pr_url=$(gh pr create \ + --repo "$FLATPAK_REPOSITORY" \ + --base main \ + --head "$branch" \ + --title "Sync AI Studio ${AI_STUDIO_TAG}" \ + --body "Synchronizes the Flatpak manifest and generated dependency sources for MindWork AI Studio ${AI_STUDIO_TAG}.") + pr_number="${pr_url##*/}" + fi + + for attempt in {1..30}; do + pr_state=$(gh pr view "$pr_number" --repo "$FLATPAK_REPOSITORY" --json mergeable,mergeStateStatus) + mergeable=$(echo "$pr_state" | jq -r '.mergeable') + merge_state_status=$(echo "$pr_state" | jq -r '.mergeStateStatus') + + echo "PR #${pr_number}: mergeable=${mergeable}, mergeStateStatus=${merge_state_status}" + + if [ "$mergeable" != "UNKNOWN" ]; then + break + fi + + sleep 5 + done + + gh pr merge "$pr_number" --repo "$FLATPAK_REPOSITORY" --merge --delete-branch + + flatpak_commit=$(gh api "repos/${FLATPAK_REPOSITORY}/commits/main" --jq .sha) + echo "flatpak_commit=${flatpak_commit}" >> "$GITHUB_OUTPUT" + + collect_flatpak_artifacts: + name: Collect Flatpak artifacts + runs-on: ubuntu-latest + needs: [determine_run_mode, read_metadata, sync_flatpak_repo] + if: needs.determine_run_mode.outputs.is_release == 'true' + permissions: + contents: read + + env: + GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} + FLATPAK_COMMIT: ${{ needs.sync_flatpak_repo.outputs.flatpak_commit }} + + steps: + - name: Wait for Flatpak main build + id: flatpak_run + run: | + set -euo pipefail + + run_id="" + + for attempt in {1..180}; do + run_json=$(gh run list \ + --repo "$FLATPAK_REPOSITORY" \ + --workflow "$FLATPAK_WORKFLOW" \ + --branch main \ + --commit "$FLATPAK_COMMIT" \ + --event push \ + --limit 1 \ + --json databaseId,status,conclusion,url,headSha) + + run_id=$(echo "$run_json" | jq -r '.[0].databaseId // empty') + + if [ -n "$run_id" ]; then + status=$(echo "$run_json" | jq -r '.[0].status') + conclusion=$(echo "$run_json" | jq -r '.[0].conclusion') + url=$(echo "$run_json" | jq -r '.[0].url') + + echo "Flatpak run ${run_id}: status=${status}, conclusion=${conclusion}, url=${url}" + + if [ "$status" = "completed" ]; then + if [ "$conclusion" != "success" ]; then + echo "Flatpak workflow failed with conclusion '${conclusion}'." + exit 1 + fi + + echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" + exit 0 + fi + else + echo "Waiting for Flatpak workflow on commit ${FLATPAK_COMMIT}..." + fi + + sleep 20 + done + + echo "Timed out waiting for the Flatpak workflow on commit ${FLATPAK_COMMIT}." + exit 1 + + - name: Download Flatpak artifacts + env: + RUN_ID: ${{ steps.flatpak_run.outputs.run_id }} + run: | + set -euo pipefail + + mkdir -p flatpak-artifacts + gh run download "$RUN_ID" --repo "$FLATPAK_REPOSITORY" --dir flatpak-artifacts + find flatpak-artifacts -type f -name '*.flatpak' -print + + - name: Upload Flatpak artifacts + uses: actions/upload-artifact@v4 + with: + name: MindWork AI Studio Flatpak Release + path: flatpak-artifacts/**/*.flatpak + if-no-files-found: error + retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} + build_main: name: Build app (${{ matrix.dotnet_runtime }}) needs: [determine_run_mode, read_metadata] @@ -789,7 +1013,7 @@ jobs: create_release: name: Prepare & create release runs-on: ubuntu-latest - needs: [build_main, read_metadata] + needs: [build_main, collect_flatpak_artifacts, read_metadata] if: startsWith(github.ref, 'refs/tags/v') permissions: {} steps: From 156cce380b7b1c8b196a1bf1b743ad8282237777 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Jul 2026 20:58:27 +0200 Subject: [PATCH 4/8] Upgraded to Rust 1.97.0 & updated dependencies (#846) --- metadata.txt | 4 +- runtime/Cargo.lock | 121 ++++++++++++++++++++---------------------- runtime/Cargo.toml | 20 +++---- runtime/src/log.rs | 2 +- runtime/src/pdfium.rs | 22 +++----- 5 files changed, 77 insertions(+), 92 deletions(-) diff --git a/metadata.txt b/metadata.txt index 75f32406..ab95e838 100644 --- a/metadata.txt +++ b/metadata.txt @@ -3,9 +3,9 @@ 244 9.0.118 (commit c8cbca4ed1) 9.0.17 (commit f2c8152eed) -1.96.1 (commit 31fca3adb) +1.97.0 (commit 2d8144b78) 8.15.0 -2.11.2 +2.11.5 4a15ff26655, release osx-arm64 148.0.7763.0 diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index b7bf4049..5de9ef13 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -459,11 +459,12 @@ dependencies = [ [[package]] name = "atoi_simd" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad17c7c205c2c28b527b9845eeb91cf1b4d008b438f98ce0e628227a822758e" +checksum = "f3cdb3708a128e559a30fb830e8a77a5022ee6902806925c216658652b452a44" dependencies = [ "debug_unsafe", + "rustversion", ] [[package]] @@ -842,9 +843,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -880,9 +881,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -933,9 +934,9 @@ dependencies = [ [[package]] name = "calamine" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8822fe6253ca47aa5ad9a3be09f6fe7cd20c6a74e41b0aa42e8f4e3d523508df" +checksum = "6975084f43060e56343ffba7f9731fa52a7dcf2e1cd8e2459fd4c6bf4a1bff59" dependencies = [ "atoi_simd", "byteorder", @@ -943,9 +944,9 @@ dependencies = [ "encoding_rs", "fast-float2", "log", - "quick-xml 0.39.2", + "quick-xml 0.41.0", "serde", - "zip 7.4.0", + "zip 8.6.0", ] [[package]] @@ -1165,9 +1166,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "codepage" @@ -1227,7 +1228,7 @@ dependencies = [ "ph", "procfs", "quick_cache", - "rand 0.10.1", + "rand 0.10.2", "roaring", "schemars", "self_cell", @@ -2517,7 +2518,7 @@ dependencies = [ "i_overlay", "log", "num-traits", - "rand 0.10.1", + "rand 0.10.2", "rand_pcg", "robust", "rstar", @@ -2765,7 +2766,7 @@ dependencies = [ "log", "lz4_flex", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_cbor", "serde_json", @@ -3874,17 +3875,11 @@ dependencies = [ "serde", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -4025,7 +4020,7 @@ dependencies = [ "pdfium-render", "pptx-to-md", "qdrant-edge", - "rand 0.10.1", + "rand 0.10.2", "rand_chacha 0.10.0", "rcgen", "rustls", @@ -4034,7 +4029,7 @@ dependencies = [ "sha2 0.11.0", "strum_macros", "sys-locale", - "sysinfo 0.39.3", + "sysinfo 0.39.6", "tauri", "tauri-build", "tauri-plugin-dialog", @@ -4835,9 +4830,9 @@ dependencies = [ [[package]] name = "pdfium-render" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076dd8f3a6c7da9298ddffbcc0d5a109f89caf967fa4871c9a172d5b3498b35b" +checksum = "e06f0df3ca17554c1b8f31eb17bc77eedbbafa120d35b90b3096fa46b2fdc94c" dependencies = [ "bitflags 2.11.1", "bytemuck", @@ -5260,7 +5255,7 @@ dependencies = [ "log", "ordered-float 5.3.0", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "segment", "serde", "serde_json", @@ -5303,7 +5298,7 @@ dependencies = [ "ordered-float 5.3.0", "parking_lot", "permutation_iterator", - "rand 0.10.1", + "rand 0.10.2", "rayon", "serde", "serde_json", @@ -5321,9 +5316,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.2" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "encoding_rs", "memchr", @@ -5442,9 +5437,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -5513,7 +5508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" dependencies = [ "num-traits", - "rand 0.10.1", + "rand 0.10.2", ] [[package]] @@ -6038,7 +6033,7 @@ dependencies = [ "procfs", "qdrant-rust-stemmers", "quantization", - "rand 0.10.1", + "rand 0.10.2", "rayon", "roaring", "schemars", @@ -6355,7 +6350,7 @@ dependencies = [ "log", "ordered-float 5.3.0", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "rmp-serde", "schemars", "segment", @@ -6520,7 +6515,7 @@ dependencies = [ "memmap2", "ordered-float 5.3.0", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "schemars", "serde", "serde_json", @@ -6677,9 +6672,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.39.3" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d0d938c10fcda3e897e28aaddf4ab462375d411f4378cd63b1c945f69aba96" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", @@ -6779,9 +6774,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.2" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" dependencies = [ "anyhow", "bytes", @@ -6830,9 +6825,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -6851,9 +6846,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", @@ -6878,9 +6873,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -7071,9 +7066,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", @@ -7096,9 +7091,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.2" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", "http", @@ -7122,9 +7117,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", @@ -7609,9 +7604,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.23.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs", @@ -7933,7 +7928,7 @@ dependencies = [ "fs4", "log", "memmap2", - "rand 0.10.1", + "rand 0.10.2", "rand_distr", "rustix 1.1.4", "serde", @@ -8434,9 +8429,9 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-native-keyring-store" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5fd986f648459dd29aa252ed3a5ad11a60c0b1251bf81625fb03a86c69d274e" +checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8" dependencies = [ "byteorder", "keyring-core", @@ -9340,9 +9335,9 @@ dependencies = [ [[package]] name = "zip" -version = "7.4.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "crc32fast", "flate2", @@ -9366,15 +9361,13 @@ checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" [[package]] name = "zopfli" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 6c30e646..a13cb483 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -6,10 +6,10 @@ description = "MindWork AI Studio" authors = ["Thorsten Sommer"] [build-dependencies] -tauri-build = { version = "2.6.2", features = [] } +tauri-build = { version = "2.6.3", features = [] } [dependencies] -tauri = { version = "2.11.2", features = [] } +tauri = { version = "2.11.5", features = [] } tauri-plugin-window-state = { version = "2.4.1" } tauri-plugin-shell = "2.3.5" tauri-plugin-dialog = "2.7.1" @@ -24,12 +24,12 @@ tokio-stream = "0.1.18" futures = "0.3.32" async-stream = "0.3.6" flexi_logger = "0.31.9" -log = { version = "0.4.30", features = ["kv"] } +log = { version = "0.4.33", features = ["kv"] } once_cell = "1.21.4" axum = { version = "0.8.9", features = ["http2", "json", "query", "tokio"] } axum-server = { version = "0.8.0", features = ["tls-rustls"] } rustls = { version = "0.23.28", default-features = false, features = ["aws_lc_rs"] } -rand = "0.10.1" +rand = "0.10.2" rand_chacha = "0.10.0" base64 = "0.22.1" aes = "0.9.1" @@ -39,16 +39,16 @@ hmac = "0.13.0" sha2 = "0.11.0" rcgen = { version = "0.14.8", features = ["pem"] } file-format = "0.29.0" -calamine = "0.35.0" -pdfium-render = "0.9.1" +calamine = "0.36.0" +pdfium-render = "0.9.2" sys-locale = "0.3.2" whoami = "2.1.2" cfg-if = "1.0.4" pptx-to-md = "0.4.0" tempfile = "3.27.0" strum_macros = "0.28.0" -sysinfo = "0.39.3" -bytes = "1.11.1" +sysinfo = "0.39.6" +bytes = "1.12.1" qdrant-edge = "0.7.2" [patch.crates-io] @@ -62,7 +62,7 @@ permutation_iterator = { git = "https://github.com/SommerEngineering/permutation [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6.1" -windows-native-keyring-store = "1.0.0" +windows-native-keyring-store = "1.1.0" [target.'cfg(target_os = "macos")'.dependencies] apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] } @@ -72,7 +72,7 @@ dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rus [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" -tauri-plugin-updater = "2.10.0" +tauri-plugin-updater = "2.10.1" [features] custom-protocol = ["tauri/custom-protocol"] diff --git a/runtime/src/log.rs b/runtime/src/log.rs index 18f0921a..bf94fc33 100644 --- a/runtime/src/log.rs +++ b/runtime/src/log.rs @@ -223,7 +223,7 @@ fn file_logger_format( write_kv_pairs(w, record)?; // Write the log message: - write!(w, "{}", &record.args()) + write!(w, "{}", record.args()) } pub async fn get_log_paths(_token: APIToken) -> Json { diff --git a/runtime/src/pdfium.rs b/runtime/src/pdfium.rs index 7a128dcb..be4d9cf6 100644 --- a/runtime/src/pdfium.rs +++ b/runtime/src/pdfium.rs @@ -1,31 +1,23 @@ use std::error::Error; use std::sync::Mutex; -use once_cell::sync::Lazy; +use once_cell::sync::{Lazy, OnceCell}; use pdfium_render::prelude::Pdfium; use log::{error, info, warn}; pub static PDFIUM_LIB_PATH: Lazy>> = Lazy::new(|| Mutex::new(None)); -static PDFIUM: Lazy>> = Lazy::new(|| Mutex::new(None)); +static PDFIUM: OnceCell = OnceCell::new(); pub trait PdfiumInit { - fn ai_studio_init() -> Result>; + fn ai_studio_init() -> Result<&'static Pdfium, Box>; } impl PdfiumInit for Pdfium { /// Initializes the PDFium library for AI Studio. - fn ai_studio_init() -> Result> { - let mut pdfium = PDFIUM.lock().unwrap(); - if let Some(pdfium) = pdfium.as_ref() { - return Ok(pdfium.clone()); - } - - let loaded_pdfium = load_pdfium().map_err(|error| { + fn ai_studio_init() -> Result<&'static Pdfium, Box> { + PDFIUM.get_or_try_init(|| load_pdfium().map_err(|error| { Box::new(std::io::Error::other(error)) as Box - })?; - *pdfium = Some(loaded_pdfium.clone()); - - Ok(loaded_pdfium) + })) } } @@ -76,4 +68,4 @@ fn load_pdfium() -> Result { Err(error_message) } } -} \ No newline at end of file +} From 32355dbe74ae11eaca710bdcefba85b803e50e08 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Jul 2026 21:15:16 +0200 Subject: [PATCH 5/8] Updated changelog (#847) --- app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 3b08da60..39b67be4 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -1,3 +1,6 @@ # v26.7.3, build 245 (2026-07-xx xx:xx UTC) - 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. -- Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder. \ No newline at end of file +- Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder. +- Upgraded Rust to v1.97.0. +- Upgraded Tauri to v2.11.5. +- Upgraded common dependencies. \ No newline at end of file From 3f95bfb157fb892c0a74c5ed95ea1b6b04831a46 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Jul 2026 21:30:09 +0200 Subject: [PATCH 6/8] Revert pdfium-render to 0.9.1 (#848) --- runtime/Cargo.lock | 4 ++-- runtime/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 5de9ef13..fde1cf0e 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -4830,9 +4830,9 @@ dependencies = [ [[package]] name = "pdfium-render" -version = "0.9.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e06f0df3ca17554c1b8f31eb17bc77eedbbafa120d35b90b3096fa46b2fdc94c" +checksum = "076dd8f3a6c7da9298ddffbcc0d5a109f89caf967fa4871c9a172d5b3498b35b" dependencies = [ "bitflags 2.11.1", "bytemuck", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a13cb483..29554c35 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -40,7 +40,7 @@ sha2 = "0.11.0" rcgen = { version = "0.14.8", features = ["pem"] } file-format = "0.29.0" calamine = "0.36.0" -pdfium-render = "0.9.2" +pdfium-render = "0.9.1" sys-locale = "0.3.2" whoami = "2.1.2" cfg-if = "1.0.4" From 51462ea64778e2830a90a0a1912e0097d9b07ea5 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Jul 2026 09:01:41 +0200 Subject: [PATCH 7/8] Fixed Blazor reconnection after system sleep (#849) --- app/MindWork AI Studio/App.razor | 3 + app/MindWork AI Studio/Program.cs | 9 +- app/MindWork AI Studio/wwwroot/boot.js | 89 +++++++++++++++---- .../wwwroot/changelog/v26.7.3.md | 1 + 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/app/MindWork AI Studio/App.razor b/app/MindWork AI Studio/App.razor index 7df24793..e05a7749 100644 --- a/app/MindWork AI Studio/App.razor +++ b/app/MindWork AI Studio/App.razor @@ -21,6 +21,9 @@ + diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index b3d58859..b37b729b 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -154,12 +154,17 @@ internal sealed class Program // ReSharper restore AccessToDisposedClosure builder.Services.AddRazorComponents() - .AddInteractiveServerComponents() + .AddInteractiveServerComponents(options => + { + options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromDays(30); + options.DisconnectedCircuitMaxRetained = 2; + }) .AddHubOptions(options => { options.MaximumReceiveMessageSize = null; - options.ClientTimeoutInterval = TimeSpan.FromDays(14); + options.ClientTimeoutInterval = TimeSpan.FromSeconds(120); options.HandshakeTimeout = TimeSpan.FromSeconds(30); + options.KeepAliveInterval = TimeSpan.FromSeconds(30); }); builder.Services.AddSingleton(new HttpClient diff --git a/app/MindWork AI Studio/wwwroot/boot.js b/app/MindWork AI Studio/wwwroot/boot.js index d18dd7e9..4d605ee0 100644 --- a/app/MindWork AI Studio/wwwroot/boot.js +++ b/app/MindWork AI Studio/wwwroot/boot.js @@ -1,33 +1,73 @@ (() => { - const maximumRetryCount = 3; - const retryIntervalMilliseconds = 500; + const maximumRetryCount = 12; const reconnectModal = document.getElementById('reconnect-modal'); + const retryDelaysMilliseconds = [ + 0, + 1_000, + 2_000, + 5_000, + 10_000, + 15_000, + 30_000, + ]; + + let currentReconnectionProcess = null; + let isConnectionDown = false; + + const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); + + const getRetryDelayMilliseconds = attempt => retryDelaysMilliseconds[Math.min(attempt, retryDelaysMilliseconds.length - 1)]; + + const showReconnectModal = () => { + if (reconnectModal) + reconnectModal.style.display = 'flex'; + }; + + const hideReconnectModal = () => { + if (reconnectModal) + reconnectModal.style.display = 'none'; + }; + + const setReconnectModalText = text => { + if (reconnectModal) + reconnectModal.textContent = text; + }; const startReconnectionProcess = () => { - reconnectModal.style.display = 'block'; + showReconnectModal(); let isCanceled = false; + let forceAttempt = false; - (async () => { - for (let i = 0; i < maximumRetryCount; i++) { - reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`; + const waitForNextAttempt = async milliseconds => { + const startedAt = Date.now(); + while (!isCanceled && !forceAttempt && Date.now() - startedAt < milliseconds) + await delay(250); - await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds)); + forceAttempt = false; + }; - if (isCanceled) { + void (async () => { + for (let attempt = 0; attempt < maximumRetryCount && !isCanceled; attempt++) { + setReconnectModalText(`Reconnecting to AI Studio (${attempt + 1}/${maximumRetryCount})...`); + + const retryDelayMilliseconds = getRetryDelayMilliseconds(attempt); + if (retryDelayMilliseconds > 0) + await waitForNextAttempt(retryDelayMilliseconds); + + if (isCanceled) return; - } try { const result = await Blazor.reconnect(); - if (!result) { + if (result === false) { // The server was reached, but the connection was rejected; reload the page. location.reload(); return; } - // Successfully reconnected to the server. - return; + if (result === true) + return; } catch { // Didn't reach the server; try again. } @@ -40,25 +80,42 @@ return { cancel: () => { isCanceled = true; - reconnectModal.style.display = 'none'; + hideReconnectModal(); + }, + triggerImmediateAttempt: () => { + forceAttempt = true; }, }; }; - let currentReconnectionProcess = null; + const triggerReconnectAfterWake = () => { + if (isConnectionDown) + currentReconnectionProcess?.triggerImmediateAttempt(); + }; + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') + triggerReconnectAfterWake(); + }); + + globalThis.addEventListener('pageshow', triggerReconnectAfterWake); Blazor.start({ circuit: { reconnectionHandler: { - onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(), + onConnectionDown: () => { + isConnectionDown = true; + currentReconnectionProcess ??= startReconnectionProcess(); + }, onConnectionUp: () => { + isConnectionDown = false; currentReconnectionProcess?.cancel(); currentReconnectionProcess = null; } }, configureSignalR: function (builder) { - builder.withServerTimeout(1_200_000); + builder.withServerTimeout(120_000); builder.withKeepAliveInterval(30_000); }, } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 39b67be4..24e7b9e3 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -1,5 +1,6 @@ # v26.7.3, build 245 (2026-07-xx xx:xx UTC) - 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. +- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. - Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder. - Upgraded Rust to v1.97.0. - Upgraded Tauri to v2.11.5. From 5890b3734a47cbd72b2e0e6daae2d27068d9968b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Jul 2026 12:03:32 +0200 Subject: [PATCH 8/8] Improved Flatpak workflow with enhanced artifact validation (#850) --- .github/workflows/build-and-release.yml | 397 +++++++++++++++++++++--- 1 file changed, 348 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 375d86a7..3f366b4e 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -21,6 +21,11 @@ env: RETENTION_RELEASE_ASSETS: 30 FLATPAK_REPOSITORY: MindWorkAI/Flatpak FLATPAK_WORKFLOW: flatpak.yml + FLATPAK_YQ_VERSION: 4.44.6 + FLATPAK_YQ_SHA256: 0c2b24e645b57d8e7c0566d18643a6d4f5580feeea3878127354a46f2a1e4598 + FLATPAK_UV_VERSION: 0.11.28 + FLATPAK_UV_SHA256: e490a6464492183c5d4534a5527fb4440f7f2bb2f228162ad7e4afe076dc0224 + FLATPAK_FREEDESKTOP_VERSION: "25.08" jobs: determine_run_mode: @@ -246,7 +251,6 @@ jobs: flatpak_commit: ${{ steps.sync.outputs.flatpak_commit }} env: - GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} AI_STUDIO_TAG: ${{ needs.read_metadata.outputs.formatted_version }} AI_STUDIO_COMMIT: ${{ needs.read_metadata.outputs.source_commit }} PDFIUM_CHROMIUM_REVISION: ${{ needs.read_metadata.outputs.pdfium_chromium_revision }} @@ -260,18 +264,48 @@ jobs: ref: main path: flatpak fetch-depth: 0 + persist-credentials: false - - name: Install Flatpak sync tools + - name: Install Flatpak sync tools and SDK run: | set -euo pipefail - sudo curl -fsSL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.6/yq_linux_amd64 - sudo chmod +x /usr/local/bin/yq + sudo apt-get update + sudo apt-get install --no-install-recommends -y flatpak - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - "$HOME/.local/bin/uv" --version - yq --version + flatpak remote-add \ + --user \ + --if-not-exists \ + flathub \ + https://flathub.org/repo/flathub.flatpakrepo + flatpak install \ + --user \ + --noninteractive \ + -y \ + flathub \ + "org.freedesktop.Sdk//${FLATPAK_FREEDESKTOP_VERSION}" \ + "org.freedesktop.Sdk.Extension.dotnet9//${FLATPAK_FREEDESKTOP_VERSION}" + + tools_dir="$RUNNER_TEMP/flatpak-sync-tools" + mkdir -p "$tools_dir" + + curl -fsSL \ + -o "$tools_dir/yq" \ + "https://github.com/mikefarah/yq/releases/download/v${FLATPAK_YQ_VERSION}/yq_linux_amd64" + echo "${FLATPAK_YQ_SHA256} ${tools_dir}/yq" | sha256sum --check --strict + chmod +x "$tools_dir/yq" + + uv_archive="$RUNNER_TEMP/uv-x86_64-unknown-linux-gnu.tar.gz" + curl -fsSL \ + -o "$uv_archive" \ + "https://github.com/astral-sh/uv/releases/download/${FLATPAK_UV_VERSION}/uv-x86_64-unknown-linux-gnu.tar.gz" + echo "${FLATPAK_UV_SHA256} ${uv_archive}" | sha256sum --check --strict + tar -xzf "$uv_archive" -C "$tools_dir" + install -m 0755 "$tools_dir/uv-x86_64-unknown-linux-gnu/uv" "$tools_dir/uv" + + echo "$tools_dir" >> "$GITHUB_PATH" + "$tools_dir/uv" --version + "$tools_dir/yq" --version - name: Update Flatpak release sources working-directory: flatpak @@ -305,11 +339,27 @@ jobs: ' org.MindWorkAI.AIStudio.yml ./update-dependencies + + test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[0].tag' org.MindWorkAI.AIStudio.yml)" = "$AI_STUDIO_TAG" + test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[0].commit' org.MindWorkAI.AIStudio.yml)" = "$AI_STUDIO_COMMIT" + test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").url' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_X64_URL" + test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").sha256' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_X64_SHA256" + test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").url' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_ARM64_URL" + test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").sha256' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_ARM64_SHA256" + + for generated_source in cargo-sources.json dotnet-sources.json tauri-cli-sources.json; do + test -s "$generated_source" + jq -e 'type == "array" and length > 0' "$generated_source" > /dev/null + done + + git diff --check git diff --stat - name: Commit and merge Flatpak sync id: sync working-directory: flatpak + env: + GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} run: | set -euo pipefail @@ -322,12 +372,23 @@ jobs: if git diff --cached --quiet; then echo "Flatpak repository is already synced for ${AI_STUDIO_TAG}." - flatpak_commit=$(git rev-parse origin/main) + flatpak_commit=$(git rev-parse HEAD) + remote_main=$(git ls-remote origin refs/heads/main | awk '{print $1}') + if [ "$remote_main" != "$flatpak_commit" ]; then + echo "Flatpak main advanced from ${flatpak_commit} to ${remote_main} during synchronization." + exit 1 + fi + echo "flatpak_commit=${flatpak_commit}" >> "$GITHUB_OUTPUT" exit 0 fi git commit -m "Sync AI Studio ${AI_STUDIO_TAG}" + sync_commit=$(git rev-parse HEAD) + + basic_auth=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 -w 0) + git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" + trap 'git config --local --unset-all http.https://github.com/.extraheader || true' EXIT remote_branch_sha=$(git ls-remote --heads origin "$branch" | awk '{print $1}') if [ -n "$remote_branch_sha" ]; then @@ -353,6 +414,7 @@ jobs: pr_number="${pr_url##*/}" fi + mergeable="UNKNOWN" for attempt in {1..30}; do pr_state=$(gh pr view "$pr_number" --repo "$FLATPAK_REPOSITORY" --json mergeable,mergeStateStatus) mergeable=$(echo "$pr_state" | jq -r '.mergeable') @@ -360,17 +422,54 @@ jobs: echo "PR #${pr_number}: mergeable=${mergeable}, mergeStateStatus=${merge_state_status}" - if [ "$mergeable" != "UNKNOWN" ]; then + if [ "$mergeable" = "MERGEABLE" ]; then break fi + if [ "$mergeable" = "CONFLICTING" ]; then + echo "Flatpak sync PR #${pr_number} has merge conflicts." + exit 1 + fi + sleep 5 done - gh pr merge "$pr_number" --repo "$FLATPAK_REPOSITORY" --merge --delete-branch + if [ "$mergeable" != "MERGEABLE" ]; then + echo "Timed out waiting for Flatpak sync PR #${pr_number} to become mergeable." + exit 1 + fi - flatpak_commit=$(gh api "repos/${FLATPAK_REPOSITORY}/commits/main" --jq .sha) - echo "flatpak_commit=${flatpak_commit}" >> "$GITHUB_OUTPUT" + gh pr merge "$pr_number" \ + --repo "$FLATPAK_REPOSITORY" \ + --merge \ + --delete-branch \ + --match-head-commit "$sync_commit" + + for attempt in {1..120}; do + pr_state=$(gh pr view "$pr_number" \ + --repo "$FLATPAK_REPOSITORY" \ + --json state,mergedAt,mergeCommit) + state=$(echo "$pr_state" | jq -r '.state') + merged_at=$(echo "$pr_state" | jq -r '.mergedAt // empty') + merge_commit=$(echo "$pr_state" | jq -r '.mergeCommit.oid // empty') + + echo "PR #${pr_number}: state=${state}, mergedAt=${merged_at:-pending}, mergeCommit=${merge_commit:-pending}" + + if [ -n "$merged_at" ] && [ -n "$merge_commit" ]; then + echo "flatpak_commit=${merge_commit}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$state" = "CLOSED" ]; then + echo "Flatpak sync PR #${pr_number} was closed without being merged." + exit 1 + fi + + sleep 5 + done + + echo "Timed out waiting for Flatpak sync PR #${pr_number} to be merged." + exit 1 collect_flatpak_artifacts: name: Collect Flatpak artifacts @@ -381,63 +480,233 @@ jobs: contents: read env: - GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} FLATPAK_COMMIT: ${{ needs.sync_flatpak_repo.outputs.flatpak_commit }} steps: - name: Wait for Flatpak main build id: flatpak_run + env: + GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} run: | set -euo pipefail - run_id="" + find_run_id() { + local created_after="${1:-}" + local runs - for attempt in {1..180}; do - run_json=$(gh run list \ + runs=$(gh run list \ --repo "$FLATPAK_REPOSITORY" \ --workflow "$FLATPAK_WORKFLOW" \ --branch main \ --commit "$FLATPAK_COMMIT" \ - --event push \ - --limit 1 \ - --json databaseId,status,conclusion,url,headSha) + --limit 20 \ + --json databaseId,event,headSha,createdAt) - run_id=$(echo "$run_json" | jq -r '.[0].databaseId // empty') - - if [ -n "$run_id" ]; then - status=$(echo "$run_json" | jq -r '.[0].status') - conclusion=$(echo "$run_json" | jq -r '.[0].conclusion') - url=$(echo "$run_json" | jq -r '.[0].url') - - echo "Flatpak run ${run_id}: status=${status}, conclusion=${conclusion}, url=${url}" - - if [ "$status" = "completed" ]; then - if [ "$conclusion" != "success" ]; then - echo "Flatpak workflow failed with conclusion '${conclusion}'." - exit 1 - fi - - echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" - exit 0 - fi + if [ -n "$created_after" ]; then + echo "$runs" | jq -r \ + --arg commit "$FLATPAK_COMMIT" \ + --arg created_after "$created_after" \ + '[.[] | select(.headSha == $commit and .event == "workflow_dispatch" and .createdAt >= $created_after)][0].databaseId // empty' else - echo "Waiting for Flatpak workflow on commit ${FLATPAK_COMMIT}..." + echo "$runs" | jq -r \ + --arg commit "$FLATPAK_COMMIT" \ + '[.[] | select(.headSha == $commit and (.event == "push" or .event == "workflow_dispatch"))][0].databaseId // empty' + fi + } + + validate_required_artifacts() { + local run_id="$1" + local artifacts + local expected_name + local match_count + local custom_count + + artifacts=$(gh api "repos/${FLATPAK_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100") + custom_count=$(echo "$artifacts" | jq \ + '[.artifacts[] | select(.name | startswith("MindWork AI Studio Flatpak ("))] | length') + + if [ "$custom_count" -ne 2 ]; then + echo "Flatpak run ${run_id} contains ${custom_count} release artifacts; expected 2." + return 1 fi + for expected_name in \ + "MindWork AI Studio Flatpak (x86_64)" \ + "MindWork AI Studio Flatpak (aarch64)"; do + match_count=$(echo "$artifacts" | jq \ + --arg name "$expected_name" \ + '[.artifacts[] | select(.name == $name and .expired == false)] | length') + + if [ "$match_count" -ne 1 ]; then + echo "Flatpak run ${run_id} does not contain one active '${expected_name}' artifact." + return 1 + fi + done + } + + wait_for_run() { + local run_id="$1" + local run_json + local status + local conclusion + local head_sha + local url + + for attempt in {1..180}; do + run_json=$(gh run view "$run_id" \ + --repo "$FLATPAK_REPOSITORY" \ + --json status,conclusion,headSha,url) + status=$(echo "$run_json" | jq -r '.status') + conclusion=$(echo "$run_json" | jq -r '.conclusion // empty') + head_sha=$(echo "$run_json" | jq -r '.headSha') + url=$(echo "$run_json" | jq -r '.url') + + echo "Flatpak run ${run_id}: status=${status}, conclusion=${conclusion:-pending}, url=${url}" + + if [ "$head_sha" != "$FLATPAK_COMMIT" ]; then + echo "Flatpak run ${run_id} targets '${head_sha}', expected '${FLATPAK_COMMIT}'." + return 1 + fi + + if [ "$status" = "completed" ]; then + if [ "$conclusion" = "success" ] && validate_required_artifacts "$run_id"; then + return 0 + fi + + echo "Flatpak run ${run_id} completed without usable release artifacts." + return 1 + fi + + sleep 20 + done + + echo "Timed out waiting for Flatpak run ${run_id}." + return 2 + } + + run_id="" + for attempt in {1..15}; do + run_id=$(find_run_id) + if [ -n "$run_id" ]; then + break + fi + + echo "Waiting for Flatpak workflow on commit ${FLATPAK_COMMIT}..." sleep 20 done - echo "Timed out waiting for the Flatpak workflow on commit ${FLATPAK_COMMIT}." - exit 1 + if [ -z "$run_id" ]; then + current_main=$(gh api "repos/${FLATPAK_REPOSITORY}/commits/main" --jq .sha) + if [ "$current_main" != "$FLATPAK_COMMIT" ]; then + echo "No Flatpak run exists for ${FLATPAK_COMMIT}, and Flatpak main has advanced to ${current_main}." + exit 1 + fi + + dispatch_started_at=$(date --utc +'%Y-%m-%dT%H:%M:%SZ') + gh workflow run "$FLATPAK_WORKFLOW" \ + --repo "$FLATPAK_REPOSITORY" \ + --ref main \ + -f "artifact_retention_days=${RETENTION_INTERMEDIATE_ASSETS}" + + for attempt in {1..15}; do + run_id=$(find_run_id "$dispatch_started_at") + if [ -n "$run_id" ]; then + break + fi + + echo "Waiting for the dispatched Flatpak workflow on commit ${FLATPAK_COMMIT}..." + sleep 20 + done + fi + + if [ -z "$run_id" ]; then + echo "Timed out waiting for a Flatpak workflow to start on commit ${FLATPAK_COMMIT}." + exit 1 + fi + + set +e + wait_for_run "$run_id" + wait_result=$? + set -e + + if [ "$wait_result" -eq 0 ]; then + echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$wait_result" -eq 2 ]; then + exit 1 + fi + + echo "Re-running Flatpak run ${run_id} once." + previous_attempt=$(gh api "repos/${FLATPAK_REPOSITORY}/actions/runs/${run_id}" --jq .run_attempt) + gh run rerun "$run_id" --repo "$FLATPAK_REPOSITORY" + + rerun_started=false + for attempt in {1..60}; do + rerun_state=$(gh api "repos/${FLATPAK_REPOSITORY}/actions/runs/${run_id}") + current_attempt=$(echo "$rerun_state" | jq -r '.run_attempt') + current_status=$(echo "$rerun_state" | jq -r '.status') + + if [ "$current_attempt" -gt "$previous_attempt" ] || [ "$current_status" != "completed" ]; then + rerun_started=true + break + fi + + sleep 2 + done + + if [ "$rerun_started" != "true" ]; then + echo "Timed out waiting for Flatpak run ${run_id} to start its retry." + exit 1 + fi + + if ! wait_for_run "$run_id"; then + echo "Flatpak run ${run_id} did not succeed after one retry." + exit 1 + fi + + echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" - name: Download Flatpak artifacts env: + GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }} RUN_ID: ${{ steps.flatpak_run.outputs.run_id }} run: | set -euo pipefail mkdir -p flatpak-artifacts - gh run download "$RUN_ID" --repo "$FLATPAK_REPOSITORY" --dir flatpak-artifacts + gh run download "$RUN_ID" \ + --repo "$FLATPAK_REPOSITORY" \ + --name "MindWork AI Studio Flatpak (x86_64)" \ + --name "MindWork AI Studio Flatpak (aarch64)" \ + --dir flatpak-artifacts + + expected_files=( + "MindWork AI Studio_x86_64.flatpak" + "MindWork AI Studio Plugin Pandoc_x86_64.flatpak" + "MindWork AI Studio_aarch64.flatpak" + "MindWork AI Studio Plugin Pandoc_aarch64.flatpak" + ) + + flatpak_count=$(find flatpak-artifacts -type f -name '*.flatpak' | wc -l | tr -d ' ') + if [ "$flatpak_count" -ne 4 ]; then + echo "Downloaded ${flatpak_count} Flatpak files; expected 4." + find flatpak-artifacts -type f -print + exit 1 + fi + + for expected_file in "${expected_files[@]}"; do + match_count=$(find flatpak-artifacts -type f -name "$expected_file" | wc -l | tr -d ' ') + if [ "$match_count" -ne 1 ]; then + echo "Expected exactly one '${expected_file}', found ${match_count}." + exit 1 + fi + + matched_file=$(find flatpak-artifacts -type f -name "$expected_file" -print -quit) + test -s "$matched_file" + done + find flatpak-artifacts -type f -name '*.flatpak' -print - name: Upload Flatpak artifacts @@ -446,6 +715,7 @@ jobs: name: MindWork AI Studio Flatpak Release path: flatpak-artifacts/**/*.flatpak if-no-files-found: error + overwrite: true retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} build_main: @@ -1032,16 +1302,19 @@ jobs: - name: Prepare release assets env: VERSION: ${{ needs.read_metadata.outputs.version }} - + run: | + set -euo pipefail + RELEASE_DIR="$GITHUB_WORKSPACE/release/assets" - + declare -A release_asset_sources + # Ensure the release directory exists: mkdir -p "$RELEASE_DIR" - + # Find and process files in the artifacts directory: - find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do - + while IFS= read -r -d '' FILE; do + if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig" elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then @@ -1054,10 +1327,36 @@ jobs: TARGET_NAME="$(basename "$FILE")" TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//") fi - + + if [ -n "${release_asset_sources[$TARGET_NAME]+x}" ]; then + echo "Duplicate release asset name '${TARGET_NAME}':" + echo " ${release_asset_sources[$TARGET_NAME]}" + echo " ${FILE}" + exit 1 + fi + + release_asset_sources[$TARGET_NAME]="$FILE" + cp "$FILE" "${RELEASE_DIR}/${TARGET_NAME}" + done < <(find "$GITHUB_WORKSPACE/artifacts" -type f -print0) + + expected_flatpaks=( + "MindWork AI Studio_x86_64.flatpak" + "MindWork AI Studio Plugin Pandoc_x86_64.flatpak" + "MindWork AI Studio_aarch64.flatpak" + "MindWork AI Studio Plugin Pandoc_aarch64.flatpak" + ) + + flatpak_count=$(find "$RELEASE_DIR" -maxdepth 1 -type f -name '*.flatpak' | wc -l | tr -d ' ') + if [ "$flatpak_count" -ne 4 ]; then + echo "Prepared ${flatpak_count} Flatpak release assets; expected 4." + exit 1 + fi + + for expected_flatpak in "${expected_flatpaks[@]}"; do + test -s "${RELEASE_DIR}/${expected_flatpak}" done - + # Display the structure of the release directory: ls -Rlhat $GITHUB_WORKSPACE/release/assets