@kevinhao/pi-terminal-mux
Terminal surface API for tmux and Orca, with native startup commands and headless fallback
Package details
Install @kevinhao/pi-terminal-mux from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@kevinhao/pi-terminal-mux- Package
@kevinhao/pi-terminal-mux- Version
0.7.0- Published
- Oct 29, 2026
- Downloads
- 287/mo · 249/wk
- Author
- kevinhao
- License
- MIT
- Types
- extension
- Size
- 88.6 KB
- Dependencies
- 1 dependency · 0 peers
Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@kevinhao/pi-terminal-mux
Terminal surface API for Pi extensions, supporting tmux, Orca, and headless background processes.
Install
npm install @kevinhao/pi-terminal-mux
Create and start a command
import { createSurface, createSurfaceSplit, readScreen, closeSurface } from "@kevinhao/pi-terminal-mux";
const surface = createSurface("my-agent", {
command: "pi --session /tmp/child.jsonl",
});
const sibling = createSurfaceSplit("review", "right", surface, {
command: "pi --session /tmp/review.jsonl",
});
const tail = readScreen(surface, 50);
closeSurface(sibling);
closeSurface(surface);
command is an optional, non-empty shell command string, including its arguments. Quote dynamic values with shellEscape() for POSIX shells. There is no separate sendCommand() call or library-side shell-ready sleep on this path:
- tmux: supplies the command to
tmux split-windowwhile creating the pane. - Orca: supplies
--commandtoterminal createorterminal split. Orca itself starts a shell and submits the command; this is not a direct argv process spawn. - Headless: starts the command with
bash -c, writing stdout/stderr to a log file.
Returning a handle confirms creation/submission, not application readiness or successful completion. Use your application's ready signal, logs, or exit protocol for that. Creation errors propagate to the caller. Orca input/read errors also propagate instead of appearing successful.
Existing calls without options keep their previous behavior:
import { createSurface, sendLongCommand } from "@kevinhao/pi-terminal-mux";
const surface = createSurface("my-agent");
const scriptPath = sendLongCommand(surface, "pi --session /tmp/child.jsonl", {
scriptPreamble: "export MY_FLAG=1",
});
sendLongCommand writes a script first and then submits bash <script> to the terminal. It remains available for existing callers and long commands. Do not also submit the startup command after passing it to createSurface, or it will run twice.
Detection
| Backend | Requirements |
|---|---|
| tmux | TMUX environment variable and the tmux executable |
| Orca | TERM_PROGRAM=Orca, the orca executable, and a reachable runtime |
| Headless | Neither terminal backend is available |
Automatic detection prefers tmux, then Orca. Force a backend with PI_TERMINAL_MUX=tmux|orca; PI_SUBAGENT_MUX remains a compatibility alias. An unavailable forced backend does not select another terminal backend. Unknown or removed backend preferences throw an explicit error.
API
| Function | Behavior |
|---|---|
createSurface(name, options?) |
tmux and Orca both breadth-first split from the caller's pane into a grid; Orca opens a new tab without a caller handle; headless fallback. options.command starts work during creation |
createSurfaceSplit(name, direction, fromSurface?, options?) |
Explicit split; direction is left/right/up/down, options.command is optional. Requires tmux or Orca; headless has no pane layout |
sendCommand(surface, command) |
Send input followed by Enter to an existing terminal |
sendLongCommand(surface, command, options?) |
Write a script, then execute it. Supports scriptPath, scriptPreamble, and `interpreter: "bash" |
sendEscape(surface) |
Send Escape; headless uses SIGINT |
readScreen(surface, lines?) / readScreenAsync |
Read terminal output or headless logs |
closeSurface(surface) |
Close a pane or headless process |
renameSurface(surface, name) |
Rename the backend's corresponding window/tab |
getLastSplitSource() / clearLastSplitSource() |
Track the most recent split source |
pollForExit(surface, signal, options) |
Wait for an .exit sidecar, sentinel, or headless process exit |
Types CreateSurfaceOptions and CreateSurfaceSplitOptions both expose command?: string. The existing first three split arguments are unchanged.
Detection helpers include getMuxBackend, isMuxAvailable, isHeadlessMode, muxSetupHint, getAgentPaneId, and backendAgentPaneEnvVar. Shell helpers include shellEscape, isFishShell, and exitStatusVar.
The shared API is also exported through @kevinhao/pi-terminal-mux/mux; Orca-native helpers are exported through @kevinhao/pi-terminal-mux/orca.
Naming ownership
createSurfaceRenameContext(surface) produces the PI_TERMINAL_RENAME_CONTEXT protocol for a child launcher. Replace inherited context on every launch and resume.
A child pane does not prove exclusive ownership of its tmux window or Orca tab. Child contexts therefore grant neither shared window/tab renaming nor workspace renaming. Invalid contexts never fall back to parent permissions.
For ordinary sessions, resolveTerminalRenameTargets({ tab, workspace }) resolves explicit identities, and renameTerminalTarget(reference, title) operates on those captured targets, never on UI focus:
- tmux: window for
tab, session forworkspace. - Orca: terminal tab for
tab;workspaceis unsupported. - Headless: naming is unsupported.
Legacy renameCurrentTab / renameWorkspace return discriminated success, unsupported, disabled, or failed results. tmux's legacy operations require PI_SUBAGENT_RENAME_TMUX_WINDOW=1 / PI_SUBAGENT_RENAME_TMUX_SESSION=1; explicit-ID resolution ignores those switches.
Migration
Only tmux and Orca remain in MuxBackend. Other backend implementations, native exports/subpaths, WezTerm's activate option, and Herdr's read-source options have been removed. Update old backend preferences and imports. This is a breaking reduction of the supported backend/API surface; tmux/Orca calls without startup options remain compatible.
No local executable paths are hardcoded. Orca's caller handle is captured at module load so later UI focus changes cannot redirect splits. Headless keeps the existing Bash default; sendLongCommand({ interpreter: "powershell" }) explicitly selects PowerShell where available.
License
MIT