@electerm/electerm-react 3.15.175 → 3.15.186

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 (37) hide show
  1. package/client/common/bookmark-schemas.js +2 -0
  2. package/client/common/drop-position.js +59 -0
  3. package/client/components/bookmark-form/bookmark-schema.js +2 -0
  4. package/client/components/bookmark-form/common/connection-hopping.jsx +33 -7
  5. package/client/components/bookmark-form/common/ssh-tunnel-form.jsx +25 -7
  6. package/client/components/bookmark-form/common/ssh-tunnels.jsx +1 -1
  7. package/client/components/bookmark-form/config/serial.js +14 -0
  8. package/client/components/main/term-fullscreen.styl +5 -0
  9. package/client/components/quick-commands/on-drop.js +6 -1
  10. package/client/components/quick-commands/quick-commands-box.jsx +18 -2
  11. package/client/components/quick-commands/quick-commands-list-form.jsx +15 -4
  12. package/client/components/quick-commands/quick-commands-list.jsx +18 -2
  13. package/client/components/session/session-control.jsx +331 -0
  14. package/client/components/session/session-control.styl +38 -0
  15. package/client/components/session/session.jsx +41 -246
  16. package/client/components/session/session.styl +1 -0
  17. package/client/components/sftp/address-bookmark-item.jsx +15 -2
  18. package/client/components/sftp/code-compare-utils.js +260 -0
  19. package/client/components/sftp/code-compare.jsx +183 -0
  20. package/client/components/sftp/code-compare.styl +13 -0
  21. package/client/components/sftp/file-compare-modal.jsx +87 -42
  22. package/client/components/shortcuts/shortcut-handler.js +20 -0
  23. package/client/components/sidebar/transport-ui.jsx +21 -14
  24. package/client/components/tabs/index.jsx +18 -3
  25. package/client/components/tabs/tab.jsx +27 -8
  26. package/client/components/tabs/tabs.styl +8 -0
  27. package/client/components/terminal/attach-addon-custom.js +147 -36
  28. package/client/components/terminal/highlight-addon.js +26 -8
  29. package/client/components/terminal/term-search.jsx +1 -1
  30. package/client/components/terminal/terminal.jsx +193 -2
  31. package/client/components/terminal/terminal.styl +0 -2
  32. package/client/components/terminal-info/run-cmd.jsx +1 -1
  33. package/client/components/tree-list/tree-list.jsx +57 -39
  34. package/client/css/basic.styl +14 -0
  35. package/client/css/mobile.styl +31 -9
  36. package/client/store/item.js +10 -6
  37. package/package.json +1 -1
@@ -214,6 +214,26 @@ export function shortcutExtend (Cls) {
214
214
  this.term.buffer.active.type === 'alternate'
215
215
  ) {
216
216
  return true
217
+ } else if (
218
+ (key === 'ArrowLeft' || key === 'ArrowRight') &&
219
+ type === 'keydown' &&
220
+ altKey &&
221
+ !ctrlKey &&
222
+ !shiftKey &&
223
+ !metaKey
224
+ ) {
225
+ // Restore xterm 5.5.0 behavior: convert Alt/Option + Left/Right into
226
+ // readline-compatible word-movement sequences.
227
+ // xterm 6.x dropped the platform-specific override in evaluateKeyboardEvent,
228
+ // which now sends CSI modifier sequences (e.g. ESC[1;3D) that old shells
229
+ // do not bind to backward-word / forward-word, causing trailing chars
230
+ // ("D", ";3D") to leak as literal input.
231
+ const seq = key === 'ArrowLeft'
232
+ ? (isMacJs ? '\x1bb' : '\x1b[1;5D')
233
+ : (isMacJs ? '\x1bf' : '\x1b[1;5C')
234
+ sendInputData(this, seq)
235
+ this.term?.scrollToBottom()
236
+ return false
217
237
  }
218
238
 
