tmex-cli 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/runtime/server.js +925 -553
  2. package/package.json +1 -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,264 @@ 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/control-mode-parser.ts
52534
+ var decoder = new TextDecoder;
52535
+ var MAX_LINE_BYTES = 4 * 1024 * 1024;
52536
+ var MAX_BLOCK_BODY_LINES = 1000;
52537
+ var BYTE_LF = 10;
52538
+ var BYTE_SPACE = 32;
52539
+ var BYTE_PERCENT = 37;
52540
+ var BYTE_BACKSLASH = 92;
52541
+ var KNOWN_NOTIFICATION_TYPES = new Set([
52542
+ "client-detached",
52543
+ "client-session-changed",
52544
+ "config-error",
52545
+ "continue",
52546
+ "layout-change",
52547
+ "message",
52548
+ "pane-mode-changed",
52549
+ "paste-buffer-changed",
52550
+ "paste-buffer-deleted",
52551
+ "pause",
52552
+ "session-changed",
52553
+ "session-renamed",
52554
+ "session-window-changed",
52555
+ "sessions-changed",
52556
+ "subscription-changed",
52557
+ "unlinked-window-add",
52558
+ "unlinked-window-close",
52559
+ "unlinked-window-renamed",
52560
+ "window-add",
52561
+ "window-close",
52562
+ "window-pane-changed",
52563
+ "window-renamed"
52564
+ ]);
52565
+ function isOctalDigit(byte) {
52566
+ return byte >= 48 && byte <= 55;
52567
+ }
52568
+ function unescapeControlModeData(line, start, onInvalidEscape) {
52569
+ const result = new Uint8Array(line.length - start);
52570
+ let written = 0;
52571
+ let index = start;
52572
+ while (index < line.length) {
52573
+ const byte = line[index];
52574
+ if (byte !== BYTE_BACKSLASH) {
52575
+ result[written] = byte;
52576
+ written += 1;
52577
+ index += 1;
52578
+ continue;
52579
+ }
52580
+ const d1 = line[index + 1];
52581
+ const d2 = line[index + 2];
52582
+ const d3 = line[index + 3];
52583
+ if (d1 !== undefined && d2 !== undefined && d3 !== undefined && isOctalDigit(d1) && isOctalDigit(d2) && isOctalDigit(d3)) {
52584
+ result[written] = d1 - 48 << 6 | d2 - 48 << 3 | d3 - 48;
52585
+ written += 1;
52586
+ index += 4;
52587
+ continue;
52588
+ }
52589
+ onInvalidEscape?.();
52590
+ result[written] = byte;
52591
+ written += 1;
52592
+ index += 1;
52593
+ }
52594
+ return result.subarray(0, written);
52537
52595
  }
52538
- function joinShellArgs(argv) {
52539
- return argv.map((arg) => quoteShellArg(arg)).join(" ");
52596
+ function concatChunks(chunks, totalLength) {
52597
+ if (chunks.length === 1) {
52598
+ return chunks[0];
52599
+ }
52600
+ const merged = new Uint8Array(totalLength);
52601
+ let offset = 0;
52602
+ for (const chunk2 of chunks) {
52603
+ merged.set(chunk2, offset);
52604
+ offset += chunk2.length;
52605
+ }
52606
+ return merged;
52540
52607
  }
52541
-
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");
52608
+ function createControlModeParser(callbacks) {
52609
+ let pendingChunks = [];
52610
+ let pendingLength = 0;
52611
+ let discardingOversizedLine = false;
52612
+ let warnedOversizedLine = false;
52613
+ let warnedInvalidEscape = false;
52614
+ let warnedUnexpectedLine = false;
52615
+ let currentBlock = null;
52616
+ function warnInvalidEscape() {
52617
+ if (!warnedInvalidEscape) {
52618
+ warnedInvalidEscape = true;
52619
+ console.warn("[tmex] control mode parser met invalid escape sequence, passing through");
52620
+ }
52621
+ }
52622
+ function findByte(line, byte, from) {
52623
+ for (let index = from;index < line.length; index += 1) {
52624
+ if (line[index] === byte) {
52625
+ return index;
52626
+ }
52627
+ }
52628
+ return -1;
52629
+ }
52630
+ function decodeRange(line, start, end) {
52631
+ return decoder.decode(line.subarray(start, end));
52632
+ }
52633
+ function handleOutputLine(line, payloadStart) {
52634
+ const paneEnd = findByte(line, BYTE_SPACE, payloadStart);
52635
+ if (paneEnd < 0) {
52636
+ return;
52637
+ }
52638
+ const paneId = decodeRange(line, payloadStart, paneEnd);
52639
+ callbacks.onOutput(paneId, unescapeControlModeData(line, paneEnd + 1, warnInvalidEscape));
52640
+ }
52641
+ function handleExtendedOutputLine(line, payloadStart) {
52642
+ const paneEnd = findByte(line, BYTE_SPACE, payloadStart);
52643
+ if (paneEnd < 0) {
52644
+ return;
52645
+ }
52646
+ const paneId = decodeRange(line, payloadStart, paneEnd);
52647
+ for (let index = paneEnd;index + 2 < line.length; index += 1) {
52648
+ if (line[index] === BYTE_SPACE && line[index + 1] === 58 && line[index + 2] === BYTE_SPACE) {
52649
+ callbacks.onOutput(paneId, unescapeControlModeData(line, index + 3, warnInvalidEscape));
52650
+ return;
52651
+ }
52652
+ }
52653
+ }
52654
+ function handleLine(line) {
52655
+ if (line.length === 0) {
52656
+ return;
52657
+ }
52658
+ if (line[0] !== BYTE_PERCENT) {
52659
+ if (currentBlock) {
52660
+ if (currentBlock.lines.length < MAX_BLOCK_BODY_LINES) {
52661
+ currentBlock.lines.push(decoder.decode(line));
52662
+ }
52663
+ return;
52664
+ }
52665
+ if (!warnedUnexpectedLine) {
52666
+ warnedUnexpectedLine = true;
52667
+ console.warn(`[tmex] control mode parser ignored unexpected line: ${decoder.decode(line.subarray(0, 80))}`);
52668
+ }
52669
+ return;
52670
+ }
52671
+ const typeEnd = findByte(line, BYTE_SPACE, 0);
52672
+ const type = typeEnd < 0 ? decodeRange(line, 1, line.length) : decodeRange(line, 1, typeEnd);
52673
+ const argsStart = typeEnd < 0 ? line.length : typeEnd + 1;
52674
+ switch (type) {
52675
+ case "output":
52676
+ handleOutputLine(line, argsStart);
52677
+ return;
52678
+ case "extended-output":
52679
+ handleExtendedOutputLine(line, argsStart);
52680
+ return;
52681
+ case "begin": {
52682
+ if (currentBlock) {
52683
+ callbacks.onBlockEnd?.(currentBlock);
52684
+ }
52685
+ currentBlock = {
52686
+ args: decodeRange(line, argsStart, line.length),
52687
+ isError: false,
52688
+ lines: []
52689
+ };
52690
+ return;
52691
+ }
52692
+ case "end":
52693
+ case "error": {
52694
+ if (!currentBlock) {
52695
+ return;
52696
+ }
52697
+ const args = decodeRange(line, argsStart, line.length);
52698
+ if (args !== currentBlock.args) {
52699
+ console.warn(`[tmex] control mode block guard mismatch: begin "${currentBlock.args}" vs ${type} "${args}"`);
52700
+ }
52701
+ currentBlock.isError = type === "error";
52702
+ callbacks.onBlockEnd?.(currentBlock);
52703
+ currentBlock = null;
52704
+ return;
52705
+ }
52706
+ case "exit": {
52707
+ const reason = argsStart < line.length ? decodeRange(line, argsStart, line.length) : null;
52708
+ callbacks.onExit(reason);
52709
+ return;
52710
+ }
52711
+ default: {
52712
+ if (currentBlock && !KNOWN_NOTIFICATION_TYPES.has(type)) {
52713
+ if (currentBlock.lines.length < MAX_BLOCK_BODY_LINES) {
52714
+ currentBlock.lines.push(decoder.decode(line));
52715
+ }
52716
+ return;
52717
+ }
52718
+ callbacks.onNotification({
52719
+ type,
52720
+ args: decodeRange(line, argsStart, line.length),
52721
+ raw: decoder.decode(line)
52722
+ });
52723
+ return;
52724
+ }
52725
+ }
52726
+ }
52727
+ function takePendingLine(tail) {
52728
+ if (pendingLength === 0) {
52729
+ return tail;
52730
+ }
52731
+ pendingChunks.push(tail);
52732
+ const line = concatChunks(pendingChunks, pendingLength + tail.length);
52733
+ pendingChunks = [];
52734
+ pendingLength = 0;
52735
+ return line;
52736
+ }
52563
52737
  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`);
52738
+ push(chunk2) {
52739
+ let start = 0;
52740
+ while (start <= chunk2.length) {
52741
+ const newlineIndex = findByte(chunk2, BYTE_LF, start);
52742
+ if (newlineIndex < 0) {
52743
+ break;
52744
+ }
52745
+ const tail = chunk2.subarray(start, newlineIndex);
52746
+ if (discardingOversizedLine) {
52747
+ discardingOversizedLine = false;
52748
+ pendingChunks = [];
52749
+ pendingLength = 0;
52750
+ } else {
52751
+ handleLine(takePendingLine(tail));
52752
+ }
52753
+ start = newlineIndex + 1;
52754
+ }
52755
+ if (start < chunk2.length) {
52756
+ const rest = chunk2.subarray(start);
52757
+ if (discardingOversizedLine) {
52758
+ return;
52759
+ }
52760
+ if (pendingLength + rest.length > MAX_LINE_BYTES) {
52761
+ if (!warnedOversizedLine) {
52762
+ warnedOversizedLine = true;
52763
+ console.warn("[tmex] control mode parser dropped oversized line");
52764
+ }
52765
+ discardingOversizedLine = true;
52766
+ pendingChunks = [];
52767
+ pendingLength = 0;
52768
+ return;
52769
+ }
52770
+ pendingChunks.push(rest);
52771
+ pendingLength += rest.length;
52772
+ }
52773
+ },
52774
+ end() {
52775
+ if (discardingOversizedLine || pendingLength === 0) {
52776
+ discardingOversizedLine = false;
52777
+ pendingChunks = [];
52778
+ pendingLength = 0;
52779
+ return;
52780
+ }
52781
+ const line = concatChunks(pendingChunks, pendingLength);
52782
+ pendingChunks = [];
52783
+ pendingLength = 0;
52784
+ handleLine(line);
52570
52785
  }
52571
52786
  };
52572
52787
  }
52573
52788
 
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
- `);
52667
- }
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")));
52678
- }
52679
- return chunks;
52680
- }
52681
-
52682
52789
  // ../../apps/gateway/src/tmux-client/pane-stream-parser.ts
