
# Live Share

`live` shares the workbench's workspace with whoever opens a link. One browser starts a session
and hands out the link; whoever opens it reads the sharer's files, opens the same tabs, and sees
everyone's caret where it is. There's no session on a server — the browser that started it hands
out the seats, and frames pass between browsers over whatever transport you give it.

```ts
import { Workbench, FileSystem } from "codelet/workbench";
import { live } from "codelet/extensions/live";

const workbench = new Workbench({
  parent: document.getElementById("app")!,
  fs: new FileSystem({ "/README.md": "# Hello" }),
  extensions: [live()],
});
```

By default a session goes no further than the browser it started in: `broadcastChannelGateway()`
reaches other tabs of the same origin and nothing else. That's the whole session working, between
two of your own tabs, and it asks for nothing. To share with another machine, pass a gateway that
leaves the page — see [The Gateway seam](#the-gateway-seam).

::warning
Anyone with the link is in the session, and a guest can edit, create, rename and delete the
sharer's files. There's no admission prompt and no read-only mode: the link is the whole of the
access control, so treat it as a credential.
::

## Options

| Option      | Type                         | Default                                  | Description                                                                                                                                                |
| ----------- | ---------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gateway`   | `() => Gateway`              | `broadcastChannelGateway`                | What carries a session. Pass `mqttGateway` to share between machines. See [Transports](#transports).                                                       |
| `upgrade`   | `(bus: Gateway) => Gateway`  | —                                        | A bus over that bus, applied once the session is sealed. `p2p()` is the one that ships. See [Connecting browsers directly](#connecting-browsers-directly). |
| `user`      | `string`                     | the name the reader gave                 | This reader's display name in the roster, decided by the page. Passing one skips the prompt too.                                                           |
| `link`      | `(id: string) => string`     | the page's own URL, no query, `#live=id` | The address a guest opens, built from the session's secret. See [The link](#the-link).                                                                     |
| `exclude`   | `readonly string[]`          | —                                        | Globs this browser never shares when it's the one sharing: never listed, never read, never mentioned in a change.                                          |
| `terminals` | `"off" \| "read" \| "write"` | `"off"`                                  | Whether the shells the terminal is showing cross too. See [Sharing terminals](#sharing-terminals).                                                         |

## Starting and joining

The extension adds a **Live Share** entry to the activity bar. Its pane is where a session starts:

- **Start Session** — begins one and shows the link, with a button to copy it and a button to open
  it in a second tab. A workspace with no files has nothing to admit anybody to, so the pane offers
  joining alone until there's a file.
- **Join Session** — asks for a link in the workbench's own field. Paste a whole link, or the bare
  id out of one.

Five commands are in the palette: `Live Share: Start Session`, `Live Share: Join Session`,
`Live Share: Leave Session`, `Live Share: Change Display Name` and `Live Share: Focus Live Share`.
`live.join` is also how anything that already holds a link gets in — handed one it joins straight
away instead of asking:

```ts
vscode.commands.executeCommand("live.join", "https://example.com/#live=…");
```

A tab opened _on_ a link joins by itself, so handing out the link is usually the whole gesture.
Sharing doesn't navigate the sharer anywhere — the link is handed out, not opened.

The status bar says which end this is and what it's running over — `$(lock) Sharing (public mqtt,
3 peers)` on the host, `$(check) Joined (tab)` on a guest, `$(error) Live Share: <reason>` for a
session that ended. Clicking it opens the pane; while a session is up, the activity bar icon shows
how many are in it. One button leaves either end: the sharer's **Stop Session** ends it for
everybody, a guest's **Leave Session** gives up their own seat and goes back to their own files.

The first time this browser is in a session it asks for a display name, and remembers the answer
in `localStorage` — that's what the roster, the caret's name tag and a file's badge in the
explorer all say. Leave it empty and you're your seat instead: `Host`, `Guest 2`, `Guest 3`… To
change it, click your own roster row and type over it, or run `Live Share: Change Display Name`.
Passing `user` skips the question entirely: nothing prompts, and your row isn't a way to a name.

## What a guest sees

A guest's own workspace is left exactly as it was. The shared tree opens _over_ it as a workspace
of the session's own, so leaving is the whole way back — their own tabs, carets and unsaved text
are still there.

Five things cross the session:

- **The files.** The sharer's tree, browsable and searchable, filled lazily: the root level on
  joining, a directory's children when it's expanded, a file's text when it's opened. A guest can
  create, write, rename and delete too — those are asked of the end holding the files.
- **The documents.** The file being read is one document in every tab: edits cross as edits, merge
  rather than overwrite, and land in each reader's own undo history.
- **The carets.** Everyone's cursor and selection, drawn in the file they're in, in that peer's own
  colour. Clicking a roster row jumps to that peer's file at their caret and **follows** them from
  there, until you click the row again or they leave. A guest joins already following the sharer.
- **The open tabs.** Whoever joins arrives on the files the sharer already has open, with the
  sharer's own tab showing. That's asked once, on arrival — opening a fifth file after that is
  reading for yourself.
- **The terminals**, if the sharer asked for them. Off by default — see
  [Sharing terminals](#sharing-terminals).

Files a guest hasn't opened are read-only until they are: what the mirror holds of one is a
placeholder, not the file. Opening it fetches the text and unlocks it.

## The link

The link carries a 128-bit secret in the fragment: `https://your.app/#live=<secret>`. That secret
is the session's name on the bus, the admission, and — on any transport that leaves the page — the
encryption key. Hand it out the way you'd hand out a password.

It's in the fragment rather than the query — a query is often the host's own page state, and a
fragment is the one part of an address a browser never sends anywhere. A `?live=` is read too, for
a host that would rather write one; the query itself is dropped from the link, since it's the
sharer's own state and none of it is a guest's.

Pass `link` to build the address yourself, for a page served somewhere other than where the
workbench lives:

```ts
live({ link: (id) => `https://example.com/join?live=${id}` });
```

Whatever you build, put the id somewhere that survives being copied — it's the only thing that
opens the session.

### Reloading

The session lives in the address and nowhere else — no storage is used. The sharing tab gets
`#share=<secret>` written into its own fragment, so reloading either end rejoins the same session
on the same side of it, and leaving the session takes it back off. Only one tab of a browser can be
the one sharing: duplicating the sharing tab copies its address exactly, so the copy joins what its
sibling is already sharing instead of opening a second session over the same link.

## The Gateway seam

A `Gateway` is what a session runs over: a topic, a string out, a string in. Two ship —
`broadcastChannelGateway` (the default) and `mqttGateway` — and a third, `p2p`, upgrades whichever
one is running to a direct connection where it can.

The room itself lives in the browser that starts it, not on a gateway: a gateway only delivers
strings to a topic, and who is in the room and what admits them is handled above it, the same way
whichever one answers. Anything that can deliver a string to everyone else on a topic is a
gateway — your own WebSocket fan-out, most usefully:

```ts
import type { Gateway } from "codelet/extensions/live";

const socketGateway = (): Gateway => {
  let socket: WebSocket | undefined;
  let received: (data: string) => void = () => {};
  let closed: (reason: string) => void = () => {};
  return {
    name: "my server",
    // Says this leaves the page, so every frame is sealed before it's sent.
    remote: true,
    open: (topic) =>
      new Promise((resolve) => {
        socket = new WebSocket(`wss://example.com/live/${topic}`);
        socket.onmessage = (event) => received(String(event.data));
        socket.onclose = () => closed("The connection was lost");
        socket.onopen = () => resolve();
      }),
    send: (data) => socket?.send(data),
    onMessage: (handler) => (received = handler),
    onClose: (handler) => (closed = handler),
    close: () => socket?.close(),
  };
};

