@electerm/electerm-react 5.3.26 → 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 +20 -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/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 +55 -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/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +7 -2
- 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/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/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 +6 -3
- 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/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 +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.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
|
@@ -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
|
|