The TUI
rtaBare rta on a terminal opens the interactive shell. In a pipe it prints help instead, so a script never hangs on an interface nobody can see.
It is the same capabilities as the CLI — the same declarations, the same safety classes, the same results. What changes is that you can browse them, fill inputs in a form, and see a table you can walk through.
The landing dashboard
A search bar across the top, and one tile per plugin that has something to show at a glance. Typing filters every capability in the catalogue on the fly.
| Key | What it does |
|---|---|
/ | Search |
enter | Open |
esc | Back |
q or ctrl+c | Quit |
? | Every key the screen answers, aliases included — what the footer had no room for |
[ ] | Move a tile |
H | Hide a tile — or remove it, when it is one you added |
+ | Add a tile — on the dashboard it opens the catalogue; on a catalogue row or a search match it adds that one |
p | Plugin inventory — where a hidden automatic tile comes back |
t | Theme |
c | Configure |
Tiles are yours to arrange. H hides one you never look at, and p opens the inventory where it comes back; one panel of an entry that expanded into several connections is hidden by its own key, which the inventory has no row for, so it comes back with the rta dashboard unhide <id> --profile <profile/instance> line its note prints. On a tile you added, H removes the entry instead — the footer says remove there, and the note prints the rta dashboard add line that puts it back; + on a catalogue row or a search match adds one the automatic set left out, asking which connection when the capability takes one.
Stating the dashboard yourself
With no dashboard: block, rta builds one: a tile per plugin that has a capability which is Read, needs no input, and is cheap enough to run unasked. Plugins installed later appear on their own.
There are three ways to change that, and the difference between the first two and the third is whether tomorrow's plugin still shows up.
Adjust the automatic set. hidden: and order: bend it without freezing it:
dashboard:
hidden:
- git.overview
order:
- sys.overview
- note.list
columns: 3Add to it. add: joins tiles to the automatic set, and it is the only way to get a capability the automatic dashboard leaves out. Anything that reaches off the box — every kube, pg, s3 and vault capability — is kept off it deliberately, however cheap it looks: a dashboard runs its tiles on load and again on a timer, and nobody expects opening a TUI to spend an API quota or disclose anything to a third party. An entry here is you asking for it, which is a decision the automatic path can't make for you. Three surfaces write the same entry: this block, rta dashboard add from a shell, and + in the TUI on a catalogue row (b) or a search match (/), which asks which connection the tile is about when the capability takes one — a profile, one of its labelled instances, or the switch — and lands on the dashboard with the new tile selected. + refuses what add refuses, in the same words, and on an automatic tile H took off it shows the tile again rather than writing a twin. A tile added from another terminal reaches an open dashboard on its next refresh.
dashboard:
add:
- id: eol.watch
- id: kube.overview
profile: prod
- id: kube.overview
profile: staging
- id: pg.overview
profile: staging/analytics
span: 2profile: pins a tile to one connection — a profile, or name/instance for one of several connections to the same plugin — and the tile is about that connection whatever rta use switched on, with the name on its panel. That is what lets one capability sit on the dashboard twice, once per cluster. Without it a tile follows the switched-on environment: switch to staging and the pg tile is about staging. with: fills the capability's inputs. span: widens a tile past what its own declared width works out to — for the one you actually read.
One entry, one panel per connection. A profile can hold several connections to the same plugin — cnpg/gitea, cnpg/keycloak, three more — and a tile whose profile names no instance becomes one panel per connection that profile holds for its plugin, each named on its panel. {id: cnpg.overview, profile: ohmlab} is every cnpg database ohmlab knows, and one added to the profile next month gets its panel on its own, the same rule the automatic set follows for a plugin installed next month. A tile that follows the switch expands the same way, into whatever environment is on: under ohmlab it is ohmlab's databases, under mirai-prod that environment's own. Name the instance, profile: ohmlab/gitea, for one panel. This is the one place a bare profile over several connections is not refused with "your call", as --profile ohmlab is on a single command: a dashboard is not a choice, showing every one is the glance, and no wrong pick is possible.
The same from a script, or without opening the file:
rta dashboard add kube.overview --profile prod
rta dashboard add kube.overview --profile staging
rta dashboard add cnpg.overview --profile ohmlab # one panel per cnpg connection ohmlab holds
rta dashboard add pg.overview --profile staging/analytics --span 2
rta dashboard add cert.expiry --set host=example.com
rta dashboard list # every panel bare rta would draw, where each came from, and what is hidden
rta dashboard hide cnpg.overview --profile ohmlab/keycloak # that one panel; its siblings stay
rta dashboard unhide cnpg.overview --profile ohmlab/keycloak
rta dashboard rm kube.overview --profile stagingadd refuses what the file would have quietly got wrong: a capability that is not a read, an input it does not declare, a credential under --set, a required input nothing fills, a profile that does not cover the plugin. Adding the same tile again replaces it, so the command is safe in a script that runs on every boot. In the TUI, H on an added tile withdraws its entry rather than hiding the capability — hiding by name would take both kube tiles down — and the footer says the command that puts it back; H on one panel of an entry that expanded hides that connection's panel by its key and leaves the rest, and unhide is the way back. hide and unhide are what H and the inventory pane do, from a script. Adding a capability the automatic dashboard already shows makes the entry that tile: rta dashboard add sys.overview --span 2 widens it rather than doubling it, and the bare form is refused, with the way back when the tile is hidden. [ and ] move an entry that expanded as one, its panels together, because the file can place the entry and nothing finer.
Or state it exactly. tiles: replaces the automatic set outright — hidden: and order: are not consulted, because the list is already both, except for a hidden: key naming one panel of an entry that expanded, which is a panel and not an entry the list could drop. Its entries take the same profile:, with: and span:, and add: entries follow the list:
dashboard:
tiles:
- id: kube.overview
profile: prod
- id: note.list
span: 2Worth knowing what a tile costs before you add one: it refreshes on a timer for as long as the TUI is open, so a cluster-wide kube.overview tile is a round of kubectl calls at every refresh, for every hour the terminal stays open. rta explain kube.overview prints what a capability actually reads, which is not always only what its name suggests, and its dashboard row says how often a tile of it re-runs.
A capability can set its own pace. The dashboard's timer runs every few seconds, and that is what a tile gets unless its capability declared a longer interval — eol.watch, eol.check and eol.products re-run every two hours, pkg.overview, pkg.outdated and pkg.os every hour — because their answers move by the day and a run costs a request per product or a dozen subprocesses. A plugin's capability declares one the same way: kube.overview re-runs every minute, since a run is five cluster-wide lists at once and nothing it reports moves faster than the cluster's own controllers decide it. So rta dashboard add eol.watch is the version watchlist on your landing screen at a pace endoflife.date would not notice. Switching environments re-runs every tile that follows the switch, since its inputs just changed, and leaves a pinned tile inside its pace, since its did not.
Two things the block will not do, whatever you write in it:
- A tile that is not
Readis dropped. Otherwise{id: kv.rm, with: {key: old-token}}would delete that key on startup and keep deleting it — on a timer, with no form and no confirmation, since the destructive gate lives on the CLI and the browse path and a tile goes through neither. - The whole block is ignored unless the config is one you named — your user config directory, or
RTA_CONFIG. rta falls back to./.rta.yamlwhen there is no user config directory (ordinary underenv -i, in a container, in CI), and a cloned repository does not get to arrange your screen:{id: http.get, with: {url: …}}there would be a beacon that starts the moment you open the TUI in that directory.hidden:is the same hazard pointed the other way — it can take the agent tile off the screen, and that tile is where you notice a parked consent request before its clock runs out.
The catalogue
Every capability as a table grouped by plugin — one row each, with its ID, its safety class and its summary. The filter stays live, every pane is bounded by the terminal and scrolls inside it, and the mouse wheel works. enter runs the row; + puts it on the dashboard.
Running something
A capability with inputs opens a form built from its declaration:
- Fields that declare
Optionsbecome a picker. - Fields that declare
Suggestcomplete from what exists on your machine — your tags, your keys, your hosts file. Pathfields complete directory by directory as you type.ctrl+eopens$EDITORon a long body.shift+enteraccepts every remaining field at its current value, for a form whose defaults are already right.
Which environment the run goes to
A capability a profile can fill opens with the environment picker first, defaulted to whatever is switched on. Under an environment that reaches its service through a forward, the host and port boxes show the forward's own coordinate — kube:homelab/databases/svc/postgres in the host box, 5432 in the port box — and the picker's help line says the same: runs through the kube forward to homelab/databases/svc/postgres:5432, which fills host, port. Leave them and the forward answers per call. Type over one and the run connects directly to what you typed, without opening the forward — the way out of a coordinate that is wrong without leaving the form to fix the profile first. It is first because it changes what every other answer means — a host typed under one environment is not the same value under another — and moving it rebuilds the form on the environment it now names, rather than leaving one environment's values on screen under another one's name.
Boxes that environment fills open showing its values, and you can still type over them. A credential is the exception: it opens empty, because seeding a masked box paints your passphrase's length in dots. So the box says where the value comes from instead:
password
password for the role — staging fills it from kv:staging-db-password (secret)
>The reference, never the value. kv:staging-db-password is the name of an entry — something you wrote, in a file you can read — and naming it answers the question an empty masked box could not: whether you have to type this at all. An exported RTA_PROFILE_STAGING_PASSWORD is named the same way, and named as the winner, because that is the one the run will actually use. A box under an environment that supplies nothing says nothing, and you type it.
The store asks once per sitting. A passphrase store unlocked from a TUI form stays open in this TUI for fifteen minutes past its last use — the header reads ◐ store unlocked · 14m while it does — so the next kv action runs without the unlock form, and a profile's kv: reference is filled instead of failing because nothing on the update loop could ask. It lives in this process's memory and nowhere else: no file, no environment variable, nothing a plugin or an agent started from here inherits, and closing the TUI ends it. A wrong passphrase ends it, and so does a rekey. What it never covers is the reveal itself: v and c still open the unlock form, because the unlock is what makes showing a secret a deliberate act rather than a slip on a list.
Tab means one thing on every field
Take me forward. What that is depends only on what the box under the cursor can still be completed to, never on which field it is:
| What the box holds | What tab does |
|---|---|
| something an offer extends | takes the offer, and stays — so a path, a cluster coordinate or a comma list is walked a segment at a time, exactly as in a shell |
| nothing yet | says what is on offer, because there is no ghost to accept until something is typed |
| everything there was to complete | moves to the next field |
shift+tab is the previous field and enter is the next one, whatever the box holds. A field that completes says so under itself, because the footer speaks for the screen rather than for the box under the cursor.
↓ and ↑ browse the offer from an empty box: down puts the first candidate in the box, the next down its neighbour, up walks the other way, and both wrap. Type over what was placed and the box is yours again — the arrows then cycle the matches of what you typed, which is what they always did once a letter was in.
A destructive capability never runs from its form. What opens instead is its own dry run — what the call would do with the values you gave, the same preview an operator sees on a parked agent call — and enter on that screen is the consent. e reopens the inputs, esc runs nothing. A plugin from outside the binary gets no dry run before you confirm, because rta does not run a plugin's own claim about itself before anyone has said yes; its screen shows the inputs the call will run with, and says so.
The plugin inventory
p opens what is installed, what each plugin puts on the dashboard, and — the part worth having a pane for — any artifact rta found on $PATH and refused to run. A trust gate's failure mode is silence: a plugin that is installed and doing nothing looks exactly like one that was never installed.
| Key | What it does |
|---|---|
p | Open the inventory (and close it) |
space | Show or hide its dashboard tile |
t | Approve an artifact, or take an approval back |
a | Choose which credential locations it may read |
c | Configure it |
enter | Its capabilities, in the search bar |
Grouped by where the bytes came from
The pane bands its rows by provenance, because that is the fact that changes how every other fact on a row reads. "13 capabilities, one of them destructive" means one thing about code compiled into the binary you chose to run and something else about a file that appeared on your $PATH, and a list sorted by name buried the two or three you did not compile among a dozen you did.
| Band | What it means |
|---|---|
| built in | Compiled into the rta binary you are running, which is why these need no digest |
| installed by rta | rta placed these bytes from an index you attached; the row carries the version, the index and what the signature check found |
| found on $PATH | Binaries rta did not place and holds no record of |
| not run | Discovered and never launched, because nothing has approved them yet |
A stock install is entirely built in, so no bands are drawn at all — one band separates nothing.
There is no "official" band, and that is a fact about rta rather than an omission. rta attaches no index by default: rta plugin index add <name> <repository> is the whole story, and the name is yours to choose. So an index called official is only an index somebody called official, and a band drawn from that name would be rta vouching for provenance out of a string anyone can pick. What rta genuinely knows is whether it placed the bytes itself, and that is what the bands say. Trust here binds to a digest, never to a name — which is also why an rta.lock record is matched to a row by digest: an entry naming this plugin and describing different bytes belongs to a half-finished upgrade, not to what you are running.
t is the decision made where the evidence is: the digest and the path are on the screen while you take it, which the command line shows you only afterwards. Neither direction takes effect on the process you are in — trust is read once, before anything is launched — so approving says it loads when rta restarts, and withdrawing says the plugin already running stays running until rta exits.
a is the permission after that one. Approving says these bytes may run; allowing says what they may read — a kubeconfig, an SSH directory, whatever the plugin declares it needs. The row shows both sides: what it has been allowed, and what it is still asking for.
a opens a form where t is a keypress, and the difference is deliberate. Approving is one yes/no about one thing already named on the row. Allowing is plural — a plugin can declare several locations, and a bare key would hand over every one of them from a cursor position. The form is also what makes taking access back expressible: the list you submit is the whole grant, so clearing a box withdraws that location and there is no second command to learn. A plugin you have not approved yet cannot be allowed anything — running at all is the decision that comes first, and the pane says so rather than opening a form that could not succeed.
Working with results
| Key | What it does |
|---|---|
enter | Open the row |
e | Edit inputs and run again |
r | Re-run |
y | Copy as JSON |
d | Delete |
esc | Stop a run that is still going; on a result, go back |
? | Every key the screen answers — on any screen where a key is a command rather than text |
A log — agent log, or any table that declares its newest row last — opens on that row, scrolled to the end, so what just happened is under the cursor and the past is a key up.
Views are actionable rather than static, and what a view offers is the plugin's own declaration rather than a list the TUI keeps: each capability says which keys its result answers, what they open and what they read off the row, and rta explain <capability> prints the same. In the notebook, t turns a note into a to-do or back, d checks one off and x removes; on the consent queue, L opens the lock form beside the call that made you want it, and on the lock list x lifts the lock under the cursor — from the list and from a record's own page, refreshing as it goes. A third-party plugin's list gets exactly the same treatment for exactly the same declaration. Detail pages are composed from other capabilities' views rather than rebuilt, so a record page shows metadata, prose and relations as separate sections.
Answering agents
The agent tile says how many calls are waiting on you. From it, and from the queue it opens:
| Key | What it does |
|---|---|
w | The queue of parked calls |
g | The record of what agents did |
enter | Everything about the call under the cursor, including what it would do |
a | Allow it — stops to confirm |
d | Deny it — one key, no form |
L | Lock the agent that asked, its name already filled in from the row |
x | On the lock list: lift the lock under the cursor |
The queue refreshes itself every few seconds while it is on screen, so a call that parks while you are reading appears, and one that expires leaves. A form opened from one of these screens asks only about this machine: the --server box that aims the same command at a remote queue, and the operator passphrase that signs it, are offered when you type the command's name, not when you act on a row.
Profiles
f opens the profiles pane — your configured environments, which one is on, and what each covers. n creates one and c edits it, which is the shortest way to define a profile: the form is generated from each plugin's declared inputs, and it knows which of them are secrets, so a credential lands in secrets: as a reference instead of in set: as a value.
| Key | What it does |
|---|---|
f | Open profiles |
u | Use this one |
enter | The plugins inside it |
n | New |
d | Delete |
s | Set a credential |
y | Copy export lines for the credentials nothing has set — shown only when there are some |
Adding a plugin to an environment is one form. Press n in the pane and the editor opens on the plugin under your cursor, already pinned to its artifact, with that plugin's own configuration keys under it — tab completes the name, and naming a different one swaps the keys below. The form asks three things and says where each starts:
── how to reach it — one of these, or neither to connect directly ────
── what staging changes about it ────If the plugin you added needs a credential and nothing supplies one, the credential editor is the next screen rather than somewhere to navigate back to. esc declines; the entry is already saved.
Switching a profile on here does the same thing rta use does, including the part worth remembering: while a profile is on, rta mcp serve refuses every other one. See Profiles.
Charts
Some capabilities render as charts when there is a terminal to draw in:
rta sys cpu --cores
rta net ping example.com --graphMarkdown bodies — notes, audit findings, anything returning prose — are rendered rather than dumped.
Untrusted plugins
If rta found an rta-plugin-* binary it has not been told to run, the TUI says so in a pane rather than a startup line. The line would be written to the primary buffer, and the TUI opens on the alternate one — so it would be covered before anyone could read it. The pane is the only place a person inside the TUI can learn a decision is pending.
See Using plugins.