@electerm/electerm-react 5.5.35 → 5.5.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/common/bookmark-schemas.js +1 -0
- package/client/common/constants.js +1 -0
- package/client/common/dangerous-session-fields.js +39 -0
- package/client/common/parse-quick-connect.js +47 -6
- package/client/common/parse-widget-log.js +57 -0
- package/client/common/safe-join.js +33 -0
- package/client/common/sanitize-filename.js +8 -0
- package/client/components/ai/agent-tool-call-card.jsx +7 -3
- package/client/components/ai/agent.js +176 -33
- package/client/components/ai/ai-auto-compress.js +119 -0
- package/client/components/ai/ai-chat-history-item.jsx +125 -40
- package/client/components/ai/ai-chat.jsx +67 -19
- package/client/components/ai/ai-compress.js +95 -0
- package/client/components/ai/ai-config.jsx +29 -16
- package/client/components/ai/ai-config.styl +20 -0
- package/client/components/ai/ai-context.js +1 -1
- package/client/components/ai/ai-output.jsx +1 -6
- package/client/components/ai/ai-presets.js +0 -15
- 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/config/ssh.js +1 -0
- package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
- package/client/components/file-transfer/remote2remote-handlers.jsx +8 -3
- package/client/components/file-transfer/transfer.jsx +3 -119
- package/client/components/main/main.jsx +1 -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-terminal.jsx +1 -0
- package/client/components/setting-panel/tab-widgets.jsx +27 -7
- package/client/components/sftp/file-item.jsx +0 -13
- package/client/components/terminal/mixins/term-init.js +1 -0
- package/client/components/terminal/mixins/term-touch.js +18 -0
- package/client/components/terminal/shell.js +11 -3
- package/client/components/terminal/shortcut-bar-press.js +192 -0
- package/client/components/terminal/shortcut-bar.jsx +199 -4
- package/client/components/terminal/shortcut-bar.styl +40 -21
- package/client/components/terminal/terminal.jsx +4 -0
- 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 +81 -23
- package/client/store/init-state.js +7 -0
- package/client/store/load-data.js +1 -0
- package/client/store/mcp-handler.js +8 -15
- package/client/store/sync.js +1 -0
- package/client/store/tab.js +2 -15
- package/client/store/widgets.js +23 -5
- package/package.json +1 -1
- package/client/components/file-transfer/zip.js +0 -42
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
Button,
|
|
6
6
|
AutoComplete,
|
|
7
7
|
Alert,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Select,
|
|
9
|
+
Space
|
|
10
10
|
} from 'antd'
|
|
11
11
|
import { useEffect, useMemo, useState } from 'react'
|
|
12
|
-
import {
|
|
12
|
+
import { ReloadOutlined } from '@ant-design/icons'
|
|
13
13
|
import Link from '../common/external-link'
|
|
14
14
|
import AiCache from './ai-cache'
|
|
15
15
|
import {
|
|
@@ -20,6 +20,7 @@ import AiHistory, { addHistoryItem } from './ai-history'
|
|
|
20
20
|
import message from '../common/message'
|
|
21
21
|
import { getAIPresets } from './ai-presets'
|
|
22
22
|
import { appendMandatoryGuardrails } from './ai-guardrails'
|
|
23
|
+
import './ai-config.styl'
|
|
23
24
|
|
|
24
25
|
const STORAGE_KEY_CONFIG = 'ai_config_history'
|
|
25
26
|
const EVENT_NAME_CONFIG = 'ai-config-history-update'
|
|
@@ -211,18 +212,32 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
211
212
|
form.setFieldsValue(values)
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
// A plain antd Select, on purpose: it already scrolls its list inside the
|
|
216
|
+
// viewport (a Dropdown menu grows past the bottom of a phone screen, so the
|
|
217
|
+
// tail of the 18 presets is unreachable there) and `showSearch` filters it.
|
|
218
|
+
// `optionFilterProp` is load-bearing: without it rc-select filters on the
|
|
219
|
+
// option *value* (our id), so "gemini" or "llama" match nothing. The label
|
|
220
|
+
// carries name and model, so filtering on it finds either.
|
|
221
|
+
// The label/select sizing lives in `.ai-preset-row` (ai-config.styl), because
|
|
222
|
+
// Space's `.ant-space-item` wrapper is the flex item, not the child.
|
|
223
|
+
function renderPresetSelect () {
|
|
224
|
+
const options = presets.map(p => ({
|
|
225
|
+
value: p.id,
|
|
226
|
+
label: p.modelAI ? `${p.nameAI} · ${p.modelAI}` : p.nameAI
|
|
219
227
|
}))
|
|
220
228
|
return (
|
|
221
|
-
<
|
|
222
|
-
<
|
|
223
|
-
{e('presets')}
|
|
224
|
-
|
|
225
|
-
|
|
229
|
+
<div className='alignright'>
|
|
230
|
+
<Space className='mg1b width-100 ai-preset-row'>
|
|
231
|
+
<span>{e('presets')}</span>
|
|
232
|
+
<Select
|
|
233
|
+
showSearch
|
|
234
|
+
optionFilterProp='label'
|
|
235
|
+
options={options}
|
|
236
|
+
value={currentPreset?.id}
|
|
237
|
+
onSelect={id => handleSelectPreset(presets.find(p => p.id === id))}
|
|
238
|
+
/>
|
|
239
|
+
</Space>
|
|
240
|
+
</div>
|
|
226
241
|
)
|
|
227
242
|
}
|
|
228
243
|
|
|
@@ -294,9 +309,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
294
309
|
type='warning'
|
|
295
310
|
className='mg2b'
|
|
296
311
|
/>
|
|
297
|
-
|
|
298
|
-
{renderPresetMenu()}
|
|
299
|
-
</div>
|
|
312
|
+
{renderPresetSelect()}
|
|
300
313
|
<p>
|
|
301
314
|
Full Url: {initialValues?.baseURLAI}{initialValues?.apiPathAI}
|
|
302
315
|
</p>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Preset row: the "Presets" label and the provider select share one line.
|
|
2
|
+
// antd's Space wraps every child in a `.ant-space-item`, and that wrapper — not
|
|
3
|
+
// the child — is the flex item, so the flex behaviour has to be set on the
|
|
4
|
+
// wrappers. Put `flex` on the Select itself and it is silently ignored, which
|
|
5
|
+
// makes the row's overflow land on the label: at 480px wide the label is
|
|
6
|
+
// squeezed into a 17px column and wraps onto four lines.
|
|
7
|
+
.ai-preset-row
|
|
8
|
+
max-width 400px
|
|
9
|
+
.ant-space-item:first-child
|
|
10
|
+
// the label keeps its full text width and never wraps or shrinks
|
|
11
|
+
flex none
|
|
12
|
+
white-space nowrap
|
|
13
|
+
|
|
14
|
+
.ant-space-item:last-child
|
|
15
|
+
// the select takes the rest of the line, shrinking to nothing if it must
|
|
16
|
+
flex 1
|
|
17
|
+
min-width 0
|
|
18
|
+
|
|
19
|
+
.ant-select
|
|
20
|
+
width 100%
|
|
@@ -23,7 +23,7 @@ const CJK_RE = /[\u2e80-\u2eff\u3000-\u303f\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9f
|
|
|
23
23
|
|
|
24
24
|
export const DEFAULT_CONTEXT_WINDOW = 128000
|
|
25
25
|
export const CONTEXT_WARN_PERCENT = 70
|
|
26
|
-
export const CONTEXT_DANGER_PERCENT =
|
|
26
|
+
export const CONTEXT_DANGER_PERCENT = 80
|
|
27
27
|
|
|
28
28
|
// Rough token count of a string. Never exact, never wildly wrong. Called on
|
|
29
29
|
// the whole conversation every time it changes, so it stays a single pass
|
|
@@ -98,12 +98,7 @@ export default function AIOutput ({ item }) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function handleToggleFlag () {
|
|
101
|
-
|
|
102
|
-
if (index === -1) {
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
window.store.aiChatHistory[index].flagged = !window.store.aiChatHistory[index].flagged
|
|
106
|
-
window.store.aiChatHistory = [...window.store.aiChatHistory]
|
|
101
|
+
window.store.updateAiHistoryEntry(item.id, { flagged: !item.flagged })
|
|
107
102
|
}
|
|
108
103
|
|
|
109
104
|
function renderFlag () {
|
|
@@ -67,21 +67,6 @@ export const defaultAIPresets = [
|
|
|
67
67
|
authHeaderNameAI: 'Authorization: Bearer',
|
|
68
68
|
siteUrl: 'https://platform.openai.com/api-keys?utm_source=electerm_app&utm_medium=link&utm_campaign=electerm'
|
|
69
69
|
},
|
|
70
|
-
{
|
|
71
|
-
id: 'openai-responses',
|
|
72
|
-
nameAI: 'OpenAI (Responses API)',
|
|
73
|
-
baseURLAI: 'https://api.openai.com/v1',
|
|
74
|
-
apiPathAI: '/responses',
|
|
75
|
-
modelAI: 'gpt-4o',
|
|
76
|
-
authHeaderNameAI: 'Authorization: Bearer',
|
|
77
|
-
modelAIs: [
|
|
78
|
-
{ value: 'gpt-4o' },
|
|
79
|
-
{ value: 'gpt-4o-mini' },
|
|
80
|
-
{ value: 'gpt-4.1' },
|
|
81
|
-
{ value: 'o4-mini' }
|
|
82
|
-
],
|
|
83
|
-
siteUrl: 'https://platform.openai.com/api-keys?utm_source=electerm_app&utm_medium=link&utm_campaign=electerm'
|
|
84
|
-
},
|
|
85
70
|
{
|
|
86
71
|
id: 'anthropic',
|
|
87
72
|
nameAI: 'Anthropic Claude',
|
|
@@ -172,6 +172,7 @@ export default class BatchOpRunner extends Component {
|
|
|
172
172
|
enableSsh: p.enableSsh !== false,
|
|
173
173
|
useSshAgent: p.useSshAgent !== false,
|
|
174
174
|
sshAgent: p.sshAgent || '',
|
|
175
|
+
agentForward: p.agentForward || false,
|
|
175
176
|
term: p.term || 'xterm-256color',
|
|
176
177
|
encode: p.encode || 'utf8',
|
|
177
178
|
envLang: p.envLang || 'en_US.UTF-8',
|
|
@@ -20,6 +20,7 @@ const bookmarkSchema = {
|
|
|
20
20
|
connectionHoppings: 'array - connection hopping definitions',
|
|
21
21
|
useSshAgent: 'boolean - use SSH agent, default is true',
|
|
22
22
|
sshAgent: 'string - ssh agent path',
|
|
23
|
+
agentForward: 'boolean - enable SSH agent forwarding (like ssh -A, needed to hop through a jump/bastion host), requires SSH agent, default is false',
|
|
23
24
|
serverHostKey: 'array - server host key algorithms',
|
|
24
25
|
cipher: 'array - cipher list',
|
|
25
26
|
compress: 'array - compression algorithms (zlib@openssh.com, zlib, none)',
|
|
@@ -9,26 +9,36 @@ const e = window.translate
|
|
|
9
9
|
|
|
10
10
|
export default function SshAgent () {
|
|
11
11
|
return (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
<>
|
|
13
|
+
<FormItem
|
|
14
|
+
{...formItemLayout}
|
|
15
|
+
label={e('useSshAgent')}
|
|
16
|
+
>
|
|
17
|
+
<Space align='center'>
|
|
18
|
+
<FormItem
|
|
19
|
+
name='useSshAgent'
|
|
20
|
+
valuePropName='checked'
|
|
21
|
+
noStyle
|
|
22
|
+
>
|
|
23
|
+
<SwitchLabel />
|
|
24
|
+
</FormItem>
|
|
25
|
+
<FormItem
|
|
26
|
+
name='sshAgent'
|
|
27
|
+
noStyle
|
|
28
|
+
>
|
|
29
|
+
<Input placeholder={e('SSH Agent Path')} />
|
|
30
|
+
</FormItem>
|
|
31
|
+
<HelpIcon link='https://github.com/electerm/electerm/wiki/ssh-agent' />
|
|
32
|
+
</Space>
|
|
33
|
+
</FormItem>
|
|
34
|
+
<FormItem
|
|
35
|
+
{...formItemLayout}
|
|
36
|
+
label={e('agentForward')}
|
|
37
|
+
name='agentForward'
|
|
38
|
+
valuePropName='checked'
|
|
39
|
+
>
|
|
40
|
+
<SwitchLabel />
|
|
41
|
+
</FormItem>
|
|
42
|
+
</>
|
|
33
43
|
)
|
|
34
44
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Component } from 'react'
|
|
2
|
-
import
|
|
2
|
+
import safeJoin from '../../common/safe-join'
|
|
3
3
|
import { typeMap } from '../../common/constants'
|
|
4
4
|
import { refsStatic } from '../common/ref'
|
|
5
5
|
import Remote2RemoteHandler from './remote2remote-handler'
|
|
@@ -35,7 +35,10 @@ export default class Remote2RemoteHandlers extends Component {
|
|
|
35
35
|
createHandler = ({ fromFile, targetPathBase, targetTab }) => {
|
|
36
36
|
const handler = new Remote2RemoteHandler({
|
|
37
37
|
fromFile,
|
|
38
|
-
|
|
38
|
+
// `fromFile.name` comes straight off the source server's READDIR, so it
|
|
39
|
+
// is attacker-controlled. It is written to the target host as the victim
|
|
40
|
+
// below, so it must not be able to leave `targetPathBase`.
|
|
41
|
+
toPath: safeJoin(targetPathBase, fromFile.name),
|
|
39
42
|
sourceHost: fromFile.host,
|
|
40
43
|
sourceTabId: fromFile.tabId,
|
|
41
44
|
title: fromFile.title,
|
|
@@ -58,7 +61,9 @@ export default class Remote2RemoteHandlers extends Component {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
onRemote2RemoteDrop = ({ fromFiles, toFile, targetTab }) => {
|
|
61
|
-
|
|
64
|
+
// `toFile.name` is an entry from the target panel's listing. The victim
|
|
65
|
+
// chose the panel, but the name is still server-supplied.
|
|
66
|
+
const targetPathBase = safeJoin(toFile.path, toFile.name)
|
|
62
67
|
const targetHost = targetTab?.host
|
|
63
68
|
let handled = false
|
|
64
69
|
for (const fromFile of fromFiles) {
|
|
@@ -12,13 +12,6 @@ import {
|
|
|
12
12
|
import resolve from '../../common/resolve'
|
|
13
13
|
import sanitizeFilename from '../../common/sanitize-filename'
|
|
14
14
|
import { refsTransfers, refsStatic, refs } from '../common/ref'
|
|
15
|
-
import {
|
|
16
|
-
zipCmd,
|
|
17
|
-
unzipCmd,
|
|
18
|
-
rmCmd,
|
|
19
|
-
mvCmd,
|
|
20
|
-
mkdirCmd
|
|
21
|
-
} from './zip'
|
|
22
15
|
import './transfer.styl'
|
|
23
16
|
|
|
24
17
|
const { assign } = Object
|
|
@@ -279,9 +272,7 @@ export default class TransportAction extends Component {
|
|
|
279
272
|
typeFrom,
|
|
280
273
|
toFile = {}
|
|
281
274
|
} = transfer
|
|
282
|
-
const toPath = transfer.
|
|
283
|
-
? transfer.toPath
|
|
284
|
-
: this.newPath || transfer.toPath
|
|
275
|
+
const toPath = this.newPath || transfer.toPath
|
|
285
276
|
const fromFile = transfer.fromFile || this.fromFile
|
|
286
277
|
const fromMode = fromFile.mode
|
|
287
278
|
const transferType = typeFrom === typeMap.local ? transferTypeMap.upload : transferTypeMap.download
|
|
@@ -408,12 +399,11 @@ export default class TransportAction extends Component {
|
|
|
408
399
|
toPath
|
|
409
400
|
} = this.props.transfer
|
|
410
401
|
this.oldPath = toPath
|
|
411
|
-
const { newPath
|
|
402
|
+
const { newPath } = this.handleRename(toPath, typeTo === typeMap.remote)
|
|
412
403
|
this.update({
|
|
413
404
|
toPath: newPath
|
|
414
405
|
})
|
|
415
406
|
this.newPath = newPath
|
|
416
|
-
this.newName = newName
|
|
417
407
|
}
|
|
418
408
|
|
|
419
409
|
const { typeFrom, typeTo } = this.props.transfer
|
|
@@ -423,120 +413,14 @@ export default class TransportAction extends Component {
|
|
|
423
413
|
this.startTransfer()
|
|
424
414
|
}
|
|
425
415
|
|
|
426
|
-
zipTransferFolder = async () => {
|
|
427
|
-
const {
|
|
428
|
-
transfer
|
|
429
|
-
} = this.props
|
|
430
|
-
const {
|
|
431
|
-
fromPath,
|
|
432
|
-
typeFrom
|
|
433
|
-
} = transfer
|
|
434
|
-
const toPath = this.oldPath || transfer.toPath
|
|
435
|
-
let p
|
|
436
|
-
let isFromRemote
|
|
437
|
-
if (typeFrom === typeMap.local) {
|
|
438
|
-
isFromRemote = false
|
|
439
|
-
p = await window.fs.zipFolder(fromPath)
|
|
440
|
-
} else {
|
|
441
|
-
isFromRemote = true
|
|
442
|
-
const terminalId = refs.get('sftp-' + this.tabId)?.terminalId
|
|
443
|
-
p = await zipCmd(terminalId, fromPath)
|
|
444
|
-
}
|
|
445
|
-
this.zipSrc = p
|
|
446
|
-
const { name } = getFolderFromFilePath(p, isFromRemote)
|
|
447
|
-
const { path } = getFolderFromFilePath(toPath, !isFromRemote)
|
|
448
|
-
const nTo = resolve(path, name)
|
|
449
|
-
this.zipPath = nTo
|
|
450
|
-
const newTrans1 = {
|
|
451
|
-
...copy(transfer),
|
|
452
|
-
toPath: nTo,
|
|
453
|
-
fromPath: p
|
|
454
|
-
}
|
|
455
|
-
this.transferFile(newTrans1, this.unzipFile)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
unzipFile = async () => {
|
|
459
|
-
const { transfer } = this.props
|
|
460
|
-
const {
|
|
461
|
-
typeTo
|
|
462
|
-
} = transfer
|
|
463
|
-
const toPath = this.zipPath
|
|
464
|
-
const fromPath = this.zipSrc
|
|
465
|
-
const isToRemote = typeTo === typeMap.remote
|
|
466
|
-
const {
|
|
467
|
-
path,
|
|
468
|
-
name,
|
|
469
|
-
targetPath
|
|
470
|
-
} = this.buildUnzipPath(transfer)
|
|
471
|
-
const {
|
|
472
|
-
newName,
|
|
473
|
-
terminalId
|
|
474
|
-
} = this
|
|
475
|
-
if (isToRemote) {
|
|
476
|
-
if (newName) {
|
|
477
|
-
await mkdirCmd(terminalId, path)
|
|
478
|
-
}
|
|
479
|
-
await unzipCmd(terminalId, toPath, path)
|
|
480
|
-
if (newName) {
|
|
481
|
-
const mvFrom = resolve(path, name)
|
|
482
|
-
const mvTo = resolve(targetPath, newName)
|
|
483
|
-
await mvCmd(terminalId, mvFrom, mvTo)
|
|
484
|
-
}
|
|
485
|
-
} else {
|
|
486
|
-
if (newName) {
|
|
487
|
-
await window.fs.mkdir(path)
|
|
488
|
-
}
|
|
489
|
-
await window.fs.unzipFile(toPath, path)
|
|
490
|
-
if (newName) {
|
|
491
|
-
const mvFrom = resolve(path, name)
|
|
492
|
-
const mvTo = resolve(targetPath, newName)
|
|
493
|
-
await window.fs.mv(mvFrom, mvTo)
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
await rmCmd(terminalId, !isToRemote ? fromPath : toPath)
|
|
497
|
-
await window.fs.rmrf(!isToRemote ? toPath : fromPath)
|
|
498
|
-
if (newName) {
|
|
499
|
-
if (isToRemote) {
|
|
500
|
-
await rmCmd(terminalId, path)
|
|
501
|
-
} else {
|
|
502
|
-
await window.fs.rmrf(path)
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
this.onEnd()
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
buildUnzipPath = (transfer) => {
|
|
509
|
-
const {
|
|
510
|
-
typeTo
|
|
511
|
-
} = transfer
|
|
512
|
-
const isToRemote = typeTo === typeMap.remote
|
|
513
|
-
const toPath = this.oldPath || transfer.toPath
|
|
514
|
-
const {
|
|
515
|
-
newName
|
|
516
|
-
} = this
|
|
517
|
-
const { path } = getFolderFromFilePath(toPath, isToRemote)
|
|
518
|
-
const oldName = getFolderFromFilePath(toPath, isToRemote).name
|
|
519
|
-
const np = newName
|
|
520
|
-
? resolve(path, 'temp-' + newName)
|
|
521
|
-
: path
|
|
522
|
-
return {
|
|
523
|
-
targetPath: path,
|
|
524
|
-
path: np,
|
|
525
|
-
name: oldName
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
416
|
startTransfer = async () => {
|
|
530
|
-
const { fromFile = this.fromFile
|
|
417
|
+
const { fromFile = this.fromFile } = this.props.transfer
|
|
531
418
|
if (!fromFile) {
|
|
532
419
|
return
|
|
533
420
|
}
|
|
534
421
|
if (!fromFile.isDirectory) {
|
|
535
422
|
return this.transferFile()
|
|
536
423
|
}
|
|
537
|
-
if (zip) {
|
|
538
|
-
return this.zipTransferFolder()
|
|
539
|
-
}
|
|
540
424
|
if (!this.isFtp) {
|
|
541
425
|
return this.transferFile()
|
|
542
426
|
} else {
|
|
@@ -289,6 +289,7 @@ export default auto(function Index (props) {
|
|
|
289
289
|
rightPanelTab,
|
|
290
290
|
agentRunning: store.agentRunning,
|
|
291
291
|
aiContextInfo: store.aiContextInfo,
|
|
292
|
+
aiAutoCompress: store.aiAutoCompress,
|
|
292
293
|
currentChatSessionId: store.currentChatSessionId,
|
|
293
294
|
showChatSessions: store.showChatSessions
|
|
294
295
|
}
|
|
@@ -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}>
|
|
@@ -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({
|