all repos — dingger @ 3731a80384f5e2b7d3343eb3e4999f626bbc7f91

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

pi extension: identify the pane from TMUX_PANE

the extension asked tmux for "the current pane", which resolves to the
session's active pane, not the pane pi runs in, so agents in background
windows reported their state onto another pane. tmux sets TMUX_PANE for
every process it starts in a pane; use it.

e2e step 12 loads the shipped extension with a fake pi from a pane that
is not its session's active pane and asserts the report lands there; it
fails against the previous extension. hook reports log at info again.
Igor Soarez igor@soarez.org
Tue, 20 Oct 2026 13:04:18 +0300
commit

3731a80384f5e2b7d3343eb3e4999f626bbc7f91

parent

4af6a4f625b1965b783c9efb857f8ac665adb8eb

3 files changed, 37 insertions(+), 18 deletions(-)

jump to
M crates/dinggerd/assets/pi/pi-agent-state.ts → crates/dinggerd/assets/pi/pi-agent-state.ts

@@ -3,29 +3,16 @@ // managed by dingger; reinstalling or updating the integration overwrites this file.

// add custom hooks/plugins beside this file instead of editing it. import net from "node:net"; -import path from "node:path"; -import { execSync } from "node:child_process"; // The dingger hook receiver listens here; override with DINGGER_AGENT_SOCKET. const socketPath = process.env.DINGGER_AGENT_SOCKET || `${process.env.HOME}/.local/state/dingger/agent.sock`; -// The extension runs inside a tmux pane, so it finds its own pane id. -function discoverPaneId(): string | undefined { - try { - const out = execSync('tmux display-message -p "#{pane_id}"', { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - const paneId = out.trim(); - return paneId.startsWith("%") ? paneId : undefined; - } catch { - return undefined; - } -} - -const paneId = process.env.DINGGER_PANE_ID || discoverPaneId(); +// tmux sets TMUX_PANE in the environment of every process it starts in a +// pane, so it names exactly the pane this pi runs in. (Asking tmux for "the +// current pane" is wrong: that is the session's active pane, not ours.) +const paneId = process.env.DINGGER_PANE_ID || process.env.TMUX_PANE; const source = "dingger:pi"; function enabled() {
M crates/dinggerd/src/hooks.rs → crates/dinggerd/src/hooks.rs

@@ -124,7 +124,7 @@ debug!(state = other, "unknown hook state");

continue; } }; - debug!(pane = %params.pane_id, ?state, "hook report"); + info!(pane = %params.pane_id, ?state, "hook report"); { let mut reports = reports(); let pane = reports.entry(params.pane_id).or_default();
M tests/e2e.sh → tests/e2e.sh

@@ -239,6 +239,38 @@ else

fail "menu bar binary not found at $MENUBAR" fi +# --- 12. the shipped pi extension reports for its own pane ------------------- +echo "12. pi extension" +cat >"$DIR/ext-harness.mjs" <<'JS' +// Loads the shipped extension with a fake pi and fires lifecycle events. +const ext = await import(process.env.EXTENSION); +const on = {}; +ext.default({ on: (name, fn) => (on[name] = fn), events: { on: () => {} } }); +const ctx = { mode: "tui", isIdle: () => true }; +for (const event of process.argv.slice(2)) { + await on[event]?.({ type: event, reason: "reload" }, ctx); + await new Promise((r) => setTimeout(r, 300)); +} +JS +ext() { # pane events... (run "inside" a pane: tmux sets TMUX_PANE there) + local pane=$1; shift + TMUX_PANE="$pane" EXTENSION="$ROOT/crates/dinggerd/assets/pi/pi-agent-state.ts" \ + node --experimental-strip-types --no-warnings "$DIR/ext-harness.mjs" "$@" +} +if command -v node >/dev/null; then + # A2 is not the active pane of session a (wa1 is the active window), the + # case where asking tmux for "the current pane" picks the wrong pane. + [ "$(t display -p -t a: '#{pane_id}')" != "$A2" ] && pass "A2 is not session a's active pane" || fail "test setup: A2 is active" + before_a1=$(opt "$A1" @agent_state) + ext "$A2" session_start agent_start + expect "extension report lands on A2 (working)" "opt $A2 @agent_state" working + expect "A1 untouched" "opt $A1 @agent_state" "$before_a1" + ext "$A2" agent_settled + expect "extension completion away from the window: A2 done" "opt $A2 @agent_state" done +else + fail "node not found; cannot test the pi extension" +fi + echo echo "$PASSES passed, $FAILS failed" [ "$FAILS" -eq 0 ] || { echo "--- daemon log tail"; tail -30 "$DIR/daemon.log"; exit 1; }