← Blog

The Surface Problem

August 21, 2026 AIAgentsAsterEngineering

You start a task in the terminal because that is where you were. The agent finds the bug and writes the fix. Then you want to read the diff properly, so you move to your editor, and the conversation you were just having is not there. You open the panel and explain it again.

Later it builds a page. You ask how it looks, and you get a paragraph describing it.

That is one task, three tools, and two explanations you should not have had to give. The work never moved. Only you did.

An agent is not a place you go

The habit in this space is to ship an agent as a destination. It gets an app, or a tab, or a panel, and that becomes where the agent lives. You go there to use it. Everything it knows lives inside it, and if you want the same agent somewhere else, you set it up again and it starts from nothing.

That is backwards. The agent is not the thing you are working on. Your repo is. The agent should turn up wherever you happen to be looking at that repo, already knowing what you told it an hour ago in a different window.

The version of Aster I shipped today is mostly about that one idea. Not new intelligence. New places for the same intelligence to appear, and one piece of plumbing that makes moving between them free.

The panel, served as a page

aster serve opens the agent at http://localhost:4187/.

It is not a second UI I wrote for the browser. It is the editor panel, served as a page. The same streaming chat, approvals, @ mentions, slash commands, saved sessions, review, and compaction, because the page speaks the protocol the editor’s webview already spoke. Every turn runs in the repo you started the server from.

The part I like most is boring. The UI ships inside the released binary. There is no second download and no separate install to keep in step, so curl … | sh gets you the browser version whether you wanted it or not. The port is guarded so no other page in your browser can drive the agent, and reaching past loopback needs a token the banner prints.

That last detail matters more than it sounds. An agent listening on a port is an agent anything on your machine can talk to. Making the remote case deliberately awkward is the right default.

Work you can look at

The other half is a tool called open_preview.

Before this, a turn that built something ended by describing it. The agent now points at the dev server it started or the file it wrote, and the page opens in your browser. A directory opens its index.html. A port with nothing listening is refused instead of handed to you as a connection error. A page it already opened this session points back at that tab instead of stacking a second one.

Loopback URLs and files inside the repo open on their own. Anything else asks first, so a link off your machine is still your call. On a server or in a container, ASTER_NO_BROWSER turns the opening off and leaves the URL in the reply.

There is a related fix that sounds small and is not: a tool can now return an image. A screenshot from the browser tool reaches the model as an actual image rather than the text [image content omitted]. Taking a screenshot was pointless until the thing that asked for it could see it.

The state lives in a file

None of that would help if each surface kept its own settings, which is the usual failure. You switch model in the desktop app and the terminal has not heard about it. You set up a provider in one place and the editor asks you for the key again.

So the state does not live in any of the apps. It lives in aster.yaml and in session files on disk, and every surface resolves from there.

aster.yaml + sessions on diskprovider, model, permissions, history Terminal Editor panel Browser Desktop app Telegram

aster provider use groq points the endpoint somewhere new and adopts a model it serves, in one write, and that is the switch everywhere rather than a preference belonging to whichever app made it. Thirty-five endpoints ship in the binary, and four of them are your own hardware: Ollama, LM Studio, vLLM, and llama.cpp. Each provider now keeps its own key, so setting up a second one stops overwriting the first, which is the kind of bug you only find by actually using two.

Sessions work the same way. A turn’s reasoning is written into the transcript instead of streamed and forgotten, so reopening a session rebuilds the whole thread in order: the thinking, the reply, then the steps it ran. Close the browser, open the editor, and the conversation is the conversation.

What it does not fix

The surfaces do not talk to each other. They read the same files. That is what makes the design simple and it is also the limit: two surfaces open on one session at the same time are two readers, not one shared live view, and they can drift until something reloads. I would rather say that plainly than pretend there is a sync layer here.

One server also serves one repo, the one you started it in. If you work across three repos at once you are running three servers on three ports, and nothing collects them into a single view.

The editor extension ships on the marketplace’s clock rather than mine, so it lags the CLI by however long a publish takes. And the desktop app currently has an Apple Silicon installer but no Intel one, which is a gap I have not closed yet. The CLI covers Intel Macs fine, so the workaround exists, but a gap with a workaround is still a gap.

The boring win

Everything above is plumbing. There is no new model in this release and no clever prompt. The agent is exactly as smart as it was last week.

What changed is that it stopped making you come to it. You can start something in the terminal, read the diff in your editor, open the result in a browser, and answer a question about it from your phone over Telegram, and it is one conversation the whole way through because none of those apps owns it. A file does.

That is the thing I would build again. Put the state somewhere every surface can read, and adding a new surface stops being a migration and becomes a client.

Aster is Apache-2.0, self-hostable, and runs against whichever model you point it at, including one on your own machine.

plaintext
curl -fsSL https://withaster.dev/install | sh