@electerm/electerm-react 3.9.15 → 3.11.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 (33) hide show
  1. package/client/common/bookmark-schemas.js +164 -0
  2. package/client/common/default-setting.js +3 -2
  3. package/client/common/ws.js +25 -6
  4. package/client/common/zod.js +180 -0
  5. package/client/components/ai/agent-tool-call-card.jsx +90 -0
  6. package/client/components/ai/agent-tools.js +193 -0
  7. package/client/components/ai/agent.js +159 -0
  8. package/client/components/ai/ai-chat-entry.jsx +11 -0
  9. package/client/components/ai/ai-chat-history-item.jsx +48 -2
  10. package/client/components/ai/ai-chat.jsx +25 -6
  11. package/client/components/ai/ai-config.jsx +54 -5
  12. package/client/components/ai/ai.styl +73 -0
  13. package/client/components/batch-op/batch-op-runner.jsx +1 -2
  14. package/client/components/bookmark-form/common/bookmark-group-tree-format.js +1 -1
  15. package/client/components/bookmark-form/common/bookmark-select.jsx +1 -1
  16. package/client/components/bookmark-form/tree-select.jsx +1 -1
  17. package/client/components/main/main.jsx +3 -3
  18. package/client/components/rdp/file-transfer.js +3 -0
  19. package/client/components/setting-panel/setting-terminal.jsx +12 -0
  20. package/client/components/setting-panel/start-session-select.jsx +1 -1
  21. package/client/components/terminal/drop-file-modal.jsx +3 -3
  22. package/client/components/terminal/terminal-apis.js +8 -0
  23. package/client/components/terminal/terminal-error-handle.jsx +1 -1
  24. package/client/components/terminal/terminal-interactive-ui.jsx +157 -0
  25. package/client/components/terminal/terminal-interactive.jsx +65 -125
  26. package/client/components/terminal/terminal.jsx +28 -14
  27. package/client/components/terminal-info/base.jsx +25 -14
  28. package/client/components/terminal-info/log-path-edit.jsx +86 -0
  29. package/client/components/terminal-info/terminal-info-entry.jsx +11 -0
  30. package/client/components/text-editor/text-editor-entry.jsx +11 -0
  31. package/client/components/widgets/widget-form.jsx +30 -2
  32. package/client/entry/worker.js +9 -5
  33. package/package.json +1 -1
@@ -41,3 +41,11 @@ export function toggleTerminalLogTimestamp (pid) {
41
41
  action: 'toggle-terminal-log-timestamp'
42
42
  })
43
43
  }
44
+
45
+ export function setTerminalLogPath (pid, logPath) {
46
+ return fetch({
47
+ pid,
48
+ logPath,
49
+ action: 'set-terminal-log-path'
50
+ })
51
+ }
@@ -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
+ }
@@ -1,30 +1,45 @@
1
1
  /**
2
- * handle terminal interactive operation
2
+ * handle terminal interactive operation - queue based
3
3
  */
4
4
 
5
- import { useEffect, useState } from 'react'
6
- import { Form, Button } from 'antd'
7
- import Modal from '../common/modal'
8
- import InputAutoFocus from '../common/input-auto-focus'
5
+ import { useEffect, useState, useRef, useCallback } from 'react'
9
6
  import wait from '../../common/wait'
10
-
11
- const e = window.translate
12
- const FormItem = Form.Item
7
+ import TermInteractiveUI from './terminal-interactive-ui'
13
8
 
