tmex-cli 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/runtime/server.js +1163 -405
  2. package/package.json +1 -1
  3. package/resources/fe-dist/assets/DevicePage-n4JoyDed.js +26 -0
  4. package/resources/fe-dist/assets/DevicePage-n4JoyDed.js.map +1 -0
  5. package/resources/fe-dist/assets/{DevicesPage-C76Xejy5.js → DevicesPage-BwLKaiUR.js} +2 -2
  6. package/resources/fe-dist/assets/{DevicesPage-C76Xejy5.js.map → DevicesPage-BwLKaiUR.js.map} +1 -1
  7. package/resources/fe-dist/assets/SettingsPage-hS99lHcp.js +17 -0
  8. package/resources/fe-dist/assets/SettingsPage-hS99lHcp.js.map +1 -0
  9. package/resources/fe-dist/assets/index-CJaX5rlK.css +1 -0
  10. package/resources/fe-dist/assets/{index-Bmahx5fj.js → index-CJyFlAt8.js} +120 -119
  11. package/resources/fe-dist/assets/index-CJyFlAt8.js.map +1 -0
  12. package/resources/fe-dist/assets/{select-Wn7lKWHQ.js → select-DGBwxGiK.js} +2 -2
  13. package/resources/fe-dist/assets/{select-Wn7lKWHQ.js.map → select-DGBwxGiK.js.map} +1 -1
  14. package/resources/fe-dist/assets/{switch-JVIhfemP.js → switch-CWUBjs7N.js} +2 -2
  15. package/resources/fe-dist/assets/{switch-JVIhfemP.js.map → switch-CWUBjs7N.js.map} +1 -1
  16. package/resources/fe-dist/assets/{useValueChanged-DU---PIl.js → useValueChanged-DwJ_SDCu.js} +2 -2
  17. package/resources/fe-dist/assets/{useValueChanged-DU---PIl.js.map → useValueChanged-DwJ_SDCu.js.map} +1 -1
  18. package/resources/fe-dist/index.html +2 -2
  19. package/resources/gateway-drizzle/0002_broad_vengeance.sql +3 -0
  20. package/resources/gateway-drizzle/meta/0002_snapshot.json +535 -0
  21. package/resources/gateway-drizzle/meta/_journal.json +7 -0
  22. package/resources/fe-dist/assets/DevicePage-BTbDSWYN.js +0 -26
  23. package/resources/fe-dist/assets/DevicePage-BTbDSWYN.js.map +0 -1
  24. package/resources/fe-dist/assets/SettingsPage-DQ9W4fOo.js +0 -17
  25. package/resources/fe-dist/assets/SettingsPage-DQ9W4fOo.js.map +0 -1
  26. package/resources/fe-dist/assets/index-Bmahx5fj.js.map +0 -1
  27. package/resources/fe-dist/assets/index-CyKyNcdz.css +0 -1
@@ -20281,8 +20281,8 @@ var require_lib3 = __commonJS((exports, module) => {
20281
20281
  });
20282
20282
 
20283
20283
  // src/runtime/server.ts
20284
- import { existsSync as existsSync3 } from "fs";
20285
- import { extname, join as join4, normalize, resolve as resolve2, sep } from "path";
20284
+ import { existsSync as existsSync4 } from "fs";
20285
+ import { extname, join as join5, normalize, resolve as resolve2, sep } from "path";
20286
20286
 
20287
20287
  // ../../apps/gateway/src/crypto/errors.ts
20288
20288
  function contextLabel(context) {
@@ -20446,8 +20446,11 @@ var I18N_RESOURCES = {
20446
20446
  siteUrl: "Site URL",
20447
20447
  siteUrlPlaceholder: "http://localhost:3000",
20448
20448
  bellThrottle: "Bell Throttle (seconds)",
20449
+ notificationThrottle: "Notification Throttle (seconds)",
20449
20450
  enableBrowserBellToast: "Enable Browser Bell Toast",
20451
+ enableBrowserNotificationToast: "Enable Browser Notification Toast",
20450
20452
  enableTelegramBellPush: "Enable Telegram Bell Push",
20453
+ enableTelegramNotificationPush: "Enable Telegram Notification Push",
20451
20454
  sshReconnectRetries: "SSH Reconnect Retries",
20452
20455
  sshReconnectDelay: "SSH Reconnect Delay (seconds)",
20453
20456
  language: "Language",
@@ -20591,6 +20594,7 @@ Time: {{time}}`,
20591
20594
  clickToJump: "Click to jump to corresponding pane",
20592
20595
  eventType: {
20593
20596
  terminal_bell: "\uD83D\uDD14 Terminal Bell",
20597
+ terminal_notification: "\uD83D\uDD14 Terminal Notification",
20594
20598
  tmux_window_close: "\uD83E\uDE9F Window Closed",
20595
20599
  tmux_pane_close: "\uD83D\uDCF1 Pane Closed",
20596
20600
  device_tmux_missing: "\u26A0\uFE0F Tmux Missing",
@@ -20609,7 +20613,8 @@ Time: {{time}}`,
20609
20613
  title: "\uD83D\uDD14 Bell from {{siteName}}: {{terminalTopbarLabel}}",
20610
20614
  viewLink: "Click to view",
20611
20615
  terminalTopbarLabel: "Window {{window}} \xB7 Pane {{pane}} @ {{device}}"
20612
- }
20616
+ },
20617
+ telegramNotification: {}
20613
20618
  },
