preview
With preview an agent puts something with a web UI on a public link, so you
can open it from your phone or send it to someone: a dev server that listens
on a port of the relay's machine, or a folder of plain files. It can also list
and close the previews of its own conversation.
Ask in plain words: "start the dev server and give me a link", "show me the
built site". The agent is told to use the tool whenever you want to see, try
or share a web UI, and never to send you to localhost.
A preview link is public
Anyone who has the link can open the app without signing in. The link's token is the only protection. The agent is told to say so when it gives you a link. Close a preview when you are done with it.
What you see #
- A card for the preview in the conversation's background-task panel, with Open, Copy link and Close.
- A link card in the transcript, on the reply in which the agent published
it. It shows a
closedbadge once the preview is gone. A shared view of the conversation shows the card's label only, never the link. - Every preview of the relay, across all conversations, under Settings → Previews, each with a button to close it.
- An
offlinebadge on the card when the dev server behind a port preview stops answering. The link stays registered: start the server again on the same port and the same link works again.
You can do the same without an agent by typing / in the composer:
/preview 5173 web app publish a port
/preview ./dist built site publish a folder
/preview list list the previews
/preview close close one
Setting it up #
Previews are off until the relay's operator sets them up. They need a hostname of their own, separate from the relay's, so that a previewed app can never reach the relay itself. Three things:
- A hostname you control that differs from the relay's by name, not only by port.
- A front proxy (Caddy, a Cloudflare Tunnel, …) that points this hostname at the preview port.
- A
previewsblock in the relay'sconfig.:json
{
"publicHostnames": ["relay.example.com"],
"previews": {
"enabled": true,
"port": 3334,
"bindHost": "127.0.0.1",
"publicBaseUrl": "https://preview.example.com",
"allowedTargetHosts": [],
"maxLive": 8
}
}
Agents have the tool whether or not this is done. Without it, a create call
comes back with the reason, and the agent is told to pass that on to you: it
is the operator's fix.
By provider #
| Provider | |
|---|---|
| Claude, Claude API Added in 0.9.18, DeepSeek Added in 0.9.17, Cursor | has the preview tool |
| Grok; GitHub Copilot and OpenAI on the extension engine | has the preview tool, through OAR's MCP server Added in 0.9.4 |
| GitHub Copilot and OpenAI on the SDK engine | no tool. While previews are set up, the agent is told how to call the relay's preview API, with the same rules and its conversation already written into the calls. What you see is the same. |
| Claude Cloud | none: a cloud session runs in a sandbox at Anthropic, and there is no port or folder on your machine to publish |
Actions #
| Action | What it does | Parameters |
|---|---|---|
create |
publish a port or a folder | port or dir, optionally label |
list |
list this conversation's previews | |
close |
close one of this conversation's previews | token, optional when the conversation has exactly one |
A result is JSON with "ok": true, or "ok": false and an error text. The
tool never fails in another way; a wrong parameter comes back as an error
the agent can read.
create #
Publishes either a port or a folder, never both in one call.
Added in 0.9.11 Publishing the same port or folder again in the same
conversation returns the preview that is already live, with the same link and
the label unchanged; the result then says "existing": true.
| Parameter | Type | |
|---|---|---|
port |
1024–65535 | the local port of a dev server |
dir |
path | a folder to serve as plain files: absolute, or relative to the conversation's working folder |
label |
text, up to 120 characters | the name on the preview card |
A dev server (port) #
The relay passes requests through to a server that listens on that port. It does not start, watch or restart that server; that stays an ordinary background task of the agent, with its own stop button.
Every preview lives under its own path on the preview hostname. The browser
asks for /; the relay removes that prefix before it passes
the request on, and tells the app the prefix in X-. It does
not rewrite pages. So an app works when the links and asset addresses it
sends out are relative or carry the prefix, while it answers requests without
the prefix. An app that asks for / from the root of the host
will not find it.
That is why the agent is told to call create first and read the
base from the result before it starts the dev server.
What agents are told does not fit every dev server
The tool's own description suggests starting the server with the base path
set (Vite --base, Next.js base, an Express mount prefix). A server
that then expects the prefix in the requests it receives does not match what
the relay passes on: Vite's dev server and an Express router mounted at the
prefix do not work that way. Previews
explains what does.
{ "action": "create", "port": 5173, "label": "web app" }
{
"ok": true,
"existing": false,
"url": "https://preview.example.com/test_0123456789abcdef0123456789abcdef/",
"basePath": "/test_0123456789abcdef0123456789abcdef/",
"token": "0123456789abcdef0123456789abcdef",
"mode": "port",
"hint": "Serve the app under /test_0123456789abcdef0123456789abcdef/ (the proxy strips the prefix and sends X-Forwarded-Prefix). Bind the dev server to 127.0.0.1, not 0.0.0.0: the relay's proxy reaches it there and nothing else should. Tell the user the URL and that the link is public."
}
Added in 0.9.17 The hint also tells the agent to bind the dev server to
127., not 0.: the relay's proxy reaches it there, and nothing
else should. Only sessions with the preview tool get this line (see By
provider). Agents that are taught the preview API instead of
the tool do not: GitHub Copilot and OpenAI on the SDK engine, and Grok and
Copilot's extension engine when the relay runs with OAR_.
A folder (dir) #
The relay serves the folder's files itself. Nothing has to run: good for a build output, a mockup, a single HTML file or a coverage report.
- The folder must be inside the conversation's working folder. This is checked again on every request, so a link cannot be made to lead outside later.
- Files and folders whose names start with a dot (
.,git .) are never served, and neither are key files (env *.,pem *.,key id_).rsa… index.is served for a folder path.html - Relative asset paths work as they are. Paths from the root of the host
(
/) do not.assets/ app. js - No WebSocket connections.
{ "action": "create", "dir": "./dist", "label": "built site" }
The result has the same fields, with "mode": "static".
list #
Lists the previews of the agent's own conversation, with token, label,
url, target (the folder, or localhost:<port>), online and the
conversation. Added in 0.9.14 Before 0.9.14 the list covered every preview of
the relay; a preview's token is its only protection, so an agent now sees
only its own. The relay-wide list is yours, in Settings → Previews and
with /. A preview the relay could not check after a restart carries "unverified": true
and is not served until the check answers (see
After a relay restart).
{ "action": "list" }
{
"ok": true,
"enabled": true,
"previews": [
{ "token": "0123456789abcdef0123456789abcdef", "label": "web app", "url": "https://preview.example.com/test_0123456789abcdef0123456789abcdef/", "target": "localhost:5173", "online": true, "conversationId": "3f9c…" }
]
}
close #
Closes the link. A dev server behind it keeps running.
| Parameter | Type | |
|---|---|---|
token |
32 hex characters | the token from create or list. May be left out when the conversation has exactly one preview. |
With no token and several previews in the conversation, the call comes back
with an error and the list to choose from.
Added in 0.9.14 An agent closes only its own conversation's previews. The token of another conversation's preview is refused:
{ "ok": false, "error": "This preview belongs to another conversation; a conversation can close only its own previews. The owner can close it in Settings → Previews." }
{ "action": "close", "token": "0123456789abcdef0123456789abcdef" }
{ "ok": true, "closed": { "token": "0123456789abcdef0123456789abcdef", "label": "web app" } }
Limits #
| Limit | Value |
|---|---|
| Previews at a time, whole relay | 8 (previews.) |
| Which previews an agent can list and close | its own conversation's Added in 0.9.14 |
| Port | 1024–65535, and never one of the relay's own ports |
| Where a port preview may point | the relay's own machine (loopback), plus hosts the operator lists in previews. |
| Where a folder preview may point | inside the conversation's working folder |
| Lifetime | until it is closed or its conversation is deleted. Added in 0.9.11 It survives a relay restart; a port preview ends when the machine reboots, a folder preview when its folder leaves the working folder. A preview does not expire by itself. |
| Label | 120 characters |
Previews are kept apart from the relay: they are served on a separate port under a separate hostname, where none of the relay's own pages or API exist. Previews share that hostname with each other, each under its own path.
The conversation scope is not a wall
The relay scopes a preview request to the conversation it names, in the
conversation query or the conversation header every session worker and
OAR's MCP server send. Agents use the same relay token as you do, so the scope
is declared by the request, not proven: a request that names no conversation
still gets every preview. The scope keeps the tool and the calls agents are
taught to their own conversation; it does not stop an agent that reads the
relay token and calls the API by hand. See
What OAR exposes.