diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index faae40b2..4516b81b 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -97,6 +97,7 @@ 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?
@@ -113,7 +114,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
await this.UpdateThemeConfiguration();
this.LoadNavItems();
- await this.RegisterGlobalShortcuts();
await base.OnInitializedAsync();
}
@@ -247,26 +247,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
#endregion
- ///
- /// Registers global shortcuts based on the current configuration.
- ///
- private async Task RegisterGlobalShortcuts()
- {
- // Only register the voice recording shortcut if the preview feature is enabled:
- if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
- {
- var shortcut = this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording;
- if (!string.IsNullOrWhiteSpace(shortcut))
- {
- var success = await this.RustService.UpdateGlobalShortcut("voice_recording_toggle", shortcut);
- if (success)
- this.Logger.LogInformation("Global voice recording shortcut '{Shortcut}' registered successfully.", shortcut);
- else
- this.Logger.LogWarning("Failed to register global voice recording shortcut '{Shortcut}'.", shortcut);
- }
- }
- }
-
private void LoadNavItems()
{
this.navItems = new List(this.GetNavItems());
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index e87dc0ca..c577b2f4 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -133,6 +133,7 @@ internal sealed class Program
builder.Services.AddHostedService();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
+ builder.Services.AddHostedService();
// ReSharper disable AccessToDisposedClosure
builder.Services.AddHostedService(_ => rust);
diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs
new file mode 100644
index 00000000..23ddb553
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs
@@ -0,0 +1,100 @@
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Tools.Services;
+
+public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiver
+{
+ private static bool IS_INITIALIZED;
+
+ private readonly ILogger logger;
+ private readonly SettingsManager settingsManager;
+ private readonly MessageBus messageBus;
+ private readonly RustService rustService;
+
+ public GlobalShortcutService(
+ ILogger logger,
+ SettingsManager settingsManager,
+ MessageBus messageBus,
+ RustService rustService)
+ {
+ this.logger = logger;
+ this.settingsManager = settingsManager;
+ this.messageBus = messageBus;
+ this.rustService = rustService;
+
+ this.messageBus.RegisterComponent(this);
+ this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]);
+ }
+
+ 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();
+ }
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ this.messageBus.Unregister(this);
+ await base.StopAsync(cancellationToken);
+ }
+
+ #region IMessageBusReceiver
+
+ public async Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
+ {
+ switch (triggeredEvent)
+ {
+ case Event.CONFIGURATION_CHANGED:
+ await this.RegisterAllShortcuts();
+ break;
+ }
+ }
+
+ public Task ProcessMessageWithResult(
+ ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
+ => Task.FromResult(default);
+
+ #endregion
+
+ private async Task RegisterAllShortcuts()
+ {
+ this.logger.LogInformation("Registering global shortcuts.");
+
+ //
+ // Voice recording shortcut (preview feature)
+ //
+ if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager))
+ {
+ var shortcut = this.settingsManager.ConfigurationData.App.ShortcutVoiceRecording;
+ if (!string.IsNullOrWhiteSpace(shortcut))
+ {
+ var success = await this.rustService.UpdateGlobalShortcut("voice_recording_toggle", shortcut);
+ if (success)
+ this.logger.LogInformation("Global shortcut 'voice_recording_toggle' ({Shortcut}) registered.", shortcut);
+ else
+ this.logger.LogWarning("Failed to register global shortcut 'voice_recording_toggle' ({Shortcut}).", shortcut);
+ }
+ else
+ {
+ // Disable shortcut when empty
+ await this.rustService.UpdateGlobalShortcut("voice_recording_toggle", string.Empty);
+ }
+ }
+ else
+ {
+ // Disable the shortcut when the preview feature is disabled:
+ await this.rustService.UpdateGlobalShortcut("voice_recording_toggle", string.Empty);
+ }
+
+ this.logger.LogInformation("Global shortcuts registration completed.");
+ }
+
+ public static void Initialize() => IS_INITIALIZED = true;
+}