@electerm/electerm-react 2.3.191 → 2.4.16

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 (78) hide show
  1. package/client/common/clipboard.js +1 -1
  2. package/client/common/constants.js +2 -2
  3. package/client/common/download.jsx +3 -2
  4. package/client/common/error-handler.jsx +5 -9
  5. package/client/common/fetch-from-server.js +1 -1
  6. package/client/common/fetch.jsx +5 -5
  7. package/client/common/icon-helpers.jsx +16 -0
  8. package/client/common/parse-json-safe.js +1 -1
  9. package/client/common/pre.js +0 -7
  10. package/client/common/sftp.js +1 -1
  11. package/client/common/terminal-theme.js +1 -1
  12. package/client/common/transfer.js +2 -2
  13. package/client/common/upgrade.js +2 -2
  14. package/client/components/ai/ai-chat.jsx +10 -1
  15. package/client/components/auth/login.jsx +1 -1
  16. package/client/components/bg/css-overwrite.jsx +1 -1
  17. package/client/components/bookmark-form/common/fields.jsx +3 -0
  18. package/client/components/bookmark-form/common/ssh-agent.jsx +33 -0
  19. package/client/components/bookmark-form/config/common-fields.js +2 -4
  20. package/client/components/bookmark-form/config/serial.js +1 -1
  21. package/client/components/bookmark-form/config/ssh.js +1 -0
  22. package/client/components/bookmark-form/form-renderer.jsx +3 -2
  23. package/client/components/common/input-auto-focus.jsx +1 -1
  24. package/client/components/common/message.jsx +131 -0
  25. package/client/components/common/message.styl +58 -0
  26. package/client/components/common/modal.jsx +176 -0
  27. package/client/components/common/modal.styl +22 -0
  28. package/client/components/common/notification-with-details.jsx +1 -1
  29. package/client/components/common/notification.jsx +94 -0
  30. package/client/components/common/notification.styl +51 -0
  31. package/client/components/main/connection-hopping-warnning.jsx +1 -3
  32. package/client/components/main/error-wrapper.jsx +3 -2
  33. package/client/components/main/main.jsx +4 -11
  34. package/client/components/main/upgrade.jsx +6 -4
  35. package/client/components/profile/profile-form-elem.jsx +1 -1
  36. package/client/components/quick-commands/quick-commands-box.jsx +5 -2
  37. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -1
  38. package/client/components/rdp/rdp-session.jsx +2 -2
  39. package/client/components/session/session.jsx +4 -9
  40. package/client/components/setting-panel/deep-link-control.jsx +4 -3
  41. package/client/components/setting-panel/keyword-input.jsx +60 -0
  42. package/client/components/setting-panel/keywords-form.jsx +2 -7
  43. package/client/components/setting-panel/setting-common.jsx +1 -1
  44. package/client/components/setting-panel/setting-terminal.jsx +1 -1
  45. package/client/components/setting-panel/tab-settings.jsx +1 -1
  46. package/client/components/setting-sync/setting-sync-form.jsx +53 -3
  47. package/client/components/setting-sync/setting-sync.jsx +2 -1
  48. package/client/components/sftp/owner-list.js +6 -6
  49. package/client/components/sftp/sftp-entry.jsx +6 -4
  50. package/client/components/shortcuts/shortcut-editor.jsx +2 -2
  51. package/client/components/ssh-config/ssh-config-load-notify.jsx +3 -2
  52. package/client/components/tabs/tab.jsx +1 -1
  53. package/client/components/tabs/workspace-save-modal.jsx +2 -1
  54. package/client/components/terminal/attach-addon-custom.js +142 -26
  55. package/client/components/terminal/command-tracker-addon.js +164 -53
  56. package/client/components/terminal/highlight-addon.js +84 -43
  57. package/client/components/terminal/shell.js +138 -0
  58. package/client/components/terminal/term-search.styl +1 -0
  59. package/client/components/terminal/terminal-command-dropdown.jsx +3 -0
  60. package/client/components/terminal/terminal.jsx +166 -104
  61. package/client/components/theme/theme-form.jsx +2 -1
  62. package/client/components/tree-list/bookmark-transport.jsx +27 -5
  63. package/client/components/vnc/vnc-session.jsx +1 -1
  64. package/client/components/widgets/widget-notification-with-details.jsx +1 -1
  65. package/client/store/common.js +5 -2
  66. package/client/store/db-upgrade.js +1 -1
  67. package/client/store/init-state.js +2 -1
  68. package/client/store/load-data.js +2 -2
  69. package/client/store/mcp-handler.js +9 -56
  70. package/client/store/setting.js +1 -3
  71. package/client/store/store.js +2 -1
  72. package/client/store/sync.js +14 -8
  73. package/client/store/system-menu.js +2 -1
  74. package/client/store/tab.js +1 -1
  75. package/client/store/widgets.js +1 -3
  76. package/package.json +1 -1
  77. package/client/common/track.js +0 -7
  78. package/client/components/batch-op/batch-op-entry.jsx +0 -13
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  Form,
3
- Input,
4
3
  Select,
