Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions SysManager/SysManager.Tests/AudioMixerViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,58 @@ public async Task AnUnknownRoute_StaysUnknownAcrossADeviceRefresh()
Assert.Empty(writes); // and re-applying a snapshot must never write back
}

/// <summary>
/// Picking a device makes the route KNOWN — SysManager is the one that just set it.
/// </summary>
/// <remarks>
/// The flag stayed set after a successful write, and the placeholder TextBlock shares its Grid cell with
/// the ComboBox at <c>Margin="10,0,0,0"</c> — exactly where the box draws its selected item — so
/// "Choose a device" was painted over the name the user had just chosen.
/// </remarks>
[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);
}

/// <summary>
/// A device the user picked must still be selected after the device list is re-read.
/// </summary>
/// <remarks>
/// The other half of the same defect, and the damaging half. <c>RefreshDevicesAsync</c> snapshots each row
/// as <c>OutputRouteUnknown ? null : id</c>, 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.
/// <para>Twelve passes for the reason the neighbouring test gives: the refresh fires ON the tenth.</para>
/// </remarks>
[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)
{
Expand Down
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<RootNamespace>SysManager</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>NU1603;NU1701</NoWarn>
<Version>1.76.8</Version>
<FileVersion>1.76.8.0</FileVersion>
<AssemblyVersion>1.76.8.0</AssemblyVersion>
<Version>1.76.9</Version>
<FileVersion>1.76.9.0</FileVersion>
<AssemblyVersion>1.76.9.0</AssemblyVersion>
<Product>SysManager</Product>
<Description>SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.</Description>
<PackageProjectUrl>https://github.com/laurentiu021/SystemManager</PackageProjectUrl>
Expand Down
11 changes: 10 additions & 1 deletion SysManager/SysManager/ViewModels/AudioSessionRowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public sealed partial class AudioSessionRowViewModel : ObservableObject
/// Not derived from <see cref="SelectedOutputDevice"/> 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.
/// <para>Two writers, and both are needed. <see cref="SetOutputDeviceFromService"/> sets it from what the
/// read produced; <see cref="OnSelectedOutputDeviceChanged"/> 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.</para>
/// </remarks>
[ObservableProperty] private bool _outputRouteUnknown;

Expand Down Expand Up @@ -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.
/// <para>A successful write also settles <see cref="OutputRouteUnknown"/>: 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.</para>
/// </summary>
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.");
}
Expand Down