@electerm/electerm-react 5.3.26 → 5.5.9

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 (146) 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 +15 -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/cmd-history.jsx +132 -45
  33. package/client/components/footer/cmd-history.styl +42 -4
  34. package/client/components/footer/footer-entry.jsx +35 -5
  35. package/client/components/footer/footer.styl +8 -1
  36. package/client/components/footer/multi-tab-run-modal.jsx +83 -0
  37. package/client/components/footer/tab-select.jsx +34 -17
  38. package/client/components/layout/layout.jsx +23 -31
  39. package/client/components/layout/pixed.js +3 -0
  40. package/client/components/main/main.jsx +55 -28
  41. package/client/components/main/term-fullscreen.styl +9 -3
  42. package/client/components/quick-commands/qm.styl +3 -2
  43. package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
  44. package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
  45. package/client/components/rdp/file-transfer.js +4 -4
  46. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  47. package/client/components/remote-monitor/monitor-details.styl +87 -0
  48. package/client/components/remote-monitor/monitor-model.js +764 -0
  49. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  50. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  51. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  52. package/client/components/remote-monitor/session-monitor.js +361 -0
  53. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  54. package/client/components/remote-monitor/visibility.js +18 -0
  55. package/client/components/session/session-control.jsx +29 -2
  56. package/client/components/session/session.jsx +40 -18
  57. package/client/components/session/sessions.jsx +5 -2
  58. package/client/components/setting-panel/setting-common.jsx +25 -56
  59. package/client/components/setting-panel/setting-modal.jsx +57 -42
  60. package/client/components/setting-panel/setting-terminal.jsx +8 -2
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/sftp/sftp-entry.jsx +4 -0
  67. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  68. package/client/components/sidebar/side-panel.jsx +1 -1
  69. package/client/components/sys-menu/menu-btn.jsx +30 -1
  70. package/client/components/tabs/index.jsx +2 -2
  71. package/client/components/tabs/tab-title.jsx +3 -2
  72. package/client/components/tabs/tab.jsx +5 -1
  73. package/client/components/terminal/attach-addon-custom.js +295 -49
  74. package/client/components/terminal/automation/index.js +64 -0
  75. package/client/components/terminal/automation/keys.js +49 -0
  76. package/client/components/terminal/automation/stream-matcher.js +82 -0
  77. package/client/components/terminal/automation/strip-ansi.js +19 -0
  78. package/client/components/terminal/automation/trigger-engine.js +233 -0
  79. package/client/components/terminal/automation/trigger-presets.js +110 -0
  80. package/client/components/terminal/command-tracker-addon.js +31 -1
  81. package/client/components/terminal/mixins/index.js +27 -0
  82. package/client/components/terminal/mixins/term-attach.js +123 -0
  83. package/client/components/terminal/mixins/term-config.jsx +84 -0
  84. package/client/components/terminal/mixins/term-content.js +126 -0
  85. package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
  86. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  87. package/client/components/terminal/mixins/term-init.js +122 -0
  88. package/client/components/terminal/mixins/term-log.jsx +96 -0
  89. package/client/components/terminal/mixins/term-resize.js +87 -0
  90. package/client/components/terminal/mixins/term-socket.js +433 -0
  91. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  92. package/client/components/terminal/mixins/term-theme.js +190 -0
  93. package/client/components/terminal/mixins/term-touch.js +523 -0
  94. package/client/components/terminal/shell-detect.js +37 -0
  95. package/client/components/terminal/shell.js +0 -28
  96. package/client/components/terminal/shortcut-bar-defs.js +30 -4
  97. package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
  98. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  99. package/client/components/terminal/shortcut-bar.jsx +17 -10
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +96 -0
  102. package/client/components/terminal/startup-queue.js +433 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal-select-text.jsx +69 -0
  105. package/client/components/terminal/terminal.jsx +227 -2064
  106. package/client/components/terminal/terminal.styl +61 -0
  107. package/client/components/terminal/transfer-client-base.js +5 -29
  108. package/client/components/terminal/trzsz-client.js +1 -3
  109. package/client/components/terminal/xmodem-client.js +1 -3
  110. package/client/components/terminal/xterm-loader.js +7 -0
  111. package/client/components/terminal/zmodem-client.js +1 -3
  112. package/client/components/terminal-info/base.jsx +18 -70
  113. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  114. package/client/components/terminal-info/terminal-info.jsx +50 -59
  115. package/client/components/terminal-info/terminal-info.styl +6 -49
  116. package/client/components/text-editor/open-text-editor.js +30 -0
  117. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  118. package/client/components/text-editor/text-editor.jsx +14 -0
  119. package/client/components/triggers/trigger-editor.jsx +246 -0
  120. package/client/components/triggers/trigger-form.jsx +200 -0
  121. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  122. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  123. package/client/components/triggers/trigger-transport.jsx +60 -0
  124. package/client/components/triggers/triggers-field.jsx +19 -0
  125. package/client/components/widgets/widgets-list.jsx +11 -1
  126. package/client/css/mobile.styl +29 -8
  127. package/client/store/common.js +12 -1
  128. package/client/store/init-state.js +7 -0
  129. package/client/store/mcp-handler.js +158 -30
  130. package/client/store/setting.js +9 -0
  131. package/client/store/store.js +2 -0
  132. package/client/store/sync.js +2 -0
  133. package/client/store/tab.js +42 -3
  134. package/client/store/trigger.js +118 -0
  135. package/client/store/watch.js +13 -5
  136. package/client/views/index.pug +3 -0
  137. package/package.json +1 -1
  138. package/client/components/terminal-info/activity.jsx +0 -57
  139. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  140. package/client/components/terminal-info/disk.jsx +0 -45
  141. package/client/components/terminal-info/format-disks.js +0 -45
  142. package/client/components/terminal-info/network.jsx +0 -121
  143. package/client/components/terminal-info/resource.jsx +0 -99
  144. package/client/components/terminal-info/run-cmd.jsx +0 -263
  145. package/client/components/terminal-info/sys-info.jsx +0 -56
  146. package/client/components/terminal-info/up.jsx +0 -17
