Learn
Live Terminal Streaming for AI Agents
Why streaming a real PTY beats polling log files when you run AI coding agents, what a pseudo-terminal gives you that a pipe does not, and how to keep sixteen panes fast.
July 8, 2026
If you run an AI coding agent from a script and read its output from a log file, you are polling. You find out an agent asked you a question some number of seconds after it asked, you lose the colors and the progress bars, and any agent that wanted a real terminal behaves differently because it did not get one. Streaming a pseudo-terminal fixes all three problems at once.
This guide explains what a PTY gives you that a pipe does not, why attention should be event-driven rather than polled, and what it costs to render many live terminals at once. The numbers at the end are ours, measured, with the method stated.
A pipe is not a terminal
When you launch a process with its output redirected to a file or a pipe, the process asks the operating system whether it is attached to a terminal, hears no, and adapts. It disables color. It stops drawing progress bars. It may switch to a non-interactive mode and refuse to prompt you at all. Many agent CLIs behave meaningfully differently under a pipe, which means the thing you are observing is not the thing your users run.
A pseudo-terminal is a kernel device pair that makes a process believe it is talking to a real terminal. It gets a TTY, so it keeps its colors, its cursor movements, its prompts, and its interactive behavior. You get the raw byte stream, control sequences included, exactly as the process emitted it.
- Colors, cursor addressing, and progress rendering survive
- Interactive prompts work, so an agent can ask and you can answer
- Window size is negotiated, so tools wrap and paginate correctly
- Control keys reach the process, so you can interrupt a runaway agent
Workspace runs every agent as a real PTY subprocess for exactly these reasons. The agent runs the way it runs in your own shell, because it is running in a shell.
Stream the bytes, do not poll the file
Once you have a PTY you get bytes as the process writes them. There is no interval to tune and no window in which you are blind. The distinction stops being academic the moment an agent stops and waits for you: with polling, the cost of noticing is bounded below by your poll interval, and with streaming, the cost is one round trip through the kernel.
The same argument applies one layer up. Once the terminal is streaming, the surface that tells you an agent needs attention should be event-driven too. Agent CLIs can emit an OSC escape sequence to say they are waiting on a human. That sequence arrives in the byte stream you are already reading, so the pane can change state, the workspace can raise a badge, the operating system dock can mark itself, and a notification can point at the pane that asked. Nothing polls, and nothing sleeps between checks.
This is what makes a grid of agents supervisable. With one agent, you watch it. With sixteen, you cannot, so the grid has to interrupt you instead. When a swarm is running, that signal is the difference between coordinating and babysitting, which is why it matters as much as the role and merge-gate machinery.
Keep the agents alive when the UI is not
An agent halfway through a refactor should not die because you restarted the window that was drawing it. The way to guarantee that is to put the pseudo-terminals in a separate, detached process from the interface. The UI attaches to that process and reads the stream; when the UI goes away, the agents keep running, and when it comes back it re-attaches and repaints the scrollback.
Workspace runs its PTYs in a detached daemon for this reason, and persists sessions so workspaces survive restarts. The practical rule is that the renderer is disposable and the agent process is not.
The render budget for sixteen live panes
Sixteen terminals streaming full-width output at once is a real rendering problem. Every pane is emitting bytes, every byte can move a cursor, and a naive implementation will drop frames long before it drops output. Two numbers decide whether the interface stays usable: the average frame rate, and the worst gap between two frames. Average frame rate tells you it looks smooth. The worst gap tells you whether it ever froze, and the worst gap is the number that users actually feel.
Here is what we measure, and how. A stress harness opens sixteen live panes, drives full-viewport scrolling output into every one of them at once, samples every frame gap, and flips panes into the needs-you state end to end while the torrent is running. At the v0.4.0 release audit that produced roughly 140 fps with all 16 panes streaming, a 28 ms worst frame gap under full load, 38 MB of renderer memory at 16 agents, and 0 frames past 100 ms.
Those are the numbers from one audited release on one desktop machine, and single measurements on single machines are the least interesting thing about a performance story. The durable claim is the budget the same harness asserts, nightly, on Windows, macOS, and Linux. Memory and correctness hold strictly everywhere: a cap of 300 MB of renderer memory, and at least twelve of the sixteen visible panes rendering on the GPU. The two frame-timing thresholds, a floor of 30 fps and a cap of 150 ms on the worst gap, hold strictly on desktop hardware. Continuous integration runners have no GPU and rasterize in software, so those two are relaxed there, and the log says so on every run.
- heap.renderer, cap 300 MB, strict everywhere
- panes.webgl, at least 12 of the 16 visible panes on the GPU, strict everywhere
- fps.avg, floor 30 fps, strict on desktop hardware
- frame.gap.max, cap 150 ms, strict on desktop hardware
A sweep that misses its budget fails, and the failure names the number it missed. That is the property worth copying. A number in a README rots quietly; a threshold in a job that runs while you sleep does not.
If you are building your own terminal surface, assert the budget rather than the measurement. Hardware changes, machines get busy, and a number you recorded once will drift. A gate that fails the build when the worst frame gap crosses a line will keep telling you the truth.
What to measure in your own setup
Measure the worst frame gap while the system is under the load you actually care about, which for agents means many panes emitting at once rather than one pane emitting a lot. Measure the time from an agent's attention signal to the pixel that tells you about it. Measure memory after an hour, not after a minute, because terminal scrollback is where memory goes to hide.
Then write the thresholds into CI, so the numbers stay honest without anyone remembering to check them. Running many agents at once also has a cost that has nothing to do with rendering, and it arrives as a provider limit rather than a dropped frame. That is covered in usage limits and profile failover.
The grid, its layouts, and its keyboard navigation are documented in the multi-pane grid docs. The wider category this guide belongs to is described on the AI agent command center.