20614
20619
  sidebar: {
20615
20620
  noWindows: "No windows",
@@ -20773,8 +20778,11 @@ Time: {{time}}`,
20773
20778
  siteUrl: "\u7AD9\u70B9\u8BBF\u95EE URL",
20774
20779
  siteUrlPlaceholder: "http://localhost:3000",
20775
20780
  bellThrottle: "Bell \u9891\u63A7\uFF08\u79D2\uFF09",
20781
+ notificationThrottle: "\u901A\u77E5\u9891\u63A7\uFF08\u79D2\uFF09",
20776
20782
  enableBrowserBellToast: "\u5F00\u542F\u6D4F\u89C8\u5668 Bell Toast",
20783
+ enableBrowserNotificationToast: "\u5F00\u542F\u6D4F\u89C8\u5668\u901A\u77E5 Toast",
20777
20784
  enableTelegramBellPush: "\u5F00\u542F Telegram Bell \u63A8\u9001",
20785
+ enableTelegramNotificationPush: "\u5F00\u542F Telegram \u901A\u77E5\u63A8\u9001",
20778
20786
  sshReconnectRetries: "SSH \u91CD\u8FDE\u6B21\u6570",
20779
20787
  sshReconnectDelay: "SSH \u91CD\u8FDE\u7B49\u5F85\uFF08\u79D2\uFF09",
20780
20788
  language: "\u8BED\u8A00",
@@ -20918,6 +20926,7 @@ Bot\uFF1A{{botName}}
20918
20926
  clickToJump: "\u70B9\u51FB\u8DF3\u8F6C\u5230\u5BF9\u5E94 Pane",
20919
20927
  eventType: {
20920
20928
  terminal_bell: "\uD83D\uDD14 \u7EC8\u7AEF Bell",
20929
+ terminal_notification: "\uD83D\uDD14 \u7EC8\u7AEF\u901A\u77E5",
20921
20930
  tmux_window_close: "\uD83E\uDE9F \u7A97\u53E3\u5173\u95ED",
20922
20931
  tmux_pane_close: "\uD83D\uDCF1 Pane \u5173\u95ED",
20923
20932
  device_tmux_missing: "\u26A0\uFE0F Tmux \u4E0D\u53EF\u7528",
@@ -20936,7 +20945,8 @@ Bot\uFF1A{{botName}}
20936
20945
  title: "\uD83D\uDD14 \u6765\u81EA {{siteName}} \u7684 Bell\uFF1A{{terminalTopbarLabel}}",
20937
20946
  viewLink: "\u70B9\u51FB\u67E5\u770B",
20938
20947
  terminalTopbarLabel: "\u7A97\u53E3 {{window}} \xB7 Pane {{pane}} @ {{device}}"
20939
- }
20948
+ },
20949
+ telegramNotification: {}
20940
20950
  },
20941
20951
  sidebar: {
20942
20952
  noWindows: "\u6682\u65E0\u7A97\u53E3",
@@ -21100,8 +21110,11 @@ Bot\uFF1A{{botName}}
21100
21110
  siteUrl: "\u30B5\u30A4\u30C8 URL",
21101
21111
  siteUrlPlaceholder: "http://localhost:3000",
21102
21112
  bellThrottle: "\u30D9\u30EB\u5236\u9650\uFF08\u79D2\uFF09",
21113
+ notificationThrottle: "\u901A\u77E5\u5236\u9650\uFF08\u79D2\uFF09",
21103
21114
  enableBrowserBellToast: "\u30D6\u30E9\u30A6\u30B6\u30D9\u30EB Toast \u3092\u6709\u52B9\u306B\u3059\u308B",
21115
+ enableBrowserNotificationToast: "\u30D6\u30E9\u30A6\u30B6\u901A\u77E5 Toast \u3092\u6709\u52B9\u306B\u3059\u308B",
21104
21116
  enableTelegramBellPush: "Telegram \u30D9\u30EB\u30D7\u30C3\u30B7\u30E5\u3092\u6709\u52B9\u306B\u3059\u308B",
21117
+ enableTelegramNotificationPush: "Telegram \u901A\u77E5\u30D7\u30C3\u30B7\u30E5\u3092\u6709\u52B9\u306B\u3059\u308B",
21105
21118
  sshReconnectRetries: "SSH \u518D\u63A5\u7D9A\u8A66\u884C\u56DE\u6570",
21106
21119
  sshReconnectDelay: "SSH \u518D\u63A5\u7D9A\u5F85\u6A5F\uFF08\u79D2\uFF09",
21107
21120
  language: "\u8A00\u8A9E",
@@ -21245,6 +21258,7 @@ Bot\uFF1A{{botName}}
21245
21258
  clickToJump: "\u5BFE\u5FDC\u3059\u308B\u30DA\u30A4\u30F3\u306B\u30B8\u30E3\u30F3\u30D7",
21246
21259
  eventType: {
21247
21260
  terminal_bell: "\uD83D\uDD14 \u30BF\u30FC\u30DF\u30CA\u30EB\u30D9\u30EB",
21261
+ terminal_notification: "\uD83D\uDD14 \u30BF\u30FC\u30DF\u30CA\u30EB\u901A\u77E5",
21248
21262
  tmux_window_close: "\uD83E\uDE9F \u30A6\u30A3\u30F3\u30C9\u30A6\u9589\u3058\u308B",
21249
21263
  tmux_pane_close: "\uD83D\uDCF1 \u30DA\u30A4\u30F3\u9589\u3058\u308B",
21250
21264
  device_tmux_missing: "\u26A0\uFE0F Tmux \u304C\u3042\u308A\u307E\u305B\u3093",
@@ -21263,7 +21277,8 @@ Bot\uFF1A{{botName}}
21263
21277
  title: "\uD83D\uDD14 {{siteName}} \u304B\u3089\u306E\u30D9\u30EB\uFF1A{{terminalTopbarLabel}}",
21264
21278
  viewLink: "\u8868\u793A",
21265
21279
  terminalTopbarLabel: "\u30A6\u30A3\u30F3\u30C9\u30A6 {{window}} \xB7 \u30DA\u30A4\u30F3 {{pane}} @ {{device}}"
21266
- }
21280
+ },
21281
+ telegramNotification: {}
21267
21282
  },
21268
21283
  sidebar: {
21269
21284
  noWindows: "\u30A6\u30A3\u30F3\u30C9\u30A6\u304C\u3042\u308A\u307E\u305B\u3093",
@@ -21550,6 +21565,7 @@ __export(exports_schema, {
21550
21565
  OptionU32Schema: () => OptionU32Schema,
21551
21566
  OptionU16Schema: () => OptionU16Schema,
21552
21567
  OptionStringSchema: () => OptionStringSchema,
21568
+ NotificationEventSchema: () => NotificationEventSchema,
21553
21569
  LiveResumeSchema: () => LiveResumeSchema,
21554
21570
  LayoutChangeEventSchema: () => LayoutChangeEventSchema,
21555
21571
  HelloS2CSchema: () => HelloS2CSchema,
@@ -21776,6 +21792,16 @@ var BellEventSchema = import_zorsh.b.struct({
21776
21792
  paneIndex: OptionU16Schema,
21777
21793
  paneUrl: OptionStringSchema
21778
21794
  });
21795
+ var NotificationEventSchema = import_zorsh.b.struct({
21796
+ source: import_zorsh.b.u8(),
21797
+ title: OptionStringSchema,
21798
+ body: import_zorsh.b.string(),
21799
+ windowId: OptionStringSchema,
21800
+ paneId: OptionStringSchema,
21801
+ windowIndex: OptionU16Schema,
21802
+ paneIndex: OptionU16Schema,
21803
+ paneUrl: OptionStringSchema
21804
+ });
21779
21805
  // ../shared/src/ws-borsh/codec.ts
21780
21806
  var MAGIC = new Uint8Array([84, 88]);
21781
21807
  var CURRENT_VERSION = 1;
@@ -21988,6 +22014,16 @@ function resetChunkStreamId() {
21988
22014
  nextChunkStreamId = 1;
21989
22015
  }
21990
22016
  // ../shared/src/ws-borsh/convert.ts
22017
+ var notificationSourceToU8 = {
22018
+ osc9: 1,
22019
+ osc777: 2,
22020
+ osc1337: 3
22021
+ };
22022
+ var notificationSourceFromU8 = {
22023
+ 1: "osc9",
22024
+ 2: "osc777",
22025
+ 3: "osc1337"
22026
+ };
21991
22027
  function encodeDeviceEventPayload(payload) {
21992
22028
  const eventTypeMap = {
21993
22029
  "tmux-missing": 1,
@@ -22015,7 +22051,8 @@ function encodeTmuxEventPayload(payload) {
22015
22051
  "pane-active": 7,
22016
22052
  "layout-change": 8,
22017
22053
  bell: 9,
22018
- output: 10
22054
+ output: 10,
22055
+ notification: 11
22019
22056
  };
22020
22057
  const eventData = encodeEventData(payload.type, payload.data);
22021
22058
  const wireData = {
@@ -22083,6 +22120,19 @@ function encodeEventData(type, data) {
22083
22120
  }
22084
22121
  case "output":
22085
22122
  return new Uint8Array;
22123
+ case "notification": {
22124
+ const d = data;
22125
+ return NotificationEventSchema.serialize({
22126
+ source: notificationSourceToU8[d.source],
22127
+ title: d.title ?? null,
22128
+ body: d.body,
22129
+ windowId: d.windowId ?? null,
22130
+ paneId: d.paneId ?? null,
22131
+ windowIndex: d.windowIndex ?? null,
22132
+ paneIndex: d.paneIndex ?? null,
22133
+ paneUrl: d.paneUrl ?? null
22134
+ });
22135
+ }
22086
22136
  default:
22087
22137
  return new Uint8Array;
22088
22138
  }
@@ -22149,9 +22199,13 @@ function decodeTmuxEventPayload(data) {
22149
22199
  7: "pane-active",
22150
22200
  8: "layout-change",
22151
22201
  9: "bell",
22152
- 10: "output"
22202
+ 10: "output",
22203
+ 11: "notification"
22153
22204
  };
22154
- const type = eventTypeMap[wire.eventType] ?? "output";
22205
+ const type = eventTypeMap[wire.eventType];
22206
+ if (!type) {
22207
+ throw new Error(`Unknown tmux event type: ${wire.eventType}`);
22208
+ }
22155
22209
  return {
22156
22210
  deviceId: wire.deviceId,
22157
22211
  type,
@@ -22189,6 +22243,19 @@ function decodeEventData(type, data) {
22189
22243
  paneUrl: bell.paneUrl ?? undefined
22190
22244
  };
22191
22245
  }
22246
+ case "notification": {
22247
+ const notification = NotificationEventSchema.deserialize(data);
22248
+ return {
22249
+ source: notificationSourceFromU8[notification.source] ?? "osc9",
22250
+ title: notification.title ?? undefined,
22251
+ body: notification.body,
22252
+ windowId: notification.windowId ?? undefined,
22253
+ paneId: notification.paneId ?? undefined,
22254
+ windowIndex: notification.windowIndex ?? undefined,
22255
+ paneIndex: notification.paneIndex ?? undefined,
22256
+ paneUrl: notification.paneUrl ?? undefined
22257
+ };
22258
+ }
22192
22259
  default:
22193
22260
  return {};
22194
22261
  }
@@ -22308,6 +22375,13 @@ var runtimeController = new RuntimeController;
22308
22375
  function getEnv(key, defaultValue) {
22309
22376
  return process.env[key] ?? defaultValue;
22310
22377
  }
22378
+ function getBooleanEnv(key, defaultValue) {
22379
+ const value = process.env[key];
22380
+ if (value === undefined) {
22381
+ return defaultValue;
22382
+ }
22383
+ return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
22384
+ }
22311
22385
  var config = {
22312
22386
  masterKey: process.env.TMEX_MASTER_KEY,
22313
22387
  port: Number.parseInt(getEnv("GATEWAY_PORT", "9663"), 10),
@@ -22315,6 +22389,8 @@ var config = {
22315
22389
  siteNameDefault: getEnv("TMEX_SITE_NAME", "tmex"),
22316
22390
  databaseUrl: getEnv("DATABASE_URL", "./tmex.db"),
22317
22391
  bellThrottleSecondsDefault: Number.parseInt(getEnv("TMEX_BELL_THROTTLE_SECONDS", "6"), 10),
22392
+ notificationThrottleSecondsDefault: Number.parseInt(getEnv("TMEX_NOTIFICATION_THROTTLE_SECONDS", "3"), 10),
22393
+ tmuxAllowPassthrough: getBooleanEnv("TMEX_TMUX_ALLOW_PASSTHROUGH", false),
22318
22394
  sshReconnectMaxRetriesDefault: Number.parseInt(getEnv("TMEX_SSH_RECONNECT_MAX_RETRIES", "2"), 10),
22319
22395
  sshReconnectDelaySecondsDefault: Number.parseInt(getEnv("TMEX_SSH_RECONNECT_DELAY_SECONDS", "10"), 10),
22320
22396
  languageDefault: getEnv("TMEX_DEFAULT_LANGUAGE", "en_US"),
@@ -28753,8 +28829,13 @@ var siteSettings = sqliteTable("site_settings", {
28753
28829
  siteName: text("site_name").notNull(),
28754
28830
  siteUrl: text("site_url").notNull(),
28755
28831
  bellThrottleSeconds: integer("bell_throttle_seconds").notNull(),
28832
+ notificationThrottleSeconds: integer("notification_throttle_seconds").notNull().default(3),
28756
28833
  enableBrowserBellToast: integer("enable_browser_bell_toast", { mode: "boolean" }).notNull().default(true),
28834
+ enableBrowserNotificationToast: integer("enable_browser_notification_toast", { mode: "boolean" }).notNull().default(true),
28757
28835
  enableTelegramBellPush: integer("enable_telegram_bell_push", { mode: "boolean" }).notNull().default(true),
28836
+ enableTelegramNotificationPush: integer("enable_telegram_notification_push", {
28837
+ mode: "boolean"
28838
+ }).notNull().default(true),
28758
28839
  sshReconnectMaxRetries: integer("ssh_reconnect_max_retries").notNull(),
28759
28840
  sshReconnectDelaySeconds: integer("ssh_reconnect_delay_seconds").notNull(),
28760
28841
  language: text("language").notNull().default("en_US"),
@@ -28869,8 +28950,11 @@ function toSiteSettings(row) {
28869
28950
  siteName: row.siteName,
28870
28951
  siteUrl: row.siteUrl,
28871
28952
  bellThrottleSeconds: row.bellThrottleSeconds,
28953
+ notificationThrottleSeconds: row.notificationThrottleSeconds,
28872
28954
  enableBrowserBellToast: row.enableBrowserBellToast,
28955
+ enableBrowserNotificationToast: row.enableBrowserNotificationToast,
28873
28956
  enableTelegramBellPush: row.enableTelegramBellPush,
28957
+ enableTelegramNotificationPush: row.enableTelegramNotificationPush,
28874
28958
  sshReconnectMaxRetries: row.sshReconnectMaxRetries,
28875
28959
  sshReconnectDelaySeconds: row.sshReconnectDelaySeconds,
28876
28960
  language: normalizeLocale(row.language),
@@ -28921,8 +29005,11 @@ function ensureSiteSettingsInitialized() {
28921
29005
  siteName: config.siteNameDefault,
28922
29006
  siteUrl: config.baseUrl,
28923
29007
  bellThrottleSeconds: config.bellThrottleSecondsDefault,
29008
+ notificationThrottleSeconds: config.notificationThrottleSecondsDefault,
28924
29009
  enableBrowserBellToast: true,
29010
+ enableBrowserNotificationToast: true,
28925
29011
  enableTelegramBellPush: true,
29012
+ enableTelegramNotificationPush: true,
28926
29013
  sshReconnectMaxRetries: config.sshReconnectMaxRetriesDefault,
28927
29014
  sshReconnectDelaySeconds: config.sshReconnectDelaySecondsDefault,
28928
29015
  language: normalizeLocale(config.languageDefault),
@@ -29048,8 +29135,11 @@ function updateSiteSettings(updates) {
29048
29135
  siteName: updates.siteName ?? current.siteName,
29049
29136
  siteUrl: updates.siteUrl ?? current.siteUrl,
29050
29137
  bellThrottleSeconds: updates.bellThrottleSeconds ?? current.bellThrottleSeconds,
29138
+ notificationThrottleSeconds: updates.notificationThrottleSeconds ?? current.notificationThrottleSeconds,
29051
29139
  enableBrowserBellToast: updates.enableBrowserBellToast ?? current.enableBrowserBellToast,
29140
+ enableBrowserNotificationToast: updates.enableBrowserNotificationToast ?? current.enableBrowserNotificationToast,
29052
29141
  enableTelegramBellPush: updates.enableTelegramBellPush ?? current.enableTelegramBellPush,
29142
+ enableTelegramNotificationPush: updates.enableTelegramNotificationPush ?? current.enableTelegramNotificationPush,
29053
29143
  sshReconnectMaxRetries: updates.sshReconnectMaxRetries ?? current.sshReconnectMaxRetries,
29054
29144
  sshReconnectDelaySeconds: updates.sshReconnectDelaySeconds ?? current.sshReconnectDelaySeconds,
29055
29145
  language: updates.language ? normalizeLocale(updates.language) : current.language,
@@ -29060,8 +29150,11 @@ function updateSiteSettings(updates) {
29060
29150
  siteName: next.siteName,
29061
29151
  siteUrl: next.siteUrl,
29062
29152
  bellThrottleSeconds: next.bellThrottleSeconds,
29153
+ notificationThrottleSeconds: next.notificationThrottleSeconds,
29063
29154
  enableBrowserBellToast: next.enableBrowserBellToast,
29155
+ enableBrowserNotificationToast: next.enableBrowserNotificationToast,
29064
29156
  enableTelegramBellPush: next.enableTelegramBellPush,
29157
+ enableTelegramNotificationPush: next.enableTelegramNotificationPush,
29065
29158
  sshReconnectMaxRetries: next.sshReconnectMaxRetries,
29066
29159
  sshReconnectDelaySeconds: next.sshReconnectDelaySeconds,
29067
29160
  language: next.language,
@@ -51725,6 +51818,7 @@ class EventNotifier {
51725
51818
  lastRefresh = 0;
51726
51819
  REFRESH_INTERVAL = 60000;
51727
51820
  bellThrottleMap = new Map;
51821
+ notificationThrottleMap = new Map;
51728
51822
  refreshConfig() {
51729
51823
  const now = Date.now();
51730
51824
  if (now - this.lastRefresh < this.REFRESH_INTERVAL)
@@ -51740,8 +51834,14 @@ class EventNotifier {
51740
51834
  eventType,
51741
51835
  timestamp: new Date().toISOString()
51742
51836
  };
51743
- if (eventType === "terminal_bell" && !this.shouldPassBellThrottle(fullEvent)) {
51744
- return;
51837
+ if (eventType === "terminal_bell") {
51838
+ if (!this.shouldPassBellThrottle(fullEvent)) {
51839
+ return;
51840
+ }
51841
+ } else if (eventType === "terminal_notification") {
51842
+ if (!this.shouldPassNotificationThrottle(fullEvent)) {
51843
+ return;
51844
+ }
51745
51845
  }
51746
51846
  await Promise.all([
51747
51847
  this.sendWebhooks(eventType, fullEvent),
@@ -51763,6 +51863,22 @@ class EventNotifier {
51763
51863
  this.bellThrottleMap.set(key, now);
51764
51864
  return true;
51765
51865
  }
51866
+ shouldPassNotificationThrottle(event) {
51867
+ const settings = getSiteSettings();
51868
+ const throttleMs = Math.max(0, settings.notificationThrottleSeconds) * 1000;
51869
+ if (throttleMs === 0) {
51870
+ return true;
51871
+ }
51872
+ const source = typeof event.payload?.source === "string" ? event.payload.source : "unknown";
51873
+ const key = `${event.device.id}:${event.tmux?.paneId ?? "-"}:notification:${source}`;
51874
+ const now = Date.now();
51875
+ const previous = this.notificationThrottleMap.get(key) ?? 0;
51876
+ if (now - previous < throttleMs) {
51877
+ return false;
51878
+ }
51879
+ this.notificationThrottleMap.set(key, now);
51880
+ return true;
51881
+ }
51766
51882
  async sendWebhooks(eventType, event) {
51767
51883
  const targets = this.webhooks.filter((w) => w.eventMask.includes(eventType));
51768
51884
  await Promise.all(targets.map(async (webhook) => {
@@ -51800,6 +51916,14 @@ class EventNotifier {
51800
51916
  await telegramService.sendToAuthorizedChats({ text: bellMessage, parseMode: "HTML" });
51801
51917
  return;
51802
51918
  }
51919
+ if (eventType === "terminal_notification") {
51920
+ if (!settings.enableTelegramNotificationPush) {
51921
+ return;
51922
+ }
51923
+ const notificationMessage = this.formatTelegramNotificationMessage(event);
51924
+ await telegramService.sendToAuthorizedChats({ text: notificationMessage, parseMode: "HTML" });
51925
+ return;
51926
+ }
51803
51927
  const message = this.formatTelegramMessage(event, settings);
51804
51928
  await telegramService.sendToAuthorizedChats({ text: message });
51805
51929
  }
@@ -51824,11 +51948,34 @@ class EventNotifier {
51824
51948
  lines.push("", `<a href="${escapeTelegramHtmlAttribute(tgSafePaneUrl)}">${escapeTelegramHtmlText(t2("notification.telegramBell.viewLink"))}</a>`);
51825
51949
  }
51826
51950
  return lines.join(`
51951
+ `);
51952
+ }
51953
+ formatTelegramNotificationMessage(event) {
51954
+ const title = typeof event.payload?.title === "string" ? event.payload.title : "";
51955
+ const body = typeof event.payload?.message === "string" ? event.payload.message : "";
51956
+ const lines = [];
51957
+ if (title) {
51958
+ lines.push(escapeTelegramHtmlText(title));
51959
+ }
51960
+ if (body) {
51961
+ lines.push(escapeTelegramHtmlText(body));
51962
+ }
51963
+ const paneUrl = normalizeHttpUrl(buildPaneUrl(event));
51964
+ const topbarLabel = this.buildTerminalTopbarLabel(event);
51965
+ const footer = `from ${event.site.name}: ${topbarLabel}`;
51966
+ if (paneUrl) {
51967
+ const tgSafePaneUrl = encodePercentForTelegramUrl(paneUrl);
51968
+ lines.push("", `<a href="${escapeTelegramHtmlAttribute(tgSafePaneUrl)}">${escapeTelegramHtmlText(footer)}</a>`);
51969
+ } else {
51970
+ lines.push("", escapeTelegramHtmlText(footer));
51971
+ }
51972
+ return lines.join(`
51827
51973
  `);
51828
51974
  }
51829
51975
  formatTelegramMessage(event, settings) {
51830
51976
  const emojiMap = {
51831
51977
  terminal_bell: "\uD83D\uDD14",
51978
+ terminal_notification: "\uD83D\uDD14",
51832
51979
  tmux_window_close: "\uD83E\uDE9F",
51833
51980
  tmux_pane_close: "\uD83D\uDCF1",
51834
51981
  device_tmux_missing: "\u26A0\uFE0F",
@@ -52075,12 +52222,80 @@ function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
52075
52222
  return chunks;
52076
52223
  }
52077
52224
 
52078
- // ../../apps/gateway/src/tmux-client/pane-title-parser.ts
52225
+ // ../../apps/gateway/src/tmux-client/pane-stream-parser.ts
52079
52226
  var decoder = new TextDecoder;
52080
- function createPaneTitleParser(options) {
52227
+ var MAX_OSC_KIND_BYTES = 16;
52228
+ var MAX_OSC_PAYLOAD_BYTES = 8 * 1024;
52229
+ function createPaneStreamParser(options) {
52081
52230
  let phase = "normal";
52082
52231
  let oscKind = "";
52232
+ let oscPayloadBytes = [];
52083
52233
  let titleBytes = [];
52234
+ let warnedOscPayloadOverflow = false;
52235
+ function resetOscState() {
52236
+ oscKind = "";
52237
+ oscPayloadBytes = [];
52238
+ }
52239
+ function appendOscPayloadByte(byte) {
52240
+ if (oscPayloadBytes.length >= MAX_OSC_PAYLOAD_BYTES) {
52241
+ if (!warnedOscPayloadOverflow) {
52242
+ warnedOscPayloadOverflow = true;
52243
+ console.warn("[tmex] pane stream parser dropped oversized OSC payload");
52244
+ }
52245
+ oscPayloadBytes = [];
52246
+ phase = "osc-body-ignore";
52247
+ return false;
52248
+ }
52249
+ oscPayloadBytes.push(byte);
52250
+ return true;
52251
+ }
52252
+ function emitTitle(bytes) {
52253
+ const title = decoder.decode(new Uint8Array(bytes)).trim();
52254
+ if (!title) {
52255
+ return;
52256
+ }
52257
+ options.onTitle(title);
52258
+ }
52259
+ function emitOsc() {
52260
+ const payload = decoder.decode(new Uint8Array(oscPayloadBytes));
52261
+ switch (oscKind) {
52262
+ case "0":
52263
+ case "1":
52264
+ case "2":
52265
+ emitTitle(oscPayloadBytes);
52266
+ return;
52267
+ case "9":
52268
+ if (/^4(;|$)/.test(payload)) {
52269
+ return;
52270
+ }
52271
+ options.onNotification({ source: "osc9", body: payload });
52272
+ return;
52273
+ case "777": {
52274
+ const verbSeparatorIndex = payload.indexOf(";");
52275
+ const verb = verbSeparatorIndex >= 0 ? payload.slice(0, verbSeparatorIndex) : payload;
52276
+ if (verb !== "notify") {
52277
+ return;
52278
+ }
52279
+ const rest = verbSeparatorIndex >= 0 ? payload.slice(verbSeparatorIndex + 1) : "";
52280
+ const titleSeparatorIndex = rest.indexOf(";");
52281
+ const title = titleSeparatorIndex >= 0 ? rest.slice(0, titleSeparatorIndex) : rest;
52282
+ const body = titleSeparatorIndex >= 0 ? rest.slice(titleSeparatorIndex + 1) : "";
52283
+ options.onNotification({
52284
+ source: "osc777",
52285
+ title: title || undefined,
52286
+ body
52287
+ });
52288
+ return;
52289
+ }
52290
+ case "1337":
52291
+ if (/^RequestAttention=(yes|once|fireworks|true)$/i.test(payload)) {
52292
+ options.onNotification({ source: "osc1337", body: "RequestAttention" });
52293
+ }
52294
+ return;
52295
+ default:
52296
+ return;
52297
+ }
52298
+ }
52084
52299
  return {
52085
52300
  push(data) {
52086
52301
  const output = [];
@@ -52088,36 +52303,57 @@ function createPaneTitleParser(options) {
52088
52303
  if (phase === "normal") {
52089
52304
  if (byte === 27) {
52090
52305
  phase = "esc";
52091
- } else {
52092
- output.push(byte);
52306
+ continue;
52093
52307
  }
52308
+ if (byte === 7) {
52309
+ options.onBell();
52310
+ continue;
52311
+ }
52312
+ output.push(byte);
52094
52313
  continue;
52095
52314
  }
52096
52315
  if (phase === "esc") {
52097
52316
  if (byte === 93) {
52098
- phase = "osc";
52099
- oscKind = "";
52317
+ resetOscState();
52318
+ phase = "osc-params";
52319
+ continue;
52320
+ }
52321
+ if (byte === 107) {
52100
52322
  titleBytes = [];
52323
+ phase = "screen-title";
52101
52324
  continue;
52102
52325
  }
52103
52326
  output.push(27, byte);
52104
52327
  phase = "normal";
52105
52328
  continue;
52106
52329
  }
52107
- if (phase === "osc") {
52330
+ if (phase === "osc-params") {
52108
52331
  if (byte === 59) {
52109
- phase = oscKind === "0" || oscKind === "2" ? "osc-data" : "normal";
52110
- if (phase === "normal") {
52111
- output.push(27, 93, ...encoderFromString(oscKind), 59);
52112
- }
52332
+ phase = oscKind === "0" || oscKind === "1" || oscKind === "2" || oscKind === "9" || oscKind === "777" || oscKind === "1337" ? "osc-body" : "osc-body-ignore";
52333
+ continue;
52334
+ }
52335
+ if (byte === 7) {
52336
+ emitOsc();
52337
+ resetOscState();
52338
+ phase = "normal";
52339
+ continue;
52340
+ }
52341
+ if (byte === 27) {
52342
+ phase = "osc-st";
52343
+ continue;
52344
+ }
52345
+ if (oscKind.length >= MAX_OSC_KIND_BYTES) {
52346
+ resetOscState();
52347
+ phase = "osc-body-ignore";
52113
52348
  continue;
52114
52349
  }
52115
52350
  oscKind += String.fromCharCode(byte);
52116
52351
  continue;
52117
52352
  }
52118
- if (phase === "osc-data") {
52353
+ if (phase === "osc-body") {
52119
52354
  if (byte === 7) {
52120
- emitTitle(options.onTitle, titleBytes);
52355
+ emitOsc();
52356
+ resetOscState();
52121
52357
  phase = "normal";
52122
52358
  continue;
52123
52359
  }
@@ -52125,31 +52361,69 @@ function createPaneTitleParser(options) {
52125
52361
  phase = "osc-st";
52126
52362
  continue;
52127
52363
  }
52364
+ appendOscPayloadByte(byte);
52365
+ continue;
52366
+ }
52367
+ if (phase === "osc-body-ignore") {
52368
+ if (byte === 7) {
52369
+ resetOscState();
52370
+ phase = "normal";
52371
+ continue;
52372
+ }
52373
+ if (byte === 27) {
52374
+ phase = "osc-st-ignore";
52375
+ }
52376
+ continue;
52377
+ }
52378
+ if (phase === "osc-st") {
52379
+ if (byte === 92) {
52380
+ emitOsc();
52381
+ resetOscState();
52382
+ phase = "normal";
52383
+ continue;
52384
+ }
52385
+ if (!appendOscPayloadByte(27)) {
52386
+ continue;
52387
+ }
52388
+ appendOscPayloadByte(byte);
52389
+ continue;
52390
+ }
52391
+ if (phase === "osc-st-ignore") {
52392
+ if (byte === 92) {
52393
+ resetOscState();
52394
+ phase = "normal";
52395
+ continue;
52396
+ }
52397
+ phase = "osc-body-ignore";
52398
+ continue;
52399
+ }
52400
+ if (phase === "screen-title") {
52401
+ if (byte === 7) {
52402
+ emitTitle(titleBytes);
52403
+ titleBytes = [];
52404
+ phase = "normal";
52405
+ continue;
52406
+ }
52407
+ if (byte === 27) {
52408
+ phase = "screen-title-st";
52409
+ continue;
52410
+ }
52128
52411
  titleBytes.push(byte);
52129
52412
  continue;
52130
52413
  }
52131
52414
  if (byte === 92) {
52132
- emitTitle(options.onTitle, titleBytes);
52415
+ emitTitle(titleBytes);
52416
+ titleBytes = [];
52133
52417
  phase = "normal";
52134
52418
  continue;
52135
52419
  }
52136
52420
  titleBytes.push(27, byte);
52137
- phase = "osc-data";
52421
+ phase = "screen-title";
52138
52422
  }
52139
52423
  return new Uint8Array(output);
52140
52424
  }
52141
52425
  };
52142
52426
  }
52143
- function emitTitle(onTitle, titleBytes) {
52144
- const title = decoder.decode(new Uint8Array(titleBytes)).trim();
52145
- if (!title) {
52146
- return;
52147
- }
52148
- onTitle(title);
52149
- }
52150
- function encoderFromString(value) {
52151
- return new TextEncoder().encode(value);
52152
- }
52153
52427
 
52154
52428
  // ../../apps/gateway/src/tmux-client/local-external-connection.ts
52155
52429
  function hasRenderableTerminalContent(value) {
@@ -52193,8 +52467,7 @@ class LocalExternalTmuxConnection {
52193
52467
  pendingPaneTitles = new Map;
52194
52468
  snapshotSession = null;
52195
52469
  snapshotWindows = new Map;
52196
- currentPipePaneId = null;
52197
- pipeReadAbort = null;
52470
+ paneReaders = new Map;
52198
52471
  pipeTransition = Promise.resolve();
52199
52472
  inputTransition = Promise.resolve();
52200
52473
  hookReadAbort = null;
@@ -52231,6 +52504,7 @@ class LocalExternalTmuxConnection {
52231
52504
  });
52232
52505
  this.ensureRuntimeDirs();
52233
52506
  await this.ensureSession();
52507
+ await this.configureSessionOptions();
52234
52508
  if (this.deps.enableHooks) {
52235
52509
  await this.startHooks();
52236
52510
  }
@@ -52248,7 +52522,7 @@ class LocalExternalTmuxConnection {
52248
52522
  }
52249
52523
  this.manualDisconnect = true;
52250
52524
  this.connected = false;
52251
- this.stopPipe();
52525
+ this.stopAllPipeReaders();
52252
52526
  if (this.deps.enableHooks) {
52253
52527
  this.stopHooks();
52254
52528
  }
@@ -52310,7 +52584,7 @@ class LocalExternalTmuxConnection {
52310
52584
  if (!this.connected) {
52311
52585
  return;
52312
52586
  }
52313
- const argv = name ? ["new-window", "-n", name] : ["new-window"];
52587
+ const argv = name ? ["new-window", "-t", this.sessionName, "-n", name] : ["new-window", "-t", this.sessionName];
52314
52588
  this.runAndRefresh(argv).catch((error) => {
52315
52589
  this.callbacks.onError(error);
52316
52590
  });
@@ -52346,6 +52620,26 @@ class LocalExternalTmuxConnection {
52346
52620
  }
52347
52621
  await this.runTmux(["new-session", "-d", "-c", homedir(), "-s", this.sessionName]);
52348
52622
  }
52623
+ async configureSessionOptions() {
52624
+ await this.runTmuxAllowFailure([
52625
+ "set-option",
52626
+ "-t",
52627
+ this.sessionName,
52628
+ "-s",
52629
+ "allow-passthrough",
52630
+ config.tmuxAllowPassthrough ? "on" : "off"
52631
+ ]);
52632
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "extended-keys", "on"]);
52633
+ await this.runTmuxAllowFailure([
52634
+ "set-option",
52635
+ "-t",
52636
+ this.sessionName,
52637
+ "-s",
52638
+ "extended-keys-format",
52639
+ "csi-u"
52640
+ ]);
52641
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
52642
+ }
52349
52643
  ensureRuntimeDirs() {
52350
52644
  mkdirSync(this.fsPaths.rootDir, { recursive: true, mode: 448 });
52351
52645
  mkdirSync(this.fsPaths.panesDir, { recursive: true, mode: 448 });
@@ -52381,14 +52675,16 @@ class LocalExternalTmuxConnection {
52381
52675
  readerProcess.kill();
52382
52676
  rmSync(fifoPath, { force: true });
52383
52677
  };
52384
- await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
52385
52678
  await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
52386
52679
  await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
52680
+ await this.installHook("after-new-window", ["refresh"]);
52681
+ await this.installHook("after-split-window", ["refresh"]);
52387
52682
  }
52388
52683
  async stopHooks() {
52389
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
52390
52684
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
52391
52685
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
52686
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-new-window"]);
52687
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-split-window"]);
52392
52688
  this.hookReadAbort?.();
52393
52689
  this.hookReadAbort = null;
52394
52690
  this.hookBuffer = "";
@@ -52419,22 +52715,9 @@ class LocalExternalTmuxConnection {
52419
52715
  }
52420
52716
  const [type, windowId, paneId] = line.split("\t");
52421
52717
  if (type === "bell") {
52422
- const key = paneId || windowId || "-";
52423
- const previous = this.bellDedup.get(key) ?? 0;
52424
- const now = Date.now();
52425
- if (now - previous >= BELL_DEDUP_WINDOW_MS) {
52426
- this.bellDedup.set(key, now);
52427
- this.callbacks.onEvent({
52428
- type: "bell",
52429
- data: {
52430
- windowId: windowId || undefined,
52431
- paneId: paneId || this.activePaneId || undefined
52432
- }
52433
- });
52434
- }
52435
52718
  continue;
52436
52719
  }
52437
- if (type === "pane-exited" || type === "pane-died") {
52720
+ if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
52438
52721
  this.requestSnapshot();
52439
52722
  }
52440
52723
  }
@@ -52465,7 +52748,6 @@ class LocalExternalTmuxConnection {
52465
52748
  this.activePaneId = paneId;
52466
52749
  await this.runTmux(["select-window", "-t", windowId], true);
52467
52750
  await this.runTmux(["select-pane", "-t", paneId], true);
52468
- await this.startPipeForPane(paneId);
52469
52751
  if (size) {
52470
52752
  await this.resizePaneInternal(paneId, size.cols, size.rows);
52471
52753
  }
@@ -52479,8 +52761,8 @@ class LocalExternalTmuxConnection {
52479
52761
  async capturePaneHistory(paneId) {
52480
52762
  const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
52481
52763
  const alternateScreen = mode === "1";
52482
- const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-p"], true)).stdout;
52483
- const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-p", "-q"], true)).stdout;
52764
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-N", "-p"], true)).stdout;
52765
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-N", "-p", "-q"], true)).stdout;
52484
52766
  const history = alternateScreen ? hasRenderableTerminalContent(normal) ? normal : alternate : normal || alternate;
52485
52767
  if (history) {
52486
52768
  this.callbacks.onTerminalHistory(paneId, history, alternateScreen);
@@ -52520,6 +52802,7 @@ class LocalExternalTmuxConnection {
52520
52802
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
52521
52803
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
52522
52804
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
52805
+ await this.syncPipeReaders();
52523
52806
  this.emitSnapshot();
52524
52807
  }
52525
52808
  parseSnapshotSession(lines) {
@@ -52617,69 +52900,122 @@ class LocalExternalTmuxConnection {
52617
52900
  }
52618
52901
  return null;
52619
52902
  }
52620
- async startPipeForPane(paneId) {
52621
- await this.queuePipeTransition(async () => {
52622
- if (this.currentPipePaneId === paneId) {
52623
- return;
52903
+ recordBell(paneId, windowId) {
52904
+ const key = paneId || windowId || "-";
52905
+ const previous = this.bellDedup.get(key) ?? 0;
52906
+ const now = Date.now();
52907
+ if (now - previous < BELL_DEDUP_WINDOW_MS) {
52908
+ return;
52909
+ }
52910
+ this.bellDedup.set(key, now);
52911
+ this.callbacks.onEvent({
52912
+ type: "bell",
52913
+ data: {
52914
+ windowId,
52915
+ paneId: paneId || this.activePaneId || undefined
52916
+ }
52917
+ });
52918
+ }
52919
+ emitNotification(paneId, notification) {
52920
+ this.callbacks.onEvent({
52921
+ type: "notification",
52922
+ data: {
52923
+ paneId,
52924
+ ...notification
52925
+ }
52926
+ });
52927
+ }
52928
+ getExpectedPaneIds() {
52929
+ return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
52930
+ }
52931
+ async startPipeForPaneNow(paneId) {
52932
+ if (this.paneReaders.has(paneId)) {
52933
+ return;
52934
+ }
52935
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
52936
+ this.ensureRuntimeDirs();
52937
+ rmSync(fifoPath, { force: true });
52938
+ await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
52939
+ const parser = createPaneStreamParser({
52940
+ onTitle: (title) => {
52941
+ this.pendingPaneTitles.set(paneId, title);
52942
+ this.requestSnapshot();
52943
+ },
52944
+ onBell: () => {
52945
+ this.recordBell(paneId);
52946
+ },
52947
+ onNotification: (notification) => {
52948
+ this.emitNotification(paneId, notification);
52624
52949
  }
52625
- await this.stopPipeNow();
52626
- const fifoPath = this.fsPaths.paneFifoPath(paneId);
52627
- this.ensureRuntimeDirs();
52950
+ });
52951
+ const readerProcess = Bun.spawn(["/bin/sh", "-lc", `cat ${quoteShellArg(fifoPath)}`], {
52952
+ stdout: "pipe",
52953
+ stderr: "pipe"
52954
+ });
52955
+ const reader = readerProcess.stdout.getReader();
52956
+ const stopReader = () => {
52957
+ reader.releaseLock();
52958
+ readerProcess.kill();
52628
52959
  rmSync(fifoPath, { force: true });
52629
- await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
52630
- const parser = createPaneTitleParser({
52631
- onTitle: (title) => {
52632
- this.pendingPaneTitles.set(paneId, title);
52633
- this.requestSnapshot();
52634
- }
52635
- });
52636
- const readerProcess = Bun.spawn(["/bin/sh", "-lc", `cat ${quoteShellArg(fifoPath)}`], {
52637
- stdout: "pipe",
52638
- stderr: "pipe"
52639
- });
52640
- const reader = readerProcess.stdout.getReader();
52641
- (async () => {
52642
- try {
52643
- while (true) {
52644
- const chunk2 = await reader.read();
52645
- if (chunk2.done) {
52646
- break;
52647
- }
52648
- const raw = chunk2.value;
52649
- const output = parser.push(raw);
52650
- if (Array.from(raw).includes(7)) {
52651
- this.callbacks.onEvent({ type: "bell", data: { paneId } });
52652
- }
52653
- if (output.length > 0) {
52654
- this.callbacks.onTerminalOutput(paneId, output);
52655
- }
52960
+ };
52961
+ this.paneReaders.set(paneId, { paneId, fifoPath, stopReader });
52962
+ (async () => {
52963
+ try {
52964
+ while (true) {
52965
+ const chunk2 = await reader.read();
52966
+ if (chunk2.done) {
52967
+ break;
52656
52968
  }
52657
- } catch (error) {
52658
- if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52659
- this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52969
+ const output = parser.push(chunk2.value);
52970
+ if (output.length > 0) {
52971
+ this.callbacks.onTerminalOutput(paneId, output);
52660
52972
  }
52661
52973
  }
52662
- })();
52663
- this.pipeReadAbort = () => {
52664
- reader.releaseLock();
52665
- readerProcess.kill();
52666
- rmSync(fifoPath, { force: true });
52667
- };
52974
+ } catch (error) {
52975
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52976
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52977
+ }
52978
+ }
52979
+ })();
52980
+ try {
52668
52981
  await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
52669
- this.currentPipePaneId = paneId;
52670
- });
52671
- }
52672
- async stopPipe() {
52673
- await this.queuePipeTransition(() => this.stopPipeNow());
52982
+ } catch (error) {
52983
+ this.paneReaders.delete(paneId);
52984
+ stopReader();
52985
+ throw error;
52986
+ }
52674
52987
  }
52675
- async stopPipeNow() {
52676
- const paneId = this.currentPipePaneId;
52677
- this.currentPipePaneId = null;
52678
- if (paneId) {
52679
- await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
52988
+ async stopPipeForPaneNow(paneId) {
52989
+ const handle = this.paneReaders.get(paneId);
52990
+ if (!handle) {
52991
+ return;
52680
52992
  }
52681
- this.pipeReadAbort?.();
52682
- this.pipeReadAbort = null;
52993
+ this.paneReaders.delete(paneId);
52994
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
52995
+ handle.stopReader();
52996
+ }
52997
+ async syncPipeReaders() {
52998
+ const expectedPaneIds = this.getExpectedPaneIds();
52999
+ const expectedSet = new Set(expectedPaneIds);
53000
+ await this.queuePipeTransition(async () => {
53001
+ for (const paneId of Array.from(this.paneReaders.keys())) {
53002
+ if (!expectedSet.has(paneId)) {
53003
+ await this.stopPipeForPaneNow(paneId);
53004
+ }
53005
+ }
53006
+ for (const paneId of expectedPaneIds) {
53007
+ if (!this.paneReaders.has(paneId)) {
53008
+ await this.startPipeForPaneNow(paneId);
53009
+ }
53010
+ }
53011
+ });
53012
+ }
53013
+ async stopAllPipeReaders() {
53014
+ await this.queuePipeTransition(async () => {
53015
+ for (const paneId of Array.from(this.paneReaders.keys())) {
53016
+ await this.stopPipeForPaneNow(paneId);
53017
+ }
53018
+ });
52683
53019
  }
52684
53020
  queuePipeTransition(task) {
52685
53021
  const next = this.pipeTransition.catch(() => {
@@ -52733,6 +53069,10 @@ class LocalExternalTmuxConnection {
52733
53069
  // ../../apps/gateway/src/tmux-client/ssh-external-connection.ts
52734
53070
  var import_ssh2 = __toESM(require_lib3(), 1);
52735
53071
 
53072
+ // ../../apps/gateway/src/tmux-client/ssh-connect-config.ts
53073
+ import { existsSync as existsSync2, readFileSync } from "fs";
53074
+ import { join as join4 } from "path";
53075
+
52736
53076
  // ../../apps/gateway/src/tmux/ssh-auth.ts
52737
53077
  function normalizeEnvValue(value) {
52738
53078
  const trimmed = value?.trim();
@@ -52765,6 +53105,219 @@ function resolveSshAgentSocket(authMode, env = process.env) {
52765
53105
  return;
52766
53106
  }
52767
53107
 
53108
+ // ../../apps/gateway/src/tmux-client/ssh-connect-config.ts
53109
+ function defaultRunSync2(cmd) {
53110
+ const result = Bun.spawnSync(cmd, {
53111
+ env: process.env,
53112
+ stdout: "pipe",
53113
+ stderr: "pipe"
53114
+ });
53115
+ return {
53116
+ exitCode: result.exitCode,
53117
+ stdout: Buffer.from(result.stdout).toString("utf8"),
53118
+ stderr: Buffer.from(result.stderr).toString("utf8")
53119
+ };
53120
+ }
53121
+ function expandHomePath(value, env) {
53122
+ const trimmed = value.trim();
53123
+ if (trimmed === "~") {
53124
+ return env.HOME?.trim() || trimmed;
53125
+ }
53126
+ if (trimmed.startsWith("~/") && env.HOME?.trim()) {
53127
+ return join4(env.HOME.trim(), trimmed.slice(2));
53128
+ }
53129
+ return trimmed;
53130
+ }
53131
+ function parseSshConfigOutput(stdout, env) {
53132
+ let host = "";
53133
+ let port;
53134
+ let username;
53135
+ let identityAgent;
53136
+ const identityFiles = [];
53137
+ for (const rawLine of stdout.split(/\r?\n/)) {
53138
+ const line = rawLine.trim();
53139
+ if (!line) {
53140
+ continue;
53141
+ }
53142
+ const firstSpace = line.indexOf(" ");
53143
+ if (firstSpace <= 0) {
53144
+ continue;
53145
+ }
53146
+ const key = line.slice(0, firstSpace).trim().toLowerCase();
53147
+ const value = line.slice(firstSpace + 1).trim();
53148
+ if (!value) {
53149
+ continue;
53150
+ }
53151
+ switch (key) {
53152
+ case "hostname":
53153
+ host = value;
53154
+ break;
53155
+ case "port": {
53156
+ const parsedPort = Number.parseInt(value, 10);
53157
+ port = Number.isNaN(parsedPort) ? undefined : parsedPort;
53158
+ break;
53159
+ }
53160
+ case "user":
53161
+ username = value;
53162
+ break;
53163
+ case "identityagent":
53164
+ identityAgent = value;
53165
+ break;
53166
+ case "identityfile":
53167
+ identityFiles.push(expandHomePath(value, env));
53168
+ break;
53169
+ }
53170
+ }
53171
+ if (!host) {
53172
+ throw new Error("ssh_config_ref_invalid: SSH Config \u5F15\u7528\u672A\u89E3\u6790\u5230 hostname");
53173
+ }
53174
+ return {
53175
+ host,
53176
+ port,
53177
+ username,
53178
+ identityAgent,
53179
+ identityFiles
53180
+ };
53181
+ }
53182
+ function toSshAuthEnv(env) {
53183
+ return {
53184
+ SSH_AUTH_SOCK: env.SSH_AUTH_SOCK,
53185
+ USER: env.USER,
53186
+ LOGNAME: env.LOGNAME
53187
+ };
53188
+ }
53189
+ function resolveAgentFromConfig(identityAgent, deps) {
53190
+ const trimmed = identityAgent?.trim();
53191
+ if (!trimmed || trimmed.toLowerCase() === "none") {
53192
+ return;
53193
+ }
53194
+ if (trimmed === "SSH_AUTH_SOCK" || trimmed === "$SSH_AUTH_SOCK") {
53195
+ return resolveSshAgentSocket("auto", toSshAuthEnv(deps.env));
53196
+ }
53197
+ const expanded = expandHomePath(trimmed, deps.env);
53198
+ return deps.fileExists(expanded) ? expanded : undefined;
53199
+ }
53200
+ function resolvePrivateKeyFromConfig(identityFiles, deps) {
53201
+ for (const identityFile of identityFiles) {
53202
+ if (!deps.fileExists(identityFile)) {
53203
+ continue;
53204
+ }
53205
+ return deps.readTextFile(identityFile);
53206
+ }
53207
+ return;
53208
+ }
53209
+ function resolveSshConfigRef(device, deps) {
53210
+ const ref = device.sshConfigRef?.trim();
53211
+ if (!ref) {
53212
+ return null;
53213
+ }
53214
+ const result = deps.runSync(["ssh", "-G", ref]);
53215
+ if (result.exitCode !== 0) {
53216
+ const detail = result.stderr.trim() || result.stdout.trim() || ref;
53217
+ throw new Error(`ssh_config_ref_resolve_failed: ${detail}`);
53218
+ }
53219
+ return parseSshConfigOutput(result.stdout, deps.env);
53220
+ }
53221
+ async function resolveSshConnectConfig(device, decrypt2, inputDeps = {}) {
53222
+ const deps = {
53223
+ env: inputDeps.env ?? process.env,
53224
+ runSync: inputDeps.runSync ?? defaultRunSync2,
53225
+ fileExists: inputDeps.fileExists ?? existsSync2,
53226
+ readTextFile: inputDeps.readTextFile ?? ((path) => readFileSync(path, "utf8"))
53227
+ };
53228
+ const sshEnv = toSshAuthEnv(deps.env);
53229
+ const resolvedConfig = resolveSshConfigRef(device, deps);
53230
+ const host = resolvedConfig?.host ?? device.host;
53231
+ const port = resolvedConfig?.port ?? device.port ?? 22;
53232
+ const username = resolvedConfig?.username ?? resolveSshUsername(device.username, device.authMode, sshEnv);
53233
+ if (!host) {
53234
+ throw new Error("SSH device missing host");
53235
+ }
53236
+ const authConfig = {
53237
+ host,
53238
+ port,
53239
+ username
53240
+ };
53241
+ const configAgent = resolveAgentFromConfig(resolvedConfig?.identityAgent, deps);
53242
+ const envAgent = resolveSshAgentSocket("auto", sshEnv);
53243
+ const configPrivateKey = resolvePrivateKeyFromConfig(resolvedConfig?.identityFiles ?? [], deps);
53244
+ switch (device.authMode) {
53245
+ case "password": {
53246
+ if (!device.passwordEnc) {
53247
+ throw new Error("auth_password_missing: \u5BC6\u7801\u8BA4\u8BC1\u672A\u63D0\u4F9B\u5BC6\u7801");
53248
+ }
53249
+ authConfig.password = await decrypt2(device.passwordEnc, {
53250
+ scope: "device",
53251
+ entityId: device.id,
53252
+ field: "password_enc"
53253
+ });
53254
+ break;
53255
+ }
53256
+ case "key": {
53257
+ if (!device.privateKeyEnc) {
53258
+ throw new Error("auth_key_missing: \u79C1\u94A5\u8BA4\u8BC1\u672A\u63D0\u4F9B\u79C1\u94A5");
53259
+ }
53260
+ authConfig.privateKey = await decrypt2(device.privateKeyEnc, {
53261
+ scope: "device",
53262
+ entityId: device.id,
53263
+ field: "private_key_enc"
53264
+ });
53265
+ if (device.privateKeyPassphraseEnc) {
53266
+ authConfig.passphrase = await decrypt2(device.privateKeyPassphraseEnc, {
53267
+ scope: "device",
53268
+ entityId: device.id,
53269
+ field: "private_key_passphrase_enc"
53270
+ });
53271
+ }
53272
+ break;
53273
+ }
53274
+ case "agent": {
53275
+ authConfig.agent = configAgent ?? resolveSshAgentSocket("agent", sshEnv);
53276
+ break;
53277
+ }
53278
+ case "configRef": {
53279
+ if (!resolvedConfig) {
53280
+ throw new Error("ssh_config_ref_missing: SSH Config \u5F15\u7528\u4E0D\u80FD\u4E3A\u7A7A");
53281
+ }
53282
+ if (configAgent ?? envAgent) {
53283
+ authConfig.agent = configAgent ?? envAgent;
53284
+ }
53285
+ if (configPrivateKey) {
53286
+ authConfig.privateKey = configPrivateKey;
53287
+ }
53288
+ if (!authConfig.agent && !authConfig.privateKey) {
53289
+ throw new Error("ssh_config_ref_auth_missing: SSH Config \u5F15\u7528\u672A\u89E3\u6790\u5230\u53EF\u7528\u8BA4\u8BC1\u65B9\u5F0F\uFF08IdentityAgent / IdentityFile / SSH_AUTH_SOCK\uFF09");
53290
+ }
53291
+ break;
53292
+ }
53293
+ case "auto": {
53294
+ if (configAgent ?? envAgent) {
53295
+ authConfig.agent = configAgent ?? envAgent;
53296
+ }
53297
+ if (device.privateKeyEnc) {
53298
+ authConfig.privateKey = await decrypt2(device.privateKeyEnc, {
53299
+ scope: "device",
53300
+ entityId: device.id,
53301
+ field: "private_key_enc"
53302
+ });
53303
+ } else if (configPrivateKey) {
53304
+ authConfig.privateKey = configPrivateKey;
53305
+ } else if (device.passwordEnc) {
53306
+ authConfig.password = await decrypt2(device.passwordEnc, {
53307
+ scope: "device",
53308
+ entityId: device.id,
53309
+ field: "password_enc"
53310
+ });
53311
+ }
53312
+ break;
53313
+ }
53314
+ }
53315
+ if (device.authMode === "auto" && !authConfig.agent && !authConfig.privateKey && !authConfig.password) {
53316
+ throw new Error("auth_auto_missing: auto \u6A21\u5F0F\u4E0B\u672A\u627E\u5230\u53EF\u7528\u8BA4\u8BC1\u65B9\u5F0F\uFF08SSH_AUTH_SOCK / \u79C1\u94A5 / \u5BC6\u7801\uFF09");
53317
+ }
53318
+ return authConfig;
53319
+ }
53320
+
52768
53321
  // ../../apps/gateway/src/tmux-client/ssh-bootstrap.ts
52769
53322
  function buildSshBootstrapScript() {
52770
53323
  return [
@@ -52810,6 +53363,31 @@ function hasRenderableTerminalContent2(value) {
52810
53363
  }
52811
53364
  var BELL_DEDUP_WINDOW_MS2 = 200;
52812
53365
  var COMMAND_SENTINEL = "\x1ETMEX_END ";
53366
+ var SNAPSHOT_FIELD_SEPARATOR = "|";
53367
+ function splitSnapshotFields(line, fieldCount) {
53368
+ const parts = line.split(SNAPSHOT_FIELD_SEPARATOR);
53369
+ if (parts.length <= fieldCount) {
53370
+ return parts;
53371
+ }
53372
+ if (fieldCount === 2) {
53373
+ return [parts[0] ?? "", parts.slice(1).join(SNAPSHOT_FIELD_SEPARATOR)];
53374
+ }
53375
+ if (fieldCount === 4) {
53376
+ return [parts[0] ?? "", parts[1] ?? "", parts.slice(2, -1).join(SNAPSHOT_FIELD_SEPARATOR), parts.at(-1) ?? ""];
53377
+ }
53378
+ if (fieldCount === 7) {
53379
+ return [
53380
+ parts[0] ?? "",
53381
+ parts[1] ?? "",
53382
+ parts[2] ?? "",
53383
+ parts.slice(3, -3).join(SNAPSHOT_FIELD_SEPARATOR),
53384
+ parts.at(-3) ?? "",
53385
+ parts.at(-2) ?? "",
53386
+ parts.at(-1) ?? ""
53387
+ ];
53388
+ }
53389
+ return parts;
53390
+ }
52813
53391
 
52814
53392
  class SshExternalTmuxConnection {
52815
53393
  deviceId;
@@ -52826,8 +53404,7 @@ class SshExternalTmuxConnection {
52826
53404
  pendingPaneTitles = new Map;
52827
53405
  snapshotSession = null;
52828
53406
  snapshotWindows = new Map;
52829
- currentPipePaneId = null;
52830
- pipeReadAbort = null;
53407
+ paneReaders = new Map;
52831
53408
  pipeTransition = Promise.resolve();
52832
53409
  hookReadAbort = null;
52833
53410
  hookBuffer = "";
@@ -52873,6 +53450,7 @@ class SshExternalTmuxConnection {
52873
53450
  await this.openCommandChannel();
52874
53451
  await this.ensureRemoteRuntimeDirs();
52875
53452
  await this.ensureSession();
53453
+ await this.configureSessionOptions();
52876
53454
  await this.startHooks();
52877
53455
  this.connected = true;
52878
53456
  updateDeviceRuntimeStatus(this.deviceId, {
@@ -52938,7 +53516,7 @@ class SshExternalTmuxConnection {
52938
53516
  if (!this.connected) {
52939
53517
  return;
52940
53518
  }
52941
- const argv = name ? ["new-window", "-n", name] : ["new-window"];
53519
+ const argv = name ? ["new-window", "-t", this.sessionName, "-n", name] : ["new-window", "-t", this.sessionName];
52942
53520
  this.runAndRefresh(argv).catch((error) => {
52943
53521
  this.callbacks.onError(error);
52944
53522
  });
@@ -52971,80 +53549,7 @@ class SshExternalTmuxConnection {
52971
53549
  if (!this.device) {
52972
53550
  throw new Error("SSH device not loaded");
52973
53551
  }
52974
- const host = this.device.host;
52975
- const port = this.device.port ?? 22;
52976
- const username = resolveSshUsername(this.device.username, this.device.authMode);
52977
- if (this.device.authMode === "configRef" || !host && this.device.sshConfigRef) {
52978
- throw new Error("ssh_config_ref_not_supported: \u5F53\u524D\u7248\u672C\u6682\u4E0D\u652F\u6301 SSH Config \u5F15\u7528\uFF0C\u8BF7\u6539\u4E3A\u586B\u5199 host + username\uFF0C\u5E76\u9009\u62E9 Agent/\u79C1\u94A5/\u5BC6\u7801\u8BA4\u8BC1");
52979
- }
52980
- if (!host) {
52981
- throw new Error("SSH device missing host");
52982
- }
52983
- const authConfig = {
52984
- host,
52985
- port,
52986
- username
52987
- };
52988
- switch (this.device.authMode) {
52989
- case "password": {
52990
- if (!this.device.passwordEnc) {
52991
- throw new Error("auth_password_missing: \u5BC6\u7801\u8BA4\u8BC1\u672A\u63D0\u4F9B\u5BC6\u7801");
52992
- }
52993
- authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
52994
- scope: "device",
52995
- entityId: this.device.id,
52996
- field: "password_enc"
52997
- });
52998
- break;
52999
- }
53000
- case "key": {
53001
- if (!this.device.privateKeyEnc) {
53002
- throw new Error("auth_key_missing: \u79C1\u94A5\u8BA4\u8BC1\u672A\u63D0\u4F9B\u79C1\u94A5");
53003
- }
53004
- authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
53005
- scope: "device",
53006
- entityId: this.device.id,
53007
- field: "private_key_enc"
53008
- });
53009
- if (this.device.privateKeyPassphraseEnc) {
53010
- authConfig.passphrase = await this.deps.decrypt(this.device.privateKeyPassphraseEnc, {
53011
- scope: "device",
53012
- entityId: this.device.id,
53013
- field: "private_key_passphrase_enc"
53014
- });
53015
- }
53016
- break;
53017
- }
53018
- case "agent": {
53019
- authConfig.agent = resolveSshAgentSocket("agent");
53020
- break;
53021
- }
53022
- case "auto": {
53023
- const agentSocket = resolveSshAgentSocket("auto");
53024
- if (agentSocket) {
53025
- authConfig.agent = agentSocket;
53026
- }
53027
- if (this.device.privateKeyEnc) {
53028
- authConfig.privateKey = await this.deps.decrypt(this.device.privateKeyEnc, {
53029
- scope: "device",
53030
- entityId: this.device.id,
53031
- field: "private_key_enc"
53032
- });
53033
- } else if (this.device.passwordEnc) {
53034
- authConfig.password = await this.deps.decrypt(this.device.passwordEnc, {
53035
- scope: "device",
53036
- entityId: this.device.id,
53037
- field: "password_enc"
53038
- });
53039
- }
53040
- break;
53041
- }
53042
- case "configRef":
53043
- break;
53044
- }
53045
- if (this.device.authMode === "auto" && !authConfig.agent && !authConfig.privateKey && !authConfig.password) {
53046
- throw new Error("auth_auto_missing: auto \u6A21\u5F0F\u4E0B\u672A\u627E\u5230\u53EF\u7528\u8BA4\u8BC1\u65B9\u5F0F\uFF08SSH_AUTH_SOCK / \u79C1\u94A5 / \u5BC6\u7801\uFF09");
53047
- }
53552
+ const authConfig = await resolveSshConnectConfig(this.device, this.deps.decrypt);
53048
53553
  const client = this.deps.createClient();
53049
53554
  this.sshClient = client;
53050
53555
  await new Promise((resolve, reject) => {
@@ -53154,6 +53659,26 @@ class SshExternalTmuxConnection {
53154
53659
  }
53155
53660
  await this.runTmux(["new-session", "-d", "-c", this.remoteHomeDir, "-s", this.sessionName]);
53156
53661
  }
53662
+ async configureSessionOptions() {
53663
+ await this.runTmuxAllowFailure([
53664
+ "set-option",
53665
+ "-t",
53666
+ this.sessionName,
53667
+ "-s",
53668
+ "allow-passthrough",
53669
+ config.tmuxAllowPassthrough ? "on" : "off"
53670
+ ]);
53671
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "extended-keys", "on"]);
53672
+ await this.runTmuxAllowFailure([
53673
+ "set-option",
53674
+ "-t",
53675
+ this.sessionName,
53676
+ "-s",
53677
+ "extended-keys-format",
53678
+ "csi-u"
53679
+ ]);
53680
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
53681
+ }
53157
53682
  async startHooks() {
53158
53683
  await this.ensureRemoteRuntimeDirs();
53159
53684
  const fifoPath = this.fsPaths.hookFifoPath;
@@ -53172,14 +53697,16 @@ class SshExternalTmuxConnection {
53172
53697
  stopReader();
53173
53698
  this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53174
53699
  };
53175
- await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
53176
53700
  await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
53177
53701
  await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
53702
+ await this.installHook("after-new-window", ["refresh"]);
53703
+ await this.installHook("after-split-window", ["refresh"]);
53178
53704
  }
53179
53705
  async stopHooks() {
53180
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
53181
53706
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
53182
53707
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
53708
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-new-window"]);
53709
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-split-window"]);
53183
53710
  this.hookReadAbort?.();
53184
53711
  this.hookReadAbort = null;
53185
53712
  this.hookBuffer = "";
@@ -53210,22 +53737,9 @@ class SshExternalTmuxConnection {
53210
53737
  }
53211
53738
  const [type, windowId, paneId] = line.split("\t");
53212
53739
  if (type === "bell") {
53213
- const key = paneId || windowId || "-";
53214
- const previous = this.bellDedup.get(key) ?? 0;
53215
- const now = Date.now();
53216
- if (now - previous >= BELL_DEDUP_WINDOW_MS2) {
53217
- this.bellDedup.set(key, now);
53218
- this.callbacks.onEvent({
53219
- type: "bell",
53220
- data: {
53221
- windowId: windowId || undefined,
53222
- paneId: paneId || this.activePaneId || undefined
53223
- }
53224
- });
53225
- }
53226
53740
  continue;
53227
53741
  }
53228
- if (type === "pane-exited" || type === "pane-died") {
53742
+ if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
53229
53743
  this.requestSnapshot();
53230
53744
  }
53231
53745
  }
@@ -53256,7 +53770,6 @@ class SshExternalTmuxConnection {
53256
53770
  this.activePaneId = paneId;
53257
53771
  await this.runTmux(["select-window", "-t", windowId], true);
53258
53772
  await this.runTmux(["select-pane", "-t", paneId], true);
53259
- await this.startPipeForPane(paneId);
53260
53773
  if (size) {
53261
53774
  await this.resizePaneInternal(paneId, size.cols, size.rows);
53262
53775
  }
@@ -53270,8 +53783,8 @@ class SshExternalTmuxConnection {
53270
53783
  async capturePaneHistory(paneId) {
53271
53784
  const mode = (await this.runTmux(["display-message", "-p", "-t", paneId, "#{alternate_on}"], true)).stdout.trim();
53272
53785
  const alternateScreen = mode === "1";
53273
- const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-p"], true, 30000)).stdout;
53274
- const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-p", "-q"], true, 30000)).stdout;
53786
+ const normal = (await this.runTmux(["capture-pane", "-t", paneId, "-S", "-", "-E", "-", "-e", "-N", "-p"], true, 30000)).stdout;
53787
+ const alternate = (await this.runTmux(["capture-pane", "-t", paneId, "-a", "-S", "-", "-E", "-", "-e", "-N", "-p", "-q"], true, 30000)).stdout;
53275
53788
  const history = alternateScreen ? hasRenderableTerminalContent2(normal) ? normal : alternate : normal || alternate;
53276
53789
  if (history) {
53277
53790
  this.callbacks.onTerminalHistory(paneId, history, alternateScreen);
@@ -53287,21 +53800,21 @@ class SshExternalTmuxConnection {
53287
53800
  "-p",
53288
53801
  "-t",
53289
53802
  this.sessionName,
53290
- "#{session_id}\t#{session_name}"
53803
+ "#{session_id}|#{session_name}"
53291
53804
  ]),
53292
53805
  this.runTmuxAllowFailure([
53293
53806
  "list-windows",
53294
53807
  "-t",
53295
53808
  this.sessionName,
53296
53809
  "-F",
53297
- "#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}"
53810
+ "#{window_id}|#{window_index}|#{window_name}|#{window_active}"
53298
53811
  ]),
53299
53812
  this.runTmuxAllowFailure([
53300
53813
  "list-panes",
53301
53814
  "-t",
53302
53815
  this.sessionName,
53303
53816
  "-F",
53304
- "#{pane_id}\t#{window_id}\t#{pane_index}\t#{pane_title}\t#{pane_active}\t#{pane_width}\t#{pane_height}"
53817
+ "#{pane_id}|#{window_id}|#{pane_index}|#{pane_title}|#{pane_active}|#{pane_width}|#{pane_height}"
53305
53818
  ])
53306
53819
  ]);
53307
53820
  if (sessionRes.exitCode !== 0 || windowsRes.exitCode !== 0 || panesRes.exitCode !== 0) {
@@ -53311,6 +53824,7 @@ class SshExternalTmuxConnection {
53311
53824
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
53312
53825
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
53313
53826
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
53827
+ await this.syncPipeReaders();
53314
53828
  this.emitSnapshot();
53315
53829
  }
53316
53830
  parseSnapshotSession(lines) {
@@ -53319,7 +53833,7 @@ class SshExternalTmuxConnection {
53319
53833
  if (!line.trim()) {
53320
53834
  continue;
53321
53835
  }
53322
- const [id, name] = line.split("\t");
53836
+ const [id, name] = splitSnapshotFields(line, 2);
53323
53837
  if (id) {
53324
53838
  this.snapshotSession = { id, name: name ?? "" };
53325
53839
  }
@@ -53332,7 +53846,7 @@ class SshExternalTmuxConnection {
53332
53846
  if (!line.trim()) {
53333
53847
  continue;
53334
53848
  }
53335
- const [id, indexRaw, name, activeRaw] = line.split("\t");
53849
+ const [id, indexRaw, name, activeRaw] = splitSnapshotFields(line, 4);
53336
53850
  if (!id) {
53337
53851
  continue;
53338
53852
  }
@@ -53358,7 +53872,7 @@ class SshExternalTmuxConnection {
53358
53872
  if (!line.trim()) {
53359
53873
  continue;
53360
53874
  }
53361
- const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = line.split("\t");
53875
+ const [paneId, windowId, indexRaw, titleRaw, activeRaw, widthRaw, heightRaw] = splitSnapshotFields(line, 7);
53362
53876
  if (!paneId || !windowId) {
53363
53877
  continue;
53364
53878
  }
@@ -53408,56 +53922,114 @@ class SshExternalTmuxConnection {
53408
53922
  }
53409
53923
  return null;
53410
53924
  }
53411
- async startPipeForPane(paneId) {
53412
- await this.queuePipeTransition(async () => {
53413
- if (this.currentPipePaneId === paneId) {
53414
- return;
53925
+ recordBell(paneId, windowId) {
53926
+ const key = paneId || windowId || "-";
53927
+ const previous = this.bellDedup.get(key) ?? 0;
53928
+ const now = Date.now();
53929
+ if (now - previous < BELL_DEDUP_WINDOW_MS2) {
53930
+ return;
53931
+ }
53932
+ this.bellDedup.set(key, now);
53933
+ this.callbacks.onEvent({
53934
+ type: "bell",
53935
+ data: {
53936
+ windowId,
53937
+ paneId: paneId || this.activePaneId || undefined
53938
+ }
53939
+ });
53940
+ }
53941
+ emitNotification(paneId, notification) {
53942
+ this.callbacks.onEvent({
53943
+ type: "notification",
53944
+ data: {
53945
+ paneId,
53946
+ ...notification
53947
+ }
53948
+ });
53949
+ }
53950
+ getExpectedPaneIds() {
53951
+ return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
53952
+ }
53953
+ async startPipeForPaneNow(paneId) {
53954
+ if (this.paneReaders.has(paneId)) {
53955
+ return;
53956
+ }
53957
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
53958
+ await this.ensureRemoteRuntimeDirs();
53959
+ await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53960
+ const parser = createPaneStreamParser({
53961
+ onTitle: (title) => {
53962
+ this.pendingPaneTitles.set(paneId, title);
53963
+ this.requestSnapshot();
53964
+ },
53965
+ onBell: () => {
53966
+ this.recordBell(paneId);
53967
+ },
53968
+ onNotification: (notification) => {
53969
+ this.emitNotification(paneId, notification);
53415
53970
  }
53416
- await this.stopPipeNow();
53417
- const fifoPath = this.fsPaths.paneFifoPath(paneId);
53418
- await this.ensureRemoteRuntimeDirs();
53419
- await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53420
- const parser = createPaneTitleParser({
53421
- onTitle: (title) => {
53422
- this.pendingPaneTitles.set(paneId, title);
53423
- this.requestSnapshot();
53971
+ });
53972
+ const stopReader = await this.openReaderChannel(`exec cat ${quoteShellArg(fifoPath)}`, {
53973
+ onData: (raw) => {
53974
+ const output = parser.push(raw);
53975
+ if (output.length > 0) {
53976
+ this.callbacks.onTerminalOutput(paneId, output);
53424
53977
  }
53425
- });
53426
- const stopReader = await this.openReaderChannel(`exec cat ${quoteShellArg(fifoPath)}`, {
53427
- onData: (raw) => {
53428
- const output = parser.push(raw);
53429
- if (Array.from(raw).includes(7)) {
53430
- this.callbacks.onEvent({ type: "bell", data: { paneId } });
53431
- }
53432
- if (output.length > 0) {
53433
- this.callbacks.onTerminalOutput(paneId, output);
53434
- }
53435
- },
53436
- onClose: () => {
53437
- if (!this.manualDisconnect && this.currentPipePaneId === paneId) {
53438
- this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
53439
- }
53978
+ },
53979
+ onClose: () => {
53980
+ if (!this.manualDisconnect && this.paneReaders.has(paneId)) {
53981
+ this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
53440
53982
  }
53441
- });
53442
- this.pipeReadAbort = () => {
53983
+ }
53984
+ });
53985
+ const handle = {
53986
+ paneId,
53987
+ fifoPath,
53988
+ stopReader: () => {
53443
53989
  stopReader();
53444
53990
  this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53445
- };
53991
+ }
53992
+ };
53993
+ this.paneReaders.set(paneId, handle);
53994
+ try {
53446
53995
  await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
53447
- this.currentPipePaneId = paneId;
53448
- });
53449
- }
53450
- async stopPipe() {
53451
- await this.queuePipeTransition(() => this.stopPipeNow());
53996
+ } catch (error) {
53997
+ this.paneReaders.delete(paneId);
53998
+ handle.stopReader();
53999
+ throw error;
54000
+ }
53452
54001
  }
53453
- async stopPipeNow() {
53454
- const paneId = this.currentPipePaneId;
53455
- this.currentPipePaneId = null;
53456
- if (paneId) {
53457
- await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
54002
+ async stopPipeForPaneNow(paneId) {
54003
+ const handle = this.paneReaders.get(paneId);
54004
+ if (!handle) {
54005
+ return;
53458
54006
  }
53459
- this.pipeReadAbort?.();
53460
- this.pipeReadAbort = null;
54007
+ this.paneReaders.delete(paneId);
54008
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
54009
+ handle.stopReader();
54010
+ }
54011
+ async syncPipeReaders() {
54012
+ const expectedPaneIds = this.getExpectedPaneIds();
54013
+ const expectedSet = new Set(expectedPaneIds);
54014
+ await this.queuePipeTransition(async () => {
54015
+ for (const paneId of Array.from(this.paneReaders.keys())) {
54016
+ if (!expectedSet.has(paneId)) {
54017
+ await this.stopPipeForPaneNow(paneId);
54018
+ }
54019
+ }
54020
+ for (const paneId of expectedPaneIds) {
54021
+ if (!this.paneReaders.has(paneId)) {
54022
+ await this.startPipeForPaneNow(paneId);
54023
+ }
54024
+ }
54025
+ });
54026
+ }
54027
+ async stopAllPipeReaders() {
54028
+ await this.queuePipeTransition(async () => {
54029
+ for (const paneId of Array.from(this.paneReaders.keys())) {
54030
+ await this.stopPipeForPaneNow(paneId);
54031
+ }
54032
+ });
53461
54033
  }
53462
54034
  queuePipeTransition(task) {
53463
54035
  const next = this.pipeTransition.catch(() => {
@@ -53627,7 +54199,7 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
53627
54199
  }
53628
54200
  this.connected = false;
53629
54201
  this.cleanupPromise = (async () => {
53630
- await this.stopPipe().catch(() => {
54202
+ await this.stopAllPipeReaders().catch(() => {
53631
54203
  return;
53632
54204
  });
53633
54205
  await this.stopHooks().catch(() => {
@@ -53702,6 +54274,7 @@ class DeviceSessionRuntime {
53702
54274
  }
53703
54275
  this.closeEmitted = true;
53704
54276
  this.terminated = true;
54277
+ this.connectPromise = null;
53705
54278
  this.broadcast((listener) => listener.onClose?.());
53706
54279
  }
53707
54280
  });
@@ -53713,7 +54286,7 @@ class DeviceSessionRuntime {
53713
54286
  };
53714
54287
  }
53715
54288
  async connect() {
53716
- if (this.terminated && !this.connectPromise) {
54289
+ if (this.terminated) {
53717
54290
  return Promise.reject(new Error(`Device session runtime already terminated: ${this.deviceId}`));
53718
54291
  }
53719
54292
  if (this.connectPromise) {
@@ -53721,6 +54294,7 @@ class DeviceSessionRuntime {
53721
54294
  }
53722
54295
  this.connectPromise = this.connection.connect().catch((error) => {
53723
54296
  this.terminated = true;
54297
+ this.connectPromise = null;
53724
54298
  throw error;
53725
54299
  });
53726
54300
  return this.connectPromise;
@@ -53731,6 +54305,7 @@ class DeviceSessionRuntime {
53731
54305
  }
53732
54306
  this.terminated = true;
53733
54307
  this.manualDisconnect = true;
54308
+ this.connectPromise = null;
53734
54309
  this.connection.disconnect();
53735
54310
  }
53736
54311
  async shutdown() {
@@ -53853,7 +54428,7 @@ function pickPaneById(windows, paneId) {
53853
54428
  }
53854
54429
  return null;
53855
54430
  }
53856
- function resolveBellContext(options) {
54431
+ function resolvePaneContext(options) {
53857
54432
  const { deviceId, snapshot, rawData } = options;
53858
54433
  const raw = rawData ?? {};
53859
54434
  const bellWindowId = typeof raw.windowId === "string" && raw.windowId ? raw.windowId : undefined;
@@ -53928,6 +54503,34 @@ var defaultDeps = {
53928
54503
  }
53929
54504
  });
53930
54505
  },
54506
+ async notifyNotification(context) {
54507
+ const { device, settings, notification } = context;
54508
+ await eventNotifier.notify("terminal_notification", {
54509
+ site: {
54510
+ name: settings.siteName,
54511
+ url: settings.siteUrl
54512
+ },
54513
+ device: {
54514
+ id: device.id,
54515
+ name: device.name,
54516
+ type: device.type,
54517
+ host: device.host
54518
+ },
54519
+ tmux: {
54520
+ sessionName: device.session,
54521
+ windowId: notification.windowId,
54522
+ paneId: notification.paneId,
54523
+ windowIndex: notification.windowIndex,
54524
+ paneIndex: notification.paneIndex,
54525
+ paneUrl: notification.paneUrl
54526
+ },
54527
+ payload: {
54528
+ source: notification.source,
54529
+ title: notification.title,
54530
+ message: notification.body
54531
+ }
54532
+ });
54533
+ },
53931
54534
  fallbackReconnectDelayMs: 60000
53932
54535
  };
53933
54536
 
@@ -54122,29 +54725,186 @@ class PushSupervisor {
54122
54725
  if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
54123
54726
  return;
54124
54727
  }
54125
- if (event.type !== "bell") {
54126
- return;
54127
- }
54128
54728
  const device = this.deps.getDevice(deviceId);
54129
54729
  if (!device) {
54130
54730
  return;
54131
54731
  }
54132
54732
  const settings = this.deps.getSettings();
54133
- const bell = resolveBellContext({
54733
+ const paneContext = resolvePaneContext({
54134
54734
  deviceId,
54135
54735
  siteUrl: settings.siteUrl,
54136
54736
  snapshot: entry.lastSnapshot,
54137
54737
  rawData: event.data
54138
54738
  });
54139
- await this.deps.notifyBell({
54140
- device,
54141
- settings,
54142
- bell
54143
- });
54739
+ if (event.type === "bell") {
54740
+ await this.deps.notifyBell({
54741
+ device,
54742
+ settings,
54743
+ bell: paneContext
54744
+ });
54745
+ return;
54746
+ }
54747
+ if (event.type === "notification") {
54748
+ const raw = event.data ?? {};
54749
+ const title = typeof raw.title === "string" && raw.title ? raw.title : undefined;
54750
+ const body = typeof raw.body === "string" ? raw.body : "";
54751
+ if (!title && !body) {
54752
+ return;
54753
+ }
54754
+ const source = raw.source === "osc9" || raw.source === "osc777" || raw.source === "osc1337" ? raw.source : "osc9";
54755
+ await this.deps.notifyNotification({
54756
+ device,
54757
+ settings,
54758
+ notification: {
54759
+ ...paneContext,
54760
+ source,
54761
+ title,
54762
+ body
54763
+ }
54764
+ });
54765
+ }
54144
54766
  }
54145
54767
  }
54146
54768
  var pushSupervisor = new PushSupervisor;
54147
54769
 
54770
+ // ../../apps/gateway/src/ws/error-classify.ts
54771
+ function classifySshError(error) {
54772
+ const msg = error.message.toLowerCase();
54773
+ if (msg.includes("ssh_config_ref_not_supported")) {
54774
+ return {
54775
+ type: "ssh_config_ref_not_supported",
54776
+ messageKey: "sshError.configRefNotSupported"
54777
+ };
54778
+ }
54779
+ if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
54780
+ return {
54781
+ type: "agent_unavailable",
54782
+ messageKey: "sshError.agentUnavailable"
54783
+ };
54784
+ }
54785
+ if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
54786
+ return {
54787
+ type: "agent_no_identity",
54788
+ messageKey: "sshError.agentNoIdentities"
54789
+ };
54790
+ }
54791
+ if (msg.includes("permission denied")) {
54792
+ return {
54793
+ type: "auth_failed",
54794
+ messageKey: "sshError.authFailed"
54795
+ };
54796
+ }
54797
+ if (msg.includes("all configured authentication methods failed")) {
54798
+ return {
54799
+ type: "auth_failed",
54800
+ messageKey: "sshError.authFailedGeneric"
54801
+ };
54802
+ }
54803
+ if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
54804
+ return {
54805
+ type: "network_unreachable",
54806
+ messageKey: "sshError.networkUnreachable"
54807
+ };
54808
+ }
54809
+ if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
54810
+ return {
54811
+ type: "connection_refused",
54812
+ messageKey: "sshError.connectionRefused"
54813
+ };
54814
+ }
54815
+ if (msg.includes("timeout") || msg.includes("etimedout")) {
54816
+ return {
54817
+ type: "timeout",
54818
+ messageKey: "sshError.connectionTimeout"
54819
+ };
54820
+ }
54821
+ if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
54822
+ return {
54823
+ type: "host_not_found",
54824
+ messageKey: "sshError.hostNotFound"
54825
+ };
54826
+ }
54827
+ if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
54828
+ return {
54829
+ type: "handshake_failed",
54830
+ messageKey: "sshError.handshakeFailed"
54831
+ };
54832
+ }
54833
+ if (msg.includes("remote tmux unavailable") || msg.includes("tmux_not_found") || msg.includes("tmux: command not found") || msg.includes("tmux control mode not ready") || msg.includes("tmux exited") || msg.includes("tmux_exec_failed")) {
54834
+ return {
54835
+ type: "tmux_unavailable",
54836
+ messageKey: "sshError.tmuxUnavailable"
54837
+ };
54838
+ }
54839
+ return {
54840
+ type: "unknown",
54841
+ messageKey: "sshError.unknown",
54842
+ messageParams: { message: error.message }
54843
+ };
54844
+ }
54845
+
54846
+ // ../../apps/gateway/src/api/test-connection.ts
54847
+ function inferFailurePhase(errorType) {
54848
+ if (errorType === "tmux_unavailable") {
54849
+ return "bootstrap";
54850
+ }
54851
+ return "connect";
54852
+ }
54853
+ function json(data, status = 200) {
54854
+ return new Response(JSON.stringify(data), {
54855
+ status,
54856
+ headers: {
54857
+ "Content-Type": "application/json"
54858
+ }
54859
+ });
54860
+ }
54861
+ async function handleDeviceTestConnection(deviceId, inputDeps = {}) {
54862
+ const deps = {
54863
+ getDevice: inputDeps.getDevice ?? ((currentDeviceId) => getDeviceById(currentDeviceId)),
54864
+ acquireRuntime: inputDeps.acquireRuntime ?? ((currentDeviceId) => tmuxRuntimeRegistry.acquire(currentDeviceId)),
54865
+ releaseRuntime: inputDeps.releaseRuntime ?? (async (currentDeviceId) => {
54866
+ await tmuxRuntimeRegistry.release(currentDeviceId);
54867
+ }),
54868
+ translate: inputDeps.translate ?? t2
54869
+ };
54870
+ const device = deps.getDevice(deviceId);
54871
+ if (!device) {
54872
+ return json({ error: deps.translate("apiError.deviceNotFound") }, 404);
54873
+ }
54874
+ const classifyErrorResponse = (error) => {
54875
+ const rawMessage = error instanceof Error ? error.message : String(error);
54876
+ const classified = classifySshError(new Error(rawMessage));
54877
+ const payload = {
54878
+ success: false,
54879
+ tmuxAvailable: false,
54880
+ phase: inferFailurePhase(classified.type),
54881
+ errorType: classified.type,
54882
+ message: deps.translate(classified.messageKey, classified.messageParams),
54883
+ rawMessage
54884
+ };
54885
+ return json(payload);
54886
+ };
54887
+ let runtime = null;
54888
+ try {
54889
+ runtime = await deps.acquireRuntime(deviceId);
54890
+ await runtime.connect();
54891
+ runtime.requestSnapshot();
54892
+ const payload = {
54893
+ success: true,
54894
+ tmuxAvailable: true,
54895
+ phase: "ready",
54896
+ message: deps.translate("common.success")
54897
+ };
54898
+ return json(payload);
54899
+ } catch (error) {
54900
+ return classifyErrorResponse(error);
54901
+ } finally {
54902
+ if (runtime) {
54903
+ await deps.releaseRuntime(deviceId, runtime);
54904
+ }
54905
+ }
54906
+ }
54907
+
54148
54908
  // ../../apps/gateway/src/api/index.ts
54149
54909
  function shouldReconnectPushSupervisor(existing, updates) {
54150
54910
  if (updates.type !== undefined && updates.type !== existing.type)
@@ -54191,18 +54951,37 @@ function normalizeSiteSettingsInput(body) {
54191
54951
  }
54192
54952
  updates.bellThrottleSeconds = value;
54193
54953
  }
54954
+ if (body.notificationThrottleSeconds !== undefined) {
54955
+ const value = Math.floor(Number(body.notificationThrottleSeconds));
54956
+ if (Number.isNaN(value) || value < 0 || value > 300) {
54957
+ throw new Error(t2("apiError.bellThrottleInvalid"));
54958
+ }
54959
+ updates.notificationThrottleSeconds = value;
54960
+ }
54194
54961
  if (body.enableBrowserBellToast !== undefined) {
54195
54962
  if (typeof body.enableBrowserBellToast !== "boolean") {
54196
54963
  throw new Error(t2("apiError.invalidRequest"));
54197
54964
  }
54198
54965
  updates.enableBrowserBellToast = body.enableBrowserBellToast;
54199
54966
  }
54967
+ if (body.enableBrowserNotificationToast !== undefined) {
54968
+ if (typeof body.enableBrowserNotificationToast !== "boolean") {
54969
+ throw new Error(t2("apiError.invalidRequest"));
54970
+ }
54971
+ updates.enableBrowserNotificationToast = body.enableBrowserNotificationToast;
54972
+ }
54200
54973
  if (body.enableTelegramBellPush !== undefined) {
54201
54974
  if (typeof body.enableTelegramBellPush !== "boolean") {
54202
54975
  throw new Error(t2("apiError.invalidRequest"));
54203
54976
  }
54204
54977
  updates.enableTelegramBellPush = body.enableTelegramBellPush;
54205
54978
  }
54979
+ if (body.enableTelegramNotificationPush !== undefined) {
54980
+ if (typeof body.enableTelegramNotificationPush !== "boolean") {
54981
+ throw new Error(t2("apiError.invalidRequest"));
54982
+ }
54983
+ updates.enableTelegramNotificationPush = body.enableTelegramNotificationPush;
54984
+ }
54206
54985
  if (body.sshReconnectMaxRetries !== undefined) {
54207
54986
  const value = Math.floor(Number(body.sshReconnectMaxRetries));
54208
54987
  if (Number.isNaN(value) || value < 0 || value > 20) {
@@ -54293,28 +55072,28 @@ function handleApiRequest(req, _server) {
54293
55072
  return handleGetManifest(req.method);
54294
55073
  }
54295
55074
  if (path === "/healthz" && req.method === "GET") {
54296
- return json({ status: "ok", restarting: runtimeController.isRestarting() });
55075
+ return json2({ status: "ok", restarting: runtimeController.isRestarting() });
54297
55076
  }
54298
- return json({ error: t2("apiError.notFound") }, 404);
55077
+ return json2({ error: t2("apiError.notFound") }, 404);
54299
55078
  }
54300
55079
  async function handleGetDevices() {
54301
55080
  const devices2 = getAllDevices();
54302
- return json({ devices: devices2 });
55081
+ return json2({ devices: devices2 });
54303
55082
  }
54304
55083
  async function handleGetDevice(id) {
54305
55084
  const device = getDeviceById(id);
54306
55085
  if (!device) {
54307
- return json({ error: t2("apiError.deviceNotFound") }, 404);
55086
+ return json2({ error: t2("apiError.deviceNotFound") }, 404);
54308
55087
  }
54309
- return json({ device });
55088
+ return json2({ device });
54310
55089
  }
54311
55090
  async function handleCreateDevice(req) {
54312
55091
  const body = await req.json();
54313
55092
  if (!body.name || !body.type || !body.authMode) {
54314
- return json({ error: t2("apiError.missingFields") }, 400);
55093
+ return json2({ error: t2("apiError.missingFields") }, 400);
54315
55094
  }
54316
55095
  if (body.type === "ssh" && !body.host && !body.sshConfigRef) {
54317
- return json({ error: t2("apiError.sshRequiresHost") }, 400);
55096
+ return json2({ error: t2("apiError.sshRequiresHost") }, 400);
54318
55097
  }
54319
55098
  const now = new Date().toISOString();
54320
55099
  const device = {
@@ -54335,12 +55114,12 @@ async function handleCreateDevice(req) {
54335
55114
  };
54336
55115
  createDevice(device);
54337
55116
  await pushSupervisor.upsert(device.id);
54338
- return json({ device }, 201);
55117
+ return json2({ device }, 201);
54339
55118
  }
54340
55119
  async function handleUpdateDevice(req, id) {
54341
55120
  const existing = getDeviceById(id);
54342
55121
  if (!existing) {
54343
- return json({ error: t2("apiError.deviceNotFound") }, 404);
55122
+ return json2({ error: t2("apiError.deviceNotFound") }, 404);
54344
55123
  }
54345
55124
  const body = await req.json();
54346
55125
  const updates = {};
@@ -54370,61 +55149,53 @@ async function handleUpdateDevice(req, id) {
54370
55149
  await pushSupervisor.reconnect(id);
54371
55150
  }
54372
55151
  const device = getDeviceById(id);
54373
- return json({ device });
55152
+ return json2({ device });
54374
55153
  }
54375
55154
  async function handleDeleteDevice(id) {
54376
55155
  const existing = getDeviceById(id);
54377
55156
  if (!existing) {
54378
- return json({ error: t2("apiError.deviceNotFound") }, 404);
55157
+ return json2({ error: t2("apiError.deviceNotFound") }, 404);
54379
55158
  }
54380
55159
  deleteDevice(id);
54381
55160
  pushSupervisor.remove(id);
54382
- return json({ success: true });
55161
+ return json2({ success: true });
54383
55162
  }
54384
55163
  async function handleTestConnection(id) {
54385
- const device = getDeviceById(id);
54386
- if (!device) {
54387
- return json({ error: t2("apiError.deviceNotFound") }, 404);
54388
- }
54389
- return json({
54390
- success: true,
54391
- tmuxAvailable: false,
54392
- message: "Connection test not fully implemented yet"
54393
- });
55164
+ return handleDeviceTestConnection(id);
54394
55165
  }
54395
55166
  async function handleGetSiteSettings() {
54396
- return json({ settings: getSiteSettings() });
55167
+ return json2({ settings: getSiteSettings() });
54397
55168
  }
54398
55169
  async function handleUpdateSiteSettings(req) {
54399
55170
  try {
54400
55171
  const body = await req.json();
54401
55172
  const updates = normalizeSiteSettingsInput(body);
54402
55173
  const settings = updateSiteSettings(updates);
54403
- return json({ settings });
55174
+ return json2({ settings });
54404
55175
  } catch (err) {
54405
- return json({ error: err instanceof Error ? err.message : t2("apiError.invalidRequest") }, 400);
55176
+ return json2({ error: err instanceof Error ? err.message : t2("apiError.invalidRequest") }, 400);
54406
55177
  }
54407
55178
  }
54408
55179
  async function handleRestartGateway() {
54409
55180
  setTimeout(() => {
54410
55181
  runtimeController.requestRestart();
54411
55182
  }, 50);
54412
- return json({
55183
+ return json2({
54413
55184
  success: true,
54414
55185
  message: t2("settings.restartScheduled")
54415
55186
  });
54416
55187
  }
54417
55188
  async function handleGetTelegramBots() {
54418
55189
  const bots = getTelegramBotsWithStats();
54419
- return json({ bots });
55190
+ return json2({ bots });
54420
55191
  }
54421
55192
  async function handleCreateTelegramBot(req) {
54422
55193
  const body = await req.json();
54423
55194
  if (!body.name?.trim()) {
54424
- return json({ error: t2("apiError.botNameRequired") }, 400);
55195
+ return json2({ error: t2("apiError.botNameRequired") }, 400);
54425
55196
  }
54426
55197
  if (!body.token?.trim()) {
54427
- return json({ error: t2("apiError.botTokenRequired") }, 400);
55198
+ return json2({ error: t2("apiError.botTokenRequired") }, 400);
54428
55199
  }
54429
55200
  const now = new Date().toISOString();
54430
55201
  createTelegramBot({
@@ -54438,26 +55209,26 @@ async function handleCreateTelegramBot(req) {
54438
55209
  updatedAt: now
54439
55210
  });
54440
55211
  await telegramService.refresh();
54441
- return json({ success: true }, 201);
55212
+ return json2({ success: true }, 201);
54442
55213
  }
54443
55214
  async function handleUpdateTelegramBot(req, botId) {
54444
55215
  const existing = getTelegramBotById(botId);
54445
55216
  if (!existing) {
54446
- return json({ error: t2("apiError.botNotFound") }, 404);
55217
+ return json2({ error: t2("apiError.botNotFound") }, 404);
54447
55218
  }
54448
55219
  const body = await req.json();
54449
55220
  const updates = {};
54450
55221
  if (body.name !== undefined) {
54451
55222
  const value = body.name.trim();
54452
55223
  if (!value) {
54453
- return json({ error: t2("apiError.botNameRequired") }, 400);
55224
+ return json2({ error: t2("apiError.botNameRequired") }, 400);
54454
55225
  }
54455
55226
  updates.name = value;
54456
55227
  }
54457
55228
  if (body.token !== undefined) {
54458
55229
  const token = body.token.trim();
54459
55230
  if (!token) {
54460
- return json({ error: t2("apiError.botTokenRequired") }, 400);
55231
+ return json2({ error: t2("apiError.botTokenRequired") }, 400);
54461
55232
  }
54462
55233
  updates.tokenEnc = await encrypt(token);
54463
55234
  }
@@ -54469,69 +55240,69 @@ async function handleUpdateTelegramBot(req, botId) {
54469
55240
  }
54470
55241
  updateTelegramBot(botId, updates);
54471
55242
  await telegramService.refresh();
54472
- return json({ success: true });
55243
+ return json2({ success: true });
54473
55244
  }
54474
55245
  async function handleDeleteTelegramBot(botId) {
54475
55246
  const existing = getTelegramBotById(botId);
54476
55247
  if (!existing) {
54477
- return json({ error: t2("apiError.botNotFound") }, 404);
55248
+ return json2({ error: t2("apiError.botNotFound") }, 404);
54478
55249
  }
54479
55250
  deleteTelegramBot(botId);
54480
55251
  await telegramService.refresh();
54481
- return json({ success: true });
55252
+ return json2({ success: true });
54482
55253
  }
54483
55254
  async function handleListTelegramChats(botId) {
54484
55255
  const existing = getTelegramBotById(botId);
54485
55256
  if (!existing) {
54486
- return json({ error: t2("apiError.botNotFound") }, 404);
55257
+ return json2({ error: t2("apiError.botNotFound") }, 404);
54487
55258
  }
54488
55259
  const chats = listTelegramChatsByBot(botId);
54489
- return json({ chats });
55260
+ return json2({ chats });
54490
55261
  }
54491
55262
  async function handleApproveTelegramChat(botId, chatId) {
54492
55263
  const existing = getTelegramBotById(botId);
54493
55264
  if (!existing) {
54494
- return json({ error: t2("apiError.botNotFound") }, 404);
55265
+ return json2({ error: t2("apiError.botNotFound") }, 404);
54495
55266
  }
54496
55267
  const chat = approveTelegramChat(botId, chatId);
54497
55268
  if (!chat) {
54498
- return json({ error: t2("apiError.chatNotFound") }, 404);
55269
+ return json2({ error: t2("apiError.chatNotFound") }, 404);
54499
55270
  }
54500
55271
  const settings = getSiteSettings();
54501
55272
  await telegramService.sendTestMessage(botId, chatId, t2("telegram.approveMessageTemplate", {
54502
55273
  botName: existing.name,
54503
55274
  time: new Date().toLocaleString(toBCP47(settings.language))
54504
55275
  }));
54505
- return json({ chat });
55276
+ return json2({ chat });
54506
55277
  }
54507
55278
  async function handleDeleteTelegramChat(botId, chatId) {
54508
55279
  const existing = getTelegramBotById(botId);
54509
55280
  if (!existing) {
54510
- return json({ error: t2("apiError.botNotFound") }, 404);
55281
+ return json2({ error: t2("apiError.botNotFound") }, 404);
54511
55282
  }
54512
55283
  deleteTelegramChat(botId, chatId);
54513
- return json({ success: true });
55284
+ return json2({ success: true });
54514
55285
  }
54515
55286
  async function handleTestTelegramChat(botId, chatId) {
54516
55287
  const bot = getTelegramBotById(botId);
54517
55288
  if (!bot) {
54518
- return json({ error: t2("apiError.botNotFound") }, 404);
55289
+ return json2({ error: t2("apiError.botNotFound") }, 404);
54519
55290
  }
54520
55291
  const settings = getSiteSettings();
54521
55292
  await telegramService.sendTestMessage(botId, chatId, t2("telegram.testMessageTemplate", {
54522
55293
  siteName: settings.siteName,
54523
55294
  time: new Date().toLocaleString(toBCP47(settings.language))
54524
55295
  }));
54525
- return json({ success: true });
55296
+ return json2({ success: true });
54526
55297
  }
54527
55298
  async function handleGetWebhooks() {
54528
55299
  const webhooks = getAllWebhookEndpoints();
54529
- return json({ webhooks });
55300
+ return json2({ webhooks });
54530
55301
  }
54531
55302
  async function handleCreateWebhook(req) {
54532
55303
  const body = await req.json();
54533
55304
  if (!body.url || !body.secret) {
54534
- return json({ error: t2("apiError.urlAndSecretRequired") }, 400);
55305
+ return json2({ error: t2("apiError.urlAndSecretRequired") }, 400);
54535
55306
  }
54536
55307
  const now = new Date().toISOString();
54537
55308
  const endpoint = {
@@ -54544,11 +55315,11 @@ async function handleCreateWebhook(req) {
54544
55315
  updatedAt: now
54545
55316
  };
54546
55317
  createWebhookEndpoint(endpoint);
54547
- return json({ webhook: endpoint }, 201);
55318
+ return json2({ webhook: endpoint }, 201);
54548
55319
  }
54549
55320
  async function handleDeleteWebhook(id) {
54550
55321
  deleteWebhookEndpoint(id);
54551
- return json({ success: true });
55322
+ return json2({ success: true });
54552
55323
  }
54553
55324
  async function handleGetManifest(method) {
54554
55325
  const settings = getSiteSettings();
@@ -54581,7 +55352,7 @@ function manifestJson(data, method) {
54581
55352
  }
54582
55353
  });
54583
55354
  }
54584
- function json(data, status = 200, headers = {}) {
55355
+ function json2(data, status = 200, headers = {}) {
54585
55356
  return new Response(JSON.stringify(data), {
54586
55357
  status,
54587
55358
  headers: {
@@ -54592,7 +55363,7 @@ function json(data, status = 200, headers = {}) {
54592
55363
  }
54593
55364
 
54594
55365
  // ../../apps/gateway/src/db/migrate.ts
54595
- import { existsSync as existsSync2 } from "fs";
55366
+ import { existsSync as existsSync3 } from "fs";
54596
55367
  import { resolve } from "path";
54597
55368
 
54598
55369
  // ../../node_modules/.bun/drizzle-orm@0.45.1+1608dc8003c5413e/node_modules/drizzle-orm/migrator.js
@@ -54639,7 +55410,7 @@ function resolveMigrationsFolder() {
54639
55410
  if (fromEnv)
54640
55411
  return fromEnv;
54641
55412
  const byCwd = resolve(process.cwd(), "drizzle");
54642
- if (existsSync2(byCwd))
55413
+ if (existsSync3(byCwd))
54643
55414
  return byCwd;
54644
55415
  return resolve(import.meta.dir, "../../drizzle");
54645
55416
  }
@@ -54716,7 +55487,8 @@ class SessionStateStore {
54716
55487
  deviceConnections: new Map,
54717
55488
  selectTransactions: new Map,
54718
55489
  outputGates: new Map,
54719
- bellThrottles: new Map
55490
+ bellThrottles: new Map,
55491
+ notificationThrottles: new Map
54720
55492
  };
54721
55493
  this.states.set(ws, state);
54722
55494
  return state;
@@ -54946,6 +55718,28 @@ class SessionStateStore {
54946
55718
  ctx.throttleSeconds = throttleSeconds;
54947
55719
  return true;
54948
55720
  }
55721
+ shouldAllowNotification(ws, deviceId, paneId, source, throttleSeconds) {
55722
+ const state = this.states.get(ws);
55723
+ if (!state)
55724
+ return false;
55725
+ const key = `${deviceId}:${paneId}:${source}`;
55726
+ const now = Date.now();
55727
+ let ctx = state.notificationThrottles.get(key);
55728
+ if (!ctx) {
55729
+ ctx = {
55730
+ lastBellAt: 0,
55731
+ throttleSeconds
55732
+ };
55733
+ state.notificationThrottles.set(key, ctx);
55734
+ }
55735
+ const throttleMs = throttleSeconds * 1000;
55736
+ if (now - ctx.lastBellAt < throttleMs) {
55737
+ return false;
55738
+ }
55739
+ ctx.lastBellAt = now;
55740
+ ctx.throttleSeconds = throttleSeconds;
55741
+ return true;
55742
+ }
54949
55743
  cleanupDevice(ws, deviceId) {
54950
55744
  const state = this.states.get(ws);
54951
55745
  if (!state)
@@ -54958,6 +55752,11 @@ class SessionStateStore {
54958
55752
  state.bellThrottles.delete(key);
54959
55753
  }
54960
55754
  }
55755
+ for (const key of state.notificationThrottles.keys()) {
55756
+ if (key.startsWith(`${deviceId}:`)) {
55757
+ state.notificationThrottles.delete(key);
55758
+ }
55759
+ }
54961
55760
  }
54962
55761
  cleanup(ws) {
54963
55762
  this.states.delete(ws);
@@ -55208,82 +56007,6 @@ class SwitchBarrier {
55208
56007
  }
55209
56008
  var switchBarrier = new SwitchBarrier;
55210
56009
 
55211
- // ../../apps/gateway/src/ws/error-classify.ts
55212
- function classifySshError(error) {
55213
- const msg = error.message.toLowerCase();
55214
- if (msg.includes("ssh_config_ref_not_supported")) {
55215
- return {
55216
- type: "ssh_config_ref_not_supported",
55217
- messageKey: "sshError.configRefNotSupported"
55218
- };
55219
- }
55220
- if (msg.includes("ssh_auth_sock") || msg.includes("auth_sock")) {
55221
- return {
55222
- type: "agent_unavailable",
55223
- messageKey: "sshError.agentUnavailable"
55224
- };
55225
- }
55226
- if (msg.includes("agent") && (msg.includes("no identities") || msg.includes("failure"))) {
55227
- return {
55228
- type: "agent_no_identity",
55229
- messageKey: "sshError.agentNoIdentities"
55230
- };
55231
- }
55232
- if (msg.includes("permission denied")) {
55233
- return {
55234
- type: "auth_failed",
55235
- messageKey: "sshError.authFailed"
55236
- };
55237
- }
55238
- if (msg.includes("all configured authentication methods failed")) {
55239
- return {
55240
- type: "auth_failed",
55241
- messageKey: "sshError.authFailedGeneric"
55242
- };
55243
- }
55244
- if (msg.includes("enetunreach") || msg.includes("ehostunreach")) {
55245
- return {
55246
- type: "network_unreachable",
55247
- messageKey: "sshError.networkUnreachable"
55248
- };
55249
- }
55250
- if (msg.includes("connect refused") || msg.includes("connection refused") || msg.includes("econnrefused")) {
55251
- return {
55252
- type: "connection_refused",
55253
- messageKey: "sshError.connectionRefused"
55254
- };
55255
- }
55256
- if (msg.includes("timeout") || msg.includes("etimedout")) {
55257
- return {
55258
- type: "timeout",
55259
- messageKey: "sshError.connectionTimeout"
55260
- };
55261
- }
55262
- if (msg.includes("host not found") || msg.includes("getaddrinfo") || msg.includes("enotfound")) {
55263
- return {
55264
- type: "host_not_found",
55265
- messageKey: "sshError.hostNotFound"
55266
- };
55267
- }
55268
- if (msg.includes("handshake failed") || msg.includes("unable to verify")) {
55269
- return {
55270
- type: "handshake_failed",
55271
- messageKey: "sshError.handshakeFailed"
55272
- };
55273
- }
55274
- if (msg.includes("remote tmux unavailable") || msg.includes("tmux_not_found") || msg.includes("tmux: command not found") || msg.includes("tmux control mode not ready") || msg.includes("tmux exited") || msg.includes("tmux_exec_failed")) {
55275
- return {
55276
- type: "tmux_unavailable",
55277
- messageKey: "sshError.tmuxUnavailable"
55278
- };
55279
- }
55280
- return {
55281
- type: "unknown",
55282
- messageKey: "sshError.unknown",
55283
- messageParams: { message: error.message }
55284
- };
55285
- }
55286
-
55287
56010
  // ../../apps/gateway/src/ws/index.ts
55288
56011
  var defaultDeps2 = {
55289
56012
  acquireRuntime: async (deviceId) => tmuxRuntimeRegistry.acquire(deviceId),
@@ -55802,13 +56525,21 @@ class WebSocketServer {
55802
56525
  return;
55803
56526
  this.scheduleSnapshot(deviceId);
55804
56527
  const extendedEvent = await this.extendTmuxEvent(deviceId, event);
56528
+ const settings = getSiteSettings();
56529
+ if (extendedEvent.type === "notification") {
56530
+ const data = extendedEvent.data ?? {};
56531
+ const title = typeof data.title === "string" && data.title ? data.title : "";
56532
+ const body = typeof data.body === "string" ? data.body : "";
56533
+ if (!title && !body) {
56534
+ return;
56535
+ }
56536
+ }
55805
56537
  const payloadBytes = exports_ws_borsh.encodeTmuxEventPayload({
55806
56538
  deviceId,
55807
56539
  type: extendedEvent.type,
55808
56540
  data: extendedEvent.data
55809
56541
  });
55810
56542
  if (extendedEvent.type === "bell") {
55811
- const settings = getSiteSettings();
55812
56543
  const data = extendedEvent.data ?? {};
55813
56544
  const paneId = typeof data.paneId === "string" && data.paneId ? data.paneId : "-";
55814
56545
  for (const client of entry.clients) {
@@ -55819,25 +56550,52 @@ class WebSocketServer {
55819
56550
  }
55820
56551
  return;
55821
56552
  }
56553
+ if (extendedEvent.type === "notification") {
56554
+ const data = extendedEvent.data ?? {};
56555
+ const paneId = typeof data.paneId === "string" && data.paneId ? data.paneId : "-";
56556
+ const source = typeof data.source === "string" && data.source ? data.source : "osc9";
56557
+ for (const client of entry.clients) {
56558
+ if (!sessionStateStore.shouldAllowNotification(client, deviceId, paneId, source, settings.notificationThrottleSeconds)) {
56559
+ continue;
56560
+ }
56561
+ this.sendEnvelope(client, exports_ws_borsh.KIND_TMUX_EVENT, payloadBytes);
56562
+ }
56563
+ return;
56564
+ }
55822
56565
  for (const client of entry.clients) {
55823
56566
  this.sendEnvelope(client, exports_ws_borsh.KIND_TMUX_EVENT, payloadBytes);
55824
56567
  }
55825
56568
  }
55826
56569
  async extendTmuxEvent(deviceId, event) {
55827
- if (event.type !== "bell") {
56570
+ if (event.type !== "bell" && event.type !== "notification") {
55828
56571
  return event;
55829
56572
  }
55830
56573
  const settings = getSiteSettings();
55831
56574
  const snapshot = this.connections.get(deviceId)?.lastSnapshot ?? null;
55832
- const data = resolveBellContext({
56575
+ const paneContext = resolvePaneContext({
55833
56576
  deviceId,
55834
56577
  siteUrl: settings.siteUrl,
55835
56578
  snapshot,
55836
56579
  rawData: event.data
55837
56580
  });
56581
+ if (event.type === "bell") {
56582
+ return {
56583
+ type: "bell",
56584
+ data: paneContext
56585
+ };
56586
+ }
56587
+ const raw = event.data ?? {};
56588
+ const source = raw.source === "osc9" || raw.source === "osc777" || raw.source === "osc1337" ? raw.source : "osc9";
56589
+ const title = typeof raw.title === "string" && raw.title ? raw.title : undefined;
56590
+ const body = typeof raw.body === "string" ? raw.body : "";
55838
56591
  return {
55839
- type: "bell",
55840
- data
56592
+ type: "notification",
56593
+ data: {
56594
+ ...paneContext,
56595
+ source,
56596
+ title,
56597
+ body
56598
+ }
55841
56599
  };
55842
56600
  }
55843
56601
  broadcastStateSnapshot(deviceId, payload) {
@@ -56302,9 +57060,9 @@ async function serveFrontend(req, staticRoot) {
56302
57060
  if (!requestedPath) {
56303
57061
  return new Response(t3("runtime.forbidden"), { status: 403 });
56304
57062
  }
56305
- const indexPath = join4(staticRoot, "index.html");
56306
- const targetPath = existsSync3(requestedPath) ? requestedPath : indexPath;
56307
- if (!existsSync3(targetPath)) {
57063
+ const indexPath = join5(staticRoot, "index.html");
57064
+ const targetPath = existsSync4(requestedPath) ? requestedPath : indexPath;
57065
+ if (!existsSync4(targetPath)) {
56308
57066
  return new Response(t3("runtime.frontendMissing"), { status: 500 });
56309
57067
  }
56310
57068
  const headers = new Headers;