Keyboard Shortcuts
LeapMux ships with a VS Code-style keyboard shortcut system: every shortcut is a command bound to a key, and each binding is active only inside a context (for example, only when a dialog is open or a terminal is focused). This chapter explains how the system works, lists every default binding, and documents the override format the engine uses for customization.
How shortcuts work
A keyboard shortcut in LeapMux is the combination of three independent pieces:
- Command — a named action with a human title, such as
app.newAgent(“New Agent”) orapp.closeActiveTab(“Close Active Tab”). The command holds the logic; it does not know which key triggers it. - Keybinding — a key string mapped to a command, optionally gated by a
when-clause. For example,$mod+nrunsapp.newAgentwhen!dialogOpen(no dialog is open). - Context — a set of named boolean/string values evaluated the moment you press a key (for example
dialogOpen,terminalFocused, orplatform == "mac"). A binding fires only if itswhen-clause evaluates to true against the current context.
Because these are decoupled, the same command can be bound to several keys, and the same key can run different commands depending on what is focused.
The $mod key
Key strings use the tinykeys vocabulary. The most important token is $mod:
- On macOS,
$modmeans Cmd (⌘). - On Windows and Linux,
$modmeans Ctrl.
So a single binding such as $mod+n is shown as ⌘N on a Mac and Ctrl+N everywhere else. The tables below give both forms.
Other modifiers are Shift, Alt (also written Option), Control/Ctrl, and Meta. Named keys used in the defaults include F5, F12, the arrow keys, PageUp/PageDown, the bracket and backslash keys, and the digits 0–9.
Chords
The engine supports chords — space-separated key sequences such as $mod+k $mod+s (press the first combination, then the second). Chords render correctly in tooltips and menus, but no default binding uses one. The override format supports chords (see Customizing keybindings), with one caveat on the desktop app (see Desktop (Tauri) accelerator differences).
When shortcuts are suppressed
LeapMux binds shortcuts at the window level so they work no matter where focus sits, but it suppresses some of them to avoid hijacking your typing:
- Input focus. When the cursor is in a text input, textarea, select, or any
contenteditableelement, a shortcut with no modifier that is not a function key is suppressed — unless itswhen-clause explicitly refers toinputFocused. The suppression does not apply to shortcuts that use a modifier ($mod,Ctrl,Alt,Meta,Shift) or a plain function key (F1–F12): those always fire, regardless of focus. - IME composition. While you are composing text with an Input Method Editor (for example, typing Chinese, Japanese, or Korean), shortcuts are held back so they cannot interfere with your text entry.
Contexts (the when clause)
A binding’s when-clause is a small boolean expression evaluated against the active context. The available context values are:
| Context | Type | Meaning |
|---|---|---|
isDesktop | boolean | Running inside the Tauri desktop app. |
platform | string | "mac", "windows", or "linux". |
dialogOpen | boolean | A modal dialog is currently open. |
sidebarVisible | boolean | The left sidebar is expanded. |
activeTabType | string | "agent", "terminal", "file", or empty when no tab is active. |
inputFocused | boolean | Focus is in an input, textarea, select, or contenteditable element. |
editorFocused | boolean | Focus is inside a rich-text editor surface. |
chatInputFocused | boolean | Focus is inside a chat message input. |
terminalFocused | boolean | Focus is inside a terminal. |
The when expression grammar supports || (or), && (and), ! (not), parentheses, ==/!= comparisons, identifiers, quoted strings, and the literals true/false. An empty or absent when means “always active.” Examples drawn from the defaults: !dialogOpen, isDesktop, !dialogOpen && isDesktop, chatInputFocused, and terminalFocused && platform == "mac".
Default keyboard shortcuts
The tables below list every default binding. The macOS column uses Apple’s symbol glyphs in HIG order (⌃⌥⇧⌘, run together with no separators); the Windows / Linux column uses Ctrl/Alt/Shift joined with +. Where a command has more than one binding, both are shown separated by / — exactly as LeapMux renders them in tooltips and menus.
App
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| Open Preferences | ⌘, | Ctrl+, | always |
Open Preferences opens the Preferences dialog. It is a workspace binding (it works everywhere inside a workspace), not desktop-only. On the macOS desktop app it is also mirrored onto the native menu bar — see Desktop (Tauri) accelerator differences. For the dialog’s contents, see Settings & Preferences.
Tabs
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| New Agent | ⌘N | Ctrl+N | no dialog open |
| New Terminal | ⌘T | Ctrl+T | no dialog open |
| Close Active Tab | ⌘W | Ctrl+W | no dialog open |
| New Agent Dialog | ⇧⌘N | Ctrl+Shift+N | no dialog open |
| New Terminal Dialog | ⇧⌘T | Ctrl+Shift+T | no dialog open |
| Toggle Floating Tab | ⇧⌘O | Ctrl+Shift+O | no dialog open |
| Switch to Tab 1–9 | ⌘1–⌘9 / ⌥1–⌥9 | Ctrl+1–Ctrl+9 / Alt+1–Alt+9 | always |
| Previous Tab | ⌘[ / ⌘PageUp | Ctrl+[ / Ctrl+PageUp | always |
| Next Tab | ⌘] / ⌘PageDown | Ctrl+] / Ctrl+PageDown | always |
Tab switching and navigation operate on the visible tabs of the focused tile (or on all tabs when no tile is focused). Previous/Next wrap around and do nothing if a tile has fewer than two tabs. See Tabs & Layout.
Layout
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| Split Tile Horizontally | ⌘\ | Ctrl+\ | always |
| Split Tile Vertically | ⇧⌘\ | Ctrl+Shift+\ | always |
| Toggle Left Sidebar | ⇧⌘[ | Ctrl+Shift+[ | always |
| Toggle Right Sidebar | ⇧⌘] | Ctrl+Shift+] | always |
Splitting acts on the focused tile. For more on tiling and sidebars, see Tabs & Layout.
View
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| Scroll Active Tab Up One Page | ⌥PageUp | Alt+PageUp | no dialog open |
| Scroll Active Tab Down One Page | ⌥PageDown | Alt+PageDown | no dialog open |
Page scrolling only acts when the active tab is an agent or a terminal.
Files
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| Refresh Directory Tree | ⌘R / F5 | Ctrl+R / F5 | always |
| Toggle Hidden Files | ⇧⌘H | Ctrl+Shift+H | always |
These act on the file browser. See File Browser.
Chat
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| Send Message | ⌘J | Ctrl+J | chat input focused |
Send Message (⌘J / Ctrl+J) is a global way to submit the focused chat input, independent of the chat editor’s own Enter behavior. The Enter-to-send vs. Cmd/Ctrl+Enter-to-send choice is a separate, editor-level setting described in Chat editor keys below.Terminal (macOS only)
These give Mac users emacs-style line and word motion under Cmd/Option + arrow keys. They are active only when a terminal is focused and the platform is macOS.
| Command | macOS | Active when |
|---|---|---|
| Go to Line Start | ⌘← | terminal focused, macOS |
| Go to Line End | ⌘→ | terminal focused, macOS |
| Go to Previous Word | ⌥← | terminal focused, macOS |
| Go to Next Word | ⌥→ | terminal focused, macOS |
Under the hood these send terminal control sequences (Ctrl-A, Ctrl-E, Esc-b, Esc-f) to the focused terminal. See Terminals.
Dialogs
Esc closes the most recently opened dialog — unless that dialog is busy (for example, mid-operation), in which case it resists closing until the operation finishes.
Esc is not a keybinding, and no command performs this action. The browser delivers the key to the innermost open layer, so one press dismisses one layer: with a dropdown menu open inside a dialog, the first Esc closes the menu and the second closes the dialog. A control that answers Esc itself comes first in the same way — the Preferences search box clears the query, and an inline rename cancels the edit.
Esc is reserved: an override that binds it is ignored, and the log records the entry that was dropped. Every keybinding fires from a single listener that runs ahead of the layers, so a binding for Esc would close a dialog underneath an open menu — which is the behaviour the reservation exists to prevent.
New Workspace
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| New Workspace Dialog | ⌥⌘N | Ctrl+Alt+N | no dialog open |
See Workspaces.
Desktop app only
These are gated to the desktop app via isDesktop and have no effect in the browser.
| Command | macOS | Windows / Linux | Active when |
|---|---|---|---|
| Open in External Editor | ⇧⌘E | Ctrl+Shift+E | no dialog open, desktop |
| Open Web Inspector | ⌥⌘I / F12 | Ctrl+Alt+I / F12 | desktop |
| Zoom In | ⌘= / ⌘Num+ | Ctrl+= / Ctrl+Num+ | desktop |
| Zoom Out | ⌘- / ⌘Num- | Ctrl+- / Ctrl+Num- | desktop |
| Actual Size | ⌘0 / ⌘Num0 | Ctrl+0 / Ctrl+Num0 | desktop |
| Quit Application | ⌘Q | Ctrl+Q | desktop |
The zoom, web-inspector, and quit shortcuts (“core” bindings) are mounted at the application root, so they work on every screen — including the launcher and the sign-in/setup pages — not just inside a workspace.
Open in external editor (desktop, solo mode)
The ⇧⌘E / Ctrl+Shift+E shortcut launches an external code editor against the active tab’s working directory. It is one face of a feature that also lives in the desktop workspace title bar as a split button.
Where it appears. The split button shows in the title bar only when all three conditions hold: you are on the desktop app in solo mode, the active tab has a working directory, and at least one editor was auto-detected on your machine. It is hidden in the browser, in distributed mode, and when no editor is found.
The two faces of the split button.
- The main face reads “Open in {EditorName}” (with the editor’s icon) when you have a preferred editor. It reads “Open in …” with a generic icon when you have not picked one. With a preferred editor, clicking the main face — or pressing
⇧⌘E/Ctrl+Shift+E— launches it. Without one, clicking the main face opens the dropdown; pressing the shortcut launches the first detected editor and records it as your preferred editor. - The chevron opens a dropdown that lists every detected editor alphabetically (with a checkmark on the current preferred one), followed by a separator and “Refresh editor list”.
What gets opened. LeapMux launches the editor against the active tab’s working directory. The directory must be absolute and must exist. LeapMux rejects relative paths, missing paths, and plain files, so nothing launches when the active tab has no real directory.
Detected editors. LeapMux auto-detects popular editors including VS Code, the JetBrains IDEs, Cursor, Zed, Sublime Text, and others, probing your PATH, known install locations, and (on macOS) .app bundles. The chevron dropdown shows exactly which editors were found on your machine.
Reading the macOS glyphs
| Glyph | Modifier |
|---|---|
| ⌘ | Cmd ($mod on macOS) |
| ⌃ | Control |
| ⌥ | Option / Alt |
| ⇧ | Shift |
A few key names also render specially: Escape shows as Esc, arrows as ← → ↑ ↓, NumpadAdd as Num+, NumpadSubtract as Num-, and Numpad0 as Num0. On macOS, modifiers always appear in the order ⌃⌥⇧⌘ with no separators, so Cmd renders last (for example, ⇧⌘N). On Windows and Linux they are joined with + (for example, Ctrl+Shift+N).
Customizing keybindings
Customize bindings in the Preferences → Keyboard Shortcuts category: a table of every command with its binding and source (Default or Custom). Click a binding to capture a new chord; a chord already bound in the same context is refused with the conflicting command’s name; Reset restores the default.
Overrides are stored account-level (the keybindings user setting, capped at 200 entries), so they follow you to every device — see Settings & Preferences. The override format below is what the editor writes.
LeapMux’s shortcut engine is built to rebind, add, or remove shortcuts through account-level overrides stored as JSON. The rest of this section describes that format and how the engine merges it with the defaults.
Where overrides live
Overrides live in your account settings on the Hub (the keybindings key), not in browser storage, so they follow you across browsers and devices when you sign in to the same account. The value is a JSON-encoded array of override objects:
[
{ "command": "app.newAgent", "key": "$mod+Shift+a" },
{ "command": "app.closeActiveTab", "key": "" },
{ "command": "app.toggleHiddenFiles", "key": "$mod+k $mod+h" }
]Each override has the shape:
| Field | Required | Meaning |
|---|---|---|
command | yes | The command id to bind (for example app.newAgent). |
key | yes | A tinykeys key string, or "" (empty) to unbind. |
when | no | A when-clause; inherits the default’s clause if omitted. |
How overrides merge with the defaults
For each overridden command, LeapMux merges the overrides with the defaults using these rules:
- The default binding(s) for that command are replaced by all non-empty-key overrides for it.
- Each override inherits the default’s
when-clause unless it specifies its ownwhen. - A command may have multiple overrides — it gets bound to every non-empty key listed.
- An override with
"key": ""(empty) unbinds the command. If every override for a command has an empty key, the command is fully unbound. - An override for a command not in the defaults is appended as a brand-new binding (with no inherited
when-clause). - An override whose key is
Escapeis dropped, and the command keeps whatever the defaults give it.Escis reserved for the innermost open layer — see Dialogs. A chord that merely contains it, such asShift+Escape, binds normally.
Changes take effect immediately: when the overrides change, LeapMux re-merges and re-binds the workspace shortcuts without a reload.
Each override targets a command by its id. Each id corresponds to one of the actions in the default tables above; the bindable workspace command ids are:
| Command id | Action |
|---|---|
app.newAgent | New Agent |
app.newTerminal | New Terminal |
app.closeActiveTab | Close Active Tab |
app.newAgentDialog | New Agent Dialog |
app.newTerminalDialog | New Terminal Dialog |
app.newWorkspaceDialog | New Workspace Dialog |
app.toggleFloatingTab | Toggle Floating Tab |
app.refreshDirectoryTree | Refresh Directory Tree |
app.toggleHiddenFiles | Toggle Hidden Files |
app.switchToTab1 … app.switchToTab9 | Switch to Tab 1–9 |
app.previousTab / app.nextTab | Previous / Next Tab |
app.scrollActiveTabPageUp / app.scrollActiveTabPageDown | Scroll Active Tab Up / Down One Page |
app.splitTileHorizontal / app.splitTileVertical | Split Tile Horizontally / Vertically |
app.toggleLeftSidebar / app.toggleRightSidebar | Toggle Left / Right Sidebar |
app.openPreferences | Open Preferences |
app.openInExternalEditor | Open in External Editor |
chat.sendMessage | Send Message |
terminal.lineStart / terminal.lineEnd | Go to Line Start / End |
terminal.wordLeft / terminal.wordRight | Go to Previous / Next Word |
keybindings setting ever holds a value the client cannot read, the engine treats it as “no overrides” and silently ignores it, falling back to the defaults — so a malformed value never breaks the default shortcuts.Desktop (Tauri) accelerator differences
On the desktop app, a small number of shortcuts are mirrored onto the native menu bar as accelerators, but only on macOS. Windows and Linux render their own controls through the app’s custom title bar instead.
Two commands get their accelerators synced onto the macOS menu:
- Open Preferences (
⌘,) appears as Preferences… in the LeapMux Desktop application menu (the leftmost app menu on macOS). - Open Web Inspector appears as Open Web Inspector in the Help submenu.
If the Open Preferences binding is overridden, the macOS menu accelerator updates to match.
$mod+k $mod+s) still works as a regular shortcut, but it will not appear as an accelerator on the native menu item.The macOS menu also carries the usual system items in each submenu (About, Services, Hide, Quit, the standard Edit and Window commands, and so on). These come from macOS, not from the LeapMux shortcut registry, so their accelerators are the OS defaults you already know and are not affected by keybinding overrides.
Chat editor keys (not part of the global system)
The chat message editor has its own in-editor key handling that is separate from the global shortcut registry described above. The most important of these is how Enter behaves, governed by the Enter key mode preference (a per-browser setting; the default is Cmd/Ctrl+Enter sends):
- Cmd/Ctrl+Enter sends (default):
⌘Enter/Ctrl+Entersends the message; a plainEnterinserts a newline. - Enter sends: a plain
Entersends the message;Shift+Enterinserts a newline.
You can flip between the two from the composer’s [+] menu. Other editor keys handle Markdown structure inside the message box — for example, Tab/Shift+Tab adjust list and heading levels, Backspace lifts blockquotes and code blocks, and Cmd/Ctrl+E toggles inline code. These editor keys are not bindable through keybinding overrides.
For more on writing and sending messages to agents, see Coding Agents.
How matching works under the hood
You do not need any of this to use the shortcuts, but it explains a few edge cases:
- Keys are matched by physical position. Single-letter keys are matched by the key’s physical position on the keyboard (the browser’s
event.code), not by the character it produces. This is deliberate: on macOS WebKit, holding Option transforms the character (for example,Cmd+Alt+Nwould otherwise produce˜). Position-based matching keeps shortcuts working across keyboard layouts and modifier combinations. - First match wins. When a key matches more than one binding, the first binding in registration order whose
when-clause is true runs; LeapMux then prevents the browser default and runs the command.
See also
- Tabs & Layout — the tabs, tiles, and sidebars these shortcuts drive.
- Terminals — terminal behavior and the macOS cursor-motion shortcuts.
- File Browser — the directory tree refreshed and filtered by the Files shortcuts.
- Settings & Preferences — the Preferences dialog and where account vs. browser settings live.
- Running LeapMux — solo vs. distributed mode, which affects the “Open in External Editor” shortcut.