@electerm/electerm-react 3.1.26 → 3.2.0

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 (29) hide show
  1. package/client/common/db.js +4 -2
  2. package/client/components/ai/ai-history.jsx +4 -4
  3. package/client/components/bookmark-form/common/bookmark-select.jsx +18 -2
  4. package/client/components/bookmark-form/common/connection-hopping-form.jsx +153 -0
  5. package/client/components/bookmark-form/common/connection-hopping.jsx +136 -129
  6. package/client/components/quick-commands/qm.styl +0 -2
  7. package/client/components/quick-commands/quick-commands-list-form.jsx +1 -1
  8. package/client/components/setting-panel/hotkey.jsx +9 -1
  9. package/client/components/setting-panel/list.jsx +0 -1
  10. package/client/components/setting-panel/list.styl +4 -0
  11. package/client/components/setting-panel/setting-modal.jsx +53 -47
  12. package/client/components/shortcuts/shortcut-editor.jsx +4 -2
  13. package/client/components/sidebar/history.jsx +1 -0
  14. package/client/components/terminal/attach-addon-custom.js +86 -0
  15. package/client/components/terminal/cmd-item.jsx +13 -3
  16. package/client/components/terminal/drop-file-modal.jsx +57 -0
  17. package/client/components/terminal/terminal-command-dropdown.jsx +91 -13
  18. package/client/components/terminal/terminal.jsx +103 -5
  19. package/client/components/terminal/terminal.styl +9 -0
  20. package/client/components/tree-list/tree-list-item.jsx +0 -1
  21. package/client/components/vnc/vnc-session.jsx +2 -0
  22. package/client/components/widgets/widget-control.jsx +3 -0
  23. package/client/components/widgets/widget-instance.jsx +26 -7
  24. package/client/css/includes/box.styl +3 -0
  25. package/client/store/init-state.js +2 -1
  26. package/client/store/load-data.js +3 -1
  27. package/client/store/mcp-handler.js +18 -0
  28. package/client/store/widgets.js +54 -0
  29. package/package.json +1 -1
@@ -4,18 +4,22 @@
4
4
 
5
5
  import { auto } from 'manate/react'
6
6
  import { pick } from 'lodash-es'
7
- import { Tabs } from 'antd'
7
+ import { Tabs, Spin } from 'antd'
8
+ import { lazy, Suspense } from 'react'
8
9
  import SettingModal from './setting-wrap'
9
10
  import {
10
11
  settingMap,
11
12
  modals
12
13
  } from '../../common/constants'
13
- import TabBookmarks from './tab-bookmarks'
14
- import TabQuickCommands from './tab-quick-commands'
15
- import TabSettings from './tab-settings'
16
- import TabThemes from './tab-themes'
17
- import TabProfiles from './tab-profiles'
18
- import TabWidgets from './tab-widgets'
14
+
15
+ const TabBookmarks = lazy(() => import('./tab-bookmarks'))
16
+ const TabQuickCommands = lazy(() => import('./tab-quick-commands'))
17
+ const TabSettings = lazy(() => import('./tab-settings'))
18
+ const TabThemes = lazy(() => import('./tab-themes'))
19
+ const TabProfiles = lazy(() => import('./tab-profiles'))
20
+ const TabWidgets = lazy(() => import('./tab-widgets'))
21
+
22
+ const Loading = () => <div style={{ padding: 20, textAlign: 'center' }}><Spin /></div>
19
23
 
20
24
  const e = window.translate
21
25
 
