Skip to content

feat: clear the "another app" warning once the other app is gone - #20

Merged
timwata merged 2 commits into
mainfrom
feat/contention-3-active-check
Oct 10, 2026
Merged

timwata merged 2 commits into
mainfrom
feat/contention-3-active-check

Conversation

@timwata

@timwata timwata commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Phase 3 of design/pad-contention.md (option C, part 2). Builds on the registry scan from #19.

What

Until now, once the "another app is driving this pad" warning came up, it stayed until Recheck or an off/on toggle, even after the other app had quit. Now the bridge also asks the IORegistry who else holds the pad, so the warning names the other app and clears by itself.

  • contendingClient is still the one flag the panel reads. It now comes from one place, updateContention(), which combines two signals: trafficSeen (a reply id we never issued, the old flag renamed) and the last HIDClientScan:
    • .unavailable (emulator, failed walk): the replies alone decide, as in Phase 1.
    • .authoritative (the USB vendor interface): any other client raises the warning by itself.
    • .advisory (Bluetooth, where the device also carries the keyboard): the list only names the apps; it cannot raise the warning.
    • An empty list from either clears trafficSeen. This only happens when a scan runs, so a reply that lands after an empty scan still raises the warning.
  • When scans run: when the panel opens (scanContention(), from both existing hooks), on Recheck (clear traffic, scan, repaint), after openDevice() connects, and after a foreign reply, at most once a second. Never while off.
  • contenders is published for the panel copy: "Also driving this pad: Input — colours may fight." The old text is kept when no names are known. Recheck stays, with updated help text.
  • Beyond the plan: unplugging the pad drops the last scan, and the reopen scans again.
  • README.md and CLAUDE.md describe both signals and the Bluetooth caveat.

Tests

  • BridgeContentionTests gains 12 tests on the virtual pad. The scanClients and uptime seams are stubbed. They cover every row of the table, the one-per-second limit on reply-triggered scans, naming the sender of a reply, the scan on open, Recheck, the off bridge and the unplug. The emulator's real scan is .unavailable, so the Phase 1 tests still check Phase 1 behaviour unchanged.
  • Every test mutation-checked (13 mutations, each caught; listed in the plan's Phase 3 Notes).
  • swift build -c release: zero warnings
  • env -u HERDR_SOCKET_PATH swift test: 226 tests, 0 failures (2 remote live tests skipped)

User checks

Not done in-session (no hardware or UI check):

  • Open the Inspector from the panel → the warning names it.
  • Quit the Inspector, reopen the panel → the warning is gone without pressing anything.
  • The same with Work Louder's Input app or the Codex desktop app, if installed.
  • Bluetooth is still unverified (see the plan's Phase 2 Notes): with the pad on Bluetooth, keyboard listeners such as Discord may be named, but must not raise the warning on their own.

🤖 Generated with Claude Code

The bridge now combines two signals in updateContention(): foreign reply
ids (trafficSeen) and an IORegistry scan of who else holds the pad. On the
USB vendor interface the scan raises the warning by itself and clears it
once nobody else is there; over Bluetooth it only names the apps. Scans run
on panel open, Recheck, device open and after a foreign reply (at most once
a second), never while off. The panel names the other apps.

An empty scan clears trafficSeen only when it runs, so a reply that lands
after it still raises the warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timwata

timwata commented Oct 10, 2026

Copy link
Copy Markdown
Owner Author

Review: high / medium findings

Overall the implementation follows the table in design/pad-contention.md: updateContention() is the one place the flag is decided, trafficSeen is cleared only when a scan runs (as the design Notes explain), and gating on isRunning && deviceConnected keeps Phase 1's "off bridge shows nothing" rule. No high-severity findings. One medium finding below; low-severity and nit items are left out of this comment.

🟠 Medium — A process that holds the vendor interface but never sends now raises a warning that nothing can dismiss

Where: Sources/WLKit/BridgeController.swift, updateContention(), the .authoritative case:

contendingClient = trafficSeen || !others.isEmpty

Problem: On .authoritative, any other process that has the vendor interface open raises the warning on its own. Before this PR, a passive holder caused no warning. Phase 2 observed a single machine, where only MicroManager held the vendor interface. Some apps open every HID device (IOHIDManagerSetDeviceMatching(manager, NULL) or broad matching), such as input remappers, game launchers, and macro or streaming tools. On a machine running one of them, the warning becomes permanent:

  • Recheck clears trafficSeen but then rescans, and the same non-empty list raises the warning again at once.
  • Switching off and on doesn't help, because openDevice() scans too.
  • The panel names the app, but the only way to clear the warning is to quit that app, even though it never touches the lighting.

Status: Not verified on hardware. This comes from reading the code and from how IOKit HID matching works: a manager with nil matching opens the 0xFF00 vendor interface like any other IOHIDDevice. It is worth confirming with a broad-matching HID utility, if one is available, before picking a fix.

Suggested fixes (smallest first):

  1. Document only. Add the limitation to README.md ("Only one bridge at a time") and to the Phase 3 Notes in design/pad-contention.md, plus a user check in the PR: run with a broad-matching HID utility installed and see whether it is named and whether the warning can be cleared.
  2. Let Recheck acknowledge the current list (recommended). On recheckContention(), remember the pids of the current .authoritative others (e.g. acknowledgedPIDs: Set<pid_t>) and leave them out of the raise rule, so that only unacknowledged others raise:
    contendingClient = trafficSeen || others.contains { !acknowledgedPIDs.contains($0.pid) }
    Acknowledged clients are still listed in contenders (named when the warning is up for another reason), and foreign replies still raise the warning through trafficSeen. Prune pids that no longer appear in a scan, so a relaunched app (with a new pid) raises again, and reset the set in resetContention() (start() / stop()). Update the Recheck help text, CLAUDE.md's "Only one HID client at a time" bullet and the design doc to match. Tests in BridgeContentionTests with the scanClients seam: Recheck with .authoritative([inspector]) clears the warning; a later scan still listing the Inspector keeps it clear; a new pid raises it; a foreign reply still raises it; stop() / start() forget the acknowledgement; mutation-check each, as the plan's rules require.
  3. Make a non-empty authoritative list name only, as advisory does. Simplest, but it drops the "idle client is seen" benefit that motivated option C, so probably too far.

An app that opens every HID device (an input remapper, a macro or
streaming tool) sits on the vendor interface without touching the
lighting. Each authoritative scan listed it, so the warning could not be
cleared short of quitting that app: Recheck and an off/on toggle both
rescan and find it again.

Recheck now records the pids its scan lists, and only other clients
raise the warning. Acknowledged clients stay named, still raise it by
sending, and are forgotten when a scan no longer lists them (so a
relaunch raises again) or the bridge stops or starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timwata
timwata merged commit eefb8e2 into main Oct 10, 2026
1 check passed
@timwata
timwata deleted the feat/contention-3-active-check branch October 10, 2026 03:34
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