all repos — dingger @ 9c6b10426bd897afe6cf6f0c022f45310c5ab3d3

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

menu bar app: live agent states from tmux pane options

single-file appkit status item (menubar/main.swift, LSUIElement) polling
@agent/@agent_state pane options every 2s: counts in the bar, per-pane
rows with click-to-focus via switch-client. daemon now also writes an
@agent label per pane so the menu bar and tmux status-right can render
agent names without duplicating identification logic.
Igor Soarez igor@soarez.org
Mon, 19 Oct 2026 16:35:40 +0300
commit

9c6b10426bd897afe6cf6f0c022f45310c5ab3d3

parent

3bace4ac392fb914263377369383f5ac23111c61

M crates/dinggerd/src/tmux.rs → crates/dinggerd/src/tmux.rs

@@ -4,7 +4,6 @@ use std::process::Command;

use detect_core::manifest::agent_state_label; use detect_core::{identify_agent, Agent, AgentState}; -use tracing::debug; #[derive(Debug, Clone)] pub struct Pane {

@@ -25,6 +24,8 @@ pub agent: Option<Agent>,

/// The `@agent_state` user option previously written for this pane, /// read straight from the pane format. pub written_state: String, + /// The `@agent` user option (agent label) previously written, if any. + pub written_agent: String, } impl Pane {

@@ -68,13 +69,14 @@ "#{window_active}\t",

"#{session_attached}\t", "#{pane_start_command}\t", "#{pane_pid}\t", - "#{@agent_state}" + "#{@agent_state}\t", + "#{@agent}" ); let out = run_tmux(&["list-panes", "-a", "-F", format])?; let mut panes = Vec::new(); for line in out.lines() { let fields: Vec<&str> = line.split('\t').collect(); - if fields.len() < 10 { + if fields.len() < 11 { continue; } let current_command = fields[2].to_string();

@@ -101,6 +103,7 @@ window_active: fields[5] == "1",

session_attached: fields[6] != "0", agent, written_state: fields[9].to_string(), + written_agent: fields[10].to_string(), }); } Ok(panes)

@@ -180,3 +183,10 @@ agent_state_label(state),

]) .map(|_| ()) } + +/// Label the pane with the agent running in it, for tmux status-line and +/// menu-bar rendering; clears the label when the pane stops running one. +pub fn set_agent(pane_id: &str, agent: Option<Agent>) -> Result<(), String> { + let label = agent.map(detect_core::agent_label).unwrap_or(""); + run_tmux(&["set-option", "-p", "-t", pane_id, "@agent", label]).map(|_| ()) +}
M crates/dinggerd/src/watcher.rs → crates/dinggerd/src/watcher.rs

@@ -113,6 +113,20 @@ let Some(new_state) = observed else {

debug!(target = %pane.target, "observation skipped (viewer screen)"); continue; }; + + // Keep the pane's agent label current even when the state is + // unchanged (e.g. an agent swap between two idle panes). + let agent_label = pane + .agent + .map(detect_core::agent_label) + .unwrap_or("") + .to_string(); + if pane.written_agent != agent_label { + if let Err(err) = tmux::set_agent(&pane.pane_id, pane.agent) { + warn!(target = %pane.target, %err, "failed to write @agent"); + } + } + if new_state == prev_state { continue; }

@@ -123,11 +137,7 @@ if let Err(err) = tmux::set_agent_state(&pane.pane_id, new_state) {

warn!(target = %pane.target, %err, "failed to write @agent_state"); } - let agent_label = pane - .agent - .map(detect_core::agent_label) - .unwrap_or("agent") - .to_string(); + if let Some(kind) = notification_kind_for_transition(prev_state, new_state) { if pane.suppresses_notifications() { debug!(target = %pane.target, "suppressed (active pane)");
A packaging/launchd/dev.dingger.menubar.plist

@@ -0,0 +1,18 @@

+<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>dev.dingger.menubar</string> + <key>ProgramArguments</key> + <array> + <string>/Users/sz/Applications/dingger.app/Contents/MacOS/dingger-menu</string> + </array> + <key>RunAtLoad</key> + <true/> + <key>KeepAlive</key> + <true/> + <key>ThrottleInterval</key> + <integer>10</integer> +</dict> +</plist>