Will ChenWill Chen
← Writingsystem design

Taking apart the chat interface

A chat window is a terminal with one command. I spent two days working out what the other commands would be.

Will ChenWill Chen5 min

Motivation

A chat interface has a simple shape underneath it. You type something, a system runs, and text comes back. Then you type again, and the whole conversation so far is sent along with your new message, because the model on the other end keeps nothing between calls. The scroll of alternating bubbles is a rendering choice on top of that loop, not the loop itself.

Every other interface built on that shape has more than one thing you can do. A terminal takes commands, and a command produces output, and the output is a file or a process or a value you can hand to the next command. A notebook takes cells, and a cell produces a result that stays on screen and can be referenced later. A chat window takes messages, and a message produces more text. That is the entire vocabulary. There is one command and its output is prose.

So in January I sat down to work out what a chat interface would look like if you gave it the rest of the vocabulary.

The substitutions

The first thing I wrote down was that this is not a chat. The user is not messaging an assistant. The user is issuing commands in natural language, the system parses the command, something runs, and you watch it run.

That reframe forces a set of replacements, and each one is a real design decision rather than a rename.

ChatConsoleWhat changes
chatsessiona transcript becomes an environment with contents
messagecellfinished text becomes a function run that can still be running
fileobjectopaque bytes become data with its methods attached
you write the shell scriptthe agent writes itand you can read what it wrote

Chats become sessions. A chat is a transcript, which means the only thing it accumulates is text. A session is an environment, which means it has contents. It starts empty, the same way a coding agent starts in an empty folder, and you load things into it or create them as you go.

Messages become cells. A message is a block of text that has already finished. A cell is a function run with a view attached, which means it can still be running. If the command you typed was to ping you every five minutes, that cell keeps a little green dot on it and stays alive in the session while you do other things. Background processes stop being an exotic feature and become the ordinary case of a cell that has not finished.

Files become objects. This is the substitution I care most about, and the reason is specific. A REPL gives you objects but they evaporate when the session ends. A filesystem gives you persistence but a file is opaque: you can read the bytes, and nothing about the file tells you what you are allowed to do with it. What I wanted was persistence with the operations attached, so an object carries a data part and a methods part, and the methods are self-describing, which means whatever is looking at the object can find out what can be done to it without being told separately. Predefined types would be nice and are not required, because you can attach methods to data dynamically. It is a REPL, constrained.

The shell script becomes something the agent writes. In a terminal you write the orchestration yourself. Here the agent writes it, and you can read what it wrote.

Put together, the description I landed on was object-oriented chat.

The orchestration language

If the agent is writing the orchestration, then something has to decide what it writes, and that is a language design question rather than an interface question. I decided it should generate Lisp.

The reason is the property Lisp is famous for, which is that code and data have the same shape. A Lisp program is a list, and a list is a thing a program can build, take apart, and hand around. When the thing writing the program is itself a program, that stops being an elegance and becomes a convenience: the output is already a data structure, so you can show it to the user, store it in the session, pass it to another cell, or rewrite it, without parsing anything back out of a string.

The next question was whether to embed one of the small JavaScript Lisp interpreters or write one. I decided to write it, for control. That is defensible here in a way it usually is not, because I did not need a general-purpose Lisp. I needed an evaluator over the objects in the environment, which is a much smaller thing, and every construct it supports is a construct I have to be able to explain in the interface.

Re-deriving the motivation

A day and a half in, with the interface reframed and the language chosen, I stopped and wrote that this was going too deep and we needed more clarity, and that we should think about why we even want the console at all.

What followed was nine reasons, written from scratch, in one go:

  1. It is not chat but works like chat.
  2. You do not think in terms of agents, you think in terms of objects and functions. In fact an agent can just be an object in the environment.
  3. User to agent becomes user to environment.
  4. A richer visualization paradigm than generated UI or tool messages, because each cell is a function run with a view, and that function can be running in the background.
  5. Each object becomes a primitive for your AI system.
  6. Custom renderers and editors for objects, which makes it a digital workspace.
  7. More abstract than a filesystem, since a filesystem can itself be an object.
  8. A template other projects can start from instead of a chatbot, where you define the object types.
  9. Easier to use agentic systems while still feeling free and composable rather than a rigid UI.

Writing that list is the part of this I would do again. I had spent a day and a half making decisions that were each defensible on their own terms, and I could feel that I was accelerating without being able to say where. Stopping to answer why the thing should exist is cheap at that point and expensive later, and the list either produces reasons or it does not.

User to environment

Most of those nine are features. Reason three is not, and it is the only one still doing work in what I build now.

User to agent becomes user to environment. In the chat framing there is a someone on the other side and you are negotiating with them: you explain, they misunderstand, you rephrase. In the environment framing there is no someone. There is a place with things in it, the agent is one more thing in that place, and your commands act on the place. The agent stops being your counterpart and becomes an instrument that operates on shared state you can also see and touch.

That is a smaller claim than object-oriented chat and it turned out to be the durable one. The console was a way of arriving at it.