Codelet logoCodelet

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 ^C sends. bash offers 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; kill in 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.

Read more in Guide > Extensions.