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
| Key | Action |
|---|---|
| q / Ctrl+C | Quit lazyshell |
| Tab | Switch the active panel |
| → | Go to the output panel |
| ? | Show the help |
| j / ↓ | Next session |
| k / ↑ | Previous session |
| n | New session |
| N | New session, asking for its name first (empty = automatic name) |
| M | New session in a chosen directory |
| c | Duplicate the selected session |
| r | Rename the selected session |
| R | Restart an exited session |
| x / d | Kill the selected session |
| D | Delete the session for good (removed from the panel) |
| w | Export the session's scrollback to a file |
| b | Mark / unmark the session for broadcast |
| B | Jump to the next blocked agent session |
| g | Assign the session to a group, picked from a list or typed |
| G | Show only the session's group; press again to clear |
| A | Mark / unmark the whole group for broadcast |
| X | Kill every session of the group |
| W | Restart every exited session of the group |
| / | Filter the sessions list |
| z | Zoom: the output panel takes the whole screen |
| [ / ] | Previous / next output panel tab |
| F12 | Show / hide the debug panel (only does something under --debug) |
Keys — output panel
While the output panel is focused, these apply instead:
| Key | Action |
|---|---|
| ← | 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 Esc | Same, without a key to learn: two Escapes in a row, within 400 ms |
| i / Enter | Resume typing: back to pass-through (only needed once locked) |
| PgUp / PgDn | Scroll one screen through the scrollback |
| Ctrl+U / Ctrl+D | Scroll half a screen |
| / | Search the scrollback; n / N for the next / previous match |
| v | Start (or extend) a line selection — copy mode |
| y or a second v | Copy the selection (OSC 52, or the configured fallback command) |
| { / } | Jump to the previous/next prompt (needs shell-integration OSC 133) |
| Y | Copy the last finished command's output (needs shell-integration OSC 133) |
| Esc | Leave 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:
-
Starting a session (n, N, M, c) or
restarting one (R) lands you straight inside it: the output panel
takes the focus and pass-through is armed (even if you were locked a moment ago), so you can
type immediately. Ctrl+O locks the panel, and so do two Esc in a row
within 400 ms — a genuine double press, the first Esc still reaching the session so
it keeps working in
vimand in an agent session. -
A shell that ends on its own —
exit, Ctrl+D, or whatever it was running finishing — takes the interface with it: the panel locks and focus goes back to the sessions panel, on that same session. It stays selected and listed, exited, so R restarts it (landing you back inside it, unlocked) and x / D disposes of it.
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.
| Marker | Meaning |
|---|---|
! | 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.
Search and copy
/ 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:
- { / } jump to the previous/next prompt in the scrollback.
- Y copies the last finished command's output in one keystroke, without entering copy mode.
- The sessions list shows
✗ <code>for a non-agent session whose last command exited non-zero (see Reading the sessions list), and a desktop notification fires the same way an agent session'sblocked/donedoes — for a non-agent session only, since an agent session already gets its own.
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.
| Tag | What it is |
|---|---|
KEY | A 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 |
ACT | An action that fired — a keybinding, a mouse gesture, or one of the branches the output panel's editor handles itself |
EVT | Session 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.