Improved enterprise update policies & Flatpak support

This commit is contained in:
Thorsten Sommer 2026-07-12 21:49:26 +02:00
parent 5890b3734a
commit f96b8654e0
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
18 changed files with 278 additions and 56 deletions

View File

@ -3043,9 +3043,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1890416390"]
-- Which preview features would you like to enable?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"] = "Which preview features would you like to enable?"
-- This setting has no effect while updates are disabled by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898114759"] = "This setting has no effect while updates are disabled by your organization."
-- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
-- Your organization has disabled update checks and installations.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Your organization has disabled update checks and installations."
-- When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2013281167"] = "When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization."
@ -3076,6 +3082,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"]
-- Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2700836219"] = "Path to a PEM file containing one or more root CA certificates. For Flatpak deployments, this file must be placed in a location that is readable inside the sandbox."
-- Updates for the Flatpak version are managed outside AI Studio. Use your Flatpak software manager or command-line tools to update the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2771850773"] = "Updates for the Flatpak version are managed outside AI Studio. Use your Flatpak software manager or command-line tools to update the app."
-- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates."
@ -6286,6 +6295,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T25417398"] = "Update from v{0
-- Install later
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T2936430090"] = "Install later"
-- Show the latest release page
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEINSTRUCTIONSDIALOG::T2595806922"] = "Show the latest release page"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEINSTRUCTIONSDIALOG::T3448155331"] = "Close"
-- Create new workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1541251414"] = "Create new workspace"
@ -6661,6 +6676,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the confi
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
-- Updates are managed by your organization. Contact your IT department if you have questions about updating AI Studio.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1402243995"] = "Updates are managed by your organization. Contact your IT department if you have questions about updating AI Studio."
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library."
@ -6766,6 +6784,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration or
-- Configuration slot:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
-- AI Studio cannot update itself when installed as a Flatpak. A Flathub listing is planned. Until then, you can find the latest release on GitHub.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254976975"] = "AI Studio cannot update itself when installed as a Flatpak. A Flathub listing is planned. Until then, you can find the latest release on GitHub."
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."
@ -7018,6 +7039,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum is used to p
-- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose."
-- How to update
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "How to update"
-- Install Pandoc
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Install Pandoc"
@ -7423,6 +7447,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107
-- Toggle the sidebar: show the workspaces next to the chat when desired
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137"] = "Toggle the sidebar: show the workspaces next to the chat when desired"
-- Updates disabled by your organization
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4048970098"] = "Updates disabled by your organization"
-- Also show features in alpha: these are in development; expect bugs and missing features
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4146964761"] = "Also show features in alpha: these are in development; expect bugs and missing features"

View File

@ -1,6 +1,7 @@
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@using AIStudio.Tools.Rust
@using AIStudio.Tools.Services
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Apps" HeaderText="@T("App Options")">
@ -15,8 +16,17 @@
<ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/>
<ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/>
<ConfigurationSlider T="int" OptionDescription="@T("Request timeout")" Min="@ExternalHttpClientTimeout.MIN_HTTP_CLIENT_TIMEOUT_SECONDS" Max="@ExternalHttpClientTimeout.MAX_HTTP_CLIENT_TIMEOUT_SECONDS" Step="60" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds = updatedValue)" OptionHelp="@T("How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.HttpClientTimeoutSeconds, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetUpdateIntervalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@T("Should updates be installed automatically or manually?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked"/>
@if (this.updatePolicyMode is UpdatePolicyMode.FLATPAK)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
@T("Updates for the Flatpak version are managed outside AI Studio. Use your Flatpak software manager or command-line tools to update the app.")
</MudAlert>
}
else
{
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetManagedUpdateIntervalData(this.SettingsManager.ConfigurationData.App.UpdateInterval)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@this.UpdateIntervalHelp" IsLocked="@this.IsUpdateIntervalLocked"/>
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@this.UpdateInstallationHelp" IsLocked="@this.IsUpdateInstallationLocked"/>
}
<ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/>
<ConfigurationSelect OptionDescription="@T("Start page")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.StartPage)" Data="@ConfigurationSelectDataFactory.GetStartPageData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.StartPage = selectedValue)" OptionHelp="@this.GetStartPageHelpText()" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.StartPage, out var meta) && meta.IsLocked"/>
<ConfigurationOption OptionDescription="@T("Show administration settings?")" LabelOn="@T("Administration settings are visible")" LabelOff="@T("Administration settings are not visible")" State="@(() => this.SettingsManager.ConfigurationData.App.ShowAdminSettings)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ShowAdminSettings = updatedState)" OptionHelp="@T("When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShowAdminSettings, out var meta) && meta.IsLocked"/>

