# Cursor Flasher — Design Document **Date:** 2026-03-10 **Status:** Approved ## Problem When Cursor's AI agent finishes its turn and is waiting for user input (approval, answering a question, or continuing the conversation), there's no visual or audible signal. The user has to keep checking the window manually. ## Solution A macOS background daemon that monitors Cursor's accessibility tree and displays a pulsing border overlay around the Cursor window when the agent is waiting for user input. Optionally plays a system sound. ## Architecture ### Detection: Accessibility Tree Polling A Python process using `pyobjc` polls Cursor's accessibility tree every ~500ms via `AXUIElement` APIs. **Detection signals:** - **Approval needed:** Accept/Reject button elements appear in the a11y tree - **Agent turn complete:** Stop/Cancel button disappears, or thinking indicator goes away - **Chat input active:** Chat text area becomes the focused element after being inactive **State machine:** - `IDLE` — not monitoring (Cursor not in chat or not running) - `AGENT_WORKING` — agent is generating (Stop button visible, thinking indicator present) - `WAITING_FOR_USER` — agent is done, user hasn't interacted yet → **trigger flash** - `USER_INTERACTING` — user started typing/clicking → dismiss flash, return to IDLE The detection heuristics will be tuned during development after dumping Cursor's full a11y tree. ### Visual Effect: Native macOS Overlay - A borderless, transparent, non-interactive `NSWindow` positioned over Cursor's window frame - Draws only a pulsing border (interior is fully click-through) - Core Animation drives the pulse: opacity oscillates between configurable min/max - Default: ~4px amber border, 1.5s cycle **Dismissal triggers:** - Keyboard input to Cursor (via accessibility or `CGEventTap`) - Mouse click in Cursor's chat area - Timeout (default 5 minutes) - Agent starts working again (Stop button reappears) ### Sound On transition to `WAITING_FOR_USER`, optionally plays a macOS system sound (default: "Glass"). Configurable sound name, volume, and on/off toggle. ### Configuration File: `~/.cursor-flasher/config.yaml` ```yaml pulse: color: "#FF9500" width: 4 speed: 1.5 opacity_min: 0.3 opacity_max: 1.0 sound: enabled: true name: "Glass" volume: 0.5 detection: poll_interval: 0.5 cooldown: 3.0 timeout: auto_dismiss: 300 ``` ### Process Management - CLI: `cursor-flasher start`, `cursor-flasher stop`, `cursor-flasher status` - Runs as a background daemon - No menu bar icon (MVP scope) ## Tech Stack - Python 3.10+ - `pyobjc-framework-Cocoa` — NSWindow, NSApplication, Core Animation - `pyobjc-framework-Quartz` — AXUIElement, CGEventTap, window management - `PyYAML` — configuration file parsing ## Installation - `pip install -e .` from the project directory - Requires Accessibility permission: System Settings > Privacy & Security > Accessibility (grant to Terminal or Python) ## Scope / Non-goals - **In scope:** Detection, overlay, sound, CLI, config file - **Not in scope (MVP):** Menu bar icon, auto-start on login, multi-monitor awareness, Linux/Windows support ## Risks - **A11y tree fragility:** Cursor UI updates could change element names/structure, breaking detection. Mitigation: make detection patterns configurable, log warnings on detection failures. - **Accessibility permissions:** Users must grant permission manually. Mitigation: clear error message and instructions on first run. - **Performance:** Polling a11y tree every 500ms could have CPU cost. Mitigation: only poll when Cursor is the frontmost app or recently active.