Fixed the color theme not following the system after waking from sleep

This commit is contained in:
Thorsten Sommer 2026-09-22 22:20:42 +02:00
parent 50c686aa9d
commit fde10ef2ba
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
4 changed files with 114 additions and 1 deletions

View File

@ -145,6 +145,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
await this.MessageBus.SendMessage<bool>(this, Event.STARTUP_PLUGIN_SYSTEM);
await this.themeProvider.WatchSystemDarkModeAsync(this.SystemThemeChanged);
this.CircuitState.ConnectionRestored += this.OnConnectionRestored;
await this.UpdateThemeConfiguration();
this.LoadNavItems();
this.LoadEmbeddingItem();
@ -583,6 +584,23 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
await this.UpdateThemeConfiguration();
}
/// <summary>
/// Reads the color theme anew once the browser connection of this circuit returned.
/// </summary>
/// <remarks>
/// The browser reports a change of the system theme exactly once. Blazor drops that report while the
/// connection is down, which happens when the machine switches its theme during sleep and wakes up
/// again. Since the circuit survives the sleep (cf. the retention settings in Program.cs), no reload
/// reads the theme anew either, so AI Studio would keep the theme it had before the sleep.
/// <br/><br/>
/// The update is deliberately not awaited: this handler runs while Blazor is still completing the
/// reconnection, and the answer to the JavaScript call inside can only arrive afterward.
/// </remarks>
private void OnConnectionRestored()
{
this.InvokeAsync(this.UpdateThemeConfiguration).Observe($"{nameof(MainLayout)}: reading the color theme after the connection returned");
}
private async Task UpdateThemeConfiguration()
{
if (this.FollowSystemTheme)
@ -677,6 +695,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
public void Dispose()
{
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
this.CircuitState.ConnectionRestored -= this.OnConnectionRestored;
this.MessageBus.Unregister(this);
this.mandatoryInfoDialogSemaphore.Dispose();
}

View File

@ -28,6 +28,18 @@ public sealed class CircuitStateService
/// </summary>
public string CircuitId { get; private set; } = "n/a";
/// <summary>
/// Occurs when the browser connection returned after it was lost.
/// </summary>
/// <remarks>
/// It does not occur for the first connection of a circuit, only for the ones which follow a loss. Use it
/// to fetch again what the browser reports on its own: Blazor drops such reports while the connection is
/// down, and nothing sends them a second time. The event is raised while Blazor is still completing the
/// reconnection, though. A handler must not wait for JavaScript interop, because the browser's answer can
/// only be processed once the reconnection has finished. Start such work without awaiting it instead.
/// </remarks>
public event Action? ConnectionRestored;
/// <summary>
/// Called by the circuit handler when the circuit was opened.
/// </summary>
@ -37,7 +49,17 @@ public sealed class CircuitStateService
/// <summary>
/// Called by the circuit handler when the browser connection was established or restored.
/// </summary>
public void MarkAsConnected() => this.isConnected = true;
/// <remarks>
/// A restored connection raises ConnectionRestored. Blazor never runs the handler's events of one circuit
/// concurrently, so reading and writing the state in two steps is safe here.
/// </remarks>
public void MarkAsConnected()
{
var wasConnected = this.isConnected;
this.isConnected = true;
if (!wasConnected)
this.ConnectionRestored?.Invoke();
}
/// <summary>
/// Called by the circuit handler when the browser connection was lost or the circuit ended.

View File

@ -88,5 +88,6 @@
- Fixed errors about a provider arriving as two messages at once, the second of which spoke of several attempts that were never made. You now get the single message which names the cause.
- Fixed the button in the chat toolbar that deletes the current chat and starts a new one doing so without asking. It now asks for your confirmation first, just like the chat list does, because a deleted chat cannot be brought back. The button shows a delete icon in red now, instead of one that looked like a reload.
- Fixed AI Studio following your system into light or dark mode even though you had chosen a fixed color theme in the app settings.
- Fixed AI Studio keeping its previous color theme after your computer woke up from sleep, when your system had switched between light and dark mode during that time.
- Upgraded the Visual Briefing assistant (in preview) from the prototype to the beta state. The assistant is now completely implemented and is undergoing a deeper testing phase in preparation for release. To try it, open the app settings, allow preview features down to beta, and then enable the Visual Briefing assistant there.
- Upgraded the vector database behind local RAG (Qdrant Edge) to version 0.8.0.

View File

@ -0,0 +1,71 @@
using AIStudio.Tools.Services;
namespace AIStudio.Tests.Tools;
/// <summary>
/// Checks when the circuit state reports that the browser connection came back.
/// </summary>
/// <remarks>
/// The report is there to fetch again what the browser sent while the connection was down, because Blazor
/// drops it. The layout reads the color theme anew on it, for instance, since the machine may have switched
/// its theme during sleep. So it has to come after every loss, and only then: missing one leaves the app
/// with stale state, while a connection which was never lost has nothing to fetch again.
/// </remarks>
[TestFixture]
public sealed class CircuitStateServiceTests
{
[Test]
public void AConnectionWhichReturnsAfterALossIsReportedOnce()
{
var circuitState = new CircuitStateService();
var numReports = 0;
circuitState.ConnectionRestored += () => numReports++;
circuitState.MarkAsDisconnected();
circuitState.MarkAsConnected();
Assert.That(numReports, Is.EqualTo(1), "The connection was lost and came back, so whatever the browser sent in between is gone.");
Assert.That(circuitState.IsConnected, Is.True);
}
[Test]
public void TheFirstConnectionIsNotReported()
{
var circuitState = new CircuitStateService();
var numReports = 0;
circuitState.ConnectionRestored += () => numReports++;
circuitState.MarkAsConnected();
Assert.That(numReports, Is.Zero, "A circuit starts out connected, so its first connection has not lost anything.");
}
[Test]
public void AConnectionWhichWasNotLostIsNotReportedAgain()
{
var circuitState = new CircuitStateService();
var numReports = 0;
circuitState.ConnectionRestored += () => numReports++;
circuitState.MarkAsDisconnected();
circuitState.MarkAsConnected();
circuitState.MarkAsConnected();
Assert.That(numReports, Is.EqualTo(1), "The second call follows a connection which was up all along.");
}
[Test]
public void EveryLossIsReportedOnItsOwn()
{
var circuitState = new CircuitStateService();
var numReports = 0;
circuitState.ConnectionRestored += () => numReports++;
circuitState.MarkAsDisconnected();
circuitState.MarkAsConnected();
circuitState.MarkAsDisconnected();
circuitState.MarkAsConnected();
Assert.That(numReports, Is.EqualTo(2), "The machine went to sleep twice, and each time something may have been lost.");
}
}