@electerm/electerm-react 5.3.15 → 5.5.6
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 +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +23 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +14 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/bookmark-form/index.jsx +10 -8
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +80 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/col.jsx +32 -6
- package/client/components/setting-panel/list.styl +12 -0
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-mobile-breadcrumb.jsx +29 -0
- package/client/components/setting-panel/setting-modal.jsx +64 -44
- package/client/components/setting-panel/setting-terminal.jsx +7 -2
- package/client/components/setting-panel/setting-wrap.jsx +0 -3
- package/client/components/setting-panel/setting-wrap.styl +80 -11
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sidebar/index.jsx +28 -21
- package/client/components/sidebar/left-sidebar-icons.jsx +1 -1
- package/client/components/sidebar/sidebar.styl +16 -0
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/tabs/tabs.styl +4 -0
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +88 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +136 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-entry.jsx +32 -27
- package/client/components/terminal/shortcut-bar.jsx +4 -6
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +56 -0
- package/client/components/terminal/startup-queue.js +379 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal.jsx +102 -2007
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/theme/theme-form.jsx +22 -0
- package/client/components/tree-list/bookmark-transport.jsx +7 -31
- package/client/components/tree-list/tree-list.styl +13 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +9 -1
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +14 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +20 -0
- package/client/store/sidebar.js +12 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +45 -9
- package/client/store/system-menu.js +2 -0
- package/client/store/tab.js +41 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { lazy, Suspense, useEffect, useState } from 'react'
|
|
40
|
+
import { auto } from 'manate/react'
|
|
41
|
+
import LazyBoundary from '../common/lazy-boundary'
|
|
40
42
|
|
|
41
43
|
const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
42
44
|
|
|
@@ -52,38 +54,37 @@ export const KEYBOARD_MIN = 120
|
|
|
52
54
|
// keyboard from a mouse-driven window resize on touch-capable desktops.
|
|
53
55
|
const TOUCH_GRACE_MS = 3000
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// effectively only reached on very old runtimes — where remote-control
|
|
58
|
-
// misreporting is not a concern.
|
|
59
|
-
export function isTouchDevice () {
|
|
60
|
-
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
const coarse = typeof window.matchMedia === 'function' &&
|
|
64
|
-
window.matchMedia('(pointer: coarse)').matches
|
|
65
|
-
const hasTouch = (navigator.maxTouchPoints || 0) > 0 ||
|
|
66
|
-
('ontouchstart' in window)
|
|
67
|
-
return Boolean(coarse || hasTouch)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export default function ShortcutBarEntry (props) {
|
|
71
|
-
// mount the lazy bar only after a soft keyboard has been seen at least once,
|
|
72
|
-
// so its code (and antd/icon deps) stays out of the main bundle on devices
|
|
73
|
-
// that never show one (desktops, remote-control clients mis-reported as
|
|
74
|
-
// touch, etc.).
|
|
75
|
-
//
|
|
57
|
+
function ShortcutBarEntry (props) {
|
|
58
|
+
const { store } = props
|
|
76
59
|
// opt out entirely when the user has disabled the shortcut bar in common
|
|
77
60
|
// settings — bail before any listener is attached or component imported.
|
|
78
|
-
const { store } = props
|
|
79
61
|
const disabled = store.config.disableShortcutBar
|
|
62
|
+
// follow the store's live touch state (flipped by main.jsx from real
|
|
63
|
+
// pointer input — mouse → false, touch/pen → true) instead of probing screen
|
|
64
|
+
// capability, which only says the screen *can* be touched, not whether the
|
|
65
|
+
// user is actually using touch, so the bar reacts when a touch-capable
|
|
66
|
+
// machine starts being used by touch.
|
|
67
|
+
const isTouch = store.isTouchDevice
|
|
68
|
+
// very old runtimes expose no visualViewport — the lazy "wait for a real
|
|
69
|
+
// keyboard" signal below can never fire there, so fall back straight to the
|
|
70
|
+
// touch state.
|
|
71
|
+
const noVisualViewport = typeof window === 'undefined' || !window.visualViewport
|
|
80
72
|
const [keyboardSeen, setKeyboardSeen] = useState(() => {
|
|
81
|
-
if (
|
|
82
|
-
return disabled
|
|
73
|
+
if (noVisualViewport) {
|
|
74
|
+
return !disabled && isTouch
|
|
83
75
|
}
|
|
84
76
|
return false
|
|
85
77
|
})
|
|
86
78
|
|
|
79
|
+
// keep the no-visualViewport fallback current as the store's touch state
|
|
80
|
+
// flips at runtime; on modern engines keyboardSeen is driven by the resize
|
|
81
|
+
// listener below and this never runs.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (noVisualViewport) {
|
|
84
|
+
setKeyboardSeen(!disabled && isTouch)
|
|
85
|
+
}
|
|
86
|
+
}, [disabled, isTouch, noVisualViewport])
|
|
87
|
+
|
|
87
88
|
useEffect(() => {
|
|
88
89
|
const vv = window.visualViewport
|
|
89
90
|
if (!vv) {
|
|
@@ -166,8 +167,12 @@ export default function ShortcutBarEntry (props) {
|
|
|
166
167
|
return null
|
|
167
168
|
}
|
|
168
169
|
return (
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
170
|
+
<LazyBoundary>
|
|
171
|
+
<Suspense fallback={null}>
|
|
172
|
+
<ShortcutBar {...props} />
|
|
173
|
+
</Suspense>
|
|
174
|
+
</LazyBoundary>
|
|
172
175
|
)
|
|
173
176
|
}
|
|
177
|
+
|
|
178
|
+
export default auto(ShortcutBarEntry)
|
|
@@ -135,12 +135,10 @@ function ShortcutBar (props) {
|
|
|
135
135
|
// keep the offset in a CSS var consumed by shortcut-bar.styl, and expose the
|
|
136
136
|
// lifted height so layout.jsx can shrink the terminal accordingly.
|
|
137
137
|
useEffect(() => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
store.shortcutBarKbOffset = kbOffset
|
|
143
|
-
}, [kbOffset])
|
|
138
|
+
const offset = store.shortcutBarVisible ? kbOffset : 0
|
|
139
|
+
document.documentElement.style.setProperty('--shortcut-bar-kb-offset', offset + 'px')
|
|
140
|
+
store.shortcutBarKbOffset = offset
|
|
141
|
+
}, [kbOffset, store.shortcutBarVisible])
|
|
144
142
|
|
|
145
143
|
// reserve layout space + lift the footer while the bar is shown.
|
|
146
144
|
// expose the bar height as a CSS var so the .styl + footer-lift rule stay
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
.shortcut-bar-on
|
|
80
80
|
.main-footer
|
|
81
81
|
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
82
|
+
.remote-monitor-bar
|
|
83
|
+
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px) + 36px)
|
|
84
|
+
.qm-wrap-tooltip
|
|
85
|
+
bottom calc(var(--footer-stack-height, 36px) + var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
82
86
|
|
|
83
87
|
// --- edit modal -----------------------------------------------------------
|
|
84
88
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const maxCwdLength = 4096
|
|
2
|
+
|
|
3
|
+
export function isSshTab (tab = {}) {
|
|
4
|
+
return !!tab.host && (!tab.type || tab.type === 'ssh')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sanitizeSshCwd (cwd) {
|
|
8
|
+
if (typeof cwd !== 'string' || !cwd || cwd.length > maxCwdLength) {
|
|
9
|
+
return ''
|
|
10
|
+
}
|
|
11
|
+
if (/[\0\r\n]/.test(cwd)) {
|
|
12
|
+
return ''
|
|
13
|
+
}
|
|
14
|
+
return cwd
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function quotePosixShellArg (value) {
|
|
18
|
+
const quote = '\''
|
|
19
|
+
const escapedQuote = quote + '"' + quote + '"' + quote
|
|
20
|
+
return quote + value.replace(/'/g, escapedQuote) + quote
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createRestoreCwdCommand (cwd) {
|
|
24
|
+
const safeCwd = sanitizeSshCwd(cwd)
|
|
25
|
+
return safeCwd ? `cd -- ${quotePosixShellArg(safeCwd)}` : ''
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function shouldCaptureSshReloadState (tab, config = {}) {
|
|
29
|
+
return !!config.restoreTerminalSessionOnReload && isSshTab(tab)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createSshReloadState ({ cwd, screen } = {}) {
|
|
33
|
+
const safeCwd = sanitizeSshCwd(cwd)
|
|
34
|
+
const safeScreen = typeof screen === 'string' ? screen : ''
|
|
35
|
+
if (!safeCwd && !safeScreen) {
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
cwd: safeCwd,
|
|
40
|
+
screen: safeScreen
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getAlternateBufferSnapshot (buffer) {
|
|
45
|
+
if (!buffer || buffer.type !== 'alternate') {
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
const lines = []
|
|
49
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
50
|
+
lines.push(buffer.getLine(i)?.translateToString(true) || '')
|
|
51
|
+
}
|
|
52
|
+
while (lines.length && !lines[lines.length - 1]) {
|
|
53
|
+
lines.pop()
|
|
54
|
+
}
|
|
55
|
+
return lines.join('\r\n')
|
|
56
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup queue for a terminal session.
|
|
3
|
+
*
|
|
4
|
+
* Everything that has to be typed into a freshly connected shell goes through
|
|
5
|
+
* here, strictly one item at a time:
|
|
6
|
+
*
|
|
7
|
+
* shell integration injection -> cd/startDirectory -> runScripts
|
|
8
|
+
*
|
|
9
|
+
* Two rules keep it reliable:
|
|
10
|
+
*
|
|
11
|
+
* 1. Only one drain loop owns the queue at a time, and every queue generation
|
|
12
|
+
* carries a token. A reconnect (or a config toggle that rebuilds the queue)
|
|
13
|
+
* bumps the token, so a loop still waiting on the *old* connection stops
|
|
14
|
+
* instead of stealing items from the new queue - that used to silently drop
|
|
15
|
+
* the remaining run scripts.
|
|
16
|
+
* 2. After each script we wait for the shell to settle (a fresh prompt, or a
|
|
17
|
+
* quiet period) before typing the next one, so nothing is ever typed into a
|
|
18
|
+
* shell that is still busy. Every wait is bounded, so a hung shell can only
|
|
19
|
+
* delay the queue, never stop it.
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
getShellIntegrationCommand,
|
|
23
|
+
detectShellType
|
|
24
|
+
} from './shell.js'
|
|
25
|
+
import { createRestoreCwdCommand } from './ssh-reload-state.js'
|
|
26
|
+
import { isMac, isWin } from '../../common/platform.js'
|
|
27
|
+
|
|
28
|
+
// Pacing: `settleIdleMs` of silence counts as "the shell is done",
|
|
29
|
+
// `settleTimeoutMs` caps the wait so a script that never returns can not
|
|
30
|
+
// stall the queue.
|
|
31
|
+
const settleIdleMs = 400
|
|
32
|
+
const settleTimeoutMs = 3000
|
|
33
|
+
// Ceiling for the best-effort remote shell probe. detectRemoteShell opens an
|
|
34
|
+
// extra exec channel and runCmd has no timeout of its own, so a server that
|
|
35
|
+
// never answers would otherwise block the whole queue forever.
|
|
36
|
+
const remoteShellDetectTimeoutMs = 3000
|
|
37
|
+
|
|
38
|
+
export class StartupQueue {
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} term - the terminal component (host)
|
|
41
|
+
* @param {object} [deps] - `detectRemoteShell` needs the network, so it is
|
|
42
|
+
* injected: it keeps this module free of the app runtime and makes the
|
|
43
|
+
* queue testable on its own.
|
|
44
|
+
*/
|
|
45
|
+
constructor (term, deps = {}) {
|
|
46
|
+
this.term = term
|
|
47
|
+
this.detectRemoteShell = deps.detectRemoteShell || (() => Promise.resolve('sh'))
|
|
48
|
+
this.queue = []
|
|
49
|
+
this.token = 0
|
|
50
|
+
this.running = false
|
|
51
|
+
this.shellInjected = false
|
|
52
|
+
this.shellType = null
|
|
53
|
+
this.timers = new Set()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
dispose = () => {
|
|
57
|
+
for (const t of this.timers) {
|
|
58
|
+
clearTimeout(t)
|
|
59
|
+
}
|
|
60
|
+
this.timers.clear()
|
|
61
|
+
this.queue = []
|
|
62
|
+
this.running = false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---- host accessors ----
|
|
66
|
+
|
|
67
|
+
get addon () {
|
|
68
|
+
return this.term.attachAddon
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get cmdAddon () {
|
|
72
|
+
return this.term.cmdAddon
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isClosed = () => {
|
|
76
|
+
return !!this.term.onClose
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
isSsh = () => {
|
|
80
|
+
return this.term.isSsh()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isLocal = () => {
|
|
84
|
+
return this.term.isLocal()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sleep = (ms) => {
|
|
88
|
+
if (ms <= 0) {
|
|
89
|
+
return Promise.resolve()
|
|
90
|
+
}
|
|
91
|
+
return new Promise(resolve => {
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
this.timers.delete(timer)
|
|
94
|
+
resolve()
|
|
95
|
+
}, ms)
|
|
96
|
+
this.timers.add(timer)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- queue ----
|
|
101
|
+
|
|
102
|
+
runInitScript = () => {
|
|
103
|
+
const { term } = this
|
|
104
|
+
window.store.triggerResize()
|
|
105
|
+
const {
|
|
106
|
+
startDirectory,
|
|
107
|
+
runScripts
|
|
108
|
+
} = term.props.tab
|
|
109
|
+
|
|
110
|
+
const scripts = runScripts ? [...runScripts] : []
|
|
111
|
+
const reloadCwd = term.props.config.restoreTerminalSessionOnReload && this.isSsh()
|
|
112
|
+
? term.props.tab._reloadState?.cwd
|
|
113
|
+
: ''
|
|
114
|
+
const startFolder = reloadCwd || startDirectory || window.initFolder
|
|
115
|
+
const cwdCommand = this.isSsh()
|
|
116
|
+
? createRestoreCwdCommand(startFolder)
|
|
117
|
+
: startFolder ? `cd "${startFolder}"` : ''
|
|
118
|
+
if (cwdCommand) {
|
|
119
|
+
scripts.unshift({ script: cwdCommand, delay: 0 })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const token = ++this.token
|
|
123
|
+
this.queue = []
|
|
124
|
+
|
|
125
|
+
if (this.canInjectShellIntegration()) {
|
|
126
|
+
this.queue.push({
|
|
127
|
+
type: 'shell_integration',
|
|
128
|
+
execute: async () => {
|
|
129
|
+
await this.injectShellIntegration()
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
scripts.forEach((script, i) => {
|
|
135
|
+
this.queue.push({
|
|
136
|
+
type: 'delayed_script',
|
|
137
|
+
script: script.script,
|
|
138
|
+
delay: script.delay || 0,
|
|
139
|
+
// Nothing follows the last one, so waiting for the shell to settle
|
|
140
|
+
// would only add latency.
|
|
141
|
+
isLast: i === scripts.length - 1,
|
|
142
|
+
execute: () => {
|
|
143
|
+
if (script.script && this.addon) {
|
|
144
|
+
this.addon._sendData(script.script + '\r')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
this.start(token)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Put a shell integration injection at the head of the queue.
|
|
155
|
+
* Inserting - instead of firing it off immediately - keeps it serialized
|
|
156
|
+
* against anything still queued and guarantees a single drain loop.
|
|
157
|
+
*/
|
|
158
|
+
enqueueShellIntegration = (sendEnter) => {
|
|
159
|
+
this.queue.unshift({
|
|
160
|
+
type: 'shell_integration',
|
|
161
|
+
execute: async () => {
|
|
162
|
+
await this.injectShellIntegration()
|
|
163
|
+
// A fresh prompt makes the shell emit OSC 633 ;P Cwd= right away, so
|
|
164
|
+
// the file manager picks the current path up without waiting for the
|
|
165
|
+
// user's next command.
|
|
166
|
+
if (sendEnter && this.addon) {
|
|
167
|
+
this.addon._sendData('\r')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
this.start(this.token)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Drain the queue one item at a time. Re-entrant calls only push items and
|
|
176
|
+
* return, so two loops can never interleave or race for the same array.
|
|
177
|
+
*/
|
|
178
|
+
start = async (token) => {
|
|
179
|
+
if (this.running) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
this.running = true
|
|
183
|
+
try {
|
|
184
|
+
while (
|
|
185
|
+
!this.isClosed() &&
|
|
186
|
+
token === this.token &&
|
|
187
|
+
this.queue.length > 0
|
|
188
|
+
) {
|
|
189
|
+
const item = this.queue.shift()
|
|
190
|
+
try {
|
|
191
|
+
await this.runItem(item)
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[Shell Integration] Error processing queue item:', item.type, error)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
this.running = false
|
|
198
|
+
// A newer queue was built while this loop was still draining (reconnect,
|
|
199
|
+
// config toggle). Pick it up here, otherwise the fresh items would sit
|
|
200
|
+
// in the array with nobody left to run them.
|
|
201
|
+
if (
|
|
202
|
+
!this.isClosed() &&
|
|
203
|
+
token !== this.token &&
|
|
204
|
+
this.queue.length > 0
|
|
205
|
+
) {
|
|
206
|
+
this.start(this.token)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
runItem = async (item) => {
|
|
212
|
+
if (this.isClosed() || !this.addon) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
if (item.type === 'shell_integration') {
|
|
216
|
+
await item.execute()
|
|
217
|
+
// Give the freshly injected prompt a moment to finish drawing before
|
|
218
|
+
// anything is typed into it.
|
|
219
|
+
await this.waitForSettle({ usePrompt: false })
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (item.type !== 'delayed_script') {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
item.execute()
|
|
226
|
+
if (item.isLast) {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
await this.sleep(item.delay)
|
|
230
|
+
// Never type the next script into a shell that is still busy. Bounded, so
|
|
231
|
+
// a script that never returns can not block the rest of the queue.
|
|
232
|
+
await this.waitForSettle()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Wait until the terminal stops producing output before typing the next
|
|
237
|
+
* queued item. Resolves on whichever comes first:
|
|
238
|
+
* - a fresh prompt (OSC 633 ;A), when shell integration is active - the
|
|
239
|
+
* exact "previous command finished" signal
|
|
240
|
+
* - no output for `idleMs`
|
|
241
|
+
* Always resolves; `timeoutMs` caps it.
|
|
242
|
+
*/
|
|
243
|
+
waitForSettle = ({ timeoutMs = settleTimeoutMs, idleMs = settleIdleMs, usePrompt = true } = {}) => {
|
|
244
|
+
const addon = this.addon
|
|
245
|
+
if (this.isClosed() || !addon || !addon.waitForOutputIdle || addon.disposed) {
|
|
246
|
+
return Promise.resolve(false)
|
|
247
|
+
}
|
|
248
|
+
const idle = addon.waitForOutputIdle({ idleMs, timeoutMs })
|
|
249
|
+
if (!usePrompt || !this.cmdAddon || !this.cmdAddon.onPrompt) {
|
|
250
|
+
return idle
|
|
251
|
+
}
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
let done = false
|
|
254
|
+
let unsubscribe = null
|
|
255
|
+
const finish = (result) => {
|
|
256
|
+
if (done) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
done = true
|
|
260
|
+
clearTimeout(capTimer)
|
|
261
|
+
if (unsubscribe) {
|
|
262
|
+
unsubscribe()
|
|
263
|
+
}
|
|
264
|
+
resolve(result)
|
|
265
|
+
}
|
|
266
|
+
unsubscribe = this.cmdAddon.onPrompt(() => finish('prompt'))
|
|
267
|
+
const capTimer = setTimeout(() => finish(false), timeoutMs + 200)
|
|
268
|
+
this.timers.add(capTimer)
|
|
269
|
+
idle.then(quiet => finish(quiet ? 'idle' : false))
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---- shell integration ----
|
|
274
|
+
|
|
275
|
+
canInjectShellIntegration = () => {
|
|
276
|
+
const { config, sftpPathFollowSsh } = this.term.props
|
|
277
|
+
return (
|
|
278
|
+
config.showCmdSuggestions ||
|
|
279
|
+
sftpPathFollowSsh ||
|
|
280
|
+
(config.restoreTerminalSessionOnReload && this.isSsh())
|
|
281
|
+
) &&
|
|
282
|
+
(
|
|
283
|
+
this.isSsh() ||
|
|
284
|
+
(this.isLocal() && !isWin)
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Inject shell integration commands from client-side.
|
|
290
|
+
* Replaces the server-side `source xxx.xxx` approach; output suppression
|
|
291
|
+
* hides the injection command. Resolves when injection is complete - or
|
|
292
|
+
* when it is given up on, never hangs.
|
|
293
|
+
*/
|
|
294
|
+
injectShellIntegration = async () => {
|
|
295
|
+
if (this.shellInjected) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const addon = this.addon
|
|
300
|
+
if (!addon) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let shellType
|
|
305
|
+
if (this.isLocal()) {
|
|
306
|
+
const { config } = this.term.props
|
|
307
|
+
const localShell = isMac ? config.execMac : config.execLinux
|
|
308
|
+
shellType = detectShellType(localShell)
|
|
309
|
+
} else if (this.isSsh()) {
|
|
310
|
+
shellType = await Promise.race([
|
|
311
|
+
this.detectRemoteShell(this.term.pid),
|
|
312
|
+
new Promise(resolve => {
|
|
313
|
+
setTimeout(() => resolve('sh'), remoteShellDetectTimeoutMs)
|
|
314
|
+
})
|
|
315
|
+
])
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.shellType = shellType
|
|
319
|
+
|
|
320
|
+
// Don't inject for sh type shells unless sftpPathFollowSsh is true
|
|
321
|
+
if (shellType === 'sh' && !this.term.props.sftpPathFollowSsh) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// The connection (or the whole terminal) can go away while
|
|
326
|
+
// detectRemoteShell is in flight. Bail instead of registering callbacks
|
|
327
|
+
// on a dead socket - that is what used to leave the queue, and every run
|
|
328
|
+
// script behind it, waiting forever.
|
|
329
|
+
if (this.isClosed() || this.addon !== addon || addon.disposed) {
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const integrationCmd = getShellIntegrationCommand(shellType)
|
|
334
|
+
const suppressionTimeout = this.isSsh() ? 5000 : 3000
|
|
335
|
+
|
|
336
|
+
return new Promise((resolve) => {
|
|
337
|
+
let done = false
|
|
338
|
+
const finish = () => {
|
|
339
|
+
if (done) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
done = true
|
|
343
|
+
this.timers.delete(safetyTimer)
|
|
344
|
+
clearTimeout(safetyTimer)
|
|
345
|
+
resolve()
|
|
346
|
+
}
|
|
347
|
+
// Hard ceiling for the whole injection. onInitialData only fires once
|
|
348
|
+
// the shell produces output; when it never does (dead pty, restricted
|
|
349
|
+
// shell, zmodem in progress) the promise - and the run scripts queued
|
|
350
|
+
// behind it - would otherwise never be released.
|
|
351
|
+
const safetyTimer = setTimeout(() => {
|
|
352
|
+
if (addon.outputSuppressed) {
|
|
353
|
+
// Unfreeze the screen rather than keep hiding output.
|
|
354
|
+
addon.stopOutputSuppression(false)
|
|
355
|
+
}
|
|
356
|
+
finish()
|
|
357
|
+
}, suppressionTimeout + 2000)
|
|
358
|
+
this.timers.add(safetyTimer)
|
|
359
|
+
|
|
360
|
+
// Wait for initial data (prompt/banner) to arrive before injecting
|
|
361
|
+
addon.onInitialData(() => {
|
|
362
|
+
if (done || this.addon !== addon || addon.disposed) {
|
|
363
|
+
return finish()
|
|
364
|
+
}
|
|
365
|
+
// Start suppressing output before sending the integration command.
|
|
366
|
+
// This hides the command and its output until OSC 633 is detected.
|
|
367
|
+
addon.startOutputSuppression(suppressionTimeout, () => {
|
|
368
|
+
if (!addon.disposed) {
|
|
369
|
+
this.shellInjected = true
|
|
370
|
+
}
|
|
371
|
+
finish()
|
|
372
|
+
})
|
|
373
|
+
addon._sendData(integrationCmd)
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export default StartupQueue
|
|
@@ -8,6 +8,9 @@ import TermInteractiveUI from './terminal-interactive-ui'
|
|
|
8
8
|
import ExternalLink from '../common/external-link.jsx'
|
|
9
9
|
import { notification } from '../common/notification'
|
|
10
10
|
|
|
11
|
+
// x11 warnings already shown, keyed by message text
|
|
12
|
+
const shownX11Warnings = new Set()
|
|
13
|
+
|
|
11
14
|
export default function TermInteractive () {
|
|
12
15
|
const [current, setCurrent] = useState(null)
|
|
13
16
|
const queueRef = useRef([])
|
|
@@ -43,6 +46,28 @@ export default function TermInteractive () {
|
|
|
43
46
|
})
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function notifyX11Warning (data) {
|
|
50
|
+
// one notification per distinct message, several tabs may report it
|
|
51
|
+
if (shownX11Warnings.has(data.message)) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
shownX11Warnings.add(data.message)
|
|
55
|
+
notification.warning({
|
|
56
|
+
message: 'X11 forwarding',
|
|
57
|
+
description: (
|
|
58
|
+
<div className='pd0 mg0'>
|
|
59
|
+
<div>{data.message}</div>
|
|
60
|
+
{
|
|
61
|
+
data.url
|
|
62
|
+
? <ExternalLink to={data.url}>{data.url}</ExternalLink>
|
|
63
|
+
: null
|
|
64
|
+
}
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
duration: 30
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
function processNext () {
|
|
47
72
|
const next = queueRef.current.shift()
|
|
48
73
|
if (next) {
|
|
@@ -82,6 +107,13 @@ export default function TermInteractive () {
|
|
|
82
107
|
e.data.includes('ssh-proxy-command-message')
|
|
83
108
|
) {
|
|
84
109
|
notifyProxyCommandMessage(JSON.parse(e.data))
|
|
110
|
+
} else if (
|
|
111
|
+
e &&
|
|
112
|
+
e.data &&
|
|
113
|
+
typeof e.data === 'string' &&
|
|
114
|
+
e.data.includes('x11-warning')
|
|
115
|
+
) {
|
|
116
|
+
notifyX11Warning(JSON.parse(e.data))
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
|