Keys
Every default binding that ships. All of them are ordinary entries in one table — set the same key in your init.ts and yours replaces ours.
Nothing here is fixed. Every key below is an ordinary binding to a command name, in the same table your init.ts writes to. Setting the same key replaces it; neosh.keymap.del removes it; a capability whose key you delete still runs from ^K by name. ^Z lists the live registry, which is the truth — this page is what ships. See keymaps for how to change any of it.
Every default is a key your terminal already sends: Ctrl with a letter, ⇧⇥, ⏎, ⌫, Esc. Nothing needs fn or Option-as-Alt. Arrows, PgUp/PgDn, Home and End are bound wherever they mean something and are never the only way to do anything.
Chat
| Key | Does |
|---|---|
⏎ |
Send. While a turn is running, steer it: the message is taken in at the next gap |
⇧⏎ |
Newline, so a pasted snippet stays one message |
Esc |
Interrupt the turn in this conversation. The conversation survives it |
^Y |
Take the last thing you queued back into the composer, to change it or drop it |
^V |
Attach the image on the clipboard — the picture, or the one it only names |
⌫ |
On an empty composer, take the last attached image back off |
^P |
Pick a model. Mid-turn too — the running agent is told |
^E |
Every option this model has, on one panel, applied as you move |
⇧⇥ |
Permission mode. Belongs to this conversation and is saved with it |
^T |
Projects and conversations |
^J |
The computers in this workspace |
^F |
The archive |
^N |
New conversation. With another computer paired it asks which one first; in a repository it then asks where |
^O |
Add a project — a path, another computer, or a repository address to clone |
^B |
Toggle the sidebar |
^K |
Command palette |
^W |
Windows, panes and tabs — hold it to see what follows. See below |
/ |
Complete a command by name — neosh’s, and whatever the agent accepts |
^L |
What the plan has left, and where the week went |
^G |
Git status |
^D |
Show what changed |
^S |
Read the transcript |
⌥Y |
Copy this conversation’s directory |
^A ^X |
Select everything, cut the selection |
^C |
Copy the selection, or clear the draft, or (twice) quit |
PgUp PgDn ^End |
Scroll, and back to the newest message |
^R |
Reload configuration |
^Q |
Close this terminal. Whatever is running keeps running |
^Z |
Every binding, live |
Dragging an image onto the terminal pastes its path, and a pasted path to an image is attached rather than typed out.
Composer editing is a text field: ←/→ by character and ^←/^→ by word, Home/End and ^Home/^End for the ends, shift with any of them to select, ^⌫ and ^U to delete a word or back to the start of the line. ^W was the word-delete and is now the window prefix — a key cannot both delete a word and wait to see whether a window verb follows it, because the waiting is what makes every word-delete feel late. ui.keys.delete_word and ui.keys.window_prefix swap them back if you would rather.
model.upgrade and model.downgrade ship with no key — they had ⌥↑/⌥↓, which is not a key every terminal sends. ^K runs both by name, and init.ts can bind them.
Windows, panes and tabs — ^W
The one prefix that ships. It earns it: twenty-four verbs, and no Ctrl-with-a-letter left to give them — so most would otherwise arrive with no key at all.
Hold ^W and wait. A panel lists everything that follows it, read out of your own keymap rather than out of this page — so a rebinding shows your letter, and a plugin that adds a verb under the prefix appears in it with nobody writing glue. ui.keys.hint_delay is how long the pause is (300 ms; 0 shows it at once), and ui.keys.window_prefix moves the prefix.
| Key | Does |
|---|---|
v s |
Split: the new pane on the right, or below. Focus follows |
c |
Split to the right on a new conversation — splitting and ^N in one key |
q |
Close this pane. The last one is refused — ^Q closes the terminal |
o |
Close every pane but this one |
h j k l |
Go to the pane that way. No wrap-around: the edge is where it stops |
w |
The next pane, or the next tab when there is only one pane — so it always goes somewhere |
< > |
Narrower, wider — Vim’s, and the same keys the project panel resizes with |
- + |
Shorter, taller. At an edge the other boundary moves rather than nothing happening |
H J K L |
Move this pane to that edge — Vim’s, and the counterpart to hjkl moving you |
x |
Swap this pane with the next, keeping their places |
= |
Give every pane an equal share |
t |
A tab. Asks what goes in it: a new conversation, a shell, or this conversation again |
T |
A tab with a shell, skipping the question |
S |
Split into a shell |
X |
Close this tab and everything in it |
n p |
The next / previous tab, wrapping |
1–9 |
That tab, counting from the left as the bar numbers them |
r |
Name this tab. ↵ on an empty name gives it back to being named by what is in it |
, . |
Move this tab along the bar |
? |
The list, now rather than after the pause |
Every pane is a whole conversation surface — its own transcript, its own message field, its own draft, scroll, search and reading position. Two chats side by side are two of all of that; typing in one does nothing to the other. A split starts on what you were already reading, which is what :split means in Vim and is the only version that needs no dialog; ^T and ^N then change what is in the pane you are in.
A pane can be a shell. ^WT for a tab with one, ^WS to split into one. Every key goes to the program inside — ^C interrupts it — except the window prefix, which is always your way out. It runs $SHELL -l in the conversation’s directory.
The strip along the top is always there. Tabs on the left, numbered as ^W1…^W9 counts them, and the keys for wherever you are on the right. The pane with the keyboard is the one whose edges are lit.
Reading the transcript — ^S
^S puts the editor in Normal, so chat’s bindings stop firing and these get their keys back. Only ^C, ^Q, ^R and ^S are bound in every mode.
| Key | Does |
|---|---|
hjkl, arrows |
Move. A count first does it that many times: 5j |
w b e ge |
By word. W B E by WORD — everything that is not a space |
0 ^ $ g_ |
Line start, first non-blank, last character, last non-blank |
f F t T then ; , |
To a character on this line, or just before it |
% |
The bracket matching the one under the cursor |
gg G |
Ends of the transcript. With a count it is a row: 12G |
^D ^U |
Half a screen |
^F ^B, PgUp PgDn |
A screen |
^E ^Y |
Scroll a line without moving the cursor |
H M L |
The top, middle and bottom row of the window |
zz zt zb |
Put the cursor’s line in the middle, at the top, at the bottom |
[ ] |
Previous / next turn |
c C |
Next / previous tool call. The card you land on opens itself |
{ } |
Previous / next block |
⇥ za |
Keep the tool card under the cursor open |
/ ? then n N |
Search, and step through the matches |
* # |
Search for the word under the cursor |
v V |
Start a selection; V by whole lines |
o |
While selecting: the other end of what you have |
gv |
The selection you last gave up, back where it was |
iw aw ip ap i" i( … |
Text objects, while selecting. a takes the delimiters |
y |
Copy the selection, and leave |
yy Y |
Copy the line |
y + a motion |
Copy what it covers: yw, y$, y5j, yG |
yi + an object |
Copy one without selecting it first — yiw, yi" |
yc |
Copy the code block the cursor is in, without its indent or language line |
ym |
Copy the whole turn — the question and everything it produced |
yp |
Copy this conversation’s directory |
ya |
Copy the entire transcript |
i a o ⏎ |
Back to the composer |
^S |
And back out, the way you came in |
Esc |
One thing per press: the selection, then the search highlight, then the mode |
There is no ^V: a blockwise selection is a rectangle rather than a range. Nothing here edits — no d, x or p — because the transcript is an artefact you take pieces out of.
The card you are standing on is open, up to chat.preview_lines. Reading the last row keeps reading it: a turn still running writes below you and carries you along. Anywhere else you stay put, and G starts following again.
Answering a question
What an agent gets when it asks you something. Every key here is bound against the neosh.question buffer kind, so ^Z lists it and init.ts can move it.
| Key | Does |
|---|---|
↵ |
Take the option under the cursor. With something typed, send that instead |
1–9 |
Take that option, while nothing has been typed |
⇥ |
Tick or untick, on a question that takes more than one |
^P ^N, ↑ ↓ |
Move |
⇧⇥ |
Back to the previous question |
| type | Your own answer. ⌫ back to the options, ^W a word, ^U all of it |
Esc |
Dismiss. The agent is told nobody answered, which it can act on |
The project panel — ^T
| Key | Does |
|---|---|
↵ |
Open a conversation, fold a project, or add one from the + row |
j k |
Move. With a count, that many rows: 5j |
^D ^U |
Half a screen; PgUp/PgDn for a whole one |
gg G |
The ends of the list, and 5gg / 5G for the fifth row |
> < = |
Wider, narrower, back to the default — it is the sidebar.width setting |
n |
New conversation in the project under the cursor |
o |
Add a project |
f |
Pin a project to the top |
J K |
Reorder within a group |
r |
Rename a conversation |
y |
Copy the row’s directory |
t |
A terminal in this project, in a tab of its own — on this computer, or on the machine the row is on |
c C |
Connect to that computer, or disconnect from it. Only on a row about another machine |
@ |
Choose the short code that computer is drawn with — the ms in @ms |
p |
Pull that repository (a git-plugin contribution) |
d |
Remove a worktree from disk. It asks first (a git-plugin contribution) |
x X |
Archive, delete. Deleting the last conversation in a worktree removes the checkout too, and says so. On a repository’s heading, X takes the project off the list; on a worktree’s, it removes the checkout |
a |
The archive |
, |
Settings |
⇥ |
On the plan rows: how much of it to show |
? |
The keys for whatever row you are on |
Esc |
Back to the composer |
Settings — , in the project panel, or /settings
Every option in the workspace on one screen: a curated General page first — the prompt appended to every message, keeping main checkouts up to date, the model, the theme — then a section per namespace, the other computers and their codes, and the live keymap. Built from the option registry, so a plugin’s own settings have a section here with nobody writing glue. A change is applied and written into [options] in config.toml as you make it, leaving every comment and every other line in that file alone; putting a value back to its default takes its line back out.
No Ctrl-letter is left to give it, so it is , in the project panel (as ⌘, is on a Mac), /settings in the composer, and settings.open in ^K.
| Key | Does |
|---|---|
⇥ ⇧⇥, ] [ |
Next / previous section |
j k, ^D ^U, gg G |
Move |
h l, ← → |
Along a switch or a list of values, or nudge a number |
<Space> |
The next value along |
↵ |
Change it: toggle, cycle, or open a field to type in. Prose — the appended prompt, the system prompt — gets a field several lines tall: ⏎ is a new line, esc keeps it, ^C throws it away |
r |
Back to the default, and out of config.toml |
/ |
Find a setting — every word, in any order, over names and descriptions |
y |
Copy the option’s name |
Esc q |
One thing per press: the search, then the panel |
The archive — ^F, or a in the project panel
Rows can be ticked, and every verb means what is ticked or — with nothing ticked — the row under the cursor.
| Key | Does |
|---|---|
↵ |
Put this one back and go to it |
u |
Put these back, and stay here |
X |
Delete these, for good. It asks, and says how many |
^X |
Empty what the panel is currently showing, so a filter first narrows it |
<Space> ⇥ |
Tick this row, and step down |
a |
Tick everything showing, or untick it |
e |
Copy these conversations to the clipboard, as markdown |
y |
Copy this conversation’s directory |
/ |
Filter — on the title, the project and the path |
s S |
The next ordering along; the next grouping along |
j k, ^D ^U, gg G |
Move, with counts |
? |
The keys |
Esc |
One thing per press: the marks, then the filter, then the panel |
^F q ^C |
Close it |
A panel that does not fit
Every read-only panel in the workspace — the key sheet, git status, a diff, another machine’s conversation — is given whatever height the screen has. When that is less than it wants to draw, the panel scrolls, a bar appears down its right border to say there is more, and the mouse wheel scrolls whatever the keyboard is in rather than the transcript underneath it.
| Key | Does |
|---|---|
j k, ↓ ↑ |
A line |
^D ^U |
Half a screen |
^F ^B, PgDn PgUp |
A screen, less a row of overlap |
gg G, Home End |
The ends |
Esc q ↵ ^C |
Close |
They are ordinary bindings on the neosh.scroll buffer kind, so ^Z lists them and init.ts moves
them:
await neosh.keymap.set("normal", "<C-n>", "scroll.down", {
scope: { kind: "buf_kind", name: "neosh.scroll" },
});
A panel with a cursor of its own — a picker, the project panel, the usage panel — is not one of
these: it moves its cursor and the view follows, which is why j there means “the next row” and not
“one line down”.
Pickers
| Key | Does |
|---|---|
↵ |
Choose |
^N/^P, ^J/^K, ↑/↓ |
Move |
| type | Filter |
⇥ ⇧⇥, → ← |
Between the two panes of the model picker |
Esc, ^C |
Close |
In the model picker, chords — because every bare letter a picker takes is a letter its filter can never contain:
| Key | Does |
|---|---|
^S |
Sign in to the provider the rail is on |
^R |
Ask that endpoint what it serves again |
^A |
Add a model by id |
^D |
Remove one you added |
These are settings (ui.keys.next, ui.keys.accept, …), so rebinding them changes what the pickers say they do.
Rebinding
await neosh.keymap.set("chat", "<C-l>", "chat.clear");
await neosh.keymap.del("chat", "<C-o>");
Keys bind to command names, never to callbacks — which is what makes every binding listable with ^Z, runnable by name from ^K, and replaceable by you or by another plugin. Keymaps covers modes, scopes, <leader> and sequences.