@electerm/electerm-react 1.70.2 → 1.72.6

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 (63) hide show
  1. package/client/common/cache.js +56 -0
  2. package/client/common/constants.js +2 -0
  3. package/client/common/download.jsx +5 -7
  4. package/client/common/setting-list.js +27 -0
  5. package/client/components/ai/ai-cache.jsx +36 -0
  6. package/client/components/ai/ai-chat-history-item.jsx +1 -1
  7. package/client/components/ai/ai-chat.jsx +5 -40
  8. package/client/components/ai/ai-config-props.js +7 -0
  9. package/client/components/ai/ai-config.jsx +5 -14
  10. package/client/components/ai/ai.styl +0 -4
  11. package/client/components/ai/providers.js +2 -2
  12. package/client/components/batch-op/batch-op-entry.jsx +1 -1
  13. package/client/components/batch-op/batch-op.jsx +2 -2
  14. package/client/components/bookmark-form/form-ssh-common.jsx +2 -3
  15. package/client/components/bookmark-form/form-tabs.jsx +2 -2
  16. package/client/components/bookmark-form/render-connection-hopping.jsx +2 -2
  17. package/client/components/bookmark-form/render-delayed-scripts.jsx +4 -4
  18. package/client/components/bookmark-form/render-ssh-tunnel.jsx +2 -2
  19. package/client/components/bookmark-form/use-quick-commands.jsx +2 -2
  20. package/client/components/common/input-auto-focus.jsx +3 -11
  21. package/client/components/main/main.jsx +5 -0
  22. package/client/components/profile/profile-form-elem.jsx +1 -3
  23. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -3
  24. package/client/components/quick-commands/quick-commands-list-form.jsx +2 -2
  25. package/client/components/session/session.jsx +50 -14
  26. package/client/components/session/session.styl +10 -3
  27. package/client/components/session/sessions.jsx +1 -1
  28. package/client/components/setting-panel/keywords-form.jsx +36 -38
  29. package/client/components/setting-panel/setting-modal.jsx +2 -2
  30. package/client/components/setting-panel/tab-settings.jsx +26 -0
  31. package/client/components/setting-sync/setting-sync-form.jsx +8 -0
  32. package/client/components/sftp/address-bar.jsx +9 -2
  33. package/client/components/sftp/address-bookmark.jsx +4 -6
  34. package/client/components/sftp/keyword-filter.jsx +63 -0
  35. package/client/components/sftp/list-table-ui.jsx +7 -9
  36. package/client/components/sftp/sftp-entry.jsx +45 -8
  37. package/client/components/sftp/sftp.styl +6 -1
  38. package/client/components/shortcuts/shortcut-control.jsx +20 -0
  39. package/client/components/shortcuts/shortcut-editor.jsx +2 -2
  40. package/client/components/shortcuts/shortcuts.jsx +2 -2
  41. package/client/components/sidebar/info-modal.jsx +2 -2
  42. package/client/components/sidebar/transfer-list-control.jsx +18 -20
  43. package/client/components/ssh-config/ssh-config-item.jsx +2 -4
  44. package/client/components/ssh-config/ssh-config-load-notify.jsx +2 -2
  45. package/client/components/sys-menu/zoom.jsx +2 -2
  46. package/client/components/tabs/index.jsx +1 -1
  47. package/client/components/tabs/tab.jsx +3 -3
  48. package/client/components/terminal/cmd-item.jsx +32 -0
  49. package/client/components/terminal/command-tracker-addon.js +3 -1
  50. package/client/components/terminal/term-search.jsx +5 -6
  51. package/client/components/terminal/terminal-command-dropdown.jsx +303 -0
  52. package/client/components/terminal/terminal.jsx +84 -3
  53. package/client/components/terminal/terminal.styl +58 -0
  54. package/client/components/terminal-info/terminal-info.jsx +2 -2
  55. package/client/components/tree-list/tree-list.jsx +1 -1
  56. package/client/components/web/address-bar.jsx +2 -2
  57. package/client/store/common.js +27 -2
  58. package/client/store/init-state.js +3 -3
  59. package/client/store/item.js +2 -1
  60. package/client/store/setting.js +3 -2
  61. package/client/store/store.js +23 -24
  62. package/client/store/watch.js +7 -1
  63. package/package.json +1 -1
