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, plus a private buffer backing ##private for output that must never reach any shared stream. 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. Sharing becomes precise. When a pane is shared — locally via ##pane listen, or to a remote collaborator via ##share pane — each stream is forwarded separately. A teammate reviewing your agent's reasoning can follow the AI stream without your shell noise. A dashboard can consume statuses without transcript text. You share a stream, not your 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.
4. Governance gets a seam. Every pane has a shared-output actor that subscribes to the pane's output, applies secret redaction, and republishes to a .sharedOutput topic — and that is the only stream other panes or remote subscribers ever see. The private/shared boundary is a topic boundary, enforced in one place, not a rendering convention scattered through UI code.
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, the KV persister, the redacting share actor, 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.
The data plane stays fast. Output bypasses actor mailboxes entirely — PTY reads and LLM responses publish directly to topics, so a firehose build log can't clog the control plane where structural commands (create pane, resize, focus) are processed in order.
The unglamorous conclusion
Nobody buys a multiplexer for its output topology. But almost everything people do buy rysh for — precise sharing, panes that watch panes, redaction before anything leaves the machine, transcripts that survive reboots — 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.
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