@electerm/electerm-react 1.70.6 → 1.72.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 (72) hide show
  1. package/client/common/cache.js +56 -0
  2. package/client/common/constants.js +2 -0
  3. package/client/common/default-setting.js +2 -1
  4. package/client/common/download.jsx +5 -7
  5. package/client/common/setting-list.js +27 -0
  6. package/client/components/ai/ai-cache.jsx +36 -0
  7. package/client/components/ai/ai-chat-history-item.jsx +1 -1
  8. package/client/components/ai/ai-chat.jsx +5 -40
  9. package/client/components/ai/ai-config-props.js +7 -0
  10. package/client/components/ai/ai-config.jsx +5 -14
  11. package/client/components/ai/ai.styl +0 -4
  12. package/client/components/ai/providers.js +2 -2
  13. package/client/components/batch-op/batch-op-entry.jsx +1 -1
  14. package/client/components/batch-op/batch-op.jsx +2 -2
  15. package/client/components/bookmark-form/form-ssh-common.jsx +2 -3
  16. package/client/components/bookmark-form/form-tabs.jsx +2 -2
  17. package/client/components/bookmark-form/render-connection-hopping.jsx +2 -2
  18. package/client/components/bookmark-form/render-delayed-scripts.jsx +4 -4
  19. package/client/components/bookmark-form/render-ssh-tunnel.jsx +2 -2
  20. package/client/components/bookmark-form/sftp-enable.jsx +9 -0
  21. package/client/components/bookmark-form/ssh-form-ui.jsx +1 -0
  22. package/client/components/bookmark-form/ssh-form.jsx +3 -0
  23. package/client/components/bookmark-form/use-quick-commands.jsx +2 -2
  24. package/client/components/bookmark-form/x11.jsx +78 -9
  25. package/client/components/common/input-auto-focus.jsx +3 -11
  26. package/client/components/layout/layout.jsx +2 -1
  27. package/client/components/main/main.jsx +5 -0
  28. package/client/components/profile/profile-form-elem.jsx +1 -3
  29. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -3
  30. package/client/components/quick-commands/quick-commands-list-form.jsx +2 -2
  31. package/client/components/session/session.jsx +80 -19
  32. package/client/components/session/session.styl +10 -3
  33. package/client/components/session/sessions.jsx +2 -1
  34. package/client/components/setting-panel/keywords-form.jsx +36 -38
  35. package/client/components/setting-panel/setting-modal.jsx +2 -2
  36. package/client/components/setting-panel/setting-terminal.jsx +2 -1
  37. package/client/components/setting-panel/tab-settings.jsx +26 -0
  38. package/client/components/sftp/address-bar.jsx +9 -2
  39. package/client/components/sftp/address-bookmark.jsx +4 -6
  40. package/client/components/sftp/file-item.jsx +1 -1
  41. package/client/components/sftp/file-read.js +14 -19
  42. package/client/components/sftp/keyword-filter.jsx +63 -0
  43. package/client/components/sftp/list-table-ui.jsx +7 -9
  44. package/client/components/sftp/sftp-entry.jsx +46 -9
  45. package/client/components/sftp/sftp.styl +6 -1
  46. package/client/components/sftp/transfer-conflict-store.jsx +1 -1
  47. package/client/components/shortcuts/shortcut-control.jsx +20 -0
  48. package/client/components/shortcuts/shortcut-editor.jsx +2 -2
  49. package/client/components/shortcuts/shortcuts.jsx +2 -2
  50. package/client/components/sidebar/info-modal.jsx +2 -2
  51. package/client/components/sidebar/transfer-list-control.jsx +18 -20
  52. package/client/components/ssh-config/ssh-config-item.jsx +2 -4
  53. package/client/components/ssh-config/ssh-config-load-notify.jsx +2 -2
  54. package/client/components/sys-menu/zoom.jsx +2 -2
  55. package/client/components/tabs/index.jsx +1 -1
  56. package/client/components/tabs/tab.jsx +3 -3
  57. package/client/components/terminal/cmd-item.jsx +32 -0
  58. package/client/components/terminal/command-tracker-addon.js +3 -1
  59. package/client/components/terminal/term-search.jsx +5 -6
  60. package/client/components/terminal/terminal-command-dropdown.jsx +303 -0
  61. package/client/components/terminal/terminal.jsx +88 -8
  62. package/client/components/terminal/terminal.styl +58 -0
  63. package/client/components/terminal-info/terminal-info.jsx +2 -2
  64. package/client/components/tree-list/tree-list.jsx +1 -1
  65. package/client/components/web/address-bar.jsx +2 -2
  66. package/client/store/common.js +27 -2
  67. package/client/store/init-state.js +3 -3
  68. package/client/store/item.js +2 -1
  69. package/client/store/setting.js +3 -2
  70. package/client/store/store.js +23 -24
  71. package/client/store/watch.js +7 -1
  72. package/package.json +1 -1
