@electerm/electerm-react 2.7.8 → 2.8.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 (29) hide show
  1. package/client/common/pre.js +38 -11
  2. package/client/components/bookmark-form/config/rdp.js +1 -0
  3. package/client/components/bookmark-form/config/vnc.js +5 -0
  4. package/client/components/common/remote-float-control.jsx +79 -0
  5. package/client/components/common/remote-float-control.styl +28 -0
  6. package/client/components/layout/layout.jsx +2 -1
  7. package/client/components/main/main.jsx +3 -6
  8. package/client/components/main/term-fullscreen.styl +1 -10
  9. package/client/components/rdp/rdp-session.jsx +131 -17
  10. package/client/components/rdp/resolutions.js +6 -0
  11. package/client/components/session/session.jsx +4 -3
  12. package/client/components/session/session.styl +18 -5
  13. package/client/components/session/sessions.jsx +2 -1
  14. package/client/components/shortcuts/shortcut-control.jsx +5 -3
  15. package/client/components/shortcuts/shortcut-handler.js +4 -2
  16. package/client/components/terminal/attach-addon-custom.js +13 -0
  17. package/client/components/terminal/event-emitter.js +27 -0
  18. package/client/components/terminal/terminal.jsx +10 -297
  19. package/client/components/terminal/zmodem-client.js +385 -0
  20. package/client/components/terminal-info/data-cols-parser.jsx +3 -2
  21. package/client/components/terminal-info/network.jsx +3 -2
  22. package/client/components/vnc/vnc-session.jsx +398 -62
  23. package/client/css/basic.styl +3 -0
  24. package/client/store/event.js +2 -2
  25. package/client/store/init-state.js +1 -1
  26. package/package.json +1 -1
  27. package/client/common/byte-format.js +0 -14
  28. package/client/components/main/term-fullscreen-control.jsx +0 -21
  29. package/client/components/terminal/xterm-zmodem.js +0 -55
@@ -89,26 +89,47 @@ const fs = {
89
89
  },
90
90
  open: (...args) => {
91
91
  const cb = args.pop()
92
- window.fs.openCustom(...args)
92
+ if (window.et.isWebApp) {
93
+ window.fs.openCustom(...args)
94
+ .then((data) => cb(undefined, data))
95
+ .catch((err) => cb(err))
96
+ return
97
+ }
98
+ runGlobalAsync('fsOpen', ...args)
93
99
  .then((data) => cb(undefined, data))
94
100
  .catch((err) => cb(err))
95
101
  },
96
102
  read: (p1, arr, ...args) => {
97
103
  const cb = args.pop()
98
- window.fs.readCustom(
99
- p1,
100
- arr.length,
101
- ...args
102
- )
104
+ if (window.et.isWebApp) {
105
+ window.fs.readCustom(
106
+ p1,
107
+ arr.length,
108
+ ...args
109
+ )
110
+ .then((data) => {
111
+ const { n, newArr } = data
112
+ const newArr1 = decodeBase64String(newArr)
113
+ cb(undefined, n, newArr1)
114
+ })
115
+ .catch(err => cb(err))
116
+ return
117
+ }
118
+ runGlobalAsync('fsRead', p1, arr.length, ...args)
103
119
  .then((data) => {
104
- const { n, newArr } = data
105
- const newArr1 = decodeBase64String(newArr)
106
- cb(undefined, n, newArr1)
120
+ const { n, buffer } = data
121
+ cb(undefined, n, buffer)
107
122
  })
108
123
  .catch(err => cb(err))
109
124
  },
110
125
  close: (fd, cb) => {
111
- window.fs.closeCustom(fd)
126
+ if (window.et.isWebApp) {
127
+ window.fs.closeCustom(fd)
128
+ .then((data) => cb(undefined, data))
129
+ .catch((err) => cb(err))
130
+ return
131
+ }
132
+ runGlobalAsync('fsClose', fd)
112
133
  .then((data) => cb(undefined, data))
113
134
  .catch((err) => cb(err))
114
135
  },
