AI agent sessions
A session whose foreground process is a known AI coding agent CLI gets a gutter marker showing its detected state — idle, working, blocked (waiting on you) or done — instead of only the generic activity marker.
The problem
The generic activity marker (●) cannot tell "it produced output" from "it wants an
answer". With three agents open, that is exactly the distinction you need: two of them are
quietly working, the third has been waiting on a permission prompt for ten minutes.
| Marker | State | Meaning |
|---|---|---|
· | idle | Agent detected, idle. |
… | working | A turn is in progress. |
‼ | blocked | The agent is waiting on an answer from you. |
✓ | done | The agent finished its turn. |
Config-free detection
The claude, codex and opencode CLIs are recognised with no
setup at all: lazyshell reads the built-in manifests (under pkg/agent/manifests)
against the session's visible screen and the terminal title it sets. Nothing to install, nothing
to declare.
Custom manifests
Drop a <process-name>.yml file in ~/.config/lazyshell/agents/ (or
your $XDG_CONFIG_HOME equivalent) to override a built-in manifest or add one for
another agent — the same file name as a built-in replaces it outright, a different name adds to
the set. See the built-in manifests for the format.
Manifests are local only: lazyshell never fetches one over the network.
Authoritative state via hooks
Manifest detection is a guess from what is on screen. A second channel lets the agent
say its state outright. Every session gets its own Unix socket, exposed to the process running
inside it as $LAZYSHELL_SOCK (alongside $LAZYSHELL_SESSION_ID), and
lazyshell hook <state> — one of idle, working,
blocked or done — writes to it.
It is meant to be wired into the agent's own hook mechanism, not typed by hand:
$ lazyshell init --agents # prints the config to paste into Claude Code / Codex
Claude Code
A hooks block in settings.json:
UserPromptSubmit→lazyshell hook workingNotification→lazyshell hook blockedStop→lazyshell hook done
Codex
A notify line in config.toml. Codex has only one event
(agent-turn-complete), so it can only ever report done.
opencode
Not wired up yet: its richest signal is an SSE subscription rather than something it pushes on its own — a different shape of integration, left for later.
Once a session has received a single hook event, manifest-based guessing stops for that session for good: the hook is authoritative from then on, not just until the next screen change.
Agent control API
The hook channel above only lets an agent describe itself. Letting one act — create
sessions, read what the others printed, type into them, or wait for one to reach a given state —
is a separate feature on a separate socket, off unless control.enabled says
otherwise. It is what an "orchestrator" agent needs.
lazyshell ctl list # id, name, status, agent state
lazyshell ctl read session-2 --tail 40 # plain text, no escape codes
lazyshell ctl new --name build --cwd ./api --command 'make test'
lazyshell ctl send build 'echo bonjour' --enter # as if typed
lazyshell ctl kill build
lazyshell ctl rename build tests
Groups (see Groups) are readable and writable from here, which is what lets one agent orchestrate several others as a unit:
lazyshell ctl list --group agents # only that group
lazyshell ctl new --name w1 --group agents
lazyshell ctl group build agents # put a session in a group
lazyshell ctl ungroup build # take it back out
lazyshell ctl group-send agents 'git pull' --enter
lazyshell ctl group-kill agents
The two fan-out verbs print how many sessions they reached. A group that is empty or does not
exist is an error, never a silent "0 sessions": a caller that typo'd a group name must not be told
its kill succeeded. group-send skips sessions that have already exited. Restarting a
group is deliberately not exposed — there is no restart verb for a single session
either; it stays the W key, in the interface.
wait blocks until a session — or, with --group instead of a target, the
first member of that group — reaches a given agent state, instead of an orchestrator having to
loop on ctl list:
lazyshell ctl wait build --state blocked --timeout 300
lazyshell ctl wait --group agents --state blocked # returns on the first one that blocks
--timeout is in seconds and optional, defaulting to 120; a timeout, an unknown
session/group/state, or the targeted session exiting before reaching the state are all reported
as a failure — same non-zero exit as any other ctl error, never a hang.
A session is named by its id (session-2, the value of
$LAZYSHELL_SESSION_ID) or by its exact name; --json prints the raw
response. Unlike lazyshell hook, which always exits 0 so it can never break an
agent's turn, ctl exits non-zero on any failure — a caller that asked for a session
and did not get one has to find out. ctl new deliberately does not steal the
selection or the keyboard the way pressing n does.
Read this before enabling it. There is no token and no per-session permission:
the socket's 0600 file permissions are the entire access control. Turning this on
means every process running under your account can create sessions, type commands into
them and read their output — not just the agents you started inside lazyshell. And
ctl read returns scrollback verbatim, secrets included: the environment tab's
masking has no equivalent here, because a credential echoed into a shell is indistinguishable
from any other text once it is on screen.
Notifications
A session going blocked or done fires a desktop notification — OSC 9
and OSC 777 to the host terminal by default (both sent unconditionally; a terminal that does not
understand one just ignores it) — or the command in notify.fallback_command instead,
with the notification text on its stdin, for a terminal that needs one.
At more than a couple of agent sessions open, B jumps the selection straight to the
next blocked one, cycling and wrapping — the point of the marker and the
notification both.
A non-agent session notifies the same way when its last command — per shell-integration OSC 133 — exits non-zero, so a build or a long-running script does not need to be watched to know it failed. An agent session never fires this second notification on top of its own.
Pattern watchers
Pattern watchers generalize that idea to any session: a regex evaluated against every output line, on top of exit-code detection rather than replacing it — useful for a dev server that logs an error and keeps running, rather than exiting. Declare them in a project file:
sessions:
- name: api
watch:
- pattern: "ERR!"
notify: true
or arm one on the fly on the selected session with v — a single, replaceable pattern
per session, on top of whatever the project file already declared for it; submitting an empty
pattern disarms it. Either way, a match notifies through the same OSC/fallback-command channel
as everything above, at most once every 3 seconds per pattern — a log spraying the same match
200 times in a burst still fires one notification, not 200. Matching is against the visible text
(escape codes stripped) and pauses while a full-screen application (vim,
htop) holds the session, the same alt-screen rule OSC 133 already follows.
Turn duration and stats
A session currently mid-turn (working) shows how long its turn has been running in
the sessions list, e.g. ⏱ 1m32s.
Setting agent_stats_command runs that command for the selected
session only (at most once every 5 seconds — it is meant for something like a token/cost lookup,
not something cheap enough to run per session on every tick) with
$LAZYSHELL_SESSION_ID in its environment, and shows its first line of output next to
the duration — the same "external command, show its output line" shape as Claude Code's own
statusLine.
# ~/.config/lazyshell/config.yml
agent_stats_command: "~/bin/my-token-counter.sh"
What lazyshell does not do
-
It does not count tokens. No agent format is parsed for that;
agent_stats_commandis the intended extension point. - It never calls the agent through the hook socket. That channel is inbound and declarative only: lazyshell listens, and the only thing a hook event can do is set that one state. The control API above is a second, separate socket, and it is closed by default.
-
The control API grants no permission it is not asked for. There is no
deleteverb —ctl killends the process and leaves the session listed as exited, never removes it for good — and nothing in the protocol lets an agent change lazyshell's own configuration or keybindings.