mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-03-29 17:31:37 +00:00
Improved global shortcut reliability
This commit is contained in:
parent
12ecd7d2fc
commit
29e651721e
@ -9,11 +9,21 @@ namespace AIStudio.Tools.Services;
|
|||||||
public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiver
|
public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiver
|
||||||
{
|
{
|
||||||
private static bool IS_INITIALIZED;
|
private static bool IS_INITIALIZED;
|
||||||
|
private static readonly TimeSpan STARTUP_RECOVERY_WINDOW = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
|
private enum ShortcutSyncSource
|
||||||
|
{
|
||||||
|
STARTUP,
|
||||||
|
CONFIGURATION_CHANGED,
|
||||||
|
PLUGINS_RELOADED,
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim registrationSemaphore = new(1, 1);
|
||||||
private readonly ILogger<GlobalShortcutService> logger;
|
private readonly ILogger<GlobalShortcutService> logger;
|
||||||
private readonly SettingsManager settingsManager;
|
private readonly SettingsManager settingsManager;
|
||||||
private readonly MessageBus messageBus;
|
private readonly MessageBus messageBus;
|
||||||
private readonly RustService rustService;
|
private readonly RustService rustService;
|
||||||
|
private readonly DateTimeOffset serviceStartedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
public GlobalShortcutService(
|
public GlobalShortcutService(
|
||||||
ILogger<GlobalShortcutService> logger,
|
ILogger<GlobalShortcutService> logger,
|
||||||
@ -27,7 +37,7 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
|||||||
this.rustService = rustService;
|
this.rustService = rustService;
|
||||||
|
|
||||||
this.messageBus.RegisterComponent(this);
|
this.messageBus.RegisterComponent(this);
|
||||||
this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]);
|
this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
@ -37,12 +47,13 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
|||||||
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
|
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
|
||||||
|
|
||||||
// Register shortcuts on startup:
|
// Register shortcuts on startup:
|
||||||
await this.RegisterAllShortcuts();
|
await this.RegisterAllShortcuts(ShortcutSyncSource.STARTUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
this.messageBus.Unregister(this);
|
this.messageBus.Unregister(this);
|
||||||
|
this.registrationSemaphore.Dispose();
|
||||||
await base.StopAsync(cancellationToken);
|
await base.StopAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +64,11 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
|||||||
switch (triggeredEvent)
|
switch (triggeredEvent)
|
||||||
{
|
{
|
||||||
case Event.CONFIGURATION_CHANGED:
|
case Event.CONFIGURATION_CHANGED:
|
||||||
await this.RegisterAllShortcuts();
|
await this.RegisterAllShortcuts(ShortcutSyncSource.CONFIGURATION_CHANGED);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.PLUGINS_RELOADED:
|
||||||
|
await this.RegisterAllShortcuts(ShortcutSyncSource.PLUGINS_RELOADED);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,16 +77,33 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task RegisterAllShortcuts()
|
private async Task RegisterAllShortcuts(ShortcutSyncSource source)
|
||||||
{
|
{
|
||||||
this.logger.LogInformation("Registering global shortcuts.");
|
await this.registrationSemaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.logger.LogInformation("Registering global shortcuts (source='{Source}').", source);
|
||||||
foreach (var shortcutId in Enum.GetValues<Shortcut>())
|
foreach (var shortcutId in Enum.GetValues<Shortcut>())
|
||||||
{
|
{
|
||||||
if(shortcutId is Shortcut.NONE)
|
if(shortcutId is Shortcut.NONE)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var shortcut = this.GetShortcutValue(shortcutId);
|
var (shortcut, isEnabled, usesPersistedFallback) = await this.GetShortcutState(shortcutId);
|
||||||
var isEnabled = this.IsShortcutAllowed(shortcutId);
|
this.logger.LogInformation(
|
||||||
|
"Sync shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').",
|
||||||
|
shortcutId,
|
||||||
|
source,
|
||||||
|
isEnabled,
|
||||||
|
shortcut);
|
||||||
|
|
||||||
|
if (usesPersistedFallback)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning(
|
||||||
|
"Using persisted shortcut fallback for '{ShortcutId}' during startup recovery (source='{Source}', configured='{Shortcut}').",
|
||||||
|
shortcutId,
|
||||||
|
source,
|
||||||
|
shortcut);
|
||||||
|
}
|
||||||
|
|
||||||
if (isEnabled && !string.IsNullOrWhiteSpace(shortcut))
|
if (isEnabled && !string.IsNullOrWhiteSpace(shortcut))
|
||||||
{
|
{
|
||||||
@ -83,12 +115,24 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
this.logger.LogInformation(
|
||||||
|
"Disabling global shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').",
|
||||||
|
shortcutId,
|
||||||
|
source,
|
||||||
|
isEnabled,
|
||||||
|
shortcut);
|
||||||
|
|
||||||
// Disable the shortcut when empty or feature is disabled:
|
// Disable the shortcut when empty or feature is disabled:
|
||||||
await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty);
|
await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.LogInformation("Global shortcuts registration completed.");
|
this.logger.LogInformation("Global shortcuts registration completed (source='{Source}').", source);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.registrationSemaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetShortcutValue(Shortcut name) => name switch
|
private string GetShortcutValue(Shortcut name) => name switch
|
||||||
@ -107,5 +151,31 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
|||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private async Task<ShortcutState> GetShortcutState(Shortcut shortcutId)
|
||||||
|
{
|
||||||
|
var shortcut = this.GetShortcutValue(shortcutId);
|
||||||
|
var isEnabled = this.IsShortcutAllowed(shortcutId);
|
||||||
|
if (isEnabled && !string.IsNullOrWhiteSpace(shortcut))
|
||||||
|
return new(shortcut, true, false);
|
||||||
|
|
||||||
|
if (!this.IsWithinStartupRecoveryWindow() || 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);
|
||||||
|
if (!fallbackEnabled || string.IsNullOrWhiteSpace(fallbackShortcut))
|
||||||
|
return new(shortcut, isEnabled, false);
|
||||||
|
|
||||||
|
return new(fallbackShortcut, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsWithinStartupRecoveryWindow() => DateTimeOffset.UtcNow - this.serviceStartedAt <= STARTUP_RECOVERY_WINDOW;
|
||||||
|
|
||||||
|
private readonly record struct ShortcutState(string Shortcut, bool IsEnabled, bool UsesPersistedFallback);
|
||||||
|
|
||||||
public static void Initialize() => IS_INITIALIZED = true;
|
public static void Initialize() => IS_INITIALIZED = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 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 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 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 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 readability by removing non-readable special characters from log entries.
|
||||||
- Improved the logbook reliability by significantly reducing duplicate log entries.
|
- Improved the logbook reliability by significantly reducing duplicate log entries.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user