@electerm/electerm-react 3.10.0 → 3.11.11

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 +165 -0
  2. package/client/common/constants.js +7 -0
  3. package/client/common/parse-quick-connect.js +13 -10
  4. package/client/common/sanitize-filename.js +66 -0
  5. package/client/common/ws.js +25 -6
  6. package/client/common/zod.js +180 -0
  7. package/client/components/ai/agent-tool-call-card.jsx +90 -0
  8. package/client/components/ai/agent-tools.js +193 -0
  9. package/client/components/ai/agent.js +159 -0
  10. package/client/components/ai/ai-chat-entry.jsx +11 -0
  11. package/client/components/ai/ai-chat-history-item.jsx +48 -2
  12. package/client/components/ai/ai-chat.jsx +25 -6
  13. package/client/components/ai/ai-config.jsx +45 -4
  14. package/client/components/ai/ai.styl +73 -0
  15. package/client/components/bookmark-form/bookmark-schema.js +1 -0
  16. package/client/components/bookmark-form/config/serial.js +2 -1
  17. package/client/components/common/font-select.jsx +45 -0
  18. package/client/components/main/main.jsx +3 -3
  19. package/client/components/rdp/file-transfer.js +3 -0
  20. package/client/components/session/session.jsx +2 -2
  21. package/client/components/setting-panel/setting-terminal.jsx +6 -28
  22. package/client/components/setting-panel/text-bg-modal.jsx +8 -27
  23. package/client/components/setting-sync/setting-sync-form.jsx +1 -1
  24. package/client/components/sftp/file-item.jsx +5 -4
  25. package/client/components/shortcuts/shortcut-handler.js +9 -9
  26. package/client/components/terminal/terminal-error-handle.jsx +1 -1
  27. package/client/components/terminal/terminal-interactive-ui.jsx +157 -0
  28. package/client/components/terminal/terminal-interactive.jsx +64 -163
  29. package/client/components/terminal/terminal.jsx +11 -0
  30. package/client/components/terminal-info/terminal-info-entry.jsx +11 -0
  31. package/client/components/text-editor/text-editor-entry.jsx +11 -0
  32. package/client/components/widgets/widget-form.jsx +27 -2
  33. package/client/entry/worker.js +9 -5
  34. package/client/store/mcp-handler.js +22 -2
  35. package/client/store/watch.js +38 -36
  36. package/package.json +1 -1
  37. package/client/common/safe-name.js +0 -19
@@ -1,5 +1,5 @@
1
1
  import { formItemLayout } from '../../../common/form-layout.js'
2
- import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities } from '../../../common/constants.js'
2
+ import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities, commonLineEndings } from '../../../common/constants.js'
3
3
  import defaultSettings from '../../../common/default-setting.js'
4
4
  import { createBaseInitValues, getTerminalBackgroundDefaults } from '../common/init-values.js'
5
5
  import { commonFields } from './common-fields.js'
