tmex-cli 0.4.0 → 0.4.2

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 (34) hide show
  1. package/dist/runtime/server.js +736 -186
  2. package/package.json +1 -1
  3. package/resources/fe-dist/assets/DevicePage-CKaPUo7L.js +26 -0
  4. package/resources/fe-dist/assets/DevicePage-CKaPUo7L.js.map +1 -0
  5. package/resources/fe-dist/assets/DevicesPage-FqU-Dxhu.js +17 -0
  6. package/resources/fe-dist/assets/{DevicesPage-CtNzaW_c.js.map → DevicesPage-FqU-Dxhu.js.map} +1 -1
  7. package/resources/fe-dist/assets/SettingsPage-BfkOW0fc.js +17 -0
  8. package/resources/fe-dist/assets/SettingsPage-BfkOW0fc.js.map +1 -0
  9. package/resources/fe-dist/assets/index-EgHfq93I.js +449 -0
  10. package/resources/fe-dist/assets/index-EgHfq93I.js.map +1 -0
  11. package/resources/fe-dist/assets/index-Ytlj3p_q.css +1 -0
  12. package/resources/fe-dist/assets/select-CNlE6MiW.js +17 -0
  13. package/resources/fe-dist/assets/{select-BNsiC9zT.js.map → select-CNlE6MiW.js.map} +1 -1
  14. package/resources/fe-dist/assets/switch-CxkzOIL6.js +12 -0
  15. package/resources/fe-dist/assets/{switch-CIU4AisU.js.map → switch-CxkzOIL6.js.map} +1 -1
  16. package/resources/fe-dist/assets/useValueChanged-CO2U5MoL.js +7 -0
  17. package/resources/fe-dist/assets/{useValueChanged-V23H0VpC.js.map → useValueChanged-CO2U5MoL.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/0000_snapshot.json +17 -6
  21. package/resources/gateway-drizzle/meta/0001_snapshot.json +17 -6
  22. package/resources/gateway-drizzle/meta/0002_snapshot.json +535 -0
  23. package/resources/gateway-drizzle/meta/_journal.json +8 -1
  24. package/resources/fe-dist/assets/DevicePage-iSkEDEpS.js +0 -4570
  25. package/resources/fe-dist/assets/DevicePage-iSkEDEpS.js.map +0 -1
  26. package/resources/fe-dist/assets/DevicesPage-CtNzaW_c.js +0 -2143
  27. package/resources/fe-dist/assets/SettingsPage-D25_d6j9.js +0 -1144
  28. package/resources/fe-dist/assets/SettingsPage-D25_d6j9.js.map +0 -1
  29. package/resources/fe-dist/assets/index-CJaX5rlK.css +0 -4527
  30. package/resources/fe-dist/assets/index-dsVN7rgz.js +0 -200
  31. package/resources/fe-dist/assets/index-dsVN7rgz.js.map +0 -1
  32. package/resources/fe-dist/assets/select-BNsiC9zT.js +0 -2805
  33. package/resources/fe-dist/assets/switch-CIU4AisU.js +0 -234
  34. package/resources/fe-dist/assets/useValueChanged-V23H0VpC.js +0 -351
@@ -20363,8 +20363,13 @@ var I18N_RESOURCES = {
20363
20363
  title: "Device Management",
20364
20364
  devices: "Devices",
20365
20365
  addDevice: "Add Device",
20366
+ addDeviceDescription: "Fill in device details and choose a connection method",
20366
20367
  addFirstDevice: "Add First Device",
20367
20368
  editDevice: "Edit Device",
20369
+ editDeviceDescription: "Update device configuration",
20370
+ sectionBasic: "Basic Info",
20371
+ sectionConnection: "Connection",
20372
+ sectionAuth: "Authentication",
20368
20373
  noDevices: "No Devices",
20369
20374
  noDevicesDescription: "Add a local or SSH device to get started",
20370
20375
  name: "Device Name",
@@ -20397,6 +20402,7 @@ var I18N_RESOURCES = {
20397
20402
  disconnected: "Disconnected",
20398
20403
  connecting: "Connecting...",
20399
20404
  deleteConfirm: "Delete this device?",
20405
+ deleteDescription: 'Device "{{name}}" will be permanently removed. This action cannot be undone.',
20400
20406
  deleteSuccess: "Device deleted",
20401
20407
  createSuccess: "Device created",
20402
20408
  updateSuccess: "Device updated",
@@ -20446,8 +20452,11 @@ var I18N_RESOURCES = {
20446
20452
  siteUrl: "Site URL",
20447
20453
  siteUrlPlaceholder: "http://localhost:3000",
20448
20454
  bellThrottle: "Bell Throttle (seconds)",
20455
+ notificationThrottle: "Notification Throttle (seconds)",
20449
20456
  enableBrowserBellToast: "Enable Browser Bell Toast",
20457
+ enableBrowserNotificationToast: "Enable Browser Notification Toast",
20450
20458
  enableTelegramBellPush: "Enable Telegram Bell Push",
20459
+ enableTelegramNotificationPush: "Enable Telegram Notification Push",
20451
20460
  sshReconnectRetries: "SSH Reconnect Retries",
20452
20461
  sshReconnectDelay: "SSH Reconnect Delay (seconds)",
20453
20462
  language: "Language",
@@ -20591,6 +20600,7 @@ Time: {{time}}`,
20591
20600
  clickToJump: "Click to jump to corresponding pane",
20592
20601
  eventType: {
20593
20602
  terminal_bell: "\uD83D\uDD14 Terminal Bell",
20603
+ terminal_notification: "\uD83D\uDD14 Terminal Notification",
20594
20604
  tmux_window_close: "\uD83E\uDE9F Window Closed",
20595
20605
  tmux_pane_close: "\uD83D\uDCF1 Pane Closed",
20596
20606
  device_tmux_missing: "\u26A0\uFE0F Tmux Missing",
@@ -20609,7 +20619,8 @@ Time: {{time}}`,
20609
20619
  title: "\uD83D\uDD14 Bell from {{siteName}}: {{terminalTopbarLabel}}",
20610
20620
  viewLink: "Click to view",
20611
20621
  terminalTopbarLabel: "Window {{window}} \xB7 Pane {{pane}} @ {{device}}"
20612
- }
20622
+ },
20623
+ telegramNotification: {}
20613
20624
  },
20614
20625
  sidebar: {
20615
20626
  noWindows: "No windows",
@@ -20690,8 +20701,13 @@ Time: {{time}}`,
20690
20701
  title: "\u8BBE\u5907\u7BA1\u7406",
20691
20702
  devices: "\u8BBE\u5907",
20692
20703
  addDevice: "\u6DFB\u52A0\u8BBE\u5907",
20704
+ addDeviceDescription: "\u586B\u5199\u8BBE\u5907\u4FE1\u606F\u5E76\u9009\u62E9\u8FDE\u63A5\u65B9\u5F0F",
20693
20705
  addFirstDevice: "\u6DFB\u52A0\u7B2C\u4E00\u4E2A\u8BBE\u5907",
20694
20706
  editDevice: "\u4FEE\u6539\u8BBE\u5907",
20707
+ editDeviceDescription: "\u66F4\u65B0\u8BBE\u5907\u914D\u7F6E",
20708
+ sectionBasic: "\u57FA\u672C\u4FE1\u606F",
20709
+ sectionConnection: "\u8FDE\u63A5\u4FE1\u606F",
20710
+ sectionAuth: "\u8BA4\u8BC1\u4FE1\u606F",
20695
20711
  noDevices: "\u6682\u65E0\u8BBE\u5907",
20696
20712
  noDevicesDescription: "\u6DFB\u52A0\u672C\u5730\u6216 SSH \u8BBE\u5907\u5F00\u59CB\u4F7F\u7528",
20697
20713
  name: "\u8BBE\u5907\u540D\u79F0",
@@ -20724,6 +20740,7 @@ Time: {{time}}`,
20724
20740
  disconnected: "\u5DF2\u65AD\u5F00",
20725
20741
  connecting: "\u8FDE\u63A5\u4E2D...",
20726
20742
  deleteConfirm: "\u5220\u9664\u6B64\u8BBE\u5907\uFF1F",
20743
+ deleteDescription: '\u8BBE\u5907 "{{name}}" \u5C06\u88AB\u6C38\u4E45\u79FB\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002',
20727
20744
  deleteSuccess: "\u8BBE\u5907\u5DF2\u5220\u9664",
20728
20745
  createSuccess: "\u8BBE\u5907\u5DF2\u521B\u5EFA",
20729
20746
  updateSuccess: "\u8BBE\u5907\u5DF2\u66F4\u65B0",
@@ -20773,8 +20790,11 @@ Time: {{time}}`,
20773
20790
  siteUrl: "\u7AD9\u70B9\u8BBF\u95EE URL",
20774
20791
  siteUrlPlaceholder: "http://localhost:3000",
20775
20792
  bellThrottle: "Bell \u9891\u63A7\uFF08\u79D2\uFF09",
20793
+ notificationThrottle: "\u901A\u77E5\u9891\u63A7\uFF08\u79D2\uFF09",
20776
20794
  enableBrowserBellToast: "\u5F00\u542F\u6D4F\u89C8\u5668 Bell Toast",
20795
+ enableBrowserNotificationToast: "\u5F00\u542F\u6D4F\u89C8\u5668\u901A\u77E5 Toast",
20777
20796
  enableTelegramBellPush: "\u5F00\u542F Telegram Bell \u63A8\u9001",
20797
+ enableTelegramNotificationPush: "\u5F00\u542F Telegram \u901A\u77E5\u63A8\u9001",
20778
20798
  sshReconnectRetries: "SSH \u91CD\u8FDE\u6B21\u6570",
20779
20799
  sshReconnectDelay: "SSH \u91CD\u8FDE\u7B49\u5F85\uFF08\u79D2\uFF09",
20780
20800
  language: "\u8BED\u8A00",
@@ -20918,6 +20938,7 @@ Bot\uFF1A{{botName}}
20918
20938
  clickToJump: "\u70B9\u51FB\u8DF3\u8F6C\u5230\u5BF9\u5E94 Pane",
20919
20939
  eventType: {
20920
20940
  terminal_bell: "\uD83D\uDD14 \u7EC8\u7AEF Bell",
20941
+ terminal_notification: "\uD83D\uDD14 \u7EC8\u7AEF\u901A\u77E5",
20921
20942
  tmux_window_close: "\uD83E\uDE9F \u7A97\u53E3\u5173\u95ED",
20922
20943
  tmux_pane_close: "\uD83D\uDCF1 Pane \u5173\u95ED",
20923
20944
  device_tmux_missing: "\u26A0\uFE0F Tmux \u4E0D\u53EF\u7528",
@@ -20936,7 +20957,8 @@ Bot\uFF1A{{botName}}
20936
20957
  title: "\uD83D\uDD14 \u6765\u81EA {{siteName}} \u7684 Bell\uFF1A{{terminalTopbarLabel}}",
20937
20958
  viewLink: "\u70B9\u51FB\u67E5\u770B",
20938
20959
  terminalTopbarLabel: "\u7A97\u53E3 {{window}} \xB7 Pane {{pane}} @ {{device}}"
20939
- }
20960
+ },
20961
+ telegramNotification: {}
20940
20962
  },
20941
20963
  sidebar: {
20942
20964
  noWindows: "\u6682\u65E0\u7A97\u53E3",
@@ -21017,8 +21039,13 @@ Bot\uFF1A{{botName}}
21017
21039
  title: "\u30C7\u30D0\u30A4\u30B9\u7BA1\u7406",
21018
21040
  devices: "\u30C7\u30D0\u30A4\u30B9",
21019
21041
  addDevice: "\u30C7\u30D0\u30A4\u30B9\u3092\u8FFD\u52A0",
21042
+ addDeviceDescription: "\u30C7\u30D0\u30A4\u30B9\u60C5\u5831\u3092\u5165\u529B\u3057\u3001\u63A5\u7D9A\u65B9\u6CD5\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
21020
21043
  addFirstDevice: "\u6700\u521D\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u8FFD\u52A0",
21021
21044
  editDevice: "\u30C7\u30D0\u30A4\u30B9\u3092\u7DE8\u96C6",
21045
+ editDeviceDescription: "\u30C7\u30D0\u30A4\u30B9\u8A2D\u5B9A\u3092\u66F4\u65B0",
21046
+ sectionBasic: "\u57FA\u672C\u60C5\u5831",
21047
+ sectionConnection: "\u63A5\u7D9A\u60C5\u5831",
21048
+ sectionAuth: "\u8A8D\u8A3C\u60C5\u5831",
21022
21049
  noDevices: "\u30C7\u30D0\u30A4\u30B9\u304C\u3042\u308A\u307E\u305B\u3093",
21023
21050
  noDevicesDescription: "\u30ED\u30FC\u30AB\u30EB\u307E\u305F\u306F SSH \u30C7\u30D0\u30A4\u30B9\u3092\u8FFD\u52A0\u3057\u3066\u958B\u59CB",
21024
21051
  name: "\u30C7\u30D0\u30A4\u30B9\u540D",
@@ -21051,6 +21078,7 @@ Bot\uFF1A{{botName}}
21051
21078
  disconnected: "\u5207\u65AD\u6E08\u307F",
21052
21079
  connecting: "\u63A5\u7D9A\u4E2D...",
21053
21080
  deleteConfirm: "\u3053\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u524A\u9664\u3057\u307E\u3059\u304B\uFF1F",
21081
+ deleteDescription: "\u30C7\u30D0\u30A4\u30B9\u300C{{name}}\u300D\u306F\u5B8C\u5168\u306B\u524A\u9664\u3055\u308C\u307E\u3059\u3002\u3053\u306E\u64CD\u4F5C\u306F\u53D6\u308A\u6D88\u305B\u307E\u305B\u3093\u3002",
21054
21082
  deleteSuccess: "\u30C7\u30D0\u30A4\u30B9\u3092\u524A\u9664\u3057\u307E\u3057\u305F",
21055
21083
  createSuccess: "\u30C7\u30D0\u30A4\u30B9\u3092\u4F5C\u6210\u3057\u307E\u3057\u305F",
21056
21084
  updateSuccess: "\u30C7\u30D0\u30A4\u30B9\u3092\u66F4\u65B0\u3057\u307E\u3057\u305F",
@@ -21100,8 +21128,11 @@ Bot\uFF1A{{botName}}
21100
21128
  siteUrl: "\u30B5\u30A4\u30C8 URL",
21101
21129
  siteUrlPlaceholder: "http://localhost:3000",
21102
21130
  bellThrottle: "\u30D9\u30EB\u5236\u9650\uFF08\u79D2\uFF09",
21131
+ notificationThrottle: "\u901A\u77E5\u5236\u9650\uFF08\u79D2\uFF09",
21103
21132
  enableBrowserBellToast: "\u30D6\u30E9\u30A6\u30B6\u30D9\u30EB Toast \u3092\u6709\u52B9\u306B\u3059\u308B",
21133
+ enableBrowserNotificationToast: "\u30D6\u30E9\u30A6\u30B6\u901A\u77E5 Toast \u3092\u6709\u52B9\u306B\u3059\u308B",
21104
21134
  enableTelegramBellPush: "Telegram \u30D9\u30EB\u30D7\u30C3\u30B7\u30E5\u3092\u6709\u52B9\u306B\u3059\u308B",
21135
+ enableTelegramNotificationPush: "Telegram \u901A\u77E5\u30D7\u30C3\u30B7\u30E5\u3092\u6709\u52B9\u306B\u3059\u308B",
21105
21136
  sshReconnectRetries: "SSH \u518D\u63A5\u7D9A\u8A66\u884C\u56DE\u6570",
21106
21137
  sshReconnectDelay: "SSH \u518D\u63A5\u7D9A\u5F85\u6A5F\uFF08\u79D2\uFF09",
21107
21138
  language: "\u8A00\u8A9E",
@@ -21245,6 +21276,7 @@ Bot\uFF1A{{botName}}
21245
21276
  clickToJump: "\u5BFE\u5FDC\u3059\u308B\u30DA\u30A4\u30F3\u306B\u30B8\u30E3\u30F3\u30D7",
21246
21277
  eventType: {
21247
21278
  terminal_bell: "\uD83D\uDD14 \u30BF\u30FC\u30DF\u30CA\u30EB\u30D9\u30EB",
21279
+ terminal_notification: "\uD83D\uDD14 \u30BF\u30FC\u30DF\u30CA\u30EB\u901A\u77E5",
21248
21280
  tmux_window_close: "\uD83E\uDE9F \u30A6\u30A3\u30F3\u30C9\u30A6\u9589\u3058\u308B",
21249
21281
  tmux_pane_close: "\uD83D\uDCF1 \u30DA\u30A4\u30F3\u9589\u3058\u308B",
21250
21282
  device_tmux_missing: "\u26A0\uFE0F Tmux \u304C\u3042\u308A\u307E\u305B\u3093",
@@ -21263,7 +21295,8 @@ Bot\uFF1A{{botName}}
21263
21295
  title: "\uD83D\uDD14 {{siteName}} \u304B\u3089\u306E\u30D9\u30EB\uFF1A{{terminalTopbarLabel}}",
21264
21296
  viewLink: "\u8868\u793A",
21265
21297
  terminalTopbarLabel: "\u30A6\u30A3\u30F3\u30C9\u30A6 {{window}} \xB7 \u30DA\u30A4\u30F3 {{pane}} @ {{device}}"
21266
- }
21298
+ },
21299
+ telegramNotification: {}
21267
21300
  },
21268
21301
  sidebar: {
21269
21302
  noWindows: "\u30A6\u30A3\u30F3\u30C9\u30A6\u304C\u3042\u308A\u307E\u305B\u3093",
@@ -21550,6 +21583,7 @@ __export(exports_schema, {
21550
21583
  OptionU32Schema: () => OptionU32Schema,
21551
21584
  OptionU16Schema: () => OptionU16Schema,
21552
21585
  OptionStringSchema: () => OptionStringSchema,
21586
+ NotificationEventSchema: () => NotificationEventSchema,
21553
21587
  LiveResumeSchema: () => LiveResumeSchema,
21554
21588
  LayoutChangeEventSchema: () => LayoutChangeEventSchema,
21555
21589
  HelloS2CSchema: () => HelloS2CSchema,
@@ -21776,6 +21810,16 @@ var BellEventSchema = import_zorsh.b.struct({
21776
21810
  paneIndex: OptionU16Schema,
21777
21811
  paneUrl: OptionStringSchema
21778
21812
  });
21813
+ var NotificationEventSchema = import_zorsh.b.struct({
21814
+ source: import_zorsh.b.u8(),
21815
+ title: OptionStringSchema,
21816
+ body: import_zorsh.b.string(),
21817
+ windowId: OptionStringSchema,
21818
+ paneId: OptionStringSchema,
21819
+ windowIndex: OptionU16Schema,
21820
+ paneIndex: OptionU16Schema,
21821
+ paneUrl: OptionStringSchema
21822
+ });
21779
21823
  // ../shared/src/ws-borsh/codec.ts
21780
21824
  var MAGIC = new Uint8Array([84, 88]);
21781
21825
  var CURRENT_VERSION = 1;
@@ -21988,6 +22032,16 @@ function resetChunkStreamId() {
21988
22032
  nextChunkStreamId = 1;
21989
22033
  }
21990
22034
  // ../shared/src/ws-borsh/convert.ts
22035
+ var notificationSourceToU8 = {
22036
+ osc9: 1,
22037
+ osc777: 2,
22038
+ osc1337: 3
22039
+ };
22040
+ var notificationSourceFromU8 = {
22041
+ 1: "osc9",
22042
+ 2: "osc777",
22043
+ 3: "osc1337"
22044
+ };
21991
22045
  function encodeDeviceEventPayload(payload) {
21992
22046
  const eventTypeMap = {
21993
22047
  "tmux-missing": 1,
@@ -22015,7 +22069,8 @@ function encodeTmuxEventPayload(payload) {
22015
22069
  "pane-active": 7,
22016
22070
  "layout-change": 8,
22017
22071
  bell: 9,
22018
- output: 10
22072
+ output: 10,
22073
+ notification: 11
22019
22074
  };
22020
22075
  const eventData = encodeEventData(payload.type, payload.data);
22021
22076
  const wireData = {
@@ -22083,6 +22138,19 @@ function encodeEventData(type, data) {
22083
22138
  }
22084
22139
  case "output":
22085
22140
  return new Uint8Array;
22141
+ case "notification": {
22142
+ const d = data;
22143
+ return NotificationEventSchema.serialize({
22144
+ source: notificationSourceToU8[d.source],
22145
+ title: d.title ?? null,
22146
+ body: d.body,
22147
+ windowId: d.windowId ?? null,
22148
+ paneId: d.paneId ?? null,
22149
+ windowIndex: d.windowIndex ?? null,
22150
+ paneIndex: d.paneIndex ?? null,
22151
+ paneUrl: d.paneUrl ?? null
22152
+ });
22153
+ }
22086
22154
  default:
22087
22155
  return new Uint8Array;
22088
22156
  }
@@ -22149,9 +22217,13 @@ function decodeTmuxEventPayload(data) {
22149
22217
  7: "pane-active",
22150
22218
  8: "layout-change",
22151
22219
  9: "bell",
22152
- 10: "output"
22220
+ 10: "output",
22221
+ 11: "notification"
22153
22222
  };
22154
- const type = eventTypeMap[wire.eventType] ?? "output";
22223
+ const type = eventTypeMap[wire.eventType];
22224
+ if (!type) {
22225
+ throw new Error(`Unknown tmux event type: ${wire.eventType}`);
22226
+ }
22155
22227
  return {
22156
22228
  deviceId: wire.deviceId,
22157
22229
  type,
@@ -22189,6 +22261,19 @@ function decodeEventData(type, data) {
22189
22261
  paneUrl: bell.paneUrl ?? undefined
22190
22262
  };
22191
22263
  }
22264
+ case "notification": {
22265
+ const notification = NotificationEventSchema.deserialize(data);
22266
+ return {
22267
+ source: notificationSourceFromU8[notification.source] ?? "osc9",
22268
+ title: notification.title ?? undefined,
22269
+ body: notification.body,
22270
+ windowId: notification.windowId ?? undefined,
22271
+ paneId: notification.paneId ?? undefined,
22272
+ windowIndex: notification.windowIndex ?? undefined,
22273
+ paneIndex: notification.paneIndex ?? undefined,
22274
+ paneUrl: notification.paneUrl ?? undefined
22275
+ };
22276
+ }
22192
22277
  default:
22193
22278
  return {};
22194
22279
  }
@@ -22308,6 +22393,13 @@ var runtimeController = new RuntimeController;
22308
22393
  function getEnv(key, defaultValue) {
22309
22394
  return process.env[key] ?? defaultValue;
22310
22395
  }
22396
+ function getBooleanEnv(key, defaultValue) {
22397
+ const value = process.env[key];
22398
+ if (value === undefined) {
22399
+ return defaultValue;
22400
+ }
22401
+ return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
22402
+ }
22311
22403
  var config = {
22312
22404
  masterKey: process.env.TMEX_MASTER_KEY,
22313
22405
  port: Number.parseInt(getEnv("GATEWAY_PORT", "9663"), 10),
@@ -22315,6 +22407,8 @@ var config = {
22315
22407
  siteNameDefault: getEnv("TMEX_SITE_NAME", "tmex"),
22316
22408
  databaseUrl: getEnv("DATABASE_URL", "./tmex.db"),
22317
22409
  bellThrottleSecondsDefault: Number.parseInt(getEnv("TMEX_BELL_THROTTLE_SECONDS", "6"), 10),
22410
+ notificationThrottleSecondsDefault: Number.parseInt(getEnv("TMEX_NOTIFICATION_THROTTLE_SECONDS", "3"), 10),
22411
+ tmuxAllowPassthrough: getBooleanEnv("TMEX_TMUX_ALLOW_PASSTHROUGH", false),
22318
22412
  sshReconnectMaxRetriesDefault: Number.parseInt(getEnv("TMEX_SSH_RECONNECT_MAX_RETRIES", "2"), 10),
22319
22413
  sshReconnectDelaySecondsDefault: Number.parseInt(getEnv("TMEX_SSH_RECONNECT_DELAY_SECONDS", "10"), 10),
22320
22414
  languageDefault: getEnv("TMEX_DEFAULT_LANGUAGE", "en_US"),
@@ -28753,8 +28847,13 @@ var siteSettings = sqliteTable("site_settings", {
28753
28847
  siteName: text("site_name").notNull(),
28754
28848
  siteUrl: text("site_url").notNull(),
28755
28849
  bellThrottleSeconds: integer("bell_throttle_seconds").notNull(),
28850
+ notificationThrottleSeconds: integer("notification_throttle_seconds").notNull().default(3),
28756
28851
  enableBrowserBellToast: integer("enable_browser_bell_toast", { mode: "boolean" }).notNull().default(true),
28852
+ enableBrowserNotificationToast: integer("enable_browser_notification_toast", { mode: "boolean" }).notNull().default(true),
28757
28853
  enableTelegramBellPush: integer("enable_telegram_bell_push", { mode: "boolean" }).notNull().default(true),
28854
+ enableTelegramNotificationPush: integer("enable_telegram_notification_push", {
28855
+ mode: "boolean"
28856
+ }).notNull().default(true),
28758
28857
  sshReconnectMaxRetries: integer("ssh_reconnect_max_retries").notNull(),
28759
28858
  sshReconnectDelaySeconds: integer("ssh_reconnect_delay_seconds").notNull(),
28760
28859
  language: text("language").notNull().default("en_US"),
@@ -28869,8 +28968,11 @@ function toSiteSettings(row) {
28869
28968
  siteName: row.siteName,
28870
28969
  siteUrl: row.siteUrl,
28871
28970
  bellThrottleSeconds: row.bellThrottleSeconds,
28971
+ notificationThrottleSeconds: row.notificationThrottleSeconds,
28872
28972
  enableBrowserBellToast: row.enableBrowserBellToast,
28973
+ enableBrowserNotificationToast: row.enableBrowserNotificationToast,
28873
28974
  enableTelegramBellPush: row.enableTelegramBellPush,
28975
+ enableTelegramNotificationPush: row.enableTelegramNotificationPush,
28874
28976
  sshReconnectMaxRetries: row.sshReconnectMaxRetries,
28875
28977
  sshReconnectDelaySeconds: row.sshReconnectDelaySeconds,
28876
28978
  language: normalizeLocale(row.language),
@@ -28921,8 +29023,11 @@ function ensureSiteSettingsInitialized() {
28921
29023
  siteName: config.siteNameDefault,
28922
29024
  siteUrl: config.baseUrl,
28923
29025
  bellThrottleSeconds: config.bellThrottleSecondsDefault,
29026
+ notificationThrottleSeconds: config.notificationThrottleSecondsDefault,
28924
29027
  enableBrowserBellToast: true,
29028
+ enableBrowserNotificationToast: true,
28925
29029
  enableTelegramBellPush: true,
29030
+ enableTelegramNotificationPush: true,
28926
29031
  sshReconnectMaxRetries: config.sshReconnectMaxRetriesDefault,
28927
29032
  sshReconnectDelaySeconds: config.sshReconnectDelaySecondsDefault,
28928
29033
  language: normalizeLocale(config.languageDefault),
@@ -29048,8 +29153,11 @@ function updateSiteSettings(updates) {
29048
29153
  siteName: updates.siteName ?? current.siteName,
29049
29154
  siteUrl: updates.siteUrl ?? current.siteUrl,
29050
29155
  bellThrottleSeconds: updates.bellThrottleSeconds ?? current.bellThrottleSeconds,
29156
+ notificationThrottleSeconds: updates.notificationThrottleSeconds ?? current.notificationThrottleSeconds,
29051
29157
  enableBrowserBellToast: updates.enableBrowserBellToast ?? current.enableBrowserBellToast,
29158
+ enableBrowserNotificationToast: updates.enableBrowserNotificationToast ?? current.enableBrowserNotificationToast,
29052
29159
  enableTelegramBellPush: updates.enableTelegramBellPush ?? current.enableTelegramBellPush,
29160
+ enableTelegramNotificationPush: updates.enableTelegramNotificationPush ?? current.enableTelegramNotificationPush,
29053
29161
  sshReconnectMaxRetries: updates.sshReconnectMaxRetries ?? current.sshReconnectMaxRetries,
29054
29162
  sshReconnectDelaySeconds: updates.sshReconnectDelaySeconds ?? current.sshReconnectDelaySeconds,
29055
29163
  language: updates.language ? normalizeLocale(updates.language) : current.language,
@@ -29060,8 +29168,11 @@ function updateSiteSettings(updates) {
29060
29168
  siteName: next.siteName,
29061
29169
  siteUrl: next.siteUrl,
29062
29170
  bellThrottleSeconds: next.bellThrottleSeconds,
29171
+ notificationThrottleSeconds: next.notificationThrottleSeconds,
29063
29172
  enableBrowserBellToast: next.enableBrowserBellToast,
29173
+ enableBrowserNotificationToast: next.enableBrowserNotificationToast,
29064
29174
  enableTelegramBellPush: next.enableTelegramBellPush,
29175
+ enableTelegramNotificationPush: next.enableTelegramNotificationPush,
29065
29176
  sshReconnectMaxRetries: next.sshReconnectMaxRetries,
29066
29177
  sshReconnectDelaySeconds: next.sshReconnectDelaySeconds,
29067
29178
  language: next.language,
@@ -51725,6 +51836,7 @@ class EventNotifier {
51725
51836
  lastRefresh = 0;
51726
51837
  REFRESH_INTERVAL = 60000;
51727
51838
  bellThrottleMap = new Map;
51839
+ notificationThrottleMap = new Map;
51728
51840
  refreshConfig() {
51729
51841
  const now = Date.now();
51730
51842
  if (now - this.lastRefresh < this.REFRESH_INTERVAL)
@@ -51740,8 +51852,14 @@ class EventNotifier {
51740
51852
  eventType,
51741
51853
  timestamp: new Date().toISOString()
51742
51854
  };
51743
- if (eventType === "terminal_bell" && !this.shouldPassBellThrottle(fullEvent)) {
51744
- return;
51855
+ if (eventType === "terminal_bell") {
51856
+ if (!this.shouldPassBellThrottle(fullEvent)) {
51857
+ return;
51858
+ }
51859
+ } else if (eventType === "terminal_notification") {
51860
+ if (!this.shouldPassNotificationThrottle(fullEvent)) {
51861
+ return;
51862
+ }
51745
51863
  }
51746
51864
  await Promise.all([
51747
51865
  this.sendWebhooks(eventType, fullEvent),
@@ -51763,6 +51881,22 @@ class EventNotifier {
51763
51881
  this.bellThrottleMap.set(key, now);
51764
51882
  return true;
51765
51883
  }
51884
+ shouldPassNotificationThrottle(event) {
51885
+ const settings = getSiteSettings();
51886
+ const throttleMs = Math.max(0, settings.notificationThrottleSeconds) * 1000;
51887
+ if (throttleMs === 0) {
51888
+ return true;
51889
+ }
51890
+ const source = typeof event.payload?.source === "string" ? event.payload.source : "unknown";
51891
+ const key = `${event.device.id}:${event.tmux?.paneId ?? "-"}:notification:${source}`;
51892
+ const now = Date.now();
51893
+ const previous = this.notificationThrottleMap.get(key) ?? 0;
51894
+ if (now - previous < throttleMs) {
51895
+ return false;
51896
+ }
51897
+ this.notificationThrottleMap.set(key, now);
51898
+ return true;
51899
+ }
51766
51900
  async sendWebhooks(eventType, event) {
51767
51901
  const targets = this.webhooks.filter((w) => w.eventMask.includes(eventType));
51768
51902
  await Promise.all(targets.map(async (webhook) => {
@@ -51800,6 +51934,14 @@ class EventNotifier {
51800
51934
  await telegramService.sendToAuthorizedChats({ text: bellMessage, parseMode: "HTML" });
51801
51935
  return;
51802
51936
  }
51937
+ if (eventType === "terminal_notification") {
51938
+ if (!settings.enableTelegramNotificationPush) {
51939
+ return;
51940
+ }
51941
+ const notificationMessage = this.formatTelegramNotificationMessage(event);
51942
+ await telegramService.sendToAuthorizedChats({ text: notificationMessage, parseMode: "HTML" });
51943
+ return;
51944
+ }
51803
51945
  const message = this.formatTelegramMessage(event, settings);
51804
51946
  await telegramService.sendToAuthorizedChats({ text: message });
51805
51947
  }
@@ -51824,11 +51966,34 @@ class EventNotifier {
51824
51966
  lines.push("", `<a href="${escapeTelegramHtmlAttribute(tgSafePaneUrl)}">${escapeTelegramHtmlText(t2("notification.telegramBell.viewLink"))}</a>`);
51825
51967
  }
51826
51968
  return lines.join(`
51969
+ `);
51970
+ }
51971
+ formatTelegramNotificationMessage(event) {
51972
+ const title = typeof event.payload?.title === "string" ? event.payload.title : "";
51973
+ const body = typeof event.payload?.message === "string" ? event.payload.message : "";
51974
+ const lines = [];
51975
+ if (title) {
51976
+ lines.push(escapeTelegramHtmlText(title));
51977
+ }
51978
+ if (body) {
51979
+ lines.push(escapeTelegramHtmlText(body));
51980
+ }
51981
+ const paneUrl = normalizeHttpUrl(buildPaneUrl(event));
51982
+ const topbarLabel = this.buildTerminalTopbarLabel(event);
51983
+ const footer = `from ${event.site.name}: ${topbarLabel}`;
51984
+ if (paneUrl) {
51985
+ const tgSafePaneUrl = encodePercentForTelegramUrl(paneUrl);
51986
+ lines.push("", `<a href="${escapeTelegramHtmlAttribute(tgSafePaneUrl)}">${escapeTelegramHtmlText(footer)}</a>`);
51987
+ } else {
51988
+ lines.push("", escapeTelegramHtmlText(footer));
51989
+ }
51990
+ return lines.join(`
51827
51991
  `);
51828
51992
  }
51829
51993
  formatTelegramMessage(event, settings) {
51830
51994
  const emojiMap = {
51831
51995
  terminal_bell: "\uD83D\uDD14",
51996
+ terminal_notification: "\uD83D\uDD14",
51832
51997
  tmux_window_close: "\uD83E\uDE9F",
51833
51998
  tmux_pane_close: "\uD83D\uDCF1",
51834
51999
  device_tmux_missing: "\u26A0\uFE0F",
@@ -52075,12 +52240,80 @@ function encodeInputToHexChunks(input, chunkBytes = SEND_KEYS_HEX_CHUNK_BYTES) {
52075
52240
  return chunks;
52076
52241
  }
52077
52242
 
52078
- // ../../apps/gateway/src/tmux-client/pane-title-parser.ts
52243
+ // ../../apps/gateway/src/tmux-client/pane-stream-parser.ts
52079
52244
  var decoder = new TextDecoder;
52080
- function createPaneTitleParser(options) {
52245
+ var MAX_OSC_KIND_BYTES = 16;
52246
+ var MAX_OSC_PAYLOAD_BYTES = 8 * 1024;
52247
+ function createPaneStreamParser(options) {
52081
52248
  let phase = "normal";
52082
52249
  let oscKind = "";
52250
+ let oscPayloadBytes = [];
52083
52251
  let titleBytes = [];
52252
+ let warnedOscPayloadOverflow = false;
52253
+ function resetOscState() {
52254
+ oscKind = "";
52255
+ oscPayloadBytes = [];
52256
+ }
52257
+ function appendOscPayloadByte(byte) {
52258
+ if (oscPayloadBytes.length >= MAX_OSC_PAYLOAD_BYTES) {
52259
+ if (!warnedOscPayloadOverflow) {
52260
+ warnedOscPayloadOverflow = true;
52261
+ console.warn("[tmex] pane stream parser dropped oversized OSC payload");
52262
+ }
52263
+ oscPayloadBytes = [];
52264
+ phase = "osc-body-ignore";
52265
+ return false;
52266
+ }
52267
+ oscPayloadBytes.push(byte);
52268
+ return true;
52269
+ }
52270
+ function emitTitle(bytes) {
52271
+ const title = decoder.decode(new Uint8Array(bytes)).trim();
52272
+ if (!title) {
52273
+ return;
52274
+ }
52275
+ options.onTitle(title);
52276
+ }
52277
+ function emitOsc() {
52278
+ const payload = decoder.decode(new Uint8Array(oscPayloadBytes));
52279
+ switch (oscKind) {
52280
+ case "0":
52281
+ case "1":
52282
+ case "2":
52283
+ emitTitle(oscPayloadBytes);
52284
+ return;
52285
+ case "9":
52286
+ if (/^4(;|$)/.test(payload)) {
52287
+ return;
52288
+ }
52289
+ options.onNotification({ source: "osc9", body: payload });
52290
+ return;
52291
+ case "777": {
52292
+ const verbSeparatorIndex = payload.indexOf(";");
52293
+ const verb = verbSeparatorIndex >= 0 ? payload.slice(0, verbSeparatorIndex) : payload;
52294
+ if (verb !== "notify") {
52295
+ return;
52296
+ }
52297
+ const rest = verbSeparatorIndex >= 0 ? payload.slice(verbSeparatorIndex + 1) : "";
52298
+ const titleSeparatorIndex = rest.indexOf(";");
52299
+ const title = titleSeparatorIndex >= 0 ? rest.slice(0, titleSeparatorIndex) : rest;
52300
+ const body = titleSeparatorIndex >= 0 ? rest.slice(titleSeparatorIndex + 1) : "";
52301
+ options.onNotification({
52302
+ source: "osc777",
52303
+ title: title || undefined,
52304
+ body
52305
+ });
52306
+ return;
52307
+ }
52308
+ case "1337":
52309
+ if (/^RequestAttention=(yes|once|fireworks|true)$/i.test(payload)) {
52310
+ options.onNotification({ source: "osc1337", body: "RequestAttention" });
52311
+ }
52312
+ return;
52313
+ default:
52314
+ return;
52315
+ }
52316
+ }
52084
52317
  return {
52085
52318
  push(data) {
52086
52319
  const output = [];
@@ -52088,36 +52321,57 @@ function createPaneTitleParser(options) {
52088
52321
  if (phase === "normal") {
52089
52322
  if (byte === 27) {
52090
52323
  phase = "esc";
52091
- } else {
52092
- output.push(byte);
52324
+ continue;
52093
52325
  }
52326
+ if (byte === 7) {
52327
+ options.onBell();
52328
+ continue;
52329
+ }
52330
+ output.push(byte);
52094
52331
  continue;
52095
52332
  }
52096
52333
  if (phase === "esc") {
52097
52334
  if (byte === 93) {
52098
- phase = "osc";
52099
- oscKind = "";
52335
+ resetOscState();
52336
+ phase = "osc-params";
52337
+ continue;
52338
+ }
52339
+ if (byte === 107) {
52100
52340
  titleBytes = [];
52341
+ phase = "screen-title";
52101
52342
  continue;
52102
52343
  }
52103
52344
  output.push(27, byte);
52104
52345
  phase = "normal";
52105
52346
  continue;
52106
52347
  }
52107
- if (phase === "osc") {
52348
+ if (phase === "osc-params") {
52108
52349
  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
- }
52350
+ phase = oscKind === "0" || oscKind === "1" || oscKind === "2" || oscKind === "9" || oscKind === "777" || oscKind === "1337" ? "osc-body" : "osc-body-ignore";
52351
+ continue;
52352
+ }
52353
+ if (byte === 7) {
52354
+ emitOsc();
52355
+ resetOscState();
52356
+ phase = "normal";
52357
+ continue;
52358
+ }
52359
+ if (byte === 27) {
52360
+ phase = "osc-st";
52361
+ continue;
52362
+ }
52363
+ if (oscKind.length >= MAX_OSC_KIND_BYTES) {
52364
+ resetOscState();
52365
+ phase = "osc-body-ignore";
52113
52366
  continue;
52114
52367
  }
52115
52368
  oscKind += String.fromCharCode(byte);
52116
52369
  continue;
52117
52370
  }
52118
- if (phase === "osc-data") {
52371
+ if (phase === "osc-body") {
52119
52372
  if (byte === 7) {
52120
- emitTitle(options.onTitle, titleBytes);
52373
+ emitOsc();
52374
+ resetOscState();
52121
52375
  phase = "normal";
52122
52376
  continue;
52123
52377
  }
@@ -52125,31 +52379,69 @@ function createPaneTitleParser(options) {
52125
52379
  phase = "osc-st";
52126
52380
  continue;
52127
52381
  }
52382
+ appendOscPayloadByte(byte);
52383
+ continue;
52384
+ }
52385
+ if (phase === "osc-body-ignore") {
52386
+ if (byte === 7) {
52387
+ resetOscState();
52388
+ phase = "normal";
52389
+ continue;
52390
+ }
52391
+ if (byte === 27) {
52392
+ phase = "osc-st-ignore";
52393
+ }
52394
+ continue;
52395
+ }
52396
+ if (phase === "osc-st") {
52397
+ if (byte === 92) {
52398
+ emitOsc();
52399
+ resetOscState();
52400
+ phase = "normal";
52401
+ continue;
52402
+ }
52403
+ if (!appendOscPayloadByte(27)) {
52404
+ continue;
52405
+ }
52406
+ appendOscPayloadByte(byte);
52407
+ continue;
52408
+ }
52409
+ if (phase === "osc-st-ignore") {
52410
+ if (byte === 92) {
52411
+ resetOscState();
52412
+ phase = "normal";
52413
+ continue;
52414
+ }
52415
+ phase = "osc-body-ignore";
52416
+ continue;
52417
+ }
52418
+ if (phase === "screen-title") {
52419
+ if (byte === 7) {
52420
+ emitTitle(titleBytes);
52421
+ titleBytes = [];
52422
+ phase = "normal";
52423
+ continue;
52424
+ }
52425
+ if (byte === 27) {
52426
+ phase = "screen-title-st";
52427
+ continue;
52428
+ }
52128
52429
  titleBytes.push(byte);
52129
52430
  continue;
52130
52431
  }
52131
52432
  if (byte === 92) {
52132
- emitTitle(options.onTitle, titleBytes);
52433
+ emitTitle(titleBytes);
52434
+ titleBytes = [];
52133
52435
  phase = "normal";
52134
52436
  continue;
52135
52437
  }
52136
52438
  titleBytes.push(27, byte);
52137
- phase = "osc-data";
52439
+ phase = "screen-title";
52138
52440
  }
52139
52441
  return new Uint8Array(output);
52140
52442
  }
52141
52443
  };
52142
52444
  }
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
52445
 
52154
52446
  // ../../apps/gateway/src/tmux-client/local-external-connection.ts
52155
52447
  function hasRenderableTerminalContent(value) {
@@ -52193,8 +52485,7 @@ class LocalExternalTmuxConnection {
52193
52485
  pendingPaneTitles = new Map;
52194
52486
  snapshotSession = null;
52195
52487
  snapshotWindows = new Map;
52196
- currentPipePaneId = null;
52197
- pipeReadAbort = null;
52488
+ paneReaders = new Map;
52198
52489
  pipeTransition = Promise.resolve();
52199
52490
  inputTransition = Promise.resolve();
52200
52491
  hookReadAbort = null;
@@ -52231,6 +52522,7 @@ class LocalExternalTmuxConnection {
52231
52522
  });
52232
52523
  this.ensureRuntimeDirs();
52233
52524
  await this.ensureSession();
52525
+ await this.configureSessionOptions();
52234
52526
  if (this.deps.enableHooks) {
52235
52527
  await this.startHooks();
52236
52528
  }
@@ -52248,7 +52540,7 @@ class LocalExternalTmuxConnection {
52248
52540
  }
52249
52541
  this.manualDisconnect = true;
52250
52542
  this.connected = false;
52251
- this.stopPipe();
52543
+ this.stopAllPipeReaders();
52252
52544
  if (this.deps.enableHooks) {
52253
52545
  this.stopHooks();
52254
52546
  }
@@ -52346,6 +52638,26 @@ class LocalExternalTmuxConnection {
52346
52638
  }
52347
52639
  await this.runTmux(["new-session", "-d", "-c", homedir(), "-s", this.sessionName]);
52348
52640
  }
52641
+ async configureSessionOptions() {
52642
+ await this.runTmuxAllowFailure([
52643
+ "set-option",
52644
+ "-t",
52645
+ this.sessionName,
52646
+ "-s",
52647
+ "allow-passthrough",
52648
+ config.tmuxAllowPassthrough ? "on" : "off"
52649
+ ]);
52650
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "extended-keys", "on"]);
52651
+ await this.runTmuxAllowFailure([
52652
+ "set-option",
52653
+ "-t",
52654
+ this.sessionName,
52655
+ "-s",
52656
+ "extended-keys-format",
52657
+ "csi-u"
52658
+ ]);
52659
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
52660
+ }
52349
52661
  ensureRuntimeDirs() {
52350
52662
  mkdirSync(this.fsPaths.rootDir, { recursive: true, mode: 448 });
52351
52663
  mkdirSync(this.fsPaths.panesDir, { recursive: true, mode: 448 });
@@ -52381,14 +52693,16 @@ class LocalExternalTmuxConnection {
52381
52693
  readerProcess.kill();
52382
52694
  rmSync(fifoPath, { force: true });
52383
52695
  };
52384
- await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
52385
52696
  await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
52386
52697
  await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
52698
+ await this.installHook("after-new-window", ["refresh"]);
52699
+ await this.installHook("after-split-window", ["refresh"]);
52387
52700
  }
52388
52701
  async stopHooks() {
52389
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
52390
52702
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
52391
52703
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
52704
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-new-window"]);
52705
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-split-window"]);
52392
52706
  this.hookReadAbort?.();
52393
52707
  this.hookReadAbort = null;
52394
52708
  this.hookBuffer = "";
@@ -52419,22 +52733,9 @@ class LocalExternalTmuxConnection {
52419
52733
  }
52420
52734
  const [type, windowId, paneId] = line.split("\t");
52421
52735
  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
52736
  continue;
52436
52737
  }
52437
- if (type === "pane-exited" || type === "pane-died") {
52738
+ if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
52438
52739
  this.requestSnapshot();
52439
52740
  }
52440
52741
  }
@@ -52465,7 +52766,6 @@ class LocalExternalTmuxConnection {
52465
52766
  this.activePaneId = paneId;
52466
52767
  await this.runTmux(["select-window", "-t", windowId], true);
52467
52768
  await this.runTmux(["select-pane", "-t", paneId], true);
52468
- await this.startPipeForPane(paneId);
52469
52769
  if (size) {
52470
52770
  await this.resizePaneInternal(paneId, size.cols, size.rows);
52471
52771
  }
@@ -52520,6 +52820,7 @@ class LocalExternalTmuxConnection {
52520
52820
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
52521
52821
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
52522
52822
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
52823
+ await this.syncPipeReaders();
52523
52824
  this.emitSnapshot();
52524
52825
  }
52525
52826
  parseSnapshotSession(lines) {
@@ -52617,69 +52918,122 @@ class LocalExternalTmuxConnection {
52617
52918
  }
52618
52919
  return null;
52619
52920
  }
52620
- async startPipeForPane(paneId) {
52621
- await this.queuePipeTransition(async () => {
52622
- if (this.currentPipePaneId === paneId) {
52623
- return;
52921
+ recordBell(paneId, windowId) {
52922
+ const key = paneId || windowId || "-";
52923
+ const previous = this.bellDedup.get(key) ?? 0;
52924
+ const now = Date.now();
52925
+ if (now - previous < BELL_DEDUP_WINDOW_MS) {
52926
+ return;
52927
+ }
52928
+ this.bellDedup.set(key, now);
52929
+ this.callbacks.onEvent({
52930
+ type: "bell",
52931
+ data: {
52932
+ windowId,
52933
+ paneId: paneId || this.activePaneId || undefined
52934
+ }
52935
+ });
52936
+ }
52937
+ emitNotification(paneId, notification) {
52938
+ this.callbacks.onEvent({
52939
+ type: "notification",
52940
+ data: {
52941
+ paneId,
52942
+ ...notification
52943
+ }
52944
+ });
52945
+ }
52946
+ getExpectedPaneIds() {
52947
+ return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
52948
+ }
52949
+ async startPipeForPaneNow(paneId) {
52950
+ if (this.paneReaders.has(paneId)) {
52951
+ return;
52952
+ }
52953
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
52954
+ this.ensureRuntimeDirs();
52955
+ rmSync(fifoPath, { force: true });
52956
+ await this.runShell(`mkfifo ${quoteShellArg(fifoPath)}`);
52957
+ const parser = createPaneStreamParser({
52958
+ onTitle: (title) => {
52959
+ this.pendingPaneTitles.set(paneId, title);
52960
+ this.requestSnapshot();
52961
+ },
52962
+ onBell: () => {
52963
+ this.recordBell(paneId);
52964
+ },
52965
+ onNotification: (notification) => {
52966
+ this.emitNotification(paneId, notification);
52624
52967
  }
52625
- await this.stopPipeNow();
52626
- const fifoPath = this.fsPaths.paneFifoPath(paneId);
52627
- this.ensureRuntimeDirs();
52968
+ });
52969
+ const readerProcess = Bun.spawn(["/bin/sh", "-lc", `cat ${quoteShellArg(fifoPath)}`], {
52970
+ stdout: "pipe",
52971
+ stderr: "pipe"
52972
+ });
52973
+ const reader = readerProcess.stdout.getReader();
52974
+ const stopReader = () => {
52975
+ reader.releaseLock();
52976
+ readerProcess.kill();
52628
52977
  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
- }
52978
+ };
52979
+ this.paneReaders.set(paneId, { paneId, fifoPath, stopReader });
52980
+ (async () => {
52981
+ try {
52982
+ while (true) {
52983
+ const chunk2 = await reader.read();
52984
+ if (chunk2.done) {
52985
+ break;
52656
52986
  }
52657
- } catch (error) {
52658
- if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52659
- this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52987
+ const output = parser.push(chunk2.value);
52988
+ if (output.length > 0) {
52989
+ this.callbacks.onTerminalOutput(paneId, output);
52660
52990
  }
52661
52991
  }
52662
- })();
52663
- this.pipeReadAbort = () => {
52664
- reader.releaseLock();
52665
- readerProcess.kill();
52666
- rmSync(fifoPath, { force: true });
52667
- };
52992
+ } catch (error) {
52993
+ if (!this.manualDisconnect && !shouldIgnoreReaderAbortError(error)) {
52994
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
52995
+ }
52996
+ }
52997
+ })();
52998
+ try {
52668
52999
  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());
53000
+ } catch (error) {
53001
+ this.paneReaders.delete(paneId);
53002
+ stopReader();
53003
+ throw error;
53004
+ }
52674
53005
  }
52675
- async stopPipeNow() {
52676
- const paneId = this.currentPipePaneId;
52677
- this.currentPipePaneId = null;
52678
- if (paneId) {
52679
- await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
53006
+ async stopPipeForPaneNow(paneId) {
53007
+ const handle = this.paneReaders.get(paneId);
53008
+ if (!handle) {
53009
+ return;
52680
53010
  }
52681
- this.pipeReadAbort?.();
52682
- this.pipeReadAbort = null;
53011
+ this.paneReaders.delete(paneId);
53012
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
53013
+ handle.stopReader();
53014
+ }
53015
+ async syncPipeReaders() {
53016
+ const expectedPaneIds = this.getExpectedPaneIds();
53017
+ const expectedSet = new Set(expectedPaneIds);
53018
+ await this.queuePipeTransition(async () => {
53019
+ for (const paneId of Array.from(this.paneReaders.keys())) {
53020
+ if (!expectedSet.has(paneId)) {
53021
+ await this.stopPipeForPaneNow(paneId);
53022
+ }
53023
+ }
53024
+ for (const paneId of expectedPaneIds) {
53025
+ if (!this.paneReaders.has(paneId)) {
53026
+ await this.startPipeForPaneNow(paneId);
53027
+ }
53028
+ }
53029
+ });
53030
+ }
53031
+ async stopAllPipeReaders() {
53032
+ await this.queuePipeTransition(async () => {
53033
+ for (const paneId of Array.from(this.paneReaders.keys())) {
53034
+ await this.stopPipeForPaneNow(paneId);
53035
+ }
53036
+ });
52683
53037
  }
52684
53038
  queuePipeTransition(task) {
52685
53039
  const next = this.pipeTransition.catch(() => {
@@ -53068,8 +53422,7 @@ class SshExternalTmuxConnection {
53068
53422
  pendingPaneTitles = new Map;
53069
53423
  snapshotSession = null;
53070
53424
  snapshotWindows = new Map;
53071
- currentPipePaneId = null;
53072
- pipeReadAbort = null;
53425
+ paneReaders = new Map;
53073
53426
  pipeTransition = Promise.resolve();
53074
53427
  hookReadAbort = null;
53075
53428
  hookBuffer = "";
@@ -53115,6 +53468,7 @@ class SshExternalTmuxConnection {
53115
53468
  await this.openCommandChannel();
53116
53469
  await this.ensureRemoteRuntimeDirs();
53117
53470
  await this.ensureSession();
53471
+ await this.configureSessionOptions();
53118
53472
  await this.startHooks();
53119
53473
  this.connected = true;
53120
53474
  updateDeviceRuntimeStatus(this.deviceId, {
@@ -53323,6 +53677,26 @@ class SshExternalTmuxConnection {
53323
53677
  }
53324
53678
  await this.runTmux(["new-session", "-d", "-c", this.remoteHomeDir, "-s", this.sessionName]);
53325
53679
  }
53680
+ async configureSessionOptions() {
53681
+ await this.runTmuxAllowFailure([
53682
+ "set-option",
53683
+ "-t",
53684
+ this.sessionName,
53685
+ "-s",
53686
+ "allow-passthrough",
53687
+ config.tmuxAllowPassthrough ? "on" : "off"
53688
+ ]);
53689
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "extended-keys", "on"]);
53690
+ await this.runTmuxAllowFailure([
53691
+ "set-option",
53692
+ "-t",
53693
+ this.sessionName,
53694
+ "-s",
53695
+ "extended-keys-format",
53696
+ "csi-u"
53697
+ ]);
53698
+ await this.runTmuxAllowFailure(["set-option", "-t", this.sessionName, "-g", "focus-events", "on"]);
53699
+ }
53326
53700
  async startHooks() {
53327
53701
  await this.ensureRemoteRuntimeDirs();
53328
53702
  const fifoPath = this.fsPaths.hookFifoPath;
@@ -53341,14 +53715,16 @@ class SshExternalTmuxConnection {
53341
53715
  stopReader();
53342
53716
  this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53343
53717
  };
53344
- await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
53345
53718
  await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
53346
53719
  await this.installHook("pane-died", ["pane-died", "#{window_id}", "#{pane_id}"]);
53720
+ await this.installHook("after-new-window", ["refresh"]);
53721
+ await this.installHook("after-split-window", ["refresh"]);
53347
53722
  }
53348
53723
  async stopHooks() {
53349
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
53350
53724
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
53351
53725
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-died"]);
53726
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-new-window"]);
53727
+ await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "after-split-window"]);
53352
53728
  this.hookReadAbort?.();
53353
53729
  this.hookReadAbort = null;
53354
53730
  this.hookBuffer = "";
@@ -53379,22 +53755,9 @@ class SshExternalTmuxConnection {
53379
53755
  }
53380
53756
  const [type, windowId, paneId] = line.split("\t");
53381
53757
  if (type === "bell") {
53382
- const key = paneId || windowId || "-";
53383
- const previous = this.bellDedup.get(key) ?? 0;
53384
- const now = Date.now();
53385
- if (now - previous >= BELL_DEDUP_WINDOW_MS2) {
53386
- this.bellDedup.set(key, now);
53387
- this.callbacks.onEvent({
53388
- type: "bell",
53389
- data: {
53390
- windowId: windowId || undefined,
53391
- paneId: paneId || this.activePaneId || undefined
53392
- }
53393
- });
53394
- }
53395
53758
  continue;
53396
53759
  }
53397
- if (type === "pane-exited" || type === "pane-died") {
53760
+ if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
53398
53761
  this.requestSnapshot();
53399
53762
  }
53400
53763
  }
@@ -53425,7 +53788,6 @@ class SshExternalTmuxConnection {
53425
53788
  this.activePaneId = paneId;
53426
53789
  await this.runTmux(["select-window", "-t", windowId], true);
53427
53790
  await this.runTmux(["select-pane", "-t", paneId], true);
53428
- await this.startPipeForPane(paneId);
53429
53791
  if (size) {
53430
53792
  await this.resizePaneInternal(paneId, size.cols, size.rows);
53431
53793
  }
@@ -53480,6 +53842,7 @@ class SshExternalTmuxConnection {
53480
53842
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
53481
53843
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
53482
53844
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
53845
+ await this.syncPipeReaders();
53483
53846
  this.emitSnapshot();
53484
53847
  }
53485
53848
  parseSnapshotSession(lines) {
@@ -53577,56 +53940,114 @@ class SshExternalTmuxConnection {
53577
53940
  }
53578
53941
  return null;
53579
53942
  }
53580
- async startPipeForPane(paneId) {
53581
- await this.queuePipeTransition(async () => {
53582
- if (this.currentPipePaneId === paneId) {
53583
- return;
53943
+ recordBell(paneId, windowId) {
53944
+ const key = paneId || windowId || "-";
53945
+ const previous = this.bellDedup.get(key) ?? 0;
53946
+ const now = Date.now();
53947
+ if (now - previous < BELL_DEDUP_WINDOW_MS2) {
53948
+ return;
53949
+ }
53950
+ this.bellDedup.set(key, now);
53951
+ this.callbacks.onEvent({
53952
+ type: "bell",
53953
+ data: {
53954
+ windowId,
53955
+ paneId: paneId || this.activePaneId || undefined
53956
+ }
53957
+ });
53958
+ }
53959
+ emitNotification(paneId, notification) {
53960
+ this.callbacks.onEvent({
53961
+ type: "notification",
53962
+ data: {
53963
+ paneId,
53964
+ ...notification
53584
53965
  }
53585
- await this.stopPipeNow();
53586
- const fifoPath = this.fsPaths.paneFifoPath(paneId);
53587
- await this.ensureRemoteRuntimeDirs();
53588
- await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53589
- const parser = createPaneTitleParser({
53590
- onTitle: (title) => {
53591
- this.pendingPaneTitles.set(paneId, title);
53592
- this.requestSnapshot();
53966
+ });
53967
+ }
53968
+ getExpectedPaneIds() {
53969
+ return Array.from(this.snapshotWindows.values()).sort((left, right) => left.index - right.index).flatMap((window2) => window2.panes.map((pane) => pane.id));
53970
+ }
53971
+ async startPipeForPaneNow(paneId) {
53972
+ if (this.paneReaders.has(paneId)) {
53973
+ return;
53974
+ }
53975
+ const fifoPath = this.fsPaths.paneFifoPath(paneId);
53976
+ await this.ensureRemoteRuntimeDirs();
53977
+ await this.runShell(`rm -f ${quoteShellArg(fifoPath)} && mkfifo ${quoteShellArg(fifoPath)} && chmod 600 ${quoteShellArg(fifoPath)}`);
53978
+ const parser = createPaneStreamParser({
53979
+ onTitle: (title) => {
53980
+ this.pendingPaneTitles.set(paneId, title);
53981
+ this.requestSnapshot();
53982
+ },
53983
+ onBell: () => {
53984
+ this.recordBell(paneId);
53985
+ },
53986
+ onNotification: (notification) => {
53987
+ this.emitNotification(paneId, notification);
53988
+ }
53989
+ });
53990
+ const stopReader = await this.openReaderChannel(`exec cat ${quoteShellArg(fifoPath)}`, {
53991
+ onData: (raw) => {
53992
+ const output = parser.push(raw);
53993
+ if (output.length > 0) {
53994
+ this.callbacks.onTerminalOutput(paneId, output);
53593
53995
  }
53594
- });
53595
- const stopReader = await this.openReaderChannel(`exec cat ${quoteShellArg(fifoPath)}`, {
53596
- onData: (raw) => {
53597
- const output = parser.push(raw);
53598
- if (Array.from(raw).includes(7)) {
53599
- this.callbacks.onEvent({ type: "bell", data: { paneId } });
53600
- }
53601
- if (output.length > 0) {
53602
- this.callbacks.onTerminalOutput(paneId, output);
53603
- }
53604
- },
53605
- onClose: () => {
53606
- if (!this.manualDisconnect && this.currentPipePaneId === paneId) {
53607
- this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
53608
- }
53996
+ },
53997
+ onClose: () => {
53998
+ if (!this.manualDisconnect && this.paneReaders.has(paneId)) {
53999
+ this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
53609
54000
  }
53610
- });
53611
- this.pipeReadAbort = () => {
54001
+ }
54002
+ });
54003
+ const handle = {
54004
+ paneId,
54005
+ fifoPath,
54006
+ stopReader: () => {
53612
54007
  stopReader();
53613
54008
  this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53614
- };
54009
+ }
54010
+ };
54011
+ this.paneReaders.set(paneId, handle);
54012
+ try {
53615
54013
  await this.runTmux(["pipe-pane", "-O", "-t", paneId, `cat >${fifoPath}`]);
53616
- this.currentPipePaneId = paneId;
53617
- });
53618
- }
53619
- async stopPipe() {
53620
- await this.queuePipeTransition(() => this.stopPipeNow());
54014
+ } catch (error) {
54015
+ this.paneReaders.delete(paneId);
54016
+ handle.stopReader();
54017
+ throw error;
54018
+ }
53621
54019
  }
53622
- async stopPipeNow() {
53623
- const paneId = this.currentPipePaneId;
53624
- this.currentPipePaneId = null;
53625
- if (paneId) {
53626
- await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
54020
+ async stopPipeForPaneNow(paneId) {
54021
+ const handle = this.paneReaders.get(paneId);
54022
+ if (!handle) {
54023
+ return;
53627
54024
  }
53628
- this.pipeReadAbort?.();
53629
- this.pipeReadAbort = null;
54025
+ this.paneReaders.delete(paneId);
54026
+ await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
54027
+ handle.stopReader();
54028
+ }
54029
+ async syncPipeReaders() {
54030
+ const expectedPaneIds = this.getExpectedPaneIds();
54031
+ const expectedSet = new Set(expectedPaneIds);
54032
+ await this.queuePipeTransition(async () => {
54033
+ for (const paneId of Array.from(this.paneReaders.keys())) {
54034
+ if (!expectedSet.has(paneId)) {
54035
+ await this.stopPipeForPaneNow(paneId);
54036
+ }
54037
+ }
54038
+ for (const paneId of expectedPaneIds) {
54039
+ if (!this.paneReaders.has(paneId)) {
54040
+ await this.startPipeForPaneNow(paneId);
54041
+ }
54042
+ }
54043
+ });
54044
+ }
54045
+ async stopAllPipeReaders() {
54046
+ await this.queuePipeTransition(async () => {
54047
+ for (const paneId of Array.from(this.paneReaders.keys())) {
54048
+ await this.stopPipeForPaneNow(paneId);
54049
+ }
54050
+ });
53630
54051
  }
53631
54052
  queuePipeTransition(task) {
53632
54053
  const next = this.pipeTransition.catch(() => {
@@ -53796,7 +54217,7 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
53796
54217
  }
53797
54218
  this.connected = false;
53798
54219
  this.cleanupPromise = (async () => {
53799
- await this.stopPipe().catch(() => {
54220
+ await this.stopAllPipeReaders().catch(() => {
53800
54221
  return;
53801
54222
  });
53802
54223
  await this.stopHooks().catch(() => {
@@ -54025,7 +54446,7 @@ function pickPaneById(windows, paneId) {
54025
54446
  }
54026
54447
  return null;
54027
54448
  }
54028
- function resolveBellContext(options) {
54449
+ function resolvePaneContext(options) {
54029
54450
  const { deviceId, snapshot, rawData } = options;
54030
54451
  const raw = rawData ?? {};
54031
54452
  const bellWindowId = typeof raw.windowId === "string" && raw.windowId ? raw.windowId : undefined;
@@ -54100,6 +54521,34 @@ var defaultDeps = {
54100
54521
  }
54101
54522
  });
54102
54523
  },
54524
+ async notifyNotification(context) {
54525
+ const { device, settings, notification } = context;
54526
+ await eventNotifier.notify("terminal_notification", {
54527
+ site: {
54528
+ name: settings.siteName,
54529
+ url: settings.siteUrl
54530
+ },
54531
+ device: {
54532
+ id: device.id,
54533
+ name: device.name,
54534
+ type: device.type,
54535
+ host: device.host
54536
+ },
54537
+ tmux: {
54538
+ sessionName: device.session,
54539
+ windowId: notification.windowId,
54540
+ paneId: notification.paneId,
54541
+ windowIndex: notification.windowIndex,
54542
+ paneIndex: notification.paneIndex,
54543
+ paneUrl: notification.paneUrl
54544
+ },
54545
+ payload: {
54546
+ source: notification.source,
54547
+ title: notification.title,
54548
+ message: notification.body
54549
+ }
54550
+ });
54551
+ },
54103
54552
  fallbackReconnectDelayMs: 60000
54104
54553
  };
54105
54554
 
@@ -54294,25 +54743,44 @@ class PushSupervisor {
54294
54743
  if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
54295
54744
  return;
54296
54745
  }
54297
- if (event.type !== "bell") {
54298
- return;
54299
- }
54300
54746
  const device = this.deps.getDevice(deviceId);
54301
54747
  if (!device) {
54302
54748
  return;
54303
54749
  }
54304
54750
  const settings = this.deps.getSettings();
54305
- const bell = resolveBellContext({
54751
+ const paneContext = resolvePaneContext({
54306
54752
  deviceId,
54307
54753
  siteUrl: settings.siteUrl,
54308
54754
  snapshot: entry.lastSnapshot,
54309
54755
  rawData: event.data
54310
54756
  });
54311
- await this.deps.notifyBell({
54312
- device,
54313
- settings,
54314
- bell
54315
- });
54757
+ if (event.type === "bell") {
54758
+ await this.deps.notifyBell({
54759
+ device,
54760
+ settings,
54761
+ bell: paneContext
54762
+ });
54763
+ return;
54764
+ }
54765
+ if (event.type === "notification") {
54766
+ const raw = event.data ?? {};
54767
+ const title = typeof raw.title === "string" && raw.title ? raw.title : undefined;
54768
+ const body = typeof raw.body === "string" ? raw.body : "";
54769
+ if (!title && !body) {
54770
+ return;
54771
+ }
54772
+ const source = raw.source === "osc9" || raw.source === "osc777" || raw.source === "osc1337" ? raw.source : "osc9";
54773
+ await this.deps.notifyNotification({
54774
+ device,
54775
+ settings,
54776
+ notification: {
54777
+ ...paneContext,
54778
+ source,
54779
+ title,
54780
+ body
54781
+ }
54782
+ });
54783
+ }
54316
54784
  }
54317
54785
  }
54318
54786
  var pushSupervisor = new PushSupervisor;
@@ -54501,18 +54969,37 @@ function normalizeSiteSettingsInput(body) {
54501
54969
  }
54502
54970
  updates.bellThrottleSeconds = value;
54503
54971
  }
54972
+ if (body.notificationThrottleSeconds !== undefined) {
54973
+ const value = Math.floor(Number(body.notificationThrottleSeconds));
54974
+ if (Number.isNaN(value) || value < 0 || value > 300) {
54975
+ throw new Error(t2("apiError.bellThrottleInvalid"));
54976
+ }
54977
+ updates.notificationThrottleSeconds = value;
54978
+ }
54504
54979
  if (body.enableBrowserBellToast !== undefined) {
54505
54980
  if (typeof body.enableBrowserBellToast !== "boolean") {
54506
54981
  throw new Error(t2("apiError.invalidRequest"));
54507
54982
  }
54508
54983
  updates.enableBrowserBellToast = body.enableBrowserBellToast;
54509
54984
  }
54985
+ if (body.enableBrowserNotificationToast !== undefined) {
54986
+ if (typeof body.enableBrowserNotificationToast !== "boolean") {
54987
+ throw new Error(t2("apiError.invalidRequest"));
54988
+ }
54989
+ updates.enableBrowserNotificationToast = body.enableBrowserNotificationToast;
54990
+ }
54510
54991
  if (body.enableTelegramBellPush !== undefined) {
54511
54992
  if (typeof body.enableTelegramBellPush !== "boolean") {
54512
54993
  throw new Error(t2("apiError.invalidRequest"));
54513
54994
  }
54514
54995
  updates.enableTelegramBellPush = body.enableTelegramBellPush;
54515
54996
  }
54997
+ if (body.enableTelegramNotificationPush !== undefined) {
54998
+ if (typeof body.enableTelegramNotificationPush !== "boolean") {
54999
+ throw new Error(t2("apiError.invalidRequest"));
55000
+ }
55001
+ updates.enableTelegramNotificationPush = body.enableTelegramNotificationPush;
55002
+ }
54516
55003
  if (body.sshReconnectMaxRetries !== undefined) {
54517
55004
  const value = Math.floor(Number(body.sshReconnectMaxRetries));
54518
55005
  if (Number.isNaN(value) || value < 0 || value > 20) {
@@ -55018,7 +55505,8 @@ class SessionStateStore {
55018
55505
  deviceConnections: new Map,
55019
55506
  selectTransactions: new Map,
55020
55507
  outputGates: new Map,
55021
- bellThrottles: new Map
55508
+ bellThrottles: new Map,
55509
+ notificationThrottles: new Map
55022
55510
  };
55023
55511
  this.states.set(ws, state);
55024
55512
  return state;
@@ -55248,6 +55736,28 @@ class SessionStateStore {
55248
55736
  ctx.throttleSeconds = throttleSeconds;
55249
55737
  return true;
55250
55738
  }
55739
+ shouldAllowNotification(ws, deviceId, paneId, source, throttleSeconds) {
55740
+ const state = this.states.get(ws);
55741
+ if (!state)
55742
+ return false;
55743
+ const key = `${deviceId}:${paneId}:${source}`;
55744
+ const now = Date.now();
55745
+ let ctx = state.notificationThrottles.get(key);
55746
+ if (!ctx) {
55747
+ ctx = {
55748
+ lastBellAt: 0,
55749
+ throttleSeconds
55750
+ };
55751
+ state.notificationThrottles.set(key, ctx);
55752
+ }
55753
+ const throttleMs = throttleSeconds * 1000;
55754
+ if (now - ctx.lastBellAt < throttleMs) {
55755
+ return false;
55756
+ }
55757
+ ctx.lastBellAt = now;
55758
+ ctx.throttleSeconds = throttleSeconds;
55759
+ return true;
55760
+ }
55251
55761
  cleanupDevice(ws, deviceId) {
55252
55762
  const state = this.states.get(ws);
55253
55763
  if (!state)
@@ -55260,6 +55770,11 @@ class SessionStateStore {
55260
55770
  state.bellThrottles.delete(key);
55261
55771
  }
55262
55772
  }
55773
+ for (const key of state.notificationThrottles.keys()) {
55774
+ if (key.startsWith(`${deviceId}:`)) {
55775
+ state.notificationThrottles.delete(key);
55776
+ }
55777
+ }
55263
55778
  }
55264
55779
  cleanup(ws) {
55265
55780
  this.states.delete(ws);
@@ -56028,13 +56543,21 @@ class WebSocketServer {
56028
56543
  return;
56029
56544
  this.scheduleSnapshot(deviceId);
56030
56545
  const extendedEvent = await this.extendTmuxEvent(deviceId, event);
56546
+ const settings = getSiteSettings();
56547
+ if (extendedEvent.type === "notification") {
56548
+ const data = extendedEvent.data ?? {};
56549
+ const title = typeof data.title === "string" && data.title ? data.title : "";
56550
+ const body = typeof data.body === "string" ? data.body : "";
56551
+ if (!title && !body) {
56552
+ return;
56553
+ }
56554
+ }
56031
56555
  const payloadBytes = exports_ws_borsh.encodeTmuxEventPayload({
56032
56556
  deviceId,
56033
56557
  type: extendedEvent.type,
56034
56558
  data: extendedEvent.data
56035
56559
  });
56036
56560
  if (extendedEvent.type === "bell") {
56037
- const settings = getSiteSettings();
56038
56561
  const data = extendedEvent.data ?? {};
56039
56562
  const paneId = typeof data.paneId === "string" && data.paneId ? data.paneId : "-";
56040
56563
  for (const client of entry.clients) {
@@ -56045,25 +56568,52 @@ class WebSocketServer {
56045
56568
  }
56046
56569
  return;
56047
56570
  }
56571
+ if (extendedEvent.type === "notification") {
56572
+ const data = extendedEvent.data ?? {};
56573
+ const paneId = typeof data.paneId === "string" && data.paneId ? data.paneId : "-";
56574
+ const source = typeof data.source === "string" && data.source ? data.source : "osc9";
56575
+ for (const client of entry.clients) {
56576
+ if (!sessionStateStore.shouldAllowNotification(client, deviceId, paneId, source, settings.notificationThrottleSeconds)) {
56577
+ continue;
56578
+ }
56579
+ this.sendEnvelope(client, exports_ws_borsh.KIND_TMUX_EVENT, payloadBytes);
56580
+ }
56581
+ return;
56582
+ }
56048
56583
  for (const client of entry.clients) {
56049
56584
  this.sendEnvelope(client, exports_ws_borsh.KIND_TMUX_EVENT, payloadBytes);
56050
56585
  }
56051
56586
  }
56052
56587
  async extendTmuxEvent(deviceId, event) {
56053
- if (event.type !== "bell") {
56588
+ if (event.type !== "bell" && event.type !== "notification") {
56054
56589
  return event;
56055
56590
  }
56056
56591
  const settings = getSiteSettings();
56057
56592
  const snapshot = this.connections.get(deviceId)?.lastSnapshot ?? null;
56058
- const data = resolveBellContext({
56593
+ const paneContext = resolvePaneContext({
56059
56594
  deviceId,
56060
56595
  siteUrl: settings.siteUrl,
56061
56596
  snapshot,
56062
56597
  rawData: event.data
56063
56598
  });
56599
+ if (event.type === "bell") {
56600
+ return {
56601
+ type: "bell",
56602
+ data: paneContext
56603
+ };
56604
+ }
56605
+ const raw = event.data ?? {};
56606
+ const source = raw.source === "osc9" || raw.source === "osc777" || raw.source === "osc1337" ? raw.source : "osc9";
56607
+ const title = typeof raw.title === "string" && raw.title ? raw.title : undefined;
56608
+ const body = typeof raw.body === "string" ? raw.body : "";
56064
56609
  return {
56065
- type: "bell",
56066
- data
56610
+ type: "notification",
56611
+ data: {
56612
+ ...paneContext,
56613
+ source,
56614
+ title,
56615
+ body
56616
+ }
56067
56617
  };
56068
56618
  }
56069
56619
  broadcastStateSnapshot(deviceId, payload) {