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:42031and the MCP sidecar onhttp://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.
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_terminalonce per pane. Passingdirectionsplits an existing pane instead of opening a fresh tab —horizontalsplits to the right,verticalsplits 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.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
terminalId | string | string[] | Yes | — | An array fans the command out. Ids are de-duplicated, so one pane is never written twice. |
command | string | Yes | — | The command sent to every terminal. |
cliType | string | No | copilot | Submit-keystroke personality: default, claude, gemini, chatgpt, or copilot. |
useBracketedPaste | boolean | No | — | Wrap 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"
}
cliType: "default" for plain shell commandscopilot 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: truemeans "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_outputper terminal (terminalIdrequired;linesdefaults to 50,offsetdefaults to 0). The clean, ANSI-stripped text comes back in therawfield. - 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
- Execute & batch — the complete field, precedence, and error reference behind these calls.
- Build an orchestrator on REST — turn this pattern into a real multi-terminal driver.