219
239
  if (
@@ -13,6 +13,7 @@ import {
13
13
  import { action } from 'manate'
14
14
  import { addClass, removeClass } from '../../common/class'
15
15
  import { refsStatic } from '../common/ref'
16
+ import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
16
17
  import './transfer.styl'
17
18
 
18
19
  const e = window.translate
@@ -70,28 +71,28 @@ export default function Transporter (props) {
70
71
  addClass(dom.current, onDragCls)
71
72
  }
72
73
 
73
- function onDragEnter () {
74
+ function updateDropIndicator (e) {
74
75
  clearCls()
75
- addClass(dom.current, onDragOverCls)
76
+ setDropIndicator(dom.current, isDropAfterHalf(e, dom.current))
77
+ }
78
+
79
+ function onDragEnter (e) {
80
+ e.preventDefault()
81
+ updateDropIndicator(e)
76
82
  }
77
83
 
78
84
  function onDragExit () {
79
85
  // debug('ondragexit')
80
- // let {target} = e
81
- // removeClass(target, 'sftp-dragover')
82
86
  }
83
87
 
84
- function onDragLeave (e) {
85
- // debug('ondragleave')
86
- const { target } = e
87
- removeClass(target, onDragOverCls)
88
+ function onDragLeave () {
89
+ clearDropIndicator(dom.current)
88
90
  }
89
91
 
90
92
  function onDragOver (e) {
91
93
  // debug('ondragover')
92
- // debug(e.target)
93
- // removeClass(dom.current, 'sftp-dragover')
94
94
  e.preventDefault()
95
+ updateDropIndicator(e)
95
96
  }
96
97
 
97
98
  function onDragStart (e) {
@@ -103,6 +104,7 @@ export default function Transporter (props) {
103
104
 
104
105
  function onDrop (e) {
105
106
  e.preventDefault()
107
+ clearDropIndicator(dom.current)
106
108
  const { target } = e
107
109
  if (!target) {
108
110
  return
@@ -126,15 +128,19 @@ export default function Transporter (props) {
126
128
 
127
129
  const arr = window.store.fileTransfers
128
130
  const indexFrom = arr.findIndex(t => t.id === fromId)
129
- let indexDrop = arr.findIndex(t => t.id === dropId)
131
+ const indexDrop = arr.findIndex(t => t.id === dropId)
132
+ // bottom half of the target row => insert after it, so the last
133
+ // transfer can receive a drop (append to the end of the queue).
134
+ const insertAfter = isDropAfterHalf(e, onDropTab)
130
135
  if (indexFrom >= 0 && indexDrop >= 0) {
131
136
  // Reorder tabs and update batch
132
137
  action(function () {
133
138
  const [tr] = arr.splice(indexFrom, 1)
134
- if (indexFrom < indexDrop) {
135
- indexDrop = indexDrop - 1
139
+ let insertIndex = insertAfter ? indexDrop + 1 : indexDrop
140
+ if (indexFrom < insertIndex) {
141
+ insertIndex = insertIndex - 1
136
142
  }
137
- arr.splice(indexDrop, 0, tr)
143
+ arr.splice(insertIndex, 0, tr)
138
144
  })()
139
145
  }
140
146
  }
@@ -142,6 +148,7 @@ export default function Transporter (props) {
142
148
  function onDragEnd (e) {
143
149
  removeClass(dom.current, onDragCls)
144
150
  clearCls()
151
+ clearDropIndicator(dom.current)
145
152
  e && e.dataTransfer && e.dataTransfer.clearData()
146
153
  }
147
154
  const isTransfer = typeTo !== typeFrom
@@ -386,6 +386,23 @@ export default class Tabs extends Component {
386
386
  )
387
387
  }
388
388
 
389
+ renderMobileTabsInner (currentTabEl) {
390
+ const inner = (
391
+ <div className='tabs-inner mobile-tabs-inner'>
392
+ {currentTabEl}
393
+ </div>
394
+ )
395
+ const { config } = this.props
396
+ if (config.useSystemTitleBar) {
397
+ return inner
398
+ }
399
+ return (
400
+ <AppDrag>
401
+ {inner}
402
+ </AppDrag>
403
+ )
404
+ }
405
+
389
406
  renderMobileTabs () {
390
407
  const { tabs = [], currentBatchTabId } = this.props
391
408
  const currentTab = tabs.find(t => t.id === currentBatchTabId)
@@ -410,9 +427,7 @@ export default class Tabs extends Component {
410
427
  : null
411
428
  return (
412
429
  <div className='tabs mobile-tabs' ref={this.tabsRef}>
413
- <div className='tabs-inner mobile-tabs-inner'>
414
- {currentTabEl}
415
- </div>
430
+ {this.renderMobileTabsInner(currentTabEl)}
416
431
  {this.renderMobileTabsExtra(items)}
417
432
  {this.renderWindowControl()}
418
433
  </div>
@@ -25,10 +25,12 @@ import isDark from '../../common/is-color-dark'
25
25
  import { action } from 'manate'
26
26
  import iconsMap from '../sys-menu/icons-map.jsx'
27
27
  import { shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
28
+ import { isDropAfterHalf } from '../../common/drop-position'
28
29
 
29
30
  const e = window.translate
30
31
  const onDragCls = 'ondrag-tab'
31
32
  const onDragOverCls = 'dragover-tab'
33
+ const onDragOverAfterCls = 'dragover-tab-after'
32
34
 
33
35
  class Tab extends Component {
34
36
  constructor (props) {
@@ -110,6 +112,9 @@ class Tab extends Component {
110
112
  document.querySelectorAll('.' + onDragOverCls).forEach((d) => {
111
113
  removeClass(d, onDragOverCls)
112
114
  })
115
+ document.querySelectorAll('.' + onDragOverAfterCls).forEach((d) => {
116
+ removeClass(d, onDragOverAfterCls)
117
+ })
113
118
  }
114
119
 
115
120
  handleClick = (e) => {
@@ -120,9 +125,8 @@ class Tab extends Component {
120
125
  addClass(this.tabRef.current, onDragCls)
121
126
  }
122
127
 
123
- onDragEnter = () => {
124
- this.clearCls()
125
- addClass(this.tabRef.current, onDragOverCls)
128
+ onDragEnter = (e) => {
129
+ this.applyDragOverCls(e)
126
130
  }
127
131
 
128
132
  onDragExit = () => {
@@ -135,13 +139,22 @@ class Tab extends Component {
135
139
  // debug('ondragleave')
136
140
  const { target } = e
137
141
  removeClass(target, onDragOverCls)
142
+ removeClass(target, onDragOverAfterCls)
143
+ }
144
+
145
+ // Pick the before/after indicator based on which half of the tab
146
+ // the pointer is in, so the drop line shows where it will actually go.
147
+ applyDragOverCls = e => {
148
+ this.clearCls()
149
+ const insertAfter = isDropAfterHalf(e, this.tabRef.current, true)
150
+ addClass(this.tabRef.current, insertAfter ? onDragOverAfterCls : onDragOverCls)
138
151
  }
139
152
 
140
153
  onDragOver = e => {
141
154
  // debug('ondragover')
142
155
  // debug(e.target)
143
- // removeClass(this.tabRef.current, 'sftp-dragover')
144
156
  e.preventDefault()
157
+ this.applyDragOverCls(e)
145
158
  }
146
159
 
147
160
  onDragStart = e => {
@@ -178,7 +191,9 @@ class Tab extends Component {
178
191
  const { id } = fromTab
179
192
  const storeTabs = window.store.tabs
180
193
  const indexFrom = storeTabs.findIndex(t => t.id === id)
181
- let indexDrop = storeTabs.findIndex(t => t.id === dropId)
194
+ const indexDrop = storeTabs.findIndex(t => t.id === dropId)
195
+ // tabs are laid out horizontally: right half of the target => insert after
196
+ const insertAfter = isDropAfterHalf(e, onDropTab, true)
182
197
 
183
198
  if (indexFrom >= 0 && indexDrop >= 0) {
184
199
  const targetTab = storeTabs[indexDrop]
@@ -197,10 +212,13 @@ class Tab extends Component {
197
212
  action(function () {
198
213
  const [tab] = storeTabs.splice(indexFrom, 1)
199
214
  tab.batch = targetTab.batch // Update the batch to match target tab's batch
200
- if (indexFrom < indexDrop) {
201
- indexDrop = indexDrop - 1
215
+ // insertAfter lets a tab land after the target, so the last tab
216
+ // can receive a drop (insertIndex can reach storeTabs.length).
217
+ let insertIndex = insertAfter ? indexDrop + 1 : indexDrop
218
+ if (indexFrom < insertIndex) {
219
+ insertIndex = insertIndex - 1
202
220
  }
203
- storeTabs.splice(indexDrop, 0, tab)
221
+ storeTabs.splice(insertIndex, 0, tab)
204
222
  })()
205
223
  window.store.focus()
206
224
  }
@@ -216,6 +234,7 @@ class Tab extends Component {
216
234
 
217
235
  onDragEnd = e => {
218
236
  removeClass(this.tabRef.current, onDragCls)
237
+ removeClass(this.tabRef.current, onDragOverAfterCls)
219
238
  this.clearCls()
220
239
  e && e.dataTransfer && e.dataTransfer.clearData()
221
240
  }
@@ -68,6 +68,14 @@
68
68
  width 1px
69
69
  border 1px dashed var(--text-dark)
70
70
  height 36px
71
+ &.dragover-tab-after::after
72
+ position absolute
73
+ content ''
74
+ right -2px
75
+ top 0
76
+ width 1px
77
+ border 1px dashed var(--text-dark)
78
+ height 36px
71
79
  @keyframes blink
72
80
  0%
73
81
  background-color #e0e0e0
@@ -23,6 +23,32 @@ export default class AttachAddonCustom {
23
23
  this._passwordPromptDetected = false
24
24
  this._pendingEchoCheck = null
25
25
  this._echoCheckTimer = null
26
+ // Write coalescing: terminal output is buffered and flushed on a short
27
+ // timer so that heavy bursts (e.g. `yum install` download progress, which
28
+ // rewrites the same line with \r + clear-line thousands of times per
29
+ // second) collapse into a few term.write() calls per frame instead of
30
+ // blocking the main thread on every WebSocket message.
31
+ this._writeBuffer = []
32
+ this._bufferBytes = 0
33
+ this._flushScheduled = false
34
+ this._flushTimer = null
35
+ // Coalescing window. Output is flushed at most once per interval.
36
+ // 16ms ~= one frame; low enough that interactive echo feels instant,
37
+ // high enough to merge a multi-MB/s flood into ~60 writes/s.
38
+ this._flushIntervalMs = 16
39
+ // Time of the last actual flush. Drives the interactive fast path in
40
+ // _enqueueWrite: output arriving after an idle gap (keystroke echo,
41
+ // command result) is flushed immediately instead of paying the
42
+ // coalescing delay. 0 = the first chunk ever flushes immediately.
43
+ this._lastFlushTime = 0
44
+ // Soft cap on buffered-but-unflushed bytes. Under a sustained flood the
45
+ // producer outruns the renderer; once pending output exceeds this we drop
46
+ // the OLDEST data (preserving the newest, visible tail and the line
47
+ // currently being rewritten). Normal interactive output is many orders of
48
+ // magnitude smaller and is never dropped.
49
+ this._maxBufferBytes = 256 * 1024
50
+ this._droppedBytes = 0
51
+ this._droppedWarned = false
26
52
  }
27
53
 
28
54
  _initBase = async () => {
@@ -182,27 +208,11 @@ export default class AttachAddonCustom {
182
208
  }
183
209
  }
184
210
 
185
- if (this.outputSuppressed) {
186
- let str = data
187
- if (typeof data !== 'string') {
188
- const decoder = this.decoder || new TextDecoder('utf-8')
189
- try {
190
- str = decoder.decode(data instanceof ArrayBuffer ? data : new Uint8Array(data))
191
- } catch (e) {
192
- str = ''
193
- }
194
- }
195
-
196
- if (this.checkForShellIntegration(str)) {
197
- this.onShellIntegrationDetected()
198
- return
199
- }
200
-
201
- this.suppressedData.push(data)
202
- return
203
- }
204
-
205
- // Password prompt detection on output
211
+ // Decode once, synchronously. The previous implementation routed binary
212
+ // chunks through `new Blob()` + `FileReader.readAsArrayBuffer()` and decoded
213
+ // in an async `onRead` callback; that async round-trip (plus a per-chunk
214
+ // Blob allocation) showed up in the profile and fragmented main-thread work.
215
+ // We already hold a TextDecoder, so decode inline like the suppression path.
206
216
  let str = data
207
217
  if (typeof data !== 'string') {
208
218
  try {
@@ -213,6 +223,18 @@ export default class AttachAddonCustom {
213
223
  str = ''
214
224
  }
215
225
  }
226
+
227
+ if (this.outputSuppressed) {
228
+ if (this.checkForShellIntegration(str)) {
229
+ this.onShellIntegrationDetected()
230
+ return
231
+ }
232
+ this.suppressedData.push(data)
233
+ return
234
+ }
235
+
236
+ // Prompt/echo detection runs per chunk (cheap) so password prompts and
237
+ // shell integration are still detected promptly.
216
238
  this._handleEchoDetection(str)
217
239
  if (this._checkPasswordPrompt(str) && !this._passwordPromptDetected) {
218
240
  this._passwordPromptDetected = true
@@ -222,27 +244,109 @@ export default class AttachAddonCustom {
222
244
  }, 100)
223
245
  }
224
246
 
225
- if (typeof data === 'string') {
226
- term?.parent?.notifyOnData()
227
- term.write(data)
228
- // Notify parent that the terminal buffer has been updated (echo received).
229
- // This allows the suggestion dropdown to refresh after the buffer reflects
230
- // the latest server-side state (e.g., after backspace echo).
231
- term?.parent?.onTerminalWrite?.()
247
+ // Coalesce the actual write (see _enqueueWrite). notifyOnData /
248
+ // onTerminalWrite fire once per flush instead of once per chunk.
249
+ this._enqueueWrite(str)
250
+ }
251
+
252
+ // Buffer decoded output and flush it on a short timer so a burst of messages
253
+ // becomes a single (size-capped) term.write() call, keeping the main thread
254
+ // free to handle UI events and input between flushes.
255
+ _enqueueWrite = (str) => {
256
+ if (!this.term || !str) {
257
+ return
258
+ }
259
+ this._writeBuffer.push(str)
260
+ this._bufferBytes += str.length
261
+ if (this._bufferBytes > this._maxBufferBytes) {
262
+ this._dropOldestUntil()
263
+ }
264
+ // A hidden window gets its timers throttled by Chromium (Electron's
265
+ // backgroundThrottling defaults to true), which would stall the flush
266
+ // timer and force the byte cap to drop real output. Nothing is painted
267
+ // while hidden, so coalescing buys nothing — flush synchronously.
268
+ if (document.hidden) {
269
+ clearTimeout(this._flushTimer)
270
+ this._flushWrites()
232
271
  return
233
272
  }
234
- data = new Uint8Array(data)
235
- const fileReader = new FileReader()
236
- fileReader.addEventListener('load', this.onRead)
237
- fileReader.readAsArrayBuffer(new window.Blob([data]))
273
+ // Interactive fast path: if the last flush was longer ago than the
274
+ // coalescing window (keystroke echo, command result after idle), flush
275
+ // immediately so typing adds zero latency. Only output arriving inside
276
+ // a burst window pays the coalescing delay — which is exactly the flood
277
+ // case coalescing exists for.
278
+ const elapsed = Date.now() - this._lastFlushTime
279
+ if (elapsed >= this._flushIntervalMs) {
280
+ clearTimeout(this._flushTimer)
281
+ this._flushWrites()
282
+ return
283
+ }
284
+ if (!this._flushScheduled) {
285
+ this._flushScheduled = true
286
+ this._flushTimer = setTimeout(this._flushWrites, this._flushIntervalMs - elapsed)
287
+ }
238
288
  }
239
289
 
240
- onRead = (ev) => {
241
- const data = ev.target.result
290
+ // Under heavy flood the producer outruns the renderer; once pending output
291
+ // exceeds the cap we drop the OLDEST chunks (preserving the newest, visible
292
+ // tail and the line currently being rewritten). The first kept chunk is
293
+ // trimmed to the next newline so we never render a half line / broken escape.
294
+ _dropOldestUntil = () => {
295
+ const buf = this._writeBuffer
296
+ let kept = 0
297
+ let cutIdx = buf.length
298
+ for (let i = buf.length - 1; i >= 0; i--) {
299
+ if (kept + buf[i].length > this._maxBufferBytes) {
300
+ cutIdx = i + 1
301
+ break
302
+ }
303
+ kept += buf[i].length
304
+ }
305
+ if (cutIdx <= 0) {
306
+ return
307
+ }
308
+ let dropped = 0
309
+ for (let i = 0; i < cutIdx; i++) {
310
+ dropped += buf[i].length
311
+ }
312
+ if (cutIdx < buf.length) {
313
+ const first = buf[cutIdx]
314
+ const nl = first.indexOf('\n')
315
+ if (nl >= 0 && nl < first.length - 1) {
316
+ dropped += nl + 1
317
+ buf[cutIdx] = first.slice(nl + 1)
318
+ } else {
319
+ dropped += buf[cutIdx].length
320
+ cutIdx += 1
321
+ }
322
+ }
323
+ this._writeBuffer = buf.slice(cutIdx)
324
+ this._bufferBytes -= dropped
325
+ this._droppedBytes += dropped
326
+ if (!this._droppedWarned) {
327
+ this._droppedWarned = true
328
+ console.warn('[AttachAddon] Heavy output detected; coalescing writes and dropping intermediate output to keep the UI responsive.')
329
+ }
330
+ }
331
+
332
+ _flushWrites = () => {
333
+ this._flushScheduled = false
334
+ this._flushTimer = null
335
+ const buf = this._writeBuffer
336
+ if (!buf.length || !this.term) {
337
+ this._writeBuffer = []
338
+ this._bufferBytes = 0
339
+ return
340
+ }
341
+ this._writeBuffer = []
342
+ this._bufferBytes = 0
343
+ const data = buf.length === 1 ? buf[0] : buf.join('')
242
344
  const { term } = this
345
+ this._lastFlushTime = Date.now()
346
+ term.write(data)
347
+ // Notify parent that the terminal buffer has been updated (echo received),
348
+ // once per flush instead of once per chunk.
243
349
  term?.parent?.notifyOnData()
244
- const str = this.decoder.decode(data)
245
- term?.write(str)
246
350
  term?.parent?.onTerminalWrite?.()
247
351
  }
248
352
 
@@ -324,6 +428,13 @@ export default class AttachAddonCustom {
324
428
  this._stopKeepalive()
325
429
  clearTimeout(this._echoCheckTimer)
326
430
  this._echoCheckTimer = null
431
+ if (this._flushTimer) {
432
+ clearTimeout(this._flushTimer)
433
+ this._flushTimer = null
434
+ }
435
+ this._flushScheduled = false
436
+ this._writeBuffer = []
437
+ this._bufferBytes = 0
327
438
  this.term = null
328
439
  this._disposables.forEach(d => d.dispose())
329
440
  this._disposables.length = 0
@@ -2,6 +2,18 @@ export class KeywordHighlighterAddon {
2
2
  constructor (keywords) {
3
3
  this.keywords = keywords || []
4
4
  this.compiledPatterns = this.compilePatterns()
5
+ // Writes larger than this bypass keyword highlighting. Highlighting is meant
6
+ // for interactive command output; a large chunk is a command flood or
7
+ // download progress where scanning every byte (and rebuilding the result
8
+ // string) is pure overhead. Coupled with write coalescing in the attach
9
+ // addon, flood output skips this path entirely.
10
+ this.maxHighlightLength = 4096
11
+ // Compile the ANSI/OSC splitter once instead of allocating a new RegExp on
12
+ // every write (this was the #1 allocation in the hot path under load).
13
+ const ESC = String.fromCharCode(27) // \x1b
14
+ const BEL = String.fromCharCode(7) // \x07
15
+ // eslint-disable-next-line no-control-regex
16
+ this._ansiPattern = new RegExp('(' + ESC + '\\][^' + BEL + ESC + ']*(?:' + BEL + '|' + ESC + '\\\\)|' + ESC + '\\[\\??[0-9;]*[A-Za-z])', 'g')
5
17
  }
6
18
 
7
19
  // Pre-compile all regex patterns once for better performance
@@ -54,11 +66,8 @@ export class KeywordHighlighterAddon {
54
66
 
55
67
  // Split text into segments: ANSI/OSC sequences vs plain text
56
68
  // Match OSC sequences (ESC ] ... BEL or ESC ] ... ESC \) and CSI sequences (ESC [ ... letter)
57
- // Use String.fromCharCode to avoid lint warnings about control characters
58
- const ESC = String.fromCharCode(27) // \x1b
59
- const BEL = String.fromCharCode(7) // \x07
60
- // eslint-disable-next-line no-control-regex
61
- const ansiPattern = new RegExp('(' + ESC + '\\][^' + BEL + ESC + ']*(?:' + BEL + '|' + ESC + '\\\\)|' + ESC + '\\[\\??[0-9;]*[A-Za-z])', 'g')
69
+ const ansiPattern = this._ansiPattern
70
+ ansiPattern.lastIndex = 0
62
71
 
63
72
  const segments = []
64
73
  let lastIndex = 0
@@ -142,9 +151,18 @@ export class KeywordHighlighterAddon {
142
151
  if (self.containsDcsSequence(data)) {
143
152
  return self.originalWrite(data)
144
153
  }
145
- self.originalWrite(
146
- terminal.displayRaw ? self.escape(data) : self.highlightKeywords(data)
147
- )
154
+ // displayRaw applies to every write regardless of size: raw mode shows
155
+ // escape sequences as text, so skipping escape() for large chunks would
156
+ // let raw ESC bytes be interpreted by xterm instead of displayed.
157
+ if (terminal.displayRaw) {
158
+ return self.originalWrite(self.escape(data))
159
+ }
160
+ // Skip keyword highlighting for large writes (command floods, download
161
+ // progress). Highlighting targets interactive command output.
162
+ if (data.length > self.maxHighlightLength) {
163
+ return self.originalWrite(data)
164
+ }
165
+ self.originalWrite(self.highlightKeywords(data))
148
166
  }
149
167
  }
150
168
 
@@ -70,7 +70,7 @@ export default class TermSearch extends PureComponent {
70
70
  } = this.props
71
71
  refs.get('term-' + activeTabId)
72
72
  ?.searchPrev(
73
- searchTerm ?? this.props.termSearch,
73
+ typeof searchTerm === 'string' ? searchTerm : this.props.termSearch,
74
74
  copy(termSearchOptions)
75
75
  )
76
76
  }