diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor b/app/MindWork AI Studio/Layout/MainLayout.razor
index 7ed77410..f73bfb3b 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor
@@ -102,4 +102,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index 801ce2f6..95f6c584 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -144,7 +144,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
// Send a message to start the plugin system:
await this.MessageBus.SendMessage(this, Event.STARTUP_PLUGIN_SYSTEM);
- await this.themeProvider.WatchSystemDarkModeAsync(this.SystemeThemeChanged);
+ await this.themeProvider.WatchSystemDarkModeAsync(this.SystemThemeChanged);
+ this.CircuitState.ConnectionRestored += this.OnConnectionRestored;
await this.UpdateThemeConfiguration();
this.LoadNavItems();
this.LoadEmbeddingItem();
@@ -564,15 +565,45 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
}
}
- private async Task SystemeThemeChanged(bool isDark)
+ ///
+ /// True, when the user wants AI Studio to follow the light or dark mode of the operating system.
+ ///
+ ///
+ /// This also decides whether the MudThemeProvider watches the operating system at all. On a system
+ /// change, the provider takes the new mode into its own state first and calls our handler only
+ /// afterward, so the handler cannot prevent it. Nor can a new render of this layout undo it: the
+ /// provider takes over its IsDarkMode parameter only when that value changes, and with a fixed theme,
+ /// it never does. Were the provider watching while the user chose a fixed theme, MudBlazor would show
+ /// the colors of the system from the next render on, while the rest of the app kept the chosen ones.
+ ///
+ private bool FollowSystemTheme => this.SettingsManager.ConfigurationData.App.PreferredTheme is Themes.SYSTEM;
+
+ private async Task SystemThemeChanged(bool isDark)
{
this.Logger.LogInformation($"The system theme changed to {(isDark ? "dark" : "light")}.");
await this.UpdateThemeConfiguration();
}
+ ///
+ /// Reads the color theme anew once the browser connection of this circuit returned.
+ ///
+ ///
+ /// 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.
+ ///
+ /// 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.
+ ///
+ private void OnConnectionRestored()
+ {
+ this.InvokeAsync(this.UpdateThemeConfiguration).Observe($"{nameof(MainLayout)}: reading the color theme after the connection returned");
+ }
+
private async Task UpdateThemeConfiguration()
{
- if (this.SettingsManager.ConfigurationData.App.PreferredTheme is Themes.SYSTEM)
+ if (this.FollowSystemTheme)
this.useDarkMode = await this.themeProvider.GetSystemDarkModeAsync();
else
this.useDarkMode = this.SettingsManager.ConfigurationData.App.PreferredTheme == Themes.DARK;
@@ -664,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();
}
diff --git a/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs b/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs
index 631d1dc8..a3cabfe8 100644
--- a/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs
+++ b/app/MindWork AI Studio/Tools/Services/CircuitStateService.cs
@@ -28,6 +28,18 @@ public sealed class CircuitStateService
///
public string CircuitId { get; private set; } = "n/a";
+ ///
+ /// Occurs when the browser connection returned after it was lost.
+ ///
+ ///
+ /// 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.
+ ///
+ public event Action? ConnectionRestored;
+
///
/// Called by the circuit handler when the circuit was opened.
///
@@ -37,7 +49,17 @@ public sealed class CircuitStateService
///
/// Called by the circuit handler when the browser connection was established or restored.
///
- public void MarkAsConnected() => this.isConnected = true;
+ ///
+ /// 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.
+ ///
+ public void MarkAsConnected()
+ {
+ var wasConnected = this.isConnected;
+ this.isConnected = true;
+ if (!wasConnected)
+ this.ConnectionRestored?.Invoke();
+ }
///
/// Called by the circuit handler when the browser connection was lost or the circuit ended.
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md
index f707ca40..61ddc6b8 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md
@@ -87,5 +87,7 @@
- Fixed AI Studio trying for minutes when a provider turns a request down for good. Such an answer does not change by asking a second time, so AI Studio now stops at the first one and tells you what the provider said about it.
- 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.
diff --git a/app/Tests/Tools/CircuitStateServiceTests.cs b/app/Tests/Tools/CircuitStateServiceTests.cs
new file mode 100644
index 00000000..7f414f68
--- /dev/null
+++ b/app/Tests/Tools/CircuitStateServiceTests.cs
@@ -0,0 +1,71 @@
+using AIStudio.Tools.Services;
+
+namespace AIStudio.Tests.Tools;
+
+///
+/// Checks when the circuit state reports that the browser connection came back.
+///
+///
+/// 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.
+///
+[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.");
+ }
+}
\ No newline at end of file