@electerm/electerm-react 3.9.5 → 3.9.15

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.
@@ -28,7 +28,11 @@ export default class FileListTable extends Component {
28
28
  containerRef = createRef()
29
29
 
30
30
  componentDidUpdate (prevProps) {
31
- if (prevProps.fileList !== this.props.fileList) {
31
+ const prevList = prevProps.fileList
32
+ const nextList = this.props.fileList
33
+ const contentChanged = prevList.length !== nextList.length ||
34
+ prevList.some((f, i) => f.id !== nextList[i].id)
35
+ if (contentChanged) {
32
36
  if (this.containerRef.current) {
33
37
  this.containerRef.current.scrollTop = 0
34
38
  }
@@ -8,7 +8,8 @@ import { refsTabs } from '../common/ref'
8
8
  import {
9
9
  CloseOutlined,
10
10
  Loading3QuartersOutlined,
11
- BorderlessTableOutlined
11
+ BorderlessTableOutlined,
12
+ LockOutlined
12
13
  } from '@ant-design/icons'
13
14
  import {
14
15
  Tooltip,
@@ -33,7 +34,7 @@ class Tab extends Component {
33
34
  constructor (props) {
34
35
  super(props)
35
36
  this.state = {
36
- terminalOnData: false
37
+ terminalOnData: ''
37
38
  }
38
39
  this.id = 'tab-' + this.props.tab.id
39
40
  refsTabs.add(this.id, this)
@@ -48,19 +49,38 @@ class Tab extends Component {
48
49
  }
49
50
 
50
51
  notifyOnData = () => {
52
+ if (this.state.terminalOnData === 'password') {
53
+ return
54
+ }
51
55
  if (this.timer) {
52
56
  clearTimeout(this.timer)
53
57
  this.timer = null
54
58
  }
55
59
  this.setState({
56
- terminalOnData: true
60
+ terminalOnData: 'feed'
57
61
  })
58
62
  this.timer = setTimeout(this.clearTerminalOnData, 4000)
59
63
  }
60
64
 
61
65
  clearTerminalOnData = () => {
62
66
  this.setState({
63
- terminalOnData: false
67
+ terminalOnData: ''
68
+ })
69
+ }
70
+
71
+ notifyPasswordPrompt = () => {
72
+ if (this.timer) {
73
+ clearTimeout(this.timer)
74
+ this.timer = null
75
+ }
76
+ this.setState({
77
+ terminalOnData: 'password'
78
+ })
79
+ }
80
+
81
+ clearPasswordPrompt = () => {
82
+ this.setState({
83
+ terminalOnData: ''
64
84
  })
65
85
  }
66
86
 
@@ -424,13 +444,7 @@ class Tab extends Component {
424
444
  {
425
445
  'tab-last': isLast
426
446
  },
427
- status,
428
- {
429
- 'is-terminal-active': terminalOnData
430
- },
431
- {
432
- 'is-transporting': isTransporting
433
- }
447
+ status
434
448
  )
435
449
  const title = createName(tab)
436
450
  let tooltipTitle = title
@@ -477,15 +491,19 @@ class Tab extends Component {
477
491
  >
478
492
  <Dropdown {...dropdownProps}>
479
493
  <div
480
- className='tab-title elli pd1x'
494
+ className='tab-title elli'
481
495
  onClick={this.handleClick}
482
496
  onDoubleClick={this.handleDup}
483
497
  >
484
- <Loading3QuartersOutlined
485
- className='pointer tab-reload mg1r'
486
- onClick={this.handleReloadTab}
487
- title={e('reload')}
488
- />
498
+ {
499
+ status === 'error' && (
500
+ <Loading3QuartersOutlined
501
+ className='pointer tab-reload mg1r'
502
+ onClick={this.handleReloadTab}
503
+ title={e('reload')}
504
+ />
505
+ )
506
+ }
489
507
  <span className='tab-title'>
490
508
  <span className='iblock mg1r tab-count' style={styleTag}>{tabCount}</span>
491
509
  <span className='mg1r'>{title}</span>
@@ -493,8 +511,9 @@ class Tab extends Component {
493
511
  </div>
494
512
  </Dropdown>
495
513
  <div className={'tab-status ' + status} />
496
- <div className='tab-traffic' />
497
- <BorderlessTableOutlined className='tab-terminal-feed' />
514
+ {isTransporting && <div className='tab-traffic' />}
515
+ {terminalOnData === 'feed' && <BorderlessTableOutlined className='tab-terminal-feed' />}
516
+ {terminalOnData === 'password' && <LockOutlined className='tab-terminal-feed password' />}
498
517
  {
499
518
  this.renderCloseIcon()
500
519
  }
@@ -36,6 +36,7 @@
36
36
  vertical-align middle
37
37
  cursor pointer
38
38
  position relative
39
+ padding 0 14px
39
40
  min-width 100px
40
41
  max-width 200px
41
42
  line-height 36px
@@ -46,7 +47,6 @@
46
47
  color var(--text-dark)
47
48
  &.tab-last
48
49
  margin-right 5px
49
- .tab-reload
50
50
  .tab-close
51
51
  display none
52
52
  &.active
@@ -68,10 +68,6 @@
68
68
  width 1px
69
69
  border 1px dashed var(--text-dark)
70
70
  height 36px
71
- &.error
72
- .tab-reload
73
- display inline-block
74
- color var(--text-light)
75
71
  @keyframes blink
76
72
  0%
77
73
  background-color #e0e0e0
@@ -96,30 +92,25 @@
96
92
  background-color var(--error)
97
93
  &.processing
98
94
  background-color var(--primary)
99
- .is-transporting .tab-traffic
100
- display block
101
- animation blink 2s infinite
102
- /* Remove opacity animation, use background-color */
103
95
  .tab-traffic
104
- display none
105
96
  left 10px
106
97
  width 5px
107
98
  border-radius 0
108
99
  background-color var(--success)
109
- .is-terminal-active .tab-terminal-feed
110
- display block
111
100
  animation blink 2s infinite
112
- background-color transparent !important
113
- /* Remove opacity animation, use background-color */
114
101
  .tab-terminal-feed
115
- display none
116
- left 20px
102
+ width 12px
103
+ height 12px
117
104
  border-radius 0
118
105
  color var(--success)
119
- font-size 8px
106
+ font-size 12px
120
107
  left 2px
121
108
  top 24px
122
109
  background none
110
+ background-color transparent !important
111
+ animation blink 2s infinite
112
+ &.password
113
+ color var(--warn)
123
114
  .tab-close
124
115
  position absolute
125
116
  right 5px
@@ -223,6 +223,7 @@ export default class AttachAddonCustom {
223
223
  }
224
224
 
225
225
  if (typeof data === 'string') {
226
+ term?.parent?.notifyOnData()
226
227
  return term.write(data)
227
228
  }
228
229
  data = new Uint8Array(data)
@@ -242,13 +243,16 @@ export default class AttachAddonCustom {
242
243
  sendToServer = (data) => {
243
244
  this._lastInputTime = Date.now()
244
245
  // Start echo detection when password prompt is suspected
245
- if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n') {
246
+ if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n' && data !== '\x03') {
246
247
  this._pendingEchoCheck = { char: data, time: Date.now() }
247
248
  clearTimeout(this._echoCheckTimer)
248
249
  this._echoCheckTimer = setTimeout(this._onEchoCheckTimeout, 200)
249
250
  }
250
- // Reset password state on Enter
251
- if (data === '\r' || data === '\n') {
251
+ // Reset password state on Enter or Ctrl+C
252
+ if (data === '\r' || data === '\n' || data === '\x03') {
253
+ if (this._passwordPromptDetected) {
254
+ this.term?.parent?.onPasswordPromptCancelled?.()
255
+ }
252
256
  this._passwordPromptDetected = false
253
257
  this._lastOutputLine = ''
254
258
  this._pendingEchoCheck = null
@@ -278,7 +278,7 @@ export default class TerminalCmdSuggestions extends Component {
278
278
  id: uid(),
279
279
  command: b.password,
280
280
  type: 'PW',
281
- hint: [b.username, b.host].filter(Boolean).join('@')
281
+ hint: [b.username, [b.host, b.port].filter(Boolean).join(':')].filter(Boolean).join('@')
282
282
  })
283
283
  }
284
284
  }
@@ -830,6 +830,7 @@ class Term extends Component {
830
830
  }
831
831
 
832
832
  onPasswordPromptDetected = () => {
833
+ window.store.notifyTabPasswordPrompt(this.props.tab.id)
833
834
  if (!this.props.config.showCmdSuggestions) {
834
835
  return
835
836
  }
@@ -842,6 +843,7 @@ class Term extends Component {
842
843
  }
843
844
 
844
845
  onPasswordPromptCancelled = () => {
846
+ window.store.clearTabPasswordPrompt(this.props.tab.id)
845
847
  const suggestions = refsStatic.get('terminal-suggestions')
846
848
  if (suggestions?.state?.passwordMode) {
847
849
  suggestions.closeSuggestions()
@@ -6,18 +6,30 @@ import { useState } from 'react'
6
6
  import { Button, Input, Space } from 'antd'
7
7
  import { safeGetItem, safeSetItem } from '../../common/safe-local-storage.js'
8
8
 
9
- const LS_KEY = 'customEditorCommand'
9
+ export const CUSTOM_EDITOR_COMMAND_LS_KEY = 'customEditorCommand'
10
+ export const CUSTOM_EDITOR_AUTO_OPEN_LS_KEY = 'customEditorAutoOpen'
10
11
  const e = window.translate
11
12
 
12
13
  export default function EditWithCustomEditor ({ loading, editWithCustom }) {
13
14
  const [editorCommand, setEditorCommand] = useState(
14
- () => safeGetItem(LS_KEY) || ''
15
+ () => safeGetItem(CUSTOM_EDITOR_COMMAND_LS_KEY) || ''
15
16
  )
17
+ const [autoOpen, setAutoOpen] = useState(
18
+ () => safeGetItem(CUSTOM_EDITOR_AUTO_OPEN_LS_KEY) === 'true'
19
+ )
20
+
21
+ const autoOpenLabel = e('autoOpen')
16
22
 
17
23
  function handleChange (ev) {
18
24
  const val = ev.target.value
19
25
  setEditorCommand(val)
20
- safeSetItem(LS_KEY, val)
26
+ safeSetItem(CUSTOM_EDITOR_COMMAND_LS_KEY, val)
27
+ }
28
+
29
+ function handleToggleAutoOpen () {
30
+ const next = !autoOpen
31
+ setAutoOpen(next)
32
+ safeSetItem(CUSTOM_EDITOR_AUTO_OPEN_LS_KEY, String(next))
21
33
  }
22
34
 
23
35
  function handleClick () {
@@ -45,6 +57,13 @@ export default function EditWithCustomEditor ({ loading, editWithCustom }) {
45
57
  onChange={handleChange}
46
58
  disabled={loading}
47
59
  />
60
+ <Button
61
+ type={autoOpen ? 'primary' : 'default'}
62
+ disabled={loading}
63
+ onClick={handleToggleAutoOpen}
64
+ >
65
+ {autoOpenLabel}: {autoOpen ? 'On' : 'Off'}
66
+ </Button>
48
67
  </Space.Compact>
49
68
  )
50
69
  }
@@ -4,9 +4,14 @@
4
4
 
5
5
  import { PureComponent } from 'react'
6
6
  import TextEditorForm from './text-editor-form'
7
+ import {
8
+ CUSTOM_EDITOR_AUTO_OPEN_LS_KEY,
9
+ CUSTOM_EDITOR_COMMAND_LS_KEY
10
+ } from './edit-with-custom-editor'
7
11
  import { Spin } from 'antd'
8
12
  import Modal from '../common/modal'
9
13
  import resolve from '../../common/resolve'
14
+ import { safeGetItem } from '../../common/safe-local-storage.js'
10
15
  import { refsStatic, refs } from '../common/ref'
11
16
 
12
17
  const e = window.translate
@@ -69,12 +74,28 @@ export default class TextEditor extends PureComponent {
69
74
  return
70
75
  }
71
76
  const text = await fileRef.fetchEditorText(p, type)
77
+ const editorCommand = this.getAutoOpenCustomEditorCommand()
72
78
  this.setStateProxy({
73
79
  text,
74
80
  loading: false
81
+ }, () => {
82
+ if (editorCommand) {
83
+ this.editWithCustom(editorCommand)
84
+ }
75
85
  })
76
86
  }
77
87
 
88
+ getAutoOpenCustomEditorCommand = () => {
89
+ if (window.et.isWebApp) {
90
+ return ''
91
+ }
92
+ const autoOpen = safeGetItem(CUSTOM_EDITOR_AUTO_OPEN_LS_KEY) === 'true'
93
+ if (!autoOpen) {
94
+ return ''
95
+ }
96
+ return safeGetItem(CUSTOM_EDITOR_COMMAND_LS_KEY).trim()
97
+ }
98
+
78
99
  doSubmit = () => {
79
100
  this.handleSubmit({
80
101
  text: this.state.text
@@ -3,17 +3,11 @@
3
3
  */
4
4
 
5
5
  import { memo } from 'react'
6
- import createName, { createTitleTag } from '../../common/create-title'
6
+ import { createTitleTag } from '../../common/create-title'
7
7
  import classnames from 'classnames'
8
8
  import highlight from '../common/highlight'
9
9
  import uid from '../../common/uid'
10
10
 
11
- function getItemLabel (item, isGroup) {
12
- return isGroup
13
- ? item?.title || ''
14
- : createName(item)
15
- }
16
-
17
11
  function areEqual (prevProps, nextProps) {
18
12
  const prevSelected = prevProps.selectedItemId === prevProps.item.id
19
13
  const nextSelected = nextProps.selectedItemId === nextProps.item.id
@@ -27,10 +21,10 @@ function areEqual (prevProps, nextProps) {
27
21
  prevSelected === nextSelected &&
28
22
  prevSearchSelected === nextSearchSelected &&
29
23
  prevProps.item.id === nextProps.item.id &&
30
- prevProps.item.level === nextProps.item.level &&
31
- prevProps.item.color === nextProps.item.color &&
32
- prevProps.item.description === nextProps.item.description &&
33
- getItemLabel(prevProps.item, prevProps.isGroup) === getItemLabel(nextProps.item, nextProps.isGroup)
24
+ prevProps.itemLevel === nextProps.itemLevel &&
25
+ prevProps.itemColor === nextProps.itemColor &&
26
+ prevProps.itemDescription === nextProps.itemDescription &&
27
+ prevProps.itemLabel === nextProps.itemLabel
34
28
  }
35
29
 
36
30
  function TreeListItem (props) {
@@ -87,7 +81,7 @@ function TreeListItem (props) {
87
81
  : null
88
82
  const title = isGroup
89
83
  ? item.title
90
- : createName(item)
84
+ : props.itemLabel
91
85
  const titleAll = title + (item.description ? ' - ' + item.description : '')
92
86
  const titleHighlight = isGroup
93
87
  ? item.title || 'no title'
@@ -2,6 +2,7 @@ import TreeExpander from './tree-expander'
2
2
  import TreeListItem from './tree-list-item'
3
3
  import TreeItemOp from './tree-item-op'
4
4
  import { treeLevelIndent } from './tree-list-layout'
5
+ import createName from '../../common/create-title'
5
6
 
6
7
  export default function TreeListRow (props) {
7
8
  const {
@@ -38,6 +39,10 @@ export default function TreeListRow (props) {
38
39
  item,
39
40
  isGroup,
40
41
  parentId,
42
+ itemLabel: isGroup ? (item?.title || '') : createName(item),
43
+ itemColor: item?.color,
44
+ itemDescription: item?.description,
45
+ itemLevel: item?.level,
41
46
  leftSidebarWidth,
42
47
  staticList,
43
48
  selectedItemId: activeItemId,
@@ -26,7 +26,9 @@ export function buildVisibleTreeRows ({
26
26
  const item = bookmarksMap.get(bookmarkId)
27
27
  const matched = Boolean(
28
28
  item &&
29
- (!lowerKeyword || createName(item).toLowerCase().includes(lowerKeyword))
29
+ (!lowerKeyword ||
30
+ createName(item).toLowerCase().includes(lowerKeyword) ||
31
+ (item.description || '').toLowerCase().includes(lowerKeyword))
30
32
  )
31
33
  bookmarkMatchCache.set(bookmarkId, matched)
32
34
  return matched
@@ -5,7 +5,7 @@
5
5
 
6
6
  import uid from '../common/uid'
7
7
  import { settingMap } from '../common/constants'
8
- import { refs } from '../components/common/ref'
8
+ import { refs, refsTabs } from '../components/common/ref'
9
9
  import deepCopy from 'json-deep-copy'
10
10
  import {
11
11
  getLocalFileInfo,
@@ -107,6 +107,9 @@ export default Store => {
107
107
  case 'get_terminal_output':
108
108
  result = store.mcpGetTerminalOutput(args)
109
109
  break
110
+ case 'wait_for_terminal_idle':
111
+ result = await store.mcpWaitForTerminalIdle(args)
112
+ break
110
113
 
111
114
  // SFTP operations
112
115
  case 'sftp_list':
@@ -331,15 +334,18 @@ export default Store => {
331
334
 
332
335
  Store.prototype.mcpListTabs = function () {
333
336
  const { store } = window
334
- return store.tabs.map(t => ({
335
- id: t.id,
336
- title: t.title,
337
- host: t.host,
338
- type: t.type || 'local',
339
- status: t.status,
340
- isTransporting: t.isTransporting,
341
- batch: t.batch
342
- }))
337
+ return store.tabs.map(t => {
338
+ return {
339
+ id: t.id,
340
+ title: t.title,
341
+ host: t.host,
342
+ type: t.type || 'local',
343
+ status: t.status,
344
+ isTransporting: t.isTransporting,
345
+ onData: refsTabs.get('tab-' + t.id)?.state.terminalOnData,
346
+ batch: t.batch
347
+ }
348
+ })
343
349
  }
344
350
 
345
351
  Store.prototype.mcpGetActiveTab = function () {
@@ -527,6 +533,73 @@ export default Store => {
527
533
  }
528
534
  }
529
535
 
536
+ Store.prototype.mcpWaitForTerminalIdle = async function (args) {
537
+ const { store } = window
538
+ const tabId = args.tabId || store.activeTabId
539
+ const timeout = Math.min(args.timeout || 30000, 120000)
540
+ const pollInterval = 500
541
+ const minWait = args.minWait !== undefined ? args.minWait : 1000
542
+ const lineCountToFetch = args.lines || 50
543
+
544
+ if (!tabId) {
545
+ throw new Error('No active terminal')
546
+ }
547
+
548
+ const start = Date.now()
549
+
550
+ // Brief initial wait so the command has time to start producing output
551
+ if (minWait > 0) {
552
+ await new Promise(resolve => setTimeout(resolve, minWait))
553
+ }
554
+
555
+ const collectOutput = () => {
556
+ const term = refs.get('term-' + tabId)
557
+ if (!term || !term.term) return { output: '', lineCount: 0 }
558
+ const buffer = term.term.buffer.active
559
+ if (!buffer) return { output: '', lineCount: 0 }
560
+ const cursorY = buffer.cursorY || 0
561
+ const baseY = buffer.baseY || 0
562
+ const totalLines = buffer.length || 0
563
+ const actualContentEnd = baseY + cursorY + 1
564
+ const startLine = Math.max(0, actualContentEnd - lineCountToFetch)
565
+ const endLine = Math.min(totalLines, actualContentEnd)
566
+ const lines = []
567
+ for (let i = startLine; i < endLine; i++) {
568
+ const line = buffer.getLine(i)
569
+ if (line) lines.push(line.translateToString(true))
570
+ }
571
+ return { output: lines.join('\n'), lineCount: lines.length }
572
+ }
573
+
574
+ // Poll until onData becomes false (4s idle debounce in tab.jsx)
575
+ while (Date.now() - start < timeout) {
576
+ const tabRef = refsTabs.get('tab-' + tabId)
577
+ const onData = tabRef?.state.terminalOnData
578
+ if (!onData) {
579
+ const { output, lineCount } = collectOutput()
580
+ return {
581
+ tabId,
582
+ elapsed: Date.now() - start,
583
+ timedOut: false,
584
+ output,
585
+ lineCount
586
+ }
587
+ }
588
+ await new Promise(resolve => setTimeout(resolve, pollInterval))
589
+ }
590
+
591
+ // Timeout reached — return whatever is currently in the buffer
592
+ const { output, lineCount } = collectOutput()
593
+ return {
594
+ tabId,
595
+ elapsed: Date.now() - start,
596
+ timedOut: true,
597
+ message: `Terminal still active after ${timeout}ms`,
598
+ output,
599
+ lineCount
600
+ }
601
+ }
602
+
530
603
  // ==================== Settings APIs ====================
531
604
 
532
605
  Store.prototype.mcpGetSettings = function () {
@@ -622,6 +622,20 @@ export default Store => {
622
622
  }
623
623
  }
624
624
 
625
+ Store.prototype.notifyTabPasswordPrompt = function (tabId) {
626
+ const tab = refsTabs.get('tab-' + tabId)
627
+ if (tab) {
628
+ tab.notifyPasswordPrompt()
629
+ }
630
+ }
631
+
632
+ Store.prototype.clearTabPasswordPrompt = function (tabId) {
633
+ const tab = refsTabs.get('tab-' + tabId)
634
+ if (tab) {
635
+ tab.clearPasswordPrompt()
636
+ }
637
+ }
638
+
625
639
  Store.prototype.remoteList = function (tabId) {
626
640
  const sftp = refs.get('sftp-' + tabId)
627
641
  if (sftp) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.9.5",
3
+ "version": "3.9.15",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",