pi-terminal-mux 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-terminal-mux
2
2
 
3
- Terminal multiplexer abstraction for pi extensions — one unified surface API across **muxy, cmux, tmux, zellij, wezterm, herdr and otty**, with automatic **headless fallback** (background child process + log file) when no multiplexer is detected.
3
+ Terminal multiplexer abstraction for pi extensions — one unified surface API across **muxy, cmux, tmux, zellij, wezterm, herdr, otty and orca**, with automatic **headless fallback** (background child process + log file) when no multiplexer is detected.
4
4
 
5
5
  Any pi extension that needs terminal interaction (splitting panes, sending commands, reading screens, closing panes, waiting for process exit) should depend on this package instead of re-implementing backend detection and command assembly.
6
6
 
@@ -57,10 +57,11 @@ closeSurface(surface);
57
57
  | wezterm | `WEZTERM_UNIX_SOCKET` + `wezterm` command |
58
58
  | herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` command |
59
59
  | otty | `TERM_PROGRAM=otty` + `otty` command |
60
+ | orca | `TERM_PROGRAM=Orca` + `orca` command + reachable Orca runtime |
60
61
 
61
62
  Default priority follows the table order (muxy first). Force a backend with:
62
63
 
63
- - `PI_TERMINAL_MUX` (preferred): `muxy | cmux | tmux | zellij | wezterm | herdr | otty`
64
+ - `PI_TERMINAL_MUX` (preferred): `muxy | cmux | tmux | zellij | wezterm | herdr | otty | orca`
64
65
  - `PI_SUBAGENT_MUX`: backward-compatible alias
65
66
 
66
67
  If the forced backend's runtime is unavailable, `getMuxBackend()` returns `null` — it never silently falls back to another backend.
@@ -71,7 +72,7 @@ If the forced backend's runtime is unavailable, `getMuxBackend()` returns `null`
71
72
 
72
73
  | Function | Description |
73
74
  |----------|-------------|
74
- | `createSurface(name)` | Smart placement (cmux: first right-split then tabs; zellij: tab-aware tiled/stacked; muxy/otty: breadth-first splits), returns a surface handle |
75
+ | `createSurface(name)` | Smart placement (cmux: first right-split then tabs; zellij: tab-aware tiled/stacked; muxy/otty: breadth-first splits; orca: new tab in the current worktree), returns a surface handle |
75
76
  | `createSurfaceSplit(name, direction, fromSurface?)` | Split in an explicit direction (left/right/up/down) |
76
77
  | `sendCommand(surface, command)` | Send a command and press Enter |
77
78
  | `sendLongCommand(surface, command, opts?)` | Write long commands to a script file first; `opts.scriptPreamble` injects env exports; returns the script path |
@@ -88,7 +89,7 @@ If the forced backend's runtime is unavailable, `getMuxBackend()` returns `null`
88
89
 
89
90
  ### Backend-native APIs
90
91
 
91
- Backend-native functions are also re-exported (e.g. `createHerdrSurface`, `splitHerdrPane`, `readHerdrScreen`, `sendOttyCommand`, `renameOttyTab`, ...). Subpath imports are available too: `pi-terminal-mux/mux`, `pi-terminal-mux/herdr`, `pi-terminal-mux/otty`.
92
+ Backend-native functions are also re-exported (e.g. `createHerdrSurface`, `splitHerdrPane`, `readHerdrScreen`, `sendOttyCommand`, `renameOttyTab`, `createOrcaSurface`, `sendOrcaCommand`, ...). Subpath imports are available too: `pi-terminal-mux/mux`, `pi-terminal-mux/herdr`, `pi-terminal-mux/otty`, `pi-terminal-mux/orca`.
92
93
 
93
94
  ## Headless mode
94
95
 
@@ -108,7 +109,7 @@ When no backend is detected, `createSurface` returns a `headless:`-prefixed surf
108
109
 
109
110
  - **No machine coupling**: every backend is selected via runtime detection (env vars + command availability); no hardcoded local paths; missing CLIs degrade backend-by-backend down to headless.
110
111
  - **Localized user-facing text**: setup hints go through the [pi-extensions-i18n](https://www.npmjs.com/package/pi-extensions-i18n) catalog with complete `zh-CN` and `en-US` entries.
111
- - **Agent pane anchoring**: the agent's own pane ID on muxy/herdr/otty is captured at module load (`AGENT_MUXY_PANE_ID` etc.), immune to later focus switches.
112
+ - **Agent pane anchoring**: the agent's own pane ID on muxy/herdr/otty/orca is captured at module load (`AGENT_MUXY_PANE_ID`, `AGENT_ORCA_TERMINAL_HANDLE` etc.), immune to later focus switches.
112
113
 
113
114
  ## License
114
115
 
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  终端多路复用器统一抽象层,供 pi 扩展复用。任何涉及终端交互(分屏、发命令、读屏、关屏、等待退出)的插件都应依赖本包,而不是各自重新实现 backend 探测与命令拼装。
4
4
 
5
- 一套统一的 surface API 跨 **muxy、cmux、tmux、zellij、wezterm、herdr、otty** 七个后端,探测不到任何后端时自动降级为 **headless**(后台子进程 + 日志文件)。
5
+ 一套统一的 surface API 跨 **muxy、cmux、tmux、zellij、wezterm、herdr、otty、orca** 八个后端,探测不到任何后端时自动降级为 **headless**(后台子进程 + 日志文件)。
6
6
 
7
7
  [English README](./README.md)
8
8
 
@@ -56,10 +56,11 @@ closeSurface(surface);
56
56
  | wezterm | `WEZTERM_UNIX_SOCKET` + `wezterm` 命令 |
57
57
  | herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` 命令 |