View File

@ -2,11 +2,48 @@ using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Settings;
public partial class SettingsPanelApp : SettingsPanelBase
{
[Inject]
private UpdatePolicy UpdatePolicy { get; init; } = null!;
private UpdatePolicyMode updatePolicyMode;
private string UpdateIntervalHelp => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED
? T("Your organization has disabled update checks and installations.")
: T("How often should we check for app updates?");
private string UpdateInstallationHelp => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED
? T("This setting has no effect while updates are disabled by your organization.")
: T("Should updates be installed automatically or manually?");
private bool IsUpdateIntervalLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED ||
ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked;
private bool IsUpdateInstallationLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED ||
ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked;
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
this.updatePolicyMode = this.UpdatePolicy.CurrentMode;
}
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED)
this.updatePolicyMode = this.UpdatePolicy.CurrentMode;
await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
}
private ConfigurationShortcutData VoiceRecordingShortcut => new()
{
Id = Shortcut.VOICE_RECORDING_TOGGLE,
@ -121,4 +158,4 @@ public partial class SettingsPanelApp : SettingsPanelBase
this.SettingsManager.ConfigurationData.App.LanguagePluginId = pluginId;
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
}
}
}

View File

@ -0,0 +1,19 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1">
@this.Message
</MudJustifiedText>
</DialogContent>
<DialogActions>
@if (!string.IsNullOrWhiteSpace(this.ReleaseUrl))
{
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.ReleaseUrl" Target="_blank">
@T("Show the latest release page")
</MudButton>
}
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>

View File

@ -0,0 +1,19 @@
using AIStudio.Components;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class UpdateInstructionsDialog : MSGComponentBase
{
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public string Message { get; set; } = string.Empty;
[Parameter]
public string? ReleaseUrl { get; set; }
private void Close() => this.MudDialog.Close();
}

View File

