- Python 88.6%
- HTML 11.4%
Three related StatusNotifierItem fixes for the niri + DankMaterialShell (Quickshell) setup, plus a regression test suite. - Missing icon: the item answered Properties.Get but not GetAll, which Quickshell uses to populate an item in one call. GetAll fell through to GDBus (UnknownObject) so the host dropped the item. Add a GetAll handler (single _sni_properties() source of truth) and embed a real ARGB32 IconPixmap (new icons.rgba_to_argb32: big-endian A,R,G,B with rowstride handling). - "C" letter instead of the logo: DankMaterialShell flattens IconName+IconThemePath to file://<themepath>/<name>, missing our freedesktop theme layout, so the image failed and DMS showed a first-letter fallback. Set IconName to the absolute PNG path and drop IconThemePath, matching other tray apps that render correctly here. - Wrong icon source: icons.py hardcoded a lowercase ~/Project/claudecode-monitor path that never matched the real capitalised checkout, so it silently used the bundled icon. Resolve claudecode-color.svg relative to the package (repo root), with a ~/.config/claude-monitor/icon.svg user override. - Right-click menu: a client-side Gtk.Menu cannot anchor over the panel icon on Wayland. Publish a com.canonical.dbusmenu (new dbusmenu.py) and a Menu property so the host renders it; clicks return as dbusmenu Event calls. Tests: add test_dbusmenu.py, test_tray.py, ARGB conversion and icon source-path tests (138 passing). |
||
|---|---|---|
| .superpowers/brainstorm/340163-1780756025 | ||
| assets | ||
| docs/superpowers | ||
| src/claude_monitor | ||
| tests | ||
| .gitignore | ||
| claude-color.svg | ||
| claudecode-color.svg | ||
| pyproject.toml | ||
| README.md | ||
| README_zh.md | ||
| uv.lock | ||
Claude Code Monitor
A Linux system tray application that monitors Claude Code sessions, displays status via tray icon, sends desktop notifications when Claude needs your input/selection, and provides a card-style sessions overview.
Features
- System Tray Icon — Claude Code logo with per-state corner badges (green=running, blue=thinking, amber!=waiting, red X=error, dimmed=idle)
- Desktop Notifications — Alerts you when Claude waits for input or prompts a selection ("Do you want to proceed?")
- Auto-dismiss — Notification closes automatically once you respond
- Notification Sound — Plays a sound on alert (configurable)
- Session Cards — Pop-up window with full session detail (path, state, PID) — un-truncated, no DBusMenu width cap
- Right-click Menu — Per-session state icons + short status text
- Waits-for-selection vs waits-for-input — Notifications and labels distinguish the two ("等待选择" vs "等待输入")
How it works
Waiting/selection detection uses Claude Code's Notification hook
(notification_type: permission_prompt, idle_prompt) — the
only reliable signal for "Claude is blocked on the user". The hook script
(hook_script.py, installed to ~/.local/share/claude-monitor/) writes
$XDG_RUNTIME_DIR/claude-monitor/session-<id>.json on hook events. The
tray app polls these files.
/proc polling is not used for waiting detection (it cannot distinguish
a selection menu from tool execution). The scanner (scanner.py) only runs
as a fallback when hooks aren't installed — it reports sessions as RUNNING,
never WAITING.
Requirements
- Python >= 3.12
- System packages (Arch):
sudo pacman -S python-gobject libayatana-appindicator3 libnotify cairo librsvg python-cairo - Notification daemon (DMS, dunst, mako, swaync, etc.)
Installation
git clone https://git.lumorian.org/Lumorian/Claudecode-monitor.git
cd Claudecode-monitor
# Install dependencies
pip install -e .
# Register Claude Code hooks (required!)
claude-monitor --install-hooks
--install-hooks merges entries into ~/.claude/settings.json (with a
backup) and coexists with hooks from other tools. Restart any running
Claude Code sessions after installing — hooks load at session start.
To remove later: claude-monitor --uninstall-hooks.
Usage
claude-monitor
The icon appears in your system tray. Right-click to see sessions. Click "查看会话" for a card overview.
Configuration
~/.config/claude-monitor/config.json (auto-created on first run):
{
"scan_interval_sec": 1,
"notify_cooldown_sec": 5,
"enable_notifications": true,
"notify_on_error": true,
"notify_sound": "message"
}
| Key | Default | Description |
|---|---|---|
scan_interval_sec |
1 | Poll interval for hook files (seconds) |
notify_cooldown_sec |
5 | Min seconds between notifications |
enable_notifications |
true | Whether to send desktop notifications |
notify_on_error |
true | Notify on session errors |
notify_sound |
"message" | freedesktop sound event, file path, or "" to disable |
States
| Icon Badge | State | Description |
|---|---|---|
| Dimmed logo | Idle | No Claude Code sessions |
| Green dot | Running | Session active |
| Blue dot | Thinking | Claude generating |
| Amber ! | Waiting | Needs input → notification |
| Red X | Error | Session error |
Project Structure
src/claude_monitor/
├── main.py # Entry point + --install-hooks / --uninstall-hooks
├── app.py # Orchestrator (hook source → state → notify → tray)
├── models.py # Data models + state_label helper
├── paths.py # Shared runtime/cache paths
├── icons.py # SVG → PNG rendering (cairo/Rsvg), state badges
├── hook_script.py # Claude Code hook (stdlib-only, writes session files)
├── hook_installer.py # Registers hooks in ~/.claude/settings.json
├── hook_watcher.py # HookSource — reads session state files
├── state.py # State manager + transition events
├── notifier.py # Desktop notifications (notify-send + gdbus auto-close)
├── scanner.py # Fallback live-process discovery (pid → RUNNING only)
├── tray.py # System tray icon + context menu
└── window.py # Card-style sessions overview popup
tests/
├── conftest.py # Shared fixtures
├── test_models.py # 19 tests (model creation, state_label)
├── test_scanner.py # 11 tests (process filter + cwd fallback)
├── test_state.py # 11 tests (session_id keying + callbacks)
├── test_notifier.py # 19 tests (notify, cooldown, ID capture, close, sound)
├── test_hook_source.py # 7 tests (HookSource reads + prunes)
├── test_hook_script.py # 8 tests (hook event → state mapping)
├── test_hook_installer.py # 6 tests (merge, idempotent, uninstall)
└── test_icons.py # 10 tests (SVG rendering, glyphs)
Development
pip install -e ".[dev]"
# Run tests (96 pass)
pytest tests/ -v
# Coverage
pytest --cov=src --cov-report=term-missing
License
MIT