lazyshell

Usage

Run lazyshell in a terminal. Tab switches focus between the sessions panel and the output panel, with / as directional equivalents; ? opens, at any time, a help popup listing every binding in effect — including the ones you have remapped.

The two panels

On the left, the sessions list: one line per session, with its state. On the right, the output panel: the live screen of the selected session. On a narrow terminal the layout switches to portrait mode and stacks the two panels instead of splitting them side by side (thresholds: portrait_max_width and portrait_min_height).

Each panel also carries its most-used keys on the bottom line of its frame. The list shortens to whatever fits the panel's width, and the output panel's adapts to what it is doing: the way back to pass-through while locked, the way to lock it while in pass-through, no scrolling hint while a full-screen application has the session.

Keys — sessions panel

KeyAction
q / Ctrl+CQuit lazyshell
TabSwitch the active panel
Go to the output panel
?Show the help
j / Next session
k / Previous session
nNew session
NNew session, asking for its name first (empty = automatic name)
MNew session in a chosen directory
cDuplicate the selected session
rRename the selected session
RRestart an exited session
x / dKill the selected session
DDelete the session for good (removed from the panel)
wExport the session's scrollback to a file
bMark / unmark the session for broadcast
BJump to the next blocked agent session
gAssign the session to a group, picked from a list or typed
GShow only the session's group; press again to clear
AMark / unmark the whole group for broadcast
XKill every session of the group
WRestart every exited session of the group
/Filter the sessions list
zZoom: the output panel takes the whole screen
[ / ]Previous / next output panel tab
F12Show / hide the debug panel (only does something under --debug)

Keys — output panel

While the output panel is focused, these apply instead:

KeyAction
Go back to the sessions panel (only while locked)
Ctrl+O (configurable)Lock the panel: leave pass-through for scrolling, search or copy-mode
Esc EscSame, without a key to learn: two Escapes in a row, within 400 ms
i / EnterResume typing: back to pass-through (only needed once locked)
PgUp / PgDnScroll one screen through the scrollback
Ctrl+U / Ctrl+DScroll half a screen
/Search the scrollback; n / N for the next / previous match
vStart (or extend) a line selection — copy mode
y or a second vCopy the selection (OSC 52, or the configured fallback command)
{ / }Jump to the previous/next prompt (needs shell-integration OSC 133)
YCopy the last finished command's output (needs shell-integration OSC 133)
EscLeave the search, or cancel the selection in progress

Who holds the keyboard

Pass-through is the default the moment a session is selected — there is nothing to press first. Moving the selection with j / k, a click or the wheel carries the current state over: land on another session while locked and you land locked; land on one while unlocked and you land ready to type. The exception is a session whose state was decided about — one a project file declared locked: for (a declared command: starts locked by default), or one you locked or unlocked by hand: that choice is remembered per session and restored when you come back to it. Beyond that, lazyshell only switches the flag on its own at the two moments where the intent is unambiguous:

Moving the selection with j / k is navigation and never does this. Nothing happens behind a popup: a confirmation or the help keeps the focus it has.

Reading the sessions list

Each session is one line: a four-column gutter, then its name, status, PID, and either the terminal title the shell set (usually the running command) or its working directory.

MarkerMeaning
!The session rang the bell while you were looking elsewhere. Cleared when you select it.
#A full-screen application (vim, htop, less) has the session. Shown as [ALT] in the status bar for the selected one.
The session produced output while it wasn't the one on screen. Cleared when you select it.
+The session is marked for broadcast — see below.
· State of an AI agent session: idle, working, blocked (waiting on you), done.
✗ <code>A non-agent session's last command exited non-zero — see shell-integration OSC 133. Never shown alongside an agent session's own state marker above.
↻<count>A session declaring restart: has needed at least one automatic restart. Cleared once a restarted run stays up long enough to count as healthy again.

Every marker is configurable, and one set to "" disappears — see the reference.

Output panel tabs

The output panel carries three tabs, switched with [ / ] or by clicking the title bar:

terminal

The session's live screen: the default tab, and the only one where pass-through exists.

resources

What the session is consuming: CPU percentage, resident memory, threads, disk I/O — with a sparkline next to each number and a four-line braille graph for the foreground process's CPU. The measured scope is the shell plus the foreground process group leader, never the whole tree: what you are looking at is claude or vim, not bash.

Sampling runs in the background for every session, whether the tab is open or not (perf.refresh_interval_ms): a curve is only worth looking at if it goes back further than the moment you opened it. Whatever an OS cannot answer is marked unavailable, never reported as zero — on macOS, thread count and disk I/O are in that category.

environment

The session's environment at launch time, sorted and scrollable. By default the value of variables whose name looks like a credential (TOKEN, SECRET, PASSWORD, AUTH, ..._KEY) is masked: the panel is as shareable as a screenshot of it. env_tab.mask_secrets: false shows the real values.

Leaving the terminal tab disarms pass-through and copy mode. The scrollback offset is not reset, though: a round trip returns you where you left off.

Known limitation. You cannot click a tab from pass-through: gocui consults the view's mouse handling before reaching the tab bar. Leaving pass-through first (Ctrl+O) is a prerequisite anyway.

