English | PortuguΓͺs (Brasil)
A translucent always-on-top overlay (Electron) that shows the state of every terminal AI agent session on your desktop as a traffic light: π’ done Β· π‘ working Β· π΄ needs you.
Monitors Claude Code, Antigravity, Codex and OpenCode today. The architecture is agent-agnostic β new agents land via adapters (see Adding an agent).
One light per terminal AI session: π’ done Β· π‘ working Β· π΄ needs you. Animated: the red LED pulses. Static shot below.
When you run several AI agent sessions in parallel across terminals, tabs and projects, you lose track of which one finished, which is still crunching, and which has been silently waiting for your approval for ten minutes. This overlay gives you one glance: a light per session, click to jump to its terminal β window and tab.
- π’π‘π΄ One light per session, plus an aggregate light in the header
- π€ Four agents, one overlay: Claude Code, Antigravity, Codex and OpenCode
- π UI in English & Portuguese β follows the system locale, switchable in Preferences
- Click-to-focus: jumps to the session's terminal β the exact window, and
in Warp the exact tab (via
warp://session/<uuid>) and in Tilix the exact terminal (via D-BusTILIX_ID) - π Beep + desktop notification when a session turns red (rate-limited)
- β° Idle escalation: a finished session left unattended turns red (configurable)
- π Per-session alert snooze (mute the beep for 1h on a red session)
- π Sessions sort by urgency (red β yellow β green); dynamic tray icon paints with the worst color and shows counts on hover
- π Quick Launcher: start an agent (+ Claude / + Antigravity / + Codex / + OpenCode) from the overlay empty-state or the tray β opens the terminal in the last project and the session lights up automatically
- βοΈ Double-click to rename a session (aliases persist per project)
- π Mark a red terminal as read: clicking a π΄ session focuses it and greys it out until a new notification arrives (toggle in Preferences)
- π Per-agent usage meters (footer): one aligned row per usage window with a % meter and next reset β Claude (5-hour + weekly, real numbers from the OAuth usage API), Codex (5-hour + weekly, read passively from the session rollout) and GLM Coding Plan (5h tokens + monthly MCP). Rows go amber β₯70%, red β₯90%; the last value sticks (greys out when stale) and survives restarts. Toggle between the meters and the Quick Launcher in the header
- ποΈ Appearance: window transparency slider + compact list mode (single dense row per session), both live and remembered
- βοΈ Preferences window (gear icon): idle threshold, global shortcut, autostart, install/remove hooks, appearance, show/hide, quit β with the app version and a link to the repo in the footer
- Auto-height, drag anywhere, width-resizable, position + UI state persisted
- π Version + update check in the header β shows the installed version and, when a newer GitHub release exists, a one-click badge to open it
- Tray icon + global shortcut
Ctrl+Alt+H(configurable) - Stays out of your way: no taskbar/alt-tab entry, never maximizes, no scrollbar
- Linux (X11: full support, Wayland: partial β see Troubleshooting) or macOS (supports Apple Silicon M1βM5).
- On Linux:
wmctrl,xdotool,jqβsudo apt install wmctrl xdotool jq - On macOS: Homebrew and
jqβbrew install jq - Node.js 20+
- A supported agent: Claude Code, Antigravity CLI, Codex or OpenCode
Pick whichever fits your platform. All options require the agent hooks so the overlay can see Claude Code / Antigravity / OpenCode sessions β from source you run npm run setup-hook; in a packaged build you click Install/update hooks in the tray menu or Preferences window (or the overlay's onboarding button).
Run the following single-line command in your terminal. It will verify dependencies (installing jq via Homebrew if needed), download the latest .dmg release, copy AI Traffic Lights.app to /Applications, and configure shell aliases (atl and ai-traffic-lights) in your ~/.zshrc and ~/.bash_profile:
curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install_macos.sh | bashTo run the application from the terminal, open a new shell session (or run source ~/.zshrc) and execute:
atl- Download the
.dmgfile from the latest release. - Open the
.dmgfile and dragAI Traffic Lights.appto your/Applicationsfolder.
A single command that fetches the latest release, makes it executable, installs the icon into the system theme, and creates an application menu shortcut:
curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bashThen open it from your application menu or run ~/Applications/AI-Traffic-Lights.AppImage. To uninstall:
curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bash -s -- --uninstallManual AppImage install
Grab the .AppImage from the latest release, drop it in a user-writable folder (the self-updater rewrites that file in place β do not use /opt or /usr), and run:
chmod +x AI-Traffic-Lights-*.AppImage
./AI-Traffic-Lights-*.AppImageDownload the .deb from the latest release and install it:
sudo dpkg -i ai-traffic-lights_*.debgit clone https://github.com/aronpc/ai-traffic-lights.git
cd ai-traffic-lights
npm install
npm run setup-hook # registers the adapters: Claude Code (~/.claude),
# Antigravity CLI (~/.gemini/antigravity-cli) and OpenCode (plugin in
# ~/.config/opencode/plugin/), whichever are present
npm start # opens the overlaysetup-hook is idempotent and surgical: it backs up settings.json and never touches hooks from other tools. The registered command points to a self-updating stable copy of the hook in ~/.local/share/ai-traffic-lights/bin/ β so moving the project (or running the packaged AppImage/App, whose mount path changes every run) never breaks it. npm run remove-hook undoes everything just as cleanly. The tray menu and Preferences window offer the same install/remove actions for packaged installs.
New Claude Code sessions show up immediately; sessions already open appear on their next event.
Claude Code session ββhooksβββΆ traffic-hook.sh (adapter, <25ms, fork-free)
β writes
βΌ
~/.local/share/ai-traffic-lights/state/<session>.json
β watched (chokidar)
βΌ
Electron main ββIPCβββΆ renderer: computeState() β π’π‘π΄
Architecture decision: the adapter only records events. The state (color) is computed in the renderer, because idle escalation (greenβred after N minutes) needs a clock β something an event-driven hook doesn't have.
The integration contract is the state file, not the code. Anything that writes a valid JSON into the state dir becomes a light in the overlay.
See docs/ARCHITECTURE.md for integration diagrams and a step-by-step guide to adding an agent.
Location: ${XDG_DATA_HOME:-~/.local/share}/ai-traffic-lights/state/<session_id>.json
Types: every *_ts is an integer epoch. windowid is a string
(xdotool decimal or 0x⦠hex; the app normalizes). pid is an integer.
Window (windowid): captured at UserPromptSubmit/SessionStart (the
focused window then is the session's terminal) via xdotool getactivewindow, preserved across events. Before using it, focusSession()
validates it against the session's process tree β a stale or recycled id
whose window no longer belongs to the session is discarded (so a click never
focuses the wrong window); the fallback is the first window owned by the
session's process.
Tab (invisible to X11 β only the terminal can select it):
| Terminal | Channel | Env var captured |
|---|---|---|
| Warp | xdg-open warp://session/<uuid> |
WARP_FOCUS_URL |
| Tilix | gdbus β¦ org.gtk.Actions.Activate activate-terminal <id> |
TILIX_ID |
The decision logic (pickWindow/tabChannel) is a pure module,
src/focus.js β main.js only does the I/O. On X11 the window
is raised, then the tab is selected; on Wayland the tab channel goes first
(wmctrl only sees XWayland).
| Adapter event | level | reason (sub-icon) |
|---|---|---|
SessionStart |
done π’ | β (initial) |
UserPromptSubmit, PreToolUse, PostToolUse |
processing π‘ | π |
Stop |
done π’ (β awaiting π΄β° if idle > 5 min) | β / β° |
PermissionRequest |
awaiting π΄ | π |
Notification |
depends on notification_type: permission_prompt / idle_prompt / elicitation_dialog β awaiting π΄β; auth_success / elicitation_complete / elicitation_response β done π’β |
β / β |
PostToolUseFailure |
awaiting π΄ |
Two steps β the app adapts to whatever you declare:
- Register it in
src/agents.js: one line withlabel(UI),comm(process names in/proc/<pid>/comm, for detecting live sessions that don't have a state file yet). - Write an adapter: anything that writes state files following the
contract above.
hooks/traffic-hook.shis the reference implementation β and it already serves two agents: for Antigravity CLI it runs withAI_TL_AGENT=antigravityand uses the canonical event vocabulary directly, so the renderer never learns per-agent dialects.
For Node-based CLIs whose process comm is just node (Antigravity), the
/proc probe identifies sessions by the script basename in the process
argv β declared via the argv field in the registry.
See CONTRIBUTING.md for details.
- Overlay shows "no active sessions" β did you run
npm run setup-hook? Already-open sessions only appear after their next event (send any prompt). - Click doesn't focus / focuses the wrong window β on Linux, check that
wmctrlandxdotoolare installed. On macOS, click-to-focus uses AppleScript (osascript) to focus the window. If it fails, ensureAI Traffic Lights.app(or your terminal if running from source) has Accessibility permissions granted in System Settings > Privacy & Security > Accessibility. - Wayland β the overlay itself runs fine (XWayland). Native-Wayland windows can't be focused by third parties, so click-to-focus relies on the terminal's focus URI (Warp today); the global shortcut may not fire while a native-Wayland app has focus. Workarounds: click the tray icon, or bind a GNOME custom shortcut to the app's launch command β relaunching toggles the overlay (single-instance).
- Where is my data? β
${XDG_DATA_HOME:-~/.local/share}/ai-traffic-lights/(state files, window position, aliases). Delete it freely; it regenerates. - Renderer debug β
ATL_DEBUG=1 npm startlogs to/tmp/atl-renderer.log.
npm install
npm startTest the adapter standalone:
echo '{"session_id":"t","hook_event_name":"Stop","cwd":"/tmp"}' | bash hooks/traffic-hook.sh
cat "${XDG_DATA_HOME:-$HOME/.local/share}/ai-traffic-lights/state/t.json" | jq .- Antigravity CLI adapter (hooks) + idle detection via argv probe
- OpenCode adapter (plugin: chat/tool/idle/permission events, model
capture β see
adapters/opencode/) - Codex adapter (same hooks schema as Claude; model from payload) β note:
after
setup-hook, run/hooksin the Codex CLI once to trust the hook - Packaging: AppImage + .deb (electron-builder) β see Releases
- Test suite (
node:test) + CI - Reliable click-to-focus: window-id validation + exact tab in Warp
(
focus_url) and Tilix (TILIX_IDvia D-Bus) - Tab focus for terminals without a native channel (GNOME Terminal, zellij/tmux)
- Full native-Wayland window focus (today: XWayland + Warp focus URI + relaunch-to-toggle)
- Configurable idle threshold & shortcut (tray β Preferences β stored in
~/.local/share/ai-traffic-lights/settings.json)



{ "schema_version": 2, // bump when the schema changes "agent": "claude", // agent id (key in src/agents.js) "session_id": "abc-123", // key, = file name "pid": 986893, // agent process PID (dead-session sweep) "cwd": "/home/user/project", // project dir (basename = default label) "term_program": "WarpTerminal",// source terminal (null if unknown) "windowid": "67108868", // X11 window of the session β see below "focus_url": "warp://session/8726β¦", // Warp: focus URI (xdg-open) "tilix_id": null, // Tilix: terminal id for activate-terminal (D-Bus) "zellij_session": null, // zellij session name, when inside zellij "last_event": "Stop", // last hook_event_name "last_event_ts": 1783124001, // epoch of the last event (UTC) "last_tool": "Bash", // last tool_name (null for tool-less events) "notification_type": null, // Notification discriminator (see below) β null unless last_event is Notification "events": [ // rolling log (last 50), append-only { "ts": 1783124000, "event": "PostToolUse", "tool": "Bash" }, { "ts": 1783124001, "event": "Stop", "tool": null } ] }