5
4
  Space,
6
5
  Button
@@ -11,6 +10,7 @@ import {
11
10
  PlusOutlined
12
11
  } from '@ant-design/icons'
13
12
  import { useEffect } from 'react'
13
+ import KeywordInput from './keyword-input'
14
14
 
15
15
  const FormItem = Form.Item
16
16
  const FormList = Form.List
@@ -26,10 +26,6 @@ export default function KeywordForm (props) {
26
26
  formChild.submit()
27
27
  }
28
28
 
29
- function handleChange (e) {
30
- formChild.submit()
31
- }
32
-
33
29
  function handleFinish (data) {
34
30
  props.submit(data)
35
31
  }
@@ -59,9 +55,8 @@ export default function KeywordForm (props) {
59
55
  name={[field.name, 'keyword']}
60
56
  rules={[{ validator: checker }]}
61
57
  >
62
- <Input
58
+ <KeywordInput
63
59
  addonBefore={renderBefore(field.name)}
64
- onChange={handleChange}
65
60
  />
66
61
  </FormItem>
67
62
  </FormItem>
@@ -5,8 +5,8 @@ import {
5
5
  SunOutlined,
6
6
  MoonOutlined
7
7
  } from '@ant-design/icons'
8
+ import message from '../common/message'
8
9
  import {
9
- message,
10
10
  Select,
11
11
  Switch,
12
12
  Input,
@@ -3,8 +3,8 @@ import {
3
3
  CodeOutlined,
4
4
  LoadingOutlined
5
5
  } from '@ant-design/icons'
6
+ import message from '../common/message'
6
7
  import {
7
- message,
8
8
  Select,
9
9
  Switch,
10
10
  Input,
@@ -1,5 +1,5 @@
1
1
  import { auto } from 'manate/react'
2
- import { message } from 'antd'
2
+ import message from '../common/message'
3
3
  import SettingCommon from './setting-common'
4
4
  import SettingTerminal from './setting-terminal'
5
5
  import SettingCol from './col'
@@ -7,7 +7,8 @@
7
7
  */
8
8
  import { useDelta, useConditionalEffect } from 'react-delta-hooks'
9
9
  import { ArrowDownOutlined, ArrowUpOutlined, SaveOutlined, ClearOutlined } from '@ant-design/icons'
10
- import { Button, Input, notification, Form } from 'antd'
10
+ import { Button, Input, Form, Alert } from 'antd'
11
+ import { notification } from '../common/notification'
11
12
  import Link from '../common/external-link'
12
13
  import dayjs from 'dayjs'
13
14
  import eq from 'fast-deep-equal'
@@ -15,6 +16,7 @@ import { syncTokenCreateUrls, syncTypes } from '../../common/constants'
15
16
  import HelpIcon from '../common/help-icon'
16
17
  import ServerDataStatus from './server-data-status'
17
18
  import Password from '../common/password'
19
+ import { isError } from 'lodash-es'
18
20
 
19
21
  const FormItem = Form.Item
20
22
  const e = window.translate
@@ -58,11 +60,17 @@ export default function SyncForm (props) {
58
60
  up[syncType + 'ApiUrl'] = 'https://electerm-cloud.html5beta.com/api/sync'
59
61
  // up[syncType + 'ApiUrl'] = 'http://127.0.0.1:5678/api/sync'
60
62
  }
63
+ if (res.proxy) {
64
+ up[syncType + 'Proxy'] = res.proxy
65
+ } else {
66
+ up[syncType + 'Proxy'] = ''
67
+ }
61
68
  window.store.updateSyncSetting(up)
62
69
  const test = await window.store.testSyncToken(syncType, res.gistId)
63
- if (!test) {
70
+ if (isError(test)) {
64
71
  return notification.error({
65
- title: 'token invalid'
72
+ message: test.message || 'Request failed',
73
+ description: test.stack || 'Request failed'
66
74
  })
67
75
  }
68
76
  if (!res.gistId && syncType !== syncTypes.custom && syncType !== syncTypes.cloud) {
@@ -136,6 +144,27 @@ export default function SyncForm (props) {
136
144
  function createId (name) {
137
145
  return 'sync-input-' + name + '-' + syncType
138
146
  }
147
+ function renderWarning () {
148
+ if (syncType === syncTypes.gitee) {
149
+ return (
150
+ <Alert
151
+ message={
152
+ <span>
153
+ Gitee data sync is not recommended. For more information, please refer to the
154
+ <Link to='https://github.com/electerm/electerm/wiki/gitee-data-sync-warning' className='mg1l'>
155
+ wiki
156
+ </Link>
157
+ .
158
+ </span>
159
+ }
160
+ type='warning'
161
+ showIcon
162
+ className='mg1b'
163
+ />
164
+ )
165
+ }
166
+ return null
167
+ }
139
168
  function createUrlItem () {
140
169
  if (syncType === syncTypes.cloud) {
141
170
  return (
@@ -215,6 +244,23 @@ export default function SyncForm (props) {
215
244
  </FormItem>
216
245
  )
217
246
  }
247
+ function createProxyItem () {
248
+ return (
249
+ <FormItem
250
+ label='Proxy'
251
+ name='proxy'
252
+ normalize={trim}
253
+ rules={[{
254
+ max: 200, message: '200 chars max'
255
+ }]}
256
+ >
257
+ <Input
258
+ placeholder='socks5://127.0.0.1:1080'
259
+ id={createId('proxy')}
260
+ />
261
+ </FormItem>
262
+ )
263
+ }
218
264
  const sprops = {
219
265
  type: syncType,
220
266
  status: props.serverStatus
@@ -228,6 +274,7 @@ export default function SyncForm (props) {
228
274
  layout='vertical'
229
275
  initialValues={props.formData}
230
276
  >
277
+ {renderWarning()}
231
278
  {createUrlItem()}
232
279
  <FormItem
233
280
  label={tokenLabel}
@@ -251,6 +298,9 @@ export default function SyncForm (props) {
251
298
  {
252
299
  createPasswordItem()
253
300
  }
301
+ {
302
+ createProxyItem()
303
+ }
254
304
  <FormItem>
255
305
  <p>
256
306
  <Button
@@ -43,7 +43,8 @@ export default auto(function SyncSettingEntry (props) {
43
43
  url: syncSetting[type + 'Url'],
44
44
  apiUrl: syncSetting[type + 'ApiUrl'],
45
45
  lastSyncTime: syncSetting[type + 'LastSyncTime'],
46
- syncPassword: syncSetting[type + 'SyncPassword']
46
+ syncPassword: syncSetting[type + 'SyncPassword'],
47
+ proxy: syncSetting[type + 'Proxy']
47
48
  }
48
49
  return (
49
50
  <SyncForm
@@ -29,7 +29,7 @@ const linuxListGroup = 'cat /etc/group'
29
29
 
30
30
  export async function remoteListUsers (pid) {
31
31
  const users = await runCmd(pid, linuxListUser)
32
- .catch(log.error)
32
+ .catch(console.error)
33
33
  if (users) {
34
34
  return parseNames(users)
35
35
  }
@@ -38,7 +38,7 @@ export async function remoteListUsers (pid) {
38
38
 
39
39
  export async function remoteListGroups (pid) {
40
40
  const groups = await runCmd(pid, linuxListGroup)
41
- .catch(log.error)
41
+ .catch(console.error)
42
42
  if (groups) {
43
43
  return parseNames(groups)
44
44
  }
@@ -50,7 +50,7 @@ export async function localListUsers () {
50
50
  return {}
51
51
  } else if (isMac) {
52
52
  const g = await fs.run('dscl . -list /Users UniqueID')
53
- .catch(log.error)
53
+ .catch(console.error)
54
54
  return g
55
55
  ? g.split('\n')
56
56
  .reduce((p, s) => {
@@ -65,7 +65,7 @@ export async function localListUsers () {
65
65
  }, {})
66
66
  : {}
67
67
  } else {
68
- const g = await fs.run(linuxListUser).catch(log.error)
68
+ const g = await fs.run(linuxListUser).catch(console.error)
69
69
  return g
70
70
  ? parseNames(g)
71
71
  : {}
@@ -77,7 +77,7 @@ export async function localListGroups () {
77
77
  return {}
78
78
  } else if (isMac) {
79
79
  const g = await fs.run('dscl . list /Groups PrimaryGroupID')
80
- .catch(log.error)
80
+ .catch(console.error)
81
81
  return g
82
82
  ? g.split('\n')
83
83
  .reduce((p, s) => {
@@ -89,7 +89,7 @@ export async function localListGroups () {
89
89
  }, {})
90
90
  : {}
91
91
  } else {
92
- const g = await fs.run(linuxListGroup).catch(log.error)
92
+ const g = await fs.run(linuxListGroup).catch(console.error)
93
93
  return g
94
94
  ? parseNames(g)
95
95
  : {}
@@ -2,7 +2,9 @@ import { Component } from 'react'
2
2
  import { refs } from '../common/ref'
3
3
  import generate from '../../common/uid'
4
4
  import runIdle from '../../common/run-idle'
5
- import { Spin, Modal, notification } from 'antd'
5
+ import { Spin } from 'antd'
6
+ import { notification } from '../common/notification'
7
+ import Modal from '../common/modal'
6
8
  import clone from '../../common/to-simple-obj'
7
9
  import { isEqual, last, isNumber, some, isArray, pick, uniq, debounce } from 'lodash-es'
8
10
  import FileSection from './file-item'
@@ -846,8 +848,8 @@ export default class Sftp extends Component {
846
848
  sftp,
847
849
  realpath
848
850
  ).catch(e => {
849
- log.debug('seems a bad symbolic link')
850
- log.debug(e)
851
+ console.debug('seems a bad symbolic link')
852
+ console.debug(e)
851
853
  return null
852
854
  })
853
855
  if (!realFileInfo) {
@@ -981,7 +983,7 @@ export default class Sftp extends Component {
981
983
  const np = this.parsePath(type, this.state[nt])
982
984
  if (!isValidPath(np)) {
983
985
  return notification.warning({
984
- title: 'path not valid'
986
+ message: 'path not valid'
985
987
  })
986
988
  }
987
989
  this.setState({
@@ -1,9 +1,9 @@
1
1
  import { PureComponent } from 'react'
2
2
  import {
3
3
  Button,
4
- Input,
5
- message
4
+ Input
6
5
  } from 'antd'
6
+ import message from '../common/message'
7
7
  import {
8
8
  EditFilled,
9
9
  CheckOutlined,
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect } from 'react'
2
- import { notification, Button } from 'antd'
2
+ import { Button } from 'antd'
3
+ import { notification } from '../common/notification'
3
4
  import * as ls from '../../common/safe-local-storage'
4
5
  import {
5
6
  sshConfigKey,
@@ -20,7 +21,7 @@ function handleIgnore () {
20
21
 
21
22
  function showNotification () {
22
23
  notification.info({
23
- title: e('loadSshConfigs'),
24
+ message: e('loadSshConfigs'),
24
25
  duration: 0,
25
26
  placement: 'bottom',
26
27
  key: 'sshConfigNotify',
@@ -12,9 +12,9 @@ import {
12
12
  } from '@ant-design/icons'
13
13
  import {
14
14
  Tooltip,
15
- message,
16
15
  Dropdown
17
16
  } from 'antd'
17
+ import message from '../common/message'
18
18
  import classnames from 'classnames'
19
19
  import { pick } from 'lodash-es'
20
20
  import Input from '../common/input-auto-focus'
@@ -5,7 +5,8 @@
5
5
  import React, { useState } from 'react'
6
6
  import { auto } from 'manate/react'
7
7
  import Modal from '../common/modal'
8
- import { Input, Select, Button, Space, message, Radio } from 'antd'
8
+ import { Input, Select, Button, Space, Radio } from 'antd'
9
+ import message from '../common/message'
9
10
  import { SaveOutlined, EditOutlined } from '@ant-design/icons'
10
11
 
11
12
  const e = window.translate
@@ -2,7 +2,6 @@
2
2
  * customize AttachAddon
3
3
  */
4
4
  import { AttachAddon } from '@xterm/addon-attach'
5
- import regEscape from 'escape-string-regexp'
6
5
 
7
6
  export default class AttachAddonCustom extends AttachAddon {
8
7
  constructor (term, socket, isWindowsShell) {
@@ -10,6 +9,80 @@ export default class AttachAddonCustom extends AttachAddon {
10
9
  this.term = term
11
10
  this.socket = socket
12
11
  this.isWindowsShell = isWindowsShell
12
+ // Output suppression state for shell integration injection
13
+ this.outputSuppressed = false
14
+ this.suppressedData = []
15
+ this.suppressTimeout = null
16
+ this.onSuppressionEndCallback = null
17
+ // Track if we've received initial data from the terminal
18
+ this.hasReceivedInitialData = false
19
+ this.onInitialDataCallback = null
20
+ }
21
+
22
+ /**
23
+ * Set callback for when initial data is received
24
+ * @param {Function} callback - Called when first data arrives
25
+ */
26
+ onInitialData = (callback) => {
27
+ if (this.hasReceivedInitialData) {
28
+ // Already received, call immediately
29
+ callback()
30
+ } else {
31
+ this.onInitialDataCallback = callback
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Start suppressing output - used during shell integration injection
37
+ * @param {number} timeout - Max time to suppress in ms (safety fallback)
38
+ * @param {Function} onEnd - Callback when suppression ends
39
+ */
40
+ startOutputSuppression = (timeout = 3000, onEnd = null) => {
41
+ this.outputSuppressed = true
42
+ this.suppressedData = []
43
+ this.onSuppressionEndCallback = onEnd
44
+ // Safety timeout to ensure we always resume
45
+ this.suppressTimeout = setTimeout(() => {
46
+ console.warn('[AttachAddon] Output suppression timeout reached, resuming')
47
+ this.stopOutputSuppression(false)
48
+ }, timeout)
49
+ }
50
+
51
+ /**
52
+ * Stop suppressing output and optionally discard buffered data
53
+ * @param {boolean} discard - If true, discard buffered data; if false, write it to terminal
54
+ */
55
+ stopOutputSuppression = (discard = true) => {
56
+ if (this.suppressTimeout) {
57
+ clearTimeout(this.suppressTimeout)
58
+ this.suppressTimeout = null
59
+ }
60
+ this.outputSuppressed = false
61
+
62
+ if (!discard && this.suppressedData.length > 0) {
63
+ // Write buffered data to terminal
64
+ for (const data of this.suppressedData) {
65
+ this.writeToTerminalDirect(data)
66
+ }
67
+ }
68
+ this.suppressedData = []
69
+
70
+ // Call the end callback if set
71
+ if (this.onSuppressionEndCallback) {
72
+ const callback = this.onSuppressionEndCallback
73
+ this.onSuppressionEndCallback = null
74
+ callback()
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Check if we should resume output based on OSC 633 detection
80
+ * Called when shell integration is detected
81
+ */
82
+ onShellIntegrationDetected = () => {
83
+ if (this.outputSuppressed) {
84
+ this.stopOutputSuppression(true) // Discard the integration command output
85
+ }
13
86
  }
14
87
 
15
88
  activate (terminal = this.term) {
@@ -43,11 +116,76 @@ export default class AttachAddonCustom extends AttachAddon {
43
116
  }
44
117
  }
45
118
 
119
+ /**
120
+ * Check if data contains OSC 633 shell integration sequences
121
+ * @param {string} str - Data string to check
122
+ * @returns {boolean} True if OSC 633 sequence detected
123
+ */
124
+ checkForShellIntegration = (str) => {
125
+ // OSC 633 sequences: ESC]633;X where X is A, B, C, D, E, or P
126
+ // ESC is character code 27 (0x1b)
127
+ // Use includes with the actual characters to avoid lint warning
128
+ const ESC = String.fromCharCode(27)
129
+ return str.includes(ESC + ']633;')
130
+ }
131
+
132
+ /**
133
+ * Write directly to terminal, bypassing suppression check
134
+ * Used for flushing buffered data
135
+ */
136
+ writeToTerminalDirect = (data) => {
137
+ const { term } = this
138
+ if (term.parent?.onZmodem) {
139
+ return
140
+ }
141
+ if (typeof data === 'string') {
142
+ return term.write(data)
143
+ }
144
+ term?.write(data)
145
+ }
146
+
46
147
  writeToTerminal = (data) => {
47
148
  const { term } = this
48
149
  if (term.parent?.onZmodem) {
49
150
  return
50
151
  }
152
+
153
+ // Track initial data arrival
154
+ if (!this.hasReceivedInitialData) {
155
+ this.hasReceivedInitialData = true
156
+ if (this.onInitialDataCallback) {
157
+ const callback = this.onInitialDataCallback
158
+ this.onInitialDataCallback = null
159
+ // Call after a micro-delay to ensure this data is written first
160
+ setTimeout(callback, 0)
161
+ }
162
+ }
163
+
164
+ // Check for shell integration in the data (only when suppressing)
165
+ if (this.outputSuppressed) {
166
+ let str = data
167
+ if (typeof data !== 'string') {
168
+ // Convert to string to check for OSC 633
169
+ const decoder = this.decoder || new TextDecoder('utf-8')
170
+ try {
171
+ str = decoder.decode(data instanceof ArrayBuffer ? data : new Uint8Array(data))
172
+ } catch (e) {
173
+ str = ''
174
+ }
175
+ }
176
+
177
+ // If we detect OSC 633, shell integration is working
178
+ if (this.checkForShellIntegration(str)) {
179
+ this.onShellIntegrationDetected()
180
+ // Don't buffer this - just discard the integration output
181
+ return
182
+ }
183
+
184
+ // Buffer the data while suppressed
185
+ this.suppressedData.push(data)
186
+ return
187
+ }
188
+
51
189
  if (typeof data === 'string') {
52
190
  return term.write(data)
53
191
  }
@@ -62,31 +200,9 @@ export default class AttachAddonCustom extends AttachAddon {
62
200
  const { term } = this
63
201
  term?.parent?.notifyOnData()
64
202
  const str = this.decoder.decode(data)
65
- if (term?.parent?.props.sftpPathFollowSsh && term?.buffer.active.type !== 'alternate') {
66
- const {
67
- cwdId
68
- } = term
69
- const nss = str.split('\r')
70
- const nnss = []
71
- for (const str1 of nss) {
72
- const ns = str1.trim()
73
- if (cwdId) {
74
- const cwdIdEscaped = regEscape(cwdId)
75
- const dirRegex = new RegExp(`${cwdIdEscaped}([^\\n]+?)${cwdIdEscaped}`, 'g')
76
- if (ns.match(dirRegex)) {
77
- const cwd = dirRegex.exec(ns)[1].trim()
78
- if (cwd === '~' || cwd === '%d' || cwd === '%/' || cwd === '$PWD') term.parent.setCwd('')
79
- else term.parent.setCwd(cwd)
80
- nnss.push(ns.replaceAll(dirRegex, ''))
81
- } else nnss.push(str1)
82
- } else {
83
- nnss.push(str1)
84
- }
85
- }
86
- term.write(nnss.join('\r'))
87
- } else {
88
- term?.write(str)
89
- }
203
+ // CWD tracking is now handled by shell integration automatically
204
+ // No need to parse PS1 markers
205
+ term?.write(str)
90
206
  }
91
207
 
92
208
  sendToServer = (data) => {