Run /hotkeys inside an gjc session to see the active chords for your current build. The list reflects any remaps loaded from disk and any bindings added by extensions.
User remaps live in ~/.gjc/agent/keybindings.json. The file is a JSON object whose keys are keybinding action IDs and whose values are either one chord string or an array of chord strings. It is not read from ~/.gjc/agent/config.yml, and there is no nested keybindings object.
{
"app.commandPalette.open": "ctrl+p",
"app.model.cycleForward": "alt+n",
"app.model.selectTemporary": "alt+p",
"app.plan.toggle": "alt+shift+p"
}Chord names are case-insensitive. New configuration should use canonical textual IDs rather than matching the labels shown in the UI.
Configuration uses portable canonical key IDs, not the labels printed by a particular host: use ctrl, alt, shift, and super with a key name, for example ctrl+p, alt+enter, shift+tab, and super+c. macOS aliases option/meta normalize to alt, and command/cmd normalize to super; canonical names are recommended for portable files.
Runtime UI labels are platform-native. On macOS, Ctrl, Alt, Shift, and Super display as ⌃, ⌥, ⇧, and ⌘; MacBook keycaps such as Return, Escape, Tab, Delete, and the arrow keys display as ↩, ⎋, ⇥, ⌫/⌦, and arrows. These glyphs are display labels only: configure super+c, not ⌘C, and alt+enter, not ⌥↩.
On macOS, both left and right Option keys use the same terminal Meta/Esc path. Option shortcuts therefore require the terminal profile to forward Option as Meta/Esc or to use an enhanced keyboard protocol. In Apple Terminal, enable Settings > Profiles > Keyboard > Use Option as Meta key for the profile used by GJC; this setting covers both physical Option keys. In Ghostty, set macos-option-as-alt = true in its config file (~/.config/ghostty/config or ~/Library/Application Support/com.mitchellh.ghostty/config.ghostty), then reload its configuration or restart it. In iTerm2, set both Option keys to Esc+ under Settings > Profiles > Keys. The parser also accepts legacy Meta-wrapped arrows, paging, function keys, and other escape sequences.
Apple Terminal reserves most Command shortcuts for its own menus, so those key events never enter the PTY and cannot be recovered by GJC. super+... bindings work when the terminal sends a Super modifier through Kitty/modifyOtherKeys or an explicit profile key mapping; map the desired Command chord under Profiles > Keyboard > Key list when using Terminal.app. Text produced by an Option key as composed Unicode cannot be reverse-inferred as an Option chord, so the character is inserted as typed. When that character is what the US/ABC layout composes for a bound Option chord (for example œ while alt+q is bound), GJC shows a one-time warning naming the terminal setting to change.
For example, to bind Command+P, set the GJC action to super+p (or command+p, which is normalized), then add a Terminal.app profile mapping for Command+P that sends Kitty CSI 112;9u (Send Escape Sequence value [112;9u, or the equivalent hex bytes including the leading ESC). The mapping is required because no PTY application can recover a Command event that Terminal.app consumed.
For terminals that do not forward Option, remap the queue actions to canonical Control chords (choose unclaimed chords appropriate for your terminal), for example:
{
"app.message.queue": "ctrl+q",
"app.message.dequeue": ["ctrl+pageup", "ctrl+pagedown"]
}Static onboarding and generated reference material describe shipped defaults and must stay host-independent. The active runtime surface is authoritative for effective bindings after user remaps and extensions load: use /hotkeys to see those bindings on the current platform.
Set an action to an empty array to disable it:
{
"app.stt.toggle": []
}| Action ID | Default | Meaning |
|---|---|---|
app.commandPalette.open |
ctrl+p |
Open the command palette |
app.model.cycleForward |
alt+n |
Cycle role models forward |
app.model.cycleBackward |
alt+shift+n |
Cycle role models backward |
app.model.selectTemporary |
alt+p |
Pick a model temporarily for this session |
app.model.select |
ctrl+l |
Open the model selector and set roles |
app.plan.toggle |
alt+shift+p |
Toggle plan mode |
app.history.search |
ctrl+r |
Search prompt history |
app.tools.expand |
ctrl+o |
Toggle tool-output expansion |
app.thinking.toggle |
ctrl+t |
Toggle thinking-block visibility |
app.thinking.cycle |
shift+tab |
Cycle thinking level |
app.editor.external |
ctrl+g |
Edit the draft in $VISUAL / $EDITOR |
app.message.followUp |
(none) | Optional remap for a follow-up message; ctrl+enter is reserved for editor newline |
app.message.queue |
alt+enter (alt+q on darwin/win32) |
Explicitly queue a message for the next turn |
app.message.dequeue |
alt+up, alt+down |
Open the queue and select a queued message to edit |
| app.clipboard.copyLine | alt+shift+l | Copy the current line |
| app.clipboard.pasteText | (none) | Paste text from configured clipboard transport (clipboard.transport: ssh); command palette only |
| app.clipboard.copyPrompt | alt+shift+c | Copy the whole prompt |
| app.stt.toggle | alt+h | Toggle speech-to-text recording |
| app.irc.sidebar.toggle | alt+i | Toggle IRC sidebar |
For setup, microphone permissions, first-use behavior, and troubleshooting, see Speech-to-text.
Older unqualified action names are migrated when keybindings.json is loaded, but new docs and new configs should use the namespaced action IDs above.
On macOS, Option+Q queues a message for the next turn when the active terminal profile forwards Option as Meta/Esc; in Apple Terminal, this is controlled by Use Option as Meta key and applies to both left and right Option keys. On native Windows terminals, the equivalent default is Alt+Q. Windows Terminal and PowerShell commonly reserve Alt+Enter for fullscreen before GJC can receive it. Users who prefer another chord can remap app.message.queue in ~/.gjc/agent/keybindings.json.
When messages are queued, use Option+Up/Down on macOS (Alt+Up/Down on Windows) to open the queue and select a message. In the queue, Return edits the selected message, Forward Delete (⌦; Fn+Delete on compact Mac keyboards) removes it, Control+Up/Down reorders it within its delivery group, and Escape closes the queue. Reordering does not convert compaction, steer, and follow-up messages into one another.
In the main GJC composer, plain PageUp / PageDown page the visible transcript lane instead of browsing prompt history; the status line and composer remain fixed at the bottom while manually scrolled. When GJC owns mouse input (mouse.enabled: true), the wheel moves the transcript by three rows per notch. Ordinary typing or paste keeps editor focus and returns to live output before editing; use Up / Down or Ctrl+R for prompt history. Autocomplete and selector surfaces still use PageUp / PageDown for list paging while they have focus.
Some default chords are intentionally reused across different UI contexts, where the focused component disambiguates them at dispatch time. For example Enter maps to both input submit and selection confirm, and Ctrl+C maps to both input copy and selection cancel. These are not conflicts — only one context is active at a time.
To audit the registry for keys whose default binding is claimed by more than one action, use detectDefaultKeyCollisions(definitions) from @gajae-code/tui/keybindings. It returns one entry per colliding key with the list of claiming action IDs, which is useful when adding new defaults or reviewing the surface. User-remap conflicts (multiple actions bound to the same chord in keybindings.json) continue to be reported separately by KeybindingsManager.getConflicts().
Two audit clarifications for the current surface:
app.clipboard.copyLineis registry-backed and dispatched through the input controller's custom key handlers, not hardcoded.tui.input.copyis declared in the registry but is not currently dispatched byEditor.handleInput.
The editor's configurable action defaults (including the platform-aware app.clipboard.pasteImage default) are derived directly from the central KEYBINDINGS registry, so there is a single source of truth for those defaults.
Authoritative inventory of the keybinding registry, one row per action. Generated from TUI_KEYBINDINGS (packages/tui/src/keybindings.ts) and KEYBINDINGS (packages/coding-agent/src/config/keybindings.ts). Every action ID below is remappable via ~/.gjc/agent/keybindings.json unless noted. A drift test (packages/coding-agent/test/keybindings-audit.test.ts) asserts every registry action ID appears in this table.
| Action ID | Default | Notes |
|---|---|---|
tui.editor.cursorUp |
up |
|
tui.editor.cursorDown |
down |
|
tui.editor.cursorLeft |
left, ctrl+b |
|
tui.editor.cursorRight |
right, ctrl+f |
|
tui.editor.cursorWordLeft |
alt+left, ctrl+left, alt+b |
ctrl+left also app.tree.foldOrUp |
tui.editor.cursorWordRight |
alt+right, ctrl+right, alt+f |
ctrl+right also app.tree.unfoldOrDown |
tui.editor.cursorLineStart |
home, ctrl+a |
|
tui.editor.cursorLineEnd |
end, ctrl+e |
|
tui.editor.jumpForward |
ctrl+] |
|
tui.editor.jumpBackward |
ctrl+alt+] |
|
tui.editor.pageUp |
pageUp |
|
tui.editor.pageDown |
pageDown |
|
tui.editor.deleteCharBackward |
backspace |
|
tui.editor.deleteCharForward |
delete, ctrl+d |
ctrl+d also app.exit / app.session.delete |
tui.editor.deleteWordBackward |
ctrl+w, alt+backspace, ctrl+backspace |
|
tui.editor.deleteWordForward |
alt+delete, alt+d |
|
tui.editor.deleteToLineStart |
ctrl+u |
|
tui.editor.deleteToLineEnd |
ctrl+k |
|
tui.editor.yank |
ctrl+y |
|
tui.editor.yankPop |
alt+y |
|
tui.editor.undo |
ctrl+-, ctrl+_ |
| Action ID | Default | Notes |
|---|---|---|
tui.input.newLine |
Shift+Enter |
Ctrl+Enter and Ctrl+Shift+Enter are also accepted by the editor when the terminal encodes them distinctly |
| tui.input.submit | enter | also tui.select.confirm (other context) |
| tui.input.tab | tab | |
| tui.input.copy | ctrl+c | declared but not dispatched by Editor.handleInput |
| Action ID | Default | Notes |
|---|---|---|
tui.select.up |
up |
|
tui.select.down |
down |
|
tui.select.pageUp |
pageUp |
|
tui.select.pageDown |
pageDown |
|
tui.select.confirm |
enter |
|
tui.select.cancel |
escape, ctrl+c |
escape also app.interrupt |
| Action ID | Default | Domains |
|---|---|---|
app.interrupt |
escape | global |
app.clear |
ctrl+c | global |
app.exit |
ctrl+d | global |
app.suspend |
ctrl+z | global |
app.thinking.cycle |
shift+tab | composer |
app.thinking.toggle |
ctrl+t | composer |
app.commandPalette.open |
ctrl+p | composer |
app.model.cycleForward |
alt+n | composer |
app.model.cycleBackward |
alt+shift+n | composer |
app.model.select |
ctrl+l | composer |
app.model.selectTemporary |
alt+p | composer |
app.tools.expand |
ctrl+o | composer |
app.tool.backgroundFold |
alt+shift+b, super+b (darwin) / alt+shift+b (win32/linux) | composer |
app.editor.external |
ctrl+g | composer |
app.message.followUp |
(none) | composer |
app.message.queue |
alt+q (darwin/win32) / alt+enter (linux) | composer |
app.message.dequeue |
alt+up, alt+down | composer |
app.clipboard.pasteImage |
ctrl+v, super+v (darwin) / ctrl+v, alt+v (win32) / ctrl+v (linux) | composer |
app.clipboard.pasteText |
(none) | composer |
app.clipboard.copyLine |
alt+shift+l | composer |
app.clipboard.copyPrompt |
alt+shift+c | composer |
app.clipboard.copyOAuthUrl |
alt+shift+u | composer |
app.session.new |
ctrl+n | composer |
app.session.tree |
alt+shift+s | composer |
app.session.fork |
alt+shift+f | composer |
app.session.resume |
alt+shift+r | composer |
app.session.observe |
ctrl+s | composer |
app.session.dashboard |
(none) | composer |
app.jobs.open |
alt+j | composer |
app.session.togglePath |
ctrl+p | selector |
app.session.toggleSort |
ctrl+s | selector |
app.session.rename |
ctrl+r | selector |
app.session.delete |
ctrl+d | selector |
app.session.deleteNoninvasive |
ctrl+backspace | selector |
app.tree.foldOrUp |
ctrl+left, alt+left | selector |
app.tree.unfoldOrDown |
ctrl+right, alt+right | selector |
app.plan.toggle |
alt+shift+p | composer |
app.history.search |
ctrl+r | composer |
app.stt.toggle |
alt+h | composer |
app.irc.sidebar.toggle |
alt+i | composer |
app.transcript.browse |
(none) | composer |
app.transcript.prevTurn |
(none) | composer |
app.transcript.nextTurn |
(none) | composer |
app.mode.cycle |
(none) | composer |
app.tasks.toggle |
alt+t | composer |
app.todo.toggle |
alt+shift+t | composer |
app.queue.togglePane |
(none) | composer |
app.message.sendNow |
(none) | composer |
| Action ID | Default | Notes |
|---|---|---|
tui.global.debug |
shift+ctrl+d |
Toggle debug overlay; resolved through the registry in tui.ts |
Cross-context default reuse (ctrl+s, ctrl+r, ctrl+d, ctrl+left/ctrl+right, enter, escape, ctrl+c) is intentional: each pair is active in a different focused context and is disambiguated at dispatch time. Use detectDefaultKeyCollisions() (above) to re-derive this list from the registry.
A focused menu owns app.interrupt (Escape by default) before background work: Escape closes the menu or returns from its nested page without cancelling manual/automatic/overflow/idle compaction, handoff, retry backoff, or MCP/Smithery browser authorization. This includes composer-replacement selectors, their focused inner lists, and overlays. The focused component uses its own cancel binding; remapping app.interrupt does not remap every menu's tui.select.cancel binding. Once focus returns to the composer, interrupt resumes its normal work-cancellation behavior. A focused menu also takes precedence over an open /btw panel.
app.clear (Ctrl+C by default) remains a global work-abort action while a menu is open, and wins when a key is configured for both clear and interrupt; it is not an alternative way to dismiss a menu safely during active work. Hook workflow prompts deliberately retain workflow-level interrupt behavior outside inline custom input. Inline hook input handles Escape locally to return to its option list.
A few contexts still match chords directly instead of resolving through the registry, and are tracked for a later phase:
- Tree selector (
tree-selector.ts): up/down/left/right/enter,ctrl+c, filter cycling (ctrl+o/ctrl+shift+o), filter modes (alt+d/t/u/l/a), label edit (shift+l). - Parts of the model selector.
- Composer editor (
editor.ts): the kill-ring, word-delete and line-kill actions (tui.editor.deleteToLineEnd,tui.editor.deleteToLineStart,tui.editor.deleteWordBackward,tui.editor.deleteWordForward,tui.editor.yank,tui.editor.yankPop) now resolve through the registry and are fully remappable. Two exceptions remain:tui.editor.cursorLineStartandtui.editor.cursorLineEndare matched by literalctrl+a/ctrl+ebranches placed ahead of their registry branches, so remapping those two actions elsewhere does not freectrl+a/ctrl+einside the composer. Thealt+enter/ctrl+j/ctrl+entersubmit and newline chords are likewise still literal.