DEV Community

Cover image for Terminal UIs for Harnesses in 2026: who builds on what, and why
Arthur Katcher
Arthur Katcher

Posted on Originally published at arthurkatcher.com

Terminal UIs for Harnesses in 2026: who builds on what, and why

We spent forty years escaping the command line. Then AI agents arrived, and the best-funded interface teams in software went back to a grid of monospaced characters. So what's happened?

The terminal came back beautiful

In 1984 the Macintosh promised we would never look at a blinking cursor again. In 2026, OpenAI, Anthropic, Google, and xAI all ship their flagship coding products as terminal apps. Not grudgingly, either. These are polished, animated, mouse-aware interfaces that happen to be drawn entirely out of text. Something strange happened on the way to the future, and it is worth understanding what.

A screen made of cells

Diagram titled How a terminal actually works: a stream of bytes with escape codes is interpreted into a grid of fixed-width cells, where colors come from the codes, wide characters take two cells, and the cursor is one cell painted in reverse.

A terminal gives a program almost nothing. There is no canvas, no DOM, no pixels. There is a grid of monospaced character cells, and two streams of bytes: one going out, one coming in. Print text and it simply fills the next cells in the grid.

Everything else is a trick. The trick is called an escape sequence: a run of bytes starting with the escape character that the terminal reads as a command instead of text. \x1b[5;10H moves the cursor to row 5, column 10. \x1b[31m turns the next characters red. Other sequences clear lines, hide the cursor, or make mouse clicks arrive on the input stream as encoded bytes. An app drawing itself is just a program printing instructions; those are the boxed bytes in fig 01.

A full-screen app adds two more moves. It switches the input to raw mode, so each keystroke arrives instantly instead of waiting for Enter; that is why a single j moves the cursor in vim. And it opens the alternate screen buffer, a second blank screen to draw on. Quit vim and your shell history is exactly where you left it. That is the alternate screen closing.

So every TUI framework ever written has the same job. Hold a model of the grid in memory. Turn your interface into cells. Compare that with what is already on screen, and emit the fewest escape sequences that make the two match. It is the same paint, diff, and flush loop as a game engine, or as React against the DOM. The whole craft lives in that loop, and in one cursed detail: a Japanese character is two cells wide, an emoji is anyone's guess, and every miscount shows up as a broken border.

Why we left, and what pulled us back

We left because the terminal of 1985 deserved to be left. Sixteen colors on a good day. No mouse. No images. The GUI won on fidelity, then the web won on reach, and the terminal became the boiler room: always there, never decorated.

Timeline titled Where the interface investment went: terminal era from the 1978 VT100, the GUI era and the web from the 1984 Macintosh, and the return with GPU terminals in 2022 and AI coding agents in 2025-26.

What pulled us back was not nostalgia. It was AI agents, and three unsentimental facts about where their work happens.

First, the terminal is the one interface that already exists everywhere an agent works: every server, every SSH session, every CI box. Second, it composes. Pipes, logs, and replayable event journals are native citizens, and an agent's output is exactly that kind of material. Third, it is cheap to render. A status row updating thirty times a second costs nothing, which matters when the interesting part of your product is the model, not the chrome.

A chat web app shows you what an agent says. The terminal is where the agent's actual work already lives. Putting the interface there removes a whole layer of translation.

The hardware quietly got good

The return would have looked terrible on the terminal we abandoned. It looks good now because the substrate improved while nobody was watching.

Color came first. Two shades became sixteen, then 256, then 24-bit truecolor: 16.7 million colors per cell, standard in every serious emulator today.

Step chart titled Colors one terminal cell can show: 2 in 1978, 16 in the 1980s, 256 in 1999, 16.7 million truecolor in the 2010s.

Then came the protocols that fixed the old embarrassments. Synchronized output lets an app wrap a whole frame in a pair of escape sequences so the terminal paints it atomically. No flicker, no tearing. Support landed in VS Code's terminal and tmux this year, work driven in part by Claude Code's anti-flicker push. The kitty graphics protocol transmits real pixels, and Ghostty, WezTerm, Konsole, iTerm2, and Warp all speak it. Where it is missing, there is a lovely fallback: the half-block character ▀, with the foreground color as the top pixel and the background as the bottom. Two pixels per cell, on any terminal made since the Reagan administration.

Diagram titled Two pixels per character cell: the half-block glyph draws two image pixels in one cell, foreground for the top pixel and background for the bottom, the image fallback used by chafa, viu, timg, ratatui-image and OpenTUI.

Add GPU-rendered emulators like Ghostty, and beauty stopped being impossible. It just needed a reason. Millions of developers staring at agent harnesses all day turned out to be the reason.

What the AI coding tools run on

Look at what the major coding agents actually ship, and the field splits into camps.

Bar chart titled AI coding CLIs by TUI stack, August 2026: Ink and React family 5, Rust and ratatui 2, plain REPL 2, OpenTUI and Solid 1, Go and Bubble Tea 1.

Tool Stack Notes
Claude Code TS, React, forked Ink custom reconciler, 140+ components
Codex CLI Rust + ratatui rewrote from TS + Ink in 2025
Gemini CLI TS, React 19 + forked Ink open source
Grok Build Rust + ratatui open-sourced July 2026
opencode OpenTUI + SolidJS was Go + Bubble Tea
Crush Go + Bubble Tea v2 the original opencode lineage
Aider prompt_toolkit + Rich a REPL by choice

Half that chart says Ink, so Ink deserves a proper introduction. Ink is a renderer that points React at the terminal instead of the browser. You write ordinary React components with hooks and state, lay them out with flexbox through Yoga, the same layout engine React Native uses, and Ink turns the tree into styled lines of text, re-printing what changed. Its pull is simple: millions of developers already think in React, and Ink lets them keep thinking in it. Its limits are just as simple: it is pure JavaScript, line-oriented, capped around 30 frames per second, with no mouse support and no images.

Those limits explain the table's real story. The biggest Ink users all ended up forking Ink rather than using it stock; Claude Code's build carries a custom React reconciler and its own layout engine. And OpenAI threw away a working TypeScript app to rewrite Codex in Rust on ratatui. When teams this well resourced keep rebuilding the same layer by hand, the layer is missing from the ecosystem.

OpenTUI is the most direct answer to that gap, and its origin is the point. The team behind opencode (Anomaly, formerly the SST crew) started with a Go interface built on Bubble Tea, but wanted the whole product in TypeScript. Nothing in the JS world could draw fast enough, so they built their own framework and rebuilt opencode on it. OpenTUI keeps React's programming model but replaces Ink's machinery: a native rendering core written in Zig holds the screen as a buffer of cells, diffs each frame, and repaints only what changed. That buys the things Ink cannot do: no frame cap, full mouse support, real images over the kitty graphics protocol with half-block fallback. It is MIT-licensed, documented at opentui.com, runs opencode in production today, and is the closest thing the TypeScript terminal world has to a common foundation forming in real time.

The other camps are healthy too. Charm shipped Bubble Tea v2 in February, hardened inside Crush. Rust has ratatui, and Python has Textual. Nobody is starving; the fight is over which model wins the next decade of terminal software.

The parts everyone rebuilds

Here is the gap all those forks point at. Chat components exist in every ecosystem: message bubbles, spinners, markdown renderers. But a harness is not a chat. A harness needs a status row that proves the agent is alive and tells you what this turn costs. An approval prompt that gates a real action on your real machine, with the safe option first. A home screen that says which model is pointed at which screen. Nobody ships those, so every team on that chart has built them from scratch, and several built them wrong in the same off-by-one-cell ways.

A copy-paste culture is forming around the problem, in the mold of shadcn/ui: registries like termcn, TermUI, and InkUI distribute terminal component source you own and edit. Vercel's @ai-sdk/tui attacks the same space from the SDK side.


I got tired of rebuilding the harness parts too, so I decided to open source the parts I use. **agentparts* is a small library of exactly these components, built on OpenTUI: the turn status row, the approval prompt, the shell layout, the prompt box. All the width arithmetic lives in a renderer-free core that runs on plain Node. Feel free to use for your harnesses!*

Screenshot of AgentParts, terminal components for agent harnesses: model and policy info, a list of recent agent sessions, and a prompt box with key hints.

>> https://github.com/arthurkatcher/agentparts


So you want to build one

Start with an honest question: does this need to be full-screen at all? Aider and Goose are proof that a well-formatted REPL can carry a serious product. If your interface is a conversation with occasional structure, print good output and stop. Reach for a TUI framework when you need layout: panes, status rows, things that update in place.

Decision tree titled Choosing a TUI stack: if it doesn't need the whole screen, a REPL with good output; if it does, a full-screen framework by language: Ink or OpenTUI for TypeScript, ratatui for Rust, Bubble Tea v2 for Go, Textual for Python.

If you do need one, the choice is mostly made by the language you already live in:

You write Reach for Why
TypeScript Ink, or OpenTUI Ink is mature; OpenTUI adds speed, mouse, images
Rust ratatui + crossterm the default; start from ratatui/templates
Go Bubble Tea v2 + Bubbles best component library, Lip Gloss for styling
Python Textual CSS-like styling, rich widget gallery

The beautiful part is not the framework. It is discipline about the medium:

Design a palette, then design its absence. Pick a handful of truecolor values on purpose, and decide what the interface looks like with color stripped away: plainly unstyled, never broken. Honor NO_COLOR, the environment variable users set when they want none. Get width right. Test with CJK text and emoji, and snapshot your layout at 40, 80, and 132 columns, because every TUI bug is an off-by-one-cell bug. Never flicker. Wrap each frame in synchronized output, the escape pair that tells the terminal to paint it in one go, and diff before you draw. Shed, don't squish. When the window gets narrow, drop the least important element entirely instead of truncating everything a little.

And steal the parts that already exist: the spinners, the status rows, the prompts. Bubbles has them in Go, the registries have them in TypeScript. The chrome is the least differentiated thing you will build, which is exactly why you should not build it twice.

Step back far enough and the cycle stops looking like a cycle. We did not return to the terminal we left. We brought everything back with us: React reconcilers running flexbox, CSS-like styling, component registries, GPU rendering, twenty years of interface craft applied to a character grid. The real trade was never terminal versus GUI. It is fidelity versus leverage, how rich an interface can be against how many places it can reach, and right now leverage is winning.

Good design finally reached the terminal, one escape sequence at a time.


Originally published on arthurkatcher.com.

Top comments (0)