tmex-cli 0.8.2 → 0.9.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.
package/dist/cli-node.js CHANGED
@@ -556,6 +556,7 @@ After=network.target
556
556
 
557
557
  [Service]
558
558
  Type=simple
559
+ KillMode=process
559
560
  WorkingDirectory=${escapedInstallDir}
560
561
  SyslogIdentifier=tmex
561
562
  StandardOutput=journal
@@ -595,6 +596,8 @@ function buildLaunchdPlist({
595
596
  <true/>
596
597
  <key>KeepAlive</key>
597
598
  <true/>
599
+ <key>AbandonProcessGroup</key>
600
+ <true/>
598
601
  <key>StandardOutPath</key>
599
602
  <string>${escapeXml(join3(installDir, "tmex.log"))}</string>
600
603
  <key>StandardErrorPath</key>
@@ -32413,10 +32413,16 @@ var telegramBotChats = sqliteTable("telegram_bot_chats", {
32413
32413
  // ../../apps/gateway/src/db/client.ts
32414
32414
  var sqliteClient = null;
32415
32415
  var db = null;
32416
+ function applyPragmas(database) {
32417
+ database.run("PRAGMA foreign_keys = ON");
32418
+ database.run("PRAGMA journal_mode = WAL");
32419
+ database.run("PRAGMA busy_timeout = 5000");
32420
+ database.run("PRAGMA synchronous = NORMAL");
32421
+ }
32416
32422
  function ensureSqliteClient() {
32417
32423
  if (!sqliteClient) {
32418
32424
  sqliteClient = new Database2(config.databaseUrl);
32419
- sqliteClient.run("PRAGMA foreign_keys = ON");
32425
+ applyPragmas(sqliteClient);
32420
32426
  }
32421
32427
  return sqliteClient;
32422
32428
  }
@@ -95733,6 +95739,9 @@ var TMEX_INJECTED_ENV_EXACT = new Set(["NODE_ENV", "DATABASE_URL", "GATEWAY_PORT
95733
95739
  function isTmexInjectedEnvKey(key) {
95734
95740
  return key.startsWith("TMEX_") || TMEX_INJECTED_ENV_EXACT.has(key);
95735
95741
  }
95742
+ function isUtf8Locale(value) {
95743
+ return typeof value === "string" && /utf-?8/i.test(value);
95744
+ }
95736
95745
  function buildLocalTmuxEnv(resolvedPath, baseEnv = process.env) {
95737
95746
  const nextEnv = {};
95738
95747
  for (const [key, value] of Object.entries(baseEnv)) {
@@ -95744,8 +95753,8 @@ function buildLocalTmuxEnv(resolvedPath, baseEnv = process.env) {
95744
95753
  if (resolvedPath) {
95745
95754
  nextEnv.PATH = resolvedPath;
95746
95755
  }
95747
- if (!nextEnv.LC_CTYPE && !nextEnv.LC_ALL && !nextEnv.LANG) {
95748
- nextEnv.LC_CTYPE = "en_US.UTF-8";
95756
+ if (!isUtf8Locale(nextEnv.LC_ALL) && (nextEnv.LC_ALL || !isUtf8Locale(nextEnv.LC_CTYPE) && !isUtf8Locale(nextEnv.LANG))) {
95757
+ nextEnv.LC_ALL = "C.UTF-8";
95749
95758
  }
95750
95759
  return nextEnv;
95751
95760
  }
@@ -96625,6 +96634,76 @@ function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
96625
96634
  return chunks;
96626
96635
  }
96627
96636
 
96637
+ // ../../apps/gateway/src/tmux-client/snapshot-format.ts
96638
+ var SNAPSHOT_FIELD_SEPARATOR = "|";
96639
+ var TMUX_SESSION_ID_PATTERN = /^\$\d+$/;
96640
+ var TMUX_WINDOW_ID_PATTERN = /^@\d+$/;
96641
+ var TMUX_PANE_ID_PATTERN = /^%\d+$/;
96642
+ function isTmuxSessionId(value) {
96643
+ return typeof value === "string" && TMUX_SESSION_ID_PATTERN.test(value);
96644
+ }
96645
+ function isTmuxWindowId(value) {
96646
+ return typeof value === "string" && TMUX_WINDOW_ID_PATTERN.test(value);
96647
+ }
96648
+ function isTmuxPaneId(value) {
96649
+ return typeof value === "string" && TMUX_PANE_ID_PATTERN.test(value);
96650
+ }
96651
+ function parseSnapshotInteger(value) {
96652
+ if (typeof value !== "string" || !/^\d+$/.test(value)) {
96653
+ return null;
96654
+ }
96655
+ return Number.parseInt(value, 10);
96656
+ }
96657
+ function formatSnapshotRowForLog(line, limit = 160) {
96658
+ if (line.length <= limit) {
96659
+ return line;
96660
+ }
96661
+ return `${line.slice(0, Math.max(0, limit - 3))}...`;
96662
+ }
96663
+ function splitSnapshotFields(line, fieldCount) {
96664
+ const parts = line.split(SNAPSHOT_FIELD_SEPARATOR);
96665
+ if (parts.length <= fieldCount) {
96666
+ return parts;
96667
+ }
96668
+ if (fieldCount === 2) {
96669
+ return [parts[0] ?? "", parts.slice(1).join(SNAPSHOT_FIELD_SEPARATOR)];
96670
+ }
96671
+ if (fieldCount === 4) {
96672
+ return [
96673
+ parts[0] ?? "",
96674
+ parts[1] ?? "",
96675
+ parts.slice(2, -1).join(SNAPSHOT_FIELD_SEPARATOR),
96676
+ parts.at(-1) ?? ""
96677
+ ];
96678
+ }
96679
+ if (fieldCount === 8) {
96680
+ return [
96681
+ parts[0] ?? "",
96682
+ parts[1] ?? "",
96683
+ parts[2] ?? "",
96684
+ parts.slice(3, -4).join(SNAPSHOT_FIELD_SEPARATOR),
96685
+ parts.at(-4) ?? "",
96686
+ parts.at(-3) ?? "",
96687
+ parts.at(-2) ?? "",
96688
+ parts.at(-1) ?? ""
96689
+ ];
96690
+ }
96691
+ if (fieldCount === 9) {
96692
+ return [
96693
+ parts[0] ?? "",
96694
+ parts[1] ?? "",
96695
+ parts[2] ?? "",
96696
+ parts.slice(3, -5).join(SNAPSHOT_FIELD_SEPARATOR),
96697
+ parts.at(-5) ?? "",
96698
+ parts.at(-4) ?? "",
96699
+ parts.at(-3) ?? "",
96700
+ parts.at(-2) ?? "",
96701
+ parts.at(-1) ?? ""
96702
+ ];
96703
+ }
96704
+ return parts;
96705
+ }
96706
+
96628
96707
  // ../../apps/gateway/src/tmux-client/target-missing.ts
96629
96708
  class TmuxTargetMissingError extends Error {
96630
96709
  constructor(message) {
@@ -96848,7 +96927,7 @@ class LocalExternalTmuxConnection {
96848
96927
  if (!this.connected) {
96849
96928
  return;
96850
96929
  }
96851
- this.runAndRefresh(["select-window", "-t", windowId]).catch((error51) => {
96930
+ this.runAndRefresh(["select-window", "-t", windowId], true).catch((error51) => {
96852
96931
  this.callbacks.onError(error51);
96853
96932
  });
96854
96933
  }
@@ -97272,14 +97351,14 @@ class LocalExternalTmuxConnection {
97272
97351
  "-p",
97273
97352
  "-t",
97274
97353
  this.sessionName,
97275
- "#{session_id}\t#{session_name}"
97354
+ ["#{session_id}", "#{session_name}"].join(SNAPSHOT_FIELD_SEPARATOR)
97276
97355
  ]),
97277
97356
  this.runTmuxAllowFailure([
97278
97357
  "list-windows",
97279
97358
  "-t",
97280
97359
  this.sessionName,
97281
97360
  "-F",
97282
- "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}"
97361
+ ["#{window_id}", "#{window_index}", "#{window_name}", "#{window_active}"].join(SNAPSHOT_FIELD_SEPARATOR)
97283
97362
  ]),
97284
97363
  this.runTmuxAllowFailure([
97285
97364
  "list-panes",
@@ -97287,7 +97366,17 @@ class LocalExternalTmuxConnection {
97287
97366
  "-t",
97288
97367
  this.sessionName,
97289
97368
  "-F",
97290
- "#{pane_id}\t#{window_id}\t#{pane_index}\t#{pane_title}\t#{pane_active}\t#{pane_width}\t#{pane_height}\t#{window_active}\t#{pane_current_command}"
97369
+ [
97370
+ "#{pane_id}",
97371
+ "#{window_id}",
97372
+ "#{pane_index}",
97373
+ "#{pane_title}",
97374
+ "#{pane_active}",
97375
+ "#{pane_width}",
97376
+ "#{pane_height}",
97377
+ "#{window_active}",
97378
+ "#{pane_current_command}"
97379
+ ].join(SNAPSHOT_FIELD_SEPARATOR)
97291
97380
  ])
97292
97381
  ]);
97293
97382
  if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
@@ -97311,6 +97400,7 @@ ${panesRes.stderr}`;
97311
97400
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
97312
97401
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
97313
97402
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
97403
+ this.discardInvalidSnapshot();
97314
97404
  this.controlSubscription?.prunePanes(new Set(this.getExpectedPaneIds()));
97315
97405
  this.emitSnapshot();
97316
97406
  }
@@ -97320,9 +97410,11 @@ ${panesRes.stderr}`;
97320
97410
  if (!line.trim()) {
97321
97411
  continue;
97322
97412
  }
97323
- const [id, name24] = line.split("\t");
97324
- if (id) {
97413
+ const [id, name24] = splitSnapshotFields(line, 2);
97414
+ if (isTmuxSessionId(id)) {
97325
97415
  this.snapshotSession = { id, name: name24 ?? "" };
97416
+ } else {
97417
+ console.warn(`[local] ignoring invalid tmux session id on ${this.deviceId}: ${id ?? ""}`);
97326
97418
  }
97327
97419
  return;
97328
97420
  }
@@ -97333,18 +97425,19 @@ ${panesRes.stderr}`;
97333
97425
  if (!line.trim()) {
97334
97426
  continue;
97335
97427
  }
97336
- const [id, indexRaw, name24, activeRaw] = line.split("\t");
97337
- if (!id) {
97428
+ const [id, indexRaw, name24, activeRaw] = splitSnapshotFields(line, 4);
97429
+ const index = parseSnapshotInteger(indexRaw);
97430
+ if (!isTmuxWindowId(id) || index === null || !this.isSnapshotFlag(activeRaw)) {
97431
+ console.warn(`[local] ignoring invalid tmux window snapshot row on ${this.deviceId}: ${formatSnapshotRowForLog(line)}`);
97338
97432
  continue;
97339
97433
  }
97340
- const index = Number.parseInt(indexRaw ?? "", 10);
97341
97434
  const active = activeRaw === "1";
97342
97435
  if (active) {
97343
97436
  this.activeWindowId = id;
97344
97437
  }
97345
97438
  this.snapshotWindows.set(id, {
97346
97439
  id,
97347
- index: Number.isNaN(index) ? 0 : index,
97440
+ index,
97348
97441
  name: name24 ?? "",
97349
97442
  active,
97350
97443
  panes: []
@@ -97359,22 +97452,23 @@ ${panesRes.stderr}`;
97359
97452
  if (!line.trim()) {
97360
97453
  continue;
97361
97454
  }
97362
- const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw, windowActiveRaw, currentCommandRaw] = line.split("\t");
97363
- if (!paneId || !windowId) {
97455
+ const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw, windowActiveRaw, currentCommandRaw] = splitSnapshotFields(line, 9);
97456
+ const index = parseSnapshotInteger(indexRaw);
97457
+ const width = parseSnapshotInteger(widthRaw);
97458
+ const height = parseSnapshotInteger(heightRaw);
97459
+ if (!isTmuxPaneId(paneId) || !isTmuxWindowId(windowId) || index === null || width === null || height === null || !this.isSnapshotFlag(activeRaw) || !this.isSnapshotFlag(windowActiveRaw)) {
97460
+ console.warn(`[local] ignoring invalid tmux pane snapshot row on ${this.deviceId}: ${formatSnapshotRowForLog(line)}`);
97364
97461
  continue;
97365
97462
  }
97366
- const index = Number.parseInt(indexRaw ?? "", 10);
97367
- const width = Number.parseInt(widthRaw ?? "", 10);
97368
- const height = Number.parseInt(heightRaw ?? "", 10);
97369
97463
  const pane = {
97370
97464
  id: paneId,
97371
97465
  windowId,
97372
- index: Number.isNaN(index) ? 0 : index,
97466
+ index,
97373
97467
  title: this.pendingPaneTitles.get(paneId) ?? (titleRaw?.trim() ? titleRaw : undefined),
97374
97468
  currentCommand: currentCommandRaw?.trim() ? currentCommandRaw.trim() : undefined,
97375
97469
  active: activeRaw === "1",
97376
- width: Number.isNaN(width) ? 0 : width,
97377
- height: Number.isNaN(height) ? 0 : height
97470
+ width,
97471
+ height
97378
97472
  };
97379
97473
  if (pane.active && windowActiveRaw === "1") {
97380
97474
  this.activePaneId = paneId;
@@ -97391,6 +97485,23 @@ ${panesRes.stderr}`;
97391
97485
  window2.panes.sort((left, right) => left.index - right.index);
97392
97486
  }
97393
97487
  }
97488
+ isSnapshotFlag(value) {
97489
+ return value === "0" || value === "1";
97490
+ }
97491
+ discardInvalidSnapshot() {
97492
+ if (!this.snapshotSession) {
97493
+ this.snapshotWindows.clear();
97494
+ this.activeWindowId = null;
97495
+ this.activePaneId = null;
97496
+ return;
97497
+ }
97498
+ if (this.snapshotWindows.size === 0) {
97499
+ console.warn(`[local] ignoring tmux snapshot with no valid windows on ${this.deviceId}`);
97500
+ this.snapshotSession = null;
97501
+ this.activeWindowId = null;
97502
+ this.activePaneId = null;
97503
+ }
97504
+ }
97394
97505
  emitSnapshot() {
97395
97506
  const session = this.snapshotSession ? {
97396
97507
  id: this.snapshotSession.id,
@@ -97451,6 +97562,7 @@ ${panesRes.stderr}`;
97451
97562
  this.recoverFromTargetMissingError(message);
97452
97563
  return result;
97453
97564
  }
97565
+ console.warn(`[local] tmux command failed deviceId=${this.deviceId} sessionName=${this.sessionName} argv=${argv.join(" ")} exitCode=${result.exitCode}: ${message}`);
97454
97566
  this.notifyRuntimeError(message);
97455
97567
  if (this.connected && !this.manualDisconnect && this.isTmuxServerGoneMessage(message)) {
97456
97568
  console.warn(`[local] tmux server gone on ${this.deviceId}: ${message}`);
@@ -97862,51 +97974,12 @@ function hasRenderableTerminalContent2(value) {
97862
97974
  }
97863
97975
  var BELL_DEDUP_WINDOW_MS2 = 200;
97864
97976
  var COMMAND_SENTINEL = "\x1ETMEX_END ";
97865
- var SNAPSHOT_FIELD_SEPARATOR = "|";
97866
97977
  var CONTROL_MAX_RESTARTS2 = 3;
97867
97978
  var CONTROL_RESTART_DELAY_MS2 = 500;
97868
97979
  var CONTROL_STABLE_RESET_MS2 = 1e4;
97869
97980
  var CONTROL_STDERR_TAIL_LIMIT2 = 2048;
97870
97981
  var CONTROL_ATTACH_READY_TIMEOUT_MS2 = 3000;
97871
97982
  var PARKING_WINDOW_NAME2 = "tmex-park";
97872
- function splitSnapshotFields(line, fieldCount) {
97873
- const parts = line.split(SNAPSHOT_FIELD_SEPARATOR);
97874
- if (parts.length <= fieldCount) {
97875
- return parts;
97876
- }
97877
- if (fieldCount === 2) {
97878
- return [parts[0] ?? "", parts.slice(1).join(SNAPSHOT_FIELD_SEPARATOR)];
97879
- }
97880
- if (fieldCount === 4) {
97881
- return [parts[0] ?? "", parts[1] ?? "", parts.slice(2, -1).join(SNAPSHOT_FIELD_SEPARATOR), parts.at(-1) ?? ""];
97882
- }
97883
- if (fieldCount === 8) {
97884
- return [
97885
- parts[0] ?? "",
97886
- parts[1] ?? "",
97887
- parts[2] ?? "",
97888
- parts.slice(3, -4).join(SNAPSHOT_FIELD_SEPARATOR),
97889
- parts.at(-4) ?? "",
97890
- parts.at(-3) ?? "",
97891
- parts.at(-2) ?? "",
97892
- parts.at(-1) ?? ""
97893
- ];
97894
- }
97895
- if (fieldCount === 9) {
97896
- return [
97897
- parts[0] ?? "",
97898
- parts[1] ?? "",
97899
- parts[2] ?? "",
97900
- parts.slice(3, -5).join(SNAPSHOT_FIELD_SEPARATOR),
97901
- parts.at(-5) ?? "",
97902
- parts.at(-4) ?? "",
97903
- parts.at(-3) ?? "",
97904
- parts.at(-2) ?? "",
97905
- parts.at(-1) ?? ""
97906
- ];
97907
- }
97908
- return parts;
97909
- }
97910
97983
 
97911
97984
  class SshExternalTmuxConnection {
97912
97985
  deviceId;
@@ -98018,7 +98091,7 @@ class SshExternalTmuxConnection {
98018
98091
  if (!this.connected) {
98019
98092
  return;
98020
98093
  }
98021
- this.runAndRefresh(["select-window", "-t", windowId]).catch((error51) => {
98094
+ this.runAndRefresh(["select-window", "-t", windowId], true).catch((error51) => {
98022
98095
  this.callbacks.onError(error51);
98023
98096
  });
98024
98097
  }
@@ -98558,6 +98631,7 @@ ${panesRes.stderr}`;
98558
98631
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
98559
98632
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
98560
98633
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
98634
+ this.discardInvalidSnapshot();
98561
98635
  this.controlSubscription?.prunePanes(new Set(this.getExpectedPaneIds()));
98562
98636
  this.emitSnapshot();
98563
98637
  }
@@ -98568,8 +98642,10 @@ ${panesRes.stderr}`;
98568
98642
  continue;
98569
98643
  }
98570
98644
  const [id, name24] = splitSnapshotFields(line, 2);
98571
- if (id) {
98645
+ if (isTmuxSessionId(id)) {
98572
98646
  this.snapshotSession = { id, name: name24 ?? "" };
98647
+ } else {
98648
+ console.warn(`[ssh] ignoring invalid tmux session id on ${this.deviceId}: ${id ?? ""}`);
98573
98649
  }
98574
98650
  return;
98575
98651
  }
@@ -98581,17 +98657,18 @@ ${panesRes.stderr}`;
98581
98657
  continue;
98582
98658
  }
98583
98659
  const [id, indexRaw, name24, activeRaw] = splitSnapshotFields(line, 4);
98584
- if (!id) {
98660
+ const index = parseSnapshotInteger(indexRaw);
98661
+ if (!isTmuxWindowId(id) || index === null || !this.isSnapshotFlag(activeRaw)) {
98662
+ console.warn(`[ssh] ignoring invalid tmux window snapshot row on ${this.deviceId}: ${formatSnapshotRowForLog(line)}`);
98585
98663
  continue;
98586
98664
  }
98587
- const index = Number.parseInt(indexRaw ?? "", 10);
98588
98665
  const active = activeRaw === "1";
98589
98666
  if (active) {
98590
98667
  this.activeWindowId = id;
98591
98668
  }
98592
98669
  this.snapshotWindows.set(id, {
98593
98670
  id,
98594
- index: Number.isNaN(index) ? 0 : index,
98671
+ index,
98595
98672
  name: name24 ?? "",
98596
98673
  active,
98597
98674
  panes: []
@@ -98607,21 +98684,22 @@ ${panesRes.stderr}`;
98607
98684
  continue;
98608
98685
  }
98609
98686
  const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw, windowActiveRaw, currentCommandRaw] = splitSnapshotFields(line, 9);
98610
- if (!paneId || !windowId) {
98687
+ const index = parseSnapshotInteger(indexRaw);
98688
+ const width = parseSnapshotInteger(widthRaw);
98689
+ const height = parseSnapshotInteger(heightRaw);
98690
+ if (!isTmuxPaneId(paneId) || !isTmuxWindowId(windowId) || index === null || width === null || height === null || !this.isSnapshotFlag(activeRaw) || !this.isSnapshotFlag(windowActiveRaw)) {
98691
+ console.warn(`[ssh] ignoring invalid tmux pane snapshot row on ${this.deviceId}: ${formatSnapshotRowForLog(line)}`);
98611
98692
  continue;
98612
98693
  }
98613
- const index = Number.parseInt(indexRaw ?? "", 10);
98614
- const width = Number.parseInt(widthRaw ?? "", 10);
98615
- const height = Number.parseInt(heightRaw ?? "", 10);
98616
98694
  const pane = {
98617
98695
  id: paneId,
98618
98696
  windowId,
98619
- index: Number.isNaN(index) ? 0 : index,
98697
+ index,
98620
98698
  title: this.pendingPaneTitles.get(paneId) ?? (titleRaw?.trim() ? titleRaw : undefined),
98621
98699
  currentCommand: currentCommandRaw?.trim() ? currentCommandRaw.trim() : undefined,
98622
98700
  active: activeRaw === "1",
98623
- width: Number.isNaN(width) ? 0 : width,
98624
- height: Number.isNaN(height) ? 0 : height
98701
+ width,
98702
+ height
98625
98703
  };
98626
98704
  if (pane.active && windowActiveRaw === "1") {
98627
98705
  this.activePaneId = paneId;
@@ -98638,6 +98716,23 @@ ${panesRes.stderr}`;
98638
98716
  window2.panes.sort((left, right) => left.index - right.index);
98639
98717
  }
98640
98718
  }
98719
+ isSnapshotFlag(value) {
98720
+ return value === "0" || value === "1";
98721
+ }
98722
+ discardInvalidSnapshot() {
98723
+ if (!this.snapshotSession) {
98724
+ this.snapshotWindows.clear();
98725
+ this.activeWindowId = null;
98726
+ this.activePaneId = null;
98727
+ return;
98728
+ }
98729
+ if (this.snapshotWindows.size === 0) {
98730
+ console.warn(`[ssh] ignoring tmux snapshot with no valid windows on ${this.deviceId}`);
98731
+ this.snapshotSession = null;
98732
+ this.activeWindowId = null;
98733
+ this.activePaneId = null;
98734
+ }
98735
+ }
98641
98736
  emitSnapshot() {
98642
98737
  const session = this.snapshotSession ? {
98643
98738
  id: this.snapshotSession.id,
@@ -98698,6 +98793,7 @@ ${panesRes.stderr}`;
98698
98793
  this.recoverFromTargetMissingError(message);
98699
98794
  return result;
98700
98795
  }
98796
+ console.warn(`[ssh] tmux command failed deviceId=${this.deviceId} sessionName=${this.sessionName} argv=${argv.join(" ")} exitCode=${result.exitCode}: ${message}`);
98701
98797
  updateDeviceRuntimeStatus(this.deviceId, {
98702
98798
  lastSeenAt: new Date().toISOString(),
98703
98799
  tmuxAvailable: false,
@@ -105067,6 +105163,7 @@ class SwitchBarrier {
105067
105163
  console.warn(`[switch-barrier] Transaction timeout at stage: ${stage} for ${deviceId}`);
105068
105164
  if (stage === "history") {
105069
105165
  this.sendLiveResume(ws, deviceId, expectedToken);
105166
+ sessionStateStore.stopOutputBuffering(ws, deviceId);
105070
105167
  pending.callbacks.onTimeout?.(stage);
105071
105168
  return;
105072
105169
  }
@@ -105566,17 +105663,15 @@ class WebSocketServer {
105566
105663
  }
105567
105664
  handleTmuxSelect(ws, data) {
105568
105665
  const deviceId = data.deviceId;
105569
- const paneId = data.paneId ?? undefined;
105570
- if (paneId) {
105571
- ws.data.borshState.selectedPanes[deviceId] = paneId;
105572
- this.refreshSnapshotPolling(deviceId);
105573
- }
105574
105666
  const entry = this.connections.get(deviceId);
105575
105667
  if (!entry)
105576
105668
  return;
105577
105669
  const windowId = data.windowId ?? undefined;
105670
+ const paneId = data.paneId ?? undefined;
105578
105671
  if (!windowId || !paneId)
105579
105672
  return;
105673
+ if (!this.canSelectPane(entry, deviceId, windowId, paneId))
105674
+ return;
105580
105675
  const started = switchBarrier.startTransaction(ws, {
105581
105676
  deviceId,
105582
105677
  windowId,
@@ -105590,6 +105685,8 @@ class WebSocketServer {
105590
105685
  this.sendError(ws, null, exports_ws_borsh.ERROR_SELECT_CONFLICT, "Failed to start select transaction", false);
105591
105686
  return;
105592
105687
  }
105688
+ ws.data.borshState.selectedPanes[deviceId] = paneId;
105689
+ this.refreshSnapshotPolling(deviceId);
105593
105690
  switchBarrier.sendSwitchAck(ws, deviceId);
105594
105691
  const cols = data.cols ?? null;
105595
105692
  const rows = data.rows ?? null;
@@ -105603,8 +105700,41 @@ class WebSocketServer {
105603
105700
  const entry = this.connections.get(deviceId);
105604
105701
  if (!entry)
105605
105702
  return;
105703
+ if (!this.canSelectWindow(entry, deviceId, windowId))
105704
+ return;
105606
105705
  entry.runtime.selectWindow(windowId);
105607
105706
  }
105707
+ canSelectWindow(entry, deviceId, windowId) {
105708
+ if (!isTmuxWindowId(windowId)) {
105709
+ console.warn(`[ws] rejecting invalid tmux window id on ${deviceId}: ${windowId ?? ""}`);
105710
+ entry.runtime.requestSnapshot();
105711
+ return false;
105712
+ }
105713
+ const windows = entry.lastSnapshot?.session?.windows;
105714
+ if (!windows?.some((window2) => window2.id === windowId)) {
105715
+ console.warn(`[ws] rejecting missing tmux window id on ${deviceId}: ${windowId}`);
105716
+ entry.runtime.requestSnapshot();
105717
+ return false;
105718
+ }
105719
+ return true;
105720
+ }
105721
+ canSelectPane(entry, deviceId, windowId, paneId) {
105722
+ if (!this.canSelectWindow(entry, deviceId, windowId)) {
105723
+ return false;
105724
+ }
105725
+ if (!isTmuxPaneId(paneId)) {
105726
+ console.warn(`[ws] rejecting invalid tmux pane id on ${deviceId}: ${paneId ?? ""}`);
105727
+ entry.runtime.requestSnapshot();
105728
+ return false;
105729
+ }
105730
+ const window2 = entry.lastSnapshot?.session?.windows.find((candidate) => candidate.id === windowId);
105731
+ if (!window2?.panes.some((pane) => pane.id === paneId)) {
105732
+ console.warn(`[ws] rejecting missing tmux pane id on ${deviceId}: ${paneId}`);
105733
+ entry.runtime.requestSnapshot();
105734
+ return false;
105735
+ }
105736
+ return true;
105737
+ }
105608
105738
  handleTermInput(deviceId, paneId, data) {
105609
105739
  const entry = this.connections.get(deviceId);
105610
105740
  if (!entry)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmex-cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tmex": "./bin/tmex.js",