@@ -0,0 +1,433 @@
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 {
26
+ createRestoreCwdCommand,
27
+ createWindowsRestoreCwdCommand,
28
+ isRestorableTerminalTab
29
+ } from './ssh-reload-state.js'
30
+ import { isMac, isWin } from '../../common/platform.js'
31
+
32
+ // Pacing: `settleIdleMs` of silence counts as "the shell is done",
33
+ // `settleTimeoutMs` caps the wait so a script that never returns can not
34
+ // stall the queue.
35
+ const settleIdleMs = 400
36
+ const settleTimeoutMs = 3000
37
+ // Ceiling for the best-effort remote shell probe. detectRemoteShell opens an
38
+ // extra exec channel and runCmd has no timeout of its own, so a server that
39
+ // never answers would otherwise block the whole queue forever.
40
+ const remoteShellDetectTimeoutMs = 3000
41
+
42
+ export class StartupQueue {
43
+ /**
44
+ * @param {object} term - the terminal component (host)
45
+ * @param {object} [deps] - `detectRemoteShell` needs the network, so it is
46
+ * injected: it keeps this module free of the app runtime and makes the
47
+ * queue testable on its own.
48
+ */
49
+ constructor (term, deps = {}) {
50
+ this.term = term
51
+ this.detectRemoteShell = deps.detectRemoteShell || (() => Promise.resolve('sh'))
52
+ this.queue = []
53
+ this.token = 0
54
+ this.running = false
55
+ this.shellInjected = false
56
+ this.shellType = null
57
+ this.timers = new Set()
58
+ }
59
+
60
+ dispose = () => {
61
+ for (const t of this.timers) {
62
+ clearTimeout(t)
63
+ }
64
+ this.timers.clear()
65
+ this.queue = []
66
+ this.running = false
67
+ }
68
+
69
+ // ---- host accessors ----
70
+
71
+ get addon () {
72
+ return this.term.attachAddon
73
+ }
74
+
75
+ get cmdAddon () {
76
+ return this.term.cmdAddon
77
+ }
78
+
79
+ isClosed = () => {
80
+ return !!this.term.onClose
81
+ }
82
+
83
+ isSsh = () => {
84
+ return this.term.isSsh ? this.term.isSsh() : this.getTabType() === 'ssh'
85
+ }
86
+
87
+ isLocal = () => {
88
+ return this.term.isLocal ? this.term.isLocal() : this.getTabType() === 'local'
89
+ }
90
+
91
+ getTabType = () => {
92
+ return this.term.props?.tab?.type
93
+ }
94
+
95
+ isTelnet = () => {
96
+ if (this.term.isTelnet) {
97
+ return this.term.isTelnet()
98
+ }
99
+ return this.getTabType() === 'telnet'
100
+ }
101
+
102
+ isSerial = () => {
103
+ if (this.term.isSerial) {
104
+ return this.term.isSerial()
105
+ }
106
+ return this.getTabType() === 'serial'
107
+ }
108
+
109
+ isRemoteShell = () => {
110
+ return this.isSsh() || this.isTelnet() || this.isSerial()
111
+ }
112
+
113
+ isRestorable = () => {
114
+ const tab = this.term.props?.tab || {}
115
+ if (typeof this.term.isShellTerminal === 'function') {
116
+ return this.term.isShellTerminal()
117
+ }
118
+ return isRestorableTerminalTab(tab)
119
+ }
120
+
121
+ isWindowsLocal = () => {
122
+ return this.isLocal() && isWin
123
+ }
124
+
125
+ isPosixShell = () => {
126
+ return this.isRemoteShell() || (this.isLocal() && !isWin)
127
+ }
128
+
129
+ sleep = (ms) => {
130
+ if (ms <= 0) {
131
+ return Promise.resolve()
132
+ }
133
+ return new Promise(resolve => {
134
+ const timer = setTimeout(() => {
135
+ this.timers.delete(timer)
136
+ resolve()
137
+ }, ms)
138
+ this.timers.add(timer)
139
+ })
140
+ }
141
+
142
+ // ---- queue ----
143
+
144
+ runInitScript = () => {
145
+ const { term } = this
146
+ window.store.triggerResize()
147
+ const {
148
+ startDirectory,
149
+ runScripts
150
+ } = term.props.tab
151
+
152
+ const scripts = runScripts ? [...runScripts] : []
153
+ const reloadCwd = term.props.config.restoreTerminalSessionOnReload && this.isRestorable()
154
+ ? term.props.tab._reloadState?.cwd
155
+ : ''
156
+ const startFolder = reloadCwd || startDirectory || window.initFolder
157
+ let cwdCommand = ''
158
+ if (startFolder) {
159
+ cwdCommand = this.isPosixShell()
160
+ ? createRestoreCwdCommand(startFolder)
161
+ : createWindowsRestoreCwdCommand(startFolder) || `cd "${startFolder}"`
162
+ }
163
+ if (cwdCommand) {
164
+ scripts.unshift({ script: cwdCommand, delay: 0 })
165
+ }
166
+
167
+ const token = ++this.token
168
+ this.queue = []
169
+
170
+ if (this.canInjectShellIntegration()) {
171
+ this.queue.push({
172
+ type: 'shell_integration',
173
+ execute: async () => {
174
+ await this.injectShellIntegration()
175
+ }
176
+ })
177
+ }
178
+
179
+ scripts.forEach((script, i) => {
180
+ this.queue.push({
181
+ type: 'delayed_script',
182
+ script: script.script,
183
+ delay: script.delay || 0,
184
+ // Nothing follows the last one, so waiting for the shell to settle
185
+ // would only add latency.
186
+ isLast: i === scripts.length - 1,
187
+ execute: () => {
188
+ if (script.script && this.addon) {
189
+ this.addon._sendData(script.script + '\r')
190
+ }
191
+ }
192
+ })
193
+ })
194
+
195
+ this.start(token)
196
+ }
197
+
198
+ /**
199
+ * Put a shell integration injection at the head of the queue.
200
+ * Inserting - instead of firing it off immediately - keeps it serialized
201
+ * against anything still queued and guarantees a single drain loop.
202
+ */
203
+ enqueueShellIntegration = (sendEnter) => {
204
+ this.queue.unshift({
205
+ type: 'shell_integration',
206
+ execute: async () => {
207
+ await this.injectShellIntegration()
208
+ // A fresh prompt makes the shell emit OSC 633 ;P Cwd= right away, so
209
+ // the file manager picks the current path up without waiting for the
210
+ // user's next command.
211
+ if (sendEnter && this.addon) {
212
+ this.addon._sendData('\r')
213
+ }
214
+ }
215
+ })
216
+ this.start(this.token)
217
+ }
218
+
219
+ /**
220
+ * Drain the queue one item at a time. Re-entrant calls only push items and
221
+ * return, so two loops can never interleave or race for the same array.
222
+ */
223
+ start = async (token) => {
224
+ if (this.running) {
225
+ return
226
+ }
227
+ this.running = true
228
+ try {
229
+ while (
230
+ !this.isClosed() &&
231
+ token === this.token &&
232
+ this.queue.length > 0
233
+ ) {
234
+ const item = this.queue.shift()
235
+ try {
236
+ await this.runItem(item)
237
+ } catch (error) {
238
+ console.error('[Shell Integration] Error processing queue item:', item.type, error)
239
+ }
240
+ }
241
+ } finally {
242
+ this.running = false
243
+ // A newer queue was built while this loop was still draining (reconnect,
244
+ // config toggle). Pick it up here, otherwise the fresh items would sit
245
+ // in the array with nobody left to run them.
246
+ if (
247
+ !this.isClosed() &&
248
+ token !== this.token &&
249
+ this.queue.length > 0
250
+ ) {
251
+ this.start(this.token)
252
+ }
253
+ }
254
+ }
255
+
256
+ runItem = async (item) => {
257
+ if (this.isClosed() || !this.addon) {
258
+ return
259
+ }
260
+ if (item.type === 'shell_integration') {
261
+ await item.execute()
262
+ // Give the freshly injected prompt a moment to finish drawing before
263
+ // anything is typed into it.
264
+ await this.waitForSettle({ usePrompt: false })
265
+ return
266
+ }
267
+ if (item.type !== 'delayed_script') {
268
+ return
269
+ }
270
+ item.execute()
271
+ if (item.isLast) {
272
+ return
273
+ }
274
+ await this.sleep(item.delay)
275
+ // Never type the next script into a shell that is still busy. Bounded, so
276
+ // a script that never returns can not block the rest of the queue.
277
+ await this.waitForSettle()
278
+ }
279
+
280
+ /**
281
+ * Wait until the terminal stops producing output before typing the next
282
+ * queued item. Resolves on whichever comes first:
283
+ * - a fresh prompt (OSC 633 ;A), when shell integration is active - the
284
+ * exact "previous command finished" signal
285
+ * - no output for `idleMs`
286
+ * Always resolves; `timeoutMs` caps it.
287
+ */
288
+ waitForSettle = ({ timeoutMs = settleTimeoutMs, idleMs = settleIdleMs, usePrompt = true } = {}) => {
289
+ const addon = this.addon
290
+ if (this.isClosed() || !addon || !addon.waitForOutputIdle || addon.disposed) {
291
+ return Promise.resolve(false)
292
+ }
293
+ const idle = addon.waitForOutputIdle({ idleMs, timeoutMs })
294
+ if (!usePrompt || !this.cmdAddon || !this.cmdAddon.onPrompt) {
295
+ return idle
296
+ }
297
+ return new Promise((resolve) => {
298
+ let done = false
299
+ let unsubscribe = null
300
+ const finish = (result) => {
301
+ if (done) {
302
+ return
303
+ }
304
+ done = true
305
+ clearTimeout(capTimer)
306
+ if (unsubscribe) {
307
+ unsubscribe()
308
+ }
309
+ resolve(result)
310
+ }
311
+ unsubscribe = this.cmdAddon.onPrompt(() => finish('prompt'))
312
+ const capTimer = setTimeout(() => finish(false), timeoutMs + 200)
313
+ this.timers.add(capTimer)
314
+ idle.then(quiet => finish(quiet ? 'idle' : false))
315
+ })
316
+ }
317
+
318
+ // ---- shell integration ----
319
+
320
+ canInjectShellIntegration = () => {
321
+ const { config, sftpPathFollowSsh } = this.term.props
322
+ return (
323
+ config.showCmdSuggestions ||
324
+ sftpPathFollowSsh ||
325
+ (config.restoreTerminalSessionOnReload && this.isRestorable())
326
+ ) &&
327
+ (
328
+ this.isSsh() ||
329
+ this.isTelnet() ||
330
+ this.isSerial() ||
331
+ (this.isLocal() && !isWin)
332
+ )
333
+ }
334
+
335
+ /**
336
+ * Inject shell integration commands from client-side.
337
+ * Replaces the server-side `source xxx.xxx` approach; output suppression
338
+ * hides the injection command. Resolves when injection is complete - or
339
+ * when it is given up on, never hangs.
340
+ */
341
+ injectShellIntegration = async () => {
342
+ if (this.shellInjected) {
343
+ return
344
+ }
345
+
346
+ const addon = this.addon
347
+ if (!addon) {
348
+ return
349
+ }
350
+
351
+ let shellType
352
+ if (this.isLocal()) {
353
+ const { config } = this.term.props
354
+ const localShell = isMac ? config.execMac : config.execLinux
355
+ shellType = detectShellType(localShell || '')
356
+ } else if (this.isRemoteShell()) {
357
+ shellType = await Promise.race([
358
+ this.detectRemoteShell(this.term.pid),
359
+ new Promise(resolve => {
360
+ setTimeout(() => resolve('sh'), remoteShellDetectTimeoutMs)
361
+ })
362
+ ])
363
+ }
364
+
365
+ this.shellType = shellType
366
+
367
+ // sh/ash only reports cwd via PS1 (no command tracking), so it is only
368
+ // worth injecting when something actually needs the cwd: sftp follow or
369
+ // session restore. Command suggestions alone stay disabled on sh.
370
+ const { config, sftpPathFollowSsh } = this.term.props
371
+ if (
372
+ shellType === 'sh' &&
373
+ !sftpPathFollowSsh &&
374
+ !config.restoreTerminalSessionOnReload
375
+ ) {
376
+ return
377
+ }
378
+
379
+ // The connection (or the whole terminal) can go away while
380
+ // detectRemoteShell is in flight. Bail instead of registering callbacks
381
+ // on a dead socket - that is what used to leave the queue, and every run
382
+ // script behind it, waiting forever.
383
+ if (this.isClosed() || this.addon !== addon || addon.disposed) {
384
+ return
385
+ }
386
+
387
+ const integrationCmd = getShellIntegrationCommand(shellType)
388
+ const suppressionTimeout = this.isRemoteShell() ? 5000 : 3000
389
+
390
+ return new Promise((resolve) => {
391
+ let done = false
392
+ const finish = () => {
393
+ if (done) {
394
+ return
395
+ }
396
+ done = true
397
+ this.timers.delete(safetyTimer)
398
+ clearTimeout(safetyTimer)
399
+ resolve()
400
+ }
401
+ // Hard ceiling for the whole injection. onInitialData only fires once
402
+ // the shell produces output; when it never does (dead pty, restricted
403
+ // shell, zmodem in progress) the promise - and the run scripts queued
404
+ // behind it - would otherwise never be released.
405
+ const safetyTimer = setTimeout(() => {
406
+ if (addon.outputSuppressed) {
407
+ // Unfreeze the screen rather than keep hiding output.
408
+ addon.stopOutputSuppression(false)
409
+ }
410
+ finish()
411
+ }, suppressionTimeout + 2000)
412
+ this.timers.add(safetyTimer)
413
+
414
+ // Wait for initial data (prompt/banner) to arrive before injecting
415
+ addon.onInitialData(() => {
416
+ if (done || this.addon !== addon || addon.disposed) {
417
+ return finish()
418
+ }
419
+ // Start suppressing output before sending the integration command.
420
+ // This hides the command and its output until OSC 633 is detected.
421
+ addon.startOutputSuppression(suppressionTimeout, () => {
422
+ if (!addon.disposed) {
423
+ this.shellInjected = true
424
+ }
425
+ finish()
426
+ })
427
+ addon._sendData(integrationCmd)
428
+ })
429
+ })
430
+ }
431
+ }
432
+
433
+ 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
 
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Full-screen plain-text view of the buffer, for touch devices.
3
+ *
4
+ * Selecting inside a canvas-rendered terminal on a phone is hopeless: there is
5
+ * no system selection to lean on, so every touch terminal ends up
6
+ * re-implementing magnifiers, draggable handles and edge auto-scroll. This
7
+ * takes the other route — hand the text to an ordinary read-only <textarea>
8
+ * and let iOS/Android supply the whole native toolbox (long-press word select,
9
+ * resizable handles, the Copy callout) for free.
10
+ *
11
+ * Rendered through a portal onto <body> rather than in place: `.term-wrap` is
12
+ * a positioned element with its own z-index, so anything inside it is trapped
13
+ * in that stacking context and would still be covered by the touch shortcut
14
+ * bar (z-index 400). The portal also keeps these styles away from
15
+ * `.term-wrap`, which switches off -webkit-touch-callout and user-select for
16
+ * the terminal's own gestures — `-webkit-touch-callout` is inherited, so
17
+ * without the reset in terminal.styl the native callout would never appear.
18
+ *
19
+ * React events still bubble along the React tree, not the DOM one, so the
20
+ * parent's touch handlers and the antd Dropdown's `contextMenu` trigger both
21
+ * still see events from here. Two guards in response:
22
+ * - term-touch.js bails out of any touch start whose target is this overlay;
23
+ * - the contextmenu handler below stops the event, which would otherwise open
24
+ * the terminal menu on top of the selection. Only propagation is stopped,
25
+ * never the default action, since that default action *is* the native
26
+ * callout being relied on.
27
+ *
28
+ * The content is a snapshot taken when the panel opens: output arriving
29
+ * afterwards keeps going to the terminal underneath and cannot disturb a
30
+ * selection the user is in the middle of making.
31
+ */
32
+
33
+ import { memo } from 'react'
34
+ import { createPortal } from 'react-dom'
35
+
36
+ const e = window.translate
37
+
38
+ export default memo(function TerminalSelectText (props) {
39
+ if (!props.visible) {
40
+ return null
41
+ }
42
+ const blockMenu = ev => ev.stopPropagation()
43
+ return createPortal(
44
+ <div
45
+ className='terminal-select-text'
46
+ onContextMenu={blockMenu}
47
+ >
48
+ <div className='terminal-select-text-bar'>
49
+ <span>{e('selectText')}</span>
50
+ <span className='terminal-select-text-btns'>
51
+ <a onClick={props.onSelectAll}>{e('selectall')}</a>
52
+ <a onClick={props.onCopy}>{e('copy')}</a>
53
+ <a onClick={props.onClose}>{e('close')}</a>
54
+ </span>
55
+ </div>
56
+ <textarea
57
+ className='terminal-select-text-area'
58
+ ref={props.textareaRef}
59
+ value={props.text}
60
+ readOnly
61
+ spellCheck={false}
62
+ autoComplete='off'
63
+ autoCorrect='off'
64
+ autoCapitalize='off'
65
+ />
66
+ </div>,
67
+ document.body
68
+ )
69
+ })