@electerm/electerm-react 2.7.9 → 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 +113 -4
  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 +397 -52
  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 = {
@@ -176,7 +179,7 @@ export default class RdpSession extends PureComponent {
176
179
  console.debug('[RDP-CLIENT] desktopSize:', width, 'x', height)
177
180
 
178
181
  const desktopSize = new window.ironRdp.DesktopSize(width, height)
179
- const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', false)
182
+ const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', true)
180
183
 
181
184
  const builder = new window.ironRdp.SessionBuilder()
182
185
  builder.username(username)
@@ -188,6 +191,29 @@ export default class RdpSession extends PureComponent {
188
191
  builder.renderCanvas(canvas)
189
192
  builder.extension(enableCredsspExt)
190
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
+
191
217
  // Cursor style callback
192
218
  builder.setCursorStyleCallbackContext(canvas)
193
219
  builder.setCursorStyleCallback(function (style) {
@@ -246,6 +272,20 @@ export default class RdpSession extends PureComponent {
246
272
  return e?.message || e?.toString() || String(e)
247
273
  }
248
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
+
249
289
  onSessionEnd = () => {
250
290
  console.debug('[RDP-CLIENT] onSessionEnd called')
251
291
  this.session = null
@@ -358,6 +398,14 @@ export default class RdpSession extends PureComponent {
358
398
  }, { passive: false })
359
399
 
360
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
+ })
361
409
  }
362
410
 
363
411
  // Get PS/2 scancode from keyboard event code using existing code-scan module
@@ -387,7 +435,7 @@ export default class RdpSession extends PureComponent {
387
435
  getAllRes = () => {
388
436
  return [
389
437
  ...this.props.resolutions,
390
- ...resolutions
438
+ ...resolutions.slice(1)
391
439
  ]
392
440
  }
393
441
 
@@ -402,7 +450,65 @@ export default class RdpSession extends PureComponent {
402
450
  return null
403
451
  }
404
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
+
405
506
  renderControl = () => {
507
+ const contrlProps = this.getControlProps({
508
+ fixedPosition: false,
509
+ showExitFullscreen: false,
510
+ className: 'mg1l'
511
+ })
406
512
  const {
407
513
  id
408
514
  } = this.state
@@ -444,6 +550,7 @@ export default class RdpSession extends PureComponent {
444
550
  </div>
445
551
  <div className='fright'>
446
552
  {this.props.fullscreenIcon()}
553
+ <RemoteFloatControl {...contrlProps} />
447
554
  </div>
448
555
  </div>
449
556
  )
@@ -481,6 +588,7 @@ export default class RdpSession extends PureComponent {
481
588
  height,
482
589
  tabIndex: 0
483
590
  }
591
+ const controlProps = this.getControlProps()
484
592
  return (
485
593
  <Spin spinning={loading}>
486
594
  <div
@@ -488,6 +596,7 @@ export default class RdpSession extends PureComponent {
488
596
  className='rdp-session-wrap session-v-wrap'
489
597
  >
490
598
  {this.renderControl()}
599
+ <RemoteFloatControl {...controlProps} />
491
600
  <canvas
492
601
  {...canvasProps}
493
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') {
@@ -0,0 +1,27 @@
1
+ export default class EventEmitter {
2
+ constructor () {
3
+ this._events = {}
4
+ }
5
+
6
+ on (event, listener) {
7
+ if (!this._events[event]) {
8
+ this._events[event] = []
9
+ }
10
+ this._events[event].push(listener)
11
+ return this
12
+ }
13
+
14
+ off (event, listener) {
15
+ if (!this._events[event]) return this
16
+ this._events[event] = this._events[event].filter(l => l !== listener)
17
+ return this
18
+ }
19
+
20
+ emit (event, ...args) {
21
+ if (!this._events[event]) return false
22
+ this._events[event].forEach(listener => {
23
+ listener(...args)
24
+ })
25
+ return true
26
+ }
27
+ }