pi-terminal-mux 0.4.1 → 0.5.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 +16 -1
- package/README.zh-CN.md +16 -1
- package/locales/mux.json +12 -0
- package/package.json +1 -1
- package/src/backends/herdr.ts +29 -14
- package/src/backends/orca.ts +6 -4
- package/src/backends/otty.ts +13 -6
- package/src/backends/wezterm.ts +1 -7
- package/src/index.ts +1 -1
- package/src/mux.ts +22 -0
- package/src/rename.ts +212 -0
- package/src/surface.ts +147 -119
package/README.md
CHANGED
|
@@ -88,10 +88,14 @@ export PI_SUBAGENT_HERDR_MODE=tab
|
|
|
88
88
|
| `sendEscape(surface)` | Send one ESC keypress |
|
|
89
89
|
| `readScreen(surface, lines?, options?)` / `readScreenAsync` | Read the last N screen lines. `options.source` (herdr-only) forwards a herdr read source such as `"recent_unwrapped"`; other backends ignore it |
|
|
90
90
|
| `closeSurface(surface)` | Close the surface |
|
|
91
|
-
| `renameSurface(surface, name)` / `
|
|
91
|
+
| `renameSurface(surface, name)` / `renameAgent(surface, name)` | Rename a known surface or agent label |
|
|
92
|
+
| `getRenameCapability(operation, backend?, env?)` | Report the actual rename target or an explicit `unsupported` / `disabled` capability without executing a command |
|
|
93
|
+
| `renameCurrentTab(title)` / `renameWorkspace(title)` | Rename and return a discriminated `renamed` / `unsupported` / `disabled` / `failed` result |
|
|
92
94
|
| `pollForExit(surface, signal, opts)` | Wait for the process in a surface to exit: `.exit` sidecar file first, then a screen sentinel (`__SUBAGENT_DONE_<code>__`); headless uses child process exit |
|
|
93
95
|
| `getLastSplitSource()` / `clearLastSplitSource()` | Source pane of the most recent split (for UI display) |
|
|
94
96
|
|
|
97
|
+
Rename targets differ by backend: muxy/zellij tab rename targets a pane; tmux targets a window/session when its opt-in variables are enabled; WezTerm workspace rename targets the window; cmux and Herdr provide native workspace rename; Otty and Orca have no workspace rename. Headless reports `unsupported` instead of silently succeeding.
|
|
98
|
+
|
|
95
99
|
### Detection and utilities
|
|
96
100
|
|
|
97
101
|
`getMuxBackend()`, `isMuxAvailable()`, `isHeadlessMode()`, `muxSetupHint()`, `getAgentPaneId(backend?)`, `backendAgentPaneEnvVar(backend)`, `shellEscape()`, `isFishShell()`, `exitStatusVar()`, plus zellij placement planning (`selectZellijPlacement` etc.) and cmux/otty JSON parsing helpers — all pure and unit-testable.
|
|
@@ -134,3 +138,14 @@ These are opt-in capabilities — existing Bash callers and `pi-interactive-suba
|
|
|
134
138
|
## License
|
|
135
139
|
|
|
136
140
|
MIT
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## Scoped naming
|
|
144
|
+
|
|
145
|
+
`createSurfaceRenameContext(surface)` describes the terminal target a launcher can grant to a child. Pass its JSON value in `PI_TERMINAL_RENAME_CONTEXT`, replacing any inherited value on **every launch and resume**. This protocol is owned by terminal-mux, not by a naming or subagent extension.
|
|
146
|
+
|
|
147
|
+
`resolveTerminalRenameTargets({ tab, workspace })` returns explicit target IDs and `surface`/`shared` scope, or individual skipped/failed results. `renameTerminalTarget(reference, title)` executes against that captured identity. Callers decide when to rename and which targets to request; the library does not generate titles or change Pi sessions.
|
|
148
|
+
|
|
149
|
+
A restricted child never renames a workspace. cmux surfaces, muxy/zellij panes, and Herdr panes or explicitly created Herdr tabs can be granted. tmux/WezTerm/Otty/Orca split surfaces do not prove exclusive ownership of their window/tab: naming is skipped rather than expanded to the shared parent. Ordinary sessions require their own target IDs; missing IDs never fall back to focus or the first tab. Existing backend rename opt-ins still apply to ordinary sessions.
|
|
150
|
+
|
|
151
|
+
Invalid JSON or an unknown protocol version is an error, not unrestricted access. Legacy child identity variables without this protocol restrict terminal naming until the launcher supplies ownership. This is a cooperation contract for trusted local processes, not a security sandbox. WezTerm/Otty split creation does not rename a shared tab.
|
package/README.zh-CN.md
CHANGED
|
@@ -87,10 +87,14 @@ export PI_SUBAGENT_HERDR_MODE=tab
|
|
|
87
87
|
| `sendEscape(surface)` | 发送一次 ESC |
|
|
88
88
|
| `readScreen(surface, lines?, options?)` / `readScreenAsync` | 读取屏幕尾部 N 行;`options.source`(仅 herdr)透传 herdr 读屏来源(如 `"recent_unwrapped"`),其他后端忽略 |
|
|
89
89
|
| `closeSurface(surface)` | 关闭 surface |
|
|
90
|
-
| `renameSurface(surface, name)` / `
|
|
90
|
+
| `renameSurface(surface, name)` / `renameAgent(surface, name)` | 重命名已知 surface 或 agent 标签 |
|
|
91
|
+
| `getRenameCapability(operation, backend?, env?)` | 不执行命令,返回实际重命名目标,或明确的 `unsupported` / `disabled` 能力结果 |
|
|
92
|
+
| `renameCurrentTab(title)` / `renameWorkspace(title)` | 执行重命名并返回可判别的 `renamed` / `unsupported` / `disabled` / `failed` 结果 |
|
|
91
93
|
| `pollForExit(surface, signal, opts)` | 等待 surface 内进程退出:优先 `.exit` sidecar 文件,其次屏幕 sentinel(`__SUBAGENT_DONE_<code>__`),headless 走子进程 exit |
|
|
92
94
|
| `getLastSplitSource()` / `clearLastSplitSource()` | 最近一次分屏的来源 pane(用于 UI 展示) |
|
|
93
95
|
|
|
96
|
+
各后端的实际目标不同:muxy/zellij 的 tab 重命名作用于 pane;tmux 在开启对应变量后作用于 window/session;WezTerm 的 workspace 重命名作用于 window;cmux 和 Herdr 提供原生 workspace 重命名;Otty、Orca 没有 workspace 重命名。Headless 会明确返回 `unsupported`,不再静默成功。
|
|
97
|
+
|
|
94
98
|
### 探测与工具
|
|
95
99
|
|
|
96
100
|
`getMuxBackend()`、`isMuxAvailable()`、`isHeadlessMode()`、`muxSetupHint()`、`getAgentPaneId(backend?)`、`backendAgentPaneEnvVar(backend)`、`shellEscape()`、`isFishShell()`、`exitStatusVar()`,以及 zellij 放置规划(`selectZellijPlacement` 等)与 cmux/otty JSON 解析等纯函数,均可直接引用做单元测试。
|
|
@@ -133,3 +137,14 @@ export PI_SUBAGENT_HERDR_MODE=tab
|
|
|
133
137
|
## License
|
|
134
138
|
|
|
135
139
|
MIT
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## 按归属改名
|
|
143
|
+
|
|
144
|
+
`createSurfaceRenameContext(surface)` 描述启动方可授予子进程的终端改名目标。把返回值序列化为 JSON 放入 `PI_TERMINAL_RENAME_CONTEXT`,**每次启动和恢复都替换继承值**。协议属于 terminal-mux,不属于命名或子代理插件。
|
|
145
|
+
|
|
146
|
+
`resolveTerminalRenameTargets({ tab, workspace })` 返回明确的目标 ID、`surface`/`shared` 范围,或逐目标跳过/失败结果。`renameTerminalTarget(reference, title)` 对捕获的身份执行改名。调用方决定何时改、改哪些目标;库不生成标题,也不修改 Pi session。
|
|
147
|
+
|
|
148
|
+
受限子进程不能改 workspace。cmux surface、muxy/zellij pane、Herdr pane 或明确新建的 Herdr tab 可以授予。tmux/WezTerm/Otty/Orca 的分屏不能证明独占 window/tab,改名会跳过,不扩大到共享父目标。普通会话必须能确定自身目标 ID;缺失时不退回当前焦点或第一个 tab。普通会话仍遵守各后端原有的改名开关。
|
|
149
|
+
|
|
150
|
+
协议 JSON 损坏或版本未知会报错,不能退回无限制范围。只有旧子代理身份标志、没有归属协议时,终端改名受限,直到启动方提供归属。此协议用于可信本地进程协作,不是安全沙箱。WezTerm/Otty 创建分屏时不会重命名共享 tab。
|
package/locales/mux.json
CHANGED
|
@@ -50,5 +50,17 @@
|
|
|
50
50
|
"error.invalidHerdrMode": {
|
|
51
51
|
"zh-CN": "不支持的 Herdr surface 模式:{value}。可选值:split、tab。",
|
|
52
52
|
"en-US": "Unsupported Herdr surface mode: {value}. Choose split or tab."
|
|
53
|
+
},
|
|
54
|
+
"error.renameIncomplete": {
|
|
55
|
+
"zh-CN": "{backend} 未完成终端改名。",
|
|
56
|
+
"en-US": "{backend} did not complete terminal renaming."
|
|
57
|
+
},
|
|
58
|
+
"rename.invalidContext": {
|
|
59
|
+
"zh-CN": "终端改名归属信息无效;未修改终端名称。",
|
|
60
|
+
"en-US": "Invalid terminal rename ownership context; terminal names were not changed."
|
|
61
|
+
},
|
|
62
|
+
"rename.missingId": {
|
|
63
|
+
"zh-CN": "无法确定终端改名目标 ID。",
|
|
64
|
+
"en-US": "Cannot determine the terminal rename target ID."
|
|
53
65
|
}
|
|
54
66
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-terminal-mux",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
package/src/backends/herdr.ts
CHANGED
|
@@ -20,6 +20,10 @@ import { createBackendLogger, withFileLock, BfsSplitStateManager, hasCommand } f
|
|
|
20
20
|
// ── 日志(统一格式,写入 /tmp/pi-mux-herdr.log) ──
|
|
21
21
|
const herdrLog = createBackendLogger("herdr", "/tmp/pi-mux-herdr.log");
|
|
22
22
|
const HERDR_TAB_CLOSE_COMMAND = ["tab", "close"] as const;
|
|
23
|
+
const HERDR_TAB_LIST_COMMAND = ["tab", "list"] as const;
|
|
24
|
+
const HERDR_TAB_RENAME_COMMAND = ["tab", "rename"] as const;
|
|
25
|
+
const HERDR_WORKSPACE_RENAME_COMMAND = ["workspace", "rename"] as const;
|
|
26
|
+
const HERDR_WORKSPACE_FLAG = "--workspace";
|
|
23
27
|
const HERDR_PANE_SPLIT_COMMAND = ["pane", "split"] as const;
|
|
24
28
|
const HERDR_PANE_CLOSE_COMMAND = ["pane", "close"] as const;
|
|
25
29
|
const HERDR_SPLIT_DIRECTION_FLAG = "--direction";
|
|
@@ -155,6 +159,11 @@ function extractCreatedHerdrTab(json: unknown): CreatedHerdrTab | null {
|
|
|
155
159
|
/** 记录由本进程创建的 tab surface,使 rename / close 操作作用于整个 tab。 */
|
|
156
160
|
const createdHerdrTabIds = new Map<string, string>();
|
|
157
161
|
|
|
162
|
+
/** 仅返回本进程实际创建的独占 tab,供启动方声明改名归属。 */
|
|
163
|
+
export function getCreatedHerdrTabId(surface: string): string | undefined {
|
|
164
|
+
return createdHerdrTabIds.get(surface);
|
|
165
|
+
}
|
|
166
|
+
|
|
158
167
|
// ── 对外 API:createSurface 系列 ──
|
|
159
168
|
|
|
160
169
|
/** 使用原有 BFS 策略创建 subagent 分屏。 */
|
|
@@ -358,24 +367,28 @@ function herdrSurfaceLabel(name: string): string {
|
|
|
358
367
|
* 用 herdr CLI 重命名 pane 对应的 tab。
|
|
359
368
|
* tab label 格式: workspace_label[name]
|
|
360
369
|
*/
|
|
361
|
-
export function renameHerdrTab(paneId: string, name: string):
|
|
370
|
+
export function renameHerdrTab(paneId: string, name: string): boolean {
|
|
362
371
|
const ws = parseWorkspaceIdFromPaneId(paneId);
|
|
363
|
-
if (!ws) return;
|
|
372
|
+
if (!ws) return false;
|
|
364
373
|
try {
|
|
365
374
|
const tabLabel = herdrSurfaceLabel(name);
|
|
366
|
-
|
|
375
|
+
if (paneId === AGENT_HERDR_PANE_ID && AGENT_HERDR_TAB_ID) {
|
|
376
|
+
herdrExecSilent([...HERDR_TAB_RENAME_COMMAND, AGENT_HERDR_TAB_ID, tabLabel]);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const tabsJson = herdrExec([...HERDR_TAB_LIST_COMMAND, HERDR_WORKSPACE_FLAG, ws]);
|
|
367
381
|
const parsed = parseHerdrJson(tabsJson);
|
|
368
|
-
if (!parsed || typeof parsed !== "object") return;
|
|
382
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
369
383
|
const result = (parsed as Record<string, unknown>).result as Record<string, unknown> | undefined;
|
|
370
384
|
const tabs = (result?.tabs as Array<Record<string, unknown>>) ?? [];
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
}
|
|
385
|
+
const firstTab = tabs[0];
|
|
386
|
+
if (!firstTab || typeof firstTab.tab_id !== "string") return false;
|
|
387
|
+
herdrExecSilent([...HERDR_TAB_RENAME_COMMAND, firstTab.tab_id, tabLabel]);
|
|
388
|
+
return true;
|
|
377
389
|
} catch (e) {
|
|
378
390
|
herdrLog(`[rename tab] pane=${paneId} name=${JSON.stringify(name)} failed: ${(e as Error).message}`);
|
|
391
|
+
return false;
|
|
379
392
|
}
|
|
380
393
|
}
|
|
381
394
|
|
|
@@ -383,13 +396,15 @@ export function renameHerdrTab(paneId: string, name: string): void {
|
|
|
383
396
|
* 重命名 workspace。herdr 中 workspace rename 命令是 `herdr workspace rename <id> <label>`。
|
|
384
397
|
* 仅当环境变量 PI_SUBAGENT_RENAME_HERDR_WORKSPACE=1 时启用(保守策略,避免影响用户命名)。
|
|
385
398
|
*/
|
|
386
|
-
export function renameHerdrWorkspace(title: string):
|
|
387
|
-
if (process.env.PI_SUBAGENT_RENAME_HERDR_WORKSPACE !== "1") return;
|
|
388
|
-
if (!AGENT_HERDR_WORKSPACE_ID) return;
|
|
399
|
+
export function renameHerdrWorkspace(title: string): boolean {
|
|
400
|
+
if (process.env.PI_SUBAGENT_RENAME_HERDR_WORKSPACE !== "1") return false;
|
|
401
|
+
if (!AGENT_HERDR_WORKSPACE_ID) return false;
|
|
389
402
|
try {
|
|
390
|
-
herdrExecSilent([
|
|
403
|
+
herdrExecSilent([...HERDR_WORKSPACE_RENAME_COMMAND, AGENT_HERDR_WORKSPACE_ID, title]);
|
|
404
|
+
return true;
|
|
391
405
|
} catch (e) {
|
|
392
406
|
herdrLog(`[rename workspace] title=${JSON.stringify(title)} failed: ${(e as Error).message}`);
|
|
407
|
+
return false;
|
|
393
408
|
}
|
|
394
409
|
}
|
|
395
410
|
|
package/src/backends/orca.ts
CHANGED
|
@@ -211,8 +211,8 @@ function orcaExec(args: string[]): string {
|
|
|
211
211
|
return result.stdout;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
/** 调用 `orca` 命令,失败只记 log
|
|
215
|
-
function orcaExecSilent(args: string[]):
|
|
214
|
+
/** 调用 `orca` 命令,失败只记 log 不抛错,并返回执行是否成功。 */
|
|
215
|
+
function orcaExecSilent(args: string[]): boolean {
|
|
216
216
|
const cmdline = `orca ${args
|
|
217
217
|
.map((a) => (a.includes(" ") || a.includes('"') ? JSON.stringify(a) : a))
|
|
218
218
|
.join(" ")}`;
|
|
@@ -228,7 +228,9 @@ function orcaExecSilent(args: string[]): void {
|
|
|
228
228
|
(result.stderr ?? "").trim().slice(0, 200),
|
|
229
229
|
)}`,
|
|
230
230
|
);
|
|
231
|
+
return false;
|
|
231
232
|
}
|
|
233
|
+
return true;
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
// ── 对外 API ──
|
|
@@ -424,8 +426,8 @@ export function closeOrcaSurface(handle: string): void {
|
|
|
424
426
|
* 注意:rename 作用于 tab 标题,split pane 与源 pane 共享 tab ——
|
|
425
427
|
* 调用方需确认 target 是独立 tab(create() 产物或 agent 自己的 terminal)。
|
|
426
428
|
*/
|
|
427
|
-
export function renameOrcaTerminal(handle: string, name: string):
|
|
428
|
-
orcaExecSilent(["terminal", "rename", "--terminal", handle, "--title", name]);
|
|
429
|
+
export function renameOrcaTerminal(handle: string, name: string): boolean {
|
|
430
|
+
return orcaExecSilent(["terminal", "rename", "--terminal", handle, "--title", name]);
|
|
429
431
|
}
|
|
430
432
|
|
|
431
433
|
// ── BackendOps 适配器(薄包装以上原生函数,行为语义见各函数注释) ──
|
package/src/backends/otty.ts
CHANGED
|
@@ -194,7 +194,7 @@ function ottyExec(args: string[]): string {
|
|
|
194
194
|
* 调用 `otty` 命令,丢弃 stdout。用于 sendCommand / sendKeys / closePane 这类
|
|
195
195
|
* 无输出的命令。
|
|
196
196
|
*/
|
|
197
|
-
function ottyExecSilent(args: string[]):
|
|
197
|
+
function ottyExecSilent(args: string[]): boolean {
|
|
198
198
|
const cmdline = `otty ${args
|
|
199
199
|
.map((a) => (a.includes(" ") || a.includes('"') ? JSON.stringify(a) : a))
|
|
200
200
|
.join(" ")}`;
|
|
@@ -209,7 +209,9 @@ function ottyExecSilent(args: string[]): void {
|
|
|
209
209
|
(result.stderr ?? "").trim().slice(0, 200),
|
|
210
210
|
)}`,
|
|
211
211
|
);
|
|
212
|
+
return false;
|
|
212
213
|
}
|
|
214
|
+
return true;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
// ── Pane 数据结构 ──
|
|
@@ -368,7 +370,7 @@ export function createOttySurface(name: string): string {
|
|
|
368
370
|
return "";
|
|
369
371
|
}
|
|
370
372
|
state.add(newId);
|
|
371
|
-
|
|
373
|
+
// 新 pane 不代表独占 tab,保留创建时的 pane 标题。
|
|
372
374
|
ottyLog(`[create] mode=first dir=right from=${agentId} new=${newId} name=${JSON.stringify(name)}`);
|
|
373
375
|
return newId;
|
|
374
376
|
}
|
|
@@ -436,7 +438,7 @@ export function createOttySurface(name: string): string {
|
|
|
436
438
|
// split 源是 agent pane(不在状态机里),只需 add。
|
|
437
439
|
if (!recovered) state.advance();
|
|
438
440
|
state.add(newId);
|
|
439
|
-
|
|
441
|
+
// 新 pane 不代表独占 tab,保留创建时的 pane 标题。
|
|
440
442
|
ottyLog(`[create] mode=next dir=${direction} from=${target} new=${newId} name=${JSON.stringify(name)}`);
|
|
441
443
|
return newId;
|
|
442
444
|
});
|
|
@@ -565,16 +567,21 @@ export function closeOttySurface(paneId: string): void {
|
|
|
565
567
|
* 重命名 pane 对应的 tab。
|
|
566
568
|
* Otty 没有"pane -> tab id"的直接命令,所以用 `panes --json` 反查 tab_id。
|
|
567
569
|
*/
|
|
568
|
-
export function renameOttyTab(paneId: string, name: string):
|
|
570
|
+
export function renameOttyTab(paneId: string, name: string): boolean {
|
|
569
571
|
const tabId = getTabIdForPane(paneId);
|
|
570
572
|
if (!tabId) {
|
|
571
573
|
ottyLog(`[rename] pane=${paneId} no tab id found`);
|
|
572
|
-
return;
|
|
574
|
+
return false;
|
|
573
575
|
}
|
|
574
576
|
try {
|
|
575
|
-
ottyExecSilent(["tab", "rename", "--tab", tabId, name])
|
|
577
|
+
if (!ottyExecSilent(["tab", "rename", "--tab", tabId, name])) {
|
|
578
|
+
ottyLog(`[rename] pane=${paneId} tab=${tabId} name=${JSON.stringify(name)} failed`);
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
576
582
|
} catch (e) {
|
|
577
583
|
ottyLog(`[rename] pane=${paneId} tab=${tabId} name=${JSON.stringify(name)} failed: ${(e as Error).message}`);
|
|
584
|
+
return false;
|
|
578
585
|
}
|
|
579
586
|
}
|
|
580
587
|
|
package/src/backends/wezterm.ts
CHANGED
|
@@ -68,13 +68,7 @@ export const ops: BackendOps = {
|
|
|
68
68
|
throw new Error(`Unexpected wezterm split-pane output: ${rawId || "(empty)"}`);
|
|
69
69
|
}
|
|
70
70
|
const paneId = rawId;
|
|
71
|
-
|
|
72
|
-
execFileSync("wezterm", ["cli", "set-tab-title", "--pane-id", paneId, name], {
|
|
73
|
-
encoding: "utf8",
|
|
74
|
-
});
|
|
75
|
-
} catch {
|
|
76
|
-
// Optional — tab title is cosmetic.
|
|
77
|
-
}
|
|
71
|
+
// 分屏与父 pane 共享 tab,不能用子任务名称覆盖共享标题。
|
|
78
72
|
if (options?.activate) {
|
|
79
73
|
execFileSync("wezterm", weztermActivateArgs(paneId), { encoding: "utf8" });
|
|
80
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - sendCommand / sendLongCommand / sendEscape
|
|
11
11
|
* - readScreen / readScreenAsync
|
|
12
12
|
* - closeSurface
|
|
13
|
-
* - renameCurrentTab / renameAgent / renameWorkspace
|
|
13
|
+
* - renameCurrentTab / renameAgent / renameWorkspace / getRenameCapability
|
|
14
14
|
* - pollForExit 等待 surface 内进程退出(.exit sidecar / sentinel)
|
|
15
15
|
*
|
|
16
16
|
* 后端探测:
|
package/src/mux.ts
CHANGED
|
@@ -51,6 +51,7 @@ export {
|
|
|
51
51
|
closeSurface,
|
|
52
52
|
renameSurface,
|
|
53
53
|
renameAgent,
|
|
54
|
+
getRenameCapability,
|
|
54
55
|
renameCurrentTab,
|
|
55
56
|
renameWorkspace,
|
|
56
57
|
pollForExit,
|
|
@@ -62,6 +63,11 @@ export type {
|
|
|
62
63
|
CreateSurfaceSplitOptions,
|
|
63
64
|
SendLongCommandOptions,
|
|
64
65
|
ReadScreenOptions,
|
|
66
|
+
RenameOperation,
|
|
67
|
+
RenameTarget,
|
|
68
|
+
RenameBackend,
|
|
69
|
+
RenameCapability,
|
|
70
|
+
RenameResult,
|
|
65
71
|
} from "./surface.ts";
|
|
66
72
|
|
|
67
73
|
// ── Cmux 公开解析函数 ──
|
|
@@ -89,3 +95,19 @@ export {
|
|
|
89
95
|
renameHerdrTab,
|
|
90
96
|
renameHerdrWorkspace,
|
|
91
97
|
} from "./backends/herdr.ts";
|
|
98
|
+
|
|
99
|
+
// ── 精确目标和跨进程改名归属 ──
|
|
100
|
+
export {
|
|
101
|
+
TERMINAL_RENAME_CONTEXT_ENV,
|
|
102
|
+
createSurfaceRenameContext,
|
|
103
|
+
readSurfaceRenameContext,
|
|
104
|
+
resolveTerminalRenameTargets,
|
|
105
|
+
renameTerminalTarget,
|
|
106
|
+
} from "./rename.ts";
|
|
107
|
+
export type {
|
|
108
|
+
SurfaceRenameContext,
|
|
109
|
+
TerminalRenameTarget,
|
|
110
|
+
TerminalRenameOutcome,
|
|
111
|
+
ResolveRenameOptions,
|
|
112
|
+
RenameCommandRunner,
|
|
113
|
+
} from "./rename.ts";
|
package/src/rename.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { getMuxBackend, type MuxBackend } from "./detection.ts";
|
|
3
|
+
import { getRenameCapability, type RenameOperation, type RenameTarget } from "./surface.ts";
|
|
4
|
+
import { getCreatedHerdrTabId } from "./backends/herdr.ts";
|
|
5
|
+
import { AGENT_OTTY_PANE_ID, getTabIdForPane } from "./backends/otty.ts";
|
|
6
|
+
import { renameOrcaTerminal } from "./backends/orca.ts";
|
|
7
|
+
import { i18n } from "./i18n.ts";
|
|
8
|
+
|
|
9
|
+
export const TERMINAL_RENAME_CONTEXT_ENV = "PI_TERMINAL_RENAME_CONTEXT";
|
|
10
|
+
const CONTEXT_VERSION = 1;
|
|
11
|
+
const COMMAND_TIMEOUT_MS = 5_000;
|
|
12
|
+
const RENAME_COMMANDS = {
|
|
13
|
+
cmuxTab: ["rename-tab", "--surface"],
|
|
14
|
+
cmuxWorkspace: ["workspace-action", "--workspace"],
|
|
15
|
+
cmuxWorkspaceAction: ["--action", "rename", "--title"],
|
|
16
|
+
muxy: ["rename-pane", "--pane"],
|
|
17
|
+
tmuxTab: ["rename-window", "-t"],
|
|
18
|
+
tmuxWorkspace: ["rename-session", "-t"],
|
|
19
|
+
tmuxLookup: ["display-message", "-p", "-t"],
|
|
20
|
+
tmuxWindowId: "#{window_id}",
|
|
21
|
+
tmuxSessionId: "#{session_id}",
|
|
22
|
+
zellij: ["action", "rename-pane"],
|
|
23
|
+
paneId: "--pane-id",
|
|
24
|
+
weztermTab: ["cli", "set-tab-title", "--pane-id"],
|
|
25
|
+
weztermWorkspace: ["cli", "set-window-title", "--pane-id"],
|
|
26
|
+
herdr: "rename",
|
|
27
|
+
otty: ["tab", "rename", "--tab"],
|
|
28
|
+
} as const;
|
|
29
|
+
const BACKENDS = ["cmux", "muxy", "tmux", "zellij", "wezterm", "herdr", "otty", "orca"] as const;
|
|
30
|
+
|
|
31
|
+
export interface TerminalRenameTarget {
|
|
32
|
+
backend: MuxBackend;
|
|
33
|
+
operation: RenameOperation;
|
|
34
|
+
target: RenameTarget;
|
|
35
|
+
id: string;
|
|
36
|
+
scope: "surface" | "shared";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 启动方授予的单个 surface 改名范围;不是操作系统安全边界。 */
|
|
40
|
+
export interface SurfaceRenameContext {
|
|
41
|
+
version: 1;
|
|
42
|
+
backend: MuxBackend | null;
|
|
43
|
+
surface: string;
|
|
44
|
+
ownedTarget: { target: "pane" | "tab" | "terminal"; id: string } | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TerminalRenameOutcome =
|
|
48
|
+
| { status: "ready" | "renamed"; reference: TerminalRenameTarget }
|
|
49
|
+
| { status: "skipped"; operation: RenameOperation; reason: "unsupported" | "disabled" | "shared" | "unverified" | "missing-id"; setting?: string }
|
|
50
|
+
| { status: "failed"; operation: RenameOperation; error: string };
|
|
51
|
+
|
|
52
|
+
/** 只把可确认独占的 surface 授给子进程,绝不把 pane 扩大为共享窗口。 */
|
|
53
|
+
export function createSurfaceRenameContext(surface: string, backend = getMuxBackend()): SurfaceRenameContext {
|
|
54
|
+
let ownedTarget: SurfaceRenameContext["ownedTarget"] = null;
|
|
55
|
+
if (surface.startsWith("headless:")) backend = null;
|
|
56
|
+
if (surface) {
|
|
57
|
+
if (backend === "cmux") ownedTarget = { target: "tab", id: surface };
|
|
58
|
+
if (backend === "muxy" || backend === "zellij") ownedTarget = { target: "pane", id: surface };
|
|
59
|
+
if (backend === "herdr") {
|
|
60
|
+
const tabId = getCreatedHerdrTabId(surface);
|
|
61
|
+
ownedTarget = tabId ? { target: "tab", id: tabId } : { target: "pane", id: surface };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { version: CONTEXT_VERSION, backend, surface, ownedTarget };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 校验跨进程协议;损坏或未知版本不退回主会话权限。 */
|
|
68
|
+
export function readSurfaceRenameContext(env: NodeJS.ProcessEnv = process.env): SurfaceRenameContext | undefined {
|
|
69
|
+
const raw = env[TERMINAL_RENAME_CONTEXT_ENV];
|
|
70
|
+
if (raw === undefined) {
|
|
71
|
+
// 旧启动方没有归属信息时,只读其身份标志以收紧范围,不能猜测独占 tab。
|
|
72
|
+
if (env.PI_SUBAGENT_ID || env.PI_SUBAGENT_SURFACE) {
|
|
73
|
+
return { version: CONTEXT_VERSION, backend: null, surface: "", ownedTarget: null };
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
let value: unknown;
|
|
78
|
+
try { value = JSON.parse(raw); } catch { throw new Error(i18n.t("rename.invalidContext")); }
|
|
79
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(i18n.t("rename.invalidContext"));
|
|
80
|
+
const context = value as Record<string, unknown>;
|
|
81
|
+
const validBackend = context.backend === null || BACKENDS.some((backend) => backend === context.backend);
|
|
82
|
+
if (context.version !== CONTEXT_VERSION || !validBackend || typeof context.surface !== "string" ||
|
|
83
|
+
Object.keys(context).some((key) => !["version", "backend", "surface", "ownedTarget"].includes(key))) {
|
|
84
|
+
throw new Error(i18n.t("rename.invalidContext"));
|
|
85
|
+
}
|
|
86
|
+
if (context.ownedTarget !== null) {
|
|
87
|
+
if (!context.ownedTarget || typeof context.ownedTarget !== "object" || Array.isArray(context.ownedTarget)) {
|
|
88
|
+
throw new Error(i18n.t("rename.invalidContext"));
|
|
89
|
+
}
|
|
90
|
+
const owned = context.ownedTarget as Record<string, unknown>;
|
|
91
|
+
const targetByBackend: Partial<Record<MuxBackend, readonly string[]>> = {
|
|
92
|
+
cmux: ["tab"], muxy: ["pane"], zellij: ["pane"], herdr: ["pane", "tab"],
|
|
93
|
+
};
|
|
94
|
+
if (!context.surface || typeof owned.id !== "string" || !owned.id.trim() ||
|
|
95
|
+
Object.keys(owned).some((key) => !["target", "id"].includes(key)) ||
|
|
96
|
+
!targetByBackend[context.backend as MuxBackend]?.includes(owned.target as string) ||
|
|
97
|
+
(!(context.backend === "herdr" && owned.target === "tab") && owned.id !== context.surface)) {
|
|
98
|
+
throw new Error(i18n.t("rename.invalidContext"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return context as unknown as SurfaceRenameContext;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ResolveRenameOptions {
|
|
105
|
+
tab: boolean;
|
|
106
|
+
workspace: boolean;
|
|
107
|
+
backend?: MuxBackend | null;
|
|
108
|
+
env?: NodeJS.ProcessEnv;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 返回当前进程的明确目标 ID,不以当前焦点或第一个 tab 代替未知身份。 */
|
|
112
|
+
function currentTargetId(reference: { backend: MuxBackend; operation: RenameOperation; env: NodeJS.ProcessEnv }, query: RenameIdQuery): string | undefined {
|
|
113
|
+
const { backend, operation, env } = reference;
|
|
114
|
+
const surfaceKeys: Record<MuxBackend, string> = {
|
|
115
|
+
cmux: "CMUX_SURFACE_ID", muxy: "MUXY_PANE_ID", tmux: "TMUX_PANE", zellij: "ZELLIJ_PANE_ID",
|
|
116
|
+
wezterm: "WEZTERM_PANE", herdr: "HERDR_TAB_ID", otty: "OTTY_PANE_ID", orca: "ORCA_TERMINAL_HANDLE",
|
|
117
|
+
};
|
|
118
|
+
if (backend === "tmux") {
|
|
119
|
+
const pane = env.TMUX_PANE;
|
|
120
|
+
return pane ? query("tmux", [...RENAME_COMMANDS.tmuxLookup, pane,
|
|
121
|
+
operation === "tab" ? RENAME_COMMANDS.tmuxWindowId : RENAME_COMMANDS.tmuxSessionId]).trim() : undefined;
|
|
122
|
+
}
|
|
123
|
+
if (backend === "otty") {
|
|
124
|
+
const pane = env.OTTY_PANE_ID ?? AGENT_OTTY_PANE_ID;
|
|
125
|
+
return pane ? getTabIdForPane(pane) ?? undefined : undefined;
|
|
126
|
+
}
|
|
127
|
+
if (operation === "workspace" && backend === "cmux") return env.CMUX_WORKSPACE_ID;
|
|
128
|
+
if (operation === "workspace" && backend === "herdr") return env.HERDR_WORKSPACE_ID;
|
|
129
|
+
return env[surfaceKeys[backend]];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type RenameIdQuery = (command: string, args: string[]) => string;
|
|
133
|
+
|
|
134
|
+
/** 查询父 window/session 的明确身份,限制外部命令等待时间。 */
|
|
135
|
+
function queryRenameId(command: string, args: string[]): string {
|
|
136
|
+
return execFileSync(command, args, { encoding: "utf8", timeout: COMMAND_TIMEOUT_MS });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 解析一次批量改名的目标;各目标的失败、关闭或跳过分别保留。 */
|
|
140
|
+
export function resolveTerminalRenameTargets(options: ResolveRenameOptions, query: RenameIdQuery = queryRenameId): TerminalRenameOutcome[] {
|
|
141
|
+
const env = options.env ?? process.env;
|
|
142
|
+
const context = readSurfaceRenameContext(env);
|
|
143
|
+
const backend = options.backend === undefined ? getMuxBackend() : options.backend;
|
|
144
|
+
const outcomes: TerminalRenameOutcome[] = [];
|
|
145
|
+
for (const operation of ["workspace", "tab"] as const) {
|
|
146
|
+
if (!options[operation]) continue;
|
|
147
|
+
if (context) {
|
|
148
|
+
if (operation === "workspace") {
|
|
149
|
+
outcomes.push({ status: "skipped", operation, reason: "shared" });
|
|
150
|
+
} else if (!context.ownedTarget || !backend || context.backend !== backend) {
|
|
151
|
+
outcomes.push({ status: "skipped", operation, reason: "unverified" });
|
|
152
|
+
} else {
|
|
153
|
+
outcomes.push({ status: "ready", reference: { backend, operation, ...context.ownedTarget, scope: "surface" } });
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const capability = getRenameCapability(operation, backend, env);
|
|
158
|
+
if (capability.status !== "supported") {
|
|
159
|
+
outcomes.push({ status: "skipped", operation, reason: capability.status,
|
|
160
|
+
...(capability.status === "disabled" ? { setting: capability.setting } : {}) });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const id = currentTargetId({ backend: capability.backend, operation, env }, query);
|
|
165
|
+
outcomes.push(id?.trim() ? { status: "ready", reference: {
|
|
166
|
+
backend: capability.backend, operation, target: capability.backend === "orca" ? "tab" : capability.target, id,
|
|
167
|
+
scope: operation === "workspace" || ["tmux", "wezterm", "herdr", "otty", "orca"].includes(capability.backend) ? "shared" : "surface",
|
|
168
|
+
} } : { status: "skipped", operation, reason: "missing-id" });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
outcomes.push({ status: "failed", operation, error: error instanceof Error ? error.message : String(error) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return outcomes;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type RenameCommandRunner = (command: string, args: string[]) => void;
|
|
177
|
+
|
|
178
|
+
/** 改名命令设置超时,异常留给调用方转成逐目标结果。 */
|
|
179
|
+
function runRenameCommand(command: string, args: string[]): void {
|
|
180
|
+
execFileSync(command, args, { encoding: "utf8", timeout: COMMAND_TIMEOUT_MS });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 用解析时捕获的身份执行改名,不再查询当前焦点。 */
|
|
184
|
+
export function renameTerminalTarget(
|
|
185
|
+
reference: TerminalRenameTarget,
|
|
186
|
+
title: string,
|
|
187
|
+
dependencies: { run: RenameCommandRunner; renameOrca: typeof renameOrcaTerminal } = { run: runRenameCommand, renameOrca: renameOrcaTerminal },
|
|
188
|
+
): TerminalRenameOutcome {
|
|
189
|
+
const { backend, operation, target, id } = reference;
|
|
190
|
+
const { run, renameOrca } = dependencies;
|
|
191
|
+
try {
|
|
192
|
+
if (!id.trim()) throw new Error(i18n.t("rename.missingId"));
|
|
193
|
+
switch (backend) {
|
|
194
|
+
case "cmux":
|
|
195
|
+
run("cmux", operation === "tab" ? [...RENAME_COMMANDS.cmuxTab, id, title]
|
|
196
|
+
: [...RENAME_COMMANDS.cmuxWorkspace, id, ...RENAME_COMMANDS.cmuxWorkspaceAction, title]);
|
|
197
|
+
break;
|
|
198
|
+
case "muxy": run("muxy", [...RENAME_COMMANDS.muxy, id, title]); break;
|
|
199
|
+
case "tmux": run("tmux", [...(operation === "tab" ? RENAME_COMMANDS.tmuxTab : RENAME_COMMANDS.tmuxWorkspace), id, title]); break;
|
|
200
|
+
case "zellij": run("zellij", [...RENAME_COMMANDS.zellij, title, RENAME_COMMANDS.paneId, id]); break;
|
|
201
|
+
case "wezterm": run("wezterm", [...(operation === "tab" ? RENAME_COMMANDS.weztermTab : RENAME_COMMANDS.weztermWorkspace), id, title]); break;
|
|
202
|
+
case "herdr": run("herdr", [target, RENAME_COMMANDS.herdr, id, title]); break;
|
|
203
|
+
case "otty": run("otty", [...RENAME_COMMANDS.otty, id, title]); break;
|
|
204
|
+
case "orca":
|
|
205
|
+
if (!renameOrca(id, title)) throw new Error(i18n.t("error.renameIncomplete", { backend }));
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
return { status: "renamed", reference };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return { status: "failed", operation, error: error instanceof Error ? error.message : String(error) };
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/surface.ts
CHANGED
|
@@ -55,6 +55,10 @@ import { renameOrcaTerminal } from "./backends/orca.ts";
|
|
|
55
55
|
const execFileAsync = promisify(execFile);
|
|
56
56
|
const ORCA_BACKEND: MuxBackend = "orca";
|
|
57
57
|
const HERDR_BACKEND: MuxBackend = "herdr";
|
|
58
|
+
const TMUX_WINDOW_RENAME_SETTING = "PI_SUBAGENT_RENAME_TMUX_WINDOW";
|
|
59
|
+
const TMUX_SESSION_RENAME_SETTING = "PI_SUBAGENT_RENAME_TMUX_SESSION";
|
|
60
|
+
const HERDR_WORKSPACE_RENAME_SETTING = "PI_SUBAGENT_RENAME_HERDR_WORKSPACE";
|
|
61
|
+
const ENABLED_SETTING_VALUE = "1";
|
|
58
62
|
|
|
59
63
|
// ── 全键注册表 ──
|
|
60
64
|
|
|
@@ -406,139 +410,163 @@ export function renameAgent(surface: string, name: string): void {
|
|
|
406
410
|
}
|
|
407
411
|
}
|
|
408
412
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return;
|
|
413
|
+
export type RenameOperation = "tab" | "workspace";
|
|
414
|
+
export type RenameTarget = "tab" | "window" | "pane" | "workspace" | "session" | "terminal";
|
|
415
|
+
export type RenameBackend = MuxBackend | "headless";
|
|
416
|
+
|
|
417
|
+
export type RenameCapability =
|
|
418
|
+
| { status: "supported"; backend: MuxBackend; operation: RenameOperation; target: RenameTarget }
|
|
419
|
+
| { status: "unsupported"; backend: RenameBackend; operation: RenameOperation }
|
|
420
|
+
| { status: "disabled"; backend: MuxBackend; operation: RenameOperation; setting: string };
|
|
421
|
+
|
|
422
|
+
export type RenameResult =
|
|
423
|
+
| { status: "renamed"; backend: MuxBackend; operation: RenameOperation; target: RenameTarget }
|
|
424
|
+
| { status: "unsupported"; backend: RenameBackend; operation: RenameOperation }
|
|
425
|
+
| { status: "disabled"; backend: MuxBackend; operation: RenameOperation; setting: string }
|
|
426
|
+
| { status: "failed"; backend: MuxBackend; operation: RenameOperation; target: RenameTarget; error: string };
|
|
427
|
+
|
|
428
|
+
/** 返回指定后端的真实重命名语义,不执行终端命令。 */
|
|
429
|
+
export function getRenameCapability(
|
|
430
|
+
operation: RenameOperation,
|
|
431
|
+
backend: MuxBackend | null = getMuxBackend(),
|
|
432
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
433
|
+
): RenameCapability {
|
|
434
|
+
if (!backend) return { status: "unsupported", backend: "headless", operation };
|
|
435
|
+
|
|
436
|
+
if (operation === "tab") {
|
|
437
|
+
if (backend === "tmux" && env[TMUX_WINDOW_RENAME_SETTING] !== ENABLED_SETTING_VALUE) {
|
|
438
|
+
return { status: "disabled", backend, operation, setting: TMUX_WINDOW_RENAME_SETTING };
|
|
436
439
|
}
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
440
|
+
const target: Record<MuxBackend, RenameTarget> = {
|
|
441
|
+
muxy: "pane",
|
|
442
|
+
cmux: "tab",
|
|
443
|
+
tmux: "window",
|
|
444
|
+
zellij: "pane",
|
|
445
|
+
wezterm: "tab",
|
|
446
|
+
herdr: "tab",
|
|
447
|
+
otty: "tab",
|
|
448
|
+
orca: "terminal",
|
|
449
|
+
};
|
|
450
|
+
return { status: "supported", backend, operation, target: target[backend] };
|
|
444
451
|
}
|
|
445
452
|
|
|
446
|
-
if (backend === "
|
|
447
|
-
|
|
448
|
-
const args = ["cli", "set-tab-title"];
|
|
449
|
-
if (paneId) args.push("--pane-id", paneId);
|
|
450
|
-
args.push(title);
|
|
451
|
-
execFileSync("wezterm", args, { encoding: "utf8" });
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (backend === "herdr") {
|
|
456
|
-
renameHerdrTab(AGENT_HERDR_PANE_ID ?? "", title);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (backend === "otty") {
|
|
461
|
-
renameOttyTab(AGENT_OTTY_PANE_ID ?? "", title);
|
|
462
|
-
return;
|
|
453
|
+
if (backend === "tmux" && env[TMUX_SESSION_RENAME_SETTING] !== ENABLED_SETTING_VALUE) {
|
|
454
|
+
return { status: "disabled", backend, operation, setting: TMUX_SESSION_RENAME_SETTING };
|
|
463
455
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (!AGENT_ORCA_TERMINAL_HANDLE) throw new Error("ORCA_TERMINAL_HANDLE not set");
|
|
467
|
-
renameOrcaTerminal(AGENT_ORCA_TERMINAL_HANDLE, title);
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// zellij: rename the agent's own pane
|
|
472
|
-
const paneId = process.env.ZELLIJ_PANE_ID;
|
|
473
|
-
if (paneId) {
|
|
474
|
-
execFileSync("zellij", ["action", "rename-pane", title, "--pane-id", paneId], { encoding: "utf8" });
|
|
475
|
-
} else {
|
|
476
|
-
execFileSync("zellij", ["action", "rename-pane", title], { encoding: "utf8" });
|
|
456
|
+
if (backend === "herdr" && env[HERDR_WORKSPACE_RENAME_SETTING] !== ENABLED_SETTING_VALUE) {
|
|
457
|
+
return { status: "disabled", backend, operation, setting: HERDR_WORKSPACE_RENAME_SETTING };
|
|
477
458
|
}
|
|
459
|
+
if (backend === "cmux") return { status: "supported", backend, operation, target: "workspace" };
|
|
460
|
+
if (backend === "tmux") return { status: "supported", backend, operation, target: "session" };
|
|
461
|
+
if (backend === "wezterm") return { status: "supported", backend, operation, target: "window" };
|
|
462
|
+
if (backend === "herdr") return { status: "supported", backend, operation, target: "workspace" };
|
|
463
|
+
return { status: "unsupported", backend, operation };
|
|
478
464
|
}
|
|
479
465
|
|
|
480
|
-
/**
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (backend === "muxy") {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
466
|
+
/** 将终端命令异常封装为可判别的失败结果。 */
|
|
467
|
+
function failedRenameResult(
|
|
468
|
+
capability: Extract<RenameCapability, { status: "supported" }>,
|
|
469
|
+
error: unknown,
|
|
470
|
+
): RenameResult {
|
|
471
|
+
return {
|
|
472
|
+
status: "failed",
|
|
473
|
+
backend: capability.backend,
|
|
474
|
+
operation: capability.operation,
|
|
475
|
+
target: capability.target,
|
|
476
|
+
error: error instanceof Error ? error.message : String(error),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
498
479
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
480
|
+
/** 重命名当前 tab/window/pane,并返回实际目标与执行结果。 */
|
|
481
|
+
export function renameCurrentTab(title: string): RenameResult {
|
|
482
|
+
const capability = getRenameCapability("tab");
|
|
483
|
+
if (capability.status !== "supported") return capability;
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const backend = capability.backend;
|
|
487
|
+
if (backend === "cmux") {
|
|
488
|
+
const surfaceId = process.env.CMUX_SURFACE_ID;
|
|
489
|
+
if (!surfaceId) throw new Error("CMUX_SURFACE_ID not set");
|
|
490
|
+
execSync(`cmux rename-tab --surface ${shellEscape(surfaceId)} ${shellEscape(title)}`, {
|
|
491
|
+
encoding: "utf8",
|
|
492
|
+
});
|
|
493
|
+
} else if (backend === "muxy") {
|
|
494
|
+
const paneId = AGENT_MUXY_PANE_ID;
|
|
495
|
+
if (!paneId) throw new Error("MUXY_PANE_ID not set");
|
|
496
|
+
execFileSync("muxy", ["rename-pane", "--pane", paneId, title], { encoding: "utf8" });
|
|
497
|
+
} else if (backend === "tmux") {
|
|
498
|
+
const paneId = process.env.TMUX_PANE;
|
|
499
|
+
if (!paneId) throw new Error("TMUX_PANE not set");
|
|
500
|
+
const windowId = execFileSync("tmux", ["display-message", "-p", "-t", paneId, "#{window_id}"], {
|
|
501
|
+
encoding: "utf8",
|
|
502
|
+
}).trim();
|
|
503
|
+
execFileSync("tmux", ["rename-window", "-t", windowId, title], { encoding: "utf8" });
|
|
504
|
+
} else if (backend === "wezterm") {
|
|
505
|
+
const paneId = process.env.WEZTERM_PANE;
|
|
506
|
+
const args = ["cli", "set-tab-title"];
|
|
507
|
+
if (paneId) args.push("--pane-id", paneId);
|
|
508
|
+
args.push(title);
|
|
509
|
+
execFileSync("wezterm", args, { encoding: "utf8" });
|
|
510
|
+
} else if (backend === "herdr") {
|
|
511
|
+
if (!renameHerdrTab(AGENT_HERDR_PANE_ID ?? "", title)) {
|
|
512
|
+
throw new Error(i18n.t("error.renameIncomplete", { backend: "Herdr" }));
|
|
513
|
+
}
|
|
514
|
+
} else if (backend === "otty") {
|
|
515
|
+
if (!renameOttyTab(AGENT_OTTY_PANE_ID ?? "", title)) {
|
|
516
|
+
throw new Error(i18n.t("error.renameIncomplete", { backend: "Otty" }));
|
|
517
|
+
}
|
|
518
|
+
} else if (backend === "orca") {
|
|
519
|
+
if (!AGENT_ORCA_TERMINAL_HANDLE) throw new Error("ORCA_TERMINAL_HANDLE not set");
|
|
520
|
+
if (!renameOrcaTerminal(AGENT_ORCA_TERMINAL_HANDLE, title)) {
|
|
521
|
+
throw new Error(i18n.t("error.renameIncomplete", { backend: "Orca" }));
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
const paneId = process.env.ZELLIJ_PANE_ID;
|
|
525
|
+
const args = ["action", "rename-pane", title];
|
|
526
|
+
if (paneId) args.push("--pane-id", paneId);
|
|
527
|
+
execFileSync("zellij", args, { encoding: "utf8" });
|
|
502
528
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"tmux",
|
|
507
|
-
["display-message", "-p", "-t", paneId, "#{session_id}"],
|
|
508
|
-
{ encoding: "utf8" },
|
|
509
|
-
).trim();
|
|
510
|
-
execFileSync("tmux", ["rename-session", "-t", sessionId, title], { encoding: "utf8" });
|
|
511
|
-
return;
|
|
529
|
+
return { status: "renamed", backend, operation: capability.operation, target: capability.target };
|
|
530
|
+
} catch (error) {
|
|
531
|
+
return failedRenameResult(capability, error);
|
|
512
532
|
}
|
|
533
|
+
}
|
|
513
534
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
535
|
+
/** 重命名当前 workspace/session/window,并明确报告不支持或未启用。 */
|
|
536
|
+
export function renameWorkspace(title: string): RenameResult {
|
|
537
|
+
const capability = getRenameCapability("workspace");
|
|
538
|
+
if (capability.status !== "supported") return capability;
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const backend = capability.backend;
|
|
542
|
+
if (backend === "cmux") {
|
|
543
|
+
execSync(`cmux workspace-action --action rename --title ${shellEscape(title)}`, {
|
|
544
|
+
encoding: "utf8",
|
|
545
|
+
});
|
|
546
|
+
} else if (backend === "tmux") {
|
|
547
|
+
const paneId = process.env.TMUX_PANE;
|
|
548
|
+
if (!paneId) throw new Error("TMUX_PANE not set");
|
|
549
|
+
const sessionId = execFileSync(
|
|
550
|
+
"tmux",
|
|
551
|
+
["display-message", "-p", "-t", paneId, "#{session_id}"],
|
|
552
|
+
{ encoding: "utf8" },
|
|
553
|
+
).trim();
|
|
554
|
+
execFileSync("tmux", ["rename-session", "-t", sessionId, title], { encoding: "utf8" });
|
|
555
|
+
} else if (backend === "wezterm") {
|
|
556
|
+
const paneId = process.env.WEZTERM_PANE;
|
|
557
|
+
const args = ["cli", "set-window-title"];
|
|
558
|
+
if (paneId) args.push("--pane-id", paneId);
|
|
559
|
+
args.push(title);
|
|
520
560
|
execFileSync("wezterm", args, { encoding: "utf8" });
|
|
521
|
-
}
|
|
522
|
-
|
|
561
|
+
} else if (backend === "herdr") {
|
|
562
|
+
if (!renameHerdrWorkspace(title)) {
|
|
563
|
+
throw new Error(i18n.t("error.renameIncomplete", { backend: "Herdr" }));
|
|
564
|
+
}
|
|
523
565
|
}
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (backend === "herdr") {
|
|
528
|
-
renameHerdrWorkspace(title);
|
|
529
|
-
return;
|
|
566
|
+
return { status: "renamed", backend, operation: capability.operation, target: capability.target };
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return failedRenameResult(capability, error);
|
|
530
569
|
}
|
|
531
|
-
|
|
532
|
-
if (backend === "otty") {
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (backend === "orca") {
|
|
537
|
-
// orca: 无独立 workspace 概念(worktree 由 Orca 管理),跳过
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// zellij: skip session rename
|
|
542
570
|
}
|
|
543
571
|
|
|
544
572
|
export { renameHerdrTab, renameHerdrWorkspace };
|