diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 07dfebd2..0fc41f7c 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -97,7 +97,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan // Set the snackbar for the update service: UpdateService.SetBlazorDependencies(this.Snackbar); - GlobalShortcutService.Initialize(); TemporaryChatService.Initialize(); // Should the navigation bar be open by default? @@ -251,6 +250,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan // Set up hot reloading for plugins: PluginFactory.SetUpHotReloading(); + await this.MessageBus.SendMessage(this, Event.STARTUP_COMPLETED); } }); break; diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index d4bfc7e3..12634ad6 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -63,18 +63,29 @@ public sealed class SettingsManager /// Loads the settings from the file system. /// public async Task LoadSettings() + { + var settingsSnapshot = await this.TryReadSettingsSnapshot(); + if (settingsSnapshot is not null) + this.ConfigurationData = settingsSnapshot; + } + + /// + /// Reads the settings from disk without mutating the current in-memory state. + /// + /// A (migrated) settings snapshot, or null if it could not be read. + public async Task TryReadSettingsSnapshot() { if(!this.IsSetUp) { this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet."); - return; + return null; } var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); if(!File.Exists(settingsPath)) { this.logger.LogWarning("Cannot load settings, because the settings file does not exist."); - return; + return null; } // We read the `"Version": "V3"` line to determine the version of the settings file: @@ -87,30 +98,28 @@ public sealed class SettingsManager // Extract the version from the line: var settingsVersionText = line.Split('"')[3]; - + // Parse the version: Enum.TryParse(settingsVersionText, out Version settingsVersion); if(settingsVersion is Version.UNKNOWN) { this.logger.LogError("Unknown version of the settings file found."); - this.ConfigurationData = new(); - return; + return new(); } - - this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); - + + var settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); + // // We filter the enabled preview features based on the preview visibility. // This is necessary when the app starts up: some preview features may have // been disabled or released from the last time the app was started. // - this.ConfigurationData.App.EnabledPreviewFeatures = this.ConfigurationData.App.PreviewVisibility.FilterPreviewFeatures(this.ConfigurationData.App.EnabledPreviewFeatures); - - return; + settingsData.App.EnabledPreviewFeatures = settingsData.App.PreviewVisibility.FilterPreviewFeatures(settingsData.App.EnabledPreviewFeatures); + return settingsData; } - + this.logger.LogError("Failed to read the version of the settings file."); - this.ConfigurationData = new(); + return new(); } /// diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index b3d3628f..aac82108 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -9,6 +9,7 @@ public enum Event CONFIGURATION_CHANGED, COLOR_THEME_CHANGED, STARTUP_PLUGIN_SYSTEM, + STARTUP_COMPLETED, STARTUP_ENTERPRISE_ENVIRONMENT, PLUGINS_RELOADED, SHOW_ERROR, diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index 4515b78b..f0bca207 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -8,8 +8,16 @@ namespace AIStudio.Tools.Services; public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiver { - private static bool IS_INITIALIZED; + private static bool IS_STARTUP_COMPLETED; + private enum ShortcutSyncSource + { + CONFIGURATION_CHANGED, + STARTUP_COMPLETED, + PLUGINS_RELOADED, + } + + private readonly SemaphoreSlim registrationSemaphore = new(1, 1); private readonly ILogger logger; private readonly SettingsManager settingsManager; private readonly MessageBus messageBus; @@ -27,22 +35,19 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv this.rustService = rustService; this.messageBus.RegisterComponent(this); - this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]); + this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED, Event.STARTUP_COMPLETED]); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - // Wait until the app is fully initialized: - while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) - await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); - - // Register shortcuts on startup: - await this.RegisterAllShortcuts(); + this.logger.LogInformation("The global shortcut service was initialized."); + await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); } public override async Task StopAsync(CancellationToken cancellationToken) { this.messageBus.Unregister(this); + this.registrationSemaphore.Dispose(); await base.StopAsync(cancellationToken); } @@ -53,7 +58,22 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv switch (triggeredEvent) { case Event.CONFIGURATION_CHANGED: - await this.RegisterAllShortcuts(); + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.CONFIGURATION_CHANGED); + break; + + case Event.STARTUP_COMPLETED: + IS_STARTUP_COMPLETED = true; + await this.RegisterAllShortcuts(ShortcutSyncSource.STARTUP_COMPLETED); + break; + + case Event.PLUGINS_RELOADED: + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.PLUGINS_RELOADED); break; } } @@ -62,33 +82,64 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv #endregion - private async Task RegisterAllShortcuts() + private async Task RegisterAllShortcuts(ShortcutSyncSource source) { - this.logger.LogInformation("Registering global shortcuts."); - foreach (var shortcutId in Enum.GetValues()) + await this.registrationSemaphore.WaitAsync(); + try { - if(shortcutId is Shortcut.NONE) - continue; - - var shortcut = this.GetShortcutValue(shortcutId); - var isEnabled = this.IsShortcutAllowed(shortcutId); - - if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + this.logger.LogInformation("Registering global shortcuts (source='{Source}').", source); + foreach (var shortcutId in Enum.GetValues()) { - var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut); - if (success) - this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut); + if(shortcutId is Shortcut.NONE) + continue; + + var shortcutState = await this.GetShortcutState(shortcutId, source); + var shortcut = shortcutState.Shortcut; + var isEnabled = shortcutState.IsEnabled; + this.logger.LogInformation( + "Sync shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').", + shortcutId, + source, + isEnabled, + shortcut); + + if (shortcutState.UsesPersistedFallback) + { + this.logger.LogWarning( + "Using persisted shortcut fallback for '{ShortcutId}' during startup completion (source='{Source}', configured='{Shortcut}').", + shortcutId, + source, + shortcut); + } + + if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + { + var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut); + if (success) + this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut); + else + this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut); + } else - this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut); - } - else - { - // Disable the shortcut when empty or feature is disabled: - await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty); - } - } + { + this.logger.LogInformation( + "Disabling global shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').", + shortcutId, + source, + isEnabled, + shortcut); - this.logger.LogInformation("Global shortcuts registration completed."); + // Disable the shortcut when empty or feature is disabled: + await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty); + } + } + + this.logger.LogInformation("Global shortcuts registration completed (source='{Source}').", source); + } + finally + { + this.registrationSemaphore.Release(); + } } private string GetShortcutValue(Shortcut name) => name switch @@ -107,5 +158,30 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv _ => true, }; - public static void Initialize() => IS_INITIALIZED = true; + private async Task GetShortcutState(Shortcut shortcutId, ShortcutSyncSource source) + { + var shortcut = this.GetShortcutValue(shortcutId); + var isEnabled = this.IsShortcutAllowed(shortcutId); + if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + return new(shortcut, true, false); + + if (source is not ShortcutSyncSource.STARTUP_COMPLETED || shortcutId is not Shortcut.VOICE_RECORDING_TOGGLE) + return new(shortcut, isEnabled, false); + + var settingsSnapshot = await this.settingsManager.TryReadSettingsSnapshot(); + if (settingsSnapshot is null) + return new(shortcut, isEnabled, false); + + var fallbackShortcut = settingsSnapshot.App.ShortcutVoiceRecording; + var fallbackEnabled = + settingsSnapshot.App.EnabledPreviewFeatures.Contains(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026) && + !string.IsNullOrWhiteSpace(settingsSnapshot.App.UseTranscriptionProvider); + + if (!fallbackEnabled || string.IsNullOrWhiteSpace(fallbackShortcut)) + return new(shortcut, isEnabled, false); + + return new(fallbackShortcut, true, true); + } + + private readonly record struct ShortcutState(string Shortcut, bool IsEnabled, bool UsesPersistedFallback); } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 2dd6c538..25d39938 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -2,6 +2,7 @@ - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. +- Improved the reliability of the global voice recording shortcut so it stays available more consistently. - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 407a5627..c0161bfc 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "brotli" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -2115,9 +2115,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", "png", @@ -2517,17 +2517,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" -dependencies = [ - "serde", - "serde_json", - "thiserror 1.0.63", -] - [[package]] name = "json-patch" version = "2.0.0" @@ -5095,9 +5084,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf327e247698d3f39af8aa99401c9708384290d1f5c544bf5d251d44c2fea22" +checksum = "3ae1f57c291a6ab8e1d2e6b8ad0a35ff769c9925deb8a89de85425ff08762d0c" dependencies = [ "anyhow", "base64 0.22.1", @@ -5157,15 +5146,15 @@ dependencies = [ [[package]] name = "tauri-build" -version = "1.5.3" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c6ec7a5c3296330c7818478948b422967ce4649094696c985f61d50076d29c" +checksum = "2db08694eec06f53625cfc6fff3a363e084e5e9a238166d2989996413c346453" dependencies = [ "anyhow", "cargo_toml", "dirs-next", "heck 0.5.0", - "json-patch 1.4.0", + "json-patch", "semver", "serde", "serde_json", @@ -5176,14 +5165,14 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a9e3f5cebf779a63bf24903e714ec91196c307d8249a0008b882424328bcda" +checksum = "53438d78c4a037ffe5eafa19e447eea599bedfb10844cb08ec53c2471ac3ac3f" dependencies = [ "base64 0.21.7", "brotli", "ico", - "json-patch 2.0.0", + "json-patch", "plist", "png", "proc-macro2", @@ -5202,9 +5191,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1d0e989f54fe06c5ef0875c5e19cf96453d099a0a774d5192ab47e80471cdab" +checksum = "233988ac08c1ed3fe794cd65528d48d8f7ed4ab3895ca64cdaa6ad4d00c45c0b" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5230,9 +5219,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33fda7d213e239077fad52e96c6b734cecedb30c2382118b64f94cb5103ff3a" +checksum = "8066855882f00172935e3fa7d945126580c34dcbabab43f5d4f0c2398a67d47b" dependencies = [ "gtk", "http 0.2.12", @@ -5251,9 +5240,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.14.10" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c447dcd9b0f09c7dc4b752cc33e72788805bfd761fbda5692d30c48289efec" +checksum = "ce361fec1e186705371f1c64ae9dd2a3a6768bc530d0a2d5e75a634bb416ad4d" dependencies = [ "cocoa", "gtk", @@ -5271,9 +5260,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0c939e88d82903a0a7dfb28388b12a3c03504d6bd6086550edaa3b6d8beaa" +checksum = "c357952645e679de02cd35007190fcbce869b93ffc61b029f33fe02648453774" dependencies = [ "brotli", "ctor", @@ -5282,7 +5271,7 @@ dependencies = [ "heck 0.5.0", "html5ever", "infer", - "json-patch 2.0.0", + "json-patch", "kuchikiki", "log", "memchr", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b3c1b32e..6a71f79c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -6,10 +6,10 @@ description = "MindWork AI Studio" authors = ["Thorsten Sommer"] [build-dependencies] -tauri-build = { version = "1.5", features = [] } +tauri-build = { version = "1.5.6", features = [] } [dependencies] -tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } +tauri = { version = "1.8.3", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149"