Skip to main content

Fan a Command to Many Panes

Run one command across many terminals in a single call — either from an agent over MCP (pass execute_command an array of terminal ids) or from your own tooling over the REST batch endpoint. This tutorial walks the whole loop: open the panes, discover their ids, fan the command out, and read the results.

By the end you'll have three panes running the same git pull && bun install and a single response object telling you which panes succeeded.

Before you start

  • TermFlow 0.1.0 running, with its API on http://127.0.0.1:42031 and the MCP sidecar on http://127.0.0.1:42032/mcp (the default prod ports).
  • To do this over MCP, an agent already connected to TermFlow's MCP server. If you haven't wired one up yet, see Connect Claude Code end-to-end.
Auth in localhost mode

In the default localhost-only mode every endpoint is unauthenticated — this is safe because the server is bound to 127.0.0.1. You only need a bearer token once you turn on Expose on local network in Settings → Connections. See Local API & auth for the full model.

The plan

1. Open 3 panes        → verify: 3 terminals listed
2. Get their ids → verify: list_terminals / GET /api/terminals returns them
3. Fan the command out → verify: one response with a per-pane results[] + summary
4. Read each pane → verify: output shows the command ran

Step 1 — Open several panes

You want more than one terminal alive at the same time. Two ways to get there:

  • In the UI: open a tab, then split it with Ctrl+Shift+D (horizontal split — the new pane opens to the right). Split again to get a third. See Split panes for every split gesture.
  • From an agent over MCP: call create_terminal once per pane. Passing direction splits an existing pane instead of opening a fresh tab — horizontal splits to the right, vertical splits to the bottom.

A three-pane tab ends up looking like this:

┌──────────────────────────────────────────────┐
│ tab: repos │
├──────────────┬───────────────┬────────────────┤
│ │ │ │
│ term-a │ term-b │ term-c │
│ ~/api │ ~/web │ ~/worker │
│ │ │ │
└──────────────┴───────────────┴────────────────┘

Each pane is an independent terminal with its own id — that id is what you fan a command to, regardless of how the panes are laid out on screen.

Step 2 — Discover the terminal ids

Over MCP, call list_terminals (it takes no parameters) to get every active terminal. Over REST, GET /api/terminals returns the same set. Grab the id of each pane you want to target.

curl http://127.0.0.1:42031/api/terminals

Note the ids — for this walkthrough call them term-a, term-b, and term-c.

Step 3 — Fan the command out

Option A — over MCP (execute_command with an array)

The execute_command tool accepts a single id or an array of ids for terminalId. Hand it an array and it fans the same command to every terminal in one shot.

ParameterTypeRequiredDefaultNotes
terminalIdstring | string[]YesAn array fans the command out. Ids are de-duplicated, so one pane is never written twice.
commandstringYesThe command sent to every terminal.
cliTypestringNocopilotSubmit-keystroke personality: default, claude, gemini, chatgpt, or copilot.
useBracketedPastebooleanNoWrap the input in bracketed paste (more reliable for long or multi-line input).

Ask your agent to call the tool with an array. The arguments look like this:

{
"terminalId": ["term-a", "term-b", "term-c"],
"command": "git pull && bun install",
"cliType": "default"
}
tip
Use cliType: "default" for plain shell commands

copilot is the default because execute_command is built to drive interactive AI CLIs, which submit on a special key combination. For a plain shell command like git pull you want default, which submits with a normal Enter.

Under the hood the array form calls the REST batch endpoint and hands you back its result verbatim — a per-terminal results array plus a summary:

{
"results": [
{ "terminalId": "term-a", "success": true },
{ "terminalId": "term-b", "success": true },
{ "terminalId": "term-c", "success": false, "error": "Terminal not found" }
],
"summary": { "total": 3, "succeeded": 2, "failed": 1 }
}

An empty array is rejected up front with Error: terminalId array must not be empty.

Option B — over REST (/batch/execute)

If you're building your own orchestrator rather than driving an agent, hit the batch endpoint directly. It takes terminalIds (an array) and a single prompt.

curl -X POST http://127.0.0.1:42031/api/terminals/batch/execute \
-H "Content-Type: application/json" \
-d '{
"terminalIds": ["term-a", "term-b", "term-c"],
"prompt": "git pull && bun install",
"cliType": "default"
}'

The batch always returns 200 with the same { results, summary } shape shown above. A bad id becomes a per-terminal failure ("Terminal not found") — it never blocks the other panes. The submit pattern, however, is validated once up front: an empty list, an empty prompt, or an unknown cliType fails the whole batch with 400. Full field reference lives in Execute & batch.

What actually happens per pane

An honest note: fan-out is fire-and-forget. The call writes the keystrokes into each PTY and returns immediately — a success: true means "the command was typed and submitted," not "the command finished." The response never contains the terminal's output. To see what happened, read each pane in Step 4.

Step 4 — Read each pane's output

Because the response only confirms delivery, read each terminal to observe the result:

  • Over MCP: call get_terminal_output per terminal (terminalId required; lines defaults to 50, offset defaults to 0). The clean, ANSI-stripped text comes back in the raw field.
  • Over REST: GET /api/terminals/:id/output, or subscribe to the WebSocket output stream for live updates.
curl "http://127.0.0.1:42031/api/terminals/term-a/output?lines=50"

Interrupt every pane at once

The same fan-out shape exists for raw input, with no prompt wrapping and no submit keystroke appended — what you send is exactly what the PTYs receive. That makes it the tool for sending a control character to a whole group, for example a Ctrl+C () to stop everything mid-run:

curl -X POST http://127.0.0.1:42031/api/terminals/batch/input \
-H "Content-Type: application/json" \
-d '{ "terminalIds": ["term-a", "term-b", "term-c"], "data": "" }'

It returns the same { results, summary } shape, always with 200.

Next steps