/ in the output panel searches the scrollback, n and N walk the matches, Esc leaves. v starts a line selection and y (or a second v) copies it.

Copying goes through OSC 52, that is, through the host terminal — so it works across an SSH connection too. There is no way to detect whether a terminal supports OSC 52, so for the ones that don't, clipboard.fallback_command is a manual switch: the command is run with the yanked text on its stdin.

While a full-screen application is in control, scrolling back through history — and copy mode, which selects out of that same history — is disabled: the alternate screen does not feed the scrollback, and those keys belong to the application.

Groups

A session belongs to one group, or to none. Grouped sessions are drawn under a header line naming the group, which is what makes a list of eight AI agent sessions readable: you see at a glance which ones are working on the same thing.

Headers are display only — they cannot be selected, clicked or collapsed, and j / k step straight past them. The order is: the groups the project file declares, in the order it declares them, then any group created at runtime in order of first appearance, then the ungrouped sessions last. With nothing grouped there are no headers at all — the list is exactly the flat one it has always been.

Declare groups in the project file, or set one at any time with g, which opens a picker: every group already in use, plus "no group" and "+ new group…" for one that isn't listed yet. Four keys then act on the selected session's whole group: A broadcasts to it, X kills it, W restarts whichever of its sessions have exited, and G narrows the list to it. A group action always reaches every member, including any a filter is currently hiding — "kill the group" means the group, not the part of it on screen.

W skips the sessions still running rather than refusing to do anything: a group that is part finished and part alive is the normal case. Unlike R on a single session, it does not hand you the keyboard afterwards. Agents can drive all of this over the control socket.

Broadcast

Mark two or more sessions with b, then attach to any one of them (i / Enter): every keystroke now goes to all of them at once, not just the one you're looking at.

The status bar carries a ⚠ BROADCAST → N sessions warning the whole time it is armed, in front of whatever else it would otherwise say. This is the one state where a keystroke you don't expect to matter can reach several shells behind your back, so it stays visible no matter what.

Unmark a session (b again) to drop it out; broadcasting stops on its own once fewer than two remain marked.

Mouse

On by default. Click a session to select it — that's navigation, so it does not hand the keyboard to the shell; double-click does. The wheel scrolls the output panel's content, and never the shell's command history: lazyshell handles the wheel itself instead of letting the terminal turn it into arrow keys, which at a prompt would recall the previous command instead of scrolling. Click and drag to select lines, then y to copy — releasing the button copies nothing on its own.

A program inside a session gets the mouse only once it asks for it (vim with set mouse=a, htop); a shell or an AI agent CLI never asks, so the wheel keeps scrolling the scrollback. Set mouse.forward_to_app: false to keep the mouse for lazyshell regardless.

What the mouse costs: Shift+↑ and Shift+↓ are no longer forwarded to a session. gocui gives those keys and the mouse buttons the same values, so they cannot both work — see ADR 0003. Set mouse.enabled: false to get them back, at the price of the gestures above.

Shell integration (OSC 133)

A shell that emits the standard OSC 133;A/B/C/D marks around each prompt and command — zsh, fish and bash all support this, usually behind a one-line addition to your shell's startup file (check your shell/prompt's own docs for "shell integration" or "semantic prompt") — unlocks three things with no further configuration needed:

Nothing here needs a hook wired up the way agent state does: a shell with integration enabled emits these marks on its own, and a shell without it simply never triggers any of the above — no marker to turn off, no config key to disable it, nothing to detect. Only the sessions list's glyph is configurable (markers.command_failed, see the reference); everything else here has no glyph or key to remap beyond the three action ids (jump_prev_prompt, jump_next_prompt, copy_last_output) in the keybindings map.

Marks are tracked per session, survive scrollback truncation (see ADR 0008 for how), and are suspended while a full-screen application has the session — the same "signals, never switches mode on its own" principle as the mouse and copy mode above: nothing typed by vim or htop is ever mistaken for a shell's own prompt or command boundary.

Debug mode

Once lazyshell owns the terminal there is nowhere left to print: stderr is gone and the status bar is one line. --debug is the way to see what the interface thinks is happening.

lazyshell --debug

It does two things at once. It appends to ~/.config/lazyshell/debug.log — next to config.yml, 0600, never truncated, so two runs can be compared — and it opens a small panel in the output panel's top-right corner showing the last events live. F12 hides and shows that panel; the file keeps being written either way.

TagWhat it is
KEYA keystroke as the output panel received it: its name, the raw key/ch/mod values, what normalisation made of them when the two differ, and which mode it landed in
ACTAn action that fired — a keybinding, a mouse gesture, or one of the branches the output panel's editor handles itself
EVTSession created / killed / exited, agent state transitions, selection and tab changes, panel resizes

Two things worth knowing before you read a log. Keys are only recorded for the output panel: gocui offers no global keyboard hook, so a key pressed on the sessions panel shows up as an ACT line if it is bound, and not at all if it is not. And F12 no longer reaches the session while lazyshell is running, debug mode or not — it is a global binding; remap toggle_debug if something you run needs it.

The log contains every keystroke typed into a shell, including at a password prompt of a program that does not turn echo off. It is written 0600 for that reason; delete it when you are done with it.