pi-terminal-mux 0.6.4 → 0.6.6
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 +2 -2
- package/src/backends/orca.ts +96 -36
- package/src/index.ts +3 -1
- package/src/orca.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-terminal-mux",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
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.8.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "24.12.4",
|
package/src/backends/orca.ts
CHANGED
|
@@ -363,58 +363,118 @@ export function readOrcaScreen(handle: string, lines = 50): string {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* null = 查询本身失败(runtime 不可达/响应异常),不能当作“已关闭”。
|
|
366
|
+
* 目标 terminal 的状态(三态,供关闭流程判定)。
|
|
367
|
+
* present 附带所在 tab 的占用情况:tabCount = 同一 tabId 下的 terminal 数量(tabId 缺失时为 null)。
|
|
369
368
|
*/
|
|
370
|
-
|
|
369
|
+
export type OrcaTerminalState =
|
|
370
|
+
| { kind: "absent" }
|
|
371
|
+
| { kind: "present"; tabId: string | null; tabCount: number | null }
|
|
372
|
+
| { kind: "unknown" };
|
|
373
|
+
|
|
374
|
+
/** 关闭 pane 的下一步动作。 */
|
|
375
|
+
export type OrcaCloseDecision = "done" | "retry-close" | "close-tab" | "skip-tab";
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 解析 `terminal list --json`,得到 handle 的状态与所在 tab 的 terminal 数量。
|
|
379
|
+
* 纯函数便于单测;列表结构异常时返回 unknown(不可判定,不能当作“已关闭”)。
|
|
380
|
+
*/
|
|
381
|
+
export function parseOrcaTerminalTabInfo(raw: string, handle: string): OrcaTerminalState {
|
|
382
|
+
const parsed = parseOrcaJson(raw);
|
|
383
|
+
const terminals = parsed?.result?.["terminals"];
|
|
384
|
+
if (!Array.isArray(terminals)) return { kind: "unknown" };
|
|
385
|
+
|
|
386
|
+
const entries = terminals.filter(
|
|
387
|
+
(t): t is Record<string, unknown> => typeof t === "object" && t !== null,
|
|
388
|
+
);
|
|
389
|
+
const self = entries.find((t) => t["handle"] === handle);
|
|
390
|
+
if (!self) return { kind: "absent" };
|
|
391
|
+
|
|
392
|
+
const tabId = typeof self["tabId"] === "string" ? self["tabId"] : null;
|
|
393
|
+
const tabCount = tabId === null ? null : entries.filter((t) => t["tabId"] === tabId).length;
|
|
394
|
+
return { kind: "present", tabId, tabCount };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 判定关闭 pane 的下一步动作。
|
|
399
|
+
*
|
|
400
|
+
* 关键约束:split 出来的子 pane 与源 pane(可能是 agent 自己的会话)共享同一个 tab,
|
|
401
|
+
* 而 `--tab` 会关掉整个 tab —— 所以只有确认 tab 内只有自己时才允许升级到 `--tab`。
|
|
402
|
+
* unknown(list 查询失败/结构异常)不可判定,同样不升级:宁可留下残留 pane,
|
|
403
|
+
* 也不能连带杀掉同 tab 的其它会话。
|
|
404
|
+
*/
|
|
405
|
+
export function decideOrcaCloseStep(params: {
|
|
406
|
+
state: OrcaTerminalState;
|
|
407
|
+
targetIsAgentPane: boolean;
|
|
408
|
+
retried: boolean;
|
|
409
|
+
}): OrcaCloseDecision {
|
|
410
|
+
const { state, targetIsAgentPane, retried } = params;
|
|
411
|
+
if (state.kind === "absent") return "done";
|
|
412
|
+
if (state.kind === "unknown") return "skip-tab";
|
|
413
|
+
if (targetIsAgentPane) return "skip-tab";
|
|
414
|
+
if (!retried) return "retry-close";
|
|
415
|
+
return state.tabCount === 1 ? "close-tab" : "skip-tab";
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** 查询 terminal 状态;查询失败/响应异常统一归一为 unknown。 */
|
|
419
|
+
function queryOrcaTerminalState(handle: string): OrcaTerminalState {
|
|
371
420
|
try {
|
|
372
|
-
|
|
373
|
-
const parsed = parseOrcaJson(raw);
|
|
374
|
-
const terminals = parsed?.result?.["terminals"];
|
|
375
|
-
if (!Array.isArray(terminals)) return null;
|
|
376
|
-
return terminals.some(
|
|
377
|
-
(t) => typeof t === "object" && t !== null && (t as Record<string, unknown>)["handle"] === handle,
|
|
378
|
-
);
|
|
421
|
+
return parseOrcaTerminalTabInfo(orcaExec(["terminal", "list", "--json"]), handle);
|
|
379
422
|
} catch {
|
|
380
|
-
return
|
|
423
|
+
return { kind: "unknown" };
|
|
381
424
|
}
|
|
382
425
|
}
|
|
383
426
|
|
|
384
427
|
/**
|
|
385
428
|
* 关闭 terminal。
|
|
386
429
|
*
|
|
387
|
-
* create()
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* 3.
|
|
392
|
-
*
|
|
430
|
+
* create() 与 createSplit() 产物可能与其他 pane 共享 tab,因此必须区分「关 pane」和「关 tab」:
|
|
431
|
+
* 1. `terminal close`(只关这个 pane)
|
|
432
|
+
* 2. 校验 list:仍在则再 `terminal close` 一次后复查 —— close 生效与子进程真正退出之间存在
|
|
433
|
+
* 竞态,首次校验的“仍在”多为误判。
|
|
434
|
+
* 3. 仍存在时,仅当同一 tab 内只有这一个 terminal(tabCount===1)且目标不是 agent pane,
|
|
435
|
+
* 才补 `terminal close --tab`(单 pane tab 的残留情况);共享 tab 一律不关 tab,否则会
|
|
436
|
+
* 连带关掉同 tab 的其它会话。
|
|
437
|
+
* 4. list 查询失败(unknown)视为不可判定,不升级为关 tab,只记 warn。
|
|
438
|
+
* 5. 清理 BFS marker;失败仅 log warn,绝不 throw(避免打断 pollForExit 退出流程)。
|
|
393
439
|
*/
|
|
394
440
|
export function closeOrcaSurface(handle: string): void {
|
|
441
|
+
const targetIsAgentPane = handle === AGENT_ORCA_TERMINAL_HANDLE;
|
|
395
442
|
try {
|
|
396
443
|
orcaExecSilent(["terminal", "close", "--terminal", handle, "--json"]);
|
|
444
|
+
let state = queryOrcaTerminalState(handle);
|
|
397
445
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
if (exists === null) {
|
|
404
|
-
// 查询失败不能当作已关闭;继续尝试 --tab 兜底
|
|
405
|
-
orcaLog(`[close] WARN terminal ${handle} list query failed, cannot verify close`);
|
|
446
|
+
if (decideOrcaCloseStep({ state, targetIsAgentPane, retried: false }) === "retry-close") {
|
|
447
|
+
orcaExecSilent(["terminal", "close", "--terminal", handle, "--json"]);
|
|
448
|
+
state = queryOrcaTerminalState(handle);
|
|
406
449
|
}
|
|
407
450
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
451
|
+
switch (decideOrcaCloseStep({ state, targetIsAgentPane, retried: true })) {
|
|
452
|
+
case "done":
|
|
453
|
+
orcaLog(`[close] terminal ${handle} closed`);
|
|
454
|
+
return;
|
|
455
|
+
case "close-tab": {
|
|
456
|
+
const tabId = state.kind === "present" ? state.tabId : null;
|
|
457
|
+
orcaLog(`[close] terminal ${handle} sole occupant of tab ${tabId}, trying --tab`);
|
|
458
|
+
orcaExecSilent(["terminal", "close", "--terminal", handle, "--tab", "--json"]);
|
|
459
|
+
if (queryOrcaTerminalState(handle).kind === "absent") {
|
|
460
|
+
orcaLog(`[close] terminal ${handle} closed via --tab`);
|
|
461
|
+
} else {
|
|
462
|
+
orcaLog(`[close] WARN terminal ${handle} still present after close --tab`);
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
default:
|
|
467
|
+
// skip-tab:绝不关整个 tab,只记录原因(残留 pane 交给用户手动关)。
|
|
468
|
+
if (state.kind === "unknown") {
|
|
469
|
+
orcaLog(`[close] WARN terminal ${handle} list query failed/unusable, skip --tab (unverifiable)`);
|
|
470
|
+
} else if (targetIsAgentPane) {
|
|
471
|
+
orcaLog(`[close] WARN terminal ${handle} is the agent pane itself, skip --tab`);
|
|
472
|
+
} else if (state.kind === "present") {
|
|
473
|
+
orcaLog(
|
|
474
|
+
`[close] WARN terminal ${handle} shares tab ${state.tabId} with ${(state.tabCount ?? 0) - 1} sibling terminal(s), skip --tab to protect them`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
418
478
|
}
|
|
419
479
|
} finally {
|
|
420
480
|
cleanupOrcaStateForSurface(handle);
|
package/src/index.ts
CHANGED
|
@@ -75,10 +75,12 @@ export {
|
|
|
75
75
|
sendOrcaCommand,
|
|
76
76
|
sendOrcaEscape,
|
|
77
77
|
readOrcaScreen,
|
|
78
|
+
parseOrcaTerminalTabInfo,
|
|
79
|
+
decideOrcaCloseStep,
|
|
78
80
|
closeOrcaSurface,
|
|
79
81
|
renameOrcaTerminal,
|
|
80
82
|
} from "./orca.ts";
|
|
81
|
-
export type { OrcaEnvelope } from "./orca.ts";
|
|
83
|
+
export type { OrcaEnvelope, OrcaTerminalState, OrcaCloseDecision } from "./orca.ts";
|
|
82
84
|
|
|
83
85
|
// ── 便捷函数 ──
|
|
84
86
|
import {
|
package/src/orca.ts
CHANGED
|
@@ -9,16 +9,18 @@ export {
|
|
|
9
9
|
AGENT_ORCA_TERMINAL_HANDLE,
|
|
10
10
|
closeOrcaSurface,
|
|
11
11
|
createOrcaSurface,
|
|
12
|
+
decideOrcaCloseStep,
|
|
12
13
|
extractOrcaCreateHandle,
|
|
13
14
|
extractOrcaReadTail,
|
|
14
15
|
extractOrcaSplitHandle,
|
|
15
16
|
isOrcaRuntimeAvailable,
|
|
16
17
|
orcaSplitDirection,
|
|
17
18
|
parseOrcaJson,
|
|
19
|
+
parseOrcaTerminalTabInfo,
|
|
18
20
|
readOrcaScreen,
|
|
19
21
|
renameOrcaTerminal,
|
|
20
22
|
sendOrcaCommand,
|
|
21
23
|
sendOrcaEscape,
|
|
22
24
|
splitOrcaTerminal,
|
|
23
25
|
} from "./backends/orca.ts";
|
|
24
|
-
export type { OrcaEnvelope } from "./backends/orca.ts";
|
|
26
|
+
export type { OrcaEnvelope, OrcaTerminalState, OrcaCloseDecision } from "./backends/orca.ts";
|