Merge branch 'main' into custom-tokenizer

This commit is contained in:
PaulKoudelka committed 2026-04-24 12:07:22 +02:00
commit e3cb7e9734
165 files changed
+4739 -1738

No files matched your search

@@ -13,6 +13,7 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"),
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
new (232, "v26.1.2, build 232 (2026-01-25 14:05 UTC)", "v26.1.2.md"),
@@ -70,6 +70,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty;
private Guid currentChatThreadId = Guid.Empty;
private int workspaceHeaderSyncVersion;
private CancellationTokenSource? cancellationTokenSource;
private HashSet<FileAttachment> chatDocumentPaths = [];
private string tokenCount = "0";
@@ -97,6 +98,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
var deferredInput = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(deferredInput))
this.userInput = deferredInput;
// Apply template's file attachments, if any:
foreach (var attachment in this.currentChatTemplate.FileAttachments)
this.chatDocumentPaths.Add(attachment.Normalize());
@@ -213,12 +218,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// workspace name is loaded:
//
if (this.ChatThread is not null)
{
this.currentChatThreadId = this.ChatThread.ChatId;
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
await this.SyncWorkspaceHeaderWithChatThreadAsync();
// Select the correct provider:
await this.SelectProviderWhenLoadingChat();
@@ -235,10 +235,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
await this.Workspaces.StoreChatAsync(this.ChatThread);
else
await WorkspaceBehaviour.StoreChatAsync(this.ChatThread);
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
await this.SyncWorkspaceHeaderWithChatThreadAsync();
}
if (firstRender && this.mustLoadChat)
@@ -251,9 +249,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
await this.SyncWorkspaceHeaderWithChatThreadAsync();
await this.SelectProviderWhenLoadingChat();
}
else
@@ -288,40 +285,59 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private async Task SyncWorkspaceHeaderWithChatThreadAsync()
{
if (this.ChatThread is null)
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
var currentChatThread = this.ChatThread;
if (currentChatThread is null)
{
if (this.currentChatThreadId != Guid.Empty || this.currentWorkspaceId != Guid.Empty || !string.IsNullOrWhiteSpace(this.currentWorkspaceName))
{
this.currentChatThreadId = Guid.Empty;
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
}
this.ClearWorkspaceHeaderState();
return;
}
// Guard: If ChatThread ID and WorkspaceId haven't changed, skip entirely.
// Using ID-based comparison instead of name-based to correctly handle
// temporary chats where the workspace name is always empty.
if (this.currentChatThreadId == this.ChatThread.ChatId
&& this.currentWorkspaceId == this.ChatThread.WorkspaceId)
if (this.currentChatThreadId == currentChatThread.ChatId
&& this.currentWorkspaceId == currentChatThread.WorkspaceId)
return;
this.currentChatThreadId = this.ChatThread.ChatId;
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
var loadedWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
var chatThreadId = currentChatThread.ChatId;
var workspaceId = currentChatThread.WorkspaceId;
var loadedWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId);
// Only notify the parent when the name actually changed to prevent
// an infinite render loop: WorkspaceName → UpdateWorkspaceName →
// StateHasChanged → re-render → OnParametersSetAsync → WorkspaceName → ...
if (this.currentWorkspaceName != loadedWorkspaceName)
{
this.currentWorkspaceName = loadedWorkspaceName;
this.WorkspaceName(this.currentWorkspaceName);
}
// A newer sync request was started while awaiting IO. Ignore stale results.
if (syncVersion != this.workspaceHeaderSyncVersion)
return;
// The active chat changed while loading the workspace name.
if (this.ChatThread is null
|| this.ChatThread.ChatId != chatThreadId
|| this.ChatThread.WorkspaceId != workspaceId)
return;
this.currentChatThreadId = chatThreadId;
this.currentWorkspaceId = workspaceId;
this.PublishWorkspaceNameIfChanged(loadedWorkspaceName);
}
private void ClearWorkspaceHeaderState()
{
this.currentChatThreadId = Guid.Empty;
this.currentWorkspaceId = Guid.Empty;
this.PublishWorkspaceNameIfChanged(string.Empty);
}
private void PublishWorkspaceNameIfChanged(string workspaceName)
{
// Only notify the parent when the name actually changed to prevent
// an infinite render loop: WorkspaceName -> UpdateWorkspaceName ->
// StateHasChanged -> re-render -> OnParametersSetAsync -> WorkspaceName -> ...
if (this.currentWorkspaceName == workspaceName)
return;
this.currentWorkspaceName = workspaceName;
this.WorkspaceName(this.currentWorkspaceName);
}
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first");
@@ -747,10 +763,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// to reset the chat thread:
//
this.ChatThread = null;
this.currentChatThreadId = Guid.Empty;
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
this.ClearWorkspaceHeaderState();
}
else
{
@@ -826,10 +839,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.ChatThread!.WorkspaceId = workspaceId;
await this.SaveThread();
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
await this.SyncWorkspaceHeaderWithChatThreadAsync();
}
private async Task LoadedChatChanged()
@@ -840,18 +851,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if (this.ChatThread is not null)
{
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
this.currentChatThreadId = this.ChatThread.ChatId;
await this.SyncWorkspaceHeaderWithChatThreadAsync();
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
}
else
{
this.currentChatThreadId = Guid.Empty;
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
this.ClearWorkspaceHeaderState();
this.ApplyStandardDataSourceOptions();
}
@@ -870,11 +875,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
this.currentChatThreadId = Guid.Empty;
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
this.ClearWorkspaceHeaderState();
this.ChatThread = null;
this.ApplyStandardDataSourceOptions();
@@ -887,22 +888,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
var chatProfile = this.ChatThread?.SelectedProfile;
var chatChatTemplate = this.ChatThread?.SelectedChatTemplate;
switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)
{
default:
case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider);
break;
case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
break;
case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER:
if(this.Provider == AIStudio.Settings.Provider.NONE)
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
break;
}
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProvider);
await this.ProviderChanged.InvokeAsync(this.Provider);
@@ -0,0 +1,47 @@
@inherits MSGComponentBase
<MudStack Spacing="2">
<MudText Typo="Typo.body2">
@T("Version"): @this.Info.VersionText
</MudText>
@if (this.ShowAcceptanceMetadata)
{
@if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.MISSING)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true">
@T("This mandatory info has not been accepted yet.")
</MudAlert>
}
else if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.VERSION_CHANGED)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true">
@T("A new version of the terms is available. Please review it again.")
<br />
@T("Last accepted version"): @this.Acceptance!.AcceptedVersion
<br />
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
</MudAlert>
}
else if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.CONTENT_CHANGED)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true">
@T("Please review this text again. The content was changed.")
<br />
@T("Last accepted version"): @this.Acceptance!.AcceptedVersion
<br />
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
</MudAlert>
}
else
{
<MudAlert Severity="Severity.Success" Variant="Variant.Outlined" Dense="@true">
@T("Accepted version"): @this.Acceptance!.AcceptedVersion
<br />
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
</MudAlert>
}
}
<MudJustifiedMarkdown Value="@this.Info.Markdown" />
</MudStack>
@@ -0,0 +1,42 @@
using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class MandatoryInfoDisplay
{
private enum MandatoryInfoAcceptanceStatus
{
MISSING,
VERSION_CHANGED,
CONTENT_CHANGED,
ACCEPTED,
}
[Parameter]
public DataMandatoryInfo Info { get; set; } = new();
[Parameter]
public DataMandatoryInfoAcceptance? Acceptance { get; set; }
[Parameter]
public bool ShowAcceptanceMetadata { get; set; }
private MandatoryInfoAcceptanceStatus AcceptanceStatus
{
get
{
if (this.Acceptance is null)
return MandatoryInfoAcceptanceStatus.MISSING;
if (!string.Equals(this.Acceptance.AcceptedVersion, this.Info.VersionText, StringComparison.Ordinal))
return MandatoryInfoAcceptanceStatus.VERSION_CHANGED;
if (!string.Equals(this.Acceptance.AcceptedHash, this.Info.AcceptanceHash, StringComparison.Ordinal))
return MandatoryInfoAcceptanceStatus.CONTENT_CHANGED;
return MandatoryInfoAcceptanceStatus.ACCEPTED;
}
}
}
@@ -0,0 +1,3 @@
<div class="justified-markdown">
<MudMarkdown Value="@this.Value" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
</div>
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class MudJustifiedMarkdown
{
[Parameter]
public string Value { get; set; } = string.Empty;
}
@@ -6,7 +6,11 @@
<MudText Typo="Typo.body1" Class="mb-3">
@T("This Agent audits newly installed or updated external Plugin-Assistant for security risks before they are activated and stores the latest audit card until the plugin manifest changes.")
</MudText>
<ConfigurationOption OptionDescription="@T("Require a security audit before activating external Assistants?")" LabelOn="@T("External Assistants must be audited before activation")" LabelOff="@T("External Assistant can be activated without an audit")" State="@(() => this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation = updatedState)" />
<MudField Label="@T("Require a security audit before activating external Assistants?")" Variant="Variant.Outlined" Underline="false" Class="mb-6" InnerPadding="false">
<MudSwitch T="bool" Value="@this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation" ValueChanged="@this.RequireAuditBeforeActivationChanged" Color="Color.Primary">
@(this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation ? T("External Assistants must be audited before activation") : T("External Assistant can be activated without an audit"))
</MudSwitch>
</MudField>
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider = selectedValue)" HelpText="@(() => T("Optionally choose a dedicated provider for assistant plugin audits. When left empty, AI Studio falls back to the app-wide default provider."))" />
<ConfigurationSelect OptionDescription="@T("Minimum required audit level")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantPluginAudit.MinimumLevel)" Data="@ConfigurationSelectDataFactory.GetAssistantAuditLevelsData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AssistantPluginAudit.MinimumLevel = selectedValue)" OptionHelp="@T("External Assistants rated below this audit level are treated as insufficiently reviewed.")" />
<ConfigurationOption OptionDescription="@T("Block activation below the minimum Audit-Level?")" LabelOn="@T("Activation is blocked below the minimum Audit-Level")" LabelOff="@T("Users may still activate plugins below the minimum Audit-Level")" State="@(() => this.SettingsManager.ConfigurationData.AssistantPluginAudit.BlockActivationBelowMinimum)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AssistantPluginAudit.BlockActivationBelowMinimum = updatedState)"
@@ -1,3 +1,37 @@
using AIStudio.Dialogs;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components.Settings;
public partial class SettingsPanelAgentAssistantAudit : SettingsPanelBase;
public partial class SettingsPanelAgentAssistantAudit : SettingsPanelBase
{
private async Task RequireAuditBeforeActivationChanged(bool updatedState)
{
if (!updatedState)
{
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{
x => x.Message,
this.T("Disabling this setting turns off assistant plugin security audits. External assistants may then be activated and used even without a valid audit or after plugin changes. Do you really want to disable this protection?")
},
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(
this.T("Disable Assistant Audit Protection"),
dialogParameters,
DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
{
await this.InvokeAsync(this.StateHasChanged);
return;
}
}
this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation = updatedState;
await this.SettingsManager.StoreSettings();
await this.SendMessage<bool>(Event.CONFIGURATION_CHANGED);
await this.InvokeAsync(this.StateHasChanged);
}
}