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

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

Jul 19, 20265 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 (running on Claude, your own 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                       rysh.ws.{session}.inbox
├── TabActor (per tab)               rysh.tab.{tabID}.inbox
│   └── LaneActor (per column)       rysh.lane.{laneID}.inbox
│       └── PaneGroupActor           rysh.pane-group.{groupID}.inbox
│           └── PaneActor            rysh.pane.{paneID}.inbox
│               ├── LLMPromptExecutionActor   (agent tool-use loops)
│               ├── SharedOutputActor         (redaction + shared stream)
│               └── listener actors           (##pane listen, remote shares)
├── 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. Actors form a supervision tree: close a tab and the tab's subtree — lanes, groups, panes, each pane's agent and sharing children — is torn down by the framework, not by hand-written cleanup that forgets things. Spawning is the same in reverse: ##agent spawn materializes an actor with children, addressable at its own subject, in one operation.

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

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

Early on, every message walked the tree. Type a line into a pane and it hopped TUI → workspace → tab → lane → group → pane: five hops, four of which were actors shrugging and forwarding.

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

Structural messages (create, close, focus, resize, layout) still traverse the full path — legitimately, because every intermediate actor updates its own layout state along the way. The hops do work.

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

Raw keystrokes (vim, htop — interactive raw mode) go further: the TUI publishes directly to the pane. One hop. The workspace never sees them.

The same cleanup pass consolidated a family of per-direction message types (focus-left, focus-right, next, prev, up, down…) into a single navigation message with a Direction enum — less codec surface, fewer types to register on the bus.

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 →