The Agent's UI Is Frontend's Real Home Field: Chat Is Just the Fallback

1. The Pit

Open Cursor, Copilot Chat, ChatGPT, Claude Code, and Windsurf, screenshot each one, crop out the logos, and you can’t tell who’s who — a column of message bubbles on the right, a multi-line input box at the bottom, a hamburger in the top-left corner folding out a history list. Frontend in the agent era seems to have been flattened into a single template.

For a while I thought this was destiny. Model output is uncertain, tasks are open-ended, human-machine dialogue naturally fits a chat window — what else could it possibly look like? Then one day I counted where I actually spend the most time in Claude Code, and it isn’t the chat area at all: it’s the tool permission list in /permissions, the server status page in /mcp, the few project rules in /memory, the permission dialog that pops up before every dispatch. What actually keeps me around was never the bubbles themselves — it’s the ring of “visible and governable” stuff around them.

Building an agent as a chatbot is laziness. The analogy is clearest against Web history: the early Web ran on one sentence — “a page is a document.” Browser = document reader, frontend = typesetter. Then Gmail and Google Maps came along and shattered that consensus: “a page is an application,” and only from that point on did frontend deserve the name. Today every mainstream agent product is still parked at the equivalent of “a page is a document” — “an agent is a chat window.” Documents weren’t wrong, and Gmail wasn’t wrong either; but parking at documents and never moving on — that’s wrong.

This is Extra B of the series “From useEffect to Agent Loop.” The ten mainline posts covered the agent engine — loop, context, tools, prompt, memory, error, permission, subagent, evals, production. The extras cover the periphery: Extra A covered MCP’s old supply-chain debts; this one covers something else — the frontend home field of agent products was never the chat window.

2. The Bridge

One line:

Chat is the lowest-cost UI for a human to build trust with “uncertain output”; once trust is established and the task is clear, the interaction should be replaced by more specialized UI.

Chat is the fallback, not the destination. “Fallback” isn’t an insult — <noscript> in HTML isn’t an insult either; it’s the safety net that still shows you something after JS dies. Chat occupies exactly that position for agents: the model can’t guarantee its output and the user can’t describe the requirement up front, so you start with an interface generic to the extreme — you speak human, I reply in human, and we close the loop on this one round first. It’s the lowest-cost trust protocol.

The problem is that many products treat the fallback as the main interface. Making people keep reading the static notice inside <noscript> while JS is perfectly fine — of course that’s hostile to humans.

Frontend spent the past twenty years doing one thing: splitting “generic containers” into “specialized components.” React abstracted the DOM into a component tree not because the DOM wasn’t enough, but because “I want an atom I can reuse, reason about, and test.” Agents have now reached the same step — the chat window isn’t enough, and the interaction needs to be abstracted into three kinds of specialized components:

        Generic container            Specialized components
Web:    <div contenteditable>   →    <Editor> <Form> <Table> ...
Agent:  <ChatBubbleList>        →    <ToolPanel> <TracePanel> <DecisionGate>

I didn’t invent the three on the right. They’re what surfaced after dogfooding Claude Code / Cursor / v0 all the way down — the three components my muscle memory says are what I actually use every day. The next section takes them apart.

3. The Real

Agent frontend’s three home fields: the tool panel, the state observation layer, the decision gate. Each maps to an old frontend word: devtools, fiber tree, git rebase -i.

3.1 The Tool Panel: Controllability

The tool panel is in charge of one thing: letting the user see which hands the agent has, which ones can be cut off, and which one it’s reaching out with on this step.

Chat bubbles handle tools one of two ways: hide them, or pour them into the conversation. With the former you have no idea what it can actually do; with the latter you’re digging through hundreds of lines of text to find which tool it actually called — both are hostile to humans.

The right answer is to make tools a persistent UI element — like the browser’s devtools panel: always there, collapsible, interactive. Claude Code’s /permissions (the allow/ask/deny list for tools) plus /mcp (each server’s connection status) already add up to a CLI edition. Merge the two pages into one panel and it looks roughly like this:

Tools & servers (12 enabled, 3 disabled):

  [x] Read           read files (auto-allow)
  [x] Edit           edit files (ask)
  [x] Bash           run shell (ask, per-command allowlist)
  [x] WebFetch       fetch URL (ask)
  [ ] WebSearch      search web (disabled by user)
  [ ] mcp__gmail__*  Gmail MCP (disabled: no auth)
  ...

Three pieces of information squeezed into one view: who exists (which tools are there), who’s trusted (allow/ask/deny), who’s switched off (disabled). The browser built an almost identical thing long ago — that little padlock in the address bar. At first it only said “the connection is encrypted”; later, clicking it grew into a per-site permission list telling you how many permissions this site asked for, which ones you granted, and that you can revoke any of them at any time. The tool panel is the agent’s padlock.

Move from CLI to a graphical frontend and there’s more you can do: hover a tool to highlight how many times it was called this round, click it to unfold the argument diffs of those calls, right-click for “disable until this task ends.” Not one of these can be built inside a chat bubble.

3.2 The State Observation Layer: Explainability

