# pi-terminal-mux

终端多路复用器统一抽象层，供 pi 扩展复用。任何涉及终端交互（分屏、发命令、读屏、关屏、等待退出）的插件都应依赖本包，而不是各自重新实现 backend 探测与命令拼装。

一套统一的 surface API 跨 **muxy、cmux、tmux、zellij、wezterm、herdr、otty、orca** 八个后端，探测不到任何后端时自动降级为 **headless**（后台子进程 + 日志文件）。

[English README](./README.md)

## 安装

```bash
npm install pi-terminal-mux
```

## 快速上手

```ts
import {
  isMuxAvailable,
  muxSetupHint,
  createSurface,
  createSurfaceSplit,
  sendCommand,
  sendLongCommand,
  sendEscape,
  readScreen,
  closeSurface,
  pollForExit,
} from "pi-terminal-mux";

if (!isMuxAvailable()) {
  console.warn(muxSetupHint()); // 中英文安装提示，由 pi-extensions-i18n 决定语言
}

// 智能放置：按后端策略分屏 / 堆叠 / 开 tab（headless 时返回 headless surface）
const surface = createSurface("my-agent");

// 长命令自动落脚本文件，避免终端宽度截断
// （默认 Bash；Windows 上显式传 interpreter: "powershell" 切到 PowerShell）
const scriptPath = sendLongCommand(surface, "pi --session abc", {
  scriptPreamble: "export MY_FLAG=1",
});

const tail = readScreen(surface, 50);
sendEscape(surface);
closeSurface(surface);
```

## 后端探测

