<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dingger.git/crates/dinggerd/src/hooks.rs, branch master</title>
<subtitle>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
</subtitle>
<id>https://git.soarez.net/sz/dingger.git/atom?h=master</id>
<link rel='self' href='https://git.soarez.net/sz/dingger.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/'/>
<updated>2026-11-03T11:05:57+00:00</updated>
<entry>
<title>show each pi agent's context use: a meter in the panel and search, text in the menu</title>
<updated>2026-11-03T11:05:57+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-11-03T11:05:57+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=bd1eb9ef9b3b89ff5825eb8bf9b2e978167c9ad0'/>
<id>urn:sha1:bd1eb9ef9b3b89ff5825eb8bf9b2e978167c9ad0</id>
<content type='text'>
</content>
</entry>
<entry>
<title>show what pi agents cost today, this week and this month, counting forked replies once</title>
<updated>2026-11-01T08:22:01+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-11-01T08:22:01+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=8146d1db419e8bdacd75920da6f3761abf724853'/>
<id>urn:sha1:8146d1db419e8bdacd75920da6f3761abf724853</id>
<content type='text'>
</content>
</entry>
<entry>
<title>mark forgotten drafts in pi agents' input boxes, and keep them across a crash or restore</title>
<updated>2026-10-30T16:59:43+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-30T16:59:43+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=959f0d46cac1a09d0821da599d8dff54d1147892'/>
<id>urn:sha1:959f0d46cac1a09d0821da599d8dff54d1147892</id>
<content type='text'>
</content>
</entry>
<entry>
<title>reload pi agents on an old extension: versioned extension, control socket, menu item and reload-agents</title>
<updated>2026-10-30T15:27:21+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-30T15:27:21+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=b0cfd10b80443d3f67d263ff4db177bb53cc90ed'/>
<id>urn:sha1:b0cfd10b80443d3f67d263ff4db177bb53cc90ed</id>
<content type='text'>
</content>
</entry>
<entry>
<title>mark as unread: make an idle agent done again, to come back to it</title>
<updated>2026-10-27T12:12:28+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-27T12:12:28+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=50c433e82c368b9861a89deecb74e5f29fa50353'/>
<id>urn:sha1:50c433e82c368b9861a89deecb74e5f29fa50353</id>
<content type='text'>
like "mark as unread" in mail: an agent you have seen (idle) becomes done
again, last among done agents of its importance, with no sound or
notification. it stays done until you come back to it: leave its tmux
window and return, or jump to it. marked while you look at it, it holds
until you look away (`held`); a new turn clears it. only idle agents can
be marked.

- daemon: `pane.mark_unread` on the hook socket; the watcher applies it
  after the tick's observations (mark_unread, advance's hold rule).
- `dingger unread [--pane]`: refuses done, working, blocked, and panes
  without an agent; returns once the agent shows as done.
- pi: /unread (refused while the agent works or is blocked).
- panel: right-click an idle agent in the full list, "Mark as unread".
- search: ⌘U or right-click marks the selected idle agent; the footer
  offers ⌘U when it applies; search stays open.

