all repos — dingger @ master

Watches the AI coding agents in your tmux panes on macOS and tells you which need you, from the menu bar, a floating panel, the status line and notifications; jumps to them, and restores tmux sessions and agents after a crash

9518a544
count pi costs as pi's footer does: compaction and other priced calls, not event logs
Igor Soarez igor@soarez.org
Tue, 03 Nov 2026 13:12:21 +0200
bd1eb9ef
show each pi agent's context use: a meter in the panel and search, text in the menu
Igor Soarez igor@soarez.org
Tue, 03 Nov 2026 13:05:57 +0200
8146d1db
show what pi agents cost today, this week and this month, counting forked replies once
Igor Soarez igor@soarez.org
Sun, 01 Nov 2026 10:22:01 +0200

dingger

macOS agent watcher for tmux panes: herdr’s agent panel, without herdr. dingger watches every pane in your tmux server, detects which AI coding agent runs in each (claude, codex, pi, gemini, and 19 others), tracks herdr’s five statuses, and tells you which agents are waiting for you: in the menu bar, in the tmux status line, and with a notification and a sound.

dingger owns no terminals. It is a client of your tmux server. The detection engine and rule manifests are extracted from herdr (Apache-2.0).

Statuses (herdr’s, verbatim)

status meaning symbol color priority
blocked needs your input × red 4
done finished a turn while you were looking elsewhere ✓ teal 3
working turn in flight ◐ yellow 2
idle at its prompt, nothing new since you looked ○ green 1

The state machine is herdr’s apply_pane_state_change and mark_active_tab_seen, implemented as one pure function (watcher::advance) and unit-tested against herdr’s own test cases:

dingger watches tmux focus through tmux hooks, so attendance updates the moment you switch windows; agent reports apply in order, so a turn shorter than a poll is still a completion.

Surfaces

Where state comes from

  1. Agent hooks (authoritative): dingger install pi installs a pi extension that reports working/blocked/idle over ~/.local/state/dingger/agent.sock. Running pi agents load it when reloaded (see “Keeping pi agents on the current extension” below). dingger install claude adds a SessionStart hook to ~/.claude/settings.json (keeping everything else; the original is backed up to settings.json.bak-dingger) that reports only Claude’s session, for restore; its state comes from screen matching.
  2. Screen matching (fallback): capture-pane text through herdr’s manifests, for agents without hooks.

All per-pane state lives in tmux pane options (@agent_state, @dingger_since, @dingger_hook, @dingger_importance, @dingger_extension, @dingger_control, @dingger_draft, @dingger_cost, @dingger_context), so restarting dingger loses nothing.

Keeping pi agents on the current extension

When the daemon starts, it updates the installed pi extension if this dingger’s differs (only its own file, which says so on its first line). Running agents keep the one they loaded, and report its version. While some run an old one, the menu bar menu offers Reload N Agents on an Old Extension; dingger reload-agents does the same. Each agent reloads (pi’s /reload) when:

The others reload once they are; the request stays until all are, or the daemon restarts. dingger sends no keystrokes: it asks the extension over a socket of its own (~/.local/state/dingger/pi-<pid>-<id>.sock), and the extension runs the reload as a command (/dingger-reload, which you can also type). A reload also resets pi’s other extensions: a pending alarm is lost, MCP servers reconnect.

An agent on an extension from before this (it reports no version) takes no requests: the menu says how many need /reload by hand, once.

The pi extension’s reports carry the agent’s importance. Other pi extensions can follow it on pi’s event bus: dingger:importance with { level }, at session start and on every change; it is saved in the session as a custom entry of type dingger-importance.

Usage

dingger watch                 # the daemon (run by launchd, see below)
dingger status                # agents in priority order, with cost and context use
dingger focus [--pane ID]     # jump to an agent (default: the top waiter)
dingger importance [LEVEL]    # an agent's importance: high, normal, low
dingger unread [--pane ID]    # mark an idle agent unread (done again)
dingger reload-agents         # reload pi agents on an old extension
dingger install pi            # install the pi state-reporting extension
dingger install claude        # report Claude Code sessions, for restore
dingger install zsh           # record typed command lines, for restore
dingger manifests update      # refresh manifests from herdr's catalog

Install

cargo build --release
cp target/release/dingger ~/.local/bin/dingger      # do not strip: invalidates
codesign --force --sign - ~/.local/bin/dingger      # the signature, macOS kills it
swiftc -O -o target/dingger-menu menubar/*.swift    # menu bar + panel, bundled
                                                    # as ~/Applications/dingger.app
  source-file /path/to/dingger/packaging/tmux/dingger.tmux

Then reload: tmux source-file ~/.tmux.conf. The snippet: - replaces status-right with the restore offer, %d-%b %H:%M, and the agent counts. Your own status-right is lost, so put the line after it, or edit the snippet’s status-right to keep what you had; - turns on focus-events, which raising the right Ghostty window needs; - adds hooks that report focus and navigation to dingger. They use index [77], so they keep your own hooks on the same events, and reloading does not duplicate them; - binds clicks on the right of the status line, which needs set -g mouse on.

The line sources the snippet from the repo, so a changed snippet applies at the next reload.

dingger does not need tmux running first; it attaches when a server appears.

Testing

cargo test           # engine tests + herdr state-machine tests
tests/e2e.sh         # end to end against an isolated tmux server
tests/pi-live.sh <pi>  # a real pi binary against dingger (new pi releases)

tests/e2e.sh starts its own tmux server, its own daemon (polling only every 30 s, so everything asserted must arrive through events), fake agents driven over the hook protocol, and a real tmux client whose focus it controls. It asserts every status transition, attendance by navigation, both click paths (including the real menu bar binary), the floating panel’s visibility and layout rules and content (compact, full, toggle), agent search (matching, ranking, selection), the status-line segment, latency with a deliberately slow notifier, and state surviving a daemon restart. It never touches your tmux server.

tests/pi-live.sh <pi binary> checks a real pi release on a tmux server and daemon of its own, with a fake local model (tests/fake-model.py) and its own pi agent directory: no network, no credentials, your pi untouched. It covers identification, the extension’s state and session reports, a turn, /reload, an extension’s blocked event, screen detection without the extension, restore, reloading an agent on an old extension (and refusing while busy or with a draft), drafts (reported, cleared, kept across a crash and put back by restore), cost (priced replies, counted once across a real pi --fork), context use (pi’s footer’s number, through a compaction), and exit. To try a release without installing it: npm install --prefix /tmp/pi-next @earendil-works/pi-coding-agent@<version> (through sfw), then tests/pi-live.sh /tmp/pi-next/node_modules/.bin/pi.

Layout

clone
git clone https://legit.soarez.net/dingger.git