Added an option for mandatory terms of use dialogs (#725)
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-04-10 17:11:05 +02:00
1 parent 09258c7548
commit 8b11e2317b
29 files changed
+836 -21

No files matched your search

+120
View File
@@ -1,4 +1,5 @@
using Markdig;
using System.Text;
namespace AIStudio.Tools;
@@ -26,4 +27,123 @@ public static class Markdown
},
}
};
public static string RemoveSharedIndentation(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
return RemoveSharedIndentation(value.AsSpan());
}
private static string RemoveSharedIndentation(ReadOnlySpan<char> value)
{
var firstContentLineStart = -1;
var lastContentLineStart = -1;
var lastContentLineEnd = -1;
var commonIndentation = int.MaxValue;
var position = 0;
while (TryGetNextLine(value, position, out var lineStart, out var currentLineEnd, out var nextPosition))
{
var lineContent = value[lineStart..currentLineEnd];
if (IsWhiteSpace(lineContent))
{
position = nextPosition;
continue;
}
if (firstContentLineStart < 0)
firstContentLineStart = lineStart;
lastContentLineStart = lineStart;
lastContentLineEnd = currentLineEnd;
commonIndentation = Math.Min(commonIndentation, CountIndentation(lineContent));
position = nextPosition;
}
if (firstContentLineStart < 0)
return string.Empty;
if (commonIndentation == int.MaxValue)
commonIndentation = 0;
var builder = new StringBuilder(lastContentLineEnd - firstContentLineStart);
var shouldAppendLineBreak = false;
position = firstContentLineStart;
while (TryGetNextLine(value, position, out var lineStart, out var lineEnd, out var nextPosition))
{
var lineContent = value[lineStart..lineEnd];
if (shouldAppendLineBreak)
builder.Append('\n');
if (IsWhiteSpace(lineContent))
shouldAppendLineBreak = true;
else if (lineContent.Length > commonIndentation)
{
builder.Append(lineContent[commonIndentation..]);
shouldAppendLineBreak = true;
}
else
shouldAppendLineBreak = true;
if (lineStart == lastContentLineStart)
break;
position = nextPosition;
}
return builder.ToString();
}
private static bool IsWhiteSpace(ReadOnlySpan<char> value)
{
foreach (var character in value)
{
if (!char.IsWhiteSpace(character))
return false;
}
return true;
}
private static int CountIndentation(ReadOnlySpan<char> value)
{
var indentation = 0;
while (indentation < value.Length && char.IsWhiteSpace(value[indentation]))
indentation++;
return indentation;
}
private static bool TryGetNextLine(ReadOnlySpan<char> value, int position, out int lineStart, out int lineEnd, out int nextPosition)
{
if (position > value.Length)
{
lineStart = 0;
lineEnd = 0;
nextPosition = position;
return false;
}
lineStart = position;
for (var i = position; i < value.Length; i++)
{
if (value[i] != '\n')
continue;
lineEnd = i > lineStart && value[i - 1] == '\r'
? i - 1
: i;
nextPosition = i + 1;
return true;
}
lineEnd = value.Length;
nextPosition = value.Length + 1;
return true;
}
}
+12 -15
View File
@@ -34,6 +34,8 @@ public static partial class Pandoc
/// </summary>
private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE;
private static readonly HttpClient WEB_CLIENT = new();
/// <summary>
/// Prepares a Pandoc process by using the Pandoc process builder.
/// </summary>
@@ -181,21 +183,18 @@ public static partial class Pandoc
// Download the latest Pandoc archive from GitHub:
//
var uri = await GenerateArchiveUriAsync();
using (var client = new HttpClient())
var response = await WEB_CLIENT.GetAsync(uri);
if (!response.IsSuccessStatusCode)
{
var response = await client.GetAsync(uri);
if (!response.IsSuccessStatusCode)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found.")));
LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage);
return;
}
// Download the archive to the temporary file:
await using var tempFileStream = File.Create(pandocTempDownloadFile);
await response.Content.CopyToAsync(tempFileStream);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found.")));
LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage);
return;
}
// Download the archive to the temporary file:
await using var tempFileStream = File.Create(pandocTempDownloadFile);
await response.Content.CopyToAsync(tempFileStream);
if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
ZipFile.ExtractToDirectory(pandocTempDownloadFile, installDir);
@@ -245,9 +244,7 @@ public static partial class Pandoc
/// <remarks>Version numbers can have the following formats: x.x, x.x.x or x.x.x.x</remarks>
/// <returns>Latest Pandoc version number</returns>
public static async Task<string> FetchLatestVersionAsync() {
using var client = new HttpClient();
var response = await client.GetAsync(LATEST_URL);
var response = await WEB_CLIENT.GetAsync(LATEST_URL);
if (!response.IsSuccessStatusCode)
{
LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}", response.StatusCode, response.RequestMessage);
@@ -1,4 +1,5 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Lua;
@@ -12,12 +13,18 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration));
private List<PluginConfigurationObject> configObjects = [];
private List<DataMandatoryInfo> mandatoryInfos = [];
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
/// </summary>
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
/// <summary>
/// The list of mandatory infos provided by this configuration plugin.
/// </summary>
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
/// <summary>
/// True/false when explicitly configured in the plugin, otherwise null.
/// </summary>
@@ -91,6 +98,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private bool TryProcessConfiguration(bool dryRun, out string message)
{
this.configObjects.Clear();
this.mandatoryInfos.Clear();
// Ensure that the main CONFIG table exists and is a valid Lua table:
if (!this.State.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
@@ -150,6 +158,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Handle configured document analysis policies:
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun);
// Handle configured mandatory infos:
this.TryReadMandatoryInfos(mainTable);
// Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@@ -163,4 +174,25 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
message = string.Empty;
return true;
}
private void TryReadMandatoryInfos(LuaTable mainTable)
{
if (!mainTable.TryGetValue("MANDATORY_INFOS", out var mandatoryInfosValue) || !mandatoryInfosValue.TryRead<LuaTable>(out var mandatoryInfosTable))
return;
for (var i = 1; i <= mandatoryInfosTable.ArrayLength; i++)
{
var luaMandatoryInfoValue = mandatoryInfosTable[i];
if (!luaMandatoryInfoValue.TryRead<LuaTable>(out var luaMandatoryInfoTable))
{
LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", i, this.Id);
continue;
}
if (DataMandatoryInfo.TryParseConfiguration(i, luaMandatoryInfoTable, this.Id, out var mandatoryInfo))
this.mandatoryInfos.Add(mandatoryInfo);
else
LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} does not contain a valid mandatory info (config plugin id: {ConfigPluginId}).", i, this.Id);
}
}
}
@@ -185,6 +185,10 @@ public static partial class PluginFactory
// Check document analysis policies:
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
wasConfigurationChanged = true;
// Check left-over mandatory info acceptances:
if (SETTINGS_MANAGER.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos()))
wasConfigurationChanged = true;
// Check for a preselected provider:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProvider, AVAILABLE_PLUGINS))
@@ -1,4 +1,5 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.PluginSystem;
@@ -127,4 +128,12 @@ public static partial class PluginFactory
HOT_RELOAD_WATCHER.Dispose();
}
public static IReadOnlyList<DataMandatoryInfo> GetMandatoryInfos()
{
return RUNNING_PLUGINS
.OfType<PluginConfiguration>()
.SelectMany(plugin => plugin.MandatoryInfos)
.ToList();
}
}
@@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public sealed record AppExitResponse(bool Success, string ErrorMessage);
@@ -1,5 +1,7 @@
using System.Security.Cryptography;
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
@@ -117,4 +119,35 @@ public sealed partial class RustService
return await response.Content.ReadAsStringAsync();
}
/// <summary>
/// Requests the Rust runtime to exit the entire desktop application.
/// </summary>
public async Task<bool> ExitApplication()
{
try
{
var response = await this.http.PostAsync("/app/exit", null);
if (!response.IsSuccessStatusCode)
{
this.logger?.LogError("Failed to exit the app due to network error: {StatusCode}.", response.StatusCode);
return false;
}
var result = await response.Content.ReadFromJsonAsync<AppExitResponse>(this.jsonRustSerializerOptions);
if (result is null || !result.Success)
{
this.logger?.LogError("Failed to exit the app: {Error}", result?.ErrorMessage ?? "Unknown error");
return false;
}
this.logger?.LogInformation("Exit request sent to Rust runtime.");
return true;
}
catch (Exception ex)
{
this.logger?.LogError(ex, "Exception while requesting application exit.");
return false;
}
}
}