Skip to content

feat: persist disconnected devices and add the Show disconnected filter - #48

Merged
hoobio merged 14 commits into
mainfrom
feat/devices-persistence-and-block-assembly
May 31, 2026
Merged

hoobio merged 14 commits into
mainfrom
feat/devices-persistence-and-block-assembly

Conversation

@hoobio

@hoobio hoobio commented May 31, 2026 •

Copy link
Copy Markdown
Owner

Implements docs/features/device-persistence-feature.md (all three parts) and docs/features/devices-block-assembly-rewrite.md.

What changed

  • Remembered devices. The Devices list is now the union of live endpoints and persisted-absent known devices. A disconnected device keeps its card (dimmed, controls disabled, a "Disconnected" pill, a "Forget device" right-click action) in its existing order / group slot, and reactivates in place on reconnect.
  • No more rebuild flash, and devices slide. Card instances are reconciled by a stable device key and reused across a rebuild (BuildCards returns plain snapshots off the UI thread; DeviceCard.RefreshFrom applies them in place), so connect/disconnect animates the block slide instead of recreating every element. The slide is now also wired to group members, so a device appearing in / leaving a group slides its neighbours.
  • Stable identity. Block order, groups, and per-device config are keyed by a device key (container id + flow, stable across a driver reinstall) instead of the volatile endpoint id. Migrated on first load with an awaited pre-migration settings.json backup; the rewrite completes convergently as absent devices reappear, so the stores end up holding only device keys with no runtime resolver. ADR: docs/adr/device-persistence-and-identity.md.
  • Show disconnected filter. The Devices visibility filter is now one view-model predicate (DeviceListFilter) keyed off intrinsic card facts, not per-card toggle state. A session-only "Show disconnected" toggle sits beside "Show hidden". The disconnected filter applies to group members too (a disconnected member hides when the toggle is off; a connected one still shows), opacity tiers (drag > disconnected > hidden-shown > normal) are explicit, and the empty state distinguishes "no devices" from "everything filtered out".
  • Bluetooth connect/disconnect button (Part 3). A Bluetooth render device's card shows a connect/disconnect button top-right of the header; it sends the same kernel-streaming one-shot Windows' Quick Settings uses (KSPROPSETID_BtAudio / ONESHOT_RECONNECT|DISCONNECT to the endpoint's BT KS filter via IKsControl). Detection is the same topology walk, cached + persisted so a disconnected BT device keeps its connect button.

Verified

  • Migration preserved every existing group / order slot / per-device config (before and after screenshots identical).
  • Disconnected card renders correctly (dimmed, "Disconnected" pill, disabled slider) - exercised via UI automation with a seeded known device.
  • The Bluetooth button renders on a real Bluetooth card (Sony WF-1000XM6); the name / id rows ellipsify to make room.
  • Rules still apply after a default-device change; no flash; 23 Earmark.Core unit tests pass (device-key computation incl. collisions, the filter predicate, the store re-key).

Notes

  • New dependency: Microsoft.Windows.CsWin32 (analyzer-only, PrivateAssets=all) so the Bluetooth COM/KS interop is generated from the Windows metadata rather than hand-maintained.
  • The Bluetooth one-shot is fire-and-attempt and runs off the UI thread; the card's connected state is settled by the device-arrival events, not the call's return. Detection + button are verified; the actual link toggle was not driven against live hardware (it would cut audio mid-session).
  • DeviceCard and HomeViewModel were over-long, so cohesive plain-member regions were split into partial files. Source-generated [ObservableProperty]/[RelayCommand] members had to stay in the main partial (the WinUI XAML markup compiler NREs when they are spread across files).
  • Device-level concerns (external volume/mute events, peak metering, per-app routing) stay keyed by the live endpoint id; only the persisted layout/identity moved to the device key.

feat(devices): add a Bluetooth connect/disconnect button on Bluetooth device cards

test(devices): cover device-key identity, list filter, and store re-key

refactor(devices): split oversized DeviceCard and HomeViewModel into partial files

hoobio added 14 commits May 31, 2026 23:26
…y migration

Add DeviceIdentity (container|flow key, stable across driver reinstall) and DeviceKeyStore
(convergent endpoint-id -> device-key re-key). Surface ContainerId + a Bluetooth flag on
AudioEndpoint, read from the endpoint property store. Add AppSettings.KnownDevices +
SettingsSchemaVersion. ADR records the identity model and Approach A (re-key, no alias layer).
…cted filter

Remember devices across disconnect (and driver reinstall): the card set is now the union of
live endpoints and persisted-absent known devices, reconciled by device key so surviving card
instances are reused across a rebuild - which is what lets the block-slide animate a connect/
disconnect instead of rebuilding the list (no flash). BuildCards returns plain snapshots off the
UI thread; DeviceCard.RefreshFrom applies them in place. A disconnected card renders dimmed with
its controls disabled, a Disconnected pill, and a Forget device action.

Rework the Devices assembly pipeline so the visibility filter is a single view-model predicate
(DeviceListFilter) keyed off intrinsic card facts, not per-card toggle state. Add a session-only
Show disconnected toggle beside Show hidden; define the opacity tiers (drag > disconnected >
hidden-shown > normal) explicitly on the card. Distinguish genuinely-empty from filtered-empty in
the empty-state copy.

Block order, group membership, and per-device config are now keyed by the stable device key
(migrated on load with a pre-migration backup); device-level concerns (volume/mute events, peak,
app routing) stay keyed by the live endpoint id.
21 tests for DeviceIdentity (container|flow keys, reinstall stability, multi-endpoint
discrimination, name fallback), DeviceListFilter (the group/hidden/disconnected truth table),
and DeviceKeyStore (convergent id->key rewrite, idempotency, dedupe). Scoped CA1707 off for the
test project so underscore-style fact names are allowed.
Move the icon / glyph / accent-tile / Wave Link theming + mute-lock tooltips out of DeviceCard.cs
into DeviceCard.Visuals.cs (1188 -> 960 + 244). Only plain members are moved: the WinUI XAML
markup compiler NREs (WMC9999) when source-generated [ObservableProperty] members are spread across
partial files, so those stay in the main partial.
…ial file

Move the contiguous app-indicator chip block (per-card reconcile, chip ordering, hidden-apps
lookup, hide/close/terminate) out of HomeViewModel.cs into HomeViewModel.Apps.cs (2516 -> 1977 +
556). Plain methods only; the source-generated [ObservableProperty]/[RelayCommand] members stay in
the main partial (XAML markup compiler constraint).
Address review findings:
- Take the one-time pre-migration backup awaited in RebuildAsync before any re-key, so it captures
  pre-migration state (it was fire-and-forget against the live settings and could snapshot
  already-migrated data).
- Disambiguate the no-container name-fallback device key too (append the endpoint-id tail), so two
  same-named virtual endpoints no longer collapse onto one key and drop a card. Covered by a test.
- Purge a forgotten / pruned device's order, group, and config footprint via one shared helper, so
  the known-devices cap / age-out no longer leaves orphaned store entries.
- Make Forget device a deliberate non-undoable action (it couldn't fully restore the row anyway).
A disconnected device no longer shows inside a group when Show disconnected is off. Group
membership now overrides only the hidden filter (a connected, rules-less member still shows), not
the disconnected filter, and SyncBlocks filters a group's displayed members through the same
predicate (a group with no member left to show after filtering doesn't render this build).
…side a group

The always-on block slide was only wired to the top-level list, so a device appearing in (or
leaving) a group snapped its neighbours into place instead of sliding them. Attach the same
implicit Offset animation to the group's inner member repeater via ElementPrepared, and have the
drag teardown restore that baseline rather than detaching it.
Part 3 of the persistence spec. A Bluetooth render device's card shows a connect/disconnect button
top-right of the header (the name / id rows ellipsify to make room); it sends the same kernel-
streaming one-shot Windows' Quick Settings uses - KSPROPSETID_BtAudio / ONESHOT_RECONNECT|DISCONNECT
to the endpoint's BT KS filter via IKsControl. The COM/KS interop is source-generated by CsWin32
(new dep, pre-approved) so the IDeviceTopology / IConnector / IKsControl vtables come from the
Windows metadata, not hand-written declarations. Detection is the same topology walk (does the
endpoint terminate in a {2}.\?\bth filter), cached and persisted so a disconnected BT device
keeps its connect button. The call is fire-and-attempt and runs off the UI thread; the card's
connected state still settles from the device-arrival events.
Captures the running app's window to a PNG for before/after UI comparison. Forces the window
foreground first (ALT-tap + AttachThreadInput) because GDI capture of a WinUI 3 window returns black
unless it's the foreground window (its swapchain is otherwise on a hardware overlay plane).
…specs

Both feature prompts are fully implemented on this branch (persistence Parts 1-3 + the block-
assembly rewrite), so the one-off implementation specs are no longer needed.
@hoobio
hoobio enabled auto-merge (squash) May 31, 2026 15:13
@hoobio
hoobio merged commit c6feb57 into main May 31, 2026
13 checks passed
@hoobio
hoobio deleted the feat/devices-persistence-and-block-assembly branch May 31, 2026 15:18
hoobio pushed a commit that referenced this pull request Jun 1, 2026
This PR was generated automatically by
[release-please](https://github.com/googleapis/release-please).
---


## [0.2.0](v0.1.9...v0.2.0)
(2026-06-01)


### ⚠ BREAKING CHANGES

* **rules:** pinned/one-shot actions, drag-and-drop, consolidated schema
([#49](#49))
* **devices:** rules.json schema replaced - actions and conditions now
use a Kind plus orthogonal mode fields (direction / membership / muted)
and a Pinned flag; the old type-per-row enum no longer deserialises.

### Features

* add "always show pinned apps" setting for rule-pinned chips
([dd387a6](dd387a6))
* add appearance backdrop setting (Mica, Acrylic, Solid)
([#39](#39))
([7f2769a](7f2769a))
* add Devices-page reset to default and new-install seeding
([baffea1](baffea1))
* card-height and card-divider options for the Devices page
([dd387a6](dd387a6))
* close and terminate an app from its chip menu, across all the
([dd387a6](dd387a6))
* device customisation panel (glyph + accent colour)
([#46](#46))
([34956eb](34956eb))
* Devices app chips, close/terminate, undo, and card options
([#44](#44))
([dd387a6](dd387a6))
* Devices page options menu (show title bar / show hidden / lock
([288d13a](288d13a))
* **devices:** add a Bluetooth connect/disconnect button on Bluetooth
([c6feb57](c6feb57))
* **devices:** add persisted device-card display toggles
([1ea0d9e](1ea0d9e))
* hide an app's chip on a single device from its chip context menu
([34956eb](34956eb))
* hide app chips via a context menu, manage from settings
([#41](#41))
([8e62187](8e62187))
* per-device accents, customise dialog, and Devices page options
([#47](#47))
([288d13a](288d13a))
* per-device app-chip indicators with drag-to-reroute on the Devices
([dd387a6](dd387a6))
* per-device deterministic accent colours, reworked customise
([288d13a](288d13a))
* persist disconnected devices and add the Show disconnected filter
([#48](#48))
([c6feb57](c6feb57))
* persist the navigation pane expand/collapse state across
([dd387a6](dd387a6))
* **rules:** flag matched-but-disconnected devices in the match chip
([1ea0d9e](1ea0d9e))
* **rules:** per-field match modes (regex / wildcard / exact) with a
([1ea0d9e](1ea0d9e))
* **rules:** pinned/one-shot actions, drag-and-drop, consolidated schema
([#49](#49))
([1ea0d9e](1ea0d9e))
* **rules:** warn when an action is superseded by a higher-priority rule
([1ea0d9e](1ea0d9e))
* undo app-chip hide, reorder, and group changes with Ctrl+Z
([dd387a6](dd387a6))


### Bug Fixes

* allow click-to-mute on the device icon when volume controls are
([34956eb](34956eb))
* always animate the Devices card reflow (apps row, device
([dd387a6](dd387a6))
* expanding a card's rules no longer shrinks a shorter sibling card
([dd387a6](dd387a6))
* glide app chips on re-sort by ranking, not collection Move
([dd387a6](dd387a6))
* keep the device accent tile coloured while a mute rule is active
([288d13a](288d13a))
* lay out Insights.Resource.dll for self-contained app notifications
([dd387a6](dd387a6))
* move a device directly from one group into another in one drag
([dd387a6](dd387a6))


### Documentation

* note the backdrop setting in the design-language section
([7f2769a](7f2769a))
* rewrite the rule schema reference for the consolidated model
([1ea0d9e](1ea0d9e))
* spec loopback / listening chip suppression heuristic
([#45](#45))
([16b4aad](16b4aad))


### Code Refactoring

* **devices:** split oversized DeviceCard and HomeViewModel into
([c6feb57](c6feb57))


### Tests

* cover rule matcher, device-rule resolver, and shadow analyzer
([1ea0d9e](1ea0d9e))
* **devices:** cover device-key identity, list filter, and store re-key
([c6feb57](c6feb57))


### Build System

* update dependencies to latest stable
([dd387a6](dd387a6))


### Continuous Integration

* use the release-please PR body for pre-release notes
([288d13a](288d13a))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: release-please-hoobi[bot] <279189756+release-please-hoobi[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant