opentmux 1.3.3

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.js ADDED
@@ -0,0 +1,841 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // node_modules/tsup/assets/esm_shims.js
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ var init_esm_shims = __esm({
15
+ "node_modules/tsup/assets/esm_shims.js"() {
16
+ "use strict";
17
+ }
18
+ });
19
+
20
+ // src/utils/logger.ts
21
+ import * as fs from "fs";
22
+ import * as os from "os";
23
+ import * as path2 from "path";
24
+ function getLogFile() {
25
+ if (fs.existsSync(NEW_LOG_FILE)) {
26
+ return NEW_LOG_FILE;
27
+ }
28
+ if (fs.existsSync(OLD_LOG_FILE)) {
29
+ console.warn(
30
+ "Deprecation: Using legacy opencode-agent-tmux log file. Please update to opentmux"
31
+ );
32
+ return OLD_LOG_FILE;
33
+ }
34
+ return NEW_LOG_FILE;
35
+ }
36
+ function log(message, data) {
37
+ try {
38
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
39
+ const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
40
+ `;
41
+ fs.appendFileSync(logFile, logEntry);
42
+ } catch {
43
+ }
44
+ }
45
+ var NEW_LOG_FILE, OLD_LOG_FILE, logFile;
46
+ var init_logger = __esm({
47
+ "src/utils/logger.ts"() {
48
+ "use strict";
49
+ init_esm_shims();
50
+ NEW_LOG_FILE = path2.join(os.tmpdir(), "opentmux.log");
51
+ OLD_LOG_FILE = path2.join(os.tmpdir(), "opencode-agent-tmux.log");
52
+ logFile = getLogFile();
53
+ }
54
+ });
55
+
56
+ // src/utils/tmux.ts
57
+ var tmux_exports = {};
58
+ __export(tmux_exports, {
59
+ closeTmuxPane: () => closeTmuxPane,
60
+ getTmuxPath: () => getTmuxPath,
61
+ isInsideTmux: () => isInsideTmux,
62
+ killTmuxSession: () => killTmuxSession,
63
+ killTmuxSessionSync: () => killTmuxSessionSync,
64
+ resetServerCheck: () => resetServerCheck,
65
+ spawnAsync: () => spawnAsync,
66
+ spawnTmuxPane: () => spawnTmuxPane,
67
+ startTmuxCheck: () => startTmuxCheck
68
+ });
69
+ import { spawn, spawnSync, execSync } from "child_process";
70
+ import { existsSync as existsSync2 } from "fs";
71
+ async function spawnAsync(command, options) {
72
+ return new Promise((resolve) => {
73
+ const [cmd, ...args] = command;
74
+ const proc = spawn(cmd, args, {
75
+ stdio: "pipe",
76
+ env: options?.env ?? process.env
77
+ });
78
+ let stdout = "";
79
+ let stderr = "";
80
+ if (!options?.ignoreOutput) {
81
+ proc.stdout?.on("data", (data) => {
82
+ stdout += data.toString();
83
+ });
84
+ proc.stderr?.on("data", (data) => {
85
+ stderr += data.toString();
86
+ });
87
+ }
88
+ proc.on("close", (code) => {
89
+ resolve({
90
+ exitCode: code ?? 1,
91
+ stdout,
92
+ stderr
93
+ });
94
+ });
95
+ proc.on("error", () => {
96
+ resolve({
97
+ exitCode: 1,
98
+ stdout,
99
+ stderr
100
+ });
101
+ });
102
+ });
103
+ }
104
+ async function isServerRunning(serverUrl) {
105
+ if (serverCheckUrl === serverUrl && serverAvailable === true) {
106
+ return true;
107
+ }
108
+ const healthUrl = new URL("/health", serverUrl).toString();
109
+ const timeoutMs = 3e3;
110
+ const maxAttempts = 2;
111
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
112
+ const controller = new AbortController();
113
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
114
+ let response = null;
115
+ try {
116
+ response = await fetch(healthUrl, { signal: controller.signal }).catch(
117
+ () => null
118
+ );
119
+ } finally {
120
+ clearTimeout(timeout);
121
+ }
122
+ const available = response?.ok ?? false;
123
+ if (available) {
124
+ serverCheckUrl = serverUrl;
125
+ serverAvailable = true;
126
+ log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
127
+ return true;
128
+ }
129
+ if (attempt < maxAttempts) {
130
+ await new Promise((r) => setTimeout(r, 250));
131
+ }
132
+ }
133
+ log("[tmux] isServerRunning: checked", { serverUrl, available: false });
134
+ return false;
135
+ }
136
+ function resetServerCheck() {
137
+ serverAvailable = null;
138
+ serverCheckUrl = null;
139
+ }
140
+ async function findTmuxPath() {
141
+ const isWindows = process.platform === "win32";
142
+ const cmd = isWindows ? "where" : "which";
143
+ try {
144
+ const result = await spawnAsync([cmd, "tmux"]);
145
+ if (result.exitCode !== 0) {
146
+ log("[tmux] findTmuxPath: 'which tmux' failed", {
147
+ exitCode: result.exitCode
148
+ });
149
+ return null;
150
+ }
151
+ const path4 = result.stdout.trim().split("\n")[0];
152
+ if (!path4) {
153
+ log("[tmux] findTmuxPath: no path in output");
154
+ return null;
155
+ }
156
+ const verifyResult = await spawnAsync([path4, "-V"]);
157
+ if (verifyResult.exitCode !== 0) {
158
+ log("[tmux] findTmuxPath: tmux -V failed", {
159
+ path: path4,
160
+ verifyExit: verifyResult.exitCode
161
+ });
162
+ return null;
163
+ }
164
+ log("[tmux] findTmuxPath: found tmux", { path: path4 });
165
+ return path4;
166
+ } catch (err) {
167
+ log("[tmux] findTmuxPath: exception", { error: String(err) });
168
+ return null;
169
+ }
170
+ }
171
+ async function getTmuxPath() {
172
+ if (tmuxChecked) {
173
+ return tmuxPath;
174
+ }
175
+ tmuxPath = await findTmuxPath();
176
+ tmuxChecked = true;
177
+ log("[tmux] getTmuxPath: initialized", { tmuxPath });
178
+ return tmuxPath;
179
+ }
180
+ function isInsideTmux() {
181
+ return !!process.env.TMUX;
182
+ }
183
+ async function applyLayout(tmux, layout, mainPaneSize) {
184
+ try {
185
+ if (layout === "dynamic-vertical") {
186
+ return;
187
+ }
188
+ await spawnAsync([tmux, "select-layout", layout]);
189
+ if (layout === "main-horizontal" || layout === "main-vertical") {
190
+ const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
191
+ await spawnAsync([
192
+ tmux,
193
+ "set-window-option",
194
+ sizeOption,
195
+ `${mainPaneSize}%`
196
+ ]);
197
+ await spawnAsync([tmux, "select-layout", layout]);
198
+ }
199
+ log("[tmux] applyLayout: applied", { layout, mainPaneSize });
200
+ } catch (err) {
201
+ log("[tmux] applyLayout: exception", { error: String(err) });
202
+ }
203
+ }
204
+ async function spawnTmuxPane(sessionId, description, config, serverUrl) {
205
+ log("[tmux] spawnTmuxPane called", {
206
+ sessionId,
207
+ description,
208
+ config,
209
+ serverUrl
210
+ });
211
+ if (!config.enabled) {
212
+ log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
213
+ return { success: false };
214
+ }
215
+ if (!isInsideTmux()) {
216
+ log("[tmux] spawnTmuxPane: not inside tmux, skipping");
217
+ return { success: false };
218
+ }
219
+ const serverRunning = await isServerRunning(serverUrl);
220
+ if (!serverRunning) {
221
+ const defaultPort = process.env.OPENCODE_PORT ?? "4096";
222
+ log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
223
+ serverUrl,
224
+ hint: `Start opencode with --port ${defaultPort}`
225
+ });
226
+ return { success: false };
227
+ }
228
+ const tmux = await getTmuxPath();
229
+ if (!tmux) {
230
+ log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
231
+ return { success: false };
232
+ }
233
+ storedConfig = config;
234
+ try {
235
+ const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
236
+ const args = [
237
+ "split-window",
238
+ "-h",
239
+ "-d",
240
+ "-P",
241
+ "-F",
242
+ "#{pane_id}",
243
+ opencodeCmd
244
+ ];
245
+ const env = {
246
+ ...process.env,
247
+ OPENCODE_HIDE_SUBAGENT_HEADER: "1"
248
+ };
249
+ log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd, env });
250
+ const result = await spawnAsync([tmux, ...args], { env });
251
+ const paneId = result.stdout.trim();
252
+ log("[tmux] spawnTmuxPane: split result", {
253
+ exitCode: result.exitCode,
254
+ paneId,
255
+ stderr: result.stderr.trim()
256
+ });
257
+ if (result.exitCode === 0 && paneId) {
258
+ await spawnAsync(
259
+ [tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)],
260
+ { ignoreOutput: true }
261
+ );
262
+ const layout = config.layout ?? "main-vertical";
263
+ const mainPaneSize = config.main_pane_size ?? 60;
264
+ await applyLayout(tmux, layout, mainPaneSize);
265
+ log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
266
+ paneId,
267
+ layout
268
+ });
269
+ return { success: true, paneId };
270
+ }
271
+ return { success: false };
272
+ } catch (err) {
273
+ log("[tmux] spawnTmuxPane: exception", { error: String(err) });
274
+ return { success: false };
275
+ }
276
+ }
277
+ async function closeTmuxPane(paneId) {
278
+ log("[tmux] closeTmuxPane called", { paneId });
279
+ if (!paneId) {
280
+ log("[tmux] closeTmuxPane: no paneId provided");
281
+ return false;
282
+ }
283
+ const tmux = await getTmuxPath();
284
+ if (!tmux) {
285
+ log("[tmux] closeTmuxPane: tmux binary not found");
286
+ return false;
287
+ }
288
+ try {
289
+ const result = await spawnAsync([tmux, "kill-pane", "-t", paneId]);
290
+ log("[tmux] closeTmuxPane: result", {
291
+ exitCode: result.exitCode,
292
+ stderr: result.stderr.trim()
293
+ });
294
+ if (result.exitCode === 0) {
295
+ log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
296
+ if (storedConfig) {
297
+ const layout = storedConfig.layout ?? "main-vertical";
298
+ const mainPaneSize = storedConfig.main_pane_size ?? 60;
299
+ await applyLayout(tmux, layout, mainPaneSize);
300
+ log("[tmux] closeTmuxPane: layout reapplied", { layout });
301
+ }
302
+ return true;
303
+ }
304
+ log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
305
+ paneId
306
+ });
307
+ return false;
308
+ } catch (err) {
309
+ log("[tmux] closeTmuxPane: exception", { error: String(err) });
310
+ return false;
311
+ }
312
+ }
313
+ function killTmuxSessionSync() {
314
+ let tmux = tmuxPath;
315
+ log("[tmux] killTmuxSessionSync starting", { tmuxPath: tmux });
316
+ if (!tmux) {
317
+ try {
318
+ tmux = execSync("which tmux", { encoding: "utf-8" }).trim();
319
+ log("[tmux] killTmuxSessionSync resolved via which", { tmux });
320
+ } catch {
321
+ tmux = "/usr/local/bin/tmux";
322
+ if (!existsSync2(tmux)) {
323
+ tmux = "/opt/homebrew/bin/tmux";
324
+ }
325
+ log("[tmux] killTmuxSessionSync using fallback", { tmux });
326
+ }
327
+ }
328
+ try {
329
+ let sessionName = "";
330
+ try {
331
+ sessionName = execSync(`${tmux} display-message -p '#S'`, { encoding: "utf-8" }).trim();
332
+ log("[tmux] killTmuxSessionSync target session identified", { sessionName });
333
+ } catch {
334
+ log("[tmux] killTmuxSessionSync could not identify session name, using default");
335
+ }
336
+ log("[tmux] killTmuxSessionSync executing kill-session", { tmux, sessionName });
337
+ const args = sessionName ? ["kill-session", "-t", sessionName] : ["kill-session"];
338
+ const result = spawnSync(tmux, args);
339
+ log("[tmux] killTmuxSessionSync result", {
340
+ status: result.status,
341
+ error: result.error?.message,
342
+ stderr: result.stderr?.toString().trim()
343
+ });
344
+ return result.status === 0;
345
+ } catch (err) {
346
+ log("[tmux] killTmuxSessionSync exception", { error: String(err) });
347
+ return false;
348
+ }
349
+ }
350
+ async function killTmuxSession() {
351
+ const tmux = await getTmuxPath();
352
+ if (!tmux) {
353
+ log("[tmux] killTmuxSession: tmux binary not found");
354
+ return false;
355
+ }
356
+ try {
357
+ log("[tmux] killTmuxSession: killing current session");
358
+ const result = spawnSync(tmux, ["kill-session"]);
359
+ log("[tmux] killTmuxSession: result", {
360
+ status: result.status,
361
+ stderr: result.stderr?.toString().trim()
362
+ });
363
+ return result.status === 0;
364
+ } catch (err) {
365
+ log("[tmux] killTmuxSession: exception", { error: String(err) });
366
+ return false;
367
+ }
368
+ }
369
+ function startTmuxCheck() {
370
+ if (!tmuxChecked) {
371
+ getTmuxPath().catch(() => {
372
+ });
373
+ }
374
+ }
375
+ var tmuxPath, tmuxChecked, storedConfig, serverAvailable, serverCheckUrl;
376
+ var init_tmux = __esm({
377
+ "src/utils/tmux.ts"() {
378
+ "use strict";
379
+ init_esm_shims();
380
+ init_logger();
381
+ tmuxPath = null;
382
+ tmuxChecked = false;
383
+ storedConfig = null;
384
+ serverAvailable = null;
385
+ serverCheckUrl = null;
386
+ }
387
+ });
388
+
389
+ // src/index.ts
390
+ init_esm_shims();
391
+ import * as fs2 from "fs";
392
+ import * as path3 from "path";
393
+
394
+ // src/config.ts
395
+ init_esm_shims();
396
+ import { z } from "zod";
397
+ var TmuxLayoutSchema = z.enum([
398
+ "main-horizontal",
399
+ "main-vertical",
400
+ "tiled",
401
+ "even-horizontal",
402
+ "even-vertical",
403
+ "dynamic-vertical"
404
+ ]);
405
+ var TmuxConfigSchema = z.object({
406
+ enabled: z.boolean().default(true),
407
+ layout: TmuxLayoutSchema.default("dynamic-vertical"),
408
+ main_pane_size: z.number().min(20).max(80).default(60),
409
+ max_agents_per_column: z.number().min(0).max(10).default(3)
410
+ });
411
+ var PluginConfigSchema = z.object({
412
+ enabled: z.boolean().default(true),
413
+ port: z.number().default(4096),
414
+ layout: TmuxLayoutSchema.default("dynamic-vertical"),
415
+ main_pane_size: z.number().min(20).max(80).default(60),
416
+ max_agents_per_column: z.number().min(0).max(10).default(3),
417
+ auto_close: z.boolean().default(true)
418
+ });
419
+ var POLL_INTERVAL_MS = 2e3;
420
+ var SESSION_TIMEOUT_MS = 10 * 60 * 1e3;
421
+ var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_MS * 3;
422
+
423
+ // src/tmux-session-manager.ts
424
+ init_esm_shims();
425
+
426
+ // src/utils/index.ts
427
+ init_esm_shims();
428
+ init_logger();
429
+ init_tmux();
430
+
431
+ // src/utils/layout.ts
432
+ init_esm_shims();
433
+ function generateLayoutString(params) {
434
+ const { width, height, mainPaneSizePercent, paneIds, mainPaneId, maxAgentsPerColumn } = params;
435
+ const cleanPaneIds = paneIds.filter((id) => id !== mainPaneId);
436
+ if (width <= 0 || height <= 0) {
437
+ return ``;
438
+ }
439
+ const mainPaneWidth = Math.floor(width * mainPaneSizePercent / 100);
440
+ const subagentAreaWidth = width - mainPaneWidth;
441
+ if (cleanPaneIds.length === 0) {
442
+ const layout = formatNode(width, height, 0, 0, mainPaneId);
443
+ return withChecksum(layout);
444
+ }
445
+ const mainNode = formatNode(mainPaneWidth, height, 0, 0, mainPaneId);
446
+ const numSubagents = cleanPaneIds.length;
447
+ const limit = maxAgentsPerColumn > 0 ? maxAgentsPerColumn : numSubagents;
448
+ const numCols = Math.ceil(numSubagents / limit);
449
+ const colWidth = Math.floor(subagentAreaWidth / numCols);
450
+ const columns = [];
451
+ let currentX = mainPaneWidth;
452
+ let remainingAgents = numSubagents;
453
+ let agentsProcessed = 0;
454
+ for (let c = 0; c < numCols; c++) {
455
+ const isLastCol = c === numCols - 1;
456
+ const thisColWidth = isLastCol ? width - currentX : colWidth;
457
+ const agentsInThisCol = Math.min(remainingAgents, limit);
458
+ remainingAgents -= agentsInThisCol;
459
+ const colPaneIds = cleanPaneIds.slice(agentsProcessed, agentsProcessed + agentsInThisCol);
460
+ agentsProcessed += agentsInThisCol;
461
+ const colNode = buildColumn(thisColWidth, height, currentX, 0, colPaneIds);
462
+ columns.push(colNode);
463
+ currentX += thisColWidth;
464
+ }
465
+ let subagentAreaNode;
466
+ if (columns.length === 1) {
467
+ subagentAreaNode = columns[0];
468
+ } else {
469
+ subagentAreaNode = `${subagentAreaWidth}x${height},${mainPaneWidth},0{${columns.join(",")}}`;
470
+ }
471
+ const rootLayout = `${width}x${height},0,0{${mainNode},${subagentAreaNode}}`;
472
+ return withChecksum(rootLayout);
473
+ }
474
+ function buildColumn(width, height, x, y, paneIds) {
475
+ if (paneIds.length === 0) return "";
476
+ if (paneIds.length === 1) {
477
+ return formatNode(width, height, x, y, paneIds[0]);
478
+ }
479
+ const rowHeight = Math.floor(height / paneIds.length);
480
+ const nodes = [];
481
+ let currentY = y;
482
+ for (let i = 0; i < paneIds.length; i++) {
483
+ const isLast = i === paneIds.length - 1;
484
+ const thisRowHeight = isLast ? height - (currentY - y) : rowHeight;
485
+ nodes.push(formatNode(width, thisRowHeight, x, currentY, paneIds[i]));
486
+ currentY += thisRowHeight;
487
+ }
488
+ return `${width}x${height},${x},${y}[${nodes.join(",")}]`;
489
+ }
490
+ function formatNode(w, h, x, y, id) {
491
+ return `${w}x${h},${x},${y},${id}`;
492
+ }
493
+ function withChecksum(layout) {
494
+ let csum = 0;
495
+ for (let i = 0; i < layout.length; i++) {
496
+ let byte = layout.charCodeAt(i);
497
+ csum = (csum >> 1) + ((csum & 1) << 15);
498
+ csum += byte;
499
+ }
500
+ csum = csum & 65535;
501
+ return `${csum.toString(16).toLowerCase()},${layout}`;
502
+ }
503
+
504
+ // src/tmux-session-manager.ts
505
+ var TmuxSessionManager = class {
506
+ client;
507
+ tmuxConfig;
508
+ serverUrl;
509
+ sessions = /* @__PURE__ */ new Map();
510
+ pollInterval;
511
+ enabled = false;
512
+ shuttingDown = false;
513
+ mainPaneId = null;
514
+ readyPromise = null;
515
+ constructor(ctx, tmuxConfig, serverUrl) {
516
+ this.client = ctx.client;
517
+ this.tmuxConfig = tmuxConfig;
518
+ this.serverUrl = serverUrl;
519
+ this.enabled = tmuxConfig.enabled && isInsideTmux();
520
+ log("[tmux-session-manager] initialized", {
521
+ enabled: this.enabled,
522
+ tmuxConfig: this.tmuxConfig,
523
+ serverUrl: this.serverUrl
524
+ });
525
+ if (this.enabled) {
526
+ this.readyPromise = this.initMainPaneId();
527
+ this.registerShutdownHandlers();
528
+ }
529
+ }
530
+ async initMainPaneId() {
531
+ try {
532
+ const tmux = await getTmuxPath();
533
+ if (!tmux) return;
534
+ const { execSync: execSync2 } = await import("child_process");
535
+ const result = execSync2(`${tmux} display-message -p "#{pane_id}"`, { encoding: "utf-8" }).trim();
536
+ if (result.startsWith("%")) {
537
+ this.mainPaneId = result;
538
+ log("[tmux-session-manager] identified main pane", { mainPaneId: this.mainPaneId });
539
+ }
540
+ } catch (err) {
541
+ log("[tmux-session-manager] failed to identify main pane", { error: String(err) });
542
+ }
543
+ }
544
+ async onSessionCreated(event) {
545
+ if (this.readyPromise) {
546
+ await this.readyPromise;
547
+ }
548
+ if (!this.enabled) return;
549
+ if (event.type !== "session.created") return;
550
+ const info = event.properties?.info;
551
+ if (!info?.id || !info?.parentID) {
552
+ return;
553
+ }
554
+ const sessionId = info.id;
555
+ const parentId = info.parentID;
556
+ const title = info.title ?? "Subagent";
557
+ if (this.sessions.has(sessionId)) {
558
+ log("[tmux-session-manager] session already tracked", { sessionId });
559
+ return;
560
+ }
561
+ log("[tmux-session-manager] child session created, spawning pane", {
562
+ sessionId,
563
+ parentId,
564
+ title
565
+ });
566
+ const paneResult = await spawnTmuxPane(
567
+ sessionId,
568
+ title,
569
+ this.tmuxConfig,
570
+ this.serverUrl
571
+ ).catch((err) => {
572
+ log("[tmux-session-manager] failed to spawn pane", {
573
+ error: String(err)
574
+ });
575
+ return { success: false, paneId: void 0 };
576
+ });
577
+ if (paneResult.success && paneResult.paneId) {
578
+ const now = Date.now();
579
+ this.sessions.set(sessionId, {
580
+ sessionId,
581
+ paneId: paneResult.paneId,
582
+ parentId,
583
+ title,
584
+ createdAt: now,
585
+ lastSeenAt: now
586
+ });
587
+ log("[tmux-session-manager] pane spawned", {
588
+ sessionId,
589
+ paneId: paneResult.paneId
590
+ });
591
+ this.startPolling();
592
+ void this.recalculateLayout();
593
+ }
594
+ }
595
+ async recalculateLayout() {
596
+ if (this.tmuxConfig.layout !== "dynamic-vertical") return;
597
+ if (!this.mainPaneId) return;
598
+ const tmux = await getTmuxPath();
599
+ if (!tmux) return;
600
+ try {
601
+ const { execSync: execSync2 } = await import("child_process");
602
+ const dims = execSync2(`${tmux} display-message -p "#{window_width},#{window_height}"`, { encoding: "utf-8" }).trim().split(",");
603
+ const width = parseInt(dims[0], 10);
604
+ const height = parseInt(dims[1], 10);
605
+ if (isNaN(width) || isNaN(height)) return;
606
+ const paneIds = Array.from(this.sessions.values()).sort((a, b) => a.createdAt - b.createdAt).map((s) => s.paneId).filter((id) => id !== this.mainPaneId);
607
+ const layoutString = generateLayoutString({
608
+ width,
609
+ height,
610
+ mainPaneSizePercent: this.tmuxConfig.main_pane_size ?? 60,
611
+ paneIds,
612
+ mainPaneId: this.mainPaneId,
613
+ maxAgentsPerColumn: this.tmuxConfig.max_agents_per_column ?? 3
614
+ });
615
+ if (layoutString) {
616
+ const { spawnAsync: spawnAsync2 } = await Promise.resolve().then(() => (init_tmux(), tmux_exports));
617
+ await spawnAsync2([tmux, "select-layout", layoutString]);
618
+ log("[tmux-session-manager] applied dynamic layout", { layoutString });
619
+ }
620
+ } catch (err) {
621
+ log("[tmux-session-manager] failed to recalculate layout", { error: String(err) });
622
+ }
623
+ }
624
+ startPolling() {
625
+ if (this.pollInterval) return;
626
+ this.pollInterval = setInterval(
627
+ () => this.pollSessions(),
628
+ POLL_INTERVAL_MS
629
+ );
630
+ log("[tmux-session-manager] polling started");
631
+ }
632
+ stopPolling() {
633
+ if (this.pollInterval) {
634
+ clearInterval(this.pollInterval);
635
+ this.pollInterval = void 0;
636
+ log("[tmux-session-manager] polling stopped");
637
+ }
638
+ }
639
+ async pollSessions() {
640
+ if (this.sessions.size === 0) {
641
+ this.stopPolling();
642
+ return;
643
+ }
644
+ try {
645
+ const statusResult = await this.client.session.status();
646
+ const allStatuses = statusResult.data ?? {};
647
+ const now = Date.now();
648
+ const sessionsToClose = [];
649
+ for (const [sessionId, tracked] of this.sessions.entries()) {
650
+ const status = allStatuses[sessionId];
651
+ const isIdle = status?.type === "idle";
652
+ if (status) {
653
+ tracked.lastSeenAt = now;
654
+ tracked.missingSince = void 0;
655
+ } else if (!tracked.missingSince) {
656
+ tracked.missingSince = now;
657
+ }
658
+ const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
659
+ const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
660
+ if (isIdle || missingTooLong || isTimedOut) {
661
+ sessionsToClose.push(sessionId);
662
+ }
663
+ }
664
+ for (const sessionId of sessionsToClose) {
665
+ await this.closeSession(sessionId);
666
+ }
667
+ } catch (err) {
668
+ log("[tmux-session-manager] poll error", { error: String(err) });
669
+ const serverAlive = await this.isServerAlive();
670
+ if (!serverAlive) {
671
+ await this.handleShutdown("server-unreachable");
672
+ }
673
+ }
674
+ }
675
+ registerShutdownHandlers() {
676
+ const handler = (reason) => {
677
+ this.handleShutdown(reason);
678
+ };
679
+ process.once("SIGINT", () => handler("SIGINT"));
680
+ process.once("SIGTERM", () => handler("SIGTERM"));
681
+ process.once("SIGHUP", () => handler("SIGHUP"));
682
+ process.once("SIGQUIT", () => handler("SIGQUIT"));
683
+ process.once("beforeExit", () => handler("beforeExit"));
684
+ }
685
+ handleShutdown(reason) {
686
+ if (this.shuttingDown) return;
687
+ this.shuttingDown = true;
688
+ log("[tmux-session-manager] shutdown detected", { reason });
689
+ if (reason === "SIGINT") {
690
+ log("[tmux-session-manager] aggressive kill triggered (SIGINT)", { reason });
691
+ const result = killTmuxSessionSync();
692
+ log("[tmux-session-manager] killTmuxSessionSync result", { result });
693
+ process.exit(0);
694
+ }
695
+ void this.cleanup().finally(() => {
696
+ if (reason !== "beforeExit") {
697
+ process.exit(0);
698
+ }
699
+ });
700
+ }
701
+ async isServerAlive() {
702
+ const healthUrl = new URL("/health", this.serverUrl).toString();
703
+ const controller = new AbortController();
704
+ const timeout = setTimeout(() => controller.abort(), 1500);
705
+ try {
706
+ const response = await fetch(healthUrl, { signal: controller.signal }).catch(
707
+ () => null
708
+ );
709
+ return response?.ok ?? false;
710
+ } catch {
711
+ return false;
712
+ } finally {
713
+ clearTimeout(timeout);
714
+ }
715
+ }
716
+ async closeSession(sessionId) {
717
+ const tracked = this.sessions.get(sessionId);
718
+ if (!tracked) return;
719
+ log("[tmux-session-manager] closing session pane", {
720
+ sessionId,
721
+ paneId: tracked.paneId
722
+ });
723
+ await closeTmuxPane(tracked.paneId);
724
+ this.sessions.delete(sessionId);
725
+ void this.recalculateLayout();
726
+ if (this.sessions.size === 0) {
727
+ this.stopPolling();
728
+ }
729
+ }
730
+ createEventHandler() {
731
+ return async (input) => {
732
+ await this.onSessionCreated(input.event);
733
+ };
734
+ }
735
+ async cleanup() {
736
+ this.stopPolling();
737
+ if (this.sessions.size > 0) {
738
+ log("[tmux-session-manager] closing all panes", {
739
+ count: this.sessions.size
740
+ });
741
+ const closePromises = Array.from(this.sessions.values()).map(
742
+ (s) => closeTmuxPane(s.paneId).catch(
743
+ (err) => log("[tmux-session-manager] cleanup error for pane", {
744
+ paneId: s.paneId,
745
+ error: String(err)
746
+ })
747
+ )
748
+ );
749
+ await Promise.all(closePromises);
750
+ this.sessions.clear();
751
+ }
752
+ log("[tmux-session-manager] cleanup complete");
753
+ }
754
+ };
755
+
756
+ // src/index.ts
757
+ function detectServerUrl() {
758
+ if (process.env.OPENCODE_PORT) {
759
+ return `http://localhost:${process.env.OPENCODE_PORT}`;
760
+ }
761
+ return "http://localhost:4096";
762
+ }
763
+ function loadConfig(directory) {
764
+ const home = process.env.HOME ?? "";
765
+ const configPaths = [
766
+ {
767
+ path: path3.join(directory, "opentmux.json"),
768
+ legacy: false
769
+ },
770
+ {
771
+ path: path3.join(directory, "opencode-agent-tmux.json"),
772
+ legacy: true
773
+ },
774
+ {
775
+ path: path3.join(home, ".config", "opencode", "opentmux.json"),
776
+ legacy: false
777
+ },
778
+ {
779
+ path: path3.join(home, ".config", "opencode", "opencode-agent-tmux.json"),
780
+ legacy: true
781
+ }
782
+ ];
783
+ for (const { path: configPath, legacy } of configPaths) {
784
+ try {
785
+ if (fs2.existsSync(configPath)) {
786
+ if (legacy) {
787
+ console.warn(
788
+ "Deprecation: Using legacy opencode-agent-tmux config. Please update to opentmux"
789
+ );
790
+ }
791
+ const content = fs2.readFileSync(configPath, "utf-8");
792
+ const parsed = JSON.parse(content);
793
+ const result = PluginConfigSchema.safeParse(parsed);
794
+ if (result.success) {
795
+ log("[plugin] loaded config", { configPath, config: result.data });
796
+ return result.data;
797
+ }
798
+ log("[plugin] config parse error", {
799
+ configPath,
800
+ error: result.error.message
801
+ });
802
+ }
803
+ } catch (err) {
804
+ log("[plugin] config load error", { configPath, error: String(err) });
805
+ }
806
+ }
807
+ const defaultConfig = PluginConfigSchema.parse({});
808
+ log("[plugin] using default config", { config: defaultConfig });
809
+ return defaultConfig;
810
+ }
811
+ var OpencodeTmux = async (ctx) => {
812
+ const config = loadConfig(ctx.directory);
813
+ const tmuxConfig = {
814
+ enabled: config.enabled,
815
+ layout: config.layout,
816
+ main_pane_size: config.main_pane_size,
817
+ max_agents_per_column: config.max_agents_per_column
818
+ };
819
+ const serverUrl = ctx.serverUrl?.toString() || detectServerUrl();
820
+ log("[plugin] initialized", {
821
+ tmuxConfig,
822
+ directory: ctx.directory,
823
+ serverUrl
824
+ });
825
+ if (tmuxConfig.enabled) {
826
+ startTmuxCheck();
827
+ }
828
+ const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig, serverUrl);
829
+ return {
830
+ name: "opentmux",
831
+ event: async (input) => {
832
+ await tmuxSessionManager.onSessionCreated(
833
+ input.event
834
+ );
835
+ }
836
+ };
837
+ };
838
+ var src_default = OpencodeTmux;
839
+ export {
840
+ src_default as default
841
+ };