14
9
  export default function TermInteractive () {
15
- const [opts, setter] = useState(null)
16
- const [form] = Form.useForm()
10
+ const [current, setCurrent] = useState(null)
11
+ const queueRef = useRef([])
12
+ const hasCurrentRef = useRef(false)
13
+
17
14
  function updateTab (data) {
18
15
  window.store.updateTab(data.tabId, data.update)
19
16
  }
20
- function onMsg (e) {
17
+
18
+ function processNext () {
19
+ const next = queueRef.current.shift()
20
+ if (next) {
21
+ setCurrent(next)
22
+ } else {
23
+ hasCurrentRef.current = false
24
+ setCurrent(null)
25
+ }
26
+ }
27
+
28
+ const onMsgRef = useRef(null)
29
+ onMsgRef.current = function onMsg (e) {
21
30
  if (
22
31
  e &&
23
32
  e.data &&
24
33
  typeof e.data === 'string' &&
25
34
  e.data.includes('session-interactive')
26
35
  ) {
27
- setter(JSON.parse(e.data))
36
+ const parsed = JSON.parse(e.data)
37
+ if (hasCurrentRef.current) {
38
+ queueRef.current.push(parsed)
39
+ } else {
40
+ hasCurrentRef.current = true
41
+ setCurrent(parsed)
42
+ }
28
43
  } else if (
29
44
  e &&
30
45
  e.data &&
@@ -34,127 +49,52 @@ export default function TermInteractive () {
34
49
  updateTab(JSON.parse(e.data))
35
50
  }
36
51
  }
37
- function clear () {
38
- setter(null)
39
- form.resetFields()
40
- }
41
- function onCancel () {
42
- window.et.commonWs.s({
43
- id: opts.id,
44
- results: []
45
- })
46
- clear()
47
- }
48
- function onOk () {
49
- form.submit()
50
- }
51
- function onIgnore () {
52
- window.et.commonWs.s({
53
- id: opts.id,
54
- results: Object.keys(opts.options.prompts).map(() => '')
55
- })
56
- clear()
57
- }
58
- function onFinish (res) {
59
- window.et.commonWs.s({
60
- id: opts.id,
61
- results: Object.values(res)
62
- })
63
- clear()
64
- }
65
- function renderFormItem (pro, i) {
66
- const {
67
- prompt,
68
- echo
69
- } = pro
70
- const note = (opts.options.instructions || [])[i]
71
- const type = echo
72
- ? 'input'
73
- : 'password'
74
- return (
75
- <FormItem
76
- key={prompt + i}
77
- label={prompt}
78
- rules={[{
79
- required: true, message: 'required'
80
- }]}
81
- >
82
- <div>
83
- <pre>{note}</pre>
84
- </div>
85
- <FormItem noStyle name={'item' + i}>
86
- <InputAutoFocus
87
- type={type}
88
- placeholder={note}
89
- />
90
- </FormItem>
91
- </FormItem>
92
- )
52
+
53
+ function onSend (data) {
54
+ window.et.commonWs.s(data)
93
55
  }
94
- async function initWatch () {
95
- let done = false
96
- while (!done) {
97
- if (window.et.commonWs) {
98
- window.et.commonWs.addEventListener('message', onMsg)
99
- done = true
100
- } else {
56
+
57
+ const onClose = useCallback(() => {
58
+ processNext()
59
+ }, [])
60
+
61
+ useEffect(() => {
62
+ let cancelled = false
63
+ function handler (e) {
64
+ if (!cancelled) {
65
+ onMsgRef.current(e)
66
+ }
67
+ }
68
+ async function initWatch () {
69
+ for (;;) {
70
+ if (cancelled) {
71
+ return
72
+ }
73
+ if (window.et.commonWs) {
74
+ window.et.commonWs.addEventListener('message', handler)
75
+ return
76
+ }
101
77
  await wait(400)
102
78
  }
103
79
  }
104
- }
105
- function init () {
106
80
  initWatch()
107
- }
108
- useEffect(() => {
109
- init()
81
+ return () => {
82
+ cancelled = true
83
+ if (window.et.commonWs) {
84
+ window.et.commonWs.removeEventListener('message', handler)
85
+ }
86
+ }
110
87
  }, [])
111
- if (!opts) {
88
+
89
+ if (!current) {
112
90
  return null
113
91
  }
114
- const props = {
115
- maskClosable: false,
116
- okText: e('submit'),
117
- onCancel,
118
- onOk,
119
- closable: false,
120
- open: true,
121
- title: opts.options?.name || '?',
122
- footer: null
123
- }
92
+
124
93
  return (
125
- <Modal
126
- {...props}
127
- >
128
- <Form
129
- form={form}
130
- layout='vertical'
131
- onFinish={onFinish}
132
- >
133
- {
134
- opts.options.prompts.map(renderFormItem)
135
- }
136
- <FormItem>
137
- <Button
138
- type='primary'
139
- htmlType='submit'
140
- >
141
- {e('submit')}
142
- </Button>
143
- <Button
144
- type='dashed'
145
- className='mg1l'
146
- onClick={onIgnore}
147
- >
148
- {e('ignore')}
149
- </Button>
150
- <Button
151
- className='mg1l'
152
- onClick={onCancel}
153
- >
154
- {e('cancel')}
155
- </Button>
156
- </FormItem>
157
- </Form>
158
- </Modal>
94
+ <TermInteractiveUI
95
+ opts={current}
96
+ onSend={onSend}
97
+ onClose={onClose}
98
+ />
159
99
  )
160
100
  }
@@ -70,6 +70,7 @@ class Term extends Component {
70
70
  hasSelection: false,
71
71
  saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
72
72
  addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
73
+ logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
73
74
  passType: 'password',
74
75
  lines: [],
75
76
  searchResults: [],
@@ -400,10 +401,15 @@ class Term extends Component {
400
401
  return
401
402
  }
402
403
  if (isSshTerminal) {
403
- this.setState({
404
- dropFileModalVisible: true,
405
- droppedFiles: [{ path: filePath, isRemote: true }]
406
- })
404
+ const behavior = this.props.config.dragDropBehavior || 'ask'
405
+ if (behavior === 'ask') {
406
+ this.setState({
407
+ dropFileModalVisible: true,
408
+ droppedFiles: [{ path: filePath, isRemote: true }]
409
+ })
410
+ } else {
411
+ this.handleDropFileAction(behavior, [{ path: filePath, isRemote: true }])
412
+ }
407
413
  return
408
414
  }
409
415
  this.attachAddon._sendData(`"${filePath}" `)
@@ -425,10 +431,15 @@ class Term extends Component {
425
431
  }
426
432
 
427
433
  if (isSshTerminal) {
428
- this.setState({
429
- dropFileModalVisible: true,
430
- droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
431
- })
434
+ const behavior = this.props.config.dragDropBehavior || 'ask'
435
+ if (behavior === 'ask') {
436
+ this.setState({
437
+ dropFileModalVisible: true,
438
+ droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
439
+ })
440
+ } else {
441
+ this.handleDropFileAction(behavior, filePaths.map(path => ({ path, isRemote: false })))
442
+ }
432
443
  return
433
444
  }
434
445
 
@@ -444,8 +455,8 @@ class Term extends Component {
444
455
  })
445
456
  }
446
457
 
447
- handleDropFileAction = (action) => {
448
- const { droppedFiles } = this.state
458
+ handleDropFileAction = (action, filesOverride) => {
459
+ const droppedFiles = filesOverride || this.state.droppedFiles
449
460
  if (!droppedFiles || !droppedFiles.length) {
450
461
  this.handleDropFileModalCancel()
451
462
  return
@@ -454,7 +465,7 @@ class Term extends Component {
454
465
  const filePaths = droppedFiles.map(f => f.path)
455
466
 
456
467
  switch (action) {
457
- case 'trzUpload': {
468
+ case 'trz': {
458
469
  if (this.trzszClient && this.trzszClient.isActive) {
459
470
  message.warning('A transfer is already in progress')
460
471
  this.handleDropFileModalCancel()
@@ -464,7 +475,7 @@ class Term extends Component {
464
475
  this.attachAddon._sendData('trz\r')
465
476
  break
466
477
  }
467
- case 'rzUpload': {
478
+ case 'rz':{
468
479
  if (this.zmodemClient && this.zmodemClient.isActive) {
469
480
  message.warning('A transfer is already in progress')
470
481
  this.handleDropFileModalCancel()
@@ -474,7 +485,7 @@ class Term extends Component {
474
485
  this.attachAddon._sendData('rz\r')
475
486
  break
476
487
  }
477
- case 'inputPath':
488
+ case 'inputOnly':
478
489
  default: {
479
490
  const filesAll = filePaths.map(path => `"${path}"`).join(' ')
480
491
  this.attachAddon._sendData(filesAll)
@@ -825,6 +836,9 @@ class Term extends Component {
825
836
  if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
826
837
  window.store.addCmdHistory(currentCmd.trim())
827
838
  }
839
+ if (currentCmd && currentCmd.trim() === 'exit') {
840
+ this.userTypeExit = true
841
+ }
828
842
  this.closeSuggestions()
829
843
  }
830
844
  }
@@ -1223,7 +1237,7 @@ class Term extends Component {
1223
1237
  ...extra,
1224
1238
  ...execOpts,
1225
1239
  logName,
1226
- sessionLogPath: config.sessionLogPath || createDefaultLogPath(),
1240
+ sessionLogPath: this.state.logPath,
1227
1241
  ...pick(config, [
1228
1242
  'addTimeStampToTermLog',
1229
1243
  'keepaliveCountMax',
@@ -2,15 +2,13 @@
2
2
  * show base terminal info, id sessionID
3
3
  */
4
4
  import { Component } from 'react'
5
- import { osResolve } from '../../common/resolve'
6
5
  import {
7
6
  Switch,
8
7
  Space,
9
8
  Button
10
9
  } from 'antd'
11
- import ShowItem from '../common/show-item'
12
10
  import defaults from '../../common/default-setting'
13
- import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
11
+ import { toggleTerminalLog, toggleTerminalLogTimestamp, setTerminalLogPath } from '../terminal/terminal-apis'
14
12
  import {
15
13
  ClockCircleOutlined,
16
14
  BorderlessTableOutlined,
@@ -19,8 +17,8 @@ import {
19
17
  ApiOutlined,
20
18
  PartitionOutlined
21
19
  } from '@ant-design/icons'
22
- import createDefaultSessionLogPath from '../../common/default-log-path'
23
20
  import { refs } from '../common/ref'
21
+ import LogPathEdit from './log-path-edit'
24
22
 
25
23
  const e = window.translate
26
24
 
@@ -36,7 +34,8 @@ const mapper = {
36
34
  export default class TerminalInfoBase extends Component {
37
35
  state = {
38
36
  saveTerminalLogToFile: false,
39
- addTimeStampToTermLog: false
37
+ addTimeStampToTermLog: false,
38
+ logPath: ''
40
39
  }
41
40
 
42
41
  componentDidMount () {
@@ -75,6 +74,17 @@ export default class TerminalInfoBase extends Component {
75
74
  })
76
75
  }
77
76
 
77
+ onLogPathChange = (v) => {
78
+ const { pid } = this.props
79
+ setTerminalLogPath(pid, v)
80
+ refs.get('term-' + pid)?.setState({
81
+ logPath: v
82
+ })
83
+ this.setState({
84
+ logPath: v
85
+ })
86
+ }
87
+
78
88
  handleToggle = () => {
79
89
  const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
80
90
  const {
@@ -101,7 +111,8 @@ export default class TerminalInfoBase extends Component {
101
111
  if (term) {
102
112
  this.setState({
103
113
  saveTerminalLogToFile: term.state.saveTerminalLogToFile,
104
- addTimeStampToTermLog: term.state.addTimeStampToTermLog
114
+ addTimeStampToTermLog: term.state.addTimeStampToTermLog,
115
+ logPath: term.state.logPath
105
116
  })
106
117
  } else {
107
118
  this.timer = setTimeout(this.getState, 100)
@@ -156,15 +167,10 @@ export default class TerminalInfoBase extends Component {
156
167
  const {
157
168
  id,
158
169
  logName,
159
- sessionLogPath
170
+ pid
160
171
  } = this.props
161
- const { saveTerminalLogToFile } = this.state
162
- const base = sessionLogPath || createDefaultSessionLogPath()
163
- const path = osResolve(base, logName + '.log')
172
+ const { saveTerminalLogToFile, logPath } = this.state
164
173
  const name = e('saveTerminalLogToFile')
165
- const to = saveTerminalLogToFile
166
- ? <ShowItem disabled={!saveTerminalLogToFile} to={path}>{path}</ShowItem>
167
- : path
168
174
  return (
169
175
  <div className='terminal-info-section terminal-info-base'>
170
176
  <div className='fix'>
@@ -187,7 +193,12 @@ export default class TerminalInfoBase extends Component {
187
193
  this.renderInfoSelection()
188
194
  }
189
195
  </div>
190
- <p><b>log:</b> {to}</p>
196
+ <LogPathEdit
197
+ pid={pid}
198
+ logPath={logPath}
199
+ logName={logName}
200
+ setLogPath={this.onLogPathChange}
201
+ />
191
202
  </div>
192
203
  )
193
204
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Per-terminal log path editor
3
+ * Reads/writes logPath on the terminal's own state via refs
4
+ */
5
+ import { Button } from 'antd'
6
+ import message from '../common/message'
7
+ import InputConfirm from '../common/input-confirm'
8
+ import ShowItem from '../common/show-item'
9
+ import { chooseSaveDirectory } from '../../common/choose-save-folder'
10
+ import createDefaultLogPath from '../../common/default-log-path'
11
+ import { osResolve } from '../../common/resolve'
12
+
13
+ const e = window.translate
14
+
15
+ export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
16
+ const defaultPath = createDefaultLogPath()
17
+ const base = logPath || defaultPath
18
+ const fullPath = osResolve(base, logName + '.log')
19
+
20
+ const testAndSet = async (v) => {
21
+ if (v) {
22
+ try {
23
+ const { fs } = window
24
+ const uid = 'test-' + Date.now()
25
+ const testFile = osResolve(v, uid + '.test.log')
26
+ await fs.touch(testFile)
27
+ await fs.unlink(testFile)
28
+ } catch (err) {
29
+ console.log('log path test failed', err)
30
+ message.error('invalid log folder')
31
+ return
32
+ }
33
+ }
34
+ setLogPath(v)
35
+ }
36
+
37
+ const handleChange = (v) => {
38
+ testAndSet(v)
39
+ }
40
+
41
+ const handleChooseFolder = async () => {
42
+ const path = await chooseSaveDirectory()
43
+ if (path) {
44
+ handleChange(path)
45
+ }
46
+ }
47
+
48
+ const handleReset = () => {
49
+ setLogPath('')
50
+ }
51
+
52
+ const inputProps = {
53
+ value: logPath,
54
+ placeholder: defaultPath,
55
+ onChange: handleChange,
56
+ addonAfter: (
57
+ <>
58
+ <Button
59
+ onClick={handleChooseFolder}
60
+ className='mg1r'
61
+ type='text'
62
+ size='small'
63
+ >
64
+ {e('chooseFolder')}
65
+ </Button>
66
+ <Button
67
+ size='small'
68
+ type='text'
69
+ onClick={handleReset}
70
+ >
71
+ {e('reset')}
72
+ </Button>
73
+ </>
74
+ ),
75
+ prefix: e('terminalLogPath') + ': '
76
+ }
77
+
78
+ return (
79
+ <div className='pd1b'>
80
+ <InputConfirm {...inputProps} />
81
+ <div className='pd1t font-xs color-grey'>
82
+ {fullPath} <ShowItem to={fullPath} />
83
+ </div>
84
+ </div>
85
+ )
86
+ }
@@ -0,0 +1,11 @@
1
+ import { lazy, Suspense } from 'react'
2
+
3
+ const TerminalInfo = lazy(() => import('./terminal-info'))
4
+
5
+ export default function TerminalInfoEntry (props) {
6
+ return (
7
+ <Suspense fallback={null}>
8
+ <TerminalInfo {...props} />
9
+ </Suspense>
10
+ )
11
+ }