@taranek/orche 0.0.31 → 0.0.33
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/dist/layout.js +3 -1
- package/dist/layout.test.d.ts +1 -0
- package/dist/layout.test.js +107 -0
- package/dist/multiplexers/cmux.d.ts +1 -1
- package/dist/multiplexers/cmux.js +6 -2
- package/package.json +1 -1
- package/src/layout.test.ts +138 -0
- package/src/layout.ts +3 -1
- package/src/multiplexers/cmux.ts +6 -2
package/dist/layout.js
CHANGED
|
@@ -19,7 +19,9 @@ export function buildLayout(mux, session, node, worktreePath) {
|
|
|
19
19
|
for (let i = 1; i < node.panes.length; i++) {
|
|
20
20
|
const child = node.panes[i];
|
|
21
21
|
const sizePercent = child.size ?? Math.floor(100 / (node.panes.length - i + 1));
|
|
22
|
-
|
|
22
|
+
// Chain off the previous sibling (like collectPaneMap) so flat columns keep
|
|
23
|
+
// left-to-right order once splitPane honors its target surface.
|
|
24
|
+
mux.splitPane(paneIds[i - 1], node.direction, sizePercent, worktreePath);
|
|
23
25
|
paneIds.push(mux.getActivePaneId(session));
|
|
24
26
|
}
|
|
25
27
|
const allPaneIds = mux.listPaneIds(session);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { buildLayout } from "./layout.js";
|
|
6
|
+
/**
|
|
7
|
+
* A fake multiplexer that mimics cmux: each split creates a fresh surface that
|
|
8
|
+
* becomes the active pane (cmux auto-focuses the new split). It records every
|
|
9
|
+
* splitPane target so tests can assert which surface each split was anchored to.
|
|
10
|
+
*/
|
|
11
|
+
class FakeMux {
|
|
12
|
+
name = "fake";
|
|
13
|
+
panes = ["p0"];
|
|
14
|
+
splitCalls = [];
|
|
15
|
+
commands = [];
|
|
16
|
+
createSession() { }
|
|
17
|
+
killSession() { }
|
|
18
|
+
attach() { }
|
|
19
|
+
getActivePaneId() {
|
|
20
|
+
return this.panes[this.panes.length - 1];
|
|
21
|
+
}
|
|
22
|
+
listPaneIds() {
|
|
23
|
+
return [...this.panes];
|
|
24
|
+
}
|
|
25
|
+
splitPane(target, direction) {
|
|
26
|
+
this.splitCalls.push({ target, direction });
|
|
27
|
+
this.panes.push(`p${this.panes.length}`);
|
|
28
|
+
}
|
|
29
|
+
focusPane() { }
|
|
30
|
+
renamePaneTitle() { }
|
|
31
|
+
sendCommand(target, command) {
|
|
32
|
+
this.commands.push({ target, command });
|
|
33
|
+
}
|
|
34
|
+
isInsideSession() {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
detectFirstPane() {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
describe("buildLayout", () => {
|
|
42
|
+
let tmp;
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tmp = mkdtempSync(path.join(os.tmpdir(), "orche-layout-"));
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
it("chains flat sibling splits off the previous sibling (left-to-right order)", () => {
|
|
50
|
+
const mux = new FakeMux();
|
|
51
|
+
const layout = {
|
|
52
|
+
direction: "horizontal",
|
|
53
|
+
panes: [
|
|
54
|
+
{ name: "a", command: "cmd-a" },
|
|
55
|
+
{ name: "b", command: "cmd-b" },
|
|
56
|
+
{ name: "c", command: "cmd-c" },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
buildLayout(mux, "sess", layout, tmp);
|
|
60
|
+
// Two splits to produce three columns, each anchored to the prior sibling
|
|
61
|
+
// (NOT all anchored to the first pane) so they render a|b|c in order.
|
|
62
|
+
expect(mux.splitCalls).toEqual([
|
|
63
|
+
{ target: "p0", direction: "horizontal" },
|
|
64
|
+
{ target: "p1", direction: "horizontal" },
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
it("anchors a nested split's children to that branch's surface", () => {
|
|
68
|
+
const mux = new FakeMux();
|
|
69
|
+
// vertical[ agent, horizontal[db, be, fe] ]
|
|
70
|
+
const layout = {
|
|
71
|
+
direction: "vertical",
|
|
72
|
+
panes: [
|
|
73
|
+
{ name: "agent", command: "claude" },
|
|
74
|
+
{
|
|
75
|
+
direction: "horizontal",
|
|
76
|
+
panes: [
|
|
77
|
+
{ name: "db", command: "db-up" },
|
|
78
|
+
{ name: "be", command: "be-up" },
|
|
79
|
+
{ name: "fe", command: "fe-up" },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
buildLayout(mux, "sess", layout, tmp);
|
|
85
|
+
// 1 vertical split (agent | branch) + 2 horizontal splits for the columns.
|
|
86
|
+
expect(mux.splitCalls).toEqual([
|
|
87
|
+
{ target: "p0", direction: "vertical" },
|
|
88
|
+
{ target: "p1", direction: "horizontal" },
|
|
89
|
+
{ target: "p2", direction: "horizontal" },
|
|
90
|
+
]);
|
|
91
|
+
const session = JSON.parse(readFileSync(path.join(tmp, ".orche", "session.json"), "utf8"));
|
|
92
|
+
expect(session.panes).toEqual({
|
|
93
|
+
agent: "p0",
|
|
94
|
+
db: "p1",
|
|
95
|
+
be: "p2",
|
|
96
|
+
fe: "p3",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("records a single pane with no splits", () => {
|
|
100
|
+
const mux = new FakeMux();
|
|
101
|
+
const layout = { name: "solo", command: "echo hi" };
|
|
102
|
+
buildLayout(mux, "sess", layout, tmp);
|
|
103
|
+
expect(mux.splitCalls).toEqual([]);
|
|
104
|
+
const session = JSON.parse(readFileSync(path.join(tmp, ".orche", "session.json"), "utf8"));
|
|
105
|
+
expect(session.panes).toEqual({ solo: "p0" });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -7,7 +7,7 @@ export declare class CmuxMultiplexer implements Multiplexer {
|
|
|
7
7
|
attach(_name: string): void;
|
|
8
8
|
getActivePaneId(_session: string): string;
|
|
9
9
|
listPaneIds(_session: string): string[];
|
|
10
|
-
splitPane(
|
|
10
|
+
splitPane(target: string, direction: "horizontal" | "vertical", _sizePercent: number, cwd: string): void;
|
|
11
11
|
focusPane(_paneId: string): void;
|
|
12
12
|
renamePaneTitle(paneId: string, name: string): void;
|
|
13
13
|
sendCommand(target: string, command: string): void;
|
|
@@ -40,10 +40,14 @@ export class CmuxMultiplexer {
|
|
|
40
40
|
log("listPaneIds →", this.surfaces);
|
|
41
41
|
return [...this.surfaces];
|
|
42
42
|
}
|
|
43
|
-
splitPane(
|
|
43
|
+
splitPane(target, direction, _sizePercent, cwd) {
|
|
44
44
|
const cmuxDir = direction === "horizontal" ? "right" : "down";
|
|
45
|
+
// Split the requested surface explicitly. Without --surface, cmux splits
|
|
46
|
+
// whatever is focused, which lands nested splits on the wrong pane
|
|
47
|
+
// (focusPane is a no-op and sendCommand refocuses the agent surface).
|
|
48
|
+
const targetFlag = target ? ` --surface ${target}` : "";
|
|
45
49
|
// new-split returns "OK surface:<ref> workspace:<ref>"
|
|
46
|
-
const output = run(`cmux new-split ${cmuxDir}`);
|
|
50
|
+
const output = run(`cmux new-split ${cmuxDir}${targetFlag}`);
|
|
47
51
|
const match = output.match(/surface:(\S+)/);
|
|
48
52
|
if (match) {
|
|
49
53
|
const newRef = `surface:${match[1]}`;
|
package/package.json
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { buildLayout } from "./layout.js";
|
|
6
|
+
import type { Multiplexer } from "./multiplexer.js";
|
|
7
|
+
import type { PaneConfig, SplitConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
interface SplitCall {
|
|
10
|
+
target: string;
|
|
11
|
+
direction: "horizontal" | "vertical";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A fake multiplexer that mimics cmux: each split creates a fresh surface that
|
|
16
|
+
* becomes the active pane (cmux auto-focuses the new split). It records every
|
|
17
|
+
* splitPane target so tests can assert which surface each split was anchored to.
|
|
18
|
+
*/
|
|
19
|
+
class FakeMux implements Multiplexer {
|
|
20
|
+
readonly name = "fake";
|
|
21
|
+
panes: string[] = ["p0"];
|
|
22
|
+
splitCalls: SplitCall[] = [];
|
|
23
|
+
commands: { target: string; command: string }[] = [];
|
|
24
|
+
|
|
25
|
+
createSession(): void {}
|
|
26
|
+
killSession(): void {}
|
|
27
|
+
attach(): void {}
|
|
28
|
+
|
|
29
|
+
getActivePaneId(): string {
|
|
30
|
+
return this.panes[this.panes.length - 1];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
listPaneIds(): string[] {
|
|
34
|
+
return [...this.panes];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
splitPane(target: string, direction: "horizontal" | "vertical"): void {
|
|
38
|
+
this.splitCalls.push({ target, direction });
|
|
39
|
+
this.panes.push(`p${this.panes.length}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
focusPane(): void {}
|
|
43
|
+
renamePaneTitle(): void {}
|
|
44
|
+
sendCommand(target: string, command: string): void {
|
|
45
|
+
this.commands.push({ target, command });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
isInsideSession(): boolean {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
detectFirstPane(): string | undefined {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("buildLayout", () => {
|
|
57
|
+
let tmp: string;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
tmp = mkdtempSync(path.join(os.tmpdir(), "orche-layout-"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("chains flat sibling splits off the previous sibling (left-to-right order)", () => {
|
|
68
|
+
const mux = new FakeMux();
|
|
69
|
+
const layout: SplitConfig = {
|
|
70
|
+
direction: "horizontal",
|
|
71
|
+
panes: [
|
|
72
|
+
{ name: "a", command: "cmd-a" },
|
|
73
|
+
{ name: "b", command: "cmd-b" },
|
|
74
|
+
{ name: "c", command: "cmd-c" },
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
buildLayout(mux, "sess", layout, tmp);
|
|
79
|
+
|
|
80
|
+
// Two splits to produce three columns, each anchored to the prior sibling
|
|
81
|
+
// (NOT all anchored to the first pane) so they render a|b|c in order.
|
|
82
|
+
expect(mux.splitCalls).toEqual([
|
|
83
|
+
{ target: "p0", direction: "horizontal" },
|
|
84
|
+
{ target: "p1", direction: "horizontal" },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("anchors a nested split's children to that branch's surface", () => {
|
|
89
|
+
const mux = new FakeMux();
|
|
90
|
+
// vertical[ agent, horizontal[db, be, fe] ]
|
|
91
|
+
const layout: SplitConfig = {
|
|
92
|
+
direction: "vertical",
|
|
93
|
+
panes: [
|
|
94
|
+
{ name: "agent", command: "claude" },
|
|
95
|
+
{
|
|
96
|
+
direction: "horizontal",
|
|
97
|
+
panes: [
|
|
98
|
+
{ name: "db", command: "db-up" },
|
|
99
|
+
{ name: "be", command: "be-up" },
|
|
100
|
+
{ name: "fe", command: "fe-up" },
|
|
101
|
+
],
|
|
102
|
+
} as SplitConfig,
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
buildLayout(mux, "sess", layout, tmp);
|
|
107
|
+
|
|
108
|
+
// 1 vertical split (agent | branch) + 2 horizontal splits for the columns.
|
|
109
|
+
expect(mux.splitCalls).toEqual([
|
|
110
|
+
{ target: "p0", direction: "vertical" },
|
|
111
|
+
{ target: "p1", direction: "horizontal" },
|
|
112
|
+
{ target: "p2", direction: "horizontal" },
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const session = JSON.parse(
|
|
116
|
+
readFileSync(path.join(tmp, ".orche", "session.json"), "utf8")
|
|
117
|
+
);
|
|
118
|
+
expect(session.panes).toEqual({
|
|
119
|
+
agent: "p0",
|
|
120
|
+
db: "p1",
|
|
121
|
+
be: "p2",
|
|
122
|
+
fe: "p3",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("records a single pane with no splits", () => {
|
|
127
|
+
const mux = new FakeMux();
|
|
128
|
+
const layout: PaneConfig = { name: "solo", command: "echo hi" };
|
|
129
|
+
|
|
130
|
+
buildLayout(mux, "sess", layout, tmp);
|
|
131
|
+
|
|
132
|
+
expect(mux.splitCalls).toEqual([]);
|
|
133
|
+
const session = JSON.parse(
|
|
134
|
+
readFileSync(path.join(tmp, ".orche", "session.json"), "utf8")
|
|
135
|
+
);
|
|
136
|
+
expect(session.panes).toEqual({ solo: "p0" });
|
|
137
|
+
});
|
|
138
|
+
});
|
package/src/layout.ts
CHANGED
|
@@ -34,7 +34,9 @@ export function buildLayout(
|
|
|
34
34
|
for (let i = 1; i < node.panes.length; i++) {
|
|
35
35
|
const child = node.panes[i];
|
|
36
36
|
const sizePercent = child.size ?? Math.floor(100 / (node.panes.length - i + 1));
|
|
37
|
-
|
|
37
|
+
// Chain off the previous sibling (like collectPaneMap) so flat columns keep
|
|
38
|
+
// left-to-right order once splitPane honors its target surface.
|
|
39
|
+
mux.splitPane(paneIds[i - 1], node.direction, sizePercent, worktreePath);
|
|
38
40
|
paneIds.push(mux.getActivePaneId(session));
|
|
39
41
|
}
|
|
40
42
|
|
package/src/multiplexers/cmux.ts
CHANGED
|
@@ -51,14 +51,18 @@ export class CmuxMultiplexer implements Multiplexer {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
splitPane(
|
|
54
|
-
|
|
54
|
+
target: string,
|
|
55
55
|
direction: "horizontal" | "vertical",
|
|
56
56
|
_sizePercent: number,
|
|
57
57
|
cwd: string
|
|
58
58
|
): void {
|
|
59
59
|
const cmuxDir = direction === "horizontal" ? "right" : "down";
|
|
60
|
+
// Split the requested surface explicitly. Without --surface, cmux splits
|
|
61
|
+
// whatever is focused, which lands nested splits on the wrong pane
|
|
62
|
+
// (focusPane is a no-op and sendCommand refocuses the agent surface).
|
|
63
|
+
const targetFlag = target ? ` --surface ${target}` : "";
|
|
60
64
|
// new-split returns "OK surface:<ref> workspace:<ref>"
|
|
61
|
-
const output = run(`cmux new-split ${cmuxDir}`);
|
|
65
|
+
const output = run(`cmux new-split ${cmuxDir}${targetFlag}`);
|
|
62
66
|
|
|
63
67
|
const match = output.match(/surface:(\S+)/);
|
|
64
68
|
if (match) {
|