Canopydocs v0.4.7

Services and ports

A service is a long-running process for one worktree: a frontend dev server, an API, a worker. You define the command once per repository, and Canopy runs one copy per worktree on that worktree's own port.

Defining a service#

Per repository, in Settings → Services:

FieldMeaning
idStable identifier (frontend, server). Part of the service key and of the port variable name.
nameDisplay name. Also exposed as a port variable, so ${WT_SERVER_PORT} works for a service named "Server" whatever its id is.
kindweb, server or worker. The web service is what "Open :port" targets and what the popover's globe opens.
commandThe shell command that starts it. Use $PORT for this service's own port.
cwdWorking directory relative to the worktree root. Blank means the worktree root.
basePortThe base for the port formula. Leave it empty for a service that doesn't listen.
envExtra environment variables, KEY=VALUE per line.
A service in Settings, with the derived-port hint under the base port.A service in Settings, with the derived-port hint under the base port.
A service in Settings, with the derived-port hint under the base port.

The service rail#

The service rail: one row holding the services, the database and the custom commands.The service rail: one row holding the services, the database and the custom commands.
The service rail: one row holding the services, the database and the custom commands.

Every chip carries a status dot, the name, the port, live CPU and memory while it runs, and a hover control whose meaning depends on the state:

StatusDotHover control
runninggreenStop
starting / stoppingamber, spinnernothing, it's busy
stoppedfaintStart
errorredRestart

Click the port to open http://localhost:<port>, which only works while it's running. Click the chip for the service detail dialog. The rail scrolls sideways when there are a lot of services, and the custom-command buttons sit outside the scroll area so their menu can escape it.

Service detail#

Service detail: the metric trio with a CPU sparkline, the port override, and Restart.Service detail: the metric trio with a CPU sparkline, the port override, and Restart.
Service detail: the metric trio with a CPU sparkline, the port override, and Restart.

The CPU sparkline shows the last seven samples, kept on the client side since the backend streams point-in-time stats every two seconds. Below it, cpu / mem / uptime for the current sample. If the process has died, the panel is titled Last samples before exit and leads with the failure, its exit code, and the last two error lines from its log.

Port is an explicit override for this service in this worktree. It's validated between 1024 and 65535 and checked against every other worktree's ports, so a clash tells you which branch and service is holding it. Esc reverts the field, and saving restarts the service.

Stop appears while it's running. The primary button is Restart, or Save & restart if you changed the port.

The port formula#

effective port = explicit override (if set)
                 else basePort + (worktree index × 10)

Every worktree has a stable index. The main checkout is always 0, so it keeps the base ports. New worktrees take the lowest free index (1, 2, 3 and so on). Removing a worktree frees its slot and the next worktree reclaims it, which stops derived ports creeping upwards forever. Indices live in state.json per repository, so ports don't shuffle between launches, and index spaces are independent per repository.

An override is per service per worktree and beats the formula.

For a service with basePort: 3000:

WorktreeIndexPort
main checkout03000
second worktree13010
third worktree23020
third removed, a new one created2 (reclaimed)3020

Referring to ports in commands#

VariableWhere it worksValue
$PORTA service's own commandThat service's effective port.
$WT_<ID>_PORTsetup, custom commands, service environmentsThe port of the service with that id, uppercased.
$WT_<NAME>_PORTsameThe port of the service with that name, uppercased with non-alphanumerics as _.
$WM_PORT_<ID>sameBack-compat alias.

A frontend that has to know where the API is:

# the frontend's own command
npm start -- --port $PORT
# and in .worktreemanager.json → provision → .env keys
API_URL = http://localhost:${WT_SERVER_PORT}

The Template variables page has the full list.

Starting and stopping#

ActionWhere
Start or stop one serviceThe rail's hover control, or the service detail dialog.
Restart one serviceService detail, the rail (when it has crashed), or the logs fix bar.
Start or stop everything in a worktreeThe next-action button, the sidebar row's play/stop, the popover row, ⌘K.
Start or stop every worktreeThe overview header, or ⌘K → Start all services / Stop all services.

On start, the command runs through your login shell, so version managers initialise the way they do in a terminal. The worktree's pinned Node (from .nvmrc, .node-version or .tool-versions) is located in your asdf, nvm or fnm installs and put first on PATH. The process gets its own process group (a Job Object on Windows), so stopping it takes the whole child tree with it, and $PORT plus every worktree variable is exported into its environment.

On stop: SIGTERM to the process group, three seconds of grace, then SIGKILL.

Crash recovery#

Spawned process-group ids are written to state.json. If Canopy dies without cleaning up, the next launch sweeps them, checking the recorded start time so an unrelated new process that reused the pid survives. Quitting from the tray or with ⌘Q kills every group before exit.

Only one Canopy instance runs at a time. A second launch focuses the first window and exits, because otherwise instance B's startup sweep would kill instance A's running services and both would race on settings.json.

What each status means#

StatusMeaning
stoppedNot running.
startingSpawned, not yet confirmed up.
runningUp.
stoppingSIGTERM sent, waiting for exit.
errorExited unexpectedly. The exit code is kept and shown in service detail.

A worktree's dot aggregates them: red if anything errored, green if everything runs, amber if something is live, faint otherwise. A worktree with no services reads as idle, not as healthy.

Custom commands#

Not services. One-shot scripts, sitting beside the runtime they operate on.

The first custom command gets its own button; the rest collapse into the Commands menu.The first custom command gets its own button; the rest collapse into the Commands menu.
The first custom command gets its own button; the rest collapse into the Commands menu.

You define them per repository as { label, command, group } in Settings → Commands. The first one renders as its own labelled button and the rest collapse into a Commands popover, with a heading per group and ungrouped commands sitting together without one.

Each runs in the worktree root, on the pinned toolchain, through your login shell, with every provisioning variable available. Only one runs at a time per worktree, since they share the worktree's operation log and a second would interleave its output. Progress shows up as toasts, and the output streams into the worktree's operation log.

Documentation for Canopy 0.4.7. Controls marked coming soon are present in the interface but have no implementation behind them yet.