Skip to content

aronpc/ai-traffic-lights

Repository files navigation

🚦 AI Traffic Lights

English | PortuguΓͺs (Brasil)

CI

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).

AI Traffic Lights β€” live overlay

One light per terminal AI session: 🟒 done Β· 🟑 working Β· πŸ”΄ needs you. Animated: the red LED pulses. Static shot below.

AI Traffic Lights overlay β€” all agents

Why

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.

Features

  • πŸŸ’πŸŸ‘πŸ”΄ 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-Bus TILIX_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

Preferences window

Requirements

  • 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

Install

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).

macOS (M1 to M5 / arm64)

Option 1: Automated script (recommended)

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 | bash

To run the application from the terminal, open a new shell session (or run source ~/.zshrc) and execute:

atl

Option 2: Manual Install

  1. Download the .dmg file from the latest release.
  2. Open the .dmg file and drag AI Traffic Lights.app to your /Applications folder.

Linux

Option 1: AppImage (recommended, self-updating)

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 | bash

Then 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 -- --uninstall
Manual 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-*.AppImage

Option 2: Debian Package (.deb)

Download the .deb from the latest release and install it:

sudo dpkg -i ai-traffic-lights_*.deb

From Source (Development - Linux & macOS)

git 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 overlay

setup-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.

How it works

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.

State file contract (schema_version 2)

Location: ${XDG_DATA_HOME:-~/.local/share}/ai-traffic-lights/state/<session_id>.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 }
  ]
}

Types: every *_ts is an integer epoch. windowid is a string (xdotool decimal or 0x… hex; the app normalizes). pid is an integer.

Focusing the right window β€” and the right tab

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).

Event β†’ state mapping (computeState, renderer)

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 πŸ”΄ ⚠️

Adding a new agent

Two steps β€” the app adapts to whatever you declare:

  1. Register it in src/agents.js: one line with label (UI), comm (process names in /proc/<pid>/comm, for detecting live sessions that don't have a state file yet).
  2. Write an adapter: anything that writes state files following the contract above. hooks/traffic-hook.sh is the reference implementation β€” and it already serves two agents: for Antigravity CLI it runs with AI_TL_AGENT=antigravity and 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.

Troubleshooting

  • 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 wmctrl and xdotool are installed. On macOS, click-to-focus uses AppleScript (osascript) to focus the window. If it fails, ensure AI 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 start logs to /tmp/atl-renderer.log.

Development

npm install
npm start

Test 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 .

Roadmap

  • 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 /hooks in 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_ID via 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)

License

MIT

About

🚦 Traffic light overlay for terminal AI agent sessions β€” Claude Code today, agent-agnostic by design

Topics

Resources

License

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors