Solo terminal vs xterm.js
xterm.js puts the whole terminal in the browser. Solo's core keeps it next to the process and sends the screen as patches. Here's when each one is the right call.
Official product sites
xterm.js is the standard way to put a terminal in a web page. It's mature, MIT-licensed, and it's the terminal inside VS Code, JupyterLab, and Azure Cloud Shell. It's built to be the whole terminal on the client: the parser, the buffer, and the renderer all run in the page, and the client receives raw PTY bytes.
Solo's terminal splits that in two. A Rust core runs next to the process, parses everything, owns the screen and the scrollback, and sends each viewer snapshots and small patches. The renderer only paints cells.
If you need one terminal in one tab, use xterm.js. If you need a session that runs for hours and shows up correctly on several screens, the split is the whole point.
Who should choose each option in Solo terminal vs xterm.js?
Start with where your sessions run and how many screens need to see them.
Choose xterm.js if...
You want a terminal in the browser today. One user, one session, one tab: SSH in a page, a notebook terminal, a cloud shell, an Electron app. You want image support, a big ecosystem, and code you can read and fork for free.
Choose Solo's core if...
Your sessions run for hours, get watched from more than one place, and can't corrupt when a client reloads or a process restarts. You'd rather license that delivery layer than build patching, backpressure, and reattach yourself.
Feature comparison
| Feature | Solo terminal | xterm.js |
|---|---|---|
| Where parsing happens | In the Rust core, next to the process | In the page, on the main thread. @xterm/headless can also run in Node (experimental) |
| What the client receives | Snapshots, row and cell patches, and history deltas | Raw PTY bytes |
| Several viewers on one session | One core, with separate sequence, queue, and credit per attach | Each client gets the bytes and parses its own copy |
| Flow control | Built in: exact-receipt credit, 8 messages or 1 MiB in flight per viewer | Yours to build with write callbacks and watermarks. Data past the 50 MB write buffer is discarded |
| Reconnect and restore | Resume from a 256-patch backlog, or a fresh snapshot | Serialize addon (experimental) replays the buffer as escape sequences |
| Stale data after a restart | Rejected by run, history, and screen fences | Up to your protocol |
| Scrollback | 10,000 lines in the core; viewers page in what they scroll to | 1,000 lines by default, held in the client |
| Renderers | WebGL2 with a glyph atlas, Canvas 2D fallback | DOM (default) and WebGL2 addon. Canvas renderer removed in 6.0 |
| Synchronized output (2026) | ||
| Kitty keyboard protocol | 6.1 beta only, opt-in | |
| OSC 8 hyperlinks | ||
| OSC 52 clipboard | Copy only | Clipboard addon |
| Images (Sixel, iTerm, Kitty graphics) | — | Image addon: Sixel and iTerm. Kitty graphics in beta |
| Ecosystem | Used in one product: Solo | A dozen-plus official addons, used by VS Code, JupyterLab, Replit, and more |
| License | Commercial, by conversation. Not packaged as a library yet | MIT, on npm |
Where the terminal lives
The terminal lives with the process. The Rust core reads the PTY, runs every byte through its parser, and keeps the grid, cursor, modes, and scrollback. That's the only copy of the truth.
A viewer is a renderer. It receives structured operations (replace this row, replace these cells, insert rows, move the cursor) and paints them. It never sees an escape sequence, so you can throw it away, reload it, or open three of them without anything drifting.
The terminal lives in the page. xterm.js parses the bytes, keeps the buffer, and renders it, all in JavaScript on the main thread. A request to move parts of it into a web worker was closed as out of scope for now.
For a terminal that belongs to one tab, that's simple and exactly right. You hand it bytes and it does everything else.
Streaming one session to several screens
Every attach to a terminal gets its own sequence numbers, delivery queue, and credit. The core diffs each frame into patches, coalesces them to about 60 per second per viewer, and sends each viewer only what it hasn't seen.
Frame patches are binary MessagePack with a deduplicated style table. A slow viewer falls behind on its own, and the others don't notice.
Each client is its own terminal. To show one session in two places, you send the same raw bytes to both, and each parses and buffers its own copy.
That works when every client is online from the start. A client that joins late needs the history first, which is where headless and serialize come in.
Flow control and floods
Delivery runs on exact receipts. Every body carries a random 128-bit token, and the viewer sends it back as soon as the body arrives. Each viewer gets 8 messages or 1 MiB of frame and history data in flight. Credit comes back only for receipts that match an exact body, never for a timeout or a count.
The core never waits on the UI, so a flood can't stall the child process. If a viewer falls too far behind, its queued patches are dropped and it gets a fresh snapshot instead of a backlog.
xterm.js has an official flow control guide, and it's worth reading. A fast program can produce far more than xterm.js can parse, and data past its 50 MB write buffer is discarded.
The guide suggests pausing the PTY with write callbacks and watermarks. Across a network, you carry that accounting yourself with your own ACK messages. The guide also warns that a badly tuned scheme can stall the stream for good.
Reconnects and restarts
When a viewer attaches, it says what it already has: run epoch, history generation, and screen sequence. If the core still holds the missing patches (up to 256), the viewer resumes from where it left off. Otherwise it gets a full snapshot.
Patches from an old run or an old attach are rejected, so restarting a process can't paint stale output on top of new output.
The usual pattern is to keep @xterm/headless on the server and use the serialize addon to write the buffer out as escape sequences, then replay them into a fresh client terminal. VS Code's terminal reconnects this way, so it clearly works at scale.
Both pieces are marked experimental. Serialize is a full-state replay rather than a diff, and it recommends restoring into a terminal of the same size. Ordering and staleness after restarts are up to your protocol.
Very long sessions
Scrollback belongs to the core: 10,000 lines in compact 256-line pages. A viewer gets a 400-line tail on attach, holds about 2,000 lines around where you're looking, and fetches older pages on demand.
So an agent that's been running all day costs a new viewer about the same to open as one that started a minute ago.
Scrollback lives in the client buffer, and the default is 1,000 lines. VS Code's own setting notes that memory is allocated based on this value, so a bigger number costs more memory in every client.
For a session you open, use, and close, that's rarely a problem.
Rendering
Solo's renderer draws with WebGL2 from a glyph atlas, with box-drawing characters drawn procedurally, and uploads only the rows that changed. It falls back to Canvas 2D if WebGL2 setup fails, the context is lost, or Linux is software rendering. Theme and font changes apply in place.
Big frames decode in a web worker before they reach the renderer.
xterm.js 6 ships a DOM renderer by default and a WebGL2 renderer as an addon. The canvas renderer was removed in 6.0. An experimental WebGPU renderer is in review but not released.
The WebGL addon is the fast path, and it's mature.
We used xterm.js first
We have used both. Solo moved to xterm.js v6 on February 10, 2026, and it removed about 1,000 lines of workarounds we'd written for our previous terminal. For a while it was great.
The trouble was the shape of the app. Solo keeps many long-running agents and dev servers alive, and you flip between them all day. With the terminal in the page, every switch or reattach meant replaying output into an xterm.js instance, heavy output caused flashing and repaint churn, and the webview owned the only correct copy of the screen.
On March 28, 2026, we moved terminal state into Rust and removed xterm.js completely. That commit was 5,303 lines lighter. Our test suite still imports escape-sequence cases from the xterm.js repo, because they're good.
Common questions
Is Solo's terminal a drop-in replacement for xterm.js?
Can xterm.js do server-side terminal state?
@xterm/headless runs in Node and keeps terminal state on the server, and the serialize addon can replay it into a client on reconnect. VS Code does this. Both are marked experimental, and live output still goes to each client as raw bytes.Is xterm.js slow?
Can I use Solo's terminal in my product?
What about libghostty?
Does Solo's terminal support images?
xterm.js is the right answer for most web terminals, and we'd tell you so. It's free, proven, and does one terminal in one tab extremely well.
Solo's core is for the case where that model breaks: sessions that run for hours, more than one screen, and clients that come and go. That's the problem we built it to solve inside Solo, and it's the part we're offering to license.
Sources reviewed: 2026-10-24
Need the terminal to outlive the tab?
Solo's terminal core is available to license for agent interfaces, remote dev tools, and control panels for long-running processes.



