@electerm/electerm-react 5.3.26 → 5.5.6

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 (134) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +20 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +14 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/common/item-filter.jsx +76 -0
  29. package/client/components/common/item-filter.styl +54 -0
  30. package/client/components/common/lazy-boundary.jsx +34 -0
  31. package/client/components/common/opacity.jsx +3 -1
  32. package/client/components/footer/footer-entry.jsx +35 -5
  33. package/client/components/footer/footer.styl +8 -1
  34. package/client/components/layout/layout.jsx +23 -31
  35. package/client/components/layout/pixed.js +3 -0
  36. package/client/components/main/main.jsx +55 -28
  37. package/client/components/main/term-fullscreen.styl +9 -3
  38. package/client/components/quick-commands/qm.styl +3 -2
  39. package/client/components/rdp/file-transfer.js +4 -4
  40. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  41. package/client/components/remote-monitor/monitor-details.styl +87 -0
  42. package/client/components/remote-monitor/monitor-model.js +764 -0
  43. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  44. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  45. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  46. package/client/components/remote-monitor/session-monitor.js +361 -0
  47. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  48. package/client/components/remote-monitor/visibility.js +18 -0
  49. package/client/components/session/session-control.jsx +29 -2
  50. package/client/components/session/session.jsx +40 -18
  51. package/client/components/session/sessions.jsx +5 -2
  52. package/client/components/setting-panel/setting-common.jsx +25 -56
  53. package/client/components/setting-panel/setting-modal.jsx +57 -42
  54. package/client/components/setting-panel/setting-terminal.jsx +7 -2
  55. package/client/components/setting-panel/setting.styl +1 -1
  56. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  57. package/client/components/sftp/file-compare-modal.jsx +6 -3
  58. package/client/components/sftp/file-item.jsx +5 -3
  59. package/client/components/sftp/owner-list.js +11 -2
  60. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  61. package/client/components/sys-menu/menu-btn.jsx +30 -1
  62. package/client/components/tabs/index.jsx +2 -2
  63. package/client/components/tabs/tab-title.jsx +3 -2
  64. package/client/components/tabs/tab.jsx +5 -1
  65. package/client/components/terminal/attach-addon-custom.js +295 -49
  66. package/client/components/terminal/automation/index.js +64 -0
  67. package/client/components/terminal/automation/keys.js +49 -0
  68. package/client/components/terminal/automation/stream-matcher.js +82 -0
  69. package/client/components/terminal/automation/strip-ansi.js +19 -0
  70. package/client/components/terminal/automation/trigger-engine.js +233 -0
  71. package/client/components/terminal/automation/trigger-presets.js +110 -0
  72. package/client/components/terminal/command-tracker-addon.js +31 -1
  73. package/client/components/terminal/mixins/index.js +27 -0
  74. package/client/components/terminal/mixins/term-attach.js +123 -0
  75. package/client/components/terminal/mixins/term-config.jsx +84 -0
  76. package/client/components/terminal/mixins/term-content.js +88 -0
  77. package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
  78. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  79. package/client/components/terminal/mixins/term-init.js +122 -0
  80. package/client/components/terminal/mixins/term-log.jsx +96 -0
  81. package/client/components/terminal/mixins/term-resize.js +87 -0
  82. package/client/components/terminal/mixins/term-socket.js +433 -0
  83. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  84. package/client/components/terminal/mixins/term-theme.js +190 -0
  85. package/client/components/terminal/mixins/term-touch.js +136 -0
  86. package/client/components/terminal/shell-detect.js +37 -0
  87. package/client/components/terminal/shell.js +0 -28
  88. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  89. package/client/components/terminal/shortcut-bar.jsx +4 -6
  90. package/client/components/terminal/shortcut-bar.styl +4 -0
  91. package/client/components/terminal/ssh-reload-state.js +56 -0
  92. package/client/components/terminal/startup-queue.js +379 -0
  93. package/client/components/terminal/terminal-interactive.jsx +32 -0
  94. package/client/components/terminal/terminal.jsx +102 -2007
  95. package/client/components/terminal/transfer-client-base.js +5 -29
  96. package/client/components/terminal/trzsz-client.js +1 -3
  97. package/client/components/terminal/xmodem-client.js +1 -3
  98. package/client/components/terminal/xterm-loader.js +7 -0
  99. package/client/components/terminal/zmodem-client.js +1 -3
  100. package/client/components/terminal-info/base.jsx +18 -70
  101. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  102. package/client/components/terminal-info/terminal-info.jsx +50 -59
  103. package/client/components/terminal-info/terminal-info.styl +6 -49
  104. package/client/components/text-editor/open-text-editor.js +30 -0
  105. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  106. package/client/components/text-editor/text-editor.jsx +14 -0
  107. package/client/components/triggers/trigger-editor.jsx +246 -0
  108. package/client/components/triggers/trigger-form.jsx +200 -0
  109. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  110. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  111. package/client/components/triggers/trigger-transport.jsx +60 -0
  112. package/client/components/triggers/triggers-field.jsx +19 -0
  113. package/client/components/widgets/widgets-list.jsx +11 -1
  114. package/client/css/mobile.styl +9 -1
  115. package/client/store/common.js +12 -1
  116. package/client/store/init-state.js +7 -0
  117. package/client/store/mcp-handler.js +158 -30
  118. package/client/store/setting.js +9 -0
  119. package/client/store/store.js +2 -0
  120. package/client/store/sync.js +2 -0
  121. package/client/store/tab.js +41 -3
  122. package/client/store/trigger.js +118 -0
  123. package/client/store/watch.js +13 -5
  124. package/client/views/index.pug +3 -0
  125. package/package.json +1 -1
  126. package/client/components/terminal-info/activity.jsx +0 -57
  127. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  128. package/client/components/terminal-info/disk.jsx +0 -45
  129. package/client/components/terminal-info/format-disks.js +0 -45
  130. package/client/components/terminal-info/network.jsx +0 -121
  131. package/client/components/terminal-info/resource.jsx +0 -99
  132. package/client/components/terminal-info/run-cmd.jsx +0 -263
  133. package/client/components/terminal-info/sys-info.jsx +0 -56
  134. package/client/components/terminal-info/up.jsx +0 -17
