Skip to content

Dev servers and ports

ws notices when something in a pane starts listening on a TCP port (a Rails app, a Vite dev server, a docs site, a game’s debug server) and shows the port where you’ll see it. prefix o opens it in your browser, and prefix O lists every one in every workspace. There’s nothing to configure: it works for any server in any pane, whether you started it, a template did, or an agent did.

Handing each workstream its own block of ports may come later.

prefix means Ctrl-Space.

  • On the pane: its frame’s bottom border, on the right, in blue:

A pane with several servers lists them all: :3000 :3036 ↗.

  • On the tab’s chip in the sidebar (current workspace): the first port in the tab, after the PR badge if there is one: Invoice #101 ✓ :3000.
  • In the dev servers list (prefix O): every port in every workspace. See below.
  • In the finder (prefix f): each port is a result, such as :3000 ruby bin/rails server in acme › Invoice #101 › zsh. Type port or ports first to list only them (port 30). Enter goes to the pane.
  • On the Dashboard: under Now, a button per port (:3000 acme › Invoice #101) that goes to its pane, and All dev servers to open the list. It shows up to six; past that, the last button says how many more.
  • For scripts: ws tab list prints the tab’s ports as a fifth column, and ws tab list --json has a ports array. See Scripting.

prefix o opens http://localhost:<port> in your default browser (open on macOS, xdg-open on Linux), and the top bar says opened http://localhost:3000.

  • If the focused pane listens on one port, that one opens.
  • If it listens on none, ws looks at the whole tab: with one port in the tab, that one opens.
  • With several to choose from, a menu lists them with the pane each is in; pick one with j/k and Enter.
  • With none in the tab: nothing in this tab is listening on a port. When other tabs have some, it adds other tabs have some (C-Space O lists them all).

The pane’s right-click menu (on its top border) also has Open http://localhost:3000, one item per port.

prefix O opens the dev servers list: every port anything in any pane listens on, in every workspace and tab, not only the one you’re in.

╭─ dev servers · 3 ──────────────────────────────────────────────────────╮
│ :3000 acme › Invoice #101 › rails ruby bin/rails server -p 3000 │
│ :5173 acme › Invoice #101 › vite node ~/acme/node_modules/.bin/vite │
│ :8765 docs › 2 › zsh python3 -m http.server 8765 │
╰────────────────────────────────────────────────────────────────────────╯

Each row is a port, where it is (workspace › tab › pane, named as the sidebar and tab bar name them) and what’s listening: the command line of the process that holds the port, shortened to fit (the program by its name, not its full path, and your home directory as ~). The list is sorted by port and keeps up while it’s open, so a server that starts or stops shows within about 3 seconds. It opens on the focused pane’s first port, if it has one.

Keys Action
j / ↓, k / ↑ Move
g / Home, G / End First, last
Enter Go to the pane: its workspace, tab and the pane itself
o Open http://localhost:<port> in your browser; the list stays open
Esc / q Close

A click on a row goes to its pane, the mouse wheel moves the selection, and a click outside closes the list. The general menu (right-click the top bar or empty sidebar) has Dev servers… too.

To use another key, bind the ports command in keys.toml (or with ws keys), as with any command. See Custom keys.

Every 3 seconds, ws looks at each pane’s process tree: the pane’s program, its foreground job and everything they started. It asks the OS which of those processes have a TCP socket in the listening state:

  • macOS: lsof -nP -a -iTCP -sTCP:LISTEN -p <pids>.
  • Linux: each process’s /proc/<pid>/fd sockets, matched against the listening entries in /proc/net/tcp and /proc/net/tcp6.

The scan runs on a background thread, so the screen never waits on it. A server shows up within about 3 seconds of starting to listen and disappears within 3 seconds of stopping.

Some ports are left out because they’re not something to open:

  • ports 49152 and above (the OS’s ephemeral range, which tools such as language servers and agents use internally);
  • Node’s inspector ports, 9229 and 9230.
  • Collisions aren’t prevented. If two workstreams both start bin/rails s on port 3000, the second still fails with “address already in use”. The first one’s pane and chip show :3000, which tells you where it is.
  • Only TCP listeners on this machine. A server inside Docker shows the port only if the process holding it (docker-proxy, or the VM’s port forwarder) is in the pane’s process tree, which usually it isn’t.
  • URLs are always http://localhost:<port>. A server that only answers HTTPS, or on a path, opens at the root over HTTP.
  • Restore re-runs the pane’s command as before; the port reappears once the server listens again.