Skip to main content

The interactive TUI


description: "Use the sendra tui terminal app (TUI) to browse, run and edit requests, switch environments, review run history and find every keybinding."

The interactive TUI

sendra tui opens a full-screen terminal app for browsing and running the requests in a collection (or a single request file), instead of one shell invocation per request. It reuses sendra-core directly — the same request model, YAML loading, environment resolution, capture and HTTP execution run/test use — with one exception: pre_request/post_request scripts are not run from the TUI (see below). Everything else behaves identically whether you send it from the TUI or from the command line.

Installing and launching

The TUI ships inside the same sendra binary as run/test — there is nothing separate to install. Building or installing sendra (see the README) gets you both.

sendra tui # start with nothing loaded
sendra tui path/to/collection.yaml
sendra tui path/to/request.yaml # a single request file works too
sendra # bare invocation — identical to `sendra tui` with no path

A bare sendra, with no subcommand at all, launches the TUI with nothing preloaded. This is deliberate, not a fallback: typing just sendra is meant to be a reasonable way to start.

The TUI requires an interactive terminal on stdout. If stdout has been redirected or piped, it refuses immediately with a plain error rather than drawing frames nobody can see:

sendra-tui requires an interactive terminal on stdout; it looks like stdout
has been redirected or piped. Run it directly in a terminal.

The welcome screen

Launching with no path (sendra tui or a bare sendra) shows a welcome screen instead of an error. It looks in the current directory for .yaml/ .yml files sitting directly inside it (not subdirectories, and not .sendra/) and offers whatever it finds as a picker:

  • Something found nearby — a selectable list of the candidate files. Move with / or j/k, Enter (or r) opens the highlighted one.
  • Nothing found — a plain message with the two things you can still do: o to type a path, e to open the environment picker.

o is always available from the welcome screen, whether or not candidates were discovered, to type a path that isn't in the current directory.

Browsing and running requests

