pi-teams 0.8.7 → 0.9.2
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/extensions/index.ts +83 -5
- package/package.json +1 -1
- package/src/adapters/cmux-adapter.test.ts +309 -0
- package/src/adapters/cmux-adapter.ts +5 -1
- package/src/adapters/terminal-registry.ts +13 -9
- package/src/adapters/tmux-adapter.test.ts +231 -0
- package/src/adapters/tmux-adapter.ts +70 -6
- package/src/adapters/windows-adapter.test.ts +86 -155
- package/src/utils/paths.ts +4 -0
- package/src/utils/runtime.test.ts +171 -0
- package/src/utils/runtime.ts +168 -0
- package/src/utils/terminal-adapter.ts +2 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tmux Adapter Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
6
|
+
import { TmuxAdapter } from "./tmux-adapter";
|
|
7
|
+
import * as terminalAdapter from "../utils/terminal-adapter";
|
|
8
|
+
|
|
9
|
+
describe("TmuxAdapter", () => {
|
|
10
|
+
let adapter: TmuxAdapter;
|
|
11
|
+
let mockExecCommand: ReturnType<typeof vi.spyOn>;
|
|
12
|
+
const originalTmux = process.env.TMUX;
|
|
13
|
+
const originalTmuxPane = process.env.TMUX_PANE;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
adapter = new TmuxAdapter();
|
|
17
|
+
mockExecCommand = vi.spyOn(terminalAdapter, "execCommand");
|
|
18
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
19
|
+
process.env.TMUX_PANE = "%16";
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
|
|
25
|
+
if (originalTmux === undefined) delete process.env.TMUX;
|
|
26
|
+
else process.env.TMUX = originalTmux;
|
|
27
|
+
|
|
28
|
+
if (originalTmuxPane === undefined) delete process.env.TMUX_PANE;
|
|
29
|
+
else process.env.TMUX_PANE = originalTmuxPane;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should have the correct name", () => {
|
|
33
|
+
expect(adapter.name).toBe("tmux");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should detect tmux when TMUX is set", () => {
|
|
37
|
+
expect(adapter.detect()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should target the originating pane and its window when spawning", () => {
|
|
41
|
+
mockExecCommand.mockImplementation((_bin: string, args: string[]) => {
|
|
42
|
+
if (
|
|
43
|
+
args[0] === "display-message" &&
|
|
44
|
+
args[1] === "-p" &&
|
|
45
|
+
args[2] === "-t" &&
|
|
46
|
+
args[3] === "%16" &&
|
|
47
|
+
args[4] === "#{pane_id}"
|
|
48
|
+
) {
|
|
49
|
+
return { stdout: "%16", stderr: "", status: 0 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (args[0] === "split-window") {
|
|
53
|
+
return { stdout: "%42", stderr: "", status: 0 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
args[0] === "display-message" &&
|
|
58
|
+
args[1] === "-p" &&
|
|
59
|
+
args[2] === "-t" &&
|
|
60
|
+
args[3] === "%16" &&
|
|
61
|
+
args[4] === "#{window_id}"
|
|
62
|
+
) {
|
|
63
|
+
return { stdout: "@7", stderr: "", status: 0 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { stdout: "", stderr: "", status: 0 };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const paneId = adapter.spawn({
|
|
70
|
+
name: "agent-1",
|
|
71
|
+
cwd: "/tmp/project",
|
|
72
|
+
command: "pi",
|
|
73
|
+
env: { PI_TEAM_NAME: "team-1", PI_AGENT_NAME: "agent-1", OTHER: "ignored" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(paneId).toBe("%42");
|
|
77
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
78
|
+
"tmux",
|
|
79
|
+
[
|
|
80
|
+
"split-window",
|
|
81
|
+
"-h", "-dP",
|
|
82
|
+
"-F", "#{pane_id}",
|
|
83
|
+
"-t", "%16",
|
|
84
|
+
"-c", "/tmp/project",
|
|
85
|
+
"env", "PI_TEAM_NAME=team-1", "PI_AGENT_NAME=agent-1",
|
|
86
|
+
"sh", "-c", "pi",
|
|
87
|
+
]
|
|
88
|
+
);
|
|
89
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
90
|
+
"tmux",
|
|
91
|
+
["set-window-option", "-t", "@7", "main-pane-width", "60%"]
|
|
92
|
+
);
|
|
93
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
94
|
+
"tmux",
|
|
95
|
+
["select-layout", "-t", "@7", "main-vertical"]
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should prefer an explicit anchor pane when spawning", () => {
|
|
100
|
+
mockExecCommand.mockImplementation((_bin: string, args: string[]) => {
|
|
101
|
+
if (
|
|
102
|
+
args[0] === "display-message" &&
|
|
103
|
+
args[1] === "-p" &&
|
|
104
|
+
args[2] === "-t" &&
|
|
105
|
+
args[3] === "%3" &&
|
|
106
|
+
args[4] === "#{pane_id}"
|
|
107
|
+
) {
|
|
108
|
+
return { stdout: "%3", stderr: "", status: 0 };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args[0] === "split-window") {
|
|
112
|
+
return { stdout: "%42", stderr: "", status: 0 };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
args[0] === "display-message" &&
|
|
117
|
+
args[1] === "-p" &&
|
|
118
|
+
args[2] === "-t" &&
|
|
119
|
+
args[3] === "%3" &&
|
|
120
|
+
args[4] === "#{window_id}"
|
|
121
|
+
) {
|
|
122
|
+
return { stdout: "@9", stderr: "", status: 0 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { stdout: "", stderr: "", status: 0 };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const paneId = adapter.spawn({
|
|
129
|
+
name: "agent-1",
|
|
130
|
+
cwd: "/tmp/project",
|
|
131
|
+
command: "pi",
|
|
132
|
+
env: { PI_TEAM_NAME: "team-1", PI_AGENT_NAME: "agent-1" },
|
|
133
|
+
anchorPaneId: "%3",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(paneId).toBe("%42");
|
|
137
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
138
|
+
"tmux",
|
|
139
|
+
[
|
|
140
|
+
"split-window",
|
|
141
|
+
"-h", "-dP",
|
|
142
|
+
"-F", "#{pane_id}",
|
|
143
|
+
"-t", "%3",
|
|
144
|
+
"-c", "/tmp/project",
|
|
145
|
+
"env", "PI_TEAM_NAME=team-1", "PI_AGENT_NAME=agent-1",
|
|
146
|
+
"sh", "-c", "pi",
|
|
147
|
+
]
|
|
148
|
+
);
|
|
149
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
150
|
+
"tmux",
|
|
151
|
+
["set-window-option", "-t", "@9", "main-pane-width", "60%"]
|
|
152
|
+
);
|
|
153
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
154
|
+
"tmux",
|
|
155
|
+
["select-layout", "-t", "@9", "main-vertical"]
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should fall back to the current pane when the explicit anchor is stale", () => {
|
|
160
|
+
mockExecCommand.mockImplementation((_bin: string, args: string[]) => {
|
|
161
|
+
if (
|
|
162
|
+
args[0] === "display-message" &&
|
|
163
|
+
args[1] === "-p" &&
|
|
164
|
+
args[2] === "-t" &&
|
|
165
|
+
args[3] === "%3" &&
|
|
166
|
+
args[4] === "#{pane_id}"
|
|
167
|
+
) {
|
|
168
|
+
return { stdout: "", stderr: "no such pane", status: 1 };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
args[0] === "display-message" &&
|
|
173
|
+
args[1] === "-p" &&
|
|
174
|
+
args[2] === "-t" &&
|
|
175
|
+
args[3] === "%16" &&
|
|
176
|
+
args[4] === "#{pane_id}"
|
|
177
|
+
) {
|
|
178
|
+
return { stdout: "%16", stderr: "", status: 0 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (args[0] === "split-window") {
|
|
182
|
+
return { stdout: "%42", stderr: "", status: 0 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
args[0] === "display-message" &&
|
|
187
|
+
args[1] === "-p" &&
|
|
188
|
+
args[2] === "-t" &&
|
|
189
|
+
args[3] === "%16" &&
|
|
190
|
+
args[4] === "#{window_id}"
|
|
191
|
+
) {
|
|
192
|
+
return { stdout: "@7", stderr: "", status: 0 };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { stdout: "", stderr: "", status: 0 };
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const paneId = adapter.spawn({
|
|
199
|
+
name: "agent-1",
|
|
200
|
+
cwd: "/tmp/project",
|
|
201
|
+
command: "pi",
|
|
202
|
+
env: { PI_TEAM_NAME: "team-1", PI_AGENT_NAME: "agent-1" },
|
|
203
|
+
anchorPaneId: "%3",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(paneId).toBe("%42");
|
|
207
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
208
|
+
"tmux",
|
|
209
|
+
[
|
|
210
|
+
"split-window",
|
|
211
|
+
"-h", "-dP",
|
|
212
|
+
"-F", "#{pane_id}",
|
|
213
|
+
"-t", "%16",
|
|
214
|
+
"-c", "/tmp/project",
|
|
215
|
+
"env", "PI_TEAM_NAME=team-1", "PI_AGENT_NAME=agent-1",
|
|
216
|
+
"sh", "-c", "pi",
|
|
217
|
+
]
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should target the current pane when setting the title", () => {
|
|
222
|
+
mockExecCommand.mockReturnValue({ stdout: "", stderr: "", status: 0 });
|
|
223
|
+
|
|
224
|
+
adapter.setTitle("team: agent-1");
|
|
225
|
+
|
|
226
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
227
|
+
"tmux",
|
|
228
|
+
["select-pane", "-t", "%16", "-T", "team: agent-1"]
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -15,19 +15,70 @@ export class TmuxAdapter implements TerminalAdapter {
|
|
|
15
15
|
return !!process.env.TMUX;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
private getCurrentPaneId(): string | null {
|
|
19
|
+
const paneId = process.env.TMUX_PANE?.trim();
|
|
20
|
+
return paneId ? paneId : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private getWindowIdForPane(paneId: string | null | undefined): string | null {
|
|
24
|
+
if (!paneId) return null;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = execCommand("tmux", ["display-message", "-p", "-t", paneId, "#{window_id}"]);
|
|
28
|
+
if (result.status !== 0) return null;
|
|
29
|
+
|
|
30
|
+
const windowId = result.stdout.trim();
|
|
31
|
+
return windowId || null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private isPaneUsable(paneId: string | null | undefined): paneId is string {
|
|
38
|
+
if (!paneId) return false;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = execCommand("tmux", ["display-message", "-p", "-t", paneId, "#{pane_id}"]);
|
|
42
|
+
return result.status === 0 && result.stdout.trim() === paneId;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private getOriginPaneId(preferredPaneId?: string | null): string | null {
|
|
49
|
+
if (this.isPaneUsable(preferredPaneId)) {
|
|
50
|
+
return preferredPaneId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const currentPaneId = this.getCurrentPaneId();
|
|
54
|
+
if (this.isPaneUsable(currentPaneId)) {
|
|
55
|
+
return currentPaneId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
spawn(options: SpawnOptions): string {
|
|
19
62
|
const envArgs = Object.entries(options.env)
|
|
20
63
|
.filter(([k]) => k.startsWith("PI_"))
|
|
21
64
|
.map(([k, v]) => `${k}=${v}`);
|
|
22
65
|
|
|
66
|
+
const originPaneId = this.getOriginPaneId(options.anchorPaneId);
|
|
23
67
|
const tmuxArgs = [
|
|
24
68
|
"split-window",
|
|
25
69
|
"-h", "-dP",
|
|
26
70
|
"-F", "#{pane_id}",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
if (originPaneId) {
|
|
74
|
+
tmuxArgs.push("-t", originPaneId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
tmuxArgs.push(
|
|
27
78
|
"-c", options.cwd,
|
|
28
79
|
"env", ...envArgs,
|
|
29
80
|
"sh", "-c", options.command
|
|
30
|
-
|
|
81
|
+
);
|
|
31
82
|
|
|
32
83
|
const result = execCommand("tmux", tmuxArgs);
|
|
33
84
|
|
|
@@ -35,11 +86,20 @@ export class TmuxAdapter implements TerminalAdapter {
|
|
|
35
86
|
throw new Error(`tmux spawn failed with status ${result.status}: ${result.stderr}`);
|
|
36
87
|
}
|
|
37
88
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
89
|
+
const newPaneId = result.stdout.trim();
|
|
90
|
+
const layoutTarget = this.getWindowIdForPane(originPaneId) ?? this.getWindowIdForPane(newPaneId);
|
|
91
|
+
|
|
92
|
+
// Apply layout to the exact window that contains the spawned pane so the
|
|
93
|
+
// split always stays anchored to the intended tmux window.
|
|
94
|
+
if (layoutTarget) {
|
|
95
|
+
execCommand("tmux", ["set-window-option", "-t", layoutTarget, "main-pane-width", "60%"]);
|
|
96
|
+
execCommand("tmux", ["select-layout", "-t", layoutTarget, "main-vertical"]);
|
|
97
|
+
} else {
|
|
98
|
+
execCommand("tmux", ["set-window-option", "main-pane-width", "60%"]);
|
|
99
|
+
execCommand("tmux", ["select-layout", "main-vertical"]);
|
|
100
|
+
}
|
|
41
101
|
|
|
42
|
-
return
|
|
102
|
+
return newPaneId;
|
|
43
103
|
}
|
|
44
104
|
|
|
45
105
|
kill(paneId: string): void {
|
|
@@ -69,7 +129,11 @@ export class TmuxAdapter implements TerminalAdapter {
|
|
|
69
129
|
|
|
70
130
|
setTitle(title: string): void {
|
|
71
131
|
try {
|
|
72
|
-
|
|
132
|
+
const paneId = this.getCurrentPaneId();
|
|
133
|
+
const args = paneId
|
|
134
|
+
? ["select-pane", "-t", paneId, "-T", title]
|
|
135
|
+
: ["select-pane", "-T", title];
|
|
136
|
+
execCommand("tmux", args);
|
|
73
137
|
} catch {
|
|
74
138
|
// Ignore errors
|
|
75
139
|
}
|