@electerm/electerm-react 3.11.12 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/common/bookmark-schemas.js +2 -1
- package/client/common/constants.js +11 -2
- package/client/components/ai/agent-tools.js +204 -0
- package/client/components/ai/agent.js +12 -10
- package/client/components/ai/ai-chat-history-item.jsx +14 -23
- package/client/components/ai/ai-chat.jsx +24 -9
- package/client/components/bookmark-form/bookmark-schema.js +2 -1
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/footer/cmd-history.jsx +20 -11
- package/client/components/shortcuts/shortcut-handler.js +7 -0
- package/client/components/sidebar/history.jsx +16 -8
- package/client/components/terminal/attach-addon-custom.js +1 -1
- package/client/components/terminal/drop-file-modal.jsx +53 -22
- package/client/components/terminal/terminal.jsx +68 -1
- package/client/components/terminal/xmodem-client.js +244 -0
- package/client/components/terminal-info/data-cols-parser.jsx +2 -1
- package/client/components/terminal-info/disk.jsx +4 -2
- package/client/components/terminal-info/network.jsx +3 -1
- package/client/components/terminal-info/resource.jsx +3 -3
- package/client/components/tree-list/tree-list.styl +7 -1
- package/client/store/mcp-handler.js +41 -9
- package/client/store/quick-command.js +3 -2
- package/client/store/watch.js +1 -1
- package/package.json +1 -1
|
@@ -96,7 +96,7 @@ export default class AttachAddonCustom {
|
|
|
96
96
|
if (typeof ev.data === 'string') {
|
|
97
97
|
try {
|
|
98
98
|
const msg = JSON.parse(ev.data)
|
|
99
|
-
if (msg.action === 'zmodem-event' || msg.action === 'trzsz-event') {
|
|
99
|
+
if (msg.action === 'zmodem-event' || msg.action === 'trzsz-event' || msg.action === 'xmodem-event') {
|
|
100
100
|
return
|
|
101
101
|
}
|
|
102
102
|
} catch (e) {}
|
|
@@ -4,11 +4,62 @@ import Modal from '../common/modal'
|
|
|
4
4
|
const e = window.translate
|
|
5
5
|
|
|
6
6
|
export class DropFileModal extends Component {
|
|
7
|
+
renderSerialFooter () {
|
|
8
|
+
const { onSelect } = this.props
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<button
|
|
12
|
+
type='button'
|
|
13
|
+
className='custom-modal-ok-btn'
|
|
14
|
+
onClick={() => onSelect('xmodem')}
|
|
15
|
+
>
|
|
16
|
+
XMODEM
|
|
17
|
+
</button>
|
|
18
|
+
<button
|
|
19
|
+
type='button'
|
|
20
|
+
className='custom-modal-cancel-btn'
|
|
21
|
+
onClick={() => onSelect('inputOnly')}
|
|
22
|
+
>
|
|
23
|
+
{e('inputOnly')}
|
|
24
|
+
</button>
|
|
25
|
+
</>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
renderSshFooter () {
|
|
30
|
+
const { onSelect } = this.props
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<button
|
|
34
|
+
type='button'
|
|
35
|
+
className='custom-modal-ok-btn'
|
|
36
|
+
onClick={() => onSelect('trz')}
|
|
37
|
+
>
|
|
38
|
+
trz
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type='button'
|
|
42
|
+
className='custom-modal-cancel-btn'
|
|
43
|
+
onClick={() => onSelect('rz')}
|
|
44
|
+
>
|
|
45
|
+
rz
|
|
46
|
+
</button>
|
|
47
|
+
<button
|
|
48
|
+
type='button'
|
|
49
|
+
className='custom-modal-cancel-btn'
|
|
50
|
+
onClick={() => onSelect('inputOnly')}
|
|
51
|
+
>
|
|
52
|
+
{e('inputOnly')}
|
|
53
|
+
</button>
|
|
54
|
+
</>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
7
58
|
render () {
|
|
8
59
|
const {
|
|
9
60
|
visible,
|
|
10
61
|
files,
|
|
11
|
-
|
|
62
|
+
isSerial,
|
|
12
63
|
onCancel
|
|
13
64
|
} = this.props
|
|
14
65
|
|
|
@@ -23,27 +74,7 @@ export class DropFileModal extends Component {
|
|
|
23
74
|
onCancel={onCancel}
|
|
24
75
|
footer={
|
|
25
76
|
<div className='custom-modal-footer-buttons'>
|
|
26
|
-
|
|
27
|
-
type='button'
|
|
28
|
-
className='custom-modal-ok-btn'
|
|
29
|
-
onClick={() => onSelect('trz')}
|
|
30
|
-
>
|
|
31
|
-
trz
|
|
32
|
-
</button>
|
|
33
|
-
<button
|
|
34
|
-
type='button'
|
|
35
|
-
className='custom-modal-cancel-btn'
|
|
36
|
-
onClick={() => onSelect('rz')}
|
|
37
|
-
>
|
|
38
|
-
rz
|
|
39
|
-
</button>
|
|
40
|
-
<button
|
|
41
|
-
type='button'
|
|
42
|
-
className='custom-modal-cancel-btn'
|
|
43
|
-
onClick={() => onSelect('inputOnly')}
|
|
44
|
-
>
|
|
45
|
-
{e('inputOnly')}
|
|
46
|
-
</button>
|
|
77
|
+
{isSerial ? this.renderSerialFooter() : this.renderSshFooter()}
|
|
47
78
|
</div>
|
|
48
79
|
}
|
|
49
80
|
width={400}
|
|
@@ -26,6 +26,7 @@ import AttachAddon from './attach-addon-custom.js'
|
|
|
26
26
|
import getProxy from '../../common/get-proxy.js'
|
|
27
27
|
import { ZmodemClient } from './zmodem-client.js'
|
|
28
28
|
import { TrzszClient } from './trzsz-client.js'
|
|
29
|
+
import { XmodemClient } from './xmodem-client.js'
|
|
29
30
|
import DropFileModal from './drop-file-modal.jsx'
|
|
30
31
|
import keyControlPressed from '../../common/key-control-pressed.js'
|
|
31
32
|
import NormalBuffer from './normal-buffer.jsx'
|
|
@@ -165,6 +166,7 @@ class Term extends Component {
|
|
|
165
166
|
this.fitAddon = null
|
|
166
167
|
this.zmodemClient = null
|
|
167
168
|
this.trzszClient = null
|
|
169
|
+
this.xmodemClient = null
|
|
168
170
|
this.searchAddon = null
|
|
169
171
|
this.fitAddon = null
|
|
170
172
|
this.cmdAddon = null
|
|
@@ -391,6 +393,7 @@ class Term extends Component {
|
|
|
391
393
|
const fromFile = dt.getData('fromFile')
|
|
392
394
|
const notSafeMsg = 'File name contains unsafe characters'
|
|
393
395
|
const isSshTerminal = this.props.tab.type === connectionMap.ssh
|
|
396
|
+
const isSerialTerminal = this.props.tab.type === connectionMap.serial
|
|
394
397
|
|
|
395
398
|
if (fromFile) {
|
|
396
399
|
try {
|
|
@@ -412,6 +415,13 @@ class Term extends Component {
|
|
|
412
415
|
}
|
|
413
416
|
return
|
|
414
417
|
}
|
|
418
|
+
if (isSerialTerminal) {
|
|
419
|
+
this.setState({
|
|
420
|
+
dropFileModalVisible: true,
|
|
421
|
+
droppedFiles: [{ path: filePath, isRemote: false }]
|
|
422
|
+
})
|
|
423
|
+
return
|
|
424
|
+
}
|
|
415
425
|
this.attachAddon._sendData(`"${filePath}" `)
|
|
416
426
|
return
|
|
417
427
|
} catch (e) {
|
|
@@ -443,6 +453,14 @@ class Term extends Component {
|
|
|
443
453
|
return
|
|
444
454
|
}
|
|
445
455
|
|
|
456
|
+
if (isSerialTerminal) {
|
|
457
|
+
this.setState({
|
|
458
|
+
dropFileModalVisible: true,
|
|
459
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
460
|
+
})
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
446
464
|
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
447
465
|
this.attachAddon._sendData(filesAll)
|
|
448
466
|
}
|
|
@@ -485,6 +503,19 @@ class Term extends Component {
|
|
|
485
503
|
this.attachAddon._sendData('rz\r')
|
|
486
504
|
break
|
|
487
505
|
}
|
|
506
|
+
case 'xmodem': {
|
|
507
|
+
if (this.xmodemClient && this.xmodemClient.isActive) {
|
|
508
|
+
message.warning('A transfer is already in progress')
|
|
509
|
+
this.handleDropFileModalCancel()
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
// Use XMODEM send with the dropped files
|
|
513
|
+
window._apiControlSelectFile = filePaths
|
|
514
|
+
if (this.xmodemClient) {
|
|
515
|
+
this.xmodemClient.initiateSend()
|
|
516
|
+
}
|
|
517
|
+
break
|
|
518
|
+
}
|
|
488
519
|
case 'inputOnly':
|
|
489
520
|
default: {
|
|
490
521
|
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
@@ -653,7 +684,8 @@ class Term extends Component {
|
|
|
653
684
|
const clearShortcut = this.getShortcut('terminal_clear')
|
|
654
685
|
const searchShortcut = this.getShortcut('terminal_search')
|
|
655
686
|
const selectAllShortcut = isMacJs ? 'meta+a' : 'ctrl+shift+a'
|
|
656
|
-
|
|
687
|
+
const isSerial = this.props.tab?.type === connectionMap.serial
|
|
688
|
+
const items = [
|
|
657
689
|
{
|
|
658
690
|
key: 'onCopy',
|
|
659
691
|
icon: <iconsMap.CopyOutlined />,
|
|
@@ -700,12 +732,44 @@ class Term extends Component {
|
|
|
700
732
|
extra: searchShortcut
|
|
701
733
|
}
|
|
702
734
|
]
|
|
735
|
+
if (isSerial) {
|
|
736
|
+
items.push(
|
|
737
|
+
{
|
|
738
|
+
type: 'divider'
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
key: 'onXmodemSend',
|
|
742
|
+
icon: <iconsMap.CloudUploadOutlined />,
|
|
743
|
+
label: 'XMODEM Send'
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
key: 'onXmodemReceive',
|
|
747
|
+
icon: <iconsMap.CloudDownloadOutlined />,
|
|
748
|
+
label: 'XMODEM Receive'
|
|
749
|
+
}
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
return items
|
|
703
753
|
}
|
|
704
754
|
|
|
705
755
|
onContextMenu = ({ key }) => {
|
|
706
756
|
this[key]()
|
|
707
757
|
}
|
|
708
758
|
|
|
759
|
+
onXmodemSend = () => {
|
|
760
|
+
if (this.xmodemClient) {
|
|
761
|
+
this.xmodemClient.initiateSend()
|
|
762
|
+
}
|
|
763
|
+
this.term.focus()
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
onXmodemReceive = () => {
|
|
767
|
+
if (this.xmodemClient) {
|
|
768
|
+
this.xmodemClient.initiateReceive()
|
|
769
|
+
}
|
|
770
|
+
this.term.focus()
|
|
771
|
+
}
|
|
772
|
+
|
|
709
773
|
notifyOnData = debounce(() => {
|
|
710
774
|
window.store.notifyTabOnData(this.props.tab.id)
|
|
711
775
|
}, 1000)
|
|
@@ -1320,6 +1384,8 @@ class Term extends Component {
|
|
|
1320
1384
|
this.zmodemClient.init(socket)
|
|
1321
1385
|
this.trzszClient = new TrzszClient(this)
|
|
1322
1386
|
this.trzszClient.init(socket)
|
|
1387
|
+
this.xmodemClient = new XmodemClient(this)
|
|
1388
|
+
this.xmodemClient.init(socket)
|
|
1323
1389
|
this.fitAddon.fit()
|
|
1324
1390
|
term.displayRaw = displayRaw
|
|
1325
1391
|
term.loadAddon(
|
|
@@ -1581,6 +1647,7 @@ class Term extends Component {
|
|
|
1581
1647
|
<DropFileModal
|
|
1582
1648
|
visible={this.state.dropFileModalVisible}
|
|
1583
1649
|
files={this.state.droppedFiles}
|
|
1650
|
+
isSerial={this.props.tab?.type === connectionMap.serial}
|
|
1584
1651
|
onSelect={this.handleDropFileAction}
|
|
1585
1652
|
onCancel={this.handleDropFileModalCancel}
|
|
1586
1653
|
/>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMODEM client handler for web terminal
|
|
3
|
+
* Handles UI interactions and communicates with server-side xmodem
|
|
4
|
+
* Unlike zmodem/trzsz, XMODEM requires explicit user initiation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { throttle } from 'lodash-es'
|
|
8
|
+
import { filesize } from 'filesize'
|
|
9
|
+
import { TransferClientBase } from './transfer-client-base.js'
|
|
10
|
+
import { transferTypeMap } from '../../common/constants.js'
|
|
11
|
+
|
|
12
|
+
const XMODEM_SAVE_PATH_KEY = 'xmodem-save-path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* XmodemClient class handles XMODEM UI and client-side logic
|
|
16
|
+
*/
|
|
17
|
+
export class XmodemClient extends TransferClientBase {
|
|
18
|
+
constructor (terminal) {
|
|
19
|
+
super(terminal, XMODEM_SAVE_PATH_KEY)
|
|
20
|
+
this.transferStartTime = 0
|
|
21
|
+
this.pendingMode = null // 'send' or 'receive' - waiting for user action
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the action name for this protocol
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
getActionName () {
|
|
29
|
+
return 'xmodem-event'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get protocol display name
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
getProtocolDisplayName () {
|
|
37
|
+
return 'XMODEM'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle server xmodem events
|
|
42
|
+
* @param {Object} msg - Message from server
|
|
43
|
+
*/
|
|
44
|
+
handleServerEvent (msg) {
|
|
45
|
+
const { event } = msg
|
|
46
|
+
|
|
47
|
+
switch (event) {
|
|
48
|
+
case 'receive-start':
|
|
49
|
+
this.onReceiveStart()
|
|
50
|
+
break
|
|
51
|
+
case 'send-start':
|
|
52
|
+
this.onSendStart()
|
|
53
|
+
break
|
|
54
|
+
case 'file-start':
|
|
55
|
+
this.onFileStart(msg.name, msg.size)
|
|
56
|
+
break
|
|
57
|
+
case 'progress':
|
|
58
|
+
this.onProgress(msg)
|
|
59
|
+
break
|
|
60
|
+
case 'file-complete':
|
|
61
|
+
this.onFileComplete(msg.name, msg.path)
|
|
62
|
+
break
|
|
63
|
+
case 'session-end':
|
|
64
|
+
this.onSessionEnd()
|
|
65
|
+
break
|
|
66
|
+
case 'session-error':
|
|
67
|
+
this.onError(msg.error)
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initiate XMODEM receive from UI
|
|
74
|
+
* User must have already started `sx` (or similar) on the remote device
|
|
75
|
+
*/
|
|
76
|
+
async initiateReceive () {
|
|
77
|
+
if (this.isActive) {
|
|
78
|
+
this.writeToTerminal('\r\n\x1b[33m\x1b[1mXMODEM: Transfer already in progress\x1b[0m\r\n')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.isActive = true
|
|
83
|
+
this.writeBanner('RECEIVE', null)
|
|
84
|
+
|
|
85
|
+
// Ask user for save directory
|
|
86
|
+
const savePath = await this.openSaveFolderSelect()
|
|
87
|
+
if (savePath) {
|
|
88
|
+
this.savePath = savePath
|
|
89
|
+
this.sendToServer({
|
|
90
|
+
event: 'start-receive'
|
|
91
|
+
})
|
|
92
|
+
// Also send save path
|
|
93
|
+
this.sendToServer({
|
|
94
|
+
event: 'set-save-path',
|
|
95
|
+
path: savePath
|
|
96
|
+
})
|
|
97
|
+
} else {
|
|
98
|
+
// User cancelled
|
|
99
|
+
this.isActive = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Initiate XMODEM send from UI
|
|
105
|
+
* User must have already started `rx` (or similar) on the remote device
|
|
106
|
+
*/
|
|
107
|
+
async initiateSend () {
|
|
108
|
+
if (this.isActive) {
|
|
109
|
+
this.writeToTerminal('\r\n\x1b[33m\x1b[1mXMODEM: Transfer already in progress\x1b[0m\r\n')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.isActive = true
|
|
114
|
+
this.writeBanner('SEND', null)
|
|
115
|
+
|
|
116
|
+
// Ask user to select files
|
|
117
|
+
const files = await this.openFileSelect({
|
|
118
|
+
title: 'Choose file(s) to send via XMODEM',
|
|
119
|
+
message: 'Choose file(s) to send via XMODEM'
|
|
120
|
+
})
|
|
121
|
+
if (files && files.length > 0) {
|
|
122
|
+
this.sendToServer({
|
|
123
|
+
event: 'start-send'
|
|
124
|
+
})
|
|
125
|
+
// Also send files
|
|
126
|
+
this.sendToServer({
|
|
127
|
+
event: 'send-files',
|
|
128
|
+
files
|
|
129
|
+
})
|
|
130
|
+
} else {
|
|
131
|
+
// User cancelled
|
|
132
|
+
this.isActive = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle receive session start (from server)
|
|
138
|
+
*/
|
|
139
|
+
onReceiveStart () {
|
|
140
|
+
// Server confirmed it's ready to receive
|
|
141
|
+
this.writeToTerminal('\r\n\x1b[36mWaiting for remote to start sending file...\x1b[0m\r\n')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle send session start (from server)
|
|
146
|
+
*/
|
|
147
|
+
onSendStart () {
|
|
148
|
+
// Server confirmed it's ready to send
|
|
149
|
+
this.writeToTerminal('\r\n\x1b[36mWaiting for remote to request file (NAK/C)...\x1b[0m\r\n')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Handle file start event
|
|
154
|
+
* @param {string} name - File name
|
|
155
|
+
* @param {number} size - File size
|
|
156
|
+
*/
|
|
157
|
+
onFileStart (name, size) {
|
|
158
|
+
this.currentTransfer = {
|
|
159
|
+
name,
|
|
160
|
+
size,
|
|
161
|
+
transferred: 0,
|
|
162
|
+
type: this.savePath ? transferTypeMap.download : transferTypeMap.upload,
|
|
163
|
+
path: null
|
|
164
|
+
}
|
|
165
|
+
this.transferStartTime = Date.now()
|
|
166
|
+
this.writeProgress()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle progress update
|
|
171
|
+
* @param {Object} msg - Progress message
|
|
172
|
+
*/
|
|
173
|
+
onProgress (msg) {
|
|
174
|
+
if (!this.currentTransfer) {
|
|
175
|
+
this.currentTransfer = {
|
|
176
|
+
name: msg.name,
|
|
177
|
+
size: msg.size,
|
|
178
|
+
transferred: 0,
|
|
179
|
+
type: msg.type,
|
|
180
|
+
path: msg.path || null
|
|
181
|
+
}
|
|
182
|
+
this.transferStartTime = Date.now()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.currentTransfer.transferred = msg.transferred
|
|
186
|
+
this.currentTransfer.serverSpeed = msg.speed
|
|
187
|
+
this.currentTransfer.path = msg.path || this.currentTransfer.path
|
|
188
|
+
this.writeProgress()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle file complete
|
|
193
|
+
* @param {string} name - File name
|
|
194
|
+
* @param {string} path - File path
|
|
195
|
+
*/
|
|
196
|
+
onFileComplete (name, path) {
|
|
197
|
+
if (this.currentTransfer) {
|
|
198
|
+
this.currentTransfer.transferred = this.currentTransfer.size || this.currentTransfer.transferred
|
|
199
|
+
this.currentTransfer.path = path
|
|
200
|
+
this._doWriteProgress(true)
|
|
201
|
+
this.writeToTerminal('\r\n')
|
|
202
|
+
this._prevProgressRows = 0
|
|
203
|
+
}
|
|
204
|
+
this.currentTransfer = null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handle error from server
|
|
209
|
+
* @param {string} error - Error message
|
|
210
|
+
*/
|
|
211
|
+
onError (error) {
|
|
212
|
+
this.writeToTerminal(`\r\n\x1b[31m\x1b[1mXMODEM Error: ${error}\x1b[0m\r\n`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Write progress to terminal (throttled)
|
|
217
|
+
*/
|
|
218
|
+
writeProgress = throttle((isComplete = false) => {
|
|
219
|
+
this._doWriteProgress(isComplete)
|
|
220
|
+
}, 500)
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Internal function to actually write progress
|
|
224
|
+
*/
|
|
225
|
+
_doWriteProgress (isComplete = false) {
|
|
226
|
+
if (!this.currentTransfer || !this.terminal?.term) return
|
|
227
|
+
|
|
228
|
+
const { name, size, transferred, path, serverSpeed } = this.currentTransfer
|
|
229
|
+
const speed = serverSpeed || 0
|
|
230
|
+
const displayName = path || name
|
|
231
|
+
const formatSize = (bytes) => filesize(bytes)
|
|
232
|
+
|
|
233
|
+
this.writeProgressBar({
|
|
234
|
+
name: displayName,
|
|
235
|
+
size,
|
|
236
|
+
transferred,
|
|
237
|
+
speed,
|
|
238
|
+
isComplete,
|
|
239
|
+
formatSize
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export default XmodemClient
|
|
@@ -24,7 +24,8 @@ export default (data) => {
|
|
|
24
24
|
return Object.keys(data).map(k => {
|
|
25
25
|
const rd = (txt) => {
|
|
26
26
|
// txt is in KB, convert to bytes for filesize function
|
|
27
|
-
const
|
|
27
|
+
const parsed = parseInt(txt, 10)
|
|
28
|
+
const r = k === 'mem' ? filesize(Number.isFinite(parsed) ? parsed * 1024 : 0) : txt
|
|
28
29
|
const itemProps = {
|
|
29
30
|
className: 'activity-item pointer',
|
|
30
31
|
'data-content': r,
|
|
@@ -14,10 +14,12 @@ export default function TerminalInfoDisk (props) {
|
|
|
14
14
|
}
|
|
15
15
|
const col = colsParser(disks[0])
|
|
16
16
|
disks.sort((a, b) => {
|
|
17
|
-
|
|
17
|
+
const af = a.filesystem || ''
|
|
18
|
+
const bf = b.filesystem || ''
|
|
19
|
+
if (af.startsWith('/') && !bf.startsWith('/')) {
|
|
18
20
|
return -1
|
|
19
21
|
}
|
|
20
|
-
if (!
|
|
22
|
+
if (!af.startsWith('/') && bf.startsWith('/')) {
|
|
21
23
|
return 1
|
|
22
24
|
}
|
|
23
25
|
return 0
|
|
@@ -34,6 +34,7 @@ export default function TerminalInfoDisk (props) {
|
|
|
34
34
|
const p = network[k]
|
|
35
35
|
const pv = net[k]
|
|
36
36
|
if (
|
|
37
|
+
diff > 0 &&
|
|
37
38
|
p &&
|
|
38
39
|
pv &&
|
|
39
40
|
p.download &&
|
|
@@ -43,6 +44,7 @@ export default function TerminalInfoDisk (props) {
|
|
|
43
44
|
p.down = Math.floor((p.download - pv.download) / diff)
|
|
44
45
|
}
|
|
45
46
|
if (
|
|
47
|
+
diff > 0 &&
|
|
46
48
|
p &&
|
|
47
49
|
pv &&
|
|
48
50
|
p.upload &&
|
|
@@ -92,7 +94,7 @@ export default function TerminalInfoDisk (props) {
|
|
|
92
94
|
},
|
|
93
95
|
render: (v) => {
|
|
94
96
|
if (k === 'up' || k === 'down') {
|
|
95
|
-
return filesize(v
|
|
97
|
+
return filesize(Number.isFinite(v) ? v : 0)
|
|
96
98
|
}
|
|
97
99
|
return v
|
|
98
100
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* cpu/swap/mem general usage
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { isEmpty
|
|
5
|
+
import { isEmpty } from 'lodash-es'
|
|
6
6
|
import { Progress } from 'antd'
|
|
7
7
|
import parseInt10 from '../../common/parse-int10'
|
|
8
8
|
|
|
@@ -51,7 +51,7 @@ export default function TerminalInfoResource (props) {
|
|
|
51
51
|
percent,
|
|
52
52
|
name
|
|
53
53
|
} = obj
|
|
54
|
-
const hasPercent =
|
|
54
|
+
const hasPercent = Number.isFinite(percent)
|
|
55
55
|
const p = hasPercent
|
|
56
56
|
? percent
|
|
57
57
|
: computePercent(used, total) || 0
|
|
@@ -74,7 +74,7 @@ export default function TerminalInfoResource (props) {
|
|
|
74
74
|
if (terminalInfos.includes('cpu')) {
|
|
75
75
|
data.push({
|
|
76
76
|
name: 'cpu',
|
|
77
|
-
percent: parseInt10(cpu)
|
|
77
|
+
percent: parseInt10(cpu) || 0
|
|
78
78
|
})
|
|
79
79
|
}
|
|
80
80
|
if (terminalInfos.includes('mem')) {
|
|
@@ -118,10 +118,16 @@
|
|
|
118
118
|
visibility hidden
|
|
119
119
|
&:hover
|
|
120
120
|
.tree-item-title
|
|
121
|
-
padding-right
|
|
121
|
+
padding-right 60px
|
|
122
122
|
.tree-item-op-wrap
|
|
123
123
|
opacity 1
|
|
124
124
|
pointer-events auto
|
|
125
|
+
.add-menu-wrap
|
|
126
|
+
.sidebar-panel
|
|
127
|
+
.tree-list-row
|
|
128
|
+
&:hover
|
|
129
|
+
.tree-item-title
|
|
130
|
+
padding-right 20px
|
|
125
131
|
|
|
126
132
|
.tree-list-row-group
|
|
127
133
|
position relative
|
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
getLocalFileInfo,
|
|
12
12
|
getRemoteFileInfo
|
|
13
13
|
} from '../components/sftp/file-read'
|
|
14
|
+
import {
|
|
15
|
+
fixBookmarkData,
|
|
16
|
+
validateBookmarkData
|
|
17
|
+
} from '../components/bookmark-form/fix-bookmark-default'
|
|
18
|
+
import newTerm from '../common/new-terminal'
|
|
14
19
|
|
|
15
20
|
export default Store => {
|
|
16
21
|
// Initialize MCP handler - called when MCP widget is started
|
|
@@ -96,6 +101,9 @@ export default Store => {
|
|
|
96
101
|
case 'open_local_terminal':
|
|
97
102
|
result = store.mcpOpenLocalTerminal()
|
|
98
103
|
break
|
|
104
|
+
case 'open_tab':
|
|
105
|
+
result = store.mcpOpenTab(args)
|
|
106
|
+
break
|
|
99
107
|
|
|
100
108
|
// Terminal operations
|
|
101
109
|
case 'send_terminal_command':
|
|
@@ -207,16 +215,14 @@ export default Store => {
|
|
|
207
215
|
|
|
208
216
|
Store.prototype.mcpAddBookmark = async function (args) {
|
|
209
217
|
const { store } = window
|
|
210
|
-
const bookmark = {
|
|
218
|
+
const bookmark = fixBookmarkData({
|
|
211
219
|
id: uid(),
|
|
212
|
-
title: args.title,
|
|
213
|
-
host: args.host || '',
|
|
214
|
-
port: args.port || 22,
|
|
215
|
-
username: args.username || '',
|
|
216
|
-
password: args.password || '',
|
|
217
|
-
type: args.type || 'local',
|
|
218
|
-
term: 'xterm-256color',
|
|
219
220
|
...args
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const { valid, errors } = validateBookmarkData(bookmark)
|
|
224
|
+
if (!valid) {
|
|
225
|
+
throw new Error(errors.join(', '))
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
store.addItem(bookmark, settingMap.bookmarks)
|
|
@@ -462,6 +468,32 @@ export default Store => {
|
|
|
462
468
|
}
|
|
463
469
|
}
|
|
464
470
|
|
|
471
|
+
Store.prototype.mcpOpenTab = function (args) {
|
|
472
|
+
const { store } = window
|
|
473
|
+
const data = fixBookmarkData({ ...args })
|
|
474
|
+
|
|
475
|
+
const { valid, errors } = validateBookmarkData(data)
|
|
476
|
+
if (!valid) {
|
|
477
|
+
throw new Error(errors.join(', '))
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const tab = {
|
|
481
|
+
...data,
|
|
482
|
+
from: 'mcp',
|
|
483
|
+
...newTerm(true, true)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
store.addTab(tab)
|
|
487
|
+
const newTabId = store.activeTabId
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
success: true,
|
|
491
|
+
tabId: newTabId,
|
|
492
|
+
type: data.type,
|
|
493
|
+
message: `Opened ${data.type || 'local'} tab`
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
465
497
|
// ==================== Terminal APIs ====================
|
|
466
498
|
|
|
467
499
|
Store.prototype.mcpSendTerminalCommand = function (args) {
|
|
@@ -477,7 +509,7 @@ export default Store => {
|
|
|
477
509
|
throw new Error('No command provided')
|
|
478
510
|
}
|
|
479
511
|
|
|
480
|
-
store.runQuickCommand(command, args.inputOnly || false)
|
|
512
|
+
store.runQuickCommand(command, args.inputOnly || false, tabId)
|
|
481
513
|
|
|
482
514
|
return {
|
|
483
515
|
success: true,
|
|
@@ -57,8 +57,9 @@ export default Store => {
|
|
|
57
57
|
window.store.delItem({ id }, settingMap.quickCommands)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
Store.prototype.runQuickCommand = function (cmd, inputOnly = false) {
|
|
61
|
-
|
|
60
|
+
Store.prototype.runQuickCommand = function (cmd, inputOnly = false, tabId) {
|
|
61
|
+
const tid = tabId || window.store.activeTabId
|
|
62
|
+
refs.get('term-' + tid)?.runQuickCommand(cmd, inputOnly)
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
Store.prototype.runQuickCommandItem = debounce(async (id) => {
|
package/client/store/watch.js
CHANGED
|
@@ -23,13 +23,13 @@ export default store => {
|
|
|
23
23
|
for (const name of dbNamesForWatch) {
|
|
24
24
|
window[`watch${name}Running`] = false
|
|
25
25
|
window[`watch${name}`] = autoRun(async () => {
|
|
26
|
+
const n = store.getItems(name)
|
|
26
27
|
if (window.migrating || window[`watch${name}Running`]) {
|
|
27
28
|
return
|
|
28
29
|
}
|
|
29
30
|
window[`watch${name}Running`] = true
|
|
30
31
|
try {
|
|
31
32
|
const old = refsStatic.get('oldState-' + name)
|
|
32
|
-
const n = store.getItems(name)
|
|
33
33
|
const { updated, added, removed } = dataCompare(
|
|
34
34
|
old,
|
|
35
35
|
n
|