58
58
  | otty | `TERM_PROGRAM=otty` + `otty` 命令 |
59
+ | orca | `TERM_PROGRAM=Orca` + `orca` 命令 + Orca runtime 可达 |
59
60
 
60
61
  默认优先级即上表顺序(muxy 优先)。可用环境变量强制指定后端:
61
62
 
62
- - `PI_TERMINAL_MUX`(推荐):`muxy | cmux | tmux | zellij | wezterm | herdr | otty`
63
+ - `PI_TERMINAL_MUX`(推荐):`muxy | cmux | tmux | zellij | wezterm | herdr | otty | orca`
63
64
  - `PI_SUBAGENT_MUX`:同上的向后兼容别名
64
65
 
65
66
  指定的后端运行环境不满足时 `getMuxBackend()` 返回 `null`,不会悄悄降级到其他后端。
@@ -70,7 +71,7 @@ closeSurface(surface);
70
71
 
71
72
  | 函数 | 说明 |
72
73
  |------|------|
73
- | `createSurface(name)` | 智能放置新 surface(cmux 首次右分屏后续开 tab、zellij tab 感知平铺/堆叠、muxy/otty 广度优先分屏),返回 surface 标识 |
74
+ | `createSurface(name)` | 智能放置新 surface(cmux 首次右分屏后续开 tab、zellij tab 感知平铺/堆叠、muxy/otty 广度优先分屏、orca 在当前 worktree 新建 tab),返回 surface 标识 |
74
75
  | `createSurfaceSplit(name, direction, fromSurface?)` | 指定方向(left/right/up/down)分屏 |
75
76
  | `sendCommand(surface, command)` | 发送命令并回车执行 |
76
77
  | `sendLongCommand(surface, command, opts?)` | 长命令先写脚本文件再执行;`opts.scriptPreamble` 可注入 env export;返回脚本路径 |
@@ -87,7 +88,7 @@ closeSurface(surface);
87
88
 
88
89
  ### 后端原生 API
89
90
 
90
- 各后端原生函数也从包入口透出(如 `createHerdrSurface`、`splitHerdrPane`、`readHerdrScreen`、`sendOttyCommand`、`renameOttyTab`……),子路径导入亦可:`pi-terminal-mux/mux`、`pi-terminal-mux/herdr`、`pi-terminal-mux/otty`。
91
+ 各后端原生函数也从包入口透出(如 `createHerdrSurface`、`splitHerdrPane`、`readHerdrScreen`、`sendOttyCommand`、`renameOttyTab`、`createOrcaSurface`、`sendOrcaCommand`……),子路径导入亦可:`pi-terminal-mux/mux`、`pi-terminal-mux/herdr`、`pi-terminal-mux/otty`、`pi-terminal-mux/orca`。
91
92
 
92
93
  ## Headless 模式
93
94
 
@@ -107,7 +108,7 @@ closeSurface(surface);
107
108
 
108
109
  - **不绑定具体机器**:全部后端通过运行时探测(环境变量 + 命令存在性)选择,零硬编码本机路径;外部 CLI 缺失时按后端逐个降级,最终落到 headless。
109
110
  - **用户文案国际化**:面向用户的提示走 [pi-extensions-i18n](https://www.npmjs.com/package/pi-extensions-i18n) catalog,中英文齐全。
110
- - **agent pane 锚定**:muxy/herdr/otty 的 agent 自身 pane ID 在模块加载时捕获(`AGENT_MUXY_PANE_ID` 等),不受用户后续焦点切换影响。
111
+ - **agent pane 锚定**:muxy/herdr/otty/orca 的 agent 自身 pane ID 在模块加载时捕获(`AGENT_MUXY_PANE_ID`、`AGENT_ORCA_TERMINAL_HANDLE` 等),不受用户后续焦点切换影响。
111
112
 
112
113
  ## License
113
114
 
package/locales/mux.json CHANGED
@@ -31,9 +31,13 @@
31
31
  "zh-CN": "请在 Otty 中运行 pi(Otty 会自动设置 TERM_PROGRAM=otty)。",
32
32
  "en-US": "Run pi inside Otty (Otty sets TERM_PROGRAM=otty automatically)."
33
33
  },
