opencode-agent-tmux 1.2.7 → 1.3.0
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/index.d.ts +24 -0
- package/dist/index.js +580 -60
- package/dist/scripts/update-plugins.js +17 -7
- package/package.json +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -36,14 +36,26 @@ declare const TmuxConfigSchema: z.ZodObject<{
|
|
|
36
36
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
37
37
|
layout: z.ZodDefault<z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical"]>>;
|
|
38
38
|
main_pane_size: z.ZodDefault<z.ZodNumber>;
|
|
39
|
+
spawn_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
40
|
+
max_retry_attempts: z.ZodDefault<z.ZodNumber>;
|
|
41
|
+
layout_debounce_ms: z.ZodDefault<z.ZodNumber>;
|
|
42
|
+
max_agents_per_column: z.ZodDefault<z.ZodNumber>;
|
|
39
43
|
}, "strip", z.ZodTypeAny, {
|
|
40
44
|
enabled: boolean;
|
|
41
45
|
layout: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical";
|
|
42
46
|
main_pane_size: number;
|
|
47
|
+
spawn_delay_ms: number;
|
|
48
|
+
max_retry_attempts: number;
|
|
49
|
+
layout_debounce_ms: number;
|
|
50
|
+
max_agents_per_column: number;
|
|
43
51
|
}, {
|
|
44
52
|
enabled?: boolean | undefined;
|
|
45
53
|
layout?: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | undefined;
|
|
46
54
|
main_pane_size?: number | undefined;
|
|
55
|
+
spawn_delay_ms?: number | undefined;
|
|
56
|
+
max_retry_attempts?: number | undefined;
|
|
57
|
+
layout_debounce_ms?: number | undefined;
|
|
58
|
+
max_agents_per_column?: number | undefined;
|
|
47
59
|
}>;
|
|
48
60
|
type TmuxConfig = z.infer<typeof TmuxConfigSchema>;
|
|
49
61
|
declare const PluginConfigSchema: z.ZodObject<{
|
|
@@ -52,18 +64,30 @@ declare const PluginConfigSchema: z.ZodObject<{
|
|
|
52
64
|
layout: z.ZodDefault<z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical"]>>;
|
|
53
65
|
main_pane_size: z.ZodDefault<z.ZodNumber>;
|
|
54
66
|
auto_close: z.ZodDefault<z.ZodBoolean>;
|
|
67
|
+
spawn_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
68
|
+
max_retry_attempts: z.ZodDefault<z.ZodNumber>;
|
|
69
|
+
layout_debounce_ms: z.ZodDefault<z.ZodNumber>;
|
|
70
|
+
max_agents_per_column: z.ZodDefault<z.ZodNumber>;
|
|
55
71
|
}, "strip", z.ZodTypeAny, {
|
|
56
72
|
enabled: boolean;
|
|
57
73
|
port: number;
|
|
58
74
|
layout: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical";
|
|
59
75
|
main_pane_size: number;
|
|
60
76
|
auto_close: boolean;
|
|
77
|
+
spawn_delay_ms: number;
|
|
78
|
+
max_retry_attempts: number;
|
|
79
|
+
layout_debounce_ms: number;
|
|
80
|
+
max_agents_per_column: number;
|
|
61
81
|
}, {
|
|
62
82
|
enabled?: boolean | undefined;
|
|
63
83
|
port?: number | undefined;
|
|
64
84
|
layout?: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | undefined;
|
|
65
85
|
main_pane_size?: number | undefined;
|
|
66
86
|
auto_close?: boolean | undefined;
|
|
87
|
+
spawn_delay_ms?: number | undefined;
|
|
88
|
+
max_retry_attempts?: number | undefined;
|
|
89
|
+
layout_debounce_ms?: number | undefined;
|
|
90
|
+
max_agents_per_column?: number | undefined;
|
|
67
91
|
}>;
|
|
68
92
|
type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
69
93
|
|
package/dist/index.js
CHANGED
|
@@ -14,14 +14,22 @@ var TmuxLayoutSchema = z.enum([
|
|
|
14
14
|
var TmuxConfigSchema = z.object({
|
|
15
15
|
enabled: z.boolean().default(true),
|
|
16
16
|
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
17
|
-
main_pane_size: z.number().min(20).max(80).default(60)
|
|
17
|
+
main_pane_size: z.number().min(20).max(80).default(60),
|
|
18
|
+
spawn_delay_ms: z.number().min(50).max(2e3).default(300),
|
|
19
|
+
max_retry_attempts: z.number().min(0).max(5).default(2),
|
|
20
|
+
layout_debounce_ms: z.number().min(50).max(1e3).default(150),
|
|
21
|
+
max_agents_per_column: z.number().min(1).max(10).default(3)
|
|
18
22
|
});
|
|
19
23
|
var PluginConfigSchema = z.object({
|
|
20
24
|
enabled: z.boolean().default(true),
|
|
21
25
|
port: z.number().default(4096),
|
|
22
26
|
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
23
27
|
main_pane_size: z.number().min(20).max(80).default(60),
|
|
24
|
-
auto_close: z.boolean().default(true)
|
|
28
|
+
auto_close: z.boolean().default(true),
|
|
29
|
+
spawn_delay_ms: z.number().min(50).max(2e3).default(300),
|
|
30
|
+
max_retry_attempts: z.number().min(0).max(5).default(2),
|
|
31
|
+
layout_debounce_ms: z.number().min(50).max(1e3).default(150),
|
|
32
|
+
max_agents_per_column: z.number().min(1).max(10).default(3)
|
|
25
33
|
});
|
|
26
34
|
var POLL_INTERVAL_MS = 2e3;
|
|
27
35
|
var SESSION_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -42,8 +50,359 @@ function log(message, data) {
|
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
// src/spawn-queue.ts
|
|
54
|
+
var BASE_BACKOFF_MS = 250;
|
|
55
|
+
var DEFAULT_STALE_THRESHOLD_MS = 3e4;
|
|
56
|
+
var SpawnQueue = class {
|
|
57
|
+
queue = [];
|
|
58
|
+
spawnFn;
|
|
59
|
+
spawnDelayMs;
|
|
60
|
+
maxRetries;
|
|
61
|
+
staleThresholdMs;
|
|
62
|
+
onQueueUpdate;
|
|
63
|
+
onQueueDrained;
|
|
64
|
+
logFn;
|
|
65
|
+
isProcessing = false;
|
|
66
|
+
hasItemInFlight = false;
|
|
67
|
+
isShutdown = false;
|
|
68
|
+
/**
|
|
69
|
+
* Map from sessionId to a pending promise for coalescing duplicate enqueues.
|
|
70
|
+
* Contains both queued and in-flight items.
|
|
71
|
+
*/
|
|
72
|
+
pendingPromises = /* @__PURE__ */ new Map();
|
|
73
|
+
constructor(options) {
|
|
74
|
+
this.spawnFn = options.spawnFn;
|
|
75
|
+
this.spawnDelayMs = options.spawnDelayMs ?? 300;
|
|
76
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
77
|
+
this.staleThresholdMs = options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
|
|
78
|
+
this.onQueueUpdate = options.onQueueUpdate;
|
|
79
|
+
this.onQueueDrained = options.onQueueDrained;
|
|
80
|
+
this.logFn = options.logFn ?? log;
|
|
81
|
+
this.logFn("[spawn-queue] initialized", {
|
|
82
|
+
spawnDelayMs: this.spawnDelayMs,
|
|
83
|
+
maxRetries: this.maxRetries,
|
|
84
|
+
staleThresholdMs: this.staleThresholdMs
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
enqueue(item) {
|
|
88
|
+
if (this.isShutdown) {
|
|
89
|
+
this.logFn("[spawn-queue] enqueue rejected (shutdown)", { sessionId: item.sessionId });
|
|
90
|
+
return Promise.resolve({ success: false });
|
|
91
|
+
}
|
|
92
|
+
const existing = this.pendingPromises.get(item.sessionId);
|
|
93
|
+
if (existing) {
|
|
94
|
+
this.logFn("[spawn-queue] duplicate enqueue coalesced", {
|
|
95
|
+
sessionId: item.sessionId,
|
|
96
|
+
queueDepth: this.queue.length,
|
|
97
|
+
pendingCount: this.getPendingCount()
|
|
98
|
+
});
|
|
99
|
+
return existing.promise;
|
|
100
|
+
}
|
|
101
|
+
let resolveOuter;
|
|
102
|
+
const promise = new Promise((resolve) => {
|
|
103
|
+
resolveOuter = resolve;
|
|
104
|
+
});
|
|
105
|
+
this.pendingPromises.set(item.sessionId, { promise, resolve: resolveOuter });
|
|
106
|
+
this.queue.push({
|
|
107
|
+
sessionId: item.sessionId,
|
|
108
|
+
title: item.title,
|
|
109
|
+
enqueuedAt: Date.now(),
|
|
110
|
+
resolve: resolveOuter
|
|
111
|
+
});
|
|
112
|
+
this.logFn("[spawn-queue] enqueued", {
|
|
113
|
+
sessionId: item.sessionId,
|
|
114
|
+
queueDepth: this.queue.length,
|
|
115
|
+
pendingCount: this.getPendingCount()
|
|
116
|
+
});
|
|
117
|
+
this.notifyQueueUpdate();
|
|
118
|
+
this.processQueue();
|
|
119
|
+
return promise;
|
|
120
|
+
}
|
|
121
|
+
getPendingCount() {
|
|
122
|
+
return this.queue.length + (this.hasItemInFlight ? 1 : 0);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Shutdown the queue: stop processing new items and resolve all pending items as failed.
|
|
126
|
+
*/
|
|
127
|
+
shutdown() {
|
|
128
|
+
if (this.isShutdown) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.isShutdown = true;
|
|
132
|
+
this.logFn("[spawn-queue] shutdown initiated", {
|
|
133
|
+
queuedItems: this.queue.length,
|
|
134
|
+
hasInFlight: this.hasItemInFlight
|
|
135
|
+
});
|
|
136
|
+
while (this.queue.length > 0) {
|
|
137
|
+
const item = this.queue.shift();
|
|
138
|
+
this.logFn("[spawn-queue] shutdown - resolving queued item as failed", {
|
|
139
|
+
sessionId: item.sessionId
|
|
140
|
+
});
|
|
141
|
+
item.resolve({ success: false });
|
|
142
|
+
this.pendingPromises.delete(item.sessionId);
|
|
143
|
+
}
|
|
144
|
+
this.notifyQueueUpdate();
|
|
145
|
+
this.logFn("[spawn-queue] shutdown complete");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Alias for shutdown() for interface consistency.
|
|
149
|
+
*/
|
|
150
|
+
dispose() {
|
|
151
|
+
this.shutdown();
|
|
152
|
+
}
|
|
153
|
+
notifyQueueUpdate() {
|
|
154
|
+
this.onQueueUpdate?.(this.queue.length);
|
|
155
|
+
}
|
|
156
|
+
async processQueue() {
|
|
157
|
+
if (this.isProcessing || this.isShutdown) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.isProcessing = true;
|
|
161
|
+
while (this.queue.length > 0 && !this.isShutdown) {
|
|
162
|
+
const item = this.queue.shift();
|
|
163
|
+
this.hasItemInFlight = true;
|
|
164
|
+
this.notifyQueueUpdate();
|
|
165
|
+
const waitTimeMs = Date.now() - item.enqueuedAt;
|
|
166
|
+
if (waitTimeMs > this.staleThresholdMs) {
|
|
167
|
+
this.logFn("[spawn-queue] stale item skipped", {
|
|
168
|
+
sessionId: item.sessionId,
|
|
169
|
+
waitTimeMs,
|
|
170
|
+
thresholdMs: this.staleThresholdMs
|
|
171
|
+
});
|
|
172
|
+
item.resolve({ success: false });
|
|
173
|
+
this.pendingPromises.delete(item.sessionId);
|
|
174
|
+
this.hasItemInFlight = false;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
this.logFn("[spawn-queue] processing start", {
|
|
178
|
+
sessionId: item.sessionId,
|
|
179
|
+
title: item.title
|
|
180
|
+
});
|
|
181
|
+
const result = await this.processItem(item);
|
|
182
|
+
item.resolve(result);
|
|
183
|
+
this.pendingPromises.delete(item.sessionId);
|
|
184
|
+
this.hasItemInFlight = false;
|
|
185
|
+
if (this.queue.length > 0 && !this.isShutdown) {
|
|
186
|
+
await this.delay(this.spawnDelayMs);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
this.notifyQueueUpdate();
|
|
190
|
+
this.notifyQueueDrained();
|
|
191
|
+
this.isProcessing = false;
|
|
192
|
+
}
|
|
193
|
+
notifyQueueDrained() {
|
|
194
|
+
if (this.queue.length === 0 && !this.hasItemInFlight) {
|
|
195
|
+
this.logFn("[spawn-queue] queue drained");
|
|
196
|
+
this.onQueueDrained?.();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async processItem(item) {
|
|
200
|
+
let retryCount = 0;
|
|
201
|
+
let lastResult = { success: false };
|
|
202
|
+
while (retryCount <= this.maxRetries && !this.isShutdown) {
|
|
203
|
+
const request = {
|
|
204
|
+
sessionId: item.sessionId,
|
|
205
|
+
title: item.title,
|
|
206
|
+
timestamp: item.enqueuedAt,
|
|
207
|
+
retryCount
|
|
208
|
+
};
|
|
209
|
+
this.logFn("[spawn-queue] spawn attempt", {
|
|
210
|
+
sessionId: item.sessionId,
|
|
211
|
+
attempt: retryCount + 1,
|
|
212
|
+
maxAttempts: this.maxRetries + 1
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
lastResult = await this.spawnFn(request);
|
|
216
|
+
} catch {
|
|
217
|
+
lastResult = { success: false };
|
|
218
|
+
}
|
|
219
|
+
if (lastResult.success) {
|
|
220
|
+
this.logFn("[spawn-queue] success", {
|
|
221
|
+
sessionId: item.sessionId,
|
|
222
|
+
paneId: lastResult.paneId,
|
|
223
|
+
attempts: retryCount + 1
|
|
224
|
+
});
|
|
225
|
+
return lastResult;
|
|
226
|
+
}
|
|
227
|
+
retryCount++;
|
|
228
|
+
if (retryCount <= this.maxRetries && !this.isShutdown) {
|
|
229
|
+
const backoffMs = BASE_BACKOFF_MS * Math.pow(2, retryCount - 1);
|
|
230
|
+
this.logFn("[spawn-queue] retry wait", {
|
|
231
|
+
sessionId: item.sessionId,
|
|
232
|
+
backoffMs,
|
|
233
|
+
nextAttempt: retryCount + 1
|
|
234
|
+
});
|
|
235
|
+
await this.delay(backoffMs);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
this.logFn("[spawn-queue] final failure", {
|
|
239
|
+
sessionId: item.sessionId,
|
|
240
|
+
attempts: retryCount
|
|
241
|
+
});
|
|
242
|
+
return lastResult;
|
|
243
|
+
}
|
|
244
|
+
delay(ms) {
|
|
245
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
45
249
|
// src/utils/tmux.ts
|
|
46
250
|
import { spawn } from "child_process";
|
|
251
|
+
|
|
252
|
+
// src/layout.ts
|
|
253
|
+
function computeColumnCount(totalAgents, maxAgentsPerColumn) {
|
|
254
|
+
if (totalAgents <= 0) {
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
if (maxAgentsPerColumn <= 0) {
|
|
258
|
+
throw new Error("maxAgentsPerColumn must be positive");
|
|
259
|
+
}
|
|
260
|
+
return Math.ceil(totalAgents / maxAgentsPerColumn);
|
|
261
|
+
}
|
|
262
|
+
function distributeAgentsRoundRobin(agentCount, maxAgentsPerColumn) {
|
|
263
|
+
const numColumns = computeColumnCount(agentCount, maxAgentsPerColumn);
|
|
264
|
+
if (numColumns === 0) {
|
|
265
|
+
return { numColumns: 0, columnAssignments: [] };
|
|
266
|
+
}
|
|
267
|
+
const columnAssignments = [];
|
|
268
|
+
for (let i = 0; i < agentCount; i++) {
|
|
269
|
+
columnAssignments.push(i % numColumns);
|
|
270
|
+
}
|
|
271
|
+
return { numColumns, columnAssignments };
|
|
272
|
+
}
|
|
273
|
+
function groupAgentsByColumn(agentIds, maxAgentsPerColumn) {
|
|
274
|
+
const { numColumns, columnAssignments } = distributeAgentsRoundRobin(
|
|
275
|
+
agentIds.length,
|
|
276
|
+
maxAgentsPerColumn
|
|
277
|
+
);
|
|
278
|
+
if (numColumns === 0) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
const columns = Array.from({ length: numColumns }, () => []);
|
|
282
|
+
for (let i = 0; i < agentIds.length; i++) {
|
|
283
|
+
const columnIndex = columnAssignments[i];
|
|
284
|
+
columns[columnIndex].push(agentIds[i]);
|
|
285
|
+
}
|
|
286
|
+
return columns;
|
|
287
|
+
}
|
|
288
|
+
function mainPanePercentForColumns(numColumns) {
|
|
289
|
+
if (numColumns <= 1) return 60;
|
|
290
|
+
if (numColumns === 2) return 45;
|
|
291
|
+
return 30;
|
|
292
|
+
}
|
|
293
|
+
function layoutChecksum(layout) {
|
|
294
|
+
let csum = 0;
|
|
295
|
+
for (let i = 0; i < layout.length; i++) {
|
|
296
|
+
csum = (csum >> 1) + ((csum & 1) << 15);
|
|
297
|
+
csum = csum + layout.charCodeAt(i) & 65535;
|
|
298
|
+
}
|
|
299
|
+
return csum;
|
|
300
|
+
}
|
|
301
|
+
function dumpLayoutCell(cell) {
|
|
302
|
+
const base = cell.wpId !== void 0 ? `${cell.sx}x${cell.sy},${cell.xoff},${cell.yoff},${cell.wpId}` : `${cell.sx}x${cell.sy},${cell.xoff},${cell.yoff}`;
|
|
303
|
+
if (cell.type === "WINDOWPANE") {
|
|
304
|
+
return base;
|
|
305
|
+
}
|
|
306
|
+
const children = cell.children ?? [];
|
|
307
|
+
const open = cell.type === "LEFTRIGHT" ? "{" : "[";
|
|
308
|
+
const close = cell.type === "LEFTRIGHT" ? "}" : "]";
|
|
309
|
+
return `${base}${open}${children.map(dumpLayoutCell).join(",")}${close}`;
|
|
310
|
+
}
|
|
311
|
+
function splitSizes(total, count) {
|
|
312
|
+
if (count <= 0) return [];
|
|
313
|
+
const separators = count - 1;
|
|
314
|
+
const available = Math.max(0, total - separators);
|
|
315
|
+
const base = Math.floor(available / count);
|
|
316
|
+
const remainder = available - base * count;
|
|
317
|
+
const out = [];
|
|
318
|
+
for (let i = 0; i < count; i++) {
|
|
319
|
+
out.push(base + (i < remainder ? 1 : 0));
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
function buildMainVerticalMultiColumnLayoutString(params) {
|
|
324
|
+
const { windowWidth, windowHeight, mainPaneWpId, columns, mainPanePercent } = params;
|
|
325
|
+
const numColumns = columns.length;
|
|
326
|
+
if (numColumns <= 0) {
|
|
327
|
+
throw new Error("columns must be non-empty");
|
|
328
|
+
}
|
|
329
|
+
const clampedPercent = Math.max(30, Math.min(80, mainPanePercent));
|
|
330
|
+
const desiredMainWidth = Math.floor(windowWidth * clampedPercent / 100);
|
|
331
|
+
const mainWidth = Math.max(0, Math.min(windowWidth - 2, desiredMainWidth));
|
|
332
|
+
const rightWidth = Math.max(0, windowWidth - mainWidth - 1);
|
|
333
|
+
const mainCell = {
|
|
334
|
+
type: "WINDOWPANE",
|
|
335
|
+
sx: mainWidth,
|
|
336
|
+
sy: windowHeight,
|
|
337
|
+
xoff: 0,
|
|
338
|
+
yoff: 0,
|
|
339
|
+
wpId: mainPaneWpId
|
|
340
|
+
};
|
|
341
|
+
const rightXoff = mainWidth + 1;
|
|
342
|
+
const colWidths = splitSizes(rightWidth, numColumns);
|
|
343
|
+
const columnCells = [];
|
|
344
|
+
let xoff = rightXoff;
|
|
345
|
+
for (let c = 0; c < numColumns; c++) {
|
|
346
|
+
const colPaneIds = columns[c];
|
|
347
|
+
const colWidth = colWidths[c] ?? 0;
|
|
348
|
+
if (colPaneIds.length === 1) {
|
|
349
|
+
columnCells.push({
|
|
350
|
+
type: "WINDOWPANE",
|
|
351
|
+
sx: colWidth,
|
|
352
|
+
sy: windowHeight,
|
|
353
|
+
xoff,
|
|
354
|
+
yoff: 0,
|
|
355
|
+
wpId: colPaneIds[0]
|
|
356
|
+
});
|
|
357
|
+
} else {
|
|
358
|
+
const rowHeights = splitSizes(windowHeight, colPaneIds.length);
|
|
359
|
+
const rows = [];
|
|
360
|
+
let yoff = 0;
|
|
361
|
+
for (let r = 0; r < colPaneIds.length; r++) {
|
|
362
|
+
rows.push({
|
|
363
|
+
type: "WINDOWPANE",
|
|
364
|
+
sx: colWidth,
|
|
365
|
+
sy: rowHeights[r] ?? 0,
|
|
366
|
+
xoff,
|
|
367
|
+
yoff,
|
|
368
|
+
wpId: colPaneIds[r]
|
|
369
|
+
});
|
|
370
|
+
yoff += (rowHeights[r] ?? 0) + 1;
|
|
371
|
+
}
|
|
372
|
+
columnCells.push({
|
|
373
|
+
type: "TOPBOTTOM",
|
|
374
|
+
sx: colWidth,
|
|
375
|
+
sy: windowHeight,
|
|
376
|
+
xoff,
|
|
377
|
+
yoff: 0,
|
|
378
|
+
children: rows
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
xoff += colWidth + 1;
|
|
382
|
+
}
|
|
383
|
+
const rightCell = numColumns === 1 ? columnCells[0] : {
|
|
384
|
+
type: "LEFTRIGHT",
|
|
385
|
+
sx: rightWidth,
|
|
386
|
+
sy: windowHeight,
|
|
387
|
+
xoff: rightXoff,
|
|
388
|
+
yoff: 0,
|
|
389
|
+
children: columnCells
|
|
390
|
+
};
|
|
391
|
+
const root = {
|
|
392
|
+
type: "LEFTRIGHT",
|
|
393
|
+
sx: windowWidth,
|
|
394
|
+
sy: windowHeight,
|
|
395
|
+
xoff: 0,
|
|
396
|
+
yoff: 0,
|
|
397
|
+
children: [mainCell, rightCell]
|
|
398
|
+
};
|
|
399
|
+
const layout = dumpLayoutCell(root);
|
|
400
|
+
const checksum = layoutChecksum(layout);
|
|
401
|
+
return `${checksum.toString(16).padStart(4, "0")},${layout}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/utils/tmux.ts
|
|
405
|
+
var BASE_BACKOFF_MS2 = 250;
|
|
47
406
|
var tmuxPath = null;
|
|
48
407
|
var tmuxChecked = false;
|
|
49
408
|
var storedConfig = null;
|
|
@@ -115,7 +474,7 @@ async function findTmuxPath() {
|
|
|
115
474
|
const isWindows = process.platform === "win32";
|
|
116
475
|
const cmd = isWindows ? "where" : "which";
|
|
117
476
|
try {
|
|
118
|
-
const result = await
|
|
477
|
+
const result = await spawnAsyncFn([cmd, "tmux"]);
|
|
119
478
|
if (result.exitCode !== 0) {
|
|
120
479
|
log("[tmux] findTmuxPath: 'which tmux' failed", {
|
|
121
480
|
exitCode: result.exitCode
|
|
@@ -127,7 +486,7 @@ async function findTmuxPath() {
|
|
|
127
486
|
log("[tmux] findTmuxPath: no path in output");
|
|
128
487
|
return null;
|
|
129
488
|
}
|
|
130
|
-
const verifyResult = await
|
|
489
|
+
const verifyResult = await spawnAsyncFn([path3, "-V"]);
|
|
131
490
|
if (verifyResult.exitCode !== 0) {
|
|
132
491
|
log("[tmux] findTmuxPath: tmux -V failed", {
|
|
133
492
|
path: path3,
|
|
@@ -156,22 +515,173 @@ function isInsideTmux() {
|
|
|
156
515
|
}
|
|
157
516
|
async function applyLayout(tmux, layout, mainPaneSize) {
|
|
158
517
|
try {
|
|
159
|
-
await
|
|
518
|
+
await spawnAsyncFn([tmux, "select-layout", layout]);
|
|
160
519
|
if (layout === "main-horizontal" || layout === "main-vertical") {
|
|
161
520
|
const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
|
|
162
|
-
await
|
|
521
|
+
await spawnAsyncFn([
|
|
163
522
|
tmux,
|
|
164
523
|
"set-window-option",
|
|
165
524
|
sizeOption,
|
|
166
525
|
`${mainPaneSize}%`
|
|
167
526
|
]);
|
|
168
|
-
await
|
|
527
|
+
await spawnAsyncFn([tmux, "select-layout", layout]);
|
|
169
528
|
}
|
|
170
529
|
log("[tmux] applyLayout: applied", { layout, mainPaneSize });
|
|
171
530
|
} catch (err) {
|
|
172
531
|
log("[tmux] applyLayout: exception", { error: String(err) });
|
|
173
532
|
}
|
|
174
533
|
}
|
|
534
|
+
async function getCurrentPaneId(tmux) {
|
|
535
|
+
const result = await spawnAsyncFn([tmux, "display-message", "-p", "#{pane_id}"]);
|
|
536
|
+
const paneId = result.stdout.trim();
|
|
537
|
+
return paneId ? paneId : null;
|
|
538
|
+
}
|
|
539
|
+
async function getWindowSize(tmux) {
|
|
540
|
+
const result = await spawnAsyncFn([
|
|
541
|
+
tmux,
|
|
542
|
+
"display-message",
|
|
543
|
+
"-p",
|
|
544
|
+
"#{window_width} #{window_height}"
|
|
545
|
+
]);
|
|
546
|
+
const parts = result.stdout.trim().split(/\s+/);
|
|
547
|
+
if (parts.length < 2) return null;
|
|
548
|
+
const width = Number(parts[0]);
|
|
549
|
+
const height = Number(parts[1]);
|
|
550
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
551
|
+
return { width, height };
|
|
552
|
+
}
|
|
553
|
+
async function listPaneIds(tmux) {
|
|
554
|
+
const result = await spawnAsyncFn([tmux, "list-panes", "-F", "#{pane_id}"]);
|
|
555
|
+
return result.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
556
|
+
}
|
|
557
|
+
function paneWpId(paneId) {
|
|
558
|
+
if (!paneId.startsWith("%")) return null;
|
|
559
|
+
const n = Number(paneId.slice(1));
|
|
560
|
+
return Number.isFinite(n) ? n : null;
|
|
561
|
+
}
|
|
562
|
+
async function tryApplyMainVerticalMultiColumnLayout(tmux, maxAgentsPerColumn) {
|
|
563
|
+
const size = await getWindowSize(tmux);
|
|
564
|
+
if (!size) return false;
|
|
565
|
+
const currentPaneId = await getCurrentPaneId(tmux);
|
|
566
|
+
if (!currentPaneId) return false;
|
|
567
|
+
const panesBefore = await listPaneIds(tmux);
|
|
568
|
+
if (panesBefore.length < 2) return false;
|
|
569
|
+
if (panesBefore[0] && panesBefore[0] !== currentPaneId) {
|
|
570
|
+
await spawnAsyncFn([tmux, "swap-pane", "-s", currentPaneId, "-t", panesBefore[0]]);
|
|
571
|
+
}
|
|
572
|
+
const panes = await listPaneIds(tmux);
|
|
573
|
+
if (panes.length < 2) return false;
|
|
574
|
+
const mainPaneId = panes[0] ?? currentPaneId;
|
|
575
|
+
const agentPaneIds = panes.slice(1);
|
|
576
|
+
const columns = groupAgentsByColumn(agentPaneIds, maxAgentsPerColumn);
|
|
577
|
+
if (columns.length <= 1) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const mainPanePercent = mainPanePercentForColumns(columns.length);
|
|
581
|
+
const mainWp = paneWpId(mainPaneId);
|
|
582
|
+
if (mainWp === null) return false;
|
|
583
|
+
const wpColumns = [];
|
|
584
|
+
for (const col of columns) {
|
|
585
|
+
const wpIds = [];
|
|
586
|
+
for (const paneId of col) {
|
|
587
|
+
const wpId = paneWpId(paneId);
|
|
588
|
+
if (wpId !== null) {
|
|
589
|
+
wpIds.push(wpId);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (wpIds.length > 0) {
|
|
593
|
+
wpColumns.push(wpIds);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (wpColumns.length <= 1) return false;
|
|
597
|
+
const layoutString = buildMainVerticalMultiColumnLayoutString({
|
|
598
|
+
windowWidth: size.width,
|
|
599
|
+
windowHeight: size.height,
|
|
600
|
+
mainPaneWpId: mainWp,
|
|
601
|
+
columns: wpColumns,
|
|
602
|
+
mainPanePercent
|
|
603
|
+
});
|
|
604
|
+
const result = await spawnAsyncFn([tmux, "select-layout", layoutString]);
|
|
605
|
+
if (result.exitCode === 0) {
|
|
606
|
+
log("[tmux] applyTmuxLayout: applied multi-column layout", {
|
|
607
|
+
columns: wpColumns.length,
|
|
608
|
+
mainPanePercent
|
|
609
|
+
});
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
log("[tmux] applyTmuxLayout: multi-column layout failed", {
|
|
613
|
+
exitCode: result.exitCode,
|
|
614
|
+
stderr: result.stderr.trim()
|
|
615
|
+
});
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
async function applyTmuxLayout() {
|
|
619
|
+
if (!storedConfig) {
|
|
620
|
+
log("[tmux] applyTmuxLayout: no stored config, skipping");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const tmux = await getTmuxPath();
|
|
624
|
+
if (!tmux) {
|
|
625
|
+
log("[tmux] applyTmuxLayout: tmux binary not found");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const layout = storedConfig.layout ?? "main-vertical";
|
|
629
|
+
const maxAgentsPerColumn = storedConfig.max_agents_per_column ?? 3;
|
|
630
|
+
const mainPaneSize = layout === "main-vertical" ? mainPanePercentForColumns(1) : storedConfig.main_pane_size ?? 60;
|
|
631
|
+
try {
|
|
632
|
+
if (layout === "main-vertical") {
|
|
633
|
+
const applied = await tryApplyMainVerticalMultiColumnLayout(
|
|
634
|
+
tmux,
|
|
635
|
+
maxAgentsPerColumn
|
|
636
|
+
);
|
|
637
|
+
if (applied) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
await applyLayout(tmux, layout, mainPaneSize);
|
|
642
|
+
} catch (err) {
|
|
643
|
+
log("[tmux] applyTmuxLayout: failed, falling back to built-in layout", {
|
|
644
|
+
error: String(err)
|
|
645
|
+
});
|
|
646
|
+
try {
|
|
647
|
+
await spawnAsyncFn([tmux, "select-layout", layout === "tiled" ? "tiled" : "main-vertical"]);
|
|
648
|
+
} catch (fallbackErr) {
|
|
649
|
+
log("[tmux] applyTmuxLayout: fallback also failed", { error: String(fallbackErr) });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
var spawnAsyncFn = spawnAsync;
|
|
654
|
+
async function attemptSpawnPane(sessionId, description, config, tmux, serverUrl) {
|
|
655
|
+
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
|
|
656
|
+
const args = [
|
|
657
|
+
"split-window",
|
|
658
|
+
"-h",
|
|
659
|
+
"-d",
|
|
660
|
+
"-P",
|
|
661
|
+
"-F",
|
|
662
|
+
"#{pane_id}",
|
|
663
|
+
opencodeCmd
|
|
664
|
+
];
|
|
665
|
+
log("[tmux] attemptSpawnPane: executing", { tmux, args, opencodeCmd });
|
|
666
|
+
const result = await spawnAsyncFn([tmux, ...args]);
|
|
667
|
+
const paneId = result.stdout.trim();
|
|
668
|
+
log("[tmux] attemptSpawnPane: split result", {
|
|
669
|
+
exitCode: result.exitCode,
|
|
670
|
+
paneId,
|
|
671
|
+
stderr: result.stderr.trim()
|
|
672
|
+
});
|
|
673
|
+
if (result.exitCode === 0 && paneId) {
|
|
674
|
+
await spawnAsyncFn(
|
|
675
|
+
[tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)],
|
|
676
|
+
{ ignoreOutput: true }
|
|
677
|
+
);
|
|
678
|
+
log("[tmux] attemptSpawnPane: SUCCESS, pane created", {
|
|
679
|
+
paneId
|
|
680
|
+
});
|
|
681
|
+
return { success: true, paneId };
|
|
682
|
+
}
|
|
683
|
+
return { success: false };
|
|
684
|
+
}
|
|
175
685
|
async function spawnTmuxPane(sessionId, description, config, serverUrl) {
|
|
176
686
|
log("[tmux] spawnTmuxPane called", {
|
|
177
687
|
sessionId,
|
|
@@ -202,44 +712,35 @@ async function spawnTmuxPane(sessionId, description, config, serverUrl) {
|
|
|
202
712
|
return { success: false };
|
|
203
713
|
}
|
|
204
714
|
storedConfig = config;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const result = await spawnAsync([tmux, ...args]);
|
|
218
|
-
const paneId = result.stdout.trim();
|
|
219
|
-
log("[tmux] spawnTmuxPane: split result", {
|
|
220
|
-
exitCode: result.exitCode,
|
|
221
|
-
paneId,
|
|
222
|
-
stderr: result.stderr.trim()
|
|
223
|
-
});
|
|
224
|
-
if (result.exitCode === 0 && paneId) {
|
|
225
|
-
await spawnAsync(
|
|
226
|
-
[tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)],
|
|
227
|
-
{ ignoreOutput: true }
|
|
228
|
-
);
|
|
229
|
-
const layout = config.layout ?? "main-vertical";
|
|
230
|
-
const mainPaneSize = config.main_pane_size ?? 60;
|
|
231
|
-
await applyLayout(tmux, layout, mainPaneSize);
|
|
232
|
-
log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
|
|
233
|
-
paneId,
|
|
234
|
-
layout
|
|
715
|
+
const maxRetries = config.max_retry_attempts ?? 2;
|
|
716
|
+
let attempt = 0;
|
|
717
|
+
let lastResult = { success: false };
|
|
718
|
+
while (attempt <= maxRetries) {
|
|
719
|
+
try {
|
|
720
|
+
lastResult = await attemptSpawnPane(sessionId, description, config, tmux, serverUrl);
|
|
721
|
+
if (lastResult.success) {
|
|
722
|
+
return lastResult;
|
|
723
|
+
}
|
|
724
|
+
log("[tmux] spawnTmuxPane: attempt failed", {
|
|
725
|
+
attempt: attempt + 1,
|
|
726
|
+
maxRetries
|
|
235
727
|
});
|
|
236
|
-
|
|
728
|
+
} catch (err) {
|
|
729
|
+
log("[tmux] spawnTmuxPane: exception on attempt", {
|
|
730
|
+
attempt: attempt + 1,
|
|
731
|
+
error: String(err)
|
|
732
|
+
});
|
|
733
|
+
lastResult = { success: false };
|
|
734
|
+
}
|
|
735
|
+
attempt++;
|
|
736
|
+
if (attempt <= maxRetries) {
|
|
737
|
+
const backoffMs = BASE_BACKOFF_MS2 * Math.pow(2, attempt - 1);
|
|
738
|
+
log("[tmux] spawnTmuxPane: waiting before retry", { backoffMs, attempt });
|
|
739
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
237
740
|
}
|
|
238
|
-
return { success: false };
|
|
239
|
-
} catch (err) {
|
|
240
|
-
log("[tmux] spawnTmuxPane: exception", { error: String(err) });
|
|
241
|
-
return { success: false };
|
|
242
741
|
}
|
|
742
|
+
log("[tmux] spawnTmuxPane: all retries exhausted", { attempts: attempt });
|
|
743
|
+
return lastResult;
|
|
243
744
|
}
|
|
244
745
|
async function closeTmuxPane(paneId) {
|
|
245
746
|
log("[tmux] closeTmuxPane called", { paneId });
|
|
@@ -260,12 +761,7 @@ async function closeTmuxPane(paneId) {
|
|
|
260
761
|
});
|
|
261
762
|
if (result.exitCode === 0) {
|
|
262
763
|
log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
|
|
263
|
-
|
|
264
|
-
const layout = storedConfig.layout ?? "main-vertical";
|
|
265
|
-
const mainPaneSize = storedConfig.main_pane_size ?? 60;
|
|
266
|
-
await applyLayout(tmux, layout, mainPaneSize);
|
|
267
|
-
log("[tmux] closeTmuxPane: layout reapplied", { layout });
|
|
268
|
-
}
|
|
764
|
+
await applyTmuxLayout();
|
|
269
765
|
return true;
|
|
270
766
|
}
|
|
271
767
|
log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
|
|
@@ -293,11 +789,24 @@ var TmuxSessionManager = class {
|
|
|
293
789
|
pollInterval;
|
|
294
790
|
enabled = false;
|
|
295
791
|
shuttingDown = false;
|
|
792
|
+
spawnQueue;
|
|
793
|
+
layoutDebounceTimer;
|
|
296
794
|
constructor(ctx, tmuxConfig, serverUrl) {
|
|
297
795
|
this.client = ctx.client;
|
|
298
796
|
this.tmuxConfig = tmuxConfig;
|
|
299
797
|
this.serverUrl = serverUrl;
|
|
300
798
|
this.enabled = tmuxConfig.enabled && isInsideTmux();
|
|
799
|
+
this.spawnQueue = new SpawnQueue({
|
|
800
|
+
spawnFn: (request) => spawnTmuxPane(request.sessionId, request.title, this.tmuxConfig, this.serverUrl),
|
|
801
|
+
spawnDelayMs: tmuxConfig.spawn_delay_ms,
|
|
802
|
+
maxRetries: 0,
|
|
803
|
+
onQueueUpdate: (pendingCount) => {
|
|
804
|
+
log("[tmux-session-manager] queue update", { pendingCount });
|
|
805
|
+
},
|
|
806
|
+
onQueueDrained: () => {
|
|
807
|
+
this.scheduleDebouncedLayout();
|
|
808
|
+
}
|
|
809
|
+
});
|
|
301
810
|
log("[tmux-session-manager] initialized", {
|
|
302
811
|
enabled: this.enabled,
|
|
303
812
|
tmuxConfig: this.tmuxConfig,
|
|
@@ -326,17 +835,7 @@ var TmuxSessionManager = class {
|
|
|
326
835
|
parentId,
|
|
327
836
|
title
|
|
328
837
|
});
|
|
329
|
-
const paneResult = await
|
|
330
|
-
sessionId,
|
|
331
|
-
title,
|
|
332
|
-
this.tmuxConfig,
|
|
333
|
-
this.serverUrl
|
|
334
|
-
).catch((err) => {
|
|
335
|
-
log("[tmux-session-manager] failed to spawn pane", {
|
|
336
|
-
error: String(err)
|
|
337
|
-
});
|
|
338
|
-
return { success: false, paneId: void 0 };
|
|
339
|
-
});
|
|
838
|
+
const paneResult = await this.spawnQueue.enqueue({ sessionId, title });
|
|
340
839
|
if (paneResult.success && paneResult.paneId) {
|
|
341
840
|
const now = Date.now();
|
|
342
841
|
this.sessions.set(sessionId, {
|
|
@@ -352,6 +851,8 @@ var TmuxSessionManager = class {
|
|
|
352
851
|
paneId: paneResult.paneId
|
|
353
852
|
});
|
|
354
853
|
this.startPolling();
|
|
854
|
+
} else {
|
|
855
|
+
log("[tmux-session-manager] failed to spawn pane", { sessionId });
|
|
355
856
|
}
|
|
356
857
|
}
|
|
357
858
|
startPolling() {
|
|
@@ -369,6 +870,16 @@ var TmuxSessionManager = class {
|
|
|
369
870
|
log("[tmux-session-manager] polling stopped");
|
|
370
871
|
}
|
|
371
872
|
}
|
|
873
|
+
scheduleDebouncedLayout() {
|
|
874
|
+
if (this.layoutDebounceTimer) {
|
|
875
|
+
clearTimeout(this.layoutDebounceTimer);
|
|
876
|
+
}
|
|
877
|
+
const debounceMs = this.tmuxConfig.layout_debounce_ms ?? 150;
|
|
878
|
+
this.layoutDebounceTimer = setTimeout(() => {
|
|
879
|
+
log("[tmux-session-manager] applying deferred layout after queue drain");
|
|
880
|
+
void applyTmuxLayout();
|
|
881
|
+
}, debounceMs);
|
|
882
|
+
}
|
|
372
883
|
async pollSessions() {
|
|
373
884
|
if (this.sessions.size === 0) {
|
|
374
885
|
this.stopPolling();
|
|
@@ -456,6 +967,11 @@ var TmuxSessionManager = class {
|
|
|
456
967
|
}
|
|
457
968
|
async cleanup() {
|
|
458
969
|
this.stopPolling();
|
|
970
|
+
this.spawnQueue.shutdown();
|
|
971
|
+
if (this.layoutDebounceTimer) {
|
|
972
|
+
clearTimeout(this.layoutDebounceTimer);
|
|
973
|
+
this.layoutDebounceTimer = void 0;
|
|
974
|
+
}
|
|
459
975
|
if (this.sessions.size > 0) {
|
|
460
976
|
log("[tmux-session-manager] closing all panes", {
|
|
461
977
|
count: this.sessions.size
|
|
@@ -520,7 +1036,11 @@ var OpencodeAgentTmux = async (ctx) => {
|
|
|
520
1036
|
const tmuxConfig = {
|
|
521
1037
|
enabled: config.enabled,
|
|
522
1038
|
layout: config.layout,
|
|
523
|
-
main_pane_size: config.main_pane_size
|
|
1039
|
+
main_pane_size: config.main_pane_size,
|
|
1040
|
+
spawn_delay_ms: config.spawn_delay_ms,
|
|
1041
|
+
max_retry_attempts: config.max_retry_attempts,
|
|
1042
|
+
layout_debounce_ms: config.layout_debounce_ms,
|
|
1043
|
+
max_agents_per_column: config.max_agents_per_column
|
|
524
1044
|
};
|
|
525
1045
|
const serverUrl = ctx.serverUrl?.toString() || detectServerUrl();
|
|
526
1046
|
log("[plugin] initialized", {
|
|
@@ -72,18 +72,28 @@ function saveConfig(config) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
function ensurePluginEntry(config) {
|
|
75
|
-
const
|
|
75
|
+
const existingPlugin = Array.isArray(config.plugin) ? [...config.plugin] : [];
|
|
76
|
+
const existingPlugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
|
|
77
|
+
const existing = [...existingPlugin, ...existingPlugins];
|
|
76
78
|
const normalized = existing.map(
|
|
77
|
-
(
|
|
79
|
+
(entry) => entry === "opencode-subagent-tmux" ? "opencode-agent-tmux" : entry
|
|
78
80
|
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const deduped = [];
|
|
82
|
+
for (const entry of normalized) {
|
|
83
|
+
if (!deduped.includes(entry)) {
|
|
84
|
+
deduped.push(entry);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!deduped.includes("opencode-agent-tmux")) {
|
|
88
|
+
deduped.push("opencode-agent-tmux");
|
|
81
89
|
}
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
const changed = JSON.stringify(existingPlugin) !== JSON.stringify(deduped) || "plugins" in config;
|
|
91
|
+
if (changed) {
|
|
92
|
+
config.plugin = deduped;
|
|
93
|
+
delete config.plugins;
|
|
84
94
|
saveConfig(config);
|
|
85
95
|
}
|
|
86
|
-
return
|
|
96
|
+
return deduped;
|
|
87
97
|
}
|
|
88
98
|
function installLatest(plugins) {
|
|
89
99
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agent-tmux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OpenCode plugin that provides tmux integration for viewing agent execution in real-time",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"build": "tsup",
|
|
16
16
|
"dev": "tsup --watch",
|
|
17
17
|
"typecheck": "tsc --noEmit",
|
|
18
|
+
"test": "bun test",
|
|
18
19
|
"prepublishOnly": "bun run build",
|
|
19
20
|
"postinstall": "test -f dist/scripts/install.js && node dist/scripts/install.js || echo 'Skipping postinstall setup (dist not found)'"
|
|
20
21
|
},
|
|
@@ -35,11 +36,13 @@
|
|
|
35
36
|
},
|
|
36
37
|
"homepage": "https://github.com/AnganSamadder/opencode-agent-tmux#readme",
|
|
37
38
|
"dependencies": {
|
|
39
|
+
"proper-lockfile": "^4.1.2",
|
|
38
40
|
"zod": "^3.24.1"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@types/bun": "^1.2.4",
|
|
42
44
|
"@types/node": "^25.0.10",
|
|
45
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
43
46
|
"tsup": "^8.3.6",
|
|
44
47
|
"typescript": "^5.7.3"
|
|
45
48
|
}
|