@@ -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 && 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
+ }
@@ -50,7 +50,7 @@ import AIIcon from '../icons/ai-icon.jsx'
50
50
  import { formatBytes } from '../../common/byte-format.js'
51
51
  import * as fs from './fs.js'
52
52
  import iconsMap from '../sys-menu/icons-map.jsx'
53
- import { refs } from '../common/ref.js'
53
+ import { refs, refsStatic } from '../common/ref.js'
54
54
  import createDefaultLogPath from '../../common/default-log-path.js'
55
55
 
56
56
  const e = window.translate
@@ -75,10 +75,7 @@ class Term extends Component {
75
75
  componentDidMount () {
76
76
  this.initTerminal()
77
77
  if (this.props.tab.enableSsh === false) {
78
- ;(
79
- document.querySelector('.session-current .term-sftp-tabs .type-tab.sftp') ||
80
- document.querySelector('.session-current .term-sftp-tabs .type-tab.fileManager')
81
- ).click()
78
+ this.props.tab.pane = paneMap.fileManager
82
79
  }
83
80
  }
84
81
 
@@ -255,7 +252,7 @@ clear\r`
255
252
 
256
253
  isActiveTerminal = () => {
257
254
  return this.props.tab.id === this.props.activeTabId &&
258
- this.props.pane === paneMap.terminal
255
+ this.props.tab.pane === paneMap.terminal
259
256
  }
260
257
 
261
258
  clearShortcut = (e) => {
@@ -560,7 +557,7 @@ clear\r`
560
557
  }
561
558
  const r = []
562
559
  for (const filePath of files) {
563
- const stat = await getLocalFileInfo(filePath)
560
+ const stat = await getLocalFileInfo(filePath).catch(console.log)
564
561
  r.push({ ...stat, filePath })
565
562
  }
566
563
  return r
@@ -822,14 +819,58 @@ clear\r`
822
819
  this.props.setCwd(cwd, this.state.id)
823
820
  }
824
821
 
822
+ getCursorPosition = () => {
823
+ if (!this.term) return null
824
+
825
+ // Get the active buffer and cursor position
826
+ const buffer = this.term.buffer.active
827
+ const cursorRow = buffer.cursorY
828
+ const cursorCol = buffer.cursorX
829
+
830
+ // Get dimensions from term element
831
+ const termElement = this.term.element
832
+ if (!termElement) return null
833
+
834
+ // Get the exact position of the terminal element
835
+ const termRect = termElement.getBoundingClientRect()
836
+
837
+ // Calculate cell dimensions
838
+ const cellWidth = termRect.width / this.term.cols
839
+ const cellHeight = termRect.height / this.term.rows
840
+
841
+ // Calculate absolute position relative to terminal element
842
+ const left = Math.floor(termRect.left + (cursorCol * cellWidth))
843
+ const top = Math.floor(termRect.top + ((cursorRow + 1) * cellHeight))
844
+
845
+ return {
846
+ cellWidth,
847
+ cellHeight,
848
+ left,
849
+ top
850
+ }
851
+ }
852
+
853
+ closeSuggestions = () => {
854
+ refsStatic
855
+ .get('terminal-suggestions')
856
+ ?.closeSuggestions()
857
+ }
858
+
825
859
  onData = (d) => {
826
860
  if (this.cmdAddon) {
827
861
  this.cmdAddon.handleData(d)
828
862
  }
863
+ const data = this.getCmd().trim()
829
864
  if (!d.includes('\r')) {
830
865
  delete this.userTypeExit
866
+ const cursorPos = this.getCursorPosition()
867
+ if (this.props.config.showCmdSuggestions && data.length > 1) {
868
+ refsStatic
869
+ .get('terminal-suggestions')
870
+ ?.openSuggestions(cursorPos, data)
871
+ }
831
872
  } else {
832
- const data = this.getCmd().trim()
873
+ this.closeSuggestions()
833
874
  if (this.term.buffer.active.type !== 'alternate') {
834
875
  this.timers.getCwd = setTimeout(this.getCwd, 200)
835
876
  }
@@ -837,6 +878,7 @@ clear\r`
837
878
  'exit',
