mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-15 19:02:10 +00:00
Fixed AI Studio installing a second copy of itself next to an existing installation (#922)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
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) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
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) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
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) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
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) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
parent
a26c4d93a4
commit
57c0b59fa5
@ -4078,6 +4078,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"]
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds"
|
||||
|
||||
-- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own."
|
||||
|
||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
||||
|
||||
@ -4138,6 +4141,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"]
|
||||
-- Save energy?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?"
|
||||
|
||||
-- AI Studio cannot install updates into this installation. Your IT department provides new versions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio cannot install updates into this installation. Your IT department provides new versions."
|
||||
|
||||
-- Development builds do not install updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Development builds do not install updates."
|
||||
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||
|
||||
@ -4180,6 +4189,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"]
|
||||
-- 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.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "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."
|
||||
|
||||
-- AI Studio cannot install updates into its current location. Install new versions yourself.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio cannot install updates into its current location. Install new versions yourself."
|
||||
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||
|
||||
@ -4189,6 +4201,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"]
|
||||
-- Use additional root certificates for external HTTPS requests?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Use additional root certificates for external HTTPS requests?"
|
||||
|
||||
-- AI Studio cannot update itself from its current location, so it does not check for updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] = "AI Studio cannot update itself from its current location, so it does not check for updates."
|
||||
|
||||
-- Select a root certificate bundle
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle"
|
||||
|
||||
@ -4207,6 +4222,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] =
|
||||
-- Energy saving is disabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energy saving is disabled"
|
||||
|
||||
-- Development builds do not check for updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T735114866"] = "Development builds do not check for updates."
|
||||
|
||||
-- Start page
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page"
|
||||
|
||||
@ -8227,6 +8245,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a privat
|
||||
-- Copies the configuration origin to the clipboard
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
|
||||
|
||||
-- Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1289059917"] = "Installation"
|
||||
|
||||
-- Unknown configuration plugin
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
|
||||
|
||||
@ -8269,6 +8290,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create
|
||||
-- Plugin directory:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:"
|
||||
|
||||
-- in a location AI Studio cannot update itself from
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "in a location AI Studio cannot update itself from"
|
||||
|
||||
-- Consent:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
|
||||
|
||||
@ -8278,6 +8302,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu
|
||||
-- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."
|
||||
|
||||
-- owned by you, self-updates possible
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "owned by you, self-updates possible"
|
||||
|
||||
-- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents."
|
||||
|
||||
@ -8311,6 +8338,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo
|
||||
-- Copies the server URL to the clipboard
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
|
||||
|
||||
-- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation."
|
||||
|
||||
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others."
|
||||
|
||||
@ -8359,6 +8389,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc
|
||||
-- Configuration origin:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
|
||||
|
||||
-- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio."
|
||||
|
||||
-- Configuration slot:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
|
||||
|
||||
@ -8449,6 +8482,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store"
|
||||
-- Enterprise configuration ID:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:"
|
||||
|
||||
-- managed by someone else, no self-updates
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "managed by someone else, no self-updates"
|
||||
|
||||
-- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI).
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)."
|
||||
|
||||
@ -8503,6 +8539,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compil
|
||||
-- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management."
|
||||
|
||||
-- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead."
|
||||
|
||||
-- development build, no self-updates
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "development build, no self-updates"
|
||||
|
||||
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"
|
||||
|
||||
|
||||
@ -15,11 +15,13 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
||||
|
||||
private UpdatePolicyMode updatePolicyMode;
|
||||
|
||||
private UpdateInterval DisplayedUpdateInterval => this.updatePolicyMode is UpdatePolicyMode.FLATPAK
|
||||
private bool CannotUpdateItself => this.updatePolicyMode is UpdatePolicyMode.FLATPAK or UpdatePolicyMode.MANAGED_INSTALLATION or UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION or UpdatePolicyMode.DEVELOPMENT;
|
||||
|
||||
private UpdateInterval DisplayedUpdateInterval => this.CannotUpdateItself
|
||||
? UpdateInterval.NO_CHECK
|
||||
: this.SettingsManager.ConfigurationData.App.UpdateInterval;
|
||||
|
||||
private UpdateInstallation DisplayedUpdateInstallation => this.updatePolicyMode is UpdatePolicyMode.FLATPAK
|
||||
private UpdateInstallation DisplayedUpdateInstallation => this.CannotUpdateItself
|
||||
? UpdateInstallation.MANUAL
|
||||
: this.SettingsManager.ConfigurationData.App.UpdateInstallation;
|
||||
|
||||
@ -27,6 +29,9 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
||||
{
|
||||
UpdatePolicyMode.ENTERPRISE_DISABLED => T("Your organization has disabled update checks and installations."),
|
||||
UpdatePolicyMode.FLATPAK => T("AI Studio cannot check for updates when running as a Flatpak. Updates are managed outside the app."),
|
||||
UpdatePolicyMode.MANAGED_INSTALLATION => T("This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own."),
|
||||
UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION => T("AI Studio cannot update itself from its current location, so it does not check for updates."),
|
||||
UpdatePolicyMode.DEVELOPMENT => T("Development builds do not check for updates."),
|
||||
_ => T("How often should we check for app updates?")
|
||||
};
|
||||
|
||||
@ -34,13 +39,16 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
||||
{
|
||||
UpdatePolicyMode.ENTERPRISE_DISABLED => T("This setting has no effect while updates are disabled by your organization."),
|
||||
UpdatePolicyMode.FLATPAK => T("AI Studio cannot install updates when running as a Flatpak. Use the update method provided by your Flatpak distribution."),
|
||||
UpdatePolicyMode.MANAGED_INSTALLATION => T("AI Studio cannot install updates into this installation. Your IT department provides new versions."),
|
||||
UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION => T("AI Studio cannot install updates into its current location. Install new versions yourself."),
|
||||
UpdatePolicyMode.DEVELOPMENT => T("Development builds do not install updates."),
|
||||
_ => T("Should updates be installed automatically or manually?")
|
||||
};
|
||||
|
||||
private bool IsUpdateIntervalLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED or UpdatePolicyMode.FLATPAK ||
|
||||
private bool IsUpdateIntervalLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED || this.CannotUpdateItself ||
|
||||
ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked;
|
||||
|
||||
private bool IsUpdateInstallationLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED or UpdatePolicyMode.FLATPAK ||
|
||||
private bool IsUpdateInstallationLocked() => this.updatePolicyMode is UpdatePolicyMode.ENTERPRISE_DISABLED || this.CannotUpdateItself ||
|
||||
ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
|
||||
@ -69,6 +69,7 @@
|
||||
{
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage" Text="@this.LinuxPackageType"/>
|
||||
}
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.InstallDesktop" Text="@this.InstallationKind"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
|
||||
@switch (HasAnyActiveEnvironment)
|
||||
{
|
||||
|
||||
@ -71,6 +71,16 @@ public partial class Information : MSGComponentBase
|
||||
_ => T("not applicable")
|
||||
};
|
||||
|
||||
private string InstallationKind => $"{T("Installation")}: {this.InstallationKindDisplayName}";
|
||||
|
||||
private string InstallationKindDisplayName => this.RuntimeInfo.InstallationKind switch
|
||||
{
|
||||
Tools.Rust.InstallationKind.MANAGED => T("managed by someone else, no self-updates"),
|
||||
Tools.Rust.InstallationKind.UNSUPPORTED_LOCATION => T("in a location AI Studio cannot update itself from"),
|
||||
Tools.Rust.InstallationKind.DEVELOPMENT => T("development build, no self-updates"),
|
||||
_ => T("owned by you, self-updates possible")
|
||||
};
|
||||
|
||||
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
|
||||
|
||||
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
|
||||
@ -655,6 +665,21 @@ public partial class Information : MSGComponentBase
|
||||
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 if (this.updatePolicyMode is UpdatePolicyMode.MANAGED_INSTALLATION)
|
||||
{
|
||||
// No release link here: this installation was deployed centrally, and installing a
|
||||
// second copy from GitHub next to it is exactly what we want to avoid.
|
||||
parameters.Add(x => x.Message, T("This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio."));
|
||||
}
|
||||
else if (this.updatePolicyMode is UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION)
|
||||
{
|
||||
parameters.Add(x => x.Message, T("AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation."));
|
||||
parameters.Add(x => x.ReleaseUrl, "https://github.com/MindWorkAI/AI-Studio/releases/latest");
|
||||
}
|
||||
else if (this.updatePolicyMode is UpdatePolicyMode.DEVELOPMENT)
|
||||
{
|
||||
parameters.Add(x => x.Message, T("You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead."));
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
|
||||
@ -271,6 +271,14 @@ CONFIG["SETTINGS"] = {}
|
||||
-- of that, their choice outlives your configuration and stays as it is.
|
||||
-- ------
|
||||
|
||||
-- Both update settings below only ever apply to installations AI Studio is able to update.
|
||||
-- Installations you rolled out yourself, for example, into C:\Program Files or into a location
|
||||
-- your users cannot write to, never update themselves, no matter what you configure here. You
|
||||
-- can therefore leave automatic updates enabled for everybody: your deployments ignore them,
|
||||
-- while installations your colleagues fetched from GitHub keep updating themselves. Place a file
|
||||
-- named "managed-installation" next to the program file to mark any other installation as one
|
||||
-- you maintain. The Enterprise IT documentation describes this in detail.
|
||||
|
||||
-- Configure the update check interval:
|
||||
-- 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.
|
||||
|
||||
@ -4080,6 +4080,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"]
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "Sekunden"
|
||||
|
||||
-- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "Diese Kopie von AI Studio wurde von einer anderen Person für Sie installiert und sucht daher nicht selbstständig nach Updates."
|
||||
|
||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Wählen Sie für die Transkription Ihrer Stimme einen Anbieter für Transkriptionen aus. Ohne einen ausgewählten Anbieter wird die Diktier- und Transkriptions-Funktion deaktiviert."
|
||||
|
||||
@ -4140,6 +4143,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"]
|
||||
-- Save energy?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?"
|
||||
|
||||
-- AI Studio cannot install updates into this installation. Your IT department provides new versions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio kann in dieser Installation keine Updates installieren. Ihre IT-Abteilung stellt neue Versionen bereit."
|
||||
|
||||
-- Development builds do not install updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Entwicklerversionen installieren keine Updates."
|
||||
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Rechtschreibprüfung ist aktiviert"
|
||||
|
||||
@ -4182,6 +4191,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"]
|
||||
-- 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.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "Wenn aktiviert, ist die Rechtschreibprüfung in allen Eingabefeldern aktiv. Je nach Betriebssystem werden Fehler möglicherweise nicht visuell hervorgehoben, aber ein Rechtsklick kann dennoch Korrekturvorschläge anzeigen."
|
||||
|
||||
-- AI Studio cannot install updates into its current location. Install new versions yourself.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio kann Updates nicht am aktuellen Speicherort installieren. Installieren Sie neue Versionen selbst."
|
||||
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Wählen Sie einen Transkriptionsanbieter aus"
|
||||
|
||||
@ -4191,6 +4203,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"]
|
||||
-- Use additional root certificates for external HTTPS requests?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Zusätzliche Stammzertifikate für externe HTTPS-Anfragen verwenden?"
|
||||
|
||||
-- AI Studio cannot update itself from its current location, so it does not check for updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] = "AI Studio kann sich an seinem aktuellen Speicherort nicht selbst aktualisieren und sucht daher nicht nach Updates."
|
||||
|
||||
-- Select a root certificate bundle
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Wählen Sie ein Stammzertifikat-Bundle aus"
|
||||
|
||||
@ -4209,6 +4224,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] =
|
||||
-- Energy saving is disabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energiesparmodus ist deaktiviert"
|
||||
|
||||
-- Development builds do not check for updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T735114866"] = "Entwicklerversionen suchen nicht nach Updates."
|
||||
|
||||
-- Start page
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Startseite"
|
||||
|
||||
@ -8229,6 +8247,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine pr
|
||||
-- Copies the configuration origin to the clipboard
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Kopiert den Ursprung der Konfiguration in die Zwischenablage"
|
||||
|
||||
-- Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1289059917"] = "Installation"
|
||||
|
||||
-- Unknown configuration plugin
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unbekanntes Konfigurations-Plugin"
|
||||
|
||||
@ -8271,6 +8292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstel
|
||||
-- Plugin directory:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin-Verzeichnis:"
|
||||
|
||||
-- in a location AI Studio cannot update itself from
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "an einem Speicherort, von dem aus AI Studio keine Updates durchführen kann"
|
||||
|
||||
-- Consent:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:"
|
||||
|
||||
@ -8280,6 +8304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Kopiert den Pfad
|
||||
-- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "Diese Bibliothek wird verwendet, um die Unterschiede zwischen zwei Texten anzuzeigen. Das ist zum Beispiel für den Grammatik- und Rechtschreibassistenten notwendig."
|
||||
|
||||
-- owned by you, self-updates possible
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "von Ihnen verwaltet, Updates sind möglich"
|
||||
|
||||
-- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "Wenn Sie auf den jeweiligen Pfad klicken, wird dieser in die Zwischenablage kopiert. Sie können diese Dateien mit einem Texteditor öffnen, um ihren Inhalt anzusehen."
|
||||
|
||||
@ -8313,6 +8340,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgende
|
||||
-- Copies the server URL to the clipboard
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage"
|
||||
|
||||
-- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio kann sich von seinem aktuellen Speicherort aus nicht selbst aktualisieren. Durch die Installation eines Updates würde eine zweite Installation entstehen, anstatt diese zu ersetzen. Um eine neue Version zu erhalten, laden Sie die aktuelle Version herunter und installieren Sie sie über Ihre bestehende Installation."
|
||||
|
||||
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "Das Projekt windows-rs ermöglicht den Zugriff auf Windows-APIs aus Rust. Wir verwenden mehrere Bibliotheken aus diesem Projekt: windows-registry wird verwendet, um die gewünschte Konfiguration in Windows-Unternehmensumgebungen auszulesen. Die Bibliotheken windows und windows-collections werden verwendet, um den nativen Windows-Dialog zum Teilen zu öffnen, zum Beispiel wenn Sie ein Plugin mit anderen teilen."
|
||||
|
||||
@ -8361,6 +8391,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installierte Pan
|
||||
-- Configuration origin:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Ursprung der Konfiguration:"
|
||||
|
||||
-- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "Diese Kopie von AI Studio wurde von einer anderen Person für Sie installiert, höchstwahrscheinlich von Ihrer IT-Abteilung. Daher kann sie sich nicht selbst aktualisieren. Wenden Sie sich an Ihre IT-Abteilung, wenn Sie Fragen zur Aktualisierung von AI Studio haben."
|
||||
|
||||
-- Configuration slot:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Slot der Konfiguration:"
|
||||
|
||||
@ -8451,6 +8484,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vektordatenbank"
|
||||
-- Enterprise configuration ID:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Unternehmenskonfigurations-ID:"
|
||||
|
||||
-- managed by someone else, no self-updates
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "von jemand anderem verwaltet, keine Selbstaktualisierung"
|
||||
|
||||
-- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI).
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Verbinden Sie AI Studio mit den Daten ihrer Organisation über unsere Schnittstelle für externe Datenabfrage (ERI)."
|
||||
|
||||
@ -8505,6 +8541,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Verwendeter Rust
|
||||
-- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird mit Unternehmenskonfigurationen unter Verwendung von Konfigurations-Plugins betrieben. Eine zentrale Konfigurationsverwaltung wird nicht eingesetzt."
|
||||
|
||||
-- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "Du verwendest eine Entwicklerversion von AI Studio, die sich niemals selbst aktualisiert. Hole stattdessen die neuesten Änderungen und erstelle die App neu."
|
||||
|
||||
-- development build, no self-updates
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "Entwicklerversion, keine automatischen Updates"
|
||||
|
||||
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!"
|
||||
|
||||
|
||||
@ -4080,6 +4080,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"]
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds"
|
||||
|
||||
-- This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T176917293"] = "This copy of AI Studio was installed for you by someone else, so it does not check for updates on its own."
|
||||
|
||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
||||
|
||||
@ -4140,6 +4143,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"]
|
||||
-- Save energy?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?"
|
||||
|
||||
-- AI Studio cannot install updates into this installation. Your IT department provides new versions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3125872224"] = "AI Studio cannot install updates into this installation. Your IT department provides new versions."
|
||||
|
||||
-- Development builds do not install updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"] = "Development builds do not install updates."
|
||||
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||
|
||||
@ -4182,6 +4191,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"]
|
||||
-- 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.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "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."
|
||||
|
||||
-- AI Studio cannot install updates into its current location. Install new versions yourself.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T408056494"] = "AI Studio cannot install updates into its current location. Install new versions yourself."
|
||||
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||
|
||||
@ -4191,6 +4203,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"]
|
||||
-- Use additional root certificates for external HTTPS requests?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4235562267"] = "Use additional root certificates for external HTTPS requests?"
|
||||
|
||||
-- AI Studio cannot update itself from its current location, so it does not check for updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4258440666"] = "AI Studio cannot update itself from its current location, so it does not check for updates."
|
||||
|
||||
-- Select a root certificate bundle
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"] = "Select a root certificate bundle"
|
||||
|
||||
@ -4209,6 +4224,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] =
|
||||
-- Energy saving is disabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energy saving is disabled"
|
||||
|
||||
-- Development builds do not check for updates.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T735114866"] = "Development builds do not check for updates."
|
||||
|
||||
-- Start page
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page"
|
||||
|
||||
@ -8229,6 +8247,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a privat
|
||||
-- Copies the configuration origin to the clipboard
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
|
||||
|
||||
-- Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1289059917"] = "Installation"
|
||||
|
||||
-- Unknown configuration plugin
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
|
||||
|
||||
@ -8271,6 +8292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create
|
||||
-- Plugin directory:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:"
|
||||
|
||||
-- in a location AI Studio cannot update itself from
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1711792389"] = "in a location AI Studio cannot update itself from"
|
||||
|
||||
-- Consent:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
|
||||
|
||||
@ -8280,6 +8304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1722690800"] = "Copies the execu
|
||||
-- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."
|
||||
|
||||
-- owned by you, self-updates possible
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1802999304"] = "owned by you, self-updates possible"
|
||||
|
||||
-- By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1806897624"] = "By clicking on the respective path, the path is copied to the clipboard. You might open these files with a text editor to view their contents."
|
||||
|
||||
@ -8313,6 +8340,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo
|
||||
-- Copies the server URL to the clipboard
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
|
||||
|
||||
-- AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2050051760"] = "AI Studio cannot update itself from its current location. Installing an update would leave a second installation behind instead of replacing this one. To get a new version, download the latest release and install it over your current installation."
|
||||
|
||||
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others."
|
||||
|
||||
@ -8361,6 +8391,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc
|
||||
-- Configuration origin:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
|
||||
|
||||
-- This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2500327819"] = "This copy of AI Studio was installed for you by someone else, most likely your IT department, so it cannot update itself. Contact your IT department if you have questions about updating AI Studio."
|
||||
|
||||
-- Configuration slot:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
|
||||
|
||||
@ -8451,6 +8484,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store"
|
||||
-- Enterprise configuration ID:
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:"
|
||||
|
||||
-- managed by someone else, no self-updates
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3131065024"] = "managed by someone else, no self-updates"
|
||||
|
||||
-- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI).
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)."
|
||||
|
||||
@ -8505,6 +8541,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3440211747"] = "Used Rust compil
|
||||
-- AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management."
|
||||
|
||||
-- You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3454691558"] = "You are running a development build of AI Studio, which never updates itself. Pull the latest changes and rebuild the app instead."
|
||||
|
||||
-- development build, no self-updates
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3459123358"] = "development build, no self-updates"
|
||||
|
||||
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"
|
||||
|
||||
|
||||
31
app/MindWork AI Studio/Tools/Rust/InstallationKind.cs
Normal file
31
app/MindWork AI Studio/Tools/Rust/InstallationKind.cs
Normal file
@ -0,0 +1,31 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// Tells whether this installation is able to update itself, and if not, why.
|
||||
/// </summary>
|
||||
public enum InstallationKind
|
||||
{
|
||||
/// <summary>
|
||||
/// An installation the current user owns and which AI Studio may update itself. This is also
|
||||
/// the fallback when the runtime reports a kind we do not know yet.
|
||||
/// </summary>
|
||||
USER,
|
||||
|
||||
/// <summary>
|
||||
/// An installation someone else deployed and maintains, for example, an IT department. Whoever
|
||||
/// deployed it distributes new versions instead.
|
||||
/// </summary>
|
||||
MANAGED,
|
||||
|
||||
/// <summary>
|
||||
/// An installation the current user owns, but which the updater cannot replace. Its owner has
|
||||
/// to install a new version themselves.
|
||||
/// </summary>
|
||||
UNSUPPORTED_LOCATION,
|
||||
|
||||
/// <summary>
|
||||
/// Not an installation at all, but a development build started from a build directory or an
|
||||
/// IDE. There is nothing here the updater could replace.
|
||||
/// </summary>
|
||||
DEVELOPMENT,
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, string LinuxPackageType);
|
||||
public readonly record struct RuntimeInfoResponse(string WorkingDirectory, string ExecutablePath, string LinuxPackageType, InstallationKind InstallationKind);
|
||||
@ -11,7 +11,18 @@ public sealed class UpdatePolicy(SettingsManager settingsManager, RuntimeInfoRes
|
||||
: runtimeInfo.LinuxPackageType switch
|
||||
{
|
||||
"flatpak" => UpdatePolicyMode.FLATPAK,
|
||||
_ => UpdatePolicyMode.SELF_UPDATE
|
||||
_ => runtimeInfo.InstallationKind switch
|
||||
{
|
||||
//
|
||||
// The runtime already refuses to update these installations. We mirror its decision
|
||||
// here so that the UI explains the situation instead of offering update actions that
|
||||
// would silently do nothing:
|
||||
//
|
||||
InstallationKind.MANAGED => UpdatePolicyMode.MANAGED_INSTALLATION,
|
||||
InstallationKind.UNSUPPORTED_LOCATION => UpdatePolicyMode.UNSUPPORTED_INSTALLATION_LOCATION,
|
||||
InstallationKind.DEVELOPMENT => UpdatePolicyMode.DEVELOPMENT,
|
||||
_ => UpdatePolicyMode.SELF_UPDATE
|
||||
}
|
||||
};
|
||||
|
||||
public bool AllowsManualChecks => this.CurrentMode is UpdatePolicyMode.SELF_UPDATE;
|
||||
|
||||
@ -4,5 +4,8 @@ public enum UpdatePolicyMode
|
||||
{
|
||||
SELF_UPDATE,
|
||||
FLATPAK,
|
||||
ENTERPRISE_DISABLED
|
||||
ENTERPRISE_DISABLED,
|
||||
MANAGED_INSTALLATION,
|
||||
UNSUPPORTED_INSTALLATION_LOCATION,
|
||||
DEVELOPMENT,
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
# v26.8.2, build 252 (2026-08-xx xx:xx UTC)
|
||||
- Added providers for which you bring your own API key. Until now, a provider your organization configured had to come with a shared API key, which meant your IT department needed one key for everybody. Such a provider can now be handed out without a key, so that everyone signs in with their own, for example, a personal OpenAI or Anthropic account. This works for chat, embedding, and transcription providers alike. The provider stays managed by your organization: the host, the model, the instance name, and everything else remain fixed, and the only thing you can edit is the API key. In the settings, these providers carry a key icon instead of the usual lock, so you can see at a glance where you have to add your key; for embedding providers, the test button stays available, so you can check your key right after entering it. Your key is stored on your device in the operating system's credential store, and it stays there even when your organization withdraws the provider later, so it is still in place should the same provider return. For IT departments: the new `AllowUserProvidedAPIKey` option does this, and it works for `LLM_PROVIDERS`, `EMBEDDING_PROVIDERS`, and `TRANSCRIPTION_PROVIDERS` alike.
|
||||
- Improved the grouped provider tables in the settings. You can now click anywhere on a group header to expand or collapse its LLM, embedding, or transcription providers.
|
||||
- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change.
|
||||
- Fixed a removed API key staying in the operating system's credential store. When you cleared the API key of a provider, the previous key remained stored and was still used. It is now removed together with your change.
|
||||
- Fixed AI Studio installing a second copy of itself next to an existing installation. Its updater always installs into your personal user folder, so an installation elsewhere, such as one your IT department rolled out, was never replaced. AI Studio now recognizes those installations and leaves them alone. The information page tells you which case applies to yours. For IT departments: automatic updates can now stay enabled for everybody, and the Enterprise IT documentation explains the rest.
|
||||
@ -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. Enterprise configuration can disable automatic checks or the entire built-in update feature so that the IT department controls 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. Installations you rolled out yourself never update themselves anyway, so the two kinds can coexist on one device.
|
||||
- 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.
|
||||
@ -21,6 +21,36 @@ Set `CONFIG["SETTINGS"]["DataApp.UpdateInterval"]` in the configuration plugin t
|
||||
|
||||
Use `DISABLE_UPDATES` when your organization distributes approved versions through its own software-management process.
|
||||
|
||||
### Installations that never update themselves
|
||||
|
||||
AI Studio recognizes installations its updater cannot replace and never updates those, no matter what `DataApp.UpdateInterval` and `DataApp.UpdateInstallation` say. You can therefore leave automatic updates enabled for your whole organization: the installations you rolled out ignore them and receive their versions from you, while installations your colleagues fetched from GitHub keep updating themselves.
|
||||
|
||||
This matters most on Windows. The installer we publish installs per user below `%LOCALAPPDATA%`, and the updater runs exactly that installer. Updating an installation that sits anywhere else therefore does not replace it: a second installation appears below `%LOCALAPPDATA%` while yours stays untouched, and from then on it is a matter of chance which one a colleague starts. Loosening the permissions of your deployment does not change this — the updater never writes into the current location to begin with.
|
||||
|
||||
AI Studio recognizes these cases:
|
||||
|
||||
| Case | How AI Studio recognizes it | What users are told |
|
||||
|---|---|---|
|
||||
| Marker file | A file named `managed-installation` next to the program file | Updates come from their IT department |
|
||||
| Machine-wide program directory | The program file sits below `%ProgramFiles%`, `%ProgramFiles(x86)%`, or `%ProgramW6432%` | Updates come from their IT department |
|
||||
| Location the user cannot write to | The directory that would have to be replaced is not writable for the current user, e.g. `/Applications` on a device managed through MDM, or `/opt` on Linux | Updates come from their IT department |
|
||||
| Self-chosen directory (Windows only) | Everything else outside `%LOCALAPPDATA%`, e.g. `D:\Tools\MindWork AI Studio` | They have to install a new version themselves, with a link to the latest release |
|
||||
| Flatpak | Running inside a Flatpak sandbox | Updates come from their Flatpak distribution |
|
||||
|
||||
The information page reports which case applies, so a support request can start from that instead of guesswork.
|
||||
|
||||
#### The marker file
|
||||
|
||||
Place an empty file named `managed-installation` next to the program file, in the same directory as `MindWork AI Studio.exe` on Windows or as the executable on Linux. Its content is ignored; only its existence matters. The marker applies to that one installation, so a colleague who installed AI Studio from GitHub on the same device is not affected by it.
|
||||
|
||||
Use the marker when the other cases do not cover your deployment, for example, when you roll out our regular per-user installer through Intune, or when you install into a directory of your own such as `D:\Program Files\MindWork AI Studio`.
|
||||
|
||||
On macOS there is no marker file: any additional file inside the app bundle would break its code signature. A bundle in a location your users cannot write to is recognized anyway. If you want AI Studio to name your organization explicitly on macOS, set `DataApp.UpdateInterval` to `DISABLE_UPDATES`, which takes precedence over all of this.
|
||||
|
||||
#### Existing double installations
|
||||
|
||||
This recognition prevents new double installations; it does not clean up ones that already exist. On affected devices, remove the second installation below `%LOCALAPPDATA%\MindWork AI Studio\` together with its uninstall entry under `HKEY_CURRENT_USER`, and make sure that shortcuts point at your deployment again.
|
||||
|
||||
## 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:
|
||||
|
||||
|
||||
@ -23,7 +23,8 @@ use crate::api_token::APIToken;
|
||||
use crate::clipboard::shutdown_clipboard;
|
||||
use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_server};
|
||||
use crate::environment::{
|
||||
is_prod, is_dev, is_flatpak, CONFIG_DIRECTORY, DATA_DIRECTORY, FLATPAK_LIBRARY_DIRECTORY,
|
||||
installation_kind, is_prod, is_dev, is_flatpak, InstallationKind, CONFIG_DIRECTORY,
|
||||
DATA_DIRECTORY, FLATPAK_LIBRARY_DIRECTORY,
|
||||
};
|
||||
use crate::log::switch_to_file_logging;
|
||||
use crate::pdfium::PDFIUM_LIB_PATH;
|
||||
@ -514,8 +515,7 @@ pub async fn change_location_to(url: &str) {
|
||||
|
||||
/// Checks for updates.
|
||||
pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
|
||||
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" };
|
||||
if let Some(reason) = self_update_blocked_reason(is_flatpak(), installation_kind()) {
|
||||
warn!(Source = "Updater"; "Skipping update check because {reason}.");
|
||||
return Json(CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
@ -600,8 +600,7 @@ pub struct CheckUpdateResponse {
|
||||
|
||||
/// Installs the update.
|
||||
pub async fn install_update(_token: APIToken) {
|
||||
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" };
|
||||
if let Some(reason) = self_update_blocked_reason(is_flatpak(), installation_kind()) {
|
||||
warn!(Source = "Updater"; "Skipping update installation because {reason}.");
|
||||
return;
|
||||
}
|
||||
@ -660,8 +659,18 @@ pub async fn install_update(_token: APIToken) {
|
||||
}
|
||||
}
|
||||
|
||||
fn self_update_allowed(development: bool, flatpak: bool) -> bool {
|
||||
!development && !flatpak
|
||||
/// Returns why this installation cannot update itself, or `None` when it can.
|
||||
fn self_update_blocked_reason(flatpak: bool, installation_kind: InstallationKind) -> Option<&'static str> {
|
||||
if flatpak {
|
||||
return Some("Flatpak installations are updated externally");
|
||||
}
|
||||
|
||||
match installation_kind {
|
||||
InstallationKind::User => None,
|
||||
InstallationKind::Managed => Some("this installation is centrally managed"),
|
||||
InstallationKind::UnsupportedLocation => Some("this installation is in a location the updater cannot replace"),
|
||||
InstallationKind::Development => Some("the app is running in development mode"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for application exit requests.
|
||||
@ -895,18 +904,47 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn self_update_is_disabled_in_development() {
|
||||
assert!(!self_update_allowed(true, false));
|
||||
assert!(self_update_blocked_reason(false, InstallationKind::Development).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_update_is_disabled_for_flatpak() {
|
||||
assert!(!self_update_allowed(false, true));
|
||||
assert!(self_update_blocked_reason(true, InstallationKind::User).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_update_is_disabled_for_managed_installations() {
|
||||
assert!(self_update_blocked_reason(false, InstallationKind::Managed).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_update_is_disabled_for_unsupported_installation_locations() {
|
||||
assert!(self_update_blocked_reason(false, InstallationKind::UnsupportedLocation).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_blocked_installation_kind_has_its_own_reason() {
|
||||
let reasons = [
|
||||
self_update_blocked_reason(false, InstallationKind::Managed),
|
||||
self_update_blocked_reason(false, InstallationKind::UnsupportedLocation),
|
||||
self_update_blocked_reason(false, InstallationKind::Development),
|
||||
];
|
||||
|
||||
for (index, reason) in reasons.iter().enumerate() {
|
||||
assert!(reason.is_some(), "expected a reason at index {index}");
|
||||
assert_eq!(
|
||||
reasons.iter().filter(|other| *other == reason).count(),
|
||||
1,
|
||||
"expected the reason at index {index} to be unique"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_update_is_enabled_for_normal_production_installations() {
|
||||
assert!(self_update_allowed(false, false));
|
||||
assert!(self_update_blocked_reason(false, InstallationKind::User).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdfium_library_directory_prefers_resources_libraries() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@ -23,6 +23,12 @@ const ENTERPRISE_REGISTRY_KEY_PATH: &str = r"Software\github\MindWork AI Studio\
|
||||
const ENTERPRISE_POLICY_SECRET_FILE_NAME: &str = "config_encryption_secret.yaml";
|
||||
const EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME: &str = "external_http_custom_root_certificates.yaml";
|
||||
|
||||
/// Marker file an IT department may place next to the executable to declare this installation
|
||||
/// as centrally managed. It is not used on macOS, because any additional file inside the app
|
||||
/// bundle would break its code signature.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", test))]
|
||||
const MANAGED_INSTALLATION_MARKER_FILE_NAME: &str = "managed-installation";
|
||||
|
||||
pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_POLICY_CONFIGURED";
|
||||
pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED";
|
||||
pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH";
|
||||
@ -47,6 +53,9 @@ pub static CONFIG_DIRECTORY: OnceLock<String> = OnceLock::new();
|
||||
/// The user language cached once per runtime process.
|
||||
static USER_LANGUAGE: OnceLock<String> = OnceLock::new();
|
||||
|
||||
/// The installation kind cached once per runtime process.
|
||||
static INSTALLATION_KIND: OnceLock<InstallationKind> = OnceLock::new();
|
||||
|
||||
/// Returns the config directory.
|
||||
pub async fn get_config_directory(_token: APIToken) -> String {
|
||||
match CONFIG_DIRECTORY.get() {
|
||||
@ -71,11 +80,36 @@ pub async fn read_user_name(_token: APIToken) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
/// Tells whether this installation is able to update itself, and if not, why.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
pub enum InstallationKind {
|
||||
/// An installation the current user owns and which the app may update itself.
|
||||
User,
|
||||
|
||||
/// An installation someone else deployed and maintains: it sits in a machine-wide program
|
||||
/// directory, the current user cannot modify it, or it was declared as centrally maintained
|
||||
/// through the marker file or by shipping it as a Flatpak. Whoever deployed it distributes new
|
||||
/// versions instead.
|
||||
Managed,
|
||||
|
||||
/// An installation the current user owns, but which the updater still cannot replace. This only
|
||||
/// happens on Windows: the NSIS updater ignores where the app currently sits and always
|
||||
/// installs below the local app data directory, so updating a self-chosen directory such as
|
||||
/// `D:\Tools\MindWork AI Studio` would leave a second installation behind. Nobody else
|
||||
/// maintains this installation, so its owner has to install a new version themselves.
|
||||
UnsupportedLocation,
|
||||
|
||||
/// Not an installation at all, but a development build started from a build directory or an
|
||||
/// IDE. There is nothing here the updater could replace.
|
||||
Development,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct RuntimeInfo {
|
||||
pub working_directory: String,
|
||||
pub executable_path: String,
|
||||
pub linux_package_type: String,
|
||||
pub installation_kind: InstallationKind,
|
||||
}
|
||||
|
||||
pub async fn get_runtime_info(_token: APIToken) -> Json<RuntimeInfo> {
|
||||
@ -87,6 +121,7 @@ pub async fn get_runtime_info(_token: APIToken) -> Json<RuntimeInfo> {
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
.unwrap_or_default(),
|
||||
linux_package_type: detect_linux_package_type().to_string(),
|
||||
installation_kind: installation_kind(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -129,6 +164,268 @@ fn env_var_has_value(key: &str) -> bool {
|
||||
env::var(key).is_ok_and(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
/// Returns the kind of this installation, cached for the lifetime of the process.
|
||||
///
|
||||
/// Installations outside the per-user location cannot be replaced by the Tauri updater: on Windows
|
||||
/// it runs the NSIS setup with its per-user defaults and creates a second installation below the
|
||||
/// local app data directory instead of updating the existing one. That happens for an enterprise
|
||||
/// deployment into `C:\Program Files` just as much as for a user who chose their own directory.
|
||||
///
|
||||
/// Whenever the kind cannot be determined, we report a user installation. Wrongly reporting that an
|
||||
/// installation cannot update itself would cut regular users off from every future update,
|
||||
/// including security updates, which is far worse than a second installation.
|
||||
pub(crate) fn installation_kind() -> InstallationKind {
|
||||
*INSTALLATION_KIND.get_or_init(|| {
|
||||
// A development build lives in a build directory, which is perfectly writable and would
|
||||
// therefore look like a regular user installation. We check it up front so that the
|
||||
// platform-specific detection below only ever deals with real installations:
|
||||
let kind = if is_dev() {
|
||||
InstallationKind::Development
|
||||
} else {
|
||||
detect_installation_kind()
|
||||
};
|
||||
|
||||
info!(Source = "Updater"; "Detected a {kind:?} installation of AI Studio.");
|
||||
kind
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn detect_installation_kind() -> InstallationKind {
|
||||
let executable_path = match env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
warn!(Source = "Updater"; "Cannot read the current executable path: {e}. Assuming a user installation.");
|
||||
return InstallationKind::User;
|
||||
}
|
||||
};
|
||||
|
||||
if has_managed_installation_marker(&executable_path) {
|
||||
return InstallationKind::Managed;
|
||||
}
|
||||
|
||||
if is_windows_machine_wide_installation(&executable_path, &windows_program_files_directories()) {
|
||||
return InstallationKind::Managed;
|
||||
}
|
||||
|
||||
if is_windows_per_user_installation(&executable_path, dirs::data_local_dir().as_deref()) {
|
||||
return InstallationKind::User;
|
||||
}
|
||||
|
||||
// The installation sits neither in the location the NSIS updater targets nor in a machine-wide
|
||||
// program directory, so an update would create a second installation next to it. Who put it
|
||||
// there decides how the app words that: a directory the current user cannot write to was set up
|
||||
// by an administrator, while a writable one is a directory the user chose in the installer.
|
||||
let Some(install_directory) = executable_path.parent() else {
|
||||
return InstallationKind::UnsupportedLocation;
|
||||
};
|
||||
|
||||
match directory_is_writable(install_directory) {
|
||||
Some(false) => InstallationKind::Managed,
|
||||
_ => InstallationKind::UnsupportedLocation,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn detect_installation_kind() -> InstallationKind {
|
||||
let executable_path = match env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
warn!(Source = "Updater"; "Cannot read the current executable path: {e}. Assuming a user installation.");
|
||||
return InstallationKind::User;
|
||||
}
|
||||
};
|
||||
|
||||
// The updater replaces the entire app bundle, so it needs to write into the directory that
|
||||
// contains the bundle. On a device managed through an MDM solution like Jamf, the bundle sits
|
||||
// in a location the user cannot write to. We deliberately do not look for a marker file here:
|
||||
// any additional file inside the bundle would break its code signature. As a consequence, a
|
||||
// macOS installation is never reported as managed, only as an unsupported location. An
|
||||
// organization that wants AI Studio to name it explicitly sets DataApp.UpdateInterval to
|
||||
// DISABLE_UPDATES in its enterprise configuration, which takes precedence anyway.
|
||||
match macos_app_bundle_directory(&executable_path) {
|
||||
Some(bundle_directory) => update_target_installation_kind(&bundle_directory),
|
||||
None => InstallationKind::User,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn detect_installation_kind() -> InstallationKind {
|
||||
// Flatpak installations are always updated from outside the app:
|
||||
if is_flatpak() {
|
||||
return InstallationKind::Managed;
|
||||
}
|
||||
|
||||
let executable_path = match env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
warn!(Source = "Updater"; "Cannot read the current executable path: {e}. Assuming a user installation.");
|
||||
return InstallationKind::User;
|
||||
}
|
||||
};
|
||||
|
||||
if has_managed_installation_marker(&executable_path) {
|
||||
return InstallationKind::Managed;
|
||||
}
|
||||
|
||||
// For AppImages, the updater replaces the AppImage file itself. Everything else is replaced
|
||||
// in place as well. A deployment into a system-wide location such as /opt is therefore not
|
||||
// updatable by the app:
|
||||
let update_target = env::var("APPIMAGE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(executable_path);
|
||||
|
||||
update_target_installation_kind(&update_target)
|
||||
}
|
||||
|
||||
/// Returns whether the executable sits in the per-user location the NSIS updater targets, which is
|
||||
/// the only Windows location it can actually replace. Everywhere else an update installs below the
|
||||
/// local app data directory and leaves the existing installation behind.
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn is_windows_per_user_installation(executable_path: &Path, local_app_data_directory: Option<&Path>) -> bool {
|
||||
let Some(local_app_data_directory) = local_app_data_directory else {
|
||||
warn!(Source = "Updater"; "Cannot read the local app data directory. Assuming a user installation.");
|
||||
return true;
|
||||
};
|
||||
|
||||
path_is_below(executable_path, local_app_data_directory)
|
||||
}
|
||||
|
||||
/// Returns whether the executable sits in one of the machine-wide program directories. The NSIS
|
||||
/// installer we ship installs per user and never picks such a directory on its own, so whatever
|
||||
/// runs from there was packaged and deployed by an IT department.
|
||||
///
|
||||
/// The permissions of that directory deliberately play no role here. Some organizations make their
|
||||
/// deployment writable for users, hoping the updater would then replace it in place. It never does:
|
||||
/// it runs our per-user setup, which installs below the local app data directory regardless of the
|
||||
/// current location and leaves a second installation behind.
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn is_windows_machine_wide_installation(executable_path: &Path, program_files_directories: &[PathBuf]) -> bool {
|
||||
program_files_directories
|
||||
.iter()
|
||||
.any(|program_files_directory| path_is_below(executable_path, program_files_directory))
|
||||
}
|
||||
|
||||
/// Returns the machine-wide program directories of this Windows system. A 32-bit process sees
|
||||
/// `ProgramFiles` as `C:\Program Files (x86)` and reaches the 64-bit directory only through
|
||||
/// `ProgramW6432`, so we read all of them instead of assuming one layout or a fixed drive.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows_program_files_directories() -> Vec<PathBuf> {
|
||||
["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"]
|
||||
.iter()
|
||||
.filter_map(|variable_name| env::var(variable_name).ok())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(PathBuf::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns whether the given path sits inside the given directory.
|
||||
///
|
||||
/// Both paths must be compared in the same form. Canonicalization resolves junctions, symbolic
|
||||
/// links, and 8.3 short names such as PROGRA~1, but it also prepends the \\?\ verbatim prefix on
|
||||
/// Windows. Applying it to only one of the two paths would make even a regular per-user
|
||||
/// installation look like it sits somewhere else. Therefore, we either use both canonicalized paths
|
||||
/// or neither of them.
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn path_is_below(path: &Path, directory: &Path) -> bool {
|
||||
let (path, directory) = match (fs::canonicalize(path), fs::canonicalize(directory)) {
|
||||
(Ok(canonical_path), Ok(canonical_directory)) => (canonical_path, canonical_directory),
|
||||
_ => (path.to_path_buf(), directory.to_path_buf()),
|
||||
};
|
||||
|
||||
path_starts_with_ignoring_case(&path, &directory)
|
||||
}
|
||||
|
||||
/// Compares the path components case-insensitively, because Windows paths are not case-sensitive.
|
||||
/// A plain string prefix check is not enough either: it would treat `C:\Users\Alice-Backup` as
|
||||
/// being below `C:\Users\Alice`.
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn path_starts_with_ignoring_case(path: &Path, prefix: &Path) -> bool {
|
||||
let mut path_components = path.components();
|
||||
for prefix_component in prefix.components() {
|
||||
let Some(path_component) = path_components.next() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let path_text = path_component.as_os_str().to_string_lossy();
|
||||
let prefix_text = prefix_component.as_os_str().to_string_lossy();
|
||||
if !path_text.eq_ignore_ascii_case(&prefix_text) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Derives the app bundle root from the executable path, e.g.
|
||||
/// `/Applications/MindWork AI Studio.app/Contents/MacOS/MindWork AI Studio` becomes
|
||||
/// `/Applications/MindWork AI Studio.app`.
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn macos_app_bundle_directory(executable_path: &Path) -> Option<PathBuf> {
|
||||
let macos_directory = executable_path.parent()?;
|
||||
if macos_directory.file_name()? != "MacOS" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let contents_directory = macos_directory.parent()?;
|
||||
if contents_directory.file_name()? != "Contents" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bundle_directory = contents_directory.parent()?;
|
||||
if !bundle_directory.extension().is_some_and(|extension| extension.eq_ignore_ascii_case("app")) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(bundle_directory.to_path_buf())
|
||||
}
|
||||
|
||||
/// Decides the installation kind for the platforms whose updater replaces the given target in
|
||||
/// place. It writes the replacement into the directory that contains the target, so that is the
|
||||
/// directory we test: whoever may write there may update the app.
|
||||
#[cfg(any(target_os = "macos", target_os = "linux", test))]
|
||||
fn update_target_installation_kind(update_target: &Path) -> InstallationKind {
|
||||
let Some(directory) = update_target.parent() else {
|
||||
return InstallationKind::User;
|
||||
};
|
||||
|
||||
// A directory the current user cannot write to was set up by an administrator or an IT
|
||||
// department, and they are the ones distributing new versions. There is no unsupported location
|
||||
// on these platforms: an in-place replacement works wherever the user may write:
|
||||
match directory_is_writable(directory) {
|
||||
Some(false) => InstallationKind::Managed,
|
||||
_ => InstallationKind::User,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests whether the current user may write into the given directory by actually creating a
|
||||
/// temporary file there. Permission bits alone are not reliable: ACLs, read-only mounts, and
|
||||
/// managed-device restrictions do not show up in them.
|
||||
///
|
||||
/// Returns `None` when the test itself could not be carried out, so that callers can fall back to
|
||||
/// treating the installation as updatable instead of locking the user out on an inconclusive probe.
|
||||
fn directory_is_writable(directory: &Path) -> Option<bool> {
|
||||
match tempfile::Builder::new().prefix(".ai-studio-write-test").tempfile_in(directory) {
|
||||
Ok(_) => Some(true),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => Some(false),
|
||||
Err(e) => {
|
||||
warn!(Source = "Updater"; "Cannot test whether '{}' is writable: {e}.", directory.display());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether an IT department declared this installation as centrally managed by placing a
|
||||
/// marker file next to the executable. This covers deployments the path check cannot recognize,
|
||||
/// for example, when an organization rolls out the regular per-user installer through Intune.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", test))]
|
||||
fn has_managed_installation_marker(executable_path: &Path) -> bool {
|
||||
match executable_path.parent() {
|
||||
Some(directory) => directory.join(MANAGED_INSTALLATION_MARKER_FILE_NAME).is_file(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the application is running in development mode.
|
||||
pub fn is_dev() -> bool {
|
||||
cfg!(debug_assertions)
|
||||
@ -1055,17 +1352,21 @@ fn normalize_enterprise_config_id(value: &str) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
enterprise_environment_key_name, enterprise_policy_file_slot_suffix,
|
||||
directory_is_writable, enterprise_environment_key_name,
|
||||
enterprise_policy_file_slot_suffix, has_managed_installation_marker,
|
||||
is_windows_machine_wide_installation, is_windows_per_user_installation,
|
||||
load_external_http_custom_root_certificate_policy_from_directories,
|
||||
linux_policy_directories_from_xdg, load_policy_values_from_directories,
|
||||
normalize_locale_tag, parse_enterprise_source_values,
|
||||
select_effective_enterprise_config_source, select_effective_enterprise_secret_source,
|
||||
macos_app_bundle_directory, normalize_locale_tag, parse_enterprise_source_values,
|
||||
path_starts_with_ignoring_case, select_effective_enterprise_config_source,
|
||||
select_effective_enterprise_secret_source, update_target_installation_kind,
|
||||
EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues,
|
||||
ExternalHttpCustomRootCertificatePolicy,
|
||||
ExternalHttpCustomRootCertificatePolicy, InstallationKind,
|
||||
MANAGED_INSTALLATION_MARKER_FILE_NAME,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::tempdir;
|
||||
|
||||
const TEST_ID_A: &str = "9072B77D-CA81-40DA-BE6A-861DA525EF7B";
|
||||
@ -1454,6 +1755,227 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a path from its components using the separator of the current platform. Windows
|
||||
/// paths written with backslashes would be a single component on Unix, so the tests below
|
||||
/// could not exercise the component comparison there.
|
||||
fn path_of(components: &[&str]) -> PathBuf {
|
||||
components.iter().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_per_user_installations_may_update_themselves() {
|
||||
let local_app_data = path_of(&["/", "Users", "Alice", "AppData", "Local"]);
|
||||
let executable = path_of(&["/", "Users", "Alice", "AppData", "Local", "MindWork AI Studio", "MindWork AI Studio.exe"]);
|
||||
|
||||
assert!(is_windows_per_user_installation(&executable, Some(&local_app_data)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_installations_outside_the_local_app_data_directory_cannot_update_themselves() {
|
||||
let local_app_data = path_of(&["/", "Users", "Alice", "AppData", "Local"]);
|
||||
|
||||
for install_directory in [
|
||||
vec!["/", "Program Files", "MindWork AI Studio"],
|
||||
vec!["/", "Program Files (x86)", "MindWork AI Studio"],
|
||||
vec!["/", "Apps", "MindWork AI Studio"],
|
||||
vec!["/", "Users", "Alice", "AppData", "Roaming", "MindWork AI Studio"],
|
||||
] {
|
||||
let mut components = install_directory.clone();
|
||||
components.push("MindWork AI Studio.exe");
|
||||
let executable = path_of(&components);
|
||||
|
||||
assert!(
|
||||
!is_windows_per_user_installation(&executable, Some(&local_app_data)),
|
||||
"expected '{}' to sit outside the per-user installation location",
|
||||
executable.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_program_directories_are_managed_regardless_of_their_permissions() {
|
||||
let program_files_directories = vec![
|
||||
path_of(&["/", "Program Files"]),
|
||||
path_of(&["/", "Program Files (x86)"]),
|
||||
];
|
||||
|
||||
for install_directory in [
|
||||
vec!["/", "Program Files", "MindWork AI Studio"],
|
||||
vec!["/", "Program Files (x86)", "MindWork AI Studio"],
|
||||
] {
|
||||
let mut components = install_directory.clone();
|
||||
components.push("MindWork AI Studio.exe");
|
||||
let executable = path_of(&components);
|
||||
|
||||
assert!(
|
||||
is_windows_machine_wide_installation(&executable, &program_files_directories),
|
||||
"expected '{}' to be a machine-wide installation",
|
||||
executable.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_directories_next_to_the_program_directories_are_not_machine_wide() {
|
||||
let program_files_directories = vec![path_of(&["/", "Program Files"])];
|
||||
|
||||
// 'Program Files (x86)' is not configured here, and a plain string prefix check would still
|
||||
// match it against 'Program Files'. The same holds for a self-chosen directory:
|
||||
for executable in [
|
||||
path_of(&["/", "Program Files (x86)", "MindWork AI Studio", "MindWork AI Studio.exe"]),
|
||||
path_of(&["/", "Apps", "MindWork AI Studio", "MindWork AI Studio.exe"]),
|
||||
] {
|
||||
assert!(
|
||||
!is_windows_machine_wide_installation(&executable, &program_files_directories),
|
||||
"expected '{}' not to be a machine-wide installation",
|
||||
executable.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_installations_are_not_machine_wide_without_program_directories() {
|
||||
let executable = path_of(&["/", "Program Files", "MindWork AI Studio", "MindWork AI Studio.exe"]);
|
||||
|
||||
assert!(!is_windows_machine_wide_installation(&executable, &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_installation_kind_ignores_case_but_respects_component_boundaries() {
|
||||
let local_app_data = path_of(&["/", "Users", "Alice", "AppData", "Local"]);
|
||||
|
||||
// Windows paths are not case-sensitive:
|
||||
let differently_cased = path_of(&["/", "users", "alice", "appdata", "local", "MindWork AI Studio", "MindWork AI Studio.exe"]);
|
||||
assert!(is_windows_per_user_installation(&differently_cased, Some(&local_app_data)));
|
||||
|
||||
// A plain string prefix check would wrongly accept this one:
|
||||
let sibling_directory = path_of(&["/", "Users", "Alice", "AppData", "LocalBackup", "MindWork AI Studio", "MindWork AI Studio.exe"]);
|
||||
assert!(!is_windows_per_user_installation(&sibling_directory, Some(&local_app_data)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_installation_kind_falls_back_to_user_without_local_app_data() {
|
||||
let executable = path_of(&["/", "Program Files", "MindWork AI Studio", "MindWork AI Studio.exe"]);
|
||||
|
||||
assert!(is_windows_per_user_installation(&executable, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_installation_kind_does_not_mix_canonical_and_raw_paths() {
|
||||
// The local app data directory exists and can be canonicalized, while the executable below
|
||||
// it does not. Canonicalizing only one of the two would compare different path forms, for
|
||||
// example '/private/var/...' against '/var/...' or '\\?\C:\...' against 'C:\...', and would
|
||||
// reject a perfectly regular per-user installation:
|
||||
let local_app_data = tempdir().unwrap();
|
||||
let executable = local_app_data
|
||||
.path()
|
||||
.join("MindWork AI Studio")
|
||||
.join("MindWork AI Studio.exe");
|
||||
|
||||
assert!(is_windows_per_user_installation(&executable, Some(local_app_data.path())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_starts_with_ignoring_case_compares_whole_components() {
|
||||
assert!(path_starts_with_ignoring_case(
|
||||
Path::new("/Applications/Some App.app/Contents"),
|
||||
Path::new("/applications/some app.app")
|
||||
));
|
||||
|
||||
assert!(!path_starts_with_ignoring_case(
|
||||
Path::new("/Applications"),
|
||||
Path::new("/Applications/Some App.app")
|
||||
));
|
||||
|
||||
assert!(!path_starts_with_ignoring_case(
|
||||
Path::new("/Applications-Backup/Some App.app"),
|
||||
Path::new("/Applications")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_app_bundle_directory_resolves_the_bundle_root() {
|
||||
assert_eq!(
|
||||
macos_app_bundle_directory(Path::new(
|
||||
"/Applications/MindWork AI Studio.app/Contents/MacOS/MindWork AI Studio"
|
||||
)),
|
||||
Some(PathBuf::from("/Applications/MindWork AI Studio.app"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_app_bundle_directory_rejects_paths_outside_a_bundle() {
|
||||
assert_eq!(
|
||||
macos_app_bundle_directory(Path::new("/usr/local/bin/mindwork-ai-studio")),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
macos_app_bundle_directory(Path::new(
|
||||
"/Applications/MindWork AI Studio/Contents/MacOS/MindWork AI Studio"
|
||||
)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writable_update_targets_are_user_installations() {
|
||||
let directory = tempdir().unwrap();
|
||||
assert_eq!(directory_is_writable(directory.path()), Some(true));
|
||||
|
||||
// An AppImage may sit anywhere as long as its directory is writable:
|
||||
let update_target = directory.path().join("MindWork AI Studio.AppImage");
|
||||
assert_eq!(
|
||||
update_target_installation_kind(&update_target),
|
||||
InstallationKind::User
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn read_only_update_targets_are_managed_installations() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let directory = tempdir().unwrap();
|
||||
let read_only_directory = directory.path().join("read-only");
|
||||
fs::create_dir(&read_only_directory).unwrap();
|
||||
fs::set_permissions(&read_only_directory, fs::Permissions::from_mode(0o500)).unwrap();
|
||||
|
||||
// Permissions do not apply to root, so the assertions below would fail there. In that case,
|
||||
// we skip them instead of asserting something the environment cannot provide:
|
||||
let running_as_root = fs::write(read_only_directory.join("root-probe"), "").is_ok();
|
||||
if !running_as_root {
|
||||
assert_eq!(directory_is_writable(&read_only_directory), Some(false));
|
||||
|
||||
// Whoever set up a directory the user cannot write to also distributes the updates:
|
||||
let update_target = read_only_directory.join("MindWork AI Studio.AppImage");
|
||||
assert_eq!(
|
||||
update_target_installation_kind(&update_target),
|
||||
InstallationKind::Managed
|
||||
);
|
||||
}
|
||||
|
||||
// Restore the permissions so that the temporary directory can be cleaned up:
|
||||
fs::set_permissions(&read_only_directory, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_marker_file_declares_a_managed_installation() {
|
||||
let directory = tempdir().unwrap();
|
||||
let executable = directory.path().join("MindWork AI Studio");
|
||||
fs::write(&executable, "").unwrap();
|
||||
|
||||
assert!(!has_managed_installation_marker(&executable));
|
||||
|
||||
fs::write(
|
||||
directory.path().join(MANAGED_INSTALLATION_MARKER_FILE_NAME),
|
||||
"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(has_managed_installation_marker(&executable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_policy_values_from_directories_uses_first_directory_wins() {
|
||||
let directory_a = tempdir().unwrap();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user