@@ -0,0 +1,56 @@
1
+ const maxCwdLength = 4096
2
+
3
+ export function isSshTab (tab = {}) {
4
+ return !!tab.host && (!tab.type || tab.type === 'ssh')
5
+ }
6
+
7
+ export function sanitizeSshCwd (cwd) {
8
+ if (typeof cwd !== 'string' || !cwd || cwd.length > maxCwdLength) {
9
+ return ''
10
+ }
11
+ if (/[\0\r\n]/.test(cwd)) {
12
+ return ''
13
+ }
14
+ return cwd
15
+ }
16
+
17
+ export function quotePosixShellArg (value) {
18
+ const quote = '\''
19
+ const escapedQuote = quote + '"' + quote + '"' + quote
20
+ return quote + value.replace(/'/g, escapedQuote) + quote
21
+ }
22
+
23
+ export function createRestoreCwdCommand (cwd) {
24
+ const safeCwd = sanitizeSshCwd(cwd)
25
+ return safeCwd ? `cd -- ${quotePosixShellArg(safeCwd)}` : ''
26
+ }
27
+
28
+ export function shouldCaptureSshReloadState (tab, config = {}) {
29
+ return !!config.restoreTerminalSessionOnReload && isSshTab(tab)
30
+ }
31
+
32
+ export function createSshReloadState ({ cwd, screen } = {}) {
33
+ const safeCwd = sanitizeSshCwd(cwd)
34
+ const safeScreen = typeof screen === 'string' ? screen : ''
35
+ if (!safeCwd && !safeScreen) {
36
+ return undefined
37
+ }
38
+ return {
39
+ cwd: safeCwd,
40
+ screen: safeScreen
41
+ }
42
+ }
43
+
44
+ export function getAlternateBufferSnapshot (buffer) {
45
+ if (!buffer || buffer.type !== 'alternate') {
46
+ return ''
47
+ }
48
+ const lines = []
49
+ for (let i = 0; i < buffer.length; i++) {
50
+ lines.push(buffer.getLine(i)?.translateToString(true) || '')
51
+ }
52
+ while (lines.length && !lines[lines.length - 1]) {
53
+ lines.pop()
54
+ }
55
+ return lines.join('\r\n')
56
+ }
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Startup queue for a terminal session.
3
+ *
4
+ * Everything that has to be typed into a freshly connected shell goes through
5
+ * here, strictly one item at a time:
6
+ *
7
+ * shell integration injection -> cd/startDirectory -> runScripts
8
+ *
9
+ * Two rules keep it reliable:
10
+ *
11
+ * 1. Only one drain loop owns the queue at a time, and every queue generation
12
+ * carries a token. A reconnect (or a config toggle that rebuilds the queue)
13
+ * bumps the token, so a loop still waiting on the *old* connection stops
14
+ * instead of stealing items from the new queue - that used to silently drop
15
+ * the remaining run scripts.
16
+ * 2. After each script we wait for the shell to settle (a fresh prompt, or a
17
+ * quiet period) before typing the next one, so nothing is ever typed into a
18
+ * shell that is still busy. Every wait is bounded, so a hung shell can only
19
+ * delay the queue, never stop it.
20
+ */
21
+ import {
22
+ getShellIntegrationCommand,
23
+ detectShellType
24
+ } from './shell.js'
25
+ import { createRestoreCwdCommand } from './ssh-reload-state.js'
26
+ import { isMac, isWin } from '../../common/platform.js'
27
+
28
+ // Pacing: `settleIdleMs` of silence counts as "the shell is done",
29
+ // `settleTimeoutMs` caps the wait so a script that never returns can not
30
+ // stall the queue.
31
+ const settleIdleMs = 400
32
+ const settleTimeoutMs = 3000
33
+ // Ceiling for the best-effort remote shell probe. detectRemoteShell opens an
34
+ // extra exec channel and runCmd has no timeout of its own, so a server that
35
+ // never answers would otherwise block the whole queue forever.
36
+ const remoteShellDetectTimeoutMs = 3000
37
+
38
+ export class StartupQueue {
39
+ /**
40
+ * @param {object} term - the terminal component (host)
41
+ * @param {object} [deps] - `detectRemoteShell` needs the network, so it is
42
+ * injected: it keeps this module free of the app runtime and makes the
43
+ * queue testable on its own.
44
+ */
45
+ constructor (term, deps = {}) {
46
+ this.term = term
47
+ this.detectRemoteShell = deps.detectRemoteShell || (() => Promise.resolve('sh'))
48
+ this.queue = []
49
+ this.token = 0
50
+ this.running = false
51
+ this.shellInjected = false
52
+ this.shellType = null
53
+ this.timers = new Set()
54
+ }
55
+
56
+ dispose = () => {
57
+ for (const t of this.timers) {
58
+ clearTimeout(t)
59
+ }
60
+ this.timers.clear()
61
+ this.queue = []
62
+ this.running = false
63
+ }
64
+
65
+ // ---- host accessors ----
66
+
67
+ get addon () {
68
+ return this.term.attachAddon
69
+ }
70
+
71
+ get cmdAddon () {
72
+ return this.term.cmdAddon
73
+ }
74
+
75
+ isClosed = () => {
76
+ return !!this.term.onClose
77
+ }
78
+
79
+ isSsh = () => {
80
+ return this.term.isSsh()
81
+ }
82
+
83
+ isLocal = () => {
84
+ return this.term.isLocal()
85
+ }
86
+
87
+ sleep = (ms) => {
88
+ if (ms <= 0) {
89
+ return Promise.resolve()
90
+ }
91
+ return new Promise(resolve => {
92
+ const timer = setTimeout(() => {
93
+ this.timers.delete(timer)
94
+ resolve()
95
+ }, ms)
96
+ this.timers.add(timer)
97
+ })
98
+ }
99
+
100
+ // ---- queue ----
101
+
102
+ runInitScript = () => {
103
+ const { term } = this
104
+ window.store.triggerResize()
105
+ const {
106
+ startDirectory,
107
+ runScripts
108
+ } = term.props.tab
109
+
110
+ const scripts = runScripts ? [...runScripts] : []
111
+ const reloadCwd = term.props.config.restoreTerminalSessionOnReload && this.isSsh()
112
+ ? term.props.tab._reloadState?.cwd
113
+ : ''
114
+ const startFolder = reloadCwd || startDirectory || window.initFolder
115
+ const cwdCommand = this.isSsh()
116
+ ? createRestoreCwdCommand(startFolder)
117
+ : startFolder ? `cd "${startFolder}"` : ''
118
+ if (cwdCommand) {
119
+ scripts.unshift({ script: cwdCommand, delay: 0 })
120
+ }
121
+
122
+ const token = ++this.token
123
+ this.queue = []
124
+
125
+ if (this.canInjectShellIntegration()) {
126
+ this.queue.push({
127
+ type: 'shell_integration',
128
+ execute: async () => {
129
+ await this.injectShellIntegration()
130
+ }
131
+ })
132
+ }
133
+
134
+ scripts.forEach((script, i) => {
135
+ this.queue.push({
136
+ type: 'delayed_script',
137
+ script: script.script,
138
+ delay: script.delay || 0,
139
+ // Nothing follows the last one, so waiting for the shell to settle
140
+ // would only add latency.
141
+ isLast: i === scripts.length - 1,
142
+ execute: () => {
143
+ if (script.script && this.addon) {
144
+ this.addon._sendData(script.script + '\r')
145
+ }
146
+ }
147
+ })
148
+ })
149
+
150
+ this.start(token)
151
+ }
152
+
153
+ /**
154
+ * Put a shell integration injection at the head of the queue.
155
+ * Inserting - instead of firing it off immediately - keeps it serialized
156
+ * against anything still queued and guarantees a single drain loop.
157
+ */
158
+ enqueueShellIntegration = (sendEnter) => {
159
+ this.queue.unshift({
160
+ type: 'shell_integration',
161
+ execute: async () => {
162
+ await this.injectShellIntegration()
163
+ // A fresh prompt makes the shell emit OSC 633 ;P Cwd= right away, so
164
+ // the file manager picks the current path up without waiting for the
165
+ // user's next command.
166
+ if (sendEnter && this.addon) {
167
+ this.addon._sendData('\r')
168
+ }
169
+ }
170
+ })
171
+ this.start(this.token)
172
+ }
173
+
174
+ /**
175
+ * Drain the queue one item at a time. Re-entrant calls only push items and
176
+ * return, so two loops can never interleave or race for the same array.
177
+ */
178
+ start = async (token) => {
179
+ if (this.running) {
180
+ return
181
+ }
182
+ this.running = true
183
+ try {
184
+ while (
185
+ !this.isClosed() &&
186
+ token === this.token &&
187
+ this.queue.length > 0
188
+ ) {
189
+ const item = this.queue.shift()
190
+ try {
191
+ await this.runItem(item)
192
+ } catch (error) {
193
+ console.error('[Shell Integration] Error processing queue item:', item.type, error)
194
+ }
195
+ }
196
+ } finally {
197
+ this.running = false
198
+ // A newer queue was built while this loop was still draining (reconnect,
199
+ // config toggle). Pick it up here, otherwise the fresh items would sit
200
+ // in the array with nobody left to run them.
201
+ if (
202
+ !this.isClosed() &&
203
+ token !== this.token &&
204
+ this.queue.length > 0
205
+ ) {
206
+ this.start(this.token)
207
+ }
208
+ }
209
+ }
210
+
211
+ runItem = async (item) => {
212
+ if (this.isClosed() || !this.addon) {
213
+ return
214
+ }
215
+ if (item.type === 'shell_integration') {
216
+ await item.execute()
217
+ // Give the freshly injected prompt a moment to finish drawing before
218
+ // anything is typed into it.
219
+ await this.waitForSettle({ usePrompt: false })
220
+ return
221
+ }
222
+ if (item.type !== 'delayed_script') {
223
+ return
224
+ }
225
+ item.execute()
226
+ if (item.isLast) {
227
+ return
228
+ }
229
+ await this.sleep(item.delay)
230
+ // Never type the next script into a shell that is still busy. Bounded, so
231
+ // a script that never returns can not block the rest of the queue.
232
+ await this.waitForSettle()
233
+ }
234
+
235
+ /**
236
+ * Wait until the terminal stops producing output before typing the next
237
+ * queued item. Resolves on whichever comes first:
238
+ * - a fresh prompt (OSC 633 ;A), when shell integration is active - the
239
+ * exact "previous command finished" signal
240
+ * - no output for `idleMs`
241
+ * Always resolves; `timeoutMs` caps it.
242
+ */
243
+ waitForSettle = ({ timeoutMs = settleTimeoutMs, idleMs = settleIdleMs, usePrompt = true } = {}) => {
244
+ const addon = this.addon
245
+ if (this.isClosed() || !addon || !addon.waitForOutputIdle || addon.disposed) {
246
+ return Promise.resolve(false)
247
+ }
248
+ const idle = addon.waitForOutputIdle({ idleMs, timeoutMs })
249
+ if (!usePrompt || !this.cmdAddon || !this.cmdAddon.onPrompt) {
250
+ return idle
251
+ }
252
+ return new Promise((resolve) => {
253
+ let done = false
254
+ let unsubscribe = null
255
+ const finish = (result) => {
256
+ if (done) {
257
+ return
258
+ }
259
+ done = true
260
+ clearTimeout(capTimer)
261
+ if (unsubscribe) {
262
+ unsubscribe()
263
+ }
264
+ resolve(result)
265
+ }
266
+ unsubscribe = this.cmdAddon.onPrompt(() => finish('prompt'))
267
+ const capTimer = setTimeout(() => finish(false), timeoutMs + 200)
268
+ this.timers.add(capTimer)
269
+ idle.then(quiet => finish(quiet ? 'idle' : false))
270
+ })
271
+ }
272
+
273
+ // ---- shell integration ----
274
+
275
+ canInjectShellIntegration = () => {
276
+ const { config, sftpPathFollowSsh } = this.term.props
277
+ return (
278
+ config.showCmdSuggestions ||
279
+ sftpPathFollowSsh ||
280
+ (config.restoreTerminalSessionOnReload && this.isSsh())
281
+ ) &&
282
+ (
283
+ this.isSsh() ||
284
+ (this.isLocal() && !isWin)
285
+ )
286
+ }
287
+
288
+ /**
289
+ * Inject shell integration commands from client-side.
290
+ * Replaces the server-side `source xxx.xxx` approach; output suppression
291
+ * hides the injection command. Resolves when injection is complete - or
292
+ * when it is given up on, never hangs.
293
+ */
294
+ injectShellIntegration = async () => {
295
+ if (this.shellInjected) {
296
+ return
297
+ }
298
+
299
+ const addon = this.addon
300
+ if (!addon) {
301
+ return
302
+ }
303
+
304
+ let shellType
305
+ if (this.isLocal()) {
306
+ const { config } = this.term.props
307
+ const localShell = isMac ? config.execMac : config.execLinux
308
+ shellType = detectShellType(localShell)
309
+ } else if (this.isSsh()) {
310
+ shellType = await Promise.race([
311
+ this.detectRemoteShell(this.term.pid),
312
+ new Promise(resolve => {
313
+ setTimeout(() => resolve('sh'), remoteShellDetectTimeoutMs)
314
+ })
315
+ ])
316
+ }
317
+
318
+ this.shellType = shellType
319
+
320
+ // Don't inject for sh type shells unless sftpPathFollowSsh is true
321
+ if (shellType === 'sh' && !this.term.props.sftpPathFollowSsh) {
322
+ return
323
+ }
324
+
325
+ // The connection (or the whole terminal) can go away while
326
+ // detectRemoteShell is in flight. Bail instead of registering callbacks
327
+ // on a dead socket - that is what used to leave the queue, and every run
328
+ // script behind it, waiting forever.
329
+ if (this.isClosed() || this.addon !== addon || addon.disposed) {
330
+ return
331
+ }
332
+
333
+ const integrationCmd = getShellIntegrationCommand(shellType)
334
+ const suppressionTimeout = this.isSsh() ? 5000 : 3000
335
+
336
+ return new Promise((resolve) => {
337
+ let done = false
338
+ const finish = () => {
339
+ if (done) {
340
+ return
341
+ }
342
+ done = true
343
+ this.timers.delete(safetyTimer)
344
+ clearTimeout(safetyTimer)
345
+ resolve()
346
+ }
347
+ // Hard ceiling for the whole injection. onInitialData only fires once
348
+ // the shell produces output; when it never does (dead pty, restricted
349
+ // shell, zmodem in progress) the promise - and the run scripts queued
350
+ // behind it - would otherwise never be released.
351
+ const safetyTimer = setTimeout(() => {
352
+ if (addon.outputSuppressed) {
353
+ // Unfreeze the screen rather than keep hiding output.
354
+ addon.stopOutputSuppression(false)
355
+ }
356
+ finish()
357
+ }, suppressionTimeout + 2000)
358
+ this.timers.add(safetyTimer)
359
+
360
+ // Wait for initial data (prompt/banner) to arrive before injecting
361
+ addon.onInitialData(() => {
362
+ if (done || this.addon !== addon || addon.disposed) {
363
+ return finish()
364
+ }
365
+ // Start suppressing output before sending the integration command.
366
+ // This hides the command and its output until OSC 633 is detected.
367
+ addon.startOutputSuppression(suppressionTimeout, () => {
368
+ if (!addon.disposed) {
369
+ this.shellInjected = true
370
+ }
371
+ finish()
372
+ })
373
+ addon._sendData(integrationCmd)
374
+ })
375
+ })
376
+ }
377
+ }
378
+
379
+ export default StartupQueue
@@ -8,6 +8,9 @@ import TermInteractiveUI from './terminal-interactive-ui'
8
8
  import ExternalLink from '../common/external-link.jsx'
9
9
  import { notification } from '../common/notification'
10
10
 
11
+ // x11 warnings already shown, keyed by message text
12
+ const shownX11Warnings = new Set()
13
+
11
14
  export default function TermInteractive () {
12
15
  const [current, setCurrent] = useState(null)
13
16
  const queueRef = useRef([])
@@ -43,6 +46,28 @@ export default function TermInteractive () {
43
46
  })
44
47
  }
