Terminals
LeapMux gives you full shell terminals that run on a Worker and stream into your Frontend (browser or desktop app) over the same end-to-end-encrypted channel as your agents. A terminal is a tab, just like an agent or a file viewer — you can tile it, float it, move it between workspaces, and it survives page refreshes and reconnects.
This chapter covers how to open a terminal, how the shell list is built, how to use the terminal view, and how persistence works. It also covers the automatic remote control wired into every terminal.
For the bigger picture of tabs, tiling, and layout, see Tabs & Layout. For the git side of opening a terminal in a worktree or branch, see Worktrees & Branches.
What a LeapMux terminal is
When you open a terminal, the Worker spawns your chosen shell as an interactive login shell (for example bash -i -l or zsh -i -l), connects it to a real pseudo-terminal (PTY), and streams its output to an xterm.js view in your tab. The shell and all of its child processes run on the Worker machine — the same machine where your code and git repositories live — not in your browser.
Every spawned shell gets:
TERM=xterm-256colorso colour-aware programs render correctly.- A set of
LEAPMUX_CONTROL_*environment variables (see Driving LeapMux from inside a terminal). - A process kill group, so closing the tab reaps the whole process tree (the shell and everything it started) rather than leaking orphans.
Each terminal is given an auto-generated title of the form Terminal <Name> (for example Terminal Aaliyah or Terminal Zoe), drawn from a fixed pool of names. You can rename it at any time (see Renaming a terminal).
Opening a terminal
There are three ways to open a terminal from the UI, plus a CLI path.
Quick action: the New terminal button
The tab bar has a terminal icon button. Its tooltip depends on context:
- “New terminal at the current working directory” when there is an active tab to inherit a Worker and working directory from.
- “New terminal…” otherwise.
The default keyboard shortcut is Cmd/Ctrl+T (command app.newTerminal, active only when no dialog is open).
The button is context-aware:
- If you already have a Worker and working directory in scope (because another tab is active), it opens a terminal immediately using that Worker’s default shell, anchored to that working directory. A later restart returns to the same directory.
- If there is no Worker or working directory in scope, it instead opens the full New terminal dialog so you can pick one.
Shell picker: the overflow menu
The tab bar’s overflow (“More options”) menu has a “Terminals” section. It contains:
- “New terminal…” — opens the full dialog.
- One entry per shell that the current Worker reports as available, each shown as its path. The Worker’s default shell is annotated with "(default)".
Clicking a specific shell opens a terminal with exactly that shell, skipping the dialog.
The full New terminal dialog
Open the dialog with Cmd/Ctrl+Shift+T (command app.newTerminalDialog, active only when no dialog is open), via the overflow menu’s “New terminal…” item, or with the quick-action button when it shows “New terminal…”. Its title is “New terminal”.
The dialog has these fields:
| Field | What it does |
|---|---|
| “Worker” | Selects which Worker spawns the shell. Options show name (version, os, arch). A “Refresh workers” button re-queries online Workers. When none are connected: “No workers online”. |
| “Shell” | Picks the shell binary. A “Refresh shells” button re-queries the selected Worker. See Shell selection. |
| “Working Directory” | Browses the Worker’s filesystem from ~. Type a path and press Enter (or click away) to go there. |
| “Title” | The tab name. Pre-filled with a random Terminal <Name>; the refresh button beside the label (tooltip “Generate random name”) picks another. Type your own to replace it. It cannot be empty. |
| “Git options” | Appears when the selected path is (or becomes) a git repository. Lets you open the terminal in a branch or worktree. See Git options. |
The directory box abbreviates your home directory as ~. If the path style does not match the Worker’s OS, a hint appears below the box. The tree includes a show/hide-hidden-files toggle and a “Refresh directory tree” button, and shows “No workers online. Connect a worker to browse directories.” when no Worker is selected.
Submit with the “Create” button; cancel with “Cancel”. The Create button stays disabled until you have a Worker, a non-blank working directory, a selected shell, a non-blank title, a valid git-mode choice, and a workspace. If creation fails, the dialog reports the failure.
Opening a terminal from the CLI
You can create a terminal from a script or another agent with the Control CLI:
leapmux control tab open --type terminal \
--worker-id <worker> \
--working-dir /home/me/project \
--shell /bin/zsh--shell is optional — leaving it empty uses the Worker’s default shell. --shell-start-dir defaults to the working directory. See Control CLI for the full flag set, entity-ID resolution, and placement flags.
Shell selection
The Shell dropdown is populated per-Worker by querying the Worker for the shells it has installed. While the list is loading it shows “Loading shells…”; if the Worker reports none it shows “No shells available”. Each option shows the shell’s path, and the default shell is labelled <path> (default).
The shell list is per-Worker. Switching workspace while staying on the same Worker does not re-fetch it; switching to a different Worker does, and resets any shell override you had selected.
How the Worker builds the list
- The Worker resolves its default shell (see below) and places it first in the list.
- It then probes a fixed set of well-known shells —
sh,bash,zsh,fish,pwsh,powershell— resolving each againstPATH. Any that are installed are added, skipping the one that is already the default.
Two names that resolve to the same binary (for example sh and bash on many systems) are kept as separate entries, because invoking a shell as sh activates its POSIX mode — the distinction is intentional.
Default shell resolution
When you don’t choose a shell explicitly, the Worker uses its default, resolved in this order:
- The
LEAPMUX_DEFAULT_SHELLenvironment variable (accepts a bare name likezshresolved viaPATH, or an absolute path like/bin/zsh). - The
SHELLenvironment variable. - Platform detection:
- macOS: the user’s login shell from
dscl, falling back to/bin/zsh. - Linux: the user’s shell from
/etc/passwd, falling back to/bin/sh. - Windows:
pwsh, thenpowershell, falling back to the bundled Windows PowerShell. - Other platforms:
/bin/sh.
- macOS: the user’s login shell from
LEAPMUX_DEFAULT_SHELL in the Worker’s environment. See Troubleshooting for the resolution order in full.Login-shell flags
The Worker invokes each shell with interactive-login flags appropriate to that shell. Most POSIX shells (bash, zsh, and the like) get -i -l (interactive login), and PowerShell Core (pwsh) gets -Login. A few edge shells differ: classic Windows PowerShell 5.1 gets none (it has no -Login), cmd gets /D, and tcsh/csh get -l.
Git options: open a terminal in a branch or worktree
When the working directory is inside a git repository, the Git options panel offers the same five modes used when opening an agent or a workspace — use current state, switch to branch, create new branch, create new worktree, or use existing worktree. The modes, their fields, branch-name validation, the worktree path formula, and the dirty-tree warnings are all covered in Worktrees & Branches.
The terminal’s tab is grouped in the sidebar under its repository and branch. If a terminal owns a worktree it created, closing its last tab can offer to remove that worktree (see Closing a terminal).
Using the terminal
The terminal view is a real xterm.js terminal: it renders 256-colour output, supports full-screen (“alt screen”) TUIs like vim, htop, and tmux, and accepts mouse interaction where the running program supports it.
Copy and paste
Selection uses copy-on-select: highlighting text in the terminal automatically copies it to the clipboard (the same behaviour as iTerm2’s “Copy on Select”). Empty selections are ignored. You don’t need a dedicated copy shortcut. Paste using your platform’s standard paste gesture.
Scrollback
The terminal keeps scrollback you can scroll through with your mouse or trackpad. Two shortcuts page the active terminal:
| Shortcut | Command | Action |
|---|---|---|
| Alt+PageUp | app.scrollActiveTabPageUp | Scroll up one page |
| Alt+PageDown | app.scrollActiveTabPageDown | Scroll down one page |
macOS line/word navigation
On macOS, when a terminal is focused, these shortcuts send the correct escape sequences to the shell:
| Shortcut | Action |
|---|---|
Cmd+Left / Cmd+Right | Move to start / end of line |
Alt+Left / Alt+Right | Move one word left / right |
Resizing
The terminal automatically fits its tile. When you resize the tile or window, LeapMux tells the Worker, which resizes the PTY so the running program re-flows correctly. For a terminal whose shell has already exited, the view re-flows the preserved output locally so it stays readable.
Appearance
The terminal uses your monospace-font preference. The default font is "Hack NF", Hack, "SF Mono", Consolas, monospace at size 13.
The Terminal theme row in Appearance settings has two halves, and a third control appears for a palette that offers more than one look per side:
| Half | Options | Behaviour |
|---|---|---|
| Palette | Match UI (default), or any palette the app itself offers | Which colors the terminal paints |
| Mode | System, Light, Dark | Which variant of that palette |
Match UI follows the app theme — palette, variant, and light/dark — and keeps following it after you switch. It governs the rest of the row, which greys out and reports what the app resolved to. Any other palette detaches the row. The controls become live and start from the app’s own mode. System then reads your OS directly; this differs from the app only when the app theme is pinned.
Some palettes offer more than one look, and a variant menu appears beside the palette when one is chosen. It lists both sides at once, under a Light and a Dark heading, and a pick applies to the side that look belongs to.
Each palette supplies the sixteen ANSI colors. The background, foreground, cursor, and selection colors come from the same palette. A terminal set to a different theme than the app therefore stays consistent. When a palette comes from another project, the entry names that project beside the palette.
See Settings & Preferences for fonts, themes, and other appearance options.
Quake-mode terminal
A Quake-mode terminal is a shell that slides over the centre of the app for one working directory. It belongs to that directory rather than to a tab or a tile, so it costs the tab underneath none of its space: press the shortcut, run a command, press it again, and the transcript is exactly where you left it.
Press Ctrl and the key under Esc to show or hide it (command
terminal.toggleQuake). The first press creates the shell; every press after
that only shows or hides the panel.
It works in any tab — agent, terminal, file viewer, image viewer — because every tab has a working directory. Two tabs that work in the same directory share one shell, so the build you started from one agent tab is right there when you press the shortcut from the one beside it.
The panel opens from the keyboard or the Control CLI, and from nowhere else: there is no button or menu item for it. On a phone you can therefore hide a panel that the CLI opened, using the control in its corner, but you cannot open one. A tablet with an external keyboard uses the shortcut like any other device.
Ctrl on every platform, and not Cmd on macOS: macOS reserves Cmd and that
key for “switch between the windows of the app you’re using”, so the shortcut
would never reach LeapMux. The binding follows the key’s POSITION rather than
the character on it, so it is the same key on a US, German, French or Spanish
layout even though that key prints `, ^, ² or º. A Japanese (JIS)
keyboard is the exception: there the same position is the 半角/全角 key, which
the input method takes, so rebind the command in Preferences. There are two more commands, unbound by
default, for a key that only ever opens or only ever closes:
terminal.openQuake and terminal.closeQuake.
The shell is the Worker’s default shell, started in the focused tab’s working directory. It is a full terminal: the same scrollback, copy and paste, and resizing every other LeapMux terminal has.
One thing differs, and it shows up in the Control CLI. A Quake terminal has no
tab, so it is given no LEAPMUX_CONTROL_TAB_ID and no LEAPMUX_CONTROL_TAB_TYPE.
That is deliberate rather than a gap. The panel belongs to a directory, and every tab in that directory can reach it — so no one of them is “the tab you are in”. Picking one anyway would hand every command a target you never chose, and which agent you got would depend on the order the tabs happened to be opened in.
What the panel does get is enough to identify itself:
| Inside the panel | Identifies |
|---|---|
LEAPMUX_CONTROL_TERMINAL_ID | the panel’s own shell |
LEAPMUX_CONTROL_WORKING_DIR | the directory that addresses the panel |
LEAPMUX_CONTROL_WORKER_ID | the host Worker |
So these work with no flags:
leapmux control terminal quake toggle # hides the panel you typed it into
leapmux control terminal quake close
leapmux control terminal get # this shell's own geometry, shell, dir
leapmux control terminal send --data 'ls\n'
leapmux control git status # worker + working dir are enoughAnd anything that acts on a tab asks you which one:
leapmux control agent send --tab-id "$AGENT" --message "done"
leapmux control tab list --workspace-id "$WS"Run without --tab-id, those report a missing id rather than guessing. Use
leapmux control tab list to find the one you mean.
What it belongs to
One shell per (Worker, working directory), shared by every tab in that directory and by every device you are signed in on. Open the panel on a second device and it attaches to the shell the first one started, with the same scrollback. Whether the panel is showing is per-device, exactly as which tab is active in a tile is per-device — so a second screen can keep the panel up while the first one hides it.
Two directories are two shells, even in one workspace: switch to a tab working somewhere else and the panel shows that directory’s terminal, with its own scrollback.
The shell keeps running while the panel is hidden, and it survives a page refresh. It ends in exactly two cases:
- its directory runs out of tabs anyone can reach it from — the last one closes, or every one of them is archived; or
- you end it yourself, with
exitorCtrl+D.
Archiving is the same rule rather than a special case. A shell reached from two workspaces survives one of them being archived, because the other still has a live tab in the directory; it ends once none is left. It is closed rather than archived, because a Quake terminal has no restart contract for an unarchive to restore — the next press starts a fresh shell.
After it ends, the panel retracts and the next press starts a fresh shell. This differs from a terminal tab, which stays on screen after its shell exits and offers Enter to restart.
Settings
Four rows under Terminal in Preferences. Each is an account setting you can override on one device.
| Setting | Default | What it does |
|---|---|---|
| Quake terminal position | Top | Edge the panel slides in from: Top, Bottom, Left, or Right. |
| Quake terminal size | 65% | Share of the centre area the panel covers. Height for top and bottom, width for left and right. |
| Quake terminal animation | 200 ms | How long the slide takes. |
| Quake terminal background opacity | 0.9 | Opacity of the panel background. The terminal text stays fully opaque. |
If your system asks for reduced motion, the panel appears and disappears without animating, whatever the duration says.
Driving the panel from the CLI
leapmux control terminal quake open, close, and toggle do the same thing
the shortcut does, in every browser and desktop window you have open:
leapmux control terminal quake toggle --working-dir <path>--working-dir defaults to $LEAPMUX_CONTROL_WORKING_DIR, which every LeapMux
shell exports — so the bare leapmux control terminal quake toggle works from
an agent’s own terminal, from a terminal tab, and from inside the Quake terminal
itself, where it hides the panel you typed it into.
These commands carry no state. They ask the frontends to act now, which is why they can move a panel although the active tab and the focused tile stay client-local. See Control CLI.
Terminal status indicators
A terminal moves through several states, reflected both in the terminal pane and in the tab label.
| Status | Meaning |
|---|---|
| Starting | The Worker spawns the PTY. |
| Ready | The PTY is up and the view can mount. |
| Startup failed | The shell could not be spawned. |
| Disconnected | The connection to the terminal’s Worker was lost. |
| Exited | The shell process exited. |
How each state appears:
- Starting: a centered spinner with a per-shell label like “Starting zsh…” (falling back to “Starting terminal…”). When you open a terminal with git options, the label may instead describe the git work, for example
Creating worktree "feature/x"…. The spinner stays until the terminal has actually painted visible content, not merely until the PTY spawns. - Startup failed: a full-pane error that states the terminal failed to start, with the Worker’s error message.
- Disconnected: the tab label is faded.
- Exited: the tab label is faded and struck through.
If a program in a background (non-active) terminal rings the terminal bell, that tab gets a notification indicator.
Persistence and reattachment
Terminals are durable. Refresh the page, switch workspaces, or lose and regain your connection, and the live shell keeps running on the Worker — the Frontend simply reattaches. A Worker restart is different: the shell process can’t survive it, but the terminal’s last screen is preserved, so the tab comes back showing where it left off and can be restarted.
This works because the Worker keeps a rolling 100 KB screen buffer for each terminal and also persists the terminal (its working directory, shell, title, dimensions, and last-seen screen) to its database:
- Page refresh / tab re-mount: the Frontend re-fetches the saved screen and resumes streaming from where it left off, so a full-screen TUI redraws correctly rather than showing a blank pane.
- Workspace switch: the on-screen contents (viewport plus scrollback) are captured when you switch away, so switching back restores exactly what was showing.
- Worker restart: the running shell cannot survive the Worker going down, but because the terminal and its last screen are persisted to the database, the terminal is still listed when the Worker returns — showing its final screen — and pressing Enter restarts the shell.
When a shell exits
When the shell process exits, the Worker writes a notice into the screen so you can see it and so it persists. The notice reports that the terminal process exited, gives its exit code, and tells you that Enter restarts the shell.
If the Worker was disconnected or forcibly shut down, the exit code is unknown. The notice then reports the disconnected Worker instead of an exit code, and it offers the same restart.
On an exited terminal, Enter is the only key that does anything — it restarts the shell. All other input is ignored. A restart reuses the terminal’s saved working directory, shell, and start directory, mints fresh remote-control credentials, and preserves the existing screen so the new prompt appears below the exit notice. If a restart can’t proceed, LeapMux reports the failure (for example, the Worker reports the terminal is still running).
Driving LeapMux from inside a terminal (remote control)
When the Worker spawns your shell, it injects a set of LEAPMUX_CONTROL_* environment variables that let any script or program running inside the terminal drive LeapMux through the leapmux control CLI — without needing to log in separately. The CLI detects these variables and routes its calls over a local socket the Worker provides, scoped to the terminal’s own identity.
The variables injected into a terminal are:
| Variable | When set | Meaning |
|---|---|---|
LEAPMUX_CONTROL_SOCK | Always | Local IPC socket the CLI connects to |
LEAPMUX_CONTROL_TOKEN | Always | Per-spawn bearer token for that socket |
LEAPMUX_CONTROL_USER_ID | Always | The authenticated user (informational; no flag defaults from it) |
LEAPMUX_CONTROL_WORKER_ID | Always | The host Worker |
LEAPMUX_CONTROL_TAB_ID | When known | This terminal’s tab id |
LEAPMUX_CONTROL_TAB_TYPE | When known | terminal |
LEAPMUX_CONTROL_TERMINAL_ID | Always | The terminal you are running inside. The same id as TAB_ID here; it differs only in a Quake panel. |
LEAPMUX_CONTROL_WORKING_DIR | When known | The working directory at spawn |
Because these are set, leapmux control commands run inside the terminal default their entity IDs from the environment. For example, this works with no flags from inside the terminal:
# Who am I, and where?
leapmux control whoami
# Open a sibling terminal next to this one
leapmux control tab open --type terminal --lastLEAPMUX_CONTROL_TAB_ID at call time, which keeps them correct even if you move the tab. Terminals also do not get LEAPMUX_CONTROL_AGENT_PROVIDER (that is agents-only).Any pre-existing LEAPMUX_CONTROL_* values are stripped before the Worker re-injects its own, so a terminal opened from inside another agent or terminal targets itself, not its parent. The per-spawn token is retired when the terminal is closed and re-minted on restart.
Controlling a terminal from outside
The reverse also works: from any authenticated leapmux control session (or from another agent), you can write to and inspect a terminal:
# Type a command into a terminal's PTY (note the trailing newline to run it)
leapmux control terminal send --tab-id <tab> --data $'ls -la\n'
# Pipe binary or escape sequences in via stdin
printf '\x03' | leapmux control terminal send --tab-id <tab> --stdin
# Inspect a terminal's metadata, or dump its current screen with ANSI intact
leapmux control terminal get --tab-id <tab>
leapmux control terminal get --tab-id <tab> --screen
# List a worker's available shells (and its default)
leapmux control terminal shells --worker-id <worker>See Control CLI for the complete terminal subcommand reference, authentication, and the JSON output contract.
Renaming a terminal
A terminal’s title updates automatically when a program sets the terminal window title (the standard OSC title escape sequence) — for example, many shells set it to the current directory or running command. You can also rename a terminal tab through its tab menu, or from a script:
leapmux control tab rename --tab-id <tab> --title "Build watcher"Closing a terminal
Closing a terminal tab removes it from your layout immediately and tells the Worker to tear down the PTY and reap the shell’s whole process tree. If the close fails on the Worker side, LeapMux reports the failure, but the tab is already gone from your view.
If the terminal you’re closing is the last tab for a worktree, or the last non-worktree tab on a branch with unsaved work — uncommitted changes, unpushed commits, or a branch that was never pushed to a remote — LeapMux shows the “Close last tab” confirmation so you don’t lose work. From there you can push, close anyway, or — for a worktree — schedule the worktree for removal. This flow is described in full in Worktrees & Branches.
Keyboard shortcuts
The terminal and tab shortcuts (opening, closing, scrollback paging, the Quake-mode toggle, and the macOS line/word navigation keys) are all customizable. See Keyboard Shortcuts for the full keybinding system and how to remap commands.
See also
- Tabs & Layout — tiling, floating, and moving terminal tabs.
- Worktrees & Branches — git options, worktree creation, and the close-last-tab flow.
- Coding Agents — agents share the same tab, Worker, and git-options model.
- Control CLI — the full
leapmux control terminal(includingterminal quake),tab, andagentcommand surface. - Settings & Preferences — terminal theme, fonts, and the four Quake-mode terminal settings.
- Keyboard Shortcuts — remap any of the shortcuts above.