52683
- var decoder = new TextDecoder;
52790
+ var decoder2 = new TextDecoder;
52684
52791
  var MAX_OSC_KIND_BYTES = 16;
52685
52792
  var MAX_OSC_PAYLOAD_BYTES = 8 * 1024;
52686
52793
  var MAX_DCS_PASSTHROUGH_BYTES = 64 * 1024;
@@ -52714,14 +52821,14 @@ function createPaneStreamParser(options) {
52714
52821
  return true;
52715
52822
  }
52716
52823
  function emitTitle(bytes) {
52717
- const title = decoder.decode(new Uint8Array(bytes)).trim();
52824
+ const title = decoder2.decode(new Uint8Array(bytes)).trim();
52718
52825
  if (!title) {
52719
52826
  return;
52720
52827
  }
52721
52828
  options.onTitle(title);
52722
52829
  }
52723
52830
  function emitOsc() {
52724
- const payload = decoder.decode(new Uint8Array(oscPayloadBytes));
52831
+ const payload = decoder2.decode(new Uint8Array(oscPayloadBytes));
52725
52832
  switch (oscKind) {
52726
52833
  case "0":
52727
52834
  case "1":
@@ -53020,7 +53127,245 @@ function createPaneStreamParser(options) {
53020
53127
  };
53021
53128
  }
53022
53129
 
53130
+ // ../../apps/gateway/src/tmux-client/control-mode-subscription.ts
53131
+ var STRUCTURE_DEBOUNCE_MS = 150;
53132
+ var STRUCTURE_NOTIFICATION_TYPES = new Set([
53133
+ "layout-change",
53134
+ "session-renamed",
53135
+ "session-window-changed",
53136
+ "sessions-changed",
53137
+ "unlinked-window-add",
53138
+ "unlinked-window-close",
53139
+ "unlinked-window-renamed",
53140
+ "window-add",
53141
+ "window-close",
53142
+ "window-pane-changed",
53143
+ "window-renamed"
53144
+ ]);
53145
+ function createControlModeSubscription(callbacks) {
53146
+ const paneParsers = new Map;
53147
+ let structureTimer = null;
53148
+ let lastStructureEmitAt = 0;
53149
+ let disposed = false;
53150
+ function getPaneParser(paneId) {
53151
+ const existing = paneParsers.get(paneId);
53152
+ if (existing) {
53153
+ return existing;
53154
+ }
53155
+ const parser2 = createPaneStreamParser({
53156
+ onTitle: (title) => callbacks.onTitle(paneId, title),
53157
+ onBell: () => callbacks.onBell(paneId),
53158
+ onNotification: (notification) => callbacks.onNotification(paneId, notification)
53159
+ });
53160
+ paneParsers.set(paneId, parser2);
53161
+ return parser2;
53162
+ }
53163
+ function scheduleStructureChanged() {
53164
+ if (disposed) {
53165
+ return;
53166
+ }
53167
+ const now = Date.now();
53168
+ if (structureTimer) {
53169
+ return;
53170
+ }
53171
+ if (now - lastStructureEmitAt >= STRUCTURE_DEBOUNCE_MS) {
53172
+ lastStructureEmitAt = now;
53173
+ callbacks.onStructureChanged();
53174
+ return;
53175
+ }
53176
+ structureTimer = setTimeout(() => {
53177
+ structureTimer = null;
53178
+ if (disposed) {
53179
+ return;
53180
+ }
53181
+ lastStructureEmitAt = Date.now();
53182
+ callbacks.onStructureChanged();
53183
+ }, STRUCTURE_DEBOUNCE_MS - (now - lastStructureEmitAt));
53184
+ }
53185
+ function handleNotification(notification) {
53186
+ if (STRUCTURE_NOTIFICATION_TYPES.has(notification.type)) {
53187
+ scheduleStructureChanged();
53188
+ }
53189
+ }
53190
+ const parser = createControlModeParser({
53191
+ onOutput: (paneId, data) => {
53192
+ const output = getPaneParser(paneId).push(data);
53193
+ if (output.length > 0) {
53194
+ callbacks.onTerminalOutput(paneId, output);
53195
+ }
53196
+ },
53197
+ onNotification: handleNotification,
53198
+ onExit: (reason) => callbacks.onExit(reason),
53199
+ onBlockEnd: (block) => callbacks.onBlockEnd?.(block)
53200
+ });
53201
+ return {
53202
+ push(chunk2) {
53203
+ if (disposed) {
53204
+ return;
53205
+ }
53206
+ parser.push(chunk2);
53207
+ },
53208
+ end() {
53209
+ if (disposed) {
53210
+ return;
53211
+ }
53212
+ parser.end();
53213
+ },
53214
+ prunePanes(validPaneIds) {
53215
+ for (const paneId of Array.from(paneParsers.keys())) {
53216
+ if (!validPaneIds.has(paneId)) {
53217
+ paneParsers.delete(paneId);
53218
+ }
53219
+ }
53220
+ },
53221
+ dispose() {
53222
+ disposed = true;
53223
+ if (structureTimer) {
53224
+ clearTimeout(structureTimer);
53225
+ structureTimer = null;
53226
+ }
53227
+ paneParsers.clear();
53228
+ }
53229
+ };
53230
+ }
53231
+
53232
+ // ../../apps/gateway/src/tmux-client/ghostty-terminfo.ts
53233
+ var XTERM_GHOSTTY_TERMINFO_SOURCE = `xterm-ghostty|ghostty|Ghostty,
53234
+ am, bce, ccc, hs, km, mc5i, mir, msgr, npc, xenl, AX, Su, Tc, XT, fullkbd,
53235
+ colors#0x100, cols#80, it#8, lines#24, pairs#0x7fff,
53236
+ acsc=++\\,\\,--..00\`\`aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
53237
+ bel=^G, blink=\\E[5m, bold=\\E[1m, cbt=\\E[Z, civis=\\E[?25l,
53238
+ clear=\\E[H\\E[2J, cnorm=\\E[?12l\\E[?25h, cr=\\r,
53239
+ csr=\\E[%i%p1%d;%p2%dr, cub=\\E[%p1%dD, cub1=^H,
53240
+ cud=\\E[%p1%dB, cud1=\\n, cuf=\\E[%p1%dC, cuf1=\\E[C,
53241
+ cup=\\E[%i%p1%d;%p2%dH, cuu=\\E[%p1%dA, cuu1=\\E[A,
53242
+ cvvis=\\E[?12;25h, dch=\\E[%p1%dP, dch1=\\E[P, dim=\\E[2m,
53243
+ dl=\\E[%p1%dM, dl1=\\E[M, dsl=\\E]2;\\007, ech=\\E[%p1%dX,
53244
+ ed=\\E[J, el=\\E[K, el1=\\E[1K, flash=\\E[?5h$<100/>\\E[?5l,
53245
+ fsl=^G, home=\\E[H, hpa=\\E[%i%p1%dG, ht=^I, hts=\\EH,
53246
+ ich=\\E[%p1%d@, ich1=\\E[@, il=\\E[%p1%dL, il1=\\E[L, ind=\\n,
53247
+ indn=\\E[%p1%dS,
53248
+ initc=\\E]4;%p1%d;rgb:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\,
53249
+ invis=\\E[8m, kDC=\\E[3;2~, kEND=\\E[1;2F, kHOM=\\E[1;2H,
53250
+ kIC=\\E[2;2~, kLFT=\\E[1;2D, kNXT=\\E[6;2~, kPRV=\\E[5;2~,
53251
+ kRIT=\\E[1;2C, kbs=^?, kcbt=\\E[Z, kcub1=\\EOD, kcud1=\\EOB,
53252
+ kcuf1=\\EOC, kcuu1=\\EOA, kdch1=\\E[3~, kend=\\EOF, kent=\\EOM,
53253
+ kf1=\\EOP, kf10=\\E[21~, kf11=\\E[23~, kf12=\\E[24~,
53254
+ kf13=\\E[1;2P, kf14=\\E[1;2Q, kf15=\\E[1;2R, kf16=\\E[1;2S,
53255
+ kf17=\\E[15;2~, kf18=\\E[17;2~, kf19=\\E[18;2~, kf2=\\EOQ,
53256
+ kf20=\\E[19;2~, kf21=\\E[20;2~, kf22=\\E[21;2~,
53257
+ kf23=\\E[23;2~, kf24=\\E[24;2~, kf25=\\E[1;5P, kf26=\\E[1;5Q,
53258
+ kf27=\\E[1;5R, kf28=\\E[1;5S, kf29=\\E[15;5~, kf3=\\EOR,
53259
+ kf30=\\E[17;5~, kf31=\\E[18;5~, kf32=\\E[19;5~,
53260
+ kf33=\\E[20;5~, kf34=\\E[21;5~, kf35=\\E[23;5~,
53261
+ kf36=\\E[24;5~, kf37=\\E[1;6P, kf38=\\E[1;6Q, kf39=\\E[1;6R,
53262
+ kf4=\\EOS, kf40=\\E[1;6S, kf41=\\E[15;6~, kf42=\\E[17;6~,
53263
+ kf43=\\E[18;6~, kf44=\\E[19;6~, kf45=\\E[20;6~,
53264
+ kf46=\\E[21;6~, kf47=\\E[23;6~, kf48=\\E[24;6~,
53265
+ kf49=\\E[1;3P, kf5=\\E[15~, kf50=\\E[1;3Q, kf51=\\E[1;3R,
53266
+ kf52=\\E[1;3S, kf53=\\E[15;3~, kf54=\\E[17;3~,
53267
+ kf55=\\E[18;3~, kf56=\\E[19;3~, kf57=\\E[20;3~,
53268
+ kf58=\\E[21;3~, kf59=\\E[23;3~, kf6=\\E[17~, kf60=\\E[24;3~,
53269
+ kf61=\\E[1;4P, kf62=\\E[1;4Q, kf63=\\E[1;4R, kf7=\\E[18~,
53270
+ kf8=\\E[19~, kf9=\\E[20~, khome=\\EOH, kich1=\\E[2~,
53271
+ kind=\\E[1;2B, kmous=\\E[<, knp=\\E[6~, kpp=\\E[5~,
53272
+ kri=\\E[1;2A, oc=\\E]104\\007, op=\\E[39;49m, rc=\\E8,
53273
+ rep=%p1%c\\E[%p2%{1}%-%db, rev=\\E[7m, ri=\\EM,
53274
+ rin=\\E[%p1%dT, ritm=\\E[23m, rmacs=\\E(B, rmam=\\E[?7l,
53275
+ rmcup=\\E[?1049l, rmir=\\E[4l, rmkx=\\E[?1l\\E>, rmso=\\E[27m,
53276
+ rmul=\\E[24m, rs1=\\E]\\E\\\\\\Ec, sc=\\E7,
53277
+ setab=\\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
53278
+ setaf=\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
53279
+ 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,
53280
+ sgr0=\\E(B\\E[m, sitm=\\E[3m, smacs=\\E(0, smam=\\E[?7h,
53281
+ smcup=\\E[?1049h, smir=\\E[4h, smkx=\\E[?1h\\E=, smso=\\E[7m,
53282
+ smul=\\E[4m, tbc=\\E[3g, tsl=\\E]2;, u6=\\E[%i%d;%dR, u7=\\E[6n,
53283
+ u8=\\E[?%[;0123456789]c, u9=\\E[c, vpa=\\E[%i%p1%dd,
53284
+ BD=\\E[?2004l, BE=\\E[?2004h, Clmg=\\E[s,
53285
+ Cmg=\\E[%i%p1%d;%p2%ds, Dsmg=\\E[?69l, E3=\\E[3J,
53286
+ Enmg=\\E[?69h, Ms=\\E]52;%p1%s;%p2%s\\007, PE=\\E[201~,
53287
+ PS=\\E[200~, RV=\\E[>c, Se=\\E[2 q,
53288
+ Setulc=\\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
53289
+ Smulx=\\E[4:%p1%dm, Ss=\\E[%p1%d q,
53290
+ Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;,
53291
+ XM=\\E[?1006;1000%?%p1%{1}%=%th%el%;, XR=\\E[>0q,
53292
+ fd=\\E[?1004l, fe=\\E[?1004h, kDC3=\\E[3;3~, kDC4=\\E[3;4~,
53293
+ kDC5=\\E[3;5~, kDC6=\\E[3;6~, kDC7=\\E[3;7~, kDN=\\E[1;2B,
53294
+ kDN3=\\E[1;3B, kDN4=\\E[1;4B, kDN5=\\E[1;5B, kDN6=\\E[1;6B,
53295
+ kDN7=\\E[1;7B, kEND3=\\E[1;3F, kEND4=\\E[1;4F,
53296
+ kEND5=\\E[1;5F, kEND6=\\E[1;6F, kEND7=\\E[1;7F,
53297
+ kHOM3=\\E[1;3H, kHOM4=\\E[1;4H, kHOM5=\\E[1;5H,
53298
+ kHOM6=\\E[1;6H, kHOM7=\\E[1;7H, kIC3=\\E[2;3~, kIC4=\\E[2;4~,
53299
+ kIC5=\\E[2;5~, kIC6=\\E[2;6~, kIC7=\\E[2;7~, kLFT3=\\E[1;3D,
53300
+ kLFT4=\\E[1;4D, kLFT5=\\E[1;5D, kLFT6=\\E[1;6D,
53301
+ kLFT7=\\E[1;7D, kNXT3=\\E[6;3~, kNXT4=\\E[6;4~,
53302
+ kNXT5=\\E[6;5~, kNXT6=\\E[6;6~, kNXT7=\\E[6;7~,
53303
+ kPRV3=\\E[5;3~, kPRV4=\\E[5;4~, kPRV5=\\E[5;5~,
53304
+ kPRV6=\\E[5;6~, kPRV7=\\E[5;7~, kRIT3=\\E[1;3C,
53305
+ kRIT4=\\E[1;4C, kRIT5=\\E[1;5C, kRIT6=\\E[1;6C,
53306
+ kRIT7=\\E[1;7C, kUP=\\E[1;2A, kUP3=\\E[1;3A, kUP4=\\E[1;4A,
53307
+ kUP5=\\E[1;5A, kUP6=\\E[1;6A, kUP7=\\E[1;7A, kxIN=\\E[I,
53308
+ kxOUT=\\E[O, rmxx=\\E[29m, rv=\\E\\\\[[0-9]+;[0-9]+;[0-9]+c,
53309
+ setrgbb=\\E[48:2:%p1%d:%p2%d:%p3%dm,
53310
+ setrgbf=\\E[38:2:%p1%d:%p2%d:%p3%dm, smxx=\\E[9m,
53311
+ xm=\\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;,
53312
+ xr=\\EP>\\\\|[ -~]+a\\E\\\\,
53313
+ `;
53314
+ var HEREDOC_MARKER = "TMEX_TERMINFO_EOF";
53315
+ function buildEnsureGhosttyTerminfoScript() {
53316
+ return [
53317
+ "if ! infocmp xterm-ghostty >/dev/null 2>&1; then",
53318
+ `tic -x - <<'${HEREDOC_MARKER}' >/dev/null 2>&1`,
53319
+ XTERM_GHOSTTY_TERMINFO_SOURCE.trimEnd(),
53320
+ HEREDOC_MARKER,
53321
+ "fi",
53322
+ "infocmp xterm-ghostty >/dev/null 2>&1"
53323
+ ].join(`
53324
+ `);
53325
+ }
53326
+
53327
+ // ../../apps/gateway/src/tmux-client/input-encoder.ts
53328
+ var encoder = new TextEncoder;
53329
+ var SEND_KEYS_HEX_CHUNK_BYTES = 256;
53330
+ function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
53331
+ const bytes = encoder.encode(input);
53332
+ const chunks = [];
53333
+ for (let offset = 0;offset < bytes.length; offset += chunkBytes) {
53334
+ const chunk2 = bytes.slice(offset, offset + chunkBytes);
53335
+ chunks.push(Array.from(chunk2, (byte) => byte.toString(16).padStart(2, "0")));
53336
+ }
53337
+ return chunks;
53338
+ }
53339
+
53340
+ // ../../apps/gateway/src/tmux-client/tmux-version.ts
53341
+ var MIN_CONTROL_MODE_VERSION = { major: 3, minor: 0 };
53342
+ function parseTmuxVersion(versionOutput) {
53343
+ const match = versionOutput.match(/(\d+)\.(\d+)/);
53344
+ if (!match) {
53345
+ return null;
53346
+ }
53347
+ return {
53348
+ major: Number.parseInt(match[1], 10),
53349
+ minor: Number.parseInt(match[2], 10)
53350
+ };
53351
+ }
53352
+ function isControlModeSupported(version2) {
53353
+ if (!version2) {
53354
+ return true;
53355
+ }
53356
+ if (version2.major !== MIN_CONTROL_MODE_VERSION.major) {
53357
+ return version2.major > MIN_CONTROL_MODE_VERSION.major;
53358
+ }
53359
+ return version2.minor >= MIN_CONTROL_MODE_VERSION.minor;
53360
+ }
53361
+
53023
53362
  // ../../apps/gateway/src/tmux-client/local-external-connection.ts
53363
+ var CONTROL_MAX_RESTARTS = 3;
53364
+ var CONTROL_RESTART_DELAY_MS = 500;
53365
+ var CONTROL_STABLE_RESET_MS = 1e4;
53366
+ var CONTROL_STDERR_TAIL_LIMIT = 2048;
53367
+ var CONTROL_ATTACH_READY_TIMEOUT_MS = 3000;
53368
+ var PARKING_WINDOW_NAME = "tmex-park";
53024
53369
  function hasRenderableTerminalContent(value) {
53025
53370
  return value.trim().length > 0;
53026
53371
  }
@@ -53048,6 +53393,26 @@ function defaultRun(argv) {
53048
53393
  }).catch(reject);
53049
53394
  });
53050
53395
  }
53396
+ function defaultSpawnControlClient(argv) {
53397
+ const subprocess = Bun.spawn(argv, {
53398
+ env: buildLocalTmuxEnv(getLocalShellPath()),
53399
+ stdin: "pipe",
53400
+ stdout: "pipe",
53401
+ stderr: "pipe"
53402
+ });
53403
+ const stdin = subprocess.stdin;
53404
+ return {
53405
+ stdout: subprocess.stdout,
53406
+ stderr: subprocess.stderr,
53407
+ exited: subprocess.exited,
53408
+ kill: () => {
53409
+ try {
53410
+ stdin?.end();
53411
+ } catch {}
53412
+ subprocess.kill();
53413
+ }
53414
+ };
53415
+ }
53051
53416
 
