feat: per-window detection — only flash windows needing attention

Detector now walks each AXWindow subtree independently and returns
both aggregate signals (for state machine) and a list of AXWindow
element refs for windows with active approval signals.

Overlay reads position/size directly from AXWindow elements via
AXValueGetValue, eliminating the CGWindowList dependency (which
returned empty names for Electron windows anyway).

Daemon passes only the active AXWindow refs to the overlay, so
only the specific window(s) waiting for user input get flashed.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 02:54:15 -04:00
parent bce6ec39f8
commit b31f39268e
5 changed files with 152 additions and 78 deletions

View File

@@ -46,9 +46,9 @@ class FlasherDaemon:
self._running = False
def _tick(self) -> None:
signals = self.detector.poll()
result = self.detector.poll()
if signals is None:
if result is None:
if self.state_machine.state == FlasherState.WAITING_FOR_USER:
self.state_machine.dismiss()
self.overlay.hide()
@@ -56,8 +56,8 @@ class FlasherDaemon:
return
changed = self.state_machine.update(
agent_working=signals.agent_working,
approval_needed=signals.approval_needed,
agent_working=result.signals.agent_working,
approval_needed=result.signals.approval_needed,
)
if not changed:
@@ -74,9 +74,8 @@ class FlasherDaemon:
match self.state_machine.state:
case FlasherState.WAITING_FOR_USER:
pid = self.detector._pid
if pid is not None:
self.overlay.show(pid)
if result.active_windows:
self.overlay.show(result.active_windows)
play_alert(self.config)
self._waiting_since = time.monotonic()
case FlasherState.AGENT_WORKING | FlasherState.IDLE: