Every default binding the TUI ships with, in one table per surface. Each
remappable action is listed under the exact tui.yaml key that rebinds it,
so this page doubles as the reference for a hand-written keymap. Source of
truth: docs/user/tui-reference.md in the sonny repository.
Remappable actions
Every action below can be rebound in tui.yaml; these are the defaults that
ship unconfigured.
Action (tui.yaml key) | Default keys | Effect |
|---|---|---|
palette.open | ctrl+p | Open the command palette |
help.open | f1 | Open help |
tab.new | ctrl+shift+t | New chat tab (opens the recipe picker); soft cap of 9 tabs; only on terminals with keyboard enhancements |
tab.chord_leader | ctrl+t | Tab-switch leader chord — press, then n/p/t/1-9 |
tab.chord_next | n | After the tab-chord leader: next tab |
tab.chord_prev | p | After the tab-chord leader: previous tab |
tab.chord_new | t | After the tab-chord leader: new chat tab (recipe picker) — works on every terminal |
session.manager | ctrl+s | Open the session manager |
history.search | ctrl+r | Filter command history and load a selection into the composer without sending it |
session.picker | ctrl+b | Session picker (local tabs + attached remotes) |
search.quick | ctrl+; | Quick-search sessions/history/mesh |
session.jump | alt+s | One list of every destination — open tabs, recent sessions, sessions on mesh nodes — Enter goes straight there |
session.resume | alt+r | Choose a session from the current directory to resume |
mesh.attach_recent | alt+m | Re-attach the last remote mesh session, no dialog |
search.files | ctrl+/ | Find file |
agent.picker | ctrl+g, ctrl+shift+a | Sub-agent picker (also /sub-agents) |
mesh.actions | ctrl+n | Mesh actions (node picker) |
tab.close | ctrl+w | Close the current tab (quits if last) — deletes the previous word instead while the draft is non-empty |
tab.next | ctrl+tab, alt+n | Cycle to the next chat tab |
tab.prev | ctrl+shift+tab, alt+p | Cycle to the previous chat tab |
tab.move_left | alt+shift+left | Move the current tab left |
tab.move_right | alt+shift+right | Move the current tab right |
tab.last | alt+l | Jump to the last-active tab |
remote.terminate | ctrl+k | Terminate the attached remote session — kills to end of line instead when the draft has text after the caret |
alert.dismiss | ctrl+x | Dismiss the failure panel above the input |
queue.clear | ctrl+q | Clear the prompt queue, after a y / n confirmation; a no-op with an empty queue |
queue.focus | ctrl+e | Toggle queue focus — moves to end of line instead when the draft has text after the caret |
clipboard.paste | ctrl+v | Paste (an image becomes an @path mention; otherwise plain text) |
clipboard.copy_reply | ctrl+y | Copy the last assistant reply |
sidebar.toggle_sections | alt+c | Collapse or expand every sidebar section (same as /sidebar toggle all) |
chat.expand | alt+z | Toggle full-width chat: hide the sidebar (same as /expand) |
transcript.verbose | ctrl+o | Toggle verbose transcript (same as /verbose) |
transcript.nav | ctrl+up | Leave the composer for transcript navigation mode; inside navigation it also steps to the previous message |
transcript.find | ctrl+f | Find in the session transcript — enters navigation mode if needed, or refocuses an open find bar |
session.plan_mode | shift+tab | Toggle the session’s requested mode between plan and execute (same as /plan); bound in the composer only. Rebinding it onto another action’s chord is refused |
tab.jump_1 … tab.jump_9 | ctrl+1…ctrl+9, alt+1…alt+9 | Jump directly to tab N |
Transcript navigation
Press ctrl+up to review a long reply without scrolling by hand: it
leaves the composer and puts the transcript into navigation mode, selecting
the most recent message and scrolling it fully into view. y/c then copy
the whole message, or just its code blocks, in one keystroke. Navigation
mode is per-tab and never saved; switching tabs or /clear exits it.
Streaming output never moves your selection.
These actions act only while navigation mode has focus; outside it the same keys type into the composer.
Action (tui.yaml key) | Default keys | Effect |
|---|---|---|
transcript.nav_prev | up, k, shift+tab | Select the previous message |
transcript.nav_next | down, j, tab | Select the next message |
transcript.nav_first | g, home | Select the first message |
transcript.nav_last | shift+g (G), end | Select the last message |
transcript.nav_page_up | pgup | Scroll up a page and select a visible message |
transcript.nav_page_down | pgdn | Scroll down a page and select a visible message |
transcript.nav_half_page_up | ctrl+u | Scroll up half a page and select a visible message |
transcript.nav_half_page_down | ctrl+d | Scroll down half a page and select a visible message |
transcript.copy | y | Copy the selected message |
transcript.copy_code | c | Copy the code blocks of the selected message |
transcript.nav_find | / | Find in the transcript (opens the find bar) |
transcript.find_next | n | Jump to the next find match |
transcript.find_prev | shift+n (N) | Jump to the previous find match |
transcript.nav_exit | ctrl+down, esc, q, i | Leave navigation and return to the chat composer |
Find bar
ctrl+f (or / once navigating) opens the find bar. It searches the
rendered transcript incrementally, is smartcase, caps the query at 256
characters, and keeps the last query when closed.
Action (tui.yaml key) | Default keys | Effect |
|---|---|---|
transcript.find_done | esc | Bar focused → unfocus it; open and unfocused → close it |
transcript.find_bar_next | enter, down | Jump to the next match — bar focused |
transcript.find_bar_prev | shift+enter, up | Jump to the previous match — bar focused |
esc is a three-step ladder: unfocus the bar, then close it, then leave
navigation. ctrl+down skips the ladder and always returns to the composer
in one press.
Fixed keys
ctrl+c and esc outside suggestions have fixed behavior and cannot be
rebound.
ctrl+c— clears the draft and cancels a local response on the first press; exits on a second consecutive press. Another key or paste resets the sequence. In transcript navigation the first press only returns to the composer and keeps the draft.esc— context-dependent, checked in order: returns focus from the queue pane to the composer; else on a remote-attached tab opens “Detach from remote?”; else goes to an open dialog; else cancels an in-flight turn (and only that); else sends the queued head when nothing is in flight; else opens the quit confirmation. The composer placeholder names whichever one the nextescwill do.
Input and navigation
Fixed, not remappable:
| Key | Effect |
|---|---|
enter | Send the message; confirms the focused dialog option |
shift+enter | Insert a newline (multi-line draft) |
pgup / pgdn | Scroll the transcript up/down by a page |
ctrl+down | Scroll the transcript down by a line (ctrl+up enters navigation) |
home / end | Line start / end in the composer; with an empty draft, transcript top / bottom (end resumes auto-follow) |
ctrl+home / ctrl+end | Start / end of the whole draft |
up / down | Recall input history (or scroll the transcript; navigate dialog lists) |
alt+up / alt+down | Reorder the selected queued prompt (while the queue has focus) |
Composer editing keys
The composer is a readline-style editor: ctrl+a/ctrl+e line start/end,
ctrl+u/ctrl+k delete before/after the caret, ctrl+w delete the previous
word, alt+b/alt+f (or ctrl+left/ctrl+right) move one word. ctrl+f
no longer moves the caret forward — it’s transcript.find now (SONNY-2560);
use → for that instead.
Four keys are shared with an application action, and the composer wins only when the key would really edit or move within the draft:
| Key | Composer | Falls back to |
|---|---|---|
home / end | Line start / end (any draft) | Transcript top / bottom, when the draft is empty |
ctrl+e | End of line, when there is text after the caret | queue.focus |
ctrl+w | Delete the previous word, whenever the draft is non-empty | tab.close |
ctrl+k | Kill to end of line, when there is text after the caret | remote.terminate |
So ctrl+w closes the tab only with an empty draft — clear the box first if
that is what you want. Only these three actions yield to the composer.
Queue keys
Anything you send while a turn is running is queued. The strip above the
input says so: ↻ ready · 2 queued, or ↻ paused · 2 queued · s to send next after a turn errored or was canceled with messages still queued.
From the composer (no queue focus needed):
| Key | Effect |
|---|---|
s | Send the first queued prompt now — only with an empty draft, a non-empty queue, and no turn in flight |
enter on an empty draft | Same as s |
esc | Sends the first queued prompt when nothing is in flight (cancels the turn instead when one is) |
ctrl+q | Clear the whole queue, after a y / n confirmation |
Once queue.focus (ctrl+e) has entered queue-focus mode:
| Key | Effect |
|---|---|
up / down | Move the selection |
alt+up / alt+down | Reorder the selected entry |
d | Remove the selected queued prompt |
s | Send the selected prompt now (cancels an in-flight turn first) |
enter / e | Load it back into the input for editing |
esc | Leave queue focus |
s, d and e also accept their shifted/CapsLock forms (S, D, E).
The agent-output pane (full-screen sub-agent stream view) closes on esc or
q.
Dialog keys
- Permission prompt —
aAllow ·AAlwaysAllow ·!Approve all for session ·eEdit params (esccancels,ctrl+ssubmits) ·dorescDeny. Publication calls (push, release, merge request, package or image publish, a forge write) get their own prompt headedPermission required — PUBLISH TO A REMOTE, whose row is[a] Allow once [e] Edit [d] Deny Esc → Deny;Adoes nothing there. - Quit dialog —
yyes, end this node’s sessions ·bbackground sessions and quit ·tterminate every session on this node and quit ·noresccancel. - Session manager —
up/downselect ·enteropen/resume ·nnew session ·k/dkill (active row) or delete (previous row) ·Kkill every active session ·escclose. - Command history — type to filter ·
up/downselect ·enterload into the composer (not sent) ·escclose. The initial filter is the draft already in the composer. - Recipe picker — alphabetical by id · type to filter ·
Tabmarks the highlighted recipe as the default ·enterconfirm ·esccancel. - Model menu (
/models) —up/downselect ·enterswitch this session to the highlighted connection ·Tabthenenterset/clear the default ·Ctrl+Llock the highlighted model (on a provider header, lock the provider) ·Ctrl+Uunlock without reconnecting ·Ctrl+Hshow/hide unreachable connections ·esccancel. - Command palette (
Ctrl+P) — alphabetical · type to filter, prefix matches rank above contains-matches ·enterrun ·escclose. - Skills dialog —
up/downselect ·spacetoggles the highlighted category ·escclose.
Customizing
Bindings live in tui.yaml under the keys: section, keyed by the action
names in the tables above. What tui.yaml does not configure is what the
fixed keys above are: the ctrl+c/esc semantics and the composer editing
keys are deliberate, not unimplemented. If you hand-edit tui.yaml, run
sonny doctor — it refuses a keymap that binds an unknown action or collides
two chords (the session.plan_mode chord in particular is refused onto
another action’s key).