53052
53417
  class LocalExternalTmuxConnection {
53053
53418
  deviceId;
@@ -53062,30 +53427,27 @@ class LocalExternalTmuxConnection {
53062
53427
  pendingPaneTitles = new Map;
53063
53428
  snapshotSession = null;
53064
53429
  snapshotWindows = new Map;
53065
- paneReaders = new Map;
53066
- pipeTransition = Promise.resolve();
53067
53430
  inputTransition = Promise.resolve();
53068
- hookReadAbort = null;
53069
- hookBuffer = "";
53070
53431
  bellDedup = new Map;
53071
53432
  closeNotified = false;
53072
53433
  cleanupPromise = null;
53073
- fsPaths = createRuntimeFsPaths({
53074
- deviceId: "pending",
53075
- sessionName: "pending",
53076
- gatewayPid: process.pid
53077
- });
53434
+ controlProcess = null;
53435
+ controlSubscription = null;
53436
+ controlStartedAt = 0;
53437
+ controlRestartCount = 0;
53438
+ controlStderrTail = "";
53078
53439
  constructor(options, inputDeps = {}) {
53079
53440
  this.deviceId = options.deviceId;
53080
53441
  this.callbacks = options;
53081
53442
  this.deps = {
53082
- enableHooks: inputDeps.enableHooks ?? true,
53443
+ enableSubscription: inputDeps.enableSubscription ?? true,
53083
53444
  getDevice: inputDeps.getDevice ?? ((deviceId) => getDeviceById(deviceId)),
53084
53445
  run: inputDeps.run ?? defaultRun,
53085
53446
  ensureGhosttyTerminfo: inputDeps.ensureGhosttyTerminfo ?? (async () => {
53086
53447
  const result = await this.deps.run(["/bin/sh", "-c", buildEnsureGhosttyTerminfoScript()]);
53087
53448
  return result.exitCode === 0;
53088
- })
53449
+ }),
53450
+ spawnControlClient: inputDeps.spawnControlClient ?? defaultSpawnControlClient
53089
53451
  };
53090
53452
  }
