Shell, AI, chat, system: why rysh keeps four output streams per pane
One scrollback for everything means shell noise buries AI answers. rysh publishes each pane's output on per-mode NATS topics — and merges on purpose.
A terminal pane traditionally has one output: the scrollback. Everything lands in it — build noise, error text, the thing you actually cared about — one undifferentiated sediment layer you scroll through like an archaeologist.
Add an AI to that pane and the problem compounds. Now the sediment contains four species of output: shell output, agent responses, chat messages, and the multiplexer's own system output. Mash them into one buffer and you can't share one without leaking the others, can't review the AI's work without wading through npm install, can't build anything downstream that consumes just one kind.
Rysh's answer: every pane publishes its output on separate, per-mode streams — and merges them deliberately, not accidentally.
The topology
Each pane owns a small family of topics on the session's embedded NATS bus:
rysh.pane.{id}.output # merged: shell + AI, interleaved by arrival
rysh.pane.{id}.output.shell # PTY output + command echo only
rysh.pane.{id}.output.ai # agent/LLM responses only
rysh.pane.{id}.output.chat # chat messages only
rysh.pane.{id}.output.rysh # ## system-command output only
The publishing rules are deliberate:
- Shell and AI output are dual-published — once to their own topic, once to the merged
.outputtopic. - Chat and rysh output go to their per-mode topics only.
Why the asymmetry? Because shell and AI output are one working narrative — you run a command, ask about the result, the agent runs another command. The merged stream interleaves them by arrival time so the transcript reads like the conversation actually happened: command, output, question, answer. Chat and system chatter are different registers; they'd pollute that narrative, so they stay in their own lanes.
The pane actor maintains matching in-memory buffers — merged, shell, AI, chat, rysh. The TUI displays whichever fits the pane's current mode: the merged transcript for shell/prompt work, the chat buffer in chat mode, system output for ## commands.
History gets the same treatment
The pattern goes all the way down. Command history isn't one list either:
rysh.pane.{id}.history # merged shell + AI history
rysh.pane.{id}.history.shell # shell commands
rysh.pane.{id}.history.ai # prompts
rysh.pane.{id}.history.chat # chat messages
rysh.pane.{id}.history.rysh # system commands
Same dual-publish rule: shell and AI history feed both their own topic and the merged one. Even history entries flow through the bus rather than being appended locally — which sounds like ceremony until you see what it buys.
What this buys you
1. Watching becomes precise. When one pane listens to another with ##pane listen, the listener subscribes to each of the source's per-mode streams separately and republishes them into the matching streams of the listening pane. Switch the listening pane to prompt mode and you follow the source's AI answers without its shell noise. You watch a stream, not a screen.
2. Downstream consumers get clean inputs. A listening pane that triggers on events (see pipeline events) can treat the source's output as structured flow rather than regex-mining a mixed scrollback. Separation at the source beats parsing at the destination, every time.
3. The transcript stays honest. Because merging happens at publish time by arrival order, the merged stream is a record, not a render-time collage. What you scroll back through is the actual sequence of events — reproducible, persistable, shareable as-is. That merged view (plus per-mode buffers) is also what gets persisted in the pane's KV entry, so the narrative survives detach and reattach.
Why NATS topics and not just arrays?
You could implement four buffers as four slices and be done. The reason rysh routes output through a message bus instead:
Fan-out is free. The TUI, a listening pane, an upstream forwarder — all consume the same published stream without the pane knowing or caring who's subscribed. Adding a consumer never touches pane code.
Location stops mattering. A stream consumed by the local renderer and a stream forwarded to a collaborator across the internet are the same stream. Remote observation isn't a bolted-on screen-scraper; it's another subscriber.
Producers stay decoupled. PTY reads and LLM responses are published to topics; the producer doesn't wait on, or even know about, whoever is reading.
The unglamorous conclusion
Nobody buys a multiplexer for its output topology. But almost everything people do buy rysh for — panes that watch panes, an AI view without the shell noise, transcripts that survive reattach — sits on this one decision: output is a set of typed streams, not a pile of bytes.
One scrollback was fine when one thing talked. Four things talk in an agentic pane. Give them four channels.
Correction (2026-09-28): an earlier version of this article said every pane has a shared-output actor that redacts secrets and republishes to a .sharedOutput topic, and that this is the only stream other panes see. That isn't how the code works: a listening pane subscribes to the source's output streams directly, and nothing on that path redacts. It also described ##private as marking output that never reaches a shared stream (##private pane print prints a pane's raw output), and said output bypasses actor mailboxes. Those lines have been removed or corrected.
Rysh is early — we're recruiting design partners to run real workflows on it and shape what we build next. → rysh.ai/design-partner
Related: ##pane listen: when one pane watches another work · Pipeline events