tmex-cli 0.4.0 → 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 (33) hide show
  1. package/dist/runtime/server.js +718 -186
  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-BwLKaiUR.js +17 -0
  6. package/resources/fe-dist/assets/{DevicesPage-CtNzaW_c.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 -4527
  10. package/resources/fe-dist/assets/index-CJyFlAt8.js +449 -0
  11. package/resources/fe-dist/assets/index-CJyFlAt8.js.map +1 -0
  12. package/resources/fe-dist/assets/select-DGBwxGiK.js +17 -0
  13. package/resources/fe-dist/assets/{select-BNsiC9zT.js.map → select-DGBwxGiK.js.map} +1 -1
  14. package/resources/fe-dist/assets/switch-CWUBjs7N.js +12 -0
  15. package/resources/fe-dist/assets/{switch-CIU4AisU.js.map → switch-CWUBjs7N.js.map} +1 -1
  16. package/resources/fe-dist/assets/useValueChanged-DwJ_SDCu.js +7 -0
  17. package/resources/fe-dist/assets/{useValueChanged-V23H0VpC.js.map → useValueChanged-DwJ_SDCu.js.map} +1 -1
  18. package/resources/fe-dist/index.html +1 -1
  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-dsVN7rgz.js +0 -200
  30. package/resources/fe-dist/assets/index-dsVN7rgz.js.map +0 -1
  31. package/resources/fe-dist/assets/select-BNsiC9zT.js +0 -2805
  32. package/resources/fe-dist/assets/switch-CIU4AisU.js +0 -234
  33. package/resources/fe-dist/assets/useValueChanged-V23H0VpC.js +0 -351
@@ -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
  }
@@ -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
  }
@@ -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(() => {
@@ -53068,8 +53404,7 @@ class SshExternalTmuxConnection {
53068
53404
  pendingPaneTitles = new Map;
53069
53405
  snapshotSession = null;
53070
53406
  snapshotWindows = new Map;
53071
- currentPipePaneId = null;
53072
- pipeReadAbort = null;
53407
+ paneReaders = new Map;
53073
53408
  pipeTransition = Promise.resolve();
53074
53409
  hookReadAbort = null;
53075
53410
  hookBuffer = "";
@@ -53115,6 +53450,7 @@ class SshExternalTmuxConnection {
53115
53450
  await this.openCommandChannel();
53116
53451
  await this.ensureRemoteRuntimeDirs();
53117
53452
  await this.ensureSession();
53453
+ await this.configureSessionOptions();
53118
53454
  await this.startHooks();
53119
53455
  this.connected = true;
53120
53456
  updateDeviceRuntimeStatus(this.deviceId, {
@@ -53323,6 +53659,26 @@ class SshExternalTmuxConnection {
53323
53659
  }
53324
53660
  await this.runTmux(["new-session", "-d", "-c", this.remoteHomeDir, "-s", this.sessionName]);
53325
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
+ }
53326
53682
  async startHooks() {
53327
53683
  await this.ensureRemoteRuntimeDirs();
53328
53684
  const fifoPath = this.fsPaths.hookFifoPath;
@@ -53341,14 +53697,16 @@ class SshExternalTmuxConnection {
53341
53697
  stopReader();
53342
53698
  this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53343
53699
  };
53344
- await this.installHook("alert-bell", ["bell", "#{window_id}", "#{pane_id}"]);
53345
53700
  await this.installHook("pane-exited", ["pane-exited", "#{window_id}", "#{pane_id}"]);
53346
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"]);
53347
53704
  }
53348
53705
  async stopHooks() {
53349
- await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "alert-bell"]);
53350
53706
  await this.runTmuxAllowFailure(["set-hook", "-u", "-t", this.sessionName, "pane-exited"]);
53351
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"]);
53352
53710
  this.hookReadAbort?.();
53353
53711
  this.hookReadAbort = null;
53354
53712
  this.hookBuffer = "";
@@ -53379,22 +53737,9 @@ class SshExternalTmuxConnection {
53379
53737
  }
53380
53738
  const [type, windowId, paneId] = line.split("\t");
53381
53739
  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
53740
  continue;
53396
53741
  }
53397
- if (type === "pane-exited" || type === "pane-died") {
53742
+ if (type === "pane-exited" || type === "pane-died" || type === "refresh") {
53398
53743
  this.requestSnapshot();
53399
53744
  }
53400
53745
  }
@@ -53425,7 +53770,6 @@ class SshExternalTmuxConnection {
53425
53770
  this.activePaneId = paneId;
53426
53771
  await this.runTmux(["select-window", "-t", windowId], true);
53427
53772
  await this.runTmux(["select-pane", "-t", paneId], true);
53428
- await this.startPipeForPane(paneId);
53429
53773
  if (size) {
53430
53774
  await this.resizePaneInternal(paneId, size.cols, size.rows);
53431
53775
  }
@@ -53480,6 +53824,7 @@ class SshExternalTmuxConnection {
53480
53824
  this.parseSnapshotSession(sessionRes.stdout.split(/\r?\n/));
53481
53825
  this.parseSnapshotWindows(windowsRes.stdout.split(/\r?\n/));
53482
53826
  this.parseSnapshotPanes(panesRes.stdout.split(/\r?\n/));
53827
+ await this.syncPipeReaders();
53483
53828
  this.emitSnapshot();
53484
53829
  }
53485
53830
  parseSnapshotSession(lines) {
@@ -53577,56 +53922,114 @@ class SshExternalTmuxConnection {
53577
53922
  }
53578
53923
  return null;
53579
53924
  }
53580
- async startPipeForPane(paneId) {
53581
- await this.queuePipeTransition(async () => {
53582
- if (this.currentPipePaneId === paneId) {
53583
- 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
53584
53947
  }
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();
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);
53970
+ }
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);
53593
53977
  }
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
- }
53978
+ },
53979
+ onClose: () => {
53980
+ if (!this.manualDisconnect && this.paneReaders.has(paneId)) {
53981
+ this.callbacks.onError(new Error(`SSH pane reader closed unexpectedly: ${paneId}`));
53609
53982
  }
53610
- });
53611
- this.pipeReadAbort = () => {
53983
+ }
53984
+ });
53985
+ const handle = {
53986
+ paneId,
53987
+ fifoPath,
53988
+ stopReader: () => {
53612
53989
  stopReader();
53613
53990
  this.runShellAllowFailure(`rm -f ${quoteShellArg(fifoPath)}`);
53614
- };
53991
+ }
53992
+ };
53993
+ this.paneReaders.set(paneId, handle);
53994
+ try {
53615
53995
  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());
