taria
ARIA for terminals. taria is an agent accessibility layer for terminal user interfaces. A TUI app declares its live widget tree, focus state, and available actions through a small protocol. An MCP bridge exposes that tree to any agent harness (Claude Code, OpenCode, Cursor): the harness drives the app through ordinary MCP tools, with no taria-specific code of its own.
Agents today reach TUIs by scraping rendered screens through tmux or headless terminals and guessing at structure. taria works on the other side of the terminal: the app publishes what is on screen semantically, the way accessibility trees transformed GUI automation.
Status: pre-alpha, at version 0.3.2. The vertical slice works end to end: a ratatui adapter, an MCP bridge, and a demo app an agent can drive today. The wire format is frozen at
PROTOCOL_VERSION1. SeeCHANGELOG.mdfor what this release changed and what the freeze promises,docs/comparison.mdfor how this differs from tmux, ht and the other ways agents reach a TUI,docs/architecture.mdfor how the pieces fit,docs/protocol.mdfor the wire format itself, anddocs/integration-guide.mdfor adding taria to an app you already have.
Install
Two sides, two different things to install.
Writing a TUI app that agents should be able to use. One dependency:
It re-exports the protocol crate, so taria::{Action, IdSpace, Node, Role}
is reachable as taria_ratatui::taria::{...} without a second dependency.
docs/integration-guide.md walks through the retrofit.
Driving an app that already speaks taria. Install the bridge binary:
Or take a prebuilt tarball from the
releases page: a tag builds
taria-mcp for x86_64 and aarch64 Linux and for both macOS architectures,
each with its sha256. Then point your harness at it, with the app's label:
The quick start below uses the demo app in this repository instead, which needs nothing installed.
Quick start
Prerequisites: a Rust toolchain (1.88 or newer) and, for step 2, the
claude CLI. Three steps, two terminals.
-
Run the demo app in one terminal. It prints its taria socket path on startup and then behaves like a normal task-manager TUI:
-
Register the bridge with your agent harness. Build it first, then for Claude Code:
Working inside this repo, you can skip
claude mcp add: the committed.mcp.jsonregisters the same server, and Claude Code picks it up here once you approve it. -
Ask the agent to drive the app: "read the tree, add a task called ship v0, mark it done, then delete it". The agent works through the
read_tree,act,type_text, andkeytools while the TUI reacts in the first terminal.
Running the demo headless
Backgrounding the demo with plain & fails. Give it a terminal with tmux
instead:
How it works
The app publishes a semantic snapshot of its widget tree over a Unix domain socket, one JSON object per line (ndjson), on every meaningful change. The bridge holds the latest snapshot and exposes it, plus input back into the app, as MCP tools. Agent input reaches the app's event loop like any other input source, and the app acknowledges each input by id, so the bridge can tell an input the app acted on from one it never saw. The bridge connects on its first tool call and lets the app go after 60 s with no tool calls by default, so an idle agent session does not keep the app from another.
+---------------+ Unix socket +-----------+ MCP over stdio +---------------+
| TUI app | <------------> | taria-mcp | <--------------> | agent harness |
| taria-ratatui | (ndjson) | bridge | | (Claude Code) |
+---------------+ +-----------+ +---------------+
What an app can say about a widget is a fixed vocabulary: 29 roles (list,
tree, table, text_input, select, dialog, log, chart,
scrollbar, terminal and the rest) and 7 actions (activate, focus,
select, toggle, scroll, set_value, dismiss), plus a custom action
for anything an app names itself. Both vocabularies are open: a peer that
meets a role or an action it does not know degrades that one field instead of
failing the tree, which is what lets either grow inside a frozen format.
MCP tools
| Tool | What it does |
|---|---|
read_tree |
Returns the app's current semantic tree as JSON: node ids, roles, labels, values, focus, and the actions each node advertises. The app's own snapshot, relayed, so a role or field this bridge has never heard of arrives under its real name. With --tree-format outline, the same snapshot comes as one indented line per node instead, which for the demo's first tree is under half the characters. |
act |
Invokes an advertised action on a node by id, with an optional value (e.g. for set_value), up to 4096 characters. The node id and the action are checked against the latest tree before anything is sent. |
key |
Sends a raw key press ("q", "enter", "ctrl+c"), up to 64 times with repeat. A key that does not match the grammar is rejected here rather than swallowed by the app. A fallback for parts of the UI without semantic coverage. |
type_text |
Types a literal string in one call instead of one key call per character, up to 4096 characters. It goes where the app puts typing, never through the app's key bindings, so move the keyboard to the target first: act on its advertised focus action, or on set_value, which many apps focus as a side effect. An app accepting no typing reports it ignored rather than acting on the characters. |
The three input tools wait up to 500 ms for the app's answer and report what
actually happened: the updated tree, an input the app deliberately ignored
(with the current tree to re-plan from), an input the app acknowledged and did
not survive (an advertised quit, working), or an error for an input that was
dropped, never applied, or left unaccounted for by an app that went away
before acknowledging it.
taria-mcp CLI
The bridge takes exactly one of --socket <path> or --app <label>. With
--app, it resolves the socket path the same way the app-side adapter does
when binding. --tree-format outline serves the tree as an indented outline
instead of JSON. --idle-release <SECS> sets how long the bridge waits with
no tool calls before it lets the app go: 60 by default, 0 for never. The
flags, the environment variables, the resolution order, the rule on labels
and the outline's grammar: docs/cli.md.
Workspace layout
crates/taria Core protocol types (widget tree, actions, snapshots)
crates/taria-ratatui Ratatui adapter: publish semantics alongside rendering
crates/taria-mcp MCP bridge binary for agent harnesses
examples/demo-app Demo ratatui app driven by an agent through taria
scripts/ Python verification harnesses (e2e, adversarial)
docs/ Protocol spec, architecture, integration guide,
comparison, landscape research
In docs/: protocol.md specifies the wire format message by message,
architecture.md explains why it is shaped that way, and landscape.md is
the research behind both.
Development
CONTRIBUTING.md covers the rest: the setup, the CI gate, the commit style, and what a change has to clear while version 1 of the wire format is frozen.
Compatibility
See docs/compatibility.md for the full list. The three that decide whether taria fits at all:
- Unix only for now: the adapter is built on unix-only APIs, and Linux is the tested platform. Limitations.
- One bridge client per app at a time: the adapter serves one, so two agents cannot drive the same app at once. They can take turns: an idle bridge lets the app go after 60 s by default. Limitations.
- The wire format is frozen at
PROTOCOL_VERSION1: changes within version 1 are additive, so a version difference costs a degraded field rather than the whole tree. What the freeze promises.
How taria compares
FAQ
License
Dual-licensed under MIT or Apache-2.0, at your option.