@@ -115,46 +119,48 @@ export default auto(function SettingModalWrap (props) {
115
119
  <Tabs
116
120
  {...tabsProps}
117
121
  />
118
- <TabQuickCommands
119
- listProps={props0}
120
- settingItem={settingItem}
121
- formProps={formProps}
122
- store={store}
123
- settingTab={settingTab}
124
- />
125
- <TabBookmarks
126
- treeProps={treeProps}
127
- settingItem={settingItem}
128
- formProps={formProps}
129
- settingTab={settingTab}
130
- />
131
- <TabSettings
132
- listProps={props0}
133
- settingItem={settingItem}
134
- settingTab={settingTab}
135
- store={store}
136
- />
137
- <TabThemes
138
- listProps={props0}
139
- settingItem={settingItem}
140
- formProps={formProps}
141
- store={store}
142
- settingTab={settingTab}
143
- />
144
- <TabProfiles
145
- listProps={props0}
146
- settingItem={settingItem}
147
- formProps={formProps}
148
- store={store}
149
- settingTab={settingTab}
150
- />
151
- <TabWidgets
152
- listProps={props0}
153
- settingItem={settingItem}
154
- formProps={formProps}
155
- store={store}
156
- settingTab={settingTab}
157
- />
122
+ <Suspense fallback={<Loading />}>
123
+ <TabQuickCommands
124
+ listProps={props0}
125
+ settingItem={settingItem}
126
+ formProps={formProps}
127
+ store={store}
128
+ settingTab={settingTab}
129
+ />
130
+ <TabBookmarks
131
+ treeProps={treeProps}
132
+ settingItem={settingItem}
133
+ formProps={formProps}
134
+ settingTab={settingTab}
135
+ />
136
+ <TabSettings
137
+ listProps={props0}
138
+ settingItem={settingItem}
139
+ settingTab={settingTab}
140
+ store={store}
141
+ />
142
+ <TabThemes
143
+ listProps={props0}
144
+ settingItem={settingItem}
145
+ formProps={formProps}
146
+ store={store}
147
+ settingTab={settingTab}
148
+ />
149
+ <TabProfiles
150
+ listProps={props0}
151
+ settingItem={settingItem}
152
+ formProps={formProps}
153
+ store={store}
154
+ settingTab={settingTab}
155
+ />
156
+ <TabWidgets
157
+ listProps={props0}
158
+ settingItem={settingItem}
159
+ formProps={formProps}
160
+ store={store}
161
+ settingTab={settingTab}
162
+ />
163
+ </Suspense>
158
164
  </>
159
165
  )
160
166
  }
@@ -135,11 +135,13 @@ export default class ShortcutEdit extends PureComponent {
135
135
  }
136
136
 
