@electerm/electerm-react 5.5.25 → 5.5.56
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/README.md +5 -5
- package/client/common/bookmark-schemas.js +4 -0
- package/client/common/constants.js +3 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +2 -0
- package/client/common/parse-quick-connect.js +1 -0
- package/client/common/parse-vv.js +119 -25
- package/client/common/parse-widget-log.js +57 -0
- package/client/common/vv-to-tab.js +120 -0
- package/client/components/ai/agent-tool-call-card.jsx +26 -7
- package/client/components/ai/agent.js +205 -45
- package/client/components/ai/ai-auto-compress.js +119 -0
- package/client/components/ai/ai-chat-history-item.jsx +133 -84
- package/client/components/ai/ai-chat.jsx +135 -30
- package/client/components/ai/ai-compress.js +95 -0
- package/client/components/ai/ai-config-props.js +12 -1
- package/client/components/ai/ai-config.jsx +43 -16
- package/client/components/ai/ai-config.styl +20 -0
- package/client/components/ai/ai-context-indicator.jsx +83 -0
- package/client/components/ai/ai-context.js +272 -0
- package/client/components/ai/ai-output.jsx +5 -6
- package/client/components/ai/ai-presets.js +1 -16
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/batch-op/batch-op-runner.jsx +1 -0
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/common/ssh-agent.jsx +31 -21
- package/client/components/bookmark-form/common/vv-file-field.jsx +3 -2
- package/client/components/bookmark-form/config/spice.js +21 -0
- package/client/components/bookmark-form/config/ssh.js +1 -0
- package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
- package/client/components/file-transfer/transfer.jsx +3 -119
- package/client/components/footer/cmd-history.jsx +279 -65
- package/client/components/footer/cmd-history.styl +68 -1
- package/client/components/footer/footer-entry.jsx +7 -1
- package/client/components/layout/layout.jsx +12 -19
- package/client/components/main/main.jsx +15 -1
- package/client/components/quick-commands/qm.styl +58 -0
- package/client/components/quick-commands/quick-commands-box.jsx +180 -83
- package/client/components/quick-commands/quick-commands-select.jsx +11 -0
- package/client/components/session/session-control.jsx +35 -2
- package/client/components/session/session-control.styl +5 -0
- package/client/components/session/session.jsx +13 -0
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/setting-panel/tab-widgets.jsx +27 -7
- package/client/components/sftp/file-item.jsx +0 -13
- package/client/components/side-panel-r/side-panel-r.jsx +11 -4
- package/client/components/sidebar/bookmark-select.jsx +14 -2
- package/client/components/sidebar/history-item.jsx +20 -4
- package/client/components/sidebar/history.jsx +7 -0
- package/client/components/terminal/mixins/term-touch.js +18 -0
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/components/widgets/widget-control.jsx +4 -2
- package/client/components/widgets/widget-instance-detail.jsx +370 -0
- package/client/components/widgets/widget-instance.jsx +30 -13
- package/client/components/widgets/widget-instances.jsx +3 -1
- package/client/components/widgets/widgets-list.jsx +8 -0
- package/client/components/widgets/widgets.styl +100 -0
- package/client/store/common.js +200 -54
- package/client/store/init-state.js +15 -0
- package/client/store/load-data.js +73 -0
- package/client/store/store.js +6 -0
- package/client/store/watch.js +3 -1
- package/client/store/widgets.js +23 -5
- package/package.json +1 -1
- package/client/components/file-transfer/zip.js +0 -42
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
max-height calc(100vh - 100px)
|
|
3
3
|
overflow-y scroll
|
|
4
4
|
padding 20px 0 10px 0
|
|
5
|
+
|
|
6
|
+
// Docked in the right side panel the panel *is* the container, so the quick
|
|
7
|
+
// commands fill it instead of sizing themselves like the footer box. The parent
|
|
8
|
+
// (.right-side-panel-content) is an absolutely positioned box with top and
|
|
9
|
+
// bottom set, so 100% resolves to a real height and the list scrolls inside it
|
|
10
|
+
// rather than scrolling the panel.
|
|
11
|
+
.qm-panel-in-right-panel
|
|
12
|
+
display flex
|
|
13
|
+
flex-direction column
|
|
14
|
+
width 100%
|
|
15
|
+
height 100%
|
|
16
|
+
> .pd2
|
|
17
|
+
display flex
|
|
18
|
+
flex-direction column
|
|
19
|
+
height 100%
|
|
20
|
+
min-height 0
|
|
21
|
+
width 100%
|
|
22
|
+
.qm-flex
|
|
23
|
+
flex-shrink 0
|
|
24
|
+
// the footer box is as wide as the footer; the right panel is not, and the
|
|
25
|
+
// search + label + sort + actions row does not fit in one line there
|
|
26
|
+
flex-wrap wrap
|
|
27
|
+
// ...so give the search a line of its own and let it fill it, instead of
|
|
28
|
+
// sitting at its footer width with a gap beside it. `flex-basis: 100%` is what
|
|
29
|
+
// forces the wrap; the controls then share the line below.
|
|
30
|
+
.qm-search-input
|
|
31
|
+
flex 1 1 100%
|
|
32
|
+
max-width none
|
|
33
|
+
.qm-list-wrap
|
|
34
|
+
flex 1
|
|
35
|
+
min-height 0
|
|
36
|
+
max-height none
|
|
37
|
+
overflow-y auto
|
|
38
|
+
|
|
39
|
+
.qm-panel-actions
|
|
40
|
+
display flex
|
|
41
|
+
align-items center
|
|
42
|
+
gap 6px
|
|
43
|
+
|
|
44
|
+
.qm-move-icon
|
|
45
|
+
font-size 14px
|
|
46
|
+
padding 4px
|
|
47
|
+
cursor pointer
|
|
48
|
+
&:hover
|
|
49
|
+
color var(--primary)
|
|
50
|
+
|
|
51
|
+
// The footer Q stays lit while the panel is docked in the right panel, same
|
|
52
|
+
// contract as the info, AI and cmd history icons
|
|
53
|
+
.quick-command-trigger
|
|
54
|
+
&.active
|
|
55
|
+
color var(--success)
|
|
56
|
+
|
|
5
57
|
.qm-wrap-tooltip
|
|
6
58
|
background var(--main)
|
|
7
59
|
position absolute
|
|
@@ -80,6 +132,12 @@
|
|
|
80
132
|
width 100%
|
|
81
133
|
.qm-label-select
|
|
82
134
|
width 100%
|
|
135
|
+
// The docked panel's search claims a whole row with `flex-basis: 100%`, which
|
|
136
|
+
// is a width only while `.qm-flex` is a row. Here it is a column, so the same
|
|
137
|
+
// basis would be 100% of the *height* and would push the controls off the
|
|
138
|
+
// panel. Same specificity as the rule above it, declared later, so it wins.
|
|
139
|
+
.qm-panel-in-right-panel .qm-search-input
|
|
140
|
+
flex none
|
|
83
141
|
|
|
84
142
|
// On mobile, stack the quick-command row (name / delay / command / remove)
|
|
85
143
|
// vertically instead of side by side — the horizontal Space.Compact layout
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* quick commands
|
|
2
|
+
* quick commands panel
|
|
3
|
+
*
|
|
4
|
+
* The panel has two homes: the floating footer box (default) and the right side
|
|
5
|
+
* panel. Which one it is in is a stored preference
|
|
6
|
+
* (store.quickCommandsInRightPanel), so the same component renders both —
|
|
7
|
+
* `inline` drops the floating box and lets the right panel's own container wrap
|
|
8
|
+
* the panel body.
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
11
|
import { useState } from 'react'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
12
|
+
import { auto } from 'manate/react'
|
|
13
|
+
import {
|
|
14
|
+
quickCommandLabelsLsKey,
|
|
15
|
+
pinnedQuickCommandBarKey
|
|
16
|
+
} from '../../common/constants'
|
|
17
|
+
import { sortBy, pick } from 'lodash-es'
|
|
8
18
|
import { Button, Input, Select, Space, Flex } from 'antd'
|
|
9
19
|
import * as ls from '../../common/safe-local-storage'
|
|
10
20
|
import CmdItem from './quick-command-item'
|
|
11
21
|
import {
|
|
12
22
|
EditOutlined,
|
|
13
23
|
CloseCircleOutlined,
|
|
14
|
-
PushpinOutlined
|
|
24
|
+
PushpinOutlined,
|
|
25
|
+
VerticalLeftOutlined,
|
|
26
|
+
VerticalAlignBottomOutlined
|
|
15
27
|
} from '@ant-design/icons'
|
|
16
28
|
import classNames from 'classnames'
|
|
17
29
|
import onDropFunc from './on-drop'
|
|
@@ -22,9 +34,42 @@ const e = window.translate
|
|
|
22
34
|
const addQuickCommands = 'addQuickCommands'
|
|
23
35
|
const { Option } = Select
|
|
24
36
|
|
|
25
|
-
export default function QuickCommandsFooterBox (props) {
|
|
37
|
+
export default auto(function QuickCommandsFooterBox (props) {
|
|
38
|
+
const { store, inline } = props
|
|
26
39
|
const [keyword, setKeyword] = useState('')
|
|
27
40
|
const [label, setLabel] = useState(ls.getItem(quickCommandLabelsLsKey, ''))
|
|
41
|
+
// read through the store so the panel re-renders on its own: it is rendered
|
|
42
|
+
// in two different places (footer box and right panel), and only one of them
|
|
43
|
+
// gets props from the layout
|
|
44
|
+
const {
|
|
45
|
+
openQuickCommandBar,
|
|
46
|
+
pinnedQuickCommandBar,
|
|
47
|
+
qmSortByFrequency,
|
|
48
|
+
quickCommandsInRightPanel,
|
|
49
|
+
inActiveTerminal,
|
|
50
|
+
leftSidePanelWidth,
|
|
51
|
+
leftSideBarWidth,
|
|
52
|
+
openedSideBar,
|
|
53
|
+
rightPanelVisible,
|
|
54
|
+
rightPanelPinned,
|
|
55
|
+
rightPanelWidth,
|
|
56
|
+
isMobile
|
|
57
|
+
} = pick(store, [
|
|
58
|
+
'openQuickCommandBar',
|
|
59
|
+
'pinnedQuickCommandBar',
|
|
60
|
+
'qmSortByFrequency',
|
|
61
|
+
'quickCommandsInRightPanel',
|
|
62
|
+
'inActiveTerminal',
|
|
63
|
+
'leftSidePanelWidth',
|
|
64
|
+
'leftSideBarWidth',
|
|
65
|
+
'openedSideBar',
|
|
66
|
+
'rightPanelVisible',
|
|
67
|
+
'rightPanelPinned',
|
|
68
|
+
'rightPanelWidth',
|
|
69
|
+
'isMobile'
|
|
70
|
+
])
|
|
71
|
+
const all = store.currentQuickCommands
|
|
72
|
+
const quickCommandTags = store.quickCommandTags
|
|
28
73
|
|
|
29
74
|
function handleTogglePinned () {
|
|
30
75
|
const current = !window.store.pinnedQuickCommandBar
|
|
@@ -58,12 +103,12 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
58
103
|
setLabel(v)
|
|
59
104
|
}
|
|
60
105
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
// Dock the panel into the right side panel. The footer box unmounts on the
|
|
107
|
+
// way out (it is gated on quickCommandsInRightPanel), so there is nothing to
|
|
108
|
+
// clean up here — unlike the cmd history popover, which has to be closed.
|
|
109
|
+
function handleMoveToRightPanel () {
|
|
110
|
+
window.store.moveQuickCommandsToRightPanel()
|
|
111
|
+
}
|
|
67
112
|
|
|
68
113
|
function onDragOver (e) {
|
|
69
114
|
e.preventDefault()
|
|
@@ -114,9 +159,6 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
114
159
|
}
|
|
115
160
|
|
|
116
161
|
function renderItem (item) {
|
|
117
|
-
const {
|
|
118
|
-
qmSortByFrequency
|
|
119
|
-
} = props
|
|
120
162
|
return (
|
|
121
163
|
<CmdItem
|
|
122
164
|
item={item}
|
|
@@ -156,26 +198,69 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
156
198
|
})
|
|
157
199
|
}
|
|
158
200
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
201
|
+
// The two "move" icons are mirror images and sit in the same slot: the footer
|
|
202
|
+
// box offers to dock the panel into the right side panel, the docked panel
|
|
203
|
+
// offers to hand it back to the footer. Same glyph pair as the cmd history
|
|
204
|
+
// panel (cmd-history.jsx), so the two panels read the same way.
|
|
205
|
+
// Note VerticalLeftOutlined is the one whose bar is on the right (arrow
|
|
206
|
+
// pointing right); VerticalAlignBottomOutlined points at the footer.
|
|
207
|
+
function renderMoveIcon () {
|
|
208
|
+
if (inline) {
|
|
209
|
+
return (
|
|
210
|
+
<VerticalAlignBottomOutlined
|
|
211
|
+
className='qm-move-icon pointer'
|
|
212
|
+
title={e('moveToFooter')}
|
|
213
|
+
onClick={() => window.store.moveQuickCommandsToFooter()}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
return (
|
|
218
|
+
<VerticalLeftOutlined
|
|
219
|
+
className='qm-move-icon pointer'
|
|
220
|
+
title={e('moveToRightPanel')}
|
|
221
|
+
onClick={handleMoveToRightPanel}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
174
224
|
}
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
225
|
+
|
|
226
|
+
// Docked, the right panel owns the pin and the close button (both live in its
|
|
227
|
+
// title bar), so the panel keeps only the actions that are about the quick
|
|
228
|
+
// commands themselves. A pin left over from the footer is kept in the store
|
|
229
|
+
// and comes back with the panel, so nothing is silently lost.
|
|
230
|
+
function renderActions () {
|
|
231
|
+
const tp = pinnedQuickCommandBar
|
|
232
|
+
? 'primary'
|
|
233
|
+
: 'text'
|
|
234
|
+
return (
|
|
235
|
+
<div className='qm-panel-actions mg2l'>
|
|
236
|
+
{renderMoveIcon()}
|
|
237
|
+
<Space.Compact>
|
|
238
|
+
{
|
|
239
|
+
!inline && (
|
|
240
|
+
<Button
|
|
241
|
+
onClick={handleTogglePinned}
|
|
242
|
+
icon={<PushpinOutlined />}
|
|
243
|
+
type={tp}
|
|
244
|
+
/>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
<Button
|
|
248
|
+
onClick={window.store.handleOpenQuickCommandsSetting}
|
|
249
|
+
icon={<EditOutlined />}
|
|
250
|
+
/>
|
|
251
|
+
{
|
|
252
|
+
!inline && (
|
|
253
|
+
<Button
|
|
254
|
+
onClick={handleClose}
|
|
255
|
+
icon={<CloseCircleOutlined />}
|
|
256
|
+
/>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
</Space.Compact>
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
179
264
|
const keyword0 = keyword.toLowerCase()
|
|
180
265
|
const filtered = filterArray(all, keyword0, label)
|
|
181
266
|
const sorted = qmSortByFrequency
|
|
@@ -188,9 +273,6 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
188
273
|
className: 'qm-label-select',
|
|
189
274
|
allowClear: true
|
|
190
275
|
}
|
|
191
|
-
const tp = pinnedQuickCommandBar
|
|
192
|
-
? 'primary'
|
|
193
|
-
: 'text'
|
|
194
276
|
const cls = classNames('qm-list-wrap')
|
|
195
277
|
const type = qmSortByFrequency ? 'primary' : 'default'
|
|
196
278
|
// Mirrors the footer's left offset. Mobile reserves nothing for the side
|
|
@@ -214,56 +296,71 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
214
296
|
className: 'qm-wrap-tooltip',
|
|
215
297
|
style: qmStyle
|
|
216
298
|
}
|
|
299
|
+
|
|
300
|
+
const content = (
|
|
301
|
+
<div className='pd2'>
|
|
302
|
+
<Flex justify='space-between' className='qm-flex'>
|
|
303
|
+
<Input.Search
|
|
304
|
+
value={keyword}
|
|
305
|
+
onChange={handleChange}
|
|
306
|
+
placeholder=''
|
|
307
|
+
className='qm-search-input'
|
|
308
|
+
/>
|
|
309
|
+
<Flex gap='small'>
|
|
310
|
+
<Select
|
|
311
|
+
{...sprops}
|
|
312
|
+
>
|
|
313
|
+
{quickCommandTags.map(
|
|
314
|
+
renderTag
|
|
315
|
+
)}
|
|
316
|
+
</Select>
|
|
317
|
+
<Button
|
|
318
|
+
type={type}
|
|
319
|
+
onClick={window.store.handleSortByFrequency}
|
|
320
|
+
>
|
|
321
|
+
{e('sortByFrequency')}
|
|
322
|
+
</Button>
|
|
323
|
+
</Flex>
|
|
324
|
+
{renderActions()}
|
|
325
|
+
</Flex>
|
|
326
|
+
<div className={cls}>
|
|
327
|
+
{sorted.map(renderItem)}
|
|
328
|
+
{
|
|
329
|
+
!sorted.length && renderNoCmd()
|
|
330
|
+
}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
// docked in the right side panel: no floating box, no pin/close of its own —
|
|
336
|
+
// the right panel decides when this is on screen, via store.rightPanelTab
|
|
337
|
+
if (inline) {
|
|
338
|
+
if (store.rightPanelTab !== 'quickCommands') {
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
return (
|
|
342
|
+
<div className='qm-panel-in-right-panel'>
|
|
343
|
+
{content}
|
|
344
|
+
</div>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Dormant while the panel is docked: the box would otherwise float over the
|
|
349
|
+
// terminal with nothing to show for it, and a pin left on would keep
|
|
350
|
+
// reserving the strip of height it occupies (see layout.jsx).
|
|
351
|
+
if (
|
|
352
|
+
quickCommandsInRightPanel ||
|
|
353
|
+
(!openQuickCommandBar && !pinnedQuickCommandBar) ||
|
|
354
|
+
!inActiveTerminal
|
|
355
|
+
) {
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
|
|
217
359
|
return (
|
|
218
360
|
<div
|
|
219
361
|
{...qmProps}
|
|
220
362
|
>
|
|
221
|
-
|
|
222
|
-
<Flex justify='space-between' className='qm-flex'>
|
|
223
|
-
<Input.Search
|
|
224
|
-
value={keyword}
|
|
225
|
-
onChange={handleChange}
|
|
226
|
-
placeholder=''
|
|
227
|
-
className='qm-search-input'
|
|
228
|
-
/>
|
|
229
|
-
<Flex gap='small'>
|
|
230
|
-
<Select
|
|
231
|
-
{...sprops}
|
|
232
|
-
>
|
|
233
|
-
{props.quickCommandTags.map(
|
|
234
|
-
renderTag
|
|
235
|
-
)}
|
|
236
|
-
</Select>
|
|
237
|
-
<Button
|
|
238
|
-
type={type}
|
|
239
|
-
onClick={window.store.handleSortByFrequency}
|
|
240
|
-
>
|
|
241
|
-
{e('sortByFrequency')}
|
|
242
|
-
</Button>
|
|
243
|
-
</Flex>
|
|
244
|
-
<Space.Compact className='mg2l'>
|
|
245
|
-
<Button
|
|
246
|
-
onClick={handleTogglePinned}
|
|
247
|
-
icon={<PushpinOutlined />}
|
|
248
|
-
type={tp}
|
|
249
|
-
/>
|
|
250
|
-
<Button
|
|
251
|
-
onClick={window.store.handleOpenQuickCommandsSetting}
|
|
252
|
-
icon={<EditOutlined />}
|
|
253
|
-
/>
|
|
254
|
-
<Button
|
|
255
|
-
onClick={handleClose}
|
|
256
|
-
icon={<CloseCircleOutlined />}
|
|
257
|
-
/>
|
|
258
|
-
</Space.Compact>
|
|
259
|
-
</Flex>
|
|
260
|
-
<div className={cls}>
|
|
261
|
-
{sorted.map(renderItem)}
|
|
262
|
-
{
|
|
263
|
-
!sorted.length && renderNoCmd()
|
|
264
|
-
}
|
|
265
|
-
</div>
|
|
266
|
-
</div>
|
|
363
|
+
{content}
|
|
267
364
|
</div>
|
|
268
365
|
)
|
|
269
|
-
}
|
|
366
|
+
})
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* on touch (mobile) and mouse (desktop) — the old hover-to-open model was
|
|
6
6
|
* unreliable on Android, where a tap synthesizes mouseleave and cancels the
|
|
7
7
|
* open timer.
|
|
8
|
+
*
|
|
9
|
+
* The panel has two homes (see quick-commands-box.jsx): the footer box and the
|
|
10
|
+
* right side panel. While it is docked, this button is repurposed to open and
|
|
11
|
+
* close that panel, exactly like the info and AI icons do — and `active` (fed
|
|
12
|
+
* by the footer, which is already reactive) keeps it lit while it is showing.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { PureComponent } from 'react'
|
|
@@ -14,15 +19,21 @@ import './qm.styl'
|
|
|
14
19
|
export default class QuickCommandsFooter extends PureComponent {
|
|
15
20
|
handleClick = () => {
|
|
16
21
|
const { store } = window
|
|
22
|
+
if (store.quickCommandsInRightPanel) {
|
|
23
|
+
store.toggleQuickCommandsPanel()
|
|
24
|
+
return
|
|
25
|
+
}
|
|
17
26
|
store.openQuickCommandBar = !store.openQuickCommandBar
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
render () {
|
|
30
|
+
const { active } = this.props
|
|
21
31
|
return (
|
|
22
32
|
<div className='fleft relative quick-command-trigger-wrap'>
|
|
23
33
|
<Button
|
|
24
34
|
size='small'
|
|
25
35
|
type='text'
|
|
36
|
+
className={'quick-command-trigger' + (active ? ' active' : '')}
|
|
26
37
|
onClick={this.handleClick}
|
|
27
38
|
>
|
|
28
39
|
Q
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PaperClipOutlined,
|
|
9
9
|
ApartmentOutlined,
|
|
10
10
|
MoreOutlined,
|
|
11
|
+
MenuOutlined,
|
|
11
12
|
ColumnWidthOutlined,
|
|
12
13
|
LogoutOutlined,
|
|
13
14
|
PlusOutlined,
|
|
@@ -39,6 +40,7 @@ export default function SessionControl (props) {
|
|
|
39
40
|
keepaliveEnabled,
|
|
40
41
|
broadcastInput,
|
|
41
42
|
wrapDisabled,
|
|
43
|
+
pasteWhenContextMenu,
|
|
42
44
|
onChangePane,
|
|
43
45
|
toggleCheckSftpPathFollowSsh,
|
|
44
46
|
onSshSftpSplitView,
|
|
@@ -47,6 +49,7 @@ export default function SessionControl (props) {
|
|
|
47
49
|
toggleWrap,
|
|
48
50
|
onFullscreen,
|
|
49
51
|
onOpenSearch,
|
|
52
|
+
onOpenContextMenu,
|
|
50
53
|
onExitGracefully,
|
|
51
54
|
onZoomFontSize
|
|
52
55
|
} = props
|
|
@@ -279,7 +282,35 @@ export default function SessionControl (props) {
|
|
|
279
282
|
)
|
|
280
283
|
}
|
|
281
284
|
|
|
282
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Touch devices have no right click, so the terminal's context menu needs a
|
|
287
|
+
* visible way in: this icon, next to the fullscreen icon on the desktop
|
|
288
|
+
* toolbar and next to the three-dot toggle in the mobile one (see below).
|
|
289
|
+
* Hidden when there is no menu to open — `pasteWhenContextMenu` turns both
|
|
290
|
+
* right click and the long press into a paste (see terminal.jsx), and it
|
|
291
|
+
* only ever acts on the terminal pane.
|
|
292
|
+
*/
|
|
293
|
+
function renderContextMenuIcon (inMobileBar) {
|
|
294
|
+
if (
|
|
295
|
+
!isTouchDevice ||
|
|
296
|
+
pasteWhenContextMenu ||
|
|
297
|
+
tab.pane !== paneMap.terminal
|
|
298
|
+
) {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
const cls = classnames(
|
|
302
|
+
'iblock pointer spliter context-menu-control-icon',
|
|
303
|
+
inMobileBar ? 'mobile-context-menu-toggle' : 'mg1r icon-info'
|
|
304
|
+
)
|
|
305
|
+
return (
|
|
306
|
+
<MenuOutlined
|
|
307
|
+
className={cls}
|
|
308
|
+
onClick={onOpenContextMenu}
|
|
309
|
+
/>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderTermControls (skipContextMenuIcon) {
|
|
283
314
|
const { pane } = tab
|
|
284
315
|
if (pane !== paneMap.terminal) {
|
|
285
316
|
return null
|
|
@@ -287,6 +318,7 @@ export default function SessionControl (props) {
|
|
|
287
318
|
return (
|
|
288
319
|
<div className='fright term-controls'>
|
|
289
320
|
{renderFontSizeIcons()}
|
|
321
|
+
{skipContextMenuIcon ? null : renderContextMenuIcon()}
|
|
290
322
|
{renderFullscreenIcon()}
|
|
291
323
|
{renderSearchIcon()}
|
|
292
324
|
</div>
|
|
@@ -303,12 +335,13 @@ export default function SessionControl (props) {
|
|
|
303
335
|
{renderBroadcastIcon()}
|
|
304
336
|
{renderWrapIcon()}
|
|
305
337
|
{renderExitGracefullyIcon()}
|
|
306
|
-
{renderTermControls()}
|
|
338
|
+
{renderTermControls(true)}
|
|
307
339
|
</div>
|
|
308
340
|
)
|
|
309
341
|
return (
|
|
310
342
|
<div className='terminal-control mobile-session-control'>
|
|
311
343
|
{renderPaneControl()}
|
|
344
|
+
{renderContextMenuIcon(true)}
|
|
312
345
|
<Popover
|
|
313
346
|
content={extraIcons}
|
|
314
347
|
trigger='click'
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
flex 1
|
|
15
15
|
min-width 0
|
|
16
16
|
.mobile-control-toggle
|
|
17
|
+
.mobile-context-menu-toggle
|
|
17
18
|
font-size 16px
|
|
18
19
|
line-height 32px
|
|
19
20
|
color var(--text)
|
|
@@ -21,6 +22,10 @@
|
|
|
21
22
|
&:hover
|
|
22
23
|
color var(--text-light)
|
|
23
24
|
|
|
25
|
+
// touch-only terminal context menu icon, in the bar next to the toggle above
|
|
26
|
+
.mobile-context-menu-toggle
|
|
27
|
+
margin-right 10px
|
|
28
|
+
|
|
24
29
|
// Popover content — icons laid out in a row, wrapping as needed
|
|
25
30
|
.mobile-control-icons
|
|
26
31
|
display flex
|
|
@@ -480,6 +480,17 @@ export default class SessionWrapper extends Component {
|
|
|
480
480
|
refs.get('term-' + this.props.tab.id)?.toggleSearch()
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
// touch-only: the toolbar menu icon opens the same context menu a right
|
|
484
|
+
// click (or a long press) does, anchored just below the icon itself
|
|
485
|
+
handleOpenContextMenu = (evt) => {
|
|
486
|
+
const term = refs.get('term-' + this.props.tab.id)
|
|
487
|
+
if (!term) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
const rect = evt.currentTarget.getBoundingClientRect()
|
|
491
|
+
term.openContextMenuAtPoint(rect.left, rect.bottom)
|
|
492
|
+
}
|
|
493
|
+
|
|
483
494
|
handleZoomFontSize = (v) => {
|
|
484
495
|
const term = refs.get('term-' + this.props.tab.id)
|
|
485
496
|
if (!term?.term) {
|
|
@@ -625,6 +636,7 @@ export default class SessionWrapper extends Component {
|
|
|
625
636
|
keepaliveEnabled={this.state.keepaliveEnabled}
|
|
626
637
|
broadcastInput={this.state.broadcastInput}
|
|
627
638
|
wrapDisabled={this.state.wrapDisabled}
|
|
639
|
+
pasteWhenContextMenu={!!this.props.config.pasteWhenContextMenu}
|
|
628
640
|
onChangePane={(pane) => this.onChangePane(pane)}
|
|
629
641
|
toggleCheckSftpPathFollowSsh={this.toggleCheckSftpPathFollowSsh}
|
|
630
642
|
onSshSftpSplitView={this.handleSshSftpSplitView}
|
|
@@ -634,6 +646,7 @@ export default class SessionWrapper extends Component {
|
|
|
634
646
|
onFullscreen={this.handleFullscreen}
|
|
635
647
|
onZoomFontSize={this.handleZoomFontSize}
|
|
636
648
|
onOpenSearch={this.handleOpenSearch}
|
|
649
|
+
onOpenContextMenu={this.handleOpenContextMenu}
|
|
637
650
|
onExitGracefully={this.handleExitGracefully}
|
|
638
651
|
/>
|
|
639
652
|
<Suspense fallback={null}>
|
|
@@ -574,7 +574,9 @@ export default class SettingCommon extends Component {
|
|
|
574
574
|
'allowMultiInstance',
|
|
575
575
|
'disableDeveloperTool',
|
|
576
576
|
'switchTabOnHover',
|
|
577
|
+
'doubleClickToOpenBookmark',
|
|
577
578
|
'disableTabIndex',
|
|
579
|
+
'onlyShowTitleInTab',
|
|
578
580
|
'disableShortcutBar',
|
|
579
581
|
'debug'
|
|
580
582
|
]
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import SettingCol from './col'
|
|
2
2
|
import WidgetControl from '../widgets/widget-control'
|
|
3
|
+
import WidgetInstanceDetail from '../widgets/widget-instance-detail'
|
|
3
4
|
import WidgetList from '../widgets/widgets-list'
|
|
5
|
+
import { auto } from 'manate/react'
|
|
4
6
|
import {
|
|
5
7
|
settingMap
|
|
6
8
|
} from '../../common/constants'
|
|
7
9
|
|
|
8
|
-
export default function TabWidgets (props) {
|
|
10
|
+
export default auto(function TabWidgets (props) {
|
|
9
11
|
const {
|
|
10
12
|
settingTab
|
|
11
13
|
} = props
|
|
@@ -15,8 +17,14 @@ export default function TabWidgets (props) {
|
|
|
15
17
|
const {
|
|
16
18
|
settingItem,
|
|
17
19
|
listProps,
|
|
18
|
-
formProps
|
|
20
|
+
formProps,
|
|
21
|
+
store
|
|
19
22
|
} = props
|
|
23
|
+
// The right column shows whatever the list selected: a widget definition (run
|
|
24
|
+
// form) or a running instance (its detail + log). A definition has `info`, an
|
|
25
|
+
// instance has `widgetId` — and an instance that has been stopped stays the
|
|
26
|
+
// selected item on purpose, so its log is still readable after it dies.
|
|
27
|
+
const isInstance = !!(settingItem && settingItem.widgetId)
|
|
20
28
|
return (
|
|
21
29
|
<div
|
|
22
30
|
className='setting-tabs-profile'
|
|
@@ -25,11 +33,23 @@ export default function TabWidgets (props) {
|
|
|
25
33
|
<WidgetList
|
|
26
34
|
{...listProps}
|
|
27
35
|
/>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
{
|
|
37
|
+
isInstance
|
|
38
|
+
? (
|
|
39
|
+
<WidgetInstanceDetail
|
|
40
|
+
key={settingItem.id}
|
|
41
|
+
instance={settingItem}
|
|
42
|
+
store={store}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
: (
|
|
46
|
+
<WidgetControl
|
|
47
|
+
{...formProps}
|
|
48
|
+
key={settingItem.id}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
32
52
|
</SettingCol>
|
|
33
53
|
</div>
|
|
34
54
|
)
|
|
35
|
-
}
|
|
55
|
+
})
|
|
@@ -902,12 +902,6 @@ export default class FileSection extends React.Component {
|
|
|
902
902
|
this.transfer()
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
-
zipAndTransfer = async () => {
|
|
906
|
-
this.transfer(transfer => {
|
|
907
|
-
transfer.zip = true
|
|
908
|
-
})
|
|
909
|
-
}
|
|
910
|
-
|
|
911
905
|
newFile = () => {
|
|
912
906
|
return this.newItem(false)
|
|
913
907
|
}
|
|
@@ -1098,13 +1092,6 @@ export default class FileSection extends React.Component {
|
|
|
1098
1092
|
icon: iconType,
|
|
1099
1093
|
text: transferText
|
|
1100
1094
|
})
|
|
1101
|
-
// if (isDirectory && !this.props.isFtp) {
|
|
1102
|
-
// res.push({
|
|
1103
|
-
// func: 'zipAndTransfer',
|
|
1104
|
-
// icon: 'FileZipOutlined',
|
|
1105
|
-
// text: e('compressAndTransfer')
|
|
1106
|
-
// })
|
|
1107
|
-
// }
|
|
1108
1095
|
}
|
|
1109
1096
|
if (!isDirectory && isRealFile && isLocal) {
|
|
1110
1097
|
res.push({
|
|
@@ -4,7 +4,9 @@ import './right-side-panel.styl'
|
|
|
4
4
|
import {
|
|
5
5
|
CloseCircleOutlined,
|
|
6
6
|
PushpinOutlined,
|
|
7
|
-
BarChartOutlined
|
|
7
|
+
BarChartOutlined,
|
|
8
|
+
HistoryOutlined,
|
|
9
|
+
ThunderboltOutlined
|
|
8
10
|
} from '@ant-design/icons'
|
|
9
11
|
import {
|
|
10
12
|
Typography,
|
|
@@ -57,9 +59,14 @@ export default memo(function RightSidePanel (
|
|
|
57
59
|
// from a desktop session must not turn the panel into a dock here — that
|
|
58
60
|
// would make it full height over the footer for no reachable reason.
|
|
59
61
|
const pinned = rightPanelPinned && !isMobile
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// one glyph per tab, so the panel says what it is showing; the info tab is
|
|
63
|
+
// the fallback, since a stale or unknown tab falls back to it everywhere else
|
|
64
|
+
const tagMap = {
|
|
65
|
+
ai: <Tag className='mg1r'>AI</Tag>,
|
|
66
|
+
cmdHistory: <HistoryOutlined className='mg1r' />,
|
|
67
|
+
quickCommands: <ThunderboltOutlined className='mg1r' />
|
|
68
|
+
}
|
|
69
|
+
const tag = tagMap[rightPanelTab] || <BarChartOutlined className='mg1r' />
|
|
63
70
|
|
|
64
71
|
function onDragEnd (nw) {
|
|
65
72
|
window.store.setRightSidePanelWidth(nw)
|