pi-terminal-mux 0.2.1 → 0.2.2
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/package.json +3 -4
- package/src/backends/cmux.ts +18 -2
- package/src/backends/herdr.ts +59 -185
- package/src/backends/muxy.ts +48 -134
- package/src/backends/otty.ts +88 -238
- package/src/backends/shared.ts +250 -0
- package/src/backends/tmux.ts +8 -0
- package/src/backends/wezterm.ts +8 -0
- package/src/backends/zellij.ts +7 -39
- package/src/detection.ts +6 -35
- package/src/mux.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-terminal-mux",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -52,12 +52,11 @@
|
|
|
52
52
|
"wezterm",
|
|
53
53
|
"coding-agent"
|
|
54
54
|
],
|
|
55
|
-
"
|
|
56
|
-
"pi-extensions-i18n": "
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"pi-extensions-i18n": "^0.3.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "24.12.4",
|
|
60
|
-
"pi-extensions-i18n": "^0.3.0",
|
|
61
60
|
"tsx": "4.23.1",
|
|
62
61
|
"typescript": "5.9.3"
|
|
63
62
|
}
|
package/src/backends/cmux.ts
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import { execSync, execFileSync, spawnSync } from "node:child_process";
|
|
10
10
|
import { shellEscape } from "../shell.ts";
|
|
11
|
+
import { createBackendLogger } from "./shared.ts";
|
|
11
12
|
import type { BackendOps } from "./types.ts";
|
|
12
13
|
|
|
14
|
+
/** Cmux 后端日志(统一格式,写入 /tmp/pi-mux-cmux.log) */
|
|
15
|
+
const cmuxLog = createBackendLogger("cmux", "/tmp/pi-mux-cmux.log");
|
|
16
|
+
|
|
13
17
|
// ── 内部辅助 ──
|
|
14
18
|
|
|
15
19
|
/** Tracked subagent pane for cmux — reused across subagent launches. */
|
|
@@ -271,7 +275,11 @@ export const ops: BackendOps = {
|
|
|
271
275
|
try {
|
|
272
276
|
const tree = execSync(`cmux tree`, { encoding: "utf8" });
|
|
273
277
|
if (tree.includes(cmuxSubagentPane)) {
|
|
274
|
-
|
|
278
|
+
const surface = createSurfaceInPane(name, cmuxSubagentPane);
|
|
279
|
+
cmuxLog(
|
|
280
|
+
`[split] mode=tab-reuse pane=${cmuxSubagentPane} new=${surface} name=${JSON.stringify(name)}`,
|
|
281
|
+
);
|
|
282
|
+
return surface;
|
|
275
283
|
}
|
|
276
284
|
} catch {}
|
|
277
285
|
// Pane 已消失 — fall through 创建新 split
|
|
@@ -280,11 +288,18 @@ export const ops: BackendOps = {
|
|
|
280
288
|
|
|
281
289
|
const created = createCmuxSplitSurface(name, "right", process.env.CMUX_SURFACE_ID);
|
|
282
290
|
cmuxSubagentPane = created.paneRef ?? null;
|
|
291
|
+
cmuxLog(
|
|
292
|
+
`[split] mode=first dir=right from=${process.env.CMUX_SURFACE_ID ?? "<unset>"} new=${created.surface} name=${JSON.stringify(name)}`,
|
|
293
|
+
);
|
|
283
294
|
return created.surface;
|
|
284
295
|
},
|
|
285
296
|
|
|
286
297
|
createSplit(name: string, direction: "left" | "right" | "up" | "down", fromSurface?: string): string {
|
|
287
|
-
|
|
298
|
+
const surface = createCmuxSplitSurface(name, direction, fromSurface).surface;
|
|
299
|
+
cmuxLog(
|
|
300
|
+
`[split] mode=createSurfaceSplit dir=${direction} from=${fromSurface ?? "<unset>"} new=${surface} name=${JSON.stringify(name)}`,
|
|
301
|
+
);
|
|
302
|
+
return surface;
|
|
288
303
|
},
|
|
289
304
|
|
|
290
305
|
send(surface: string, command: string): void {
|
|
@@ -318,6 +333,7 @@ export const ops: BackendOps = {
|
|
|
318
333
|
execSync(`cmux close-surface --surface ${shellEscape(surface)}`, {
|
|
319
334
|
encoding: "utf8",
|
|
320
335
|
});
|
|
336
|
+
cmuxLog(`[close] surface=${surface}`);
|
|
321
337
|
},
|
|
322
338
|
|
|
323
339
|
rename(surface: string, name: string): void {
|
package/src/backends/herdr.ts
CHANGED
|
@@ -9,21 +9,16 @@
|
|
|
9
9
|
* HERDR_PANE_ID(公开 id,如 "1-1")
|
|
10
10
|
*
|
|
11
11
|
* 所有 pane 操作通过 `herdr` CLI 完成,详见 SKILL.md。
|
|
12
|
+
* 日志、文件锁、BFS 分屏状态机、命令检测复用 backends/shared.ts。
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import { execFileSync
|
|
15
|
-
import {
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { rmSync } from "node:fs";
|
|
16
17
|
import { i18n } from "../i18n.ts";
|
|
18
|
+
import { createBackendLogger, withFileLock, BfsSplitStateManager, hasCommand } from "./shared.ts";
|
|
17
19
|
|
|
18
|
-
// ──
|
|
19
|
-
const
|
|
20
|
-
function herdrLog(msg: string): void {
|
|
21
|
-
try {
|
|
22
|
-
appendFileSync(HERDR_SPLIT_LOG, `[${new Date().toISOString()}] ${msg}`);
|
|
23
|
-
} catch {
|
|
24
|
-
/* 写日志失败不影响主流程 */
|
|
25
|
-
}
|
|
26
|
-
}
|
|
20
|
+
// ── 日志(统一格式,写入 /tmp/pi-mux-herdr.log) ──
|
|
21
|
+
const herdrLog = createBackendLogger("herdr", "/tmp/pi-mux-herdr.log");
|
|
27
22
|
|
|
28
23
|
/**
|
|
29
24
|
* 捕获于模块加载时的 agent pane id。
|
|
@@ -34,27 +29,6 @@ export const AGENT_HERDR_PANE_ID = process.env.HERDR_PANE_ID;
|
|
|
34
29
|
export const AGENT_HERDR_WORKSPACE_ID = process.env.HERDR_WORKSPACE_ID;
|
|
35
30
|
export const AGENT_HERDR_TAB_ID = process.env.HERDR_TAB_ID;
|
|
36
31
|
|
|
37
|
-
// ── 命令可用性缓存 ──
|
|
38
|
-
|
|
39
|
-
const commandAvailability = new Map<string, boolean>();
|
|
40
|
-
|
|
41
|
-
function hasCommand(command: string): boolean {
|
|
42
|
-
if (commandAvailability.has(command)) {
|
|
43
|
-
return commandAvailability.get(command)!;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let available = false;
|
|
47
|
-
try {
|
|
48
|
-
execFileSync("which", [command], { stdio: "ignore" });
|
|
49
|
-
available = true;
|
|
50
|
-
} catch {
|
|
51
|
-
available = false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
commandAvailability.set(command, available);
|
|
55
|
-
return available;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
32
|
/**
|
|
59
33
|
* 检测 herdr backend 是否可用:
|
|
60
34
|
* 1. `herdr` 命令在 PATH 中
|
|
@@ -77,12 +51,12 @@ export function isHerdrRuntimeAvailable(): boolean {
|
|
|
77
51
|
|
|
78
52
|
/**
|
|
79
53
|
* 调用 `herdr` 命令并返回 stdout。
|
|
80
|
-
*
|
|
54
|
+
* 失败时原样抛错(调用方决定如何处理)。
|
|
81
55
|
*/
|
|
82
56
|
function herdrExec(args: string[]): string {
|
|
83
|
-
herdrLog(`[
|
|
57
|
+
herdrLog(`[exec] herdr ${args.map((a) => (a.includes(" ") ? JSON.stringify(a) : a)).join(" ")}`);
|
|
84
58
|
const out = execFileSync("herdr", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
85
|
-
herdrLog(`[
|
|
59
|
+
herdrLog(`[exec] -> ${JSON.stringify(out.trim().slice(0, 200))}`);
|
|
86
60
|
return out;
|
|
87
61
|
}
|
|
88
62
|
|
|
@@ -91,7 +65,7 @@ function herdrExec(args: string[]): string {
|
|
|
91
65
|
* 无输出的命令,遵循 SKILL.md 中"pane send-text/send-keys/run print nothing on success"。
|
|
92
66
|
*/
|
|
93
67
|
function herdrExecSilent(args: string[]): void {
|
|
94
|
-
herdrLog(`[
|
|
68
|
+
herdrLog(`[exec silent] herdr ${args.map((a) => (a.includes(" ") ? JSON.stringify(a) : a)).join(" ")}`);
|
|
95
69
|
execFileSync("herdr", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
96
70
|
}
|
|
97
71
|
|
|
@@ -125,12 +99,12 @@ function extractPaneId(json: unknown): string | null {
|
|
|
125
99
|
return null;
|
|
126
100
|
}
|
|
127
101
|
|
|
128
|
-
// ──
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
102
|
+
// ── BFS 分屏状态 marker 路径 ──
|
|
103
|
+
|
|
104
|
+
/** herdr BFS 分屏状态 marker 文件路径(按 agent pane id 区分) */
|
|
105
|
+
function herdrMarkerPath(): string {
|
|
106
|
+
return `/tmp/herdr-subagent-pane-${(AGENT_HERDR_PANE_ID ?? "default").replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
|
|
107
|
+
}
|
|
134
108
|
|
|
135
109
|
// ── 对外 API:createSurface 系列 ──
|
|
136
110
|
|
|
@@ -139,6 +113,7 @@ const herdrPaneSources = new Map<string, string>();
|
|
|
139
113
|
*
|
|
140
114
|
* 实现:split 当前 agent pane 右侧(--no-focus 保持 agent 焦点不变)。
|
|
141
115
|
* 后续 subagent 按 breadth-first 模式轮转 right/down/right/down…(与 cmux/muxy 行为一致)。
|
|
116
|
+
* 锁与 BFS 状态机复用 shared.ts。
|
|
142
117
|
*/
|
|
143
118
|
export function createHerdrSurface(name: string): string {
|
|
144
119
|
if (!AGENT_HERDR_PANE_ID) {
|
|
@@ -148,142 +123,66 @@ export function createHerdrSurface(name: string): string {
|
|
|
148
123
|
);
|
|
149
124
|
}
|
|
150
125
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// 第二轮:从第一个 pane 向下分,pos=0, base=1
|
|
154
|
-
// 第三轮:从第一个 pane 向右、第二个 pane 向右,pos=0, base=2
|
|
155
|
-
// ……
|
|
156
|
-
// 状态文件:/tmp/herdr-subagent-pane-<agent_pane_id>.json
|
|
157
|
-
const markerFile = `/tmp/herdr-subagent-pane-${AGENT_HERDR_PANE_ID.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
|
|
158
|
-
const lockFile = `${markerFile}.lock`;
|
|
159
|
-
|
|
160
|
-
// 全局锁:所有分屏操作串行化
|
|
161
|
-
const acquired = (() => {
|
|
162
|
-
for (let i = 0; i < 60; i++) {
|
|
163
|
-
if (!existsSync(lockFile)) {
|
|
164
|
-
try {
|
|
165
|
-
writeFileSync(lockFile, `${process.pid}`, { flag: "wx" });
|
|
166
|
-
return true;
|
|
167
|
-
} catch {
|
|
168
|
-
// 竞争失败,继续等待
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
spawnSync("sleep", ["0.05"]);
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
})();
|
|
126
|
+
const markerFile = herdrMarkerPath();
|
|
127
|
+
const lockPath = `${markerFile}.lock`;
|
|
175
128
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return "";
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
let state: { panes: string[]; pos: number; base: number; dir: "right" | "down" } = {
|
|
183
|
-
panes: [],
|
|
184
|
-
pos: 0,
|
|
185
|
-
base: 0,
|
|
186
|
-
dir: "right",
|
|
187
|
-
};
|
|
188
|
-
try {
|
|
189
|
-
state = JSON.parse(readFileSync(markerFile, "utf8"));
|
|
190
|
-
} catch {
|
|
191
|
-
/* 文件不存在或损坏,用初始状态 */
|
|
192
|
-
}
|
|
129
|
+
return withFileLock(lockPath, {}, () => {
|
|
130
|
+
const state = new BfsSplitStateManager(markerFile);
|
|
193
131
|
|
|
194
132
|
// 首次 split
|
|
195
|
-
if (state.panes.length === 0) {
|
|
133
|
+
if (state.panes().length === 0) {
|
|
196
134
|
const output = herdrExec([
|
|
197
135
|
"pane",
|
|
198
136
|
"split",
|
|
199
|
-
AGENT_HERDR_PANE_ID
|
|
137
|
+
AGENT_HERDR_PANE_ID!,
|
|
200
138
|
"--direction",
|
|
201
139
|
"right",
|
|
202
140
|
"--no-focus",
|
|
203
141
|
]);
|
|
204
|
-
const
|
|
205
|
-
const newPaneId = extractPaneId(json);
|
|
142
|
+
const newPaneId = extractPaneId(parseHerdrJson(output));
|
|
206
143
|
if (newPaneId) {
|
|
207
|
-
state.
|
|
208
|
-
state.pos = 0;
|
|
209
|
-
state.base = 1;
|
|
210
|
-
state.dir = "down";
|
|
211
|
-
writeFileSync(markerFile, JSON.stringify(state));
|
|
212
|
-
herdrPaneSources.set(newPaneId, AGENT_HERDR_PANE_ID);
|
|
144
|
+
state.add(newPaneId);
|
|
213
145
|
renameHerdrPane(newPaneId, name);
|
|
214
146
|
herdrLog(
|
|
215
|
-
`[
|
|
147
|
+
`[split] mode=first dir=right from=${AGENT_HERDR_PANE_ID} new=${newPaneId} name=${JSON.stringify(name)}`,
|
|
216
148
|
);
|
|
217
149
|
return newPaneId;
|
|
218
150
|
}
|
|
219
|
-
herdrLog(`[
|
|
151
|
+
herdrLog(`[split] first split returned no pane id, output=${JSON.stringify(output)}`);
|
|
220
152
|
return "";
|
|
221
153
|
}
|
|
222
154
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
state.base = state.panes.length;
|
|
227
|
-
state.dir = state.dir === "right" ? "down" : "right";
|
|
228
|
-
}
|
|
155
|
+
// 后续:BFS 分屏
|
|
156
|
+
const next = state.next();
|
|
157
|
+
if (!next) return "";
|
|
229
158
|
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
herdrLog(`[herdr split] state.panes[${state.pos}] is undefined\n`);
|
|
233
|
-
return "";
|
|
234
|
-
}
|
|
159
|
+
let { source } = next;
|
|
160
|
+
const { direction } = next;
|
|
235
161
|
|
|
236
|
-
// 若
|
|
162
|
+
// 若 source 过期(pane 被关闭 / session 重启),重置状态从 agent pane 重新分屏
|
|
237
163
|
let output: string;
|
|
238
|
-
let sourcePane = targetPane;
|
|
239
164
|
try {
|
|
240
|
-
output = herdrExec([
|
|
241
|
-
"pane",
|
|
242
|
-
"split",
|
|
243
|
-
targetPane,
|
|
244
|
-
"--direction",
|
|
245
|
-
state.dir,
|
|
246
|
-
"--no-focus",
|
|
247
|
-
]);
|
|
165
|
+
output = herdrExec(["pane", "split", source, "--direction", direction, "--no-focus"]);
|
|
248
166
|
} catch {
|
|
249
167
|
try { rmSync(markerFile); } catch { /* ignore */ }
|
|
250
|
-
herdrLog(
|
|
251
|
-
|
|
252
|
-
);
|
|
253
|
-
state = { panes: [], pos: 0, base: 0, dir: "right" };
|
|
254
|
-
targetPane = AGENT_HERDR_PANE_ID;
|
|
255
|
-
sourcePane = AGENT_HERDR_PANE_ID;
|
|
256
|
-
output = herdrExec([
|
|
257
|
-
"pane",
|
|
258
|
-
"split",
|
|
259
|
-
AGENT_HERDR_PANE_ID,
|
|
260
|
-
"--direction",
|
|
261
|
-
"right",
|
|
262
|
-
"--no-focus",
|
|
263
|
-
]);
|
|
168
|
+
herdrLog(`[split] pane ${source} gone, resetting from agent pane ${AGENT_HERDR_PANE_ID}`);
|
|
169
|
+
source = AGENT_HERDR_PANE_ID!;
|
|
170
|
+
output = herdrExec(["pane", "split", source, "--direction", "right", "--no-focus"]);
|
|
264
171
|
}
|
|
265
|
-
|
|
266
|
-
const newPaneId = extractPaneId(
|
|
172
|
+
|
|
173
|
+
const newPaneId = extractPaneId(parseHerdrJson(output));
|
|
267
174
|
if (newPaneId) {
|
|
268
|
-
state.
|
|
269
|
-
state.
|
|
270
|
-
writeFileSync(markerFile, JSON.stringify(state));
|
|
271
|
-
herdrPaneSources.set(newPaneId, sourcePane);
|
|
175
|
+
state.advance();
|
|
176
|
+
state.add(newPaneId);
|
|
272
177
|
renameHerdrPane(newPaneId, name);
|
|
273
178
|
herdrLog(
|
|
274
|
-
`[
|
|
179
|
+
`[split] mode=next dir=${direction} from=${source} new=${newPaneId} name=${JSON.stringify(name)}`,
|
|
275
180
|
);
|
|
276
181
|
return newPaneId;
|
|
277
182
|
}
|
|
278
|
-
herdrLog(`[
|
|
183
|
+
herdrLog(`[split] next split returned no pane id, output=${JSON.stringify(output)}`);
|
|
279
184
|
return "";
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
rmSync(lockFile);
|
|
283
|
-
} catch {
|
|
284
|
-
/* ignore */
|
|
285
|
-
}
|
|
286
|
-
}
|
|
185
|
+
});
|
|
287
186
|
}
|
|
288
187
|
|
|
289
188
|
/**
|
|
@@ -302,11 +201,8 @@ export function splitHerdrPane(
|
|
|
302
201
|
if (!newPaneId) {
|
|
303
202
|
throw new Error(`Unexpected herdr pane split output: ${output.trim() || "(empty)"}`);
|
|
304
203
|
}
|
|
305
|
-
herdrPaneSources.set(newPaneId, fromPane);
|
|
306
204
|
if (name) renameHerdrPane(newPaneId, name);
|
|
307
|
-
herdrLog(
|
|
308
|
-
`[herdr split] mode=direct dir=${dir} from=${fromPane} new=${newPaneId} name=${JSON.stringify(name ?? "")}\n`,
|
|
309
|
-
);
|
|
205
|
+
herdrLog(`[split] mode=direct dir=${dir} from=${fromPane} new=${newPaneId} name=${JSON.stringify(name ?? "")}`);
|
|
310
206
|
return newPaneId;
|
|
311
207
|
}
|
|
312
208
|
|
|
@@ -320,7 +216,7 @@ export function renameHerdrPane(paneId: string, name: string): void {
|
|
|
320
216
|
const paneLabel = wsLabel ? `${wsLabel}[${name}]` : name;
|
|
321
217
|
herdrExecSilent(["pane", "rename", paneId, paneLabel]);
|
|
322
218
|
} catch (e) {
|
|
323
|
-
herdrLog(`[
|
|
219
|
+
herdrLog(`[rename pane] pane=${paneId} name=${JSON.stringify(name)} failed: ${(e as Error).message}`);
|
|
324
220
|
}
|
|
325
221
|
}
|
|
326
222
|
|
|
@@ -333,7 +229,7 @@ export function renameHerdrAgent(paneId: string, name: string): void {
|
|
|
333
229
|
try {
|
|
334
230
|
herdrExecSilent(["agent", "rename", paneId, name]);
|
|
335
231
|
} catch (e) {
|
|
336
|
-
herdrLog(`[
|
|
232
|
+
herdrLog(`[rename agent] pane=${paneId} name=${JSON.stringify(name)} failed: ${(e as Error).message}`);
|
|
337
233
|
}
|
|
338
234
|
}
|
|
339
235
|
|
|
@@ -376,7 +272,7 @@ export function renameHerdrTab(paneId: string, name: string): void {
|
|
|
376
272
|
}
|
|
377
273
|
}
|
|
378
274
|
} catch (e) {
|
|
379
|
-
herdrLog(`[
|
|
275
|
+
herdrLog(`[rename tab] pane=${paneId} name=${JSON.stringify(name)} failed: ${(e as Error).message}`);
|
|
380
276
|
}
|
|
381
277
|
}
|
|
382
278
|
|
|
@@ -390,7 +286,7 @@ export function renameHerdrWorkspace(title: string): void {
|
|
|
390
286
|
try {
|
|
391
287
|
herdrExecSilent(["workspace", "rename", AGENT_HERDR_WORKSPACE_ID, title]);
|
|
392
288
|
} catch (e) {
|
|
393
|
-
herdrLog(`[
|
|
289
|
+
herdrLog(`[rename workspace] title=${JSON.stringify(title)} failed: ${(e as Error).message}`);
|
|
394
290
|
}
|
|
395
291
|
}
|
|
396
292
|
|
|
@@ -435,38 +331,16 @@ export function readHerdrScreen(paneId: string, lines = 50, source: "visible" |
|
|
|
435
331
|
export function closeHerdrSurface(paneId: string): void {
|
|
436
332
|
herdrExecSilent(["pane", "close", paneId]);
|
|
437
333
|
|
|
438
|
-
//
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
parsed.panes.splice(idx, 1);
|
|
448
|
-
if (typeof parsed.pos === "number" && idx < parsed.pos) {
|
|
449
|
-
parsed.pos = Math.max(0, parsed.pos - 1);
|
|
450
|
-
}
|
|
451
|
-
if (parsed.panes.length === 0) {
|
|
452
|
-
rmSync(markerFile);
|
|
453
|
-
herdrLog(
|
|
454
|
-
`[herdr close] pane=${paneId} panes=${JSON.stringify(beforePanes)} -> [] pos=${beforePos} -> <marker-removed>\n`,
|
|
455
|
-
);
|
|
456
|
-
} else {
|
|
457
|
-
writeFileSync(markerFile, JSON.stringify(parsed));
|
|
458
|
-
herdrLog(
|
|
459
|
-
`[herdr close] pane=${paneId} panes=${JSON.stringify(beforePanes)} -> ${JSON.stringify(parsed.panes)} pos=${beforePos} -> ${parsed.pos}\n`,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
} else {
|
|
463
|
-
herdrLog(`[herdr close] pane=${paneId} (not in marker state.panes)\n`);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
} catch {
|
|
467
|
-
herdrLog(`[herdr close] pane=${paneId} (no marker, nothing to clean)\n`);
|
|
334
|
+
// 从 BFS 状态移除已关闭的 subagent,避免僵尸 ID 累积
|
|
335
|
+
const state = new BfsSplitStateManager(herdrMarkerPath());
|
|
336
|
+
const beforePanes = state.panes();
|
|
337
|
+
state.remove(paneId);
|
|
338
|
+
const afterPanes = state.panes();
|
|
339
|
+
if (beforePanes.length !== afterPanes.length) {
|
|
340
|
+
herdrLog(`[close] pane=${paneId} panes=${JSON.stringify(beforePanes)} -> ${JSON.stringify(afterPanes)}`);
|
|
341
|
+
} else {
|
|
342
|
+
herdrLog(`[close] pane=${paneId} (not in marker state.panes)`);
|
|
468
343
|
}
|
|
469
|
-
herdrPaneSources.delete(paneId);
|
|
470
344
|
}
|
|
471
345
|
|
|
472
346
|
// ── 辅助:从 pane id 解析 workspace id ──
|
|
@@ -530,4 +404,4 @@ export const ops: BackendOps = {
|
|
|
530
404
|
rename(surface: string, name: string): void {
|
|
531
405
|
renameHerdrPane(surface, name);
|
|
532
406
|
},
|
|
533
|
-
};
|
|
407
|
+
};
|