53996
+ } catch (error) {
53997
+ this.paneReaders.delete(paneId);
53998
+ handle.stopReader();
53999
+ throw error;
54000
+ }
53621
54001
  }
53622
- async stopPipeNow() {
53623
- const paneId = this.currentPipePaneId;
53624
- this.currentPipePaneId = null;
53625
- if (paneId) {
53626
- await this.runTmuxAllowFailure(["pipe-pane", "-t", paneId]);
54002
+ async stopPipeForPaneNow(paneId) {
54003
+ const handle = this.paneReaders.get(paneId);
54004
+ if (!handle) {
54005
+ return;
53627
54006
  }
53628
- this.pipeReadAbort?.();
53629
- 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
+ });
53630
54033
  }
53631
54034
  queuePipeTransition(task) {
53632
54035
  const next = this.pipeTransition.catch(() => {
@@ -53796,7 +54199,7 @@ printf '\\036TMEX_END %s %d\\036\\n' ${quoteShellArg(commandId)} $?
53796
54199
  }
53797
54200
  this.connected = false;
53798
54201
  this.cleanupPromise = (async () => {
53799
- await this.stopPipe().catch(() => {
54202
+ await this.stopAllPipeReaders().catch(() => {
53800
54203
  return;
53801
54204
  });
53802
54205
  await this.stopHooks().catch(() => {
@@ -54025,7 +54428,7 @@ function pickPaneById(windows, paneId) {
54025
54428
  }
54026
54429
  return null;
54027
54430
  }
54028
- function resolveBellContext(options) {
54431
+ function resolvePaneContext(options) {
54029
54432
  const { deviceId, snapshot, rawData } = options;
54030
54433
  const raw = rawData ?? {};
54031
54434
  const bellWindowId = typeof raw.windowId === "string" && raw.windowId ? raw.windowId : undefined;
@@ -54100,6 +54503,34 @@ var defaultDeps = {
54100
54503
  }
54101
54504
  });
54102
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
+ },
54103
54534
  fallbackReconnectDelayMs: 60000
54104
54535
  };
54105
54536
 
@@ -54294,25 +54725,44 @@ class PushSupervisor {
54294
54725
  if (!entry || entry.generation !== generation || entry.runtime !== runtime) {
54295
54726
  return;
54296
54727
  }
54297
- if (event.type !== "bell") {
54298
- return;
54299
- }
54300
54728
  const device = this.deps.getDevice(deviceId);
54301
54729
  if (!device) {
54302
54730
  return;
54303
54731
  }
54304
54732
  const settings = this.deps.getSettings();
54305
- const bell = resolveBellContext({
54733
+ const paneContext = resolvePaneContext({
54306
54734
  deviceId,
54307
54735
  siteUrl: settings.siteUrl,
54308
54736
  snapshot: entry.lastSnapshot,
54309
54737
  rawData: event.data
54310
54738
  });
54311
- await this.deps.notifyBell({
54312
- device,
54313
- settings,
54314
- bell
54315
- });
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
+ }
54316
54766
  }
54317
54767
  }
54318
54768
  var pushSupervisor = new PushSupervisor;
@@ -54501,18 +54951,37 @@ function normalizeSiteSettingsInput(body) {
54501
54951
  }
54502
54952
  updates.bellThrottleSeconds = value;
54503
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
+ }
54504
54961
  if (body.enableBrowserBellToast !== undefined) {
54505
54962
  if (typeof body.enableBrowserBellToast !== "boolean") {
54506
54963
  throw new Error(t2("apiError.invalidRequest"));
54507
54964
  }
54508
54965
  updates.enableBrowserBellToast = body.enableBrowserBellToast;
54509
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
+ }
54510
54973
  if (body.enableTelegramBellPush !== undefined) {
54511
54974
  if (typeof body.enableTelegramBellPush !== "boolean") {
54512
54975
  throw new Error(t2("apiError.invalidRequest"));
54513
54976
  }
54514
54977
  updates.enableTelegramBellPush = body.enableTelegramBellPush;
54515
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
+ }
54516
54985
  if (body.sshReconnectMaxRetries !== undefined) {
54517
54986
  const value = Math.floor(Number(body.sshReconnectMaxRetries));
54518
54987
  if (Number.isNaN(value) || value < 0 || value > 20) {
@@ -55018,7 +55487,8 @@ class SessionStateStore {
55018
55487
  deviceConnections: new Map,
55019
55488
  selectTransactions: new Map,
55020
55489
  outputGates: new Map,
55021
- bellThrottles: new Map
55490
+ bellThrottles: new Map,
55491
+ notificationThrottles: new Map
55022
55492
  };
55023
55493
  this.states.set(ws, state);
55024
55494
  return state;
@@ -55248,6 +55718,28 @@ class SessionStateStore {
55248
55718
  ctx.throttleSeconds = throttleSeconds;
55249
55719
  return true;
55250
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
+ }
55251
55743
  cleanupDevice(ws, deviceId) {
55252
55744
  const state = this.states.get(ws);
55253
55745
  if (!state)
@@ -55260,6 +55752,11 @@ class SessionStateStore {
55260
55752
  state.bellThrottles.delete(key);
55261
55753
  }
55262
55754
  }
55755
+ for (const key of state.notificationThrottles.keys()) {
55756
+ if (key.startsWith(`${deviceId}:`)) {
55757
+ state.notificationThrottles.delete(key);
55758
+ }
55759
+ }
55263
55760
  }
55264
55761
  cleanup(ws) {
55265
55762
  this.states.delete(ws);
@@ -56028,13 +56525,21 @@ class WebSocketServer {
56028
56525
  return;
56029
56526
  this.scheduleSnapshot(deviceId);
56030
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
+ }
56031
56537
  const payloadBytes = exports_ws_borsh.encodeTmuxEventPayload({
56032
56538
  deviceId,
56033
56539
  type: extendedEvent.type,
56034
56540
  data: extendedEvent.data
56035
56541
  });
56036
56542
  if (extendedEvent.type === "bell") {
56037
- const settings = getSiteSettings();
56038
56543
  const data = extendedEvent.data ?? {};
56039
56544
  const paneId = typeof data.paneId === "string" && data.paneId ? data.paneId : "-";
56040
56545
  for (const client of entry.clients) {
@@ -56045,25 +56550,52 @@ class WebSocketServer {
56045
56550
  }
56046
56551
  return;
56047
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
+ }
56048
56565
  for (const client of entry.clients) {
56049
56566
  this.sendEnvelope(client, exports_ws_borsh.KIND_TMUX_EVENT, payloadBytes);
56050
56567
  }
56051
56568
  }
56052
56569
  async extendTmuxEvent(deviceId, event) {
56053
- if (event.type !== "bell") {
56570
+ if (event.type !== "bell" && event.type !== "notification") {
56054
56571
  return event;
56055
56572
  }
56056
56573
  const settings = getSiteSettings();
56057
56574
  const snapshot = this.connections.get(deviceId)?.lastSnapshot ?? null;
56058
- const data = resolveBellContext({
56575
+ const paneContext = resolvePaneContext({
56059
56576
  deviceId,
56060
56577
  siteUrl: settings.siteUrl,
56061
56578
  snapshot,
56062
56579
  rawData: event.data
56063
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 : "";
56064
56591
  return {
56065
- type: "bell",
56066
- data
56592
+ type: "notification",
56593
+ data: {
56594
+ ...paneContext,
56595
+ source,
56596
+ title,
56597
+ body
56598
+ }
56067
56599
  };
56068
56600
  }
56069
56601
  broadcastStateSnapshot(deviceId, payload) {