34
+ "setupHint.orca": {
35
+ "zh-CN": "请在 Orca 中运行 pi(Orca 会自动设置 TERM_PROGRAM=Orca 和 ORCA_TERMINAL_HANDLE)。",
36
+ "en-US": "Run pi inside Orca (Orca sets TERM_PROGRAM=Orca and ORCA_TERMINAL_HANDLE automatically)."
37
+ },
34
38
  "setupHint.generic": {
35
- "zh-CN": "请在 Muxy、cmux(`cmux pi`)、tmux(`tmux new -A -s pi 'pi'`)、zellij(`zellij --session pi`,然后运行 `pi`)、WezTerm、herdr(运行 `herdr`,拆分 pane 后在其中运行 `pi`)或 Otty(Otty 会自动设置 TERM_PROGRAM=otty)中启动 pi。",
36
- "en-US": "Start pi inside Muxy, cmux (`cmux pi`), tmux (`tmux new -A -s pi 'pi'`), zellij (`zellij --session pi`, then run `pi`), WezTerm, herdr (run `herdr`, split a pane, then run `pi` in it), or Otty (Otty sets TERM_PROGRAM=otty automatically)."
39
+ "zh-CN": "请在 Muxy、cmux(`cmux pi`)、tmux(`tmux new -A -s pi 'pi'`)、zellij(`zellij --session pi`,然后运行 `pi`)、WezTerm、herdr(运行 `herdr`,拆分 pane 后在其中运行 `pi`)、Otty(Otty 会自动设置 TERM_PROGRAM=otty)或 Orca(Orca 会自动设置 TERM_PROGRAM=Orca)中启动 pi。",
40
+ "en-US": "Start pi inside Muxy, cmux (`cmux pi`), tmux (`tmux new -A -s pi 'pi'`), zellij (`zellij --session pi`, then run `pi`), WezTerm, herdr (run `herdr`, split a pane, then run `pi` in it), Otty (Otty sets TERM_PROGRAM=otty automatically), or Orca (Orca sets TERM_PROGRAM=Orca automatically)."
37
41
  },
38
42
  "setupHint.herdrPreferred": {
39
43
  "zh-CN": "请在 herdr 中启动 pi(必须设置 HERDR_ENV=1;先运行 herdr,再在 pane 中启动 pi)。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-terminal-mux",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal multiplexer abstraction for pi extensions — unified surface API across muxy, cmux, tmux, zellij, wezterm, herdr and otty, with headless fallback",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -9,7 +9,8 @@
9
9
  "./package.json": "./package.json",
10
10
  "./mux": "./src/mux.ts",
11
11
  "./herdr": "./src/herdr.ts",
12
- "./otty": "./src/otty.ts"
12
+ "./otty": "./src/otty.ts",
13
+ "./orca": "./src/orca.ts"
13
14
  },