| 后端 | 探测条件 |
|------|----------|
| muxy | `MUXY_SOCKET_PATH` + `muxy` 命令 |
| cmux | `CMUX_SOCKET_PATH` + `cmux` 命令 |
| tmux | `TMUX` + `tmux` 命令 |
| zellij | `ZELLIJ` / `ZELLIJ_SESSION_NAME` + `zellij` 命令 |
| wezterm | `WEZTERM_UNIX_SOCKET` + `wezterm` 命令 |
| herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` 命令（`tab` 模式还需要 `HERDR_WORKSPACE_ID`） |
| otty | `TERM_PROGRAM=otty` + `otty` 命令 |
| orca | `TERM_PROGRAM=Orca` + `orca` 命令 + Orca runtime 可达 |

默认优先级即上表顺序（muxy 优先）。可用环境变量强制指定后端：

- `PI_TERMINAL_MUX`（推荐）：`muxy | cmux | tmux | zellij | wezterm | herdr | otty | orca`
- `PI_SUBAGENT_MUX`：同上的向后兼容别名

指定的后端运行环境不满足时 `getMuxBackend()` 返回 `null`，不会悄悄降级到其他后端。

### Herdr surface 模式

Herdr 默认保持向后兼容的广度优先分屏模式。设置 `PI_SUBAGENT_HERDR_MODE=tab` 后，每个 subagent 会创建独立后台 Tab；设置为 `split` 可显式选择原有分屏布局。`createSurfaceSplit()` 始终保留显式 pane 分屏语义。

```bash
export PI_SUBAGENT_HERDR_MODE=tab
```

## API 概览

### 统一 surface API（跨后端语义一致）

| 函数 | 说明 |
|------|------|
| `createSurface(name)` | 智能放置新 surface（herdr 默认广度优先分屏，设置 `PI_SUBAGENT_HERDR_MODE=tab` 后每个 surface 创建独立后台 Tab；cmux 首次右分屏后续开 tab、zellij tab 感知平铺/堆叠、muxy/otty/orca 广度优先分屏；orca 缺少 agent handle 时新建 tab），返回 surface 标识 |
| `createSurfaceSplit(name, direction, fromSurface?, options?)` | 指定方向（left/right/up/down）分屏；`options.activate`（仅 wezterm，默认 false）分屏后聚焦新 pane |
| `sendCommand(surface, command)` | 发送命令并回车执行 |
| `sendLongCommand(surface, command, opts?)` | 长命令先写脚本文件再执行；`opts.scriptPreamble` 可注入前置片段；`opts.interpreter`（默认 `"bash"`，Windows 可显式 `"powershell"`）选择脚本运行时；返回脚本路径 |
| `sendEscape(surface)` | 发送一次 ESC |
| `readScreen(surface, lines?, options?)` / `readScreenAsync` | 读取屏幕尾部 N 行；`options.source`（仅 herdr）透传 herdr 读屏来源（如 `"recent_unwrapped"`），其他后端忽略 |
| `closeSurface(surface)` | 关闭 surface |
| `renameSurface(surface, name)` / `renameAgent(surface, name)` | 重命名已知 surface 或 agent 标签 |
| `getRenameCapability(operation, backend?, env?)` | 不执行命令，返回实际重命名目标，或明确的 `unsupported` / `disabled` 能力结果 |
| `renameCurrentTab(title)` / `renameWorkspace(title)` | 执行重命名并返回可判别的 `renamed` / `unsupported` / `disabled` / `failed` 结果 |
| `pollForExit(surface, signal, opts)` | 等待 surface 内进程退出：优先 `.exit` sidecar 文件，其次屏幕 sentinel（`__SUBAGENT_DONE_<code>__`），headless 走子进程 exit |
| `getLastSplitSource()` / `clearLastSplitSource()` | 最近一次分屏的来源 pane（用于 UI 展示） |

各后端的实际目标不同：muxy/zellij 的 tab 重命名作用于 pane；tmux 作用于 window/session；WezTerm 的 workspace 重命名作用于 window；cmux 和 Herdr 提供原生 workspace 重命名；Otty、Orca 没有 workspace 重命名。`resolveTerminalRenameTargets` 是明确 ID 路径，忽略旧环境开关；`getRenameCapability`、`renameCurrentTab`、`renameWorkspace` 保持旧开关行为。Headless 会明确返回 `unsupported`，不再静默成功。

### 探测与工具

`getMuxBackend()`、`isMuxAvailable()`、`isHeadlessMode()`、`muxSetupHint()`、`getAgentPaneId(backend?)`、`backendAgentPaneEnvVar(backend)`、`shellEscape()`、`isFishShell()`、`exitStatusVar()`，以及 zellij 放置规划（`selectZellijPlacement` 等）与 cmux/otty JSON 解析等纯函数，均可直接引用做单元测试。

### 后端原生 API

各后端原生函数也从包入口透出（如 `createHerdrSurface`、`splitHerdrPane`、`readHerdrScreen`、`sendOttyCommand`、`renameOttyTab`、`createOrcaSurface`、`sendOrcaCommand`……），子路径导入亦可：`pi-terminal-mux/mux`、`pi-terminal-mux/herdr`、`pi-terminal-mux/otty`、`pi-terminal-mux/orca`。

## Headless 模式

探测不到任何后端时，`createSurface` 返回 `headless:` 前缀的 surface，`sendLongCommand` 直接 spawn 后台子进程并把输出写入日志文件，`readScreen`/`pollForExit`/`closeSurface` 语义保持不变，调用方无需特判。

## Windows PowerShell 支持

所有平台默认保持 **Bash** 作为脚本运行时，以不变更现有调用方语义。Windows 11 PowerShell/WezTerm/herdr 组合下显式开启：

- **命令提交（wezterm）**：Enter 终止符在 `win32` 用 `\r`、其他平台用 `\n`，PowerShell 输入只会提交一次，不会停在续行提示。
- **长命令（`sendLongCommand`）**：传 `interpreter: "powershell"` 会生成 `.ps1`，mux 通过 `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File <path>` 执行、headless 走 `-Command "& <path>"`。显式 `scriptPath` 原样保留；自动路径按解释器选 `.ps1`/`.sh`。未传 interpreter 时保持既有 Bash command、`.sh` 路径与 `$?` 数值哨兵不变。
- **读屏（`readScreen` / `readScreenAsync`）**：传 `{ source: "recent_unwrapped" }`（仅 herdr）选择 herdr 的软换行合并捕获；不传 options 时 herdr 保持 `recent`，其他后端保持各自读屏语义。

这些都是可选项——既有 Bash 调用方与 `pi-interactive-subagents` 在所有平台继续走默认 Bash 运行时。

## 环境变量

| 变量 | 说明 |
|------|------|
| `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` | 强制指定后端 |
| `PI_SUBAGENT_ZELLIJ_MIN_COLUMNS` / `PI_SUBAGENT_ZELLIJ_MIN_ROWS` | zellij 分屏最小可用尺寸（默认 50×10，不满足时改堆叠） |
| `PI_SUBAGENT_RENAME_TMUX_WINDOW` / `PI_SUBAGENT_RENAME_TMUX_SESSION` | tmux 下旧 `getRenameCapability` / `renameCurrentTab` / `renameWorkspace` 的兼容开关；明确目标解析忽略 |
| `PI_SUBAGENT_HERDR_MODE` | herdr surface 放置模式：`split`（默认）或 `tab` |
| `PI_SUBAGENT_RENAME_HERDR_WORKSPACE` | herdr 下旧 `getRenameCapability` / `renameWorkspace` 的兼容开关；明确目标解析忽略 |
| `PI_EXTENSIONS_LOCALE` | 提示文案语言（`zh-CN` / `en-US` / `auto`），由 pi-extensions-i18n 提供 |

## 设计约束

- **不绑定具体机器**：全部后端通过运行时探测（环境变量 + 命令存在性）选择，零硬编码本机路径；外部 CLI 缺失时按后端逐个降级，最终落到 headless。
- **用户文案国际化**：面向用户的提示走 [pi-extensions-i18n](https://www.npmjs.com/package/pi-extensions-i18n) catalog，中英文齐全。
- **agent pane 锚定**：muxy/herdr/otty/orca 的 agent 自身 pane ID 在模块加载时捕获（`AGENT_MUXY_PANE_ID`、`AGENT_ORCA_TERMINAL_HANDLE` 等），不受用户后续焦点切换影响。

## License

MIT


## 按归属改名

`createSurfaceRenameContext(surface)` 描述启动方可授予子进程的终端改名目标。把返回值序列化为 JSON 放入 `PI_TERMINAL_RENAME_CONTEXT`，**每次启动和恢复都替换继承值**。协议属于 terminal-mux，不属于命名或子代理插件。

`resolveTerminalRenameTargets({ tab, workspace })` 返回明确的目标 ID、`surface`/`shared` 范围，或逐目标跳过/失败结果。`renameTerminalTarget(reference, title)` 对捕获的身份执行改名。调用方决定何时改、改哪些目标；库不生成标题，也不修改 Pi session。

受限子进程不能改 workspace。cmux surface、muxy/zellij pane、Herdr pane 或明确新建的 Herdr tab 可以授予。tmux/WezTerm/Otty/Orca 的分屏不能证明独占 window/tab，改名会跳过，不扩大到共享父目标。普通会话必须能确定自身目标 ID；缺失时不退回当前焦点或第一个 tab。明确目标解析直接使用后端能力，独立于旧改名开关。

协议 JSON 损坏或版本未知会报错，不能退回无限制范围。只有旧子代理身份标志、没有归属协议时，终端改名受限，直到启动方提供归属。此协议用于可信本地进程协作，不是安全沙箱。WezTerm/Otty 创建分屏时不会重命名共享 tab。
