Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Driving the keyboard

Two tools reach TermHerd’s own interface — the sidebar, the tab strip, the overlays — rather than a terminal. Typing into a session stays run_in_session’s job.

ToolTakesTests
press_keyschords in settings.json syntax — "cmd+shift+s", "ctrl+tab", "escape"the binding, resolved through the live keymap, including your overrides
run_actionkebab-case action names — "split-vertical", "activate-tab-3"the behaviour, skipping the keymap, so it survives a rebind

Both take a list applied in order and return one steps entry per item, plus the resulting focused_handle.

Why it is a real key event

A chord is dispatched as a synthesised key event fed to the app’s key handler — the whole routing ladder, not just a keymap lookup.

That is what makes Escape and Enter reachable at all: they are overlay keys, bound to no action. Without them an agent that armed a close confirmation would have no way to answer it, and would park the app until a human intervened.

The two are not equally complete, and the difference matters to a caller. escape leaves every prompt — that is what guarantees an agent can always back out. enter confirms the confirmation prompts but commits neither rename, because both commit through a widget callback no synthesised event reaches (#246).

The corollary: an open overlay consumes an MCP press exactly as it consumes a keypress. The step reports which prompt ate it, so a caller learns why its chord did nothing. run_action is gated on the same ladder on purpose — neither tool may reach a state the keyboard cannot.

Reading a step

OutcomeMeansExtra field
ranthe ladder applied itaction — the name that ran
inertnothing happenedreason — see below
overlayan open prompt consumed itwhich prompt
typedbound to nothing, so it reached the focused terminal
unboundnothing claimed it

ran means the shell applied the event, not that the effect was interesting: activate-tab-9 on a single-tab workspace reports ran, because the event was applied and absorbed. Collapsing that into inert would make the distinction useless.

The two kinds of nothing

inert carries a reason because they call for opposite responses:

ReasonMeansDo
no-surfacethe action is wired to nothing yet (open-new-session is the one)stop — retrying is pointless
no-contexta precondition was absent — nothing focused to derive a repo from, no closed tab to reopen, nothing to scroll, nothing selected to copycreate it, then retry

Seven handlers can refuse this way, and each says so at its own refusal site.

copy refuses on the terminal’s own answer: it holds a selection or it does not, wherever that selection has scrolled to. An MCP drag in a forwarded pane selects nothing (it carries no Shift, so it is the program’s), so copy after it is no-context — and the program’s own clipboard write (Claude Code copies a drag on release) is left alone. A human’s Shift+drag there is a selection, and copy runs on it.

Answering an overlay

escape usually cancels; enter usually confirms. Three cautions:

  • On quit-confirm, enter quits the app — killing every session and the connection you are speaking over.
  • session-rename (the sidebar’s inline ✎ field) does not commit on enter, and neither does tab-rename: both commit through the widget’s own submit, which a synthesised key event never reaches. escape abandons either — so you can always back out and start over — but committing a rename over MCP is a missing capability, tracked as #246.
  • Every other overlay is exitable from the keyboard, and a test sweep derived from the overlay enumeration — not a hand-written list — is what keeps it that way. A new overlay added without an exit fails there.

Rejection is all-or-nothing

A malformed chord or an unknown action name rejects the whole call before anything applies. The error names the offender and the syntax. This is deliberate: a half-applied sequence is worse than none, because the caller cannot tell how far it got.

The action catalogue — less the activate-tab-N family, which run_action accepts all the same — is published by the stdio server at termherd://keys/schema; the live bridge serves tools only, so its run_action error message carries the syntax instead.

Example: verify a keyboard gesture end to end

run_action(["split-vertical"])        → { steps: [{ result: "ran",
                                                    action: "split-vertical" }],
                                          focused_handle: "4" }
screenshot({ max_width: 900 })        → the pixels, to check the divider
press_keys(["cmd+w"])                 → { steps: [{ result: "overlay",
                                                    overlay: "tab-close-confirm" }] }
press_keys(["escape"])                → cancelled