tests: unit (hold, jump, new turn, idle only), e2e step 22 (queue
position, hold, look away and back, jump, refusals, no sound, menu, the
extension), pi-live in real pi 1.4 (a mark, not a turn: an unknown
command would also end done), swift selftest (menu only for idle, footer).
</content>
</entry>
<entry>
<title>hooks: answer each message, so the extension sends a report once</title>
<updated>2026-10-26T14:40:50+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-26T14:40:50+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=53e86be78d7a80a114cf265dbf8189606297d040'/>
<id>urn:sha1:53e86be78d7a80a114cf265dbf8189606297d040</id>
<content type='text'>
the pi extension (herdr's protocol) waits for a reply line and sends a
report again after 0.5 s without one; dingger never replied. every report
arrived twice and took about 2 s, delaying the next one (the live log: 196
of 400 reports repeated the previous one, 0.49 s later). the daemon now
answers each line with {"ok":true}; senders that do not read replies
(dingger poke, importance, the claude hook) are unaffected.

e2e step 21: one report, done in 0.38 s; without replies, two, 2.08 s.
</content>
</entry>
<entry>
<title>importance: mark an agent high, normal, or low</title>
<updated>2026-10-26T14:32:18+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-26T14:32:18+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=185af91b4d525db6c192a050b53615772fb5fe11'/>
<id>urn:sha1:185af91b4d525db6c192a050b53615772fb5fe11</id>
<content type='text'>
within a status, more important agents are listed first, and so jumped to
first; status stays first. a high agent shows a bold ! before its name, a
low agent's name is lightly dimmed with "low" on its second line, and it
makes no sound (it still notifies and waits in line).

in pi, /importance high|normal|low (completes the levels; none once one is
typed out, so Enter runs it). the extension saves it in the session as a
custom entry (dingger-importance), so /reload, restarts, /resume and
restore bring it back; /new starts at normal. it shows a level other than
normal in pi's footer, emits dingger:importance { level } for other
extensions, and sends it with every report. for other agents, `dingger
importance [level] [--pane]`; it refuses a pi that reports itself, whose
saved level would win.

the daemon stays the one writer of pane state: it writes
@dingger_importance (unset for normal), which goes with the agent and
survives a daemon restart. `dingger status` marks high and low.

tests: e2e step 21 (order, the option, no sound for low, the menu bar,
the command, the extension through a harness that now matches pi's
extension API), a DINGGER_SOUND seam so sounds are observable, and a
status-line check that read the counts once (flaky) now polls.
pi-live: /importance in real pi 1.4, its footer and session entry, and the
level after a restore; every pi there now uses the test's agent directory
(a restored pi read the user's). negative controls fail each part.
</content>
</entry>
<entry>
<title>restore Claude Code agents in their own sessions</title>
<updated>2026-10-23T12:08:53+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-23T12:08:53+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=7520141a55e8876824dcb4e2cf5e8e3027efa59d'/>
<id>urn:sha1:7520141a55e8876824dcb4e2cf5e8e3027efa59d</id>
<content type='text'>
`dingger install claude` adds a SessionStart hook to Claude's settings
(every other key and hook kept, in order; the original backed up; written
through a symlink; installing again changes nothing). The hook is
`dingger hook claude`: it reports the session's transcript for its tmux
pane, prints nothing (Claude adds SessionStart output to the conversation)
and always exits 0. Claude's state still comes from its screen.

restore types `claude --resume &lt;id&gt;` in the directory the session ran in
(elsewhere, Claude moves the conversation there). it resumes an agent only
when its session file exists: a Claude session that never got a message
has no transcript, and `--resume` fails ("No conversation found"). the
snapshot now keeps an agent's command line too, so such a pane gets it
retyped instead of nothing.

- the hook receiver takes a report without a state: only the session.
- a report that arrives after the watcher listed the panes is not dropped
  as "agent gone": Claude reports once, as it starts.
- serde_json preserve_order (indexmap is already a dependency, via toml).

e2e step 18: install against a settings file like a real one, a fake
claude running the exact installed hook command, resume by id, retype
without a transcript, the resumed agent reporting again. a server that
restore starts now gets the test's plain shells: it got the user's login
shell, whose profile put the real claude first, which started (stuck at
its trust dialog) during development. every test restore now checks that
no real pi or claude runs in a test pane.
</content>
</entry>
<entry>
<title>restore, step 2: resume agents, retype command lines</title>
<updated>2026-10-23T10:20:11+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-23T10:20:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=c082f5044eedd2294d72f920d1018e40d73aab33'/>
<id>urn:sha1:c082f5044eedd2294d72f920d1018e40d73aab33</id>
<content type='text'>
- the pi extension reports its session file; the daemon keeps it in the
  pane option @agent_session, and snapshots carry it. restore runs
  `pi --session &lt;file&gt;` in the rebuilt pane, one agent every 0.5s, then
  reports which agents run and which were interrupted mid-turn.
- other panes: the command line running in them is typed at the new
  prompt but not run. exactly as typed with the zsh hook (dingger install
  zsh: preexec/precmd keep @dingger_cmd), else the foreground program's
  exact argv from the kernel (KERN_PROCARGS2), shell-quoted.
- "at a prompt" is the pane's own shell owning the terminal's foreground
  process group, not a name check: `sh -c ...` is a running program.
- an agent or command that exits keeps its place in snapshots for 60s, so
  a shutdown that stops programs before tmux still restores them.
- typing waits for each new prompt (text typed earlier echoes twice).

e2e step 16 covers resume with a session path containing a space, the
interrupted report, an agent that exits just before the server dies, a
quoted command retyped not run, and the hook's text; dropping the grace
period, judging prompts by name, running retyped commands, or not storing
the session reference each fail it. the test's fake pi is guarded so a
real pi never starts.
</content>
</entry>
<entry>
<title>restore, step 1: layout snapshots and dingger restore</title>
<updated>2026-10-23T10:01:52+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-23T10:01:52+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=0f06b0b15d6b064703fa1166fb8e7ceda209aaa8'/>
<id>urn:sha1:0f06b0b15d6b064703fa1166fb8e7ceda209aaa8</id>
<content type='text'>
the daemon saves the tmux layout when it changes: sessions, windows
(index, name, automatic-rename, layout, size, zoom, active), and panes
(directory, active, agent). a new history file for layout changes, a
rewrite for details, the last 50 kept, nothing saved while tmux is down.
when a different server runs (or none), the old server's last snapshot is
pinned as the restore candidate before the new server's snapshots start.

dingger restore rebuilds missing sessions by tmux id: panes split from the
newest so they keep their order, the saved layout applied at the saved
size, then the size released so windows follow clients again. a bare
session of the same name (a terminal's `new-session -As 0`) is replaced,
its clients switched over first; used sessions are kept. --dry-run,
--list, --from, --session.

e2e step 16 kills a server with a client attached like ghostty's, restarts
it, restores, and compares every window and pane; splitting the wrong
pane, not replacing bare sessions, not pinning, or keeping the saved size
each fail it. e2e keeps its state in its temp dir.
</content>
</entry>
</feed>
