@electerm/electerm-react 3.1.6 → 3.1.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.
@@ -352,6 +352,4 @@ export const terminalTypes = [
352
352
  export const sshConfigLoadKey = 'ssh-config-loaded'
353
353
  export const sshConfigKey = 'ignore-ssh-config'
354
354
  export const connectionHoppingWarnKey = 'connectionHoppingWarnned'
355
- export const aiChatHistoryKey = 'ai-chat-history'
356
355
  export const syncServerDataKey = 'sync-server-data'
357
- export const cmdHistoryKey = 'cmd-history'
@@ -23,17 +23,27 @@ const dbAction = (...args) => {
23
23
  /**
24
24
  * standalone db names
25
25
  */
26
- export const dbNames = without(
27
- Object.keys(settingMap),
28
- settingMap.setting,
29
- settingMap.widgets
30
- )
26
+ export const dbNames = [
27
+ ...without(
28
+ Object.keys(settingMap),
29
+ settingMap.setting,
30
+ settingMap.widgets
31
+ ),
32
+ 'history',
33
+ 'terminalCommandHistory',
34
+ 'aiChatHistory'
35
+ ]
31
36
 
32
- export const dbNamesForWatch = without(
33
- Object.keys(settingMap),
34
- settingMap.setting,
35
- settingMap.widgets
36
- )
37
+ export const dbNamesForWatch = [
38
+ ...without(
39
+ Object.keys(settingMap),
40
+ settingMap.setting,
41
+ settingMap.widgets
42
+ ),
43
+ 'history',
44
+ 'terminalCommandHistory',
45
+ 'aiChatHistory'
46
+ ]
37
47
 
38
48
  /**
39
49
  * db insert
@@ -45,6 +45,14 @@ export default class ConfirmModalStore extends Component {
45
45
  if (this.activeTransferId === transferId || this.queuedTransferIds.has(transferId)) {
46
46
  return
47
47
  }
48
+ const globalPolicy = window._transferConflictPolicy
49
+ if (globalPolicy && Object.values(fileActions).includes(globalPolicy)) {
50
+ const { id, transferBatch } = transfer
51
+ const trid = `tr-${transferBatch}-${id}`
52
+ const currentTransfer = refsTransfers.get(trid)
53
+ currentTransfer?.onDecision(globalPolicy)
54
+ return
55
+ }
48
56
  this.queue.push(transfer)
49
57
  this.queuedTransferIds.add(transferId)
50
58
  if (!this.activeTransferId) {
@@ -31,7 +31,7 @@ export default auto(function CmdHistory (props) {
31
31
 
32
32
  function handleDeleteCommand (cmd, ev) {
33
33
  ev.stopPropagation()
34
- terminalCommandHistory.delete(cmd)
34
+ window.store.deleteCmdHistory(cmd)
35
35
  }
36
36
 
37
37
  function handleCopyCommand (cmd, ev) {
@@ -54,9 +54,7 @@ export default auto(function CmdHistory (props) {
54
54
  setKeyword(e.target.value)
55
55
  }
56
56
 
57
- const historyArray = Array.from(terminalCommandHistory || [])
58
- .map(([cmd, info]) => ({ cmd, ...info }))
59
- .reverse()
57
+ const historyArray = (terminalCommandHistory || []).slice().reverse()
60
58
 
61
59
  let filtered = filterArray(historyArray, keyword)
62
60
 
@@ -25,6 +25,14 @@
25
25
 
26
26
  .item-list-unit.dragover
27
27
  border: 1px dashed var(--primary)
28
+ .qm-item-dragover
29
+ border-left 2px solid var(--primary)
30
+ .qm-drag-handle
31
+ cursor grab
32
+ .qm-field-dragging
33
+ opacity 0.4
34
+ .qm-field-dragover
35
+ border-top 2px solid var(--primary)
28
36
  .qm-label-select
29
37
  min-width 120px
30
38
 
@@ -23,6 +23,8 @@ export default class QuickCommandsItem extends PureComponent {
23
23
  draggable,
24
24
  handleDragOver,
25
25
  handleDragStart,
26
+ handleDragEnter,
27
+ handleDragLeave,
26
28
  handleDrop
27
29
  } = this.props
28
30
  const cls = classNames('qm-item mg1r mg1b')
@@ -34,6 +36,8 @@ export default class QuickCommandsItem extends PureComponent {
34
36
  draggable,
35
37
  onDragOver: handleDragOver,
36
38
  onDragStart: handleDragStart,
39
+ onDragEnter: handleDragEnter,
40
+ onDragLeave: handleDragLeave,
37
41
  onDrop: handleDrop
38
42
  }
39
43
  return (
@@ -87,6 +87,14 @@ export default function QuickCommandsFooterBox (props) {
87
87
  e.dataTransfer.setData('idDragged', e.target.getAttribute('data-id'))
88
88
  }
89
89
 
90
+ function onDragEnter (e) {
91
+ e.target.closest('.qm-item')?.classList.add('qm-item-dragover')
92
+ }
93
+
94
+ function onDragLeave (e) {
95
+ e.target.closest('.qm-item')?.classList.remove('qm-item-dragover')
96
+ }
97
+
90
98
  function onDrop (e) {
91
99
  onDropFunc(e, '.qm-item')
92
100
  }
@@ -116,6 +124,8 @@ export default function QuickCommandsFooterBox (props) {
116
124
  draggable={!qmSortByFrequency}
117
125
  handleDragOver={onDragOver}
118
126
  handleDragStart={onDragStart}
127
+ handleDragEnter={onDragEnter}
128
+ handleDragLeave={onDragLeave}
119
129
  handleDrop={onDrop}
120
130
  />
121
131
  )
@@ -127,7 +127,7 @@ export default function QuickCommandForm (props) {
127
127
  >
128
128
  <InputAutoFocus />
129
129
  </FormItem>
130
- {renderQm()}
130
+ {renderQm(form)}
131
131
  <FormItem
132
132
  name='labels'
133
133
  label={e('label')}
@@ -5,7 +5,7 @@ import {
5
5
  Button,
6
6
  Input
7
7
  } from 'antd'
8
- import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
8
+ import { MinusCircleOutlined, PlusOutlined, HolderOutlined } from '@ant-design/icons'
9
9
  import HelpIcon from '../common/help-icon'
10
10
  import { copy } from '../../common/clipboard'
11
11
  import { useRef } from 'react'
@@ -14,15 +14,70 @@ const FormItem = Form.Item
14
14
  const FormList = Form.List
15
15
  const e = window.translate
16
16
 
17
- export default function renderQm () {
17
+ export default function renderQm (form) {
18
18
  const focused = useRef(0)
19
- function renderItem (field, i, add, remove) {
19
+ const dragIndexRef = useRef(null)
20
+
21
+ function handleDragStart (e, index) {
22
+ dragIndexRef.current = index
23
+ e.target.closest('.ant-space-compact')?.classList.add('qm-field-dragging')
24
+ e.dataTransfer.effectAllowed = 'move'
25
+ e.dataTransfer.setData('text/plain', String(index))
26
+ }
27
+
28
+ function handleDragOver (e, index) {
29
+ e.preventDefault()
30
+ e.dataTransfer.dropEffect = 'move'
31
+ const el = e.target.closest('.ant-space-compact')
32
+ if (dragIndexRef.current !== index && el) {
33
+ el.classList.add('qm-field-dragover')
34
+ }
35
+ }
36
+
37
+ function handleDragLeave (e) {
38
+ e.target.closest('.ant-space-compact')?.classList.remove('qm-field-dragover')
39
+ }
40
+
41
+ function handleDrop (e, index, form) {
42
+ e.preventDefault()
43
+ const el = e.target.closest('.ant-space-compact')
44
+ el?.classList.remove('qm-field-dragover')
45
+ const dragIndex = dragIndexRef.current
46
+ if (dragIndex === null || dragIndex === index) {
47
+ dragIndexRef.current = null
48
+ return
49
+ }
50
+ const commands = form.getFieldValue('commands') || []
51
+ const item = commands[dragIndex]
52
+ const newCommands = [...commands]
53
+ newCommands.splice(dragIndex, 1)
54
+ newCommands.splice(index, 0, item)
55
+ form.setFieldValue('commands', newCommands)
56
+ dragIndexRef.current = null
57
+ }
58
+
59
+ function handleDragEnd (e) {
60
+ const el = e.target.closest('.ant-space-compact')
61
+ el?.classList.remove('qm-field-dragging')
62
+ el?.classList.remove('qm-field-dragover')
63
+ dragIndexRef.current = null
64
+ }
65
+
66
+ function renderItem (field, i, add, remove, form) {
20
67
  return (
21
68
  <Space.Compact
22
69
  align='center'
23
70
  className='width-100 mg2b'
24
71
  key={field.key}
72
+ draggable
73
+ onDragStart={(e) => handleDragStart(e, i)}
74
+ onDragOver={(e) => handleDragOver(e, i)}
75
+ onDragLeave={handleDragLeave}
76
+ onDrop={(e) => handleDrop(e, i, form)}
77
+ onDragEnd={handleDragEnd}
25
78
  >
79
+ <HolderOutlined className='mg1r qm-drag-handle' />
80
+
26
81
  <Space.Addon>{e('delay')}</Space.Addon>
27
82
  <FormItem
28
83
  label=''
@@ -119,7 +174,7 @@ export default function renderQm () {
119
174
  <>
120
175
  {
121
176
  fields.map((field, i) => {
122
- return renderItem(field, i, add, remove)
177
+ return renderItem(field, i, add, remove, form)
123
178
  })
124
179
  }
125
180
  <FormItem>
@@ -3,12 +3,14 @@
3
3
  */
4
4
 
5
5
  import List from '../setting-panel/list'
6
- import { PlusOutlined } from '@ant-design/icons'
6
+ import { PlusOutlined, CopyOutlined } from '@ant-design/icons'
7
7
  import { Select } from 'antd'
8
8
  import classnames from 'classnames'
9
9
  import highlight from '../common/highlight'
10
10
  import QmTransport from './quick-command-transport'
11
11
  import onDrop from './on-drop'
12
+ import copy from 'json-deep-copy'
13
+ import uid from '../../common/uid'
12
14
 
13
15
  const { Option } = Select
14
16
  const e = window.translate
@@ -45,17 +47,44 @@ export default class QuickCommandsList extends List {
45
47
  }
46
48
 
47
49
  handleDragEnter = e => {
48
- e.target.closest('.item-list-unit').classList.add('dragover')
50
+ e.target.closest('.item-list-unit').classList.add('qm-field-dragover')
49
51
  }
50
52
 
51
53
  handleDragLeave = e => {
52
- e.target.closest('.item-list-unit').classList.remove('dragover')
54
+ e.target.closest('.item-list-unit').classList.remove('qm-field-dragover')
53
55
  }
54
56
 
55
57
  handleDrop = e => {
56
58
  onDrop(e, '.item-list-unit')
57
59
  }
58
60
 
61
+ duplicateItem = (e, item) => {
62
+ e.stopPropagation()
63
+ const { store } = window
64
+ const newCommand = copy(item)
65
+ newCommand.id = uid()
66
+ const baseName = item.name.replace(/\(\d+\)$/, '')
67
+ const sameNameCount = store.currentQuickCommands.filter(
68
+ cmd => cmd.name && cmd.name.replace(/\(\d+\)$/, '').includes(baseName)
69
+ ).length
70
+ const duplicateIndex = sameNameCount > 0 ? sameNameCount : 1
71
+ newCommand.name = baseName + '(' + duplicateIndex + ')'
72
+ store.addQuickCommand(newCommand)
73
+ }
74
+
75
+ renderDuplicateBtn = (item) => {
76
+ if (!item.id) {
77
+ return null
78
+ }
79
+ return (
80
+ <CopyOutlined
81
+ title={e('duplicate')}
82
+ className='pointer list-item-duplicate'
83
+ onClick={(e) => this.duplicateItem(e, item)}
84
+ />
85
+ )
86
+ }
87
+
59
88
  renderItem = (item, i) => {
60
89
  if (!item) {
61
90
  return null
@@ -94,6 +123,7 @@ export default class QuickCommandsList extends List {
94
123
  }
95
124
  {title}
96
125
  </div>
126
+ {this.renderDuplicateBtn(item)}
97
127
  {this.renderDelBtn(item)}
98
128
  </div>
99
129
  )
@@ -7,6 +7,7 @@
7
7
  .list-item-apply
8
8
  .list-item-remove
9
9
  .list-item-bookmark
10
+ .list-item-duplicate
10
11
  display none
11
12
  width 24px
12
13
  line-height 35px
@@ -35,6 +36,7 @@
35
36
  .list-item-edit
36
37
  .list-item-remove
37
38
  .list-item-bookmark
39
+ .list-item-duplicate
38
40
  display block
39
41
  .theme-item:hover
40
42
  .list-item-remove
@@ -49,4 +51,6 @@
49
51
  // right 20px
50
52
  .item-list-unit
51
53
  .list-item-bookmark
52
- right 18px
54
+ right 18px
55
+ .list-item-duplicate
56
+ right 24px
@@ -164,7 +164,7 @@ export default class TerminalCmdSuggestions extends Component {
164
164
  }
165
165
 
166
166
  handleDelete = (item) => {
167
- window.store.terminalCommandHistory.delete(item.command)
167
+ window.store.deleteCmdHistory(item.command)
168
168
  }
169
169
 
170
170
  handleSelect = (item) => {
@@ -484,9 +484,22 @@ class Term extends Component {
484
484
  }
485
485
 
486
486
  onClear = () => {
487
+ const shouldClear = this.searchAddon &&
488
+ window.store.termSearchOpen &&
489
+ window.store.termSearch
490
+ if (
491
+ shouldClear
492
+ ) {
493
+ this.searchAddon.clearDecorations()
494
+ }
487
495
  this.term.clear()
488
496
  this.term.focus()
489
- // this.notifyOnData('')
497
+ if (shouldClear) {
498
+ this.searchAddon._linesCache = undefined
499
+ this.timers.clearSearchTimer = setTimeout(() => {
500
+ refsStatic.get('term-search')?.next()
501
+ }, 100)
502
+ }
490
503
  }
491
504
 
492
505
  isRemote = () => {
@@ -171,6 +171,10 @@ export default function TreeListItem (props) {
171
171
  props.onDragStart(e)
172
172
  }
173
173
 
174
+ const onDragEnter = e => {
175
+ props.onDragEnter(e)
176
+ }
177
+
174
178
  const onDragLeave = e => {
175
179
  props.onDragLeave(e)
176
180
  }
@@ -224,6 +228,7 @@ export default function TreeListItem (props) {
224
228
  'data-is-group': isGroup ? 'true' : 'false',
225
229
  onDragOver,
226
230
  onDragStart,
231
+ onDragEnter,
227
232
  onDragLeave,
228
233
  onDrop
229
234
  }
@@ -381,6 +381,18 @@ export default class ItemListTree extends Component {
381
381
  )
382
382
  }
383
383
 
384
+ onDragEnter = e => {
385
+ e.preventDefault()
386
+ let {
387
+ target
388
+ } = e
389
+ const tar = findParentBySel(target, '.tree-item')
390
+ if (tar) {
391
+ target = tar
392
+ }
393
+ target.classList.add('item-dragover-top')
394
+ }
395
+
384
396
  onDragLeave = e => {
385
397
  e.preventDefault()
386
398
  let {
@@ -390,7 +402,7 @@ export default class ItemListTree extends Component {
390
402
  if (tar) {
391
403
  target = tar
392
404
  }
393
- target.classList.remove('item-dragover')
405
+ target.classList.remove('item-dragover-top')
394
406
  }
395
407
 
396
408
  onDragOver = e => {
@@ -401,14 +413,14 @@ export default class ItemListTree extends Component {
401
413
  if (tar) {
402
414
  target = tar
403
415
  }
404
- target.classList.add('item-dragover')
416
+ target.classList.add('item-dragover-top')
405
417
  }
406
418
 
407
419
  onDrop = action(e => {
408
420
  e.preventDefault()
409
- const elems = document.querySelectorAll('.tree-item.item-dragover')
421
+ const elems = document.querySelectorAll('.tree-item.item-dragover-top')
410
422
  elems.forEach(elem => {
411
- elem.classList.remove('item-dragover')
423
+ elem.classList.remove('item-dragover-top')
412
424
  })
413
425
  let {
414
426
  target
@@ -662,6 +674,7 @@ export default class ItemListTree extends Component {
662
674
  'duplicateItem',
663
675
  'onDragStart',
664
676
  'onDrop',
677
+ 'onDragEnter',
665
678
  'onDragLeave',
666
679
  'onDragOver'
667
680
  ]
@@ -30,7 +30,7 @@
30
30
  vertical-align middle
31
31
  line-height 26px
32
32
  &.item-dragover-top
33
- border-top 1px solid #18d551
33
+ border 2px solid var(--primary)
34
34
  .tree-item-title
35
35
  flex-grow 1
36
36
  line-height 26px
@@ -18,6 +18,7 @@ import {
18
18
  import * as ls from '../common/safe-local-storage'
19
19
  import { refs, refsStatic } from '../components/common/ref'
20
20
  import { action } from 'manate'
21
+ import uid from '../common/uid'
21
22
  import deepCopy from 'json-deep-copy'
22
23
  import { aiConfigsArr } from '../components/ai/ai-config-props'
23
24
  import settingList from '../common/setting-list'
@@ -345,36 +346,35 @@ export default Store => {
345
346
  return
346
347
  }
347
348
  const { terminalCommandHistory } = window.store
348
- const existing = terminalCommandHistory.get(cmd)
349
+ const existing = terminalCommandHistory.find(item => item.cmd === cmd)
349
350
  if (existing) {
350
- // Use set() to trigger reactivity
351
- terminalCommandHistory.set(cmd, {
352
- count: existing.count + 1,
353
- lastUseTime: new Date().toISOString()
354
- })
351
+ existing.count = existing.count + 1
352
+ existing.lastUseTime = new Date().toISOString()
355
353
  } else {
356
- terminalCommandHistory.set(cmd, {
354
+ terminalCommandHistory.push({
355
+ id: uid(),
356
+ cmd,
357
357
  count: 1,
358
358
  lastUseTime: new Date().toISOString()
359
359
  })
360
360
  }
361
- if (terminalCommandHistory.size > 100) {
361
+ if (terminalCommandHistory.length > 200) {
362
362
  // Delete oldest 20 items when history exceeds 100
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])
367
- }
363
+ terminalCommandHistory.sort((a, b) => new Date(a.lastUseTime).getTime() - new Date(b.lastUseTime).getTime())
364
+ terminalCommandHistory.splice(0, 20)
368
365
  }
369
366
  })
370
367
 
371
368
  Store.prototype.deleteCmdHistory = function (cmd) {
372
369
  const { terminalCommandHistory } = window.store
373
- terminalCommandHistory.delete(cmd)
370
+ const idx = terminalCommandHistory.findIndex(item => item.cmd === cmd)
371
+ if (idx !== -1) {
372
+ terminalCommandHistory.splice(idx, 1)
373
+ }
374
374
  }
375
375
 
376
376
  Store.prototype.clearAllCmdHistory = function () {
377
- window.store.terminalCommandHistory = new Map()
377
+ window.store.terminalCommandHistory = []
378
378
  }
379
379
 
380
380
  Store.prototype.runCmdFromHistory = function (cmd) {
@@ -21,10 +21,8 @@ import {
21
21
  dismissDelKeyTipLsKey,
22
22
  qmSortByFrequencyKey,
23
23
  resolutionsLsKey,
24
- aiChatHistoryKey,
25
24
  syncServerDataKey,
26
- splitMap,
27
- cmdHistoryKey
25
+ splitMap
28
26
  } from '../common/constants'
29
27
  import * as ls from '../common/safe-local-storage'
30
28
  import { exclude } from 'manate'
@@ -54,7 +52,7 @@ export default () => {
54
52
  lastDataUpdateTime: 0,
55
53
  tabs: [],
56
54
  activeTabId: '',
57
- history: ls.safeGetItemJSON('history', []),
55
+ history: [],
58
56
  sshConfigs: [],
59
57
  bookmarks: [],
60
58
  bookmarksMap: new Map(),
@@ -74,32 +72,9 @@ export default () => {
74
72
  addressBookmarksLocal: ls.getItemJSON(localAddrBookmarkLsKey, []),
75
73
  openResolutionEdit: false,
76
74
  resolutions: ls.getItemJSON(resolutionsLsKey, []),
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.safeGetItemJSON(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
- })(),
75
+ // terminalCommandHistory: [{ id, cmd, count, lastUseTime }]
76
+ // Loaded from DB in initData
77
+ terminalCommandHistory: [],
103
78
 
104
79
  // workspaces
105
80
  workspaces: [],
@@ -111,7 +86,7 @@ export default () => {
111
86
 
112
87
  // batch input selected tab ids
113
88
  _batchInputSelectedTabIds: new Set(),
114
- aiChatHistory: ls.safeGetItemJSON(aiChatHistoryKey, []),
89
+ aiChatHistory: [],
115
90
 
116
91
  // sftp
117
92
  fileOperation: fileOperationsMap.cp, // cp or mv
@@ -7,6 +7,10 @@ import uid from '../common/uid'
7
7
  import { settingMap } from '../common/constants'
8
8
  import { refs } from '../components/common/ref'
9
9
  import deepCopy from 'json-deep-copy'
10
+ import {
11
+ getLocalFileInfo,
12
+ getRemoteFileInfo
13
+ } from '../components/sftp/file-read'
10
14
 
11
15
  export default Store => {
12
16
  // Initialize MCP handler - called when MCP widget is started
@@ -56,8 +60,7 @@ export default Store => {
56
60
  case 'add_bookmark_group':
57
61
  result = await store.mcpAddBookmarkGroup(args)
58
62
  break
59
-
60
- // Quick command operations
63
+ /*
61
64
  case 'list_quick_commands':
62
65
  result = store.mcpListQuickCommands()
63
66
  break
@@ -70,7 +73,7 @@ export default Store => {
70
73
  case 'delete_quick_command':
71
74
  result = store.mcpDeleteQuickCommand(args)
72
75
  break
73
-
76
+ */
74
77
  // Tab operations
75
78
  case 'list_tabs':
76
79
  result = store.mcpListTabs()
@@ -105,20 +108,34 @@ export default Store => {
105
108
  result = store.mcpGetTerminalOutput(args)
106
109
  break
107
110
 
108
- // History operations
109
- case 'list_history':
110
- result = store.mcpListHistory(args)
111
+ // SFTP operations
112
+ case 'sftp_list':
113
+ result = await store.mcpSftpList(args)
114
+ break
115
+ case 'sftp_del':
116
+ result = await store.mcpSftpDel(args)
111
117
  break
112
- case 'clear_history':
113
- result = store.mcpClearHistory()
118
+ case 'sftp_stat':
119
+ result = await store.mcpSftpStat(args)
120
+ break
121
+ case 'sftp_read_file':
122
+ result = await store.mcpSftpReadFile(args)
114
123
  break
115
124
 
116
- // Transfer operations
117
- case 'list_transfers':
118
- result = store.mcpListTransfers()
125
+ // File transfer operations
126
+ case 'sftp_upload':
127
+ result = await store.mcpSftpUpload(args)
119
128
  break
120
- case 'list_transfer_history':
121
- result = store.mcpListTransferHistory(args)
129
+ case 'sftp_download':
130
+ result = await store.mcpSftpDownload(args)
131
+ break
132
+
133
+ // Zmodem (trzsz/rzsz) operations
134
+ case 'zmodem_upload':
135
+ result = store.mcpZmodemUpload(args)
136
+ break
137
+ case 'zmodem_download':
138
+ result = store.mcpZmodemDownload(args)
122
139
  break
123
140
 
124
141
  // Settings operations
@@ -249,58 +266,58 @@ export default Store => {
249
266
 
250
267
  // ==================== Quick Command APIs ====================
251
268
 
252
- Store.prototype.mcpListQuickCommands = function () {
253
- return deepCopy(window.store.quickCommands)
254
- }
255
-
256
- Store.prototype.mcpAddQuickCommand = function (args) {
257
- const { store } = window
258
- const qm = {
259
- id: uid(),
260
- name: args.name,
261
- commands: args.commands,
262
- inputOnly: args.inputOnly || false,
263
- labels: args.labels || []
264
- }
265
-
266
- store.addQuickCommand(qm)
267
-
268
- return {
269
- success: true,
270
- id: qm.id,
271
- message: `Quick command "${qm.name}" created`
272
- }
273
- }
274
-
275
- Store.prototype.mcpRunQuickCommand = function (args) {
276
- const { store } = window
277
- const qm = store.quickCommands.find(q => q.id === args.id)
278
- if (!qm) {
279
- throw new Error(`Quick command not found: ${args.id}`)
280
- }
281
-
282
- store.runQuickCommandItem(args.id)
283
-
284
- return {
285
- success: true,
286
- message: `Executed quick command "${qm.name}"`
287
- }
288
- }
289
-
290
- Store.prototype.mcpDeleteQuickCommand = function (args) {
291
- const { store } = window
292
- const qm = store.quickCommands.find(q => q.id === args.id)
293
- if (!qm) {
294
- throw new Error(`Quick command not found: ${args.id}`)
295
- }
296
-
297
- store.delQuickCommand({ id: args.id })
298
-
299
- return {
300
- success: true,
301
- message: `Deleted quick command "${qm.name}"`
302
- }
303
- }
269
+ // Store.prototype.mcpListQuickCommands = function () {
270
+ // return deepCopy(window.store.quickCommands)
271
+ // }
272
+
273
+ // Store.prototype.mcpAddQuickCommand = function (args) {
274
+ // const { store } = window
275
+ // const qm = {
276
+ // id: uid(),
277
+ // name: args.name,
278
+ // commands: args.commands,
279
+ // inputOnly: args.inputOnly || false,
280
+ // labels: args.labels || []
281
+ // }
282
+
283
+ // store.addQuickCommand(qm)
284
+
285
+ // return {
286
+ // success: true,
287
+ // id: qm.id,
288
+ // message: `Quick command "${qm.name}" created`
289
+ // }
290
+ // }
291
+
292
+ // Store.prototype.mcpRunQuickCommand = function (args) {
293
+ // const { store } = window
294
+ // const qm = store.quickCommands.find(q => q.id === args.id)
295
+ // if (!qm) {
296
+ // throw new Error(`Quick command not found: ${args.id}`)
297
+ // }
298
+
299
+ // store.runQuickCommandItem(args.id)
300
+
301
+ // return {
302
+ // success: true,
303
+ // message: `Executed quick command "${qm.name}"`
304
+ // }
305
+ // }
306
+
307
+ // Store.prototype.mcpDeleteQuickCommand = function (args) {
308
+ // const { store } = window
309
+ // const qm = store.quickCommands.find(q => q.id === args.id)
310
+ // if (!qm) {
311
+ // throw new Error(`Quick command not found: ${args.id}`)
312
+ // }
313
+
314
+ // store.delQuickCommand({ id: args.id })
315
+
316
+ // return {
317
+ // success: true,
318
+ // message: `Deleted quick command "${qm.name}"`
319
+ // }
320
+ // }
304
321
 
305
322
  // ==================== Tab APIs ====================
306
323
 
@@ -502,92 +519,261 @@ export default Store => {
502
519
  }
503
520
  }
504
521
 
505
- // ==================== History APIs ====================
522
+ // ==================== Settings APIs ====================
506
523
 
507
- Store.prototype.mcpListHistory = function (args = {}) {
524
+ Store.prototype.mcpGetSettings = function () {
508
525
  const { store } = window
509
- const limit = args.limit || 50
510
- const history = store.history.slice(0, limit)
511
-
512
- return history.map(h => ({
513
- id: h.id,
514
- title: h.title,
515
- host: h.host,
516
- type: h.type,
517
- time: h.time
518
- }))
526
+ // Return safe settings (no sensitive data)
527
+ const config = store.config
528
+ const excludeKeys = ['apiKeyAI', 'syncSetting']
529
+ const safeConfig = Object.fromEntries(
530
+ Object.entries(config).filter(([key]) => !excludeKeys.includes(key))
531
+ )
532
+ return safeConfig
519
533
  }
520
534
 
521
- Store.prototype.mcpClearHistory = function () {
535
+ // ==================== SFTP APIs ====================
536
+
537
+ Store.prototype.mcpGetSshSftpRef = function (tabId) {
522
538
  const { store } = window
523
- store.history = []
539
+ const resolvedTabId = tabId || store.activeTabId
540
+ if (!resolvedTabId) {
541
+ throw new Error('No active tab')
542
+ }
543
+ const tab = store.tabs.find(t => t.id === resolvedTabId)
544
+ if (!tab) {
545
+ throw new Error(`Tab not found: ${resolvedTabId}`)
546
+ }
547
+ if (tab.type !== 'ssh' && tab.type !== 'ftp') {
548
+ throw new Error(`Tab "${resolvedTabId}" is not an SSH/SFTP tab (type: ${tab.type || 'local'})`)
549
+ }
550
+ const sftpEntry = refs.get('sftp-' + resolvedTabId)
551
+ if (!sftpEntry || !sftpEntry.sftp) {
552
+ throw new Error(`SFTP not initialized for tab "${resolvedTabId}". Open the SFTP panel first.`)
553
+ }
554
+ return { sftp: sftpEntry.sftp, tab, tabId: resolvedTabId }
555
+ }
524
556
 
525
- return {
526
- success: true,
527
- message: 'History cleared'
557
+ Store.prototype.mcpSftpList = async function (args) {
558
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
559
+ const remotePath = args.remotePath
560
+ if (!remotePath) {
561
+ throw new Error('remotePath is required')
528
562
  }
563
+ const list = await sftp.list(remotePath)
564
+ return { tabId, host: tab.host, path: remotePath, list }
529
565
  }
530
566
 
531
- // ==================== Transfer APIs ====================
567
+ Store.prototype.mcpSftpStat = async function (args) {
568
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
569
+ const remotePath = args.remotePath
570
+ if (!remotePath) {
571
+ throw new Error('remotePath is required')
572
+ }
573
+ const stat = await sftp.stat(remotePath)
574
+ return { tabId, host: tab.host, path: remotePath, stat }
575
+ }
532
576
 
533
- Store.prototype.mcpListTransfers = function () {
534
- const { store } = window
535
- return store.fileTransfers.map(t => ({
536
- id: t.id,
537
- localPath: t.localPath,
538
- remotePath: t.remotePath,
539
- type: t.type,
540
- percent: t.percent,
541
- status: t.status
542
- }))
577
+ Store.prototype.mcpSftpReadFile = async function (args) {
578
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
579
+ const remotePath = args.remotePath
580
+ if (!remotePath) {
581
+ throw new Error('remotePath is required')
582
+ }
583
+ const content = await sftp.readFile(remotePath)
584
+ return { tabId, host: tab.host, path: remotePath, content }
543
585
  }
544
586
 
545
- Store.prototype.mcpListTransferHistory = function (args = {}) {
546
- const { store } = window
547
- const limit = args.limit || 50
548
- return store.transferHistory.slice(0, limit).map(t => ({
549
- id: t.id,
550
- localPath: t.localPath,
551
- remotePath: t.remotePath,
552
- type: t.type,
553
- status: t.status,
554
- time: t.time
555
- }))
587
+ Store.prototype.mcpSftpDel = async function (args) {
588
+ const { sftp, tab, tabId } = window.store.mcpGetSshSftpRef(args.tabId)
589
+ const remotePath = args.remotePath
590
+ if (!remotePath) {
591
+ throw new Error('remotePath is required')
592
+ }
593
+ // Use stat to determine if it's a file or directory
594
+ const stat = await sftp.stat(remotePath)
595
+ const isDirectory = typeof stat.isDirectory === 'function'
596
+ ? stat.isDirectory()
597
+ : !!stat.isDirectory
598
+ if (isDirectory) {
599
+ await sftp.rmdir(remotePath)
600
+ } else {
601
+ await sftp.rm(remotePath)
602
+ }
603
+ return { success: true, tabId, host: tab.host, path: remotePath, type: isDirectory ? 'directory' : 'file' }
556
604
  }
557
605
 
558
- // ==================== Settings APIs ====================
606
+ // ==================== File Transfer APIs ====================
559
607
 
560
- Store.prototype.mcpGetSettings = function () {
608
+ Store.prototype.mcpSftpUpload = async function (args) {
561
609
  const { store } = window
562
- // Return safe settings (no sensitive data)
563
- const config = store.config
564
- const safeConfig = {
565
- theme: config.theme,
566
- language: config.language,
567
- fontSize: config.fontSize,
568
- fontFamily: config.fontFamily,
569
- terminalType: config.terminalType,
570
- cursorStyle: config.cursorStyle,
571
- cursorBlink: config.cursorBlink,
572
- scrollback: config.scrollback
610
+ const { tab, tabId } = store.mcpGetSshSftpRef(args.tabId)
611
+ const localPath = args.localPath
612
+ const remotePath = args.remotePath
613
+ if (!localPath) {
614
+ throw new Error('localPath is required')
615
+ }
616
+ if (!remotePath) {
617
+ throw new Error('remotePath is required')
618
+ }
619
+
620
+ window._transferConflictPolicy = args.conflictPolicy || 'overwrite'
621
+
622
+ const fromFile = await getLocalFileInfo(localPath)
623
+ const transferItem = {
624
+ host: tab.host,
625
+ tabType: tab.type || 'ssh',
626
+ typeFrom: 'local',
627
+ typeTo: 'remote',
628
+ fromPath: localPath,
629
+ toPath: remotePath,
630
+ fromFile: {
631
+ ...fromFile,
632
+ host: tab.host,
633
+ tabType: tab.type || 'ssh',
634
+ tabId,
635
+ title: tab.title
636
+ },
637
+ id: uid(),
638
+ title: tab.title,
639
+ tabId,
640
+ operation: ''
641
+ }
642
+
643
+ store.addTransferList([transferItem])
644
+
645
+ return {
646
+ success: true,
647
+ message: `Upload started: ${localPath} → ${tab.host}:${remotePath}`,
648
+ transferId: transferItem.id,
649
+ tabId
573
650
  }
574
- return safeConfig
575
651
  }
576
652
 
577
- Store.prototype.mcpListTerminalThemes = function () {
653
+ Store.prototype.mcpSftpDownload = async function (args) {
578
654
  const { store } = window
579
- return store.terminalThemes.map(t => ({
580
- id: t.id,
581
- name: t.name,
582
- themeLight: t.themeLight
583
- }))
655
+ const { sftp, tab, tabId } = store.mcpGetSshSftpRef(args.tabId) // sftp used for getRemoteFileInfo
656
+ const remotePath = args.remotePath
657
+ const localPath = args.localPath
658
+ if (!remotePath) {
659
+ throw new Error('remotePath is required')
660
+ }
661
+ if (!localPath) {
662
+ throw new Error('localPath is required')
663
+ }
664
+
665
+ window._transferConflictPolicy = args.conflictPolicy || 'overwrite'
666
+
667
+ const fromFile = await getRemoteFileInfo(sftp, remotePath)
668
+ const transferItem = {
669
+ host: tab.host,
670
+ tabType: tab.type || 'ssh',
671
+ typeFrom: 'remote',
672
+ typeTo: 'local',
673
+ fromPath: remotePath,
674
+ toPath: localPath,
675
+ fromFile: {
676
+ ...fromFile,
677
+ id: uid(),
678
+ isSymbolicLink: false
679
+ },
680
+ id: uid(),
681
+ title: tab.title,
682
+ tabId
683
+ }
684
+
685
+ store.addTransferList([transferItem])
686
+
687
+ return {
688
+ success: true,
689
+ message: `Download started: ${tab.host}:${remotePath} → ${localPath}`,
690
+ transferId: transferItem.id,
691
+ tabId
692
+ }
584
693
  }
585
694
 
586
- Store.prototype.mcpListUiThemes = function () {
695
+ // ==================== Zmodem (trzsz/rzsz) APIs ====================
696
+
697
+ Store.prototype.mcpZmodemUpload = function (args) {
587
698
  const { store } = window
588
- return (store.uiThemes || []).map(t => ({
589
- id: t.id,
590
- name: t.name
591
- }))
699
+ const tabId = args.tabId || store.activeTabId
700
+ if (!tabId) {
701
+ throw new Error('No active tab')
702
+ }
703
+ const tab = store.tabs.find(t => t.id === tabId)
704
+ if (!tab) {
705
+ throw new Error(`Tab not found: ${tabId}`)
706
+ }
707
+
708
+ const files = args.files
709
+ if (!files || !Array.isArray(files) || files.length === 0) {
710
+ throw new Error('files array is required (list of local file paths to upload)')
711
+ }
712
+
713
+ const protocol = args.protocol || 'rzsz'
714
+ const uploadCmd = protocol === 'trzsz' ? 'trz' : 'rz'
715
+
716
+ // Set the control variable to bypass native file dialog
717
+ window._apiControlSelectFile = files
718
+
719
+ const term = refs.get('term-' + tabId)
720
+ if (!term) {
721
+ throw new Error(`Terminal not found for tab: ${tabId}`)
722
+ }
723
+ term.runQuickCommand(uploadCmd)
724
+
725
+ return {
726
+ success: true,
727
+ protocol,
728
+ command: uploadCmd,
729
+ message: `${uploadCmd} upload initiated for ${files.length} file(s)`,
730
+ files,
731
+ tabId
732
+ }
733
+ }
734
+
735
+ Store.prototype.mcpZmodemDownload = function (args) {
736
+ const { store } = window
737
+ const tabId = args.tabId || store.activeTabId
738
+ if (!tabId) {
739
+ throw new Error('No active tab')
740
+ }
741
+ const tab = store.tabs.find(t => t.id === tabId)
742
+ if (!tab) {
743
+ throw new Error(`Tab not found: ${tabId}`)
744
+ }
745
+
746
+ const saveFolder = args.saveFolder
747
+ if (!saveFolder) {
748
+ throw new Error('saveFolder is required (local folder to save downloaded files)')
749
+ }
750
+
751
+ const remoteFiles = args.remoteFiles
752
+ if (!remoteFiles || !Array.isArray(remoteFiles) || remoteFiles.length === 0) {
753
+ throw new Error('remoteFiles array is required (list of remote file paths to download)')
754
+ }
755
+
756
+ const protocol = args.protocol || 'rzsz'
757
+ const downloadCmd = protocol === 'trzsz' ? 'tsz' : 'sz'
758
+
759
+ // Set the control variable to bypass native folder dialog
760
+ window._apiControlSelectFolder = saveFolder
761
+
762
+ const term = refs.get('term-' + tabId)
763
+ if (!term) {
764
+ throw new Error(`Terminal not found for tab: ${tabId}`)
765
+ }
766
+ const quotedFiles = remoteFiles.map(f => `"${f}"`).join(' ')
767
+ term.runQuickCommand(`${downloadCmd} ${quotedFiles}`)
768
+
769
+ return {
770
+ success: true,
771
+ protocol,
772
+ command: downloadCmd,
773
+ message: `${downloadCmd} download initiated for ${remoteFiles.length} file(s) to ${saveFolder}`,
774
+ remoteFiles,
775
+ saveFolder,
776
+ tabId
777
+ }
592
778
  }
593
779
  }
@@ -176,7 +176,7 @@ class Store {
176
176
 
177
177
  get terminalCommandSuggestions () {
178
178
  const { store } = window
179
- const historyCommands = Array.from(store.terminalCommandHistory.keys())
179
+ const historyCommands = store.terminalCommandHistory.map(item => item.cmd)
180
180
  const batchInputCommands = store.batchInputs || []
181
181
  const quickCommands = (store.quickCommands || []).reduce(
182
182
  (p, q) => {
@@ -21,6 +21,7 @@ export default Store => {
21
21
  }
22
22
 
23
23
  Store.prototype.addTransferList = function (items) {
24
+ // console.log('addTransferList', JSON.stringify(items, null, 2))
24
25
  const { fileTransfers } = window.store
25
26
  const transferBatch = uid()
26
27
  const nextItems = items.map(t => {
@@ -11,9 +11,7 @@ import {
11
11
  expandedKeysLsKey,
12
12
  resolutionsLsKey,
13
13
  localAddrBookmarkLsKey,
14
- syncServerDataKey,
15
- aiChatHistoryKey,
16
- cmdHistoryKey
14
+ syncServerDataKey
17
15
  } from '../common/constants'
18
16
  import * as ls from '../common/safe-local-storage'
19
17
  import { debounce, isEmpty } from 'lodash-es'
@@ -134,28 +132,6 @@ export default store => {
134
132
  return store.syncServerStatus
135
133
  }).start()
136
134
 
137
- autoRun(() => {
138
- ls.safeSetItemJSON('history', store.history)
139
- return store.history
140
- }).start()
141
-
142
- autoRun(() => {
143
- ls.safeSetItemJSON(aiChatHistoryKey, store.aiChatHistory)
144
- return store.aiChatHistory
145
- }).start()
146
-
147
- autoRun(() => {
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.safeSetItemJSON(cmdHistoryKey, data)
156
- return store.terminalCommandHistory
157
- }).start()
158
-
159
135
  autoRun(() => {
160
136
  store.updateBatchInputSelectedTabIds()
161
137
  const tabs = store.getTabs()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.1.6",
3
+ "version": "3.1.16",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",