@electerm/electerm-react 2.10.26 → 2.11.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.
Files changed (35) hide show
  1. package/client/common/default-setting.js +1 -0
  2. package/client/common/has-active-input.js +1 -1
  3. package/client/common/parse-quick-connect.js +438 -0
  4. package/client/components/ai/ai-chat-history-item.jsx +1 -1
  5. package/client/components/ai/ai-config.jsx +1 -1
  6. package/client/components/bookmark-form/ai-bookmark-form.jsx +40 -1
  7. package/client/components/bookmark-form/bookmark-form.styl +21 -0
  8. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +141 -0
  9. package/client/components/bookmark-form/tree-select.jsx +72 -23
  10. package/client/components/common/input-context-menu.jsx +13 -5
  11. package/client/components/main/main.jsx +3 -0
  12. package/client/components/rdp/rdp-session.jsx +4 -8
  13. package/client/components/rdp/rdp.styl +15 -0
  14. package/client/components/setting-panel/bookmark-tree-list.jsx +1 -0
  15. package/client/components/setting-panel/list.styl +10 -4
  16. package/client/components/setting-panel/setting-terminal.jsx +3 -2
  17. package/client/components/sftp/list-table-ui.jsx +29 -2
  18. package/client/components/sftp/paged-list.jsx +3 -8
  19. package/client/components/sidebar/history-item.jsx +13 -1
  20. package/client/components/sidebar/index.jsx +13 -10
  21. package/client/components/spice/spice.styl +7 -0
  22. package/client/components/tabs/add-btn-menu.jsx +2 -0
  23. package/client/components/tabs/no-session.jsx +25 -9
  24. package/client/components/tabs/no-session.styl +21 -0
  25. package/client/components/tabs/quick-connect.jsx +130 -0
  26. package/client/components/tabs/tabs.styl +1 -19
  27. package/client/components/terminal/highlight-addon.js +11 -0
  28. package/client/components/terminal/terminal-interactive.jsx +1 -0
  29. package/client/components/terminal/terminal.jsx +16 -1
  30. package/client/components/terminal/trzsz-client.js +6 -0
  31. package/client/components/terminal/xterm-loader.js +11 -0
  32. package/client/components/terminal-info/run-cmd.jsx +2 -1
  33. package/client/store/load-data.js +4 -0
  34. package/client/store/sync.js +1 -0
  35. package/package.json +1 -1
