Prepared release v26.11.1 (#1173)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-07 13:14:58 +01:00
1 parent 9d1768e2f4
commit 520758771b
11 files changed
+339 -66

No files matched your search

+170 -38
View File
@@ -1,25 +1,62 @@
using System.Formats.Tar;
using System.IO.Compression;
using Build.Tools;
using SharedTools;
namespace Build.Commands;
public static class Pdfium
{
private static readonly HttpClient CLIENT = new()
private const int MAX_DOWNLOAD_ATTEMPTS = 3;
private static readonly TimeSpan CONNECT_TIMEOUT = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RESPONSE_TIMEOUT = TimeSpan.FromSeconds(60);
private static readonly TimeSpan TRANSFER_TIMEOUT = TimeSpan.FromMinutes(5);
private static readonly TimeSpan RETRY_DELAY = TimeSpan.FromSeconds(5);
//
// Without a handler of its own, HttpClient waits for a connection without any limit, so a single
// connection attempt which hangs uses up the whole timeout of the request. That happened twice while
// preparing v26.11.1, while curl fetched the same file within a second. The handler limits each
// connection attempt, the TLS handshake included, and each download attempt has its own time budget:
//
private static readonly HttpClient CLIENT = new(new SocketsHttpHandler { ConnectTimeout = CONNECT_TIMEOUT })
{
Timeout = TimeSpan.FromMinutes(5)
Timeout = Timeout.InfiniteTimeSpan
};
/// <summary>
/// Installs the Pdfium library of the given version into the resources of the Rust runtime.
/// </summary>
/// <remarks>
/// Every failure ends the build: without the library, AI Studio cannot read PDF files, and Tauri would fail
/// later with a far less clear message, because its resources include the folder of the library. The download
/// is skipped when the library of this version is already installed, which a version marker in the target
/// folder of the runtime records. The marker cannot sit next to the library, because Tauri ships everything
/// in that folder.
/// </remarks>
/// <param name="rid">The RID to install the library for.</param>
/// <param name="version">The Chromium revision of the Pdfium build, e.g., 7763.</param>
/// <param name="offline">True, when the build must not download anything and uses the library already present.</param>
public static async Task InstallAsync(RID rid, string version, bool offline)
{
Console.Write($"- Installing Pdfium {version} for {rid.UserFriendlyName} ...");
var cwd = Environment.GetRustRuntimeDirectory();
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
var library = GetLibraryPath(rid);
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
if (string.IsNullOrWhiteSpace(library.Path) || string.IsNullOrWhiteSpace(pdfiumUrl))
{
Console.WriteLine(" failed.");
Console.WriteLine($"- Error: There is no Pdfium library for {rid.UserFriendlyName}.");
throw new CommandExitedException(1);
}
var cwd = Environment.GetRustRuntimeDirectory();
var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename);
var versionMarkerPath = Path.Join(cwd, "target", "pdfium.version");
var versionMarker = $"{version} {rid.MicrosoftRid}";
if (offline)
{
@@ -29,45 +66,90 @@ public static class Pdfium
return;
}
Console.WriteLine($" failed because offline mode is enabled and '{pdfiumLibTargetPath}' does not exist");
return;
Console.WriteLine(" failed.");
Console.WriteLine($"- Error: Offline mode is enabled, but '{pdfiumLibTargetPath}' does not exist. Build once without --offline to download it.");
throw new CommandExitedException(1);
}
if (string.IsNullOrWhiteSpace(library.Path))
if (File.Exists(pdfiumLibTargetPath) && await ReadVersionMarkerAsync(versionMarkerPath) == versionMarker)
{
Console.WriteLine($" failed to find the library path for {rid.UserFriendlyName}");
Console.WriteLine(" already installed, skipping download");
return;
}
var pdfiumLibTargetDirectory = Path.Join(cwd, "resources", "libraries");
var pdfiumLibTmpTargetPath = Path.Join(pdfiumLibTargetDirectory, $"{library.Filename}.{Guid.NewGuid():N}.tmp");
var pdfiumLibArchivePath = library.Path.Replace('\\', '/');
//
// Download the file:
//
Console.Write(" downloading ...");
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
for (var attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt++)
{
Console.WriteLine($" failed to download Pdfium {version} for {rid.UserFriendlyName} from {pdfiumUrl}");
return;
try
{
await DownloadAndDeployAsync(pdfiumUrl, library.Path.Replace('\\', '/'), pdfiumLibTargetPath, versionMarkerPath, versionMarker);
Console.WriteLine(" done.");
return;
}
catch (Exception exception) when (IsTransientFailure(exception))
{
Console.Write($" failed: {DescribeFailure(exception)}");
if (attempt < MAX_DOWNLOAD_ATTEMPTS)
{
Console.Write($" Retrying in {RETRY_DELAY.TotalSeconds:0} seconds ...");
await Task.Delay(RETRY_DELAY);
}
}
}
//
// Extract the library from the downloaded file:
//
Console.Write(" extracting ...");
Directory.CreateDirectory(pdfiumLibTargetDirectory);
Console.WriteLine();
Console.WriteLine($"- Error: Could not download Pdfium {version} for {rid.UserFriendlyName} from {pdfiumUrl} in {MAX_DOWNLOAD_ATTEMPTS} attempts.");
if (File.Exists(pdfiumLibTargetPath))
Console.WriteLine(" To build with the library which is already present, use --offline.");
var foundLibrary = false;
throw new CommandExitedException(1);
}
/// <summary>
/// Downloads the Pdfium archive once, extracts the library from it, and puts the library in place.
/// </summary>
/// <remarks>
/// A failure which the next attempt might not run into, such as a broken connection or a timeout, leaves this
/// method as an exception, which IsTransientFailure recognizes. A failure which every attempt would run into,
/// such as an unknown version, ends the build right here.
/// </remarks>
/// <param name="pdfiumUrl">The address of the Pdfium archive.</param>
/// <param name="pdfiumLibArchivePath">The path of the library inside the archive, with forward slashes.</param>
/// <param name="pdfiumLibTargetPath">Where to put the library.</param>
/// <param name="versionMarkerPath">Where to record which version the library has.</param>
/// <param name="versionMarker">The version and RID of the library.</param>
private static async Task DownloadAndDeployAsync(string pdfiumUrl, string pdfiumLibArchivePath, string pdfiumLibTargetPath, string versionMarkerPath, string versionMarker)
{
var pdfiumLibTmpTargetPath = $"{pdfiumLibTargetPath}.{Guid.NewGuid():N}.tmp";
var receivedResponse = false;
using var attemptTimeout = new CancellationTokenSource(RESPONSE_TIMEOUT);
try
{
await using var downloadStream = await response.Content.ReadAsStreamAsync();
//
// Download the file:
//
Console.Write(" downloading ...");
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead, attemptTimeout.Token);
receivedResponse = true;
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(" failed.");
Console.WriteLine($"- Error: The download of {pdfiumUrl} failed with {(int)response.StatusCode} {response.ReasonPhrase}. Check the Pdfium version in metadata.txt.");
throw new CommandExitedException(1);
}
//
// Extract the library from the downloaded file:
//
attemptTimeout.CancelAfter(TRANSFER_TIMEOUT);
Console.Write(" extracting ...");
Directory.CreateDirectory(Path.GetDirectoryName(pdfiumLibTargetPath)!);
var foundLibrary = false;
await using var downloadStream = await response.Content.ReadAsStreamAsync(attemptTimeout.Token);
await using var uncompressedStream = new GZipStream(downloadStream, CompressionMode.Decompress);
await using var tarReader = new TarReader(uncompressedStream);
while (await tarReader.GetNextEntryAsync() is { } entry)
while (await tarReader.GetNextEntryAsync(cancellationToken: attemptTimeout.Token) is { } entry)
{
if (!string.Equals(entry.Name.Replace('\\', '/'), pdfiumLibArchivePath, StringComparison.Ordinal))
continue;
@@ -76,35 +158,85 @@ public static class Pdfium
break;
await using var fileStream = File.Create(pdfiumLibTmpTargetPath);
await entry.DataStream.CopyToAsync(fileStream);
await entry.DataStream.CopyToAsync(fileStream, attemptTimeout.Token);
foundLibrary = true;
break;
}
if (!foundLibrary)
{
Console.WriteLine($" failed to find the library file '{pdfiumLibArchivePath}' in the Pdfium archive");
return;
Console.WriteLine(" failed.");
Console.WriteLine($"- Error: The Pdfium archive {pdfiumUrl} does not contain the library file '{pdfiumLibArchivePath}'.");
throw new CommandExitedException(1);
}
//
// Deploy the library. The marker goes first, so that it never describes a library
// which was only half replaced:
//
Console.Write(" deploying ...");
File.Delete(versionMarkerPath);
File.Move(pdfiumLibTmpTargetPath, pdfiumLibTargetPath, true);
Directory.CreateDirectory(Path.GetDirectoryName(versionMarkerPath)!);
await File.WriteAllTextAsync(versionMarkerPath, versionMarker, Environment.UTF8_NO_BOM);
}
catch (OperationCanceledException) when (attemptTimeout.IsCancellationRequested)
{
throw new TimeoutException(receivedResponse
? $"The download did not finish within {TRANSFER_TIMEOUT.TotalMinutes:0} minutes."
: $"There was no answer within {RESPONSE_TIMEOUT.TotalSeconds:0} seconds.");
}
catch (OperationCanceledException exception) when (exception.GetBaseException() is TimeoutException)
{
// The handler reports its connect timeout without the time it waited:
throw new TimeoutException($"No connection could be established within {CONNECT_TIMEOUT.TotalSeconds:0} seconds.");
}
finally
{
if (File.Exists(pdfiumLibTmpTargetPath))
File.Delete(pdfiumLibTmpTargetPath);
}
Console.WriteLine(" done.");
}
/// <summary>
/// Whether another attempt may succeed where a download attempt failed with the given exception.
/// </summary>
/// <param name="exception">The exception the attempt failed with.</param>
/// <returns>True, when the download is worth another attempt.</returns>
private static bool IsTransientFailure(Exception exception) => exception is HttpRequestException or OperationCanceledException or TimeoutException or IOException or InvalidDataException;
/// <summary>
/// Describes why a download attempt failed, in one line.
/// </summary>
/// <remarks>
/// The innermost exception names the actual cause, such as the socket error behind an HttpRequestException
/// or the timeout behind a canceled connection attempt, while the outer one often names the host.
/// </remarks>
/// <param name="exception">The exception the attempt failed with.</param>
/// <returns>The reason of the failure.</returns>
private static string DescribeFailure(Exception exception)
{
var innermost = exception.GetBaseException();
if (innermost == exception || exception is OperationCanceledException)
return innermost.Message;
return $"{exception.Message} {innermost.Message}";
}
private static async Task<string> ReadVersionMarkerAsync(string versionMarkerPath)
{
if (!File.Exists(versionMarkerPath))
return string.Empty;
return (await File.ReadAllTextAsync(versionMarkerPath, Encoding.UTF8)).Trim();
}
private static Library GetLibraryPath(RID rid) => rid switch
{
RID.LINUX_ARM64 or RID.LINUX_X64 => new(Path.Join("lib", "libpdfium.so"), "libpdfium.so"),
RID.OSX_ARM64 or RID.OSX_X64 => new(Path.Join("lib", "libpdfium.dylib"), "libpdfium.dylib"),
RID.WIN_ARM64 or RID.WIN_X64 => new(Path.Join("bin", "pdfium.dll"), "pdfium.dll"),
_ => new(string.Empty, string.Empty),
};
@@ -115,13 +247,13 @@ public static class Pdfium
{
RID.LINUX_ARM64 => $"{baseUrl}linux-arm64.tgz",
RID.LINUX_X64 => $"{baseUrl}linux-x64.tgz",
RID.OSX_ARM64 => $"{baseUrl}mac-arm64.tgz",
RID.OSX_X64 => $"{baseUrl}mac-x64.tgz",
RID.WIN_ARM64 => $"{baseUrl}win-arm64.tgz",
RID.WIN_X64 => $"{baseUrl}win-x64.tgz",
_ => string.Empty,
};
}
+95 -17
View File
@@ -270,12 +270,35 @@ public sealed partial class UpdateMetadataCommands
throw new CommandExitedException(1);
}
// The previous release created the changelog with a placeholder in its first line, which
// reserved the build number of this release. We fill in the build time below, so check now,
// before anything is written, that the placeholder is still there:
var changelogPlaceholderHeader = GetFirstLine(await File.ReadAllTextAsync(changelogPath, Encoding.UTF8));
var expectedBuildNumber = await ReadBuildNumber() + 1;
if (!IsChangelogPlaceholderHeader(changelogPlaceholderHeader, appVersion.VersionText, expectedBuildNumber))
{
Console.WriteLine($"- Error: The first line of '{Path.GetFileName(changelogPath)}' must be the placeholder '# v{appVersion.VersionText}, build {expectedBuildNumber} (YYYY-MM-xx xx:xx UTC)', but it is '{changelogPlaceholderHeader}'.");
throw new CommandExitedException(1);
}
try
{
await EnsureMetainfoReleaseCanBeWritten(appVersion.VersionText);
}
catch (InvalidOperationException exception)
{
Console.WriteLine($"- Error: {exception.Message}");
throw new CommandExitedException(1);
}
await this.UpdateAppVersion(appVersion);
var buildNumber = await this.IncreaseBuildNumber();
var buildTime = await this.UpdateBuildTime();
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime);
var buildTimeText = await this.UpdateBuildTime();
var buildTime = ParseMetadataBuildTime(buildTimeText);
await UpdateChangelogHeader(changelogPath, changelogPlaceholderHeader, appVersion.VersionText, buildNumber, buildTime);
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTimeText);
await this.CreateNextChangelog(buildNumber, appVersion);
await WriteMetainfoRelease(appVersion.VersionText, ParseMetadataBuildTime(buildTime));
await WriteMetainfoRelease(appVersion.VersionText, buildTime);
await this.UpdateProjectCommitHash();
await this.UpdateReleaseDependenciesAndLicence();
Console.WriteLine();
@@ -647,13 +670,8 @@ public sealed partial class UpdateMetadataCommands
var nextChangelog = nextChangelogCandidates[0];
// 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.");
if (!ReleasesStartRegex().IsMatch(await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8)))
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
// missing and moves it to the top otherwise. Here, we only ensure that it can be written:
await EnsureMetainfoReleaseCanBeWritten(appVersion);
var headCommitHash = (await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD")).Text.Trim();
if (!GitCommitHashRegex().IsMatch(headCommitHash))
@@ -731,13 +749,7 @@ public sealed partial class UpdateMetadataCommands
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 <releases> element.");
var metainfo = await ReadMetainfo();
var changelogEntries = await ReadChangelogEntries(appVersion);
// Drop any earlier entry of this version, so that the version stays unique and moves to the top.
@@ -771,6 +783,34 @@ public sealed partial class UpdateMetadataCommands
Console.WriteLine($"- Updated the AppStream metainfo for v{appVersion}, released on {releaseDate}, with {changelogEntries.Count} changelog entries.");
}
/// <summary>
/// Checks everything WriteMetainfoRelease needs for the given version, without writing anything.
/// </summary>
/// <remarks>
/// Preparing a release writes the AppStream entry last, after the metadata and the changelogs were changed. When
/// it failed only there, e.g., because the changelog has no entry yet, it would leave a half-prepared release
/// behind. Hence, the commands call this before they write anything.
/// </remarks>
/// <param name="appVersion">The version whose release entry will be written.</param>
private static async Task EnsureMetainfoReleaseCanBeWritten(string appVersion)
{
await ReadMetainfo();
await ReadChangelogEntries(appVersion);
}
private static async Task<string> ReadMetainfo()
{
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 <releases> element.");
return metainfo;
}
private static async Task<IReadOnlyList<string>> ReadChangelogEntries(string appVersion)
{
var changelogPath = GetChangelogPath(appVersion);
@@ -853,6 +893,37 @@ public sealed partial class UpdateMetadataCommands
return $"# v{appVersion}, build {buildNumber} ({buildTime.ToUniversalTime():yyyy-MM-dd HH:mm} UTC)";
}
/// <summary>
/// Whether a changelog header is the placeholder which the previous release created for this version and build.
/// </summary>
/// <remarks>
/// The month of the placeholder is only a guess of when the release would happen, so any month matches.
/// </remarks>
/// <param name="changelogHeader">The first line of the changelog.</param>
/// <param name="appVersion">The version of the release.</param>
/// <param name="buildNumber">The build number of the release.</param>
/// <returns>True, when the header is the placeholder.</returns>
private static bool IsChangelogPlaceholderHeader(string changelogHeader, string appVersion, int buildNumber)
{
return Regex.IsMatch(changelogHeader, $"^# v{Regex.Escape(appVersion)}, build {buildNumber} \\(20[0-9]{{2}}-[0-9]{{2}}-xx xx:xx UTC\\)$");
}
/// <summary>
/// Replaces the placeholder in the first line of a changelog with the header of the release.
/// </summary>
/// <param name="changelogPath">The path of the changelog.</param>
/// <param name="placeholderHeader">The placeholder, as checked before anything was written.</param>
/// <param name="appVersion">The version of the release.</param>
/// <param name="buildNumber">The build number of the release.</param>
/// <param name="buildTime">The build time of the release.</param>
private static async Task UpdateChangelogHeader(string changelogPath, string placeholderHeader, string appVersion, int buildNumber, DateTime buildTime)
{
var changelogContent = await File.ReadAllTextAsync(changelogPath, Encoding.UTF8);
var updatedChangelog = ReplaceExactlyOnce(changelogContent, placeholderHeader, FormatChangelogHeader(appVersion, buildNumber, buildTime));
await File.WriteAllTextAsync(changelogPath, updatedChangelog, Environment.UTF8_NO_BOM);
Console.WriteLine($"- Updated the header of '{Path.GetFileName(changelogPath)}'.");
}
private static string FormatChangelogLogEntry(string appVersion, int buildNumber, DateTime buildTime, string changelogFilename)
{
return $"new ({buildNumber}, \"v{appVersion}, build {buildNumber} ({buildTime.ToUniversalTime():yyyy-MM-dd HH:mm} UTC)\", \"{changelogFilename}\"),";
@@ -1263,6 +1334,13 @@ public sealed partial class UpdateMetadataCommands
return new(sb.ToString(), process.ExitCode);
}
private static async Task<int> ReadBuildNumber()
{
const int BUILD_NUMBER_INDEX = 2;
var lines = await File.ReadAllLinesAsync(Environment.GetMetadataPath(), Encoding.UTF8);
return int.Parse(lines[BUILD_NUMBER_INDEX]);
}
private async Task<int> IncreaseBuildNumber()
{
const int BUILD_NUMBER_INDEX = 2;
@@ -13,6 +13,7 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (260, "v26.11.1, build 260 (2026-11-07 11:04 UTC)", "v26.11.1.md"),
new (259, "v26.10.1, build 259 (2026-10-29 07:39 UTC)", "v26.10.1.md"),
new (257, "v26.9.1, build 257 (2026-09-28 18:24 UTC)", "v26.9.1.md"),
new (255, "v26.8.2, build 255 (2026-08-31 07:45 UTC)", "v26.8.2.md"),
@@ -1,4 +1,4 @@
# v26.11.1, build 260 (2026-11-xx xx:xx UTC)
# v26.11.1, build 260 (2026-11-07 11:04 UTC)
- Added a setting for the size of the interface, which you find in the app settings. You can choose a size for each of your screens, and AI Studio switches to it when you move its window to another screen. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
- Added formulas in code blocks to the chat: when an AI model puts a single formula into a code block marked as LaTeX, the chat now shows the formula instead of its code. A code block with a LaTeX document or with text in it stays code.
- Added buttons to copy and save the formulas in the chat. When you point at a formula, you can copy its LaTeX code or save it as a LaTeX file, and the export menu of an answer offers its formulas as well.
@@ -0,0 +1 @@
# v26.11.2, build 261 (2026-11-xx xx:xx UTC)
+4 -2
View File
@@ -23,6 +23,8 @@ Regardless of whether you want to build the app locally for yourself (not trusti
This is necessary because the build script and the Tauri framework assume that the .NET app is available as a so-called "sidecar." Although the sidecar is only necessary for the final release and shipping, Tauri requires it to be present during development as well.
The build also downloads the PDF library Pdfium in the version which `metadata.txt` names, and downloads it again only when that version changes. `dotnet run build --offline` skips the download and uses the library which is already present.
## The quality gate
One command checks that what you are about to build is sound:
@@ -79,10 +81,10 @@ Changes which touch only the documentation, such as the README or the files in `
## Create a release
In order to create a release:
1. To create a new release, you need to be a maintainer of the repository—see step 8.
2. Make sure there's a changelog file for the version you want to create in the `/app/MindWork AI Studio/wwwroot/changelog` directory. Name the file `vX.Y.Z.md` and include all release changes—your updates and any others included in this version.
2. Make sure there's a changelog file for the version you want to create in the `/app/MindWork AI Studio/wwwroot/changelog` directory. Name the file `vX.Y.Z.md` and include all release changes—your updates and any others included in this version. Keep its first line, the placeholder which the previous release created, such as `# v26.11.2, build 261 (2026-11-xx xx:xx UTC)`; when you rename the file for another version, change the version in that line as well. The release fills in the build time and refuses to start when the line names another version or build.
3. After you have created the changelog file, you must commit the changes to the repository.
4. To prepare a new release, open a terminal, go to `/app/Build` and run `dotnet run release --action <ACTION>`, where `<ACTION>` raises one part of the version, which reads year.month.release: `build` raises the release (26.11.1 becomes 26.11.2), `month` the month (26.12.1), and `year` the year (27.1.1). When a month passed without a release, set the version directly instead, e.g., `dotnet run release --version 27.2.1`.
5. Now wait until all process steps have been completed. Among other things, the version number will be incremented, the new changelog registered, and the version numbers of central dependencies updated, etc.
5. Now wait until all process steps have been completed. Among other things, the version number will be incremented, the build time written into the header of the new changelog, the changelog registered, and the version numbers of central dependencies updated, etc.
6. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes.
7. Your proposed changes will be reviewed and merged.
8. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`.
+5 -5
View File
@@ -1,11 +1,11 @@
26.10.1
2026-10-29 07:39:07 UTC
259
26.11.1
2026-11-07 11:04:21 UTC
260
10.0.403 (commit 5a2162bea6)
10.0.14 (commit 0427b9abbe)
1.99.1 (commit 58cf8f9c3)
9.12.0
2.11.5
fd0a42fa8ab, release
2.12.5
9d1768e2f40, release
148.0.7763.0
0.8.0
+1 -1
View File
@@ -4296,7 +4296,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mindwork-ai-studio"
version = "26.10.1"
version = "26.11.1"
dependencies = [
"aes 0.9.1",
"aho-corasick",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "mindwork-ai-studio"
version = "26.10.1"
version = "26.11.1"
edition = "2024"
description = "MindWork AI Studio"
authors = ["Thorsten Sommer"]
@@ -120,6 +120,65 @@
</screenshots>
<releases>
<release type="stable" version="26.11.1" date="2026-11-07">
<description>
<ul>
<li>Added a setting for the size of the interface, which you find in the app settings. You can choose a size for each of your screens, and AI Studio switches to it when you move its window to another screen. Thanks, Paul Koudelka (<code>PaulKoudelka</code>), for this contribution.</li>
<li>Added formulas in code blocks to the chat: when an AI model puts a single formula into a code block marked as LaTeX, the chat now shows the formula instead of its code. A code block with a LaTeX document or with text in it stays code.</li>
<li>Added buttons to copy and save the formulas in the chat. When you point at a formula, you can copy its LaTeX code or save it as a LaTeX file, and the export menu of an answer offers its formulas as well.</li>
<li>Added support for OneNote sections (<code>.one</code>): you can attach them to chats and assistants and use them in your local data sources. AI Studio reads every page, including the text OneNote recognized in pictures and handwriting.</li>
<li>Added the Mathematics tools as a prototype: the AI can now calculate exactly and work with formulas, right on your computer. It keeps fractions exact, gives results with up to 100 digits, solves equations, differentiates and integrates, and can check whether an integral really has the value someone claims. AI Studio checks the results numerically wherever possible, and the AI tells you when a result is only approximate. To try it, open the app settings, allow preview features down to prototype, enable Mathematics, and then choose Mathematics among the tools of a chat.</li>
<li>Added a way for IT departments to preselect the options of the Prompt Optimizer, such as the prompt guide of your organization, the language of the optimized prompt, and the provider. They can lock each option or let you change it.</li>
<li>Added your own prompt guide to the options of the Prompt Optimizer. Choose the file once, and the assistant uses it whenever you open it or start over, always with the current content of the file.</li>
<li>Added a way to see the complete system prompt of a chat: the button with the eye in the chat toolbar shows the instructions the AI receives ahead of your conversation, part by part, such as your chat template, your profile, data from your data sources, and the instructions of your tools. You can copy each part or the complete prompt. Thanks, Jens Erler (<code>j-erler</code>), for this contribution.</li>
<li>Added an overview of all files you sent in a chat: the button next to the paperclip in the chat toolbar lists the files of all your messages, grouped by their folder. When you sent a file in several versions, the overview lists each of them with the time the file was saved. You can read the text the AI received from each file, even when you moved or deleted the file since. Thanks, Jens Erler (<code>j-erler</code>), for this contribution.</li>
<li>Improved the documentation for IT departments, which now lives in our wiki. It is sorted by task, explains the setup on Windows, Linux, and macOS, and offers complete configurations for typical needs, such as using only local models. The link in the app settings leads there, and the wiki also holds the installation guide in English and German.</li>
<li>Improved the German translation throughout: we proofread all texts and corrected wrong labels as well as spelling and grammar mistakes. The texts now always address you formally and use the same terms everywhere.</li>
<li>Improved the security of the connection between the app window and AI Studio.</li>
<li>Improved how reliably AI Studio reads and stores your settings, chats, and backups.</li>
<li>Improved how reliably AI Studio exchanges data with AI providers, external data sources, and web search services.</li>
<li>Improved how reliably the Visual Briefing assistant stores your briefings, and how reliably the assistants of AI Studio read the answers of AI models.</li>
<li>Improved how AI Studio colors code blocks: it now highlights them itself instead of through an extra script, which makes the app smaller. This works for Python, JavaScript, TypeScript, Java, C, C++, C#, Go, Kotlin, Rust, SQL, shell scripts, PowerShell, Lua, CSS, HTML, XML, JSON, and YAML; code in other languages, such as Ruby or PHP, appears without colors. When a code block names no language, AI Studio recognizes it from the code, colors the block, and suggests the matching file extension when you save it.</li>
<li>Improved how carefully AI Studio reads the answers of AI models and external data sources: an answer that contains the same entry twice, possibly with two different values, is now refused instead of AI Studio picking one of them. This covers tool calls, the Assistant Builder, the Visual Briefing assistant, and ERI servers.</li>
<li>Improved the order of the models when you add or edit a provider: AI Studio now sorts the numbers in model names by their value, so that, for example, a model with version 4.10 comes before one with version 4.9, and the newest models stay at the top of the list.</li>
<li>Improved the information page, which now shows the Rust compiler that actually built your copy of AI Studio. Before, it always showed the compiler of our official builds, even for builds such as the Flatpak, which bring their own one.</li>
<li>Improved the options of the Slide Assistant: a button in the field of the PowerPoint template removes your template again, so the assistant plans an outline only.</li>
<li>Improved the chats you start from the result of an assistant with "Send to ..." and "New Chat": the AI there now knows the role of the assistant and the settings you chose, such as the language, answers your follow-up questions about the result, and revises it on request. This applies to the Text Summarizer, Translation, Grammar &amp; Spelling Checker, Rewrite &amp; Improve Text, Synonyms, Agenda Planner, E-Mail, Job Posting, SWOT Analysis, Document Analysis, and Slide assistants.</li>
<li>Improved the chats you start from the result of the Legal Check or My Tasks assistant with "Send to ..." and "New Chat": the AI there now follows the instructions of the assistant.</li>
<li>Changed the size of AI Studio on laptop screens: it now appears in the same size on every laptop, whatever the scaling of your operating system. If you prefer the previous size, choose "System scaling" for the interface size in the app settings. Contributed by Paul Koudelka.</li>
<li>Changed your local data sources and mailboxes to return at most as many matches as you set in their options. Before, they could return up to twice as many. Now the best matches of the search by meaning and of the search by words take turns, and when one of them finds only a few, the other one fills in.</li>
<li>Changed the connection to Hetzner's inference API to its new address. Hetzner turns off the old address on November 10, 2026, so older versions of AI Studio can no longer use Hetzner after that date.</li>
<li>Fixed AI Studio becoming unusable when an update failed after its download. The app now restarts with the version you have installed, so you can simply try the update again.</li>
<li>Fixed configuration plugins you installed yourself being able to change the tool settings of your organization, such as the minimum provider confidence of a tool. These settings now follow the same rules as all other settings, which also gives you back your own values once your organization removes its configuration.</li>
<li>Fixed the log of AI Studio reporting that a configuration plugin you installed yourself tried to take over a setting of your organization, even when the plugin does not contain that setting.</li>
<li>Fixed the Agenda Planner assistant leaving out the location of your event when it suggests a social activity.</li>
<li>Fixed the ERI Server assistant leaving out your description of the authentication process when it generates a server.</li>
<li>Fixed the ERI Server assistant not writing some generated files to disk, such as docker-compose.yml. It now tells you about every file it could not write.</li>
<li>Fixed configuration plugins being unable to lock the preselected provider, the preselected profile, or the transcription provider to none. An empty value now does this, as the example configuration plugin describes. Older versions of AI Studio ignore the empty value and leave the choice to your users.</li>
<li>Fixed AI Studio loading none of your organization's configurations when one place, such as the Windows registry, named a configuration with an invalid ID. AI Studio now skips that entry and uses the configurations from the other places.</li>
<li>Fixed chats being stored although you chose "Store chats manually" or "Disable workspaces". AI Studio now stores a chat only when you click "Save chat", or not at all while workspaces are disabled. In both modes, leaving a chat whose answer is still being written asks you first and stops that answer. Chats outside of workspaces that earlier versions stored this way appear among your disappearing chats once you use workspaces again.</li>
<li>Fixed saving a code block as a file, where the last line could end differently from all other lines, for example, on Windows. All lines of the file now end the same way.</li>
<li>Fixed the result of a tool in the chat showing umlauts, quotation marks, and other special characters as codes instead of the characters themselves.</li>
<li>Fixed the Assistant Builder telling you in English why it could not use the answer of the AI model, even when you chose another language for AI Studio.</li>
<li>Fixed requests to a provider failing when a configuration plugin gave it additional API parameters that contain the same parameter twice. AI Studio now sends the request without these parameters and names the repeated parameter in its log, as it does for other invalid parameters.</li>
<li>Fixed the information page leaving out three libraries that AI Studio uses: MailKit and MimeKit, which read your mailboxes, and the Open XML SDK, with which the Slide Assistant reads and writes PowerPoint files.</li>
<li>Fixed AI Studio asking you to install Pandoc when you load a file as the input of an assistant, although PDF, Word, Markdown, and most other files need no Pandoc.</li>
<li>Fixed dropping a file onto your custom prompt guide in the Prompt Optimizer. Once you switch on "Use custom prompt guide", a marked area appears where you can drop your guide or select it with a button.</li>
<li>Fixed Semantic Search leaving out passages of your data sources which did not fit into a single search. The AI now gets every passage it finds, page by page, and a passage too long for a whole page is shortened instead of left out.</li>
<li>Fixed chats with data sources failing when you switched off Semantic Search, because AI Studio sent more passages along with your message than the AI model can read. AI Studio now sends only as many passages as fit, taking turns among your data sources, and tells you with each answer when some passages had to be left out.</li>
<li>Fixed the AI search in your mailboxes leaving out mails which did not fit into a single search. The AI now gets every mail it finds, page by page.</li>
<li>Fixed pages of AI Studio, such as those behind the icons of the navigation bar, opening in your web browser when you clicked them while holding Shift or Ctrl. Such clicks now do nothing, while links to websites still open in your browser, and now also from the AppImage on Linux. The right-click menu now appears only in text fields and on selected text, so it no longer offers entries such as opening a link in a new window.</li>
<li>Fixed chats which you start from the result of the Coding Assistant, the Icon Finder, or Bias of the Day losing the instructions of the assistant. Since v26.4.1, these chats got the default instructions of AI Studio instead.</li>
<li>Fixed the Assistant Builder offering "Send to ...", which only opened an empty chat or an empty assistant. The button is gone; copy the plugin with "Copy result" instead.</li>
<li>Fixed the message for plugin developers when an assistant plugin lacks a valid <code>SubmitText</code>: it said that the system prompt was missing.</li>
<li>Fixed the chat showing the chat template or profile preselected for new chats when you open a chat which uses none, e.g., a chat started from an assistant. The AI never received that chat template or profile in such a chat, and the field for your message no longer starts with the predefined message of that chat template.</li>
<li>Upgraded several libraries to improve security.</li>
<li>Upgraded Tauri to v2.12.5.</li>
<li>Upgraded MudBlazor to v9.12.0.</li>
<li>Upgraded to .NET v10.0.14.</li>
</ul>
</description>
</release>
<release type="stable" version="26.10.1" date="2026-10-29">
<description>
<ul>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"productName": "MindWork AI Studio",
"mainBinaryName": "MindWork AI Studio",
"version": "26.10.1",
"version": "26.11.1",
"identifier": "com.github.mindwork-ai.ai-studio",
"build": {