@electerm/electerm-react 3.9.5 → 3.10.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/default-setting.js +3 -2
- package/client/components/ai/ai-config.jsx +9 -1
- package/client/components/batch-op/batch-op-runner.jsx +1 -2
- package/client/components/bookmark-form/common/bookmark-group-tree-format.js +1 -1
- package/client/components/bookmark-form/common/bookmark-select.jsx +1 -1
- package/client/components/bookmark-form/tree-select.jsx +1 -1
- package/client/components/setting-panel/setting-terminal.jsx +12 -0
- package/client/components/setting-panel/start-session-select.jsx +1 -1
- package/client/components/sftp/list-table-ui.jsx +5 -1
- package/client/components/tabs/tab.jsx +38 -19
- package/client/components/tabs/tabs.styl +8 -17
- package/client/components/terminal/attach-addon-custom.js +7 -3
- package/client/components/terminal/drop-file-modal.jsx +3 -3
- package/client/components/terminal/terminal-apis.js +8 -0
- package/client/components/terminal/terminal-command-dropdown.jsx +1 -1
- package/client/components/terminal/terminal-interactive.jsx +69 -30
- package/client/components/terminal/terminal.jsx +30 -14
- package/client/components/terminal-info/base.jsx +25 -14
- package/client/components/terminal-info/log-path-edit.jsx +86 -0
- package/client/components/text-editor/edit-with-custom-editor.jsx +22 -3
- package/client/components/text-editor/text-editor.jsx +21 -0
- package/client/components/tree-list/tree-list-item.jsx +6 -12
- package/client/components/tree-list/tree-list-row.jsx +5 -0
- package/client/components/tree-list/tree-list-rows.js +3 -1
- package/client/components/widgets/widget-form.jsx +3 -0
- package/client/store/mcp-handler.js +83 -10
- package/client/store/tab.js +14 -0
- package/package.json +1 -1
|
@@ -64,7 +64,7 @@ export default {
|
|
|
64
64
|
],
|
|
65
65
|
hideIP: false,
|
|
66
66
|
dataSyncSelected: 'all',
|
|
67
|
-
baseURLAI: 'https://api.
|
|
67
|
+
baseURLAI: 'https://api.atlascloud.ai/v1',
|
|
68
68
|
modelAI: 'deepseek-chat',
|
|
69
69
|
roleAI: '终端专家,提供不同系统下命令,简要解释用法,用markdown格式',
|
|
70
70
|
apiPathAI: '/chat/completions',
|
|
@@ -75,5 +75,6 @@ export default {
|
|
|
75
75
|
autoReconnectTerminal: false,
|
|
76
76
|
startDirectoryLocal: '',
|
|
77
77
|
allowMultiInstance: false,
|
|
78
|
-
disableDeveloperTool: false
|
|
78
|
+
disableDeveloperTool: false,
|
|
79
|
+
dragDropBehavior: 'ask'
|
|
79
80
|
}
|
|
@@ -36,6 +36,7 @@ const proxyOptions = [
|
|
|
36
36
|
|
|
37
37
|
export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
|
|
38
38
|
const [form] = Form.useForm()
|
|
39
|
+
const baseURLAI = Form.useWatch('baseURLAI', form)
|
|
39
40
|
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
if (initialValues) {
|
|
@@ -67,6 +68,13 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
67
68
|
return { label, title }
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function renderApiUrlLabel () {
|
|
72
|
+
if (baseURLAI === 'https://api.atlascloud.ai/v1') {
|
|
73
|
+
return <span>API URL (<Link to='https://atlascloud.ai'>AtlasCloud</Link>)</span>
|
|
74
|
+
}
|
|
75
|
+
return 'API URL'
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
if (!showAIConfig) {
|
|
71
79
|
return null
|
|
72
80
|
}
|
|
@@ -90,7 +98,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
90
98
|
layout='vertical'
|
|
91
99
|
className='ai-config-form'
|
|
92
100
|
>
|
|
93
|
-
<Form.Item label=
|
|
101
|
+
<Form.Item label={renderApiUrlLabel()} required>
|
|
94
102
|
<Space.Compact className='width-100'>
|
|
95
103
|
<Form.Item
|
|
96
104
|
label='API URL'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Component } from 'react'
|
|
2
|
-
import { refsStatic } from '../common/ref'
|
|
2
|
+
import { refsStatic, refs } from '../common/ref'
|
|
3
3
|
import { statusMap } from '../../common/constants'
|
|
4
4
|
import { autoRun } from 'manate'
|
|
5
5
|
import uid from '../../common/uid'
|
|
@@ -237,7 +237,6 @@ export default class BatchOpRunner extends Component {
|
|
|
237
237
|
throw new Error('No active tab. Please connect first.')
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
const { refs } = await import('../common/ref')
|
|
241
240
|
const term = refs.get('term-' + tabId)
|
|
242
241
|
if (!term || !term.term) {
|
|
243
242
|
throw new Error('Terminal not found')
|
|
@@ -21,7 +21,7 @@ export default (bookmarkGroups = [], disabledId = '', returnMap = false, current
|
|
|
21
21
|
}
|
|
22
22
|
return y
|
|
23
23
|
}
|
|
24
|
-
const level1 = bookmarkGroups.filter(d => d.level
|
|
24
|
+
const level1 = bookmarkGroups.filter(d => d.level === 1 || !d.level)
|
|
25
25
|
.map(d => {
|
|
26
26
|
const r = {
|
|
27
27
|
title: d.title,
|
|
@@ -38,7 +38,7 @@ function buildTreeData (bookmarkGroups, tree) {
|
|
|
38
38
|
if (!x) return ''
|
|
39
39
|
return { value: x.id, key: x.id, title: createTitle(x) }
|
|
40
40
|
}
|
|
41
|
-
const level1 = cats.filter(d => d.level
|
|
41
|
+
const level1 = cats.filter(d => d.level === 1 || !d.level)
|
|
42
42
|
.map(d => {
|
|
43
43
|
const r = {
|
|
44
44
|
title: d.title,
|
|
@@ -79,7 +79,7 @@ function buildData (bookmarks, bookmarkGroups, searchText = '') {
|
|
|
79
79
|
title: createTitleWithTag(x)
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
const level1 = cats.filter(d => d.level
|
|
82
|
+
const level1 = cats.filter(d => d.level === 1 || !d.level)
|
|
83
83
|
.map(d => {
|
|
84
84
|
const children = [
|
|
85
85
|
...(d.bookmarkGroupIds || []).map(buildSubCats),
|
|
@@ -84,6 +84,7 @@ export default class SettingTerminal extends Component {
|
|
|
84
84
|
|
|
85
85
|
handleChangeDelMode = v => this.onChangeValue(v, 'backspaceMode')
|
|
86
86
|
handleChangeRenderType = v => this.onChangeValue(v, 'rendererType')
|
|
87
|
+
handleChangeDragDropBehavior = v => this.onChangeValue(v, 'dragDropBehavior')
|
|
87
88
|
|
|
88
89
|
handleChangeFont = (values) => {
|
|
89
90
|
this.onChangeValue(
|
|
@@ -471,6 +472,7 @@ export default class SettingTerminal extends Component {
|
|
|
471
472
|
const {
|
|
472
473
|
rendererType,
|
|
473
474
|
backspaceMode = '^?',
|
|
475
|
+
dragDropBehavior = 'ask',
|
|
474
476
|
keywords = [{ color: 'red' }]
|
|
475
477
|
} = this.props.config
|
|
476
478
|
const {
|
|
@@ -593,6 +595,16 @@ export default class SettingTerminal extends Component {
|
|
|
593
595
|
'autoReconnectTerminal'
|
|
594
596
|
].map(d => this.renderToggle(d))
|
|
595
597
|
}
|
|
598
|
+
<div className='pd1b'>{e('dragDropBehavior')}</div>
|
|
599
|
+
<Select
|
|
600
|
+
onChange={this.handleChangeDragDropBehavior}
|
|
601
|
+
value={dragDropBehavior}
|
|
602
|
+
popupMatchSelectWidth={false}
|
|
603
|
+
>
|
|
604
|
+
{['ask', 'trz', 'rz', 'inputOnly'].map(id => (
|
|
605
|
+
<Option key={id} value={id}>{e(id)}</Option>
|
|
606
|
+
))}
|
|
607
|
+
</Select>
|
|
596
608
|
<div className='pd1b'>{e('terminalBackSpaceMode')}</div>
|
|
597
609
|
<Select
|
|
598
610
|
onChange={this.handleChangeDelMode}
|
|
@@ -28,7 +28,11 @@ export default class FileListTable extends Component {
|
|
|
28
28
|
containerRef = createRef()
|
|
29
29
|
|
|
30
30
|
componentDidUpdate (prevProps) {
|
|
31
|
-
|
|
31
|
+
const prevList = prevProps.fileList
|
|
32
|
+
const nextList = this.props.fileList
|
|
33
|
+
const contentChanged = prevList.length !== nextList.length ||
|
|
34
|
+
prevList.some((f, i) => f.id !== nextList[i].id)
|
|
35
|
+
if (contentChanged) {
|
|
32
36
|
if (this.containerRef.current) {
|
|
33
37
|
this.containerRef.current.scrollTop = 0
|
|
34
38
|
}
|
|
@@ -8,7 +8,8 @@ import { refsTabs } from '../common/ref'
|
|
|
8
8
|
import {
|
|
9
9
|
CloseOutlined,
|
|
10
10
|
Loading3QuartersOutlined,
|
|
11
|
-
BorderlessTableOutlined
|
|
11
|
+
BorderlessTableOutlined,
|
|
12
|
+
LockOutlined
|
|
12
13
|
} from '@ant-design/icons'
|
|
13
14
|
import {
|
|
14
15
|
Tooltip,
|
|
@@ -33,7 +34,7 @@ class Tab extends Component {
|
|
|
33
34
|
constructor (props) {
|
|
34
35
|
super(props)
|
|
35
36
|
this.state = {
|
|
36
|
-
terminalOnData:
|
|
37
|
+
terminalOnData: ''
|
|
37
38
|
}
|
|
38
39
|
this.id = 'tab-' + this.props.tab.id
|
|
39
40
|
refsTabs.add(this.id, this)
|
|
@@ -48,19 +49,38 @@ class Tab extends Component {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
notifyOnData = () => {
|
|
52
|
+
if (this.state.terminalOnData === 'password') {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
51
55
|
if (this.timer) {
|
|
52
56
|
clearTimeout(this.timer)
|
|
53
57
|
this.timer = null
|
|
54
58
|
}
|
|
55
59
|
this.setState({
|
|
56
|
-
terminalOnData:
|
|
60
|
+
terminalOnData: 'feed'
|
|
57
61
|
})
|
|
58
62
|
this.timer = setTimeout(this.clearTerminalOnData, 4000)
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
clearTerminalOnData = () => {
|
|
62
66
|
this.setState({
|
|
63
|
-
terminalOnData:
|
|
67
|
+
terminalOnData: ''
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
notifyPasswordPrompt = () => {
|
|
72
|
+
if (this.timer) {
|
|
73
|
+
clearTimeout(this.timer)
|
|
74
|
+
this.timer = null
|
|
75
|
+
}
|
|
76
|
+
this.setState({
|
|
77
|
+
terminalOnData: 'password'
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clearPasswordPrompt = () => {
|
|
82
|
+
this.setState({
|
|
83
|
+
terminalOnData: ''
|
|
64
84
|
})
|
|
65
85
|
}
|
|
66
86
|
|
|
@@ -424,13 +444,7 @@ class Tab extends Component {
|
|
|
424
444
|
{
|
|
425
445
|
'tab-last': isLast
|
|
426
446
|
},
|
|
427
|
-
status
|
|
428
|
-
{
|
|
429
|
-
'is-terminal-active': terminalOnData
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
'is-transporting': isTransporting
|
|
433
|
-
}
|
|
447
|
+
status
|
|
434
448
|
)
|
|
435
449
|
const title = createName(tab)
|
|
436
450
|
let tooltipTitle = title
|
|
@@ -477,15 +491,19 @@ class Tab extends Component {
|
|
|
477
491
|
>
|
|
478
492
|
<Dropdown {...dropdownProps}>
|
|
479
493
|
<div
|
|
480
|
-
className='tab-title elli
|
|
494
|
+
className='tab-title elli'
|
|
481
495
|
onClick={this.handleClick}
|
|
482
496
|
onDoubleClick={this.handleDup}
|
|
483
497
|
>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
498
|
+
{
|
|
499
|
+
status === 'error' && (
|
|
500
|
+
<Loading3QuartersOutlined
|
|
501
|
+
className='pointer tab-reload mg1r'
|
|
502
|
+
onClick={this.handleReloadTab}
|
|
503
|
+
title={e('reload')}
|
|
504
|
+
/>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
489
507
|
<span className='tab-title'>
|
|
490
508
|
<span className='iblock mg1r tab-count' style={styleTag}>{tabCount}</span>
|
|
491
509
|
<span className='mg1r'>{title}</span>
|
|
@@ -493,8 +511,9 @@ class Tab extends Component {
|
|
|
493
511
|
</div>
|
|
494
512
|
</Dropdown>
|
|
495
513
|
<div className={'tab-status ' + status} />
|
|
496
|
-
<div className='tab-traffic' />
|
|
497
|
-
<BorderlessTableOutlined className='tab-terminal-feed' />
|
|
514
|
+
{isTransporting && <div className='tab-traffic' />}
|
|
515
|
+
{terminalOnData === 'feed' && <BorderlessTableOutlined className='tab-terminal-feed' />}
|
|
516
|
+
{terminalOnData === 'password' && <LockOutlined className='tab-terminal-feed password' />}
|
|
498
517
|
{
|
|
499
518
|
this.renderCloseIcon()
|
|
500
519
|
}
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
vertical-align middle
|
|
37
37
|
cursor pointer
|
|
38
38
|
position relative
|
|
39
|
+
padding 0 14px
|
|
39
40
|
min-width 100px
|
|
40
41
|
max-width 200px
|
|
41
42
|
line-height 36px
|
|
@@ -46,7 +47,6 @@
|
|
|
46
47
|
color var(--text-dark)
|
|
47
48
|
&.tab-last
|
|
48
49
|
margin-right 5px
|
|
49
|
-
.tab-reload
|
|
50
50
|
.tab-close
|
|
51
51
|
display none
|
|
52
52
|
&.active
|
|
@@ -68,10 +68,6 @@
|
|
|
68
68
|
width 1px
|
|
69
69
|
border 1px dashed var(--text-dark)
|
|
70
70
|
height 36px
|
|
71
|
-
&.error
|
|
72
|
-
.tab-reload
|
|
73
|
-
display inline-block
|
|
74
|
-
color var(--text-light)
|
|
75
71
|
@keyframes blink
|
|
76
72
|
0%
|
|
77
73
|
background-color #e0e0e0
|
|
@@ -96,30 +92,25 @@
|
|
|
96
92
|
background-color var(--error)
|
|
97
93
|
&.processing
|
|
98
94
|
background-color var(--primary)
|
|
99
|
-
.is-transporting .tab-traffic
|
|
100
|
-
display block
|
|
101
|
-
animation blink 2s infinite
|
|
102
|
-
/* Remove opacity animation, use background-color */
|
|
103
95
|
.tab-traffic
|
|
104
|
-
display none
|
|
105
96
|
left 10px
|
|
106
97
|
width 5px
|
|
107
98
|
border-radius 0
|
|
108
99
|
background-color var(--success)
|
|
109
|
-
.is-terminal-active .tab-terminal-feed
|
|
110
|
-
display block
|
|
111
100
|
animation blink 2s infinite
|
|
112
|
-
background-color transparent !important
|
|
113
|
-
/* Remove opacity animation, use background-color */
|
|
114
101
|
.tab-terminal-feed
|
|
115
|
-
|
|
116
|
-
|
|
102
|
+
width 12px
|
|
103
|
+
height 12px
|
|
117
104
|
border-radius 0
|
|
118
105
|
color var(--success)
|
|
119
|
-
font-size
|
|
106
|
+
font-size 12px
|
|
120
107
|
left 2px
|
|
121
108
|
top 24px
|
|
122
109
|
background none
|
|
110
|
+
background-color transparent !important
|
|
111
|
+
animation blink 2s infinite
|
|
112
|
+
&.password
|
|
113
|
+
color var(--warn)
|
|
123
114
|
.tab-close
|
|
124
115
|
position absolute
|
|
125
116
|
right 5px
|
|
@@ -223,6 +223,7 @@ export default class AttachAddonCustom {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
if (typeof data === 'string') {
|
|
226
|
+
term?.parent?.notifyOnData()
|
|
226
227
|
return term.write(data)
|
|
227
228
|
}
|
|
228
229
|
data = new Uint8Array(data)
|
|
@@ -242,13 +243,16 @@ export default class AttachAddonCustom {
|
|
|
242
243
|
sendToServer = (data) => {
|
|
243
244
|
this._lastInputTime = Date.now()
|
|
244
245
|
// Start echo detection when password prompt is suspected
|
|
245
|
-
if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n') {
|
|
246
|
+
if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n' && data !== '\x03') {
|
|
246
247
|
this._pendingEchoCheck = { char: data, time: Date.now() }
|
|
247
248
|
clearTimeout(this._echoCheckTimer)
|
|
248
249
|
this._echoCheckTimer = setTimeout(this._onEchoCheckTimeout, 200)
|
|
249
250
|
}
|
|
250
|
-
// Reset password state on Enter
|
|
251
|
-
if (data === '\r' || data === '\n') {
|
|
251
|
+
// Reset password state on Enter or Ctrl+C
|
|
252
|
+
if (data === '\r' || data === '\n' || data === '\x03') {
|
|
253
|
+
if (this._passwordPromptDetected) {
|
|
254
|
+
this.term?.parent?.onPasswordPromptCancelled?.()
|
|
255
|
+
}
|
|
252
256
|
this._passwordPromptDetected = false
|
|
253
257
|
this._lastOutputLine = ''
|
|
254
258
|
this._pendingEchoCheck = null
|
|
@@ -26,21 +26,21 @@ export class DropFileModal extends Component {
|
|
|
26
26
|
<button
|
|
27
27
|
type='button'
|
|
28
28
|
className='custom-modal-ok-btn'
|
|
29
|
-
onClick={() => onSelect('
|
|
29
|
+
onClick={() => onSelect('trz')}
|
|
30
30
|
>
|
|
31
31
|
trz
|
|
32
32
|
</button>
|
|
33
33
|
<button
|
|
34
34
|
type='button'
|
|
35
35
|
className='custom-modal-cancel-btn'
|
|
36
|
-
onClick={() => onSelect('
|
|
36
|
+
onClick={() => onSelect('rz')}
|
|
37
37
|
>
|
|
38
38
|
rz
|
|
39
39
|
</button>
|
|
40
40
|
<button
|
|
41
41
|
type='button'
|
|
42
42
|
className='custom-modal-cancel-btn'
|
|
43
|
-
onClick={() => onSelect('
|
|
43
|
+
onClick={() => onSelect('inputOnly')}
|
|
44
44
|
>
|
|
45
45
|
{e('inputOnly')}
|
|
46
46
|
</button>
|
|
@@ -278,7 +278,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
278
278
|
id: uid(),
|
|
279
279
|
command: b.password,
|
|
280
280
|
type: 'PW',
|
|
281
|
-
hint: [b.username, b.host].filter(Boolean).join('@')
|
|
281
|
+
hint: [b.username, [b.host, b.port].filter(Boolean).join(':')].filter(Boolean).join('@')
|
|
282
282
|
})
|
|
283
283
|
}
|
|
284
284
|
}
|
|
@@ -48,6 +48,13 @@ export default function TermInteractive () {
|
|
|
48
48
|
function onOk () {
|
|
49
49
|
form.submit()
|
|
50
50
|
}
|
|
51
|
+
function onConfirm () {
|
|
52
|
+
window.et.commonWs.s({
|
|
53
|
+
id: opts.id,
|
|
54
|
+
results: [opts.options.confirmResult || 'yes']
|
|
55
|
+
})
|
|
56
|
+
clear()
|
|
57
|
+
}
|
|
51
58
|
function onIgnore () {
|
|
52
59
|
window.et.commonWs.s({
|
|
53
60
|
id: opts.id,
|
|
@@ -91,6 +98,32 @@ export default function TermInteractive () {
|
|
|
91
98
|
</FormItem>
|
|
92
99
|
)
|
|
93
100
|
}
|
|
101
|
+
function renderConfirmBody () {
|
|
102
|
+
const instructions = opts.options.instructions || []
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
{
|
|
106
|
+
instructions.map((note, index) => {
|
|
107
|
+
return <pre key={note + index}>{note}</pre>
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
<FormItem>
|
|
111
|
+
<Button
|
|
112
|
+
type='primary'
|
|
113
|
+
onClick={onConfirm}
|
|
114
|
+
>
|
|
115
|
+
{opts.options.submitText || e('submit')}
|
|
116
|
+
</Button>
|
|
117
|
+
<Button
|
|
118
|
+
className='mg1l'
|
|
119
|
+
onClick={onCancel}
|
|
120
|
+
>
|
|
121
|
+
{opts.options.cancelText || e('cancel')}
|
|
122
|
+
</Button>
|
|
123
|
+
</FormItem>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
94
127
|
async function initWatch () {
|
|
95
128
|
let done = false
|
|
96
129
|
while (!done) {
|
|
@@ -125,36 +158,42 @@ export default function TermInteractive () {
|
|
|
125
158
|
<Modal
|
|
126
159
|
{...props}
|
|
127
160
|
>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
{
|
|
162
|
+
opts.options?.mode === 'confirm'
|
|
163
|
+
? renderConfirmBody()
|
|
164
|
+
: (
|
|
165
|
+
<Form
|
|
166
|
+
form={form}
|
|
167
|
+
layout='vertical'
|
|
168
|
+
onFinish={onFinish}
|
|
169
|
+
>
|
|
170
|
+
{
|
|
171
|
+
opts.options.prompts.map(renderFormItem)
|
|
172
|
+
}
|
|
173
|
+
<FormItem>
|
|
174
|
+
<Button
|
|
175
|
+
type='primary'
|
|
176
|
+
htmlType='submit'
|
|
177
|
+
>
|
|
178
|
+
{e('submit')}
|
|
179
|
+
</Button>
|
|
180
|
+
<Button
|
|
181
|
+
type='dashed'
|
|
182
|
+
className='mg1l'
|
|
183
|
+
onClick={onIgnore}
|
|
184
|
+
>
|
|
185
|
+
{e('ignore')}
|
|
186
|
+
</Button>
|
|
187
|
+
<Button
|
|
188
|
+
className='mg1l'
|
|
189
|
+
onClick={onCancel}
|
|
190
|
+
>
|
|
191
|
+
{e('cancel')}
|
|
192
|
+
</Button>
|
|
193
|
+
</FormItem>
|
|
194
|
+
</Form>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
158
197
|
</Modal>
|
|
159
198
|
)
|
|
160
199
|
}
|
|
@@ -70,6 +70,7 @@ class Term extends Component {
|
|
|
70
70
|
hasSelection: false,
|
|
71
71
|
saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
|
|
72
72
|
addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
|
|
73
|
+
logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
|
|
73
74
|
passType: 'password',
|
|
74
75
|
lines: [],
|
|
75
76
|
searchResults: [],
|
|
@@ -400,10 +401,15 @@ class Term extends Component {
|
|
|
400
401
|
return
|
|
401
402
|
}
|
|
402
403
|
if (isSshTerminal) {
|
|
403
|
-
this.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
405
|
+
if (behavior === 'ask') {
|
|
406
|
+
this.setState({
|
|
407
|
+
dropFileModalVisible: true,
|
|
408
|
+
droppedFiles: [{ path: filePath, isRemote: true }]
|
|
409
|
+
})
|
|
410
|
+
} else {
|
|
411
|
+
this.handleDropFileAction(behavior, [{ path: filePath, isRemote: true }])
|
|
412
|
+
}
|
|
407
413
|
return
|
|
408
414
|
}
|
|
409
415
|
this.attachAddon._sendData(`"${filePath}" `)
|
|
@@ -425,10 +431,15 @@ class Term extends Component {
|
|
|
425
431
|
}
|
|
426
432
|
|
|
427
433
|
if (isSshTerminal) {
|
|
428
|
-
this.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
434
|
+
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
435
|
+
if (behavior === 'ask') {
|
|
436
|
+
this.setState({
|
|
437
|
+
dropFileModalVisible: true,
|
|
438
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
439
|
+
})
|
|
440
|
+
} else {
|
|
441
|
+
this.handleDropFileAction(behavior, filePaths.map(path => ({ path, isRemote: false })))
|
|
442
|
+
}
|
|
432
443
|
return
|
|
433
444
|
}
|
|
434
445
|
|
|
@@ -444,8 +455,8 @@ class Term extends Component {
|
|
|
444
455
|
})
|
|
445
456
|
}
|
|
446
457
|
|
|
447
|
-
handleDropFileAction = (action) => {
|
|
448
|
-
const
|
|
458
|
+
handleDropFileAction = (action, filesOverride) => {
|
|
459
|
+
const droppedFiles = filesOverride || this.state.droppedFiles
|
|
449
460
|
if (!droppedFiles || !droppedFiles.length) {
|
|
450
461
|
this.handleDropFileModalCancel()
|
|
451
462
|
return
|
|
@@ -454,7 +465,7 @@ class Term extends Component {
|
|
|
454
465
|
const filePaths = droppedFiles.map(f => f.path)
|
|
455
466
|
|
|
456
467
|
switch (action) {
|
|
457
|
-
case '
|
|
468
|
+
case 'trz': {
|
|
458
469
|
if (this.trzszClient && this.trzszClient.isActive) {
|
|
459
470
|
message.warning('A transfer is already in progress')
|
|
460
471
|
this.handleDropFileModalCancel()
|
|
@@ -464,7 +475,7 @@ class Term extends Component {
|
|
|
464
475
|
this.attachAddon._sendData('trz\r')
|
|
465
476
|
break
|
|
466
477
|
}
|
|
467
|
-
case '
|
|
478
|
+
case 'rz':{
|
|
468
479
|
if (this.zmodemClient && this.zmodemClient.isActive) {
|
|
469
480
|
message.warning('A transfer is already in progress')
|
|
470
481
|
this.handleDropFileModalCancel()
|
|
@@ -474,7 +485,7 @@ class Term extends Component {
|
|
|
474
485
|
this.attachAddon._sendData('rz\r')
|
|
475
486
|
break
|
|
476
487
|
}
|
|
477
|
-
case '
|
|
488
|
+
case 'inputOnly':
|
|
478
489
|
default: {
|
|
479
490
|
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
480
491
|
this.attachAddon._sendData(filesAll)
|
|
@@ -825,11 +836,15 @@ class Term extends Component {
|
|
|
825
836
|
if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
|
|
826
837
|
window.store.addCmdHistory(currentCmd.trim())
|
|
827
838
|
}
|
|
839
|
+
if (currentCmd && currentCmd.trim() === 'exit') {
|
|
840
|
+
this.userTypeExit = true
|
|
841
|
+
}
|
|
828
842
|
this.closeSuggestions()
|
|
829
843
|
}
|
|
830
844
|
}
|
|
831
845
|
|
|
832
846
|
onPasswordPromptDetected = () => {
|
|
847
|
+
window.store.notifyTabPasswordPrompt(this.props.tab.id)
|
|
833
848
|
if (!this.props.config.showCmdSuggestions) {
|
|
834
849
|
return
|
|
835
850
|
}
|
|
@@ -842,6 +857,7 @@ class Term extends Component {
|
|
|
842
857
|
}
|
|
843
858
|
|
|
844
859
|
onPasswordPromptCancelled = () => {
|
|
860
|
+
window.store.clearTabPasswordPrompt(this.props.tab.id)
|
|
845
861
|
const suggestions = refsStatic.get('terminal-suggestions')
|
|
846
862
|
if (suggestions?.state?.passwordMode) {
|
|
847
863
|
suggestions.closeSuggestions()
|
|
@@ -1221,7 +1237,7 @@ class Term extends Component {
|
|
|
1221
1237
|
...extra,
|
|
1222
1238
|
...execOpts,
|
|
1223
1239
|
logName,
|
|
1224
|
-
sessionLogPath:
|
|
1240
|
+
sessionLogPath: this.state.logPath,
|
|
1225
1241
|
...pick(config, [
|
|
1226
1242
|
'addTimeStampToTermLog',
|
|
1227
1243
|
'keepaliveCountMax',
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
* show base terminal info, id sessionID
|
|
3
3
|
*/
|
|
4
4
|
import { Component } from 'react'
|
|
5
|
-
import { osResolve } from '../../common/resolve'
|
|
6
5
|
import {
|
|
7
6
|
Switch,
|
|
8
7
|
Space,
|
|
9
8
|
Button
|
|
10
9
|
} from 'antd'
|
|
11
|
-
import ShowItem from '../common/show-item'
|
|
12
10
|
import defaults from '../../common/default-setting'
|
|
13
|
-
import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
|
|
11
|
+
import { toggleTerminalLog, toggleTerminalLogTimestamp, setTerminalLogPath } from '../terminal/terminal-apis'
|
|
14
12
|
import {
|
|
15
13
|
ClockCircleOutlined,
|
|
16
14
|
BorderlessTableOutlined,
|
|
@@ -19,8 +17,8 @@ import {
|
|
|
19
17
|
ApiOutlined,
|
|
20
18
|
PartitionOutlined
|
|
21
19
|
} from '@ant-design/icons'
|
|
22
|
-
import createDefaultSessionLogPath from '../../common/default-log-path'
|
|
23
20
|
import { refs } from '../common/ref'
|
|
21
|
+
import LogPathEdit from './log-path-edit'
|
|
24
22
|
|
|
25
23
|
const e = window.translate
|
|
26
24
|
|
|
@@ -36,7 +34,8 @@ const mapper = {
|
|
|
36
34
|
export default class TerminalInfoBase extends Component {
|
|
37
35
|
state = {
|
|
38
36
|
saveTerminalLogToFile: false,
|
|
39
|
-
addTimeStampToTermLog: false
|
|
37
|
+
addTimeStampToTermLog: false,
|
|
38
|
+
logPath: ''
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
componentDidMount () {
|
|
@@ -75,6 +74,17 @@ export default class TerminalInfoBase extends Component {
|
|
|
75
74
|
})
|
|
76
75
|
}
|
|
77
76
|
|
|
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
|
+
|
|
78
88
|
handleToggle = () => {
|
|
79
89
|
const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
|
|
80
90
|
const {
|
|
@@ -101,7 +111,8 @@ export default class TerminalInfoBase extends Component {
|
|
|
101
111
|
if (term) {
|
|
102
112
|
this.setState({
|
|
103
113
|
saveTerminalLogToFile: term.state.saveTerminalLogToFile,
|
|
104
|
-
addTimeStampToTermLog: term.state.addTimeStampToTermLog
|
|
114
|
+
addTimeStampToTermLog: term.state.addTimeStampToTermLog,
|
|
115
|
+
logPath: term.state.logPath
|
|
105
116
|
})
|
|
106
117
|
} else {
|
|
107
118
|
this.timer = setTimeout(this.getState, 100)
|
|
@@ -156,15 +167,10 @@ export default class TerminalInfoBase extends Component {
|
|
|
156
167
|
const {
|
|
157
168
|
id,
|
|
158
169
|
logName,
|
|
159
|
-
|
|
170
|
+
pid
|
|
160
171
|
} = this.props
|
|
161
|
-
const { saveTerminalLogToFile } = this.state
|
|
162
|
-
const base = sessionLogPath || createDefaultSessionLogPath()
|
|
163
|
-
const path = osResolve(base, logName + '.log')
|
|
172
|
+
const { saveTerminalLogToFile, logPath } = this.state
|
|
164
173
|
const name = e('saveTerminalLogToFile')
|
|
165
|
-
const to = saveTerminalLogToFile
|
|
166
|
-
? <ShowItem disabled={!saveTerminalLogToFile} to={path}>{path}</ShowItem>
|
|
167
|
-
: path
|
|
168
174
|
return (
|
|
169
175
|
<div className='terminal-info-section terminal-info-base'>
|
|
170
176
|
<div className='fix'>
|
|
@@ -187,7 +193,12 @@ export default class TerminalInfoBase extends Component {
|
|
|
187
193
|
this.renderInfoSelection()
|
|
188
194
|
}
|
|
189
195
|
</div>
|
|
190
|
-
<
|
|
196
|
+
<LogPathEdit
|
|
197
|
+
pid={pid}
|
|
198
|
+
logPath={logPath}
|
|
199
|
+
logName={logName}
|
|
200
|
+
setLogPath={this.onLogPathChange}
|
|
201
|
+
/>
|
|
191
202
|
</div>
|
|
192
203
|
)
|
|
193
204
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-terminal log path editor
|
|
3
|
+
* Reads/writes logPath on the terminal's own state via refs
|
|
4
|
+
*/
|
|
5
|
+
import { Button } from 'antd'
|
|
6
|
+
import message from '../common/message'
|
|
7
|
+
import InputConfirm from '../common/input-confirm'
|
|
8
|
+
import ShowItem from '../common/show-item'
|
|
9
|
+
import { chooseSaveDirectory } from '../../common/choose-save-folder'
|
|
10
|
+
import createDefaultLogPath from '../../common/default-log-path'
|
|
11
|
+
import { osResolve } from '../../common/resolve'
|
|
12
|
+
|
|
13
|
+
const e = window.translate
|
|
14
|
+
|
|
15
|
+
export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
|
|
16
|
+
const defaultPath = createDefaultLogPath()
|
|
17
|
+
const base = logPath || defaultPath
|
|
18
|
+
const fullPath = osResolve(base, logName + '.log')
|
|
19
|
+
|
|
20
|
+
const testAndSet = async (v) => {
|
|
21
|
+
if (v) {
|
|
22
|
+
try {
|
|
23
|
+
const { fs } = window
|
|
24
|
+
const uid = 'test-' + Date.now()
|
|
25
|
+
const testFile = osResolve(v, uid + '.test.log')
|
|
26
|
+
await fs.touch(testFile)
|
|
27
|
+
await fs.unlink(testFile)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.log('log path test failed', err)
|
|
30
|
+
message.error('invalid log folder')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
setLogPath(v)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handleChange = (v) => {
|
|
38
|
+
testAndSet(v)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleChooseFolder = async () => {
|
|
42
|
+
const path = await chooseSaveDirectory()
|
|
43
|
+
if (path) {
|
|
44
|
+
handleChange(path)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleReset = () => {
|
|
49
|
+
setLogPath('')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const inputProps = {
|
|
53
|
+
value: logPath,
|
|
54
|
+
placeholder: defaultPath,
|
|
55
|
+
onChange: handleChange,
|
|
56
|
+
addonAfter: (
|
|
57
|
+
<>
|
|
58
|
+
<Button
|
|
59
|
+
onClick={handleChooseFolder}
|
|
60
|
+
className='mg1r'
|
|
61
|
+
type='text'
|
|
62
|
+
size='small'
|
|
63
|
+
>
|
|
64
|
+
{e('chooseFolder')}
|
|
65
|
+
</Button>
|
|
66
|
+
<Button
|
|
67
|
+
size='small'
|
|
68
|
+
type='text'
|
|
69
|
+
onClick={handleReset}
|
|
70
|
+
>
|
|
71
|
+
{e('reset')}
|
|
72
|
+
</Button>
|
|
73
|
+
</>
|
|
74
|
+
),
|
|
75
|
+
prefix: e('terminalLogPath') + ': '
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className='pd1b'>
|
|
80
|
+
<InputConfirm {...inputProps} />
|
|
81
|
+
<div className='pd1t font-xs color-grey'>
|
|
82
|
+
{fullPath} <ShowItem to={fullPath} />
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -6,18 +6,30 @@ import { useState } from 'react'
|
|
|
6
6
|
import { Button, Input, Space } from 'antd'
|
|
7
7
|
import { safeGetItem, safeSetItem } from '../../common/safe-local-storage.js'
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
export const CUSTOM_EDITOR_COMMAND_LS_KEY = 'customEditorCommand'
|
|
10
|
+
export const CUSTOM_EDITOR_AUTO_OPEN_LS_KEY = 'customEditorAutoOpen'
|
|
10
11
|
const e = window.translate
|
|
11
12
|
|
|
12
13
|
export default function EditWithCustomEditor ({ loading, editWithCustom }) {
|
|
13
14
|
const [editorCommand, setEditorCommand] = useState(
|
|
14
|
-
() => safeGetItem(
|
|
15
|
+
() => safeGetItem(CUSTOM_EDITOR_COMMAND_LS_KEY) || ''
|
|
15
16
|
)
|
|
17
|
+
const [autoOpen, setAutoOpen] = useState(
|
|
18
|
+
() => safeGetItem(CUSTOM_EDITOR_AUTO_OPEN_LS_KEY) === 'true'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const autoOpenLabel = e('autoOpen')
|
|
16
22
|
|
|
17
23
|
function handleChange (ev) {
|
|
18
24
|
const val = ev.target.value
|
|
19
25
|
setEditorCommand(val)
|
|
20
|
-
safeSetItem(
|
|
26
|
+
safeSetItem(CUSTOM_EDITOR_COMMAND_LS_KEY, val)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleToggleAutoOpen () {
|
|
30
|
+
const next = !autoOpen
|
|
31
|
+
setAutoOpen(next)
|
|
32
|
+
safeSetItem(CUSTOM_EDITOR_AUTO_OPEN_LS_KEY, String(next))
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
function handleClick () {
|
|
@@ -45,6 +57,13 @@ export default function EditWithCustomEditor ({ loading, editWithCustom }) {
|
|
|
45
57
|
onChange={handleChange}
|
|
46
58
|
disabled={loading}
|
|
47
59
|
/>
|
|
60
|
+
<Button
|
|
61
|
+
type={autoOpen ? 'primary' : 'default'}
|
|
62
|
+
disabled={loading}
|
|
63
|
+
onClick={handleToggleAutoOpen}
|
|
64
|
+
>
|
|
65
|
+
{autoOpenLabel}: {autoOpen ? 'On' : 'Off'}
|
|
66
|
+
</Button>
|
|
48
67
|
</Space.Compact>
|
|
49
68
|
)
|
|
50
69
|
}
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { PureComponent } from 'react'
|
|
6
6
|
import TextEditorForm from './text-editor-form'
|
|
7
|
+
import {
|
|
8
|
+
CUSTOM_EDITOR_AUTO_OPEN_LS_KEY,
|
|
9
|
+
CUSTOM_EDITOR_COMMAND_LS_KEY
|
|
10
|
+
} from './edit-with-custom-editor'
|
|
7
11
|
import { Spin } from 'antd'
|
|
8
12
|
import Modal from '../common/modal'
|
|
9
13
|
import resolve from '../../common/resolve'
|
|
14
|
+
import { safeGetItem } from '../../common/safe-local-storage.js'
|
|
10
15
|
import { refsStatic, refs } from '../common/ref'
|
|
11
16
|
|
|
12
17
|
const e = window.translate
|
|
@@ -69,12 +74,28 @@ export default class TextEditor extends PureComponent {
|
|
|
69
74
|
return
|
|
70
75
|
}
|
|
71
76
|
const text = await fileRef.fetchEditorText(p, type)
|
|
77
|
+
const editorCommand = this.getAutoOpenCustomEditorCommand()
|
|
72
78
|
this.setStateProxy({
|
|
73
79
|
text,
|
|
74
80
|
loading: false
|
|
81
|
+
}, () => {
|
|
82
|
+
if (editorCommand) {
|
|
83
|
+
this.editWithCustom(editorCommand)
|
|
84
|
+
}
|
|
75
85
|
})
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
getAutoOpenCustomEditorCommand = () => {
|
|
89
|
+
if (window.et.isWebApp) {
|
|
90
|
+
return ''
|
|
91
|
+
}
|
|
92
|
+
const autoOpen = safeGetItem(CUSTOM_EDITOR_AUTO_OPEN_LS_KEY) === 'true'
|
|
93
|
+
if (!autoOpen) {
|
|
94
|
+
return ''
|
|
95
|
+
}
|
|
96
|
+
return safeGetItem(CUSTOM_EDITOR_COMMAND_LS_KEY).trim()
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
doSubmit = () => {
|
|
79
100
|
this.handleSubmit({
|
|
80
101
|
text: this.state.text
|
|
@@ -3,17 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { memo } from 'react'
|
|
6
|
-
import
|
|
6
|
+
import { createTitleTag } from '../../common/create-title'
|
|
7
7
|
import classnames from 'classnames'
|
|
8
8
|
import highlight from '../common/highlight'
|
|
9
9
|
import uid from '../../common/uid'
|
|
10
10
|
|
|
11
|
-
function getItemLabel (item, isGroup) {
|
|
12
|
-
return isGroup
|
|
13
|
-
? item?.title || ''
|
|
14
|
-
: createName(item)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
function areEqual (prevProps, nextProps) {
|
|
18
12
|
const prevSelected = prevProps.selectedItemId === prevProps.item.id
|
|
19
13
|
const nextSelected = nextProps.selectedItemId === nextProps.item.id
|
|
@@ -27,10 +21,10 @@ function areEqual (prevProps, nextProps) {
|
|
|
27
21
|
prevSelected === nextSelected &&
|
|
28
22
|
prevSearchSelected === nextSearchSelected &&
|
|
29
23
|
prevProps.item.id === nextProps.item.id &&
|
|
30
|
-
prevProps.
|
|
31
|
-
prevProps.
|
|
32
|
-
prevProps.
|
|
33
|
-
|
|
24
|
+
prevProps.itemLevel === nextProps.itemLevel &&
|
|
25
|
+
prevProps.itemColor === nextProps.itemColor &&
|
|
26
|
+
prevProps.itemDescription === nextProps.itemDescription &&
|
|
27
|
+
prevProps.itemLabel === nextProps.itemLabel
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
function TreeListItem (props) {
|
|
@@ -87,7 +81,7 @@ function TreeListItem (props) {
|
|
|
87
81
|
: null
|
|
88
82
|
const title = isGroup
|
|
89
83
|
? item.title
|
|
90
|
-
:
|
|
84
|
+
: props.itemLabel
|
|
91
85
|
const titleAll = title + (item.description ? ' - ' + item.description : '')
|
|
92
86
|
const titleHighlight = isGroup
|
|
93
87
|
? item.title || 'no title'
|
|
@@ -2,6 +2,7 @@ import TreeExpander from './tree-expander'
|
|
|
2
2
|
import TreeListItem from './tree-list-item'
|
|
3
3
|
import TreeItemOp from './tree-item-op'
|
|
4
4
|
import { treeLevelIndent } from './tree-list-layout'
|
|
5
|
+
import createName from '../../common/create-title'
|
|
5
6
|
|
|
6
7
|
export default function TreeListRow (props) {
|
|
7
8
|
const {
|
|
@@ -38,6 +39,10 @@ export default function TreeListRow (props) {
|
|
|
38
39
|
item,
|
|
39
40
|
isGroup,
|
|
40
41
|
parentId,
|
|
42
|
+
itemLabel: isGroup ? (item?.title || '') : createName(item),
|
|
43
|
+
itemColor: item?.color,
|
|
44
|
+
itemDescription: item?.description,
|
|
45
|
+
itemLevel: item?.level,
|
|
41
46
|
leftSidebarWidth,
|
|
42
47
|
staticList,
|
|
43
48
|
selectedItemId: activeItemId,
|
|
@@ -26,7 +26,9 @@ export function buildVisibleTreeRows ({
|
|
|
26
26
|
const item = bookmarksMap.get(bookmarkId)
|
|
27
27
|
const matched = Boolean(
|
|
28
28
|
item &&
|
|
29
|
-
(!lowerKeyword ||
|
|
29
|
+
(!lowerKeyword ||
|
|
30
|
+
createName(item).toLowerCase().includes(lowerKeyword) ||
|
|
31
|
+
(item.description || '').toLowerCase().includes(lowerKeyword))
|
|
30
32
|
)
|
|
31
33
|
bookmarkMatchCache.set(bookmarkId, matched)
|
|
32
34
|
return matched
|
|
@@ -50,6 +50,9 @@ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInsta
|
|
|
50
50
|
case 'string':
|
|
51
51
|
control = <Input placeholder={description} />
|
|
52
52
|
break
|
|
53
|
+
case 'textarea':
|
|
54
|
+
control = <Input.TextArea autoSize={{ minRows: 3 }} placeholder={description} />
|
|
55
|
+
break
|
|
53
56
|
case 'number':
|
|
54
57
|
control = <InputNumber style={{ width: '100%' }} placeholder={description} />
|
|
55
58
|
break
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import uid from '../common/uid'
|
|
7
7
|
import { settingMap } from '../common/constants'
|
|
8
|
-
import { refs } from '../components/common/ref'
|
|
8
|
+
import { refs, refsTabs } from '../components/common/ref'
|
|
9
9
|
import deepCopy from 'json-deep-copy'
|
|
10
10
|
import {
|
|
11
11
|
getLocalFileInfo,
|
|
@@ -107,6 +107,9 @@ export default Store => {
|
|
|
107
107
|
case 'get_terminal_output':
|
|
108
108
|
result = store.mcpGetTerminalOutput(args)
|
|
109
109
|
break
|
|
110
|
+
case 'wait_for_terminal_idle':
|
|
111
|
+
result = await store.mcpWaitForTerminalIdle(args)
|
|
112
|
+
break
|
|
110
113
|
|
|
111
114
|
// SFTP operations
|
|
112
115
|
case 'sftp_list':
|
|
@@ -331,15 +334,18 @@ export default Store => {
|
|
|
331
334
|
|
|
332
335
|
Store.prototype.mcpListTabs = function () {
|
|
333
336
|
const { store } = window
|
|
334
|
-
return store.tabs.map(t =>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
337
|
+
return store.tabs.map(t => {
|
|
338
|
+
return {
|
|
339
|
+
id: t.id,
|
|
340
|
+
title: t.title,
|
|
341
|
+
host: t.host,
|
|
342
|
+
type: t.type || 'local',
|
|
343
|
+
status: t.status,
|
|
344
|
+
isTransporting: t.isTransporting,
|
|
345
|
+
onData: refsTabs.get('tab-' + t.id)?.state.terminalOnData,
|
|
346
|
+
batch: t.batch
|
|
347
|
+
}
|
|
348
|
+
})
|
|
343
349
|
}
|
|
344
350
|
|
|
345
351
|
Store.prototype.mcpGetActiveTab = function () {
|
|
@@ -527,6 +533,73 @@ export default Store => {
|
|
|
527
533
|
}
|
|
528
534
|
}
|
|
529
535
|
|
|
536
|
+
Store.prototype.mcpWaitForTerminalIdle = async function (args) {
|
|
537
|
+
const { store } = window
|
|
538
|
+
const tabId = args.tabId || store.activeTabId
|
|
539
|
+
const timeout = Math.min(args.timeout || 30000, 120000)
|
|
540
|
+
const pollInterval = 500
|
|
541
|
+
const minWait = args.minWait !== undefined ? args.minWait : 1000
|
|
542
|
+
const lineCountToFetch = args.lines || 50
|
|
543
|
+
|
|
544
|
+
if (!tabId) {
|
|
545
|
+
throw new Error('No active terminal')
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const start = Date.now()
|
|
549
|
+
|
|
550
|
+
// Brief initial wait so the command has time to start producing output
|
|
551
|
+
if (minWait > 0) {
|
|
552
|
+
await new Promise(resolve => setTimeout(resolve, minWait))
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const collectOutput = () => {
|
|
556
|
+
const term = refs.get('term-' + tabId)
|
|
557
|
+
if (!term || !term.term) return { output: '', lineCount: 0 }
|
|
558
|
+
const buffer = term.term.buffer.active
|
|
559
|
+
if (!buffer) return { output: '', lineCount: 0 }
|
|
560
|
+
const cursorY = buffer.cursorY || 0
|
|
561
|
+
const baseY = buffer.baseY || 0
|
|
562
|
+
const totalLines = buffer.length || 0
|
|
563
|
+
const actualContentEnd = baseY + cursorY + 1
|
|
564
|
+
const startLine = Math.max(0, actualContentEnd - lineCountToFetch)
|
|
565
|
+
const endLine = Math.min(totalLines, actualContentEnd)
|
|
566
|
+
const lines = []
|
|
567
|
+
for (let i = startLine; i < endLine; i++) {
|
|
568
|
+
const line = buffer.getLine(i)
|
|
569
|
+
if (line) lines.push(line.translateToString(true))
|
|
570
|
+
}
|
|
571
|
+
return { output: lines.join('\n'), lineCount: lines.length }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Poll until onData becomes false (4s idle debounce in tab.jsx)
|
|
575
|
+
while (Date.now() - start < timeout) {
|
|
576
|
+
const tabRef = refsTabs.get('tab-' + tabId)
|
|
577
|
+
const onData = tabRef?.state.terminalOnData
|
|
578
|
+
if (!onData) {
|
|
579
|
+
const { output, lineCount } = collectOutput()
|
|
580
|
+
return {
|
|
581
|
+
tabId,
|
|
582
|
+
elapsed: Date.now() - start,
|
|
583
|
+
timedOut: false,
|
|
584
|
+
output,
|
|
585
|
+
lineCount
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Timeout reached — return whatever is currently in the buffer
|
|
592
|
+
const { output, lineCount } = collectOutput()
|
|
593
|
+
return {
|
|
594
|
+
tabId,
|
|
595
|
+
elapsed: Date.now() - start,
|
|
596
|
+
timedOut: true,
|
|
597
|
+
message: `Terminal still active after ${timeout}ms`,
|
|
598
|
+
output,
|
|
599
|
+
lineCount
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
530
603
|
// ==================== Settings APIs ====================
|
|
531
604
|
|
|
532
605
|
Store.prototype.mcpGetSettings = function () {
|
package/client/store/tab.js
CHANGED
|
@@ -622,6 +622,20 @@ export default Store => {
|
|
|
622
622
|
}
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
+
Store.prototype.notifyTabPasswordPrompt = function (tabId) {
|
|
626
|
+
const tab = refsTabs.get('tab-' + tabId)
|
|
627
|
+
if (tab) {
|
|
628
|
+
tab.notifyPasswordPrompt()
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
Store.prototype.clearTabPasswordPrompt = function (tabId) {
|
|
633
|
+
const tab = refsTabs.get('tab-' + tabId)
|
|
634
|
+
if (tab) {
|
|
635
|
+
tab.clearPasswordPrompt()
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
625
639
|
Store.prototype.remoteList = function (tabId) {
|
|
626
640
|
const sftp = refs.get('sftp-' + tabId)
|
|
627
641
|
if (sftp) {
|