Skip to content

fix(gui): keep asleep devices and their panels in the device list#224

Merged
AprilNEA merged 1 commit into
masterfrom
fix/identity-driven-device-list
Jun 13, 2026
Merged

fix(gui): keep asleep devices and their panels in the device list#224
AprilNEA merged 1 commit into
masterfrom
fix/identity-driven-device-list

Conversation

@AprilNEA

Copy link
Copy Markdown
Owner

Problem

The device list was rebuilt purely from the live inventory snapshot. A device that is asleep at GUI launch — or simply hasn't won its probe race yet — doesn't appear in that snapshot, so its card (and its Pointer/Buttons config panels) vanish until a cold probe happens to complete. Users read this as "my mouse disappeared from the app" (#159).

Fix

Persist a per-device identity and build the list as the union of live and known devices:

  • New DeviceIdentity { display_name, kind, capabilities } persisted under the device's config block. Every field is a static property of the model (an MX Master 3S has adjustable DPI whether or not it's awake), so it never goes stale; it carries no per-unit identifier beyond the config_key already used as the map key.
  • persist_identities records the identity of every online, fully-probed device on each snapshot (change-guarded, so quiet ticks don't touch the disk). Only measured capabilities are recorded — never a presumed fallback — so a placeholder can't persist empty panels.
  • build_device_list appends an offline placeholder for every known device absent from the live snapshot: capabilities from the persisted measurement (keeps the config panels visible), route: None (keeps every hardware write a no-op until the device wakes and the live inventory supplies the real route).
  • The refresh guard now also compares online and capabilities, so a device waking up — or a probe resolving its feature table on a stable route — refreshes the carousel instead of being swallowed by the unchanged-check.

Rebased onto current master: the identity field is threaded through the RawDeviceConfig deserialization shim introduced by the binding unification, and the list-refresh logic is merged with the asset-sync force rebuild.

Fixes #159

Tests

  • Config round-trip + iteration test for DeviceIdentity (also proves the RawDeviceConfig mapping).
  • Union-rule tests: offline placeholder is present-but-inert; known devices are appended only when absent from live.
  • cargo fmt / clippy --workspace --all-targets -D warnings / full test suite green.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes issue #159 where devices asleep at GUI launch (or not yet probed) would vanish from the device carousel along with their Pointer/Buttons config panels. The fix adds a DeviceIdentity struct persisted per-device in config.toml and builds the carousel as the union of the live inventory snapshot and every previously-seen device, so the panel layout is never gated on a probe completing.

  • DeviceIdentity (name, kind, capabilities) is persisted under each device's config block by persist_identities, which only records online, fully-probed devices and guards against unnecessary disk writes; the RawDeviceConfig deserialization shim is extended to thread the new field through.
  • build_device_list now accepts &Config and calls append_offline_known to append an offline placeholder (capabilities from persisted measurement, route: None) for every known device absent from the live inventory snapshot.
  • The unchanged guard in refresh_inventories is widened to compare online and capabilities in addition to config_key and route, ensuring a device waking up or a probe resolving its feature table on a stable route still triggers a carousel refresh.

Confidence Score: 5/5

Safe to merge; the core logic is well-tested, the new Config methods are additive, and the serde round-trip is exercised by the added test.

The change is purely additive — new config field with serde(default, skip_serializing_if) so existing configs round-trip unchanged, the union logic is unit-tested at both the record level and the list level, and every hardware-write path is kept safe because offline placeholders carry route: None. The one behavioral nuance (grace period bypass for persisted devices) affects UI flicker during rare transient probe misses rather than correctness.

No files require special attention; the grace-period interaction in state/devices.rs is worth considering for a follow-up but does not block this fix.

Important Files Changed

Filename Overview
crates/openlogi-core/src/config.rs Adds DeviceIdentity struct and Config methods for persisting/querying per-device identity; identity field threaded through DeviceConfig and RawDeviceConfig deserialization shim; roundtrip test added.
crates/openlogi-gui/src/state.rs Adds persist_identities function; threads config into build_device_list calls; expands the unchanged guard to compare online and capabilities in addition to config_key and route.
crates/openlogi-gui/src/state/devices.rs build_device_list gains config parameter and calls append_offline_known to union live inventory with persisted known devices; offline_record synthesizes a placeholder with capabilities from persisted identity but route: None; unit tests cover the offline-placeholder contract and union semantics.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[inventory snapshot arrives] --> B[build_device_list]
    B --> C{device in live inventory?}
    C -->|yes| D[live DeviceRecord online=true, route=Some]
    C -->|no| E{persisted identity?}
    E -->|yes| F[offline_record online=false, route=None, capabilities from identity]
    E -->|no| G[device absent from list]
    D --> H[append_offline_known skips]
    F --> I[appended to list]
    H --> J[sort_device_list]
    I --> J
    J --> K[merge_inventory_snapshot]
    K --> L[persist_identities online only]
    L --> M{identity changed?}
    M -->|yes| N[config.save_atomic]
    M -->|no| O[no-op]
    L --> P{unchanged check}
    P -->|unchanged| Q[return false]
    P -->|changed| R[refresh carousel]
Loading

Reviews (2): Last reviewed commit: "fix(gui): keep asleep devices and their ..." | Re-trigger Greptile

Comment on lines +123 to +137
fn offline_record(config_key: &str, identity: &DeviceIdentity) -> DeviceRecord {
DeviceRecord {
config_key: config_key.to_string(),
display_name: identity.display_name.clone(),
asset: None,
serial_number: None,
unit_id: [0; 4],
route: None,
kind: identity.kind,
capabilities: Some(identity.capabilities),
slot: 0,
online: false,
battery: None,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Offline placeholder sort-key instability in multi-device setups

offline_record sets unit_id: [0; 4], serial_number: None, slot: 0, and route: None. device_order_key feeds all four of these directly into DeviceStableId::from_parts, so an offline placeholder produces a completely different sort key from the same device when it was online. In a multi-device carousel (e.g. two Bolt-paired mice), the sleeping device can swap positions relative to the remaining online devices every time it transitions between live and offline — the online flag in the unchanged guard ensures the carousel re-renders, but the position change can be visually jarring. Adding serial_number: Option<String> and unit_id: [u8; 4] to DeviceIdentity and threading them through offline_record would keep the sort key stable across sleep transitions.

Fix in Codex Fix in Claude Code

A device that is asleep or not yet re-probed at agent cold start could
vanish from the carousel entirely: the whole DeviceRecord hangs off a live
HID++ 0x0003 read, so when that probe loses its race the device — and its
DPI / button panels — disappear until a later launch happens to probe it
while awake (#159).

Persist each device's static identity (display name, kind, measured
capabilities) to config.toml while it is online, and build the carousel as
the union of the live inventory and these known devices. A known device is
now always shown — with the right panels — and the live probe only enriches
it (online state, battery, asset photo) instead of gating whether it appears.

- core/config: add DeviceIdentity + a per-device `identity`, with accessors.
- gui/state: seed offline placeholders from known identities; record identity
  for every online, fully-probed device (writing only on change); compare
  online + capabilities in the refresh guard so a late-resolving panel
  surfaces in-session instead of waiting for a route change.

Capabilities are a static property of the model, never of the current
connection, so caching them is safe and never goes stale; no per-unit id is
stored, so it adds no privacy surface beyond the config_key already used as
the map key.
@AprilNEA AprilNEA force-pushed the fix/identity-driven-device-list branch from 64ec25e to 3416088 Compare June 13, 2026 13:30
@AprilNEA AprilNEA merged commit 570253b into master Jun 13, 2026
8 checks passed
@AprilNEA AprilNEA deleted the fix/identity-driven-device-list branch June 13, 2026 14:23
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.

[Bug]: Cannot adjust DPI for MX Master 3S

1 participant