tmex-cli 0.5.1 → 0.6.1

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.
Files changed (19) hide show
  1. package/dist/runtime/server.js +966 -561
  2. package/package.json +1 -1
  3. package/resources/fe-dist/assets/DevicePage-BGPoxSI1.js +36 -0
  4. package/resources/fe-dist/assets/DevicePage-BGPoxSI1.js.map +1 -0
  5. package/resources/fe-dist/assets/{DevicesPage-_g8yXgKX.js → DevicesPage-ImyBiwEt.js} +2 -2
  6. package/resources/fe-dist/assets/{DevicesPage-_g8yXgKX.js.map → DevicesPage-ImyBiwEt.js.map} +1 -1
  7. package/resources/fe-dist/assets/{SettingsPage-Bi8Kez5a.js → SettingsPage-CyMAR4ec.js} +2 -2
  8. package/resources/fe-dist/assets/{SettingsPage-Bi8Kez5a.js.map → SettingsPage-CyMAR4ec.js.map} +1 -1
  9. package/resources/fe-dist/assets/{index-kXpemMRY.js → index-qxM5NVWd.js} +3 -3
  10. package/resources/fe-dist/assets/{index-kXpemMRY.js.map → index-qxM5NVWd.js.map} +1 -1
  11. package/resources/fe-dist/assets/{select-v4N0NzCz.js → select-CALI7PWC.js} +2 -2
  12. package/resources/fe-dist/assets/{select-v4N0NzCz.js.map → select-CALI7PWC.js.map} +1 -1
  13. package/resources/fe-dist/assets/{switch-BJu9baQM.js → switch-CkhOAzoZ.js} +2 -2
  14. package/resources/fe-dist/assets/{switch-BJu9baQM.js.map → switch-CkhOAzoZ.js.map} +1 -1
  15. package/resources/fe-dist/assets/{useValueChanged-D47LrxaP.js → useValueChanged-WtzSkCNQ.js} +2 -2
  16. package/resources/fe-dist/assets/{useValueChanged-D47LrxaP.js.map → useValueChanged-WtzSkCNQ.js.map} +1 -1
  17. package/resources/fe-dist/index.html +1 -1
  18. package/resources/fe-dist/assets/DevicePage-CQuQ0yeD.js +0 -36
  19. package/resources/fe-dist/assets/DevicePage-CQuQ0yeD.js.map +0 -1