@@ -124,7 +145,13 @@ const fs = {
124
145
  .catch((err) => cb(err))
125
146
  },
126
147
  write: (p1, buf, cb) => {
127
- window.fs.writeCustom(p1, encodeUint8Array(buf))
148
+ if (window.et.isWebApp) {
149
+ window.fs.writeCustom(p1, encodeUint8Array(buf))
150
+ .then((data) => cb(undefined, data))
151
+ .catch((err) => cb(err))
152
+ return
153
+ }
154
+ runGlobalAsync('fsWrite', p1, buf)
128
155
  .then((data) => cb(undefined, data))
129
156
  .catch((err) => cb(err))
130
157
  },
@@ -31,6 +31,7 @@ const rdpConfig = {
31
31
  { ...commonFields.password, rules: [{ required: true, message: e('password') + ' required' }] },
32
32
  commonFields.description,
33
33
  { type: 'input', name: 'domain', label: () => e('domain') },
34
+ commonFields.proxy,
34
35
  commonFields.type
35
36
  ]
36
37
  }
@@ -15,6 +15,9 @@ const vncConfig = {
15
15
  viewOnly: false,
16
16
  clipViewport: true,
17
17
  scaleViewport: true,
18
+ qualityLevel: 3, // 0-9, lower = faster performance, default 6
19
+ compressionLevel: 1, // 0-9, lower = faster performance, default 2
20
+ shared: true,
18
21
  connectionHoppings: [],
19
22
  ...getAuthTypeDefault(props)
20
23
  })
