changelog

Mains 0.12: Serve, Theme, Browse

Mains runs without the Mac app now: one npm install puts the same backend and web UI on any macOS or Linux machine, and keeps it up as a service. Settings → Appearance ships thirty-one themes you can set per provider, a browser signs in with a one-use link instead of the owner token, and the in-app browser keeps its tabs with the chat that opened them.

0.10 put a phone in front of the Mac's runs. 0.11 made the Mac send you what was waiting on you. What was left was the Mac itself: until now the backend came inside the desktop app, and if the app was closed, so was everything else.

0.12 takes it out. The backend that runs your agents is a command now, mains, and it runs on any macOS or Linux machine with Node — a Mac mini in a cupboard, a Linux box under a desk, the laptop you're on. Your phone pairs with it, a browser signs in to it, and the desktop app drives it over SSH.

The rest of the release is about the window you look at all day: thirty-one themes, one per provider if you like, and an in-app browser that keeps its tabs with the chat that opened them.

Mains without the app

npm install --global https://github.com/mainsdotdev/mains/releases/latest/download/mains-server.tgz
mains serve

That's the whole install. It needs Node.js 22.12 or later, and it ships the web UI inside the package — it never serves one from whatever directory you happened to run it in.

It's the same Mains, not a second one. By default the server opens the desktop app's own data directory: the same database, workspaces, collections and run history, with nothing to import. Because both would be writing to one SQLite file, an ownership lock lets exactly one backend hold it at a time — quit the desktop app before mains serve, stop the server before reopening the app, and the second one is refused before it opens the database rather than after. --data-dir gives a server data of its own when that's what you want.

It binds 127.0.0.1:8787 unless told otherwise. --lan listens on your private interfaces and advertises their addresses, --tailscale-serve publishes it on your tailnet over HTTPS, and a public address is something you put behind your own TLS proxy and advertise explicitly with --public-url.

The commands

  • mains serve — run the backend and web UI.
  • mains pair — print a new pairing QR for the running server.
  • mains web — print a one-use browser login.
  • mains auth list and mains auth revoke <device-id> — see what's paired and cut one off, without a restart.
  • mains service install — keep it running with no terminal open.

Running it as a service

mains service install --tailscale-serve writes a macOS LaunchAgent or a Linux systemd user service and starts it; status, restart, stop and uninstall do what they say. It restarts on failure, logs to the data directory, and uninstalling keeps the database, the token and the logs. Nothing else has to stay open — not Electron, not a terminal, not an SSH session.

Themes

Settings → Appearance is its own page, and it opens on a strip of thirty-one themes beside the stock one: Absolutely, Ayu, Catppuccin, Codex, Dracula, Everforest, Flexoki, GitHub, Gruvbox, Kanagawa, Linear, Lobster, Material, Matrix, Monokai, Night Owl, Nightfox, Nord, Notion, One, Oscurange, Proof, Raycast, Rose Pine, Sentry, Solarized, Temple, Tokyo Night, Vercel, VS Code Plus and Xcode. Most come in light and dark; the ones that only exist one way — Tokyo Night, Dracula, Nord — only show up for that appearance.

A theme restyles nothing directly. Every surface, border, hover fill and line of text in Mains was already a step on one neutral scale, so a theme re-derives the scale from a background, a foreground and an accent, and the whole app follows — hover states included, with no reload. The stock Mains theme has no palette at all, which is why it looks exactly the way it did before themes existed.

Yours to edit

A preset is where you start. Light and dark are edited separately, and each has its own accent (the theme's, or your own), background, foreground, a contrast slider that pulls secondary text toward the foreground while leaving surfaces where they are, and whether the sidebar is translucent. Every change has a reset back to the preset.

Every preset is held to WCAG AA for secondary text, and a theme's accent that would sit under AA on its background is moved in lightness until it doesn't — hue and chroma kept, so Solarized's yellow stays yellow. An accent you pick yourself is painted exactly as you picked it.

A theme per provider

Customize for sets a default for everything, and any provider can override it — Claude in cream and clay, Codex in something darker, so the space tells you which agent you're talking to before you read a word. It takes over from the per-space tint colours, which only ever changed the backdrop.

Fonts

The same page picks the UI font and the code font. SF Pro, Helvetica Neue and Avenir Next are offered up front; the rest of what's installed on your Mac is listed through the browser's Local Font Access API once you open the menu. A font that goes missing falls back to the rest of the stack, never to the browser's default. The interface and code sizes moved here too.

Elsewhere in the app

Signing in from a browser

Any Mains backend — the server, or the desktop app with remote access on — serves its own UI to a browser. What changed is how the browser gets in.

The browser link used to carry the owner token in its query string: the credential that can do anything, in your history, in a proxy's access log, in whatever you pasted it into. Now the owner token never enters a browser at all. A login link carries a separate one-use code, in the fragment (#login=…) where it isn't sent to the server as part of the request line, and bound to the one origin it was made for. It expires after five minutes, and it's spent before anything is minted from it, so two tabs racing the same link can't both get in.

What the browser gets in exchange are two random cookies, HttpOnly and SameSite, good for eight hours: one for the WebSocket, one for the image proxy. They live only in the server's memory — restarting it, or rotating the owner token, signs out every browser at once. Their names are random per server, so two Mains on one hostname don't overwrite each other's sessions.

In the desktop app, every address under Settings → Mains Connect → This machine has a Copy one-use browser login button that mints a link only when you click it. On the server, mains serve prints one at startup — but only to a person at a terminal. Run as a service or with its output redirected, it prints none, so a login never ends up in a log; mains web makes one on demand.

Browser tabs belong to the chat

The in-app browser's tabs now belong to the conversation they were opened in. Switch chats and you get that chat's tabs back, not whichever you had open last. Tabs opened while you were still writing the first prompt follow it into the run when you send, and deleting a chat discards its tabs with it. Clear browsing data now also clears each open tab's back and forward history, so a page you cleared doesn't come back with the Back button.

A right-click menu in the browser

It has one now, and it's built for what's under the pointer: a link opens in a new tab or your external browser, downloads, or copies its address; an image opens, downloads, or copies itself or its address; a text field gets undo, redo, cut, copy, paste and select all. Back, Forward, Reload and Inspect are always there.

Images open as images

PNG, JPEG, GIF, SVG and WebP files open in the editor as a picture instead of a wall of bytes, and editor tabs carry an icon for what kind of file they are.

Smaller things

  • One image, not two. When Codex generates an image and then saves it into the project, the transcript shows the saved deliverable instead of the same picture twice.
  • A file chip stays a chip. Moving the caret next to one read its absolute path as a fresh @ query and reopened the file search; chips are a boundary for suggestions now.
  • A pull request too big for GitHub's preview says so. Tasks used to report No changes in this pull request; it now says the diff couldn't be previewed and offers Open on GitHub.
  • Claude's output styles explain themselves. Their descriptions were cut off in the dropdown; hovering one now shows all of it.
  • Cursor models read like namesComposer 2.5 (Fast) rather than composer-2.5.
  • Horizontal lists scroll like they should. Rows that run off the edge — the theme strip, the Codex plugin categories — fade at the edges and show arrows on hover.

Mains 0.12 is available for macOS (Apple Silicon and Intel). The server runs on macOS and Linux with Node.js 22.12+, and everything is open source under the MIT license at github.com/mainsdotdev/mains.

Questions or feedback? Reach out to team@mains.dev