45
48
 
49
+ function notifyX11Warning (data) {
50
+ // one notification per distinct message, several tabs may report it
51
+ if (shownX11Warnings.has(data.message)) {
52
+ return
53
+ }
54
+ shownX11Warnings.add(data.message)
55
+ notification.warning({
56
+ message: 'X11 forwarding',
57
+ description: (
58
+ <div className='pd0 mg0'>
59
+ <div>{data.message}</div>
60
+ {
61
+ data.url
62
+ ? <ExternalLink to={data.url}>{data.url}</ExternalLink>
63
+ : null
64
+ }
65
+ </div>
66
+ ),
67
+ duration: 30
68
+ })
69
+ }
70
+
46
71
  function processNext () {
47
72
  const next = queueRef.current.shift()
48
73
  if (next) {
@@ -82,6 +107,13 @@ export default function TermInteractive () {
82
107
  e.data.includes('ssh-proxy-command-message')
83
108
  ) {
84
109
  notifyProxyCommandMessage(JSON.parse(e.data))
110
+ } else if (
111
+ e &&
112
+ e.data &&
113
+ typeof e.data === 'string' &&
114
+ e.data.includes('x11-warning')
115
+ ) {
116
+ notifyX11Warning(JSON.parse(e.data))
85
117
  }
86
118
  }
87
119