diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7aa78b1..f01f5199 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,20 @@ That paragraph is not decoration: the release workflow copies each entry verbati
the GitHub release body and the announcement discussion, so it is the first thing a
prospective user reads. CI fails a pull request whose newest entry is missing it.
+## [1.76.9] - 2026-09-04
+
+In Volume Control, sending an app to a particular speaker or headset looked like it had not worked: the "Choose
+a device" prompt stayed printed over the name you picked, and about ten seconds later the box emptied itself.
+The sound really was going where you sent it — only the screen had forgotten.
+
+### Fixed
+- **Volume Control keeps showing the output device you picked.** The picker shows a "Choose a device" prompt
+ while SysManager cannot read where Windows is sending an app, which is every app today because Windows does
+ not report it. Choosing a device did route the app, but it did not clear that prompt, so the prompt was drawn
+ on top of the chosen name — and the tab's ten-second device re-read then treated the app as unrouted and
+ blanked the box, discarding the only record of what the user had set. Picking a device now counts as knowing
+ the route, because SysManager is what set it.
+
## [1.76.8] - 2026-09-03
Quick Cleanup's "Clean TEMP" could delete files that other running programs were relying on, and the amount it
diff --git a/SysManager/SysManager.Tests/AudioMixerViewModelTests.cs b/SysManager/SysManager.Tests/AudioMixerViewModelTests.cs
index dad109d8..d5b316f7 100644
--- a/SysManager/SysManager.Tests/AudioMixerViewModelTests.cs
+++ b/SysManager/SysManager.Tests/AudioMixerViewModelTests.cs
@@ -289,6 +289,58 @@ public async Task AnUnknownRoute_StaysUnknownAcrossADeviceRefresh()
Assert.Empty(writes); // and re-applying a snapshot must never write back
}
+ ///
+ /// Picking a device makes the route KNOWN — SysManager is the one that just set it.
+ ///
+ ///
+ /// The flag stayed set after a successful write, and the placeholder TextBlock shares its Grid cell with
+ /// the ComboBox at Margin="10,0,0,0" — exactly where the box draws its selected item — so
+ /// "Choose a device" was painted over the name the user had just chosen.
+ ///
+ [Fact]
+ public void AUserPickedDevice_IsNoLongerAnUnknownRoute()
+ {
+ var writes = new List<(string Session, string Device)>();
+ using var vm = NewVm(RoutableService(writes, route: null));
+ var row = vm.Sessions.Single();
+ Assert.True(row.OutputRouteUnknown, "the row should start unknown — the route read is a stub");
+
+ row.SelectedOutputDevice = row.OutputDevices.Single(d => d.Id == "{hdst}");
+
+ Assert.False(row.OutputRouteUnknown,
+ "the write succeeded, so the route is not unreadable any more — SysManager set it. While the flag "
+ + "stays up the placeholder is drawn on top of the device the user picked.");
+ Assert.Equal([("s1", "{hdst}")], writes);
+ }
+
+ ///
+ /// A device the user picked must still be selected after the device list is re-read.
+ ///
+ ///
+ /// The other half of the same defect, and the damaging half. RefreshDevicesAsync snapshots each row
+ /// as OutputRouteUnknown ? null : id, so a row still flagged unknown after a hand-made pick
+ /// snapshotted as null and was re-applied as null — the picker went blank on the tenth reconcile pass,
+ /// about ten seconds after the choice. Windows kept the route; only the UI forgot, which leaves the user
+ /// with no way to see or undo what they set. Introduced with the three-state flag in v1.76.7: before it,
+ /// the snapshot read the selection directly and a hand-made pick survived.
+ /// Twelve passes for the reason the neighbouring test gives: the refresh fires ON the tenth.
+ ///
+ [Fact]
+ public async Task AUserPickedDevice_SurvivesADeviceRefresh()
+ {
+ var writes = new List<(string Session, string Device)>();
+ using var vm = NewVm(RoutableService(writes, route: null));
+ var row = vm.Sessions.Single();
+
+ row.SelectedOutputDevice = row.OutputDevices.Single(d => d.Id == "{hdst}");
+
+ for (var pass = 0; pass < 12; pass++) await vm.ReconcileAsync();
+
+ Assert.Equal("{hdst}", row.SelectedOutputDevice?.Id);
+ Assert.False(row.OutputRouteUnknown);
+ Assert.Equal([("s1", "{hdst}")], writes); // re-applying the snapshot must not write again
+ }
+
private static IAudioMixerService RoutableService(
List<(string Session, string Device)> writes, string? route = null)
{
diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj
index 58627832..8adb4fed 100644
--- a/SysManager/SysManager/SysManager.csproj
+++ b/SysManager/SysManager/SysManager.csproj
@@ -10,9 +10,9 @@
SysManager
true
NU1603;NU1701
- 1.76.8
- 1.76.8.0
- 1.76.8.0
+ 1.76.9
+ 1.76.9.0
+ 1.76.9.0
SysManager
SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.
https://github.com/laurentiu021/SystemManager
diff --git a/SysManager/SysManager/ViewModels/AudioSessionRowViewModel.cs b/SysManager/SysManager/ViewModels/AudioSessionRowViewModel.cs
index 5e3d9c49..197472f1 100644
--- a/SysManager/SysManager/ViewModels/AudioSessionRowViewModel.cs
+++ b/SysManager/SysManager/ViewModels/AudioSessionRowViewModel.cs
@@ -75,6 +75,10 @@ public sealed partial class AudioSessionRowViewModel : ObservableObject
/// Not derived from being null, although today the two agree. A user
/// who opens the picker and closes it without choosing leaves the selection null, and that is not the
/// same claim: this flag says the SERVICE could not tell us, which is what the placeholder is about.
+ /// Two writers, and both are needed. sets it from what the
+ /// read produced; clears it after a successful write. Without
+ /// the second one an app the user routed by hand stayed flagged unreadable, which drew the placeholder over
+ /// the chosen name and made the parent's refresh snapshot discard the choice ten seconds later.
///
[ObservableProperty] private bool _outputRouteUnknown;
@@ -213,11 +217,16 @@ partial void OnIsMutedChanged(bool value)
/// choice on failure — reverting it would fight the user's own click — but the status now SAYS the
/// routing did not take, which the old comment promised ("left to the parent VM's status") without
/// anything ever reporting it.
+ /// A successful write also settles : the route is no longer
+ /// unreadable once SysManager is the one that set it. Failure leaves the flag alone, because a refused
+ /// write moved nothing — whatever the route was before, it still is.
///
partial void OnSelectedOutputDeviceChanged(AudioDevice? value)
{
if (_suppressPropagation || !RoutingSupported || value is null) return;
- if (!_service.SetSessionOutputDevice(SessionId, value.IsDefault ? string.Empty : value.Id))
+ if (_service.SetSessionOutputDevice(SessionId, value.IsDefault ? string.Empty : value.Id))
+ OutputRouteUnknown = false;
+ else
_reportFailure?.Invoke(
$"Could not move {DisplayName} to {value.FriendlyName} — Windows refused the change.");
}