Ports
A panel tab listing every server running in the workbench, with a way to open or copy each one.
ports adds a Ports tab to the panel, after the terminal: one row per server anything in the
workbench has published, with the address it's actually reachable at beside it. Click a row to open
it in a window, or use the row's menu to copy the address.
import { Workbench, FileSystem } from "codelet/workbench";
import { ports } from "codelet/extensions/ports";
import { terminal } from "codelet/extensions/terminal";
import { bash } from "codelet/extensions/bash";
new Workbench({
parent: document.getElementById("app")!,
fs: new FileSystem({ "/package.json": '{ "scripts": { "dev": "vite" } }' }),
extensions: [ports, terminal(), bash()],
});Note
The Ports tab only appears while something is listening. Mount ports on its own with nothing
publishing a port and the tab never shows up — that's expected, not a bug. It draws a registry, and
an empty registry means nothing to draw a tab for.
#What it adds
- A Ports tab in the panel, one row per running server: the port number, the address it's reachable at, and the name of whatever published it where more than one thing could have.
- Open in Window on a row — the same window a port opening throws up by itself, for one you closed or one that never opened one.
- Copy Address, for pasting the real address into a browser or a
curl. Only on a page with a clipboard, which means a secure context. - Stop Server, on a row whose publisher offered a way to end it — the same abort
^Csends.bashoffers one when it can tell which command opened the port, which is whenever a single terminal is busy. Run two long commands at once and the rows that come up while both are running carry no Stop rather than a Stop that might end the wrong one;killin the terminal always works.
All three live in the row's context menu — right-click a port.
#Where the ports come from
ports publishes nothing of its own. It draws whatever has been registered on codelet.ports,
the workbench's own registry — the same shape scm has, where one extension
writes the list and another draws it.
bash is the built-in that fills it. On a cross-origin isolated page it boots
a real machine, and every port a server opens in there lands in this tab. A server there isn't at
the localhost:3000 its command printed — the machine has an address of its own — which is most of
why the list is worth having.
#The same list in the terminal
The tab isn't the only thing reading the registry. bash registers netstat,
ss, lsof -i, nc -z and fuser over it, so netstat -tlnp in a terminal lists exactly the
rows this tab draws — including ports published by something other than that shell. fuser -k 3000/tcp is the Stop gesture from a command line. You don't need this extension for those: they
read the registry, and so does the tab.
Anything else can publish too:
import { defineExtension } from "codelet/extensions";
export const myServers = defineExtension({
manifest: { name: "my-servers", displayName: "My Servers" },
activate(context, vscode, codelet) {
const port = codelet.ports.register({
port: 3000,
address: "https://tunnel.example.com",
label: "remote",
show: () => void vscode.env.openExternal(vscode.Uri.parse("https://tunnel.example.com")),
});
// `open` is the registry's answer about the number, not yours: "preview" for a port the
// reader chose, "silent" for one a tool picked. Read it to decide whether to raise a window.
if (port.open === "preview") port.show?.();
// The address is writable, and writing it repaints the row. A server that came back
// somewhere else is one line, not a re-registration.
context.subscriptions.push({ dispose: () => port.dispose() });
},
});port.open is "preview" below port 10000 and "silent" at or above it: five digits is what a
tool picks when it didn't want to be looked at — an HMR socket, an inspector, a test runner's ipc
— and a server you started yourself is a port you chose, which is four. Both kinds are listed
either way; the difference is only whether a window is raised for it.