aboutsummaryrefslogtreecommitdiff
path: root/docs/reference.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/reference.md')
-rw-r--r--docs/reference.md323
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`).