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. See Shell selection. |
| “Working Directory” | Browses the Worker’s filesystem from ~. Type a path and press Enter (or click away) to go there. |
| “Git options” | Appears in the right column 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 (it reads “Creating…” while in flight); cancel with “Cancel”. The Create button stays disabled until you have a Worker, a non-blank working directory, a selected shell, 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.
Terminal status indicators
A terminal moves through several states, reflected both in the terminal pane and in the tab label.
| Status | Meaning |
|---|---|
| Starting | The PTY is being spawned. |
| Ready | The PTY has spawned 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_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, 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 terminalandtabcommand surface. - Settings & Preferences — terminal theme and fonts.
- Keyboard Shortcuts — remap any of the shortcuts above.