pi-terminal-mux 0.3.2 → 0.4.1
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 +11 -2
- package/README.zh-CN.md +11 -2
- package/locales/mux.json +4 -0
- package/package.json +2 -2
- package/src/backends/herdr.ts +162 -41
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ closeSurface(surface);
|
|
|
56
56
|
| tmux | `TMUX` + `tmux` command |
|
|
57
57
|
| zellij | `ZELLIJ` / `ZELLIJ_SESSION_NAME` + `zellij` command |
|
|
58
58
|
| wezterm | `WEZTERM_UNIX_SOCKET` + `wezterm` command |
|
|
59
|
-
| herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` command |
|
|
59
|
+
| herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` command (`tab` mode also requires `HERDR_WORKSPACE_ID`) |
|
|
60
60
|
| otty | `TERM_PROGRAM=otty` + `otty` command |
|
|
61
61
|
| orca | `TERM_PROGRAM=Orca` + `orca` command + reachable Orca runtime |
|
|
62
62
|
|
|
@@ -67,13 +67,21 @@ Default priority follows the table order (muxy first). Force a backend with:
|
|
|
67
67
|
|
|
68
68
|
If the forced backend's runtime is unavailable, `getMuxBackend()` returns `null` — it never silently falls back to another backend.
|
|
69
69
|
|
|
70
|
+
### Herdr surface mode
|
|
71
|
+
|
|
72
|
+
Herdr keeps the backward-compatible breadth-first split mode by default. Set `PI_SUBAGENT_HERDR_MODE=tab` to create one background tab per subagent, or `split` to select the original pane layout explicitly. `createSurfaceSplit()` always remains an explicit pane split.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export PI_SUBAGENT_HERDR_MODE=tab
|
|
76
|
+
```
|
|
77
|
+
|
|
70
78
|
## API overview
|
|
71
79
|
|
|
72
80
|
### Unified surface API (same semantics across backends)
|
|
73
81
|
|
|
74
82
|
| Function | Description |
|
|
75
83
|
|----------|-------------|
|
|
76
|
-
| `createSurface(name)` | Smart placement (cmux: first right-split then tabs; zellij: tab-aware tiled/stacked; muxy/otty/orca: breadth-first splits; orca falls back to a new tab without an agent handle), returns a surface handle |
|
|
84
|
+
| `createSurface(name)` | Smart placement (herdr: breadth-first splits by default, or one background tab per surface with `PI_SUBAGENT_HERDR_MODE=tab`; cmux: first right-split then tabs; zellij: tab-aware tiled/stacked; muxy/otty/orca: breadth-first splits; orca falls back to a new tab without an agent handle), returns a surface handle |
|
|
77
85
|
| `createSurfaceSplit(name, direction, fromSurface?, options?)` | Split in an explicit direction (left/right/up/down). `options.activate` (WezTerm only, default `false`) focuses the new pane after splitting |
|
|
78
86
|
| `sendCommand(surface, command)` | Send a command and press Enter |
|
|
79
87
|
| `sendLongCommand(surface, command, opts?)` | Write long commands to a script file first. `opts.scriptPreamble` injects leading lines; `opts.interpreter` (`"bash"` default, or `"powershell"` on Windows) selects the scripting runtime; returns the script path |
|
|
@@ -113,6 +121,7 @@ These are opt-in capabilities — existing Bash callers and `pi-interactive-suba
|
|
|
113
121
|
| `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` | Force a backend |
|
|
114
122
|
| `PI_SUBAGENT_ZELLIJ_MIN_COLUMNS` / `PI_SUBAGENT_ZELLIJ_MIN_ROWS` | Minimum usable size for zellij splits (default 50x10; stacks instead when smaller) |
|
|
115
123
|
| `PI_SUBAGENT_RENAME_TMUX_WINDOW` / `PI_SUBAGENT_RENAME_TMUX_SESSION` | Allow renameCurrentTab / renameWorkspace on tmux (user naming untouched by default) |
|
|
124
|
+
| `PI_SUBAGENT_HERDR_MODE` | Herdr surface placement: `split` (default) or `tab` |
|
|
116
125
|
| `PI_SUBAGENT_RENAME_HERDR_WORKSPACE` | Allow renameWorkspace on herdr |
|
|
117
126
|
| `PI_EXTENSIONS_LOCALE` | Hint language (`zh-CN` / `en-US` / `auto`), provided by pi-extensions-i18n |
|
|
118
127
|
|
package/README.zh-CN.md
CHANGED
|
@@ -55,7 +55,7 @@ closeSurface(surface);
|
|
|
55
55
|
| tmux | `TMUX` + `tmux` 命令 |
|
|
56
56
|
| zellij | `ZELLIJ` / `ZELLIJ_SESSION_NAME` + `zellij` 命令 |
|
|
57
57
|
| wezterm | `WEZTERM_UNIX_SOCKET` + `wezterm` 命令 |
|
|
58
|
-
| herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr`
|
|
58
|
+
| herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` 命令(`tab` 模式还需要 `HERDR_WORKSPACE_ID`) |
|
|
59
59
|
| otty | `TERM_PROGRAM=otty` + `otty` 命令 |
|
|
60
60
|
| orca | `TERM_PROGRAM=Orca` + `orca` 命令 + Orca runtime 可达 |
|
|
61
61
|
|
|
@@ -66,13 +66,21 @@ closeSurface(surface);
|
|
|
66
66
|
|
|
67
67
|
指定的后端运行环境不满足时 `getMuxBackend()` 返回 `null`,不会悄悄降级到其他后端。
|
|
68
68
|
|
|
69
|
+
### Herdr surface 模式
|
|
70
|
+
|
|
71
|
+
Herdr 默认保持向后兼容的广度优先分屏模式。设置 `PI_SUBAGENT_HERDR_MODE=tab` 后,每个 subagent 会创建独立后台 Tab;设置为 `split` 可显式选择原有分屏布局。`createSurfaceSplit()` 始终保留显式 pane 分屏语义。
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export PI_SUBAGENT_HERDR_MODE=tab
|
|
75
|
+
```
|
|
76
|
+
|
|
69
77
|
## API 概览
|
|
70
78
|
|
|
71
79
|
### 统一 surface API(跨后端语义一致)
|
|
72
80
|
|
|
73
81
|
| 函数 | 说明 |
|
|
74
82
|
|------|------|
|
|
75
|
-
| `createSurface(name)` | 智能放置新 surface(cmux 首次右分屏后续开 tab、zellij tab 感知平铺/堆叠、muxy/otty/orca 广度优先分屏;orca 缺少 agent handle 时新建 tab),返回 surface 标识 |
|
|
83
|
+
| `createSurface(name)` | 智能放置新 surface(herdr 默认广度优先分屏,设置 `PI_SUBAGENT_HERDR_MODE=tab` 后每个 surface 创建独立后台 Tab;cmux 首次右分屏后续开 tab、zellij tab 感知平铺/堆叠、muxy/otty/orca 广度优先分屏;orca 缺少 agent handle 时新建 tab),返回 surface 标识 |
|
|
76
84
|
| `createSurfaceSplit(name, direction, fromSurface?, options?)` | 指定方向(left/right/up/down)分屏;`options.activate`(仅 wezterm,默认 false)分屏后聚焦新 pane |
|
|
77
85
|
| `sendCommand(surface, command)` | 发送命令并回车执行 |
|
|
78
86
|
| `sendLongCommand(surface, command, opts?)` | 长命令先写脚本文件再执行;`opts.scriptPreamble` 可注入前置片段;`opts.interpreter`(默认 `"bash"`,Windows 可显式 `"powershell"`)选择脚本运行时;返回脚本路径 |
|
|
@@ -112,6 +120,7 @@ closeSurface(surface);
|
|
|
112
120
|
| `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` | 强制指定后端 |
|
|
113
121
|
| `PI_SUBAGENT_ZELLIJ_MIN_COLUMNS` / `PI_SUBAGENT_ZELLIJ_MIN_ROWS` | zellij 分屏最小可用尺寸(默认 50×10,不满足时改堆叠) |
|
|
114
122
|
| `PI_SUBAGENT_RENAME_TMUX_WINDOW` / `PI_SUBAGENT_RENAME_TMUX_SESSION` | tmux 下允许 renameCurrentTab / renameWorkspace(默认不动用户命名) |
|
|
123
|
+
| `PI_SUBAGENT_HERDR_MODE` | herdr surface 放置模式:`split`(默认)或 `tab` |
|
|
115
124
|
| `PI_SUBAGENT_RENAME_HERDR_WORKSPACE` | herdr 下允许 renameWorkspace |
|
|
116
125
|
| `PI_EXTENSIONS_LOCALE` | 提示文案语言(`zh-CN` / `en-US` / `auto`),由 pi-extensions-i18n 提供 |
|
|
117
126
|
|
package/locales/mux.json
CHANGED
|
@@ -46,5 +46,9 @@
|
|
|
46
46
|
"setupHint.ottySendKeys": {
|
|
47
47
|
"zh-CN": "Otty 的 `ipc-allow-send-keys` 未启用。如需让 pi 驱动子 agent 分屏,请在 ~/.config/otty/config.toml 中添加 `ipc-allow-send-keys = true` 并重新加载 Otty。",
|
|
48
48
|
"en-US": "Otty's `ipc-allow-send-keys` is disabled. To let pi drive subagent panes, add `ipc-allow-send-keys = true` to ~/.config/otty/config.toml and reload Otty."
|
|
49
|
+
},
|
|
50
|
+
"error.invalidHerdrMode": {
|
|
51
|
+
"zh-CN": "不支持的 Herdr surface 模式:{value}。可选值:split、tab。",
|
|
52
|
+
"en-US": "Unsupported Herdr surface mode: {value}. Choose split or tab."
|
|
49
53
|
}
|
|
50
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-terminal-mux",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Terminal multiplexer abstraction for pi extensions — unified surface API across muxy, cmux, tmux, zellij, wezterm, herdr, otty and orca, with headless fallback",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"coding-agent"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"pi-extensions-i18n": "^0.
|
|
57
|
+
"pi-extensions-i18n": "^0.4.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "24.12.4",
|
package/src/backends/herdr.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* HERDR_TAB_ID(公开 id,如 "1:1")
|
|
9
9
|
* HERDR_PANE_ID(公开 id,如 "1-1")
|
|
10
10
|
*
|
|
11
|
-
* 所有 pane 操作通过 `herdr` CLI 完成,详见 SKILL.md。
|
|
11
|
+
* 所有 tab / pane 操作通过 `herdr` CLI 完成,详见 SKILL.md。
|
|
12
12
|
* 日志、文件锁、BFS 分屏状态机、命令检测复用 backends/shared.ts。
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -19,6 +19,32 @@ import { createBackendLogger, withFileLock, BfsSplitStateManager, hasCommand } f
|
|
|
19
19
|
|
|
20
20
|
// ── 日志(统一格式,写入 /tmp/pi-mux-herdr.log) ──
|
|
21
21
|
const herdrLog = createBackendLogger("herdr", "/tmp/pi-mux-herdr.log");
|
|
22
|
+
const HERDR_TAB_CLOSE_COMMAND = ["tab", "close"] as const;
|
|
23
|
+
const HERDR_PANE_SPLIT_COMMAND = ["pane", "split"] as const;
|
|
24
|
+
const HERDR_PANE_CLOSE_COMMAND = ["pane", "close"] as const;
|
|
25
|
+
const HERDR_SPLIT_DIRECTION_FLAG = "--direction";
|
|
26
|
+
const HERDR_NO_FOCUS_FLAG = "--no-focus";
|
|
27
|
+
const HERDR_SPLIT_RIGHT = "right";
|
|
28
|
+
const HERDR_SPLIT_DOWN = "down";
|
|
29
|
+
const HERDR_SPLIT_UP = "up";
|
|
30
|
+
const HERDR_MARKER_PATH_PREFIX = "/tmp/herdr-subagent-pane-";
|
|
31
|
+
const HERDR_MARKER_DEFAULT_ID = "default";
|
|
32
|
+
const HERDR_LOCK_SUFFIX = ".lock";
|
|
33
|
+
export const HERDR_SURFACE_MODE_SPLIT = "split";
|
|
34
|
+
export const HERDR_SURFACE_MODE_TAB = "tab";
|
|
35
|
+
const DEFAULT_HERDR_SURFACE_MODE = HERDR_SURFACE_MODE_SPLIT;
|
|
36
|
+
|
|
37
|
+
export type HerdrSurfaceMode =
|
|
38
|
+
| typeof HERDR_SURFACE_MODE_SPLIT
|
|
39
|
+
| typeof HERDR_SURFACE_MODE_TAB;
|
|
40
|
+
|
|
41
|
+
/** 解析 Herdr surface 放置模式;未配置时保持旧版 split,非法显式值直接报错。 */
|
|
42
|
+
export function resolveHerdrSurfaceMode(value = process.env.PI_SUBAGENT_HERDR_MODE): HerdrSurfaceMode {
|
|
43
|
+
const normalized = value?.trim().toLowerCase();
|
|
44
|
+
if (!normalized || normalized === HERDR_SURFACE_MODE_SPLIT) return DEFAULT_HERDR_SURFACE_MODE;
|
|
45
|
+
if (normalized === HERDR_SURFACE_MODE_TAB) return HERDR_SURFACE_MODE_TAB;
|
|
46
|
+
throw new Error(i18n.t("error.invalidHerdrMode", { value }));
|
|
47
|
+
}
|
|
22
48
|
|
|
23
49
|
/**
|
|
24
50
|
* 捕获于模块加载时的 agent pane id。
|
|
@@ -33,18 +59,20 @@ export const AGENT_HERDR_TAB_ID = process.env.HERDR_TAB_ID;
|
|
|
33
59
|
* 检测 herdr backend 是否可用:
|
|
34
60
|
* 1. `herdr` 命令在 PATH 中
|
|
35
61
|
* 2. 当前进程在 herdr pane 内运行(HERDR_ENV=1)
|
|
36
|
-
* 3. HERDR_PANE_ID
|
|
62
|
+
* 3. HERDR_PANE_ID 已注入;tab 模式还要求 HERDR_WORKSPACE_ID
|
|
37
63
|
*
|
|
38
64
|
* 注意:即使 socket 暂时不通,只要命令存在且 env 注入,就认为"runtime available"。
|
|
39
|
-
* 子 agent
|
|
65
|
+
* 子 agent 创建时再通过对应的 pane split / tab create 命令触发 socket 调用。
|
|
40
66
|
*/
|
|
41
67
|
export function isHerdrRuntimeAvailable(): boolean {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
process.env.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
if (
|
|
69
|
+
process.env.HERDR_ENV !== "1" ||
|
|
70
|
+
!process.env.HERDR_PANE_ID ||
|
|
71
|
+
!hasCommand("herdr")
|
|
72
|
+
) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return resolveHerdrSurfaceMode() === HERDR_SURFACE_MODE_SPLIT || !!process.env.HERDR_WORKSPACE_ID;
|
|
48
76
|
}
|
|
49
77
|
|
|
50
78
|
// ── herdr CLI 调用的薄封装 ──
|
|
@@ -103,19 +131,34 @@ function extractPaneId(json: unknown): string | null {
|
|
|
103
131
|
|
|
104
132
|
/** herdr BFS 分屏状态 marker 文件路径(按 agent pane id 区分) */
|
|
105
133
|
function herdrMarkerPath(): string {
|
|
106
|
-
|
|
134
|
+
const paneId = (AGENT_HERDR_PANE_ID ?? HERDR_MARKER_DEFAULT_ID).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
135
|
+
return `${HERDR_MARKER_PATH_PREFIX}${paneId}.json`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface CreatedHerdrTab {
|
|
139
|
+
tabId: string;
|
|
140
|
+
paneId: string;
|
|
107
141
|
}
|
|
108
142
|
|
|
143
|
+
/** 从 `herdr tab create` 响应提取新 tab 与 root pane 的公开 id。 */
|
|
144
|
+
function extractCreatedHerdrTab(json: unknown): CreatedHerdrTab | null {
|
|
145
|
+
if (!json || typeof json !== "object") return null;
|
|
146
|
+
const result = (json as Record<string, unknown>).result;
|
|
147
|
+
if (!result || typeof result !== "object") return null;
|
|
148
|
+
const record = result as Record<string, unknown>;
|
|
149
|
+
const tab = record.tab as Record<string, unknown> | undefined;
|
|
150
|
+
const rootPane = record.root_pane as Record<string, unknown> | undefined;
|
|
151
|
+
if (typeof tab?.tab_id !== "string" || typeof rootPane?.pane_id !== "string") return null;
|
|
152
|
+
return { tabId: tab.tab_id, paneId: rootPane.pane_id };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 记录由本进程创建的 tab surface,使 rename / close 操作作用于整个 tab。 */
|
|
156
|
+
const createdHerdrTabIds = new Map<string, string>();
|
|
157
|
+
|
|
109
158
|
// ── 对外 API:createSurface 系列 ──
|
|
110
159
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
* 实现:split 当前 agent pane 右侧(--no-focus 保持 agent 焦点不变)。
|
|
115
|
-
* 后续 subagent 按 breadth-first 模式轮转 right/down/right/down…(与 cmux/muxy 行为一致)。
|
|
116
|
-
* 锁与 BFS 状态机复用 shared.ts。
|
|
117
|
-
*/
|
|
118
|
-
export function createHerdrSurface(name: string): string {
|
|
160
|
+
/** 使用原有 BFS 策略创建 subagent 分屏。 */
|
|
161
|
+
function createHerdrSplitSurface(name: string): string {
|
|
119
162
|
if (!AGENT_HERDR_PANE_ID) {
|
|
120
163
|
throw new Error(
|
|
121
164
|
"HERDR_PANE_ID not set; cannot determine parent pane for subagent split. " +
|
|
@@ -124,20 +167,18 @@ export function createHerdrSurface(name: string): string {
|
|
|
124
167
|
}
|
|
125
168
|
|
|
126
169
|
const markerFile = herdrMarkerPath();
|
|
127
|
-
const lockPath = `${markerFile}
|
|
170
|
+
const lockPath = `${markerFile}${HERDR_LOCK_SUFFIX}`;
|
|
128
171
|
|
|
129
172
|
return withFileLock(lockPath, {}, () => {
|
|
130
173
|
const state = new BfsSplitStateManager(markerFile);
|
|
131
174
|
|
|
132
|
-
// 首次 split
|
|
133
175
|
if (state.panes().length === 0) {
|
|
134
176
|
const output = herdrExec([
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"--no-focus",
|
|
177
|
+
...HERDR_PANE_SPLIT_COMMAND,
|
|
178
|
+
AGENT_HERDR_PANE_ID,
|
|
179
|
+
HERDR_SPLIT_DIRECTION_FLAG,
|
|
180
|
+
HERDR_SPLIT_RIGHT,
|
|
181
|
+
HERDR_NO_FOCUS_FLAG,
|
|
141
182
|
]);
|
|
142
183
|
const newPaneId = extractPaneId(parseHerdrJson(output));
|
|
143
184
|
if (newPaneId) {
|
|
@@ -152,22 +193,31 @@ export function createHerdrSurface(name: string): string {
|
|
|
152
193
|
return "";
|
|
153
194
|
}
|
|
154
195
|
|
|
155
|
-
// 后续:BFS 分屏
|
|
156
196
|
const next = state.next();
|
|
157
197
|
if (!next) return "";
|
|
158
198
|
|
|
159
199
|
let { source } = next;
|
|
160
200
|
const { direction } = next;
|
|
161
|
-
|
|
162
|
-
// 若 source 过期(pane 被关闭 / session 重启),重置状态从 agent pane 重新分屏
|
|
163
201
|
let output: string;
|
|
164
202
|
try {
|
|
165
|
-
output = herdrExec([
|
|
203
|
+
output = herdrExec([
|
|
204
|
+
...HERDR_PANE_SPLIT_COMMAND,
|
|
205
|
+
source,
|
|
206
|
+
HERDR_SPLIT_DIRECTION_FLAG,
|
|
207
|
+
direction,
|
|
208
|
+
HERDR_NO_FOCUS_FLAG,
|
|
209
|
+
]);
|
|
166
210
|
} catch {
|
|
167
211
|
try { rmSync(markerFile); } catch { /* ignore */ }
|
|
168
212
|
herdrLog(`[split] pane ${source} gone, resetting from agent pane ${AGENT_HERDR_PANE_ID}`);
|
|
169
|
-
source = AGENT_HERDR_PANE_ID
|
|
170
|
-
output = herdrExec([
|
|
213
|
+
source = AGENT_HERDR_PANE_ID;
|
|
214
|
+
output = herdrExec([
|
|
215
|
+
...HERDR_PANE_SPLIT_COMMAND,
|
|
216
|
+
source,
|
|
217
|
+
HERDR_SPLIT_DIRECTION_FLAG,
|
|
218
|
+
HERDR_SPLIT_RIGHT,
|
|
219
|
+
HERDR_NO_FOCUS_FLAG,
|
|
220
|
+
]);
|
|
171
221
|
}
|
|
172
222
|
|
|
173
223
|
const newPaneId = extractPaneId(parseHerdrJson(output));
|
|
@@ -185,6 +235,46 @@ export function createHerdrSurface(name: string): string {
|
|
|
185
235
|
});
|
|
186
236
|
}
|
|
187
237
|
|
|
238
|
+
/** 在当前 workspace 中创建独立后台 tab,并返回 root pane id。 */
|
|
239
|
+
function createHerdrTabSurface(name: string): string {
|
|
240
|
+
if (!AGENT_HERDR_WORKSPACE_ID) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"HERDR_WORKSPACE_ID not set; cannot determine workspace for subagent tab. " +
|
|
243
|
+
"Start pi inside herdr so HERDR_WORKSPACE_ID is injected at launch.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const tabLabel = herdrSurfaceLabel(name);
|
|
248
|
+
const output = herdrExec([
|
|
249
|
+
"tab",
|
|
250
|
+
"create",
|
|
251
|
+
"--workspace",
|
|
252
|
+
AGENT_HERDR_WORKSPACE_ID,
|
|
253
|
+
"--cwd",
|
|
254
|
+
process.cwd(),
|
|
255
|
+
"--label",
|
|
256
|
+
tabLabel,
|
|
257
|
+
"--no-focus",
|
|
258
|
+
]);
|
|
259
|
+
const createdTab = extractCreatedHerdrTab(parseHerdrJson(output));
|
|
260
|
+
if (!createdTab) {
|
|
261
|
+
throw new Error(`Unexpected herdr tab create output: ${output.trim() || "(empty)"}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
createdHerdrTabIds.set(createdTab.paneId, createdTab.tabId);
|
|
265
|
+
herdrLog(
|
|
266
|
+
`[tab create] workspace=${AGENT_HERDR_WORKSPACE_ID} tab=${createdTab.tabId} pane=${createdTab.paneId} name=${JSON.stringify(name)}`,
|
|
267
|
+
);
|
|
268
|
+
return createdTab.paneId;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 按 PI_SUBAGENT_HERDR_MODE 选择兼容分屏或独立 tab;默认保持 split。 */
|
|
272
|
+
export function createHerdrSurface(name: string): string {
|
|
273
|
+
return resolveHerdrSurfaceMode() === HERDR_SURFACE_MODE_TAB
|
|
274
|
+
? createHerdrTabSurface(name)
|
|
275
|
+
: createHerdrSplitSurface(name);
|
|
276
|
+
}
|
|
277
|
+
|
|
188
278
|
/**
|
|
189
279
|
* 从指定 pane 直接分屏(不走广度优先状态机),供 createSurfaceSplit 使用。
|
|
190
280
|
* herdr 文档仅明确 right/down,left/up 分别归一到 right/down。
|
|
@@ -195,8 +285,16 @@ export function splitHerdrPane(
|
|
|
195
285
|
direction: "left" | "right" | "up" | "down",
|
|
196
286
|
name?: string,
|
|
197
287
|
): string {
|
|
198
|
-
const dir = direction ===
|
|
199
|
-
|
|
288
|
+
const dir = direction === HERDR_SPLIT_DOWN || direction === HERDR_SPLIT_UP
|
|
289
|
+
? HERDR_SPLIT_DOWN
|
|
290
|
+
: HERDR_SPLIT_RIGHT;
|
|
291
|
+
const output = herdrExec([
|
|
292
|
+
...HERDR_PANE_SPLIT_COMMAND,
|
|
293
|
+
fromPane,
|
|
294
|
+
HERDR_SPLIT_DIRECTION_FLAG,
|
|
295
|
+
dir,
|
|
296
|
+
HERDR_NO_FOCUS_FLAG,
|
|
297
|
+
]);
|
|
200
298
|
const newPaneId = extractPaneId(parseHerdrJson(output));
|
|
201
299
|
if (!newPaneId) {
|
|
202
300
|
throw new Error(`Unexpected herdr pane split output: ${output.trim() || "(empty)"}`);
|
|
@@ -250,6 +348,12 @@ function getWorkspaceLabel(): string | null {
|
|
|
250
348
|
}
|
|
251
349
|
}
|
|
252
350
|
|
|
351
|
+
/** 生成 herdr pane / tab 的统一显示标签。 */
|
|
352
|
+
function herdrSurfaceLabel(name: string): string {
|
|
353
|
+
const workspaceLabel = getWorkspaceLabel();
|
|
354
|
+
return workspaceLabel ? `${workspaceLabel}[${name}]` : name;
|
|
355
|
+
}
|
|
356
|
+
|
|
253
357
|
/**
|
|
254
358
|
* 用 herdr CLI 重命名 pane 对应的 tab。
|
|
255
359
|
* tab label 格式: workspace_label[name]
|
|
@@ -258,8 +362,7 @@ export function renameHerdrTab(paneId: string, name: string): void {
|
|
|
258
362
|
const ws = parseWorkspaceIdFromPaneId(paneId);
|
|
259
363
|
if (!ws) return;
|
|
260
364
|
try {
|
|
261
|
-
const
|
|
262
|
-
const tabLabel = wsLabel ? `${wsLabel}[${name}]` : name;
|
|
365
|
+
const tabLabel = herdrSurfaceLabel(name);
|
|
263
366
|
const tabsJson = herdrExec(["tab", "list", "--workspace", ws]);
|
|
264
367
|
const parsed = parseHerdrJson(tabsJson);
|
|
265
368
|
if (!parsed || typeof parsed !== "object") return;
|
|
@@ -331,13 +434,19 @@ export function readHerdrScreen(paneId: string, lines = 50, source: "visible" |
|
|
|
331
434
|
}
|
|
332
435
|
|
|
333
436
|
/**
|
|
334
|
-
* 关闭 pane
|
|
335
|
-
* `herdr pane close <id>` 是 herdr CLI 子命令。
|
|
437
|
+
* 关闭 surface。本进程创建的 tab surface 关闭整个 tab;其他 pane id 仍按 pane 关闭。
|
|
336
438
|
*/
|
|
337
439
|
export function closeHerdrSurface(paneId: string): void {
|
|
338
|
-
|
|
440
|
+
const tabId = createdHerdrTabIds.get(paneId);
|
|
441
|
+
if (tabId) {
|
|
442
|
+
herdrExecSilent([...HERDR_TAB_CLOSE_COMMAND, tabId]);
|
|
443
|
+
createdHerdrTabIds.delete(paneId);
|
|
444
|
+
herdrLog(`[close] tab=${tabId} pane=${paneId}`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
herdrExecSilent([...HERDR_PANE_CLOSE_COMMAND, paneId]);
|
|
339
449
|
|
|
340
|
-
// 从 BFS 状态移除已关闭的 subagent,避免僵尸 ID 累积
|
|
341
450
|
const state = new BfsSplitStateManager(herdrMarkerPath());
|
|
342
451
|
const beforePanes = state.panes();
|
|
343
452
|
state.remove(paneId);
|
|
@@ -408,6 +517,18 @@ export const ops: BackendOps = {
|
|
|
408
517
|
closeHerdrSurface(surface);
|
|
409
518
|
},
|
|
410
519
|
rename(surface: string, name: string): void {
|
|
411
|
-
|
|
520
|
+
const tabId = createdHerdrTabIds.get(surface);
|
|
521
|
+
if (!tabId) {
|
|
522
|
+
renameHerdrPane(surface, name);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const tabLabel = herdrSurfaceLabel(name);
|
|
527
|
+
herdrExecSilent(["tab", "rename", tabId, tabLabel]);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
herdrLog(
|
|
530
|
+
`[rename tab] tab=${tabId} pane=${surface} name=${JSON.stringify(name)} failed: ${(error as Error).message}`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
412
533
|
},
|
|
413
534
|
};
|