@agentuity/opencode 0.1.41 → 0.1.43
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/README.md +3 -10
- package/dist/agents/lead.d.ts +1 -1
- package/dist/agents/lead.d.ts.map +1 -1
- package/dist/agents/lead.js +2 -3
- package/dist/agents/lead.js.map +1 -1
- package/dist/background/manager.d.ts +1 -0
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +6 -0
- package/dist/background/manager.js.map +1 -1
- package/dist/plugin/hooks/cadence.d.ts.map +1 -1
- package/dist/plugin/hooks/cadence.js +3 -1
- package/dist/plugin/hooks/cadence.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +54 -11
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/skills/frontmatter.js +1 -1
- package/dist/skills/frontmatter.js.map +1 -1
- package/dist/tmux/executor.d.ts +57 -6
- package/dist/tmux/executor.d.ts.map +1 -1
- package/dist/tmux/executor.js +676 -57
- package/dist/tmux/executor.js.map +1 -1
- package/dist/tmux/index.d.ts +1 -1
- package/dist/tmux/index.d.ts.map +1 -1
- package/dist/tmux/index.js +1 -1
- package/dist/tmux/index.js.map +1 -1
- package/dist/tmux/manager.d.ts +70 -0
- package/dist/tmux/manager.d.ts.map +1 -1
- package/dist/tmux/manager.js +357 -22
- package/dist/tmux/manager.js.map +1 -1
- package/dist/tmux/state-query.d.ts.map +1 -1
- package/dist/tmux/state-query.js +4 -1
- package/dist/tmux/state-query.js.map +1 -1
- package/dist/tmux/types.d.ts +11 -0
- package/dist/tmux/types.d.ts.map +1 -1
- package/dist/tmux/types.js.map +1 -1
- package/dist/tmux/utils.d.ts +17 -0
- package/dist/tmux/utils.d.ts.map +1 -1
- package/dist/tmux/utils.js +39 -0
- package/dist/tmux/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/agents/lead.ts +2 -3
- package/src/background/manager.ts +6 -0
- package/src/plugin/hooks/cadence.ts +2 -1
- package/src/plugin/plugin.ts +67 -11
- package/src/skills/frontmatter.ts +1 -1
- package/src/tmux/executor.ts +748 -55
- package/src/tmux/index.ts +6 -0
- package/src/tmux/manager.ts +410 -21
- package/src/tmux/state-query.ts +4 -1
- package/src/tmux/types.ts +12 -0
- package/src/tmux/utils.ts +39 -0
package/dist/tmux/executor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runTmuxCommand, runTmuxCommandSync } from './utils';
|
|
2
|
+
import { spawn, spawnSync } from 'bun';
|
|
2
3
|
/**
|
|
3
4
|
* Escape a string for safe use in shell commands.
|
|
4
5
|
* Wraps in single quotes and escapes any internal single quotes.
|
|
@@ -9,10 +10,285 @@ function shellEscape(str) {
|
|
|
9
10
|
}
|
|
10
11
|
/** Maximum retries for recursive spawn attempts to prevent infinite loops */
|
|
11
12
|
const MAX_SPAWN_RETRIES = 3;
|
|
13
|
+
const OPENCODE_TAG = '@agentuity.opencode';
|
|
14
|
+
const OPENCODE_SERVER_TAG = '@agentuity.opencode.server';
|
|
15
|
+
const OPENCODE_OWNER_TAG = '@agentuity.opencode.ownerPid';
|
|
16
|
+
const OPENCODE_INSTANCE_TAG = '@agentuity.opencode.instance';
|
|
17
|
+
const OPENCODE_SESSION_TAG = '@agentuity.opencode.session';
|
|
18
|
+
const PROCESS_TERM_WAIT_MS = 1000;
|
|
19
|
+
function isProcessAlive(pid) {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const code = error.code;
|
|
26
|
+
return code !== 'ESRCH';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function getPanePid(paneId) {
|
|
30
|
+
if (!paneId)
|
|
31
|
+
return undefined;
|
|
32
|
+
const result = await runTmuxCommand(['display', '-p', '-t', paneId, '#{pane_pid}']);
|
|
33
|
+
if (!result.success)
|
|
34
|
+
return undefined;
|
|
35
|
+
const pid = Number(result.output.trim());
|
|
36
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return pid;
|
|
39
|
+
}
|
|
40
|
+
export function getPanePidSync(paneId) {
|
|
41
|
+
if (!paneId)
|
|
42
|
+
return undefined;
|
|
43
|
+
const result = runTmuxCommandSync(['display', '-p', '-t', paneId, '#{pane_pid}']);
|
|
44
|
+
if (!result.success)
|
|
45
|
+
return undefined;
|
|
46
|
+
const pid = Number(result.output.trim());
|
|
47
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
return pid;
|
|
50
|
+
}
|
|
51
|
+
async function resolvePanePidWithRetry(paneId, maxAttempts = 3, delayMs = 75) {
|
|
52
|
+
let pid = await getPanePid(paneId);
|
|
53
|
+
if (pid)
|
|
54
|
+
return pid;
|
|
55
|
+
for (let attempt = 1; attempt < maxAttempts; attempt += 1) {
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
57
|
+
pid = await getPanePid(paneId);
|
|
58
|
+
if (pid)
|
|
59
|
+
return pid;
|
|
60
|
+
}
|
|
61
|
+
return pid;
|
|
62
|
+
}
|
|
63
|
+
async function setWindowTags(windowId, ownership) {
|
|
64
|
+
await runTmuxCommand(['set-option', '-w', '-t', windowId, OPENCODE_TAG, '1']);
|
|
65
|
+
await runTmuxCommand([
|
|
66
|
+
'set-option',
|
|
67
|
+
'-w',
|
|
68
|
+
'-t',
|
|
69
|
+
windowId,
|
|
70
|
+
OPENCODE_SERVER_TAG,
|
|
71
|
+
ownership.serverKey,
|
|
72
|
+
]);
|
|
73
|
+
await runTmuxCommand([
|
|
74
|
+
'set-option',
|
|
75
|
+
'-w',
|
|
76
|
+
'-t',
|
|
77
|
+
windowId,
|
|
78
|
+
OPENCODE_OWNER_TAG,
|
|
79
|
+
String(ownership.ownerPid),
|
|
80
|
+
]);
|
|
81
|
+
await runTmuxCommand([
|
|
82
|
+
'set-option',
|
|
83
|
+
'-w',
|
|
84
|
+
'-t',
|
|
85
|
+
windowId,
|
|
86
|
+
OPENCODE_INSTANCE_TAG,
|
|
87
|
+
ownership.instanceId,
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
async function setPaneTags(paneId, ownership, sessionId) {
|
|
91
|
+
await runTmuxCommand([
|
|
92
|
+
'set-option',
|
|
93
|
+
'-p',
|
|
94
|
+
'-t',
|
|
95
|
+
paneId,
|
|
96
|
+
OPENCODE_INSTANCE_TAG,
|
|
97
|
+
ownership.instanceId,
|
|
98
|
+
]);
|
|
99
|
+
if (sessionId) {
|
|
100
|
+
await runTmuxCommand(['set-option', '-p', '-t', paneId, OPENCODE_SESSION_TAG, sessionId]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function findOwnedAgentsWindow(serverKey, instanceId, tmuxSessionId) {
|
|
104
|
+
const result = await runTmuxCommand([
|
|
105
|
+
'list-windows',
|
|
106
|
+
'-a',
|
|
107
|
+
'-F',
|
|
108
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}\t#{session_id}`,
|
|
109
|
+
]);
|
|
110
|
+
if (!result.success || !result.output)
|
|
111
|
+
return undefined;
|
|
112
|
+
for (const line of result.output.split('\n')) {
|
|
113
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId, sessionId] = line.split('\t');
|
|
114
|
+
if (!windowId)
|
|
115
|
+
continue;
|
|
116
|
+
if (windowName !== 'Agents')
|
|
117
|
+
continue;
|
|
118
|
+
if (isOpencode !== '1')
|
|
119
|
+
continue;
|
|
120
|
+
if (windowServerKey !== serverKey)
|
|
121
|
+
continue;
|
|
122
|
+
if (windowInstanceId !== instanceId)
|
|
123
|
+
continue;
|
|
124
|
+
// If tmuxSessionId is provided, only match windows in that session
|
|
125
|
+
if (tmuxSessionId && sessionId !== tmuxSessionId)
|
|
126
|
+
continue;
|
|
127
|
+
return windowId;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function findOwnedAgentsWindowSync(serverKey, instanceId, tmuxSessionId) {
|
|
132
|
+
const result = runTmuxCommandSync([
|
|
133
|
+
'list-windows',
|
|
134
|
+
'-a',
|
|
135
|
+
'-F',
|
|
136
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}\t#{session_id}`,
|
|
137
|
+
]);
|
|
138
|
+
if (!result.success || !result.output)
|
|
139
|
+
return undefined;
|
|
140
|
+
for (const line of result.output.split('\n')) {
|
|
141
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId, sessionId] = line.split('\t');
|
|
142
|
+
if (!windowId)
|
|
143
|
+
continue;
|
|
144
|
+
if (windowName !== 'Agents')
|
|
145
|
+
continue;
|
|
146
|
+
if (isOpencode !== '1')
|
|
147
|
+
continue;
|
|
148
|
+
if (windowServerKey !== serverKey)
|
|
149
|
+
continue;
|
|
150
|
+
if (windowInstanceId !== instanceId)
|
|
151
|
+
continue;
|
|
152
|
+
// If tmuxSessionId is provided, only match windows in that session
|
|
153
|
+
if (tmuxSessionId && sessionId !== tmuxSessionId)
|
|
154
|
+
continue;
|
|
155
|
+
return windowId;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
export async function findOwnedAgentPanes(serverKey) {
|
|
160
|
+
const windowsResult = await runTmuxCommand([
|
|
161
|
+
'list-windows',
|
|
162
|
+
'-a',
|
|
163
|
+
'-F',
|
|
164
|
+
`#{window_id}\t#{${OPENCODE_SERVER_TAG}}`,
|
|
165
|
+
]);
|
|
166
|
+
if (!windowsResult.success || !windowsResult.output)
|
|
167
|
+
return [];
|
|
168
|
+
const serverWindowIds = new Set();
|
|
169
|
+
for (const line of windowsResult.output.split('\n')) {
|
|
170
|
+
const [windowId, windowServerKey] = line.split('\t');
|
|
171
|
+
if (!windowId)
|
|
172
|
+
continue;
|
|
173
|
+
if (windowServerKey !== serverKey)
|
|
174
|
+
continue;
|
|
175
|
+
serverWindowIds.add(windowId);
|
|
176
|
+
}
|
|
177
|
+
if (serverWindowIds.size === 0)
|
|
178
|
+
return [];
|
|
179
|
+
const result = await runTmuxCommand([
|
|
180
|
+
'list-panes',
|
|
181
|
+
'-a',
|
|
182
|
+
'-F',
|
|
183
|
+
`#{pane_id}\t#{pane_pid}\t#{window_id}\t#{${OPENCODE_INSTANCE_TAG}}\t#{${OPENCODE_SESSION_TAG}}`,
|
|
184
|
+
]);
|
|
185
|
+
if (!result.success || !result.output)
|
|
186
|
+
return [];
|
|
187
|
+
const panes = [];
|
|
188
|
+
for (const line of result.output.split('\n')) {
|
|
189
|
+
const [paneId, panePidRaw, windowId, paneInstanceId, paneSessionId] = line.split('\t');
|
|
190
|
+
if (!paneId)
|
|
191
|
+
continue;
|
|
192
|
+
if (!windowId || !serverWindowIds.has(windowId))
|
|
193
|
+
continue;
|
|
194
|
+
const panePid = Number(panePidRaw);
|
|
195
|
+
panes.push({
|
|
196
|
+
paneId,
|
|
197
|
+
panePid: Number.isFinite(panePid) && panePid > 0 ? panePid : undefined,
|
|
198
|
+
sessionId: paneSessionId || undefined,
|
|
199
|
+
instanceId: paneInstanceId || undefined,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return panes;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Kill a process and all its children (the entire process tree).
|
|
206
|
+
*
|
|
207
|
+
* This is necessary because we spawn `bash -c "opencode attach ...; tmux kill-pane"`
|
|
208
|
+
* and #{pane_pid} returns the bash PID, not the opencode attach PID.
|
|
209
|
+
* We need to kill the children (opencode attach) not just the parent (bash).
|
|
210
|
+
*/
|
|
211
|
+
export async function killProcessByPid(pid) {
|
|
212
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
213
|
+
return false;
|
|
214
|
+
// First, kill all child processes
|
|
215
|
+
try {
|
|
216
|
+
const proc = spawn(['pkill', '-TERM', '-P', String(pid)], {
|
|
217
|
+
stdout: 'pipe',
|
|
218
|
+
stderr: 'pipe',
|
|
219
|
+
});
|
|
220
|
+
await proc.exited;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Ignore errors - children may not exist
|
|
224
|
+
}
|
|
225
|
+
// Then kill the parent
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 'SIGTERM');
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const code = error.code;
|
|
231
|
+
if (code === 'ESRCH')
|
|
232
|
+
return true;
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, PROCESS_TERM_WAIT_MS));
|
|
236
|
+
// Check if parent and children are dead
|
|
237
|
+
if (!isProcessAlive(pid))
|
|
238
|
+
return true;
|
|
239
|
+
// Force kill children
|
|
240
|
+
try {
|
|
241
|
+
const proc = spawn(['pkill', '-KILL', '-P', String(pid)], {
|
|
242
|
+
stdout: 'pipe',
|
|
243
|
+
stderr: 'pipe',
|
|
244
|
+
});
|
|
245
|
+
await proc.exited;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Ignore errors
|
|
249
|
+
}
|
|
250
|
+
// Force kill parent
|
|
251
|
+
try {
|
|
252
|
+
process.kill(pid, 'SIGKILL');
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
const code = error.code;
|
|
256
|
+
if (code === 'ESRCH')
|
|
257
|
+
return true;
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return !isProcessAlive(pid);
|
|
261
|
+
}
|
|
12
262
|
/**
|
|
13
263
|
* State for separate-window mode - tracks the dedicated "Agents" window
|
|
264
|
+
* Keyed by `${serverKey}:${instanceId}:${tmuxSessionId}` to support multiple
|
|
265
|
+
* opencode instances running in different tmux sessions.
|
|
266
|
+
*/
|
|
267
|
+
const agentsWindowIdByKey = new Map();
|
|
268
|
+
/**
|
|
269
|
+
* Get the cache key for the agents window
|
|
270
|
+
*/
|
|
271
|
+
function getAgentsWindowCacheKey(ownership) {
|
|
272
|
+
return `${ownership.serverKey}:${ownership.instanceId}:${ownership.tmuxSessionId ?? 'default'}`;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get the cached agents window ID for the given ownership context
|
|
276
|
+
*/
|
|
277
|
+
function getCachedAgentsWindowId(ownership) {
|
|
278
|
+
return agentsWindowIdByKey.get(getAgentsWindowCacheKey(ownership));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Set the cached agents window ID for the given ownership context
|
|
14
282
|
*/
|
|
15
|
-
|
|
283
|
+
function setCachedAgentsWindowId(ownership, windowId) {
|
|
284
|
+
const key = getAgentsWindowCacheKey(ownership);
|
|
285
|
+
if (windowId) {
|
|
286
|
+
agentsWindowIdByKey.set(key, windowId);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
agentsWindowIdByKey.delete(key);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
16
292
|
/**
|
|
17
293
|
* Execute a single pane action
|
|
18
294
|
*
|
|
@@ -21,7 +297,10 @@ let agentsWindowId;
|
|
|
21
297
|
export async function executeAction(action, ctx) {
|
|
22
298
|
switch (action.type) {
|
|
23
299
|
case 'spawn':
|
|
24
|
-
return spawnInAgentsWindow(action, {
|
|
300
|
+
return spawnInAgentsWindow(action, {
|
|
301
|
+
serverUrl: ctx.serverUrl,
|
|
302
|
+
ownership: ctx.ownership,
|
|
303
|
+
});
|
|
25
304
|
case 'close':
|
|
26
305
|
return closePane(action);
|
|
27
306
|
case 'replace':
|
|
@@ -51,17 +330,20 @@ export async function executeActions(actions, ctx) {
|
|
|
51
330
|
* Uses: tmux kill-pane -t <paneId>
|
|
52
331
|
*/
|
|
53
332
|
async function closePane(action) {
|
|
54
|
-
|
|
55
|
-
if (!result.success) {
|
|
56
|
-
return { success: false, error: result.output };
|
|
57
|
-
}
|
|
58
|
-
return { success: true };
|
|
333
|
+
return closePaneById(action.paneId);
|
|
59
334
|
}
|
|
60
335
|
/**
|
|
61
336
|
* Close a pane by its ID
|
|
62
337
|
* Exported for use by TmuxSessionManager when sessions complete
|
|
63
338
|
*/
|
|
64
|
-
export async function closePaneById(paneId) {
|
|
339
|
+
export async function closePaneById(paneId, pid) {
|
|
340
|
+
let resolvedPid = pid;
|
|
341
|
+
if (!resolvedPid) {
|
|
342
|
+
resolvedPid = await resolvePanePidWithRetry(paneId);
|
|
343
|
+
}
|
|
344
|
+
if (resolvedPid) {
|
|
345
|
+
await killProcessByPid(resolvedPid);
|
|
346
|
+
}
|
|
65
347
|
const result = await runTmuxCommand(['kill-pane', '-t', paneId]);
|
|
66
348
|
if (!result.success) {
|
|
67
349
|
return { success: false, error: result.output };
|
|
@@ -73,16 +355,20 @@ export async function closePaneById(paneId) {
|
|
|
73
355
|
* Pane self-destructs when command exits (session complete, server died, etc.)
|
|
74
356
|
*/
|
|
75
357
|
async function replacePane(action, ctx) {
|
|
76
|
-
//
|
|
358
|
+
// Use exec to replace bash with opencode attach directly.
|
|
359
|
+
// This ensures signals go directly to opencode attach (no wrapper process).
|
|
360
|
+
// When opencode attach exits, the pane closes automatically (tmux remain-on-exit off).
|
|
77
361
|
// Use shellEscape to prevent shell injection via session IDs
|
|
78
362
|
const escapedServerUrl = shellEscape(ctx.serverUrl);
|
|
79
363
|
const escapedSessionId = shellEscape(action.newSessionId);
|
|
80
|
-
const command = `opencode attach ${escapedServerUrl} --session ${escapedSessionId}
|
|
364
|
+
const command = `exec opencode attach ${escapedServerUrl} --session ${escapedSessionId}`;
|
|
81
365
|
const result = await runTmuxCommand(['respawn-pane', '-k', '-t', action.paneId, command]);
|
|
82
366
|
if (!result.success) {
|
|
83
367
|
return { success: false, error: result.output };
|
|
84
368
|
}
|
|
85
|
-
|
|
369
|
+
await setPaneTags(action.paneId, ctx.ownership, action.newSessionId);
|
|
370
|
+
const pid = await resolvePanePidWithRetry(action.paneId);
|
|
371
|
+
return { success: true, paneId: action.paneId, pid };
|
|
86
372
|
}
|
|
87
373
|
/**
|
|
88
374
|
* Spawn agent in a dedicated "Agents" window with tiled grid layout
|
|
@@ -104,69 +390,99 @@ async function spawnInAgentsWindow(action, ctx, retryCount = 0) {
|
|
|
104
390
|
error: `Failed to spawn agent pane after ${MAX_SPAWN_RETRIES} attempts`,
|
|
105
391
|
};
|
|
106
392
|
}
|
|
107
|
-
//
|
|
393
|
+
// Use exec to replace bash with opencode attach directly.
|
|
394
|
+
// This ensures signals go directly to opencode attach (no wrapper process).
|
|
395
|
+
// When opencode attach exits, the pane closes automatically (tmux remain-on-exit off).
|
|
108
396
|
// Use shellEscape to prevent shell injection via session IDs
|
|
109
397
|
const escapedServerUrl = shellEscape(ctx.serverUrl);
|
|
110
398
|
const escapedSessionId = shellEscape(action.sessionId);
|
|
111
|
-
const command = `opencode attach ${escapedServerUrl} --session ${escapedSessionId}
|
|
399
|
+
const command = `exec opencode attach ${escapedServerUrl} --session ${escapedSessionId}`;
|
|
112
400
|
const layout = 'tiled'; // Always use tiled layout for grid arrangement
|
|
113
401
|
// Check if we have a cached agents window ID and if it still exists
|
|
114
|
-
|
|
402
|
+
let cachedWindowId = getCachedAgentsWindowId(ctx.ownership);
|
|
403
|
+
if (cachedWindowId) {
|
|
115
404
|
const checkResult = await runTmuxCommand([
|
|
116
|
-
'
|
|
405
|
+
'display',
|
|
406
|
+
'-p',
|
|
117
407
|
'-t',
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'#{pane_id}',
|
|
408
|
+
cachedWindowId,
|
|
409
|
+
`#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
121
410
|
]);
|
|
122
|
-
if (!checkResult.success) {
|
|
123
|
-
|
|
124
|
-
|
|
411
|
+
if (!checkResult.success || !checkResult.output) {
|
|
412
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
413
|
+
cachedWindowId = undefined;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
const [isOpencode, windowServerKey, windowInstanceId] = checkResult.output
|
|
417
|
+
.trim()
|
|
418
|
+
.split('\t');
|
|
419
|
+
if (isOpencode !== '1' ||
|
|
420
|
+
windowServerKey !== ctx.ownership.serverKey ||
|
|
421
|
+
windowInstanceId !== ctx.ownership.instanceId) {
|
|
422
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
423
|
+
cachedWindowId = undefined;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!cachedWindowId) {
|
|
428
|
+
cachedWindowId = await findOwnedAgentsWindow(ctx.ownership.serverKey, ctx.ownership.instanceId, ctx.ownership.tmuxSessionId);
|
|
429
|
+
if (cachedWindowId) {
|
|
430
|
+
setCachedAgentsWindowId(ctx.ownership, cachedWindowId);
|
|
125
431
|
}
|
|
126
432
|
}
|
|
127
433
|
// If no agents window exists, create one
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
'
|
|
137
|
-
|
|
138
|
-
|
|
434
|
+
if (!cachedWindowId) {
|
|
435
|
+
// Build the new-window command args
|
|
436
|
+
// CRITICAL: Use -t <session>: to target the correct tmux session
|
|
437
|
+
// Without this, tmux may create the window in the wrong session when
|
|
438
|
+
// multiple opencode instances run in different tmux sessions
|
|
439
|
+
const newWindowArgs = ['new-window'];
|
|
440
|
+
if (ctx.ownership.tmuxSessionId) {
|
|
441
|
+
// Target the specific tmux session (the colon after session_id is important)
|
|
442
|
+
newWindowArgs.push('-t', `${ctx.ownership.tmuxSessionId}:`);
|
|
443
|
+
}
|
|
444
|
+
newWindowArgs.push('-d', // Don't switch to new window
|
|
445
|
+
'-P', '-F', '#{window_id}:#{pane_id}', '-n', 'Agents', command);
|
|
446
|
+
const createResult = await runTmuxCommand(newWindowArgs);
|
|
139
447
|
if (!createResult.success) {
|
|
140
448
|
return { success: false, error: createResult.output };
|
|
141
449
|
}
|
|
142
450
|
// Parse window_id:pane_id from output
|
|
143
451
|
const output = createResult.output?.trim() || '';
|
|
144
452
|
const [windowId, paneId] = output.split(':');
|
|
145
|
-
|
|
453
|
+
cachedWindowId = windowId;
|
|
454
|
+
if (cachedWindowId) {
|
|
455
|
+
setCachedAgentsWindowId(ctx.ownership, cachedWindowId);
|
|
456
|
+
await setWindowTags(cachedWindowId, ctx.ownership);
|
|
457
|
+
}
|
|
146
458
|
// Apply initial layout (useful when more panes are added later)
|
|
147
|
-
if (
|
|
148
|
-
await runTmuxCommand(['select-layout', '-t',
|
|
459
|
+
if (cachedWindowId && layout) {
|
|
460
|
+
await runTmuxCommand(['select-layout', '-t', cachedWindowId, layout]);
|
|
149
461
|
}
|
|
150
|
-
|
|
462
|
+
if (paneId) {
|
|
463
|
+
await setPaneTags(paneId, ctx.ownership, action.sessionId);
|
|
464
|
+
}
|
|
465
|
+
const pid = paneId ? await resolvePanePidWithRetry(paneId) : undefined;
|
|
466
|
+
return { success: true, paneId, windowId, pid };
|
|
151
467
|
}
|
|
152
468
|
// Agents window exists - split within it
|
|
153
469
|
// First, get the first pane in the agents window to use as split target
|
|
154
470
|
const listResult = await runTmuxCommand([
|
|
155
471
|
'list-panes',
|
|
156
472
|
'-t',
|
|
157
|
-
|
|
473
|
+
cachedWindowId,
|
|
158
474
|
'-F',
|
|
159
475
|
'#{pane_id}',
|
|
160
476
|
]);
|
|
161
477
|
if (!listResult.success || !listResult.output) {
|
|
162
478
|
// Fallback: create new window (with retry counter)
|
|
163
|
-
|
|
479
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
164
480
|
return spawnInAgentsWindow(action, ctx, retryCount + 1);
|
|
165
481
|
}
|
|
166
482
|
const targetPaneId = listResult.output.split('\n')[0]?.trim();
|
|
167
483
|
if (!targetPaneId) {
|
|
168
484
|
// Fallback: create new window (with retry counter)
|
|
169
|
-
|
|
485
|
+
setCachedAgentsWindowId(ctx.ownership, undefined);
|
|
170
486
|
return spawnInAgentsWindow(action, ctx, retryCount + 1);
|
|
171
487
|
}
|
|
172
488
|
// Split within the agents window
|
|
@@ -184,48 +500,351 @@ async function spawnInAgentsWindow(action, ctx, retryCount = 0) {
|
|
|
184
500
|
return { success: false, error: splitResult.output };
|
|
185
501
|
}
|
|
186
502
|
const paneId = splitResult.output?.trim();
|
|
503
|
+
if (cachedWindowId) {
|
|
504
|
+
await setWindowTags(cachedWindowId, ctx.ownership);
|
|
505
|
+
}
|
|
506
|
+
if (paneId) {
|
|
507
|
+
await setPaneTags(paneId, ctx.ownership, action.sessionId);
|
|
508
|
+
}
|
|
187
509
|
// Apply the configured layout to the agents window (e.g., tiled for grid)
|
|
188
|
-
if (
|
|
189
|
-
await runTmuxCommand(['select-layout', '-t',
|
|
510
|
+
if (cachedWindowId && layout) {
|
|
511
|
+
await runTmuxCommand(['select-layout', '-t', cachedWindowId, layout]);
|
|
190
512
|
}
|
|
513
|
+
const pid = paneId ? await resolvePanePidWithRetry(paneId) : undefined;
|
|
191
514
|
return {
|
|
192
515
|
success: true,
|
|
193
516
|
paneId: paneId || undefined,
|
|
194
|
-
windowId:
|
|
517
|
+
windowId: cachedWindowId,
|
|
518
|
+
pid,
|
|
195
519
|
};
|
|
196
520
|
}
|
|
521
|
+
function killProcessByPidSync(pid) {
|
|
522
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
523
|
+
return;
|
|
524
|
+
try {
|
|
525
|
+
spawnSync(['pkill', '-TERM', '-P', String(pid)]);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// Ignore errors - children may not exist
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
process.kill(pid, 'SIGTERM');
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
const code = error.code;
|
|
535
|
+
if (code === 'ESRCH')
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const buffer = new SharedArrayBuffer(4);
|
|
540
|
+
const view = new Int32Array(buffer);
|
|
541
|
+
Atomics.wait(view, 0, 0, PROCESS_TERM_WAIT_MS);
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
// ignore sleep errors
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
process.kill(pid, 0);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
const code = error.code;
|
|
551
|
+
if (code === 'ESRCH')
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
spawnSync(['pkill', '-KILL', '-P', String(pid)]);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Ignore errors
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
process.kill(pid, 'SIGKILL');
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// ignore errors
|
|
565
|
+
}
|
|
566
|
+
}
|
|
197
567
|
/**
|
|
198
568
|
* Reset the agents window state (for cleanup)
|
|
199
569
|
*/
|
|
200
|
-
export function resetAgentsWindow() {
|
|
201
|
-
|
|
570
|
+
export function resetAgentsWindow(ownership) {
|
|
571
|
+
if (ownership) {
|
|
572
|
+
setCachedAgentsWindowId(ownership, undefined);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
agentsWindowIdByKey.clear();
|
|
576
|
+
}
|
|
202
577
|
}
|
|
203
578
|
/**
|
|
204
579
|
* Close the agents window if it exists
|
|
205
580
|
* This kills the entire window, which closes all panes within it
|
|
581
|
+
*
|
|
582
|
+
* SAFETY: Verifies the window is named "Agents" before killing to prevent
|
|
583
|
+
* accidentally killing user windows if the cached ID is stale.
|
|
206
584
|
*/
|
|
207
|
-
export async function closeAgentsWindow() {
|
|
208
|
-
|
|
585
|
+
export async function closeAgentsWindow(ownership) {
|
|
586
|
+
// Build a pseudo-ownership context for cache lookup
|
|
587
|
+
const cacheKey = ownership
|
|
588
|
+
? {
|
|
589
|
+
serverKey: ownership.serverKey,
|
|
590
|
+
instanceId: ownership.instanceId,
|
|
591
|
+
ownerPid: 0,
|
|
592
|
+
tmuxSessionId: ownership.tmuxSessionId,
|
|
593
|
+
}
|
|
594
|
+
: undefined;
|
|
595
|
+
const cachedId = cacheKey ? getCachedAgentsWindowId(cacheKey) : undefined;
|
|
596
|
+
const windowId = cachedId ??
|
|
597
|
+
(ownership
|
|
598
|
+
? await findOwnedAgentsWindow(ownership.serverKey, ownership.instanceId, ownership.tmuxSessionId)
|
|
599
|
+
: undefined);
|
|
600
|
+
if (!windowId)
|
|
601
|
+
return;
|
|
602
|
+
const checkFormat = ownership
|
|
603
|
+
? `#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`
|
|
604
|
+
: '#{window_name}';
|
|
605
|
+
const checkResult = await runTmuxCommand(['display', '-p', '-t', windowId, checkFormat]);
|
|
606
|
+
if (!checkResult.success) {
|
|
607
|
+
if (cacheKey)
|
|
608
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
209
609
|
return;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
610
|
+
}
|
|
611
|
+
const parts = checkResult.output?.trim().split('\t') ?? [];
|
|
612
|
+
const windowName = parts[0];
|
|
613
|
+
if (windowName !== 'Agents') {
|
|
614
|
+
if (cacheKey)
|
|
615
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (ownership) {
|
|
619
|
+
const [, isOpencode, windowServerKey, windowInstanceId] = parts;
|
|
620
|
+
if (isOpencode !== '1' ||
|
|
621
|
+
windowServerKey !== ownership.serverKey ||
|
|
622
|
+
windowInstanceId !== ownership.instanceId) {
|
|
623
|
+
if (cacheKey)
|
|
624
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
await runTmuxCommand(['kill-window', '-t', windowId]);
|
|
629
|
+
if (cacheKey)
|
|
630
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
213
631
|
}
|
|
214
632
|
/**
|
|
215
633
|
* Synchronously close the agents window (for shutdown)
|
|
216
|
-
* Uses
|
|
634
|
+
* Uses runTmuxCommandSync to ensure it completes before process exit
|
|
635
|
+
*
|
|
636
|
+
* SAFETY: Verifies the window is named "Agents" before killing to prevent
|
|
637
|
+
* accidentally killing user windows if the cached ID is stale.
|
|
217
638
|
*/
|
|
218
|
-
export function closeAgentsWindowSync() {
|
|
219
|
-
|
|
639
|
+
export function closeAgentsWindowSync(ownership) {
|
|
640
|
+
// Build a pseudo-ownership context for cache lookup
|
|
641
|
+
const cacheKey = ownership
|
|
642
|
+
? {
|
|
643
|
+
serverKey: ownership.serverKey,
|
|
644
|
+
instanceId: ownership.instanceId,
|
|
645
|
+
ownerPid: 0,
|
|
646
|
+
tmuxSessionId: ownership.tmuxSessionId,
|
|
647
|
+
}
|
|
648
|
+
: undefined;
|
|
649
|
+
const cachedId = cacheKey ? getCachedAgentsWindowId(cacheKey) : undefined;
|
|
650
|
+
const windowId = cachedId ??
|
|
651
|
+
(ownership
|
|
652
|
+
? findOwnedAgentsWindowSync(ownership.serverKey, ownership.instanceId, ownership.tmuxSessionId)
|
|
653
|
+
: undefined);
|
|
654
|
+
if (!windowId)
|
|
655
|
+
return;
|
|
656
|
+
const checkFormat = ownership
|
|
657
|
+
? `#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`
|
|
658
|
+
: '#{window_name}';
|
|
659
|
+
const checkResult = runTmuxCommandSync(['display', '-p', '-t', windowId, checkFormat]);
|
|
660
|
+
if (!checkResult.success) {
|
|
661
|
+
if (cacheKey)
|
|
662
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
220
663
|
return;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
664
|
+
}
|
|
665
|
+
const parts = checkResult.output?.trim().split('\t') ?? [];
|
|
666
|
+
const windowName = parts[0];
|
|
667
|
+
if (windowName !== 'Agents') {
|
|
668
|
+
if (cacheKey)
|
|
669
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (ownership) {
|
|
673
|
+
const [, isOpencode, windowServerKey, windowInstanceId] = parts;
|
|
674
|
+
if (isOpencode !== '1' ||
|
|
675
|
+
windowServerKey !== ownership.serverKey ||
|
|
676
|
+
windowInstanceId !== ownership.instanceId) {
|
|
677
|
+
if (cacheKey)
|
|
678
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
runTmuxCommandSync(['kill-window', '-t', windowId]);
|
|
683
|
+
if (cacheKey)
|
|
684
|
+
setCachedAgentsWindowId(cacheKey, undefined);
|
|
224
685
|
}
|
|
225
686
|
/**
|
|
226
687
|
* Get the current agents window ID (for testing/debugging)
|
|
227
688
|
*/
|
|
228
|
-
export function getAgentsWindowId() {
|
|
229
|
-
|
|
689
|
+
export function getAgentsWindowId(ownership) {
|
|
690
|
+
if (ownership) {
|
|
691
|
+
return getCachedAgentsWindowId(ownership);
|
|
692
|
+
}
|
|
693
|
+
// Return first cached window ID (for backwards compatibility in tests)
|
|
694
|
+
const values = agentsWindowIdByKey.values();
|
|
695
|
+
const first = values.next();
|
|
696
|
+
return first.done ? undefined : first.value;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Clean up owned tmux windows/panes using ownership tags.
|
|
700
|
+
*/
|
|
701
|
+
export async function cleanupOwnedResources(serverKey, instanceId) {
|
|
702
|
+
if (!instanceId)
|
|
703
|
+
return { panesClosed: 0, windowClosed: false };
|
|
704
|
+
const windowsResult = await runTmuxCommand([
|
|
705
|
+
'list-windows',
|
|
706
|
+
'-a',
|
|
707
|
+
'-F',
|
|
708
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
709
|
+
]);
|
|
710
|
+
const windowsToClose = new Set();
|
|
711
|
+
const serverWindowIds = new Set();
|
|
712
|
+
if (windowsResult.success && windowsResult.output) {
|
|
713
|
+
for (const line of windowsResult.output.split('\n')) {
|
|
714
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId] = line.split('\t');
|
|
715
|
+
if (!windowId)
|
|
716
|
+
continue;
|
|
717
|
+
if (windowServerKey === serverKey) {
|
|
718
|
+
serverWindowIds.add(windowId);
|
|
719
|
+
}
|
|
720
|
+
if (windowName !== 'Agents')
|
|
721
|
+
continue;
|
|
722
|
+
if (isOpencode !== '1')
|
|
723
|
+
continue;
|
|
724
|
+
if (windowServerKey !== serverKey)
|
|
725
|
+
continue;
|
|
726
|
+
if (windowInstanceId !== instanceId)
|
|
727
|
+
continue;
|
|
728
|
+
windowsToClose.add(windowId);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const panesResult = await runTmuxCommand([
|
|
732
|
+
'list-panes',
|
|
733
|
+
'-a',
|
|
734
|
+
'-F',
|
|
735
|
+
`#{pane_id}\t#{pane_pid}\t#{window_id}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
736
|
+
]);
|
|
737
|
+
const panesToClose = [];
|
|
738
|
+
if (panesResult.success && panesResult.output) {
|
|
739
|
+
for (const line of panesResult.output.split('\n')) {
|
|
740
|
+
const [paneId, panePidRaw, windowId, paneInstanceId] = line.split('\t');
|
|
741
|
+
if (!paneId)
|
|
742
|
+
continue;
|
|
743
|
+
if (!windowId || !serverWindowIds.has(windowId))
|
|
744
|
+
continue;
|
|
745
|
+
if (paneInstanceId !== instanceId)
|
|
746
|
+
continue;
|
|
747
|
+
const panePid = Number(panePidRaw);
|
|
748
|
+
panesToClose.push({
|
|
749
|
+
paneId,
|
|
750
|
+
panePid: Number.isFinite(panePid) && panePid > 0 ? panePid : undefined,
|
|
751
|
+
windowId,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
for (const pane of panesToClose) {
|
|
756
|
+
if (pane.panePid) {
|
|
757
|
+
await killProcessByPid(pane.panePid);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
let windowClosed = false;
|
|
761
|
+
for (const windowId of windowsToClose) {
|
|
762
|
+
await runTmuxCommand(['kill-window', '-t', windowId]);
|
|
763
|
+
windowClosed = true;
|
|
764
|
+
}
|
|
765
|
+
let panesClosed = 0;
|
|
766
|
+
for (const pane of panesToClose) {
|
|
767
|
+
if (windowsToClose.has(pane.windowId))
|
|
768
|
+
continue;
|
|
769
|
+
await runTmuxCommand(['kill-pane', '-t', pane.paneId]);
|
|
770
|
+
panesClosed += 1;
|
|
771
|
+
}
|
|
772
|
+
return { panesClosed, windowClosed };
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Synchronous cleanup for owned tmux windows/panes.
|
|
776
|
+
*/
|
|
777
|
+
export function cleanupOwnedResourcesSync(serverKey, instanceId) {
|
|
778
|
+
if (!instanceId)
|
|
779
|
+
return { panesClosed: 0, windowClosed: false };
|
|
780
|
+
const windowsResult = runTmuxCommandSync([
|
|
781
|
+
'list-windows',
|
|
782
|
+
'-a',
|
|
783
|
+
'-F',
|
|
784
|
+
`#{window_id}\t#{window_name}\t#{${OPENCODE_TAG}}\t#{${OPENCODE_SERVER_TAG}}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
785
|
+
]);
|
|
786
|
+
const windowsToClose = new Set();
|
|
787
|
+
const serverWindowIds = new Set();
|
|
788
|
+
if (windowsResult.success && windowsResult.output) {
|
|
789
|
+
for (const line of windowsResult.output.split('\n')) {
|
|
790
|
+
const [windowId, windowName, isOpencode, windowServerKey, windowInstanceId] = line.split('\t');
|
|
791
|
+
if (!windowId)
|
|
792
|
+
continue;
|
|
793
|
+
if (windowServerKey === serverKey) {
|
|
794
|
+
serverWindowIds.add(windowId);
|
|
795
|
+
}
|
|
796
|
+
if (windowName !== 'Agents')
|
|
797
|
+
continue;
|
|
798
|
+
if (isOpencode !== '1')
|
|
799
|
+
continue;
|
|
800
|
+
if (windowServerKey !== serverKey)
|
|
801
|
+
continue;
|
|
802
|
+
if (windowInstanceId !== instanceId)
|
|
803
|
+
continue;
|
|
804
|
+
windowsToClose.add(windowId);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const panesResult = runTmuxCommandSync([
|
|
808
|
+
'list-panes',
|
|
809
|
+
'-a',
|
|
810
|
+
'-F',
|
|
811
|
+
`#{pane_id}\t#{pane_pid}\t#{window_id}\t#{${OPENCODE_INSTANCE_TAG}}`,
|
|
812
|
+
]);
|
|
813
|
+
const panesToClose = [];
|
|
814
|
+
if (panesResult.success && panesResult.output) {
|
|
815
|
+
for (const line of panesResult.output.split('\n')) {
|
|
816
|
+
const [paneId, panePidRaw, windowId, paneInstanceId] = line.split('\t');
|
|
817
|
+
if (!paneId)
|
|
818
|
+
continue;
|
|
819
|
+
if (!windowId || !serverWindowIds.has(windowId))
|
|
820
|
+
continue;
|
|
821
|
+
if (paneInstanceId !== instanceId)
|
|
822
|
+
continue;
|
|
823
|
+
const panePid = Number(panePidRaw);
|
|
824
|
+
panesToClose.push({
|
|
825
|
+
paneId,
|
|
826
|
+
panePid: Number.isFinite(panePid) && panePid > 0 ? panePid : undefined,
|
|
827
|
+
windowId,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
for (const pane of panesToClose) {
|
|
832
|
+
if (pane.panePid) {
|
|
833
|
+
killProcessByPidSync(pane.panePid);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
let windowClosed = false;
|
|
837
|
+
for (const windowId of windowsToClose) {
|
|
838
|
+
runTmuxCommandSync(['kill-window', '-t', windowId]);
|
|
839
|
+
windowClosed = true;
|
|
840
|
+
}
|
|
841
|
+
let panesClosed = 0;
|
|
842
|
+
for (const pane of panesToClose) {
|
|
843
|
+
if (windowsToClose.has(pane.windowId))
|
|
844
|
+
continue;
|
|
845
|
+
runTmuxCommandSync(['kill-pane', '-t', pane.paneId]);
|
|
846
|
+
panesClosed += 1;
|
|
847
|
+
}
|
|
848
|
+
return { panesClosed, windowClosed };
|
|
230
849
|
}
|
|
231
850
|
//# sourceMappingURL=executor.js.map
|