aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-20 12:43:07 +0300
committerIgor Soarez <igor@soarez.org>2026-10-20 12:43:07 +0300
commit4af6a4f625b1965b783c9efb857f8ac665adb8eb (patch)
treedd2dac9627735fd02b3372a4f569125a702ba57a /README.md
parent943fc940d9c387916708cb3fdc912323c92ef6dc (diff)
rebuild the status model as herdr's state machine, event-driven and tested
the seen logic was inverted: first observations were marked done and real completions idle, so the menu showed teal for agents that did nothing and green for agents that finished, and attending changed nothing visible. - watcher::advance is herdr's apply_pane_state_change + mark_active_tab_seen as a pure function, unit-tested against herdr's own cases; attendance is window-level like herdr's active tab - hook reports are events applied in order, so a turn shorter than a poll is still a completion - event-driven: hook reports, dingger focus, and tmux focus hooks wake the watcher; notifications are delivered off-thread (the macos call blocked ticks for 2+ seconds) after the pane options are written - all per-pane state persists in tmux pane options, so restarts and deploys lose nothing, including the authoritative hook state - every click goes through dingger focus: the menu bar shells out to it and the status-line binding runs it; it picks the right client, skips the window already shown, and activates the terminal app hosting the client - the ready queue is ordered like the menu (blocked first, then oldest done) - menu bar and tmux segment render herdr's symbols in herdr's palette - tests/e2e.sh: 60 assertions against an isolated tmux server with a real client, a 30s poll interval, and a slow fake notifier
Diffstat (limited to 'README.md')
-rw-r--r--README.md149
1 files changed, 91 insertions, 58 deletions
diff --git a/README.md b/README.md
index 6e96683..7fda838 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,107 @@
# dingger
-macOS agent watcher for tmux panes. dingger watches every pane in your tmux
-server, detects which AI coding agent is running in each one (claude, codex,
-pi, gemini, and 19 others), tracks its state (idle / working / blocked), and
-sends a macOS notification — with sound — when an agent needs attention or
-finishes.
-
-dingger owns no terminals. It is a client of your tmux server: `capture-pane`
-for screen text, `#{pane_title}` for OSC titles, and `#{pane_pid}` plus a `ps`
-tree walk for agent identity. The detection engine and rule manifests are
-extracted from [herdr](https://github.com/herdrdev/herdr) (Apache-2.0), so
-detection quality matches herdr's screen-matching engine. herdr's manifests
-use the same terminal-screen regions (`capture-pane` output maps 1:1); OSC
-progress sequences are not re-exposed by tmux (a small confidence loss on a
-few rules). Agents launched as bare `node <entry.js>` scripts (rather than via
-an install shim) are not identified yet.
+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](https://github.com/herdrdev/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:
+
+- a turn that finishes while you look at another window becomes **done**;
+- a turn that finishes in the window you are looking at stays **idle**;
+- looking at a window (a tmux window is herdr's tab) turns its done agents
+ **idle**, as does jumping to one from the menu bar or status line;
+- an agent seen for the first time is **idle**, not done.
+
+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
+
+- **Menu bar** (`menubar/main.swift`): `×1 ✓3 ◐2` in herdr's colors; the
+ menu lists every agent in priority order, oldest waiting first. Clicking a
+ row jumps to that pane.
+- **tmux status line** (`packaging/tmux/dingger.tmux`): the same summary after
+ the clock. Clicking it jumps to the top waiting agent (the menu's top row),
+ skipping the window you already look at.
+- **Notifications**: "needs attention" (blocked) and "finished" (done), with
+ herdr's sounds; silent for completions you are already watching.
+
+## 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`. Restart running pi agents to load it.
+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`), so restarting dingger loses nothing.
## Usage
```bash
-dingger watch # run the watcher; notify on transitions
-dingger status # table of panes, agents, states
-dingger manifests update # refresh manifests from herdr's published catalog
+dingger watch # the daemon (run by launchd, see below)
+dingger status # agents in priority order
+dingger focus [--pane ID] # jump to an agent (default: the top waiter)
+dingger install pi # install the pi state-reporting extension
+dingger manifests update # refresh manifests from herdr's catalog
```
-`dingger status` reads state written back into tmux as the per-pane user
-option `@agent_state`, so it works without any daemon IPC. Render it directly
-in your status line with `#{:@agent_state}` in `status-format` /
-`pane-border-status`. `dingger manifests update` stores refreshed manifests
-from herdr's published catalog; restart `dingger watch` to pick them up.
+## Install
-Environment:
-
-- `DINGGER_TMUX_SOCKET` — target a named tmux server socket (`tmux -L <name>`)
-- `DINGGER_CONFIG_DIR` / `DINGGER_STATE_DIR` — override/remote manifest locations
- (default `~/.config/dingger` / `~/.local/state/dingger`)
-
-## Notification policy
-
-herdr's transition policy: notify on any transition to `blocked` ("needs
-attention", `request.mp3`) and on `working`/`blocked` → `idle` ("finished",
-`done.mp3`), with no notification for `unknown` panes. One deliberate
-difference from herdr: a pane on the active window of an attached client is
-suppressed entirely (herdr still plays the blocker sound there). Viewer
-screens such as Claude's transcript never change state.
-
-## Layout
-
-- `crates/detect-core` — the detection engine (manifest compiler, region
- extractors, priority arbitration) and 22 bundled manifests, extracted from
- herdr's `src/detect/`. Pure text-in → state-out.
-- `crates/dinggerd` — the `dingger` binary: tmux watcher, notification
- policy, sounds, CLI.
-- `sounds/` — herdr's bundled notification sounds.
-
-## Status
+```bash
+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/main.swift # menu bar, bundled as
+ # ~/Applications/dingger.app
+```
-Early scaffold. Roadmap: menu bar UI (Swift shell over the daemon), configurable
-notification policy.
+- launchd agents (start at login, restart on exit):
+ `packaging/launchd/dev.dingger.{dingger,menubar}.plist`; adjust the user
+ path, copy to `~/Library/LaunchAgents/`, `launchctl bootstrap gui/$(id -u) <plist>`.
+- tmux: `source-file /path/to/dingger/packaging/tmux/dingger.tmux` (needs
+ `set -g mouse on` for the click).
-### Running as a login agent (launchd)
+dingger does not need tmux running first; it attaches when a server appears.
-`packaging/launchd/dev.dingger.dingger.plist` runs `dingger watch` at login
-with auto-restart; adjust the user path, then:
+## Testing
```bash
-launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.dingger.dingger.plist
+cargo test # engine tests + herdr state-machine tests
+tests/e2e.sh # end to end against an isolated tmux server
```
-dingger does not require tmux to be running first — it waits for a server and
-attaches when one appears. All tmux invocations use `-u` (forced UTF-8): under
-launchd's C locale, tmux mangles tab-separated `-F` output into underscores,
-which would silently hide every pane.
+`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 status-line segment, latency with a
+deliberately slow notifier, and state surviving a daemon restart. It never
+touches your tmux server.
+
+## Layout
+
+- `crates/detect-core`: the detection engine and 22 manifests, from herdr.
+- `crates/dinggerd`: the `dingger` binary (watcher, hooks, focus, CLI).
+- `menubar/`: the menu bar app.
+- `packaging/`: launchd agents and the tmux snippet.
+- `tests/e2e.sh`: the end-to-end suite.