@ -29,6 +29,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private UpdatePolicy UpdatePolicy { get; init; } = null!;
[Inject]
private AIJobService AIJobService { get; init; } = null!;
@ -186,6 +189,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
switch (triggeredEvent)
{
case Event.INSTALL_UPDATE:
if (!this.UpdatePolicy.AllowsInstallations)
break;
this.performingUpdate = true;
this.StateHasChanged();
break;
@ -371,6 +376,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
private async Task ShowUpdateDialog()
{
if (!this.UpdatePolicy.AllowsInstallations)
return;
if(this.currentUpdateResponse is null)
return;
@ -397,6 +405,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
if (!this.UpdatePolicy.AllowsInstallations)
return;
this.performingUpdate = true;
this.StateHasChanged();

View File

@ -1,5 +1,6 @@
@attribute [Route(Routes.ABOUT)]
@using AIStudio.Tools.PluginSystem
@using AIStudio.Tools.Services
@inherits MSGComponentBase
<div class="inner-scrolling-context">
@ -53,7 +54,7 @@
<MudText Typo="Typo.body1">
@this.WorkingDirectory
</MudText>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the working directory to the clipboard"))" StringContent="@this.runtimeInfo.WorkingDirectory"/>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the working directory to the clipboard"))" StringContent="@this.RuntimeInfo.WorkingDirectory"/>
</div>
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.InsertDriveFile">
@ -61,7 +62,7 @@
<MudText Typo="Typo.body1">
@this.ExecutablePath
</MudText>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the executable path to the clipboard"))" StringContent="@this.runtimeInfo.ExecutablePath"/>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the executable path to the clipboard"))" StringContent="@this.RuntimeInfo.ExecutablePath"/>
</div>
</MudListItem>
@if (OperatingSystem.IsLinux())
@ -193,7 +194,7 @@
</MudList>
<MudStack Row="true">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="@this.CheckForUpdate">
@T("Check for updates")
@(this.updatePolicyMode is UpdatePolicyMode.SELF_UPDATE ? T("Check for updates") : T("How to update"))
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Default" StartIcon="@Icons.Material.Filled.Download" OnClick="@(async () => await this.ShowPandocDialog())">
@this.PandocButtonText

View File

@ -28,6 +28,12 @@ public partial class Information : MSGComponentBase
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private UpdatePolicy UpdatePolicy { get; init; } = null!;
[Inject]
private RuntimeInfoResponse RuntimeInfo { get; init; }
[Inject]
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
@ -42,7 +48,7 @@ public partial class Information : MSGComponentBase
private string osLanguage = string.Empty;
private string osUserName = string.Empty;
private RuntimeInfoResponse runtimeInfo;
private UpdatePolicyMode updatePolicyMode;
private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum}, {META_DATA_ARCH.Architecture.ToRID().ToUserFriendlyName()})";
@ -54,13 +60,13 @@ public partial class Information : MSGComponentBase
private string OSUserName => $"{T("Username provided by the OS")}: '{this.osUserName}'";
private string WorkingDirectory => $"{T("Working directory")}: {this.runtimeInfo.WorkingDirectory}";
private string WorkingDirectory => $"{T("Working directory")}: {this.RuntimeInfo.WorkingDirectory}";
private string ExecutablePath => $"{T("Executable path")}: {this.runtimeInfo.ExecutablePath}";
private string ExecutablePath => $"{T("Executable path")}: {this.RuntimeInfo.ExecutablePath}";
private string LinuxPackageType => $"{T("Linux package")}: {this.LinuxPackageTypeDisplayName}";
private string LinuxPackageTypeDisplayName => this.runtimeInfo.LinuxPackageType switch
private string LinuxPackageTypeDisplayName => this.RuntimeInfo.LinuxPackageType switch
{
"appimage" => "AppImage",
"flatpak" => "Flatpak",
@ -162,7 +168,7 @@ public partial class Information : MSGComponentBase
this.osLanguage = await this.RustService.ReadUserLanguage();
this.osUserName = await this.RustService.ReadUserName();
this.runtimeInfo = await this.RustService.GetRuntimeInfo();
this.updatePolicyMode = this.UpdatePolicy.CurrentMode;
this.logPaths = await this.RustService.GetLogPaths();
await this.RefreshVectorStoreInfo(CancellationToken.None);
@ -185,6 +191,7 @@ public partial class Information : MSGComponentBase
case Event.PLUGINS_RELOADED:
case Event.ENTERPRISE_ENVIRONMENTS_CHANGED:
case Event.CONFIGURATION_CHANGED:
this.updatePolicyMode = this.UpdatePolicy.CurrentMode;
this.RefreshEnterpriseConfigurationState();
await this.InvokeAsync(this.StateHasChanged);
break;
@ -604,6 +611,26 @@ public partial class Information : MSGComponentBase
private async Task CheckForUpdate()
{
await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
this.updatePolicyMode = this.UpdatePolicy.CurrentMode;
if (this.updatePolicyMode is UpdatePolicyMode.SELF_UPDATE)
{
await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
return;
}
var parameters = new DialogParameters<UpdateInstructionsDialog>();
if (this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED)
{
parameters.Add(x => x.Message, T("Updates are managed by your organization. Contact your IT department if you have questions about updating AI Studio."));
}
else if (this.updatePolicyMode is UpdatePolicyMode.FLATPAK)
{
parameters.Add(x => x.Message, T("AI Studio cannot update itself when installed as a Flatpak. A Flathub listing is planned. Until then, you can find the latest release on GitHub."));
parameters.Add(x => x.ReleaseUrl, "https://github.com/MindWorkAI/AI-Studio/releases/latest");
}
else
return;
await this.DialogService.ShowAsync<UpdateInstructionsDialog>(T("How to update"), parameters, DialogOptions.FULLSCREEN);
}
}

