Previews
A preview puts something with a web UI on a public link: a dev server that listens on a port of the relay's machine, or a folder of plain files. You open the link from any device, and you can send it to anyone. The relay is not exposed: previews are served by a separate listener on a separate hostname.
Typical uses:
- Open the app on your phone. The agent starts the dev server, you tap the link and try the layout on the real device.
- Send a link to someone. A colleague opens a mockup or a built site without access to your machine or your relay.
- Point a webhook at a dev server. A service that needs a public address can call the app you are building.
This page is the guide for owners and operators. The agent's tool, with its parameters, results and limits, is described on preview.
A preview link is public
The token in the link (128 bits) is the only protection. Anyone who has the link reaches the app without signing in. A preview of something with real data is a real exposure. Close a preview when you are done.
How previews are kept apart from the relay #
Previews are served by a second HTTP listener on its own port, published on a hostname of its own. Two things follow:
- The relay is not on that port. Its API, its web app, its live connection and its share links are not mounted there. Preview traffic cannot reach them, because they do not exist on that listener. Creating or closing a preview needs the relay's access token and happens on the relay's own port only.
- The hostname differs. A previewed app runs in another browser origin than the relay. Its JavaScript cannot send your relay login cookie and cannot read the relay web app's storage.
The hostname has to differ, not only the port. Cookies ignore ports, so
relay. would share the login cookie of relay..
The relay refuses to start the lane when the hostnames match.
On top of that, the lane never forwards the relay's login cookie
(copilot_) or an Authorization header that holds the relay's access
token to a previewed app, even when a hand-made request carries them. An
app's own Authorization header passes through.
Previews do share one hostname with each other, each under its own path. See What to keep in mind.
Setting it up #
Previews are off until the operator sets them up. There are three steps.
1. A hostname #
Pick a hostname you control that differs from the relay's by name, for example
preview. next to relay.. Point its DNS at your front
proxy or tunnel. Previews are namespaced by path, not by subdomain, so one
single-label hostname under a zone you already have is enough. Under
Cloudflare that name is covered by the Universal certificate. A two-label
wildcard such as *. would not be, which is why there is no
subdomain per preview.
2. A front proxy #
The proxy sends the preview hostname to the preview listener, which defaults
to the relay's port plus one (3334 when the relay uses 3333). Use a port
you fix with previews.; the proxy needs a stable target.
Caddy
preview.example.com {
reverse_proxy 127.0.0.1:3334
}
Cloudflare Tunnel
Add a second public hostname to the tunnel you already use (in the Cloudflare
dashboard under Public Hostnames → Add, or in the tunnel's config.):
ingress:
- hostname: preview.example.com
service: http://127.0.0.1:3334
- hostname: relay.example.com
service: http://127.0.0.1:3333
- service: http_status:404
3. The previews block #
Add the block to the relay's config. and restart the relay (see
oar restart):
{
"publicHostnames": ["relay.example.com"],
"previews": {
"enabled": true,
"port": 3334,
"bindHost": "127.0.0.1",
"publicBaseUrl": "https://preview.example.com",
"allowedTargetHosts": [],
"maxLive": 8
}
}
| Key | What to put there |
|---|---|
public |
Every hostname the relay itself answers on. The check against public uses this list (and always localhost and 127.), so a hostname you leave out is not checked. |
previews. |
true to turn the lane on. It is off by default. |
previews. |
The listener's port. Default: the relay's port plus one. 0 picks a free port at each start, which a front proxy cannot follow. |
previews. |
Where the listener binds. Default 127.. Keep it on loopback: the front proxy runs on the same machine. |
previews. |
Only needed when bind is not loopback. Every preview is then reachable from the network without the proxy. |
previews. |
The public address of the lane: https://. Links are built from it. |
previews. |
Hosts besides this machine that a port preview may point at, such as a container or a VM. |
previews. |
Most previews at once, for the whole relay. Default 8. |
Every key, default and edge case is in the table on config.json.
Environment overrides #
COPILOT_, COPILOT_,
COPILOT_, COPILOT_ and
COPILOT_ override the matching keys. Their
values and what wins over what are on
Environment variables.
When the lane refuses to start #
A misconfigured lane never takes the relay down. The relay keeps running and the lane stays off. None of these checks becomes a warning, because each one protects the separation described above.
| Reason | Message |
|---|---|
public is missing or not an http(s) address |
previews. |
public uses a relay hostname |
previews. |
| The port is below 1024 | previews. |
| The port is the relay's port or its CLI port | previews. |
| The port cannot be worked out | previews. |
bind is not loopback |
previews. |
You see it in two places:
- The relay log, at start: one line
PREVIEW LANE disabled: <message>per failed check. When the checks pass, the log says[preview] listener on http://. Where the log is: Files and folders.127. 0. 0. 1: 3334 → https:// preview. example. com - The answer to a publish request. The API answers 503 with
Preview lane failed its startup checksand the messages indetails. The agent'spreviewtool passes this on, and the agent is told to tell you. Whenpreviews.is not set the answer isenabled Preview lane is disabled (set previews., and the log has no line.enabled and previews. public Base Url)
Settings → Previews and the / notice do not show the reason. The
settings tab lists the previews that exist, and the notice says only that the
publish failed (see /). Look in the relay log.
If the port is taken by another program, the checks pass but the listener
cannot bind. The log then says
PREVIEW LANE failed to bind :<port> — <reason>. The lane is up as far as the
API is concerned, but nothing answers on the preview port, so the links do not
open. Free the port or set another previews..
Three ways to publish #
Ask the agent #
Say what you want: "start the dev server and give me a link", "show me the
built site". The agent uses the preview tool, or the relay's preview API
where a provider has no tool. It tells you the link and that the link is
public. Which providers have the tool is in the
table on the reference page.
/preview in the composer #
The command runs in your browser against the relay. It works with every provider and costs no agent turn. It runs when nothing is attached to the message. The notice appears in the conversation.
| You type | What happens |
|---|---|
/ |
Publishes port 5173 with the label "web app". Prints Preview published: <url>. |
/ |
Publishes the folder ./ as a static preview, label "built site". The first word is the folder, so it cannot contain a space. Prints Preview published: <url>. |
/ |
Lists every preview of the relay, one per line: first 8 characters of the token, the label, the port or folder, — offline if it is, and the link. Prints No live previews. if there are none, and The preview lane is disabled on this relay. when it is off. |
/ |
Closes the preview of this conversation. With none, prints No live previews in this conversation. With several, prints the matches and asks for a longer prefix. |
/ |
Closes the preview whose token starts with those 4 to 32 hex characters, in any conversation. Prints Preview closed: <label>. With no match: No preview matches "3f9c". |
/ |
Prints the usage line, as does anything it cannot read. |
A first word made of digits is always a port, and a port outside 1 to 65535
prints the usage line. For a folder named 8080 type ./.
If publishing fails, the notice is the same for every cause (a wrong port, a folder outside the working folder, a full lane, a disabled lane):
Preview failed — check the port/path, or whether the preview lane is enabled (Settings → Live previews).
The exact reason is not shown. The list is on Settings → Previews, under
the heading Live previews. To see the reason, call the API (below) or
look at the table in Troubleshooting. If the relay cannot
be reached, the notice is Preview command failed — could not reach the relay.
The command menu and its keys are on
Composer.
The API #
Everything above uses the same API on the relay's port. It needs the relay's
access token (auth in config., the same token you sign in with) as
a bearer token. Register first, so you know the base path, then start the dev
server under it:
curl -sX POST http://127.0.0.1:3333/api/previews \
-H "authorization: Bearer $RELAY_TOKEN" -H 'content-type: application/json' \
-d '{"conversationId":"<conversation id>","port":5173,"label":"web app"}'
{
"ok": true,
"url": "https://preview.example.com/test_0123456789abcdef0123456789abcdef/",
"basePath": "/test_0123456789abcdef0123456789abcdef/",
"preview": { "token": "0123456789abcdef0123456789abcdef" },
"existing": false
}
existing is true when the conversation already had a live preview of the
same port or folder and you got that one back.
| Request | Purpose |
|---|---|
POST / |
port or dir (not both), optionally conversation, host, label. |
GET / |
All previews. ?conversation, or the conversation header a session worker sends, narrows it to one conversation. |
DELETE / |
Closes one. The dev server is untouched. Added in 0.9.14 A request that names a conversation (as for GET) may close only that conversation's previews; another's is refused (403). |
Making an app work under the preview's path #
Every preview lives under / on the shared preview hostname. The
rule that follows from what the proxy does:
- The browser asks for
/.test_<token>/ … - The app receives the path without that prefix.
- So the app must put links and asset URLs in its pages that either carry the prefix or are relative, and it must still answer requests that do not carry it.
What the proxy does on the way:
| The proxy | |
|---|---|
| Removes the prefix | The app receives the path after /, with the query. A visit to the bare / is redirected (301) to the same address with a trailing slash, so relative URLs resolve inside the preview. |
Sends X- |
The prefix without a trailing slash. |
Sends X- |
The Host the preview listener received. |
Sends X- |
The one the front proxy sent; https if it sent none. |
Sends X- |
The address of whoever connected to the listener. Behind a front proxy on the same machine that is the proxy. |
Sets Host |
To the app's own address (127.), for dev servers that check it. |
Rewrites Location |
A redirect to / becomes /, as does an absolute redirect to the app's own address. A redirect that already carries the prefix, a // one, a relative one or one to another site is left alone. |
Scopes Set- |
The cookie gets Path=/ and loses Domain, so one preview's cookies do not reach another. A cookie named copilot_ is dropped. |
| Marks responses | X-, and it drops Service-. |
It does not rewrite page, stylesheet or script bodies. An app that writes
/ into its HTML asks the root of the preview hostname, where no
preview lives, and the browser gets the No such preview page (404). That is
the sign that the app does not know its prefix.
What works #
- Relative URLs. An app whose pages use
./style addresses works as it is: the page is atassets/ app. js /, so they resolve inside the preview. This follows from how the proxy works; it was not tried per framework.test_<token>/ - Reading the prefix. An app that routes on unprefixed paths and builds its
links from
X-fits the rule exactly.Forwarded- Prefix - A built site. Build with a base that is relative or equal to the prefix,
then publish the output folder as a static preview. Requests for
/are served from the folder, so both kinds of URL work.test_<token>/ assets/ …
What does not work as is #
A tool setting that makes the app answer only requests that carry the prefix does not fit, because the proxy removes it.
- Vite dev server with
baseset to the prefix (--base). Checked against the Vite 8.3.1 source on the relay's machine: the dev server handles only paths that start withbase. It answers a request for/with a redirect tobase, which the proxy leaves alone, so the browser loops. Other unprefixed requests get a 404 that says the server has a public base URL. Hot reload listens only at the path underbase, which the stripped request does not match. Use the built-site approach, or a Vite setup that serves from the root with relative URLs. - Express with the router mounted at the prefix
(
app.). The app receivesuse(prefix, router) /, notx /, so the mount does not match. Mount the router attest_<token>/ x /and build links fromX-.Forwarded- Prefix
Next.js (base, asset) and SvelteKit (kit.) are named
in OAR's own notes for agents, but those notes were not checked against what
the proxy does, and neither tool is available here to test. If their base
setting makes the app expect the prefix on every request, it does not fit for
the same reason as Vite. Not verified: try it before you rely on it.
Publish first when the app needs the prefix
An agent calls create before it starts the dev server, so the app can be
given the prefix if it needs it. For an app that works with relative URLs the
order does not matter.
Static previews #
With dir instead of port the relay serves a folder itself. Nothing has to
run, which suits a build output, a mockup, a single HTML file or a coverage
report.
- The folder is taken from the conversation's working folder (absolute, or relative to it) and must be inside it. A preview published with no conversation uses the relay's own working folder. This is checked when you publish and on every request, so a symlink made later cannot lead out.
index.is served for a folder path. Nothing is cached.html - Never served: files and folders whose names start with a dot (
.,git .) and key files (env *.,pem *., names starting withkey id_,rsa id_,ed25519 id_orecdsa id_).dsa - Relative asset paths work. So do ones that start with the preview's prefix.
Root-absolute ones (
/) do not.assets/ app. js - A folder without an
index., and any missing file, gets ahtml Not foundpage (404). - A static preview is always
onlineand refuses WebSocket connections.
Managing previews #
- The card in the background tasks panel. Each preview of the
conversation has a card with its label, the port, a note that the link is
public, an
onlineorofflinebadge once the relay has checked it, and the buttons Open, Copy link and Close. The panel is described on Background tasks. - A link card in the transcript. When a preview is published while a turn
of the conversation is running, the reply carries a link card, whether the
agent or you with
/published it. A preview published between turns has no transcript card. Once the preview is gone, the card shows apreview closedbadge and no link. A shared conversation shows the card with its label only, and every preview link quoted in its text reads[preview link removed]. - Settings → Previews. The list Live previews shows every preview of the relay, from all conversations, with the first 8 characters of the conversation id and Added in 0.9.11 when it was published and last opened. Close one here when you have left its conversation. See Settings.
offline and closed are different. The relay checks every port preview
every 15 seconds, by trying to connect to the port (and once straight away
when you publish). When nothing answers, the card shows an offline badge
and the note "offline — dev server not responding". The link stays valid:
start the server again on the same port and the same link works. closed
means the preview is gone and the link no longer resolves.
Closing a preview ends the link only. The dev server keeps running as the background task it is.
A preview does not expire by itself. It ends when you close it, or Added in 0.9.11 when you delete its conversation (also through a rebuild of the relay's cache). Archiving a conversation keeps its previews live, in Settings → Previews.
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, instead of a new one.
At most 8 previews live at a time, for the whole relay (previews.).
The refusal names the three oldest, with when each was published and last
opened, and how to close them.
After a relay restart Added in 0.9.11 #
Previews survive a relay restart, an update or a crash: the relay keeps them in its database and restores them when it starts, so a link you handed out keeps working. Each one is checked again first, and ends when the check finds it no longer valid:
- a folder preview whose folder is gone, or no longer inside its conversation's working folder;
- a port preview after the machine rebooted: the dev server is gone, and the port may later belong to something else;
- a preview whose conversation no longer exists.
Linux and macOS report a reboot reliably. On Windows a Restart counts as one, but with fast startup on (the Windows default) Shut down and power on does not: a port preview from before comes back and reaches whatever listens on its port then. Close previews you no longer need.
A check that cannot answer, because the database is busy or the folder cannot
be read for another reason than being gone, does not end the preview. It is
not served meanwhile: Settings → Previews and the tool's list show it as
not served (unverified), it counts towards the limit, and it can be closed.
The relay checks it again every 15 seconds and serves it again under its link
as soon as the check answers; publishing the same port or folder again in its
conversation does that at once. After ten minutes without an answer the
preview ends, and the log says so.
A port preview is also probed once right after the start, so its online or
offline badge is current. The relay's log names a preview by a short hash,
never by its token. See also
What a restart keeps and what it loses.
WebSockets #
A port preview passes WebSocket connections through under the same prefix, with the prefix removed as for any request, so an app's own sockets work when the browser connects through the preview link. (Hot reload is the app's business: see the Vite note above.) A static preview, and an unknown or closed link, answer a WebSocket request with 404. If the app answers the upgrade with an ordinary response, the browser gets that status. If the app is down, 502.
What to keep in mind #
- Previews share an origin with each other. Cookies are scoped to each preview's path and the tokens cannot be guessed, but two previews are not as separate from each other as each one is from the relay.
- Only your own machine, by default. A port preview may point at loopback
addresses only, plus the hosts in
previews.. The relay's own ports are always refused, since publishing one would put the relay's login page on the public hostname.allowed Target Hosts - The relay does not run the app. It never starts or restarts a dev
server. If the server stops, the card goes
offline.
By provider #
Previews look the same on every provider; only the way an agent publishes
differs, as the reference page
shows. / and Settings → Previews work with all of them.
Troubleshooting #
| What you see | Cause and fix |
|---|---|
The agent or the API answers Preview lane is disabled or Preview lane failed its startup checks (503) |
The lane is off or refused to start. Read the PREVIEW LANE disabled lines in the relay log, fix previews, restart the relay. See When the lane refuses to start. |
The link shows a Preview upstream unreachable page (502) |
Nothing listens on the port, or the connection failed. Start the dev server again on the same port and reload; the link stays valid. |
Assets are missing or the page is blank, and requests for / get No such preview (404) |
The app writes root-absolute paths. Make it emit relative or prefixed URLs (rule). |
| The page loads in a loop, or the app answers 404 with a note about a "public base URL" | The app expects the prefix on every request, but the proxy removes it (what does not work). |
No such preview on the whole link |
The link is unknown or closed, its conversation was deleted, or a restart found it no longer valid (after a restart). Publish again. |
A preview shows as not served (unverified) |
The relay could not check it at its start. It is checked again every 15 seconds; publishing the same target again in its conversation brings it back at once. |
| The link does not open at all | The front proxy or DNS is not set up, or the listener could not bind its port (PREVIEW LANE failed to bind in the log). |
The card says offline |
The dev server has stopped answering. Start it again on the same port. |
Too many live previews (max 8); previews stay until closed. Oldest: … (429) |
Close one in Settings → Previews, with /, or with DELETE /; an agent can close its own conversation's with the tool's close action; or raise previews.. |
Port <n> belongs to the relay and cannot be previewed (400) |
You gave the relay's port, its CLI port or the preview port. Publish the dev server's port. |
Target port must be between 1024 and 65535 (400) |
Use a higher port. |
Target host ... is not loopback and not in previews. (400) |
Add the host to previews.. |
dir must live inside the workspace root (..., Directory not found: ... or Not a directory: ... (400) |
The folder must exist and be inside the conversation's working folder. |