Releases: mohseenrm/zj-agent-mob
Release list
v0.4.0
A feature release, aimed at one loop: start an agent, look away, come back the
moment it needs you. Until now the panel could only help while you were looking
at it. This release makes it reach out when you are not, and makes answering a
keystroke instead of a context switch.
Added
Desktop notifications, on by default. When an agent starts waiting or
failed, you get a system notification even when Zellij is not the focused
window - which is exactly when a blocked agent is invisible. show_self only
raises a floating pane inside Zellij, so this is the case the panel
structurally could not cover.
The plugin decides rather than the hook, because only the plugin holds every
agent's state. That buys three things a per-hook notifier could not: a burst of
five blocked agents becomes one banner instead of five stacked ones, each agent
is rate-limited, and nothing fires while the panel is already on screen.
LaunchOrFocusPlugin "file:~/.config/zellij/plugins/zj-agent-mob.wasm" {
floating true
notify "waiting,failed,done" // add done; "" turns notifications off
notify_sound "true"
}| option | default | |
|---|---|---|
notify |
waiting,failed |
Which transitions notify. Any of waiting, idlewait, failed, done |
notify_cooldown |
60 |
Seconds before the same agent may notify again |
notify_sound |
false |
Play a sound with the notification |
The notifier is probed once (terminal-notifier → osascript → notify-send)
and cached. Finding none disables the feature silently, and the panel behaves
exactly as it did before.
Quick reply. y answers a blocked agent, m opens a
one-line editor - Enter sends, Esc cancels. Both are
restricted to agents that are actually blocked, because keystrokes sent to an
agent mid-turn land as stray input.
Fleet summary for status bars. Opt-in. Writes 2 waiting · 3 working on
every change and pipes it as zj-agent-mob-summary, so you can see whether
anyone needs you without opening anything:
summary_file "/tmp/zj-agent-mob-summary"zjstatus picks it up with its pipe widget;
anything else (a starship prompt, a shell script) can read the file.
Fleet ergonomics. D clears every done badge at once,
n opens a new agent in the selected row's directory, and a row blocked
for more than two minutes paints as an error so it stops blending into the list.
Changed
x, y and m now work across sessions. These
were previously refused for foreign rows, because the plugin's shims are
session-local - the old README told you to jump first. They are now routed
through zellij --session X action, verified against live sessions for all
three.
Urgent transitions skip the poll. Each open panel drops a beacon, and the
hook pipes waiting / failed / done straight to those sessions instead of
letting them wait for the next scan. Heartbeats keep the spool-only path, so the
hot path costs nothing extra. Set ZJ_AGENT_FANOUT=0 to opt out.
Fixed
Three wrong-target bugs, all found during review of this branch. Each is covered
by a test that fails without its fix.
Replies went to the cursor, not the agent they were composed for. Another
agent blocking mid-typing re-sorts the list, so the text would have landed in the
newly-selected agent's prompt.
The second x acted on the selection, not the arm. kill_armed
held an AgentId, but only ever used it as an equality test - so any pipe
arriving between the two presses could move the confirmation onto whatever had
taken the cursor. A failed row outranks everything in the sort and is itself
a fan-out trigger, which makes this routine rather than contrived.
Sessions were addressed by their sanitized name. AgentId.session is folded
to [a-zA-Z0-9._-] so it can key a spool filename, and that same string was
being passed to zellij --session. A session named my session keys as
my_session, which addresses nothing:
$ zellij --session my_session action write-chars "y"
Session 'my_session' not found. The following sessions are active:
...
$ echo $?
0The row was removed optimistically, so the kill appeared to succeed while the
agent kept running. Identity and addressing are now separate concerns: sanitized
for filenames, real_session() for anything taking --session. This also fixes
a pre-existing bug in the jump path on v0.3.1.
Two related fixes fell out of the above:
zellijexits 0 for a session that does not exist, reporting it on stderr
only. The failure handler checks both streams, or it would have missed exactly
the case it exists for. Failures now render under the list instead of the row
silently vanishing.- Waiting-escalation never fired locally.
arm_timeronly ticked for active
or foreign rows, so a blocked local agent - the single-session case the
feature is for - froze the clock and never reached its threshold.
Upgrading
No configuration changes are required; notifications turn themselves on, and
everything else is opt-in. Restart your agents after upgrading so they pick up
the new hook script.
curl -fsSL https://github.com/mohseenrm/zj-agent-mob/releases/download/v0.4.0/init.sh | shRequires jq. See the README
for the manual path and configuration.
v0.3.1
A bug-fix release. Agents in sessions whose name contains a non-ASCII character
were invisible to panels in other sessions; they now appear. No new features,
and nothing to change in an existing install beyond upgrading.
Fixed
Sessions with non-ASCII names were invisible cross-session.
The hook writes a status record to <session>.<pane> and the plugin reads that
filename back after folding the session name Zellij hands it. Both sides fold
unusual characters to _, and the two folds disagreed.
The hook used tr -c '[:alnum:]._-', which is locale-dependent: it works on
characters under a UTF-8 locale and on bytes under C. The plugin folded
characters and only accepted ASCII. So a session named café produced three
different answers:
| filename | |
|---|---|
hook, under LC_ALL=C |
caf__.3 |
| hook, under a UTF-8 locale | café.3 |
| plugin looked for | caf_.3 |
The plugin found the record under neither locale, so those agents never
showed up in another session's panel, and which way it broke depended on the
user's environment rather than on anything in the repo.
The hook now pins LC_ALL=C and spells the class as a-zA-Z0-9._-, so the fold
cannot move with the locale, and the plugin folds bytes to match what tr
actually does (one _ per byte, so café → caf__). Three differential tests
run both implementations against each other and read the hook's own SESSION=
line out of the shipping script, so a restated copy cannot drift.
Upgrading: ASCII session names — mob, my-session, my session — fold
identically before and after, so this is a no-op for most installs. Only names
that were already broken change behaviour.
Internal
The hook end-to-end suite is now Rust. tests/e2e-hook.sh (708 lines of
shell) is replaced by tests/hook_e2e.rs, which runs under cargo test. It
parses the hook's --args into a map instead of substring-matching, which
closed a live false pass: the old suite asserted detail=rate limited against
an emitted rate limited retry in 30s, so a regression anywhere past the prefix
went unnoticed. All 113 shell assertions were audited by name and carried over,
plus two cases the shell suite never had.
The plugin bin is cfg-gated to wasm32. register_plugin! pulls in
host_run_plugin_command, a Zellij wasm import with no native definition, so
cargo build --bin had never linked natively — nothing forced it while there
were no integration tests. Off-wasm the target is now an empty main. The
shipped wasm is unchanged and still exports _start, load, update,
render, pipe and plugin_version.
serde_json is a dev-dependency only and never reaches the wasm binary.
Install
curl -fsSL https://github.com/mohseenrm/zj-agent-mob/releases/download/v0.3.1/init.sh | shRequires jq. See the README
for the manual path and configuration.
v0.3.0
Agents in other Zellij sessions now report live status.
Before this release the panel found agents everywhere and could jump to them, but only agents in your current session reported what they were doing. Everything else sat at found and decayed to unknown after 60 seconds. That undercut the whole point of the panel: "who is blocked waiting on you" only held inside one session.
Now every agent reports live status, across every session — including sessions with no attached client, which Zellij's SessionUpdate structurally cannot see.
zj-agent-mob 1 waiting · 2 working · 1 done
▶ 1 ● codex waiting 2s web Fix flaky checkout test
└ needs approval: rm -rf node_modules · pane:5
2 ⠙ claude working 14s api Add retry to webhook client
└ Edit src/webhook.rs · 2 turns · pane:9
Rows 1 and 2 can now be in three different sessions and all stay live.
How it works
The hook writes one small record per agent to $TMPDIR/zj-agent-mob-<uid>/status/<session>.<pane_id>, and the panel reads that directory on the same command that already runs its process scan — so polling still costs one command, not two.
The write is a printf plus an atomic rename(2). No subprocess, nothing to block on. That matters because this runs on the critical path of every tool call.
Two designs were considered and rejected:
| Approach | Why not |
|---|---|
| Hook pipes to every session | N subprocess spawns per tool event, on the turn's critical path |
| A designated hub session | zellij --session X pipe blocks when the hub has no panel open — stalls every agent on the machine |
The rule that makes it safe
A spool record never creates a row. Existence comes from the process scan; the spool only refines a row the scan already justified.
So a leftover file cannot resurrect an agent that exited, and losing or clearing the spool degrades to the previous behaviour rather than breaking anything. Four defences keep a stale record from showing wrong data:
| Defense | Stops |
|---|---|
| No process, no row | A record for an agent that has exited |
session_id must match |
A recycled pane id inheriting the previous agent's status |
ts past the stale threshold ignored on read |
A record from a previous boot or a long-idle agent |
| Filename must match the record's own session/pane | A malformed or mislabelled file |
Records are dated relative to the newest one seen rather than against a wall clock the plugin does not have, so a host clock jump cannot pin a row as permanently current.
Upgrading
curl -fsSL https://github.com/mohseenrm/zj-agent-mob/releases/download/v0.3.0/init.sh | shImportant
Restart your claude / codex sessions. Hooks are read at session start, so an agent still running with the old hook writes no record and will keep showing found from other sessions. This is the one manual step.
No configuration changes are needed. Existing keybindings and layouts keep working.
Privacy and opting out
Records contain task summaries, which are your own prompts. The spool directory is created 0700 and namespaced by uid, so on a shared /tmp another user cannot read it.
To turn the cross-session transport off entirely, set ZJ_AGENT_SPOOL=0 in the agent's environment. Status for the agent's own session keeps working through the existing pipe.
| Variable | Default | Meaning |
|---|---|---|
ZJ_AGENT_SPOOL |
1 |
Set 0 to stop writing status records |
ZJ_AGENT_SPOOL_DIR |
$TMPDIR/zj-agent-mob-<uid>/status |
Where records are written |
Also in this release
- Hardening:
ZELLIJ_PANE_IDreached a file path unsanitized while session names were already folded. Found by adversarial testing; now rejected unless numeric. No shell execution was possible either way. - Docs:
how-it-works.mdgains Cross-session status: the spool and a Who owns which row section;troubleshooting.mdgains a step-by-step guide for a row stuck onfound. - The README's "no daemon, socket, or state file" is now "no daemon and no socket" — the spool is a state file, and quietly dropping the claim would have been worse than correcting it.
Verification
174 unit tests (up from 148), 113 hook end-to-end cases (up from 85), and 98 installer cases.
Two tests drive the whole loop rather than one layer: one runs the real hook script, the real scan script, and the real merge in sequence; the other replays bytes captured from two live Zellij 0.44.3 sessions.
Full Changelog: v0.2.0...v0.3.0
v0.2.0
Better hook integration with Claude Code and Codex, and the panel now sees every agent you have running rather than just the ones in the current session.
Added
Cross-session monitoring. Agents in other Zellij sessions now appear in the panel, and Enter jumps straight to them across tabs and sessions. Rows carry a (session, pane) identity so ids can no longer collide between sessions.
Agent discovery. A process-environment scan finds agents that have never fired a hook, so an agent that was already running when you installed the hooks still shows up as found instead of being invisible. Toggle with the discover config key.
Answering permission prompts from the panel. With ZJ_AGENT_APPROVE=1, a permission prompt parks in the panel and a / r approve or reject it without leaving the panel. Opt-in, since it is the one hook that blocks a turn, and it falls through to the agent's own prompt after ZJ_AGENT_APPROVE_TIMEOUT seconds (default 30).
Richer statuses. failed (rate limit, billing, auth), idle-wait (waiting on you a while), compact (compacting context, which otherwise looks like a hang), found (seen by the scan, no hook yet), and unknown (its session is gone). Rows sort by urgency, so whatever needs you most is at the top.
More hook events. PermissionRequest, StopFailure, PreCompact / PostCompact, plus SubagentStart / SubagentStop and TaskCreated / TaskCompleted as counter deltas on the parent's row.
Notifications. The panel can pop itself open the moment an agent blocks (popup_on_waiting, on by default).
init.sh in the release. One-line install with no clone and no Rust toolchain: the installer fetches the hook and wasm from the same tag it was downloaded from, and leaves a copy at ~/.config/zj-agent-mob/install.sh so the in-panel install screen works from then on. Adds --version, --from-release, --no-download, and --dry-run.
Changed
- Task summaries read Claude's
ai-titleand Codex's session rollout, refreshed only on turn boundaries from a bounded window so multi-megabyte transcripts stay cheap. Stopshows the turn's closing message rather than the summary from before the turn.- Installer resolves symlinks and writes through to the real file, so stow-managed settings land in your dotfiles repo instead of being detached.
- Hook entries in settings carry a literal
$HOME, keeping usernames out of committed dotfiles. Older absolute-path installs migrate automatically.
Fixed
- Two-step kill: x interrupts and arms the row, and only closes the pane on a second press. Refused for agents in other sessions, where pane ids would resolve to the wrong pane.
- Highlight offsets use character rather than byte positions, so multi-byte glyphs no longer shift colouring onto the following word.
- Counters saturate at zero, so a stray
Stopcannot underflow.
Docs
Rewritten README leading with what the plugin does, a three-step quick start, and reference docs for setup, how it works, troubleshooting, and development.
Install
curl -fsSL https://github.com/mohseenrm/zj-agent-mob/releases/download/v0.2.0/init.sh | shThen bind a key and restart any running claude / codex sessions. Full instructions in the README.
v0.1.0
Full Changelog: https://github.com/mohseenrm/zj-agent-mob/commits/v0.1.0