53091
53453
  async connect() {
@@ -53099,16 +53461,13 @@ class LocalExternalTmuxConnection {
53099
53461
  throw new Error(`LocalExternalTmuxConnection only supports local device: ${this.deviceId}`);
53100
53462
  }
53101
53463
  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();
53464
+ if (this.deps.enableSubscription) {
53465
+ await this.assertControlModeSupport();
53466
+ }
53108
53467
  await this.ensureSession();
53109
53468
  await this.configureSessionOptions();
53110
- if (this.deps.enableHooks) {
53111
- await this.startHooks();
53469
+ if (this.deps.enableSubscription) {
53470
+ await this.startControlClient();
53112
53471
  }
53113
53472
  this.connected = true;
53114
53473
  updateDeviceRuntimeStatus(this.deviceId, {
@@ -53125,11 +53484,7 @@ class LocalExternalTmuxConnection {
53125
53484
  }
53126
53485
  this.manualDisconnect = true;
53127
53486
  this.connected = false;
53128
- this.stopAllPipeReaders();
53129
- if (this.deps.enableHooks) {
53130
- this.stopHooks();
53131
- }
53132
- rmSync(this.fsPaths.rootDir, { recursive: true, force: true });
53487
+ this.stopControlClient();
53133
53488
  }
53134
53489
  requestSnapshot() {
53135
53490
  this.requestSnapshotInternal();
@@ -53241,7 +53596,14 @@ class LocalExternalTmuxConnection {
53241
53596
  "extended-keys-format",
53242
53597
  "csi-u"
53243
53598
  ]);
53244
- await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
53599
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "off"]);
53600
+ await this.runTmuxAllowFailure([
53601
+ "set-option",
53602
+ "-t",
53603
+ this.sessionName,
53604
+ "destroy-unattached",
53605
+ "off"
53606
+ ]);
53245
53607
  const termProgram = config.tmuxTermProgram.trim();
53246
53608
  if (termProgram && termProgram.toLowerCase() !== "off") {
53247
53609
  await this.runTmuxAllowFailure([
@@ -53262,86 +53624,204 @@ class LocalExternalTmuxConnection {
53262
53624
  }
53263
53625
  }
53264
53626
  }
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 });
53627
+ async assertControlModeSupport() {
53628
+ const result = await this.runTmuxAllowFailure(["-V"]);
53629
+ if (result.exitCode !== 0) {
53630
+ return;
53631
+ }
53632
+ const version2 = parseTmuxVersion(result.stdout.trim());
53633
+ if (!isControlModeSupported(version2)) {
53634
+ throw new Error(`tmux ${version2?.major}.${version2?.minor} is too old for tmex (control mode requires tmux >= 3.0)`);
53635
+ }
53269
53636
  }
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",
53637
+ async createParkingWindow() {
53638
+ const result = await this.runTmuxAllowFailure([
53639
+ "new-window",
53319
53640
  "-t",
53320
53641
  this.sessionName,
53321
- hookName,
53322
- `run-shell -b ${quoteShellArg(innerScript)}`
53642
+ "-n",
53643
+ PARKING_WINDOW_NAME,
53644
+ "-P",
53645
+ "-F",
53646
+ "#{window_id}",
53647
+ "sleep 30"
53323
53648
  ]);
53649
+ if (result.exitCode !== 0) {
53650
+ console.warn(`[local] failed to create parking window on ${this.deviceId}, attaching without focus shield`);
53651
+ return null;
53652
+ }
53653
+ return result.stdout.trim() || null;
53324
53654
  }
