diff options
Diffstat (limited to 'docs/reference.md')
| -rw-r--r-- | docs/reference.md | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..e159880 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,323 @@ +# dingger reference + +The details behind the [README](../README.md): how each part behaves, where +state comes from, and how dingger is tested. + +## 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. + +## Features in detail + +- **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. +- **Floating panel** (`menubar/panel.swift`): an always-on-top HUD that never + takes focus, on every Space and over full-screen apps. It lists the agents + waiting for you (blocked, done) and summarizes the rest + (`◐ 2 working · ○ 15 idle`); click a row to jump there, drag to move it, + right-click an idle agent in the full list to mark it unread. + **⌃⌥⌘E** (or "Show Panel" in the menu) turns it on or off; off stays off + until you turn it back on. While on, "Panel Shows" picks *Only When Agents + Are Waiting* (default: it hides itself while nothing waits; turning it on + then shows it for 3 seconds so you see it is armed; compact) or *Always* + (with a "Show all / Show less" toggle for the full list). It grows down + to the bottom of the screen at most; a longer list scrolls, with the + summary line kept under it. The panel is always dark, in herdr's + palette, so it reads the same over any desktop. +- **Jump shortcut**: **⌃⌥⌘U** jumps to the top waiting agent from anywhere, + like clicking the tmux status line. +- **Agent search**: **⌃⌥⇧⌘U** opens a Spotlight-style search over every + agent. Type to fuzzy-filter (each word must match, letters in order, the + name, session, window, harness, status, or directory; or, as written, + a forgotten draft); ↑↓ or ⌃N ⌃P / ⌃J ⌃K + select, ↩ or a click jumps, esc closes, ⌃U clears, ⌘U (or a right-click) + marks the selected idle agent unread. With nothing typed, + agents are in the menu's order, so ↩ jumps to the top waiting agent. It + takes the keyboard without activating dingger, so focus returns where it + was. +- **Restore**: dingger saves your tmux layout as it changes (sessions, + windows, panes, layouts, directories) under + `~/.local/state/dingger/snapshots/`. After a crash, shutdown, or + `kill-server`, `dingger restore` brings back what is missing (`--dry-run` + to preview, `--list` for saved snapshots, `--session` for some). Sessions + that exist are left alone, except a bare one of the same name (what a + terminal creates on startup), which is replaced with its clients moved + over. Agents are resumed in their own sessions: pi with `pi --session + <file>` (reported by the pi extension), Claude Code with `claude --resume + <id>` (reported by its SessionStart hook, `dingger install claude`), in + the directory they ran in. A session that never got a message has nothing + to resume, so its command line is typed instead. Agents that were + mid-turn are listed. Other panes get the command line that was running typed at the + prompt, not run: exactly as typed with the zsh hook (`dingger install + zsh`, then source the file it names from your `.zshrc`), otherwise the + program's arguments. An agent or program that exits within a minute + before tmux stops (a shutdown) is still restored. + When tmux stops, the panel (first row: "↺ Restore 9 sessions · 22 + agents", or "Not now"), the menu (all, or one session at a time), and the + status line (`↺9`, clickable) offer the restore; it runs through your + login shell, like a terminal starting tmux, and a notification + summarizes (full report: `~/.local/state/dingger/restore.log`). +- All shortcuts live in `~/.config/dingger/config.toml` (menu bar, + Settings…), reloaded on save. +- **Jumping to an agent** (any click, a shortcut, search) shows it in a + terminal window and raises that window. With several terminal windows, + one already on the agent's session wins (it shows the agent without + switching), then the focused one, then the one used last. In Ghostty + (1.3+), the exact window, tab, and split is raised: dingger learns which + terminal holds each tmux client when it gains focus (tmux's focus hook + asks Ghostty which terminal is focused; nothing visible changes). + Other terminals: the app is activated. +- **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. A pi + run that ends in an error (after pi's own retries) is still done, but + says so: "failed" with the error as one readable line (`401: invalid + x-api-key`), with the "needs attention" sound, and `failed 3m · <why>` + in red where the wait shows (plain text in the menu; `dingger status` + too), until you have seen it or it runs again. +- **Importance**: mark an agent high, normal (the default), or low. Within a + status, more important agents are listed first, so a jump goes to them + first; status still comes first (a blocked low agent before a done high + one). A high agent shows a peach `!` before its name (in a column of its + own, so names line up); a low agent's name is dimmed, its second line + says "low", and it makes no sound (it + still notifies, and waits in line like any other). In pi, type + `/importance high|normal|low` (no argument: the current level); pi saves + it with the session, so it comes back after `/reload`, a restart, or a + restore. For other agents, + run `dingger importance high|normal|low` in the agent's pane (in Claude + Code: `! dingger importance high`, which Claude may answer with a model + turn), or pass `--pane`; it lasts until the agent exits. +- **Mark as unread**: like in mail, an idle agent (one you have seen) can be + made done again, to come back to later: right-click it in the panel's + full list, ⌘U in agent search, `/unread` in pi, or `dingger unread + [--pane]`. It goes last among done agents of its importance, with no + sound or notification, and stays done until you come back to it: you + leave its tmux window and return, or jump to it. Only idle agents can be + marked: a working, blocked, or done one is left alone. Claude Code has no + `/unread`, and `! dingger unread` does not work in it: Claude (2.1.311) + answers a `!` command with a model turn, so the agent is working and the + mark is refused. Use the panel or search for Claude agents. +- **What agents cost** (pi): the menu's top line totals what pi agents + spent today, this week, and this month (`$8.86 today · $457 this week · + $8.86 this month`); each agent's line adds what its session spent today + (at the right edge in the panel and search, after the place in the + menu), and `dingger status` shows both. Periods start at + 00:00 UTC (a week on Monday, a month on its 1st). The daemon reads pi's + session files (`~/.pi/agent/sessions`, and those of running agents + elsewhere) in a thread of its own every 15 seconds, only what was + appended since. Each reply counts once, for the session it first appeared + in: a fork (`/fork`, `pi --fork`) copies its parent's replies into its + own file, and summing every file would count them again (in one month + here, $1,024 of $2,991). The totals include agents since closed or + started over. Amounts are what pi prices each call at (its model + configuration), counted as pi's footer counts them: replies, compaction + and branch summaries, and other recorded calls. Claude Code records no + prices, so it is not counted. +- **Context use** (pi): how full each agent's context window is, the + number pi's own footer shows. The panel and search draw it as a small + meter at the right edge, after the cost (gray; yellow above 70% and red + above 90%, as in pi; the number on hover); the menu adds it as text + (`$0.70 · 10%`), and `dingger status` has a CTX column. The extension + reports it after each message, compaction, model change, and turn; right + after compacting pi does not know it until the next reply, and neither + does dingger (no meter). Agents on an older extension show none until + they reload. +- **Session names** (pi): every pi session gets a short name of what it is + about, from the model, after its first run settles; it is refreshed + every 8 of your messages, and an old unnamed session is named when you + resume it. A name you give (`/name`, `pi --name`, `/autoname <name>`) is + never replaced; `/autoname` alone renames now. It runs in every pi + session (print runs and outside tmux too) and costs one short model call + per naming; `PI_AUTONAME_MODEL=provider/model-id` picks a cheaper model. + It replaces the `pi-auto-session-name` package (remove it: `pi remove + git:github.com/Optomatica/pi-auto-session-name`), and keeps the names + that package gave automatic. +- **Forgotten drafts** (pi): an agent with text left in its input box shows + `✎` before its name wherever you are not looking at it (where you look, + you may be typing it). Idle agents with one come first among idle agents; + the compact panel counts them (`✎ 2 drafts`, a click jumps to the + first); search finds an agent by its draft's words (as written, not + fuzzily); `dingger status` marks them too. No sound, no notification. + The extension reads the input box half a second after you stop typing, + and keeps each session's draft in a file + (`~/.local/state/dingger/drafts/`) that puts it back in the input box + when the session comes back: after a crash, a restore, or `pi --session`. + The draft is kept on the pane as one line (`@dingger_draft`), never in + dingger's log. + +## 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). + It also reports the session's name (`/name`, or pi's own), which is + the agent's name in dingger whatever the terminal title says: any + program in the pane can set a title. Agents without it (Claude Code, a + pi on an older extension) are named from their terminal title. + Blocked covers a dialog that opens during a run (an extension asking + before it goes on), until you answer it; one you open while the agent + is idle changes nothing. A run you cancel (Escape) ends idle, not done, + with no "finished" notification, even when it settles after you look + away. + `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`, `@dingger_error`), 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: + +- it is idle (done included: it stays done), +- it is not on screen, where you may be typing into it, +- and its input box is empty: the extension checks this itself, at the + moment it reloads, so nothing you type is lost or sent. + +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`. + +## Install in detail + +```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/*.swift # menu bar + panel, bundled + # as ~/Applications/dingger.app +``` + +- 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: you add the snippet yourself; no `dingger install` command does it. + Put this line at the end of your tmux config (`~/.tmux.conf`, or a file + it sources): + + ```tmux + 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 + +```bash +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/shutdown.sh [dingger] [rounds] # restore after the ways an OS stops tmux +``` + +`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, dialogs (blocked during a run, +nothing while idle), a cancelled run (idle, not done), a failed run (done, +with why; cleared when seen or run again), names (the session's, not the +terminal title's; named after the first run, renamed every 8 messages, +yours kept, print runs and resumed old sessions named), screen detection +without the +extension, restore (after tmux and its real pis are stopped as a restart +stops them: SIGTERM to all at once, then SIGKILL), 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`. + +`tests/shutdown.sh` stops tmux the ways a restart or logout can, each on +servers, a daemon and a state directory of its own, with fake agents: SIGTERM +to tmux; to the agents first; the daemon first; the terminal quitting first; +and, for 20 rounds each by default, SIGTERM and SIGKILL to everything at once +(tmux, its panes, the terminal's client, the daemon) in a random order at a +random moment. After each it logs in again (a fresh daemon if its own was +stopped; a terminal running `tmux new-session -As 0`) and checks the old +layout is offered and restoring brings back every session, window, pane, +directory, agent and command. + +## 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 (`model.swift` rows and colors, `panel.swift` + the floating panel, `hotkey.swift` the global shortcut and settings). +- `packaging/`: launchd agents and the tmux snippet. +- `tests/`: the end-to-end suite (`e2e.sh`), a real pi (`pi-live.sh`), + shutdowns (`shutdown.sh`). +- `docs/`: this reference, the README's images, and the script that makes + them (`screenshots.sh`, from made-up agents in `demo-agents.sh`). |
