<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dingger.git/packaging/tmux/dingger.tmux, 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-10-23T16:11:23+00:00</updated>
<entry>
<title>raise the terminal window that holds the tmux client</title>
<updated>2026-10-23T16:11:23+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-23T16:11:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=e4d3bc242bd3d18aa83a76f25dcbe5cb7a2ced78'/>
<id>urn:sha1:e4d3bc242bd3d18aa83a76f25dcbe5cb7a2ced78</id>
<content type='text'>
a jump (any click, a shortcut, search) activated the terminal app, which
raised whichever window was used last. now:

- the client: the named one (a status-line click), else one already on a
  session holding the agent's window (several terminal windows can show
  one session; it shows the agent without switching, and keeps its own
  session in a session group), else any; the focused one first, then the
  one used last (its last input or focus).
- Ghostty (1.3+, scriptable by terminal id, not by tty): dingger learns
  which terminal holds each client when it gains focus. tmux's
  client-focus-in hook runs `dingger terminal focused`, which asks Ghostty
  for its focused terminal and records it in @dingger_terminal_&lt;tty&gt;, but
  only if the client is still focused once Ghostty answered (a fast switch
  must not pair the wrong terminal); a terminal holds one client, so an
  older pairing with it is dropped. `focus` raises the exact terminal
  (window, tab, split) through `tmux run-shell`: Apple Events from the tmux
  server count as Ghostty's own, so no permission prompt. a terminal that
  is gone is forgotten and the app activated, as without a pairing.
- in a hook, #{client_name} can be another client (the previous event's);
  the snippet's hook uses #{hook_client}.

e2e step 20: two nested clients on one session, focused by typing focus
reports into their outer panes, a fake osascript as Ghostty, a fake open
(tests never activate a real app); it loads the snippet's own hook line.
each rule has a negative control that fails.
</content>
</entry>
<entry>
<title>restore, step 3: offer it in the panel, menu, and status line</title>
<updated>2026-10-23T10:59:59+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-23T10:59:59+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=9c9c9dad93b16781ec2fc40bd7582db62240171c'/>
<id>urn:sha1:9c9c9dad93b16781ec2fc40bd7582db62240171c</id>
<content type='text'>
when tmux stops (and its sessions are missing from the running server, or
no server runs), the daemon publishes the restore offer: offer.json for the
menu bar app and @dingger_restore for the status line, withdrawn once
restored or dismissed.

- panel: a first row "↺ Restore 9 sessions · 22 agents" with when it was
  saved and how many agents were mid-turn; clicking restores, "Not now"
  dismisses. an offer shows the panel like a waiting agent; off stays off.
- menu: the offer on top; restore all, one session, or not now.
- status line: a clickable blue ↺N before the clock.
- restore runs through the user's login shell, as a terminal starts tmux,
  so restored panes get a real environment, not launchd's; --report writes
  restore.log and notifies a summary. restore --dismiss.
- commands log only warnings by default; SIGPIPE ends them quietly.

e2e: the offer through the real menu bar binary, the summary notification,
the offer clearing, a restore with no tmux running, and not now staying
dismissed when a terminal starts tmux. the second test daemon is started
directly (it leaked through a function's $! before, and dozens of leaked
daemons fought over the test socket and loaded the machine); each run uses
its own socket and checks its daemons stop.
</content>
</entry>
<entry>
<title>rebuild the status model as herdr's state machine, event-driven and tested</title>
<updated>2026-10-20T09:43:07+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-20T09:43:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=4af6a4f625b1965b783c9efb857f8ac665adb8eb'/>
<id>urn:sha1:4af6a4f625b1965b783c9efb857f8ac665adb8eb</id>
<content type='text'>
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
</content>
</entry>
</feed>