The agent’s per-step intermediate state: how many tokens this turn ate, how much context headroom is left, what the last tool returned, what it plans to do next, whether any subagent clones are running — none of this is visible in a pure chat window; you can only guess from the model’s own muttering of “let me read the file first.”

The analogy for this layer is React devtools’ component tree plus Chrome’s Performance timeline. The former is structure: what shape an agent loop has grown into — the main agent running solo, or three subagents forked out, or MCP called nested inside. The latter is time: how long each step took, which step it’s stuck on, when the context curve turns steep.

The signal I trust most in my own tiny-agent is that line at the end of each turn’s stderr: [turn · in=6120 out=95] — in is context input tokens, out is model output tokens, written in as early as v0.2 (the first observability organ blog02 grafted onto blog01’s 32 lines); in Claude Code the counterparts are /context, /cost, and the token count on the statusline. It looks like a log line, but it’s actually the minimum viable state observation layer — two numbers telling me the shape of the bill curve and how much space is left above the context’s head. The day this becomes a graphical frontend, it’s a curve climbing in real time and a headroom gauge that looks like a CPU thermometer.

The subagent layer needs observation even more. blog213 covered this: once a subagent spins up, the main loop can’t see what happens inside — it can only wait for the return. Without a tree visualization, you can’t tell whether it’s stuck in an infinite loop, making steady progress, or already crashed without exiting yet. A fiber-tree-style interface — the whole component tree at a glance plus each node’s current state — is a hard requirement for agents, not a nice-to-have.

3.3 The Decision Gate: Human-in-the-Loop

The decision gate handles blog212’s business: “even when it’s right, it doesn’t get to just do it” — permission dialogs, A/B/C pick-one, rollback controls, diff previews, “dry-run this step first, let me look, then I’ll decide.”

Chat bubbles can’t hold this class of UI. Can you imagine cramming “overwrite README.md, 214 existing bytes will be lost, type yes to continue” into a bubble? — that’s CLI interaction force-fed into a dialog box. Hostile to humans.

The right answer is a standalone, modal or semi-modal decision component: it pops up, blocks the main flow, and demands one explicit decision before letting anything through. Frontend has ready-made counterparts: window.confirm is the most primitive edition, GitHub’s “type the repository name to confirm deletion” is the most earnest edition, and git rebase -i is the most engineered edition — list every pending operation and let you pick / squash / drop them one by one, which is essentially marching a queue of tool_use calls through the gate one at a time.

All three controls have a place in real agent products:

  • The permission dialog: Claude Code’s Yes / Yes-and-don’t-ask-again / No three-option confirmation before it touches anything is exactly this; every dispatch blocks until you pick one.
  • The A/B/C choice: have the model generate three versions in one shot and let the human pick one to continue with. More efficient than “regenerate” — you don’t throw away the two versions already generated.
  • The rollback control: let the user rewind to step 2’s state and rerun after the agent has walked 5 steps. The analogy is Git’s reset + reflog; the frontend needs a visualization that shows at a glance “which cell of the timeline I’m standing in, and which cells I can go back to.”

Put the three together and agent UI stops being “read a wall of messages + type the next sentence” and becomes “look at a tool map + follow an execution timeline + press one button at the critical junctions.”

4. The Work: A Real Dogfood Timeline

The evidence that most convinced me “chat is not the destination” is my own UI evolution history on Claude Code — over half a year, a new layer of non-chat interface grew roughly every two months.

Stage one (before Claude Code): pure chat. The web chat box era — one input field, one response area, nothing visible. Writing code was basically “I say a sentence, it answers a paragraph, I copy it back into the editor.” A motormouth search engine.

Stage two: the tool panel appears. /permissions lets you inspect and adjust allow/ask/deny per tool; /mcp shows whether each server is up, and lets you disable one per session. The first time I felt “controllability” — I finally knew how many hands it was actually reaching out with.

Stage three: the memory display layer. /memory spreads out the rules remembered across sessions for you to see, edit, and layer by project/user dimension. The memory layer blog210 covered, once productized, looks exactly like this: an invisible state turned into a visible, editable, diffable UI object.

Stage four: the subagent tree. When the main agent forks subtasks, a hierarchical progress view shows up — which subagent is running, for how long, what it returned. Once blog213’s spawn semantics got a UI, I dared for the first time to let it run three subagents in parallel: because I could see any one of them get stuck.

Stage five (the one I added myself): the A/B/C gate. This layer I hand-installed into my own blog publishing flow. When translating my Chinese drafts, the early setup was one translation sent straight to me for review — if I couldn’t spot a problem I approved; if I could, I bounced it back for a redo. It crashed three times in two months: once a technical term got translated into a generic word, once my first-person voice got translated into textbook-speak, once a Chinese cultural reference got silently deleted.

Then I changed one rule: the finished Chinese draft gets pasted for my review first, and before translating I must be given three candidates (A/B/C) — different emphases, different styles, different terminology choices — and I pick one before anything moves forward. Written into memory it’s just three clauses:

