Codelet logoCodelet

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

OptionTypeDefaultDescription
urlstringWhere 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) => TransporthttpTransportHow to talk to the server. Pass websocketTransport to use one socket instead of one request per call.
namestring"remote"Names the extension, its commands and its status bar item.
rootstring"/"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.
mountstring"/"Where root lands in the workbench's tree. Ignored under workspace: "own".
watchbooleantrueFollow 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.
excludestring[]Globs never fetched, applied or pushed. Written against the mirror's own root (root on the server, mount in the workbench).
debouncenumber400Milliseconds 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.

Read more in Guide > Server.