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."); }