Home › Blog › The actor hierarchy: workspace → tab → lane → group → pane → agent
Blog

The actor hierarchy: workspace → tab → lane → group → pane → agent

Jul 19, 20266 min readBy the Rysh team

rysh's UI tree is an actor tree. One owner per piece of state — and a routing shortcut that cut message hops from 5 to 2.

Every process has an org chart, whether you draw it or not. In most codebases it's implicit — this goroutine kind of owns that map, this package sort of manages those handles. In rysh we drew it, and then we made the compiler-adjacent thing enforce it: the org chart is the actor tree.

rysh is an agentic terminal multiplexer — every pane is a conversation that can be a shell, an autonomous agent (by default running on Claude, with your own Anthropic key), a chat, or an external channel. The UI you see — tabs containing columns of stacked panes — maps one-to-one onto a supervision tree of actors, each with its own NATS inbox and its own exclusively-owned state.

The tree

WorkspaceActor                       {session}.ws.inbox
├── TabActor (per tab)               {session}.tab.{tabID}.inbox
│   └── LaneActor (per column)       {session}.lane.{laneID}.inbox
│       └── PaneGroupActor           {session}.pane-group.{groupID}.inbox
│           └── PaneActor            {session}.pane.{paneID}.inbox
│               ├── LLMPromptExecutionActor   (agent tool-use loops)
│               ├── RemoteUpstreamActor       (##pane share stream)
│               └── listener actor            (##pane listen)
├── ShareRegistryActor → UpstreamShareActor (per active share)
├── AgentRegistryActor → AgentActor (per autonomous agent)
└── HumanoidRegistryActor → HumanoidActor (per external-channel agent)

Each level owns exactly what its name says:

And to the side, the registries: autonomous agents and humanoids are headless — no PTY, no UI presence — so they hang off the workspace through their own registry actors rather than living in the visual tree. Same engine underneath (each spawns its own LLMPromptExecutionActor), different parent.

Why mirror the UI?

Because the hard question in any stateful system is "who is allowed to touch this?" — and mirroring makes the answer visual. Where does column flex live? Look at the screen: columns are a tab-level arrangement → TabActor. Who knows which pane in a stack is on top? The group → PaneGroupActor. Who owns scrollback? The pane. You can navigate the codebase by looking at the UI.

It also makes lifecycle mechanical. Close a tab and the workspace stops one actor: supervision takes it through the lanes and groups, each group stops the panes it holds (panes are spawned detached so ##move can carry one to another group), and each pane stops its children and kills its shell. Spawning is the same in reverse: ##agent spawn sends the registry one message, and the actor it spawns subscribes to its own subject and spawns its own LLM child.

And because every actor processes its mailbox sequentially, ownership isn't just documentation — it's a concurrency guarantee. One owner, one queue, so the state it owns needs no locks. (The full argument is in the proto.actor + NATS article.)

The routing lesson: hierarchy is for structure, not for transit

Routed through the full tree, a line typed into a pane would hop TUI → workspace → tab → lane → group → pane: five hops, four of them actors shrugging and forwarding.

That's the classic hierarchy tax. The fix is to sort messages by what they actually touch:

Structural messages (create, close, focus, resize, layout) travel down the tree as far as the state they change — legitimately, because each actor on the way updates its own layout state. The hops do work.

Pass-through messages (input submission, rename, listener start/stop, ##pane share start/stop) skip the middle entirely. The workspace does its one real job — parsing ## system commands and @agent routing — then sends straight to {session}.pane.{paneID}.inbox. Five hops become two.

Raw keystrokes (vim, htop — interactive raw mode) go further: for a local pane the TUI publishes straight to the pane's raw-input subject, where a subscription writes them to the PTY. No actor at all. The workspace never sees them.

Navigation messages share one Direction type (next, prev, left, right, up, down) rather than a message type per direction — less codec surface, though a few per-direction types remain.

The principle we ended with: a message should visit exactly the actors whose state it changes. The hierarchy earns its hops on structural traffic and gets bypassed everywhere else.

Trade-offs, honestly

If you take one thing: draw your process's org chart before the code makes one up for you. Ours happens to look exactly like the screen — which means anyone who has used rysh for five minutes already knows the architecture.


rysh is in early access and we're recruiting design partners — teams who want agents in their real workflow and will tell us what breaks. Direct line to the founders: rysh.ai/design-partner.

More in this series: Control plane vs data plane · Snapshot-driven rendering

Try Rysh

Every pane is a shell and an AI agent — install takes one command.

Get started free →