@@ -33,6 +36,8 @@ const vncConfig = {
33
36
  { type: 'switch', name: 'viewOnly', label: () => e('viewOnly'), valuePropName: 'checked' },
34
37
  { type: 'switch', name: 'clipViewport', label: () => e('clipViewport'), valuePropName: 'checked' },
35
38
  { type: 'switch', name: 'scaleViewport', label: () => e('scaleViewport'), valuePropName: 'checked' },
39
+ { type: 'number', name: 'qualityLevel', label: () => e('qualityLevel') + ' (0-9)', min: 0, max: 9, step: 1 },
40
+ { type: 'number', name: 'compressionLevel', label: () => e('compressionLevel') + ' (0-9)', min: 0, max: 9, step: 1 },
36
41
  { type: 'profileItem', name: '__profile__', label: '', profileFilter: d => !isEmpty(d.vnc) },
37
42
  commonFields.username,
38
43
  commonFields.password,
@@ -0,0 +1,79 @@
1
+ import React from 'react'
2
+ import {
3
+ FullscreenExitOutlined,
4
+ AppstoreOutlined,
5
+ DesktopOutlined,
6
+ MoreOutlined,
7
+ DownOutlined
8
+ } from '@ant-design/icons'
9
+ import { Dropdown } from 'antd'
10
+ import './remote-float-control.styl'
11
+
12
+ const e = window.translate
13
+
14
+ export default function RemoteFloatControl (props) {
15
+ const {
16
+ isFullScreen,
17
+ onSendCtrlAltDel,
18
+ screens = [],
19
+ onSelectScreen,
20
+ currentScreen,
21
+ fixedPosition = true,
22
+ showExitFullscreen = true,
23
+ className = ''
24
+ } = props
25
+
26
+ if (fixedPosition && !isFullScreen) return null
27
+
28
+ function onExitFullScreen () {
29
+ window.store.toggleSessFullscreen(false)
30
+ }
31
+
32
+ const items = []
33
+
34
+ if (showExitFullscreen && isFullScreen) {
35
+ items.push({
36
+ key: 'exit-fullscreen',
37
+ label: e('exitFullscreen') || 'Exit Fullscreen',
38
+ icon: <FullscreenExitOutlined />,
39
+ onClick: onExitFullScreen
40
+ })
41
+ }
42
+
43
+ if (onSendCtrlAltDel) {
44
+ items.push({
45
+ key: 'ctrl-alt-del',
46
+ label: 'Send Ctrl+Alt+Del',
47
+ icon: <AppstoreOutlined />,
48
+ onClick: onSendCtrlAltDel
49
+ })
50
+ }
51
+
52
+ if (screens && screens.length > 0) {
53
+ items.push({
54
+ key: 'screens',
55
+ label: 'Select Screen',
56
+ icon: <DesktopOutlined />,
57
+ children: screens.map(s => ({
58
+ key: s.id,
59
+ label: s.name,
60
+ onClick: () => onSelectScreen(s.id),
61
+ icon: currentScreen === s.id ? <DownOutlined /> : null
62
+ }))
63
+ })
64
+ }
65
+
66
+ const containerClassName = (fixedPosition ? 'remote-float-control' : 'remote-float-control-inline') + (className ? ' ' + className : '')
67
+ const buttonClassName = fixedPosition ? 'remote-float-btn' : 'remote-float-btn-inline'
68
+ const iconClassName = fixedPosition ? 'font20' : ''
69
+
70
+ return (
71
+ <div className={containerClassName}>
72
+ <Dropdown menu={{ items }} trigger={['click']}>
73
+ <div className={buttonClassName}>
74
+ <MoreOutlined className={iconClassName} />
75
+ </div>
76
+ </Dropdown>
77
+ </div>
78
+ )
79
+ }
@@ -0,0 +1,28 @@
1
+ .remote-float-control
2
+ position fixed
3
+ top 10px
4
+ right 10px
5
+ z-index 9999
6
+
7
+ .remote-float-control-inline
8
+ display inline-block
9
+ position relative
10
+
11
+ .remote-float-btn-inline
12
+ display inline
13
+ cursor pointer
14
+
15
+ .remote-float-btn
16
+ padding 8px
17
+ background rgba(0, 0, 0, 0.5)
18
+ border-radius 4px
19
+ color #fff
20
+ cursor pointer
21
+ opacity 0.8
22
+ transition opacity 0.3s
23
+ display flex
24
+ align-items center
25
+ justify-content center
26
+ &:hover
27
+ &.ant-dropdown-open
28
+ opacity 1
@@ -179,7 +179,8 @@ export default auto(function Layout (props) {
179
179
  'leftSidebarWidth',
180
180
  'pinned',
181
181
  'openedSideBar',
182
- 'config'
182
+ 'config',
183
+ 'fullscreen'
183
184
  ]),
184
185
  tabs: store.tabs,
185
186
  layout
@@ -19,7 +19,6 @@ import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
19
19
  import classnames from 'classnames'
20
20
  import ShortcutControl from '../shortcuts/shortcut-control.jsx'
21
21
  import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
22
- import TermFullscreenControl from './term-fullscreen-control'
23
22
  import TerminalInfo from '../terminal-info/terminal-info'
24
23
  import { ConfigProvider } from 'antd'
25
24
  import { NotificationContainer } from '../common/notification'
@@ -36,6 +35,7 @@ import WorkspaceSaveModal from '../tabs/workspace-save-modal'
36
35
  import { pick } from 'lodash-es'
37
36
  import deepCopy from 'json-deep-copy'
38
37
  import './wrapper.styl'
38
+ import './term-fullscreen.styl'
39
39
 
40
40
  export default auto(function Index (props) {
41
41
  useEffect(() => {
@@ -79,7 +79,7 @@ export default auto(function Index (props) {
79
79
  const {
80
80
  configLoaded,
81
81
  config,
82
- terminalFullScreen,
82
+ fullscreen,
83
83
  pinned,
84
84
  isSecondInstance,
85
85
  pinnedQuickCommandBar,
@@ -105,7 +105,7 @@ export default auto(function Index (props) {
105
105
  pinned,
106
106
  'not-win': !isWin,
107
107
  'qm-pinned': pinnedQuickCommandBar,
108
- 'term-fullscreen': terminalFullScreen,
108
+ fullscreen,
109
109
  'is-main': !isSecondInstance
110
110
  })
111
111
  const ext1 = {
@@ -243,9 +243,6 @@ export default auto(function Index (props) {
243
243
  <div {...ext1}>
244
244
  <InputContextMenu />
245
245
  <ShortcutControl config={config} />
246
- <TermFullscreenControl
247
- terminalFullScreen={terminalFullScreen}
248
- />
249
246
  <CssOverwrite
250
247
  {...confsCss}
251
248
  wsInited={wsInited}
@@ -1,4 +1,4 @@
1
- .term-fullscreen
1
+ .fullscreen
2
2
  .sidebar
3
3
  .tabs
4
4
  .terminal-footer
@@ -7,13 +7,6 @@
7
7
  .main-footer
8
8
  .session-v-info
9
9
  display none
10
- .term-fullscreen-control
11
- display block
12
- right 10px
13
- top 10px
14
- position fixed
15
- z-index 100
16
- background #2df56c
17
10
  // Hide all sessions first
18
11
  .session-wrap
19
12
  display none
@@ -37,5 +30,3 @@
37
30
  top 10px !important
38
31
  right 10px !important
39
32
  bottom 10px !important
40
- .term-fullscreen-control
41
- display none
@@ -17,6 +17,8 @@ import {
17
17
  import * as ls from '../../common/safe-local-storage'
18
18
  import scanCode from './code-scan'
19
19
  import resolutions from './resolutions'
20
+ import { readClipboardAsync } from '../../common/clipboard'
21
+ import RemoteFloatControl from '../common/remote-float-control'
20
22
 
21
23
  const { Option } = Select
22
24
 
@@ -32,7 +34,8 @@ async function loadWasmModule () {
32
34
  DesktopSize: mod.DesktopSize,
33
35
  InputTransaction: mod.InputTransaction,
34
36
  DeviceEvent: mod.DeviceEvent,
35
- Extension: mod.Extension
37
+ Extension: mod.Extension,
38
+ ClipboardData: mod.ClipboardData
36
39
  }
37
40
  await window.ironRdp.wasmInit()
38
41
  window.ironRdp.wasmSetup('info')
@@ -42,7 +45,7 @@ async function loadWasmModule () {
42
45
  export default class RdpSession extends PureComponent {
43
46
  constructor (props) {
44
47
  const id = `rdp-reso-${props.tab.host}`
45
- const resObj = ls.getItemJSON(id, resolutions[0])
48
+ const resObj = ls.getItemJSON(id, resolutions[1])
46
49
  super(props)
47
50
  this.canvasRef = createRef()
48
51
  this.state = {
@@ -82,16 +85,27 @@ export default class RdpSession extends PureComponent {
82
85
  })
83
86
  }
84
87
 
88
+ buildWsUrl = (port, type = 'rdp', extra = '') => {
89
+ const { host, tokenElecterm } = this.props.config
90
+ const { id } = this.props.tab
91
+ if (window.et.buildWsUrl) {
92
+ return window.et.buildWsUrl(
93
+ host,
94
+ port,
95
+ tokenElecterm,
96
+ id,
97
+ type,
98
+ extra
99
+ )
100
+ }
101
+ return `ws://${host}:${port}/${type}/${id}?token=${tokenElecterm}${extra}`
102
+ }
103
+
85
104
  remoteInit = async () => {
86
105
  this.setState({
87
106
  loading: true
88
107
  })
89
108
  const { config } = this.props
90
- const {
91
- host,
92
- tokenElecterm,
93
- server = ''
94
- } = config
95
109
  const { id } = this.props
96
110
  const tab = window.store.applyProfile(deepCopy(this.props.tab || {}))
97
111
  const {
@@ -127,16 +141,10 @@ export default class RdpSession extends PureComponent {
127
141
  console.debug('[RDP-CLIENT] Term created, pid=', pid, 'port=', port)
128
142
 
129
143
  // Build the WebSocket proxy address for IronRDP WASM
130
- const hs = server
131
- ? server.replace(/https?:\/\//, '')
132
- : `${host}:${port}`
133
- const pre = server.startsWith('https') ? 'wss' : 'ws'
134
144
  const { width, height } = this.state
135
145
  // IronRDP connects to the proxy address, which then proxies via RDCleanPath
136
- // The WebSocket URL includes the pid and token for auth
137
- const proxyAddress = `${pre}://${hs}/rdp/${pid}?token=${tokenElecterm}&width=${width}&height=${height}`
138
- console.debug('[RDP-CLIENT] Proxy address:', proxyAddress)
139
-
146
+ const extra = `&width=${width}&height=${height}`
147
+ const proxyAddress = this.buildWsUrl(port, 'rdp', extra)
140
148
  // Load WASM module if not already loaded
141
149
  try {
142
150
  await loadWasmModule()
@@ -171,7 +179,7 @@ export default class RdpSession extends PureComponent {
171
179
  console.debug('[RDP-CLIENT] desktopSize:', width, 'x', height)
172
180
 
173
181
  const desktopSize = new window.ironRdp.DesktopSize(width, height)
174
- const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', false)
182
+ const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', true)
175
183
 
176
184
  const builder = new window.ironRdp.SessionBuilder()
177
185
  builder.username(username)
@@ -183,6 +191,29 @@ export default class RdpSession extends PureComponent {
183
191
  builder.renderCanvas(canvas)
184
192
  builder.extension(enableCredsspExt)
185
193
 
194
+ // Clipboard callbacks
195
+ builder.remoteClipboardChangedCallback((clipboardData) => {
196
+ try {
197
+ if (clipboardData.isEmpty()) {
198
+ return
199
+ }
200
+ const items = clipboardData.items()
201
+ for (const item of items) {
202
+ if (item.mimeType() === 'text/plain') {
203
+ const text = item.value()
204
+ console.debug('[RDP-CLIENT] Received clipboard text:', text)
205
+ window.pre.writeClipboard(text)
206
+ }
207
+ }
208
+ } catch (e) {
209
+ console.error('[RDP-CLIENT] Clipboard error:', e)
210
+ }
211
+ })
212
+
213
+ builder.forceClipboardUpdateCallback(() => {
214
+ this.syncLocalToRemote()
215
+ })
216
+
186
217
  // Cursor style callback
187
218
  builder.setCursorStyleCallbackContext(canvas)
188
219
  builder.setCursorStyleCallback(function (style) {
@@ -241,6 +272,20 @@ export default class RdpSession extends PureComponent {
241
272
  return e?.message || e?.toString() || String(e)
242
273
  }
243
274
 
275
+ syncLocalToRemote = async () => {
276
+ if (!this.session) return
277
+ try {
278
+ const text = await readClipboardAsync()
279
+ if (text) {
280
+ const data = new window.ironRdp.ClipboardData()
281
+ data.addText('text/plain', text)
282
+ await this.session.onClipboardPaste(data)
283
+ }
284
+ } catch (e) {
285
+ console.error('[RDP-CLIENT] Local clipboard sync error:', e)
286
+ }
287
+ }
288
+
244
289
  onSessionEnd = () => {
245
290
  console.debug('[RDP-CLIENT] onSessionEnd called')
246
291
  this.session = null
@@ -353,6 +398,14 @@ export default class RdpSession extends PureComponent {
353
398
  }, { passive: false })
354
399
 
355
400
  canvas.addEventListener('contextmenu', (e) => e.preventDefault())
401
+
402
+ canvas.addEventListener('paste', () => {
403
+ this.syncLocalToRemote()
404
+ })
405
+
406
+ canvas.addEventListener('focus', () => {
407
+ this.syncLocalToRemote()
408
+ })
356
409
  }
357
410
 
358
411
  // Get PS/2 scancode from keyboard event code using existing code-scan module
@@ -382,7 +435,7 @@ export default class RdpSession extends PureComponent {
382
435
  getAllRes = () => {
383
436
  return [
384
437
  ...this.props.resolutions,
385
- ...resolutions
438
+ ...resolutions.slice(1)
386
439
  ]
387
440
  }
388
441
 
@@ -397,7 +450,65 @@ export default class RdpSession extends PureComponent {
397
450
  return null
398
451
  }
399
452
 
453
+ getControlProps = (options = {}) => {
454
+ const {
455
+ fixedPosition = true,
456
+ showExitFullscreen = true,
457
+ className = ''
458
+ } = options
459
+
460
+ return {
461
+ isFullScreen: this.props.fullscreen,
462
+ onSendCtrlAltDel: this.handleSendCtrlAltDel,
463
+ screens: [], // RDP doesn't have multi-screen support like VNC
464
+ currentScreen: null,
465
+ onSelectScreen: () => {}, // No-op for RDP
466
+ fixedPosition,
467
+ showExitFullscreen,
468
+ className
469
+ }
470
+ }
471
+
472
+ handleSendCtrlAltDel = () => {
473
+ if (!this.session) return
474
+ try {
475
+ // Send Ctrl+Alt+Del sequence using IronRDP
476
+ const tx = new window.ironRdp.InputTransaction()
477
+
478
+ // Ctrl key press
479
+ const ctrlScancode = 0x1D // Left Ctrl scancode
480
+ tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(ctrlScancode))
481
+
482
+ // Alt key press
483
+ const altScancode = 0x38 // Left Alt scancode
484
+ tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(altScancode))
485
+
486
+ // Del key press
487
+ const delScancode = 0x53 // Delete scancode
488
+ tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(delScancode))
489
+
490
+ // Del key release
491
+ tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(delScancode))
492
+
493
+ // Alt key release
494
+ tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(altScancode))
495
+
496
+ // Ctrl key release
497
+ tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(ctrlScancode))
498
+
499
+ this.session.applyInputs(tx)
500
+ console.log('[RDP-CLIENT] Sent Ctrl+Alt+Del')
501
+ } catch (err) {
502
+ console.error('[RDP-CLIENT] Failed to send Ctrl+Alt+Del:', err)
503
+ }
504
+ }
505
+
400
506
  renderControl = () => {
507
+ const contrlProps = this.getControlProps({
508
+ fixedPosition: false,
509
+ showExitFullscreen: false,
510
+ className: 'mg1l'
511
+ })
401
512
  const {
402
513
  id
403
514
  } = this.state
@@ -439,6 +550,7 @@ export default class RdpSession extends PureComponent {
439
550
  </div>
440
551
  <div className='fright'>
441
552
  {this.props.fullscreenIcon()}
553
+ <RemoteFloatControl {...contrlProps} />
442
554
  </div>
443
555
  </div>
444
556
  )
@@ -476,6 +588,7 @@ export default class RdpSession extends PureComponent {
476
588
  height,
477
589
  tabIndex: 0
478
590
  }
591
+ const controlProps = this.getControlProps()
479
592
  return (
480
593
  <Spin spinning={loading}>
481
594
  <div
@@ -483,6 +596,7 @@ export default class RdpSession extends PureComponent {
483
596
  className='rdp-session-wrap session-v-wrap'
484
597
  >
485
598
  {this.renderControl()}
599
+ <RemoteFloatControl {...controlProps} />
486
600
  <canvas
487
601
  {...canvasProps}
488
602
  ref={this.canvasRef}
@@ -1,4 +1,10 @@
1
1
  export default [
2
+ {
3
+ id: 'res-auto',
4
+ width: 'auto',
5
+ height: 'auto',
6
+ readonly: true
7
+ },
2
8
  {
3
9
  id: 'res-1024x768',
4
10
  width: 1024,
@@ -283,7 +283,8 @@ export default class SessionWrapper extends Component {
283
283
  'delTab',
284
284
  'config',
285
285
  'reloadTab',
286
- 'editTab'
286
+ 'editTab',
287
+ 'fullscreen'
287
288
  ]),
288
289
  ...pick(
289
290
  this,
@@ -462,7 +463,7 @@ export default class SessionWrapper extends Component {
462
463
  handleFullscreen = () => {
463
464
  // Make this tab the active tab before fullscreening
464
465
  window.store.activeTabId = this.props.tab.id
465
- window.store.toggleTermFullscreen(true, this.props.tab.id)
466
+ window.store.toggleSessFullscreen(true, this.props.tab.id)
466
467
  }
467
468
 
468
469
  toggleBroadcastInput = () => {
@@ -492,7 +493,7 @@ export default class SessionWrapper extends Component {
492
493
  return (
493
494
  <Tooltip title={title} placement='bottomLeft'>
494
495
  <FullscreenOutlined
495
- className='mg1r icon-info iblock pointer spliter term-fullscreen-control1'
496
+ className='mg1r icon-info iblock pointer spliter fullscreen-control-icon'
496
497
  onClick={this.handleFullscreen}
497
498
  />
498
499
  </Tooltip>
@@ -60,14 +60,27 @@
60
60
  .web-session-wrap
61
61
  height 100vh
62
62
  background var(--main)
63
- .session-v-wrap
63
+ .session-v-info
64
+ line-height 30px
65
+ .vnc-scroll-wrapper
66
+ position relative
64
67
  background var(--main)
65
- overflow scroll
66
- z-index 99
68
+ z-index 299
69
+ &::-webkit-scrollbar
70
+ width 16px
71
+ height 16px
72
+ background var(--main-darker)
73
+ &::-webkit-scrollbar-track
74
+ background var(--main-darker)
75
+ box-shadow inset 0 0 5px var(--main-darker)
76
+ &::-webkit-scrollbar-thumb
77
+ background var(--primary)
78
+ border-radius 0
79
+ &::-webkit-scrollbar-corner
80
+ background var(--main-darker)
67
81
  .vnc-session-wrap > div
68
82
  display block !important
69
- width auto !important
70
- height auto !important
83
+ overflow hidden !important
71
84
  .not-split-view > .ant-splitter-bar .ant-splitter-bar-dragger
72
85
  display none
73
86
 
@@ -75,7 +75,8 @@ export default class Sessions extends Component {
75
75
  'appPath',
76
76
  'leftSidebarWidth',
77
77
  'pinned',
78
- 'openedSideBar'
78
+ 'openedSideBar',
79
+ 'fullscreen'
79
80
  ]),
80
81
  config,
81
82
  ...pick(this, [
@@ -184,9 +184,11 @@ class ShortcutControl extends React.PureComponent {
184
184
 
185
185
  togglefullscreenShortcut = throttle((e) => {
186
186
  e.stopPropagation()
187
- const x = document.querySelector('.term-fullscreen-control') ||
188
- document.querySelector('.session-current .term-fullscreen-control1')
189
- x && x.click()
187
+ if (window.store.fullscreen) {
188
+ window.store.toggleSessFullscreen(false)
189
+ } else {
190
+ window.store.toggleSessFullscreen(true)
191
+ }
190
192
  }, 500)
191
193
 
192
194
  zoominShortcut = throttle((e) => {
@@ -176,9 +176,11 @@ export function shortcutExtend (Cls) {
176
176
  !altKey &&
177
177
  !shiftKey &&
178
178
  ctrlKey &&
179
- this.onZmodem
179
+ this.zmodemClient &&
180
+ this.zmodemClient.isActive
180
181
  ) {
181
- this.onZmodemEnd()
182
+ this.zmodemClient.cancel()
183
+ return false
182
184
  }
183
185
 
184
186
  let codeName
@@ -107,6 +107,19 @@ export default class AttachAddonCustom extends AttachAddon {
107
107
  }
108
108
 
109
109
  onMsg = (ev) => {
110
+ // Check if it's a JSON zmodem control message
111
+ if (typeof ev.data === 'string') {
112
+ try {
113
+ const msg = JSON.parse(ev.data)
114
+ if (msg.action === 'zmodem-event') {
115
+ // Let zmodem-client handle this, don't write to terminal
116
+ return
117
+ }
118
+ } catch (e) {
119
+ // Not JSON, continue processing
120
+ }
121
+ }
122
+
110
123
  // When in alternate screen mode (like vim, less, or TUI apps like Claude Code),
111
124
  // bypass trzsz processing to avoid interference with the application's display
112
125
  if (this.term?.buffer?.active?.type === 'alternate') {