14
15
  "files": [
15
16
  "index.ts",
@@ -0,0 +1,380 @@
1
+ /**
2
+ * orca.ts — Orca 终端后端(https://orca.dev,Stably 的 agent 工作台)
3
+ *
4
+ * Orca 是面向 agent 的桌面工作台,通过 `orca` CLI 提供 terminal 编程化控制:
5
+ * - `orca terminal create --title <name> --json` 新建 tab(不抢焦点)
6
+ * - `orca terminal split --terminal <h> --direction horizontal|vertical --json`
7
+ * - `orca terminal send --terminal <h> --text <text> [--enter]`
8
+ * - `orca terminal read --terminal <h> --limit <N> --json`
9
+ * - `orca terminal close --terminal <h> [--tab]`
10
+ * - `orca terminal rename --terminal <h> --title <name>`
11
+ * - `orca terminal list --json` / `orca status --json`
12
+ *
13
+ * 所有 JSON 响应共享信封:{ id, ok, result, _meta }。
14
+ * - create → result.terminal.handle
15
+ * - split → result.split.handle
16
+ * - read → result.terminal.tail(字符串数组,最新在尾部)
17
+ *
18
+ * 与其他 backend 的关键差异:
19
+ * 1. Orca 注入 ORCA_TERMINAL_HANDLE(类似 cmux 的 CMUX_SURFACE_ID),
20
+ * 模块加载时冻结为 AGENT_ORCA_TERMINAL_HANDLE。
21
+ * 2. create() 走"新 tab"而非分屏:Orca 是 worktree 中心的工作台,
22
+ * 官方建议 agent 用 `terminal create` 开新终端;不拆分 agent 自己的
23
+ * pane,避免压缩 pi 的 UI。因此不使用 shared.ts 的 BFS 分屏状态机。
24
+ * 3. Escape 通过发送裸 ESC(`\u001b`,即 `0x1b`)字节实现,TUI 会将其解释为 Escape 键。
25
+ * 4. rename 作用于 tab 标题;split 出来的 pane 与源 pane 共享 tab,
26
+ * 所以 createSplit 不做 rename(否则会把 agent 所在 tab 一起改名)。
27
+ * 5. split 方向只有 horizontal|vertical。实测(Orca 1.4.174):
28
+ * horizontal = 水平分割线 = 上下堆叠,vertical = 垂直分割线 = 左右并排。
29
+ * 因此映射 left/right→vertical、up/down→horizontal。
30
+ *
31
+ * 日志、命令检测复用 backends/shared.ts。
32
+ */
33
+
34
+ import { spawnSync } from "node:child_process";
35
+ import { createBackendLogger, hasCommand } from "./shared.ts";
36
+ import type { BackendOps } from "./types.ts";
37
+
38
+ const ORCA_RUNTIME_CHECK_TIMEOUT_MS = 1_500;
39
+ const ORCA_COMMAND_TIMEOUT_MS = 10_000;
40
+
41
+ // ── 日志(统一格式,写入 /tmp/pi-mux-orca.log) ──
42
+ const orcaLog = createBackendLogger("orca", "/tmp/pi-mux-orca.log");
43
+
44
+ // ── Orca 检测 ──
45
+
46
+ /**
47
+ * 检测 orca backend 是否可用:
48
+ * 1. 当前进程在 Orca 终端内运行(TERM_PROGRAM=Orca)
49
+ * 2. `orca` 命令在 PATH 中
50
+ * 3. Orca runtime 可达(`orca status --json` 返回 ok:true,带 1.5s 超时)
51
+ *
52
+ * 不要求 ORCA_TERMINAL_HANDLE:create/read/send 等操作都针对显式 handle,
53
+ * 只有 createSplit 的默认 split 源需要它(缺失时由 splitOrcaTerminal 报错)。
54
+ */
55
+ export function isOrcaRuntimeAvailable(): boolean {
56
+ if (process.env.TERM_PROGRAM !== "Orca") return false;
57
+ if (!hasCommand("orca")) return false;
58
+
59
+ // 最后一道闸:CLI 存在但 runtime 未启动时 `orca status` 会失败或 hang。
60
+ // spawnSync timeout 兜底,避免探测卡死主流程。
61
+ try {
62
+ const result = spawnSync("orca", ["status", "--json"], {
63
+ encoding: "utf8",
64
+ stdio: ["ignore", "pipe", "pipe"],
65
+ timeout: ORCA_RUNTIME_CHECK_TIMEOUT_MS,
66
+ });
67
+ if (result.error || result.status !== 0) return false;
68
+ const parsed = parseOrcaJson(result.stdout ?? "");
69
+ return parsed?.ok === true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // ── Agent terminal handle ──
76
+
77
+ /**
78
+ * 捕获于模块加载时的 agent terminal handle。
79
+ * Orca 注入 ORCA_TERMINAL_HANDLE(类似 cmux 注入 CMUX_SURFACE_ID),
80
+ * 冻结到常量,不受用户后续切换 tab 影响。
81
+ */
82
+ export const AGENT_ORCA_TERMINAL_HANDLE: string | null = process.env.ORCA_TERMINAL_HANDLE ?? null;
83
+
84
+ // ── JSON 信封解析(纯函数,可单测) ──
85
+
86
+ /** orca CLI 的 JSON 响应信封 */
87
+ export interface OrcaEnvelope {
88
+ ok?: boolean;
89
+ result?: Record<string, unknown>;
90
+ }
91
+
92
+ export function parseOrcaJson(output: string): OrcaEnvelope | null {
93
+ const trimmed = output.trim();
94
+ if (!trimmed) return null;
95
+ try {
96
+ const parsed = JSON.parse(trimmed) as OrcaEnvelope;
97
+ if (typeof parsed !== "object" || parsed === null) return null;
98
+ return parsed;
99
+ } catch (e) {
100
+ orcaLog(`[parse json] failed: ${(e as Error).message} raw=${JSON.stringify(trimmed.slice(0, 200))}`);
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /** 从 `terminal create --json` 响应提取 handle:result.terminal.handle */
106
+ export function extractOrcaCreateHandle(payload: OrcaEnvelope | null): string | null {
107
+ if (payload?.ok !== true) return null;
108
+ const terminal = payload?.result?.["terminal"];
109
+ if (typeof terminal !== "object" || terminal === null) return null;
110
+ const handle = (terminal as Record<string, unknown>)["handle"];
111
+ return typeof handle === "string" && handle ? handle : null;
112
+ }
113
+
114
+ /** 从 `terminal split --json` 响应提取新 pane handle:result.split.handle */
115
+ export function extractOrcaSplitHandle(payload: OrcaEnvelope | null): string | null {
116
+ if (payload?.ok !== true) return null;
117
+ const split = payload?.result?.["split"];
118
+ if (typeof split !== "object" || split === null) return null;
119
+ const handle = (split as Record<string, unknown>)["handle"];
120
+ return typeof handle === "string" && handle ? handle : null;
121
+ }
122
+
123
+ /** 从 `terminal read --json` 响应提取屏幕行:result.terminal.tail(数组,最新在尾部) */
124
+ export function extractOrcaReadTail(payload: OrcaEnvelope | null): string[] {
125
+ if (payload?.ok !== true) return [];
126
+ const terminal = payload?.result?.["terminal"];
127
+ if (typeof terminal !== "object" || terminal === null) return [];
128
+ const tail = (terminal as Record<string, unknown>)["tail"];
129
+ if (!Array.isArray(tail)) return [];
130
+ return tail.filter((line): line is string => typeof line === "string");
131
+ }
132
+
133
+ /**
134
+ * 统一方向 → orca split 方向。
135
+ * 实测(Orca 1.4.174,分割线轴约定,与 iTerm 一致):
136
+ * horizontal = 上下堆叠(up/down),vertical = 左右并排(left/right)。
137
+ */
138
+ export function orcaSplitDirection(direction: "left" | "right" | "up" | "down"): "horizontal" | "vertical" {
139
+ return direction === "left" || direction === "right" ? "vertical" : "horizontal";
140
+ }
141
+
142
+ // ── Orca CLI 调用的薄封装 ──
143
+
144
+ /**
145
+ * 调用 `orca` 命令并返回 stdout。
146
+ * 失败时 stderr 写入 log,原样抛错(调用方决定如何处理)。
147
+ */
148
+ function orcaExec(args: string[]): string {
149
+ const cmdline = `orca ${args
150
+ .map((a) => (a.includes(" ") || a.includes('"') ? JSON.stringify(a) : a))
151
+ .join(" ")}`;
152
+ orcaLog(`[exec] ${cmdline}`);
153
+ const result = spawnSync("orca", args, {
154
+ encoding: "utf8",
155
+ stdio: ["ignore", "pipe", "pipe"],
156
+ timeout: ORCA_COMMAND_TIMEOUT_MS,
157
+ });
158
+ if (result.error) {
159
+ orcaLog(`[exec] ERROR (spawn): ${result.error.message}`);
160
+ throw result.error;
161
+ }
162
+ if (result.status !== 0) {
163
+ const stderr = (result.stderr ?? "").trim();
164
+ orcaLog(`[exec] ERROR status=${result.status} stderr=${JSON.stringify(stderr)}`);
165
+ throw new Error(`orca ${args[0]} ${args[1] ?? ""} failed (status=${result.status}): ${stderr}`);
166
+ }
167
+ orcaLog(`[exec] -> ${JSON.stringify(result.stdout.trim().slice(0, 200))}`);
168
+ return result.stdout;
169
+ }
170
+
171
+ /** 调用 `orca` 命令,失败只记 log 不抛错。用于 send/rename/close 这类 best-effort 操作。 */
172
+ function orcaExecSilent(args: string[]): void {
173
+ const cmdline = `orca ${args
174
+ .map((a) => (a.includes(" ") || a.includes('"') ? JSON.stringify(a) : a))
175
+ .join(" ")}`;
176
+ orcaLog(`[exec silent] ${cmdline}`);
177
+ const result = spawnSync("orca", args, {
178
+ encoding: "utf8",
179
+ stdio: ["ignore", "pipe", "pipe"],
180
+ timeout: ORCA_COMMAND_TIMEOUT_MS,
181
+ });
182
+ if (result.error || result.status !== 0) {
183
+ orcaLog(
184
+ `[exec silent] ERROR status=${result.status} stderr=${JSON.stringify(
185
+ (result.stderr ?? "").trim().slice(0, 200),
186
+ )}`,
187
+ );
188
+ }
189
+ }
190
+
191
+ // ── 对外 API ──
192
+
193
+ /**
194
+ * 创建一个新的 subagent terminal。
195
+ *
196
+ * 实现:`orca terminal create --title <name>` 在当前 worktree 新建 tab
197
+ * (CLI 语义:不抢焦点)。与 otty/muxy 的 BFS 分屏不同,新 tab 不会压缩
198
+ * agent 自己的 pane,符合 Orca 的 worktree 中心工作流。
199
+ *
200
+ * 失败返回 ""(与 otty createSurface 一致,调用方统一处理)。
201
+ */
202
+ export function createOrcaSurface(name: string): string {
203
+ try {
204
+ const raw = orcaExec(["terminal", "create", "--title", name, "--json"]);
205
+ const handle = extractOrcaCreateHandle(parseOrcaJson(raw));
206
+ if (!handle) {
207
+ orcaLog(`[create] no handle in response for name=${JSON.stringify(name)}`);
208
+ return "";
209
+ }
210
+ orcaLog(`[create] new=${handle} name=${JSON.stringify(name)}`);
211
+ return handle;
212
+ } catch (e) {
213
+ orcaLog(`[create] failed: ${(e as Error).message}`);
214
+ return "";
215
+ }
216
+ }
217
+
218
+ /**
219
+ * 指定方向分屏创建新 pane。
220
+ * fromSurface 缺省时从 agent 自己的 terminal(ORCA_TERMINAL_HANDLE)拆。
221
+ * 失败返回 ""。
222
+ */
223
+ export function splitOrcaTerminal(
224
+ direction: "left" | "right" | "up" | "down",
225
+ fromSurface?: string,
226
+ ): string {
227
+ const source = fromSurface ?? AGENT_ORCA_TERMINAL_HANDLE;
228
+ if (!source) {
229
+ orcaLog(`[split] no source terminal (ORCA_TERMINAL_HANDLE not set)`);
230
+ return "";
231
+ }
232
+ const orcaDir = orcaSplitDirection(direction);
233
+ try {
234
+ const raw = orcaExec(["terminal", "split", "--terminal", source, "--direction", orcaDir, "--json"]);
235
+ const handle = extractOrcaSplitHandle(parseOrcaJson(raw));
236
+ if (!handle) {
237
+ orcaLog(`[split] no handle in response (from=${source} dir=${orcaDir})`);
238
+ return "";
239
+ }
240
+ orcaLog(`[split] from=${source} dir=${orcaDir} new=${handle}`);
241
+ return handle;
242
+ } catch (e) {
243
+ orcaLog(`[split] from=${source} dir=${orcaDir} failed: ${(e as Error).message}`);
244
+ return "";
245
+ }
246
+ }
247
+
248
+ /**
249
+ * 给 terminal 发送命令 + Enter。
250
+ * `orca terminal send --text <cmd> --enter`,失败只记 log(best-effort)。
251
+ */
252
+ export function sendOrcaCommand(handle: string, command: string): void {
253
+ orcaExecSilent(["terminal", "send", "--terminal", handle, "--text", command, "--enter"]);
254
+ }
255
+
256
+ /**
257
+ * 给 terminal 发送 Escape。
258
+ * 实现:发送裸 ESC(`\u001b`,即 `0x1b`)字节,TUI 会将其解释为 Escape 键。
259
+ * 注意不能用 --enter(会把 ESC 和回车拼成一行输入)。
260
+ */
261
+ export function sendOrcaEscape(handle: string): void {
262
+ orcaExecSilent(["terminal", "send", "--terminal", handle, "--text", "\u001b"]);
263
+ }
264
+
265
+ /**
266
+ * 读取 terminal 屏幕最后 N 行。
267
+ * `orca terminal read --limit <N> --json` → result.terminal.tail,拼成文本返回。
268
+ * 失败返回 ""(与 otty readScreen 一致)。
269
+ */
270
+ export function readOrcaScreen(handle: string, lines = 50): string {
271
+ try {
272
+ const raw = orcaExec(["terminal", "read", "--terminal", handle, "--limit", String(lines), "--json"]);
273
+ return extractOrcaReadTail(parseOrcaJson(raw)).join("\n");
274
+ } catch (e) {
275
+ orcaLog(`[read] terminal=${handle} failed: ${(e as Error).message}`);
276
+ return "";
277
+ }
278
+ }
279
+
280
+ /**
281
+ * 查询 terminal 是否仍在 list 中(三态)。
282
+ * true = 确认存在;false = 成功取到 list 且 handle 不在其中;
283
+ * null = 查询本身失败(runtime 不可达/响应异常),不能当作“已关闭”。
284
+ */
285
+ function queryOrcaTerminalExists(handle: string): boolean | null {
286
+ try {
287
+ const raw = orcaExec(["terminal", "list", "--json"]);
288
+ const parsed = parseOrcaJson(raw);
289
+ const terminals = parsed?.result?.["terminals"];
290
+ if (!Array.isArray(terminals)) return null;
291
+ return terminals.some(
292
+ (t) => typeof t === "object" && t !== null && (t as Record<string, unknown>)["handle"] === handle,
293
+ );
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * 关闭 terminal。
301
+ *
302
+ * create() 生成的 surface 独占一个 tab,split 生成的 pane 共享 tab。
303
+ * 策略(best-effort,绝不 throw,避免 pollForExit 退出流程被打断):
304
+ * 1. `terminal close`(关 pane/session)
305
+ * 2. 验证 handle 是否还在列表;不在了则完成
306
+ * 3. 仍在则补 `terminal close --tab`(单 pane tab 的残留情况)
307
+ * 4. 仍失败仅 log warn
308
+ */
309
+ export function closeOrcaSurface(handle: string): void {
310
+ orcaExecSilent(["terminal", "close", "--terminal", handle, "--json"]);
311
+
312
+ let exists = queryOrcaTerminalExists(handle);
313
+ if (exists === false) {
314
+ orcaLog(`[close] terminal ${handle} closed`);
315
+ return;
316
+ }
317
+ if (exists === null) {
318
+ // 查询失败不能当作已关闭;继续尝试 --tab 兜底
319
+ orcaLog(`[close] WARN terminal ${handle} list query failed, cannot verify close`);
320
+ }
321
+
322
+ orcaLog(`[close] terminal ${handle} still present, trying --tab`);
323
+ orcaExecSilent(["terminal", "close", "--terminal", handle, "--tab", "--json"]);
324
+
325
+ exists = queryOrcaTerminalExists(handle);
326
+ if (exists === false) {
327
+ orcaLog(`[close] terminal ${handle} closed via --tab`);
328
+ } else if (exists === null) {
329
+ orcaLog(`[close] WARN terminal ${handle} close sent but unverifiable (list query failed)`);
330
+ } else {
331
+ orcaLog(`[close] WARN terminal ${handle} still present after close --tab`);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * 重命名 terminal 所在 tab。
337
+ * 注意:rename 作用于 tab 标题,split pane 与源 pane 共享 tab ——
338
+ * 调用方需确认 target 是独立 tab(create() 产物或 agent 自己的 terminal)。
339
+ */
340
+ export function renameOrcaTerminal(handle: string, name: string): void {
341
+ orcaExecSilent(["terminal", "rename", "--terminal", handle, "--title", name]);
342
+ }
343
+
344
+ // ── BackendOps 适配器(薄包装以上原生函数,行为语义见各函数注释) ──
345
+
346
+ /** BackendOps 适配器:所有方法薄包装 orca 原生函数 */
347
+ export const ops: BackendOps = {
348
+ /** 创建 orca surface(新 tab,不抢焦点) */
349
+ create(name: string): string {
350
+ return createOrcaSurface(name);
351
+ },
352
+ /** 指定方向分屏;不做 rename(split pane 与源 pane 共享 tab 标题) */
353
+ createSplit(name: string, direction: "left" | "right" | "up" | "down", fromSurface?: string): string {
354
+ return splitOrcaTerminal(direction, fromSurface);
355
+ },
356
+ /** 向 orca terminal 发送命令并执行 */
357
+ send(surface: string, command: string): void {
358
+ sendOrcaCommand(surface, command);
359
+ },
360
+ /** 向 orca terminal 发送 Escape(裸 ESC 字节) */
361
+ sendEscape(surface: string): void {
362
+ sendOrcaEscape(surface);
363
+ },
364
+ /** 同步读取 orca terminal 屏幕最后 N 行 */
365
+ read(surface: string, lines = 50): string {
366
+ return readOrcaScreen(surface, lines);
367
+ },
368
+ /** 异步读取 orca terminal 屏幕最后 N 行 */
369
+ async readAsync(surface: string, lines = 50): Promise<string> {
370
+ return readOrcaScreen(surface, lines);
371
+ },
372
+ /** 关闭 orca terminal(best-effort:close → close --tab → log warn) */
373
+ close(surface: string): void {
374
+ closeOrcaSurface(surface);
375
+ },
376
+ /** 重命名 orca terminal 所在 tab */
377
+ rename(surface: string, name: string): void {
378
+ renameOrcaTerminal(surface, name);
379
+ },
380
+ };
package/src/detection.ts CHANGED
@@ -14,6 +14,7 @@ import { appendFileSync } from "node:fs";
14
14
  import { i18n } from "./i18n.ts";
15
15
  import { isHerdrRuntimeAvailable } from "./herdr.ts";
16
16
  import { isOttyRuntimeAvailable, ottySetupHint } from "./otty.ts";
17
+ import { isOrcaRuntimeAvailable } from "./orca.ts";
17
18
  import { hasCommand } from "./backends/shared.ts";
18
19
 
19
20
  // ── 分屏调试日志 ──
@@ -49,7 +50,7 @@ export const AGENT_MUXY_PANE_ID = process.env.MUXY_PANE_ID;
49
50
 
50
51
  // ── 后端类型 ──
51
52
 
52
- export type MuxBackend = "cmux" | "muxy" | "tmux" | "zellij" | "wezterm" | "herdr" | "otty";
53
+ export type MuxBackend = "cmux" | "muxy" | "tmux" | "zellij" | "wezterm" | "herdr" | "otty" | "orca";
53
54
 
54
55
  // 命令可用性检测复用 backends/shared.ts 的 hasCommand(跨平台 + 缓存)。
55
56
 
@@ -65,7 +66,8 @@ function muxPreference(): MuxBackend | null {
65
66
  pref === "zellij" ||
66
67
  pref === "wezterm" ||
67
68
  pref === "herdr" ||
68
- pref === "otty"
69
+ pref === "otty" ||
70
+ pref === "orca"
69
71
  )
70
72
  return pref;
71
73
  return null;
@@ -123,6 +125,10 @@ export function isOttyAvailable(): boolean {
123
125
  return isOttyRuntimeAvailable();
124
126
  }
125
127
 
128
+ export function isOrcaAvailable(): boolean {
129
+ return isOrcaRuntimeAvailable();
130
+ }
131
+
126
132
  // ── 后端探测入口 ──
127
133
 
128
134
  export function getMuxBackend(): MuxBackend | null {
@@ -134,6 +140,7 @@ export function getMuxBackend(): MuxBackend | null {
134
140
  if (pref === "wezterm") return isWezTermRuntimeAvailable() ? "wezterm" : null;
135
141
  if (pref === "herdr") return isHerdrRuntimeAvailable() ? "herdr" : null;
136
142
  if (pref === "otty") return isOttyRuntimeAvailable() ? "otty" : null;
143
+ if (pref === "orca") return isOrcaRuntimeAvailable() ? "orca" : null;
137
144
 
138
145
  if (isMuxyRuntimeAvailable()) return "muxy";
139
146
  if (isCmuxRuntimeAvailable()) return "cmux";
@@ -142,6 +149,7 @@ export function getMuxBackend(): MuxBackend | null {
142
149
  if (isWezTermRuntimeAvailable()) return "wezterm";
143
150
  if (isHerdrRuntimeAvailable()) return "herdr";
144
151
  if (isOttyRuntimeAvailable()) return "otty";
152
+ if (isOrcaRuntimeAvailable()) return "orca";
145
153
  return null;
146
154
  }
147
155
 
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pi-terminal-mux — 终端多路复用器统一抽象层
3
3
  *
4
- * 支持后端:muxy / cmux / tmux / zellij / wezterm / herdr / otty,
4
+ * 支持后端:muxy / cmux / tmux / zellij / wezterm / herdr / otty / orca
5
5
  * 探测不到任何后端时自动降级为 headless(后台子进程 + 日志文件)。
6
6
  *
7
7
  * 统一 surface API(跨后端一致语义):
@@ -61,6 +61,25 @@ export {
61
61
  } from "./otty.ts";
62
62
  export type { OttyPaneSnapshot } from "./otty.ts";
63
63
 
64
+ // ── orca 后端原生 API ──
65
+ export {
66
+ AGENT_ORCA_TERMINAL_HANDLE,
67
+ isOrcaRuntimeAvailable,
68
+ parseOrcaJson,
69
+ extractOrcaCreateHandle,
70
+ extractOrcaSplitHandle,
71
+ extractOrcaReadTail,
72
+ orcaSplitDirection,
73
+ createOrcaSurface,
74
+ splitOrcaTerminal,
75
+ sendOrcaCommand,
76
+ sendOrcaEscape,
77
+ readOrcaScreen,
78
+ closeOrcaSurface,
79
+ renameOrcaTerminal,
80
+ } from "./orca.ts";
81
+ export type { OrcaEnvelope } from "./orca.ts";
82
+
64
83
  // ── 便捷函数 ──
65
84
  import {
66
85
  AGENT_MUXY_PANE_ID,
@@ -69,6 +88,7 @@ import {
69
88
  } from "./mux.ts";
70
89
  import { AGENT_HERDR_PANE_ID } from "./herdr.ts";
71
90
  import { AGENT_OTTY_PANE_ID } from "./otty.ts";
91
+ import { AGENT_ORCA_TERMINAL_HANDLE } from "./orca.ts";
72
92
 
73
93
  /**
74
94
  * 返回各后端注入 agent pane 标识的环境变量名(用于错误提示)。
@@ -90,6 +110,8 @@ export function backendAgentPaneEnvVar(backend: MuxBackend): string | null {
90
110
  return "HERDR_PANE_ID";
91
111
  case "otty":
92
112
  return null;
113
+ case "orca":
114
+ return "ORCA_TERMINAL_HANDLE";
93
115
  }
94
116
  }
95
117
 
@@ -103,6 +125,7 @@ export function getAgentPaneId(backend?: MuxBackend | null): string | null {
103
125
  if (resolved === "muxy") return AGENT_MUXY_PANE_ID ?? null;
104
126
  if (resolved === "herdr") return AGENT_HERDR_PANE_ID ?? null;
105
127
  if (resolved === "otty") return AGENT_OTTY_PANE_ID ?? null;
128
+ if (resolved === "orca") return AGENT_ORCA_TERMINAL_HANDLE ?? null;
106
129
  if (resolved === "tmux") return process.env.TMUX_PANE ?? null;
107
130
  if (resolved === "wezterm") return process.env.WEZTERM_PANE ?? null;
108
131
  if (resolved === "zellij") return process.env.ZELLIJ_PANE_ID ?? null;
package/src/mux.ts CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  isWezTermAvailable,
21
21
  isHerdrAvailable,
22
22
  isOttyAvailable,
23
+ isOrcaAvailable,
23
24
  } from "./detection.ts";
24
25
 
25
26
  export {
package/src/orca.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * orca.ts — orca 后端 barrel(向后兼容 subpath 导入)
3
+ *
4
+ * 所有实现已迁至 backends/orca.ts,此文件仅做 re-export。
5
+ * 显式列出符号(不用 export *),避免内部 BackendOps 适配器 ops 泄漏到公开 API。
6
+ */
7
+
8
+ export {
9
+ AGENT_ORCA_TERMINAL_HANDLE,
10
+ closeOrcaSurface,
11
+ createOrcaSurface,
12
+ extractOrcaCreateHandle,
13
+ extractOrcaReadTail,
14
+ extractOrcaSplitHandle,
15
+ isOrcaRuntimeAvailable,
16
+ orcaSplitDirection,
17
+ parseOrcaJson,
18
+ readOrcaScreen,
19
+ renameOrcaTerminal,
20
+ sendOrcaCommand,
21
+ sendOrcaEscape,
22
+ splitOrcaTerminal,
23
+ } from "./backends/orca.ts";
24
+ export type { OrcaEnvelope } from "./backends/orca.ts";
package/src/surface.ts CHANGED
@@ -45,10 +45,12 @@ import { ops as zellijOps } from "./backends/zellij.ts";
45
45
  import { ops as weztermOps } from "./backends/wezterm.ts";
46
46
  import { ops as herdrOps, AGENT_HERDR_PANE_ID, renameHerdrTab, renameHerdrWorkspace } from "./backends/herdr.ts";
47
47
  import { ops as ottyOps, AGENT_OTTY_PANE_ID } from "./backends/otty.ts";
48
+ import { ops as orcaOps, AGENT_ORCA_TERMINAL_HANDLE } from "./backends/orca.ts";
48
49
 
49
50
  // 各后端直接引用的公开函数(非对称操作不进 BackendOps)
50
51
  import { renameHerdrPane, renameHerdrAgent, sendHerdrCommand, sendHerdrEscape, readHerdrScreen, closeHerdrSurface } from "./backends/herdr.ts";
51
52
  import { sendOttyCommand, sendOttyEscape, readOttyScreen, closeOttySurface, renameOttyTab } from "./backends/otty.ts";
53
+ import { renameOrcaTerminal } from "./backends/orca.ts";
52
54
 
53
55
  const execFileAsync = promisify(execFile);
54
56
 
@@ -63,6 +65,7 @@ const backendOps: Record<MuxBackend, BackendOps> = {
63
65
  wezterm: weztermOps,
64
66
  herdr: herdrOps,
65
67
  otty: ottyOps,
68
+ orca: orcaOps,
66
69
  };
67
70
 
68
71
  // ── 内部辅助 ──
@@ -155,6 +158,9 @@ export function createSurfaceSplit(
155
158
  lastSplitSource = sourcePane;
156
159
  } else if (backend === "otty") {
157
160
  lastSplitSource = fromSurface ?? AGENT_OTTY_PANE_ID ?? null;
161
+ } else if (backend === "orca") {
162
+ // orca split 默认从 agent 自己的 terminal 拆,与 splitOrcaTerminal 的回退逻辑一致
163
+ lastSplitSource = fromSurface ?? AGENT_ORCA_TERMINAL_HANDLE ?? null;
158
164
  } else {
159
165
  // tmux / wezterm / zellij
160
166
  const source = backend === "tmux" ? process.env.TMUX_PANE : fromSurface;
@@ -359,6 +365,12 @@ export function renameCurrentTab(title: string): void {
359
365
  return;
360
366
  }
361
367
 
368
+ if (backend === "orca") {
369
+ if (!AGENT_ORCA_TERMINAL_HANDLE) throw new Error("ORCA_TERMINAL_HANDLE not set");
370
+ renameOrcaTerminal(AGENT_ORCA_TERMINAL_HANDLE, title);
371
+ return;
372
+ }
373
+
362
374
  // zellij: rename the agent's own pane
363
375
  const paneId = process.env.ZELLIJ_PANE_ID;
364
376
  if (paneId) {
@@ -424,6 +436,11 @@ export function renameWorkspace(title: string): void {
424
436
  return;
425
437
  }
426
438
 
439
+ if (backend === "orca") {
440
+ // orca: 无独立 workspace 概念(worktree 由 Orca 管理),跳过
441
+ return;
442
+ }
443
+
427
444
  // zellij: skip session rename
428
445
  }
429
446