live({ gateway: socketGateway });
```

`remote: true` is what has every frame sealed before it reaches your server: encrypted with a key
derived from the link's secret, so your server sees ciphertext addressed to a topic name and never
the secret itself. A gateway may also implement `carrying()` — a name, whether it's up, a round
trip — to draw its own row in the pane, and `arm(data)` to prepare the frame that has to go out on
`pagehide`, where there's no time to await anything.

::warning
`crypto.subtle` only exists in a secure context. A page served over plain HTTP — a LAN address
while testing, typically — can't seal anything: the pane warns and shows `not encrypted`, and a
guest on `https` can't join an unencrypted host at all. `broadcastChannelGateway` is never sealed
and doesn't need to be — it never leaves the browser.
::

Under **Connection** the pane draws four pictures derived from the link alone, computed at both
ends and never sent. Matching pictures mean you're in the same session; they say nothing about who
_else_ holds the link.

## Transports

### Sharing between machines

`mqttGateway()` puts the session on public MQTT brokers, still with no server of yours anywhere —
brokers carry sealed frames and know nothing about a session. It publishes to every broker in the
list at once, so two browsers on different brokers still find each other and one broker going down
mid-session costs nothing:

```ts
import { live, mqttGateway, p2p } from "codelet/extensions/live";

live({ gateway: mqttGateway, upgrade: p2p() });
```

| Option    | Type                | Default         | Description                                                                              |
| --------- | ------------------- | --------------- | ---------------------------------------------------------------------------------------- |
| `brokers` | `readonly string[]` | `publicBrokers` | WebSocket broker URLs. A login is `user:pass@` on the address. Any list is joined whole. |

```ts
live({ gateway: () => mqttGateway({ brokers: ["wss://mqtt.example.com:8084/mqtt"] }) });
```

::note
`publicBrokers` are open, rate-limited and nobody's to depend on. A workbench that matters should
point at a broker of its own — no MQTT client is bundled, only the packet shapes a fan-out needs,
so any standard broker works.
::

### Connecting browsers directly

`p2p()` sits over whichever gateway left the page and moves the payload onto WebRTC data channels
between the browsers themselves. It's an upgrade, never a requirement — the session keeps running
on the relay while a connection is negotiated, and a pair that never connects (a symmetric NAT,
say) carries on exactly as it was. A page with no `RTCPeerConnection` gets the gateway back
unchanged, so passing it is always safe.

| Option       | Type             | Default            | Description                                                                                     |
| ------------ | ---------------- | ------------------ | ----------------------------------------------------------------------------------------------- |
| `iceServers` | `RTCIceServer[]` | `publicIceServers` | STUN (and TURN, if you have one) servers. An empty list only ever finds a peer on the same LAN. |

There's no TURN server here, because none would be free — a pair no STUN can get through stays on
the relay. Pass your own if you have one:

```ts
live({
  gateway: mqttGateway,
  upgrade: p2p({ iceServers: [{ urls: "turn:turn.example.com", username: "u", credential: "p" }] }),
});
```

## What you have to provide

For two tabs of one browser, nothing — the default `broadcastChannelGateway` needs no host at all.
To share between machines, you need either:

- **A broker.** `mqttGateway()` works out of the box against public brokers, no signup required.
  Point `brokers` at your own for anything that matters.
- **Your own relay.** Write a `Gateway` (see [above](#the-gateway-seam)) over any WebSocket fan-out
  you already run.

Nothing that reaches a gateway is ever plaintext except over `broadcastChannelGateway`, which never
leaves the browser. What's never sent anywhere at all: the link's secret itself (only its hash and
sealed frames cross a remote gateway), paths matched by `exclude`, and anything in an unopened
file or an unexpanded directory.

## Excluding files

`exclude` is enforced by the browser doing the sharing, everywhere a path could get out — a
listing, a read, a change notification, a rename, a caret's filename, the tab strip a guest arrives
onto. An excluded path a guest asks for is refused, not just left out:

```ts
live({ exclude: ["node_modules", ".env*", "secrets/**"] });
```

Globs are written against the root of the workspace.

## Sharing terminals

Off unless it's asked for, and worth reading before it is:

```ts
live({ terminals: "read" });
```

- `"off"` (default) — no shell crosses, and a guest draws no shared terminal at all.
- `"read"` — the shells this browser has open appear as tabs in the guest's panel, output and all.
  Watching, not typing.
- `"write"` — the guest's keystrokes reach the shell too.

::warning
`exclude` is a workspace rule, and none of it reaches a terminal. A shell is not a path — it's
every path at once and the machine around it, so sharing one hands whoever holds the link a `cat`
of any excluded file, whatever credentials are on that machine, and the network the browser is
sitting in. There's no narrower version of this to offer.
::

This needs [the terminal extension](/extensions/terminal) with a shell in it — what crosses is the
sharer's own terminal tabs. A guest gets a tab per shell, named for whoever is sharing it; closing
it is done reading, not the shell ending.

## What it does not do

- **No admission and no read-only guest.** The link is the whole of the access control, and
  everyone holding it can edit.
- **No reconnecting.** A gateway that drops ends the session; joining again is a fresh seat on the
  same link, as long as the sharer is still there.
- **No multi-root.** The shared workspace shows in place of the reader's own, and a path is the
  sharer's own path at both ends.
- **The sharer is the session.** There's nothing on a server to outlive the tab that started it:
  closing it ends the session for everybody.

:read-more{to="/extensions/terminal"}
