made extension tmux-only, replaced cmux.ts with tmux.ts (~1368 -> 354 lines)

Removed the cmux, zellij, and wezterm backends, the MuxBackend type, the
PI_SUBAGENT_MUX override, and dead exports (renameCurrentTab, renameWorkspace,
sendEscape, isFishShell, exitStatusVar). The ten-function surface API used by
index.ts is unchanged. Integration harness and tests are tmux-only; README and
package.json now state tmux-only with a fork note pointing multi-multiplexer
users at the upstream project.
This commit is contained in:
Amos
2026-08-20 18:48:14 +03:00
parent 865648c4f1
commit 9cbc5b4878
9 changed files with 421 additions and 1848 deletions
+4 -284
View File
@@ -30,20 +30,7 @@ import {
summarizeSessionStats,
} from "../pi-extension/subagents/session.ts";
import {
shellEscape,
isCmuxAvailable,
isWezTermAvailable,
parseCmuxFocusedSnapshot,
parseCmuxFocusedSnapshotFromJson,
parseCmuxJson,
parseCmuxPaneRefForSurface,
parseCmuxPaneRefForSurfaceFromJson,
canSplitZellijPane,
predictZellijSplitDirection,
selectZellijPlacement,
selectZellijStackPlacement,
} from "../pi-extension/subagents/cmux.ts";
import { shellEscape } from "../pi-extension/subagents/tmux.ts";
import {
advanceStatusState,
capStatusLines,
@@ -69,7 +56,7 @@ import {
runningChildrenCount,
} from "../pi-extension/subagents/subagent-done.ts";
import subagentDoneExtension from "../pi-extension/subagents/subagent-done.ts";
import { __pollForExitTest__ } from "../pi-extension/subagents/cmux.ts";
import { __pollForExitTest__ } from "../pi-extension/subagents/tmux.ts";
// --- Helpers ---
@@ -1754,7 +1741,7 @@ describe("subagent-done.ts", () => {
});
});
describe("cmux.ts interpretExitSidecar", () => {
describe("tmux.ts interpretExitSidecar", () => {
const { interpretExitSidecar } = __pollForExitTest__;
it("no longer decodes ping payloads (ask_question keeps the session open instead)", () => {
@@ -2665,7 +2652,7 @@ describe("subagent display helpers", () => {
});
});
describe("cmux.ts", () => {
describe("tmux.ts", () => {
describe("shellEscape", () => {
it("wraps in single quotes", () => {
assert.equal(shellEscape("hello"), "'hello'");
@@ -2688,271 +2675,4 @@ describe("cmux.ts", () => {
assert.ok(escaped.includes("$world"));
});
});
describe("parseCmuxFocusedSnapshot", () => {
it("parses focused surface and pane refs", () => {
assert.deepEqual(
parseCmuxFocusedSnapshot({ focused: { surface_ref: "surface:3", pane_ref: "pane:2" } }),
{ surfaceRef: "surface:3", paneRef: "pane:2" },
);
});
it("does not fall back to caller refs", () => {
assert.equal(
parseCmuxFocusedSnapshot({ caller: { surface_ref: "surface:1", pane_ref: "pane:1" } }),
null,
);
});
it("returns null for malformed values", () => {
assert.equal(parseCmuxFocusedSnapshot(null), null);
assert.equal(parseCmuxFocusedSnapshot({ focused: {} }), null);
});
});
describe("parseCmuxJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxJson("not json"), null);
});
it("parses valid JSON text", () => {
assert.deepEqual(parseCmuxJson('{"ok":true}'), { ok: true });
});
});
describe("parseCmuxFocusedSnapshotFromJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxFocusedSnapshotFromJson("not json"), null);
});
it("returns null when focused is absent or not an object", () => {
assert.equal(
parseCmuxFocusedSnapshotFromJson('{"focused":null,"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"}}'),
null,
);
assert.equal(
parseCmuxFocusedSnapshotFromJson('{"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"}}'),
null,
);
});
it("parses focused refs without falling back to caller refs", () => {
assert.deepEqual(
parseCmuxFocusedSnapshotFromJson(
'{"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"},"focused":{"surface_ref":"surface:2","pane_ref":"pane:3"}}',
),
{ surfaceRef: "surface:2", paneRef: "pane:3" },
);
});
});
describe("parseCmuxPaneRefForSurface", () => {
it("parses top-level pane refs for a surface", () => {
assert.equal(
parseCmuxPaneRefForSurface({ surface_ref: "surface:7", pane_ref: "pane:4" }, "surface:7"),
"pane:4",
);
});
it("parses caller pane refs for identify --surface output", () => {
assert.equal(
parseCmuxPaneRefForSurface(
{ caller: { surface_ref: "surface:7", pane_ref: "pane:4" } },
"surface:7",
),
"pane:4",
);
});
it("returns null when the surface does not match", () => {
assert.equal(
parseCmuxPaneRefForSurface({ surface_ref: "surface:8", pane_ref: "pane:4" }, "surface:7"),
null,
);
});
});
describe("parseCmuxPaneRefForSurfaceFromJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxPaneRefForSurfaceFromJson("not json", "surface:7"), null);
});
it("parses caller refs from cmux identify --surface JSON text", () => {
assert.equal(
parseCmuxPaneRefForSurfaceFromJson(
'{"caller":{"surface_ref":"surface:7","pane_ref":"pane:4"}}',
"surface:7",
),
"pane:4",
);
});
});
describe("zellij placement", () => {
const pane = (overrides: any) => ({
id: 1,
is_plugin: false,
is_floating: false,
is_selectable: true,
exited: false,
pane_rows: 20,
pane_columns: 80,
tab_id: 1,
...overrides,
});
it("matches Zellij direction and minimum split rules", () => {
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 5, pane_columns: 11 })), "right");
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 11, pane_columns: 5 })), "down");
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 5, pane_columns: 10 })), null);
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 4, pane_columns: 80 })), null);
assert.equal(canSplitZellijPane(pane({ pane_rows: 5, pane_columns: 11 })), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 11, pane_columns: 5 })), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 5, pane_columns: 10 })), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 4, pane_columns: 80 })), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 30, pane_columns: 100 }), 80, 20), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 45, pane_columns: 100 }), 80, 20), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 30, pane_columns: 170 }), 80, 20), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 31, pane_columns: 47 }), 50, 10), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 31, pane_columns: 77 }), 50, 10), true);
});
it("uses tab-scoped split only when all Zellij split candidates are safe", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 40, pane_columns: 120 }),
pane({ id: 11, tab_id: 1, pane_rows: 120, pane_columns: 100 }),
pane({ id: 12, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.deepEqual(plan, {
mode: "split",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
splitDirection: "down",
});
});
it("stacks when any Zellij split candidate would fall below Pi's configured minimum", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 100, pane_columns: 47 }),
pane({ id: 11, tab_id: 1, pane_rows: 31, pane_columns: 77 }),
],
10,
50,
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("stacks when Zellij would split a pane below Pi's usable minimum", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 20, pane_columns: 20 }),
pane({ id: 11, tab_id: 1, pane_rows: 18, pane_columns: 60 }),
pane({ id: 12, tab_id: 1, pane_rows: 10, pane_columns: 70 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("never chooses the parent pane as the stack target", () => {
const plan = selectZellijStackPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 60, pane_columns: 200 }),
pane({ id: 11, tab_id: 1, pane_rows: 10, pane_columns: 20 }),
pane({ id: 12, tab_id: 1, pane_rows: 8, pane_columns: 30 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 12,
targetPaneId: 12,
tabId: 1,
});
});
it("does not stack when the only usable pane is the parent", () => {
const plan = selectZellijStackPlacement(
[pane({ id: 10, tab_id: 1, pane_rows: 60, pane_columns: 200 })],
10,
);
assert.equal(plan, null);
});
it("stacks on the largest usable non-parent pane when none can split", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 5, pane_columns: 10 }),
pane({ id: 11, tab_id: 1, pane_rows: 6, pane_columns: 8 }),
pane({ id: 12, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("ignores floating, plugin, exited, unselectable, and other-tab panes", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 5, pane_columns: 10 }),
pane({ id: 11, tab_id: 1, pane_rows: 60, pane_columns: 200, is_floating: true }),
pane({ id: 12, tab_id: 1, pane_rows: 60, pane_columns: 200, is_plugin: true }),
pane({ id: 13, tab_id: 1, pane_rows: 60, pane_columns: 200, exited: true }),
pane({ id: 14, tab_id: 1, pane_rows: 60, pane_columns: 200, is_selectable: false }),
pane({ id: 15, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.equal(plan, null);
});
it("returns null when the parent pane cannot be found", () => {
assert.equal(selectZellijPlacement([pane({ id: 10 })], 99), null);
});
});
describe("isCmuxAvailable", () => {
it("returns boolean based on CMUX_SOCKET_PATH", () => {
// Can't easily mock env in node:test, just verify it returns a boolean
const result = isCmuxAvailable();
assert.equal(typeof result, "boolean");
});
});
describe("isWezTermAvailable", () => {
it("returns boolean based on WEZTERM_UNIX_SOCKET", () => {
const result = isWezTermAvailable();
assert.equal(typeof result, "boolean");
});
});
});