@electerm/electerm-react 2.15.8 → 2.16.8

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 (47) hide show
  1. package/client/components/ai/ai-chat.jsx +44 -2
  2. package/client/components/ai/ai-stop-icon.jsx +13 -0
  3. package/client/components/ai/ai.styl +10 -0
  4. package/client/components/bg/css-overwrite.jsx +158 -187
  5. package/client/components/bg/custom-css.jsx +8 -17
  6. package/client/components/bookmark-form/bookmark-schema.js +7 -1
  7. package/client/components/bookmark-form/common/color-picker.jsx +4 -8
  8. package/client/components/bookmark-form/common/exec-settings-field.jsx +44 -0
  9. package/client/components/bookmark-form/common/fields.jsx +3 -0
  10. package/client/components/bookmark-form/config/common-fields.js +1 -0
  11. package/client/components/bookmark-form/config/local.js +3 -1
  12. package/client/components/common/animate-text.jsx +22 -23
  13. package/client/components/common/modal.jsx +2 -0
  14. package/client/components/common/password.jsx +19 -32
  15. package/client/components/footer/cmd-history.jsx +154 -0
  16. package/client/components/footer/cmd-history.styl +73 -0
  17. package/client/components/footer/footer-entry.jsx +15 -1
  18. package/client/components/main/main.jsx +2 -3
  19. package/client/components/quick-commands/quick-commands-box.jsx +6 -3
  20. package/client/components/quick-commands/quick-commands-select.jsx +1 -4
  21. package/client/components/rdp/rdp-session.jsx +23 -4
  22. package/client/components/session/session.styl +1 -3
  23. package/client/components/setting-panel/terminal-bg-config.jsx +2 -0
  24. package/client/components/setting-panel/text-bg-modal.jsx +9 -9
  25. package/client/components/sftp/file-item.jsx +22 -0
  26. package/client/components/sidebar/history-item.jsx +6 -3
  27. package/client/components/sidebar/history.jsx +48 -5
  28. package/client/components/sidebar/sidebar-panel.jsx +0 -13
  29. package/client/components/sidebar/sidebar.styl +19 -0
  30. package/client/components/tabs/add-btn-menu.jsx +28 -4
  31. package/client/components/tabs/add-btn.jsx +1 -1
  32. package/client/components/tabs/add-btn.styl +8 -0
  33. package/client/components/terminal/terminal-command-dropdown.jsx +1 -1
  34. package/client/components/terminal/terminal.jsx +28 -11
  35. package/client/components/terminal/transfer-client-base.js +18 -2
  36. package/client/components/terminal/trzsz-client.js +2 -1
  37. package/client/components/terminal/zmodem-client.js +2 -1
  38. package/client/components/text-editor/edit-with-custom-editor.jsx +49 -0
  39. package/client/components/text-editor/text-editor-form.jsx +13 -5
  40. package/client/components/text-editor/text-editor.jsx +20 -1
  41. package/client/components/vnc/vnc-session.jsx +3 -0
  42. package/client/components/vnc/vnc.styl +1 -1
  43. package/client/store/common.js +31 -4
  44. package/client/store/init-state.js +26 -1
  45. package/client/store/store.js +3 -3
  46. package/client/store/watch.js +8 -1
  47. package/package.json +1 -1