@@ -20282,7 +20282,7 @@ var require_lib3 = __commonJS((exports, module) => {
20282
20282
 
20283
20283
  // src/runtime/server.ts
20284
20284
  import { existsSync as existsSync4 } from "fs";
20285
- import { extname, join as join5, normalize, resolve as resolve2, sep } from "path";
20285
+ import { extname, join as join4, normalize, resolve as resolve2, sep } from "path";
20286
20286
 
20287
20287
  // ../../apps/gateway/src/crypto/errors.ts
20288
20288
  function contextLabel(context) {
@@ -52139,7 +52139,6 @@ class EventNotifier {
52139
52139
  var eventNotifier = new EventNotifier;
52140
52140
 
52141
52141
  // ../../apps/gateway/src/tmux-client/local-external-connection.ts
52142
- import { mkdirSync, rmSync } from "fs";
52143
52142
  import { homedir } from "os";
52144
52143
 
52145
52144
  // ../../apps/gateway/src/ws/error-classify.ts
@@ -52531,156 +52530,297 @@ function buildLocalTmuxEnv(resolvedPath, baseEnv = process.env) {
52531
52530
  return nextEnv;
52532
52531
  }
52533
52532
 
52534
- // ../../apps/gateway/src/tmux-client/command-builder.ts
52535
- function quoteShellArg(value) {
52536
- return `'${value.replaceAll("'", "'\\''")}'`;
52533
+ // ../../apps/gateway/src/tmux-client/capture-history.ts
52534
+ var PANE_SCREEN_INFO_FORMAT = "#{alternate_on} #{cursor_x} #{cursor_y} #{pane_height}";
52535
+ function parsePaneScreenInfo(stdout) {
52536
+ const parts = stdout.trim().split(/\s+/);
52537
+ const toInt = (value) => {
52538
+ if (value === undefined) {
52539
+ return null;
52540
+ }
52541
+ const parsed = Number.parseInt(value, 10);
52542
+ return Number.isNaN(parsed) || parsed < 0 ? null : parsed;
52543
+ };
52544
+ return {
52545
+ alternateScreen: parts[0] === "1",
52546
+ cursorX: toInt(parts[1]),
52547
+ cursorY: toInt(parts[2]),
52548
+ paneHeight: toInt(parts[3])
52549
+ };
52537
52550
  }
52538
- function joinShellArgs(argv) {
52539
- return argv.map((arg) => quoteShellArg(arg)).join(" ");
52551
+ function appendCursorRestore(history, info) {
52552
+ const { cursorX, cursorY, paneHeight } = info;
52553
+ if (cursorX === null || cursorY === null || paneHeight === null || paneHeight < 1) {
52554
+ return history;
52555
+ }
52556
+ const trimmed = history.endsWith(`
52557
+ `) ? history.slice(0, -1) : history;
52558
+ if (info.alternateScreen) {
52559
+ return `${trimmed}\x1B[${cursorY + 1};${cursorX + 1}H`;
52560
+ }
52561
+ const up = Math.min(Math.max(0, paneHeight - 1 - cursorY), paneHeight - 1);
52562
+ const moveUp = up > 0 ? `\x1B[${up}A` : "";
52563
+ return `${trimmed}${moveUp}\x1B[${cursorX + 1}G`;
52540
52564
  }
52541
52565
 
52542
- // ../../apps/gateway/src/tmux-client/fs-paths.ts
52543
- import { randomUUID as randomUUID2 } from "crypto";
52544
- import { join as join3 } from "path";
52545
- var DEFAULT_ROOT_DIR = "/tmp/tmex";
52546
- var DEFAULT_GATEWAY_RUNTIME_ID = randomUUID2();
52547
- function toSafePathSegment(value) {
52548
- return Array.from(value).map((char) => {
52549
- if (/^[A-Za-z0-9._-]$/.test(char)) {
52550
- return char;
52551
- }
52552
- return `_${char.codePointAt(0)?.toString(16) ?? "00"}_`;
52553
- }).join("");
52554
- }
52555
- function createRuntimeFsPaths(options) {
52556
- const baseRootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
52557
- const safeSessionName = toSafePathSegment(options.sessionName?.trim() || "tmex");
52558
- const safeGatewayRuntimeId = toSafePathSegment(options.gatewayRuntimeId?.trim() || DEFAULT_GATEWAY_RUNTIME_ID);
52559
- const runtimeDirName = `${toSafePathSegment(options.deviceId)}-${safeGatewayRuntimeId}-${options.gatewayPid}`;
52560
- const runtimeRootDir = join3(baseRootDir, runtimeDirName);
52561
- const panesDir = join3(runtimeRootDir, "panes");
52562
- const hooksDir = join3(runtimeRootDir, "hooks");
52563
- return {
52564
- rootDir: runtimeRootDir,
52565
- panesDir,
52566
- hooksDir,
52567
- hookFifoPath: join3(hooksDir, "events.fifo"),
52568
- paneFifoPath(paneId) {
52569
- return join3(panesDir, `${safeSessionName}-${toSafePathSegment(paneId)}.fifo`);
52566
+ // ../../apps/gateway/src/tmux-client/control-mode-parser.ts
52567
+ var decoder = new TextDecoder;
52568
+ var MAX_LINE_BYTES = 4 * 1024 * 1024;
52569
+ var MAX_BLOCK_BODY_LINES = 1000;
52570
+ var BYTE_LF = 10;
52571
+ var BYTE_SPACE = 32;
52572
+ var BYTE_PERCENT = 37;
52573
+ var BYTE_BACKSLASH = 92;
52574
+ var KNOWN_NOTIFICATION_TYPES = new Set([
52575
+ "client-detached",
52576
+ "client-session-changed",
52577
+ "config-error",
52578
+ "continue",
52579
+ "layout-change",
52580
+ "message",
52581
+ "pane-mode-changed",
52582
+ "paste-buffer-changed",
52583
+ "paste-buffer-deleted",
52584
+ "pause",
52585
+ "session-changed",
52586
+ "session-renamed",
52587
+ "session-window-changed",
52588
+ "sessions-changed",
52589
+ "subscription-changed",
52590
+ "unlinked-window-add",
52591
+ "unlinked-window-close",
52592
+ "unlinked-window-renamed",
52593
+ "window-add",
52594
+ "window-close",
52595
+ "window-pane-changed",
52596
+ "window-renamed"
52597
+ ]);
52598
+ function isOctalDigit(byte) {
52599
+ return byte >= 48 && byte <= 55;
52600
+ }
52601
+ function unescapeControlModeData(line, start, onInvalidEscape) {
52602
+ const result = new Uint8Array(line.length - start);
52603
+ let written = 0;
52604
+ let index = start;
52605
+ while (index < line.length) {
52606
+ const byte = line[index];
52607
+ if (byte !== BYTE_BACKSLASH) {
52608
+ result[written] = byte;
52609
+ written += 1;
52610
+ index += 1;
52611
+ continue;
52570
52612
  }
52571
- };
52613
+ const d1 = line[index + 1];
52614
+ const d2 = line[index + 2];
52615
+ const d3 = line[index + 3];
52616
+ if (d1 !== undefined && d2 !== undefined && d3 !== undefined && isOctalDigit(d1) && isOctalDigit(d2) && isOctalDigit(d3)) {
52617
+ result[written] = d1 - 48 << 6 | d2 - 48 << 3 | d3 - 48;
52618
+ written += 1;
52619
+ index += 4;
52620
+ continue;
52621
+ }
52622
+ onInvalidEscape?.();
52623
+ result[written] = byte;
52624
+ written += 1;
52625
+ index += 1;
52626
+ }
52627
+ return result.subarray(0, written);
52572
52628
  }
52573
-
52574
- // ../../apps/gateway/src/tmux-client/ghostty-terminfo.ts
52575
- var XTERM_GHOSTTY_TERMINFO_SOURCE = `xterm-ghostty|ghostty|Ghostty,
52576
- am, bce, ccc, hs, km, mc5i, mir, msgr, npc, xenl, AX, Su, Tc, XT, fullkbd,
52577
- colors#0x100, cols#80, it#8, lines#24, pairs#0x7fff,
52578
- acsc=++\\,\\,--..00\`\`aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
52579
- bel=^G, blink=\\E[5m, bold=\\E[1m, cbt=\\E[Z, civis=\\E[?25l,
52580
- clear=\\E[H\\E[2J, cnorm=\\E[?12l\\E[?25h, cr=\\r,
52581
- csr=\\E[%i%p1%d;%p2%dr, cub=\\E[%p1%dD, cub1=^H,
52582
- cud=\\E[%p1%dB, cud1=\\n, cuf=\\E[%p1%dC, cuf1=\\E[C,
52583
- cup=\\E[%i%p1%d;%p2%dH, cuu=\\E[%p1%dA, cuu1=\\E[A,
52584
- cvvis=\\E[?12;25h, dch=\\E[%p1%dP, dch1=\\E[P, dim=\\E[2m,
52585
- dl=\\E[%p1%dM, dl1=\\E[M, dsl=\\E]2;\\007, ech=\\E[%p1%dX,
52586
- ed=\\E[J, el=\\E[K, el1=\\E[1K, flash=\\E[?5h$<100/>\\E[?5l,
52587
- fsl=^G, home=\\E[H, hpa=\\E[%i%p1%dG, ht=^I, hts=\\EH,
52588
- ich=\\E[%p1%d@, ich1=\\E[@, il=\\E[%p1%dL, il1=\\E[L, ind=\\n,
52589
- indn=\\E[%p1%dS,
52590
- initc=\\E]4;%p1%d;rgb:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\,
52591
- invis=\\E[8m, kDC=\\E[3;2~, kEND=\\E[1;2F, kHOM=\\E[1;2H,
52592
- kIC=\\E[2;2~, kLFT=\\E[1;2D, kNXT=\\E[6;2~, kPRV=\\E[5;2~,
52593
- kRIT=\\E[1;2C, kbs=^?, kcbt=\\E[Z, kcub1=\\EOD, kcud1=\\EOB,
52594
- kcuf1=\\EOC, kcuu1=\\EOA, kdch1=\\E[3~, kend=\\EOF, kent=\\EOM,
52595
- kf1=\\EOP, kf10=\\E[21~, kf11=\\E[23~, kf12=\\E[24~,
52596
- kf13=\\E[1;2P, kf14=\\E[1;2Q, kf15=\\E[1;2R, kf16=\\E[1;2S,
52597
- kf17=\\E[15;2~, kf18=\\E[17;2~, kf19=\\E[18;2~, kf2=\\EOQ,
52598
- kf20=\\E[19;2~, kf21=\\E[20;2~, kf22=\\E[21;2~,
52599
- kf23=\\E[23;2~, kf24=\\E[24;2~, kf25=\\E[1;5P, kf26=\\E[1;5Q,
52600
- kf27=\\E[1;5R, kf28=\\E[1;5S, kf29=\\E[15;5~, kf3=\\EOR,
52601
- kf30=\\E[17;5~, kf31=\\E[18;5~, kf32=\\E[19;5~,
52602
- kf33=\\E[20;5~, kf34=\\E[21;5~, kf35=\\E[23;5~,
52603
- kf36=\\E[24;5~, kf37=\\E[1;6P, kf38=\\E[1;6Q, kf39=\\E[1;6R,
52604
- kf4=\\EOS, kf40=\\E[1;6S, kf41=\\E[15;6~, kf42=\\E[17;6~,
52605
- kf43=\\E[18;6~, kf44=\\E[19;6~, kf45=\\E[20;6~,
52606
- kf46=\\E[21;6~, kf47=\\E[23;6~, kf48=\\E[24;6~,
52607
- kf49=\\E[1;3P, kf5=\\E[15~, kf50=\\E[1;3Q, kf51=\\E[1;3R,
52608
- kf52=\\E[1;3S, kf53=\\E[15;3~, kf54=\\E[17;3~,
52609
- kf55=\\E[18;3~, kf56=\\E[19;3~, kf57=\\E[20;3~,
52610
- kf58=\\E[21;3~, kf59=\\E[23;3~, kf6=\\E[17~, kf60=\\E[24;3~,
52611
- kf61=\\E[1;4P, kf62=\\E[1;4Q, kf63=\\E[1;4R, kf7=\\E[18~,
52612
- kf8=\\E[19~, kf9=\\E[20~, khome=\\EOH, kich1=\\E[2~,
52613
- kind=\\E[1;2B, kmous=\\E[<, knp=\\E[6~, kpp=\\E[5~,
52614
- kri=\\E[1;2A, oc=\\E]104\\007, op=\\E[39;49m, rc=\\E8,
52615
- rep=%p1%c\\E[%p2%{1}%-%db, rev=\\E[7m, ri=\\EM,
52616
- rin=\\E[%p1%dT, ritm=\\E[23m, rmacs=\\E(B, rmam=\\E[?7l,
52617
- rmcup=\\E[?1049l, rmir=\\E[4l, rmkx=\\E[?1l\\E>, rmso=\\E[27m,
52618
- rmul=\\E[24m, rs1=\\E]\\E\\\\\\Ec, sc=\\E7,
52619
- setab=\\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
52620
- setaf=\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
52621
- sgr=%?%p9%t\\E(0%e\\E(B%;\\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
52622
- sgr0=\\E(B\\E[m, sitm=\\E[3m, smacs=\\E(0, smam=\\E[?7h,
52623
- smcup=\\E[?1049h, smir=\\E[4h, smkx=\\E[?1h\\E=, smso=\\E[7m,
52624
- smul=\\E[4m, tbc=\\E[3g, tsl=\\E]2;, u6=\\E[%i%d;%dR, u7=\\E[6n,
52625
- u8=\\E[?%[;0123456789]c, u9=\\E[c, vpa=\\E[%i%p1%dd,
52626
- BD=\\E[?2004l, BE=\\E[?2004h, Clmg=\\E[s,
52627
- Cmg=\\E[%i%p1%d;%p2%ds, Dsmg=\\E[?69l, E3=\\E[3J,
52628
- Enmg=\\E[?69h, Ms=\\E]52;%p1%s;%p2%s\\007, PE=\\E[201~,
52629
- PS=\\E[200~, RV=\\E[>c, Se=\\E[2 q,
52630
- Setulc=\\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
52631
- Smulx=\\E[4:%p1%dm, Ss=\\E[%p1%d q,
52632
- Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;,
52633
- XM=\\E[?1006;1000%?%p1%{1}%=%th%el%;, XR=\\E[>0q,
52634
- fd=\\E[?1004l, fe=\\E[?1004h, kDC3=\\E[3;3~, kDC4=\\E[3;4~,
52635
- kDC5=\\E[3;5~, kDC6=\\E[3;6~, kDC7=\\E[3;7~, kDN=\\E[1;2B,
52636
- kDN3=\\E[1;3B, kDN4=\\E[1;4B, kDN5=\\E[1;5B, kDN6=\\E[1;6B,
52637
- kDN7=\\E[1;7B, kEND3=\\E[1;3F, kEND4=\\E[1;4F,
52638
- kEND5=\\E[1;5F, kEND6=\\E[1;6F, kEND7=\\E[1;7F,
52639
- kHOM3=\\E[1;3H, kHOM4=\\E[1;4H, kHOM5=\\E[1;5H,
52640
- kHOM6=\\E[1;6H, kHOM7=\\E[1;7H, kIC3=\\E[2;3~, kIC4=\\E[2;4~,
52641
- kIC5=\\E[2;5~, kIC6=\\E[2;6~, kIC7=\\E[2;7~, kLFT3=\\E[1;3D,
52642
- kLFT4=\\E[1;4D, kLFT5=\\E[1;5D, kLFT6=\\E[1;6D,
52643
- kLFT7=\\E[1;7D, kNXT3=\\E[6;3~, kNXT4=\\E[6;4~,
52644
- kNXT5=\\E[6;5~, kNXT6=\\E[6;6~, kNXT7=\\E[6;7~,
52645
- kPRV3=\\E[5;3~, kPRV4=\\E[5;4~, kPRV5=\\E[5;5~,
52646
- kPRV6=\\E[5;6~, kPRV7=\\E[5;7~, kRIT3=\\E[1;3C,
52647
- kRIT4=\\E[1;4C, kRIT5=\\E[1;5C, kRIT6=\\E[1;6C,
52648
- kRIT7=\\E[1;7C, kUP=\\E[1;2A, kUP3=\\E[1;3A, kUP4=\\E[1;4A,
52649
- kUP5=\\E[1;5A, kUP6=\\E[1;6A, kUP7=\\E[1;7A, kxIN=\\E[I,
52650
- kxOUT=\\E[O, rmxx=\\E[29m, rv=\\E\\\\[[0-9]+;[0-9]+;[0-9]+c,
52651
- setrgbb=\\E[48:2:%p1%d:%p2%d:%p3%dm,
52652
- setrgbf=\\E[38:2:%p1%d:%p2%d:%p3%dm, smxx=\\E[9m,
52653
- xm=\\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;,
52654
- xr=\\EP>\\\\|[ -~]+a\\E\\\\,
52655
- `;
52656
- var HEREDOC_MARKER = "TMEX_TERMINFO_EOF";
52657
- function buildEnsureGhosttyTerminfoScript() {
52658
- return [
52659
- "if ! infocmp xterm-ghostty >/dev/null 2>&1; then",
52660
- `tic -x - <<'${HEREDOC_MARKER}' >/dev/null 2>&1`,
52661
- XTERM_GHOSTTY_TERMINFO_SOURCE.trimEnd(),
52662
- HEREDOC_MARKER,
52663
- "fi",
52664
- "infocmp xterm-ghostty >/dev/null 2>&1"
52665
- ].join(`
52666
- `);
52629
+ function concatChunks(chunks, totalLength) {
52630
+ if (chunks.length === 1) {
52631
+ return chunks[0];
52632
+ }
52633
+ const merged = new Uint8Array(totalLength);
52634
+ let offset = 0;
52635
+ for (const chunk2 of chunks) {
52636
+ merged.set(chunk2, offset);
52637
+ offset += chunk2.length;
52638
+ }
52639
+ return merged;
52667
52640
  }
52668
-
52669
- // ../../apps/gateway/src/tmux-client/input-encoder.ts
52670
- var encoder = new TextEncoder;
52671
- var SEND_KEYS_HEX_CHUNK_BYTES = 256;
52672
- function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
52673
- const bytes = encoder.encode(input);
52674
- const chunks = [];
52675
- for (let offset = 0;offset < bytes.length; offset += chunkBytes) {
52676
- const chunk2 = bytes.slice(offset, offset + chunkBytes);
52677
- chunks.push(Array.from(chunk2, (byte) => byte.toString(16).padStart(2, "0")));
52641
+ function createControlModeParser(callbacks) {
52642
+ let pendingChunks = [];
52643
+ let pendingLength = 0;
52644
+ let discardingOversizedLine = false;
52645
+ let warnedOversizedLine = false;
52646
+ let warnedInvalidEscape = false;
52647
+ let warnedUnexpectedLine = false;
52648
+ let currentBlock = null;
52649
+ function warnInvalidEscape() {
52650
+ if (!warnedInvalidEscape) {
52651
+ warnedInvalidEscape = true;
52652
+ console.warn("[tmex] control mode parser met invalid escape sequence, passing through");
52653
+ }
52678
52654
  }
52679
- return chunks;
52655
+ function findByte(line, byte, from) {
52656
+ for (let index = from;index < line.length; index += 1) {
52657
+ if (line[index] === byte) {
52658
+ return index;
52659
+ }
52660
+ }
52661
+ return -1;
52662
+ }
52663
+ function decodeRange(line, start, end) {
52664
+ return decoder.decode(line.subarray(start, end));
52665
+ }
52666
+ function handleOutputLine(line, payloadStart) {
52667
+ const paneEnd = findByte(line, BYTE_SPACE, payloadStart);
52668
+ if (paneEnd < 0) {
52669
+ return;
52670
+ }
52671
+ const paneId = decodeRange(line, payloadStart, paneEnd);
52672
+ callbacks.onOutput(paneId, unescapeControlModeData(line, paneEnd + 1, warnInvalidEscape));
52673
+ }
52674
+ function handleExtendedOutputLine(line, payloadStart) {
52675
+ const paneEnd = findByte(line, BYTE_SPACE, payloadStart);
52676
+ if (paneEnd < 0) {
52677
+ return;
52678
+ }
52679
+ const paneId = decodeRange(line, payloadStart, paneEnd);
52680
+ for (let index = paneEnd;index + 2 < line.length; index += 1) {
52681
+ if (line[index] === BYTE_SPACE && line[index + 1] === 58 && line[index + 2] === BYTE_SPACE) {
52682
+ callbacks.onOutput(paneId, unescapeControlModeData(line, index + 3, warnInvalidEscape));
52683
+ return;
52684
+ }
52685
+ }
52686
+ }
52687
+ function handleLine(line) {
52688
+ if (line.length === 0) {
52689
+ return;
52690
+ }
52691
+ if (line[0] !== BYTE_PERCENT) {
52692
+ if (currentBlock) {
52693
+ if (currentBlock.lines.length < MAX_BLOCK_BODY_LINES) {
52694
+ currentBlock.lines.push(decoder.decode(line));
52695
+ }
52696
+ return;
52697
+ }
52698
+ if (!warnedUnexpectedLine) {
52699
+ warnedUnexpectedLine = true;
52700
+ console.warn(`[tmex] control mode parser ignored unexpected line: ${decoder.decode(line.subarray(0, 80))}`);
52701
+ }
52702
+ return;
52703
+ }
52704
+ const typeEnd = findByte(line, BYTE_SPACE, 0);
52705
+ const type = typeEnd < 0 ? decodeRange(line, 1, line.length) : decodeRange(line, 1, typeEnd);
52706
+ const argsStart = typeEnd < 0 ? line.length : typeEnd + 1;
52707
+ switch (type) {
52708
+ case "output":
52709
+ handleOutputLine(line, argsStart);
52710
+ return;
52711
+ case "extended-output":
52712
+ handleExtendedOutputLine(line, argsStart);
52713
+ return;
52714
+ case "begin": {
52715
+ if (currentBlock) {
52716
+ callbacks.onBlockEnd?.(currentBlock);
52717
+ }
52718
+ currentBlock = {
52719
+ args: decodeRange(line, argsStart, line.length),
52720
+ isError: false,
52721
+ lines: []
52722
+ };
52723
+ return;
52724
+ }
52725
+ case "end":
52726
+ case "error": {
52727
+ if (!currentBlock) {
52728
+ return;
52729
+ }
52730
+ const args = decodeRange(line, argsStart, line.length);
52731
+ if (args !== currentBlock.args) {
52732
+ console.warn(`[tmex] control mode block guard mismatch: begin "${currentBlock.args}" vs ${type} "${args}"`);
52733
+ }
52734
+ currentBlock.isError = type === "error";
52735
+ callbacks.onBlockEnd?.(currentBlock);
52736
+ currentBlock = null;
52737
+ return;
52738
+ }
52739
+ case "exit": {
52740
+ const reason = argsStart < line.length ? decodeRange(line, argsStart, line.length) : null;
52741
+ callbacks.onExit(reason);
52742
+ return;
52743
+ }
52744
+ default: {
52745
+ if (currentBlock && !KNOWN_NOTIFICATION_TYPES.has(type)) {
52746
+ if (currentBlock.lines.length < MAX_BLOCK_BODY_LINES) {
52747
+ currentBlock.lines.push(decoder.decode(line));
52748
+ }
52749
+ return;
52750
+ }
52751
+ callbacks.onNotification({
52752
+ type,
52753
+ args: decodeRange(line, argsStart, line.length),
52754
+ raw: decoder.decode(line)
52755
+ });
52756
+ return;
52757
+ }
52758
+ }
52759
+ }
52760
+ function takePendingLine(tail) {
52761
+ if (pendingLength === 0) {
52762
+ return tail;
52763
+ }
52764
+ pendingChunks.push(tail);
52765
+ const line = concatChunks(pendingChunks, pendingLength + tail.length);
52766
+ pendingChunks = [];
52767
+ pendingLength = 0;
52768
+ return line;
52769
+ }
52770
+ return {
52771
+ push(chunk2) {
52772
+ let start = 0;
52773
+ while (start <= chunk2.length) {
52774
+ const newlineIndex = findByte(chunk2, BYTE_LF, start);
52775
+ if (newlineIndex < 0) {
52776
+ break;
52777
+ }
52778
+ const tail = chunk2.subarray(start, newlineIndex);
52779
+ if (discardingOversizedLine) {
52780
+ discardingOversizedLine = false;
52781
+ pendingChunks = [];
52782
+ pendingLength = 0;
52783
+ } else {
52784
+ handleLine(takePendingLine(tail));
52785
+ }
52786
+ start = newlineIndex + 1;
52787
+ }
52788
+ if (start < chunk2.length) {
52789
+ const rest = chunk2.subarray(start);
52790
+ if (discardingOversizedLine) {
52791
+ return;
52792
+ }
52793
+ if (pendingLength + rest.length > MAX_LINE_BYTES) {
52794
+ if (!warnedOversizedLine) {
52795
+ warnedOversizedLine = true;
52796
+ console.warn("[tmex] control mode parser dropped oversized line");
52797
+ }
52798
+ discardingOversizedLine = true;
52799
+ pendingChunks = [];
52800
+ pendingLength = 0;
52801
+ return;
52802
+ }
52803
+ pendingChunks.push(rest);
52804
+ pendingLength += rest.length;
52805
+ }
52806
+ },
52807
+ end() {
52808
+ if (discardingOversizedLine || pendingLength === 0) {
52809
+ discardingOversizedLine = false;
52810
+ pendingChunks = [];
52811
+ pendingLength = 0;
52812
+ return;
52813
+ }
52814
+ const line = concatChunks(pendingChunks, pendingLength);
52815
+ pendingChunks = [];
52816
+ pendingLength = 0;
52817
+ handleLine(line);
52818
+ }
52819
+ };
52680
52820
  }
52681
52821
 
52682
52822
  // ../../apps/gateway/src/tmux-client/pane-stream-parser.ts
52683
- var decoder = new TextDecoder;
52823
+ var decoder2 = new TextDecoder;
52684
52824
  var MAX_OSC_KIND_BYTES = 16;
52685
52825
  var MAX_OSC_PAYLOAD_BYTES = 8 * 1024;
52686
52826
  var MAX_DCS_PASSTHROUGH_BYTES = 64 * 1024;
@@ -52714,14 +52854,14 @@ function createPaneStreamParser(options) {
52714
52854
  return true;
52715
52855
  }
52716
52856
  function emitTitle(bytes) {
52717
- const title = decoder.decode(new Uint8Array(bytes)).trim();
52857
+ const title = decoder2.decode(new Uint8Array(bytes)).trim();
52718
52858
  if (!title) {
52719
52859
  return;
52720
52860
  }
52721
52861
  options.onTitle(title);
52722
52862
  }
52723
52863
  function emitOsc() {
52724
- const payload = decoder.decode(new Uint8Array(oscPayloadBytes));
52864
+ const payload = decoder2.decode(new Uint8Array(oscPayloadBytes));
52725
52865
  switch (oscKind) {
52726
52866
  case "0":
52727
52867
  case "1":
@@ -53020,7 +53160,245 @@ function createPaneStreamParser(options) {
53020
53160
  };
53021
53161
  }
53022
53162
 
53163
+ // ../../apps/gateway/src/tmux-client/control-mode-subscription.ts
53164
+ var STRUCTURE_DEBOUNCE_MS = 150;
53165
+ var STRUCTURE_NOTIFICATION_TYPES = new Set([
53166
+ "layout-change",
53167
+ "session-renamed",
53168
+ "session-window-changed",
53169
+ "sessions-changed",
53170
+ "unlinked-window-add",
53171
+ "unlinked-window-close",
53172
+ "unlinked-window-renamed",
53173
+ "window-add",
53174
+ "window-close",
53175
+ "window-pane-changed",
53176
+ "window-renamed"
53177
+ ]);
53178
+ function createControlModeSubscription(callbacks) {
53179
+ const paneParsers = new Map;
53180
+ let structureTimer = null;
53181
+ let lastStructureEmitAt = 0;
53182
+ let disposed = false;
53183
+ function getPaneParser(paneId) {
53184
+ const existing = paneParsers.get(paneId);
53185
+ if (existing) {
53186
+ return existing;
53187
+ }
53188
+ const parser2 = createPaneStreamParser({
53189
+ onTitle: (title) => callbacks.onTitle(paneId, title),
53190
+ onBell: () => callbacks.onBell(paneId),
53191
+ onNotification: (notification) => callbacks.onNotification(paneId, notification)
53192
+ });
53193
+ paneParsers.set(paneId, parser2);
53194
+ return parser2;
53195
+ }
53196
+ function scheduleStructureChanged() {
53197
+ if (disposed) {
53198
+ return;
53199
+ }
53200
+ const now = Date.now();
53201
+ if (structureTimer) {
53202
+ return;
53203
+ }
53204
+ if (now - lastStructureEmitAt >= STRUCTURE_DEBOUNCE_MS) {
53205
+ lastStructureEmitAt = now;
53206
+ callbacks.onStructureChanged();
53207
+ return;
53208
+ }
53209
+ structureTimer = setTimeout(() => {
53210
+ structureTimer = null;
53211
+ if (disposed) {
53212
+ return;
53213
+ }
53214
+ lastStructureEmitAt = Date.now();
53215
+ callbacks.onStructureChanged();
53216
+ }, STRUCTURE_DEBOUNCE_MS - (now - lastStructureEmitAt));
53217
+ }
53218
+ function handleNotification(notification) {
53219
+ if (STRUCTURE_NOTIFICATION_TYPES.has(notification.type)) {
53220
+ scheduleStructureChanged();
53221
+ }
53222
+ }
53223
+ const parser = createControlModeParser({
53224
+ onOutput: (paneId, data) => {
53225
+ const output = getPaneParser(paneId).push(data);
53226
+ if (output.length > 0) {
53227
+ callbacks.onTerminalOutput(paneId, output);
53228
+ }
53229
+ },
53230
+ onNotification: handleNotification,
53231
+ onExit: (reason) => callbacks.onExit(reason),
53232
+ onBlockEnd: (block) => callbacks.onBlockEnd?.(block)
53233
+ });
53234
+ return {
53235
+ push(chunk2) {
53236
+ if (disposed) {
53237
+ return;
53238
+ }
53239
+ parser.push(chunk2);
53240
+ },
53241
+ end() {
53242
+ if (disposed) {
53243
+ return;
53244
+ }
53245
+ parser.end();
53246
+ },
53247
+ prunePanes(validPaneIds) {
53248
+ for (const paneId of Array.from(paneParsers.keys())) {
53249
+ if (!validPaneIds.has(paneId)) {
53250
+ paneParsers.delete(paneId);
53251
+ }
53252
+ }
53253
+ },
53254
+ dispose() {
53255
+ disposed = true;
53256
+ if (structureTimer) {
53257
+ clearTimeout(structureTimer);
53258
+ structureTimer = null;
53259
+ }
53260
+ paneParsers.clear();
53261
+ }
53262
+ };
53263
+ }
53264
+
53265
+ // ../../apps/gateway/src/tmux-client/ghostty-terminfo.ts
53266
+ var XTERM_GHOSTTY_TERMINFO_SOURCE = `xterm-ghostty|ghostty|Ghostty,
53267
+ am, bce, ccc, hs, km, mc5i, mir, msgr, npc, xenl, AX, Su, Tc, XT, fullkbd,
53268
+ colors#0x100, cols#80, it#8, lines#24, pairs#0x7fff,
53269
+ acsc=++\\,\\,--..00\`\`aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
53270
+ bel=^G, blink=\\E[5m, bold=\\E[1m, cbt=\\E[Z, civis=\\E[?25l,
53271
+ clear=\\E[H\\E[2J, cnorm=\\E[?12l\\E[?25h, cr=\\r,
53272
+ csr=\\E[%i%p1%d;%p2%dr, cub=\\E[%p1%dD, cub1=^H,
53273
+ cud=\\E[%p1%dB, cud1=\\n, cuf=\\E[%p1%dC, cuf1=\\E[C,
53274
+ cup=\\E[%i%p1%d;%p2%dH, cuu=\\E[%p1%dA, cuu1=\\E[A,
53275
+ cvvis=\\E[?12;25h, dch=\\E[%p1%dP, dch1=\\E[P, dim=\\E[2m,
53276
+ dl=\\E[%p1%dM, dl1=\\E[M, dsl=\\E]2;\\007, ech=\\E[%p1%dX,
53277
+ ed=\\E[J, el=\\E[K, el1=\\E[1K, flash=\\E[?5h$<100/>\\E[?5l,
53278
+ fsl=^G, home=\\E[H, hpa=\\E[%i%p1%dG, ht=^I, hts=\\EH,
53279
+ ich=\\E[%p1%d@, ich1=\\E[@, il=\\E[%p1%dL, il1=\\E[L, ind=\\n,
53280
+ indn=\\E[%p1%dS,
53281
+ initc=\\E]4;%p1%d;rgb:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\,
53282
+ invis=\\E[8m, kDC=\\E[3;2~, kEND=\\E[1;2F, kHOM=\\E[1;2H,
53283
+ kIC=\\E[2;2~, kLFT=\\E[1;2D, kNXT=\\E[6;2~, kPRV=\\E[5;2~,
53284
+ kRIT=\\E[1;2C, kbs=^?, kcbt=\\E[Z, kcub1=\\EOD, kcud1=\\EOB,
53285
+ kcuf1=\\EOC, kcuu1=\\EOA, kdch1=\\E[3~, kend=\\EOF, kent=\\EOM,
53286
+ kf1=\\EOP, kf10=\\E[21~, kf11=\\E[23~, kf12=\\E[24~,
53287
+ kf13=\\E[1;2P, kf14=\\E[1;2Q, kf15=\\E[1;2R, kf16=\\E[1;2S,
53288
+ kf17=\\E[15;2~, kf18=\\E[17;2~, kf19=\\E[18;2~, kf2=\\EOQ,
53289
+ kf20=\\E[19;2~, kf21=\\E[20;2~, kf22=\\E[21;2~,
53290
+ kf23=\\E[23;2~, kf24=\\E[24;2~, kf25=\\E[1;5P, kf26=\\E[1;5Q,
53291
+ kf27=\\E[1;5R, kf28=\\E[1;5S, kf29=\\E[15;5~, kf3=\\EOR,
53292
+ kf30=\\E[17;5~, kf31=\\E[18;5~, kf32=\\E[19;5~,
53293
+ kf33=\\E[20;5~, kf34=\\E[21;5~, kf35=\\E[23;5~,
53294
+ kf36=\\E[24;5~, kf37=\\E[1;6P, kf38=\\E[1;6Q, kf39=\\E[1;6R,
53295
+ kf4=\\EOS, kf40=\\E[1;6S, kf41=\\E[15;6~, kf42=\\E[17;6~,
53296
+ kf43=\\E[18;6~, kf44=\\E[19;6~, kf45=\\E[20;6~,
53297
+ kf46=\\E[21;6~, kf47=\\E[23;6~, kf48=\\E[24;6~,
53298
+ kf49=\\E[1;3P, kf5=\\E[15~, kf50=\\E[1;3Q, kf51=\\E[1;3R,
53299
+ kf52=\\E[1;3S, kf53=\\E[15;3~, kf54=\\E[17;3~,
53300
+ kf55=\\E[18;3~, kf56=\\E[19;3~, kf57=\\E[20;3~,
53301
+ kf58=\\E[21;3~, kf59=\\E[23;3~, kf6=\\E[17~, kf60=\\E[24;3~,
53302
+ kf61=\\E[1;4P, kf62=\\E[1;4Q, kf63=\\E[1;4R, kf7=\\E[18~,
53303
+ kf8=\\E[19~, kf9=\\E[20~, khome=\\EOH, kich1=\\E[2~,
53304
+ kind=\\E[1;2B, kmous=\\E[<, knp=\\E[6~, kpp=\\E[5~,
53305
+ kri=\\E[1;2A, oc=\\E]104\\007, op=\\E[39;49m, rc=\\E8,
53306
+ rep=%p1%c\\E[%p2%{1}%-%db, rev=\\E[7m, ri=\\EM,
53307
+ rin=\\E[%p1%dT, ritm=\\E[23m, rmacs=\\E(B, rmam=\\E[?7l,
53308
+ rmcup=\\E[?1049l, rmir=\\E[4l, rmkx=\\E[?1l\\E>, rmso=\\E[27m,
53309
+ rmul=\\E[24m, rs1=\\E]\\E\\\\\\Ec, sc=\\E7,
53310
+ setab=\\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
53311
+ setaf=\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
53312
+ sgr=%?%p9%t\\E(0%e\\E(B%;\\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
53313
+ sgr0=\\E(B\\E[m, sitm=\\E[3m, smacs=\\E(0, smam=\\E[?7h,
53314
+ smcup=\\E[?1049h, smir=\\E[4h, smkx=\\E[?1h\\E=, smso=\\E[7m,
53315
+ smul=\\E[4m, tbc=\\E[3g, tsl=\\E]2;, u6=\\E[%i%d;%dR, u7=\\E[6n,
53316
+ u8=\\E[?%[;0123456789]c, u9=\\E[c, vpa=\\E[%i%p1%dd,
53317
+ BD=\\E[?2004l, BE=\\E[?2004h, Clmg=\\E[s,
53318
+ Cmg=\\E[%i%p1%d;%p2%ds, Dsmg=\\E[?69l, E3=\\E[3J,
53319
+ Enmg=\\E[?69h, Ms=\\E]52;%p1%s;%p2%s\\007, PE=\\E[201~,
53320
+ PS=\\E[200~, RV=\\E[>c, Se=\\E[2 q,
53321
+ Setulc=\\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
53322
+ Smulx=\\E[4:%p1%dm, Ss=\\E[%p1%d q,
53323
+ Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;,
53324
+ XM=\\E[?1006;1000%?%p1%{1}%=%th%el%;, XR=\\E[>0q,
53325
+ fd=\\E[?1004l, fe=\\E[?1004h, kDC3=\\E[3;3~, kDC4=\\E[3;4~,
53326
+ kDC5=\\E[3;5~, kDC6=\\E[3;6~, kDC7=\\E[3;7~, kDN=\\E[1;2B,
53327
+ kDN3=\\E[1;3B, kDN4=\\E[1;4B, kDN5=\\E[1;5B, kDN6=\\E[1;6B,
53328
+ kDN7=\\E[1;7B, kEND3=\\E[1;3F, kEND4=\\E[1;4F,
53329
+ kEND5=\\E[1;5F, kEND6=\\E[1;6F, kEND7=\\E[1;7F,
53330
+ kHOM3=\\E[1;3H, kHOM4=\\E[1;4H, kHOM5=\\E[1;5H,
53331
+ kHOM6=\\E[1;6H, kHOM7=\\E[1;7H, kIC3=\\E[2;3~, kIC4=\\E[2;4~,
53332
+ kIC5=\\E[2;5~, kIC6=\\E[2;6~, kIC7=\\E[2;7~, kLFT3=\\E[1;3D,
53333
+ kLFT4=\\E[1;4D, kLFT5=\\E[1;5D, kLFT6=\\E[1;6D,
53334
+ kLFT7=\\E[1;7D, kNXT3=\\E[6;3~, kNXT4=\\E[6;4~,
53335
+ kNXT5=\\E[6;5~, kNXT6=\\E[6;6~, kNXT7=\\E[6;7~,
53336
+ kPRV3=\\E[5;3~, kPRV4=\\E[5;4~, kPRV5=\\E[5;5~,
53337
+ kPRV6=\\E[5;6~, kPRV7=\\E[5;7~, kRIT3=\\E[1;3C,
53338
+ kRIT4=\\E[1;4C, kRIT5=\\E[1;5C, kRIT6=\\E[1;6C,
53339
+ kRIT7=\\E[1;7C, kUP=\\E[1;2A, kUP3=\\E[1;3A, kUP4=\\E[1;4A,
53340
+ kUP5=\\E[1;5A, kUP6=\\E[1;6A, kUP7=\\E[1;7A, kxIN=\\E[I,
53341
+ kxOUT=\\E[O, rmxx=\\E[29m, rv=\\E\\\\[[0-9]+;[0-9]+;[0-9]+c,
53342
+ setrgbb=\\E[48:2:%p1%d:%p2%d:%p3%dm,
53343
+ setrgbf=\\E[38:2:%p1%d:%p2%d:%p3%dm, smxx=\\E[9m,
53344
+ xm=\\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;,
53345
+ xr=\\EP>\\\\|[ -~]+a\\E\\\\,
53346
+ `;
53347
+ var HEREDOC_MARKER = "TMEX_TERMINFO_EOF";
53348
+ function buildEnsureGhosttyTerminfoScript() {
53349
+ return [
53350
+ "if ! infocmp xterm-ghostty >/dev/null 2>&1; then",
53351
+ `tic -x - <<'${HEREDOC_MARKER}' >/dev/null 2>&1`,
53352
+ XTERM_GHOSTTY_TERMINFO_SOURCE.trimEnd(),
53353
+ HEREDOC_MARKER,
53354
+ "fi",
53355
+ "infocmp xterm-ghostty >/dev/null 2>&1"
53356
+ ].join(`
53357
+ `);
53358
+ }
53359
+
53360
+ // ../../apps/gateway/src/tmux-client/input-encoder.ts
53361
+ var encoder = new TextEncoder;
53362
+ var SEND_KEYS_HEX_CHUNK_BYTES = 256;
53363
+ function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
53364
+ const bytes = encoder.encode(input);
53365
+ const chunks = [];
53366
+ for (let offset = 0;offset < bytes.length; offset += chunkBytes) {
53367
+ const chunk2 = bytes.slice(offset, offset + chunkBytes);
53368
+ chunks.push(Array.from(chunk2, (byte) => byte.toString(16).padStart(2, "0")));
53369
+ }
53370
+ return chunks;
53371
+ }
53372
+
53373
+ // ../../apps/gateway/src/tmux-client/tmux-version.ts
53374
+ var MIN_CONTROL_MODE_VERSION = { major: 3, minor: 0 };
53375
+ function parseTmuxVersion(versionOutput) {
53376
+ const match = versionOutput.match(/(\d+)\.(\d+)/);
53377
+ if (!match) {
53378
+ return null;
53379
+ }
53380
+ return {
53381
+ major: Number.parseInt(match[1], 10),
53382
+ minor: Number.parseInt(match[2], 10)
53383
+ };
53384
+ }
53385
+ function isControlModeSupported(version2) {
53386
+ if (!version2) {
53387
+ return true;
53388
+ }
53389
+ if (version2.major !== MIN_CONTROL_MODE_VERSION.major) {
53390
+ return version2.major > MIN_CONTROL_MODE_VERSION.major;
53391
+ }
53392
+ return version2.minor >= MIN_CONTROL_MODE_VERSION.minor;
53393
+ }
53394
+
53023
53395
  // ../../apps/gateway/src/tmux-client/local-external-connection.ts
53396
+ var CONTROL_MAX_RESTARTS = 3;
53397
+ var CONTROL_RESTART_DELAY_MS = 500;
53398
+ var CONTROL_STABLE_RESET_MS = 1e4;
53399
+ var CONTROL_STDERR_TAIL_LIMIT = 2048;
53400
+ var CONTROL_ATTACH_READY_TIMEOUT_MS = 3000;
53401
+ var PARKING_WINDOW_NAME = "tmex-park";
53024
53402
  function hasRenderableTerminalContent(value) {
53025
53403
  return value.trim().length > 0;
53026
53404
  }
@@ -53048,6 +53426,26 @@ function defaultRun(argv) {
53048
53426
  }).catch(reject);
53049
53427
  });
53050
53428
  }
53429
+ function defaultSpawnControlClient(argv) {
53430
+ const subprocess = Bun.spawn(argv, {
53431
+ env: buildLocalTmuxEnv(getLocalShellPath()),
53432
+ stdin: "pipe",
53433
+ stdout: "pipe",
53434
+ stderr: "pipe"
53435
+ });
53436
+ const stdin = subprocess.stdin;
53437
+ return {
53438
+ stdout: subprocess.stdout,
53439
+ stderr: subprocess.stderr,
53440
+ exited: subprocess.exited,
53441
+ kill: () => {
53442
+ try {
53443
+ stdin?.end();
53444
+ } catch {}
53445
+ subprocess.kill();
53446
+ }
53447
+ };
53448
+ }
53051
53449
 
53052
53450
  class LocalExternalTmuxConnection {
53053
53451
  deviceId;
@@ -53062,30 +53460,27 @@ class LocalExternalTmuxConnection {
53062
53460
  pendingPaneTitles = new Map;
53063
53461
  snapshotSession = null;
53064
53462
  snapshotWindows = new Map;
53065
- paneReaders = new Map;
53066
- pipeTransition = Promise.resolve();
53067
53463
  inputTransition = Promise.resolve();
53068
- hookReadAbort = null;
53069
- hookBuffer = "";
53070
53464
  bellDedup = new Map;
53071
53465
  closeNotified = false;
53072
53466
  cleanupPromise = null;
53073
- fsPaths = createRuntimeFsPaths({
53074
- deviceId: "pending",
53075
- sessionName: "pending",
53076
- gatewayPid: process.pid
53077
- });
53467
+ controlProcess = null;
53468
+ controlSubscription = null;
53469
+ controlStartedAt = 0;
53470
+ controlRestartCount = 0;
53471
+ controlStderrTail = "";
53078
53472
  constructor(options, inputDeps = {}) {
53079
53473
  this.deviceId = options.deviceId;
53080
53474
  this.callbacks = options;
53081
53475
  this.deps = {
53082
- enableHooks: inputDeps.enableHooks ?? true,
53476
+ enableSubscription: inputDeps.enableSubscription ?? true,
53083
53477
  getDevice: inputDeps.getDevice ?? ((deviceId) => getDeviceById(deviceId)),
53084
53478
  run: inputDeps.run ?? defaultRun,
53085
53479
  ensureGhosttyTerminfo: inputDeps.ensureGhosttyTerminfo ?? (async () => {
53086
53480
  const result = await this.deps.run(["/bin/sh", "-c", buildEnsureGhosttyTerminfoScript()]);
53087
53481
  return result.exitCode === 0;
53088
- })
53482
+ }),
53483
+ spawnControlClient: inputDeps.spawnControlClient ?? defaultSpawnControlClient
53089
53484
  };
53090
53485
  }
53091
53486
  async connect() {
@@ -53099,16 +53494,13 @@ class LocalExternalTmuxConnection {
53099
53494
  throw new Error(`LocalExternalTmuxConnection only supports local device: ${this.deviceId}`);
53100
53495
  }
53101
53496
  this.sessionName = this.device.session?.trim() || "tmex";
53102
- this.fsPaths = createRuntimeFsPaths({
53103
- deviceId: this.deviceId,
53104
- sessionName: this.sessionName,
53105
- gatewayPid: process.pid
53106
- });
53107
- this.ensureRuntimeDirs();
53497
+ if (this.deps.enableSubscription) {
53498
+ await this.assertControlModeSupport();
53499
+ }
53108
53500
  await this.ensureSession();
53109
53501
  await this.configureSessionOptions();
53110
- if (this.deps.enableHooks) {
53111
- await this.startHooks();
53502
+ if (this.deps.enableSubscription) {
53503
+ await this.startControlClient();
53112
53504
  }
53113
53505
  this.connected = true;
53114
53506
  updateDeviceRuntimeStatus(this.deviceId, {
@@ -53125,11 +53517,7 @@ class LocalExternalTmuxConnection {
53125
53517
  }
53126
53518
  this.manualDisconnect = true;
53127
53519
  this.connected = false;
53128
- this.stopAllPipeReaders();
53129
- if (this.deps.enableHooks) {
53130
- this.stopHooks();
53131
- }
53132
- rmSync(this.fsPaths.rootDir, { recursive: true, force: true });
53520
+ this.stopControlClient();
53133
53521
  }
53134
53522
  requestSnapshot() {
53135
53523
  this.requestSnapshotInternal();
@@ -53241,7 +53629,14 @@ class LocalExternalTmuxConnection {
53241
53629
  "extended-keys-format",
53242
53630
  "csi-u"
53243
53631
  ]);
53244
- await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
53632
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "off"]);
53633
+ await this.runTmuxAllowFailure([
53634
+ "set-option",
53635
+ "-t",
53636
+ this.sessionName,
53637
+ "destroy-unattached",
53638
+ "off"
53639
+ ]);
53245
53640
  const termProgram = config.tmuxTermProgram.trim();
53246
53641
  if (termProgram && termProgram.toLowerCase() !== "off") {
53247
53642
  await this.runTmuxAllowFailure([
@@ -53262,86 +53657,204 @@ class LocalExternalTmuxConnection {
53262
53657
  }
53263
53658
  }
53264
53659
  }
53265
- ensureRuntimeDirs() {
53266
- mkdirSync(this.fsPaths.rootDir, { recursive: true, mode: 448 });
53267
- mkdirSync(this.fsPaths.panesDir, { recursive: true, mode: 448 });
53268
- mkdirSync(this.fsPaths.hooksDir, { recursive: true, mode: 448 });
53660
+ async assertControlModeSupport() {
53661
+ const result = await this.runTmuxAllowFailure(["-V"]);
53662
+ if (result.exitCode !== 0) {
53663
+ return;
53664
+ }
53665
+ const version2 = parseTmuxVersion(result.stdout.trim());
53666
+ if (!isControlModeSupported(version2)) {
53667
+ throw new Error(`tmux ${version2?.major}.${version2?.minor} is too old for tmex (control mode requires tmux >= 3.0)`);
53668
+ }
53269
53669
  }
53270
- async startHooks() {
53271
- this.ensureRuntimeDirs();
53272
- const fifoPath = this.fsPaths.hookFifoPath;
53273
- rmSync(fifoPath, { force: true });
53274
- await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
53275
- const readerProcess = Bun.spawn(["/bin/sh", "-lc", `tail -n +1 -f ${quoteShellArg(fifoPath)}`], {
53276
- stdout: "pipe",
53277
- stderr: "pipe"
53278
- });
53279
- const reader = readerProcess.stdout.getReader();
53280
- (async () => {
53281
- try {
53282
- while (true) {
53283
- const chunk2 = await reader.read();
53284
- if (chunk2.done) {
53285
- break;
53286
- }
53287
- this.handleHookChunk(new TextDecoder().decode(chunk2.value));
53288
- }
53289
- } catch (error) {
53290
- if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
53291
- this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
53292
- }
53293
- }
53294
- })();
53295
- this.hookReadAbort = () => {
53296
- reader.releaseLock();
53297
- readerProcess.kill();
53298
- rmSync(fifoPath, { force: true });
53299
- };
53300
- await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
53301
- await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
53302
- await this.installHook("after-new-window", ["refresh"]);
53303
- await this.installHook("after-split-window", ["refresh"]);
53304
- }
53305
- async stopHooks() {
53306
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
53307
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
53308
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-new-window"]);
53309
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-split-window"]);
53310
- this.hookReadAbort?.();
53311
- this.hookReadAbort = null;
53312
- this.hookBuffer = "";
53313
- }
53314
- async installHook(hookName, fields) {
53315
- const fifoPath = this.fsPaths.hookFifoPath;
53316
- const innerScript = `printf '%s\\t%s\\t%s\\n' ${fields.map((field) => quoteShellArg(field)).join(" ")} >> ${quoteShellArg(fifoPath)}`;
53317
- await this.runTmux([
53318
- "set-hook",
53670
+ async createParkingWindow() {
53671
+ const result = await this.runTmuxAllowFailure([
53672
+ "new-window",
53319
53673
  "-t",
53320
53674
  this.sessionName,
53321
- hookName,
53322
- `run-shell -b ${quoteShellArg(innerScript)}`
53675
+ "-n",
53676
+ PARKING_WINDOW_NAME,
53677
+ "-P",
53678
+ "-F",
53679
+ "#{window_id}",
53680
+ "sleep 30"
53323
53681
  ]);
53682
+ if (result.exitCode !== 0) {
53683
+ console.warn(`[local] failed to create parking window on ${this.deviceId}, attaching without focus shield`);
53684
+ return null;
53685
+ }
53686
+ return result.stdout.trim() || null;
53324
53687
  }
53325
- handleHookChunk(text2) {
53326
- this.hookBuffer += text2;
53327
- while (true) {
53328
- const newlineIndex = this.hookBuffer.indexOf(`
53329
- `);
53330
- if (newlineIndex < 0) {
53331
- return;
53688
+ async removeParkingWindow(windowId) {
53689
+ if (!windowId) {
53690
+ return;
53691
+ }
53692
+ await this.runTmuxAllowFailure(["last-window", "-t", this.sessionName]);
53693
+ await this.runTmuxAllowFailure(["kill-window", "-t", windowId]);
53694
+ }
53695
+ async startControlClient() {
53696
+ let attachReadyResolve = null;
53697
+ const attachReady = new Promise((resolve) => {
53698
+ attachReadyResolve = resolve;
53699
+ });
53700
+ const parkingWindowId = await this.createParkingWindow();
53701
+ let proc;
53702
+ try {
53703
+ proc = this.spawnControlClientProcess(() => {
53704
+ attachReadyResolve?.();
53705
+ attachReadyResolve = null;
53706
+ });
53707
+ await Promise.race([
53708
+ attachReady,
53709
+ new Promise((resolve) => setTimeout(resolve, CONTROL_ATTACH_READY_TIMEOUT_MS))
53710
+ ]);
53711
+ } finally {
53712
+ await this.removeParkingWindow(parkingWindowId);
53713
+ }
53714
+ if (this.controlProcess !== proc) {
53715
+ const message = this.controlStderrTail.trim() || "tmux control client exited during attach";
53716
+ console.warn(`[local] tmux control client died during attach on ${this.deviceId}: ${message}`);
53717
+ throw new Error(message);
53718
+ }
53719
+ }
53720
+ spawnControlClientProcess(onAttachReady) {
53721
+ const subscription = createControlModeSubscription({
53722
+ onTerminalOutput: (paneId, data) => {
53723
+ this.callbacks.onTerminalOutput(paneId, data);
53724
+ },
53725
+ onTitle: (paneId, title) => {
53726
+ this.pendingPaneTitles.set(paneId, title);
53727
+ this.requestSnapshot();
53728
+ },
53729
+ onBell: (paneId) => {
53730
+ this.recordBell(paneId);
53731
+ },
53732
+ onNotification: (paneId, notification) => {
53733
+ this.emitNotification(paneId, notification);
53734
+ },
53735
+ onStructureChanged: () => {
53736
+ this.requestSnapshot();
53737
+ },
53738
+ onExit: () => {},
53739
+ onBlockEnd: () => {
53740
+ onAttachReady();
53332
53741
  }
53333
- const line = this.hookBuffer.slice(0, newlineIndex).trim();
53334
- this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
53335
- if (!line) {
53336
- continue;
53742
+ });
53743
+ const proc = this.deps.spawnControlClient([
53744
+ "tmux",
53745
+ "-C",
53746
+ "attach-session",
53747
+ "-t",
53748
+ this.sessionName
53749
+ ]);
53750
+ this.controlProcess = proc;
53751
+ this.controlSubscription = subscription;
53752
+ this.controlStartedAt = Date.now();
53753
+ this.controlStderrTail = "";
53754
+ this.pumpControlStdout(proc, subscription);
53755
+ this.pumpControlStderr(proc);
53756
+ proc.exited.then((exitCode) => {
53757
+ this.handleControlClientExit(proc, exitCode);
53758
+ }).catch(() => {
53759
+ this.handleControlClientExit(proc, -1);
53760
+ });
53761
+ return proc;
53762
+ }
53763
+ async pumpControlStdout(proc, subscription) {
53764
+ const reader = proc.stdout.getReader();
53765
+ try {
53766
+ while (true) {
53767
+ const chunk2 = await reader.read();
53768
+ if (chunk2.done || this.controlProcess !== proc) {
53769
+ break;
53770
+ }
53771
+ subscription.push(chunk2.value);
53337
53772
  }
53338
- const [type, windowId, paneId] = line.split("\t");
53339
- if (type === "bell") {
53340
- continue;
53773
+ } catch (error) {
53774
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
53775
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
53341
53776
  }
53342
- if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
53343
- this.requestSnapshot();
53777
+ }
53778
+ subscription.end();
53779
+ }
53780
+ async pumpControlStderr(proc) {
53781
+ const reader = proc.stderr.getReader();
53782
+ const decoder3 = new TextDecoder;
53783
+ try {
53784
+ while (true) {
53785
+ const chunk2 = await reader.read();
53786
+ if (chunk2.done) {
53787
+ break;
53788
+ }
53789
+ if (this.controlProcess === proc) {
53790
+ this.controlStderrTail = (this.controlStderrTail + decoder3.decode(chunk2.value)).slice(-CONTROL_STDERR_TAIL_LIMIT);
53791
+ }
53344
53792
  }
53793
+ } catch {}
53794
+ }
53795
+ stopControlClient() {
53796
+ const proc = this.controlProcess;
53797
+ this.controlProcess = null;
53798
+ this.controlSubscription?.dispose();
53799
+ this.controlSubscription = null;
53800
+ proc?.kill();
53801
+ }
53802
+ handleControlClientExit(proc, exitCode) {
53803
+ if (this.controlProcess !== proc) {
53804
+ return;
53805
+ }
53806
+ this.controlProcess = null;
53807
+ this.controlSubscription?.dispose();
53808
+ this.controlSubscription = null;
53809
+ if (!this.connected || this.manualDisconnect) {
53810
+ return;
53811
+ }
53812
+ this.reconnectControlClient(exitCode);
53813
+ }
53814
+ async reconnectControlClient(exitCode) {
53815
+ if (Date.now() - this.controlStartedAt > CONTROL_STABLE_RESET_MS) {
53816
+ this.controlRestartCount = 0;
53817
+ }
53818
+ this.controlRestartCount += 1;
53819
+ const stderrMessage = this.controlStderrTail.trim();
53820
+ if (this.controlRestartCount > CONTROL_MAX_RESTARTS) {
53821
+ const message = stderrMessage || `tmux control client exited repeatedly (last code ${exitCode})`;
53822
+ console.warn(`[local] tmux control client gave up on ${this.deviceId}: ${message}`);
53823
+ this.notifyRuntimeError(message);
53824
+ this.shutdownInternal(true);
53825
+ return;
53826
+ }
53827
+ console.warn(`[local] tmux control client exited (code ${exitCode}) on ${this.deviceId}, reconnecting (attempt ${this.controlRestartCount})`);
53828
+ await new Promise((resolve) => setTimeout(resolve, CONTROL_RESTART_DELAY_MS * this.controlRestartCount));
53829
+ if (!this.connected || this.manualDisconnect) {
53830
+ return;
53831
+ }
53832
+ const probe = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
53833
+ if (probe.exitCode !== 0) {
53834
+ const message = probe.stderr.trim() || probe.stdout.trim() || "tmux session gone";
53835
+ console.warn(`[local] tmux session gone on ${this.deviceId}: ${message}`);
53836
+ updateDeviceRuntimeStatus(this.deviceId, {
53837
+ lastSeenAt: new Date().toISOString(),
53838
+ tmuxAvailable: false,
53839
+ lastError: message
53840
+ });
53841
+ this.shutdownInternal(true);
53842
+ return;
53843
+ }
53844
+ if (!this.connected || this.manualDisconnect) {
53845
+ return;
53846
+ }
53847
+ try {
53848
+ await this.startControlClient();
53849
+ } catch (error) {
53850
+ console.warn(`[local] control client restart failed on ${this.deviceId}:`, error);
53851
+ return;
53852
+ }
53853
+ this.requestSnapshot();
53854
+ if (this.activePaneId) {
53855
+ this.capturePaneHistory(this.activePaneId).catch(() => {
53856
+ return;
53857
+ });
53345
53858
  }
53346
53859
  }
53347
53860
  async runAndRefresh(argv, allowTargetMissing = false) {
@@ -53381,13 +53894,13 @@ class LocalExternalTmuxConnection {
53381
53894
  await this.requestSnapshotInternal();
53382
53895
  }
53383
53896
  async capturePaneHistory(paneId) {
53384
- const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
53385
- const alternateScreen = mode === "1";
53386
- const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-N", "-p"], true)).stdout;
53387
- const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-N", "-p", "-q"], true)).stdout;
53897
+ const screenInfo = parsePaneScreenInfo((await this.runTmux(["display-message", "-p", "-t", paneId, PANE_SCREEN_INFO_FORMAT], true)).stdout);
53898
+ const alternateScreen = screenInfo.alternateScreen;
53899
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-J", "-N", "-p"], true)).stdout;
53900
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-J", "-N", "-p", "-q"], true)).stdout;
53388
53901
  const history = alternateScreen ? hasRenderableTerminalContent(normal) ? normal : alternate : normal || alternate;
53389
53902
  if (history) {
53390
- this.callbacks.onTerminalHistory(paneId, history, alternateScreen);
53903
+ this.callbacks.onTerminalHistory(paneId, appendCursorRestore(history, screenInfo), alternateScreen);
53391
53904
  }
53392
53905
  }
53393
53906
  async requestSnapshotInternal() {
@@ -53438,7 +53951,7 @@ ${panesRes.stderr}`;
53438
53951
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
53439
53952
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
53440
53953
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
53441
- await this.syncPipeReaders();
53954
+ this.controlSubscription?.prunePanes(new Set(this.getExpectedPaneIds()));
53442
53955
  this.emitSnapshot();
53443
53956
  }
53444
53957
  parseSnapshotSession(lines) {
@@ -53564,108 +54077,6 @@ ${panesRes.stderr}`;
53564
54077
  getExpectedPaneIds() {
53565
54078
  return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
53566
54079
  }
53567
- async startPipeForPaneNow(paneId) {
53568
- if (this.paneReaders.has(paneId)) {
53569
- return;
53570
- }
53571
- const fifoPath = this.fsPaths.paneFifoPath(paneId);
53572
- this.ensureRuntimeDirs();
53573
- rmSync(fifoPath, { force: true });
53574
- await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
53575
- const parser = createPaneStreamParser({
53576
- onTitle: (title) => {
53577
- this.pendingPaneTitles.set(paneId, title);
53578
- this.requestSnapshot();
53579
- },
53580
- onBell: () => {
53581
- this.recordBell(paneId);
53582
- },
53583
- onNotification: (notification) => {
53584
- this.emitNotification(paneId, notification);
53585
- }
53586
- });
53587
- const readerProcess = Bun.spawn(["/bin/sh", "-lc", `cat ${quoteShellArg(fifoPath)}`], {
53588
- stdout: "pipe",
53589
- stderr: "pipe"
53590
- });
53591
- const reader = readerProcess.stdout.getReader();
53592
- const stopReader = () => {
53593
- reader.releaseLock();
53594
- readerProcess.kill();
53595
- rmSync(fifoPath, { force: true });
53596
- };
53597
- this.paneReaders.set(paneId, { paneId, fifoPath, stopReader });
53598
- (async () => {
53599
- try {
53600
- while (true) {
53601
- const chunk2 = await reader.read();
53602
- if (chunk2.done) {
53603
- break;
53604
- }
53605
- const output = parser.push(chunk2.value);
53606
- if (output.length > 0) {
53607
- this.callbacks.onTerminalOutput(paneId, output);
53608
- }
53609
- }
53610
- } catch (error) {
53611
- if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
53612
- this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
53613
- }
53614
- }
53615
- })();
53616
- try {
53617
- await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
53618
- } catch (error) {
53619
- this.paneReaders.delete(paneId);
53620
- stopReader();
53621
- throw error;
53622
- }
53623
- }
53624
- async stopPipeForPaneNow(paneId) {
53625
- const handle = this.paneReaders.get(paneId);
53626
- if (!handle) {
53627
- return;
53628
- }
53629
- this.paneReaders.delete(paneId);
53630
- await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
53631
- handle.stopReader();
53632
- }
53633
- async syncPipeReaders() {
53634
- const expectedPaneIds = this.getExpectedPaneIds();
53635
- const expectedSet = new Set(expectedPaneIds);
53636
- await this.queuePipeTransition(async () => {
53637
- for (const paneId of Array.from(this.paneReaders.keys())) {
53638
- if (!expectedSet.has(paneId)) {
53639
- await this.stopPipeForPaneNow(paneId);
53640
- }
53641
- }
53642
- for (const paneId of expectedPaneIds) {
53643
- if (!this.paneReaders.has(paneId)) {
53644
- await this.startPipeForPaneNow(paneId);
53645
- }
53646
- }
53647
- });
53648
- }
53649
- async stopAllPipeReaders() {
53650
- await this.queuePipeTransition(async () => {
53651
- for (const paneId of Array.from(this.paneReaders.keys())) {
53652
- await this.stopPipeForPaneNow(paneId);
53653
- }
53654
- });
53655
- }
53656
- queuePipeTransition(task) {
53657
- const next = this.pipeTransition.catch(() => {
53658
- return;
53659
- }).then(task);
53660
- this.pipeTransition = next;
53661
- return next;
53662
- }
53663
- async runShell(command) {
53664
- const result = await this.deps.run(["/bin/sh", "-lc", command]);
53665
- if (result.exitCode !== 0) {
53666
- throw new Error(result.stderr.trim() || `shell command failed: ${command}`);
53667
- }
53668
- }
53669
54080
  async runTmux(argv, allowTargetMissing = false) {
53670
54081
  const result = await this.runTmuxAllowFailure(argv);
53671
54082
  if (result.exitCode === 0) {
@@ -53722,17 +54133,7 @@ ${panesRes.stderr}`;
53722
54133
  }
53723
54134
  this.connected = false;
53724
54135
  this.cleanupPromise = (async () => {
53725
- await this.stopAllPipeReaders().catch(() => {
53726
- return;
53727
- });
53728
- if (this.deps.enableHooks) {
53729
- await this.stopHooks().catch(() => {
53730
- return;
53731
- });
53732
- }
53733
- try {
53734
- rmSync(this.fsPaths.rootDir, { recursive: true, force: true });
53735
- } catch {}
54136
+ this.stopControlClient();
53736
54137
  })();
53737
54138
  await this.cleanupPromise;
53738
54139
  this.cleanupPromise = null;
@@ -53756,9 +54157,17 @@ ${panesRes.stderr}`;
53756
54157
  // ../../apps/gateway/src/tmux-client/ssh-external-connection.ts
53757
54158
  var import_ssh2 = __toESM(require_lib3(), 1);
53758
54159
 
54160
+ // ../../apps/gateway/src/tmux-client/command-builder.ts
54161
+ function quoteShellArg(value) {
54162
+ return `'${value.replaceAll("'", "'\\''")}'`;
54163
+ }
54164
+ function joinShellArgs(argv) {
54165
+ return argv.map((arg) => quoteShellArg(arg)).join(" ");
54166
+ }
54167
+
53759
54168
  // ../../apps/gateway/src/tmux-client/ssh-connect-config.ts
53760
54169
  import { existsSync as existsSync2, readFileSync } from "fs";
53761
- import { join as join4 } from "path";
54170
+ import { join as join3 } from "path";
53762
54171
 
53763
54172
  // ../../apps/gateway/src/tmux/ssh-auth.ts
53764
54173
  function normalizeEnvValue(value) {
@@ -53811,7 +54220,7 @@ function expandHomePath(value, env) {
53811
54220
  return env.HOME?.trim() || trimmed;
53812
54221
  }
53813
54222
  if (trimmed.startsWith("~/") && env.HOME?.trim()) {
53814
- return join4(env.HOME.trim(), trimmed.slice(2));
54223
+ return join3(env.HOME.trim(), trimmed.slice(2));
53815
54224
  }
53816
54225
  return trimmed;
53817
54226
  }
@@ -54094,6 +54503,12 @@ function hasRenderableTerminalContent2(value) {
54094
54503
  var BELL_DEDUP_WINDOW_MS2 = 200;
54095
54504
  var COMMAND_SENTINEL = "\x1ETMEX_END ";
54096
54505
  var SNAPSHOT_FIELD_SEPARATOR = "|";
54506
+ var CONTROL_MAX_RESTARTS2 = 3;
54507
+ var CONTROL_RESTART_DELAY_MS2 = 500;
54508
+ var CONTROL_STABLE_RESET_MS2 = 1e4;
54509
+ var CONTROL_STDERR_TAIL_LIMIT2 = 2048;
54510
+ var CONTROL_ATTACH_READY_TIMEOUT_MS2 = 3000;
54511
+ var PARKING_WINDOW_NAME2 = "tmex-park";
54097
54512
  function splitSnapshotFields(line, fieldCount) {
54098
54513
  const parts = line.split(SNAPSHOT_FIELD_SEPARATOR);
54099
54514
  if (parts.length <= fieldCount) {
@@ -54134,16 +54549,12 @@ class SshExternalTmuxConnection {
54134
54549
  pendingPaneTitles = new Map;
54135
54550
  snapshotSession = null;
54136
54551
  snapshotWindows = new Map;
54137
- paneReaders = new Map;
54138
- pipeTransition = Promise.resolve();
54139
- hookReadAbort = null;
54140
- hookBuffer = "";
54141
54552
  bellDedup = new Map;
54142
- fsPaths = createRuntimeFsPaths({
54143
- deviceId: "pending",
54144
- sessionName: "pending",
54145
- gatewayPid: process.pid
54146
- });
54553
+ controlChannel = null;
54554
+ controlSubscription = null;
54555
+ controlStartedAt = 0;
54556
+ controlRestartCount = 0;
54557
+ controlStderrTail = "";
54147
54558
  sshClient = null;
54148
54559
  commandStream = null;
54149
54560
  commandStdoutBuffer = "";
@@ -54171,17 +54582,11 @@ class SshExternalTmuxConnection {
54171
54582
  throw new Error(`SshExternalTmuxConnection only supports ssh device: ${this.deviceId}`);
54172
54583
  }
54173
54584
  this.sessionName = this.device.session?.trim() || "tmex";
54174
- this.fsPaths = createRuntimeFsPaths({
54175
- deviceId: this.deviceId,
54176
- sessionName: this.sessionName,
54177
- gatewayPid: process.pid
54178
- });
54179
54585
  await this.connectSshClient();
54180
54586
  await this.openCommandChannel();
54181
- await this.ensureRemoteRuntimeDirs();
54182
54587
  await this.ensureSession();
54183
54588
  await this.configureSessionOptions();
54184
- await this.startHooks();
54589
+ await this.startControlClient();
54185
54590
  this.connected = true;
54186
54591
  updateDeviceRuntimeStatus(this.deviceId, {
54187
54592
  lastSeenAt: new Date().toISOString(),
@@ -54371,17 +54776,16 @@ class SshExternalTmuxConnection {
54371
54776
  }
54372
54777
  this.tmuxBin = parsed.tmuxBin;
54373
54778
  this.remoteHomeDir = parsed.homeDir;
54374
- }
54375
- async ensureRemoteRuntimeDirs() {
54376
- await this.runShell([
54377
- `mkdir -p ${quoteShellArg(this.fsPaths.rootDir)}`,
54378
- `mkdir -p ${quoteShellArg(this.fsPaths.panesDir)}`,
54379
- `mkdir -p ${quoteShellArg(this.fsPaths.hooksDir)}`,
54380
- `chmod 700 ${quoteShellArg(this.fsPaths.rootDir)}`,
54381
- `chmod 700 ${quoteShellArg(this.fsPaths.panesDir)}`,
54382
- `chmod 700 ${quoteShellArg(this.fsPaths.hooksDir)}`
54383
- ].join(`
54384
- `));
54779
+ const version2 = parseTmuxVersion(parsed.tmuxVersion);
54780
+ if (!isControlModeSupported(version2)) {
54781
+ const message = `remote tmux too old for tmex (control mode requires tmux >= 3.0, found ${parsed.tmuxVersion || "unknown"})`;
54782
+ updateDeviceRuntimeStatus(this.deviceId, {
54783
+ lastSeenAt: new Date().toISOString(),
54784
+ tmuxAvailable: false,
54785
+ lastError: message
54786
+ });
54787
+ throw new Error(message);
54788
+ }
54385
54789
  }
54386
54790
  async ensureSession() {
54387
54791
  const exists3 = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
@@ -54408,7 +54812,14 @@ class SshExternalTmuxConnection {
54408
54812
  "extended-keys-format",
54409
54813
  "csi-u"
54410
54814
  ]);
54411
- await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
54815
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "off"]);
54816
+ await this.runTmuxAllowFailure([
54817
+ "set-option",
54818
+ "-t",
54819
+ this.sessionName,
54820
+ "destroy-unattached",
54821
+ "off"
54822
+ ]);
54412
54823
  const termProgram = config.tmuxTermProgram.trim();
54413
54824
  if (termProgram && termProgram.toLowerCase() !== "off") {
54414
54825
  await this.runTmuxAllowFailure([
@@ -54437,71 +54848,167 @@ class SshExternalTmuxConnection {
54437
54848
  return false;
54438
54849
  }
54439
54850
  }
54440
- async startHooks() {
54441
- await this.ensureRemoteRuntimeDirs();
54442
- const fifoPath = this.fsPaths.hookFifoPath;
54443
- await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
54444
- const stopReader = await this.openReaderChannel(`exec tail -n +1 -f ${quoteShellArg(fifoPath)}`, {
54851
+ async createParkingWindow() {
54852
+ const result = await this.runTmuxAllowFailure([
54853
+ "new-window",
54854
+ "-t",
54855
+ this.sessionName,
54856
+ "-n",
54857
+ PARKING_WINDOW_NAME2,
54858
+ "-P",
54859
+ "-F",
54860
+ "#{window_id}",
54861
+ "sleep 30"
54862
+ ]);
54863
+ if (result.exitCode !== 0) {
54864
+ console.warn(`[ssh] failed to create parking window on ${this.deviceId}, attaching without focus shield`);
54865
+ return null;
54866
+ }
54867
+ return result.stdout.trim() || null;
54868
+ }
54869
+ async removeParkingWindow(windowId) {
54870
+ if (!windowId) {
54871
+ return;
54872
+ }
54873
+ await this.runTmuxAllowFailure(["last-window", "-t", this.sessionName]);
54874
+ await this.runTmuxAllowFailure(["kill-window", "-t", windowId]);
54875
+ }
54876
+ async startControlClient() {
54877
+ let attachReadyResolve = null;
54878
+ const attachReady = new Promise((resolve) => {
54879
+ attachReadyResolve = resolve;
54880
+ });
54881
+ const parkingWindowId = await this.createParkingWindow();
54882
+ let handle;
54883
+ try {
54884
+ handle = await this.openControlChannel(() => {
54885
+ attachReadyResolve?.();
54886
+ attachReadyResolve = null;
54887
+ });
54888
+ await Promise.race([
54889
+ attachReady,
54890
+ new Promise((resolve) => setTimeout(resolve, CONTROL_ATTACH_READY_TIMEOUT_MS2))
54891
+ ]);
54892
+ } finally {
54893
+ await this.removeParkingWindow(parkingWindowId);
54894
+ }
54895
+ if (this.controlChannel !== handle) {
54896
+ throw new Error(this.controlStderrTail.trim() || "tmux control client channel closed during attach");
54897
+ }
54898
+ }
54899
+ async openControlChannel(onAttachReady) {
54900
+ const subscription = createControlModeSubscription({
54901
+ onTerminalOutput: (paneId, data) => {
54902
+ this.callbacks.onTerminalOutput(paneId, data);
54903
+ },
54904
+ onTitle: (paneId, title) => {
54905
+ this.pendingPaneTitles.set(paneId, title);
54906
+ this.requestSnapshot();
54907
+ },
54908
+ onBell: (paneId) => {
54909
+ this.recordBell(paneId);
54910
+ },
54911
+ onNotification: (paneId, notification) => {
54912
+ this.emitNotification(paneId, notification);
54913
+ },
54914
+ onStructureChanged: () => {
54915
+ this.requestSnapshot();
54916
+ },
54917
+ onExit: () => {},
54918
+ onBlockEnd: () => {
54919
+ onAttachReady();
54920
+ }
54921
+ });
54922
+ const handle = { stop: () => {} };
54923
+ this.controlChannel = handle;
54924
+ this.controlSubscription = subscription;
54925
+ this.controlStartedAt = Date.now();
54926
+ this.controlStderrTail = "";
54927
+ const stopReader = await this.openReaderChannel(`exec ${quoteShellArg(this.tmuxBin)} -C attach-session -t ${quoteShellArg(this.sessionName)}`, {
54445
54928
  onData: (data) => {
54446
- this.handleHookChunk(data.toString());
54929
+ if (this.controlChannel === handle) {
54930
+ subscription.push(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
54931
+ }
54447
54932
  },
54448
- onClose: () => {
54449
- if (this.manualDisconnect) {
54450
- return;
54933
+ onStderr: (data) => {
54934
+ if (this.controlChannel === handle) {
54935
+ this.controlStderrTail = (this.controlStderrTail + data.toString()).slice(-CONTROL_STDERR_TAIL_LIMIT2);
54451
54936
  }
54452
- console.error("[ssh] hook reader channel closed unexpectedly, tearing down");
54453
- this.shutdownInternal(true);
54937
+ },
54938
+ onClose: () => {
54939
+ this.handleControlChannelClose(handle);
54454
54940
  }
54455
54941
  });
54456
- this.hookReadAbort = () => {
54457
- stopReader();
54458
- this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
54459
- };
54460
- await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
54461
- await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
54462
- await this.installHook("after-new-window", ["refresh"]);
54463
- await this.installHook("after-split-window", ["refresh"]);
54464
- }
54465
- async stopHooks() {
54466
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
54467
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
54468
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-new-window"]);
54469
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-split-window"]);
54470
- this.hookReadAbort?.();
54471
- this.hookReadAbort = null;
54472
- this.hookBuffer = "";
54473
- }
54474
- async installHook(hookName, fields) {
54475
- const fifoPath = this.fsPaths.hookFifoPath;
54476
- const innerScript = `printf '%s\\t%s\\t%s\\n' ${fields.map((field) => quoteShellArg(field)).join(" ")} >> ${quoteShellArg(fifoPath)}`;
54477
- await this.runTmux([
54478
- "set-hook",
54479
- "-t",
54480
- this.sessionName,
54481
- hookName,
54482
- `run-shell -b ${quoteShellArg(innerScript)}`
54483
- ]);
54942
+ handle.stop = stopReader;
54943
+ return handle;
54484
54944
  }
54485
- handleHookChunk(text2) {
54486
- this.hookBuffer += text2;
54487
- while (true) {
54488
- const newlineIndex = this.hookBuffer.indexOf(`
54489
- `);
54490
- if (newlineIndex < 0) {
54945
+ stopControlClient() {
54946
+ const handle = this.controlChannel;
54947
+ this.controlChannel = null;
54948
+ this.controlSubscription?.dispose();
54949
+ this.controlSubscription = null;
54950
+ handle?.stop();
54951
+ }
54952
+ handleControlChannelClose(handle) {
54953
+ if (this.controlChannel !== handle) {
54954
+ return;
54955
+ }
54956
+ this.controlChannel = null;
54957
+ this.controlSubscription?.dispose();
54958
+ this.controlSubscription = null;
54959
+ if (!this.connected || this.manualDisconnect) {
54960
+ return;
54961
+ }
54962
+ this.reconnectControlClient();
54963
+ }
54964
+ async reconnectControlClient() {
54965
+ if (Date.now() - this.controlStartedAt > CONTROL_STABLE_RESET_MS2) {
54966
+ this.controlRestartCount = 0;
54967
+ }
54968
+ this.controlRestartCount += 1;
54969
+ const stderrMessage = this.controlStderrTail.trim();
54970
+ if (this.controlRestartCount > CONTROL_MAX_RESTARTS2) {
54971
+ const message = stderrMessage || "tmux control client channel closed repeatedly";
54972
+ console.warn(`[ssh] tmux control client gave up on ${this.deviceId}: ${message}`);
54973
+ updateDeviceRuntimeStatus(this.deviceId, {
54974
+ lastSeenAt: new Date().toISOString(),
54975
+ tmuxAvailable: false,
54976
+ lastError: message
54977
+ });
54978
+ this.shutdownInternal(true);
54979
+ return;
54980
+ }
54981
+ console.warn(`[ssh] tmux control client channel closed on ${this.deviceId}, reconnecting (attempt ${this.controlRestartCount})`);
54982
+ await new Promise((resolve) => setTimeout(resolve, CONTROL_RESTART_DELAY_MS2 * this.controlRestartCount));
54983
+ if (!this.connected || this.manualDisconnect) {
54984
+ return;
54985
+ }
54986
+ const probe = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
54987
+ if (probe.exitCode !== 0) {
54988
+ const message = probe.stderr.trim() || probe.stdout.trim() || "tmux session gone";
54989
+ console.warn(`[ssh] tmux session gone on ${this.deviceId}: ${message}`);
54990
+ updateDeviceRuntimeStatus(this.deviceId, {
54991
+ lastSeenAt: new Date().toISOString(),
54992
+ tmuxAvailable: false,
54993
+ lastError: message
54994
+ });
54995
+ this.shutdownInternal(true);
54996
+ return;
54997
+ }
54998
+ if (!this.connected || this.manualDisconnect) {
54999
+ return;
55000
+ }
55001
+ try {
55002
+ await this.startControlClient();
55003
+ } catch (error) {
55004
+ console.warn(`[ssh] control client restart failed on ${this.deviceId}:`, error);
55005
+ return;
55006
+ }
55007
+ this.requestSnapshot();
55008
+ if (this.activePaneId) {
55009
+ this.capturePaneHistory(this.activePaneId).catch(() => {
54491
55010
  return;
54492
- }
54493
- const line = this.hookBuffer.slice(0, newlineIndex).trim();
54494
- this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
54495
- if (!line) {
54496
- continue;
54497
- }
54498
- const [type, windowId, paneId] = line.split("\t");
54499
- if (type === "bell") {
54500
- continue;
54501
- }
54502
- if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
54503
- this.requestSnapshot();
54504
- }
55011
+ });
54505
55012
  }
54506
55013
  }
54507
55014
  async runAndRefresh(argv, allowTargetMissing = false) {
@@ -54541,13 +55048,13 @@ class SshExternalTmuxConnection {
54541
55048
  await this.requestSnapshotInternal();
54542
55049
  }
54543
55050
  async capturePaneHistory(paneId) {
54544
- const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
54545
- const alternateScreen = mode === "1";
54546
- const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-N", "-p"], true, 30000)).stdout;
54547
- const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-N", "-p", "-q"], true, 30000)).stdout;
55051
+ const screenInfo = parsePaneScreenInfo((await this.runTmux(["display-message", "-p", "-t", paneId, PANE_SCREEN_INFO_FORMAT], true)).stdout);
55052
+ const alternateScreen = screenInfo.alternateScreen;
55053
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-J", "-N", "-p"], true, 30000)).stdout;
55054
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-J", "-N", "-p", "-q"], true, 30000)).stdout;
54548
55055
  const history = alternateScreen ? hasRenderableTerminalContent2(normal) ? normal : alternate : normal || alternate;
54549
55056
  if (history) {
54550
- this.callbacks.onTerminalHistory(paneId, history, alternateScreen);
55057
+ this.callbacks.onTerminalHistory(paneId, appendCursorRestore(history, screenInfo), alternateScreen);
54551
55058
  }
54552
55059
  }
54553
55060
  async requestSnapshotInternal() {
@@ -54598,7 +55105,7 @@ ${panesRes.stderr}`;
54598
55105
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
54599
55106
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
54600
55107
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
54601
- await this.syncPipeReaders();
55108
+ this.controlSubscription?.prunePanes(new Set(this.getExpectedPaneIds()));
54602
55109
  this.emitSnapshot();
54603
55110
  }
54604
55111
  parseSnapshotSession(lines) {
@@ -54724,104 +55231,6 @@ ${panesRes.stderr}`;
54724
55231
  getExpectedPaneIds() {
54725
55232
  return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
54726
55233
  }
54727
- async startPipeForPaneNow(paneId) {
54728
- if (this.paneReaders.has(paneId)) {
54729
- return;
54730
- }
54731
- const fifoPath = this.fsPaths.paneFifoPath(paneId);
54732
- await this.ensureRemoteRuntimeDirs();
54733
- await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
54734
- const parser = createPaneStreamParser({
54735
- onTitle: (title) => {
54736
- this.pendingPaneTitles.set(paneId, title);
54737
- this.requestSnapshot();
54738
- },
54739
- onBell: () => {
54740
- this.recordBell(paneId);
54741
- },
54742
- onNotification: (notification) => {
54743
- this.emitNotification(paneId, notification);
54744
- }
54745
- });
54746
- const stopReader = await this.openReaderChannel(`exec cat ${quoteShellArg(fifoPath)}`, {
54747
- onData: (raw) => {
54748
- const output = parser.push(raw);
54749
- if (output.length > 0) {
54750
- this.callbacks.onTerminalOutput(paneId, output);
54751
- }
54752
- },
54753
- onClose: () => {
54754
- if (this.manualDisconnect) {
54755
- return;
54756
- }
54757
- const existing = this.paneReaders.get(paneId);
54758
- if (!existing) {
54759
- return;
54760
- }
54761
- console.warn(`[ssh] pane reader channel closed for ${paneId}, resync on next snapshot`);
54762
- this.paneReaders.delete(paneId);
54763
- this.runShellAllowFailure(`rm -f ${quoteShellArg(existing.fifoPath)}`);
54764
- this.requestSnapshot();
54765
- }
54766
- });
54767
- const handle = {
54768
- paneId,
54769
- fifoPath,
54770
- stopReader: () => {
54771
- stopReader();
54772
- this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
54773
- }
54774
- };
54775
- this.paneReaders.set(paneId, handle);
54776
- try {
54777
- await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
54778
- } catch (error) {
54779
- this.paneReaders.delete(paneId);
54780
- handle.stopReader();
54781
- throw error;
54782
- }
54783
- }
54784
- async stopPipeForPaneNow(paneId) {
54785
- const handle = this.paneReaders.get(paneId);
54786
- if (!handle) {
54787
- return;
54788
- }
54789
- this.paneReaders.delete(paneId);
54790
- await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
54791
- handle.stopReader();
54792
- }
54793
- async syncPipeReaders() {
54794
- const expectedPaneIds = this.getExpectedPaneIds();
54795
- const expectedSet = new Set(expectedPaneIds);
54796
- await this.queuePipeTransition(async () => {
54797
- for (const paneId of Array.from(this.paneReaders.keys())) {
54798
- if (!expectedSet.has(paneId)) {
54799
- await this.stopPipeForPaneNow(paneId);
54800
- }
54801
- }
54802
- for (const paneId of expectedPaneIds) {
54803
- if (!this.paneReaders.has(paneId)) {
54804
- await this.startPipeForPaneNow(paneId);
54805
- }
54806
- }
54807
- });
54808
- }
54809
- async stopAllPipeReaders() {
54810
- await this.queuePipeTransition(async () => {
54811
- for (const paneId of Array.from(this.paneReaders.keys())) {
54812
- await this.stopPipeForPaneNow(paneId);
54813
- }
54814
- });
54815
- }
54816
- queuePipeTransition(task) {
54817
- const next = this.pipeTransition.catch(() => {
54818
- return;
54819
- }).then(task);
54820
- this.pipeTransition = next.catch(() => {
54821
- return;
54822
- });
54823
- return next;
54824
- }
54825
55234
  async runTmux(argv, allowTargetMissing = false, timeoutMs = 1e4) {
54826
55235
  const result = await this.runTmuxAllowFailure(argv, timeoutMs);
54827
55236
  if (result.exitCode === 0) {
@@ -54949,6 +55358,10 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
54949
55358
  options.onData(data);
54950
55359
  });
54951
55360
  stream.stderr.on("data", (data) => {
55361
+ if (options.onStderr) {
55362
+ options.onStderr(data);
55363
+ return;
55364
+ }
54952
55365
  if (!this.manualDisconnect) {
54953
55366
  this.callbacks.onError(new Error(data.toString().trim() || "SSH reader stderr output"));
54954
55367
  }
@@ -54993,15 +55406,7 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
54993
55406
  }
54994
55407
  this.connected = false;
54995
55408
  this.cleanupPromise = (async () => {
54996
- await this.stopAllPipeReaders().catch(() => {
54997
- return;
54998
- });
54999
- await this.stopHooks().catch(() => {
55000
- return;
55001
- });
55002
- await this.runShellAllowFailure(`rm -rf ${quoteShellArg(this.fsPaths.rootDir)}`).catch(() => {
55003
- return;
55004
- });
55409
+ this.stopControlClient();
55005
55410
  this.rejectPendingCommand(new Error("SSH command channel closed"));
55006
55411
  this.commandStream?.end();
55007
55412
  this.commandStream?.close();
@@ -57816,7 +58221,7 @@ async function serveFrontend(req, staticRoot) {
57816
58221
  if (!requestedPath) {
57817
58222
  return new Response(t3("runtime.forbidden"), { status: 403 });
57818
58223
  }
57819
- const indexPath = join5(staticRoot, "index.html");
58224
+ const indexPath = join4(staticRoot, "index.html");
57820
58225
  const targetPath = existsSync4(requestedPath) ? requestedPath : indexPath;
57821
58226
  if (!existsSync4(targetPath)) {
57822
58227
  return new Response(t3("runtime.frontendMissing"), { status: 500 });