838
879
  'logout'
839
880
  ]
881
+ window.store.addCmdHistory(data)
840
882
  if (exitCmds.includes(data)) {
841
883
  this.userTypeExit = true
842
884
  this.timers.userTypeExit = setTimeout(() => {
@@ -1083,6 +1125,7 @@ clear\r`
1083
1125
  socket.onclose = this.oncloseSocket
1084
1126
  socket.onerror = this.onerrorSocket
1085
1127
  this.socket = socket
1128
+ this.initSocketEvents()
1086
1129
  this.term = term
1087
1130
  socket.onopen = () => {
1088
1131
  this.initAttachAddon()
@@ -1106,6 +1149,43 @@ clear\r`
1106
1149
  )
1107
1150
  }
1108
1151
 
1152
+ initSocketEvents = () => {
1153
+ const originalSend = this.socket.send
1154
+ this.socket.send = (data) => {
1155
+ // Call original send first
1156
+ originalSend.call(this.socket, data)
1157
+
1158
+ // Broadcast to other terminals
1159
+ this.broadcastSocketData(data)
1160
+ }
1161
+ }
1162
+
1163
+ canReceiveBroadcast = (termRef) => {
1164
+ const tabId = termRef.props?.tab?.id
1165
+ const isActiveInBatch = termRef.props.currentBatchTabId === tabId
1166
+ return (
1167
+ isActiveInBatch &&
1168
+ termRef.socket &&
1169
+ termRef.props?.tab.pane === paneMap.terminal
1170
+ )
1171
+ }
1172
+
1173
+ broadcastSocketData = (data) => {
1174
+ if (!this.isActiveTerminal() || !this.props.broadcastInput) {
1175
+ return
1176
+ }
1177
+
1178
+ window.refs.forEach((termRef, refId) => {
1179
+ if (
1180
+ refId !== this.id &&
1181
+ refId.startsWith('term-') &&
1182
+ this.canReceiveBroadcast(termRef)
1183
+ ) {
1184
+ termRef.socket.send(data)
1185
+ }
1186
+ })
1187
+ }
1188
+
1109
1189
  onResize = throttle(() => {
1110
1190
  const cid = this.props.currentBatchTabId
1111
1191
  const tid = this.props.tab?.id
@@ -77,3 +77,61 @@
77
77
  background main-light
78
78
  .batch-input-wrap
79
79
  width calc(100% - 80px)
80
+
81
+ .terminal-suggestions-wrap
82
+ position absolute
83
+ z-index 100
84
+ color text
85
+ max-height 300px
86
+ max-width 300px
87
+ min-width 200px
88
+ box-shadow 0px 0px 3px 3px main-light
89
+ display flex
90
+ flex-direction column
91
+ background main
92
+ border-radius 4px
93
+ &.reverse .terminal-suggestions-list
94
+ border-top 1px solid main-light
95
+ border-bottom none
96
+
97
+ .terminal-suggestions-list
98
+ flex 1
99
+ overflow-x hidden
100
+ overflow-y auto
101
+ max-height 268px
102
+ max-width 300px
103
+ border-bottom 1px solid main-light
104
+
105
+ .terminal-suggestions-sticky
106
+ flex-shrink 0
107
+ height 32px
108
+ padding 0 10px
109
+ line-height 32px
110
+
111
+ .suggestion-item
112
+ display flex
113
+ align-items center
114
+ padding 5px 10px
115
+ cursor pointer
116
+ color text
117
+ &:hover
118
+ background-color main-light
119
+ .suggestion-delete
120
+ visibility visible
121
+
122
+ .suggestion-command
123
+ flex-grow 1
124
+ white-space nowrap
125
+ overflow hidden
126
+ text-overflow ellipsis
127
+
128
+ .suggestion-type
129
+ margin-left 5px
130
+ font-size 0.8em
131
+ opacity .8
132
+ &:hover
133
+ opacity 1
134
+
135
+ .suggestion-delete
136
+ margin-left 5px
137
+ visibility hidden
@@ -43,7 +43,7 @@ export default class TerminalInfoContent extends PureComponent {
43
43
  return null
44
44
  }
45
45
  return (
46
- <div>
46
+ <>
47
47
  <TerminalInfoBase {...props} {...state} />
48
48
  <TerminalInfoUp {...props} {...state} />
49
49
  <TerminalInfoResource
@@ -57,7 +57,7 @@ export default class TerminalInfoContent extends PureComponent {
57
57
  <TerminalInfoNetwork {...props} {...state} />
58
58
  <TerminalInfoDisk {...props} {...state} />
59
59
  <RunCmd {...props} setState={this.setStateRef} />
60
- </div>
60
+ </>
61
61
  )
62
62
  }
63
63
  }
@@ -2,7 +2,7 @@
2
2
  * tree list for bookmarks
3
3
  */
4
4
 
5
- import { Component } from '../common/component'
5
+ import { Component } from 'manate/react/class-components'
6
6
  import {
7
7
  CheckOutlined,
8
8
  CloseOutlined,
@@ -17,10 +17,10 @@ export default function AddressBar (props) {
17
17
  description
18
18
  } = props
19
19
  const content = (
20
- <div>
20
+ <>
21
21
  <h1>{title}</h1>
22
22
  <p>{description}</p>
23
- </div>
23
+ </>
24
24
  )
25
25
  function handleClick () {
26
26
  copy(url)
@@ -10,12 +10,16 @@ import {
10
10
  leftSidebarWidthKey,
11
11
  rightSidebarWidthKey,
12
12
  dismissDelKeyTipLsKey,
13
- connectionMap
13
+ connectionMap,
14
+ settingMap,
15
+ settingAiId
14
16
  } from '../common/constants'
15
17
  import * as ls from '../common/safe-local-storage'
16
18
  import { refs, refsStatic } from '../components/common/ref'
17
19
  import { action } from 'manate'
18
20
  import deepCopy from 'json-deep-copy'
21
+ import { aiConfigsArr } from '../components/ai/ai-config-props'
22
+ import settingList from '../common/setting-list'
19
23
 
20
24
  const e = window.translate
21
25
  const { assign } = Object
@@ -49,7 +53,12 @@ export default Store => {
49
53
  }
50
54
 
51
55
  Store.prototype.toggleAIConfig = function () {
52
- window.store.showAIConfig = !window.store.showAIConfig
56
+ const { store } = window
57
+ store.storeAssign({
58
+ settingTab: settingMap.setting
59
+ })
60
+ store.setSettingItem(settingList().find(d => d.id === settingAiId))
61
+ store.openSettingModal()
53
62
  }
54
63
 
55
64
  Store.prototype.onResize = debounce(async function () {
@@ -303,4 +312,20 @@ export default Store => {
303
312
  profiles.splice(i, 1, np)
304
313
  }
305
314
  }
315
+
316
+ Store.prototype.aiConfigMissing = function () {
317
+ return aiConfigsArr.some(k => !window.store.config[k])
318
+ }
319
+
320
+ Store.prototype.addCmdHistory = action(function (cmd) {
321
+ const { terminalCommandHistory } = window.store
322
+ terminalCommandHistory.add(cmd)
323
+ if (terminalCommandHistory.size > 100) {
324
+ // Delete oldest 20 items when history exceeds 100
325
+ const values = Array.from(terminalCommandHistory.values())
326
+ for (let i = 0; i < 20 && i < values.length; i++) {
327
+ terminalCommandHistory.delete(values[i])
328
+ }
329
+ }
330
+ })
306
331
  }
@@ -21,7 +21,8 @@ import {
21
21
  resolutionsLsKey,
22
22
  aiChatHistoryKey,
23
23
  syncServerDataKey,
24
- splitMap
24
+ splitMap,
25
+ cmdHistoryKey
25
26
  } from '../common/constants'
26
27
  import { buildDefaultThemes } from '../common/terminal-theme'
27
28
  import * as ls from '../common/safe-local-storage'
@@ -72,6 +73,7 @@ export default () => {
72
73
  addressBookmarksLocal: ls.getItemJSON(localAddrBookmarkLsKey, []),
73
74
  openResolutionEdit: false,
74
75
  resolutions: ls.getItemJSON(resolutionsLsKey, []),
76
+ terminalCommandHistory: new Set(ls.getItemJSON(cmdHistoryKey, [])),
75
77
 
76
78
  // init session control
77
79
  selectedSessions: [],
@@ -79,7 +81,6 @@ export default () => {
79
81
 
80
82
  // batch input selected tab ids
81
83
  _batchInputSelectedTabIds: new Set(),
82
- showAIConfig: false,
83
84
  aiChatHistory: ls.getItemJSON(aiChatHistoryKey, []),
84
85
 
85
86
  // sftp
@@ -114,7 +115,6 @@ export default () => {
114
115
  rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
115
116
 
116
117
  // for settings related
117
- _setting: '',
118
118
  settingItem: initSettingItem([], settingMap.bookmarks),
119
119
  settingTab: settingMap.bookmarks, // setting tab
120
120
  bookmarkId: undefined,
@@ -6,6 +6,7 @@ import deepCopy from 'json-deep-copy'
6
6
  import {
7
7
  settingMap
8
8
  } from '../common/constants'
9
+ import settingList from '../common/setting-list'
9
10
  import getInitItem from '../common/init-setting-item'
10
11
 
11
12
  export default Store => {
@@ -72,7 +73,7 @@ export default Store => {
72
73
 
73
74
  Store.prototype.getItems = function (type) {
74
75
  if (type === 'setting') {
75
- return window.store.setting
76
+ return settingList()
76
77
  }
77
78
  return window.store[type]
78
79
  }
@@ -15,6 +15,7 @@ import {
15
15
  import { buildNewTheme } from '../common/terminal-theme'
16
16
  import getInitItem from '../common/init-setting-item'
17
17
  import newTerm from '../common/new-terminal'
18
+ import settingList from '../common/setting-list'
18
19
 
19
20
  const e = window.translate
20
21
 
@@ -101,7 +102,7 @@ export default Store => {
101
102
  const { store } = window
102
103
  if (
103
104
  store.settingTab === settingMap.setting &&
104
- store.settingItem.id === store.setting[0].id &&
105
+ store.settingItem.id === settingList()[0].id &&
105
106
  store.showModal === modals.setting
106
107
  ) {
107
108
  return store.hideSettingModal()
@@ -109,7 +110,7 @@ export default Store => {
109
110
  store.storeAssign({
110
111
  settingTab: settingMap.setting
111
112
  })
112
- store.setSettingItem(copy(store.setting.find(d => d.id === settingSyncId)))
113
+ store.setSettingItem(settingList().find(d => d.id === settingSyncId))
113
114
  store.openSettingModal()
114
115
  }
115
116