@electerm/electerm-react 3.12.0 → 3.15.28
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/default-setting.js +1 -0
- package/client/common/is-absolute-path.js +1 -1
- package/client/common/normalize-remote-path.js +20 -0
- package/client/common/resolve.js +16 -0
- package/client/components/ai/agent-tools.js +121 -1
- package/client/components/ai/ai-chat-history-item.jsx +7 -2
- package/client/components/ai/ai-chat.jsx +1 -0
- package/client/components/ai/ai-config-props.js +1 -0
- package/client/components/ai/ai-config.jsx +13 -2
- package/client/components/ai/ai-output.jsx +6 -2
- package/client/components/main/main.jsx +1 -1
- package/client/components/sftp/address-bar.jsx +22 -6
- package/client/components/sftp/file-item.jsx +43 -2
- package/client/components/sftp/file-read.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +38 -3
- package/client/components/sys-menu/icons-map.jsx +10 -2
- package/client/components/terminal/osc52-addon.js +147 -0
- package/client/components/terminal/terminal-apis.js +9 -0
- package/client/components/terminal/terminal.jsx +119 -3
- package/client/components/terminal/xmodem-client.js +62 -0
- package/client/components/terminal-info/base.jsx +41 -38
- package/client/components/terminal-info/log-path-edit.jsx +3 -2
- package/client/store/common.js +1 -1
- package/client/store/load-data.js +14 -0
- package/client/store/mcp-handler.js +231 -0
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Osc52Addon - Handles OSC 52 clipboard escape sequences
|
|
3
|
+
*
|
|
4
|
+
* OSC 52 allows terminal programs (TUI apps like vim, tmux, opencode, etc.)
|
|
5
|
+
* to copy and paste text via the system clipboard.
|
|
6
|
+
*
|
|
7
|
+
* Format: ESC ] 52 ; Pc ; Pd BEL (or ST)
|
|
8
|
+
* Pc = clipboard selection: c (clipboard), p (primary), s (secondary)
|
|
9
|
+
* Pd = base64-encoded content, or "?" to read
|
|
10
|
+
*
|
|
11
|
+
* Write request: ESC ] 52 ; c ; <base64data> BEL
|
|
12
|
+
* -> decodes base64 and writes to system clipboard
|
|
13
|
+
*
|
|
14
|
+
* Read request: ESC ] 52 ; c ; ? BEL
|
|
15
|
+
* -> reads system clipboard, encodes base64, sends response
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export class Osc52Addon {
|
|
19
|
+
constructor () {
|
|
20
|
+
this.terminal = undefined
|
|
21
|
+
this._disposables = []
|
|
22
|
+
this._sendData = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set the function used to send data back to the PTY
|
|
27
|
+
* @param {function} sendDataFn - function(string) to send data to server
|
|
28
|
+
*/
|
|
29
|
+
setSendData (sendDataFn) {
|
|
30
|
+
this._sendData = sendDataFn
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
activate (terminal) {
|
|
34
|
+
this.terminal = terminal
|
|
35
|
+
|
|
36
|
+
if (terminal.parser && terminal.parser.registerOscHandler) {
|
|
37
|
+
const oscHandler = terminal.parser.registerOscHandler(52, (data) => {
|
|
38
|
+
return this._handleOsc52(data)
|
|
39
|
+
})
|
|
40
|
+
this._disposables.push(oscHandler)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dispose () {
|
|
45
|
+
this.terminal = null
|
|
46
|
+
if (this._disposables) {
|
|
47
|
+
this._disposables.forEach(d => d.dispose())
|
|
48
|
+
this._disposables.length = 0
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle OSC 52 clipboard sequence
|
|
54
|
+
* @param {string} data - The OSC data after "52;"
|
|
55
|
+
* @returns {boolean} Whether the sequence was handled
|
|
56
|
+
*/
|
|
57
|
+
_handleOsc52 (data) {
|
|
58
|
+
if (!data) return false
|
|
59
|
+
|
|
60
|
+
// Parse: Pc;Pd where Pc is selection target and Pd is payload
|
|
61
|
+
const semicolonIdx = data.indexOf(';')
|
|
62
|
+
if (semicolonIdx === -1) return false
|
|
63
|
+
|
|
64
|
+
const target = data.substring(0, semicolonIdx)
|
|
65
|
+
const payload = data.substring(semicolonIdx + 1)
|
|
66
|
+
|
|
67
|
+
// Only handle clipboard target ('c'), also accept 'c' among multiple targets
|
|
68
|
+
if (!target.includes('c')) return false
|
|
69
|
+
|
|
70
|
+
if (payload === '?') {
|
|
71
|
+
// Read request - send clipboard content back
|
|
72
|
+
return this._handleReadRequest()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Write request - decode and write to clipboard
|
|
76
|
+
return this._handleWriteRequest(payload)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle clipboard read request (ESC]52;c;? BEL)
|
|
81
|
+
* Reads system clipboard and sends response back to the terminal
|
|
82
|
+
*/
|
|
83
|
+
_handleReadRequest () {
|
|
84
|
+
try {
|
|
85
|
+
const text = window.pre.readClipboard()
|
|
86
|
+
const base64 = text ? this._encodeBase64(text) : ''
|
|
87
|
+
this._sendResponse(base64)
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error('OSC 52 clipboard read failed:', e)
|
|
90
|
+
this._sendResponse('')
|
|
91
|
+
}
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle clipboard write request (ESC]52;c;<base64> BEL)
|
|
97
|
+
* Decodes base64 content and writes to system clipboard
|
|
98
|
+
*/
|
|
99
|
+
_handleWriteRequest (base64Data) {
|
|
100
|
+
try {
|
|
101
|
+
const text = this._decodeBase64(base64Data)
|
|
102
|
+
if (text) {
|
|
103
|
+
window.pre.writeClipboard(text)
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error('OSC 52 clipboard write failed:', e)
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Send OSC 52 response back to the terminal (for read requests)
|
|
113
|
+
* @param {string} base64Data - base64-encoded clipboard content
|
|
114
|
+
*/
|
|
115
|
+
_sendResponse (base64Data) {
|
|
116
|
+
if (!this._sendData) return
|
|
117
|
+
// Response format: ESC ] 52 ; c ; <base64> BEL
|
|
118
|
+
const response = `\x1b]52;c;${base64Data}\x07`
|
|
119
|
+
this._sendData(response)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Encode a string to base64 (handles UTF-8 properly)
|
|
124
|
+
*/
|
|
125
|
+
_encodeBase64 (str) {
|
|
126
|
+
const encoder = new TextEncoder()
|
|
127
|
+
const bytes = encoder.encode(str)
|
|
128
|
+
let binary = ''
|
|
129
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
130
|
+
binary += String.fromCharCode(bytes[i])
|
|
131
|
+
}
|
|
132
|
+
return btoa(binary)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Decode a base64 string (handles UTF-8 properly)
|
|
137
|
+
*/
|
|
138
|
+
_decodeBase64 (base64) {
|
|
139
|
+
const binary = atob(base64)
|
|
140
|
+
const bytes = new Uint8Array(binary.length)
|
|
141
|
+
for (let i = 0; i < binary.length; i++) {
|
|
142
|
+
bytes[i] = binary.charCodeAt(i)
|
|
143
|
+
}
|
|
144
|
+
const decoder = new TextDecoder()
|
|
145
|
+
return decoder.decode(bytes)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -49,3 +49,12 @@ export function setTerminalLogPath (pid, logPath) {
|
|
|
49
49
|
action: 'set-terminal-log-path'
|
|
50
50
|
})
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export function startTerminalLogFile (pid, logFilePath, addTimeStampToTermLog) {
|
|
54
|
+
return fetch({
|
|
55
|
+
pid,
|
|
56
|
+
logFilePath,
|
|
57
|
+
addTimeStampToTermLog,
|
|
58
|
+
action: 'start-terminal-log-file'
|
|
59
|
+
})
|
|
60
|
+
}
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
Dropdown
|
|
8
8
|
} from 'antd'
|
|
9
9
|
import message from '../common/message'
|
|
10
|
+
import { notification } from '../common/notification'
|
|
11
|
+
import ShowItem from '../common/show-item.jsx'
|
|
10
12
|
import Modal from '../common/modal'
|
|
11
13
|
import classnames from 'classnames'
|
|
12
14
|
import './terminal.styl'
|
|
@@ -30,11 +32,13 @@ import { XmodemClient } from './xmodem-client.js'
|
|
|
30
32
|
import DropFileModal from './drop-file-modal.jsx'
|
|
31
33
|
import keyControlPressed from '../../common/key-control-pressed.js'
|
|
32
34
|
import NormalBuffer from './normal-buffer.jsx'
|
|
33
|
-
import { createTerm, resizeTerm } from './terminal-apis.js'
|
|
35
|
+
import { createTerm, resizeTerm, startTerminalLogFile, toggleTerminalLog } from './terminal-apis.js'
|
|
34
36
|
import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
35
37
|
import { KeywordHighlighterAddon } from './highlight-addon.js'
|
|
36
38
|
import { getFilePath, isUnsafeFilename } from '../../common/file-drop-utils.js'
|
|
39
|
+
import { getFolderFromFilePath } from '../sftp/file-read.js'
|
|
37
40
|
import { CommandTrackerAddon } from './command-tracker-addon.js'
|
|
41
|
+
import { Osc52Addon } from './osc52-addon.js'
|
|
38
42
|
import AIIcon from '../icons/ai-icon.jsx'
|
|
39
43
|
import {
|
|
40
44
|
getShellIntegrationCommand,
|
|
@@ -72,6 +76,9 @@ class Term extends Component {
|
|
|
72
76
|
saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
|
|
73
77
|
addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
|
|
74
78
|
logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
|
|
79
|
+
logFileName: '',
|
|
80
|
+
recording: false,
|
|
81
|
+
recordingFilePath: '',
|
|
75
82
|
passType: 'password',
|
|
76
83
|
lines: [],
|
|
77
84
|
searchResults: [],
|
|
@@ -198,6 +205,9 @@ class Term extends Component {
|
|
|
198
205
|
this.encode || this.props.tab.encode || 'utf-8'
|
|
199
206
|
)
|
|
200
207
|
await this.attachAddon.activate(this.term)
|
|
208
|
+
if (this.osc52Addon) {
|
|
209
|
+
this.osc52Addon.setSendData(this.attachAddon._sendData.bind(this.attachAddon))
|
|
210
|
+
}
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
getValue = (props, type, name) => {
|
|
@@ -385,7 +395,8 @@ class Term extends Component {
|
|
|
385
395
|
if (isUnsafeFilename(p)) {
|
|
386
396
|
return message.error('File name contains unsafe characters')
|
|
387
397
|
}
|
|
388
|
-
|
|
398
|
+
const isWinPath = /^[a-zA-Z]:\\/.test(p)
|
|
399
|
+
this.runQuickCommand(isWinPath ? `cd /d "${p}"` : `cd "${p}"`)
|
|
389
400
|
}
|
|
390
401
|
|
|
391
402
|
onDrop = e => {
|
|
@@ -676,8 +687,101 @@ class Term extends Component {
|
|
|
676
687
|
)
|
|
677
688
|
}
|
|
678
689
|
|
|
690
|
+
getTerminalBufferText = () => {
|
|
691
|
+
const { addTimeStampToTermLog } = this.state
|
|
692
|
+
const buffer = this.term.buffer.active
|
|
693
|
+
const len = buffer.length
|
|
694
|
+
const rawLines = []
|
|
695
|
+
for (let i = 0; i < len; i++) {
|
|
696
|
+
const line = buffer.getLine(i)
|
|
697
|
+
rawLines.push(line ? line.translateToString(false) : '')
|
|
698
|
+
}
|
|
699
|
+
// trim trailing blank lines before applying timestamps
|
|
700
|
+
while (rawLines.length && !rawLines[rawLines.length - 1].trim()) {
|
|
701
|
+
rawLines.pop()
|
|
702
|
+
}
|
|
703
|
+
if (!addTimeStampToTermLog) {
|
|
704
|
+
return rawLines.join('\n')
|
|
705
|
+
}
|
|
706
|
+
return rawLines.map(text => {
|
|
707
|
+
const now = new Date()
|
|
708
|
+
const ts = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}] `
|
|
709
|
+
return ts + text
|
|
710
|
+
}).join('\n')
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
syncTermInfo = (stateUpdate) => {
|
|
714
|
+
this.setState(stateUpdate)
|
|
715
|
+
const infoUpdate = pick(stateUpdate, ['saveTerminalLogToFile', 'addTimeStampToTermLog', 'logPath', 'logFileName'])
|
|
716
|
+
if (Object.keys(infoUpdate).length) {
|
|
717
|
+
refs.get('term-info-' + this.props.tab.id)?.setState(infoUpdate)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
openLogSaveDialog = async (titleKey) => {
|
|
722
|
+
const { logName } = this.props
|
|
723
|
+
const result = await window.api.saveDialog({
|
|
724
|
+
title: e(titleKey),
|
|
725
|
+
defaultPath: logName + '.log',
|
|
726
|
+
filters: [
|
|
727
|
+
{ name: 'Log files', extensions: ['log'] }
|
|
728
|
+
],
|
|
729
|
+
properties: ['createDirectory', 'showOverwriteConfirmation']
|
|
730
|
+
})
|
|
731
|
+
if (result.canceled || !result.filePath) {
|
|
732
|
+
return null
|
|
733
|
+
}
|
|
734
|
+
return result.filePath
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
onSaveTerminalLog = async () => {
|
|
738
|
+
const filePath = await this.openLogSaveDialog('saveTerminalLogToFile')
|
|
739
|
+
if (!filePath) {
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
const content = this.getTerminalBufferText()
|
|
743
|
+
await window.fs.writeFile(filePath, content).catch(window.store.onError)
|
|
744
|
+
const { addTimeStampToTermLog } = this.state
|
|
745
|
+
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
746
|
+
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
747
|
+
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
748
|
+
notification.success({
|
|
749
|
+
message: e('saveTerminalLogToFile'),
|
|
750
|
+
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
751
|
+
duration: 5
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
onRecord = async () => {
|
|
756
|
+
const filePath = await this.openLogSaveDialog('record')
|
|
757
|
+
if (!filePath) {
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
const { addTimeStampToTermLog } = this.state
|
|
761
|
+
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
762
|
+
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
763
|
+
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
764
|
+
this.setState({ recording: true, recordingFilePath: filePath })
|
|
765
|
+
notification.success({
|
|
766
|
+
message: e('record'),
|
|
767
|
+
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
768
|
+
duration: 5
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
onStopRecord = () => {
|
|
773
|
+
const { recordingFilePath } = this.state
|
|
774
|
+
toggleTerminalLog(this.pid).catch(window.store.onError)
|
|
775
|
+
this.syncTermInfo({ saveTerminalLogToFile: false })
|
|
776
|
+
this.setState({ recording: false, recordingFilePath: '' })
|
|
777
|
+
notification.success({
|
|
778
|
+
message: e('stopRecord'),
|
|
779
|
+
description: <ShowItem to={recordingFilePath}>{recordingFilePath}</ShowItem>
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
|
|
679
783
|
renderContextMenu = () => {
|
|
680
|
-
const { hasSelection } = this.state
|
|
784
|
+
const { hasSelection, recording } = this.state
|
|
681
785
|
const copyed = true
|
|
682
786
|
const copyShortcut = this.getShortcut('terminal_copy')
|
|
683
787
|
const pasteShortcut = this.getShortcut('terminal_paste')
|
|
@@ -730,6 +834,16 @@ class Term extends Component {
|
|
|
730
834
|
icon: <iconsMap.SearchOutlined />,
|
|
731
835
|
label: e('search'),
|
|
732
836
|
extra: searchShortcut
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
key: 'onSaveTerminalLog',
|
|
840
|
+
icon: <iconsMap.SaveOutlined />,
|
|
841
|
+
label: e('saveTerminalLogToFile')
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
key: recording ? 'onStopRecord' : 'onRecord',
|
|
845
|
+
icon: recording ? <iconsMap.StopOutlined /> : <iconsMap.PlayCircleFilled />,
|
|
846
|
+
label: e(recording ? 'stopRecord' : 'record')
|
|
733
847
|
}
|
|
734
848
|
]
|
|
735
849
|
if (isSerial) {
|
|
@@ -1016,6 +1130,8 @@ class Term extends Component {
|
|
|
1016
1130
|
term.loadAddon(this.fitAddon)
|
|
1017
1131
|
term.loadAddon(this.searchAddon)
|
|
1018
1132
|
term.loadAddon(this.cmdAddon)
|
|
1133
|
+
this.osc52Addon = new Osc52Addon()
|
|
1134
|
+
term.loadAddon(this.osc52Addon)
|
|
1019
1135
|
if (tab.enableTerminalImage) {
|
|
1020
1136
|
const ImageAddon = await loadImageAddon()
|
|
1021
1137
|
this.imageAddon = new ImageAddon({
|
|
@@ -66,6 +66,68 @@ export class XmodemClient extends TransferClientBase {
|
|
|
66
66
|
case 'session-error':
|
|
67
67
|
this.onError(msg.error)
|
|
68
68
|
break
|
|
69
|
+
case 'auto-trigger-receive':
|
|
70
|
+
this.handleAutoReceive(msg.name)
|
|
71
|
+
break
|
|
72
|
+
case 'auto-trigger-send':
|
|
73
|
+
this.handleAutoSend()
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Auto-triggered: device is sending a file, electerm should receive it.
|
|
80
|
+
* Opens save folder dialog then starts xmodem receive.
|
|
81
|
+
* @param {string} fileName - Original filename from the device
|
|
82
|
+
*/
|
|
83
|
+
async handleAutoReceive (fileName) {
|
|
84
|
+
if (this.isActive) return
|
|
85
|
+
|
|
86
|
+
this.isActive = true
|
|
87
|
+
this.writeBanner('RECEIVE', null)
|
|
88
|
+
|
|
89
|
+
const savePath = await this.openSaveFolderSelect()
|
|
90
|
+
if (savePath) {
|
|
91
|
+
this.savePath = savePath
|
|
92
|
+
this.sendToServer({
|
|
93
|
+
event: 'start-receive'
|
|
94
|
+
})
|
|
95
|
+
this.sendToServer({
|
|
96
|
+
event: 'set-save-path',
|
|
97
|
+
path: savePath,
|
|
98
|
+
name: fileName
|
|
99
|
+
})
|
|
100
|
+
} else {
|
|
101
|
+
this.isActive = false
|
|
102
|
+
this.writeToTerminal('\r\n\x1b[33mXMODEM Receive cancelled\x1b[0m\r\n')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Auto-triggered: device wants to receive a file, electerm should send it.
|
|
108
|
+
* Opens file select dialog then starts xmodem send.
|
|
109
|
+
*/
|
|
110
|
+
async handleAutoSend () {
|
|
111
|
+
if (this.isActive) return
|
|
112
|
+
|
|
113
|
+
this.isActive = true
|
|
114
|
+
this.writeBanner('SEND', null)
|
|
115
|
+
|
|
116
|
+
const files = await this.openFileSelect({
|
|
117
|
+
title: 'Choose file to send via XMODEM',
|
|
118
|
+
message: 'Choose file to send via XMODEM'
|
|
119
|
+
})
|
|
120
|
+
if (files && files.length > 0) {
|
|
121
|
+
this.sendToServer({
|
|
122
|
+
event: 'start-send'
|
|
123
|
+
})
|
|
124
|
+
this.sendToServer({
|
|
125
|
+
event: 'send-files',
|
|
126
|
+
files
|
|
127
|
+
})
|
|
128
|
+
} else {
|
|
129
|
+
this.isActive = false
|
|
130
|
+
this.writeToTerminal('\r\n\x1b[33mXMODEM Send cancelled\x1b[0m\r\n')
|
|
69
131
|
}
|
|
70
132
|
}
|
|
71
133
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Button
|
|
9
9
|
} from 'antd'
|
|
10
10
|
import defaults from '../../common/default-setting'
|
|
11
|
-
import { toggleTerminalLog, toggleTerminalLogTimestamp
|
|
11
|
+
import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
|
|
12
12
|
import {
|
|
13
13
|
ClockCircleOutlined,
|
|
14
14
|
BorderlessTableOutlined,
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
PartitionOutlined
|
|
19
19
|
} from '@ant-design/icons'
|
|
20
20
|
import { refs } from '../common/ref'
|
|
21
|
-
import
|
|
21
|
+
import ShowItem from '../common/show-item'
|
|
22
|
+
import { osResolve } from '../../common/resolve'
|
|
23
|
+
import createDefaultLogPath from '../../common/default-log-path'
|
|
22
24
|
|
|
23
25
|
const e = window.translate
|
|
24
26
|
|
|
@@ -35,15 +37,20 @@ export default class TerminalInfoBase extends Component {
|
|
|
35
37
|
state = {
|
|
36
38
|
saveTerminalLogToFile: false,
|
|
37
39
|
addTimeStampToTermLog: false,
|
|
38
|
-
logPath: ''
|
|
40
|
+
logPath: '',
|
|
41
|
+
logFileName: ''
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
componentDidMount () {
|
|
45
|
+
const { pid } = this.props
|
|
46
|
+
refs.add('term-info-' + pid, this)
|
|
42
47
|
this.getState()
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
componentWillUnmount () {
|
|
46
51
|
clearTimeout(this.timer)
|
|
52
|
+
const { pid } = this.props
|
|
53
|
+
refs.remove('term-info-' + pid)
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
handleToggleTimestamp = () => {
|
|
@@ -74,17 +81,6 @@ export default class TerminalInfoBase extends Component {
|
|
|
74
81
|
})
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
onLogPathChange = (v) => {
|
|
78
|
-
const { pid } = this.props
|
|
79
|
-
setTerminalLogPath(pid, v)
|
|
80
|
-
refs.get('term-' + pid)?.setState({
|
|
81
|
-
logPath: v
|
|
82
|
-
})
|
|
83
|
-
this.setState({
|
|
84
|
-
logPath: v
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
handleToggle = () => {
|
|
89
85
|
const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
|
|
90
86
|
const {
|
|
@@ -112,7 +108,8 @@ export default class TerminalInfoBase extends Component {
|
|
|
112
108
|
this.setState({
|
|
113
109
|
saveTerminalLogToFile: term.state.saveTerminalLogToFile,
|
|
114
110
|
addTimeStampToTermLog: term.state.addTimeStampToTermLog,
|
|
115
|
-
logPath: term.state.logPath
|
|
111
|
+
logPath: term.state.logPath,
|
|
112
|
+
logFileName: term.state.logFileName || ''
|
|
116
113
|
})
|
|
117
114
|
} else {
|
|
118
115
|
this.timer = setTimeout(this.getState, 100)
|
|
@@ -166,39 +163,45 @@ export default class TerminalInfoBase extends Component {
|
|
|
166
163
|
render () {
|
|
167
164
|
const {
|
|
168
165
|
id,
|
|
169
|
-
logName
|
|
170
|
-
pid
|
|
166
|
+
logName
|
|
171
167
|
} = this.props
|
|
172
|
-
const { saveTerminalLogToFile, logPath } = this.state
|
|
168
|
+
const { saveTerminalLogToFile, logPath, logFileName } = this.state
|
|
173
169
|
const name = e('saveTerminalLogToFile')
|
|
170
|
+
const base = logPath || createDefaultLogPath()
|
|
171
|
+
const fileName = logFileName || (logName + '.log')
|
|
172
|
+
const fullPath = osResolve(base, fileName)
|
|
174
173
|
return (
|
|
175
174
|
<div className='terminal-info-section terminal-info-base'>
|
|
176
|
-
<div className='
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
175
|
+
<div className='pd1b'>
|
|
176
|
+
<b>ID:</b> {id}
|
|
177
|
+
</div>
|
|
178
|
+
<div className='pd1b'>
|
|
179
|
+
<Switch
|
|
180
|
+
checkedChildren={name}
|
|
181
|
+
unCheckedChildren={name}
|
|
182
|
+
checked={saveTerminalLogToFile}
|
|
183
|
+
onChange={this.handleToggle}
|
|
184
|
+
className='mg1r mg1b'
|
|
185
|
+
/>
|
|
186
|
+
{
|
|
187
|
+
this.renderTimestamp()
|
|
188
|
+
}
|
|
190
189
|
</div>
|
|
190
|
+
{
|
|
191
|
+
saveTerminalLogToFile
|
|
192
|
+
? (
|
|
193
|
+
<div className='pd1b font-xs color-grey'>
|
|
194
|
+
{e('terminalLogPath')}: {fullPath} <ShowItem to={fullPath} />
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
: null
|
|
198
|
+
}
|
|
191
199
|
<div className='pd2y'>
|
|
192
200
|
{
|
|
193
201
|
this.renderInfoSelection()
|
|
194
202
|
}
|
|
195
203
|
</div>
|
|
196
|
-
|
|
197
|
-
pid={pid}
|
|
198
|
-
logPath={logPath}
|
|
199
|
-
logName={logName}
|
|
200
|
-
setLogPath={this.onLogPathChange}
|
|
201
|
-
/>
|
|
204
|
+
|
|
202
205
|
</div>
|
|
203
206
|
)
|
|
204
207
|
}
|
|
@@ -12,10 +12,11 @@ import { osResolve } from '../../common/resolve'
|
|
|
12
12
|
|
|
13
13
|
const e = window.translate
|
|
14
14
|
|
|
15
|
-
export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
|
|
15
|
+
export default function LogPathEdit ({ pid, logPath, logName, logFileName, setLogPath }) {
|
|
16
16
|
const defaultPath = createDefaultLogPath()
|
|
17
17
|
const base = logPath || defaultPath
|
|
18
|
-
const
|
|
18
|
+
const fileName = logFileName || (logName + '.log')
|
|
19
|
+
const fullPath = osResolve(base, fileName)
|
|
19
20
|
|
|
20
21
|
const testAndSet = async (v) => {
|
|
21
22
|
if (v) {
|
package/client/store/common.js
CHANGED
|
@@ -298,7 +298,7 @@ export default Store => {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
Store.prototype.aiConfigMissing = function () {
|
|
301
|
-
return aiConfigsArr.filter(k => k !== 'apiKeyAI' && k !== 'proxyAI').some(k => !window.store.config[k])
|
|
301
|
+
return aiConfigsArr.filter(k => k !== 'apiKeyAI' && k !== 'proxyAI' && k !== 'nameAI').some(k => !window.store.config[k])
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
Store.prototype.clearHistory = function () {
|
|
@@ -52,6 +52,20 @@ export async function addTabFromCommandLine (store, opts) {
|
|
|
52
52
|
if (isHelp) {
|
|
53
53
|
return store.openAbout(infoTabs.cmd)
|
|
54
54
|
}
|
|
55
|
+
// Check if argv contains a protocol URL (e.g., ssh://user@host)
|
|
56
|
+
// and use parseQuickConnect for proper parsing
|
|
57
|
+
if (argv && argv.length) {
|
|
58
|
+
const protocolUrl = argv.find(arg =>
|
|
59
|
+
/^(ssh|telnet|rdp|vnc|serial|spice|ftp|http|https|electerm):\/\//i.test(arg)
|
|
60
|
+
)
|
|
61
|
+
if (protocolUrl) {
|
|
62
|
+
const parsed = parseQuickConnect(protocolUrl)
|
|
63
|
+
if (parsed) {
|
|
64
|
+
return store.ipcOpenTab(parsed)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
const conf = getHost(argv, options)
|
|
56
70
|
const update = {
|
|
57
71
|
passphrase: options.passphrase,
|