Remote
Back the workbench's file tree with real files on a server, over codelet/server.
Remote puts a real filesystem behind the workbench's tree. It pulls a
codelet/server directory into the tree, pushes your edits back, and applies
whatever the server reports has changed — so the explorer, the command palette, search and every
language server keep reading one synchronous tree. None of them has to know a server exists.
import { Workbench } from "codelet/workbench";
import { remote } from "codelet/extensions/remote";
const workbench = new Workbench({
parent: document.getElementById("app")!,
extensions: [remote({ url: "/api/codelet/" })],
});url is where that server's fetch is mounted. See Server for routing it in
your framework of choice.
#Options
| Option | Type | Default | Description |
|---|---|---|---|
url | string | — | Where the server is mounted. Also the address a binary file is mirrored as. |
connect | "auto" \| "manual" | "auto" | "manual" waits for you to enter a URL instead of connecting on activation. |
transport | (url: string) => Transport | httpTransport | How to talk to the server. Pass websocketTransport to use one socket instead of one request per call. |
name | string | "remote" | Names the extension, its commands and its status bar item. |
root | string | "/" | The subtree of the server's root to mirror. |
workspace | "home" \| "own" | "home" | "home" mounts the server's files into your own tree. "own" opens a tree of its own instead. |
mount | string | "/" | Where root lands in the workbench's tree. Ignored under workspace: "own". |
watch | boolean | true | Follow the server's change feed. Off leaves the tree as loaded until a manual sync. |
preload | "none" \| "all" | "none" | "none" fetches a directory level, or a file's text, only once it's opened. "all" fetches the whole tree and every file's text up front. |
save | "explicit" \| "auto" | "explicit" | "explicit" pushes a file when you save it. "auto" pushes on every change, debounced. |
exclude | string[] | — | Globs never fetched, applied or pushed. Written against the mirror's own root (root on the server, mount in the workbench). |
debounce | number | 400 | Milliseconds an edit waits before it's pushed, when save is "auto". |
#Connecting manually
By default Remote connects the moment it activates. Pass connect: "manual" to wait instead: the
status bar shows a plug icon, and an empty explorer offers a "Remote Connection" prompt asking
where the server is.
remote({ url: "/api/codelet/", connect: "manual" });Whatever URL you enter there replaces url for the rest of the page's life. Click the status bar
item again to connect somewhere else.
#Mounting a subdirectory
root and mount let a server's tree sit inside a larger workbench rather than take it over.
root is the subtree on the server; mount is where it lands in the workbench's own tree,
leaving anything else already loaded untouched:
remote({ url: "/api/codelet/", root: "/src", mount: "/project/src" });Only paths under mount are ever read from or pushed to the server — a file the workbench loaded
some other way is left alone. Give the server and the client the same picture: a mount other
than the root shows up as a folded, empty folder from the first paint, before anything connects.
To show the server's files on their own, with nothing of the reader's own beside them, pass
workspace: "own" instead. It opens a tree of its own — the same mechanism as
codelet.workspace.open() — the moment the server answers, and
closes it when the extension stops. mount still applies inside that tree, but there's nothing
for it to sit beside.
#Excluding files, and loading everything up front
exclude keeps globs out of the mirror entirely: never fetched, never watched, never pushed.
remote({ url: "/api/codelet/", exclude: ["node_modules", "*.lock", ".git"] });By default Remote loads lazily: a directory's children when you expand it, a file's text when you open it. For a small workspace, fetch the whole tree and every file's text in one request instead:
remote({ url: "/api/codelet/", preload: "all" });#Using a WebSocket
Pass websocketTransport to hold one socket open instead of making a request per call:
import { remote, websocketTransport } from "codelet/extensions/remote";
remote({ url: "/api/codelet/", transport: websocketTransport });The server needs a matching WebSocketTransport mounted beside its fetch handler — see
WebSockets on the Server page. One thing still goes over HTTP either
way: a binary file's address, which the media extension resolves as a URL a
socket has no way to give.
#What to know before you ship it
Warning
The protocol carries no authentication. Add a header to every request by wrapping fetch through
httpTransport's own fetch option, and pass the result as transport:
import { httpTransport, remote } from "codelet/extensions/remote";
remote({
url: "/api/codelet/",
transport: (url) =>
httpTransport(url, {
fetch: (input, init) =>
fetch(input, {
...init,
headers: { ...init?.headers, Authorization: `Bearer ${token}` },
}),
}),
});This runs for every request the mirror makes, including the change-watching stream.
Binary files — an image, a video, or anything too large to pull as text — mirror as a URL
rather than as content, which the media extension reads. The mirror never
pushes one of these back, so nothing overwrites the real bytes with a URL. The other direction
does work: an image dropped into the explorer is held as a data: URI, and the mirror sends the
bytes that URI stands for, so what lands on the server is the file itself.
Note
The server's own maxFileSize (4 MiB by default) is smaller than what the workbench accepts from
a drop (16 MiB): a larger file lands in the tree, is refused by the server with ETOOBIG, and
shows up as a row in Problems on that file. Raise maxFileSize on the server if you want those to
land.
Readonly files. A server started readonly, or a file the server can't write, shows locked in the workbench and refuses local edits before a save is even attempted.
Connection status shows in the status bar, labelled with name: connecting, connected, stale
(no change events for a while, though nothing has actually failed) or failed with a reason.
Clicking it runs <name>.reconnect. A <name>.sync command re-fetches everything currently
loaded — reach for it if the server changed while nothing was watching.