@electerm/electerm-react 5.2.0 → 5.3.5
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/constants.js +12 -1
- package/client/common/term-of-use.jsx +69 -0
- package/client/components/ai/ai-config-props.js +8 -0
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/config/common-fields.js +11 -0
- package/client/components/footer/footer-entry.jsx +1 -1
- package/client/components/layout/layout.jsx +0 -2
- package/client/components/rdp/rdp-session.jsx +1 -1
- package/client/components/session/session-control.jsx +1 -23
- package/client/components/session/session.jsx +1 -31
- package/client/components/session/sessions.jsx +0 -1
- package/client/components/setting-sync/setting-sync.jsx +15 -7
- package/client/components/shortcuts/shortcut-handler.js +0 -1
- package/client/components/sidebar/info-modal.jsx +7 -0
- package/client/components/terminal/attach-addon-custom.js +12 -10
- package/client/components/terminal/shell.js +22 -11
- package/client/components/terminal/shortcut-bar-entry.jsx +52 -1
- package/client/components/terminal/terminal-interactive.jsx +35 -0
- package/client/components/terminal/terminal.jsx +11 -6
- package/client/components/terminal-info/activity.jsx +5 -1
- package/client/components/terminal-info/base.jsx +37 -22
- package/client/components/terminal-info/disk.jsx +5 -1
- package/client/components/terminal-info/local-info-resolver.js +89 -0
- package/client/components/terminal-info/network.jsx +5 -1
- package/client/components/terminal-info/run-cmd.jsx +42 -3
- package/client/components/terminal-info/sys-info.jsx +56 -0
- package/client/components/terminal-info/terminal-info.jsx +4 -1
- package/client/components/terminal-info/terminal-info.styl +22 -7
- package/client/store/common.js +19 -7
- package/client/store/init-state.js +0 -2
- package/client/store/mcp-handler.js +3 -1
- package/client/store/setting.js +7 -4
- package/client/store/tab.js +16 -11
- package/package.json +1 -1
|
@@ -212,6 +212,16 @@ export const syncTypes = buildConst([
|
|
|
212
212
|
'cloud',
|
|
213
213
|
'webdav'
|
|
214
214
|
])
|
|
215
|
+
export const allowedSyncTypes = () => {
|
|
216
|
+
const custom = window.et.syncTypes
|
|
217
|
+
if (!Array.isArray(custom)) {
|
|
218
|
+
return Object.keys(syncTypes)
|
|
219
|
+
}
|
|
220
|
+
const list = custom.filter(type => syncTypes[type])
|
|
221
|
+
return list.length
|
|
222
|
+
? list
|
|
223
|
+
: Object.keys(syncTypes)
|
|
224
|
+
}
|
|
215
225
|
export const syncTokenCreateUrls = {
|
|
216
226
|
gitee: 'https://gitee.com/github-zxdong262/electerm/wikis/Create%20personal%20access%20token?sort_id=3028409',
|
|
217
227
|
github: 'https://github.com/electerm/electerm/wiki/Create-personal-access-token',
|
|
@@ -253,7 +263,6 @@ export const resolutionsLsKey = 'custom-resolution-key'
|
|
|
253
263
|
export const checkedKeysLsKey = 'checked-keys'
|
|
254
264
|
export const quickCommandLabelsLsKey = 'quick-command-label'
|
|
255
265
|
export const localAddrBookmarkLsKey = 'local-addr-bookmark-keys'
|
|
256
|
-
export const dismissDelKeyTipLsKey = 'dismiss-del-key-tip'
|
|
257
266
|
export const treeSortLsKey = 'tree-sort'
|
|
258
267
|
export const sshTunnelHelpLink = 'https://github.com/electerm/electerm/wiki/How-to-use-ssh-tunnel'
|
|
259
268
|
export const proxyHelpLink = 'https://github.com/electerm/electerm/wiki/proxy-format'
|
|
@@ -262,6 +271,8 @@ export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/w
|
|
|
262
271
|
export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
|
|
263
272
|
export const aiChatModeLsKey = 'ai-chat-mode'
|
|
264
273
|
export const lastAiChatSessionIdKey = 'last-ai-chat-session-id'
|
|
274
|
+
export const aiTermOfUseConfirmedLsKey = 'ai-term-of-use-confirmed'
|
|
275
|
+
export const syncTermOfUseConfirmedLsKey = 'sync-term-of-use-confirmed'
|
|
265
276
|
export const modals = {
|
|
266
277
|
hide: 0,
|
|
267
278
|
setting: 1
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Term of use confirmation gate for the AI config / data sync pages.
|
|
3
|
+
*
|
|
4
|
+
* Define `window.et.AITermOfUse` / `window.et.SyncTermOfUse` (injected the
|
|
5
|
+
* same way as `defaultAIPreset` into the built index.html `_global` data)
|
|
6
|
+
* to require users to confirm the term before entering those pages.
|
|
7
|
+
* Confirmation is saved to localStorage, so it is only asked once.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Modal from '../components/common/modal'
|
|
11
|
+
import {
|
|
12
|
+
aiTermOfUseConfirmedLsKey,
|
|
13
|
+
syncTermOfUseConfirmedLsKey
|
|
14
|
+
} from './constants'
|
|
15
|
+
import * as ls from './safe-local-storage'
|
|
16
|
+
|
|
17
|
+
const e = window.translate
|
|
18
|
+
|
|
19
|
+
const termOfUseTypes = {
|
|
20
|
+
ai: {
|
|
21
|
+
etKey: 'AITermOfUse',
|
|
22
|
+
lsKey: aiTermOfUseConfirmedLsKey
|
|
23
|
+
},
|
|
24
|
+
sync: {
|
|
25
|
+
etKey: 'SyncTermOfUse',
|
|
26
|
+
lsKey: syncTermOfUseConfirmedLsKey
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isConfirmed (type) {
|
|
31
|
+
return ls.getItem(termOfUseTypes[type].lsKey) === 'yes'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run `onConfirmed` after the term of use of `type` is confirmed.
|
|
36
|
+
* When no term is defined, or it was confirmed before, `onConfirmed`
|
|
37
|
+
* runs synchronously; otherwise a confirmation modal is shown and
|
|
38
|
+
* `onConfirmed` only runs when the user confirms it.
|
|
39
|
+
*/
|
|
40
|
+
export function requireTermOfUse (type, onConfirmed) {
|
|
41
|
+
const { etKey, lsKey } = termOfUseTypes[type]
|
|
42
|
+
const content = window.et?.[etKey]
|
|
43
|
+
const text = typeof content === 'string' ? content.trim() : ''
|
|
44
|
+
if (!text || isConfirmed(type)) {
|
|
45
|
+
return onConfirmed()
|
|
46
|
+
}
|
|
47
|
+
Modal.confirm({
|
|
48
|
+
title: 'Terms of use',
|
|
49
|
+
width: 600,
|
|
50
|
+
maskClosable: false,
|
|
51
|
+
content: (
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
maxHeight: '50vh',
|
|
55
|
+
overflow: 'auto',
|
|
56
|
+
whiteSpace: 'pre-wrap'
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{text}
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
okText: e('haveRead'),
|
|
63
|
+
cancelText: e('cancel'),
|
|
64
|
+
onOk: () => {
|
|
65
|
+
ls.setItem(lsKey, 'yes')
|
|
66
|
+
onConfirmed()
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -27,6 +27,14 @@ export const defaultAIPresets = [
|
|
|
27
27
|
modelAI: 'DEEPSEEK_V4_FLASH',
|
|
28
28
|
authHeaderNameAI: 'Authorization: Bearer'
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
id: 'apimart',
|
|
32
|
+
nameAI: 'ApiMart',
|
|
33
|
+
baseURLAI: 'https://api.apimart.ai/v1',
|
|
34
|
+
apiPathAI: '/chat/completions',
|
|
35
|
+
modelAI: 'deepseek-v4-flash',
|
|
36
|
+
authHeaderNameAI: 'Authorization: Bearer'
|
|
37
|
+
},
|
|
30
38
|
{
|
|
31
39
|
id: 'deepseek',
|
|
32
40
|
nameAI: 'DeepSeek',
|
|
@@ -26,6 +26,7 @@ const bookmarkSchema = {
|
|
|
26
26
|
runScripts: 'array - run scripts after connected ({delay,script})',
|
|
27
27
|
quickCommands: 'array - quick commands ({name,command})',
|
|
28
28
|
proxy: 'string - proxy address (socks5://...)',
|
|
29
|
+
proxyCommand: 'string - ssh proxy command, supports %h (host) %p (port) %r (username) placeholders, e.g. `netbird ssh proxy %h %p` or `cloudflared access ssh --hostname %h`',
|
|
29
30
|
x11: 'boolean - enable x11 forwarding, default is false',
|
|
30
31
|
term: 'string - terminal type, default is xterm-256color, required',
|
|
31
32
|
displayRaw: 'boolean - display raw output, default is false',
|
|
@@ -210,6 +210,16 @@ export const commonFields = {
|
|
|
210
210
|
label: () => e('proxy')
|
|
211
211
|
},
|
|
212
212
|
|
|
213
|
+
proxyCommand: {
|
|
214
|
+
type: 'input',
|
|
215
|
+
name: 'proxyCommand',
|
|
216
|
+
label: () => `${e('proxy')} command`,
|
|
217
|
+
props: {
|
|
218
|
+
placeholder: 'netbird ssh proxy %h %p',
|
|
219
|
+
maxLength: 1024
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
213
223
|
x11: {
|
|
214
224
|
type: 'x11',
|
|
215
225
|
name: '__x11__',
|
|
@@ -281,6 +291,7 @@ export const sshSettings = [
|
|
|
281
291
|
},
|
|
282
292
|
commonFields.enableTerminalImage,
|
|
283
293
|
...terminalSettings.slice(0, -1), // All except terminalBackground
|
|
294
|
+
commonFields.proxyCommand,
|
|
284
295
|
commonFields.x11,
|
|
285
296
|
commonFields.terminalBackground
|
|
286
297
|
]
|
|
@@ -151,7 +151,7 @@ export default auto(function FooterEntry (props) {
|
|
|
151
151
|
<div className='terminal-footer-unit terminal-footer-info'>
|
|
152
152
|
<InfoCircleOutlined
|
|
153
153
|
onClick={handleInfoPanel}
|
|
154
|
-
className='pointer
|
|
154
|
+
className='pointer font14 terminal-info-icon'
|
|
155
155
|
/>
|
|
156
156
|
</div>
|
|
157
157
|
)
|
|
@@ -119,7 +119,6 @@ export default auto(function Layout (props) {
|
|
|
119
119
|
'isMaximized',
|
|
120
120
|
'config',
|
|
121
121
|
'resolutions',
|
|
122
|
-
'hideDelKeyTip',
|
|
123
122
|
'fileOperation',
|
|
124
123
|
'pinnedQuickCommandBar',
|
|
125
124
|
'tabsHeight',
|
|
@@ -181,7 +180,6 @@ export default auto(function Layout (props) {
|
|
|
181
180
|
'activeTabId3',
|
|
182
181
|
'batch',
|
|
183
182
|
'resolutions',
|
|
184
|
-
'hideDelKeyTip',
|
|
185
183
|
'fileOperation',
|
|
186
184
|
'file',
|
|
187
185
|
'pinnedQuickCommandBar',
|
|
@@ -137,7 +137,7 @@ export default class RdpSession extends PureComponent {
|
|
|
137
137
|
}
|
|
138
138
|
// keep resolved credentials for the connect-time prompt
|
|
139
139
|
this.tab = tab
|
|
140
|
-
if (!tab.username
|
|
140
|
+
if (!tab.username) {
|
|
141
141
|
this.setState({
|
|
142
142
|
loading: false,
|
|
143
143
|
showCredPrompt: true
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
SearchOutlined,
|
|
7
7
|
FullscreenOutlined,
|
|
8
8
|
PaperClipOutlined,
|
|
9
|
-
CloseOutlined,
|
|
10
9
|
ApartmentOutlined,
|
|
11
10
|
MoreOutlined,
|
|
12
11
|
ColumnWidthOutlined,
|
|
@@ -37,8 +36,6 @@ export default function SessionControl (props) {
|
|
|
37
36
|
keepaliveEnabled,
|
|
38
37
|
broadcastInput,
|
|
39
38
|
wrapDisabled,
|
|
40
|
-
delKeyPressed,
|
|
41
|
-
hideDelKeyTip,
|
|
42
39
|
onChangePane,
|
|
43
40
|
toggleCheckSftpPathFollowSsh,
|
|
44
41
|
onSshSftpSplitView,
|
|
@@ -47,7 +44,6 @@ export default function SessionControl (props) {
|
|
|
47
44
|
toggleWrap,
|
|
48
45
|
onFullscreen,
|
|
49
46
|
onOpenSearch,
|
|
50
|
-
onDismissDelKeyTip,
|
|
51
47
|
onExitGracefully
|
|
52
48
|
} = props
|
|
53
49
|
|
|
@@ -108,26 +104,11 @@ export default function SessionControl (props) {
|
|
|
108
104
|
)
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
function renderDelTip (isS) {
|
|
112
|
-
if (!isS || hideDelKeyTip || !delKeyPressed) {
|
|
113
|
-
return null
|
|
114
|
-
}
|
|
115
|
-
return (
|
|
116
|
-
<div className='type-tab'>
|
|
117
|
-
<span className='mg1r'>Try <b>Shift + Backspace</b>?</span>
|
|
118
|
-
<CloseOutlined
|
|
119
|
-
onClick={onDismissDelKeyTip}
|
|
120
|
-
className='pointer'
|
|
121
|
-
/>
|
|
122
|
-
</div>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
107
|
function renderSftpPathFollowControl () {
|
|
127
108
|
if (isDisabled) {
|
|
128
109
|
return null
|
|
129
110
|
}
|
|
130
|
-
const {
|
|
111
|
+
const { enableSsh } = tab
|
|
131
112
|
const checkTxt = e('sftpPathFollowSsh')
|
|
132
113
|
const checkProps = {
|
|
133
114
|
onClick: toggleCheckSftpPathFollowSsh,
|
|
@@ -138,8 +119,6 @@ export default function SessionControl (props) {
|
|
|
138
119
|
}
|
|
139
120
|
)
|
|
140
121
|
}
|
|
141
|
-
const isS = pane === paneMap.terminal ||
|
|
142
|
-
sshSftpSplitView
|
|
143
122
|
return (
|
|
144
123
|
<>
|
|
145
124
|
{
|
|
@@ -153,7 +132,6 @@ export default function SessionControl (props) {
|
|
|
153
132
|
)
|
|
154
133
|
: null
|
|
155
134
|
}
|
|
156
|
-
{renderDelTip(isS)}
|
|
157
135
|
</>
|
|
158
136
|
)
|
|
159
137
|
}
|
|
@@ -46,7 +46,6 @@ export default class SessionWrapper extends Component {
|
|
|
46
46
|
key: Math.random(),
|
|
47
47
|
splitSize: [50, 50],
|
|
48
48
|
sessionOptions: null,
|
|
49
|
-
delKeyPressed: false,
|
|
50
49
|
broadcastInput: false,
|
|
51
50
|
keepaliveEnabled: false,
|
|
52
51
|
wrapDisabled: false
|
|
@@ -59,10 +58,6 @@ export default class SessionWrapper extends Component {
|
|
|
59
58
|
minWithForSplit = 640
|
|
60
59
|
minHeightForSplit = 400
|
|
61
60
|
|
|
62
|
-
componentWillUnmount () {
|
|
63
|
-
clearTimeout(this.backspaceKeyPressedTimer)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
61
|
getDom = () => {
|
|
67
62
|
return this.domRef.current
|
|
68
63
|
}
|
|
@@ -178,27 +173,6 @@ export default class SessionWrapper extends Component {
|
|
|
178
173
|
e && e.dataTransfer && e.dataTransfer.clearData()
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
onDelKeyPressed = () => {
|
|
182
|
-
this.setState({
|
|
183
|
-
delKeyPressed: true
|
|
184
|
-
})
|
|
185
|
-
this.backspaceKeyPressedTimer = setTimeout(() => {
|
|
186
|
-
this.setState({
|
|
187
|
-
delKeyPressed: false
|
|
188
|
-
})
|
|
189
|
-
}, 5000)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
handleChangeDelMode = (backspaceMode) => {
|
|
193
|
-
this.setState({
|
|
194
|
-
backspaceMode
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
handleDismissDelKeyTip = () => {
|
|
199
|
-
window.store.dismissDelKeyTip()
|
|
200
|
-
}
|
|
201
|
-
|
|
202
176
|
setCwd = (cwd) => {
|
|
203
177
|
this.setState({
|
|
204
178
|
cwd
|
|
@@ -351,8 +325,7 @@ export default class SessionWrapper extends Component {
|
|
|
351
325
|
this,
|
|
352
326
|
[
|
|
353
327
|
'onChangePane',
|
|
354
|
-
'setCwd'
|
|
355
|
-
'onDelKeyPressed'
|
|
328
|
+
'setCwd'
|
|
356
329
|
]),
|
|
357
330
|
...this.computePosition(),
|
|
358
331
|
width,
|
|
@@ -633,8 +606,6 @@ export default class SessionWrapper extends Component {
|
|
|
633
606
|
keepaliveEnabled={this.state.keepaliveEnabled}
|
|
634
607
|
broadcastInput={this.state.broadcastInput}
|
|
635
608
|
wrapDisabled={this.state.wrapDisabled}
|
|
636
|
-
delKeyPressed={this.state.delKeyPressed}
|
|
637
|
-
hideDelKeyTip={this.props.hideDelKeyTip}
|
|
638
609
|
onChangePane={(pane) => this.onChangePane(pane)}
|
|
639
610
|
toggleCheckSftpPathFollowSsh={this.toggleCheckSftpPathFollowSsh}
|
|
640
611
|
onSshSftpSplitView={this.handleSshSftpSplitView}
|
|
@@ -643,7 +614,6 @@ export default class SessionWrapper extends Component {
|
|
|
643
614
|
toggleWrap={this.toggleWrap}
|
|
644
615
|
onFullscreen={this.handleFullscreen}
|
|
645
616
|
onOpenSearch={this.handleOpenSearch}
|
|
646
|
-
onDismissDelKeyTip={this.handleDismissDelKeyTip}
|
|
647
617
|
onExitGracefully={this.handleExitGracefully}
|
|
648
618
|
/>
|
|
649
619
|
{this.renderViews()}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Tabs, Spin } from 'antd'
|
|
6
|
+
import { useEffect } from 'react'
|
|
6
7
|
import SyncForm from './setting-sync-form'
|
|
7
|
-
import {
|
|
8
|
+
import { allowedSyncTypes, syncDataMaps } from '../../common/constants'
|
|
8
9
|
import DataTransport from './data-import'
|
|
9
10
|
import DataSelect from './data-select'
|
|
10
11
|
import { pick } from 'lodash-es'
|
|
@@ -24,6 +25,15 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
24
25
|
const {
|
|
25
26
|
store
|
|
26
27
|
} = window
|
|
28
|
+
const types = allowedSyncTypes()
|
|
29
|
+
const type = types.includes(props.syncType)
|
|
30
|
+
? props.syncType
|
|
31
|
+
: types[0]
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (store.syncType !== type) {
|
|
34
|
+
store.syncType = type
|
|
35
|
+
}
|
|
36
|
+
}, [type])
|
|
27
37
|
function renderForm () {
|
|
28
38
|
const syncProps = {
|
|
29
39
|
...syncSetting,
|
|
@@ -31,12 +41,11 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
31
41
|
'isSyncingSetting',
|
|
32
42
|
'isSyncDownload',
|
|
33
43
|
'isSyncUpload',
|
|
34
|
-
'syncType',
|
|
35
44
|
'serverStatus'
|
|
36
45
|
]),
|
|
37
|
-
|
|
46
|
+
syncType: type,
|
|
47
|
+
serverStatus: deepCopy(store.syncServerStatus[type])
|
|
38
48
|
}
|
|
39
|
-
const type = props.syncType
|
|
40
49
|
const formData = {
|
|
41
50
|
gistId: syncSetting[type + 'GistId'],
|
|
42
51
|
token: syncSetting[type + 'AccessToken'],
|
|
@@ -59,8 +68,7 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
59
68
|
/>
|
|
60
69
|
)
|
|
61
70
|
}
|
|
62
|
-
|
|
63
|
-
const syncItems = Object.keys(syncTypes).map(type => {
|
|
71
|
+
const syncItems = types.map(type => {
|
|
64
72
|
return {
|
|
65
73
|
key: type,
|
|
66
74
|
label: type,
|
|
@@ -84,7 +92,7 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
84
92
|
<DataTransport {...dataImportProps} />
|
|
85
93
|
<Spin spinning={store.isSyncingSetting}>
|
|
86
94
|
<Tabs
|
|
87
|
-
activeKey={
|
|
95
|
+
activeKey={type}
|
|
88
96
|
onChange={handleChange}
|
|
89
97
|
items={syncItems}
|
|
90
98
|
/>
|
|
@@ -187,7 +187,6 @@ export function shortcutExtend (Cls) {
|
|
|
187
187
|
!altKey &&
|
|
188
188
|
!ctrlKey
|
|
189
189
|
) {
|
|
190
|
-
this.props.onDelKeyPressed()
|
|
191
190
|
const delKey = this.props.config.backspaceMode === '^?' ? 8 : 127
|
|
192
191
|
const altDelDelKey = delKey === 8 ? 127 : 8
|
|
193
192
|
const char = String.fromCharCode(shiftKey ? delKey : altDelDelKey)
|
|
@@ -127,6 +127,7 @@ export default auto(function InfoModal (props) {
|
|
|
127
127
|
devDependencies,
|
|
128
128
|
dependencies,
|
|
129
129
|
languageRepo,
|
|
130
|
+
privacyNoticeLink,
|
|
130
131
|
author: {
|
|
131
132
|
name: authorName,
|
|
132
133
|
email,
|
|
@@ -203,6 +204,12 @@ export default auto(function InfoModal (props) {
|
|
|
203
204
|
{bugReportLink}
|
|
204
205
|
</Link>
|
|
205
206
|
</p>
|
|
207
|
+
<p className='mg1b'>
|
|
208
|
+
<InfoCircleOutlined /> <b className='mg1r'>{e('privacyNotice')} ➾</b>
|
|
209
|
+
<Link to={privacyNoticeLink} className='mg1l'>
|
|
210
|
+
{privacyNoticeLink}
|
|
211
|
+
</Link>
|
|
212
|
+
</p>
|
|
206
213
|
<p className='mg1b'>
|
|
207
214
|
<HighlightOutlined /> <b className='mg1r'>{e('changeLog')} ➾</b>
|
|
208
215
|
<Link to={releaseLink} className='mg1l'>
|
|
@@ -98,12 +98,6 @@ export default class AttachAddonCustom {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
onShellIntegrationDetected = () => {
|
|
102
|
-
if (this.outputSuppressed) {
|
|
103
|
-
this.stopOutputSuppression(true)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
101
|
activate = async (terminal = this.term) => {
|
|
108
102
|
await this._initBase()
|
|
109
103
|
this.addSocketListener(this._socket, 'message', this.onMsg)
|
|
@@ -177,9 +171,10 @@ export default class AttachAddonCustom {
|
|
|
177
171
|
}
|
|
178
172
|
}
|
|
179
173
|
|
|
180
|
-
|
|
174
|
+
// Index of the first OSC 633 sequence in the chunk, or -1.
|
|
175
|
+
indexOfShellIntegration = (str) => {
|
|
181
176
|
const ESC = String.fromCharCode(27)
|
|
182
|
-
return str.
|
|
177
|
+
return str.indexOf(ESC + ']633;')
|
|
183
178
|
}
|
|
184
179
|
|
|
185
180
|
writeToTerminalDirect = (data) => {
|
|
@@ -232,8 +227,15 @@ export default class AttachAddonCustom {
|
|
|
232
227
|
}
|
|
233
228
|
|
|
234
229
|
if (this.outputSuppressed) {
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
const oscIdx = this.indexOfShellIntegration(str)
|
|
231
|
+
if (oscIdx !== -1) {
|
|
232
|
+
// Shell integration is confirmed active. The pty often coalesces the
|
|
233
|
+
// tail of the echoed injection command and the first OSC 633 output
|
|
234
|
+
// into one chunk, so write only from the first OSC sequence on —
|
|
235
|
+
// everything before it is echo and stays hidden. A leading newline
|
|
236
|
+
// keeps the fresh prompt off the injection-time prompt line.
|
|
237
|
+
this.stopOutputSuppression(true)
|
|
238
|
+
this._enqueueWrite('\r\n' + str.slice(oscIdx))
|
|
237
239
|
return
|
|
238
240
|
}
|
|
239
241
|
this.suppressedData.push(data)
|
|
@@ -27,7 +27,7 @@ function getBashInlineIntegration () {
|
|
|
27
27
|
'if [[ $- == *i* ]] && [[ -z "${ELECTERM_SHELL_INTEGRATION:-}" ]]',
|
|
28
28
|
'then export ELECTERM_SHELL_INTEGRATION=1',
|
|
29
29
|
'__e_esc() { local v="$1"; v="${v//\\\\/\\\\\\\\}"; v="${v//;/\\\\x3b}"; printf \'%s\' "$v"; }',
|
|
30
|
-
'__e_pre() { [[ "$BASH_COMMAND" == "$
|
|
30
|
+
'__e_pre() { local p; [[ "$BASH_COMMAND" == __e_* || "$BASH_COMMAND" == PROMPT_COMMAND=* ]] && return; [[ "$BASH_COMMAND" == "$PROMPT_COMMAND" ]] && return; if [[ -n "${PROMPT_COMMAND:-}" ]]; then local IFS=\';\'; for p in $PROMPT_COMMAND; do p="${p#"${p%%[! ]*}"}"; [[ "$BASH_COMMAND" == "$p" ]] && return; done; fi; [[ "${__e_in:-0}" == "0" ]] && { __e_in=1; printf \'\\e]633;E;%s\\a\\e]633;C\\a\' "$(__e_esc "$BASH_COMMAND")"; }; }',
|
|
31
31
|
'__e_cmd() { local c="$?"; [[ "${__e_in:-0}" == "1" ]] && { printf \'\\e]633;D;%s\\a\' "$c"; __e_in=0; }; printf \'\\e]633;P;Cwd=%s\\a\\e]633;A\\a\' "$(__e_esc "$PWD")"; return "$c"; }',
|
|
32
32
|
'trap \'__e_pre\' DEBUG',
|
|
33
33
|
'PROMPT_COMMAND="__e_cmd${PROMPT_COMMAND:+; $PROMPT_COMMAND}"',
|
|
@@ -40,17 +40,19 @@ function getBashInlineIntegration () {
|
|
|
40
40
|
* Properly formatted for semicolon joining
|
|
41
41
|
*/
|
|
42
42
|
function getZshInlineIntegration () {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
43
|
+
// Register hooks by appending to the precmd/preexec hook arrays directly.
|
|
44
|
+
// add-zsh-hook is an autoloadable shell function: on systems like NixOS
|
|
45
|
+
// fpath may not include the standard functions directory, the autoload
|
|
46
|
+
// fails silently (stderr is suppressed) and the hooks never run, breaking
|
|
47
|
+
// path following. Appending to the arrays works on every zsh.
|
|
45
48
|
return [
|
|
46
49
|
'if [[ -o interactive ]] && [[ -z "${ELECTERM_SHELL_INTEGRATION:-}" ]]',
|
|
47
50
|
'then export ELECTERM_SHELL_INTEGRATION=1',
|
|
48
51
|
'__e_esc() { local v="$1"; v="${v//\\\\/\\\\\\\\}"; v="${v//;/\\\\x3b}"; builtin printf \'%s\' "$v"; }',
|
|
49
52
|
'__e_preexec() { __e_cmd="$1"; builtin printf \'\\e]633;E;%s\\a\\e]633;C\\a\' "$(__e_esc "$1")"; }',
|
|
50
53
|
'__e_precmd() { local c="$?"; [[ -n "$__e_cmd" ]] && builtin printf \'\\e]633;D;%s\\a\' "$c"; __e_cmd=""; builtin printf \'\\e]633;P;Cwd=%s\\a\\e]633;A\\a\' "$(__e_esc "$PWD")"; }',
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'add-zsh-hook preexec __e_preexec',
|
|
54
|
+
'precmd_functions+=(__e_precmd)',
|
|
55
|
+
'preexec_functions+=(__e_preexec)',
|
|
54
56
|
'fi'
|
|
55
57
|
].join('; ')
|
|
56
58
|
}
|
|
@@ -78,11 +80,13 @@ function getShInlineIntegration () {
|
|
|
78
80
|
return [
|
|
79
81
|
'if [ -z "$ELECTERM_SHELL_INTEGRATION" ]',
|
|
80
82
|
'then export ELECTERM_SHELL_INTEGRATION=1',
|
|
81
|
-
'__e_esc() { printf
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
83
|
+
'__e_esc() { printf \'%s\' "$1" | sed \'s/\\\\/\\\\\\\\/g; s/;/\\\\x3b/g\'; }',
|
|
84
|
+
// sh/dash/ash re-evaluate $(...) inside PS1 every time the prompt is
|
|
85
|
+
// shown, but only if the $() stays literal — so PS1 must be assigned
|
|
86
|
+
// single-quoted. printf interprets \033/\a, unlike PS1 backslash
|
|
87
|
+
// escapes which dash does not expand.
|
|
88
|
+
'__e_ps1() { printf \'\\033]633;P;Cwd=%s\\a\\033]633;A\\a\' "$(__e_esc "$PWD")"; }',
|
|
89
|
+
'PS1=\'$(__e_ps1)\'"${PS1:-# }"',
|
|
86
90
|
'fi'
|
|
87
91
|
].join('; ')
|
|
88
92
|
}
|
|
@@ -139,6 +143,13 @@ export function wrapSilent (cmd, shellType) {
|
|
|
139
143
|
* @returns {string} Complete command to send to terminal
|
|
140
144
|
*/
|
|
141
145
|
export function getShellIntegrationCommand (shellType = 'bash') {
|
|
146
|
+
if (shellType === 'fish') {
|
|
147
|
+
// fish is not a Bourne shell: the eval '\''...'\'' wrapper of wrapSilent
|
|
148
|
+
// is a parse error in fish (nested single quotes stay unbalanced), so
|
|
149
|
+
// the whole injection silently fails. Send the script raw instead —
|
|
150
|
+
// it is already valid fish syntax as-is.
|
|
151
|
+
return ' ' + getInlineShellIntegration(shellType) + '\r'
|
|
152
|
+
}
|
|
142
153
|
const cmd = getInlineShellIntegration(shellType)
|
|
143
154
|
return wrapSilent(cmd, shellType)
|
|
144
155
|
}
|
|
@@ -22,6 +22,18 @@
|
|
|
22
22
|
* visualViewport.height against a baseline captured at the moment the
|
|
23
23
|
* terminal input is focused (i.e. before the keyboard opens), which holds
|
|
24
24
|
* the full pre-keyboard height on both platforms.
|
|
25
|
+
*
|
|
26
|
+
* Telling a keyboard from a desktop window resize (pure size heuristics
|
|
27
|
+
* fail — dragging a window's top/bottom edge is also a height-only shrink)
|
|
28
|
+
* needs one of two extra signals, checked in onResize:
|
|
29
|
+
* - an overlay-style keyboard (iOS / HarmonyOS) shrinks the visual
|
|
30
|
+
* viewport but keeps the layout viewport (window.innerHeight) full —
|
|
31
|
+
* a window resize shrinks both.
|
|
32
|
+
* - an Android-style keyboard resizes both viewports, but a soft keyboard
|
|
33
|
+
* only ever opens in response to a *touch* into the terminal — a window
|
|
34
|
+
* resized with a mouse never shows one.
|
|
35
|
+
* Tablets running the desktop program still get the bar; mouse-driven
|
|
36
|
+
* desktops never do.
|
|
25
37
|
*/
|
|
26
38
|
|
|
27
39
|
import { lazy, Suspense, useEffect, useState } from 'react'
|
|
@@ -35,6 +47,11 @@ const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
|
35
47
|
// bar above a keyboard that overlays the page (iOS / HarmonyOS).
|
|
36
48
|
export const KEYBOARD_MIN = 120
|
|
37
49
|
|
|
50
|
+
// how long (ms) after an in-page touch an Android-style (layout-resizing)
|
|
51
|
+
// keyboard is expected to be up. Bounds the touch signal used to tell that
|
|
52
|
+
// keyboard from a mouse-driven window resize on touch-capable desktops.
|
|
53
|
+
const TOUCH_GRACE_MS = 3000
|
|
54
|
+
|
|
38
55
|
// kept as a coarse capability fallback for engines without visualViewport.
|
|
39
56
|
// Modern Chromium (Electron) always exposes window.visualViewport, so this is
|
|
40
57
|
// effectively only reached on very old runtimes — where remote-control
|
|
@@ -77,6 +94,12 @@ export default function ShortcutBarEntry (props) {
|
|
|
77
94
|
// the pre-keyboard (full) size, and the subsequent resize lets us measure
|
|
78
95
|
// exactly how much the keyboard took — on both iOS and Android.
|
|
79
96
|
let baseline = { height: vv.height, width: vv.width }
|
|
97
|
+
// when the last in-page touch happened (0 = never). An Android-style
|
|
98
|
+
// keyboard resizes the layout viewport exactly like a window resize does,
|
|
99
|
+
// so size alone can't separate them there — but a keyboard only ever
|
|
100
|
+
// opens right after a *touch*, while a window drag doesn't fire in-page
|
|
101
|
+
// pointer events at all, so an old touch must not count forever.
|
|
102
|
+
let lastTouchAt = 0
|
|
80
103
|
|
|
81
104
|
function onFocusIn (ev) {
|
|
82
105
|
const el = ev.target
|
|
@@ -85,11 +108,25 @@ export default function ShortcutBarEntry (props) {
|
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
|
|
111
|
+
function onPointerDown (ev) {
|
|
112
|
+
// touch or stylus — both tap like a finger and can raise the keyboard
|
|
113
|
+
if (ev.pointerType === 'touch' || ev.pointerType === 'pen') {
|
|
114
|
+
lastTouchAt = Date.now()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
function onResize () {
|
|
89
119
|
// ignore viewport changes that also changed width (rotation / split) —
|
|
90
120
|
// a keyboard changes height only.
|
|
91
121
|
const sameWidth = Math.abs(vv.width - baseline.width) < 20
|
|
92
|
-
if (sameWidth
|
|
122
|
+
if (!sameWidth || baseline.height - vv.height <= KEYBOARD_MIN) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
// overlay keyboard (iOS / HarmonyOS): the layout viewport stays full
|
|
126
|
+
// while the visual one shrinks. A window resize shrinks both — this is
|
|
127
|
+
// what rules out mouse window-dragging on those platforms.
|
|
128
|
+
const layoutShrunk = window.innerHeight < baseline.height - KEYBOARD_MIN
|
|
129
|
+
if (!layoutShrunk) {
|
|
93
130
|
setKeyboardSeen(true)
|
|
94
131
|
// The user is focused in a terminal with the keyboard up — they need
|
|
95
132
|
// the bar right now. Set visibility here rather than waiting for the
|
|
@@ -100,13 +137,27 @@ export default function ShortcutBarEntry (props) {
|
|
|
100
137
|
if (store && store.inActiveTerminal) {
|
|
101
138
|
store.shortcutBarVisible = true
|
|
102
139
|
}
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
// Android-style resize keyboard: both viewports shrink, so require a
|
|
143
|
+
// recent touch — a keyboard opens within moments of the tap, while a
|
|
144
|
+
// window drag fires no in-page pointerdown at all, so any touch old
|
|
145
|
+
// enough to have expired can't have been the cause.
|
|
146
|
+
if (Date.now() - lastTouchAt < TOUCH_GRACE_MS) {
|
|
147
|
+
setKeyboardSeen(true)
|
|
148
|
+
const { store } = window
|
|
149
|
+
if (store && store.inActiveTerminal) {
|
|
150
|
+
store.shortcutBarVisible = true
|
|
151
|
+
}
|
|
103
152
|
}
|
|
104
153
|
}
|
|
105
154
|
|
|
106
155
|
document.addEventListener('focusin', onFocusIn)
|
|
156
|
+
document.addEventListener('pointerdown', onPointerDown)
|
|
107
157
|
vv.addEventListener('resize', onResize)
|
|
108
158
|
return () => {
|
|
109
159
|
document.removeEventListener('focusin', onFocusIn)
|
|
160
|
+
document.removeEventListener('pointerdown', onPointerDown)
|
|
110
161
|
vv.removeEventListener('resize', onResize)
|
|
111
162
|
}
|
|
112
163
|
}, [])
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
6
6
|
import wait from '../../common/wait'
|
|
7
7
|
import TermInteractiveUI from './terminal-interactive-ui'
|
|
8
|
+
import ExternalLink from '../common/external-link.jsx'
|
|
9
|
+
import { notification } from '../common/notification'
|
|
8
10
|
|
|
9
11
|
export default function TermInteractive () {
|
|
10
12
|
const [current, setCurrent] = useState(null)
|
|
@@ -15,6 +17,32 @@ export default function TermInteractive () {
|
|
|
15
17
|
window.store.updateTab(data.tabId, data.update)
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function notifyProxyCommandMessage (data) {
|
|
21
|
+
const lines = (data.message || '').split('\n')
|
|
22
|
+
const title = lines[0] || 'ssh proxy command'
|
|
23
|
+
const body = lines.slice(1).join('\n').trim()
|
|
24
|
+
// make any http(s) link in the message clickable (SSO login url)
|
|
25
|
+
const renderBody = (text) => {
|
|
26
|
+
const parts = text.split(/(https?:\/\/\S+)/)
|
|
27
|
+
return (
|
|
28
|
+
<pre className='pd0 mg0 overflow-auto'>
|
|
29
|
+
{
|
|
30
|
+
parts.map((part, i) => /^https?:\/\//.test(part)
|
|
31
|
+
? <ExternalLink key={i} to={part}>{part}</ExternalLink>
|
|
32
|
+
: part)
|
|
33
|
+
}
|
|
34
|
+
</pre>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
notification.info({
|
|
38
|
+
message: title,
|
|
39
|
+
description: body
|
|
40
|
+
? renderBody(body)
|
|
41
|
+
: undefined,
|
|
42
|
+
duration: 30
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
function processNext () {
|
|
19
47
|
const next = queueRef.current.shift()
|
|
20
48
|
if (next) {
|
|
@@ -47,6 +75,13 @@ export default function TermInteractive () {
|
|
|
47
75
|
e.data.includes('ssh-tunnel-result')
|
|
48
76
|
) {
|
|
49
77
|
updateTab(JSON.parse(e.data))
|
|
78
|
+
} else if (
|
|
79
|
+
e &&
|
|
80
|
+
e.data &&
|
|
81
|
+
typeof e.data === 'string' &&
|
|
82
|
+
e.data.includes('ssh-proxy-command-message')
|
|
83
|
+
) {
|
|
84
|
+
notifyProxyCommandMessage(JSON.parse(e.data))
|
|
50
85
|
}
|
|
51
86
|
}
|
|
52
87
|
|
|
@@ -1723,12 +1723,6 @@ class Term extends Component {
|
|
|
1723
1723
|
}
|
|
1724
1724
|
|
|
1725
1725
|
this.shellType = shellType
|
|
1726
|
-
if (shellType === 'fish') {
|
|
1727
|
-
if (this.props.sftpPathFollowSsh) {
|
|
1728
|
-
this.warnSftpFollowUnsupported()
|
|
1729
|
-
}
|
|
1730
|
-
return Promise.resolve()
|
|
1731
|
-
}
|
|
1732
1726
|
|
|
1733
1727
|
// Don't inject for sh type shells unless sftpPathFollowSsh is true
|
|
1734
1728
|
if (shellType === 'sh' && !this.props.sftpPathFollowSsh) {
|
|
@@ -1946,6 +1940,17 @@ class Term extends Component {
|
|
|
1946
1940
|
if (this.isElementVisible()) {
|
|
1947
1941
|
this.fitAddon.fit()
|
|
1948
1942
|
}
|
|
1943
|
+
// Force-send the current size now that `this.pid` is finally valid.
|
|
1944
|
+
// A resize may already have fired earlier (e.g. from the tab-becomes-
|
|
1945
|
+
// visible fit in componentDidUpdate, which can run before createTerm()
|
|
1946
|
+
// resolves) while this.pid was still undefined - the backend silently
|
|
1947
|
+
// drops resize calls for an unknown pid. If that earlier call already
|
|
1948
|
+
// updated term.cols/rows locally, xterm's resize() no-ops on the next
|
|
1949
|
+
// fit() (dims unchanged) and never fires onResize again, so the
|
|
1950
|
+
// backend pty would otherwise be stuck at its initial (possibly wrong)
|
|
1951
|
+
// size forever. Sending explicitly here, keyed off the real pid,
|
|
1952
|
+
// guarantees the remote pty/ssh channel is told the true current size.
|
|
1953
|
+
resizeTerm(this.pid, term.cols, term.rows)
|
|
1949
1954
|
term.displayRaw = displayRaw
|
|
1950
1955
|
term.loadAddon(
|
|
1951
1956
|
new KeywordHighlighterAddon(keywords)
|
|
@@ -42,7 +42,11 @@ export default function TerminalInfoActivities (props) {
|
|
|
42
42
|
bordered: true,
|
|
43
43
|
columns: col,
|
|
44
44
|
size: 'small',
|
|
45
|
-
pagination: false
|
|
45
|
+
pagination: false,
|
|
46
|
+
scroll: {
|
|
47
|
+
y: 180,
|
|
48
|
+
x: 'max-content'
|
|
49
|
+
}
|
|
46
50
|
}
|
|
47
51
|
return (
|
|
48
52
|
<div className='terminal-info-section terminal-info-act'>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Component } from 'react'
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Button,
|
|
7
|
+
Dropdown
|
|
8
8
|
} from 'antd'
|
|
9
9
|
import SwitchLabel from '../common/switch'
|
|
10
10
|
import defaults from '../../common/default-setting'
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
DatabaseOutlined,
|
|
16
16
|
BarsOutlined,
|
|
17
17
|
ApiOutlined,
|
|
18
|
-
PartitionOutlined
|
|
18
|
+
PartitionOutlined,
|
|
19
|
+
FilterOutlined,
|
|
20
|
+
CheckOutlined
|
|
19
21
|
} from '@ant-design/icons'
|
|
20
22
|
import { refs } from '../common/ref'
|
|
21
23
|
import ShowItem from '../common/show-item'
|
|
@@ -136,26 +138,39 @@ export default class TerminalInfoBase extends Component {
|
|
|
136
138
|
const {
|
|
137
139
|
terminalInfos
|
|
138
140
|
} = this.props
|
|
141
|
+
const items = defaults.terminalInfos.map(f => {
|
|
142
|
+
const checked = terminalInfos.includes(f)
|
|
143
|
+
return {
|
|
144
|
+
key: f,
|
|
145
|
+
label: (
|
|
146
|
+
<span
|
|
147
|
+
className='term-info-filter-label'
|
|
148
|
+
style={{
|
|
149
|
+
display: 'flex',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
gap: '6px'
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<span style={{ width: '14px' }}>{checked ? <CheckOutlined /> : null}</span>
|
|
155
|
+
{mapper[f]} {f}
|
|
156
|
+
</span>
|
|
157
|
+
),
|
|
158
|
+
onClick: () => this.toggleTerminalLogInfo(f)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
139
161
|
return (
|
|
140
|
-
<
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
>
|
|
153
|
-
{f}
|
|
154
|
-
</Button>
|
|
155
|
-
)
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
</Space.Compact>
|
|
162
|
+
<Dropdown
|
|
163
|
+
menu={{ items }}
|
|
164
|
+
trigger={['click']}
|
|
165
|
+
placement='bottomRight'
|
|
166
|
+
>
|
|
167
|
+
<Button
|
|
168
|
+
size='small'
|
|
169
|
+
icon={<FilterOutlined />}
|
|
170
|
+
>
|
|
171
|
+
{e('filter')}({terminalInfos.length}/{defaults.terminalInfos.length})
|
|
172
|
+
</Button>
|
|
173
|
+
</Dropdown>
|
|
159
174
|
)
|
|
160
175
|
}
|
|
161
176
|
|
|
@@ -30,7 +30,11 @@ export default function TerminalInfoDisk (props) {
|
|
|
30
30
|
bordered: true,
|
|
31
31
|
columns: col,
|
|
32
32
|
size: 'small',
|
|
33
|
-
pagination: false
|
|
33
|
+
pagination: false,
|
|
34
|
+
scroll: {
|
|
35
|
+
y: 180,
|
|
36
|
+
x: 'max-content'
|
|
37
|
+
}
|
|
34
38
|
}
|
|
35
39
|
return (
|
|
36
40
|
<div className='terminal-info-section terminal-info-disk'>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolve local terminal info: os / hostname / kernel / arch / shell + version
|
|
3
|
+
* uses the electron os module via window.pre.osInfo() (sync) and probes the
|
|
4
|
+
* configured shell binary for its version through the local fs run bridge
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isMac, isWin } from '../../common/constants'
|
|
8
|
+
import fs from '../../common/fs'
|
|
9
|
+
|
|
10
|
+
function parseOsInfo (list) {
|
|
11
|
+
const map = {}
|
|
12
|
+
for (const { k, v } of list || []) {
|
|
13
|
+
let val
|
|
14
|
+
try {
|
|
15
|
+
val = JSON.parse(v)
|
|
16
|
+
} catch (e) {
|
|
17
|
+
val = v
|
|
18
|
+
}
|
|
19
|
+
map[k] = Array.isArray(val) ? val.join(' ') : String(val)
|
|
20
|
+
}
|
|
21
|
+
return map
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const shellNameReg = /(\w+)$/
|
|
25
|
+
|
|
26
|
+
function formatOsName (map) {
|
|
27
|
+
const ver = map.release || ''
|
|
28
|
+
if (isWin) {
|
|
29
|
+
const main = parseInt(ver.split('.')[0], 10)
|
|
30
|
+
if (main >= 10) {
|
|
31
|
+
const build = parseInt(ver.split('.')[2], 10)
|
|
32
|
+
return build >= 22000 ? `Windows 11 (${ver})` : `Windows 10 (${ver})`
|
|
33
|
+
}
|
|
34
|
+
return `Windows (${ver})`
|
|
35
|
+
}
|
|
36
|
+
if (isMac) {
|
|
37
|
+
return `macOS ${map.version || ''}`.trim()
|
|
38
|
+
}
|
|
39
|
+
return `Linux ${ver}`.trim()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function probeShellVersion (shellPath) {
|
|
43
|
+
if (!shellPath) {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
let out = ''
|
|
48
|
+
if (isWin) {
|
|
49
|
+
// runWinCmd wraps the given script with powershell.exe -Command
|
|
50
|
+
const res = await fs.runWinCmd(
|
|
51
|
+
'$PSVersionTable.PSVersion.ToString()'
|
|
52
|
+
).catch(() => null)
|
|
53
|
+
out = res?.stdout || ''
|
|
54
|
+
} else {
|
|
55
|
+
out = await fs.run(`'${shellPath.replace(/'/g, "'\\''")}' --version`).catch(() => '')
|
|
56
|
+
}
|
|
57
|
+
return (out || '').split('\n')[0].trim()
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return ''
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default async function resolveLocalInfo (pid) {
|
|
64
|
+
const store = window.store
|
|
65
|
+
if (!window.pre?.osInfo) {
|
|
66
|
+
// web app build: no local os bridge available
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
const tab = store?.getTabs?.().find(t => t.id === pid)
|
|
70
|
+
const config = store?.config || {}
|
|
71
|
+
const execProp = isWin ? 'execWindows' : isMac ? 'execMac' : 'execLinux'
|
|
72
|
+
// tab settings (bookmark) win over global config, same as terminal spawn logic
|
|
73
|
+
const shellPath = tab?.[execProp] || config[execProp] || ''
|
|
74
|
+
const shellBase = isWin
|
|
75
|
+
? (shellPath.split(/[\\/]/).pop() || '')
|
|
76
|
+
: shellPath.split('/').pop() || ''
|
|
77
|
+
const nameMatch = shellBase.match(shellNameReg)
|
|
78
|
+
const shellName = nameMatch ? nameMatch[1] : shellBase
|
|
79
|
+
const map = parseOsInfo(window.pre.osInfo())
|
|
80
|
+
const shellVersion = await probeShellVersion(shellPath)
|
|
81
|
+
return {
|
|
82
|
+
os: formatOsName(map),
|
|
83
|
+
hostname: map.hostname || '',
|
|
84
|
+
kernel: isWin ? `Windows NT ${map.release || ''}`.trim() : (map.release || ''),
|
|
85
|
+
arch: map.arch || '',
|
|
86
|
+
shell: shellName,
|
|
87
|
+
shellVersion
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -106,7 +106,11 @@ export default function TerminalInfoDisk (props) {
|
|
|
106
106
|
bordered: true,
|
|
107
107
|
size: 'small',
|
|
108
108
|
columns: col,
|
|
109
|
-
pagination: false
|
|
109
|
+
pagination: false,
|
|
110
|
+
scroll: {
|
|
111
|
+
y: 180,
|
|
112
|
+
x: 'max-content'
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
return (
|
|
112
116
|
<div className='terminal-info-section terminal-info-network'>
|
|
@@ -32,6 +32,33 @@ function formatActivities (str) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function formatSysInfo (unameStr, prettyName) {
|
|
36
|
+
if (!unameStr) {
|
|
37
|
+
return {}
|
|
38
|
+
}
|
|
39
|
+
const parts = unameStr.trim().split(/\s+/)
|
|
40
|
+
if (parts.length < 4) {
|
|
41
|
+
return {}
|
|
42
|
+
}
|
|
43
|
+
const [sysname, hostname, kernel, arch] = parts
|
|
44
|
+
let os = sysname
|
|
45
|
+
const m = (prettyName || '').match(/PRETTY_NAME="?([^"\r\n]+)"?/)
|
|
46
|
+
if (m) {
|
|
47
|
+
os = m[1].trim()
|
|
48
|
+
} else if (sysname === 'Darwin') {
|
|
49
|
+
os = 'macOS'
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
sysInfo: {
|
|
53
|
+
os,
|
|
54
|
+
sysname,
|
|
55
|
+
hostname,
|
|
56
|
+
kernel,
|
|
57
|
+
arch
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
function formatCpu (str) {
|
|
36
63
|
if (!str) {
|
|
37
64
|
return {
|
|
@@ -153,9 +180,11 @@ function InfoGetter (props) {
|
|
|
153
180
|
props.setState(update)
|
|
154
181
|
}
|
|
155
182
|
run()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
if (interval > 0) {
|
|
184
|
+
const ref = setInterval(run, interval)
|
|
185
|
+
return () => {
|
|
186
|
+
clearInterval(ref)
|
|
187
|
+
}
|
|
159
188
|
}
|
|
160
189
|
}, [pid])
|
|
161
190
|
return null
|
|
@@ -166,6 +195,16 @@ export default (props) => {
|
|
|
166
195
|
return null
|
|
167
196
|
}
|
|
168
197
|
const cmds = [
|
|
198
|
+
{
|
|
199
|
+
name: 'sysinfo',
|
|
200
|
+
cmds: [
|
|
201
|
+
'uname -s -n -r -m',
|
|
202
|
+
'grep PRETTY_NAME= /etc/os-release 2>/dev/null || echo'
|
|
203
|
+
],
|
|
204
|
+
interval: 0,
|
|
205
|
+
delay: 0,
|
|
206
|
+
formatter: formatSysInfo
|
|
207
|
+
},
|
|
169
208
|
{
|
|
170
209
|
name: 'uptime',
|
|
171
210
|
cmd: 'uptime -p',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* system info: os / hostname / kernel / arch (+ shell for local terminals)
|
|
3
|
+
* remote info comes from uname via the session runCmd bridge,
|
|
4
|
+
* local info from the os module + a shell version probe
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import { DesktopOutlined } from '@ant-design/icons'
|
|
9
|
+
import resolveLocalInfo from './local-info-resolver'
|
|
10
|
+
|
|
11
|
+
export default function TerminalInfoSys (props) {
|
|
12
|
+
const { sysInfo, isRemote, pid } = props
|
|
13
|
+
const [localInfo, setLocalInfo] = useState(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (isRemote) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
resolveLocalInfo(pid).then(setLocalInfo)
|
|
20
|
+
}, [isRemote, pid])
|
|
21
|
+
|
|
22
|
+
const info = isRemote ? sysInfo : localInfo
|
|
23
|
+
if (!info || !info.os) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
const rows = [
|
|
27
|
+
['os', info.os],
|
|
28
|
+
['hostname', info.hostname],
|
|
29
|
+
['kernel', info.kernel],
|
|
30
|
+
['arch', info.arch]
|
|
31
|
+
]
|
|
32
|
+
if (info.shell) {
|
|
33
|
+
rows.push(['shell', info.shellVersion ? `${info.shell} ${info.shellVersion}` : info.shell])
|
|
34
|
+
}
|
|
35
|
+
const final = rows.filter(d => d[1])
|
|
36
|
+
if (!final.length) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
<div className='terminal-info-section terminal-info-sys'>
|
|
41
|
+
<div className='pd1y bold'><DesktopOutlined /> System</div>
|
|
42
|
+
<div className='terminal-info-sys-rows'>
|
|
43
|
+
{
|
|
44
|
+
final.map(([k, v]) => {
|
|
45
|
+
return (
|
|
46
|
+
<div className='terminal-info-sys-row' key={k}>
|
|
47
|
+
<span className='terminal-info-sys-key'>{k}</span>
|
|
48
|
+
<span className='terminal-info-sys-val' title={v}>{v}</span>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -9,6 +9,7 @@ import TerminalInfoNetwork from './network'
|
|
|
9
9
|
import TerminalInfoResource from './resource'
|
|
10
10
|
import TerminalInfoActivities from './activity'
|
|
11
11
|
import TerminalInfoDisk from './disk'
|
|
12
|
+
import TerminalInfoSys from './sys-info'
|
|
12
13
|
import RunCmd from './run-cmd'
|
|
13
14
|
import { runCmd } from '../terminal/terminal-apis'
|
|
14
15
|
import './terminal-info.styl'
|
|
@@ -21,7 +22,8 @@ export default class TerminalInfoContent extends PureComponent {
|
|
|
21
22
|
swap: {},
|
|
22
23
|
activities: [],
|
|
23
24
|
disks: [],
|
|
24
|
-
network: {}
|
|
25
|
+
network: {},
|
|
26
|
+
sysInfo: null
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
setStateRef = (...args) => {
|
|
@@ -44,6 +46,7 @@ export default class TerminalInfoContent extends PureComponent {
|
|
|
44
46
|
return (
|
|
45
47
|
<>
|
|
46
48
|
<TerminalInfoBase {...props} {...state} />
|
|
49
|
+
<TerminalInfoSys {...props} {...state} />
|
|
47
50
|
<TerminalInfoUp {...props} {...state} />
|
|
48
51
|
<TerminalInfoResource
|
|
49
52
|
{...props} {...state}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
.terminal-info-icon
|
|
3
2
|
color var(--text-dark)
|
|
4
3
|
&:hover
|
|
@@ -23,11 +22,27 @@
|
|
|
23
22
|
display block
|
|
24
23
|
.terminal-info-section
|
|
25
24
|
padding 10px 0
|
|
26
|
-
.terminal-info-
|
|
27
|
-
|
|
28
|
-
.terminal-info-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
.terminal-info-sys
|
|
26
|
+
border-bottom 1px solid var(--main-darker)
|
|
27
|
+
.terminal-info-sys-rows
|
|
28
|
+
display flex
|
|
29
|
+
flex-direction column
|
|
30
|
+
gap 4px
|
|
31
|
+
.terminal-info-sys-row
|
|
32
|
+
display flex
|
|
33
|
+
gap 8px
|
|
34
|
+
align-items baseline
|
|
35
|
+
font-size 12px
|
|
36
|
+
.terminal-info-sys-key
|
|
37
|
+
flex none
|
|
38
|
+
width 80px
|
|
39
|
+
color var(--text-dark)
|
|
40
|
+
text-transform capitalize
|
|
41
|
+
.terminal-info-sys-val
|
|
42
|
+
flex 1
|
|
43
|
+
overflow hidden
|
|
44
|
+
text-overflow ellipsis
|
|
45
|
+
white-space nowrap
|
|
31
46
|
.activity-item
|
|
32
47
|
max-height 1.6em
|
|
33
48
|
line-height 1.6
|
|
@@ -36,4 +51,4 @@
|
|
|
36
51
|
max-height none
|
|
37
52
|
.info-panel-wrap-pin
|
|
38
53
|
.toggle-info-panel-wrap
|
|
39
|
-
color var(--success)
|
|
54
|
+
color var(--success)
|
package/client/store/common.js
CHANGED
|
@@ -11,14 +11,16 @@ import {
|
|
|
11
11
|
leftSideBarOpenKey,
|
|
12
12
|
rightSidebarWidthKey,
|
|
13
13
|
addPanelWidthLsKey,
|
|
14
|
-
dismissDelKeyTipLsKey,
|
|
15
14
|
connectionMap,
|
|
16
15
|
lastAiChatSessionIdKey,
|
|
17
16
|
mobileBreakpoint,
|
|
18
|
-
splitMap
|
|
17
|
+
splitMap,
|
|
18
|
+
settingAiId,
|
|
19
|
+
settingSyncId
|
|
19
20
|
} from '../common/constants'
|
|
20
21
|
import * as ls from '../common/safe-local-storage'
|
|
21
22
|
import { refs, refsStatic } from '../components/common/ref'
|
|
23
|
+
import { requireTermOfUse } from '../common/term-of-use'
|
|
22
24
|
import { action } from 'manate'
|
|
23
25
|
import uid from '../common/uid'
|
|
24
26
|
import deepCopy from 'json-deep-copy'
|
|
@@ -56,7 +58,9 @@ export default Store => {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
Store.prototype.toggleAIConfig = function () {
|
|
59
|
-
|
|
61
|
+
requireTermOfUse('ai', () => {
|
|
62
|
+
window.store.showAIConfigModal = true
|
|
63
|
+
})
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
Store.prototype.onResize = debounce(async function () {
|
|
@@ -99,6 +103,18 @@ export default Store => {
|
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
Store.prototype.setSettingItem = function (v) {
|
|
106
|
+
// entering the AI / sync setting page requires the term of use
|
|
107
|
+
// confirmation first (when the term is defined)
|
|
108
|
+
if (
|
|
109
|
+
v && (
|
|
110
|
+
v.id === settingAiId || v.id === settingSyncId
|
|
111
|
+
)
|
|
112
|
+
) {
|
|
113
|
+
const type = v.id === settingAiId ? 'ai' : 'sync'
|
|
114
|
+
return requireTermOfUse(type, () => {
|
|
115
|
+
window.store.settingItem = v
|
|
116
|
+
})
|
|
117
|
+
}
|
|
102
118
|
window.store.settingItem = v
|
|
103
119
|
}
|
|
104
120
|
|
|
@@ -131,10 +147,6 @@ export default Store => {
|
|
|
131
147
|
ls.setItem(rightSidebarWidthKey, v)
|
|
132
148
|
window.store._rightPanelWidth = v
|
|
133
149
|
}
|
|
134
|
-
Store.prototype.dismissDelKeyTip = function (v) {
|
|
135
|
-
ls.setItem(dismissDelKeyTipLsKey, 'y')
|
|
136
|
-
window.store.hideDelKeyTip = true
|
|
137
|
-
}
|
|
138
150
|
Store.prototype.beforeExit = function (evt) {
|
|
139
151
|
const { confirmBeforeExit } = window.store.config
|
|
140
152
|
if (
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
leftSidePanelWidthKey,
|
|
20
20
|
rightSidebarWidthKey,
|
|
21
21
|
addPanelWidthLsKey,
|
|
22
|
-
dismissDelKeyTipLsKey,
|
|
23
22
|
qmSortByFrequencyKey,
|
|
24
23
|
resolutionsLsKey,
|
|
25
24
|
syncServerDataKey,
|
|
@@ -216,7 +215,6 @@ export default () => {
|
|
|
216
215
|
hasNodePty: window.pre.runSync('nodePtyCheck'),
|
|
217
216
|
isMobile: window.innerWidth <= mobileBreakpoint,
|
|
218
217
|
fullscreen: false,
|
|
219
|
-
hideDelKeyTip: ls.getItem(dismissDelKeyTipLsKey) === 'y',
|
|
220
218
|
tabsHeight: 36,
|
|
221
219
|
|
|
222
220
|
// widgets
|
|
@@ -810,7 +810,9 @@ export default Store => {
|
|
|
810
810
|
const { command, tabId, timeoutMs } = args
|
|
811
811
|
const marker = `__ET_EXEC_${uid()}__`
|
|
812
812
|
const startLine = `${marker}S`
|
|
813
|
-
|
|
813
|
+
// Run in a subshell so commands like `exit`/`exec` cannot kill the outer
|
|
814
|
+
// shell before the end sentinel is emitted; $? holds the subshell's code.
|
|
815
|
+
const wrapped = `echo "${startLine}"; ( ${command} ); echo "${marker}E$?"`
|
|
814
816
|
|
|
815
817
|
store.runQuickCommand(wrapped, false, tabId)
|
|
816
818
|
|
package/client/store/setting.js
CHANGED
|
@@ -14,6 +14,7 @@ import { buildNewTheme } from '../common/terminal-theme'
|
|
|
14
14
|
import getInitItem from '../common/init-setting-item'
|
|
15
15
|
import newTerm from '../common/new-terminal'
|
|
16
16
|
import settingList from '../common/setting-list'
|
|
17
|
+
import { requireTermOfUse } from '../common/term-of-use'
|
|
17
18
|
|
|
18
19
|
const e = window.translate
|
|
19
20
|
|
|
@@ -103,11 +104,13 @@ export default Store => {
|
|
|
103
104
|
) {
|
|
104
105
|
return store.hideSettingModal()
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
requireTermOfUse('sync', () => {
|
|
108
|
+
store.storeAssign({
|
|
109
|
+
settingTab: settingMap.setting
|
|
110
|
+
})
|
|
111
|
+
store.setSettingItem(settingList().find(d => d.id === settingSyncId))
|
|
112
|
+
store.openSettingModal()
|
|
108
113
|
})
|
|
109
|
-
store.setSettingItem(settingList().find(d => d.id === settingSyncId))
|
|
110
|
-
store.openSettingModal()
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
Store.prototype.openTerminalThemes = function () {
|
package/client/store/tab.js
CHANGED
|
@@ -111,17 +111,22 @@ export default Store => {
|
|
|
111
111
|
// Remove old tab
|
|
112
112
|
tabs.splice(index, 1)
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
114
|
+
// Update active tab id synchronously (same as addTab) so the new tab's
|
|
115
|
+
// container is already visible (display:block) by the time its Terminal
|
|
116
|
+
// mounts. Deferring this via setTimeout(0) left the container hidden at
|
|
117
|
+
// mount time, causing the initial fit()/createTerm() to use xterm's
|
|
118
|
+
// default 80x24 size instead of the real fitted size - which then got
|
|
119
|
+
// baked into the remote pty and only partially corrected later, leaving
|
|
120
|
+
// full-screen apps like tmux/vim rendering at the wrong size.
|
|
121
|
+
if (store.activeTabId === tabId) {
|
|
122
|
+
store.activeTabId = newTab.id
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Update batch current tab ID if needed
|
|
126
|
+
const batchProp = `activeTabId${oldBatch}`
|
|
127
|
+
if (store[batchProp] === tabId) {
|
|
128
|
+
store[batchProp] = newTab.id
|
|
129
|
+
}
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
Store.prototype.reloadAllTabs = function () {
|