<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dingger.git/crates/dinggerd/src/tmux.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>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>terminal commands are silent when tmux runs them</title>
<updated>2026-10-25T05:44:25+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-25T05:44:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=5d7ed0172366eef885fd490136e8b826a9722339'/>
<id>urn:sha1:5d7ed0172366eef885fd490136e8b826a9722339</id>
<content type='text'>
tmux shows a hook's output and a non-zero exit in view mode over the pane.
the focus hook's normal outcomes (focus moved on, Ghostty not in front)
exited 1, and the one-second settling made "focus moved on" common:
"'dingger terminal focused --client ...' returned 1" kept covering panes.
`dingger terminal focused` and `terminal raise` now always exit 0 and
print nothing unless run by hand.

tmux::client_format checks that tmux answered for the client asked about:
for a client that is gone, tmux answers for the current one, so a raise
for a client that had just detached raised another client's terminal.

e2e step 20: no pane in view mode after the hook's failures, a gone
client raises nothing, and a focus held past the settling second; the old
code fails the new checks.
</content>
</entry>
<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>session groups: list each pane once, save each group once</title>
<updated>2026-10-23T15:45:06+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-23T15:45:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=b4f45632debe6b8dc2f44ca6cc1b003d9093ad33'/>
<id>urn:sha1:b4f45632debe6b8dc2f44ca6cc1b003d9093ad33</id>
<content type='text'>
a pane in several sessions (a session group, `new-session -t`, or a
linked window) was listed once per session: every agent of a group showed
twice in the menu, panel, search, `dingger status`, and the counts. and
snapshots saved each session of a group with its own copy of the windows,
so restore would have rebuilt them twice and resumed each agent twice on
one session file.

- a pane keeps its first listing (tmux lists sessions by name) and is
  attended if the user looks at it through any session of the group.
- snapshots keep the first session of each group; the others are not
  restored.

e2e step 19 covers both; the previous binaries fail it. two checks (step
11's and this one) waited for idle after a turn that started from idle,
which passed before the turn was even processed: they now wait for
working first, and the negative control for group attendance fails as it
should. step 18 now waits for the snapshot it later restores from.
</content>
</entry>
<entry>
<title>write a pane's options in one tmux command list</title>
<updated>2026-10-23T11:07:15+00:00</updated>
<author>
<name>Igor Soarez</name>
<email>igor@soarez.org</email>
</author>
<published>2026-10-23T11:07:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.soarez.net/sz/dingger.git/commit/?id=a7d34521c7f874beb44e0314017ab28979550495'/>
<id>urn:sha1:a7d34521c7f874beb44e0314017ab28979550495</id>
<content type='text'>
the watcher wrote @agent_state, then @dingger_hook, as separate tmux
calls. a daemon stopped between them left a done display state next to a
stale "working" hook state, which the restarted daemon trusted: the agent
came back as working. now every changed option of a pane is one command
list, which tmux applies as a unit (and one process instead of several).

tests/kill-race.sh kills the daemon the instant an agent shows done: 24 of
40 kills left the options inconsistent before, 0 of 40 now. e2e step 17
runs it 20 times; the previous binary fails it.
</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>
</feed>
