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:
+4
-284
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user