@@ -57,6 +57,7 @@ const serialConfig = {
57
57
  { type: 'switch', name: 'xon', label: 'xon', valuePropName: 'checked' },
58
58
  { type: 'switch', name: 'xoff', label: 'xoff', valuePropName: 'checked' },
59
59
  { type: 'switch', name: 'xany', label: 'xany', valuePropName: 'checked' },
60
+ { type: 'select', name: 'lineEnding', label: 'lineEnding', options: commonLineEndings.map(d => ({ value: d.value, label: d.label })) },
60
61
  commonFields.runScripts,
61
62
  commonFields.description,
62
63
  { type: 'input', name: 'type', label: 'type', hidden: true }
@@ -0,0 +1,45 @@
1
+ import React, { useMemo, useCallback } from 'react'
2
+ import { Select } from 'antd'
3
+
4
+ const e = window.translate
5
+
6
+ export default function FontSelect ({
7
+ value,
8
+ onChange,
9
+ placeholder,
10
+ style
11
+ }) {
12
+ const { fonts = [] } = window.et || {}
13
+
14
+ const options = useMemo(() => {
15
+ return fonts.map(f => ({
16
+ value: f,
17
+ label: (
18
+ <span
19
+ className='iblock'
20
+ style={{ fontFamily: f }}
21
+ >
22
+ {f}
23
+ </span>
24
+ )
25
+ }))
26
+ }, [fonts])
27
+
28
+ const handleChange = useCallback((vals) => {
29
+ onChange(vals)
30
+ }, [onChange])
31
+
32
+ return (
33
+ <Select
34
+ mode='tags'
35
+ value={value}
36
+ onChange={handleChange}
37
+ className='width-100'
38
+ placeholder={placeholder || e('selectFontFamily')}
39
+ showSearch
40
+ options={options}
41
+ filterOption={(input, option) =>
42
+ (option?.value ?? '').toLowerCase().includes(input.toLowerCase())}
43
+ />
44
+ )
45
+ }
@@ -4,7 +4,7 @@ import Layout from '../layout/layout'
4
4
  import FileInfoModal from '../sftp/file-info-modal'
5
5
  import UpdateCheck from './upgrade'
6
6
  import SettingModal from '../setting-panel/setting-modal'
7
- import TextEditor from '../text-editor/text-editor'
7
+ import TextEditor from '../text-editor/text-editor-entry'
8
8
  import Sidebar from '../sidebar'
9
9
  import CssOverwrite from '../bg/css-overwrite'
10
10
  import UiTheme from './ui-theme'
@@ -19,7 +19,6 @@ import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
19
19
  import classnames from 'classnames'
20
20
  import ShortcutControl from '../shortcuts/shortcut-control.jsx'
21
21
  import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
22
- import TerminalInfo from '../terminal-info/terminal-info'
23
22
  import { ConfigProvider } from 'antd'
24
23
  import { NotificationContainer } from '../common/notification'
25
24
  import InfoModal from '../sidebar/info-modal.jsx'
@@ -27,7 +26,7 @@ import RightSidePanel from '../side-panel-r/side-panel-r'
27
26
  import ConnectionHoppingWarning from './connection-hopping-warnning'
28
27
  import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
29
28
  import LoadSshConfigs from '../ssh-config/load-ssh-configs'
30
- import AIChat from '../ai/ai-chat'
29
+ import AIChat from '../ai/ai-chat-entry'
31
30
  import AIConfigModal from '../ai/ai-config-modal'
32
31
  import Opacity from '../common/opacity'
33
32
  import MoveItemModal from '../tree-list/move-item-modal'
@@ -40,6 +39,7 @@ import UnixTimestampTooltip from '../terminal/unix-timestamp-tooltip'
40
39
  import { pick } from 'lodash-es'
41
40
  import deepCopy from 'json-deep-copy'
42
41
  import './wrapper.styl'
42
+ import TerminalInfo from '../terminal-info/terminal-info-entry'
43
43
  import './term-fullscreen.styl'
44
44
 
45
45
  export default auto(function Index (props) {
@@ -273,6 +273,9 @@ export class FileTransferManager {
273
273
  })
274
274
 
275
275
  this.log(`Downloaded ${fileInfo.name} (${filesize(fileInfo._totalSize)}) to ${fullPath}`, 'success')
276
+ this.hasRemoteFiles = false
277
+ this.pendingDownloads.clear()
278
+ this.notifyStateChange()
276
279
  if (this.onDownloadComplete) {
277
280
  this.onDownloadComplete(fullPath, fileInfo.name, fileInfo._totalSize)
278
281
  }
@@ -35,7 +35,7 @@ import {
35
35
  } from '../../common/constants'
36
36
  import { SplitViewIcon } from '../icons/split-view'
37
37
  import { refs } from '../common/ref'
38
- import safeName from '../../common/safe-name'
38
+ import sanitizeFilename from '../../common/sanitize-filename.js'
39
39
  import { HeartbeatIcon } from '../icons/heartbeat'
40
40
  import './session.styl'
41
41
 
@@ -343,7 +343,7 @@ export default class SessionWrapper extends Component {
343
343
  height
344
344
  } = this.calcTermWidthHeight()
345
345
  const themeConfig = copy(window.store.getThemeConfig())
346
- const logName = safeName(`${tab.title ? tab.title + '_' : ''}${tab.host ? tab.host + '_' : ''}${tab.id}`)
346
+ const logName = sanitizeFilename(`${tab.title ? tab.title + '_' : ''}${tab.host ? tab.host + '_' : ''}${tab.id}`)
347
347
  const pops = {
348
348
  ...this.props,
349
349
  sftpPathFollowSsh,
@@ -28,6 +28,7 @@ import { chooseSaveDirectory } from '../../common/choose-save-folder'
28
28
  import mapper from '../../common/auto-complete-data-mapper'
29
29
  import KeywordForm from './keywords-form'
30
30
  import Link from '../common/external-link'
31
+ import FontSelect from '../common/font-select'
31
32
  import HelpIcon from '../common/help-icon'
32
33
  import KeywordsTransport from './keywords-transport'
33
34
  import fs from '../../common/fs'
@@ -427,36 +428,13 @@ export default class SettingTerminal extends Component {
427
428
  }
428
429
 
429
430
  renderFontFamily = () => {
430
- const { fonts = [] } = window.et
431
431
  const { fontFamily } = this.props.config
432
- const props = {
433
- mode: 'multiple',
434
- onChange: this.handleChangeFont,
435
- value: fontFamily.split(/, */g).filter(d => d.trim()),
436
- style: { width: '100%' }
437
- }
438
432
  return (
439
- <Select
440
- {...props}
441
- showSearch
442
- >
443
- {
444
- fonts.map(f => {
445
- return (
446
- <Option value={f} key={f}>
447
- <span
448
- className='font-option'
449
- style={{
450
- fontFamily: f
451
- }}
452
- >
453
- {f}
454
- </span>
455
- </Option>
456
- )
457
- })
458
- }
459
- </Select>
433
+ <FontSelect
434
+ onChange={this.handleChangeFont}
435
+ value={fontFamily.split(/, */g).filter(d => d.trim())}
436
+ style={{ width: '100%' }}
437
+ />
460
438
  )
461
439
  }
462
440
 
@@ -3,11 +3,11 @@ import {
3
3
  Input,
4
4
  InputNumber,
5
5
  Space,
6
- Select,
7
6
  Button,
8
7
  Modal
9
8
  } from 'antd'
10
9
  import { ColorPicker } from '../bookmark-form/common/color-picker.jsx'
10
+ import FontSelect from '../common/font-select'
11
11
 
12
12
  const { TextArea } = Input
13
13
  const e = window.translate
@@ -24,16 +24,16 @@ export default function TextBgModal ({
24
24
  const [text, setText] = useState(initialText)
25
25
  const [fontSize, setFontSize] = useState(initialSize)
26
26
  const [color, setColor] = useState(initialColor)
27
- const [fontFamily, setFontFamily] = useState(initialFontFamily)
28
-
29
- const { fonts = [] } = window.et || {}
27
+ const [fontFamily, setFontFamily] = useState(
28
+ initialFontFamily.split(/, */g).filter(d => d.trim())
29
+ )
30
30
 
31
31
  const handleOk = () => {
32
32
  onOk({
33
33
  text,
34
34
  fontSize,
35
35
  color,
36
- fontFamily
36
+ fontFamily: fontFamily.join(', ')
37
37
  })
38
38
  }
39
39
 
@@ -43,7 +43,7 @@ export default function TextBgModal ({
43
43
  setText(initialText)
44
44
  setFontSize(initialSize)
45
45
  setColor(initialColor)
46
- setFontFamily(initialFontFamily)
46
+ setFontFamily(initialFontFamily.split(/, */g).filter(d => d.trim()))
47
47
  }
48
48
 
49
49
  const footer = (
@@ -108,30 +108,11 @@ export default function TextBgModal ({
108
108
 
109
109
  <div>
110
110
  <b>{e('fontFamily')}</b>
111
- <Select
111
+ <FontSelect
112
112
  value={fontFamily}
113
113
  onChange={setFontFamily}
114
- style={{ width: '100%' }}
115
114
  placeholder={e('selectFontFamily')}
116
- showSearch
117
- >
118
- {
119
- fonts.map(f => {
120
- return (
121
- <Select.Option value={f} key={f}>
122
- <span
123
- className='font-option'
124
- style={{
125
- fontFamily: f
126
- }}
127
- >
128
- {f}
129
- </span>
130
- </Select.Option>
131
- )
132
- })
133
- }
134
- </Select>
115
+ />
135
116
  </div>
136
117
  </Space>
137
118
  </div>
@@ -309,7 +309,7 @@ export default function SyncForm (props) {
309
309
  )
310
310
  }
311
311
  function createPasswordItem () {
312
- if (syncType === syncTypes.cloud || syncType === syncTypes.webdav) {
312
+ if (syncType === syncTypes.cloud) {
313
313
  return null
314
314
  }
315
315
  return (
@@ -36,6 +36,7 @@ import time from '../../common/time'
36
36
  import { filesize } from 'filesize'
37
37
  import { createTransferProps } from './transfer-common'
38
38
  import generate from '../../common/uid'
39
+ import sanitizeFilename from '../../common/sanitize-filename'
39
40
  import { refsStatic, refs, filesRef } from '../common/ref'
40
41
  import iconsMap from '../sys-menu/icons-map'
41
42
 
@@ -174,7 +175,7 @@ export default class FileSection extends React.Component {
174
175
  ? item.replace(/^remote:/, '')
175
176
  : item
176
177
  const { name } = getFolderFromFilePath(fromPath, isRemote)
177
- const toPath = resolve(path, name)
178
+ const toPath = resolve(path, sanitizeFilename(name))
178
179
  res.push({
179
180
  typeFrom: isRemote ? typeMap.remote : typeMap.local,
180
181
  typeTo: type,
@@ -326,7 +327,7 @@ export default class FileSection extends React.Component {
326
327
  toFile = {
327
328
  ...toFile,
328
329
  ...getFolderFromFilePath(
329
- resolve(toFile.path, toFile.name)
330
+ resolve(toFile.path, sanitizeFilename(toFile.name))
330
331
  ),
331
332
  id: undefined
332
333
  }
@@ -367,7 +368,7 @@ export default class FileSection extends React.Component {
367
368
  return this.doTransferSelected(
368
369
  null,
369
370
  files,
370
- resolve(toFile.path, toFile.name),
371
+ resolve(toFile.path, sanitizeFilename(toFile.name)),
371
372
  toFile.type,
372
373
  operation
373
374
  )
@@ -784,7 +785,7 @@ export default class FileSection extends React.Component {
784
785
  if (toPathBase) {
785
786
  toPath = toPathBase
786
787
  }
787
- toPath = resolve(toPath, name)
788
+ toPath = resolve(toPath, sanitizeFilename(name))
788
789
  const obj = {
789
790
  host: this.props.tab?.host,
790
791
  tabType: this.props.tab?.type,
@@ -54,6 +54,9 @@ export function handleTerminalSelectionReplace (event, ctx) {
54
54
  const isPrintable = key && key.length === 1
55
55
  if (!isBackspace && !isDelete && !isPrintable) return false
56
56
 
57
+ const info = getSelectionReplaceInfo(ctx.term)
58
+ if (!info) return false
59
+
57
60
  if (event && event.preventDefault) {
58
61
  event.preventDefault()
59
62
  }
@@ -61,9 +64,6 @@ export function handleTerminalSelectionReplace (event, ctx) {
61
64
  event.stopPropagation()
62
65
  }
63
66
 
64
- const info = getSelectionReplaceInfo(ctx.term)
65
- if (!info) return false
66
-
67
67
  const { startX, endX, cursorX } = info
68
68
  const move = startX - cursorX
69
69
  if (move > 0) {
@@ -146,6 +146,12 @@ export function shortcutExtend (Cls) {
146
146
  if (isInAntdInput()) {
147
147
  return
148
148
  }
149
+ // During IME composition, let xterm handle the event through its
150
+ // CompositionHelper. Intercepting here breaks composition (e.g. first
151
+ // char lost, closing bracket duplicated when typing Chinese inside brackets).
152
+ if (event.isComposing) {
153
+ return
154
+ }
149
155
  if (handleTerminalSelectionReplace(event, this)) {
150
156
  return false
151
157
  }
@@ -156,12 +162,6 @@ export function shortcutExtend (Cls) {
156
162
  !altKey &&
157
163
  !ctrlKey
158
164
  ) {
159
- // If IME is composing, let the browser delete the composition char only
160
- // Returning false tells xterm not to process the event (and not to call
161
- // preventDefault), so the native textarea backspace still works for IME.
162
- if (event.isComposing) {
163
- return false
164
- }
165
165
  this.props.onDelKeyPressed()
166
166
  const delKey = this.props.config.backspaceMode === '^?' ? 8 : 127
167
167
  const altDelDelKey = delKey === 8 ? 127 : 8
@@ -33,7 +33,7 @@ export default memo(function TerminalErrorHandle ({
33
33
  return (
34
34
  <Alert
35
35
  className='terminal-error-handle'
36
- message={errorMessage}
36
+ title={errorMessage}
37
37
  type='error'
38
38
  showIcon
39
39
  banner
@@ -0,0 +1,157 @@
1
+ /**
2
+ * terminal interactive UI - renders a single interactive event modal
3
+ */
4
+
5
+ import { Form, Button } from 'antd'
6
+ import Modal from '../common/modal'
7
+ import InputAutoFocus from '../common/input-auto-focus'
8
+
9
+ const e = window.translate
10
+ const FormItem = Form.Item
11
+
12
+ export default function TermInteractiveUI ({
13
+ opts,
14
+ onSend,
15
+ onClose
16
+ }) {
17
+ const [form] = Form.useForm()
18
+
19
+ function onCancel () {
20
+ onSend({
21
+ id: opts.id,
22
+ results: []
23
+ })
24
+ onClose()
25
+ }
26
+ function onOk () {
27
+ form.submit()
28
+ }
29
+ function onConfirm () {
30
+ onSend({
31
+ id: opts.id,
32
+ results: [opts.options.confirmResult || 'yes']
33
+ })
34
+ onClose()
35
+ }
36
+ function onIgnore () {
37
+ onSend({
38
+ id: opts.id,
39
+ results: Object.keys(opts.options.prompts).map(() => '')
40
+ })
41
+ onClose()
42
+ }
43
+ function onFinish (res) {
44
+ onSend({
45
+ id: opts.id,
46
+ results: Object.values(res)
47
+ })
48
+ onClose()
49
+ }
50
+ function renderFormItem (pro, i) {
51
+ const {
52
+ prompt,
53
+ echo
54
+ } = pro
55
+ const note = (opts.options.instructions || [])[i]
56
+ const type = echo
57
+ ? 'input'
58
+ : 'password'
59
+ return (
60
+ <FormItem
61
+ key={prompt + i}
62
+ label={prompt}
63
+ rules={[{
64
+ required: true, message: 'required'
65
+ }]}
66
+ >
67
+ <div>
68
+ <pre>{note}</pre>
69
+ </div>
70
+ <FormItem noStyle name={'item' + i}>
71
+ <InputAutoFocus
72
+ type={type}
73
+ placeholder={note}
74
+ />
75
+ </FormItem>
76
+ </FormItem>
77
+ )
78
+ }
79
+ function renderConfirmBody () {
80
+ const instructions = opts.options.instructions || []
81
+ return (
82
+ <div>
83
+ {
84
+ instructions.map((note, index) => {
85
+ return <pre key={note + index}>{note}</pre>
86
+ })
87
+ }
88
+ <FormItem>
89
+ <Button
90
+ type='primary'
91
+ onClick={onConfirm}
92
+ >
93
+ {opts.options.submitText || e('submit')}
94
+ </Button>
95
+ <Button
96
+ className='mg1l'
97
+ onClick={onCancel}
98
+ >
99
+ {opts.options.cancelText || e('cancel')}
100
+ </Button>
101
+ </FormItem>
102
+ </div>
103
+ )
104
+ }
105
+ const props = {
106
+ maskClosable: false,
107
+ okText: e('submit'),
108
+ onCancel,
109
+ onOk,
110
+ closable: false,
111
+ open: true,
112
+ title: opts.options?.name || '?',
113
+ footer: null
114
+ }
115
+ return (
116
+ <Modal
117
+ {...props}
118
+ >
119
+ {
120
+ opts.options?.mode === 'confirm'
121
+ ? renderConfirmBody()
122
+ : (
123
+ <Form
124
+ form={form}
125
+ layout='vertical'
126
+ onFinish={onFinish}
127
+ >
128
+ {
129
+ opts.options.prompts.map(renderFormItem)
130
+ }
131
+ <FormItem>
132
+ <Button
133
+ type='primary'
134
+ htmlType='submit'
135
+ >
136
+ {e('submit')}
137
+ </Button>
138
+ <Button
139
+ type='dashed'
140
+ className='mg1l'
141
+ onClick={onIgnore}
142
+ >
143
+ {e('ignore')}
144
+ </Button>
145
+ <Button
146
+ className='mg1l'
147
+ onClick={onCancel}
148
+ >
149
+ {e('cancel')}
150
+ </Button>
151
+ </FormItem>
152
+ </Form>
153
+ )
154
+ }
155
+ </Modal>
156
+ )
157
+ }