53325
- handleHookChunk(text2) {
53326
- this.hookBuffer += text2;
53327
- while (true) {
53328
- const newlineIndex = this.hookBuffer.indexOf(`
53329
- `);
53330
- if (newlineIndex < 0) {
53331
- return;
53655
+ async removeParkingWindow(windowId) {
53656
+ if (!windowId) {
53657
+ return;
53658
+ }
53659
+ await this.runTmuxAllowFailure(["last-window", "-t", this.sessionName]);
53660
+ await this.runTmuxAllowFailure(["kill-window", "-t", windowId]);
53661
+ }
53662
+ async startControlClient() {
53663
+ let attachReadyResolve = null;
53664
+ const attachReady = new Promise((resolve) => {
53665
+ attachReadyResolve = resolve;
53666
+ });
53667
+ const parkingWindowId = await this.createParkingWindow();
53668
+ let proc;
53669
+ try {
53670
+ proc = this.spawnControlClientProcess(() => {
53671
+ attachReadyResolve?.();
53672
+ attachReadyResolve = null;
53673
+ });
53674
+ await Promise.race([
53675
+ attachReady,
53676
+ new Promise((resolve) => setTimeout(resolve, CONTROL_ATTACH_READY_TIMEOUT_MS))
53677
+ ]);
53678
+ } finally {
53679
+ await this.removeParkingWindow(parkingWindowId);
53680
+ }
53681
+ if (this.controlProcess !== proc) {
53682
+ const message = this.controlStderrTail.trim() || "tmux control client exited during attach";
53683
+ console.warn(`[local] tmux control client died during attach on ${this.deviceId}: ${message}`);
53684
+ throw new Error(message);
53685
+ }
53686
+ }
53687
+ spawnControlClientProcess(onAttachReady) {
53688
+ const subscription = createControlModeSubscription({
53689
+ onTerminalOutput: (paneId, data) => {
53690
+ this.callbacks.onTerminalOutput(paneId, data);
53691
+ },
53692
+ onTitle: (paneId, title) => {
53693
+ this.pendingPaneTitles.set(paneId, title);
53694
+ this.requestSnapshot();
53695
+ },
53696
+ onBell: (paneId) => {
53697
+ this.recordBell(paneId);
53698
+ },
53699
+ onNotification: (paneId, notification) => {
53700
+ this.emitNotification(paneId, notification);
53701
+ },
53702
+ onStructureChanged: () => {
53703
+ this.requestSnapshot();
53704
+ },
53705
+ onExit: () => {},
53706
+ onBlockEnd: () => {
53707
+ onAttachReady();
53332
53708
  }
53333
- const line = this.hookBuffer.slice(0, newlineIndex).trim();
53334
- this.hookBuffer = this.hookBuffer.slice(newlineIndex + 1);
53335
- if (!line) {
53336
- continue;
53709
+ });
53710
+ const proc = this.deps.spawnControlClient([
53711
+ "tmux",
53712
+ "-C",
53713
+ "attach-session",
53714
+ "-t",
53715
+ this.sessionName
53716
+ ]);
53717
+ this.controlProcess = proc;
53718
+ this.controlSubscription = subscription;
53719
+ this.controlStartedAt = Date.now();
53720
+ this.controlStderrTail = "";
53721
+ this.pumpControlStdout(proc, subscription);
53722
+ this.pumpControlStderr(proc);
53723
+ proc.exited.then((exitCode) => {
53724
+ this.handleControlClientExit(proc, exitCode);
53725
+ }).catch(() => {
53726
+ this.handleControlClientExit(proc, -1);
53727
+ });
53728
+ return proc;
53729
+ }
53730
+ async pumpControlStdout(proc, subscription) {
53731
+ const reader = proc.stdout.getReader();
53732
+ try {
53733
+ while (true) {
53734
+ const chunk2 = await reader.read();
53735
+ if (chunk2.done || this.controlProcess !== proc) {
53736
+ break;
53737
+ }
53738
+ subscription.push(chunk2.value);
53337
53739
  }
53338
- const [type, windowId, paneId] = line.split("\t");
53339
- if (type === "bell") {
53340
- continue;
53740
+ } catch (error) {
53741
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
53742
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
53341
53743
  }
53342
- if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
53343
- this.requestSnapshot();
53744
+ }
53745
+ subscription.end();
53746
+ }
53747
+ async pumpControlStderr(proc) {
53748
+ const reader = proc.stderr.getReader();
53749
+ const decoder3 = new TextDecoder;
53750
+ try {
53751
+ while (true) {
53752
+ const chunk2 = await reader.read();
53753
+ if (chunk2.done) {
53754
+ break;
53755
+ }
53756
+ if (this.controlProcess === proc) {
53757
+ this.controlStderrTail = (this.controlStderrTail + decoder3.decode(chunk2.value)).slice(-CONTROL_STDERR_TAIL_LIMIT);
53758
+ }
53344
53759
  }
53760
+ } catch {}
53761
+ }
53762
+ stopControlClient() {
53763
+ const proc = this.controlProcess;
53764
+ this.controlProcess = null;
53765
+ this.controlSubscription?.dispose();
53766
+ this.controlSubscription = null;
53767
+ proc?.kill();
53768
+ }
53769
+ handleControlClientExit(proc, exitCode) {
53770
+ if (this.controlProcess !== proc) {
53771
+ return;
53772
+ }
53773
+ this.controlProcess = null;
53774
+ this.controlSubscription?.dispose();
53775
+ this.controlSubscription = null;
53776
+ if (!this.connected || this.manualDisconnect) {
53777
+ return;
53778
+ }
53779
+ this.reconnectControlClient(exitCode);
53780
+ }
53781
+ async reconnectControlClient(exitCode) {
53782
+ if (Date.now() - this.controlStartedAt > CONTROL_STABLE_RESET_MS) {
53783
+ this.controlRestartCount = 0;
53784
+ }
53785
+ this.controlRestartCount += 1;
53786
+ const stderrMessage = this.controlStderrTail.trim();
53787
+ if (this.controlRestartCount > CONTROL_MAX_RESTARTS) {
53788
+ const message = stderrMessage || `tmux control client exited repeatedly (last code ${exitCode})`;
53789
+ console.warn(`[local] tmux control client gave up on ${this.deviceId}: ${message}`);
53790
+ this.notifyRuntimeError(message);
53791
+ this.shutdownInternal(true);
53792
+ return;
53793
+ }
53794
+ console.warn(`[local] tmux control client exited (code ${exitCode}) on ${this.deviceId}, reconnecting (attempt ${this.controlRestartCount})`);
53795
+ await new Promise((resolve) => setTimeout(resolve, CONTROL_RESTART_DELAY_MS * this.controlRestartCount));
53796
+ if (!this.connected || this.manualDisconnect) {
53797
+ return;
53798
+ }
53799
+ const probe = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
53800
+ if (probe.exitCode !== 0) {
53801
+ const message = probe.stderr.trim() || probe.stdout.trim() || "tmux session gone";
53802
+ console.warn(`[local] tmux session gone on ${this.deviceId}: ${message}`);
53803
+ updateDeviceRuntimeStatus(this.deviceId, {
53804
+ lastSeenAt: new Date().toISOString(),
53805
+ tmuxAvailable: false,
53806
+ lastError: message
53807
+ });
53808
+ this.shutdownInternal(true);
53809
+ return;
53810
+ }
53811
+ if (!this.connected || this.manualDisconnect) {
53812
+ return;
53813
+ }
53814
+ try {
53815
+ await this.startControlClient();
53816
+ } catch (error) {
53817
+ console.warn(`[local] control client restart failed on ${this.deviceId}:`, error);
53818
+ return;
53819
+ }
53820
+ this.requestSnapshot();
53821
+ if (this.activePaneId) {
53822
+ this.capturePaneHistory(this.activePaneId).catch(() => {
53823
+ return;
53824
+ });
53345
53825
  }
53346
53826
  }
53347
53827
  async runAndRefresh(argv, allowTargetMissing = false) {
@@ -53438,7 +53918,7 @@ ${panesRes.stderr}`;
53438
53918
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
53439
53919
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
53440
53920
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
53441
- await this.syncPipeReaders();
53921
+ this.controlSubscription?.prunePanes(new Set(this.getExpectedPaneIds()));
53442
53922
  this.emitSnapshot();
53443
53923
  }
53444
53924
  parseSnapshotSession(lines) {
@@ -53564,108 +54044,6 @@ ${panesRes.stderr}`;
53564
54044
  getExpectedPaneIds() {
53565
54045
  return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
53566
54046
  }
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
54047
  async runTmux(argv, allowTargetMissing = false) {
53670
54048
  const result = await this.runTmuxAllowFailure(argv);
53671
54049
  if (result.exitCode === 0) {
@@ -53722,17 +54100,7 @@ ${panesRes.stderr}`;
53722
54100
  }
53723
54101
  this.connected = false;
53724
54102
  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 {}
54103
+ this.stopControlClient();
53736
54104
  })();
53737
54105
  await this.cleanupPromise;
53738
54106
  this.cleanupPromise = null;
@@ -53756,9 +54124,17 @@ ${panesRes.stderr}`;
53756
54124
  // ../../apps/gateway/src/tmux-client/ssh-external-connection.ts
53757
54125
  var import_ssh2 = __toESM(require_lib3(), 1);
53758
54126
 
54127
+ // ../../apps/gateway/src/tmux-client/command-builder.ts
54128
+ function quoteShellArg(value) {
54129
+ return `'${value.replaceAll("'", "'\\''")}'`;
54130
+ }
54131
+ function joinShellArgs(argv) {
54132
+ return argv.map((arg) => quoteShellArg(arg)).join(" ");
54133
+ }
54134
+
53759
54135
  // ../../apps/gateway/src/tmux-client/ssh-connect-config.ts
53760
54136
  import { existsSync as existsSync2, readFileSync } from "fs";
53761
- import { join as join4 } from "path";
54137
+ import { join as join3 } from "path";
53762
54138
 
53763
54139
  // ../../apps/gateway/src/tmux/ssh-auth.ts
53764
54140
  function normalizeEnvValue(value) {
@@ -53811,7 +54187,7 @@ function expandHomePath(value, env) {
53811
54187
  return env.HOME?.trim() || trimmed;
53812
54188
  }
53813
54189
  if (trimmed.startsWith("~/") && env.HOME?.trim()) {
53814
- return join4(env.HOME.trim(), trimmed.slice(2));
54190
+ return join3(env.HOME.trim(), trimmed.slice(2));
53815
54191
  }
53816
54192
  return trimmed;
53817
54193
  }
@@ -54094,6 +54470,12 @@ function hasRenderableTerminalContent2(value) {
54094
54470
  var BELL_DEDUP_WINDOW_MS2 = 200;
54095
54471
  var COMMAND_SENTINEL = "\x1ETMEX_END ";
54096
54472
  var SNAPSHOT_FIELD_SEPARATOR = "|";
54473
+ var CONTROL_MAX_RESTARTS2 = 3;
54474
+ var CONTROL_RESTART_DELAY_MS2 = 500;
54475
+ var CONTROL_STABLE_RESET_MS2 = 1e4;
54476
+ var CONTROL_STDERR_TAIL_LIMIT2 = 2048;
54477
+ var CONTROL_ATTACH_READY_TIMEOUT_MS2 = 3000;
54478
+ var PARKING_WINDOW_NAME2 = "tmex-park";
54097
54479
  function splitSnapshotFields(line, fieldCount) {
54098
54480
  const parts = line.split(SNAPSHOT_FIELD_SEPARATOR);
54099
54481
  if (parts.length <= fieldCount) {
@@ -54134,16 +54516,12 @@ class SshExternalTmuxConnection {
54134
54516
  pendingPaneTitles = new Map;
54135
54517
  snapshotSession = null;
54136
54518
  snapshotWindows = new Map;
54137
- paneReaders = new Map;
54138
- pipeTransition = Promise.resolve();
54139
- hookReadAbort = null;
54140
- hookBuffer = "";
54141
54519
  bellDedup = new Map;
54142
- fsPaths = createRuntimeFsPaths({
54143
- deviceId: "pending",
54144
- sessionName: "pending",
54145
- gatewayPid: process.pid
54146
- });
54520
+ controlChannel = null;
54521
+ controlSubscription = null;
54522
+ controlStartedAt = 0;
54523
+ controlRestartCount = 0;
54524
+ controlStderrTail = "";
54147
54525
  sshClient = null;
54148
54526
  commandStream = null;
54149
54527
  commandStdoutBuffer = "";
@@ -54171,17 +54549,11 @@ class SshExternalTmuxConnection {
54171
54549
  throw new Error(`SshExternalTmuxConnection only supports ssh device: ${this.deviceId}`);
54172
54550
  }
54173
54551
  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
54552
  await this.connectSshClient();
54180
54553
  await this.openCommandChannel();
54181
- await this.ensureRemoteRuntimeDirs();
54182
54554
  await this.ensureSession();
54183
54555
  await this.configureSessionOptions();
54184
- await this.startHooks();
54556
+ await this.startControlClient();
54185
54557
  this.connected = true;
54186
54558
  updateDeviceRuntimeStatus(this.deviceId, {
54187
54559
  lastSeenAt: new Date().toISOString(),
@@ -54371,17 +54743,16 @@ class SshExternalTmuxConnection {
54371
54743
  }
54372
54744
  this.tmuxBin = parsed.tmuxBin;
54373
54745
  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
- `));
54746
+ const version2 = parseTmuxVersion(parsed.tmuxVersion);
54747
+ if (!isControlModeSupported(version2)) {
54748
+ const message = `remote tmux too old for tmex (control mode requires tmux >= 3.0, found ${parsed.tmuxVersion || "unknown"})`;
54749
+ updateDeviceRuntimeStatus(this.deviceId, {
54750
+ lastSeenAt: new Date().toISOString(),
54751
+ tmuxAvailable: false,
54752
+ lastError: message
54753
+ });
54754
+ throw new Error(message);
54755
+ }
54385
54756
  }
54386
54757
  async ensureSession() {
54387
54758
  const exists3 = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
@@ -54408,7 +54779,14 @@ class SshExternalTmuxConnection {
54408
54779
  "extended-keys-format",
54409
54780
  "csi-u"
54410
54781
  ]);
54411
- await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
54782
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "off"]);
54783
+ await this.runTmuxAllowFailure([
54784
+ "set-option",
54785
+ "-t",
54786
+ this.sessionName,
54787
+ "destroy-unattached",
54788
+ "off"
54789
+ ]);
54412
54790
  const termProgram = config.tmuxTermProgram.trim();
54413
54791
  if (termProgram && termProgram.toLowerCase() !== "off") {
54414
54792
  await this.runTmuxAllowFailure([
@@ -54437,71 +54815,167 @@ class SshExternalTmuxConnection {
54437
54815
  return false;
54438
54816
  }
54439
54817
  }
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)}`, {
54818
+ async createParkingWindow() {
54819
+ const result = await this.runTmuxAllowFailure([
54820
+ "new-window",
54821
+ "-t",
54822
+ this.sessionName,
54823
+ "-n",
54824
+ PARKING_WINDOW_NAME2,
54825
+ "-P",
54826
+ "-F",
54827
+ "#{window_id}",
54828
+ "sleep 30"
54829
+ ]);
54830
+ if (result.exitCode !== 0) {
54831
+ console.warn(`[ssh] failed to create parking window on ${this.deviceId}, attaching without focus shield`);
54832
+ return null;
54833
+ }
54834
+ return result.stdout.trim() || null;
54835
+ }
54836
+ async removeParkingWindow(windowId) {
54837
+ if (!windowId) {
54838
+ return;
54839
+ }
54840
+ await this.runTmuxAllowFailure(["last-window", "-t", this.sessionName]);
54841
+ await this.runTmuxAllowFailure(["kill-window", "-t", windowId]);
54842
+ }
54843
+ async startControlClient() {
54844
+ let attachReadyResolve = null;
54845
+ const attachReady = new Promise((resolve) => {
54846
+ attachReadyResolve = resolve;
54847
+ });
54848
+ const parkingWindowId = await this.createParkingWindow();
54849
+ let handle;
54850
+ try {
54851
+ handle = await this.openControlChannel(() => {
54852
+ attachReadyResolve?.();
54853
+ attachReadyResolve = null;
54854
+ });
54855
+ await Promise.race([
54856
+ attachReady,
54857
+ new Promise((resolve) => setTimeout(resolve, CONTROL_ATTACH_READY_TIMEOUT_MS2))
54858
+ ]);
54859
+ } finally {
54860
+ await this.removeParkingWindow(parkingWindowId);
54861
+ }
54862
+ if (this.controlChannel !== handle) {
54863
+ throw new Error(this.controlStderrTail.trim() || "tmux control client channel closed during attach");
54864
+ }
54865
+ }
54866
+ async openControlChannel(onAttachReady) {
54867
+ const subscription = createControlModeSubscription({
54868
+ onTerminalOutput: (paneId, data) => {
54869
+ this.callbacks.onTerminalOutput(paneId, data);
54870
+ },
54871
+ onTitle: (paneId, title) => {
54872
+ this.pendingPaneTitles.set(paneId, title);
54873
+ this.requestSnapshot();
54874
+ },
54875
+ onBell: (paneId) => {
54876
+ this.recordBell(paneId);
54877
+ },
54878
+ onNotification: (paneId, notification) => {
54879
+ this.emitNotification(paneId, notification);
54880
+ },
54881
+ onStructureChanged: () => {
54882
+ this.requestSnapshot();
54883
+ },
54884
+ onExit: () => {},
54885
+ onBlockEnd: () => {
54886
+ onAttachReady();
54887
+ }
54888
+ });
54889
+ const handle = { stop: () => {} };
54890
+ this.controlChannel = handle;
54891
+ this.controlSubscription = subscription;
54892
+ this.controlStartedAt = Date.now();
54893
+ this.controlStderrTail = "";
54894
+ const stopReader = await this.openReaderChannel(`exec ${quoteShellArg(this.tmuxBin)} -C attach-session -t ${quoteShellArg(this.sessionName)}`, {
54445
54895
  onData: (data) => {
54446
- this.handleHookChunk(data.toString());
54896
+ if (this.controlChannel === handle) {
54897
+ subscription.push(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
54898
+ }
54447
54899
  },
54448
- onClose: () => {
54449
- if (this.manualDisconnect) {
54450
- return;
54900
+ onStderr: (data) => {
54901
+ if (this.controlChannel === handle) {
54902
+ this.controlStderrTail = (this.controlStderrTail + data.toString()).slice(-CONTROL_STDERR_TAIL_LIMIT2);
54451
54903
  }
54452
- console.error("[ssh] hook reader channel closed unexpectedly, tearing down");
54453
- this.shutdownInternal(true);
54904
+ },
54905
+ onClose: () => {
54906
+ this.handleControlChannelClose(handle);
54454
54907
  }
54455
54908
  });
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
- ]);
54909
+ handle.stop = stopReader;
54910
+ return handle;
54484
54911
  }
54485
- handleHookChunk(text2) {
54486
- this.hookBuffer += text2;
54487
- while (true) {
54488
- const newlineIndex = this.hookBuffer.indexOf(`
54489
- `);
54490
- if (newlineIndex < 0) {
54912
+ stopControlClient() {
54913
+ const handle = this.controlChannel;
54914
+ this.controlChannel = null;
54915
+ this.controlSubscription?.dispose();
54916
+ this.controlSubscription = null;
54917
+ handle?.stop();
54918
+ }
54919
+ handleControlChannelClose(handle) {
54920
+ if (this.controlChannel !== handle) {
54921
+ return;
54922
+ }
54923
+ this.controlChannel = null;
54924
+ this.controlSubscription?.dispose();
54925
+ this.controlSubscription = null;
54926
+ if (!this.connected || this.manualDisconnect) {
54927
+ return;
54928
+ }
54929
+ this.reconnectControlClient();
54930
+ }
54931
+ async reconnectControlClient() {
54932
+ if (Date.now() - this.controlStartedAt > CONTROL_STABLE_RESET_MS2) {
54933
+ this.controlRestartCount = 0;
54934
+ }
54935
+ this.controlRestartCount += 1;
54936
+ const stderrMessage = this.controlStderrTail.trim();
54937
+ if (this.controlRestartCount > CONTROL_MAX_RESTARTS2) {
54938
+ const message = stderrMessage || "tmux control client channel closed repeatedly";
54939
+ console.warn(`[ssh] tmux control client gave up on ${this.deviceId}: ${message}`);
54940
+ updateDeviceRuntimeStatus(this.deviceId, {
54941
+ lastSeenAt: new Date().toISOString(),
54942
+ tmuxAvailable: false,
54943
+ lastError: message
54944
+ });
54945
+ this.shutdownInternal(true);
54946
+ return;
54947
+ }
54948
+ console.warn(`[ssh] tmux control client channel closed on ${this.deviceId}, reconnecting (attempt ${this.controlRestartCount})`);
54949
+ await new Promise((resolve) => setTimeout(resolve, CONTROL_RESTART_DELAY_MS2 * this.controlRestartCount));
54950
+ if (!this.connected || this.manualDisconnect) {
54951
+ return;
54952
+ }
54953
+ const probe = await this.runTmuxAllowFailure(["has-session", "-t", this.sessionName]);
54954
+ if (probe.exitCode !== 0) {
54955
+ const message = probe.stderr.trim() || probe.stdout.trim() || "tmux session gone";
54956
+ console.warn(`[ssh] tmux session gone on ${this.deviceId}: ${message}`);
54957
+ updateDeviceRuntimeStatus(this.deviceId, {
54958
+ lastSeenAt: new Date().toISOString(),
54959
+ tmuxAvailable: false,
54960
+ lastError: message
54961
+ });
54962
+ this.shutdownInternal(true);
54963
+ return;
54964
+ }
54965
+ if (!this.connected || this.manualDisconnect) {
54966
+ return;
54967
+ }
54968
+ try {
54969
+ await this.startControlClient();
54970
+ } catch (error) {
54971
+ console.warn(`[ssh] control client restart failed on ${this.deviceId}:`, error);
54972
+ return;
54973
+ }
54974
+ this.requestSnapshot();
54975
+ if (this.activePaneId) {
54976
+ this.capturePaneHistory(this.activePaneId).catch(() => {
54491
54977
  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
- }
54978
+ });
54505
54979
  }
54506
54980
  }
54507
54981
  async runAndRefresh(argv, allowTargetMissing = false) {
@@ -54598,7 +55072,7 @@ ${panesRes.stderr}`;
54598
55072
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
54599
55073
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
54600
55074
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
54601
- await this.syncPipeReaders();
55075
+ this.controlSubscription?.prunePanes(new Set(this.getExpectedPaneIds()));
54602
55076
  this.emitSnapshot();
54603
55077
  }
54604
55078
  parseSnapshotSession(lines) {
@@ -54724,104 +55198,6 @@ ${panesRes.stderr}`;
54724
55198
  getExpectedPaneIds() {
54725
55199
  return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
54726
55200
  }
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
55201
  async runTmux(argv, allowTargetMissing = false, timeoutMs = 1e4) {
54826
55202
  const result = await this.runTmuxAllowFailure(argv, timeoutMs);
54827
55203
  if (result.exitCode === 0) {
@@ -54949,6 +55325,10 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
54949
55325
  options.onData(data);
54950
55326
  });
54951
55327
  stream.stderr.on("data", (data) => {
55328
+ if (options.onStderr) {
55329
+ options.onStderr(data);
55330
+ return;
55331
+ }
54952
55332
  if (!this.manualDisconnect) {
54953
55333
  this.callbacks.onError(new Error(data.toString().trim() || "SSH reader stderr output"));
54954
55334
  }
@@ -54993,15 +55373,7 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
54993
55373
  }
54994
55374
  this.connected = false;
54995
55375
  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
- });
55376
+ this.stopControlClient();
55005
55377
  this.rejectPendingCommand(new Error("SSH command channel closed"));
55006
55378
  this.commandStream?.end();
55007
55379
  this.commandStream?.close();
@@ -57816,7 +58188,7 @@ async function serveFrontend(req, staticRoot) {
57816
58188
  if (!requestedPath) {
57817
58189
  return new Response(t3("runtime.forbidden"), { status: 403 });
57818
58190
  }
57819
- const indexPath = join5(staticRoot, "index.html");
58191
+ const indexPath = join4(staticRoot, "index.html");
57820
58192
  const targetPath = existsSync4(requestedPath) ? requestedPath : indexPath;
57821
58193
  if (!existsSync4(targetPath)) {
57822
58194
  return new Response(t3("runtime.frontendMissing"), { status: 500 });