@@ -35,6 +35,7 @@ class ShortcutControl extends React.PureComponent {
35
35
  this.handleSftpKeyboardEvent(e)
36
36
  // Then handle extended shortcuts
37
37
  this.handleKeyboardEvent(e)
38
+ this.handleAiChat(e)
38
39
  }
39
40
 
40
41
  getActiveSftp = () => {
@@ -45,6 +46,25 @@ class ShortcutControl extends React.PureComponent {
45
46
  return ref
46
47
  }
47
48
 
49
+ handleAiChat = (e) => {
50
+ const { rightPanelTab } = window.store
51
+ if (rightPanelTab !== 'ai') {
52
+ return
53
+ }
54
+ const elem = document.activeElement
55
+ if (
56
+ e.ctrlKey &&
57
+ e.key === 'Enter' &&
58
+ !e.shiftKey &&
59
+ !e.altKey &&
60
+ !e.metaKey &&
61
+ elem?.tagName === 'TEXTAREA' &&
62
+ elem?.classList.contains('ai-chat-textarea')
63
+ ) {
64
+ refsStatic.get('AIChat')?.handleSubmit()
65
+ }
66
+ }
67
+
48
68
  // SFTP shortcuts handler
49
69
  handleSftpKeyboardEvent = (e) => {
50
70
  const activeSftp = this.getActiveSftp()
@@ -175,7 +175,7 @@ export default class ShortcutEdit extends PureComponent {
175
175
  return null
176
176
  }
177
177
  return (
178
- <div>
178
+ <>
179
179
  <CheckOutlined
180
180
  onClick={this.handleConfirm}
181
181
  className='pointer'
@@ -184,7 +184,7 @@ export default class ShortcutEdit extends PureComponent {
184
184
  onClick={this.handleCancel}
185
185
  className='pointer mg1l'
186
186
  />
187
- </div>
187
+ </>
188
188
  )
189
189
  }
190
190
 
@@ -140,7 +140,7 @@ export default class Shortcuts extends PureComponent {
140
140
  rowKey: 'id'
141
141
  }
142
142
  return (
143
- <div>
143
+ <>
144
144
  <Table
145
145
  {...props}
146
146
  />
@@ -151,7 +151,7 @@ export default class Shortcuts extends PureComponent {
151
151
  {e('resetAllToDefault')}
152
152
  </Button>
153
153
  </div>
154
- </div>
154
+ </>
155
155
  )
156
156
  }
157
157
  }
@@ -119,7 +119,7 @@ export default auto(function InfoModal (props) {
119
119
  key: infoTabs.info,
120
120
  label: e('about'),
121
121
  children: (
122
- <div>
122
+ <>
123
123
  <LogoElem />
124
124
  <p className='mg2b'>{e('desc')}</p>
125
125
  <RunningTime />
@@ -187,7 +187,7 @@ export default auto(function InfoModal (props) {
187
187
  </Link>
188
188
  </p>
189
189
  {renderCheckUpdate()}
190
- </div>
190
+ </>
191
191
  )
192
192
  },
193
193
  {
@@ -137,26 +137,24 @@ export default class TransferModalUI extends Component {
137
137
  ...groups
138
138
  ]
139
139
  return (
140
- <div>
141
- <Select
142
- value={this.state.filter}
143
- onChange={this.handleFilter}
144
- popupMatchSelectWidth={false}
145
- >
146
- {
147
- all.map(item => {
148
- return (
149
- <Option
150
- key={item.id}
151
- value={item.id}
152
- >
153
- {item.title}
154
- </Option>
155
- )
156
- })
157
- }
158
- </Select>
159
- </div>
140
+ <Select
141
+ value={this.state.filter}
142
+ onChange={this.handleFilter}
143
+ popupMatchSelectWidth={false}
144
+ >
145
+ {
146
+ all.map(item => {
147
+ return (
148
+ <Option
149
+ key={item.id}
150
+ value={item.id}
151
+ >
152
+ {item.title}
153
+ </Option>
154
+ )
155
+ })
156
+ }
157
+ </Select>
160
158
  )
161
159
  }
162
160
 
@@ -16,10 +16,8 @@ export default function SshConfigItem (props) {
16
16
 
17
17
  return (
18
18
  <Tooltip title={generateTooltipContent(item)}>
19
- <div>
20
- <div className='elli pd1y pd2x'>
21
- ssh {item.title}
22
- </div>
19
+ <div className='elli pd1y pd2x'>
20
+ ssh {item.title}
23
21
  </div>
24
22
  </Tooltip>
25
23
  )
@@ -25,7 +25,7 @@ function showNotification () {
25
25
  placement: 'bottom',
26
26
  key: 'sshConfigNotify',
27
27
  description: (
28
- <div>
28
+ <>
29
29
  <p>{e('sshConfigNotice')}</p>
30
30
  <Button type='primary' onClick={handleLoad} className='mg1r mg1b'>
31
31
  {e('import')}
@@ -33,7 +33,7 @@ function showNotification () {
33
33
  <Button onClick={handleIgnore} className='mg1r mg1b'>
34
34
  {e('ignore')}
35
35
  </Button>
36
- </div>
36
+ </>
37
37
  )
38
38
  })
39
39
  }
@@ -20,7 +20,7 @@ export default function ZoomMenu (props) {
20
20
  max={500}
21
21
  addonAfter='%'
22
22
  addonBefore={
23
- <div>
23
+ <>
24
24
  <PlusCircleOutlined
25
25
  onClick={() => store.zoom(0.25, true)}
26
26
  className='mg1r pointer font16'
@@ -29,7 +29,7 @@ export default function ZoomMenu (props) {
29
29
  onClick={() => store.zoom(-0.25, true)}
30
30
  className='pointer font16'
31
31
  />
32
- </div>
32
+ </>
33
33
  }
34
34
  />
35
35
  )
@@ -2,7 +2,7 @@
2
2
  * session tabs component
3
3
  */
4
4
 
5
- import { Component } from '../common/component'
5
+ import { Component } from 'manate/react/class-components'
6
6
  import React from 'react'
7
7
  import runIdle from '../../common/run-idle'
8
8
  import { throttle } from 'lodash-es'
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { createRef } from 'react'
6
- import { Component } from '../common/component'
6
+ import { Component } from 'manate/react/class-components'
7
7
  import { refs } from '../common/ref'
8
8
  import {
9
9
  CloseOutlined,
@@ -373,10 +373,10 @@ class Tab extends Component {
373
373
  return <div key={tunnel}>{tunnel}</div>
374
374
  })
375
375
  return (
376
- <div>
376
+ <>
377
377
  <div>{title}</div>
378
378
  {list}
379
- </div>
379
+ </>
380
380
  )
381
381
  }
382
382
 
@@ -0,0 +1,32 @@
1
+ import React from 'react'
2
+ import { CloseCircleOutlined } from '@ant-design/icons'
3
+
4
+ const SuggestionItem = ({ item, onSelect, onDelete }) => {
5
+ const handleClick = () => {
6
+ onSelect(item)
7
+ }
8
+
9
+ const handleDelete = (e) => {
10
+ e.stopPropagation()
11
+ onDelete(item)
12
+ }
13
+
14
+ return (
15
+ <div className='suggestion-item'>
16
+ <span className='suggestion-command' onClick={handleClick}>
17
+ {item.command}
18
+ </span>
19
+ <span className='suggestion-type'>
20
+ {item.type}
21
+ </span>
22
+ {item.type === 'H' && (
23
+ <CloseCircleOutlined
24
+ className='suggestion-delete'
25
+ onClick={handleDelete}
26
+ />
27
+ )}
28
+ </div>
29
+ )
30
+ }
31
+
32
+ export default SuggestionItem
@@ -45,7 +45,9 @@ export class CommandTrackerAddon {
45
45
  // This is now our internal handler
46
46
  _handleKey = (e) => {
47
47
  const { key } = e
48
- if (key === 'Enter') {
48
+ if (e.ctrlKey && key.toLowerCase() === 'c') {
49
+ this.clearCommand()
50
+ } else if (key === 'Enter') {
49
51
  // Command executed, reset
50
52
  this.currentCommand = this.activeCommand
51
53
  this.activeCommand = ''
@@ -127,14 +127,14 @@ export default class TermSearch extends PureComponent {
127
127
 
128
128
  renderAfter = () => {
129
129
  return (
130
- <div>
130
+ <>
131
131
  {
132
132
  this.renderMatchData()
133
133
  }
134
134
  {
135
135
  this.searchActions.map(this.renderSearchAction)
136
136
  }
137
- </div>
137
+ </>
138
138
  )
139
139
  }
140
140
 
@@ -174,11 +174,11 @@ export default class TermSearch extends PureComponent {
174
174
 
175
175
  renderSuffix = () => {
176
176
  return (
177
- <div>
177
+ <>
178
178
  {
179
179
  this.searchControls.map(this.renderSearchControl)
180
180
  }
181
- </div>
181
+ </>
182
182
  )
183
183
  }
184
184
 
@@ -191,8 +191,7 @@ export default class TermSearch extends PureComponent {
191
191
  if (
192
192
  !termSearchOpen ||
193
193
  !currentTab ||
194
- currentTab.pane === paneMap.fileManager ||
195
- currentTab.pane === paneMap.sftp
194
+ currentTab.pane === paneMap.fileManager
196
195
  ) {
197
196
  return null
198
197
  }
@@ -0,0 +1,303 @@
1
+ import { Component } from 'manate/react/class-components'
2
+ import { refsStatic, refs } from '../common/ref'
3
+ import SuggestionItem from './cmd-item'
4
+ import { aiSuggestionsCache } from '../../common/cache'
5
+ import uid from '../../common/uid'
6
+ import classnames from 'classnames'
7
+ import {
8
+ LoadingOutlined
9
+ } from '@ant-design/icons'
10
+
11
+ export default class TerminalCmdSuggestions extends Component {
12
+ state = {
13
+ cursorPosition: {},
14
+ showSuggestions: false,
15
+ loadingAiSuggestions: false,
16
+ aiSuggestions: [],
17
+ cmdIsDescription: false,
18
+ reverse: false,
19
+ cmd: ''
20
+ }
21
+
22
+ componentDidMount () {
23
+ refsStatic.add('terminal-suggestions', this)
24
+ }
25
+
26
+ componentWillUnmount () {
27
+ refsStatic.remove('terminal-suggestions')
28
+ }
29
+
30
+ parseAiSuggestions = (aiResponse) => {
31
+ try {
32
+ return JSON.parse(aiResponse.response).map(d => {
33
+ return {
34
+ command: d,
35
+ type: 'AI'
36
+ }
37
+ })
38
+ } catch (e) {
39
+ console.log('parseAiSuggestions error:', e)
40
+ return []
41
+ }
42
+ }
43
+
44
+ getAiSuggestions = async (event) => {
45
+ event.stopPropagation()
46
+ const { cmd } = this.state
47
+ if (window.store.aiConfigMissing()) {
48
+ window.store.toggleAIConfig()
49
+ }
50
+ this.setState({
51
+ loadingAiSuggestions: true
52
+ })
53
+ const {
54
+ config
55
+ } = window.store
56
+ const prompt = `give me max 5 command suggestions for user input: "${cmd}", return pure json format result only, no extra words, no markdown format, follow this format: ["command1","command2"...]`
57
+ const cached = aiSuggestionsCache.get(cmd)
58
+ if (cached) {
59
+ this.setState({
60
+ loadingAiSuggestions: false,
61
+ aiSuggestions: cached
62
+ })
63
+ return
64
+ }
65
+
66
+ const aiResponse = aiSuggestionsCache.get(prompt) || await window.pre.runGlobalAsync(
67
+ 'AIchat',
68
+ prompt,
69
+ config.modelAI,
70
+ config.roleAI,
71
+ config.baseURLAI,
72
+ config.apiPathAI,
73
+ config.apiKeyAI
74
+ ).catch(
75
+ window.store.onError
76
+ )
77
+ if (cmd !== this.state.cmd) {
78
+ this.setState({
79
+ loadingAiSuggestions: false
80
+ })
81
+ return
82
+ }
83
+ if (aiResponse && aiResponse.error) {
84
+ this.setState({
85
+ loadingAiSuggestions: false
86
+ })
87
+ return window.store.onError(
88
+ new Error(aiResponse.error)
89
+ )
90
+ }
91
+ this.setState({
92
+ loadingAiSuggestions: false,
93
+ aiSuggestions: this.parseAiSuggestions(aiResponse, cmd)
94
+ })
95
+ }
96
+
97
+ openSuggestions = (cursorPosition, cmd) => {
98
+ if (!this.state.showSuggestions) {
99
+ document.addEventListener('click', this.handleClickOutside)
100
+ document.addEventListener('keydown', this.handleKeyDown)
101
+ }
102
+
103
+ const {
104
+ left,
105
+ top,
106
+ cellHeight
107
+ } = cursorPosition
108
+ const w = window.innerWidth
109
+ const h = window.innerHeight
110
+
111
+ const position = {}
112
+ const reverse = top > h / 2
113
+
114
+ // Use right position if close to right edge
115
+ if (left > w / 2) {
116
+ position.right = w - left
117
+ } else {
118
+ position.left = left
119
+ }
120
+
121
+ // Use bottom position if close to bottom edge
122
+ if (reverse) {
123
+ position.bottom = h - top + cellHeight
124
+ } else {
125
+ position.top = top
126
+ }
127
+ this.setState({
128
+ showSuggestions: true,
129
+ cursorPosition: position,
130
+ cmd,
131
+ reverse
132
+ })
133
+ }
134
+
135
+ closeSuggestions = () => {
136
+ document.removeEventListener('click', this.handleClickOutside)
137
+ document.removeEventListener('keydown', this.handleKeyDown)
138
+ const {
139
+ aiSuggestions
140
+ } = this.state
141
+ if (aiSuggestions.length) {
142
+ aiSuggestionsCache.set(this.state.cmd, aiSuggestions)
143
+ aiSuggestions.forEach(item => {
144
+ window.store.addCmdHistory(item.command, 'aiCmdHistory')
145
+ })
146
+ }
147
+ this.setState({
148
+ showSuggestions: false,
149
+ aiSuggestions: []
150
+ })
151
+ }
152
+
153
+ handleClickOutside = (event) => {
154
+ const suggestionElement = document.querySelector('.terminal-suggestions-wrap')
155
+ if (suggestionElement && !suggestionElement.contains(event.target)) {
156
+ this.closeSuggestions()
157
+ }
158
+ }
159
+
160
+ handleKeyDown = (event) => {
161
+ if (event.key === 'Escape') {
162
+ this.closeSuggestions()
163
+ }
164
+ }
165
+
166
+ handleDelete = (item) => {
167
+ window.store.terminalCommandHistory.delete(item.command)
168
+ }
169
+
170
+ handleSelect = (item) => {
171
+ const { activeTabId } = window.store
172
+ const terminal = refs.get('term-' + activeTabId)
173
+ if (!terminal) {
174
+ return
175
+ }
176
+
177
+ // const titleElement = domEvent.target.closest('.ant-menu-title-content')
178
+ // const command = titleElement?.firstChild?.textContent
179
+ const { command } = item
180
+ const { cmd } = this.state
181
+ let txt = ''
182
+ if (cmd && command.startsWith(cmd)) {
183
+ txt = command.slice(cmd.length)
184
+ } else {
185
+ const pre = '\b'.repeat(cmd.length)
186
+ txt = pre + command
187
+ }
188
+ terminal.attachAddon._sendData(txt)
189
+ terminal.term.focus()
190
+ this.closeSuggestions()
191
+ }
192
+
193
+ processCommands = (commands, type, uniqueCommands, res) => {
194
+ const { cmd } = this.state
195
+ commands
196
+ .filter(command => command.startsWith(cmd))
197
+ .forEach(command => {
198
+ if (!uniqueCommands.has(command)) {
199
+ uniqueCommands.add(command)
200
+ res.push({
201
+ id: uid(),
202
+ command,
203
+ type
204
+ })
205
+ }
206
+ })
207
+ }
208
+
209
+ getSuggestions = () => {
210
+ const uniqueCommands = new Set()
211
+ const {
212
+ history = [],
213
+ batch = [],
214
+ quick = []
215
+ } = this.props.suggestions
216
+ const res = []
217
+ this.state.aiSuggestions
218
+ .forEach(item => {
219
+ if (!uniqueCommands.has(item.command)) {
220
+ uniqueCommands.add(item.command)
221
+ }
222
+ res.push({
223
+ id: uid(),
224
+ ...item
225
+ })
226
+ })
227
+ this.processCommands(history, 'H', uniqueCommands, res)
228
+ this.processCommands(batch, 'B', uniqueCommands, res)
229
+ this.processCommands(quick, 'Q', uniqueCommands, res)
230
+ return this.state.reverse ? res.reverse() : res
231
+ }
232
+
233
+ renderAIIcon () {
234
+ const e = window.translate
235
+ const {
236
+ loadingAiSuggestions
237
+ } = this.state
238
+ if (loadingAiSuggestions) {
239
+ return (
240
+ <>
241
+ <LoadingOutlined /> {e('getAiSuggestions')}
242
+ </>
243
+ )
244
+ }
245
+ const aiProps = {
246
+ onClick: this.getAiSuggestions,
247
+ className: 'pointer'
248
+ }
249
+ return (
250
+ <div {...aiProps}>
251
+ {e('getAiSuggestions')}
252
+ </div>
253
+ )
254
+ }
255
+
256
+ renderSticky (pos) {
257
+ const {
258
+ reverse
259
+ } = this.state
260
+ if (
261
+ (pos === 'top' && !reverse) ||
262
+ (pos === 'bottom' && reverse)
263
+ ) {
264
+ return null
265
+ }
266
+ return (
267
+ <div className='terminal-suggestions-sticky'>
268
+ {this.renderAIIcon()}
269
+ </div>
270
+ )
271
+ }
272
+
273
+ render () {
274
+ const { showSuggestions, cursorPosition, reverse } = this.state
275
+ if (!showSuggestions) {
276
+ return null
277
+ }
278
+ const suggestions = this.getSuggestions()
279
+ const cls = classnames('terminal-suggestions-wrap', {
280
+ reverse
281
+ })
282
+ return (
283
+ <div className={cls} style={cursorPosition}>
284
+ {this.renderSticky('top')}
285
+ <div className='terminal-suggestions-list'>
286
+ {
287
+ suggestions.map(item => {
288
+ return (
289
+ <SuggestionItem
290
+ key={item.id}
291
+ item={item}
292
+ onSelect={this.handleSelect}
293
+ onDelete={this.handleDelete}
294
+ />
295
+ )
296
+ })
297
+ }
298
+ </div>
299
+ {this.renderSticky('bottom')}
300
+ </div>
301
+ )
302
+ }
303
+ }