@@ -6,8 +6,7 @@ import { refsStatic } from '../common/ref'
6
6
  export default function HistoryItem (props) {
7
7
  const { store } = window
8
8
  const {
9
- item,
10
- index
9
+ item
11
10
  } = props
12
11
  const timeoutRef = useRef(null)
13
12
 
@@ -30,7 +29,11 @@ export default function HistoryItem (props) {
30
29
 
31
30
  function handleDelete (e) {
32
31
  e.stopPropagation()
33
- store.history.splice(index, 1)
32
+ const { id } = item
33
+ const i = store.history.findIndex((i) => i.id === id)
34
+ if (i !== -1) {
35
+ store.history.splice(i, 1)
36
+ }
34
37
  }
35
38
 
36
39
  function handleBookmark (e) {
@@ -2,31 +2,74 @@
2
2
  * history select
3
3
  */
4
4
 
5
+ import React, { useState, useEffect } from 'react'
5
6
  import { auto } from 'manate/react'
7
+ import { Switch } from 'antd'
8
+ import { UnorderedListOutlined } from '@ant-design/icons'
6
9
  import HistoryItem from './history-item'
10
+ import { getItemJSON, setItemJSON } from '../../common/safe-local-storage.js'
11
+
12
+ const SORT_BY_FREQ_KEY = 'electerm-history-sort-by-frequency'
7
13
 
8
14
  export default auto(function HistoryPanel (props) {
9
15
  const { store } = window
10
16
  if (store.config.disableConnectionHistory) {
11
17
  return null
12
18
  }
19
+ const [sortByFrequency, setSortByFrequency] = useState(() => {
20
+ return getItemJSON(SORT_BY_FREQ_KEY, false)
21
+ })
22
+
23
+ useEffect(() => {
24
+ setItemJSON(SORT_BY_FREQ_KEY, sortByFrequency)
25
+ }, [sortByFrequency])
26
+
13
27
  const {
14
28
  history
15
29
  } = store
16
- const arr = props.sort
17
- ? [...history].sort((a, b) => { return b.count - a.count })
18
- : history
30
+ let arr = [...history]
31
+ if (sortByFrequency) {
32
+ arr = arr.sort((a, b) => { return b.count - a.count })
33
+ }
34
+
35
+ const handleSortByFrequencyChange = (checked) => {
36
+ setSortByFrequency(checked)
37
+ }
38
+
39
+ const handleClearHistory = () => {
40
+ store.clearHistory()
41
+ }
42
+ const e = window.translate
43
+ const switchProps = {
44
+ checkedChildren: e('sortByFrequency'),
45
+ unCheckedChildren: e('sortByFrequency'),
46
+ checked: sortByFrequency,
47
+ onChange: handleSortByFrequencyChange,
48
+ size: 'small'
49
+ }
50
+ const clearIconProps = {
51
+ className: 'history-clear-icon pointer clear-ai-icon icon-hover',
52
+ title: window.translate('clear'),
53
+ onClick: handleClearHistory
54
+ }
19
55
  return (
20
56
  <div
21
57
  className='sidebar-panel-history'
22
58
  >
23
- <div className='pd2x'>
59
+ <div className='history-header pd2x pd2b'>
60
+ <Switch
61
+ {...switchProps}
62
+ />
63
+ <UnorderedListOutlined
64
+ {...clearIconProps}
65
+ />
66
+ </div>
67
+ <div className='history-body'>
24
68
  {
25
69
  arr.map((item, i) => {
26
70
  return (
27
71
  <HistoryItem
28
72
  key={item.id}
29
- index={i}
30
73
  item={item}
31
74
  />
32
75
  )
@@ -13,7 +13,6 @@ import {
13
13
  PlusCircleOutlined,
14
14
  ShrinkOutlined,
15
15
  PushpinOutlined,
16
- UnorderedListOutlined,
17
16
  SelectOutlined
18
17
  } from '@ant-design/icons'
19
18
 
@@ -29,21 +28,9 @@ export default memo(function SidebarPanel (props) {
29
28
  const prps1 = {
30
29
  className: prps.className + (pinned ? ' pinned' : '')
31
30
  }
32
- const props2 = {
33
- onClick: store.clearHistory,
34
- className: 'mg2x pointer clear-ai-icon icon-hover'
35
- }
36
- const tabBarExtraContent = sidebarPanelTab === 'history'
37
- ? (
38
- <UnorderedListOutlined
39
- {...props2}
40
- />
41
- )
42
- : null
43
31
  const tabsProps = {
44
32
  activeKey: sidebarPanelTab,
45
33
  onChange: store.handleSidebarPanelTab,
46
- tabBarExtraContent,
47
34
  items: [
48
35
  {
49
36
  key: 'bookmarks',
@@ -48,6 +48,25 @@
48
48
  flex-direction column
49
49
  overflow hidden
50
50
  min-height 0
51
+ .history-header
52
+ flex-shrink 0
53
+ display flex
54
+ align-items center
55
+ border-bottom 1px solid var(--border)
56
+ position sticky
57
+ top 0
58
+ z-index 10
59
+ background var(--main)
60
+ .history-clear-icon
61
+ margin-left auto
62
+ margin-right 0
63
+ color var(--text-dark)
64
+ &:hover
65
+ color var(--error)
66
+ .history-body
67
+ flex 1
68
+ overflow-y auto
69
+ overflow-x hidden
51
70
  .not-system-ui.is-mac
52
71
  .sidebar-bar
53
72
  margin-top 20px
@@ -2,13 +2,15 @@
2
2
  * Add button menu component
3
3
  */
4
4
 
5
- import React, { useCallback } from 'react'
5
+ import React, { useCallback, useState } from 'react'
6
+ import { Tabs } from 'antd'
6
7
  import {
7
8
  CodeFilled,
8
9
  RightSquareFilled,
9
10
  RobotOutlined
10
11
  } from '@ant-design/icons'
11
12
  import BookmarksList from '../sidebar/bookmark-select'
13
+ import History from '../sidebar/history'
12
14
  import DragHandle from '../common/drag-handle'
13
15
  import QuickConnect from './quick-connect'
14
16
 
@@ -26,6 +28,7 @@ export default function AddBtnMenu ({
26
28
  setAddPanelWidth
27
29
  }) {
28
30
  const { onNewSsh, onNewSshAI } = window.store
31
+ const [activeTab, setActiveTab] = useState('bookmarks')
29
32
  const cls = 'pd2x pd1y context-item pointer'
30
33
  const addTabBtn = window.store.hasNodePty
31
34
  ? (
@@ -59,6 +62,24 @@ export default function AddBtnMenu ({
59
62
  left: menuPosition === 'right'
60
63
  }
61
64
 
65
+ const tabItems = [
66
+ {
67
+ key: 'bookmarks',
68
+ label: e('bookmarks')
69
+ },
70
+ {
71
+ key: 'history',
72
+ label: e('history')
73
+ }
74
+ ]
75
+
76
+ let listContent
77
+ if (activeTab === 'bookmarks') {
78
+ listContent = <BookmarksList store={window.store} />
79
+ } else {
80
+ listContent = <History store={window.store} />
81
+ }
82
+
62
83
  return (
63
84
  <div
64
85
  ref={menuRef}
@@ -89,11 +110,14 @@ export default function AddBtnMenu ({
89
110
  <RobotOutlined /> {e('createBookmarkByAI')}
90
111
  </div>
91
112
  <QuickConnect batch={batch} inputOnly />
113
+ <Tabs
114
+ activeKey={activeTab}
115
+ onChange={setActiveTab}
116
+ items={tabItems}
117
+ />
92
118
  </div>
93
119
  <div className='add-menu-list'>
94
- <BookmarksList
95
- store={window.store}
96
- />
120
+ {listContent}
97
121
  </div>
98
122
  </div>
99
123
  )
@@ -118,7 +118,7 @@ export default class AddBtn extends Component {
118
118
  focusSearchInput = () => {
119
119
  // Focus the search input after the menu renders
120
120
  this.focusTimeout = setTimeout(() => {
121
- const searchInput = this.menuRef.current?.querySelector('.ant-input')
121
+ const searchInput = this.menuRef.current?.querySelector('.add-menu-list .ant-input')
122
122
  if (searchInput) {
123
123
  searchInput.focus()
124
124
  searchInput.select()
@@ -24,4 +24,12 @@
24
24
  display flex
25
25
  flex-direction column
26
26
  min-height 0
27
+ .sidebar-panel-history
28
+ position relative
29
+ top auto
30
+ left auto
31
+ right auto
32
+ bottom auto
33
+ flex 1
34
+ min-height 0
27
35
 
@@ -215,7 +215,7 @@ export default class TerminalCmdSuggestions extends Component {
215
215
  history = [],
216
216
  batch = [],
217
217
  quick = []
218
- } = this.props.suggestions
218
+ } = this.props.suggestions || {}
219
219
  const res = []
220
220
  this.state.aiSuggestions
221
221
  .forEach(item => {
@@ -82,7 +82,6 @@ class Term extends Component {
82
82
  this.currentInput = ''
83
83
  this.shellInjected = false
84
84
  this.shellType = null
85
- this.manualCommandHistory = new Set()
86
85
  }
87
86
 
88
87
  domRef = createRef()
@@ -737,7 +736,6 @@ class Term extends Component {
737
736
  if (d === '\r' || d === '\n') {
738
737
  const currentCmd = this.getCurrentInput()
739
738
  if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
740
- this.manualCommandHistory.add(currentCmd.trim())
741
739
  window.store.addCmdHistory(currentCmd.trim())
742
740
  }
743
741
  this.closeSuggestions()
@@ -903,9 +901,7 @@ class Term extends Component {
903
901
  }
904
902
 
905
903
  shouldUseManualHistory = () => {
906
- const useManual = this.props.config.showCmdSuggestions &&
907
- (this.shellType === 'sh' || (isWin && this.isLocal()))
908
- return useManual
904
+ return !this.cmdAddon || !this.cmdAddon.hasShellIntegration()
909
905
  }
910
906
 
911
907
  canInjectShellIntegration = () => {
@@ -1078,6 +1074,32 @@ class Term extends Component {
1078
1074
  const { savePassword } = this.state
1079
1075
  const termType = type
1080
1076
  const extra = this.props.sessionOptions
1077
+ // Determine if this is a local terminal (no host)
1078
+ const isLocalType = !tab.host
1079
+ // Build exec settings: only for local type, prefer tab settings over config
1080
+ let execOpts = {}
1081
+ let execPropName = 'execLinux'
1082
+ if (isWin) {
1083
+ execPropName = 'execWindows'
1084
+ } else if (isMac) {
1085
+ execPropName = 'execMac'
1086
+ }
1087
+ if (isLocalType) {
1088
+ // Check flat properties on tab first (bookmark data), then fall back to config
1089
+ if (tab[execPropName]) {
1090
+ // Use bookmark's exec setting directly
1091
+ execOpts = {
1092
+ [execPropName]: tab[execPropName],
1093
+ [`${execPropName}Args`]: tab[`${execPropName}Args`] || []
1094
+ }
1095
+ } else if (config[execPropName]) {
1096
+ // Use global config exec settings
1097
+ execOpts = {
1098
+ [execPropName]: config[execPropName],
1099
+ [`${execPropName}Args`]: config[`${execPropName}Args`] || []
1100
+ }
1101
+ }
1102
+ }
1081
1103
  const opts = clone({
1082
1104
  cols,
1083
1105
  rows,
@@ -1085,18 +1107,13 @@ class Term extends Component {
1085
1107
  saveTerminalLogToFile: config.saveTerminalLogToFile,
1086
1108
  ...tab,
1087
1109
  ...extra,
1110
+ ...execOpts,
1088
1111
  logName,
1089
1112
  sessionLogPath: config.sessionLogPath || createDefaultLogPath(),
1090
1113
  ...pick(config, [
1091
1114
  'addTimeStampToTermLog',
1092
1115
  'keepaliveInterval',
1093
1116
  'keepaliveCountMax',
1094
- 'execWindows',
1095
- 'execMac',
1096
- 'execLinux',
1097
- 'execWindowsArgs',
1098
- 'execMacArgs',
1099
- 'execLinuxArgs',
1100
1117
  'keyword2FA',
1101
1118
  'debug'
1102
1119
  ]),
@@ -18,6 +18,7 @@ export class TransferClientBase {
18
18
  this.currentTransfer = null
19
19
  this.savePath = null
20
20
  this.messageHandler = null
21
+ this._prevProgressRows = 0
21
22
  }
22
23
 
23
24
  /**
@@ -130,8 +131,23 @@ export class TransferClientBase {
130
131
  const speedStr = speed > 0 ? `, ${formatSize(speed)}/s` : ''
131
132
  const doneStr = isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''
132
133
 
133
- const str = `\r\x1b[2K\x1b[32m${name}\x1b[0m: ${percent}% ${bar} ${sizeStr}${speedStr}${doneStr}`
134
- this.writeToTerminal(str + '\r')
134
+ const str = `\x1b[32m${name}\x1b[0m: ${percent}% ${bar} ${sizeStr}${speedStr}${doneStr}`
135
+
136
+ // Calculate visible length (no ANSI codes) to detect line wrapping
137
+ const visibleLen = name.length + 2 + String(percent).length + 2 + barWidth + 1 + sizeStr.length + speedStr.length + (isComplete ? 7 : 0)
138
+ const cols = this.terminal?.term?.cols || 80
139
+ const currentRows = Math.max(1, Math.ceil(visibleLen / cols))
140
+
141
+ // Move cursor back up to the start of the previous progress block, then
142
+ // erase everything from there to end-of-display so wrapped lines are gone.
143
+ let clearSeq = '\r'
144
+ for (let i = 0; i < this._prevProgressRows; i++) {
145
+ clearSeq += '\x1b[A' // cursor up one row
146
+ }
147
+ clearSeq += '\x1b[J' // erase from cursor to end of display
148
+
149
+ this._prevProgressRows = currentRows - 1
150
+ this.writeToTerminal(clearSeq + str + '\r')
135
151
  return str
136
152
  }
137
153
 
@@ -208,8 +208,9 @@ export class TrzszClient extends TransferClientBase {
208
208
  this.currentTransfer.path = path
209
209
  // Call directly to ensure 100% is displayed immediately
210
210
  this._doWriteProgress(true)
211
- // Add newline after completion
211
+ // Add newline after completion and reset row tracker for next file
212
212
  this.writeToTerminal('\r\n')
213
+ this._prevProgressRows = 0
213
214
  }
214
215
  this.currentTransfer = null
215
216
  }
@@ -180,8 +180,9 @@ export class ZmodemClient extends TransferClientBase {
180
180
  this.currentTransfer.path = path
181
181
  // Call directly to ensure 100% is displayed immediately
182
182
  this._doWriteProgress(true)
183
- // Add newline after completion
183
+ // Add newline after completion and reset row tracker for next file
184
184
  this.writeToTerminal('\r\n')
185
+ this._prevProgressRows = 0
185
186
  }
186
187
  this.currentTransfer = null
187
188
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Edit with custom editor - input + button component
3
+ */
4
+
5
+ import { useState } from 'react'
6
+ import { Button, Input, Space } from 'antd'
7
+
8
+ const LS_KEY = 'customEditorCommand'
9
+ const e = window.translate
10
+
11
+ export default function EditWithCustomEditor ({ loading, editWithCustom }) {
12
+ const [editorCommand, setEditorCommand] = useState(
13
+ () => window.localStorage.getItem(LS_KEY) || ''
14
+ )
15
+
16
+ function handleChange (ev) {
17
+ const val = ev.target.value
18
+ setEditorCommand(val)
19
+ window.localStorage.setItem(LS_KEY, val)
20
+ }
21
+
22
+ function handleClick () {
23
+ const cmd = editorCommand.trim()
24
+ if (cmd) {
25
+ editWithCustom(cmd)
26
+ }
27
+ }
28
+
29
+ if (window.et.isWebApp) {
30
+ return null
31
+ }
32
+
33
+ return (
34
+ <Space.Compact className='mg1b'>
35
+ <Button
36
+ type='primary'
37
+ disabled={loading || !editorCommand.trim()}
38
+ onClick={handleClick}
39
+ >
40
+ {e('editWith')}
41
+ </Button>
42
+ <Input
43
+ value={editorCommand}
44
+ onChange={handleChange}
45
+ disabled={loading}
46
+ />
47
+ </Space.Compact>
48
+ )
49
+ }
@@ -5,6 +5,7 @@
5
5
  import { useEffect } from 'react'
6
6
  import { Form, Button } from 'antd'
7
7
  import SimpleEditor from './simple-editor'
8
+ import EditWithCustomEditor from './edit-with-custom-editor'
8
9
 
9
10
  const FormItem = Form.Item
10
11
  const e = window.translate
@@ -38,7 +39,7 @@ export default function TextEditorForm (props) {
38
39
  } = props
39
40
  const popsEdit = {
40
41
  type: 'primary',
41
- className: 'mg3r mg1b',
42
+ className: 'mg1r mg1b',
42
43
  disabled: loading,
43
44
  onClick: props.editWith
44
45
  }
@@ -62,10 +63,6 @@ export default function TextEditorForm (props) {
62
63
  <SimpleEditor />
63
64
  </FormItem>
64
65
  <div className='pd1t pd2b'>
65
- <Button
66
- {...popsEdit}
67
- >{e('editWithSystemEditor')}
68
- </Button>
69
66
  <Button
70
67
  type='primary'
71
68
  className='mg1r mg1b'
@@ -85,6 +82,17 @@ export default function TextEditorForm (props) {
85
82
  >{e('cancel')}
86
83
  </Button>
87
84
  </div>
85
+ <div className='pd1t pd2b'>
86
+ <Button
87
+ {...popsEdit}
88
+ >
89
+ {e('editWithSystemEditor')}
90
+ </Button>
91
+ <EditWithCustomEditor
92
+ loading={loading}
93
+ editWithCustom={props.editWithCustom}
94
+ />
95
+ </div>
88
96
  </Form>
89
97
  )
90
98
  }
@@ -118,6 +118,24 @@ export default class TextEditor extends PureComponent {
118
118
  fileRef.editWithSystemEditor(text)
119
119
  }
120
120
 
121
+ editWithCustom = async (editorCommand) => {
122
+ this.setStateProxy({
123
+ loading: true
124
+ })
125
+ const {
126
+ id, text
127
+ } = this.state
128
+ const fileRef = refs.get(id)
129
+ if (!fileRef) {
130
+ return
131
+ }
132
+ await fileRef.editWithCustomEditor(text, editorCommand)
133
+ .catch(err => {
134
+ this.setStateProxy({ loading: false })
135
+ window.store.onError(err)
136
+ })
137
+ }
138
+
121
139
  cancel = () => {
122
140
  this.setStateProxy({
123
141
  id: '',
@@ -150,7 +168,8 @@ export default class TextEditor extends PureComponent {
150
168
  submit: this.handleSubmit,
151
169
  text,
152
170
  cancel: this.cancel,
153
- editWith: this.editWith
171
+ editWith: this.editWith,
172
+ editWithCustom: this.editWithCustom
154
173
  }
155
174
  return (
156
175
  <Modal
@@ -373,6 +373,9 @@ export default class VncSession extends PureComponent {
373
373
  }
374
374
  rfb.scaleViewport = scaleViewport
375
375
  rfb.clipViewport = clipViewport
376
+ rfb.qualityLevel = qualityLevel
377
+ rfb.compressionLevel = compressionLevel
378
+ rfb.viewOnly = viewOnly
376
379
  this.rfb = rfb
377
380
  }
378
381
 
@@ -14,4 +14,4 @@
14
14
  height: 100% !important
15
15
  object-fit: contain
16
16
  > div
17
- background: transparent !important
17
+ background: transparent !important
@@ -345,13 +345,40 @@ export default Store => {
345
345
  return
346
346
  }
347
347
  const { terminalCommandHistory } = window.store
348
- terminalCommandHistory.add(cmd)
348
+ const existing = terminalCommandHistory.get(cmd)
349
+ if (existing) {
350
+ // Use set() to trigger reactivity
351
+ terminalCommandHistory.set(cmd, {
352
+ count: existing.count + 1,
353
+ lastUseTime: new Date().toISOString()
354
+ })
355
+ } else {
356
+ terminalCommandHistory.set(cmd, {
357
+ count: 1,
358
+ lastUseTime: new Date().toISOString()
359
+ })
360
+ }
349
361
  if (terminalCommandHistory.size > 100) {
350
362
  // Delete oldest 20 items when history exceeds 100
351
- const values = Array.from(terminalCommandHistory.values())
352
- for (let i = 0; i < 20 && i < values.length; i++) {
353
- terminalCommandHistory.delete(values[i])
363
+ const entries = Array.from(terminalCommandHistory.entries())
364
+ entries.sort((a, b) => new Date(a[1].lastUseTime).getTime() - new Date(b[1].lastUseTime).getTime())
365
+ for (let i = 0; i < 20 && i < entries.length; i++) {
366
+ terminalCommandHistory.delete(entries[i][0])
354
367
  }
355
368
  }
356
369
  })
370
+
371
+ Store.prototype.deleteCmdHistory = function (cmd) {
372
+ const { terminalCommandHistory } = window.store
373
+ terminalCommandHistory.delete(cmd)
374
+ }
375
+
376
+ Store.prototype.clearAllCmdHistory = function () {
377
+ window.store.terminalCommandHistory = new Map()
378
+ }
379
+
380
+ Store.prototype.runCmdFromHistory = function (cmd) {
381
+ window.store.runQuickCommand(cmd)
382
+ window.store.addCmdHistory(cmd)
383
+ }
357
384
  }
@@ -74,7 +74,32 @@ export default () => {
74
74
  addressBookmarksLocal: ls.getItemJSON(localAddrBookmarkLsKey, []),
75
75
  openResolutionEdit: false,
76
76
  resolutions: ls.getItemJSON(resolutionsLsKey, []),
77
- terminalCommandHistory: new Set(ls.getItemJSON(cmdHistoryKey, [])),
77
+ // terminalCommandHistory: Map<cmdString, {count: Number, lastUseTime: DateString}>
78
+ // Load from localStorage and migrate from old format (Set of strings) if needed
79
+ terminalCommandHistory: (() => {
80
+ const savedData = ls.getItemJSON(cmdHistoryKey, [])
81
+ const map = new Map()
82
+ if (Array.isArray(savedData)) {
83
+ // Check if old format (array of strings) or new format (array of objects)
84
+ if (savedData.length > 0 && typeof savedData[0] === 'string') {
85
+ // Old format: migrate to new format
86
+ savedData.forEach(cmd => {
87
+ map.set(cmd, { count: 1, lastUseTime: new Date().toISOString() })
88
+ })
89
+ } else {
90
+ // New format: array of {cmd, count, lastUseTime}
91
+ savedData.forEach(item => {
92
+ if (item.cmd) {
93
+ map.set(item.cmd, {
94
+ count: item.count || 1,
95
+ lastUseTime: item.lastUseTime || new Date().toISOString()
96
+ })
97
+ }
98
+ })
99
+ }
100
+ }
101
+ return map
102
+ })(),
78
103
 
79
104
  // workspaces
80
105
  workspaces: [],
@@ -176,9 +176,9 @@ class Store {
176
176
 
177
177
  get terminalCommandSuggestions () {
178
178
  const { store } = window
179
- const historyCommands = Array.from(store.terminalCommandHistory)
180
- const batchInputCommands = store.batchInputs
181
- const quickCommands = store.quickCommands.reduce(
179
+ const historyCommands = Array.from(store.terminalCommandHistory.keys())
180
+ const batchInputCommands = store.batchInputs || []
181
+ const quickCommands = (store.quickCommands || []).reduce(
182
182
  (p, q) => {
183
183
  return [
184
184
  ...p,
@@ -145,7 +145,14 @@ export default store => {
145
145
  }).start()
146
146
 
147
147
  autoRun(() => {
148
- ls.setItemJSON(cmdHistoryKey, Array.from(store.terminalCommandHistory))
148
+ const history = store.terminalCommandHistory
149
+ // Save in new format: array of {cmd, count, lastUseTime}
150
+ const data = Array.from(history.entries()).map(([cmd, info]) => ({
151
+ cmd,
152
+ count: info.count,
153
+ lastUseTime: info.lastUseTime
154
+ }))
155
+ ls.setItemJSON(cmdHistoryKey, data)
149
156
  return store.terminalCommandHistory
150
157
  }).start()
151
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.15.8",
3
+ "version": "2.16.8",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",