137
137
  renderClear () {
138
- if (this.props.renderClear && this.props.data.shortcut) {
138
+ const { renderClear, handleClear, data } = this.props
139
+ const hasShortcut = data && data.shortcut
140
+ if (renderClear && hasShortcut && handleClear) {
139
141
  return (
140
142
  <CloseOutlined
141
143
  className='pointer mg1l'
142
- onClick={this.props.handleClear}
144
+ onClick={handleClear}
143
145
  />
144
146
  )
145
147
  }
@@ -8,6 +8,7 @@ import { Switch } from 'antd'
8
8
  import { UnorderedListOutlined } from '@ant-design/icons'
9
9
  import HistoryItem from './history-item'
10
10
  import { getItemJSON, setItemJSON } from '../../common/safe-local-storage.js'
11
+ import '../setting-panel/list.styl'
11
12
 
12
13
  const SORT_BY_FREQ_KEY = 'electerm-history-sort-by-frequency'
13
14
 
@@ -19,6 +19,10 @@ export default class AttachAddonCustom {
19
19
  this._lastInputTime = Date.now()
20
20
  this._keepaliveTimer = null
21
21
  this._keepaliveInterval = 3000
22
+ this._lastOutputLine = ''
23
+ this._passwordPromptDetected = false
24
+ this._pendingEchoCheck = null
25
+ this._echoCheckTimer = null
22
26
  }
23
27
 
24
28
  _initBase = async () => {
@@ -101,6 +105,52 @@ export default class AttachAddonCustom {
101
105
  this.writeToTerminal(ev.data)
102
106
  }
103
107
 
108
+ static passwordPromptPatterns = [
109
+ /password\s*[:\]>]\s*$/i,
110
+ /\[sudo\]\s*password\s+for\s+\S+\s*:\s*$/i,
111
+ /enter\s+passphrase/i,
112
+ /enter\s+password/i,
113
+ /密码[::]\s*$/,
114
+ /パスワード[::]\s*$/,
115
+ /mot de passe\s*[:\]]\s*$/i,
116
+ /passwort[:\]]\s*$/i,
117
+ /contraseña[:\]]\s*$/i
118
+ ]
119
+
120
+ _checkPasswordPrompt = (str) => {
121
+ // Extract last non-empty line from the output
122
+ const lines = str.split(/\r?\n|\r/)
123
+ for (let i = lines.length - 1; i >= 0; i--) {
124
+ const line = lines[i].trim()
125
+ if (line) {
126
+ this._lastOutputLine = line
127
+ break
128
+ }
129
+ }
130
+ return AttachAddonCustom.passwordPromptPatterns.some(
131
+ p => p.test(this._lastOutputLine)
132
+ )
133
+ }
134
+
135
+ _onEchoCheckTimeout = () => {
136
+ // No echo received within timeout → confirms password mode
137
+ this._pendingEchoCheck = null
138
+ }
139
+
140
+ _handleEchoDetection = (str) => {
141
+ if (this._pendingEchoCheck) {
142
+ // Server sent data back while we were waiting → echo is ON → not password
143
+ if (str.includes(this._pendingEchoCheck.char)) {
144
+ this._passwordPromptDetected = false
145
+ clearTimeout(this._echoCheckTimer)
146
+ this._pendingEchoCheck = null
147
+ this._echoCheckTimer = null
148
+ // Cancel the password dropdown if it was shown
149
+ this.term?.parent?.onPasswordPromptCancelled?.()
150
+ }
151
+ }
152
+ }
153
+
104
154
  checkForShellIntegration = (str) => {
105
155
  const ESC = String.fromCharCode(27)
106
156
  return str.includes(ESC + ']633;')
@@ -152,6 +202,26 @@ export default class AttachAddonCustom {
152
202
  return
153
203
  }
154
204
 
205
+ // Password prompt detection on output
206
+ let str = data
207
+ if (typeof data !== 'string') {
208
+ try {
209
+ str = this.decoder.decode(
210
+ data instanceof ArrayBuffer ? data : new Uint8Array(data)
211
+ )
212
+ } catch (e) {
213
+ str = ''
214
+ }
215
+ }
216
+ this._handleEchoDetection(str)
217
+ if (this._checkPasswordPrompt(str) && !this._passwordPromptDetected) {
218
+ this._passwordPromptDetected = true
219
+ // Show password dropdown immediately after terminal renders the prompt
220
+ setTimeout(() => {
221
+ this.term?.parent?.onPasswordPromptDetected?.()
222
+ }, 100)
223
+ }
224
+
155
225
  if (typeof data === 'string') {
156
226
  return term.write(data)
157
227
  }
@@ -171,6 +241,20 @@ export default class AttachAddonCustom {
171
241
 
172
242
  sendToServer = (data) => {
173
243
  this._lastInputTime = Date.now()
244
+ // Start echo detection when password prompt is suspected
245
+ if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n') {
246
+ this._pendingEchoCheck = { char: data, time: Date.now() }
247
+ clearTimeout(this._echoCheckTimer)
248
+ this._echoCheckTimer = setTimeout(this._onEchoCheckTimeout, 200)
249
+ }
250
+ // Reset password state on Enter
251
+ if (data === '\r' || data === '\n') {
252
+ this._passwordPromptDetected = false
253
+ this._lastOutputLine = ''
254
+ this._pendingEchoCheck = null
255
+ clearTimeout(this._echoCheckTimer)
256
+ this._echoCheckTimer = null
257
+ }
174
258
  this._sendData(data)
175
259
  }
176
260
 
@@ -228,6 +312,8 @@ export default class AttachAddonCustom {
228
312
 
229
313
  dispose = () => {
230
314
  this._stopKeepalive()
315
+ clearTimeout(this._echoCheckTimer)
316
+ this._echoCheckTimer = null
231
317
  this.term = null
232
318
  this._disposables.forEach(d => d.dispose())
233
319
  this._disposables.length = 0
@@ -11,11 +11,21 @@ const SuggestionItem = ({ item, onSelect, onDelete }) => {
11
11
  onDelete(item)
12
12
  }
13
13
 
14
+ const isPassword = item.type === 'PW'
15
+ const displayText = isPassword
16
+ ? '••••••••'
17
+ : item.command
18
+
14
19
  return (
15
- <div className='suggestion-item'>
16
- <span className='suggestion-command' onClick={handleClick}>
17
- {item.command}
20
+ <div className='suggestion-item' onClick={handleClick}>
21
+ <span className='suggestion-command'>
22
+ {displayText}
18
23
  </span>
24
+ {item.hint && (
25
+ <span className='suggestion-hint'>
26
+ {item.hint}
27
+ </span>
28
+ )}
19
29
  <span className='suggestion-type'>
20
30
  {item.type}
21
31
  </span>
@@ -0,0 +1,57 @@
1
+ import { Component } from 'react'
2
+ import Modal from '../common/modal'
3
+
4
+ const e = window.translate
5
+
6
+ export class DropFileModal extends Component {
7
+ render () {
8
+ const {
9
+ visible,
10
+ files,
11
+ onSelect,
12
+ onCancel
13
+ } = this.props
14
+
15
+ if (!visible) {
16
+ return null
17
+ }
18
+
19
+ return (
20
+ <Modal
21
+ title='?'
22
+ open={visible}
23
+ onCancel={onCancel}
24
+ footer={
25
+ <div className='custom-modal-footer-buttons'>
26
+ <button
27
+ type='button'
28
+ className='custom-modal-ok-btn'
29
+ onClick={() => onSelect('trzUpload')}
30
+ >
31
+ trz
32
+ </button>
33
+ <button
34
+ type='button'
35
+ className='custom-modal-cancel-btn'
36
+ onClick={() => onSelect('rzUpload')}
37
+ >
38
+ rz
39
+ </button>
40
+ <button
41
+ type='button'
42
+ className='custom-modal-cancel-btn'
43
+ onClick={() => onSelect('inputPath')}
44
+ >
45
+ {e('inputOnly')}
46
+ </button>
47
+ </div>
48
+ }
49
+ width={400}
50
+ >
51
+ <p>{files?.map(f => f.path).join(', ')}</p>
52
+ </Modal>
53
+ )
54
+ }
55
+ }
56
+
57
+ export default DropFileModal
@@ -16,7 +16,8 @@ export default class TerminalCmdSuggestions extends Component {
16
16
  aiSuggestions: [],
17
17
  cmdIsDescription: false,
18
18
  reverse: false,
19
- cmd: ''
19
+ cmd: '',
20
+ passwordMode: false
20
21
  }
21
22
 
22
23
  componentDidMount () {
@@ -95,6 +96,9 @@ export default class TerminalCmdSuggestions extends Component {
95
96
  }
96
97
 
97
98
  openSuggestions = (cursorPosition, cmd) => {
99
+ if (this.state.passwordMode) {
100
+ return
101
+ }
98
102
  if (!this.state.showSuggestions) {
99
103
  document.addEventListener('click', this.handleClickOutside)
100
104
  document.addEventListener('keydown', this.handleKeyDown)
@@ -128,7 +132,45 @@ export default class TerminalCmdSuggestions extends Component {
128
132
  showSuggestions: true,
129
133
  cursorPosition: position,
130
134
  cmd,
131
- reverse
135
+ reverse,
136
+ passwordMode: false
137
+ })
138
+ }
139
+
140
+ openPasswordSuggestions = (cursorPosition) => {
141
+ if (!this.state.showSuggestions) {
142
+ document.addEventListener('click', this.handleClickOutside)
143
+ document.addEventListener('keydown', this.handleKeyDown)
144
+ }
145
+
146
+ const {
147
+ left,
148
+ top,
149
+ cellHeight
150
+ } = cursorPosition
151
+ const w = window.innerWidth
152
+ const h = window.innerHeight
153
+
154
+ const position = {}
155
+ const reverse = top > h / 2
156
+
157
+ if (left > w / 2) {
158
+ position.right = w - left
159
+ } else {
160
+ position.left = left
161
+ }
162
+
163
+ if (reverse) {
164
+ position.bottom = h - top + cellHeight * 1.5
165
+ } else {
166
+ position.top = top + cellHeight
167
+ }
168
+ this.setState({
169
+ showSuggestions: true,
170
+ cursorPosition: position,
171
+ cmd: '',
172
+ reverse,
173
+ passwordMode: true
132
174
  })
133
175
  }
134
176
 
@@ -146,7 +188,8 @@ export default class TerminalCmdSuggestions extends Component {
146
188
  }
147
189
  this.setState({
148
190
  showSuggestions: false,
149
- aiSuggestions: []
191
+ aiSuggestions: [],
192
+ passwordMode: false
150
193
  })
151
194
  }
152
195
 
@@ -172,18 +215,33 @@ export default class TerminalCmdSuggestions extends Component {
172
215
  const terminal = refs.get('term-' + activeTabId)
173
216
  if (!terminal) {
174
217
  console.log('No active terminal found')
218
+ this.closeSuggestions()
219
+ return
220
+ }
221
+
222
+ if (item.type === 'PW') {
223
+ try {
224
+ // Send password + Enter directly, no backspace needed
225
+ terminal.attachAddon._sendData(item.command + '\r')
226
+ terminal.attachAddon._passwordPromptDetected = false
227
+ terminal.attachAddon._lastOutputLine = ''
228
+ } catch (e) {
229
+ console.error('Failed to send password:', e)
230
+ }
231
+ terminal.term.focus()
232
+ this.closeSuggestions()
175
233
  return
176
234
  }
177
235
 
178
- // const titleElement = domEvent.target.closest('.ant-menu-title-content')
179
- // const command = titleElement?.firstChild?.textContent
180
236
  const { command } = item
181
- const { cmd } = this.state
237
+ // Read current input from buffer directly to avoid stale state
238
+ // (onData fires before echo, so this.state.cmd may lag behind)
239
+ const currentInput = terminal.getCurrentInput() || ''
182
240
  let txt = ''
183
- if (cmd && command.startsWith(cmd)) {
184
- txt = command.slice(cmd.length)
241
+ if (currentInput && command.startsWith(currentInput)) {
242
+ txt = command.slice(currentInput.length)
185
243
  } else {
186
- const pre = '\b'.repeat(cmd.length)
244
+ const pre = '\b'.repeat(currentInput.length)
187
245
  txt = pre + command
188
246
  }
189
247
  terminal.attachAddon._sendData(txt)
@@ -209,6 +267,24 @@ export default class TerminalCmdSuggestions extends Component {
209
267
  })
210
268
  }
211
269
 
270
+ getPasswordSuggestions = () => {
271
+ const bookmarks = window.store.bookmarks || []
272
+ const seen = new Set()
273
+ const res = []
274
+ for (const b of bookmarks) {
275
+ if (b.password && !seen.has(b.password)) {
276
+ seen.add(b.password)
277
+ res.push({
278
+ id: uid(),
279
+ command: b.password,
280
+ type: 'PW',
281
+ hint: [b.username, b.host].filter(Boolean).join('@')
282
+ })
283
+ }
284
+ }
285
+ return this.state.reverse ? res.reverse() : res
286
+ }
287
+
212
288
  getSuggestions = () => {
213
289
  const uniqueCommands = new Set()
214
290
  const {
@@ -274,17 +350,19 @@ export default class TerminalCmdSuggestions extends Component {
274
350
  }
275
351
 
276
352
  render () {
277
- const { showSuggestions, cursorPosition, reverse } = this.state
353
+ const { showSuggestions, cursorPosition, reverse, passwordMode } = this.state
278
354
  if (!showSuggestions) {
279
355
  return null
280
356
  }
281
- const suggestions = this.getSuggestions()
357
+ const suggestions = passwordMode
358
+ ? this.getPasswordSuggestions()
359
+ : this.getSuggestions()
282
360
  const cls = classnames('terminal-suggestions-wrap', {
283
361
  reverse
284
362
  })
285
363
  return (
286
364
  <div className={cls} style={cursorPosition}>
287
- {this.renderSticky('top')}
365
+ {!passwordMode && this.renderSticky('top')}
288
366
  <div className='terminal-suggestions-list'>
289
367
  {
290
368
  suggestions.map(item => {
@@ -299,7 +377,7 @@ export default class TerminalCmdSuggestions extends Component {
299
377
  })
300
378
  }
301
379
  </div>
302
- {this.renderSticky('bottom')}
380
+ {!passwordMode && this.renderSticky('bottom')}
303
381
  </div>
304
382
  )
305
383
  }