@@ -29,6 +29,7 @@ export default {
29
29
  terminalBackgroundTextColor: '#ffffff',
30
30
  terminalBackgroundTextFontFamily: 'Maple Mono',
31
31
  rendererType: 'canvas',
32
+ enableSixel: true,
32
33
  terminalType: 'xterm-256color',
33
34
  keepaliveCountMax: 10,
34
35
  saveTerminalLogToFile: false,
@@ -5,6 +5,6 @@ export default function hasActiveInput (className = 'ant-input-search') {
5
5
  activeElement.tagName === 'TEXTAREA'
6
6
  )
7
7
  const hasClass = className ? activeElement.classList.contains(className) : true
8
- const hasInputDropDown = document.querySelector('.ant-dropdown:not(.ant-dropdown-hidden)')
8
+ const hasInputDropDown = document.querySelector('.input-context-menu')
9
9
  return (isInput && hasClass) || hasInputDropDown
10
10
  }
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Quick Connect String Parser
3
+ * Parses connection strings according to temp/quick-connect.wiki.md specification
4
+ *
5
+ * Supported Protocols: ssh, telnet, vnc, rdp, spice, serial, ftp, http, https, electerm
6
+ *
7
+ * Basic Format:
8
+ * protocol://[username:password@]host[:port]?anyQueryParam=anyValue&opts={"key":"value"}
9
+ *
10
+ * electerm:// Format (default type is ssh):
11
+ * electerm://[username:password@]host[:port]?type=ssh&anyQueryParam=anyValue
12
+ * electerm://host?type=telnet
13
+ * electerm://user@host:22?type=vnc
14
+ *
15
+ * Shortcut Format (SSH default):
16
+ * user@host
17
+ * user@host:22
18
+ * 192.168.1.100
19
+ * 192.168.1.100:22
20
+ */
21
+
22
+ const SUPPORTED_PROTOCOLS = ['ssh', 'telnet', 'vnc', 'rdp', 'spice', 'serial', 'ftp', 'http', 'https', 'electerm']
23
+
24
+ /**
25
+ * Default ports for each protocol
26
+ */
27
+ const DEFAULT_PORTS = {
28
+ ssh: 22,
29
+ telnet: 23,
30
+ vnc: 5900,
31
+ rdp: 3389,
32
+ spice: 5900,
33
+ serial: undefined, // Serial doesn't have a default port
34
+ ftp: 21,
35
+ http: 80,
36
+ https: 443,
37
+ electerm: 22 // electerm defaults to SSH port
38
+ }
39
+
40
+ /**
41
+ * Default values for each protocol type
42
+ * Based on src/client/components/bookmark-form/config
43
+ */
44
+ const TYPE_DEFAULT_VALUES = {
45
+ ssh: {
46
+ port: 22,
47
+ enableSsh: true,
48
+ enableSftp: true,
49
+ useSshAgent: true,
50
+ term: 'xterm-256color',
51
+ encode: 'utf-8',
52
+ envLang: 'en_US.UTF-8'
53
+ },
54
+ telnet: {
55
+ port: 23
56
+ },
57
+ vnc: {
58
+ port: 5900,
59
+ viewOnly: false,
60
+ clipViewport: false,
61
+ scaleViewport: true,
62
+ qualityLevel: 3,
63
+ compressionLevel: 1,
64
+ shared: true
65
+ },
66
+ rdp: {
67
+ port: 3389
68
+ },
69
+ spice: {
70
+ port: 5900,
71
+ viewOnly: false,
72
+ scaleViewport: true
73
+ },
74
+ serial: {
75
+ baudRate: 9600,
76
+ dataBits: 8,
77
+ lock: true,
78
+ stopBits: 1,
79
+ parity: 'none',
80
+ rtscts: false,
81
+ xon: false,
82
+ xoff: false,
83
+ xany: false,
84
+ term: 'xterm-256color',
85
+ displayRaw: false
86
+ },
87
+ ftp: {
88
+ port: 21,
89
+ encode: 'utf-8',
90
+ secure: false
91
+ },
92
+ web: {},
93
+ local: {}
94
+ }
95
+
96
+ /**
97
+ * Parse a quick connect string into connection options
98
+ * @param {string} str - The connection string
99
+ * @returns {object|null} - Parsed options or null if invalid
100
+ */
101
+ function parseQuickConnect (str) {
102
+ if (!str || typeof str !== 'string') {
103
+ return null
104
+ }
105
+
106
+ const trimmed = str.trim()
107
+ if (!trimmed) {
108
+ return null
109
+ }
110
+
111
+ try {
112
+ // Detect protocol
113
+ const protocolMatch = trimmed.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i)
114
+
115
+ let protocol = ''
116
+ let connectionString = ''
117
+ let originalProtocol = 'ssh'
118
+
119
+ if (protocolMatch) {
120
+ originalProtocol = protocolMatch[1].toLowerCase()
121
+ protocol = originalProtocol
122
+ // Normalize http/https to web
123
+ if (protocol === 'http' || protocol === 'https') {
124
+ protocol = 'web'
125
+ }
126
+ connectionString = trimmed.slice(protocolMatch[0].length)
127
+ } else {
128
+ // Shortcut format - default to SSH
129
+ // Match user@host or user@host:port or just host or host:port
130
+ // Use last colon to determine port for host:port format
131
+ if (/^[\w.-]+@[\w.-]+/.test(trimmed)) {
132
+ // user@host or user@host:port
133
+ protocol = 'ssh'
134
+ connectionString = trimmed
135
+ } else if (/^[\w.-]+:.*:[\d]+$/.test(trimmed)) {
136
+ // host:port format with colons in hostname (e.g., localhost:23344, zxd:localhost:23344)
137
+ // Check if the last colon is followed by digits (port number)
138
+ protocol = 'ssh'
139
+ connectionString = trimmed
140
+ } else if (/^[\w.-]+:[\d]+$/.test(trimmed)) {
141
+ // host:port (no username, simple format like host:22)
142
+ protocol = 'ssh'
143
+ connectionString = trimmed
144
+ } else if (/^[\w.-]+$/.test(trimmed)) {
145
+ // just host
146
+ protocol = 'ssh'
147
+ connectionString = trimmed
148
+ } else {
149
+ return null
150
+ }
151
+ }
152
+
153
+ if (!SUPPORTED_PROTOCOLS.includes(protocol) && protocol !== 'web') {
154
+ return null
155
+ }
156
+
157
+ // Extract opts from the connection string before parsing
158
+ let optsStr = ''
159
+ const optsMatch = connectionString.match(/[?&]opts=('|")(.+?)('|")$/)
160
+ if (!optsMatch) {
161
+ // Try without quotes
162
+ const optsMatchNoQuote = connectionString.match(/[?&]opts=(\{.+?\})$/)
163
+ if (optsMatchNoQuote) {
164
+ optsStr = optsMatchNoQuote[1]
165
+ connectionString = connectionString.slice(0, optsMatchNoQuote.index)
166
+ }
167
+ } else {
168
+ optsStr = optsMatch[2]
169
+ connectionString = connectionString.slice(0, optsMatch.index)
170
+ }
171
+
172
+ // Extract query string for web type and electerm type
173
+ let queryStr = ''
174
+ const queryMatch = connectionString.match(/\?(.+)$/)
175
+ if (queryMatch) {
176
+ queryStr = queryMatch[1]
177
+ connectionString = connectionString.slice(0, queryMatch.index)
178
+ }
179
+
180
+ // Parse username:password@host:port
181
+ // First, check if there's an @ for auth
182
+ let username = ''
183
+ let password = ''
184
+ let hostOrPath = ''
185
+ let port = ''
186
+
187
+ const atIndex = connectionString.indexOf('@')
188
+ if (atIndex !== -1) {
189
+ // Has auth
190
+ const authPart = connectionString.slice(0, atIndex)
191
+ const hostPart = connectionString.slice(atIndex + 1)
192
+ const colonIndex = authPart.indexOf(':')
193
+ if (colonIndex !== -1) {
194
+ username = authPart.slice(0, colonIndex)
195
+ password = authPart.slice(colonIndex + 1)
196
+ } else {
197
+ username = authPart
198
+ }
199
+ // Parse host:port from hostPart
200
+ const hostColonIndex = hostPart.lastIndexOf(':')
201
+ if (hostColonIndex !== -1) {
202
+ hostOrPath = hostPart.slice(0, hostColonIndex)
203
+ port = hostPart.slice(hostColonIndex + 1)
204
+ } else {
205
+ hostOrPath = hostPart
206
+ }
207
+ } else {
208
+ // No @ sign - check for special case: protocol://password:host (e.g., spice://password:host)
209
+ // This only applies to spice protocol
210
+ if (protocol === 'spice') {
211
+ // Count colons in the connection string
212
+ const colonCount = (connectionString.match(/:/g) || []).length
213
+
214
+ if (colonCount >= 2) {
215
+ // Multiple colons - could be password:host:port or host:port with IP
216
+ // Use lastIndexOf for port, then check if first part is password or IP
217
+ const lastColonIndex = connectionString.lastIndexOf(':')
218
+ const portCandidate = connectionString.slice(lastColonIndex + 1)
219
+
220
+ if (/^\d+$/.test(portCandidate)) {
221
+ // Last part is a port number
222
+ const hostPortPart = connectionString.slice(0, lastColonIndex)
223
+ const secondLastColonIndex = hostPortPart.lastIndexOf(':')
224
+
225
+ if (secondLastColonIndex !== -1) {
226
+ // There's another colon - first part could be password
227
+ const potentialPassword = hostPortPart.slice(0, secondLastColonIndex)
228
+ const hostPart = hostPortPart.slice(secondLastColonIndex + 1)
229
+
230
+ // Check if potentialPassword is NOT an IP/hostname
231
+ // An IP/hostname should contain dots, a password typically doesn't
232
+ // Also check it's not a simple number (port)
233
+ const isIPorHostname = (potentialPassword.includes('.') || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(potentialPassword))
234
+
235
+ if (isIPorHostname) {
236
+ // It's IP, no password
237
+ hostOrPath = hostPortPart
238
+ port = portCandidate
239
+ } else {
240
+ // It's password
241
+ password = potentialPassword
242
+ hostOrPath = hostPart
243
+ port = portCandidate
244
+ }
245
+ } else {
246
+ // Only one colon before the port - it's host:port
247
+ hostOrPath = hostPortPart
248
+ port = portCandidate
249
+ }
250
+ } else {
251
+ // Last part is not a port
252
+ hostOrPath = connectionString
253
+ }
254
+ } else if (colonCount === 1) {
255
+ // Single colon - could be host:port or just a word with colon
256
+ const colonIndex = connectionString.indexOf(':')
257
+ const firstPart = connectionString.slice(0, colonIndex)
258
+ const secondPart = connectionString.slice(colonIndex + 1)
259
+
260
+ // Check if first part is an IP
261
+ const isIP = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(firstPart)
262
+
263
+ if (isIP) {
264
+ // IP with port
265
+ hostOrPath = firstPart
266
+ port = secondPart
267
+ } else if (/^\d+$/.test(secondPart)) {
268
+ // Just a word with port number
269
+ // This is likely password (for spice) or host without port
270
+ // For spice, treat first part as host (not password) since there's only one colon
271
+ hostOrPath = firstPart
272
+ port = secondPart
273
+ } else {
274
+ // host or hostname
275
+ hostOrPath = connectionString
276
+ }
277
+ } else {
278
+ // No colon - just host
279
+ hostOrPath = connectionString
280
+ }
281
+ } else {
282
+ // Normal case - just host:port
283
+ const hostColonIndex = connectionString.lastIndexOf(':')
284
+ if (hostColonIndex !== -1) {
285
+ // Make sure it's a port number (all digits)
286
+ const potentialPort = connectionString.slice(hostColonIndex + 1)
287
+ if (/^\d+$/.test(potentialPort)) {
288
+ hostOrPath = connectionString.slice(0, hostColonIndex)
289
+ port = potentialPort
290
+ } else {
291
+ hostOrPath = connectionString
292
+ }
293
+ } else {
294
+ hostOrPath = connectionString
295
+ }
296
+ }
297
+ }
298
+
299
+ if (!hostOrPath) {
300
+ return null
301
+ }
302
+
303
+ // Build base options
304
+ // For electerm protocol, we need to handle the type from query params
305
+ let finalProtocol = protocol
306
+ let webProtocol = originalProtocol // Store original for web type
307
+
308
+ // Handle electerm:// protocol - extract type from query params, default to ssh
309
+ if (originalProtocol === 'electerm') {
310
+ // Parse query params to get type
311
+ const params = new URLSearchParams(queryStr)
312
+ finalProtocol = params.get('type') || params.get('tp') || 'ssh'
313
+
314
+ // Validate the type is supported
315
+ if (!SUPPORTED_PROTOCOLS.includes(finalProtocol) && finalProtocol !== 'web') {
316
+ return null
317
+ }
318
+
319
+ // Normalize http/https to web
320
+ if (finalProtocol === 'http' || finalProtocol === 'https') {
321
+ webProtocol = finalProtocol // Store the http/https before normalizing
322
+ finalProtocol = 'web'
323
+ // Remove type/tp from query string for web URL construction
324
+ params.delete('type')
325
+ params.delete('tp')
326
+ queryStr = params.toString()
327
+ }
328
+ } else {
329
+ webProtocol = originalProtocol
330
+ }
331
+
332
+ const opts = {
333
+ type: finalProtocol
334
+ }
335
+
336
+ // Handle different protocol types
337
+ if (finalProtocol === 'serial') {
338
+ // Serial: path is the port
339
+ opts.path = hostOrPath
340
+ if (port) {
341
+ opts.baudRate = parseInt(port, 10)
342
+ }
343
+ // Parse query params for serial (like baudRate)
344
+ if (queryStr) {
345
+ const params = new URLSearchParams(queryStr)
346
+ if (params.has('baudRate')) {
347
+ opts.baudRate = parseInt(params.get('baudRate'), 10)
348
+ }
349
+ }
350
+ } else if (finalProtocol === 'web') {
351
+ // Web: construct URL from protocol + host + port + query
352
+ let url = `${webProtocol}://${hostOrPath}`
353
+ if (port) {
354
+ // Add non-standard port to URL
355
+ const defaultPort = originalProtocol === 'https' ? 443 : 80
356
+ if (parseInt(port, 10) !== defaultPort) {
357
+ url += `:${port}`
358
+ }
359
+ }
360
+ // Add query string if present
361
+ if (queryStr) {
362
+ const separator = url.includes('?') ? '&' : '?'
363
+ url += `${separator}${queryStr}`
364
+ }
365
+ opts.url = url
366
+ } else {
367
+ // SSH, Telnet, VNC, RDP, Spice, FTP
368
+ opts.host = hostOrPath
369
+ if (port) {
370
+ opts.port = parseInt(port, 10)
371
+ }
372
+ if (username !== undefined && username !== '') {
373
+ opts.username = username
374
+ }
375
+ if (password !== undefined && password !== '') {
376
+ opts.password = password
377
+ }
378
+ // Parse query params for other protocols (like title)
379
+ if (queryStr) {
380
+ const params = new URLSearchParams(queryStr)
381
+ if (params.has('title')) {
382
+ opts.title = params.get('title')
383
+ }
384
+ }
385
+ }
386
+
387
+ // Parse opts JSON to extend params
388
+ if (optsStr) {
389
+ try {
390
+ const extraOpts = JSON.parse(optsStr)
391
+ Object.assign(opts, extraOpts)
392
+ } catch (err) {
393
+ console.error('Failed to parse opts:', err)
394
+ }
395
+ }
396
+
397
+ // Apply default values for the protocol type
398
+ const typeDefaults = TYPE_DEFAULT_VALUES[finalProtocol]
399
+ if (typeDefaults) {
400
+ Object.keys(typeDefaults).forEach(key => {
401
+ // Only apply default if not already set
402
+ if (opts[key] === undefined) {
403
+ opts[key] = typeDefaults[key]
404
+ }
405
+ })
406
+ }
407
+
408
+ return opts
409
+ } catch (error) {
410
+ console.error('Error parsing quick connect string:', error)
411
+ return null
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Get default port for a protocol
417
+ * @param {string} protocol - The protocol name
418
+ * @returns {number|undefined} - Default port or undefined
419
+ */
420
+ function getDefaultPort (protocol) {
421
+ return DEFAULT_PORTS[protocol]
422
+ }
423
+
424
+ /**
425
+ * Get list of supported protocols
426
+ * @returns {string[]} - List of supported protocols
427
+ */
428
+ function getSupportedProtocols () {
429
+ return [...SUPPORTED_PROTOCOLS]
430
+ }
431
+
432
+ export {
433
+ parseQuickConnect,
434
+ getDefaultPort,
435
+ getSupportedProtocols,
436
+ SUPPORTED_PROTOCOLS,
437
+ DEFAULT_PORTS
438
+ }
@@ -25,7 +25,7 @@ export default function AIChatHistoryItem ({ item }) {
25
25
  }
26
26
 
27
27
  const alertProps = {
28
- message: (
28
+ title: (
29
29
  <>
30
30
  <span className='pointer mg1r' onClick={toggleOutput}>
31
31
  {showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
@@ -100,7 +100,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
100
100
  return (
101
101
  <>
102
102
  <Alert
103
- message={
103
+ title={
104
104
  <Link to={aiConfigWikiLink}>WIKI: {aiConfigWikiLink}</Link>
105
105
  }
106
106
  type='info'
@@ -11,7 +11,8 @@ import {
11
11
  EditOutlined,
12
12
  CopyOutlined,
13
13
  DownloadOutlined,
14
- EyeOutlined
14
+ EyeOutlined,
15
+ ThunderboltOutlined
15
16
  } from '@ant-design/icons'
16
17
  import SimpleEditor from '../text-editor/simple-editor'
17
18
  import { copy } from '../../common/clipboard'
@@ -24,6 +25,7 @@ import { fixBookmarkData } from './fix-bookmark-default.js'
24
25
  import generate from '../../common/id-with-stamp'
25
26
  import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
26
27
  import { getItem, setItem } from '../../common/safe-local-storage'
28
+ import newTerminal from '../../common/new-terminal'
27
29
 
28
30
  const STORAGE_KEY_DESC = 'ai_bookmark_description'
29
31
  const STORAGE_KEY_HISTORY = 'ai_bookmark_history'
@@ -166,6 +168,30 @@ export default function AIBookmarkForm (props) {
166
168
  setShowConfirm(false)
167
169
  }
168
170
 
171
+ const handleQuickConnect = () => {
172
+ const parsed = getGeneratedData()
173
+ if (!parsed.length || !parsed[0]) {
174
+ return
175
+ }
176
+ const bm = parsed[0]
177
+ // Create a new tab with quick connect options
178
+ const { store } = window
179
+
180
+ // Close the setting panel first
181
+ store.hideSettingModal()
182
+
183
+ const tabOptions = {
184
+ ...bm,
185
+ ...newTerminal(),
186
+ from: 'quickConnect'
187
+ }
188
+
189
+ store.addTab(tabOptions)
190
+ setShowConfirm(false)
191
+ setDescription('')
192
+ message.success(e('Done'))
193
+ }
194
+
169
195
  const handleCancel = () => {
170
196
  if (onCancel) {
171
197
  onCancel()
@@ -245,6 +271,18 @@ export default function AIBookmarkForm (props) {
245
271
  loading
246
272
  }
247
273
 
274
+ function renderQuickConnectBtn () {
275
+ const parsed = getGeneratedData()
276
+ if (!parsed.length || !parsed[0] || parsed.length > 1) {
277
+ return null
278
+ }
279
+ return (
280
+ <Button onClick={handleQuickConnect} icon={<ThunderboltOutlined />}>
281
+ {e('quickConnect')}
282
+ </Button>
283
+ )
284
+ }
285
+
248
286
  const modalProps = {
249
287
  title: e('confirmBookmarkData') || 'Confirm Bookmark Data',
250
288
  open: showConfirm,
@@ -254,6 +292,7 @@ export default function AIBookmarkForm (props) {
254
292
  <Button onClick={handleCancelConfirm}>
255
293
  <CloseOutlined /> {e('cancel')}
256
294
  </Button>
295
+ {renderQuickConnectBtn()}
257
296
  <Button type='primary' onClick={handleConfirm}>
258
297
  <CheckOutlined /> {e('confirm')}
259
298
  </Button>
@@ -15,3 +15,24 @@
15
15
  display inline-block
16
16
  .number-input
17
17
  min-width 210px
18
+
19
+ .tree-select-wrapper
20
+ display flex
21
+ flex-direction column
22
+ height 100%
23
+ overflow hidden
24
+
25
+ .tree-select-header
26
+ flex-shrink 0
27
+
28
+ .tree-select-content
29
+ flex 1
30
+ overflow auto
31
+ margin-top 8px
32
+
33
+ .bookmark-json-preview
34
+ padding 12px
35
+ max-height 300px
36
+ overflow auto
37
+ font-size 12px
38
+ font-family monospace