Restructure config for per-mode style/sound and fix pulse dismiss
Major changes: - Add StyleConfig dataclass with independent color, width, opacity, duration, pulse_speed, sound, and volume per mode (running/completed) - Replace flat flash_*/sound_*/play_on config with running: and completed: YAML sections - Replace CGEventTap (silently fails in forked daemon) with CGEventSourceSecondsSinceLastEventType polling for reliable input-based pulse dismissal when Cursor is already frontmost - Update overlay, sound, and daemon to pass StyleConfig per call - Rewrite tests for new config shape and dismiss mechanism Made-with: Cursor
This commit is contained in:
145
README.md
145
README.md
@@ -1,111 +1,104 @@
|
||||
# cursor-flasher
|
||||
|
||||
A macOS daemon that flashes a pulsing border around Cursor IDE windows when the AI agent is waiting for your input. Optionally plays a system sound.
|
||||
Flash a colored border on the Cursor IDE window when the AI agent needs your attention — tool approval, questions, or task completion.
|
||||
|
||||
## How It Works
|
||||
|
||||
Uses [Cursor hooks](https://cursor.com/docs/agent/hooks) for reliable detection:
|
||||
|
||||
- **`preToolUse`** — fires when the agent wants to run a shell command, write a file, or use any tool that may need approval. **Pulses** the border continuously and plays a sound until you click the Cursor window.
|
||||
- **`stop`** — fires when the agent loop ends. **Flashes** the border once, briefly.
|
||||
|
||||
Only tools in the `approval_tools` list trigger the pulse (default: `Shell`, `Write`, `Delete`). Auto-approved tools like `Read` and `Grep` are ignored.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS
|
||||
- Python 3.10+
|
||||
- [uv](https://docs.astral.sh/uv/) (recommended)
|
||||
- **Accessibility permission** granted to your terminal (System Settings → Privacy & Security → Accessibility)
|
||||
- [uv](https://docs.astral.sh/uv/)
|
||||
- Cursor IDE
|
||||
- Accessibility permission for your terminal (System Settings → Privacy & Security → Accessibility)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone and install
|
||||
git clone <repo-url> && cd cursor-flasher
|
||||
uv sync
|
||||
|
||||
# Install Cursor hooks (global, applies to all projects)
|
||||
uv run cursor-flasher install
|
||||
|
||||
# Start the daemon
|
||||
uv run cursor-flasher start
|
||||
```
|
||||
|
||||
The `install` command copies the hook script to `~/.cursor/hooks/` and adds entries to `~/.cursor/hooks.json`. Cursor auto-reloads hooks.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Verify accessibility permissions work from your terminal
|
||||
uv run cursor-flasher check
|
||||
|
||||
# Start the daemon (backgrounds automatically)
|
||||
uv run cursor-flasher start
|
||||
|
||||
# Start in foreground (useful for debugging)
|
||||
uv run cursor-flasher start --foreground
|
||||
|
||||
# Check if the daemon is running
|
||||
uv run cursor-flasher start # background daemon
|
||||
uv run cursor-flasher start --foreground # foreground (for debugging)
|
||||
uv run cursor-flasher status
|
||||
|
||||
# Stop the daemon
|
||||
uv run cursor-flasher stop
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Polls Cursor's macOS accessibility tree every 500ms
|
||||
2. Detects agent state by looking for specific UI elements (Stop/Accept/Reject buttons)
|
||||
3. When the agent finishes and is waiting for input, shows a pulsing amber border around **only the window(s) that need attention**
|
||||
4. Plays a system sound (default: "Glass")
|
||||
5. Dismisses automatically when the agent starts working again, or after a timeout
|
||||
|
||||
## Configuration
|
||||
|
||||
Create `~/.cursor-flasher/config.yaml` to customize:
|
||||
Optional config file at `~/.cursor-flasher/config.yaml`:
|
||||
|
||||
```yaml
|
||||
pulse:
|
||||
color: "#FF9500" # Border color (hex)
|
||||
width: 4 # Border thickness (px)
|
||||
speed: 1.5 # Pulse cycle duration (seconds)
|
||||
opacity_min: 0.3 # Minimum pulse opacity
|
||||
opacity_max: 1.0 # Maximum pulse opacity
|
||||
running: # approval pulse (continuous until you interact)
|
||||
color: "#FF9500" # border color (hex)
|
||||
width: 4 # border thickness in pixels
|
||||
opacity: 0.85 # max border opacity
|
||||
pulse_speed: 1.5 # pulse cycle speed in seconds
|
||||
sound: "Glass" # macOS system sound ("" to disable)
|
||||
volume: 0.5 # 0.0 to 1.0
|
||||
# sounds: Basso, Blow, Bottle, Frog, Funk, Glass, Hero,
|
||||
# Morse, Ping, Pop, Purr, Sosumi, Submarine, Tink
|
||||
|
||||
sound:
|
||||
enabled: true # Play sound on trigger
|
||||
name: "Glass" # macOS system sound name
|
||||
volume: 0.5 # Volume (0.0 - 1.0)
|
||||
completed: # agent stop flash (brief fade-in/out)
|
||||
color: "#00FF00" # different color for completion
|
||||
width: 4
|
||||
opacity: 0.85
|
||||
duration: 1.5 # flash duration in seconds
|
||||
sound: "" # no sound by default (Cursor plays its own)
|
||||
volume: 0.0
|
||||
|
||||
detection:
|
||||
poll_interval: 0.5 # Seconds between accessibility tree polls
|
||||
cooldown: 3.0 # Seconds before re-triggering after dismissal
|
||||
flash:
|
||||
mode: "screen" # "window", "screen", or "allscreens"
|
||||
|
||||
timeout:
|
||||
auto_dismiss: 300 # Auto-hide overlay after N seconds
|
||||
# Tools that trigger the pulse + sound (approval mode).
|
||||
# Others are silently ignored (e.g., Read, Grep, Glob, Task).
|
||||
approval_tools:
|
||||
- Shell
|
||||
- Write
|
||||
- Delete
|
||||
|
||||
general:
|
||||
approval_delay: 2.5 # seconds to wait before pulsing (filters auto-approvals)
|
||||
cooldown: 2.0 # minimum seconds between flashes
|
||||
```
|
||||
|
||||
All values are optional — defaults are used for anything not specified.
|
||||
Each mode (`running` and `completed`) has its own color, border style, and sound settings. Set `sound: ""` to disable sound for a particular mode.
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
uv run cursor-flasher uninstall
|
||||
uv run cursor-flasher stop
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot read accessibility tree" / no detection
|
||||
**Flashing on every tool call (too noisy):**
|
||||
- Edit `~/.cursor-flasher/config.yaml` and narrow down `approval_tools` to just `Shell`.
|
||||
|
||||
This is almost always an Accessibility permission issue. Run:
|
||||
**No flash at all:**
|
||||
- Check daemon: `uv run cursor-flasher status`
|
||||
- Check hooks installed: `ls ~/.cursor/hooks/cursor-flasher-notify.py`
|
||||
- Check Cursor Settings → Hooks tab for execution logs
|
||||
|
||||
```bash
|
||||
uv run cursor-flasher check
|
||||
```
|
||||
|
||||
If it reports a failure, your terminal app needs Accessibility permission:
|
||||
|
||||
1. Open **System Settings → Privacy & Security → Accessibility**
|
||||
2. Click the **+** button and add your terminal app (Terminal.app, Ghostty, iTerm2, etc.)
|
||||
3. Restart the terminal after granting permission
|
||||
|
||||
### Cursor not detected
|
||||
|
||||
Make sure Cursor is running. The daemon identifies it by bundle ID (`com.todesktop.230313mzl4w4u92`).
|
||||
|
||||
### Overlay appears on wrong windows
|
||||
|
||||
Detection is per-window — only windows with active approval prompts (Accept, Reject, Run, etc.) should flash. If you see false positives, the detection patterns may need tuning for your Cursor version. Use `scripts/dump_a11y_tree.py` to inspect what the accessibility tree looks like.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
uv run pytest tests/ -v
|
||||
|
||||
# Dump Cursor's accessibility tree (for debugging detection)
|
||||
uv run python scripts/dump_a11y_tree.py --depth 8
|
||||
|
||||
# Manual overlay test (flashes all windows for 10 seconds)
|
||||
uv run python scripts/test_overlay.py
|
||||
|
||||
# Manual overlay test (only windows needing attention)
|
||||
uv run python scripts/test_overlay.py --per-window
|
||||
```
|
||||
**Pulse doesn't stop:**
|
||||
- Click the Cursor window to bring it to focus — the pulse auto-dismisses when Cursor is the frontmost app.
|
||||
|
||||
Reference in New Issue
Block a user