Feedback:
  1. Paste the finished Chinese draft for my review first; never quietly start translating.
  2. Before translating, give me three candidates (A/B/C):
     - A: literal translation, most faithful
     - B: free translation, most idiomatic
     - C: hybrid, literal terminology + idiomatic phrasing
  3. Only proceed after I reply A / B / C; never pick one on your own.

Once those three clauses took effect, the same translation pipeline’s crash rate dropped to near zero. The translation model didn’t get stronger — “translation” changed from “a one-shot dispatch” into “a decision gate carrying three candidates.” A pure UI/process-layer renovation; not one line of model code changed.

This echoes the subagent lesson blog213 paid for in full: the crashes weren’t the subagent doing bad work, they were me writing a bad brief — A/B/C lets me see three possible briefs materialized into three real translations before I spend five minutes deciding, which beats asking me to write a complete and precise brief up front by a wide margin.

The meta-conclusion of this section: the A/B/C gate is a UI element I hand-implemented with memory + prompt. It has no graphical interface — it’s a paragraph of text rules; but the position it occupies in my workflow is identical to a modal dialog’s: block the main flow, demand one explicit decision, release only after the decision. Many of the “agent UI components” that frontend product managers haven’t built yet, users are already cobbling together out of rules and prompt patches — and that’s the market signal: these components deserve to become first-class citizens.

5. The Boundary: Chat Isn’t Dying Either

By this point you might think I’m heading toward “chat is dead, go build tool panels” — no. Chat has scenarios it genuinely carries, and spelling those out is more honest than dismissing it wholesale.

Chat’s stronghold is uncertain tasks:

  • I haven’t figured out what I want yet; let’s just chat and feel out a direction
  • I’m exploring an unfamiliar domain and need Q&A-style dialogue to build up a mental model
  • I’m doing pure text work (writing, translation, brainstorming) with hardly any “tools” to call in the first place
  • I’m in an emotional or advisory scenario where what I want is the response itself — not a diff, not execution

Bolting a tool panel onto these scenarios is dead weight — you don’t have five tools to choose between, and an empty list sitting there only splits your attention.

Chat’s weak zone is well-defined tasks:

  • I want to walk through my fixed 7-step blog publishing flow
  • I want the agent to batch-refactor the import paths across 30 files
  • I want to review the 20 diffs in a PR and decide accept/reject on each one
  • I want the agent to spin up 3 subagents in parallel, and I want to watch them

In these scenarios, “visible and governable” beats “sounds natural” by a factor of a hundred. Making me negotiate with the agent inside bubbles about “which file should you change now” is a waste of my time — just give me a file tree + 20 diff cards + an Approve All / Reject All button. What takes me one minute there takes twenty in a chat window.

So a mature agent product should run two UIs side by side — open with chat (low-cost entry), switch the main interface to tool panel + decision gate once the task is clear, and drop back to chat when the task ends. Cursor’s Ask mode vs Agent mode (the early Chat / Composer dual panels) already sketches this shape, and Claude Code’s / command palette counts as the UX compromise of stuffing a tool panel into a CLI. But none of it is anywhere near enough — the industry’s prevailing water line today is “70% chat + 30% tool panel,” and I’m betting it flips to “30% chat + 70% specialized UI” within three years.

One line to close the boundary: chat is the entrance, not the main hall. Nothing wrong with decorating the entrance lavishly — but a house that never built its main hall shouldn’t be called a house.

6. The Hook: What’s Next & One Action Item

This post has exactly one action item: open the agent product you use most, and count the time you spend in the pure chat area versus in non-chat elements (tool lists, permission dialogs, subagent trees, diff views). If the non-chat share approaches or exceeds half — congratulations, you’re already using a “partially grown-up” agent UI; the product just hasn’t given that half its rightful name. If it’s almost all chat — either your tasks genuinely are all uncertain (fair), or the product is still parked at Web 1.0 (switch, or push them).

The action item for fellow frontend engineers: the next frontend home field is not “build yet another chat component library” — there are already twenty ChatUI libraries out there, and nobody will use the twenty-first. What genuinely nobody has built is the design system for the three component families — <ToolPanel>, <AgentTrace>, <DecisionGate>: design tokens, interaction specs, accessibility, state machines. Whoever turns that layer into the community default the way shadcn/ui did becomes the Ant Design of the agent era.

A preview of Extra C: “Fiber’s Double Buffering Teaches Agent State Three Moves: workInProgress, Commit, Rollback.” This post covered what the UI should look like; the next covers how the data underneath the UI should be organized — the problem React solved ten years ago (“an error mid-render must be able to roll back without polluting the screen”) and the problem agents face now (“a subagent crashing mid-flight must not pollute the main loop’s messages”) are the same problem met twice. Fiber’s workInProgress / current double buffer is very nearly a ready-made blueprint for agent state.

Echoing the series spine — control handed to a probabilistic function (blog01), context as its per-turn input (blog02), execution kept in your hands (blog03), blog07 on the window.confirm gate, blog216 on how config and memory grow into a rule hub — this post translates those “mechanisms inside the engine” into “UI components the user can see.” The engine and the interface were always supposed to come of age on the same day.