Once a collection is loaded, the main screen shows the request list on one side and a detail/response pane on the other.

  • /k, /j — move the selection.
  • Enter / r — run the selected request.
  • PgUp/PgDn/Home/End — scroll the response panel. Arrow keys and j/k always move the request-list selection, never the response scroll, regardless of whether a response is currently showing — the response panel's scroll lives entirely on its own four keys so the same physical key never means two different things depending on invisible state.
  • c — reveal or hide captured values (and, while merely browsing, auth-derived header/query values shown in the request preview). Starts masked every time; toggling never persists and is never auto-revealed on the next run.
  • / — filter the request list by name, live as you type. / moves within the filtered list, Enter runs the highlighted match, Esc clears the filter and shows every request again. The underlying selection is never reinterpreted while filtering — closing the filter leaves you on the same real request you were looking at.
  • i — edit the selected request.
  • n — add a new, mostly-empty request to the collection and immediately open it in edit mode.
  • d — delete the selected request, with a confirmation prompt first.
  • e — open the environment picker.
  • h — open the run-history browser for the selected request.
  • o — open another collection, as a new tab.
  • ] / [ — next / previous collection tab (see below).
  • Ctrl+w — close the current collection tab.
  • ? — open the keybinding cheatsheet; q / Ctrl+C quits (asking for confirmation first if anything open would be lost).

A request's status, once it has run, shows in the detail pane alongside its headers and body. Requests that have never been run yet just show their resolved request preview.

A request's pre_request/post_request scripts, if it has any, are not run when sent from the TUI — only sendra run/sendra test run them. Every other part of the send pipeline (environment substitution, config header merging, auth/query/body resolution, assertions, capture) is identical either way; this is the one honest gap, not a silent one. See Scripting.

Editing a request

i opens the selected request in an edit form covering every part of the request that has a corresponding editor. Editing is entirely in-memory until you save — Esc discards every change and restores exactly the state browsing was in, no matter what was typed.

  • Tab / Shift+Tab — move between fields, in this order: name, method, URL, each header row, the body (if editable), each auth field (if any), each assertion row, each capture row, then back to name.
  • Left/Right — move the cursor within a text field, or toggle a closed-set field (API key location, OAuth grant type, an assertion's operator, a capture's kind) when the focused field is one of those instead of free text.
  • Up/Down — move within the body field specifically (only meaningful while the body has focus).
  • Enter — insert a newline (body field only).
  • Backspace/Delete — edit the focused text field.
  • Ctrl+n / Ctrl+d — add / delete a header row.
  • Ctrl+a / Ctrl+x — add / delete an assertion row.
  • Ctrl+p / Ctrl+k — add / delete a capture row.
  • Ctrl+l — start an interactive OAuth login (only reachable when editing an auth.oauth block whose grant type is authorization_code — see below).
  • Ctrl+s — save. Refuses (staying in edit mode, with the problem shown) when the method is invalid, an assertion value doesn't parse, or a json:-typed body doesn't parse as JSON.
  • Esc — cancel, discarding the edit.

Method, URL, name. Plain text fields. An invalid method is flagged live and blocks saving; the name is optional — clearing it back to blank saves the request as genuinely nameless again, not as an empty string.

Headers. A list of key/value rows, added and removed with Ctrl+n/ Ctrl+d, each a separate text field for the key and the value.

Body. Editable as raw text when the request has a plain body:, a json: body, or no body at all. A json: body is shown pretty-printed and parsed back into JSON on save; a bad-JSON edit blocks saving with the parse error shown, the same way an invalid method does. body_file, form and multipart bodies are shown read-only, with a note explaining why: a body_file names a file some other tool may have open, and form/ multipart are structured, list-of-parts bodies that would need their own row editor — both are honest gaps, not silent ones.

Auth. Only a request that already has an auth: block can edit one here — this editor never introduces a new auth type on a request that had none. Whichever of bearer/basic/api_key/oauth is set shapes which fields appear:

  • Bearer — one token field.
  • Basic — username and password fields.
  • API key — name, value, and a Left/Right-toggled location (header/query).
  • OAuth — grant type (client_credentials/password/ authorization_code, cycled with Left/Right), token URL, client ID, client secret and scope always shown; username/password only reachable under password; authorization_url/redirect_uri only reachable under authorization_code. These fields are the static configuration sent to the token endpoint — editing them never fetches or validates a token itself.

OAuth authorization_code login. Because this grant needs a real human in a real browser, it cannot acquire a token just from the fields above the way client_credentials/password can. With the auth editor focused on an authorization_code block, Ctrl+l starts an interactive login: it opens your browser to the configured authorization URL, runs a local callback listener bound to the configured redirect URI, waits (up to five minutes) for the provider to redirect back, and exchanges the resulting code for a token. The acquired token is cached in memory for the rest of the session — every later run of a request sharing that oauth: config reuses it automatically, with no separate step. As a side effect, client_credentials/password tokens are cached the same way and also survive between runs in the same session, subject to their own expiry.

Assertions. Only json: path assertions (and their not: negated form) have a row editor here — Ctrl+a/Ctrl+x add/remove a row of path, operator, expected value and a negate toggle. The operator (equals, greater_than, greater_than_or_equal, less_than, less_than_or_equal, contains, length, matches) is cycled with Left/Right. Every other assertion kind (status, status_in, headers, body_contains, body_matches, elapsed_ms_under) is preserved exactly as loaded but has no editor here — an intentional gap, not a silent one.

Captures. A full row editor — Ctrl+p/Ctrl+k add/remove a row of name, kind (json path/header/status, cycled with Left/Right) and, for the two kinds that need one, the path or header name to read.

Multi-collection tabs

o opens another collection alongside the ones already open, as a new tab. Each tab is a fully independent session: its own selection, run state, edit session, environment, dirty markers and run history never bleed into another tab's.

  • ] / [ — next / previous tab, wrapping around.
  • Ctrl+w — close the active tab. If it has nothing at stake (no edit in progress, no unsaved new request, no run in flight), it closes immediately; otherwise a confirmation prompt asks first. Closing the very last remaining tab never leaves you with zero tabs — it resets that tab back to the welcome screen instead.

The tab bar itself only appears once a second collection is open — a single open collection draws with no tab bar at all, and the ]/[/Ctrl+w hints in the status bar only appear once a second tab exists to switch to or close.

Run history

h, while browsing, opens the run-history browser for the currently selected request — every past run of that specific request made during this session, most recent first. History is in-memory only: it is never written to disk, and it is gone the moment the process exits or the tab closes, since a response can carry arbitrary (and possibly sensitive) response bodies never asked to be persisted.

  • /k, /j — move the selection.
  • Space — expand or collapse the highlighted entry in place (a quick status-code-and-summary glance without leaving the list; more than one entry can be left expanded at once).
  • Enter — view the highlighted entry's full result, in the same response panel a live run uses.
  • Esc — back to the list (from the full-result view), or close the browser (from the list).
  • c, while viewing one entry's full result — reveal or hide that entry's captured values, independent of the live run's own reveal state.

Capped at 20 entries per request. Once a request's history passes 20 runs in this session, the oldest are dropped to make room for the newest — generous for what browsing actually needs, while bounding memory for a long session that re-sends the same request many times. Dropping is never silent: the browser says how many older runs were dropped once it happens.

Deleting a request drops its history outright along with it — there is no longer a request left for it to be about.

Environments

e opens the environment picker — every environment sendra-tui found at startup (from .sendra/environments/), sorted by name. An environment file that exists but failed to load is shown as a visible in-app error rather than silently missing from the list.

  • /k, /j — move the selection.
  • Enter — select this environment as the one the detail pane resolves variables against.
  • i — edit this environment's variables.
  • Esc — close, without changing which environment is active.

Editing an environment's variables (i) opens a row editor — name/value pairs, the same shape a request's own headers use:

  • Tab/Shift+Tab — move between fields.
  • Left/Right — move the cursor.
  • Backspace/Delete — edit the focused field.
  • Ctrl+n — add a variable row.
  • Ctrl+d — delete the focused row (asks to confirm first).
  • Ctrl+s — save; refuses, with the problem shown, on an empty or duplicated variable name.
  • Esc — cancel, discarding every change.

Confirmations

Every destructive action — deleting a request, deleting an environment variable, closing a tab, quitting with something unsaved — asks first, through the same yes/no prompt:

  • y / Enter — confirm.
  • n / Esc — cancel.

Quitting (q / Ctrl+C) checks every open tab, not just the active one: an edit left open, a request added but not yet saved, a pending delete, an environment edit in progress, or a run still in flight in any tab is enough to trigger the prompt. With nothing at stake anywhere, quitting exits immediately with no prompt at all. Pressing the quit key a second time while the prompt is already open confirms and exits, the same low-friction "ask once" behavior the dedicated y/Enter gives.

Opening another collection

o opens a path-input prompt:

  • Left/Right — move the cursor.
  • Backspace/Delete — edit the typed path.
  • Enter — open the typed path as a new tab.
  • Esc — cancel, opening nothing.

A failed load (a bad path, invalid YAML) keeps the prompt open with the error shown, rather than discarding what was typed.

The keybinding cheatsheet

?, from anywhere in the app, opens a full reference of every binding in the current context — global keys, then whichever screen or mode is active. ? or Esc closes it again. It draws on top of every other overlay, including the quit confirmation, so "what can I press right now" always has an answer.

The reference below mirrors that in-app cheatsheet — the same table this document's own keybinding sections above are drawn from.

Global — always available

KeyAction
q / Ctrl+Cquit (asks first if anything is unsaved)
?open/close the cheatsheet

Browsing (a collection is loaded, nothing else open)

KeyAction
/k, /jmove selection
Enter / rrun the selected request
iedit the selected request
nadd a new request
ddelete the selected request
eopen the environment picker
hopen the run-history browser
oopen another collection
/filter the request list by name
] / [next / previous collection tab
Ctrl+wclose the current collection tab
PgUp/PgDn/Home/Endscroll the response panel
creveal / hide captured values

Filtering the request list (after pressing /)

KeyAction
(type)narrow the list to matching names, live
/move selection within the filtered list
Enterrun the highlighted (filtered) request
Escclear the filter and show every request again

Welcome screen (no collection path given yet)

KeyAction
/k, /jchoose a discovered collection
Enter / ropen the chosen collection
otype a collection path instead
eopen the environment picker

Editing a request

KeyAction
Tab / Shift+Tabnext / previous field
Left/Rightmove cursor (or toggle an enum field)
Up/Downmove within the body field (body focused only)
Enternewline (body field only)
Backspace/Deleteedit text
Ctrl+n / Ctrl+dadd / delete a header row
Ctrl+a / Ctrl+xadd / delete an assertion row
Ctrl+p / Ctrl+kadd / delete a capture row
Ctrl+llog in (auth.oauth grant_type: authorization_code only)
Ctrl+ssave
Esccancel
(any other character)type into the focused field

Environment picker

KeyAction
/k, /jmove selection
Enterselect this environment
iedit this environment's variables
Escclose

Editing an environment's variables

KeyAction
Tab / Shift+Tabnext / previous field
Left/Rightmove cursor
Backspace/Deleteedit text
Ctrl+nadd a variable row
Ctrl+ddelete the focused row (asks to confirm)
Ctrl+ssave
Esccancel
(any other character)type into the focused field

Run-history browser

KeyAction
/k, /jmove selection
Enterview this entry's full result
Spaceexpand / collapse this entry
Escclose

Viewing one history entry

KeyAction
PgUp/PgDn/Home/Endscroll the response
creveal / hide captured values
Escback to the history list

Any confirmation (delete request, delete variable, close tab, quit)

KeyAction
y / Enterconfirm
n / Esccancel

Open-collection prompt

KeyAction
Left/Rightmove cursor
Backspace/Deleteedit text
Enteropen the typed path
Esccancel
(any other character)type into the path field