View File

@ -200,7 +200,10 @@ CONFIG["DATA_SOURCES"] = {}
CONFIG["SETTINGS"] = {}
-- Configure the update check interval:
-- Allowed values are: NO_CHECK, ONCE_STARTUP, HOURLY, DAILY, WEEKLY
-- Allowed values are: NO_CHECK, DISABLE_UPDATES, ONCE_STARTUP, HOURLY, DAILY, WEEKLY
-- NO_CHECK disables automatic checks, but users can still check and install updates manually.
-- DISABLE_UPDATES is intended for enterprise configurations and disables all update checks
-- and installations. It is not offered as a selectable option in the normal app settings.
-- CONFIG["SETTINGS"]["DataApp.UpdateInterval"] = "NO_CHECK"
-- Configure how updates are installed:

View File

@ -6,6 +6,7 @@ using AIStudio.Tools.AIJobs;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.PluginSystem.Assistants;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
@ -88,6 +89,7 @@ internal sealed class Program
return;
}
var runtimeInfo = await rust.GetRuntimeInfo();
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
{
@ -127,6 +129,7 @@ internal sealed class Program
builder.Services.AddSingleton(new MudTheme());
builder.Services.AddSingleton(MessageBus.INSTANCE);
builder.Services.AddSingleton(rust);
builder.Services.AddSingleton(typeof(RuntimeInfoResponse), runtimeInfo);
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>();
@ -134,6 +137,7 @@ internal sealed class Program
builder.Services.AddSingleton<AssistantSessionService>();
builder.Services.AddSingleton<VoiceRecordingAvailabilityService>();
builder.Services.AddSingleton<AssistantPluginInstallService>();
builder.Services.AddSingleton<UpdatePolicy>();
builder.Services.AddSingleton<DataSourceService>();
builder.Services.AddScoped<PandocAvailabilityService>();
builder.Services.AddTransient<HTMLParser>();

View File

@ -74,6 +74,15 @@ public static class ConfigurationSelectDataFactory
yield return new (TB("Check every week"), UpdateInterval.WEEKLY);
}
public static IEnumerable<ConfigurationSelectData<UpdateInterval>> GetManagedUpdateIntervalData(UpdateInterval currentValue)
{
if (currentValue is UpdateInterval.DISABLE_UPDATES)
yield return new(TB("Updates disabled by your organization"), UpdateInterval.DISABLE_UPDATES);
foreach (var option in GetUpdateIntervalData())
yield return option;
}
public static IEnumerable<ConfigurationSelectData<UpdateInstallation>> GetUpdateBehaviourData()
{
yield return new(TB("Install updates manually"), UpdateInstallation.MANUAL);

View File

@ -3,6 +3,7 @@ namespace AIStudio.Settings.DataModel;
public enum UpdateInterval
{
NO_CHECK,
DISABLE_UPDATES,
ONCE_STARTUP,
HOURLY,
DAILY,

View File

@ -0,0 +1,23 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Services;
public sealed class UpdatePolicy(SettingsManager settingsManager, RuntimeInfoResponse runtimeInfo)
{
public UpdatePolicyMode CurrentMode => settingsManager.ConfigurationData.App.UpdateInterval is UpdateInterval.DISABLE_UPDATES
? UpdatePolicyMode.ENTERPRISE_DISABLED
: runtimeInfo.LinuxPackageType switch
{
"flatpak" => UpdatePolicyMode.FLATPAK,
_ => UpdatePolicyMode.SELF_UPDATE
};
public bool AllowsManualChecks => this.CurrentMode is UpdatePolicyMode.SELF_UPDATE;
public bool AllowsAutomaticChecks => this.CurrentMode is UpdatePolicyMode.SELF_UPDATE &&
settingsManager.ConfigurationData.App.UpdateInterval is not UpdateInterval.NO_CHECK;
public bool AllowsInstallations => this.CurrentMode is UpdatePolicyMode.SELF_UPDATE;
}

View File

@ -0,0 +1,8 @@
namespace AIStudio.Tools.Services;
public enum UpdatePolicyMode
{
SELF_UPDATE,
FLATPAK,
ENTERPRISE_DISABLED
}

View File

@ -16,15 +16,15 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
private readonly SettingsManager settingsManager;
private readonly MessageBus messageBus;
private readonly RustService rust;
private readonly UpdatePolicy updatePolicy;
private readonly ILogger<UpdateService> logger;
private TimeSpan updateInterval;
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust, ILogger<UpdateService> logger)
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust, UpdatePolicy updatePolicy, ILogger<UpdateService> logger)
{
this.settingsManager = settingsManager;
this.messageBus = messageBus;
this.rust = rust;
this.updatePolicy = updatePolicy;
this.logger = logger;
this.messageBus.RegisterComponent(this);
@ -41,42 +41,21 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
//
// Set the update interval based on the user's settings.
//
this.updateInterval = this.settingsManager.ConfigurationData.App.UpdateInterval switch
{
UpdateInterval.NO_CHECK => Timeout.InfiniteTimeSpan,
UpdateInterval.ONCE_STARTUP => Timeout.InfiniteTimeSpan,
UpdateInterval.HOURLY => TimeSpan.FromHours(1),
UpdateInterval.DAILY => TimeSpan.FromDays(1),
UpdateInterval.WEEKLY => TimeSpan.FromDays(7),
_ => TimeSpan.FromHours(1)
};
//
// When the user doesn't want to check for updates, we can
// return early.
//
if(this.settingsManager.ConfigurationData.App.UpdateInterval is UpdateInterval.NO_CHECK)
return;
//
// Check for updates at the beginning. The user aspects this when the app
// is started.
//
await this.CheckForUpdate();
//
// Start the update loop. This will check for updates based on the
// user's settings.
//
DateTimeOffset? lastAutomaticCheck = null;
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(this.updateInterval, stoppingToken);
await this.CheckForUpdate();
var interval = this.GetCurrentUpdateInterval();
if (this.updatePolicy.AllowsAutomaticChecks &&
(
lastAutomaticCheck is null ||
interval != Timeout.InfiniteTimeSpan && DateTimeOffset.UtcNow - lastAutomaticCheck >= interval)
)
{
await this.CheckForUpdate();
lastAutomaticCheck = DateTimeOffset.UtcNow;
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
@ -97,7 +76,8 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
switch (triggeredEvent)
{
case Event.USER_SEARCH_FOR_UPDATE:
await this.CheckForUpdate(notifyUserWhenNoUpdate: true);
if (this.updatePolicy.AllowsManualChecks)
await this.CheckForUpdate(notifyUserWhenNoUpdate: true);
break;
}
}
@ -143,6 +123,9 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
if (!isDevEnvironment && this.settingsManager.ConfigurationData.App.UpdateInstallation is UpdateInstallation.AUTOMATIC)
{
if (!this.updatePolicy.AllowsInstallations)
return;
try
{
await this.messageBus.SendMessage<bool>(null, Event.INSTALL_UPDATE);
@ -174,6 +157,16 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
}
}
}
private TimeSpan GetCurrentUpdateInterval() => this.settingsManager.ConfigurationData.App.UpdateInterval switch
{
UpdateInterval.ONCE_STARTUP => Timeout.InfiniteTimeSpan,
UpdateInterval.HOURLY => TimeSpan.FromHours(1),
UpdateInterval.DAILY => TimeSpan.FromDays(1),
UpdateInterval.WEEKLY => TimeSpan.FromDays(7),
_ => Timeout.InfiniteTimeSpan
};
public static void SetBlazorDependencies(ISnackbar snackbar)
{

View File

@ -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.
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
- 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.

View File

@ -4,7 +4,7 @@
Do you want to manage MindWork AI Studio in a corporate environment or within an organization? This documentation explains what you need to do and how it works. First, here's an overview of the entire process:
- You can distribute MindWork AI Studio to employees' devices using tools like Microsoft System Center Configuration Manager (SCCM).
- Employees can get updates through the built-in update feature. If you want, you can disable automatic updates and control which version gets distributed.
- Employees can get updates through the built-in update feature. Enterprise configuration can disable automatic checks or the entire built-in update feature so that the IT department controls which version gets distributed.
- AI Studio checks about every 16 minutes to see where and which configuration it should load. This information is loaded from the local system. On Windows, you might use the registry, for example.
- If it finds the necessary metadata, AI Studio downloads the configuration as a ZIP file from the specified server.
- The configuration is an AI Studio plugin written in Lua.
@ -12,6 +12,15 @@ Do you want to manage MindWork AI Studio in a corporate environment or within an
AI Studio checks about every 16 minutes to see if the configuration ID, the server for the configuration, or the configuration itself has changed. If it finds any changes, it loads the updated configuration from the server and applies it right away.
### Manage app updates
Set `CONFIG["SETTINGS"]["DataApp.UpdateInterval"]` in the configuration plugin to control update checks:
- `NO_CHECK` disables automatic update checks. Users can still check for and install updates manually.
- `DISABLE_UPDATES` disables automatic and manual update checks and installations. AI Studio tells users that updates are managed by their organization and directs questions to their IT department. This policy takes effect immediately when the enterprise configuration changes.
Use `DISABLE_UPDATES` when your organization distributes approved versions through its own software-management process.
## Configure the devices
So that MindWork AI Studio knows where to load which configuration, this information must be provided as metadata on employees' devices. Currently, the following options are available:

View File

@ -451,8 +451,9 @@ pub async fn change_location_to(url: &str) {
/// Checks for updates.
pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
if is_dev() {
warn!(Source = "Updater"; "The app is running in development mode; skipping update check.");
if !self_update_allowed(is_dev(), is_flatpak()) {
let reason = if is_flatpak() { "Flatpak installations are updated externally" } else { "the app is running in development mode" };
warn!(Source = "Updater"; "Skipping update check because {reason}.");
return Json(CheckUpdateResponse {
update_is_available: false,
error: false,
@ -536,8 +537,9 @@ pub struct CheckUpdateResponse {
/// Installs the update.
pub async fn install_update(_token: APIToken) {
if is_dev() {
warn!(Source = "Updater"; "The app is running in development mode; skipping update installation.");
if !self_update_allowed(is_dev(), is_flatpak()) {
let reason = if is_flatpak() { "Flatpak installations are updated externally" } else { "the app is running in development mode" };
warn!(Source = "Updater"; "Skipping update installation because {reason}.");
return;
}
@ -595,6 +597,10 @@ pub async fn install_update(_token: APIToken) {
}
}
fn self_update_allowed(development: bool, flatpak: bool) -> bool {
!development && !flatpak
}
/// Request payload for registering a global shortcut.
#[derive(Clone, Deserialize)]
pub struct RegisterShortcutRequest {
@ -1027,6 +1033,20 @@ mod tests {
use super::*;
use std::fs;
#[test]
fn self_update_is_disabled_in_development() {
assert!(!self_update_allowed(true, false));
}
#[test]
fn self_update_is_disabled_for_flatpak() {
assert!(!self_update_allowed(false, true));
}
#[test]
fn self_update_is_enabled_for_normal_production_installations() {
assert!(self_update_allowed(false, false));
}
#[test]
fn pdfium_library_directory_prefers_resources_libraries() {
let temp_dir = tempfile::tempdir().unwrap();