A terminal client for AI agents has to do four things: put the conversation, the agent’s plan, its shell output and its file edits in one scrollback; stop a risky call with a question you can answer inline; let you address files without typing paths; and show which model and session you are in. The one thing it must not do is own the rules. I learned that by building the same client three times, and by drawing the wrong conclusion the first time.
Build one: beam, the terminal client
I called it beam. It was a full-screen terminal client for agent sessions. Chat, plan, shell output and file edits scrolled in one place. A gated tool call stopped at an approval card inside the scrollback. A command palette completed arguments, @ addressed files, and a status bar carried the live model, session and context pressure.

A chat with an agent in a project folder: the agent reads the files, a gated write stops at the permission card, one click allows it.
What it proved, after the fact: almost none of beam knew which agent it was talking to. It had become a client for the Agent Client Protocol without anyone designing it as one. Its command palette filled its choices from what the agent already advertised, and nothing had to be added to the protocol for that.
Build two: the same model in a browser
The web UI came from beam’s model, not from a fresh design. It shipped inside the single Go binary and was served on 127.0.0.1, so there was nothing to host and no separate frontend to deploy. Each tool call was its own card. Before a gated write or shell command ran, you saw an inline diff and approved or rejected it in place.
That card pattern is the one I kept. The diff in front of the question is what makes the question answerable.
Build three: a desktop shell that deleted HTTP
Beam Desktop was an Electron shell, and it turned the relationship around. The window loaded from disk. The shell spawned the engine as a child process and spoke to it over stdio. No port, no origin, no cookie, no handshake: removing HTTP from the loading path removed that whole class of question instead of answering it better.

The window had no Node access and reached the engine through a bridge with four functions: send, onMessage, onClose, onStderr. One NDJSON pipe, one JSON-RPC message per line, carried ACP and a private family of editor methods (workspace roots, git status and diff, search, terminals, model catalogs) without forking either. I checked it against the real binary over one connection: ACP initialize at protocol version 1, an editor autocomplete, a mission list, fifteen commands, and an unknown method answered with -32601.
The shell itself I threw away. It was about 42,800 lines of TypeScript, generated in roughly a day. That is past the point where I could review it, and code I cannot review is code I do not own.
The two-homes trap: about 28 editor methods were reachable from the desktop shell and from no other client, while the terminal client reached around ACP into services directly, down to a second, separate file browser. Every capability had two homes and neither was the real one. No work in the shell could fix that. The defect was underneath it.
The finding: a surface must not own policy
Wherever beam enforced a rule itself, the rule held only while beam was running. Open the same session from a browser or an editor and the rule was not there. A boundary that depends on which window you happen to be looking at is not a boundary.
My first write-up of this drew the wrong conclusion. I retired the client and wrote that the best shape for using Contenox is not a terminal. On 2026-08-18 I withdrew that. I had read a defect in where the boundary sat as a verdict on what the surface was. Retiring beam removed that one instance and fixed nothing: the next client would have grown the same reach, because nothing underneath was answering the question.
What answered it was a durable ask. When a call is gated, it becomes a row and a checkpoint before any client renders anything. The verdict is recorded once, by compare-and-swap. The run resumes from the checkpoint once. It does not matter whether the answer came from a terminal, an editor, or a phone three days later. The agent enforces the rule under every client, not the client in front of you.
The same lesson held for features. A slash command belongs in the agent, advertised over the protocol. A client only classifies input. Implement the feature in one client and that client alone has it.
The reversal
With the boundary moved, the reason to have no terminal client was gone. But the terminal was never the destination, and that is the part most “we built a TUI” stories get wrong. It came back for a plainer reason: a terminal is the cheapest surface there is to test. A session is a stream of text in and text out, so a human can check it in a scrollback, a script can diff it, and an AI agent can drive it end to end without a browser or a window. Every other surface I built needed a harness to test; this one was the harness.
So beam came back as a small client written against a contract that had stopped moving, to exercise the engine where it is cheapest to watch. It is not the old client restored, and it was never meant to be where people work.
Then came the October 2026 IDE plan. It would have moved the terminal client’s job to the host editor and deleted beam’s 33,331 lines, because a terminal client had no job next to an IDE. That plan died, for reasons in AI IDE: why I built one, killed it. Contenox today is a desktop app.
Today beam is gone a second time. The desktop app is the surface, and contenox-runtime is a command line for scripts and CI, not a full-screen client. What survived is the card: the diff in front of the question, answered once, enforced under every client. It looks the same in the desktop app as it did in the scrollback.
What I’d build today
If you are writing a terminal client for an agent, in this order:
- Speak ACP and read everything you can from what the agent advertises: commands, session settings, choices. Hard-code nothing you could have been told.
- Make approval a blocking question, never a notification. If the client fails to render it, the agent must stay stopped.
- Put no rule in the client. If deleting your client would loosen a rule, the rule is in the wrong place.
- Render the diff next to the question.
- Keep the client small enough to review. Mine was 33,331 lines, and the generated desktop shell was about 42,800.
What this does not solve
- A terminal is still a wall for people who do not live in one. That is why the surface people work in became a desktop app, and the terminal stayed a place to test.
- I did not measure whether people prefer the terminal client. What I have is what it settled about the architecture, not a usage number.
- The durable ask moves the rule, not the review. Someone still has to read the diff and decide.
Contenox
Contenox is a free, open-source desktop app for coding agents, Windows first, with permission rules you can read per project. It is where these lessons ended up. Download it here.
Related guides
- What is a TUI?: how full-screen terminal programs draw and read keys
- Terminal coding agents: Claude Code, Codex CLI and OpenCode compared
- Agent Client Protocol (ACP): the protocol beam turned out to speak
- AI IDE: why I built one, killed it: the plan that would have deleted beam