
# Ports

`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.

```ts
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`](/extensions/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`](/extensions/scm) has, where one extension
writes the list and another draws it.

[`bash`](/extensions/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`](/extensions/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:

```ts
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{to="/guide/extensions"}
