Skip to content

Releases: mohseenrm/zj-agent-mob

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 10 Aug 06:57

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-notifierosascriptnotify-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 $?
0

The 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:

  • zellij exits 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_timer only 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 | sh

Requires jq. See the README
for the manual path and configuration.

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 09 Aug 20:16

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 | sh

Requires jq. See the README
for the manual path and configuration.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 09 Aug 18:56

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 | sh

Important

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_ID reached 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.md gains Cross-session status: the spool and a Who owns which row section; troubleshooting.md gains a step-by-step guide for a row stuck on found.
  • 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

Choose a tag to compare

@github-actions github-actions released this 08 Aug 21:56

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-title and Codex's session rollout, refreshed only on turn boundaries from a bounded window so multi-megabyte transcripts stay cheap.
  • Stop shows 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 Stop cannot 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 | sh

Then bind a key and restart any running claude / codex sessions. Full instructions in the README.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 06 Aug 23:44